// utils/resolveAvatar.util.js // // Resolves a stored avatar into a browser-usable reference at read time. // // 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 as file_url; there's nothing of ours to sign or hide. // const { signMediaToken } = require('../services/mediaToken.service'); async function resolveAvatarUrl(avatar) { if (!avatar) return avatar ?? null; 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 // instance) with personal_info.avatar resolved. Safe to call on a user with // no avatar at all. async function resolveUserAvatar(user) { if (!user) return user; const u = user.toJSON ? user.toJSON() : { ...user }; if (!u.personal_info?.avatar) return u; u.personal_info = { ...u.personal_info, avatar: await resolveAvatarUrl(u.personal_info.avatar), }; return u; } module.exports = { resolveAvatarUrl, resolveUserAvatar };