Merge remote-tracking branch 'origin/main'
Some checks failed
Workflow / ʦ TypeScript (push) Failing after 4m51s
Workflow / ⚫️ Test (push) Has been skipped

This commit is contained in:
T. Zehetbauer
2026-04-01 14:05:13 +02:00
7 changed files with 64 additions and 29 deletions

View File

@@ -5,7 +5,7 @@ WORKDIR /app
# --- Install + Build in one stage --- # --- Install + Build in one stage ---
FROM base AS builder FROM base AS builder
# CACHE_BUST: change this value to force a full rebuild (busts Docker layer cache) # 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}" RUN echo "Cache bust: ${CACHE_BUST}"
COPY . . COPY . .
RUN pnpm install --no-frozen-lockfile RUN pnpm install --no-frozen-lockfile

View File

@@ -26,20 +26,11 @@ export default async function MeetingsPage({ params }: PageProps) {
const api = createMeetingsApi(client); const api = createMeetingsApi(client);
let stats = { totalProtocols: 0, thisYearProtocols: 0, openTasks: 0, overdueTasks: 0 }; const [stats, recentProtocols, overdueTasks] = await Promise.all([
let recentProtocols: Awaited<ReturnType<typeof api.getRecentProtocols>> = []; api.getDashboardStats(acct.id),
let overdueTasks: Awaited<ReturnType<typeof api.getOverdueTasks>> = []; api.getRecentProtocols(acct.id),
api.getOverdueTasks(acct.id),
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);
}
return ( return (
<CmsPageShell account={account} title="Sitzungsprotokolle"> <CmsPageShell account={account} title="Sitzungsprotokolle">

View File

@@ -25,14 +25,7 @@ export default async function VerbandPage({ params }: PageProps) {
if (!acct) return <AccountNotFound />; if (!acct) return <AccountNotFound />;
const api = createVerbandApi(client); const api = createVerbandApi(client);
const stats = await api.getDashboardStats(acct.id);
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);
}
return ( return (
<CmsPageShell account={account} title="Verbandsverwaltung"> <CmsPageShell account={account} title="Verbandsverwaltung">

View File

@@ -339,10 +339,12 @@ services:
environment: environment:
NODE_ENV: production NODE_ENV: production
NEXT_PUBLIC_SITE_URL: ${SITE_URL:-http://localhost:3000} 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_URL: ${API_EXTERNAL_URL:-http://localhost:8000}
NEXT_PUBLIC_SUPABASE_PUBLIC_KEY: ${SUPABASE_ANON_KEY} NEXT_PUBLIC_SUPABASE_PUBLIC_KEY: ${SUPABASE_ANON_KEY}
NEXT_PUBLIC_DEFAULT_LOCALE: de 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_SECRET_KEY: ${SUPABASE_SERVICE_ROLE_KEY}
SUPABASE_DB_WEBHOOK_SECRET: ${DB_WEBHOOK_SECRET:-webhooksecret} SUPABASE_DB_WEBHOOK_SECRET: ${DB_WEBHOOK_SECRET:-webhooksecret}
EMAIL_SENDER: ${EMAIL_SENDER:-noreply@myeasycms.de} EMAIL_SENDER: ${EMAIL_SENDER:-noreply@myeasycms.de}

View File

@@ -9,8 +9,8 @@ import { getSupabaseClientKeys } from '../get-supabase-client-keys';
/** /**
* Creates a middleware client for Supabase. * Creates a middleware client for Supabase.
* *
* @param {NextRequest} request - The Next.js request object. * Uses SUPABASE_INTERNAL_URL when available for reliable Docker networking,
* @param {NextResponse} response - The Next.js response object. * with cookieOptions.name matching the external URL's cookie key.
*/ */
export function createMiddlewareClient<GenericSchema = Database>( export function createMiddlewareClient<GenericSchema = Database>(
request: NextRequest, request: NextRequest,
@@ -18,7 +18,15 @@ export function createMiddlewareClient<GenericSchema = Database>(
) { ) {
const keys = getSupabaseClientKeys(); const keys = getSupabaseClientKeys();
return createServerClient<GenericSchema>(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<GenericSchema>(url, keys.publicKey, {
...(cookieOptions ? { cookieOptions } : {}),
cookies: { cookies: {
getAll() { getAll() {
return request.cookies.getAll(); return request.cookies.getAll();
@@ -35,3 +43,13 @@ export function createMiddlewareClient<GenericSchema = Database>(
}, },
}); });
} }
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';
}
}

View File

@@ -15,7 +15,8 @@ import { getSupabaseClientKeys } from '../get-supabase-client-keys';
export function getSupabaseServerAdminClient<GenericSchema = Database>() { export function getSupabaseServerAdminClient<GenericSchema = Database>() {
warnServiceRoleKeyUsage(); warnServiceRoleKeyUsage();
const url = getSupabaseClientKeys().url; const keys = getSupabaseClientKeys();
const url = process.env.SUPABASE_INTERNAL_URL || keys.url;
const secretKey = getSupabaseSecretKey(); const secretKey = getSupabaseSecretKey();
return createClient<GenericSchema>(url, secretKey, { return createClient<GenericSchema>(url, secretKey, {

View File

@@ -9,11 +9,27 @@ import { getSupabaseClientKeys } from '../get-supabase-client-keys';
/** /**
* @name getSupabaseServerClient * @name getSupabaseServerClient
* @description Creates a Supabase client for use in the Server. * @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<GenericSchema = Database>() { export function getSupabaseServerClient<GenericSchema = Database>() {
const keys = getSupabaseClientKeys(); const keys = getSupabaseClientKeys();
return createServerClient<GenericSchema>(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<GenericSchema>(url, keys.publicKey, {
...(cookieOptions ? { cookieOptions } : {}),
cookies: { cookies: {
async getAll() { async getAll() {
const cookieStore = await cookies(); const cookieStore = await cookies();
@@ -36,3 +52,17 @@ export function getSupabaseServerClient<GenericSchema = Database>() {
}, },
}); });
} }
/**
* 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';
}
}