diff --git a/src/alpine_bits_python/api.py b/src/alpine_bits_python/api.py index 5113c3f..f68bf09 100644 --- a/src/alpine_bits_python/api.py +++ b/src/alpine_bits_python/api.py @@ -7,6 +7,7 @@ import urllib.parse from collections import defaultdict from datetime import UTC, date, datetime from functools import partial +from pathlib import Path from typing import Any import httpx @@ -27,10 +28,9 @@ from .alpinebits_server import ( ) from .auth import generate_api_key, generate_unique_id, validate_api_key from .config_loader import load_config -from .db import Base +from .db import Base, get_database_url from .db import Customer as DBCustomer from .db import Reservation as DBReservation -from .db import get_database_url from .rate_limit import ( BURST_RATE_LIMIT, DEFAULT_RATE_LIMIT, @@ -476,65 +476,6 @@ async def process_wix_form_submission(request: Request, data: dict[str, Any], db } -@api_router.post("/webhook/wix-form") -@webhook_limiter.limit(WEBHOOK_RATE_LIMIT) -async def handle_wix_form( - request: Request, data: dict[str, Any], db_session=Depends(get_async_session) -): - """Unified endpoint to handle Wix form submissions (test and production). - No authentication required for this endpoint. - """ - try: - return await process_wix_form_submission(request, data, db_session) - except Exception as e: - _LOGGER.error(f"Error in handle_wix_form: {e!s}") - # log stacktrace - import traceback - - traceback_str = traceback.format_exc() - _LOGGER.error(f"Stack trace for handle_wix_form: {traceback_str}") - raise HTTPException(status_code=500, detail="Error processing Wix form data") - - -@api_router.post("/webhook/wix-form/test") -@limiter.limit(DEFAULT_RATE_LIMIT) -async def handle_wix_form_test( - request: Request, data: dict[str, Any], db_session=Depends(get_async_session) -): - """Test endpoint to verify the API is working with raw JSON data. - No authentication required for testing purposes. - """ - try: - return await process_wix_form_submission(request, data, db_session) - except Exception as e: - _LOGGER.error(f"Error in handle_wix_form_test: {e!s}") - raise HTTPException(status_code=500, detail="Error processing test data") - - -# UNUSED -@api_router.post("/admin/generate-api-key") -@limiter.limit("5/hour") # Very restrictive for admin operations -async def generate_new_api_key( - request: Request, admin_key: str = Depends(validate_api_key) -): - """Admin endpoint to generate new API keys. - Requires admin API key and is heavily rate limited. - """ - if admin_key != "admin-key": - raise HTTPException(status_code=403, detail="Admin access required") - - new_key = generate_api_key() - _LOGGER.info(f"Generated new API key (requested by: {admin_key})") - - return { - "status": "success", - "message": "New API key generated", - "api_key": new_key, - "timestamp": datetime.now().isoformat(), - "note": "Store this key securely - it won't be shown again", - } - - async def validate_basic_auth( credentials: HTTPBasicCredentials = Depends(security_basic), ) -> str: @@ -572,6 +513,128 @@ async def validate_basic_auth( return credentials.username, credentials.password +@api_router.post("/webhook/wix-form") +@webhook_limiter.limit(WEBHOOK_RATE_LIMIT) +async def handle_wix_form( + request: Request, data: dict[str, Any], db_session=Depends(get_async_session) +): + """Unified endpoint to handle Wix form submissions (test and production). + No authentication required for this endpoint. + """ + try: + return await process_wix_form_submission(request, data, db_session) + except Exception as e: + _LOGGER.error(f"Error in handle_wix_form: {e!s}") + # log stacktrace + import traceback + + traceback_str = traceback.format_exc() + _LOGGER.error(f"Stack trace for handle_wix_form: {traceback_str}") + raise HTTPException(status_code=500, detail="Error processing Wix form data") + + +@api_router.post("/webhook/wix-form/test") +@limiter.limit(DEFAULT_RATE_LIMIT) +async def handle_wix_form_test( + request: Request, data: dict[str, Any], db_session=Depends(get_async_session) +): + """Test endpoint to verify the API is working with raw JSON data. + No authentication required for testing purposes. + """ + try: + return await process_wix_form_submission(request, data, db_session) + except Exception as e: + _LOGGER.error(f"Error in handle_wix_form_test: {e!s}") + raise HTTPException(status_code=500, detail="Error processing test data") + + +@api_router.post("/hoteldata/conversions_import") +@limiter.limit(DEFAULT_RATE_LIMIT) +async def handle_xml_upload( + request: Request, credentials_tupel: tuple = Depends(validate_basic_auth) +): + """Endpoint for receiving XML files for conversion processing. + Requires basic authentication and saves XML files to log directory. + """ + try: + # Get the raw body content + body = await request.body() + + if not body: + raise HTTPException( + status_code=400, detail="ERROR: No XML content provided" + ) + + # Try to decode as UTF-8 + try: + xml_content = body.decode("utf-8") + except UnicodeDecodeError: + # If UTF-8 fails, try with latin-1 as fallback + xml_content = body.decode("latin-1") + + # Basic validation that it's XML-like + if not xml_content.strip().startswith("<"): + raise HTTPException( + status_code=400, detail="ERROR: Content does not appear to be XML" + ) + + # Create logs directory for XML conversions + logs_dir = Path("logs/conversions_import") + if not logs_dir.exists(): + logs_dir.mkdir(parents=True, mode=0o755, exist_ok=True) + _LOGGER.info("Created directory: %s", logs_dir) + + # Generate filename with timestamp and authenticated user + username, _ = credentials_tupel + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + log_filename = logs_dir / f"xml_import_{username}_{timestamp}.xml" + + # Save XML content to file + log_filename.write_text(xml_content, encoding="utf-8") + + _LOGGER.info("XML file saved to %s by user %s", log_filename, username) + + return { + "status": "success", + "message": "XML file received and saved", + "filename": log_filename.name, + "size_bytes": len(body), + "authenticated_user": username, + } + + except HTTPException: + raise + except Exception as e: + _LOGGER.exception("Error in handle_xml_upload") + raise HTTPException( + status_code=500, detail="Error processing XML upload" + ) from e + + +# UNUSED +@api_router.post("/admin/generate-api-key") +@limiter.limit("5/hour") # Very restrictive for admin operations +async def generate_new_api_key( + request: Request, admin_key: str = Depends(validate_api_key) +): + """Admin endpoint to generate new API keys. + Requires admin API key and is heavily rate limited. + """ + if admin_key != "admin-key": + raise HTTPException(status_code=403, detail="Admin access required") + + new_key = generate_api_key() + _LOGGER.info(f"Generated new API key (requested by: {admin_key})") + + return { + "status": "success", + "message": "New API key generated", + "api_key": new_key, + "timestamp": datetime.now().isoformat(), + "note": "Store this key securely - it won't be shown again", + } + + # TODO Bit sketchy. May need requests-toolkit in the future def parse_multipart_data(content_type: str, body: bytes) -> dict[str, Any]: """Parse multipart/form-data from raw request body.