import { Injectable, UnauthorizedException, ConflictException, BadRequestException } from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; import { PrismaService } from '../../common/prisma.service'; import { RegisterDto, LoginDto, RequestOtpDto, VerifyOtpDto, RequestPasswordResetDto, ResetPasswordDto } from './auth.dto'; import * as bcrypt from 'bcrypt'; import * as crypto from 'crypto'; @Injectable() export class AuthService { constructor(private prisma: PrismaService, private jwt: JwtService) {} async register(dto: RegisterDto) { const exists = await this.prisma.user.findFirst({ where: { OR: [{ email: dto.email }, { phone: dto.phone }] }, }); if (exists) throw new ConflictException('Email or phone already registered'); const passwordHash = await bcrypt.hash(dto.password, 10); const user = await this.prisma.user.create({ data: { fullName: dto.fullName, email: dto.email, phone: dto.phone, passwordHash, nationality: dto.nationality, nationalId: dto.nationalId, passportNumber: dto.passportNumber }, }); const passenger = await this.prisma.passenger.create({ data: { userId: user.id } }); 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 } }); await this.createAuditLog(user.id, 'USER_REGISTERED', 'User', user.id, null, { email: user.email }); return this.signToken(user.id, user.email, user.role, passenger.id); } async login(dto: LoginDto) { const user = await this.prisma.user.findUnique({ where: { email: dto.email }, include: { passenger: true, agent: true }, }); if (!user) throw new UnauthorizedException('Invalid credentials'); if (user.lockedUntil && user.lockedUntil > new Date()) { throw new UnauthorizedException(`Account locked until ${user.lockedUntil.toISOString()}`); } if (!(await bcrypt.compare(dto.password, user.passwordHash))) { await this.prisma.user.update({ where: { id: user.id }, data: { failedLoginAttempts: { increment: 1 }, lockedUntil: user.failedLoginAttempts >= 4 ? new Date(Date.now() + 15 * 60 * 1000) : null } }); throw new UnauthorizedException('Invalid credentials'); } await this.prisma.user.update({ where: { id: user.id }, data: { failedLoginAttempts: 0, lockedUntil: null } }); await this.createAuditLog(user.id, 'USER_LOGIN', 'User', user.id, null, null); return this.signToken(user.id, user.email, user.role, user.passenger?.id, user.agent?.id); } 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'); } const passwordHash = await bcrypt.hash(dto.newPassword, 10); await this.prisma.user.update({ where: { id: resetToken.userId }, data: { passwordHash, failedLoginAttempts: 0, lockedUntil: null } }); await this.prisma.passwordResetToken.update({ where: { id: resetToken.id }, data: { used: true } }); await this.createAuditLog(resetToken.userId, 'PASSWORD_RESET', 'User', resetToken.userId, null, null); return { reset: true }; } private signToken(userId: string, email: string, role: string, passengerId?: string, agentId?: string) { const token = this.jwt.sign({ sub: userId, email, role, passengerId, agentId }); return { token, user: { id: userId, email, role, passengerId, agentId } }; } private async createAuditLog(userId: string, action: string, entityType: string, entityId: string, oldData: any, newData: any) { await this.prisma.auditLog.create({ data: { userId, action, entityType, entityId, oldData, newData } }); } }