From 7121ca31a5e550cac0956d2b4ae5985aba1eb917 Mon Sep 17 00:00:00 2001 From: U Bhalraam Date: Thu, 9 Apr 2026 16:44:48 +0100 Subject: [PATCH 1/4] feat: add option to disable password login and skip login page Add a "Password Login" toggle to the Authentication settings page. When disabled, the password login form is hidden from the login page and the /api/auth/login/password endpoint returns 403. An amber warning alerts admins about potential lockout before saving. Add a "Skip login page" toggle to OAuth Provider Management, visible when password login is disabled and exactly one OAuth provider is configured. When enabled, users are redirected straight to the OAuth provider instead of seeing the login page. Closes #478 --- pkg/ai/handler.go | 30 ++++--- pkg/auth/login_handler.go | 23 ++++- pkg/model/general_setting.go | 2 + .../settings/authentication-management.tsx | 83 +++++++++++++++++-- .../settings/oauth-provider-management.tsx | 76 ++++++++++++++++- ui/src/contexts/auth-context.tsx | 11 ++- ui/src/lib/api/admin.ts | 5 ++ ui/src/lib/api/auth.ts | 2 + ui/src/pages/login.tsx | 14 ++++ 9 files changed, 219 insertions(+), 27 deletions(-) diff --git a/pkg/ai/handler.go b/pkg/ai/handler.go index 3f1c4d63..db2652f0 100644 --- a/pkg/ai/handler.go +++ b/pkg/ai/handler.go @@ -208,21 +208,25 @@ func HandleGetGeneralSetting(c *gin.Context) { "nodeTerminalImage": setting.NodeTerminalImage, "enableAnalytics": setting.EnableAnalytics, "enableVersionCheck": setting.EnableVersionCheck, + "passwordLoginDisabled": setting.PasswordLoginDisabled, + "skipLoginPage": setting.SkipLoginPage, }) } type UpdateGeneralSettingRequest struct { - AIAgentEnabled bool `json:"aiAgentEnabled"` - AIProvider string `json:"aiProvider"` - AIModel string `json:"aiModel"` - AIAPIKey *string `json:"aiApiKey"` - AIBaseURL string `json:"aiBaseUrl"` - AIMaxTokens int `json:"aiMaxTokens"` - KubectlEnabled bool `json:"kubectlEnabled"` - KubectlImage string `json:"kubectlImage"` - NodeTerminalImage string `json:"nodeTerminalImage"` - EnableAnalytics bool `json:"enableAnalytics"` - EnableVersionCheck bool `json:"enableVersionCheck"` + AIAgentEnabled bool `json:"aiAgentEnabled"` + AIProvider string `json:"aiProvider"` + AIModel string `json:"aiModel"` + AIAPIKey *string `json:"aiApiKey"` + AIBaseURL string `json:"aiBaseUrl"` + AIMaxTokens int `json:"aiMaxTokens"` + KubectlEnabled bool `json:"kubectlEnabled"` + KubectlImage string `json:"kubectlImage"` + NodeTerminalImage string `json:"nodeTerminalImage"` + EnableAnalytics bool `json:"enableAnalytics"` + EnableVersionCheck bool `json:"enableVersionCheck"` + PasswordLoginDisabled bool `json:"passwordLoginDisabled"` + SkipLoginPage bool `json:"skipLoginPage"` } func HandleUpdateGeneralSetting(c *gin.Context) { @@ -297,6 +301,8 @@ func HandleUpdateGeneralSetting(c *gin.Context) { "node_terminal_image": nodeTerminalImage, "enable_analytics": req.EnableAnalytics, "enable_version_check": req.EnableVersionCheck, + "password_login_disabled": req.PasswordLoginDisabled, + "skip_login_page": req.SkipLoginPage, } if shouldUpdateAIAPIKey { updates["ai_api_key"] = model.SecretString(aiAPIKey) @@ -322,6 +328,8 @@ func HandleUpdateGeneralSetting(c *gin.Context) { "nodeTerminalImage": updated.NodeTerminalImage, "enableAnalytics": updated.EnableAnalytics, "enableVersionCheck": updated.EnableVersionCheck, + "passwordLoginDisabled": updated.PasswordLoginDisabled, + "skipLoginPage": updated.SkipLoginPage, }) } diff --git a/pkg/auth/login_handler.go b/pkg/auth/login_handler.go index 51e20383..da17aa33 100644 --- a/pkg/auth/login_handler.go +++ b/pkg/auth/login_handler.go @@ -15,23 +15,35 @@ import ( ) func (h *AuthHandler) GetProviders(c *gin.Context) { - credentialProviders := []string{model.AuthProviderPassword} + var credentialProviders []string + + generalSetting, err := model.GetGeneralSetting() + if err != nil { + klog.Warningf("Failed to load general setting for providers: %v", err) + } + if generalSetting == nil || !generalSetting.PasswordLoginDisabled { + credentialProviders = append(credentialProviders, model.AuthProviderPassword) + } + oauthProviders := uniqueStrings(h.manager.GetAvailableProviders()) - setting, err := model.GetLDAPSetting() + ldapSetting, err := model.GetLDAPSetting() if err != nil { klog.Warningf("Failed to load ldap setting for providers: %v", err) - } else if setting.Enabled { + } else if ldapSetting.Enabled { credentialProviders = append(credentialProviders, model.AuthProviderLDAP) } credentialProviders = uniqueStrings(credentialProviders) providers := append(append([]string{}, credentialProviders...), oauthProviders...) + skipLoginPage := generalSetting != nil && generalSetting.SkipLoginPage + c.JSON(http.StatusOK, gin.H{ "providers": providers, "credentialProviders": credentialProviders, "oauthProviders": oauthProviders, + "skipLoginPage": skipLoginPage, }) } @@ -67,6 +79,11 @@ func (h *AuthHandler) Login(c *gin.Context) { } func (h *AuthHandler) PasswordLogin(c *gin.Context) { + setting, err := model.GetGeneralSetting() + if err == nil && setting.PasswordLoginDisabled { + c.JSON(http.StatusForbidden, gin.H{"error": "Password login is disabled"}) + return + } h.handleCredentialLogin(c, model.AuthProviderPassword, h.authenticatePasswordUser) } diff --git a/pkg/model/general_setting.go b/pkg/model/general_setting.go index bf1a1eb7..02da8088 100644 --- a/pkg/model/general_setting.go +++ b/pkg/model/general_setting.go @@ -41,6 +41,8 @@ type GeneralSetting struct { NodeTerminalImage string `json:"nodeTerminalImage" gorm:"column:node_terminal_image;type:varchar(255);not null;default:'busybox:latest'"` EnableAnalytics bool `json:"enableAnalytics" gorm:"column:enable_analytics;type:boolean;not null;default:true"` EnableVersionCheck bool `json:"enableVersionCheck" gorm:"column:enable_version_check;type:boolean;not null;default:true"` + PasswordLoginDisabled bool `json:"passwordLoginDisabled" gorm:"column:password_login_disabled;type:boolean;not null;default:false"` + SkipLoginPage bool `json:"skipLoginPage" gorm:"column:skip_login_page;type:boolean;not null;default:false"` JWTSecret SecretString `json:"-" gorm:"column:jwt_secret;type:text"` GlobalSidebarPreference string `json:"-" gorm:"column:global_sidebar_preference;type:text"` } diff --git a/ui/src/components/settings/authentication-management.tsx b/ui/src/components/settings/authentication-management.tsx index f24c2bf7..cd4693c5 100644 --- a/ui/src/components/settings/authentication-management.tsx +++ b/ui/src/components/settings/authentication-management.tsx @@ -1,5 +1,5 @@ import { useEffect, useState } from 'react' -import { IconKey } from '@tabler/icons-react' +import { IconKey, IconAlertTriangle } from '@tabler/icons-react' import { useMutation, useQueryClient } from '@tanstack/react-query' import { useTranslation } from 'react-i18next' import { toast } from 'sonner' @@ -8,7 +8,9 @@ import { LDAPSetting, LDAPSettingUpdateRequest, updateLDAPSetting, + updateGeneralSetting, useLDAPSetting, + useGeneralSetting, } from '@/lib/api' import { translateError } from '@/lib/utils' import { Button } from '@/components/ui/button' @@ -65,20 +67,34 @@ export function AuthenticationManagement() { const { t } = useTranslation() const queryClient = useQueryClient() const { data, error, isError, isLoading, refetch } = useLDAPSetting() + const { data: generalSetting } = useGeneralSetting() const [formData, setFormData] = useState( createDefaultSettings ) + const [passwordLoginEnabled, setPasswordLoginEnabled] = useState(true) useEffect(() => { setFormData(toFormData(data)) }, [data]) + useEffect(() => { + if (generalSetting) { + setPasswordLoginEnabled(!generalSetting.passwordLoginDisabled) + } + }, [generalSetting]) + const mutation = useMutation({ - mutationFn: updateLDAPSetting, + mutationFn: (params: { ldap: LDAPSettingUpdateRequest; passwordLoginDisabled: boolean }) => + Promise.all([ + updateLDAPSetting(params.ldap), + updateGeneralSetting({ + ...generalSetting!, + passwordLoginDisabled: params.passwordLoginDisabled, + }), + ]), onSuccess: () => { - queryClient.invalidateQueries({ - queryKey: ['ldap-setting'], - }) + queryClient.invalidateQueries({ queryKey: ['ldap-setting'] }) + queryClient.invalidateQueries({ queryKey: ['general-setting'] }) toast.success( t( 'authenticationManagement.messages.updated', @@ -104,7 +120,7 @@ export function AuthenticationManagement() { return } - const payload: LDAPSettingUpdateRequest = { + const ldapPayload: LDAPSettingUpdateRequest = { enabled: formData.enabled, serverUrl: formData.serverUrl.trim(), useStartTLS: formData.useStartTLS, @@ -118,10 +134,13 @@ export function AuthenticationManagement() { groupNameAttribute: formData.groupNameAttribute.trim(), } if (formData.bindPassword !== '') { - payload.bindPassword = formData.bindPassword + ldapPayload.bindPassword = formData.bindPassword } - mutation.mutate(payload) + mutation.mutate({ + ldap: ldapPayload, + passwordLoginDisabled: !passwordLoginEnabled, + }) } if (isLoading && !data) { @@ -166,6 +185,52 @@ export function AuthenticationManagement() { +
+
+
+ +

