feat: Add token-based authorization to category deletion and enhance user registration with error handling and audit logging.
All checks were successful
Build and Deploy Backend / Build Backend Docker Image (push) Successful in 29s
Build and Deploy Backend / Deploy E-learning Backend to Dev Server (push) Successful in 4s
Build and Deploy Backend / Notify Deployment Status (push) Successful in 2s

This commit is contained in:
JakkrapartXD 2026-02-12 17:55:45 +07:00
parent 11f9ad57cd
commit bb38c0f3c9
16 changed files with 1202 additions and 237 deletions

View file

@ -634,6 +634,8 @@ enum AuditAction {
VERIFY_EMAIL
DEACTIVATE_USER
ACTIVATE_USER
ERROR
WARNING
}
model AuditLog {

View file

@ -25,10 +25,8 @@ export class AdminCourseApprovalController {
@Response('403', 'Forbidden - Admin only')
public async listPendingCourses(@Request() request: any): Promise<ListPendingCoursesResponse> {
const token = request.headers.authorization?.replace('Bearer ', '');
if (!token) {
throw new ValidationError('No token provided');
}
return await AdminCourseApprovalService.listPendingCourses();
if (!token) throw new ValidationError('No token provided');
return await AdminCourseApprovalService.listPendingCourses(token);
}
/**
@ -44,10 +42,8 @@ export class AdminCourseApprovalController {
@Response('404', 'Course not found')
public async getCourseDetail(@Request() request: any, @Path() courseId: number): Promise<GetCourseDetailForAdminResponse> {
const token = request.headers.authorization?.replace('Bearer ', '');
if (!token) {
throw new ValidationError('No token provided');
}
return await AdminCourseApprovalService.getCourseDetail(courseId);
if (!token) throw new ValidationError('No token provided');
return await AdminCourseApprovalService.getCourseDetail(token, courseId);
}
/**
@ -68,9 +64,7 @@ export class AdminCourseApprovalController {
@Body() body?: ApproveCourseBody
): Promise<ApproveCourseResponse> {
const token = request.headers.authorization?.replace('Bearer ', '');
if (!token) {
throw new ValidationError('No token provided');
}
if (!token) throw new ValidationError('No token provided');
return await AdminCourseApprovalService.approveCourse(token, courseId, body?.comment);
}
@ -92,9 +86,7 @@ export class AdminCourseApprovalController {
@Body() body: RejectCourseBody
): Promise<RejectCourseResponse> {
const token = request.headers.authorization?.replace('Bearer ', '');
if (!token) {
throw new ValidationError('No token provided');
}
if (!token) throw new ValidationError('No token provided');
return await AdminCourseApprovalService.rejectCourse(token, courseId, body.comment);
}
}

View file

@ -45,6 +45,6 @@ export class CategoriesAdminController {
@Response('401', 'Invalid or expired token')
public async deleteCategory(@Request() request: any, @Path() id: number): Promise<deleteCategoryResponse> {
const token = request.headers.authorization?.replace('Bearer ', '') || '';
return await this.categoryService.deleteCategory(id);
return await this.categoryService.deleteCategory(token,id);
}
}

View file

@ -22,10 +22,8 @@ export class RecommendedCoursesController {
@Response('403', 'Forbidden - Admin only')
public async listApprovedCourses(@Request() request: any): Promise<ListApprovedCoursesResponse> {
const token = request.headers.authorization?.replace('Bearer ', '');
if (!token) {
throw new ValidationError('No token provided');
}
return await RecommendedCoursesService.listApprovedCourses();
if (!token) throw new ValidationError('No token provided');
return await RecommendedCoursesService.listApprovedCourses(token);
}
/**
@ -42,10 +40,8 @@ export class RecommendedCoursesController {
@Response('404', 'Course not found')
public async getCourseById(@Request() request: any, @Path() courseId: number): Promise<GetCourseByIdResponse> {
const token = request.headers.authorization?.replace('Bearer ', '');
if (!token) {
throw new ValidationError('No token provided');
}
return await RecommendedCoursesService.getCourseById(courseId);
if (!token) throw new ValidationError('No token provided');
return await RecommendedCoursesService.getCourseById(token, courseId);
}
/**
@ -66,9 +62,7 @@ export class RecommendedCoursesController {
@Query() is_recommended: boolean
): Promise<ToggleRecommendedResponse> {
const token = request.headers.authorization?.replace('Bearer ', '');
if (!token) {
throw new ValidationError('No token provided');
}
if (!token) throw new ValidationError('No token provided');
return await RecommendedCoursesService.toggleRecommended(token, courseId, is_recommended);
}
}

View file

@ -18,7 +18,7 @@ export class AdminCourseApprovalService {
/**
* Get all pending courses for admin review
*/
static async listPendingCourses(): Promise<ListPendingCoursesResponse> {
static async listPendingCourses(token: string): Promise<ListPendingCoursesResponse> {
try {
const courses = await prisma.course.findMany({
where: { status: 'PENDING' },
@ -96,6 +96,16 @@ export class AdminCourseApprovalService {
};
} catch (error) {
logger.error('Failed to list pending courses', { error });
const decoded = jwt.decode(token) as { id: number } | null;
await auditService.logSync({
userId: decoded?.id || 0,
action: AuditAction.ERROR,
entityType: 'Course',
entityId: 0,
metadata: {
error: error instanceof Error ? error.message : String(error)
}
});
throw error;
}
}
@ -103,7 +113,7 @@ export class AdminCourseApprovalService {
/**
* Get course details for admin review
*/
static async getCourseDetail(courseId: number): Promise<GetCourseDetailForAdminResponse> {
static async getCourseDetail(token: string,courseId: number): Promise<GetCourseDetailForAdminResponse> {
try {
const course = await prisma.course.findUnique({
where: { id: courseId },
@ -214,6 +224,16 @@ export class AdminCourseApprovalService {
};
} catch (error) {
logger.error('Failed to get course detail', { error });
const decoded = jwt.decode(token) as { id: number } | null;
await auditService.logSync({
userId: decoded?.id || 0,
action: AuditAction.ERROR,
entityType: 'Course',
entityId: courseId,
metadata: {
error: error instanceof Error ? error.message : String(error)
}
});
throw error;
}
}
@ -275,6 +295,17 @@ export class AdminCourseApprovalService {
};
} catch (error) {
logger.error('Failed to approve course', { error });
const decoded = jwt.decode(token) as { id: number } | null;
await auditService.logSync({
userId: decoded?.id || 0,
action: AuditAction.ERROR,
entityType: 'Course',
entityId: courseId,
metadata: {
operation: 'approve_course',
error: error instanceof Error ? error.message : String(error)
}
});
throw error;
}
}
@ -341,6 +372,17 @@ export class AdminCourseApprovalService {
};
} catch (error) {
logger.error('Failed to reject course', { error });
const decoded = jwt.decode(token) as { id: number } | null;
await auditService.logSync({
userId: decoded?.id || 0,
action: AuditAction.ERROR,
entityType: 'Course',
entityId: courseId,
metadata: {
operation: 'reject_course',
error: error instanceof Error ? error.message : String(error)
}
});
throw error;
}
}

View file

@ -142,6 +142,17 @@ export class ChaptersLessonService {
return { code: 200, message: 'Chapter created successfully', data: chapter as ChapterData };
} catch (error) {
logger.error(`Error creating chapter: ${error}`);
const decodedToken = jwt.decode(request.token) as { id: number } | null;
await auditService.logSync({
userId: decodedToken?.id || 0,
action: AuditAction.ERROR,
entityType: 'Chapter',
entityId: 0,
metadata: {
operation: 'create_chapter',
error: error instanceof Error ? error.message : String(error)
}
});
throw error;
}
}
@ -163,6 +174,17 @@ export class ChaptersLessonService {
return { code: 200, message: 'Chapter updated successfully', data: chapter as ChapterData };
} catch (error) {
logger.error(`Error updating chapter: ${error}`);
const decodedToken = jwt.decode(request.token) as { id: number } | null;
await auditService.logSync({
userId: decodedToken?.id || 0,
action: AuditAction.ERROR,
entityType: 'Chapter',
entityId: request.chapter_id,
metadata: {
operation: 'update_chapter',
error: error instanceof Error ? error.message : String(error)
}
});
throw error;
}
}
@ -197,6 +219,17 @@ export class ChaptersLessonService {
return { code: 200, message: 'Chapter deleted successfully' };
} catch (error) {
logger.error(`Error deleting chapter: ${error}`);
const decodedToken = jwt.decode(request.token) as { id: number } | null;
await auditService.logSync({
userId: decodedToken?.id || 0,
action: AuditAction.ERROR,
entityType: 'Chapter',
entityId: request.chapter_id,
metadata: {
operation: 'delete_chapter',
error: error instanceof Error ? error.message : String(error)
}
});
throw error;
}
}
@ -280,6 +313,17 @@ export class ChaptersLessonService {
return { code: 200, message: 'Chapter reordered successfully', data: chapters as ChapterData[] };
} catch (error) {
logger.error(`Error reordering chapter: ${error}`);
const decodedToken = jwt.decode(request.token) as { id: number } | null;
await auditService.logSync({
userId: decodedToken?.id || 0,
action: AuditAction.ERROR,
entityType: 'Chapter',
entityId: request.chapter_id,
metadata: {
operation: 'reorder_chapter',
error: error instanceof Error ? error.message : String(error)
}
});
throw error;
}
}
@ -354,6 +398,17 @@ export class ChaptersLessonService {
return { code: 200, message: 'Lesson created successfully', data: lesson as LessonData };
} catch (error) {
logger.error(`Error creating lesson: ${error}`);
const decodedToken = jwt.decode(request.token) as { id: number } | null;
await auditService.logSync({
userId: decodedToken?.id || 0,
action: AuditAction.ERROR,
entityType: 'Lesson',
entityId: 0,
metadata: {
operation: 'create_lesson',
error: error instanceof Error ? error.message : String(error)
}
});
throw error;
}
}
@ -494,6 +549,17 @@ export class ChaptersLessonService {
return { code: 200, message: 'Lesson fetched successfully', data: lessonData as LessonData };
} catch (error) {
logger.error(`Error fetching lesson: ${error}`);
const decodedToken = jwt.decode(request.token) as { id: number } | null;
await auditService.logSync({
userId: decodedToken?.id || 0,
action: AuditAction.ERROR,
entityType: 'Lesson',
entityId: request.lesson_id,
metadata: {
operation: 'get_lesson',
error: error instanceof Error ? error.message : String(error)
}
});
throw error;
}
}
@ -515,6 +581,17 @@ export class ChaptersLessonService {
return { code: 200, message: 'Lesson updated successfully', data: lesson as LessonData };
} catch (error) {
logger.error(`Error updating lesson: ${error}`);
const decodedToken = jwt.decode(request.token) as { id: number } | null;
await auditService.logSync({
userId: decodedToken?.id || 0,
action: AuditAction.ERROR,
entityType: 'Lesson',
entityId: request.lesson_id,
metadata: {
operation: 'update_lesson',
error: error instanceof Error ? error.message : String(error)
}
});
throw error;
}
}
@ -605,6 +682,17 @@ export class ChaptersLessonService {
return { code: 200, message: 'Lessons reordered successfully', data: lessons as LessonData[] };
} catch (error) {
logger.error(`Error reordering lessons: ${error}`);
const decodedToken = jwt.decode(request.token) as { id: number } | null;
await auditService.logSync({
userId: decodedToken?.id || 0,
action: AuditAction.ERROR,
entityType: 'Lesson',
entityId: request.lesson_id,
metadata: {
operation: 'reorder_lessons',
error: error instanceof Error ? error.message : String(error)
}
});
throw error;
}
}
@ -676,6 +764,17 @@ export class ChaptersLessonService {
return { code: 200, message: 'Lesson deleted successfully' };
} catch (error) {
logger.error(`Error deleting lesson: ${error}`);
const decodedToken = jwt.decode(request.token) as { id: number } | null;
await auditService.logSync({
userId: decodedToken?.id || 0,
action: AuditAction.ERROR,
entityType: 'Lesson',
entityId: request.lesson_id,
metadata: {
operation: 'delete_lesson',
error: error instanceof Error ? error.message : String(error)
}
});
throw error;
}
}
@ -754,6 +853,17 @@ export class ChaptersLessonService {
};
} catch (error) {
logger.error(`Error uploading video: ${error}`);
const decodedToken = jwt.decode(request.token) as { id: number } | null;
await auditService.logSync({
userId: decodedToken?.id || 0,
action: AuditAction.ERROR,
entityType: 'Lesson',
entityId: request.lesson_id,
metadata: {
operation: 'upload_video',
error: error instanceof Error ? error.message : String(error)
}
});
throw error;
}
}
@ -836,6 +946,17 @@ export class ChaptersLessonService {
};
} catch (error) {
logger.error(`Error updating video: ${error}`);
const decodedToken = jwt.decode(request.token) as { id: number } | null;
await auditService.logSync({
userId: decodedToken?.id || 0,
action: AuditAction.ERROR,
entityType: 'Lesson',
entityId: request.lesson_id,
metadata: {
operation: 'update_video',
error: error instanceof Error ? error.message : String(error)
}
});
throw error;
}
}
@ -917,6 +1038,17 @@ export class ChaptersLessonService {
};
} catch (error) {
logger.error(`Error setting YouTube video: ${error}`);
const decodedToken = jwt.decode(request.token) as { id: number } | null;
await auditService.logSync({
userId: decodedToken?.id || 0,
action: AuditAction.ERROR,
entityType: 'Lesson',
entityId: request.lesson_id,
metadata: {
operation: 'set_youtube_video',
error: error instanceof Error ? error.message : String(error)
}
});
throw error;
}
}
@ -993,6 +1125,17 @@ export class ChaptersLessonService {
};
} catch (error) {
logger.error(`Error uploading attachment: ${error}`);
const decodedToken = jwt.decode(request.token) as { id: number } | null;
await auditService.logSync({
userId: decodedToken?.id || 0,
action: AuditAction.ERROR,
entityType: 'LessonAttachment',
entityId: request.lesson_id,
metadata: {
operation: 'upload_attachment',
error: error instanceof Error ? error.message : String(error)
}
});
throw error;
}
}
@ -1051,6 +1194,17 @@ export class ChaptersLessonService {
return { code: 200, message: 'Attachment deleted successfully' };
} catch (error) {
logger.error(`Error deleting attachment: ${error}`);
const decodedToken = jwt.decode(request.token) as { id: number } | null;
await auditService.logSync({
userId: decodedToken?.id || 0,
action: AuditAction.ERROR,
entityType: 'LessonAttachment',
entityId: request.attachment_id,
metadata: {
operation: 'delete_attachment',
error: error instanceof Error ? error.message : String(error)
}
});
throw error;
}
}
@ -1127,6 +1281,17 @@ export class ChaptersLessonService {
return { code: 200, message: 'Question added successfully', data: completeQuestion as QuizQuestionData };
} catch (error) {
logger.error(`Error adding question: ${error}`);
const decodedToken = jwt.decode(request.token) as { id: number } | null;
await auditService.logSync({
userId: decodedToken?.id || 0,
action: AuditAction.ERROR,
entityType: 'Question',
entityId: 0,
metadata: {
operation: 'add_question',
error: error instanceof Error ? error.message : String(error)
}
});
throw error;
}
}
@ -1202,6 +1367,17 @@ export class ChaptersLessonService {
return { code: 200, message: 'Question updated successfully', data: completeQuestion as QuizQuestionData };
} catch (error) {
logger.error(`Error updating question: ${error}`);
const decodedToken = jwt.decode(request.token) as { id: number } | null;
await auditService.logSync({
userId: decodedToken?.id || 0,
action: AuditAction.ERROR,
entityType: 'Question',
entityId: request.question_id,
metadata: {
operation: 'update_question',
error: error instanceof Error ? error.message : String(error)
}
});
throw error;
}
}
@ -1295,6 +1471,17 @@ export class ChaptersLessonService {
return { code: 200, message: 'Question reordered successfully', data: questions as QuizQuestionData[] };
} catch (error) {
logger.error(`Error reordering question: ${error}`);
const decodedToken = jwt.decode(request.token) as { id: number } | null;
await auditService.logSync({
userId: decodedToken?.id || 0,
action: AuditAction.ERROR,
entityType: 'Question',
entityId: request.question_id,
metadata: {
operation: 'reorder_question',
error: error instanceof Error ? error.message : String(error)
}
});
throw error;
}
}
@ -1343,6 +1530,17 @@ export class ChaptersLessonService {
return { code: 200, message: 'Question deleted successfully' };
} catch (error) {
logger.error(`Error deleting question: ${error}`);
const decodedToken = jwt.decode(request.token) as { id: number } | null;
await auditService.logSync({
userId: decodedToken?.id || 0,
action: AuditAction.ERROR,
entityType: 'Question',
entityId: request.question_id,
metadata: {
operation: 'delete_question',
error: error instanceof Error ? error.message : String(error)
}
});
throw error;
}
}

