Initial commit, basic pages and sessions.

This commit is contained in:
DebaucheryLibrarian 2023-05-29 00:54:17 +02:00
commit bc9fec207b
57 changed files with 15967 additions and 0 deletions

14
.editorconfig Executable file
View File

@ -0,0 +1,14 @@
# top-most EditorConfig file
root = true
# Unix-style newlines with a newline ending every file
[*]
end_of_line = lf
insert_final_newline = true
indent_style = tab
indent_size = 4
# Matches multiple files with brace expansion notation
# Set default charset
[*.js]
charset = utf-8

32
.eslintrc Executable file
View File

@ -0,0 +1,32 @@
{
"root": true,
"extends": ["airbnb-base", "plugin:vue/recommended"],
"parserOptions": {
"parser": "@babel/eslint-parser",
"ecmaVersion": 2019,
"sourceType": "module",
"requireConfigFile": false
},
"rules": {
"indent": ["error", "tab"],
"no-tabs": "off",
"no-unused-vars": ["error", {"argsIgnorePattern": "^_"}],
"no-console": 0,
"template-curly-spacing": "off",
"import/prefer-default-export": 0,
"max-len": 0,
"vue/no-v-html": 0,
"vue/html-indent": ["error", "tab"],
"vue/multiline-html-element-content-newline": 0,
"vue/singleline-html-element-content-newline": 0,
"vue/multi-word-component-names": 0,
"no-param-reassign": ["error", {
"props": true,
"ignorePropertyModificationsFor": ["state", "acc"]
}]
},
"globals": {
"CONFIG": "readonly",
"CLIENT_VERSION": "readonly"
}
}

6
.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
node_modules/
dist/
config/
!config/default.js
assets/js/config/
!assets/js/config/default.js

1
README.md Normal file
View File

@ -0,0 +1 @@
# shack

48
assets/css/forms.css Normal file
View File

@ -0,0 +1,48 @@
.form {
width: 100%;
max-width: 30rem;
display: flex;
flex-direction: column;
}
.form-section {
margin-bottom: 1rem;
}
.form-row {
display: flex;
flex-wrap: wrap;
margin-bottom: .5rem;
.input {
flex-grow: 1;
}
}
.form-column {
display: flex;
flex-grow: 1;
flex-direction: column;
min-width: 10rem;
}
.form-actions {
display: flex;
margin-bottom: .5rem;
justify-content: flex-end;
margin-top: .5rem;
}
.form-heading {
color: var(--primary-light-10);
margin: 0 0 .75rem 0;
}
.form-error {
background: var(--error);
color: var(--text-light);
padding: 1rem;
border-radius: .25rem;
text-align: center;
font-weight: bold;
}

44
assets/css/inputs.css Normal file
View File

@ -0,0 +1,44 @@
.input {
padding: .5rem .75rem;
font-size: 1rem;
flex-basis: 0;
border: solid 1px var(--grey-light-30);
border-radius: .25rem;
font: inherit;
&:focus {
outline: none;
border-color: var(--primary-light-30);
}
}
.button {
padding: .5rem 1rem;
border: none;
border-radius: .25rem;
background: var(--grey-light-30);
font-size: 1rem;
font-weight: bold;
&:focus {
outline: none;
}
}
.button-submit {
background: var(--primary-light-30);
color: var(--text-light);
&:hover:not(:disabled) {
background: var(--primary);
cursor: pointer;
}
&:disabled {
background: var(--shadow-weak-10);
}
}
.radio {
margin: 0 .5rem 0 0;
}

21
assets/css/markdown.css Normal file
View File

@ -0,0 +1,21 @@
.markdown-body {
margin: 0 auto;
max-width: 50rem;
flex-grow: 1;
padding: 1rem;
line-height: 1.5;
text-align: justify;
& h1 {
margin: 0;
}
& h2 {
color: var(--primary);
margin: 1rem 0 0 0;
}
& p {
margin: 0;
}
}

36
assets/css/states.css Executable file
View File

@ -0,0 +1,36 @@
.noselect {
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
-webkit-tap-highlight-color: transparent;
}
.nolist {
list-style: none;
padding: 0;
margin: 0;
li {
display: inline-block;
padding: 0;
margin: 0;
}
}
.nolink {
display: inline-block;
color: inherit;
text-decoration: none;
}
.nobar {
scrollbar-width: none;
-mis-overflow-style: none;
&::-webkit-scrollbar {
background: transparent;
width: 0px;
height: 0px;
}
}

31
assets/css/style.css Normal file
View File

@ -0,0 +1,31 @@
@import 'theme';
@import 'states';
@import 'inputs';
@import 'forms';
@import 'markdown';
html,
body,
#app {
height: 100%;
}
body {
margin: 0;
color: var(--text);
font-family: sans-serif;
}
.link {
color: var(--link);
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
.heading {
margin: 0 0 1rem 0;
color: var(--primary-light-20);
}

34
assets/css/theme.css Normal file
View File

@ -0,0 +1,34 @@
:root {
--primary: hsl(300, 100%, 30%);
--primary-light-10: hsl(300, 50%, 40%);
--primary-light-20: hsl(300, 50%, 50%);
--primary-light-30: hsl(300, 50%, 60%);
--grey-dark-40: #222;
--grey-dark-30: #444;
--grey-dark-20: #666;
--grey-dark-10: #888;
--grey: #aaa;
--grey-light-10: #bbb;
--grey-light-20: #ccc;
--grey-light-30: #ddd;
--grey-light-40: #eee;
--background-dark-20: #eee;
--background-dark-10: #f8f8f8;
--background: #fff;
--shadow-weak-30: rgba(0, 0, 0, .1);
--shadow-weak-20: rgba(0, 0, 0, .2);
--shadow-weak-10: rgba(0, 0, 0, .35);
--shadow: rgba(0, 0, 0, .5);
--shadow-strong-10: rgba(0, 0, 0, .6);
--shadow-strong-20: rgba(0, 0, 0, .75);
--shadow-strong-30: rgba(0, 0, 0, .9);
--text: #222;
--text-light: #fff;
--link: #48f;
--error: #f66;
}

71
assets/img/favicon.svg Normal file
View File

@ -0,0 +1,71 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="22.806mm"
height="22.806mm"
viewBox="0 0 22.806 22.805999"
version="1.1"
id="svg5"
sodipodi:docname="favicon.svg"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview7"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
showgrid="false"
inkscape:zoom="5.4247457"
inkscape:cx="35.024683"
inkscape:cy="42.674811"
inkscape:window-width="1920"
inkscape:window-height="1020"
inkscape:window-x="0"
inkscape:window-y="32"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" />
<defs
id="defs2">
<linearGradient
id="linearGradient2720"
inkscape:swatch="solid">
<stop
style="stop-color:#000000;stop-opacity:1;"
offset="0"
id="stop2718" />
</linearGradient>
</defs>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-81.250065,-117.45377)">
<g
aria-label="shack"
id="text236"
style="font-size:35.2777px;font-family:LOVES;-inkscape-font-specification:LOVES;stroke-width:0.264583"
transform="translate(1.8428824)">
<g
id="g2730"
style="stroke-width:0.265;stroke-dasharray:none">
<path
d="m 95.564404,131.63031 -4.478614,-11.07596 -4.788672,11.07596 z m 4.805896,8.62994 h -1.309132 l -3.014451,-7.44139 H 85.780355 l -3.221157,7.44139 h -1.309133 l 9.870176,-22.80648 z"
id="path2625"
style="fill:#880088;fill-opacity:1;stroke:none;stroke-width:0.265;stroke-dasharray:none;stroke-opacity:1" />
</g>
</g>
<path
style="color:#000000;fill:#880088;fill-opacity:1;-inkscape-stroke:none"
d="m 90.289784,134.44531 v 5.81446 h 1 v -4.81446 h 2.726562 v 4.81446 h 1 v -5.81446 z"
id="path2716" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

83
assets/img/logo.svg Normal file
View File

@ -0,0 +1,83 @@
<svg
width="106.43599mm"
height="23.46104mm"
viewBox="0 0 106.43599 23.46104"
version="1.1"
id="svg5"
sodipodi:docname="logo.svg"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview7"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
showgrid="false"
inkscape:zoom="8.2527944"
inkscape:cx="181.87779"
inkscape:cy="65.977652"
inkscape:window-width="1920"
inkscape:window-height="1020"
inkscape:window-x="0"
inkscape:window-y="32"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" />
<defs
id="defs2">
<linearGradient
id="linearGradient2720"
inkscape:swatch="solid">
<stop
style="stop-color:#000000;stop-opacity:1;"
offset="0"
id="stop2718" />
</linearGradient>
</defs>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-40.339649,-117.14372)">
<g
aria-label="shack"
id="text236"
style="font-size:35.2777px;font-family:LOVES;-inkscape-font-specification:LOVES;stroke-width:0.264583">
<g
id="g2730"
style="stroke-width:0.265;stroke-dasharray:none">
<path
d="m 47.488206,140.38083 q -2.170405,0 -4.168556,-0.99907 -1.998151,-1.0163 -2.980001,-3.13503 l 1.102428,-0.46509 q 1.016301,2.15318 3.203932,2.92832 1.412486,0.49954 2.842197,0.49954 2.652718,0 4.530291,-1.48139 1.309133,-1.03352 1.378035,-2.87664 -0.05168,-2.20486 -3.152256,-4.01353 -1.291907,-0.74069 -2.56659,-1.34359 -3.858498,-1.80867 -5.787747,-4.09965 -1.102428,-1.30913 -1.102428,-2.66994 0,-1.87758 1.274682,-3.29006 1.291908,-1.41249 3.634568,-1.94648 0.98185,-0.22393 1.980925,-0.22393 0.740694,0 1.980926,0.20671 1.257457,0.18948 2.721619,1.17133 1.481388,0.98185 2.273758,2.73884 l -1.102428,0.44786 q -1.033527,-2.1704 -3.203932,-2.92832 -1.343584,-0.46509 -2.687168,-0.46509 -2.687169,0 -4.478614,1.53307 -1.188556,1.0163 -1.188556,2.82497 0.120578,1.24023 1.257457,2.37711 1.136879,1.11965 2.53214,1.98093 1.39526,0.84404 2.428787,1.2919 1.360809,0.63734 2.790521,1.49862 1.446937,0.86127 2.497688,2.06705 1.050752,1.20578 1.102428,2.8422 -0.03445,1.99815 -1.446936,3.42786 -1.412486,1.42971 -3.841273,1.92925 -0.895723,0.17225 -1.825897,0.17225 z"
id="path2621"
style="stroke-width:0.265;stroke-dasharray:none" />
<path
d="m 76.719776,140.26025 h -1.205781 v -10.7659 H 60.510638 v 10.7659 h -1.188555 v -22.77203 h 1.188555 v 10.80035 h 15.003357 v -10.80035 h 1.205781 z"
id="path2623"
style="stroke-width:0.265;stroke-dasharray:none" />
<path
d="m 95.564404,131.63031 -4.478614,-11.07596 -4.788672,11.07596 z m 4.805896,8.62994 h -1.309132 l -3.014451,-7.44139 H 85.780355 l -3.221157,7.44139 h -1.309133 l 9.870176,-22.80648 z"
id="path2625"
style="stroke-width:0.265;stroke-dasharray:none" />
<path
d="m 114.54684,140.58754 q -3.23838,0 -5.90833,-1.58474 -2.66994,-1.58474 -4.25468,-4.23746 -1.58474,-2.66994 -1.58474,-5.8911 0,-3.23838 1.58474,-5.8911 1.58474,-2.66994 4.25468,-4.25468 2.66995,-1.58474 5.90833,-1.58474 3.41064,0 6.16671,1.73976 2.77329,1.73977 4.28913,4.61642 h -1.42971 q -1.41249,-2.34266 -3.7896,-3.73792 -2.35988,-1.41248 -5.23653,-1.41248 -2.89387,0 -5.27099,1.42971 -2.37711,1.41248 -3.80682,3.80682 -1.41248,2.37711 -1.41248,5.28821 0,2.89387 1.41248,5.28821 1.42971,2.37711 3.80682,3.80682 2.37712,1.41249 5.27099,1.41249 2.66994,0 4.90925,-1.22301 2.25653,-1.22301 3.70347,-3.30728 h 1.48139 q -1.56752,2.61826 -4.22024,4.18578 -2.65271,1.55029 -5.87387,1.55029 z"
id="path2627"
style="stroke-width:0.265;stroke-dasharray:none" />
<path
d="m 130.58373,140.60476 v -23.11654 h 1.20578 v 19.03411 l 12.21284,-19.03411 h 1.41248 l -7.83757,12.21284 9.19838,10.55919 h -1.58474 l -8.26821,-9.52566 z"
id="path2629"
style="stroke-width:0.265;stroke-dasharray:none" />
</g>
</g>
<path
style="color:#000000;-inkscape-stroke:none"
d="m 88.447266,134.44531 v 5.81446 h 1 v -4.81446 h 2.726562 v 4.81446 h 1 v -5.81446 z"
id="path2716" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.6 KiB

