mirror of
https://git.meshkee.com/BaloutPastry/backend.git
synced 2026-08-11 22:31:00 +04:30
Initial commit of Balout Pastry NestJS API.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,149 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user