diff --git a/packages/server-nest/src/modules/Auth/Auth.interfaces.ts b/packages/server-nest/src/modules/Auth/Auth.interfaces.ts new file mode 100644 index 000000000..a483e5e0e --- /dev/null +++ b/packages/server-nest/src/modules/Auth/Auth.interfaces.ts @@ -0,0 +1,5 @@ +export interface IAuthSignedInEventPayload {} + +export interface IAuthSigningInEventPayload {} + +export interface IAuthSignInPOJO {} diff --git a/packages/server-nest/src/modules/Auth/AuthApplication.sevice.ts b/packages/server-nest/src/modules/Auth/AuthApplication.sevice.ts index e69de29bb..d93cf0dfc 100644 --- a/packages/server-nest/src/modules/Auth/AuthApplication.sevice.ts +++ b/packages/server-nest/src/modules/Auth/AuthApplication.sevice.ts @@ -0,0 +1,29 @@ +import { AuthForgetPasswordService } from './AuthForgetPassword.service'; +import { AuthSendResetPasswordService } from './AuthResetPassword.service'; +import { AuthSigninService } from './AuthSignin.service'; +import { AuthSignupService } from './AuthSignup.service'; + +export class AuthApplication { + constructor( + private readonly authSigninService: AuthSigninService, + private readonly authSignupService: AuthSignupService, + private readonly authResetPasswordService: AuthSendResetPasswordService, + private readonly authForgetPasswordService: AuthForgetPasswordService, + ) {} + + async signin(email: string, password: string) { + return this.authSigninService.signIn(email, password); + } + + async signup(data: any) { + return this.authSignupService.signup(data); + } + + async resetPassword(data: any) { + return this.authResetPasswordService.resetPassword(data); + } + + async forgetPassword(data: any) { + return this.authForgetPasswordService.execute(data); + } +} diff --git a/packages/server-nest/src/modules/Auth/AuthForgetPassword.service.ts b/packages/server-nest/src/modules/Auth/AuthForgetPassword.service.ts index e69de29bb..eb21b7992 100644 --- a/packages/server-nest/src/modules/Auth/AuthForgetPassword.service.ts +++ b/packages/server-nest/src/modules/Auth/AuthForgetPassword.service.ts @@ -0,0 +1 @@ +export class AuthForgetPasswordService {} diff --git a/packages/server-nest/src/modules/Auth/AuthResetPassword.service.ts b/packages/server-nest/src/modules/Auth/AuthResetPassword.service.ts index e69de29bb..02d38d867 100644 --- a/packages/server-nest/src/modules/Auth/AuthResetPassword.service.ts +++ b/packages/server-nest/src/modules/Auth/AuthResetPassword.service.ts @@ -0,0 +1,132 @@ +import { Injectable, Inject } from '@nestjs/common'; +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'; + +@Injectable() +export class AuthSendResetPasswordService { + constructor( + private readonly eventPublisher: EventPublisher, + @Inject('SystemUserRepository') private readonly systemUserRepository: any, + + @Inject('PasswordResetModel') + private readonly passwordResetModel: typeof PasswordReset, + ) {} + + /** + * 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 this.passwordResetModel + .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 { + // Finds the password reset token. + const tokenModel: IPasswordReset = await this.passwordResetModel + .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 token 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 this.systemUserRepository.findOneByEmail( + tokenModel.email, + ); + + if (!user) { + throw new ServiceError(ERRORS.USER_NOT_FOUND); + } + const hashedPassword = await hashPassword(password); + + await this.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 this.passwordResetModel.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 userByEmail = await this.systemUserRepository.findOneByEmail(email); + + if (!userByEmail) { + throw new ServiceError(ERRORS.EMAIL_NOT_FOUND); + } + return userByEmail; + } +} diff --git a/packages/server-nest/src/modules/Auth/AuthSignin.service.ts b/packages/server-nest/src/modules/Auth/AuthSignin.service.ts index 139597f9c..c50ff9054 100644 --- a/packages/server-nest/src/modules/Auth/AuthSignin.service.ts +++ b/packages/server-nest/src/modules/Auth/AuthSignin.service.ts @@ -1,2 +1,88 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { cloneDeep } from 'lodash'; +import { SystemUser } from '../System/models/SystemUser'; +import { TenantModel } from '../System/models/TenantModel'; +import { EventEmitter2 } from '@nestjs/event-emitter'; +import { events } from '@/common/events/events'; +import { ServiceError } from '../Items/ServiceError'; +import { ERRORS } from '../Items/Items.constants'; +import { + IAuthSignedInEventPayload, + IAuthSigningInEventPayload, + IAuthSignInPOJO, +} from './Auth.interfaces'; +@Injectable() +export class AuthSigninService { + constructor( + private readonly eventEmitter: EventEmitter2, + @Inject(SystemUser.name) + private readonly systemUserModel: typeof SystemUser, + + @Inject(TenantModel.name) + private readonly tenantModel: typeof TenantModel, + ) {} + + /** + * Validates the given email and password. + * @param {ISystemUser} user + */ + public async validateSignIn(user: SystemUser) { + // Validate if the given user is inactive. + if (!user.active) { + throw new ServiceError(ERRORS.USER_INACTIVE); + } + } + + /** + * sign-in and generates JWT token. + * @param {string} email - Email address. + * @param {string} password - Password. + * @return {Promise<{user: IUser, token: string}>} + */ + public async signIn( + email: string, + password: string, + ): Promise { + // Finds the user of the given email address. + const user = await SystemUser.query() + .findOne('email', email) + .modify('inviteAccepted'); + + // Validate the given email and password. + await this.validateSignIn(user); + + // Triggers on signing-in event. + await this.eventEmitter.emitAsync(events.auth.signingIn, { + email, + password, + user, + } as IAuthSigningInEventPayload); + + const token = generateToken(user); + + // Update the last login at of the user. + // await systemUserRepository.patchLastLoginAt(user.id); + + // Triggers `onSignIn` event. + await this.eventEmitter.emitAsync(events.auth.signIn, { + email, + password, + user, + } as IAuthSignedInEventPayload); + + const tenant = await this.tenantModel + .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-nest/src/modules/Auth/AuthSignup.service.ts b/packages/server-nest/src/modules/Auth/AuthSignup.service.ts index e69de29bb..873b010c1 100644 --- a/packages/server-nest/src/modules/Auth/AuthSignup.service.ts +++ b/packages/server-nest/src/modules/Auth/AuthSignup.service.ts @@ -0,0 +1 @@ +export class AuthSignupService {}