feat(rbac): unified session store, deny-by-default guards, and tenant isolation

This commit is contained in:
ochenstarik 2026-09-11 15:00:40 +07:00
parent ffc5646ff5
commit fcabd60523
8 changed files with 440 additions and 85 deletions

View file

@ -1,4 +1,5 @@
import React from 'react';
import { useSessionStore } from '../../core/session/sessionStore';
import { useDemoStore } from '../../stores/demo';
import { SYSTEM_ROLES, SystemRoleId } from '../../core/permissions/types';
import { BUSINESS_PROFILES } from '../../core/profiles/profiles';
@ -19,7 +20,8 @@ export const RouteGuard: React.FC<RouteGuardProps> = ({
requiredCapability,
children,
}) => {
const { currentRoleId, currentProfileId, stateOverride } = useDemoStore();
const { currentRoleId, currentProfileId, capabilities, hasModulePermission } = useSessionStore();
const { stateOverride } = useDemoStore();
// 1. Check Demo State Override
if (stateOverride === 'permission_denied') {
@ -34,29 +36,26 @@ export const RouteGuard: React.FC<RouteGuardProps> = ({
throw new Error(`[Демо-ошибка]: Имитация сбоя при загрузке раздела «${moduleName || module}».`);
}
// 2. Check RBAC Role permissions
// 2. Strict deny-by-default role permission check
const role = SYSTEM_ROLES[currentRoleId as SystemRoleId];
if (role) {
const isSuperRole = role.id === 'owner' || role.id === 'administrator' || role.allowedModules.includes('*');
if (!isSuperRole && !role.allowedModules.includes(module)) {
return (
<ForbiddenPage
moduleName={moduleName || module}
requiredPermission={`Доступ к модулю «${module}» для роли «${role.label}»`}
/>
);
}
if (!role || !hasModulePermission(module)) {
return (
<ForbiddenPage
moduleName={moduleName || module}
requiredPermission={`Доступ к модулю «${module}» для роли «${role?.label || 'Не определена'}»`}
/>
);
}
// 3. Check Business Profile Capabilities
const profile = BUSINESS_PROFILES[currentProfileId];
if (profile && requiredCapability) {
const isCapabilityEnabled = profile.capabilities[requiredCapability];
if (requiredCapability) {
const isCapabilityEnabled = capabilities ? capabilities[requiredCapability] : profile?.capabilities[requiredCapability];
if (isCapabilityEnabled === false) {
return (
<ForbiddenPage
moduleName={moduleName || module}
requiredPermission={`Возможность «${requiredCapability}» в бизнес-профиле «${profile.name}»`}
requiredPermission={`Возможность «${requiredCapability}» в бизнес-профиле «${profile?.name || currentProfileId}»`}
/>
);
}

View file

@ -10,6 +10,7 @@ import {
PRESET_TENANTS,
TenantContextValue,
} from './tenantContext';
import { useSessionStore } from '../../core/session/sessionStore';
export interface TenantProviderProps {
children: React.ReactNode;
@ -20,15 +21,24 @@ export const TenantProvider: React.FC<TenantProviderProps> = ({
children,
initialTenantId,
}) => {
const sessionTenantId = useSessionStore((s) => s.activeTenantId);
const [currentTenantId, setCurrentTenantId] = useState<string>(() => {
return initialTenantId || getActiveGlobalTenantId() || DEFAULT_TENANT_ID;
return initialTenantId || sessionTenantId || getActiveGlobalTenantId() || DEFAULT_TENANT_ID;
});
const switchTenant = useCallback((tenantId: string) => {
setCurrentTenantId(tenantId);
setActiveGlobalTenantId(tenantId);
useSessionStore.getState().setActiveTenantId(tenantId);
}, []);
useEffect(() => {
if (sessionTenantId && sessionTenantId !== currentTenantId) {
setCurrentTenantId(sessionTenantId);
setActiveGlobalTenantId(sessionTenantId);
}
}, [sessionTenantId, currentTenantId]);
useEffect(() => {
setActiveGlobalTenantId(currentTenantId);
}, [currentTenantId]);

View file

@ -1,6 +1,6 @@
import React, { useState } from 'react';
import { NavLink } from 'react-router-dom';
import { useDemoStore } from '../../mock/store';
import { useSessionStore } from '../../core/session/sessionStore';
import { useMetadata } from '../../services/hooks/useMetadata';
import { SYSTEM_MODULES } from '../../core/modules/registry';
import { SYSTEM_ROLES } from '../../core/permissions/types';
@ -55,19 +55,13 @@ export interface SidebarProps {
export const Sidebar: React.FC<SidebarProps> = ({ isTabletCompact = false }) => {
const [userCollapsed, setUserCollapsed] = useState<boolean | null>(null);
const collapsed = userCollapsed !== null ? userCollapsed : isTabletCompact;
const { currentRoleId } = useDemoStore();
const { currentRoleId, hasModulePermission } = useSessionStore();
const { entities } = useMetadata();
const customEntities = entities.filter((e) => !e.isSystem);
const role = SYSTEM_ROLES[currentRoleId];
// Filter modules by role permissions (industry profile does not hide modules)
const visibleModules = SYSTEM_MODULES.filter((m) => {
// Role check
if (!role.allowedModules.includes('*') && !role.allowedModules.includes(m.id)) {
return false;
}
return true;
});
// Filter modules by role permissions (strict deny-by-default via sessionStore)
const visibleModules = SYSTEM_MODULES.filter((m) => hasModulePermission(m.id));
return (
<aside

View file

@ -0,0 +1,234 @@
import { create } from 'zustand';
import { SystemRoleId, SYSTEM_ROLES } from '../permissions/types';
import { CapabilityId } from '../capabilities/types';
import { BUSINESS_PROFILES } from '../profiles/profiles';
import { BusinessProfile } from '../profiles/types';
import { setActiveGlobalTenantId, getActiveGlobalTenantId } from '../../services/contracts/context';
export interface SessionState {
activeTenantId: string;
activeOrgId: string;
currentUserId: string;
currentRoleId: SystemRoleId;
currentProfileId: string;
activeLocation: string;
capabilities: Record<CapabilityId, boolean>;
// Actions
setActiveTenantId: (tenantId: string) => void;
setActiveOrgId: (orgId: string) => void;
setCurrentUserId: (userId: string) => void;
setCurrentRoleId: (roleId: SystemRoleId) => void;
setCurrentProfileId: (profileId: string) => void;
setActiveLocation: (location: string) => void;
setCapabilities: (capabilities: Record<CapabilityId, boolean>) => void;
toggleCapability: (id: CapabilityId) => void;
resetCapabilitiesToProfile: () => void;
resetSession: () => void;
// RBAC & Permission helpers (Strict deny-by-default)
hasModulePermission: (moduleId: string) => boolean;
getActiveProfile: () => BusinessProfile;
getActiveCapabilities: () => Record<CapabilityId, boolean>;
}
const DEFAULT_PROFILE_ID = 'restaurant_hall_delivery';
const DEFAULT_ROLE_ID: SystemRoleId = 'owner';
const DEFAULT_TENANT_ID = 'tenant_main';
const DEFAULT_ORG_ID = 'org_main';
const DEFAULT_USER_ID = 'user_admin';
function getInitialProfileId(): string {
if (typeof window !== 'undefined') {
const saved = localStorage.getItem('krona_profile_id');
if (saved && BUSINESS_PROFILES[saved]) return saved;
}
return DEFAULT_PROFILE_ID;
}
function getInitialRoleId(): SystemRoleId {
if (typeof window !== 'undefined') {
const saved = localStorage.getItem('krona_role_id') as SystemRoleId;
if (saved && SYSTEM_ROLES[saved]) return saved;
}
return DEFAULT_ROLE_ID;
}
function getInitialTenantId(): string {
if (typeof window !== 'undefined') {
const saved = localStorage.getItem('krona_tenant_id') || localStorage.getItem('krona_active_tenant_id');
if (saved) return saved;
}
return getActiveGlobalTenantId() || DEFAULT_TENANT_ID;
}
function getInitialOrgId(): string {
if (typeof window !== 'undefined') {
const saved = localStorage.getItem('krona_org_id') || localStorage.getItem('krona_active_org_id');
if (saved) return saved;
}
return DEFAULT_ORG_ID;
}
function getInitialUserId(): string {
if (typeof window !== 'undefined') {
const saved = localStorage.getItem('krona_user_id') || localStorage.getItem('krona_current_user_id');
if (saved) return saved;
}
return DEFAULT_USER_ID;
}
function getInitialLocation(profileId: string): string {
if (typeof window !== 'undefined') {
const saved = localStorage.getItem('krona_location') || localStorage.getItem('krona_active_location');
if (saved) return saved;
}
const prof = BUSINESS_PROFILES[profileId] || BUSINESS_PROFILES[DEFAULT_PROFILE_ID];
return prof.locations[0] || 'Горский 1/1';
}
function getInitialCapabilities(profileId: string): Record<CapabilityId, boolean> {
const prof = BUSINESS_PROFILES[profileId] || BUSINESS_PROFILES[DEFAULT_PROFILE_ID];
return { ...prof.capabilities };
}
export const useSessionStore = create<SessionState>((set, get) => {
const initialProfileId = getInitialProfileId();
const initialTenantId = getInitialTenantId();
// Sync global tenant id on startup
setActiveGlobalTenantId(initialTenantId);
return {
activeTenantId: initialTenantId,
activeOrgId: getInitialOrgId(),
currentUserId: getInitialUserId(),
currentRoleId: getInitialRoleId(),
currentProfileId: initialProfileId,
activeLocation: getInitialLocation(initialProfileId),
capabilities: getInitialCapabilities(initialProfileId),
setActiveTenantId: (activeTenantId: string) => {
if (typeof window !== 'undefined') {
localStorage.setItem('krona_tenant_id', activeTenantId);
localStorage.setItem('krona_active_tenant_id', activeTenantId);
}
setActiveGlobalTenantId(activeTenantId);
set({ activeTenantId });
},
setActiveOrgId: (activeOrgId: string) => {
if (typeof window !== 'undefined') {
localStorage.setItem('krona_org_id', activeOrgId);
localStorage.setItem('krona_active_org_id', activeOrgId);
}
set({ activeOrgId });
},
setCurrentUserId: (currentUserId: string) => {
if (typeof window !== 'undefined') {
localStorage.setItem('krona_user_id', currentUserId);
localStorage.setItem('krona_current_user_id', currentUserId);
}
set({ currentUserId });
},
setCurrentRoleId: (currentRoleId: SystemRoleId) => {
if (typeof window !== 'undefined') {
localStorage.setItem('krona_role_id', currentRoleId);
}
set({ currentRoleId });
},
setCurrentProfileId: (currentProfileId: string) => {
if (typeof window !== 'undefined') {
localStorage.setItem('krona_profile_id', currentProfileId);
}
const profile = BUSINESS_PROFILES[currentProfileId] || BUSINESS_PROFILES[DEFAULT_PROFILE_ID];
const newLoc = profile.locations[0] || 'Главная точка';
if (typeof window !== 'undefined') {
localStorage.setItem('krona_location', newLoc);
}
set({
currentProfileId,
capabilities: { ...profile.capabilities },
activeLocation: newLoc,
});
},
setActiveLocation: (activeLocation: string) => {
if (typeof window !== 'undefined') {
localStorage.setItem('krona_location', activeLocation);
localStorage.setItem('krona_active_location', activeLocation);
}
set({ activeLocation });
},
setCapabilities: (capabilities: Record<CapabilityId, boolean>) => {
set({ capabilities });
},
toggleCapability: (id: CapabilityId) => {
set((state) => ({
capabilities: {
...state.capabilities,
[id]: !state.capabilities[id],
},
}));
},
resetCapabilitiesToProfile: () => {
const state = get();
const profile = BUSINESS_PROFILES[state.currentProfileId] || BUSINESS_PROFILES[DEFAULT_PROFILE_ID];
set({ capabilities: { ...profile.capabilities } });
},
resetSession: () => {
const defaultProf = BUSINESS_PROFILES[DEFAULT_PROFILE_ID];
setActiveGlobalTenantId(DEFAULT_TENANT_ID);
if (typeof window !== 'undefined') {
localStorage.removeItem('krona_tenant_id');
localStorage.removeItem('krona_active_tenant_id');
localStorage.removeItem('krona_org_id');
localStorage.removeItem('krona_active_org_id');
localStorage.removeItem('krona_user_id');
localStorage.removeItem('krona_current_user_id');
localStorage.removeItem('krona_role_id');
localStorage.removeItem('krona_profile_id');
localStorage.removeItem('krona_location');
}
set({
activeTenantId: DEFAULT_TENANT_ID,
activeOrgId: DEFAULT_ORG_ID,
currentUserId: DEFAULT_USER_ID,
currentRoleId: DEFAULT_ROLE_ID,
currentProfileId: DEFAULT_PROFILE_ID,
activeLocation: defaultProf.locations[0] || 'Горский 1/1',
capabilities: { ...defaultProf.capabilities },
});
},
// Strict deny-by-default RBAC check
hasModulePermission: (moduleId: string) => {
const state = get();
const role = SYSTEM_ROLES[state.currentRoleId];
if (!role) return false;
// Super roles or full wildcard
if (role.id === 'owner' || role.id === 'administrator' || role.allowedModules.includes('*')) {
return true;
}
return role.allowedModules.includes(moduleId);
},
getActiveProfile: () => {
const state = get();
return BUSINESS_PROFILES[state.currentProfileId] || BUSINESS_PROFILES[DEFAULT_PROFILE_ID];
},
getActiveCapabilities: () => {
return get().capabilities;
},
};
});

View file

@ -0,0 +1,127 @@
import { describe, it, expect, beforeEach } from 'vitest';
import React from 'react';
import { render, screen, act } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import '@testing-library/jest-dom/vitest';
import { RouteGuard } from '../../app/guards/RouteGuard';
import { Sidebar } from '../../app/shell/Sidebar';
import { useSessionStore } from '../../core/session/sessionStore';
import { useDemoStore } from '../../stores/demo';
import { createRequestContext } from '../../services/contracts/context';
describe('RBAC & Session Integrity Suite (F01)', () => {
beforeEach(() => {
localStorage.clear();
act(() => {
useSessionStore.getState().resetSession();
useDemoStore.getState().resetDemoControls();
});
});
const renderWithRouter = (ui: React.ReactElement) => {
return render(<MemoryRouter>{ui}</MemoryRouter>);
};
it('1. Hidden in navigation: restricted modules are omitted from Sidebar for restricted roles', () => {
// Cook role is only permitted to access 'operations'
act(() => {
useSessionStore.getState().setCurrentRoleId('cook');
});
renderWithRouter(<Sidebar />);
// 'Операции' should be visible
expect(screen.getByText('Операции')).toBeInTheDocument();
// Restricted modules must NOT be rendered in Sidebar
expect(screen.queryByText('Финансы')).not.toBeInTheDocument();
expect(screen.queryByText('Клиенты и CRM')).not.toBeInTheDocument();
expect(screen.queryByText('Складской учёт')).not.toBeInTheDocument();
expect(screen.queryByText('Персонал и HR')).not.toBeInTheDocument();
expect(screen.queryByText('Маркетинг')).not.toBeInTheDocument();
});
it('2. Direct navigation to forbidden URL renders 403 Forbidden with strict deny-by-default', () => {
// Cashier role only has ['operations', 'sales', 'crm'], lacking 'finance'
act(() => {
useSessionStore.getState().setCurrentRoleId('cashier');
});
renderWithRouter(
<RouteGuard module="finance" moduleName="Финансы и казначейство">
<div data-testid="finance-secret-data">Конфиденциальные финансовые данные</div>
</RouteGuard>
);
// Protected content must not be present
expect(screen.queryByTestId('finance-secret-data')).not.toBeInTheDocument();
// 403 Forbidden page is rendered
expect(screen.getByTestId('forbidden-page')).toBeInTheDocument();
expect(screen.getByText(/403 Доступ ограничен/i)).toBeInTheDocument();
expect(screen.getByText(/Нет прав на просмотр «Финансы и казначейство»/i)).toBeInTheDocument();
});
it('3. Immediate reactivity: switching role immediately unblocks/blocks RouteGuard without delay', () => {
act(() => {
useSessionStore.getState().setCurrentRoleId('cook');
});
const { rerender } = renderWithRouter(
<RouteGuard module="crm" moduleName="Клиенты и CRM">
<div data-testid="crm-content">CRM База клиентов</div>
</RouteGuard>
);
// Cook is blocked
expect(screen.queryByTestId('crm-content')).not.toBeInTheDocument();
expect(screen.getByTestId('forbidden-page')).toBeInTheDocument();
// Immediately switch role to 'owner'
act(() => {
useSessionStore.getState().setCurrentRoleId('owner');
});
rerender(
<MemoryRouter>
<RouteGuard module="crm" moduleName="Клиенты и CRM">
<div data-testid="crm-content">CRM База клиентов</div>
</RouteGuard>
</MemoryRouter>
);
// Now immediately accessible
expect(screen.getByTestId('crm-content')).toBeInTheDocument();
expect(screen.queryByTestId('forbidden-page')).not.toBeInTheDocument();
});
it('4. Multi-tenant and Organization isolation: switching tenant updates context and isolates session', () => {
// Default tenant is 'tenant_main'
expect(useSessionStore.getState().activeTenantId).toBe('tenant_main');
const defaultContext = createRequestContext();
expect(defaultContext.tenantId).toBe('tenant_main');
// Switch tenant to 'tenant_alpha'
act(() => {
useSessionStore.getState().setActiveTenantId('tenant_alpha');
useSessionStore.getState().setActiveOrgId('org_alpha_north');
});
expect(useSessionStore.getState().activeTenantId).toBe('tenant_alpha');
expect(useSessionStore.getState().activeOrgId).toBe('org_alpha_north');
const alphaContext = createRequestContext();
expect(alphaContext.tenantId).toBe('tenant_alpha');
// Switch tenant to 'tenant_beta'
act(() => {
useSessionStore.getState().setActiveTenantId('tenant_beta');
});
expect(useSessionStore.getState().activeTenantId).toBe('tenant_beta');
const betaContext = createRequestContext();
expect(betaContext.tenantId).toBe('tenant_beta');
expect(betaContext.tenantId).not.toBe(alphaContext.tenantId);
});
});

View file

@ -338,6 +338,9 @@ const INITIAL_NOTIFICATIONS: AppNotification[] = [
},
];
import { useSessionStore } from '../core/session/sessionStore';
export { useSessionStore };
export const useDemoStore = create<DemoStore>((set, get) => ({
// Theme & Viewport defaults
theme:
@ -356,48 +359,27 @@ export const useDemoStore = create<DemoStore>((set, get) => ({
viewportMode: 'desktop',
setViewportMode: (viewportMode) => set({ viewportMode }),
// Profile & Role defaults
currentProfileId:
(typeof window !== 'undefined' && localStorage.getItem('krona_profile_id')) ||
'restaurant_hall_delivery',
// Profile & Role defaults (Delegated to canonical useSessionStore)
currentProfileId: useSessionStore.getState().currentProfileId,
setProfileId: (currentProfileId) => {
if (typeof window !== 'undefined') {
localStorage.setItem('krona_profile_id', currentProfileId);
}
const profile = BUSINESS_PROFILES[currentProfileId] || BUSINESS_PROFILES.restaurant_hall_delivery;
set({
currentProfileId,
customCapabilities: { ...profile.capabilities },
activeLocation: profile.locations[0] || 'Главная точка',
});
useDemoControlsStore.getState().setCurrentProfileId(currentProfileId);
useSessionStore.getState().setCurrentProfileId(currentProfileId);
},
currentRoleId:
(typeof window !== 'undefined' && (localStorage.getItem('krona_role_id') as SystemRoleId)) ||
'owner',
currentRoleId: useSessionStore.getState().currentRoleId,
setRoleId: (currentRoleId) => {
if (typeof window !== 'undefined') {
localStorage.setItem('krona_role_id', currentRoleId);
}
set({ currentRoleId });
useDemoControlsStore.getState().setCurrentRoleId(currentRoleId);
useSessionStore.getState().setCurrentRoleId(currentRoleId);
},
activeLocation: useSessionStore.getState().activeLocation,
setActiveLocation: (activeLocation) => {
useSessionStore.getState().setActiveLocation(activeLocation);
},
activeLocation: 'Горский 1/1',
setActiveLocation: (activeLocation) => set({ activeLocation }),
// Capabilities
customCapabilities: { ...BUSINESS_PROFILES.restaurant_hall_delivery.capabilities },
toggleCapability: (id) =>
set((state) => ({
customCapabilities: {
...state.customCapabilities,
[id]: !state.customCapabilities[id],
},
})),
// Capabilities (Delegated to canonical useSessionStore)
customCapabilities: useSessionStore.getState().capabilities,
toggleCapability: (id) => {
useSessionStore.getState().toggleCapability(id);
},
resetCapabilitiesToProfile: () => {
const state = get();
const profile = BUSINESS_PROFILES[state.currentProfileId] || BUSINESS_PROFILES.restaurant_hall_delivery;
set({ customCapabilities: { ...profile.capabilities } });
useSessionStore.getState().resetCapabilitiesToProfile();
},
// Panels
@ -574,10 +556,18 @@ export const useDemoStore = create<DemoStore>((set, get) => ({
// Getters
getActiveProfile: () => {
const s = get();
return BUSINESS_PROFILES[s.currentProfileId] || BUSINESS_PROFILES.restaurant_hall_delivery;
return useSessionStore.getState().getActiveProfile();
},
getActiveCapabilities: () => {
return get().customCapabilities;
return useSessionStore.getState().getActiveCapabilities();
},
}));
useSessionStore.subscribe((session) => {
useDemoStore.setState({
currentRoleId: session.currentRoleId,
currentProfileId: session.currentProfileId,
activeLocation: session.activeLocation,
customCapabilities: session.capabilities,
});
});

View file

@ -2,8 +2,7 @@ import React from 'react';
import { ShieldAlert, Home, RefreshCw } from 'lucide-react';
import { useNavigate } from 'react-router-dom';
import { Button } from '../../shared/ui/Button';
import { useDemoStore } from '../../stores/demo';
import { useDemoStore as useMockStore } from '../../mock/store';
import { useSessionStore } from '../../core/session/sessionStore';
export interface ForbiddenPageProps {
requiredPermission?: string;
@ -15,11 +14,10 @@ export const ForbiddenPage: React.FC<ForbiddenPageProps> = ({
moduleName,
}) => {
const navigate = useNavigate();
const { currentRoleId, setCurrentRoleId } = useDemoStore();
const { currentRoleId, setCurrentRoleId } = useSessionStore();
const handleSwitchToOwner = () => {
setCurrentRoleId('owner');
useMockStore.getState().setRoleId('owner');
};
return (

View file

@ -1,4 +1,6 @@
import { create } from 'zustand';
import { useSessionStore } from '../core/session/sessionStore';
import { SystemRoleId } from '../core/permissions/types';
export type DemoStateOverride = 'normal' | 'loading' | 'empty' | 'error' | 'permission_denied';
export type DemoErrorType = 'none' | 'network_error' | 'forbidden_403' | 'conflict_409' | 'validation_422';
@ -32,12 +34,8 @@ interface DemoState {
const DEFAULT_DEMO_DATE = '2026-09-10T10:00:00.000Z';
export const useDemoStore = create<DemoState>((set) => ({
currentProfileId:
(typeof window !== 'undefined' && localStorage.getItem('krona_profile_id')) ||
'multi-business',
currentRoleId:
(typeof window !== 'undefined' && localStorage.getItem('krona_role_id')) ||
'owner',
currentProfileId: useSessionStore.getState().currentProfileId,
currentRoleId: useSessionStore.getState().currentRoleId,
simulateLatency: false,
latencyMs: 300,
simulateErrors: false,
@ -48,15 +46,11 @@ export const useDemoStore = create<DemoState>((set) => ({
selectedScenario: 'retail-flow',
setCurrentProfileId: (profileId) => {
if (typeof window !== 'undefined') {
localStorage.setItem('krona_profile_id', profileId);
}
useSessionStore.getState().setCurrentProfileId(profileId);
set({ currentProfileId: profileId });
},
setCurrentRoleId: (roleId) => {
if (typeof window !== 'undefined') {
localStorage.setItem('krona_role_id', roleId);
}
useSessionStore.getState().setCurrentRoleId(roleId as SystemRoleId);
set({ currentRoleId: roleId });
},
setSimulateLatency: (enable) => set({ simulateLatency: enable }),
@ -73,10 +67,11 @@ export const useDemoStore = create<DemoState>((set) => ({
setStateOverride: (override) => set({ stateOverride: override }),
setShowMaturityBadges: (show) => set({ showMaturityBadges: show }),
setSelectedScenario: (scenario) => set({ selectedScenario: scenario }),
resetDemoControls: () =>
resetDemoControls: () => {
useSessionStore.getState().resetSession();
set({
currentProfileId: 'multi-business',
currentRoleId: 'owner',
currentProfileId: useSessionStore.getState().currentProfileId,
currentRoleId: useSessionStore.getState().currentRoleId,
simulateLatency: false,
latencyMs: 300,
simulateErrors: false,
@ -85,5 +80,13 @@ export const useDemoStore = create<DemoState>((set) => ({
stateOverride: 'normal',
showMaturityBadges: true,
selectedScenario: 'retail-flow',
}),
});
},
}));
useSessionStore.subscribe((session) => {
useDemoStore.setState({
currentRoleId: session.currentRoleId,
currentProfileId: session.currentProfileId,
});
});