feat: enable file upload support for announcement creation with multipart form data handling.

This commit is contained in:
JakkrapartXD 2026-01-27 16:43:59 +07:00
parent dd5a8c1cc8
commit d2b3842564
3 changed files with 64 additions and 16 deletions

View file

@ -1,4 +1,4 @@
import { Body, Delete, Get, Path, Post, Put, Query, Request, Response, Route, Security, SuccessResponse, Tags, UploadedFile } from 'tsoa'; import { Body, Delete, Get, Path, Post, Put, Query, Request, Response, Route, Security, SuccessResponse, Tags, UploadedFile, UploadedFiles, FormField } from 'tsoa';
import { ValidationError } from '../middleware/errorHandler'; import { ValidationError } from '../middleware/errorHandler';
import { AnnouncementsService } from '../services/announcements.service'; import { AnnouncementsService } from '../services/announcements.service';
import { import {
@ -47,8 +47,8 @@ export class AnnouncementsController {
} }
/** /**
* * ()
* Create a new announcement * Create a new announcement with optional file attachments
* @param courseId - / Course ID * @param courseId - / Course ID
*/ */
@Post() @Post()
@ -59,17 +59,23 @@ export class AnnouncementsController {
public async createAnnouncement( public async createAnnouncement(
@Request() request: any, @Request() request: any,
@Path() courseId: number, @Path() courseId: number,
@Body() body: CreateAnnouncementBody @FormField() data: string,
@UploadedFiles() files?: Express.Multer.File[]
): Promise<CreateAnnouncementResponse> { ): Promise<CreateAnnouncementResponse> {
const token = request.headers.authorization?.replace('Bearer ', ''); const token = request.headers.authorization?.replace('Bearer ', '');
if (!token) throw new ValidationError('No token provided'); if (!token) throw new ValidationError('No token provided');
// Parse JSON data field
const parsed = JSON.parse(data) as CreateAnnouncementBody;
return await announcementsService.createAnnouncement({ return await announcementsService.createAnnouncement({
token, token,
course_id: courseId, course_id: courseId,
title: body.title, title: parsed.title,
content: body.content, content: parsed.content,
status: body.status, status: parsed.status,
is_pinned: body.is_pinned, is_pinned: parsed.is_pinned,
files,
}); });
} }

View file

@ -110,17 +110,18 @@ export class AnnouncementsService {
} }
/** /**
* * ()
* Create a new announcement * Create a new announcement with optional file attachments
*/ */
async createAnnouncement(input: CreateAnnouncementInput): Promise<CreateAnnouncementResponse> { async createAnnouncement(input: CreateAnnouncementInput): Promise<CreateAnnouncementResponse> {
try { try {
const { token, course_id, title, content, status, is_pinned } = input; const { token, course_id, title, content, status, is_pinned, files } = input;
const decoded = jwt.verify(token, config.jwt.secret) as { id: number }; const decoded = jwt.verify(token, config.jwt.secret) as { id: number };
// Validate instructor access // Validate instructor access
await CoursesInstructorService.validateCourseInstructor(token, course_id); await CoursesInstructorService.validateCourseInstructor(token, course_id);
// Create announcement
const announcement = await prisma.announcement.create({ const announcement = await prisma.announcement.create({
data: { data: {
course_id, course_id,
@ -131,11 +132,52 @@ export class AnnouncementsService {
published_at: status === 'PUBLISHED' ? new Date() : null, published_at: status === 'PUBLISHED' ? new Date() : null,
created_by: decoded.id, created_by: decoded.id,
}, },
include: {
attachments: true,
},
}); });
// Upload attachments if provided
const attachments: {
id: number;
announcement_id: number;
file_name: string;
file_path: string;
created_at: Date;
updated_at: Date;
}[] = [];
if (files && files.length > 0) {
for (const file of files) {
const timestamp = Date.now();
const uniqueId = Math.random().toString(36).substring(2, 15);
const fileName = file.originalname || 'file';
const extension = fileName.split('.').pop() || '';
const safeFilename = `${timestamp}-${uniqueId}.${extension}`;
const filePath = `courses/${course_id}/announcements/${announcement.id}/${safeFilename}`;
// Upload to MinIO
await uploadFile(filePath, file.buffer, file.mimetype || 'application/octet-stream');
// Create attachment record
const attachment = await prisma.announcementAttachment.create({
data: {
announcement_id: announcement.id,
file_name: fileName,
file_path: filePath,
file_size: file.size,
mime_type: file.mimetype || 'application/octet-stream',
},
});
attachments.push({
id: attachment.id,
announcement_id: attachment.announcement_id,
file_name: attachment.file_name,
file_path: attachment.file_path,
created_at: attachment.created_at,
updated_at: attachment.created_at,
});
}
}
return { return {
code: 201, code: 201,
message: 'Announcement created successfully', message: 'Announcement created successfully',
@ -147,7 +189,7 @@ export class AnnouncementsService {
is_pinned: announcement.is_pinned, is_pinned: announcement.is_pinned,
created_at: announcement.created_at, created_at: announcement.created_at,
updated_at: announcement.updated_at, updated_at: announcement.updated_at,
attachments: [], attachments,
}, },
}; };
} catch (error) { } catch (error) {

View file

@ -43,7 +43,7 @@ export interface CreateAnnouncementInput{
content: MultiLanguageText; content: MultiLanguageText;
status: string; status: string;
is_pinned: boolean; is_pinned: boolean;
attachments?: AnnouncementAttachment[]; files?: Express.Multer.File[];
} }
export interface UploadAnnouncementAttachmentInput{ export interface UploadAnnouncementAttachmentInput{