admin

package
v1.1.16 Latest Latest
Warning

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

Go to latest
Published: Aug 22, 2026 License: MPL-2.0 Imports: 36 Imported by: 0

Documentation

Overview

Package admin serves the CMS admin area: login, dashboard, and user management, with content, media, and settings arriving in later phases. All UI assets are embedded; the package has no external runtime files.

Index

Constants

View Source
const (
	// DefaultMaxRequestBytes bounds an authenticated unsafe request when
	// the host sets no Deps.MaxRequestBytes and there is no media manager
	// to size one from.
	DefaultMaxRequestBytes = 64 << 20
)

Request body ceilings, applied by the csrf middleware — see readToken for why they have to live there rather than in the handlers.

View Source
const DefaultPerPage = 25

DefaultPerPage is how many rows a paginated admin list shows when Deps does not say. An editor's table is a working list rather than a page of a site, so it holds rather more than a public listing does.

View Source
const SectionPathPrefix = "/x"

SectionPathPrefix is the URL segment custom sections are mounted under, between the admin path and the section's own path: a section with Path "reports" serves {AdminPath}/x/reports. The namespace keeps host sections from ever colliding with built-in admin routes, present or future.

Variables

This section is empty.

Functions

func CSRFToken

func CSRFToken(r *http.Request) string

CSRFToken returns the session's CSRF token for a request served by a custom admin section. Forms that POST back to the section must send it in a hidden csrf_token field (or an X-CSRF-Token header); the admin middleware rejects unsafe requests without it.

func New

func New(d Deps) http.Handler

New returns the admin http.Handler. The host mounts it under Deps.AdminPath with the prefix stripped.

func RenderPage

func RenderPage(w http.ResponseWriter, r *http.Request, title string, body template.HTML)

RenderPage writes a 200 response wrapping body in the standard admin chrome (top bar, navigation, flash messages, admin stylesheet). Body is trusted host HTML, inserted unescaped. For any other status code or a fully custom look, write the response directly instead.

The admin serves a strict Content-Security-Policy with no unsafe-inline, so inline <script> and <style> in body are blocked by browsers; serve scripts and stylesheets as files from the section's own handler.

func SectionPath

func SectionPath(r *http.Request) string

SectionPath returns the browser-facing base URL of the custom admin section serving this request, with a trailing slash — e.g. "/admin/x/reports/". Section handlers see mount-stripped paths, so a redirect after a POST must target this absolute URL, not a relative one:

http.Redirect(w, r, admin.SectionPath(r), http.StatusSeeOther)

Append a segment for sub-routes: SectionPath(r) + "settings". Outside a section handler it returns "".

func SetFlash

func SetFlash(r *http.Request, msg string)

SetFlash queues a one-time message shown at the top of the next admin page the user loads — the usual post/redirect/get confirmation. It is a no-op outside a section handler.

func UserFrom

func UserFrom(r *http.Request) *auth.User

UserFrom returns the logged-in CMS user for a request served by a custom admin section. Inside a section handler it is never nil — the admin middleware has already required a login. Outside one it is nil.

func ValidateSections

func ValidateSections(sections []Section) error

ValidateSections checks host-registered sections for empty, malformed, or duplicate paths and nil handlers. cms.New calls it; it is exported for hosts that want to fail earlier.

Types

type DashboardCard added in v0.9.0

type DashboardCard struct {
	// Title is the card's heading. Empty uses the section's NavLabel;
	// one of the two must be set.
	Title string

	// Description is the card's one-line explanation, shown under the
	// title. Host text, rendered as-is.
	Description string

	// Count supplies the card's number. Called once per dashboard render
	// with the request context; an error is logged and the count renders
	// as zero, exactly as a failed NavCount does. Nil falls back to the
	// section's NavCount, and a card with neither shows no number.
	Count func(ctx context.Context) (int, error)

	// Note, when non-nil, supplies a short dynamic line rendered under
	// the description — the card's freshness or urgency in the host's
	// words ("Oldest unhandled: 2 days"). Called once per dashboard
	// render; returning "" shows nothing, and an error is logged and
	// shows nothing, so a note never blocks the page.
	Note func(ctx context.Context) (string, error)
}

DashboardCard is a section's card on the admin dashboard: a heading, a one-line description, and a number. The number is the card's point — a dashboard answers "what needs my attention?", so give it the count that asks for attention (items pending, unread submissions), which is not always the nav link's "how many are there".

type Deps