View file

@ -102,6 +102,16 @@ export class CoursesInstructorService {
};
} catch (error) {
logger.error('Failed to create course', { error });
await auditService.logSync({
userId: userId,
action: AuditAction.ERROR,
entityType: 'Course',
entityId: 0, // Failed to create, so no ID
metadata: {
operation: 'create_course',
error: error instanceof Error ? error.message : String(error)
}
});
throw error;
}
}
@ -200,6 +210,17 @@ export class CoursesInstructorService {
};
} catch (error) {
logger.error('Failed to retrieve course', { error });
const decoded = jwt.decode(getmyCourse.token) as { id: number } | null;
await auditService.logSync({
userId: decoded?.id || 0,
action: AuditAction.ERROR,
entityType: 'Course',
entityId: getmyCourse.course_id,
metadata: {
operation: 'get_course',
error: error instanceof Error ? error.message : String(error)
}
});
throw error;
}
}
@ -222,6 +243,17 @@ export class CoursesInstructorService {
};
} catch (error) {
logger.error('Failed to update course', { error });
const decoded = jwt.decode(token) as { id: number } | null;
await auditService.logSync({
userId: decoded?.id || 0,
action: AuditAction.ERROR,
entityType: 'Course',
entityId: courseId,
metadata: {
operation: 'update_course',
error: error instanceof Error ? error.message : String(error)
}
});
throw error;
}
}
@ -275,6 +307,17 @@ export class CoursesInstructorService {
};
} catch (error) {
logger.error('Failed to upload thumbnail', { error });
const decoded = jwt.decode(token) as { id: number } | null;
await auditService.logSync({
userId: decoded?.id || 0,
action: AuditAction.ERROR,
entityType: 'Course',
entityId: courseId,
metadata: {
operation: 'upload_thumbnail',
error: error instanceof Error ? error.message : String(error)
}
});
throw error;
}
}
@ -291,6 +334,15 @@ export class CoursesInstructorService {
id: courseId
}
});
await auditService.logSync({
userId: courseInstructorId.user_id,
action: AuditAction.DELETE,
entityType: 'Course',
entityId: courseId,
metadata: {
operation: 'delete_course'
}
});
return {
code: 200,
message: 'Course deleted successfully',
@ -298,6 +350,17 @@ export class CoursesInstructorService {
};
} catch (error) {
logger.error('Failed to delete course', { error });
const decoded = jwt.decode(token) as { id: number } | null;
await auditService.logSync({
userId: decoded?.id || 0,
action: AuditAction.ERROR,
entityType: 'Course',
entityId: courseId,
metadata: {
operation: 'delete_course',
error: error instanceof Error ? error.message : String(error)
}
});
throw error;
}
}
@ -319,12 +382,32 @@ export class CoursesInstructorService {
status: 'PENDING'
}
});
await auditService.logSync({
userId: decoded.id,
action: AuditAction.UPDATE,
entityType: 'Course',
entityId: sendCourseForReview.course_id,
metadata: {
operation: 'send_course_for_review'
}
});
return {
code: 200,
message: 'Course sent for review successfully',
};
} catch (error) {
logger.error('Failed to send course for review', { error });
const decoded = jwt.decode(sendCourseForReview.token) as { id: number } | null;
await auditService.logSync({
userId: decoded?.id || 0,
action: AuditAction.ERROR,
entityType: 'Course',
entityId: sendCourseForReview.course_id,
metadata: {
operation: 'send_course_for_review',
error: error instanceof Error ? error.message : String(error)
}
});
throw error;
}
}
@ -347,6 +430,17 @@ export class CoursesInstructorService {
};
} catch (error) {
logger.error('Failed to set course to draft', { error });
const decoded = jwt.decode(setCourseDraft.token) as { id: number } | null;
await auditService.logSync({
userId: decoded?.id || 0,
action: AuditAction.ERROR,
entityType: 'Course',
entityId: setCourseDraft.course_id,
metadata: {
operation: 'set_course_draft',
error: error instanceof Error ? error.message : String(error)
}
});
throw error;
}
}
@ -358,8 +452,6 @@ export class CoursesInstructorService {
total: number;
}> {
try {
const decoded = jwt.verify(token, config.jwt.secret) as { id: number; type: string };
// Validate instructor access
await this.validateCourseInstructor(token, courseId);
@ -384,6 +476,17 @@ export class CoursesInstructorService {
};
} catch (error) {
logger.error('Failed to retrieve course approvals', { error });
const decoded = jwt.decode(token) as { id: number } | null;
await auditService.logSync({
userId: decoded?.id || 0,
action: AuditAction.ERROR,
entityType: 'Course',
entityId: courseId,
metadata: {
operation: 'get_course_approvals',
error: error instanceof Error ? error.message : String(error)
}
});
throw error;
}
}
@ -445,6 +548,17 @@ export class CoursesInstructorService {
};
} catch (error) {
logger.error('Failed to search instructors', { error });
const decoded = jwt.decode(input.token) as { id: number } | null;
await auditService.logSync({
userId: decoded?.id || 0,
action: AuditAction.ERROR,
entityType: 'Course',
entityId: input.course_id,
metadata: {
operation: 'search_instructors',
error: error instanceof Error ? error.message : String(error)
}
});
throw error;
}
}
@ -490,12 +604,35 @@ export class CoursesInstructorService {
}
});
const decoded = jwt.decode(addinstructorCourse.token) as { id: number } | null;
await auditService.logSync({
userId: decoded?.id || 0,
action: AuditAction.CREATE,
entityType: 'Course',
entityId: addinstructorCourse.course_id,
metadata: {
operation: 'add_instructor_to_course',
instructor_id: user.id,
}
});
return {
code: 200,
message: 'Instructor added to course successfully',
};
} catch (error) {
logger.error('Failed to add instructor to course', { error });
const decoded = jwt.decode(addinstructorCourse.token) as { id: number } | null;
await auditService.logSync({
userId: decoded?.id || 0,
action: AuditAction.ERROR,
entityType: 'Course',
entityId: addinstructorCourse.course_id,
metadata: {
operation: 'add_instructor_to_course',
error: error instanceof Error ? error.message : String(error)
}
});
throw error;
}
}
@ -511,12 +648,36 @@ export class CoursesInstructorService {
},
}
});
await auditService.logSync({
userId: decoded?.id || 0,
action: AuditAction.DELETE,
entityType: 'Course',
entityId: removeinstructorCourse.course_id,
metadata: {
operation: 'remove_instructor_from_course',
instructor_id: removeinstructorCourse.user_id,
course_id: removeinstructorCourse.course_id,
}
});
return {
code: 200,
message: 'Instructor removed from course successfully',
};
} catch (error) {
logger.error('Failed to remove instructor from course', { error });
const decoded = jwt.decode(removeinstructorCourse.token) as { id: number } | null;
await auditService.logSync({
userId: decoded?.id || 0,
action: AuditAction.ERROR,
entityType: 'Course',
entityId: removeinstructorCourse.course_id,
metadata: {
operation: 'remove_instructor_from_course',
error: error instanceof Error ? error.message : String(error)
}
});
throw error;
}
}
@ -567,6 +728,18 @@ export class CoursesInstructorService {
};
} catch (error) {
logger.error('Failed to retrieve instructors of course', { error });
const decoded = jwt.decode(listinstructorCourse.token) as { id: number } | null;
await auditService.logSync({
userId: decoded?.id || 0,
action: AuditAction.ERROR,
entityType: 'Course',
entityId: listinstructorCourse.course_id,
metadata: {
operation: 'list_instructors_of_course',
error: error instanceof Error ? error.message : String(error)
}
});
throw error;
}
}
@ -585,12 +758,36 @@ export class CoursesInstructorService {
is_primary: true,
}
});
await auditService.logSync({
userId: decoded?.id || 0,
action: AuditAction.UPDATE,
entityType: 'Course',
entityId: setprimaryCourseInstructor.course_id,
metadata: {
operation: 'set_primary_instructor',
instructor_id: setprimaryCourseInstructor.user_id,
course_id: setprimaryCourseInstructor.course_id,
}
});
return {
code: 200,
message: 'Primary instructor set successfully',
};
} catch (error) {
logger.error('Failed to set primary instructor', { error });
const decoded = jwt.decode(setprimaryCourseInstructor.token) as { id: number } | null;
await auditService.logSync({
userId: decoded?.id || 0,
action: AuditAction.ERROR,
entityType: 'Course',
entityId: setprimaryCourseInstructor.course_id,
metadata: {
operation: 'set_primary_instructor',
error: error instanceof Error ? error.message : String(error)
}
});
throw error;
}
}
@ -629,7 +826,6 @@ export class CoursesInstructorService {
static async getEnrolledStudents(input: GetEnrolledStudentsInput): Promise<GetEnrolledStudentsResponse> {
try {
const { token, course_id, page = 1, limit = 20, search, status } = input;
const decoded = jwt.verify(token, config.jwt.secret) as { id: number };
// Validate instructor
await this.validateCourseInstructor(token, course_id);
@ -707,6 +903,17 @@ export class CoursesInstructorService {
};
} catch (error) {
logger.error(`Error getting enrolled students: ${error}`);
const decoded = jwt.decode(input.token) as { id: number } | null;
await auditService.logSync({
userId: decoded?.id || 0,
action: AuditAction.ERROR,
entityType: 'Course',
entityId: input.course_id,
metadata: {
operation: 'get_enrolled_students',
error: error instanceof Error ? error.message : String(error)
}
});
throw error;
}
}
@ -874,6 +1081,17 @@ export class CoursesInstructorService {
};
} catch (error) {
logger.error(`Error getting quiz scores: ${error}`);
const decoded = jwt.decode(input.token) as { id: number } | null;
await auditService.logSync({
userId: decoded?.id || 0,
action: AuditAction.ERROR,
entityType: 'Course',
entityId: input.course_id,
metadata: {
operation: 'get_quiz_scores',
error: error instanceof Error ? error.message : String(error)
}
});
throw error;
}
}
@ -885,7 +1103,6 @@ export class CoursesInstructorService {
static async getQuizAttemptDetail(input: GetQuizAttemptDetailInput): Promise<GetQuizAttemptDetailResponse> {
try {
const { token, course_id, lesson_id, student_id } = input;
const decoded = jwt.verify(token, config.jwt.secret) as { id: number };
// Validate instructor
await this.validateCourseInstructor(token, course_id);
@ -988,6 +1205,17 @@ export class CoursesInstructorService {
};
} catch (error) {
logger.error(`Error getting quiz attempt detail: ${error}`);
const decoded = jwt.decode(input.token) as { id: number } | null;
await auditService.logSync({
userId: decoded?.id || 0,
action: AuditAction.ERROR,
entityType: 'Course',
entityId: input.course_id,
metadata: {
operation: 'get_quiz_attempt_detail',
error: error instanceof Error ? error.message : String(error)
}
});
throw error;
}
}
@ -1125,6 +1353,17 @@ export class CoursesInstructorService {
};
} catch (error) {
logger.error(`Error getting enrolled student detail: ${error}`);
const decoded = jwt.decode(input.token) as { id: number } | null;
await auditService.logSync({
userId: decoded?.id || 0,
action: AuditAction.ERROR,
entityType: 'Course',
entityId: input.course_id,
metadata: {
operation: 'get_enrolled_student_detail',
error: error instanceof Error ? error.message : String(error)
}
});
throw error;
}
}
@ -1181,6 +1420,17 @@ export class CoursesInstructorService {
};
} catch (error) {
logger.error(`Error getting course approval history: ${error}`);
const decoded = jwt.decode(token) as { id: number } | null;
await auditService.logSync({
userId: decoded?.id || 0,
action: AuditAction.ERROR,
entityType: 'Course',
entityId: courseId,
metadata: {
operation: 'get_course_approval_history',
error: error instanceof Error ? error.message : String(error)
}
});
throw error;
}
}

