diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..9654c87 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,46 @@ +# Logs +logs +*.log +npm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Dependency directories +node_modules/ + +# Optional npm cache directory +.npm + +# Output of 'npm pack' +*.tgz + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# build artifacts +dist/ +build/ +*.spec + +# docker stuff +Dockerfile +.dockerignore +.git +.gitignore +.github +.vscode + +# config and data +config/ +data/ diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 4482ad1..ee44dfe 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -12,3 +12,5 @@ updates: assignees: - "fizitzfux" target-branch: "main" + ignore: + - dependency-name: "@types/*" diff --git a/.github/workflows/changelog.yml b/.github/workflows/changelog.yml new file mode 100644 index 0000000..3402cdc --- /dev/null +++ b/.github/workflows/changelog.yml @@ -0,0 +1,11 @@ +on: + push: + branches: + - main + +jobs: + Generate-Changelog: + runs-on: ubuntu-latest + steps: + - name: Release Changelog Builder + uses: mikepenz/release-changelog-builder-action@v5 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..6c22be0 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,13 @@ +FROM node:23.7-alpine +USER node + +RUN mkdir -p /home/node/app +RUN mkdir -p /home/node/app/data + +WORKDIR /home/node/app + +COPY . . + +RUN npm install + +CMD [ "npm", "run", "start" ] diff --git a/config/texts.example.ts b/config/texts.example.ts index 3ed357a..ea8c3dd 100644 --- a/config/texts.example.ts +++ b/config/texts.example.ts @@ -1,11 +1,6 @@ export default { index: `

Welcome to KeukNet!

-

- If you're new here we recommend reading the getting started guide immediately after registering.
- It can be found top-left next to the logo.
- Registering and logging in can be done at the top-right. -

