# `PhoenixKit.Users.LoginAlerts`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v2.22.16/lib/phoenix_kit/users/login_alerts.ex#L1)

New-login security alerts ("we noticed a new login to your account").

On every login (`PhoenixKitWeb.Users.Auth.log_in_user/3`), the request's
`(ip_address, user_agent_hash)` pair is checked against
`PhoenixKit.Users.Auth.KnownDevice` rows for that user, and a row is
persisted for every new pair either way (still used to enrich the
self-service "Active Sessions" list with browser/OS/location per
session — see `PhoenixKit.Users.Sessions.list_user_device_sessions/2`).
A `user.new_login_detected` activity entry is always logged too, for the
audit trail.

The reader-facing alarms — the email
(`PhoenixKit.Users.Auth.UserNotifier.deliver_new_login_alert/2`, gated
behind `new_login_alert_enabled`) and the in-app notification — are
narrower than "unrecognized pair", though, and are skipped when either:

  * this is the very first `KnownDevice` row the account has ever had
    (see below), or
  * the browser/OS (`user_agent_hash` alone, regardless of IP) HAS been
    seen before for this account — an IP alone changing is not "a new
    device" from the user's point of view. Most residential/mobile
    connections don't have a static IP, so alerting on IP change alone
    fires on a large fraction of logins from an already-trusted browser
    and trains people to ignore the email — the exact alert-fatigue
    failure this codebase already avoids elsewhere (see
    `PhoenixKit.Utils.SessionFingerprint`, which treats an IP-only
    mismatch as a mere warning, never an alarm).

The first-device skip exists because registration ends by logging the new
user in through this exact path (`log_in_user/3`), and an account with no
device history yet cannot help but treat its own signup as "a new device" —
without it, every signup on an installation with alerts on immediately
received a "we noticed a new login" security email about the login it just
performed to finish registering. The device is still recorded (so the
*second* login, from anywhere else, correctly reads as new), and the
activity entry still logs for the audit trail — only the two
reader-facing alarms are suppressed.

A recognized `(ip, ua)` pair just bumps `last_seen_at` — no alert, no email.

Sends synchronously (matching every other PhoenixKit auth email —
confirmation, password reset, magic link — none of which are queued
through Oban): a "new device" login is inherently rare per user (every
subsequent login from the same device is silent), so the odd extra
round-trip on a first-time login doesn't justify background-job
infrastructure this feature would otherwise be the only user of. A
send failure is logged and swallowed — it must never block sign-in.

# `check`

```elixir
@spec check(map(), Plug.Conn.t()) :: :ok
```

Records a login from `conn` for `user`, alerting on a new device.

No-ops entirely (no DB write, no email) when the feature is disabled.
Never raises — a failure here must never block sign-in.

# `enabled?`

```elixir
@spec enabled?() :: boolean()
```

Whether new-login alerts are enabled (setting `new_login_alert_enabled`,
default `false`).

# `location_for`

```elixir
@spec location_for(String.t()) :: String.t() | nil
```

Best-effort "City, Country" string for `ip_address`, or `nil`.

Never raises; a lookup failure (disabled, rate-limited, invalid IP)
just means the alert email omits the location line.

---

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