From ec1135cafe86fc9a9aef4a241bdccf22d96c799b Mon Sep 17 00:00:00 2001 From: Akis Date: Tue, 27 Dec 2022 16:44:22 +0200 Subject: [PATCH] blog api --- .gitignore | 2 +- config/config.example.yml | 3 +- package.json | 4 +- pnpm-lock.yaml | 296 ++++++++++++++++++++++++-- scripts/build.ts | 20 +- src/apis/announcements.ts | 25 +-- src/apis/blog.ts | 434 ++++++++++++++++++++++++++++++++++++++ src/apis/form.ts | 67 +++--- src/apis/status.ts | 8 +- src/apis/user.ts | 144 +++++++++---- src/index.ts | 36 +++- src/templates/blog.hbs | 53 +++++ src/templates/index.hbs | 27 ++- src/templates/layout.hbs | 31 ++- src/templates/user.hbs | 214 +++++++++++-------- src/utils/config.ts | 1 + src/utils/db.ts | 18 +- src/utils/fetchStatus.ts | 3 +- tsconfig.json | 7 +- 19 files changed, 1154 insertions(+), 239 deletions(-) create mode 100644 src/apis/blog.ts create mode 100644 src/templates/blog.hbs diff --git a/.gitignore b/.gitignore index 59affa9..da5fb0d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ node_modules config/config.yml .env -src/main.js \ No newline at end of file +dist \ No newline at end of file diff --git a/config/config.example.yml b/config/config.example.yml index 6fe8dc2..9741762 100644 --- a/config/config.example.yml +++ b/config/config.example.yml @@ -6,10 +6,11 @@ db: app: port: 6893 hcaptcha: - secret: yourhcaptchasecret + secret: "yourhcaptchasecret" # must be in quotes sitekey: yourhcaptchasitekey webhook: yourdiscordwebhookurl state: announcements: true form: true status: true + blog: true diff --git a/package.json b/package.json index 6028107..5d6bbf8 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "scripts": { "dev": "tsx watch src/index.ts", "build": "tsx scripts/build.ts", - "preview": "node src/main.js", + "preview": "node dist/index.js", "preview:tsx": "tsx src/index.ts", "format": "prettier --write .", "check": "tsc --noEmit" @@ -37,6 +37,8 @@ "@types/node": "^18.11.9", "@types/request-ip": "^0.0.37", "esbuild": "^0.16.6", + "esbuild-plugin-clean": "^1.0.1", + "esbuild-plugin-copy": "^2.0.1", "prettier": "^2.7.1", "tsx": "^3.12.1", "typescript": "^4.9.3" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7cf2c03..026ff73 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,6 +14,8 @@ specifiers: chalk: ^5.1.2 discord-webhook-node: ^1.1.8 esbuild: ^0.16.6 + esbuild-plugin-clean: ^1.0.1 + esbuild-plugin-copy: ^2.0.1 fastify: ^4.9.2 handlebars: ^4.7.7 hcaptcha: ^0.1.1 @@ -50,6 +52,8 @@ devDependencies: '@types/node': 18.11.9 '@types/request-ip': 0.0.37 esbuild: 0.16.6 + esbuild-plugin-clean: 1.0.1_esbuild@0.16.6 + esbuild-plugin-copy: 2.0.1_esbuild@0.16.6 prettier: 2.7.1 tsx: 3.12.1 typescript: 4.9.3 @@ -1178,6 +1182,27 @@ packages: - supports-color dev: false + /@nodelib/fs.scandir/2.1.5: + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + dev: true + + /@nodelib/fs.stat/2.0.5: + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + dev: true + + /@nodelib/fs.walk/1.2.8: + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.13.0 + dev: true + /@sideway/address/4.1.4: resolution: {integrity: sha512-7vwq+rOHVWjyXxVlR76Agnvhy8I9rpzjosTESvmhNeXOXdZZB15Fl+TI9x1SiHZH5Jv2wTGduSxFDIaq0m3DUw==} dependencies: @@ -1246,6 +1271,14 @@ packages: - supports-color dev: false + /aggregate-error/3.1.0: + resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} + engines: {node: '>=8'} + dependencies: + clean-stack: 2.2.0 + indent-string: 4.0.0 + dev: true + /ajv-formats/2.1.1: resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} peerDependenciesMeta: @@ -1269,6 +1302,13 @@ packages: engines: {node: '>=8'} dev: false + /ansi-styles/4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + dependencies: + color-convert: 2.0.1 + dev: true + /aproba/2.0.0: resolution: {integrity: sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==} dev: false @@ -1285,6 +1325,11 @@ packages: readable-stream: 3.6.0 dev: false + /array-union/2.1.0: + resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} + engines: {node: '>=8'} + dev: true + /asynckit/0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} dev: false @@ -1316,7 +1361,6 @@ packages: /balanced-match/1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - dev: false /base64-js/1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} @@ -1344,7 +1388,13 @@ packages: dependencies: balanced-match: 1.0.2 concat-map: 0.0.1 - dev: false + + /braces/3.0.2: + resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} + engines: {node: '>=8'} + dependencies: + fill-range: 7.0.1 + dev: true /bson/4.7.0: resolution: {integrity: sha512-VrlEE4vuiO1WTpfof4VmaVolCVYkYTgB9iWgYNOrVlnifpME/06fhFRmONgBhClD5pFC1t9ZWqFUQEQAzY43bA==} @@ -1371,6 +1421,14 @@ packages: ieee754: 1.2.1 dev: false + /chalk/4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + dev: true + /chalk/5.1.2: resolution: {integrity: sha512-E5CkT4jWURs1Vy5qGJye+XwCkNj7Od3Af7CP6SujMetSMkLs8Do2RWJK5yx1wamHV/op8Rz+9rltjaTQWDnEFQ==} engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} @@ -1381,6 +1439,22 @@ packages: engines: {node: '>=10'} dev: false + /clean-stack/2.2.0: + resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} + engines: {node: '>=6'} + dev: true + + /color-convert/2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + dependencies: + color-name: 1.1.4 + dev: true + + /color-name/1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + dev: true + /color-support/1.1.3: resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==} hasBin: true @@ -1395,7 +1469,6 @@ packages: /concat-map/0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - dev: false /console-control-strings/1.1.0: resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==} @@ -1418,6 +1491,20 @@ packages: ms: 2.1.2 dev: false + /del/6.1.1: + resolution: {integrity: sha512-ua8BhapfP0JUJKC/zV9yHHDW/rDoDxP4Zhn3AkA6/xT6gY7jYXJiaeyBZznYVujhZZET+UgcbZiQ7sN3WqcImg==} + engines: {node: '>=10'} + dependencies: + globby: 11.1.0 + graceful-fs: 4.2.10 + is-glob: 4.0.3 + is-path-cwd: 2.2.0 + is-path-inside: 3.0.3 + p-map: 4.0.0 + rimraf: 3.0.2 + slash: 3.0.0 + dev: true + /delayed-stream/1.0.0: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} @@ -1437,6 +1524,13 @@ packages: engines: {node: '>=8'} dev: false + /dir-glob/3.0.1: + resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} + engines: {node: '>=8'} + dependencies: + path-type: 4.0.0 + dev: true + /discord-webhook-node/1.1.8: resolution: {integrity: sha512-3u0rrwywwYGc6HrgYirN/9gkBYqmdpvReyQjapoXARAHi0P0fIyf3W5tS5i3U3cc7e44E+e7dIHYUeec7yWaug==} dependencies: @@ -1594,6 +1688,27 @@ packages: dev: true optional: true + /esbuild-plugin-clean/1.0.1_esbuild@0.16.6: + resolution: {integrity: sha512-ul606g0wX6oeobBgi3EqpZtCBCwNwCDivvnshsNS5pUsRylKoxUnDqK0ZIyPinlMbP6s8Opc9y2zOeY1Plhe8Q==} + peerDependencies: + esbuild: '>= 0.14.0' + dependencies: + chalk: 4.1.2 + del: 6.1.1 + esbuild: 0.16.6 + dev: true + + /esbuild-plugin-copy/2.0.1_esbuild@0.16.6: + resolution: {integrity: sha512-/mvriqGv2QAyrkui3REZaLEjwqESBKWZQQJtOZEausI8C4QMChREXGASNzmWpTlHo/v+ipLW73QCiNemBKggMw==} + peerDependencies: + esbuild: '>= 0.14.0' + dependencies: + chalk: 4.1.2 + esbuild: 0.16.6 + fs-extra: 10.1.0 + globby: 11.1.0 + dev: true + /esbuild-sunos-64/0.15.14: resolution: {integrity: sha512-DNVjSp/BY4IfwtdUAvWGIDaIjJXY5KI4uD82+15v6k/w7px9dnaDaJJ2R6Mu+KCgr5oklmFc0KjBjh311Gxl9Q==} engines: {node: '>=12'} @@ -1708,6 +1823,17 @@ packages: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} dev: false + /fast-glob/3.2.12: + resolution: {integrity: sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==} + engines: {node: '>=8.6.0'} + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.5 + dev: true + /fast-json-stringify/5.4.0: resolution: {integrity: sha512-PIzon53oX/zEGLrGbu4DpfNcYiV4K4rk+JsVrawRPO/G8cNBEMZ3KlIk2BCGqN+m1KCCA4zt5E7Hh3GG9ojRVA==} dependencies: @@ -1771,7 +1897,13 @@ packages: resolution: {integrity: sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==} dependencies: reusify: 1.0.4 - dev: false + + /fill-range/7.0.1: + resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} + engines: {node: '>=8'} + dependencies: + to-regex-range: 5.0.1 + dev: true /find-my-way/7.3.1: resolution: {integrity: sha512-kGvM08SOkqvheLcuQ8GW9t/H901Qb9rZEbcNWbXopzy4jDRoaJpJoObPSKf4MnQLZ20ZTp7rL5MpF6rf+pqmyg==} @@ -1815,6 +1947,15 @@ packages: engines: {node: '>= 0.6'} dev: false + /fs-extra/10.1.0: + resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} + engines: {node: '>=12'} + dependencies: + graceful-fs: 4.2.10 + jsonfile: 6.1.0 + universalify: 2.0.0 + dev: true + /fs-minipass/2.1.0: resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==} engines: {node: '>= 8'} @@ -1824,7 +1965,6 @@ packages: /fs.realpath/1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} - dev: false /fsevents/2.3.2: resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} @@ -1853,6 +1993,13 @@ packages: resolution: {integrity: sha512-X8u8fREiYOE6S8hLbq99PeykTDoLVnxvF4DjWKJmz9xy2nNRdUcV8ZN9tniJFeKyTU3qnC9lL8n4Chd6LmVKHg==} dev: true + /glob-parent/5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + dependencies: + is-glob: 4.0.3 + dev: true + /glob/7.1.6: resolution: {integrity: sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==} dependencies: @@ -1862,7 +2009,22 @@ packages: minimatch: 3.1.2 once: 1.4.0 path-is-absolute: 1.0.1 - dev: false + + /globby/11.1.0: + resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} + engines: {node: '>=10'} + dependencies: + array-union: 2.1.0 + dir-glob: 3.0.1 + fast-glob: 3.2.12 + ignore: 5.2.4 + merge2: 1.4.1 + slash: 3.0.0 + dev: true + + /graceful-fs/4.2.10: + resolution: {integrity: sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==} + dev: true /handlebars/4.7.7: resolution: {integrity: sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA==} @@ -1877,6 +2039,11 @@ packages: uglify-js: 3.17.4 dev: false + /has-flag/4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + dev: true + /has-unicode/2.0.1: resolution: {integrity: sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==} dev: false @@ -1914,16 +2081,24 @@ packages: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} dev: false + /ignore/5.2.4: + resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==} + engines: {node: '>= 4'} + dev: true + + /indent-string/4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} + dev: true + /inflight/1.0.6: resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} dependencies: once: 1.4.0 wrappy: 1.0.2 - dev: false /inherits/2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - dev: false /ip/2.0.0: resolution: {integrity: sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==} @@ -1940,11 +2115,38 @@ packages: hasBin: true dev: false + /is-extglob/2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + dev: true + /is-fullwidth-code-point/3.0.0: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} dev: false + /is-glob/4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + dependencies: + is-extglob: 2.1.1 + dev: true + + /is-number/7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + dev: true + + /is-path-cwd/2.2.0: + resolution: {integrity: sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==} + engines: {node: '>=6'} + dev: true + + /is-path-inside/3.0.3: + resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} + engines: {node: '>=8'} + dev: true + /joi/17.7.0: resolution: {integrity: sha512-1/ugc8djfn93rTE3WRKdCzGGt/EtiYKxITMO4Wiv6q5JL1gl9ePt4kBsl1S499nbosspfctIQTpYIhSmHA3WAg==} dependencies: @@ -1959,6 +2161,14 @@ packages: resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} dev: false + /jsonfile/6.1.0: + resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} + dependencies: + universalify: 2.0.0 + optionalDependencies: + graceful-fs: 4.2.10 + dev: true + /light-my-request/5.6.1: resolution: {integrity: sha512-sbJnC1UBRivi9L1kICr3CESb82pNiPNB3TvtdIrZZqW0Qh8uDXvoywMmWKZlihDcmw952CMICCzM+54LDf+E+g==} dependencies: @@ -1991,6 +2201,19 @@ packages: dev: false optional: true + /merge2/1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + dev: true + + /micromatch/4.0.5: + resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==} + engines: {node: '>=8.6'} + dependencies: + braces: 3.0.2 + picomatch: 2.3.1 + dev: true + /mime-db/1.52.0: resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} engines: {node: '>= 0.6'} @@ -2007,7 +2230,6 @@ packages: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} dependencies: brace-expansion: 1.1.11 - dev: false /minimist/1.2.7: resolution: {integrity: sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==} @@ -2130,12 +2352,27 @@ packages: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} dependencies: wrappy: 1.0.2 - dev: false + + /p-map/4.0.0: + resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==} + engines: {node: '>=10'} + dependencies: + aggregate-error: 3.1.0 + dev: true /path-is-absolute/1.0.1: resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} engines: {node: '>=0.10.0'} - dev: false + + /path-type/4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + dev: true + + /picomatch/2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + dev: true /pino-abstract-transport/1.0.0: resolution: {integrity: sha512-c7vo5OpW4wIS42hUVcT5REsL8ZljsUfBjqV/e2sFxmFEFZiq1XLUp5EYLtuDH6PEHq9W1egWqRbnLUP5FuZmOA==} @@ -2197,6 +2434,10 @@ packages: engines: {node: '>=6'} dev: false + /queue-microtask/1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + dev: true + /quick-format-unescaped/4.0.4: resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} dev: false @@ -2242,7 +2483,6 @@ packages: /reusify/1.0.4: resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - dev: false /rfdc/1.3.0: resolution: {integrity: sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==} @@ -2253,7 +2493,12 @@ packages: hasBin: true dependencies: glob: 7.1.6 - dev: false + + /run-parallel/1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + dependencies: + queue-microtask: 1.2.3 + dev: true /safe-buffer/5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} @@ -2312,6 +2557,11 @@ packages: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} dev: false + /slash/3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + dev: true + /smart-buffer/4.2.0: resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} @@ -2386,6 +2636,13 @@ packages: dev: false optional: true + /supports-color/7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + dependencies: + has-flag: 4.0.0 + dev: true + /tar/6.1.13: resolution: {integrity: sha512-jdIBIN6LTIe2jqzay/2vtYLlBHa3JF42ot3h1dW8Q0PaAG4v8rm0cvpVePtau5C6OKXGGcgO9q2AMNSWxiLqKw==} engines: {node: '>=10'} @@ -2409,6 +2666,13 @@ packages: engines: {node: '>=6'} dev: false + /to-regex-range/5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + dependencies: + is-number: 7.0.0 + dev: true + /toidentifier/1.0.1: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} @@ -2468,6 +2732,11 @@ packages: dev: false optional: true + /universalify/2.0.0: + resolution: {integrity: sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==} + engines: {node: '>= 10.0.0'} + dev: true + /uri-js/4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} dependencies: @@ -2525,7 +2794,6 @@ packages: /wrappy/1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - dev: false /yallist/4.0.0: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} diff --git a/scripts/build.ts b/scripts/build.ts index 1394a59..db0858a 100644 --- a/scripts/build.ts +++ b/scripts/build.ts @@ -1,6 +1,8 @@ import { build } from "esbuild"; import { join, dirname } from "path"; import { fileURLToPath } from "url"; +import { copy } from "esbuild-plugin-copy"; +import { clean } from "esbuild-plugin-clean"; const __dirname = dirname(fileURLToPath(import.meta.url)); @@ -11,6 +13,20 @@ await build({ format: "esm", entryPoints: ["src/index.ts"], platform: "node", - outfile: "src/main.js", - packages: "external" + outdir: "dist", + packages: "external", + plugins: [ + copy({ + resolveFrom: join(__dirname, ".."), + assets: [ + { + from: ["src/templates/**/*"], + to: ["dist/templates"] + } + ] + }), + clean({ + patterns: ["dist"] + }) + ] }); diff --git a/src/apis/announcements.ts b/src/apis/announcements.ts index 45bfcaa..6956b5f 100644 --- a/src/apis/announcements.ts +++ b/src/apis/announcements.ts @@ -10,28 +10,28 @@ const announcementsApi = (fastify: FastifyInstance) => { log("The announcements api is disabled.", "warning"); fastify.get( "/tools/announcements", - (request: FastifyRequest, reply: FastifyReply) => { + (_request: FastifyRequest, reply: FastifyReply) => { reply.send("The announcements api is disabled."); } ); fastify.get( "/api/v1/state/announcements", - (request: FastifyRequest, reply: FastifyReply) => { + (_request: FastifyRequest, reply: FastifyReply) => { reply.send({ enabled: false }); } ); fastify.get( "/api/v1/announcements", - (request: FastifyRequest, reply: FastifyReply) => { + (_request: FastifyRequest, reply: FastifyReply) => { reply.send("The announcements api is disabled."); } ); } else { fastify.get( "/tools/announcements", - (request: FastifyRequest, reply: FastifyReply) => { + (_request: FastifyRequest, reply: FastifyReply) => { reply.view("announcements", { title: "announcement command centre" }); @@ -40,7 +40,7 @@ const announcementsApi = (fastify: FastifyInstance) => { fastify.get( "/api/v1/state/announcements", - (request: FastifyRequest, reply: FastifyReply) => { + (_request: FastifyRequest, reply: FastifyReply) => { reply.send({ enabled: true }); } ); @@ -90,14 +90,9 @@ const setAnnouncements = async ( request: FastifyRequest<{ Body: BodyType }>, reply: FastifyReply ) => { - if (userMap.get("isLoggedIn")) { - if ( - BodyTypeSchema.validate(request.body).error - ) { - reply.badRequest( - `${BodyTypeSchema.validate(request.body).error}` - ); + if (BodyTypeSchema.validate(request.body).error) { + reply.badRequest(`${BodyTypeSchema.validate(request.body).error}`); } else { const collection = db.collection("announcements"); @@ -121,10 +116,9 @@ const setAnnouncements = async ( }; const deleteAnnouncements = async ( - request: FastifyRequest<{ Body: BodyType }>, + _request: FastifyRequest<{ Body: BodyType }>, reply: FastifyReply ) => { - if (userMap.get("isLoggedIn")) { const collection = db.collection("announcements"); @@ -139,10 +133,9 @@ const deleteAnnouncements = async ( }; const readAnnouncements = async ( - request: FastifyRequest, + _request: FastifyRequest, reply: FastifyReply ) => { - const collection = db.collection("announcements"); await collection diff --git a/src/apis/blog.ts b/src/apis/blog.ts new file mode 100644 index 0000000..2323898 --- /dev/null +++ b/src/apis/blog.ts @@ -0,0 +1,434 @@ +import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify"; +import log from "../utils/logUtil"; +import { db } from "../utils/db"; +import { userMap } from "./user"; +import config from "../utils/config"; +import Joi from "joi"; + +const blogApi = (fastify: FastifyInstance) => { + if (!config.app.state.blog) { + log("The blog api is disabled.", "warning"); + fastify.get( + "/tools/blog", + (_request: FastifyRequest, reply: FastifyReply) => { + reply.send("The blog api is disabled."); + } + ); + + fastify.get( + "/api/v1/state/blog", + (_request: FastifyRequest, reply: FastifyReply) => { + reply.send({ enabled: false }); + } + ); + + fastify.get( + "/api/v1/blog", + (_request: FastifyRequest, reply: FastifyReply) => { + reply.send("The blog api is disabled."); + } + ); + + fastify.get( + "/api/v1/blog/*", + (_request: FastifyRequest, reply: FastifyReply) => { + reply.send("The blog api is disabled."); + } + ); + } else { + fastify.get( + "/tools/blog", + async (_request: FastifyRequest, reply: FastifyReply) => { + const collection = db.collection("blog"); + + const postTitles = new Promise((resolve, reject) => { + collection + .find({}) + .toArray() + .then((docs) => { + const titles = docs.map((doc) => doc["title"]); + resolve(titles); + }) + .catch((err) => reject(err)); + }); + + return await reply.view("blog", { + title: "blog command centre", + isLoggedIn: userMap.get("isLoggedIn"), + postTitles: await postTitles + }); + } + ); + + fastify.get( + "/api/v1/state/blog", + (_request: FastifyRequest, reply: FastifyReply) => { + reply.send({ enabled: true }); + } + ); + + fastify.get( + "/api/v1/blog", + (request: FastifyRequest, reply: FastifyReply) => { + getPosts(request, reply); + } + ); + fastify.post( + "/api/v1/blog/add", + ( + request: FastifyRequest<{ Body: AddPostType }>, + reply: FastifyReply + ) => { + addPost(request, reply); + } + ); + fastify.post( + "/api/v1/blog/delete", + ( + request: FastifyRequest<{ Body: DeletePostType }>, + reply: FastifyReply + ) => { + deletePost(request, reply); + } + ); + fastify.post( + "/api/v1/blog/edit", + ( + request: FastifyRequest<{ Body: EditPostType }>, + reply: FastifyReply + ) => { + editPost(request, reply); + } + ); + fastify.get( + "/api/v1/blog/tags", + (request: FastifyRequest, reply: FastifyReply) => { + getTags(request, reply); + } + ); + fastify.get( + "/api/v1/blog/tags/:tag", + ( + request: FastifyRequest<{ Params: GetPostsByTagType }>, + reply: FastifyReply + ) => { + getPostsByTag(request, reply); + } + ); + fastify.get( + "/api/v1/blog/authors", + (request: FastifyRequest, reply: FastifyReply) => { + getAuthors(request, reply); + } + ); + fastify.get( + "/api/v1/blog/authors/:author", + ( + request: FastifyRequest<{ Params: GetPostsByAuthorType }>, + reply: FastifyReply + ) => { + getPostsByAuthor(request, reply); + } + ); + fastify.get( + "/api/v1/blog/:title", + ( + request: FastifyRequest<{ Params: GetPostsByTitleType }>, + reply: FastifyReply + ) => { + getPostsByTitle(request, reply); + } + ); + } +}; + +interface AddPostType { + title: string; + content: string; + tags?: string; + author: string; +} + +const AddPostTypeSchema = Joi.object({ + title: Joi.string().required(), + content: Joi.string().required(), + tags: Joi.string().optional().allow(""), + author: Joi.string().required() +}); + +const addPost = async ( + request: FastifyRequest<{ Body: AddPostType }>, + reply: FastifyReply +) => { + if (userMap.get("isLoggedIn")) { + if (AddPostTypeSchema.validate(request.body).error) { + reply.badRequest( + `${AddPostTypeSchema.validate(request.body).error}` + ); + } else { + const collection = db.collection("blog"); + + const now = Math.floor(Date.now() / 1000); + const data = { + title: request.body.title, + content: request.body.content, + tags: request.body.tags ? request.body.tags.split(" ") : [], + author: request.body.author, + created: now, + words: request.body.content.trim().split(/\s+/).length, + readingTime: Math.ceil( + request.body.content.trim().split(/\s+/).length / 225 + ) + }; + + if (await collection.findOne({ title: request.body.title })) { + reply.conflict("Title already exists."); + } else { + await collection.insertOne(data); + + reply.send( + "Your post has been posted." + JSON.stringify(request.body) + ); + } + } + } else { + reply.unauthorized("You need to log in in order to post a post."); + } +}; + +interface DeletePostType { + title: string; +} + +const deletePost = async ( + request: FastifyRequest<{ Body: DeletePostType }>, + reply: FastifyReply +) => { + if (userMap.get("isLoggedIn")) { + const collection = db.collection("blog"); + + await collection + .deleteOne({ title: request.body.title }) + .then((data) => { + if (data.deletedCount === 0) { + reply.notFound("Post not found."); + } else { + reply.send("Post deleted."); + } + }); + } else { + reply.unauthorized("You need to log in in order to delete a post."); + } +}; + +interface EditPostType { + title: string; + newTitle?: string; + content?: string; + tags?: string; + area: "title" | "content" | "tags"; +} + +const EditPostTypeSchema = Joi.object({ + title: Joi.string().required(), + newTitle: Joi.string().optional().allow(""), + content: Joi.string().optional().allow(""), + tags: Joi.string().optional().allow(""), + area: Joi.string().required().allow("title", "content", "tags") +}); + +const editPost = async ( + request: FastifyRequest<{ Body: EditPostType }>, + reply: FastifyReply +) => { + if (userMap.get("isLoggedIn")) { + if (EditPostTypeSchema.validate(request.body).error) { + reply.badRequest( + `${EditPostTypeSchema.validate(request.body).error}` + ); + } else { + if (request.body.area === "title") { + const collection = db.collection("blog"); + + await collection + .updateOne( + { title: request.body.title }, + { $set: { title: request.body.newTitle } } + ) + .then((data) => { + if (data.modifiedCount === 0) { + reply.notFound( + "Post not found." + JSON.stringify(request.body) + ); + } else { + reply.send("Post edited."); + } + }); + } else if (request.body.area === "content") { + const collection = db.collection("blog"); + + const now = Math.floor(Date.now() / 1000); + + await collection + .updateOne( + { title: request.body.title }, + { + $set: { + content: request.body.content, + updated: now, + words: request.body.content!.trim().split(/\s+/) + .length + } + } + ) + .then((data) => { + if (data.modifiedCount === 0) { + reply.notFound("Post not found."); + } else { + reply.send("Post edited."); + } + }); + } else if (request.body.area === "tags") { + const collection = db.collection("blog"); + + await collection + .updateOne( + { title: request.body.title }, + { + $set: { + tags: request.body.tags + ? request.body.tags.split(" ") + : [] + } + } + ) + .then((data) => { + if (data.modifiedCount === 0) { + reply.notFound("Post not found."); + } else { + reply.send("Post edited."); + } + }); + } + } + } else { + reply.unauthorized("You need to log in in order to edit a post."); + } +}; + +const getPosts = async (_request: FastifyRequest, reply: FastifyReply) => { + const collection = db.collection("blog"); + + await collection + .find({}, { projection: { _id: false } }) + .toArray() + .then((data) => { + if (data.length === 0 || data[0] === undefined) { + reply.notFound("There are no blog posts."); + } else { + reply.send(data.sort((a, b) => b["created"] - a["created"])); + } + }); +}; + +interface GetPostsByTagType { + tag: string; +} + +const getPostsByTag = async ( + request: FastifyRequest<{ Params: GetPostsByTagType }>, + reply: FastifyReply +) => { + const collection = db.collection("blog"); + + await collection + .find({ tags: request.params.tag }, { projection: { _id: false } }) + .toArray() + .then((data) => { + if (data.length === 0 || data[0] === undefined) { + reply.notFound("There are no blog posts with that tag."); + } else { + reply.send(data); + } + }); +}; + +interface GetPostsByAuthorType { + author: string; +} + +const getPostsByAuthor = async ( + request: FastifyRequest<{ Params: GetPostsByAuthorType }>, + reply: FastifyReply +) => { + const collection = db.collection("blog"); + + await collection + .find({ author: request.params.author }, { projection: { _id: false } }) + .toArray() + .then((data) => { + if (data.length === 0 || data[0] === undefined) { + reply.notFound("There are no blog posts with that author."); + } else { + reply.send(data); + } + }); +}; + +interface GetPostsByTitleType { + title: string; +} + +const getPostsByTitle = async ( + request: FastifyRequest<{ Params: GetPostsByTitleType }>, + reply: FastifyReply +) => { + const collection = db.collection("blog"); + + await collection + .find({ title: request.params.title }, { projection: { _id: false } }) + .toArray() + .then((data) => { + if (data.length === 0 || data[0] === undefined) { + reply.notFound("There are no blog posts with that title."); + } else { + reply.send(data[0]); + } + }); +}; + +const getTags = async (_request: FastifyRequest, reply: FastifyReply) => { + const collection = db.collection("blog"); + + await collection + .find({}, { projection: { _id: false, tags: true } }) + .toArray() + .then((data) => { + if (data.length === 0 || data[0] === undefined) { + reply.notFound("There are no blog posts."); + } else { + const tags = data.map((post) => post["tags"]).flat(); + const uniqueTags = [...new Set(tags)]; + reply.send(uniqueTags); + } + }); +}; + +const getAuthors = async (_request: FastifyRequest, reply: FastifyReply) => { + const collection = db.collection("blog"); + + await collection + .find({}, { projection: { _id: false, author: true } }) + .toArray() + .then((data) => { + if (data.length === 0 || data[0] === undefined) { + reply.notFound("There are no blog posts."); + } else { + const authors = data.map((post) => post["author"]); + const uniqueAuthors = [...new Set(authors)]; + reply.send(uniqueAuthors); + } + }); +}; + +export default blogApi; diff --git a/src/apis/form.ts b/src/apis/form.ts index a0a5be4..7c4e4ca 100644 --- a/src/apis/form.ts +++ b/src/apis/form.ts @@ -11,20 +11,20 @@ const formApi = (fastify: FastifyInstance) => { log("The form api is disabled.", "warning"); fastify.get( "/tools/form", - async (request: FastifyRequest, reply: FastifyReply) => { + async (_request: FastifyRequest, reply: FastifyReply) => { reply.send("The form api is disabled."); } ); fastify.get( "/api/v1/state/form", - async (request: FastifyRequest, reply: FastifyReply) => { + async (_request: FastifyRequest, reply: FastifyReply) => { reply.send({ enabled: false }); } ); } else { fastify.get( "/tools/form", - (request: FastifyRequest, reply: FastifyReply) => { + (_request: FastifyRequest, reply: FastifyReply) => { reply.view("form", { title: "form implementation example", sitekey: config.app.hcaptcha.sitekey @@ -34,7 +34,7 @@ const formApi = (fastify: FastifyInstance) => { fastify.get( "/api/v1/state/form", - async (request: FastifyRequest, reply: FastifyReply) => { + async (_request: FastifyRequest, reply: FastifyReply) => { reply.send({ enabled: true }); } ); @@ -72,42 +72,41 @@ const handleForm = ( reply: FastifyReply ) => { if (BodyTypeSchema.validate(request.body).error) { - reply.badRequest( - `${BodyTypeSchema.validate(request.body).error}` - ); + reply.badRequest(`${BodyTypeSchema.validate(request.body).error}`); } else { const ip = getIp(request); - verify(config.app.hcaptcha.secret, request.body["h-captcha-response"]) - .then((data) => { - const hook = new Webhook(config.app.webhook); - if (data.success) { - const embed = new MessageBuilder() - .setAuthor( - `${ip}, ${request.body.email}, https://abuseipdb.com/check/${ip}` - ) - .addField("Comment type", request.body.commentType, true) - .addField("Message", request.body.message) - .setTimestamp(); + verify( + config.app.hcaptcha.secret, + request.body["h-captcha-response"] + ).then((data) => { + const hook = new Webhook(config.app.webhook); + if (data.success) { + const embed = new MessageBuilder() + .setAuthor( + `${ip}, ${request.body.email}, https://abuseipdb.com/check/${ip}` + ) + .addField("Comment type", request.body.commentType, true) + .addField("Message", request.body.message) + .setTimestamp(); - reply.send( - "Thanks for your message, we will get back to you as soon as possible." - ); + reply.send( + "Thanks for your message, we will get back to you as soon as possible." + ); - hook.send(embed); - } else { - (data); - reply.unauthorized( - "Captcha failed or expired, please try again. If this keeps happening, assume the captcha is broken and contact us on Matrix." + - " Error: " + data["error-codes"] - ); + hook.send(embed); + } else { + reply.unauthorized( + "Captcha failed or expired, please try again. If this keeps happening, assume the captcha is broken and contact us on Matrix." + + " Error: " + + data["error-codes"] + ); - hook.send( - `IP: ${ip}, https://abuseipdb.com/check/${ip}\nfailed to complete the captcha.` - ); - } - }) - .catch(console.error); + hook.send( + `IP: ${ip}, https://abuseipdb.com/check/${ip}\nfailed to complete the captcha with error: ${data["error-codes"]}.` + ); + } + }); } }; diff --git a/src/apis/status.ts b/src/apis/status.ts index 1c5da92..f99eb30 100644 --- a/src/apis/status.ts +++ b/src/apis/status.ts @@ -6,13 +6,13 @@ const statusApi = async (fastify: FastifyInstance) => { if (!config.app.state.status) { fastify.get( "/api/v1/status", - (request: FastifyRequest, reply: FastifyReply) => { + (_request: FastifyRequest, reply: FastifyReply) => { reply.send("The status api is disabled."); } ); fastify.get( "/api/v1/state/status", - (request: FastifyRequest, reply: FastifyReply) => { + (_request: FastifyRequest, reply: FastifyReply) => { reply.send({ enabled: false }); } ); @@ -26,14 +26,14 @@ const statusApi = async (fastify: FastifyInstance) => { fastify.get( "/api/v1/state/status", - (request: FastifyRequest, reply: FastifyReply) => { + (_request: FastifyRequest, reply: FastifyReply) => { reply.send({ enabled: true }); } ); } }; -const setData = (request: FastifyRequest, reply: FastifyReply) => { +const setData = (_request: FastifyRequest, reply: FastifyReply) => { const map = new Map(); const updateMap = () => { diff --git a/src/apis/user.ts b/src/apis/user.ts index c568d17..342de17 100644 --- a/src/apis/user.ts +++ b/src/apis/user.ts @@ -10,10 +10,18 @@ export const isAdmin = userMap.get("isAdmin"); const userApi = (fastify: FastifyInstance) => { fastify.get( "/tools/user", - (request: FastifyRequest, reply: FastifyReply) => { + (_request: FastifyRequest, reply: FastifyReply) => { + const collection = db.collection("users"); + reply.view("user", { title: "user command centre", - isAdmin: userMap.get("isAdmin") + isAdmin: userMap.get("isAdmin"), + isLoggedIn: userMap.get("isLoggedIn"), + hasAdmin: + collection.findOne({ username: "admin" }) !== null + ? true + : false, + isUser: true }); } ); @@ -30,6 +38,12 @@ const userApi = (fastify: FastifyInstance) => { userLogin(request, reply); } ); + fastify.post( + "/api/v1/user/logout", + (request: FastifyRequest, reply: FastifyReply) => { + userLogout(request, reply); + } + ); fastify.post( "/api/v1/user/delete", (request: FastifyRequest<{ Body: BodyType }>, reply: FastifyReply) => { @@ -68,7 +82,10 @@ const userApi = (fastify: FastifyInstance) => { ); fastify.post( "/api/v1/user/unbanip", - (request: FastifyRequest<{ Body: UnbanIpType }>, reply: FastifyReply) => { + ( + request: FastifyRequest<{ Body: UnbanIpType }>, + reply: FastifyReply + ) => { unbanIp(request, reply); } ); @@ -88,7 +105,6 @@ const userSignup = async ( request: FastifyRequest<{ Body: BodyType }>, reply: FastifyReply ) => { - if (userMap.get("isAdmin")) { if (BodyTypeSchema.validate(request.body).error) { reply.badRequest(`${BodyTypeSchema.validate(request.body).error}`); @@ -99,7 +115,7 @@ const userSignup = async ( username: request.body.username, password: await bcrypt.hash(request.body.password, 10) }; - + if (await collection.findOne({ username: request.body.username })) { reply.conflict("User already exists."); } else { @@ -107,7 +123,9 @@ const userSignup = async ( if (result.insertedId !== undefined) { reply.send("User created."); } else { - reply.internalServerError("An error occurred while writing to MongoDB."); + reply.internalServerError( + "An error occurred while writing to MongoDB." + ); } }); } @@ -121,7 +139,6 @@ const userLogin = async ( request: FastifyRequest<{ Body: BodyType }>, reply: FastifyReply ) => { - if (BodyTypeSchema.validate(request.body).error) { reply.badRequest(`${BodyTypeSchema.validate(request.body).error}`); } else { @@ -142,7 +159,9 @@ const userLogin = async ( data[0]?.["password"], (err, result) => { if (err) { - reply.internalServerError("An error occurred with bcrypt. " + err); + reply.internalServerError( + "An error occurred with bcrypt. " + err + ); } else { if (result) { userMap.set("isLoggedIn", true); @@ -163,12 +182,21 @@ const userLogin = async ( } }; +const userLogout = async (_request: FastifyRequest, reply: FastifyReply) => { + if (userMap.get("isLoggedIn")) { + userMap.set("isLoggedIn", false); + userMap.set("username", ""); + userMap.set("isAdmin", false); + reply.send("User logged out."); + } else { + reply.unauthorized("You are not logged in."); + } +}; + const userDelete = async ( request: FastifyRequest<{ Body: BodyType }>, reply: FastifyReply ) => { - - if (userMap.get("isLoggedIn")) { if (BodyTypeSchema.validate(request.body).error) { reply.badRequest(`${BodyTypeSchema.validate(request.body).error}`); @@ -206,7 +234,6 @@ const userChangePassword = async ( request: FastifyRequest<{ Body: ChangePasswordType }>, reply: FastifyReply ) => { - if (userMap.get("isLoggedIn")) { if (ChangePasswordTypeSchema.validate(request.body).error) { reply.badRequest( @@ -216,7 +243,9 @@ const userChangePassword = async ( const collection = db.collection("users"); const data = { - username: isAdmin ? request.body.username : userMap.get("username"), + username: isAdmin + ? request.body.username + : userMap.get("username"), password: await bcrypt.hash(request.body.newpassword, 10) }; @@ -255,7 +284,9 @@ const createAdmin = async ( reply: FastifyReply ) => { if (CreateAdminTypeSchema.validate(request.body).error) { - reply.badRequest(`${CreateAdminTypeSchema.validate(request.body).error}`); + reply.badRequest( + `${CreateAdminTypeSchema.validate(request.body).error}` + ); } else { const collection = db.collection("users"); @@ -271,7 +302,9 @@ const createAdmin = async ( if (result.insertedId !== undefined) { reply.send("Admin created."); } else { - reply.internalServerError("An error occurred while writing to MongoDB."); + reply.internalServerError( + "An error occurred while writing to MongoDB." + ); } }); } @@ -292,7 +325,6 @@ const changeUsername = async ( request: FastifyRequest<{ Body: ChangeUserNameType }>, reply: FastifyReply ) => { - if (userMap.get("isLoggedIn")) { if (ChangeUserNameTypeSchema.validate(request.body).error) { reply.badRequest( @@ -312,7 +344,9 @@ const changeUsername = async ( .then((data) => data?.["password"]) }; - if (await collection.findOne({ username: request.body.newusername })) { + if ( + await collection.findOne({ username: request.body.newusername }) + ) { reply.conflict("Username already taken."); } else { await collection @@ -350,7 +384,10 @@ const BanIpTypeSchema = Joi.object({ bannedBy: Joi.string().required() }); -const banIp = async (request: FastifyRequest<{ Body: BanIpType }>, reply: FastifyReply) => { +const banIp = async ( + request: FastifyRequest<{ Body: BanIpType }>, + reply: FastifyReply +) => { if (userMap.get("isAdmin")) { if (BanIpTypeSchema.validate(request.body).error) { reply.badRequest(`${BanIpTypeSchema.validate(request.body).error}`); @@ -360,19 +397,26 @@ const banIp = async (request: FastifyRequest<{ Body: BanIpType }>, reply: Fastif if (await collection.findOne({ ip: request.body.ip })) { reply.conflict("IP already banned."); } else { - await collection.insertOne({ ...request.body, time: Math.floor(Date.now() / 1000) }).then((result) => { - if (result.insertedId !== undefined) { - reply.send("IP banned."); - } else { - reply.internalServerError("An error occurred while writing to MongoDB."); - } - }); + await collection + .insertOne({ + ...request.body, + time: Math.floor(Date.now() / 1000) + }) + .then((result) => { + if (result.insertedId !== undefined) { + reply.send("IP banned."); + } else { + reply.internalServerError( + "An error occurred while writing to MongoDB." + ); + } + }); } } } else { reply.unauthorized("You need to be an admin to ban IPs."); } -} +}; interface UnbanIpType { ip: string; @@ -386,34 +430,46 @@ const UnbanIpTypeSchema = Joi.object({ unbannedBy: Joi.string().required() }); -const unbanIp = async (request: FastifyRequest<{ Body: UnbanIpType }>, reply: FastifyReply) => { +const unbanIp = async ( + request: FastifyRequest<{ Body: UnbanIpType }>, + reply: FastifyReply +) => { if (userMap.get("isAdmin")) { if (UnbanIpTypeSchema.validate(request.body).error) { - reply.badRequest(`${UnbanIpTypeSchema.validate(request.body).error}`); + reply.badRequest( + `${UnbanIpTypeSchema.validate(request.body).error}` + ); } else { - const collection = db.collection("blocklist"); + const collection = db.collection("banned"); const unbannedCollection = db.collection("unbanned"); - await collection.deleteOne({ ip: request.body.ip }).then((result) => { - if (result.deletedCount === 1) { - reply.send("IP unbanned."); - } else { - reply.notFound("IP is not banned."); - } - }); - - await unbannedCollection.insertOne({ ...request.body, time: Math.floor(Date.now() / 1000) }).then((result) => { - if (result.insertedId !== undefined) { - reply.send("Wrote unban to unbanned database."); - } else { - reply.internalServerError("An error occurred while writing to MongoDB."); - } - }); + await collection + .deleteOne({ ip: request.body.ip }) + .then(async (result) => { + if (result.deletedCount === 1) { + await unbannedCollection + .insertOne({ + ...request.body, + time: Math.floor(Date.now() / 1000) + }) + .then((result) => { + if (result.insertedId !== undefined) { + reply.send("IP unbanned."); + } else { + reply.internalServerError( + "An error occurred while writing to MongoDB." + ); + } + }); + } else { + reply.notFound("IP is not banned."); + } + }); } } else { reply.unauthorized("You need to be an admin to ban IPs."); } -} +}; export default userApi; diff --git a/src/index.ts b/src/index.ts index b9172dc..9257080 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,7 +8,8 @@ import Handlebars from "handlebars"; import statusApi from "./apis/status"; import announcementsApi from "./apis/announcements"; import formApi from "./apis/form"; -import userApi from "./apis/user"; +import userApi, { userMap } from "./apis/user"; +import blogApi from "./apis/blog"; import log from "./utils/logUtil"; import { join, dirname } from "path"; import { fileURLToPath } from "url"; @@ -38,24 +39,40 @@ fastify.register(pointOfView, { }, root: join(__dirname, "templates"), layout: "layout", + defaultContext: { + isIndex: false, + isLoggedIn: userMap.get("isLoggedIn"), + isUser: false + }, viewExt: "hbs" }); -fastify.addHook("preHandler", async (request: FastifyRequest, reply: FastifyReply) => { - const collection = db.collection("banned"); +fastify.addHook( + "preHandler", + async (request: FastifyRequest, reply: FastifyReply) => { + const collection = db.collection("banned"); - if (getIp(request) === await collection.findOne({ ip: getIp(request) }).then((doc) => doc?.["ip"])) { - reply.unauthorized("You are banned."); + if ( + getIp(request) === + (await collection + .findOne({ ip: getIp(request) }) + .then((doc) => doc?.["ip"])) + ) { + reply.unauthorized("You are banned."); + } } -}); +); -fastify.get("/", (request: FastifyRequest, reply: FastifyReply) => { +fastify.get("/", (_request: FastifyRequest, reply: FastifyReply) => { reply.view("index", { port: config.app.port, title: "index", announcementsEnabled: config.app.state.announcements, formEnabled: config.app.state.form, - statusEnabled: config.app.state.status + statusEnabled: config.app.state.status, + blogEnabled: config.app.state.blog, + isIndex: true, + env: isProd ? "production" : "development" }); }); @@ -65,6 +82,7 @@ announcementsApi(fastify); formApi(fastify); statusApi(fastify); userApi(fastify); +blogApi(fastify); fastify.listen( { port: config.app.port, host: isProd ? "0.0.0.0" : "localhost" }, @@ -85,4 +103,4 @@ process.on("SIGINT", () => { process.on("SIGTERM", () => { dbCleanUp(); process.exit(0); -}); \ No newline at end of file +}); diff --git a/src/templates/blog.hbs b/src/templates/blog.hbs new file mode 100644 index 0000000..fef3929 --- /dev/null +++ b/src/templates/blog.hbs @@ -0,0 +1,53 @@ +