BIN
assets/img/logo_pixels.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

86
assets/js/api.js Normal file
View File

@ -0,0 +1,86 @@
const postHeaders = {
mode: 'cors',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
},
};
function getQuery(data) {
if (!data) {
return '';
}
return `?${new URLSearchParams(data).toString()}`;
}
export async function get(path, query = {}) {
const res = await fetch(`${path}${getQuery(query)}`);
const body = await res.json();
if (res.ok) {
return body;
}
throw new Error(body.message);
}
export async function post(path, data, { query } = {}) {
const res = await fetch(`${path}${getQuery(query)}`, {
method: 'POST',
body: JSON.stringify(data),
...postHeaders,
});
if (res.status === 204) {
return null;
}
const body = await res.json();
if (res.ok) {
return body;
}
throw new Error(body.message);
}
export async function patch(path, data, { query } = {}) {
const res = await fetch(`${path}${getQuery(query)}`, {
method: 'PATCH',
body: JSON.stringify(data),
...postHeaders,
});
if (res.status === 204) {
return null;
}
const body = await res.json();
if (res.ok) {
return body;
}
throw new Error(body.message);
}
export async function del(path, { data, query } = {}) {
const res = await fetch(`${path}${getQuery(query)}`, {
method: 'DELETE',
body: JSON.stringify(data),
...postHeaders,
});
if (res.status === 204) {
return null;
}
const body = await res.json();
if (res.ok) {
return body;
}
throw new Error(body.message);
}

4
assets/js/navigate.js Normal file
View File

@ -0,0 +1,4 @@
// centralize navigation to simplify switching between client and server routing
export default function navigate(path) {
window.location.href = path;
}

110
components/form/checkbox.vue Executable file
View File

@ -0,0 +1,110 @@
<template>
<label class="check-container noselect">
<span
v-if="label"
class="check-label"
>{{ label }}</span>
<input
v-show="false"
:id="`checkbox-${uid}`"
:checked="checked"
type="checkbox"
class="check-checkbox"
@change="$emit('change', $event.target.checked)"
>
<label
:for="`checkbox-${uid}`"
class="check"
/>
</label>
</template>
<script>
import { nanoid } from 'nanoid';
export default {
props: {
checked: {
type: Boolean,
default: false,
},
label: {
type: String,
default: null,
},
},
emits: ['change'],
data() {
return {
uid: nanoid(),
};
},
};
</script>
<style>
.check-container {
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer;
padding: .25rem 0;
}
.check {
width: 1.25rem;
height: 1.25rem;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
position: relative;
border-radius: .25rem;
background-color: var(--shadow-weak-20);
cursor: pointer;
transition: background .15s ease;
&::after {
content: '';
width: .5rem;
height: .3rem;
border: solid 2px var(--text-light);
border-top: none;
border-right: none;
margin: -.2rem 0 0 0;
transform: rotateZ(-45deg) scaleX(0);
transition: transform .15s ease;
}
}
.check-cross .check::before {
content: '';
width: 100%;
height: 100%;
position: absolute;
background: url('/img/icons/cross3.svg') no-repeat center/80%;
opacity: .15;
transition: transform .1s ease;
}
.check-checkbox:checked + .check {
background: var(--primary);
&::after {
transform: rotateZ(-45deg) scaleX(1);
}
&::before {
transform: scaleX(0);
}
}
.check-label {
overflow: hidden;
text-transform: capitalize;
text-overflow: ellipsis;
margin: 0 .5rem 0 0;
}
</style>

6
knexfile.js Executable file
View File

@ -0,0 +1,6 @@
const config = require('config');
module.exports = {
client: 'pg',
connection: config.database,
};

15
log/2023-05-26.log Normal file
View File

@ -0,0 +1,15 @@
2023-05-26 22:00:04 info [/home/niels/Projects/shack/src/web/server.js] Server running at :::7477
2023-05-26 22:01:41 info [server] Server running at :::7477
2023-05-26 22:02:51 info [server] Server running at :::7477
2023-05-26 22:13:32 info [server] Server running at :::7477
2023-05-26 22:14:07 info [server] Server running at :::7477
2023-05-26 22:14:56 info [server] Server running at :::7477
2023-05-26 22:15:07 info [server] Server running at :::7477
2023-05-26 22:15:33 info [server] Server running at :::7477
2023-05-26 22:16:20 info [server] Server running at :::7477
2023-05-26 22:16:33 info [server] Server running at :::7477
2023-05-26 22:23:24 info [server] Server running at :::7477
2023-05-26 23:50:58 info [server] Server running at :::7477
2023-05-26 23:52:16 info [server] Server running at :::7477
2023-05-26 23:59:17 info [server] Server running at :::7477
2023-05-26 23:59:40 info [server] Server running at :::7477

42
log/2023-05-27.log Normal file
View File

@ -0,0 +1,42 @@
2023-05-27 00:02:12 info [server] Server running at :::7477
2023-05-27 00:02:53 info [server] Server running at :::7477
2023-05-27 00:03:46 info [server] Server running at :::7477
2023-05-27 16:31:42 info [server] Server running at :::7477
2023-05-27 17:15:42 info [server] Server running at :::7477
2023-05-27 17:24:43 info [server] Server running at :::7477
2023-05-27 17:28:55 info [server] Server running at :::7477
2023-05-27 17:30:14 info [server] Server running at :::7477
2023-05-27 17:30:48 info [server] Server running at :::7477
2023-05-27 17:31:08 info [server] Server running at :::7477
2023-05-27 17:34:32 info [server] Server running at :::7477
2023-05-27 17:53:55 info [server] Server running at :::7477
2023-05-27 22:40:18 info [server] Server running at :::7477
2023-05-27 22:48:27 info [server] Server running at :::7477
2023-05-27 23:23:00 info [server] Server running at :::7477
2023-05-27 23:29:39 info [server] Server running at :::7477
2023-05-27 23:36:51 info [server] Server running at :::7477
2023-05-27 23:41:25 info [server] Server running at :::7477
2023-05-27 23:45:38 info [server] Server running at :::7477
2023-05-27 23:46:09 info [users] Registered user admin (1, undefined, ::ffff:127.0.0.1)
2023-05-27 23:47:00 info [server] Server running at :::7477
2023-05-27 23:47:51 info [server] Server running at :::7477
2023-05-27 23:54:47 info [server] Server running at :::7477
2023-05-27 23:55:07 info [server] Server running at :::7477
2023-05-27 23:55:23 info [server] Server running at :::7477
2023-05-27 23:55:31 warn [error] Failed to fulfill request to /api/users: Username or e-mail already registered
2023-05-27 23:55:31 error [error] HttpError: Username or e-mail already registered
at createUser (/home/niels/Projects/shack/src/users.js:129:10)
at processTicksAndRejections (node:internal/process/task_queues:96:5)
at async createUserApi (/home/niels/Projects/shack/src/web/users.js:4:15)
2023-05-27 23:56:25 info [server] Server running at :::7477
2023-05-27 23:57:11 info [server] Server running at :::7477
2023-05-27 23:57:17 warn [error] Failed to fulfill request to /api/users: Username or e-mail already registered
2023-05-27 23:57:17 error [error] HttpError: Username or e-mail already registered
at createUser (/home/niels/Projects/shack/src/users.js:129:10)
at processTicksAndRejections (node:internal/process/task_queues:96:5)
at async createUserApi (/home/niels/Projects/shack/src/web/users.js:4:15)
2023-05-27 23:58:08 warn [error] Failed to fulfill request to /api/users: Username or e-mail already registered
2023-05-27 23:58:08 error [error] HttpError: Username or e-mail already registered
at createUser (/home/niels/Projects/shack/src/users.js:129:10)
at processTicksAndRejections (node:internal/process/task_queues:96:5)
at async createUserApi (/home/niels/Projects/shack/src/web/users.js:4:15)

269
log/2023-05-28.log Normal file
View File