For help or questions please contact @fizitzfux on Discord
or join our Discord server. @@ -13,5 +8,7 @@ export default {

Please do be aware of the fact that this is still very much in development and a lot of stuff will be improved with time.

+ + Login `, } diff --git a/package-lock.json b/package-lock.json index 62b0a58..5c175a9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,16 +1,15 @@ { "name": "keuknet", - "version": "3.0.0-beta-1", + "version": "3.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "keuknet", - "version": "3.0.0-beta-1", + "version": "3.0.0", "license": "MPL-2.0", "dependencies": { "cookie": "^1.0.2", - "dotenv": "^16.4.5", "knex": "^3.1.0", "nunjucks": "^3.2.4", "sqlite3": "^5.1.6", @@ -22,12 +21,15 @@ "@types/node": "^22.10.7", "@types/nunjucks": "^3.2.6", "tsx": "^4.19.2" + }, + "engines": { + "node": "^23" } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.1.tgz", - "integrity": "sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==", + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.3.tgz", + "integrity": "sha512-W8bFfPA8DowP8l//sxjJLSLkD8iEjMc7cBVyP+u4cEv9sM7mdUCkgsj+t0n/BWPFtv7WWCN5Yzj0N6FJNUUqBQ==", "cpu": [ "ppc64" ], @@ -42,9 +44,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.23.1.tgz", - "integrity": "sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==", + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.3.tgz", + "integrity": "sha512-PuwVXbnP87Tcff5I9ngV0lmiSu40xw1At6i3GsU77U7cjDDB4s0X2cyFuBiDa1SBk9DnvWwnGvVaGBqoFWPb7A==", "cpu": [ "arm" ], @@ -59,9 +61,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.23.1.tgz", - "integrity": "sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==", + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.3.tgz", + "integrity": "sha512-XelR6MzjlZuBM4f5z2IQHK6LkK34Cvv6Rj2EntER3lwCBFdg6h2lKbtRjpTTsdEjD/WSe1q8UyPBXP1x3i/wYQ==", "cpu": [ "arm64" ], @@ -76,9 +78,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.23.1.tgz", - "integrity": "sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==", + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.3.tgz", + "integrity": "sha512-ogtTpYHT/g1GWS/zKM0cc/tIebFjm1F9Aw1boQ2Y0eUQ+J89d0jFY//s9ei9jVIlkYi8AfOjiixcLJSGNSOAdQ==", "cpu": [ "x64" ], @@ -93,9 +95,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.23.1.tgz", - "integrity": "sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==", + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.3.tgz", + "integrity": "sha512-eESK5yfPNTqpAmDfFWNsOhmIOaQA59tAcF/EfYvo5/QWQCzXn5iUSOnqt3ra3UdzBv073ykTtmeLJZGt3HhA+w==", "cpu": [ "arm64" ], @@ -110,9 +112,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.23.1.tgz", - "integrity": "sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==", + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.3.tgz", + "integrity": "sha512-Kd8glo7sIZtwOLcPbW0yLpKmBNWMANZhrC1r6K++uDR2zyzb6AeOYtI6udbtabmQpFaxJ8uduXMAo1gs5ozz8A==", "cpu": [ "x64" ], @@ -127,9 +129,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.1.tgz", - "integrity": "sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==", + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.3.tgz", + "integrity": "sha512-EJiyS70BYybOBpJth3M0KLOus0n+RRMKTYzhYhFeMwp7e/RaajXvP+BWlmEXNk6uk+KAu46j/kaQzr6au+JcIw==", "cpu": [ "arm64" ], @@ -144,9 +146,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.23.1.tgz", - "integrity": "sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==", + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.3.tgz", + "integrity": "sha512-Q+wSjaLpGxYf7zC0kL0nDlhsfuFkoN+EXrx2KSB33RhinWzejOd6AvgmP5JbkgXKmjhmpfgKZq24pneodYqE8Q==", "cpu": [ "x64" ], @@ -161,9 +163,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.23.1.tgz", - "integrity": "sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==", + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.3.tgz", + "integrity": "sha512-dUOVmAUzuHy2ZOKIHIKHCm58HKzFqd+puLaS424h6I85GlSDRZIA5ycBixb3mFgM0Jdh+ZOSB6KptX30DD8YOQ==", "cpu": [ "arm" ], @@ -178,9 +180,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.23.1.tgz", - "integrity": "sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==", + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.3.tgz", + "integrity": "sha512-xCUgnNYhRD5bb1C1nqrDV1PfkwgbswTTBRbAd8aH5PhYzikdf/ddtsYyMXFfGSsb/6t6QaPSzxtbfAZr9uox4A==", "cpu": [ "arm64" ], @@ -195,9 +197,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.23.1.tgz", - "integrity": "sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==", + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.3.tgz", + "integrity": "sha512-yplPOpczHOO4jTYKmuYuANI3WhvIPSVANGcNUeMlxH4twz/TeXuzEP41tGKNGWJjuMhotpGabeFYGAOU2ummBw==", "cpu": [ "ia32" ], @@ -212,9 +214,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.23.1.tgz", - "integrity": "sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==", + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.3.tgz", + "integrity": "sha512-P4BLP5/fjyihmXCELRGrLd793q/lBtKMQl8ARGpDxgzgIKJDRJ/u4r1A/HgpBpKpKZelGct2PGI4T+axcedf6g==", "cpu": [ "loong64" ], @@ -229,9 +231,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.23.1.tgz", - "integrity": "sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==", + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.3.tgz", + "integrity": "sha512-eRAOV2ODpu6P5divMEMa26RRqb2yUoYsuQQOuFUexUoQndm4MdpXXDBbUoKIc0iPa4aCO7gIhtnYomkn2x+bag==", "cpu": [ "mips64el" ], @@ -246,9 +248,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.23.1.tgz", - "integrity": "sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==", + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.3.tgz", + "integrity": "sha512-ZC4jV2p7VbzTlnl8nZKLcBkfzIf4Yad1SJM4ZMKYnJqZFD4rTI+pBG65u8ev4jk3/MPwY9DvGn50wi3uhdaghg==", "cpu": [ "ppc64" ], @@ -263,9 +265,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.23.1.tgz", - "integrity": "sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==", + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.3.tgz", + "integrity": "sha512-LDDODcFzNtECTrUUbVCs6j9/bDVqy7DDRsuIXJg6so+mFksgwG7ZVnTruYi5V+z3eE5y+BJZw7VvUadkbfg7QA==", "cpu": [ "riscv64" ], @@ -280,9 +282,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.23.1.tgz", - "integrity": "sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==", + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.3.tgz", + "integrity": "sha512-s+w/NOY2k0yC2p9SLen+ymflgcpRkvwwa02fqmAwhBRI3SC12uiS10edHHXlVWwfAagYSY5UpmT/zISXPMW3tQ==", "cpu": [ "s390x" ], @@ -297,9 +299,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.23.1.tgz", - "integrity": "sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==", + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.3.tgz", + "integrity": "sha512-nQHDz4pXjSDC6UfOE1Fw9Q8d6GCAd9KdvMZpfVGWSJztYCarRgSDfOVBY5xwhQXseiyxapkiSJi/5/ja8mRFFA==", "cpu": [ "x64" ], @@ -313,10 +315,27 @@ "node": ">=18" } }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.3.tgz", + "integrity": "sha512-1QaLtOWq0mzK6tzzp0jRN3eccmN3hezey7mhLnzC6oNlJoUJz4nym5ZD7mDnS/LZQgkrhEbEiTn515lPeLpgWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.23.1.tgz", - "integrity": "sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==", + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.3.tgz", + "integrity": "sha512-i5Hm68HXHdgv8wkrt+10Bc50zM0/eonPb/a/OFVfB6Qvpiirco5gBA5bz7S2SHuU+Y4LWn/zehzNX14Sp4r27g==", "cpu": [ "x64" ], @@ -331,9 +350,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.1.tgz", - "integrity": "sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==", + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.3.tgz", + "integrity": "sha512-zGAVApJEYTbOC6H/3QBr2mq3upG/LBEXr85/pTtKiv2IXcgKV0RT0QA/hSXZqSvLEpXeIxah7LczB4lkiYhTAQ==", "cpu": [ "arm64" ], @@ -348,9 +367,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.23.1.tgz", - "integrity": "sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==", + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.3.tgz", + "integrity": "sha512-fpqctI45NnCIDKBH5AXQBsD0NDPbEFczK98hk/aa6HJxbl+UtLkJV2+Bvy5hLSLk3LHmqt0NTkKNso1A9y1a4w==", "cpu": [ "x64" ], @@ -365,9 +384,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.23.1.tgz", - "integrity": "sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==", + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.3.tgz", + "integrity": "sha512-ROJhm7d8bk9dMCUZjkS8fgzsPAZEjtRJqCAmVgB0gMrvG7hfmPmz9k1rwO4jSiblFjYmNvbECL9uhaPzONMfgA==", "cpu": [ "x64" ], @@ -382,9 +401,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.23.1.tgz", - "integrity": "sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==", + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.3.tgz", + "integrity": "sha512-YWcow8peiHpNBiIXHwaswPnAXLsLVygFwCB3A7Bh5jRkIBFWHGmNQ48AlX4xDvQNoMZlPYzjVOQDYEzWCqufMQ==", "cpu": [ "arm64" ], @@ -399,9 +418,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.23.1.tgz", - "integrity": "sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==", + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.3.tgz", + "integrity": "sha512-qspTZOIGoXVS4DpNqUYUs9UxVb04khS1Degaw/MnfMe7goQ3lTfQ13Vw4qY/Nj0979BGvMRpAYbs/BAxEvU8ew==", "cpu": [ "ia32" ], @@ -416,9 +435,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.23.1.tgz", - "integrity": "sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==", + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.3.tgz", + "integrity": "sha512-ICgUR+kPimx0vvRzf+N/7L7tVSQeE3BYY+NhHRHXS1kBuPO7z2+7ea2HbhDyZdTephgvNvKrlDDKUexuCVBVvg==", "cpu": [ "x64" ], @@ -501,13 +520,13 @@ } }, "node_modules/@types/node": { - "version": "22.10.7", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.7.tgz", - "integrity": "sha512-V09KvXxFiutGp6B7XkpaDXlNadZxrzajcY50EuoLIpQ6WWYCSvf19lVIazzfIzQvhUN2HjX12spLojTnhuKlGg==", + "version": "22.15.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.2.tgz", + "integrity": "sha512-uKXqKN9beGoMdBfcaTY1ecwz6ctxuJAcUlwE55938g0ZJ8lRxwAZqRz2AJ4pzpt5dHdTPMB863UZ0ESiFUcP7A==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~6.20.0" + "undici-types": "~6.21.0" } }, "node_modules/@types/nunjucks": { @@ -836,26 +855,14 @@ "optional": true }, "node_modules/detect-libc": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", - "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", + "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", "license": "Apache-2.0", "engines": { "node": ">=8" } }, - "node_modules/dotenv": { - "version": "16.4.7", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", - "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -900,9 +907,9 @@ "optional": true }, "node_modules/esbuild": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.1.tgz", - "integrity": "sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==", + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.3.tgz", + "integrity": "sha512-qKA6Pvai73+M2FtftpNKRxJ78GIjmFXFxd/1DVBqGo/qNhLSfv+G12n9pNoWdytJC8U00TrViOwpjT0zgqQS8Q==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -913,30 +920,31 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.23.1", - "@esbuild/android-arm": "0.23.1", - "@esbuild/android-arm64": "0.23.1", - "@esbuild/android-x64": "0.23.1", - "@esbuild/darwin-arm64": "0.23.1", - "@esbuild/darwin-x64": "0.23.1", - "@esbuild/freebsd-arm64": "0.23.1", - "@esbuild/freebsd-x64": "0.23.1", - "@esbuild/linux-arm": "0.23.1", - "@esbuild/linux-arm64": "0.23.1", - "@esbuild/linux-ia32": "0.23.1", - "@esbuild/linux-loong64": "0.23.1", - "@esbuild/linux-mips64el": "0.23.1", - "@esbuild/linux-ppc64": "0.23.1", - "@esbuild/linux-riscv64": "0.23.1", - "@esbuild/linux-s390x": "0.23.1", - "@esbuild/linux-x64": "0.23.1", - "@esbuild/netbsd-x64": "0.23.1", - "@esbuild/openbsd-arm64": "0.23.1", - "@esbuild/openbsd-x64": "0.23.1", - "@esbuild/sunos-x64": "0.23.1", - "@esbuild/win32-arm64": "0.23.1", - "@esbuild/win32-ia32": "0.23.1", - "@esbuild/win32-x64": "0.23.1" + "@esbuild/aix-ppc64": "0.25.3", + "@esbuild/android-arm": "0.25.3", + "@esbuild/android-arm64": "0.25.3", + "@esbuild/android-x64": "0.25.3", + "@esbuild/darwin-arm64": "0.25.3", + "@esbuild/darwin-x64": "0.25.3", + "@esbuild/freebsd-arm64": "0.25.3", + "@esbuild/freebsd-x64": "0.25.3", + "@esbuild/linux-arm": "0.25.3", + "@esbuild/linux-arm64": "0.25.3", + "@esbuild/linux-ia32": "0.25.3", + "@esbuild/linux-loong64": "0.25.3", + "@esbuild/linux-mips64el": "0.25.3", + "@esbuild/linux-ppc64": "0.25.3", + "@esbuild/linux-riscv64": "0.25.3", + "@esbuild/linux-s390x": "0.25.3", + "@esbuild/linux-x64": "0.25.3", + "@esbuild/netbsd-arm64": "0.25.3", + "@esbuild/netbsd-x64": "0.25.3", + "@esbuild/openbsd-arm64": "0.25.3", + "@esbuild/openbsd-x64": "0.25.3", + "@esbuild/sunos-x64": "0.25.3", + "@esbuild/win32-arm64": "0.25.3", + "@esbuild/win32-ia32": "0.25.3", + "@esbuild/win32-x64": "0.25.3" } }, "node_modules/escalade": { @@ -1591,9 +1599,9 @@ } }, "node_modules/node-abi": { - "version": "3.73.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.73.0.tgz", - "integrity": "sha512-z8iYzQGBu35ZkTQ9mtR8RqugJZ9RCLn8fv3d7LsgDBzOijGQP3RdKTX4LA7LXw03ZhU5z0l4xfhIMgSES31+cg==", + "version": "3.74.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.74.0.tgz", + "integrity": "sha512-c5XK0MjkGBrQPGYG24GBADZud0NCbznxNx0ZkS+ebUTrmV1qTDxPxSL8zEAPURXSbLRWVexxmP4986BziahL5w==", "license": "MIT", "dependencies": { "semver": "^7.3.5" @@ -1939,9 +1947,9 @@ "optional": true }, "node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -2021,9 +2029,9 @@ } }, "node_modules/socks": { - "version": "2.8.3", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz", - "integrity": "sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==", + "version": "2.8.4", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.4.tgz", + "integrity": "sha512-D3YaD0aRxR3mEcqnidIs7ReYJFVzWdd6fXJYUM8ixcQcJRGTka/b3saV0KflYhyVJXKhb947GndU35SxYNResQ==", "license": "MIT", "optional": true, "dependencies": { @@ -2231,13 +2239,13 @@ } }, "node_modules/tsx": { - "version": "4.19.2", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.2.tgz", - "integrity": "sha512-pOUl6Vo2LUq/bSa8S5q7b91cgNSjctn9ugq/+Mvow99qW6x/UZYwzxy/3NmqoT66eHYfCVvFvACC58UBPFf28g==", + "version": "4.19.3", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.3.tgz", + "integrity": "sha512-4H8vUNGNjQ4V2EOoGw005+c+dGuPSnhpPBPHBtsZdGZBk/iJb4kguGlPWaZTZ3q5nMtFOEsY0nRDlh9PJyd6SQ==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "~0.23.0", + "esbuild": "~0.25.0", "get-tsconfig": "^4.7.5" }, "bin": { @@ -2263,9 +2271,9 @@ } }, "node_modules/typescript": { - "version": "5.7.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", - "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -2276,9 +2284,9 @@ } }, "node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "dev": true, "license": "MIT" }, diff --git a/package.json b/package.json index 0df0837..e2c377f 100644 --- a/package.json +++ b/package.json @@ -1,23 +1,16 @@ { "name": "keuknet", - "version": "3.0.0-beta-1", + "version": "3.0.0", "description": "A webserver and client program for easily managing a WireGuard-network in a client-server setting.", "main": "src/index.ts", "type": "module", "scripts": { "start": "tsx src/index.ts", - "dev": "sudo tsx watch --exclude './data/**/*' --clear-screen=false ./src/index.ts" + "dev": "sudo tsx watch --exclude './data/**/*' --clear-screen=false --env-file=.env ./src/index.ts", + "dev-wsl": "npx tsx watch --exclude './data/**/*' --clear-screen=false --env-file=.env ./src/index.ts" }, "author": "Keukeiland, Fizitzfux", "license": "MPL-2.0", - "dependencies": { - "cookie": "^1.0.2", - "dotenv": "^16.4.5", - "knex": "^3.1.0", - "nunjucks": "^3.2.4", - "sqlite3": "^5.1.6", - "typescript": "^5.7.3" - }, "repository": { "type": "git", "url": "git+https://github.com/keukeiland/keuknet.git" @@ -26,6 +19,17 @@ "url": "https://github.com/keukeiland/keuknet/issues" }, "homepage": "https://keuk.net/", + "engineStrict": true, + "engines": { + "node": "^23" + }, + "dependencies": { + "cookie": "^1.0.2", + "knex": "^3.1.0", + "nunjucks": "^3.2.4", + "sqlite3": "^5.1.6", + "typescript": "^5.7.3" + }, "devDependencies": { "@types/cookie": "^0.6.0", "@types/knex": "^0.15.2", diff --git a/src/classes/extension.ts b/src/classes/extension.ts index 7eefdfe..824a484 100644 --- a/src/classes/extension.ts +++ b/src/classes/extension.ts @@ -5,6 +5,8 @@ import { unpack } from "../util.ts" export abstract class ExtensionBase implements Extension { admin_only = false tables = false + disabled = false + hidden = false initialized_deps: DependencyMap = new DependencyMapImpl() name: Extension['name'] = "default_name" title: Extension['title'] = "Default Title" @@ -153,13 +155,16 @@ export abstract class ExtensionBase implements Extension { key, value, { secure: true, - httpOnly: true + httpOnly: true, + path: '/' } ) else return cookie.serialize( key, - value + value, { + path: '/' + } ) } diff --git a/src/classes/tables.ts b/src/classes/tables.ts index 4c01992..c2d60db 100644 --- a/src/classes/tables.ts +++ b/src/classes/tables.ts @@ -18,77 +18,72 @@ export abstract class Tables { let migrations = this.migrations(this.knex, new Map()) // Serialize?? - this.raw_knex('db_table_versions') + let data = await this.raw_knex('db_table_versions') .select('table_id', 'version') - .then(async (data) => { - let current_versions = new Map() - for (const row of data) { - current_versions.set(row.table_id, row.version) + + let current_versions = new Map() + for (const row of data) { + current_versions.set(row.table_id, row.version) + } + + for (var [table, latest] of versions) { + let table_id: TableId | string = `_${this.prefix}_${table}` + + if (!current_versions.has(table_id)) { + console.log(`Adding table ${table_id}`) + + if (!migrations.has(table)) + return reject(`Missing entry in migrations for table '${table_id}'`) + let migration = migrations.get(table) as MigrationRecord + if (!migration[0]) + return reject(`Missing migration entry 0 for table '${table}'`) + await migration[0]() + + current_versions.set(table_id, 0) + let err = await this.raw_knex('db_table_versions') + .insert({ + table_id, + version: current_versions.get(table_id), + }).then(() => null, (err) => err) + + if (err) + return reject(`Failed adding table version identifier. ${err}`) + console.log(`Added table ${table_id}`) + } + + let current_version: number + while (true) { + current_version = current_versions.get(table_id) ?? -1 + if (current_version < 0 || current_version == latest) break + let new_version = current_version +1 + + console.log(`Upgrading table ${table_id} from ${current_version} to ${new_version}`) + try { + let migration = Tables.getMigration(migrations, table, new_version) + if (migration instanceof Error) + return reject(migration) + + await migration() } - - for (var [table, latest] of versions) { - let table_id: TableId | string = `_${this.prefix}_${table}` - - if (!current_versions.has(table_id)) { - console.log(`Adding table ${table_id}`) - - if (!migrations.has(table)) - return reject(`Missing entry in migrations for table '${table_id}'`) - let migration = migrations.get(table) as MigrationRecord - if (!migration[0]) - return reject(`Missing migration entry 0 for table '${table}'`) - await migration[0]() - - current_versions.set(table_id, 0) - this.raw_knex('db_table_versions') - .insert({ - table_id, - version: current_versions.get(table_id), - }) - .then( - () => { - console.log(`Added table ${table_id}`) - }, (err) => { - if (err) return reject(`Failed adding table version identifier. ${err}`) - } - ) - } - - let current_version: number - while (true) { - current_version = current_versions.get(table_id) ?? -1 - if (current_version < 0 || current_version == latest) break - let new_version = current_version +1 - - console.log(`Upgrading table ${table_id} from ${current_version} to ${new_version}`) - try { - let migration = Tables.getMigration(migrations, table, new_version) - if (migration instanceof Error) return reject(migration) - await migration() - } - catch (err) { - console.error(err) - break - } - - current_versions.set(table_id, new_version) - - this.raw_knex('db_table_versions') - .update({ - version: new_version, - }) - .whereIn('table_id', [table_id]) - .then( - () => { - console.log(`Upgraded table ${table_id} to ${new_version}`) - }, - (err) => { - return reject(`Failed updating table version identifier. ${err}`) - } - ) - } + catch (err) { + return reject(`Failed upgrading table ${table_id} from ${current_version} to ${new_version}. ${err}`) } - }) + + current_versions.set(table_id, new_version) + + let err = await this.raw_knex('db_table_versions') + .update({ + version: new_version, + }) + .whereIn('table_id', [table_id]) + .then(() => null, (err) => err) + + if (err) + return reject(`Failed updating table version identifier. ${err}`) + + console.log(`Upgraded table ${table_id} to ${new_version}`) + } + } resolve() }) } diff --git a/src/extensions/admin/index.html b/src/extensions/admin/index.html index e143478..c59efb4 100644 --- a/src/extensions/admin/index.html +++ b/src/extensions/admin/index.html @@ -1,5 +1,38 @@ {% extends "extension.html" %} +{% block head %} + +{% endblock %} + + {% block body %} -

This page is currently under development and will be available soon!

+Invites +
+ + + + + + + + + + + + {% for info in user_info %} + + + + + + + + {% else %} +

No users

+ {% endfor %} + +
IDNameRegistration DateIs AdminActions
{{info.id}}{{info.name}}{{info.registration_date}}{{"Yes" if info.is_admin else "No"}} + {{"Remove Admin" if info.is_admin else "Make Admin"}} + Remove Account +
{% endblock %} diff --git a/src/extensions/admin/index.ts b/src/extensions/admin/index.ts index b547f58..116e2c0 100644 --- a/src/extensions/admin/index.ts +++ b/src/extensions/admin/index.ts @@ -1,11 +1,60 @@ -import { ExtensionBase } from "../../modules.ts" +import { ExtensionBase, Knex } from '../../modules.ts' +import { unpack } from '../../util.ts' export default class extends ExtensionBase { override name = 'admin' override title = 'Admin' override admin_only = true - override handle: Extension['handle'] = (ctx) => { - this.return_html(ctx, 'index') + user_info: {id: any, username: any, reg_date: any, admin: any}[] = [] + sortingType = "id" + + override handle: Extension['handle'] = async (ctx) => { + let [knex]: [Knex] = this.get_dependencies('Knex') + var location = ctx.path.shift() + + switch (location) { + case '': + case undefined:{ + var [user_list, err] = await knex + .query('user') + .select('*') + .then(unpack) + + ctx.context.user_info = user_list + return this.return_html(ctx, 'index', err) + } + case 'toggle_admin':{ + var id = ctx.args.get("id") + + var [user, err] = await knex + .query('user') + .select('*') + .where('id', id) + .first() + .then(unpack) + + await knex + .query('user') + .update('is_admin', !user?.is_admin) + .where('id', id) + + return this.return(ctx, undefined, location='/admin') + } + case 'remove_account':{ + var id = ctx.args.get("id") + + await knex + .query('user') + .where('id', id) + .delete('*') + + return this.return(ctx, undefined, location='/admin') + } + default: { + return this.return_file(ctx, location) + } + } + } } diff --git a/src/extensions/admin/static/index.css b/src/extensions/admin/static/index.css new file mode 100644 index 0000000..025a5cb --- /dev/null +++ b/src/extensions/admin/static/index.css @@ -0,0 +1,27 @@ +#content { + padding-top: 10px; +} + +#user_table{ + border-collapse: collapse; + margin: 10px, 0; +} + +#user_table thead tr{ + text-align: center; + font-weight: bold; +} + +#user_table tbody tr{ + text-align: center; + border-bottom: 1px black; +} + +#user_table tbody tr:nth-of-type(even){ + background-color: rgb(192, 192, 192); +} + +#user_table th, +#user_table td{ + padding: 6px 8px; +} \ No newline at end of file diff --git a/src/extensions/chat/index.html b/src/extensions/chat/index.html index e96ed42..2f05716 100644 --- a/src/extensions/chat/index.html +++ b/src/extensions/chat/index.html @@ -3,61 +3,15 @@ {% block head %} + {% endblock %} {% block body %}
- - {% for msg in chat %} - - - - - - - {% else %} -

No chat items

- {% endfor %} -
{{msg.user.name}}{{msg.time}}{{msg.content}}
+
+
diff --git a/src/extensions/chat/index.ts b/src/extensions/chat/index.ts index 6876236..97637f3 100644 --- a/src/extensions/chat/index.ts +++ b/src/extensions/chat/index.ts @@ -1,60 +1,92 @@ -import { ExtensionBase } from "../../modules.ts" +import { ExtensionBase, Knex } from "../../modules.ts" +import { unpack } from "../../util.ts" + +type message = {name: any, pfp_code: any, created_at: any, content: any} export default class extends ExtensionBase { override name = 'chat' override title = 'Chat' + override tables = true - messages: {user: {name: any, pfp_code: any}, time: any, content: any}[] = [{ - user: {name:'SYSTEM',pfp_code:'seed=SYSTEM'}, - time:(new Date()).toLocaleTimeString('en-US', {hour12: false}), - content: 'Welcome to the chatroom!' - }] - last_got_id: {[user_id: number]: number} = {} - - override handle: Extension['handle'] = (ctx) => { - var location = ctx.path.shift() - - const user_id = ctx.context.user?.id as number - - if (!location) { - if (ctx.data && ctx.data.form.message) { - var message = ctx.data.form.message.substring(0,255) - var now = (new Date()).toLocaleTimeString('en-US', {hour12: false}) - this.messages.push({ - user: { - name: ctx.context.user?.name, - pfp_code: ctx.context.user?.pfp_code, - }, - time: now, - content: message, - }) + private MessageStore = class { + onPushListeners: Set<(msg: message) => void> = new Set() + + async push(ctx: Context, content: string, knex : Knex){ + const message: message = { + name: ctx.context.user?.name, + pfp_code: ctx.context.user?.pfp_code, + created_at: (Date.now()), + content, } - ctx.context.chat = this.messages - this.last_got_id[user_id] = this.messages.length - return this.return_html(ctx, 'index') - } - else if (location == 'getnew') { - var part = this.last_got_id.hasOwnProperty(user_id) ? this.last_got_id[user_id] : 0 - this.last_got_id[user_id] = this.messages.length - return this.return_data(ctx, `{"messages":${JSON.stringify(this.messages.slice(part))}}`) + let userID = Number(ctx.context.user?.id) + await knex.query('_message') + // @ts-expect-error + .insert({user_id: userID, created_at: message.created_at, content: message.content}) + + this.onPushListeners.forEach((listener) => listener(message)) } - else if (location == 'postmessage') { - if (ctx.data && ctx.data.form.message) { - var message = ctx.data.form.message.substring(0,255) - var now = (new Date()).toLocaleTimeString('en-US', {hour12: false}) - this.messages.push({ - user: { - name: ctx.context.user?.name, - pfp_code: ctx.context.user?.pfp_code, - }, - time: now, - content: message, + } + message_store = new this.MessageStore() + + override handle: Extension['handle'] = async (ctx) => { + const location = ctx.path.shift() + let [knex]: [Knex] = this.get_dependencies('Knex') + + switch (location) { + case '': + case undefined: { + if (ctx.data && ctx.data.form.message) { + const message = ctx.data.form.message.substring(0,255) + this.message_store.push(ctx, message, knex) + } + return this.return_html(ctx, 'index') + } + case 'history': { + // Should at some point return history by request + + let [message_list, err] = await knex + .query('_message') + .select('_message.created_at', '_message.content', 'user.name', 'user.pfp_code') + .join('user', '_message.user_id', '=', 'user.id') + .then(unpack) + + return this.return_data(ctx, JSON.stringify({messages: message_list})) + } + case 'new_message_event': { + const {req, res} = ctx + + res.writeHead(200, { + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache', + 'X-Accel-Buffering': 'no', + }); + + let counter = 0; + + // Send a message on connection + res.write('event: connected\n'); + res.write(`data: null\n`); + res.write(`id: ${counter}\n\n`); + counter += 1; + + const listener = (msg: message) => { + res.write('event: message\n'); + res.write(`data: ${JSON.stringify(msg)}\n`); + res.write(`id: ${counter}\n\n`); + counter += 1; + } + this.message_store.onPushListeners.add(listener) + + // Close the connection when the client disconnects + req.on('close', () => { + this.message_store.onPushListeners.delete(listener) + res.end('OK') }) + return + } + default: { + return this.return_file(ctx, location) } - return this.return(ctx) - } - else { - return this.return_file(ctx, location) } } } diff --git a/src/extensions/chat/static/chat.js b/src/extensions/chat/static/chat.js new file mode 100644 index 0000000..c41f3d0 --- /dev/null +++ b/src/extensions/chat/static/chat.js @@ -0,0 +1,94 @@ +/** @type {HTMLTableElement} */ +let messages + +window.onload = function() { + const send = document.getElementById('sendmessage') + send.removeAttribute('action') + send.onsubmit = sendMessage + + messages = document.getElementById('messages') + getHistory() +} + +function sendMessage (e) { + e.preventDefault() + const msg = e.target.message.value + + if (msg !== '') { + var http = new XMLHttpRequest() + http.open('POST', window.location.href, true) + http.send('message='+encodeURIComponent(msg)) + } + + e.target.message.value = '' +} + +let lastMessageDate +function addMessage(msg) { + function field(c) { + let f = document.createElement('span') + f.textContent = c + return f + } + + const messageDate = new Date(msg.created_at); // Convert UNIX timestamp to Date + const messageDay = messageDate.toISOString().split('T', 1).at(0) + + //ADD DAY SEPARATOR + if (messageDay !== lastMessageDate) { + const separator_row = messages.insertRow(-1) + separator_row.insertCell(-1) + const separator_col = separator_row.insertCell(-1) + const date_col = separator_col.appendChild(document.createElement('div')) + date_col.classList.add('separator') + date_col.appendChild(field(messageDay)) + } + + + const row = messages.insertRow(-1) + const img_col = row.insertCell(-1) + img_col.innerHTML = `` + const col = row.insertCell(-1) + const info_col = col.appendChild(document.createElement('div')) + info_col.appendChild(field(msg.name)) + info_col.appendChild(field(messageDate.toLocaleTimeString('en-US', {hour12: false}))) + const msg_col = col.appendChild(document.createElement('div')) + msg_col.appendChild(field(msg.content)) + + lastMessageDate = messageDay +} + +function getHistory() { + const http = new XMLHttpRequest() + http.open('GET', window.location.href+'/history', true) + http.responseType = 'json' + + http.onreadystatechange = () => { + if (http.readyState == 4 && http.status == 200) { + data = http.response.messages + if (data) { + for (var i=0; i { + console.log('Connected') +}) + +subscription.addEventListener('error', () => { + console.log('Connection error') +}) + +subscription.addEventListener('message', (e) => { + console.log('Received message') + const message = JSON.parse(e.data) + addMessage(message) +}) diff --git a/src/extensions/chat/static/index.css b/src/extensions/chat/static/index.css index 13cce15..657b2c2 100644 --- a/src/extensions/chat/static/index.css +++ b/src/extensions/chat/static/index.css @@ -4,8 +4,10 @@ } .content > .chat { margin: auto; - max-width: 650px; width: 100%; + height: 100%; + display: flex; + flex-direction: column; } .content #messages { background-color: white; @@ -15,34 +17,57 @@ margin-bottom: var(--margin-normal); width: 100%; display: flex; + flex-grow: 1; overflow-y: scroll; + overflow-x: hidden; scroll-snap-type: y proximity; - max-height: 500px; flex-direction: column-reverse; } .content #messages tr:last-child { scroll-snap-align: end; } -.content #messages tr td { - text-align: start; +.content #messages table { + border-collapse: collapse; } -.content #messages tr :nth-child(1) { +.content #messages tr .pfp { width: 35px; height: 35px; - margin: -15px 0; + margin: 0 0; } -.content #messages tr :nth-child(2) { - font-style: italic; - width: 0; +.content #messages tr > :nth-child(1) { + vertical-align: top; +} +.content #messages tr > :nth-child(2) { + height: 100%; + width: 100%; + display: flex; +} +.content #messages tr > :nth-child(2) > :nth-child(1) { + text-align: start; + height: 100%; + width: max-content; padding-right: var(--margin-small); } -.content #messages tr :nth-child(3) { +.content #messages tr > :nth-child(2) > :nth-child(1) > :nth-child(1) { + display: block; + height: 50%; + width: max-content; + font-style: italic; +} +.content #messages tr > :nth-child(2) > :nth-child(1) > :nth-child(2) { + display: block; + height: 50%; + width: max-content; font-weight: bold; - width: 0; - padding-right: var(--margin-small); + font-size: 12px; +} +.content #messages tr > :nth-child(2) > :nth-child(2) { + padding-left: var(--margin-small); + border-left: 3px solid lightgray; } -.content #messages tr :nth-child(4) { +.content #messages tr > :nth-child(2) > :nth-child(2) > * { word-break: break-all; + width: 100%; } .content form { @@ -65,3 +90,31 @@ .content form > input[type=submit] { width: fit-content; } + +.separator { + margin: auto; +} + + +/* Phones */ +@media (max-width: 700px) { + .content #messages tr > :nth-child(2) { + flex-wrap: wrap; + } + .content #messages tr > :nth-child(2) > :nth-child(1) { + width: 100%; + display: flex; + flex-wrap: nowrap; + gap: 10px; + } + .content #messages tr > :nth-child(2) > :nth-child(1) > :nth-child(1) { + height: 100%; + width: max-content; + text-align: center; + } +} +@media (max-width: 460px) { + .content #messages tr > :nth-child(1) { + display: none; + } +} diff --git a/src/extensions/chat/tables.ts b/src/extensions/chat/tables.ts new file mode 100644 index 0000000..eb3050d --- /dev/null +++ b/src/extensions/chat/tables.ts @@ -0,0 +1,27 @@ +import { MigrationMap, Tables, VersionMap } from '../../classes/tables.ts' +import { Knex } from '../../modules.ts' + +export default class extends Tables { + override versions(versions: VersionMap) { + versions.set('message', 0) + + return versions + } + + override migrations(knex: Knex, migrations: MigrationMap) { + migrations.set('message', { + 0: async ()=>{ + await knex.schema() + .createTable('_message', (table) => { + table.increments('id').primary() + table.integer('user_id').notNullable() + table.foreign('user_id', 'fk_user_id').references('user.id') + table.timestamp('created_at').notNullable().defaultTo(knex.raw('CURRENT_TIMESTAMP')) + table.string('content').notNullable() + }) + }, + }) + + return migrations + } +} diff --git a/src/extensions/invite/index.html b/src/extensions/invite/index.html new file mode 100644 index 0000000..d360296 --- /dev/null +++ b/src/extensions/invite/index.html @@ -0,0 +1,40 @@ +{% extends "extension.html" %} + +{% block head %} + +{% endblock %} + + +{% block body %} + + + + + + + {% if user.is_admin %} + + + {% else %} + + {% endif %} + + + + {% for link in invite_links %} + + + + + {% if user.is_admin %} + + + {% else %} + + {% endif %} + + {% endfor %} + +
IDInvite CodeCreated atCreated byUsed byUsed
{{link.id}}{{link.code}}{{link.created_at}}{{link.created_by}}{{link.used_by if link.used else "Not used"}}{{"Yes" if link.used else "No"}}
+Create Invite +{% endblock %} diff --git a/src/extensions/invite/index.ts b/src/extensions/invite/index.ts new file mode 100644 index 0000000..269b01c --- /dev/null +++ b/src/extensions/invite/index.ts @@ -0,0 +1,185 @@ +import crypto from 'crypto' +import { ExtensionBase, Knex } from '../../modules.ts' +import { unpack } from '../../util.ts' +import minecraft from '../minecraft/index.ts' + +export default class extends ExtensionBase { + override name = 'invite' + override title = 'Invite' + override tables = true + + salt: string + + override init = (context: InitContext) => { + this.salt = context.modules.config.salt + return ExtensionBase.init(this, context) + } + + override requires_login: Extension['requires_login'] = (path) => { + if (['register', 'create_acc', 'register.css'].includes(path.at(0)??'')) { + return false + } + return true + } + + override handle: Extension['handle'] = async (ctx) => { + let [knex]: [Knex] = this.get_dependencies('Knex') + let location = ctx.path.shift() + + switch (location) { + case '': + case undefined: { + if (ctx.context.user?.is_admin) { + let [invite_links, err] = await knex + .query('_invite') + .select('_invite.id', '_invite.code', '_invite.created_at', '_invite.used', 'a.name as used_by', 'b.name as created_by') + .leftJoin(knex.raw('user a'), '_invite.user_id', '=', 'a.id') + .leftJoin(knex.raw('user b'), '_invite.created_by', '=', 'b.id') + .then(unpack) + + ctx.context.invite_links = invite_links + return this.return_html(ctx, 'index') + }else + { + let [invite_links, err] = await knex + .query('_invite') + .select('*') + .where('created_by', ctx.context.user?.id) + .then(unpack) + + ctx.context.invite_links = invite_links + return this.return_html(ctx, 'index') + } + } + case 'create':{ + + const chars = 'abcdefghijklmnopqrstuvwxyz0123456789' + let random_chars = '' + for (let i = 0; i < 10; i++) { + random_chars += chars.charAt(Math.floor(Math.random() * chars.length)); + } + + let code = random_chars + await knex.query('_invite') + // @ts-expect-error + .insert({code: code, created_by: ctx.context.user?.id}) + + return this.return(ctx, undefined, location='/invite') + } + case 'register': { + const invite_code = ctx.args.get('code') + if (!invite_code) + return + + if (await this.inviteCodeValid(invite_code)){ + ctx.context.invite_code = invite_code + return this.return_html(ctx, 'register', undefined, 500, 200, { + "Set-Cookie": this.del_cookie('auth') + }) + } + return this.return(ctx, undefined, "/") + } + case 'create_acc':{ + if (ctx.data) + { + let form: {invite_code?: string, username?: string, password?: string, minecraft_name?: string} = ctx.data.form + const valid_invite_code = await this.inviteCodeValid(form.invite_code) + if (form.username && form.password && valid_invite_code) { + form.username = form.username.substring(0, 32) + + this.addUser(form.username, form.password, async (id?: number, err?: Error) => { + // if invalid credentials + if (err) { + ctx.context.auth_err = err + return this.return_html(ctx, 'login') + } + // success + else { + let auth = Buffer.from(form.username+":"+form.password).toString('base64') + await knex + .query('_invite') + // @ts-expect-error + .update({ + used: true, + user_id: id + }) + .where('code', form.invite_code) + .then() + if (form.minecraft_name)//If a minecraft name was entered + { + await knex + .query('_minecraft_minecraft') + // @ts-expect-error + .insert({minecraft_name: form.minecraft_name, user_id: id}) + + ;(ctx.context.extensions.get('minecraft') as any)?.update_whitelist() + } + + return this.return_html(ctx, 'login', undefined, 500, 303, { + "Location": "/", + "Set-Cookie": this.set_cookie('auth', 'Basic '+auth, true) + }) + } + }) + } + } + return + } + default: { + return this.return_file(ctx, location) + } + } + + } + + private hash_pw(password: string): string { + return crypto.pbkdf2Sync(password, this.salt, 10000, 128, 'sha512').toString('base64') + } + + addUser(name: User['name'], password: User['password'], callback: (id?: number, err?: Error) => void) { + let [knex]: [Knex] = this.get_dependencies('Knex') + + password = this.hash_pw(password) + // Check if username is already taken + this.exists(name, (exists, err) => { + if (err) return callback(undefined, err) + if (exists) return callback(undefined, new Error("Username already taken")) + // add user to db + knex.query('user') + // @ts-expect-error + .insert({name, password, pfp_code: `seed=${name}`}) + .then((id) => callback(id[0] as unknown as number), (err) => callback(undefined, err)) + }) + } + + async inviteCodeValid(code?: string): Promise{ + if (code === undefined) + return false + let [knex]: [Knex] = this.get_dependencies('Knex') + + let [invite, err] = await knex + .query('_invite') + .select('used') + .where('code', code) + .first() + .then(unpack) + + if (!err && !invite?.used) + return true + else + return false + } + + private exists(name: User['name'], callback: (exists: boolean, err?: Error) => void): void { + let [knex]: [Knex] = this.get_dependencies('Knex') + // check if name already exists + knex.query('user') + .select('id') + .where('name', name) + .then((value) => { + callback(!!value.length) + }, (err) => { + callback(false, err) + }) + } +} diff --git a/src/extensions/invite/login.html b/src/extensions/invite/login.html new file mode 100644 index 0000000..e69de29 diff --git a/src/extensions/invite/register.html b/src/extensions/invite/register.html new file mode 100644 index 0000000..bacdfa4 --- /dev/null +++ b/src/extensions/invite/register.html @@ -0,0 +1,36 @@ +{% extends "layout.html" %} + +{% block head %} + +{% endblock %} + + +{% block body %} +

Registration

+
+ + + + + + + + + + + + + + + + + + + + + +
(optional)
+ {{auth_err}} + +
+{% endblock %} diff --git a/src/extensions/invite/static/index.css b/src/extensions/invite/static/index.css new file mode 100644 index 0000000..ad92414 --- /dev/null +++ b/src/extensions/invite/static/index.css @@ -0,0 +1,24 @@ + +table { + margin: var(--margin-normal); + padding-bottom: 10px; + text-align: center; + font-size: smaller; +} + +table th, td{ + border: 1px solid; + padding: 10px; +} + +.used{ + color: #a5a5a5; + font-style: italic; + font-weight: lighter; +} + +@media (max-width: 550px) { + table { + margin: var(--margin-normal) auto; + } +} diff --git a/src/extensions/invite/static/register.css b/src/extensions/invite/static/register.css new file mode 100644 index 0000000..5ff0d17 --- /dev/null +++ b/src/extensions/invite/static/register.css @@ -0,0 +1,48 @@ + +div{ + display: flex; + align-items: center; + justify-content: center; + margin: 2%; +} + +form { + display: flex; + flex-wrap: nowrap; +} +form input { + background-color: silver; + border-radius: 5px; + border: solid 2px darkgrey; + outline: none; +} +form input:focus { + background-color: rgb(221, 221, 221); +} + +.form-button, +.form-button:visited { + all: unset; + background-color: rgb(221, 221, 221); + padding: 5px 10px; + border-radius: var(--margin-small); + display: inline-block; + width: fit-content; + height: 16px; + font-size: 15px; + box-shadow: 0 0 2px black; + text-decoration: none; + color: black; + margin-left: 2px; + box-sizing: content-box; + cursor: pointer; + + margin: 5px; + display: flex; + align-items: center; + justify-content: center; +} + +.form-button:hover { + background-color: darkgrey; +} diff --git a/src/extensions/invite/tables.ts b/src/extensions/invite/tables.ts new file mode 100644 index 0000000..81968c7 --- /dev/null +++ b/src/extensions/invite/tables.ts @@ -0,0 +1,33 @@ +import { MigrationMap, Tables, VersionMap } from '../../classes/tables.ts' +import { Knex } from '../../modules.ts' + +export default class extends Tables { + override versions(versions: VersionMap) { + versions.set('invite', 0) + + return versions + } + + override migrations(knex: Knex, migrations: MigrationMap) { + migrations.set('invite', { + 0: async ()=>{ + await knex.schema() + .createTable('_invite', (table) => { + table.increments('id').primary() + table.string('code').notNullable() + table.datetime('created_at').notNullable().defaultTo(knex.raw('CURRENT_TIMESTAMP')) + table.boolean('used').notNullable().defaultTo(false) + table.integer('user_id') + table.foreign('user_id', 'fk_user_id').references('user.id') + table.integer('created_by') + table.foreign('created_by', 'fk_created_by').references('user.id') + }) + await knex.query('_invite') + // @ts-expect-error + .insert({code: 'admin'}) + }, + }) + + return migrations + } +} diff --git a/src/extensions/minecraft/index.html b/src/extensions/minecraft/index.html new file mode 100644 index 0000000..a86863b --- /dev/null +++ b/src/extensions/minecraft/index.html @@ -0,0 +1,22 @@ +{% extends "extension.html" %} + +{% block head %} + +{% endblock %} + +{% block body %} + +
+ + + + + + +
+ {{auth_err}} +
+ +Updating the whitelist can take up to a minute, please be patient. + +{% endblock %} diff --git a/src/extensions/minecraft/index.ts b/src/extensions/minecraft/index.ts new file mode 100644 index 0000000..4fbed5b --- /dev/null +++ b/src/extensions/minecraft/index.ts @@ -0,0 +1,111 @@ +import { ExtensionBase } from "../../modules.js" +import Knex from "../../modules/knex.ts" +import { unpack } from "../../util.ts" +import * as rcon from "./lib.ts" + +export default class extends ExtensionBase { + override name = 'minecraft' + override title = 'Minecraft' + override tables = true + + override init: Extension['init'] = async (context) => { + // Init super here as rest of init happens async + const result = ExtensionBase.init(this, context) + + // On a separate "thread" in case we can't connect to the RCON server immediately + setTimeout(this.update_whitelist, 0) + + return result + } + + override handle: Extension['handle'] = async (ctx) => { + var location = ctx.path.shift() + let [knex]: [Knex] = this.get_dependencies('Knex') + + switch (location) { + case '': + case undefined: { + let [name, err] = await knex + .query('_minecraft') + .select('minecraft_name') + .where('user_id', ctx.context.user?.id) + .first() + .then(unpack) + + if (name) + ctx.context.minecraft_username = name.minecraft_name + else + ctx.context.minecraft_username = "" + return this.return_html(ctx, 'index') + } + case 'change':{ + if (ctx.data) + { + let new_MCName = ctx.data.form.minecraft_name ?? '' + let [name, err] = await knex + .query('_minecraft') + .select('minecraft_name') + .where('user_id', ctx.context.user?.id) + + if (name){ + await knex + .query('_minecraft') + .update('minecraft_name', new_MCName) + .where('user_id', ctx.context.user?.id) + }else{ + await knex + .query('_minecraft') + // @ts-expect-error + .insert({minecraft_name: new_MCName, user_id: ctx.context.user?.id}) + } + + // Intentionally not awaited as it can take a while + this.update_whitelist() + } + + return this.return(ctx, undefined, location='/minecraft') + } + default: { + return this.return_file(ctx, location) + } + } + } + + update_whitelist = async () => { + const [knex]: [Knex] = this.get_dependencies('Knex') + const [raw_names, err] = await knex + .query('_minecraft') + .select('minecraft_name') + .then(unpack<{minecraft_name: string}[]>, unpack) + if (err) + return + const names: string[] = (raw_names ?? []) + // Unpack objects + .flatMap((name) => name.minecraft_name) + // Remove empty rows + .filter((name) => name != '') + + const currently_whitelisted_raw = await rcon.send("whitelist list") + const currently_whitelisted = currently_whitelisted_raw + .split(' ') + // Remove trash + .slice(5) + .flatMap((name) => { + if (name.endsWith(',')) + return name.substring(0, name.length -1) + return name + }) + + const to_remove = currently_whitelisted.filter((name) => !names.includes(name)) + const to_add = names.filter((name) => !currently_whitelisted.includes(name)) + + for (const name of to_remove) { + const response = await rcon.send(`whitelist remove ${name}`) + console.log(response) + } + for (const name of to_add) { + const response = await rcon.send(`whitelist add ${name}`) + console.log(response) + } + } +} diff --git a/src/extensions/minecraft/lib.ts b/src/extensions/minecraft/lib.ts new file mode 100644 index 0000000..291e67a --- /dev/null +++ b/src/extensions/minecraft/lib.ts @@ -0,0 +1,60 @@ +import { Rcon } from "rcon-client" + +function log(...args: any[]) { + console.log("[MINECRAFT]:",...args) +} + +let is_connected = false + +const rcon = new Rcon({ + host: '127.0.0.1', + port: 25575, + password: '1234', +}) +export const raw = rcon + +export const send = async (command: string) => { + await connected() + return rcon.send(command).catch(() => '') +} +export const sendRaw = async (buffer: Buffer) => { + await connected() + return rcon.sendRaw(buffer).catch(() => '') +} + +export const connected = async (): Promise => new Promise((resolve) => { + const loop = setInterval(async () => { + if (!is_connected) { + await rcon.connect().then( + () => { + is_connected = true + clearInterval(loop) + resolve() + }, + (err: Error) => { + is_connected = false + log("Failed connecting:", err.message) + } + ) + } + else { + clearInterval(loop) + resolve() + } + }, 5000) +}) + +rcon.on('connect', () => { + is_connected = true + log("connected") +}) +rcon.on('end', () => { + is_connected = false + log("disconnected") +}) +rcon.on('error', (err) => { + log("err: ", err) +}) +rcon.on('authenticated', () => { + log("authenticated") +}) diff --git a/src/extensions/minecraft/package-lock.json b/src/extensions/minecraft/package-lock.json new file mode 100644 index 0000000..49e7b6c --- /dev/null +++ b/src/extensions/minecraft/package-lock.json @@ -0,0 +1,30 @@ +{ + "name": "minecraft", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "minecraft", + "version": "0.0.1", + "dependencies": { + "rcon-client": "^4.2.5" + } + }, + "node_modules/rcon-client": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/rcon-client/-/rcon-client-4.2.5.tgz", + "integrity": "sha512-AnX1GU/ZTlwtYup3H6h0J1hwfP3OYltXVe+8ReBzmNEepX3xGH8nDg7gYqT5Y9rpAS/LmQ48h0BKINt1YGd8bA==", + "license": "MIT", + "dependencies": { + "typed-emitter": "^0.1.0" + } + }, + "node_modules/typed-emitter": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/typed-emitter/-/typed-emitter-0.1.0.tgz", + "integrity": "sha512-Tfay0l6gJMP5rkil8CzGbLthukn+9BN/VXWcABVFPjOoelJ+koW8BuPZYk+h/L+lEeIp1fSzVRiWRPIjKVjPdg==", + "license": "MIT" + } + } +} diff --git a/src/extensions/minecraft/package.json b/src/extensions/minecraft/package.json new file mode 100644 index 0000000..8c967cc --- /dev/null +++ b/src/extensions/minecraft/package.json @@ -0,0 +1,8 @@ +{ + "name": "minecraft", + "version": "0.0.1", + "type": "module", + "dependencies": { + "rcon-client": "^4.2.5" + } +} diff --git a/src/extensions/minecraft/static/index.css b/src/extensions/minecraft/static/index.css new file mode 100644 index 0000000..5ab529b --- /dev/null +++ b/src/extensions/minecraft/static/index.css @@ -0,0 +1,42 @@ +form{ + display: flex; + align-items: center; + justify-content: center; + margin-top: 5%; +} + +td{ + padding: 5px; +} + +.form-button, +.form-button:visited { + all: unset; + background-color: rgb(221, 221, 221); + padding: 5px 10px; + border-radius: var(--margin-small); + display: inline-block; + width: fit-content; + height: 16px; + font-size: 15px; + box-shadow: 0 0 2px black; + text-decoration: none; + color: black; + margin-left: 2px; + box-sizing: content-box; + cursor: pointer; +} + +.form-button:hover { + background-color: darkgrey; +} + +form input { + background-color: silver; + border-radius: 5px; + border: solid 2px darkgrey; + outline: none; +} +form input:focus { + background-color: rgb(221, 221, 221); +} diff --git a/src/extensions/minecraft/tables.ts b/src/extensions/minecraft/tables.ts new file mode 100644 index 0000000..9f43237 --- /dev/null +++ b/src/extensions/minecraft/tables.ts @@ -0,0 +1,26 @@ +import { MigrationMap, Tables, VersionMap } from '../../classes/tables.ts' +import { Knex } from '../../modules.ts' + +export default class extends Tables { + override versions(versions: VersionMap) { + versions.set('minecraft', 0) + + return versions + } + + override migrations(knex: Knex, migrations: MigrationMap) { + migrations.set('minecraft', { + 0: async ()=>{ + await knex.schema() + .createTable('_minecraft', (table) => { + table.increments('id').primary() + table.string('minecraft_name').notNullable() + table.integer('user_id') + table.foreign('user_id', 'fk_user_id').references('user.id') + }) + }, + }) + + return migrations + } +} diff --git a/src/extensions/profile/index.ts b/src/extensions/profile/index.ts index 3a76bc2..7e36ed5 100644 --- a/src/extensions/profile/index.ts +++ b/src/extensions/profile/index.ts @@ -6,6 +6,7 @@ export default class extends ExtensionBase { override name = 'profile' override title = 'Network' override tables = true + override disabled = true wg: any = null wg_config: any = null diff --git a/src/extensions/profile/tables.ts b/src/extensions/profile/tables.ts index c92609b..0887e95 100644 --- a/src/extensions/profile/tables.ts +++ b/src/extensions/profile/tables.ts @@ -14,7 +14,7 @@ export default class extends Tables { await knex.schema().createTable('_device', (table) => { table.increments('id').primary() table.integer('user_id').notNullable() - table.foreign('user_id', 'fk_user_id').references('_root_user.id') + table.foreign('user_id', 'fk_user_id').references('user.id') table.string('name') table.uuid('uuid').notNullable() table.string('ip').notNullable() diff --git a/src/extensions/root/index.html b/src/extensions/root/index.html deleted file mode 100644 index 4431dfd..0000000 --- a/src/extensions/root/index.html +++ /dev/null @@ -1,5 +0,0 @@ -{% extends "layout.html" %} - -{% block body %} - {{index_text |safe}} -{% endblock %} diff --git a/src/extensions/root/index.ts b/src/extensions/root/index.ts index ee737e1..9c76711 100644 --- a/src/extensions/root/index.ts +++ b/src/extensions/root/index.ts @@ -56,7 +56,7 @@ export default class extends ExtensionBase implements RootExtension { if (!ctx.context.user) { // Attempt if (ctx.data) { - let form = ctx.data.form + let form: {login?: string, register?: string, username?: string, password?: string} = ctx.data.form // Login if (form.login) { let auth = ''; @@ -68,25 +68,6 @@ export default class extends ExtensionBase implements RootExtension { "Set-Cookie": this.set_cookie('auth', 'Basic '+auth, true) }) } - // Register - else if (form.register) { - this.addUser(form.username, form.password, (err?: Error) => { - // if invalid credentials - if (err) { - ctx.context.auth_err = err - return this.return_html(ctx, 'login') - } - // success - else { - let auth = Buffer.from(form.username+":"+form.password).toString('base64') - return this.return_html(ctx, 'login', undefined, 500, 303, { - "Location": "/", - "Set-Cookie": this.set_cookie('auth', 'Basic '+auth, true) - }) - } - }) - return - } } // First load return this.return_html(ctx, 'login', undefined, 500, 200, { @@ -181,17 +162,17 @@ export default class extends ExtensionBase implements RootExtension { else return new Error('Wrong name or password') } - else if (ip.startsWith(subnet)) { - // Try using IP-address if no name and password - const user = await knex - .query({u: 'user', p: '_profile_device'}) - .select('u.*') - .join('_profile_device', 'u.id', '=', 'p.user_id') - .where('p.ip', ip) - .first() - - return user - } + // else if (ip.startsWith(subnet)) { + // // Try using IP-address if no name and password + // const user = await knex + // .query({u: 'user', p: '_profile_device'}) + // .select('u.*') + // .join('_profile_device', 'u.id', '=', 'p.user_id') + // .where('p.ip', ip) + // .first() + + // return user + // } } private decrypt_auth(auth: BasicAuth): [name: string, password: string] | Error { diff --git a/src/extensions/root/login.html b/src/extensions/root/login.html index 8427eb0..94dd479 100644 --- a/src/extensions/root/login.html +++ b/src/extensions/root/login.html @@ -1,21 +1,20 @@ {% extends "layout.html" %} {% block body %} -

Register / Log in

+

Log in

- + - + - diff --git a/src/extensions/root/static/index.css b/src/extensions/root/static/index.css index 0cad870..b0dde89 100644 --- a/src/extensions/root/static/index.css +++ b/src/extensions/root/static/index.css @@ -8,8 +8,6 @@ --header-spacing: 3px; --header-spacing-large: 6px; - --ext-height: 25px; - --margin-small: 5px; --margin-normal: 10px; --margin-normal-large: 15px; @@ -21,6 +19,10 @@ --color-link: darkslateblue; } +* { + box-sizing: border-box; +} + body { padding: 0; margin: 0; @@ -63,6 +65,7 @@ a:visited { text-decoration: none; color: black; margin-left: 2px; + box-sizing: content-box; } .button:hover { background-color: darkgrey; @@ -73,49 +76,6 @@ a:visited { cursor: default; } -header { - display: flex; - flex-wrap: wrap; - background-color: rgba(119, 136, 153, 0.9); - align-items: flex-start; - max-width: var(--max-width); - width: 100%; - margin: 0 auto; - border-bottom-left-radius: var(--margin-normal); - border-bottom-right-radius: var(--margin-normal); - overflow: hidden; -} -header > nav { - display: flex; - flex-wrap: wrap; -} -header > nav > * { - max-height: var(--header-height); -} -header > nav.left { - margin-right: auto; -} -header > nav.left > :first-child { - padding-left: var(--header-spacing-large); -} -header > nav.right > :last-child { - padding-right: var(--header-spacing-large); -} -header > nav > a { - all: initial; - cursor: pointer; - text-decoration: underline dotted rgba(0,0,0,0.4); - line-height: 25px; - padding: 0 var(--header-spacing); -} -header > nav > a:hover { - background-color: rgba(0,0,0,0.1); - text-decoration: underline dotted rgba(0,0,0,0.9); -} -header > nav > a:visited { - color: black; -} - .main { max-width: var(--max-width); width: 100%; @@ -126,60 +86,148 @@ header > nav > a:visited { border-radius: 10px; padding: 10px 10px 10px 10px; box-sizing: border-box; + display: flex; } -.extensions { - width: 100%; - display: flex; - flex-wrap: wrap; +.sidebar { + width: max-content; + max-width: 150px; } -.extensions > a { - background-color: gray; - padding: 5px 10px; + +.sidebar > * { + margin-right: 5px; +} + +.sidebar > hr { + border-color: rgba(0, 0, 0, 0.3); +} + +.sidebar .item { + display: flex; + background-color: darkgray; + padding: 5px 10px 3px; + margin-bottom: 3px; box-sizing: border-box; - height: var(--ext-height); font-size: 15px; box-shadow: inset 0 0 2px black; border-radius: var(--margin-small); - border-bottom-left-radius: 0; - border-bottom-right-radius: 0; color: black; text-decoration: none; } -.extensions > a:hover { + +.sidebar .item * { + margin: auto; +} + +.sidebar .item:hover { background-color: darkgrey; color: black; text-decoration: none; } -.extensions .selected { +.sidebar .selected { background-color: var(--background-content) !important; - box-shadow: none; +} +.sidebar .item > img { + width: 25px; + height: 25px; + position: relative; + top: -2px; + right: -5px; } -.extensions .right { - margin-left: auto; +.sidebar > .extensions { + display: flex; + flex-wrap: wrap; + flex-flow: column; } -.extensions span { - position: relative; - top: -15px; +.sidebar > .extensions > * { + background-color: gray; } -.extensions img { - width: calc(var(--ext-height) - 2px); - height: var(--ext-height); - position: relative; - top: -5px; - margin: 0 -5px; +.topbar { + display: none; + width: 100%; + height: max-content; + align-items: baseline; + border-radius: 0; + z-index: 3; + background-color: rgba(0,0,0,0.5); +} +.topbar > h3 { + width: 100%; + color: white; + text-align: center; +} +.topbar > .button { + width: 30px; + height: 30px; + font-size: large; + margin-left: var(--margin-normal-large); + margin-top: 10px; + margin-bottom: 10px; + box-sizing: border-box; } .content { - max-height: var(--max-window-height); + max-height: 100%; + height: min(var(--max-window-height), 90vh); max-width: var(--max-window-width); width: 100%; overflow-y: auto; overflow-x: hidden; background-color: var(--background-content); - border-bottom-left-radius: var(--margin-small); padding-left: var(--margin-normal); box-sizing: border-box; + border-radius: 5px; +} + + +/* Phones */ +@media (max-width: 550px) { + .sidebar { + display: none; + } + .topbar { + display: flex; + } + .main { + max-width: 100%; + max-height: 100%; + width: 100%; + height: 100%; + margin: 0; + padding: 0; + flex-direction: column; + border-radius: 0; + } + .content { + height: 100%; + border-radius: 0; + } + .sidebar { + flex-direction: column-reverse; + position: absolute; + max-width: 100%; + width: 100%; + height: calc(100% - 60px); + top: 60px; + padding: var(--margin-normal) var(--margin-normal); + overflow-y: auto; + } + .sidebar > :nth-child(1) img { + height: var(--margin-normal-large); + width: var(--margin-normal-large); + top: 0; + right: 0; + } + .sidebar > * { + margin-right: 0; + text-align: center; + } + .sidebar .item { + width: 100%; + display: block; + height: 40px; + line-height: 30px; + } } diff --git a/src/extensions/root/static/manifest.json b/src/extensions/root/static/manifest.json new file mode 100644 index 0000000..081ee99 --- /dev/null +++ b/src/extensions/root/static/manifest.json @@ -0,0 +1,47 @@ +{ + "name": "KeukNet", + "short_name": "KeukNet", + "theme_color": "#694fad", + "background_color": "#694fad", + "display_override": ["standalone"], + "display": "standalone", + "scope": "/", + "start_url": "/", + "icons": [ + { + "src": "/android-chrome-36x36.png", + "sizes": "36x36", + "type": "image/png" + }, + { + "src": "/android-chrome-48x48.png", + "sizes": "48x48", + "type": "image/png" + }, + { + "src": "/android-chrome-72x72.png", + "sizes": "72x72", + "type": "image/png" + }, + { + "src": "/android-chrome-96x96.png", + "sizes": "96x96", + "type": "image/png" + }, + { + "src": "/android-chrome-144x144.png", + "sizes": "144x144", + "type": "image/png" + }, + { + "src": "/android-chrome-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/android-chrome-256x256.png", + "sizes": "256x256", + "type": "image/png" + } + ] +} \ No newline at end of file diff --git a/src/extensions/root/user.html b/src/extensions/root/user.html index 3fb66e8..54b13a8 100644 --- a/src/extensions/root/user.html +++ b/src/extensions/root/user.html @@ -5,7 +5,9 @@

Hello {{user.name}}!


Edit -

You registered at {{user.regdate}}

+ Logout + Get started +

You registered at {{user.registration_date}}

{% endblock %} diff --git a/src/extensions/webrtc/client.html b/src/extensions/webrtc/client.html new file mode 100644 index 0000000..8fdab65 --- /dev/null +++ b/src/extensions/webrtc/client.html @@ -0,0 +1,28 @@ +{% extends "extension.html" %} + +{% block head %} + + + + + + +{% endblock %} + +{% block body %} +

STATUS:

+

CHANNEL:

+
+
+ {% for channel in channels %} +
+ {% endfor %} +
+{% endblock %} diff --git a/src/extensions/webrtc/index.ts b/src/extensions/webrtc/index.ts new file mode 100644 index 0000000..be8b880 --- /dev/null +++ b/src/extensions/webrtc/index.ts @@ -0,0 +1,243 @@ +import { ExtensionBase } from "../../modules.js" +import fs from "fs" +import http from "http" +import https from "https" +import {Server, Socket} from "socket.io" + +type user = {style: string, name: string} +type data = {channel: string, user?: user, id: string} + +export default class extends ExtensionBase { + override name = 'webrtc' + override title = 'Voice Chat' + + port = 0 + domain = "" + channels = ['1', '2', '3', 'exile'] + + private DataStore = class { + data: {[channel: string]: Map} = {} + onJoinListeners: Set<(data: data) => void> = new Set() + onPartListeners: Set<(data: data) => void> = new Set() + + constructor(channels: string[]) { + channels.forEach(channel => { + this.data[`channel-${channel}`] = new Map() + }) + } + + join(id: string, channel: string, user: user) { + const data: data = { + user: { + name: user.name, + style: user.style, + }, + channel: channel, + id: id, + } + this.data[channel]?.set(id, user) + this.onJoinListeners.forEach((listener) => listener(data)) + } + + part(id: string, channel: string) { + const data: data = { + channel: channel, + id: id, + } + this.data[channel]?.delete(id) + this.onPartListeners.forEach((listener) => listener(data)) + } + } + data_store = new this.DataStore(this.channels) + + override init: Extension['init'] = async (context) => { + // Port of Socket.IO server + const PORT = 8080 + // Port that is sent to clients, can differ for reverse-proxied services + this.port = 8080 + this.domain = context.modules.config.domain + + + let server + if (context.modules.config.nginx) { + server = http.createServer(() => {}) + } + else { + let privateKey = fs.readFileSync(context.modules.config.private_key_path, "utf8") + let certificate = fs.readFileSync(context.modules.config.server_cert_path, "utf8") + let credentials = { key: privateKey, cert: certificate } + server = https.createServer(credentials, () => {}) + } + + server.listen(PORT, undefined, function() { + console.log("Listening on port " + PORT) + }) + + const io = new Server(server, { + cors: { + origin: `https://${this.domain}:8080`, + credentials: true, + } + }) + + let channels: {[channel: string]: any} = {} + let sockets: {[id: string]: Socket} = {} + let socket_channels: {[id: string]: any} = {} + + io.sockets.on('connection', (socket) => { + socket_channels[socket.id] = {} + sockets[socket.id] = socket + + console.log(`RTC [${socket.id}] connection accepted`) + socket.on('disconnect', () => { + for (const channel in socket_channels[socket.id]) { + part(channel) + } + console.log(`RTC [${socket.id}] disconnected`) + delete sockets[socket.id] + }) + + + socket.on('join', (config) => { + console.log(`RTC [${socket.id}] join '${config.channel}'`) + const channel = config.channel + const userdata = config.userdata + + if (channel in socket_channels[socket.id]) { + console.log(`RTC [${socket.id}] ERROR: already joined '${channel}'`) + return + } + + if (!(channel in channels)) { + channels[channel] = {} + } + + this.data_store.join(socket.id, channel, userdata) + + for (const id in channels[channel]) { + channels[channel][id].emit('addPeer', {peer_id: socket.id, should_create_offer: false}) + socket.emit('addPeer', {peer_id: id, should_create_offer: true}) + } + + channels[channel][socket.id] = socket + socket_channels[socket.id][channel] = channel + }); + + const part = (channel: string) => { + console.log(`RTC [${socket.id}] part`) + + if (!(channel in socket_channels[socket.id])) { + console.log(`RTC [${socket.id}] ERROR: not in '${channel}'`) + return + } + + delete socket_channels[socket.id][channel] + delete channels[channel][socket.id] + + this.data_store.part(socket.id, channel) + + for (const id in channels[channel]) { + channels[channel][id].emit('removePeer', {'peer_id': socket.id}) + socket.emit('removePeer', {'peer_id': id}) + } + } + socket.on('part', part) + + socket.on('relayICECandidate', (config) => { + const peer_id = config.peer_id + const ice_candidate = config.ice_candidate + console.log(`[${socket.id}] relaying ICE candidate to [${peer_id}]`) + + if (peer_id in sockets) { + sockets[peer_id]?.emit('iceCandidate', {peer_id: socket.id, ice_candidate: ice_candidate}) + } + }) + + socket.on('relaySessionDescription', (config) => { + const peer_id = config.peer_id + const session_description = config.session_description + console.log(`[${socket.id}] relaying session description to [${peer_id}]`) + + if (peer_id in sockets) { + sockets[peer_id]?.emit('sessionDescription', {peer_id: socket.id, session_description: session_description}) + } + }) + }) + + return ExtensionBase.init(this, context) + } + + + override requires_login: Extension['requires_login'] = (path) => { + if (path.at(0) == 'client.js') return false + return true + } + + override handle: Extension['handle'] = (ctx) => { + var location = ctx.path.shift() + + switch (location) { + case '': + case undefined: { + ctx.context.domain = this.domain + ctx.context.port = this.port + ctx.context.channels = this.channels + ctx.context.channel_users = JSON.stringify(this.data_store.data, (k, v) => { + if (v instanceof Map) { + return { + dataType: 'Map', + value: Array.from(v.entries()), + } + } + else { + return v + } + }) + return this.return_html(ctx, 'client') + } + case 'user_channel_event': { + const {req, res} = ctx + + res.writeHead(200, { + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache', + 'X-Accel-Buffering': 'no', + }) + + let counter = 0 + + // Send a message on connection + res.write('event: connected\n') + res.write(`data: null\n`) + res.write(`id: ${counter}\n\n`) + counter += 1 + + const onJoinListener = (data: data) => { + res.write('event: user_join\n') + res.write(`data: ${JSON.stringify(data)}\n`) + res.write(`id: ${counter}\n\n`) + counter += 1 + } + const onPartListener = (data: data) => { + res.write('event: user_part\n') + res.write(`data: ${JSON.stringify(data)}\n`) + res.write(`id: ${counter}\n\n`) + counter += 1 + } + this.data_store.onJoinListeners.add(onJoinListener) + this.data_store.onPartListeners.add(onPartListener) + + // Close the connection when the client disconnects + req.on('close', () => { + this.data_store.onJoinListeners.delete(onJoinListener) + this.data_store.onPartListeners.delete(onPartListener) + res.end('OK') + }) + return + } + default: { + this.return_file(ctx, location) + } + } + } +} diff --git a/src/extensions/webrtc/package-lock.json b/src/extensions/webrtc/package-lock.json new file mode 100644 index 0000000..00b05bb --- /dev/null +++ b/src/extensions/webrtc/package-lock.json @@ -0,0 +1,317 @@ +{ + "name": "webrtc", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "webrtc", + "version": "0.0.1", + "dependencies": { + "socket.io": "4.7.5" + } + }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" + }, + "node_modules/@types/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==", + "license": "MIT" + }, + "node_modules/@types/cors": { + "version": "2.8.17", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", + "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "22.5.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.0.tgz", + "integrity": "sha512-DkFrJOe+rfdHTqqMg0bSNlGlQ85hSoh2TPzZyhHsXnMtligRWpxUySiyw8FY14ITt24HVCiQPWxS3KO/QlGmWg==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "license": "MIT", + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/engine.io": { + "version": "6.5.5", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.5.tgz", + "integrity": "sha512-C5Pn8Wk+1vKBoHghJODM63yk8MvrO9EWZUfkAt5HAqIgPE4/8FF0PEGHXtEd40l223+cE5ABWuPzm38PHFXfMA==", + "license": "MIT", + "dependencies": { + "@types/cookie": "^0.4.1", + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.4.1", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io/node_modules/cookie": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", + "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/node_modules/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "license": "MIT", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/engine.io/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "license": "MIT" + }, + "node_modules/mime-db": { + "version": "1.52.0", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/negotiator": { + "version": "0.6.3", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/socket.io": { + "version": "4.7.5", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.7.5.tgz", + "integrity": "sha512-DmeAkF6cwM9jSfmp6Dr/5/mfMwb5Z5qRrSXLpo3Fq5SqyU8CMF15jIN4ZhfSwu35ksM1qmHZDQ/DK5XTccSTvA==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.3.2", + "engine.io": "~6.5.2", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", + "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", + "license": "MIT", + "dependencies": { + "debug": "~4.3.4", + "ws": "~8.17.1" + } + }, + "node_modules/socket.io-adapter/node_modules/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "license": "MIT", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-adapter/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "license": "MIT" + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser/node_modules/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "license": "MIT", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-parser/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "license": "MIT" + }, + "node_modules/socket.io/node_modules/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "license": "MIT", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "license": "MIT" + }, + "node_modules/vary": { + "version": "1.1.2", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + } +} diff --git a/src/extensions/webrtc/package.json b/src/extensions/webrtc/package.json new file mode 100644 index 0000000..e867716 --- /dev/null +++ b/src/extensions/webrtc/package.json @@ -0,0 +1,8 @@ +{ + "name": "webrtc", + "version": "0.0.1", + "type": "module", + "dependencies": { + "socket.io": "4.7.5" + } +} diff --git a/src/extensions/webrtc/static/client.js b/src/extensions/webrtc/static/client.js new file mode 100644 index 0000000..5fa5d0c --- /dev/null +++ b/src/extensions/webrtc/static/client.js @@ -0,0 +1,221 @@ +var local_user = {audio: null, stream: null, con: null} + +/** @type {Map} */ +let peers = new Map() + +const sounds = { + 'join': "join.mp3", + 'leave': "leave.mp3", +} + +document.addEventListener('DOMContentLoaded', init, false) + +function play(url) { + new Audio('/webrtc/'+url).play() +} + +function new_audio(stream, peer_id) { + let audio = new Audio() + audio.autoplay = "autoplay" + audio.controls = "" + audio.preload = "none" + audio.crossOrigin = "anonymous" + audio.id = peer_id + + document.body.append(audio) + audio.srcObject = stream + + return audio +} + +function remove(id) { + const peer = peers.get(id) + peer.close() + peers.delete(id) + + peer_audio.get(id).remove() + peer_audio.delete(id) +} + +function init() { + set_status("connecting to server") + console.log("Connecting to signaling server") + let connection = io(SIGNALING_SERVER, {transports: ["websocket", "polling"]}) + local_user.con = connection + + connection.on('connect', async () => { + set_status("connected to server") + console.log("Connected to signaling server") + await setup_local_media() + set_status("ready to join") + update_displays() + }) + + connection.on('disconnect', () => { + set_status("disconnected from server") + update_displays() + console.log("Disconnected from signaling server") + + for (const peer of peers.values()) { + remove(peer) + } + }) + + connection.on('connect_error', (err) => { + set_status("error whilst connecting to server: "+err.message) + }) + + connection.on('connect_failed', (err) => { + set_status("failed to connect to server") + }) + + connection.on('addPeer', async (config) => { + console.log('Signaling server said to add peer:', config) + const peer_id = config.peer_id + + if (peer_id in peers) { + console.log("Already connected to peer ", peer_id) + return + } + + const peer_connection = new RTCPeerConnection({ + "optional": [{"DtlsSrtpKeyAgreement": true}], + iceServers: [ + { + urls: "stun:stun.relay.metered.ca:80", + }, + { + urls: "turn:global.relay.metered.ca:80", + username: "f73a3cd0408ad7d03bf58707", + credential: "AuH9kjSoj6Ps8IJ0", + }, + { + urls: "turn:global.relay.metered.ca:80?transport=tcp", + username: "f73a3cd0408ad7d03bf58707", + credential: "AuH9kjSoj6Ps8IJ0", + }, + { + urls: "turn:global.relay.metered.ca:443", + username: "f73a3cd0408ad7d03bf58707", + credential: "AuH9kjSoj6Ps8IJ0", + }, + { + urls: "turns:global.relay.metered.ca:443?transport=tcp", + username: "f73a3cd0408ad7d03bf58707", + credential: "AuH9kjSoj6Ps8IJ0", + }, + ], + }) + + peers.set(peer_id, peer_connection) + + peer_connection.onicecandidate = (event) => { + if (event.candidate) { + connection.emit('relayICECandidate', { + 'peer_id': peer_id, + 'ice_candidate': { + 'sdpMLineIndex': event.candidate.sdpMLineIndex, + 'candidate': event.candidate.candidate + } + }) + } + } + peer_connection.ontrack = (event) => { + console.log("ontrack", event) + const audio = new_audio(event.streams[0], peer_id) + peer_audio.set(peer_id, audio) + update_displays() + play(sounds.join) + } + + /* Add our local stream */ + peer_connection.addStream(local_user.stream) + + /* Only one side of the peer connection should create the + * offer, the signaling server picks one to be the offerer. + * The other user will get a 'sessionDescription' event and will + * create an offer, then send back an answer 'sessionDescription' to us + */ + if (config.should_create_offer) { + console.log("Creating RTC offer to ", peer_id) + const local_description = await peer_connection.createOffer({offerToReceiveAudio: true}) + await peer_connection.setLocalDescription(local_description).catch(() => { + alert("Offer setLocalDescription failed!") + }) + connection.emit('relaySessionDescription', {'peer_id': peer_id, 'session_description': local_description}) + console.log("Offer setLocalDescription succeeded") + } + }) + + connection.on('sessionDescription', async (config) => { + const peer_id = config.peer_id + const peer = peers.get(peer_id) + const remote_description = config.session_description + + const desc = new RTCSessionDescription(remote_description) + await peer.setRemoteDescription(desc).catch((error) => { + console.log("setRemoteDescription error: ", error) + }) + + console.log("setRemoteDescription succeeded") + if (remote_description.type == "offer") { + console.log("Creating answer") + const local_description = await peer.createAnswer().catch((error) => { + console.log("Error creating answer: ", error) + }) + + await peer.setLocalDescription(local_description).catch(() => { + Alert("Answer setLocalDescription failed!") + }) + + connection.emit('relaySessionDescription', {'peer_id': peer_id, 'session_description': local_description}) + console.log("Answer setLocalDescription succeeded") + } + }) + + connection.on('iceCandidate', (config) => { + const peer = peers.get(config.peer_id) + const ice_candidate = config.ice_candidate + peer.addIceCandidate(new RTCIceCandidate(ice_candidate)) + }) + + connection.on('removePeer', (config) => { + console.log('Signaling server said to remove peer:', config) + const peer_id = config.peer_id + if (peers.has(peer_id)) { + remove(peer_id) + } + play(sounds.leave) + }) +} + +async function setup_local_media() { + if (local_user.stream != null) + return + + set_status("Obtaining microphone access") + console.log("Requesting access to local audio inputs") + + navigator.getUserMedia = ( navigator.getUserMedia || + navigator.webkitGetUserMedia || + navigator.mozGetUserMedia || + navigator.msGetUserMedia) + + const stream = await navigator.mediaDevices.getUserMedia({"audio":true}).catch((err) => { + /* user denied access to microphone */ + set_status("failed to obtain microphone access") + throw Error(`Access denied for audio: ${err}`) + }) + + /* user accepted access to microphone */ + set_status("successfully obtained microphone access") + console.log("Access granted to audio") + local_user.stream = stream + local_user.audio = new_audio(stream) + local_user.audio.volume = 0.0 + peer_audio.set('local', local_user.audio) +} + +function set_status(text) { + document.getElementById('status').value = text +} diff --git a/src/extensions/webrtc/static/display.js b/src/extensions/webrtc/static/display.js new file mode 100644 index 0000000..92d296b --- /dev/null +++ b/src/extensions/webrtc/static/display.js @@ -0,0 +1,122 @@ +/** + * @typedef {{name: string, style: string}} user + */ + +/** @type {Object.>} All channels and their users */ +const channel_users = JSON.parse(CHANNEL_USERS_STUFF, (k, v) => { + if (typeof v === 'object' && v !== null) { + if (v.dataType === 'Map') { + return new Map(v.value) + } + } + return v +}) + +/** + * @param {string} name + * @returns {HTMLDivElement} + */ +function new_channel(name) { + const channel = document.createElement('div') + channel.id = name + channel.classList = ['channel'] + + const channel_title = document.createElement('b') + channel_title.innerText = name.substring(8) + + const toggle_button = document.createElement('a') + toggle_button.classList = ['button'] + toggle_button.textContent = current_channel == name ? "Leave" : "Join" + toggle_button.onclick = (e) => { + if (current_channel == name) { + toggle_button.innerText = "Join" + part_channel() + } + else { + toggle_button.innerText = "Leave" + join_channel(name) + } + } + + const users_div = document.createElement('div') + users_div.classList = ['users'] + const users = channel_users[name] + for (const [id, user] of users.entries()) { + const user_div = document.createElement('div') + user_div.classList = ['user'] + + const icon = new Image() + icon.src = DICEBEAR_HOST + "?" + user.style + + const text = document.createElement('h3') + text.innerText = user.name + + user_div.append(icon, text) + + if (peer_audio.has(id) || (user.name == USERNAME && peer_audio.has('local'))) { + /** @type {HTMLAudioElement} */ + let audio = peer_audio.get(id) + if (!audio && user.name == USERNAME) { + audio = peer_audio.get('local') + } + + const slider = document.createElement('input') + slider.type = 'range' + slider.min = 0.0 + slider.max = 1.0 + slider.step = 0.01 + + slider.value = audio.volume + slider.addEventListener('input', (e) => { + audio.volume = parseFloat(e.target.value) + }) + + user_div.append(slider) + } + + users_div.append(user_div) + } + + channel.append(channel_title, toggle_button, users_div) + return channel +} + +function update_displays() { + let channels_div = document.getElementById('channels') + for (const child of channels_div.children) { + child.replaceWith(new_channel(child.id)) + } + + if (current_channel) + document.getElementById('channel_display').innerText = current_channel + else + document.getElementById('channel_display').innerText = 'none yet' +} + +window.onload = update_displays + +const subscription = new EventSource('/webrtc/user_channel_event') + +subscription.addEventListener('open', () => { + console.log('[DISPLAY] Connected') +}) + +subscription.addEventListener('error', () => { + console.log('[DISPLAY] Connection error') +}) + +subscription.addEventListener('user_join', (e) => { + console.log('[DISPLAY] User join') + const data = JSON.parse(e.data) + const {id, channel, user} = data + channel_users[channel].set(id, user) + update_displays() +}) + +subscription.addEventListener('user_part', (e) => { + console.log('[DISPLAY] User part') + const data = JSON.parse(e.data) + const {id, channel} = data + channel_users[channel].delete(id) + update_displays() +}) diff --git a/src/extensions/webrtc/static/index.css b/src/extensions/webrtc/static/index.css new file mode 100644 index 0000000..526504a --- /dev/null +++ b/src/extensions/webrtc/static/index.css @@ -0,0 +1,44 @@ +#channels { + display: flex; + flex-wrap: wrap; + margin-bottom: var(--margin-normal); + margin-right: var(--margin-normal); + gap: 10px; +} +.channel { + border: 1px dashed black; + flex-grow: 1; + width: 49.2%; + display: flex; + flex-wrap: wrap; + padding: 10px; + padding-bottom: 6px; +} +.channel > :first-child { + flex-grow: 1; + text-align: center; + line-height: 32px; +} +.channel .users { + width: 100%; +} +.channel .users .user { + display: flex; + flex-wrap: nowrap; + border-top: solid 1px black; +} +.channel .users .user > :nth-child(1) { + width: 32px; + height: 32px; +} +.channel .users .user > :nth-child(2) { + flex-grow: 1; + margin: 0; + margin-left: 10px; + line-height: 32px; +} +.channel .users .user > :nth-child(3) { + max-width: 100px; + margin: 0; + margin-left: 10px; +} diff --git a/src/extensions/webrtc/static/join.mp3 b/src/extensions/webrtc/static/join.mp3 new file mode 100644 index 0000000..8c31eeb Binary files /dev/null and b/src/extensions/webrtc/static/join.mp3 differ diff --git a/src/extensions/webrtc/static/leave.mp3 b/src/extensions/webrtc/static/leave.mp3 new file mode 100644 index 0000000..11f6c85 Binary files /dev/null and b/src/extensions/webrtc/static/leave.mp3 differ diff --git a/src/extensions/webrtc/static/mediator.js b/src/extensions/webrtc/static/mediator.js new file mode 100644 index 0000000..1086611 --- /dev/null +++ b/src/extensions/webrtc/static/mediator.js @@ -0,0 +1,20 @@ +/** @type {string} Currently selected channel */ +var current_channel = null + +var peer_audio = new Map() + +function join_channel(new_channel) { + if (current_channel) { + part_channel() + } + current_channel = new_channel + local_user.con.emit('join', {'channel': current_channel, 'userdata': {name: USERNAME, style: USERSTYLE}}) + update_displays() +} +function part_channel() { + if (current_channel) { + local_user.con.emit('part', current_channel) + } + current_channel = null + update_displays() +} diff --git a/src/extman.ts b/src/extman.ts index e337e46..541f7a8 100644 --- a/src/extman.ts +++ b/src/extman.ts @@ -1,3 +1,5 @@ +import { exec } from "child_process" +import { existsSync } from "fs" import { Knex } from "knex" export async function load(modules: any, namespace: string, knex: Knex): Promise { @@ -8,12 +10,21 @@ export async function load(modules: any, namespace: string, knex: Knex): Promise name: namespace, knex, } + + if (existsSync(`${context.path}package.json`)) { + await new Promise((resolve) => { + exec('npm install', {cwd: context.path}, resolve) + }) + } let ext = new (await import(`./extensions/${namespace}/index`)).default as Extension + if (ext.disabled) + return null + let status = ext.init(context) if (status instanceof Promise) - status.catch(err => console.error(`Failed initializing [${namespace}]: ${err}`)) + await status.catch(err => console.error(`Failed initializing [${namespace}]: ${err}`)) return ext } diff --git a/src/handle.ts b/src/handle.ts index 952f9af..54fde3c 100644 --- a/src/handle.ts +++ b/src/handle.ts @@ -1,13 +1,11 @@ import { load } from "./extman.ts" import Log from "./modules/log.ts" import { unpack } from "./util.ts" +import { readdir } from "fs/promises" let log = new Log(true) export default class implements Handle { - extensions_list = [ - 'profile','nothing','admin','chat' - ] root: RootExtension wg_config: any extensions = new Map() @@ -24,20 +22,24 @@ export default class implements Handle { init: Handle['init'] = async (modules, knex) => { this.root = await load(modules, 'root', knex) as RootExtension - for (const path of this.extensions_list) { - try { - this.extensions.set(path, await load(modules, path, knex) as Extension) - } catch (err: any) { - log.err(`Unable to load extension '${path}':\n\t${err.message}\n${err.stack}`) + for (const path of await readdir(`${import.meta.dirname}/extensions`)) { + if (path != 'root') { + try { + let extension = await load(modules, path, knex) as Extension + if (extension == null) { + continue + } + if (extension.admin_only) { + this.admin_extensions.set(extension.name, extension) + } + else { + this.extensions.set(extension.name, extension) + } + } catch (err: any) { + log.err(`Unable to load extension '${path}':\n\t${err.message}\n${err.stack}`) + } } } - - this.extensions.forEach((extension, name, m) => { - if (extension.admin_only) { - this.admin_extensions.set(name, extension) - this.extensions.delete(name) - } - }) } main: Handle['main'] = async (partial_ctx: PartialContext) => { @@ -48,7 +50,7 @@ export default class implements Handle { ...partial_ctx, context: { ...partial_ctx.args, - extensions: this.extensions, + extensions: new Map(this.extensions), location, } } @@ -60,7 +62,7 @@ export default class implements Handle { ctx.context.auth_err = err if (user && user.is_admin) - ctx.context.extensions = {...ctx.context.extensions, ...this.admin_extensions} + this.admin_extensions.forEach((v, k) => ctx.context.extensions.set(k, v)) // Extension const selected_extension = ctx.context.extensions.get(location) diff --git a/src/index.ts b/src/index.ts index 867101d..2ad7f21 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,20 +1,21 @@ import http2 from 'http2' -import http1, { IncomingMessage, ServerResponse } from 'http' +import http1, { IncomingMessage, RequestListener, ServerResponse } from 'http' import Knex from 'knex' -import dotenv from 'dotenv' import {cookie, config, Log } from './modules.ts' import * as modules from './modules.ts' import Handle from './handle.ts' -// enable use of dotenv -dotenv.config() - // init database const knex = Knex({ client: 'sqlite3', connection: { filename: `${import.meta.dirname}/../data/db.sqlite` }, + pool: { + afterCreate: (con: any, cb: any) => { + con.run('PRAGMA foreign_keys = ON', cb) + }, + }, /** * `__
- +
` => selects `__
` * `_
` => selects `__
` @@ -199,7 +200,7 @@ async function startServer(http_enabled: boolean, https_enabled: boolean) { if (http_enabled) { // Start server http1.createServer( - https_enabled ? httpsRedirect : requestListenerCompat + https_enabled ? httpsRedirect : requestListener as unknown as RequestListener ).listen( config.http_port, config.host, diff --git a/src/modules/content_type.ts b/src/modules/content_type.ts index 1299b52..36ef5e9 100644 --- a/src/modules/content_type.ts +++ b/src/modules/content_type.ts @@ -1,29 +1,20 @@ // Source: https://stackoverflow.com/a/51398471/15181929 -export default (class implements Module { - static readonly HTML = {"Content-Type": "text/html"} - static readonly ASCII = {"Content-Type": "text/plain charset us-ascii"} - static readonly TXT = {"Content-Type": "text/plain charset utf-8"} - static readonly JSON = {"Content-Type": "application/json"} - static readonly ICO = {"Content-Type": "image/x-icon", "Cache-Control": "private, max-age=3600"} - static readonly CSS = {"Content-Type": "text/css", "Cache-Control": "private, max-age=3600"} - static readonly GIF = {"Content-Type": "image/gif", "Cache-Control": "private, max-age=3600"} - static readonly JPG = {"Content-Type": "image/jpeg", "Cache-Control": "private, max-age=3600"} - static readonly JS = {"Content-Type": "text/javascript", "Cache-Control": "private, max-age=3600"} - static readonly PNG = {"Content-Type": "image/png", "Cache-Control": "private, max-age=3600"} - static readonly MD = {"Content-Type": "text/x-markdown"} - static readonly XML = {"Content-Type": "application/xml"} - static readonly SVG = {"Content-Type": "image/svg+xml", "Cache-Control": "private, max-age=3600"} - static readonly WEBMANIFEST = {"Content-Type": "application/manifest+json", "Cache-Control": "private, max-age=3600"} - static readonly MP3 = {"Content-Type": "audio/mpeg", "Cache-Control": "private, max-age=3600"} - static readonly EXE = {"Content-Type": "application/vnd.microsoft.portable-executable", "Cache-Control": "private, max-age=3600"} - static readonly PY = {"Content-Type": "text/x-python", "Cache-Control": "private, max-age=3600"} - - // Force singleton - private constructor(private readonly key: string, public readonly value: any) { - } - - init: Module['init'] = (_context) => { - return [true] - } - +export default ({ + html: {"Content-Type": "text/html"}, + ascii: {"Content-Type": "text/plain charset us-ascii"}, + txt: {"Content-Type": "text/plain charset utf-8"}, + json: {"Content-Type": "application/json"}, + ico: {"Content-Type": "image/x-icon", "Cache-Control": "private, max-age=3600"}, + css: {"Content-Type": "text/css", "Cache-Control": "private, max-age=3600"}, + gif: {"Content-Type": "image/gif", "Cache-Control": "private, max-age=3600"}, + jpg: {"Content-Type": "image/jpeg", "Cache-Control": "private, max-age=3600"}, + js: {"Content-Type": "text/javascript", "Cache-Control": "private, max-age=3600"}, + png: {"Content-Type": "image/png", "Cache-Control": "private, max-age=3600"}, + md: {"Content-Type": "text/x-markdown"}, + xml: {"Content-Type": "application/xml"}, + svg: {"Content-Type": "image/svg+xml", "Cache-Control": "private, max-age=3600"}, + webmanifest: {"Content-Type": "application/manifest+json", "Cache-Control": "private, max-age=3600"}, + mp3: {"Content-Type": "audio/mpeg", "Cache-Control": "private, max-age=3600"}, + exe: {"Content-Type": "application/vnd.microsoft.portable-executable", "Cache-Control": "private, max-age=3600"}, + py: {"Content-Type": "text/x-python", "Cache-Control": "private, max-age=3600"}, }) as ContentType diff --git a/src/templates/extension.html b/src/templates/extension.html index 35b468b..955560a 100644 --- a/src/templates/extension.html +++ b/src/templates/extension.html @@ -14,11 +14,9 @@ - {% include "includes/header.html" %} -
- {% include "includes/extensions.html" %} -
+ {% include "includes/sidebar.html" %} +
{% block body %} {% endblock %}
diff --git a/src/templates/includes/extensions.html b/src/templates/includes/extensions.html index 0abe0ef..5cb4369 100644 --- a/src/templates/includes/extensions.html +++ b/src/templates/includes/extensions.html @@ -1,10 +1,8 @@
{% for _,ext in extensions %} - {{ext.title}} + {% if not ext.hidden %} + {{ext.title}} + {% endif %} {% endfor %} - - {{user.name}} - -
diff --git a/src/templates/includes/favicons.html b/src/templates/includes/favicons.html index 61fc3df..d204bda 100644 --- a/src/templates/includes/favicons.html +++ b/src/templates/includes/favicons.html @@ -3,7 +3,7 @@ - + diff --git a/src/templates/includes/sidebar.html b/src/templates/includes/sidebar.html new file mode 100644 index 0000000..d4805b7 --- /dev/null +++ b/src/templates/includes/sidebar.html @@ -0,0 +1,45 @@ +
+ + + + {% for _,ext in extensions %} + {% if ext.name == location %} +

{{ext.title}}

+ {% endif %} + {% endfor %} +
+ + diff --git a/src/templates/layout.html b/src/templates/layout.html index e556e6e..07ca694 100644 --- a/src/templates/layout.html +++ b/src/templates/layout.html @@ -9,11 +9,11 @@ Keuknet + {% block head %} + {% endblock %} - {% include "includes/header.html" %} -
{% block body %} diff --git a/types/classes/extension.d.ts b/types/classes/extension.d.ts index f96fd08..1a4fbb6 100644 --- a/types/classes/extension.d.ts +++ b/types/classes/extension.d.ts @@ -1,6 +1,8 @@ declare interface Extension { admin_only: boolean tables: boolean + disabled: boolean + hidden: boolean name: string title: string diff --git a/types/global.d.ts b/types/global.d.ts index ade90e8..def21b5 100644 --- a/types/global.d.ts +++ b/types/global.d.ts @@ -44,7 +44,7 @@ declare type User = { id: number, name: string, password: string, - regdate: Date, + registration_date: Date, is_admin: boolean, pfp_code: string } diff --git a/types/modules/content_type.d.ts b/types/modules/content_type.d.ts index 7781b49..d1b7e7c 100644 --- a/types/modules/content_type.d.ts +++ b/types/modules/content_type.d.ts @@ -1,19 +1,19 @@ declare interface ContentType { - readonly HTML: HttpHeader - readonly ASCII: HttpHeader - readonly TXT: HttpHeader - readonly JSON: HttpHeader - readonly ICO: HttpHeader - readonly CSS: HttpHeader - readonly GIF: HttpHeader - readonly JPG: HttpHeader - readonly JS: HttpHeader - readonly PNG: HttpHeader - readonly MD: HttpHeader - readonly XML : HttpHeader - readonly SVG: HttpHeader - readonly WEBMANIFEST: HttpHeader - readonly MP3: HttpHeader - readonly EXE: HttpHeader - readonly PY: HttpHeader + readonly html: HttpHeader + readonly ascii: HttpHeader + readonly txt: HttpHeader + readonly json: HttpHeader + readonly ico: HttpHeader + readonly css: HttpHeader + readonly gif: HttpHeader + readonly jpg: HttpHeader + readonly js: HttpHeader + readonly png: HttpHeader + readonly md: HttpHeader + readonly xml : HttpHeader + readonly svg: HttpHeader + readonly webmanifest: HttpHeader + readonly mp3: HttpHeader + readonly exe: HttpHeader + readonly py: HttpHeader }