// utils/resolveAvatar.util.js // // Resolves a stored avatar into a browser-usable URL 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. // - 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. // const { getPublicUrl } = require('../services/s3.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 } } // 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 };