@ -0,0 +1,269 @@
2023-05-28 00:08:00 info [server] Server running at :::7477
2023-05-28 00:11:09 info [server] Server running at :::7477
2023-05-28 00:14:04 info [server] Server running at :::7477
2023-05-28 00:21:15 info [server] Server running at :::7477
2023-05-28 00:22:47 info [server] Server running at :::7477
2023-05-28 00:36:00 info [server] Server running at :::7477
2023-05-28 00:36:38 info [server] Server running at :::7477
2023-05-28 00:39:32 info [server] Server running at :::7477
2023-05-28 00:41:42 info [server] Server running at :::7477
2023-05-28 00:44:41 info [server] Server running at :::7477
2023-05-28 01:06:18 info [server] Server running at :::7477
2023-05-28 01:06:43 info [server] Server running at :::7477
2023-05-28 01:09:47 info [server] Server running at :::7477
2023-05-28 01:10:11 info [server] Server running at :::7477
2023-05-28 01:11:03 info [server] Server running at :::7477
2023-05-28 01:11:33 info [server] Server running at :::7477
2023-05-28 01:11:46 info [server] Server running at :::7477
2023-05-28 01:12:07 info [server] Server running at :::7477
2023-05-28 01:12:37 info [server] Server running at :::7477
2023-05-28 01:15:28 info [server] Server running at :::7477
2023-05-28 01:16:12 info [server] Server running at :::7477
2023-05-28 01:17:01 info [server] Server running at :::7477
2023-05-28 01:17:28 info [server] Server running at :::7477
2023-05-28 01:17:38 info [server] Server running at :::7477
2023-05-28 01:20:33 info [server] Server running at :::7477
2023-05-28 01:22:29 info [server] Server running at :::7477
2023-05-28 01:23:20 info [server] Server running at :::7477
2023-05-28 01:23:38 info [server] Server running at :::7477
2023-05-28 01:24:21 info [server] Server running at :::7477
2023-05-28 01:24:39 info [server] Server running at :::7477
2023-05-28 01:26:25 info [server] Server running at :::7477
2023-05-28 01:28:39 info [server] Server running at :::7477
2023-05-28 01:29:11 info [server] Server running at :::7477
2023-05-28 01:30:10 info [server] Server running at :::7477
2023-05-28 01:31:36 info [server] Server running at :::7477
2023-05-28 01:31:46 info [server] Server running at :::7477
2023-05-28 01:31:47 warn [error] Failed to fulfill request to /: secret option required for sessions
2023-05-28 01:31:47 warn [error] Failed to fulfill request to /favicon.ico: secret option required for sessions
2023-05-28 01:32:32 info [server] Server running at :::7477
2023-05-28 01:32:34 warn [error] Failed to fulfill request to /: secret option required for sessions
2023-05-28 01:32:34 warn [error] Failed to fulfill request to /favicon.ico: secret option required for sessions
2023-05-28 01:32:45 info [server] Server running at :::7477
2023-05-28 01:32:56 info [server] Server running at :::7477
2023-05-28 01:34:50 info [server] Server running at :::7477
2023-05-28 01:36:08 info [server] Server running at :::7477
2023-05-28 01:36:25 info [server] Server running at :::7477
2023-05-28 02:09:21 info [server] Server running at :::7477
2023-05-28 02:11:16 info [server] Server running at :::7477
2023-05-28 02:11:58 info [server] Server running at :::7477
2023-05-28 02:13:02 info [server] Server running at :::7477
2023-05-28 02:14:03 info [server] Server running at :::7477
2023-05-28 02:18:24 info [redis] Redis module initialized
2023-05-28 02:18:25 info [server] Server running at :::7477
2023-05-28 02:19:39 info [redis] Redis module initialized
2023-05-28 02:19:39 info [server] Server running at :::7477
2023-05-28 02:20:44 info [redis] Redis module initialized
2023-05-28 02:20:44 info [server] Server running at :::7477
2023-05-28 16:06:50 info [redis] Redis module initialized
2023-05-28 16:06:50 info [server] Server running at :::7477
2023-05-28 17:45:15 info [redis] Redis module initialized
2023-05-28 17:45:16 info [server] Server running at :::7477
2023-05-28 21:22:50 info [redis] Redis module initialized
2023-05-28 21:22:51 info [server] Server running at :::7477
2023-05-28 21:25:31 info [redis] Redis module initialized
2023-05-28 21:25:31 info [server] Server running at :::7477
2023-05-28 21:25:46 info [redis] Redis module initialized
2023-05-28 21:25:46 info [server] Server running at :::7477
2023-05-28 21:25:56 warn [error] Failed to fulfill request to /api/session: Username or password incorrect
2023-05-28 21:25:56 error [error] HttpError: Username or password incorrect
at verifyPassword (/home/niels/Projects/shack/src/users.js:31:9)
at async login (/home/niels/Projects/shack/src/users.js:86:2)
at async loginApi (/home/niels/Projects/shack/src/web/users.js:7:15)
2023-05-28 21:26:02 warn [error] Failed to fulfill request to /api/session: The client is closed
2023-05-28 21:26:02 error [error] Error: The client is closed
at Commander._RedisClient_sendCommand (/home/niels/Projects/shack/node_modules/@redis/client/dist/lib/client/index.js:490:31)
at Commander.commandsExecutor (/home/niels/Projects/shack/node_modules/@redis/client/dist/lib/client/index.js:188:154)
at Commander.BaseClass.<computed> [as set] (/home/niels/Projects/shack/node_modules/@redis/client/dist/lib/commander.js:8:29)
at Object.set (/home/niels/Projects/shack/node_modules/connect-redis/dist/cjs/index.js:24:34)
at RedisStore.set (/home/niels/Projects/shack/node_modules/connect-redis/dist/cjs/index.js:71:39)
at Session.save (/home/niels/Projects/shack/node_modules/express-session/session/session.js:72:25)
at Session.save (/home/niels/Projects/shack/node_modules/express-session/index.js:406:15)
at ServerResponse.end (/home/niels/Projects/shack/node_modules/express-session/index.js:335:21)
at ServerResponse.send (/home/niels/Projects/shack/node_modules/express/lib/response.js:232:10)
at ServerResponse.response.send (/home/niels/Projects/shack/node_modules/errors/lib/errors.js:753:22)
at ServerResponse.json (/home/niels/Projects/shack/node_modules/express/lib/response.js:278:15)
at ServerResponse.send (/home/niels/Projects/shack/node_modules/express/lib/response.js:162:21)
at ServerResponse.response.send (/home/niels/Projects/shack/node_modules/errors/lib/errors.js:753:22)
at loginApi (/home/niels/Projects/shack/src/web/users.js:14:6)
2023-05-28 21:26:02 warn [error] Failed to fulfill request to /: The client is closed
2023-05-28 21:26:02 error [error] Error: The client is closed
at Commander._RedisClient_sendCommand (/home/niels/Projects/shack/node_modules/@redis/client/dist/lib/client/index.js:490:31)
at Commander.commandsExecutor (/home/niels/Projects/shack/node_modules/@redis/client/dist/lib/client/index.js:188:154)
at Commander.BaseClass.<computed> [as get] (/home/niels/Projects/shack/node_modules/@redis/client/dist/lib/commander.js:8:29)
at Object.get (/home/niels/Projects/shack/node_modules/connect-redis/dist/cjs/index.js:20:34)
at RedisStore.get (/home/niels/Projects/shack/node_modules/connect-redis/dist/cjs/index.js:53:42)
at session (/home/niels/Projects/shack/node_modules/express-session/index.js:485:11)
at handleReturn (/home/niels/Projects/shack/node_modules/express-promise-router/lib/express-promise-router.js:24:23)
at session (/home/niels/Projects/shack/node_modules/express-promise-router/lib/express-promise-router.js:64:7)
at Layer.handle [as handle_request] (/home/niels/Projects/shack/node_modules/express/lib/router/layer.js:95:5)
at trim_prefix (/home/niels/Projects/shack/node_modules/express/lib/router/index.js:328:13)
at /home/niels/Projects/shack/node_modules/express/lib/router/index.js:286:9
at Function.process_params (/home/niels/Projects/shack/node_modules/express/lib/router/index.js:346:12)
at next (/home/niels/Projects/shack/node_modules/express/lib/router/index.js:280:10)
at Function.handle (/home/niels/Projects/shack/node_modules/express/lib/router/index.js:175:3)
at router (/home/niels/Projects/shack/node_modules/express/lib/router/index.js:47:12)
at Layer.handle [as handle_request] (/home/niels/Projects/shack/node_modules/express/lib/router/layer.js:95:5)
at trim_prefix (/home/niels/Projects/shack/node_modules/express/lib/router/index.js:328:13)
at /home/niels/Projects/shack/node_modules/express/lib/router/index.js:286:9
at Function.process_params (/home/niels/Projects/shack/node_modules/express/lib/router/index.js:346:12)
at Immediate.next (/home/niels/Projects/shack/node_modules/express/lib/router/index.js:280:10)
at processImmediate (node:internal/timers:468:21)
2023-05-28 21:26:30 info [redis] Redis module initialized
2023-05-28 21:26:30 info [server] Server running at :::7477
2023-05-28 21:26:37 warn [error] Failed to fulfill request to /api/session: Username or password incorrect
2023-05-28 21:26:37 error [error] HttpError: Username or password incorrect
at verifyPassword (/home/niels/Projects/shack/src/users.js:31:9)
at async login (/home/niels/Projects/shack/src/users.js:86:2)
at async loginApi (/home/niels/Projects/shack/src/web/users.js:7:15)
2023-05-28 21:51:08 info [redis] Redis module initialized
2023-05-28 21:51:25 info [redis] Redis module initialized
2023-05-28 21:51:25 info [server] Server running at 0.0.0.0:7477
2023-05-28 21:55:59 info [redis] Redis module initialized
2023-05-28 21:55:59 info [server] Server running at 0.0.0.0:7477
2023-05-28 21:56:12 info [redis] Redis module initialized
2023-05-28 21:56:12 info [server] Server running at 0.0.0.0:7477
2023-05-28 22:01:38 info [redis] Redis module initialized
2023-05-28 22:01:39 info [server] Server running at 0.0.0.0:7477
2023-05-28 22:05:13 info [redis] Redis module initialized
2023-05-28 22:05:13 info [server] Server running at 0.0.0.0:7477
2023-05-28 22:06:29 info [redis] Redis module initialized
2023-05-28 22:06:30 info [server] Server running at 0.0.0.0:7477
2023-05-28 22:09:37 info [redis] Redis module initialized
2023-05-28 22:09:37 info [server] Server running at 0.0.0.0:7477
2023-05-28 22:10:01 info [redis] Redis module initialized
2023-05-28 22:10:01 info [server] Server running at 0.0.0.0:7477
2023-05-28 23:17:39 info [redis] Redis module initialized
2023-05-28 23:17:39 info [server] Server running at 0.0.0.0:7477
2023-05-28 23:19:24 info [redis] Redis module initialized
2023-05-28 23:19:25 info [server] Server running at 0.0.0.0:7477
2023-05-28 23:20:09 info [redis] Redis module initialized
2023-05-28 23:20:09 info [server] Server running at 0.0.0.0:7477
2023-05-28 23:37:14 info [redis] Redis module initialized
2023-05-28 23:37:14 info [server] Server running at 0.0.0.0:7477
2023-05-28 23:42:22 info [redis] Redis module initialized
2023-05-28 23:42:22 info [server] Server running at 0.0.0.0:7477
2023-05-28 23:49:02 info [redis] Redis module initialized
2023-05-28 23:49:02 info [server] Server running at 0.0.0.0:7477
2023-05-28 23:49:03 warn [error] Failed to fulfill request to /: [vite-plugin-ssr@0.4.126][Wrong Usage][renderPage(pageContextInit)] You passed 2 arguments but `renderPage()` accepts only one argument.'
2023-05-28 23:49:03 error [error] Error: [vite-plugin-ssr@0.4.126][Wrong Usage][renderPage(pageContextInit)] You passed 2 arguments but `renderPage()` accepts only one argument.'
at assertArguments (/home/niels/Projects/shack/node_modules/vite-plugin-ssr/dist/cjs/node/runtime/renderPage/assertArguments.js:10:29)
at renderPage (/home/niels/Projects/shack/node_modules/vite-plugin-ssr/dist/cjs/node/runtime/renderPage.js:19:43)
at defaultHandler (/home/niels/Projects/shack/src/web/default.js:10:29)
at handleReturn (/home/niels/Projects/shack/node_modules/express-promise-router/lib/express-promise-router.js:24:23)
at defaultHandler (/home/niels/Projects/shack/node_modules/express-promise-router/lib/express-promise-router.js:64:7)
at handleReturn (/home/niels/Projects/shack/node_modules/express-promise-router/lib/express-promise-router.js:24:23)
at defaultHandler (/home/niels/Projects/shack/node_modules/express-promise-router/lib/express-promise-router.js:64:7)
at Layer.handle [as handle_request] (/home/niels/Projects/shack/node_modules/express/lib/router/layer.js:95:5)
at next (/home/niels/Projects/shack/node_modules/express/lib/router/route.js:144:13)
at Route.dispatch (/home/niels/Projects/shack/node_modules/express/lib/router/route.js:114:3)
at Layer.handle [as handle_request] (/home/niels/Projects/shack/node_modules/express/lib/router/layer.js:95:5)
at /home/niels/Projects/shack/node_modules/express/lib/router/index.js:284:15
at param (/home/niels/Projects/shack/node_modules/express/lib/router/index.js:365:14)
at param (/home/niels/Projects/shack/node_modules/express/lib/router/index.js:376:14)
at Function.process_params (/home/niels/Projects/shack/node_modules/express/lib/router/index.js:421:3)
at next (/home/niels/Projects/shack/node_modules/express/lib/router/index.js:280:10)
at jsonParser (/home/niels/Projects/shack/node_modules/body-parser/lib/types/json.js:113:7)
at handleReturn (/home/niels/Projects/shack/node_modules/express-promise-router/lib/express-promise-router.js:24:23)
at jsonParser (/home/niels/Projects/shack/node_modules/express-promise-router/lib/express-promise-router.js:64:7)
at Layer.handle [as handle_request] (/home/niels/Projects/shack/node_modules/express/lib/router/layer.js:95:5)
at trim_prefix (/home/niels/Projects/shack/node_modules/express/lib/router/index.js:328:13)
at /home/niels/Projects/shack/node_modules/express/lib/router/index.js:286:9
at Function.process_params (/home/niels/Projects/shack/node_modules/express/lib/router/index.js:346:12)
at next (/home/niels/Projects/shack/node_modules/express/lib/router/index.js:280:10)
at setIp (/home/niels/Projects/shack/src/web/ip.js:15:2)
at handleReturn (/home/niels/Projects/shack/node_modules/express-promise-router/lib/express-promise-router.js:24:23)
at setIp (/home/niels/Projects/shack/node_modules/express-promise-router/lib/express-promise-router.js:64:7)
at Layer.handle [as handle_request] (/home/niels/Projects/shack/node_modules/express/lib/router/layer.js:95:5)
at trim_prefix (/home/niels/Projects/shack/node_modules/express/lib/router/index.js:328:13)
at /home/niels/Projects/shack/node_modules/express/lib/router/index.js:286:9
at Function.process_params (/home/niels/Projects/shack/node_modules/express/lib/router/index.js:346:12)
at next (/home/niels/Projects/shack/node_modules/express/lib/router/index.js:280:10)
at /home/niels/Projects/shack/node_modules/express-session/index.js:506:7
at RedisStore.get (/home/niels/Projects/shack/node_modules/connect-redis/dist/cjs/index.js:56:20)
2023-05-28 23:49:03 warn [error] Failed to fulfill request to /: [vite-plugin-ssr@0.4.126][Wrong Usage][renderPage(pageContextInit)] You passed 2 arguments but `renderPage()` accepts only one argument.'
2023-05-28 23:49:03 error [error] Error: [vite-plugin-ssr@0.4.126][Wrong Usage][renderPage(pageContextInit)] You passed 2 arguments but `renderPage()` accepts only one argument.'
at assertArguments (/home/niels/Projects/shack/node_modules/vite-plugin-ssr/dist/cjs/node/runtime/renderPage/assertArguments.js:10:29)
at renderPage (/home/niels/Projects/shack/node_modules/vite-plugin-ssr/dist/cjs/node/runtime/renderPage.js:19:43)
at defaultHandler (/home/niels/Projects/shack/src/web/default.js:10:29)
at handleReturn (/home/niels/Projects/shack/node_modules/express-promise-router/lib/express-promise-router.js:24:23)
at defaultHandler (/home/niels/Projects/shack/node_modules/express-promise-router/lib/express-promise-router.js:64:7)
at handleReturn (/home/niels/Projects/shack/node_modules/express-promise-router/lib/express-promise-router.js:24:23)
at defaultHandler (/home/niels/Projects/shack/node_modules/express-promise-router/lib/express-promise-router.js:64:7)
at Layer.handle [as handle_request] (/home/niels/Projects/shack/node_modules/express/lib/router/layer.js:95:5)
at next (/home/niels/Projects/shack/node_modules/express/lib/router/route.js:144:13)
at Route.dispatch (/home/niels/Projects/shack/node_modules/express/lib/router/route.js:114:3)
at Layer.handle [as handle_request] (/home/niels/Projects/shack/node_modules/express/lib/router/layer.js:95:5)
at /home/niels/Projects/shack/node_modules/express/lib/router/index.js:284:15
at param (/home/niels/Projects/shack/node_modules/express/lib/router/index.js:365:14)
at param (/home/niels/Projects/shack/node_modules/express/lib/router/index.js:376:14)
at Function.process_params (/home/niels/Projects/shack/node_modules/express/lib/router/index.js:421:3)
at next (/home/niels/Projects/shack/node_modules/express/lib/router/index.js:280:10)
at jsonParser (/home/niels/Projects/shack/node_modules/body-parser/lib/types/json.js:113:7)
at handleReturn (/home/niels/Projects/shack/node_modules/express-promise-router/lib/express-promise-router.js:24:23)
at jsonParser (/home/niels/Projects/shack/node_modules/express-promise-router/lib/express-promise-router.js:64:7)
at Layer.handle [as handle_request] (/home/niels/Projects/shack/node_modules/express/lib/router/layer.js:95:5)
at trim_prefix (/home/niels/Projects/shack/node_modules/express/lib/router/index.js:328:13)
at /home/niels/Projects/shack/node_modules/express/lib/router/index.js:286:9
at Function.process_params (/home/niels/Projects/shack/node_modules/express/lib/router/index.js:346:12)
at next (/home/niels/Projects/shack/node_modules/express/lib/router/index.js:280:10)
at setIp (/home/niels/Projects/shack/src/web/ip.js:15:2)
at handleReturn (/home/niels/Projects/shack/node_modules/express-promise-router/lib/express-promise-router.js:24:23)
at setIp (/home/niels/Projects/shack/node_modules/express-promise-router/lib/express-promise-router.js:64:7)
at Layer.handle [as handle_request] (/home/niels/Projects/shack/node_modules/express/lib/router/layer.js:95:5)
at trim_prefix (/home/niels/Projects/shack/node_modules/express/lib/router/index.js:328:13)
at /home/niels/Projects/shack/node_modules/express/lib/router/index.js:286:9
at Function.process_params (/home/niels/Projects/shack/node_modules/express/lib/router/index.js:346:12)
at next (/home/niels/Projects/shack/node_modules/express/lib/router/index.js:280:10)
at /home/niels/Projects/shack/node_modules/express-session/index.js:506:7
at RedisStore.get (/home/niels/Projects/shack/node_modules/connect-redis/dist/cjs/index.js:56:20)
2023-05-28 23:49:20 info [redis] Redis module initialized
2023-05-28 23:49:20 info [server] Server running at 0.0.0.0:7477
2023-05-28 23:49:21 warn [error] Failed to fulfill request to /: [vite-plugin-ssr@0.4.126][Wrong Usage][renderPage(pageContextInit)] You passed 2 arguments but `renderPage()` accepts only one argument.'
2023-05-28 23:49:21 error [error] Error: [vite-plugin-ssr@0.4.126][Wrong Usage][renderPage(pageContextInit)] You passed 2 arguments but `renderPage()` accepts only one argument.'
at assertArguments (/home/niels/Projects/shack/node_modules/vite-plugin-ssr/dist/cjs/node/runtime/renderPage/assertArguments.js:10:29)
at renderPage (/home/niels/Projects/shack/node_modules/vite-plugin-ssr/dist/cjs/node/runtime/renderPage.js:19:43)
at defaultHandler (/home/niels/Projects/shack/src/web/default.js:10:29)
at handleReturn (/home/niels/Projects/shack/node_modules/express-promise-router/lib/express-promise-router.js:24:23)
at defaultHandler (/home/niels/Projects/shack/node_modules/express-promise-router/lib/express-promise-router.js:64:7)
at handleReturn (/home/niels/Projects/shack/node_modules/express-promise-router/lib/express-promise-router.js:24:23)
at defaultHandler (/home/niels/Projects/shack/node_modules/express-promise-router/lib/express-promise-router.js:64:7)
at Layer.handle [as handle_request] (/home/niels/Projects/shack/node_modules/express/lib/router/layer.js:95:5)
at next (/home/niels/Projects/shack/node_modules/express/lib/router/route.js:144:13)
at Route.dispatch (/home/niels/Projects/shack/node_modules/express/lib/router/route.js:114:3)
at Layer.handle [as handle_request] (/home/niels/Projects/shack/node_modules/express/lib/router/layer.js:95:5)
at /home/niels/Projects/shack/node_modules/express/lib/router/index.js:284:15
at param (/home/niels/Projects/shack/node_modules/express/lib/router/index.js:365:14)
at param (/home/niels/Projects/shack/node_modules/express/lib/router/index.js:376:14)
at Function.process_params (/home/niels/Projects/shack/node_modules/express/lib/router/index.js:421:3)
at next (/home/niels/Projects/shack/node_modules/express/lib/router/index.js:280:10)
at jsonParser (/home/niels/Projects/shack/node_modules/body-parser/lib/types/json.js:113:7)
at handleReturn (/home/niels/Projects/shack/node_modules/express-promise-router/lib/express-promise-router.js:24:23)
at jsonParser (/home/niels/Projects/shack/node_modules/express-promise-router/lib/express-promise-router.js:64:7)
at Layer.handle [as handle_request] (/home/niels/Projects/shack/node_modules/express/lib/router/layer.js:95:5)
at trim_prefix (/home/niels/Projects/shack/node_modules/express/lib/router/index.js:328:13)
at /home/niels/Projects/shack/node_modules/express/lib/router/index.js:286:9
at Function.process_params (/home/niels/Projects/shack/node_modules/express/lib/router/index.js:346:12)
at next (/home/niels/Projects/shack/node_modules/express/lib/router/index.js:280:10)
at setIp (/home/niels/Projects/shack/src/web/ip.js:15:2)
at handleReturn (/home/niels/Projects/shack/node_modules/express-promise-router/lib/express-promise-router.js:24:23)
at setIp (/home/niels/Projects/shack/node_modules/express-promise-router/lib/express-promise-router.js:64:7)
at Layer.handle [as handle_request] (/home/niels/Projects/shack/node_modules/express/lib/router/layer.js:95:5)
at trim_prefix (/home/niels/Projects/shack/node_modules/express/lib/router/index.js:328:13)
at /home/niels/Projects/shack/node_modules/express/lib/router/index.js:286:9
at Function.process_params (/home/niels/Projects/shack/node_modules/express/lib/router/index.js:346:12)
at next (/home/niels/Projects/shack/node_modules/express/lib/router/index.js:280:10)
at /home/niels/Projects/shack/node_modules/express-session/index.js:506:7
at RedisStore.get (/home/niels/Projects/shack/node_modules/connect-redis/dist/cjs/index.js:56:20)
2023-05-28 23:50:21 info [redis] Redis module initialized
2023-05-28 23:50:22 info [server] Server running at 0.0.0.0:7477
2023-05-28 23:54:06 info [redis] Redis module initialized
2023-05-28 23:54:06 info [server] Server running at 0.0.0.0:7477
2023-05-28 23:55:47 info [redis] Redis module initialized
2023-05-28 23:55:47 info [server] Server running at 0.0.0.0:7477
2023-05-28 23:56:21 info [redis] Redis module initialized
2023-05-28 23:56:22 info [server] Server running at 0.0.0.0:7477
2023-05-28 23:57:23 info [redis] Redis module initialized
2023-05-28 23:57:24 info [server] Server running at 0.0.0.0:7477
2023-05-28 23:59:13 info [redis] Redis module initialized
2023-05-28 23:59:14 info [server] Server running at 0.0.0.0:7477

