- Fix Google Wallet class creation: add required issuerName field (merchant name), programLogo with default logo fallback, hexBackgroundColor default - Add default loyalty logo assets (200px + 512px) for programs without custom logos - Smart retry: skip retries on 400/401/403/404 client errors (not transient) - Fix enrollment success page: use sessionStorage for wallet URLs instead of authenticated API call (self-enrolled customers have no session) - Hide wallet section on success page when no wallet URLs available - Wire up T&C modal on enrollment page with program.terms_text - Add startup validation for Google/Apple Wallet configs in lifespan - Add admin wallet status dashboard endpoint and UI (moved to service layer) - Fix Apple Wallet push notifications with real APNs HTTP/2 implementation - Fix docs: correct enrollment URLs (port, path segments, /v1 prefix) - Fix test assertion: !loyalty-enroll! → !enrollment! Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
392 lines
14 KiB
Python
392 lines
14 KiB
Python
# app/core/config.py
|
|
"""
|
|
Application configuration using Pydantic Settings.
|
|
|
|
This module provides classes and functions for:
|
|
- Configuration management via environment variables
|
|
- Database settings
|
|
- JWT and authentication configuration
|
|
- Platform domain and multi-tenancy settings
|
|
- Admin initialization settings
|
|
|
|
Note: Environment detection is handled by app.core.environment module.
|
|
This module focuses purely on configuration storage and validation.
|
|
"""
|
|
|
|
from pydantic_settings import BaseSettings
|
|
|
|
|
|
class Settings(BaseSettings):
|
|
"""
|
|
Settings class for application configuration.
|
|
|
|
Environment detection is delegated to app.core.environment.
|
|
This class focuses on configuration values only.
|
|
"""
|
|
|
|
# =============================================================================
|
|
# PROJECT INFORMATION
|
|
# =============================================================================
|
|
project_name: str = "Orion - Multi-Store Marketplace Platform"
|
|
version: str = "2.2.0"
|
|
|
|
# Clean description without HTML
|
|
description: str = """
|
|
Marketplace product import and management system with multi-store support.
|
|
|
|
**Features:**
|
|
- JWT Authentication with role-based access
|
|
- Multi-marketplace product import (CSV processing)
|
|
- Inventory management across multiple locations
|
|
- Store management with individual configurations
|
|
|
|
**Documentation:** Visit /documentation for complete guides
|
|
**API Testing:** Use /docs for interactive API exploration
|
|
"""
|
|
|
|
# =============================================================================
|
|
# DATABASE (PostgreSQL only)
|
|
# =============================================================================
|
|
database_url: str = "postgresql://orion_user:secure_password@localhost:5432/orion_db"
|
|
|
|
# =============================================================================
|
|
# ADMIN INITIALIZATION (for init_production.py)
|
|
# =============================================================================
|
|
admin_email: str = "admin@orion.lu"
|
|
admin_username: str = "admin"
|
|
admin_password: str = "admin123" # CHANGE IN PRODUCTION!
|
|
admin_first_name: str = "Platform"
|
|
admin_last_name: str = "Administrator"
|
|
|
|
# =============================================================================
|
|
# JWT AUTHENTICATION
|
|
# =============================================================================
|
|
jwt_secret_key: str = "change-this-in-production"
|
|
jwt_expire_hours: int = 24
|
|
jwt_expire_minutes: int = 30
|
|
|
|
# =============================================================================
|
|
# API SERVER
|
|
# =============================================================================
|
|
api_host: str = "0.0.0.0"
|
|
api_port: int = 8000
|
|
debug: bool = True
|
|
|
|
# =============================================================================
|
|
# DOCUMENTATION
|
|
# =============================================================================
|
|
documentation_url: str = "http://localhost:8001"
|
|
|
|
# =============================================================================
|
|
# MIDDLEWARE & SECURITY
|
|
# =============================================================================
|
|
allowed_hosts: list[str] = ["*"] # Configure for production
|
|
|
|
# Rate Limiting
|
|
rate_limit_enabled: bool = True
|
|
rate_limit_requests: int = 100
|
|
rate_limit_window: int = 3600
|
|
|
|
# =============================================================================
|
|
# LOGGING
|
|
# =============================================================================
|
|
log_level: str = "INFO"
|
|
log_file: str | None = None
|
|
|
|
# =============================================================================
|
|
# PLATFORM DOMAIN CONFIGURATION
|
|
# =============================================================================
|
|
platform_domain: str = "wizard.lu"
|
|
|
|
# Custom domain features
|
|
allow_custom_domains: bool = True
|
|
require_domain_verification: bool = True
|
|
|
|
# SSL/TLS configuration for custom domains
|
|
ssl_provider: str = "letsencrypt" # or "cloudflare", "manual"
|
|
auto_provision_ssl: bool = False
|
|
|
|
# DNS verification
|
|
dns_verification_prefix: str = "_orion-verify"
|
|
dns_verification_ttl: int = 3600
|
|
|
|
# =============================================================================
|
|
# PLATFORM LIMITS
|
|
# =============================================================================
|
|
max_stores_per_user: int = 5
|
|
max_team_members_per_store: int = 50
|
|
invitation_expiry_days: int = 7
|
|
|
|
# =============================================================================
|
|
# STRIPE BILLING
|
|
# =============================================================================
|
|
stripe_secret_key: str = ""
|
|
stripe_publishable_key: str = ""
|
|
stripe_webhook_secret: str = ""
|
|
stripe_trial_days: int = 30 # 1-month free trial (card collected upfront but not charged)
|
|
|
|
# =============================================================================
|
|
# EMAIL CONFIGURATION
|
|
# =============================================================================
|
|
# Provider: smtp, sendgrid, mailgun, ses
|
|
email_provider: str = "smtp"
|
|
email_from_address: str = "noreply@orion.lu"
|
|
email_from_name: str = "Orion"
|
|
email_reply_to: str = "" # Optional reply-to address
|
|
|
|
# SMTP Settings (used when email_provider=smtp)
|
|
smtp_host: str = "localhost"
|
|
smtp_port: int = 587
|
|
smtp_user: str = ""
|
|
smtp_password: str = ""
|
|
smtp_use_tls: bool = True
|
|
smtp_use_ssl: bool = False # For port 465
|
|
|
|
# SendGrid (used when email_provider=sendgrid)
|
|
sendgrid_api_key: str = ""
|
|
|
|
# Mailgun (used when email_provider=mailgun)
|
|
mailgun_api_key: str = ""
|
|
mailgun_domain: str = ""
|
|
|
|
# Amazon SES (used when email_provider=ses)
|
|
aws_access_key_id: str = ""
|
|
aws_secret_access_key: str = ""
|
|
aws_region: str = "eu-west-1"
|
|
|
|
# Email behavior
|
|
email_enabled: bool = True # Set to False to disable all emails
|
|
email_debug: bool = False # Log emails instead of sending (for development)
|
|
|
|
# =============================================================================
|
|
# STOREFRONT DEFAULTS
|
|
# =============================================================================
|
|
# These can be overridden by AdminSetting in the database
|
|
default_storefront_locale: str = "fr-LU" # Currency/number formatting locale
|
|
default_currency: str = "EUR" # Default currency code
|
|
|
|
# =============================================================================
|
|
# DEMO/SEED DATA CONFIGURATION
|
|
# =============================================================================
|
|
# Controls for demo data seeding
|
|
seed_demo_stores: int = 3 # Number of demo stores to create
|
|
seed_customers_per_store: int = 15 # Customers per store
|
|
seed_products_per_store: int = 20 # Products per store
|
|
seed_orders_per_store: int = 10 # Orders per store
|
|
|
|
# =============================================================================
|
|
# CELERY / REDIS TASK QUEUE
|
|
# =============================================================================
|
|
# Redis URL for Celery broker and result backend
|
|
redis_url: str = "redis://localhost:6379/0"
|
|
|
|
# Feature flag: enable Celery for background tasks (False = use FastAPI BackgroundTasks)
|
|
use_celery: bool = False
|
|
|
|
# Flower monitoring dashboard
|
|
flower_url: str = "http://localhost:5555"
|
|
flower_password: str = "changeme" # CHANGE IN PRODUCTION!
|
|
|
|
# =============================================================================
|
|
# SENTRY ERROR TRACKING
|
|
# =============================================================================
|
|
sentry_dsn: str | None = None # Set to enable Sentry
|
|
sentry_environment: str = "development" # development, staging, production
|
|
sentry_traces_sample_rate: float = 0.1 # 10% of transactions for performance monitoring
|
|
|
|
# =============================================================================
|
|
# MONITORING
|
|
# =============================================================================
|
|
enable_metrics: bool = False
|
|
grafana_url: str = "https://grafana.wizard.lu"
|
|
grafana_admin_user: str = "admin"
|
|
grafana_admin_password: str = ""
|
|
|
|
# =============================================================================
|
|
# CLOUDFLARE R2 STORAGE
|
|
# =============================================================================
|
|
storage_backend: str = "local" # "local" or "r2"
|
|
r2_account_id: str | None = None
|
|
r2_access_key_id: str | None = None
|
|
r2_secret_access_key: str | None = None
|
|
r2_bucket_name: str = "orion-media"
|
|
r2_public_url: str | None = None # Custom domain for public access (e.g., https://media.yoursite.com)
|
|
|
|
# =============================================================================
|
|
# CLOUDFLARE CDN / PROXY
|
|
# =============================================================================
|
|
cloudflare_enabled: bool = False # Set to True when using CloudFlare proxy
|
|
|
|
# =============================================================================
|
|
# GOOGLE WALLET (LOYALTY MODULE)
|
|
# =============================================================================
|
|
loyalty_google_issuer_id: str | None = None
|
|
loyalty_google_service_account_json: str | None = None # Path to service account JSON
|
|
loyalty_google_wallet_origins: list[str] = [] # Allowed origins for save-to-wallet JWT
|
|
loyalty_default_logo_url: str = "https://rewardflow.lu/static/modules/loyalty/shared/img/default-logo-200.png"
|
|
|
|
# =============================================================================
|
|
# APPLE WALLET (LOYALTY MODULE)
|
|
# =============================================================================
|
|
loyalty_apple_pass_type_id: str | None = None
|
|
loyalty_apple_team_id: str | None = None
|
|
loyalty_apple_wwdr_cert_path: str | None = None
|
|
loyalty_apple_signer_cert_path: str | None = None
|
|
loyalty_apple_signer_key_path: str | None = None
|
|
|
|
model_config = {"env_file": ".env"}
|
|
|
|
|
|
# Singleton settings instance
|
|
settings = Settings()
|
|
|
|
# =============================================================================
|
|
# ENVIRONMENT UTILITIES - Module-level functions
|
|
# =============================================================================
|
|
# Import environment detection utilities
|
|
from app.core.environment import (
|
|
get_environment,
|
|
is_development,
|
|
is_production,
|
|
is_staging,
|
|
should_use_secure_cookies,
|
|
)
|
|
|
|
|
|
def get_current_environment() -> str:
|
|
"""
|
|
Get current environment.
|
|
|
|
Convenience function that delegates to app.core.environment.
|
|
Use this when you need just the environment string.
|
|
"""
|
|
return get_environment()
|
|
|
|
|
|
def is_production_environment() -> bool:
|
|
"""
|
|
Check if running in production.
|
|
|
|
Convenience function that delegates to app.core.environment.
|
|
Use this for production-specific logic.
|
|
"""
|
|
return is_production()
|
|
|
|
|
|
def is_development_environment() -> bool:
|
|
"""
|
|
Check if running in development.
|
|
|
|
Convenience function that delegates to app.core.environment.
|
|
Use this for development-specific logic.
|
|
"""
|
|
return is_development()
|
|
|
|
|
|
def is_staging_environment() -> bool:
|
|
"""
|
|
Check if running in staging.
|
|
|
|
Convenience function that delegates to app.core.environment.
|
|
Use this for staging-specific logic.
|
|
"""
|
|
return is_staging()
|
|
|
|
|
|
# =============================================================================
|
|
# VALIDATION FUNCTIONS
|
|
# =============================================================================
|
|
|
|
|
|
def validate_database_url() -> None:
|
|
"""
|
|
Validate that database URL is PostgreSQL.
|
|
|
|
Raises:
|
|
ValueError: If database URL is not PostgreSQL
|
|
"""
|
|
if settings.database_url.startswith("sqlite"):
|
|
raise ValueError(
|
|
"SQLite is not supported. Please use PostgreSQL.\n"
|
|
"Set DATABASE_URL environment variable to a PostgreSQL connection string.\n"
|
|
"Example: postgresql://user:password@localhost:5432/dbname\n"
|
|
"For local development, run: docker-compose up -d db"
|
|
)
|
|
if not settings.database_url.startswith("postgresql"):
|
|
raise ValueError(
|
|
f"Unsupported database: {settings.database_url.split(':')[0]}\n"
|
|
"Only PostgreSQL is supported."
|
|
)
|
|
|
|
|
|
def validate_production_settings() -> list[str]:
|
|
"""
|
|
Validate settings for production environment.
|
|
|
|
Returns:
|
|
List of warning messages if configuration is insecure
|
|
"""
|
|
warnings = []
|
|
|
|
if is_production():
|
|
# Check for default/insecure values
|
|
if settings.admin_password == "admin123":
|
|
warnings.append("⚠️ Using default admin password in production!")
|
|
|
|
if settings.jwt_secret_key == "change-this-in-production":
|
|
warnings.append("⚠️ Using default JWT secret key in production!")
|
|
|
|
if settings.debug:
|
|
warnings.append("⚠️ Debug mode enabled in production!")
|
|
|
|
if "*" in settings.allowed_hosts:
|
|
warnings.append("⚠️ ALLOWED_HOSTS is set to wildcard (*) in production!")
|
|
|
|
return warnings
|
|
|
|
|
|
def print_environment_info():
|
|
"""Print current environment configuration."""
|
|
print("\n" + "=" * 70)
|
|
print(f" ENVIRONMENT: {get_environment().upper()}")
|
|
print("=" * 70)
|
|
print(f" Database: {settings.database_url}")
|
|
print(f" Debug mode: {settings.debug}")
|
|
print(f" API port: {settings.api_port}")
|
|
print(f" Platform: {settings.platform_domain}")
|
|
print(f" Secure cookies: {should_use_secure_cookies()}")
|
|
print("=" * 70 + "\n")
|
|
|
|
# Show warnings if in production
|
|
if is_production():
|
|
warnings = validate_production_settings()
|
|
if warnings:
|
|
print("\n⚠️ PRODUCTION WARNINGS:")
|
|
for warning in warnings:
|
|
print(f" {warning}")
|
|
print()
|
|
|
|
|
|
# =============================================================================
|
|
# PUBLIC API
|
|
# =============================================================================
|
|
__all__ = [
|
|
# Settings singleton
|
|
"settings",
|
|
# Environment detection (re-exported from app.core.environment)
|
|
"get_environment",
|
|
"is_development",
|
|
"is_production",
|
|
"is_staging",
|
|
"should_use_secure_cookies",
|
|
# Convenience functions
|
|
"get_current_environment",
|
|
"is_production_environment",
|
|
"is_development_environment",
|
|
"is_staging_environment",
|
|
# Validation
|
|
"validate_database_url",
|
|
"validate_production_settings",
|
|
"print_environment_info",
|
|
]
|