Improve demo tennat and user get

This commit is contained in:
Urtzi Alfaro
2026-01-17 09:19:42 +01:00
parent 4b65817b3e
commit fbc670ddb3
9 changed files with 225 additions and 88 deletions

View File

@@ -60,6 +60,37 @@ export interface CloneDataRequest {
* Demo session response * Demo session response
* Backend: services/demo_session/app/api/schemas.py:18-30 (DemoSessionResponse) * Backend: services/demo_session/app/api/schemas.py:18-30 (DemoSessionResponse)
*/ */
/**
* Demo user data returned in session response
* Matches the structure of a real login user response
*/
export interface DemoUser {
id: string;
email: string;
full_name: string;
role: string;
is_active: boolean;
is_verified: boolean;
tenant_id: string;
created_at: string;
}
/**
* Demo tenant data returned in session response
* Matches the structure of a real tenant response
*/
export interface DemoTenant {
id: string;
name: string;
subdomain: string;
subscription_tier: string;
tenant_type: string;
business_type: string;
business_model: string;
description: string;
is_active: boolean;
}
export interface DemoSessionResponse { export interface DemoSessionResponse {
session_id: string; session_id: string;
virtual_tenant_id: string; virtual_tenant_id: string;
@@ -69,9 +100,11 @@ export interface DemoSessionResponse {
expires_at: string; // ISO datetime expires_at: string; // ISO datetime
demo_config: Record<string, any>; demo_config: Record<string, any>;
session_token: string; session_token: string;
subscription_tier: string; // NEW: Subscription tier from demo session subscription_tier: string;
is_enterprise: boolean; // NEW: Whether this is an enterprise demo is_enterprise: boolean;
tenant_name: string; // NEW: Tenant name for display // Complete user and tenant data (like a real login response)
user: DemoUser;
tenant: DemoTenant;
} }
/** /**

View File

@@ -72,9 +72,19 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
} }
} }
} else if (authStore.isAuthenticated) { } else if (authStore.isAuthenticated) {
// User is marked as authenticated but no tokens, logout // User is marked as authenticated but no tokens
console.log('No tokens found but user marked as authenticated, logging out'); // Check if this is a demo session - demo sessions don't use JWT tokens
authStore.logout(); const isDemoMode = localStorage.getItem('demo_mode') === 'true';
const demoSessionId = localStorage.getItem('demo_session_id');
if (isDemoMode && demoSessionId) {
// Demo session: authentication is valid via X-Demo-Session-Id header
console.log('Demo session detected, maintaining authentication state');
} else {
// Regular user without tokens - logout
console.log('No tokens found but user marked as authenticated, logging out');
authStore.logout();
}
} else { } else {
console.log('No stored auth data found'); console.log('No stored auth data found');
} }

View File

@@ -364,13 +364,14 @@ export function BakeryDashboard({ plan }: { plan?: string }) {
</div> </div>
{/* Setup Flow - Three States */} {/* Setup Flow - Three States */}
{loadingOnboarding ? ( {/* DEMO MODE BYPASS: Demo tenants have pre-configured data, skip onboarding blocker */}
/* Loading state for onboarding checks */ {loadingOnboarding && !isDemoMode ? (
/* Loading state for onboarding checks (non-demo only) */
<div className="flex items-center justify-center py-12"> <div className="flex items-center justify-center py-12">
<div className="animate-spin rounded-full h-12 w-12 border-b-2" style={{ borderColor: 'var(--color-primary)' }}></div> <div className="animate-spin rounded-full h-12 w-12 border-b-2" style={{ borderColor: 'var(--color-primary)' }}></div>
</div> </div>
) : progressPercentage < 50 ? ( ) : progressPercentage < 50 && !isDemoMode ? (
/* STATE 1: Critical Missing (<50%) - Full-page blocker */ /* STATE 1: Critical Missing (<50%) - Full-page blocker (non-demo only) */
<SetupWizardBlocker <SetupWizardBlocker
criticalSections={setupSections} criticalSections={setupSections}
/> />

View File

@@ -198,26 +198,29 @@ const DemoPage = () => {
localStorage.setItem('virtual_tenant_id', sessionData.virtual_tenant_id); localStorage.setItem('virtual_tenant_id', sessionData.virtual_tenant_id);
localStorage.setItem('demo_expires_at', sessionData.expires_at); localStorage.setItem('demo_expires_at', sessionData.expires_at);
// BUG FIX: Store the session token as access_token for authentication // Store the session token as access_token for authentication
if (sessionData.session_token) { if (sessionData.session_token && sessionData.user) {
console.log('🔐 [DemoPage] Storing session token:', sessionData.session_token.substring(0, 20) + '...'); console.log('🔐 [DemoPage] Storing session token:', sessionData.session_token.substring(0, 20) + '...');
localStorage.setItem('access_token', sessionData.session_token); localStorage.setItem('access_token', sessionData.session_token);
// CRITICAL FIX: Update Zustand auth store with demo auth // Use complete user data from backend (like a real login response)
// This ensures the token persists across navigation and page reloads
setDemoAuth(sessionData.session_token, { setDemoAuth(sessionData.session_token, {
id: 'demo-user', id: sessionData.user.id,
email: 'demo@bakery.com', email: sessionData.user.email,
full_name: 'Demo User', full_name: sessionData.user.full_name,
is_active: true, is_active: sessionData.user.is_active,
is_verified: true, is_verified: sessionData.user.is_verified,
created_at: new Date().toISOString(), created_at: sessionData.user.created_at,
tenant_id: sessionData.virtual_tenant_id, tenant_id: sessionData.user.tenant_id,
}, tier); // NEW: Pass subscription tier to setDemoAuth role: sessionData.user.role as any,
}, tier);
console.log('✅ [DemoPage] Demo auth set in store'); // Store tenant data in localStorage for tenant initializer
localStorage.setItem('demo_tenant_data', JSON.stringify(sessionData.tenant));
console.log('✅ [DemoPage] Demo auth set in store with complete user data');
} else { } else {
console.error('❌ [DemoPage] No session_token in response!', sessionData); console.error('❌ [DemoPage] Missing session_token or user in response!', sessionData);
} }
// Now poll for status until ready // Now poll for status until ready

View File

@@ -1,5 +1,5 @@
import { useEffect } from 'react'; import { useEffect } from 'react';
import { useIsAuthenticated } from './auth.store'; import { useIsAuthenticated, useAuthStore } from './auth.store';
import { useTenantActions, useAvailableTenants, useCurrentTenant, useParentTenant } from './tenant.store'; import { useTenantActions, useAvailableTenants, useCurrentTenant, useParentTenant } from './tenant.store';
import { useIsDemoMode, useDemoSessionId, useDemoAccountType } from '../hooks/useAccessControl'; import { useIsDemoMode, useDemoSessionId, useDemoAccountType } from '../hooks/useAccessControl';
import { useSubscription } from '../api/hooks/subscription'; import { useSubscription } from '../api/hooks/subscription';
@@ -19,29 +19,19 @@ const getDemoTierForAccountType = (accountType: string | null): SubscriptionTier
}; };
/** /**
* Defines appropriate tenant characteristics based on account type * Gets tenant data from localStorage (set by DemoPage from backend response)
* Returns null if not available - caller must handle this case
*/ */
const getTenantDetailsForAccountType = (accountType: string | null) => { const getTenantDataFromStorage = (): Record<string, any> | null => {
// These details match the fixture files: const storedTenantData = localStorage.getItem('demo_tenant_data');
// professional: shared/demo/fixtures/professional/01-tenant.json if (storedTenantData) {
// enterprise: shared/demo/fixtures/enterprise/parent/01-tenant.json try {
const details = { return JSON.parse(storedTenantData);
professional: { } catch (e) {
name: 'Panadería Artesana Madrid - Demo', console.error('Failed to parse stored tenant data:', e);
business_type: 'bakery',
business_model: 'professional',
description: 'Professional tier demo tenant for bakery operations'
},
enterprise: {
name: 'Panadería Artesana España - Central',
business_type: 'bakery',
business_model: 'enterprise',
description: 'Central production facility and parent tenant for Panadería Artesana España multi-location bakery chain'
} }
}; }
return null;
const defaultDetails = details.professional;
return details[accountType as keyof typeof details] || defaultDetails;
}; };
/** /**
@@ -58,6 +48,8 @@ export const useTenantInitializer = () => {
const parentTenant = useParentTenant(); // Track if we're viewing a child tenant const parentTenant = useParentTenant(); // Track if we're viewing a child tenant
const { loadUserTenants, loadChildTenants, setCurrentTenant, setAvailableTenants } = useTenantActions(); const { loadUserTenants, loadChildTenants, setCurrentTenant, setAvailableTenants } = useTenantActions();
const { subscriptionInfo } = useSubscription(); const { subscriptionInfo } = useSubscription();
// Use reactive hook for user to ensure re-run when auth store rehydrates
const authUser = useAuthStore((state) => state.user);
// Load tenants for authenticated users (but not demo users - they have special initialization below) // Load tenants for authenticated users (but not demo users - they have special initialization below)
useEffect(() => { useEffect(() => {
@@ -101,30 +93,40 @@ export const useTenantInitializer = () => {
typeof currentTenant === 'object' && typeof currentTenant === 'object' &&
currentTenant.id === virtualTenantId; currentTenant.id === virtualTenantId;
// Determine the appropriate subscription tier based on stored value or account type // Get tenant data from localStorage (set by DemoPage from backend)
const subscriptionTier = storedTier as SubscriptionTier || getDemoTierForAccountType(demoAccountType); const tenantData = getTenantDataFromStorage();
// Use reactive authUser from hook instead of getState() to ensure re-run when user is available
const userId = authUser?.id;
// Get appropriate tenant details based on account type // Guard: If no tenant data or user is available, skip tenant setup
const tenantDetails = getTenantDetailsForAccountType(demoAccountType); // This means the demo session wasn't properly initialized or auth store hasn't rehydrated yet
if (!tenantData || !userId) {
console.log('[useTenantInitializer] Waiting for tenant data or user - tenantData:', !!tenantData, 'userId:', userId);
return;
}
// Create a complete tenant object matching TenantResponse structure // Determine the appropriate subscription tier
const subscriptionTier = storedTier as SubscriptionTier || tenantData.subscription_tier || getDemoTierForAccountType(demoAccountType);
// Create a complete tenant object using data from backend response
const mockTenant = { const mockTenant = {
id: virtualTenantId, id: tenantData.id || virtualTenantId,
name: tenantDetails.name, name: tenantData.name,
subdomain: `demo-${demoSessionId.slice(0, 8)}`, subdomain: tenantData.subdomain,
business_type: tenantDetails.business_type, business_type: tenantData.business_type,
business_model: tenantDetails.business_model, business_model: tenantData.business_model,
description: tenantDetails.description, description: tenantData.description,
address: 'Demo Address', address: 'Demo Address',
city: 'Madrid', city: 'Madrid',
postal_code: '28001', postal_code: '28001',
phone: null, phone: null,
is_active: true, is_active: tenantData.is_active,
subscription_plan: subscriptionTier, subscription_plan: subscriptionTier,
subscription_tier: subscriptionTier, subscription_tier: subscriptionTier,
tenant_type: tenantData.tenant_type,
ml_model_trained: false, ml_model_trained: false,
last_training_date: null, last_training_date: null,
owner_id: 'demo-user', owner_id: userId,
created_at: new Date().toISOString(), created_at: new Date().toISOString(),
}; };
@@ -179,5 +181,5 @@ export const useTenantInitializer = () => {
}); });
} }
} }
}, [isDemoMode, demoSessionId, demoAccountType, currentTenant, availableTenants, parentTenant, setCurrentTenant, setAvailableTenants]); }, [isDemoMode, demoSessionId, demoAccountType, currentTenant, availableTenants, parentTenant, setCurrentTenant, setAvailableTenants, authUser]);
}; };

View File

@@ -34,6 +34,13 @@ DEMO_TENANT_IDS = {
str(DEMO_TENANT_CHILD_3), # Enterprise chain child 3 str(DEMO_TENANT_CHILD_3), # Enterprise chain child 3
} }
# Demo user IDs - Maps demo account type to actual user UUIDs from fixture files
# These IDs are the owner IDs from the respective 01-tenant.json files
DEMO_USER_IDS = {
"professional": "c1a2b3c4-d5e6-47a8-b9c0-d1e2f3a4b5c6", # María García López (professional/01-tenant.json -> owner.id)
"enterprise": "d2e3f4a5-b6c7-48d9-e0f1-a2b3c4d5e6f7" # Director (enterprise/parent/01-tenant.json -> owner.id)
}
# Allowed operations for demo accounts (limited write) # Allowed operations for demo accounts (limited write)
DEMO_ALLOWED_OPERATIONS = { DEMO_ALLOWED_OPERATIONS = {
# Read operations - all allowed # Read operations - all allowed
@@ -185,19 +192,21 @@ class DemoMiddleware(BaseHTTPMiddleware):
current_status = session_info.get("status") if session_info else None current_status = session_info.get("status") if session_info else None
if session_info and current_status in valid_statuses: if session_info and current_status in valid_statuses:
# NOTE: Path transformation for demo-user removed.
# Frontend now receives the real demo_user_id from session creation
# and uses it directly in API calls.
# Inject virtual tenant ID # Inject virtual tenant ID
request.state.tenant_id = session_info["virtual_tenant_id"] # Use scope state directly to avoid potential state property issues
request.state.is_demo_session = True request.scope.setdefault("state", {})
request.state.demo_account_type = session_info["demo_account_type"] state = request.scope["state"]
request.state.demo_session_status = current_status # Track status for monitoring state["tenant_id"] = session_info["virtual_tenant_id"]
state["is_demo_session"] = True
state["demo_account_type"] = session_info["demo_account_type"]
state["demo_session_status"] = current_status # Track status for monitoring
# Inject demo user context for auth middleware # Inject demo user context for auth middleware
# Map demo account type to the actual demo user IDs from fixture files # Uses DEMO_USER_IDS constant defined at module level
# These IDs are the owner IDs from the respective 01-tenant.json files
DEMO_USER_IDS = {
"professional": "c1a2b3c4-d5e6-47a8-b9c0-d1e2f3a4b5c6", # María García López (professional/01-tenant.json -> owner.id)
"enterprise": "d2e3f4a5-b6c7-48d9-e0f1-a2b3c4d5e6f7" # Director (enterprise/parent/01-tenant.json -> owner.id)
}
demo_user_id = DEMO_USER_IDS.get( demo_user_id = DEMO_USER_IDS.get(
session_info.get("demo_account_type", "professional"), session_info.get("demo_account_type", "professional"),
DEMO_USER_IDS["professional"] DEMO_USER_IDS["professional"]
@@ -207,7 +216,7 @@ class DemoMiddleware(BaseHTTPMiddleware):
# NEW: Extract subscription tier from demo account type # NEW: Extract subscription tier from demo account type
subscription_tier = "enterprise" if session_info.get("demo_account_type") == "enterprise" else "professional" subscription_tier = "enterprise" if session_info.get("demo_account_type") == "enterprise" else "professional"
request.state.user = { state["user"] = {
"user_id": demo_user_id, # Use actual demo user UUID "user_id": demo_user_id, # Use actual demo user UUID
"email": f"demo-{session_id}@demo.local", "email": f"demo-{session_id}@demo.local",
"tenant_id": session_info["virtual_tenant_id"], "tenant_id": session_info["virtual_tenant_id"],
@@ -282,8 +291,10 @@ class DemoMiddleware(BaseHTTPMiddleware):
# Check if this is a demo tenant (base template) # Check if this is a demo tenant (base template)
elif tenant_id in DEMO_TENANT_IDS: elif tenant_id in DEMO_TENANT_IDS:
# Direct access to demo tenant without session - block writes # Direct access to demo tenant without session - block writes
request.state.is_demo_session = True request.scope.setdefault("state", {})
request.state.tenant_id = tenant_id state = request.scope["state"]
state["is_demo_session"] = True
state["tenant_id"] = tenant_id
if request.method not in ["GET", "HEAD", "OPTIONS"]: if request.method not in ["GET", "HEAD", "OPTIONS"]:
return JSONResponse( return JSONResponse(

View File

@@ -212,17 +212,18 @@ async def create_demo_session(
# Add error handling for the task to prevent silent failures # Add error handling for the task to prevent silent failures
task.add_done_callback(lambda t: _handle_task_result(t, session.session_id)) task.add_done_callback(lambda t: _handle_task_result(t, session.session_id))
# Get complete demo account data from config (includes user, tenant, subscription info)
subscription_tier = demo_config.get("subscription_tier", "professional")
user_data = demo_config.get("user", {})
tenant_data = demo_config.get("tenant", {})
# Generate session token with subscription data # Generate session token with subscription data
# Map demo_account_type to subscription tier
subscription_tier = "enterprise" if session.demo_account_type == "enterprise" else "professional"
session_token = jwt.encode( session_token = jwt.encode(
{ {
"session_id": session.session_id, "session_id": session.session_id,
"virtual_tenant_id": str(session.virtual_tenant_id), "virtual_tenant_id": str(session.virtual_tenant_id),
"demo_account_type": request.demo_account_type, "demo_account_type": request.demo_account_type,
"exp": session.expires_at.timestamp(), "exp": session.expires_at.timestamp(),
# NEW: Subscription context (same structure as user JWT)
"tenant_id": str(session.virtual_tenant_id), "tenant_id": str(session.virtual_tenant_id),
"subscription": { "subscription": {
"tier": subscription_tier, "tier": subscription_tier,
@@ -235,14 +236,7 @@ async def create_demo_session(
algorithm=settings.JWT_ALGORITHM algorithm=settings.JWT_ALGORITHM
) )
# Map demo_account_type to subscription tier # Build complete response like a real login would return
subscription_tier = "enterprise" if session.demo_account_type == "enterprise" else "professional"
tenant_name = (
"Panadería Artesana España - Central"
if session.demo_account_type == "enterprise"
else "Panadería Artesana Madrid - Demo"
)
return { return {
"session_id": session.session_id, "session_id": session.session_id,
"virtual_tenant_id": str(session.virtual_tenant_id), "virtual_tenant_id": str(session.virtual_tenant_id),
@@ -254,7 +248,29 @@ async def create_demo_session(
"session_token": session_token, "session_token": session_token,
"subscription_tier": subscription_tier, "subscription_tier": subscription_tier,
"is_enterprise": session.demo_account_type == "enterprise", "is_enterprise": session.demo_account_type == "enterprise",
"tenant_name": tenant_name # Complete user data (like a real login response)
"user": {
"id": user_data.get("id"),
"email": user_data.get("email"),
"full_name": user_data.get("full_name"),
"role": user_data.get("role", "owner"),
"is_active": user_data.get("is_active", True),
"is_verified": user_data.get("is_verified", True),
"tenant_id": str(session.virtual_tenant_id),
"created_at": session.created_at.isoformat()
},
# Complete tenant data
"tenant": {
"id": str(session.virtual_tenant_id),
"name": demo_config.get("name"),
"subdomain": demo_config.get("subdomain"),
"subscription_tier": subscription_tier,
"tenant_type": demo_config.get("tenant_type", "standalone"),
"business_type": tenant_data.get("business_type"),
"business_model": tenant_data.get("business_model"),
"description": tenant_data.get("description"),
"is_active": True
}
} }
except Exception as e: except Exception as e:

View File

@@ -16,8 +16,33 @@ class DemoSessionCreate(BaseModel):
user_agent: Optional[str] = None user_agent: Optional[str] = None
class DemoUser(BaseModel):
"""Demo user data returned in session response"""
id: str
email: str
full_name: str
role: str
is_active: bool
is_verified: bool
tenant_id: str
created_at: str
class DemoTenant(BaseModel):
"""Demo tenant data returned in session response"""
id: str
name: str
subdomain: str
subscription_tier: str
tenant_type: str
business_type: Optional[str] = None
business_model: Optional[str] = None
description: Optional[str] = None
is_active: bool
class DemoSessionResponse(BaseModel): class DemoSessionResponse(BaseModel):
"""Demo session response""" """Demo session response - mirrors a real login response with user and tenant data"""
session_id: str session_id: str
virtual_tenant_id: str virtual_tenant_id: str
demo_account_type: str demo_account_type: str
@@ -26,6 +51,11 @@ class DemoSessionResponse(BaseModel):
expires_at: datetime expires_at: datetime
demo_config: Dict[str, Any] demo_config: Dict[str, Any]
session_token: str session_token: str
subscription_tier: str
is_enterprise: bool
# Complete user and tenant data (like a real login response)
user: DemoUser
tenant: DemoTenant
class Config: class Config:
from_attributes = True from_attributes = True

View File

@@ -35,6 +35,7 @@ class Settings(BaseServiceSettings):
DEMO_SESSION_CLEANUP_INTERVAL_MINUTES: int = 60 DEMO_SESSION_CLEANUP_INTERVAL_MINUTES: int = 60
# Demo account credentials (public) # Demo account credentials (public)
# Contains complete user, tenant, and subscription data matching fixture files
DEMO_ACCOUNTS: dict = { DEMO_ACCOUNTS: dict = {
"professional": { "professional": {
"email": "demo.professional@panaderiaartesana.com", "email": "demo.professional@panaderiaartesana.com",
@@ -42,7 +43,22 @@ class Settings(BaseServiceSettings):
"subdomain": "demo-artesana", "subdomain": "demo-artesana",
"base_tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6", "base_tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
"subscription_tier": "professional", "subscription_tier": "professional",
"tenant_type": "standalone" "tenant_type": "standalone",
# User data from fixtures/professional/01-tenant.json
"user": {
"id": "c1a2b3c4-d5e6-47a8-b9c0-d1e2f3a4b5c6",
"email": "maria.garcia@panaderiaartesana.com",
"full_name": "María García López",
"role": "owner",
"is_active": True,
"is_verified": True
},
# Tenant data
"tenant": {
"business_type": "bakery",
"business_model": "production_retail",
"description": "Professional tier demo tenant for bakery operations"
}
}, },
"enterprise": { "enterprise": {
"email": "central@panaderiaartesana.es", "email": "central@panaderiaartesana.es",
@@ -51,6 +67,21 @@ class Settings(BaseServiceSettings):
"base_tenant_id": "80000000-0000-4000-a000-000000000001", "base_tenant_id": "80000000-0000-4000-a000-000000000001",
"subscription_tier": "enterprise", "subscription_tier": "enterprise",
"tenant_type": "parent", "tenant_type": "parent",
# User data from fixtures/enterprise/parent/01-tenant.json
"user": {
"id": "d2e3f4a5-b6c7-48d9-e0f1-a2b3c4d5e6f7",
"email": "director@panaderiaartesana.es",
"full_name": "Director",
"role": "owner",
"is_active": True,
"is_verified": True
},
# Tenant data
"tenant": {
"business_type": "bakery_chain",
"business_model": "multi_location",
"description": "Central production facility and parent tenant for multi-location bakery chain"
},
"children": [ "children": [
{ {
"name": "Madrid - Salamanca", "name": "Madrid - Salamanca",