feat: F002 authentication & session layer

Add permission matrix, ProtectedRoute, RoleGuard, PermissionGate,
DemoRoleSwitcher, OrganizationSwitcher, AccessDenied, SessionExpired,
and institutional LoginScreen. Wire workspace-level route protection
into App.tsx; sidebar filters tabs by allowedWorkspaces.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Sutter
2026-05-15 12:01:46 +02:00
parent 9a2476c58a
commit 7bed2fec86
15 changed files with 852 additions and 29 deletions
+28 -3
View File
@@ -1,5 +1,5 @@
import { create } from 'zustand'
import { UserRole } from '../domain/enums'
import { UserRole, WorkspaceType } from '../domain/enums'
export interface MockUser {
id: string
@@ -8,15 +8,27 @@ export interface MockUser {
role: UserRole
organizationId: string
organizationName: string
allowedWorkspaces: WorkspaceType[]
}
export const SessionStatus = {
UNAUTHENTICATED: 'unauthenticated',
AUTHENTICATED: 'authenticated',
EXPIRED: 'expired',
RESTRICTED: 'restricted',
ONBOARDING: 'onboarding',
} as const
export type SessionStatus = typeof SessionStatus[keyof typeof SessionStatus]
interface SessionState {
currentUser: MockUser | null
activeOrganizationId: string | null
isAuthenticated: boolean
sessionStatus: SessionStatus
login: (user: MockUser) => void
logout: () => void
switchOrganization: (organizationId: string) => void
setSessionStatus: (status: SessionStatus) => void
}
const mockUser: MockUser = {
@@ -26,13 +38,26 @@ const mockUser: MockUser = {
role: UserRole.ORGANIZATION_ADMIN,
organizationId: 'org-wincasa',
organizationName: 'Wincasa AG',
allowedWorkspaces: [WorkspaceType.SUPPLY, WorkspaceType.DEMAND, WorkspaceType.OPERATIONS],
}
export const useSessionStore = create<SessionState>((set) => ({
currentUser: mockUser,
activeOrganizationId: mockUser.organizationId,
isAuthenticated: true,
login: (user) => set({ currentUser: user, activeOrganizationId: user.organizationId, isAuthenticated: true }),
logout: () => set({ currentUser: null, activeOrganizationId: null, isAuthenticated: false }),
sessionStatus: SessionStatus.AUTHENTICATED,
login: (user) => set({
currentUser: user,
activeOrganizationId: user.organizationId,
isAuthenticated: true,
sessionStatus: SessionStatus.AUTHENTICATED,
}),
logout: () => set({
currentUser: null,
activeOrganizationId: null,
isAuthenticated: false,
sessionStatus: SessionStatus.UNAUTHENTICATED,
}),
switchOrganization: (organizationId) => set({ activeOrganizationId: organizationId }),
setSessionStatus: (status) => set({ sessionStatus: status }),
}))