RBAC: Enforce read-only viewer role across backend and frontend
- Add global WriteAccessGuard that blocks POST/PUT/PATCH/DELETE for viewer role - Add @AllowViewer() decorator for endpoints viewers need (switch-org, intro-seen, AI recommendations) - Add useIsReadOnly hook to auth store for frontend role checks - Hide write UI (add/edit/delete/import buttons, inline editors) in all 13 data pages for viewers - Disable inline NumberInputs on Budgets and Monthly Actuals pages for viewers - Skip onboarding wizard for viewer role users Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -13,7 +13,7 @@ import {
|
||||
} from '@tabler/icons-react';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import api from '../../services/api';
|
||||
import { useAuthStore } from '../../stores/authStore';
|
||||
import { useAuthStore, useIsReadOnly } from '../../stores/authStore';
|
||||
|
||||
interface OrgMember {
|
||||
id: string;
|
||||
@@ -52,6 +52,7 @@ export function OrgMembersPage() {
|
||||
const [editingMember, setEditingMember] = useState<OrgMember | null>(null);
|
||||
const queryClient = useQueryClient();
|
||||
const { user, currentOrg } = useAuthStore();
|
||||
const isReadOnly = useIsReadOnly();
|
||||
|
||||
const { data: members = [], isLoading } = useQuery<OrgMember[]>({
|
||||
queryKey: ['org-members'],
|
||||
@@ -162,9 +163,11 @@ export function OrgMembersPage() {
|
||||
<Title order={2}>Organization Members</Title>
|
||||
<Text c="dimmed" size="sm">Manage who has access to {currentOrg?.name}</Text>
|
||||
</div>
|
||||
<Button leftSection={<IconUserPlus size={16} />} onClick={openAdd}>
|
||||
Add Member
|
||||
</Button>
|
||||
{!isReadOnly && (
|
||||
<Button leftSection={<IconUserPlus size={16} />} onClick={openAdd}>
|
||||
Add Member
|
||||
</Button>
|
||||
)}
|
||||
</Group>
|
||||
|
||||
<SimpleGrid cols={{ base: 1, sm: 3 }}>
|
||||
@@ -259,20 +262,22 @@ export function OrgMembersPage() {
|
||||
{member.lastLoginAt ? new Date(member.lastLoginAt).toLocaleDateString() : 'Never'}
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Group gap={4}>
|
||||
<Tooltip label="Change role">
|
||||
<ActionIcon variant="subtle" onClick={() => handleEditRole(member)}>
|
||||
<IconEdit size={16} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
{member.userId !== user?.id && (
|
||||
<Tooltip label="Remove member">
|
||||
<ActionIcon variant="subtle" color="red" onClick={() => handleRemove(member)}>
|
||||
<IconTrash size={16} />
|
||||
{!isReadOnly && (
|
||||
<Group gap={4}>
|
||||
<Tooltip label="Change role">
|
||||
<ActionIcon variant="subtle" onClick={() => handleEditRole(member)}>
|
||||
<IconEdit size={16} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Group>
|
||||
{member.userId !== user?.id && (
|
||||
<Tooltip label="Remove member">
|
||||
<ActionIcon variant="subtle" color="red" onClick={() => handleRemove(member)}>
|
||||
<IconTrash size={16} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Group>
|
||||
)}
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
))}
|
||||
|
||||
Reference in New Issue
Block a user