View file

@ -186,7 +186,20 @@ export class CoursesStudentService {
},
};
} catch (error) {
logger.error(error);
logger.error(`Error enrolling in course: ${error}`);
const decoded = jwt.decode(input.token) as { id: number } | null;
await auditService.logSync({
userId: decoded?.id || 0,
action: AuditAction.ERROR,
entityType: 'Enrollment',
entityId: 0,
metadata: {
operation: 'enroll_course',
course_id: input.course_id,
error: error instanceof Error ? error.message : String(error)
}
});
throw error;
}
}
@ -261,6 +274,17 @@ export class CoursesStudentService {
};
} catch (error) {
logger.error(error);
const decoded = jwt.decode(input.token) as { id: number } | null;
await auditService.logSync({
userId: decoded?.id || 0,
action: AuditAction.ERROR,
entityType: 'Enrollment',
entityId: 0,
metadata: {
operation: 'get_enrolled_courses',
error: error instanceof Error ? error.message : String(error)
}
});
throw error;
}
}
@ -416,6 +440,17 @@ export class CoursesStudentService {
};
} catch (error) {
logger.error(error);
const decoded = jwt.decode(input.token) as { id: number } | null;
await auditService.logSync({
userId: decoded?.id || 0,
action: AuditAction.ERROR,
entityType: 'Enrollment',
entityId: 0,
metadata: {
operation: 'get_course_learning',
error: error instanceof Error ? error.message : String(error)
}
});
throw error;
}
}
@ -678,6 +713,17 @@ export class CoursesStudentService {
};
} catch (error) {
logger.error(error);
const decoded = jwt.decode(input.token) as { id: number } | null;
await auditService.logSync({
userId: decoded?.id || 0,
action: AuditAction.ERROR,
entityType: 'Enrollment',
entityId: 0,
metadata: {
operation: 'get_course_learning',
error: error instanceof Error ? error.message : String(error)
}
});
throw error;
}
}
@ -866,6 +912,17 @@ export class CoursesStudentService {
};
} catch (error) {
logger.error(error);
const decoded = jwt.decode(input.token) as { id: number } | null;
await auditService.logSync({
userId: decoded?.id || 0,
action: AuditAction.ERROR,
entityType: 'Enrollment',
entityId: 0,
metadata: {
operation: 'get_course_learning',
error: error instanceof Error ? error.message : String(error)
}
});
throw error;
}
}
@ -940,6 +997,17 @@ export class CoursesStudentService {
};
} catch (error) {
logger.error(error);
const decoded = jwt.decode(input.token) as { id: number } | null;
await auditService.logSync({
userId: decoded?.id || 0,
action: AuditAction.ERROR,
entityType: 'Enrollment',
entityId: 0,
metadata: {
operation: 'get_course_learning',
error: error instanceof Error ? error.message : String(error)
}
});
throw error;
}
}
@ -1037,6 +1105,17 @@ export class CoursesStudentService {
};
} catch (error) {
logger.error(error);
const decoded = jwt.decode(input.token) as { id: number } | null;
await auditService.logSync({
userId: decoded?.id || 0,
action: AuditAction.ERROR,
entityType: 'Enrollment',
entityId: 0,
metadata: {
operation: 'get_course_learning',
error: error instanceof Error ? error.message : String(error)
}
});
throw error;
}
}
@ -1168,7 +1247,19 @@ export class CoursesStudentService {
},
};
} catch (error) {
logger.error(error);
logger.error(`Error completing lesson: ${error}`);
const decoded = jwt.decode(input.token) as { id: number } | null;
await auditService.logSync({
userId: decoded?.id || 0,
action: AuditAction.ERROR,
entityType: 'LessonProgress',
entityId: input.lesson_id,
metadata: {
operation: 'complete_lesson',
lesson_id: input.lesson_id,
error: error instanceof Error ? error.message : String(error)
}
});
throw error;
}
}
@ -1213,22 +1304,14 @@ export class CoursesStudentService {
},
});
if (!lesson) {
throw new NotFoundError('Lesson not found');
}
if (!lesson) throw new NotFoundError('Lesson not found');
if (lesson.type !== 'QUIZ') {
throw new ValidationError('This lesson is not a quiz');
}
if (lesson.type !== 'QUIZ') throw new ValidationError('This lesson is not a quiz');
if (!lesson.quiz) {
throw new NotFoundError('Quiz not found for this lesson');
}
if (!lesson.quiz) throw new NotFoundError('Quiz not found for this lesson');
// Verify lesson belongs to the course
if (lesson.chapter.course_id !== course_id) {
throw new NotFoundError('Lesson not found in this course');
}
if (lesson.chapter.course_id !== course_id) throw new NotFoundError('Lesson not found in this course');
const quiz = lesson.quiz;
@ -1332,7 +1415,20 @@ export class CoursesStudentService {
},
};
} catch (error) {
logger.error(error);
logger.error(`Error submitting quiz: ${error}`);
const decoded = jwt.decode(input.token) as { id: number } | null;
await auditService.logSync({
userId: decoded?.id || 0,
action: AuditAction.ERROR,
entityType: 'QuizAttempt',
entityId: 0,
metadata: {
operation: 'submit_quiz',
course_id: input.course_id,
lesson_id: input.lesson_id,
error: error instanceof Error ? error.message : String(error)
}
});
throw error;
}
}
@ -1373,22 +1469,14 @@ export class CoursesStudentService {
},
});
if (!lesson) {
throw new NotFoundError('Lesson not found');
}
if (!lesson) throw new NotFoundError('Lesson not found');
if (lesson.type !== 'QUIZ') {
throw new ValidationError('This lesson is not a quiz');
}
if (lesson.type !== 'QUIZ') throw new ValidationError('This lesson is not a quiz');
if (!lesson.quiz) {
throw new NotFoundError('Quiz not found for this lesson');
}
if (!lesson.quiz) throw new NotFoundError('Quiz not found for this lesson');
// Verify lesson belongs to the course
if (lesson.chapter.course_id !== course_id) {
throw new NotFoundError('Lesson not found in this course');
}
if (lesson.chapter.course_id !== course_id) throw new NotFoundError('Lesson not found in this course');
// Get all quiz attempts for this user
const attempts = await prisma.quizAttempt.findMany({
@ -1438,6 +1526,21 @@ export class CoursesStudentService {
};
} catch (error) {
logger.error(error);
const decoded = jwt.decode(input.token) as { id: number } | null;
if (decoded?.id) {
await auditService.logSync({
userId: decoded.id,
action: AuditAction.ERROR,
entityType: 'QuizAttempt',
entityId: 0,
metadata: {
operation: 'get_quiz_attempts',
course_id: input.course_id,
lesson_id: input.lesson_id,
error: error instanceof Error ? error.message : String(error)
}
});
}
throw error;
}
}

