be6cbf90d7
Clean.Email-parity feature build-out plus a Stripe/Notion-style UI rebuild on Tailwind + shadcn-style primitives. Locks the hybrid-automation, Gmail-only, light+dark, incremental-rollout decisions and lays out the backend/frontend build sequence. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
92 lines
4.1 KiB
Markdown
92 lines
4.1 KiB
Markdown
# 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<string> DataClasses, bool IsVerified, string? Description);
|
|
|
|
public interface IBreachProvider
|
|
{
|
|
bool IsEnabled { get; }
|
|
/// <summary>Breaches for an address; empty list if clean; throws only on hard errors.</summary>
|
|
Task<IReadOnlyList<BreachDto>> 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; }
|
|
/// <summary>Check the signed-in user's own address. Cached per PrivacyOptions.CacheHours.</summary>
|
|
Task<PrivacyReportDto> CheckSelfAsync(Guid userId, CancellationToken ct = default);
|
|
}
|
|
|
|
public record PrivacyReportDto(string Address, bool Checked, int BreachCount,
|
|
IReadOnlyList<BreachDto> 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.
|