feat: Add error audit logging to instructor course operations and implement status filtering for listing courses.
This commit is contained in:
parent
21273fcaeb
commit
45941fbe6c
4 changed files with 242 additions and 19 deletions
|
|
@ -5,6 +5,7 @@ import {
|
|||
createCourses,
|
||||
createCourseResponse,
|
||||
GetMyCourseResponse,
|
||||
ListMyCoursesInput,
|
||||
ListMyCourseResponse,
|
||||
addinstructorCourseResponse,
|
||||
removeinstructorCourseResponse,
|
||||
|
|
@ -41,12 +42,15 @@ export class CoursesInstructorController {
|
|||
@SuccessResponse('200', 'Courses retrieved successfully')
|
||||
@Response('401', 'Invalid or expired token')
|
||||
@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 ', '');
|
||||
if (!token) {
|
||||
throw new ValidationError('No token provided');
|
||||
}
|
||||
return await CoursesInstructorService.listMyCourses(token);
|
||||
return await CoursesInstructorService.listMyCourses({ token, status });
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import {
|
|||
UpdateCourseInput,
|
||||
createCourseResponse,
|
||||
GetMyCourseResponse,
|
||||
ListMyCoursesInput,
|
||||
ListMyCourseResponse,
|
||||
addinstructorCourse,
|
||||
addinstructorCourseResponse,
|
||||
|
|
@ -102,16 +103,27 @@ 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;
|
||||
}
|
||||
}
|
||||
|
||||
static async listMyCourses(token: string): Promise<ListMyCourseResponse> {
|
||||
static async listMyCourses(input: ListMyCoursesInput): Promise<ListMyCourseResponse> {
|
||||
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({
|
||||
where: {
|
||||
user_id: decoded.id
|
||||
user_id: decoded.id,
|
||||
course: input.status ? { status: input.status } : undefined
|
||||
},
|
||||
include: {
|
||||
course: true
|
||||
|
|
@ -143,6 +155,17 @@ export class CoursesInstructorService {
|
|||
};
|
||||
} catch (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;
|
||||
}
|
||||
}
|
||||
|
|
@ -200,6 +223,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 || 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -222,6 +256,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 || undefined,
|
||||
action: AuditAction.ERROR,
|
||||
entityType: 'Course',
|
||||
entityId: courseId,
|
||||
metadata: {
|
||||
operation: 'update_course',
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
}
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
|
@ -275,6 +320,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 || undefined,
|
||||
action: AuditAction.ERROR,
|
||||
entityType: 'Course',
|
||||
entityId: courseId,
|
||||
metadata: {
|
||||
operation: 'upload_thumbnail',
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
}
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
|
@ -298,6 +354,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 || undefined,
|
||||
action: AuditAction.ERROR,
|
||||
entityType: 'Course',
|
||||
entityId: courseId,
|
||||
metadata: {
|
||||
operation: 'delete_course',
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
}
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
|
@ -325,6 +392,17 @@ export class CoursesInstructorService {
|
|||
};
|
||||
} 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 || 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -347,6 +425,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 || 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -384,6 +473,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 || undefined,
|
||||
action: AuditAction.ERROR,
|
||||
entityType: 'Course',
|
||||
entityId: courseId,
|
||||
metadata: {
|
||||
operation: 'get_course_approvals',
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
}
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
|
@ -445,6 +545,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 || undefined,
|
||||
action: AuditAction.ERROR,
|
||||
entityType: 'Course',
|
||||
entityId: input.course_id,
|
||||
metadata: {
|
||||
operation: 'search_instructors',
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
}
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
|
@ -496,6 +607,17 @@ export class CoursesInstructorService {
|
|||
};
|
||||
} 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 || 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -517,6 +639,17 @@ export class CoursesInstructorService {
|
|||
};
|
||||
} 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 || 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -567,6 +700,17 @@ 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 || 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -591,6 +735,17 @@ export class CoursesInstructorService {
|
|||
};
|
||||
} 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 || 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -707,6 +862,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 || 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -874,6 +1040,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 || 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -988,6 +1165,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 || 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -1125,6 +1313,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 || 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -1181,6 +1380,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 || undefined,
|
||||
action: AuditAction.ERROR,
|
||||
entityType: 'Course',
|
||||
entityId: courseId,
|
||||
metadata: {
|
||||
operation: 'get_course_approval_history',
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
}
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,19 +37,23 @@ export class AuditService {
|
|||
* Log พร้อม await (สำหรับ critical actions)
|
||||
*/
|
||||
async logSync(params: CreateAuditLogParams): Promise<void> {
|
||||
await prisma.auditLog.create({
|
||||
data: {
|
||||
user_id: params.userId,
|
||||
action: params.action,
|
||||
entity_type: params.entityType,
|
||||
entity_id: params.entityId,
|
||||
old_value: params.oldValue,
|
||||
new_value: params.newValue,
|
||||
ip_address: params.ipAddress,
|
||||
user_agent: params.userAgent,
|
||||
metadata: params.metadata,
|
||||
},
|
||||
});
|
||||
try {
|
||||
await prisma.auditLog.create({
|
||||
data: {
|
||||
user_id: params.userId,
|
||||
action: params.action,
|
||||
entity_type: params.entityType,
|
||||
entity_id: params.entityId,
|
||||
old_value: params.oldValue,
|
||||
new_value: params.newValue,
|
||||
ip_address: params.ipAddress,
|
||||
user_agent: params.userAgent,
|
||||
metadata: params.metadata,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Failed to create audit log (sync)', { error, params });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -23,6 +23,11 @@ export interface createCourseResponse {
|
|||
data: Course;
|
||||
}
|
||||
|
||||
export interface ListMyCoursesInput {
|
||||
token: string;
|
||||
status?: 'DRAFT' | 'PENDING' | 'APPROVED' | 'REJECTED' | 'ARCHIVED';
|
||||
}
|
||||
|
||||
export interface ListMyCourseResponse {
|
||||
code: number;
|
||||
message: string;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue