backoffice

package
v0.3.6 Latest Latest
Warning

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

Go to latest
Published: Oct 27, 2026 License: MIT Imports: 16 Imported by: 0

Documentation

Overview

Package backoffice is the back office the apps share: who may use an app (roles, access states), the session check that enforces it on every request, the admin routes over the accounts, View as, the announcement shown to everyone, the audit log and the account stats. schema.sql lists the columns and the table it needs beside lib's auth tables and audit_events.

An app keeps its sign-in flows: they call Admit and AuditSignIn in the transaction that creates the session, and every handler that needs a signed-in user calls Check (or RequireAdmin). Mount serves the admin routes under /api/v1/admin/; the UI kit (lib's web/admin) draws them.

Index

Constants

View Source
const (
	AccessActive     = "active"
	AccessRestrained = "restrained"
	AccessPending    = "pending"
)

Access states. The database keeps a pending account restrained with pending_since set: AccessPending only exists in the API.

View Source
const (
	JoinedAdminEmail = "admin_email"
	JoinedFirstUser  = "first_user"
	JoinedOpen       = "open"
	JoinedUnknown    = "unknown_email"
)

Why a sign-in created an account, as the signed_up audit row and log line say it.

View Source
const RoleAdmin = "admin"

RoleAdmin is the role of the accounts that use the back office; every app has it.

Variables

View Source
var (
	// ErrRestrained refuses the sign-in of an account an admin restrained.
	ErrRestrained = errors.New("user_restrained")
)

Functions

func IsPending

func IsPending(accessState, pendingSince string) bool

IsPending reports whether an account signed up and waits for an admin, as opposed to one an admin restrained.

Types

type AdminUser

type AdminUser struct {
	User
	LastSeenAt         string `json:"last_seen_at"`
	ActiveSessionCount int64  `json:"active_session_count"`
	Connected          bool   `json:"connected"`
}

AdminUser is an account in the admin list. Connected means it has a live session of its own (not a View as).

type Announcement

type Announcement struct {
	Text      string `json:"text"`
	Tone      string `json:"tone"`
	UpdatedAt string `json:"updated_at"`
}

Announcement is the banner an admin shows to every active account.

type Backoffice

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

Backoffice serves the back office of one app.

func New

func New(cfg Config) (*Backoffice, error)

New checks the Config.

func (*Backoffice) AccountStats

func (b *Backoffice) AccountStats(ctx context.Context, now time.Time) (Stats, error)

AccountStats counts the accounts at now; last seen leaves out the sessions of a View as.

func (*Backoffice) ActiveAnnouncement

func (b *Backoffice) ActiveAnnouncement(ctx context.Context) (*Announcement, error)

ActiveAnnouncement returns the announcement shown now, or nil when there is none.

func (*Backoffice) Admit

func (b *Backoffice) Admit(ctx context.Context, tx *sql.Tx, email string, now time.Time) (User, string, error)

Admit returns the account of a sign-in that proved email, in the transaction that creates its session:

  • a new account is created: an admin when its email is a super admin's or the database has no account yet (or nobody could ever approve anyone), else the member role, active unless Config.Approval keeps it pending;
  • a super admin is made an admin again at every sign-in;
  • an account an admin restrained gets ErrRestrained.

joined says why the account was created, "" for an existing one. email must be normalized.

func (*Backoffice) AuditSignIn

func (b *Backoffice) AuditSignIn(ctx context.Context, tx *sql.Tx, u User, joined, via, ip string) error

AuditSignIn writes signed_up first when the sign-in created the account, then signed_in, in the sign-in transaction. via names the sign-in method.

func (*Backoffice) Check

func (b *Backoffice) Check(w http.ResponseWriter, r *http.Request) (Session, bool)

Check returns the session of an active account, or answers 401 (no valid session), 403 access_pending (an account waiting for an admin, who keeps the cookie) or 403 read_only_impersonation (a write while viewing the app as someone else) and returns false.

func (*Backoffice) CheckAllowPending

func (b *Backoffice) CheckAllowPending(w http.ResponseWriter, r *http.Request) (Session, bool)

CheckAllowPending is Check that also lets an account waiting for an admin through: for /me and sign out.

func (*Backoffice) EndSession

func (b *Backoffice) EndSession(w http.ResponseWriter, r *http.Request) (found bool, err error)

EndSession signs the request out and clears its cookie: a View as goes back to the admin's own session, any other session is revoked with a signed_out audit row. found is false when the request had no valid session.

func (*Backoffice) IsSuperAdmin

func (b *Backoffice) IsSuperAdmin(role, email string) bool

IsSuperAdmin reports whether an account is an admin whose email is in Config.SuperAdmins.

func (*Backoffice) ListUsers

func (b *Backoffice) ListUsers(ctx context.Context, connectedOnly bool) ([]AdminUser, error)

ListUsers returns every account, the pending ones first, then the newest; connectedOnly keeps the connected ones.

func (*Backoffice) MeOf

func (b *Backoffice) MeOf(ctx context.Context, sess Session) (Me, error)

MeOf returns the access part of /me for a session from CheckAllowPending.

func (*Backoffice) Mount

func (b *Backoffice) Mount(mux *http.ServeMux)

Mount serves the admin routes; every one needs an admin session.

func (*Backoffice) RequireAdmin

func (b *Backoffice) RequireAdmin(w http.ResponseWriter, r *http.Request) (Session, bool)

RequireAdmin is Check for the admins only: anyone else gets 403 forbidden.

type Config

type Config struct {
	DB *sql.DB
	// Roles an admin can give, RoleAdmin included; MemberRole is the role of a new account that is not an admin.
	Roles      []string
	MemberRole string
	// SuperAdmins are the normalized emails of the app's admin flag: always admins, protected from the other
	// admins, the only ones who can view the app as someone else.
	SuperAdmins map[string]struct{}
	// Approval makes a new account wait for an admin unless Admit lets it in; without it every account is active.
	Approval bool
	// Admit, with Approval, lets a new account in at once and says why ("" keeps it pending); it runs in the
	// sign-in transaction.
	Admit func(ctx context.Context, tx *sql.Tx, email string) (string, error)
	// Actions are the app's own audit codes, listed after audit.AccountActions in the audit log filter.
	Actions []string
	// Sessions is the session lifetime policy the check refreshes sessions with.
	Sessions sessions.SessionPolicy
}

Config is what an app decides about its accounts.

type LoggedEvent

type LoggedEvent struct {
	audit.Event
	Actor       string `json:"actor"`
	TargetLabel string `json:"target_label"`
}

LoggedEvent is an audit event with its actor and target named for a person: emails for accounts.

type Me

type Me struct {
	Role           string        `json:"role"`
	AccessState    string        `json:"access_state"`
	ImpersonatedBy string        `json:"impersonated_by,omitempty"`
	CanImpersonate bool          `json:"can_impersonate"`
	Announcement   *Announcement `json:"announcement,omitempty"`
}

Me is what /me adds about access: the API access state, who views the app as this user, whether the user may View as, and the announcement to show (only to an active account).

type Session

type Session struct {
	ID           string
	UserID       string
	Email        string
	Role         string
	AccessState  string // as stored: active or restrained
	PendingSince string
	// ImpersonatorID is the super admin viewing the app as this user, "" for a normal session.
	ImpersonatorID string
	CreatedAt      string
	ExpiresAt      string
	LastSeenAt     string
}

Session is the session of a request, with its account.

func (Session) Pending

func (s Session) Pending() bool

Pending reports whether the account waits for an admin.

type Stats

type Stats struct {
	Total      int            `json:"total"`
	ByRole     map[string]int `json:"by_role"`
	Active24h  int            `json:"active_24h"`
	Active7d   int            `json:"active_7d"`
	NeverBack  int            `json:"never_seen_since_sign_up"`
	New7d      int            `json:"new_7d"`
	Pending    int            `json:"pending"`
	Restrained int            `json:"restrained"`
}

Stats counts the accounts: by role and access, new and active over the last day and week.

type User

type User struct {
	ID           string `json:"id"`
	Email        string `json:"email"`
	Role         string `json:"role"`
	AccessState  string `json:"access_state"` // the API value: active, restrained or pending
	PendingSince string `json:"-"`
	CreatedAt    string `json:"created_at"`
}

User is an account as the back office sees it.

Jump to

Keyboard shortcuts

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