package api import ( "fmt" "net/http" httpPprof "net/http/pprof" "os" "time" "github.com/glueops/autoglue/docs" "github.com/glueops/autoglue/internal/api/httpmiddleware" "github.com/glueops/autoglue/internal/config" "github.com/glueops/autoglue/internal/handlers" "github.com/glueops/autoglue/internal/web" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" "github.com/go-chi/cors" "github.com/go-chi/httprate" "gorm.io/gorm" "github.com/rs/zerolog" "github.com/rs/zerolog/log" httpSwagger "github.com/swaggo/http-swagger/v2" ) func NewRouter(db *gorm.DB) http.Handler { zerolog.TimeFieldFormat = time.RFC3339 l := log.Output(zerolog.ConsoleWriter{Out: os.Stdout, TimeFormat: "15:04:05"}) log.Logger = l r := chi.NewRouter() r.Use(middleware.RequestID) r.Use(middleware.RealIP) r.Use(zeroLogMiddleware()) r.Use(middleware.Recoverer) r.Use(SecurityHeaders) r.Use(requestBodyLimit(10 << 20)) r.Use(httprate.LimitByIP(100, 1*time.Minute)) allowed := getAllowedOrigins() r.Use(cors.Handler(cors.Options{ AllowedOrigins: allowed, AllowedMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"}, AllowedHeaders: []string{ "Content-Type", "Authorization", "X-Org-ID", "X-API-KEY", "X-ORG-KEY", "X-ORG-SECRET", }, ExposedHeaders: []string{"Link"}, AllowCredentials: true, MaxAge: 600, })) r.Use(middleware.AllowContentType("application/json")) r.Get("/.well-known/jwks.json", handlers.JWKSHandler) r.Route("/api", func(api chi.Router) { api.Route("/v1", func(v1 chi.Router) { authUser := httpmiddleware.AuthMiddleware(db, false) authOrg := httpmiddleware.AuthMiddleware(db, true) // Also serving a versioned JWKS for swagger, which uses BasePath v1.Get("/.well-known/jwks.json", handlers.JWKSHandler) v1.Route("/auth", func(a chi.Router) { a.Post("/{provider}/start", handlers.AuthStart(db)) a.Get("/{provider}/callback", handlers.AuthCallback(db)) a.Post("/refresh", handlers.Refresh(db)) a.Post("/logout", handlers.Logout(db)) }) v1.Route("/me", func(me chi.Router) { me.Use(authUser) me.Get("/", handlers.GetMe(db)) me.Patch("/", handlers.UpdateMe(db)) me.Get("/api-keys", handlers.ListUserAPIKeys(db)) me.Post("/api-keys", handlers.CreateUserAPIKey(db)) me.Delete("/api-keys/{id}", handlers.DeleteUserAPIKey(db)) }) v1.Route("/orgs", func(o chi.Router) { o.Use(authUser) o.Get("/", handlers.ListMyOrgs(db)) o.Post("/", handlers.CreateOrg(db)) o.Group(func(og chi.Router) { og.Use(authOrg) og.Get("/{id}", handlers.GetOrg(db)) og.Patch("/{id}", handlers.UpdateOrg(db)) og.Delete("/{id}", handlers.DeleteOrg(db)) // members og.Get("/{id}/members", handlers.ListMembers(db)) og.Post("/{id}/members", handlers.AddOrUpdateMember(db)) og.Delete("/{id}/members/{user_id}", handlers.RemoveMember(db)) // org-scoped key/secret pair og.Get("/{id}/api-keys", handlers.ListOrgKeys(db)) og.Post("/{id}/api-keys", handlers.CreateOrgKey(db)) og.Delete("/{id}/api-keys/{key_id}", handlers.DeleteOrgKey(db)) }) }) v1.Route("/ssh", func(s chi.Router) { s.Use(authOrg) s.Get("/", handlers.ListPublicSshKeys(db)) s.Post("/", handlers.CreateSSHKey(db)) s.Get("/{id}", handlers.GetSSHKey(db)) s.Delete("/{id}", handlers.DeleteSSHKey(db)) s.Get("/{id}/download", handlers.DownloadSSHKey(db)) }) v1.Route("/servers", func(s chi.Router) { s.Use(authOrg) s.Get("/", handlers.ListServers(db)) s.Post("/", handlers.CreateServer(db)) s.Get("/{id}", handlers.GetServer(db)) s.Patch("/{id}", handlers.UpdateServer(db)) s.Delete("/{id}", handlers.DeleteServer(db)) }) v1.Route("/taints", func(s chi.Router) { s.Use(authOrg) s.Get("/", handlers.ListTaints(db)) s.Post("/", handlers.CreateTaint(db)) s.Get("/{id}", handlers.GetTaint(db)) s.Patch("/{id}", handlers.UpdateTaint(db)) s.Delete("/{id}", handlers.DeleteTaint(db)) }) v1.Route("/labels", func(s chi.Router) { s.Use(authOrg) s.Get("/", handlers.ListLabels(db)) s.Post("/", handlers.CreateLabel(db)) s.Get("/{id}", handlers.GetLabel(db)) s.Patch("/{id}", handlers.UpdateLabel(db)) s.Delete("/{id}", handlers.DeleteLabel(db)) }) }) }) if config.IsDebug() { r.Route("/debug/pprof", func(pr chi.Router) { pr.Get("/", httpPprof.Index) pr.Get("/cmdline", httpPprof.Cmdline) pr.Get("/profile", httpPprof.Profile) pr.Get("/symbol", httpPprof.Symbol) pr.Get("/trace", httpPprof.Trace) pr.Handle("/allocs", httpPprof.Handler("allocs")) pr.Handle("/block", httpPprof.Handler("block")) pr.Handle("/goroutine", httpPprof.Handler("goroutine")) pr.Handle("/heap", httpPprof.Handler("heap")) pr.Handle("/mutex", httpPprof.Handler("mutex")) pr.Handle("/threadcreate", httpPprof.Handler("threadcreate")) }) } if config.IsSwaggerEnabled() { r.Get("/swagger/*", httpSwagger.Handler( httpSwagger.URL("swagger.json"), )) r.Get("/swagger/swagger.json", serveSwaggerFromEmbed(docs.SwaggerJSON, "application/json")) r.Get("/swagger/swagger.yaml", serveSwaggerFromEmbed(docs.SwaggerYAML, "application/x-yaml")) } if config.IsUIDev() { fmt.Println("Running in development mode") // Dev: isolate proxy from chi middlewares so WS upgrade can hijack. proxy, err := web.DevProxy("http://localhost:5173") if err != nil { log.Error().Err(err).Msg("dev proxy init failed") return r // fallback } mux := http.NewServeMux() // Send API/Swagger/pprof to chi mux.Handle("/api/", r) mux.Handle("/api", r) mux.Handle("/swagger/", r) mux.Handle("/debug/pprof/", r) // Everything else (/, /brand-preview, assets) → proxy (no middlewares) mux.Handle("/", proxy) return mux } else { fmt.Println("Running in production mode") if h, err := web.SPAHandler(); err == nil { r.NotFound(h.ServeHTTP) } else { log.Error().Err(err).Msg("spa handler init failed") } } return r }