Documentation
¶
Overview ¶
Package identity owns the rules for account login names, and the single door that turns a typed identifier into a user.
Why this package exists ¶
filex accounts have always been addressed by e-mail. The protocols filex is growing do not carry an e-mail comfortably: `sftp://ada@example.com@host` needs escaping, rclone and WinSCP config files split on `@`, and FTP's USER command with an `@` in it confuses proxying clients. So accounts get a second, short identifier — and every surface accepts EITHER (the owner's words, translated from Turkish: "wherever we can, let's look at both the e-mail and the username — think of it like a dual-side login").
"Every surface" is the load-bearing half. A login rule that lives in one handler is a login rule that is subtly different in the next one, and the difference is invisible until a user reports that the same credentials work in the browser and fail over SFTP. So resolution lives here, once, and the surfaces call Resolve.
The disambiguation rule ¶
An identifier containing `@` is an e-mail; anything else is a username. The lookup NEVER tries both.
That is a security rule, not a tidiness one. If resolution fell back from one to the other, a username could be chosen to collide with the local part of someone else's e-mail (or vice versa), and which account answered would depend on row order and on which query happened to run first. Usernames are therefore forbidden from containing `@` (Validate rejects it), which makes the two namespaces provably disjoint and the rule total: every identifier has exactly one interpretation.
Index ¶
- Constants
- Variables
- func ClaimBootstrap(ctx context.Context, c Claimer, u *model.User) error
- func EnsureUsername(ctx context.Context, c Claimer, u *model.User) (string, error)
- func LooksLikeEmail(identifier string) bool
- func Names(u *model.User, identifier string) bool
- func Normalize(identifier string) string
- func Resolve(ctx context.Context, l Lookup, identifier string) (*model.User, error)
- func Suggest(email string) string
- func Validate(username string) error
- type Claimer
- type Lookup
- type Problem
Constants ¶
const ( MinLen = 3 MaxLen = 32 )
Bounds for a username. 32 is also the width of the MySQL column, so the validator and the schema cannot disagree.
const ( ProblemEmpty = "empty" ProblemAt = "at" ProblemShort = "short" ProblemLong = "long" ProblemUpper = "upper" ProblemDigit = "digit" ProblemChar = "char" ProblemReserved = "reserved" )
Problem kinds Check answers. They are the rule's names for the HTTP layer and the browser, which each say them in the reader's language; the English sentence Validate builds is for logs and the CLI.
const BootstrapUsername = "admin"
BootstrapUsername is the login name of the administrator filex creates at first run — the one account allowed to hold a reserved name.
Variables ¶
var ( ErrInvalidUsername = errors.New("invalid username") ErrReservedUsername = errors.New("reserved username") ErrNotFound = errors.New("no such account") )
Sentinel errors. Callers match on these; the wrapped text is what a person reads, and the API boundary is where it gets translated.
Functions ¶
func ClaimBootstrap ¶
ClaimBootstrap gives the first-run administrator the username "admin".
⚠ The owner's decision (2026-09-22): the first administrator of a new install is "admin", not "admin2". "admin" is reserved so that nobody else can take it — a later `admin@…` account still becomes "admin2" through EnsureUsername, and no person can rename themselves to it (Check refuses it) — but the bootstrap account is the one the reservation was made FOR. Every screen named it "admin2" because EnsureUsername's collision loop stepped past the reserved name.
Only server.FirstRun calls this, on an install with no accounts. Existing installs are not touched: an "admin2" stays "admin2" (no migration). A name already held by another account is left alone and reported.
func EnsureUsername ¶
EnsureUsername assigns a username to an account that has none, derived from its e-mail, and returns the name it claimed. It is idempotent: an account that already has one is left alone, and re-running the backfill after an interruption renames nobody.
Uniqueness is settled here rather than in Suggest because only this function can see the database. The suffix is a plain counter appended to the derived name — deterministic given the same order of arrival, and recognisable to its owner, which a random string would not be.
func LooksLikeEmail ¶
LooksLikeEmail reports whether an identifier should be resolved as an e-mail. The test is deliberately the crudest one that is total: usernames cannot contain `@` (Validate enforces it), so the presence of one is decisive. A stricter parse would only add ways for a valid address to be misread.
func Names ¶
Names reports whether the identifier addresses this account — as its e-mail or as its username, case-insensitively.
It exists for the surfaces that have already resolved a user by some other route and now need to confirm the typed identifier belongs to them: the WebDAV credential cache (is the cached account still the one being named?) and the WebDAV API-token path (does the Basic username name the token's owner?). Comparing only the e-mail there would let a credential work in the browser and fail over the protocol, which is the per-surface drift this package exists to prevent.
func Normalize ¶
Normalize folds an identifier to its canonical form for lookup: trimmed and lowercased. Both namespaces are case-insensitive — a user who types `Ada` into FileZilla means `ada`, and an e-mail address is already stored lower.
func Resolve ¶
Resolve turns what a person typed into an account: e-mail or username, one interpretation each (see the package comment for why it never tries both).
This is the ONLY function a login surface should call. The browser session, the desktop app, WebDAV Basic, FTPS, SFTP and the protocol credential pages all go through here, so "which identifiers work" has exactly one answer instead of one answer per surface.
Returns ErrNotFound when there is no such account — including when the identifier is not even a well-formed username, because "no account" is the only thing a login surface may reveal about a failed lookup.
func Suggest ¶
Suggest derives a candidate username from an e-mail address. It is deterministic: the same address always yields the same candidate, so a backfill that is interrupted and re-run does not rename anybody.
Uniqueness is NOT this function's job — Suggest can and will return a name that is already taken, including a reserved one. Claiming is EnsureUsername's job, because only it can see the database. That split is deliberate: Suggest never invents an unrelated name to dodge a collision.
Types ¶
type Claimer ¶
type Claimer interface {
Lookup
SetUserUsername(ctx context.Context, id int64, username string) error
}
Claimer additionally assigns a username. Used by account creation and by the one-time backfill.
type Lookup ¶
type Lookup interface {
GetUserByEmail(ctx context.Context, email string) (*model.User, error)
GetUserByUsername(ctx context.Context, username string) (*model.User, error)
}
Lookup is the slice of db.Store this package needs to resolve an identifier. Declared here rather than imported so identity stays a leaf package that the db drivers, the auth drivers and every protocol server can all depend on.
type Problem ¶
type Problem struct {
Kind string
Char rune // ProblemChar: the first character that is not allowed
Limit int // ProblemShort / ProblemLong: MinLen or MaxLen
}
Problem is why a username is refused, in parts: the kind, and the one detail the sentence needs (the character, or the limit).
func Check ¶
Check reports why a username is not acceptable, nil when it is.
The character set is ASCII on purpose, and this is the one place in filex where that is the right answer: a username travels through SSH USER records, FTP command lines, S3 key labels and shell config files, all of which mangle non-ASCII differently. The DISPLAY name is where a person's name belongs, and it has always accepted the full alphabet.
⚠ The browser checks the same rules while a person types (packages/core/src/lib/accountRules.ts); this is the authority, that is the mirror. A rule added here must be added there, or the form lets through a name the save then refuses.