Skip to content

Mount and test operator panels

Use these tasks to mount the embedded console behind your application’s authentication. Install Echo Pro and construct the store or limiter each panel needs. Run console examples with:

Terminal window
go test -run Example ./admin/...

The admin overview explains sessions, access checks and audit recording. The reference lists shell and panel configuration fields.

Construct quotaadmin.New with the existing limiter. Map a validated session to a Viewer principal and a tenant allowlist. The example uses a random fixture session and shows final route verification:

package admin_test
import (
"crypto/rand"
"fmt"
"github.com/labstack/echo/v5"
"echo.labstack.com/pro/admin"
"echo.labstack.com/pro/quota"
"echo.labstack.com/pro/quota/quotaadmin"
)
func ExampleConfig_Mount() {
l, err := quota.New(quota.Config{Store: quota.NewMemoryStore(
quota.MemoryConfig{},
), Plans: []quota.Plan{{ID: "team", Limits: []quota.Limit{
quota.PerDay("calls", 100),
}}}})
if err != nil {
fmt.Println(err)
return
}
p, err := quotaadmin.New(quotaadmin.Config{Limiter: l})
if err != nil {
fmt.Println(err)
return
}
// A fixture login binding; map the actual validated session in production.
session := rand.Text()
e := echo.New()
cfg := admin.Config{Panels: []admin.Panel{p}, Instance: "node-1", Authenticate: func(
*echo.Context,
) (admin.Principal, error) {
return admin.Principal{
ID: "alice",
Session: session,
Tenants: []string{"tenant-a"},
}, nil
}}
if err = cfg.Mount(e, "/admin"); err != nil {
fmt.Println(err)
return
}
// Register remaining app routes before the final reservation check.
fmt.Println(admin.VerifyRoutes(e, "/admin"))
fmt.Println(admin.RouteKeys("/admin"))
// Output:
// <nil>
// [echo_route_any /admin echo_route_any /admin/*]
}

Replace the fixture Authenticate callback with your login check or an authbind mapping. Populate Session from that login’s session ID. A fixed application-wide session cannot represent separate browser logins. Header-only credentials use HeaderCredential as in the complete read-only example.

Register the remaining application routes, then call VerifyRoutes before serving. Keep the console prefix reserved. Set PublicPath if a reverse proxy serves the console under another path. Set Origins to your exact deployment origins.

Make outer middleware pass console requests through unchanged. The console checks origin and CSRF itself. The local program skips outer CORS middleware for console routes. It mounts the panel and closes the limiter at shutdown:

