""" Internal Demo Cloning API for Auth Service Service-to-service endpoint for cloning authentication and user data """ from fastapi import APIRouter, Depends, HTTPException, Header from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select import structlog import uuid from datetime import datetime, timezone from typing import Optional import os import sys from pathlib import Path # Add shared path sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent.parent)) from app.core.database import get_db from app.models.users import User from app.core.config import settings logger = structlog.get_logger() router = APIRouter() # Base demo tenant IDs DEMO_TENANT_PROFESSIONAL = "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6" def verify_internal_api_key(x_internal_api_key: Optional[str] = Header(None)): """Verify internal API key for service-to-service communication""" if x_internal_api_key != settings.INTERNAL_API_KEY: logger.warning("Unauthorized internal API access attempted") raise HTTPException(status_code=403, detail="Invalid internal API key") return True @router.post("/internal/demo/clone") async def clone_demo_data( base_tenant_id: str, virtual_tenant_id: str, demo_account_type: str, session_id: Optional[str] = None, session_created_at: Optional[str] = None, db: AsyncSession = Depends(get_db), _: bool = Depends(verify_internal_api_key) ): """ Clone auth service data for a virtual demo tenant Clones: - Demo users (owner and staff) Note: Tenant memberships are handled by the tenant service's internal_demo endpoint Args: base_tenant_id: Template tenant UUID to clone from virtual_tenant_id: Target virtual tenant UUID demo_account_type: Type of demo account session_id: Originating session ID for tracing Returns: Cloning status and record counts """ start_time = datetime.now(timezone.utc) # Parse session creation time if session_created_at: try: session_time = datetime.fromisoformat(session_created_at.replace('Z', '+00:00')) except (ValueError, AttributeError): session_time = start_time else: session_time = start_time logger.info( "Starting auth data cloning", base_tenant_id=base_tenant_id, virtual_tenant_id=virtual_tenant_id, demo_account_type=demo_account_type, session_id=session_id, session_created_at=session_created_at ) try: # Validate UUIDs base_uuid = uuid.UUID(base_tenant_id) virtual_uuid = uuid.UUID(virtual_tenant_id) # Note: We don't check for existing users since User model doesn't have demo_session_id # Demo users are identified by their email addresses from the seed data # Idempotency is handled by checking if each user email already exists below # Load demo users from JSON seed file try: from shared.utils.seed_data_paths import get_seed_data_path if demo_account_type == "professional": json_file = get_seed_data_path("professional", "02-auth.json") elif demo_account_type == "enterprise": json_file = get_seed_data_path("enterprise", "02-auth.json") else: raise ValueError(f"Invalid demo account type: {demo_account_type}") except ImportError: # Fallback to original path seed_data_dir = Path(__file__).parent.parent.parent.parent / "infrastructure" / "seed-data" if demo_account_type == "professional": json_file = seed_data_dir / "professional" / "02-auth.json" elif demo_account_type == "enterprise": json_file = seed_data_dir / "enterprise" / "parent" / "02-auth.json" else: raise ValueError(f"Invalid demo account type: {demo_account_type}") if not json_file.exists(): raise HTTPException( status_code=404, detail=f"Seed data file not found: {json_file}" ) # Load JSON data import json with open(json_file, 'r', encoding='utf-8') as f: seed_data = json.load(f) # Get demo users for this account type demo_users_data = seed_data.get("users", []) records_cloned = 0 # Create users and tenant memberships for user_data in demo_users_data: user_id = uuid.UUID(user_data["id"]) # Create user if not exists user_result = await db.execute( select(User).where(User.id == user_id) ) existing_user = user_result.scalars().first() if not existing_user: # Apply date adjustments to created_at and updated_at from shared.utils.demo_dates import adjust_date_for_demo # Adjust created_at date created_at_str = user_data.get("created_at", session_time.isoformat()) if isinstance(created_at_str, str): try: original_created_at = datetime.fromisoformat(created_at_str.replace('Z', '+00:00')) adjusted_created_at = adjust_date_for_demo(original_created_at, session_time) except ValueError: adjusted_created_at = session_time else: adjusted_created_at = session_time # Adjust updated_at date (same as created_at for demo users) adjusted_updated_at = adjusted_created_at # Get full_name from either "name" or "full_name" field full_name = user_data.get("full_name") or user_data.get("name", "Demo User") # For demo users, use a placeholder hashed password (they won't actually log in) # In production, this would be properly hashed demo_hashed_password = "$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LewY5GyYqNlI.eFKW" # "demo_password" user = User( id=user_id, email=user_data["email"], full_name=full_name, hashed_password=demo_hashed_password, is_active=user_data.get("is_active", True), is_verified=True, role=user_data.get("role", "member"), language=user_data.get("language", "es"), timezone=user_data.get("timezone", "Europe/Madrid"), created_at=adjusted_created_at, updated_at=adjusted_updated_at ) db.add(user) records_cloned += 1 # Note: Tenant memberships are handled by tenant service # Only create users in auth service await db.commit() duration_ms = int((datetime.now(timezone.utc) - start_time).total_seconds() * 1000) logger.info( "Auth data cloning completed", virtual_tenant_id=virtual_tenant_id, session_id=session_id, records_cloned=records_cloned, duration_ms=duration_ms ) return { "service": "auth", "status": "completed", "records_cloned": records_cloned, "base_tenant_id": str(base_tenant_id), "virtual_tenant_id": str(virtual_tenant_id), "session_id": session_id, "demo_account_type": demo_account_type, "duration_ms": duration_ms } except ValueError as e: logger.error("Invalid UUID format", error=str(e), virtual_tenant_id=virtual_tenant_id) raise HTTPException(status_code=400, detail=f"Invalid UUID: {str(e)}") except Exception as e: logger.error( "Failed to clone auth data", error=str(e), virtual_tenant_id=virtual_tenant_id, exc_info=True ) # Rollback on error await db.rollback() return { "service": "auth", "status": "failed", "records_cloned": 0, "duration_ms": int((datetime.now(timezone.utc) - start_time).total_seconds() * 1000), "error": str(e) } @router.get("/clone/health") async def clone_health_check(_: bool = Depends(verify_internal_api_key)): """ Health check for internal cloning endpoint Used by orchestrator to verify service availability """ return { "service": "auth", "clone_endpoint": "available", "version": "1.0.0" }