mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
chore: relocate backend into apps/api ahead of monorepo merge
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
/***********************************************************************************************************************************************************************
|
||||
* 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);
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user