diff --git a/frontend/src/components/ViewerModal.tsx b/frontend/src/components/ViewerModal.tsx index a225cd2..a78bf62 100644 --- a/frontend/src/components/ViewerModal.tsx +++ b/frontend/src/components/ViewerModal.tsx @@ -23,6 +23,8 @@ interface ViewerModalProps { onClose: () => void; onDelete?: (assetId: string) => void; onShare?: (assetId: string) => void; + shareToken?: string; + sharePassword?: string; } export default function ViewerModal({ @@ -31,10 +33,13 @@ export default function ViewerModal({ onClose, onDelete, onShare, + shareToken, + sharePassword, }: ViewerModalProps) { const [currentUrl, setCurrentUrl] = useState(''); const [loading, setLoading] = useState(true); const [currentIndex, setCurrentIndex] = useState(-1); + const [isBlobUrl, setIsBlobUrl] = useState(false); useEffect(() => { if (asset) { @@ -45,7 +50,7 @@ export default function ViewerModal({ // Cleanup blob URL on unmount or asset change return () => { - if (currentUrl) { + if (currentUrl && isBlobUrl) { URL.revokeObjectURL(currentUrl); } }; @@ -80,13 +85,26 @@ export default function ViewerModal({ try { setLoading(true); // Revoke previous blob URL to prevent memory leaks - if (currentUrl) { + if (currentUrl && isBlobUrl) { URL.revokeObjectURL(currentUrl); } - // Load media through backend proxy with auth - const blob = await api.getMediaBlob(asset.id, 'original'); - const url = URL.createObjectURL(blob); + + let url: string; + let isBlob: boolean; + + // If viewing shared content, use share download URL (pre-signed S3 URL) + if (shareToken) { + url = await api.getShareDownloadUrl(shareToken, asset.id, 'original', sharePassword); + isBlob = false; + } else { + // Otherwise use authenticated media endpoint with blob + const blob = await api.getMediaBlob(asset.id, 'original'); + url = URL.createObjectURL(blob); + isBlob = true; + } + setCurrentUrl(url); + setIsBlobUrl(isBlob); } catch (error) { console.error('Failed to load media:', error); } finally { diff --git a/frontend/src/pages/ShareViewPage.tsx b/frontend/src/pages/ShareViewPage.tsx index e382f84..d2a85e5 100644 --- a/frontend/src/pages/ShareViewPage.tsx +++ b/frontend/src/pages/ShareViewPage.tsx @@ -171,6 +171,8 @@ export default function ShareViewPage() { asset={viewerOpen ? asset : null} assets={[asset]} onClose={() => setViewerOpen(false)} + shareToken={token} + sharePassword={password} /> )} diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts index c24b485..29bb207 100644 --- a/frontend/src/services/api.ts +++ b/frontend/src/services/api.ts @@ -25,11 +25,17 @@ class ApiClient { }, }); - // Add auth token to requests + // Add auth token to requests (except for public share endpoints) this.client.interceptors.request.use((config) => { - const token = localStorage.getItem('access_token'); - if (token) { - config.headers.Authorization = `Bearer ${token}`; + // Skip auth for public share endpoints + const isShareEndpoint = config.url?.startsWith('/shares/') && + !config.url?.includes('/revoke'); + + if (!isShareEndpoint) { + const token = localStorage.getItem('access_token'); + if (token) { + config.headers.Authorization = `Bearer ${token}`; + } } return config; }); @@ -41,8 +47,10 @@ class ApiClient { if (error.response?.status === 401) { localStorage.removeItem('access_token'); localStorage.removeItem('refresh_token'); - // Redirect only if not already on login/register page - if (!['/login', '/register'].includes(window.location.pathname)) { + // Redirect only if not on login/register/share pages + const path = window.location.pathname; + const isPublicPage = ['/login', '/register'].includes(path) || path.startsWith('/share/'); + if (!isPublicPage) { window.location.href = '/login'; } }