mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
86 lines
3.3 KiB
JavaScript
86 lines
3.3 KiB
JavaScript
/***********************************************************************************************************************************************************************
|
|
* 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);
|
|
}
|
|
};
|