Files
iddaai-be/src/modules/auth/auth.service.ts
T
2026-04-16 17:21:48 +03:00

249 lines
6.0 KiB
TypeScript
Executable File

import {
Injectable,
UnauthorizedException,
ConflictException,
} from "@nestjs/common";
import { JwtService } from "@nestjs/jwt";
import { ConfigService } from "@nestjs/config";
import * as bcrypt from "bcrypt";
import * as crypto from "crypto";
import { PrismaService } from "../../database/prisma.service";
import { RegisterDto, LoginDto, TokenResponseDto } from "./dto/auth.dto";
import { User, UserRole } from "@prisma/client";
export interface JwtPayload {
sub: string;
email: string;
role: string;
tenantId?: string;
}
@Injectable()
export class AuthService {
constructor(
private readonly prisma: PrismaService,
private readonly jwtService: JwtService,
private readonly configService: ConfigService,
) {}
/**
* Register a new user
*/
async register(dto: RegisterDto): Promise<TokenResponseDto> {
// Check if email already exists
const existingUser = await this.prisma.user.findUnique({
where: { email: dto.email },
});
if (existingUser) {
throw new ConflictException("EMAIL_ALREADY_EXISTS");
}
// Hash password
const hashedPassword = await this.hashPassword(dto.password);
// Create user with default role
const user = await this.prisma.user.create({
data: {
email: dto.email,
passwordHash: hashedPassword,
firstName: dto.firstName,
lastName: dto.lastName,
role: UserRole.user,
},
});
// Create usage limit for user
await this.prisma.usageLimit.create({
data: {
userId: user.id,
analysisCount: 0,
couponCount: 0,
lastResetDate: new Date(),
},
});
return this.generateTokens(user);
}
/**
* Login with email and password
*/
async login(dto: LoginDto): Promise<TokenResponseDto> {
// Find user by email
const user = await this.prisma.user.findUnique({
where: { email: dto.email },
});
if (!user) {
throw new UnauthorizedException("INVALID_CREDENTIALS");
}
// Verify password
const isPasswordValid = await this.comparePassword(
dto.password,
user.passwordHash,
);
if (!isPasswordValid) {
throw new UnauthorizedException("INVALID_CREDENTIALS");
}
if (!user.isActive) {
throw new UnauthorizedException("ACCOUNT_DISABLED");
}
return this.generateTokens(user);
}
/**
* Refresh access token using refresh token
*/
async refreshToken(refreshToken: string): Promise<TokenResponseDto> {
// Find refresh token
const storedToken = await this.prisma.refreshToken.findUnique({
where: { token: refreshToken },
include: {
user: true,
},
});
if (!storedToken) {
throw new UnauthorizedException("INVALID_REFRESH_TOKEN");
}
if (storedToken.expiresAt < new Date()) {
// Delete expired token
await this.prisma.refreshToken.delete({
where: { id: storedToken.id },
});
throw new UnauthorizedException("INVALID_REFRESH_TOKEN");
}
// Delete old refresh token
await this.prisma.refreshToken.delete({
where: { id: storedToken.id },
});
return this.generateTokens(storedToken.user);
}
/**
* Logout - invalidate refresh token
*/
async logout(refreshToken: string): Promise<void> {
await this.prisma.refreshToken.deleteMany({
where: { token: refreshToken },
});
}
/**
* Validate user by ID (used by JWT strategy)
*/
async validateUser(userId: string) {
const user = await this.prisma.user.findUnique({
where: { id: userId },
});
if (!user || !user.isActive) {
return null;
}
// Remove password from user object
const { passwordHash: _, ...result } = user;
return result;
}
/**
* Generate access and refresh tokens
*/
private async generateTokens(user: User): Promise<TokenResponseDto> {
const payload: JwtPayload = {
sub: user.id,
email: user.email,
role: user.role,
tenantId: undefined,
};
// Generate access token
const accessToken = this.jwtService.sign(payload, {
expiresIn: this.configService.get("JWT_ACCESS_EXPIRATION", "15m"),
});
// Generate refresh token
const refreshTokenValue = crypto.randomUUID();
const refreshExpiration = this.parseExpiration(
this.configService.get("JWT_REFRESH_EXPIRATION", "7d"),
);
// Store refresh token
await this.prisma.refreshToken.create({
data: {
token: refreshTokenValue,
userId: user.id,
expiresAt: new Date(Date.now() + refreshExpiration),
},
});
return {
accessToken,
refreshToken: refreshTokenValue,
expiresIn:
this.parseExpiration(
this.configService.get("JWT_ACCESS_EXPIRATION", "15m"),
) / 1000, // Convert to seconds
user: {
id: user.id,
email: user.email,
firstName: user.firstName || undefined,
lastName: user.lastName || undefined,
roles: [user.role], // Single role as array for backwards compatibility
},
};
}
/**
* Hash password using bcrypt
*/
private async hashPassword(password: string): Promise<string> {
const saltRounds = 12;
return bcrypt.hash(password, saltRounds);
}
/**
* Compare password with hash
*/
private async comparePassword(
password: string,
hashedPassword: string,
): Promise<boolean> {
return bcrypt.compare(password, hashedPassword);
}
/**
* Parse expiration string to milliseconds
*/
private parseExpiration(expiration: string): number {
const match = expiration.match(/^(\d+)([smhd])$/);
if (!match) {
return 15 * 60 * 1000; // Default 15 minutes
}
const value = parseInt(match[1], 10);
const unit = match[2];
switch (unit) {
case "s":
return value * 1000;
case "m":
return value * 60 * 1000;
case "h":
return value * 60 * 60 * 1000;
case "d":
return value * 24 * 60 * 60 * 1000;
default:
return 15 * 60 * 1000;
}
}
}