import { Injectable, UnauthorizedException, ConflictException, } from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; import { SuperAdmin, SESSION_STATUS } from '@prisma/client'; import { StringValue } from 'ms'; import { SuperAdminCoreService } from 'src/core/super-admin-core/super-admin-core.service'; import { SuperAdminCredentialCoreService } from 'src/core/super-admin-credential-core/super-admin-credential-core.service'; import { SuperAdminSessionCoreService } from 'src/core/super-admin-session-core/super-admin-session-core.service'; import { SuperAdminTwoFactorMethodCoreService } from 'src/core/super-admin-two-factor-method-core/super-admin-two-factor-method-core.service'; import { SuperAdminTrustedDeviceCoreService } from 'src/core/super-admin-trusted-device-core/super-admin-trusted-device-core.service'; import { CommonService } from 'src/shared/modules/common/common.service'; import { SuperAdminRegisterDto } from './dto/register.dto'; import { SuperAdminLoginDto } from './dto/login.dto'; import { SuperAdminRefreshTokenDto } from './dto/refresh-token.dto'; import { SuperAdminSessionType } from 'src/shared/types/super-admin-session.type'; import { accessTokenSignSettings, refreshTokenSignSettings, refreshTokenVerifySettings, TOKEN_TYPE, TOKEN_USER_TYPE, AuthMessages, } from 'src/shared/keys/auth.keys'; @Injectable() export class SuperAdminAuthService { constructor( private readonly jwtService: JwtService, private readonly superAdminCoreService: SuperAdminCoreService, private readonly superAdminCredentialCoreService: SuperAdminCredentialCoreService, private readonly superAdminSessionCoreService: SuperAdminSessionCoreService, private readonly twoFactorMethodCoreService: SuperAdminTwoFactorMethodCoreService, private readonly trustedDeviceCoreService: SuperAdminTrustedDeviceCoreService, private readonly commonService: CommonService, ) {} async register( registerDto: SuperAdminRegisterDto, request: any, ): Promise<{ superAdmin: SuperAdmin; accessToken: string; refreshToken: string; }> { const { email, password, name, profileImage } = registerDto; // Check if super admin already exists const existingSuperAdmin = await this.superAdminCoreService .findFirst({ where: { email, isDeleted: false }, }) .catch(() => null); if (existingSuperAdmin) { throw new ConflictException('Super admin with this email already exists'); } // Create super admin const superAdmin = await this.superAdminCoreService.create({ data: { email, name, profileImage, }, }); // Hash and store password const hashedPassword = await this.commonService.hashPassword(password); await this.superAdminCredentialCoreService.create({ data: { superAdminId: superAdmin.id, password: hashedPassword, }, }); // Generate tokens const { accessToken, refreshToken } = await this.getNewToken(superAdmin); // Get client info const clientInfo = this.commonService.getClientInfo(request); // Create session await this.superAdminSessionCoreService.create({ data: { superAdminId: superAdmin.id, accessToken, refreshToken, ipAddress: clientInfo.ipAddress, userAgent: clientInfo.userAgent, geoIpCountry: clientInfo.geoLocation?.country || '', city: clientInfo.geoLocation?.city || '', state: clientInfo.geoLocation?.region || '', latitude: clientInfo.geoLocation?.ll?.[0] || null, longitude: clientInfo.geoLocation?.ll?.[1] || null, isFromAdmin: true, status: SESSION_STATUS.CURRENT, loginAt: new Date(), }, }); return { superAdmin, accessToken, refreshToken, }; } async login( loginDto: SuperAdminLoginDto, request: any, ): Promise< | { superAdmin: SuperAdmin; accessToken: string; refreshToken: string; } | { requires2FA: true; pending2faToken: string; availableMethods: string[]; message: string; } > { const { email, password, deviceId } = loginDto; // Find super admin const superAdmin = await this.superAdminCoreService .findFirst({ where: { email, isDeleted: false }, }) .catch(() => null); if (!superAdmin) { throw new UnauthorizedException('Invalid credentials'); } // Get super admin credential const credential = await this.superAdminCredentialCoreService .findFirst({ where: { superAdminId: superAdmin.id, isDeleted: false }, }) .catch(() => null); if (!credential) { throw new UnauthorizedException('Invalid credentials'); } // Verify password const isPasswordValid = await this.commonService.comparePassword( password, credential.password, ); if (!isPasswordValid) { throw new UnauthorizedException('Invalid credentials'); } // Check if 2FA is enabled if (superAdmin.isTwoFactorEnabled) { // Check if device is trusted (skip 2FA) if (deviceId) { const trustedDevice = await this.checkTrustedDevice( superAdmin.id, deviceId, ); if (trustedDevice) { // Device is trusted, proceed with normal login return await this.completeLogin(superAdmin, request); } } // Require 2FA verification const enabledMethods = await this.getEnabled2FAMethods(superAdmin.id); if (enabledMethods.length === 0) { // No 2FA methods enabled, proceed with normal login return await this.completeLogin(superAdmin, request); } // Generate pending 2FA token const pending2FATokenSettings = { secret: process.env.PENDING_2FA_TOKEN_SECRET, expiresIn: (process.env.PENDING_2FA_TOKEN_EXPIRY || '10m') as StringValue, }; const pending2faToken = await this.jwtService.signAsync( { superAdminId: superAdmin.id, type: 'PENDING_2FA', }, pending2FATokenSettings, ); return { requires2FA: true, pending2faToken, availableMethods: enabledMethods, message: 'Two-factor authentication required. Please verify using one of the available methods.', }; } // 2FA not enabled, proceed with normal login return await this.completeLogin(superAdmin, request); } private async completeLogin( superAdmin: SuperAdmin, request: any, ): Promise<{ superAdmin: SuperAdmin; accessToken: string; refreshToken: string; }> { // Generate tokens const { accessToken, refreshToken } = await this.getNewToken(superAdmin); // Get client info const clientInfo = this.commonService.getClientInfo(request); // Create session await this.superAdminSessionCoreService.create({ data: { superAdminId: superAdmin.id, accessToken, refreshToken, ipAddress: clientInfo.ipAddress, userAgent: clientInfo.userAgent, geoIpCountry: clientInfo.geoLocation?.country || '', city: clientInfo.geoLocation?.city || '', state: clientInfo.geoLocation?.region || '', latitude: clientInfo.geoLocation?.ll?.[0] || null, longitude: clientInfo.geoLocation?.ll?.[1] || null, isFromAdmin: true, status: SESSION_STATUS.CURRENT, loginAt: new Date(), }, }); return { superAdmin, accessToken, refreshToken, }; } private async checkTrustedDevice( superAdminId: string, deviceId: string, ): Promise { try { const device = await this.trustedDeviceCoreService.findFirst({ where: { deviceId, superAdminId, isDeleted: false, }, }); if (!device) { return false; } // Check if device is still valid (not expired) const now = new Date(); if (device.expiresAt < now) { return false; } // Update last used timestamp await this.trustedDeviceCoreService.update({ where: { id: device.id }, data: { lastUsedAt: now }, }); return true; } catch { return false; } } private async getEnabled2FAMethods(superAdminId: string): Promise { try { const methods = await this.twoFactorMethodCoreService.findMany({ where: { superAdminId, isEnabled: true, isDeleted: false, }, }); return methods.map((method) => method.type); } catch { return []; } } async logout( sessionData: SuperAdminSessionType, ): Promise<{ message: string }> { const { session } = sessionData; // Update session to expired await this.superAdminSessionCoreService.update({ where: { id: session.id }, data: { status: SESSION_STATUS.EXPIRED, expiredAt: new Date(), }, }); return { message: AuthMessages.LOGOUT_SUCCESS }; } async refreshToken(refreshTokenDto: SuperAdminRefreshTokenDto): Promise<{ accessToken: string; refreshToken: string; }> { const { refreshToken } = refreshTokenDto; let validateRefreshToken: any = null; try { validateRefreshToken = await this.jwtService.verifyAsync( refreshToken, refreshTokenVerifySettings, ); } catch { throw new UnauthorizedException('Invalid refresh token'); } // Verify user type if (validateRefreshToken.userType !== TOKEN_USER_TYPE.SUPER_ADMIN) { throw new UnauthorizedException('Invalid token type'); } const superAdmin = await this.superAdminCoreService.findUnique({ where: { id: validateRefreshToken.id, isDeleted: false }, }); if (!superAdmin) { throw new UnauthorizedException('Super admin not found'); } // Generate new tokens const newTokens = await this.getNewToken(superAdmin); // Update session with new tokens await this.superAdminSessionCoreService.updateMany({ where: { superAdminId: superAdmin.id, refreshToken, status: SESSION_STATUS.CURRENT, }, data: { accessToken: newTokens.accessToken, refreshToken: newTokens.refreshToken, }, }); return newTokens; } private async getNewToken(superAdmin: SuperAdmin): Promise<{ accessToken: string; refreshToken: string; }> { const payload = { id: superAdmin.id, email: superAdmin.email, userType: TOKEN_USER_TYPE.SUPER_ADMIN, }; const accessToken = await this.jwtService.signAsync( { ...payload, type: TOKEN_TYPE.ACCESS }, accessTokenSignSettings, ); const refreshToken = await this.jwtService.signAsync( { ...payload, type: TOKEN_TYPE.REFRESH }, refreshTokenSignSettings, ); return { accessToken, refreshToken }; } async performJWTStrategy({ request, superAdmin, }: { request: Request & { headers: { authorization: string } }; superAdmin: SuperAdmin; }): Promise { const accessToken = request.headers.authorization?.replace('Bearer ', ''); if (!accessToken) { throw new UnauthorizedException('Invalid token'); } // Get full super admin object const fullSuperAdmin = await this.superAdminCoreService.findUnique({ where: { id: superAdmin.id, isDeleted: false }, }); if (!fullSuperAdmin) { throw new UnauthorizedException('Super admin not found'); } // Find active session const session = await this.superAdminSessionCoreService.findFirst({ where: { superAdminId: superAdmin.id, accessToken, status: SESSION_STATUS.CURRENT, isDeleted: false, }, }); if (!session) { throw new UnauthorizedException('Session not found or expired'); } return { superAdmin: fullSuperAdmin, session, }; } }