2026-01-16 15:43:14 +07:00
|
|
|
import { prisma } from '../config/database';
|
|
|
|
|
import { Prisma } from '@prisma/client';
|
|
|
|
|
import { config } from '../config';
|
|
|
|
|
import { logger } from '../config/logger';
|
2026-01-20 16:51:42 +07:00
|
|
|
import { UnauthorizedError, ValidationError, ForbiddenError, NotFoundError } from '../middleware/errorHandler';
|
2026-01-16 15:43:14 +07:00
|
|
|
import jwt from 'jsonwebtoken';
|
2026-01-28 15:31:21 +07:00
|
|
|
import { uploadFile, deleteFile, getPresignedUrl } from '../config/minio';
|
2026-01-16 15:43:14 +07:00
|
|
|
import {
|
|
|
|
|
CreateCourseInput,
|
2026-01-16 17:36:32 +07:00
|
|
|
UpdateCourseInput,
|
2026-01-16 15:43:14 +07:00
|
|
|
createCourseResponse,
|
|
|
|
|
GetMyCourseResponse,
|
|
|
|
|
ListMyCourseResponse,
|
|
|
|
|
addinstructorCourse,
|
|
|
|
|
addinstructorCourseResponse,
|
|
|
|
|
removeinstructorCourse,
|
|
|
|
|
removeinstructorCourseResponse,
|
|
|
|
|
setprimaryCourseInstructor,
|
2026-01-16 17:36:32 +07:00
|
|
|
setprimaryCourseInstructorResponse,
|
|
|
|
|
submitCourseResponse,
|
|
|
|
|
listinstructorCourseResponse,
|
|
|
|
|
sendCourseForReview,
|
2026-01-16 17:52:36 +07:00
|
|
|
getmyCourse,
|
|
|
|
|
listinstructorCourse,
|
2026-01-29 15:52:10 +07:00
|
|
|
SearchInstructorInput,
|
|
|
|
|
SearchInstructorResponse,
|
2026-01-16 15:43:14 +07:00
|
|
|
} from "../types/CoursesInstructor.types";
|
|
|
|
|
|
|
|
|
|
export class CoursesInstructorService {
|
2026-01-28 14:38:11 +07:00
|
|
|
static async createCourse(courseData: CreateCourseInput, userId: number, thumbnailFile?: Express.Multer.File): Promise<createCourseResponse> {
|
2026-01-16 15:43:14 +07:00
|
|
|
try {
|
2026-01-28 14:38:11 +07:00
|
|
|
let thumbnailUrl: string | undefined;
|
|
|
|
|
|
|
|
|
|
// Upload thumbnail to MinIO if provided
|
|
|
|
|
if (thumbnailFile) {
|
|
|
|
|
const timestamp = Date.now();
|
|
|
|
|
const uniqueId = Math.random().toString(36).substring(2, 15);
|
|
|
|
|
const extension = thumbnailFile.originalname.split('.').pop() || 'jpg';
|
|
|
|
|
const safeFilename = `${timestamp}-${uniqueId}.${extension}`;
|
|
|
|
|
const filePath = `courses/thumbnails/${safeFilename}`;
|
|
|
|
|
|
|
|
|
|
await uploadFile(filePath, thumbnailFile.buffer, thumbnailFile.mimetype || 'image/jpeg');
|
|
|
|
|
thumbnailUrl = filePath;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-19 14:48:45 +07:00
|
|
|
// Use transaction to create course and instructor together
|
|
|
|
|
const result = await prisma.$transaction(async (tx) => {
|
|
|
|
|
// Create the course
|
|
|
|
|
const courseCreated = await tx.course.create({
|
|
|
|
|
data: {
|
|
|
|
|
category_id: courseData.category_id,
|
|
|
|
|
title: courseData.title,
|
|
|
|
|
slug: courseData.slug,
|
|
|
|
|
description: courseData.description,
|
2026-01-28 14:38:11 +07:00
|
|
|
thumbnail_url: thumbnailUrl,
|
2026-01-19 14:48:45 +07:00
|
|
|
price: courseData.price || 0,
|
|
|
|
|
is_free: courseData.is_free ?? false,
|
|
|
|
|
have_certificate: courseData.have_certificate ?? false,
|
|
|
|
|
created_by: userId,
|
|
|
|
|
status: 'DRAFT'
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Add creator as primary instructor
|
|
|
|
|
await tx.courseInstructor.create({
|
|
|
|
|
data: {
|
|
|
|
|
course_id: courseCreated.id,
|
|
|
|
|
user_id: userId,
|
|
|
|
|
is_primary: true,
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return courseCreated;
|
2026-01-16 15:43:14 +07:00
|
|
|
});
|
2026-01-19 14:48:45 +07:00
|
|
|
|
2026-01-16 15:43:14 +07:00
|
|
|
return {
|
|
|
|
|
code: 201,
|
|
|
|
|
message: 'Course created successfully',
|
2026-01-19 14:48:45 +07:00
|
|
|
data: result
|
2026-01-16 15:43:14 +07:00
|
|
|
};
|
|
|
|
|
} catch (error) {
|
|
|
|
|
logger.error('Failed to create course', { error });
|
|
|
|
|
throw error;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static async listMyCourses(token: string): Promise<ListMyCourseResponse> {
|
|
|
|
|
try {
|
|
|
|
|
const decoded = jwt.verify(token, config.jwt.secret) as { id: number; type: string };
|
|
|
|
|
const courseInstructors = await prisma.courseInstructor.findMany({
|
|
|
|
|
where: {
|
|
|
|
|
user_id: decoded.id
|
|
|
|
|
},
|
|
|
|
|
include: {
|
|
|
|
|
course: true
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2026-01-28 15:31:21 +07:00
|
|
|
const courses = await Promise.all(
|
|
|
|
|
courseInstructors.map(async (ci) => {
|
|
|
|
|
let thumbnail_presigned_url: string | null = null;
|
|
|
|
|
if (ci.course.thumbnail_url) {
|
|
|
|
|
try {
|
|
|
|
|
thumbnail_presigned_url = await getPresignedUrl(ci.course.thumbnail_url, 3600);
|
|
|
|
|
} catch (err) {
|
|
|
|
|
logger.warn(`Failed to generate presigned URL for thumbnail: ${err}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return {
|
|
|
|
|
...ci.course,
|
|
|
|
|
thumbnail_url: thumbnail_presigned_url,
|
|
|
|
|
};
|
|
|
|
|
})
|
|
|
|
|
);
|
2026-01-16 15:43:14 +07:00
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
code: 200,
|
|
|
|
|
message: 'Courses retrieved successfully',
|
|
|
|
|
data: courses,
|
|
|
|
|
total: courses.length
|
|
|
|
|
};
|
|
|
|
|
} catch (error) {
|
|
|
|
|
logger.error('Failed to retrieve courses', { error });
|
|
|
|
|
throw error;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-16 17:36:32 +07:00
|
|
|
static async getmyCourse(getmyCourse: getmyCourse): Promise<GetMyCourseResponse> {
|
2026-01-16 15:43:14 +07:00
|
|
|
try {
|
2026-01-16 17:36:32 +07:00
|
|
|
const decoded = jwt.verify(getmyCourse.token, config.jwt.secret) as { id: number; type: string };
|
2026-01-16 15:43:14 +07:00
|
|
|
|
|
|
|
|
// Check if user is instructor of this course
|
|
|
|
|
const courseInstructor = await prisma.courseInstructor.findFirst({
|
|
|
|
|
where: {
|
|
|
|
|
user_id: decoded.id,
|
2026-01-16 17:36:32 +07:00
|
|
|
course_id: getmyCourse.course_id
|
2026-01-16 15:43:14 +07:00
|
|
|
},
|
|
|
|
|
include: {
|
|
|
|
|
course: {
|
|
|
|
|
include: {
|
|
|
|
|
chapters: {
|
|
|
|
|
include: {
|
|
|
|
|
lessons: {
|
|
|
|
|
include: {
|
|
|
|
|
attachments: true,
|
|
|
|
|
progress: true,
|
|
|
|
|
quiz: true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!courseInstructor) {
|
|
|
|
|
throw new ForbiddenError('You are not an instructor of this course');
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-28 15:31:21 +07:00
|
|
|
// Generate presigned URL for thumbnail
|
|
|
|
|
let thumbnail_presigned_url: string | null = null;
|
|
|
|
|
if (courseInstructor.course.thumbnail_url) {
|
|
|
|
|
try {
|
|
|
|
|
thumbnail_presigned_url = await getPresignedUrl(courseInstructor.course.thumbnail_url, 3600);
|
|
|
|
|
} catch (err) {
|
|
|
|
|
logger.warn(`Failed to generate presigned URL for thumbnail: ${err}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-16 15:43:14 +07:00
|
|
|
return {
|
|
|
|
|
code: 200,
|
|
|
|
|
message: 'Course retrieved successfully',
|
2026-01-28 15:31:21 +07:00
|
|
|
data: {
|
|
|
|
|
...courseInstructor.course,
|
|
|
|
|
thumbnail_url: thumbnail_presigned_url,
|
|
|
|
|
}
|
2026-01-16 15:43:14 +07:00
|
|
|
};
|
|
|
|
|
} catch (error) {
|
|
|
|
|
logger.error('Failed to retrieve course', { error });
|
|
|
|
|
throw error;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-01-16 17:36:32 +07:00
|
|
|
|
|
|
|
|
static async updateCourse(token: string, courseId: number, courseData: UpdateCourseInput): Promise<createCourseResponse> {
|
|
|
|
|
try {
|
2026-01-28 16:46:54 +07:00
|
|
|
await this.validateCourseInstructor(token, courseId);
|
2026-01-16 17:36:32 +07:00
|
|
|
|
|
|
|
|
const course = await prisma.course.update({
|
|
|
|
|
where: {
|
2026-01-20 13:39:42 +07:00
|
|
|
id: courseId
|
2026-01-16 17:36:32 +07:00
|
|
|
},
|
|
|
|
|
data: courseData
|
|
|
|
|
});
|
2026-01-28 15:31:21 +07:00
|
|
|
|
2026-01-28 16:46:54 +07:00
|
|
|
return {
|
|
|
|
|
code: 200,
|
|
|
|
|
message: 'Course updated successfully',
|
|
|
|
|
data: course
|
|
|
|
|
};
|
|
|
|
|
} catch (error) {
|
|
|
|
|
logger.error('Failed to update course', { error });
|
|
|
|
|
throw error;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static async uploadThumbnail(token: string, courseId: number, file: Express.Multer.File): Promise<{ code: number; message: string; data: { course_id: number; thumbnail_url: string } }> {
|
|
|
|
|
try {
|
|
|
|
|
await this.validateCourseInstructor(token, courseId);
|
|
|
|
|
|
|
|
|
|
// Get current course to check for existing thumbnail
|
|
|
|
|
const currentCourse = await prisma.course.findUnique({
|
|
|
|
|
where: { id: courseId }
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Delete old thumbnail if exists
|
|
|
|
|
if (currentCourse?.thumbnail_url) {
|
2026-01-28 15:31:21 +07:00
|
|
|
try {
|
2026-01-28 16:46:54 +07:00
|
|
|
await deleteFile(currentCourse.thumbnail_url);
|
|
|
|
|
logger.info(`Deleted old thumbnail: ${currentCourse.thumbnail_url}`);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
logger.warn(`Failed to delete old thumbnail: ${error}`);
|
2026-01-28 15:31:21 +07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-28 16:46:54 +07:00
|
|
|
// Generate unique filename
|
|
|
|
|
const timestamp = Date.now();
|
|
|
|
|
const uniqueId = Math.random().toString(36).substring(2, 15);
|
|
|
|
|
const extension = file.originalname.split('.').pop() || 'jpg';
|
|
|
|
|
const safeFilename = `${timestamp}-${uniqueId}.${extension}`;
|
|
|
|
|
const filePath = `courses/${courseId}/thumbnail/${safeFilename}`;
|
|
|
|
|
|
|
|
|
|
// Upload to MinIO
|
|
|
|
|
await uploadFile(filePath, file.buffer, file.mimetype || 'image/jpeg');
|
|
|
|
|
logger.info(`Uploaded thumbnail: ${filePath}`);
|
|
|
|
|
|
|
|
|
|
// Update course with new thumbnail path
|
|
|
|
|
await prisma.course.update({
|
|
|
|
|
where: { id: courseId },
|
|
|
|
|
data: { thumbnail_url: filePath }
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Generate presigned URL for response
|
|
|
|
|
const presignedUrl = await getPresignedUrl(filePath, 3600);
|
|
|
|
|
|
2026-01-16 17:36:32 +07:00
|
|
|
return {
|
|
|
|
|
code: 200,
|
2026-01-28 16:46:54 +07:00
|
|
|
message: 'Thumbnail uploaded successfully',
|
2026-01-28 15:31:21 +07:00
|
|
|
data: {
|
2026-01-28 16:46:54 +07:00
|
|
|
course_id: courseId,
|
|
|
|
|
thumbnail_url: presignedUrl
|
2026-01-28 15:31:21 +07:00
|
|
|
}
|
2026-01-16 17:36:32 +07:00
|
|
|
};
|
|
|
|
|
} catch (error) {
|
2026-01-28 16:46:54 +07:00
|
|
|
logger.error('Failed to upload thumbnail', { error });
|
2026-01-16 17:36:32 +07:00
|
|
|
throw error;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static async deleteCourse(token: string, courseId: number): Promise<createCourseResponse> {
|
|
|
|
|
try {
|
|
|
|
|
const courseInstructorId = await this.validateCourseInstructor(token, courseId);
|
|
|
|
|
if (!courseInstructorId.is_primary) {
|
|
|
|
|
throw new ForbiddenError('You have no permission to delete this course');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const course = await prisma.course.delete({
|
|
|
|
|
where: {
|
2026-01-20 13:39:42 +07:00
|
|
|
id: courseId
|
2026-01-16 17:36:32 +07:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
return {
|
|
|
|
|
code: 200,
|
|
|
|
|
message: 'Course deleted successfully',
|
|
|
|
|
data: course
|
|
|
|
|
};
|
|
|
|
|
} catch (error) {
|
|
|
|
|
logger.error('Failed to delete course', { error });
|
|
|
|
|
throw error;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static async sendCourseForReview(sendCourseForReview: sendCourseForReview): Promise<submitCourseResponse> {
|
|
|
|
|
try {
|
|
|
|
|
const decoded = jwt.verify(sendCourseForReview.token, config.jwt.secret) as { id: number; type: string };
|
|
|
|
|
await prisma.courseApproval.create({
|
|
|
|
|
data: {
|
|
|
|
|
course_id: sendCourseForReview.course_id,
|
|
|
|
|
submitted_by: decoded.id,
|
|
|
|
|
}
|
|
|
|
|
});
|
2026-01-20 16:51:42 +07:00
|
|
|
await prisma.course.update({
|
|
|
|
|
where: {
|
|
|
|
|
id: sendCourseForReview.course_id
|
|
|
|
|
},
|
|
|
|
|
data: {
|
|
|
|
|
status: 'PENDING'
|
|
|
|
|
}
|
|
|
|
|
});
|
2026-01-16 17:36:32 +07:00
|
|
|
return {
|
|
|
|
|
code: 200,
|
|
|
|
|
message: 'Course sent for review successfully',
|
|
|
|
|
};
|
|
|
|
|
} catch (error) {
|
|
|
|
|
logger.error('Failed to send course for review', { error });
|
|
|
|
|
throw error;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-23 13:16:41 +07:00
|
|
|
static async getCourseApprovals(token: string, courseId: number): Promise<{
|
|
|
|
|
code: number;
|
|
|
|
|
message: string;
|
|
|
|
|
data: any[];
|
|
|
|
|
total: number;
|
|
|
|
|
}> {
|
|
|
|
|
try {
|
|
|
|
|
const decoded = jwt.verify(token, config.jwt.secret) as { id: number; type: string };
|
|
|
|
|
|
|
|
|
|
// Validate instructor access
|
|
|
|
|
await this.validateCourseInstructor(token, courseId);
|
|
|
|
|
|
|
|
|
|
const approvals = await prisma.courseApproval.findMany({
|
|
|
|
|
where: { course_id: courseId },
|
|
|
|
|
orderBy: { created_at: 'desc' },
|
|
|
|
|
include: {
|
|
|
|
|
submitter: {
|
|
|
|
|
select: { id: true, username: true, email: true }
|
|
|
|
|
},
|
|
|
|
|
reviewer: {
|
|
|
|
|
select: { id: true, username: true, email: true }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
code: 200,
|
|
|
|
|
message: 'Course approvals retrieved successfully',
|
|
|
|
|
data: approvals,
|
|
|
|
|
total: approvals.length,
|
|
|
|
|
};
|
|
|
|
|
} catch (error) {
|
|
|
|
|
logger.error('Failed to retrieve course approvals', { error });
|
|
|
|
|
throw error;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-16 17:36:32 +07:00
|
|
|
|
|
|
|
|
|
2026-01-29 15:52:10 +07:00
|
|
|
static async searchInstructors(input: SearchInstructorInput): Promise<SearchInstructorResponse> {
|
|
|
|
|
try {
|
2026-01-29 16:17:25 +07:00
|
|
|
jwt.verify(input.token, config.jwt.secret) as { id: number };
|
2026-01-29 15:52:10 +07:00
|
|
|
|
2026-01-29 16:17:25 +07:00
|
|
|
// Search all instructors by email or username
|
2026-01-29 15:52:10 +07:00
|
|
|
const users = await prisma.user.findMany({
|
|
|
|
|
where: {
|
2026-01-29 16:17:25 +07:00
|
|
|
OR: [
|
|
|
|
|
{ email: { contains: input.query, mode: 'insensitive' } },
|
|
|
|
|
{ username: { contains: input.query, mode: 'insensitive' } },
|
|
|
|
|
],
|
2026-01-29 16:39:33 +07:00
|
|
|
role: { code: 'INSTRUCTOR' }
|
2026-01-29 15:52:10 +07:00
|
|
|
},
|
|
|
|
|
include: {
|
|
|
|
|
profile: true
|
|
|
|
|
},
|
|
|
|
|
take: 10
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const results = await Promise.all(users.map(async (user) => {
|
|
|
|
|
let avatar_url: string | null = null;
|
|
|
|
|
if (user.profile?.avatar_url) {
|
|
|
|
|
try {
|
|
|
|
|
avatar_url = await getPresignedUrl(user.profile.avatar_url, 3600);
|
|
|
|
|
} catch (err) {
|
|
|
|
|
logger.warn(`Failed to generate presigned URL for avatar: ${err}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return {
|
|
|
|
|
id: user.id,
|
|
|
|
|
username: user.username,
|
|
|
|
|
email: user.email,
|
|
|
|
|
first_name: user.profile?.first_name || null,
|
|
|
|
|
last_name: user.profile?.last_name || null,
|
|
|
|
|
avatar_url,
|
|
|
|
|
};
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
code: 200,
|
|
|
|
|
message: 'Instructors found',
|
|
|
|
|
data: results,
|
|
|
|
|
};
|
|
|
|
|
} catch (error) {
|
|
|
|
|
logger.error('Failed to search instructors', { error });
|
|
|
|
|
throw error;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-16 17:52:36 +07:00
|
|
|
static async addInstructorToCourse(addinstructorCourse: addinstructorCourse): Promise<addinstructorCourseResponse> {
|
2026-01-16 17:36:32 +07:00
|
|
|
try {
|
2026-01-29 15:52:10 +07:00
|
|
|
// Validate user is instructor of this course
|
|
|
|
|
await this.validateCourseInstructor(addinstructorCourse.token, addinstructorCourse.course_id);
|
|
|
|
|
|
|
|
|
|
// Find user by email or username
|
|
|
|
|
const user = await prisma.user.findFirst({
|
|
|
|
|
where: {
|
|
|
|
|
OR: [
|
|
|
|
|
{ email: addinstructorCourse.email_or_username },
|
|
|
|
|
{ username: addinstructorCourse.email_or_username },
|
|
|
|
|
],
|
2026-01-29 16:39:33 +07:00
|
|
|
role: { code: 'INSTRUCTOR' }
|
2026-01-29 15:52:10 +07:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!user) {
|
|
|
|
|
throw new NotFoundError('Instructor not found with this email or username');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check if already added
|
|
|
|
|
const existing = await prisma.courseInstructor.findUnique({
|
|
|
|
|
where: {
|
|
|
|
|
course_id_user_id: {
|
|
|
|
|
course_id: addinstructorCourse.course_id,
|
|
|
|
|
user_id: user.id,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (existing) {
|
|
|
|
|
throw new ValidationError('This instructor is already added to the course');
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-16 17:36:32 +07:00
|
|
|
await prisma.courseInstructor.create({
|
|
|
|
|
data: {
|
|
|
|
|
course_id: addinstructorCourse.course_id,
|
2026-01-29 15:52:10 +07:00
|
|
|
user_id: user.id,
|
2026-01-16 17:36:32 +07:00
|
|
|
}
|
|
|
|
|
});
|
2026-01-29 15:52:10 +07:00
|
|
|
|
2026-01-16 17:36:32 +07:00
|
|
|
return {
|
|
|
|
|
code: 200,
|
|
|
|
|
message: 'Instructor added to course successfully',
|
|
|
|
|
};
|
|
|
|
|
} catch (error) {
|
|
|
|
|
logger.error('Failed to add instructor to course', { error });
|
|
|
|
|
throw error;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-16 17:52:36 +07:00
|
|
|
static async removeInstructorFromCourse(removeinstructorCourse: removeinstructorCourse): Promise<removeinstructorCourseResponse> {
|
2026-01-16 17:36:32 +07:00
|
|
|
try {
|
|
|
|
|
const decoded = jwt.verify(removeinstructorCourse.token, config.jwt.secret) as { id: number; type: string };
|
|
|
|
|
await prisma.courseInstructor.delete({
|
|
|
|
|
where: {
|
|
|
|
|
course_id_user_id: {
|
|
|
|
|
course_id: removeinstructorCourse.course_id,
|
|
|
|
|
user_id: removeinstructorCourse.user_id,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
return {
|
|
|
|
|
code: 200,
|
|
|
|
|
message: 'Instructor removed from course successfully',
|
|
|
|
|
};
|
|
|
|
|
} catch (error) {
|
|
|
|
|
logger.error('Failed to remove instructor from course', { error });
|
|
|
|
|
throw error;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-16 17:52:36 +07:00
|
|
|
static async listInstructorsOfCourse(listinstructorCourse: listinstructorCourse): Promise<listinstructorCourseResponse> {
|
2026-01-16 17:36:32 +07:00
|
|
|
try {
|
2026-01-16 17:52:36 +07:00
|
|
|
const decoded = jwt.verify(listinstructorCourse.token, config.jwt.secret) as { id: number; type: string };
|
2026-01-16 17:36:32 +07:00
|
|
|
const courseInstructors = await prisma.courseInstructor.findMany({
|
|
|
|
|
where: {
|
2026-01-16 17:52:36 +07:00
|
|
|
course_id: listinstructorCourse.course_id,
|
2026-01-16 17:36:32 +07:00
|
|
|
},
|
|
|
|
|
include: {
|
2026-01-29 15:52:10 +07:00
|
|
|
user: {
|
|
|
|
|
include: {
|
|
|
|
|
profile: true
|
|
|
|
|
}
|
|
|
|
|
},
|
2026-01-16 17:36:32 +07:00
|
|
|
}
|
|
|
|
|
});
|
2026-01-29 15:52:10 +07:00
|
|
|
|
|
|
|
|
const data = await Promise.all(courseInstructors.map(async (ci) => {
|
|
|
|
|
let avatar_url: string | null = null;
|
|
|
|
|
if (ci.user.profile?.avatar_url) {
|
|
|
|
|
try {
|
|
|
|
|
avatar_url = await getPresignedUrl(ci.user.profile.avatar_url, 3600);
|
|
|
|
|
} catch (err) {
|
|
|
|
|
logger.warn(`Failed to generate presigned URL for avatar: ${err}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return {
|
|
|
|
|
user_id: ci.user_id,
|
|
|
|
|
is_primary: ci.is_primary,
|
|
|
|
|
user: {
|
|
|
|
|
id: ci.user.id,
|
|
|
|
|
username: ci.user.username,
|
|
|
|
|
email: ci.user.email,
|
|
|
|
|
first_name: ci.user.profile?.first_name || null,
|
|
|
|
|
last_name: ci.user.profile?.last_name || null,
|
|
|
|
|
avatar_url,
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
}));
|
|
|
|
|
|
2026-01-16 17:36:32 +07:00
|
|
|
return {
|
|
|
|
|
code: 200,
|
|
|
|
|
message: 'Instructors retrieved successfully',
|
2026-01-29 15:52:10 +07:00
|
|
|
data,
|
2026-01-16 17:36:32 +07:00
|
|
|
};
|
|
|
|
|
} catch (error) {
|
|
|
|
|
logger.error('Failed to retrieve instructors of course', { error });
|
|
|
|
|
throw error;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-16 17:52:36 +07:00
|
|
|
static async setPrimaryInstructor(setprimaryCourseInstructor: setprimaryCourseInstructor): Promise<setprimaryCourseInstructorResponse> {
|
2026-01-16 17:36:32 +07:00
|
|
|
try {
|
|
|
|
|
const decoded = jwt.verify(setprimaryCourseInstructor.token, config.jwt.secret) as { id: number; type: string };
|
|
|
|
|
await prisma.courseInstructor.update({
|
|
|
|
|
where: {
|
|
|
|
|
course_id_user_id: {
|
|
|
|
|
course_id: setprimaryCourseInstructor.course_id,
|
|
|
|
|
user_id: setprimaryCourseInstructor.user_id,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
data: {
|
|
|
|
|
is_primary: true,
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
return {
|
|
|
|
|
code: 200,
|
|
|
|
|
message: 'Primary instructor set successfully',
|
|
|
|
|
};
|
|
|
|
|
} catch (error) {
|
|
|
|
|
logger.error('Failed to set primary instructor', { error });
|
|
|
|
|
throw error;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-20 16:51:42 +07:00
|
|
|
static async validateCourseInstructor(token: string, courseId: number): Promise<{ user_id: number; is_primary: boolean }> {
|
2026-01-16 17:36:32 +07:00
|
|
|
const decoded = jwt.verify(token, config.jwt.secret) as { id: number; type: string };
|
|
|
|
|
const courseInstructor = await prisma.courseInstructor.findFirst({
|
|
|
|
|
where: {
|
|
|
|
|
user_id: decoded.id,
|
|
|
|
|
course_id: courseId
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!courseInstructor) {
|
|
|
|
|
throw new ForbiddenError('You are not an instructor of this course');
|
|
|
|
|
} else return { user_id: courseInstructor.user_id, is_primary: courseInstructor.is_primary };
|
|
|
|
|
}
|
2026-01-20 16:51:42 +07:00
|
|
|
|
|
|
|
|
static async validateCourseStatus(courseId: number): Promise<void> {
|
|
|
|
|
const course = await prisma.course.findUnique({ where: { id: courseId } });
|
|
|
|
|
if (!course) {
|
|
|
|
|
throw new NotFoundError('Course not found');
|
|
|
|
|
}
|
2026-01-26 15:15:46 +07:00
|
|
|
if (course.status === 'APPROVED' || course.status === 'PENDING') {
|
2026-01-20 16:51:42 +07:00
|
|
|
throw new ForbiddenError('Course is already approved Cannot Edit');
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-01-16 15:43:14 +07:00
|
|
|
}
|