feat: major code refactor, add login and register endpoints with swagger
All checks were successful
Update changelog / changelog (push) Successful in 27s

This commit is contained in:
2025-12-29 18:26:50 +01:00
parent 3f225a1ecb
commit 41f3b0f0f2
22 changed files with 1425 additions and 128 deletions

View File

@@ -0,0 +1,186 @@
import { Request, Response } from 'express';
import { User } from '../entities/User';
import * as ms from '../schemas/miscSchema';
import * as as from '../schemas/authSchema';
import { UserService } from '../services/userService';
import { generateSha512 } from '../tools/hasher';
import * as jwt from '../tools/jwt';
/**
* Handles requests for user logon
*
* @param {Request} req The Express request
* @param {Response} res The Express resource
*/
export async function loginUserHandler(
req: Request<{}, {}, as.LoginRequestDTO['body']>,
res: Response
) {
const userService: UserService = new UserService();
const requestBody = req.body;
// Compare against what exists in the DB
let existingUser: User | null = await userService.findByCredentials({
name: requestBody.name
});
if (existingUser === null) {
// User not found? Return an error.
const error: ms.ErrorDTO = {
status: 'error',
error: 'User does not exist'
};
return res.status(404)
.json(error);
}
// For the reasoning behind this, check the long
// comment inside of createUserHandler().
// TL;DR: This is a fail-safe in case of
// a server shutdown mid user creation.
if (existingUser.dirtyPasswordHashBit) {
existingUser.passwordHash = generateSha512(`${existingUser.id}:${existingUser.passwordHash}`);
existingUser.dirtyPasswordHashBit = false;
userService.save(existingUser);
}
// Compute salted SHA512
const passwordHashed: string = generateSha512(`${existingUser.id}:${requestBody.password}`);
if (passwordHashed !== existingUser.passwordHash) {
// User not found? Return an error.
const error: ms.ErrorDTO = {
status: 'error',
error: 'Username <-> password pair not found'
};
return res.status(404)
.json(error);
}
// User found? Generate a JWT.
const token = jwt.signJwt(
{ sub: existingUser.id, role: existingUser.role },
'accessTokenPrivateKey',
{ expiresIn: requestBody.ttl ?? as.DEFAULT_TOKEN_LIFETIME }
);
// Respond with UserInfoDTO
const userResponse: as.UserInfoDTO = {
status: 'ok',
name: requestBody.name,
role: existingUser.role,
token: token,
ttl: requestBody.ttl ?? as.DEFAULT_TOKEN_LIFETIME
}
return res.status(200)
.send(userResponse);
}
/**
* Handles requests for user registration
*
* @param {Request} req The Express request
* @param {Response} res The Express resource
*/
export async function createUserHandler(
req: Request<{}, {}, as.LoginRequestDTO['body']>,
res: Response
) {
// Ideally, there would be a registration DTO which would incorporate
// some sort of CAPTCHA or CSRF token.
const userService = new UserService();
const requestBody = req.body;
// Compare against what exists in the DB
let usersFound: number = await userService.countAll(); // CAN THIS BE REPLACED WITH userService.lastId()??????
let role: number = 0; // Let the default role be a standard user.
// No users found? Make the new (and only) user an Administrator.
if (usersFound == 0)
role = 1;
// Users found? The new user should NOT be an Administrator.
// Since role by default is 0 (non-admin), nothing needs to change.
// Check if user already exists. If yes, return an error.
let existingUserId: number = await userService.findIdByCredentials({
name: requestBody.name
});
if (existingUserId >= 0) {
const error: ms.ErrorDTO = {
status: 'error',
error: 'User already exists'
};
return res.status(403)
.json(error);
}
// Otherwise we're free to add him/her.
let user: User = {
id: undefined,
name: requestBody.name,
passwordHash: requestBody.password,
dirtyPasswordHashBit: true,
role: role,
createdAt: Date.now(),
links: []
};
await userService.add(user);
// Note how we're setting the retrieved password as the password hash,
// without hashing it first. This is because we don't know ahead of time
// what id will the user have (which we could use to salt the password).
// We also could hash the password with a user's name,
// but then we would either need to deem username unchangeable,
// or prompt the user for password once he/she wants to change his/her name.
// Out of the three not ideal solutions, the first has been incorporated,
// thus we add the user, then retrieve his/her id, to then salt user's
// password with it later. This requires setting a "dirty bit" and storing
// the password as received, so in a way in "plain text". Then recalculate
// the password and remove the dirty bit here, or, in a miniscule case if
// it so happens the server crashes or gets restarted while registering a user,
// the password will be recalculated on login attempt if dirty bit is set.
// TODO: explore alternative options with queryRunner's transactions?
// Check if user added successfully.
// If so, recalculate the salted hash.
const insertedUser: User | null = await userService.findByCredentials({
name: requestBody.name
});
// Return an error if - for whatever reason - the user has not been added.
if (insertedUser === null) {
const error: ms.ErrorDTO = {
status: 'error',
error: 'Could not add a new user',
code: 'server_error'
}
return res.status(500)
.json(error);
}
// Rewrite the user's password to use the salted hash
const passwordHashed: string = generateSha512(`${insertedUser.id}:${requestBody.password}`);
insertedUser.passwordHash = passwordHashed;
insertedUser.dirtyPasswordHashBit = false;
await userService.save(insertedUser);
// Generate a JWT.
const token = jwt.signJwt(
{ sub: insertedUser.id, role: role },
'accessTokenPrivateKey',
{ expiresIn: requestBody.ttl ?? as.DEFAULT_TOKEN_LIFETIME }
);
// Respond with UserInfoDTO
const userResponse: as.UserInfoDTO = {
status: 'ok',
name: requestBody.name,
role: role,
token: token,
ttl: requestBody.ttl ?? as.DEFAULT_TOKEN_LIFETIME
}
return res.status(201)
.send(userResponse);
}