import { type NextRequest, NextResponse } from 'next/server' const guessFileName = (imageUrl: string) => { try { const pathname = new URL(imageUrl).pathname const base = pathname.split('/').filter(Boolean).at(-1) if (base && /\.[a-z0-9]{2,5}$/i.test(base)) return decodeURIComponent(base) } catch { // ignore } return `chat-image-${Date.now()}.jpg` } const resolveAllowedUploadOrigins = (): string[] => { const origins = new Set() const fileServer = process.env.NEXT_PUBLIC_FILE_SERVER_URL?.trim() const siteUrl = process.env.NEXT_PUBLIC_SITE_URL?.trim() const apiUrl = process.env.NEXT_PUBLIC_API_URL?.trim() for (const value of [fileServer, siteUrl, apiUrl]) { if (!value) continue try { origins.add(new URL(value).origin) } catch { // ignore invalid env URLs } } origins.add('https://ghabilee.ir') origins.add('https://www.ghabilee.ir') origins.add('https://dev.ghabilee.ir') return [...origins] } /** * Same-origin download proxy for chat/upload images. * Needed when the page origin (e.g. localhost:3008) differs from the file host * (ghabilee.ir) and the CDN has not yet emitted Access-Control-Allow-Origin. */ export async function GET(request: NextRequest) { const rawUrl = request.nextUrl.searchParams.get('url')?.trim() if (!rawUrl) { return NextResponse.json({ message: 'url is required' }, { status: 400 }) } let target: URL try { target = new URL(rawUrl) } catch { return NextResponse.json({ message: 'invalid url' }, { status: 400 }) } if (target.protocol !== 'https:' && target.protocol !== 'http:') { return NextResponse.json({ message: 'unsupported protocol' }, { status: 400 }) } const allowed = resolveAllowedUploadOrigins() if (!allowed.includes(target.origin)) { return NextResponse.json({ message: 'url host is not allowed' }, { status: 403 }) } if (!target.pathname.startsWith('/uploads/')) { return NextResponse.json({ message: 'only /uploads paths are allowed' }, { status: 403 }) } const upstream = await fetch(target.toString(), { headers: { Accept: 'image/*,*/*' }, redirect: 'error', }) if (!upstream.ok) { return NextResponse.json({ message: 'upstream fetch failed' }, { status: 502 }) } const contentType = upstream.headers.get('content-type') || 'application/octet-stream' const fileName = guessFileName(target.toString()) const bytes = await upstream.arrayBuffer() return new NextResponse(bytes, { status: 200, headers: { 'Content-Type': contentType, 'Content-Disposition': `attachment; filename="${fileName}"`, 'Cache-Control': 'private, no-store', }, }) }