Skip to content

Commit 84e51b6

Browse files
authored
fix(ui): pass by staticApiKeyRequired to show login when only api key is configured (#9220)
This fixes #9213 Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
1 parent 7962dd1 commit 84e51b6

5 files changed

Lines changed: 78 additions & 14 deletions

File tree

core/http/react-ui/src/components/RequireAuth.jsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ import { Navigate } from 'react-router-dom'
22
import { useAuth } from '../context/AuthContext'
33

44
export default function RequireAuth({ children }) {
5-
const { authEnabled, user, loading } = useAuth()
5+
const { authEnabled, staticApiKeyRequired, user, loading } = useAuth()
66
if (loading) return null
7-
if (authEnabled && !user) return <Navigate to="/login" replace />
7+
if ((authEnabled || staticApiKeyRequired) && !user) return <Navigate to="/login" replace />
88
return children
99
}

core/http/react-ui/src/context/AuthContext.jsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export function AuthProvider({ children }) {
77
const [state, setState] = useState({
88
loading: true,
99
authEnabled: false,
10+
staticApiKeyRequired: false,
1011
user: null,
1112
permissions: {},
1213
})
@@ -20,12 +21,13 @@ export function AuthProvider({ children }) {
2021
setState({
2122
loading: false,
2223
authEnabled: data.authEnabled || false,
24+
staticApiKeyRequired: data.staticApiKeyRequired || false,
2325
user,
2426
permissions,
2527
})
2628
})
2729
.catch(() => {
28-
setState({ loading: false, authEnabled: false, user: null, permissions: {} })
30+
setState({ loading: false, authEnabled: false, staticApiKeyRequired: false, user: null, permissions: {} })
2931
})
3032
}
3133

