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 { AnnouncementsService } from '../services/announcements.service';
import {
@ -47,8 +47,8 @@ export class AnnouncementsController {
}
/**
*
* Create a new announcement
* ()
* Create a new announcement with optional file attachments
* @param courseId - / Course ID
*/
@Post()
@ -59,17 +59,23 @@ export class AnnouncementsController {
public async createAnnouncement(
@Request() request: any,
@Path() courseId: number,
@Body() body: CreateAnnouncementBody
@FormField() data: string,
@UploadedFiles() files?: Express.Multer.File[]
): Promise<CreateAnnouncementResponse> {
const token = request.headers.authorization?.replace('Bearer ', '');
if (!token) throw new ValidationError('No token provided');
// Parse JSON data field
const parsed = JSON.parse(data) as CreateAnnouncementBody;
return await announcementsService.createAnnouncement({
token,
course_id: courseId,
title: body.title,
content: body.content,
status: body.status,
is_pinned: body.is_pinned,
title: parsed.title,
content: parsed.content,
status: parsed.status,
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> {
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 };
// Validate instructor access
await CoursesInstructorService.validateCourseInstructor(token, course_id);
// Create announcement
const announcement = await prisma.announcement.create({
data: {
course_id,
@ -131,11 +132,52 @@ export class AnnouncementsService {
published_at: status === 'PUBLISHED' ? new Date() : null,
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 {
code: 201,
message: 'Announcement created successfully',
@ -147,7 +189,7 @@ export class AnnouncementsService {
is_pinned: announcement.is_pinned,
created_at: announcement.created_at,
updated_at: announcement.updated_at,
attachments: [],
attachments,
},
};
} catch (error) {

View file

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