admin

package
v1.9.6 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 12, 2026 License: Apache-2.0 Imports: 53 Imported by: 0

Documentation

Overview

Package admin provides an auto-generated administration panel for Nucleus, similar to Django's contrib.admin. It exposes a REST API for CRUD operations on registered models and serves an embedded SPA frontend.

Index

Constants

View Source
const DefaultPrefix = "/admin"
View Source
const DefaultTitle = "Orbit"

DefaultTitle is the product name shown in the UI when no Title is configured. The panel is Orbit — it used to introduce itself as "Nucleus Admin", which left the product invisible in its own interface (OH-2/PR-ORB-01).

Variables

This section is empty.

Functions

func EnsureBootstrapAdminUsersSchema

func EnsureBootstrapAdminUsersSchema(ctx context.Context, sqlDB *sql.DB, system string) error

EnsureBootstrapAdminUsersSchema guarantees the admin users table exists WITHOUT inserting any user. orbit's mount calls it unconditionally, so the schema exists even when BootstrapPassword is empty and the operator provisions the admin account another way (e.g. `nucleus createuser`, whose own error message tells them to "start the app once to create the schema" — advice that used to be false, because the schema was only created when a bootstrap password was set, leaving the secure-default onboarding with no way in).

func NormalizePrefix

func NormalizePrefix(raw string) string

NormalizePrefix canonicalizes the admin mount path.

func ParseImportData

func ParseImportData(reader io.Reader, format string) ([]map[string]interface{}, error)

ParseImportData parses uploaded data and returns a slice of record maps.

func ValidateImportConfig

func ValidateImportConfig(cfg ImportConfig, src datasource.DataSource) error

ValidateImportConfig checks import configuration.

Types

type AdminAuth

type AdminAuth interface {
	Authenticate(r *http.Request) (*auth.User, error)
	Authorize(user *auth.User, model string, action string) bool
	LoginHandler() http.Handler
}

AdminAuth is the interface for admin panel authentication and authorization.

type AuditEntry

type AuditEntry struct {
	ID        uint           `json:"id"`
	UserID    string         `json:"user_id"`
	Username  string         `json:"username"`
	Action    string         `json:"action"`
	ModelName string         `json:"model_name"` // Model or surface affected (e.g. "User", "rbac", "feature_flag")
	RecordID  string         `json:"record_id"`  // ID of the affected record (or key / name of the affected object)
	OldValue  map[string]any `json:"old_value"`  // Previous state (updates, deletes, removals) — redacted
	NewValue  map[string]any `json:"new_value"`  // New state or outcome (creates, updates, management actions) — redacted
	IP        string         `json:"ip"`
	UserAgent string         `json:"user_agent"`
	CreatedAt time.Time      `json:"created_at"`
}

AuditEntry represents a single audit log record.

Every mutating handler of the panel records its own entry (see audit_coverage_test.go for the route-by-route contract). Action is the verb: Data Studio uses create/update/delete/bulk_delete/bulk_export and schema.update; the management surfaces use dotted names (rbac.policy.add, flag.set, migration.apply, cache.flush, export.create, import.execute, live.exclude.add, audit.clear, ...); the session surfaces use login, login.failed, login.locked, logout and session.terminate.

type BootstrapAdminConfig

type BootstrapAdminConfig struct {
	Username string
	Email    string
	Password string

	// System is the database dialect of the target connection, as
	// reported by `db.DB.System()` ("sqlite", "postgresql", "mysql",
	// "mssql", "oracle"). It selects the dialect-appropriate
	// CREATE-TABLE form for the admin users table. An empty System
	// falls back to the portable `CREATE TABLE IF NOT EXISTS` form
	// (accepted by SQLite, PostgreSQL, and MySQL) — preserving the
	// pre-dialect-aware behaviour for any caller that does not set it.
	System string
}

BootstrapAdminConfig defines how the framework should initialize the first admin account when the admin users table is empty.

type BootstrapAdminResult

type BootstrapAdminResult struct {
	Created           bool
	Username          string
	Password          string
	PasswordGenerated bool
}

BootstrapAdminResult reports whether a bootstrap admin account was created. Password is only populated when generated by the framework.

