import { NextRequest, NextResponse } from 'next/server'; import { logger } from '@/lib/logger'; import { configManager } from '@/lib/admin/config-manager'; import { parseJmapServers, redactJmapServers } from '@/lib/admin/jmap-servers'; import { hasSessionSecret } from '@/lib/auth/session-secret'; import { getOauthScopes } from '@/lib/oauth/tokens'; import { matchDomainBranding, parseDomainBranding, pickRequestHost, type BrandingOverrideKey, } from '@/lib/admin/domain-branding'; /** * Runtime configuration endpoint * * This endpoint serves configuration values that can be set at runtime * via environment variables or admin dashboard overrides, enabling * post-build configuration for Docker deployments. * * Priority order: * 1. Per-domain branding override (admin-configured, matched on request host) * 2. Admin dashboard overrides (data/admin/config.json) * 3. Runtime env vars (APP_NAME, JMAP_SERVER_URL) * 4. Build-time env vars (NEXT_PUBLIC_APP_NAME, NEXT_PUBLIC_JMAP_SERVER_URL) * 5. Default values */ export async function GET(request: NextRequest) { logger.debug('Config requested'); await configManager.ensureLoaded(); const host = pickRequestHost(request); const domainOverrides = matchDomainBranding( host, parseDomainBranding(configManager.get('domainBranding', [])), ); // Per-domain override wins over the global value, but only when the // entry explicitly sets that key. Otherwise we fall through to the // global admin/env/default chain. const branded = (key: BrandingOverrideKey, fallback: T): T => { const override = domainOverrides[key]; if (typeof override === 'string' && override.length > 0) return override as T; return configManager.get(key, fallback); }; const appName = branded('appName', '') || process.env.NEXT_PUBLIC_APP_NAME || 'Webmail'; const jmapServerUrl = configManager.get('jmapServerUrl') || process.env.NEXT_PUBLIC_JMAP_SERVER_URL || ''; const oauthEnabled = configManager.get('oauthEnabled', false); const oauthOnly = oauthEnabled && configManager.get('oauthOnly', false); const stalwartFeaturesEnabled = configManager.get('stalwartFeaturesEnabled', true); const allowedFrameAncestors = configManager.get('allowedFrameAncestors', ''); return NextResponse.json( { appName, jmapServerUrl, oauthEnabled, oauthOnly, oauthClientId: configManager.get('oauthClientId', ''), oauthIssuerUrl: configManager.get('oauthIssuerUrl', ''), oauthScopes: getOauthScopes(), rememberMeEnabled: hasSessionSecret(), settingsSyncEnabled: configManager.get('settingsSyncEnabled', false) && hasSessionSecret(), stalwartFeaturesEnabled, devMode: configManager.get('devMode', false), faviconUrl: branded('faviconUrl', '/branding/Bulwark_Favicon.svg'), appLogoLightUrl: branded('appLogoLightUrl', ''), appLogoDarkUrl: branded('appLogoDarkUrl', ''), loginLogoLightUrl: branded('loginLogoLightUrl', '/branding/Bulwark_Logo_Color.svg'), loginLogoDarkUrl: branded('loginLogoDarkUrl', '/branding/Bulwark_Logo_White.svg'), loginCompanyName: branded('loginCompanyName', ''), loginImprintUrl: branded('loginImprintUrl', ''), loginPrivacyPolicyUrl: branded('loginPrivacyPolicyUrl', ''), loginWebsiteUrl: branded('loginWebsiteUrl', ''), loginShowTotp: configManager.get('loginShowTotp', true), loginShowVersion: configManager.get('loginShowVersion', true), demoMode: configManager.get('demoMode', false), allowCustomJmapEndpoint: configManager.get('allowCustomJmapEndpoint', false), jmapServers: redactJmapServers(parseJmapServers(configManager.get('jmapServers', []))), jmapServerAutoPickByDomain: configManager.get('jmapServerAutoPickByDomain', false), autoSsoEnabled: configManager.get('autoSsoEnabled', false), embeddedMode: !!allowedFrameAncestors && allowedFrameAncestors !== "'none'", parentOrigin: configManager.get('parentOrigin', ''), }, { // Branding varies by host, so any cache between us and the browser // must key its entry by the host headers we consulted. headers: { Vary: 'Host, X-Forwarded-Host' }, }, ); }