Files
worker/internal/webapp/handlers_settings.go
Gleb Tv 2c7a0236da feat: publish standalone worker
Separate worker packaging and service lifecycle from the control plane.
2026-07-13 17:55:14 +03:00

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")
}