28
log/2023-05-29.log Normal file
View File

@ -0,0 +1,28 @@
2023-05-29 00:03:09 info [redis] Redis module initialized
2023-05-29 00:03:09 info [server] Server running at 0.0.0.0:7477
2023-05-29 00:03:23 info [redis] Redis module initialized
2023-05-29 00:03:24 info [server] Server running at 0.0.0.0:7477
2023-05-29 00:06:47 info [redis] Redis module initialized
2023-05-29 00:06:48 info [server] Server running at 0.0.0.0:7477
2023-05-29 00:11:09 info [redis] Redis module initialized
2023-05-29 00:11:10 info [server] Server running at 0.0.0.0:7477
2023-05-29 00:11:27 info [redis] Redis module initialized
2023-05-29 00:11:27 info [server] Server running at 0.0.0.0:7477
2023-05-29 00:13:14 info [redis] Redis module initialized
2023-05-29 00:13:14 info [server] Server running at 0.0.0.0:7477
2023-05-29 00:14:55 info [redis] Redis module initialized
2023-05-29 00:14:55 info [server] Server running at 0.0.0.0:7477
2023-05-29 00:15:56 info [redis] Redis module initialized
2023-05-29 00:15:56 info [server] Server running at 0.0.0.0:7477
2023-05-29 00:23:49 info [redis] Redis module initialized
2023-05-29 00:23:49 info [server] Server running at 0.0.0.0:7477
2023-05-29 00:24:16 info [redis] Redis module initialized
2023-05-29 00:24:17 info [server] Server running at 0.0.0.0:7477
2023-05-29 00:32:13 info [redis] Redis module initialized
2023-05-29 00:32:13 info [server] Server running at 0.0.0.0:7477
2023-05-29 00:34:52 info [redis] Redis module initialized
2023-05-29 00:34:53 info [server] Server running at 0.0.0.0:7477
2023-05-29 00:35:22 info [redis] Redis module initialized
2023-05-29 00:35:22 info [server] Server running at 0.0.0.0:7477
2023-05-29 00:42:59 info [redis] Redis module initialized
2023-05-29 00:42:59 info [server] Server running at 0.0.0.0:7477

