@@ -22,6 +22,8 @@ import {
2222 useSshPort ,
2323 useLinearApiKey ,
2424 useGitHubPat ,
25+ useBasicAuthUsername ,
26+ useBasicAuthPassword ,
2527 useUpdateConfig ,
2628 useResetConfig ,
2729 useNotificationSettings ,
@@ -59,6 +61,8 @@ function SettingsPage() {
5961 const { data : sshPort , isLoading : sshPortLoading } = useSshPort ( )
6062 const { data : linearApiKey , isLoading : linearApiKeyLoading } = useLinearApiKey ( )
6163 const { data : githubPat , isLoading : githubPatLoading } = useGitHubPat ( )
64+ const { data : basicAuthUsername , isLoading : basicAuthUsernameLoading } = useBasicAuthUsername ( )
65+ const { data : basicAuthPassword , isLoading : basicAuthPasswordLoading } = useBasicAuthPassword ( )
6266 const { data : notificationSettings , isLoading : notificationsLoading } = useNotificationSettings ( )
6367 const { data : zAiSettings , isLoading : zAiLoading } = useZAiSettings ( )
6468 const { data : developerMode } = useDeveloperMode ( )
@@ -76,6 +80,8 @@ function SettingsPage() {
7680 const [ localSshPort , setLocalSshPort ] = useState ( '' )
7781 const [ localLinearApiKey , setLocalLinearApiKey ] = useState ( '' )
7882 const [ localGitHubPat , setLocalGitHubPat ] = useState ( '' )
83+ const [ localBasicAuthUsername , setLocalBasicAuthUsername ] = useState ( '' )
84+ const [ localBasicAuthPassword , setLocalBasicAuthPassword ] = useState ( '' )
7985 const [ reposDirBrowserOpen , setReposDirBrowserOpen ] = useState ( false )
8086 const [ saved , setSaved ] = useState ( false )
8187
@@ -108,7 +114,10 @@ function SettingsPage() {
108114 if ( sshPort !== undefined ) setLocalSshPort ( String ( sshPort ) )
109115 if ( linearApiKey !== undefined ) setLocalLinearApiKey ( linearApiKey )
110116 if ( githubPat !== undefined ) setLocalGitHubPat ( githubPat )
111- } , [ port , defaultGitReposDir , hostname , sshPort , linearApiKey , githubPat ] )
117+ // For username, sync directly. For password, the server returns masked value - only update if empty (not yet loaded)
118+ if ( basicAuthUsername !== undefined ) setLocalBasicAuthUsername ( basicAuthUsername )
119+ // Don't sync password from server since it's masked - user must re-enter to change
120+ } , [ port , defaultGitReposDir , hostname , sshPort , linearApiKey , githubPat , basicAuthUsername ] )
112121
113122 // Sync notification settings
114123 useEffect ( ( ) => {
@@ -137,7 +146,7 @@ function SettingsPage() {
137146 } , [ zAiSettings ] )
138147
139148 const isLoading =
140- portLoading || reposDirLoading || hostnameLoading || sshPortLoading || linearApiKeyLoading || githubPatLoading || notificationsLoading || zAiLoading
149+ portLoading || reposDirLoading || hostnameLoading || sshPortLoading || linearApiKeyLoading || githubPatLoading || basicAuthUsernameLoading || basicAuthPasswordLoading || notificationsLoading || zAiLoading
141150
142151 const hasZAiChanges = zAiSettings && (
143152 zAiEnabled !== zAiSettings . enabled ||
@@ -159,13 +168,19 @@ function SettingsPage() {
159168 pushoverUserKey !== ( notificationSettings . pushover ?. userKey ?? '' )
160169 )
161170
171+ // Auth has changes if username differs OR password is non-empty (user wants to update it)
172+ const hasAuthChanges =
173+ localBasicAuthUsername !== basicAuthUsername ||
174+ localBasicAuthPassword !== ''
175+
162176 const hasChanges =
163177 localPort !== String ( port ) ||
164178 localReposDir !== defaultGitReposDir ||
165179 localHostname !== hostname ||
166180 localSshPort !== String ( sshPort ) ||
167181 localLinearApiKey !== linearApiKey ||
168182 localGitHubPat !== githubPat ||
183+ hasAuthChanges ||
169184 hasNotificationChanges ||
170185 hasZAiChanges
171186
@@ -241,6 +256,31 @@ function SettingsPage() {
241256 )
242257 }
243258
259+ // Save auth settings
260+ if ( localBasicAuthUsername !== basicAuthUsername ) {
261+ promises . push (
262+ new Promise ( ( resolve ) => {
263+ updateConfig . mutate (
264+ { key : CONFIG_KEYS . BASIC_AUTH_USERNAME , value : localBasicAuthUsername } ,
265+ { onSettled : resolve }
266+ )
267+ } )
268+ )
269+ }
270+
271+ if ( localBasicAuthPassword !== '' ) {
272+ promises . push (
273+ new Promise ( ( resolve ) => {
274+ updateConfig . mutate (
275+ { key : CONFIG_KEYS . BASIC_AUTH_PASSWORD , value : localBasicAuthPassword } ,
276+ { onSettled : resolve }
277+ )
278+ } )
279+ )
280+ // Clear local password after saving
281+ setLocalBasicAuthPassword ( '' )
282+ }
283+
244284 // Save notification settings
245285 if ( hasNotificationChanges ) {
246286 promises . push (
@@ -332,6 +372,22 @@ function SettingsPage() {
332372 } )
333373 }
334374
375+ const handleResetBasicAuthUsername = ( ) => {
376+ resetConfig . mutate ( CONFIG_KEYS . BASIC_AUTH_USERNAME , {
377+ onSuccess : ( data ) => {
378+ setLocalBasicAuthUsername ( data . value !== null && data . value !== undefined ? String ( data . value ) : '' )
379+ } ,
380+ } )
381+ }
382+
383+ const handleResetBasicAuthPassword = ( ) => {
384+ resetConfig . mutate ( CONFIG_KEYS . BASIC_AUTH_PASSWORD , {
385+ onSuccess : ( ) => {
386+ setLocalBasicAuthPassword ( '' )
387+ } ,
388+ } )
389+ }
390+
335391 const handleTestChannel = async ( channel : 'sound' | 'slack' | 'discord' | 'pushover' ) => {
336392 testChannel . mutate ( channel , {
337393 onSuccess : ( result ) => {
@@ -432,6 +488,72 @@ function SettingsPage() {
432488 </ div >
433489 </ SettingsSection >
434490
491+ { /* Authentication */ }
492+ < SettingsSection title = { t ( 'sections.authentication' ) } >
493+ < div className = "space-y-4" >
494+ { /* Username */ }
495+ < div className = "space-y-1" >
496+ < div className = "flex flex-col gap-2 sm:flex-row sm:items-center" >
497+ < label className = "text-sm text-muted-foreground sm:w-32 sm:shrink-0" >
498+ { t ( 'fields.auth.username' ) }
499+ </ label >
500+ < div className = "flex flex-1 items-center gap-2" >
501+ < Input
502+ value = { localBasicAuthUsername }
503+ onChange = { ( e ) => setLocalBasicAuthUsername ( e . target . value ) }
504+ placeholder = { t ( 'fields.auth.usernamePlaceholder' ) }
505+ disabled = { isLoading }
506+ className = "flex-1 font-mono text-sm"
507+ />
508+ < Button
509+ variant = "ghost"
510+ size = "icon"
511+ className = "h-8 w-8 text-muted-foreground hover:text-foreground"
512+ onClick = { handleResetBasicAuthUsername }
513+ disabled = { isLoading || resetConfig . isPending }
514+ title = { tc ( 'buttons.reset' ) }
515+ >
516+ < HugeiconsIcon icon = { RotateLeft01Icon } size = { 14 } strokeWidth = { 2 } />
517+ </ Button >
518+ </ div >
519+ </ div >
520+ </ div >
521+
522+ { /* Password */ }
523+ < div className = "space-y-1" >
524+ < div className = "flex flex-col gap-2 sm:flex-row sm:items-center" >
525+ < label className = "text-sm text-muted-foreground sm:w-32 sm:shrink-0" >
526+ { t ( 'fields.auth.password' ) }
527+ </ label >
528+ < div className = "flex flex-1 items-center gap-2" >
529+ < Input
530+ type = "password"
531+ value = { localBasicAuthPassword }
532+ onChange = { ( e ) => setLocalBasicAuthPassword ( e . target . value ) }
533+ placeholder = { basicAuthPassword ? t ( 'fields.auth.passwordSet' ) : t ( 'fields.auth.passwordPlaceholder' ) }
534+ disabled = { isLoading }
535+ className = "flex-1 font-mono text-sm"
536+ />
537+ < Button
538+ variant = "ghost"
539+ size = "icon"
540+ className = "h-8 w-8 text-muted-foreground hover:text-foreground"
541+ onClick = { handleResetBasicAuthPassword }
542+ disabled = { isLoading || resetConfig . isPending }
543+ title = { tc ( 'buttons.reset' ) }
544+ >
545+ < HugeiconsIcon icon = { RotateLeft01Icon } size = { 14 } strokeWidth = { 2 } />
546+ </ Button >
547+ </ div >
548+ </ div >
549+ </ div >
550+
551+ < p className = "text-xs text-muted-foreground" >
552+ { t ( 'fields.auth.description' ) }
553+ </ p >
554+ </ div >
555+ </ SettingsSection >
556+
435557 { /* Remote Access + Integrations side by side */ }
436558 < div className = "grid grid-cols-1 gap-4 sm:grid-cols-2" >
437559 { /* Remote Access */ }
0 commit comments