#!/usr/bin/env python3 """ Production Database Initialization for Orion Platform This script initializes ESSENTIAL data required for production: - Platform admin user - Default store roles and permissions - Admin settings - Platform configuration This is SAFE to run in production and should be run after migrations. Usage: make init-prod This script is idempotent - safe to run multiple times. """ import sys from datetime import UTC, datetime from pathlib import Path # Add project root to path project_root = Path(__file__).parent.parent sys.path.insert(0, str(project_root)) import contextlib from sqlalchemy import select from sqlalchemy.orm import Session from app.core.config import ( print_environment_info, settings, validate_production_settings, ) from app.core.database import SessionLocal from app.core.environment import is_production from app.modules.billing.models.subscription import SubscriptionTier from app.modules.tenancy.models import AdminSetting, Platform, PlatformModule, User from app.modules.tenancy.services.permission_discovery_service import ( permission_discovery_service, ) from middleware.auth import AuthManager # Register all models with SQLAlchemy so string-based relationships resolve for _mod in [ "app.modules.billing.models", "app.modules.inventory.models", "app.modules.cart.models", "app.modules.messaging.models", "app.modules.loyalty.models", "app.modules.catalog.models", "app.modules.customers.models", "app.modules.orders.models", "app.modules.marketplace.models", "app.modules.cms.models", ]: with contextlib.suppress(ImportError): __import__(_mod) # ============================================================================= # HELPER FUNCTIONS # ============================================================================= def print_header(text: str): """Print formatted header.""" print("\n" + "=" * 70) print(f" {text}") print("=" * 70) def print_step(step: int, text: str): """Print step indicator.""" print(f"\n[{step}] {text}") def print_success(text: str): """Print success message.""" print(f" āœ“ {text}") def print_warning(text: str): """Print warning message.""" print(f" ⚠ {text}") def print_error(text: str): """Print error message.""" print(f" āœ— {text}") # ============================================================================= # INITIALIZATION FUNCTIONS # ============================================================================= def create_admin_user(db: Session, auth_manager: AuthManager) -> User: """Create or get the platform admin user.""" # Check if admin already exists admin = db.execute( select(User).where(User.email == settings.admin_email) ).scalar_one_or_none() if admin: print_warning(f"Admin user already exists: {admin.email}") return admin # Create new admin hashed_password = auth_manager.hash_password(settings.admin_password) admin = User( username=settings.admin_username, email=settings.admin_email, hashed_password=hashed_password, role="super_admin", first_name=settings.admin_first_name, last_name=settings.admin_last_name, is_active=True, is_email_verified=True, created_at=datetime.now(UTC), updated_at=datetime.now(UTC), ) db.add(admin) db.flush() print_success(f"Created admin user: {admin.email}") return admin def create_loyalty_admin(db: Session, auth_manager: AuthManager, loyalty_platform: Platform) -> User | None: """Create a platform admin for the Loyalty+ platform.""" from app.modules.tenancy.models.admin_platform import AdminPlatform email = "admin@rewardflow.lu" existing = db.execute(select(User).where(User.email == email)).scalar_one_or_none() if existing: print_warning(f"Loyalty admin already exists: {email}") return existing password = "admin123" # noqa: SEC001 Dev default, change in production admin = User( username="loyalty_admin", email=email, hashed_password=auth_manager.hash_password(password), role="platform_admin", first_name="Loyalty", last_name="Administrator", is_active=True, is_email_verified=True, ) db.add(admin) db.flush() # Assign to loyalty platform assignment = AdminPlatform( user_id=admin.id, platform_id=loyalty_platform.id, is_active=True, ) db.add(assignment) db.flush() print_success(f"Created loyalty admin: {email} (password: {password})") # noqa: SEC021 return admin def create_default_platforms(db: Session) -> list[Platform]: """Create all default platforms (OMS, Main, Loyalty+).""" platform_defs = [ { "code": "oms", "name": "OMS", "description": "Order Management System for multi-store e-commerce", "domain": "omsflow.lu", "path_prefix": "oms", "default_language": "fr", "supported_languages": ["fr", "de", "en"], "settings": {}, "theme_config": {}, }, { "code": "main", "name": "Wizard", "description": "Main marketing site showcasing all Orion platforms", "domain": "wizard.lu", "path_prefix": None, "default_language": "fr", "supported_languages": ["fr", "de", "en"], "settings": {"is_marketing_site": True}, "theme_config": {"primary_color": "#2563EB", "secondary_color": "#3B82F6"}, }, { "code": "loyalty", "name": "Loyalty", "description": "Customer loyalty program platform for Luxembourg businesses", "domain": "rewardflow.lu", "path_prefix": "loyalty", "default_language": "fr", "supported_languages": ["fr", "de", "en"], "settings": {"features": ["points", "rewards", "tiers", "analytics"]}, "theme_config": {"primary_color": "#8B5CF6", "secondary_color": "#A78BFA"}, }, ] platforms = [] for pdef in platform_defs: existing = db.execute( select(Platform).where(Platform.code == pdef["code"]) ).scalar_one_or_none() if existing: print_warning(f"Platform already exists: {existing.name} ({existing.code})") platforms.append(existing) continue platform = Platform( code=pdef["code"], name=pdef["name"], description=pdef["description"], domain=pdef["domain"], path_prefix=pdef["path_prefix"], default_language=pdef["default_language"], supported_languages=pdef["supported_languages"], settings=pdef["settings"], theme_config=pdef["theme_config"], is_active=True, is_public=True, created_at=datetime.now(UTC), updated_at=datetime.now(UTC), ) db.add(platform) # noqa: PERF006 db.flush() platforms.append(platform) print_success(f"Created platform: {platform.name} ({platform.code})") return platforms def create_default_role_templates(db: Session) -> dict: """Create default role templates (not tied to any store). These serve as templates that can be copied when creating store-specific roles. Note: Actual roles are store-specific and created when stores are onboarded. """ print(" Available role presets:") print(" - Manager: Nearly full access (except team management)") print(" - Staff: Day-to-day operations") print(" - Support: Customer service focused") print(" - Viewer: Read-only access") print(" - Marketing: Marketing and customer communication") print_success("Role templates ready for store onboarding") return { "manager": list(permission_discovery_service.get_preset_permissions("manager")), "staff": list(permission_discovery_service.get_preset_permissions("staff")), "support": list(permission_discovery_service.get_preset_permissions("support")), "viewer": list(permission_discovery_service.get_preset_permissions("viewer")), "marketing": list(permission_discovery_service.get_preset_permissions("marketing")), } def create_admin_settings(db: Session) -> int: """Create essential admin settings.""" settings_created = 0 # Essential platform settings default_settings = [ { "key": "platform_name", "value": settings.project_name, "value_type": "string", "description": "Platform name displayed in admin panel", "is_public": True, }, { "key": "platform_url", "value": f"https://{settings.platform_domain}", "value_type": "string", "description": "Main platform URL", "is_public": True, }, { "key": "support_email", "value": f"support@{settings.platform_domain}", "value_type": "string", "description": "Platform support email", "is_public": True, }, { "key": "max_stores_per_user", "value": str(settings.max_stores_per_user), "value_type": "integer", "description": "Maximum stores a user can own", "is_public": False, }, { "key": "max_team_members_per_store", "value": str(settings.max_team_members_per_store), "value_type": "integer", "description": "Maximum team members per store", "is_public": False, }, { "key": "invitation_expiry_days", "value": str(settings.invitation_expiry_days), "value_type": "integer", "description": "Days until team invitation expires", "is_public": False, }, { "key": "require_store_verification", "value": "true", "value_type": "boolean", "description": "Require admin verification for new stores", "is_public": False, }, { "key": "allow_custom_domains", "value": str(settings.allow_custom_domains).lower(), "value_type": "boolean", "description": "Allow stores to use custom domains", "is_public": False, }, { "key": "maintenance_mode", "value": "false", "value_type": "boolean", "description": "Enable maintenance mode", "is_public": True, }, # Logging settings { "key": "log_level", "value": "INFO", "value_type": "string", "category": "logging", "description": "Application log level (DEBUG, INFO, WARNING, ERROR, CRITICAL)", "is_public": False, }, { "key": "log_file_max_size_mb", "value": "10", "value_type": "integer", "category": "logging", "description": "Maximum log file size in MB before rotation", "is_public": False, }, { "key": "log_file_backup_count", "value": "5", "value_type": "integer", "category": "logging", "description": "Number of rotated log files to keep", "is_public": False, }, { "key": "db_log_retention_days", "value": "30", "value_type": "integer", "category": "logging", "description": "Number of days to retain logs in database", "is_public": False, }, { "key": "file_logging_enabled", "value": "true", "value_type": "boolean", "category": "logging", "description": "Enable file-based logging", "is_public": False, }, { "key": "db_logging_enabled", "value": "true", "value_type": "boolean", "category": "logging", "description": "Enable database logging for critical events", "is_public": False, }, ] for setting_data in default_settings: # Check if setting already exists existing = db.execute( select(AdminSetting).where(AdminSetting.key == setting_data["key"]) ).scalar_one_or_none() if not existing: setting = AdminSetting( key=setting_data["key"], value=setting_data["value"], value_type=setting_data["value_type"], category=setting_data.get("category"), description=setting_data.get("description"), is_public=setting_data.get("is_public", False), created_at=datetime.now(UTC), updated_at=datetime.now(UTC), ) db.add(setting) # noqa: PERF006 settings_created += 1 db.flush() if settings_created > 0: print_success(f"Created {settings_created} admin settings") else: print_warning("Admin settings already exist") return settings_created def create_subscription_tiers(db: Session, platform: Platform) -> int: """Create default subscription tiers for a platform.""" tier_defs = [ { "code": "essential", "name": "Essential", "price_monthly_cents": 2900, "price_annual_cents": 29000, "is_public": True, "display_order": 10, }, { "code": "professional", "name": "Professional", "price_monthly_cents": 7900, "price_annual_cents": 79000, "is_public": True, "display_order": 20, }, { "code": "business", "name": "Business", "price_monthly_cents": 14900, "price_annual_cents": 149000, "is_public": True, "display_order": 30, }, { "code": "enterprise", "name": "Enterprise", "price_monthly_cents": 29900, "price_annual_cents": None, "is_public": False, "display_order": 40, }, ] tiers_created = 0 for tdef in tier_defs: existing = db.execute( select(SubscriptionTier).where( SubscriptionTier.code == tdef["code"], SubscriptionTier.platform_id == platform.id, ) ).scalar_one_or_none() if existing: print_warning(f"Tier already exists: {existing.name} ({existing.code}) for {platform.name}") continue tier = SubscriptionTier( platform_id=platform.id, code=tdef["code"], name=tdef["name"], price_monthly_cents=tdef["price_monthly_cents"], price_annual_cents=tdef["price_annual_cents"], is_public=tdef["is_public"], display_order=tdef["display_order"], is_active=True, ) db.add(tier) # noqa: PERF006 db.flush() tiers_created += 1 print_success(f"Created tier: {tier.name} ({tier.code})") return tiers_created def create_platform_modules(db: Session, platforms: list[Platform]) -> int: """Create PlatformModule records for all platforms. Core modules are enabled for every platform. Optional modules are selectively enabled per platform. All other modules are created but disabled (available to toggle on later via the admin API). """ from app.modules.registry import MODULES, is_core_module # Optional modules enabled per platform (core modules always enabled) PLATFORM_MODULES = { "oms": ["inventory", "catalog", "orders", "marketplace", "analytics", "cart", "checkout"], "main": ["analytics", "monitoring", "dev-tools"], "loyalty": ["loyalty"], } now = datetime.now(UTC) records_created = 0 for platform in platforms: enabled_extras = set(PLATFORM_MODULES.get(platform.code, [])) for code in MODULES: existing = db.execute( select(PlatformModule).where( PlatformModule.platform_id == platform.id, PlatformModule.module_code == code, ) ).scalar_one_or_none() if existing: continue enabled = is_core_module(code) or code in enabled_extras pm = PlatformModule( platform_id=platform.id, module_code=code, is_enabled=enabled, enabled_at=now if enabled else None, config={}, ) db.add(pm) # noqa: PERF006 records_created += 1 db.flush() if records_created > 0: print_success(f"Created {records_created} platform module records") else: print_warning("Platform module records already exist") return records_created def verify_rbac_schema(db: Session) -> bool: """Verify that RBAC schema is in place.""" try: from sqlalchemy import inspect inspector = inspect(db.bind) tables = inspector.get_table_names() # Check users table has is_email_verified if "users" in tables: user_cols = {col["name"] for col in inspector.get_columns("users")} if "is_email_verified" not in user_cols: print_error("Missing 'is_email_verified' column in users table") return False # Check store_users has RBAC columns if "store_users" in tables: vu_cols = {col["name"] for col in inspector.get_columns("store_users")} required_cols = { "invitation_token", "invitation_sent_at", "invitation_accepted_at", } missing = required_cols - vu_cols if missing: print_error(f"Missing columns in store_users: {missing}") return False # Check roles table exists if "roles" not in tables: print_error("Missing 'roles' table") return False print_success("RBAC schema verified") return True except Exception as e: print_error(f"Schema verification failed: {e}") return False # ============================================================================= # MAIN INITIALIZATION # ============================================================================= def initialize_production(db: Session, auth_manager: AuthManager): """Initialize production database with essential data.""" print_header("PRODUCTION INITIALIZATION") # Step 1: Verify RBAC schema print_step(1, "Verifying RBAC schema...") if not verify_rbac_schema(db): print_error("RBAC schema not ready. Run migrations first:") print(" make migrate-up") sys.exit(1) # Step 2: Create admin user print_step(2, "Creating platform admin...") create_admin_user(db, auth_manager) # Step 3: Create default platforms print_step(3, "Creating default platforms...") platforms = create_default_platforms(db) # Step 3b: Create loyalty platform admin print_step("3b", "Creating loyalty platform admin...") loyalty_platform = next((p for p in platforms if p.code == "loyalty"), None) if loyalty_platform: create_loyalty_admin(db, auth_manager, loyalty_platform) else: print_warning("Loyalty platform not found, skipping loyalty admin creation") # Step 4: Set up default role templates print_step(4, "Setting up role templates...") create_default_role_templates(db) # Step 5: Create admin settings print_step(5, "Creating admin settings...") create_admin_settings(db) # Step 6: Seed subscription tiers for all platforms print_step(6, "Seeding subscription tiers...") for platform in platforms: create_subscription_tiers(db, platform) # Step 7: Create platform module records print_step(7, "Creating platform module records...") create_platform_modules(db, platforms) # Commit all changes db.commit() print_success("All changes committed") def print_summary(db: Session): """Print initialization summary.""" print_header("INITIALIZATION SUMMARY") # Count records user_count = db.query(User).filter( User.role.in_(["super_admin", "platform_admin"]) ).count() setting_count = db.query(AdminSetting).count() platform_count = db.query(Platform).count() tier_count = db.query(SubscriptionTier).filter(SubscriptionTier.is_active.is_(True)).count() module_count = db.query(PlatformModule).count() print("\nšŸ“Š Database Status:") print(f" Admin users: {user_count}") print(f" Platforms: {platform_count}") print(f" Platform mods: {module_count}") print(f" Admin settings: {setting_count}") print(f" Sub. tiers: {tier_count}") # Show platforms platforms = db.query(Platform).order_by(Platform.code).all() enabled_counts = {} for pm in db.query(PlatformModule).filter(PlatformModule.is_enabled.is_(True)).all(): enabled_counts[pm.platform_id] = enabled_counts.get(pm.platform_id, 0) + 1 port = settings.api_port print("\n" + "─" * 70) print("🌐 PLATFORMS") print("─" * 70) for p in platforms: n_enabled = enabled_counts.get(p.id, 0) if p.code == "main": dev_url = f"http://localhost:{port}/" else: dev_url = f"http://localhost:{port}/platforms/{p.code}/" print(f" {p.name} ({p.code})") print(f" Domain: {p.domain}") print(f" Dev URL: {dev_url}") print(f" Modules: {n_enabled} enabled") print("\n" + "─" * 70) print("šŸ” ADMIN CREDENTIALS") print("─" * 70) admin_url = f"http://localhost:{port}/admin/login" print(" Super Admin (all platforms):") print(f" URL: {admin_url}") print(f" Username: {settings.admin_username}") print(f" Password: {settings.admin_password}") # noqa: SEC021 print() print(" Loyalty Platform Admin (loyalty only):") print(f" URL: {admin_url}") print(" Username: loyalty_admin") print(" Password: admin123") # noqa: SEC021 print("─" * 70) # Show security warnings if in production if is_production(): warnings = validate_production_settings() if warnings: print("\nāš ļø SECURITY WARNINGS:") for warning in warnings: print(f" {warning}") print("\n Please update your .env file with secure values!") else: print("\nāš ļø DEVELOPMENT MODE:") print(" Default credentials are OK for development") print(" Change them in production via .env file") print("\nšŸš€ NEXT STEPS:") if is_production(): print(f" 1. Login to admin panel: {admin_url}") print(" 2. CHANGE DEFAULT PASSWORD IMMEDIATELY!") # noqa: SEC021 print(" 3. Configure admin settings") print(" 4. Create first store") else: print(" 1. Start development: make dev") print(f" 2. Admin panel: {admin_url}") print(f" 3. Merchant panel: http://localhost:{port}/merchants/login") print(" 4. Create demo data: make seed-demo") # ============================================================================= # MAIN ENTRY POINT # ============================================================================= def main(): """Main entry point.""" print("\n" + "ā•”" + "═" * 68 + "ā•—") print("ā•‘" + " " * 16 + "PRODUCTION INITIALIZATION" + " " * 27 + "ā•‘") print("ā•š" + "═" * 68 + "ā•") # Show environment info print_environment_info() # Production safety check if is_production(): warnings = validate_production_settings() if warnings: print("\nāš ļø PRODUCTION WARNINGS DETECTED:") for warning in warnings: print(f" {warning}") print("\nUpdate your .env file before continuing!") response = input("\nContinue anyway? (yes/no): ") if response.lower() != "yes": print("Initialization cancelled.") sys.exit(0) db = SessionLocal() auth_manager = AuthManager() try: initialize_production(db, auth_manager) print_summary(db) print_header("āœ… INITIALIZATION COMPLETED") except KeyboardInterrupt: db.rollback() print("\n\nāš ļø Initialization interrupted") sys.exit(1) except Exception as e: db.rollback() print_header("āŒ INITIALIZATION FAILED") print(f"\nError: {e}\n") import traceback traceback.print_exc() sys.exit(1) finally: db.close() if __name__ == "__main__": main()