feat(admin): add authenticated restart endpoint + fix admin username
Cyrene CI / backend (push) Has been cancelled
Cyrene CI / frontend (push) Has been cancelled

- POST /api/v1/admin/restart: admin-only restart (forks new process)
- Fix: MemUserStore now uses cfg.AdminUsername instead of hardcoded 'admin'
- RestartFunc injected by main.go for clean process fork + exit
This commit is contained in:
Aska committed 2026-08-12 21:26:58 +08:00
1 parent dee48b6766
commit 628d416dcd
4 files changed
+115 -6

No files matched your search

+19 -1
View File
@@ -212,7 +212,7 @@ func main() {
log.Printf("[bridge] config store ready (path=%s, %d adapters)", bridgeCfgPath, bridgeStore.Count())
// === 12. Gateway ===
userStore := api.NewMemUserStore(cfg.AdminPassword)
userStore := api.NewMemUserStore(cfg.AdminUsername, cfg.AdminPassword)
authMgr := api.NewRealAuthManager(cfg.JWTSecret, cfg.JWTExpiryHours, userStore)
// Build dependencies for gateway handlers
@@ -254,6 +254,24 @@ func main() {
srv := &http.Server{Addr: addr, Handler: mainMux}
// === 13. Start ===
// Register restart handler — forks a new process and exits
api.RestartFunc = func() {
log.Printf("[gateway] restarting...")
reg.Shutdown()
srv.Close()
// Re-exec self with same args and env
exe, _ := os.Executable()
proc, err := os.StartProcess(exe, os.Args, &os.ProcAttr{
Env: os.Environ(),
Files: []*os.File{os.Stdin, os.Stdout, os.Stderr},
})
if err != nil {
log.Fatalf("[gateway] restart failed: %v", err)
}
log.Printf("[gateway] new process PID=%d, exiting...", proc.Pid)
os.Exit(0)
}
go func() {
log.Printf("[gateway] listening on %s", addr)
log.Printf("[gateway] plugins=%d tools=%d", reg.Count(), len(reg.GetAllToolDefinitions()))
+7 -5
View File
@@ -28,14 +28,16 @@ type MemUserStore struct {
users map[string]*UserRecord
}
// NewMemUserStore creates an in-memory user store with a default admin.
func NewMemUserStore(adminPassword string) *MemUserStore {
// NewMemUserStore creates an in-memory user store with the configured admin account.
func NewMemUserStore(adminUsername, adminPassword string) *MemUserStore {
s := &MemUserStore{users: make(map[string]*UserRecord)}
hash, _ := auth.HashPassword(adminPassword)
s.users["admin"] = &UserRecord{
UserID: "admin", Username: "admin",
PasswordHash: hash, Role: "admin",
s.users[adminUsername] = &UserRecord{
UserID: adminUsername,
Username: adminUsername,
PasswordHash: hash,
Role: "admin",
}
return s
}
+15
View File
@@ -136,6 +136,7 @@ func (r *Router) setupRoutes() {
// Admin
r.mux.HandleFunc("/api/v1/admin/dashboard", r.authMW(r.adminMW(r.adminDashboardHandler)))
r.mux.HandleFunc("/api/v1/admin/restart", r.authMW(r.adminMW(r.adminRestartHandler)))
r.mux.HandleFunc("/api/v1/admin/llm-calls", r.authMW(r.adminMW(r.llmCallsHandler)))
r.mux.HandleFunc("/api/v1/admin/system", r.authMW(r.adminMW(r.systemInfoHandler)))
r.mux.HandleFunc("/api/v1/admin/plugins", r.authMW(r.adminMW(r.pluginListHandler)))
@@ -417,6 +418,19 @@ func (r *Router) adminDashboardHandler(w http.ResponseWriter, _ *http.Request) {
})
}
// RestartFunc is injected by main.go — called to restart the process.
var RestartFunc func()
func (r *Router) adminRestartHandler(w http.ResponseWriter, _ *http.Request) {
writeJSON(w, 200, M{"restart": "ok", "message": "Cyrene is restarting..."})
go func() {
time.Sleep(200 * time.Millisecond) // Let the response flush
if RestartFunc != nil {
RestartFunc()
}
}()
}
// --- Admin extra handlers ---
func (r *Router) llmCallsHandler(w http.ResponseWriter, req *http.Request) {
@@ -638,3 +652,4 @@ func (r *Router) userID(req *http.Request) string { return req.Header.Get("X-Use
// Ensure unused imports don't cause issues
// Debug: this will be overwritten