Files
kittyBE/src/controllers/authController.ts

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);
}