Documentation
¶
Overview ¶
Package admin is an admin panel generated from ORM models: paginated lists with search, sort and filters, detail pages, create/edit forms built from z schemas, deletes with confirmation, and an audit log of every change. The UI is server-rendered html/template with htmx, all embedded (no CDN, no inline scripts or styles, so it runs under a strict CSP).
Access fails closed: every route requires an authenticated user (the guard in Config), the panel ability (default "admin") and the policy ability for the action (viewAny, view, create, update, delete) on the model type. A missing ability or policy denies.
panel, err := admin.New(admin.Config{DB: db, Guard: acc.Web, Authorizer: gate})
if err != nil { return err }
if err := panel.Register(admin.NewResource[models.Post](requests.PostSchema).Search("title")); err != nil {
return err
}
return panel.Mount(r)
See https://github.com/AlexGalhardo/vukem/blob/main/docs/admin.md.
Index ¶
- Constants
- func Migration(id string) schema.Migration
- func Templates() fs.FS
- type AnyResource
- type AuditLog
- type Change
- type Config
- type Panel
- type Resource
- func (r *Resource[T]) BeforeSave(fn func(ctx context.Context, rec *T) error) *Resource[T]
- func (r *Resource[T]) Column(label string, fn func(T) string) *Resource[T]
- func (r *Resource[T]) Filter(col string, options ...string) *Resource[T]
- func (r *Resource[T]) Hidden(cols ...string) *Resource[T]
- func (r *Resource[T]) Input(key, kind string) *Resource[T]
- func (r *Resource[T]) Label(col, label string) *Resource[T]
- func (r *Resource[T]) Labels(singular, plural string) *Resource[T]
- func (r *Resource[T]) List(cols ...string) *Resource[T]
- func (r *Resource[T]) PerPage(n int) *Resource[T]
- func (r *Resource[T]) ReadOnly(cols ...string) *Resource[T]
- func (r *Resource[T]) Search(cols ...string) *Resource[T]
- func (r *Resource[T]) Sensitive(cols ...string) *Resource[T]
- func (r *Resource[T]) Slug(slug string) *Resource[T]
- func (r *Resource[T]) Sortable(cols ...string) *Resource[T]
Examples ¶
Constants ¶
const ( // AbilityAdmin is the default gate ability required for every page. AbilityAdmin = "admin" // AbilityAudit is the gate ability required to read the audit log. AbilityAudit = "viewAuditLogs" AbilityViewAny = "viewAny" AbilityView = "view" AbilityCreate = "create" AbilityUpdate = "update" AbilityDelete = "delete" )
Abilities checked by the panel. Policy abilities are checked on the model type ((*T)(nil)) or on the record (*T).
const ( ActionCreate = "create" ActionUpdate = "update" ActionDelete = "delete" )
Audit actions.
const ( InputText = "text" InputTextarea = "textarea" InputNumber = "number" InputEmail = "email" InputURL = "url" InputPassword = "password" InputCheckbox = "checkbox" InputSelect = "select" InputDate = "date" InputDateTime = "datetime-local" )
Input types a form field can be rendered with (Resource.Input).
const AuditTable = "admin_audit_logs"
AuditTable is the table holding the admin audit log.
const FlashKey = "admin.success"
FlashKey is the session flash key of the panel's success messages.
Variables ¶
This section is empty.
Functions ¶
Types ¶
type AnyResource ¶
type AnyResource interface {
// contains filtered or unexported methods
}
AnyResource is a Resource of any model, as accepted by Panel.Register. Only Resource implements it.
type AuditLog ¶
type AuditLog struct {
ID int64 `db:"id"`
// ActorID is the AuthID of the user who made the change.
ActorID string `db:"actor_id"`
// Resource is the resource slug ("posts").
Resource string `db:"resource"`
// RecordID is the primary key of the changed record.
RecordID string `db:"record_id"`
// Action is ActionCreate, ActionUpdate or ActionDelete.
Action string `db:"action"`
// Changes maps changed columns to their values before and after.
// Sensitive columns only record that they changed.
Changes orm.JSON[map[string]Change] `db:"changes"`
CreatedAt time.Time `db:"created_at"`
}
AuditLog is one change made through the admin panel.
type Change ¶
type Change struct {
Before any `json:"before,omitempty"`
After any `json:"after,omitempty"`
Redacted bool `json:"redacted,omitempty"`
}
Change is the before/after value of one column. Redacted changes (to sensitive columns) carry no values.
type Config ¶
type Config struct {
// DB stores the resources and the audit log (required).
DB *database.DB
// Guard authenticates admins (required), e.g. the "web" session guard.
// Guests are redirected to the route named "login" (401 for JSON).
Guard auth.Checker
// Authorizer decides the panel and policy abilities, usually an
// authz.Gate. Nil uses the router's Services.Authorizer; with neither,
// everything is denied.
Authorizer web.Authorizer
// Prefix is the URL prefix (default "/admin").
Prefix string
// Ability is the gate ability required for every page (default
// "admin").
Ability string
// Title is shown in the header (default "Admin").
Title string
// Views overrides embedded templates by path: layouts/admin.html,
// index.html, show.html, form.html, delete.html, dashboard.html,
// audit.html.
Views fs.FS
}
Config configures a Panel.
type Panel ¶
type Panel struct {
// contains filtered or unexported fields
}
Panel is an admin panel; build it with New, add resources with Register and serve it with Mount.
func New ¶
New validates cfg and returns an empty panel.
Example ¶
The panel needs a database, a guard and an authorizer granting the "admin" ability plus a policy per model.
mount := func(r *router.Router, db *database.DB, guard *auth.SessionGuard[User]) error {
gate := authz.New(func(ctx context.Context) (*User, error) {
u, err := auth.UserFrom[User](ctx, guard.Name())
return &u, err
})
gate.Define(admin.AbilityAdmin, func(_ context.Context, u *User) bool { return u.Role == "admin" })
if err := gate.Register(authz.For[*Post](adminPolicy[*Post]{})); err != nil {
return err
}
panel, err := admin.New(admin.Config{DB: db, Guard: guard, Authorizer: gate})
if err != nil {
return err
}
posts := admin.NewResource[Post](postSchema).
Search("title").
Filter("status", "draft", "live").
Column("Excerpt", func(p Post) string { return strings.SplitN(p.Body, "\n", 2)[0] })
if err := panel.Register(posts); err != nil {
return err
}
return panel.Mount(r) // GET /admin, /admin/posts, ...
}
_ = mount
func (*Panel) Mount ¶
Mount registers the panel's routes on r. Every route requires the guard, the panel ability and same-origin (CSRF-checked) unsafe requests.
func (*Panel) Register ¶
func (p *Panel) Register(resources ...AnyResource) error
Register validates and adds resources; configuration mistakes are returned here, at boot.
type Resource ¶
type Resource[T any] struct { // contains filtered or unexported fields }
Resource exposes model T in the admin panel: a paginated list with search, sort and filters, a detail page, and create/edit forms generated from a z schema. Options chain and return the resource; mistakes (unknown columns, sensitive columns in lists) are reported by Panel.Register, at boot.
func NewResource ¶
func NewResource[T, I any](schema z.StructSchema[I]) *Resource[T]
NewResource returns a resource for model T whose forms are generated from schema: each schema key is a form field written to the model column with the same name (or the snake_case of the input field name). Only schema keys are ever written (mass-assignment protection); the primary key and timestamps cannot be schema keys. T is given explicitly and I is inferred:
admin.NewResource[models.Post](requests.PostSchema)
func (*Resource[T]) BeforeSave ¶
BeforeSave runs fn on the record before every create and update (after validation, inside the transaction), e.g. to hash a password or set an owner. Returning an error aborts the write.
func (*Resource[T]) Column ¶
Column adds a computed column to the list; fn's result is HTML-escaped.
func (*Resource[T]) Filter ¶
Filter adds an exact-match filter on col; with options it renders a select, otherwise a text box. Boolean columns get yes/no options.
func (*Resource[T]) Hidden ¶
Hidden hides columns from the list, the detail page and the forms; hidden schema keys are never written.
func (*Resource[T]) Input ¶
Input overrides the form control of a schema key (one of the Input* constants).
func (*Resource[T]) List ¶
List sets the columns of the list, in order (default: every column that is not hidden or sensitive).
func (*Resource[T]) ReadOnly ¶
ReadOnly shows columns in the edit form without letting the admin change them: read-only schema keys keep the stored value and are never written.
func (*Resource[T]) Search ¶
Search enables the search box over the given string columns (case-insensitive LIKE with bound parameters).
func (*Resource[T]) Sensitive ¶
Sensitive marks columns holding secrets (password hashes, tokens) beyond the ones detected by name or json:"-": they are never displayed, listed, searched, sorted, filtered or written to the audit log. A sensitive schema key renders as an empty password field and keeps the stored value when left blank on edit.