refactor( iam ): remove local auth service delegate OTP and password reset to IAM

This commit is contained in:
Abubeker Yasin
2026-06-06 10:23:53 +03:00
parent 023522c3fb
commit 254d2a171c
5 changed files with 40 additions and 141 deletions

View File

@@ -1,17 +0,0 @@
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(config: ConfigService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: config.get('JWT_SECRET'),
});
}
async validate(payload: any) {
return { userId: payload.sub, email: payload.email, role: payload.role, passengerId: payload.passengerId };
}
}

View File

@@ -1,17 +1,13 @@
import { Body, Controller, Post, HttpCode, HttpStatus, UseGuards, Get, Request, UnauthorizedException } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiResponse, ApiBody, ApiBearerAuth } from '@nestjs/swagger';
import { AuthService } from './auth.service';
import { PassengerAuthService } from './passenger-auth.service';
import { RegisterDto, LoginDto, RequestOtpDto, VerifyOtpDto, RequestPasswordResetDto, ResetPasswordDto } from './auth.dto';
import { RegisterDto, LoginDto } from './auth.dto';
import { JwtGuard } from '../../common/jwt.guard';
@ApiTags('Auth')
@Controller('auth')
export class AuthController {
constructor(
private service: AuthService,
private passengerAuthService: PassengerAuthService,
) {}
constructor(private passengerAuthService: PassengerAuthService) {}
@Post('register')
@ApiOperation({ summary: 'Register new passenger account' })
@@ -32,34 +28,6 @@ export class AuthController {
return this.passengerAuthService.login(dto, req);
}
@Post('otp/request')
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: 'Request OTP verification code' })
@ApiResponse({ status: 200, description: 'OTP sent successfully' })
@ApiBody({ type: RequestOtpDto })
requestOtp(@Body() dto: RequestOtpDto) { return this.service.requestOtp(dto); }
@Post('otp/verify')
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: 'Verify OTP code' })
@ApiResponse({ status: 200, description: 'OTP verified successfully' })
@ApiBody({ type: VerifyOtpDto })
verifyOtp(@Body() dto: VerifyOtpDto) { return this.service.verifyOtp(dto); }
@Post('password/reset-request')
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: 'Request password reset link' })
@ApiResponse({ status: 200, description: 'Password reset email sent' })
@ApiBody({ type: RequestPasswordResetDto })
requestPasswordReset(@Body() dto: RequestPasswordResetDto) { return this.service.requestPasswordReset(dto); }
@Post('password/reset')
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: 'Reset password with token' })
@ApiResponse({ status: 200, description: 'Password reset successfully' })
@ApiBody({ type: ResetPasswordDto })
resetPassword(@Body() dto: ResetPasswordDto) { return this.service.resetPassword(dto); }
@Post('logout')
@HttpCode(HttpStatus.OK)
@UseGuards(JwtGuard)
@@ -89,8 +57,8 @@ export class AuthController {
@ApiResponse({ status: 200, description: 'User profile retrieved successfully' })
@ApiResponse({ status: 401, description: 'Unauthorized' })
getProfile(@Request() req: any) {
const userId = req.user?.id ?? req.user?.userId;
const userId = req.user?.id;
if (!userId) throw new UnauthorizedException('User not authenticated');
return this.service.getProfile(userId);
return this.passengerAuthService.getProfile(userId);
}
}

View File

@@ -1,10 +1,9 @@
import { Module } from '@nestjs/common';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { PassengerAuthService } from './passenger-auth.service';
@Module({
controllers: [AuthController],
providers: [AuthService, PassengerAuthService],
providers: [PassengerAuthService],
})
export class AuthModule {}

View File

