Documentation
¶
Overview ¶
Package admin serves the app's admin pages at /admin: an overview with a setup checklist, users, the credentials of the sign-in, mail, language-model and storage providers (saved sealed with the master key and applied without a restart) plus the app's own (Options.Sections), token usage, the mail outbox, jobs and stored files. Mount it from routes.go:
admin.Mount(r, admin.Options{})
The first account and the users ADMIN_USERS lists (ids or emails, comma separated) get in, unless Options.Allow decides otherwise. The pages are server-rendered HTML on Tabler (https://tabler.io, MIT), light and dark, with their stylesheet, script and icons served from the binary. An app themes them with admin/theme.css (the --admin-* variables, or any Tabler variable) and replaces the frame with admin/layout.html.
Index ¶
Constants ¶
const EnvAdminUsers = "ADMIN_USERS"
EnvAdminUsers lists who may open the admin pages.
Variables ¶
This section is empty.
Functions ¶
func TemplateFS ¶
templateFS exposes the embedded templates, for an app that wants to start its layout.html from the pack's.
Types ¶
type Action ¶
Action handles one of a page's forms. It reads r.Form (parsed) and the request's context, and returns the message the page shows next.
type Audit ¶
type Audit struct {
// Path is the action's address under the admin pages, without the
// prefix: "/users/<subject>/disable", "/settings", "/orders/refund",
// "/orders/export".
Path string
// Form is what the form sent, secrets redacted: a secret setting's
// value, every credential value, and any field named like a password,
// secret, token, key or code read "[redacted]"; long values are cut
// to 500 characters; an uploaded file is "file: name (bytes)".
Form url.Values
// Message is the page's confirmation; Error its alert. One is set for
// a form; neither for a download, whose Status says how it went.
Message, Error string
// Download reports a download; Status is its HTTP status.
Download bool
Status int
// Request is the request (the admin, the client address).
Request *http.Request
}
Audit is one admin action, as Options.OnAudit sees it: a form sent on any page (the pack's own and the app's) or a download.
type Choice ¶ added in v0.1.126
Choice is one value a field may hold, with a label when the source gives one.
type Field ¶
type Field struct {
// Name is the environment name (MAIL_API_KEY).
Name string
Label string
// Kind is "text", "number", "url" (an http or https address),
// "secret" (never shown again once saved), "select" (one of Options)
// or "multi" (any of Options, saved comma-separated).
Kind string
Options []string
// Labels name options for people ("Cloudflare R2" for r2).
Labels map[string]string
// Dev lists the options meant for development and tests (log, fake,
// local); the page flags a section that runs on one.
Dev []string
// Default is what the pack uses when the setting is empty.
Default string
Help string
Placeholder string
// Placeholders vary the placeholder with the selector's value
// (the default model of each provider).
Placeholders map[string]string
// Group gathers fields under a heading within the section (Google,
// SMTP server).
Group string
// For lists the values of the section's selector under which the
// field applies (mailgun, sendgrid); empty means always. The empty
// value ("") names the state where nothing is chosen, for a selector
// whose empty value works (Section.None).
For []string
// Advanced folds the field away under "Advanced" (a proxy address, a
// token limit): rarely needed, never required.
Advanced bool
// Choices, when set, lists what the field may hold given the
// section's other values as they stand on the page (a provider's
// models for its key): the page suggests them as the operator types,
// and a value outside the list is refused on Save with the nearest
// ones named. An error (no key yet, the provider unreachable) leaves
// the field free text; llm.ErrNoModelList means there is no list.
Choices func(ctx context.Context, values map[string]string) ([]Choice, error)
}
Field is one setting.
type Handler ¶
type Handler struct {
// contains filtered or unexported fields
}
Handler serves the pages.
type Link ¶
type Link struct {
// For is the selector value it belongs to.
For, Title, URL string
}
Link is a pointer to a provider's console, shown next to the section's fields while that provider is chosen.
type Options ¶
type Options struct {
// Path is where the pages live; "/admin" by default.
Path string
// Dir holds the app's theme on disk: Dir/theme.css replaces the
// variables, Dir/layout.html the frame. "admin" by default. Both are
// read from Templates first, so an app that embeds them runs from the
// binary alone.
Dir string
// Title heads every page; the app's name is a good one. "Admin" by
// default.
Title string
// Version is the app's own version ("v1.4.0", a build id), shown
// beside Līdza's in the footer and on the Overview; empty shows
// Līdza's alone.
Version string
// Auth protects the pages; auth.Require() by default. Tests pass a
// no-op.
Auth middleware.Middleware
// Allow decides whether the signed-in user is an admin. By default
// the first user ever to sign in is one, and so is anyone whose id
// or email claim ADMIN_USERS lists (in .env, the credentials, or
// saved from the Overview page).
Allow func(ctx context.Context) bool
// OnAudit runs after every admin action, for a staff audit log: each
// form sent on any page (the pack's own and the app's, with its
// outcome) and each download. Audit.Form has the secrets redacted. It
// cannot refuse anything and runs on the request: insert a row.
OnAudit func(ctx context.Context, a Audit)
// FirstUser returns the id of the app's first account, an admin by
// default; the auth pack's FirstSubject unless set. Empty means no
// such rule.
FirstUser func(ctx context.Context) string
// NoFirstUserAdmin turns that rule off: only listed users get in.
NoFirstUserAdmin bool
// WorkspaceOwner is the auth.Teams owner role the Workspaces page
// lists; "owner" when empty.
WorkspaceOwner string
// FoldGroups folds the sidebar's labelled groups (Services and the
// app's Page.Group sections) but the one holding the page shown; a
// click opens one. For an app with many pages, so the list fits.
FoldGroups bool
// CredentialsDir is where config/credentials.yml.enc lives when no
// db pack holds the runtime values; "." by default.
CredentialsDir string
// Sections add the app's own settings to the Credentials page after
// the packs': API keys of the services the app calls, feature
// switches. Each field is an environment name the app reads with
// pkg/env, saved sealed like the packs' and applied through
// lidza.Reconfigure.
Sections []Section
// Pages add the app's own admin pages (orders to refund, reports,
// moderation queues) to the sidebar, rendered in the same frame. See
// Page.
Pages []Page
// Templates holds the app's page templates, theme.css and
// layout.html, usually the app's admin/ folder embedded
// (//go:embed admin, then lidza.Sub(files, "admin")) so they ship in
// the binary. A file it lacks is read from Dir on disk; the theme and
// the frame then fall back to the pack's own.
Templates fs.FS
}
Options configure Mount.
type Page ¶
type Page struct {
// Name is the sidebar entry and the page's title ("Orders").
Name string
// Path is the page's address under the admin pages ("orders" serves
// /admin/orders); it may not be one of the built-in pages'.
Path string
// Icon is one of the vendored Tabler icons (assets/icons.txt in the
// admin pack); "layout-dashboard" when empty.
Icon string
// Group is the sidebar section the page is listed in ("Billing").
// Pages with the same Group are listed together, the groups in the
// order their first page appears in Options.Pages; pages without one
// are under "App", with the app's settings.
Group string
// Template names the page's file in Options.Templates (else in
// Options.Dir on disk). It defines "content" and reads .Data.
Template string
// Data loads what the page shows, from the request's context (db.From,
// the packs). An error is shown on the page instead of the content.
Data func(r *http.Request) (any, error)
// Actions are the page's forms: a POST to <Path>/<name> runs
// Actions[name] and redirects back to the page with its message, or
// with its error as an alert. A form that posts the frame's .Back as
// "back" (<input type="hidden" name="back" value="{{$.Back}}">)
// returns to the view it was sent from: the same page with the same
// query (?order=1001), never another address.
Actions map[string]Action
// MaxUpload, when above zero, lets the page's forms send files
// (enctype="multipart/form-data"): the body is limited to MaxUpload
// bytes and an action reads r.MultipartForm.File; a larger body is
// refused with an alert. Zero refuses file forms.
MaxUpload int64
// Downloads answer GET <Path>/<name> behind the same gate as the
// page: an attachment, an export. The handler writes the whole
// response; set Content-Type, Content-Disposition and
// X-Content-Type-Options: nosniff for anything a user uploaded.
Downloads map[string]http.HandlerFunc
}
Page is an admin page the app adds: an entry in the sidebar under the app's heading, served at Options.Path plus Page.Path behind the same admin gate, and rendered inside the pack's frame, so it has the sidebar, the light and dark themes, Tabler's classes, the icons and the template functions (icon, since, bytes, num, dict, initials) of the built-in pages.
admin.Mount(r, admin.Options{
Templates: lidza.Sub(adminFS, "admin"), // //go:embed admin
Pages: []admin.Page{{
Name: "Orders", Path: "orders", Icon: "inbox", Template: "orders.html",
Data: func(r *http.Request) (any, error) { return queries.New(db.From(r.Context())).RecentOrders(r.Context()) },
Actions: map[string]admin.Action{"refund": refundOrder},
}},
})
type Section ¶
type Section struct {
// Key names the section in the page's URL (?section=mail).
Key string
// Page is the pack page the section sits on (mail, llm, storage,
// users); an app's sections (empty Page) are on the Settings page.
Page string
// Links point at the providers' consoles, per selector value.
Links []Link
// Title and Description head the section; Icon is a Tabler icon
// name among the vendored ones (assets/icons.txt).
Title, Description, Icon string
// Selector names the field whose value decides which other fields
// apply (MAIL_PROVIDER, AUTH_PROVIDERS); see Field.For.
Selector string
Fields []Field
// Enabled reports whether the section applies to the running app;
// nil means always.
Enabled func(ctx context.Context) bool
// None, when set, makes an empty selector a working state with this
// status text (sign-in with email and password alone), instead of
// "Not set up".
None string
// Optional marks a section whose settings all have defaults: it is
// "Defaults" until one is set, never "Not set up" or "Incomplete".
Optional bool
}
Section is one group of settings on the Credentials page: a pack's provider and keys, or the app's own (Options.Sections). Every field is an environment name the app reads through pkg/env; the page saves it sealed with the master key and asks the packs to reload.