From de09ea039bea2fdfe3f3a9b3bc6c368a3a27f9f7 Mon Sep 17 00:00:00 2001 From: Fringg Date: Sun, 8 Feb 2026 17:40:24 +0300 Subject: [PATCH] fix: hide backend URL from logo by fetching as blob Logo images were exposing the backend API URL in the DOM via . Now preloadLogo() fetches the image as a blob and serves it through URL.createObjectURL(), so only blob: URLs appear in the page source. Blob is invalidated on logo upload/delete. --- src/api/branding.ts | 78 ++++++++++++------- src/components/layout/AppShell/AppHeader.tsx | 2 +- .../layout/AppShell/DesktopSidebar.tsx | 2 +- src/hooks/useBranding.ts | 2 +- src/pages/Login.tsx | 2 +- 5 files changed, 53 insertions(+), 33 deletions(-) diff --git a/src/api/branding.ts b/src/api/branding.ts index e3e50f4..8dfc21c 100644 --- a/src/api/branding.ts +++ b/src/api/branding.ts @@ -28,16 +28,19 @@ export interface AnalyticsCounters { const BRANDING_CACHE_KEY = 'cabinet_branding'; const LOGO_PRELOADED_KEY = 'cabinet_logo_preloaded'; +// In-memory blob URL cache to avoid exposing backend URL +let _logoBlobUrl: string | null = null; + // Check if logo was already preloaded in this session export const isLogoPreloaded = (): boolean => { try { + if (_logoBlobUrl) return true; const cached = getCachedBranding(); if (!cached?.has_custom_logo || !cached?.logo_url) { return false; } - const logoUrl = `${import.meta.env.VITE_API_URL || ''}${cached.logo_url}`; const preloaded = sessionStorage.getItem(LOGO_PRELOADED_KEY); - return preloaded === logoUrl; + return preloaded === cached.logo_url; } catch { return false; } @@ -65,33 +68,42 @@ export const setCachedBranding = (branding: BrandingInfo) => { } }; -// Preload logo image for instant display -export const preloadLogo = (branding: BrandingInfo): Promise => { - return new Promise((resolve) => { - if (!branding.has_custom_logo || !branding.logo_url) { - resolve(); - return; - } +// Preload logo image as blob to hide backend URL +export const preloadLogo = async (branding: BrandingInfo): Promise => { + if (!branding.has_custom_logo || !branding.logo_url) { + return; + } + // Check if already preloaded in this session + if (_logoBlobUrl) { + return; + } + + const preloaded = sessionStorage.getItem(LOGO_PRELOADED_KEY); + if (preloaded === branding.logo_url && _logoBlobUrl) { + return; + } + + try { const logoUrl = `${import.meta.env.VITE_API_URL || ''}${branding.logo_url}`; + const response = await fetch(logoUrl); + if (!response.ok) return; - // Check if already preloaded in this session - const preloaded = sessionStorage.getItem(LOGO_PRELOADED_KEY); - if (preloaded === logoUrl) { - resolve(); - return; + const blob = await response.blob(); + // Revoke previous blob URL if exists + if (_logoBlobUrl) { + URL.revokeObjectURL(_logoBlobUrl); } - - const img = new Image(); - img.onload = () => { - sessionStorage.setItem(LOGO_PRELOADED_KEY, logoUrl); - resolve(); - }; - img.onerror = () => resolve(); - img.src = logoUrl; - }); + _logoBlobUrl = URL.createObjectURL(blob); + sessionStorage.setItem(LOGO_PRELOADED_KEY, branding.logo_url); + } catch { + // Fetch failed, logo will use letter fallback + } }; +// Get the blob URL for the logo (safe, doesn't expose backend) +export const getLogoBlobUrl = (): string | null => _logoBlobUrl; + // Initialize logo preload from cache on page load export const initLogoPreload = () => { const cached = getCachedBranding(); @@ -122,21 +134,29 @@ export const brandingApi = { 'Content-Type': 'multipart/form-data', }, }); + // Invalidate cached blob so it gets re-fetched + if (_logoBlobUrl) { + URL.revokeObjectURL(_logoBlobUrl); + _logoBlobUrl = null; + } + sessionStorage.removeItem(LOGO_PRELOADED_KEY); return response.data; }, // Delete custom logo (admin only) deleteLogo: async (): Promise => { const response = await apiClient.delete('/cabinet/branding/logo'); + if (_logoBlobUrl) { + URL.revokeObjectURL(_logoBlobUrl); + _logoBlobUrl = null; + } + sessionStorage.removeItem(LOGO_PRELOADED_KEY); return response.data; }, - // Get logo URL (without cache busting - server handles caching via Cache-Control headers) - getLogoUrl: (branding: BrandingInfo): string | null => { - if (!branding.has_custom_logo || !branding.logo_url) { - return null; - } - return `${import.meta.env.VITE_API_URL || ''}${branding.logo_url}`; + // Get logo URL as blob (hides backend URL from DOM) + getLogoUrl: (_branding: BrandingInfo): string | null => { + return _logoBlobUrl; }, // Get animation enabled (public, no auth required) diff --git a/src/components/layout/AppShell/AppHeader.tsx b/src/components/layout/AppShell/AppHeader.tsx index 4799b62..84d6ee0 100644 --- a/src/components/layout/AppShell/AppHeader.tsx +++ b/src/components/layout/AppShell/AppHeader.tsx @@ -89,7 +89,7 @@ export function AppHeader({ queryFn: async () => { const data = await brandingApi.getBranding(); setCachedBranding(data); - preloadLogo(data); + await preloadLogo(data); return data; }, initialData: getCachedBranding() ?? undefined, diff --git a/src/components/layout/AppShell/DesktopSidebar.tsx b/src/components/layout/AppShell/DesktopSidebar.tsx index 9b71433..2f826a5 100644 --- a/src/components/layout/AppShell/DesktopSidebar.tsx +++ b/src/components/layout/AppShell/DesktopSidebar.tsx @@ -58,7 +58,7 @@ export function DesktopSidebar({ queryFn: async () => { const data = await brandingApi.getBranding(); setCachedBranding(data); - preloadLogo(data); + await preloadLogo(data); return data; }, initialData: getCachedBranding() ?? undefined, diff --git a/src/hooks/useBranding.ts b/src/hooks/useBranding.ts index b68462d..c827ea4 100644 --- a/src/hooks/useBranding.ts +++ b/src/hooks/useBranding.ts @@ -23,7 +23,7 @@ export function useBranding() { queryFn: async () => { const data = await brandingApi.getBranding(); setCachedBranding(data); - preloadLogo(data); + await preloadLogo(data); return data; }, initialData: getCachedBranding() ?? undefined, diff --git a/src/pages/Login.tsx b/src/pages/Login.tsx index 6f1a58f..f35fb15 100644 --- a/src/pages/Login.tsx +++ b/src/pages/Login.tsx @@ -79,7 +79,7 @@ export default function Login() { queryFn: async () => { const data = await brandingApi.getBranding(); setCachedBranding(data); - preloadLogo(data); + await preloadLogo(data); return data; }, staleTime: 60000,