feat(login): add LOGIN_SHOW_TOTP and LOGIN_SHOW_VERSION config flags
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>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
2704d5dc23
commit
65cb6be8a9
@@ -133,7 +133,7 @@ export default function LoginPage() {
|
|||||||
const isMobileHandoff = Boolean(mobileRedirectUri);
|
const isMobileHandoff = Boolean(mobileRedirectUri);
|
||||||
const { login, loginDemo, isLoading, error, clearError, isAuthenticated } = useAuthStore();
|
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 { 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 resolvedTheme = useThemeStore((s) => s.resolvedTheme);
|
||||||
|
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
@@ -815,7 +815,7 @@ export default function LoginPage() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<VersionBadge />
|
{loginShowVersion && <VersionBadge />}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1142,8 +1142,12 @@ export default function LoginPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 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 ? (
|
{!showTotpField ? (
|
||||||
|
loginShowTotp ? (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -1155,6 +1159,7 @@ export default function LoginPage() {
|
|||||||
<Shield className="w-3.5 h-3.5" />
|
<Shield className="w-3.5 h-3.5" />
|
||||||
{t("totp_toggle")}
|
{t("totp_toggle")}
|
||||||
</button>
|
</button>
|
||||||
|
) : null
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<label htmlFor="totp" className="block text-sm font-medium text-foreground">
|
<label htmlFor="totp" className="block text-sm font-medium text-foreground">
|
||||||
@@ -1349,7 +1354,7 @@ export default function LoginPage() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<VersionBadge />
|
{loginShowVersion && <VersionBadge />}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -74,6 +74,8 @@ export async function GET(request: NextRequest) {
|
|||||||
loginImprintUrl: branded<string>('loginImprintUrl', ''),
|
loginImprintUrl: branded<string>('loginImprintUrl', ''),
|
||||||
loginPrivacyPolicyUrl: branded<string>('loginPrivacyPolicyUrl', ''),
|
loginPrivacyPolicyUrl: branded<string>('loginPrivacyPolicyUrl', ''),
|
||||||
loginWebsiteUrl: branded<string>('loginWebsiteUrl', ''),
|
loginWebsiteUrl: branded<string>('loginWebsiteUrl', ''),
|
||||||
|
loginShowTotp: configManager.get<boolean>('loginShowTotp', true),
|
||||||
|
loginShowVersion: configManager.get<boolean>('loginShowVersion', true),
|
||||||
demoMode: configManager.get<boolean>('demoMode', false),
|
demoMode: configManager.get<boolean>('demoMode', false),
|
||||||
allowCustomJmapEndpoint: configManager.get<boolean>('allowCustomJmapEndpoint', false),
|
allowCustomJmapEndpoint: configManager.get<boolean>('allowCustomJmapEndpoint', false),
|
||||||
jmapServers: redactJmapServers(parseJmapServers(configManager.get<unknown>('jmapServers', []))),
|
jmapServers: redactJmapServers(parseJmapServers(configManager.get<unknown>('jmapServers', []))),
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ interface ConfigData {
|
|||||||
loginImprintUrl: string;
|
loginImprintUrl: string;
|
||||||
loginPrivacyPolicyUrl: string;
|
loginPrivacyPolicyUrl: string;
|
||||||
loginWebsiteUrl: string;
|
loginWebsiteUrl: string;
|
||||||
|
loginShowTotp: boolean;
|
||||||
|
loginShowVersion: boolean;
|
||||||
demoMode: boolean;
|
demoMode: boolean;
|
||||||
autoSsoEnabled: boolean;
|
autoSsoEnabled: boolean;
|
||||||
allowCustomJmapEndpoint: boolean;
|
allowCustomJmapEndpoint: boolean;
|
||||||
@@ -105,6 +107,8 @@ export function useConfig(): AppConfig {
|
|||||||
loginImprintUrl: configCache?.loginImprintUrl || '',
|
loginImprintUrl: configCache?.loginImprintUrl || '',
|
||||||
loginPrivacyPolicyUrl: configCache?.loginPrivacyPolicyUrl || '',
|
loginPrivacyPolicyUrl: configCache?.loginPrivacyPolicyUrl || '',
|
||||||
loginWebsiteUrl: configCache?.loginWebsiteUrl || '',
|
loginWebsiteUrl: configCache?.loginWebsiteUrl || '',
|
||||||
|
loginShowTotp: configCache?.loginShowTotp ?? true,
|
||||||
|
loginShowVersion: configCache?.loginShowVersion ?? true,
|
||||||
demoMode: configCache?.demoMode || false,
|
demoMode: configCache?.demoMode || false,
|
||||||
autoSsoEnabled: configCache?.autoSsoEnabled || false,
|
autoSsoEnabled: configCache?.autoSsoEnabled || false,
|
||||||
allowCustomJmapEndpoint: configCache?.allowCustomJmapEndpoint || false,
|
allowCustomJmapEndpoint: configCache?.allowCustomJmapEndpoint || false,
|
||||||
@@ -140,6 +144,8 @@ export function useConfig(): AppConfig {
|
|||||||
loginImprintUrl: configCache.loginImprintUrl,
|
loginImprintUrl: configCache.loginImprintUrl,
|
||||||
loginPrivacyPolicyUrl: configCache.loginPrivacyPolicyUrl,
|
loginPrivacyPolicyUrl: configCache.loginPrivacyPolicyUrl,
|
||||||
loginWebsiteUrl: configCache.loginWebsiteUrl,
|
loginWebsiteUrl: configCache.loginWebsiteUrl,
|
||||||
|
loginShowTotp: configCache.loginShowTotp,
|
||||||
|
loginShowVersion: configCache.loginShowVersion,
|
||||||
demoMode: configCache.demoMode,
|
demoMode: configCache.demoMode,
|
||||||
autoSsoEnabled: configCache.autoSsoEnabled,
|
autoSsoEnabled: configCache.autoSsoEnabled,
|
||||||
allowCustomJmapEndpoint: configCache.allowCustomJmapEndpoint,
|
allowCustomJmapEndpoint: configCache.allowCustomJmapEndpoint,
|
||||||
@@ -176,6 +182,8 @@ export function useConfig(): AppConfig {
|
|||||||
loginImprintUrl: data.loginImprintUrl,
|
loginImprintUrl: data.loginImprintUrl,
|
||||||
loginPrivacyPolicyUrl: data.loginPrivacyPolicyUrl,
|
loginPrivacyPolicyUrl: data.loginPrivacyPolicyUrl,
|
||||||
loginWebsiteUrl: data.loginWebsiteUrl,
|
loginWebsiteUrl: data.loginWebsiteUrl,
|
||||||
|
loginShowTotp: data.loginShowTotp,
|
||||||
|
loginShowVersion: data.loginShowVersion,
|
||||||
demoMode: data.demoMode,
|
demoMode: data.demoMode,
|
||||||
autoSsoEnabled: data.autoSsoEnabled,
|
autoSsoEnabled: data.autoSsoEnabled,
|
||||||
allowCustomJmapEndpoint: data.allowCustomJmapEndpoint,
|
allowCustomJmapEndpoint: data.allowCustomJmapEndpoint,
|
||||||
|
|||||||
@@ -166,6 +166,14 @@ export const CONFIG_ENV_MAP: Record<string, { envVar: string; fileEnvVar?: strin
|
|||||||
loginImprintUrl: { envVar: 'LOGIN_IMPRINT_URL', type: 'url', defaultValue: '' },
|
loginImprintUrl: { envVar: 'LOGIN_IMPRINT_URL', type: 'url', defaultValue: '' },
|
||||||
loginPrivacyPolicyUrl: { envVar: 'LOGIN_PRIVACY_POLICY_URL', type: 'url', defaultValue: '' },
|
loginPrivacyPolicyUrl: { envVar: 'LOGIN_PRIVACY_POLICY_URL', type: 'url', defaultValue: '' },
|
||||||
loginWebsiteUrl: { envVar: 'LOGIN_WEBSITE_URL', type: 'url', defaultValue: '' },
|
loginWebsiteUrl: { envVar: 'LOGIN_WEBSITE_URL', type: 'url', defaultValue: '' },
|
||||||
|
// Hide 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 leads to a failed
|
||||||
|
// login. Server-required TOTP (totp_required) still shows regardless.
|
||||||
|
loginShowTotp: { envVar: 'LOGIN_SHOW_TOTP', type: 'boolean', defaultValue: true },
|
||||||
|
// Show the build version in the login footer. Off keeps the exact version
|
||||||
|
// from being disclosed to unauthenticated visitors.
|
||||||
|
loginShowVersion: { envVar: 'LOGIN_SHOW_VERSION', type: 'boolean', defaultValue: true },
|
||||||
oauthEnabled: { envVar: 'OAUTH_ENABLED', type: 'boolean', defaultValue: false },
|
oauthEnabled: { envVar: 'OAUTH_ENABLED', type: 'boolean', defaultValue: false },
|
||||||
oauthOnly: { envVar: 'OAUTH_ONLY', type: 'boolean', defaultValue: false },
|
oauthOnly: { envVar: 'OAUTH_ONLY', type: 'boolean', defaultValue: false },
|
||||||
oauthClientId: { envVar: 'OAUTH_CLIENT_ID', type: 'string', defaultValue: '' },
|
oauthClientId: { envVar: 'OAUTH_CLIENT_ID', type: 'string', defaultValue: '' },
|
||||||
|
|||||||
Reference in New Issue
Block a user