feat: major code refactor, add login and register endpoints with swagger
All checks were successful
Update changelog / changelog (push) Successful in 27s
All checks were successful
Update changelog / changelog (push) Successful in 27s
This commit is contained in:
186
src/controllers/authController.ts
Normal file
186
src/controllers/authController.ts
Normal 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);
|
||||
}
|
||||
Reference in New Issue
Block a user