feat(modules): RequireModule middleware to gate route groups
Add RequireModule middleware that returns 404 with
{"error":"module '<id>' is not enabled"} when a module is disabled.
Wrap route groups:
- projects → RequireModule("projects")
- audit → RequireModule("audit")
- integrations/odoo → RequireModule("odoo")
- jobs, job-definitions, runners → RequireModule("jobs")
- /api/runner (runner-facing) → RequireModule("jobs")
- dag → RequireModule("dag") (extracted into sub-route)
Ref #98
This commit is contained in:
@@ -183,6 +183,22 @@ func (s *Server) RequireRunnerAuth(next http.Handler) http.Handler {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RequireModule returns middleware that rejects requests with 404 when
|
||||||
|
// the named module is not enabled.
|
||||||
|
func (s *Server) RequireModule(id string) func(http.Handler) http.Handler {
|
||||||
|
return func(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if !s.modules.IsEnabled(id) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
w.Write([]byte(`{"error":"module '` + id + `' is not enabled"}`))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func extractBearerToken(r *http.Request) string {
|
func extractBearerToken(r *http.Request) string {
|
||||||
h := r.Header.Get("Authorization")
|
h := r.Header.Get("Authorization")
|
||||||
if strings.HasPrefix(h, "Bearer ") {
|
if strings.HasPrefix(h, "Bearer ") {
|
||||||
|
|||||||
@@ -101,6 +101,7 @@ func NewRouter(server *Server, logger zerolog.Logger) http.Handler {
|
|||||||
|
|
||||||
// Projects (read: viewer, write: editor)
|
// Projects (read: viewer, write: editor)
|
||||||
r.Route("/projects", func(r chi.Router) {
|
r.Route("/projects", func(r chi.Router) {
|
||||||
|
r.Use(server.RequireModule("projects"))
|
||||||
r.Get("/", server.HandleListProjects)
|
r.Get("/", server.HandleListProjects)
|
||||||
r.Get("/{code}", server.HandleGetProject)
|
r.Get("/{code}", server.HandleGetProject)
|
||||||
r.Get("/{code}/items", server.HandleGetProjectItems)
|
r.Get("/{code}/items", server.HandleGetProjectItems)
|
||||||
@@ -150,10 +151,20 @@ func NewRouter(server *Server, logger zerolog.Logger) http.Handler {
|
|||||||
r.Get("/bom/export.csv", server.HandleExportBOMCSV)
|
r.Get("/bom/export.csv", server.HandleExportBOMCSV)
|
||||||
r.Get("/bom/export.ods", server.HandleExportBOMODS)
|
r.Get("/bom/export.ods", server.HandleExportBOMODS)
|
||||||
|
|
||||||
// DAG (read: viewer, write: editor)
|
// DAG (gated by dag module)
|
||||||
r.Get("/dag", server.HandleGetDAG)
|
r.Route("/dag", func(r chi.Router) {
|
||||||
r.Get("/dag/forward-cone/{nodeKey}", server.HandleGetForwardCone)
|
r.Use(server.RequireModule("dag"))
|
||||||
r.Get("/dag/dirty", server.HandleGetDirtySubgraph)
|
r.Get("/", server.HandleGetDAG)
|
||||||
|
r.Get("/forward-cone/{nodeKey}", server.HandleGetForwardCone)
|
||||||
|
r.Get("/dirty", server.HandleGetDirtySubgraph)
|
||||||
|
|
||||||
|
r.Group(func(r chi.Router) {
|
||||||
|
r.Use(server.RequireWritable)
|
||||||
|
r.Use(server.RequireRole(auth.RoleEditor))
|
||||||
|
r.Put("/", server.HandleSyncDAG)
|
||||||
|
r.Post("/mark-dirty/{nodeKey}", server.HandleMarkDirty)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
r.Group(func(r chi.Router) {
|
r.Group(func(r chi.Router) {
|
||||||
r.Use(server.RequireWritable)
|
r.Use(server.RequireWritable)
|
||||||
@@ -174,20 +185,20 @@ func NewRouter(server *Server, logger zerolog.Logger) http.Handler {
|
|||||||
r.Post("/bom/merge", server.HandleMergeBOM)
|
r.Post("/bom/merge", server.HandleMergeBOM)
|
||||||
r.Put("/bom/{childPartNumber}", server.HandleUpdateBOMEntry)
|
r.Put("/bom/{childPartNumber}", server.HandleUpdateBOMEntry)
|
||||||
r.Delete("/bom/{childPartNumber}", server.HandleDeleteBOMEntry)
|
r.Delete("/bom/{childPartNumber}", server.HandleDeleteBOMEntry)
|
||||||
r.Put("/dag", server.HandleSyncDAG)
|
|
||||||
r.Post("/dag/mark-dirty/{nodeKey}", server.HandleMarkDirty)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
// Audit (read-only, viewer role)
|
// Audit (read-only, viewer role)
|
||||||
r.Route("/audit", func(r chi.Router) {
|
r.Route("/audit", func(r chi.Router) {
|
||||||
|
r.Use(server.RequireModule("audit"))
|
||||||
r.Get("/completeness", server.HandleAuditCompleteness)
|
r.Get("/completeness", server.HandleAuditCompleteness)
|
||||||
r.Get("/completeness/{partNumber}", server.HandleAuditItemDetail)
|
r.Get("/completeness/{partNumber}", server.HandleAuditItemDetail)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Integrations (read: viewer, write: editor)
|
// Integrations (read: viewer, write: editor)
|
||||||
r.Route("/integrations/odoo", func(r chi.Router) {
|
r.Route("/integrations/odoo", func(r chi.Router) {
|
||||||
|
r.Use(server.RequireModule("odoo"))
|
||||||
r.Get("/config", server.HandleGetOdooConfig)
|
r.Get("/config", server.HandleGetOdooConfig)
|
||||||
r.Get("/sync-log", server.HandleGetOdooSyncLog)
|
r.Get("/sync-log", server.HandleGetOdooSyncLog)
|
||||||
|
|
||||||
@@ -210,6 +221,7 @@ func NewRouter(server *Server, logger zerolog.Logger) http.Handler {
|
|||||||
|
|
||||||
// Jobs (read: viewer, write: editor)
|
// Jobs (read: viewer, write: editor)
|
||||||
r.Route("/jobs", func(r chi.Router) {
|
r.Route("/jobs", func(r chi.Router) {
|
||||||
|
r.Use(server.RequireModule("jobs"))
|
||||||
r.Get("/", server.HandleListJobs)
|
r.Get("/", server.HandleListJobs)
|
||||||
r.Get("/{jobID}", server.HandleGetJob)
|
r.Get("/{jobID}", server.HandleGetJob)
|
||||||
r.Get("/{jobID}/logs", server.HandleGetJobLogs)
|
r.Get("/{jobID}/logs", server.HandleGetJobLogs)
|
||||||
@@ -224,6 +236,7 @@ func NewRouter(server *Server, logger zerolog.Logger) http.Handler {
|
|||||||
|
|
||||||
// Job definitions (read: viewer, reload: admin)
|
// Job definitions (read: viewer, reload: admin)
|
||||||
r.Route("/job-definitions", func(r chi.Router) {
|
r.Route("/job-definitions", func(r chi.Router) {
|
||||||
|
r.Use(server.RequireModule("jobs"))
|
||||||
r.Get("/", server.HandleListJobDefinitions)
|
r.Get("/", server.HandleListJobDefinitions)
|
||||||
r.Get("/{name}", server.HandleGetJobDefinition)
|
r.Get("/{name}", server.HandleGetJobDefinition)
|
||||||
|
|
||||||
@@ -235,6 +248,7 @@ func NewRouter(server *Server, logger zerolog.Logger) http.Handler {
|
|||||||
|
|
||||||
// Runners (admin)
|
// Runners (admin)
|
||||||
r.Route("/runners", func(r chi.Router) {
|
r.Route("/runners", func(r chi.Router) {
|
||||||
|
r.Use(server.RequireModule("jobs"))
|
||||||
r.Use(server.RequireRole(auth.RoleAdmin))
|
r.Use(server.RequireRole(auth.RoleAdmin))
|
||||||
r.Get("/", server.HandleListRunners)
|
r.Get("/", server.HandleListRunners)
|
||||||
r.Post("/", server.HandleRegisterRunner)
|
r.Post("/", server.HandleRegisterRunner)
|
||||||
@@ -251,6 +265,7 @@ func NewRouter(server *Server, logger zerolog.Logger) http.Handler {
|
|||||||
|
|
||||||
// Runner-facing API (runner token auth, not user auth)
|
// Runner-facing API (runner token auth, not user auth)
|
||||||
r.Route("/api/runner", func(r chi.Router) {
|
r.Route("/api/runner", func(r chi.Router) {
|
||||||
|
r.Use(server.RequireModule("jobs"))
|
||||||
r.Use(server.RequireRunnerAuth)
|
r.Use(server.RequireRunnerAuth)
|
||||||
r.Post("/heartbeat", server.HandleRunnerHeartbeat)
|
r.Post("/heartbeat", server.HandleRunnerHeartbeat)
|
||||||
r.Post("/claim", server.HandleRunnerClaim)
|
r.Post("/claim", server.HandleRunnerClaim)
|
||||||
|
|||||||
Reference in New Issue
Block a user