Compare commits
5 Commits
ff25142f62
...
1.0.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
52f95bd677 | ||
|
|
6701dcd6bf | ||
|
|
9f0a77ca39 | ||
|
|
259243d44b | ||
|
|
84a57f3d98 |
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@@ -19,9 +19,6 @@
|
||||
"notebook.output.textLineLimit": 200,
|
||||
"jupyter.debugJustMyCode": false,
|
||||
"python.testing.pytestEnabled": true,
|
||||
"python.testing.pytestArgs": [
|
||||
"tests"
|
||||
],
|
||||
"files.exclude": {
|
||||
"**/*.egg-info": true,
|
||||
"**/htmlcov": true,
|
||||
|
||||
@@ -2,19 +2,28 @@
|
||||
# Use annotatedyaml for secrets and environment-specific overrides
|
||||
|
||||
database:
|
||||
url: "sqlite+aiosqlite:///alpinebits.db" # For local dev, use SQLite. For prod, override with PostgreSQL URL.
|
||||
url: "sqlite+aiosqlite:///alpinebits.db" # For local dev, use SQLite. For prod, override with PostgreSQL URL.
|
||||
# url: "postgresql://user:password@host:port/dbname" # Example for Postgres
|
||||
|
||||
# AlpineBits Python config
|
||||
# Use annotatedyaml for secrets and environment-specific overrides
|
||||
|
||||
alpine_bits_auth:
|
||||
- hotel_id: "12345"
|
||||
- hotel_id: "39054_001"
|
||||
hotel_name: "Bemelmans Post"
|
||||
username: "alice"
|
||||
password: !secret ALICE_PASSWORD
|
||||
push_endpoint:
|
||||
url: "https://example.com/push"
|
||||
token: !secret PUSH_TOKEN_ALICE
|
||||
username: "alice"
|
||||
username: "bemelman"
|
||||
password: !secret BEMELMANS_PASSWORD
|
||||
- hotel_id: "135"
|
||||
hotel_name: "Bemelmans"
|
||||
hotel_name: "Testhotel"
|
||||
username: "sebastian"
|
||||
password: !secret BOB_PASSWORD
|
||||
password: !secret BOB_PASSWORD
|
||||
|
||||
- hotel_id: "39052_001"
|
||||
hotel_name: "Jagthof Kaltern"
|
||||
username: "jagthof"
|
||||
password: !secret JAGTHOF_PASSWORD
|
||||
|
||||
- hotel_id: "39040_001"
|
||||
hotel_name: "Residence Erika"
|
||||
username: "erika"
|
||||
password: !secret ERIKA_PASSWORD
|
||||
|
||||
@@ -36,7 +36,7 @@ alpine-bits-server = "alpine_bits_python.main:main"
|
||||
packages = ["src/alpine_bits_python"]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = ["test"]
|
||||
testpaths = ["tests"]
|
||||
pythonpath = ["src"]
|
||||
|
||||
[tool.ruff]
|
||||
|
||||
@@ -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,142 @@ 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.
|
||||
Supports gzip compression via Content-Encoding header.
|
||||
"""
|
||||
try:
|
||||
# Get the raw body content
|
||||
body = await request.body()
|
||||
|
||||
if not body:
|
||||
raise HTTPException(
|
||||
status_code=400, detail="ERROR: No XML content provided"
|
||||
)
|
||||
|
||||
# Check if content is gzip compressed
|
||||
content_encoding = request.headers.get("content-encoding", "").lower()
|
||||
is_gzipped = content_encoding == "gzip"
|
||||
|
||||
# Decompress if gzipped
|
||||
if is_gzipped:
|
||||
try:
|
||||
body = gzip.decompress(body)
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"ERROR: Failed to decompress gzip content: {e}",
|
||||
) from e
|
||||
|
||||
# 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)
|
||||
|
||||
response_headers = {
|
||||
"Content-Type": "application/xml; charset=utf-8",
|
||||
"X-AlpineBits-Server-Accept-Encoding": "gzip",
|
||||
}
|
||||
|
||||
return Response(
|
||||
content="Xml received", headers=response_headers, status_code=200
|
||||
)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception:
|
||||
_LOGGER.exception("Error in handle_xml_upload")
|
||||
raise HTTPException(status_code=500, detail="Error processing XML upload")
|
||||
|
||||
|
||||
# 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.
|
||||
|
||||
@@ -45,7 +45,7 @@ class Reservation(Base):
|
||||
id = Column(Integer, primary_key=True)
|
||||
customer_id = Column(Integer, ForeignKey("customers.id"))
|
||||
unique_id = Column(String, unique=True)
|
||||
md5_unique_id = Column(String, unique=True) # max length 35
|
||||
md5_unique_id = Column(String(32), unique=True) # max length 32 guaranteed
|
||||
start_date = Column(Date)
|
||||
end_date = Column(Date)
|
||||
num_adults = Column(Integer)
|
||||
|
||||
@@ -1,131 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Configuration and setup script for the Wix Form Handler API
|
||||
"""
|
||||
|
||||
import os
|
||||
import secrets
|
||||
import sys
|
||||
|
||||
# Add parent directory to path to import from src
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from alpine_bits_python.auth import generate_api_key
|
||||
|
||||
|
||||
def generate_secure_keys():
|
||||
"""Generate secure API keys for the application"""
|
||||
print("🔐 Generating Secure API Keys")
|
||||
print("=" * 50)
|
||||
|
||||
# Generate API keys
|
||||
wix_api_key = generate_api_key()
|
||||
admin_api_key = generate_api_key()
|
||||
webhook_secret = secrets.token_urlsafe(32)
|
||||
|
||||
print(f"🔑 Wix Webhook API Key: {wix_api_key}")
|
||||
print(f"🔐 Admin API Key: {admin_api_key}")
|
||||
print(f"🔒 Webhook Secret: {webhook_secret}")
|
||||
|
||||
print("\n📋 Environment Variables")
|
||||
print("-" * 30)
|
||||
print(f"export WIX_API_KEY='{wix_api_key}'")
|
||||
print(f"export ADMIN_API_KEY='{admin_api_key}'")
|
||||
print(f"export WIX_WEBHOOK_SECRET='{webhook_secret}'")
|
||||
print("export REDIS_URL='redis://localhost:6379' # Optional for production")
|
||||
|
||||
print("\n🔧 .env File Content")
|
||||
print("-" * 20)
|
||||
print(f"WIX_API_KEY={wix_api_key}")
|
||||
print(f"ADMIN_API_KEY={admin_api_key}")
|
||||
print(f"WIX_WEBHOOK_SECRET={webhook_secret}")
|
||||
print("REDIS_URL=redis://localhost:6379")
|
||||
|
||||
# Optionally write to .env file
|
||||
create_env = input("\n❓ Create .env file? (y/n): ").lower().strip()
|
||||
if create_env == "y":
|
||||
# Create .env in the project root (two levels up from scripts)
|
||||
env_path = os.path.join(
|
||||
os.path.dirname(os.path.dirname(os.path.dirname(__file__))), ".env"
|
||||
)
|
||||
with open(env_path, "w") as f:
|
||||
f.write(f"WIX_API_KEY={wix_api_key}\n")
|
||||
f.write(f"ADMIN_API_KEY={admin_api_key}\n")
|
||||
f.write(f"WIX_WEBHOOK_SECRET={webhook_secret}\n")
|
||||
f.write("REDIS_URL=redis://localhost:6379\n")
|
||||
print(f"✅ .env file created at {env_path}!")
|
||||
print("⚠️ Add .env to your .gitignore file!")
|
||||
|
||||
print("\n🌐 Wix Configuration")
|
||||
print("-" * 20)
|
||||
print("1. In your Wix site, go to Settings > Webhooks")
|
||||
print("2. Add webhook URL: https://yourdomain.com/webhook/wix-form")
|
||||
print("3. Add custom header: Authorization: Bearer " + wix_api_key)
|
||||
print("4. Optionally configure webhook signature with the secret above")
|
||||
|
||||
return {
|
||||
"wix_api_key": wix_api_key,
|
||||
"admin_api_key": admin_api_key,
|
||||
"webhook_secret": webhook_secret,
|
||||
}
|
||||
|
||||
|
||||
def check_security_setup():
|
||||
"""Check current security configuration"""
|
||||
print("🔍 Security Configuration Check")
|
||||
print("=" * 40)
|
||||
|
||||
# Check environment variables
|
||||
wix_key = os.getenv("WIX_API_KEY")
|
||||
admin_key = os.getenv("ADMIN_API_KEY")
|
||||
webhook_secret = os.getenv("WIX_WEBHOOK_SECRET")
|
||||
redis_url = os.getenv("REDIS_URL")
|
||||
|
||||
print("Environment Variables:")
|
||||
print(f" WIX_API_KEY: {'✅ Set' if wix_key else '❌ Not set'}")
|
||||
print(f" ADMIN_API_KEY: {'✅ Set' if admin_key else '❌ Not set'}")
|
||||
print(f" WIX_WEBHOOK_SECRET: {'✅ Set' if webhook_secret else '❌ Not set'}")
|
||||
print(f" REDIS_URL: {'✅ Set' if redis_url else '⚠️ Optional (using in-memory)'}")
|
||||
|
||||
# Security recommendations
|
||||
print("\n🛡️ Security Recommendations:")
|
||||
if not wix_key:
|
||||
print(" ❌ Set WIX_API_KEY environment variable")
|
||||
elif len(wix_key) < 32:
|
||||
print(" ⚠️ WIX_API_KEY should be longer for better security")
|
||||
else:
|
||||
print(" ✅ WIX_API_KEY looks secure")
|
||||
|
||||
if not admin_key:
|
||||
print(" ❌ Set ADMIN_API_KEY environment variable")
|
||||
elif wix_key and admin_key == wix_key:
|
||||
print(" ❌ Admin and Wix keys should be different")
|
||||
else:
|
||||
print(" ✅ ADMIN_API_KEY configured")
|
||||
|
||||
if not webhook_secret:
|
||||
print(" ⚠️ Consider setting WIX_WEBHOOK_SECRET for signature validation")
|
||||
else:
|
||||
print(" ✅ Webhook signature validation enabled")
|
||||
|
||||
print("\n🚀 Production Checklist:")
|
||||
print(" - Use HTTPS in production")
|
||||
print(" - Set up Redis for distributed rate limiting")
|
||||
print(" - Configure proper CORS origins")
|
||||
print(" - Set up monitoring and logging")
|
||||
print(" - Regular key rotation")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("🔐 Wix Form Handler API - Security Setup")
|
||||
print("=" * 50)
|
||||
|
||||
choice = input(
|
||||
"Choose an option:\n1. Generate new API keys\n2. Check current setup\n\nEnter choice (1 or 2): "
|
||||
).strip()
|
||||
|
||||
if choice == "1":
|
||||
generate_secure_keys()
|
||||
elif choice == "2":
|
||||
check_security_setup()
|
||||
else:
|
||||
print("Invalid choice. Please run again and choose 1 or 2.")
|
||||
@@ -1,219 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Test script for the Secure Wix Form Handler API
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
from datetime import datetime
|
||||
|
||||
import aiohttp
|
||||
|
||||
# Add parent directory to path to import from src
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
# API Configuration
|
||||
BASE_URL = "http://localhost:8000"
|
||||
|
||||
# API Keys for testing - replace with your actual keys
|
||||
TEST_API_KEY = os.getenv("WIX_API_KEY", "sk_live_your_secure_api_key_here")
|
||||
ADMIN_API_KEY = os.getenv("ADMIN_API_KEY", "sk_admin_your_admin_key_here")
|
||||
|
||||
# Sample Wix form data based on your example
|
||||
SAMPLE_WIX_DATA = {
|
||||
"formName": "Contact Form",
|
||||
"submissions": [],
|
||||
"submissionTime": "2024-03-20T10:30:00+00:00",
|
||||
"formFieldMask": ["email", "name", "phone"],
|
||||
"submissionId": "test-submission-123",
|
||||
"contactId": "test-contact-456",
|
||||
"submissionsLink": "https://www.wix.app/forms/test-form/submissions",
|
||||
"submissionPdf": {
|
||||
"url": "https://example.com/submission.pdf",
|
||||
"filename": "submission.pdf",
|
||||
},
|
||||
"formId": "test-form-789",
|
||||
"field:email_5139": "test@example.com",
|
||||
"field:first_name_abae": "John",
|
||||
"field:last_name_d97c": "Doe",
|
||||
"field:phone_4c77": "+1234567890",
|
||||
"field:anrede": "Herr",
|
||||
"field:anzahl_kinder": "2",
|
||||
"field:alter_kind_3": "8",
|
||||
"field:alter_kind_4": "12",
|
||||
"field:long_answer_3524": "This is a long answer field with more details about the inquiry.",
|
||||
"contact": {
|
||||
"name": {"first": "John", "last": "Doe"},
|
||||
"email": "test@example.com",
|
||||
"locale": "de",
|
||||
"company": "Test Company",
|
||||
"birthdate": "1985-05-15",
|
||||
"labelKeys": {},
|
||||
"contactId": "test-contact-456",
|
||||
"address": {
|
||||
"street": "Test Street 123",
|
||||
"city": "Test City",
|
||||
"country": "Germany",
|
||||
"postalCode": "12345",
|
||||
},
|
||||
"jobTitle": "Manager",
|
||||
"phone": "+1234567890",
|
||||
"createdDate": "2024-03-20T10:00:00.000Z",
|
||||
"updatedDate": "2024-03-20T10:30:00.000Z",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
async def test_api():
|
||||
"""Test the API endpoints with authentication"""
|
||||
headers_with_auth = {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {TEST_API_KEY}",
|
||||
}
|
||||
|
||||
admin_headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {ADMIN_API_KEY}",
|
||||
}
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
# Test health endpoint (no auth required)
|
||||
print("1. Testing health endpoint (no auth)...")
|
||||
try:
|
||||
async with session.get(f"{BASE_URL}/api/health") as response:
|
||||
result = await response.json()
|
||||
print(f" ✅ Health check: {response.status} - {result.get('status')}")
|
||||
except Exception as e:
|
||||
print(f" ❌ Health check failed: {e}")
|
||||
|
||||
# Test root endpoint (no auth required)
|
||||
print("\n2. Testing root endpoint (no auth)...")
|
||||
try:
|
||||
async with session.get(f"{BASE_URL}/api/") as response:
|
||||
result = await response.json()
|
||||
print(f" ✅ Root: {response.status} - {result.get('message')}")
|
||||
except Exception as e:
|
||||
print(f" ❌ Root endpoint failed: {e}")
|
||||
|
||||
# Test webhook endpoint without auth (should fail)
|
||||
print("\n3. Testing webhook endpoint WITHOUT auth (should fail)...")
|
||||
try:
|
||||
async with session.post(
|
||||
f"{BASE_URL}/api/webhook/wix-form",
|
||||
json=SAMPLE_WIX_DATA,
|
||||
headers={"Content-Type": "application/json"},
|
||||
) as response:
|
||||
result = await response.json()
|
||||
if response.status == 401:
|
||||
print(
|
||||
f" ✅ Correctly rejected: {response.status} - {result.get('detail')}"
|
||||
)
|
||||
else:
|
||||
print(f" ❌ Unexpected response: {response.status} - {result}")
|
||||
except Exception as e:
|
||||
print(f" ❌ Test failed: {e}")
|
||||
|
||||
# Test webhook endpoint with valid auth
|
||||
print("\n4. Testing webhook endpoint WITH valid auth...")
|
||||
try:
|
||||
async with session.post(
|
||||
f"{BASE_URL}/api/webhook/wix-form",
|
||||
json=SAMPLE_WIX_DATA,
|
||||
headers=headers_with_auth,
|
||||
) as response:
|
||||
result = await response.json()
|
||||
if response.status == 200:
|
||||
print(
|
||||
f" ✅ Webhook success: {response.status} - {result.get('status')}"
|
||||
)
|
||||
else:
|
||||
print(f" ❌ Webhook failed: {response.status} - {result}")
|
||||
except Exception as e:
|
||||
print(f" ❌ Webhook test failed: {e}")
|
||||
|
||||
# Test test endpoint with auth
|
||||
print("\n5. Testing simple test endpoint WITH auth...")
|
||||
try:
|
||||
async with session.post(
|
||||
f"{BASE_URL}/api/webhook/wix-form/test",
|
||||
json={"test": "data", "timestamp": datetime.now().isoformat()},
|
||||
headers=headers_with_auth,
|
||||
) as response:
|
||||
result = await response.json()
|
||||
if response.status == 200:
|
||||
print(
|
||||
f" ✅ Test endpoint: {response.status} - {result.get('status')}"
|
||||
)
|
||||
else:
|
||||
print(f" ❌ Test endpoint failed: {response.status} - {result}")
|
||||
except Exception as e:
|
||||
print(f" ❌ Test endpoint failed: {e}")
|
||||
|
||||
# Test rate limiting by making multiple rapid requests
|
||||
print("\n6. Testing rate limiting (making 5 rapid requests)...")
|
||||
rate_limit_test_count = 0
|
||||
for i in range(5):
|
||||
try:
|
||||
async with session.get(f"{BASE_URL}/api/health") as response:
|
||||
if response.status == 200:
|
||||
rate_limit_test_count += 1
|
||||
elif response.status == 429:
|
||||
print(f" ✅ Rate limit triggered on request {i + 1}")
|
||||
break
|
||||
except Exception as e:
|
||||
print(f" ❌ Rate limit test failed: {e}")
|
||||
break
|
||||
|
||||
if rate_limit_test_count == 5:
|
||||
print(" ℹ️ No rate limit reached (normal for low request volume)")
|
||||
|
||||
# Test admin endpoint (if admin key is configured)
|
||||
print("\n7. Testing admin stats endpoint...")
|
||||
try:
|
||||
async with session.get(
|
||||
f"{BASE_URL}/api/admin/stats", headers=admin_headers
|
||||
) as response:
|
||||
result = await response.json()
|
||||
if response.status == 200:
|
||||
print(
|
||||
f" ✅ Admin stats: {response.status} - {result.get('status')}"
|
||||
)
|
||||
elif response.status == 401:
|
||||
print(
|
||||
f" ⚠️ Admin access denied (API key not configured): {result.get('detail')}"
|
||||
)
|
||||
else:
|
||||
print(f" ❌ Admin endpoint failed: {response.status} - {result}")
|
||||
except Exception as e:
|
||||
print(f" ❌ Admin test failed: {e}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("🔒 Testing Secure Wix Form Handler API...")
|
||||
print("=" * 60)
|
||||
print("📍 API URL:", BASE_URL)
|
||||
print(
|
||||
"🔑 Using API Key:",
|
||||
TEST_API_KEY[:20] + "..." if len(TEST_API_KEY) > 20 else TEST_API_KEY,
|
||||
)
|
||||
print(
|
||||
"🔐 Using Admin Key:",
|
||||
ADMIN_API_KEY[:20] + "..." if len(ADMIN_API_KEY) > 20 else ADMIN_API_KEY,
|
||||
)
|
||||
print("=" * 60)
|
||||
print("Make sure the API is running with: python3 run_api.py")
|
||||
print("-" * 60)
|
||||
|
||||
try:
|
||||
asyncio.run(test_api())
|
||||
print("\n" + "=" * 60)
|
||||
print("✅ Testing completed!")
|
||||
print("\n📋 Quick Setup Reminder:")
|
||||
print("1. Set environment variables:")
|
||||
print(" export WIX_API_KEY='your_secure_api_key'")
|
||||
print(" export ADMIN_API_KEY='your_admin_key'")
|
||||
print("2. Configure Wix webhook URL: https://yourdomain.com/webhook/wix-form")
|
||||
print("3. Add Authorization header: Bearer your_api_key")
|
||||
except Exception as e:
|
||||
print(f"\n❌ Error testing API: {e}")
|
||||
print("Make sure the API server is running!")
|
||||
Reference in New Issue
Block a user