fix(email): reword password-reset email + show request country (BUNYIP-397) #389

Merged
nrupard merged 1 commit from fix/BUNYIP-397-reset-email-copy into main 2026-07-22 17:18:14 +02:00
Owner

What

Reword the password-reset email so it reads as a routine notification instead of "Someone asked to reset the password for your account" (which sounds like an intrusion), and show the country the request came from when the IP resolves.

Before: "Someone asked to reset the password for your {app} account. Click below to choose a new one."

After: "A password reset was requested for your {app} account (from United States). If this was you, choose a new password below. If it wasn't, you can ignore this email and nothing changes."

How

  • GeoIpService::country_name(ip): the IP2Location LITE DB already stores the long country name alongside the code, so this needs no new dependency. Mirrors country_code's "-"/blank normalization.
  • AuthService::country_name_for_ip(ip): the same best-effort, privacy-preserving filter the login-location signal uses (GeoIP present, IP present, public IP only), yielding None otherwise.
  • request_password_reset handler: resolves the country from the already-captured client IP and threads it into send_password_reset.
  • EmailService::send_password_reset(email, token, country: Option<&str>): inserts the country as an empty string when absent, so the Tera {% if country %} guard hides the "(from ...)" clause.
  • Admin-initiated reset (admin.rs): passes None (no requester IP).
  • Both password_reset.html and password_reset.txt reworded.

Scope / decisions (with Nate)

Country-level only (GeoIP resolves country, not city). When the location is unknown (admin reset, local IP, or IP2LOCATION_DB_PATH unset) the email omits the "(from ...)" clause and reads correctly. The login-location alert email keeps its terser country code; unifying that is out of scope.

Tests

GeoIpService gains a normalize_country_name unit test (placeholder/blank rejection, trimming). No new SQL, so the committed .sqlx cache is unchanged. cargo clippy --workspace --all-targets -- -D warnings, cargo fmt --all --check, and cargo test --workspace --lib pass.

#BUNYIP-397

## What Reword the password-reset email so it reads as a routine notification instead of "Someone asked to reset the password for your account" (which sounds like an intrusion), and show the country the request came from when the IP resolves. Before: "Someone asked to reset the password for your {app} account. Click below to choose a new one." After: "A password reset was requested for your {app} account (from United States). If this was you, choose a new password below. If it wasn't, you can ignore this email and nothing changes." ## How - `GeoIpService::country_name(ip)`: the IP2Location LITE DB already stores the long country name alongside the code, so this needs no new dependency. Mirrors `country_code`'s `"-"`/blank normalization. - `AuthService::country_name_for_ip(ip)`: the same best-effort, privacy-preserving filter the login-location signal uses (GeoIP present, IP present, public IP only), yielding `None` otherwise. - `request_password_reset` handler: resolves the country from the already-captured client IP and threads it into `send_password_reset`. - `EmailService::send_password_reset(email, token, country: Option<&str>)`: inserts the country as an empty string when absent, so the Tera `{% if country %}` guard hides the "(from ...)" clause. - Admin-initiated reset (`admin.rs`): passes `None` (no requester IP). - Both `password_reset.html` and `password_reset.txt` reworded. ## Scope / decisions (with Nate) Country-level only (GeoIP resolves country, not city). When the location is unknown (admin reset, local IP, or `IP2LOCATION_DB_PATH` unset) the email omits the "(from ...)" clause and reads correctly. The login-location alert email keeps its terser country code; unifying that is out of scope. ## Tests `GeoIpService` gains a `normalize_country_name` unit test (placeholder/blank rejection, trimming). No new SQL, so the committed `.sqlx` cache is unchanged. `cargo clippy --workspace --all-targets -- -D warnings`, `cargo fmt --all --check`, and `cargo test --workspace --lib` pass. #BUNYIP-397
fix(email): reword password-reset email and show the request country (BUNYIP-397)
All checks were successful
E2E / Playwright against deployment (pull_request) Successful in 31s
Check / fmt + clippy + build + tests (pull_request) Successful in 12m13s
Create release / Create release from merged PR (pull_request) Has been skipped
d66073d356
The reset email opened with "Someone asked to reset the password for your account", which reads as alarming rather than as a routine notification. Reword it to "A password reset was requested for your account ... If this was you, choose a new password below. If it wasn't, you can ignore this email and nothing changes.", and show the country the request came from when the IP resolves.

Bunyip already resolves an IP to a country for login-location alerts and the reset handler already captures the client IP, so this reuses both: GeoIpService gains country_name (the IP2Location LITE DB carries the long country name, so no new dependency), AuthService gains country_name_for_ip (the same GeoIP-present / public-IP filter the login signal uses), and the handler threads the resolved country into send_password_reset. The name is inserted as an empty string when absent, so the Tera "{% if country %}" guard hides the "(from ...)" clause for admin-initiated resets, local IPs, or when IP2LOCATION_DB_PATH is unset. Country-level only (GeoIP has no city).

No new SQL, so the .sqlx offline cache is unchanged.

#BUNYIP-397

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
nrupard scheduled this pull request to auto merge when all checks succeed 2026-07-22 17:13:40 +02:00
nrupard deleted branch fix/BUNYIP-397-reset-email-copy 2026-07-22 17:18:14 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/bunyip!389
No description provided.