231 lines
6.7 KiB
TypeScript
231 lines
6.7 KiB
TypeScript
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';
|
|
|
|
/**
|
|
* `POST /api/v1/user/signIn`
|
|
*
|
|
* 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);
|
|
}
|
|
|
|
/**
|
|
* `POST /api/v1/user/signUp`
|
|
*
|
|
* 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);
|
|
}
|
|
|
|
/**
|
|
* `GET /api/v1/user/account`
|
|
*
|
|
* Handles requests for user info retrieval
|
|
*
|
|
* @param {Request} req The Express request
|
|
* @param {Response} res The Express resource
|
|
*/
|
|
export async function getUserHandler(
|
|
req: Request,
|
|
res: Response
|
|
) {
|
|
|
|
const userService = new UserService();
|
|
const user: jwt.JwtDecoded = res.locals.user.decoded;
|
|
|
|
// Get detailed data from DB
|
|
let existingUser: User | null = await userService.findById(user.sub);
|
|
|
|
if (existingUser === null) {
|
|
const error: ms.ErrorDTO = {
|
|
status: 'error',
|
|
error: 'User does not exist',
|
|
code: 'deleted_user'
|
|
};
|
|
return res.status(404)
|
|
.json(error);
|
|
}
|
|
|
|
// Respond with ShortUserInfoDTO
|
|
const userResponse: as.ShortUserInfoDTO = {
|
|
status: 'ok',
|
|
id: existingUser.id!,
|
|
name: existingUser.name,
|
|
role: existingUser.role
|
|
}
|
|
return res.status(201)
|
|
.send(userResponse);
|
|
}
|