View file

@ -18,7 +18,7 @@ export class RecommendedCoursesService {
/**
* List all approved courses (for admin to manage recommendations)
*/
static async listApprovedCourses(): Promise<ListApprovedCoursesResponse> {
static async listApprovedCourses(token: string): Promise<ListApprovedCoursesResponse> {
try {
const courses = await prisma.course.findMany({
where: { status: 'APPROVED' },
@ -94,6 +94,19 @@ export class RecommendedCoursesService {
};
} catch (error) {
logger.error('Failed to list approved courses', { error });
const decoded = jwt.decode(token) as { id: number } | null;
if (decoded?.id) {
await auditService.logSync({
userId: decoded.id,
action: AuditAction.ERROR,
entityType: 'RecommendedCourses',
entityId: 0,
metadata: {
operation: 'list_approved_courses',
error: error instanceof Error ? error.message : String(error)
}
});
}
throw error;
}
}
@ -101,7 +114,7 @@ export class RecommendedCoursesService {
/**
* Get course by ID (for admin to view details)
*/
static async getCourseById(courseId: number): Promise<GetCourseByIdResponse> {
static async getCourseById(token: string, courseId: number): Promise<GetCourseByIdResponse> {
try {
const course = await prisma.course.findUnique({
where: { id: courseId },
@ -179,6 +192,19 @@ export class RecommendedCoursesService {
};
} catch (error) {
logger.error('Failed to get course by ID', { error });
const decoded = jwt.decode(token) as { id: number } | null;
if (decoded?.id) {
await auditService.logSync({
userId: decoded.id,
action: AuditAction.ERROR,
entityType: 'RecommendedCourses',
entityId: 0,
metadata: {
operation: 'get_course_by_id',
error: error instanceof Error ? error.message : String(error)
}
});
}
throw error;
}
}
@ -229,6 +255,17 @@ export class RecommendedCoursesService {
};
} catch (error) {
logger.error('Failed to toggle recommended status', { error });
const decoded = jwt.decode(token) as { id: number } | null;
await auditService.logSync({
userId: decoded?.id || 0,
action: AuditAction.ERROR,
entityType: 'RecommendedCourses',
entityId: courseId,
metadata: {
operation: 'toggle_recommended',
error: error instanceof Error ? error.message : String(error)
}
});
throw error;
}
}

View file

@ -20,6 +20,8 @@ import {
} from '../types/announcements.types';
import { CoursesInstructorService } from './CoursesInstructor.service';
import { uploadFile, deleteFile, getPresignedUrl } from '../config/minio';
import { auditService } from './audit.service';
import { AuditAction } from '@prisma/client';
export class AnnouncementsService {
@ -37,9 +39,7 @@ export class AnnouncementsService {
where: { id: decoded.id },
include: { role: true },
});
if (!user) {
throw new UnauthorizedError('Invalid token');
}
if (!user) throw new UnauthorizedError('Invalid token');
// Admin can access all courses
const isAdmin = user.role.code === 'ADMIN';
@ -130,6 +130,16 @@ export class AnnouncementsService {
};
} catch (error) {
logger.error(`Error listing announcements: ${error}`);
const decoded = jwt.decode(input.token) as { id: number } | null;
await auditService.logSync({
userId: decoded?.id || 0,
action: AuditAction.ERROR,
entityType: 'Announcement',
entityId: 0,
metadata: {
error: error instanceof Error ? error.message : String(error)
}
});
throw error;
}
}
@ -226,6 +236,16 @@ export class AnnouncementsService {
};
} catch (error) {
logger.error(`Error creating announcement: ${error}`);
const decoded = jwt.decode(input.token) as { id: number } | null;
await auditService.logSync({
userId: decoded?.id || 0,
action: AuditAction.ERROR,
entityType: 'Announcement',
entityId: 0,
metadata: {
error: error instanceof Error ? error.message : String(error)
}
});
throw error;
}
}
@ -300,6 +320,16 @@ export class AnnouncementsService {
};
} catch (error) {
logger.error(`Error updating announcement: ${error}`);
const decoded = jwt.decode(input.token) as { id: number } | null;
await auditService.logSync({
userId: decoded?.id || 0,
action: AuditAction.ERROR,
entityType: 'Announcement',
entityId: 0,
metadata: {
error: error instanceof Error ? error.message : String(error)
}
});
throw error;
}
}
@ -346,6 +376,16 @@ export class AnnouncementsService {
};
} catch (error) {
logger.error(`Error deleting announcement: ${error}`);
const decoded = jwt.decode(input.token) as { id: number } | null;
await auditService.logSync({
userId: decoded?.id || 0,
action: AuditAction.ERROR,
entityType: 'Announcement',
entityId: 0,
metadata: {
error: error instanceof Error ? error.message : String(error)
}
});
throw error;
}
}
@ -411,6 +451,16 @@ export class AnnouncementsService {
};
} catch (error) {
logger.error(`Error uploading attachment: ${error}`);
const decoded = jwt.decode(input.token) as { id: number } | null;
await auditService.logSync({
userId: decoded?.id || 0,
action: AuditAction.ERROR,
entityType: 'Announcement',
entityId: 0,
metadata: {
error: error instanceof Error ? error.message : String(error)
}
});
throw error;
}
}
@ -458,6 +508,16 @@ export class AnnouncementsService {
};
} catch (error) {
logger.error(`Error deleting attachment: ${error}`);
const decoded = jwt.decode(input.token) as { id: number } | null;
await auditService.logSync({
userId: decoded?.id || 0,
action: AuditAction.ERROR,
entityType: 'Announcement',
entityId: 0,
metadata: {
error: error instanceof Error ? error.message : String(error)
}
});
throw error;
}
}

