Files
starr-philproperties/controllers/admin/media.controller.js
T
kennethobsequio 89acdfc239 push
pushy

Signed-off-by: Kenneth Obsequio <k80308392@gmail.com>
2026-06-28 11:29:07 +08:00

131 lines
4.7 KiB
JavaScript

/***********************************************************************************************************************************************************************
* 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 jwt = require("jsonwebtoken");
const R = require("../../utils/response.util");
const mdl_Assets = require("../../models/assets/assets.mdl");
const s3 = require("../../services/s3.service");
const MEDIA_SECRET = process.env.MEDIA_JWT_SECRET ?? process.env.JWT_SECRET;
const TOKEN_TTL_SEC = 30 * 60; // 30 min — admin preview session
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";
}
function signToken(asset, userId, ip) {
return jwt.sign(
{
asset_id: asset.asset_id,
user_id: userId,
storage_key: asset.storage_key,
file_type: asset.file_type,
mime_type: asset.mime_type,
ip,
},
MEDIA_SECRET,
{ expiresIn: TOKEN_TTL_SEC }
);
}
// ─── 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 (!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 = resolveIp(req);
const token = signToken(asset, req.user.user_id, ip);
// ── Presign thumbnail URL so the browser can load it directly ─────────────
let thumbnail_url = null;
if (asset.thumbnail_storage_key) {
try {
thumbnail_url = await s3.getSignedDownloadUrl(asset.thumbnail_storage_key, TOKEN_TTL_SEC);
} catch {
// Non-fatal — thumbnail is cosmetic
}
}
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"],
});
const ip = resolveIp(req);
const tokens = {};
for (const asset of assets) {
tokens[String(asset.asset_id)] = signToken(asset, req.user.user_id, ip);
}
return R.success(res, "Tokens issued.", { tokens });
} catch (err) {
console.error("[ADMIN][MEDIA][TOKENS BATCH]", err);
return R.error(res, "Could not issue media tokens.", 500);
}
};