Extract admin dashboard from ghabilee-frontend2 into a dedicated Next.js app for backoffice.ghabilee.ir (no SEO indexing / Clarity).
94 lines
2.6 KiB
TypeScript
94 lines
2.6 KiB
TypeScript
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<string>()
|
|
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',
|
|
},
|
|
})
|
|
}
|