1- import { useState , type FormEvent } from 'react' ;
1+ import { useState , useEffect , type FormEvent } from 'react' ;
22import { Navigate , useLocation } from 'react-router-dom' ;
33import { useAuthStore } from '../stores/authStore' ;
44import { api } from '../api/client' ;
5+ import type { HealthAuthConfig } from '../types' ;
56
67export function LoginPage ( ) {
78 const [ username , setUsername ] = useState ( '' ) ;
89 const [ password , setPassword ] = useState ( '' ) ;
10+ const [ apiAvailable , setApiAvailable ] = useState < boolean | null > ( null ) ;
11+ const [ authConfig , setAuthConfig ] = useState < HealthAuthConfig | null > ( null ) ;
912 const { login, isAuthenticated, isLoading, error, clearError } = useAuthStore ( ) ;
1013 const location = useLocation ( ) ;
1114
1215 const from = ( location . state as { from ?: { pathname : string } } ) ?. from ?. pathname || '/' ;
1316
17+ // Check API health on mount and periodically
18+ useEffect ( ( ) => {
19+ let mounted = true ;
20+
21+ const checkHealth = async ( ) => {
22+ try {
23+ const health = await api . getHealth ( ) ;
24+ if ( mounted ) {
25+ setApiAvailable ( true ) ;
26+ setAuthConfig ( health . config . auth ) ;
27+ }
28+ } catch {
29+ if ( mounted ) {
30+ setApiAvailable ( false ) ;
31+ setAuthConfig ( null ) ;
32+ }
33+ }
34+ } ;
35+
36+ checkHealth ( ) ;
37+
38+ // Poll more frequently when API is unavailable
39+ const interval = setInterval ( checkHealth , apiAvailable === false ? 5000 : 30000 ) ;
40+
41+ return ( ) => {
42+ mounted = false ;
43+ clearInterval ( interval ) ;
44+ } ;
45+ } , [ apiAvailable ] ) ;
46+
1447 if ( isAuthenticated ) {
1548 return < Navigate to = { from } replace /> ;
1649 }
@@ -29,6 +62,10 @@ export function LoginPage() {
2962 window . location . href = api . getGitHubAuthUrl ( ) ;
3063 } ;
3164
65+ const showBasicAuth = authConfig ?. basic ?? false ;
66+ const showGitHubAuth = authConfig ?. github ?? false ;
67+ const noAuthConfigured = authConfig !== null && ! showBasicAuth && ! showGitHubAuth ;
68+
3269 return (
3370 < div className = "min-h-dvh flex items-center justify-center bg-zinc-950 px-4" >
3471 < div className = "w-full max-w-sm" >
@@ -43,68 +80,106 @@ export function LoginPage() {
4380 </ div >
4481
4582 < div className = "bg-zinc-900 rounded-lg p-6 border border-zinc-800" >
46- < form onSubmit = { handleSubmit } className = "space-y-4" >
47- { error && (
48- < div className = "bg-red-500/10 border border-red-500/20 text-red-400 px-4 py-3 rounded-sm text-sm" >
49- { error }
83+ { /* API unavailable warning */ }
84+ { apiAvailable === false && (
85+ < div className = "bg-red-500/10 border border-red-500/20 text-red-400 px-4 py-3 rounded-sm text-sm mb-4" >
86+ < p className = "font-medium" > Unable to connect to API server</ p >
87+ < p className = "mt-1 text-red-400/80" > Please check if the server is running.</ p >
88+ </ div >
89+ ) }
90+
91+ { /* Loading state while checking API */ }
92+ { apiAvailable === null && (
93+ < div className = "flex items-center justify-center py-8" >
94+ < div className = "flex items-center gap-2 text-zinc-400" >
95+ < svg className = "size-5 animate-spin" fill = "none" viewBox = "0 0 24 24" >
96+ < circle className = "opacity-25" cx = "12" cy = "12" r = "10" stroke = "currentColor" strokeWidth = "4" />
97+ < path className = "opacity-75" fill = "currentColor" d = "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
98+ </ svg >
99+ < span > Connecting to API...</ span >
50100 </ div >
51- ) }
52-
53- < div >
54- < label htmlFor = "username" className = "block text-sm font-medium text-zinc-300 mb-1" >
55- Username
56- </ label >
57- < input
58- id = "username"
59- type = "text"
60- value = { username }
61- onChange = { ( e ) => setUsername ( e . target . value ) }
62- className = "w-full px-3 py-2 bg-zinc-800 border border-zinc-700 rounded-sm text-white placeholder-zinc-500 focus:outline-hidden focus:ring-2 focus:ring-blue-500 focus:border-transparent"
63- placeholder = "Enter your username"
64- required
65- disabled = { isLoading }
66- />
67101 </ div >
102+ ) }
68103
69- < div >
70- < label htmlFor = "password" className = "block text-sm font-medium text-zinc-300 mb-1" >
71- Password
72- </ label >
73- < input
74- id = "password"
75- type = "password"
76- value = { password }
77- onChange = { ( e ) => setPassword ( e . target . value ) }
78- className = "w-full px-3 py-2 bg-zinc-800 border border-zinc-700 rounded-sm text-white placeholder-zinc-500 focus:outline-hidden focus:ring-2 focus:ring-blue-500 focus:border-transparent"
79- placeholder = "Enter your password"
80- required
81- disabled = { isLoading }
82- />
104+ { /* No auth methods configured */ }
105+ { noAuthConfigured && (
106+ < div className = "text-center py-4 text-zinc-400" >
107+ < p > No authentication methods configured.</ p >
108+ < p className = "text-sm mt-1" > Please contact your administrator.</ p >
83109 </ div >
110+ ) }
84111
85- < button
86- type = "submit"
87- disabled = { isLoading }
88- className = "w-full py-2 px-4 bg-blue-600 hover:bg-blue-700 disabled:bg-blue-600/50 text-white font-medium rounded-sm transition-colors focus:outline-hidden focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 focus:ring-offset-zinc-900"
89- >
90- { isLoading ? 'Signing in...' : 'Sign in' }
91- </ button >
92- </ form >
112+ { /* Basic auth form */ }
113+ { showBasicAuth && (
114+ < form onSubmit = { handleSubmit } className = "space-y-4" >
115+ { error && (
116+ < div className = "bg-red-500/10 border border-red-500/20 text-red-400 px-4 py-3 rounded-sm text-sm" >
117+ { error }
118+ </ div >
119+ ) }
120+
121+ < div >
122+ < label htmlFor = "username" className = "block text-sm font-medium text-zinc-300 mb-1" >
123+ Username
124+ </ label >
125+ < input
126+ id = "username"
127+ type = "text"
128+ value = { username }
129+ onChange = { ( e ) => setUsername ( e . target . value ) }
130+ className = "w-full px-3 py-2 bg-zinc-800 border border-zinc-700 rounded-sm text-white placeholder-zinc-500 focus:outline-hidden focus:ring-2 focus:ring-blue-500 focus:border-transparent"
131+ placeholder = "Enter your username"
132+ required
133+ disabled = { isLoading || apiAvailable === false }
134+ />
135+ </ div >
93136
94- < div className = "mt-6" >
95- < div className = "relative" >
96- < div className = "absolute inset-0 flex items-center" >
97- < div className = "w-full border-t border-zinc-700" />
137+ < div >
138+ < label htmlFor = "password" className = "block text-sm font-medium text-zinc-300 mb-1" >
139+ Password
140+ </ label >
141+ < input
142+ id = "password"
143+ type = "password"
144+ value = { password }
145+ onChange = { ( e ) => setPassword ( e . target . value ) }
146+ className = "w-full px-3 py-2 bg-zinc-800 border border-zinc-700 rounded-sm text-white placeholder-zinc-500 focus:outline-hidden focus:ring-2 focus:ring-blue-500 focus:border-transparent"
147+ placeholder = "Enter your password"
148+ required
149+ disabled = { isLoading || apiAvailable === false }
150+ />
98151 </ div >
99- < div className = "relative flex justify-center text-sm" >
100- < span className = "px-2 bg-zinc-900 text-zinc-500" > Or continue with</ span >
152+
153+ < button
154+ type = "submit"
155+ disabled = { isLoading || apiAvailable === false }
156+ className = "w-full py-2 px-4 bg-blue-600 hover:bg-blue-700 disabled:bg-blue-600/50 text-white font-medium rounded-sm transition-colors focus:outline-hidden focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 focus:ring-offset-zinc-900"
157+ >
158+ { isLoading ? 'Signing in...' : 'Sign in' }
159+ </ button >
160+ </ form >
161+ ) }
162+
163+ { /* Divider between basic auth and GitHub auth */ }
164+ { showBasicAuth && showGitHubAuth && (
165+ < div className = "mt-6" >
166+ < div className = "relative" >
167+ < div className = "absolute inset-0 flex items-center" >
168+ < div className = "w-full border-t border-zinc-700" />
169+ </ div >
170+ < div className = "relative flex justify-center text-sm" >
171+ < span className = "px-2 bg-zinc-900 text-zinc-500" > Or continue with</ span >
172+ </ div >
101173 </ div >
102174 </ div >
175+ ) }
103176
177+ { /* GitHub auth button */ }
178+ { showGitHubAuth && (
104179 < button
105180 onClick = { handleGitHubLogin }
106- disabled = { isLoading }
107- className = "mt-4 w-full py-2 px-4 bg-zinc-800 hover:bg-zinc-700 disabled:bg-zinc-800/50 text-white font-medium rounded-sm transition-colors border border-zinc-700 flex items-center justify-center gap-2"
181+ disabled = { isLoading || apiAvailable === false }
182+ className = { ` w-full py-2 px-4 bg-zinc-800 hover:bg-zinc-700 disabled:bg-zinc-800/50 text-white font-medium rounded-sm transition-colors border border-zinc-700 flex items-center justify-center gap-2 ${ showBasicAuth ? 'mt-4' : '' } ` }
108183 >
109184 < svg className = "size-5" fill = "currentColor" viewBox = "0 0 24 24" >
110185 < path
@@ -113,9 +188,9 @@ export function LoginPage() {
113188 d = "M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"
114189 />
115190 </ svg >
116- GitHub
191+ Sign in with GitHub
117192 </ button >
118- </ div >
193+ ) }
119194 </ div >
120195 </ div >
121196 </ div >
0 commit comments