Files
starr-philproperties/apps/api/controllers/client/task_upload.controller.js
T

118 lines
5.5 KiB
JavaScript

/***********************************************************************************************************************************************************************
* 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);
}
};