mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-18 22:00:31 +00:00
refactor: Authentication service.
This commit is contained in:
@@ -1,327 +0,0 @@
|
||||
import express from 'express';
|
||||
import { check, validationResult } from 'express-validator';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import Mustache from 'mustache';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { pick } from 'lodash';
|
||||
import uniqid from 'uniqid';
|
||||
import moment from 'moment';
|
||||
import Logger from '@/services/Logger';
|
||||
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
|
||||
import SystemUser from '@/system/models/SystemUser';
|
||||
import mail from '@/services/mail';
|
||||
import { hashPassword } from '@/utils';
|
||||
import dbManager from '@/database/manager';
|
||||
import Tenant from '@/system/models/Tenant';
|
||||
import TenantUser from '@/models/TenantUser';
|
||||
import TenantsManager from '@/system/TenantsManager';
|
||||
import TenantModel from '@/models/TenantModel';
|
||||
import PasswordReset from '@/system/models/PasswordReset';
|
||||
|
||||
|
||||
export default {
|
||||
/**
|
||||
* Constructor method.
|
||||
*/
|
||||
router() {
|
||||
const router = express.Router();
|
||||
|
||||
router.post('/login',
|
||||
this.login.validation,
|
||||
asyncMiddleware(this.login.handler));
|
||||
|
||||
router.post('/register',
|
||||
this.register.validation,
|
||||
asyncMiddleware(this.register.handler));
|
||||
|
||||
router.post('/send_reset_password',
|
||||
this.sendResetPassword.validation,
|
||||
asyncMiddleware(this.sendResetPassword.handler));
|
||||
|
||||
router.post('/reset/:token',
|
||||
this.resetPassword.validation,
|
||||
asyncMiddleware(this.resetPassword.handler));
|
||||
|
||||
return router;
|
||||
},
|
||||
|
||||
/**
|
||||
* User login authentication request.
|
||||
*/
|
||||
login: {
|
||||
validation: [
|
||||
check('crediential').exists().isEmail(),
|
||||
check('password').exists().isLength({ min: 5 }),
|
||||
],
|
||||
async handler(req, res) {
|
||||
const validationErrors = validationResult(req);
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
return res.boom.badData(null, {
|
||||
code: 'validation_error', ...validationErrors,
|
||||
});
|
||||
}
|
||||
const form = { ...req.body };
|
||||
const { JWT_SECRET_KEY } = process.env;
|
||||
|
||||
Logger.log('info', 'Someone trying to login.', { form });
|
||||
|
||||
const user = await SystemUser.query()
|
||||
.withGraphFetched('tenant')
|
||||
.where('email', form.crediential)
|
||||
.orWhere('phone_number', form.crediential)
|
||||
.first();
|
||||
|
||||
if (!user) {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'INVALID_DETAILS', code: 100 }],
|
||||
});
|
||||
}
|
||||
if (!user.verifyPassword(form.password)) {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'INVALID_DETAILS', code: 100 }],
|
||||
});
|
||||
}
|
||||
if (!user.active) {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'USER_INACTIVE', code: 110 }],
|
||||
});
|
||||
}
|
||||
const lastLoginAt = moment().format('YYYY/MM/DD HH:mm:ss');
|
||||
|
||||
const tenantDb = TenantsManager.knexInstance(user.tenant.organizationId);
|
||||
TenantModel.knexBinded = tenantDb;
|
||||
|
||||
const updateTenantUser = TenantUser.tenant().query()
|
||||
.where('id', user.id)
|
||||
.update({ last_login_at: lastLoginAt });
|
||||
|
||||
const updateSystemUser = SystemUser.query()
|
||||
.where('id', user.id)
|
||||
.update({ last_login_at: lastLoginAt });
|
||||
|
||||
await Promise.all([updateTenantUser, updateSystemUser]);
|
||||
|
||||
const token = jwt.sign(
|
||||
{ email: user.email, _id: user.id },
|
||||
JWT_SECRET_KEY,
|
||||
{ expiresIn: '1d' },
|
||||
);
|
||||
Logger.log('info', 'Logging success.', { form });
|
||||
|
||||
return res.status(200).send({ token, user });
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Registers a new organization.
|
||||
*/
|
||||
register: {
|
||||
validation: [
|
||||
check('organization_name').exists().trim().escape(),
|
||||
check('first_name').exists().trim().escape(),
|
||||
check('last_name').exists().trim().escape(),
|
||||
check('email').exists().trim().escape(),
|
||||
check('phone_number').exists().trim().escape(),
|
||||
check('password').exists().trim().escape(),
|
||||
check('country').exists().trim().escape(),
|
||||
],
|
||||
async handler(req, res) {
|
||||
const validationErrors = validationResult(req);
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
return res.boom.badData(null, {
|
||||
code: 'validation_error', ...validationErrors,
|
||||
});
|
||||
}
|
||||
const form = { ...req.body };
|
||||
Logger.log('info', 'Someone trying to register.', { form });
|
||||
|
||||
const user = await SystemUser.query()
|
||||
.where('email', form.email)
|
||||
.orWhere('phone_number', form.phone_number)
|
||||
.first();
|
||||
|
||||
const errorReasons = [];
|
||||
|
||||
if (user && user.phoneNumber === form.phone_number) {
|
||||
errorReasons.push({ type: 'PHONE_NUMBER_EXISTS', code: 100 });
|
||||
}
|
||||
if (user && user.email === form.email) {
|
||||
errorReasons.push({ type: 'EMAIL_EXISTS', code: 200 });
|
||||
}
|
||||
if (errorReasons.length > 0) {
|
||||
return res.status(400).send({ errors: errorReasons });
|
||||
}
|
||||
const organizationId = uniqid();
|
||||
const tenantOrganization = await Tenant.query().insert({
|
||||
organization_id: organizationId,
|
||||
});
|
||||
|
||||
const hashedPassword = await hashPassword(form.password);
|
||||
const userInsert = {
|
||||
...pick(form, ['first_name', 'last_name', 'email', 'phone_number']),
|
||||
active: true,
|
||||
};
|
||||
const registeredUser = await SystemUser.query().insert({
|
||||
...userInsert,
|
||||
password: hashedPassword,
|
||||
tenant_id: tenantOrganization.id,
|
||||
});
|
||||
await dbManager.createDb(`bigcapital_tenant_${organizationId}`);
|
||||
|
||||
const tenantDb = TenantsManager.knexInstance(organizationId);
|
||||
await tenantDb.migrate.latest();
|
||||
|
||||
TenantModel.knexBinded = tenantDb;
|
||||
|
||||
await TenantUser.bindKnex(tenantDb).query().insert({
|
||||
...userInsert,
|
||||
invite_accepted_at: moment().format('YYYY/MM/DD HH:mm:ss'),
|
||||
});
|
||||
Logger.log('info', 'New tenant has been created.', { organizationId });
|
||||
|
||||
const filePath = path.join(global.rootPath, 'views/mail/Welcome.html');
|
||||
const template = fs.readFileSync(filePath, 'utf8');
|
||||
const rendered = Mustache.render(template, { ...form });
|
||||
const mailOptions = {
|
||||
to: userInsert.email,
|
||||
from: `${process.env.MAIL_FROM_NAME} ${process.env.MAIL_FROM_ADDRESS}`,
|
||||
subject: 'Welcome to Bigcapital',
|
||||
html: rendered,
|
||||
};
|
||||
mail.sendMail(mailOptions, (error) => {
|
||||
if (error) {
|
||||
Logger.log('error', 'Failed send welcome mail', { error, form });
|
||||
return;
|
||||
}
|
||||
Logger.log('info', 'User has been sent welcome email successfuly.', { form });
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
organization_id: organizationId,
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Send reset password link via email or SMS.
|
||||
*/
|
||||
sendResetPassword: {
|
||||
validation: [
|
||||
check('email').exists().isEmail(),
|
||||
],
|
||||
// eslint-disable-next-line consistent-return
|
||||
async handler(req, res) {
|
||||
const validationErrors = validationResult(req);
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
return res.boom.badData(null, {
|
||||
code: 'validation_error', ...validationErrors,
|
||||
});
|
||||
}
|
||||
const form = { ...req.body };
|
||||
Logger.log('info', 'User trying to send reset password.', { form });
|
||||
|
||||
const user = await SystemUser.query().where('email', form.email).first();
|
||||
|
||||
if (!user) {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'EMAIL.NOT.REGISTERED', code: 200 }],
|
||||
});
|
||||
}
|
||||
// Delete all stored tokens of reset password that associate to the give email.
|
||||
await PasswordReset.query()
|
||||
.where('email', form.email)
|
||||
.delete();
|
||||
|
||||
const token = uniqid();
|
||||
const passwordReset = await PasswordReset.query()
|
||||
.insert({ email: form.email, token });
|
||||
|
||||
const filePath = path.join(global.rootPath, 'views/mail/ResetPassword.html');
|
||||
const template = fs.readFileSync(filePath, 'utf8');
|
||||
const rendered = Mustache.render(template, {
|
||||
url: `${req.protocol}://${req.hostname}/reset/${passwordReset.token}`,
|
||||
first_name: user.firstName,
|
||||
last_name: user.lastName,
|
||||
// contact_us_email: config.contactUsMail,
|
||||
});
|
||||
|
||||
const mailOptions = {
|
||||
to: user.email,
|
||||
from: `${process.env.MAIL_FROM_NAME} ${process.env.MAIL_FROM_ADDRESS}`,
|
||||
subject: 'Bigcapital - Password Reset',
|
||||
html: rendered,
|
||||
};
|
||||
mail.sendMail(mailOptions, (error) => {
|
||||
if (error) {
|
||||
Logger.log('error', 'Failed send reset password mail', { error, form });
|
||||
return;
|
||||
}
|
||||
Logger.log('info', 'User has been sent reset password email successfuly.', { form });
|
||||
});
|
||||
res.status(200).send({ email: passwordReset.email });
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Reset password.
|
||||
*/
|
||||
resetPassword: {
|
||||
validation: [
|
||||
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;
|
||||
}
|
||||
}),
|
||||
],
|
||||
async handler(req, res) {
|
||||
const validationErrors = validationResult(req);
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
return res.boom.badData(null, {
|
||||
code: 'validation_error', ...validationErrors,
|
||||
});
|
||||
}
|
||||
Logger.log('info', 'User trying to reset password.');
|
||||
const { token } = req.params;
|
||||
const { password } = req.body;
|
||||
|
||||
const tokenModel = await PasswordReset.query()
|
||||
.where('token', token)
|
||||
// .where('created_at', '>=', Date.now() - 3600000)
|
||||
.first();
|
||||
|
||||
if (!tokenModel) {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'TOKEN_INVALID', code: 100 }],
|
||||
});
|
||||
}
|
||||
const user = await SystemUser.query()
|
||||
.where('email', tokenModel.email).first();
|
||||
|
||||
if (!user) {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'USER_NOT_FOUND', code: 120 }],
|
||||
});
|
||||
}
|
||||
const hashedPassword = await hashPassword(password);
|
||||
|
||||
await SystemUser.query()
|
||||
.where('email', tokenModel.email)
|
||||
.update({
|
||||
password: hashedPassword,
|
||||
});
|
||||
|
||||
// Delete the reset password token.
|
||||
await PasswordReset.query().where('token', token).delete();
|
||||
Logger.log('info', 'User password has been reset successfully.');
|
||||
|
||||
return res.status(200).send({});
|
||||
},
|
||||
},
|
||||
};
|
||||
225
server/src/http/controllers/Authentication.ts
Normal file
225
server/src/http/controllers/Authentication.ts
Normal file
@@ -0,0 +1,225 @@
|
||||
import { Request, Response, Router } from 'express';
|
||||
import { check, validationResult, matchedData, ValidationChain } from 'express-validator';
|
||||
import { Service, Inject } from 'typedi';
|
||||
import { camelCase, mapKeys } from 'lodash';
|
||||
import validateMiddleware from '@/http/middleware/validateMiddleware';
|
||||
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
|
||||
import prettierMiddleware from '@/http/middleware/prettierMiddleware';
|
||||
import AuthenticationService from '@/services/Authentication';
|
||||
import { IUserOTD, ISystemUser, IRegisterOTD } from '@/interfaces';
|
||||
import { ServiceError, ServiceErrors } from "@/exceptions";
|
||||
import { IRegisterDTO } from 'src/interfaces';
|
||||
|
||||
@Service()
|
||||
export default class AuthenticationController {
|
||||
@Inject()
|
||||
authService: AuthenticationService;
|
||||
|
||||
/**
|
||||
* Constructor method.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.post(
|
||||
'/login',
|
||||
this.loginSchema,
|
||||
validateMiddleware,
|
||||
asyncMiddleware(this.login.bind(this))
|
||||
);
|
||||
router.post(
|
||||
'/register',
|
||||
this.registerSchema,
|
||||
validateMiddleware,
|
||||
asyncMiddleware(this.register.bind(this))
|
||||
);
|
||||
router.post(
|
||||
'/send_reset_password',
|
||||
this.sendResetPasswordSchema,
|
||||
validateMiddleware,
|
||||
asyncMiddleware(this.sendResetPassword.bind(this))
|
||||
);
|
||||
router.post(
|
||||
'/reset/:token',
|
||||
this.resetPasswordSchema,
|
||||
validateMiddleware,
|
||||
asyncMiddleware(this.resetPassword.bind(this))
|
||||
);
|
||||
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('organization_name').exists().trim().escape(),
|
||||
check('first_name').exists().trim().escape(),
|
||||
check('last_name').exists().trim().escape(),
|
||||
check('email').exists().trim().escape(),
|
||||
check('phone_number').exists().trim().escape(),
|
||||
check('password').exists().trim().escape(),
|
||||
check('country').exists().trim().escape(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}),
|
||||
]
|
||||
}
|
||||
|
||||
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: IUserOTD = mapKeys(matchedData(req, {
|
||||
locations: ['body'],
|
||||
includeOptionals: true,
|
||||
}), (v, k) => camelCase(k));
|
||||
|
||||
try {
|
||||
const { token, user } = await this.authService.signIn(
|
||||
userDTO.crediential,
|
||||
userDTO.password
|
||||
);
|
||||
return res.status(200).send({ token, user });
|
||||
} catch (error) {
|
||||
if (error instanceof ServiceError) {
|
||||
if (error.errorType === 'invalid_details') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'INVALID_DETAILS', code: 100 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'user_inactive') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'INVALID_DETAILS', code: 200 }],
|
||||
});
|
||||
}
|
||||
}
|
||||
next();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Organization register handler.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
*/
|
||||
async register(req: Request, res: Response, next: Function) {
|
||||
const registerDTO: IRegisterDTO = mapKeys(matchedData(req, {
|
||||
locations: ['body'],
|
||||
includeOptionals: true,
|
||||
}), (v, k) => camelCase(k));
|
||||
|
||||
try {
|
||||
const registeredUser = await this.authService.register(registerDTO);
|
||||
|
||||
return res.status(200).send({
|
||||
code: 'REGISTER.SUCCESS',
|
||||
message: 'Register organization has been success.',
|
||||
});
|
||||
} catch (error) {
|
||||
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.status(200).send({ errors: errorReasons });
|
||||
}
|
||||
}
|
||||
next();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send reset password handler
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
*/
|
||||
async sendResetPassword(req: Request, res: Response, next: Function) {
|
||||
const { email } = req.body;
|
||||
|
||||
try {
|
||||
await this.authService.sendResetPassword(email);
|
||||
|
||||
return res.status(200).send({
|
||||
code: 'SEND_RESET_PASSWORD_SUCCESS',
|
||||
});
|
||||
} catch(error) {
|
||||
if (error instanceof ServiceError) {
|
||||
if (error.errorType === 'email_not_found') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'EMAIL.NOT.REGISTERED', code: 200 }],
|
||||
});
|
||||
}
|
||||
}
|
||||
next();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset password handler
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
*/
|
||||
async resetPassword(req: Request, res: Response) {
|
||||
const { token } = req.params;
|
||||
const { password } = req.body;
|
||||
|
||||
try {
|
||||
await this.authService.resetPassword(token, password);
|
||||
|
||||
return res.status(200).send({
|
||||
type: 'RESET_PASSWORD_SUCCESS',
|
||||
})
|
||||
} catch(error) {
|
||||
console.log(error);
|
||||
|
||||
if (error instanceof ServiceError) {
|
||||
if (error.errorType === 'token_invalid') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'TOKEN_INVALID', code: 100 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'user_not_found') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'USER_NOT_FOUND', code: 120 }],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -10,7 +10,6 @@ import path from 'path';
|
||||
import fs from 'fs';
|
||||
import Mustache from 'mustache';
|
||||
import moment from 'moment';
|
||||
import mail from '@/services/mail';
|
||||
import { hashPassword } from '@/utils';
|
||||
import SystemUser from '@/system/models/SystemUser';
|
||||
import Invite from '@/system/models/Invite';
|
||||
|
||||
@@ -31,7 +31,7 @@ import TenantDependencyInjection from '@/http/middleware/TenantDependencyInjecti
|
||||
import SubscriptionMiddleware from '@/http/middleware/SubscriptionMiddleware';
|
||||
|
||||
export default (app) => {
|
||||
app.use('/api/auth', Authentication.router());
|
||||
app.use('/api/auth', Container.get(Authentication).router());
|
||||
app.use('/api/invite', InviteUsers.router());
|
||||
app.use('/api/vouchers', Container.get(VouchersController).router());
|
||||
app.use('/api/subscription', Container.get(Subscription).router());
|
||||
|
||||
Reference in New Issue
Block a user