/*********************************************************************************************************************************************************************** * File Name: task_download.controller.js (client) * Type of Program: Controller * Description: Proxies file downloads for task completion attachments through * the backend, so the raw Garage/S3 URL is never exposed to the * browser. Sets Content-Disposition: attachment with the original * filename. * * storage_key is DERIVED from file_url at request time (no schema * change needed) by stripping the known S3_PUBLIC_URL + bucket * prefix, since both are constants defined in s3.service.js / .env. * * Route: GET /client/groups/:groupId/task-lists/:taskListId/tasks/:taskId/completions/:completionId/files/:fileId/download * * Access: only the completion's owner (req.user.user_id === completion.user_id) * can download — admin downloads go through a separate admin route. * * Author: Kenneth Obsequio (@lash0000) * Date Created: Jun. 15, 2026 ***********************************************************************************************************************************************************************/ const { Task, TaskList, TaskListGroup } = require('../../models/task/task.mdl'); const { TaskCompletion, TaskCompletionFile } = require('../../models/task/task_completion.mdl'); const { mdl_UserGroups, mdl_UserGroupMembers } = require('../../models/users/user_groups.mdl'); const { getObjectStream } = 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: derive S3 storage_key from a public file_url ───────────────────── // Strips "{S3_PUBLIC_URL}/{S3_BUCKET}/" prefix, leaving e.g. "images/uuid.jpg" const deriveStorageKey = (fileUrl) => { const publicUrl = (process.env.S3_PUBLIC_URL || '').replace(/\/$/, ''); const bucket = process.env.S3_BUCKET || 'philproperties'; const prefix = `${publicUrl}/${bucket}/`; if (fileUrl && fileUrl.startsWith(prefix)) { return fileUrl.slice(prefix.length); } return null; }; // ============================================================================= // ── STREAM FILE (inline preview — no Content-Disposition: attachment) ──────── // ============================================================================= // // GET /client/groups/:groupId/task-lists/:taskListId/tasks/:taskId/completions/:completionId/files/:fileId/stream // // Used by FilePreview.jsx for /