View File

@ -0,0 +1,97 @@
exports.up = async function(knex) {
await knex.schema.createTable('users', (table) => {
table.increments('id');
table.text('username', 32)
.notNullable()
.unique();
table.text('email', 32)
.notNullable()
.unique();
table.text('password')
.notNullable();
table.text('name', 32);
table.text('bio', 1000);
table.specificType('ip', 'inet');
table.datetime('created_at')
.notNullable()
.defaultTo(knex.fn.now());
});
await knex.schema.createTable('shelves', (table) => {
table.increments('id');
table.text('slug')
.notNullable();
table.integer('founder_id')
.notNullable()
.references('id')
.inTable('users');
table.datetime('created_at')
.notNullable()
.defaultTo(knex.fn.now());
});
await knex.schema.createTable('shelves_settings', (table) => {
table.increments('id');
table.integer('shelf_id')
.primary()
.references('id')
.inTable('shelves');
table.text('name');
table.text('title');
table.text('description');
table.enum('view_access', ['public', 'registered', 'private'])
.notNullable()
.defaultTo('public');
table.enum('post_access', ['registered', 'private'])
.notNullable()
.defaultTo('public');
table.boolean('is_nsfw');
});
await knex.schema.createTable('posts', (table) => {
table.increments('id');
table.text('title')
.notNullable();
table.text('body');
table.text('url');
table.integer('shelf_id')
.notNullable()
.references('id')
.inTable('shelves');
table.integer('user_id')
.notNullable()
.references('id')
.inTable('users');
table.datetime('created_at')
.notNullable()
.defaultTo(knex.fn.now());
});
await knex.raw(`ALTER TABLE posts ADD CONSTRAINT post_content CHECK (body IS NOT NULL OR url IS NOT NULL)`);
};
exports.down = async function(knex) {
await knex.schema.dropTable('posts');
await knex.schema.dropTable('shelves_settings');
await knex.schema.dropTable('shelves');
await knex.schema.dropTable('users');
};

13275
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

64
package.json Normal file
View File

@ -0,0 +1,64 @@
{
"name": "shack",
"version": "0.1.0",
"description": "Shack is a self-hosted social news aggregate",
"main": "src/web/server.js",
"repository": {
"type": "git",
"url": "https://gitea.unknown.name/DebaucheryLibrarian/shack"
},
"keywords": [
"aggregate",
"social",
"communities"
],
"author": "DebaucheryLibrarian",
"scripts": {
"dev": "npm run server",
"prod": "npm run build && npm run server:prod",
"build": "vite build",
"server": "node ./src/web/server",
"server:prod": "cross-env NODE_ENV=production node ./src/web/server",
"migrate-make": "knex-migrate generate",
"migrate": "knex-migrate up",
"rollback": "knex-migrate down"
},
"dependencies": {
"@babel/cli": "^7.21.5",
"@babel/core": "^7.21.8",
"@babel/eslint-parser": "^7.21.8",
"@babel/preset-env": "^7.21.5",
"@hcaptcha/vue3-hcaptcha": "^1.2.1",
"@vitejs/plugin-vue": "^4.0.0",
"@vue/compiler-sfc": "^3.2.33",
"@vue/server-renderer": "^3.2.33",
"bhttp": "^1.2.8",
"body-parser": "^1.20.2",
"compression": "^1.7.4",
"config": "^3.3.9",
"connect-redis": "^7.1.0",
"cross-env": "^7.0.3",
"eslint": "^8.41.0",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-plugin-vue": "^9.14.0",
"express": "^4.18.1",
"express-promise-router": "^4.1.1",
"express-session": "^1.17.3",
"ip-cidr": "^3.1.0",
"knex": "^2.4.2",
"knex-migrate": "^1.7.4",
"nanoid": "^4.0.2",
"pg": "^8.11.0",
"pinia": "^2.1.3",
"redis": "^4.6.6",
"sirv": "^2.0.2",
"vite": "^4.0.3",
"vite-plugin-ssr": "^0.4.126",
"vite-plugin-vue-markdown": "^0.23.5",
"vite-svg-loader": "^4.0.0",
"vue": "^3.2.33",
"winston": "^3.9.0",
"winston-daily-rotate-file": "^4.7.1",
"yargs": "^17.7.2"
}
}

View File

@ -0,0 +1,177 @@
<template>
<div class="content">
<h2 class="heading">Create new account</h2>
<form
class="form create"
@submit.prevent="signup"
>
<div
v-if="errorMsg"
class="form-section form-error"
>{{ errorMsg }}</div>
<div class="form-section">
<div class="form-row">
<input
v-model="username"
placeholder="Username"
maxlength="32"
class="input"
>
</div>
<div class="form-row">
<input
v-model="email"
type="email"
placeholder="E-mail"
maxlength="500"
class="input"
>
</div>
<div class="form-row">
<input
v-model="password"
type="password"
minlength="8"
maxlength="500"
placeholder="Password"
class="input"
>
</div>
</div>
<div
v-if="config.captchaEnabled"
class="form-row captcha"
>
<VueHcaptcha
:sitekey="config.captchaKey"
@verify="(token) => captchaToken = token"
@expired="() => captchaToken = null"
@error="() => captchaToken = null"
/>
</div>
<div class="form-section">
<label class="check-container noselect">
<span>
<span class="description">I have read and accept the <a
href="/help/user-agreement"
target="_blank"
class="link"
>User Agreement</a></span>
</span>
<Checkbox
:checked="agreeTos"
@change="(checked) => agreeTos = checked"
/>
</label>
</div>
<div class="form-row form-actions">
<button
:disabled="!username || !email || !password || !agreeTos || (config.captchaEnabled && !captchaToken)"
class="button button-submit"
>Sign up</button>
</div>
</form>
</div>
</template>
<script setup>
import { ref } from 'vue';
import VueHcaptcha from '@hcaptcha/vue3-hcaptcha';
import Checkbox from '../../components/form/checkbox.vue';
import navigate from '../../assets/js/navigate';
import { post } from '../../assets/js/api';
const config = CONFIG;
const username = ref('');
const email = ref('');
const password = ref('');
const agreeTos = ref(false);
const captchaToken = ref(null);
const errorMsg = ref(null);
async function signup() {
try {
await post('/api/users', {
username: username.value,
email: email.value,
password: password.value,
captchaToken: captchaToken.value,
});
navigate('/account/login');
} catch (error) {
errorMsg.value = error.message;
}
}
</script>
<style scoped>
.content {
display: flex;
justify-content: center;
align-items: center;
}
.create {
background: var(--background);
padding: 1rem;
border-radius: .5rem;
}
.name {
position: relative;
.input {
padding-left: 1.65rem;
}
.prefix {
color: var(--shadow);
position: absolute;
top: 1px;
left: .25rem;
letter-spacing: .1rem;
padding: .5rem;
}
}
.access {
font-weight: bold;
margin-bottom: .75rem;
cursor: pointer;
}
.access-description {
margin: .25rem 0 0 1.25rem;
color: var(--shadow);
font-size: .9rem;
font-weight: normal;
}
.nsfw {
margin-right: .5rem;
font-weight: bold;
color: var(--error);
}
.captcha {
justify-content: center;
}
.form-actions {
justify-content: center;
}
</style>

View File

@ -0,0 +1,142 @@
<template>
<div class="content">
<h2 class="heading">Log in</h2>
<form
class="form create"
@submit.prevent="signup"
>
<div
v-if="errorMsg"
class="form-section form-error"
>{{ errorMsg }}</div>
<div class="form-section">
<div class="form-row">
<input
v-model="username"
placeholder="Username or e-mail"
maxlength="500"
class="input"
>
</div>
<div class="form-row">
<input
v-model="password"
type="password"
minlength="8"
maxlength="500"
placeholder="Password"
class="input"
>
</div>
</div>
<!--
<div
v-if="$config.public.captchaEnabled"
class="form-row captcha"
>
<VueHcaptcha
:sitekey="$config.public.captchaKey"
@verify="(token) => captchaToken = token"
@expired="() => captchaToken = null"
@error="() => captchaToken = null"
/>
</div>
-->
<div class="form-row form-actions">
<button
:disabled="!username || !password"
class="button button-submit"
>Log in</button>
</div>
</form>
</div>
</template>
<script setup>
import { ref } from 'vue';
// import VueHcaptcha from '@hcaptcha/vue3-hcaptcha';
import { post } from '../../assets/js/api';
import navigate from '../../assets/js/navigate';
const username = ref('');
const password = ref('');
const errorMsg = ref(null);
async function signup() {
try {
await post('/api/session', {
username: username.value,
password: password.value,
});
navigate('/');
} catch (error) {
errorMsg.value = error.statusMessage;
}
}
</script>
<style scoped>
.content {
display: flex;
justify-content: center;
align-items: center;
}
.create {
background: var(--background);
padding: 1rem;
border-radius: .5rem;
}
.name {
position: relative;
.input {
padding-left: 1.65rem;
}
.prefix {
color: var(--shadow);
position: absolute;
top: 1px;
left: .25rem;
letter-spacing: .1rem;
padding: .5rem;
}
}
.access {
font-weight: bold;
margin-bottom: .75rem;
cursor: pointer;
}
.access-description {
margin: .25rem 0 0 1.25rem;
color: var(--shadow);
font-size: .9rem;
font-weight: normal;
}
.nsfw {
margin-right: .5rem;
font-weight: bold;
color: var(--error);
}
.captcha {
justify-content: center;
}
.form-actions {
justify-content: center;
}
</style>

