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,28 @@
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',
};
export const IS_PUBLIC_ROUTE = 'isPublic';
export const SendResetPasswordMailQueue = 'SendResetPasswordMailQueue';
export const SendResetPasswordMailJob = 'SendResetPasswordMailJob';
export const SendSignupVerificationMailQueue =
'SendSignupVerificationMailQueue';
export const SendSignupVerificationMailJob = 'SendSignupVerificationMailJob';

View File

@@ -0,0 +1,96 @@
// @ts-nocheck
import {
Body,
Controller,
Get,
Param,
Post,
Request,
UseGuards,
} from '@nestjs/common';
import { ApiTags, ApiOperation, ApiBody, ApiParam } from '@nestjs/swagger';
import { JwtAuthGuard, PublicRoute } from './guards/jwt.guard';
import { AuthenticationApplication } from './AuthApplication.sevice';
import { AuthSignupDto } from './dtos/AuthSignup.dto';
import { AuthSigninDto } from './dtos/AuthSignin.dto';
import { LocalAuthGuard } from './guards/local.guard';
import { JwtService } from '@nestjs/jwt';
import { AuthSigninService } from './commands/AuthSignin.service';
@Controller('/auth')
@ApiTags('Auth')
@PublicRoute()
export class AuthController {
constructor(
private readonly authApp: AuthenticationApplication,
private readonly authSignin: AuthSigninService,
) {}
@Post('/signin')
@UseGuards(LocalAuthGuard)
@ApiOperation({ summary: 'Sign in a user' })
@ApiBody({ type: AuthSigninDto })
signin(@Request() req: Request, @Body() signinDto: AuthSigninDto) {
const { user } = req;
return { access_token: this.authSignin.signToken(user) };
}
@Post('/signup')
@ApiOperation({ summary: 'Sign up a new user' })
@ApiBody({ type: AuthSignupDto })
signup(@Request() req: Request, @Body() signupDto: AuthSignupDto) {
return this.authApp.signUp(signupDto);
}
@Post('/signup/confirm')
@ApiOperation({ summary: 'Confirm user signup' })
@ApiBody({
schema: {
type: 'object',
properties: {
email: { type: 'string', example: 'user@example.com' },
token: { type: 'string', example: 'confirmation-token' },
},
},
})
signupConfirm(@Body('email') email: string, @Body('token') token: string) {
return this.authApp.signUpConfirm(email, token);
}
@Post('/send_reset_password')
@ApiOperation({ summary: 'Send reset password email' })
@ApiBody({
schema: {
type: 'object',
properties: {
email: { type: 'string', example: 'user@example.com' },
},
},
})
sendResetPassword(@Body('email') email: string) {
return this.authApp.sendResetPassword(email);
}
@Post('/reset_password/:token')
@ApiOperation({ summary: 'Reset password using token' })
@ApiParam({ name: 'token', description: 'Reset password token' })
@ApiBody({
schema: {
type: 'object',
properties: {
password: { type: 'string', example: 'new-password' },
},
},
})
resetPassword(
@Param('token') token: string,
@Body('password') password: string,
) {
return this.authApp.resetPassword(token, password);
}
@Get('/meta')
meta() {
return this.authApp.getAuthMeta();
}
}

View File

@@ -0,0 +1,74 @@
import { ModelObject } from 'objection';
import { SystemUser } from '../System/models/SystemUser';
import { TenantModel } from '../System/models/TenantModel';
import { AuthSignupDto } from './dtos/AuthSignup.dto';
export interface JwtPayload {
sub: string;
iat: number;
exp: number;
}
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;
}
export interface IAuthSignUpVerifingEventPayload {
email: string;
verifyToken: string;
userId: number;
}
export interface IAuthSignUpVerifiedEventPayload {
email: string;
verifyToken: string;
userId: number;
}

View File