func EnsureBootstrapAdminUser

func EnsureBootstrapAdminUser(ctx context.Context, sqlDB *sql.DB, cfg BootstrapAdminConfig) (BootstrapAdminResult, error)

EnsureBootstrapAdminUser guarantees the admin users table exists and creates one default superuser if the table is currently empty.

type DatabaseAdminAuth

type DatabaseAdminAuth struct {
	// contains filtered or unexported fields
}

DatabaseAdminAuth is the default admin auth provider wired by pkg/app. Behavior: - Admin is always protected: login is required.

func NewDatabaseAdminAuth

func NewDatabaseAdminAuth(sqlDB *sql.DB, session *auth.SessionManager, prefix string) *DatabaseAdminAuth

func (*DatabaseAdminAuth) Authenticate

func (a *DatabaseAdminAuth) Authenticate(r *http.Request) (*auth.User, error)

Authenticate returns an authenticated admin user from server-side session.

func (*DatabaseAdminAuth) Authorize

func (a *DatabaseAdminAuth) Authorize(user *auth.User, _ string, _ string) bool

Authorize currently allows all actions for authenticated admin users.

func (*DatabaseAdminAuth) LoginHandler

func (a *DatabaseAdminAuth) LoginHandler() http.Handler

LoginHandler renders the login page (GET) and validates credentials (POST).

func (*DatabaseAdminAuth) WithAuthChain

func (a *DatabaseAdminAuth) WithAuthChain(chain *auth.Chain) *DatabaseAdminAuth

NewDatabaseAdminAuth creates a DB-backed AdminAuth provider that validates credentials against nucleus_admin_users (same table used by createuser). WithAuthChain delegates credential verification to the application's declared authentication chain (auth_backends), so an operator who configured a corporate directory gets directory login in the admin panel without orbit shipping an LDAP client.

It delegates AUTHENTICATION only. Authorization stays here: a directory user who is not in the admin table is refused. Skipping that would turn an LDAP integration into a privilege escalation — every employee in the company would become an administrator of this panel.

func (*DatabaseAdminAuth) WithSystem

func (a *DatabaseAdminAuth) WithSystem(system string) *DatabaseAdminAuth

WithSystem names the SQL dialect of the admin database so that the user lookups Authenticate and the login run on every request are bounded queries (WHERE id = ? / WHERE LOWER(username) = LOWER(?) OR ...) instead of a full read of the admin table followed by a linear scan. orbit.go passes db.DB.System() of the auth database. A caller that leaves it unset keeps the full-read path, which needs no placeholder style.

func (*DatabaseAdminAuth) WithTitle

func (a *DatabaseAdminAuth) WithTitle(title string) *DatabaseAdminAuth

WithTitle sets the heading the login page renders — the panel's configured Title. Returns the receiver for chaining, like WithAuthChain.

type DatabaseRuntimeInfo

type DatabaseRuntimeInfo struct {
	Alias     string `json:"alias"`
	Engine    string `json:"engine"`
	Dialect   string `json:"dialect"`
	IsDefault bool   `json:"is_default"`
}

DatabaseRuntimeInfo describes one configured DB alias for admin observability.

type DjangoFixtureRecord

type DjangoFixtureRecord struct {
	Model  string                 `json:"model"`
	PK     interface{}            `json:"pk"`
	Fields map[string]interface{} `json:"fields"`
}

DjangoFixtureRecord represents a single record in Django-style fixture format. Format: {"model": "app.ModelName", "pk": 1, "fields": {...}}

type DumpdataConfig

type DumpdataConfig struct {
	Models   []string `json:"models"`    // Models to export (empty = all)
	Database string   `json:"database"`  // Source database alias
	TenantID string   `json:"tenant_id"` // Tenant scope (empty = all)
}

DumpdataConfig configures the dumpdata operation.

type ExportConfig

type ExportConfig struct {
	Models   []string          `json:"models"`    // Models to export (empty = all registered)
	Database string            `json:"database"`  // Source database alias
	TenantID string            `json:"tenant_id"` // Tenant scope (empty = all)
	Format   ExportFormat      `json:"format"`    // csv | json | sql
	Filters  map[string]string `json:"filters"`   // Additional filters (model.field=value)
}

