package api import ( "database/sql" "log/slog" "net/http" "runtime/debug" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" "github.com/syncserver/internal/auth" "github.com/syncserver/internal/config" "github.com/syncserver/internal/sshmanager" "github.com/syncserver/internal/syncengine" "github.com/syncserver/internal/webui" ) type Server struct { router *chi.Mux cfg *config.Config engine *syncengine.Engine } func NewServer(cfg *config.Config, db *sql.DB, engine *syncengine.Engine) *Server { auth.InitJWTManager(cfg.Auth.JWTSecret, cfg.Auth.JWTExpiryH) r := chi.NewRouter() r.Use(middleware.RequestID) r.Use(middleware.RealIP) r.Use(middleware.Logger) r.Use(recoverer) s := &Server{router: r, cfg: cfg, engine: engine} authHandler := NewAuthHandler(db) machineHandler := NewMachineHandler(db) syncPairHandler := NewSyncPairHandler(db) jobHandler := NewJobHandler(db, engine) sseHandler := NewSSEHandler(engine) sshKeyHandler := NewSSHKeyHandler(db, cfg) r.Route("/api", func(r chi.Router) { r.Route("/auth", func(r chi.Router) { r.Post("/login", authHandler.Login) r.Post("/logout", authHandler.Logout) r.With(auth.RequireAuth).Get("/me", authHandler.Me) }) r.With(auth.RequireAuth).Route("/machines", func(r chi.Router) { r.Get("/", machineHandler.List) r.Post("/", machineHandler.Create) r.Get("/{id}", machineHandler.Get) r.Put("/{id}", machineHandler.Update) r.Delete("/{id}", machineHandler.Delete) r.Post("/{id}/test-wol", machineHandler.TestWoL) }) r.With(auth.RequireAuth).Route("/sync-pairs", func(r chi.Router) { r.Get("/", syncPairHandler.List) r.Post("/", syncPairHandler.Create) r.Get("/{id}", syncPairHandler.Get) r.Put("/{id}", syncPairHandler.Update) r.Delete("/{id}", syncPairHandler.Delete) r.Post("/{id}/run", jobHandler.TriggerRun) }) r.With(auth.RequireAuth).Route("/jobs", func(r chi.Router) { r.Get("/", jobHandler.List) r.Get("/{id}", jobHandler.Get) r.Post("/{id}/cancel", jobHandler.Cancel) r.Get("/{id}/log", jobHandler.GetLog) r.Get("/{id}/log/download", jobHandler.DownloadLog) r.Get("/{id}/log/stream", sseHandler.StreamJob) }) r.With(auth.RequireAuth).Get("/jobs/stream", sseHandler.StreamAll) r.With(auth.RequireAuth).Get("/settings/pubkey", func(w http.ResponseWriter, r *http.Request) { _, _, pubKey, _ := sshmanager.EnsureServerKey(cfg.SSHDir()) w.Header().Set("Content-Type", "text/plain") w.Write([]byte(pubKey)) }) r.With(auth.RequireAuth).Route("/ssh-keys", func(r chi.Router) { r.Get("/", sshKeyHandler.List) r.Post("/", sshKeyHandler.Create) r.Get("/{id}", sshKeyHandler.Get) r.Delete("/{id}", sshKeyHandler.Delete) r.Get("/{id}/private", sshKeyHandler.DownloadPrivate) }) }) r.Get("/health", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("ok")) })) r.NotFound(func(w http.ResponseWriter, r *http.Request) { webui.ServeSPA().ServeHTTP(w, r) }) return s } func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { s.router.ServeHTTP(w, r) } func recoverer(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { defer func() { if err := recover(); err != nil { slog.Error("panic recovered", "error", err, "stack", string(debug.Stack()), "method", r.Method, "path", r.URL.Path, ) w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusInternalServerError) http.Error(w, `{"error":"internal server error"}`, http.StatusInternalServerError) } }() next.ServeHTTP(w, r) }) }