@@ -0,0 +1,75 @@
import { Module } from '@nestjs/common';
import { AuthController } from './Auth.controller';
import { JwtModule } from '@nestjs/jwt';
import { JwtStrategy } from './strategies/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';
import { PasswordReset } from './models/PasswordReset';
import { TenantDBManagerModule } from '../TenantDBManager/TenantDBManager.module';
import { AuthenticationMailMesssages } from './AuthMailMessages.esrvice';
import { LocalStrategy } from './strategies/Local.strategy';
import { PassportModule } from '@nestjs/passport';
import { APP_GUARD } from '@nestjs/core';
import { JwtAuthGuard } from './guards/jwt.guard';
import { AuthMailSubscriber } from './Subscribers/AuthMail.subscriber';
import { BullModule } from '@nestjs/bullmq';
import {
SendResetPasswordMailQueue,
SendSignupVerificationMailQueue,
} from './Auth.constants';
import { SendResetPasswordMailProcessor } from './processors/SendResetPasswordMail.processor';
import { SendSignupVerificationMailProcessor } from './processors/SendSignupVerificationMail.processor';
import { MailModule } from '../Mail/Mail.module';
import { ConfigService } from '@nestjs/config';
import { InjectSystemModel } from '../System/SystemModels/SystemModels.module';
import { GetAuthMetaService } from './queries/GetAuthMeta.service';
const models = [InjectSystemModel(PasswordReset)];
@Module({
controllers: [AuthController],
imports: [
MailModule,
PassportModule.register({ defaultStrategy: 'jwt' }),
JwtModule.registerAsync({
inject: [ConfigService],
useFactory: (configService: ConfigService) => ({
secret: configService.get('jwt.secret'),
signOptions: { expiresIn: '1d', algorithm: 'HS384' },
verifyOptions: { algorithms: ['HS384'] },
}),
}),
TenantDBManagerModule,
BullModule.registerQueue({ name: SendResetPasswordMailQueue }),
BullModule.registerQueue({ name: SendSignupVerificationMailQueue }),
],
exports: [...models],
providers: [
...models,
LocalStrategy,
JwtStrategy,
AuthenticationApplication,
AuthSendResetPasswordService,
AuthResetPasswordService,
AuthSignupConfirmResendService,
AuthSignupConfirmService,
AuthSignupService,
AuthSigninService,
AuthenticationMailMesssages,
SendResetPasswordMailProcessor,
SendSignupVerificationMailProcessor,
GetAuthMetaService,
{
provide: APP_GUARD,
useClass: JwtAuthGuard,
},
AuthMailSubscriber,
],
})
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

@@ -0,0 +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';
import { GetAuthMetaService } from './queries/GetAuthMeta.service';
@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: GetAuthMetaService,
) {}
/**
* Signin and generates JWT token.
* @param {string} email - Email address.
* @param {string} password - Password.
*/
public async signIn(email: string, password: string) {
return this.authSigninService.signin(email, password);
}
/**
* 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,82 @@
import { Injectable } from '@nestjs/common';
import * as path from 'path';
import { SystemUser } from '../System/models/SystemUser';
import { ModelObject } from 'objection';
import { ConfigService } from '@nestjs/config';
import { Mail } from '../Mail/Mail';
import { MailTransporter } from '../Mail/MailTransporter.service';
@Injectable()
export class AuthenticationMailMesssages {
constructor(
private readonly configService: ConfigService,
private readonly mailTransporter: MailTransporter,
) {}
/**
* Sends reset password message.
* @param {ISystemUser} user - The system user.
* @param {string} token - Reset password token.
* @returns {Mail}
*/
resetPasswordMessage(user: ModelObject<SystemUser>, token: string) {
const baseURL = this.configService.get('baseURL');
return new Mail()
.setSubject('Bigcapital - Password Reset')
.setView('mail/ResetPassword.html')
.setTo(user.email)
.setAttachments([
{
filename: 'bigcapital.png',
path: path.join(global.__static_dirname, `/images/bigcapital.png`),
cid: 'bigcapital_logo',
},
])
.setData({
resetPasswordUrl: `${baseURL}/auth/reset_password/${token}`,
first_name: user.firstName,
last_name: user.lastName,
});
}
sendResetPasswordMail(user: ModelObject<SystemUser>, token: string) {
const mail = this.resetPasswordMessage(user, token);
return this.mailTransporter.send(mail);
}
/**
* Sends signup verification mail.
* @param {string} email - Email address
* @param {string} fullName - User name.
* @param {string} token - Verification token.
* @returns {Mail}
*/
signupVerificationMail(email: string, fullName: string, token: string) {
const baseURL = this.configService.get('baseURL');
const verifyUrl = `${baseURL}/auth/email_confirmation?token=${token}&email=${email}`;
return new Mail()
.setSubject('Bigcapital - Verify your email')
.setView('mail/SignupVerifyEmail.html')
.setTo(email)
.setAttachments([
{
filename: 'bigcapital.png',
path: path.join(global.__static_dirname, `/images/bigcapital.png`),
cid: 'bigcapital_logo',
},
])
.setData({ verifyUrl, fullName });
}
sendSignupVerificationMail(email: string, fullName: string, token: string) {
const mail = this.signupVerificationMail(
email,
fullName,
token,
);
return this.mailTransporter.send(mail);
}
}

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

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

