This repository has been archived on 2026-03-09. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
oubliette/internal/web/web.go
Torjus Håkestad cb7be28f42 feat: add server-side session filtering with input bytes and human score
Replace client-side session table filtering with server-side filtering
via a new /fragments/recent-sessions htmx endpoint. Add InputBytes column
to session tables, Human score > 0 checkbox filter, and Sort by Input
Bytes option to help identify sessions with actual shell interaction.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 09:12:51 +01:00

84 lines
2.5 KiB
Go

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 /api/charts/attempts-over-time", s.handleAPIAttemptsOverTime)
s.mux.HandleFunc("GET /api/charts/hourly-pattern", s.handleAPIHourlyPattern)
s.mux.HandleFunc("GET /api/charts/country-stats", s.handleAPICountryStats)
s.mux.HandleFunc("GET /", s.handleDashboard)
s.mux.HandleFunc("GET /fragments/stats", s.handleFragmentStats)
s.mux.HandleFunc("GET /fragments/active-sessions", s.handleFragmentActiveSessions)
s.mux.HandleFunc("GET /fragments/dashboard-content", s.handleFragmentDashboardContent)
s.mux.HandleFunc("GET /fragments/recent-sessions", s.handleFragmentRecentSessions)
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)
})
}