@@ -45,17 +47,20 @@ export function AuthProvider({ children }) {
4547

4648
const refresh = () => fetchStatus()
4749

50+
const noAuthRequired = !state.authEnabled && !state.staticApiKeyRequired
51+
4852
const hasFeature = (name) => {
49-
if (state.user?.role === 'admin' || !state.authEnabled) return true
53+
if (state.user?.role === 'admin' || noAuthRequired) return true
5054
return !!state.permissions[name]
5155
}
5256

5357
const value = {
5458
loading: state.loading,
5559
authEnabled: state.authEnabled,
60+
staticApiKeyRequired: state.staticApiKeyRequired,
5661
user: state.user,
5762
permissions: state.permissions,
58-
isAdmin: state.user?.role === 'admin' || !state.authEnabled,
63+
isAdmin: state.user?.role === 'admin' || noAuthRequired,
5964
hasFeature,
6065
logout,
6166
refresh,

core/http/react-ui/src/pages/Login.jsx

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ export default function Login() {
88
const navigate = useNavigate()
99
const { code: urlInviteCode } = useParams()
1010
const [searchParams] = useSearchParams()
11-
const { authEnabled, user, loading: authLoading, refresh } = useAuth()
11+
const { authEnabled, staticApiKeyRequired, user, loading: authLoading, refresh } = useAuth()
1212
const [providers, setProviders] = useState([])
1313
const [hasUsers, setHasUsers] = useState(true)
1414
const [registrationMode, setRegistrationMode] = useState('open')
@@ -66,7 +66,7 @@ export default function Login() {
6666

6767
// Redirect if auth is disabled or user is already logged in
6868
useEffect(() => {
69-
if (!authLoading && (!authEnabled || user)) {
69+
if (!authLoading && ((!authEnabled && !staticApiKeyRequired) || user)) {
7070
navigate('/app', { replace: true })
7171
}
7272
}, [authLoading, authEnabled, user, navigate])
@@ -176,6 +176,40 @@ export default function Login() {
176176

177177
if (authLoading || statusLoading) return null
178178

179+
// Legacy API key-only mode: show a simplified login with just the token input
180+
if (staticApiKeyRequired && !authEnabled) {
181+
return (
182+
<div className="login-page">
183+
<div className="card login-card">
184+
<div className="login-header">
185+
<img src={apiUrl('/static/logo.png')} alt="LocalAI" className="login-logo" />
186+
<p className="login-subtitle">Enter your API key to continue</p>
187+
</div>
188+
189+
{error && (
190+
<div className="login-alert login-alert-error">{error}</div>
191+
)}
192+
193+
<form onSubmit={handleTokenLogin}>
194+
<div className="form-group">
195+
<input
196+
className="input"
197+
type="password"
198+
value={token}
199+
onChange={(e) => { setToken(e.target.value); setError('') }}
200+
placeholder="Enter API key..."
201+
autoFocus
202+
/>
203+
</div>
204+
<button type="submit" className="btn btn-primary login-btn-full" disabled={submitting}>
205+
{submitting ? 'Signing in...' : 'Sign In'}
206+
</button>
207+
</form>
208+
</div>
209+
</div>
210+
)
211+
}
212+
179213
const hasGitHub = providers.includes('github')
180214
const hasOIDC = providers.includes('oidc')
181215
const hasLocal = providers.includes('local')

core/http/routes/auth.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -157,10 +157,11 @@ func RegisterAuthRoutes(e *echo.Echo, app *application.Application) {
157157
}
158158

159159
resp := map[string]any{
160-
"authEnabled": authEnabled,
161-
"providers": providers,
162-
"hasUsers": hasUsers,
163-
"registrationMode": registrationMode,
160+
"authEnabled": authEnabled,
161+
"staticApiKeyRequired": !authEnabled && len(appConfig.ApiKeys) > 0,
162+
"providers": providers,
163+
"hasUsers": hasUsers,
164+
"registrationMode": registrationMode,
164165
}
165166

166167
// Include current user if authenticated

core/http/routes/auth_test.go

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,10 @@ func newTestAuthApp(db *gorm.DB, appConfig *config.ApplicationConfig) *echo.Echo
4545
}
4646

4747
resp := map[string]any{
48-
"authEnabled": authEnabled,
49-
"providers": providers,
50-
"hasUsers": hasUsers,
48+
"authEnabled": authEnabled,
49+
"staticApiKeyRequired": !authEnabled && len(appConfig.ApiKeys) > 0,
50+
"providers": providers,
51+
"hasUsers": hasUsers,
5152
}
5253

5354
user := auth.GetUser(c)
@@ -407,6 +408,29 @@ var _ = Describe("Auth Routes", Label("auth"), func() {
407408
json.Unmarshal(rec.Body.Bytes(), &resp)
408409
Expect(resp["hasUsers"]).To(BeFalse())
409410
})
411+
412+
It("returns staticApiKeyRequired=true when no DB but API keys configured", func() {
413+
cfg := config.NewApplicationConfig()
414+
config.WithApiKeys([]string{"test-key-123"})(cfg)
415+
app := newTestAuthApp(nil, cfg)
416+
rec := doAuthRequest(app, "GET", "/api/auth/status", nil)
417+
Expect(rec.Code).To(Equal(http.StatusOK))
418+
419+
var resp map[string]any
420+
json.Unmarshal(rec.Body.Bytes(), &resp)
421+
Expect(resp["authEnabled"]).To(BeFalse())
422+
Expect(resp["staticApiKeyRequired"]).To(BeTrue())
423+
})
424+
425+
It("returns staticApiKeyRequired=false when no DB and no API keys", func() {
426+
app := newTestAuthApp(nil, config.NewApplicationConfig())
427+
rec := doAuthRequest(app, "GET", "/api/auth/status", nil)
428+
Expect(rec.Code).To(Equal(http.StatusOK))
429+
430+
var resp map[string]any
431+
json.Unmarshal(rec.Body.Bytes(), &resp)
432+
Expect(resp["staticApiKeyRequired"]).To(BeFalse())
433+
})
410434
})
411435

412436
Context("POST /api/auth/logout", func() {

0 commit comments

Comments
 (0)