Compare commits
3 Commits
259243d44b
...
52f95bd677
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
52f95bd677 | ||
|
|
6701dcd6bf | ||
|
|
9f0a77ca39 |
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@@ -19,9 +19,6 @@
|
|||||||
"notebook.output.textLineLimit": 200,
|
"notebook.output.textLineLimit": 200,
|
||||||
"jupyter.debugJustMyCode": false,
|
"jupyter.debugJustMyCode": false,
|
||||||
"python.testing.pytestEnabled": true,
|
"python.testing.pytestEnabled": true,
|
||||||
"python.testing.pytestArgs": [
|
|
||||||
"tests"
|
|
||||||
],
|
|
||||||
"files.exclude": {
|
"files.exclude": {
|
||||||
"**/*.egg-info": true,
|
"**/*.egg-info": true,
|
||||||
"**/htmlcov": true,
|
"**/htmlcov": true,
|
||||||
|
|||||||
@@ -2,19 +2,28 @@
|
|||||||
# Use annotatedyaml for secrets and environment-specific overrides
|
# Use annotatedyaml for secrets and environment-specific overrides
|
||||||
|
|
||||||
database:
|
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
|
# url: "postgresql://user:password@host:port/dbname" # Example for Postgres
|
||||||
|
|
||||||
|
# AlpineBits Python config
|
||||||
|
# Use annotatedyaml for secrets and environment-specific overrides
|
||||||
|
|
||||||
alpine_bits_auth:
|
alpine_bits_auth:
|
||||||
- hotel_id: "12345"
|
- hotel_id: "39054_001"
|
||||||
hotel_name: "Bemelmans Post"
|
hotel_name: "Bemelmans Post"
|
||||||
username: "alice"
|
username: "bemelman"
|
||||||
password: !secret ALICE_PASSWORD
|
password: !secret BEMELMANS_PASSWORD
|
||||||
push_endpoint:
|
|
||||||
url: "https://example.com/push"
|
|
||||||
token: !secret PUSH_TOKEN_ALICE
|
|
||||||
username: "alice"
|
|
||||||
- hotel_id: "135"
|
- hotel_id: "135"
|
||||||
hotel_name: "Bemelmans"
|
hotel_name: "Testhotel"
|
||||||
username: "sebastian"
|
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"]
|
packages = ["src/alpine_bits_python"]
|
||||||
|
|
||||||
[tool.pytest.ini_options]
|
[tool.pytest.ini_options]
|
||||||
testpaths = ["test"]
|
testpaths = ["tests"]
|
||||||
pythonpath = ["src"]
|
pythonpath = ["src"]
|
||||||
|
|
||||||
[tool.ruff]
|
[tool.ruff]
|
||||||
|
|||||||
@@ -555,6 +555,7 @@ async def handle_xml_upload(
|
|||||||
):
|
):
|
||||||
"""Endpoint for receiving XML files for conversion processing.
|
"""Endpoint for receiving XML files for conversion processing.
|
||||||
Requires basic authentication and saves XML files to log directory.
|
Requires basic authentication and saves XML files to log directory.
|
||||||
|
Supports gzip compression via Content-Encoding header.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# Get the raw body content
|
# Get the raw body content
|
||||||
@@ -565,6 +566,20 @@ async def handle_xml_upload(
|
|||||||
status_code=400, detail="ERROR: No XML content provided"
|
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 to decode as UTF-8
|
||||||
try:
|
try:
|
||||||
xml_content = body.decode("utf-8")
|
xml_content = body.decode("utf-8")
|
||||||
@@ -594,21 +609,20 @@ async def handle_xml_upload(
|
|||||||
|
|
||||||
_LOGGER.info("XML file saved to %s by user %s", log_filename, username)
|
_LOGGER.info("XML file saved to %s by user %s", log_filename, username)
|
||||||
|
|
||||||
return {
|
response_headers = {
|
||||||
"status": "success",
|
"Content-Type": "application/xml; charset=utf-8",
|
||||||
"message": "XML file received and saved",
|
"X-AlpineBits-Server-Accept-Encoding": "gzip",
|
||||||
"filename": log_filename.name,
|
|
||||||
"size_bytes": len(body),
|
|
||||||
"authenticated_user": username,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return Response(
|
||||||
|
content="Xml received", headers=response_headers, status_code=200
|
||||||
|
)
|
||||||
|
|
||||||
except HTTPException:
|
except HTTPException:
|
||||||
raise
|
raise
|
||||||
except Exception as e:
|
except Exception:
|
||||||
_LOGGER.exception("Error in handle_xml_upload")
|
_LOGGER.exception("Error in handle_xml_upload")
|
||||||
raise HTTPException(
|
raise HTTPException(status_code=500, detail="Error processing XML upload")
|
||||||
status_code=500, detail="Error processing XML upload"
|
|
||||||
) from e
|
|
||||||
|
|
||||||
|
|
||||||
# UNUSED
|
# UNUSED
|
||||||
|
|||||||
@@ -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