mirror of
https://github.com/sipeed/picoclaw.git
synced 2026-06-12 18:08:54 +00:00
fix(launcher): fall back to token auth on unsupported platforms (#2466)
Handle platforms where the dashboard password store is unavailable by treating legacy token auth as initialized, rejecting password setup, and adding platform-specific store stubs and tests.
This commit is contained in:
+18
-2
@@ -81,8 +81,13 @@ type launcherAuthHandlers struct {
|
||||
loginLimit *loginRateLimiter
|
||||
}
|
||||
|
||||
func (h *launcherAuthHandlers) usesLegacyTokenAuth() bool {
|
||||
return h.store == nil && h.storeErr == nil && h.token != ""
|
||||
}
|
||||
|
||||
// isStoreInitialized safely queries the store.
|
||||
// Returns (false, nil) when no store is configured (storeErr also nil).
|
||||
// Returns (true, nil) when legacy token auth is active without a password store.
|
||||
// Returns (false, nil) when no store/token fallback is configured.
|
||||
// Returns (false, err) on store errors — callers must treat this as a 5xx, not as
|
||||
// "uninitialized", to keep auth fail-closed.
|
||||
// Exception: handleLogin swallows storeErr and falls back to token auth so
|
||||
@@ -95,6 +100,9 @@ func (h *launcherAuthHandlers) isStoreInitialized(ctx context.Context) (bool, er
|
||||
"to recover, stop the application, delete the database file and restart ",
|
||||
h.storeErr)
|
||||
}
|
||||
if h.usesLegacyTokenAuth() {
|
||||
return true, nil
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
return h.store.IsInitialized(ctx)
|
||||
@@ -129,7 +137,7 @@ func (h *launcherAuthHandlers) handleLogin(w http.ResponseWriter, r *http.Reques
|
||||
}
|
||||
}
|
||||
|
||||
if initialized {
|
||||
if initialized && h.store != nil {
|
||||
// Bcrypt path: verify against the stored hash.
|
||||
var err error
|
||||
ok, err = h.store.VerifyPassword(r.Context(), in)
|
||||
@@ -218,6 +226,14 @@ func (h *launcherAuthHandlers) handleStatus(w http.ResponseWriter, r *http.Reque
|
||||
func (h *launcherAuthHandlers) handleSetup(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
if h.usesLegacyTokenAuth() {
|
||||
w.WriteHeader(http.StatusNotImplemented)
|
||||
_, _ = w.Write(
|
||||
[]byte(`{"error":"password setup is unavailable on this platform; use the dashboard token instead"}`),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if h.store == nil {
|
||||
w.WriteHeader(http.StatusNotImplemented)
|
||||
_, _ = w.Write([]byte(`{"error":"password store not configured"}`))
|
||||
|
||||
@@ -75,6 +75,67 @@ func TestLauncherAuthLoginAndStatus(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestLauncherAuthLegacyTokenFallbackReportsInitialized(t *testing.T) {
|
||||
key := make([]byte, 32)
|
||||
const tok = "legacy-fallback-token"
|
||||
sess := middleware.SessionCookieValue(key, tok)
|
||||
mux := http.NewServeMux()
|
||||
RegisterLauncherAuthRoutes(mux, LauncherAuthRouteOpts{
|
||||
DashboardToken: tok,
|
||||
SessionCookie: sess,
|
||||
})
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
mux.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/api/auth/status", nil))
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status code = %d body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
|
||||
var body struct {
|
||||
Authenticated bool `json:"authenticated"`
|
||||
Initialized bool `json:"initialized"`
|
||||
}
|
||||
if err := json.NewDecoder(rec.Body).Decode(&body); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !body.Initialized {
|
||||
t.Fatalf("initialized = false, want true in legacy token fallback mode")
|
||||
}
|
||||
if body.Authenticated {
|
||||
t.Fatalf("unexpected authenticated=true: %+v", body)
|
||||
}
|
||||
|
||||
rec = httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/auth/login", strings.NewReader(`{"password":"`+tok+`"}`))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
mux.ServeHTTP(rec, req)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("login code = %d body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestLauncherAuthSetupRejectedInLegacyTokenFallback(t *testing.T) {
|
||||
key := make([]byte, 32)
|
||||
sess := middleware.SessionCookieValue(key, "legacy-token")
|
||||
mux := http.NewServeMux()
|
||||
RegisterLauncherAuthRoutes(mux, LauncherAuthRouteOpts{
|
||||
DashboardToken: "legacy-token",
|
||||
SessionCookie: sess,
|
||||
})
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(
|
||||
http.MethodPost,
|
||||
"/api/auth/setup",
|
||||
strings.NewReader(`{"password":"12345678","confirm":"12345678"}`),
|
||||
)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
mux.ServeHTTP(rec, req)
|
||||
if rec.Code != http.StatusNotImplemented {
|
||||
t.Fatalf("setup code = %d body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestLauncherAuthLogoutRequiresPostAndJSON(t *testing.T) {
|
||||
key := make([]byte, 32)
|
||||
sess := middleware.SessionCookieValue(key, "tok")
|
||||
|
||||
Reference in New Issue
Block a user