elearning/Backend/src/services/auth.service.ts
JakkrapartXD f026c14f0c auth api
2026-01-13 06:49:37 +00:00

340 lines
11 KiB
TypeScript

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,
ResetPasswordResponse,
ChangePasswordResponse,
ResetRequestResponse
} from '../types/auth.types';
import { UnauthorizedError, ValidationError, ForbiddenError } from '../middleware/errorHandler';
import nodemailer from 'nodemailer';
export class AuthService {
/**
* User login
*/
async login(data: LoginRequest): Promise<LoginResponse> {
const { email, password } = data;
// Find user with role and profile
const user = await prisma.user.findUnique({
where: { email },
include: {
role: true,
profile: true
}
});
if (!user) {
logger.warn('Login attempt with invalid email', { email });
throw new UnauthorizedError('Invalid email or password');
}
// Verify password
const isPasswordValid = await bcrypt.compare(password, user.password);
if (!isPasswordValid) {
logger.warn('Login attempt with invalid password', { email });
throw new UnauthorizedError('Invalid email or password');
}
// Generate tokens
const token = this.generateAccessToken(user.id, user.email, user.email, user.role.code);
const refreshToken = this.generateRefreshToken(user.id);
logger.info('User logged in successfully', { userId: user.id, email: user.email });
return {
token,
refreshToken,
user: this.formatUserResponse(user)
};
}
/**
* User registration
*/
async register(data: RegisterRequest): Promise<RegisterResponse> {
const { username, email, password, first_name, last_name, prefix, phone } = 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');
}
// Check if phone number already exists
const existingPhone = await prisma.user.findUnique({
where: { phone }
});
if (existingPhone) {
throw new ValidationError('Phone number 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<RefreshTokenResponse> {
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;
}
}
/**
* Reset request
*/
async resetRequest(email: string): Promise<ResetRequestResponse> {
try {
// Find user
const user = await prisma.user.findUnique({ where: { email } });
if (!user) throw new UnauthorizedError('User not found');
const token = jwt.sign({ id: user.id, email: user.email }, config.jwt.secret, { expiresIn: '1h' });
// Create reset URL
const resetURL = `${process.env.FRONTEND_URL || 'http://localhost:3000'}/reset-password?id=${user.id}&token=${token}`;
// Create transporter
const transporter = nodemailer.createTransport({
host: config.smtp.host,
port: config.smtp.port,
secure: config.smtp.secure,
...(config.smtp.user && config.smtp.pass && {
auth: {
user: config.smtp.user,
pass: config.smtp.pass
}
})
});
// Send email
const mailOptions = {
from: config.smtp.from,
to: user.email,
subject: 'Password Reset Request',
text: `You are receiving this because you (or someone else) have requested the reset of the password for your account.\n\nPlease click on the following link, or paste this into your browser to complete the process:\n\n${resetURL}\n\nIf you did not request this, please ignore this email and your password will remain unchanged.\n`,
html: `
<h2>Password Reset Request</h2>
<p>You are receiving this because you (or someone else) have requested the reset of the password for your account.</p>
<p>Please click on the following link to complete the process:</p>
<p><a href="${resetURL}">${resetURL}</a></p>
<p>If you did not request this, please ignore this email and your password will remain unchanged.</p>
`
};
await transporter.sendMail(mailOptions);
logger.info('Password reset email sent', { email: user.email });
return {
code: 200,
message: 'Password reset email sent successfully'
};
} catch (error) {
logger.error('Failed to send password reset email', { email, error });
throw error;
}
}
async resetPassword(id: number, token: string, password: string): Promise<ResetPasswordResponse> {
try {
const user = await prisma.user.findUnique({ where: { id } });
if (!user) throw new UnauthorizedError('User not found');
const secret = config.jwt.secret;
const verify = jwt.verify(token, secret);
const encryptedPassword = await bcrypt.hash(password, 10);
if (!verify) throw new UnauthorizedError('Invalid token');
await prisma.user.update({
where: { id: user.id },
data: { password: encryptedPassword }
});
logger.info('Password reset successfully', { userId: user.id });
return {
code: 200,
message: 'Password reset successfully'
};
} catch (error) {
logger.error('Failed to reset password', { id, error });
throw error;
}
}
async changePassword(id: number, oldPassword: string, newPassword: string): Promise<ChangePasswordResponse> {
try {
const user = await prisma.user.findUnique({ where: { id } });
if (!user) throw new UnauthorizedError('User not found');
const isPasswordValid = await bcrypt.compare(oldPassword, user.password);
if (!isPasswordValid) throw new UnauthorizedError('Invalid password');
const encryptedPassword = await bcrypt.hash(newPassword, 10);
await prisma.user.update({
where: { id: user.id },
data: { password: encryptedPassword }
});
logger.info('Password changed successfully', { userId: user.id });
return {
code: 200,
message: 'Password changed successfully'
};
} catch (error) {
logger.error('Failed to change password', { id, error });
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
};
}
}