Files
backend/src/auth/auth.service.ts
T
2026-08-02 17:21:34 +03:30

150 lines
4.1 KiB
TypeScript

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<string>(
'JWT_ACCESS_EXPIRES_IN',
'15m',
);
const accessToken = await this.jwt.signAsync(
{ sub: user.id, role: user.role },
{
secret: this.config.getOrThrow<string>('JWT_ACCESS_SECRET'),
expiresIn: accessExpiresIn as `${number}${'s' | 'm' | 'h' | 'd'}`,
},
);
const refreshToken = randomBytes(48).toString('hex');
const refreshDays = this.parseDurationDays(
this.config.get<string>('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;
}
}