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 { UnauthorizedError, ValidationError, ForbiddenError, NotFoundError } from '../middleware/errorHandler';
import jwt from 'jsonwebtoken';
import { uploadFile, deleteFile } from '../config/minio';
import { uploadFile, deleteFile, getPresignedUrl } from '../config/minio';
import {
CreateCourseInput,
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 {
code: 200,
@ -141,10 +156,23 @@ export class CoursesInstructorService {
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 {
code: 200,
message: 'Course retrieved successfully',
data: courseInstructor.course
data: {
...courseInstructor.course,
thumbnail_url: thumbnail_presigned_url,
}
};
} catch (error) {
logger.error('Failed to retrieve course', { error });
@ -162,10 +190,24 @@ export class CoursesInstructorService {
},
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 {
code: 200,
message: 'Course updated successfully',
data: course
data: {
...course,
thumbnail_url: thumbnail_presigned_url,
}
};
} catch (error) {
logger.error('Failed to update course', { error });

View file

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

View file

@ -4,6 +4,7 @@ import { config } from '../config';
import { logger } from '../config/logger';
import { listCourseResponse, getCourseResponse } from '../types/courses.types';
import { UnauthorizedError, ValidationError, ForbiddenError } from '../middleware/errorHandler';
import { getPresignedUrl } from '../config/minio';
export class CoursesService {
async ListCourses(category_id?: number): Promise<listCourseResponse> {
@ -17,12 +18,30 @@ export class CoursesService {
}
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 {
code: 200,
message: 'Courses fetched successfully',
total: courses.length,
data: courses,
total: coursesWithUrls.length,
data: coursesWithUrls,
};
} catch (error) {
logger.error('Failed to fetch courses', { error });
@ -38,11 +57,32 @@ export class CoursesService {
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 {
code: 200,
message: 'Course fetched successfully',
data: course,
data: {
...course,
thumbnail_url: thumbnail_presigned_url,
},
};
} catch (error) {
logger.error('Failed to fetch course', { error });