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' ;
2026-02-10 15:26:39 +07:00
import bcrypt from '@node-rs/bcrypt' ;
2026-01-09 06:28:15 +00:00
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' ;
feat: integrate audit logging across authentication, course management, and user operations
Add comprehensive audit trail tracking by integrating auditService throughout the application. Track user authentication (LOGIN, REGISTER), course lifecycle (CREATE, APPROVE_COURSE, REJECT_COURSE, ENROLL), content management (CREATE/DELETE Chapter/Lesson), file operations (UPLOAD_FILE, DELETE_FILE for videos and attachments), password management (CHANGE_PASSWORD, RESET_PASSWORD), user role updates (UPDATE
2026-02-05 17:35:37 +07:00
import { auditService } from './audit.service' ;
import { AuditAction } from '@prisma/client' ;
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
feat: integrate audit logging across authentication, course management, and user operations
Add comprehensive audit trail tracking by integrating auditService throughout the application. Track user authentication (LOGIN, REGISTER), course lifecycle (CREATE, APPROVE_COURSE, REJECT_COURSE, ENROLL), content management (CREATE/DELETE Chapter/Lesson), file operations (UPLOAD_FILE, DELETE_FILE for videos and attachments), password management (CHANGE_PASSWORD, RESET_PASSWORD), user role updates (UPDATE
2026-02-05 17:35:37 +07:00
// Audit log - LOGIN
auditService . log ( {
userId : user.id ,
action : AuditAction.LOGIN ,
entityType : 'User' ,
entityId : user.id ,
metadata : { email : user.email , role : user.role.code }
} ) ;
2026-01-09 06:28:15 +00:00
return {
2026-01-30 17:54:43 +07:00
code : 200 ,
message : 'Login successful' ,
data : {
token ,
refreshToken ,
user : await this . formatUserResponse ( user )
}
2026-01-09 06:28:15 +00:00
} ;
}
/ * *
* User registration
* /
async register ( data : RegisterRequest ) : Promise < RegisterResponse > {
2026-02-12 17:55:45 +07:00
try {
const { username , email , password , first_name , last_name , prefix , phone } = data ;
2026-01-09 06:28:15 +00:00
2026-02-12 17:55:45 +07:00
// Check if username already exists
const existingUsername = await prisma . user . findUnique ( {
where : { username }
} ) ;
2026-01-09 06:28:15 +00:00
2026-02-12 17:55:45 +07:00
if ( existingUsername ) {
throw new ValidationError ( 'Username already exists' ) ;
}
2026-01-09 06:28:15 +00:00
2026-02-12 17:55:45 +07:00
// Check if email already exists
const existingEmail = await prisma . user . findUnique ( {
where : { email }
} ) ;
2026-01-09 06:28:15 +00:00
2026-02-12 17:55:45 +07:00
if ( existingEmail ) {
throw new ValidationError ( 'Email already exists' ) ;
}
2026-01-09 10:59:26 +00:00
2026-02-12 17:55:45 +07:00
// Check if phone number already exists in user profiles
const existingPhone = await prisma . userProfile . findFirst ( {
where : { phone }
} ) ;
2026-01-09 10:59:26 +00:00
2026-02-12 17:55:45 +07:00
if ( existingPhone ) {
throw new ValidationError ( 'Phone number already exists' ) ;
}
2026-01-09 06:28:15 +00:00
2026-02-12 17:55:45 +07:00
// Get STUDENT role
const studentRole = await prisma . role . findUnique ( {
where : { code : 'STUDENT' }
} ) ;
2026-01-09 06:28:15 +00:00
2026-02-12 17:55:45 +07:00
if ( ! studentRole ) {
logger . error ( 'STUDENT role not found in database' ) ;
throw new Error ( 'System configuration error' ) ;
}
2026-01-09 06:28:15 +00:00
2026-02-12 17:55:45 +07:00
// 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 ,
phone
}
2026-01-09 06:28:15 +00:00
}
2026-02-12 17:55:45 +07:00
} ,
include : {
role : true ,
profile : true
2026-01-09 06:28:15 +00:00
}
2026-02-12 17:55:45 +07:00
} ) ;
logger . info ( 'New user registered' , { userId : user.id , username : user.username } ) ;
// Audit log - REGISTER (Student)
auditService . log ( {
userId : user.id ,
action : AuditAction.CREATE ,
entityType : 'User' ,
entityId : user.id ,
newValue : { username : user.username , email : user.email , role : 'STUDENT' }
} ) ;
return {
user : this.formatUserResponseSync ( user ) ,
message : 'Registration successful'
} ;
} catch ( error ) {
logger . error ( 'Failed to register user' , { error } ) ;
await auditService . logSync ( {
userId : 0 ,
action : AuditAction.ERROR ,
entityType : 'User' ,
entityId : 0 ,
metadata : {
operation : 'register_user' ,
email : data.email ,
username : data.username ,
error : error instanceof Error ? error.message : String ( error )
}
} ) ;
throw error ;
}
2026-01-09 06:28:15 +00:00
}
2026-01-19 07:30:28 +00:00
async registerInstructor ( data : RegisterRequest ) : Promise < RegisterResponse > {
2026-02-12 17:55:45 +07:00
try {
const { username , email , password , first_name , last_name , prefix , phone } = data ;
2026-01-19 07:30:28 +00:00
2026-02-12 17:55:45 +07:00
// Check if username already exists
const existingUsername = await prisma . user . findUnique ( {
where : { username }
} ) ;
2026-01-19 07:30:28 +00:00
2026-02-12 17:55:45 +07:00
if ( existingUsername ) {
throw new ValidationError ( 'Username already exists' ) ;
}
2026-01-19 07:30:28 +00:00
2026-02-12 17:55:45 +07:00
// Check if email already exists
const existingEmail = await prisma . user . findUnique ( {
where : { email }
} ) ;
2026-01-19 07:30:28 +00:00
2026-02-12 17:55:45 +07:00
if ( existingEmail ) {
throw new ValidationError ( 'Email already exists' ) ;
}
2026-01-19 07:30:28 +00:00
2026-02-12 17:55:45 +07:00
// Check if phone number already exists in user profiles
const existingPhone = await prisma . userProfile . findFirst ( {
where : { phone }
} ) ;
2026-01-19 07:30:28 +00:00
2026-02-12 17:55:45 +07:00
if ( existingPhone ) {
throw new ValidationError ( 'Phone number already exists' ) ;
}
2026-01-19 07:30:28 +00:00
2026-02-12 17:55:45 +07:00
// Get INSTRUCTOR role
const instructorRole = await prisma . role . findUnique ( {
where : { code : 'INSTRUCTOR' }
} ) ;
2026-01-19 07:30:28 +00:00
2026-02-12 17:55:45 +07:00
if ( ! instructorRole ) {
logger . error ( 'INSTRUCTOR role not found in database' ) ;
throw new Error ( 'System configuration error' ) ;
}
2026-01-19 07:30:28 +00:00
2026-02-12 17:55:45 +07:00
// 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
}
2026-01-19 07:30:28 +00:00
}
2026-02-12 17:55:45 +07:00
} ,
include : {
role : true ,
profile : true
2026-01-19 07:30:28 +00:00
}
2026-02-12 17:55:45 +07:00
} ) ;
logger . info ( 'New user registered' , { userId : user.id , username : user.username } ) ;
// Audit log - REGISTER (Instructor)
auditService . log ( {
userId : user.id ,
action : AuditAction.CREATE ,
entityType : 'User' ,
entityId : user.id ,
newValue : { username : user.username , email : user.email , role : 'INSTRUCTOR' }
} ) ;
return {
user : this.formatUserResponseSync ( user ) ,
message : 'Registration successful'
} ;
} catch ( error ) {
logger . error ( 'Failed to register instructor' , { error } ) ;
await auditService . logSync ( {
userId : 0 ,
action : AuditAction.ERROR ,
entityType : 'User' ,
entityId : 0 ,
metadata : {
operation : 'register_instructor' ,
email : data.email ,
username : data.username ,
error : error instanceof Error ? error.message : String ( error )
}
} ) ;
throw error ;
}
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 {
2026-01-30 17:29:08 +07:00
// Find user with role
2026-02-10 15:26:39 +07:00
const user = await prisma . user . findUnique ( {
2026-01-30 17:29:08 +07:00
where : { email } ,
include : { role : true }
} ) ;
2026-01-13 03:10:44 +00:00
if ( ! user ) throw new UnauthorizedError ( 'User not found' ) ;
const token = jwt . sign ( { id : user.id , email : user.email } , config . jwt . secret , { expiresIn : '1h' } ) ;
2026-01-30 17:29:08 +07:00
// Create reset URL based on role
const isInstructor = user . role . code === 'INSTRUCTOR' ;
2026-02-10 15:26:39 +07:00
const baseUrl = isInstructor
2026-01-30 17:29:08 +07:00
? ( process . env . FRONTEND_URL_INSTRUCTOR || 'http://localhost:3001' )
: ( process . env . FRONTEND_URL_STUDENT || 'http://localhost:3000' ) ;
const resetURL = ` ${ baseUrl } /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 } ) ;
feat: integrate audit logging across authentication, course management, and user operations
Add comprehensive audit trail tracking by integrating auditService throughout the application. Track user authentication (LOGIN, REGISTER), course lifecycle (CREATE, APPROVE_COURSE, REJECT_COURSE, ENROLL), content management (CREATE/DELETE Chapter/Lesson), file operations (UPLOAD_FILE, DELETE_FILE for videos and attachments), password management (CHANGE_PASSWORD, RESET_PASSWORD), user role updates (UPDATE
2026-02-05 17:35:37 +07:00
// Audit log - RESET_PASSWORD
auditService . log ( {
userId : user.id ,
action : AuditAction.RESET_PASSWORD ,
entityType : 'User' ,
entityId : user.id ,
metadata : { email : user.email }
} ) ;
2026-01-13 03:10:44 +00:00
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-02-03 10:38:59 +07:00
email_verified_at : user.email_verified_at ,
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 ,
2026-02-03 10:38:59 +07:00
email_verified_at : user.email_verified_at ,
2026-01-29 15:52:10 +07:00
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
}