View file

@ -83,6 +83,7 @@ export class AuthService {
* User registration
*/
async register(data: RegisterRequest): Promise<RegisterResponse> {
try {
const { username, email, password, first_name, last_name, prefix, phone } = data;
// Check if username already exists
@ -162,9 +163,26 @@ export class AuthService {
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;
}
}
async registerInstructor(data: RegisterRequest): Promise<RegisterResponse> {
try {
const { username, email, password, first_name, last_name, prefix, phone } = data;
// Check if username already exists
@ -244,6 +262,22 @@ export class AuthService {
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;
}
}
/**

View file

@ -5,6 +5,8 @@ import { logger } from '../config/logger';
import jwt from 'jsonwebtoken';
import { createCategory, createCategoryResponse, deleteCategoryResponse, updateCategory, updateCategoryResponse, ListCategoriesResponse, Category } from '../types/categories.type';
import { UnauthorizedError, ValidationError, ForbiddenError } from '../middleware/errorHandler';
import { auditService } from './audit.service';
import { AuditAction } from '@prisma/client';
export class CategoryService {
async listCategories(): Promise<ListCategoriesResponse> {
@ -30,6 +32,13 @@ export class CategoryService {
const newCategory = await prisma.category.create({
data: category
});
auditService.log({
userId: decoded.id,
action: AuditAction.CREATE,
entityType: 'Category',
entityId: newCategory.id,
newValue: { name: newCategory.name as { th: string; en: string }, slug: newCategory.slug, description: newCategory.description as { th: string; en: string } },
});
return {
code: 200,
message: 'Category created successfully',
@ -43,6 +52,16 @@ export class CategoryService {
};
} catch (error) {
logger.error('Failed to create category', { error });
await auditService.logSync({
userId: 0,
action: AuditAction.ERROR,
entityType: 'Category',
entityId: 0,
metadata: {
operation: 'create_category',
error: error instanceof Error ? error.message : String(error)
}
});
throw error;
}
}
@ -54,6 +73,13 @@ export class CategoryService {
where: { id },
data: category
});
auditService.log({
userId: decoded.id,
action: AuditAction.UPDATE,
entityType: 'Category',
entityId: id,
newValue: { name: updatedCategory.name as { th: string; en: string }, slug: updatedCategory.slug, description: updatedCategory.description as { th: string; en: string } },
});
return {
code: 200,
message: 'Category updated successfully',
@ -67,21 +93,49 @@ export class CategoryService {
};
} catch (error) {
logger.error('Failed to update category', { error });
await auditService.logSync({
userId: 0,
action: AuditAction.ERROR,
entityType: 'Category',
entityId: 0,
metadata: {
operation: 'update_category',
error: error instanceof Error ? error.message : String(error)
}
});
throw error;
}
}
async deleteCategory(id: number): Promise<deleteCategoryResponse> {
async deleteCategory(token: string, id: number): Promise<deleteCategoryResponse> {
try {
const decoded = jwt.verify(token, config.jwt.secret) as { id: number; username: string; email: string; roleCode: string };
const deletedCategory = await prisma.category.delete({
where: { id }
});
auditService.log({
userId: decoded.id,
action: AuditAction.DELETE,
entityType: 'Category',
entityId: id,
newValue: { name: deletedCategory.name as { th: string; en: string }, slug: deletedCategory.slug, description: deletedCategory.description as { th: string; en: string } },
});
return {
code: 200,
message: 'Category deleted successfully',
};
} catch (error) {
logger.error('Failed to delete category', { error });
await auditService.logSync({
userId: 0,
action: AuditAction.ERROR,
entityType: 'Category',
entityId: 0,
metadata: {
operation: 'delete_category',
error: error instanceof Error ? error.message : String(error)
}
});
throw error;
}
}

View file

@ -16,6 +16,8 @@ import {
ListMyCertificatesInput,
ListMyCertificatesResponse,
} from '../types/certificate.types';
import { auditService } from './audit.service';
import { AuditAction } from '@prisma/client';
export class CertificateService {
private static TEMPLATE_PATH = path.join(__dirname, '../../assets/templates/Certificate.pdf');
@ -54,17 +56,11 @@ export class CertificateService {
},
});
if (!enrollment) {
throw new NotFoundError('Enrollment not found');
}
if (!enrollment) throw new NotFoundError('Enrollment not found');
if (enrollment.status !== 'COMPLETED') {
throw new ForbiddenError('Course not completed yet');
}
if (enrollment.status !== 'COMPLETED') throw new ForbiddenError('Course not completed yet');
if (!enrollment.course.have_certificate) {
throw new ValidationError('This course does not offer certificates');
}
if (!enrollment.course.have_certificate) throw new ValidationError('This course does not offer certificates');
// Check if certificate already exists
const existingCertificate = await prisma.certificate.findFirst({
@ -121,6 +117,14 @@ export class CertificateService {
},
});
auditService.log({
userId: decoded.id,
action: AuditAction.CREATE,
entityType: 'Certificate',
entityId: certificate.id,
newValue: { file_path: certificate.file_path, issued_at: certificate.issued_at },
});
const downloadUrl = await getPresignedUrl(filePath, 3600);
return {
@ -135,6 +139,18 @@ export class CertificateService {
};
} catch (error) {
logger.error('Failed to generate certificate', { error });
const decoded = jwt.decode(input.token) as { id: number } | null;
await auditService.logSync({
userId: decoded?.id,
action: AuditAction.ERROR,
entityType: 'Certificate',
entityId: 0,
metadata: {
operation: 'generate_certificate',
course_id: input.course_id,
error: error instanceof Error ? error.message : String(error)
}
});
throw error;
}
}
@ -186,6 +202,18 @@ export class CertificateService {
};
} catch (error) {
logger.error('Failed to get certificate', { error });
const decoded = jwt.decode(input.token) as { id: number } | null;
await auditService.logSync({
userId: decoded?.id,
action: AuditAction.ERROR,
entityType: 'Certificate',
entityId: 0,
metadata: {
operation: 'get_certificate',
course_id: input.course_id,
error: error instanceof Error ? error.message : String(error)
}
});
throw error;
}
}
@ -239,6 +267,17 @@ export class CertificateService {
};
} catch (error) {
logger.error('Failed to list certificates', { error });
const decoded = jwt.decode(input.token) as { id: number } | null;
await auditService.logSync({
userId: decoded?.id,
action: AuditAction.ERROR,
entityType: 'Certificate',
entityId: 0,
metadata: {
operation: 'list_my_certificates',
error: error instanceof Error ? error.message : String(error)
}
});
throw error;
}
}

View file

@ -5,6 +5,8 @@ import { logger } from '../config/logger';
import { listCourseResponse, getCourseResponse, ListCoursesInput } from '../types/courses.types';
import { UnauthorizedError, ValidationError, ForbiddenError } from '../middleware/errorHandler';
import { getPresignedUrl } from '../config/minio';
import { auditService } from './audit.service';
import { AuditAction } from '@prisma/client';
export class CoursesService {
async ListCourses(input: ListCoursesInput): Promise<listCourseResponse> {
@ -82,6 +84,16 @@ export class CoursesService {
};
} catch (error) {
logger.error('Failed to fetch courses', { error });
await auditService.logSync({
userId: 0,
action: AuditAction.ERROR,
entityType: 'Course',
entityId: 0,
metadata: {
operation: 'list_courses',
error: error instanceof Error ? error.message : String(error)
}
});
throw error;
}
}
@ -122,6 +134,16 @@ export class CoursesService {
};
} catch (error) {
logger.error('Failed to fetch course', { error });
await auditService.logSync({
userId: 0,
action: AuditAction.ERROR,
entityType: 'Course',
entityId: id,
metadata: {
operation: 'get_course',
error: error instanceof Error ? error.message : String(error)
}
});
throw error;
}
}

View file

@ -135,6 +135,17 @@ export class UserService {
throw new UnauthorizedError('Token expired');
}
logger.error('Failed to change password', { error });
const decoded = jwt.decode(token) as { id: number } | null;
await auditService.logSync({
userId: decoded?.id || 0,
action: AuditAction.ERROR,
entityType: 'User',
entityId: decoded?.id || 0,
metadata: {
operation: 'change_password',
error: error instanceof Error ? error.message : String(error)
}
});
throw error;
}
}
@ -186,6 +197,17 @@ export class UserService {
throw new UnauthorizedError('Token expired');
}
logger.error('Failed to update profile', { error });
const decoded = jwt.decode(token) as { id: number } | null;
await auditService.logSync({
userId: decoded?.id || 0,
action: AuditAction.UPDATE,
entityType: 'UserProfile',
entityId: decoded?.id || 0,
metadata: {
operation: 'update_profile',
error: error instanceof Error ? error.message : String(error)
}
});
throw error;
}
}
@ -252,6 +274,18 @@ export class UserService {
});
}
// Audit log - UPLOAD_AVATAR
await auditService.logSync({
userId: decoded.id,
action: AuditAction.UPLOAD_FILE,
entityType: 'User',
entityId: decoded.id,
metadata: {
operation: 'upload_avatar',
filePath
}
});
// Generate presigned URL for response
const presignedUrl = await this.getAvatarPresignedUrl(filePath);
@ -273,6 +307,18 @@ export class UserService {
throw new UnauthorizedError('Token expired');
}
logger.error('Failed to upload avatar', { error });
const decoded = jwt.decode(token) as { id: number } | null;
await auditService.logSync({
userId: decoded?.id || 0,
action: AuditAction.UPLOAD_FILE,
entityType: 'UserProfile',
entityId: decoded?.id || 0,
metadata: {
operation: 'upload_avatar',
error: error instanceof Error ? error.message : String(error)
}
});
throw error;
}
}
@ -385,6 +431,17 @@ export class UserService {
if (error instanceof jwt.JsonWebTokenError) throw new UnauthorizedError('Invalid token');
if (error instanceof jwt.TokenExpiredError) throw new UnauthorizedError('Token expired');
logger.error('Failed to send verification email', { error });
const decoded = jwt.decode(token) as { id: number } | null;
await auditService.logSync({
userId: decoded?.id || 0,
action: AuditAction.ERROR,
entityType: 'UserProfile',
entityId: decoded?.id || 0,
metadata: {
operation: 'send_verification_email',
error: error instanceof Error ? error.message : String(error)
}
});
throw error;
}
}
@ -415,6 +472,15 @@ export class UserService {
});
logger.info('Email verified successfully', { userId: user.id, email: user.email });
await auditService.logSync({
userId: user.id,
action: AuditAction.VERIFY_EMAIL,
entityType: 'UserProfile',
entityId: user.id,
metadata: {
operation: 'verify_email'
}
});
return {
code: 200,
message: 'Email verified successfully'
@ -423,6 +489,17 @@ export class UserService {
if (error instanceof jwt.JsonWebTokenError) throw new UnauthorizedError('Invalid verification token');
if (error instanceof jwt.TokenExpiredError) throw new UnauthorizedError('Verification link has expired');
logger.error('Failed to verify email', { error });
const decoded = jwt.decode(verifyToken) as { id: number } | null;
await auditService.logSync({
userId: decoded?.id || 0,
action: AuditAction.ERROR,
entityType: 'UserProfile',
entityId: decoded?.id || 0,
metadata: {
operation: 'verify_email',
error: error instanceof Error ? error.message : String(error)
}
});
throw error;
}
}

View file

@ -39,6 +39,16 @@ export class UserManagementService {
};
} catch (error) {
logger.error('Failed to fetch users', { error });
await auditService.logSync({
userId: 0,
action: AuditAction.ERROR,
entityType: 'User',
entityId: 0,
metadata: {
operation: 'list_users',
error: error instanceof Error ? error.message : String(error)
}
});
throw error;
}
}
@ -61,6 +71,16 @@ export class UserManagementService {
};
} catch (error) {
logger.error('Failed to fetch user by ID', { error });
await auditService.logSync({
userId: id,
action: AuditAction.ERROR,
entityType: 'User',
entityId: id,
metadata: {
operation: 'get_user_by_id',
error: error instanceof Error ? error.message : String(error)
}
});
throw error;
}
}
@ -95,6 +115,17 @@ export class UserManagementService {
};
} catch (error) {
logger.error('Failed to update user role', { error });
await auditService.logSync({
userId: id,
action: AuditAction.ERROR,
entityType: 'User',
entityId: id,
metadata: {
operation: 'update_user_role',
target_role_id: role_id,
error: error instanceof Error ? error.message : String(error)
}
});
throw error;
}
}
@ -114,6 +145,16 @@ export class UserManagementService {
};
} catch (error) {
logger.error('Failed to deactivate user', { error });
await auditService.logSync({
userId: id,
action: AuditAction.ERROR,
entityType: 'User',
entityId: id,
metadata: {
operation: 'delete_user',
error: error instanceof Error ? error.message : String(error)
}
});
throw error;
}
}
@ -160,6 +201,16 @@ export class UserManagementService {
throw new UnauthorizedError('Token expired');
}
logger.error('Failed to deactivate account', { error });
await auditService.logSync({
userId: id,
action: AuditAction.ERROR,
entityType: 'User',
entityId: id,
metadata: {
operation: 'deactivate_account',
error: error instanceof Error ? error.message : String(error)
}
});
throw error;
}
}
@ -207,6 +258,16 @@ export class UserManagementService {
throw new UnauthorizedError('Token expired');
}
logger.error('Failed to activate account', { error });
await auditService.logSync({
userId: id,
action: AuditAction.ERROR,
entityType: 'User',
entityId: id,
metadata: {
operation: 'activate_account',
error: error instanceof Error ? error.message : String(error)
}
});
throw error;
}
}