ExportConfig defines the scope and format of an export operation.

type ExportFormat

type ExportFormat string

ExportFormat defines supported export formats.

const (
	ExportFormatCSV  ExportFormat = "csv"
	ExportFormatJSON ExportFormat = "json"
	ExportFormatSQL  ExportFormat = "sql"
)

type ExportResult

type ExportResult struct {
	ID         string    `json:"id"`
	Status     string    `json:"status"` // completed, processing, failed
	Format     string    `json:"format"`
	Tenant     string    `json:"tenant,omitempty"` // tenant the export was confined to ("" = every tenant)
	Filename   string    `json:"filename"`
	StorageKey string    `json:"storage_key"` // Key in storage for download
	Size       int64     `json:"size"`
	Records    int       `json:"records"`
	Error      string    `json:"error,omitempty"`
	CreatedAt  time.Time `json:"created_at"`
	URL        string    `json:"url,omitempty"` // Download URL when available
}

ExportResult holds the result of an export operation.

type ImportConfig

type ImportConfig struct {
	Database   string `json:"database"`    // Target database alias
	TenantID   string `json:"tenant_id"`   // Target tenant (for tenant field injection)
	Model      string `json:"model"`       // Target model
	Format     string `json:"format"`      // csv | json
	OnConflict string `json:"on_conflict"` // skip | update | error
	DryRun     bool   `json:"dry_run"`     // Validate only, no changes
	BatchSize  int    `json:"batch_size"`  // Records per batch (default 100)
}

ImportConfig defines the target and behavior of an import.

type ImportError

type ImportError struct {
	Row     int    `json:"row"`
	Field   string `json:"field,omitempty"`
	Message string `json:"message"`
}

ImportError describes a single row import error.

func ValidateImportData

func ValidateImportData(mi datasource.ModelInfo, records []map[string]interface{}, tenantID string) []ImportError

ValidateImportData validates records against model schema without importing.

type ImportReport

type ImportReport struct {
	Total    int           `json:"total"`
	Imported int           `json:"imported"`
	Skipped  int           `json:"skipped"`
	Updated  int           `json:"updated"`
	Failed   int           `json:"failed"`
	Errors   []ImportError `json:"errors"`
	DryRun   bool          `json:"dry_run"`
}

ImportReport summarizes import results.

type LoaddataConfig

type LoaddataConfig struct {
	StorageKey string `json:"key"`         // Storage key of the fixture file
	OnConflict string `json:"on_conflict"` // "skip" (default) or "update"
	Database   string `json:"database"`    // Target database alias
	TenantID   string `json:"tenant_id"`   // Tenant ID for auto-injection
}

LoaddataConfig configures the loaddata operation.

type Panel

type Panel struct {
	// contains filtered or unexported fields
}

Panel is the admin panel instance that provides CRUD UI for registered models.

func NewPanel

func NewPanel(src datasource.DataSource, logger *slog.Logger, cfg PanelConfig) *Panel

NewPanel creates a new admin panel. Data Studio reads and writes records through the neutral datasource contract (ADR-001); the Nucleus-backed adapter is built by the caller (orbit.go) from the same Runtime accessors and passed in as src. The panel no longer imports the model registry for record access — only the optional cfg.SchemaRegistry, for the field-metadata editor.

func (*Panel) Close

func (p *Panel) Close(ctx context.Context) error

Close releases optional admin panel background resources.

func (*Panel) ConsumeEventBus

func (p *Panel) ConsumeEventBus(eb nucleus.EventBus) func()

ConsumeEventBus wires the live SQL and HTTP feeds to the framework's first-party EventBus (nucleus.Runtime.Observability()) — the orbit counterpart to ConsumeObservability, which takes the experimental *observability.Bus directly (SQL only). It subscribes to SQL and HTTP events, drains them into the live ring buffers + stream, and returns a stop function (also invoked from Close) that cancels both subscriptions.

The HTTP lane is what makes host-application traffic reach the in-process panel: the framework's app-level HTTP middleware emits an HTTPEvent for every request, so the panel no longer depends on the host mounting LiveTrafficMiddleware (an internal type the module API cannot expose). Events whose path falls under the admin prefix are skipped here — the panel's own middleware lane records those (and only those), so a request is never recorded twice; live_exclude_patterns apply the same way they do in the middleware and the cluster relay ingest.

