diff --git a/backend/cmd/regen/commands/root.go b/backend/cmd/regen/commands/root.go index 4307bf7..97e3a52 100644 --- a/backend/cmd/regen/commands/root.go +++ b/backend/cmd/regen/commands/root.go @@ -1,6 +1,19 @@ package commands -import "github.com/spf13/cobra" +import ( + "github.com/FluidifyAI/Regen/backend/enterprise" + "github.com/spf13/cobra" +) + +// proHooks holds the enterprise extension points used by the serve command. +// Defaults to no-op stubs; regen-pro overrides via SetEnterpriseHooks. +var proHooks = enterprise.NewNoOp() + +// SetEnterpriseHooks replaces the default no-op hooks with Pro implementations. +// Must be called before Execute(). +func SetEnterpriseHooks(h enterprise.Hooks) { + proHooks = h +} // NewRootCmd returns the cobra root command. All subcommands are attached here. func NewRootCmd() *cobra.Command { diff --git a/backend/cmd/regen/commands/serve.go b/backend/cmd/regen/commands/serve.go index a58064b..f4bc537 100644 --- a/backend/cmd/regen/commands/serve.go +++ b/backend/cmd/regen/commands/serve.go @@ -16,7 +16,6 @@ import ( "github.com/FluidifyAI/Regen/backend/internal/coordinator" "github.com/FluidifyAI/Regen/backend/internal/coordinator/agents" "github.com/FluidifyAI/Regen/backend/internal/database" - "github.com/FluidifyAI/Regen/backend/internal/enterprise" "github.com/FluidifyAI/Regen/backend/internal/licence" "github.com/FluidifyAI/Regen/backend/internal/metrics" "github.com/FluidifyAI/Regen/backend/internal/redis" @@ -177,9 +176,8 @@ func runServe(_ *cobra.Command, _ []string) error { } // Enterprise hooks — no-op stubs in the OSS build. - // Replace enterprise.NewNoOp() with the real implementation in the - // enterprise binary to unlock SCIM, audit log export, RBAC, and retention. - enterpriseHooks := enterprise.NewNoOp() + // regen-pro sets these via SetEnterpriseHooks() before calling Execute(). + enterpriseHooks := proHooks // Create the telemetry worker before SetupRoutes so we can inject its // announcement getter as a closure — no import cycle with internal/worker. diff --git a/backend/internal/enterprise/enterprise.go b/backend/enterprise/enterprise.go similarity index 71% rename from backend/internal/enterprise/enterprise.go rename to backend/enterprise/enterprise.go index 7fecd01..de97807 100644 --- a/backend/internal/enterprise/enterprise.go +++ b/backend/enterprise/enterprise.go @@ -4,7 +4,7 @@ // // Enterprise repo usage: // -// import "github.com/FluidifyAI/Regen/backend/internal/enterprise" +// import "github.com/FluidifyAI/Regen/backend/enterprise" // // hooks := enterprise.Hooks{ // RBAC: myrbac.NewProvider(db), @@ -17,9 +17,11 @@ package enterprise import ( "context" + "io/fs" "net/http" "time" + "github.com/FluidifyAI/Regen/backend/ui" "github.com/gin-gonic/gin" "gorm.io/gorm" ) @@ -82,24 +84,47 @@ type RetentionEnforcer interface { Start(ctx context.Context, db *gorm.DB) } +// ── UI ──────────────────────────────────────────────────────────────────────── + +// UIProvider supplies the embedded frontend filesystem served by the API server. +// The OSS no-op returns the OSS build; the Pro binary returns a Pro-built FS +// that includes all Pro-only pages and components. +type UIProvider interface { + // FS returns the embedded frontend as an fs.FS rooted at dist/, or nil when + // no frontend has been built (the API still works, just no SPA). + FS() fs.FS +} + +// ── Custom Fields ───────────────────────────────────────────────────────────── + +// CustomFieldsHandler mounts custom field definition endpoints. +// The no-op stub returns 402 on all routes — custom fields require a Pro licence. +type CustomFieldsHandler interface { + RegisterRoutes(group *gin.RouterGroup, db *gorm.DB) +} + // ── Hooks — the single struct threaded through the app ─────────────────────── // Hooks is passed from serve.go to routes.go and worker.StartAll. // All fields default to their no-op stubs via NewNoOp(). type Hooks struct { - RBAC RBACProvider - Audit AuditExporter - SCIM SCIMHandler - Retention RetentionEnforcer + RBAC RBACProvider + Audit AuditExporter + SCIM SCIMHandler + Retention RetentionEnforcer + CustomFields CustomFieldsHandler + UI UIProvider } // NewNoOp returns Hooks with all no-op stubs — the default for the OSS build. func NewNoOp() Hooks { return Hooks{ - RBAC: noopRBAC{}, - Audit: noopAudit{}, - SCIM: noopSCIM{}, - Retention: noopRetention{}, + RBAC: noopRBAC{}, + Audit: noopAudit{}, + SCIM: noopSCIM{}, + Retention: noopRetention{}, + CustomFields: noopCustomFields{}, + UI: noopUI{}, } } @@ -132,3 +157,20 @@ func (noopSCIM) RegisterRoutes(group *gin.RouterGroup) { type noopRetention struct{} func (noopRetention) Start(_ context.Context, _ *gorm.DB) {} + +// noopUI serves the OSS-built frontend. When no frontend has been compiled, +// FS() returns nil and the router silently skips static file serving. +type noopUI struct{} + +func (noopUI) FS() fs.FS { return ui.FS() } + +// noopCustomFields returns 402 on all routes — custom fields are a Pro feature. +type noopCustomFields struct{} + +func (noopCustomFields) RegisterRoutes(group *gin.RouterGroup, _ *gorm.DB) { + group.Any("/*path", func(c *gin.Context) { + c.JSON(http.StatusPaymentRequired, gin.H{ + "error": "custom fields require a Fluidify Regen Pro licence", + }) + }) +} diff --git a/backend/internal/api/middleware/audit.go b/backend/internal/api/middleware/audit.go index d2e1d4d..0f4e1b5 100644 --- a/backend/internal/api/middleware/audit.go +++ b/backend/internal/api/middleware/audit.go @@ -3,7 +3,7 @@ package middleware import ( "time" - "github.com/FluidifyAI/Regen/backend/internal/enterprise" + "github.com/FluidifyAI/Regen/backend/enterprise" "github.com/gin-gonic/gin" ) diff --git a/backend/internal/api/routes.go b/backend/internal/api/routes.go index 931ae4e..06a30a4 100644 --- a/backend/internal/api/routes.go +++ b/backend/internal/api/routes.go @@ -12,12 +12,11 @@ import ( "github.com/FluidifyAI/Regen/backend/internal/api/handlers" "github.com/FluidifyAI/Regen/backend/internal/api/middleware" "github.com/FluidifyAI/Regen/backend/internal/config" - "github.com/FluidifyAI/Regen/backend/internal/enterprise" + "github.com/FluidifyAI/Regen/backend/enterprise" "github.com/FluidifyAI/Regen/backend/internal/metrics" "github.com/FluidifyAI/Regen/backend/internal/models/webhooks" "github.com/FluidifyAI/Regen/backend/internal/repository" "github.com/FluidifyAI/Regen/backend/internal/services" - "github.com/FluidifyAI/Regen/backend/ui" "github.com/crewjam/saml/samlsp" "github.com/gin-gonic/gin" "github.com/prometheus/client_golang/prometheus/promhttp" @@ -437,6 +436,10 @@ func SetupRoutes(router *gin.Engine, db *gorm.DB, cfg *config.Config, teamsSvc * settingsGroup.PATCH("/system/telemetry", handlers.PatchTelemetrySettings(systemSettingsRepo)) } + // Custom fields — Pro tier; no-op returns 402 in OSS build. + cfGroup := protected.Group("/custom-fields", middleware.RequireAdmin()) + hooks.CustomFields.RegisterRoutes(cfGroup, db) + // Migrations — admin only (OPE-67) migrationsGroup := protected.Group("/migrations", middleware.RequireAdmin()) { @@ -456,7 +459,7 @@ func SetupRoutes(router *gin.Engine, db *gorm.DB, cfg *config.Config, teamsSvc * // ui.Files() returns nil when the frontend has not been built (e.g. local // development using `npm run dev`), in which case we skip static serving // so the API remains fully functional on its own. - if distFS := ui.FS(); distFS != nil { + if distFS := hooks.UI.FS(); distFS != nil { slog.Info("serving embedded frontend") // Read index.html once at startup. All SPA routes serve this same file; diff --git a/backend/internal/worker/start.go b/backend/internal/worker/start.go index 6a7ef70..ea20bcb 100644 --- a/backend/internal/worker/start.go +++ b/backend/internal/worker/start.go @@ -5,7 +5,7 @@ import ( "log/slog" "github.com/FluidifyAI/Regen/backend/internal/config" - "github.com/FluidifyAI/Regen/backend/internal/enterprise" + "github.com/FluidifyAI/Regen/backend/enterprise" "github.com/FluidifyAI/Regen/backend/internal/repository" "github.com/FluidifyAI/Regen/backend/internal/services" "gorm.io/gorm"