101 строка
2.8 KiB
Go
101 строка
2.8 KiB
Go
package webapp
|
|
|
|
import (
|
|
"net/http"
|
|
"time"
|
|
)
|
|
|
|
// handleSettings renders the worker fields (worker id, region,
|
|
// capabilities, version, last heartbeat ack, last token rotation).
|
|
// The token is masked; rotation is a POST to
|
|
// /settings/rotate-token (see handleRotateToken).
|
|
func (s *Server) handleSettings(w http.ResponseWriter, r *http.Request) {
|
|
writeNoStore(w)
|
|
sess, _ := sessionFromContext(r.Context())
|
|
token := ""
|
|
if s.deps.Runner != nil {
|
|
token = s.deps.Runner.Token()
|
|
}
|
|
rotatedAt := time.Time{}
|
|
if s.deps.Runner != nil {
|
|
rotatedAt = s.deps.Runner.TokenRotatedAt()
|
|
}
|
|
data := settingsPageData{
|
|
basePageData: s.newBasePage(r, "Settings", sess),
|
|
WorkerID: workerIDOrDash(s.deps.Runner),
|
|
RegionCode: regionOrDash(s.deps.Runner),
|
|
WorkerVersion: workerVersionOrDash(s.deps.Runner),
|
|
Capabilities: capabilitiesOrEmpty(s.deps.Runner),
|
|
LastAckAt: lastAckOrZero(s.deps.Runner),
|
|
TokenMasked: MaskToken(token),
|
|
TokenRotatedAt: rotatedAt,
|
|
}
|
|
if err := s.templates.Execute(w, "settings.html", data); err != nil {
|
|
s.deps.Logger.Printf("render settings: %v", err)
|
|
http.Error(w, "template error", http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
func workerVersionOrDash(v WorkerView) string {
|
|
if v == nil {
|
|
return "—"
|
|
}
|
|
return v.WorkerVersion()
|
|
}
|
|
|
|
func capabilitiesOrEmpty(v WorkerView) []string {
|
|
if v == nil {
|
|
return nil
|
|
}
|
|
return v.WorkerCapabilities()
|
|
}
|
|
|
|
type settingsPageData struct {
|
|
basePageData
|
|
WorkerID string
|
|
RegionCode string
|
|
WorkerVersion string
|
|
Capabilities []string
|
|
LastAckAt time.Time
|
|
TokenMasked string
|
|
TokenRotatedAt time.Time
|
|
}
|
|
|
|
// handleRotateToken calls the worker-defined rotator (if any) to
|
|
// issue a fresh token via the main app's API and update the in-
|
|
// memory runner config. On failure, return 502 and keep the old
|
|
// token (the doc's Phase 1 contract).
|
|
func (s *Server) handleRotateToken(w http.ResponseWriter, r *http.Request) {
|
|
writeNoStore(w)
|
|
sess, ok := sessionFromContext(r.Context())
|
|
if !ok {
|
|
redirectTo(w, r, "/web/login")
|
|
return
|
|
}
|
|
if !s.requireCSRF(sess, r) {
|
|
http.Error(w, "csrf token required", http.StatusForbidden)
|
|
return
|
|
}
|
|
if s.deps.TokenRotator == nil {
|
|
http.Error(w, "token rotation is not configured", http.StatusNotImplemented)
|
|
return
|
|
}
|
|
newToken, err := s.deps.TokenRotator(r.Context())
|
|
if err != nil {
|
|
s.deps.Logger.Printf("token rotation: %v", err)
|
|
http.Error(w, "rotation failed: "+err.Error(), http.StatusBadGateway)
|
|
return
|
|
}
|
|
_ = s.store.WriteAudit(r.Context(), &AuditEntry{
|
|
Actor: auditActorLocal,
|
|
Role: auditRoleAdmin,
|
|
AuthMode: auditAuthModeLocal,
|
|
IP: clientIP(r),
|
|
UA: r.UserAgent(),
|
|
Action: "token_rotation",
|
|
Target: auditTargetSelf,
|
|
})
|
|
_ = newToken // the rotator updates the runner; the page just confirms success.
|
|
redirectTo(w, r, "/settings")
|
|
}
|