Files
bakery-ia/services/auth/app/api/internal_demo.py

245 lines
8.5 KiB
Python
Raw Normal View History

2025-12-13 23:57:54 +01:00
"""
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
2025-12-14 11:58:14 +01:00
import json
2025-12-13 23:57:54 +01:00
# 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"
}