Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion backend/cmd/regen/commands/root.go
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down
6 changes: 2 additions & 4 deletions backend/cmd/regen/commands/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -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"
)
Expand Down Expand Up @@ -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{},
}
}

Expand Down Expand Up @@ -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",
})
})
}
2 changes: 1 addition & 1 deletion backend/internal/api/middleware/audit.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down
9 changes: 6 additions & 3 deletions backend/internal/api/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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())
{
Expand All @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion backend/internal/worker/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Loading