access control by role plus by person in orgsettings
This commit is contained in:
@@ -13,6 +13,7 @@ import { usePermissions } from '../../hooks/usePermissions';
|
||||
import { toast } from 'sonner';
|
||||
import { computeRolePermissionStats } from '../../utils/permissions';
|
||||
import OrgTeamManagement from '../../components/owner/OrgTeamManagement';
|
||||
import AccessControlPersonModal from '../../components/owner/AccessControlPersonModal';
|
||||
|
||||
const COMMISSION_TYPES = [
|
||||
{ value: 'flat', label: 'Flat Amount', hint: 'Fixed dollar amount' },
|
||||
@@ -231,13 +232,25 @@ const RoleManagementSection = ({ roles, members }) => {
|
||||
// ────────────────────────────────────────────────────
|
||||
// 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 [filterRole, setFilterRole] = useState('all');
|
||||
const [changeLog, setChangeLog] = useState([]);
|
||||
const [showLog, setShowLog] = useState(false);
|
||||
const [personModalModule, setPersonModalModule] = useState(null);
|
||||
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 entries = Object.entries(permissionModules);
|
||||
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" />
|
||||
{module.label}
|
||||
</div>
|
||||
<div className="flex gap-0.5">
|
||||
<div className="flex items-center gap-0.5">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleAllForModule(moduleKey, true)}
|
||||
@@ -471,6 +484,19 @@ const AccessControlMatrix = ({ permissions, permissionModules, roles, onUpdate }
|
||||
>
|
||||
<Square size={10} />
|
||||
</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>
|
||||
</td>
|
||||
@@ -531,7 +557,22 @@ const AccessControlMatrix = ({ permissions, permissionModules, roles, onUpdate }
|
||||
<div className="p-4 space-y-3">
|
||||
{moduleEntries.map(([moduleKey, module]) => (
|
||||
<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">
|
||||
{module.actions.map(action => {
|
||||
const hasPermission = (permissions[moduleKey]?.[role.key] || []).includes(action);
|
||||
@@ -607,6 +648,21 @@ const AccessControlMatrix = ({ permissions, permissionModules, roles, onUpdate }
|
||||
)}
|
||||
</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 ── */}
|
||||
<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">
|
||||
@@ -614,6 +670,7 @@ const AccessControlMatrix = ({ permissions, permissionModules, roles, onUpdate }
|
||||
<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>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>
|
||||
@@ -1053,7 +1110,7 @@ const OrgSettings = () => {
|
||||
id: 'access',
|
||||
icon: Shield,
|
||||
title: 'Access Control Matrix',
|
||||
subtitle: 'Configure module-level permissions per role',
|
||||
subtitle: 'Configure module-level permissions by role and by person',
|
||||
color: '#10b981',
|
||||
content: (
|
||||
<AccessControlMatrix
|
||||
@@ -1061,6 +1118,10 @@ const OrgSettings = () => {
|
||||
permissionModules={store.PERMISSION_MODULES}
|
||||
roles={store.orgRoles}
|
||||
onUpdate={store.updateOrgPermissions}
|
||||
members={store.orgMembers}
|
||||
userPermissionOverrides={store.userPermissionOverrides}
|
||||
onSetUserOverride={store.setUserPermissionOverride}
|
||||
onRemoveUserOverride={store.removeUserPermissionOverride}
|
||||
/>
|
||||
),
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user