diff --git a/frontend/src/api/types/demo.ts b/frontend/src/api/types/demo.ts index 025b43b3..423d7412 100644 --- a/frontend/src/api/types/demo.ts +++ b/frontend/src/api/types/demo.ts @@ -60,6 +60,37 @@ export interface CloneDataRequest { * Demo session response * 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 { session_id: string; virtual_tenant_id: string; @@ -69,9 +100,11 @@ export interface DemoSessionResponse { expires_at: string; // ISO datetime demo_config: Record; session_token: string; - subscription_tier: string; // NEW: Subscription tier from demo session - is_enterprise: boolean; // NEW: Whether this is an enterprise demo - tenant_name: string; // NEW: Tenant name for display + subscription_tier: string; + is_enterprise: boolean; + // Complete user and tenant data (like a real login response) + user: DemoUser; + tenant: DemoTenant; } /** diff --git a/frontend/src/contexts/AuthContext.tsx b/frontend/src/contexts/AuthContext.tsx index 800e53d8..454a9a9c 100644 --- a/frontend/src/contexts/AuthContext.tsx +++ b/frontend/src/contexts/AuthContext.tsx @@ -72,9 +72,19 @@ export const AuthProvider: React.FC = ({ children }) => { } } } else if (authStore.isAuthenticated) { - // User is marked as authenticated but no tokens, logout - console.log('No tokens found but user marked as authenticated, logging out'); - authStore.logout(); + // User is marked as authenticated but no tokens + // Check if this is a demo session - demo sessions don't use JWT tokens + 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 { console.log('No stored auth data found'); } diff --git a/frontend/src/pages/app/DashboardPage.tsx b/frontend/src/pages/app/DashboardPage.tsx index 31b08cf4..979b18d9 100644 --- a/frontend/src/pages/app/DashboardPage.tsx +++ b/frontend/src/pages/app/DashboardPage.tsx @@ -364,13 +364,14 @@ export function BakeryDashboard({ plan }: { plan?: string }) { {/* Setup Flow - Three States */} - {loadingOnboarding ? ( - /* Loading state for onboarding checks */ + {/* DEMO MODE BYPASS: Demo tenants have pre-configured data, skip onboarding blocker */} + {loadingOnboarding && !isDemoMode ? ( + /* Loading state for onboarding checks (non-demo only) */
- ) : progressPercentage < 50 ? ( - /* STATE 1: Critical Missing (<50%) - Full-page blocker */ + ) : progressPercentage < 50 && !isDemoMode ? ( + /* STATE 1: Critical Missing (<50%) - Full-page blocker (non-demo only) */ diff --git a/frontend/src/pages/public/DemoPage.tsx b/frontend/src/pages/public/DemoPage.tsx index 49f5ac15..4812db64 100644 --- a/frontend/src/pages/public/DemoPage.tsx +++ b/frontend/src/pages/public/DemoPage.tsx @@ -198,26 +198,29 @@ const DemoPage = () => { localStorage.setItem('virtual_tenant_id', sessionData.virtual_tenant_id); localStorage.setItem('demo_expires_at', sessionData.expires_at); - // BUG FIX: Store the session token as access_token for authentication - if (sessionData.session_token) { + // Store the session token as access_token for authentication + if (sessionData.session_token && sessionData.user) { console.log('🔐 [DemoPage] Storing session token:', sessionData.session_token.substring(0, 20) + '...'); localStorage.setItem('access_token', sessionData.session_token); - // CRITICAL FIX: Update Zustand auth store with demo auth - // This ensures the token persists across navigation and page reloads + // Use complete user data from backend (like a real login response) setDemoAuth(sessionData.session_token, { - id: 'demo-user', - email: 'demo@bakery.com', - full_name: 'Demo User', - is_active: true, - is_verified: true, - created_at: new Date().toISOString(), - tenant_id: sessionData.virtual_tenant_id, - }, tier); // NEW: Pass subscription tier to setDemoAuth + id: sessionData.user.id, + email: sessionData.user.email, + full_name: sessionData.user.full_name, + is_active: sessionData.user.is_active, + is_verified: sessionData.user.is_verified, + created_at: sessionData.user.created_at, + tenant_id: sessionData.user.tenant_id, + 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 { - 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 diff --git a/frontend/src/stores/useTenantInitializer.ts b/frontend/src/stores/useTenantInitializer.ts index b6824ee4..c0bf1740 100644 --- a/frontend/src/stores/useTenantInitializer.ts +++ b/frontend/src/stores/useTenantInitializer.ts @@ -1,5 +1,5 @@ import { useEffect } from 'react'; -import { useIsAuthenticated } from './auth.store'; +import { useIsAuthenticated, useAuthStore } from './auth.store'; import { useTenantActions, useAvailableTenants, useCurrentTenant, useParentTenant } from './tenant.store'; import { useIsDemoMode, useDemoSessionId, useDemoAccountType } from '../hooks/useAccessControl'; 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) => { - // These details match the fixture files: - // professional: shared/demo/fixtures/professional/01-tenant.json - // enterprise: shared/demo/fixtures/enterprise/parent/01-tenant.json - const details = { - professional: { - name: 'PanaderĂ­a Artesana Madrid - Demo', - 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' +const getTenantDataFromStorage = (): Record | null => { + const storedTenantData = localStorage.getItem('demo_tenant_data'); + if (storedTenantData) { + try { + return JSON.parse(storedTenantData); + } catch (e) { + console.error('Failed to parse stored tenant data:', e); } - }; - - const defaultDetails = details.professional; - return details[accountType as keyof typeof details] || defaultDetails; + } + return null; }; /** @@ -58,6 +48,8 @@ export const useTenantInitializer = () => { const parentTenant = useParentTenant(); // Track if we're viewing a child tenant const { loadUserTenants, loadChildTenants, setCurrentTenant, setAvailableTenants } = useTenantActions(); 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) useEffect(() => { @@ -101,30 +93,40 @@ export const useTenantInitializer = () => { typeof currentTenant === 'object' && currentTenant.id === virtualTenantId; - // Determine the appropriate subscription tier based on stored value or account type - const subscriptionTier = storedTier as SubscriptionTier || getDemoTierForAccountType(demoAccountType); + // Get tenant data from localStorage (set by DemoPage from backend) + 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 - const tenantDetails = getTenantDetailsForAccountType(demoAccountType); + // Guard: If no tenant data or user is available, skip tenant setup + // 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 = { - id: virtualTenantId, - name: tenantDetails.name, - subdomain: `demo-${demoSessionId.slice(0, 8)}`, - business_type: tenantDetails.business_type, - business_model: tenantDetails.business_model, - description: tenantDetails.description, + id: tenantData.id || virtualTenantId, + name: tenantData.name, + subdomain: tenantData.subdomain, + business_type: tenantData.business_type, + business_model: tenantData.business_model, + description: tenantData.description, address: 'Demo Address', city: 'Madrid', postal_code: '28001', phone: null, - is_active: true, + is_active: tenantData.is_active, subscription_plan: subscriptionTier, subscription_tier: subscriptionTier, + tenant_type: tenantData.tenant_type, ml_model_trained: false, last_training_date: null, - owner_id: 'demo-user', + owner_id: userId, 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]); }; \ No newline at end of file diff --git a/gateway/app/middleware/demo_middleware.py b/gateway/app/middleware/demo_middleware.py index c8313815..32bdf9be 100644 --- a/gateway/app/middleware/demo_middleware.py +++ b/gateway/app/middleware/demo_middleware.py @@ -34,6 +34,13 @@ DEMO_TENANT_IDS = { 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) DEMO_ALLOWED_OPERATIONS = { # Read operations - all allowed @@ -185,19 +192,21 @@ class DemoMiddleware(BaseHTTPMiddleware): current_status = session_info.get("status") if session_info else None 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 - request.state.tenant_id = session_info["virtual_tenant_id"] - request.state.is_demo_session = True - request.state.demo_account_type = session_info["demo_account_type"] - request.state.demo_session_status = current_status # Track status for monitoring + # Use scope state directly to avoid potential state property issues + request.scope.setdefault("state", {}) + state = request.scope["state"] + 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 - # Map demo account type to the actual demo user IDs 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) - } + # Uses DEMO_USER_IDS constant defined at module level demo_user_id = DEMO_USER_IDS.get( session_info.get("demo_account_type", "professional"), DEMO_USER_IDS["professional"] @@ -207,7 +216,7 @@ class DemoMiddleware(BaseHTTPMiddleware): # NEW: Extract subscription tier from demo account type 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 "email": f"demo-{session_id}@demo.local", "tenant_id": session_info["virtual_tenant_id"], @@ -282,8 +291,10 @@ class DemoMiddleware(BaseHTTPMiddleware): # Check if this is a demo tenant (base template) elif tenant_id in DEMO_TENANT_IDS: # Direct access to demo tenant without session - block writes - request.state.is_demo_session = True - request.state.tenant_id = tenant_id + request.scope.setdefault("state", {}) + state = request.scope["state"] + state["is_demo_session"] = True + state["tenant_id"] = tenant_id if request.method not in ["GET", "HEAD", "OPTIONS"]: return JSONResponse( diff --git a/services/demo_session/app/api/demo_sessions.py b/services/demo_session/app/api/demo_sessions.py index 8d7077c9..2a73b91a 100644 --- a/services/demo_session/app/api/demo_sessions.py +++ b/services/demo_session/app/api/demo_sessions.py @@ -212,17 +212,18 @@ async def create_demo_session( # Add error handling for the task to prevent silent failures 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 - # Map demo_account_type to subscription tier - subscription_tier = "enterprise" if session.demo_account_type == "enterprise" else "professional" - session_token = jwt.encode( { "session_id": session.session_id, "virtual_tenant_id": str(session.virtual_tenant_id), "demo_account_type": request.demo_account_type, "exp": session.expires_at.timestamp(), - # NEW: Subscription context (same structure as user JWT) "tenant_id": str(session.virtual_tenant_id), "subscription": { "tier": subscription_tier, @@ -235,14 +236,7 @@ async def create_demo_session( algorithm=settings.JWT_ALGORITHM ) - # Map demo_account_type to subscription tier - 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" - ) - + # Build complete response like a real login would return return { "session_id": session.session_id, "virtual_tenant_id": str(session.virtual_tenant_id), @@ -254,7 +248,29 @@ async def create_demo_session( "session_token": session_token, "subscription_tier": subscription_tier, "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: diff --git a/services/demo_session/app/api/schemas.py b/services/demo_session/app/api/schemas.py index 4a4fa3dc..7c64909c 100644 --- a/services/demo_session/app/api/schemas.py +++ b/services/demo_session/app/api/schemas.py @@ -16,8 +16,33 @@ class DemoSessionCreate(BaseModel): 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): - """Demo session response""" + """Demo session response - mirrors a real login response with user and tenant data""" session_id: str virtual_tenant_id: str demo_account_type: str @@ -26,6 +51,11 @@ class DemoSessionResponse(BaseModel): expires_at: datetime demo_config: Dict[str, Any] 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: from_attributes = True diff --git a/services/demo_session/app/core/config.py b/services/demo_session/app/core/config.py index 1f516864..d93bb0f1 100644 --- a/services/demo_session/app/core/config.py +++ b/services/demo_session/app/core/config.py @@ -35,6 +35,7 @@ class Settings(BaseServiceSettings): DEMO_SESSION_CLEANUP_INTERVAL_MINUTES: int = 60 # Demo account credentials (public) + # Contains complete user, tenant, and subscription data matching fixture files DEMO_ACCOUNTS: dict = { "professional": { "email": "demo.professional@panaderiaartesana.com", @@ -42,7 +43,22 @@ class Settings(BaseServiceSettings): "subdomain": "demo-artesana", "base_tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6", "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": { "email": "central@panaderiaartesana.es", @@ -51,6 +67,21 @@ class Settings(BaseServiceSettings): "base_tenant_id": "80000000-0000-4000-a000-000000000001", "subscription_tier": "enterprise", "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": [ { "name": "Madrid - Salamanca",