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:
+103
-2
@@ -1,31 +1,132 @@
|
||||
import { useSessionStore } from '../stores/sessionStore'
|
||||
import type { MockUser } from '../stores/sessionStore'
|
||||
import type { ItemResponse } from './types'
|
||||
import { UserRole } from '../domain/enums'
|
||||
import { UserRole, WorkspaceType } from '../domain/enums'
|
||||
import { getPermissions, getAccessibleWorkspaces } from '../lib/permissions'
|
||||
import type { Permission } from '../lib/permissions'
|
||||
|
||||
// Mock organizations for org switching
|
||||
const MOCK_ORGANIZATIONS: { id: string; name: string }[] = [
|
||||
{ id: 'org-wincasa', name: 'Wincasa AG' },
|
||||
{ id: 'org-mobimo', name: 'Mobimo Management AG' },
|
||||
{ id: 'org-ubs', name: 'UBS Asset Management RE' },
|
||||
]
|
||||
|
||||
// Demo user presets per role
|
||||
const DEMO_USERS: Record<UserRole, MockUser> = {
|
||||
[UserRole.SUPER_ADMIN]: {
|
||||
id: 'user-super',
|
||||
email: 'super@ideal-sharing.ch',
|
||||
name: 'Super Admin',
|
||||
role: UserRole.SUPER_ADMIN,
|
||||
organizationId: 'org-wincasa',
|
||||
organizationName: 'Wincasa AG',
|
||||
allowedWorkspaces: [WorkspaceType.SUPPLY, WorkspaceType.DEMAND, WorkspaceType.OPERATIONS],
|
||||
},
|
||||
[UserRole.ORGANIZATION_ADMIN]: {
|
||||
id: 'user-001',
|
||||
email: 'admin@ideal-sharing.ch',
|
||||
name: 'Admin User',
|
||||
role: UserRole.ORGANIZATION_ADMIN,
|
||||
organizationId: 'org-wincasa',
|
||||
organizationName: 'Wincasa AG',
|
||||
allowedWorkspaces: [WorkspaceType.SUPPLY, WorkspaceType.DEMAND, WorkspaceType.OPERATIONS],
|
||||
},
|
||||
[UserRole.PROPERTY_MANAGER]: {
|
||||
id: 'user-pm',
|
||||
email: 'pm@ideal-sharing.ch',
|
||||
name: 'Property Manager',
|
||||
role: UserRole.PROPERTY_MANAGER,
|
||||
organizationId: 'org-wincasa',
|
||||
organizationName: 'Wincasa AG',
|
||||
allowedWorkspaces: getAccessibleWorkspaces(UserRole.PROPERTY_MANAGER),
|
||||
},
|
||||
[UserRole.REVIEWER]: {
|
||||
id: 'user-rev',
|
||||
email: 'reviewer@ideal-sharing.ch',
|
||||
name: 'Reviewer',
|
||||
role: UserRole.REVIEWER,
|
||||
organizationId: 'org-wincasa',
|
||||
organizationName: 'Wincasa AG',
|
||||
allowedWorkspaces: getAccessibleWorkspaces(UserRole.REVIEWER),
|
||||
},
|
||||
[UserRole.OWNER_VIEWER]: {
|
||||
id: 'user-ov',
|
||||
email: 'owner@ideal-sharing.ch',
|
||||
name: 'Owner Viewer',
|
||||
role: UserRole.OWNER_VIEWER,
|
||||
organizationId: 'org-wincasa',
|
||||
organizationName: 'Wincasa AG',
|
||||
allowedWorkspaces: getAccessibleWorkspaces(UserRole.OWNER_VIEWER),
|
||||
},
|
||||
[UserRole.DEMAND_USER]: {
|
||||
id: 'user-dem',
|
||||
email: 'demand@ideal-sharing.ch',
|
||||
name: 'Demand User',
|
||||
role: UserRole.DEMAND_USER,
|
||||
organizationId: 'org-mobimo',
|
||||
organizationName: 'Mobimo Management AG',
|
||||
allowedWorkspaces: getAccessibleWorkspaces(UserRole.DEMAND_USER),
|
||||
},
|
||||
}
|
||||
|
||||
export const authService = {
|
||||
async getCurrentUser(): Promise<ItemResponse<MockUser | null>> {
|
||||
const data = useSessionStore.getState().currentUser
|
||||
return { data }
|
||||
},
|
||||
|
||||
async getCurrentOrganization(): Promise<ItemResponse<{ id: string; name: string } | null>> {
|
||||
const { activeOrganizationId } = useSessionStore.getState()
|
||||
const org = MOCK_ORGANIZATIONS.find((o) => o.id === activeOrganizationId) ?? null
|
||||
return { data: org }
|
||||
},
|
||||
|
||||
async login(email: string, _password: string): Promise<ItemResponse<MockUser>> {
|
||||
const user: MockUser = {
|
||||
const existing = Object.values(DEMO_USERS).find((u) => u.email === email)
|
||||
const user: MockUser = existing ?? {
|
||||
id: 'user-001',
|
||||
email,
|
||||
name: 'Admin User',
|
||||
role: UserRole.ORGANIZATION_ADMIN,
|
||||
organizationId: 'org-wincasa',
|
||||
organizationName: 'Wincasa AG',
|
||||
allowedWorkspaces: [WorkspaceType.SUPPLY, WorkspaceType.DEMAND, WorkspaceType.OPERATIONS],
|
||||
}
|
||||
useSessionStore.getState().login(user)
|
||||
return { data: user }
|
||||
},
|
||||
|
||||
async logout(): Promise<ItemResponse<void>> {
|
||||
useSessionStore.getState().logout()
|
||||
return { data: undefined }
|
||||
},
|
||||
|
||||
async isAuthenticated(): Promise<ItemResponse<boolean>> {
|
||||
const data = useSessionStore.getState().isAuthenticated
|
||||
return { data }
|
||||
},
|
||||
|
||||
async switchDemoRole(role: UserRole): Promise<ItemResponse<MockUser>> {
|
||||
const user = DEMO_USERS[role]
|
||||
useSessionStore.getState().login(user)
|
||||
return { data: user }
|
||||
},
|
||||
|
||||
async switchOrganization(organizationId: string): Promise<ItemResponse<void>> {
|
||||
const org = MOCK_ORGANIZATIONS.find((o) => o.id === organizationId)
|
||||
if (org) {
|
||||
const state = useSessionStore.getState()
|
||||
if (state.currentUser) {
|
||||
state.login({ ...state.currentUser, organizationId: org.id, organizationName: org.name })
|
||||
} else {
|
||||
state.switchOrganization(organizationId)
|
||||
}
|
||||
}
|
||||
return { data: undefined }
|
||||
},
|
||||
|
||||
async getPermissions(user: MockUser): Promise<ItemResponse<Permission[]>> {
|
||||
return { data: getPermissions(user) }
|
||||
},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user