useradmin

package module
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Aug 26, 2026 License: AGPL-3.0 Imports: 12 Imported by: 0

README

useradmin

Tests Status Go Report Card PkgGoDev

License

This project is licensed under the GNU Affero General Public License v3.0 (AGPL-3.0). You can find a copy of the license at https://www.gnu.org/licenses/agpl-3.0.en.html

For commercial use, please use my contact page to obtain a commercial license.

Introduction

Admin interface for github.com/dracory/userstore. Provides a ready-to-use admin panel for managing users: list with AJAX, create, update, delete, and impersonate.

Modeled after github.com/dracory/blogadmin — same folder-per-controller pattern, same UiConfig/UiBase conventions.

Features

  • User management — list with AJAX pagination/sorting/filtering, create, update, delete
  • User impersonation — sign in as another user for support/debugging
  • Geo integration — country and timezone pickers via geostore
  • Vault tokenization — pluggable vault tokenizer for encrypted user fields (first name, last name, email, phone, business name)
  • Blind index search — pluggable blind index stores for filtered search on tokenized fields
  • Custom layouts — bring your own layout via FuncLayout
  • Bootstrap + Vue CDN — default UI works out of the box

Installation

go get github.com/dracory/useradmin

Quick Start

package main

import (
    "log/slog"
    "net/http"
    "os"

    "github.com/dracory/useradmin"

    "github.com/dracory/geostore"
    "github.com/dracory/sessionstore"
    "github.com/dracory/userstore"
)

func main() {
    userStore, _ := userstore.NewStore(userstore.NewStoreOptions{
        DB:                 yourDB,
        UserTableName:      "user",
        AutomigrateEnabled: true,
    })

    geoStore, _ := geostore.NewStore(geostore.NewStoreOptions{
        DB:                 yourDB,
        CountryTableName:   "geo_country",
        TimezoneTableName:  "geo_timezone",
        AutomigrateEnabled: true,
        AutoseedEnabled:    true,
    })

    sessionStore, _ := sessionstore.NewStore(sessionstore.NewStoreOptions{
        DB:                 yourDB,
        SessionTableName:   "session",
        AutomigrateEnabled: true,
    })

    admin, err := useradmin.New(useradmin.AdminOptions{
        UserStore:    userStore,
        GeoStore:     geoStore,
        Logger:       slog.New(slog.NewTextHandler(os.Stderr, nil)),
        SessionStore: sessionStore,
        AdminHomeURL: "/admin",
        UserAdminURL: "/admin/users",
    })
    if err != nil {
        log.Fatal(err)
    }

    http.Handle("/admin/users", http.HandlerFunc(admin.Handle))
    http.ListenAndServe(":8080", nil)
}

See example/ for a complete runnable server with in-memory SQLite and seed data.

Integration with a Router

useradmin.AdminInterface exposes Handle(w, r), which is an http.HandlerFunc-compatible method. Wire it into any router that accepts standard http.Handler:

// stdlib
mux.Handle("/admin/users", http.HandlerFunc(admin.Handle))

// github.com/dracory/rtr
route := rtr.NewRoute().
    SetName("Admin > Users").
    SetPath("/admin/users").
    SetHTMLHandler(admin.Handle)

Optional Dependencies

The following AdminOptions fields are optional. When nil/empty, the corresponding features degrade gracefully:

  • BlindIndexFirstName / BlindIndexLastName / BlindIndexEmail — when nil, filtered search by that field is disabled (the filter returns no matches instead of erroring).
  • TaskStore + BlindIndexRebuildTaskAlias — when nil/empty, the blind index rebuild enqueue on email change is skipped.
  • VaultTokenizer — when nil, user fields are treated as plain text (no tokenization/untokenization).
  • AuthUser — when nil, the create/delete/impersonate controllers treat the request as unauthenticated.
  • FlashRedirect — when nil, plain http.Redirect is used instead of a flash-message redirect.
  • FuncLayout — when nil, a default bare-bones HTML page is used (Bootstrap + Vue CDN).

Custom Layout

By default, useradmin renders a bare-bones HTML page with Bootstrap and Vue from CDN. To embed the admin inside your own layout (branding, menus, etc.), provide FuncLayout:

admin, _ := useradmin.New(useradmin.AdminOptions{
    UserStore:    userStore,
    GeoStore:     geoStore,
    Logger:       logger,
    SessionStore: sessionStore,
    FuncLayout: func(w http.ResponseWriter, r *http.Request, title, body string, opts struct {
        Styles     []string
        StyleURLs  []string
        Scripts    []string
        ScriptURLs []string
    }) string {
        return myLayout(w, r, title, body, opts)
    },
})

FuncLayout receives the request and response writer so the host project can access request context (auth user, locale, etc.) when rendering the layout.

Testing

go test ./...

Documentation

Overview

Package useradmin provides a standalone user admin interface following the folder-per-controller pattern. Each controller is in its own subfolder and handles its own views and AJAX data.

