Initial commit, basic pages and sessions.
This commit is contained in:
14
.editorconfig
Executable file
14
.editorconfig
Executable 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
32
.eslintrc
Executable 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
6
.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
node_modules/
|
||||
dist/
|
||||
config/
|
||||
!config/default.js
|
||||
assets/js/config/
|
||||
!assets/js/config/default.js
|
||||
48
assets/css/forms.css
Normal file
48
assets/css/forms.css
Normal 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
44
assets/css/inputs.css
Normal 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
21
assets/css/markdown.css
Normal 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
36
assets/css/states.css
Executable 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
31
assets/css/style.css
Normal 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
34
assets/css/theme.css
Normal 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
71
assets/img/favicon.svg
Normal 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
83
assets/img/logo.svg
Normal 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
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
86
assets/js/api.js
Normal 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
4
assets/js/navigate.js
Normal 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
110
components/form/checkbox.vue
Executable 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
6
knexfile.js
Executable file
@@ -0,0 +1,6 @@
|
||||
const config = require('config');
|
||||
|
||||
module.exports = {
|
||||
client: 'pg',
|
||||
connection: config.database,
|
||||
};
|
||||
15
log/2023-05-26.log
Normal file
15
log/2023-05-26.log
Normal file
@@ -0,0 +1,15 @@
|
||||
2023-05-26 22:00:04 [32minfo[39m [/home/niels/Projects/shack/src/web/server.js] Server running at :::7477
|
||||
2023-05-26 22:01:41 [32minfo[39m [server] Server running at :::7477
|
||||
2023-05-26 22:02:51 [32minfo[39m [server] Server running at :::7477
|
||||
2023-05-26 22:13:32 [32minfo[39m [server] Server running at :::7477
|
||||
2023-05-26 22:14:07 [32minfo[39m [server] Server running at :::7477
|
||||
2023-05-26 22:14:56 [32minfo[39m [server] Server running at :::7477
|
||||
2023-05-26 22:15:07 [32minfo[39m [server] Server running at :::7477
|
||||
2023-05-26 22:15:33 [32minfo[39m [server] Server running at :::7477
|
||||
2023-05-26 22:16:20 [32minfo[39m [server] Server running at :::7477
|
||||
2023-05-26 22:16:33 [32minfo[39m [server] Server running at :::7477
|
||||
2023-05-26 22:23:24 [32minfo[39m [server] Server running at :::7477
|
||||
2023-05-26 23:50:58 [32minfo[39m [server] Server running at :::7477
|
||||
2023-05-26 23:52:16 [32minfo[39m [server] Server running at :::7477
|
||||
2023-05-26 23:59:17 [32minfo[39m [server] Server running at :::7477
|
||||
2023-05-26 23:59:40 [32minfo[39m [server] Server running at :::7477
|
||||
42
log/2023-05-27.log
Normal file
42
log/2023-05-27.log
Normal file
@@ -0,0 +1,42 @@
|
||||
2023-05-27 00:02:12 [32minfo[39m [server] Server running at :::7477
|
||||
2023-05-27 00:02:53 [32minfo[39m [server] Server running at :::7477
|
||||
2023-05-27 00:03:46 [32minfo[39m [server] Server running at :::7477
|
||||
2023-05-27 16:31:42 [32minfo[39m [server] Server running at :::7477
|
||||
2023-05-27 17:15:42 [32minfo[39m [server] Server running at :::7477
|
||||
2023-05-27 17:24:43 [32minfo[39m [server] Server running at :::7477
|
||||
2023-05-27 17:28:55 [32minfo[39m [server] Server running at :::7477
|
||||
2023-05-27 17:30:14 [32minfo[39m [server] Server running at :::7477
|
||||
2023-05-27 17:30:48 [32minfo[39m [server] Server running at :::7477
|
||||
2023-05-27 17:31:08 [32minfo[39m [server] Server running at :::7477
|
||||
2023-05-27 17:34:32 [32minfo[39m [server] Server running at :::7477
|
||||
2023-05-27 17:53:55 [32minfo[39m [server] Server running at :::7477
|
||||
2023-05-27 22:40:18 [32minfo[39m [server] Server running at :::7477
|
||||
2023-05-27 22:48:27 [32minfo[39m [server] Server running at :::7477
|
||||
2023-05-27 23:23:00 [32minfo[39m [server] Server running at :::7477
|
||||
2023-05-27 23:29:39 [32minfo[39m [server] Server running at :::7477
|
||||
2023-05-27 23:36:51 [32minfo[39m [server] Server running at :::7477
|
||||
2023-05-27 23:41:25 [32minfo[39m [server] Server running at :::7477
|
||||
2023-05-27 23:45:38 [32minfo[39m [server] Server running at :::7477
|
||||
2023-05-27 23:46:09 [32minfo[39m [users] Registered user admin (1, undefined, ::ffff:127.0.0.1)
|
||||
2023-05-27 23:47:00 [32minfo[39m [server] Server running at :::7477
|
||||
2023-05-27 23:47:51 [32minfo[39m [server] Server running at :::7477
|
||||
2023-05-27 23:54:47 [32minfo[39m [server] Server running at :::7477
|
||||
2023-05-27 23:55:07 [32minfo[39m [server] Server running at :::7477
|
||||
2023-05-27 23:55:23 [32minfo[39m [server] Server running at :::7477
|
||||
2023-05-27 23:55:31 [33mwarn[39m [error] Failed to fulfill request to /api/users: Username or e-mail already registered
|
||||
2023-05-27 23:55:31 [31merror[39m [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 [32minfo[39m [server] Server running at :::7477
|
||||
2023-05-27 23:57:11 [32minfo[39m [server] Server running at :::7477
|
||||
2023-05-27 23:57:17 [33mwarn[39m [error] Failed to fulfill request to /api/users: Username or e-mail already registered
|
||||
2023-05-27 23:57:17 [31merror[39m [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 [33mwarn[39m [error] Failed to fulfill request to /api/users: Username or e-mail already registered
|
||||
2023-05-27 23:58:08 [31merror[39m [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
269
log/2023-05-28.log
Normal file
@@ -0,0 +1,269 @@
|
||||
2023-05-28 00:08:00 [32minfo[39m [server] Server running at :::7477
|
||||
2023-05-28 00:11:09 [32minfo[39m [server] Server running at :::7477
|
||||
2023-05-28 00:14:04 [32minfo[39m [server] Server running at :::7477
|
||||
2023-05-28 00:21:15 [32minfo[39m [server] Server running at :::7477
|
||||
2023-05-28 00:22:47 [32minfo[39m [server] Server running at :::7477
|
||||
2023-05-28 00:36:00 [32minfo[39m [server] Server running at :::7477
|
||||
2023-05-28 00:36:38 [32minfo[39m [server] Server running at :::7477
|
||||
2023-05-28 00:39:32 [32minfo[39m [server] Server running at :::7477
|
||||
2023-05-28 00:41:42 [32minfo[39m [server] Server running at :::7477
|
||||
2023-05-28 00:44:41 [32minfo[39m [server] Server running at :::7477
|
||||
2023-05-28 01:06:18 [32minfo[39m [server] Server running at :::7477
|
||||
2023-05-28 01:06:43 [32minfo[39m [server] Server running at :::7477
|
||||
2023-05-28 01:09:47 [32minfo[39m [server] Server running at :::7477
|
||||
2023-05-28 01:10:11 [32minfo[39m [server] Server running at :::7477
|
||||
2023-05-28 01:11:03 [32minfo[39m [server] Server running at :::7477
|
||||
2023-05-28 01:11:33 [32minfo[39m [server] Server running at :::7477
|
||||
2023-05-28 01:11:46 [32minfo[39m [server] Server running at :::7477
|
||||
2023-05-28 01:12:07 [32minfo[39m [server] Server running at :::7477
|
||||
2023-05-28 01:12:37 [32minfo[39m [server] Server running at :::7477
|
||||
2023-05-28 01:15:28 [32minfo[39m [server] Server running at :::7477
|
||||
2023-05-28 01:16:12 [32minfo[39m [server] Server running at :::7477
|
||||
2023-05-28 01:17:01 [32minfo[39m [server] Server running at :::7477
|
||||
2023-05-28 01:17:28 [32minfo[39m [server] Server running at :::7477
|
||||
2023-05-28 01:17:38 [32minfo[39m [server] Server running at :::7477
|
||||
2023-05-28 01:20:33 [32minfo[39m [server] Server running at :::7477
|
||||
2023-05-28 01:22:29 [32minfo[39m [server] Server running at :::7477
|
||||
2023-05-28 01:23:20 [32minfo[39m [server] Server running at :::7477
|
||||
2023-05-28 01:23:38 [32minfo[39m [server] Server running at :::7477
|
||||
2023-05-28 01:24:21 [32minfo[39m [server] Server running at :::7477
|
||||
2023-05-28 01:24:39 [32minfo[39m [server] Server running at :::7477
|
||||
2023-05-28 01:26:25 [32minfo[39m [server] Server running at :::7477
|
||||
2023-05-28 01:28:39 [32minfo[39m [server] Server running at :::7477
|
||||
2023-05-28 01:29:11 [32minfo[39m [server] Server running at :::7477
|
||||
2023-05-28 01:30:10 [32minfo[39m [server] Server running at :::7477
|
||||
2023-05-28 01:31:36 [32minfo[39m [server] Server running at :::7477
|
||||
2023-05-28 01:31:46 [32minfo[39m [server] Server running at :::7477
|
||||
2023-05-28 01:31:47 [33mwarn[39m [error] Failed to fulfill request to /: secret option required for sessions
|
||||
2023-05-28 01:31:47 [33mwarn[39m [error] Failed to fulfill request to /favicon.ico: secret option required for sessions
|
||||
2023-05-28 01:32:32 [32minfo[39m [server] Server running at :::7477
|
||||
2023-05-28 01:32:34 [33mwarn[39m [error] Failed to fulfill request to /: secret option required for sessions
|
||||
2023-05-28 01:32:34 [33mwarn[39m [error] Failed to fulfill request to /favicon.ico: secret option required for sessions
|
||||
2023-05-28 01:32:45 [32minfo[39m [server] Server running at :::7477
|
||||
2023-05-28 01:32:56 [32minfo[39m [server] Server running at :::7477
|
||||
2023-05-28 01:34:50 [32minfo[39m [server] Server running at :::7477
|
||||
2023-05-28 01:36:08 [32minfo[39m [server] Server running at :::7477
|
||||
2023-05-28 01:36:25 [32minfo[39m [server] Server running at :::7477
|
||||
2023-05-28 02:09:21 [32minfo[39m [server] Server running at :::7477
|
||||
2023-05-28 02:11:16 [32minfo[39m [server] Server running at :::7477
|
||||
2023-05-28 02:11:58 [32minfo[39m [server] Server running at :::7477
|
||||
2023-05-28 02:13:02 [32minfo[39m [server] Server running at :::7477
|
||||
2023-05-28 02:14:03 [32minfo[39m [server] Server running at :::7477
|
||||
2023-05-28 02:18:24 [32minfo[39m [redis] Redis module initialized
|
||||
2023-05-28 02:18:25 [32minfo[39m [server] Server running at :::7477
|
||||
2023-05-28 02:19:39 [32minfo[39m [redis] Redis module initialized
|
||||
2023-05-28 02:19:39 [32minfo[39m [server] Server running at :::7477
|
||||
2023-05-28 02:20:44 [32minfo[39m [redis] Redis module initialized
|
||||
2023-05-28 02:20:44 [32minfo[39m [server] Server running at :::7477
|
||||
2023-05-28 16:06:50 [32minfo[39m [redis] Redis module initialized
|
||||
2023-05-28 16:06:50 [32minfo[39m [server] Server running at :::7477
|
||||
2023-05-28 17:45:15 [32minfo[39m [redis] Redis module initialized
|
||||
2023-05-28 17:45:16 [32minfo[39m [server] Server running at :::7477
|
||||
2023-05-28 21:22:50 [32minfo[39m [redis] Redis module initialized
|
||||
2023-05-28 21:22:51 [32minfo[39m [server] Server running at :::7477
|
||||
2023-05-28 21:25:31 [32minfo[39m [redis] Redis module initialized
|
||||
2023-05-28 21:25:31 [32minfo[39m [server] Server running at :::7477
|
||||
2023-05-28 21:25:46 [32minfo[39m [redis] Redis module initialized
|
||||
2023-05-28 21:25:46 [32minfo[39m [server] Server running at :::7477
|
||||
2023-05-28 21:25:56 [33mwarn[39m [error] Failed to fulfill request to /api/session: Username or password incorrect
|
||||
2023-05-28 21:25:56 [31merror[39m [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 [33mwarn[39m [error] Failed to fulfill request to /api/session: The client is closed
|
||||
2023-05-28 21:26:02 [31merror[39m [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 [33mwarn[39m [error] Failed to fulfill request to /: The client is closed
|
||||
2023-05-28 21:26:02 [31merror[39m [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 [32minfo[39m [redis] Redis module initialized
|
||||
2023-05-28 21:26:30 [32minfo[39m [server] Server running at :::7477
|
||||
2023-05-28 21:26:37 [33mwarn[39m [error] Failed to fulfill request to /api/session: Username or password incorrect
|
||||
2023-05-28 21:26:37 [31merror[39m [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 [32minfo[39m [redis] Redis module initialized
|
||||
2023-05-28 21:51:25 [32minfo[39m [redis] Redis module initialized
|
||||
2023-05-28 21:51:25 [32minfo[39m [server] Server running at 0.0.0.0:7477
|
||||
2023-05-28 21:55:59 [32minfo[39m [redis] Redis module initialized
|
||||
2023-05-28 21:55:59 [32minfo[39m [server] Server running at 0.0.0.0:7477
|
||||
2023-05-28 21:56:12 [32minfo[39m [redis] Redis module initialized
|
||||
2023-05-28 21:56:12 [32minfo[39m [server] Server running at 0.0.0.0:7477
|
||||
2023-05-28 22:01:38 [32minfo[39m [redis] Redis module initialized
|
||||
2023-05-28 22:01:39 [32minfo[39m [server] Server running at 0.0.0.0:7477
|
||||
2023-05-28 22:05:13 [32minfo[39m [redis] Redis module initialized
|
||||
2023-05-28 22:05:13 [32minfo[39m [server] Server running at 0.0.0.0:7477
|
||||
2023-05-28 22:06:29 [32minfo[39m [redis] Redis module initialized
|
||||
2023-05-28 22:06:30 [32minfo[39m [server] Server running at 0.0.0.0:7477
|
||||
2023-05-28 22:09:37 [32minfo[39m [redis] Redis module initialized
|
||||
2023-05-28 22:09:37 [32minfo[39m [server] Server running at 0.0.0.0:7477
|
||||
2023-05-28 22:10:01 [32minfo[39m [redis] Redis module initialized
|
||||
2023-05-28 22:10:01 [32minfo[39m [server] Server running at 0.0.0.0:7477
|
||||
2023-05-28 23:17:39 [32minfo[39m [redis] Redis module initialized
|
||||
2023-05-28 23:17:39 [32minfo[39m [server] Server running at 0.0.0.0:7477
|
||||
2023-05-28 23:19:24 [32minfo[39m [redis] Redis module initialized
|
||||
2023-05-28 23:19:25 [32minfo[39m [server] Server running at 0.0.0.0:7477
|
||||
2023-05-28 23:20:09 [32minfo[39m [redis] Redis module initialized
|
||||
2023-05-28 23:20:09 [32minfo[39m [server] Server running at 0.0.0.0:7477
|
||||
2023-05-28 23:37:14 [32minfo[39m [redis] Redis module initialized
|
||||
2023-05-28 23:37:14 [32minfo[39m [server] Server running at 0.0.0.0:7477
|
||||
2023-05-28 23:42:22 [32minfo[39m [redis] Redis module initialized
|
||||
2023-05-28 23:42:22 [32minfo[39m [server] Server running at 0.0.0.0:7477
|
||||
2023-05-28 23:49:02 [32minfo[39m [redis] Redis module initialized
|
||||
2023-05-28 23:49:02 [32minfo[39m [server] Server running at 0.0.0.0:7477
|
||||
2023-05-28 23:49:03 [33mwarn[39m [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 [31merror[39m [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 [33mwarn[39m [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 [31merror[39m [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 [32minfo[39m [redis] Redis module initialized
|
||||
2023-05-28 23:49:20 [32minfo[39m [server] Server running at 0.0.0.0:7477
|
||||
2023-05-28 23:49:21 [33mwarn[39m [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 [31merror[39m [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 [32minfo[39m [redis] Redis module initialized
|
||||
2023-05-28 23:50:22 [32minfo[39m [server] Server running at 0.0.0.0:7477
|
||||
2023-05-28 23:54:06 [32minfo[39m [redis] Redis module initialized
|
||||
2023-05-28 23:54:06 [32minfo[39m [server] Server running at 0.0.0.0:7477
|
||||
2023-05-28 23:55:47 [32minfo[39m [redis] Redis module initialized
|
||||
2023-05-28 23:55:47 [32minfo[39m [server] Server running at 0.0.0.0:7477
|
||||
2023-05-28 23:56:21 [32minfo[39m [redis] Redis module initialized
|
||||
2023-05-28 23:56:22 [32minfo[39m [server] Server running at 0.0.0.0:7477
|
||||
2023-05-28 23:57:23 [32minfo[39m [redis] Redis module initialized
|
||||
2023-05-28 23:57:24 [32minfo[39m [server] Server running at 0.0.0.0:7477
|
||||
2023-05-28 23:59:13 [32minfo[39m [redis] Redis module initialized
|
||||
2023-05-28 23:59:14 [32minfo[39m [server] Server running at 0.0.0.0:7477
|
||||
28
log/2023-05-29.log
Normal file
28
log/2023-05-29.log
Normal file
@@ -0,0 +1,28 @@
|
||||
2023-05-29 00:03:09 [32minfo[39m [redis] Redis module initialized
|
||||
2023-05-29 00:03:09 [32minfo[39m [server] Server running at 0.0.0.0:7477
|
||||
2023-05-29 00:03:23 [32minfo[39m [redis] Redis module initialized
|
||||
2023-05-29 00:03:24 [32minfo[39m [server] Server running at 0.0.0.0:7477
|
||||
2023-05-29 00:06:47 [32minfo[39m [redis] Redis module initialized
|
||||
2023-05-29 00:06:48 [32minfo[39m [server] Server running at 0.0.0.0:7477
|
||||
2023-05-29 00:11:09 [32minfo[39m [redis] Redis module initialized
|
||||
2023-05-29 00:11:10 [32minfo[39m [server] Server running at 0.0.0.0:7477
|
||||
2023-05-29 00:11:27 [32minfo[39m [redis] Redis module initialized
|
||||
2023-05-29 00:11:27 [32minfo[39m [server] Server running at 0.0.0.0:7477
|
||||
2023-05-29 00:13:14 [32minfo[39m [redis] Redis module initialized
|
||||
2023-05-29 00:13:14 [32minfo[39m [server] Server running at 0.0.0.0:7477
|
||||
2023-05-29 00:14:55 [32minfo[39m [redis] Redis module initialized
|
||||
2023-05-29 00:14:55 [32minfo[39m [server] Server running at 0.0.0.0:7477
|
||||
2023-05-29 00:15:56 [32minfo[39m [redis] Redis module initialized
|
||||
2023-05-29 00:15:56 [32minfo[39m [server] Server running at 0.0.0.0:7477
|
||||
2023-05-29 00:23:49 [32minfo[39m [redis] Redis module initialized
|
||||
2023-05-29 00:23:49 [32minfo[39m [server] Server running at 0.0.0.0:7477
|
||||
2023-05-29 00:24:16 [32minfo[39m [redis] Redis module initialized
|
||||
2023-05-29 00:24:17 [32minfo[39m [server] Server running at 0.0.0.0:7477
|
||||
2023-05-29 00:32:13 [32minfo[39m [redis] Redis module initialized
|
||||
2023-05-29 00:32:13 [32minfo[39m [server] Server running at 0.0.0.0:7477
|
||||
2023-05-29 00:34:52 [32minfo[39m [redis] Redis module initialized
|
||||
2023-05-29 00:34:53 [32minfo[39m [server] Server running at 0.0.0.0:7477
|
||||
2023-05-29 00:35:22 [32minfo[39m [redis] Redis module initialized
|
||||
2023-05-29 00:35:22 [32minfo[39m [server] Server running at 0.0.0.0:7477
|
||||
2023-05-29 00:42:59 [32minfo[39m [redis] Redis module initialized
|
||||
2023-05-29 00:42:59 [32minfo[39m [server] Server running at 0.0.0.0:7477
|
||||
97
migrations/20230513004141_init.js
Normal file
97
migrations/20230513004141_init.js
Normal 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
13275
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
64
package.json
Normal file
64
package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
177
pages/account/create.page.vue
Normal file
177
pages/account/create.page.vue
Normal 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>
|
||||
142
pages/account/login.page.vue
Normal file
142
pages/account/login.page.vue
Normal 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>
|
||||
7
pages/help/user-agreement.page.md
Normal file
7
pages/help/user-agreement.page.md
Normal 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
8
pages/index/Counter.vue
Normal 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>
|
||||
25
pages/index/index.page.vue
Normal file
25
pages/index/index.page.vue
Normal 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>
|
||||
12
pages/index/indexbackup.page.vue
Normal file
12
pages/index/indexbackup.page.vue
Normal 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>
|
||||
71
pages/shelf/@id/index.page.vue
Normal file
71
pages/shelf/@id/index.page.vue
Normal 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
201
pages/shelf/create.page.vue
Normal 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
21
renderer/Link.vue
Normal 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
47
renderer/PageShell.vue
Normal 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>
|
||||
15
renderer/_default.page.client.js
Normal file
15
renderer/_default.page.client.js
Normal 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 };
|
||||
80
renderer/_default.page.server.js
Normal file
80
renderer/_default.page.server.js
Normal 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
14
renderer/_error.page.vue
Normal 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
30
renderer/app.js
Normal 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
127
renderer/container.vue
Normal 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
36
renderer/logo.svg
Normal 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 |
19
renderer/usePageContext.js
Normal file
19
renderer/usePageContext.js
Normal 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
36
src/.eslintrc
Executable 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
11
src/cli.js
Executable 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
12
src/errors.js
Executable 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
10
src/knex.js
Executable 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
35
src/logger.js
Executable 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
14
src/redis.js
Normal 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
17
src/shelves.js
Normal 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
147
src/users.js
Normal 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
32
src/web/default.js
Normal 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
22
src/web/error.js
Executable 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
18
src/web/ip.js
Normal 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
77
src/web/server.js
Normal 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
13
src/web/shelves.js
Normal 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
33
src/web/users.js
Normal 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
18
stores/user.js
Normal 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
23
vite.config.js
Normal 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,
|
||||
}),
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user