diff --git a/packages/server/src/api/controllers/Authentication.ts b/packages/server/src/api/controllers/Authentication.ts index dbe5b62a1..44c4cd147 100644 --- a/packages/server/src/api/controllers/Authentication.ts +++ b/packages/server/src/api/controllers/Authentication.ts @@ -1,26 +1,23 @@ 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'; +import AuthenticationApplication from '@/services/Authentication/AuthApplication'; @Service() export default class AuthenticationController extends BaseController { @Inject() - authService: AuthenticationService; + private authApplication: AuthenticationApplication; /** * Constructor method. */ - router() { + public router() { const router = Router(); router.post( @@ -56,9 +53,10 @@ export default class AuthenticationController extends BaseController { } /** - * Login schema. + * Login validation schema. + * @returns {ValidationChain[]} */ - get loginSchema(): ValidationChain[] { + private get loginSchema(): ValidationChain[] { return [ check('crediential').exists().isEmail(), check('password').exists().isLength({ min: 5 }), @@ -66,9 +64,10 @@ export default class AuthenticationController extends BaseController { } /** - * Register schema. + * Register validation schema. + * @returns {ValidationChain[]} */ - get registerSchema(): ValidationChain[] { + private get registerSchema(): ValidationChain[] { return [ check('first_name') .exists() @@ -89,71 +88,20 @@ export default class AuthenticationController extends BaseController { .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. + * @returns {ValidationChain[]} */ - get resetPasswordSchema(): ValidationChain[] { + private get resetPasswordSchema(): ValidationChain[] { return [ check('password') .exists() @@ -170,8 +118,9 @@ export default class AuthenticationController extends BaseController { /** * Send reset password validation schema. + * @returns {ValidationChain[]} */ - get sendResetPasswordSchema(): ValidationChain[] { + private get sendResetPasswordSchema(): ValidationChain[] { return [check('email').exists().isEmail().trim().escape()]; } @@ -180,11 +129,11 @@ export default class AuthenticationController extends BaseController { * @param {Request} req * @param {Response} res */ - async login(req: Request, res: Response, next: Function): Response { + private async login(req: Request, res: Response, next: Function): Response { const userDTO: ILoginDTO = this.matchedBodyData(req); try { - const { token, user, tenant } = await this.authService.signIn( + const { token, user, tenant } = await this.authApplication.signIn( userDTO.crediential, userDTO.password ); @@ -199,14 +148,13 @@ export default class AuthenticationController extends BaseController { * @param {Request} req * @param {Response} res */ - async register(req: Request, res: Response, next: Function) { + private async register(req: Request, res: Response, next: Function) { const registerDTO: IRegisterDTO = this.matchedBodyData(req); try { - const registeredUser: ISystemUser = await this.authService.register( + const registeredUser: ISystemUser = await this.authApplication.signUp( registerDTO ); - return res.status(200).send({ type: 'success', code: 'REGISTER.SUCCESS', @@ -222,11 +170,11 @@ export default class AuthenticationController extends BaseController { * @param {Request} req * @param {Response} res */ - async sendResetPassword(req: Request, res: Response, next: Function) { + private async sendResetPassword(req: Request, res: Response, next: Function) { const { email } = this.matchedBodyData(req); try { - await this.authService.sendResetPassword(email); + await this.authApplication.sendResetPassword(email); return res.status(200).send({ code: 'SEND_RESET_PASSWORD_SUCCESS', @@ -244,12 +192,12 @@ export default class AuthenticationController extends BaseController { * @param {Request} req * @param {Response} res */ - async resetPassword(req: Request, res: Response, next: Function) { + private async resetPassword(req: Request, res: Response, next: Function) { const { token } = req.params; const { password } = req.body; try { - await this.authService.resetPassword(token, password); + await this.authApplication.resetPassword(token, password); return res.status(200).send({ type: 'RESET_PASSWORD_SUCCESS', @@ -263,7 +211,7 @@ export default class AuthenticationController extends BaseController { /** * Handles the service errors. */ - handlerErrors(error, req: Request, res: Response, next: Function) { + private handlerErrors(error, req: Request, res: Response, next: Function) { if (error instanceof ServiceError) { if ( ['INVALID_DETAILS', 'invalid_password'].indexOf(error.errorType) !== -1 diff --git a/packages/server/src/interfaces/Authentication.ts b/packages/server/src/interfaces/Authentication.ts index be86dfdd3..fb26c4fa6 100644 --- a/packages/server/src/interfaces/Authentication.ts +++ b/packages/server/src/interfaces/Authentication.ts @@ -1,29 +1,77 @@ import { ISystemUser } from './User'; import { ITenant } from './Tenancy'; +import { SystemUser } from '@/system/models'; export interface IRegisterDTO { - firstName: string, - lastName: string, - email: string, - password: string, - organizationName: string, -}; + firstName: string; + lastName: string; + email: string; + password: string; + organizationName: string; +} export interface ILoginDTO { - crediential: string, - password: string, -}; + crediential: string; + password: string; +} export interface IPasswordReset { - id: number, - email: string, - token: string, - createdAt: Date, -}; + id: number; + email: string; + token: string; + createdAt: Date; +} export interface IAuthenticationService { - signIn(emailOrPhone: string, password: string): Promise<{ user: ISystemUser, token: string, tenant: ITenant }>; + signIn( + email: string, + password: string + ): Promise<{ user: ISystemUser; token: string; tenant: ITenant }>; register(registerDTO: IRegisterDTO): Promise; sendResetPassword(email: string): Promise; resetPassword(token: string, password: string): Promise; +} + +export interface IAuthSigningInEventPayload { + email: string; + password: string; + user: ISystemUser; +} + +export interface IAuthSignedInEventPayload { + email: string; + password: string; + user: ISystemUser; +} + +export interface IAuthSigningUpEventPayload { + signupDTO: IRegisterDTO; +} + +export interface IAuthSignedUpEventPayload { + signupDTO: IRegisterDTO; + tenant: ITenant; + user: ISystemUser; +} + +export interface IAuthSignInPOJO { + user: ISystemUser; + token: string; + tenant: ITenant; +} + +export interface IAuthResetedPasswordEventPayload { + user: SystemUser; + token: string; + password: string; +} + + +export interface IAuthSendingResetPassword { + user: ISystemUser, + token: string; +} +export interface IAuthSendedResetPassword { + user: ISystemUser, + token: string; } \ No newline at end of file diff --git a/packages/server/src/jobs/ResetPasswordMail.ts b/packages/server/src/jobs/ResetPasswordMail.ts index cd33e49b3..f9c3c15ff 100644 --- a/packages/server/src/jobs/ResetPasswordMail.ts +++ b/packages/server/src/jobs/ResetPasswordMail.ts @@ -1,5 +1,5 @@ import { Container, Inject } from 'typedi'; -import AuthenticationService from '@/services/Authentication'; +import AuthenticationService from '@/services/Authentication/AuthApplication'; export default class WelcomeEmailJob { /** diff --git a/packages/server/src/jobs/WelcomeSMS.ts b/packages/server/src/jobs/WelcomeSMS.ts index 4dc135db7..67a72c368 100644 --- a/packages/server/src/jobs/WelcomeSMS.ts +++ b/packages/server/src/jobs/WelcomeSMS.ts @@ -1,5 +1,5 @@ import { Container, Inject } from 'typedi'; -import AuthenticationService from '@/services/Authentication'; +import AuthenticationService from '@/services/Authentication/AuthApplication'; export default class WelcomeSMSJob { /** diff --git a/packages/server/src/jobs/welcomeEmail.ts b/packages/server/src/jobs/welcomeEmail.ts index b9c8bbde9..6b076556a 100644 --- a/packages/server/src/jobs/welcomeEmail.ts +++ b/packages/server/src/jobs/welcomeEmail.ts @@ -1,5 +1,5 @@ import { Container } from 'typedi'; -import AuthenticationService from '@/services/Authentication'; +import AuthenticationService from '@/services/Authentication/AuthApplication'; export default class WelcomeEmailJob { /** diff --git a/packages/server/src/services/Authentication/AuthApplication.ts b/packages/server/src/services/Authentication/AuthApplication.ts new file mode 100644 index 000000000..5c077530d --- /dev/null +++ b/packages/server/src/services/Authentication/AuthApplication.ts @@ -0,0 +1,56 @@ +import { Service, Inject, Container } from 'typedi'; +import { IRegisterDTO, ISystemUser, IPasswordReset } from '@/interfaces'; +import { AuthSigninService } from './AuthSignin'; +import { AuthSignupService } from './AuthSignup'; +import { AuthSendResetPassword } from './AuthSendResetPassword'; + +@Service() +export default class AuthenticationApplication { + @Inject() + private authSigninService: AuthSigninService; + + @Inject() + private authSignupService: AuthSignupService; + + @Inject() + private authResetPasswordService: AuthSendResetPassword; + + /** + * Signin and generates JWT token. + * @throws {ServiceError} + * @param {string} email - Email address. + * @param {string} password - Password. + * @return {Promise<{user: IUser, token: string}>} + */ + public async signIn(email: string, password: string) { + return this.authSigninService.signIn(email, password); + } + + /** + * Signup a new user. + * @param {IRegisterDTO} signupDTO + * @returns {Promise} + */ + public async signUp(signupDTO: IRegisterDTO): Promise { + return this.authSignupService.signUp(signupDTO); + } + + /** + * Generates and retrieve password reset token for the given user email. + * @param {string} email + * @return {} + */ + public async sendResetPassword(email: string): Promise { + return this.authResetPasswordService.sendResetPassword(email); + } + + /** + * Resets a user password from given token. + * @param {string} token - Password reset token. + * @param {string} password - New Password. + * @return {Promise} + */ + public async resetPassword(token: string, password: string): Promise { + return this.authResetPasswordService.resetPassword(token, password); + } +} diff --git a/packages/server/src/services/Authentication/AuthSendResetPassword.ts b/packages/server/src/services/Authentication/AuthSendResetPassword.ts new file mode 100644 index 000000000..c3a12b47b --- /dev/null +++ b/packages/server/src/services/Authentication/AuthSendResetPassword.ts @@ -0,0 +1,130 @@ +import { Inject, Service } from 'typedi'; +import uniqid from 'uniqid'; +import moment from 'moment'; +import config from '@/config'; +import { + IAuthResetedPasswordEventPayload, + IAuthSendedResetPassword, + IAuthSendingResetPassword, + IPasswordReset, + ISystemUser, +} from '@/interfaces'; +import { EventPublisher } from '@/lib/EventPublisher/EventPublisher'; +import events from '@/subscribers/events'; +import { PasswordReset } from '@/system/models'; +import { ERRORS } from './_constants'; +import { ServiceError } from '@/exceptions'; +import { hashPassword } from '@/utils'; + +@Service() +export class AuthSendResetPassword { + @Inject() + private eventPublisher: EventPublisher; + + @Inject('repositories') + private sysRepositories: any; + + /** + * Generates and retrieve password reset token for the given user email. + * @param {string} email + * @return {} + */ + public async sendResetPassword(email: string): Promise { + const user = await this.validateEmailExistance(email); + + const token: string = uniqid(); + + // Triggers sending reset password event. + await this.eventPublisher.emitAsync(events.auth.sendingResetPassword, { + user, + token, + } as IAuthSendingResetPassword); + + // Delete all stored tokens of reset password that associate to the give email. + this.deletePasswordResetToken(email); + + // Creates a new password reset row with unique token. + const passwordReset = await PasswordReset.query().insert({ email, token }); + + // Triggers sent reset password event. + await this.eventPublisher.emitAsync(events.auth.sendResetPassword, { + user, + token, + } as IAuthSendedResetPassword); + + return passwordReset; + } + + /** + * Resets a user password from given token. + * @param {string} token - Password reset token. + * @param {string} password - New Password. + * @return {Promise} + */ + public async resetPassword(token: string, password: string): Promise { + const { systemUserRepository } = this.sysRepositories; + + // Finds the password reset token. + const tokenModel: IPasswordReset = await PasswordReset.query().findOne( + 'token', + token + ); + // In case the password reset token not found throw token invalid error.. + if (!tokenModel) { + throw new ServiceError(ERRORS.TOKEN_INVALID); + } + // Different between tokne creation datetime and current time. + if ( + moment().diff(tokenModel.createdAt, 'seconds') > + config.resetPasswordSeconds + ) { + // Deletes the expired token by expired token email. + await this.deletePasswordResetToken(tokenModel.email); + throw new ServiceError(ERRORS.TOKEN_EXPIRED); + } + const user = await systemUserRepository.findOneByEmail(tokenModel.email); + + if (!user) { + throw new ServiceError(ERRORS.USER_NOT_FOUND); + } + const hashedPassword = await hashPassword(password); + + await systemUserRepository.update( + { password: hashedPassword }, + { id: user.id } + ); + // Deletes the used token. + await this.deletePasswordResetToken(tokenModel.email); + + // Triggers `onResetPassword` event. + await this.eventPublisher.emitAsync(events.auth.resetPassword, { + user, + token, + password, + } as IAuthResetedPasswordEventPayload); + } + + /** + * Deletes the password reset token by the given email. + * @param {string} email + * @returns {Promise} + */ + private async deletePasswordResetToken(email: string) { + return PasswordReset.query().where('email', email).delete(); + } + + /** + * Validates the given email existance on the storage. + * @throws {ServiceError} + * @param {string} email - email address. + */ + private async validateEmailExistance(email: string): Promise { + const { systemUserRepository } = this.sysRepositories; + const userByEmail = await systemUserRepository.findOneByEmail(email); + + if (!userByEmail) { + throw new ServiceError(ERRORS.EMAIL_NOT_FOUND); + } + return userByEmail; + } +} diff --git a/packages/server/src/services/Authentication/AuthSignin.ts b/packages/server/src/services/Authentication/AuthSignin.ts new file mode 100644 index 000000000..072aa7ab2 --- /dev/null +++ b/packages/server/src/services/Authentication/AuthSignin.ts @@ -0,0 +1,101 @@ +import { Container, Inject } from 'typedi'; +import { cloneDeep } from 'lodash'; +import { Tenant } from '@/system/models'; +import { + IAuthSigningInEventPayload, + IAuthSignInPOJO, + ISystemUser, +} from '@/interfaces'; +import { ServiceError } from '@/exceptions'; +import events from '@/subscribers/events'; +import { EventPublisher } from '@/lib/EventPublisher/EventPublisher'; +import { generateToken } from './_utils'; +import { ERRORS } from './_constants'; + +@Inject() +export class AuthSigninService { + @Inject() + private eventPublisher: EventPublisher; + + @Inject('repositories') + private sysRepositories: any; + + /** + * Validates the given email and password. + * @param {ISystemUser} user + * @param {string} email + * @param {string} password + */ + public async validateSignIn( + user: ISystemUser, + email: string, + password: string + ) { + const loginThrottler = Container.get('rateLimiter.login'); + + // Validate if the user is not exist. + if (!user) { + await loginThrottler.hit(email); + throw new ServiceError(ERRORS.INVALID_DETAILS); + } + // Validate if the given user's password is wrong. + if (!user.verifyPassword(password)) { + await loginThrottler.hit(email); + throw new ServiceError(ERRORS.INVALID_DETAILS); + } + // Validate if the given user is inactive. + if (!user.active) { + throw new ServiceError(ERRORS.USER_INACTIVE); + } + } + + /** + * Signin and generates JWT token. + * @throws {ServiceError} + * @param {string} email - Email address. + * @param {string} password - Password. + * @return {Promise<{user: IUser, token: string}>} + */ + public async signIn( + email: string, + password: string + ): Promise { + const { systemUserRepository } = this.sysRepositories; + + // Finds the user of the given email address. + const user = await systemUserRepository.findOneByEmail(email); + + // Validate the given email and password. + await this.validateSignIn(user, email, password); + + // Triggers on signing-in event. + await this.eventPublisher.emitAsync(events.auth.logining, { + email, + password, + user, + } as IAuthSigningInEventPayload); + + const token = generateToken(user); + + // Update the last login at of the user. + await systemUserRepository.patchLastLoginAt(user.id); + + // Triggers `onLogin` event. + await this.eventPublisher.emitAsync(events.auth.login, { + email, + password, + user, + }); + const tenant = await Tenant.query() + .findById(user.tenantId) + .withGraphFetched('metadata'); + + // Keep the user object immutable. + const outputUser = cloneDeep(user); + + // Remove password property from user object. + Reflect.deleteProperty(outputUser, 'password'); + + return { user: outputUser, token, tenant }; + } +} diff --git a/packages/server/src/services/Authentication/AuthSignup.ts b/packages/server/src/services/Authentication/AuthSignup.ts new file mode 100644 index 000000000..c8690b72f --- /dev/null +++ b/packages/server/src/services/Authentication/AuthSignup.ts @@ -0,0 +1,77 @@ +import { omit } from 'lodash'; +import moment from 'moment'; +import { ServiceError } from '@/exceptions'; +import { + IAuthSignedUpEventPayload, + IAuthSigningUpEventPayload, + IRegisterDTO, + ISystemUser, +} from '@/interfaces'; +import { ERRORS } from './_constants'; +import { Inject } from 'typedi'; +import { EventPublisher } from '@/lib/EventPublisher/EventPublisher'; +import TenantsManagerService from '../Tenancy/TenantsManager'; +import events from '@/subscribers/events'; +import { hashPassword } from '@/utils'; + +export class AuthSignupService { + @Inject() + private eventPublisher: EventPublisher; + + @Inject('repositories') + private sysRepositories: any; + + @Inject() + private tenantsManager: TenantsManagerService; + + /** + * Registers a new tenant with user from user input. + * @throws {ServiceErrors} + * @param {IRegisterDTO} signupDTO + * @returns {Promise} + */ + public async signUp(signupDTO: IRegisterDTO): Promise { + const { systemUserRepository } = this.sysRepositories; + + // Validates the given email uniqiness. + await this.validateEmailUniqiness(signupDTO.email); + + const hashedPassword = await hashPassword(signupDTO.password); + + // Triggers signin up event. + await this.eventPublisher.emitAsync(events.auth.registering, { + signupDTO, + } as IAuthSigningUpEventPayload); + + const tenant = await this.tenantsManager.createTenant(); + const registeredUser = await systemUserRepository.create({ + ...omit(signupDTO, 'country'), + active: true, + password: hashedPassword, + tenantId: tenant.id, + inviteAcceptedAt: moment().format('YYYY-MM-DD'), + }); + // Triggers signed up event. + await this.eventPublisher.emitAsync(events.auth.register, { + signupDTO, + tenant, + user: registeredUser, + } as IAuthSignedUpEventPayload); + + return registeredUser; + } + + /** + * Validates email uniqiness on the storage. + * @throws {ServiceErrors} + * @param {string} email - Email address + */ + private async validateEmailUniqiness(email: string) { + const { systemUserRepository } = this.sysRepositories; + const isEmailExists = await systemUserRepository.findOneByEmail(email); + + if (isEmailExists) { + throw new ServiceError(ERRORS.EMAIL_EXISTS); + } + } +} diff --git a/packages/server/src/services/Authentication/_constants.ts b/packages/server/src/services/Authentication/_constants.ts new file mode 100644 index 000000000..16a3a5831 --- /dev/null +++ b/packages/server/src/services/Authentication/_constants.ts @@ -0,0 +1,10 @@ +export const ERRORS = { + INVALID_DETAILS: 'INVALID_DETAILS', + USER_INACTIVE: 'USER_INACTIVE', + EMAIL_NOT_FOUND: 'EMAIL_NOT_FOUND', + TOKEN_INVALID: 'TOKEN_INVALID', + USER_NOT_FOUND: 'USER_NOT_FOUND', + TOKEN_EXPIRED: 'TOKEN_EXPIRED', + PHONE_NUMBER_EXISTS: 'PHONE_NUMBER_EXISTS', + EMAIL_EXISTS: 'EMAIL_EXISTS', +}; diff --git a/packages/server/src/services/Authentication/_utils.ts b/packages/server/src/services/Authentication/_utils.ts new file mode 100644 index 000000000..24158e3c9 --- /dev/null +++ b/packages/server/src/services/Authentication/_utils.ts @@ -0,0 +1,22 @@ +import JWT from 'jsonwebtoken'; +import { ISystemUser } from '@/interfaces'; +import config from '@/config'; + +/** + * Generates JWT token for the given user. + * @param {ISystemUser} user + * @return {string} token + */ +export const generateToken = (user: ISystemUser): string => { + const today = new Date(); + const exp = new Date(today); + exp.setDate(today.getDate() + 60); + + return JWT.sign( + { + id: user.id, // We are gonna use this in the middleware 'isAuth' + exp: exp.getTime() / 1000, + }, + config.jwtSecret + ); +}; diff --git a/packages/server/src/services/Authentication/index.ts b/packages/server/src/services/Authentication/index.ts deleted file mode 100644 index 8e5a35d77..000000000 --- a/packages/server/src/services/Authentication/index.ts +++ /dev/null @@ -1,322 +0,0 @@ -import { Service, Inject, Container } from 'typedi'; -import JWT from 'jsonwebtoken'; -import uniqid from 'uniqid'; -import { omit, cloneDeep } from 'lodash'; -import moment from 'moment'; -import { PasswordReset, Tenant } from '@/system/models'; -import { - IRegisterDTO, - ITenant, - ISystemUser, - IPasswordReset, - IAuthenticationService, -} from '@/interfaces'; -import { hashPassword } from 'utils'; -import { ServiceError, ServiceErrors } from '@/exceptions'; -import config from '@/config'; -import events from '@/subscribers/events'; -import AuthenticationMailMessages from '@/services/Authentication/AuthenticationMailMessages'; -import TenantsManager from '@/services/Tenancy/TenantsManager'; -import { EventPublisher } from '@/lib/EventPublisher/EventPublisher'; - -const ERRORS = { - INVALID_DETAILS: 'INVALID_DETAILS', - USER_INACTIVE: 'USER_INACTIVE', - EMAIL_NOT_FOUND: 'EMAIL_NOT_FOUND', - TOKEN_INVALID: 'TOKEN_INVALID', - USER_NOT_FOUND: 'USER_NOT_FOUND', - TOKEN_EXPIRED: 'TOKEN_EXPIRED', - PHONE_NUMBER_EXISTS: 'PHONE_NUMBER_EXISTS', - EMAIL_EXISTS: 'EMAIL_EXISTS', -}; -@Service() -export default class AuthenticationService implements IAuthenticationService { - @Inject('logger') - logger: any; - - @Inject() - eventPublisher: EventPublisher; - - @Inject() - mailMessages: AuthenticationMailMessages; - - @Inject('repositories') - sysRepositories: any; - - @Inject() - tenantsManager: TenantsManager; - - /** - * Signin and generates JWT token. - * @throws {ServiceError} - * @param {string} emailOrPhone - Email or phone number. - * @param {string} password - Password. - * @return {Promise<{user: IUser, token: string}>} - */ - public async signIn( - emailOrPhone: string, - password: string - ): Promise<{ - user: ISystemUser; - token: string; - tenant: ITenant; - }> { - this.logger.info('[login] Someone trying to login.', { - emailOrPhone, - password, - }); - const { systemUserRepository } = this.sysRepositories; - const loginThrottler = Container.get('rateLimiter.login'); - - // Finds the user of the given email or phone number. - const user = await systemUserRepository.findByCrediential(emailOrPhone); - - if (!user) { - // Hits the loging throttler to the given crediential. - await loginThrottler.hit(emailOrPhone); - - this.logger.info('[login] invalid data'); - throw new ServiceError(ERRORS.INVALID_DETAILS); - } - - this.logger.info('[login] check password validation.', { - emailOrPhone, - password, - }); - if (!user.verifyPassword(password)) { - // Hits the loging throttler to the given crediential. - await loginThrottler.hit(emailOrPhone); - - throw new ServiceError(ERRORS.INVALID_DETAILS); - } - if (!user.active) { - this.logger.info('[login] user inactive.', { userId: user.id }); - throw new ServiceError(ERRORS.USER_INACTIVE); - } - - this.logger.info('[login] generating JWT token.', { userId: user.id }); - const token = this.generateToken(user); - - this.logger.info('[login] updating user last login at.', { - userId: user.id, - }); - await systemUserRepository.patchLastLoginAt(user.id); - - this.logger.info('[login] Logging success.', { user, token }); - - // Triggers `onLogin` event. - await this.eventPublisher.emitAsync(events.auth.login, { - emailOrPhone, - password, - user, - }); - const tenant = await Tenant.query().findById(user.tenantId).withGraphFetched('metadata'); - - // Keep the user object immutable. - const outputUser = cloneDeep(user); - - // Remove password property from user object. - Reflect.deleteProperty(outputUser, 'password'); - - return { user: outputUser, token, tenant }; - } - - /** - * Validates email and phone number uniqiness on the storage. - * @throws {ServiceErrors} - * @param {IRegisterDTO} registerDTO - Register data object. - */ - private async validateEmailAndPhoneUniqiness(registerDTO: IRegisterDTO) { - const { systemUserRepository } = this.sysRepositories; - - const isEmailExists = await systemUserRepository.findOneByEmail( - registerDTO.email - ); - const isPhoneExists = await systemUserRepository.findOneByPhoneNumber( - registerDTO.phoneNumber - ); - const errorReasons: ServiceError[] = []; - - if (isPhoneExists) { - this.logger.info('[register] phone number exists on the storage.'); - errorReasons.push(new ServiceError(ERRORS.PHONE_NUMBER_EXISTS)); - } - if (isEmailExists) { - this.logger.info('[register] email exists on the storage.'); - errorReasons.push(new ServiceError(ERRORS.EMAIL_EXISTS)); - } - if (errorReasons.length > 0) { - throw new ServiceErrors(errorReasons); - } - } - - /** - * Registers a new tenant with user from user input. - * @throws {ServiceErrors} - * @param {IUserDTO} user - */ - public async register(registerDTO: IRegisterDTO): Promise { - this.logger.info('[register] Someone trying to register.'); - await this.validateEmailAndPhoneUniqiness(registerDTO); - - this.logger.info('[register] Creating a new tenant organization.'); - const tenant = await this.newTenantOrganization(); - - this.logger.info('[register] Trying hashing the password.'); - const hashedPassword = await hashPassword(registerDTO.password); - - const { systemUserRepository } = this.sysRepositories; - const registeredUser = await systemUserRepository.create({ - ...omit(registerDTO, 'country'), - active: true, - password: hashedPassword, - tenantId: tenant.id, - inviteAcceptedAt: moment().format('YYYY-MM-DD'), - }); - // Triggers `onRegister` event. - await this.eventPublisher.emitAsync(events.auth.register, { - registerDTO, - tenant, - user: registeredUser, - }); - return registeredUser; - } - - /** - * Generates and insert new tenant organization id. - * @async - * @return {Promise} - */ - private async newTenantOrganization(): Promise { - return this.tenantsManager.createTenant(); - } - - /** - * Validate the given email existance on the storage. - * @throws {ServiceError} - * @param {string} email - email address. - */ - private async validateEmailExistance(email: string): Promise { - const { systemUserRepository } = this.sysRepositories; - const userByEmail = await systemUserRepository.findOneByEmail(email); - - if (!userByEmail) { - this.logger.info('[send_reset_password] The given email not found.'); - throw new ServiceError(ERRORS.EMAIL_NOT_FOUND); - } - return userByEmail; - } - - /** - * Generates and retrieve password reset token for the given user email. - * @param {string} email - * @return {} - */ - public async sendResetPassword(email: string): Promise { - this.logger.info('[send_reset_password] Trying to send reset password.'); - const user = await this.validateEmailExistance(email); - - // Delete all stored tokens of reset password that associate to the give email. - this.logger.info( - '[send_reset_password] trying to delete all tokens by email.' - ); - this.deletePasswordResetToken(email); - - const token: string = uniqid(); - - this.logger.info('[send_reset_password] insert the generated token.'); - const passwordReset = await PasswordReset.query().insert({ email, token }); - - // Triggers `onSendResetPassword` event. - await this.eventPublisher.emitAsync(events.auth.sendResetPassword, { - user, - token, - }); - return passwordReset; - } - - /** - * Resets a user password from given token. - * @param {string} token - Password reset token. - * @param {string} password - New Password. - * @return {Promise} - */ - public async resetPassword(token: string, password: string): Promise { - const { systemUserRepository } = this.sysRepositories; - - // Finds the password reset token. - const tokenModel: IPasswordReset = await PasswordReset.query().findOne( - 'token', - token - ); - // In case the password reset token not found throw token invalid error.. - if (!tokenModel) { - this.logger.info('[reset_password] token invalid.'); - throw new ServiceError(ERRORS.TOKEN_INVALID); - } - // Different between tokne creation datetime and current time. - if ( - moment().diff(tokenModel.createdAt, 'seconds') > - config.resetPasswordSeconds - ) { - this.logger.info('[reset_password] token expired.'); - - // Deletes the expired token by expired token email. - await this.deletePasswordResetToken(tokenModel.email); - throw new ServiceError(ERRORS.TOKEN_EXPIRED); - } - const user = await systemUserRepository.findOneByEmail(tokenModel.email); - - if (!user) { - throw new ServiceError(ERRORS.USER_NOT_FOUND); - } - const hashedPassword = await hashPassword(password); - - this.logger.info('[reset_password] saving a new hashed password.'); - await systemUserRepository.update( - { password: hashedPassword }, - { id: user.id } - ); - - // Deletes the used token. - await this.deletePasswordResetToken(tokenModel.email); - - // Triggers `onResetPassword` event. - await this.eventPublisher.emitAsync(events.auth.resetPassword, { - user, - token, - password, - }); - this.logger.info('[reset_password] reset password success.'); - } - - /** - * Deletes the password reset token by the given email. - * @param {string} email - * @returns {Promise} - */ - private async deletePasswordResetToken(email: string) { - this.logger.info('[reset_password] trying to delete all tokens by email.'); - return PasswordReset.query().where('email', email).delete(); - } - - /** - * Generates JWT token for the given user. - * @param {ISystemUser} user - * @return {string} token - */ - generateToken(user: ISystemUser): string { - const today = new Date(); - const exp = new Date(today); - exp.setDate(today.getDate() + 60); - - this.logger.silly(`Sign JWT for userId: ${user.id}`); - return JWT.sign( - { - id: user.id, // We are gonna use this in the middleware 'isAuth' - exp: exp.getTime() / 1000, - }, - config.jwtSecret - ); - } -} diff --git a/packages/server/src/subscribers/events.ts b/packages/server/src/subscribers/events.ts index a4abf80fb..5f28dec40 100644 --- a/packages/server/src/subscribers/events.ts +++ b/packages/server/src/subscribers/events.ts @@ -4,8 +4,11 @@ export default { */ auth: { login: 'onLogin', + logining: 'onLogining', register: 'onRegister', + registering: 'onAuthRegistering', sendResetPassword: 'onSendResetPassword', + sendingResetPassword: 'onSendingResetPassword', resetPassword: 'onResetPassword', }, diff --git a/packages/server/src/system/migrations/20230405011450_drop_phone_number_column_from_users_table.js b/packages/server/src/system/migrations/20230405011450_drop_phone_number_column_from_users_table.js new file mode 100644 index 000000000..9ab142779 --- /dev/null +++ b/packages/server/src/system/migrations/20230405011450_drop_phone_number_column_from_users_table.js @@ -0,0 +1,9 @@ +exports.up = function (knex) { + return knex.schema.table('users', (table) => { + table.dropColumn('phone_number'); + }); +}; + +exports.down = function (knex) { + return knex.schema.table('users', (table) => {}); +};