More cleanup.
This commit is contained in:
@@ -24,10 +24,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,
|
||||
@@ -406,6 +405,7 @@ async def process_wix_form_submission(request: Request, data: dict[str, Any], db
|
||||
|
||||
unique_id = data.get("submissionId", generate_unique_id())
|
||||
|
||||
# TODO MAGIC shortening
|
||||
if len(unique_id) > 32:
|
||||
# strip to first 35 chars
|
||||
unique_id = unique_id[:32]
|
||||
@@ -486,7 +486,7 @@ async def process_wix_form_submission(request: Request, data: dict[str, Any], db
|
||||
await dispatcher.dispatch_for_hotel(
|
||||
"form_processed", hotel_code, db_customer, db_reservation
|
||||
)
|
||||
_LOGGER.info(f"Dispatched form_processed event for hotel {hotel_code}")
|
||||
_LOGGER.info("Dispatched form_processed event for hotel %s", hotel_code)
|
||||
else:
|
||||
_LOGGER.warning(
|
||||
"No hotel_code in reservation, skipping push notifications"
|
||||
@@ -539,6 +539,7 @@ async def handle_wix_form_test(
|
||||
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(
|
||||
@@ -566,6 +567,7 @@ async def validate_basic_auth(
|
||||
credentials: HTTPBasicCredentials = Depends(security_basic),
|
||||
) -> str:
|
||||
"""Validate basic authentication for AlpineBits protocol.
|
||||
|
||||
Returns username if valid, raises HTTPException if not.
|
||||
"""
|
||||
# Accept any username/password pair present in config['alpine_bits_auth']
|
||||
@@ -592,11 +594,13 @@ async def validate_basic_auth(
|
||||
headers={"WWW-Authenticate": "Basic"},
|
||||
)
|
||||
_LOGGER.info(
|
||||
f"AlpineBits authentication successful for user: {credentials.username} (from config)"
|
||||
"AlpineBits authentication successful for user: %s (from config)",
|
||||
credentials.username,
|
||||
)
|
||||
return credentials.username, credentials.password
|
||||
|
||||
|
||||
# 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.
|
||||
This is a simplified parser for the AlpineBits use case.
|
||||
@@ -688,12 +692,12 @@ async def alpinebits_server_handshake(
|
||||
"No X-AlpineBits-ClientProtocolVersion header found, assuming pre-2013-04"
|
||||
)
|
||||
else:
|
||||
_LOGGER.info(f"Client protocol version: {client_protocol_version}")
|
||||
_LOGGER.info("Client protocol version: %s", client_protocol_version)
|
||||
|
||||
# Optional client ID
|
||||
client_id = request.headers.get("X-AlpineBits-ClientID")
|
||||
if client_id:
|
||||
_LOGGER.info(f"Client ID: {client_id}")
|
||||
_LOGGER.info("Client ID: %s", client_id)
|
||||
|
||||
# Check content encoding
|
||||
content_encoding = request.headers.get("Content-Encoding")
|
||||
@@ -705,50 +709,14 @@ async def alpinebits_server_handshake(
|
||||
# Get content type before processing
|
||||
content_type = request.headers.get("Content-Type", "")
|
||||
|
||||
_LOGGER.info(f"Content-Type: {content_type}")
|
||||
_LOGGER.info(f"Content-Encoding: {content_encoding}")
|
||||
_LOGGER.info("Content-Type: %s", content_type)
|
||||
_LOGGER.info("Content-Encoding: %s", content_encoding)
|
||||
|
||||
# Get request body
|
||||
body = await request.body()
|
||||
|
||||
# Decompress if needed
|
||||
if is_compressed:
|
||||
try:
|
||||
body = gzip.decompress(body)
|
||||
|
||||
except Exception:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="ERROR: Failed to decompress gzip content",
|
||||
)
|
||||
|
||||
# Check content type (after decompression)
|
||||
if (
|
||||
"multipart/form-data" not in content_type
|
||||
and "application/x-www-form-urlencoded" not in content_type
|
||||
):
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="ERROR: Content-Type must be multipart/form-data or application/x-www-form-urlencoded",
|
||||
)
|
||||
|
||||
# Parse multipart data
|
||||
if "multipart/form-data" in content_type:
|
||||
try:
|
||||
form_data = parse_multipart_data(content_type, body)
|
||||
except Exception:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="ERROR: Failed to parse multipart/form-data",
|
||||
)
|
||||
elif "application/x-www-form-urlencoded" in content_type:
|
||||
# Parse as urlencoded
|
||||
form_data = dict(urllib.parse.parse_qsl(body.decode("utf-8")))
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="ERROR: Content-Type must be multipart/form-data or application/x-www-form-urlencoded",
|
||||
)
|
||||
form_data = validate_alpinebits_body(is_compressed, content_type, body)
|
||||
|
||||
# Check for required action parameter
|
||||
action = form_data.get("action")
|
||||
@@ -807,6 +775,49 @@ async def alpinebits_server_handshake(
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
|
||||
def validate_alpinebits_body(is_compressed, content_type, body):
|
||||
"""Check if the body conforms to AlpineBits expectations."""
|
||||
if is_compressed:
|
||||
try:
|
||||
body = gzip.decompress(body)
|
||||
|
||||
except Exception:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="ERROR: Failed to decompress gzip content",
|
||||
)
|
||||
|
||||
# Check content type (after decompression)
|
||||
if (
|
||||
"multipart/form-data" not in content_type
|
||||
and "application/x-www-form-urlencoded" not in content_type
|
||||
):
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="ERROR: Content-Type must be multipart/form-data or application/x-www-form-urlencoded",
|
||||
)
|
||||
|
||||
# Parse multipart data
|
||||
if "multipart/form-data" in content_type:
|
||||
try:
|
||||
form_data = parse_multipart_data(content_type, body)
|
||||
except Exception:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="ERROR: Failed to parse multipart/form-data",
|
||||
)
|
||||
elif "application/x-www-form-urlencoded" in content_type:
|
||||
# Parse as urlencoded
|
||||
form_data = dict(urllib.parse.parse_qsl(body.decode("utf-8")))
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="ERROR: Content-Type must be multipart/form-data or application/x-www-form-urlencoded",
|
||||
)
|
||||
|
||||
return form_data
|
||||
|
||||
|
||||
@api_router.get("/admin/stats")
|
||||
@limiter.limit("10/minute")
|
||||
async def get_api_stats(request: Request, admin_key: str = Depends(validate_api_key)):
|
||||
|
||||
Reference in New Issue
Block a user