Help us shape the future of Go! Take the 2026 Go Developer Survey

admin

package
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Sep 16, 2026 License: GPL-3.0 Imports: 28 Imported by: 0

Documentation

Overview

Package admin is the operator's panel: sign in, sign out, a layout, and one page that says what this server is and how it is doing.

It has grown a settings page since: everything an operator would otherwise have written into the settings table by hand, grouped the way they think about it rather than the way it is stored — who this team is, what it sounds like, what the language-model features may spend, what the limits are, and the two things that cannot be undone.

Drivers, devices and data followed, through that same seam: the roster and what each driver has recorded, every paired machine and the one button that signs one out, and what is stored together with the retention that bounds it. Build client is the last of them: the page an operator presses once to get the Windows client their drivers install, already carrying this server's address, and the page where the signing question is put to them honestly rather than sold.

It is server-rendered html/template, embedded in the binary. No JavaScript framework, no content delivery network, nothing fetched from anywhere: an operator's admin panel should work on a machine with no route to the internet, and the content security policy that allows nothing is only possible because there is nothing to allow.

Index

Constants

View Source
const (
	CertificatePath       = ClientsPath + "/certificate"
	CertificateRemovePath = CertificatePath + "/remove"
)

Where the certificate is uploaded and removed, both under the build page.

View Source
const CSRFCookie = "pacenote_admin_csrf"

CSRFCookie is the other half of the double-submit check on every form that changes something.

View Source
const ClientDownloadPath = ClientsPath + "/download/"

ClientDownloadPath is the prefix a built client is downloaded from. The last part of the address is the build's reference, which is also the name of the file on the disk and the identifier stamped inside the binary.

View Source
const ClientsPath = "/admin/clients"

ClientsPath is where the build client page is mounted.

View Source
const DataPath = "/admin/data"

DataPath is where the data page is mounted.

View Source
const DevicesPath = "/admin/devices"

DevicesPath is where the devices page is mounted.

View Source
const DriversPath = "/admin/drivers"

DriversPath is where the roster is mounted.

View Source
const EnterpriseNote = "Part of Enterprise — it is not in the community edition."

EnterpriseNote is the one line beside a setting this build does not have. Such settings are shown and disabled rather than hidden: an operator should be able to see what exists and decide whether they want it, and a gap where a control will be is more confusing than a control that is plainly off.

View Source
const MaxCertificateBytes int64 = 256 << 10

MaxCertificateBytes is the largest PKCS#12 file this server will read. A certificate and a key and a chain is a few kilobytes; the cap is here so that a wrong file chosen by mistake is a sentence on the page rather than a server holding a gigabyte in memory.

View Source
const PairPath = "/pair"

PairPath is the page a driver is sent to.

It is the one address in this product that a person is told out loud. The client prints it on the machine being paired — "open this and enter 5DH-EHY" — and it is published in the discovery document as wire.Discovery.PairURI, so it has to exist and it has to be at exactly this path. It answered 404 for a while, which meant the first thing a new driver was asked to do failed.

View Source
const PluginsPath = "/admin/plugins"

PluginsPath is where the plugins page is mounted.

View Source
const Prefix = "/admin"

Prefix is where the panel is mounted.

View Source
const PruneConfirmation = "prune"

PruneConfirmation is the word an operator types to run a prune. It is the name of the thing about to happen rather than "yes", so that a phrase copied out of a support message cannot be pasted into the wrong box.

View Source
const PruneTimeout = 6 * time.Hour

PruneTimeout bounds one whole prune. A job that has been going for six hours is a job that is not going to finish, and one that stops on its own leaves the page able to start another rather than showing "running" for ever.

View Source
const SessionCookie = "pacenote_admin"

SessionCookie carries the admin session. The value is random; only its SHA-256 reaches the database, so a dump cannot be used to sign in.

View Source
const SessionTTL = 12 * time.Hour

SessionTTL is how long a session lasts without use. Each request that finds one extends it, so an operator working in the panel is not signed out underneath them and one who walked away is.

View Source
const SessionsRevokePath = "/admin/drivers/sessions/revoke"

SessionsRevokePath is where a session is ended.

View Source
const SettingsPath = "/admin/settings"

SettingsPath is where the settings page is mounted.

View Source
const StintsPath = "/admin/stints"