The EventBus hands back detached value events and owns the underlying bus's pooled-event Release discipline internally, so these drains are plain (no Release, no buffered-event drain on cancel — cancel() closes the channels). Safe no-op when p, p.live, or eb is nil.

func (*Panel) ConsumeObservability

func (p *Panel) ConsumeObservability(bus *observability.Bus) func()

ConsumeObservability wires the live view's SQL feed to the application observability bus. The bus receives EVERY model.CRUD query across the whole application — the framework's default SQL observer (installed in pkg/app) emits to it — not just the queries issued by the admin's own Data Studio CRUDs. This is the "bus becomes the single SQL feed" step: application queries (REST resources, app-side CRUD) now surface in the live view, which previously only saw the admin panel's own browsing.

It marks the panel as bus-connected so getCRUD skips the now-redundant per-CRUD observer (avoiding double-recording), starts a goroutine draining the subscription into the SQL ring buffer + live stream, and returns a stop function (also invoked from Close). Safe no-op when p, p.live, or bus is nil.

func (*Panel) Dumpdata

func (p *Panel) Dumpdata(ctx context.Context, cfg DumpdataConfig) (ExportResult, error)

Dumpdata exports registered models to a Django-compatible JSON fixture file. Each record is serialized as {"model": "AppName.ModelName", "pk": <id>, "fields": {...}}.

func (*Panel) EnableLiveClusterRelay

func (p *Panel) EnableLiveClusterRelay() error

EnableLiveClusterRelay enables cluster-aware live telemetry distribution. It is optional and safe to call multiple times.

func (*Panel) ExecuteImport

func (p *Panel) ExecuteImport(ctx context.Context, cfg ImportConfig, records []map[string]interface{}) (*ImportReport, error)

ExecuteImport imports validated records into the database.

func (*Panel) FeatureFlag

func (p *Panel) FeatureFlag(name string) (enabled bool, ok bool)

FeatureFlag returns one in-memory feature flag value.

func (*Panel) Handler

func (p *Panel) Handler() *router.Mux

Handler returns a *router.Mux that can be mounted on the application router.

func (*Panel) ImportFromFile

func (p *Panel) ImportFromFile(ctx context.Context, storageKey string, cfg ImportConfig) (*ImportReport, error)

ImportFromFile handles the complete import flow: read file → parse → validate → import.

func (*Panel) LiveTrafficMiddleware

func (p *Panel) LiveTrafficMiddleware() func(http.Handler) http.Handler

LiveTrafficMiddleware returns non-blocking runtime observation middleware that records requests into the live feed. In a framework-wired deployment it is NOT the lane host-application traffic arrives on: ConsumeEventBus drains the framework's HTTP events (emitted by the app-level middleware), so mounting this at app level is unnecessary there — the panel keeps it on its own SPA branch, where it also records session activity (which the bus event does not carry). It remains useful for hand-wired panels that have no nucleus.EventBus to consume.

func (*Panel) Loaddata

func (p *Panel) Loaddata(ctx context.Context, cfg LoaddataConfig) (*ImportReport, error)

Loaddata imports data from a Django-compatible JSON fixture file. It auto-detects models from the "model" field, skips unknown models, and handles conflicts based on the OnConflict setting.

func (*Panel) SetFeatureFlag

func (p *Panel) SetFeatureFlag(name string, enabled bool)

SetFeatureFlag upserts one in-memory feature flag value.

func (*Panel) SetSignalBus

func (p *Panel) SetSignalBus(bus *signals.Bus)

SetSignalBus sets the signal bus for CRUD operations.

type PanelConfig

