refactor: refactor the code

This commit is contained in:
Abdalhamid Alhamad
2025-08-11 15:33:32 +03:00
parent 150027fb71
commit ac63d4cdc7
14 changed files with 98 additions and 264 deletions

View File

@ -4,12 +4,12 @@ import { JwtModule } from '@nestjs/jwt';
import { JuniorModule } from '~/junior/junior.module';
import { UserModule } from '~/user/user.module';
import { AuthController } from './controllers';
import { AuthService, Oauth2Service } from './services';
import { AuthService } from './services';
import { AccessTokenStrategy } from './strategies';
@Module({
imports: [JwtModule.register({}), UserModule, JuniorModule, HttpModule],
providers: [AuthService, AccessTokenStrategy, Oauth2Service],
providers: [AuthService, AccessTokenStrategy],
controllers: [AuthController],
exports: [],
})

View File

@ -86,25 +86,4 @@ export class AuthController {
async logout(@Req() request: Request) {
await this.authService.logout(request);
}
// @Post('biometric/enable')
// @HttpCode(HttpStatus.NO_CONTENT)
// @UseGuards(AccessTokenGuard)
// enableBiometric(@AuthenticatedUser() { sub }: IJwtPayload, @Body() enableBiometricDto: EnableBiometricRequestDto) {
// return this.authService.enableBiometric(sub, enableBiometricDto);
// }
// @Post('biometric/disable')
// @HttpCode(HttpStatus.NO_CONTENT)
// @UseGuards(AccessTokenGuard)
// disableBiometric(@AuthenticatedUser() { sub }: IJwtPayload, @Body() disableBiometricDto: DisableBiometricRequestDto) {
// return this.authService.disableBiometric(sub, disableBiometricDto);
// }
// @Post('junior/set-passcode')
// @HttpCode(HttpStatus.NO_CONTENT)
// @Public()
// setJuniorPasscode(@Body() setPasscodeDto: setJuniorPasswordRequestDto) {
// return this.authService.setJuniorPasscode(setPasscodeDto);
// }
}

View File

@ -1,14 +0,0 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsString } from 'class-validator';
import { i18nValidationMessage as i18n } from 'nestjs-i18n';
export class AppleAdditionalData {
@ApiProperty({ example: 'Ahmad' })
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'customer.firstName' }) })
@IsNotEmpty({ message: i18n('validation.IsNotEmpty', { path: 'general', property: 'customer.firstName' }) })
firstName!: string;
@ApiProperty({ example: 'Khan' })
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'customer.lastName' }) })
@IsNotEmpty({ message: i18n('validation.IsNotEmpty', { path: 'general', property: 'customer.lastName' }) })
lastName!: string;
}

View File

@ -1,21 +0,0 @@
import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { IsNotEmpty, IsOptional, IsString, ValidateNested } from 'class-validator';
import { i18nValidationMessage as i18n } from 'nestjs-i18n';
import { AppleAdditionalData } from './apple-additional-data.request.dto';
export class AppleLoginRequestDto {
@ApiProperty({ example: 'apple_token' })
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'auth.appleToken' }) })
@IsNotEmpty({ message: i18n('validation.IsNotEmpty', { path: 'general', property: 'auth.appleToken' }) })
appleToken!: string;
@ApiProperty({ type: AppleAdditionalData })
@ValidateNested({
each: true,
message: i18n('validation.ValidateNested', { path: 'general', property: 'auth.apple.additionalData' }),
})
@IsOptional()
@Type(() => AppleAdditionalData)
additionalData?: AppleAdditionalData;
}

View File

@ -1,4 +0,0 @@
import { PickType } from '@nestjs/swagger';
import { EnableBiometricRequestDto } from './enable-biometric.request.dto';
export class DisableBiometricRequestDto extends PickType(EnableBiometricRequestDto, ['deviceId']) {}

View File

@ -1,14 +0,0 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsString } from 'class-validator';
import { i18nValidationMessage as i18n } from 'nestjs-i18n';
export class EnableBiometricRequestDto {
@ApiProperty({ example: 'device-id' })
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'auth.deviceId' }) })
@IsNotEmpty({ message: i18n('validation.IsNotEmpty', { path: 'general', property: 'auth.deviceId' }) })
deviceId!: string;
@ApiProperty({ example: 'publicKey' })
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'auth.publicKey' }) })
@IsNotEmpty({ message: i18n('validation.IsNotEmpty', { path: 'general', property: 'auth.publicKey' }) })
publicKey!: string;
}

