fix(rbac): sales/operational roles see deal alerts; quiet admin-only onboarding probe
UAT findings from the Sales-role functional walkthrough: F1 — The deal-alert feed (stale interest, hot-lead-silent, EOI unsigned, signer overdue, reservation-needs-agreement, berth stalled, expense dupes) was gated on admin.view_audit_log, so salespeople got a 403 on the Alerts inbox. None of the 9 alert rules are audit/security signals — they're all operational — so re-gate the list route to interests.view (sales, director, viewer get it; external residential partners don't) and hide the Alerts section in the inbox for users without it instead of letting the query 403. F2 — Non-admins triggered /api/v1/admin/onboarding/status (admin-only) and ate a 403 in the console. Make useOnboardingStatus strictly opt-in (enabled: opts.enabled === true) so a transient/stale isSuperAdmin during permission hydration can't fire the privileged request. 1664 vitest pass; tsc + eslint clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,7 @@ import { PageHeader } from '@/components/shared/page-header';
|
||||
import { AlertsPageShell } from '@/components/alerts/alerts-page-shell';
|
||||
import { ReminderList } from '@/components/reminders/reminder-list';
|
||||
import { useAlertCount } from '@/components/alerts/use-alerts';
|
||||
import { usePermissions } from '@/hooks/use-permissions';
|
||||
|
||||
/**
|
||||
* Merged "Inbox" surface - replaces the previously-separate /alerts and
|
||||
@@ -29,6 +30,11 @@ export function InboxPageShell() {
|
||||
const [alertsOpen, setAlertsOpen] = useState(true);
|
||||
const [remindersOpen, setRemindersOpen] = useState(true);
|
||||
const { data: alertCount } = useAlertCount();
|
||||
// The deal-alert feed (stale interests, overdue signers, …) is gated on
|
||||
// interests.view — operational roles see it; external residential partners
|
||||
// don't. Hide the whole section rather than letting its query 403.
|
||||
const { can } = usePermissions();
|
||||
const canSeeAlerts = can('interests', 'view');
|
||||
|
||||
// localStorage hydration on mount - canonical "read from external
|
||||
// store" pattern. setState in effect is intentional.
|
||||
@@ -95,20 +101,22 @@ export function InboxPageShell() {
|
||||
) : null}
|
||||
</section>
|
||||
|
||||
<section id="inbox-section-alerts" className="rounded-lg border bg-card shadow-xs">
|
||||
<SectionHeader
|
||||
icon={<ShieldAlert className="size-4 text-muted-foreground" aria-hidden />}
|
||||
label="Alerts"
|
||||
count={activeAlerts}
|
||||
open={alertsOpen}
|
||||
onToggle={toggleAlerts}
|
||||
/>
|
||||
{alertsOpen ? (
|
||||
<div className="border-t px-4 pb-4 pt-3">
|
||||
<AlertsPageShell embedded />
|
||||
</div>
|
||||
) : null}
|
||||
</section>
|
||||
{canSeeAlerts ? (
|
||||
<section id="inbox-section-alerts" className="rounded-lg border bg-card shadow-xs">
|
||||
<SectionHeader
|
||||
icon={<ShieldAlert className="size-4 text-muted-foreground" aria-hidden />}
|
||||
label="Alerts"
|
||||
count={activeAlerts}
|
||||
open={alertsOpen}
|
||||
onToggle={toggleAlerts}
|
||||
/>
|
||||
{alertsOpen ? (
|
||||
<div className="border-t px-4 pb-4 pt-3">
|
||||
<AlertsPageShell embedded />
|
||||
</div>
|
||||
) : null}
|
||||
</section>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user