package web import ( "crypto/subtle" "embed" "log/slog" "net/http" "strings" "git.t-juice.club/torjus/oubliette/internal/storage" ) //go:embed static/* var staticFS embed.FS // Server is the web dashboard HTTP server. type Server struct { store storage.Store logger *slog.Logger mux *http.ServeMux tmpl *templateSet } // NewServer creates a new web Server with routes registered. // If metricsHandler is non-nil, a /metrics endpoint is registered. // If metricsToken is non-empty, the metrics endpoint requires Bearer token auth. func NewServer(store storage.Store, logger *slog.Logger, metricsHandler http.Handler, metricsToken string) (*Server, error) { tmpl, err := loadTemplates() if err != nil { return nil, err } s := &Server{ store: store, logger: logger, mux: http.NewServeMux(), tmpl: tmpl, } s.mux.Handle("GET /static/", http.FileServerFS(staticFS)) s.mux.HandleFunc("GET /sessions/{id}", s.handleSessionDetail) s.mux.HandleFunc("GET /api/sessions/{id}/events", s.handleAPISessionEvents) s.mux.HandleFunc("GET /", s.handleDashboard) s.mux.HandleFunc("GET /fragments/stats", s.handleFragmentStats) s.mux.HandleFunc("GET /fragments/active-sessions", s.handleFragmentActiveSessions) if metricsHandler != nil { h := metricsHandler if metricsToken != "" { h = requireBearerToken(metricsToken, h) } s.mux.Handle("GET /metrics", h) } return s, nil } // ServeHTTP delegates to the internal mux. func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { s.mux.ServeHTTP(w, r) } // requireBearerToken wraps a handler to require a valid Bearer token. func requireBearerToken(token string, next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { auth := r.Header.Get("Authorization") if !strings.HasPrefix(auth, "Bearer ") { http.Error(w, "Unauthorized", http.StatusUnauthorized) return } provided := auth[len("Bearer "):] if subtle.ConstantTimeCompare([]byte(provided), []byte(token)) != 1 { http.Error(w, "Unauthorized", http.StatusUnauthorized) return } next.ServeHTTP(w, r) }) }