refactor: authentication module to nestjs

This commit is contained in:
Ahmed Bouhuolia
2025-03-29 22:29:12 +02:00
parent 173610d0fa
commit 85946d3161
27 changed files with 604 additions and 35 deletions

View File

@@ -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: [

View File

@@ -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',
};

View File

@@ -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);
}
}

View File

@@ -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<SystemUser>;
}
export interface IAuthSignedInEventPayload {
email: string;
password: string;
user: ModelObject<SystemUser>;
}
export interface IAuthSigningUpEventPayload {
signupDTO: AuthSignupDto;
}
export interface IAuthSignedUpEventPayload {
signupDTO: AuthSignupDto;
tenant: TenantModel;
user: SystemUser;
}
export interface IAuthSignInPOJO {
user: ModelObject<SystemUser>;
token: string;
tenant: ModelObject<TenantModel>;
}
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;
}

View File

@@ -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 {}

View File

@@ -0,0 +1,10 @@
import * as bcrypt from 'bcrypt';
export const hashPassword = (password: string): Promise<string> =>
new Promise((resolve) => {
bcrypt.genSalt(10, (error, salt) => {
bcrypt.hash(password, salt, (err, hash: string) => {
resolve(hash);
});
});
});

View File

@@ -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<SystemUser>}
*/
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<void>}
*/
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 {<Promise<IPasswordReset>}
*/
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<void>}
*/
public async resetPassword(token: string, password: string) {
return this.authResetPasswordService.resetPassword(token, password);
}
/**
* Retrieves the authentication meta for SPA.
* @returns {Promise<IAuthGetMetaPOJO>}
*/
public async getAuthMeta() {
// return this.authGetMeta.getAuthMeta();
}
}

View File

@@ -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<any> {
const user = await this.systemUserModel
.query()
.findOne({ email: username });
if (user && user.password === pass) {
const { password, ...result } = user;
return result;
}
return null;
}
}

View File

@@ -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' });
});
}
}

View File

@@ -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' });
});
}
}

View File

@@ -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) {}
}

View File

@@ -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);
}
}
}

View File

@@ -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<ISystemUser>}
*/
public async signupConfirm(
email: string,
verifyToken: string,
): Promise<SystemUser> {
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;
}
}

View File

@@ -0,0 +1,5 @@
export class AuthSignupConfirmResendService {
signUpConfirmResend(userId: number) {
return;
}
}

View File

@@ -0,0 +1,11 @@
import { IsNotEmpty, IsString } from 'class-validator';
export class AuthSigninDto {
@IsNotEmpty()
@IsString()
password: string;
@IsNotEmpty()
@IsString()
email: string;
}

View File

@@ -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;
}

View File

@@ -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 {
/**

View File

@@ -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({

View File

@@ -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';

View File

@@ -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,
) {

View File

@@ -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'];