Files
autoglue/internal/api/routes.go
2025-11-11 04:01:57 +00:00

282 lines
8.7 KiB
Go

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/bg"
"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, jobs *bg.Jobs, studio http.Handler) 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.Get("/healthz", handlers.HealthCheck)
v1.Get("/version", handlers.Version)
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("/admin", func(admin chi.Router) {
admin.Route("/archer", func(archer chi.Router) {
archer.Use(authUser)
archer.Use(httpmiddleware.RequirePlatformAdmin())
archer.Get("/jobs", handlers.AdminListArcherJobs(db))
archer.Post("/jobs", handlers.AdminEnqueueArcherJob(db, jobs))
archer.Post("/jobs/{id}/retry", handlers.AdminRetryArcherJob(db))
archer.Post("/jobs/{id}/cancel", handlers.AdminCancelArcherJob(db))
archer.Get("/queues", handlers.AdminListArcherQueues(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("/credentials", func(c chi.Router) {
c.Use(authOrg)
c.Get("/", handlers.ListCredentials(db))
c.Post("/", handlers.CreateCredential(db))
c.Get("/{id}", handlers.GetCredential(db))
c.Patch("/{id}", handlers.UpdateCredential(db))
c.Delete("/{id}", handlers.DeleteCredential(db))
c.Post("/{id}/reveal", handlers.RevealCredential(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(l chi.Router) {
l.Use(authOrg)
l.Get("/", handlers.ListLabels(db))
l.Post("/", handlers.CreateLabel(db))
l.Get("/{id}", handlers.GetLabel(db))
l.Patch("/{id}", handlers.UpdateLabel(db))
l.Delete("/{id}", handlers.DeleteLabel(db))
})
v1.Route("/annotations", func(a chi.Router) {
a.Use(authOrg)
a.Get("/", handlers.ListAnnotations(db))
a.Post("/", handlers.CreateAnnotation(db))
a.Get("/{id}", handlers.GetAnnotation(db))
a.Patch("/{id}", handlers.UpdateAnnotation(db))
a.Delete("/{id}", handlers.DeleteAnnotation(db))
})
v1.Route("/node-pools", func(n chi.Router) {
n.Use(authOrg)
n.Get("/", handlers.ListNodePools(db))
n.Post("/", handlers.CreateNodePool(db))
n.Get("/{id}", handlers.GetNodePool(db))
n.Patch("/{id}", handlers.UpdateNodePool(db))
n.Delete("/{id}", handlers.DeleteNodePool(db))
// Servers
n.Get("/{id}/servers", handlers.ListNodePoolServers(db))
n.Post("/{id}/servers", handlers.AttachNodePoolServers(db))
n.Delete("/{id}/servers/{serverId}", handlers.DetachNodePoolServer(db))
// Taints
n.Get("/{id}/taints", handlers.ListNodePoolTaints(db))
n.Post("/{id}/taints", handlers.AttachNodePoolTaints(db))
n.Delete("/{id}/taints/{taintId}", handlers.DetachNodePoolTaint(db))
// Labels
n.Get("/{id}/labels", handlers.ListNodePoolLabels(db))
n.Post("/{id}/labels", handlers.AttachNodePoolLabels(db))
n.Delete("/{id}/labels/{labelId}", handlers.DetachNodePoolLabel(db))
// Annotations
n.Get("/{id}/annotations", handlers.ListNodePoolAnnotations(db))
n.Post("/{id}/annotations", handlers.AttachNodePoolAnnotations(db))
n.Delete("/{id}/annotations/{annotationId}", handlers.DetachNodePoolAnnotation(db))
})
})
})
if studio != nil {
r.Group(func(gr chi.Router) {
authUser := httpmiddleware.AuthMiddleware(db, false)
adminOnly := httpmiddleware.RequirePlatformAdmin()
gr.Use(authUser)
gr.Use(adminOnly)
gr.Mount("/db-studio", studio)
})
}
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("/db-studio/", 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
}