refactor: auth module to nestjs

This commit is contained in:
Ahmed Bouhuolia
2025-03-30 05:20:50 +02:00
parent 85946d3161
commit 682be715ae
13 changed files with 419 additions and 64 deletions

View File

@@ -1,25 +1,74 @@
import { Body, Controller, Post, Request } from '@nestjs/common';
import { Body, Controller, Param, Post, Request } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiBody, ApiParam } from '@nestjs/swagger';
import { PublicRoute } from './Jwt.guard';
import { AuthenticationApplication } from './AuthApplication.sevice';
import { AuthSignupDto } from './dtos/AuthSignup.dto';
import { AuthSigninDto } from './dtos/AuthSignin.dto';
@ApiTags('Auth')
@Controller('/auth')
@PublicRoute()
export class AuthController {
constructor(private readonly authApp: AuthenticationApplication) {}
@Post('/signin')
@ApiOperation({ summary: 'Sign in a user' })
@ApiBody({ type: AuthSigninDto })
signin(@Request() req: Request, @Body() signinDto: AuthSigninDto) {
return this.authApp.signIn(signinDto);
}
@Post('/signup')
@ApiOperation({ summary: 'Sign up a new user' })
@ApiBody({ type: AuthSignupDto })
signup(@Request() req: Request, @Body() signupDto: AuthSignupDto) {
this.authApp.signUp(signupDto);
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);
}
}

View File

@@ -10,6 +10,12 @@ import { AuthSignupConfirmResendService } from './commands/AuthSignupConfirmRese
import { AuthSignupConfirmService } from './commands/AuthSignupConfirm.service';
import { AuthSignupService } from './commands/AuthSignup.service';
import { AuthSigninService } from './commands/AuthSignin.service';
import { RegisterTenancyModel } from '../Tenancy/TenancyModels/Tenancy.module';
import { PasswordReset } from './models/PasswordReset';
import { TenantDBManagerModule } from '../TenantDBManager/TenantDBManager.module';
import { AuthenticationMailMesssages } from './AuthMailMessages.esrvice';
const models = [RegisterTenancyModel(PasswordReset)];
@Module({
controllers: [AuthController],
@@ -18,7 +24,10 @@ import { AuthSigninService } from './commands/AuthSignin.service';
secret: 'asdfasdfasdf',
signOptions: { expiresIn: '60s' },
}),
TenantDBManagerModule,
...models,
],
exports: [...models],
providers: [
AuthService,
JwtStrategy,
@@ -29,6 +38,7 @@ import { AuthSigninService } from './commands/AuthSignin.service';
AuthSignupConfirmService,
AuthSignupService,
AuthSigninService,
AuthenticationMailMesssages,
],
})
export class AuthModule {}

View File

@@ -0,0 +1,61 @@
import { Injectable } from '@nestjs/common';
import { SystemUser } from '../System/models/SystemUser';
import { ModelObject } from 'objection';
import { ConfigService } from '@nestjs/config';
import { Mail } from '../Mail/Mail';
@Injectable()
export class AuthenticationMailMesssages {
constructor(private readonly configService: ConfigService) {}
/**
* Sends reset password message.
* @param {ISystemUser} user - The system user.
* @param {string} token - Reset password token.
* @returns {Mail}
*/
sendResetPasswordMessage(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: `${global.__views_dir}/images/bigcapital.png`,
cid: 'bigcapital_logo',
},
])
.setData({
resetPasswordUrl: `${baseURL}/auth/reset_password/${token}`,
first_name: user.firstName,
last_name: user.lastName,
});
}
/**
* Sends signup verification mail.
* @param {string} email - Email address
* @param {string} fullName - User name.
* @param {string} token - Verification token.
* @returns {Mail}
*/
sendSignupVerificationMail(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: `${global.__views_dir}/images/bigcapital.png`,
cid: 'bigcapital_logo',
},
])
.setData({ verifyUrl, fullName });
}
}

View File

@@ -1,10 +1,88 @@
import { Injectable } from '@nestjs/common';
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 {
resetPassword(token: string, password: string): Promise<{ message: string }> {
return new Promise((resolve) => {
resolve({ message: 'Reset password link sent to your email' });
});
/**
* @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

@@ -1,10 +1,70 @@
import { Injectable } from '@nestjs/common';
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 {
sendResetPassword(email: string): Promise<{ message: string }> {
return new Promise((resolve) => {
resolve({ message: 'Reset password link sent to your email' });
/**
* @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

@@ -1,4 +1,5 @@
import crypto from 'crypto';
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';

View File

@@ -0,0 +1,22 @@
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

@@ -103,8 +103,8 @@ export class PlanSubscription extends mixin(SystemModel) {
* Relations mappings.
*/
static get relationMappings() {
const Tenant = require('system/models/Tenant');
const Plan = require('system/models/Subscriptions/Plan');
const { TenantModel } = require('../../System/models/TenantModel');
const { Plan } = require('./Plan');
return {
/**
@@ -112,7 +112,7 @@ export class PlanSubscription extends mixin(SystemModel) {
*/
tenant: {
relation: Model.BelongsToOneRelation,
modelClass: Tenant.default,
modelClass: TenantModel,
join: {
from: 'subscription_plan_subscriptions.tenantId',
to: 'tenants.id',
@@ -124,7 +124,7 @@ export class PlanSubscription extends mixin(SystemModel) {
*/
plan: {
relation: Model.BelongsToOneRelation,
modelClass: Plan.default,
modelClass: Plan,
join: {
from: 'subscription_plan_subscriptions.planId',
to: 'subscription_plans.id',

View File

@@ -1,6 +1,6 @@
import { Inject, Injectable } from '@nestjs/common';
import { Knex } from 'knex';
import { Global, Inject, Injectable } from '@nestjs/common';
import uniqid from 'uniqid';
import * as uniqid from 'uniqid';
import { TenantRepository as TenantBaseRepository } from '@/common/repository/TenantRepository';
import { SystemKnexConnection } from '../SystemDB/SystemDB.constants';
import { TenantModel } from '../models/TenantModel';