11"""Breach-related API endpoints."""
22
33# Standard library imports
4+ import hashlib
45import json
56from datetime import datetime , timedelta
67from typing import Dict , Optional , Union
@@ -81,6 +82,11 @@ def cache_breaches(
8182 pass
8283
8384
85+ def hash_email (email : str ) -> str :
86+ """Hash email for privacy-safe cache keys."""
87+ return hashlib .sha256 (email .lower ().encode ()).hexdigest ()[:16 ]
88+
89+
8490@router .get ("/breaches" , response_model = BreachListResponse )
8591@custom_rate_limiter ("2 per second;50 per hour;100 per day" )
8692async def get_xposed_breaches (
@@ -436,11 +442,8 @@ async def search_email(
436442 return EmailBreachErrorResponse (Error = "Not found" )
437443
438444 email = email .lower ()
439- breach_data = await get_exposure (email )
440-
441- if not breach_data :
442- return EmailBreachErrorResponse (Error = "Not found" )
443445
446+ # Always check shieldOn first (privacy - can't cache this)
444447 data_store = datastore .Client ()
445448 alert_key = data_store .key ("xon_alert" , email )
446449 alert_record = data_store .get (alert_key )
@@ -455,6 +458,19 @@ async def search_email(
455458 },
456459 )
457460
461+ # Check cache (after shieldOn check passes)
462+ cache_key = f"check-email:{ hash_email (email )} :{ details } "
463+ cached_result = get_cached_breaches (cache_key )
464+ if cached_result :
465+ # Replace hashed email with actual email in response
466+ cached_result ["email" ] = email
467+ return JSONResponse (status_code = 200 , content = cached_result )
468+
469+ breach_data = await get_exposure (email )
470+
471+ if not breach_data :
472+ return EmailBreachErrorResponse (Error = "Not found" )
473+
458474 xon_key = data_store .key ("xon" , email )
459475 xon_record = data_store .get (xon_key )
460476
@@ -503,6 +519,11 @@ async def search_email(
503519
504520 response_content ["breach_details" ] = formatted_breaches
505521
522+ # Cache the response (use placeholder for email to avoid storing PII)
523+ cache_content = response_content .copy ()
524+ cache_content ["email" ] = "__cached__"
525+ cache_breaches (cache_key , cache_content )
526+
506527 return JSONResponse (status_code = 200 , content = response_content )
507528
508529 return JSONResponse (
0 commit comments