// Command admin demonstrates local-only, read-only customer authentication.
package main
import (
"context"
"crypto/rand"
"errors"
"fmt"
"net/http"
"os"
"os/signal"
"strings"
"time"
"github.com/labstack/echo/v5"
"github.com/labstack/echo/v5/middleware"
"echo.labstack.com/pro/admin"
"echo.labstack.com/pro/quota"
"echo.labstack.com/pro/quota/quotaadmin"
)
func runWithContext(ctx context.Context) error {
l, err := quota.New(quota.Config{Store: quota.NewMemoryStore(
quota.MemoryConfig{},
), Plans: []quota.Plan{{ID: "team", Limits: []quota.Limit{
quota.PerDay("calls", 100),
quota.Credits("credits").Meter("tokens"),
}}}})
if err != nil {
return err
}
defer func() {
closeCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), time.Second)
defer cancel()
if closeErr := l.Close(closeCtx); closeErr != nil {
fmt.Fprintln(os.Stderr, "limiter close failed")
}
}()
p, err := quotaadmin.New(quotaadmin.Config{Limiter: l})
if err != nil {
return err
}
// One process-local fixture login; use the validated login's session binding
// for each request in a deployed application, never a shared fixed value.
session := rand.Text()
e := echo.New()
prefix := "/admin"
// The outer CORS middleware must let every console method enter its wrapper.
cors, err := (middleware.CORSConfig{AllowOrigins: []string{
"http://127.0.0.1:8080",
}, Skipper: func(c *echo.Context) bool {
path := c.Request().URL.Path
return path == prefix || strings.HasPrefix(path, prefix+"/")
}}).ToMiddleware()
if err != nil {
return err
}
e.Use(cors)
cfg := admin.Config{Panels: []admin.Panel{
p,
}, Instance: "local-demo", Theme: admin.Theme{
Environment: "Local demonstration",
}, Authenticate: func(*echo.Context) (admin.Principal, error) {
// Replace with validated customer session/bearer claims in a deployed app.
// Fixed auth is confined to this loopback-only demonstration.
return admin.Principal{
ID: "local-reader",
Session: session,
Tenants: []string{"demo"},
}, nil
}}
if err := cfg.Mount(e, prefix); err != nil {
return err
}
if err := admin.VerifyRoutes(e, prefix); err != nil {
return err
}
server := &http.Server{
Addr: "127.0.0.1:8080",
Handler: e,
ReadHeaderTimeout: 5 * time.Second,
ReadTimeout: 10 * time.Second,
WriteTimeout: 15 * time.Second,
MaxHeaderBytes: 16 << 10,
}
done := make(chan error, 1)
go func() { done <- server.ListenAndServe() }()
select {
case err := <-done:
if !errors.Is(err, http.ErrServerClosed) {
return err
}
return nil
case <-ctx.Done():
shutdown, cancel := context.WithTimeout(context.WithoutCancel(ctx), 5*time.Second)
defer cancel()
err := server.Shutdown(shutdown)
if serveErr := <-done; !errors.Is(serveErr, http.ErrServerClosed) {
return errors.Join(err, serveErr)
}
return err
}
}
func main() {
if err := run(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
func run() error {
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
defer cancel()
if err := runWithContext(ctx); err != nil {
return err
}
return nil
}

Run it through the local admin tutorial. Keep its fixed local reader on a loopback listener. A deployed application needs validated authentication, per-session binding and a reviewed origin list.

Pass a journal operator to executionadmin. Pass an authorized audit.Reader to auditadmin.NewPanel. A webhook panel needs *webhookpg.Store and global scope. The memory webhook store cannot back that panel.

The full console tutorial shows the complete wiring and the PostgreSQL prerequisites. Close pools only after workers and in-flight console reads stop. Durable audit needs its separate PostgreSQL roles. Without them, the demo falls back to memory audit.

Enable the specific panel action, then grant the required Operator role and scope. Configure audit and signing keys before mounting write actions. Keep previews, typed confirmations, acknowledgements and required reasons. Preserve action IDs and business inputs across retries.

Enable credit actions with EnableCredits and destructive partition resets with EnableReset. quotaadmin.Config has no per-action role, reason or confirmation settings. The shell requires Operator and access to the resolved scope. All quota mutations require a reason; corrections and resets also require typed confirmation and acknowledgements fixed by the adapter. admin.Config.Authorize can further restrict actions by principal, action, tenant, target or canonical input; it cannot grant access beyond the built-in checks. A reset clears every subject in the partition. Its preview records the expected reset generations; a later reset makes those generations stale. Execution resolution and uncertain webhook decisions require verified provider records. An HTTP timeout does not prove that a provider did nothing. Use the same action ID when you retry an action with an unknown result.

This executable credit fixture calls the actual preview and commit protocol. It explicitly opts out of audit for disposable local state. Replace that opt-out with a durable audit sink in a deployment. It prints the HTTP status, new balance, whether the credit applied, and audit outcome: 200 10 true applied:

func ExamplePanel_creditWrites() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
lim, err := quota.New(quota.Config{Store: quota.NewMemoryStore(
quota.MemoryConfig{},
), Plans: []quota.Plan{{ID: "team", Limits: []quota.Limit{
quota.Credits("credits"),
}}}})
if err != nil {
fmt.Println(err)
return
}
defer func() {
if err := lim.Close(context.WithoutCancel(ctx)); err != nil {
fmt.Println(err)
}
}()
panel, err := quotaadmin.New(quotaadmin.Config{Limiter: lim, EnableCredits: true})
if err != nil {
fmt.Println(err)
return
}
key := make([]byte, 32)
if _, err = rand.Read(key); err != nil {
fmt.Println(err)
return
}
e := echo.New()
// Only this disposable example opts out; production supplies auditadmin.New(store).
err = (admin.Config{Panels: []admin.Panel{panel}, Instance: "example", Keys: [][]byte{
key,
}, AllowUnauditedWrites: true, Authenticate: func(
*echo.Context,
) (admin.Principal, error) {
return admin.Principal{
ID: "operator",
Role: admin.Operator,
Tenants: []string{"acme"},
HeaderCredential: true,
}, nil
}}).Mount(e, "/admin")
if err != nil {
fmt.Println(err)
return
}
if err = admin.VerifyRoutes(e, "/admin"); err != nil {
fmt.Println(err)
return
}
fields := map[string]string{
"mode": "preview",
"action_id": "00000000000000000000000000000001",
"tenant": "acme",
"plan": "team",
"limit": "credits",
"amount": "10",
"invoice_id": "ticket-57",
}
post := func() *httptest.ResponseRecorder {
body, err := json.Marshal(fields)
if err != nil {
panic(err)
}
request := httptest.NewRequestWithContext(
ctx,
"POST",
"http://console.example/admin/api/v1/quota/credit-topup",
bytes.NewReader(body),
)
request.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
e.ServeHTTP(w, request)
return w
}
preview := post()
var confirmation struct {
Token string `json:"confirmation"`
}
if err = json.Unmarshal(preview.Body.Bytes(), &confirmation); err != nil {
fmt.Println(err)
return
}
fields["mode"] = "commit"
fields["confirmation"] = confirmation.Token
fields["reason"] = "Invoice payment"
committed := post()
var answer struct {
Result quota.CreditResult `json:"result"`
Audit struct {
Outcome string `json:"outcome"`
} `json:"audit"`
}
if err = json.Unmarshal(committed.Body.Bytes(), &answer); err != nil {
fmt.Println(err)
return
}
fmt.Println(
committed.Code,
answer.Result.Balance,
answer.Result.Applied,
answer.Audit.Outcome,
)
// Output: 200 10 true applied
}

Build a tenant-scoped test principal without global or reveal permission:

func ExampleTenantPrincipal() {
p := admintest.TenantPrincipal("acme", admin.Viewer, false)
fmt.Println(p.Tenants, p.Global, p.Reveal)
// Output: [acme] false false
}

Exercise both permitted and denied views. Test revoked access on continuation and receipt replay. Check that the console checks access before it reports a malformed target. For mutations, assert that preview performs no effect and a failed audit intent performs no mutation.

Handle admin unauthenticated by obtaining a valid login. Handle CSRF invalid by refreshing controls in the current session. On admin timeout, narrow an expensive read; after a mutation, reread state before choosing another action. The problem catalog distinguishes these cases.