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:
@@ -0,0 +1,132 @@
|
||||
import type { MockUser } from '../stores/sessionStore'
|
||||
import { UserRole, WorkspaceType, ResultType } from '../domain/enums'
|
||||
|
||||
// ── Permission Strings ────────────────────────────────────────────────────────
|
||||
|
||||
export const Permission = {
|
||||
SUPPLY_VIEW: 'supply:view',
|
||||
SUPPLY_EDIT: 'supply:edit',
|
||||
DEMAND_VIEW: 'demand:view',
|
||||
DEMAND_EDIT: 'demand:edit',
|
||||
DEMAND_REQUEST_CONTACT: 'demand:request_contact',
|
||||
OPS_VIEW: 'ops:view',
|
||||
OPS_REVIEW: 'ops:review',
|
||||
OPS_APPROVE: 'ops:approve',
|
||||
FUTURE_SIGNAL_VIEW: 'future_signal:view',
|
||||
FUTURE_SIGNAL_REVIEW: 'future_signal:review',
|
||||
CONTACT_RELEASE_REQUEST: 'contact_release:request',
|
||||
CONTACT_RELEASE_APPROVE: 'contact_release:approve',
|
||||
} as const
|
||||
export type Permission = typeof Permission[keyof typeof Permission]
|
||||
|
||||
// ── Role → Permission Matrix ──────────────────────────────────────────────────
|
||||
|
||||
const ALL_PERMISSIONS = Object.values(Permission)
|
||||
|
||||
const ROLE_PERMISSIONS: Record<UserRole, Permission[]> = {
|
||||
[UserRole.SUPER_ADMIN]: ALL_PERMISSIONS,
|
||||
[UserRole.ORGANIZATION_ADMIN]: ALL_PERMISSIONS,
|
||||
[UserRole.PROPERTY_MANAGER]: [
|
||||
Permission.SUPPLY_VIEW,
|
||||
Permission.SUPPLY_EDIT,
|
||||
Permission.FUTURE_SIGNAL_VIEW,
|
||||
Permission.FUTURE_SIGNAL_REVIEW,
|
||||
Permission.CONTACT_RELEASE_APPROVE,
|
||||
],
|
||||
[UserRole.REVIEWER]: [
|
||||
Permission.OPS_VIEW,
|
||||
Permission.OPS_REVIEW,
|
||||
Permission.FUTURE_SIGNAL_VIEW,
|
||||
Permission.FUTURE_SIGNAL_REVIEW,
|
||||
],
|
||||
[UserRole.OWNER_VIEWER]: [
|
||||
Permission.SUPPLY_VIEW,
|
||||
Permission.CONTACT_RELEASE_APPROVE,
|
||||
],
|
||||
[UserRole.DEMAND_USER]: [
|
||||
Permission.DEMAND_VIEW,
|
||||
Permission.DEMAND_EDIT,
|
||||
Permission.DEMAND_REQUEST_CONTACT,
|
||||
Permission.CONTACT_RELEASE_REQUEST,
|
||||
],
|
||||
}
|
||||
|
||||
// ── Role → Workspace Access ───────────────────────────────────────────────────
|
||||
|
||||
const WORKSPACE_ROLES: Record<WorkspaceType, UserRole[]> = {
|
||||
[WorkspaceType.SUPPLY]: [
|
||||
UserRole.SUPER_ADMIN,
|
||||
UserRole.ORGANIZATION_ADMIN,
|
||||
UserRole.PROPERTY_MANAGER,
|
||||
UserRole.OWNER_VIEWER,
|
||||
],
|
||||
[WorkspaceType.DEMAND]: [
|
||||
UserRole.SUPER_ADMIN,
|
||||
UserRole.ORGANIZATION_ADMIN,
|
||||
UserRole.DEMAND_USER,
|
||||
],
|
||||
[WorkspaceType.OPERATIONS]: [
|
||||
UserRole.SUPER_ADMIN,
|
||||
UserRole.ORGANIZATION_ADMIN,
|
||||
UserRole.REVIEWER,
|
||||
],
|
||||
}
|
||||
|
||||
// ── Core Functions ────────────────────────────────────────────────────────────
|
||||
|
||||
export function getPermissions(user: MockUser): Permission[] {
|
||||
return ROLE_PERMISSIONS[user.role] ?? []
|
||||
}
|
||||
|
||||
export function hasPermission(user: MockUser, permission: Permission): boolean {
|
||||
return getPermissions(user).includes(permission)
|
||||
}
|
||||
|
||||
export function canAccessWorkspace(user: MockUser, workspace: WorkspaceType): boolean {
|
||||
return WORKSPACE_ROLES[workspace].includes(user.role)
|
||||
}
|
||||
|
||||
export function getAccessibleWorkspaces(role: UserRole): WorkspaceType[] {
|
||||
return Object.entries(WORKSPACE_ROLES)
|
||||
.filter(([, roles]) => roles.includes(role))
|
||||
.map(([ws]) => ws as WorkspaceType)
|
||||
}
|
||||
|
||||
// ── Domain Permission Functions ───────────────────────────────────────────────
|
||||
|
||||
export function canViewProperty(
|
||||
user: MockUser,
|
||||
property: { organizationId: string; resultType: ResultType },
|
||||
): boolean {
|
||||
if (user.role === UserRole.SUPER_ADMIN) return true
|
||||
if (property.resultType === ResultType.VERIFIED_PORTFOLIO) {
|
||||
return property.organizationId === user.organizationId
|
||||
}
|
||||
if (property.resultType === ResultType.EXTERNAL_MARKET) return true
|
||||
if (property.resultType === ResultType.FUTURE_AVAILABILITY) {
|
||||
return hasPermission(user, Permission.FUTURE_SIGNAL_VIEW)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
export function canViewMatch(
|
||||
user: MockUser,
|
||||
match: { organizationId?: string },
|
||||
): boolean {
|
||||
if (user.role === UserRole.SUPER_ADMIN || user.role === UserRole.ORGANIZATION_ADMIN) return true
|
||||
if (user.role === UserRole.PROPERTY_MANAGER || user.role === UserRole.REVIEWER) return true
|
||||
if (user.role === UserRole.DEMAND_USER) return match.organizationId === user.organizationId
|
||||
return false // OWNER_VIEWER: only released matches — handled at component level
|
||||
}
|
||||
|
||||
export function canReviewFutureSignal(user: MockUser): boolean {
|
||||
return hasPermission(user, Permission.FUTURE_SIGNAL_REVIEW)
|
||||
}
|
||||
|
||||
export function canApproveContactRelease(user: MockUser): boolean {
|
||||
return hasPermission(user, Permission.CONTACT_RELEASE_APPROVE)
|
||||
}
|
||||
|
||||
export function canRequestContactRelease(user: MockUser): boolean {
|
||||
return hasPermission(user, Permission.CONTACT_RELEASE_REQUEST)
|
||||
}
|
||||
Reference in New Issue
Block a user