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>
This commit is contained in:
Ettore Di Giacinto
2026-04-04 12:11:22 +02:00
committed by GitHub
parent 7962dd16f7
commit 84e51b68ef
5 changed files with 78 additions and 14 deletions

View File

@@ -2,8 +2,8 @@ import { Navigate } from 'react-router-dom'
import { useAuth } from '../context/AuthContext'
export default function RequireAuth({ children }) {
const { authEnabled, user, loading } = useAuth()
const { authEnabled, staticApiKeyRequired, user, loading } = useAuth()
if (loading) return null
if (authEnabled && !user) return <Navigate to="/login" replace />
if ((authEnabled || staticApiKeyRequired) && !user) return <Navigate to="/login" replace />
return children
}

View File

@@ -7,6 +7,7 @@ export function AuthProvider({ children }) {
const [state, setState] = useState({
loading: true,
authEnabled: false,
staticApiKeyRequired: false,
user: null,
permissions: {},
})
@@ -20,12 +21,13 @@ export function AuthProvider({ children }) {
setState({
loading: false,
authEnabled: data.authEnabled || false,
staticApiKeyRequired: data.staticApiKeyRequired || false,
user,
permissions,
})
})
.catch(() => {
setState({ loading: false, authEnabled: false, user: null, permissions: {} })
setState({ loading: false, authEnabled: false, staticApiKeyRequired: false, user: null, permissions: {} })
})
}
@@ -45,17 +47,20 @@ export function AuthProvider({ children }) {
const refresh = () => fetchStatus()
const noAuthRequired = !state.authEnabled && !state.staticApiKeyRequired
const hasFeature = (name) => {
if (state.user?.role === 'admin' || !state.authEnabled) return true
if (state.user?.role === 'admin' || noAuthRequired) return true
return !!state.permissions[name]
}
const value = {
loading: state.loading,
authEnabled: state.authEnabled,
staticApiKeyRequired: state.staticApiKeyRequired,
user: state.user,
permissions: state.permissions,
isAdmin: state.user?.role === 'admin' || !state.authEnabled,
isAdmin: state.user?.role === 'admin' || noAuthRequired,
hasFeature,
logout,
refresh,

View File

@@ -8,7 +8,7 @@ export default function Login() {
const navigate = useNavigate()
const { code: urlInviteCode } = useParams()
const [searchParams] = useSearchParams()
const { authEnabled, user, loading: authLoading, refresh } = useAuth()
const { authEnabled, staticApiKeyRequired, user, loading: authLoading, refresh } = useAuth()
const [providers, setProviders] = useState([])
const [hasUsers, setHasUsers] = useState(true)
const [registrationMode, setRegistrationMode] = useState('open')
@@ -66,7 +66,7 @@ export default function Login() {
// Redirect if auth is disabled or user is already logged in
useEffect(() => {
if (!authLoading && (!authEnabled || user)) {
if (!authLoading && ((!authEnabled && !staticApiKeyRequired) || user)) {
navigate('/app', { replace: true })
}
}, [authLoading, authEnabled, user, navigate])
@@ -176,6 +176,40 @@ export default function Login() {
if (authLoading || statusLoading) return null
// Legacy API key-only mode: show a simplified login with just the token input
if (staticApiKeyRequired && !authEnabled) {
return (
<div className="login-page">
<div className="card login-card">
<div className="login-header">
<img src={apiUrl('/static/logo.png')} alt="LocalAI" className="login-logo" />
<p className="login-subtitle">Enter your API key to continue</p>
</div>
{error && (
<div className="login-alert login-alert-error">{error}</div>
)}
<form onSubmit={handleTokenLogin}>
<div className="form-group">
<input
className="input"
type="password"
value={token}
onChange={(e) => { setToken(e.target.value); setError('') }}
placeholder="Enter API key..."
autoFocus
/>
</div>
<button type="submit" className="btn btn-primary login-btn-full" disabled={submitting}>
{submitting ? 'Signing in...' : 'Sign In'}
</button>
</form>
</div>
</div>
)
}
const hasGitHub = providers.includes('github')
const hasOIDC = providers.includes('oidc')
const hasLocal = providers.includes('local')

View File

@@ -157,10 +157,11 @@ func RegisterAuthRoutes(e *echo.Echo, app *application.Application) {
}
resp := map[string]any{
"authEnabled": authEnabled,
"providers": providers,
"hasUsers": hasUsers,
"registrationMode": registrationMode,
"authEnabled": authEnabled,
"staticApiKeyRequired": !authEnabled && len(appConfig.ApiKeys) > 0,
"providers": providers,
"hasUsers": hasUsers,
"registrationMode": registrationMode,
}
// Include current user if authenticated

View File

@@ -45,9 +45,10 @@ func newTestAuthApp(db *gorm.DB, appConfig *config.ApplicationConfig) *echo.Echo
}
resp := map[string]any{
"authEnabled": authEnabled,
"providers": providers,
"hasUsers": hasUsers,
"authEnabled": authEnabled,
"staticApiKeyRequired": !authEnabled && len(appConfig.ApiKeys) > 0,
"providers": providers,
"hasUsers": hasUsers,
}
user := auth.GetUser(c)
@@ -407,6 +408,29 @@ var _ = Describe("Auth Routes", Label("auth"), func() {
json.Unmarshal(rec.Body.Bytes(), &resp)
Expect(resp["hasUsers"]).To(BeFalse())
})
It("returns staticApiKeyRequired=true when no DB but API keys configured", func() {
cfg := config.NewApplicationConfig()
config.WithApiKeys([]string{"test-key-123"})(cfg)
app := newTestAuthApp(nil, cfg)
rec := doAuthRequest(app, "GET", "/api/auth/status", nil)
Expect(rec.Code).To(Equal(http.StatusOK))
var resp map[string]any
json.Unmarshal(rec.Body.Bytes(), &resp)
Expect(resp["authEnabled"]).To(BeFalse())
Expect(resp["staticApiKeyRequired"]).To(BeTrue())
})
It("returns staticApiKeyRequired=false when no DB and no API keys", func() {
app := newTestAuthApp(nil, config.NewApplicationConfig())
rec := doAuthRequest(app, "GET", "/api/auth/status", nil)
Expect(rec.Code).To(Equal(http.StatusOK))
var resp map[string]any
json.Unmarshal(rec.Body.Bytes(), &resp)
Expect(resp["staticApiKeyRequired"]).To(BeFalse())
})
})
Context("POST /api/auth/logout", func() {