Signed-off-by: Kenneth Obsequio <k80308392@gmail.com>
This commit is contained in:
2026-08-13 19:58:52 +08:00
parent 9018d6d158
commit 2e9c2ad43f
23 changed files with 954 additions and 287 deletions
+26 -19
View File
@@ -1,33 +1,40 @@
// utils/resolveAvatar.util.js
//
// Resolves a stored avatar into a browser-usable URL at read time.
// Resolves a stored avatar into a browser-usable reference at read time.
//
// personal_info.avatar.url is never trustworthy as stored:
// - S3-stored avatars (avatar.uuid present) were historically saved with a
// raw, unsigned bucket URL (see s3.service.js buildPublicUrl). The current
// Garage ingress (Cloudflare lane, see chibistar/Caddyfile) forwards reads
// straight to Garage with no re-signing, and Garage rejects anonymous
// requests outright — so that stored URL 403s in the browser. Even a
// presigned URL would go stale if persisted (getPublicUrl() expires in 4h),
// so the only correct fix is to mint a fresh one on every read from the
// stored key (avatar.uuid), never trust what's on the row.
// personal_info.avatar is never handed to the browser as a raw S3 URL —
// same protection as every other media type in this app (see
// controllers/client/media.controller.js): the browser only ever gets a
// short-lived opaque stream_token, proxied through
// GET /client/media/stream/:token, which mints the real presigned URL
// server-side and pipes the bytes back. The real bucket/key/signature never
// reach the DOM.
//
// - S3-stored avatars (avatar.uuid present) → sign a fresh media JWT from
// the stored key on every read (never persist a token/URL on the row).
// - Google-picture avatars (reg_type: 'google', no uuid — see
// auth.controller.js googleCallback) are an external CDN URL and pass
// through unchanged; there's nothing of ours to sign.
// through unchanged as file_url; there's nothing of ours to sign or hide.
//
const { getPublicUrl } = require('../services/s3.service');
const { signMediaToken } = require('../services/mediaToken.service');
async function resolveAvatarUrl(avatar) {
if (!avatar) return avatar ?? null;
if (!avatar.uuid) return avatar; // external URL (e.g. Google) — nothing to sign
try {
const url = await getPublicUrl(avatar.uuid);
return { ...avatar, url };
} catch (err) {
console.error('[AVATAR] Failed to resolve presigned URL for', avatar.uuid, err);
return avatar; // fall back to the stored value rather than failing the whole response
if (!avatar.uuid) {
// External URL (e.g. Google) — nothing to sign, just normalize the field
// name to match the { stream_token?, file_url? } shape resolveAssetSrc()
// already expects for every other media payload.
return { ...avatar, url: undefined, file_url: avatar.url };
}
const token = signMediaToken({
storage_key: avatar.uuid,
file_type: 'image',
mime_type: avatar.mime_type,
});
return { ...avatar, url: undefined, file_url: undefined, stream_token: token };
}
// Mutates-and-returns a shallow copy of a user (plain object or Sequelize
+48
View File
@@ -0,0 +1,48 @@
/***********************************************************************************************************************************************************************
* File Name : suspendGuard.util.js
* Type : Utility
* Description : Local-dev-only watchdog that detects the host machine coming
* back from sleep/idle (laptop lid closed, suspended, etc.)
* and stops the dev server immediately.
*
* Why: when the process is frozen mid-sleep, node-cron's own
* heartbeat later finds itself hours behind schedule and logs
* a "[NODE-CRON] missed execution" warning for every tick that
* elapsed — one line per missed minute, easily hundreds after
* an overnight sleep. There's nothing to recover here (no
* request was dropped, no job silently failed); the correct
* behavior is just "the dev server wasn't meaningfully running
* during that time," so we exit instead of logging noise.
*
* Never runs when NODE_ENV=production — the droplet process
* is long-running and must never self-exit on its own.
*
* Author: Kenneth Obsequio (@lash0000)
* Date Created: Aug. 11, 2026
***********************************************************************************************************************************************************************/
'use strict';
const CHECK_INTERVAL_MS = 250;
const GAP_THRESHOLD_MS = 10_000; // far beyond normal event-loop jitter
function startSuspendGuard() {
if (process.env.NODE_ENV === 'production') return;
let last = Date.now();
setInterval(() => {
const now = Date.now();
const gap = now - last - CHECK_INTERVAL_MS;
last = now;
if (gap > GAP_THRESHOLD_MS) {
console.log(
`\n🛑 Dev server was asleep/idle for ~${Math.round(gap / 1000)}s (laptop suspend or similar). ` +
`Stopping instead of letting node-cron dump missed-execution warnings.\n`
);
process.exit(0);
}
}, CHECK_INTERVAL_MS).unref();
}
module.exports = { startSuspendGuard };