Documentation
¶
Overview ¶
Package admin is tanGO's installable admin site: list, search, create, edit and delete pages for every model an application registers with it, behind a session-cookie login with CSRF protection and rate-limited login attempts.
Install it by adding New to a project's InstalledApps. Apps register their models through the admin Registry they receive, with Options for list columns, search, ordering and form presentation. Accounts are managed from the command line (tango admin create, resetpassword, deactivate, grant-staff and the rest), which HandleCLI serves, or directly with CreateAccount and its siblings.
Widget, the presentation fields of Options, the built-in widgets and Branding are best-effort: they may change in any minor release. The admin's HTML, templates, CSS and URLs are not covered by the compatibility promise either. See https://tangoframework.com/docs/guides/admin-registration/.
Index ¶
- Constants
- Variables
- func ByteSize(size int64) string
- func ContentSecurityPolicy() tango.CSP
- func CreateAccount(ctx context.Context, store *db.Store, username string, password string, ...) error
- func Deactivate(ctx context.Context, store *db.Store, username string) error
- func GrantStaff(ctx context.Context, store *db.Store, username string) error
- func GrantSuperuser(ctx context.Context, store *db.Store, username string) error
- func HandleCLI(ctx context.Context, store *db.Store, args []string, stdin io.Reader, ...) (bool, error)
- func New(store *db.Store, opts ...Option) tango.App
- func ResetPassword(ctx context.Context, store *db.Store, username string, password string) error
- func RevokeStaff(ctx context.Context, store *db.Store, username string) error
- func RevokeSuperuser(ctx context.Context, store *db.Store, username string) error
- func ShortHash(digest string) string
- type AccountOption
- type AdminSession
- type AdminUser
- type Branding
- type DashboardConfig
- type DashboardStatus
- type DatabaseState
- type Display
- type DisplayFunc
- type FieldContext
- type FieldError
- type FieldValues
- type MarkdownOption
- type MarkdownPreview
- type ModelRegistration
- type Option
- type Options
- type PreviewWidget
- type Registry
- type SelectOption
- type TrackingTableState
- type Widget
Constants ¶
const EventBulkDelete = "tango.admin.bulk_delete"
EventBulkDelete is the structured log event emitted once for each bulk delete that committed. It carries the model name, selected_count (the number of selected rows accepted, not the rows physically deleted, which cascades can raise) and the Admin account's username, never keys or row values.
Variables ¶
var ErrAccountExists = errors.New("tango: admin account already exists")
ErrAccountExists is returned by CreateAccount when the username is already taken.
var ErrAccountNotFound = errors.New("tango: admin account not found")
ErrAccountNotFound is returned by ResetPassword and Deactivate when no account with the given username exists.
var NewRegistry = adminregistry.NewRegistry
NewRegistry returns an empty, ready-to-use admin Registry.
Functions ¶
func ByteSize ¶ added in v0.4.0
ByteSize formats a byte count with binary units and one decimal place, dropping a trailing ".0": 512 is "512 B", 1536 is "1.5 KiB" and 1 MiB is "1 MiB". A negative size is shown as "0 B". It is a presentation helper for changelist cells.
func ContentSecurityPolicy ¶ added in v0.8.0
ContentSecurityPolicy returns the policy the admin's own pages satisfy. The admin sends no policy by default. A host opts in by composing it into the admin tier:
admin.New(store, admin.WithMiddleware( tango.SecurityHeaders(tango.WithContentSecurityPolicy(admin.ContentSecurityPolicy())), ))
A host whose branding names a logo on another origin adds that origin to ImgSrc before composing the policy. Each call returns a new value, so such a change does not reach other callers.
func CreateAccount ¶
func CreateAccount(ctx context.Context, store *db.Store, username string, password string, opts ...AccountOption) error
CreateAccount creates a new Admin account with a bcrypt-hashed password. It is the only code path in tanGO that accepts a raw password and turns it into a stored credential — see the "tango admin create" CLI command. The account defaults to IsStaff=true, IsSuperuser=true; pass WithoutStaff and/or WithoutSuperuser to opt out of either.
func Deactivate ¶
Deactivate disables an account (without deleting its row, preserving audit history) and invalidates every existing session for it.
func GrantStaff ¶
GrantStaff sets IsStaff=true on an existing account, leaving Active, PasswordHash, and IsSuperuser untouched. Unlike Deactivate, this does not invalidate existing sessions: IsStaff is checked fresh from the database on every request (not cached in the session), so a revoked account is denied on its very next request regardless.
func GrantSuperuser ¶
GrantSuperuser sets IsSuperuser=true on an existing account. See GrantStaff for why this does not invalidate existing sessions.
func HandleCLI ¶
func HandleCLI(ctx context.Context, store *db.Store, args []string, stdin io.Reader, stdout io.Writer, stderr io.Writer) (bool, error)
HandleCLI scans args for the "tango admin" app-side flags (-tango-admin-create=<username>, -tango-admin-resetpassword=<username>, -tango-admin-deactivate=<username>, -tango-admin-grant-staff=<username>, -tango-admin-revoke-staff=<username>, -tango-admin-grant-superuser=<username>, -tango-admin-revoke-superuser=<username>) and, if one is present, performs the corresponding account operation against store and reports handled=true. If none is present it returns handled=false, nil so the caller (a generated main.go) proceeds to its other flag handling / serving.
Unlike tango.DispatchFlags, HandleCLI does not use the flag package: both functions are called with the same os.Args[1:] from a generated main.go, and flag.Parse errors out on any flag it doesn't itself define — it can't tolerate seeing the other dispatcher's flags. A manual scan lets each dispatcher recognize only its own flags and ignore the rest.
create and resetpassword take the new password from TANGO_ADMIN_PASSWORD when it is set and non-empty, and otherwise read it from stdin. Neither is a flag value, so it never appears in a shell history or the process arguments. The environment is visible to process-inspection tools, so prefer the deployment platform's secret mechanism for long-lived values.
func New ¶
New constructs the admin application. Authentication is session-cookie based, against Admin accounts managed by the "tango admin" CLI family (create/resetpassword/deactivate) — there is no static credential value to pass in here. opts configures optional admin-wide behavior, currently WithBranding and WithMiddleware; admin.New(store) with no options is unchanged from before Option existed.
func ResetPassword ¶
ResetPassword hashes and stores a new password for an existing account, and invalidates every existing session for it.
func RevokeStaff ¶
RevokeStaff sets IsStaff=false on an existing account. See GrantStaff for why this does not invalidate existing sessions.
func RevokeSuperuser ¶
RevokeSuperuser sets IsSuperuser=false on an existing account. See GrantStaff for why this does not invalidate existing sessions.
Types ¶
type AccountOption ¶
type AccountOption func(*AdminUser)
AccountOption customizes a new admin account's IsStaff/IsSuperuser flags at creation time. CreateAccount defaults both to true — "create an immediately usable full admin account" is its long-standing behavior — so these are opt-outs, not opt-ins.
func WithoutStaff ¶
func WithoutStaff() AccountOption
WithoutStaff creates the account with IsStaff=false instead of the default true.
func WithoutSuperuser ¶
func WithoutSuperuser() AccountOption
WithoutSuperuser creates the account with IsSuperuser=false instead of the default true.
type AdminSession ¶
type AdminSession struct {
ID int64 `tango:"pk"`
Token string `tango:"unique"`
UserID int64 `tango:"index"`
ExpiresAt time.Time
}
AdminSession is one active login for an AdminUser: a session token, which AdminUser it belongs to, and a fixed expiry. Deleting a session's row logs out that one login without affecting the AdminUser it belongs to. Like AdminUser, it is registered as an ordinary model but never exposed through the admin's own CRUD UI.
type AdminUser ¶
type AdminUser struct {
ID int64 `tango:"pk"`
Username string `tango:"unique"`
PasswordHash string
Active bool
// IsStaff gates whether an already-authenticated, active account may
// access the admin panel at all. Checked by requireSession alongside
// (not instead of) the existing session-validity check. Independent of
// Active: Active is checked at login, IsStaff only on an already-valid
// session. It is the only one of these two flags with real effect.
IsStaff bool
// IsSuperuser is currently ignored: it has no distinct behavior
// and does not bypass IsStaff — a non-staff superuser still gets 403,
// same as any other non-staff account. It exists only as forward-
// compatible groundwork for a future, finer-grained permission bypass,
// so that a later milestone doesn't force a second migration for one
// boolean column.
IsSuperuser bool
CreatedAt time.Time
}
AdminUser is an Admin account: a single authenticatable admin identity. Its PasswordHash is only ever written by the "tango admin" CLI family (create/resetpassword), which is the sole path that produces a valid bcrypt hash — no other code path accepts a raw password for this field. AdminUser is registered as an ordinary tanGO model (picked up by "tango makemigrations" like any project model) but is never registered with the admin's own CRUD registry, so it never appears in the generic admin UI.
type Branding ¶
type Branding struct {
// Name replaces the sidebar's generic "tanGO Admin" title when set.
Name string
// LogoURL replaces the sidebar's generic brand mark with an <img> when
// set — typically a URL the app already serves via its own embed.FS
// route.
LogoURL string
}
Branding lets an app replace admin's generic sidebar title with its own name and logo — the only two named customization slots admin exposes beyond field-level Widgets. This surface is best-effort, not a stable contract, and grants no access to any other part of admin's layout/template.
type DashboardConfig ¶ added in v0.6.0
type DashboardConfig struct {
// Status reports database and migration state. The host builds it from
// the values it owns, for example with tango.Status. Without it the
// status cards say they are not configured.
Status func(context.Context) (DashboardStatus, error)
// Activity reports recent request and job activity, for example an
// observability.Window. Without it the activity card says so.
Activity observability.Snapshotter
}
DashboardConfig selects what the optional dashboard shows. Either field may be nil; the dashboard exists only when at least one is set.
type DashboardStatus ¶ added in v0.6.0
type DashboardStatus struct {
Database DatabaseState
Dialect string
Applied int
Pending []string
TrackingTable TrackingTableState
// Release is the tanGO version the application was built with.
Release string
// Started is when this process started. It is not the start of the
// Activity window, which Snapshot.Since reports.
Started time.Time
}
DashboardStatus is the safe display data a status provider hands the dashboard. It holds plain values only: no DSN, credential, error text or host metadata, so nothing in it can leak what the host did not choose to show.
type DatabaseState ¶ added in v0.6.0
type DatabaseState string
DatabaseState is whether the host's status provider could reach its database.
const ( DatabaseReachable DatabaseState = "reachable" DatabaseUnreachable DatabaseState = "unreachable" DatabaseUnknown DatabaseState = "unknown" )
The values a status provider reports for the database.
type Display ¶ added in v0.4.0
type Display = adminregistry.Display
Display is what one changelist cell shows: escaped Text, and an optional Href that is rendered only when it is a safe URL (see safeHref).
type DisplayFunc ¶ added in v0.4.0
type DisplayFunc = adminregistry.DisplayFunc
DisplayFunc computes one changelist cell from a model row, which is passed as a pointer to the model value. Set it through Options.Display; see adminregistry.DisplayFunc.
type FieldContext ¶
type FieldContext = adminregistry.FieldContext
FieldContext, FieldValues, Widget, and SelectOption are defined in internal/adminregistry (aliased here) rather than in this package, because Options.Widgets needs to reference Widget without admin and adminregistry importing each other in a cycle — the same reason Options itself is an alias below.
type FieldError ¶ added in v0.4.0
type FieldError struct {
Message string
// contains filtered or unexported fields
}
FieldError is a validation failure a Widget's Parse reports for its own field. Admin shows Message beside that field, marks the field's control aria-invalid and re-renders the form with a 422. Any other Parse error is shown beside its own field as a generic message.
Parse may return a FieldError (or a *FieldError) directly, wrapped with fmt.Errorf("%w"), or inside errors.Join; every one found is shown.
func (FieldError) Error ¶ added in v0.4.0
func (e FieldError) Error() string
type FieldValues ¶
type FieldValues = adminregistry.FieldValues
FieldContext, FieldValues, Widget, and SelectOption are defined in internal/adminregistry (aliased here) rather than in this package, because Options.Widgets needs to reference Widget without admin and adminregistry importing each other in a cycle — the same reason Options itself is an alias below.
type MarkdownOption ¶ added in v0.4.0
type MarkdownOption func(*markdownConfig)
MarkdownOption configures the Markdown widget built by Markdown.
func WithMarkdownPreview ¶ added in v0.4.0
func WithMarkdownPreview(preview MarkdownPreview) MarkdownOption
WithMarkdownPreview sets the host's renderer for the Markdown widget's preview. A widget without one is a plain Markdown editor.
type MarkdownPreview ¶ added in v0.4.0
MarkdownPreview renders a Markdown source to HTML for the Markdown widget's preview. The host owns rendering and sanitization policy: tanGO ships no Markdown parser and does not sanitize what the callback returns.
type ModelRegistration ¶
type ModelRegistration = adminregistry.ModelRegistration
ModelRegistration is the admin metadata for a registered model.
type Option ¶
type Option func(*adminConfig)
Option configures admin.New.
func WithBranding ¶
WithBranding sets the admin's brand name and logo. Omitting it — calling admin.New(store) with no options, as before this existed — leaves the sidebar showing tanGO's generic default, unchanged.
func WithDashboard ¶ added in v0.6.0
func WithDashboard(cfg DashboardConfig) Option
WithDashboard turns the admin landing page into an operational dashboard. An app that omits it, or passes a config with neither source, keeps the landing page it had before.
func WithMiddleware ¶
func WithMiddleware(middleware ...tango.Middleware) Option
WithMiddleware wraps every admin route with host-provided Middleware. It is scoped to admin's internal /admin/ route group; global tango.Config.Middleware still wraps outside it.
func WithTrustedProxies ¶ added in v0.3.0
WithTrustedProxies tells the login limiter which reverse proxies sit in front of the application. Without it the limiter counts failed logins per connection address and never reads a forwarded header, which is right for a direct deployment. Behind a proxy that address is the proxy's, so every user would share one limit. Naming the proxy's network here makes the limiter use the client address the proxy saw: the rightmost X-Forwarded-For entry outside these networks, and only for a request whose connection comes from one of them. A header from any other peer is ignored. This affects the login limiter and nothing else.
type Options ¶
type Options = adminregistry.Options
Options controls how a model appears in the admin.
type PreviewWidget ¶ added in v0.4.0
type PreviewWidget = adminregistry.PreviewWidget
FieldContext, FieldValues, Widget, and SelectOption are defined in internal/adminregistry (aliased here) rather than in this package, because Options.Widgets needs to reference Widget without admin and adminregistry importing each other in a cycle — the same reason Options itself is an alias below.
type Registry ¶
type Registry = adminregistry.Registry
Registry stores admin registrations by model name.
type SelectOption ¶
type SelectOption = adminregistry.SelectOption
FieldContext, FieldValues, Widget, and SelectOption are defined in internal/adminregistry (aliased here) rather than in this package, because Options.Widgets needs to reference Widget without admin and adminregistry importing each other in a cycle — the same reason Options itself is an alias below.
type TrackingTableState ¶ added in v0.6.0
type TrackingTableState string
TrackingTableState is whether the migration tracking table exists.
const ( TrackingTablePresent TrackingTableState = "present" TrackingTableMissing TrackingTableState = "missing" TrackingTableUnknown TrackingTableState = "unknown" )
The values a status provider reports for the tracking table.
type Widget ¶
type Widget = adminregistry.Widget
FieldContext, FieldValues, Widget, and SelectOption are defined in internal/adminregistry (aliased here) rather than in this package, because Options.Widgets needs to reference Widget without admin and adminregistry importing each other in a cycle — the same reason Options itself is an alias below.
func JSON ¶ added in v0.4.0
func JSON() Widget
JSON returns tanGO's JSON Widget for a string or text field, set via Options.Widgets. It validates JSON syntax on save and offers a browser-side Formatted tab that shows the text indented; the server stays the authority, and the saved value is exactly the submitted text.
func Markdown ¶ added in v0.4.0
func Markdown(opts ...MarkdownOption) Widget
Markdown returns tanGO's Markdown Widget: a monospace, resizable textarea with a character count. Set it on a string field via Options.Widgets, e.g. Options{Widgets: map[string]Widget{"Bio": admin.Markdown()}}. It stores the submitted text exactly as typed.
Source Files
¶
- accounts.go
- admin.go
- assets.go
- branding.go
- bulk_delete.go
- cli.go
- content_security_policy.go
- create.go
- csrf.go
- dashboard.go
- dashboard_activity.go
- dashboard_status.go
- delete.go
- density_cookie.go
- density_view.go
- display.go
- doc.go
- edit.go
- error_page.go
- fielderror.go
- fields.go
- fk_errors.go
- flash.go
- flash_view.go
- form_summary.go
- format.go
- hidden.go
- href.go
- index.go
- json_widget.go
- layout.go
- list.go
- list_filter_choices.go
- list_filters.go
- list_links.go
- login.go
- markdown.go
- markdown_preview.go
- models.go
- nouns.go
- ratelimit.go
- redirect.go
- related.go
- render.go
- session.go
- signed_in.go
- theme_cookie.go
- theme_view.go
- unique_errors.go
- widget.go
- widget_text.go