summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
authorRory& <root@rory.gay>2025-06-01 08:04:30 +0200
committerRory& <root@rory.gay>2025-06-01 08:04:30 +0200
commit0ca7c01bc4a6c5ab50ac80f9a8e5d5c5db442f45 (patch)
tree8434910cae60074aa51113f9b99d3e9635ea39e0 /src
parentAdd register with validation (diff)
downloadnodejs-final-assignment-0ca7c01bc4a6c5ab50ac80f9a8e5d5c5db442f45.tar.xz
Register works, part of login and auth middleware
Diffstat (limited to 'src')
-rw-r--r--src/api/middlewares/authMiddleware.js25
-rw-r--r--src/api/middlewares/index.js2
-rw-r--r--src/api/routes.js26
-rw-r--r--src/api/routes/auth/accountRoutes.js36
-rw-r--r--src/api/routes/auth/deviceRoutes.js36
-rw-r--r--src/api/routes/auth/index.js2
-rw-r--r--src/api/routes/auth/registerRoute.js20
-rw-r--r--src/db/db.js7
-rw-r--r--src/db/dbAccess/user.js83
-rw-r--r--src/db/schemas/user.js22
-rw-r--r--src/dto/auth/AuthDto.js (renamed from src/dto/auth/LoginDto.js)20
-rw-r--r--src/dto/auth/DeviceDto.js36
-rw-r--r--src/dto/auth/WhoAmIDto.js26
-rw-r--r--src/dto/auth/index.js2
-rw-r--r--src/util/error.js3
15 files changed, 292 insertions, 54 deletions
diff --git a/src/api/middlewares/authMiddleware.js b/src/api/middlewares/authMiddleware.js
new file mode 100644

