demo seed change
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
from .internal_demo import router as internal_demo_router
|
||||
|
||||
__all__ = ["internal_demo_router"]
|
||||
244
services/auth/app/api/internal_demo.py
Normal file
244
services/auth/app/api/internal_demo.py
Normal file
@@ -0,0 +1,244 @@
|
||||
"""
|
||||
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"
|
||||
}
|
||||
@@ -6,7 +6,7 @@ from fastapi import FastAPI
|
||||
from sqlalchemy import text
|
||||
from app.core.config import settings
|
||||
from app.core.database import database_manager
|
||||
from app.api import auth_operations, users, onboarding_progress, consent, data_export, account_deletion
|
||||
from app.api import auth_operations, users, onboarding_progress, consent, data_export, account_deletion, internal_demo
|
||||
from shared.service_base import StandardFastAPIService
|
||||
from shared.messaging import UnifiedEventPublisher
|
||||
|
||||
@@ -169,3 +169,4 @@ service.add_router(onboarding_progress.router, tags=["onboarding"])
|
||||
service.add_router(consent.router, tags=["gdpr", "consent"])
|
||||
service.add_router(data_export.router, tags=["gdpr", "data-export"])
|
||||
service.add_router(account_deletion.router, tags=["gdpr", "account-deletion"])
|
||||
service.add_router(internal_demo.router, tags=["internal-demo"])
|
||||
|
||||
Reference in New Issue
Block a user