feat(backoffice): guard routes and actions by permission

Mirrors the new keys in lib/permissions.ts, wraps the warehouse,
overview, reports, support and booking-request routes in
RequirePermission, and gates the dispatch, mark-paid, invoice pay/cancel,
export and support-send actions behind their own keys. Removes duplicate
route blocks.
This commit is contained in:
Nathnael
2026-08-07 07:31:31 +00:00
parent 0114673120
commit c1fcabcba2
10 changed files with 153 additions and 237 deletions

View File

@@ -68,6 +68,8 @@ export function ClearanceOpsTabs({
Boolean(exchangeEntityId) &&
(hasPermission(user, FREIGHT_PERMS.contracts.clearanceEtActions) ||
hasPermission(user, FREIGHT_PERMS.contracts.clearanceDjActions));
// Risk assignment + incident reporting hit bookings:operations endpoints.
const canOps = hasPermission(user, FREIGHT_PERMS.bookings.operations);
const hasTabs = (showOpsTabs && hasOps) || showDocuments || showExchange;
if (!hasTabs) {
@@ -98,12 +100,12 @@ export function ClearanceOpsTabs({
Document exchange
</Tabs.Tab>
) : null}
{showOpsTabs && riskMs ? (
{showOpsTabs && canOps && riskMs ? (
<Tabs.Tab value="risk" leftSection={<ShieldAlert size={14} />}>
Risk assignment
</Tabs.Tab>
) : null}
{showOpsTabs && bookingId ? (
{showOpsTabs && canOps && bookingId ? (
<Tabs.Tab value="incidents" leftSection={<AlertTriangle size={14} />}>
Incidents
</Tabs.Tab>
@@ -129,7 +131,7 @@ export function ClearanceOpsTabs({
</Tabs.Panel>
) : null}
{showOpsTabs && riskMs && bookingId ? (
{showOpsTabs && canOps && riskMs && bookingId ? (
<Tabs.Panel value="risk">
<SectionCard icon={ShieldAlert} title="Customs risk" accent="edr-green">
<AssignRiskCard bookingId={bookingId} milestone={riskMs} />
@@ -137,7 +139,7 @@ export function ClearanceOpsTabs({
</Tabs.Panel>
) : null}
{showOpsTabs && bookingId ? (
{showOpsTabs && canOps && bookingId ? (
<Tabs.Panel value="incidents">
<SectionCard icon={AlertTriangle} title="Incident reports" accent="edr-green">
<Stack gap="sm">

View File

@@ -41,6 +41,8 @@ import {
fetchViewableFile,
} from "@/services/files.service";
import { useContractClearanceMutations } from "@/hooks/contracts/useContracts";
import { useAuth } from "@/auth/useAuth";
import { FREIGHT_PERMS, hasPermission } from "@/lib/permissions";
import { useFileViewer } from "@/hooks/useFileViewer";
export interface ContractClearanceReviewSectionProps {
@@ -123,6 +125,21 @@ export function ContractClearanceReviewSection({
} | null>(null);
const { view, viewer } = useFileViewer();
// Mirror the API guards: Path A (self-clearance) actions need
// ops_clearance_review; Path B (customs) review needs clearance_review or
// the ET phased key. Without the matching key every action would 403 — show
// the audit view instead of dead buttons.
const { user } = useAuth();
const canReviewHere = selfClear
? hasPermission(user, FREIGHT_PERMS.contracts.opsClearanceReview)
: hasPermission(user, FREIGHT_PERMS.contracts.clearanceReview) ||
hasPermission(user, FREIGHT_PERMS.contracts.clearanceEtActions);
// Finalize has its own API key on the customs path (contracts:finalize_clearance).
const canFinalizeHere = selfClear
? hasPermission(user, FREIGHT_PERMS.contracts.opsClearanceReview)
: hasPermission(user, FREIGHT_PERMS.contracts.finalizeClearance);
readOnly = readOnly || !canReviewHere;
const reviewerTeam = selfClear ? "Operations" : "Global Logistics";
const { data: clearance, isLoading } = useQuery({
@@ -484,7 +501,7 @@ export function ContractClearanceReviewSection({
color="edr-green"
radius="md"
leftSection={<CheckCircle2 size={16} />}
disabled={!clearance.allApproved}
disabled={!clearance.allApproved || !canFinalizeHere}
loading={finalizeClearance.isPending}
onClick={() =>
finalizeClearance.mutate(undefined, {