V197: Failed sign-in attempts.
phoenix_kit_login_attempts records sign-ins that did NOT succeed. Until
now a wrong password produced a flash and nothing else: no activity entry,
no row, nothing either the targeted account holder or the site owner could
ever see. The only trace was Hammer's in-memory counter, which is node-local,
lost on restart, and only says anything once a bucket overflows.
Rows are aggregated at write time rather than one-per-attempt. The
unique index is the dedup key, and the write is a single
INSERT ... ON CONFLICT DO UPDATE SET attempt_count = attempt_count + 1 —
no read, no lock contention, and a brute-force run against one account from
one network collapses into one row per hour instead of thousands.
bucket_start is that hour, truncated by the caller
(PhoenixKit.Users.LoginAttempts) rather than by a database default, so the
value written and the value conflicted on are always the same one.
identifier is what the person typed, normalized and truncated to 160
characters — the same cap PhoenixKitWeb.Users.Session already applies
before echoing it back into a flash. It is attacker-controlled text and must
be treated as such by anything that renders it.
user_uuid is NULL when the identifier matched no account, and the FK is
ON DELETE CASCADE so deleting a user takes their attempt history with them.
It is deliberately NOT part of the dedup key: NULL never equals NULL in a
unique index, so keying on it would silently stop deduplicating for exactly
the rows an attacker generates most of.