summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
authorRory& <root@rory.gay>2025-06-02 10:12:34 +0200
committerRory& <root@rory.gay>2025-06-02 10:12:34 +0200
commitc4fd9c93a63bce7c322aec1fc304b4dc5ac5a9cd (patch)
tree4cea31f08115376fefc2cdfed5befbc5610150f8 /src
parentAdd shortcut for generating http file, part of API documentation (diff)
downloadnodejs-final-assignment-c4fd9c93a63bce7c322aec1fc304b4dc5ac5a9cd.tar.xz
Split authentication and authorization
Diffstat (limited to 'src')
-rw-r--r--src/api/middlewares/authMiddleware.js79
-rw-r--r--src/api/routes.js57
-rw-r--r--src/api/routes/alarmRoutes.js2
-rw-r--r--src/api/routes/auth/accountRoutes.js21
-rw-r--r--src/api/routes/auth/adminAccountRoutes.js2
-rw-r--r--src/api/routes/auth/deviceRoutes.js4
-rw-r--r--src/api/routes/budgetRoutes.js17
-rw-r--r--src/api/start.js3
-rw-r--r--src/dto/auth/WhoAmIDto.js1
9 files changed, 145 insertions, 41 deletions
diff --git a/src/api/middlewares/authMiddleware.js b/src/api/middlewares/authMiddleware.js

