Files
Game_Calendar_BE/src/modules/auth/auth.service.ts
Harun CAN 8674911033
All checks were successful
CI / build (push) Successful in 2m23s
main
2026-01-30 02:52:42 +03:00

337 lines
7.9 KiB
TypeScript

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';
export interface JwtPayload {
sub: string;
email: string;
roles: string[];
permissions: string[];
tenantId?: string;
}
interface UserWithRoles {
id: string;
email: string;
password: string;
firstName: string | null;
lastName: string | null;
isActive: boolean;
tenantId: string | null;
roles: Array<{
role: {
name: string;
permissions: Array<{
permission: {
name: 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,
password: hashedPassword,
firstName: dto.firstName,
lastName: dto.lastName,
roles: {
create: {
role: {
connectOrCreate: {
where: { name: 'user' },
create: { name: 'user', description: 'Default user role' },
},
},
},
},
},
include: {
roles: {
include: {
role: {
include: {
permissions: {
include: {
permission: true,
},
},
},
},
},
},
},
});
return this.generateTokens(user as unknown as UserWithRoles);
}
/**
* 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 },
include: {
roles: {
include: {
role: {
include: {
permissions: {
include: {
permission: true,
},
},
},
},
},
},
},
});
if (!user) {
throw new UnauthorizedException('INVALID_CREDENTIALS');
}
// Verify password
const isPasswordValid = await this.comparePassword(
dto.password,
user.password,
);
if (!isPasswordValid) {
throw new UnauthorizedException('INVALID_CREDENTIALS');
}
if (!user.isActive) {
throw new UnauthorizedException('ACCOUNT_DISABLED');
}
return this.generateTokens(user as unknown as UserWithRoles);
}
/**
* 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: {
include: {
roles: {
include: {
role: {
include: {
permissions: {
include: {
permission: 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 as unknown as UserWithRoles);
}
/**
* 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 },
include: {
roles: {
include: {
role: {
include: {
permissions: {
include: {
permission: true,
},
},
},
},
},
},
},
});
if (!user || !user.isActive) {
return null;
}
// Remove password from user object
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { password: _, ...result } = user;
return result;
}
/**
* Generate access and refresh tokens
*/
private async generateTokens(user: UserWithRoles): Promise<TokenResponseDto> {
// Extract roles and permissions
const roles = user.roles.map((ur) => ur.role.name);
const permissions = user.roles.flatMap((ur) =>
ur.role.permissions.map((rp) => rp.permission.name),
);
const payload: JwtPayload = {
sub: user.id,
email: user.email,
roles,
permissions,
tenantId: user.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,
},
};
}
/**
* 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;
}
}
}