type Deps struct {
	Sessions *scs.SessionManager
	Users    *auth.Store
	Content  *content.Store
	Renderer *render.Renderer // nil when the host has not configured templates
	// RequestFuncs binds the host's template functions
	// (Config.TemplateFuncs) to a request, for the page and post
	// previews. Previews run the same page templates the public site
	// does, so a function that reads the request's context has to be
	// bound here too or it behaves subtly differently under preview than
	// in front of a visitor. Nil when the host registered none.
	RequestFuncs func(*http.Request) template.FuncMap
	Media        *media.Manager // nil when the host has not configured an object store
	Snippets     *snippets.Store
	// CodeSnippets is the custom-code library — the markup-and-JavaScript
	// blocks pages reference by key. Nil leaves the editor's code API
	// unmounted, so the drawer offers no code blocks.
	CodeSnippets   *snippets.CodeStore
	Captcha        *captcha.Client       // nil when login CAPTCHA is not configured
	Mailer         Mailer                // nil disables the forgot-password flow
	ConfigSnippets []snippets.Snippet    // host-registered palette entries
	SectionStyles  *render.SectionStyles // curated section settings
	Sections       []Section             // host-registered admin pages, already validated
	Permissions    []PermissionDef       // host-declared custom permissions, already validated
	Logger         *slog.Logger
	AdminPath      string
	DefaultLocale  string
	Locales        []string // all configured locales, [0] = DefaultLocale

	// Version is the CMS release the layout stamps in the footer of every
	// admin page (the host passes cms.Version()). Empty hides the footer,
	// which is what tests and direct package use get.
	Version string

	// PostTemplate is the template blog and news posts render with; the
	// zero value disables the Blog & News admin.
	PostTemplate render.PageTemplate

	// RememberFor is how long a "Remember me" login persists. The zero
	// value falls back to 30 days so a partially-populated Deps (tests,
	// direct package use) behaves sensibly.
	RememberFor time.Duration

	// PerPage is how many rows a paginated admin list shows on one page.
	// The zero value falls back to DefaultPerPage, like RememberFor.
	PerPage int

	// MaxRequestBytes caps the body of an unsafe request from a signed-in
	// user. It is enforced by the CSRF middleware, which is the first
	// thing to read the body and therefore the only place a cap can still
	// take effect — see readToken.
	//
	// Raise it above the largest upload the admin accepts, remembering
	// that a multipart post carries every field at once: a form taking
	// forty 32 MB photos needs more than 32 MB here. The zero value is
	// sized from the media manager's own limits, or DefaultMaxRequestBytes
	// when there is none. Requests with no session user are held to a much
	// smaller fixed ceiling regardless of this value.
	MaxRequestBytes int64

	// SiteBaseURL returns the site's absolute public base
	// ("scheme://host", no trailing slash) for the given request, so the
	// admin can offer links that work when pasted somewhere else. Nil
	// leaves such links site-relative, which is fine for tests and direct
	// package use.
	SiteBaseURL func(*http.Request) string

	// SiteDevelopment reports whether the site is in development mode
	// (content.SiteSettings.Mode), which the sidebar stamps so nobody
	// forgets a finished site is still hidden from search engines. Nil
	// leaves the stamp off, which is what tests and direct package use
	// want; the CMS supplies its own cached reader.
	SiteDevelopment func(context.Context) bool

	// ContentChanged, when set, is called (without waiting) after any
	// mutation that can change which CSS classes stored content uses:
	// region/section saves, publish/discard, page create/delete, and
	// snippet changes. The CMS uses it to rebuild the generated
	// Tailwind stylesheet.
	ContentChanged func()
}

Deps is everything the admin area needs from the rest of the CMS.

type Mailer added in v0.9.0

type Mailer interface {
	Send(ctx context.Context, to, subject, textBody, htmlBody string) error
}

Mailer sends the messages the CMS itself originates — today only the password reset email. The CMS authors the content; an implementation supplies only delivery, so it decides transport and From address and nothing else. Implementations must be safe for concurrent use.

htmlBody may be empty, in which case the message is plain text only.

type PermissionDef added in v0.9.0

type PermissionDef struct {
	Key             auth.Permission
	Label           string
	AdminsNeedGrant bool

	// GrantsMedia opens the media library to holders of this permission.
	//
	// The library is shared: one bucket behind every section that puts a
	// picture on the site, and a bulk delete inside it reaches all of
	// them. So it is not open to anyone merely signed in — it is open to
	// people who edit something that carries media. The CMS's own pages,
	// blogs, and news imply that on their own; a host section has to say
	// so, because only the host knows whether its records have pictures in
	// them.
	GrantsMedia bool
}

