diff --git a/cors.json b/cors.json new file mode 100644 index 0000000..3a2c660 --- /dev/null +++ b/cors.json @@ -0,0 +1,8 @@ +[ + { + "origin": ["https://cardex.xavidop.me", "http://localhost:3000", "http://localhost:9002"], + "method": ["GET", "HEAD"], + "maxAgeSeconds": 3600 + } + ] + \ No newline at end of file diff --git a/src/app/api/download/route.ts b/src/app/api/download/route.ts new file mode 100644 index 0000000..ffea9d5 --- /dev/null +++ b/src/app/api/download/route.ts @@ -0,0 +1,57 @@ +import { NextRequest, NextResponse } from 'next/server'; + +export async function GET(request: NextRequest) { + try { + const url = request.nextUrl.searchParams.get('url'); + + if (!url) { + return NextResponse.json( + { error: 'URL parameter is required' }, + { status: 400 } + ); + } + + // Validate that the URL is from Firebase Storage (production or emulator) + const isFirebaseStorage = url.includes('firebasestorage.googleapis.com'); + const isFirebaseEmulator = url.includes('localhost:9199') || url.includes('127.0.0.1:9199'); + + if (!isFirebaseStorage && !isFirebaseEmulator) { + return NextResponse.json( + { error: 'Only Firebase Storage URLs are allowed' }, + { status: 403 } + ); + } + + // Fetch the file from Firebase Storage + const response = await fetch(url, { + headers: { + 'User-Agent': 'Cardex-Download-Proxy/1.0', + }, + }); + + if (!response.ok) { + throw new Error(`Failed to fetch: ${response.status} ${response.statusText}`); + } + + // Get the content type from the original response + const contentType = response.headers.get('content-type') || 'application/octet-stream'; + + // Stream the response + return new NextResponse(response.body, { + headers: { + 'Content-Type': contentType, + 'Content-Disposition': 'attachment', + 'Cache-Control': 'public, max-age=31536000', + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET', + 'Access-Control-Allow-Headers': 'Content-Type', + }, + }); + } catch (error) { + console.error('Download proxy error:', error); + return NextResponse.json( + { error: 'Failed to download file' }, + { status: 500 } + ); + } +} diff --git a/src/utils/downloadUtils.ts b/src/utils/downloadUtils.ts index aa857dc..a42d798 100644 --- a/src/utils/downloadUtils.ts +++ b/src/utils/downloadUtils.ts @@ -21,16 +21,35 @@ export async function downloadCardImage(imageUrl: string, cardName: string): Pro // Data URL - use directly link.href = imageUrl; } else if (imageUrl.startsWith('http')) { - // Firebase Storage URL or other HTTP URL - fetch and convert to blob URL - const response = await fetch(imageUrl); - const blob = await response.blob(); - const blobUrl = URL.createObjectURL(blob); - link.href = blobUrl; - - // Clean up the blob URL after download - link.addEventListener('click', () => { - setTimeout(() => URL.revokeObjectURL(blobUrl), 100); - }); + // Firebase Storage URL or other HTTP URL - use proxy approach to avoid CORS + try { + const proxyUrl = `/api/download?url=${encodeURIComponent(imageUrl)}`; + const response = await fetch(proxyUrl); + + if (!response.ok) { + throw new Error('Failed to fetch through proxy'); + } + + const blob = await response.blob(); + const blobUrl = URL.createObjectURL(blob); + link.href = blobUrl; + + // Clean up the blob URL after download + link.addEventListener('click', () => { + setTimeout(() => URL.revokeObjectURL(blobUrl), 100); + }); + } catch (proxyError) { + console.error('Proxy download failed, trying direct fetch:', proxyError); + // Fallback to direct fetch (might still hit CORS) + const response = await fetch(imageUrl, { mode: 'cors' }); + const blob = await response.blob(); + const blobUrl = URL.createObjectURL(blob); + link.href = blobUrl; + + link.addEventListener('click', () => { + setTimeout(() => URL.revokeObjectURL(blobUrl), 100); + }); + } } else { // Assume it's a base64 string without data URL prefix const dataUrl = `data:image/png;base64,${imageUrl}`; @@ -84,16 +103,35 @@ export async function downloadCardVideo(videoUrl: string, cardName: string): Pro // Data URL - use directly link.href = videoUrl; } else if (videoUrl.startsWith('http')) { - // Firebase Storage URL or other HTTP URL - fetch and convert to blob URL - const response = await fetch(videoUrl); - const blob = await response.blob(); - const blobUrl = URL.createObjectURL(blob); - link.href = blobUrl; - - // Clean up the blob URL after download - link.addEventListener('click', () => { - setTimeout(() => URL.revokeObjectURL(blobUrl), 100); - }); + // Firebase Storage URL or other HTTP URL - use proxy approach to avoid CORS + try { + const proxyUrl = `/api/download?url=${encodeURIComponent(videoUrl)}`; + const response = await fetch(proxyUrl); + + if (!response.ok) { + throw new Error('Failed to fetch through proxy'); + } + + const blob = await response.blob(); + const blobUrl = URL.createObjectURL(blob); + link.href = blobUrl; + + // Clean up the blob URL after download + link.addEventListener('click', () => { + setTimeout(() => URL.revokeObjectURL(blobUrl), 100); + }); + } catch (proxyError) { + console.error('Proxy download failed, trying direct fetch:', proxyError); + // Fallback to direct fetch (might still hit CORS) + const response = await fetch(videoUrl, { mode: 'cors' }); + const blob = await response.blob(); + const blobUrl = URL.createObjectURL(blob); + link.href = blobUrl; + + link.addEventListener('click', () => { + setTimeout(() => URL.revokeObjectURL(blobUrl), 100); + }); + } } else { // Assume it's a base64 string without data URL prefix const dataUrl = `data:video/mp4;base64,${videoUrl}`;