Spaces:
Runtime error
Runtime error
| 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'; | |
| () | |
| 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<boolean> { | |
| 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<string[]> { | |
| 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<SuperAdminSessionType> { | |
| 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, | |
| }; | |
| } | |
| } | |