diff --git a/core/http/react-ui/src/components/RequireAuth.jsx b/core/http/react-ui/src/components/RequireAuth.jsx
index 57268961a..b4a6b2b56 100644
--- a/core/http/react-ui/src/components/RequireAuth.jsx
+++ b/core/http/react-ui/src/components/RequireAuth.jsx
@@ -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
+ if ((authEnabled || staticApiKeyRequired) && !user) return
return children
}
diff --git a/core/http/react-ui/src/context/AuthContext.jsx b/core/http/react-ui/src/context/AuthContext.jsx
index 5fd33f0ad..9e3de7f29 100644
--- a/core/http/react-ui/src/context/AuthContext.jsx
+++ b/core/http/react-ui/src/context/AuthContext.jsx
@@ -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,
diff --git a/core/http/react-ui/src/pages/Login.jsx b/core/http/react-ui/src/pages/Login.jsx
index 34d02c8f4..d8aa19c98 100644
--- a/core/http/react-ui/src/pages/Login.jsx
+++ b/core/http/react-ui/src/pages/Login.jsx
@@ -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 (
+
+
+
+
})
+
Enter your API key to continue
+
+
+ {error && (
+
{error}
+ )}
+
+
+
+
+ )
+ }
+
const hasGitHub = providers.includes('github')
const hasOIDC = providers.includes('oidc')
const hasLocal = providers.includes('local')
diff --git a/core/http/routes/auth.go b/core/http/routes/auth.go
index f44c569e9..e66b3c69e 100644
--- a/core/http/routes/auth.go
+++ b/core/http/routes/auth.go
@@ -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
diff --git a/core/http/routes/auth_test.go b/core/http/routes/auth_test.go
index a6a6e3f8a..e9575e59f 100644
--- a/core/http/routes/auth_test.go
+++ b/core/http/routes/auth_test.go
@@ -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() {