fix: Route all banner API calls through Next.js proxy (SSL cert fix)
Build + Deploy / build-dsms-node (push) Successful in 7s
CI / branch-name (push) Has been skipped
CI / guardrail-integrity (push) Has been skipped
CI / loc-budget (push) Failing after 14s
CI / secret-scan (push) Has been skipped
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / nodejs-build (push) Successful in 2m30s
CI / dep-audit (push) Has been skipped
CI / sbom-scan (push) Has been skipped
CI / test-go (push) Successful in 37s
Build + Deploy / build-admin-compliance (push) Successful in 2m6s
Build + Deploy / build-backend-compliance (push) Successful in 2m58s
Build + Deploy / build-ai-sdk (push) Successful in 8s
Build + Deploy / build-developer-portal (push) Successful in 7s
Build + Deploy / build-tts (push) Successful in 7s
Build + Deploy / build-document-crawler (push) Successful in 7s
Build + Deploy / build-dsms-gateway (push) Successful in 7s
Build + Deploy / trigger-orca (push) Successful in 2m11s
CI / test-python-backend (push) Successful in 36s
CI / test-python-document-crawler (push) Successful in 26s
CI / test-python-dsms-gateway (push) Successful in 23s
CI / validate-canonical-controls (push) Successful in 14s