index 0000000..4cdbb51 --- /dev/null +++ b/src/api/middlewares/authMiddleware.js
@@ -0,0 +1,25 @@ +import { validateJwtToken } from '#util/jwtUtils.js'; +import { DbUser } from '#db/schemas/index.js'; + +/** + * @param options {AuthValidationOptions} + * @returns {(function(*, *, *): void)|*} + */ +export function validateAuth(options) { + return async function (req, res, next) { + var auth = validateJwtToken(req.headers.authorization); + if (!auth) { + res.status(401).send('Unauthorized'); + return; + } + + req.user = await DbUser.findById(auth.id).exec(); + + req.auth = auth; + req = next(); + }; +} + +class AuthValidationOptions { + roles; +} diff --git a/src/api/middlewares/index.js b/src/api/middlewares/index.js
index 1894f1a..f712465 100644 --- a/src/api/middlewares/index.js +++ b/src/api/middlewares/index.js
@@ -1,2 +1,4 @@ export * from './corsMiddleware.js'; export * from './loggingMiddleware.js'; +export * from './errorMiddleware.js'; +export * from './authMiddleware.js'; diff --git a/src/api/routes.js b/src/api/routes.js
index 73d954e..0da8be9 100644 --- a/src/api/routes.js +++ b/src/api/routes.js
@@ -2,6 +2,7 @@ import * as routes from './routes/index.js'; export function registerRoutes(app) { // app.get("/status", routes.statusRoute); + let routeCount = 0; Object.values(routes).forEach(route => { console.log('Registering route:', route); if (!route.route) @@ -10,9 +11,26 @@ export function registerRoutes(app) { JSON.stringify(route) ); - if (route.onGet) app.get(route.route, route.onGet); - if (route.onPost) app.post(route.route, route.onPost); - if (route.onPut) app.put(route.route, route.onPut); - if (route.onDelete) app.put(route.route, route.onDelete); + if (route.onGet) { + app.get(route.route, route.onGet); + routeCount++; + } + if (route.onPost) { + app.post(route.route, route.onPost); + routeCount++; + } + if (route.onPut) { + app.put(route.route, route.onPut); + routeCount++; + } + if (route.onDelete) { + app.put(route.route, route.onDelete); + routeCount++; + } + if (route.onPatch) { + app.patch(route.route, route.onPatch); + routeCount++; + } }); + console.log(`Registered ${routeCount} routes.`); } diff --git a/src/api/routes/auth/accountRoutes.js b/src/api/routes/auth/accountRoutes.js new file mode 100644
index 0000000..6655ecb --- /dev/null +++ b/src/api/routes/auth/accountRoutes.js
@@ -0,0 +1,36 @@ +import { deleteUser, loginUser, registerUser } from '#db/index.js'; +import { AuthDto, RegisterDto } from '#dto/index.js'; + +export const registerRoute = { + route: '/auth/register', + async onPost(req, res) { + const data = await RegisterDto.create(req.body); + const registerResult = await registerUser(data); + res.send(registerResult); + } +}; + +export const loginRoute = { + route: '/auth/login', + /** + * + * @param req {Request} + * @param res + * @returns {Promise<WhoAmIDto>} + */ + async onPost(req, res) { + const data = await AuthDto.create(req.body); + console.log(req.headers['user-agent']); + const loginResult = await loginUser(data, req.headers['user-agent']); + res.send(loginResult); + } +}; + +export const deleteRoute = { + route: '/auth/delete', + async onDelete(req, res) { + const data = await AuthDto.create(req.body); + await deleteUser(data); + res.status(204).send(); + } +}; diff --git a/src/api/routes/auth/deviceRoutes.js b/src/api/routes/auth/deviceRoutes.js new file mode 100644
index 0000000..6655ecb --- /dev/null +++ b/src/api/routes/auth/deviceRoutes.js
@@ -0,0 +1,36 @@ +import { deleteUser, loginUser, registerUser } from '#db/index.js'; +import { AuthDto, RegisterDto } from '#dto/index.js'; + +export const registerRoute = { + route: '/auth/register', + async onPost(req, res) { + const data = await RegisterDto.create(req.body); + const registerResult = await registerUser(data); + res.send(registerResult); + } +}; + +export const loginRoute = { + route: '/auth/login', + /** + * + * @param req {Request} + * @param res + * @returns {Promise<WhoAmIDto>} + */ + async onPost(req, res) { + const data = await AuthDto.create(req.body); + console.log(req.headers['user-agent']); + const loginResult = await loginUser(data, req.headers['user-agent']); + res.send(loginResult); + } +}; + +export const deleteRoute = { + route: '/auth/delete', + async onDelete(req, res) { + const data = await AuthDto.create(req.body); + await deleteUser(data); + res.status(204).send(); + } +}; diff --git a/src/api/routes/auth/index.js b/src/api/routes/auth/index.js
index 7113a17..29a07ad 100644 --- a/src/api/routes/auth/index.js +++ b/src/api/routes/auth/index.js
@@ -1 +1 @@ -export * from './registerRoute.js'; +export * from './accountRoutes.js'; diff --git a/src/api/routes/auth/registerRoute.js b/src/api/routes/auth/registerRoute.js deleted file mode 100644
index 87762d3..0000000 --- a/src/api/routes/auth/registerRoute.js +++ /dev/null
@@ -1,20 +0,0 @@ -import { registerUser } from '#db/index.js'; -import { LoginDto, RegisterDto } from '#dto/index.js'; - -export const registerRoute = { - route: '/auth/register', - async onPost(req, res) { - const data = await RegisterDto.create(req.body); - await registerUser(data); - res.send(data); - } -}; - -export const loginRoute = { - route: '/auth/login', - async onPost(req, res) { - const data = await LoginDto.create(req.body); - await registerUser(data); - res.send(data); - } -}; diff --git a/src/db/db.js b/src/db/db.js
index b9a425c..2035731 100644 --- a/src/db/db.js +++ b/src/db/db.js
@@ -1,11 +1,14 @@ -import { connect } from 'mongoose'; +import mongoose, { connect } from 'mongoose'; import { readSecret } from '#util/secretUtils.js'; export async function initDb() { const connectionString = await readSecret( - "MongoDB connection string", + 'MongoDB connection string', process.env['DATABASE_SECRET_PATH'] ); + + if (process.env['LOG_QUERIES']) mongoose.set('debug', true); + try { const res = await connect(connectionString); if (res.connection.readyState === 1) { diff --git a/src/db/dbAccess/user.js b/src/db/dbAccess/user.js
index 6301cb5..a461f3e 100644 --- a/src/db/dbAccess/user.js +++ b/src/db/dbAccess/user.js
@@ -1,16 +1,53 @@ -import { hash, compare } from 'bcrypt'; -import { DbUser } from '#db/schemas/index.js'; -import { RegisterDto } from '#dto/auth/index.js'; +import { hash, compare, genSalt } from 'bcrypt'; +import { DbUser, deviceSchema } from '#db/schemas/index.js'; +import { AuthDto, RegisterDto } from '#dto/index.js'; +import { SafeNSoundError } from '#util/error.js'; +import { WhoAmIDto } from '#dto/auth/WhoAmIDto.js'; + +async function whoAmI(token) {} + +async function getUserByAuth(data) { + if (!(data instanceof AuthDto)) + throw new Error('Invalid data type. Expected AuthDto.'); + + let user; + + if (data.email) { + user = await DbUser.findOne({ email: data.email }); + } else if (data.username) { + user = await DbUser.findOne({ username: data.username }); + } + + console.log('user', user); + if (!user) { + // Sneaky: prevent user enumeration + throw new SafeNSoundError({ + errCode: 'INVALID_AUTH', + message: 'Invalid username or password.' + }); + } + + const isPasswordValid = await compare(data.password, user.passwordHash); + if (!isPasswordValid) { + throw new SafeNSoundError({ + errCode: 'INVALID_AUTH', + message: 'Invalid username or password.' + }); + } + + return user; +} /** * @param data {RegisterDto} - * @returns {Promise<(Error | HydratedDocument<InferSchemaType<module:mongoose.Schema>, ObtainSchemaGeneric<module:mongoose.Schema, "TVirtuals"> & ObtainSchemaGeneric<module:mongoose.Schema, "TInstanceMethods">, ObtainSchemaGeneric<module:mongoose.Schema, "TQueryHelpers">, ObtainSchemaGeneric<module:mongoose.Schema, "TVirtuals">>)[]>} + * @returns {Promise<DbUser>} */ export async function registerUser(data) { if (!(data instanceof RegisterDto)) throw new Error('Invalid data type. Expected RegisterDto.'); - const passwordHash = await hash(data.password, 10); + const salt = await genSalt(12); + const passwordHash = await hash(data.password, salt); if (!passwordHash) { throw new Error('Failed to hash password.'); } @@ -23,21 +60,29 @@ export async function registerUser(data) { }); } -export async function deleteUser(id, password) { - const user = await DbUser.findById(id); - DbUser.exists({ _id: id }).then(exists => { - if (!exists) { - throw new Error('User does not exist.'); - } +export async function deleteUser(data) { + var user = await getUserByAuth(data); + + await DbUser.findByIdAndDelete(data._id); +} + +/** + * @param data {AuthDto} + * @param deviceName {string} + * @returns {Promise<WhoAmIDto>} + */ +export async function loginUser(data, deviceName) { + const user = await getUserByAuth(data); + const device = await user.devices.create({ + name: deviceName }); - if (!user) { - throw new Error('User not found.'); - } - const isPasswordValid = await compare(password, user.passwordHash); - if (!isPasswordValid) { - throw new Error('Invalid password.'); - } + user.devices.push(device); + await user.save(); - await DbUser.findByIdAndDelete(id); + return WhoAmIDto.create({ + userId: user._id, + username: user.username, + deviceId: device._id + }); } diff --git a/src/db/schemas/user.js b/src/db/schemas/user.js
index 9d08680..f490966 100644 --- a/src/db/schemas/user.js +++ b/src/db/schemas/user.js
@@ -1,6 +1,24 @@ import { model, Schema } from 'mongoose'; import { hash, compare } from 'bcrypt'; +export const deviceSchema = new Schema({ + name: { + type: String, + required: true, + trim: true + }, + createdAt: { + type: Date, + default: Date.now, + immutable: true + }, + lastSeen: { + type: Date, + default: Date.now, + required: true + } +}); + /** * User schema for MongoDB. * @type {module:mongoose.Schema} @@ -31,6 +49,10 @@ export const userSchema = new Schema({ type: Date, default: Date.now, immutable: true + }, + devices: { + type: [deviceSchema], + default: [] } }); diff --git a/src/dto/auth/LoginDto.js b/src/dto/auth/AuthDto.js
index b675b4d..14e09ae 100644 --- a/src/dto/auth/LoginDto.js +++ b/src/dto/auth/AuthDto.js
@@ -1,7 +1,10 @@ import { SafeNSoundError } from '#util/error.js'; import Joi from 'joi'; -export class LoginDto { +/** + * Generic authentication DTO. + */ +export class AuthDto { static schema = new Joi.object({ username: Joi.string().required(), email: Joi.string().email().required(), @@ -9,16 +12,19 @@ export class LoginDto { }).or('username', 'email'); username; + email; password; - async Create({ username, email, password }) { - this.username = username ?? email; - this.password = password; + static async create(data) { + const obj = new AuthDto(); + for (const key of Object.keys(data)) { + if (key in obj) { + obj[key] = data[key]; + } + } try { - return await LoginDto.schema.validateAsync(this, { - abortEarly: true - }); + return await AuthDto.schema.validateAsync(obj); } catch (e) { console.log(e); throw new SafeNSoundError({ diff --git a/src/dto/auth/DeviceDto.js b/src/dto/auth/DeviceDto.js new file mode 100644
index 0000000..40f1959 --- /dev/null +++ b/src/dto/auth/DeviceDto.js
@@ -0,0 +1,36 @@ +import { SafeNSoundError } from '#util/error.js'; +import Joi from 'joi'; + +export class RegisterDto { + static schema = new Joi.object({ + username: Joi.string().required(), + email: Joi.string().email().required(), + password: Joi.string().required(), + type: Joi.string().valid('user', 'monitor', 'admin').required() + }); + + username; + email; + password; + type = 'user'; + + static async create(data) { + const obj = new RegisterDto(); + for (const key of Object.keys(data)) { + if (key in obj) { + obj[key] = data[key]; + } + } + + try { + return await RegisterDto.schema.validateAsync(obj); + } catch (e) { + console.log(e); + throw new SafeNSoundError({ + errCode: 'JOI_VALIDATION_ERROR', + message: e.message, + validation_details: e.details + }); + } + } +} diff --git a/src/dto/auth/WhoAmIDto.js b/src/dto/auth/WhoAmIDto.js new file mode 100644
index 0000000..ae1795a --- /dev/null +++ b/src/dto/auth/WhoAmIDto.js
@@ -0,0 +1,26 @@ +import { SafeNSoundError } from '#util/error.js'; +import Joi from 'joi'; + +/** + * Generic authentication DTO. + */ +export class WhoAmIDto { + userId; + username; + deviceId; + + /** + * @param data {WhoAmIDto} + * @returns {Promise<WhoAmIDto>} + */ + static async create(data) { + const obj = new WhoAmIDto(); + for (const key of Object.keys(data)) { + if (key in obj) { + obj[key] = data[key]; + } + } + + return obj; + } +} diff --git a/src/dto/auth/index.js b/src/dto/auth/index.js
index 6d57d5e..aa1d435 100644 --- a/src/dto/auth/index.js +++ b/src/dto/auth/index.js
@@ -1,2 +1,2 @@ -export * from './LoginDto.js'; +export * from './AuthDto.js'; export * from './RegisterDto.js'; diff --git a/src/util/error.js b/src/util/error.js
index 5fc454a..9816b65 100644 --- a/src/util/error.js +++ b/src/util/error.js
@@ -1,4 +1,7 @@ export class SafeNSoundError extends Error { + /** + * @param options {SafeNSoundError} + */ constructor(options) { super(); if (typeof options === 'string') {