user

package
v1.85.0 Latest Latest
Warning

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

Go to latest
Published: Jun 13, 2026 License: Apache-2.0 Imports: 11 Imported by: 0

Documentation

Overview

Package user provides a directory of known people keyed by email (#614).

It is NOT an authorization layer. A row simply records that a person exists so share pickers can resolve a name from an email address. Rows are upserted when a person authenticates (token claims fill the name) and can be pre-added by an admin before the person has ever logged in. Admin-entered names take precedence: a login only fills blank name fields.

Index

Constants

View Source
const (
	// SourceAuth marks a row upserted from a real authenticated session.
	SourceAuth = "auth"
	// SourceAdmin marks a row pre-added by an admin via the API.
	SourceAdmin = "admin"
)

Source records how a directory row first came to exist.

View Source
const DefaultListLimit = 100

DefaultListLimit caps a List query that supplies no limit.

View Source
const DefaultObserveTTL = 5 * time.Minute

DefaultObserveTTL is the minimum interval between directory writes for the same email. Authentication runs on every tool call, so without throttling we would upsert the same row constantly.

View Source
const MaxListLimit = 100

MaxListLimit is the hard upper bound on a List page size. It bounds the response size for the directory endpoints (the portal picker is readable by any authenticated user, so an unbounded limit would let one request dump the whole directory).

View Source
const MaxNameLen = 100

MaxNameLen caps a first or last name.

Variables

View Source
var (
	// ErrNotFound is returned when a directory user does not exist.
	ErrNotFound = errors.New("user not found")
	// ErrAlreadyExists is returned by Insert when the email is already present.
	ErrAlreadyExists = errors.New("user already exists")
)

Sentinel errors returned by Store implementations.

Functions

func NameFromClaims

func NameFromClaims(claims map[string]any, fullName string) (first, last string)

NameFromClaims derives a first and last name from OIDC claims. It prefers the standard given_name/family_name pair and falls back to splitting a full name (the provided fullName, or the "name" claim when fullName is empty). This is the single source of truth for name derivation across the token auth path and the browser-session login path.

func NormalizeEmail

func NormalizeEmail(email string) (string, error)

NormalizeEmail trims, lowercases, and validates an email address, returning the bare normalized address. Lowercasing matches the convention used by portal_shares.shared_with_email so directory lookups and share matching agree on identity.

func SanitizeName

func SanitizeName(name string) string

SanitizeName trims, strips control characters, and bounds the length of a name taken from an untrusted source (token claims on the auth path). The admin API rejects oversized/invalid names outright; the auth upsert instead sanitizes, since blocking is not an option there — a malformed claim must not let a hostile or misconfigured IdP inject control characters or a multi-kilobyte string into the shared directory.

func SplitFullName

func SplitFullName(full string) (first, last string)

SplitFullName splits a display name into first and last components on the first whitespace run. A single token becomes the first name with an empty last name; empty input yields two empty strings.

func ValidateName

func ValidateName(name string) error

ValidateName checks a first or last name length. An empty name is allowed: names are optional, since a person may be known only by email until they log in and their claims fill it in.

Types

type Directory

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

Directory wraps a Store with in-memory throttling and asynchronous, best-effort writes. Recording who has authenticated must never block or fail the authentication path, so Observe returns immediately and swallows errors.

func NewDirectory

func NewDirectory(store Store) *Directory

NewDirectory wraps store with the default throttle window.

func (*Directory) Observe

func (d *Directory) Observe(email, firstName, lastName string)

Observe records a person seen via authentication. It throttles repeat writes for the same email within the TTL and performs the store write on a background goroutine. Errors are logged, never returned: directory upkeep must not affect auth.

The throttle is checked against a cheaply-normalized key BEFORE the full RFC 5322 parse, so the ~99% of calls that are throttled never pay for parsing. Names are sanitized (control characters stripped, length bounded) because they come from untrusted token claims.

type Filter

type Filter struct {
	// Query optionally matches (case-insensitive substring) against email,
	// first name, or last name.
	Query  string
	Limit  int
	Offset int
}

Filter specifies criteria for listing directory users.

type PostgresStore

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

PostgresStore implements Store backed by PostgreSQL.

func NewPostgresStore

func NewPostgresStore(db *sql.DB) *PostgresStore

NewPostgresStore creates a PostgreSQL-backed user directory store.

func (*PostgresStore) Delete

func (s *PostgresStore) Delete(ctx context.Context, email string) error

Delete removes a user by email.

func (*PostgresStore) Get

func (s *PostgresStore) Get(ctx context.Context, email string) (*User, error)

Get returns a single directory user by email.

func (*PostgresStore) Insert

func (s *PostgresStore) Insert(ctx context.Context, u User) error

Insert adds a new directory row. The source defaults to 'admin' when unset, matching the pre-add use case.

func (*PostgresStore) List

func (s *PostgresStore) List(ctx context.Context, filter Filter) ([]User, int, error)

List returns directory users matching the filter, ordered by last name then first name then email, plus the total count of matches (before the limit/offset window).

func (*PostgresStore) Observe

func (s *PostgresStore) Observe(ctx context.Context, email, firstName, lastName string) error

Observe upserts a person seen via authentication. On conflict it fills only the blank name fields so an admin-entered name is never overwritten, and it always marks the row confirmed and bumps last_seen_at.

func (*PostgresStore) Update

func (s *PostgresStore) Update(ctx context.Context, email string, u Update) error

Update applies the non-nil fields of u to the named user.

type Store

type Store interface {
	// Observe records a person seen via a real authenticated session. It
	// inserts a new confirmed row or, on conflict, fills ONLY blank name
	// fields (admin-entered names win) and stamps last_seen_at + confirmed.
	// firstName/lastName may be empty.
	Observe(ctx context.Context, email, firstName, lastName string) error
	// Insert adds a directory row, returning ErrAlreadyExists if the email is
	// already present. Used by the admin pre-add path.
	Insert(ctx context.Context, u User) error
	// Get returns a single user by email, or ErrNotFound.
	Get(ctx context.Context, email string) (*User, error)
	// List returns directory users matching the filter plus the total count.
	List(ctx context.Context, filter Filter) ([]User, int, error)
	// Update applies the non-nil fields of u, returning ErrNotFound if absent.
	Update(ctx context.Context, email string, u Update) error
	// Delete removes a user by email, returning ErrNotFound if absent.
	Delete(ctx context.Context, email string) error
}

Store persists and queries the known-users directory.

type Update

type Update struct {
	FirstName *string `json:"first_name,omitempty"`
	LastName  *string `json:"last_name,omitempty"`
}

Update holds mutable fields for an admin edit. A nil pointer leaves the field unchanged.

type User

type User struct {
	Email      string     `json:"email" example:"marcus.johnson@example.com"`
	FirstName  string     `json:"first_name" example:"Marcus"`
	LastName   string     `json:"last_name" example:"Johnson"`
	Source     string     `json:"source" example:"auth"`
	Confirmed  bool       `json:"confirmed" example:"true"`
	AddedBy    string     `json:"added_by,omitempty" example:"admin@example.com"`
	LastSeenAt *time.Time `json:"last_seen_at,omitempty"`
	CreatedAt  time.Time  `json:"created_at"`
	UpdatedAt  time.Time  `json:"updated_at"`
}

User is a directory entry for a known person.

Jump to

Keyboard shortcuts

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