feat: Add token-based authorization to category deletion and enhance user registration with error handling and audit logging.
This commit is contained in:
parent
11f9ad57cd
commit
bb38c0f3c9
16 changed files with 1202 additions and 237 deletions
|
|
@ -634,6 +634,8 @@ enum AuditAction {
|
|||
VERIFY_EMAIL
|
||||
DEACTIVATE_USER
|
||||
ACTIVATE_USER
|
||||
ERROR
|
||||
WARNING
|
||||
}
|
||||
|
||||
model AuditLog {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -62,13 +58,11 @@ export class RecommendedCoursesController {
|
|||
@Response('404', 'Course not found')
|
||||
public async toggleRecommended(
|
||||
@Request() request: any,
|
||||
@Path() courseId: number,
|
||||
@Path() courseId: number,
|
||||
@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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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' },
|
||||
|
|
@ -68,18 +68,18 @@ export class AdminCourseApprovalService {
|
|||
description: course.description as { th: string; en: string },
|
||||
thumbnail_url: thumbnail_presigned_url,
|
||||
status: course.status,
|
||||
created_at: course.created_at,
|
||||
updated_at: course.updated_at,
|
||||
created_by: course.created_by,
|
||||
creator: course.creator,
|
||||
instructors: course.instructors.map(i => ({
|
||||
user_id: i.user_id,
|
||||
is_primary: i.is_primary,
|
||||
user: i.user
|
||||
})),
|
||||
chapters_count: course.chapters.length,
|
||||
lessons_count: course.chapters.reduce((sum, ch) => sum + ch.lessons.length, 0),
|
||||
latest_submission: course.courseApprovals[0] ? {
|
||||
created_at: course.created_at,
|
||||
updated_at: course.updated_at,
|
||||
created_by: course.created_by,
|
||||
creator: course.creator,
|
||||
instructors: course.instructors.map(i => ({
|
||||
user_id: i.user_id,
|
||||
is_primary: i.is_primary,
|
||||
user: i.user
|
||||
})),
|
||||
chapters_count: course.chapters.length,
|
||||
lessons_count: course.chapters.reduce((sum, ch) => sum + ch.lessons.length, 0),
|
||||
latest_submission: course.courseApprovals[0] ? {
|
||||
id: course.courseApprovals[0].id,
|
||||
submitted_by: course.courseApprovals[0].submitted_by,
|
||||
created_at: course.courseApprovals[0].created_at,
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -238,7 +258,7 @@ export class AdminCourseApprovalService {
|
|||
// Update course status
|
||||
prisma.course.update({
|
||||
where: { id: courseId },
|
||||
data: {
|
||||
data: {
|
||||
status: 'APPROVED',
|
||||
approved_by: decoded.id,
|
||||
approved_at: new Date()
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -303,12 +334,12 @@ export class AdminCourseApprovalService {
|
|||
// Update course status back to REJECTED
|
||||
prisma.course.update({
|
||||
where: { id: courseId },
|
||||
data: {
|
||||
data: {
|
||||
status: 'REJECTED',
|
||||
rejection_reason: comment,
|
||||
approved_by: null,
|
||||
approved_at: null
|
||||
}
|
||||
}
|
||||
}),
|
||||
// Create rejection record
|
||||
prisma.courseApproval.create({
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -758,7 +965,7 @@ export class CoursesInstructorService {
|
|||
|
||||
// Get all enrolled students who have attempted this quiz
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
|
||||
// Get unique users who attempted this quiz
|
||||
const quizAttempts = await prisma.quizAttempt.findMany({
|
||||
where: { quiz_id: lesson.quiz.id },
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -83,167 +83,201 @@ export class AuthService {
|
|||
* User registration
|
||||
*/
|
||||
async register(data: RegisterRequest): Promise<RegisterResponse> {
|
||||
const { username, email, password, first_name, last_name, prefix, phone } = data;
|
||||
try {
|
||||
const { username, email, password, first_name, last_name, prefix, phone } = data;
|
||||
|
||||
// Check if username already exists
|
||||
const existingUsername = await prisma.user.findUnique({
|
||||
where: { username }
|
||||
});
|
||||
// Check if username already exists
|
||||
const existingUsername = await prisma.user.findUnique({
|
||||
where: { username }
|
||||
});
|
||||
|
||||
if (existingUsername) {
|
||||
throw new ValidationError('Username already exists');
|
||||
}
|
||||
|
||||
// Check if email already exists
|
||||
const existingEmail = await prisma.user.findUnique({
|
||||
where: { email }
|
||||
});
|
||||
|
||||
if (existingEmail) {
|
||||
throw new ValidationError('Email already exists');
|
||||
}
|
||||
|
||||
// Check if phone number already exists in user profiles
|
||||
const existingPhone = await prisma.userProfile.findFirst({
|
||||
where: { phone }
|
||||
});
|
||||
|
||||
if (existingPhone) {
|
||||
throw new ValidationError('Phone number already exists');
|
||||
}
|
||||
|
||||
// Get STUDENT role
|
||||
const studentRole = await prisma.role.findUnique({
|
||||
where: { code: 'STUDENT' }
|
||||
});
|
||||
|
||||
if (!studentRole) {
|
||||
logger.error('STUDENT role not found in database');
|
||||
throw new Error('System configuration error');
|
||||
}
|
||||
|
||||
// Hash password
|
||||
const hashedPassword = await bcrypt.hash(password, 10);
|
||||
|
||||
// Create user with profile
|
||||
const user = await prisma.user.create({
|
||||
data: {
|
||||
username,
|
||||
email,
|
||||
password: hashedPassword,
|
||||
role_id: studentRole.id,
|
||||
profile: {
|
||||
create: {
|
||||
prefix: prefix ? prefix as Prisma.InputJsonValue : Prisma.JsonNull,
|
||||
first_name,
|
||||
last_name,
|
||||
phone
|
||||
}
|
||||
}
|
||||
},
|
||||
include: {
|
||||
role: true,
|
||||
profile: true
|
||||
if (existingUsername) {
|
||||
throw new ValidationError('Username already exists');
|
||||
}
|
||||
});
|
||||
|
||||
logger.info('New user registered', { userId: user.id, username: user.username });
|
||||
// Check if email already exists
|
||||
const existingEmail = await prisma.user.findUnique({
|
||||
where: { email }
|
||||
});
|
||||
|
||||
// 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' }
|
||||
});
|
||||
if (existingEmail) {
|
||||
throw new ValidationError('Email already exists');
|
||||
}
|
||||
|
||||
return {
|
||||
user: this.formatUserResponseSync(user),
|
||||
message: 'Registration successful'
|
||||
};
|
||||
// Check if phone number already exists in user profiles
|
||||
const existingPhone = await prisma.userProfile.findFirst({
|
||||
where: { phone }
|
||||
});
|
||||
|
||||
if (existingPhone) {
|
||||
throw new ValidationError('Phone number already exists');
|
||||
}
|
||||
|
||||
// Get STUDENT role
|
||||
const studentRole = await prisma.role.findUnique({
|
||||
where: { code: 'STUDENT' }
|
||||
});
|
||||
|
||||
if (!studentRole) {
|
||||
logger.error('STUDENT role not found in database');
|
||||
throw new Error('System configuration error');
|
||||
}
|
||||
|
||||
// Hash password
|
||||
const hashedPassword = await bcrypt.hash(password, 10);
|
||||
|
||||
// Create user with profile
|
||||
const user = await prisma.user.create({
|
||||
data: {
|
||||
username,
|
||||
email,
|
||||
password: hashedPassword,
|
||||
role_id: studentRole.id,
|
||||
profile: {
|
||||
create: {
|
||||
prefix: prefix ? prefix as Prisma.InputJsonValue : Prisma.JsonNull,
|
||||
first_name,
|
||||
last_name,
|
||||
phone
|
||||
}
|
||||
}
|
||||
},
|
||||
include: {
|
||||
role: true,
|
||||
profile: true
|
||||
}
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
async registerInstructor(data: RegisterRequest): Promise<RegisterResponse> {
|
||||
const { username, email, password, first_name, last_name, prefix, phone } = data;
|
||||
try {
|
||||
const { username, email, password, first_name, last_name, prefix, phone } = data;
|
||||
|
||||
// Check if username already exists
|
||||
const existingUsername = await prisma.user.findUnique({
|
||||
where: { username }
|
||||
});
|
||||
// Check if username already exists
|
||||
const existingUsername = await prisma.user.findUnique({
|
||||
where: { username }
|
||||
});
|
||||
|
||||
if (existingUsername) {
|
||||
throw new ValidationError('Username already exists');
|
||||
}
|
||||
|
||||
// Check if email already exists
|
||||
const existingEmail = await prisma.user.findUnique({
|
||||
where: { email }
|
||||
});
|
||||
|
||||
if (existingEmail) {
|
||||
throw new ValidationError('Email already exists');
|
||||
}
|
||||
|
||||
// Check if phone number already exists in user profiles
|
||||
const existingPhone = await prisma.userProfile.findFirst({
|
||||
where: { phone }
|
||||
});
|
||||
|
||||
if (existingPhone) {
|
||||
throw new ValidationError('Phone number already exists');
|
||||
}
|
||||
|
||||
// Get INSTRUCTOR role
|
||||
const instructorRole = await prisma.role.findUnique({
|
||||
where: { code: 'INSTRUCTOR' }
|
||||
});
|
||||
|
||||
if (!instructorRole) {
|
||||
logger.error('INSTRUCTOR role not found in database');
|
||||
throw new Error('System configuration error');
|
||||
}
|
||||
|
||||
// Hash password
|
||||
const hashedPassword = await bcrypt.hash(password, 10);
|
||||
|
||||
// Create user with profile
|
||||
const user = await prisma.user.create({
|
||||
data: {
|
||||
username,
|
||||
email,
|
||||
password: hashedPassword,
|
||||
role_id: instructorRole.id,
|
||||
profile: {
|
||||
create: {
|
||||
prefix: prefix ? prefix as Prisma.InputJsonValue : Prisma.JsonNull,
|
||||
first_name,
|
||||
last_name,
|
||||
phone
|
||||
}
|
||||
}
|
||||
},
|
||||
include: {
|
||||
role: true,
|
||||
profile: true
|
||||
if (existingUsername) {
|
||||
throw new ValidationError('Username already exists');
|
||||
}
|
||||
});
|
||||
|
||||
logger.info('New user registered', { userId: user.id, username: user.username });
|
||||
// Check if email already exists
|
||||
const existingEmail = await prisma.user.findUnique({
|
||||
where: { email }
|
||||
});
|
||||
|
||||
// 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' }
|
||||
});
|
||||
if (existingEmail) {
|
||||
throw new ValidationError('Email already exists');
|
||||
}
|
||||
|
||||
return {
|
||||
user: this.formatUserResponseSync(user),
|
||||
message: 'Registration successful'
|
||||
};
|
||||
// Check if phone number already exists in user profiles
|
||||
const existingPhone = await prisma.userProfile.findFirst({
|
||||
where: { phone }
|
||||
});
|
||||
|
||||
if (existingPhone) {
|
||||
throw new ValidationError('Phone number already exists');
|
||||
}
|
||||
|
||||
// Get INSTRUCTOR role
|
||||
const instructorRole = await prisma.role.findUnique({
|
||||
where: { code: 'INSTRUCTOR' }
|
||||
});
|
||||
|
||||
if (!instructorRole) {
|
||||
logger.error('INSTRUCTOR role not found in database');
|
||||
throw new Error('System configuration error');
|
||||
}
|
||||
|
||||
// Hash password
|
||||
const hashedPassword = await bcrypt.hash(password, 10);
|
||||
|
||||
// Create user with profile
|
||||
const user = await prisma.user.create({
|
||||
data: {
|
||||
username,
|
||||
email,
|
||||
password: hashedPassword,
|
||||
role_id: instructorRole.id,
|
||||
profile: {
|
||||
create: {
|
||||
prefix: prefix ? prefix as Prisma.InputJsonValue : Prisma.JsonNull,
|
||||
first_name,
|
||||
last_name,
|
||||
phone
|
||||
}
|
||||
}
|
||||
},
|
||||
include: {
|
||||
role: true,
|
||||
profile: true
|
||||
}
|
||||
});
|
||||
|
||||
logger.info('New user registered', { userId: user.id, username: user.username });
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue