feat: Implement two-factor authentication (2FA) with dedicated endpoints for setup, enabling, disabling, verification, and backup code management.
This commit is contained in:
@@ -4,6 +4,8 @@ import {
|
||||
ConflictException,
|
||||
BadRequestException,
|
||||
NotFoundException,
|
||||
Inject,
|
||||
forwardRef,
|
||||
} from '@nestjs/common';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
@@ -22,6 +24,7 @@ import {
|
||||
VerifyEmailDto,
|
||||
} from './dto';
|
||||
import { UserRole, AuthProvider, UserStatus } from '@prisma/client';
|
||||
import { TwoFactorService } from './two-factor/two-factor.service';
|
||||
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
@@ -30,6 +33,8 @@ export class AuthService {
|
||||
private readonly jwtService: JwtService,
|
||||
private readonly configService: ConfigService,
|
||||
private readonly eventEmitter: EventEmitter2,
|
||||
@Inject(forwardRef(() => TwoFactorService))
|
||||
private readonly twoFactorService: TwoFactorService,
|
||||
) {}
|
||||
|
||||
// ==========================================
|
||||
@@ -162,6 +167,21 @@ export class AuthService {
|
||||
throw new UnauthorizedException('Invalid email or password');
|
||||
}
|
||||
|
||||
// Check if 2FA is enabled
|
||||
if (user.twoFactorEnabled) {
|
||||
// Generate temporary token for 2FA verification
|
||||
const tempToken = await this.twoFactorService.generateTempToken(user.id);
|
||||
|
||||
return {
|
||||
requiresTwoFactor: true,
|
||||
tempToken,
|
||||
user: {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Generate tokens
|
||||
const tokens = await this.generateTokens(user.id, user.email, user.role);
|
||||
|
||||
@@ -615,6 +635,141 @@ export class AuthService {
|
||||
return { message: 'Logged out successfully' };
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// TWO-FACTOR AUTHENTICATION - VERIFY LOGIN
|
||||
// ==========================================
|
||||
async verifyTwoFactorLogin(
|
||||
tempToken: string,
|
||||
token: string,
|
||||
userAgent?: string,
|
||||
ipAddress?: string,
|
||||
) {
|
||||
// Verify temp token and get user ID
|
||||
const userId = await this.twoFactorService.verifyTempToken(tempToken);
|
||||
|
||||
// Verify the TOTP token
|
||||
const isValid = await this.twoFactorService.verifyToken(userId, token);
|
||||
if (!isValid) {
|
||||
throw new UnauthorizedException('Invalid verification code');
|
||||
}
|
||||
|
||||
// Get user with profile
|
||||
const user = await this.prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
include: {
|
||||
userProfile: true,
|
||||
agentProfile: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user || user.status !== UserStatus.ACTIVE) {
|
||||
throw new UnauthorizedException('User not found or inactive');
|
||||
}
|
||||
|
||||
// Generate tokens
|
||||
const tokens = await this.generateTokens(user.id, user.email, user.role);
|
||||
|
||||
// Create session
|
||||
await this.createSession(
|
||||
user.id,
|
||||
tokens.accessToken,
|
||||
tokens.refreshToken,
|
||||
userAgent,
|
||||
ipAddress,
|
||||
);
|
||||
|
||||
// Update last login
|
||||
await this.prisma.user.update({
|
||||
where: { id: user.id },
|
||||
data: { lastLoginAt: new Date() },
|
||||
});
|
||||
|
||||
// Get profile based on role
|
||||
const profile =
|
||||
user.role === UserRole.AGENT ? user.agentProfile : user.userProfile;
|
||||
|
||||
return {
|
||||
user: {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
role: user.role,
|
||||
firstName: profile?.firstName,
|
||||
lastName: profile?.lastName,
|
||||
avatar: profile?.avatar,
|
||||
},
|
||||
...tokens,
|
||||
};
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// TWO-FACTOR AUTHENTICATION - VERIFY BACKUP CODE
|
||||
// ==========================================
|
||||
async verifyBackupCodeLogin(
|
||||
tempToken: string,
|
||||
backupCode: string,
|
||||
userAgent?: string,
|
||||
ipAddress?: string,
|
||||
) {
|
||||
// Verify temp token and get user ID
|
||||
const userId = await this.twoFactorService.verifyTempToken(tempToken);
|
||||
|
||||
// Verify and consume the backup code
|
||||
const isValid = await this.twoFactorService.verifyBackupCode(
|
||||
userId,
|
||||
backupCode,
|
||||
);
|
||||
if (!isValid) {
|
||||
throw new UnauthorizedException('Invalid backup code');
|
||||
}
|
||||
|
||||
// Get user with profile
|
||||
const user = await this.prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
include: {
|
||||
userProfile: true,
|
||||
agentProfile: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user || user.status !== UserStatus.ACTIVE) {
|
||||
throw new UnauthorizedException('User not found or inactive');
|
||||
}
|
||||
|
||||
// Generate tokens
|
||||
const tokens = await this.generateTokens(user.id, user.email, user.role);
|
||||
|
||||
// Create session
|
||||
await this.createSession(
|
||||
user.id,
|
||||
tokens.accessToken,
|
||||
tokens.refreshToken,
|
||||
userAgent,
|
||||
ipAddress,
|
||||
);
|
||||
|
||||
// Update last login
|
||||
await this.prisma.user.update({
|
||||
where: { id: user.id },
|
||||
data: { lastLoginAt: new Date() },
|
||||
});
|
||||
|
||||
// Get profile based on role
|
||||
const profile =
|
||||
user.role === UserRole.AGENT ? user.agentProfile : user.userProfile;
|
||||
|
||||
return {
|
||||
user: {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
role: user.role,
|
||||
firstName: profile?.firstName,
|
||||
lastName: profile?.lastName,
|
||||
avatar: profile?.avatar,
|
||||
},
|
||||
...tokens,
|
||||
};
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// GET CURRENT USER
|
||||
// ==========================================
|
||||
|
||||
Reference in New Issue
Block a user