Documentation
¶
Overview ¶
Package username adds handle-based identity to the auth core.
Like the phone plugin it stores nothing of its own: a username is an identifier and lives in the core's shared auth_identity table. What this package adds is the policy a handle needs and an email address does not — reserved names, and the rule about what happens to a handle after a rename.
Index ¶
- Constants
- Variables
- func ByHandleQuery(a *auth.Auth, rawHandle string, dst *auth.User) (sqlb.Query, error)
- func Normalize(raw string) (string, error)
- func Of(a *auth.Auth, userID string, dst *Data) (sqlb.Query, error)
- func UserBindings(dst *Data) []sqlb.Binding
- func UserJoin() sqlb.Join
- func Validate(v string) error
- func ValidateChars(v string) error
- type Availability
- type Data
- type Option
- func DisplayNormalizer(fn func(display string) string) Option
- func DisplayValidator(fn func(display string) error) Option
- func Immutable() Option
- func Length(min, max int) Option
- func Login() Option
- func Reserved(names ...string) Option
- func ScopedByTenant() Option
- func ValidateDisplayAfterNormalization() Option
- func WithAvailableLimit(n int, window time.Duration) Option
- func WithNormalizer(fn func(raw string) (string, error)) Option
- func WithValidator(fn func(normalized string) error) Option
- func WithoutAvailability() Option
- type Plugin
- func (p *Plugin) Available(ctx context.Context, handle string) (bool, error)
- func (p *Plugin) Claim(ctx context.Context, userID, handle string) error
- func (p *Plugin) ClaimWithDisplay(ctx context.Context, userID, handle, display string) error
- func (p *Plugin) DataHooks() auth.DataHooks
- func (p *Plugin) DisplayForm(handle, display string) (string, error)
- func (p *Plugin) Identifiers() []auth.IdentifierKind
- func (p *Plugin) Init(a *auth.Auth) error
- func (p *Plugin) IsReserved(handle string) bool
- func (p *Plugin) Name() string
- func (p *Plugin) Operations() []auth.Operation
- func (p *Plugin) OptionErr() error
- func (p *Plugin) RateLimits() []auth.RateRule
- func (p *Plugin) Set(ctx context.Context, handle string) (Result, error)
- func (p *Plugin) SetWithDisplay(ctx context.Context, handle, display string) (Result, error)
- func (p *Plugin) Tables() []sqlb.Table
- func (p *Plugin) UserJoin() sqlb.Join
- func (p *Plugin) Validate(*auth.Config) error
- type Request
- type Result
Constants ¶
const ( EndpointSet auth.EndpointID = "username.set" EndpointAvailable auth.EndpointID = "username.available" )
Operation ids, exported so a hook or a rate rule can name them. They are wire names, stable across releases.
const ( //ds:def id=minlength-x3pjevmb owner=@khanakia stability=stable MinLength = 2 //ds:def id=maxlength-xnr4ndys owner=@khanakia stability=stable MaxLength = 39 )
Handle length bounds. Short handles invite squatting and long ones break layouts; both ends are product choices made explicit.
const ( //ds:def id=username-availablelimit-6tq2nw8k owner=@khanakia stability=stable DefaultAvailableLimit = 30 //ds:def id=username-availablewindow-3hv7mc5p owner=@khanakia stability=stable DefaultAvailableWindow = time.Minute )
The available route's default rate limit, per client IP. The route is public and answers "taken" or "free" for any handle, so without a limit of its own one client could walk a dictionary and list every account; the numbers leave room for a sign-up form that checks as the user types.
const KindUsername auth.IdentifierKindName = "username"
KindUsername is the identifier kind this plugin registers. It is stored in every auth_identity row this plugin owns, so it never changes.
const PluginName = "username"
PluginName is the stable identity of this plugin.
const TableAliasIdentity = "uni"
TableAliasIdentity is this plugin's alias for the shared identity table. Distinct from every other plugin's alias because a composite user query joins that table once per identifier kind.
Variables ¶
var ( ErrReserved = auth.NewError("username.reserved", http.StatusConflict, "This username is not available.") // ErrImmutable refuses a rename under Immutable(): the handle was set // once and the product decided it stays. ErrImmutable = auth.NewError("username.immutable", http.StatusConflict, "Your username cannot be changed.") // ErrBadLength is a configuration error from auth.New: Length was given // bounds nobody could satisfy. ErrBadLength = errors.New("username: Length needs 1 <= min <= max") // ErrBadAvailableLimit is a configuration error from auth.New: // WithAvailableLimit was given a limit below one or a window of zero. ErrBadAvailableLimit = errors.New("username: WithAvailableLimit needs a limit of one or more and a positive window") // ErrDisplayInvalid refuses a display form (Request.DisplayUsername) // that DisplayValidator rejected, or that does not normalize to the // handle it is shown for; better-auth's INVALID_DISPLAY_USERNAME. //ds:def id=username-errdisplayinvalid-4kq8wz2m owner=@khanakia stability=stable ErrDisplayInvalid = auth.NewError("username.display_invalid", http.StatusBadRequest, "This display username is not valid for the username.") )
ErrReserved refuses a handle on the reserved list. It reads "not available" rather than "reserved" so it gives nothing away about the list.
var ( IdentityUserID = sqlb.Col[string](TableIdentity, auth.ColIdentityUserID) IdentityKind = sqlb.Col[auth.IdentifierKindName](TableIdentity, auth.ColIdentityKind) IdentityValue = sqlb.Col[string](TableIdentity, auth.ColIdentityValue) IdentityValueRaw = sqlb.Col[string](TableIdentity, auth.ColIdentityValueRaw) IdentityIsPrimary = sqlb.Col[bool](TableIdentity, auth.ColIdentityIsPrimary) IdentityCreatedAt = sqlb.Col[time.Time](TableIdentity, auth.ColIdentityCreatedAt) )
Typed columns on the identity table under this plugin's alias, for UserJoin, Of and ByHandleQuery; the core's columns, qualified by "uni".
var DefaultReserved = []string{
"admin", "administrator", "root", "support", "help", "about", "api",
"auth", "login", "logout", "signup", "register", "settings", "account",
"billing", "security", "system", "static", "assets", "www", "mail",
}
DefaultReserved is the baseline set of handles a deployment must not hand out.
Why this list exists at all: the moment handles appear in URLs, every handle competes with every route. A user who claims "settings" makes /settings ambiguous, and a user who claims "admin" or "support" can impersonate the operator in any context where a handle is shown without qualification. This is the minimum; a deployment adds its own route names.
var TableIdentity = sqlb.Table{Name: auth.TableNameIdentity, Alias: TableAliasIdentity}
TableIdentity is the core identity table under this plugin's alias.
Functions ¶
func ByHandleQuery ¶
ByHandleQuery resolves a user from a handle.
The handle is normalized in Go and compared exactly. Handles are stored in the same normal form, so the exact comparison finds them and uses the plain unique index; a SQL lower() would add a second folding rule that differs between engines. See auth.Auth.FindIdentity.
func Normalize ¶
Normalize lowercases and trims a handle. The typed spelling survives in Identity.ValueRaw for display.
The error is always nil; the signature is the auth.IdentifierKind contract. Code in this package calls fold directly so it has no error path to ignore.
func Of ¶
Of builds the standalone lookup for one user's handle. It fails with auth.ErrPluginNotRegistered when a is not wired with this plugin.
func UserBindings ¶
UserBindings binds this plugin's columns to dst, flattening the LEFT join's nulls to empty strings.
func UserJoin ¶
UserJoin is the package-level form for an application assembling a composite destination.
LEFT because a user need not have a handle, and constrained by kind because the identity table holds every identifier kind in one place.
func Validate ¶
Validate checks length and characters: lowercase letters, digits, "-" and "_", starting with a letter. The leading letter keeps a handle from ever looking like a phone number, and the missing "@" from looking like an address, so one sign-in field can tell the three apart.
func ValidateChars ¶
ValidateChars is the default character rule of Validate, without the length bounds: lowercase letters, digits, "-" and "_", starting with a letter.
Types ¶
type Availability ¶
type Availability struct {
Available bool `json:"available"`
}
Availability answers whether a handle can be claimed. Handles are public by nature, so answering is not an enumeration leak.
type Data ¶
Data is this plugin's contribution to a user, shaped as a nested edge.
Handle is the normalized lowercase form used for lookup and uniqueness. Display is what the user typed, so a handle shown back to them keeps its original casing. Both are empty when the user has no handle.
Nested rather than flattened for the same reason as the phone plugin's: a handle lives in auth_identity, not in auth_user, and the Go shape follows the schema so a reader can see the join boundary.
type Option ¶
type Option func(*Plugin)
Option configures the plugin; options are applied in order by New.
func DisplayNormalizer ¶
DisplayNormalizer rewrites the display form before it is stored, better-auth's displayUsernameNormalization (say, collapsing spaces or NFC-normalizing). By default the display form is kept as sent. The rewritten form must still normalize to the handle.
func DisplayValidator ¶
DisplayValidator adds a rule for the display form a client sends beside the handle (Request.DisplayUsername), better-auth's displayUsernameValidator: an error refuses the request with ErrDisplayInvalid. It runs on the display form as sent, or on its DisplayNormalizer result under ValidateDisplayAfterNormalization. The display form must also normalize to the handle whatever this rule says; see DisplayForm.
func Immutable ¶
func Immutable() Option
Immutable makes a handle permanent: set refuses with ErrImmutable once the user has one. Pick it when handles appear in URLs other people have saved.
func Length ¶
Length replaces MinLength and MaxLength, the bounds on a handle's length after normalization. auth.New refuses min below 1 or max below min.
func Login ¶
func Login() Option
Login allows handles in sign-in, beside or instead of an email address.
func Reserved ¶
Reserved adds handles to the reserved set, on top of DefaultReserved.
Additive rather than replacing, because a deployment adding its own route names almost never intends to also release "admin" — and if the option replaced the defaults, that release would be silent.
func ScopedByTenant ¶
func ScopedByTenant() Option
ScopedByTenant makes handles unique per tenant, for an app where each tenant has its own namespace; see auth.WithScope.
func ValidateDisplayAfterNormalization ¶
func ValidateDisplayAfterNormalization() Option
ValidateDisplayAfterNormalization runs DisplayValidator on the DisplayNormalizer result instead of the form as sent, better-auth's validationOrder.displayUsername "post-normalization".
func WithAvailableLimit ¶
WithAvailableLimit replaces the available route's default rate limit (DefaultAvailableLimit per DefaultAvailableWindow), counted per client IP and apart from every other route's. Lower it for a product whose handles are private; raise it for a form that checks on every keystroke. An app's auth.WithRateRules for EndpointAvailable still replaces it.
func WithNormalizer ¶
WithNormalizer replaces Normalize (trim and lowercase), for an app that folds look-alike characters or keeps case. The reserved list is still matched on the trimmed, lowercased form.
func WithValidator ¶
WithValidator replaces the character rule (ValidateChars) for the normalized handle; the length bounds still apply. A rule that allows "@" or a leading "+" breaks the one sign-in field that tells a handle from an address or a number, so keep those out.
func WithoutAvailability ¶
func WithoutAvailability() Option
WithoutAvailability leaves the public available route out, for an app that would rather not let anyone probe which handles exist.
type Plugin ¶
type Plugin struct {
// contains filtered or unexported fields
}
Plugin adds username identity. Build it with New: the zero value has no kind, no reserved list and no operations.
func New ¶
New builds the plugin. A handle is always unique over every row: it has no verification, so "unique among verified" would mean not unique at all.
func (*Plugin) Available ¶
Available reports whether a handle can be claimed now. The answer can be stale by the time Set runs; the unique index is the authority.
func (*Plugin) Claim ¶
Claim gives userID the handle as part of a larger operation, such as a password sign-up that takes a username in the same body. It applies every rule Set applies — normalization, length, characters, the reserved list, uniqueness — and refuses a user who already has a handle with ErrImmutable under Immutable(), or replaces it otherwise.
It is not an operation of its own: the caller is one (the password plugin's sign-up), already inside the pipeline and its transaction, so the handle and the account commit or roll back together. From outside an operation use Set.
func (*Plugin) ClaimWithDisplay ¶
ClaimWithDisplay is Claim with a display form other than the handle as typed (a sign-up body's displayUsername); see DisplayForm.
func (*Plugin) DisplayForm ¶
DisplayForm returns the form stored and shown for handle (Data.Display): handle as typed when display is empty, otherwise display after DisplayNormalizer and DisplayValidator.
One rule is not configurable: the display form must normalize to the same handle. Data.Display is shown wherever the handle is, so a display form free of the handle would let "joe" be shown as "admin", around the reserved list and the uniqueness the handle exists for. better-auth checks nothing by default; DisplayValidator is the place for a stricter rule.
func (*Plugin) Identifiers ¶
func (p *Plugin) Identifiers() []auth.IdentifierKind
Identifiers registers the username kind with the core, which from then on owns its uniqueness check.
func (*Plugin) IsReserved ¶
IsReserved reports whether a handle is withheld. The argument is normalized first so a check against "Admin" behaves like a check against "admin".
func (*Plugin) Operations ¶
Operations lists set, and available unless WithoutAvailability left it out.
func (*Plugin) OptionErr ¶
OptionErr is every error the options recorded, such as auth.ErrNilOption naming an option given nil; auth.New refuses to start while it is not nil (auth.OptionChecker).
func (*Plugin) RateLimits ¶
RateLimits gives the available route its own ByIP limit, so probing which handles exist is bounded however the app sets its blanket limit. Nothing when WithoutAvailability left the route out.
func (*Plugin) SetWithDisplay ¶
SetWithDisplay is Set with a display form other than the handle as typed; see DisplayForm.
func (*Plugin) Tables ¶
Tables declares the aliased identity table, so auth.New can reject another plugin claiming the same alias.
type Request ¶
type Request struct {
Username string `json:"username"`
DisplayUsername string `json:"displayUsername,omitempty"`
}
Request names a handle as the user typed it; the display form keeps its casing, the stored form is normalized. DisplayUsername, optional, is the form to show instead of the one typed (better-auth's displayUsername), such as "AdaLovelace" for the handle "adalovelace"; see DisplayForm.