/*********************************************************************************************************************************************************************** * File Name: media.controller.js (public) * Type of Program: Controller * Description: Issues stream tokens for publicly accessible S3 assets — no auth required. * Guards: * 1. Asset must exist and not be deleted * 2. is_public must be true — private assets are always rejected (403) * 3. storage_provider must be "s3" — Chibisafe uses its raw CDN URL * Token shape is identical to the client/admin token flows so the * shared stream endpoint (/api/client/media/stream/:token) accepts it. * No user_id is embedded — the token is anonymous. * IP is still bound so a leaked token is useless on another machine. * * Author: Kenneth Obsequio (@lash0000) * Date Created: Jun. 22, 2026 ***********************************************************************************************************************************************************************/ "use strict"; const jwt = require("jsonwebtoken"); const R = require("../../utils/response.util"); const mdl_Assets = require("../../models/assets/assets.mdl"); const MEDIA_SECRET = process.env.MEDIA_JWT_SECRET ?? process.env.JWT_SECRET; const TOKEN_TTL_SEC = 4 * 60 * 60; // 4 hours — matches client TTL const SUPPORTED_TYPES = ["video", "audio", "document", "image"]; function resolveIp(req) { const forwarded = req.headers["x-forwarded-for"]; if (forwarded) return forwarded.split(",")[0].trim(); return req.ip ?? req.socket?.remoteAddress ?? "unknown"; } // ─── GET /public/media/token?asset_id=X ────────────────────────────────────── exports.issueToken = async (req, res) => { try { const asset_id = req.query.asset_id ?? req.body?.asset_id; 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", "is_public"], }); if (!asset) return R.error(res, "File not found.", 404); // Private assets are never served through the public endpoint if (!asset.is_public) return R.error(res, "Forbidden.", 403); if (!SUPPORTED_TYPES.includes(asset.file_type)) { return R.error(res, `File type "${asset.file_type}" is not supported.`, 400); } if (asset.storage_provider !== "s3") { return R.error(res, "Token flow is for S3 files only. Use the raw file_url for other providers.", 400); } const ip = resolveIp(req); const token = jwt.sign( { asset_id, // no user_id — anonymous public token storage_key: asset.storage_key, file_type: asset.file_type, mime_type: asset.mime_type, ip, }, MEDIA_SECRET, { expiresIn: TOKEN_TTL_SEC } ); return R.success(res, "Token issued.", { token, provider: "s3", file_type: asset.file_type, }); } catch (err) { console.error("[PUBLIC][MEDIA][TOKEN]", err); return R.error(res, "Could not issue media token.", 500); } };