access control by role plus by person in orgsettings

This commit is contained in:
Satyam-Rastogi
2026-05-14 18:13:42 +05:30
parent 69d81c78d0
commit d01e699250
4 changed files with 388 additions and 6 deletions
@@ -0,0 +1,293 @@
import React, { useState, useMemo } from 'react';
import ReactDOM from 'react-dom';
import { motion, AnimatePresence } from 'framer-motion';
import { X, Search, Shield, Users, Check } from 'lucide-react';
function StateChip({ label, variant }) {
const styles = {
role: 'bg-blue-50 dark:bg-blue-500/10 text-blue-600 dark:text-blue-400',
granted: 'bg-emerald-50 dark:bg-emerald-500/10 text-emerald-600 dark:text-emerald-400',
denied: 'bg-amber-50 dark:bg-amber-500/10 text-amber-600 dark:text-amber-400',
};
return (
<span className={`text-[10px] font-bold uppercase tracking-wider px-2 py-0.5 rounded-full ${styles[variant]}`}>
{label}
</span>
);
}
export default function AccessControlPersonModal({
isOpen, onClose,
moduleKey, moduleLabel, actions,
permissions, roles, members,
userPermissionOverrides,
onSetOverride, onRemoveOverride,
}) {
const [search, setSearch] = useState('');
const nonOwnerMembers = useMemo(() =>
(members || []).filter(m => m.roleKey !== 'OWNER' && m.status === 'active'),
[members]
);
const nonOwnerRoles = useMemo(() =>
(roles || []).filter(r => r.key !== 'OWNER'),
[roles]
);
const moduleOverrides = useMemo(() =>
(userPermissionOverrides || []).filter(o => o.moduleKey === moduleKey),
[userPermissionOverrides, moduleKey]
);
const getEffectiveAccess = (userId, roleKey, action) => {
const override = moduleOverrides.find(
o => o.userId === userId && o.action === action
);
if (override) return { access: override.effect === 'grant', source: override.effect === 'grant' ? 'granted' : 'denied' };
const roleActions = permissions[moduleKey]?.[roleKey] || [];
return { access: roleActions.includes(action), source: 'role' };
};
const handleToggle = (userId, roleKey, action) => {
const override = moduleOverrides.find(
o => o.userId === userId && o.action === action
);
if (override) {
onRemoveOverride(userId, moduleKey, action);
} else {
const roleActions = permissions[moduleKey]?.[roleKey] || [];
onSetOverride(userId, moduleKey, action, roleActions.includes(action) ? 'deny' : 'grant');
}
};
const handleClearModuleOverrides = (userId) => {
const toRemove = moduleOverrides.filter(o => o.userId === userId);
toRemove.forEach(o => onRemoveOverride(userId, moduleKey, o.action));
};
const filteredMembers = useMemo(() => {
const q = search.toLowerCase();
const list = nonOwnerMembers.filter(m =>
!q || m.name.toLowerCase().includes(q) || m.email.toLowerCase().includes(q) || m.roleKey.toLowerCase().includes(q)
);
return [...list].sort((a, b) => {
const aHas = moduleOverrides.some(o => o.userId === a.userId);
const bHas = moduleOverrides.some(o => o.userId === b.userId);
if (aHas && !bHas) return -1;
if (!aHas && bHas) return 1;
return 0;
});
}, [nonOwnerMembers, search, moduleOverrides]);
const overrideCount = useMemo(() => {
const userIds = new Set(moduleOverrides.map(o => o.userId));
return userIds.size;
}, [moduleOverrides]);
if (!isOpen) return null;
return ReactDOM.createPortal(
<AnimatePresence>
{isOpen && (
<div className="fixed inset-0 z-[90] flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm" onClick={onClose}>
<motion.div
initial={{ scale: 0.96, opacity: 0, y: 8 }}
animate={{ scale: 1, opacity: 1, y: 0 }}
exit={{ scale: 0.96, opacity: 0, y: 8 }}
transition={{ duration: 0.18 }}
className="bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-white/10 rounded-2xl shadow-2xl w-full max-w-lg flex flex-col max-h-[85vh]"
onClick={e => e.stopPropagation()}
>
{/* Header */}
<div className="flex items-center justify-between px-5 py-4 border-b border-zinc-100 dark:border-white/[0.07] shrink-0">
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-xl bg-emerald-50 dark:bg-emerald-500/10 flex items-center justify-center">
<Shield size={15} className="text-emerald-600 dark:text-emerald-400" />
</div>
<div>
<h2 className="font-bold text-zinc-900 dark:text-white text-sm leading-none">{moduleLabel} Access</h2>
<p className="text-[11px] text-zinc-400 mt-0.5">
{overrideCount > 0
? `${overrideCount} person-level override${overrideCount !== 1 ? 's' : ''}`
: 'No person-level overrides'}
</p>
</div>
</div>
<button
onClick={onClose}
className="p-1.5 rounded-lg text-zinc-400 hover:text-zinc-600 dark:hover:text-zinc-200 hover:bg-zinc-100 dark:hover:bg-white/10 transition-colors"
>
<X size={16} />
</button>
</div>
<div className="overflow-y-auto flex-1 px-5 py-4 space-y-5">
{/* By Role */}
<div>
<p className="text-[11px] font-bold text-zinc-400 dark:text-zinc-500 uppercase tracking-widest mb-3">By Role</p>
<div className="space-y-2">
{nonOwnerRoles.map(role => {
const roleActions = permissions[moduleKey]?.[role.key] || [];
const grantedCount = roleActions.length;
return (
<div key={role.key} className="flex items-center gap-3 p-3 rounded-xl border border-zinc-100 dark:border-white/[0.07] bg-zinc-50 dark:bg-zinc-800/40">
<div className="w-8 h-8 rounded-lg flex items-center justify-center shrink-0" style={{ backgroundColor: `${role.color}15` }}>
<Users size={14} style={{ color: role.color }} />
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-semibold text-zinc-800 dark:text-zinc-100">{role.name}</p>
<div className="flex flex-wrap gap-1 mt-1">
{actions.map(action => {
const has = roleActions.includes(action);
return (
<span key={action} className={`text-[9px] font-bold capitalize px-1.5 py-0.5 rounded ${
has
? 'bg-emerald-50 dark:bg-emerald-500/10 text-emerald-600 dark:text-emerald-400'
: 'bg-zinc-100 dark:bg-white/5 text-zinc-400 dark:text-zinc-600'
}`}>
{has && <Check size={7} className="inline mr-0.5 -mt-0.5" />}
{action}
</span>
);
})}
</div>
</div>
<span className="text-[10px] font-mono text-zinc-400 shrink-0">{grantedCount}/{actions.length}</span>
</div>
);
})}
</div>
</div>
{/* Divider */}
<div className="relative">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-zinc-100 dark:border-white/[0.07]" />
</div>
<div className="relative flex justify-center">
<span className="px-3 bg-white dark:bg-zinc-900 text-[11px] font-bold text-zinc-400 uppercase tracking-widest">By Person</span>
</div>
</div>
{/* Search */}
<div className="relative">
<Search size={13} className="absolute left-3 top-1/2 -translate-y-1/2 text-zinc-400 pointer-events-none" />
<input
value={search}
onChange={e => setSearch(e.target.value)}
placeholder="Search by name, email, or role..."
className="w-full pl-8 pr-4 py-2 rounded-xl border border-zinc-200 dark:border-zinc-700 bg-white dark:bg-zinc-800 text-zinc-900 dark:text-zinc-100 text-sm outline-none focus:ring-2 focus:ring-emerald-500/30 focus:border-emerald-500 transition-shadow"
/>
</div>
{/* Person list */}
<div className="space-y-1.5">
{filteredMembers.map(member => {
const role = roles.find(r => r.key === member.roleKey);
const hasOverrides = moduleOverrides.some(o => o.userId === member.userId);
const initials = member.name.split(' ').map(w => w[0]).join('').slice(0, 2).toUpperCase();
return (
<div
key={member.id}
className={`p-3 rounded-xl border transition-colors ${
hasOverrides
? 'border-emerald-200 dark:border-emerald-500/20 bg-emerald-50/30 dark:bg-emerald-500/5'
: 'border-zinc-100 dark:border-white/[0.05] bg-transparent hover:bg-zinc-50 dark:hover:bg-white/[0.03]'
}`}
>
<div className="flex items-center gap-3 mb-2">
<div
className="w-8 h-8 rounded-full flex items-center justify-center shrink-0 text-[11px] font-bold"
style={{ backgroundColor: `${role?.color || '#3b82f6'}15`, color: role?.color || '#3b82f6' }}
>
{initials}
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 flex-wrap">
<span className="text-sm font-semibold text-zinc-800 dark:text-zinc-100 truncate">{member.name}</span>
{hasOverrides && <StateChip label={moduleOverrides.filter(o => o.userId === member.userId && o.effect === 'deny').length > 0 ? 'Has Overrides' : 'Overridden'} variant="granted" />}
</div>
<div className="flex items-center gap-2 mt-0.5">
<span className="text-[11px] text-zinc-400">{member.email}</span>
<span className="text-[10px] font-bold px-1.5 py-0.5 rounded-full border border-zinc-200 dark:border-white/10 text-zinc-500">
{role?.name || member.roleKey}
</span>
</div>
</div>
{hasOverrides && (
<button
type="button"
onClick={() => handleClearModuleOverrides(member.userId)}
className="text-[9px] font-semibold text-red-500 hover:text-red-600 dark:hover:text-red-400 transition-colors shrink-0 px-2 py-1 rounded-lg hover:bg-red-50 dark:hover:bg-red-500/10"
>
Clear
</button>
)}
</div>
<div className="flex flex-wrap gap-1.5 pl-11">
{actions.map(action => {
const eff = getEffectiveAccess(member.userId, member.roleKey, action);
const hasOverride = eff.source !== 'role';
return (
<button
key={action}
type="button"
onClick={() => handleToggle(member.userId, member.roleKey, action)}
className={`group px-2.5 py-1.5 rounded-lg text-[11px] font-semibold capitalize border transition-all duration-150 ${
eff.access
? hasOverride
? 'bg-emerald-50 dark:bg-emerald-500/10 border-emerald-300 dark:border-emerald-500/30 text-emerald-700 dark:text-emerald-400 ring-1 ring-emerald-200 dark:ring-emerald-500/20'
: 'bg-emerald-50 dark:bg-emerald-500/10 border-emerald-200 dark:border-emerald-500/20 text-emerald-600 dark:text-emerald-400'
: hasOverride
? 'bg-amber-50 dark:bg-amber-500/10 border-amber-300 dark:border-amber-500/30 text-amber-700 dark:text-amber-400 ring-1 ring-amber-200 dark:ring-amber-500/20'
: 'bg-zinc-50 dark:bg-white/[0.03] border-zinc-200 dark:border-white/10 text-zinc-400 dark:text-zinc-600 hover:text-zinc-600 dark:hover:text-zinc-400'
}`}
title={`${action}${eff.source === 'role' ? 'Via Role' : eff.source === 'granted' ? 'Granted' : 'Denied'}${hasOverride ? ' (click to revert)' : ' (click to override)'}`}
>
{eff.access && <Check size={9} className="inline mr-0.5 -mt-0.5" />}
{action}
{hasOverride && (
<span className={`ml-1 text-[8px] font-bold uppercase px-1 py-0 rounded ${
eff.source === 'granted'
? 'bg-emerald-100 dark:bg-emerald-500/20 text-emerald-600 dark:text-emerald-400'
: 'bg-amber-100 dark:bg-amber-500/20 text-amber-600 dark:text-amber-400'
}`}>
{eff.source === 'granted' ? 'Granted' : 'Denied'}
</span>
)}
</button>
);
})}
</div>
</div>
);
})}
{filteredMembers.length === 0 && (
<p className="text-center text-sm text-zinc-400 py-6">No members found</p>
)}
</div>
</div>
{/* Footer */}
<div className="px-5 py-3 border-t border-zinc-100 dark:border-white/[0.07] shrink-0 flex items-center justify-between">
<p className="text-xs text-zinc-400">
Person overrides take priority over role permissions
</p>
<button
onClick={onClose}
className="px-4 py-2 rounded-xl bg-emerald-600 hover:bg-emerald-700 text-white text-sm font-semibold transition-colors"
>
Done
</button>
</div>
</motion.div>
</div>
)}
</AnimatePresence>,
document.body
);
}
+19
View File
@@ -5048,6 +5048,10 @@ export const MockStoreProvider = ({ children }) => {
subcontractor_tasks: { label: 'Subcontractor Tasks', actions: ['view','assign','receive'] }, subcontractor_tasks: { label: 'Subcontractor Tasks', actions: ['view','assign','receive'] },
}; };
// Per-user permission overrides: grant or deny specific module+action regardless of role
// Shape: [{ userId, moduleKey, action, effect: 'grant' | 'deny' }]
const [userPermissionOverrides, setUserPermissionOverrides] = useState([]);
const [roleCommissionOverrides, setRoleCommissionOverrides] = useState([ const [roleCommissionOverrides, setRoleCommissionOverrides] = useState([
{ roleKey: 'SALES_REP', enabled: true, type: 'percent_net', rate: 5 }, { roleKey: 'SALES_REP', enabled: true, type: 'percent_net', rate: 5 },
{ roleKey: 'CANVASSER', enabled: true, type: 'flat', rate: 300 }, { roleKey: 'CANVASSER', enabled: true, type: 'flat', rate: 300 },
@@ -5306,6 +5310,21 @@ export const MockStoreProvider = ({ children }) => {
[moduleKey]: { ...prev[moduleKey], [roleKey]: actions }, [moduleKey]: { ...prev[moduleKey], [roleKey]: actions },
})); }));
}, },
userPermissionOverrides,
setUserPermissionOverride: (userId, moduleKey, action, effect) => {
setUserPermissionOverrides(prev => {
const filtered = prev.filter(o => !(o.userId === userId && o.moduleKey === moduleKey && o.action === action));
return [...filtered, { userId, moduleKey, action, effect }];
});
},
removeUserPermissionOverride: (userId, moduleKey, action) => {
setUserPermissionOverrides(prev =>
prev.filter(o => !(o.userId === userId && o.moduleKey === moduleKey && o.action === action))
);
},
clearUserPermissionOverrides: (userId) => {
setUserPermissionOverrides(prev => prev.filter(o => o.userId !== userId));
},
roleCommissionOverrides, roleCommissionOverrides,
setRoleCommissionOverride: (override) => { setRoleCommissionOverride: (override) => {
setRoleCommissionOverrides(prev => { setRoleCommissionOverrides(prev => {
+11 -2
View File
@@ -13,7 +13,7 @@ import { resolveOrgRoleKey } from '../utils/permissions';
*/ */
export const usePermissions = () => { export const usePermissions = () => {
const { user } = useAuth(); const { user } = useAuth();
const { orgPermissions, orgMembers } = useMockStore(); const { orgPermissions, orgMembers, userPermissionOverrides } = useMockStore();
const orgRoleKey = useMemo( const orgRoleKey = useMemo(
() => resolveOrgRoleKey(user, orgMembers), () => resolveOrgRoleKey(user, orgMembers),
@@ -24,10 +24,19 @@ export const usePermissions = () => {
(moduleKey, action) => { (moduleKey, action) => {
if (!orgRoleKey) return false; if (!orgRoleKey) return false;
if (orgRoleKey === 'OWNER') return true; if (orgRoleKey === 'OWNER') return true;
// Person-level override takes priority
if (user?.id && userPermissionOverrides) {
const override = userPermissionOverrides.find(
o => o.userId === user.id && o.moduleKey === moduleKey && o.action === action
);
if (override) return override.effect === 'grant';
}
const actions = orgPermissions[moduleKey]?.[orgRoleKey] || []; const actions = orgPermissions[moduleKey]?.[orgRoleKey] || [];
return actions.includes(action); return actions.includes(action);
}, },
[orgRoleKey, orgPermissions], [orgRoleKey, orgPermissions, user, userPermissionOverrides],
); );
const canAny = useCallback( const canAny = useCallback(
+65 -4
View File
@@ -13,6 +13,7 @@ import { usePermissions } from '../../hooks/usePermissions';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { computeRolePermissionStats } from '../../utils/permissions'; import { computeRolePermissionStats } from '../../utils/permissions';
import OrgTeamManagement from '../../components/owner/OrgTeamManagement'; import OrgTeamManagement from '../../components/owner/OrgTeamManagement';
import AccessControlPersonModal from '../../components/owner/AccessControlPersonModal';
const COMMISSION_TYPES = [ const COMMISSION_TYPES = [
{ value: 'flat', label: 'Flat Amount', hint: 'Fixed dollar amount' }, { value: 'flat', label: 'Flat Amount', hint: 'Fixed dollar amount' },
@@ -231,13 +232,25 @@ const RoleManagementSection = ({ roles, members }) => {
// ──────────────────────────────────────────────────── // ────────────────────────────────────────────────────
// 3. Access Control Matrix (Full) // 3. Access Control Matrix (Full)
// ──────────────────────────────────────────────────── // ────────────────────────────────────────────────────
const AccessControlMatrix = ({ permissions, permissionModules, roles, onUpdate }) => { const AccessControlMatrix = ({ permissions, permissionModules, roles, onUpdate, members, userPermissionOverrides, onSetUserOverride, onRemoveUserOverride }) => {
const [filterModule, setFilterModule] = useState('all'); const [filterModule, setFilterModule] = useState('all');
const [filterRole, setFilterRole] = useState('all'); const [filterRole, setFilterRole] = useState('all');
const [changeLog, setChangeLog] = useState([]); const [changeLog, setChangeLog] = useState([]);
const [showLog, setShowLog] = useState(false); const [showLog, setShowLog] = useState(false);
const [personModalModule, setPersonModalModule] = useState(null);
const snapshotRef = useRef(JSON.stringify(permissions)); const snapshotRef = useRef(JSON.stringify(permissions));
const moduleOverrideCounts = useMemo(() => {
const counts = {};
for (const o of (userPermissionOverrides || [])) {
if (!counts[o.moduleKey]) counts[o.moduleKey] = new Set();
counts[o.moduleKey].add(o.userId);
}
const result = {};
for (const [k, v] of Object.entries(counts)) result[k] = v.size;
return result;
}, [userPermissionOverrides]);
const moduleEntries = useMemo(() => { const moduleEntries = useMemo(() => {
const entries = Object.entries(permissionModules); const entries = Object.entries(permissionModules);
if (filterModule === 'all') return entries; if (filterModule === 'all') return entries;
@@ -454,7 +467,7 @@ const AccessControlMatrix = ({ permissions, permissionModules, roles, onUpdate }
<Shield size={12} className="text-zinc-400 dark:text-zinc-600" /> <Shield size={12} className="text-zinc-400 dark:text-zinc-600" />
{module.label} {module.label}
</div> </div>
<div className="flex gap-0.5"> <div className="flex items-center gap-0.5">
<button <button
type="button" type="button"
onClick={() => toggleAllForModule(moduleKey, true)} onClick={() => toggleAllForModule(moduleKey, true)}
@@ -471,6 +484,19 @@ const AccessControlMatrix = ({ permissions, permissionModules, roles, onUpdate }
> >
<Square size={10} /> <Square size={10} />
</button> </button>
<button
type="button"
onClick={() => setPersonModalModule(moduleKey)}
title={`Manage person overrides for ${module.label}`}
className="relative p-0.5 rounded hover:bg-blue-50 dark:hover:bg-blue-500/10 text-zinc-300 dark:text-zinc-700 hover:text-blue-500 transition-colors"
>
<Users size={10} />
{moduleOverrideCounts[moduleKey] > 0 && (
<span className="absolute -top-1 -right-1 w-3 h-3 rounded-full bg-blue-500 text-white text-[7px] font-bold flex items-center justify-center">
{moduleOverrideCounts[moduleKey]}
</span>
)}
</button>
</div> </div>
</div> </div>
</td> </td>
@@ -531,7 +557,22 @@ const AccessControlMatrix = ({ permissions, permissionModules, roles, onUpdate }
<div className="p-4 space-y-3"> <div className="p-4 space-y-3">
{moduleEntries.map(([moduleKey, module]) => ( {moduleEntries.map(([moduleKey, module]) => (
<div key={moduleKey} className="space-y-1.5"> <div key={moduleKey} className="space-y-1.5">
<span className="text-[10px] font-bold uppercase tracking-wider text-zinc-500">{module.label}</span> <div className="flex items-center justify-between">
<span className="text-[10px] font-bold uppercase tracking-wider text-zinc-500">{module.label}</span>
<button
type="button"
onClick={() => setPersonModalModule(moduleKey)}
className="relative flex items-center gap-1 px-2 py-0.5 rounded-md text-[9px] font-bold bg-blue-50 dark:bg-blue-500/10 text-blue-600 dark:text-blue-400 border border-blue-200 dark:border-blue-500/20 hover:bg-blue-100 dark:hover:bg-blue-500/20 transition-colors"
>
<Users size={9} />
Person
{moduleOverrideCounts[moduleKey] > 0 && (
<span className="ml-0.5 w-3.5 h-3.5 rounded-full bg-blue-500 text-white text-[7px] font-bold flex items-center justify-center">
{moduleOverrideCounts[moduleKey]}
</span>
)}
</button>
</div>
<div className="flex flex-wrap gap-1.5"> <div className="flex flex-wrap gap-1.5">
{module.actions.map(action => { {module.actions.map(action => {
const hasPermission = (permissions[moduleKey]?.[role.key] || []).includes(action); const hasPermission = (permissions[moduleKey]?.[role.key] || []).includes(action);
@@ -607,6 +648,21 @@ const AccessControlMatrix = ({ permissions, permissionModules, roles, onUpdate }
)} )}
</AnimatePresence> </AnimatePresence>
{/* ── Person Override Modal ── */}
<AccessControlPersonModal
isOpen={!!personModalModule}
onClose={() => setPersonModalModule(null)}
moduleKey={personModalModule || ''}
moduleLabel={personModalModule ? (permissionModules[personModalModule]?.label || personModalModule) : ''}
actions={personModalModule ? (permissionModules[personModalModule]?.actions || []) : []}
permissions={permissions}
roles={roles}
members={members}
userPermissionOverrides={userPermissionOverrides}
onSetOverride={onSetUserOverride}
onRemoveOverride={onRemoveUserOverride}
/>
{/* ── Info Footer ── */} {/* ── Info Footer ── */}
<div className="p-4 bg-zinc-50 dark:bg-white/[0.02] rounded-xl border border-zinc-200 dark:border-white/5"> <div className="p-4 bg-zinc-50 dark:bg-white/[0.02] rounded-xl border border-zinc-200 dark:border-white/5">
<div className="flex items-start gap-2"> <div className="flex items-start gap-2">
@@ -614,6 +670,7 @@ const AccessControlMatrix = ({ permissions, permissionModules, roles, onUpdate }
<div className="text-[11px] text-zinc-500 space-y-1"> <div className="text-[11px] text-zinc-500 space-y-1">
<p><span className="font-bold text-zinc-700 dark:text-zinc-400">Org Owner</span> always has full access to all modules and cannot be restricted.</p> <p><span className="font-bold text-zinc-700 dark:text-zinc-400">Org Owner</span> always has full access to all modules and cannot be restricted.</p>
<p>Permission changes apply immediately to all users with the affected role. Use the <span className="font-mono">Save</span> button to persist changes or <span className="font-mono">Reset</span> to revert.</p> <p>Permission changes apply immediately to all users with the affected role. Use the <span className="font-mono">Save</span> button to persist changes or <span className="font-mono">Reset</span> to revert.</p>
<p><span className="font-bold text-zinc-700 dark:text-zinc-400">Person overrides</span> take priority over role permissions. A denied person override blocks access even if the role grants it.</p>
</div> </div>
</div> </div>
</div> </div>
@@ -1053,7 +1110,7 @@ const OrgSettings = () => {
id: 'access', id: 'access',
icon: Shield, icon: Shield,
title: 'Access Control Matrix', title: 'Access Control Matrix',
subtitle: 'Configure module-level permissions per role', subtitle: 'Configure module-level permissions by role and by person',
color: '#10b981', color: '#10b981',
content: ( content: (
<AccessControlMatrix <AccessControlMatrix
@@ -1061,6 +1118,10 @@ const OrgSettings = () => {
permissionModules={store.PERMISSION_MODULES} permissionModules={store.PERMISSION_MODULES}
roles={store.orgRoles} roles={store.orgRoles}
onUpdate={store.updateOrgPermissions} onUpdate={store.updateOrgPermissions}
members={store.orgMembers}
userPermissionOverrides={store.userPermissionOverrides}
onSetUserOverride={store.setUserPermissionOverride}
onRemoveUserOverride={store.removeUserPermissionOverride}
/> />
), ),
}, },