index 3a71e45..13d0d27 100644 --- a/src/api/middlewares/authMiddleware.js +++ b/src/api/middlewares/authMiddleware.js
@@ -1,17 +1,68 @@ import { validateJwtToken } from '#util/jwtUtils.js'; import { DbUser, UserType } from '#db/schemas/index.js'; +import { SafeNSoundError } from '#util/error.js'; + +const shouldLogAuth = !!process.env['LOG_AUTH']; +function logAuth(...params) { + if (shouldLogAuth) { + console.log('[AUTH]', ...params); + } +} + +function getTokenFromHeader(authHeader) { + if (!authHeader || !authHeader.startsWith('Bearer ')) { + return null; + } + const parts = authHeader.split(' '); + return { + scheme: parts[0], + token: parts[1] + }; +} + +export async function useAuthentication(req, res, next) { + if (!req.headers.authorization) { + logAuth('Request to', req.path, 'without auth'); + next(); + return; + } + + const auth = (req.auth = await validateJwtToken( + getTokenFromHeader(req.headers.authorization).token + )); + logAuth('Token data:', auth); + + // req.user = auth; + next(); +} + +export async function requireAuth(req, res, next) { + if (!req.auth) { + logAuth('Unauthorized request to', req.path); + res.status(401).send( + new SafeNSoundError({ + errCode: 'UNAUTHORIZED', + message: 'Unauthorized' + }) + ); + return; + } + + next(); +} /** * @param options {AuthValidationOptions} * @returns {(function(*, *, *): void)|*} */ -export function validateAuth(options) { +export function requireRole(options) { return async function (req, res, next) { - const auth = (req.auth = validateJwtToken(req.headers.authorization)); - if (!auth) { - res.status(401).send('Unauthorized'); - return; - } + res.status(401).send( + new SafeNSoundError({ + errCode: 'UNAUTHORIZED', + message: 'Unauthorized' + }) + ); const user = (req.user = await DbUser.findById(auth.id).exec()); @@ -22,7 +73,12 @@ export function validateAuth(options) { } if (options.roles && !options.roles.includes(user.type)) { - res.status(401).send('Unauthorized'); + res.status(401).send( + new SafeNSoundError({ + errCode: 'UNAUTHORIZED', + message: 'Unauthorized' + }) + ); return; } @@ -30,11 +86,10 @@ export function validateAuth(options) { }; } -export const requireAuth = validateAuth({}); -export const requireAdmin = validateAuth({ roles: [UserType.ADMIN] }); -export const requireMonitor = validateAuth({ roles: [UserType.MONITOR] }); -export const requireUser = validateAuth({ roles: [UserType.USER] }); -export const requireUserOrMonitor = validateAuth({ +export const requireAdmin = requireRole({ roles: [UserType.ADMIN] }); +export const requireMonitor = requireRole({ roles: [UserType.MONITOR] }); +export const requireUser = requireRole({ roles: [UserType.USER] }); +export const requireUserOrMonitor = requireRole({ roles: [UserType.USER, UserType.MONITOR] }); diff --git a/src/api/routes.js b/src/api/routes.js
index b91e7ba..1853b57 100644 --- a/src/api/routes.js +++ b/src/api/routes.js
@@ -2,34 +2,60 @@ import * as routes from './routes/index.js'; import * as url from 'node:url'; import express from 'express'; +let dumpHttpEndpoints = false; + +function logPrefixed(...args) { + console.log('%', ...args); +} + +function logHttpHeader() { + if (!dumpHttpEndpoints) return; + logPrefixed('@baseUrl=http://localhost:3000'); + logPrefixed('@username=admin'); + logPrefixed('@password=admin'); + logPrefixed('@email=admin@example.com'); + logPrefixed('@userType=admin'); + logPrefixed(''); + logPrefixed(''); +} + function logHttpEntry(method, route, routeMethod) { - if (routeMethod.description) console.log('%', '#', routeMethod.description); + console.log( + 'Registering', + method.toUpperCase(), + route + ':', + routeMethod.description || 'No description provided' + ); + if (!dumpHttpEndpoints) return; + + if (routeMethod.description) logPrefixed('#', routeMethod.description); + + logPrefixed(method, '{{baseUrl}}' + route, 'HTTP/1.1'); - console.log('%', method, '{{baseUrl}}' + route, 'HTTP/1.1'); if (routeMethod.exampleHeaders) { for (var key of Object.keys(routeMethod.exampleHeaders)) { - console.log('%', `${key}: ${routeMethod.exampleHeaders[key]}`); + logPrefixed(`${key}: ${routeMethod.exampleHeaders[key]}`); } } + if (routeMethod.exampleBody) { - console.log('%', 'Content-Type: application/json'); - console.log('% '); - console.log( - '%', + logPrefixed('Content-Type: application/json'); + logPrefixed(''); + logPrefixed( JSON.stringify(routeMethod.exampleBody, null, 4).replaceAll( '\n', '\n% ' ) ); - console.log('% '); + logPrefixed(''); } - console.log('% '); - console.log('%', '###'); + + logPrefixed(''); + logPrefixed('###'); } export function registerRoutes(app) { - // http file header: - console.log('%', ''); + logHttpHeader(); let routeCount = 0; Object.keys(routes).forEach(routeName => { @@ -46,11 +72,6 @@ export function registerRoutes(app) { const routeMethod = route.methods[routeMethodName]; if (routeMethod === undefined) return; - console.log( - 'Registering', - routeMethodName.toUpperCase(), - route.path - ); logHttpEntry( routeMethodName.toUpperCase(), route.path, @@ -67,10 +88,12 @@ export function registerRoutes(app) { console.log(`Registered ${routeCount} routes.`); } +// Check if the script is run directly, to create the routes.html file. if (import.meta.url.startsWith('file:')) { const modulePath = url.fileURLToPath(import.meta.url); if (process.argv[1] === modulePath) { const app = express(); + dumpHttpEndpoints = true; registerRoutes(app); process.exit(1); } diff --git a/src/api/routes/alarmRoutes.js b/src/api/routes/alarmRoutes.js
index 9739f4f..f62aa6c 100644 --- a/src/api/routes/alarmRoutes.js +++ b/src/api/routes/alarmRoutes.js
@@ -1,7 +1,7 @@ import { requireMonitor, requireUser, - validateAuth + requireRole } from '#api/middlewares/index.js'; import { UserType } from '#db/schemas/index.js'; import { RouteMethod } from '#api/RouteDescription.js'; diff --git a/src/api/routes/auth/accountRoutes.js b/src/api/routes/auth/accountRoutes.js
index a2181d1..547110e 100644 --- a/src/api/routes/auth/accountRoutes.js +++ b/src/api/routes/auth/accountRoutes.js
@@ -1,6 +1,7 @@ import { deleteUser, loginUser, registerUser } from '#db/index.js'; import { AuthDto, RegisterDto } from '#dto/index.js'; import { RouteDescription, RouteMethod } from '#api/RouteDescription.js'; +import { WhoAmIDto } from '#dto/auth/WhoAmIDto.js'; /** * @type {RouteDescription} @@ -91,3 +92,23 @@ export const deleteRoute = { }) } }; + +/** + * @type {RouteDescription} + */ +export const whoAmI = { + path: '/auth/whoami', + methods: { + get: new RouteMethod({ + description: 'Get current user', + async method(req, res) { + const data = await WhoAmIDto.create({ + userId: req.auth.sub, + deviceId: req.auth.deviceId, + type: req.auth.type + }); + res.send(data); + } + }) + } +}; diff --git a/src/api/routes/auth/adminAccountRoutes.js b/src/api/routes/auth/adminAccountRoutes.js
index 2153945..13cca53 100644 --- a/src/api/routes/auth/adminAccountRoutes.js +++ b/src/api/routes/auth/adminAccountRoutes.js
@@ -1,6 +1,6 @@ import { deleteUser, loginUser, registerUser, UserType } from '#db/index.js'; import { AuthDto, RegisterDto } from '#dto/index.js'; -import { requireAdmin, validateAuth } from '#api/middlewares/index.js'; +import { requireAdmin, requireRole } from '#api/middlewares/index.js'; import { RouteDescription, RouteMethod } from '#api/RouteDescription.js'; /** diff --git a/src/api/routes/auth/deviceRoutes.js b/src/api/routes/auth/deviceRoutes.js
index 849a48c..40090e8 100644 --- a/src/api/routes/auth/deviceRoutes.js +++ b/src/api/routes/auth/deviceRoutes.js
@@ -1,6 +1,6 @@ import { registerUser } from '#db/index.js'; import { RegisterDto } from '#dto/index.js'; -import { validateAuth } from '#api/middlewares/index.js'; +import { requireRole } from '#api/middlewares/index.js'; import { RouteDescription, RouteMethod } from '#api/RouteDescription.js'; /** @@ -10,7 +10,7 @@ export const getDevicesRoute = { path: '/auth/devices', methods: { get: new RouteMethod({ - middlewares: [validateAuth({})], + middlewares: [requireRole({})], async method(req, res) { const data = await RegisterDto.create(req.body); const registerResult = await registerUser(data); diff --git a/src/api/routes/budgetRoutes.js b/src/api/routes/budgetRoutes.js
index d7ebde4..4ad4897 100644 --- a/src/api/routes/budgetRoutes.js +++ b/src/api/routes/budgetRoutes.js
@@ -1,7 +1,7 @@ import { requireMonitor, requireUser, - validateAuth + requireRole } from '#api/middlewares/index.js'; import { UserType } from '#db/schemas/index.js'; import { @@ -10,25 +10,30 @@ import { RouteMethodList } from '#api/RouteDescription.js'; import { getUserById } from '#db/dbAccess/index.js'; +import { SafeNSoundError } from '#util/error.js'; /** * @type {RouteDescription} */ export const getBudgetByUserRoute = { path: '/budget/:id', - methods: new RouteMethodList({ + methods: { get: new RouteMethod({ middlewares: [requireMonitor], async method(req, res) { if (req.user.type !== UserType.ADMIN) { if (!req.user.monitoredUsers.includes(req.params.id)) - throw new Error('meow'); + throw new SafeNSoundError({ + errCode: 'UNAUTHORIZED', + message: + "You do not have permission to access this user's budget." + }); } - //if (!req.) - // const user = await getUserById(req.); + const user = await getUserById(req.params.id); + res.send({ balance: user.balance }); } }) - }) + } }; /** diff --git a/src/api/start.js b/src/api/start.js
index dcd5cf3..fcc4a28 100644 --- a/src/api/start.js +++ b/src/api/start.js
@@ -1,6 +1,6 @@ import express from 'express'; import { registerRoutes } from './routes.js'; -import { useCors, useLogging } from './middlewares/index.js'; +import { useAuthentication, useCors, useLogging } from './middlewares/index.js'; import { initDb } from '#db/index.js'; import { initJwt } from '#util/index.js'; import { handleErrors } from '#api/middlewares/errorMiddleware.js'; @@ -15,6 +15,7 @@ await initJwt(); // Configure Express app.use(express.json()); app.use(useCors); +app.use(useAuthentication); app.disable('x-powered-by'); app.set('json spaces', 2); diff --git a/src/dto/auth/WhoAmIDto.js b/src/dto/auth/WhoAmIDto.js
index 686194c..5e64282 100644 --- a/src/dto/auth/WhoAmIDto.js +++ b/src/dto/auth/WhoAmIDto.js
@@ -6,7 +6,6 @@ import Joi from 'joi'; */ export class WhoAmIDto { userId; - username; deviceId; type;