Skip to content

Commit 58cbb33

Browse files
⚡ perf: Add Redis caching to /v1/check-email with privacy-safe email hashing
1 parent 00970d6 commit 58cbb33

2 files changed

Lines changed: 34 additions & 4 deletions

File tree

api/v1/breaches.py

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Breach-related API endpoints."""
22

33
# Standard library imports
4+
import hashlib
45
import json
56
from datetime import datetime, timedelta
67
from 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")
8692
async 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(

api/v1/metrics.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,13 @@ async def get_domain_metrics(request: Request, domain: str) -> JSONResponse:
180180
if not validate_url(request):
181181
raise HTTPException(status_code=400, detail="Invalid request URL")
182182

183+
# Check cache first
184+
cache_key = f"metrics:domain:{domain.lower()}"
185+
cached_result = get_cached_metrics(cache_key)
186+
if cached_result:
187+
return JSONResponse(content=cached_result)
188+
189+
# Cache miss - build response
183190
domain_metrics = {
184191
"status": "success",
185192
"message": "Domain metrics retrieved successfully",
@@ -195,6 +202,8 @@ async def get_domain_metrics(request: Request, domain: str) -> JSONResponse:
195202
},
196203
}
197204

205+
cache_metrics(cache_key, domain_metrics)
206+
198207
return JSONResponse(content=domain_metrics)
199208

200209
except Exception as e:

0 commit comments

Comments
 (0)