View File

@ -1,10 +0,0 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsString } from 'class-validator';
import { i18nValidationMessage as i18n } from 'nestjs-i18n';
export class GoogleLoginRequestDto {
@ApiProperty({ example: 'google_token' })
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'auth.googleToken' }) })
@IsNotEmpty({ message: i18n('validation.IsNotEmpty', { path: 'general', property: 'auth.googleToken' }) })
googleToken!: string;
}

View File

@ -1,8 +0,0 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsEmail } from 'class-validator';
import { i18nValidationMessage as i18n } from 'nestjs-i18n';
export class SetEmailRequestDto {
@ApiProperty({ example: 'test@test.com' })
@IsEmail({}, { message: i18n('validation.IsEmail', { path: 'general', property: 'auth.email' }) })
email!: string;
}

View File

@ -1,11 +0,0 @@
export interface ApplePayload {
iss: string;
aud: string;
exp: number;
iat: number;
sub: string;
c_hash: string;
auth_time: number;
nonce_supported: boolean;
email?: string;
}

View File

@ -13,8 +13,6 @@ import { User } from '../../user/entities';
import {
ChangePasswordRequestDto,
CreateUnverifiedUserRequestDto,
DisableBiometricRequestDto,
EnableBiometricRequestDto,
ForgetPasswordRequestDto,
LoginRequestDto,
SendForgetPasswordOtpRequestDto,
@ -24,7 +22,6 @@ import {
} from '../dtos/request';
import { Roles } from '../enums';
import { IJwtPayload, ILoginResponse } from '../interfaces';
import { Oauth2Service } from './oauth2.service';
const ONE_THOUSAND = 1000;
const SALT_ROUNDS = 10;
@ -40,7 +37,6 @@ export class AuthService {
private readonly deviceService: DeviceService,
private readonly userTokenService: UserTokenService,
private readonly cacheService: CacheService,
private readonly oauth2Service: Oauth2Service,
) {}
async sendRegisterOtp(body: CreateUnverifiedUserRequestDto) {
@ -100,43 +96,6 @@ export class AuthService {
return [tokens, user];
}
async enableBiometric(userId: string, { deviceId, publicKey }: EnableBiometricRequestDto) {
this.logger.log(`Enabling biometric for user with id ${userId}`);
const device = await this.deviceService.findUserDeviceById(deviceId, userId);
if (!device) {
this.logger.log(`Device not found, creating new device for user with id ${userId}`);
return this.deviceService.createDevice({
deviceId,
userId,
publicKey,
});
}
if (device.publicKey) {
this.logger.error(`Biometric already enabled for user with id ${userId}`);
throw new BadRequestException('AUTH.BIOMETRIC_ALREADY_ENABLED');
}
return this.deviceService.updateDevice(deviceId, { publicKey });
}
async disableBiometric(userId: string, { deviceId }: DisableBiometricRequestDto) {
const device = await this.deviceService.findUserDeviceById(deviceId, userId);
if (!device) {
this.logger.error(`Device not found for user with id ${userId} and device id ${deviceId}`);
throw new BadRequestException('AUTH.DEVICE_NOT_FOUND');
}
if (!device.publicKey) {
this.logger.error(`Biometric already disabled for user with id ${userId}`);
throw new BadRequestException('AUTH.BIOMETRIC_ALREADY_DISABLED');
}
return this.deviceService.updateDevice(deviceId, { publicKey: null });
}
async sendForgetPasswordOtp({ countryCode, phoneNumber }: SendForgetPasswordOtpRequestDto) {
this.logger.log(`Sending forget password OTP to ${countryCode + phoneNumber}`);
const user = await this.userService.findUserOrThrow({ countryCode, phoneNumber });
@ -319,40 +278,6 @@ export class AuthService {
return [tokens, user];
}
// private async loginWithBiometric(loginDto: LoginRequestDto, deviceId: string): Promise<[ILoginResponse, User]> {
// const user = await this.userService.findUserOrThrow({ email: loginDto.email });
// this.logger.log(`validating biometric for user with email ${loginDto.email}`);
// const device = await this.deviceService.findUserDeviceById(deviceId, user.id);
// if (!device) {
// this.logger.error(`Device not found for user with email ${loginDto.email} and device id ${deviceId}`);
// throw new UnauthorizedException('AUTH.DEVICE_NOT_FOUND');
// }
// if (!device.publicKey) {
// this.logger.error(`Biometric not enabled for user with email ${loginDto.email}`);
// throw new UnauthorizedException('AUTH.BIOMETRIC_NOT_ENABLED');
// }
// const cleanToken = removePadding(loginDto.signature);
// const isValidToken = await verifySignature(
// device.publicKey,
// cleanToken,
// `${user.email} - ${device.deviceId}`,
// 'SHA1',
// );
// if (!isValidToken) {
// this.logger.error(`Invalid biometric for user with email ${loginDto.email}`);
// throw new UnauthorizedException('AUTH.INVALID_BIOMETRIC');
// }
// const tokens = await this.generateAuthToken(user);
// this.logger.log(`Biometric validated successfully for user with email ${loginDto.email}`);
// return [tokens, user];
// }
private async generateAuthToken(user: User) {
this.logger.log(`Generating auth token for user with id ${user.id}`);
const [accessToken, refreshToken] = await Promise.all([

View File

@ -1,2 +1 @@
export * from './auth.service';
export * from './oauth2.service';

View File

@ -1,83 +0,0 @@
import { HttpService } from '@nestjs/axios';
import { Injectable, Logger, UnauthorizedException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { JwtService } from '@nestjs/jwt';
import { OAuth2Client } from 'google-auth-library';
import jwkToPem from 'jwk-to-pem';
import { lastValueFrom } from 'rxjs';
import { ApplePayload } from '../interfaces';
@Injectable()
export class Oauth2Service {
private readonly logger = new Logger(Oauth2Service.name);
private appleKeysEndpoint = 'https://appleid.apple.com/auth/keys';
private appleIssuer = 'https://appleid.apple.com';
private readonly googleWebClientId = this.configService.getOrThrow('GOOGLE_WEB_CLIENT_ID');
private readonly googleAndroidClientId = this.configService.getOrThrow('GOOGLE_ANDROID_CLIENT_ID');
private readonly googleIosClientId = this.configService.getOrThrow('GOOGLE_IOS_CLIENT_ID');
private readonly client = new OAuth2Client();
constructor(
private readonly httpService: HttpService,
private readonly jwtService: JwtService,
private readonly configService: ConfigService,
) {}
async verifyAppleToken(appleToken: string): Promise<ApplePayload> {
try {
const response = await lastValueFrom(this.httpService.get(this.appleKeysEndpoint));
const keys = response.data.keys;
const decodedHeader = this.jwtService.decode(appleToken, { complete: true })?.header;
if (!decodedHeader) {
this.logger.error(`Invalid apple token`);
throw new UnauthorizedException();
}
const keyId = decodedHeader.kid;
const appleKey = keys.find((key: any) => key.kid === keyId);
if (!appleKey) {
this.logger.error(`Invalid apple token`);
throw new UnauthorizedException();
}
const publicKey = jwkToPem(appleKey);
const payload = this.jwtService.verify(appleToken, {
publicKey,
algorithms: ['RS256'],
audience: this.configService.getOrThrow('APPLE_CLIENT_ID').split(','),
issuer: this.appleIssuer,
});
return payload;
} catch (error) {
this.logger.error(`Error verifying apple token: ${error} `);
throw new UnauthorizedException(error);
}
}
async verifyGoogleToken(googleToken: string): Promise<any> {
try {
const ticket = await this.client.verifyIdToken({
idToken: googleToken,
audience: [this.googleWebClientId, this.googleAndroidClientId, this.googleIosClientId],
});
const payload = ticket.getPayload();
if (!payload) {
this.logger.error(`payload not found in google token`);
throw new UnauthorizedException();
}
return payload;
} catch (error) {
this.logger.error(`Invalid google token`, error);
throw new UnauthorizedException();
}
}
}

View File

@ -0,0 +1,95 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class CreateNeoleapRelatedEntities1754915164809 implements MigrationInterface {
name = 'CreateNeoleapRelatedEntities1754915164809';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "customers" DROP CONSTRAINT "FK_e7574892da11dd01de5cfc46499"`);
await queryRunner.query(
`CREATE TABLE "transactions" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "transaction_scope" character varying NOT NULL, "transaction_type" character varying NOT NULL DEFAULT 'EXTERNAL', "card_reference" character varying, "account_reference" character varying, "transaction_id" character varying, "card_masked_number" character varying, "transaction_date" TIMESTAMP WITH TIME ZONE, "rrn" character varying, "transaction_amount" numeric(12,2) NOT NULL, "transaction_currency" character varying NOT NULL, "billing_amount" numeric(12,2) NOT NULL, "settlement_amount" numeric(12,2) NOT NULL, "fees" numeric(12,2) NOT NULL, "vat_on_fees" numeric(12,2) NOT NULL DEFAULT '0', "card_id" uuid, "account_id" uuid, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), CONSTRAINT "UQ_9162bf9ab4e31961a8f7932974c" UNIQUE ("transaction_id"), CONSTRAINT "PK_a219afd8dd77ed80f5a862f1db9" PRIMARY KEY ("id"))`,
);
await queryRunner.query(
`CREATE TABLE "accounts" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "account_reference" character varying(255) NOT NULL, "account_number" character varying(255) NOT NULL, "iban" character varying(255) NOT NULL, "currency" character varying(255) NOT NULL, "balance" numeric(10,2) NOT NULL DEFAULT '0', "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), CONSTRAINT "UQ_829183fe026a0ce5fc8026b2417" UNIQUE ("account_reference"), CONSTRAINT "PK_5a7a02c20412299d198e097a8fe" PRIMARY KEY ("id"))`,
);
await queryRunner.query(
`CREATE UNIQUE INDEX "IDX_829183fe026a0ce5fc8026b241" ON "accounts" ("account_reference") `,
);
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_ffd1ae96513bfb2c6eada0f7d3" ON "accounts" ("account_number") `);
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_9a4b004902294416b096e7556e" ON "accounts" ("iban") `);
await queryRunner.query(
`CREATE TABLE "cards" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "card_reference" character varying NOT NULL, "vpan" character varying NOT NULL, "first_six_digits" character varying(6) NOT NULL, "last_four_digits" character varying(4) NOT NULL, "expiry" character varying NOT NULL, "customer_type" character varying NOT NULL, "color" character varying NOT NULL DEFAULT 'BLUE', "status" character varying NOT NULL DEFAULT 'PENDING', "statusDescription" character varying NOT NULL DEFAULT 'PENDING_ACTIVATION', "limit" numeric(10,2) NOT NULL DEFAULT '0', "scheme" character varying NOT NULL DEFAULT 'VISA', "issuer" character varying NOT NULL, "customer_id" uuid NOT NULL, "parent_id" uuid, "account_id" uuid NOT NULL, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), CONSTRAINT "PK_5f3269634705fdff4a9935860fc" PRIMARY KEY ("id"))`,
);
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_4f7df5c5dc950295dc417a11d8" ON "cards" ("card_reference") `);
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_1ec2ef68b0370f26639261e87b" ON "cards" ("vpan") `);
await queryRunner.query(`ALTER TABLE "customers" DROP CONSTRAINT "REL_e7574892da11dd01de5cfc4649"`);
await queryRunner.query(`ALTER TABLE "customers" DROP COLUMN "profile_picture_id"`);
await queryRunner.query(`ALTER TABLE "users" ADD "first_name" character varying(255) NOT NULL`);
await queryRunner.query(`ALTER TABLE "users" ADD "last_name" character varying(255) NOT NULL`);
await queryRunner.query(`ALTER TABLE "users" ADD "profile_picture_id" uuid`);
await queryRunner.query(
`ALTER TABLE "users" ADD CONSTRAINT "UQ_02ec15de199e79a0c46869895f4" UNIQUE ("profile_picture_id")`,
);
await queryRunner.query(
`ALTER TABLE "documents" ADD "upload_status" character varying(255) NOT NULL DEFAULT 'NOT_UPLOADED'`,
);
await queryRunner.query(`ALTER TABLE "customers" ADD "country" character varying(255)`);
await queryRunner.query(`ALTER TABLE "customers" ADD "region" character varying(255)`);
await queryRunner.query(`ALTER TABLE "customers" ADD "city" character varying(255)`);
await queryRunner.query(`ALTER TABLE "customers" ADD "neighborhood" character varying(255)`);
await queryRunner.query(`ALTER TABLE "customers" ADD "street" character varying(255)`);
await queryRunner.query(`ALTER TABLE "customers" ADD "building" character varying(255)`);
await queryRunner.query(
`ALTER TABLE "users" ADD CONSTRAINT "FK_02ec15de199e79a0c46869895f4" FOREIGN KEY ("profile_picture_id") REFERENCES "documents"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "transactions" ADD CONSTRAINT "FK_80ad48141be648db2d84ff32f79" FOREIGN KEY ("card_id") REFERENCES "cards"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "transactions" ADD CONSTRAINT "FK_49c0d6e8ba4bfb5582000d851f0" FOREIGN KEY ("account_id") REFERENCES "accounts"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "cards" ADD CONSTRAINT "FK_8ba18e7060c38ddae12e5bdf907" FOREIGN KEY ("parent_id") REFERENCES "customers"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "cards" ADD CONSTRAINT "FK_2fd0ee722ec57594d2e448c73d7" FOREIGN KEY ("customer_id") REFERENCES "customers"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "cards" ADD CONSTRAINT "FK_b2874ef49ff7da2dee49e4bc6d3" FOREIGN KEY ("account_id") REFERENCES "accounts"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "cards" DROP CONSTRAINT "FK_b2874ef49ff7da2dee49e4bc6d3"`);
await queryRunner.query(`ALTER TABLE "cards" DROP CONSTRAINT "FK_2fd0ee722ec57594d2e448c73d7"`);
await queryRunner.query(`ALTER TABLE "cards" DROP CONSTRAINT "FK_8ba18e7060c38ddae12e5bdf907"`);
await queryRunner.query(`ALTER TABLE "transactions" DROP CONSTRAINT "FK_49c0d6e8ba4bfb5582000d851f0"`);
await queryRunner.query(`ALTER TABLE "transactions" DROP CONSTRAINT "FK_80ad48141be648db2d84ff32f79"`);
await queryRunner.query(`ALTER TABLE "users" DROP CONSTRAINT "FK_02ec15de199e79a0c46869895f4"`);
await queryRunner.query(`ALTER TABLE "customers" DROP COLUMN "building"`);
await queryRunner.query(`ALTER TABLE "customers" DROP COLUMN "street"`);
await queryRunner.query(`ALTER TABLE "customers" DROP COLUMN "neighborhood"`);
await queryRunner.query(`ALTER TABLE "customers" DROP COLUMN "city"`);
await queryRunner.query(`ALTER TABLE "customers" DROP COLUMN "region"`);
await queryRunner.query(`ALTER TABLE "customers" DROP COLUMN "country"`);
await queryRunner.query(`ALTER TABLE "documents" DROP COLUMN "upload_status"`);
await queryRunner.query(`ALTER TABLE "users" DROP CONSTRAINT "UQ_02ec15de199e79a0c46869895f4"`);
await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "profile_picture_id"`);
await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "last_name"`);
await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "first_name"`);
await queryRunner.query(`ALTER TABLE "customers" ADD "profile_picture_id" uuid`);
await queryRunner.query(
`ALTER TABLE "customers" ADD CONSTRAINT "REL_e7574892da11dd01de5cfc4649" UNIQUE ("profile_picture_id")`,
);
await queryRunner.query(`DROP INDEX "public"."IDX_1ec2ef68b0370f26639261e87b"`);
await queryRunner.query(`DROP INDEX "public"."IDX_4f7df5c5dc950295dc417a11d8"`);
await queryRunner.query(`DROP TABLE "cards"`);
await queryRunner.query(`DROP INDEX "public"."IDX_9a4b004902294416b096e7556e"`);
await queryRunner.query(`DROP INDEX "public"."IDX_ffd1ae96513bfb2c6eada0f7d3"`);
await queryRunner.query(`DROP INDEX "public"."IDX_829183fe026a0ce5fc8026b241"`);
await queryRunner.query(`DROP TABLE "accounts"`);
await queryRunner.query(`DROP TABLE "transactions"`);
await queryRunner.query(
`ALTER TABLE "customers" ADD CONSTRAINT "FK_e7574892da11dd01de5cfc46499" FOREIGN KEY ("profile_picture_id") REFERENCES "documents"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
);
}
}

View File

@ -1,2 +1,3 @@
export * from './1754913378460-initial-migration';
export * from './1754913378461-seed-default-avatar';
export * from './1754915164809-create-neoleap-related-entities';