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>
4.1 KiB
4.1 KiB
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):
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
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, headerhibp-api-key, a descriptiveuser-agent. Handle:404= no breaches (return empty),401= misconfig (log, treat as disabled),429= rate-limited (respectRetry-After, return cached/empty). Uses a namedHttpClient"hibp" with a sane timeout.NullBreachProvider—IsEnabled => false, returns empty. Registered when the flag is off (mirrors theNullAiProviderpattern inDependencyInjection).
3. Service
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 smallBreachCheck/JSON column, or a dedicated table if we later check multiple addresses). For v1, store onUser:LastBreachCheckUtc,BreachCountCached. - Respect
CacheHours: return cached unless stale orforcerequested.
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 whenGET /privacy/statusis 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.