From 273314739f4107ace6e0da6b1f271ee3f6060f90 Mon Sep 17 00:00:00 2001 From: Jonas Kappa Date: Mon, 22 Dec 2025 13:03:14 +0100 Subject: [PATCH] Added File uploads and fetching of said files --- .gitignore | 1 + .prettierrc | 7 ++ Dockerfile | 3 + Readme.md | 2 +- index.js | 32 ++++---- package-lock.json | 192 +++++++++++++++++++++++++++++++++++++++++++++- package.json | 4 +- src/auth.js | 50 ++++++++---- src/cdn.js | 91 ++++++++++++++++++++++ src/user.js | 7 +- 10 files changed, 354 insertions(+), 35 deletions(-) create mode 100644 .prettierrc create mode 100644 src/cdn.js diff --git a/.gitignore b/.gitignore index a244f78..456293f 100644 --- a/.gitignore +++ b/.gitignore @@ -143,3 +143,4 @@ vite.config.ts.timestamp-* # user files /responses +/data diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..5f0a661 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,7 @@ +{ + "tabWidth": 4, + "useTabs": false, + "semi": true, + "singleQuote": true, + "endOfLine": "lf" +} diff --git a/Dockerfile b/Dockerfile index c222ce8..4478b22 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,6 +11,8 @@ COPY . . RUN npm prune --production FROM node:${NODE_VERSION}-alpine +RUN mkdir -p /data +RUN chown node /data WORKDIR /app COPY --from=builder /app/node_modules node_modules/ COPY --from=builder /app/index.js . @@ -18,4 +20,5 @@ COPY --from=builder /app/src src/ USER node EXPOSE 12345 ENV NODE_ENV=production +ENV JEOPARDY_CDN_DATA_PATH=/data CMD [ "node", "."] diff --git a/Readme.md b/Readme.md index 922c500..eb046cf 100644 --- a/Readme.md +++ b/Readme.md @@ -25,7 +25,7 @@ Ansonsten kann man auch mit `npm run dev` entwickeln. cd /opt/jeopardy/Jeopardy-Server git fetch --tags git checkout - docker build -t jeopardy . + docker build -t jeopardyserver . docker tag jeopardyserver:latest jeopardyserver: cd .. docker compose up -d diff --git a/index.js b/index.js index ffbd28d..90824f1 100644 --- a/index.js +++ b/index.js @@ -1,25 +1,26 @@ -import dotenv from "dotenv"; +import dotenv from 'dotenv'; dotenv.config(); -import express from "express"; -import expressWs from "express-ws"; -import morgan from "morgan"; -import cookieParser from "cookie-parser"; -import cors from "cors"; -import { initWebsocket } from "./src/websocket.js"; -import { initAuth } from "./src/auth.js"; -import { close as closeDbConnection, initDbConnection, db } from "./src/db.js"; -import { initUsers } from "./src/user.js"; +import express from 'express'; +import expressWs from 'express-ws'; +import morgan from 'morgan'; +import cookieParser from 'cookie-parser'; +import cors from 'cors'; +import { initWebsocket } from './src/websocket.js'; +import { initAuth } from './src/auth.js'; +import { close as closeDbConnection, initDbConnection, db } from './src/db.js'; +import { initUsers } from './src/user.js'; +import { initCdn } from './src/cdn.js'; const app = express(); const appWs = expressWs(app); const port = 12345; -process.on('exit', function() { - console.log('Shutting down...'); - console.log('Closing db connection...'); - closeDbConnection(); +process.on('exit', function () { + console.log('Shutting down...'); + console.log('Closing db connection...'); + closeDbConnection(); }); -app.use(cors({credentials: true, origin: process.env.JEOPARDY_URL})); +app.use(cors({ credentials: true, origin: process.env.JEOPARDY_URL })); app.use(morgan(process.env.production ? 'common' : 'dev')); app.use(express.json()); app.use(cookieParser()); @@ -29,6 +30,7 @@ await initDbConnection(); initAuth(app, db); initUsers(app, db); initWebsocket(app); +initCdn(app, db); app.listen(port, () => { console.log(`Listening on port ${port}`); diff --git a/package-lock.json b/package-lock.json index 81f951e..c7e0bb8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,10 +17,12 @@ "express-ws": "^5.0.2", "mongodb": "^6.20.0", "morgan": "^1.10.1", + "multer": "^2.0.2", "ws": "^8.18.3" }, "devDependencies": { - "@types/node": "^24.6.0" + "@types/node": "^24.6.0", + "prettier": "^3.7.4" } }, "node_modules/@mongodb-js/saslprep": { @@ -156,6 +158,12 @@ "node": ">= 0.6" } }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", + "license": "MIT" + }, "node_modules/basic-auth": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", @@ -203,6 +211,23 @@ "node": ">=16.20.1" } }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -241,6 +266,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "engines": [ + "node >= 6.0" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, "node_modules/content-disposition": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", @@ -734,6 +774,27 @@ "node": ">= 0.6" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, "node_modules/mongodb": { "version": "6.20.0", "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.20.0.tgz", @@ -839,6 +900,67 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/multer": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/multer/-/multer-2.0.2.tgz", + "integrity": "sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw==", + "license": "MIT", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.6.0", + "concat-stream": "^2.0.0", + "mkdirp": "^0.5.6", + "object-assign": "^4.1.1", + "type-is": "^1.6.18", + "xtend": "^4.0.2" + }, + "engines": { + "node": ">= 10.16.0" + } + }, + "node_modules/multer/node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/multer/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/multer/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/multer/node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/negotiator": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", @@ -918,6 +1040,22 @@ "url": "https://opencollective.com/express" } }, + "node_modules/prettier": { + "version": "3.7.4", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.7.4.tgz", + "integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -995,6 +1133,20 @@ "url": "https://opencollective.com/express" } }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/router": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", @@ -1170,6 +1322,23 @@ "node": ">= 0.8" } }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -1205,6 +1374,12 @@ "node": ">= 0.6" } }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, "node_modules/undici-types": { "version": "7.13.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.13.0.tgz", @@ -1220,6 +1395,12 @@ "node": ">= 0.8" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -1277,6 +1458,15 @@ "optional": true } } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } } } } diff --git a/package.json b/package.json index 525a7d9..8d59cdf 100644 --- a/package.json +++ b/package.json @@ -19,9 +19,11 @@ "express-ws": "^5.0.2", "mongodb": "^6.20.0", "morgan": "^1.10.1", + "multer": "^2.0.2", "ws": "^8.18.3" }, "devDependencies": { - "@types/node": "^24.6.0" + "@types/node": "^24.6.0", + "prettier": "^3.7.4" } } diff --git a/src/auth.js b/src/auth.js index 16f194e..99f6df7 100644 --- a/src/auth.js +++ b/src/auth.js @@ -1,4 +1,4 @@ -import { createUser, generateHash, updateSessionToken } from "./userHelper.js"; +import { createUser, generateHash, updateSessionToken } from './userHelper.js'; let db; let users; @@ -19,19 +19,19 @@ async function getUserInfo(req, res) { res.status(200).send({ username: req.user.username, role: req.user.role, - _id: req.user._id + _id: req.user._id, }); } async function checkSessionToken(req, res, next) { - if (req.path.startsWith("/auth/")) { + if (req.path.startsWith('/auth/')) { next(); return; } const token = req.cookies.jeopardytoken; - let user = await users.findOne({sessiontoken: token}); + let user = await users.findOne({ sessiontoken: token }); if (user === null) { res.sendStatus(401); @@ -41,14 +41,14 @@ async function checkSessionToken(req, res, next) { req.user = { role: user.role, username: user.username, - _id: user._id - } + _id: user._id, + }; next(); } function checkAuthorization(role) { - return (req, res, next) => { + return (req, res, next) => { if (req.user === undefined) { res.status(403).send(); return; @@ -59,7 +59,7 @@ function checkAuthorization(role) { } else { res.status(403).send(); } - } + }; } async function loginUser(req, res) { @@ -79,24 +79,42 @@ async function loginUser(req, res) { if (userobj !== null) { setTokenCookie(res, userobj.sessiontoken); - res.status(200).send({username: userobj.username, role: userobj.role, _id: userobj._id}); + res.status(200).send({ + username: userobj.username, + role: userobj.role, + _id: userobj._id, + }); } else { res.sendStatus(403); } } -export async function authenticateUser(username, password, updateSession = true) { - let foundUser = await users.findOne({username}); +export async function authenticateUser( + username, + password, + updateSession = true, +) { + let foundUser = await users.findOne({ username }); if (foundUser === null) return null; const hash = generateHash(password, foundUser.salt, foundUser.iterations); if (hash === foundUser.hash) { if (updateSession) { - let sessiontoken = await updateSessionToken(users, foundUser._id); - return {sessiontoken, username, role: foundUser.role, _id: foundUser._id}; + let sessiontoken = await updateSessionToken(users, foundUser._id); + return { + sessiontoken, + username, + role: foundUser.role, + _id: foundUser._id, + }; } else { - return {sessiontoken: foundUser.sessiontoken, username, role: foundUser.role, _id: foundUser._id}; + return { + sessiontoken: foundUser.sessiontoken, + username, + role: foundUser.role, + _id: foundUser._id, + }; } } @@ -109,6 +127,6 @@ function setTokenCookie(res, sessiontoken) { res.cookie('jeopardytoken', sessiontoken, { maxAge: 1e3 * 60 * 60 * 24 * 7, - path: "/" - }) + path: '/', + }); } diff --git a/src/cdn.js b/src/cdn.js new file mode 100644 index 0000000..445bbe1 --- /dev/null +++ b/src/cdn.js @@ -0,0 +1,91 @@ +import { rmSync } from 'fs'; +import { copyFile, mkdir } from 'fs/promises'; +import { Collection, Db, ObjectId } from 'mongodb'; +import multer from 'multer'; + +const dataPath = process.env.JEOPARDY_CDN_DATA_PATH; +const upload = multer({ dest: dataPath }); + +/** + * @type {Collection} + */ +let ressources; + +/** + * + * @param {*} app + * @param {Db} db + */ +export function initCdn(app, db) { + ressources = db.collection('ressources'); + app.post('/upload', upload.single('file'), uploadFile); + app.get('/cdn/:userid/:resid', fetchFile); +} + +/** + * + * @param {import('express').Request} req + * @param {import('express').Response} res + * @param {import('express').NextFunction} next + */ +function uploadFile(req, res, next) { + console.log(req.file, req.body); + + /** + * @type {string | undefined} + */ + const path = req.body.path; + + if (path !== undefined && path.startsWith('/') && !path.includes('.')) { + let destinationPath = `${dataPath}/${req.user._id}${req.body.path}`; + mkdir(destinationPath, { + recursive: true, + }) + .then(() => { + return copyFile( + req.file.path, + `${destinationPath}/${req.file.filename}`, + ); + }) + .then(async () => { + rmSync(req.file.path); + + await ressources.insertOne({ + fullpath: destinationPath, + path: path, + user: req.user._id, + mimetype: req.file.mimetype, + name: req.file.originalname, + filename: req.file.filename, + }); + + res.sendStatus(200); + }) + .catch((err) => { + console.error(err); + rmSync(req.file.path); + res.sendStatus(500); + }); + } else { + rmSync(req.file.path); + res.sendStatus(400); + } +} + +/** + * + * @param {import('express').Request} req + * @param {import('express').Response} res + */ +async function fetchFile(req, res) { + let ressource = await ressources.findOne({ + user: new ObjectId(req.params.userid), + filename: req.params.resid, + }); + + if (ressource) { + res.sendFile(ressource.fullpath + ressource.filename); + } else { + res.sendStatus(404); + } +} diff --git a/src/user.js b/src/user.js index eb7ebc6..a46f78a 100644 --- a/src/user.js +++ b/src/user.js @@ -1,4 +1,4 @@ -import { ObjectId } from "mongodb"; +import { Db, ObjectId } from "mongodb"; import { createUser as userHelperCreateUser, generateSessionToken, updatePassword, userExists } from "./userHelper.js"; import { isValidRole, roles } from "./roles.js"; import { authenticateUser } from "./auth.js"; @@ -6,6 +6,11 @@ import { authenticateUser } from "./auth.js"; let db; let users; +/** + * + * @param {*} app + * @param {Db} db + */ export function initUsers(app, db) { users = db.collection('users'); app.put('/admin/user', createUser);