@@ -0,0 +1,5 @@
import { Injectable, ExecutionContext } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class LocalAuthGuard extends AuthGuard('local') {}

View File

@@ -0,0 +1,28 @@
import { ExecutionContext, Injectable, SetMetadata } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport';
import { ClsService } from 'nestjs-cls';
import { IS_PUBLIC_ROUTE } from '../Auth.constants';
export const PublicRoute = () => SetMetadata(IS_PUBLIC_ROUTE, true);
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
constructor(
private reflector: Reflector,
private readonly cls: ClsService,
) {
super();
}
canActivate(context: ExecutionContext) {
const isPublic = this.reflector.getAllAndOverride<boolean>(
IS_PUBLIC_ROUTE,
[context.getHandler(), context.getClass()],
);
if (isPublic) {
return true;
}
return super.canActivate(context);
}
}

View File

@@ -0,0 +1,21 @@
import { SystemModel } from '@/modules/System/models/SystemModel';
export class PasswordReset extends SystemModel {
readonly email: string;
readonly token: string;
readonly createdAt: Date;
/**
* Table name
*/
static get tableName() {
return 'password_resets';
}
/**
* Timestamps columns.
*/
get timestamps() {
return ['createdAt'];
}
}

View File

@@ -0,0 +1,42 @@
import { Processor, WorkerHost } from '@nestjs/bullmq';
import { Scope } from '@nestjs/common';
import {
SendResetPasswordMailJob,
SendResetPasswordMailQueue,
} from '../Auth.constants';
import { Process } from '@nestjs/bull';
import { Job } from 'bullmq';
import { AuthenticationMailMesssages } from '../AuthMailMessages.esrvice';
import { MailTransporter } from '@/modules/Mail/MailTransporter.service';
import { ModelObject } from 'objection';
import { SystemUser } from '@/modules/System/models/SystemUser';
@Processor({
name: SendResetPasswordMailQueue,
scope: Scope.REQUEST,
})
export class SendResetPasswordMailProcessor extends WorkerHost {
constructor(
private readonly authMailMesssages: AuthenticationMailMesssages,
private readonly mailTransporter: MailTransporter,
) {
super();
}
@Process(SendResetPasswordMailJob)
async process(job: Job<SendResetPasswordMailJobPayload>) {
try {
await this.authMailMesssages.sendResetPasswordMail(
job.data.user,
job.data.token,
);
} catch (error) {
console.log('Error occured during send reset password mail', error);
}
}
}
export interface SendResetPasswordMailJobPayload {
user: ModelObject<SystemUser>;
token: string;
}

View File

@@ -0,0 +1,42 @@
import { Scope } from '@nestjs/common';
import { Job } from 'bullmq';
import { Process } from '@nestjs/bull';
import { Processor, WorkerHost } from '@nestjs/bullmq';
import {
SendSignupVerificationMailJob,
SendSignupVerificationMailQueue,
} from '../Auth.constants';
import { MailTransporter } from '@/modules/Mail/MailTransporter.service';
import { AuthenticationMailMesssages } from '../AuthMailMessages.esrvice';
@Processor({
name: SendSignupVerificationMailQueue,
scope: Scope.REQUEST,
})
export class SendSignupVerificationMailProcessor extends WorkerHost {
constructor(
private readonly authMailMesssages: AuthenticationMailMesssages,
private readonly mailTransporter: MailTransporter,
) {
super();
}
@Process(SendSignupVerificationMailJob)
async process(job: Job<SendSignupVerificationMailJobPayload>) {
try {
await this.authMailMesssages.sendSignupVerificationMail(
job.data.email,
job.data.fullName,
job.data.token,
);
} catch (error) {
console.log('Error occured during send signup verification mail', error);
}
}
}
export interface SendSignupVerificationMailJobPayload {
email: string;
fullName: string;
token: string;
}

