Two opt-out branding/login flags, both default true (no behaviour change for existing deployments): - LOGIN_SHOW_TOTP=false hides the manual "I have a 2FA code" toggle on the login form. Deployments that delegate auth to an external directory (LDAP/OIDC) where 2FA lives in the IdP have no server-side TOTP, so the toggle only ever leads to a failed login. Server-required TOTP (totp_required, which auto-shows the field) is unaffected. - LOGIN_SHOW_VERSION=false hides the build version in the login footer, so the exact version isn't disclosed to unauthenticated visitors. Wired through the existing config registry (CONFIG_ENV_MAP) → /api/config → useConfig, matching the surrounding LOGIN_* options. Refs #519. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
94 lines
4.4 KiB
TypeScript
94 lines
4.4 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', ''),
|
|
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' },
|
|
},
|
|
);
|
|
}
|