Use API route proxy for Directus assets instead of direct internal URL
Next.js image optimizer blocks internal Docker URLs due to SSRF protection (private IP resolution). Instead, proxy assets through /api/assets/[id] which fetches from internal Directus URL server-side. This bypasses both Cloudflare Access and SSRF protection since the <Image> src is a same-origin path, not an external URL. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
cd7c0200f3
commit
5f0d2eff16
|
|
@ -12,14 +12,10 @@ COPY . .
|
|||
|
||||
# Build args
|
||||
ARG DIRECTUS_API_TOKEN
|
||||
ARG NEXT_PUBLIC_DIRECTUS_ASSET_URL=http://katheryn-cms:8055
|
||||
ARG NEXT_PUBLIC_DIRECTUS_ASSET_TOKEN
|
||||
|
||||
# Set environment for build
|
||||
ENV NEXT_PUBLIC_DIRECTUS_URL=https://katheryn-cms.jeffemmett.com
|
||||
ENV DIRECTUS_API_TOKEN=${DIRECTUS_API_TOKEN}
|
||||
ENV NEXT_PUBLIC_DIRECTUS_ASSET_URL=${NEXT_PUBLIC_DIRECTUS_ASSET_URL}
|
||||
ENV NEXT_PUBLIC_DIRECTUS_ASSET_TOKEN=${NEXT_PUBLIC_DIRECTUS_ASSET_TOKEN}
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
|
||||
# Build the application
|
||||
|
|
|
|||
|
|
@ -5,8 +5,6 @@ services:
|
|||
dockerfile: Dockerfile
|
||||
args:
|
||||
- DIRECTUS_API_TOKEN=katheryn-frontend-readonly-8591de0316ded82fab45328cf1e49cb1
|
||||
- NEXT_PUBLIC_DIRECTUS_ASSET_URL=http://katheryn-cms:8055
|
||||
- NEXT_PUBLIC_DIRECTUS_ASSET_TOKEN=katheryn-frontend-readonly-8591de0316ded82fab45328cf1e49cb1
|
||||
container_name: katheryn-frontend
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
|
|
@ -15,8 +13,6 @@ services:
|
|||
- NEXT_PUBLIC_DIRECTUS_URL=https://katheryn-cms.jeffemmett.com
|
||||
- DIRECTUS_API_TOKEN=katheryn-frontend-readonly-8591de0316ded82fab45328cf1e49cb1
|
||||
- DIRECTUS_INTERNAL_URL=http://katheryn-cms:8055
|
||||
- NEXT_PUBLIC_DIRECTUS_ASSET_URL=http://katheryn-cms:8055
|
||||
- NEXT_PUBLIC_DIRECTUS_ASSET_TOKEN=katheryn-frontend-readonly-8591de0316ded82fab45328cf1e49cb1
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.katheryn-staging.rule=Host(`katheryn-staging.jeffemmett.com`)"
|
||||
|
|
|
|||
|
|
@ -15,12 +15,6 @@ const nextConfig: NextConfig = {
|
|||
port: '8055',
|
||||
pathname: '/assets/**',
|
||||
},
|
||||
{
|
||||
protocol: 'http',
|
||||
hostname: 'katheryn-cms',
|
||||
port: '8055',
|
||||
pathname: '/assets/**',
|
||||
},
|
||||
{
|
||||
protocol: 'https',
|
||||
hostname: 'images.squarespace-cdn.com',
|
||||
|
|
|
|||
|
|
@ -0,0 +1,42 @@
|
|||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
const DIRECTUS_URL = process.env.DIRECTUS_INTERNAL_URL || process.env.NEXT_PUBLIC_DIRECTUS_URL || 'https://katheryn-cms.jeffemmett.com';
|
||||
const API_TOKEN = process.env.DIRECTUS_API_TOKEN || '';
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
const { id } = await params;
|
||||
const { searchParams } = request.nextUrl;
|
||||
|
||||
// Build the Directus asset URL
|
||||
const assetUrl = new URL(`/assets/${id}`, DIRECTUS_URL);
|
||||
if (API_TOKEN) assetUrl.searchParams.set('access_token', API_TOKEN);
|
||||
|
||||
// Forward image transformation params to Directus
|
||||
for (const key of ['width', 'height', 'quality', 'format']) {
|
||||
const value = searchParams.get(key);
|
||||
if (value) assetUrl.searchParams.set(key, value);
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(assetUrl.toString());
|
||||
|
||||
if (!response.ok) {
|
||||
return new NextResponse(null, { status: response.status });
|
||||
}
|
||||
|
||||
const buffer = await response.arrayBuffer();
|
||||
const contentType = response.headers.get('content-type') || 'image/jpeg';
|
||||
|
||||
return new NextResponse(buffer, {
|
||||
headers: {
|
||||
'Content-Type': contentType,
|
||||
'Cache-Control': 'public, max-age=31536000, immutable',
|
||||
},
|
||||
});
|
||||
} catch {
|
||||
return new NextResponse(null, { status: 502 });
|
||||
}
|
||||
}
|
||||
|
|
@ -185,27 +185,21 @@ const publicUrl = process.env.NEXT_PUBLIC_DIRECTUS_URL || 'https://katheryn-cms.
|
|||
const directusUrl = process.env.DIRECTUS_INTERNAL_URL || publicUrl;
|
||||
const apiToken = process.env.DIRECTUS_API_TOKEN || '';
|
||||
|
||||
// For asset URLs: use internal Docker URL to bypass Cloudflare Access,
|
||||
// and use NEXT_PUBLIC_ token so it's available in client components
|
||||
const assetBaseUrl = process.env.NEXT_PUBLIC_DIRECTUS_ASSET_URL || publicUrl;
|
||||
const assetToken = process.env.NEXT_PUBLIC_DIRECTUS_ASSET_TOKEN || apiToken;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export const directus = createDirectus<any>(directusUrl)
|
||||
.with(staticToken(apiToken))
|
||||
.with(rest());
|
||||
|
||||
// Helper to get asset URL (includes auth token since Directus assets require authentication)
|
||||
// Uses internal Docker URL when available to bypass Cloudflare Access
|
||||
// Helper to get asset URL via local API proxy (/api/assets/[id])
|
||||
// The proxy fetches from internal Directus URL, bypassing both:
|
||||
// 1. Cloudflare Access (uses Docker internal network)
|
||||
// 2. Next.js SSRF protection (same-origin URL, no private IP resolution)
|
||||
export function getAssetUrl(fileId: string | DirectusFile | undefined, options?: { width?: number; height?: number; quality?: number; format?: 'webp' | 'jpg' | 'png' }): string {
|
||||
if (!fileId) return '/placeholder.jpg';
|
||||
|
||||
const id = typeof fileId === 'string' ? fileId : fileId.id;
|
||||
const params = new URLSearchParams();
|
||||
|
||||
// Auth token required for asset access
|
||||
if (assetToken) params.append('access_token', assetToken);
|
||||
|
||||
if (options) {
|
||||
if (options.width) params.append('width', options.width.toString());
|
||||
if (options.height) params.append('height', options.height.toString());
|
||||
|
|
@ -213,7 +207,8 @@ export function getAssetUrl(fileId: string | DirectusFile | undefined, options?:
|
|||
if (options.format) params.append('format', options.format);
|
||||
}
|
||||
|
||||
return `${assetBaseUrl}/assets/${id}?${params.toString()}`;
|
||||
const query = params.toString();
|
||||
return `/api/assets/${id}${query ? `?${query}` : ''}`;
|
||||
}
|
||||
|
||||
// Map Directus artwork fields to frontend Artwork interface
|
||||
|
|
|
|||
Loading…
Reference in New Issue