feat: Add token-based authorization to category deletion and enhance user registration with error handling and audit logging.
This commit is contained in:
parent
45941fbe6c
commit
af14610442
16 changed files with 1003 additions and 236 deletions
|
|
@ -634,6 +634,8 @@ enum AuditAction {
|
||||||
VERIFY_EMAIL
|
VERIFY_EMAIL
|
||||||
DEACTIVATE_USER
|
DEACTIVATE_USER
|
||||||
ACTIVATE_USER
|
ACTIVATE_USER
|
||||||
|
ERROR
|
||||||
|
WARNING
|
||||||
}
|
}
|
||||||
|
|
||||||
model AuditLog {
|
model AuditLog {
|
||||||
|
|
|
||||||
|
|
@ -25,10 +25,8 @@ export class AdminCourseApprovalController {
|
||||||
@Response('403', 'Forbidden - Admin only')
|
@Response('403', 'Forbidden - Admin only')
|
||||||
public async listPendingCourses(@Request() request: any): Promise<ListPendingCoursesResponse> {
|
public async listPendingCourses(@Request() request: any): Promise<ListPendingCoursesResponse> {
|
||||||
const token = request.headers.authorization?.replace('Bearer ', '');
|
const token = request.headers.authorization?.replace('Bearer ', '');
|
||||||
if (!token) {
|
if (!token) throw new ValidationError('No token provided');
|
||||||
throw new ValidationError('No token provided');
|
return await AdminCourseApprovalService.listPendingCourses(token);
|
||||||
}
|
|
||||||
return await AdminCourseApprovalService.listPendingCourses();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -44,10 +42,8 @@ export class AdminCourseApprovalController {
|
||||||
@Response('404', 'Course not found')
|
@Response('404', 'Course not found')
|
||||||
public async getCourseDetail(@Request() request: any, @Path() courseId: number): Promise<GetCourseDetailForAdminResponse> {
|
public async getCourseDetail(@Request() request: any, @Path() courseId: number): Promise<GetCourseDetailForAdminResponse> {
|
||||||
const token = request.headers.authorization?.replace('Bearer ', '');
|
const token = request.headers.authorization?.replace('Bearer ', '');
|
||||||
if (!token) {
|
if (!token) throw new ValidationError('No token provided');
|
||||||
throw new ValidationError('No token provided');
|
return await AdminCourseApprovalService.getCourseDetail(token, courseId);
|
||||||
}
|
|
||||||
return await AdminCourseApprovalService.getCourseDetail(courseId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -68,9 +64,7 @@ export class AdminCourseApprovalController {
|
||||||
@Body() body?: ApproveCourseBody
|
@Body() body?: ApproveCourseBody
|
||||||
): Promise<ApproveCourseResponse> {
|
): Promise<ApproveCourseResponse> {
|
||||||
const token = request.headers.authorization?.replace('Bearer ', '');
|
const token = request.headers.authorization?.replace('Bearer ', '');
|
||||||
if (!token) {
|
if (!token) throw new ValidationError('No token provided');
|
||||||
throw new ValidationError('No token provided');
|
|
||||||
}
|
|
||||||
return await AdminCourseApprovalService.approveCourse(token, courseId, body?.comment);
|
return await AdminCourseApprovalService.approveCourse(token, courseId, body?.comment);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -92,9 +86,7 @@ export class AdminCourseApprovalController {
|
||||||
@Body() body: RejectCourseBody
|
@Body() body: RejectCourseBody
|
||||||
): Promise<RejectCourseResponse> {
|
): Promise<RejectCourseResponse> {
|
||||||
const token = request.headers.authorization?.replace('Bearer ', '');
|
const token = request.headers.authorization?.replace('Bearer ', '');
|
||||||
if (!token) {
|
if (!token) throw new ValidationError('No token provided');
|
||||||
throw new ValidationError('No token provided');
|
|
||||||
}
|
|
||||||
return await AdminCourseApprovalService.rejectCourse(token, courseId, body.comment);
|
return await AdminCourseApprovalService.rejectCourse(token, courseId, body.comment);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,6 @@ export class CategoriesAdminController {
|
||||||
@Response('401', 'Invalid or expired token')
|
@Response('401', 'Invalid or expired token')
|
||||||
public async deleteCategory(@Request() request: any, @Path() id: number): Promise<deleteCategoryResponse> {
|
public async deleteCategory(@Request() request: any, @Path() id: number): Promise<deleteCategoryResponse> {
|
||||||
const token = request.headers.authorization?.replace('Bearer ', '') || '';
|
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')
|
@Response('403', 'Forbidden - Admin only')
|
||||||
public async listApprovedCourses(@Request() request: any): Promise<ListApprovedCoursesResponse> {
|
public async listApprovedCourses(@Request() request: any): Promise<ListApprovedCoursesResponse> {
|
||||||
const token = request.headers.authorization?.replace('Bearer ', '');
|
const token = request.headers.authorization?.replace('Bearer ', '');
|
||||||
if (!token) {
|
if (!token) throw new ValidationError('No token provided');
|
||||||
throw new ValidationError('No token provided');
|
return await RecommendedCoursesService.listApprovedCourses(token);
|
||||||
}
|
|
||||||
return await RecommendedCoursesService.listApprovedCourses();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -42,10 +40,8 @@ export class RecommendedCoursesController {
|
||||||
@Response('404', 'Course not found')
|
@Response('404', 'Course not found')
|
||||||
public async getCourseById(@Request() request: any, @Path() courseId: number): Promise<GetCourseByIdResponse> {
|
public async getCourseById(@Request() request: any, @Path() courseId: number): Promise<GetCourseByIdResponse> {
|
||||||
const token = request.headers.authorization?.replace('Bearer ', '');
|
const token = request.headers.authorization?.replace('Bearer ', '');
|
||||||
if (!token) {
|
if (!token) throw new ValidationError('No token provided');
|
||||||
throw new ValidationError('No token provided');
|
return await RecommendedCoursesService.getCourseById(token, courseId);
|
||||||
}
|
|
||||||
return await RecommendedCoursesService.getCourseById(courseId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -62,13 +58,11 @@ export class RecommendedCoursesController {
|
||||||
@Response('404', 'Course not found')
|
@Response('404', 'Course not found')
|
||||||
public async toggleRecommended(
|
public async toggleRecommended(
|
||||||
@Request() request: any,
|
@Request() request: any,
|
||||||
@Path() courseId: number,
|
@Path() courseId: number,
|
||||||
@Query() is_recommended: boolean
|
@Query() is_recommended: boolean
|
||||||
): Promise<ToggleRecommendedResponse> {
|
): Promise<ToggleRecommendedResponse> {
|
||||||
const token = request.headers.authorization?.replace('Bearer ', '');
|
const token = request.headers.authorization?.replace('Bearer ', '');
|
||||||
if (!token) {
|
if (!token) throw new ValidationError('No token provided');
|
||||||
throw new ValidationError('No token provided');
|
|
||||||
}
|
|
||||||
return await RecommendedCoursesService.toggleRecommended(token, courseId, is_recommended);
|
return await RecommendedCoursesService.toggleRecommended(token, courseId, is_recommended);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ export class AdminCourseApprovalService {
|
||||||
/**
|
/**
|
||||||
* Get all pending courses for admin review
|
* Get all pending courses for admin review
|
||||||
*/
|
*/
|
||||||
static async listPendingCourses(): Promise<ListPendingCoursesResponse> {
|
static async listPendingCourses(token: string): Promise<ListPendingCoursesResponse> {
|
||||||
try {
|
try {
|
||||||
const courses = await prisma.course.findMany({
|
const courses = await prisma.course.findMany({
|
||||||
where: { status: 'PENDING' },
|
where: { status: 'PENDING' },
|
||||||
|
|
@ -68,18 +68,18 @@ export class AdminCourseApprovalService {
|
||||||
description: course.description as { th: string; en: string },
|
description: course.description as { th: string; en: string },
|
||||||
thumbnail_url: thumbnail_presigned_url,
|
thumbnail_url: thumbnail_presigned_url,
|
||||||
status: course.status,
|
status: course.status,
|
||||||
created_at: course.created_at,
|
created_at: course.created_at,
|
||||||
updated_at: course.updated_at,
|
updated_at: course.updated_at,
|
||||||
created_by: course.created_by,
|
created_by: course.created_by,
|
||||||
creator: course.creator,
|
creator: course.creator,
|
||||||
instructors: course.instructors.map(i => ({
|
instructors: course.instructors.map(i => ({
|
||||||
user_id: i.user_id,
|
user_id: i.user_id,
|
||||||
is_primary: i.is_primary,
|
is_primary: i.is_primary,
|
||||||
user: i.user
|
user: i.user
|
||||||
})),
|
})),
|
||||||
chapters_count: course.chapters.length,
|
chapters_count: course.chapters.length,
|
||||||
lessons_count: course.chapters.reduce((sum, ch) => sum + ch.lessons.length, 0),
|
lessons_count: course.chapters.reduce((sum, ch) => sum + ch.lessons.length, 0),
|
||||||
latest_submission: course.courseApprovals[0] ? {
|
latest_submission: course.courseApprovals[0] ? {
|
||||||
id: course.courseApprovals[0].id,
|
id: course.courseApprovals[0].id,
|
||||||
submitted_by: course.courseApprovals[0].submitted_by,
|
submitted_by: course.courseApprovals[0].submitted_by,
|
||||||
created_at: course.courseApprovals[0].created_at,
|
created_at: course.courseApprovals[0].created_at,
|
||||||
|
|
@ -96,6 +96,16 @@ export class AdminCourseApprovalService {
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Failed to list pending courses', { 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;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -103,7 +113,7 @@ export class AdminCourseApprovalService {
|
||||||
/**
|
/**
|
||||||
* Get course details for admin review
|
* Get course details for admin review
|
||||||
*/
|
*/
|
||||||
static async getCourseDetail(courseId: number): Promise<GetCourseDetailForAdminResponse> {
|
static async getCourseDetail(token: string,courseId: number): Promise<GetCourseDetailForAdminResponse> {
|
||||||
try {
|
try {
|
||||||
const course = await prisma.course.findUnique({
|
const course = await prisma.course.findUnique({
|
||||||
where: { id: courseId },
|
where: { id: courseId },
|
||||||
|
|
@ -214,6 +224,16 @@ export class AdminCourseApprovalService {
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Failed to get course detail', { 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;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -238,7 +258,7 @@ export class AdminCourseApprovalService {
|
||||||
// Update course status
|
// Update course status
|
||||||
prisma.course.update({
|
prisma.course.update({
|
||||||
where: { id: courseId },
|
where: { id: courseId },
|
||||||
data: {
|
data: {
|
||||||
status: 'APPROVED',
|
status: 'APPROVED',
|
||||||
approved_by: decoded.id,
|
approved_by: decoded.id,
|
||||||
approved_at: new Date()
|
approved_at: new Date()
|
||||||
|
|
@ -275,6 +295,17 @@ export class AdminCourseApprovalService {
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Failed to approve course', { 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;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -303,12 +334,12 @@ export class AdminCourseApprovalService {
|
||||||
// Update course status back to REJECTED
|
// Update course status back to REJECTED
|
||||||
prisma.course.update({
|
prisma.course.update({
|
||||||
where: { id: courseId },
|
where: { id: courseId },
|
||||||
data: {
|
data: {
|
||||||
status: 'REJECTED',
|
status: 'REJECTED',
|
||||||
rejection_reason: comment,
|
rejection_reason: comment,
|
||||||
approved_by: null,
|
approved_by: null,
|
||||||
approved_at: null
|
approved_at: null
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
// Create rejection record
|
// Create rejection record
|
||||||
prisma.courseApproval.create({
|
prisma.courseApproval.create({
|
||||||
|
|
@ -341,6 +372,17 @@ export class AdminCourseApprovalService {
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Failed to reject course', { 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;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -142,6 +142,17 @@ export class ChaptersLessonService {
|
||||||
return { code: 200, message: 'Chapter created successfully', data: chapter as ChapterData };
|
return { code: 200, message: 'Chapter created successfully', data: chapter as ChapterData };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Error creating chapter: ${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;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -163,6 +174,17 @@ export class ChaptersLessonService {
|
||||||
return { code: 200, message: 'Chapter updated successfully', data: chapter as ChapterData };
|
return { code: 200, message: 'Chapter updated successfully', data: chapter as ChapterData };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Error updating chapter: ${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;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -197,6 +219,17 @@ export class ChaptersLessonService {
|
||||||
return { code: 200, message: 'Chapter deleted successfully' };
|
return { code: 200, message: 'Chapter deleted successfully' };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Error deleting chapter: ${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;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -280,6 +313,17 @@ export class ChaptersLessonService {
|
||||||
return { code: 200, message: 'Chapter reordered successfully', data: chapters as ChapterData[] };
|
return { code: 200, message: 'Chapter reordered successfully', data: chapters as ChapterData[] };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Error reordering chapter: ${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;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -354,6 +398,17 @@ export class ChaptersLessonService {
|
||||||
return { code: 200, message: 'Lesson created successfully', data: lesson as LessonData };
|
return { code: 200, message: 'Lesson created successfully', data: lesson as LessonData };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Error creating lesson: ${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;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -494,6 +549,17 @@ export class ChaptersLessonService {
|
||||||
return { code: 200, message: 'Lesson fetched successfully', data: lessonData as LessonData };
|
return { code: 200, message: 'Lesson fetched successfully', data: lessonData as LessonData };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Error fetching lesson: ${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;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -515,6 +581,17 @@ export class ChaptersLessonService {
|
||||||
return { code: 200, message: 'Lesson updated successfully', data: lesson as LessonData };
|
return { code: 200, message: 'Lesson updated successfully', data: lesson as LessonData };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Error updating lesson: ${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;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -605,6 +682,17 @@ export class ChaptersLessonService {
|
||||||
return { code: 200, message: 'Lessons reordered successfully', data: lessons as LessonData[] };
|
return { code: 200, message: 'Lessons reordered successfully', data: lessons as LessonData[] };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Error reordering lessons: ${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;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -676,6 +764,17 @@ export class ChaptersLessonService {
|
||||||
return { code: 200, message: 'Lesson deleted successfully' };
|
return { code: 200, message: 'Lesson deleted successfully' };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Error deleting lesson: ${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;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -754,6 +853,17 @@ export class ChaptersLessonService {
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Error uploading video: ${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;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -836,6 +946,17 @@ export class ChaptersLessonService {
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Error updating video: ${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;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -917,6 +1038,17 @@ export class ChaptersLessonService {
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Error setting YouTube video: ${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;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -993,6 +1125,17 @@ export class ChaptersLessonService {
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Error uploading attachment: ${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;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1051,6 +1194,17 @@ export class ChaptersLessonService {
|
||||||
return { code: 200, message: 'Attachment deleted successfully' };
|
return { code: 200, message: 'Attachment deleted successfully' };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Error deleting attachment: ${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;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1127,6 +1281,17 @@ export class ChaptersLessonService {
|
||||||
return { code: 200, message: 'Question added successfully', data: completeQuestion as QuizQuestionData };
|
return { code: 200, message: 'Question added successfully', data: completeQuestion as QuizQuestionData };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Error adding question: ${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;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1202,6 +1367,17 @@ export class ChaptersLessonService {
|
||||||
return { code: 200, message: 'Question updated successfully', data: completeQuestion as QuizQuestionData };
|
return { code: 200, message: 'Question updated successfully', data: completeQuestion as QuizQuestionData };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Error updating question: ${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;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1295,6 +1471,17 @@ export class ChaptersLessonService {
|
||||||
return { code: 200, message: 'Question reordered successfully', data: questions as QuizQuestionData[] };
|
return { code: 200, message: 'Question reordered successfully', data: questions as QuizQuestionData[] };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Error reordering question: ${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;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1343,6 +1530,17 @@ export class ChaptersLessonService {
|
||||||
return { code: 200, message: 'Question deleted successfully' };
|
return { code: 200, message: 'Question deleted successfully' };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Error deleting question: ${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;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -347,6 +347,15 @@ export class CoursesInstructorService {
|
||||||
id: courseId
|
id: courseId
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
await auditService.logSync({
|
||||||
|
userId: courseInstructorId.user_id,
|
||||||
|
action: AuditAction.DELETE,
|
||||||
|
entityType: 'Course',
|
||||||
|
entityId: courseId,
|
||||||
|
metadata: {
|
||||||
|
operation: 'delete_course'
|
||||||
|
}
|
||||||
|
});
|
||||||
return {
|
return {
|
||||||
code: 200,
|
code: 200,
|
||||||
message: 'Course deleted successfully',
|
message: 'Course deleted successfully',
|
||||||
|
|
@ -386,6 +395,15 @@ export class CoursesInstructorService {
|
||||||
status: 'PENDING'
|
status: 'PENDING'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
await auditService.logSync({
|
||||||
|
userId: decoded.id,
|
||||||
|
action: AuditAction.UPDATE,
|
||||||
|
entityType: 'Course',
|
||||||
|
entityId: sendCourseForReview.course_id,
|
||||||
|
metadata: {
|
||||||
|
operation: 'send_course_for_review'
|
||||||
|
}
|
||||||
|
});
|
||||||
return {
|
return {
|
||||||
code: 200,
|
code: 200,
|
||||||
message: 'Course sent for review successfully',
|
message: 'Course sent for review successfully',
|
||||||
|
|
@ -447,8 +465,6 @@ export class CoursesInstructorService {
|
||||||
total: number;
|
total: number;
|
||||||
}> {
|
}> {
|
||||||
try {
|
try {
|
||||||
const decoded = jwt.verify(token, config.jwt.secret) as { id: number; type: string };
|
|
||||||
|
|
||||||
// Validate instructor access
|
// Validate instructor access
|
||||||
await this.validateCourseInstructor(token, courseId);
|
await this.validateCourseInstructor(token, courseId);
|
||||||
|
|
||||||
|
|
@ -601,6 +617,18 @@ 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 {
|
return {
|
||||||
code: 200,
|
code: 200,
|
||||||
message: 'Instructor added to course successfully',
|
message: 'Instructor added to course successfully',
|
||||||
|
|
@ -633,6 +661,19 @@ 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 {
|
return {
|
||||||
code: 200,
|
code: 200,
|
||||||
message: 'Instructor removed from course successfully',
|
message: 'Instructor removed from course successfully',
|
||||||
|
|
@ -729,6 +770,19 @@ export class CoursesInstructorService {
|
||||||
is_primary: true,
|
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 {
|
return {
|
||||||
code: 200,
|
code: 200,
|
||||||
message: 'Primary instructor set successfully',
|
message: 'Primary instructor set successfully',
|
||||||
|
|
@ -784,7 +838,6 @@ export class CoursesInstructorService {
|
||||||
static async getEnrolledStudents(input: GetEnrolledStudentsInput): Promise<GetEnrolledStudentsResponse> {
|
static async getEnrolledStudents(input: GetEnrolledStudentsInput): Promise<GetEnrolledStudentsResponse> {
|
||||||
try {
|
try {
|
||||||
const { token, course_id, page = 1, limit = 20, search, status } = input;
|
const { token, course_id, page = 1, limit = 20, search, status } = input;
|
||||||
const decoded = jwt.verify(token, config.jwt.secret) as { id: number };
|
|
||||||
|
|
||||||
// Validate instructor
|
// Validate instructor
|
||||||
await this.validateCourseInstructor(token, course_id);
|
await this.validateCourseInstructor(token, course_id);
|
||||||
|
|
@ -1062,7 +1115,6 @@ export class CoursesInstructorService {
|
||||||
static async getQuizAttemptDetail(input: GetQuizAttemptDetailInput): Promise<GetQuizAttemptDetailResponse> {
|
static async getQuizAttemptDetail(input: GetQuizAttemptDetailInput): Promise<GetQuizAttemptDetailResponse> {
|
||||||
try {
|
try {
|
||||||
const { token, course_id, lesson_id, student_id } = input;
|
const { token, course_id, lesson_id, student_id } = input;
|
||||||
const decoded = jwt.verify(token, config.jwt.secret) as { id: number };
|
|
||||||
|
|
||||||
// Validate instructor
|
// Validate instructor
|
||||||
await this.validateCourseInstructor(token, course_id);
|
await this.validateCourseInstructor(token, course_id);
|
||||||
|
|
|
||||||
|
|
@ -186,7 +186,20 @@ export class CoursesStudentService {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} 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;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -261,6 +274,17 @@ export class CoursesStudentService {
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(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;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -416,6 +440,17 @@ export class CoursesStudentService {
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(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;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -678,6 +713,17 @@ export class CoursesStudentService {
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(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;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -866,6 +912,17 @@ export class CoursesStudentService {
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(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;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -940,6 +997,17 @@ export class CoursesStudentService {
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(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;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1037,6 +1105,17 @@ export class CoursesStudentService {
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(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;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1168,7 +1247,19 @@ export class CoursesStudentService {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} 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;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1213,22 +1304,14 @@ export class CoursesStudentService {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!lesson) {
|
if (!lesson) throw new NotFoundError('Lesson not found');
|
||||||
throw new NotFoundError('Lesson not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lesson.type !== 'QUIZ') {
|
if (lesson.type !== 'QUIZ') throw new ValidationError('This lesson is not a quiz');
|
||||||
throw new ValidationError('This lesson is not a quiz');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!lesson.quiz) {
|
if (!lesson.quiz) throw new NotFoundError('Quiz not found for this lesson');
|
||||||
throw new NotFoundError('Quiz not found for this lesson');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify lesson belongs to the course
|
// Verify lesson belongs to the course
|
||||||
if (lesson.chapter.course_id !== course_id) {
|
if (lesson.chapter.course_id !== course_id) throw new NotFoundError('Lesson not found in this course');
|
||||||
throw new NotFoundError('Lesson not found in this course');
|
|
||||||
}
|
|
||||||
|
|
||||||
const quiz = lesson.quiz;
|
const quiz = lesson.quiz;
|
||||||
|
|
||||||
|
|
@ -1332,7 +1415,20 @@ export class CoursesStudentService {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} 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;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1373,22 +1469,14 @@ export class CoursesStudentService {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!lesson) {
|
if (!lesson) throw new NotFoundError('Lesson not found');
|
||||||
throw new NotFoundError('Lesson not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lesson.type !== 'QUIZ') {
|
if (lesson.type !== 'QUIZ') throw new ValidationError('This lesson is not a quiz');
|
||||||
throw new ValidationError('This lesson is not a quiz');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!lesson.quiz) {
|
if (!lesson.quiz) throw new NotFoundError('Quiz not found for this lesson');
|
||||||
throw new NotFoundError('Quiz not found for this lesson');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify lesson belongs to the course
|
// Verify lesson belongs to the course
|
||||||
if (lesson.chapter.course_id !== course_id) {
|
if (lesson.chapter.course_id !== course_id) throw new NotFoundError('Lesson not found in this course');
|
||||||
throw new NotFoundError('Lesson not found in this course');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get all quiz attempts for this user
|
// Get all quiz attempts for this user
|
||||||
const attempts = await prisma.quizAttempt.findMany({
|
const attempts = await prisma.quizAttempt.findMany({
|
||||||
|
|
@ -1438,6 +1526,21 @@ export class CoursesStudentService {
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(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;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ export class RecommendedCoursesService {
|
||||||
/**
|
/**
|
||||||
* List all approved courses (for admin to manage recommendations)
|
* List all approved courses (for admin to manage recommendations)
|
||||||
*/
|
*/
|
||||||
static async listApprovedCourses(): Promise<ListApprovedCoursesResponse> {
|
static async listApprovedCourses(token: string): Promise<ListApprovedCoursesResponse> {
|
||||||
try {
|
try {
|
||||||
const courses = await prisma.course.findMany({
|
const courses = await prisma.course.findMany({
|
||||||
where: { status: 'APPROVED' },
|
where: { status: 'APPROVED' },
|
||||||
|
|
@ -94,6 +94,19 @@ export class RecommendedCoursesService {
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Failed to list approved courses', { 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;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -101,7 +114,7 @@ export class RecommendedCoursesService {
|
||||||
/**
|
/**
|
||||||
* Get course by ID (for admin to view details)
|
* 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 {
|
try {
|
||||||
const course = await prisma.course.findUnique({
|
const course = await prisma.course.findUnique({
|
||||||
where: { id: courseId },
|
where: { id: courseId },
|
||||||
|
|
@ -179,6 +192,19 @@ export class RecommendedCoursesService {
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Failed to get course by ID', { 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;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -229,6 +255,17 @@ export class RecommendedCoursesService {
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Failed to toggle recommended status', { 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;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,8 @@ import {
|
||||||
} from '../types/announcements.types';
|
} from '../types/announcements.types';
|
||||||
import { CoursesInstructorService } from './CoursesInstructor.service';
|
import { CoursesInstructorService } from './CoursesInstructor.service';
|
||||||
import { uploadFile, deleteFile, getPresignedUrl } from '../config/minio';
|
import { uploadFile, deleteFile, getPresignedUrl } from '../config/minio';
|
||||||
|
import { auditService } from './audit.service';
|
||||||
|
import { AuditAction } from '@prisma/client';
|
||||||
|
|
||||||
export class AnnouncementsService {
|
export class AnnouncementsService {
|
||||||
|
|
||||||
|
|
@ -37,9 +39,7 @@ export class AnnouncementsService {
|
||||||
where: { id: decoded.id },
|
where: { id: decoded.id },
|
||||||
include: { role: true },
|
include: { role: true },
|
||||||
});
|
});
|
||||||
if (!user) {
|
if (!user) throw new UnauthorizedError('Invalid token');
|
||||||
throw new UnauthorizedError('Invalid token');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Admin can access all courses
|
// Admin can access all courses
|
||||||
const isAdmin = user.role.code === 'ADMIN';
|
const isAdmin = user.role.code === 'ADMIN';
|
||||||
|
|
@ -130,6 +130,16 @@ export class AnnouncementsService {
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Error listing announcements: ${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;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -226,6 +236,16 @@ export class AnnouncementsService {
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Error creating announcement: ${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;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -300,6 +320,16 @@ export class AnnouncementsService {
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Error updating announcement: ${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;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -346,6 +376,16 @@ export class AnnouncementsService {
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Error deleting announcement: ${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;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -411,6 +451,16 @@ export class AnnouncementsService {
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Error uploading attachment: ${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;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -458,6 +508,16 @@ export class AnnouncementsService {
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Error deleting attachment: ${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;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -83,167 +83,201 @@ export class AuthService {
|
||||||
* User registration
|
* User registration
|
||||||
*/
|
*/
|
||||||
async register(data: RegisterRequest): Promise<RegisterResponse> {
|
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
|
// Check if username already exists
|
||||||
const existingUsername = await prisma.user.findUnique({
|
const existingUsername = await prisma.user.findUnique({
|
||||||
where: { username }
|
where: { username }
|
||||||
});
|
});
|
||||||
|
|
||||||
if (existingUsername) {
|
if (existingUsername) {
|
||||||
throw new ValidationError('Username already exists');
|
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
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
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)
|
if (existingEmail) {
|
||||||
auditService.log({
|
throw new ValidationError('Email already exists');
|
||||||
userId: user.id,
|
}
|
||||||
action: AuditAction.CREATE,
|
|
||||||
entityType: 'User',
|
|
||||||
entityId: user.id,
|
|
||||||
newValue: { username: user.username, email: user.email, role: 'STUDENT' }
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
// Check if phone number already exists in user profiles
|
||||||
user: this.formatUserResponseSync(user),
|
const existingPhone = await prisma.userProfile.findFirst({
|
||||||
message: 'Registration successful'
|
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> {
|
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
|
// Check if username already exists
|
||||||
const existingUsername = await prisma.user.findUnique({
|
const existingUsername = await prisma.user.findUnique({
|
||||||
where: { username }
|
where: { username }
|
||||||
});
|
});
|
||||||
|
|
||||||
if (existingUsername) {
|
if (existingUsername) {
|
||||||
throw new ValidationError('Username already exists');
|
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
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
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)
|
if (existingEmail) {
|
||||||
auditService.log({
|
throw new ValidationError('Email already exists');
|
||||||
userId: user.id,
|
}
|
||||||
action: AuditAction.CREATE,
|
|
||||||
entityType: 'User',
|
|
||||||
entityId: user.id,
|
|
||||||
newValue: { username: user.username, email: user.email, role: 'INSTRUCTOR' }
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
// Check if phone number already exists in user profiles
|
||||||
user: this.formatUserResponseSync(user),
|
const existingPhone = await prisma.userProfile.findFirst({
|
||||||
message: 'Registration successful'
|
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 jwt from 'jsonwebtoken';
|
||||||
import { createCategory, createCategoryResponse, deleteCategoryResponse, updateCategory, updateCategoryResponse, ListCategoriesResponse, Category } from '../types/categories.type';
|
import { createCategory, createCategoryResponse, deleteCategoryResponse, updateCategory, updateCategoryResponse, ListCategoriesResponse, Category } from '../types/categories.type';
|
||||||
import { UnauthorizedError, ValidationError, ForbiddenError } from '../middleware/errorHandler';
|
import { UnauthorizedError, ValidationError, ForbiddenError } from '../middleware/errorHandler';
|
||||||
|
import { auditService } from './audit.service';
|
||||||
|
import { AuditAction } from '@prisma/client';
|
||||||
|
|
||||||
export class CategoryService {
|
export class CategoryService {
|
||||||
async listCategories(): Promise<ListCategoriesResponse> {
|
async listCategories(): Promise<ListCategoriesResponse> {
|
||||||
|
|
@ -30,6 +32,13 @@ export class CategoryService {
|
||||||
const newCategory = await prisma.category.create({
|
const newCategory = await prisma.category.create({
|
||||||
data: category
|
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 {
|
return {
|
||||||
code: 200,
|
code: 200,
|
||||||
message: 'Category created successfully',
|
message: 'Category created successfully',
|
||||||
|
|
@ -43,6 +52,16 @@ export class CategoryService {
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Failed to create category', { 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;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -54,6 +73,13 @@ export class CategoryService {
|
||||||
where: { id },
|
where: { id },
|
||||||
data: category
|
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 {
|
return {
|
||||||
code: 200,
|
code: 200,
|
||||||
message: 'Category updated successfully',
|
message: 'Category updated successfully',
|
||||||
|
|
@ -67,21 +93,49 @@ export class CategoryService {
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Failed to update category', { 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;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteCategory(id: number): Promise<deleteCategoryResponse> {
|
async deleteCategory(token: string, id: number): Promise<deleteCategoryResponse> {
|
||||||
try {
|
try {
|
||||||
|
const decoded = jwt.verify(token, config.jwt.secret) as { id: number; username: string; email: string; roleCode: string };
|
||||||
const deletedCategory = await prisma.category.delete({
|
const deletedCategory = await prisma.category.delete({
|
||||||
where: { id }
|
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 {
|
return {
|
||||||
code: 200,
|
code: 200,
|
||||||
message: 'Category deleted successfully',
|
message: 'Category deleted successfully',
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Failed to delete category', { 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;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,8 @@ import {
|
||||||
ListMyCertificatesInput,
|
ListMyCertificatesInput,
|
||||||
ListMyCertificatesResponse,
|
ListMyCertificatesResponse,
|
||||||
} from '../types/certificate.types';
|
} from '../types/certificate.types';
|
||||||
|
import { auditService } from './audit.service';
|
||||||
|
import { AuditAction } from '@prisma/client';
|
||||||
|
|
||||||
export class CertificateService {
|
export class CertificateService {
|
||||||
private static TEMPLATE_PATH = path.join(__dirname, '../../assets/templates/Certificate.pdf');
|
private static TEMPLATE_PATH = path.join(__dirname, '../../assets/templates/Certificate.pdf');
|
||||||
|
|
@ -54,17 +56,11 @@ export class CertificateService {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!enrollment) {
|
if (!enrollment) throw new NotFoundError('Enrollment not found');
|
||||||
throw new NotFoundError('Enrollment not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (enrollment.status !== 'COMPLETED') {
|
if (enrollment.status !== 'COMPLETED') throw new ForbiddenError('Course not completed yet');
|
||||||
throw new ForbiddenError('Course not completed yet');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!enrollment.course.have_certificate) {
|
if (!enrollment.course.have_certificate) throw new ValidationError('This course does not offer certificates');
|
||||||
throw new ValidationError('This course does not offer certificates');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if certificate already exists
|
// Check if certificate already exists
|
||||||
const existingCertificate = await prisma.certificate.findFirst({
|
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);
|
const downloadUrl = await getPresignedUrl(filePath, 3600);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -135,6 +139,18 @@ export class CertificateService {
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Failed to generate certificate', { 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;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -186,6 +202,18 @@ export class CertificateService {
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Failed to get certificate', { 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;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -239,6 +267,17 @@ export class CertificateService {
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Failed to list certificates', { 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;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@ import { logger } from '../config/logger';
|
||||||
import { listCourseResponse, getCourseResponse, ListCoursesInput } from '../types/courses.types';
|
import { listCourseResponse, getCourseResponse, ListCoursesInput } from '../types/courses.types';
|
||||||
import { UnauthorizedError, ValidationError, ForbiddenError } from '../middleware/errorHandler';
|
import { UnauthorizedError, ValidationError, ForbiddenError } from '../middleware/errorHandler';
|
||||||
import { getPresignedUrl } from '../config/minio';
|
import { getPresignedUrl } from '../config/minio';
|
||||||
|
import { auditService } from './audit.service';
|
||||||
|
import { AuditAction } from '@prisma/client';
|
||||||
|
|
||||||
export class CoursesService {
|
export class CoursesService {
|
||||||
async ListCourses(input: ListCoursesInput): Promise<listCourseResponse> {
|
async ListCourses(input: ListCoursesInput): Promise<listCourseResponse> {
|
||||||
|
|
@ -82,6 +84,16 @@ export class CoursesService {
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Failed to fetch courses', { 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;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -122,6 +134,16 @@ export class CoursesService {
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Failed to fetch course', { 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;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -135,6 +135,17 @@ export class UserService {
|
||||||
throw new UnauthorizedError('Token expired');
|
throw new UnauthorizedError('Token expired');
|
||||||
}
|
}
|
||||||
logger.error('Failed to change password', { error });
|
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;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -186,6 +197,17 @@ export class UserService {
|
||||||
throw new UnauthorizedError('Token expired');
|
throw new UnauthorizedError('Token expired');
|
||||||
}
|
}
|
||||||
logger.error('Failed to update profile', { error });
|
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;
|
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
|
// Generate presigned URL for response
|
||||||
const presignedUrl = await this.getAvatarPresignedUrl(filePath);
|
const presignedUrl = await this.getAvatarPresignedUrl(filePath);
|
||||||
|
|
||||||
|
|
@ -273,6 +307,18 @@ export class UserService {
|
||||||
throw new UnauthorizedError('Token expired');
|
throw new UnauthorizedError('Token expired');
|
||||||
}
|
}
|
||||||
logger.error('Failed to upload avatar', { error });
|
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;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -385,6 +431,17 @@ export class UserService {
|
||||||
if (error instanceof jwt.JsonWebTokenError) throw new UnauthorizedError('Invalid token');
|
if (error instanceof jwt.JsonWebTokenError) throw new UnauthorizedError('Invalid token');
|
||||||
if (error instanceof jwt.TokenExpiredError) throw new UnauthorizedError('Token expired');
|
if (error instanceof jwt.TokenExpiredError) throw new UnauthorizedError('Token expired');
|
||||||
logger.error('Failed to send verification email', { error });
|
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;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -415,6 +472,15 @@ export class UserService {
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.info('Email verified successfully', { userId: user.id, email: user.email });
|
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 {
|
return {
|
||||||
code: 200,
|
code: 200,
|
||||||
message: 'Email verified successfully'
|
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.JsonWebTokenError) throw new UnauthorizedError('Invalid verification token');
|
||||||
if (error instanceof jwt.TokenExpiredError) throw new UnauthorizedError('Verification link has expired');
|
if (error instanceof jwt.TokenExpiredError) throw new UnauthorizedError('Verification link has expired');
|
||||||
logger.error('Failed to verify email', { error });
|
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;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,16 @@ export class UserManagementService {
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Failed to fetch users', { 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;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -61,6 +71,16 @@ export class UserManagementService {
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Failed to fetch user by ID', { 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;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -95,6 +115,17 @@ export class UserManagementService {
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Failed to update user role', { 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;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -114,6 +145,16 @@ export class UserManagementService {
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Failed to deactivate user', { 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;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -160,6 +201,16 @@ export class UserManagementService {
|
||||||
throw new UnauthorizedError('Token expired');
|
throw new UnauthorizedError('Token expired');
|
||||||
}
|
}
|
||||||
logger.error('Failed to deactivate account', { error });
|
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;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -207,6 +258,16 @@ export class UserManagementService {
|
||||||
throw new UnauthorizedError('Token expired');
|
throw new UnauthorizedError('Token expired');
|
||||||
}
|
}
|
||||||
logger.error('Failed to activate account', { error });
|
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;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue