feat: Introduce announcements service, integrate MinIO for lesson media with presigned URLs, and restrict editing of pending courses.

This commit is contained in:
JakkrapartXD 2026-01-26 15:15:46 +07:00
parent 5f8e0a3687
commit bbfb62093e
5 changed files with 145 additions and 4 deletions

View file

@ -2,7 +2,7 @@ import { prisma } from '../config/database';
import { Prisma } from '@prisma/client';
import { config } from '../config';
import { logger } from '../config/logger';
import { deleteFile, generateFilePath, uploadFile } from '../config/minio';
import { deleteFile, generateFilePath, uploadFile, getPresignedUrl, listObjects, getVideoFolder, getAttachmentsFolder } from '../config/minio';
import { UnauthorizedError, ValidationError, ForbiddenError, NotFoundError } from '../middleware/errorHandler';
import jwt from 'jsonwebtoken';
import {
@ -333,7 +333,90 @@ export class ChaptersLessonService {
throw new ForbiddenError('This lesson is not available yet');
}
return { code: 200, message: 'Lesson fetched successfully', data: lesson as LessonData };
// Get video URL from MinIO
let video_url: string | null = null;
try {
const videoPrefix = getVideoFolder(course_id, lesson_id);
const videoFiles = await listObjects(videoPrefix);
if (videoFiles.length > 0) {
video_url = await getPresignedUrl(videoFiles[0].name, 3600);
}
} catch (err) {
logger.error(`Failed to get video from MinIO: ${err}`);
}
// Get attachments with presigned URLs from MinIO
const attachmentsWithUrls: {
id: number;
lesson_id: number;
file_name: string;
file_path: string;
file_size: number;
mime_type: string;
sort_order: number;
created_at: Date;
presigned_url: string | null;
}[] = [];
try {
const attachmentsPrefix = getAttachmentsFolder(course_id, lesson_id);
const attachmentFiles = await listObjects(attachmentsPrefix);
for (const file of attachmentFiles) {
let presigned_url: string | null = null;
try {
presigned_url = await getPresignedUrl(file.name, 3600);
} catch (err) {
logger.error(`Failed to generate presigned URL for ${file.name}: ${err}`);
}
// Extract filename from path
const fileName = file.name.split('/').pop() || file.name;
// Guess mime type from extension
const ext = fileName.split('.').pop()?.toLowerCase() || '';
const mimeTypes: { [key: string]: string } = {
'pdf': 'application/pdf',
'doc': 'application/msword',
'docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'ppt': 'application/vnd.ms-powerpoint',
'pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
'xls': 'application/vnd.ms-excel',
'xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'png': 'image/png',
'jpg': 'image/jpeg',
'jpeg': 'image/jpeg',
'gif': 'image/gif',
'mp4': 'video/mp4',
'zip': 'application/zip',
};
const mime_type = mimeTypes[ext] || 'application/octet-stream';
// Find matching attachment from database or create entry
const dbAttachment = lesson.attachments?.find(a => a.file_path === file.name);
attachmentsWithUrls.push({
id: dbAttachment?.id || 0,
lesson_id: lesson_id,
file_name: fileName,
file_path: file.name,
file_size: file.size,
mime_type: dbAttachment?.mime_type || mime_type,
sort_order: dbAttachment?.sort_order || attachmentsWithUrls.length,
created_at: dbAttachment?.created_at || file.lastModified,
presigned_url,
});
}
} catch (err) {
logger.error(`Failed to list attachments from MinIO: ${err}`);
}
// Build response with MinIO URLs
const lessonData = {
...lesson,
video_url,
attachments: attachmentsWithUrls.length > 0 ? attachmentsWithUrls : lesson.attachments,
};
return { code: 200, message: 'Lesson fetched successfully', data: lessonData as LessonData };
} catch (error) {
logger.error(`Error fetching lesson: ${error}`);
throw error;