View File

@ -0,0 +1,7 @@
# User Agreement
## Access
You must be over the age of 13 to access Shack, with or without account. Communities (shelves) labeled `NSFW` or `18+` indicate the presence of explicit content such as pornography or (simulated) violence, and may only be viewed if you are over the age of 18, you are legally permitted to view explicit content in your jurisdiction, and you do not consider explicit content as obscene or offensive.
## Privacy
Shack may collect information about your device and connection, such as your IP address, operating system and browser configuration, for the purpose of moderation and internal analytics. This information is not shared with third parties or used for targeted advertising.

8
pages/index/Counter.vue Normal file
View File

@ -0,0 +1,8 @@
<template>
<button type="button" @click="state.count++">Counter {{ state.count }}</button>
</template>
<script setup>
import { reactive } from 'vue'
const state = reactive({ count: 0 })
</script>

View File

@ -0,0 +1,25 @@
<template>
<div class="content">
<ul>
<li><a
href="/shelf/1"
class="link"
>Go to shelf</a></li>
<li><a
href="/shelf/create"
class="link"
>Create new shelf</a></li>
<li><a
href="/account/login"
class="link"
>Log in</a></li>
<li><a
href="/account/create"
class="link"
>Sign up</a></li>
</ul>
</div>
</template>

View File

@ -0,0 +1,12 @@
<template>
<h1>Welcome</h1>
This page is:
<ul>
<li>Rendered to HTML.</li>
<li>Interactive. <Counter /></li>
</ul>
</template>
<script setup>
import Counter from './Counter.vue'
</script>

View File

@ -0,0 +1,71 @@
<template>
<div class="content">
<a
href="/"
class="link"
>Go back home</a>
<h3>{{ shuck }}</h3>
<form
class="form compose"
@submit.prevent="submitPost"
>
<div class="form-row">
<input
v-model="title"
placeholder="Title"
class="input"
>
</div>
<div class="form-row">
<input
v-model="link"
class="input"
placeholder="Link"
>
</div>
<div class="form-row">
<textarea
v-model="body"
placeholder="Body"
class="input body"
/>
</div>
<div class="form-actions">
<button class="button button-submit">Post</button>
</div>
</form>
</div>
</template>
<script setup>
import { ref } from 'vue';
const shuck = 'Eratic';
// const route = useRoute();
// const res = await useFetch(`/api/shucks/${route.params.id}/posts`);
const title = ref();
const link = ref();
const body = ref();
async function submitPost() {
console.log('POST', link.value);
/*
await useFetch(`/api/shucks/${route.params.id}/posts`, {
method: 'post',
body: {
title,
link,
body,
},
});
*/
}
</script>

201
pages/shelf/create.page.vue Normal file
View File

@ -0,0 +1,201 @@
<template>
<div class="content">
<h2 class="heading">Create new shelf</h2>
<form
class="form create"
@submit.prevent="create"
>
<div class="form-section">
<div class="form-row name">
<span class="prefix">s/</span>
<input
v-model="slug"
placeholder="home"
class="input"
>
</div>
<div class="form-row">
<input
v-model="title"
placeholder="Tag line"
class="input"
>
</div>
<div class="form-row">
<textarea
v-model="description"
placeholder="Description"
class="input"
/>
</div>
</div>
<div class="form-section">
<div class="form-row">
<div class="form-column">
<h4 class="form-heading">View access</h4>
<label class="access">
<input
v-model="viewAccess"
type="radio"
value="public"
class="radio"
>Public
<div class="access-description">Everyone can browse this shelf</div>
</label>
<label class="access">
<input
v-model="viewAccess"
type="radio"
value="registered"
class="radio"
>Registered
<div class="access-description">Registered shack users can browse</div>
</label>
<label class="access">
<input
v-model="viewAccess"
type="radio"
value="private"
class="radio"
>Private
<div class="access-description">Only invited users can browse</div>
</label>
</div>
<div class="form-column">
<h4 class="form-heading">Post access</h4>
<label class="access">
<input
v-model="postAccess"
type="radio"
value="registered"
class="radio"
>Registered
<div class="access-description">Registered users can post</div>
</label>
<label class="access">
<input
v-model="postAccess"
type="radio"
value="private"
class="radio"
>Private
<div class="access-description">Only invited users can post</div>
</label>
</div>
</div>
</div>
<div class="form-section">
<label class="check-container">
<span>
<span class="nsfw">NSFW 18+</span>
<span class="description">This community allows explicit content</span>
</span>
<Checkbox
:checked="isNsfw"
@change="(checked) => isNsfw = checked"
/>
</label>
</div>
<div class="form-row form-actions">
<button class="button button-submit">Create shelf</button>
</div>
</form>
</div>
</template>
<script setup>
import { ref } from 'vue';
import Checkbox from '../../components/form/checkbox.vue';
import { post } from '../../assets/js/api';
const slug = ref();
const title = ref();
const description = ref();
const viewAccess = ref('public');
const postAccess = ref('registered');
const isNsfw = ref(false);
async function create() {
await post('/api/shelves', {
slug: slug.value,
title: title.value,
description: description.value,
settings: {
viewAccess: viewAccess.value,
postAccess: postAccess.value,
isNsfw: isNsfw.value,
},
});
}
</script>
<style scoped>
.content {
display: flex;
justify-content: center;
align-items: center;
}
.create {
background: var(--background);
padding: 1rem;
border-radius: .5rem;
}
.name {
position: relative;
.input {
padding-left: 1.65rem;
}
.prefix {
color: var(--shadow);
position: absolute;
top: 1px;
left: .25rem;
letter-spacing: .1rem;
padding: .5rem;
}
}
.access {
font-weight: bold;
margin-bottom: .75rem;
cursor: pointer;
}
.access-description {
margin: .25rem 0 0 1.25rem;
color: var(--shadow);
font-size: .9rem;
font-weight: normal;
}
.nsfw {
margin-right: .5rem;
font-weight: bold;
color: var(--error);
}
</style>

21
renderer/Link.vue Normal file
View File

@ -0,0 +1,21 @@
<template>
<a :class="{ active: pageContext.urlPathname === $attrs.href }">
<slot />
</a>
</template>
<script setup>
import { usePageContext } from './usePageContext';
const pageContext = usePageContext();
</script>
<style scoped>
a {
padding: 3px 10px;
}
a.active {
background-color: #eee;
}
</style>

47
renderer/PageShell.vue Normal file
View File

@ -0,0 +1,47 @@
<template>
<div class="layout">
<div class="content"><slot /></div>
</div>
</template>
<style>
body {
margin: 0;
font-family: sans-serif;
}
* {
box-sizing: border-box;
}
a {
text-decoration: none;
}
</style>
<style scoped>
.layout {
display: flex;
max-width: 900px;
margin: auto;
}
.content {
padding: 20px;
border-left: 2px solid #eee;
padding-bottom: 50px;
min-height: 100vh;
}
.navigation {
padding: 20px;
flex-shrink: 0;
display: flex;
flex-direction: column;
align-items: center;
line-height: 1.8em;
}
.logo {
margin-top: 20px;
margin-bottom: 10px;
}
</style>

View File

@ -0,0 +1,15 @@
import { createApp } from './app';
async function render(pageContext) {
if (!pageContext.Page) {
throw new Error('Client-side render() hook expects pageContext.Page to be defined');
}
const { app, store } = createApp(pageContext);
store.state.value = pageContext.initialState;
app.mount('#app');
}
export { render };

View File

@ -0,0 +1,80 @@
import { renderToString as renderToString_ } from '@vue/server-renderer';
import { escapeInject, dangerouslySkipEscape } from 'vite-plugin-ssr/server';
import { createApp } from './app';
import { useUser } from '../stores/user';
import logoUrl from './logo.svg';
async function renderToString(app) {
let err;
// Workaround: renderToString_() swallows errors in production, see https://github.com/vuejs/core/issues/7876
app.config.errorHandler = (err_) => { // eslint-disable-line no-param-reassign
err = err_;
};
const appHtml = await renderToString_(app);
if (err) {
throw err;
}
return appHtml;
}
async function render(pageContext) {
const appHtml = await renderToString(pageContext.app);
// See https://vite-plugin-ssr.com/head
const { documentProps } = pageContext.exports;
const title = (documentProps && documentProps.title) || 'shack';
const desc = (documentProps && documentProps.description) || 'Shack';
const documentHtml = escapeInject`
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="${logoUrl}" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="${desc}" />
<title>${title}</title>
</head>
<body>
<div id="app">${dangerouslySkipEscape(appHtml)}</div>
</body>
</html>
`;
return {
documentHtml,
};
}
async function onBeforeRender(pageContext) {
if (!pageContext.Page) {
throw new Error('My render() hook expects pageContext.Page to be defined');
}
const { app, store } = createApp(pageContext);
const userStore = useUser();
userStore.user = pageContext.session.user;
return {
pageContext: {
app,
initialState: store.state.value,
pageData: {
user: pageContext.session.user,
},
},
};
}
export {
render,
onBeforeRender,
};
export const passToClient = ['urlPathname', 'initialState', 'pageData'];

14
renderer/_error.page.vue Normal file
View File

@ -0,0 +1,14 @@
<template>
<div v-if="is404">
<h1>404 Page Not Found</h1>
<p>This page could not be found.</p>
</div>
<div v-else>
<h1>500 Internal Error</h1>
<p>Something went wrong.</p>
</div>
</template>
<script setup>
defineProps(['is404'])
</script>

30
renderer/app.js Normal file
View File

@ -0,0 +1,30 @@
import { createSSRApp, h } from 'vue';
import { createPinia } from 'pinia';
import Container from './container.vue';
import { setPageContext } from './usePageContext';
import '../assets/css/style.css';
function createApp(pageContext) {
const PageWithLayout = {
render() {
return h(Container, {}, {
default() {
return h(pageContext.Page, pageContext.pageData || {});
},
});
},
};
const app = createSSRApp(PageWithLayout);
const store = createPinia();
app.use(store);
// We make pageContext available from any Vue component
setPageContext(app, pageContext);
return { app, store };
}
export { createApp };

127
renderer/container.vue Normal file
View File

