import { Injectable, UnauthorizedException, ConflictException, BadRequestException, NotFoundException, } 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 { constructor( @InjectRepository(User) private userRepository: Repository, @InjectRepository(License) private licenseRepository: Repository, private jwtService: JwtService, private configService: ConfigService, ) {} async register(dto: RegisterDto) { // Check if user already exists const existingUser = await this.userRepository.findOne({ where: [{ email: dto.email }, { username: dto.username }], }); if (existingUser) { if (existingUser.email === dto.email) { throw new ConflictException('Email already registered'); } throw new ConflictException('Username already taken'); } // Hash password const password_hash = await argon2.hash(dto.password); // Create user const user = this.userRepository.create({ email: dto.email, 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_key: licenseKey, }, }; } async login(dto: LoginDto) { // Find user by email const user = await this.userRepository.findOne({ where: { email: dto.email }, }); 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_key: license?.license_key, }, }; } 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 access token const accessToken = await this.jwtService.signAsync( { sub: user.id, email: user.email, username: user.username, is_super_admin: user.is_super_admin, }, { secret: this.configService.get('jwt.secret'), expiresIn: this.configService.get('jwt.accessExpirySeconds') || 900, }, ); return { access_token: accessToken, }; } 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) { // Stub - SMTP integration later console.log(`Password reset requested for: ${email}`); // In production, generate reset token, save to DB, send email return { message: 'If an account with that email exists, a password reset link has been sent.', }; } async resetPassword(token: string, password: string) { // Stub - SMTP integration later console.log(`Password reset with token: ${token}`); // In production, validate token, update password return { message: 'Password has been reset successfully.', }; } // 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}`; } }