package cabana import ( "encoding/json" "net/http" "strings" "time" "git.golem15.com/golem15/summercms/modules/bouncer" "git.golem15.com/golem15/summercms/modules/pact" "gorm.io/gorm" ) // BackendUserRole is the Winter backend_user_roles row. type BackendUserRole struct { ID uint `gorm:"column:id;primaryKey"` Name string `gorm:"column:name"` Code string `gorm:"column:code"` Description string `gorm:"column:description"` Permissions string `gorm:"column:permissions"` IsSystem bool `gorm:"column:is_system"` CreatedAt time.Time `gorm:"column:created_at"` UpdatedAt time.Time `gorm:"column:updated_at"` } func (BackendUserRole) TableName() string { return "backend_user_roles" } // BackendUser is the Winter backend_users row. It is not a frontend user. // Nullable Winter columns are mapped so a copied row round-trips without a // schema transform. TokensValidAfter is the admin reset cutoff, not a Winter column. type BackendUser struct { ID uint `gorm:"column:id;primaryKey"` FirstName string `gorm:"column:first_name"` LastName string `gorm:"column:last_name"` Login string `gorm:"column:login"` Email string `gorm:"column:email"` Password string `gorm:"column:password"` ActivationCode string `gorm:"column:activation_code"` PersistCode string `gorm:"column:persist_code"` ResetPasswordCode string `gorm:"column:reset_password_code"` Permissions string `gorm:"column:permissions"` IsActivated bool `gorm:"column:is_activated"` IsSuperuser bool `gorm:"column:is_superuser"` RoleID *uint `gorm:"column:role_id"` ActivatedAt *time.Time `gorm:"column:activated_at"` LastLogin *time.Time `gorm:"column:last_login"` CreatedAt time.Time `gorm:"column:created_at"` UpdatedAt time.Time `gorm:"column:updated_at"` DeletedAt gorm.DeletedAt `gorm:"column:deleted_at"` TokensValidAfter *time.Time `gorm:"column:tokens_valid_after"` Role BackendUserRole } func (BackendUser) TableName() string { return "backend_users" } // Option is a dropdown choice shared with later schema plans. type Option = pact.Option // WritableField is one schema field bound to a model fill key at activation. // FillKey comes from the model column, not from request JSON. type WritableField struct { Name string FillKey string } // CompiledController is one admin controller after YAML compilation. type CompiledController struct { PluginID string Controller pact.AdminController List *ListSchema Form *FormSchema Relations map[string]*CompiledRelation Writable []WritableField // FieldRelations are the form's `type: relation` fields keyed by field name. FieldRelations map[string]*CompiledFieldRelation // Actions are the controller's pact.HasAdminActions entries keyed by name: // the single namespace that toolbar.buttons names and widget action: keys // resolve through. create and delete are reserved built-in names. Actions map[string]pact.AdminAction // scripts and styles are the controller's declared plugin JS and CSS, // in declared order. scripts []*pluginAsset styles []*pluginAsset // partials are the parsed controller partials (headerPartial and every // `type: partial` path) keyed by name; formPartials are the names form // fields declare, the only ones a request may render with a record id. partials map[string]*compiledPartial formPartials map[string]bool } // Registry is the immutable controller map keyed by controller ID. type Registry struct { byID map[string]*CompiledController permissions map[string]pact.Permission roleGrants map[string]map[string]bool navigation []pact.NavigationItem settings map[string]*CompiledSetting // assets are every controller's declared plugin files keyed by URL tail // (vendor/plugin/); the asset route serves only these. assets map[string]*pluginAsset } // Get returns the compiled controller for a D-09 id (vendor.plugin.controller). func (r *Registry) Get(id string) (*CompiledController, bool) { if r == nil { return nil, false } cc, ok := r.byID[id] return cc, ok && cc != nil } // Setting returns one compiled singleton setting by stable code. func (r *Registry) Setting(code string) (*CompiledSetting, bool) { if r == nil { return nil, false } setting, ok := r.settings[code] return setting, ok && setting != nil } // Allows reports whether principal satisfies every required permission code. // A nil principal fails. Superusers pass. An empty requirement list allows // any authenticated principal. Grants ending in ".*" match by prefix. func Allows(principal *bouncer.Principal, required []string) bool { if principal == nil { return false } if principal.IsSuperuser || len(required) == 0 { return true } for _, code := range required { if !granted(principal.PermissionGrants, code) { return false } } return true } func granted(grants map[string]bool, code string) bool { if grants[code] { return true } for key, on := range grants { if !on || !strings.HasSuffix(key, ".*") { continue } prefix := strings.TrimSuffix(key, "*") if strings.HasPrefix(code, prefix) { return true } } return false } func requiredOf(ctl pact.AdminController) []string { if p, ok := ctl.(pact.AdminPermissioned); ok && p != nil { return p.RequiredPermissions() } return nil } // WriteData writes a D-10 success envelope. func WriteData(w http.ResponseWriter, status int, data, meta any) { if meta == nil { meta = map[string]any{} } writeJSON(w, status, map[string]any{"data": data, "meta": meta}) } // WriteError writes a D-10 error envelope. details is always an object. func WriteError(w http.ResponseWriter, status int, code, message string) { WriteErrorDetails(w, status, code, message, nil) } // WriteErrorDetails writes a D-10 error envelope with field messages. func WriteErrorDetails(w http.ResponseWriter, status int, code, message string, details map[string]any) { if details == nil { details = map[string]any{} } writeJSON(w, status, map[string]any{ "error": map[string]any{ "code": code, "message": message, "details": details, }, }) } func writeJSON(w http.ResponseWriter, status int, body any) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) _ = json.NewEncoder(w).Encode(body) }