feat: Add error audit logging to instructor course operations and implement status filtering for listing courses.

This commit is contained in:
JakkrapartXD 2026-02-13 14:45:59 +07:00
parent 21273fcaeb
commit 45941fbe6c
4 changed files with 242 additions and 19 deletions

View file

@ -5,6 +5,7 @@ import {
createCourses, createCourses,
createCourseResponse, createCourseResponse,
GetMyCourseResponse, GetMyCourseResponse,
ListMyCoursesInput,
ListMyCourseResponse, ListMyCourseResponse,
addinstructorCourseResponse, addinstructorCourseResponse,
removeinstructorCourseResponse, removeinstructorCourseResponse,
@ -41,12 +42,15 @@ export class CoursesInstructorController {
@SuccessResponse('200', 'Courses retrieved successfully') @SuccessResponse('200', 'Courses retrieved successfully')
@Response('401', 'Invalid or expired token') @Response('401', 'Invalid or expired token')
@Response('404', 'Courses not found') @Response('404', 'Courses not found')
public async listMyCourses(@Request() request: any): Promise<ListMyCourseResponse> { public async listMyCourses(
@Request() request: any,
@Query() status?: 'DRAFT' | 'PENDING' | 'APPROVED' | 'REJECTED' | 'ARCHIVED'
): Promise<ListMyCourseResponse> {
const token = request.headers.authorization?.replace('Bearer ', ''); const token = request.headers.authorization?.replace('Bearer ', '');
if (!token) { if (!token) {
throw new ValidationError('No token provided'); throw new ValidationError('No token provided');
} }
return await CoursesInstructorService.listMyCourses(token); return await CoursesInstructorService.listMyCourses({ token, status });
} }
/** /**

View file

@ -10,6 +10,7 @@ import {
UpdateCourseInput, UpdateCourseInput,
createCourseResponse, createCourseResponse,
GetMyCourseResponse, GetMyCourseResponse,
ListMyCoursesInput,
ListMyCourseResponse, ListMyCourseResponse,
addinstructorCourse, addinstructorCourse,
addinstructorCourseResponse, addinstructorCourseResponse,
@ -102,16 +103,27 @@ export class CoursesInstructorService {
}; };
} catch (error) { } catch (error) {
logger.error('Failed to create course', { 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; throw error;
} }
} }
static async listMyCourses(token: string): Promise<ListMyCourseResponse> { static async listMyCourses(input: ListMyCoursesInput): Promise<ListMyCourseResponse> {
try { try {
const decoded = jwt.verify(token, config.jwt.secret) as { id: number; type: string }; const decoded = jwt.verify(input.token, config.jwt.secret) as { id: number; type: string };
const courseInstructors = await prisma.courseInstructor.findMany({ const courseInstructors = await prisma.courseInstructor.findMany({
where: { where: {
user_id: decoded.id user_id: decoded.id,
course: input.status ? { status: input.status } : undefined
}, },
include: { include: {
course: true course: true
@ -143,6 +155,17 @@ export class CoursesInstructorService {
}; };
} catch (error) { } catch (error) {
logger.error('Failed to retrieve courses', { error }); logger.error('Failed to retrieve courses', { error });
const decoded = jwt.decode(input.token) as { id: number } | null;
await auditService.logSync({
userId: decoded?.id || undefined,
action: AuditAction.ERROR,
entityType: 'Course',
entityId: 0,
metadata: {
operation: 'list_my_courses',
error: error instanceof Error ? error.message : String(error)
}
});
throw error; throw error;
} }
} }
@ -200,6 +223,17 @@ export class CoursesInstructorService {
}; };
} catch (error) { } catch (error) {
logger.error('Failed to retrieve course', { error }); logger.error('Failed to retrieve course', { error });
const decoded = jwt.decode(getmyCourse.token) as { id: number } | null;
await auditService.logSync({
userId: decoded?.id || undefined,
action: AuditAction.ERROR,
entityType: 'Course',
entityId: getmyCourse.course_id,
metadata: {
operation: 'get_my_course',
error: error instanceof Error ? error.message : String(error)
}
});
throw error; throw error;
} }
} }
@ -222,6 +256,17 @@ export class CoursesInstructorService {
}; };
} catch (error) { } catch (error) {
logger.error('Failed to update course', { error }); logger.error('Failed to update course', { error });
const decoded = jwt.decode(token) as { id: number } | null;
await auditService.logSync({
userId: decoded?.id || undefined,
action: AuditAction.ERROR,
entityType: 'Course',
entityId: courseId,
metadata: {
operation: 'update_course',
error: error instanceof Error ? error.message : String(error)
}
});
throw error; throw error;
} }
} }
@ -275,6 +320,17 @@ export class CoursesInstructorService {
}; };
} catch (error) { } catch (error) {
logger.error('Failed to upload thumbnail', { error }); logger.error('Failed to upload thumbnail', { error });
const decoded = jwt.decode(token) as { id: number } | null;
await auditService.logSync({
userId: decoded?.id || undefined,
action: AuditAction.ERROR,
entityType: 'Course',
entityId: courseId,
metadata: {
operation: 'upload_thumbnail',
error: error instanceof Error ? error.message : String(error)
}
});
throw error; throw error;
} }
} }
@ -298,6 +354,17 @@ export class CoursesInstructorService {
}; };
} catch (error) { } catch (error) {
logger.error('Failed to delete course', { error }); logger.error('Failed to delete course', { error });
const decoded = jwt.decode(token) as { id: number } | null;
await auditService.logSync({
userId: decoded?.id || undefined,
action: AuditAction.ERROR,
entityType: 'Course',
entityId: courseId,
metadata: {
operation: 'delete_course',
error: error instanceof Error ? error.message : String(error)
}
});
throw error; throw error;
} }
} }
@ -325,6 +392,17 @@ export class CoursesInstructorService {
}; };
} catch (error) { } catch (error) {
logger.error('Failed to send course for review', { 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 || undefined,
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; throw error;
} }
} }
@ -347,6 +425,17 @@ export class CoursesInstructorService {
}; };
} catch (error) { } catch (error) {
logger.error('Failed to set course to draft', { 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 || undefined,
action: AuditAction.ERROR,
entityType: 'Course',
entityId: setCourseDraft.course_id,
metadata: {
operation: 'set_course_draft',
error: error instanceof Error ? error.message : String(error)
}
});
throw error; throw error;
} }
} }
@ -384,6 +473,17 @@ export class CoursesInstructorService {
}; };
} catch (error) { } catch (error) {
logger.error('Failed to retrieve course approvals', { error }); logger.error('Failed to retrieve course approvals', { error });
const decoded = jwt.decode(token) as { id: number } | null;
await auditService.logSync({
userId: decoded?.id || undefined,
action: AuditAction.ERROR,
entityType: 'Course',
entityId: courseId,
metadata: {
operation: 'get_course_approvals',
error: error instanceof Error ? error.message : String(error)
}
});
throw error; throw error;
} }
} }
@ -445,6 +545,17 @@ export class CoursesInstructorService {
}; };
} catch (error) { } catch (error) {
logger.error('Failed to search instructors', { error }); logger.error('Failed to search instructors', { error });
const decoded = jwt.decode(input.token) as { id: number } | null;
await auditService.logSync({
userId: decoded?.id || undefined,
action: AuditAction.ERROR,
entityType: 'Course',
entityId: input.course_id,
metadata: {
operation: 'search_instructors',
error: error instanceof Error ? error.message : String(error)
}
});
throw error; throw error;
} }
} }
@ -496,6 +607,17 @@ export class CoursesInstructorService {
}; };
} catch (error) { } catch (error) {
logger.error('Failed to add instructor to course', { 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 || undefined,
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; throw error;
} }
} }
@ -517,6 +639,17 @@ export class CoursesInstructorService {
}; };
} catch (error) { } catch (error) {
logger.error('Failed to remove instructor from course', { 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 || undefined,
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; throw error;
} }
} }
@ -567,6 +700,17 @@ export class CoursesInstructorService {
}; };
} catch (error) { } catch (error) {
logger.error('Failed to retrieve instructors of course', { 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 || undefined,
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; throw error;
} }
} }
@ -591,6 +735,17 @@ export class CoursesInstructorService {
}; };
} catch (error) { } catch (error) {
logger.error('Failed to set primary instructor', { 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 || undefined,
action: AuditAction.ERROR,
entityType: 'Course',
entityId: setprimaryCourseInstructor.course_id,
metadata: {
operation: 'set_primary_instructor',
error: error instanceof Error ? error.message : String(error)
}
});
throw error; throw error;
} }
} }
@ -707,6 +862,17 @@ export class CoursesInstructorService {
}; };
} catch (error) { } catch (error) {
logger.error(`Error getting enrolled students: ${error}`); logger.error(`Error getting enrolled students: ${error}`);
const decoded = jwt.decode(input.token) as { id: number } | null;
await auditService.logSync({
userId: decoded?.id || undefined,
action: AuditAction.ERROR,
entityType: 'Course',
entityId: input.course_id,
metadata: {
operation: 'get_enrolled_students',
error: error instanceof Error ? error.message : String(error)
}
});
throw error; throw error;
} }
} }
@ -874,6 +1040,17 @@ export class CoursesInstructorService {
}; };
} catch (error) { } catch (error) {
logger.error(`Error getting quiz scores: ${error}`); logger.error(`Error getting quiz scores: ${error}`);
const decoded = jwt.decode(input.token) as { id: number } | null;
await auditService.logSync({
userId: decoded?.id || undefined,
action: AuditAction.ERROR,
entityType: 'Course',
entityId: input.course_id,
metadata: {
operation: 'get_quiz_scores',
error: error instanceof Error ? error.message : String(error)
}
});
throw error; throw error;
} }
} }
@ -988,6 +1165,17 @@ export class CoursesInstructorService {
}; };
} catch (error) { } catch (error) {
logger.error(`Error getting quiz attempt detail: ${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 || undefined,
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; throw error;
} }
} }
@ -1125,6 +1313,17 @@ export class CoursesInstructorService {
}; };
} catch (error) { } catch (error) {
logger.error(`Error getting enrolled student detail: ${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 || undefined,
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; throw error;
} }
} }
@ -1181,6 +1380,17 @@ export class CoursesInstructorService {
}; };
} catch (error) { } catch (error) {
logger.error(`Error getting course approval history: ${error}`); logger.error(`Error getting course approval history: ${error}`);
const decoded = jwt.decode(token) as { id: number } | null;
await auditService.logSync({
userId: decoded?.id || undefined,
action: AuditAction.ERROR,
entityType: 'Course',
entityId: courseId,
metadata: {
operation: 'get_course_approval_history',
error: error instanceof Error ? error.message : String(error)
}
});
throw error; throw error;
} }
} }

View file

@ -37,6 +37,7 @@ export class AuditService {
* Log await ( critical actions) * Log await ( critical actions)
*/ */
async logSync(params: CreateAuditLogParams): Promise<void> { async logSync(params: CreateAuditLogParams): Promise<void> {
try {
await prisma.auditLog.create({ await prisma.auditLog.create({
data: { data: {
user_id: params.userId, user_id: params.userId,
@ -50,6 +51,9 @@ export class AuditService {
metadata: params.metadata, metadata: params.metadata,
}, },
}); });
} catch (error) {
logger.error('Failed to create audit log (sync)', { error, params });
}
} }
/** /**

View file

@ -23,6 +23,11 @@ export interface createCourseResponse {
data: Course; data: Course;
} }
export interface ListMyCoursesInput {
token: string;
status?: 'DRAFT' | 'PENDING' | 'APPROVED' | 'REJECTED' | 'ARCHIVED';
}
export interface ListMyCourseResponse { export interface ListMyCourseResponse {
code: number; code: number;
message: string; message: string;