feat(auth): alert users on a new-country login (LC-580) #550
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/lc580-login-location-alerts"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Detects a country-level login-location change on the Bunyip SSO callback and notifies the user, per LC-580 (country-level MVP; region/ASN deferred). The Lets-Chat analog of BUNYIP-366 / PMS-657 / VS-91, built by wiring up the orphaned
login_alertscaffold.On a successful login the trusted-proxy-aware session IP is resolved to an ISO 3166-1 alpha-2 country via an offline IP2Location LITE database and compared to the country recorded at the user's previous login. On a change (and only when the user has alerts enabled) Lets-Chat emails a "new sign-in" alert, then persists the new country. The first geolocatable login records the country silently.
The alert is spliced into
bunyip_sso::get_callbackon a detachedtokio::spawn, so the geoip lookup and any SMTP send never add latency to the login redirect. It no-ops entirely whenIP2LOCATION_DB_PATHis unset or the.BINfails to load (kill switch), and is best-effort throughout (every failure logged and swallowed).Reuses the orphaned scaffold
The removed local-auth path left disconnected login-alert scaffold; this wires it up rather than building new:
templates/email/login_alert.{txt,html}+views/login_alert.rsview structs (extended with acountryfield; reworded from device to country semantics).notify_login_alerts_enabledopt-out column + its existing settings setter (db::auth::set_notify_login_alerts_enabled,settings.rs).Mailer(send_multipart) and the recipient/verified/opt-in/mailer gating order fromemail::notification.The dead device-fingerprint
login_alert_devicestable is left untouched.New
0040_login_country.sql:users.last_login_country TEXT.db::auth::get_last_login_country/set_last_login_country(dedicated helpers, noUserRecord/SELECT churn).geoip.rs:GeoipResolveroverip2location, plus the pureis_public_ipfilter and therecord/unchanged/alertdecision, with unit tests.login_alert.rs: the orchestrator (maybe_alert).AppState.geoip: Option<Arc<GeoipResolver>>, built fromIP2LOCATION_DB_PATHinmain.rs(mirrors the mailer/stt/llmfrom_envpattern;ArcbecauseAppStateisClone).Scope
Consistent with the sibling MVPs:
last_login_countryon the user row (not a separate raw-IP history table); country-only sensitivity; region/ASN deferred. Non-public IPs (loopback, RFC1918, unique-local, link-local, unspecified) are ignored, and repeated logins from the same new country do not re-alert.Tests
geoip::tests: country-code normalization (rejects IP2Location's"-"/blanks), the non-public-IP filter, and the record/unchanged/alert decision.-D warnings) on both the standalone and saas feature sets (compiling all test targets, so every AppState literal is checked) + the geoip unit tests.Diff note
The bulk of the changed files is mechanical: every test's inline
AppStateliteral gainsgeoip: None(the repo builds full inlineAppStateliterals per test rather than sharing a helper). The substantive change is ~11 files.Deploy note
Dormant until a deployment sets
IP2LOCATION_DB_PATHto a readable LITE DB11.BIN; otherwise a single startup log line and no behavior change.#LC-580
🤖 Generated with Claude Code
The alert email showed the ISO 3166-1 alpha-2 code ("US") rather than the country name. IP2Location records already carry the full name (long_name), so GeoipResolver::resolve now returns both the code and the name (ResolvedCountry). The change comparison and the stored last_login_country stay on the stable ISO code; only the email display uses the name (e.g. "United States"), falling back to the code when the DB carries no usable long name. #LC-580 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>