# Spec: Privacy Monitor (data-breach checking) Checks the user's email address (and optionally addresses they've corresponded with) against known data breaches. Spec'd around **Have I Been Pwned (HIBP)**, shipped behind a config flag that stays **off until an API key is provided** — zero cost until then. ## 1. Configuration `PrivacyOptions` (new, `Configuration/Options.cs`): ```csharp public class PrivacyOptions { public const string SectionName = "Privacy"; public bool Enabled { get; set; } = false; // master flag public string Provider { get; set; } = "Hibp"; // "Hibp" | "None" public string? HibpApiKey { get; set; } // required for Hibp public int CacheHours { get; set; } = 24; // don't hammer the API } ``` `appsettings.json` gains a `Privacy` section with `Enabled: false`, empty key. `IsEnabled => Enabled && Provider == "Hibp" && !string.IsNullOrWhiteSpace(HibpApiKey)`. ## 2. Provider abstraction ```csharp public record BreachDto(string Name, string Title, string Domain, DateOnly BreachDate, IReadOnlyList DataClasses, bool IsVerified, string? Description); public interface IBreachProvider { bool IsEnabled { get; } /// Breaches for an address; empty list if clean; throws only on hard errors. Task> CheckAsync(string emailAddress, CancellationToken ct = default); } ``` - `HibpBreachProvider` — `GET https://haveibeenpwned.com/api/v3/breachedaccount/{account}?truncateResponse=false`, header `hibp-api-key`, a descriptive `user-agent`. Handle: `404` = no breaches (return empty), `401` = misconfig (log, treat as disabled), `429` = rate-limited (respect `Retry-After`, return cached/empty). Uses a named `HttpClient` "hibp" with a sane timeout. - `NullBreachProvider` — `IsEnabled => false`, returns empty. Registered when the flag is off (mirrors the `NullAiProvider` pattern in `DependencyInjection`). ## 3. Service ```csharp public interface IPrivacyService { bool IsEnabled { get; } /// Check the signed-in user's own address. Cached per PrivacyOptions.CacheHours. Task CheckSelfAsync(Guid userId, CancellationToken ct = default); } public record PrivacyReportDto(string Address, bool Checked, int BreachCount, IReadOnlyList Breaches, DateTimeOffset? LastCheckedUtc); ``` - Resolve the user's address from `User`/identity. - Cache the last result + timestamp (new `User.LastBreachCheckUtc` + a small `BreachCheck`/JSON column, or a dedicated table if we later check multiple addresses). For v1, store on `User`: `LastBreachCheckUtc`, `BreachCountCached`. - Respect `CacheHours`: return cached unless stale or `force` requested. ## 4. API (`PrivacyController : ApiControllerBase`) | Method | Route | Purpose | |--------|-------|---------| | GET | `/privacy/status` | `{ enabled }` so the UI can hide the feature when off | | GET | `/privacy/self` | cached report for the signed-in user | | POST | `/privacy/self/refresh` | force a fresh check (rate-limit aware) | All scoped to `UserId`. We **only** check the authenticated user's own address in v1 — never arbitrary addresses (avoids turning the app into a breach-lookup tool for others). ## 5. Frontend - **Privacy page** (`/app/privacy`), hidden from nav when `GET /privacy/status` is disabled. - Shows: address checked, breach count, and a card per breach (title, date, what leaked, verified badge), plus a "Re-check" button and "what this means / next steps" guidance. - Dashboard widget (optional): a small "Privacy" tile with breach count + link, also hidden when disabled. ## 6. Security & safety - [ ] Only the authenticated user's own address is ever checked (no lookup of others). - [ ] API key read from config/secrets, never logged, never sent to the client. - [ ] Feature fully inert (endpoints return `{ enabled:false }`, nav hidden) until a key is set. - [ ] Rate-limit/backoff respected; failures degrade to cached/empty, never crash. - [ ] HIBP responses cached to minimize external calls and avoid leaking usage patterns.