feat: publish standalone worker
Separate worker packaging and service lifecycle from the control plane.
Этот коммит содержится в:
100
internal/webapp/handlers_settings.go
Обычный файл
100
internal/webapp/handlers_settings.go
Обычный файл
@@ -0,0 +1,100 @@
|
||||
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")
|
||||
}
|
||||
Ссылка в новой задаче
Block a user