/*********************************************************************************************************************************************************************** * File Name: media.controller.js (admin) * Type of Program: Controller * Description: Issues short-lived JWT stream tokens for admin asset preview. * Works identically to the client media token flow but is scoped to * admin-authenticated requests and allows any asset regardless of * is_public. The stream endpoint (/api/client/media/stream/:token) * is shared — the JWT payload shape is identical. * * Author: Kenneth Obsequio (@lash0000) * Date Created: Jun. 22, 2026 ***********************************************************************************************************************************************************************/ "use strict"; const { Op } = require("sequelize"); const R = require("../../utils/response.util"); const mdl_Assets = require("../../models/assets/assets.mdl"); const mediaToken = require("../../services/mediaToken.service"); // ─── POST /admin/media/token ────────────────────────────────────────────────── exports.issueToken = async (req, res) => { try { const { asset_id } = req.body; if (!asset_id) return R.error(res, "asset_id is required.", 400); const asset = await mdl_Assets.findOne({ where: { asset_id, deletedAt: null }, attributes: ["asset_id", "file_type", "storage_provider", "storage_key", "mime_type", "thumbnail_storage_key"], }); if (!asset) return R.error(res, "Asset not found.", 404); if (!mediaToken.SUPPORTED_TYPES.includes(asset.file_type)) { return R.error(res, `Asset type "${asset.file_type}" is not supported.`, 400); } if (asset.storage_provider !== "s3") { return R.error(res, "Token flow is for S3 assets only. Use the raw file_url for other providers.", 400); } const ip = mediaToken.resolveIp(req); const { token, thumbnail_url } = await mediaToken.issueForAsset(asset, req.user.user_id, ip); return R.success(res, "Token issued.", { token, provider: "s3", file_type: asset.file_type, thumbnail_url, }); } catch (err) { console.error("[ADMIN][MEDIA][TOKEN]", err); return R.error(res, "Could not issue media token.", 500); } }; // ─── POST /admin/media/tokens (batch) ─────────────────────────────────────── // // Accepts { asset_ids: [id, ...] } — S3 assets only, max 50. // Returns { tokens: { [asset_id]: token } } // One round-trip instead of N per-card requests. exports.issueTokensBatch = async (req, res) => { try { const { asset_ids } = req.body; if (!Array.isArray(asset_ids) || !asset_ids.length) return R.error(res, "asset_ids must be a non-empty array.", 400); if (asset_ids.length > 50) return R.error(res, "Maximum 50 asset_ids per batch.", 400); const assets = await mdl_Assets.findAll({ where: { asset_id: { [Op.in]: asset_ids }, storage_provider: "s3", deletedAt: null, }, attributes: ["asset_id", "file_type", "storage_key", "mime_type", "thumbnail_storage_key"], }); const ip = mediaToken.resolveIp(req); const tokens = {}; const thumbnails = {}; await Promise.all(assets.map(async (asset) => { const { token, thumbnail_url } = await mediaToken.issueForAsset(asset, req.user.user_id, ip); tokens[String(asset.asset_id)] = token; if (thumbnail_url) thumbnails[String(asset.asset_id)] = thumbnail_url; })); return R.success(res, "Tokens issued.", { tokens, thumbnails }); } catch (err) { console.error("[ADMIN][MEDIA][TOKENS BATCH]", err); return R.error(res, "Could not issue media tokens.", 500); } };