Initial commit - Customer Portal for Coolify

This commit is contained in:
2025-12-17 10:08:34 +01:00
commit 9fca32567c
153 changed files with 16432 additions and 0 deletions

147
customer_portal/services/token.py Executable file
View File

@@ -0,0 +1,147 @@
"""Token service for secure form pre-filling.
Generates signed tokens for cross-system communication between
Customer Portal and WordPress kurs-booking plugin.
Sprint 6.6: Extended to include all custom_fields for dynamic sync.
"""
import base64
import hashlib
import hmac
import json
import time
from typing import Any
from flask import current_app
class TokenService:
"""Generate and validate signed tokens for cross-system communication."""
@staticmethod
def generate_prefill_token(customer: Any) -> str:
"""
Generate signed token for WordPress form pre-fill.
The token contains all customer data (core fields + custom_fields)
signed with the shared WP_API_SECRET. WordPress validates the
signature and uses the data to pre-fill the booking form.
Sprint 6.6: Now includes all custom_fields for complete sync.
Args:
customer: Customer model instance with id, name, email, phone, custom_fields
Returns:
Base64-URL-safe encoded signed token string
Raises:
ValueError: If WP_API_SECRET is not configured
"""
secret = current_app.config.get("WP_API_SECRET", "")
expiry = current_app.config.get("PREFILL_TOKEN_EXPIRY", 300)
if not secret:
raise ValueError("WP_API_SECRET not configured")
# Sprint 12: Use display properties - all data from custom_fields
addr = customer.display_address
payload = {
"customer_id": customer.id,
"name": customer.display_name,
"email": customer.email or "",
"phone": customer.display_phone,
"address_street": addr.get("street", ""),
"address_city": addr.get("city", ""),
"address_zip": addr.get("zip", ""),
"exp": int(time.time()) + expiry,
}
# Add all custom_fields to payload for full data access
custom_fields = customer.get_custom_fields()
if custom_fields:
payload["custom_fields"] = custom_fields
# JSON encode with minimal whitespace.
payload_json = json.dumps(payload, separators=(",", ":"), ensure_ascii=False)
# Create HMAC-SHA256 signature.
signature = hmac.new(
secret.encode("utf-8"),
payload_json.encode("utf-8"),
hashlib.sha256,
).hexdigest()
# Combine payload and signature.
token_data = f"{payload_json}.{signature}"
# Base64 URL-safe encode.
return base64.urlsafe_b64encode(token_data.encode("utf-8")).decode("utf-8")
@staticmethod
def validate_prefill_token(token: str) -> dict | None:
"""
Validate a prefill token and extract customer data.
This method is primarily for testing purposes. The actual validation
happens in WordPress (class-frontend.php).
Args:
token: Base64-URL-safe encoded signed token
Returns:
Dictionary with customer data if valid, None otherwise
"""
secret = current_app.config.get("WP_API_SECRET", "")
if not secret:
return None
try:
# Decode base64.
decoded = base64.urlsafe_b64decode(token.encode("utf-8")).decode("utf-8")
# Split payload and signature.
parts = decoded.split(".", 1)
if len(parts) != 2:
return None
payload_json, signature = parts
# Verify signature.
expected_sig = hmac.new(
secret.encode("utf-8"),
payload_json.encode("utf-8"),
hashlib.sha256,
).hexdigest()
if not hmac.compare_digest(expected_sig, signature):
return None
# Parse payload.
payload = json.loads(payload_json)
# Check expiration.
if payload.get("exp", 0) < time.time():
return None
# Sprint 6.6: Return all fields including custom_fields
result = {
"customer_id": payload.get("customer_id"),
"name": payload.get("name", ""),
"email": payload.get("email", ""),
"phone": payload.get("phone", ""),
"address_street": payload.get("address_street", ""),
"address_city": payload.get("address_city", ""),
"address_zip": payload.get("address_zip", ""),
}
# Include custom_fields if present
if "custom_fields" in payload:
result["custom_fields"] = payload["custom_fields"]
return result
except (ValueError, json.JSONDecodeError, UnicodeDecodeError):
return None