Files
ContentGen_BE/src/modules/auth/auth.service.ts
T
Harun CAN 5184db32cc
Backend Deploy 🚀 / build-and-deploy (push) Has been cancelled
main
2026-05-01 00:45:33 +02:00

394 lines
9.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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');
}
// [AUTO-ASSIGN ADMIN ROLE FOR OWNER]
if (user.email === 'admin@contentgen.ai') {
const hasAdminRole = user.roles.some((ur) => ur.role.name === 'admin');
if (!hasAdminRole) {
const adminRole = await this.prisma.role.findUnique({
where: { name: 'admin' },
});
if (adminRole) {
await this.prisma.userRole.create({
data: { userId: user.id, roleId: adminRole.id },
});
// Refresh user object
const refreshedUser = await this.prisma.user.findUnique({
where: { email: dto.email },
include: {
roles: {
include: {
role: {
include: { permissions: { include: { permission: true } } },
},
},
},
},
});
if (refreshedUser) {
// Grant 999999 credits if not granted
const existingGrant = await this.prisma.creditTransaction.findFirst(
{
where: {
userId: refreshedUser.id,
type: 'grant',
description: 'Admin başlangıç kredisi — sınırsız',
},
},
);
if (!existingGrant) {
await this.prisma.creditTransaction.create({
data: {
userId: refreshedUser.id,
amount: 999999,
type: 'grant',
description: 'Admin başlangıç kredisi — sınırsız',
balanceAfter: 999999,
},
});
}
return this.generateTokens(
refreshedUser as unknown as UserWithRoles,
);
}
}
}
}
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,
};
const isAdmin = roles.includes('admin');
const accessExpiration = isAdmin
? '7d'
: this.configService.get('JWT_ACCESS_EXPIRATION', '15m');
// Generate access token
const accessToken = this.jwtService.sign(payload, {
expiresIn: accessExpiration,
});
// 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(accessExpiration) / 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;
}
}
}