This module is modeled on github.com/dracory/blogadmin.

Index

Constants

View Source
const (
	SearchOpEquals      = shared.SearchOpEquals
	SearchOpContains    = shared.SearchOpContains
	SearchOpNotContains = shared.SearchOpNotContains
	SearchOpStartsWith  = shared.SearchOpStartsWith
)

Search operator constants.

View Source
const (
	SearchFieldFirstName    = shared.SearchFieldFirstName
	SearchFieldLastName     = shared.SearchFieldLastName
	SearchFieldEmail        = shared.SearchFieldEmail
	SearchFieldPhone        = shared.SearchFieldPhone
	SearchFieldBusinessName = shared.SearchFieldBusinessName
)

Search field constants.

View Source
const (
	SearchAnd = shared.SearchAnd
	SearchOr  = shared.SearchOr
)

Search combinator constants.

Variables

View Source
var (
	// ErrUserStoreRequired is returned when UserStore is not provided
	ErrUserStoreRequired = errors.New("user store is required")

	// ErrLoggerRequired is returned when Logger is not provided
	ErrLoggerRequired = errors.New("logger is required")

	// ErrGeoResolverRequired is returned when GeoResolver is not
	// provided. The user update controller needs it to list countries
	// and timezones.
	ErrGeoResolverRequired = errors.New("geo resolver is required for the user update controller")
)

Common errors

Functions

This section is empty.

Types

type AdminInterface

type AdminInterface interface {
	Handle(w http.ResponseWriter, r *http.Request)
}

AdminInterface defines the interface for the user admin

func New

func New(opts AdminOptions) (AdminInterface, error)

New creates a new user admin instance. Returns ErrUserStoreRequired if UserStore is nil, ErrLoggerRequired if Logger is nil, ErrGeoResolverRequired if GeoResolver is nil.

OnUserImpersonate is optional — when nil, the impersonate button is hidden and the impersonate route returns 404.

This makes misconfiguration fail fast at construction instead of surfacing as runtime errors inside individual controllers.

type AdminOptions

type AdminOptions struct {
	// UserStore is required
	UserStore userstore.StoreInterface

	// GeoResolver is required for the user update controller (country
	// and timezone lists).
	GeoResolver shared.GeoResolverInterface

	// Logger is required
	Logger *slog.Logger

	// OnUserImpersonate is optional — when nil, the impersonate
	// button is hidden and the impersonate route is not registered.
	// The host owns the auth mechanism (session+cookie, JWT, etc.).
	OnUserImpersonate shared.OnUserImpersonateFunc

	// OnUserSearch is an optional callback for custom user search
	// (e.g. blind index, Elasticsearch). When nil, useradmin falls
	// back to userstore query-based search.
	OnUserSearch shared.OnUserSearchFunc

	// OnUserUpdated is an optional callback invoked after a user is
	// updated. The host can use it to trigger side effects (blind
	// index rebuild, audit log, notifications, etc.). When nil, the
	// callback is skipped.
	OnUserUpdated shared.OnUserUpdatedFunc

	// UserPiiSeal transforms a user from display representation to
	// storage representation (e.g. tokenize, encrypt PII). Optional —
	// when nil, the user is stored as-is (plain text).
	UserPiiSeal shared.UserPiiSealFunc

	// UserPiiUnseal transforms a user from storage representation to
	// display representation (e.g. detokenize, decrypt PII). Optional —
	// when nil, the user is used as-is (plain text).
	UserPiiUnseal shared.UserPiiUnsealFunc

	// UsersPiiUnseal is the batch version of UserPiiUnseal. It allows
	// the host to unseal all users in a single call for efficiency.
	// Optional — when nil, useradmin falls back to UserPiiUnseal per
	// user (or plain text when that is also nil).
	UsersPiiUnseal shared.UsersPiiUnsealFunc

	// FlashRedirect redirects with a flash message. Optional — when
	// nil, plain http.Redirect is used.
	FlashRedirect shared.FlashRedirectFunc

	// FuncLayout is an optional function to render the admin interface
	// inside your own layout (branding, menus, etc.). It receives the
	// request and response writer so the host project can access
	// request context (auth user, locale, etc.) when rendering the
	// layout.
	FuncLayout func(w http.ResponseWriter, r *http.Request, title string, body string, options struct {
		Styles     []string
		StyleURLs  []string
		Scripts    []string
		ScriptURLs []string
	}) string

	// AdminHomeURL is the URL for the admin home page (default: "/admin")
	AdminHomeURL string

	// UserAdminURL is the base URL for the user admin (default: "/admin/users")
	UserAdminURL string

	// UserHomeURL is the URL the impersonate controller redirects to
	// after a successful impersonation (default: "/"). This is where
	// the impersonated user lands.
	UserHomeURL string
}

AdminOptions contains all dependencies and configuration for the user admin.

