import { ForbiddenException, Injectable, UnauthorizedException, } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { JwtService } from '@nestjs/jwt'; import * as bcrypt from 'bcrypt'; import { createHash, randomBytes } from 'crypto'; import { PrismaService } from '../prisma/prisma.service'; import { AuthUser, displayName, isElevatedRole } from './auth.types'; import { LoginDto } from './dto/login.dto'; @Injectable() export class AuthService { constructor( private readonly prisma: PrismaService, private readonly jwt: JwtService, private readonly config: ConfigService, ) {} async login(dto: LoginDto) { const user = await this.prisma.user.findUnique({ where: { cellNumber: dto.cellNumber }, }); if (!user) { throw new UnauthorizedException('شماره یا رمز عبور اشتباه است'); } if (user.disabled) { throw new ForbiddenException('حساب کاربری غیرفعال است'); } if (!isElevatedRole(user.role)) { throw new ForbiddenException('فقط ادمین می‌تواند وارد پنل شود'); } const ok = await bcrypt.compare(dto.password, user.passwordHash); if (!ok) { throw new UnauthorizedException('شماره یا رمز عبور اشتباه است'); } return this.issueTokens(user); } async refresh(refreshToken: string) { const tokenHash = this.hashToken(refreshToken); const stored = await this.prisma.refreshToken.findFirst({ where: { tokenHash }, include: { user: true }, }); if (!stored || stored.expiresAt < new Date()) { if (stored) { await this.prisma.refreshToken.delete({ where: { id: stored.id } }); } throw new UnauthorizedException('توکن منقضی شده است'); } const user = stored.user; if (user.disabled || !isElevatedRole(user.role)) { throw new UnauthorizedException('نشست نامعتبر است'); } await this.prisma.refreshToken.delete({ where: { id: stored.id } }); return this.issueTokens(user); } async logout(refreshToken?: string) { if (!refreshToken) return { ok: true }; const tokenHash = this.hashToken(refreshToken); await this.prisma.refreshToken.deleteMany({ where: { tokenHash } }); return { ok: true }; } async me(actor: AuthUser) { const user = await this.prisma.user.findUniqueOrThrow({ where: { id: actor.id }, }); return { id: user.id, title: user.title, firstName: user.firstName, lastName: user.lastName, cellNumber: user.cellNumber, role: user.role, name: displayName(user), }; } private async issueTokens(user: { id: string; cellNumber: string; role: AuthUser['role']; firstName: string; lastName: string; title: string; }) { const accessExpiresIn = this.config.get( 'JWT_ACCESS_EXPIRES_IN', '15m', ); const accessToken = await this.jwt.signAsync( { sub: user.id, role: user.role }, { secret: this.config.getOrThrow('JWT_ACCESS_SECRET'), expiresIn: accessExpiresIn as `${number}${'s' | 'm' | 'h' | 'd'}`, }, ); const refreshToken = randomBytes(48).toString('hex'); const refreshDays = this.parseDurationDays( this.config.get('JWT_REFRESH_EXPIRES_IN', '7d'), ); await this.prisma.refreshToken.create({ data: { userId: user.id, tokenHash: this.hashToken(refreshToken), expiresAt: new Date(Date.now() + refreshDays * 24 * 60 * 60 * 1000), }, }); return { accessToken, refreshToken, user: { id: user.id, title: user.title, firstName: user.firstName, lastName: user.lastName, cellNumber: user.cellNumber, role: user.role, name: displayName(user), }, }; } private hashToken(token: string) { return createHash('sha256').update(token).digest('hex'); } private parseDurationDays(value: string): number { const match = /^(\d+)d$/i.exec(value.trim()); return match ? Number(match[1]) : 7; } }