diff --git a/packages/server/src/routes/users.ts b/packages/server/src/routes/users.ts index 9153dc16..261b0b75 100644 --- a/packages/server/src/routes/users.ts +++ b/packages/server/src/routes/users.ts @@ -183,6 +183,10 @@ export async function userRoutes(app: FastifyInstance): Promise { return reply.code(400).send({ error: 'Avatar URL must be a relative upload path or http/https URL', statusCode: 400 }); } updateData.avatar = avatar; + // Normalize to bare filename — profileSync historically stored /api/uploads/ prefix + if (typeof updateData.avatar === 'string' && updateData.avatar.startsWith('/api/uploads/')) { + updateData.avatar = updateData.avatar.slice('/api/uploads/'.length); + } } if (banner !== undefined) { @@ -191,6 +195,10 @@ export async function userRoutes(app: FastifyInstance): Promise { } if (banner && typeof banner === 'string' && banner.trim().length > 0) { updateData.banner = banner.trim(); + // Normalize to bare filename — profileSync historically stored /api/uploads/ prefix + if (updateData.banner.startsWith('/api/uploads/')) { + updateData.banner = updateData.banner.slice('/api/uploads/'.length); + } } else { updateData.banner = null; } diff --git a/packages/web/src/utils/assetUrls.ts b/packages/web/src/utils/assetUrls.ts index 3da3b630..0c049853 100644 --- a/packages/web/src/utils/assetUrls.ts +++ b/packages/web/src/utils/assetUrls.ts @@ -1,5 +1,10 @@ import { getApiForOrigin } from '../stores/spaceStore'; +/** Strip /api/uploads/ prefix to get bare filename. No-op for bare filenames and absolute URLs. */ +export function stripUploadPrefix(filename: string): string { + return filename.startsWith('/api/uploads/') ? filename.slice('/api/uploads/'.length) : filename; +} + /** * Resolve a relative asset filename to an absolute URL for remote origins. * Home-origin filenames are returned as-is (components handle the /api/uploads/ prefix). @@ -7,7 +12,7 @@ import { getApiForOrigin } from '../stores/spaceStore'; */ export function resolveAssetUrl(filename: string | null | undefined, origin: string): typeof filename { if (!filename || !origin || filename.startsWith('http')) return filename; - return getApiForOrigin(origin).uploads.url(filename); + return getApiForOrigin(origin).uploads.url(stripUploadPrefix(filename)); } /** diff --git a/packages/web/src/utils/profileSync.ts b/packages/web/src/utils/profileSync.ts index 027ab64b..820a7208 100644 --- a/packages/web/src/utils/profileSync.ts +++ b/packages/web/src/utils/profileSync.ts @@ -10,7 +10,9 @@ async function downloadAsset(filename: string, origin?: string): Promise { const res = await fetch(filename); return res.blob(); } - const base = origin ? `${origin}/api/uploads/${filename}` : `/api/uploads/${filename}`; + // Strip /api/uploads/ prefix to avoid double-path URLs (profileSync previously stored full paths) + const bare = filename.startsWith('/api/uploads/') ? filename.slice('/api/uploads/'.length) : filename; + const base = origin ? `${origin}/api/uploads/${bare}` : `/api/uploads/${bare}`; const res = await fetch(base); return res.blob(); } @@ -62,7 +64,7 @@ async function pushProfileToRemote(inst: ConnectedInstance, homeUser: NonNullabl try { const blob = await downloadAsset(homeUser.avatar); const attachment = await inst.api.uploads.upload(new File([blob], homeUser.avatar)); - payload.avatar = `/api/uploads/${attachment.filename}`; + payload.avatar = attachment.filename; } catch (err) { console.warn('[ProfileSync] Failed to upload avatar to remote:', err); } @@ -75,7 +77,7 @@ async function pushProfileToRemote(inst: ConnectedInstance, homeUser: NonNullabl try { const blob = await downloadAsset(homeUser.banner); const attachment = await inst.api.uploads.upload(new File([blob], homeUser.banner)); - payload.banner = `/api/uploads/${attachment.filename}`; + payload.banner = attachment.filename; } catch (err) { console.warn('[ProfileSync] Failed to upload banner to remote:', err); } @@ -108,7 +110,7 @@ async function pullProfileFromRemote(inst: ConnectedInstance): Promise { try { const blob = await downloadAsset(remoteUser.avatar, inst.origin); const attachment = await api.uploads.upload(new File([blob], remoteUser.avatar.split('/').pop() || 'avatar')); - payload.avatar = `/api/uploads/${attachment.filename}`; + payload.avatar = attachment.filename; } catch (err) { console.warn('[ProfileSync] Failed to download/upload avatar from remote:', err); } @@ -121,7 +123,7 @@ async function pullProfileFromRemote(inst: ConnectedInstance): Promise { try { const blob = await downloadAsset(remoteUser.banner, inst.origin); const attachment = await api.uploads.upload(new File([blob], remoteUser.banner.split('/').pop() || 'banner')); - payload.banner = `/api/uploads/${attachment.filename}`; + payload.banner = attachment.filename; } catch (err) { console.warn('[ProfileSync] Failed to download/upload banner from remote:', err); } @@ -206,7 +208,7 @@ export async function syncProfileUpdateToRemotes(update: Partial