type PanelConfig struct {
	Prefix              string // URL prefix (default "/admin")
	Title               string // Site title shown in the UI
	Environment         string
	OTLPEndpoint        string          // optional OTLP endpoint configured by the host app
	RedisURL            string          // optional Redis URL for background jobs runtime snapshot
	TaskInspector       tasks.Inspector // optional configured queue inspector
	LiveExcludePatterns []string        // optional path patterns excluded from live HTTP capture
	LiveClusterEnabled  bool            // when true, publish/subscribe live telemetry through Redis
	LiveClusterRedisURL string          // optional Redis URL for live cluster relay (falls back to RedisURL)
	LiveClusterChannel  string          // optional pub/sub channel (default nucleus:admin:live:v1)
	LiveClusterNodeID   string          // optional explicit node id (defaults to runtime identity)
	LiveClusterToken    string          // optional shared token to reject untrusted relay messages
	TraceURLTemplate    string          // optional trace explorer URL template (supports {trace_id})
	Databases           []DatabaseRuntimeInfo
	DatabaseHandles     map[string]*db.DB // optional alias->db handle mapping for runtime stats
	EnvironmentSnapshot []string          // optional env snapshot (defaults to os.Environ at startup)
	FeatureFlags        map[string]bool   // optional initial in-memory feature flags
	MailDriver          string
	MailFrom            string
	SMTPHost            string
	Auth                AdminAuth
	Session             *auth.SessionManager // optional session manager for admin telemetry
	SessionStore        string               // configured session store label (memory|sql|redis)
	SessionRuntime      auth.SessionRuntimeIdentity

	// Multi-tenant configuration. When enabled, Data Studio is confined to
	// the tenant of each request — list, get, create, update, delete, bulk,
	// CSV export, exports (and their job list, status and download),
	// imports and fixtures — resolved by TenantResolver (the host
	// application's request scope) and falling back to MultiTenantDefault.
	// A request that resolves no tenant is refused (403) unless its operator
	// may switch tenants; it does not fall back to every tenant. Only a
	// superuser, or a subject granted the tenant_switch RBAC action on
	// admin:*, may switch the request to another tenant or to all of them
	// with ?tenant=; every switch is audited. The confinement trusts the
	// resolver: a tenant read from a header the client controls is the
	// client's choice, not the host's.
	MultiTenantEnabled    bool     // whether multi-tenant mode is active
	MultiTenantDefault    string   // default tenant ID when none resolved
	MultiTenantAutoFilter bool     // confine Data Studio to the request's tenant (default true when multi-tenant enabled)
	MultiTenantField      string   // override tenant field name (empty = auto-detect from model)
	MultiTenantIDs        []string // known tenant IDs for the selector UI (empty = discover from scope)
	// TenantResolver returns the tenant the host application resolved for the
	// request (nucleus resolves it from the subdomain or a header before the
	// panel runs). Nil means no host resolution: the default tenant applies.
	TenantResolver   func(*http.Request) (tenant string, ok bool)
	MultiSiteEnabled bool     // whether multi-site mode is active
	MultiSiteDefault string   // default site name
	MultiSiteNames   []string // known site names for the selector UI

	// RBAC configuration
	RBACEnforcer *authz.Enforcer // optional Casbin enforcer for fine-grained authorization

	// Audit logging configuration
	AuditEnabled bool // whether audit logging is enabled
	AuditMaxSize int  // max audit entries in memory (default 10000)

	// Migrations path
	MigrationsPath string // path to migration files directory

	// Storage for exports/imports
	Store storage.Store

	// SchemaRegistry is an optional Nucleus model registry used ONLY by the
	// runtime field-metadata editor (handleUpdateFieldMeta), which mutates model
	// metadata and has no equivalent in the neutral datasource contract. Data
	// Studio CRUD does not use it — it speaks datasource.DataSource (ADR-001).
	SchemaRegistry *model.Registry
}

PanelConfig configures the admin panel.

type TenantContext

type TenantContext struct {
	Enabled     bool   // Whether multi-tenant is enabled
	TenantID    string // Current tenant ID (empty = global/all tenants)
	TenantField string // The column name for tenant isolation
	AutoFilter  bool   // Whether Data Studio is confined to TenantID
	Overridden  bool   // TenantID came from ?tenant= (superuser / tenant_switch)
}

TenantContext holds the tenant resolution of one admin request. It is built by tenantContextMiddleware from, in order of precedence, the ?tenant= override (gated and audited), the tenant the host resolved for the request (PanelConfig.TenantResolver) and the configured default.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL