package server import ( "io/fs" "net/http" "time" "git.eeqj.de/sneak/chat/web" sentryhttp "github.com/getsentry/sentry-go/http" "github.com/go-chi/chi" "github.com/go-chi/chi/middleware" "github.com/prometheus/client_golang/prometheus/promhttp" "github.com/spf13/viper" ) const routeTimeout = 60 * time.Second // SetupRoutes configures the HTTP routes and middleware chain. func (s *Server) SetupRoutes() { s.router = chi.NewRouter() s.router.Use(middleware.Recoverer) s.router.Use(middleware.RequestID) s.router.Use(s.mw.Logging()) if viper.GetString("METRICS_USERNAME") != "" { s.router.Use(s.mw.Metrics()) } s.router.Use(s.mw.CORS()) s.router.Use(middleware.Timeout(routeTimeout)) if s.sentryEnabled { sentryHandler := sentryhttp.New(sentryhttp.Options{ Repanic: true, }) s.router.Use(sentryHandler.Handle) } // Health check s.router.Get("/.well-known/healthcheck.json", s.h.HandleHealthCheck()) // Protected metrics endpoint if viper.GetString("METRICS_USERNAME") != "" { s.router.Group(func(r chi.Router) { r.Use(s.mw.MetricsAuth()) r.Get("/metrics", http.HandlerFunc(promhttp.Handler().ServeHTTP)) }) } // API v1 s.router.Route("/api/v1", func(r chi.Router) { r.Get("/server", s.h.HandleServerInfo()) r.Post("/session", s.h.HandleCreateSession()) // Unified state and message endpoints r.Get("/state", s.h.HandleState()) r.Get("/messages", s.h.HandleGetMessages()) r.Post("/messages", s.h.HandleSendCommand()) r.Get("/history", s.h.HandleGetHistory()) // Channels r.Get("/channels", s.h.HandleListAllChannels()) r.Get("/channels/{channel}/members", s.h.HandleChannelMembers()) }) // Serve embedded SPA distFS, err := fs.Sub(web.Dist, "dist") if err != nil { s.log.Error("failed to get web dist filesystem", "error", err) } else { fileServer := http.FileServer(http.FS(distFS)) s.router.Get("/*", func(w http.ResponseWriter, r *http.Request) { s.serveSPA(distFS, fileServer, w, r) }) } } func (s *Server) serveSPA( distFS fs.FS, fileServer http.Handler, w http.ResponseWriter, r *http.Request, ) { readFS, ok := distFS.(fs.ReadFileFS) if !ok { http.Error(w, "filesystem error", http.StatusInternalServerError) return } // Try to serve the file; fall back to index.html for SPA routing. f, err := readFS.ReadFile(r.URL.Path[1:]) if err != nil || len(f) == 0 { indexHTML, _ := readFS.ReadFile("index.html") w.Header().Set("Content-Type", "text/html; charset=utf-8") w.WriteHeader(http.StatusOK) _, _ = w.Write(indexHTML) return } fileServer.ServeHTTP(w, r) }