1111
1212from app .api .v1 .auth import verify_credentials
1313from app .api .v1 .utils import create_response , sanitize_for_json
14+ from app .security_utils import require_safe_filename
1415from app .services .admin_service import (
1516 AdminService ,
1617 CacheManagementError ,
@@ -116,12 +117,12 @@ async def clear_cache(
116117 try :
117118 result = await service .clear_cache (cache_type )
118119 return create_response (result )
119- except CacheManagementError as e :
120- logger .error (f "Cache management error: { str ( e ) } " )
121- return create_response ({"error" : str ( e ) }, status_code = 500 )
122- except Exception as e :
123- logger .error (f "Unexpected error clearing cache: { str ( e ) } " )
124- return create_response ({"error" : f "Failed to clear cache: { str ( e ) } " }, status_code = 500 )
120+ except CacheManagementError :
121+ logger .error ("Cache management error while clearing cache" , exc_info = True )
122+ return create_response ({"error" : "Failed to clear cache" }, status_code = 500 )
123+ except Exception :
124+ logger .error ("Unexpected error clearing cache" , exc_info = True )
125+ return create_response ({"error" : "Failed to clear cache" }, status_code = 500 )
125126
126127
127128class CachePurgeRequest (BaseModel ):
@@ -162,8 +163,8 @@ async def purge_cache(
162163 except HTTPException :
163164 raise
164165 except Exception as e :
165- logger .error (f "Unexpected error purging cache: { e } " , exc_info = True )
166- raise HTTPException (status_code = 500 , detail = str ( e ) ) from e
166+ logger .error ("Unexpected error purging cache" , exc_info = True )
167+ raise HTTPException (status_code = 500 , detail = "Failed to purge cache" ) from e
167168
168169
169170@router .post ("/reindex" )
@@ -176,15 +177,11 @@ async def reindex(
176177 result = await service .reindex_resources ()
177178 return create_response (result , callback )
178179 except ReindexingError as e :
179- logger .error (f"Reindexing error: { str (e )} " )
180- raise HTTPException (
181- status_code = 500 , detail = {"message" : "Reindexing failed" , "error" : str (e )}
182- ) from e
180+ logger .error ("Reindexing error" , exc_info = True )
181+ raise HTTPException (status_code = 500 , detail = {"message" : "Reindexing failed" }) from e
183182 except Exception as e :
184- logger .error (f"Unexpected error during reindexing: { str (e )} " )
185- raise HTTPException (
186- status_code = 500 , detail = {"message" : "Reindexing failed" , "error" : str (e )}
187- ) from e
183+ logger .error ("Unexpected error during reindexing" , exc_info = True )
184+ raise HTTPException (status_code = 500 , detail = {"message" : "Reindexing failed" }) from e
188185
189186
190187@router .post ("/resources/{id}/summarize" )
@@ -209,14 +206,18 @@ async def summarize_resource(
209206 sanitized_response = sanitize_for_json (result )
210207 return create_response (sanitized_response , callback )
211208 except ResourceNotFoundError as e :
212- logger .error (f "Resource not found: { str ( e ) } " )
213- raise HTTPException (status_code = 404 , detail = str ( e ) ) from e
209+ logger .error ("Resource not found while summarizing %s" , id , exc_info = True )
210+ raise HTTPException (status_code = 404 , detail = "Resource not found" ) from e
214211 except ResourceProcessingError as e :
215- logger .error (f "Resource processing error: { str ( e ) } " )
216- raise HTTPException (status_code = 500 , detail = str ( e ) ) from e
212+ logger .error ("Resource processing error while summarizing %s" , id , exc_info = True )
213+ raise HTTPException (status_code = 500 , detail = "Failed to start summary generation" ) from e
217214 except Exception as e :
218- logger .error (f"Unexpected error triggering summary generation for resource { id } : { str (e )} " )
219- raise HTTPException (status_code = 500 , detail = str (e )) from e
215+ logger .error (
216+ "Unexpected error triggering summary generation for resource %s" ,
217+ id ,
218+ exc_info = True ,
219+ )
220+ raise HTTPException (status_code = 500 , detail = "Failed to start summary generation" ) from e
220221
221222
222223@router .post ("/resources/{id}/identify-geo-entities" )
@@ -237,17 +238,30 @@ async def identify_geo_entities(
237238 result = await service .identify_geo_entities (id )
238239 return create_response (result , callback )
239240 except ResourceNotFoundError as e :
240- logger .error (f"Resource not found: { str (e )} " )
241- raise HTTPException (status_code = 404 , detail = str (e )) from e
241+ logger .error (
242+ "Resource not found while identifying geographic entities for %s" ,
243+ id ,
244+ exc_info = True ,
245+ )
246+ raise HTTPException (status_code = 404 , detail = "Resource not found" ) from e
242247 except ResourceProcessingError as e :
243- logger .error (f"Resource processing error: { str (e )} " )
244- raise HTTPException (status_code = 500 , detail = str (e )) from e
248+ logger .error (
249+ "Resource processing error while identifying geographic entities for %s" ,
250+ id ,
251+ exc_info = True ,
252+ )
253+ raise HTTPException (
254+ status_code = 500 , detail = "Failed to start geographic entity identification"
255+ ) from e
245256 except Exception as e :
246257 logger .error (
247- f"Unexpected error triggering geographic entity identification "
248- f"for resource { id } : { str (e )} "
258+ "Unexpected error triggering geographic entity identification for resource %s" ,
259+ id ,
260+ exc_info = True ,
249261 )
250- raise HTTPException (status_code = 500 , detail = str (e )) from e
262+ raise HTTPException (
263+ status_code = 500 , detail = "Failed to start geographic entity identification"
264+ ) from e
251265
252266
253267# API Key Management Endpoints
@@ -279,8 +293,8 @@ async def create_api_key(
279293 except HTTPException :
280294 raise
281295 except Exception as e :
282- logger .error (f "Error creating API key: { str ( e ) } " )
283- raise HTTPException (status_code = 500 , detail = str ( e ) ) from e
296+ logger .error ("Error creating API key" , exc_info = True )
297+ raise HTTPException (status_code = 500 , detail = "Failed to create API key" ) from e
284298
285299
286300@router .get ("/api-keys" )
@@ -290,8 +304,8 @@ async def list_api_keys():
290304 keys = await api_key_service .list_api_keys ()
291305 return create_response ({"keys" : keys })
292306 except Exception as e :
293- logger .error (f "Error listing API keys: { str ( e ) } " )
294- raise HTTPException (status_code = 500 , detail = str ( e ) ) from e
307+ logger .error ("Error listing API keys" , exc_info = True )
308+ raise HTTPException (status_code = 500 , detail = "Failed to list API keys" ) from e
295309
296310
297311@router .patch ("/api-keys/{key_id}" )
@@ -329,8 +343,8 @@ async def update_api_key(
329343 except HTTPException :
330344 raise
331345 except Exception as e :
332- logger .error (f "Error updating API key: { str ( e ) } " )
333- raise HTTPException (status_code = 500 , detail = str ( e ) ) from e
346+ logger .error ("Error updating API key" , exc_info = True )
347+ raise HTTPException (status_code = 500 , detail = "Failed to update API key" ) from e
334348
335349
336350@router .delete ("/api-keys/{key_id}" )
@@ -348,8 +362,8 @@ async def revoke_api_key(key_id: int):
348362 except HTTPException :
349363 raise
350364 except Exception as e :
351- logger .error (f "Error revoking API key: { str ( e ) } " )
352- raise HTTPException (status_code = 500 , detail = str ( e ) ) from e
365+ logger .error ("Error revoking API key" , exc_info = True )
366+ raise HTTPException (status_code = 500 , detail = "Failed to revoke API key" ) from e
353367
354368
355369@router .get ("/api-tiers" )
@@ -359,8 +373,8 @@ async def list_api_tiers():
359373 tiers = await api_key_service .list_tiers ()
360374 return create_response ({"tiers" : tiers })
361375 except Exception as e :
362- logger .error (f "Error listing API tiers: { str ( e ) } " )
363- raise HTTPException (status_code = 500 , detail = str ( e ) ) from e
376+ logger .error ("Error listing API tiers" , exc_info = True )
377+ raise HTTPException (status_code = 500 , detail = "Failed to list API tiers" ) from e
364378
365379
366380# --- OpenGeoMetadata (OGM) admin endpoints ---
@@ -818,20 +832,26 @@ async def download_ogm_dump_file(run_id: int, filename: str):
818832 if not run or not run .get ("ogm_dump_dir" ):
819833 raise HTTPException (status_code = 404 , detail = "Dump not found for run" )
820834
835+ try :
836+ safe_filename = require_safe_filename (filename )
837+ except ValueError as exc :
838+ raise HTTPException (status_code = 400 , detail = "Invalid filename" ) from exc
839+
821840 dump_dir = Path (run ["ogm_dump_dir" ])
841+ resolved_dump_dir = dump_dir .resolve ()
822842 # Prevent path traversal
823- candidate = (dump_dir / filename ).resolve ()
824- if dump_dir . resolve () not in candidate .parents and candidate != dump_dir . resolve () :
843+ candidate = (resolved_dump_dir / safe_filename ).resolve ()
844+ if resolved_dump_dir not in candidate .parents :
825845 raise HTTPException (status_code = 400 , detail = "Invalid filename" )
826846
827847 if not candidate .exists () or not candidate .is_file ():
828848 raise HTTPException (status_code = 404 , detail = "File not found" )
829849
830850 # Basic content type mapping
831851 media_type = "application/octet-stream"
832- if filename .endswith (".json" ) or filename .endswith (".ndjson" ):
852+ if safe_filename .endswith (".json" ) or safe_filename .endswith (".ndjson" ):
833853 media_type = "application/json"
834- elif filename .endswith (".parquet" ):
854+ elif safe_filename .endswith (".parquet" ):
835855 media_type = "application/octet-stream"
836856
837- return FileResponse (str (candidate ), media_type = media_type , filename = filename )
857+ return FileResponse (str (candidate ), media_type = media_type , filename = safe_filename )
0 commit comments