import { BadRequestException, forwardRef, Inject, Injectable, Logger, UnauthorizedException, } from "@nestjs/common"; import { ConfigService } from "@nestjs/config"; import { authenticator } from "otplib"; import { toDataURL } from "qrcode"; import { HashingService } from "../crypto/services/hashing.service"; import { JwtService } from "../crypto/services/jwt.service"; import { SessionsService } from "../sessions/sessions.service"; import { UsersService } from "../users/users.service"; import { LoginDto } from "./dto/login.dto"; import { RegisterDto } from "./dto/register.dto"; @Injectable() export class AuthService { private readonly logger = new Logger(AuthService.name); constructor( @Inject(forwardRef(() => UsersService)) private readonly usersService: UsersService, private readonly hashingService: HashingService, private readonly jwtService: JwtService, private readonly sessionsService: SessionsService, private readonly configService: ConfigService, ) {} async generateTwoFactorSecret(userId: string) { this.logger.log(`Generating 2FA secret for user ${userId}`); const user = await this.usersService.findOne(userId); if (!user) throw new UnauthorizedException(); const secret = authenticator.generateSecret(); const otpauthUrl = authenticator.keyuri( user.username, this.configService.get("DOMAIN_NAME") || "Memegoat", secret, ); await this.usersService.setTwoFactorSecret(userId, secret); const qrCodeDataUrl = await toDataURL(otpauthUrl); return { secret, qrCodeDataUrl, }; } async enableTwoFactor(userId: string, token: string) { this.logger.log(`Enabling 2FA for user ${userId}`); const secret = await this.usersService.getTwoFactorSecret(userId); if (!secret) { throw new BadRequestException("2FA not initiated"); } const isValid = authenticator.verify({ token, secret }); if (!isValid) { throw new BadRequestException("Invalid 2FA token"); } await this.usersService.toggleTwoFactor(userId, true); return { message: "2FA enabled successfully" }; } async disableTwoFactor(userId: string, token: string) { this.logger.log(`Disabling 2FA for user ${userId}`); const secret = await this.usersService.getTwoFactorSecret(userId); if (!secret) { throw new BadRequestException("2FA not enabled"); } const isValid = authenticator.verify({ token, secret }); if (!isValid) { throw new BadRequestException("Invalid 2FA token"); } await this.usersService.toggleTwoFactor(userId, false); return { message: "2FA disabled successfully" }; } async register(dto: RegisterDto) { this.logger.log(`Registering new user: ${dto.username}`); const { username, email, password } = dto; const passwordHash = await this.hashingService.hashPassword(password); const emailHash = await this.hashingService.hashEmail(email); const user = await this.usersService.create({ username, email, passwordHash, emailHash, }); return { message: "User registered successfully", userId: user.uuid, }; } async login(dto: LoginDto, userAgent?: string, ip?: string) { this.logger.log(`Login attempt for email: ${dto.email}`); const { email, password } = dto; const emailHash = await this.hashingService.hashEmail(email); const user = await this.usersService.findByEmailHash(emailHash); if (!user) { this.logger.warn(`Login failed: user not found for email hash`); throw new UnauthorizedException("Invalid credentials"); } const isPasswordValid = await this.hashingService.verifyPassword( password, user.passwordHash, ); if (!isPasswordValid) { this.logger.warn(`Login failed: invalid password for user ${user.uuid}`); throw new UnauthorizedException("Invalid credentials"); } if (user.isTwoFactorEnabled) { this.logger.log(`2FA required for user ${user.uuid}`); return { message: "2FA required", requires2FA: true, userId: user.uuid, }; } const accessToken = await this.jwtService.generateJwt({ sub: user.uuid, username: user.username, }); const session = await this.sessionsService.createSession( user.uuid, userAgent, ip, ); this.logger.log(`User ${user.uuid} logged in successfully`); return { message: "User logged in successfully", access_token: accessToken, refresh_token: session.refreshToken, }; } async verifyTwoFactorLogin( userId: string, token: string, userAgent?: string, ip?: string, ) { this.logger.log(`2FA verification attempt for user ${userId}`); const user = await this.usersService.findOneWithPrivateData(userId); if (!user || !user.isTwoFactorEnabled) { throw new UnauthorizedException(); } const secret = await this.usersService.getTwoFactorSecret(userId); if (!secret) throw new UnauthorizedException(); const isValid = authenticator.verify({ token, secret }); if (!isValid) { this.logger.warn( `2FA verification failed for user ${userId}: invalid token`, ); throw new UnauthorizedException("Invalid 2FA token"); } const accessToken = await this.jwtService.generateJwt({ sub: user.uuid, username: user.username, }); const session = await this.sessionsService.createSession( user.uuid, userAgent, ip, ); this.logger.log(`User ${userId} logged in successfully via 2FA`); return { message: "User logged in successfully (2FA)", access_token: accessToken, refresh_token: session.refreshToken, }; } async refresh(refreshToken: string) { const session = await this.sessionsService.refreshSession(refreshToken); const user = await this.usersService.findOne(session.userId); if (!user) { throw new UnauthorizedException("User not found"); } const accessToken = await this.jwtService.generateJwt({ sub: user.uuid, username: user.username, }); return { access_token: accessToken, refresh_token: session.refreshToken, }; } async logout() { return { message: "User logged out" }; } }