From 3acd29c3f30d231f96e41682bd8622f0a97ef461 Mon Sep 17 00:00:00 2001
From: "courtmanr@gmail.com"
Date: Tue, 25 Nov 2025 08:14:19 +0000
Subject: [PATCH] Implement UI toggle for Hide Local Login (related to issue
#750)
---
docs/CONFIGURATION.md | 1 +
docs/OIDC.md | 2 +
.../src/components/Settings/Settings.tsx | 690 +++++++++---------
frontend-modern/src/types/config.ts | 2 +
internal/api/config_handlers.go | 16 +-
internal/config/config.go | 2 +
internal/config/persistence.go | 1 +
7 files changed, 382 insertions(+), 332 deletions(-)
diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md
index 07eb806..340df09 100644
--- a/docs/CONFIGURATION.md
+++ b/docs/CONFIGURATION.md
@@ -82,6 +82,7 @@ Environment variables take precedence over `system.json`.
| `LOG_LEVEL` | Log verbosity | `info` |
| `DISCOVERY_ENABLED` | Auto-discover nodes | `false` |
| `ALLOWED_ORIGINS` | CORS allowed domains | `""` (Same origin) |
+| `PULSE_AUTH_HIDE_LOCAL_LOGIN` | Hide username/password form (useful for SSO) | `false` |
---
diff --git a/docs/OIDC.md b/docs/OIDC.md
index 080dc1f..e34a07d 100644
--- a/docs/OIDC.md
+++ b/docs/OIDC.md
@@ -13,6 +13,8 @@ Enable Single Sign-On (SSO) with providers like Authentik, Keycloak, Okta, and A
* **Client ID & Secret**: From your IdP.
4. **Save**: The login page will now show a "Continue with Single Sign-On" button.
+> **Tip**: To hide the username/password form and only show the SSO button, set `PULSE_AUTH_HIDE_LOCAL_LOGIN=true` in your environment. You can still access the local login by appending `?show_local=true` to the login URL.
+
## ⚙️ Configuration
| Setting | Description |
diff --git a/frontend-modern/src/components/Settings/Settings.tsx b/frontend-modern/src/components/Settings/Settings.tsx
index 7693117..293c46b 100644
--- a/frontend-modern/src/components/Settings/Settings.tsx
+++ b/frontend-modern/src/components/Settings/Settings.tsx
@@ -638,10 +638,16 @@ const Settings: Component = (props) => {
const [temperatureMonitoringEnabled, setTemperatureMonitoringEnabled] = createSignal(true);
const [savingTemperatureSetting, setSavingTemperatureSetting] = createSignal(false);
const [hostProxyStatus, setHostProxyStatus] = createSignal(null);
+ const [hideLocalLogin, setHideLocalLogin] = createSignal(false);
+ const [savingHideLocalLogin, setSavingHideLocalLogin] = createSignal(false);
+
const temperatureMonitoringLocked = () =>
Boolean(
envOverrides().temperatureMonitoringEnabled || envOverrides()['ENABLE_TEMPERATURE_MONITORING'],
);
+ const hideLocalLoginLocked = () =>
+ Boolean(envOverrides().hideLocalLogin || envOverrides()['PULSE_AUTH_HIDE_LOCAL_LOGIN']);
+
const pvePollingEnvLocked = () =>
Boolean(envOverrides().pvePollingInterval || envOverrides().PVE_POLLING_INTERVAL);
let discoverySubnetInputRef: HTMLInputElement | undefined;
@@ -705,6 +711,35 @@ const Settings: Component = (props) => {
});
};
+ const handleHideLocalLoginChange = async (enabled: boolean): Promise => {
+ if (hideLocalLoginLocked() || savingHideLocalLogin()) {
+ return;
+ }
+
+ const previous = hideLocalLogin();
+ setHideLocalLogin(enabled);
+ setSavingHideLocalLogin(true);
+
+ try {
+ await SettingsAPI.updateSystemSettings({ hideLocalLogin: enabled });
+ if (enabled) {
+ notificationStore.success('Local login hidden', 2000);
+ } else {
+ notificationStore.info('Local login visible', 2000);
+ }
+ // Reload security status to reflect changes
+ await loadSecurityStatus();
+ } catch (error) {
+ logger.error('Failed to update hide local login setting', error);
+ notificationStore.error(
+ error instanceof Error ? error.message : 'Failed to update hide local login setting',
+ );
+ setHideLocalLogin(previous);
+ } finally {
+ setSavingHideLocalLogin(false);
+ }
+ };
+
const applySavedDiscoverySubnet = (subnet?: string | null) => {
const raw = typeof subnet === 'string' ? subnet.trim() : '';
if (raw === '' || raw.toLowerCase() === 'auto') {
@@ -980,8 +1015,8 @@ const Settings: Component = (props) => {
diag.temperatureProxy.socketFound && diag.temperatureProxy.proxyReachable
? 'healthy'
: diag.temperatureProxy.socketFound
- ? 'error'
- : 'missing';
+ ? 'error'
+ : 'missing';
const cooldowns: Record = {};
const socketHosts = diag.temperatureProxy.socketHostCooldowns || [];
(socketHosts as TemperatureProxySocketHost[]).forEach((entry) => {
@@ -1018,11 +1053,11 @@ const Settings: Component = (props) => {
setHostProxyStatus(
diag?.temperatureProxy?.hostProxySummary
? {
- hostSocketPresent: Boolean(diag.temperatureProxy?.socketFound),
- containerSocketPresent:
- diag.temperatureProxy?.hostProxySummary?.containerSocketPresent ?? undefined,
- summary: diag.temperatureProxy?.hostProxySummary ?? undefined,
- }
+ hostSocketPresent: Boolean(diag.temperatureProxy?.socketFound),
+ containerSocketPresent:
+ diag.temperatureProxy?.hostProxySummary?.containerSocketPresent ?? undefined,
+ summary: diag.temperatureProxy?.hostProxySummary ?? undefined,
+ }
: null,
);
} catch (err) {
@@ -1090,10 +1125,10 @@ const Settings: Component = (props) => {
const nodes = Array.isArray(data.nodes)
? (data.nodes as Array>).map((node) => ({
- name: typeof node.name === 'string' ? node.name : 'unknown',
- sshReady: Boolean(node.ssh_ready),
- error: typeof node.error === 'string' ? node.error : undefined,
- }))
+ name: typeof node.name === 'string' ? node.name : 'unknown',
+ sshReady: Boolean(node.ssh_ready),
+ error: typeof node.error === 'string' ? node.error : undefined,
+ }))
: [];
setProxyRegisterSummary(nodes);
@@ -1191,83 +1226,83 @@ const Settings: Component = (props) => {
disabled?: boolean;
}[];
}[] = [
- {
- id: 'platforms',
- label: 'Platforms',
- items: [
- { id: 'proxmox', label: 'Proxmox', icon: ProxmoxIcon },
- { id: 'docker', label: 'Docker', icon: Boxes },
- { id: 'hosts', label: 'Hosts', icon: Monitor, iconProps: { strokeWidth: 2 } },
- ],
- },
- {
- id: 'operations',
- label: 'Operations',
- items: [
- { id: 'api', label: 'API Tokens', icon: BadgeCheck },
- {
- id: 'diagnostics',
- label: 'Diagnostics',
- icon: Activity,
- iconProps: { strokeWidth: 2 },
- },
- ],
- },
- {
- id: 'system',
- label: 'System',
- items: [
- {
- id: 'system-general',
- label: 'General',
- icon: Sliders,
- iconProps: { strokeWidth: 2 },
- },
- {
- id: 'system-network',
- label: 'Network',
- icon: Network,
- iconProps: { strokeWidth: 2 },
- },
- {
- id: 'system-updates',
- label: 'Updates',
- icon: RefreshCw,
- iconProps: { strokeWidth: 2 },
- },
- {
- id: 'system-backups',
- label: 'Backups',
- icon: Clock,
- iconProps: { strokeWidth: 2 },
- },
- ],
- },
- {
- id: 'security',
- label: 'Security',
- items: [
- {
- id: 'security-overview',
- label: 'Overview',
- icon: Shield,
- iconProps: { strokeWidth: 2 },
- },
- {
- id: 'security-auth',
- label: 'Authentication',
- icon: Lock,
- iconProps: { strokeWidth: 2 },
- },
- {
- id: 'security-sso',
- label: 'Single Sign-On',
- icon: Key,
- iconProps: { strokeWidth: 2 },
- },
- ],
- },
- ];
+ {
+ id: 'platforms',
+ label: 'Platforms',
+ items: [
+ { id: 'proxmox', label: 'Proxmox', icon: ProxmoxIcon },
+ { id: 'docker', label: 'Docker', icon: Boxes },
+ { id: 'hosts', label: 'Hosts', icon: Monitor, iconProps: { strokeWidth: 2 } },
+ ],
+ },
+ {
+ id: 'operations',
+ label: 'Operations',
+ items: [
+ { id: 'api', label: 'API Tokens', icon: BadgeCheck },
+ {
+ id: 'diagnostics',
+ label: 'Diagnostics',
+ icon: Activity,
+ iconProps: { strokeWidth: 2 },
+ },
+ ],
+ },
+ {
+ id: 'system',
+ label: 'System',
+ items: [
+ {
+ id: 'system-general',
+ label: 'General',
+ icon: Sliders,
+ iconProps: { strokeWidth: 2 },
+ },
+ {
+ id: 'system-network',
+ label: 'Network',
+ icon: Network,
+ iconProps: { strokeWidth: 2 },
+ },
+ {
+ id: 'system-updates',
+ label: 'Updates',
+ icon: RefreshCw,
+ iconProps: { strokeWidth: 2 },
+ },
+ {
+ id: 'system-backups',
+ label: 'Backups',
+ icon: Clock,
+ iconProps: { strokeWidth: 2 },
+ },
+ ],
+ },
+ {
+ id: 'security',
+ label: 'Security',
+ items: [
+ {
+ id: 'security-overview',
+ label: 'Overview',
+ icon: Shield,
+ iconProps: { strokeWidth: 2 },
+ },
+ {
+ id: 'security-auth',
+ label: 'Authentication',
+ icon: Lock,
+ iconProps: { strokeWidth: 2 },
+ },
+ {
+ id: 'security-sso',
+ label: 'Single Sign-On',
+ icon: Key,
+ iconProps: { strokeWidth: 2 },
+ },
+ ],
+ },
+ ];
const flatTabs = tabGroups.flatMap((group) => group.items);
@@ -1960,6 +1995,9 @@ const Settings: Component = (props) => {
? systemSettings.temperatureMonitoringEnabled
: true,
);
+ // Load hideLocalLogin setting
+ setHideLocalLogin(systemSettings.hideLocalLogin ?? false);
+
// Backup polling controls
if (typeof systemSettings.backupPollingEnabled === 'boolean') {
setBackupPollingEnabled(systemSettings.backupPollingEnabled);
@@ -2558,15 +2596,13 @@ const Settings: Component = (props) => {
type="button"
aria-current={isActive() ? 'page' : undefined}
disabled={item.disabled}
- class={`flex w-full items-center ${sidebarCollapsed() ? 'justify-center' : 'gap-2.5'} rounded-md ${
- sidebarCollapsed() ? 'px-2 py-2.5' : 'px-3 py-2'
- } text-sm font-medium transition-colors ${
- item.disabled
+ class={`flex w-full items-center ${sidebarCollapsed() ? 'justify-center' : 'gap-2.5'} rounded-md ${sidebarCollapsed() ? 'px-2 py-2.5' : 'px-3 py-2'
+ } text-sm font-medium transition-colors ${item.disabled
? 'opacity-60 cursor-not-allowed text-gray-400 dark:text-gray-600'
: isActive()
? 'bg-blue-50 text-blue-600 dark:bg-blue-900/30 dark:text-blue-200'
: 'text-gray-600 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-300 dark:hover:bg-gray-700/60 dark:hover:text-gray-100'
- }`}
+ }`}
onClick={() => {
if (item.disabled) return;
setActiveTab(item.id);
@@ -2604,13 +2640,12 @@ const Settings: Component = (props) => {
@@ -3596,11 +3631,10 @@ const Settings: Component = (props) => {
{(option) => (