diff --git a/Dockerfile b/Dockerfile index 421a4a52b..d20cf643f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,7 +5,7 @@ WORKDIR /app # --- Install + Build in one stage --- FROM base AS builder # CACHE_BUST: change this value to force a full rebuild (busts Docker layer cache) -ARG CACHE_BUST=13 +ARG CACHE_BUST=14 RUN echo "Cache bust: ${CACHE_BUST}" COPY . . RUN pnpm install --no-frozen-lockfile diff --git a/apps/web/app/[locale]/home/[account]/meetings/page.tsx b/apps/web/app/[locale]/home/[account]/meetings/page.tsx index 08a9cb723..90b4a24bc 100644 --- a/apps/web/app/[locale]/home/[account]/meetings/page.tsx +++ b/apps/web/app/[locale]/home/[account]/meetings/page.tsx @@ -26,20 +26,11 @@ export default async function MeetingsPage({ params }: PageProps) { const api = createMeetingsApi(client); - let stats = { totalProtocols: 0, thisYearProtocols: 0, openTasks: 0, overdueTasks: 0 }; - let recentProtocols: Awaited> = []; - let overdueTasks: Awaited> = []; - - try { - [stats, recentProtocols, overdueTasks] = await Promise.all([ - api.getDashboardStats(acct.id), - api.getRecentProtocols(acct.id), - api.getOverdueTasks(acct.id), - ]); - } catch (e) { - // Supabase query failed — render with empty data instead of crashing - console.error('Failed to load meetings dashboard:', e); - } + const [stats, recentProtocols, overdueTasks] = await Promise.all([ + api.getDashboardStats(acct.id), + api.getRecentProtocols(acct.id), + api.getOverdueTasks(acct.id), + ]); return ( diff --git a/apps/web/app/[locale]/home/[account]/verband/page.tsx b/apps/web/app/[locale]/home/[account]/verband/page.tsx index 610cfefe1..384ac6661 100644 --- a/apps/web/app/[locale]/home/[account]/verband/page.tsx +++ b/apps/web/app/[locale]/home/[account]/verband/page.tsx @@ -25,14 +25,7 @@ export default async function VerbandPage({ params }: PageProps) { if (!acct) return ; const api = createVerbandApi(client); - - let stats = { totalClubs: 0, totalMembers: 0, totalRoles: 0, totalFeeTypes: 0 }; - - try { - stats = await api.getDashboardStats(acct.id); - } catch (e) { - console.error('Failed to load verband dashboard:', e); - } + const stats = await api.getDashboardStats(acct.id); return ( diff --git a/docker-compose.yml b/docker-compose.yml index a00901a62..19ef51ee3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -339,10 +339,12 @@ services: environment: NODE_ENV: production NEXT_PUBLIC_SITE_URL: ${SITE_URL:-http://localhost:3000} - # Same URL for browser AND server — keeps Supabase cookie names consistent + # Same URL for browser AND server cookie names NEXT_PUBLIC_SUPABASE_URL: ${API_EXTERNAL_URL:-http://localhost:8000} NEXT_PUBLIC_SUPABASE_PUBLIC_KEY: ${SUPABASE_ANON_KEY} NEXT_PUBLIC_DEFAULT_LOCALE: de + # Internal URL for server-side Supabase calls (Docker network, no hairpin NAT) + SUPABASE_INTERNAL_URL: http://supabase-kong:8000 SUPABASE_SECRET_KEY: ${SUPABASE_SERVICE_ROLE_KEY} SUPABASE_DB_WEBHOOK_SECRET: ${DB_WEBHOOK_SECRET:-webhooksecret} EMAIL_SENDER: ${EMAIL_SENDER:-noreply@myeasycms.de} diff --git a/packages/supabase/src/clients/middleware-client.ts b/packages/supabase/src/clients/middleware-client.ts index 23d3e3bdd..d9f784486 100644 --- a/packages/supabase/src/clients/middleware-client.ts +++ b/packages/supabase/src/clients/middleware-client.ts @@ -9,8 +9,8 @@ import { getSupabaseClientKeys } from '../get-supabase-client-keys'; /** * Creates a middleware client for Supabase. * - * @param {NextRequest} request - The Next.js request object. - * @param {NextResponse} response - The Next.js response object. + * Uses SUPABASE_INTERNAL_URL when available for reliable Docker networking, + * with cookieOptions.name matching the external URL's cookie key. */ export function createMiddlewareClient( request: NextRequest, @@ -18,7 +18,15 @@ export function createMiddlewareClient( ) { const keys = getSupabaseClientKeys(); - return createServerClient(keys.url, keys.publicKey, { + const internalUrl = process.env.SUPABASE_INTERNAL_URL; + const url = internalUrl || keys.url; + + const cookieOptions = internalUrl + ? { name: deriveCookieName(keys.url) } + : undefined; + + return createServerClient(url, keys.publicKey, { + ...(cookieOptions ? { cookieOptions } : {}), cookies: { getAll() { return request.cookies.getAll(); @@ -35,3 +43,13 @@ export function createMiddlewareClient( }, }); } + +function deriveCookieName(supabaseUrl: string): string { + try { + const hostname = new URL(supabaseUrl).hostname; + const ref = hostname.split('.')[0]!; + return `sb-${ref}-auth-token`; + } catch { + return 'sb-localhost-auth-token'; + } +} diff --git a/packages/supabase/src/clients/server-admin-client.ts b/packages/supabase/src/clients/server-admin-client.ts index 1cb82cd04..3d002ff00 100644 --- a/packages/supabase/src/clients/server-admin-client.ts +++ b/packages/supabase/src/clients/server-admin-client.ts @@ -15,7 +15,8 @@ import { getSupabaseClientKeys } from '../get-supabase-client-keys'; export function getSupabaseServerAdminClient() { warnServiceRoleKeyUsage(); - const url = getSupabaseClientKeys().url; + const keys = getSupabaseClientKeys(); + const url = process.env.SUPABASE_INTERNAL_URL || keys.url; const secretKey = getSupabaseSecretKey(); return createClient(url, secretKey, { diff --git a/packages/supabase/src/clients/server-client.ts b/packages/supabase/src/clients/server-client.ts index 8ce1b6a74..c2f87e182 100644 --- a/packages/supabase/src/clients/server-client.ts +++ b/packages/supabase/src/clients/server-client.ts @@ -9,11 +9,27 @@ import { getSupabaseClientKeys } from '../get-supabase-client-keys'; /** * @name getSupabaseServerClient * @description Creates a Supabase client for use in the Server. + * + * In Docker deployments, the server can optionally use SUPABASE_INTERNAL_URL + * (e.g. http://supabase-kong:8000) for faster, more reliable connections. + * The cookieOptions.name is set to match the external URL's cookie name + * so session cookies from the browser are correctly read. */ export function getSupabaseServerClient() { const keys = getSupabaseClientKeys(); - return createServerClient(keys.url, keys.publicKey, { + // Use internal URL for server-side requests if available + const internalUrl = process.env.SUPABASE_INTERNAL_URL; + const url = internalUrl || keys.url; + + // When using an internal URL, we must set the cookie name to match + // the external URL's cookie key so the server can read browser cookies. + const cookieOptions = internalUrl + ? { name: deriveCookieName(keys.url) } + : undefined; + + return createServerClient(url, keys.publicKey, { + ...(cookieOptions ? { cookieOptions } : {}), cookies: { async getAll() { const cookieStore = await cookies(); @@ -36,3 +52,17 @@ export function getSupabaseServerClient() { }, }); } + +/** + * Derives the Supabase cookie name from a URL, matching @supabase/ssr behavior. + * e.g. "https://myeasycms.frontieralgorithmics.de" → "sb-myeasycms-auth-token" + */ +function deriveCookieName(supabaseUrl: string): string { + try { + const hostname = new URL(supabaseUrl).hostname; + const ref = hostname.split('.')[0]!; + return `sb-${ref}-auth-token`; + } catch { + return 'sb-localhost-auth-token'; + } +}