+ {t( + 'authenticationManagement.password.description', + 'Allow users to sign in with a username and password.' + )} +

+
+ +
+ + {!passwordLoginEnabled && ( +
+
+ +
+

+ {t( + 'authenticationManagement.password.warning.title', + 'Warning: You may lose access!' + )} +

+

+ {t( + 'authenticationManagement.password.warning.description', + 'Verify that your LDAP or OAuth provider is working before saving. Without a working alternative login method, you will be locked out and can only recover by resetting the database.' + )} +

+
+
+
+ )} +
+
@@ -427,7 +492,7 @@ export function AuthenticationManagement() {
-
diff --git a/ui/src/components/settings/oauth-provider-management.tsx b/ui/src/components/settings/oauth-provider-management.tsx index 20a5207b..8dd2a17e 100644 --- a/ui/src/components/settings/oauth-provider-management.tsx +++ b/ui/src/components/settings/oauth-provider-management.tsx @@ -1,5 +1,5 @@ -import { useCallback, useMemo, useState } from 'react' -import { IconEdit, IconKey, IconPlus, IconTrash } from '@tabler/icons-react' +import { useCallback, useEffect, useMemo, useState } from 'react' +import { IconEdit, IconKey, IconPlus, IconTrash, IconPlayerSkipForward } from '@tabler/icons-react' import { useMutation, useQueryClient } from '@tanstack/react-query' import { ColumnDef } from '@tanstack/react-table' import { useTranslation } from 'react-i18next' @@ -12,11 +12,15 @@ import { OAuthProviderCreateRequest, OAuthProviderUpdateRequest, updateOAuthProvider, + updateGeneralSetting, + useGeneralSetting, useOAuthProviderList, } from '@/lib/api' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' +import { Label } from '@/components/ui/label' +import { Switch } from '@/components/ui/switch' import { DeleteConfirmationDialog } from '@/components/delete-confirmation-dialog' import { Action, ActionTable } from '../action-table' @@ -28,6 +32,36 @@ export function OAuthProviderManagement() { // Use real API to fetch OAuth providers const { data: providers = [], isLoading, error } = useOAuthProviderList() + const { data: generalSetting } = useGeneralSetting() + const [skipLoginPage, setSkipLoginPage] = useState(false) + + useEffect(() => { + if (generalSetting) { + setSkipLoginPage(generalSetting.skipLoginPage ?? false) + } + }, [generalSetting]) + + const skipLoginMutation = useMutation({ + mutationFn: (skip: boolean) => { + if (!generalSetting) return Promise.reject(new Error('Settings not loaded')) + return updateGeneralSetting({ + ...generalSetting, + skipLoginPage: skip, + }) + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['general-setting'] }) + toast.success( + t( + 'authenticationManagement.messages.updated', + 'Authentication settings updated' + ) + ) + }, + onError: (error) => { + toast.error(String(error)) + }, + }) const [showProviderDialog, setShowProviderDialog] = useState(false) const [editingProvider, setEditingProvider] = useState( @@ -295,6 +329,44 @@ export function OAuthProviderManagement() {

)} + + {generalSetting?.passwordLoginDisabled && providers.length === 1 && ( +
+
+
+ +
+ +

+ {t( + 'oauthManagement.skipLogin.description', + 'OAuth is the only login method.' + )} +

+

+ {t( + 'oauthManagement.skipLogin.hint', + 'Enable this to redirect users straight to your OAuth provider instead of showing the login page.' + )} +

+
+
+ { + setSkipLoginPage(checked) + skipLoginMutation.mutate(checked) + }} + disabled={skipLoginMutation.isPending || !generalSetting} + /> +
+
+ )} diff --git a/ui/src/contexts/auth-context.tsx b/ui/src/contexts/auth-context.tsx index 65b2e0ad..e9ff2756 100644 --- a/ui/src/contexts/auth-context.tsx +++ b/ui/src/contexts/auth-context.tsx @@ -35,6 +35,7 @@ interface AuthContextType { globalSidebarPreference: string credentialProviders: CredentialProvider[] oauthProviders: string[] + skipLoginPage: boolean login: (provider?: string) => Promise loginWithCredentials: ( provider: CredentialProvider, @@ -78,10 +79,12 @@ function normalizeUser(user: AuthUser): User { function applyAuthProviderCatalog( catalog: AuthProviderCatalog, setCredentialProviders: (providers: CredentialProvider[]) => void, - setOAuthProviders: (providers: string[]) => void + setOAuthProviders: (providers: string[]) => void, + setSkipLoginPage: (skip: boolean) => void ) { setCredentialProviders(catalog.credentialProviders) setOAuthProviders(catalog.oauthProviders) + setSkipLoginPage(catalog.skipLoginPage ?? false) } function applyCurrentUser( @@ -107,6 +110,7 @@ export function AuthProvider({ children }: AuthProviderProps) { CredentialProvider[] >([]) const [oauthProviders, setOAuthProviders] = useState([]) + const [skipLoginPage, setSkipLoginPage] = useState(false) const { refetch: refetchAuthProviders } = useAuthProviders({ enabled: false, @@ -139,7 +143,8 @@ export function AuthProvider({ children }: AuthProviderProps) { applyAuthProviderCatalog( providersResult.data, setCredentialProviders, - setOAuthProviders + setOAuthProviders, + setSkipLoginPage ) } @@ -223,6 +228,7 @@ export function AuthProvider({ children }: AuthProviderProps) { globalSidebarPreference, credentialProviders, oauthProviders, + skipLoginPage, login, loginWithCredentials, logout, @@ -236,6 +242,7 @@ export function AuthProvider({ children }: AuthProviderProps) { globalSidebarPreference, credentialProviders, oauthProviders, + skipLoginPage, login, loginWithCredentials, logout, diff --git a/ui/src/lib/api/admin.ts b/ui/src/lib/api/admin.ts index 64a80073..e639c99d 100644 --- a/ui/src/lib/api/admin.ts +++ b/ui/src/lib/api/admin.ts @@ -354,6 +354,8 @@ export interface GeneralSetting { nodeTerminalImage: string enableAnalytics: boolean enableVersionCheck: boolean + passwordLoginDisabled: boolean + skipLoginPage: boolean } export interface GeneralSettingUpdateRequest { @@ -368,6 +370,8 @@ export interface GeneralSettingUpdateRequest { nodeTerminalImage?: string enableAnalytics?: boolean enableVersionCheck?: boolean + passwordLoginDisabled?: boolean + skipLoginPage?: boolean } export type CredentialProvider = 'password' | 'ldap' @@ -376,6 +380,7 @@ export interface AuthProviderCatalog { providers: string[] credentialProviders: CredentialProvider[] oauthProviders: string[] + skipLoginPage: boolean } export interface LDAPSetting { diff --git a/ui/src/lib/api/auth.ts b/ui/src/lib/api/auth.ts index fa52c07d..5f8108f6 100644 --- a/ui/src/lib/api/auth.ts +++ b/ui/src/lib/api/auth.ts @@ -32,6 +32,7 @@ function normalizeAuthProviderCatalog( providers: data.providers || [], credentialProviders: data.credentialProviders || [], oauthProviders: data.oauthProviders || [], + skipLoginPage: data.skipLoginPage ?? false, } } @@ -48,6 +49,7 @@ function normalizeAuthProviderCatalog( providers, credentialProviders, oauthProviders, + skipLoginPage: data.skipLoginPage ?? false, } } diff --git a/ui/src/pages/login.tsx b/ui/src/pages/login.tsx index 6b454328..39a994a9 100644 --- a/ui/src/pages/login.tsx +++ b/ui/src/pages/login.tsx @@ -29,6 +29,7 @@ export function LoginPage() { loginWithCredentials, credentialProviders, oauthProviders, + skipLoginPage, isLoading, } = useAuth() const [searchParams] = useSearchParams() @@ -51,6 +52,19 @@ export function LoginPage() { } }, [credentialProviders, credentialsProvider]) + // Auto-redirect to OAuth when admin has enabled "Skip login page" + useEffect(() => { + if ( + !isLoading && + !user && + !error && + skipLoginPage && + oauthProviders.length === 1 + ) { + login(oauthProviders[0]) + } + }, [isLoading, user, error, skipLoginPage, oauthProviders, login]) + if (user && !isLoading) { return } From 9c878e0efe86f0e24a005b82843764428cbfb61c Mon Sep 17 00:00:00 2001 From: U Bhalraam Date: Fri, 10 Apr 2026 10:31:00 +0100 Subject: [PATCH 2/4] refactor: drop skip login page feature per review --- pkg/ai/handler.go | 4 - pkg/auth/login_handler.go | 7 +- pkg/model/general_setting.go | 1 - .../settings/authentication-management.tsx | 10 ++- .../settings/oauth-provider-management.tsx | 76 +------------------ ui/src/contexts/auth-context.tsx | 11 +-- ui/src/lib/api/admin.ts | 3 - ui/src/lib/api/auth.ts | 2 - ui/src/pages/login.tsx | 14 ---- 9 files changed, 12 insertions(+), 116 deletions(-) diff --git a/pkg/ai/handler.go b/pkg/ai/handler.go index db2652f0..c2c832b3 100644 --- a/pkg/ai/handler.go +++ b/pkg/ai/handler.go @@ -209,7 +209,6 @@ func HandleGetGeneralSetting(c *gin.Context) { "enableAnalytics": setting.EnableAnalytics, "enableVersionCheck": setting.EnableVersionCheck, "passwordLoginDisabled": setting.PasswordLoginDisabled, - "skipLoginPage": setting.SkipLoginPage, }) } @@ -226,7 +225,6 @@ type UpdateGeneralSettingRequest struct { EnableAnalytics bool `json:"enableAnalytics"` EnableVersionCheck bool `json:"enableVersionCheck"` PasswordLoginDisabled bool `json:"passwordLoginDisabled"` - SkipLoginPage bool `json:"skipLoginPage"` } func HandleUpdateGeneralSetting(c *gin.Context) { @@ -302,7 +300,6 @@ func HandleUpdateGeneralSetting(c *gin.Context) { "enable_analytics": req.EnableAnalytics, "enable_version_check": req.EnableVersionCheck, "password_login_disabled": req.PasswordLoginDisabled, - "skip_login_page": req.SkipLoginPage, } if shouldUpdateAIAPIKey { updates["ai_api_key"] = model.SecretString(aiAPIKey) @@ -329,7 +326,6 @@ func HandleUpdateGeneralSetting(c *gin.Context) { "enableAnalytics": updated.EnableAnalytics, "enableVersionCheck": updated.EnableVersionCheck, "passwordLoginDisabled": updated.PasswordLoginDisabled, - "skipLoginPage": updated.SkipLoginPage, }) } diff --git a/pkg/auth/login_handler.go b/pkg/auth/login_handler.go index da17aa33..2df456bb 100644 --- a/pkg/auth/login_handler.go +++ b/pkg/auth/login_handler.go @@ -27,23 +27,20 @@ func (h *AuthHandler) GetProviders(c *gin.Context) { oauthProviders := uniqueStrings(h.manager.GetAvailableProviders()) - ldapSetting, err := model.GetLDAPSetting() + setting, err := model.GetLDAPSetting() if err != nil { klog.Warningf("Failed to load ldap setting for providers: %v", err) - } else if ldapSetting.Enabled { + } else if setting.Enabled { credentialProviders = append(credentialProviders, model.AuthProviderLDAP) } credentialProviders = uniqueStrings(credentialProviders) providers := append(append([]string{}, credentialProviders...), oauthProviders...) - skipLoginPage := generalSetting != nil && generalSetting.SkipLoginPage - c.JSON(http.StatusOK, gin.H{ "providers": providers, "credentialProviders": credentialProviders, "oauthProviders": oauthProviders, - "skipLoginPage": skipLoginPage, }) } diff --git a/pkg/model/general_setting.go b/pkg/model/general_setting.go index 02da8088..4bd2a88f 100644 --- a/pkg/model/general_setting.go +++ b/pkg/model/general_setting.go @@ -42,7 +42,6 @@ type GeneralSetting struct { EnableAnalytics bool `json:"enableAnalytics" gorm:"column:enable_analytics;type:boolean;not null;default:true"` EnableVersionCheck bool `json:"enableVersionCheck" gorm:"column:enable_version_check;type:boolean;not null;default:true"` PasswordLoginDisabled bool `json:"passwordLoginDisabled" gorm:"column:password_login_disabled;type:boolean;not null;default:false"` - SkipLoginPage bool `json:"skipLoginPage" gorm:"column:skip_login_page;type:boolean;not null;default:false"` JWTSecret SecretString `json:"-" gorm:"column:jwt_secret;type:text"` GlobalSidebarPreference string `json:"-" gorm:"column:global_sidebar_preference;type:text"` } diff --git a/ui/src/components/settings/authentication-management.tsx b/ui/src/components/settings/authentication-management.tsx index cd4693c5..5382efc0 100644 --- a/ui/src/components/settings/authentication-management.tsx +++ b/ui/src/components/settings/authentication-management.tsx @@ -93,7 +93,9 @@ export function AuthenticationManagement() { }), ]), onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['ldap-setting'] }) + queryClient.invalidateQueries({ + queryKey: ['ldap-setting'], + }) queryClient.invalidateQueries({ queryKey: ['general-setting'] }) toast.success( t( @@ -120,7 +122,7 @@ export function AuthenticationManagement() { return } - const ldapPayload: LDAPSettingUpdateRequest = { + const payload: LDAPSettingUpdateRequest = { enabled: formData.enabled, serverUrl: formData.serverUrl.trim(), useStartTLS: formData.useStartTLS, @@ -134,11 +136,11 @@ export function AuthenticationManagement() { groupNameAttribute: formData.groupNameAttribute.trim(), } if (formData.bindPassword !== '') { - ldapPayload.bindPassword = formData.bindPassword + payload.bindPassword = formData.bindPassword } mutation.mutate({ - ldap: ldapPayload, + ldap: payload, passwordLoginDisabled: !passwordLoginEnabled, }) } diff --git a/ui/src/components/settings/oauth-provider-management.tsx b/ui/src/components/settings/oauth-provider-management.tsx index 8dd2a17e..20a5207b 100644 --- a/ui/src/components/settings/oauth-provider-management.tsx +++ b/ui/src/components/settings/oauth-provider-management.tsx @@ -1,5 +1,5 @@ -import { useCallback, useEffect, useMemo, useState } from 'react' -import { IconEdit, IconKey, IconPlus, IconTrash, IconPlayerSkipForward } from '@tabler/icons-react' +import { useCallback, useMemo, useState } from 'react' +import { IconEdit, IconKey, IconPlus, IconTrash } from '@tabler/icons-react' import { useMutation, useQueryClient } from '@tanstack/react-query' import { ColumnDef } from '@tanstack/react-table' import { useTranslation } from 'react-i18next' @@ -12,15 +12,11 @@ import { OAuthProviderCreateRequest, OAuthProviderUpdateRequest, updateOAuthProvider, - updateGeneralSetting, - useGeneralSetting, useOAuthProviderList, } from '@/lib/api' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' -import { Label } from '@/components/ui/label' -import { Switch } from '@/components/ui/switch' import { DeleteConfirmationDialog } from '@/components/delete-confirmation-dialog' import { Action, ActionTable } from '../action-table' @@ -32,36 +28,6 @@ export function OAuthProviderManagement() { // Use real API to fetch OAuth providers const { data: providers = [], isLoading, error } = useOAuthProviderList() - const { data: generalSetting } = useGeneralSetting() - const [skipLoginPage, setSkipLoginPage] = useState(false) - - useEffect(() => { - if (generalSetting) { - setSkipLoginPage(generalSetting.skipLoginPage ?? false) - } - }, [generalSetting]) - - const skipLoginMutation = useMutation({ - mutationFn: (skip: boolean) => { - if (!generalSetting) return Promise.reject(new Error('Settings not loaded')) - return updateGeneralSetting({ - ...generalSetting, - skipLoginPage: skip, - }) - }, - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['general-setting'] }) - toast.success( - t( - 'authenticationManagement.messages.updated', - 'Authentication settings updated' - ) - ) - }, - onError: (error) => { - toast.error(String(error)) - }, - }) const [showProviderDialog, setShowProviderDialog] = useState(false) const [editingProvider, setEditingProvider] = useState( @@ -329,44 +295,6 @@ export function OAuthProviderManagement() {

)} - - {generalSetting?.passwordLoginDisabled && providers.length === 1 && ( -
-
-
- -
- -

- {t( - 'oauthManagement.skipLogin.description', - 'OAuth is the only login method.' - )} -

-

- {t( - 'oauthManagement.skipLogin.hint', - 'Enable this to redirect users straight to your OAuth provider instead of showing the login page.' - )} -

-
-
- { - setSkipLoginPage(checked) - skipLoginMutation.mutate(checked) - }} - disabled={skipLoginMutation.isPending || !generalSetting} - /> -
-
- )}
diff --git a/ui/src/contexts/auth-context.tsx b/ui/src/contexts/auth-context.tsx index e9ff2756..65b2e0ad 100644 --- a/ui/src/contexts/auth-context.tsx +++ b/ui/src/contexts/auth-context.tsx @@ -35,7 +35,6 @@ interface AuthContextType { globalSidebarPreference: string credentialProviders: CredentialProvider[] oauthProviders: string[] - skipLoginPage: boolean login: (provider?: string) => Promise loginWithCredentials: ( provider: CredentialProvider, @@ -79,12 +78,10 @@ function normalizeUser(user: AuthUser): User { function applyAuthProviderCatalog( catalog: AuthProviderCatalog, setCredentialProviders: (providers: CredentialProvider[]) => void, - setOAuthProviders: (providers: string[]) => void, - setSkipLoginPage: (skip: boolean) => void + setOAuthProviders: (providers: string[]) => void ) { setCredentialProviders(catalog.credentialProviders) setOAuthProviders(catalog.oauthProviders) - setSkipLoginPage(catalog.skipLoginPage ?? false) } function applyCurrentUser( @@ -110,7 +107,6 @@ export function AuthProvider({ children }: AuthProviderProps) { CredentialProvider[] >([]) const [oauthProviders, setOAuthProviders] = useState([]) - const [skipLoginPage, setSkipLoginPage] = useState(false) const { refetch: refetchAuthProviders } = useAuthProviders({ enabled: false, @@ -143,8 +139,7 @@ export function AuthProvider({ children }: AuthProviderProps) { applyAuthProviderCatalog( providersResult.data, setCredentialProviders, - setOAuthProviders, - setSkipLoginPage + setOAuthProviders ) } @@ -228,7 +223,6 @@ export function AuthProvider({ children }: AuthProviderProps) { globalSidebarPreference, credentialProviders, oauthProviders, - skipLoginPage, login, loginWithCredentials, logout, @@ -242,7 +236,6 @@ export function AuthProvider({ children }: AuthProviderProps) { globalSidebarPreference, credentialProviders, oauthProviders, - skipLoginPage, login, loginWithCredentials, logout, diff --git a/ui/src/lib/api/admin.ts b/ui/src/lib/api/admin.ts index e639c99d..418c091d 100644 --- a/ui/src/lib/api/admin.ts +++ b/ui/src/lib/api/admin.ts @@ -355,7 +355,6 @@ export interface GeneralSetting { enableAnalytics: boolean enableVersionCheck: boolean passwordLoginDisabled: boolean - skipLoginPage: boolean } export interface GeneralSettingUpdateRequest { @@ -371,7 +370,6 @@ export interface GeneralSettingUpdateRequest { enableAnalytics?: boolean enableVersionCheck?: boolean passwordLoginDisabled?: boolean - skipLoginPage?: boolean } export type CredentialProvider = 'password' | 'ldap' @@ -380,7 +378,6 @@ export interface AuthProviderCatalog { providers: string[] credentialProviders: CredentialProvider[] oauthProviders: string[] - skipLoginPage: boolean } export interface LDAPSetting { diff --git a/ui/src/lib/api/auth.ts b/ui/src/lib/api/auth.ts index 5f8108f6..fa52c07d 100644 --- a/ui/src/lib/api/auth.ts +++ b/ui/src/lib/api/auth.ts @@ -32,7 +32,6 @@ function normalizeAuthProviderCatalog( providers: data.providers || [], credentialProviders: data.credentialProviders || [], oauthProviders: data.oauthProviders || [], - skipLoginPage: data.skipLoginPage ?? false, } } @@ -49,7 +48,6 @@ function normalizeAuthProviderCatalog( providers, credentialProviders, oauthProviders, - skipLoginPage: data.skipLoginPage ?? false, } } diff --git a/ui/src/pages/login.tsx b/ui/src/pages/login.tsx index 39a994a9..6b454328 100644 --- a/ui/src/pages/login.tsx +++ b/ui/src/pages/login.tsx @@ -29,7 +29,6 @@ export function LoginPage() { loginWithCredentials, credentialProviders, oauthProviders, - skipLoginPage, isLoading, } = useAuth() const [searchParams] = useSearchParams() @@ -52,19 +51,6 @@ export function LoginPage() { } }, [credentialProviders, credentialsProvider]) - // Auto-redirect to OAuth when admin has enabled "Skip login page" - useEffect(() => { - if ( - !isLoading && - !user && - !error && - skipLoginPage && - oauthProviders.length === 1 - ) { - login(oauthProviders[0]) - } - }, [isLoading, user, error, skipLoginPage, oauthProviders, login]) - if (user && !isLoading) { return } From 3b2b1f34732a8a46547a94f710c985b0a7b0c328 Mon Sep 17 00:00:00 2001 From: Zzde Date: Sun, 19 Apr 2026 21:18:33 +0800 Subject: [PATCH 3/4] format code & fix zero value issue Signed-off-by: Zzde --- pkg/ai/handler.go | 54 ++++++++++--------- .../settings/authentication-management.tsx | 16 ++++-- 2 files changed, 39 insertions(+), 31 deletions(-) diff --git a/pkg/ai/handler.go b/pkg/ai/handler.go index c2c832b3..fde36bc7 100644 --- a/pkg/ai/handler.go +++ b/pkg/ai/handler.go @@ -196,18 +196,18 @@ func HandleGetGeneralSetting(c *gin.Context) { } hasAIAPIKey := strings.TrimSpace(string(setting.AIAPIKey)) != "" c.JSON(http.StatusOK, gin.H{ - "aiAgentEnabled": setting.AIAgentEnabled, - "aiProvider": setting.AIProvider, - "aiModel": setting.AIModel, - "aiApiKey": "", - "aiApiKeyConfigured": hasAIAPIKey, - "aiBaseUrl": setting.AIBaseURL, - "aiMaxTokens": setting.AIMaxTokens, - "kubectlEnabled": setting.KubectlEnabled, - "kubectlImage": setting.KubectlImage, - "nodeTerminalImage": setting.NodeTerminalImage, - "enableAnalytics": setting.EnableAnalytics, - "enableVersionCheck": setting.EnableVersionCheck, + "aiAgentEnabled": setting.AIAgentEnabled, + "aiProvider": setting.AIProvider, + "aiModel": setting.AIModel, + "aiApiKey": "", + "aiApiKeyConfigured": hasAIAPIKey, + "aiBaseUrl": setting.AIBaseURL, + "aiMaxTokens": setting.AIMaxTokens, + "kubectlEnabled": setting.KubectlEnabled, + "kubectlImage": setting.KubectlImage, + "nodeTerminalImage": setting.NodeTerminalImage, + "enableAnalytics": setting.EnableAnalytics, + "enableVersionCheck": setting.EnableVersionCheck, "passwordLoginDisabled": setting.PasswordLoginDisabled, }) } @@ -224,7 +224,7 @@ type UpdateGeneralSettingRequest struct { NodeTerminalImage string `json:"nodeTerminalImage"` EnableAnalytics bool `json:"enableAnalytics"` EnableVersionCheck bool `json:"enableVersionCheck"` - PasswordLoginDisabled bool `json:"passwordLoginDisabled"` + PasswordLoginDisabled *bool `json:"passwordLoginDisabled"` } func HandleUpdateGeneralSetting(c *gin.Context) { @@ -299,7 +299,9 @@ func HandleUpdateGeneralSetting(c *gin.Context) { "node_terminal_image": nodeTerminalImage, "enable_analytics": req.EnableAnalytics, "enable_version_check": req.EnableVersionCheck, - "password_login_disabled": req.PasswordLoginDisabled, + } + if req.PasswordLoginDisabled != nil { + updates["password_login_disabled"] = *req.PasswordLoginDisabled } if shouldUpdateAIAPIKey { updates["ai_api_key"] = model.SecretString(aiAPIKey) @@ -313,18 +315,18 @@ func HandleUpdateGeneralSetting(c *gin.Context) { hasAIAPIKey := strings.TrimSpace(string(updated.AIAPIKey)) != "" c.JSON(http.StatusOK, gin.H{ - "aiAgentEnabled": updated.AIAgentEnabled, - "aiProvider": updated.AIProvider, - "aiModel": updated.AIModel, - "aiApiKey": "", - "aiApiKeyConfigured": hasAIAPIKey, - "aiBaseUrl": updated.AIBaseURL, - "aiMaxTokens": updated.AIMaxTokens, - "kubectlEnabled": updated.KubectlEnabled, - "kubectlImage": updated.KubectlImage, - "nodeTerminalImage": updated.NodeTerminalImage, - "enableAnalytics": updated.EnableAnalytics, - "enableVersionCheck": updated.EnableVersionCheck, + "aiAgentEnabled": updated.AIAgentEnabled, + "aiProvider": updated.AIProvider, + "aiModel": updated.AIModel, + "aiApiKey": "", + "aiApiKeyConfigured": hasAIAPIKey, + "aiBaseUrl": updated.AIBaseURL, + "aiMaxTokens": updated.AIMaxTokens, + "kubectlEnabled": updated.KubectlEnabled, + "kubectlImage": updated.KubectlImage, + "nodeTerminalImage": updated.NodeTerminalImage, + "enableAnalytics": updated.EnableAnalytics, + "enableVersionCheck": updated.EnableVersionCheck, "passwordLoginDisabled": updated.PasswordLoginDisabled, }) } diff --git a/ui/src/components/settings/authentication-management.tsx b/ui/src/components/settings/authentication-management.tsx index 5382efc0..216fc702 100644 --- a/ui/src/components/settings/authentication-management.tsx +++ b/ui/src/components/settings/authentication-management.tsx @@ -1,5 +1,5 @@ import { useEffect, useState } from 'react' -import { IconKey, IconAlertTriangle } from '@tabler/icons-react' +import { IconAlertTriangle, IconKey } from '@tabler/icons-react' import { useMutation, useQueryClient } from '@tanstack/react-query' import { useTranslation } from 'react-i18next' import { toast } from 'sonner' @@ -7,10 +7,10 @@ import { toast } from 'sonner' import { LDAPSetting, LDAPSettingUpdateRequest, - updateLDAPSetting, updateGeneralSetting, - useLDAPSetting, + updateLDAPSetting, useGeneralSetting, + useLDAPSetting, } from '@/lib/api' import { translateError } from '@/lib/utils' import { Button } from '@/components/ui/button' @@ -84,7 +84,10 @@ export function AuthenticationManagement() { }, [generalSetting]) const mutation = useMutation({ - mutationFn: (params: { ldap: LDAPSettingUpdateRequest; passwordLoginDisabled: boolean }) => + mutationFn: (params: { + ldap: LDAPSettingUpdateRequest + passwordLoginDisabled: boolean + }) => Promise.all([ updateLDAPSetting(params.ldap), updateGeneralSetting({ @@ -494,7 +497,10 @@ export function AuthenticationManagement() {
-
From c7e47bc8a626e9e9e28f694cd8315080594590c9 Mon Sep 17 00:00:00 2001 From: Zzde Date: Sun, 19 Apr 2026 21:43:06 +0800 Subject: [PATCH 4/4] fix e2e Signed-off-by: Zzde --- e2e/specs/external-auth.spec.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/e2e/specs/external-auth.spec.ts b/e2e/specs/external-auth.spec.ts index 12ec4f54..f36f2cd9 100644 --- a/e2e/specs/external-auth.spec.ts +++ b/e2e/specs/external-auth.spec.ts @@ -104,7 +104,11 @@ async function configureLDAPViaUI(page: Page) { await page.goto('/settings?tab=oauth') await expect(page.getByRole('heading', { name: 'Settings' })).toBeVisible() - const ldapSwitch = page.getByRole('switch').first() + const ldapSection = page + .locator('div.rounded-lg.border') + .filter({ has: page.getByText(/^LDAP$/) }) + .first() + const ldapSwitch = ldapSection.getByRole('switch') if ((await ldapSwitch.getAttribute('data-state')) !== 'checked') { await ldapSwitch.click() }