# `PhoenixKit.Users.LoginAttempt`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v2.32.1/lib/phoenix_kit/users/login_attempt.ex#L1)

A sign-in that did not succeed, aggregated into an hourly bucket.

One row is NOT one attempt. The unique index
`(identifier, ip_network, outcome, bucket_start)` is a dedup key, and
`PhoenixKit.Users.LoginAttempts.record/1` upserts into it, so a brute-force
run against one account from one network collapses to one row per hour with
a rising `attempt_count`. Read `attempt_count`, never `count(*)`.

## Fields worth knowing about

  * `identifier` — what the person typed, normalized (trimmed, downcased)
    and truncated to 160 characters. **Attacker-controlled text.** It is
    stored verbatim because "someone is hammering `admin@`" is a thing a
    site owner wants to see, but anything rendering it must escape it, and
    it is never used to build a query fragment.

  * `user_uuid` — `nil` when the identifier matched no account. Kept out of
    the dedup key on purpose: NULL never equals NULL in a Postgres unique
    index, so keying on it would silently stop deduplicating exactly the
    rows an attacker generates most of.

  * `outcome` — why it failed. `"invalid_credentials"` (wrong password, or
    no such account), `"rate_limited"` (rejected before credentials were
    even checked), `"inactive"` (correct password, deactivated account).
    The last is the interesting one: someone has the password.

  * `bucket_start` — the hour, truncated by the caller rather than by a
    database default, so the value written and the value conflicted on are
    always the same one.

# `t`

```elixir
@type t() :: %PhoenixKit.Users.LoginAttempt{
  __meta__: term(),
  attempt_count: term(),
  browser: term(),
  bucket_start: term(),
  first_at: term(),
  identifier: term(),
  ip_address: term(),
  ip_network: term(),
  last_at: term(),
  os: term(),
  outcome: term(),
  user: term(),
  user_agent_hash: term(),
  user_uuid: term(),
  uuid: term()
}
```

# `outcomes`

```elixir
@spec outcomes() :: [String.t()]
```

The outcomes `outcome` may take.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