@@ -1,86 +0,0 @@
import { Injectable, UnauthorizedException, BadRequestException } from '@nestjs/common';
import { PrismaService } from '../../common/prisma.service';
import { RequestOtpDto, VerifyOtpDto, RequestPasswordResetDto, ResetPasswordDto } from './auth.dto';
import * as crypto from 'crypto';
@Injectable()
export class AuthService {
constructor(private prisma: PrismaService) {}
async requestOtp(dto: RequestOtpDto) {
const code = Math.floor(100000 + Math.random() * 900000).toString();
const expiresAt = new Date(Date.now() + 10 * 60 * 1000);
await this.prisma.otpCode.create({
data: { email: dto.email, code, purpose: dto.purpose, expiresAt },
});
console.log(`[OTP] ${dto.email} - ${code} (${dto.purpose})`);
return { sent: true, expiresIn: 600 };
}
async verifyOtp(dto: VerifyOtpDto) {
const otp = await this.prisma.otpCode.findFirst({
where: { email: dto.email, code: dto.code, purpose: dto.purpose, verified: false, expiresAt: { gt: new Date() } },
orderBy: { createdAt: 'desc' },
});
if (!otp) throw new BadRequestException('Invalid or expired OTP');
await this.prisma.otpCode.update({ where: { id: otp.id }, data: { verified: true } });
return { verified: true };
}
async requestPasswordReset(dto: RequestPasswordResetDto) {
const user = await this.prisma.user.findUnique({ where: { email: dto.email } });
if (!user) return { sent: true };
const token = crypto.randomBytes(32).toString('hex');
const expiresAt = new Date(Date.now() + 60 * 60 * 1000);
await this.prisma.passwordResetToken.create({
data: { userId: user.id, token, expiresAt },
});
console.log(`[PASSWORD_RESET] ${dto.email} - ${token}`);
return { sent: true };
}
async resetPassword(dto: ResetPasswordDto) {
const resetToken = await this.prisma.passwordResetToken.findUnique({ where: { token: dto.token } });
if (!resetToken || resetToken.used || resetToken.expiresAt < new Date()) {
throw new BadRequestException('Invalid or expired reset token');
}
await this.prisma.passwordResetToken.update({ where: { id: resetToken.id }, data: { used: true } });
return { reset: true };
}
async getProfile(userId: string) {
if (!userId) throw new UnauthorizedException('User ID not found in token');
const user = await this.prisma.user.findFirst({
where: { OR: [{ id: userId }, { passenger: { iamUserId: userId } }] },
include: {
passenger: { include: { loyalty: true, wallet: true } },
preferences: true,
},
});
if (!user) throw new UnauthorizedException('User not found');
return {
id: user.id,
iamUserId: user.passenger?.iamUserId,
email: user.email,
phone: user.phone,
fullName: user.fullName,
role: user.role,
nationality: user.nationality,
nationalId: user.nationalId,
passportNumber: user.passportNumber,
faydaVerified: user.faydaVerified,
createdAt: user.createdAt,
passenger: user.passenger ? {
id: user.passenger.id,
preferredLanguage: user.passenger.preferredLanguage,
loyalty: user.passenger.loyalty
? { tier: user.passenger.loyalty.tier, pointsBalance: user.passenger.loyalty.pointsBalance, lifetimePoints: user.passenger.loyalty.lifetimePoints }
: null,
wallet: user.passenger.wallet
? { balanceMinor: user.passenger.wallet.balanceMinor, currency: user.passenger.wallet.currency }
: null,
} : null,
preferences: user.preferences,
};
}
}

View File

@@ -212,6 +212,41 @@ export class PassengerAuthService {
return { success: true, message: 'Logged out successfully' };
}
async getProfile(iamUserId: string) {
const passenger = await this.prisma.passenger.findUnique({
where: { iamUserId },
include: {
user: true,
loyalty: true,
wallet: true,
},
});
if (!passenger) {
throw new Error('Passenger not found');
}
return {
iamUserId,
email: passenger.user?.email,
phone: passenger.user?.phone,
fullName: passenger.user?.fullName,
nationality: passenger.user?.nationality,
nationalId: passenger.user?.nationalId,
passportNumber: passenger.user?.passportNumber,
faydaVerified: passenger.user?.faydaVerified,
createdAt: passenger.createdAt,
passenger: {
id: passenger.id,
preferredLanguage: passenger.preferredLanguage,
loyalty: passenger.loyalty
? { tier: passenger.loyalty.tier, pointsBalance: passenger.loyalty.pointsBalance, lifetimePoints: passenger.loyalty.lifetimePoints }
: null,
wallet: passenger.wallet
? { balanceMinor: passenger.wallet.balanceMinor, currency: passenger.wallet.currency }
: null,
},
};
}
private async compensateIamSignup(email: string): Promise<void> {
try {
await this.dataSource.query(`DELETE FROM iam.sessions WHERE email = $1`, [email]);