Files
BountyBoard/internal/http/server.go
T
etalon 458ba6a7ee feat: admin area with customers, users, settings, audit log, service status (phase 5)
- customer CRUD wizard backend: per-system credential shapes validated via
  connector construction, AES-256-GCM at rest, write-only over the API
- ticketing connectors (jira/azure_devops/youtrack/demo) with test-connection
- user management: roles, disable (revokes sessions), force password reset,
  delete; self-demotion/disable/delete guards
- admin-editable runtime settings ({_id:app}) incl. atomizer URL override
- audit log written on every privileged mutation + filterable list API
- service status panel: mongo/atomizer/work-performer health + latency with
  late-bound breaker, sync and jobs status providers
- tabbed admin UI (customers wizard dialog, users table, settings, status, audit)
- compose: mongo nofile ulimit 64000 (1024 default crashed WiredTiger)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 18:57:10 +02:00

110 lines
2.9 KiB
Go

package httpx
import (
"context"
"errors"
"fmt"
"html/template"
"log/slog"
"net/http"
"time"
"bountyboard/internal/auth"
"bountyboard/internal/config"
"bountyboard/internal/files"
"bountyboard/internal/metrics"
"bountyboard/internal/store"
)
// shutdownBudget is the §12 graceful-shutdown drain window.
const shutdownBudget = 15 * time.Second
type Server struct {
cfg *config.Config
log *slog.Logger
metrics *metrics.Registry
store *store.Store
files *files.Store
templates map[string]*template.Template
oidc *auth.OIDCClient
loginLimiter *auth.RateLimiter
checks []ReadinessCheck
startedAt time.Time
httpSrv *http.Server
// late-bound subsystem hooks (see providers.go)
atomizer BreakerInfo
performer BreakerInfo
syncProvider StatusProvider
jobsProvider StatusProvider
syncTrigger func(customerID string)
}
func New(cfg *config.Config, log *slog.Logger, reg *metrics.Registry, st *store.Store, fs *files.Store) *Server {
templates, err := parseTemplates()
if err != nil {
// Templates are embedded; failure is a build defect caught by any
// test or first boot, never a runtime condition.
panic(fmt.Sprintf("parse templates: %v", err))
}
s := &Server{
cfg: cfg,
log: log,
metrics: reg,
store: st,
files: fs,
templates: templates,
oidc: auth.NewOIDCClient(cfg, log),
loginLimiter: auth.NewRateLimiter(10, 15*time.Minute),
startedAt: time.Now(),
}
mux := http.NewServeMux()
mux.HandleFunc("GET /healthz", s.handleHealthz)
mux.HandleFunc("GET /readyz", s.handleReadyz)
mux.HandleFunc("GET /metricsz", s.handleMetricsz)
s.routesAuth(mux)
s.routesProfile(mux)
s.routesAdmin(mux)
s.routesWeb(mux)
s.httpSrv = &http.Server{
Addr: fmt.Sprintf(":%d", cfg.AppPort),
Handler: s.withMiddleware(mux),
ReadHeaderTimeout: 10 * time.Second,
ErrorLog: slog.NewLogLogger(log.Handler(), slog.LevelWarn),
}
return s
}
// Handler exposes the full middleware-wrapped handler for tests.
func (s *Server) Handler() http.Handler { return s.httpSrv.Handler }
// Run serves until ctx is canceled (SIGTERM/SIGINT), then drains in-flight
// requests within the shutdown budget.
func (s *Server) Run(ctx context.Context) error {
errCh := make(chan error, 1)
go func() {
s.log.Info("http server listening", "addr", s.httpSrv.Addr, "baseUrl", s.cfg.AppBaseURL)
if err := s.httpSrv.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {
errCh <- err
return
}
errCh <- nil
}()
select {
case err := <-errCh:
return fmt.Errorf("http server: %w", err)
case <-ctx.Done():
}
s.log.Info("shutting down", "budget", shutdownBudget.String())
shutdownCtx, cancel := context.WithTimeout(context.Background(), shutdownBudget)
defer cancel()
if err := s.httpSrv.Shutdown(shutdownCtx); err != nil {
return fmt.Errorf("graceful shutdown: %w", err)
}
return <-errCh
}