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
- Variables
- func NameFromClaims(claims map[string]any, fullName string) (first, last string)
- func NormalizeEmail(email string) (string, error)
- func SanitizeName(name string) string
- func SplitFullName(full string) (first, last string)
- func ValidateName(name string) error
- type Directory
- type Filter
- type PostgresStore
- func (s *PostgresStore) Delete(ctx context.Context, email string) error
- func (s *PostgresStore) Get(ctx context.Context, email string) (*User, error)
- func (s *PostgresStore) Insert(ctx context.Context, u User) error
- func (s *PostgresStore) List(ctx context.Context, filter Filter) ([]User, int, error)
- func (s *PostgresStore) Observe(ctx context.Context, email, firstName, lastName string) error
- func (s *PostgresStore) Update(ctx context.Context, email string, u Update) error
- type Store
- type Update
- type User
Constants ¶
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.
const DefaultListLimit = 100
DefaultListLimit caps a List query that supplies no limit.
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.
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).
const MaxNameLen = 100
MaxNameLen caps a first or last name.
Variables ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
NewDirectory wraps store with the default throttle window.
func (*Directory) Observe ¶
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) 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 ¶
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.
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.