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