From 7b2c5f444b03a1d065481f645b51402e149581a8 Mon Sep 17 00:00:00 2001 From: OpenClaw Agent Date: Tue, 22 Sep 2026 04:48:06 +0800 Subject: [PATCH] fix: preserve login session across refreshes --- cmd/chfs/main.go | 21 +++++++++++++++++---- internal/api/server.go | 10 ++++++++++ internal/api/web/index.html | 4 ++-- 3 files changed, 29 insertions(+), 6 deletions(-) diff --git a/cmd/chfs/main.go b/cmd/chfs/main.go index f42cca8..c194ce5 100644 --- a/cmd/chfs/main.go +++ b/cmd/chfs/main.go @@ -122,17 +122,30 @@ func main() { } else if !filepath.IsAbs(userFile) { userFile = filepath.Join(root, userFile) } + if err := os.MkdirAll(filepath.Dir(userFile), 0700); err != nil { + log.Fatal(err) + } users := &auth.UserStore{Path: userFile} if err := users.Load(mgr, cfg.UserHome); err != nil { log.Fatal(err) } jwtSecret := cfg.JWTSecret if jwtSecret == "" { - buf := make([]byte, 32) - if _, err := rand.Read(buf); err != nil { - log.Fatal(err) + // Keep sessions valid across process restarts. A random in-memory secret + // would invalidate every login whenever the service is restarted. + secretPath := userFile + ".jwt-secret" + if saved, err := os.ReadFile(secretPath); err == nil && len(strings.TrimSpace(string(saved))) >= 32 { + jwtSecret = strings.TrimSpace(string(saved)) + } else { + buf := make([]byte, 32) + if _, err := rand.Read(buf); err != nil { + log.Fatal(err) + } + jwtSecret = hex.EncodeToString(buf) + if err := os.WriteFile(secretPath, []byte(jwtSecret+"\n"), 0600); err != nil { + log.Printf("warning: could not persist JWT secret at %s: %v", secretPath, err) + } } - jwtSecret = hex.EncodeToString(buf) } for _, a := range mgr.Accounts { if a.Home != "" { diff --git a/internal/api/server.go b/internal/api/server.go index fb653f9..b9cd8cf 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -146,11 +146,20 @@ func clientIPString(r *http.Request) string { func writeJSON(w http.ResponseWriter, v any) { w.Header().Set("Content-Type", "application/json") + w.Header().Set("Cache-Control", "no-store") + w.Header().Set("Vary", "Cookie") json.NewEncoder(w).Encode(v) } +func noStore(w http.ResponseWriter) { + w.Header().Set("Cache-Control", "no-store") + w.Header().Set("Vary", "Cookie") +} + func writeJSONError(w http.ResponseWriter, status int, message string) { w.Header().Set("Content-Type", "application/json") + w.Header().Set("Cache-Control", "no-store") + w.Header().Set("Vary", "Cookie") w.WriteHeader(status) _ = json.NewEncoder(w).Encode(map[string]string{"error": message}) } @@ -511,6 +520,7 @@ func (s *Server) exist(w http.ResponseWriter, r *http.Request) { } func (s *Server) session(w http.ResponseWriter, r *http.Request) { + noStore(w) if r.Method == http.MethodDelete { http.SetCookie(w, &http.Cookie{Name: "JWT", Value: "", Path: "/", MaxAge: -1, Expires: time.Unix(1, 0), HttpOnly: true, SameSite: http.SameSiteLaxMode}) http.SetCookie(w, &http.Cookie{Name: "user", Value: "", Path: "/", MaxAge: -1, Expires: time.Unix(1, 0), SameSite: http.SameSiteLaxMode}) diff --git a/internal/api/web/index.html b/internal/api/web/index.html index 4f59b73..6db68d5 100644 --- a/internal/api/web/index.html +++ b/internal/api/web/index.html @@ -46,7 +46,7 @@ button.ghost{background:var(--surface2);color:var(--text)}button.danger{backgrou