feat(nestjs): migrate to NestJS

This commit is contained in:
Ahmed Bouhuolia
2025-04-07 11:51:24 +02:00
parent f068218a16
commit 55fcc908ef
3779 changed files with 631 additions and 195332 deletions

View File

@@ -0,0 +1,88 @@
import { ConfigService } from '@nestjs/config';
import { Inject, Injectable } from '@nestjs/common';
import { SystemUser } from '@/modules/System/models/SystemUser';
import { PasswordReset } from '../models/PasswordReset';
import { ServiceError } from '@/modules/Items/ServiceError';
import { ERRORS } from '../Auth.constants';
import { hashPassword } from '../Auth.utils';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { events } from '@/common/events/events';
import { IAuthResetedPasswordEventPayload } from '../Auth.interfaces';
@Injectable()
export class AuthResetPasswordService {
/**
* @param {ConfigService} configService - Config service.
* @param {EventEmitter2} eventEmitter - Event emitter.
* @param {typeof SystemUser} systemUserModel
* @param {typeof PasswordReset} passwordResetModel - Reset password model.
*/
constructor(
private readonly configService: ConfigService,
private readonly eventEmitter: EventEmitter2,
@Inject(SystemUser.name)
private readonly systemUserModel: typeof SystemUser,
@Inject(PasswordReset.name)
private readonly passwordResetModel: typeof PasswordReset,
) {}
/**
* 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): Promise<void> {
// Finds the password reset token.
const tokenModel = 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);
}
const resetPasswordSeconds = this.configService.get('resetPasswordSeconds');
// Different between tokne creation datetime and current time.
if (moment().diff(tokenModel.createdAt, 'seconds') > resetPasswordSeconds) {
// Deletes the expired token by expired token email.
await this.deletePasswordResetToken(tokenModel.email);
throw new ServiceError(ERRORS.TOKEN_EXPIRED);
}
const user = await this.systemUserModel
.query()
.findOne({ email: tokenModel.email });
if (!user) {
throw new ServiceError(ERRORS.USER_NOT_FOUND);
}
const hashedPassword = await hashPassword(password);
await this.systemUserModel
.query()
.findById(user.id)
.update({ password: hashedPassword });
// Deletes the used token.
await this.deletePasswordResetToken(tokenModel.email);
// Triggers `onResetPassword` event.
await this.eventEmitter.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();
}
}

View File

@@ -0,0 +1,70 @@
import { Inject, Injectable } from '@nestjs/common';
import * as uniqid from 'uniqid';
import {
IAuthSendedResetPassword,
IAuthSendingResetPassword,
} from '../Auth.interfaces';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { PasswordReset } from '../models/PasswordReset';
import { SystemUser } from '@/modules/System/models/SystemUser';
import { events } from '@/common/events/events';
@Injectable()
export class AuthSendResetPasswordService {
/**
* @param {EventEmitter2} eventPublisher - Event emitter.
* @param {typeof PasswordReset} resetPasswordModel - Password reset model.
* @param {typeof SystemUser} systemUserModel - System user model.
*/
constructor(
private readonly eventPublisher: EventEmitter2,
@Inject(PasswordReset.name)
private readonly resetPasswordModel: typeof PasswordReset,
@Inject(SystemUser.name)
private readonly systemUserModel: typeof SystemUser,
) {}
/**
* Sends the given email reset password email.
* @param {string} email - Email address.
*/
async sendResetPassword(email: string): Promise<void> {
const user = await this.systemUserModel
.query()
.findOne({ email })
.throwIfNotFound();
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.resetPasswordModel.query().insert({
email,
token,
});
// Triggers sent reset password event.
await this.eventPublisher.emitAsync(events.auth.sendResetPassword, {
user,
token,
} as IAuthSendedResetPassword);
}
/**
* Deletes the password reset token by the given email.
* @param {string} email
* @returns {Promise}
*/
private async deletePasswordResetToken(email: string) {
return this.resetPasswordModel.query().where('email', email).delete();
}
}

View File

@@ -0,0 +1,87 @@
import { ClsService } from 'nestjs-cls';
import { Inject, Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { SystemUser } from '@/modules/System/models/SystemUser';
import { ModelObject } from 'objection';
import { JwtPayload } from '../Auth.interfaces';
@Injectable()
export class AuthSigninService {
constructor(
@Inject(SystemUser.name)
private readonly systemUserModel: typeof SystemUser,
private readonly jwtService: JwtService,
private readonly clsService: ClsService,
) {}
/**
* Validates the given email and password.
* @param {string} email - Signin email address.
* @param {string} password - Signin password.
* @returns {Promise<ModelObject<SystemUser>>}
*/
async signin(
email: string,
password: string,
): Promise<ModelObject<SystemUser>> {
let user: SystemUser;
try {
user = await this.systemUserModel
.query()
.findOne({ email })
.throwIfNotFound();
} catch (err) {
throw new UnauthorizedException(
`There isn't any user with email: ${email}`,
);
}
if (!(await user.checkPassword(password))) {
throw new UnauthorizedException(
`Wrong password for user with email: ${email}`,
);
}
if (!user.verified) {
throw new UnauthorizedException(
`The user is not verified yet, check out your mail inbox.`
);
}
return user;
}
/**
* Verifies the given jwt payload.
* @param {JwtPayload} payload
* @returns {Promise<any>}
*/
async verifyPayload(payload: JwtPayload): Promise<any> {
let user: SystemUser;
try {
user = await this.systemUserModel
.query()
.findOne({ email: payload.sub })
.throwIfNotFound();
this.clsService.set('tenantId', user.tenantId);
this.clsService.set('userId', user.id);
} catch (error) {
throw new UnauthorizedException(
`There isn't any user with email: ${payload.sub}`,
);
}
return payload;
}
/**
*
* @param {SystemUser} user
* @returns {string}
*/
signToken(user: SystemUser): string {
const payload = {
sub: user.email,
};
return this.jwtService.sign(payload);
}
}

View File

@@ -0,0 +1,130 @@
import * as crypto from 'crypto';
import * as moment from 'moment';
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,
organizationId: tenant.organizationId,
};
}
/**
* 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;
}
}