mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
+26
-19
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user