/*********************************************************************************************************************************************************************** * File Name: task_upload.controller.js (client) * Type of Program: Controller * Description: Handles file uploads for task completion attachments. * Files are uploaded to S3 (Garage) via s3.service.js. * Returns file metadata for use in the completion submit payload. * * This is intentionally separate from the completion submit endpoint * so the client can upload files first, then submit completion with * the returned file references — matching the two-step flow in * ViewTaskDetails.jsx handleSubmit(). * * Route: POST /client/groups/:groupId/task-lists/:taskListId/tasks/:taskId/upload * * Author: rgrgogu * Date Created: Jun. 13, 2026 ***********************************************************************************************************************************************************************/ const { Task, TaskList, TaskListGroup } = require('../../models/task/task.mdl'); const { mdl_UserGroups, mdl_UserGroupMembers } = require('../../models/users/user_groups.mdl'); const { uploadFile } = require('../../services/s3.service'); const R = require('../../utils/response.util'); // ─── Helper: verify user is a member of the group ───────────────────────────── const isMember = async (userId, groupId) => { const membership = await mdl_UserGroupMembers.findOne({ where: { user_id: userId, group_id: groupId, deletedAt: null }, }); return !!membership; }; // ─── Helper: verify task belongs to task list AND is assigned to this group ─── const getAccessibleTask = async (groupId, taskListId, taskId) => { return Task.findOne({ where: { task_id: taskId, task_list_id: taskListId }, include: [ { model: TaskList, as: 'taskList', required: true, include: [ { model: mdl_UserGroups, as: 'groups', where: { group_id: groupId }, required: true, attributes: [], through: { model: TaskListGroup, attributes: [] }, }, ], }, ], }); }; // ─── Resolve S3 ownerType from mime type ────────────────────────────────────── const resolveOwnerType = (mimetype = '') => { if (mimetype.startsWith('image/')) return 'image'; if (mimetype.startsWith('video/')) return 'video'; if (mimetype.startsWith('audio/')) return 'audio'; return 'document'; }; // ============================================================================= // ── UPLOAD FILE ─────────────────────────────────────────────────────────────── // ============================================================================= // POST /client/groups/:groupId/task-lists/:taskListId/tasks/:taskId/upload // // Accepts: multipart/form-data // file — single file field (multer attaches to req.file) // // Returns: // { // file_url : "https://cdn.yourdomain.com/your-bucket/documents/uuid.pdf", // file_name : "social_media_slides.pdf", // file_size : 2400000, // mime_type : "application/pdf", // storage_key: "documents/uuid.pdf" ← for admin reference / future delete // } exports.uploadTaskFile = async (req, res) => { try { const { groupId, taskListId, taskId } = req.params; // ── Validate member ─────────────────────────────────────────────────── const member = await isMember(req.user.user_id, groupId); if (!member) return R.error(res, 'Group not found or you are not a member.', 403); // ── Validate task accessibility ─────────────────────────────────────── const task = await getAccessibleTask(groupId, taskListId, taskId); if (!task) return R.error(res, 'Task not found or not accessible.', 404); // ── Validate file presence ──────────────────────────────────────────── if (!req.file) return R.error(res, 'No file provided.', 400); const { buffer, originalname, mimetype, size } = req.file; const ownerType = resolveOwnerType(mimetype); // ── Upload to S3 ────────────────────────────────────────────────────── const { url, uuid: storage_key } = await uploadFile({ buffer, originalname, mimetype, ownerType, }); return R.success(res, 'File uploaded successfully.', { file_url: url, file_name: originalname, file_size: size, mime_type: mimetype, storage_key, }, 201); } catch (err) { console.error('[CLIENT][UPLOAD TASK FILE]', err); return R.error(res, 'Could not upload file.', 500); } };