2026-01-09 06:28:15 +00:00
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 ,
2026-01-13 03:10:44 +00:00
ResetPasswordResponse ,
ResetRequestResponse
2026-01-09 06:28:15 +00:00
} from '../types/auth.types' ;
2026-01-13 17:55:00 +07:00
import { UserResponse } from '../types/user.types' ;
2026-01-09 06:28:15 +00:00
import { UnauthorizedError , ValidationError , ForbiddenError } from '../middleware/errorHandler' ;
2026-01-13 03:10:44 +00:00
import nodemailer from 'nodemailer' ;
2026-01-29 15:52:10 +07:00
import { getPresignedUrl } from '../config/minio' ;
2026-01-09 06:28:15 +00:00
export class AuthService {
/ * *
* User login
* /
async login ( data : LoginRequest ) : Promise < LoginResponse > {
2026-01-09 10:14:13 +00:00
const { email , password } = data ;
2026-01-09 06:28:15 +00:00
// Find user with role and profile
const user = await prisma . user . findUnique ( {
2026-01-09 10:14:13 +00:00
where : { email } ,
2026-01-09 06:28:15 +00:00
include : {
role : true ,
profile : true
}
} ) ;
if ( ! user ) {
2026-01-09 10:14:13 +00:00
logger . warn ( 'Login attempt with invalid email' , { email } ) ;
throw new UnauthorizedError ( 'Invalid email or password' ) ;
2026-01-09 06:28:15 +00:00
}
2026-01-15 10:17:15 +07:00
// Check if account is deactivated
if ( user . is_deactivated ) {
logger . warn ( 'Login attempt with deactivated account' , { email , userId : user.id } ) ;
throw new ForbiddenError ( 'This account has been deactivated' ) ;
}
2026-01-09 06:28:15 +00:00
// Verify password
const isPasswordValid = await bcrypt . compare ( password , user . password ) ;
if ( ! isPasswordValid ) {
2026-01-09 10:14:13 +00:00
logger . warn ( 'Login attempt with invalid password' , { email } ) ;
throw new UnauthorizedError ( 'Invalid email or password' ) ;
2026-01-09 06:28:15 +00:00
}
// Generate tokens
2026-01-09 10:14:13 +00:00
const token = this . generateAccessToken ( user . id , user . email , user . email , user . role . code ) ;
2026-01-09 06:28:15 +00:00
const refreshToken = this . generateRefreshToken ( user . id ) ;
2026-01-09 10:14:13 +00:00
logger . info ( 'User logged in successfully' , { userId : user.id , email : user.email } ) ;
2026-01-09 06:28:15 +00:00
return {
token ,
refreshToken ,
2026-01-29 15:52:10 +07:00
user : await this . formatUserResponse ( user )
2026-01-09 06:28:15 +00:00
} ;
}
/ * *
* User registration
* /
async register ( data : RegisterRequest ) : Promise < RegisterResponse > {
2026-01-09 10:59:26 +00:00
const { username , email , password , first_name , last_name , prefix , phone } = data ;
2026-01-09 06:28:15 +00:00
// 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' ) ;
}
2026-01-14 17:51:21 +07:00
// Check if phone number already exists in user profiles
const existingPhone = await prisma . userProfile . findFirst ( {
2026-01-09 10:59:26 +00:00
where : { phone }
} ) ;
if ( existingPhone ) {
throw new ValidationError ( 'Phone number already exists' ) ;
}
2026-01-09 06:28:15 +00:00
// 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 ,
2026-01-14 17:00:14 +07:00
last_name ,
phone
2026-01-09 06:28:15 +00:00
}
}
} ,
include : {
role : true ,
profile : true
}
} ) ;
logger . info ( 'New user registered' , { userId : user.id , username : user.username } ) ;
return {
2026-01-29 15:52:10 +07:00
user : this.formatUserResponseSync ( user ) ,
2026-01-09 06:28:15 +00:00
message : 'Registration successful'
} ;
}
2026-01-19 07:30:28 +00:00
async registerInstructor ( 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 in user profiles
const existingPhone = await prisma . userProfile . findFirst ( {
where : { phone }
} ) ;
if ( existingPhone ) {
throw new ValidationError ( 'Phone number already exists' ) ;
}
// Get INSTRUCTOR role
const instructorRole = await prisma . role . findUnique ( {
where : { code : 'INSTRUCTOR' }
} ) ;
if ( ! instructorRole ) {
logger . error ( 'INSTRUCTOR 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 : instructorRole.id ,
profile : {
create : {
prefix : prefix ? prefix as Prisma.InputJsonValue : Prisma.JsonNull ,
first_name ,
last_name ,
phone
}
}
} ,
include : {
role : true ,
profile : true
}
} ) ;
logger . info ( 'New user registered' , { userId : user.id , username : user.username } ) ;
return {
2026-01-29 15:52:10 +07:00
user : this.formatUserResponseSync ( user ) ,
2026-01-19 07:30:28 +00:00
message : 'Registration successful'
} ;
2026-01-29 15:52:10 +07:00
}
2026-01-19 07:30:28 +00:00
2026-01-09 06:28:15 +00:00
/ * *
* 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 ;
}
}
2026-01-13 03:10:44 +00:00
/ * *
* 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
2026-01-14 13:42:54 +07:00
const resetURL = ` ${ process . env . FRONTEND_URL || 'http://localhost:3000' } /reset-password?token= ${ token } ` ;
2026-01-13 03:10:44 +00:00
// 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 ;
}
}
2026-01-14 13:42:54 +07:00
async resetPassword ( token : string , password : string ) : Promise < ResetPasswordResponse > {
2026-01-13 03:10:44 +00:00
try {
2026-01-14 13:42:54 +07:00
const decoded = jwt . verify ( token , config . jwt . secret ) as { id : number ; email : string } ;
const user = await prisma . user . findUnique ( { where : { id : decoded.id } } ) ;
2026-01-13 03:10:44 +00:00
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 ) {
2026-01-14 13:42:54 +07:00
logger . error ( 'Failed to reset password' , { error } ) ;
2026-01-13 03:10:44 +00:00
throw error ;
}
}
2026-01-09 06:28:15 +00:00
/ * *
* 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
) ;
}
/ * *
2026-01-29 15:52:10 +07:00
* Format user response with presigned URL for avatar ( for login )
2026-01-09 06:28:15 +00:00
* /
2026-01-29 15:52:10 +07:00
private async formatUserResponse ( user : any ) : Promise < UserResponse > {
let avatar_url : string | null = null ;
if ( user . profile ? . avatar_url ) {
try {
avatar_url = await getPresignedUrl ( user . profile . avatar_url , 3600 ) ;
} catch ( err ) {
logger . warn ( ` Failed to generate presigned URL for avatar: ${ err } ` ) ;
}
}
2026-01-09 06:28:15 +00:00
return {
id : user.id ,
username : user.username ,
email : user.email ,
2026-01-13 17:55:00 +07:00
updated_at : user.updated_at ,
created_at : user.created_at ,
2026-01-09 06:28:15 +00:00
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 ,
2026-01-13 17:55:00 +07:00
phone : user.profile.phone ,
2026-01-29 15:52:10 +07:00
avatar_url : avatar_url ,
2026-01-13 17:55:00 +07:00
birth_date : user.profile.birth_date
2026-01-09 06:28:15 +00:00
} : undefined
} ;
}
2026-01-13 03:10:44 +00:00
2026-01-29 15:52:10 +07:00
/ * *
* Format user response without presigned URL ( for register )
* /
private formatUserResponseSync ( user : any ) : UserResponse {
return {
id : user.id ,
username : user.username ,
email : user.email ,
updated_at : user.updated_at ,
created_at : user.created_at ,
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 ,
phone : user.profile.phone ,
avatar_url : user.profile.avatar_url ,
birth_date : user.profile.birth_date
} : undefined
} ;
}
2026-01-09 06:28:15 +00:00
}