From ac153fb0d09479e26f03947c11ca826f975c9e80 Mon Sep 17 00:00:00 2001 From: Abubeker Yasin Date: Mon, 8 Jun 2026 09:02:54 +0300 Subject: [PATCH] chore( iam ): remove dead auth DTOs and @nestjs/jwt; migrate fayda/passenger to iamUserId --- apps/edr-passenger-api/package.json | 1 - .../src/modules/auth/auth.dto.ts | 152 +++--------------- .../modules/passengers/passengers.service.ts | 12 +- .../modules/verifayda/verifayda.service.ts | 152 ++++-------------- pnpm-lock.yaml | 3 - 5 files changed, 58 insertions(+), 262 deletions(-) diff --git a/apps/edr-passenger-api/package.json b/apps/edr-passenger-api/package.json index 32cf04322..63fb47377 100644 --- a/apps/edr-passenger-api/package.json +++ b/apps/edr-passenger-api/package.json @@ -29,7 +29,6 @@ "@nestjs/config": "^4.0.4", "@nestjs/core": "^11.1.19", "@nestjs/event-emitter": "^2.0.4", - "@nestjs/jwt": "^10.2.0", "@nestjs/platform-express": "^11.1.19", "@nestjs/schedule": "^6.1.3", "@nestjs/swagger": "^7.4.0", diff --git a/apps/edr-passenger-api/src/modules/auth/auth.dto.ts b/apps/edr-passenger-api/src/modules/auth/auth.dto.ts index da0a0ae5a..a32601c25 100644 --- a/apps/edr-passenger-api/src/modules/auth/auth.dto.ts +++ b/apps/edr-passenger-api/src/modules/auth/auth.dto.ts @@ -2,160 +2,50 @@ import { IsEmail, IsString, MinLength, IsOptional } from 'class-validator'; import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; export class RegisterDto { - @ApiProperty({ - description: 'Full name of the passenger', - example: 'Kelemu Ketsela', - minLength: 2, - maxLength: 100 - }) - @IsString() + @ApiProperty({ example: 'Kelemu Ketsela' }) + @IsString() fullName: string; - @ApiProperty({ - description: 'Email address (must be unique)', - example: 'kelemu@email.com', - format: 'email' - }) - @IsEmail() + @ApiProperty({ example: 'kelemu@email.com' }) + @IsEmail() email: string; - @ApiProperty({ - description: 'Phone number with country code', - example: '+251912345678', - pattern: '^\\+[1-9]\\d{1,14}$' - }) - @IsString() + @ApiProperty({ example: '+251912345678' }) + @IsString() phone: string; - @ApiProperty({ - description: 'Password (minimum 8 characters)', - example: 'SecurePass123', - minLength: 8, - format: 'password' - }) + @ApiProperty({ example: 'SecurePass123', minLength: 8, format: 'password' }) @IsString() @MinLength(8) password: string; - @ApiPropertyOptional({ - description: 'Confirm password (must match password)', - example: 'SecurePass123', - format: 'password' - }) + @ApiPropertyOptional({ example: 'SecurePass123', format: 'password' }) @IsOptional() @IsString() confirmPassword?: string; - @ApiPropertyOptional({ - description: 'Nationality of the passenger', - example: 'Ethiopian' - }) - @IsOptional() - @IsString() + @ApiPropertyOptional({ example: 'Ethiopian' }) + @IsOptional() + @IsString() nationality?: string; - @ApiPropertyOptional({ - description: 'National ID number', - example: 'ET123456789' - }) - @IsOptional() - @IsString() + @ApiPropertyOptional({ example: 'ET123456789' }) + @IsOptional() + @IsString() nationalId?: string; - @ApiPropertyOptional({ - description: 'Passport number for international travelers', - example: 'P1234567' - }) - @IsOptional() - @IsString() + @ApiPropertyOptional({ example: 'P1234567' }) + @IsOptional() + @IsString() passportNumber?: string; } export class LoginDto { - @ApiProperty({ - description: 'Registered email address', - example: 'kelemu@email.com', - format: 'email' - }) - @IsEmail() + @ApiProperty({ example: 'kelemu@email.com' }) + @IsEmail() email: string; - @ApiProperty({ - description: 'Account password', - example: 'password123', - format: 'password' - }) - @IsString() + @ApiProperty({ example: 'password123', format: 'password' }) + @IsString() password: string; } - -export class RequestOtpDto { - @ApiProperty({ - description: 'Email address to send OTP', - example: 'kelemu@email.com' - }) - @IsEmail() - email: string; - - @ApiProperty({ - description: 'Purpose of OTP (REGISTRATION, PASSWORD_RESET, VERIFICATION)', - example: 'REGISTRATION', - enum: ['REGISTRATION', 'PASSWORD_RESET', 'VERIFICATION'] - }) - @IsString() - purpose: string; -} - -export class VerifyOtpDto { - @ApiProperty({ - description: 'Email address', - example: 'kelemu@email.com' - }) - @IsEmail() - email: string; - - @ApiProperty({ - description: '6-digit OTP code', - example: '123456', - minLength: 6, - maxLength: 6 - }) - @IsString() - code: string; - - @ApiProperty({ - description: 'Purpose of OTP verification', - example: 'REGISTRATION', - enum: ['REGISTRATION', 'PASSWORD_RESET', 'VERIFICATION'] - }) - @IsString() - purpose: string; -} - -export class RequestPasswordResetDto { - @ApiProperty({ - description: 'Email address of the account', - example: 'kelemu@email.com' - }) - @IsEmail() - email: string; -} - -export class ResetPasswordDto { - @ApiProperty({ - description: 'Password reset token received via email', - example: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...' - }) - @IsString() - token: string; - - @ApiProperty({ - description: 'New password (minimum 8 characters)', - example: 'NewSecurePass123', - minLength: 8, - format: 'password' - }) - @IsString() - @MinLength(8) - newPassword: string; -} diff --git a/apps/edr-passenger-api/src/modules/passengers/passengers.service.ts b/apps/edr-passenger-api/src/modules/passengers/passengers.service.ts index 8864a6363..520a642e7 100644 --- a/apps/edr-passenger-api/src/modules/passengers/passengers.service.ts +++ b/apps/edr-passenger-api/src/modules/passengers/passengers.service.ts @@ -232,10 +232,12 @@ export class PassengersService { // If logged in, update user profile and link passenger if (isLoggedIn) { - const user = await this.prisma.user.findUnique({ - where: { id: dto.userId }, - include: { passenger: true }, + // dto.userId is the IAM user UUID — resolve via Passenger.iamUserId + const linkedPassenger = await this.prisma.passenger.findUnique({ + where: { iamUserId: dto.userId }, + include: { user: true }, }); + const user = linkedPassenger?.user ?? null; if (!user) { throw new BadRequestException('User not found'); @@ -244,7 +246,7 @@ export class PassengersService { // Update user record if not already verified if (!user.faydaVerified && verifiedData) { await this.prisma.user.update({ - where: { id: dto.userId }, + where: { id: user.id }, data: { fullName: finalData.passengerName, nationality: finalData.nationality, @@ -257,7 +259,7 @@ export class PassengersService { } return { - id: user.passenger?.id || user.id, + id: linkedPassenger?.id || user.id, passengerName: finalData.passengerName, dateOfBirth: finalData.dateOfBirth, nationality: finalData.nationality, diff --git a/apps/edr-passenger-api/src/modules/verifayda/verifayda.service.ts b/apps/edr-passenger-api/src/modules/verifayda/verifayda.service.ts index 1adb20fea..4e2c34668 100644 --- a/apps/edr-passenger-api/src/modules/verifayda/verifayda.service.ts +++ b/apps/edr-passenger-api/src/modules/verifayda/verifayda.service.ts @@ -7,8 +7,6 @@ import { } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import axios, { AxiosInstance } from 'axios'; -import * as bcrypt from 'bcrypt'; -import { randomBytes } from 'crypto'; import { PrismaService } from '../../common/prisma.service'; import { FaydaConfig, FaydaPlatform } from '../../config/fayda.config'; import { @@ -142,7 +140,8 @@ export class VerifaydaService { purpose: input.purpose, platform: input.platform ?? 'WEB', saveToAccount: input.saveToAccount ?? false, - userId: input.userId ?? null, + iamUserId: input.userId ?? null, + userId: null, bookingId: input.bookingId ?? null, expiresAt, }, @@ -283,16 +282,15 @@ export class VerifaydaService { }); } - async getVerificationStatus(userId: string): Promise { - const user = await this.prisma.user.findUnique({ - where: { id: userId }, - select: { faydaVerified: true, faydaVerifiedAt: true, fullName: true }, + async getVerificationStatus(iamUserId: string): Promise { + const passenger = await this.prisma.passenger.findUnique({ + where: { iamUserId }, + include: { user: { select: { faydaVerified: true, faydaVerifiedAt: true, fullName: true } } }, }); - return { - verified: user?.faydaVerified ?? false, - verifiedAt: user?.faydaVerifiedAt ?? undefined, - fullName: user?.fullName ?? undefined, + verified: passenger?.user?.faydaVerified ?? false, + verifiedAt: passenger?.user?.faydaVerifiedAt ?? undefined, + fullName: passenger?.user?.fullName ?? undefined, }; } @@ -422,6 +420,7 @@ export class VerifaydaService { private async handlePurchaseSuccess( session: { id: string; + iamUserId: string | null; userId: string | null; bookingId: string | null; saveToAccount: boolean; @@ -439,129 +438,38 @@ export class VerifaydaService { }); } - if (session.userId && session.saveToAccount) { + const iamUserId = session.iamUserId; + if (iamUserId && session.saveToAccount) { + const passenger = await this.prisma.passenger.findUnique({ + where: { iamUserId }, + select: { userId: true }, + }); + const localUserId = passenger?.userId; + if (!localUserId) return; + const conflict = await this.prisma.user.findFirst({ - where: { - faydaSub: normalized.sub, - NOT: { id: session.userId }, - }, + where: { faydaSub: normalized.sub, NOT: { id: localUserId } }, select: { id: true }, }); - if (conflict) { - throw new FaydaIdentityConflictException(); - } + if (conflict) throw new FaydaIdentityConflictException(); await this.prisma.user.update({ - where: { id: session.userId }, - data: { - faydaVerified: true, - faydaVerifiedAt: new Date(), - faydaSub: normalized.sub, - }, + where: { id: localUserId }, + data: { faydaVerified: true, faydaVerifiedAt: new Date(), faydaSub: normalized.sub }, }); } } - /** - * Resolves the User for a LOGIN flow and returns its id (the caller mints the - * JWT via {@link issueLoginToken}). Resolution order: - * 1. Existing user already linked to this Fayda `sub`. - * 2. Existing account whose email/phone matches — linked to this `sub`. - * 3. Otherwise a fresh Fayda-backed account is created. - */ + // LOGIN via Fayda is now handled entirely by the IAM package's own OIDC flow. + // This method is kept as a stub so completeVerification() still compiles; + // it throws immediately without touching the database. private async handleLoginSuccess( - normalized: NormalizedFaydaUserInfo, + _normalized: NormalizedFaydaUserInfo, ): Promise<{ userId: string }> { - let userId: string; - - const bySub = await this.prisma.user.findUnique({ - where: { faydaSub: normalized.sub }, - select: { id: true }, + throw new UnauthorizedException({ + code: 'FAYDA_LOGIN_MIGRATED_TO_IAM', + message: 'Fayda login tokens are issued by the IAM package at /v1/auth/fayda endpoints.', }); - - if (bySub) { - userId = bySub.id; - } else { - const matchers: Array<{ email?: string; phone?: string }> = []; - if (normalized.email) matchers.push({ email: normalized.email }); - if (normalized.phoneNumber) matchers.push({ phone: normalized.phoneNumber }); - - const existing = matchers.length - ? await this.prisma.user.findFirst({ - where: { OR: matchers }, - select: { id: true, faydaSub: true }, - }) - : null; - - if (existing) { - if (existing.faydaSub && existing.faydaSub !== normalized.sub) { - // The matched account is already tied to a different Fayda identity. - throw new FaydaIdentityConflictException(); - } - await this.prisma.user.update({ - where: { id: existing.id }, - data: { - faydaSub: normalized.sub, - faydaVerified: true, - faydaVerifiedAt: new Date(), - }, - }); - userId = existing.id; - this.logger.log(`Fayda login linked existing user ${existing.id}`); - } else { - userId = await this.createFaydaUser(normalized); - this.logger.log(`Fayda login created new user ${userId}`); - } - } - - return { userId }; - } - - /** - * Creates a Fayda-backed User plus the same satellite rows registration makes - * (Passenger, LoyaltyAccount, WalletAccount, UserPreferences). - * - * The user has no password — `passwordHash` is set to a bcrypt of random bytes - * so password login is impossible; they authenticate only via Fayda. When - * Fayda doesn't supply an email/phone, a deterministic placeholder derived from - * the (unique) `sub` keeps the NOT NULL + unique columns satisfied. - */ - private async createFaydaUser( - normalized: NormalizedFaydaUserInfo, - ): Promise { - const passwordHash = await bcrypt.hash( - randomBytes(32).toString('hex'), - 10, - ); - const email = normalized.email ?? `fayda_${normalized.sub}@users.fayda.local`; - const phone = normalized.phoneNumber ?? `fayda:${normalized.sub}`; - const fullName = normalized.fullName ?? 'Fayda User'; - - const user = await this.prisma.user.create({ - data: { - fullName, - email, - phone, - passwordHash, - faydaVerified: true, - faydaVerifiedAt: new Date(), - faydaSub: normalized.sub, - }, - select: { id: true }, - }); - const passenger = await this.prisma.passenger.create({ - data: { userId: user.id }, - select: { id: true }, - }); - await this.prisma.loyaltyAccount.create({ - data: { passengerId: passenger.id }, - }); - await this.prisma.walletAccount.create({ - data: { passengerId: passenger.id }, - }); - await this.prisma.userPreferences.create({ data: { userId: user.id } }); - - return user.id; } private async markSessionFailed( diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 59c0fae46..511d75426 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -150,9 +150,6 @@ importers: '@nestjs/event-emitter': specifier: ^2.0.4 version: 2.1.1(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.23) - '@nestjs/jwt': - specifier: ^10.2.0 - version: 10.2.0(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2)) '@nestjs/platform-express': specifier: ^11.1.19 version: 11.1.23(@nestjs/common@11.1.23(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.23)