mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
@@ -0,0 +1,117 @@
|
||||
/***********************************************************************************************************************************************************************
|
||||
* File Name: mediaToken.service.js
|
||||
* Type of Program: Service
|
||||
* Description: Issues + caches short-lived JWT stream tokens (and presigned S3
|
||||
* thumbnail URLs) for asset preview. Shared by:
|
||||
* - controllers/admin/media.controller.js (POST /admin/media/token(s))
|
||||
* - controllers/admin/assets.controller.js (embeds tokens directly
|
||||
* into GET /admin/assets rows so pickers don't need a second
|
||||
* round-trip just to render thumbnails)
|
||||
*
|
||||
* Author: Kenneth Obsequio (@lash0000)
|
||||
***********************************************************************************************************************************************************************/
|
||||
"use strict";
|
||||
|
||||
const jwt = require("jsonwebtoken");
|
||||
const s3 = require("./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"];
|
||||
|
||||
// ─── In-memory token cache (no Redis yet) ──────────────────────────────────────
|
||||
// Avoids re-signing a JWT / re-presigning the thumbnail S3 URL for an asset that
|
||||
// already has a still-valid token. Keyed by (asset_id, ip) because the stream
|
||||
// endpoint (/api/client/media/stream/:token) pins the token to the issuing
|
||||
// request's IP — reusing a token minted for a different IP would get rejected.
|
||||
// Single-process only; each app instance keeps its own cache.
|
||||
const TOKEN_CACHE_MARGIN_SEC = 120; // re-mint a bit before actual expiry
|
||||
const tokenCache = new Map(); // `${asset_id}:${ip}` -> { token, thumbnail_url, expiresAt }
|
||||
|
||||
function tokenCacheKey(assetId, ip) {
|
||||
return `${assetId}:${ip}`;
|
||||
}
|
||||
|
||||
function getCachedToken(assetId, ip) {
|
||||
const key = tokenCacheKey(assetId, ip);
|
||||
const entry = tokenCache.get(key);
|
||||
if (!entry) return null;
|
||||
if (Date.now() >= entry.expiresAt) {
|
||||
tokenCache.delete(key);
|
||||
return null;
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
|
||||
function setCachedToken(assetId, ip, token, thumbnail_url) {
|
||||
tokenCache.set(tokenCacheKey(assetId, ip), {
|
||||
token,
|
||||
thumbnail_url,
|
||||
expiresAt: Date.now() + (TOKEN_TTL_SEC - TOKEN_CACHE_MARGIN_SEC) * 1000,
|
||||
});
|
||||
}
|
||||
|
||||
// Collapses IPv4-mapped IPv6 ("::ffff:127.0.0.1") and IPv6 loopback ("::1")
|
||||
// down to a single canonical form. Without this, a token minted off one
|
||||
// "localhost" connection (IPv4) fails IP-pin verification on a sibling
|
||||
// request that happened to land on the other stack (IPv6) — browsers race
|
||||
// both when resolving "localhost", so mint and stream requests can land on
|
||||
// different stacks even from the same client.
|
||||
function normalizeIp(ip) {
|
||||
if (ip === "::1") return "127.0.0.1";
|
||||
if (ip.startsWith("::ffff:")) return ip.slice(7);
|
||||
return ip;
|
||||
}
|
||||
|
||||
function resolveIp(req) {
|
||||
const forwarded = req.headers["x-forwarded-for"];
|
||||
const raw = forwarded ? forwarded.split(",")[0].trim() : (req.ip ?? req.socket?.remoteAddress ?? "unknown");
|
||||
return normalizeIp(raw);
|
||||
}
|
||||
|
||||
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 }
|
||||
);
|
||||
}
|
||||
|
||||
// ─── issueForAsset ─────────────────────────────────────────────────────────────
|
||||
//
|
||||
// Returns { token, thumbnail_url } for an S3 asset, minting + caching on first
|
||||
// call and serving from tokenCache on subsequent calls within the TTL margin.
|
||||
// `asset` needs: asset_id, storage_key, file_type, mime_type, thumbnail_storage_key.
|
||||
//
|
||||
async function issueForAsset(asset, userId, ip) {
|
||||
const cached = getCachedToken(asset.asset_id, ip);
|
||||
if (cached) return { token: cached.token, thumbnail_url: cached.thumbnail_url };
|
||||
|
||||
const token = signToken(asset, userId, ip);
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
setCachedToken(asset.asset_id, ip, token, thumbnail_url);
|
||||
return { token, thumbnail_url };
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
TOKEN_TTL_SEC,
|
||||
SUPPORTED_TYPES,
|
||||
resolveIp,
|
||||
issueForAsset,
|
||||
};
|
||||
Reference in New Issue
Block a user