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:
@@ -12,6 +12,7 @@ import { IconPlus, IconEye, IconCheck, IconX, IconTrash, IconShieldCheck } from
|
||||
import { AttachmentPanel } from '../../components/attachments/AttachmentPanel';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import api from '../../services/api';
|
||||
import { useIsReadOnly } from '../../stores/authStore';
|
||||
|
||||
interface JournalEntryLine {
|
||||
id?: string;
|
||||
@@ -48,6 +49,7 @@ export function TransactionsPage() {
|
||||
const [opened, { open, close }] = useDisclosure(false);
|
||||
const [viewId, setViewId] = useState<string | null>(null);
|
||||
const queryClient = useQueryClient();
|
||||
const isReadOnly = useIsReadOnly();
|
||||
|
||||
const { data: entries = [], isLoading } = useQuery<JournalEntry[]>({
|
||||
queryKey: ['journal-entries'],
|
||||
@@ -164,9 +166,11 @@ export function TransactionsPage() {
|
||||
<Stack>
|
||||
<Group justify="space-between">
|
||||
<Title order={2}>Journal Entries</Title>
|
||||
<Button leftSection={<IconPlus size={16} />} onClick={open}>
|
||||
New Entry
|
||||
</Button>
|
||||
{!isReadOnly && (
|
||||
<Button leftSection={<IconPlus size={16} />} onClick={open}>
|
||||
New Entry
|
||||
</Button>
|
||||
)}
|
||||
</Group>
|
||||
|
||||
<Table striped highlightOnHover>
|
||||
@@ -216,14 +220,14 @@ export function TransactionsPage() {
|
||||
<IconEye size={16} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
{!e.is_posted && !e.is_void && (
|
||||
{!isReadOnly && !e.is_posted && !e.is_void && (
|
||||
<Tooltip label="Post">
|
||||
<ActionIcon variant="subtle" color="green" onClick={() => postMutation.mutate(e.id)}>
|
||||
<IconCheck size={16} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
)}
|
||||
{e.is_posted && !e.is_void && (
|
||||
{!isReadOnly && e.is_posted && !e.is_void && (
|
||||
<Tooltip label="Void">
|
||||
<ActionIcon variant="subtle" color="red" onClick={() => voidMutation.mutate(e.id)}>
|
||||
<IconX size={16} />
|
||||
|
||||
Reference in New Issue
Block a user