Initial commit, basic pages and sessions.
This commit is contained in:
commit
bc9fec207b
|
@ -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
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
config/
|
||||||
|
!config/default.js
|
||||||
|
assets/js/config/
|
||||||
|
!assets/js/config/default.js
|
|
@ -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;
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -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 |
|
@ -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 |
Binary file not shown.
After Width: | Height: | Size: 17 KiB |
|
@ -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);
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
// centralize navigation to simplify switching between client and server routing
|
||||||
|
export default function navigate(path) {
|
||||||
|
window.location.href = path;
|
||||||
|
}
|
|
@ -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>
|
|
@ -0,0 +1,6 @@
|
||||||
|
const config = require('config');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
client: 'pg',
|
||||||
|
connection: config.database,
|
||||||
|
};
|
|
@ -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
|
|
@ -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)
|
|
@ -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
|
|
@ -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
|
|
@ -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');
|
||||||
|
};
|
File diff suppressed because it is too large
Load Diff
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>
|
|
@ -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>
|
|
@ -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.
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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 };
|
|
@ -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'];
|
|
@ -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>
|
|
@ -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 };
|
|
@ -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>
|
|
@ -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 |
|
@ -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 };
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
|
@ -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 };
|
|
@ -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',
|
||||||
|
});
|
|
@ -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',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
};
|
|
@ -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,
|
||||||
|
};
|
|
@ -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,
|
||||||
|
};
|
|
@ -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,
|
||||||
|
};
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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();
|
|
@ -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,
|
||||||
|
};
|
|
@ -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,
|
||||||
|
};
|
|
@ -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,
|
||||||
|
},
|
||||||
|
});
|
|
@ -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,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
};
|
Loading…
Reference in New Issue