The website password gate: a hard block before anyone sees anything.
When on, every request that is not the gate's own page gets a blank page with one password prompt — no login screen, no front page. The password is needed to see the site at all and to reach one's own login. The unlock lives in the session as the gate's current EPOCH — a random value that carries nothing about the password — and the epoch rotates when the password changes, when the gate is switched on, or when an admin relocks everyone; a relock is also broadcast so connected LiveViews leave. The password itself is a restricted (encrypted) setting, readable by the admin who has to tell it to the client and withheld from the settings history.
The gate stands in the browser pipeline: it protects the site's pages. Files a host serves outside that pipeline are not behind it.
Every try is kept (Attempt) with a verdict, so a bot at the door can be
told from a client mistyping. An optional lockout refuses an address after
N failures in M minutes (off by default — a whole office shares one
address). The access link unlocks without typing, for the client who
keeps getting it wrong: it is a random token in a restricted setting,
opened as a page with one button (never a bare GET that unlocks).
Summary
Functions
The access link token, or nil when none has been made.
Whether key is the current access link token.
Records a try and answers the verdict. What was typed is written down
according to keep_typed/0 — everything by default, or only a near miss
(:case/:close), or nothing. With locked: true the try is refused
unjudged (verdict :locked) but, with "all", still written down.
Counts by verdict, for the summary line.
Tells connected LiveViews the gate changed: they must pass it again.
Whether the gate is on AND has a password — a gate without a password would lock everyone out with no way in, so it is never enforced.
The gate's epoch: a random value every unlocked session carries. It says
nothing about the password. Rotating it (relock_everyone/1) locks every
session at once; it rotates on its own when the password changes or the
gate is switched on.
Judges what was typed against the password: :correct, :case (the right
letters, wrong case — caps lock), :close (a typo: one edit per four
characters of the password, at most 3), :unrelated, or :empty. Constant-time for the exact
comparison; the distance is only computed after that fails.
What of a try is written down: "all" (default — the boss wants to see
exactly what was typed, the right password and a locked-out try
included), "near" (only the right letters in the wrong case or a typo
away), "none".
The most recent attempts, newest first. :limit (default 200).
Whether address has failed too often too recently. {:locked, seconds}
with the seconds until the oldest counted failure ages out, or :ok.
Off (always :ok) when the attempts setting is 0.
The password, decrypted (a restricted setting). Read through the settings cache — it stores the decrypted value and is invalidated on every write — because the plug asks on every request while the gate is on.
The PubSub topic a relock is broadcast on.
Makes a new token (replacing any old one — old links stop working).
Rotates the epoch and tells connected LiveViews to pass the gate again.
The session key the unlock is kept under.
Whether a LiveView session map (the session of on_mount) is unlocked.
Switches the gate. Switching it ON relocks every session.
Sets the password. A changed password relocks every session.
Switching it OFF relocks every session — the ones let in by a login too.
Whether the switch itself is on, password or not (for the settings page).
Whether an unlock token taken out of a session earlier (a LiveView keeps the one it mounted with) is still the current epoch — i.e. no relock has happened since.
One try at the door, the lockout check and the record in one step under a
per-address lock, so a burst of parallel guesses cannot all slip past the
lockout before any of them is written (panel finding). Returns
{:locked, seconds} or attempt/2's {verdict, row}.
Marks conn's session unlocked for the current epoch — minting the first
epoch if the gate never had one.
Whether conn's session is unlocked for the current epoch.
Whether a logged-in user passes the gate without the password (default: yes — they proved more than the password already, and it keeps the admin who just changed the password from being thrown out by their own change).
Functions
@spec access_link_token() :: String.t() | nil
The access link token, or nil when none has been made.
Whether key is the current access link token.
@spec attempt(String.t() | nil, keyword()) :: {atom(), PhoenixKit.WebsiteAccess.Attempt.t() | nil}
Records a try and answers the verdict. What was typed is written down
according to keep_typed/0 — everything by default, or only a near miss
(:case/:close), or nothing. With locked: true the try is refused
unjudged (verdict :locked) but, with "all", still written down.
@spec attempt_counts() :: %{required(String.t()) => non_neg_integer()}
Counts by verdict, for the summary line.
Tells connected LiveViews the gate changed: they must pass it again.
@spec clear_attempts() :: {non_neg_integer(), nil}
@spec enabled?() :: boolean()
Whether the gate is on AND has a password — a gate without a password would lock everyone out with no way in, so it is never enforced.
@spec epoch() :: String.t() | nil
The gate's epoch: a random value every unlocked session carries. It says
nothing about the password. Rotating it (relock_everyone/1) locks every
session at once; it rotates on its own when the password changes or the
gate is switched on.
@spec judge(term()) :: :correct | :case | :close | :unrelated | :empty
Judges what was typed against the password: :correct, :case (the right
letters, wrong case — caps lock), :close (a typo: one edit per four
characters of the password, at most 3), :unrelated, or :empty. Constant-time for the exact
comparison; the distance is only computed after that fails.
What of a try is written down: "all" (default — the boss wants to see
exactly what was typed, the right password and a locked-out try
included), "near" (only the right letters in the wrong case or a typo
away), "none".
@spec list_attempts(keyword()) :: [PhoenixKit.WebsiteAccess.Attempt.t()]
The most recent attempts, newest first. :limit (default 200).
@spec lockout(String.t() | nil) :: :ok | {:locked, pos_integer()}
Whether address has failed too often too recently. {:locked, seconds}
with the seconds until the oldest counted failure ages out, or :ok.
Off (always :ok) when the attempts setting is 0.
@spec lockout_attempts() :: non_neg_integer()
@spec lockout_minutes() :: pos_integer()
@spec password() :: String.t() | nil
The password, decrypted (a restricted setting). Read through the settings cache — it stores the decrypted value and is invalidated on every write — because the plug asks on every request while the gate is on.
@spec password_set?() :: boolean()
The PubSub topic a relock is broadcast on.
Makes a new token (replacing any old one — old links stop working).
Rotates the epoch and tells connected LiveViews to pass the gate again.
The session key the unlock is kept under.
Whether a LiveView session map (the session of on_mount) is unlocked.
Switches the gate. Switching it ON relocks every session.
Sets the password. A changed password relocks every session.
Switching it OFF relocks every session — the ones let in by a login too.
@spec switched_on?() :: boolean()
Whether the switch itself is on, password or not (for the settings page).
Whether an unlock token taken out of a session earlier (a LiveView keeps the one it mounted with) is still the current epoch — i.e. no relock has happened since.
@spec try(String.t() | nil, keyword()) :: {:locked, pos_integer()} | {atom(), PhoenixKit.WebsiteAccess.Attempt.t() | nil}
One try at the door, the lockout check and the record in one step under a
per-address lock, so a burst of parallel guesses cannot all slip past the
lockout before any of them is written (panel finding). Returns
{:locked, seconds} or attempt/2's {verdict, row}.
@spec unlock(Plug.Conn.t()) :: Plug.Conn.t()
Marks conn's session unlocked for the current epoch — minting the first
epoch if the gate never had one.
@spec unlocked?(Plug.Conn.t()) :: boolean()
Whether conn's session is unlocked for the current epoch.
Whether a logged-in user passes the gate without the password (default: yes — they proved more than the password already, and it keeps the admin who just changed the password from being thrown out by their own change).