import { ConflictException, Injectable, UnauthorizedException, } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import * as bcrypt from 'bcryptjs'; import * as jwt from 'jsonwebtoken'; import { User } from '../entities/user.entity'; import { RegisterDto } from './dto.register.dto'; import { LoginDto } from './dto.login.dto'; import { AuditService } from '../audit/audit.service'; function parseDurationToSeconds(input: string, fallbackSeconds: number): number { const raw = input.trim(); if (/^\d+$/.test(raw)) return Number(raw); const m = raw.match(/^(\d+)([smhd])$/i); if (!m) return fallbackSeconds; const value = Number(m[1]); const unit = m[2].toLowerCase(); if (unit === 's') return value; if (unit === 'm') return value * 60; if (unit === 'h') return value * 3600; if (unit === 'd') return value * 86400; return fallbackSeconds; } function signAccessToken(userId: string, email: string): string { const secret = process.env.JWT_ACCESS_SECRET as string; const expiresIn = parseDurationToSeconds(process.env.JWT_ACCESS_EXPIRES_IN ?? '15m', 900); return jwt.sign({ sub: userId, email }, secret, { expiresIn }); } function signRefreshToken(userId: string, email: string): string { const secret = process.env.JWT_REFRESH_SECRET as string; const expiresIn = parseDurationToSeconds(process.env.JWT_REFRESH_EXPIRES_IN ?? '30d', 2592000); return jwt.sign({ sub: userId, email, typ: 'refresh' }, secret, { expiresIn }); } @Injectable() export class AuthService { constructor( @InjectRepository(User) private readonly userRepo: Repository, private readonly audit: AuditService, ) {} async register(input: RegisterDto) { const exists = await this.userRepo.findOne({ where: { email: input.email } }); if (exists) { throw new ConflictException('email already exists'); } const passwordHash = await bcrypt.hash(input.password, 10); const user = this.userRepo.create({ email: input.email, passwordHash, refreshTokenHash: null, }); const saved = await this.userRepo.save(user); await this.audit.write({ action: 'auth.register', actorId: saved.id, targetType: 'user', targetId: saved.id, detail: JSON.stringify({ email: saved.email }), }); return { id: saved.id, email: saved.email, createdAt: saved.createdAt, }; } async login(input: LoginDto) { const user = await this.userRepo.findOne({ where: { email: input.email } }); if (!user) throw new UnauthorizedException('invalid credentials'); const ok = await bcrypt.compare(input.password, user.passwordHash); if (!ok) throw new UnauthorizedException('invalid credentials'); const accessToken = signAccessToken(user.id, user.email); const refreshToken = signRefreshToken(user.id, user.email); user.refreshTokenHash = await bcrypt.hash(refreshToken, 10); await this.userRepo.save(user); await this.audit.write({ action: 'auth.login', actorId: user.id, targetType: 'user', targetId: user.id, detail: JSON.stringify({ email: user.email }), }); return { accessToken, refreshToken, tokenType: 'Bearer', user: { id: user.id, email: user.email, }, }; } async refresh(refreshToken: string) { let payload: jwt.JwtPayload; try { payload = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET as string) as jwt.JwtPayload; } catch { throw new UnauthorizedException('invalid refresh token'); } const userId = String(payload.sub ?? ''); const user = await this.userRepo.findOne({ where: { id: userId } }); if (!user || !user.refreshTokenHash) { throw new UnauthorizedException('invalid refresh token'); } const tokenOk = await bcrypt.compare(refreshToken, user.refreshTokenHash); if (!tokenOk) throw new UnauthorizedException('invalid refresh token'); const newAccessToken = signAccessToken(user.id, user.email); const newRefreshToken = signRefreshToken(user.id, user.email); user.refreshTokenHash = await bcrypt.hash(newRefreshToken, 10); await this.userRepo.save(user); await this.audit.write({ action: 'auth.refresh', actorId: user.id, targetType: 'user', targetId: user.id, detail: null, }); return { accessToken: newAccessToken, refreshToken: newRefreshToken, tokenType: 'Bearer', }; } async logout(refreshToken: string) { let payload: jwt.JwtPayload; try { payload = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET as string) as jwt.JwtPayload; } catch { return { status: 'ok' }; } const userId = String(payload.sub ?? ''); if (!userId) return { status: 'ok' }; const user = await this.userRepo.findOne({ where: { id: userId } }); if (!user) return { status: 'ok' }; user.refreshTokenHash = null; await this.userRepo.save(user); await this.audit.write({ action: 'auth.logout', actorId: user.id, targetType: 'user', targetId: user.id, detail: null, }); return { status: 'ok' }; } }