Add post

+ +
+ + + + + +
+ +

Delete post

+ +
+ + +
+ +

Edit post

+ +
+ + + + + + +
\ No newline at end of file diff --git a/src/templates/index.hbs b/src/templates/index.hbs index 3cbf0d4..074d5c5 100644 --- a/src/templates/index.hbs +++ b/src/templates/index.hbs @@ -5,6 +5,8 @@ Running on port {{port}} - GitHub + - + Environment: {{env}}

Current APIs:

@@ -36,6 +38,23 @@
{{/if}} + {{#if blogEnabled}} +
+

Blog

+ Blog posts + Blog tags +
/api/v1/blog/tags/TAG - Posts with tag
+ Blog authors +
/api/v1/blog/tags/AUTHOR - Posts from author
+
/api/v1/blog/tags/TITLE - Post with this title
+ Add post (POST) + Delete post (POST) + Edit post (POST) + Blog web configuration tool + Blog state api +
+ {{/if}} +

User

Signup (POST) @@ -44,10 +63,8 @@ Change password (POST) Create admin user (POST) Change username (POST) + Ban IP (POST) + Unban IP (POST) User web configuration tool
- - - \ No newline at end of file + \ No newline at end of file diff --git a/src/templates/layout.hbs b/src/templates/layout.hbs index 8e4ae8d..5f946e4 100644 --- a/src/templates/layout.hbs +++ b/src/templates/layout.hbs @@ -8,11 +8,38 @@ font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif; color-scheme: dark; } a { color: #00d4aa; text-decoration: underline; } .col { display: flex; flex-direction: column; gap: - 0.5rem; width: fit-content; } + 0.5rem; width: fit-content; } input, button, select, textarea { + padding: 0.5rem; border-radius: 0.5rem; border: none; + background-color: #333333; color: #dddddd; outline: none; } button, + input[type="submit"], select { cursor: pointer; } -

Back

+ {{#unless isIndex}} +

Back

+ {{/unless}} + + {{#unless isUser}} +
+ + + +
+ {{/unless}} {{{body}}} diff --git a/src/templates/user.hbs b/src/templates/user.hbs index e9c4ea2..7899c1c 100644 --- a/src/templates/user.hbs +++ b/src/templates/user.hbs @@ -1,15 +1,21 @@
-

User signup

-
- - - -
+ {{#if isAdmin}} +

User signup

+
+ + + +
+ {{/if}}

User login

@@ -18,89 +24,117 @@
-

User delete

-
- {{#if isAdmin}} + {{#if isLoggedIn}} +

User logout

+ + +
+ {{/if}} + + {{#if isLoggedIn}} +

User delete

+
+ {{#if isAdmin}} + + {{/if}} + +
+ {{/if}} + + {{#if isLoggedIn}} +

User change password

+
+ {{#if isAdmin}} + + {{/if}} - {{/if}} - -
+ + + {{/if}} -

User change password

-
- {{#if isAdmin}} - - {{/if}} - - -
+ {{#unless hasAdmin}} +

Create admin user

+
+ + +
+ {{/unless}} -

Create admin user

-
- - -
+ {{#if isLoggedIn}} +

User change username

+
+ {{#if isAdmin}} + + {{/if}} + + +
+ {{/if}} -

User change username

-
- {{#if isAdmin}} - - {{/if}} - - -
+ {{#if isAdmin}} +

Ban IP

+
+ + + + +
+ {{/if}} -

Ban IP

-
- - - - -
- -

Unban IP

-
- - - - -
+ {{#if isAdmin}} +

Unban IP

+
+ + + + +
+ {{/if}}
\ No newline at end of file diff --git a/src/utils/config.ts b/src/utils/config.ts index ca55135..b1d843c 100644 --- a/src/utils/config.ts +++ b/src/utils/config.ts @@ -21,6 +21,7 @@ interface Config { announcements: boolean; form: boolean; status: boolean; + blog: boolean; }; }; } diff --git a/src/utils/db.ts b/src/utils/db.ts index 6fb12ad..954fbca 100644 --- a/src/utils/db.ts +++ b/src/utils/db.ts @@ -13,14 +13,14 @@ export const db = mongoClient.db(mongoName); export const initializeDb = async () => { await mongoClient - .connect() - .then(() => { - log("Connected to MongoDB", "info"); - }) - .catch((err) => { - log("Error connecting to MongoDB", "error"); - log(err, "error"); - }); + .connect() + .then(() => { + log("Connected to MongoDB", "info"); + }) + .catch((err) => { + log("Error connecting to MongoDB", "error"); + log(err, "error"); + }); }; const closeDb = async () => { @@ -37,4 +37,4 @@ const closeDb = async () => { export const dbCleanUp = () => { closeDb(); -} \ No newline at end of file +}; diff --git a/src/utils/fetchStatus.ts b/src/utils/fetchStatus.ts index f540658..7bfbe1b 100644 --- a/src/utils/fetchStatus.ts +++ b/src/utils/fetchStatus.ts @@ -3,8 +3,7 @@ import axios from "axios"; const fetchStatus = (domain: string) => { const req = axios("https://" + domain, { timeout: 10000 }) .then((res) => res.status) - .then((statusCode) => (statusCode === 200 ? true : false)) - .catch(() => false); + .catch((err) => err.response.status); return req; }; diff --git a/tsconfig.json b/tsconfig.json index e675ab3..4e14808 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,8 +1,5 @@ { "extends": "@tsconfig/node18-strictest-esm/tsconfig.json", - "exclude": ["node_modules", "dist", "src/main.js"], - "include": ["src/**/*", "scripts/**/*"], - "compilerOptions": { - "noUnusedParameters": false - } + "exclude": ["node_modules", "dist"], + "include": ["src/**/*", "scripts/**/*"] }