diff --git a/src/app/api/v1/alerts/route.ts b/src/app/api/v1/alerts/route.ts index f01c90f3..dcdccf1e 100644 --- a/src/app/api/v1/alerts/route.ts +++ b/src/app/api/v1/alerts/route.ts @@ -5,11 +5,13 @@ import { listAlertsForPort } from '@/lib/services/alerts.service'; type AlertStatus = 'open' | 'dismissed' | 'resolved'; -// Tier-4 (authz-auditor): alerts include permission_denied + audit-adjacent -// signals. Gated on admin.view_audit_log - same permission the audit log -// page uses. +// The alert feed is entirely operational/deal signals (stale interest, hot lead +// silent, EOI unsigned, signer overdue, reservation needs agreement, berth +// stalled, duplicate/unscanned expense) — there are no audit/security alert +// rules. Gated on interests.view so the operational roles that act on these +// (sales, director, viewer) see them; external residential partners don't. export const GET = withAuth( - withPermission('admin', 'view_audit_log', async (req: NextRequest, ctx) => { + withPermission('interests', 'view', async (req: NextRequest, ctx) => { const url = new URL(req.url); const status = (url.searchParams.get('status') ?? 'open') as AlertStatus; diff --git a/src/components/inbox/inbox-page-shell.tsx b/src/components/inbox/inbox-page-shell.tsx index 47a94afd..e9c2c548 100644 --- a/src/components/inbox/inbox-page-shell.tsx +++ b/src/components/inbox/inbox-page-shell.tsx @@ -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} -
- } - label="Alerts" - count={activeAlerts} - open={alertsOpen} - onToggle={toggleAlerts} - /> - {alertsOpen ? ( -
- -
- ) : null} -
+ {canSeeAlerts ? ( +
+ } + label="Alerts" + count={activeAlerts} + open={alertsOpen} + onToggle={toggleAlerts} + /> + {alertsOpen ? ( +
+ +
+ ) : null} +
+ ) : null} ); } diff --git a/src/hooks/use-onboarding-status.ts b/src/hooks/use-onboarding-status.ts index 82d056ce..59c31c5c 100644 --- a/src/hooks/use-onboarding-status.ts +++ b/src/hooks/use-onboarding-status.ts @@ -27,8 +27,11 @@ export interface OnboardingStatusPayload { * and the admin checklist summary. Cached for 60s so all three surfaces * share a single fetch on first paint. * - * Pass `enabled=false` to skip the network call (e.g. when the current - * user isn't a super_admin and the surface won't render anyway). + * Defaults to OFF: the endpoint is admin-only (admin.manage_settings), so + * callers must opt in with `enabled: true` once they've confirmed the user is + * a super_admin. This prevents a transient 403 (e.g. a stale `isSuperAdmin` + * during permission hydration) from firing the privileged request for + * non-admins. */ export function useOnboardingStatus(opts: { enabled?: boolean } = {}) { return useQuery({ @@ -38,7 +41,7 @@ export function useOnboardingStatus(opts: { enabled?: boolean } = {}) { (r) => r.data, ), staleTime: 60_000, - enabled: opts.enabled ?? true, + enabled: opts.enabled === true, retry: false, }); }