#!/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, 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="admin", is_super_admin=True, 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_default_platforms(db: Session) -> list[Platform]: """Create all default platforms (OMS, Main, Loyalty+).""" platform_defs = [ { "code": "oms", "name": "Orion OMS", "description": "Order Management System for multi-store e-commerce", "domain": "oms.lu", "path_prefix": "oms", "default_language": "fr", "supported_languages": ["fr", "de", "en"], "settings": {}, "theme_config": {}, }, { "code": "main", "name": "Orion", "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) 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) 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 the OMS 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"]) ).scalar_one_or_none() if existing: print_warning(f"Tier already exists: {existing.name} ({existing.code})") 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) db.flush() tiers_created += 1 print_success(f"Created tier: {tier.name} ({tier.code})") return tiers_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 = { "user_type", "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 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 print_step(6, "Seeding subscription tiers...") oms_platform = next((p for p in platforms if p.code == "oms"), None) if oms_platform: create_subscription_tiers(db, oms_platform) else: print_warning("OMS platform not found, skipping tier seeding") # 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 == "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() print("\nšŸ“Š Database Status:") print(f" Admin users: {user_count}") print(f" Platforms: {platform_count}") print(f" Admin settings: {setting_count}") print(f" Sub. tiers: {tier_count}") print("\n" + "─" * 70) print("šŸ” ADMIN CREDENTIALS") print("─" * 70) print(" URL: /admin/login") print(f" Username: {settings.admin_username}") print(f" Password: {settings.admin_password}") # noqa: SEC-021 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:") print(" 1. Login to admin panel") if is_production(): print(" 2. CHANGE DEFAULT PASSWORD IMMEDIATELY!") # noqa: SEC-021 print(" 3. Configure admin settings") print(" 4. Create first store") else: print(" 2. Create demo data: make seed-demo") print(" 3. Start development: make dev") print("\nšŸ“ FOR DEMO DATA (Development only):") print(" 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()