StintsPath is where one stint's laps are mounted. A stint hangs off a driver on the page, but it is addressed on its own: an operator pastes a stint identifier out of a log line more often than they walk to it from the roster.

Variables

This section is empty.

Functions

func Authenticate

func Authenticate(ctx context.Context, store *db.Store, email, password string) (db.Admin, bool, error)

Authenticate checks an email and password and returns the administrator.

A missing account and a wrong password are the same answer on purpose: the sign-in page must not tell a stranger which addresses have accounts. The hash of a known-bad password is verified even when there is no account, so the two paths take the same time.

func Certificate

func Certificate(store *db.Store, keyring *auth.Keyring) func(context.Context) (*clientbuild.Certificate, error)

Certificate is the builder's way in: the stored certificate opened with the data key in force right now, or nil on a server that has not been given one.

It is a function of the store and the keyring rather than a method so that wiring it into the builder does not mean handing the builder a whole panel.

func ClientAddress

func ClientAddress(typed string) (string, error)

ClientAddress is the address an operator may stamp into a client.

It is the client's own rule, applied here so that the mistake is caught on this page rather than by a driver whose app will not start: a scheme and a host, nothing else, and plain HTTP only for a server on the driver's own machine. A missing scheme is filled in rather than refused, because typing a host name is what an operator does.

Types

type Deps

type Deps struct {
	// Log receives the panel's own lines.
	Log *slog.Logger
	// Store is the database.
	Store *db.Store
	// Version is the build, shown in the footer and on the overview.
	Version string
	// StartedAt is when the process started, which is what uptime counts from.
	StartedAt time.Time
	// Settings reads the organisation's current settings. It is a function
	// rather than a value because settings change while the server runs.
	Settings func(context.Context) (config.Settings, error)
	// Now is the clock, injectable for tests.
	Now func() time.Time
	// OnLogin is called after every sign-in attempt with "accepted",
	// "rejected" or "rate_limited".
	OnLogin func(outcome string)
	// Keyring holds the data key that seals the operator's API keys. The
	// panel needs it to seal what an operator types, to show the last four
	// characters of what is stored, and to re-seal everything when the key is
	// regenerated. A nil keyring is a server with no data key, which the page
	// says rather than pretending a key could be stored safely.
	Keyring *auth.Keyring
	// SaveDataKey writes a newly minted data key into the configuration file
	// beside the binary. nil means this server cannot rewrite that file, and
	// the danger zone says so rather than offering a button that does nothing.
	SaveDataKey func(context.Context, auth.SecretKey) error
	// OnSettingsChanged is called after every saved change, so that the parts
	// of the server holding a copy of the settings re-read them. It is what
	// makes a change take effect without a restart. nil means nothing is told.
	OnSettingsChanged func()
	// Discovery renders the document as it would be served with the settings
	// given, for the preview on the settings page. nil means no preview.
	Discovery func(config.Settings) wire.Discovery
	// Clients is the prebuilt Windows client this server stamps copies of and
	// the directory the copies are kept in. The zero value is a server that
	// has not been given one, which the build page says plainly rather than
	// failing: a fresh installation has no client binary yet, and the page's
	// job then is to say what to put where.
	Clients clientbuild.Builder
	// PluginDir is where a plugin is dropped to install it by hand. It is
	// shown in the empty state, because "there are no plugins" without saying
	// where they go is a dead end.
	PluginDir string
	// Plugins is the plugin host. nil is a server running in a mode that has
	// none — setup, or a configuration that switched them off — and the page
	// says so rather than being absent, because a missing menu entry is a
	// thing an operator cannot ask a question about.
	Plugins Plugins
}

Deps is everything the panel needs from the rest of the server.

type Panel

type Panel struct {
	// contains filtered or unexported fields
}

Panel serves the admin pages.

func New

func New(d Deps) (*Panel, error)

New builds the panel.

func (*Panel) PruneProgress

func (p *Panel) PruneProgress() PruneProgress

PruneProgress reports what a retention prune is doing right now, or what the last one in this process did.

func (*Panel) Routes

func (p *Panel) Routes(mux *http.ServeMux)

Routes registers the panel on a mux. It does not own the mux, because the same server carries the API and the discovery document.

func (*Panel) SignedInAdmin

