mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-13 03:10:31 +00:00
315 lines
8.2 KiB
TypeScript
315 lines
8.2 KiB
TypeScript
import { Request, Response, Router } from 'express';
|
|
import { check, ValidationChain } from 'express-validator';
|
|
import { Service, Inject } from 'typedi';
|
|
import countries from 'country-codes-list';
|
|
import parsePhoneNumber from 'libphonenumber-js';
|
|
import BaseController from '@/api/controllers/BaseController';
|
|
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
|
import AuthenticationService from '@/services/Authentication';
|
|
import { ILoginDTO, ISystemUser, IRegisterDTO } from '@/interfaces';
|
|
import { ServiceError, ServiceErrors } from '@/exceptions';
|
|
import { DATATYPES_LENGTH } from '@/data/DataTypes';
|
|
import LoginThrottlerMiddleware from '@/api/middleware/LoginThrottlerMiddleware';
|
|
import config from '@/config';
|
|
|
|
@Service()
|
|
export default class AuthenticationController extends BaseController {
|
|
@Inject()
|
|
authService: AuthenticationService;
|
|
|
|
/**
|
|
* Constructor method.
|
|
*/
|
|
router() {
|
|
const router = Router();
|
|
|
|
router.post(
|
|
'/login',
|
|
this.loginSchema,
|
|
this.validationResult,
|
|
LoginThrottlerMiddleware,
|
|
asyncMiddleware(this.login.bind(this)),
|
|
this.handlerErrors
|
|
);
|
|
router.post(
|
|
'/register',
|
|
this.registerSchema,
|
|
this.validationResult,
|
|
asyncMiddleware(this.register.bind(this)),
|
|
this.handlerErrors
|
|
);
|
|
router.post(
|
|
'/send_reset_password',
|
|
this.sendResetPasswordSchema,
|
|
this.validationResult,
|
|
asyncMiddleware(this.sendResetPassword.bind(this)),
|
|
this.handlerErrors
|
|
);
|
|
router.post(
|
|
'/reset/:token',
|
|
this.resetPasswordSchema,
|
|
this.validationResult,
|
|
asyncMiddleware(this.resetPassword.bind(this)),
|
|
this.handlerErrors
|
|
);
|
|
return router;
|
|
}
|
|
|
|
/**
|
|
* Login schema.
|
|
*/
|
|
get loginSchema(): ValidationChain[] {
|
|
return [
|
|
check('crediential').exists().isEmail(),
|
|
check('password').exists().isLength({ min: 5 }),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Register schema.
|
|
*/
|
|
get registerSchema(): ValidationChain[] {
|
|
return [
|
|
check('first_name')
|
|
.exists()
|
|
.isString()
|
|
.trim()
|
|
.escape()
|
|
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
|
check('last_name')
|
|
.exists()
|
|
.isString()
|
|
.trim()
|
|
.escape()
|
|
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
|
check('email')
|
|
.exists()
|
|
.isString()
|
|
.isEmail()
|
|
.trim()
|
|
.escape()
|
|
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
|
check('phone_number')
|
|
.exists()
|
|
.isString()
|
|
.trim()
|
|
.escape()
|
|
.custom(this.phoneNumberValidator)
|
|
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
|
check('password')
|
|
.exists()
|
|
.isString()
|
|
.trim()
|
|
.escape()
|
|
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
|
check('country')
|
|
.exists()
|
|
.isString()
|
|
.trim()
|
|
.escape()
|
|
.custom(this.countryValidator)
|
|
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Country validator.
|
|
*/
|
|
countryValidator(value, { req }) {
|
|
const {
|
|
countries: { whitelist, blacklist },
|
|
} = config.registration;
|
|
const foundCountry = countries.findOne('countryCode', value);
|
|
|
|
if (!foundCountry) {
|
|
throw new Error('The country code is invalid.');
|
|
}
|
|
if (
|
|
// Focus with me! In case whitelist is not empty and the given coutry is not
|
|
// in whitelist throw the error.
|
|
//
|
|
// Or in case the blacklist is not empty and the given country exists
|
|
// in the blacklist throw the goddamn error.
|
|
(whitelist.length > 0 && whitelist.indexOf(value) === -1) ||
|
|
(blacklist.length > 0 && blacklist.indexOf(value) !== -1)
|
|
) {
|
|
throw new Error('The country code is not supported yet.');
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Phone number validator.
|
|
*/
|
|
phoneNumberValidator(value, { req }) {
|
|
const phoneNumber = parsePhoneNumber(value, req.body.country);
|
|
|
|
if (!phoneNumber || !phoneNumber.isValid()) {
|
|
throw new Error('Phone number is invalid with the given country code.');
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Reset password schema.
|
|
*/
|
|
get resetPasswordSchema(): ValidationChain[] {
|
|
return [
|
|
check('password')
|
|
.exists()
|
|
.isLength({ min: 5 })
|
|
.custom((value, { req }) => {
|
|
if (value !== req.body.confirm_password) {
|
|
throw new Error("Passwords don't match");
|
|
} else {
|
|
return value;
|
|
}
|
|
}),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Send reset password validation schema.
|
|
*/
|
|
get sendResetPasswordSchema(): ValidationChain[] {
|
|
return [check('email').exists().isEmail().trim().escape()];
|
|
}
|
|
|
|
/**
|
|
* Handle user login.
|
|
* @param {Request} req
|
|
* @param {Response} res
|
|
*/
|
|
async login(req: Request, res: Response, next: Function): Response {
|
|
const userDTO: ILoginDTO = this.matchedBodyData(req);
|
|
|
|
try {
|
|
const { token, user, tenant } = await this.authService.signIn(
|
|
userDTO.crediential,
|
|
userDTO.password
|
|
);
|
|
return res.status(200).send({ token, user, tenant });
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Organization register handler.
|
|
* @param {Request} req
|
|
* @param {Response} res
|
|
*/
|
|
async register(req: Request, res: Response, next: Function) {
|
|
const registerDTO: IRegisterDTO = this.matchedBodyData(req);
|
|
|
|
try {
|
|
const registeredUser: ISystemUser = await this.authService.register(
|
|
registerDTO
|
|
);
|
|
|
|
return res.status(200).send({
|
|
type: 'success',
|
|
code: 'REGISTER.SUCCESS',
|
|
message: 'Register organization has been success.',
|
|
});
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Send reset password handler
|
|
* @param {Request} req
|
|
* @param {Response} res
|
|
*/
|
|
async sendResetPassword(req: Request, res: Response, next: Function) {
|
|
const { email } = this.matchedBodyData(req);
|
|
|
|
try {
|
|
await this.authService.sendResetPassword(email);
|
|
|
|
return res.status(200).send({
|
|
code: 'SEND_RESET_PASSWORD_SUCCESS',
|
|
message: 'The reset password message has been sent successfully.',
|
|
});
|
|
} catch (error) {
|
|
if (error instanceof ServiceError) {
|
|
}
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Reset password handler
|
|
* @param {Request} req
|
|
* @param {Response} res
|
|
*/
|
|
async resetPassword(req: Request, res: Response, next: Function) {
|
|
const { token } = req.params;
|
|
const { password } = req.body;
|
|
|
|
try {
|
|
await this.authService.resetPassword(token, password);
|
|
|
|
return res.status(200).send({
|
|
type: 'RESET_PASSWORD_SUCCESS',
|
|
message: 'The password has been reset successfully.',
|
|
});
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handles the service errors.
|
|
*/
|
|
handlerErrors(error, req: Request, res: Response, next: Function) {
|
|
if (error instanceof ServiceError) {
|
|
if (
|
|
['INVALID_DETAILS', 'invalid_password'].indexOf(error.errorType) !== -1
|
|
) {
|
|
return res.boom.badRequest(null, {
|
|
errors: [{ type: 'INVALID_DETAILS', code: 100 }],
|
|
});
|
|
}
|
|
if (error.errorType === 'USER_INACTIVE') {
|
|
return res.boom.badRequest(null, {
|
|
errors: [{ type: 'USER_INACTIVE', code: 200 }],
|
|
});
|
|
}
|
|
if (
|
|
error.errorType === 'TOKEN_INVALID' ||
|
|
error.errorType === 'TOKEN_EXPIRED'
|
|
) {
|
|
return res.boom.badRequest(null, {
|
|
errors: [{ type: 'TOKEN_INVALID', code: 300 }],
|
|
});
|
|
}
|
|
if (error.errorType === 'USER_NOT_FOUND') {
|
|
return res.boom.badRequest(null, {
|
|
errors: [{ type: 'USER_NOT_FOUND', code: 400 }],
|
|
});
|
|
}
|
|
if (error.errorType === 'EMAIL_NOT_FOUND') {
|
|
return res.status(400).send({
|
|
errors: [{ type: 'EMAIL.NOT.REGISTERED', code: 500 }],
|
|
});
|
|
}
|
|
}
|
|
if (error instanceof ServiceErrors) {
|
|
const errorReasons = [];
|
|
|
|
if (error.hasType('PHONE_NUMBER_EXISTS')) {
|
|
errorReasons.push({ type: 'PHONE_NUMBER_EXISTS', code: 100 });
|
|
}
|
|
if (error.hasType('EMAIL_EXISTS')) {
|
|
errorReasons.push({ type: 'EMAIL.EXISTS', code: 200 });
|
|
}
|
|
if (errorReasons.length > 0) {
|
|
return res.boom.badRequest(null, { errors: errorReasons });
|
|
}
|
|
}
|
|
next(error);
|
|
}
|
|
}
|