PermissionDef declares a custom permission to the admin so the user form can offer it as a grant checkbox: Key is what handlers check with auth.User.Can, Label is the checkbox text. Sections that name a Permission are declared automatically; cms.Config.Permissions is for permissions not tied to a section.

AdminsNeedGrant marks a permission that gates the admin role too (see Section.AdminsNeedGrant); the user form annotates its checkbox so whoever is granting knows it binds admins as well as editors. For a permission carried by a section, it must match the section's own flag — two answers to "does this bind admins?" would be a configuration error, and cms.New refuses it.

type Section

type Section struct {
	// Path is the URL segment the section is mounted under: the section
	// root is served at {AdminPath}/x/{Path}/ (the bare URL without the
	// trailing slash redirects there, so relative links inside the
	// section resolve under it). One path segment of RFC 3986 unreserved
	// characters (letters, digits, "-", ".", "_", "~").
	Path string

	// NavLabel is the section's link text in the admin top bar. Empty
	// means no nav link; the section is still routable.
	NavLabel string

	// NavAfter names the built-in sidebar entry this section's link
	// follows: "dashboard", "pages", "posts", "media", "snippets", or
	// "users". Empty keeps the default placement, after the built-in
	// entries. Sections naming the same anchor keep their registration
	// order, and the link holds its position even when the anchor itself
	// is hidden from the user. Ignored when NavLabel is empty.
	NavAfter string

	// NavCount, when non-nil, supplies the number shown beside the nav
	// link, with the same leader line and count style as the built-in
	// entries ("Inventory ····· 42"). Called with the request context on
	// every admin page render whose sidebar shows the link, so it should
	// be a cheap query — the built-in counts run the same way. An error
	// is logged and the count renders as zero, exactly as a failed
	// built-in count does. Ignored when NavLabel is empty.
	NavCount func(ctx context.Context) (int, error)

	// Confirm, when non-empty, makes the nav link ask before it
	// navigates: clicking it opens the admin's shared confirmation
	// dialog with this message ("Question? Detail." — the question
	// becomes the heading), and the browser only follows the link on a
	// yes. For nav entries that do something — generate a file, run a
	// job — rather than open a page. Ignored when NavLabel is empty.
	Confirm string

	// AdminOnly restricts the section to users with the admin role.
	// Editors receive 403 and don't see the nav link.
	AdminOnly bool

	// SuperadminOnly restricts the section to superadmins. Everyone
	// else — admins included — receives 403 and doesn't see the nav
	// link. Subsumes AdminOnly; setting both is allowed and redundant.
	SuperadminOnly bool

	// Permission, when non-empty, restricts the section to users holding
	// the named permission (admin roles hold every permission). Naming a
	// built-in permission reuses it; any other key declares a custom
	// permission that appears as a grant checkbox on the user form,
	// labelled with NavLabel. Composes with AdminOnly: both must pass.
	Permission string

	// AdminsNeedGrant makes Permission an explicit grant for the admin
	// role too: admins see and open the section only when the permission
	// is ticked on their user page, exactly as editors do. Superadmins
	// hold everything, as always. Without it, Permission keeps its usual
	// meaning — admins hold every permission implicitly. Requires
	// Permission to be set.
	AdminsNeedGrant bool

	// Dashboard, when non-nil, puts a card for this section on the admin
	// dashboard, linking to the section root. Host cards render ahead of
	// the built-in cards (which are superadmin-only), in registration
	// order, and a card is shown exactly when the section's nav link
	// would be: SuperadminOnly, AdminOnly, Permission, and
	// AdminsNeedGrant all apply.
	Dashboard *DashboardCard

	// Handler serves the section's requests. The mount prefix is
	// stripped: it sees "/" at the section root and may serve its own
	// sub-routes and static assets beneath it. Requests only reach the
	// handler with a logged-in user, and unsafe methods (POST, PUT, ...)
	// have already passed CSRF validation — forms need only include
	// CSRFToken(r) as the csrf_token field.
	Handler http.Handler
}

Section is one host-registered admin extension: an http.Handler mounted inside the admin's middleware chain (session, CSRF validation, security headers, and login requirement), with an optional link in the admin's top navigation bar.

Jump to

Keyboard shortcuts

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