From 65cb6be8a9e39157981404b24b3defc04f68a39b Mon Sep 17 00:00:00 2001 From: Maarten Draijer Date: Mon, 29 Jun 2026 12:04:10 +0000 Subject: [PATCH] feat(login): add LOGIN_SHOW_TOTP and LOGIN_SHOW_VERSION config flags MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- app/(main)/[locale]/login/page.tsx | 13 +++++++++---- app/api/config/route.ts | 2 ++ hooks/use-config.ts | 8 ++++++++ lib/admin/types.ts | 8 ++++++++ 4 files changed, 27 insertions(+), 4 deletions(-) diff --git a/app/(main)/[locale]/login/page.tsx b/app/(main)/[locale]/login/page.tsx index 6cdc3838..645250c3 100644 --- a/app/(main)/[locale]/login/page.tsx +++ b/app/(main)/[locale]/login/page.tsx @@ -133,7 +133,7 @@ export default function LoginPage() { const isMobileHandoff = Boolean(mobileRedirectUri); const { login, loginDemo, isLoading, error, clearError, isAuthenticated } = useAuthStore(); const { theme, setTheme, initializeTheme } = useThemeStore(useShallow((s) => ({ theme: s.theme, setTheme: s.setTheme, initializeTheme: s.initializeTheme }))); - const { appName, jmapServerUrl: configuredServerUrl, oauthEnabled, oauthOnly, oauthClientId: globalOauthClientId, oauthIssuerUrl: globalOauthIssuerUrl, oauthScopes, rememberMeEnabled, devMode, demoMode, loginLogoLightUrl, loginLogoDarkUrl, loginCompanyName, loginImprintUrl, loginPrivacyPolicyUrl, loginWebsiteUrl, isLoading: configLoading, error: configError, autoSsoEnabled, embeddedMode: _embeddedMode, allowCustomJmapEndpoint, jmapServers, jmapServerAutoPickByDomain } = useConfig(); + const { appName, jmapServerUrl: configuredServerUrl, oauthEnabled, oauthOnly, oauthClientId: globalOauthClientId, oauthIssuerUrl: globalOauthIssuerUrl, oauthScopes, rememberMeEnabled, devMode, demoMode, loginLogoLightUrl, loginLogoDarkUrl, loginCompanyName, loginImprintUrl, loginPrivacyPolicyUrl, loginWebsiteUrl, loginShowTotp, loginShowVersion, isLoading: configLoading, error: configError, autoSsoEnabled, embeddedMode: _embeddedMode, allowCustomJmapEndpoint, jmapServers, jmapServerAutoPickByDomain } = useConfig(); const resolvedTheme = useThemeStore((s) => s.resolvedTheme); const [formData, setFormData] = useState({ @@ -815,7 +815,7 @@ export default function LoginPage() { )} )} - + {loginShowVersion && } @@ -1142,8 +1142,12 @@ export default function LoginPage() { - {/* 2FA toggle / field */} + {/* 2FA toggle / field. The manual toggle can be hidden via + LOGIN_SHOW_TOTP (loginShowTotp) for deployments whose mail + server has no per-account TOTP (auth delegated to an + external directory); server-required TOTP still shows. */} {!showTotpField ? ( + loginShowTotp ? ( + ) : null ) : (
)} - + {loginShowVersion && } diff --git a/app/api/config/route.ts b/app/api/config/route.ts index d0c79255..65d665c2 100644 --- a/app/api/config/route.ts +++ b/app/api/config/route.ts @@ -74,6 +74,8 @@ export async function GET(request: NextRequest) { 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', []))), diff --git a/hooks/use-config.ts b/hooks/use-config.ts index db30f363..1e143f10 100644 --- a/hooks/use-config.ts +++ b/hooks/use-config.ts @@ -26,6 +26,8 @@ interface ConfigData { loginImprintUrl: string; loginPrivacyPolicyUrl: string; loginWebsiteUrl: string; + loginShowTotp: boolean; + loginShowVersion: boolean; demoMode: boolean; autoSsoEnabled: boolean; allowCustomJmapEndpoint: boolean; @@ -105,6 +107,8 @@ export function useConfig(): AppConfig { loginImprintUrl: configCache?.loginImprintUrl || '', loginPrivacyPolicyUrl: configCache?.loginPrivacyPolicyUrl || '', loginWebsiteUrl: configCache?.loginWebsiteUrl || '', + loginShowTotp: configCache?.loginShowTotp ?? true, + loginShowVersion: configCache?.loginShowVersion ?? true, demoMode: configCache?.demoMode || false, autoSsoEnabled: configCache?.autoSsoEnabled || false, allowCustomJmapEndpoint: configCache?.allowCustomJmapEndpoint || false, @@ -140,6 +144,8 @@ export function useConfig(): AppConfig { loginImprintUrl: configCache.loginImprintUrl, loginPrivacyPolicyUrl: configCache.loginPrivacyPolicyUrl, loginWebsiteUrl: configCache.loginWebsiteUrl, + loginShowTotp: configCache.loginShowTotp, + loginShowVersion: configCache.loginShowVersion, demoMode: configCache.demoMode, autoSsoEnabled: configCache.autoSsoEnabled, allowCustomJmapEndpoint: configCache.allowCustomJmapEndpoint, @@ -176,6 +182,8 @@ export function useConfig(): AppConfig { loginImprintUrl: data.loginImprintUrl, loginPrivacyPolicyUrl: data.loginPrivacyPolicyUrl, loginWebsiteUrl: data.loginWebsiteUrl, + loginShowTotp: data.loginShowTotp, + loginShowVersion: data.loginShowVersion, demoMode: data.demoMode, autoSsoEnabled: data.autoSsoEnabled, allowCustomJmapEndpoint: data.allowCustomJmapEndpoint, diff --git a/lib/admin/types.ts b/lib/admin/types.ts index 1a415502..8782d0b1 100644 --- a/lib/admin/types.ts +++ b/lib/admin/types.ts @@ -166,6 +166,14 @@ export const CONFIG_ENV_MAP: Record