From 85946d3161dc730be1b1c0ef00b41e185e86ddc5 Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Sat, 29 Mar 2025 22:29:12 +0200 Subject: [PATCH] refactor: authentication module to nestjs --- packages/server-nest/package.json | 1 + .../server-nest/src/common/config/index.ts | 4 + .../src/common/config/signup-confirmation.ts | 6 + .../src/common/config/signup-restrictions.ts | 11 ++ .../server-nest/src/modules/App/App.module.ts | 2 + .../src/modules/Auth/Auth.constants.ts | 15 ++ .../src/modules/Auth/Auth.controller.ts | 25 ++++ .../src/modules/Auth/Auth.interfaces.ts | 71 +++++++++- .../src/modules/Auth/Auth.module.ts | 34 +++++ .../src/modules/Auth/Auth.utils.ts | 10 ++ .../modules/Auth/AuthApplication.sevice.ts | 86 +++++++++++- .../src/modules/Auth/AuthService.ts | 22 +++ .../commands/AuthResetPassword.service.ts | 10 ++ .../commands/AuthSendResetPassword.service.ts | 10 ++ .../Auth/commands/AuthSignin.service.ts | 22 +++ .../Auth/commands/AuthSignup.service.ts | 128 ++++++++++++++++++ .../commands/AuthSignupConfirm.service.ts | 62 +++++++++ .../AuthSignupConfirmResend.service.ts | 5 + .../src/modules/Auth/dtos/AuthSignin.dto.ts | 11 ++ .../src/modules/Auth/dtos/AuthSignup.dto.ts | 20 +++ .../models/UncategorizedBankTransaction.ts | 34 +++-- .../Organization/Organization.module.ts | 1 - .../src/modules/System/models/SystemUser.ts | 8 +- .../TransactionsLocking.controller.ts | 27 ++-- .../CommandTransactionsLockingService.ts | 6 +- .../src/utils/cast-comma-list-envvar-Array.ts | 5 + pnpm-lock.yaml | 3 + 27 files changed, 604 insertions(+), 35 deletions(-) create mode 100644 packages/server-nest/src/common/config/signup-confirmation.ts create mode 100644 packages/server-nest/src/common/config/signup-restrictions.ts create mode 100644 packages/server-nest/src/modules/Auth/Auth.controller.ts create mode 100644 packages/server-nest/src/modules/Auth/Auth.module.ts create mode 100644 packages/server-nest/src/modules/Auth/Auth.utils.ts create mode 100644 packages/server-nest/src/modules/Auth/AuthService.ts create mode 100644 packages/server-nest/src/modules/Auth/commands/AuthResetPassword.service.ts create mode 100644 packages/server-nest/src/modules/Auth/commands/AuthSendResetPassword.service.ts create mode 100644 packages/server-nest/src/modules/Auth/commands/AuthSignin.service.ts create mode 100644 packages/server-nest/src/modules/Auth/commands/AuthSignup.service.ts create mode 100644 packages/server-nest/src/modules/Auth/commands/AuthSignupConfirm.service.ts create mode 100644 packages/server-nest/src/modules/Auth/commands/AuthSignupConfirmResend.service.ts create mode 100644 packages/server-nest/src/modules/Auth/dtos/AuthSignin.dto.ts create mode 100644 packages/server-nest/src/modules/Auth/dtos/AuthSignup.dto.ts create mode 100644 packages/server-nest/src/utils/cast-comma-list-envvar-Array.ts diff --git a/packages/server-nest/package.json b/packages/server-nest/package.json index 7e14561ac..dd3d1c59d 100644 --- a/packages/server-nest/package.json +++ b/packages/server-nest/package.json @@ -49,6 +49,7 @@ "async": "^3.2.0", "async-mutex": "^0.5.0", "axios": "^1.6.0", + "bcryptjs": "^2.4.3", "bluebird": "^3.7.2", "bull": "^4.16.3", "bullmq": "^5.25.6", diff --git a/packages/server-nest/src/common/config/index.ts b/packages/server-nest/src/common/config/index.ts index 7a4a940ce..68fb49b4c 100644 --- a/packages/server-nest/src/common/config/index.ts +++ b/packages/server-nest/src/common/config/index.ts @@ -8,6 +8,8 @@ import s3 from './s3'; import openExchange from './open-exchange'; import posthog from './posthog'; import stripePayment from './stripe-payment'; +import signupConfirmation from './signup-confirmation'; +import signupRestrictions from './signup-restrictions'; export const config = [ systemDatabase, @@ -20,4 +22,6 @@ export const config = [ openExchange, posthog, stripePayment, + signupConfirmation, + signupRestrictions, ]; diff --git a/packages/server-nest/src/common/config/signup-confirmation.ts b/packages/server-nest/src/common/config/signup-confirmation.ts new file mode 100644 index 000000000..3549f60dd --- /dev/null +++ b/packages/server-nest/src/common/config/signup-confirmation.ts @@ -0,0 +1,6 @@ +import { parseBoolean } from '@/utils/parse-boolean'; +import { registerAs } from '@nestjs/config'; + +export default registerAs('signupConfirmation', () => ({ + enabled: parseBoolean(process.env.SIGNUP_EMAIL_CONFIRMATION, false), +})); diff --git a/packages/server-nest/src/common/config/signup-restrictions.ts b/packages/server-nest/src/common/config/signup-restrictions.ts new file mode 100644 index 000000000..023daea03 --- /dev/null +++ b/packages/server-nest/src/common/config/signup-restrictions.ts @@ -0,0 +1,11 @@ +import { castCommaListEnvVarToArray } from '@/utils/cast-comma-list-envvar-Array'; +import { parseBoolean } from '@/utils/parse-boolean'; +import { registerAs } from '@nestjs/config'; + +export default registerAs('signupRestrictions', () => ({ + disabled: parseBoolean(process.env.SIGNUP_DISABLED, false), + allowedDomains: castCommaListEnvVarToArray( + process.env.SIGNUP_ALLOWED_DOMAINS, + ), + allowedEmails: castCommaListEnvVarToArray(process.env.SIGNUP_ALLOWED_EMAILS), +})); diff --git a/packages/server-nest/src/modules/App/App.module.ts b/packages/server-nest/src/modules/App/App.module.ts index 9da8084e5..9b0086111 100644 --- a/packages/server-nest/src/modules/App/App.module.ts +++ b/packages/server-nest/src/modules/App/App.module.ts @@ -79,6 +79,7 @@ import { SubscriptionModule } from '../Subscription/Subscription.module'; import { OrganizationModule } from '../Organization/Organization.module'; import { TenantDBManagerModule } from '../TenantDBManager/TenantDBManager.module'; import { PaymentServicesModule } from '../PaymentServices/PaymentServices.module'; +import { AuthModule } from '../Auth/Auth.module'; @Module({ imports: [ @@ -193,6 +194,7 @@ import { PaymentServicesModule } from '../PaymentServices/PaymentServices.module OrganizationModule, TenantDBManagerModule, PaymentServicesModule, + AuthModule, ], controllers: [AppController], providers: [ diff --git a/packages/server-nest/src/modules/Auth/Auth.constants.ts b/packages/server-nest/src/modules/Auth/Auth.constants.ts index 9e8c4061f..6cb14c9a7 100644 --- a/packages/server-nest/src/modules/Auth/Auth.constants.ts +++ b/packages/server-nest/src/modules/Auth/Auth.constants.ts @@ -2,3 +2,18 @@ export const jwtConstants = { secret: 'DO NOT USE THIS VALUE. INSTEAD, CREATE A COMPLEX SECRET AND KEEP IT SAFE OUTSIDE OF THE SOURCE CODE.', }; + +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', + SIGNUP_RESTRICTED_NOT_ALLOWED: 'SIGNUP_RESTRICTED_NOT_ALLOWED', + SIGNUP_RESTRICTED: 'SIGNUP_RESTRICTED', + SIGNUP_CONFIRM_TOKEN_INVALID: 'SIGNUP_CONFIRM_TOKEN_INVALID', + USER_ALREADY_VERIFIED: 'USER_ALREADY_VERIFIED', +}; diff --git a/packages/server-nest/src/modules/Auth/Auth.controller.ts b/packages/server-nest/src/modules/Auth/Auth.controller.ts new file mode 100644 index 000000000..1e8af9b07 --- /dev/null +++ b/packages/server-nest/src/modules/Auth/Auth.controller.ts @@ -0,0 +1,25 @@ +import { Body, Controller, Post, Request } from '@nestjs/common'; +import { PublicRoute } from './Jwt.guard'; +import { AuthenticationApplication } from './AuthApplication.sevice'; +import { AuthSignupDto } from './dtos/AuthSignup.dto'; +import { AuthSigninDto } from './dtos/AuthSignin.dto'; + +@Controller('/auth') +@PublicRoute() +export class AuthController { + constructor(private readonly authApp: AuthenticationApplication) {} + @Post('/signin') + signin(@Request() req: Request, @Body() signinDto: AuthSigninDto) { + return this.authApp.signIn(signinDto); + } + + @Post('/signup') + signup(@Request() req: Request, @Body() signupDto: AuthSignupDto) { + this.authApp.signUp(signupDto); + } + + @Post('/signup/confirm') + signupConfirm(@Body('email') email: string, @Body('token') token: string) { + return this.authApp.signUpConfirm(email, token); + } +} diff --git a/packages/server-nest/src/modules/Auth/Auth.interfaces.ts b/packages/server-nest/src/modules/Auth/Auth.interfaces.ts index a483e5e0e..65cc917d5 100644 --- a/packages/server-nest/src/modules/Auth/Auth.interfaces.ts +++ b/packages/server-nest/src/modules/Auth/Auth.interfaces.ts @@ -1,5 +1,72 @@ +import { ModelObject } from 'objection'; +import { SystemUser } from '../System/models/SystemUser'; +import { TenantModel } from '../System/models/TenantModel'; +import { AuthSignupDto } from './dtos/AuthSignup.dto'; + export interface IAuthSignedInEventPayload {} - export interface IAuthSigningInEventPayload {} - export interface IAuthSignInPOJO {} + +export interface IAuthSigningInEventPayload { + email: string; + password: string; + user: ModelObject; +} + +export interface IAuthSignedInEventPayload { + email: string; + password: string; + user: ModelObject; +} + +export interface IAuthSigningUpEventPayload { + signupDTO: AuthSignupDto; +} + +export interface IAuthSignedUpEventPayload { + signupDTO: AuthSignupDto; + tenant: TenantModel; + user: SystemUser; +} + +export interface IAuthSignInPOJO { + user: ModelObject; + token: string; + tenant: ModelObject; +} + +export interface IAuthResetedPasswordEventPayload { + user: SystemUser; + token: string; + password: string; +} + +export interface IAuthSendingResetPassword { + user: SystemUser; + token: string; +} + +export interface IAuthSendedResetPassword { + user: SystemUser; + token: string; +} + +export interface IAuthGetMetaPOJO { + signupDisabled: boolean; + oneClickDemo: { + enable: boolean; + demoUrl: string; + }; +} + +export interface IAuthSignUpVerifingEventPayload { + email: string; + verifyToken: string; + userId: number; +} + +export interface IAuthSignUpVerifiedEventPayload { + email: string; + verifyToken: string; + userId: number; +} diff --git a/packages/server-nest/src/modules/Auth/Auth.module.ts b/packages/server-nest/src/modules/Auth/Auth.module.ts new file mode 100644 index 000000000..f500285ea --- /dev/null +++ b/packages/server-nest/src/modules/Auth/Auth.module.ts @@ -0,0 +1,34 @@ +import { Module } from '@nestjs/common'; +import { AuthService } from './AuthService'; +import { AuthController } from './Auth.controller'; +import { JwtModule } from '@nestjs/jwt'; +import { JwtStrategy } from './Jwt.strategy'; +import { AuthenticationApplication } from './AuthApplication.sevice'; +import { AuthSendResetPasswordService } from './commands/AuthSendResetPassword.service'; +import { AuthResetPasswordService } from './commands/AuthResetPassword.service'; +import { AuthSignupConfirmResendService } from './commands/AuthSignupConfirmResend.service'; +import { AuthSignupConfirmService } from './commands/AuthSignupConfirm.service'; +import { AuthSignupService } from './commands/AuthSignup.service'; +import { AuthSigninService } from './commands/AuthSignin.service'; + +@Module({ + controllers: [AuthController], + imports: [ + JwtModule.register({ + secret: 'asdfasdfasdf', + signOptions: { expiresIn: '60s' }, + }), + ], + providers: [ + AuthService, + JwtStrategy, + AuthenticationApplication, + AuthSendResetPasswordService, + AuthResetPasswordService, + AuthSignupConfirmResendService, + AuthSignupConfirmService, + AuthSignupService, + AuthSigninService, + ], +}) +export class AuthModule {} diff --git a/packages/server-nest/src/modules/Auth/Auth.utils.ts b/packages/server-nest/src/modules/Auth/Auth.utils.ts new file mode 100644 index 000000000..38ef94a23 --- /dev/null +++ b/packages/server-nest/src/modules/Auth/Auth.utils.ts @@ -0,0 +1,10 @@ +import * as bcrypt from 'bcrypt'; + +export const hashPassword = (password: string): Promise => + new Promise((resolve) => { + bcrypt.genSalt(10, (error, salt) => { + bcrypt.hash(password, salt, (err, hash: string) => { + resolve(hash); + }); + }); + }); diff --git a/packages/server-nest/src/modules/Auth/AuthApplication.sevice.ts b/packages/server-nest/src/modules/Auth/AuthApplication.sevice.ts index 38371c565..31830f295 100644 --- a/packages/server-nest/src/modules/Auth/AuthApplication.sevice.ts +++ b/packages/server-nest/src/modules/Auth/AuthApplication.sevice.ts @@ -1,4 +1,86 @@ +import { Injectable } from '@nestjs/common'; +import { AuthSigninService } from './commands/AuthSignin.service'; +import { AuthSignupService } from './commands/AuthSignup.service'; +import { AuthSignupConfirmService } from './commands/AuthSignupConfirm.service'; +import { AuthSignupConfirmResendService } from './commands/AuthSignupConfirmResend.service'; +import { AuthSigninDto } from './dtos/AuthSignin.dto'; +import { AuthSignupDto } from './dtos/AuthSignup.dto'; +import { AuthSendResetPasswordService } from './commands/AuthSendResetPassword.service'; +import { AuthResetPasswordService } from './commands/AuthResetPassword.service'; -export class AuthApplication { - +@Injectable() +export class AuthenticationApplication { + constructor( + private readonly authSigninService: AuthSigninService, + private readonly authSignupService: AuthSignupService, + private readonly authSignupConfirmService: AuthSignupConfirmService, + private readonly authSignUpConfirmResendService: AuthSignupConfirmResendService, + private readonly authResetPasswordService: AuthResetPasswordService, + private readonly authSendResetPasswordService: AuthSendResetPasswordService, + // private readonly authGetMeta: GetAuthMeta, + ) {} + + /** + * Signin and generates JWT token. + * @throws {ServiceError} + * @param {string} email - Email address. + * @param {string} password - Password. + */ + public async signIn(signinDto: AuthSigninDto) { + return this.authSigninService.signIn(signinDto); + } + + /** + * Signup a new user. + * @param {IRegisterDTO} signupDTO + */ + public async signUp(signupDto: AuthSignupDto) { + return this.authSignupService.signUp(signupDto); + } + + /** + * Verifying the provided user's email after signin-up. + * @param {string} email - User email. + * @param {string} token - Verification token. + * @returns {Promise} + */ + public async signUpConfirm(email: string, token: string) { + return this.authSignupConfirmService.signupConfirm(email, token); + } + + /** + * Re-sends the confirmation email of the given system user. + * @param {number} userId - System user id. + * @returns {Promise} + */ + public async signUpConfirmResend(userId: number) { + return this.authSignUpConfirmResendService.signUpConfirmResend(userId); + } + + /** + * Generates and retrieve password reset token for the given user email. + * @param {string} email + * @return {} + */ + public async sendResetPassword(email: string) { + return this.authSendResetPasswordService.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) { + return this.authResetPasswordService.resetPassword(token, password); + } + + /** + * Retrieves the authentication meta for SPA. + * @returns {Promise} + */ + public async getAuthMeta() { + // return this.authGetMeta.getAuthMeta(); + } } diff --git a/packages/server-nest/src/modules/Auth/AuthService.ts b/packages/server-nest/src/modules/Auth/AuthService.ts new file mode 100644 index 000000000..56b6e8d17 --- /dev/null +++ b/packages/server-nest/src/modules/Auth/AuthService.ts @@ -0,0 +1,22 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { SystemUser } from '@/modules/System/models/SystemUser'; + +@Injectable() +export class AuthService { + constructor( + @Inject(SystemUser.name) + private readonly systemUserModel: typeof SystemUser, + ) {} + + async validateUser(username: string, pass: string): Promise { + const user = await this.systemUserModel + .query() + .findOne({ email: username }); + + if (user && user.password === pass) { + const { password, ...result } = user; + return result; + } + return null; + } +} diff --git a/packages/server-nest/src/modules/Auth/commands/AuthResetPassword.service.ts b/packages/server-nest/src/modules/Auth/commands/AuthResetPassword.service.ts new file mode 100644 index 000000000..a2f0e23cd --- /dev/null +++ b/packages/server-nest/src/modules/Auth/commands/AuthResetPassword.service.ts @@ -0,0 +1,10 @@ +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class AuthResetPasswordService { + resetPassword(token: string, password: string): Promise<{ message: string }> { + return new Promise((resolve) => { + resolve({ message: 'Reset password link sent to your email' }); + }); + } +} diff --git a/packages/server-nest/src/modules/Auth/commands/AuthSendResetPassword.service.ts b/packages/server-nest/src/modules/Auth/commands/AuthSendResetPassword.service.ts new file mode 100644 index 000000000..ae657d1bc --- /dev/null +++ b/packages/server-nest/src/modules/Auth/commands/AuthSendResetPassword.service.ts @@ -0,0 +1,10 @@ +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class AuthSendResetPasswordService { + sendResetPassword(email: string): Promise<{ message: string }> { + return new Promise((resolve) => { + resolve({ message: 'Reset password link sent to your email' }); + }); + } +} diff --git a/packages/server-nest/src/modules/Auth/commands/AuthSignin.service.ts b/packages/server-nest/src/modules/Auth/commands/AuthSignin.service.ts new file mode 100644 index 000000000..81e360cb4 --- /dev/null +++ b/packages/server-nest/src/modules/Auth/commands/AuthSignin.service.ts @@ -0,0 +1,22 @@ +import { SystemUser } from '@/modules/System/models/SystemUser'; +import { Inject, Injectable } from '@nestjs/common'; +import { JwtService } from '@nestjs/jwt'; +import { AuthSigninDto } from '../dtos/AuthSignin.dto'; + +@Injectable() +export class AuthSigninService { + constructor( + private readonly jwtService: JwtService, + + @Inject(SystemUser.name) + private readonly systemUserModel: typeof SystemUser, + ) {} + + private async validate() {} + + private getUserByEmail(email: string) { + return this.systemUserModel.query().findOne({ email }); + } + + public async signIn(signinDto: AuthSigninDto) {} +} diff --git a/packages/server-nest/src/modules/Auth/commands/AuthSignup.service.ts b/packages/server-nest/src/modules/Auth/commands/AuthSignup.service.ts new file mode 100644 index 000000000..4a8a6a5e1 --- /dev/null +++ b/packages/server-nest/src/modules/Auth/commands/AuthSignup.service.ts @@ -0,0 +1,128 @@ +import crypto from 'crypto'; +import { events } from '@/common/events/events'; +import { ServiceError } from '@/modules/Items/ServiceError'; +import { SystemUser } from '@/modules/System/models/SystemUser'; +import { TenantsManagerService } from '@/modules/TenantDBManager/TenantsManager'; +import { Inject, Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { EventEmitter2 } from '@nestjs/event-emitter'; +import { isEmpty } from 'class-validator'; +import { AuthSignupDto } from '../dtos/AuthSignup.dto'; +import { + IAuthSignedUpEventPayload, + IAuthSigningUpEventPayload, +} from '../Auth.interfaces'; +import { defaultTo } from 'ramda'; +import { ERRORS } from '../Auth.constants'; +import { hashPassword } from '../Auth.utils'; + +@Injectable() +export class AuthSignupService { + /** + * @param {ConfigService} configService - Config service + * @param {EventEmitter2} eventEmitter - Event emitter + * @param {TenantsManagerService} tenantsManager - Tenants manager + * @param {typeof SystemUser} systemUserModel - System user model + */ + constructor( + private readonly configService: ConfigService, + private readonly eventEmitter: EventEmitter2, + private readonly tenantsManager: TenantsManagerService, + + @Inject(SystemUser.name) + private readonly systemUserModel: typeof SystemUser, + ) {} + + /** + * Registers a new tenant with user from user input. + * @param {AuthSignupDto} signupDTO + */ + public async signUp(signupDTO: AuthSignupDto) { + // Validates the signup disable restrictions. + await this.validateSignupRestrictions(signupDTO.email); + + // Validates the given email uniqiness. + await this.validateEmailUniqiness(signupDTO.email); + + const hashedPassword = await hashPassword(signupDTO.password); + const signupConfirmation = this.configService.get('signupConfirmation'); + + const verifyTokenCrypto = crypto.randomBytes(64).toString('hex'); + const verifiedEnabed = defaultTo(signupConfirmation.enabled, false); + const verifyToken = verifiedEnabed ? verifyTokenCrypto : ''; + const verified = !verifiedEnabed; + + const inviteAcceptedAt = moment().format('YYYY-MM-DD'); + + // Triggers signin up event. + await this.eventEmitter.emitAsync(events.auth.signingUp, { + signupDTO, + } as IAuthSigningUpEventPayload); + + const tenant = await this.tenantsManager.createTenant(); + const user = await this.systemUserModel.query().insert({ + ...signupDTO, + verifyToken, + verified, + active: true, + password: hashedPassword, + tenantId: tenant.id, + inviteAcceptedAt, + }); + // Triggers signed up event. + await this.eventEmitter.emitAsync(events.auth.signUp, { + signupDTO, + tenant, + user, + } as IAuthSignedUpEventPayload); + + return { + userId: user.id, + tenantId: user.tenantId, + }; + } + + /** + * Validates email uniqiness on the storage. + * @param {string} email - Email address + */ + private async validateEmailUniqiness(email: string) { + const isEmailExists = await this.systemUserModel.query().findOne({ email }); + + if (isEmailExists) { + throw new ServiceError(ERRORS.EMAIL_EXISTS); + } + } + + /** + * Validate sign-up disable restrictions. + * @param {string} email - Signup email address + */ + private async validateSignupRestrictions(email: string) { + const signupRestrictions = this.configService.get('signupRestrictions'); + + // Can't continue if the signup is not disabled. + if (!signupRestrictions.disabled) return; + + // Validate the allowed email addresses and domains. + if ( + !isEmpty(signupRestrictions.allowedEmails) || + !isEmpty(signupRestrictions.allowedDomains) + ) { + const emailDomain = email.split('@').pop(); + const isAllowedEmail = + signupRestrictions.allowedEmails.indexOf(email) !== -1; + + const isAllowedDomain = signupRestrictions.allowedDomains.some( + (domain) => emailDomain === domain, + ); + + if (!isAllowedEmail && !isAllowedDomain) { + throw new ServiceError(ERRORS.SIGNUP_RESTRICTED_NOT_ALLOWED); + } + // Throw error if the signup is disabled with no exceptions. + } else { + throw new ServiceError(ERRORS.SIGNUP_RESTRICTED); + } + } +} diff --git a/packages/server-nest/src/modules/Auth/commands/AuthSignupConfirm.service.ts b/packages/server-nest/src/modules/Auth/commands/AuthSignupConfirm.service.ts new file mode 100644 index 000000000..4b926285c --- /dev/null +++ b/packages/server-nest/src/modules/Auth/commands/AuthSignupConfirm.service.ts @@ -0,0 +1,62 @@ +import { ServiceError } from '@/modules/Items/ServiceError'; +import { SystemUser } from '@/modules/System/models/SystemUser'; +import { Inject, Injectable } from '@nestjs/common'; +import { ERRORS } from '../Auth.constants'; +import { EventEmitter2 } from '@nestjs/event-emitter'; +import { + IAuthSignUpVerifiedEventPayload, + IAuthSignUpVerifingEventPayload, +} from '../Auth.interfaces'; +import { events } from '@/common/events/events'; + +@Injectable() +export class AuthSignupConfirmService { + constructor( + private readonly eventPublisher: EventEmitter2, + + @Inject(SystemUser.name) + private readonly systemUserModel: typeof SystemUser, + ) {} + + /** + * Verifies the provided user's email after signing-up. + * @throws {ServiceErrors} + * @param {IRegisterDTO} signupDTO + * @returns {Promise} + */ + public async signupConfirm( + email: string, + verifyToken: string, + ): Promise { + const foundUser = await this.systemUserModel + .query() + .findOne({ email, verifyToken }); + + if (!foundUser) { + throw new ServiceError(ERRORS.SIGNUP_CONFIRM_TOKEN_INVALID); + } + const userId = foundUser.id; + + // Triggers `signUpConfirming` event. + await this.eventPublisher.emitAsync(events.auth.signUpConfirming, { + email, + verifyToken, + userId, + } as IAuthSignUpVerifingEventPayload); + + const updatedUser = await this.systemUserModel + .query() + .patchAndFetchById(foundUser.id, { + verified: true, + verifyToken: '', + }); + // Triggers `signUpConfirmed` event. + await this.eventPublisher.emitAsync(events.auth.signUpConfirmed, { + email, + verifyToken, + userId, + } as IAuthSignUpVerifiedEventPayload); + + return updatedUser as SystemUser; + } +} diff --git a/packages/server-nest/src/modules/Auth/commands/AuthSignupConfirmResend.service.ts b/packages/server-nest/src/modules/Auth/commands/AuthSignupConfirmResend.service.ts new file mode 100644 index 000000000..40444be5c --- /dev/null +++ b/packages/server-nest/src/modules/Auth/commands/AuthSignupConfirmResend.service.ts @@ -0,0 +1,5 @@ +export class AuthSignupConfirmResendService { + signUpConfirmResend(userId: number) { + return; + } +} diff --git a/packages/server-nest/src/modules/Auth/dtos/AuthSignin.dto.ts b/packages/server-nest/src/modules/Auth/dtos/AuthSignin.dto.ts new file mode 100644 index 000000000..9d9922d7a --- /dev/null +++ b/packages/server-nest/src/modules/Auth/dtos/AuthSignin.dto.ts @@ -0,0 +1,11 @@ +import { IsNotEmpty, IsString } from 'class-validator'; + +export class AuthSigninDto { + @IsNotEmpty() + @IsString() + password: string; + + @IsNotEmpty() + @IsString() + email: string; +} diff --git a/packages/server-nest/src/modules/Auth/dtos/AuthSignup.dto.ts b/packages/server-nest/src/modules/Auth/dtos/AuthSignup.dto.ts new file mode 100644 index 000000000..1830fca13 --- /dev/null +++ b/packages/server-nest/src/modules/Auth/dtos/AuthSignup.dto.ts @@ -0,0 +1,20 @@ +import { IsEmail, IsNotEmpty, IsString } from 'class-validator'; + +export class AuthSignupDto { + @IsNotEmpty() + @IsString() + firstName: string; + + @IsNotEmpty() + @IsString() + lastName: string; + + @IsNotEmpty() + @IsString() + @IsEmail() + email: string; + + @IsNotEmpty() + @IsString() + password: string; +} diff --git a/packages/server-nest/src/modules/BankingTransactions/models/UncategorizedBankTransaction.ts b/packages/server-nest/src/modules/BankingTransactions/models/UncategorizedBankTransaction.ts index eced57060..5d0d433b9 100644 --- a/packages/server-nest/src/modules/BankingTransactions/models/UncategorizedBankTransaction.ts +++ b/packages/server-nest/src/modules/BankingTransactions/models/UncategorizedBankTransaction.ts @@ -1,26 +1,22 @@ /* eslint-disable global-require */ import * as moment from 'moment'; import { Model } from 'objection'; -// import TenantModel from 'models/TenantModel'; -// import ModelSettings from './ModelSetting'; -// import UncategorizedCashflowTransactionMeta from './UncategorizedCashflowTransaction.meta'; import { BaseModel } from '@/models/Model'; export class UncategorizedBankTransaction extends BaseModel { - amount!: number; - date!: Date | string; - categorized!: boolean; - accountId!: number; - referenceNo!: string; - payee!: string; - description!: string; - plaidTransactionId!: string; - recognizedTransactionId!: number; - excludedAt: Date; - pending: boolean; - - categorizeRefId!: number; - categorizeRefType!: string; + readonly amount!: number; + readonly date!: Date | string; + readonly categorized!: boolean; + readonly accountId!: number; + readonly referenceNo!: string; + readonly payee!: string; + readonly description!: string; + readonly plaidTransactionId!: string; + readonly recognizedTransactionId!: number; + readonly excludedAt: Date; + readonly pending: boolean; + readonly categorizeRefId!: number; + readonly categorizeRefType!: string; /** * Table name. @@ -199,7 +195,9 @@ export class UncategorizedBankTransaction extends BaseModel { const { RecognizedBankTransaction, } = require('../../BankingTranasctionsRegonize/models/RecognizedBankTransaction'); - const { MatchedBankTransaction } = require('../../BankingMatching/models/MatchedBankTransaction'); + const { + MatchedBankTransaction, + } = require('../../BankingMatching/models/MatchedBankTransaction'); return { /** diff --git a/packages/server-nest/src/modules/Organization/Organization.module.ts b/packages/server-nest/src/modules/Organization/Organization.module.ts index 35bd55362..86c19c524 100644 --- a/packages/server-nest/src/modules/Organization/Organization.module.ts +++ b/packages/server-nest/src/modules/Organization/Organization.module.ts @@ -9,7 +9,6 @@ import { OrganizationBuildProcessor } from './processors/OrganizationBuild.proce import { CommandOrganizationValidators } from './commands/CommandOrganizationValidators.service'; import { TenancyContext } from '../Tenancy/TenancyContext.service'; import { TenantDBManagerModule } from '../TenantDBManager/TenantDBManager.module'; -import { TenantsManagerService } from '../TenantDBManager/TenantsManager'; import { OrganizationBaseCurrencyLocking } from './Organization/OrganizationBaseCurrencyLocking.service'; @Module({ diff --git a/packages/server-nest/src/modules/System/models/SystemUser.ts b/packages/server-nest/src/modules/System/models/SystemUser.ts index b382b85e4..d213544bd 100644 --- a/packages/server-nest/src/modules/System/models/SystemUser.ts +++ b/packages/server-nest/src/modules/System/models/SystemUser.ts @@ -3,10 +3,14 @@ import { BaseModel } from '@/models/Model'; export class SystemUser extends BaseModel { public readonly firstName: string; public readonly lastName: string; - public readonly active: boolean; - public readonly password: string; public readonly email: string; + public readonly password: string; + + public readonly active: boolean; public readonly tenantId: number; + public readonly verifyToken: string; + public readonly verified: boolean; + public readonly inviteAcceptedAt!: string; static get tableName() { return 'users'; diff --git a/packages/server-nest/src/modules/TransactionsLocking/TransactionsLocking.controller.ts b/packages/server-nest/src/modules/TransactionsLocking/TransactionsLocking.controller.ts index 9e69e11e2..ca358a62e 100644 --- a/packages/server-nest/src/modules/TransactionsLocking/TransactionsLocking.controller.ts +++ b/packages/server-nest/src/modules/TransactionsLocking/TransactionsLocking.controller.ts @@ -1,14 +1,15 @@ -import { Controller, Put, Get, Body, Param, UseGuards } from '@nestjs/common'; +import { Controller, Put, Get, Body, Param } from '@nestjs/common'; import { TransactionsLockingService } from './commands/CommandTransactionsLockingService'; import { TransactionsLockingGroup } from './types/TransactionsLocking.types'; -import { ITransactionsLockingAllDTO } from './types/TransactionsLocking.types'; -import { ICancelTransactionsLockingDTO } from './types/TransactionsLocking.types'; import { ITransactionLockingPartiallyDTO } from './types/TransactionsLocking.types'; import { QueryTransactionsLocking } from './queries/QueryTransactionsLocking'; import { PublicRoute } from '../Auth/Jwt.guard'; import { ApiOperation } from '@nestjs/swagger'; import { ApiTags } from '@nestjs/swagger'; -import { CancelTransactionsLockingDto, TransactionsLockingDto, UnlockTransactionsLockingDto } from './dtos/TransactionsLocking.dto'; +import { + CancelTransactionsLockingDto, + TransactionsLockingDto, +} from './dtos/TransactionsLocking.dto'; @Controller('transactions-locking') @ApiTags('Transactions Locking') @@ -20,7 +21,9 @@ export class TransactionsLockingController { ) {} @Put('lock') - @ApiOperation({ summary: 'Lock all transactions for a module or all modules' }) + @ApiOperation({ + summary: 'Lock all transactions for a module or all modules', + }) async commandTransactionsLocking( @Body('module') module: TransactionsLockingGroup, @Body() transactionLockingDTO: TransactionsLockingDto, @@ -37,7 +40,9 @@ export class TransactionsLockingController { } @Put('cancel-lock') - @ApiOperation({ summary: 'Cancel all transactions locking for a module or all modules' }) + @ApiOperation({ + summary: 'Cancel all transactions locking for a module or all modules', + }) async cancelTransactionLocking( @Body('module') module: TransactionsLockingGroup, @Body() cancelLockingDTO: CancelTransactionsLockingDto, @@ -53,7 +58,10 @@ export class TransactionsLockingController { } @Put('unlock-partial') - @ApiOperation({ summary: 'Partial unlock all transactions locking for a module or all modules' }) + @ApiOperation({ + summary: + 'Partial unlock all transactions locking for a module or all modules', + }) async unlockTransactionsLockingBetweenPeriod( @Body('module') module: TransactionsLockingGroup, @Body() unlockDTO: ITransactionLockingPartiallyDTO, @@ -70,7 +78,10 @@ export class TransactionsLockingController { } @Put('cancel-unlock-partial') - @ApiOperation({ summary: 'Cancel partial unlocking all transactions locking for a module or all modules' }) + @ApiOperation({ + summary: + 'Cancel partial unlocking all transactions locking for a module or all modules', + }) async cancelPartialUnlocking( @Body('module') module: TransactionsLockingGroup, ) { diff --git a/packages/server-nest/src/modules/TransactionsLocking/commands/CommandTransactionsLockingService.ts b/packages/server-nest/src/modules/TransactionsLocking/commands/CommandTransactionsLockingService.ts index 9321089a7..d5d43ef67 100644 --- a/packages/server-nest/src/modules/TransactionsLocking/commands/CommandTransactionsLockingService.ts +++ b/packages/server-nest/src/modules/TransactionsLocking/commands/CommandTransactionsLockingService.ts @@ -1,6 +1,5 @@ import { omit } from 'lodash'; import { - ICancelTransactionsLockingDTO, ITransactionLockingPartiallyDTO, ITransactionMeta, ITransactionsLockingAllDTO, @@ -15,7 +14,10 @@ import { Injectable } from '@nestjs/common'; import { EventEmitter2 } from '@nestjs/event-emitter'; import { events } from '@/common/events/events'; import { ServiceError } from '@/modules/Items/ServiceError'; -import { CancelTransactionsLockingDto, TransactionsLockingDto } from '../dtos/TransactionsLocking.dto'; +import { + CancelTransactionsLockingDto, + TransactionsLockingDto, +} from '../dtos/TransactionsLocking.dto'; const Modules = ['all', 'sales', 'purchases', 'financial']; diff --git a/packages/server-nest/src/utils/cast-comma-list-envvar-Array.ts b/packages/server-nest/src/utils/cast-comma-list-envvar-Array.ts new file mode 100644 index 000000000..42ce4c175 --- /dev/null +++ b/packages/server-nest/src/utils/cast-comma-list-envvar-Array.ts @@ -0,0 +1,5 @@ +import { trim } from 'lodash'; + +export const castCommaListEnvVarToArray = (envVar: string): Array => { + return envVar ? envVar?.split(',')?.map(trim) : []; +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f68b9ffb9..676423c4f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -571,6 +571,9 @@ importers: axios: specifier: ^1.6.0 version: 1.7.7 + bcryptjs: + specifier: ^2.4.3 + version: 2.4.3 bluebird: specifier: ^3.7.2 version: 3.7.2