Spaces:
Runtime error
Runtime error
| import { | |
| Injectable, | |
| UnauthorizedException, | |
| BadRequestException, | |
| NotFoundException, | |
| } from '@nestjs/common'; | |
| import { JwtService } from '@nestjs/jwt'; | |
| import { ConfigService } from '@nestjs/config'; | |
| import { createHash, randomInt } from 'crypto'; | |
| import * as CryptoJS from 'crypto-js'; | |
| import { authenticator } from 'otplib'; | |
| import { toDataURL } from 'qrcode'; | |
| import { nanoid } from 'nanoid'; | |
| import { EmailService } from 'src/shared/modules/email/email.service'; | |
| import { CommonService } from 'src/shared/modules/common/common.service'; | |
| import { SuperAdminCoreService } from 'src/core/super-admin-core/super-admin-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 { SuperAdminEmailTwoFactorCoreService } from 'src/core/super-admin-email-two-factor-core/super-admin-email-two-factor-core.service'; | |
| import { SuperAdminTotpTwoFactorCoreService } from 'src/core/super-admin-totp-two-factor-core/super-admin-totp-two-factor-core.service'; | |
| import { SuperAdminRecoveryCodeCoreService } from 'src/core/super-admin-recovery-code-core/super-admin-recovery-code-core.service'; | |
| import { SuperAdminTrustedDeviceCoreService } from 'src/core/super-admin-trusted-device-core/super-admin-trusted-device-core.service'; | |
| import { SuperAdminEmailTwoFactorMessages } from 'src/shared/keys/super-admin-email-two-factor.keys'; | |
| import { SuperAdminTwoFactorMethodMessages } from 'src/shared/keys/super-admin-two-factor-method.keys'; | |
| import { SuperAdminTotpTwoFactorMessages } from 'src/shared/keys/super-admin-totp-two-factor.keys'; | |
| import { SuperAdminRecoveryCodeMessages } from 'src/shared/keys/super-admin-recovery-code.keys'; | |
| import { SuperAdminMessages } from 'src/shared/keys/super-admin.keys'; | |
| import { | |
| accessTokenSignSettings, | |
| refreshTokenSignSettings, | |
| TOKEN_USER_TYPE, | |
| TOKEN_TYPE, | |
| } from 'src/shared/keys/auth.keys'; | |
| import { TWO_FACTOR_METHOD_TYPE, SESSION_STATUS } from '@prisma/client'; | |
| import { COMMON_ERROR_MESSAGES } from 'src/keys'; | |
| () | |
| export class TwoFactorAuthService { | |
| constructor( | |
| private readonly jwtService: JwtService, | |
| private readonly configService: ConfigService, | |
| private readonly emailService: EmailService, | |
| private readonly commonService: CommonService, | |
| private readonly superAdminCoreService: SuperAdminCoreService, | |
| private readonly superAdminSessionCoreService: SuperAdminSessionCoreService, | |
| private readonly twoFactorMethodCoreService: SuperAdminTwoFactorMethodCoreService, | |
| private readonly emailTwoFactorCoreService: SuperAdminEmailTwoFactorCoreService, | |
| private readonly totpTwoFactorCoreService: SuperAdminTotpTwoFactorCoreService, | |
| private readonly recoveryCodeCoreService: SuperAdminRecoveryCodeCoreService, | |
| private readonly trustedDeviceCoreService: SuperAdminTrustedDeviceCoreService, | |
| ) {} | |
| // ==================== EMAIL 2FA ==================== | |
| async enableEmail2FA(email: string) { | |
| const superAdmin = await this.superAdminCoreService.findFirst({ | |
| where: { email, isDeleted: false }, | |
| }); | |
| if (!superAdmin) { | |
| throw new NotFoundException(SuperAdminMessages.NOT_FOUND); | |
| } | |
| // Check if already enabled | |
| const existingMethod = await this.twoFactorMethodCoreService | |
| .findFirst({ | |
| where: { | |
| superAdminId: superAdmin.id, | |
| type: TWO_FACTOR_METHOD_TYPE.EMAIL, | |
| isDeleted: false, | |
| }, | |
| }) | |
| .catch(() => null); | |
| if (existingMethod && existingMethod.isEnabled) { | |
| throw new BadRequestException( | |
| SuperAdminTwoFactorMethodMessages.ALREADY_ENABLED, | |
| ); | |
| } | |
| // Create or update 2FA method | |
| const method = existingMethod | |
| ? await this.twoFactorMethodCoreService.update({ | |
| where: { id: existingMethod.id }, | |
| data: { isEnabled: true }, | |
| }) | |
| : await this.twoFactorMethodCoreService.create({ | |
| data: { | |
| superAdminId: superAdmin.id, | |
| type: TWO_FACTOR_METHOD_TYPE.EMAIL, | |
| isEnabled: true, | |
| isPrimary: true, | |
| }, | |
| }); | |
| // Create email 2FA config if not exists | |
| const emailConfig = await this.emailTwoFactorCoreService | |
| .findFirst({ | |
| where: { methodId: method.id, isDeleted: false }, | |
| }) | |
| .catch(() => null); | |
| if (!emailConfig) { | |
| await this.emailTwoFactorCoreService.create({ | |
| data: { | |
| methodId: method.id, | |
| failedAttempts: 0, | |
| }, | |
| }); | |
| } | |
| // Update super admin | |
| await this.superAdminCoreService.update({ | |
| where: { id: superAdmin.id }, | |
| data: { isTwoFactorEnabled: true }, | |
| }); | |
| return { | |
| message: SuperAdminTwoFactorMethodMessages.ENABLE_SUCCESS, | |
| }; | |
| } | |
| async sendEmail2FACode(pending2faToken: string, request: any) { | |
| const { ipAddress, userAgent } = this.commonService.getClientInfo(request); | |
| const clientInfo = this.commonService.getClientInfo(request); | |
| // Verify pending token | |
| const payload = await this.verifyPending2FAToken(pending2faToken); | |
| const superAdmin = await this.superAdminCoreService.findUnique({ | |
| where: { id: payload.superAdminId }, | |
| }); | |
| if (!superAdmin) { | |
| throw new UnauthorizedException(COMMON_ERROR_MESSAGES.INVALID_TOKEN); | |
| } | |
| // Get email 2FA method | |
| const method = await this.twoFactorMethodCoreService.findFirst({ | |
| where: { | |
| superAdminId: superAdmin.id, | |
| type: TWO_FACTOR_METHOD_TYPE.EMAIL, | |
| isEnabled: true, | |
| isDeleted: false, | |
| }, | |
| }); | |
| if (!method) { | |
| throw new NotFoundException( | |
| SuperAdminTwoFactorMethodMessages.NOT_ENABLED, | |
| ); | |
| } | |
| const emailTwoFactor = await this.emailTwoFactorCoreService.findFirst({ | |
| where: { methodId: method.id, isDeleted: false }, | |
| }); | |
| if (!emailTwoFactor) { | |
| throw new NotFoundException(SuperAdminEmailTwoFactorMessages.NOT_FOUND); | |
| } | |
| // Generate 2FA code | |
| const code = this.generateTwoFactorCode(); | |
| const codeHash = this.hashCode(code); | |
| const expiresAt = new Date( | |
| Date.now() + | |
| this.configService.get<number>('TWO_FA_CODE_EXPIRY_MINUTES', 10) * | |
| 60 * | |
| 1000, | |
| ); | |
| // Update email 2FA config | |
| await this.emailTwoFactorCoreService.update({ | |
| where: { id: emailTwoFactor.id }, | |
| data: { | |
| lastCodeHash: codeHash, | |
| lastCodeExpiresAt: expiresAt, | |
| lastSentAt: new Date(), | |
| failedAttempts: 0, | |
| }, | |
| }); | |
| // Build location string from geoIP data | |
| const location = clientInfo.geoLocation | |
| ? `${clientInfo.geoLocation.city || ''}, ${clientInfo.geoLocation.region || ''}, ${clientInfo.geoLocation.country || ''}` | |
| .replace(/, ,/g, ',') | |
| .replace(/^,\s*|,\s*$/g, '') || 'Unknown Location' | |
| : 'Unknown Location'; | |
| // Send email | |
| await this.emailService.send2FACode( | |
| superAdmin.email, | |
| superAdmin.name, | |
| code, | |
| ipAddress ?? undefined, | |
| userAgent ?? 'Unknown', | |
| location, | |
| ); | |
| return { | |
| message: SuperAdminEmailTwoFactorMessages.CODE_SENT, | |
| }; | |
| } | |
| async verifyEmail2FACode( | |
| pending2faToken: string, | |
| code: string, | |
| trustDevice: boolean, | |
| request: any, | |
| ) { | |
| const { ipAddress, userAgent } = this.commonService.getClientInfo(request); | |
| // Verify pending token | |
| const payload = await this.verifyPending2FAToken(pending2faToken); | |
| const superAdmin = await this.superAdminCoreService.findUnique({ | |
| where: { id: payload.superAdminId }, | |
| }); | |
| if (!superAdmin) { | |
| throw new UnauthorizedException(COMMON_ERROR_MESSAGES.INVALID_TOKEN); | |
| } | |
| // Get email 2FA config | |
| const method = await this.twoFactorMethodCoreService.findFirst({ | |
| where: { | |
| superAdminId: superAdmin.id, | |
| type: TWO_FACTOR_METHOD_TYPE.EMAIL, | |
| isEnabled: true, | |
| isDeleted: false, | |
| }, | |
| }); | |
| if (!method) { | |
| throw new NotFoundException( | |
| SuperAdminTwoFactorMethodMessages.NOT_ENABLED, | |
| ); | |
| } | |
| const emailConfig = await this.emailTwoFactorCoreService.findFirst({ | |
| where: { methodId: method.id, isDeleted: false }, | |
| }); | |
| if (!emailConfig) { | |
| throw new NotFoundException(SuperAdminEmailTwoFactorMessages.NOT_FOUND); | |
| } | |
| // Check attempts | |
| const maxAttempts = this.configService.get<number>( | |
| 'TWO_FA_MAX_ATTEMPTS', | |
| 5, | |
| ); | |
| if (emailConfig.failedAttempts >= maxAttempts) { | |
| throw new BadRequestException( | |
| SuperAdminEmailTwoFactorMessages.TOO_MANY_ATTEMPTS, | |
| ); | |
| } | |
| // Check expiry | |
| if ( | |
| !emailConfig.lastCodeExpiresAt || | |
| emailConfig.lastCodeExpiresAt < new Date() | |
| ) { | |
| throw new BadRequestException( | |
| SuperAdminEmailTwoFactorMessages.CODE_EXPIRED, | |
| ); | |
| } | |
| // Verify code | |
| const codeHash = this.hashCode(code); | |
| if (codeHash !== emailConfig.lastCodeHash) { | |
| // Increment failed attempts | |
| await this.emailTwoFactorCoreService.update({ | |
| where: { id: emailConfig.id }, | |
| data: { failedAttempts: emailConfig.failedAttempts + 1 }, | |
| }); | |
| throw new UnauthorizedException( | |
| SuperAdminEmailTwoFactorMessages.CODE_INVALID, | |
| ); | |
| } | |
| // Code is valid - clear the code | |
| await this.emailTwoFactorCoreService.update({ | |
| where: { id: emailConfig.id }, | |
| data: { | |
| lastCodeHash: null, | |
| lastCodeExpiresAt: null, | |
| failedAttempts: 0, | |
| }, | |
| }); | |
| // Generate tokens and create session | |
| const { accessToken, refreshToken } = await this.generateTokensAndSession( | |
| superAdmin, | |
| request, | |
| ); | |
| // Handle trusted device | |
| let trustedDeviceId: string | null = null; | |
| if (trustDevice) { | |
| trustedDeviceId = await this.createTrustedDevice( | |
| superAdmin.id, | |
| ipAddress ?? 'Unknown', | |
| userAgent ?? 'Unknown', | |
| ); | |
| } | |
| return { | |
| message: SuperAdminEmailTwoFactorMessages.VERIFIED_SUCCESS, | |
| accessToken, | |
| refreshToken, | |
| ...(trustedDeviceId && { trustedDeviceId }), | |
| }; | |
| } | |
| // ==================== TOTP 2FA ==================== | |
| async initTotp2FA(email: string) { | |
| const superAdmin = await this.superAdminCoreService.findFirst({ | |
| where: { email, isDeleted: false }, | |
| }); | |
| if (!superAdmin) { | |
| throw new NotFoundException(SuperAdminMessages.NOT_FOUND); | |
| } | |
| // Check if already enabled | |
| const existingMethod = await this.twoFactorMethodCoreService | |
| .findFirst({ | |
| where: { | |
| superAdminId: superAdmin.id, | |
| type: TWO_FACTOR_METHOD_TYPE.AUTHENTICATOR, | |
| isDeleted: false, | |
| }, | |
| }) | |
| .catch(() => null); | |
| let totpTwoFactor: Awaited< | |
| ReturnType<typeof this.totpTwoFactorCoreService.findFirst> | |
| > | null = null; | |
| if (existingMethod) { | |
| totpTwoFactor = await this.totpTwoFactorCoreService | |
| .findFirst({ | |
| where: { methodId: existingMethod.id, isDeleted: false }, | |
| }) | |
| .catch(() => null); | |
| } | |
| if (totpTwoFactor?.confirmedAt) { | |
| throw new BadRequestException( | |
| SuperAdminTotpTwoFactorMessages.ALREADY_CONFIRMED, | |
| ); | |
| } | |
| // Generate secret | |
| const secret = authenticator.generateSecret(); | |
| // Build otpauth URL | |
| const otpauthUrl = authenticator.keyuri( | |
| superAdmin.email, | |
| this.configService.get<string>('TOTP_ISSUER', 'Streamflix'), | |
| secret, | |
| ); | |
| // Encrypt secret | |
| const encryptedSecret = this.encryptTotpSecret(secret); | |
| // Create or update method | |
| let method = existingMethod; | |
| if (!method) { | |
| method = await this.twoFactorMethodCoreService.create({ | |
| data: { | |
| superAdminId: superAdmin.id, | |
| type: TWO_FACTOR_METHOD_TYPE.AUTHENTICATOR, | |
| isEnabled: false, | |
| }, | |
| }); | |
| } | |
| // Create or update TOTP config | |
| if (totpTwoFactor) { | |
| await this.totpTwoFactorCoreService.update({ | |
| where: { id: totpTwoFactor.id }, | |
| data: { secretEncrypted: encryptedSecret, confirmedAt: null }, | |
| }); | |
| } else { | |
| await this.totpTwoFactorCoreService.create({ | |
| data: { | |
| methodId: method.id, | |
| secretEncrypted: encryptedSecret, | |
| }, | |
| }); | |
| } | |
| // Generate QR code | |
| const qrCodeDataUrl = await toDataURL(otpauthUrl); | |
| return { | |
| message: SuperAdminTotpTwoFactorMessages.SETUP_SUCCESS, | |
| secret, // For manual entry | |
| otpauthUrl, | |
| qrCodeDataUrl, | |
| appName: this.configService.get<string>( | |
| 'TOTP_APP_NAME', | |
| 'Streamflix Admin', | |
| ), | |
| }; | |
| } | |
| async verifyTotpSetup(email: string, code: string) { | |
| const superAdmin = await this.superAdminCoreService.findFirst({ | |
| where: { email, isDeleted: false }, | |
| }); | |
| if (!superAdmin) { | |
| throw new NotFoundException(SuperAdminMessages.NOT_FOUND); | |
| } | |
| // Get TOTP method | |
| const method = await this.twoFactorMethodCoreService.findFirst({ | |
| where: { | |
| superAdminId: superAdmin.id, | |
| type: TWO_FACTOR_METHOD_TYPE.AUTHENTICATOR, | |
| isDeleted: false, | |
| }, | |
| }); | |
| if (!method) { | |
| throw new NotFoundException(SuperAdminTotpTwoFactorMessages.NOT_FOUND); | |
| } | |
| const totpConfig = await this.totpTwoFactorCoreService.findFirst({ | |
| where: { methodId: method.id, isDeleted: false }, | |
| }); | |
| if (!totpConfig) { | |
| throw new NotFoundException(SuperAdminTotpTwoFactorMessages.NOT_FOUND); | |
| } | |
| if (totpConfig.confirmedAt) { | |
| throw new BadRequestException( | |
| SuperAdminTotpTwoFactorMessages.ALREADY_CONFIRMED, | |
| ); | |
| } | |
| // Decrypt secret | |
| const secret = this.decryptTotpSecret(totpConfig.secretEncrypted); | |
| // Verify code | |
| const isValid = authenticator.verify({ token: code, secret }); | |
| if (!isValid) { | |
| throw new UnauthorizedException( | |
| SuperAdminTotpTwoFactorMessages.INVALID_CODE, | |
| ); | |
| } | |
| // Confirm TOTP | |
| await this.totpTwoFactorCoreService.update({ | |
| where: { id: totpConfig.id }, | |
| data: { confirmedAt: new Date() }, | |
| }); | |
| // Enable method | |
| await this.twoFactorMethodCoreService.update({ | |
| where: { id: method.id }, | |
| data: { isEnabled: true }, | |
| }); | |
| // Update super admin | |
| await this.superAdminCoreService.update({ | |
| where: { id: superAdmin.id }, | |
| data: { isTwoFactorEnabled: true }, | |
| }); | |
| // Generate recovery codes | |
| const recoveryCodes = await this.generateRecoveryCodes(superAdmin.id); | |
| return { | |
| message: SuperAdminTotpTwoFactorMessages.VERIFIED_SUCCESS, | |
| recoveryCodes, | |
| warning: | |
| 'Save these recovery codes in a safe place. They will not be shown again!', | |
| }; | |
| } | |
| async verifyTotp2FACode( | |
| pending2faToken: string, | |
| code: string, | |
| trustDevice: boolean, | |
| request: any, | |
| ) { | |
| const { ipAddress, userAgent } = this.commonService.getClientInfo(request); | |
| // Verify pending token | |
| const payload = await this.verifyPending2FAToken(pending2faToken); | |
| const superAdmin = await this.superAdminCoreService.findUnique({ | |
| where: { id: payload.superAdminId }, | |
| }); | |
| if (!superAdmin) { | |
| throw new UnauthorizedException(COMMON_ERROR_MESSAGES.INVALID_TOKEN); | |
| } | |
| // Get TOTP method | |
| const method = await this.twoFactorMethodCoreService.findFirst({ | |
| where: { | |
| superAdminId: superAdmin.id, | |
| type: TWO_FACTOR_METHOD_TYPE.AUTHENTICATOR, | |
| isEnabled: true, | |
| isDeleted: false, | |
| }, | |
| }); | |
| if (!method) { | |
| throw new NotFoundException( | |
| SuperAdminTwoFactorMethodMessages.NOT_ENABLED, | |
| ); | |
| } | |
| const totpConfig = await this.totpTwoFactorCoreService.findFirst({ | |
| where: { methodId: method.id, isDeleted: false }, | |
| }); | |
| if (!totpConfig) { | |
| throw new NotFoundException(SuperAdminTotpTwoFactorMessages.NOT_FOUND); | |
| } | |
| if (!totpConfig.confirmedAt) { | |
| throw new BadRequestException( | |
| SuperAdminTotpTwoFactorMessages.NOT_CONFIRMED, | |
| ); | |
| } | |
| // Decrypt secret | |
| const secret = this.decryptTotpSecret(totpConfig.secretEncrypted); | |
| // Verify code | |
| const isValid = authenticator.verify({ token: code, secret }); | |
| if (!isValid) { | |
| throw new UnauthorizedException( | |
| SuperAdminTotpTwoFactorMessages.INVALID_CODE, | |
| ); | |
| } | |
| // Generate tokens and create session | |
| const tokens = await this.generateTokensAndSession(superAdmin, request); | |
| // Handle trusted device | |
| let trustedDeviceId: string | null = null; | |
| if (trustDevice) { | |
| trustedDeviceId = await this.createTrustedDevice( | |
| superAdmin.id, | |
| ipAddress ?? 'Unknown', | |
| userAgent ?? 'Unknown', | |
| ); | |
| } | |
| return { | |
| message: SuperAdminTotpTwoFactorMessages.VERIFIED_SUCCESS, | |
| accessToken: tokens.accessToken, | |
| refreshToken: tokens.refreshToken, | |
| trustedDeviceId, | |
| superAdmin: { | |
| id: superAdmin.id, | |
| email: superAdmin.email, | |
| name: superAdmin.name, | |
| }, | |
| }; | |
| } | |
| // ==================== RECOVERY CODES ==================== | |
| async verifyRecoveryCode( | |
| pending2faToken: string, | |
| code: string, | |
| trustDevice: boolean, | |
| request: any, | |
| ) { | |
| const { ipAddress, userAgent } = this.commonService.getClientInfo(request); | |
| // Verify pending token | |
| const payload = await this.verifyPending2FAToken(pending2faToken); | |
| const superAdmin = await this.superAdminCoreService.findUnique({ | |
| where: { id: payload.superAdminId }, | |
| }); | |
| if (!superAdmin) { | |
| throw new UnauthorizedException(COMMON_ERROR_MESSAGES.INVALID_TOKEN); | |
| } | |
| // Hash the code | |
| const codeHash = this.hashCode(code); | |
| // Find unused recovery code | |
| const recoveryCode = await this.recoveryCodeCoreService | |
| .findFirst({ | |
| where: { | |
| superAdminId: superAdmin.id, | |
| codeHash, | |
| usedAt: null, | |
| isDeleted: false, | |
| }, | |
| }) | |
| .catch(() => null); | |
| if (!recoveryCode) { | |
| throw new UnauthorizedException( | |
| SuperAdminRecoveryCodeMessages.INVALID_CODE, | |
| ); | |
| } | |
| // Mark as used | |
| await this.recoveryCodeCoreService.update({ | |
| where: { id: recoveryCode.id }, | |
| data: { usedAt: new Date() }, | |
| }); | |
| // Generate tokens and create session | |
| const tokens = await this.generateTokensAndSession(superAdmin, request); | |
| // Handle trusted device | |
| let trustedDeviceId: string | null = null; | |
| if (trustDevice) { | |
| trustedDeviceId = await this.createTrustedDevice( | |
| superAdmin.id, | |
| ipAddress ?? 'Unknown', | |
| userAgent ?? 'Unknown', | |
| ); | |
| } | |
| return { | |
| message: SuperAdminRecoveryCodeMessages.VERIFIED_SUCCESS, | |
| warning: SuperAdminRecoveryCodeMessages.ALREADY_USED_VALID, | |
| accessToken: tokens.accessToken, | |
| refreshToken: tokens.refreshToken, | |
| trustedDeviceId, | |
| superAdmin: { | |
| id: superAdmin.id, | |
| email: superAdmin.email, | |
| name: superAdmin.name, | |
| }, | |
| }; | |
| } | |
| async regenerateRecoveryCodes(email: string) { | |
| const superAdmin = await this.superAdminCoreService.findFirst({ | |
| where: { email, isDeleted: false }, | |
| }); | |
| if (!superAdmin) { | |
| throw new NotFoundException(SuperAdminMessages.NOT_FOUND); | |
| } | |
| // Delete old codes | |
| await this.recoveryCodeCoreService.deleteMany({ | |
| where: { superAdminId: superAdmin.id }, | |
| }); | |
| // Generate new codes | |
| const recoveryCodes = await this.generateRecoveryCodes(superAdmin.id); | |
| return { | |
| message: SuperAdminRecoveryCodeMessages.GENERATED_SUCCESS, | |
| recoveryCodes, | |
| warning: | |
| 'Save these recovery codes in a safe place. They will not be shown again!', | |
| }; | |
| } | |
| // ==================== HELPER METHODS ==================== | |
| /** | |
| * Generate a cryptographically secure random code for 2FA | |
| * Uses crypto.randomInt() for security instead of Math.random() | |
| */ | |
| private generateTwoFactorCode(): string { | |
| const codeLength = this.configService.get<number>('TWO_FA_CODE_LENGTH', 6); | |
| const min = Math.pow(10, codeLength - 1); | |
| const max = Math.pow(10, codeLength); | |
| return randomInt(min, max).toString(); | |
| } | |
| private hashCode(code: string): string { | |
| return createHash('sha256').update(code).digest('hex'); | |
| } | |
| private encryptTotpSecret(secret: string): string { | |
| const encryptionKey = this.configService.get<string>('TOTP_ENCRYPTION_KEY'); | |
| if (!encryptionKey) { | |
| throw new Error('TOTP_ENCRYPTION_KEY is not configured'); | |
| } | |
| return CryptoJS.AES.encrypt(secret, encryptionKey).toString(); | |
| } | |
| private decryptTotpSecret(encryptedSecret: string): string { | |
| const encryptionKey = this.configService.get<string>('TOTP_ENCRYPTION_KEY'); | |
| if (!encryptionKey) { | |
| throw new Error('TOTP_ENCRYPTION_KEY is not configured'); | |
| } | |
| const bytes = CryptoJS.AES.decrypt(encryptedSecret, encryptionKey); | |
| return bytes.toString(CryptoJS.enc.Utf8); | |
| } | |
| private async generateRecoveryCodes(superAdminId: string): Promise<string[]> { | |
| const count = this.configService.get<number>('RECOVERY_CODES_COUNT', 10); | |
| const codeLength = this.configService.get<number>( | |
| 'RECOVERY_CODE_LENGTH', | |
| 8, | |
| ); | |
| const segmentLength = Math.floor(codeLength / 2); | |
| const codes: string[] = []; | |
| for (let i = 0; i < count; i++) { | |
| const code = | |
| `${nanoid(segmentLength)}-${nanoid(segmentLength)}`.toUpperCase(); | |
| codes.push(code); | |
| const codeHash = this.hashCode(code); | |
| await this.recoveryCodeCoreService.create({ | |
| data: { | |
| superAdminId, | |
| codeHash, | |
| }, | |
| }); | |
| } | |
| return codes; | |
| } | |
| private async verifyPending2FAToken(token: string): Promise<any> { | |
| try { | |
| return this.jwtService.verify(token, { | |
| secret: this.configService.get<string>('PENDING_2FA_TOKEN_SECRET'), | |
| }); | |
| } catch { | |
| throw new UnauthorizedException(COMMON_ERROR_MESSAGES.INVALID_2FA_TOKEN); | |
| } | |
| } | |
| private async generateTokensAndSession(superAdmin: any, request: any) { | |
| // Generate tokens using auth service pattern | |
| 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, | |
| ); | |
| // 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 { accessToken, refreshToken }; | |
| } | |
| /** | |
| * Create a trusted device and return a JWT token signed with TRUSTED_DEVICE_TOKEN_SECRET | |
| * This token can be stored in cookies/localStorage for device recognition | |
| */ | |
| private async createTrustedDevice( | |
| superAdminId: string, | |
| ipAddress: string, | |
| userAgent: string, | |
| ): Promise<string> { | |
| const deviceId = nanoid(32); | |
| const expiryDays = parseInt( | |
| this.configService | |
| .get<string>('TRUSTED_DEVICE_TOKEN_EXPIRY', '30d') | |
| .replace('d', ''), | |
| ); | |
| const expiresAt = new Date(Date.now() + expiryDays * 24 * 60 * 60 * 1000); | |
| await this.trustedDeviceCoreService.create({ | |
| data: { | |
| superAdminId, | |
| deviceId, | |
| deviceName: userAgent || 'Unknown device', | |
| ipAddress, | |
| userAgent, | |
| expiresAt, | |
| }, | |
| }); | |
| // Create JWT token using TRUSTED_DEVICE_TOKEN_SECRET | |
| const token = await this.jwtService.signAsync( | |
| { | |
| deviceId, | |
| superAdminId, | |
| ipAddress, | |
| type: 'TRUSTED_DEVICE', | |
| }, | |
| { | |
| secret: this.configService.get<string>('TRUSTED_DEVICE_TOKEN_SECRET'), | |
| expiresIn: `${expiryDays}d`, | |
| }, | |
| ); | |
| return token; | |
| } | |
| async get2FAStatus(email: string) { | |
| const superAdmin = await this.superAdminCoreService.findFirst({ | |
| where: { email, isDeleted: false }, | |
| }); | |
| if (!superAdmin) { | |
| throw new NotFoundException(SuperAdminMessages.NOT_FOUND); | |
| } | |
| const methods = await this.twoFactorMethodCoreService.findMany({ | |
| where: { | |
| superAdminId: superAdmin.id, | |
| isDeleted: false, | |
| }, | |
| }); | |
| const enabledMethods = methods | |
| .filter((m) => m.isEnabled) | |
| .map((m) => m.type); | |
| return { | |
| is2FAEnabled: superAdmin.isTwoFactorEnabled, | |
| enabledMethods, | |
| }; | |
| } | |
| } | |