@ -0,0 +1,127 @@
<template>
<div class="container">
<header class="header">
<a href="/">
<h1 class="title">
<div
class="logo"
v-html="logo"
/>shack
</h1>
</a>
<div class="search">
<input
type="search"
class="input"
placeholder="Search the shack"
>
</div>
<span
v-if="user"
class="userpanel"
@click="logout"
>{{ user.username }}</span>
</header>
<div class="content-container">
<slot />
<footer class="footer">shuck {{ version }}</footer>
</div>
</div>
</template>
<script setup>
// import { onMounted } from 'vue';
// import { usePageContext } from './usePageContext';
import { storeToRefs } from 'pinia';
import logo from '../assets/img/logo.svg?raw';
import { del } from '../assets/js/api';
import { useUser } from '../stores/user';
// const pageContext = usePageContext();
const version = CLIENT_VERSION;
const userStore = useUser();
const { user } = storeToRefs(userStore);
async function logout() {
await del('/api/session');
window.location.reload();
}
</script>
<style>
.content {
display: flex;
flex-direction: column;
padding: 1rem;
flex-grow: 1;
}
.logo svg {
height: 100%;
width: auto;
}
</style>
<style scoped>
.container {
height: 100%;
display: flex;
flex-direction: column;
background: var(--background-dark-10);
}
.content-container {
display: flex;
flex-direction: column;
flex-grow: 1;
overflow-y: auto;
}
.header {
display: flex;
flex-shrink: 0;
background: var(--background);
box-shadow: 0 0 3px var(--shadow-weak-10);
overflow: hidden;
}
.logo {
width: auto;
height: 1.75rem;
fill: var(--primary);
margin: .75rem .5rem 1rem 1rem;
}
.title {
font-size: 0;
}
.search {
display: flex;
flex-grow: 1;
align-items: center;
margin: 0 1rem;
.input {
flex-grow: 1;
}
}
.userpanel {
display: flex;
align-items: center;
padding: 1rem;
font-weight: bold;
}
.footer {
padding: .5rem 1rem;
color: var(--shadow);
text-align: center;
}
</style>

36
renderer/logo.svg Normal file
View File

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="175" height="175" fill="none" version="1.1" viewBox="0 0 175 175" xmlns="http://www.w3.org/2000/svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
<metadata>
<rdf:RDF>
<cc:Work rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
<dc:title/>
</cc:Work>
</rdf:RDF>
</metadata>
<defs>
<linearGradient id="linearGradient880" x1="108.64" x2="115.51" y1="88.726" y2="136.2" gradientTransform="matrix(1.0498 0 0 1.0498 -2.9171 -2.9658)" gradientUnits="userSpaceOnUse">
<stop stop-color="#ffea83" offset="0"/>
<stop stop-color="#FFDD35" offset=".083333"/>
<stop stop-color="#FFA800" offset="1"/>
</linearGradient>
<linearGradient id="paint2_linear" x1="48.975" x2="61.299" y1="3.9232" y2="158.04" gradientTransform="translate(-2.832e-5)" gradientUnits="userSpaceOnUse">
<stop stop-color="#FFEA83" offset="0"/>
<stop stop-color="#FFDD35" offset=".083333"/>
<stop stop-color="#FFA800" offset="1"/>
</linearGradient>
<linearGradient id="paint0_linear-6" x1="-1.4492" x2="116.62" y1="-5.8123" y2="137.08" gradientTransform="translate(-2.832e-5)" gradientUnits="userSpaceOnUse">
<stop stop-color="#41D1FF" offset="0"/>
<stop stop-color="#BD34FE" offset="1"/>
</linearGradient>
</defs>
<circle cx="87.5" cy="87.5" r="87.5" fill="#c4c4c4"/>
<circle cx="87.5" cy="87.5" r="87.5" fill="url(#paint0_linear-6)"/>
<g transform="translate(632.92 54.355)" fill="#d38787" stroke-width="1.0614">
<path d="m-549.75 68.457c-5.7533-3.1217-6.1166-5.2295-6.1166-35.489 0-30.458 0.35464-32.448 6.3339-35.54 3.9943-2.0655 24.279-2.2805 26.735-0.28333 0.89718 0.72974 6.7203 6.6637 12.94 13.187l11.309 11.86v19.575c0 18.473-0.12956 19.74-2.3011 22.5-4.0223 5.1136-7.558 5.8565-27.65 5.8099-14.15-0.03287-19.008-0.40294-21.25-1.6191zm42.473-6.3594c2.27-1.59 2.359-2.2909 2.359-18.575v-16.923h-6.9521c-12.443 0-16.4-4.0845-16.4-16.93v-7.4828h-8.9464c-6.7178 0-9.3619 0.41549-10.614 1.668-2.5031 2.5031-2.5031 55.724 0 58.228 2.4502 2.4502 37.058 2.4636 40.553 0.01609zm-1.8867-42.165c0-0.16422-2.8659-3.1346-6.3686-6.6008l-6.3686-6.3022v4.9328c0 6.3185 1.8955 8.2687 8.0366 8.2687 2.5854 0 4.7007-0.13434 4.7007-0.29859zm-57.57 44.279c-5.6185-3.0486-6.1166-5.593-6.1166-31.243 0-18.891 0.31331-24.063 1.6101-26.571 1.809-3.4981 6.5048-6.3339 10.489-6.3339 2.4847 0 2.5814 0.19984 1.541 3.1843-0.61054 1.7514-1.7457 3.1843-2.5226 3.1843-0.77686 0-2.1631 0.75059-3.0805 1.668-2.4923 2.4923-2.4923 47.244 0 49.736 0.91739 0.9174 2.3036 1.668 3.0805 1.668 0.77688 0 1.912 1.4329 2.5226 3.1843 1.0562 3.0298 0.97108 3.1822-1.7537 3.1418-1.575-0.02331-4.1713-0.75194-5.7694-1.6191zm-16.983-4.2458c-5.4392-2.9512-6.1166-5.9415-6.1166-26.997 0-15.096 0.345-19.878 1.6101-22.325 1.7476-3.3796 6.4758-6.3339 10.137-6.3339 1.8666 0 2.1789 0.44955 1.6594 2.3882-0.35184 1.3135-0.64655 2.7465-0.65453 3.1843-8e-3 0.43784-0.69682 0.79608-1.5308 0.79608-0.83399 0-2.2669 0.75059-3.1843 1.668-2.4767 2.4767-2.4767 38.768 0 41.244 0.91741 0.91739 2.2946 1.668 3.0605 1.668 1.196 0 2.6402 2.995 2.6871 5.5726 0.0241 1.3294-4.5804 0.80962-7.6676-0.8655z" style="mix-blend-mode:lighten"/>
<path d="m-552.2 68.911c-5.7533-3.1217-6.1166-5.2295-6.1166-35.489 0-30.458 0.35463-32.448 6.3339-35.54 3.9943-2.0655 24.279-2.2805 26.735-0.28333 0.89718 0.72974 6.7203 6.6637 12.94 13.187l11.309 11.86v19.575c0 18.473-0.12957 19.74-2.3011 22.5-4.0223 5.1136-7.558 5.8565-27.65 5.8099-14.15-0.03287-19.008-0.40294-21.25-1.6191zm42.473-6.3594c2.27-1.59 2.359-2.2909 2.359-18.575v-16.923h-6.952c-12.443 0-16.4-4.0845-16.4-16.93v-7.4828h-8.9464c-6.7179 0-9.3619 0.41549-10.614 1.668-2.5031 2.5031-2.5031 55.724 0 58.228 2.4502 2.4502 37.058 2.4636 40.553 0.01609zm-1.8867-42.165c0-0.16422-2.8659-3.1346-6.3686-6.6008l-6.3686-6.3022v4.9328c0 6.3185 1.8955 8.2688 8.0366 8.2688 2.5854 0 4.7007-0.13434 4.7007-0.29859zm-57.57 44.279c-5.6185-3.0486-6.1166-5.593-6.1166-31.243 0-18.891 0.31331-24.063 1.6101-26.571 1.809-3.4981 6.5048-6.3339 10.489-6.3339 2.4847 0 2.5814 0.19984 1.541 3.1843-0.61054 1.7514-1.7457 3.1843-2.5226 3.1843-0.77687 0-2.1631 0.75059-3.0805 1.668-2.4923 2.4923-2.4923 47.244 0 49.736 0.91741 0.91739 2.3036 1.668 3.0805 1.668 0.77686 0 1.912 1.4329 2.5226 3.1843 1.0562 3.0298 0.97107 3.1822-1.7537 3.1418-1.575-0.02331-4.1713-0.75194-5.7694-1.6191zm-16.983-4.2458c-5.4392-2.9512-6.1166-5.9415-6.1166-26.997 0-15.096 0.34502-19.878 1.6101-22.325 1.7476-3.3796 6.4758-6.3339 10.137-6.3339 1.8666 0 2.1789 0.44955 1.6594 2.3882-0.35182 1.3135-0.64653 2.7465-0.65452 3.1843-8e-3 0.43784-0.69683 0.79608-1.5308 0.79608-0.83397 0-2.2669 0.75059-3.1843 1.668-2.4767 2.4767-2.4767 38.768 0 41.245 0.9174 0.91739 2.2946 1.668 3.0605 1.668 1.196 0 2.6402 2.995 2.6871 5.5726 0.0241 1.3294-4.5804 0.80962-7.6676-0.8655z" fill-opacity=".47466" style="mix-blend-mode:lighten"/>
</g>
<path d="m128.48 88.913-24.027 4.6784c-0.39475 0.07685-0.68766 0.40944-0.71076 0.80849l-1.4782 24.805c-0.0347 0.58371 0.50497 1.0372 1.0792 0.90602l6.6886-1.5338c0.62676-0.14383 1.1916 0.40419 1.0635 1.0299l-1.9874 9.6702c-0.13438 0.65091 0.48084 1.2073 1.1202 1.0142l4.1322-1.2472c0.64041-0.19317 1.2556 0.36535 1.1202 1.0162l-3.158 15.191c-0.19842 0.95011 1.074 1.4677 1.6042 0.653l0.35485-0.54382 19.578-38.827c0.32755-0.64985-0.23727-1.391-0.95641-1.2535l-6.8849 1.3207c-0.6467 0.12389-1.1979-0.47453-1.0152-1.1034l4.4944-15.482c0.18266-0.63012-0.36955-1.2295-1.0173-1.1034z" fill="url(#linearGradient880)" stroke-width="1.0498"/>
<rect x="3" y="3" width="169" height="169" rx="84.5" stroke="url(#paint2_linear)" stroke-width="6" style="mix-blend-mode:soft-light"/>
</svg>

After

Width:  |  Height:  |  Size: 5.7 KiB

View File

@ -0,0 +1,19 @@
// `usePageContext` allows us to access `pageContext` in any Vue component.
// See https://vite-plugin-ssr.com/pageContext-anywhere
import { inject } from 'vue';
const key = Symbol();
function usePageContext() {
const pageContext = inject(key);
return pageContext;
}
function setPageContext(app, pageContext) {
app.provide(key, pageContext);
}
export { usePageContext };
export { setPageContext };

36
src/.eslintrc Executable file
View File

@ -0,0 +1,36 @@
{
"root": true,
"extends": ["airbnb-base", "plugin:vue/recommended"],
"parserOptions": {
"parser": "@babel/eslint-parser",
"ecmaVersion": 2019,
"sourceType": "script",
"requireConfigFile": false
},
"rules": {
"indent": ["error", "tab"],
"no-tabs": "off",
"no-unused-vars": ["error", {"argsIgnorePattern": "^_"}],
"no-console": 0,
"template-curly-spacing": "off",
"import/prefer-default-export": 0,
"max-len": 0,
"vue/no-v-html": 0,
"vue/html-indent": ["error", "tab"],
"vue/multiline-html-element-content-newline": 0,
"vue/singleline-html-element-content-newline": 0,
"vue/multi-word-component-names": 0,
"no-param-reassign": ["error", {
"props": true,
"ignorePropertyModificationsFor": ["state", "acc"]
}]
},
"globals": {
"CONFIG": "readonly",
"$fetch": "readonly",
"createError": "readonly",
"defineEventHandler": "readonly",
"defineNuxtConfig": "readonly",
"readBody": "readonly"
}
}

