feat(auth): alert users on a new-country login (PMS-657) #449
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/PMS-657-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 legacy cookie login and notifies the user, per PMS-657 (country-level MVP; region/ASN deferred). Mirrors the bunyip BUNYIP-366 work for mokosh's own credential login.
On a successful login the client 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 not opted out) mokosh sends a "new sign-in" email, then persists the new country. The first geolocatable login records the country silently, with no alert.
The whole check is best-effort: every failure (DB miss, unresolvable IP, mail send error) is logged and swallowed so it can never block a login, and it no-ops entirely when
IP2LOCATION_DB_PATHis unset or the.BINfails to load (kill switch).Scope
The legacy credential login only: password (
AuthService::login) and Google (login_with_google). The SPA path verifies bunyip-issued Resource-Server tokens and inherits detection from BUNYIP-366, so it is deliberately out of scope here.Design
GeoIpService(src/utils/geoip.rs) wraps the IP2Location DB, the same LITE.BINthe ecosystem already deploys for dmarc-reporter and bunyip (/data/IP2LOCATION-LITE-DB11.BIN). Lookups are fully offline: no per-login external call, no client IP leaves the host.AuthService::check_login_locationruns off the existingupdate_last_loginmarker at both genuine-login branches. It deliberately does not hook token issuance, which is shared with refresh and would fire on every rotation.with_geoipsetter (built from config inmain.rs, threaded throughcreate_api_router), so the three constructors and every test fixture keep working with the feature disabled and no signature churn.set_last_login_countryis tenant-scoped viabegin_with_tenant, carrying the RLS GUC exactly likeupdate_last_login.Schema
New migration
089_users_login_location_alerts.sqladds tousers:last_login_countryTEXT, nullable (ISO2, capped at 64 chars).login_location_alertsBOOLEAN NOT NULL DEFAULT TRUE (per-user opt-out).Both are threaded through
UserRow, theUsermodel (mokosh-types), theFrom<UserRow>conversion, and all four user SELECT column lists.Mail + config
Mailer::send_new_login_locationcomposes a plain-text body and routes it throughsend_text, soLogMailer,SmtpMailer, and test mocks all inherit it with no per-impl change.IP2LOCATION_DB_PATH(optional) ->AppConfig.ip2location_db_path. Unset disables the feature.Tests
geoip::tests: country-code normalization rejects IP2Location's"-"placeholder and blanks, trims real ISO2 codes.auth::service::tests::non_public_ip_detection: loopback / RFC1918 / link-local / unique-local / unspecified are non-public; routable v4/v6 are public.auth::service::tests::login_location_decision_branches: None -> Record, same -> Unchanged, differ -> Alert.-D warnings) +cargo test --lib(285 passed) green in the pinned rust-builder (rust 1.94.1).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.#PMS-657
🤖 Generated with Claude Code