func (p *Panel) SignedInAdmin(r *http.Request) string

SignedInAdmin is the operator reading this request, or empty for nobody.

It exists for the one caller outside this package that has to know: the router that mounts plugins, which will not forward an operator-only page to somebody who is not one. The answer comes from the session table and never from a header, so nothing a browser sends can become an identity.

type Plugins

type Plugins interface {
	// All is every plugin this server knows about, running or not.
	All(ctx context.Context) ([]plugins.Status, error)
	// Status is one of them.
	Status(ctx context.Context, name string) (plugins.Status, error)
	// Discover re-reads the plugin directory. It is the rescan button.
	Discover(ctx context.Context) error
	// Enable, Disable and Restart are the three buttons on a row.
	Enable(ctx context.Context, name string) error
	Disable(ctx context.Context, name string) error
	Restart(ctx context.Context, name string) error
	// Remove uninstalls: it stops the process, drops the plugin's database and
	// forgets its row. It is the only one of these that destroys anything.
	Remove(ctx context.Context, name string) error
	// Spending is what this plugin may spend in a day and what it has spent.
	Spending(ctx context.Context, name string) (plugins.Spending, error)
	// SetDailyCap records what it may spend. Zero is no cap.
	SetDailyCap(ctx context.Context, name string, tokens int64) error
	// InterfaceVersion is the contract this host speaks, shown beside a plugin
	// that was built against another one.
	InterfaceVersion() int
}

Plugins is what the panel needs from the plugin host. It is an interface rather than the host itself so that this package does not depend on the host's internals, and so the pages can be tested without running a plugin process for every assertion about how a table renders.

type PruneProgress

type PruneProgress struct {
	// Running is whether a prune is going on right now.
	Running bool
	// StartedAt and FinishedAt bound the run. FinishedAt is zero while it is
	// still going.
	StartedAt, FinishedAt time.Time
	// Cutoff is the instant traces had to be older than.
	Cutoff time.Time
	// Actor is the administrator who started it.
	Actor string
	// Total is how many laps were going to lose their trace when it began, and
	// Done how many have. Total can be short of Done's eventual value if laps
	// aged past the cutoff while the job ran, which is why the page reads
	// "of about".
	Total, Done int64
	// Freed is how many bytes the preview said would be freed.
	Freed int64
	// Err is why it stopped early, or empty.
	Err string
}

PruneProgress is what a retention prune is doing, or what the last one did.

It is exported because it is the one piece of this panel that outlives a request: a caller shutting the server down, or a test that must not close the database underneath a job, needs to be able to ask.

func (PruneProgress) Percent

func (p PruneProgress) Percent() int

Percent is how far through the job is, between 0 and 100.

func (PruneProgress) Started

func (p PruneProgress) Started() bool

Started reports whether a prune has ever been run in this process.

type Sessions

type Sessions struct {
	// contains filtered or unexported fields
}

Sessions stores admin sessions in the database rather than in this process, so that signing out actually ends a session and a restart does not sign everyone out.

func NewSessions

func NewSessions(store *db.Store, now func() time.Time) *Sessions

NewSessions builds the session store.

func (*Sessions) CheckCSRF

func (s *Sessions) CheckCSRF(r *http.Request) bool

CheckCSRF compares the form's token against the cookie's, in constant time. A request with neither fails, so a missing cookie is not a way past it.

func (*Sessions) Current

func (s *Sessions) Current(ctx context.Context, r *http.Request) (db.AdminSession, bool)

Current returns the session this request carries, and extends it. A session that has expired is simply absent: expiry is enforced by the lookup, so there is no window in which an old cookie works.

func (*Sessions) End

End signs this browser out and removes the row, so the cookie is dead everywhere rather than only in this browser.

func (*Sessions) EnsureCSRF

func (s *Sessions) EnsureCSRF(w http.ResponseWriter, r *http.Request) string

EnsureCSRF returns this browser's CSRF token, minting one if it has none. The sign-in form needs it before there is a session, which is why it is not tied to one.

func (*Sessions) Start

func (s *Sessions) Start(ctx context.Context, w http.ResponseWriter, r *http.Request, adminID int64) (string, error)

Start creates a session for an administrator and sets both cookies.

Jump to

Keyboard shortcuts

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