import { Injectable, ConflictException, UnauthorizedException } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { JwtService } from '@nestjs/jwt'; import * as bcrypt from 'bcrypt'; import { PrismaService } from '../prisma/prisma.service'; import { RegisterDto } from './dto/register.dto'; import { LoginDto } from './dto/login.dto'; import { UpdateProfileDto } from './dto/update-profile.dto'; import { JwtPayload } from './interfaces/jwt-payload.interface'; const SALT_ROUNDS = 12; @Injectable() export class AuthService { constructor( private readonly prisma: PrismaService, private readonly jwtService: JwtService, private readonly configService: ConfigService, ) {} async register(dto: RegisterDto) { const existing = await this.prisma.user.findUnique({ where: { email: dto.email } }); if (existing) { throw new ConflictException('Email already registered'); } const passwordHash = await bcrypt.hash(dto.password, SALT_ROUNDS); const user = await this.prisma.user.create({ data: { email: dto.email, password: passwordHash, name: dto.name ?? null, }, }); return this.generateTokens(user); } async login(dto: LoginDto) { const user = await this.prisma.user.findUnique({ where: { email: dto.email } }); if (!user) { throw new UnauthorizedException('Invalid email or password'); } const isValid = await bcrypt.compare(dto.password, user.password); if (!isValid) { throw new UnauthorizedException('Invalid email or password'); } return this.generateTokens(user); } async refresh(refreshToken: string) { try { const payload = await this.jwtService.verifyAsync<{ sub: number; jti: string }>( refreshToken, { secret: this.configService.get('app.auth.jwtRefreshSecret'), }, ); const user = await this.prisma.user.findUnique({ where: { id: payload.sub } }); if (!user?.refreshToken) { throw new UnauthorizedException('Invalid refresh token'); } const isValid = await bcrypt.compare(refreshToken, user.refreshToken); if (!isValid) { throw new UnauthorizedException('Invalid refresh token'); } return this.generateTokens(user); } catch { throw new UnauthorizedException('Invalid refresh token'); } } async logout(userId: number) { await this.prisma.user.update({ where: { id: userId }, data: { refreshToken: null }, }); } async getProfile(userId: number) { const user = await this.prisma.user.findUnique({ where: { id: userId } }); if (!user) { throw new UnauthorizedException('User not found'); } return this.sanitizeUser(user); } async updateProfile(userId: number, dto: UpdateProfileDto) { const user = await this.prisma.user.update({ where: { id: userId }, data: { ...(dto.name !== undefined && { name: dto.name }), }, }); return this.sanitizeUser(user); } private async generateTokens(user: { id: number; email: string; name: string | null; role: string; }) { const accessPayload: JwtPayload = { sub: user.id, email: user.email, role: user.role }; const accessToken = await this.jwtService.signAsync(accessPayload, { secret: this.configService.get('app.auth.jwtSecret'), expiresIn: this.configService.get('app.auth.jwtAccessExpires'), }); const jti = crypto.randomUUID(); const refreshPayload = { sub: user.id, jti }; const refreshToken = await this.jwtService.signAsync(refreshPayload, { secret: this.configService.get('app.auth.jwtRefreshSecret'), expiresIn: this.configService.get('app.auth.jwtRefreshExpires'), }); const refreshHash = await bcrypt.hash(refreshToken, SALT_ROUNDS); await this.prisma.user.update({ where: { id: user.id }, data: { refreshToken: refreshHash }, }); return { user: this.sanitizeUser(user), accessToken, refreshToken, }; } private sanitizeUser(user: { id: number; email: string; name: string | null; role: string }) { return { id: user.id, email: user.email, name: user.name, role: user.role, }; } }