feat: add presigned URL generation for course thumbnails across all course services.

This commit is contained in:
JakkrapartXD 2026-01-28 15:31:21 +07:00
parent b28dd410e2
commit 10821d093c
3 changed files with 131 additions and 27 deletions

View file

@ -4,7 +4,7 @@ import { config } from '../config';
import { logger } from '../config/logger'; import { logger } from '../config/logger';
import { UnauthorizedError, ValidationError, ForbiddenError, NotFoundError } from '../middleware/errorHandler'; import { UnauthorizedError, ValidationError, ForbiddenError, NotFoundError } from '../middleware/errorHandler';
import jwt from 'jsonwebtoken'; import jwt from 'jsonwebtoken';
import { uploadFile, deleteFile } from '../config/minio'; import { uploadFile, deleteFile, getPresignedUrl } from '../config/minio';
import { import {
CreateCourseInput, CreateCourseInput,
UpdateCourseInput, UpdateCourseInput,
@ -94,7 +94,22 @@ export class CoursesInstructorService {
} }
}); });
const courses = courseInstructors.map(ci => ci.course); 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,
};
})
);
return { return {
code: 200, code: 200,
@ -141,10 +156,23 @@ export class CoursesInstructorService {
throw new ForbiddenError('You are not an instructor of this course'); throw new ForbiddenError('You are not an instructor of this course');
} }
// 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}`);
}
}
return { return {
code: 200, code: 200,
message: 'Course retrieved successfully', message: 'Course retrieved successfully',
data: courseInstructor.course data: {
...courseInstructor.course,
thumbnail_url: thumbnail_presigned_url,
}
}; };
} catch (error) { } catch (error) {
logger.error('Failed to retrieve course', { error }); logger.error('Failed to retrieve course', { error });
@ -162,10 +190,24 @@ export class CoursesInstructorService {
}, },
data: courseData data: courseData
}); });
// Generate presigned URL for thumbnail
let thumbnail_presigned_url: string | null = null;
if (course.thumbnail_url) {
try {
thumbnail_presigned_url = await getPresignedUrl(course.thumbnail_url, 3600);
} catch (err) {
logger.warn(`Failed to generate presigned URL for thumbnail: ${err}`);
}
}
return { return {
code: 200, code: 200,
message: 'Course updated successfully', message: 'Course updated successfully',
data: course data: {
...course,
thumbnail_url: thumbnail_presigned_url,
}
}; };
} catch (error) { } catch (error) {
logger.error('Failed to update course', { error }); logger.error('Failed to update course', { error });

View file

@ -109,23 +109,35 @@ export class CoursesStudentService {
}, },
}); });
const data = enrollments.map(enrollment => ({ const data = await Promise.all(
id: enrollment.id, enrollments.map(async (enrollment) => {
course_id: enrollment.course_id, let thumbnail_presigned_url: string | null = null;
course: { if (enrollment.course.thumbnail_url) {
id: enrollment.course.id, try {
title: enrollment.course.title as { th: string; en: string }, thumbnail_presigned_url = await getPresignedUrl(enrollment.course.thumbnail_url, 3600);
slug: enrollment.course.slug, } catch (err) {
thumbnail_url: enrollment.course.thumbnail_url, logger.warn(`Failed to generate presigned URL for thumbnail: ${err}`);
description: enrollment.course.description as { th: string; en: string }, }
}, }
status: enrollment.status, return {
progress_percentage: enrollment.progress_percentage, id: enrollment.id,
enrolled_at: enrollment.enrolled_at, course_id: enrollment.course_id,
started_at: enrollment.started_at, course: {
completed_at: enrollment.completed_at, id: enrollment.course.id,
last_accessed_at: enrollment.last_accessed_at, title: enrollment.course.title as { th: string; en: string },
})); slug: enrollment.course.slug,
thumbnail_url: thumbnail_presigned_url,
description: enrollment.course.description as { th: string; en: string },
},
status: enrollment.status,
progress_percentage: enrollment.progress_percentage,
enrolled_at: enrollment.enrolled_at,
started_at: enrollment.started_at,
completed_at: enrollment.completed_at,
last_accessed_at: enrollment.last_accessed_at,
};
})
);
return { return {
code: 200, code: 200,
@ -256,6 +268,16 @@ export class CoursesStudentService {
const total_lessons = lessonIds.length; const total_lessons = lessonIds.length;
const completed_lessons = lessonProgress.filter(p => p.is_completed).length; const completed_lessons = lessonProgress.filter(p => p.is_completed).length;
// Generate presigned URL for thumbnail
let thumbnail_presigned_url: string | null = null;
if (course.thumbnail_url) {
try {
thumbnail_presigned_url = await getPresignedUrl(course.thumbnail_url, 3600);
} catch (err) {
logger.warn(`Failed to generate presigned URL for thumbnail: ${err}`);
}
}
return { return {
code: 200, code: 200,
message: 'Course learning retrieved successfully', message: 'Course learning retrieved successfully',
@ -265,7 +287,7 @@ export class CoursesStudentService {
title: course.title as { th: string; en: string }, title: course.title as { th: string; en: string },
slug: course.slug, slug: course.slug,
description: course.description as { th: string; en: string }, description: course.description as { th: string; en: string },
thumbnail_url: course.thumbnail_url, thumbnail_url: thumbnail_presigned_url,
have_certificate: course.have_certificate, have_certificate: course.have_certificate,
}, },
enrollment: { enrollment: {

View file

@ -4,6 +4,7 @@ import { config } from '../config';
import { logger } from '../config/logger'; import { logger } from '../config/logger';
import { listCourseResponse, getCourseResponse } from '../types/courses.types'; import { listCourseResponse, getCourseResponse } from '../types/courses.types';
import { UnauthorizedError, ValidationError, ForbiddenError } from '../middleware/errorHandler'; import { UnauthorizedError, ValidationError, ForbiddenError } from '../middleware/errorHandler';
import { getPresignedUrl } from '../config/minio';
export class CoursesService { export class CoursesService {
async ListCourses(category_id?: number): Promise<listCourseResponse> { async ListCourses(category_id?: number): Promise<listCourseResponse> {
@ -17,12 +18,30 @@ export class CoursesService {
} }
const courses = await prisma.course.findMany({ where }); const courses = await prisma.course.findMany({ where });
// Generate presigned URLs for thumbnails
const coursesWithUrls = await Promise.all(
courses.map(async (course) => {
let thumbnail_presigned_url: string | null = null;
if (course.thumbnail_url) {
try {
thumbnail_presigned_url = await getPresignedUrl(course.thumbnail_url, 3600);
} catch (err) {
logger.warn(`Failed to generate presigned URL for thumbnail: ${err}`);
}
}
return {
...course,
thumbnail_url: thumbnail_presigned_url,
};
})
);
return { return {
code: 200, code: 200,
message: 'Courses fetched successfully', message: 'Courses fetched successfully',
total: courses.length, total: coursesWithUrls.length,
data: courses, data: coursesWithUrls,
}; };
} catch (error) { } catch (error) {
logger.error('Failed to fetch courses', { error }); logger.error('Failed to fetch courses', { error });
@ -38,11 +57,32 @@ export class CoursesService {
status: 'APPROVED' // Only show approved courses to students status: 'APPROVED' // Only show approved courses to students
} }
}); });
if (!course) {
return {
code: 200,
message: 'Course fetched successfully',
data: null,
};
}
// Generate presigned URL for thumbnail
let thumbnail_presigned_url: string | null = null;
if (course.thumbnail_url) {
try {
thumbnail_presigned_url = await getPresignedUrl(course.thumbnail_url, 3600);
} catch (err) {
logger.warn(`Failed to generate presigned URL for thumbnail: ${err}`);
}
}
return { return {
code: 200, code: 200,
message: 'Course fetched successfully', message: 'Course fetched successfully',
data: course, data: {
...course,
thumbnail_url: thumbnail_presigned_url,
},
}; };
} catch (error) { } catch (error) {
logger.error('Failed to fetch course', { error }); logger.error('Failed to fetch course', { error });