mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-15 20:30:33 +00:00
feat(nestjs): migrate to NestJS
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
130
packages/server/src/modules/Auth/commands/AuthSignup.service.ts
Normal file
130
packages/server/src/modules/Auth/commands/AuthSignup.service.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export class AuthSignupConfirmResendService {
|
||||
signUpConfirmResend(userId: number) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user