import { prisma } from '../config/database'; import { Prisma } from '@prisma/client'; import { config } from '../config'; import { logger } from '../config/logger'; import bcrypt from 'bcrypt'; import jwt from 'jsonwebtoken'; import { LoginRequest, RegisterRequest, LoginResponse, RegisterResponse, RefreshTokenResponse, UserResponse } from '../types/auth.types'; import { UnauthorizedError, ValidationError, ForbiddenError } from '../middleware/errorHandler'; export class AuthService { /** * User login */ async login(data: LoginRequest): Promise { const { username, password } = data; // Find user with role and profile const user = await prisma.user.findUnique({ where: { username }, include: { role: true, profile: true } }); if (!user) { logger.warn('Login attempt with invalid username', { username }); throw new UnauthorizedError('Invalid username or password'); } // Verify password const isPasswordValid = await bcrypt.compare(password, user.password); if (!isPasswordValid) { logger.warn('Login attempt with invalid password', { username }); throw new UnauthorizedError('Invalid username or password'); } // Generate tokens const token = this.generateAccessToken(user.id, user.username, user.email, user.role.code); const refreshToken = this.generateRefreshToken(user.id); logger.info('User logged in successfully', { userId: user.id, username: user.username }); return { token, refreshToken, user: this.formatUserResponse(user) }; } /** * User registration */ async register(data: RegisterRequest): Promise { const { username, email, password, first_name, last_name, prefix } = data; // Check if username already exists const existingUsername = await prisma.user.findUnique({ where: { username } }); if (existingUsername) { throw new ValidationError('Username already exists'); } // Check if email already exists const existingEmail = await prisma.user.findUnique({ where: { email } }); if (existingEmail) { throw new ValidationError('Email already exists'); } // Get STUDENT role const studentRole = await prisma.role.findUnique({ where: { code: 'STUDENT' } }); if (!studentRole) { logger.error('STUDENT role not found in database'); throw new Error('System configuration error'); } // Hash password const hashedPassword = await bcrypt.hash(password, 10); // Create user with profile const user = await prisma.user.create({ data: { username, email, password: hashedPassword, role_id: studentRole.id, profile: { create: { prefix: prefix ? prefix as Prisma.InputJsonValue : Prisma.JsonNull, first_name, last_name } } }, include: { role: true, profile: true } }); logger.info('New user registered', { userId: user.id, username: user.username }); return { user: this.formatUserResponse(user), message: 'Registration successful' }; } /** * Refresh access token */ async refreshToken(refreshToken: string): Promise { try { // Verify refresh token const decoded = jwt.verify(refreshToken, config.jwt.secret) as { id: number; type: string }; if (decoded.type !== 'refresh') { throw new UnauthorizedError('Invalid token type'); } // Get user const user = await prisma.user.findUnique({ where: { id: decoded.id }, include: { role: true } }); if (!user) { throw new UnauthorizedError('User not found'); } // Generate new tokens const newToken = this.generateAccessToken(user.id, user.username, user.email, user.role.code); const newRefreshToken = this.generateRefreshToken(user.id); logger.info('Token refreshed', { userId: user.id }); return { token: newToken, refreshToken: newRefreshToken }; } catch (error) { if (error instanceof jwt.JsonWebTokenError) { throw new UnauthorizedError('Invalid refresh token'); } if (error instanceof jwt.TokenExpiredError) { throw new UnauthorizedError('Refresh token expired'); } throw error; } } /** * Generate access token (JWT) */ private generateAccessToken(id: number, username: string, email: string, roleCode: string): string { return jwt.sign( { id, username, email, roleCode }, config.jwt.secret, { expiresIn: config.jwt.expiresIn } as jwt.SignOptions ); } /** * Generate refresh token */ private generateRefreshToken(id: number): string { return jwt.sign( { id, type: 'refresh' }, config.jwt.secret, { expiresIn: config.jwt.refreshExpiresIn } as jwt.SignOptions ); } /** * Format user response */ private formatUserResponse(user: any): UserResponse { return { id: user.id, username: user.username, email: user.email, role: { code: user.role.code, name: user.role.name }, profile: user.profile ? { prefix: user.profile.prefix, first_name: user.profile.first_name, last_name: user.profile.last_name, avatar_url: user.profile.avatar_url } : undefined }; } }