Added File uploads and fetching of said files

This commit is contained in:
2025-12-22 13:03:14 +01:00
parent ba6d8eeffc
commit 273314739f
10 changed files with 354 additions and 35 deletions

1
.gitignore vendored
View File

@@ -143,3 +143,4 @@ vite.config.ts.timestamp-*
# user files
/responses
/data

7
.prettierrc Normal file
View File

@@ -0,0 +1,7 @@
{
"tabWidth": 4,
"useTabs": false,
"semi": true,
"singleQuote": true,
"endOfLine": "lf"
}

View File

@@ -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", "."]

View File

@@ -25,7 +25,7 @@ Ansonsten kann man auch mit `npm run dev` entwickeln.
cd /opt/jeopardy/Jeopardy-Server
git fetch --tags
git checkout <versionsnummer>
docker build -t jeopardy .
docker build -t jeopardyserver .
docker tag jeopardyserver:latest jeopardyserver:<versionsnummer>
cd ..
docker compose up -d

View File

@@ -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}`);

192
package-lock.json generated
View File

@@ -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"
}
}
}
}

View File

@@ -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"
}
}

View File

@@ -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: '/',
});
}

91
src/cdn.js Normal file
View File

@@ -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);
}
}

View File

@@ -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);