22
33# Standard library imports
44import json
5- from datetime import datetime
6- from typing import Optional , Union
5+ from datetime import datetime , timedelta
6+ from typing import Dict , Optional , Union
77from urllib .parse import urlparse
88
99# Third-party imports
1010from fastapi import APIRouter , Header , HTTPException , Path , Query , Request
1111from fastapi .responses import JSONResponse , Response
1212from google .cloud import datastore
13+ from redis import Redis
1314
1415# Local imports
15- from config .settings import MAX_EMAIL_LENGTH
16+ from config .settings import MAX_EMAIL_LENGTH , REDIS_DB , REDIS_HOST , REDIS_PORT
1617from models .responses import (
1718 BreachAnalyticsResponse ,
1819 BreachAnalyticsV2Response ,
4142
4243router = APIRouter ()
4344
45+ # Redis client for caching
46+ redis_client = Redis (
47+ host = REDIS_HOST , port = REDIS_PORT , db = REDIS_DB , decode_responses = True
48+ )
49+
50+ # Cache TTL: 24 hours
51+ BREACH_CACHE_TTL_HOURS = 24
52+
53+
54+ def get_breach_cache_key (breach_id : Optional [str ], domain : Optional [str ]) -> str :
55+ """Generate cache key based on query parameters."""
56+ if breach_id :
57+ return f"breaches:id:{ breach_id .lower ()} "
58+ elif domain :
59+ return f"breaches:domain:{ domain .lower ()} "
60+ return "breaches:all"
61+
62+
63+ def get_cached_breaches (cache_key : str ) -> Optional [Dict ]:
64+ """Retrieve cached breach results from Redis."""
65+ try :
66+ cached_data = redis_client .get (cache_key )
67+ if cached_data :
68+ return json .loads (cached_data )
69+ except Exception :
70+ pass
71+ return None
72+
73+
74+ def cache_breaches (
75+ cache_key : str , result : Dict , expiry_hours : int = BREACH_CACHE_TTL_HOURS
76+ ) -> None :
77+ """Cache breach results in Redis."""
78+ try :
79+ redis_client .setex (cache_key , timedelta (hours = expiry_hours ), json .dumps (result ))
80+ except Exception :
81+ pass
82+
4483
4584@router .get ("/breaches" , response_model = BreachListResponse )
4685@custom_rate_limiter ("2 per second;50 per hour;100 per day" )
@@ -55,13 +94,10 @@ async def get_xposed_breaches(
5594 or for all domains if no domain is specified.
5695 """
5796 try :
58- client = datastore .Client ()
59- query = client .query (kind = "xon_breaches" )
60-
97+ # Validate inputs first
6198 if breach_id :
6299 if not validate_variables ([breach_id ]):
63100 raise HTTPException (status_code = 400 , detail = "Invalid Breach ID" )
64- query .key_filter (client .key ("xon_breaches" , breach_id ), "=" )
65101 elif domain :
66102 # Try to extract domain from URL if a full URL is provided
67103 if not validate_domain (domain ):
@@ -71,23 +107,24 @@ async def get_xposed_breaches(
71107 domain = extracted
72108 else :
73109 raise HTTPException (status_code = 400 , detail = "Invalid Domain" )
110+
111+ # Check cache first
112+ cache_key = get_breach_cache_key (breach_id , domain )
113+ cached_result = get_cached_breaches (cache_key )
114+ if cached_result :
115+ return BreachListResponse (** cached_result )
116+
117+ # Cache miss - query Datastore
118+ client = datastore .Client ()
119+ query = client .query (kind = "xon_breaches" )
120+
121+ if breach_id :
122+ query .key_filter (client .key ("xon_breaches" , breach_id ), "=" )
123+ elif domain :
74124 query .add_filter ("domain" , "=" , domain )
75125 else :
76126 query .order = ["-timestamp" ]
77127
78- # Check if-modified-since header
79- latest_entity = list (query .fetch (limit = 1 ))
80- if latest_entity and if_modified_since :
81- latest_timestamp = latest_entity [0 ]["timestamp" ]
82- try :
83- if_modified_dt = datetime .strptime (
84- if_modified_since , "%a, %d %b %Y %H:%M:%S GMT"
85- )
86- if latest_timestamp .replace (tzinfo = None ) <= if_modified_dt :
87- return Response (status_code = 304 )
88- except ValueError :
89- pass
90-
91128 entities = list (query .fetch ())
92129 if not entities :
93130 return BreachListResponse (
@@ -136,6 +173,13 @@ async def get_xposed_breaches(
136173 status = "notFound" , message = f"No breaches found for domain { domain } "
137174 )
138175
176+ # Build response and cache it
177+ response_data = {
178+ "status" : "success" ,
179+ "exposedBreaches" : [breach .model_dump () for breach in breach_details ],
180+ }
181+ cache_breaches (cache_key , response_data )
182+
139183 return BreachListResponse (status = "success" , exposedBreaches = breach_details )
140184
141185 except HTTPException :
0 commit comments