import { Injectable, UnauthorizedException, ConflictException, BadRequestException, NotFoundException, NotImplementedException, Logger, } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { JwtService } from '@nestjs/jwt'; import { ConfigService } from '@nestjs/config'; import * as argon2 from 'argon2'; import * as OTPAuth from 'otpauth'; import * as QRCode from 'qrcode'; import { randomBytes } from 'crypto'; import { User } from '../../entities/user.entity'; import { License } from '../../entities/license.entity'; import { RegisterDto } from './dto/register.dto'; import { LoginDto } from './dto/login.dto'; import { UpdateProfileDto } from './dto/update-profile.dto'; @Injectable() export class AuthService { private readonly logger = new Logger(AuthService.name); constructor( @InjectRepository(User) private userRepository: Repository, @InjectRepository(License) private licenseRepository: Repository, private jwtService: JwtService, private configService: ConfigService, ) {} async register(dto: RegisterDto) { // Normalize email to lowercase to prevent case-sensitive duplicates const normalizedEmail = dto.email.toLowerCase(); // Check if user already exists const existingUser = await this.userRepository .createQueryBuilder('user') .where('LOWER(user.email) = :email OR user.username = :username', { email: normalizedEmail, username: dto.username, }) .getOne(); if (existingUser) { if (existingUser.email.toLowerCase() === normalizedEmail) { throw new ConflictException('Email already registered'); } throw new ConflictException('Username already taken'); } // Hash password const password_hash = await argon2.hash(dto.password); // Create user (email stored lowercase) const user = this.userRepository.create({ email: normalizedEmail, username: dto.username, password_hash, email_verified: false, is_super_admin: false, }); await this.userRepository.save(user); // Create license for the user const licenseKey = this.generateLicenseKey(); const license = this.licenseRepository.create({ license_key: licenseKey, owner_user_id: user.id, status: 'active', modules_enabled: [], webstore_active: false, }); await this.licenseRepository.save(license); // Generate tokens const tokens = await this.generateTokens(user); return { ...tokens, requires_totp: false, user: { id: user.id, email: user.email, username: user.username, is_super_admin: user.is_super_admin, totp_enabled: user.totp_enabled, }, license: { id: license.id, license_key: license.license_key, status: license.status, server_name: license.server_name ?? null, subdomain: license.subdomain ?? null, custom_domain: license.custom_domain ?? null, modules_enabled: license.modules_enabled, webstore_active: license.webstore_active, created_at: license.created_at, expires_at: license.expires_at ?? null, }, }; } async login(dto: LoginDto) { // Find user by email (case-insensitive) const user = await this.userRepository .createQueryBuilder('user') .where('LOWER(user.email) = :email', { email: dto.email.toLowerCase() }) .getOne(); if (!user) { throw new UnauthorizedException('Invalid credentials'); } // Verify password const passwordValid = await argon2.verify(user.password_hash, dto.password); if (!passwordValid) { throw new UnauthorizedException('Invalid credentials'); } // Check TOTP if enabled if (user.totp_enabled) { if (!dto.totp_code) { return { requires_totp: true, user: { id: user.id, email: user.email, username: user.username, }, }; } const totpValid = await this.verifyTotpCode(user, dto.totp_code); if (!totpValid) { throw new UnauthorizedException('Invalid TOTP code'); } } // Generate tokens const tokens = await this.generateTokens(user); // Get user's license const license = await this.licenseRepository.findOne({ where: { owner_user_id: user.id }, }); return { ...tokens, requires_totp: false, user: { id: user.id, email: user.email, username: user.username, is_super_admin: user.is_super_admin, totp_enabled: user.totp_enabled, }, license: license ? { id: license.id, license_key: license.license_key, status: license.status, server_name: license.server_name, subdomain: license.subdomain, custom_domain: license.custom_domain, modules_enabled: license.modules_enabled, webstore_active: license.webstore_active, created_at: license.created_at, expires_at: license.expires_at, } : null, }; } async refresh(refreshToken: string) { try { const payload = await this.jwtService.verifyAsync(refreshToken, { secret: this.configService.get('jwt.secret'), }); const user = await this.userRepository.findOne({ where: { id: payload.sub }, }); if (!user) { throw new UnauthorizedException('User not found'); } // Generate new token pair (rotating refresh tokens) const tokens = await this.generateTokens(user); return { access_token: tokens.access_token, refresh_token: tokens.refresh_token, }; } catch (error) { throw new UnauthorizedException('Invalid refresh token'); } } async setupTotp(userId: string) { const user = await this.userRepository.findOne({ where: { id: userId }, }); if (!user) { throw new NotFoundException('User not found'); } if (user.totp_enabled) { throw new BadRequestException('TOTP already enabled'); } // Generate TOTP secret const secret = new OTPAuth.Secret({ size: 20 }); const totp = new OTPAuth.TOTP({ issuer: 'Corrosion', label: user.email, algorithm: 'SHA1', digits: 6, period: 30, secret, }); // Save secret to user (but don't enable yet) await this.userRepository.update(user.id, { totp_secret: secret.base32, }); // Generate QR code const qrCode = await QRCode.toDataURL(totp.toString()); return { qr_code: qrCode, secret: secret.base32, }; } async verifyTotp(userId: string, code: string) { const user = await this.userRepository.findOne({ where: { id: userId }, }); if (!user) { throw new NotFoundException('User not found'); } if (!user.totp_secret) { throw new BadRequestException('TOTP not set up'); } const valid = await this.verifyTotpCode(user, code); if (!valid) { throw new UnauthorizedException('Invalid TOTP code'); } // Enable TOTP await this.userRepository.update(user.id, { totp_enabled: true, }); return { success: true, }; } async getProfile(userId: string) { const user = await this.userRepository.findOne({ where: { id: userId }, select: ['id', 'email', 'username', 'totp_enabled', 'is_super_admin', 'created_at', 'last_login_at'], }); if (!user) { throw new NotFoundException('User not found'); } // Get user's license const license = await this.licenseRepository.findOne({ where: { owner_user_id: user.id }, }); return { ...user, license_key: license?.license_key, }; } async updateProfile(userId: string, dto: UpdateProfileDto) { const user = await this.userRepository.findOne({ where: { id: userId }, }); if (!user) { throw new NotFoundException('User not found'); } // Check if username is already taken if (dto.username && dto.username !== user.username) { const existingUser = await this.userRepository.findOne({ where: { username: dto.username }, }); if (existingUser) { throw new ConflictException('Username already taken'); } await this.userRepository.update(user.id, { username: dto.username, }); } return this.getProfile(userId); } async forgotPassword(email: string) { // SMTP integration pending — returns 200 to avoid leaking account enumeration this.logger.warn(`Password reset requested for ${email} — SMTP not yet configured`); return { message: 'Password reset is not yet configured. Contact your administrator.', }; } async resetPassword(token: string, _password: string) { this.logger.warn(`Password reset attempted with token ${token} — SMTP not yet configured`); throw new NotImplementedException('Password reset not yet configured'); } // Helper methods private async generateTokens(user: User) { const payload = { sub: user.id, email: user.email, username: user.username, is_super_admin: user.is_super_admin, }; const accessToken = await this.jwtService.signAsync(payload, { secret: this.configService.get('jwt.secret'), expiresIn: this.configService.get('jwt.accessExpirySeconds') || 900, }); const refreshToken = await this.jwtService.signAsync(payload, { secret: this.configService.get('jwt.secret'), expiresIn: this.configService.get('jwt.refreshExpirySeconds') || 604800, }); return { access_token: accessToken, refresh_token: refreshToken, }; } private async verifyTotpCode(user: User, code: string): Promise { if (!user.totp_secret) { return false; } const totp = new OTPAuth.TOTP({ issuer: 'Corrosion', label: user.email, algorithm: 'SHA1', digits: 6, period: 30, secret: OTPAuth.Secret.fromBase32(user.totp_secret), }); const delta = totp.validate({ token: code, window: 1 }); return delta !== null; } private generateLicenseKey(): string { const part1 = randomBytes(2).toString('hex').toUpperCase(); const part2 = randomBytes(2).toString('hex').toUpperCase(); const part3 = randomBytes(2).toString('hex').toUpperCase(); return `CORR-${part1}-${part2}-${part3}`; } }