Browser blocks direct calls to backend-compliance:8093 due to
self-signed SSL certificate. All banner API calls now go through
Next.js API proxy at /api/sdk/v1/banner/* which runs server-side.

- New catch-all proxy: /api/sdk/v1/banner/[[...path]]/route.ts
  Maps to backend-compliance:8002/api/compliance/banner/*
- Preview page: uses /api/sdk/v1/banner/ instead of https://macmini:8093
- CMP Dashboard: uses proxy for banner stats + compliance proxy for DSR/einwilligungen
- Fixes: banner not closeable due to API errors, consent not saving

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-05-03 08:53:20 +02:00
parent bb2ebd03cd
commit e8b5c90a49
3 changed files with 87 additions and 14 deletions
@@ -0,0 +1,74 @@
/**
* Banner API Proxy — catch-all route for cookie banner endpoints.
*
* Maps: /api/sdk/v1/banner/<path> → backend-compliance:8002/api/compliance/banner/<path>
*
* Solves: Browser cannot call backend-compliance:8093 directly due to
* self-signed SSL certificates. This proxy runs server-side where
* certificate validation is not an issue.
*/
import { NextRequest, NextResponse } from 'next/server'
const BACKEND_URL = process.env.BACKEND_URL || 'http://backend-compliance:8002'
const DEFAULT_TENANT = process.env.DEFAULT_TENANT_ID || '9282a473-5c95-4b3a-bf78-0ecc0ec71d3e'
async function proxyRequest(
request: NextRequest,
pathSegments: string[] | undefined,
method: string,
) {
const pathStr = pathSegments?.join('/') || ''
const qs = request.nextUrl.searchParams.toString()
const base = `${BACKEND_URL}/api/compliance/banner`
const url = pathStr
? `${base}/${pathStr}${qs ? `?${qs}` : ''}`
: `${base}${qs ? `?${qs}` : ''}`
try {
const headers: HeadersInit = {
'X-Tenant-ID': request.headers.get('x-tenant-id') || DEFAULT_TENANT,
}
const ct = request.headers.get('Content-Type')
if (ct) headers['Content-Type'] = ct
const opts: RequestInit = { method, headers, signal: AbortSignal.timeout(30000) }
if (method === 'POST' || method === 'PUT') {
const body = await request.text()
if (body) opts.body = body
}
const res = await fetch(url, opts)
const text = await res.text()
let data
try { data = JSON.parse(text) } catch { data = { raw: text } }
if (!res.ok) {
return NextResponse.json(
{ error: `Backend ${res.status}`, ...data },
{ status: res.status },
)
}
return NextResponse.json(data)
} catch (err: any) {
console.error('Banner proxy error:', err?.message)
return NextResponse.json(
{ error: 'Backend nicht erreichbar' },
{ status: 503 },
)
}
}
export async function GET(req: NextRequest, { params }: { params: Promise<{ path?: string[] }> }) {
return proxyRequest(req, (await params).path, 'GET')
}
export async function POST(req: NextRequest, { params }: { params: Promise<{ path?: string[] }> }) {
return proxyRequest(req, (await params).path, 'POST')
}
export async function PUT(req: NextRequest, { params }: { params: Promise<{ path?: string[] }> }) {
return proxyRequest(req, (await params).path, 'PUT')
}
export async function DELETE(req: NextRequest, { params }: { params: Promise<{ path?: string[] }> }) {
return proxyRequest(req, (await params).path, 'DELETE')
}
+9 -9
View File
@@ -11,11 +11,10 @@ import Link from 'next/link'
* but with EWR-Only as unique differentiator. * but with EWR-Only as unique differentiator.
*/ */
const API_BASE = typeof window !== 'undefined' // Use Next.js API proxy to avoid SSL cert issues
? (process.env.NEXT_PUBLIC_SDK_URL || `${window.location.protocol}//${window.location.hostname}:8093`) const BANNER_API = '/api/sdk/v1/banner'
: ''
const TENANT_ID = '9282a473-5c95-4b3a-bf78-0ecc0ec71d3e' const TENANT_ID = '9282a473-5c95-4b3a-bf78-0ecc0ec71d3e'
const HEADERS = { 'X-Tenant-ID': TENANT_ID } const HEADERS = { 'x-tenant-id': TENANT_ID }
interface BannerStats { total_consents: number; category_acceptance: Record<string, { count: number; rate: number }> } interface BannerStats { total_consents: number; category_acceptance: Record<string, { count: number; rate: number }> }
interface ConsentStats { total_consents: number; active_consents: number; revoked_consents: number; unique_users: number; conversion_rate: number } interface ConsentStats { total_consents: number; active_consents: number; revoked_consents: number; unique_users: number; conversion_rate: number }
@@ -59,12 +58,13 @@ export default function CMPDashboardPage() {
useEffect(() => { useEffect(() => {
async function load() { async function load() {
const f = (url: string) => fetch(`${API_BASE}${url}`, { headers: HEADERS }).then(r => r.ok ? r.json() : null).catch(() => null) const fb = (path: string) => fetch(`${BANNER_API}/${path}`, { headers: HEADERS }).then(r => r.ok ? r.json() : null).catch(() => null)
const fa = (path: string) => fetch(`/api/sdk/v1/compliance/${path}`, { headers: HEADERS }).then(r => r.ok ? r.json() : null).catch(() => null)
const [banner, consent, dsr, siteList] = await Promise.all([ const [banner, consent, dsr, siteList] = await Promise.all([
f('/banner/admin/stats/preview-test-site'), fb('admin/stats/preview-test-site'),
f('/einwilligungen/consents/stats'), fa('einwilligungen/consents/stats'),
f('/dsr/stats'), fa('dsr/stats'),
f('/banner/admin/sites'), fb('admin/sites'),
]) ])
setBannerStats(banner) setBannerStats(banner)
setConsentStats(consent) setConsentStats(consent)
@@ -17,9 +17,8 @@ import {
* This page runs OUTSIDE the SDK layout to simulate a real website experience. * This page runs OUTSIDE the SDK layout to simulate a real website experience.
*/ */
const API_BASE = typeof window !== 'undefined' // Use Next.js API proxy to avoid SSL cert issues with direct backend calls
? (process.env.NEXT_PUBLIC_SDK_URL || `${window.location.protocol}//${window.location.hostname}:8093`) const API_BASE = '/api/sdk/v1/banner'
: ''
const SITE_ID = 'preview-test-site' const SITE_ID = 'preview-test-site'
const TENANT_ID = '9282a473-5c95-4b3a-bf78-0ecc0ec71d3e' const TENANT_ID = '9282a473-5c95-4b3a-bf78-0ecc0ec71d3e'
@@ -63,7 +62,7 @@ export default function CookieBannerPreviewPage() {
try { try {
const res = await fetch( const res = await fetch(
`${API_BASE}/banner/consent?site_id=${SITE_ID}&device_fingerprint=${fingerprint}`, `${API_BASE}/banner/consent?site_id=${SITE_ID}&device_fingerprint=${fingerprint}`,
{ headers: { 'X-Tenant-ID': TENANT_ID } }, { headers: { 'x-tenant-id': TENANT_ID } },
) )
if (res.ok) { if (res.ok) {
const data = await res.json() const data = await res.json()
@@ -91,7 +90,7 @@ export default function CookieBannerPreviewPage() {
try { try {
const res = await fetch(`${API_BASE}/banner/consent`, { const res = await fetch(`${API_BASE}/banner/consent`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-Tenant-ID': TENANT_ID }, headers: { 'Content-Type': 'application/json', 'x-tenant-id': TENANT_ID },
body: JSON.stringify({ body: JSON.stringify({
site_id: SITE_ID, site_id: SITE_ID,
device_fingerprint: fingerprint, device_fingerprint: fingerprint,