From 34696a1fc81a4df8415fc42df8a301341250c99e Mon Sep 17 00:00:00 2001 From: Jonas Kappa Date: Sat, 4 Oct 2025 13:40:30 +0200 Subject: [PATCH] Added user management --- .gitignore | 5 ++ index.js | 2 +- package-lock.json | 4 +- package.json | 2 +- requests/{test.http => auth.http} | 6 +- requests/user.http | 45 ++++++++++++ src/auth.js | 96 +++++++++++++------------ src/roles.js | 12 ++++ src/user.js | 113 ++++++++++++++++++++++++++++-- src/userHelper.js | 76 ++++++++++++++++++++ 10 files changed, 308 insertions(+), 53 deletions(-) rename requests/{test.http => auth.http} (64%) create mode 100644 requests/user.http create mode 100644 src/roles.js create mode 100644 src/userHelper.js diff --git a/.gitignore b/.gitignore index c9aa34f..a244f78 100644 --- a/.gitignore +++ b/.gitignore @@ -138,3 +138,8 @@ dist # Vite logs files vite.config.js.timestamp-* vite.config.ts.timestamp-* + + +# user files + +/responses diff --git a/index.js b/index.js index 6ca4437..ffbd28d 100644 --- a/index.js +++ b/index.js @@ -27,7 +27,7 @@ app.use(cookieParser()); await initDbConnection(); initAuth(app, db); -initUsers(app); +initUsers(app, db); initWebsocket(app); app.listen(port, () => { diff --git a/package-lock.json b/package-lock.json index deeae50..12b7fb8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "jeopardyserver", - "version": "1.0.2", + "version": "1.0.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "jeopardyserver", - "version": "1.0.2", + "version": "1.0.3", "license": "ISC", "dependencies": { "@types/express": "^5.0.3", diff --git a/package.json b/package.json index 1aa6aad..52b6dc2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "jeopardyserver", - "version": "1.0.2", + "version": "1.0.3", "description": "", "license": "ISC", "author": "", diff --git a/requests/test.http b/requests/auth.http similarity index 64% rename from requests/test.http rename to requests/auth.http index 937e930..9c553a7 100644 --- a/requests/test.http +++ b/requests/auth.http @@ -6,5 +6,9 @@ content-type: application/json { "username": "jonas", - "password": "kappa" + "password": "paula" } + +### + +GET {{url}}/auth HTTP/1.1 diff --git a/requests/user.http b/requests/user.http new file mode 100644 index 0000000..915fc55 --- /dev/null +++ b/requests/user.http @@ -0,0 +1,45 @@ +@url = http://{{host}}:{{port}} + +PUT {{url}}/admin/user HTTP/1.1 +Content-Type: application/json + +{ + "username": "Paula" +} + +### + +GET {{url}}/admin/user/list HTTP/1.1 + +### + +POST {{url}}/admin/user/resetpw HTTP/1.1 +Content-Type: application/json + +{ + "userid": "68e1058faf78b3aabbdfe8dc" +} + +### + +GET {{url}}/admin/roles HTTP/1.1 + +### + +POST {{url}}/admin/user/changerole HTTP/1.1 +Content-Type: application/json + +{ + "userid": "68e0efc6e4ac740114d8fc9d", + "role": "default" +} + +### + +POST {{url}}/user/changepw HTTP/1.1 +Content-Type: application/json + +{ + "old": "DkgnWspm4To2ww==", + "new": "Kolata" +} diff --git a/src/auth.js b/src/auth.js index eea585c..128fe74 100644 --- a/src/auth.js +++ b/src/auth.js @@ -1,14 +1,28 @@ -import { createHash, pbkdf2Sync, randomBytes } from "node:crypto"; +import { createUser, generateHash, updateSessionToken } from "./userHelper.js"; let db; let users; export function initAuth(app, db) { app.use(checkSessionToken); + app.use('/admin', checkAuthorization('admin')); users = db.collection('users'); + app.get('/auth', getUserInfo); app.post('/auth/login', loginUser); } +async function getUserInfo(req, res) { + const sessiontoken = await updateSessionToken(users, req.user._id); + + setTokenCookie(res, sessiontoken); + + res.status(200).send({ + username: req.user.username, + role: req.user.role, + _id: req.user._id + }); +} + async function checkSessionToken(req, res, next) { if (req.path.startsWith("/auth/")) { next(); @@ -26,81 +40,75 @@ async function checkSessionToken(req, res, next) { req.user = { role: user.role, - username: user.username + username: user.username, + _id: user._id } next(); } +function checkAuthorization(role) { + return (req, res, next) => { + if (req.user === undefined) { + res.status(403).send(); + return; + } + + if (req.user.role === role) { + next(); + } else { + res.status(403).send(); + } + } +} + async function loginUser(req, res) { const username = req.body.username; const password = req.body.password; let userCount = await users.estimatedDocumentCount(); - let sessiontoken = null; + let userobj = null; if (userCount <= 0) { // create first user - sessiontoken = await createUser(username, password, 'admin'); + userobj = await createUser(users, username, password, 'admin', true); } else { // authenticate user - sessiontoken = await authenticateUser(username, password); + userobj = await authenticateUser(username, password); } - if (sessiontoken !== null) { - const expires = new Date(); - expires.setDate(expires.getDate() + 1); + if (userobj !== null) { + setTokenCookie(res, userobj.sessiontoken); - res.cookie('jeopardytoken', sessiontoken, { - maxAge: 1e3 * 60 * 60 * 24, - path: "/" - }) - - res.status(200).send(username); + res.status(200).send({username: userobj.username, role: userobj.role, _id: userobj._id}); } else { res.sendStatus(403); } } -async function createUser(username, password, role) { - const salt = randomBytes(128).toString('base64'); - const iterations = Math.floor(Math.random() * 5000) + 5000; - const hash = generateHash(password, salt, iterations); - - const sessiontoken = generateSessionToken(); - - await users.insertOne({ - username, - role, - salt, - iterations, - hash, - sessiontoken - }); - - return sessiontoken; -} - -async function authenticateUser(username, password) { +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) { - const sessiontoken = generateSessionToken(); - await users.updateOne({_id: foundUser._id}, {$set: { - sessiontoken - }}); - return sessiontoken; + if (updateSession) { + 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 null; } -function generateSessionToken() { - return randomBytes(128).toString('base64'); -} +function setTokenCookie(res, sessiontoken) { + const expires = new Date(); + expires.setDate(expires.getDate() + 1); -function generateHash(password, salt, iterations) { - return pbkdf2Sync(password, salt, iterations, 128, 'sha512').toString('hex'); + res.cookie('jeopardytoken', sessiontoken, { + maxAge: 1e3 * 60 * 60 * 24, + path: "/" + }) } diff --git a/src/roles.js b/src/roles.js new file mode 100644 index 0000000..2e7c352 --- /dev/null +++ b/src/roles.js @@ -0,0 +1,12 @@ +export const roles = [ + "admin", + "default" +] + +/** + * + * @param {string} newrole + */ +export function isValidRole(newrole) { + return roles.includes(newrole); +} diff --git a/src/user.js b/src/user.js index c6276ee..3d28457 100644 --- a/src/user.js +++ b/src/user.js @@ -1,9 +1,114 @@ +import { ObjectId } from "mongodb"; +import { createUser as userHelperCreateUser, generateSessionToken, updatePassword, userExists } from "./userHelper.js"; +import { isValidRole, roles } from "./roles.js"; +import { authenticateUser } from "./auth.js"; +let db; +let users; -export function initUsers(app) { - app.get('/user/username', returnUsername); +export function initUsers(app, db) { + users = db.collection('users'); + app.put('/admin/user', createUser); + app.get('/admin/user/list', userlist); + app.post('/admin/user/resetpw', resetpassword); + app.post('/admin/user/changerole', changerole); + app.get('/admin/roles', getRoles); + app.post('/user/changepw', changePassword); } -function returnUsername(req, res) { - res.status(200).send(req.user.username); +async function createUser(req, res) { + const username = req.body.username; + // check if user exists + let foundUser = await users.findOne({username}); + + if (foundUser !== null) { + res.status(400).send(); + return; + } + + const password = generateSessionToken(10); + + const userobj = await userHelperCreateUser(users, username, password, 'default', false); + + res.status(200).send({ + username: userobj.username, + role: userobj.role, + _id: userobj._id, + password + }); +} + +async function userlist(req, res) { + const result = await users.find().project({ + username: 1, + role: 1 + }).toArray(); + + res.status(200).send(result); +} + +async function resetpassword(req, res) { + /** @type {string} */ + const userid = req.body.userid; + const _id = new ObjectId(userid); + + const foundUser = userExists(res, users, _id); + if (foundUser === null) return; + + const password = generateSessionToken(10); + + await updatePassword(users, _id, password, false); + res.status(200).send({ + _id: userid, + username: foundUser.username, + role: foundUser.role, + password + }); +} + +async function changerole(req, res) { + /** @type {string} */ + const userid = req.body.userid; + const _id = new ObjectId(userid); + const newrole = req.body.role; + + if (!isValidRole(newrole)) { + res.status(400).send(); + return; + } + + const foundUser = await userExists(res, users, _id); + if (foundUser === null) return; + + await users.updateOne({_id}, { + $set: { + role: newrole + } + }); + + res.status(200).send({ + _id, + username: foundUser.username, + role: newrole + }); +} + +function getRoles(req, res) { + res.status(200).send(roles); +} + +async function changePassword(req, res) { + const oldpassword = req.body.old; + const newpassword = req.body.new; + + const userobj = await authenticateUser(req.user.username, oldpassword, false); + + if (userobj === null) { + res.status(400).send(); + return; + } + + await updatePassword(users, req.user._id, newpassword, false); + + res.status(200).send(); } diff --git a/src/userHelper.js b/src/userHelper.js new file mode 100644 index 0000000..b4f6f6a --- /dev/null +++ b/src/userHelper.js @@ -0,0 +1,76 @@ +import { pbkdf2Sync, randomBytes } from "node:crypto"; + +export async function createUser(collection, username, password, role, withSession = true) { + const {salt, iterations, hash} = createHash(password); + + let sessiontoken = ""; + if (withSession) { + sessiontoken = generateSessionToken(); + } + + const result = await collection.insertOne({ + username, + role, + salt, + iterations, + hash, + sessiontoken + }); + + return {sessiontoken, username, role, _id: result.insertedId}; +} + +export async function updatePassword(collection, _id, password, keepSession = true) { + const {salt, iterations, hash} = createHash(password); + + if (keepSession) { + await collection.updateOne({_id}, {$set: { + salt, + iterations, + hash + }}); + } else { + await collection.updateOne({_id}, {$set: { + salt, + iterations, + hash, + sessiontoken: "" + }}); + } +} + +export function generateSessionToken(length = 128, encoding = 'base64') { + return randomBytes(length).toString(encoding); +} + +export function generateHash(password, salt, iterations) { + return pbkdf2Sync(password, salt, iterations, 128, 'sha512').toString('hex'); +} + +export async function updateSessionToken(collection, _id) { + const sessiontoken = generateSessionToken(); + await collection.updateOne({_id: _id}, {$set: { + sessiontoken + }}); + return sessiontoken; +} + +export async function userExists(res, collection, _id) { + const foundUser = await collection.findOne({_id}); + + if (foundUser === null) { + res.status(400).send(); + } + + return foundUser; +} + +function createHash(password) { + const salt = randomBytes(128).toString('base64'); + const iterations = Math.floor(Math.random() * 5000) + 5000; + const hash = generateHash(password, salt, iterations); + + return { + salt, hash, iterations + } +}