demo seed change

This commit is contained in:
Urtzi Alfaro
2025-12-13 23:57:54 +01:00
parent f3688dfb04
commit ff830a3415
299 changed files with 20328 additions and 19485 deletions

View File

@@ -0,0 +1,3 @@
from .internal_demo import router as internal_demo_router
__all__ = ["internal_demo_router"]

View 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"
}

View File

@@ -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"])

View File

@@ -1,151 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Seed Demo Users
Creates demo user accounts for production demo environment
"""
import asyncio
import sys
from pathlib import Path
project_root = Path(__file__).parent.parent.parent
sys.path.insert(0, str(project_root))
import os
os.environ.setdefault("AUTH_DATABASE_URL", os.getenv("AUTH_DATABASE_URL"))
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
from sqlalchemy import select
import structlog
import uuid
import json
logger = structlog.get_logger()
# Demo user configurations (public credentials for prospects)
DEMO_USERS = [
{
"id": "c1a2b3c4-d5e6-47a8-b9c0-d1e2f3a4b5c6",
"email": "demo.individual@panaderiasanpablo.com",
"password_hash": "$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LewY5GyYVPWzO8hGi", # DemoSanPablo2024!
"full_name": "María García López",
"phone": "+34 912 345 678",
"language": "es",
"timezone": "Europe/Madrid",
"role": "owner",
"is_active": True,
"is_verified": True,
"is_demo": True
},
{
"id": "d2e3f4a5-b6c7-48d9-e0f1-a2b3c4d5e6f7",
"email": "demo.central@panaderialaespiga.com",
"password_hash": "$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LewY5GyYVPWzO8hGi", # DemoLaEspiga2024!
"full_name": "Carlos Martínez Ruiz",
"phone": "+34 913 456 789",
"language": "es",
"timezone": "Europe/Madrid",
"role": "owner",
"is_active": True,
"is_verified": True,
"is_demo": True
}
]
def load_staff_users():
"""Load staff users from JSON file"""
json_file = Path(__file__).parent / "usuarios_staff_es.json"
if not json_file.exists():
logger.warning(f"Staff users JSON not found: {json_file}, skipping staff users")
return []
with open(json_file, 'r', encoding='utf-8') as f:
data = json.load(f)
# Combine both individual and central bakery staff
all_staff = data.get("staff_individual_bakery", []) + data.get("staff_central_bakery", [])
logger.info(f"Loaded {len(all_staff)} staff users from JSON")
return all_staff
async def seed_demo_users():
"""Seed demo users into auth database"""
database_url = os.getenv("AUTH_DATABASE_URL")
if not database_url:
logger.error("AUTH_DATABASE_URL environment variable not set")
return False
logger.info("Connecting to auth database", url=database_url.split("@")[-1])
engine = create_async_engine(database_url, echo=False)
session_factory = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
try:
async with session_factory() as session:
# Import User model
try:
from app.models.users import User
except ImportError:
from services.auth.app.models.users import User
from datetime import datetime, timezone
# Load staff users from JSON
staff_users = load_staff_users()
# Combine owner users with staff users
all_users = DEMO_USERS + staff_users
logger.info(f"Seeding {len(all_users)} total users ({len(DEMO_USERS)} owners + {len(staff_users)} staff)")
created_count = 0
skipped_count = 0
for user_data in all_users:
# Check if user already exists
result = await session.execute(
select(User).where(User.email == user_data["email"])
)
existing_user = result.scalar_one_or_none()
if existing_user:
logger.debug(f"Demo user already exists: {user_data['email']}")
skipped_count += 1
continue
# Create new demo user
user = User(
id=uuid.UUID(user_data["id"]),
email=user_data["email"],
hashed_password=user_data["password_hash"],
full_name=user_data["full_name"],
phone=user_data.get("phone"),
language=user_data.get("language", "es"),
timezone=user_data.get("timezone", "Europe/Madrid"),
role=user_data.get("role", "owner"),
is_active=user_data.get("is_active", True),
is_verified=user_data.get("is_verified", True),
created_at=datetime.now(timezone.utc),
updated_at=datetime.now(timezone.utc)
)
session.add(user)
created_count += 1
logger.debug(f"Created demo user: {user_data['email']} ({user_data.get('role', 'owner')})")
await session.commit()
logger.info(f"Demo users seeded successfully: {created_count} created, {skipped_count} skipped")
return True
except Exception as e:
logger.error(f"Failed to seed demo users: {str(e)}")
return False
finally:
await engine.dispose()
if __name__ == "__main__":
result = asyncio.run(seed_demo_users())
sys.exit(0 if result else 1)