Skip to content

Commit 19c4d05

Browse files
⚡ perf: Add Redis caching to /v1/breach-analytics with privacy-safe email hashing
1 parent 58cbb33 commit 19c4d05

1 file changed

Lines changed: 29 additions & 17 deletions

File tree

api/v1/breaches.py

Lines changed: 29 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,7 @@ async def search_data_breaches(
300300
alert_key = datastore_client.key("xon_alert", email)
301301
alert_record = datastore_client.get(alert_key)
302302

303+
# Always check shieldOn first (privacy - can't cache this)
303304
if alert_record and alert_record.get("shieldOn", False):
304305
raise HTTPException(status_code=404, detail="Not found")
305306

@@ -309,6 +310,13 @@ async def search_data_breaches(
309310
if not token_valid:
310311
raise HTTPException(status_code=403, detail="Invalid token")
311312

313+
# Check cache (after shieldOn/token validation)
314+
has_token = "with_token" if token else "no_token"
315+
cache_key = f"breach-analytics:{hash_email(email)}:{has_token}"
316+
cached_result = get_cached_breaches(cache_key)
317+
if cached_result:
318+
return BreachAnalyticsResponse(**cached_result)
319+
312320
breach_data = await get_exposure(email)
313321
sensitive_data = await get_sensitive_exposure(email) if token else None
314322

@@ -348,24 +356,28 @@ async def search_data_breaches(
348356
) = summary_result
349357

350358
if breach_summary or paste_summary:
351-
return BreachAnalyticsResponse(
352-
ExposedBreaches=exposed_breaches,
353-
BreachesSummary=breach_summary
359+
response_data = {
360+
"ExposedBreaches": exposed_breaches,
361+
"BreachesSummary": breach_summary
354362
or {"domain": "", "site": "", "tmpstmp": ""},
355-
BreachMetrics=breach_metrics,
356-
PastesSummary=paste_summary or {"cnt": 0, "domain": "", "tmpstmp": ""},
357-
ExposedPastes=exposed_pastes,
358-
PasteMetrics=paste_metrics,
359-
)
360-
361-
return BreachAnalyticsResponse(
362-
BreachesSummary={"domain": "", "site": "", "tmpstmp": ""},
363-
PastesSummary={"cnt": 0, "domain": "", "tmpstmp": ""},
364-
ExposedBreaches=None,
365-
ExposedPastes=None,
366-
BreachMetrics=None,
367-
PasteMetrics=None,
368-
)
363+
"BreachMetrics": breach_metrics,
364+
"PastesSummary": paste_summary
365+
or {"cnt": 0, "domain": "", "tmpstmp": ""},
366+
"ExposedPastes": exposed_pastes,
367+
"PasteMetrics": paste_metrics,
368+
}
369+
cache_breaches(cache_key, response_data)
370+
return BreachAnalyticsResponse(**response_data)
371+
372+
empty_response = {
373+
"BreachesSummary": {"domain": "", "site": "", "tmpstmp": ""},
374+
"PastesSummary": {"cnt": 0, "domain": "", "tmpstmp": ""},
375+
"ExposedBreaches": None,
376+
"ExposedPastes": None,
377+
"BreachMetrics": None,
378+
"PasteMetrics": None,
379+
}
380+
return BreachAnalyticsResponse(**empty_response)
369381

370382
except HTTPException:
371383
raise

0 commit comments

Comments
 (0)