import { Injectable, UnauthorizedException, Logger } from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; import { DepartmentsService } from '../departments/departments.service'; import { ApplicantsService } from '../applicants/applicants.service'; import { UsersService } from '../users/users.service'; import { HashUtil } from '../../common/utils'; import { UserRole } from '../../common/enums'; import { ERROR_CODES } from '../../common/constants'; import { JwtPayload } from '../../common/interfaces/request-context.interface'; import { LoginDto, DigiLockerLoginDto, EmailPasswordLoginDto } from './dto'; import * as bcrypt from 'bcrypt'; @Injectable() export class AuthService { private readonly logger = new Logger(AuthService.name); constructor( private readonly jwtService: JwtService, private readonly departmentsService: DepartmentsService, private readonly applicantsService: ApplicantsService, private readonly usersService: UsersService, ) {} /** * Validate department API key and return JWT token */ async validateDepartmentApiKey( apiKey: string, departmentCode: string, ): Promise<{ accessToken: string; department: { id: string; code: string; name: string } }> { const department = await this.departmentsService.findByCode(departmentCode); if (!department) { throw new UnauthorizedException({ code: ERROR_CODES.INVALID_API_KEY, message: 'Invalid department code', }); } if (!department.isActive) { throw new UnauthorizedException({ code: ERROR_CODES.INVALID_API_KEY, message: 'Department is inactive', }); } const isValidApiKey = await HashUtil.comparePassword(apiKey, department.apiKeyHash || ''); if (!isValidApiKey) { throw new UnauthorizedException({ code: ERROR_CODES.INVALID_API_KEY, message: 'Invalid API key', }); } const payload: JwtPayload = { sub: department.id, role: UserRole.DEPARTMENT, departmentCode: department.code, }; return { accessToken: this.jwtService.sign(payload), department: { id: department.id, code: department.code, name: department.name, }, }; } /** * Mock DigiLocker login (for POC) * In production, this would integrate with actual DigiLocker OAuth */ async digiLockerLogin( dto: DigiLockerLoginDto, ): Promise<{ accessToken: string; applicant: { id: string; name: string; email: string } }> { let applicant = await this.applicantsService.findByDigilockerId(dto.digilockerId); // Auto-create applicant if not exists (mock behavior) if (!applicant) { this.logger.log(`Creating new applicant for DigiLocker ID: ${dto.digilockerId}`); applicant = await this.applicantsService.create({ digilockerId: dto.digilockerId, name: dto.name || 'DigiLocker User', email: dto.email || `${dto.digilockerId}@digilocker.gov.in`, phone: dto.phone, }); } if (!applicant.isActive) { throw new UnauthorizedException({ code: ERROR_CODES.UNAUTHORIZED, message: 'Applicant account is inactive', }); } const payload: JwtPayload = { sub: applicant.id, email: applicant.email, role: UserRole.APPLICANT, }; return { accessToken: this.jwtService.sign(payload), applicant: { id: applicant.id, name: applicant.name, email: applicant.email, }, }; } /** * Validate JWT token and return payload */ async validateJwtPayload(payload: JwtPayload): Promise { if (payload.role === UserRole.DEPARTMENT && payload.departmentCode) { const department = await this.departmentsService.findByCode(payload.departmentCode); if (!department || !department.isActive) { throw new UnauthorizedException({ code: ERROR_CODES.INVALID_TOKEN, message: 'Department no longer valid', }); } } else if (payload.role === UserRole.APPLICANT) { const applicant = await this.applicantsService.findById(payload.sub); if (!applicant || !applicant.isActive) { throw new UnauthorizedException({ code: ERROR_CODES.INVALID_TOKEN, message: 'Applicant account no longer valid', }); } } return payload; } /** * Email/Password login for all user types (Admin, Department, Citizen) */ async emailPasswordLogin( dto: EmailPasswordLoginDto, ): Promise<{ accessToken: string; user: { id: string; email: string; name: string; role: string; walletAddress: string; departmentId?: string; }; }> { const user = await this.usersService.findByEmailWithDepartment(dto.email); if (!user) { throw new UnauthorizedException({ code: ERROR_CODES.INVALID_CREDENTIALS, message: 'Invalid email or password', }); } if (!user.isActive) { throw new UnauthorizedException({ code: ERROR_CODES.UNAUTHORIZED, message: 'User account is inactive', }); } const isValidPassword = await bcrypt.compare(dto.password, user.passwordHash); if (!isValidPassword) { throw new UnauthorizedException({ code: ERROR_CODES.INVALID_CREDENTIALS, message: 'Invalid email or password', }); } // Update last login await this.usersService.updateLastLogin(user.id); const payload: JwtPayload = { sub: user.id, email: user.email, role: user.role as UserRole, departmentCode: (user as any).department?.code, }; return { accessToken: this.jwtService.sign(payload), user: { id: user.id, email: user.email, name: user.name, role: user.role, walletAddress: user.walletAddress || '', departmentId: user.departmentId, }, }; } /** * Generate admin token (for internal use) */ generateAdminToken(adminId: string): string { const payload: JwtPayload = { sub: adminId, role: UserRole.ADMIN, }; return this.jwtService.sign(payload); } }