11
src/cli.js Executable file
View File

@ -0,0 +1,11 @@
// const config = require('config');
const yargs = require('yargs');
const { argv } = yargs
.command('npm start')
.option('log-level', {
alias: 'level',
type: 'string',
});
module.exports = argv;

12
src/errors.js Executable file
View File

@ -0,0 +1,12 @@
class HttpError extends Error {
constructor({ message, statusCode = 400, statusMessage }) {
super(message || statusMessage);
this.name = 'HttpError';
this.statusCode = statusCode;
this.statusMessage = statusMessage;
}
}
module.exports = { HttpError };

10
src/knex.js Executable file
View File

@ -0,0 +1,10 @@
const config = require('config');
const knex = require('knex');
module.exports = knex({
client: 'pg',
connection: config.database,
// performance overhead, don't use asyncStackTraces in production
asyncStackTraces: process.env.NODE_ENV === 'development',
// debug: process.env.NODE_ENV === 'development',
});

35
src/logger.js Executable file
View File

@ -0,0 +1,35 @@
const util = require('util');
const path = require('path');
const winston = require('winston');
require('winston-daily-rotate-file');
// import args from './args';
module.exports = function initLogger(filepath) {
const contextLabel = path.basename(filepath, '.js');
return winston.createLogger({
format: winston.format.combine(
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
winston.format((info) => (info instanceof Error
? { ...info, message: info.stack }
: { ...info, message: typeof info.message === 'string' ? info.message : util.inspect(info.message) }))(),
winston.format.colorize(),
winston.format.printf(({
level, timestamp, label, message,
}) => `${timestamp} ${level} [${label || contextLabel}] ${message}`),
),
transports: [
new winston.transports.Console({
level: 'silly',
timestamp: true,
}),
new winston.transports.DailyRotateFile({
datePattern: 'YYYY-MM-DD',
filename: path.join('log', '%DATE%.log'),
level: 'silly',
}),
],
});
};

14
src/redis.js Normal file
View File

@ -0,0 +1,14 @@
const config = require('config');
const redis = require('redis');
const logger = require('./logger')(__filename);
const client = redis.createClient({
socket: config.redis,
});
client.connect();
logger.info('Redis module initialized');
module.exports = {
client,
};

17
src/shelves.js Normal file
View File

@ -0,0 +1,17 @@
const knex = require('./knex');
async function createShelf(shelf) {
console.log('create', shelf);
const shelfEntry = await knex('shelves').insert({
slug: shelf.slug,
});
console.log('entry', shelfEntry);
return true;
}
module.exports = {
createShelf,
};

147
src/users.js Normal file
View File

@ -0,0 +1,147 @@
const config = require('config');
const util = require('util');
const crypto = require('crypto');
const bhttp = require('bhttp');
const logger = require('./logger')(__filename);
const { HttpError } = require('./errors');
const knex = require('./knex');
const scrypt = util.promisify(crypto.scrypt);
function curateDatabaseUser(user) {
return {
id: user.id,
username: user.username,
};
}
async function hashPassword(password) {
const salt = crypto.randomBytes(16).toString('hex');
const hash = await scrypt(password, salt, 64);
return `${salt}/${hash.toString('hex')}`;
}
async function verifyPassword(password, storedPassword) {
const [salt, hash] = storedPassword.split('/');
const hashedPassword = await scrypt(password, salt, 64);
if (hashedPassword.toString('hex') !== hash) {
throw new HttpError({
statusCode: 400,
statusMessage: 'Username or password incorrect',
});
}
}
async function verifyCaptcha(captchaToken) {
if (!captchaToken) {
throw new HttpError({
statusCode: 400,
statusMessage: 'No CAPTCHA provided',
});
}
const res = await bhttp.post('https://hcaptcha.com/siteverify', {
response: captchaToken,
secret: config.auth.captcha.secret,
});
if (res.statusCode !== 200 || !res.body.success) {
throw new HttpError({
statusCode: 498,
statusMessage: 'Invalid CAPTCHA',
});
}
}
async function login(credentials) {
if (!credentials.username) {
throw new HttpError({
statusCode: 400,
statusMessage: 'You must provide a username',
});
}
if (!credentials.password) {
throw new HttpError({
statusCode: 400,
statusMessage: 'You must provide a password',
});
}
const user = await knex('users')
.where('username', credentials.username)
.orWhere('email', credentials.username)
.first();
if (!user) {
throw new HttpError({
statusCode: 400,
statusMessage: 'Username or password incorrect',
});
}
await verifyPassword(credentials.password, user.password);
return curateDatabaseUser(user);
}
async function createUser(credentials, context) {
if (!credentials.username) {
throw new HttpError({
statusCode: 400,
statusMessage: 'You must provide a username',
});
}
if (!credentials.password) {
throw new HttpError({
statusCode: 400,
statusMessage: 'You must provide a password',
});
}
if (config.auth.captcha.enabled) {
await verifyCaptcha(credentials.captchaToken);
}
const hashedPassword = await hashPassword(credentials.password);
try {
const [userEntry] = await knex('users')
.insert({
username: credentials.username,
email: credentials.email,
password: hashedPassword,
ip: context.ip,
})
.returning('*');
const user = curateDatabaseUser(userEntry);
logger.info(`Registered user ${user.username} (${user.id}, ${user.email}, ${userEntry.ip})`);
return user;
} catch (error) {
if (error.code === '23505') {
throw new HttpError({
statusCode: 409,
statusMessage: 'Username or e-mail already registered',
});
}
logger.error(error.message);
throw new HttpError({
statusCode: 500,
statusMessage: 'Sign-up failed',
});
}
}
module.exports = {
createUser,
login,
};

32
src/web/default.js Normal file
View File

@ -0,0 +1,32 @@
const { renderPage } = require('vite-plugin-ssr/server');
async function initDefaultHandler() {
async function defaultHandler(req, res, next) {
const pageContextInit = {
urlOriginal: req.originalUrl,
session: req.session,
};
const pageContext = await renderPage(pageContextInit);
const { httpResponse } = pageContext;
if (!httpResponse) {
next();
return;
}
const {
body, statusCode, contentType, earlyHints,
} = httpResponse;
if (res.writeEarlyHints) {
res.writeEarlyHints({ link: earlyHints.map((e) => e.earlyHintLink) });
}
res.status(statusCode).type(contentType).send(body);
}
return defaultHandler;
}
module.exports = initDefaultHandler;

22
src/web/error.js Executable file
View File

@ -0,0 +1,22 @@
const logger = require('../logger')(__filename);
function errorHandler(error, req, res, _next) {
logger.warn(`Failed to fulfill request to ${req.path}: ${error.message}`);
if (process.env.NODE_ENV === 'development') {
logger.error(error);
}
if (error.statusCode) {
res.status(error.statusCode).send({
statusCode: error.statusCode,
message: error.statusMessage,
});
return;
}
res.status(500).send('Something didn\'t quite go as expected... Our apologies for the inconvenience.');
}
module.exports = errorHandler;

18
src/web/ip.js Normal file
View File

@ -0,0 +1,18 @@
const IPCIDR = require('ip-cidr');
function setIp(req, res, next) {
const ip = req.headers['x-forwarded-for']
? req.headers['x-forwarded-for'].split(',')[0]
: req.connection.remoteAddress;
// ensure IP is in expanded notation to simplify matching
const expandedIp = /:/.test(ip)
? new IPCIDR(`${ip}/128`) // IPv6
: new IPCIDR(`${ip}/32`); // IPv4
req.userIp = expandedIp.addressStart?.addressMinusSuffix || null; // eslint-disable-line no-param-reassign
next();
}
module.exports = setIp;

77
src/web/server.js Normal file
View File

@ -0,0 +1,77 @@
// Note that this file isn't processed by Vite, see https://github.com/brillout/vite-plugin-ssr/issues/562
const config = require('config');
const express = require('express');
const Router = require('express-promise-router');
const session = require('express-session');
const RedisStore = require('connect-redis').default;
const bodyParser = require('body-parser');
const compression = require('compression');
const sirv = require('sirv');
const vite = require('vite');
const logger = require('../logger')(__filename);
const redis = require('../redis').client;
const initDefaultHandler = require('./default');
const setIp = require('./ip');
const errorHandler = require('./error');
const {
login,
logout,
fetchUser,
createUser,
} = require('./users');
const { createShelf } = require('./shelves');
const root = `${__dirname}/../..`;
async function startServer() {
const app = express();
const router = Router();
const sessionStore = new RedisStore({ client: redis });
const defaultHandler = await initDefaultHandler();
app.disable('x-powered-by');
app.set('trust proxy', 1);
router.use(session({ ...config.web.session, store: sessionStore }));
router.use(setIp);
router.use(bodyParser.json({ strict: false }));
app.use(compression());
if (process.env.NODE_ENV === 'production') {
app.use(sirv(`${root}/dist/client`));
} else {
const viteDevMiddleware = (await vite.createServer({
root,
server: { middlewareMode: true },
})).middlewares;
app.use(viteDevMiddleware);
}
router.get('/api/session', fetchUser);
router.post('/api/session', login);
router.delete('/api/session', logout);
router.post('/api/shelves', createShelf);
router.post('/api/users', createUser);
router.get('*', defaultHandler);
router.use(errorHandler);
app.use(router);
const server = app.listen(process.env.PORT || config.web.port, process.env.HOST || config.web.host, () => {
const { address, port } = server.address();
logger.info(`Server running at ${address}:${port}`);
});
}
startServer();

13
src/web/shelves.js Normal file
View File

@ -0,0 +1,13 @@
const { createShelf } = require('../shelves');
async function createShelfApi(req) {
console.log('create shelf', req.body);
const shelf = await createShelf(req.body);
return shelf;
}
module.exports = {
createShelf: createShelfApi,
};

33
src/web/users.js Normal file
View File

@ -0,0 +1,33 @@
const {
login,
createUser,
} = require('../users');
async function fetchUserApi(req, res) {
res.send(req.session.user);
}
async function loginApi(req, res) {
const user = await login(req.body);
req.session.user = user; // eslint-disable-line no-param-reassign
res.send(user);
}
async function logoutApi(req, res) {
req.session.destroy();
res.status(204).send();
}
async function createUserApi(req, res) {
const user = await createUser(req.body, { ip: req.userIp });
res.send(user);
}
module.exports = {
login: loginApi,
logout: logoutApi,
fetchUser: fetchUserApi,
createUser: createUserApi,
};

18
stores/user.js Normal file
View File

@ -0,0 +1,18 @@
import { defineStore } from 'pinia';
import { get } from '../assets/js/api';
async function fetchUser() {
const user = await get('/api/session');
this.user = user;
}
export const useUser = defineStore('user', {
state: () => ({
user: null,
}),
actions: {
fetchUser,
},
});

23
vite.config.js Normal file
View File

@ -0,0 +1,23 @@
import config from 'config';
import vue from '@vitejs/plugin-vue';
import ssr from 'vite-plugin-ssr/plugin';
import markdown from 'vite-plugin-vue-markdown';
import pkg from './package.json';
import clientConfig from './assets/js/config/local';
export default {
plugins: [
vue({ include: [/\.vue$/, /\.md$/] }),
markdown(),
ssr(),
],
define: {
CLIENT_VERSION: JSON.stringify(pkg.version),
CONFIG: JSON.stringify({
captchaEnabled: config.auth.captcha.enabled,
...clientConfig,
}),
},
};