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
|
# Build args
|
||||||
ARG DIRECTUS_API_TOKEN
|
ARG DIRECTUS_API_TOKEN
|
||||||
ARG NEXT_PUBLIC_DIRECTUS_ASSET_URL=http://katheryn-cms:8055
|
|
||||||
ARG NEXT_PUBLIC_DIRECTUS_ASSET_TOKEN
|
|
||||||
|
|
||||||
# Set environment for build
|
# Set environment for build
|
||||||
ENV NEXT_PUBLIC_DIRECTUS_URL=https://katheryn-cms.jeffemmett.com
|
ENV NEXT_PUBLIC_DIRECTUS_URL=https://katheryn-cms.jeffemmett.com
|
||||||
ENV DIRECTUS_API_TOKEN=${DIRECTUS_API_TOKEN}
|
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
|
ENV NEXT_TELEMETRY_DISABLED=1
|
||||||
|
|
||||||
# Build the application
|
# Build the application
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,6 @@ services:
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
args:
|
args:
|
||||||
- DIRECTUS_API_TOKEN=katheryn-frontend-readonly-8591de0316ded82fab45328cf1e49cb1
|
- 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
|
container_name: katheryn-frontend
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
env_file:
|
env_file:
|
||||||
|
|
@ -15,8 +13,6 @@ services:
|
||||||
- NEXT_PUBLIC_DIRECTUS_URL=https://katheryn-cms.jeffemmett.com
|
- NEXT_PUBLIC_DIRECTUS_URL=https://katheryn-cms.jeffemmett.com
|
||||||
- DIRECTUS_API_TOKEN=katheryn-frontend-readonly-8591de0316ded82fab45328cf1e49cb1
|
- DIRECTUS_API_TOKEN=katheryn-frontend-readonly-8591de0316ded82fab45328cf1e49cb1
|
||||||
- DIRECTUS_INTERNAL_URL=http://katheryn-cms:8055
|
- 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:
|
labels:
|
||||||
- "traefik.enable=true"
|
- "traefik.enable=true"
|
||||||
- "traefik.http.routers.katheryn-staging.rule=Host(`katheryn-staging.jeffemmett.com`)"
|
- "traefik.http.routers.katheryn-staging.rule=Host(`katheryn-staging.jeffemmett.com`)"
|
||||||
|
|
|
||||||
|
|
@ -15,12 +15,6 @@ const nextConfig: NextConfig = {
|
||||||
port: '8055',
|
port: '8055',
|
||||||
pathname: '/assets/**',
|
pathname: '/assets/**',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
protocol: 'http',
|
|
||||||
hostname: 'katheryn-cms',
|
|
||||||
port: '8055',
|
|
||||||
pathname: '/assets/**',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
protocol: 'https',
|
protocol: 'https',
|
||||||
hostname: 'images.squarespace-cdn.com',
|
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 directusUrl = process.env.DIRECTUS_INTERNAL_URL || publicUrl;
|
||||||
const apiToken = process.env.DIRECTUS_API_TOKEN || '';
|
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
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
export const directus = createDirectus<any>(directusUrl)
|
export const directus = createDirectus<any>(directusUrl)
|
||||||
.with(staticToken(apiToken))
|
.with(staticToken(apiToken))
|
||||||
.with(rest());
|
.with(rest());
|
||||||
|
|
||||||
// Helper to get asset URL (includes auth token since Directus assets require authentication)
|
// Helper to get asset URL via local API proxy (/api/assets/[id])
|
||||||
// Uses internal Docker URL when available to bypass Cloudflare Access
|
// 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 {
|
export function getAssetUrl(fileId: string | DirectusFile | undefined, options?: { width?: number; height?: number; quality?: number; format?: 'webp' | 'jpg' | 'png' }): string {
|
||||||
if (!fileId) return '/placeholder.jpg';
|
if (!fileId) return '/placeholder.jpg';
|
||||||
|
|
||||||
const id = typeof fileId === 'string' ? fileId : fileId.id;
|
const id = typeof fileId === 'string' ? fileId : fileId.id;
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
|
|
||||||
// Auth token required for asset access
|
|
||||||
if (assetToken) params.append('access_token', assetToken);
|
|
||||||
|
|
||||||
if (options) {
|
if (options) {
|
||||||
if (options.width) params.append('width', options.width.toString());
|
if (options.width) params.append('width', options.width.toString());
|
||||||
if (options.height) params.append('height', options.height.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);
|
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
|
// Map Directus artwork fields to frontend Artwork interface
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue