streamflix-api / src /modules /super-admin /auth /auth.service.ts
Akshar2325
✨ feat(super-admin-2fa): introduce two-factor authentication
add3ead
Raw
History Blame
12.1 kB
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<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,
};
}
}