UserStore, GeoResolver, and Logger are required. SessionStore is required for the impersonate controller. Blind index stores, UserPiiSeal/UserPiiUnseal/UsersPiiUnseal are optional — when nil, corresponding features degrade gracefully (filtered search disabled, email-change rebuild skipped, user fields treated as plain text).

FuncLayout is an optional function to render the admin interface inside your own layout (branding, menus, etc.). If nil, a default bare-bones HTML page is used (Bootstrap + Vue CDN). Uses an anonymous struct to match blogadmin/shopadmin exactly, so consumers can reuse their existing layout function for useradmin.

type Country added in v0.2.0

type Country = shared.Country

Re-exports of shared types so consumers can import everything from the top-level useradmin package without reaching into useradmin/shared. Follows the blogadmin/shopadmin convention (e.g. shopadmin.CustomerResolverInterface).

type FlashRedirectFunc added in v0.2.0

type FlashRedirectFunc = shared.FlashRedirectFunc

Re-exports of shared types so consumers can import everything from the top-level useradmin package without reaching into useradmin/shared. Follows the blogadmin/shopadmin convention (e.g. shopadmin.CustomerResolverInterface).

type GeoResolverInterface added in v0.2.0

type GeoResolverInterface = shared.GeoResolverInterface

Re-exports of shared types so consumers can import everything from the top-level useradmin package without reaching into useradmin/shared. Follows the blogadmin/shopadmin convention (e.g. shopadmin.CustomerResolverInterface).

type OnUserImpersonateFunc added in v0.2.0

type OnUserImpersonateFunc = shared.OnUserImpersonateFunc

Re-exports of shared types so consumers can import everything from the top-level useradmin package without reaching into useradmin/shared. Follows the blogadmin/shopadmin convention (e.g. shopadmin.CustomerResolverInterface).

type OnUserSearchFunc added in v0.2.0

type OnUserSearchFunc = shared.OnUserSearchFunc

Re-exports of shared types so consumers can import everything from the top-level useradmin package without reaching into useradmin/shared. Follows the blogadmin/shopadmin convention (e.g. shopadmin.CustomerResolverInterface).

type OnUserUpdatedFunc added in v0.2.0

type OnUserUpdatedFunc = shared.OnUserUpdatedFunc

Re-exports of shared types so consumers can import everything from the top-level useradmin package without reaching into useradmin/shared. Follows the blogadmin/shopadmin convention (e.g. shopadmin.CustomerResolverInterface).

type SearchCombine added in v0.2.0

type SearchCombine = shared.SearchCombine

Re-exports of shared types so consumers can import everything from the top-level useradmin package without reaching into useradmin/shared. Follows the blogadmin/shopadmin convention (e.g. shopadmin.CustomerResolverInterface).

type SearchCondition added in v0.2.0

type SearchCondition = shared.SearchCondition

Re-exports of shared types so consumers can import everything from the top-level useradmin package without reaching into useradmin/shared. Follows the blogadmin/shopadmin convention (e.g. shopadmin.CustomerResolverInterface).

type SearchField added in v0.2.0

type SearchField = shared.SearchField

Re-exports of shared types so consumers can import everything from the top-level useradmin package without reaching into useradmin/shared. Follows the blogadmin/shopadmin convention (e.g. shopadmin.CustomerResolverInterface).

type SearchOp added in v0.2.0

type SearchOp = shared.SearchOp

Re-exports of shared types so consumers can import everything from the top-level useradmin package without reaching into useradmin/shared. Follows the blogadmin/shopadmin convention (e.g. shopadmin.CustomerResolverInterface).

type Timezone added in v0.2.0

type Timezone = shared.Timezone

Re-exports of shared types so consumers can import everything from the top-level useradmin package without reaching into useradmin/shared. Follows the blogadmin/shopadmin convention (e.g. shopadmin.CustomerResolverInterface).

type UserPiiSealFunc added in v0.2.0

type UserPiiSealFunc = shared.UserPiiSealFunc

Re-exports of shared types so consumers can import everything from the top-level useradmin package without reaching into useradmin/shared. Follows the blogadmin/shopadmin convention (e.g. shopadmin.CustomerResolverInterface).

type UserPiiUnsealFunc added in v0.2.0

type UserPiiUnsealFunc = shared.UserPiiUnsealFunc

Re-exports of shared types so consumers can import everything from the top-level useradmin package without reaching into useradmin/shared. Follows the blogadmin/shopadmin convention (e.g. shopadmin.CustomerResolverInterface).

type UsersPiiUnsealFunc added in v0.2.0

type UsersPiiUnsealFunc = shared.UsersPiiUnsealFunc

Re-exports of shared types so consumers can import everything from the top-level useradmin package without reaching into useradmin/shared. Follows the blogadmin/shopadmin convention (e.g. shopadmin.CustomerResolverInterface).

Directories

Path Synopsis
Example useradmin server.
Example useradmin server.

Jump to

Keyboard shortcuts

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