diff --git a/auth.js b/auth.js index 1e6ce77..7eb2a9a 100644 --- a/auth.js +++ b/auth.js @@ -1,9 +1,81 @@ -import {db} from './db.js'; +import { createHash, pbkdf2Sync, randomBytes } from "node:crypto"; -const users = db.collection('users'); +let db; +let users; -export function initAuth(app) { - app.get('/auth/login', (req, res) => { - - }) +export function initAuth(app, db) { + users = db.collection('users'); + app.post('/auth/login', loginUser); +} + +async function loginUser(req, res) { + const username = req.body.username; + const password = req.body.password; + + let userCount = await users.estimatedDocumentCount(); + let sessiontoken = null; + if (userCount <= 0) { + // create first user + sessiontoken = await createUser(username, password, 'admin'); + } else { + // authenticate user + sessiontoken = await authenticateUser(username, password); + } + + if (sessiontoken !== null) { + const expires = new Date(); + expires.setDate(expires.getDate() + 1); + + res.cookie('jeopardytoken', sessiontoken, { + maxAge: 1e3 * 60 * 60 * 24 + }) + + res.sendStatus(200); + } 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) { + 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; + } + + return null; +} + +function generateSessionToken() { + return randomBytes(128).toString('base64'); +} + +function generateHash(password, salt, iterations) { + return pbkdf2Sync(password, salt, iterations, 128, 'sha512').toString('hex'); } diff --git a/db.js b/db.js index 9348a50..4f9c90a 100644 --- a/db.js +++ b/db.js @@ -1,6 +1,6 @@ import { MongoClient } from "mongodb"; -const client = new MongoClient(`mongodb://${process.env.MONGODB_USER}:${process.env.MONGODB_PASSWORD}@${process.env.MONGODB_URL}`); +let client; const dbName = `jeopardy`; @@ -10,6 +10,7 @@ const dbName = `jeopardy`; export let db; export async function initDbConnection() { + client = new MongoClient(`mongodb://${process.env.JEOPARDYSERVER_MONGO_USERNAME}:${process.env.JEOPARDYSERVER_MONGO_PASSWORD}@${process.env.JEOPARDYSERVER_MONGO_URL}/`); await client.connect(); console.log('Connected successfully to mongodb'); db = client.db(dbName); diff --git a/index.js b/index.js index ca8f9d0..0cf04cd 100644 --- a/index.js +++ b/index.js @@ -1,8 +1,11 @@ +import dotenv from "dotenv"; +dotenv.config(); import express from "express"; import expressWs from "express-ws"; +import morgan from "morgan"; import { initWebsocket } from "./websocket.js"; import { initAuth } from "./auth.js"; -import { close as closeDbConnection, initDbConnection } from "./db.js"; +import { close as closeDbConnection, initDbConnection, db } from "./db.js"; const app = express(); const appWs = expressWs(app); const port = 12345; @@ -13,9 +16,12 @@ process.on('exit', function() { closeDbConnection(); }); +app.use(morgan(process.env.production ? 'common' : 'dev')); +app.use(express.json()); + await initDbConnection(); -initAuth(app); +initAuth(app, db); initWebsocket(app); app.listen(port, () => { diff --git a/package-lock.json b/package-lock.json index 8bae0cc..74abbc9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,9 +10,11 @@ "license": "ISC", "dependencies": { "@types/express": "^5.0.3", + "dotenv": "^17.2.3", "express": "^5.1.0", "express-ws": "^5.0.2", "mongodb": "^6.20.0", + "morgan": "^1.10.1", "ws": "^8.18.3" }, "devDependencies": { @@ -152,6 +154,24 @@ "node": ">= 0.6" } }, + "node_modules/basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/basic-auth/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, "node_modules/body-parser": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", @@ -284,6 +304,18 @@ "node": ">= 0.8" } }, + "node_modules/dotenv": { + "version": "17.2.3", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz", + "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -724,6 +756,49 @@ "whatwg-url": "^14.1.0 || ^13.0.0" } }, + "node_modules/morgan": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.1.tgz", + "integrity": "sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A==", + "license": "MIT", + "dependencies": { + "basic-auth": "~2.0.1", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-finished": "~2.3.0", + "on-headers": "~1.1.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/morgan/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/morgan/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/morgan/node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -763,6 +838,15 @@ "node": ">= 0.8" } }, + "node_modules/on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", diff --git a/package.json b/package.json index c095705..bc74bb5 100644 --- a/package.json +++ b/package.json @@ -12,9 +12,11 @@ }, "dependencies": { "@types/express": "^5.0.3", + "dotenv": "^17.2.3", "express": "^5.1.0", "express-ws": "^5.0.2", "mongodb": "^6.20.0", + "morgan": "^1.10.1", "ws": "^8.18.3" }, "devDependencies": { diff --git a/requests/test.http b/requests/test.http index eaf6cfe..8a65ad5 100644 --- a/requests/test.http +++ b/requests/test.http @@ -1,2 +1,8 @@ -GET http://localhost:12345/ HTTP/1.1 +POST http://localhost:12345/auth/login HTTP/1.1 +content-type: application/json + +{ + "username": "jonas", + "password": "kappa" +}