Both sides added adjacent LOGIN_* config entries (upstream: loginShowHeading/loginShowSubtitle/logo sizing; this branch: loginShowTotp/loginShowVersion) — resolution keeps both. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LrR2CVfvcPWxr9ub299VwW
98 lines
4.7 KiB
TypeScript
98 lines
4.7 KiB
TypeScript
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<unknown>('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 = <T,>(key: BrandingOverrideKey, fallback: T): T => {
|
|
const override = domainOverrides[key];
|
|
if (typeof override === 'string' && override.length > 0) return override as T;
|
|
return configManager.get<T>(key, fallback);
|
|
};
|
|
|
|
const appName =
|
|
branded<string>('appName', '') || process.env.NEXT_PUBLIC_APP_NAME || 'Webmail';
|
|
const jmapServerUrl = configManager.get<string>('jmapServerUrl') || process.env.NEXT_PUBLIC_JMAP_SERVER_URL || '';
|
|
const oauthEnabled = configManager.get<boolean>('oauthEnabled', false);
|
|
const oauthOnly = oauthEnabled && configManager.get<boolean>('oauthOnly', false);
|
|
const stalwartFeaturesEnabled = configManager.get<boolean>('stalwartFeaturesEnabled', true);
|
|
const allowedFrameAncestors = configManager.get<string>('allowedFrameAncestors', '');
|
|
|
|
return NextResponse.json(
|
|
{
|
|
appName,
|
|
jmapServerUrl,
|
|
oauthEnabled,
|
|
oauthOnly,
|
|
oauthClientId: configManager.get<string>('oauthClientId', ''),
|
|
oauthIssuerUrl: configManager.get<string>('oauthIssuerUrl', ''),
|
|
oauthScopes: getOauthScopes(),
|
|
rememberMeEnabled: hasSessionSecret(),
|
|
settingsSyncEnabled: configManager.get<boolean>('settingsSyncEnabled', false) && hasSessionSecret(),
|
|
stalwartFeaturesEnabled,
|
|
devMode: configManager.get<boolean>('devMode', false),
|
|
faviconUrl: branded<string>('faviconUrl', '/branding/Bulwark_Favicon.svg'),
|
|
appLogoLightUrl: branded<string>('appLogoLightUrl', ''),
|
|
appLogoDarkUrl: branded<string>('appLogoDarkUrl', ''),
|
|
loginLogoLightUrl: branded<string>('loginLogoLightUrl', '/branding/Bulwark_Logo_Color.svg'),
|
|
loginLogoDarkUrl: branded<string>('loginLogoDarkUrl', '/branding/Bulwark_Logo_White.svg'),
|
|
loginCompanyName: branded<string>('loginCompanyName', ''),
|
|
loginImprintUrl: branded<string>('loginImprintUrl', ''),
|
|
loginPrivacyPolicyUrl: branded<string>('loginPrivacyPolicyUrl', ''),
|
|
loginWebsiteUrl: branded<string>('loginWebsiteUrl', ''),
|
|
loginLogoMaxHeight: configManager.get<string>('loginLogoMaxHeight', ''),
|
|
loginLogoMaxWidth: configManager.get<string>('loginLogoMaxWidth', ''),
|
|
loginShowHeading: configManager.get<boolean>('loginShowHeading', true),
|
|
loginShowSubtitle: configManager.get<boolean>('loginShowSubtitle', true),
|
|
loginShowTotp: configManager.get<boolean>('loginShowTotp', true),
|
|
loginShowVersion: configManager.get<boolean>('loginShowVersion', true),
|
|
demoMode: configManager.get<boolean>('demoMode', false),
|
|
allowCustomJmapEndpoint: configManager.get<boolean>('allowCustomJmapEndpoint', false),
|
|
jmapServers: redactJmapServers(parseJmapServers(configManager.get<unknown>('jmapServers', []))),
|
|
jmapServerAutoPickByDomain: configManager.get<boolean>('jmapServerAutoPickByDomain', false),
|
|
autoSsoEnabled: configManager.get<boolean>('autoSsoEnabled', false),
|
|
embeddedMode: !!allowedFrameAncestors && allowedFrameAncestors !== "'none'",
|
|
parentOrigin: configManager.get<string>('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' },
|
|
},
|
|
);
|
|
}
|