View File

@@ -0,0 +1,22 @@
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { IAuthGetMetaPOJO } from '../Auth.interfaces';
@Injectable()
export class GetAuthMetaService {
constructor(
private readonly configService: ConfigService,
) {
}
/**
* Retrieves the authentication meta for SPA.
* @returns {Promise<IAuthGetMetaPOJO>}
*/
public async getAuthMeta(): Promise<IAuthGetMetaPOJO> {
return {
signupDisabled: this.configService.get('signupRestrictions.disabled'),
};
}
}

View File

@@ -0,0 +1,24 @@
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { AuthSigninService } from '../commands/AuthSignin.service';
import { JwtPayload } from '../Auth.interfaces';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
constructor(
private readonly authSigninService: AuthSigninService,
private readonly configService: ConfigService,
) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: configService.get('jwt.secret'),
});
}
validate(payload: JwtPayload) {
return this.authSigninService.verifyPayload(payload);
}
}

View File

@@ -0,0 +1,21 @@
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Strategy } from 'passport-local';
import { AuthSigninService } from '../commands/AuthSignin.service';
import { ModelObject } from 'objection';
import { SystemUser } from '../../System/models/SystemUser';
@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy, 'local') {
constructor(private readonly authSigninService: AuthSigninService) {
super({
usernameField: 'email',
passReqToCallback: false,
session: false,
});
}
validate(email: string, password: string): Promise<ModelObject<SystemUser>> {
return this.authSigninService.signin(email, password);
}
}

View File

@@ -0,0 +1,68 @@
import { Injectable } from '@nestjs/common';
import { events } from '@/common/events/events';
import { OnEvent } from '@nestjs/event-emitter';
import {
IAuthSendedResetPassword,
IAuthSignedUpEventPayload,
} from '../Auth.interfaces';
import { Queue } from 'bullmq';
import { InjectQueue } from '@nestjs/bullmq';
import { SendResetPasswordMailJobPayload } from '../processors/SendResetPasswordMail.processor';
import {
SendResetPasswordMailJob,
SendResetPasswordMailQueue,
SendSignupVerificationMailJob,
SendSignupVerificationMailQueue,
} from '../Auth.constants';
import { SendSignupVerificationMailJobPayload } from '../processors/SendSignupVerificationMail.processor';
@Injectable()
export class AuthMailSubscriber {
constructor(
@InjectQueue(SendResetPasswordMailQueue)
private readonly sendResetPasswordMailQueue: Queue,
@InjectQueue(SendSignupVerificationMailQueue)
private readonly sendSignupVerificationMailQueue: Queue,
) {}
/**
* @param {IAuthSignedUpEventPayload} payload
*/
@OnEvent(events.auth.signUp)
async handleSignupSendVerificationMail(payload: IAuthSignedUpEventPayload) {
try {
const job = await this.sendSignupVerificationMailQueue.add(
SendSignupVerificationMailJob,
{
email: payload.user.email,
fullName: payload.user.firstName,
token: payload.user.verifyToken,
} as SendSignupVerificationMailJobPayload,
{
delay: 0,
},
);
console.log(job);
} catch (error) {
console.log(error);
}
}
/**
* @param {IAuthSendedResetPassword} payload
*/
@OnEvent(events.auth.sendResetPassword)
async handleSendResetPasswordMail(payload: IAuthSendedResetPassword) {
await this.sendResetPasswordMailQueue.add(
SendResetPasswordMailJob,
{
user: payload.user,
token: payload.token,
} as SendResetPasswordMailJobPayload,
{
delay: 0,
},
);
}
}