2026-04-08 15:47:11 -04:00
|
|
|
'use client';
|
fix(audit-tier-2-routes): manual NextResponse.json error sweep + admin form banners
Two final waves of error-surface hygiene closing the audit's MED §12 +
HIGH §15 + HIGH §17 findings:
* 50 route files swept (61 sites): manual NextResponse.json({error,
status: 4xx|5xx}) early-returns replaced by typed throws +
errorResponse(err) at the catch.
- Super-admin gates (13 sites) use new requireSuperAdmin(ctx, action)
helper from src/lib/api/helpers.ts so denials hit the audit log.
- Path-param + body validation 400s become ValidationError throws.
- 404s become NotFoundError or CodedError('NOT_FOUND') for AI
feature-flag paths.
- 11 manual 5xx returns now re-throw so error_events captures the
request-id (the admin error inspector becomes usable from real
incidents).
- website-analytics 200-with-error anti-pattern flipped to 409 +
UMAMI_NOT_CONFIGURED. 502 upstream paths use UMAMI_UPSTREAM_ERROR.
- 11 sites intentionally preserved: storage/[token] anti-enumeration
token-failure paths, webhook-secret 401, "Unknown port" 400 in
public intake.
* 7 admin forms (roles, users, ports, webhooks, custom-fields,
document-templates, tags) gain a formatErrorBanner() helper from
src/lib/api/toast-error.ts that builds a multi-line "Error code / Reference ID"
banner — the rep can copy the request id when reporting a failed
save. Banners get whitespace-pre-line so newlines render.
Test status: 1168/1168 vitest, tsc clean.
Refs: docs/audit-comprehensive-2026-05-05.md MED §12 (auditor-F Issue 1)
+ HIGH §15 (auditor-F Issue 2) + HIGH §17 (auditor-H Issue 2).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 20:36:59 +02:00
|
|
|
import { formatErrorBanner } from '@/lib/api/toast-error';
|
2026-04-08 15:47:11 -04:00
|
|
|
|
|
|
|
|
import { useState, useEffect } from 'react';
|
|
|
|
|
import { Button } from '@/components/ui/button';
|
|
|
|
|
import { Input } from '@/components/ui/input';
|
|
|
|
|
import { Label } from '@/components/ui/label';
|
|
|
|
|
import { Textarea } from '@/components/ui/textarea';
|
|
|
|
|
import { Checkbox } from '@/components/ui/checkbox';
|
|
|
|
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
|
|
|
|
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetFooter } from '@/components/ui/sheet';
|
|
|
|
|
import {
|
|
|
|
|
Accordion,
|
|
|
|
|
AccordionContent,
|
|
|
|
|
AccordionItem,
|
|
|
|
|
AccordionTrigger,
|
|
|
|
|
} from '@/components/ui/accordion';
|
|
|
|
|
import { apiFetch } from '@/lib/api/client';
|
|
|
|
|
|
feat(admin): inquiry inbox, send log, email-template overrides, reports dashboard, recommender keys, role-editor coverage; replace placeholder pages
Closes the bulk of audit-pass-#1 admin gaps in one batch.
New admin pages:
- /admin/inquiries reads website_submissions with filter chips for
berth/residence/contact + payload viewer per row.
- /admin/sends reads document_sends with sent/failed filter chips and
expandable body markdown; failures surface errorReason and any
fallback-to-link reason from the SMTP retry.
- /admin/email-templates lets per-port admins override the subject of
each transactional template (8 templates catalogued in
template-catalog.ts). Body editing is a follow-on; portal_activation
+ portal_reset are wired to honor the override via loadSubjectOverride.
- /admin/reports replaces the "Coming in Layer 3" placeholder with a
KPI dashboard: 4 KPI tiles, pipeline funnel bars, berth occupancy
donut-bars, conversion %, refresh every 60s.
- backup/import/onboarding admin pages replace placeholders with
actionable guidance: backup posture + planned features, available CLI
imports + planned UI, ordered onboarding checklist linking to admin
pages.
Existing pages widened:
- settings-manager exposes the 9 berth-recommender tunables that were
previously code-only (recommender_*, heat_weight_*, fallthrough_*,
tier_ladder_hide_late_stage).
- role-form covers all 19 RolePermissions schema groups; previously
missing yachts/companies/memberships/reservations + missing
documents.edit + files.edit checkboxes. snake_case residential
labels replaced with friendly text.
portal-auth.service.ts now also writes audit_log rows for portal
invite, resend, activate, password-reset request, and reset (closes one
more audit-pass-#2 gap while we were touching the file).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 14:58:17 +02:00
|
|
|
/** Default permissions structure matching RolePermissions type in
|
|
|
|
|
* src/lib/db/schema/users.ts. Keep this in sync when actions are added. */
|
2026-04-08 15:47:11 -04:00
|
|
|
const DEFAULT_PERMISSIONS: Record<string, Record<string, boolean>> = {
|
|
|
|
|
clients: { view: false, create: false, edit: false, delete: false, merge: false, export: false },
|
|
|
|
|
interests: {
|
|
|
|
|
view: false,
|
|
|
|
|
create: false,
|
|
|
|
|
edit: false,
|
|
|
|
|
delete: false,
|
|
|
|
|
change_stage: false,
|
feat(interests): manual stage override + Residential Partner system role
Manual stage override
Sales reps need to skip canTransitionStage rules when the data was
entered out of order — e.g. recording a contract_signed deal whose
earlier stages were never tracked in the system.
- New permission flag interests.override_stage in RolePermissions.
Plumbed through the schema TS type, the role-editor UI, the seed
file's pre-built roles (super_admin/director/sales_manager get it,
sales_agent + viewer don't), and the test factories.
- changeStageSchema gains an optional `override` boolean and the
service checks it before evaluating canTransitionStage. When
override=true the reason field becomes required (min 5 chars) and
is recorded in the audit log.
- The route handler gates `override` on the new permission so a
sales_agent without it can't pass override=true and bypass.
- InterestStagePicker auto-detects when the requested transition is
blocked by the table and switches into "override mode" — shows an
amber warning, requires the reason, button label flips to
"Override stage". When the operator lacks the permission, the
warning is red and the button is disabled.
Residential Partner role
Per the smart-archive scoping conversation: external partners who
handle residential inquiries shouldn't see marina clients, yachts,
berths, or financials. The two residential_* permission groups
already exist; this commit just seeds a pre-built system role
("residential_partner") with those flags + minimal own-reminders, so
admins can invite a partner today via /admin/users without manually
building the permission set.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 18:32:57 +02:00
|
|
|
override_stage: false,
|
2026-04-08 15:47:11 -04:00
|
|
|
generate_eoi: false,
|
|
|
|
|
export: false,
|
|
|
|
|
},
|
|
|
|
|
berths: { view: false, edit: false, import: false, manage_waiting_list: false },
|
|
|
|
|
documents: {
|
|
|
|
|
view: false,
|
|
|
|
|
create: false,
|
feat(admin): inquiry inbox, send log, email-template overrides, reports dashboard, recommender keys, role-editor coverage; replace placeholder pages
Closes the bulk of audit-pass-#1 admin gaps in one batch.
New admin pages:
- /admin/inquiries reads website_submissions with filter chips for
berth/residence/contact + payload viewer per row.
- /admin/sends reads document_sends with sent/failed filter chips and
expandable body markdown; failures surface errorReason and any
fallback-to-link reason from the SMTP retry.
- /admin/email-templates lets per-port admins override the subject of
each transactional template (8 templates catalogued in
template-catalog.ts). Body editing is a follow-on; portal_activation
+ portal_reset are wired to honor the override via loadSubjectOverride.
- /admin/reports replaces the "Coming in Layer 3" placeholder with a
KPI dashboard: 4 KPI tiles, pipeline funnel bars, berth occupancy
donut-bars, conversion %, refresh every 60s.
- backup/import/onboarding admin pages replace placeholders with
actionable guidance: backup posture + planned features, available CLI
imports + planned UI, ordered onboarding checklist linking to admin
pages.
Existing pages widened:
- settings-manager exposes the 9 berth-recommender tunables that were
previously code-only (recommender_*, heat_weight_*, fallthrough_*,
tier_ladder_hide_late_stage).
- role-form covers all 19 RolePermissions schema groups; previously
missing yachts/companies/memberships/reservations + missing
documents.edit + files.edit checkboxes. snake_case residential
labels replaced with friendly text.
portal-auth.service.ts now also writes audit_log rows for portal
invite, resend, activate, password-reset request, and reset (closes one
more audit-pass-#2 gap while we were touching the file).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 14:58:17 +02:00
|
|
|
edit: false,
|
2026-04-08 15:47:11 -04:00
|
|
|
send_for_signing: false,
|
|
|
|
|
upload_signed: false,
|
|
|
|
|
delete: false,
|
|
|
|
|
},
|
|
|
|
|
expenses: {
|
|
|
|
|
view: false,
|
|
|
|
|
create: false,
|
|
|
|
|
edit: false,
|
|
|
|
|
delete: false,
|
|
|
|
|
export: false,
|
|
|
|
|
scan_receipt: false,
|
|
|
|
|
},
|
|
|
|
|
invoices: {
|
|
|
|
|
view: false,
|
|
|
|
|
create: false,
|
|
|
|
|
edit: false,
|
|
|
|
|
delete: false,
|
|
|
|
|
send: false,
|
|
|
|
|
record_payment: false,
|
|
|
|
|
export: false,
|
|
|
|
|
},
|
feat(admin): inquiry inbox, send log, email-template overrides, reports dashboard, recommender keys, role-editor coverage; replace placeholder pages
Closes the bulk of audit-pass-#1 admin gaps in one batch.
New admin pages:
- /admin/inquiries reads website_submissions with filter chips for
berth/residence/contact + payload viewer per row.
- /admin/sends reads document_sends with sent/failed filter chips and
expandable body markdown; failures surface errorReason and any
fallback-to-link reason from the SMTP retry.
- /admin/email-templates lets per-port admins override the subject of
each transactional template (8 templates catalogued in
template-catalog.ts). Body editing is a follow-on; portal_activation
+ portal_reset are wired to honor the override via loadSubjectOverride.
- /admin/reports replaces the "Coming in Layer 3" placeholder with a
KPI dashboard: 4 KPI tiles, pipeline funnel bars, berth occupancy
donut-bars, conversion %, refresh every 60s.
- backup/import/onboarding admin pages replace placeholders with
actionable guidance: backup posture + planned features, available CLI
imports + planned UI, ordered onboarding checklist linking to admin
pages.
Existing pages widened:
- settings-manager exposes the 9 berth-recommender tunables that were
previously code-only (recommender_*, heat_weight_*, fallthrough_*,
tier_ladder_hide_late_stage).
- role-form covers all 19 RolePermissions schema groups; previously
missing yachts/companies/memberships/reservations + missing
documents.edit + files.edit checkboxes. snake_case residential
labels replaced with friendly text.
portal-auth.service.ts now also writes audit_log rows for portal
invite, resend, activate, password-reset request, and reset (closes one
more audit-pass-#2 gap while we were touching the file).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 14:58:17 +02:00
|
|
|
files: { view: false, upload: false, edit: false, delete: false, manage_folders: false },
|
2026-04-08 15:47:11 -04:00
|
|
|
email: { view: false, send: false, configure_account: false },
|
|
|
|
|
reminders: {
|
|
|
|
|
view_own: false,
|
|
|
|
|
view_all: false,
|
|
|
|
|
create: false,
|
|
|
|
|
edit_own: false,
|
|
|
|
|
edit_all: false,
|
|
|
|
|
assign_others: false,
|
|
|
|
|
},
|
|
|
|
|
calendar: { connect: false, view_events: false },
|
|
|
|
|
reports: { view_dashboard: false, view_analytics: false, export: false },
|
|
|
|
|
document_templates: { view: false, generate: false, manage: false },
|
feat(admin): inquiry inbox, send log, email-template overrides, reports dashboard, recommender keys, role-editor coverage; replace placeholder pages
Closes the bulk of audit-pass-#1 admin gaps in one batch.
New admin pages:
- /admin/inquiries reads website_submissions with filter chips for
berth/residence/contact + payload viewer per row.
- /admin/sends reads document_sends with sent/failed filter chips and
expandable body markdown; failures surface errorReason and any
fallback-to-link reason from the SMTP retry.
- /admin/email-templates lets per-port admins override the subject of
each transactional template (8 templates catalogued in
template-catalog.ts). Body editing is a follow-on; portal_activation
+ portal_reset are wired to honor the override via loadSubjectOverride.
- /admin/reports replaces the "Coming in Layer 3" placeholder with a
KPI dashboard: 4 KPI tiles, pipeline funnel bars, berth occupancy
donut-bars, conversion %, refresh every 60s.
- backup/import/onboarding admin pages replace placeholders with
actionable guidance: backup posture + planned features, available CLI
imports + planned UI, ordered onboarding checklist linking to admin
pages.
Existing pages widened:
- settings-manager exposes the 9 berth-recommender tunables that were
previously code-only (recommender_*, heat_weight_*, fallthrough_*,
tier_ladder_hide_late_stage).
- role-form covers all 19 RolePermissions schema groups; previously
missing yachts/companies/memberships/reservations + missing
documents.edit + files.edit checkboxes. snake_case residential
labels replaced with friendly text.
portal-auth.service.ts now also writes audit_log rows for portal
invite, resend, activate, password-reset request, and reset (closes one
more audit-pass-#2 gap while we were touching the file).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 14:58:17 +02:00
|
|
|
yachts: { view: false, create: false, edit: false, delete: false, transfer: false },
|
|
|
|
|
companies: { view: false, create: false, edit: false, delete: false },
|
|
|
|
|
memberships: { view: false, manage: false },
|
|
|
|
|
reservations: { view: false, create: false, activate: false, cancel: false },
|
2026-04-08 15:47:11 -04:00
|
|
|
admin: {
|
|
|
|
|
manage_users: false,
|
|
|
|
|
view_audit_log: false,
|
|
|
|
|
manage_settings: false,
|
|
|
|
|
manage_webhooks: false,
|
|
|
|
|
manage_reports: false,
|
|
|
|
|
manage_custom_fields: false,
|
|
|
|
|
manage_forms: false,
|
|
|
|
|
manage_tags: false,
|
|
|
|
|
system_backup: false,
|
feat(platform): residential module + admin UI + reliability fixes
Residential platform
- New schema: residentialClients, residentialInterests (separate from
marina/yacht clients) with migration 0010
- Service layer with CRUD + audit + sockets + per-port portal toggle
- v1 + public API routes (/api/v1/residential/*, /api/public/residential-inquiries)
- List + detail pages with inline editing for clients and interests
- Per-user residentialAccess toggle on userPortRoles (migration 0011)
- Permission keys: residential_clients, residential_interests
- Sidebar nav + role form integration
- Smoke spec covering page loads, UI create flow, public endpoint
Admin & shared UI
- Admin → Forms (form templates CRUD) with validators + service
- Notification preferences page (in-app + email per type)
- Email composition + accounts list + threads view
- Branded auth shell shared across CRM + portal auth surfaces
- Inline editing extended to yacht/company/interest detail pages
- InlineTagEditor + per-entity tags endpoints (yachts, companies)
- Notes service polymorphic across clients/interests/yachts/companies
- Client list columns: yachtCount + companyCount badges
- Reservation file-download via presigned URL (replaces stale <a href>)
Route handler refactor
- Extracted yachts/companies/berths reservation handlers to sibling
handlers.ts files (Next.js 15 route.ts only allows specific exports)
Reliability fixes
- apiFetch double-stringify bug fixed across 13 components
(apiFetch already JSON.stringifies its body; passing a stringified
body produced double-encoded JSON which failed zod validation)
- SocketProvider gated behind useSyncExternalStore-based mount check
to avoid useSession() SSR crashes under React 19 + Next 15
- apiFetch falls back to URL-pathname → port-id resolution when the
Zustand store hasn't hydrated yet (fresh contexts, e2e tests)
- CRM invite flow (schema, service, route, email, dev script)
- Dashboard route → [portSlug]/dashboard/page.tsx + redirect
- Document the dev-server restart-after-migration gotcha in CLAUDE.md
Tests
- 5-case residential smoke spec
- Integration test updates for new service signatures
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 21:54:32 +02:00
|
|
|
},
|
|
|
|
|
residential_clients: { view: false, create: false, edit: false, delete: false },
|
|
|
|
|
residential_interests: {
|
|
|
|
|
view: false,
|
|
|
|
|
create: false,
|
|
|
|
|
edit: false,
|
|
|
|
|
delete: false,
|
|
|
|
|
change_stage: false,
|
2026-04-08 15:47:11 -04:00
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const GROUP_LABELS: Record<string, string> = {
|
|
|
|
|
clients: 'Clients',
|
|
|
|
|
interests: 'Interests / Pipeline',
|
|
|
|
|
berths: 'Berths',
|
|
|
|
|
documents: 'Documents',
|
|
|
|
|
expenses: 'Expenses',
|
|
|
|
|
invoices: 'Invoices',
|
|
|
|
|
files: 'Files',
|
|
|
|
|
email: 'Email',
|
|
|
|
|
reminders: 'Reminders',
|
|
|
|
|
calendar: 'Calendar',
|
|
|
|
|
reports: 'Reports',
|
|
|
|
|
document_templates: 'Document Templates',
|
feat(admin): inquiry inbox, send log, email-template overrides, reports dashboard, recommender keys, role-editor coverage; replace placeholder pages
Closes the bulk of audit-pass-#1 admin gaps in one batch.
New admin pages:
- /admin/inquiries reads website_submissions with filter chips for
berth/residence/contact + payload viewer per row.
- /admin/sends reads document_sends with sent/failed filter chips and
expandable body markdown; failures surface errorReason and any
fallback-to-link reason from the SMTP retry.
- /admin/email-templates lets per-port admins override the subject of
each transactional template (8 templates catalogued in
template-catalog.ts). Body editing is a follow-on; portal_activation
+ portal_reset are wired to honor the override via loadSubjectOverride.
- /admin/reports replaces the "Coming in Layer 3" placeholder with a
KPI dashboard: 4 KPI tiles, pipeline funnel bars, berth occupancy
donut-bars, conversion %, refresh every 60s.
- backup/import/onboarding admin pages replace placeholders with
actionable guidance: backup posture + planned features, available CLI
imports + planned UI, ordered onboarding checklist linking to admin
pages.
Existing pages widened:
- settings-manager exposes the 9 berth-recommender tunables that were
previously code-only (recommender_*, heat_weight_*, fallthrough_*,
tier_ladder_hide_late_stage).
- role-form covers all 19 RolePermissions schema groups; previously
missing yachts/companies/memberships/reservations + missing
documents.edit + files.edit checkboxes. snake_case residential
labels replaced with friendly text.
portal-auth.service.ts now also writes audit_log rows for portal
invite, resend, activate, password-reset request, and reset (closes one
more audit-pass-#2 gap while we were touching the file).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 14:58:17 +02:00
|
|
|
yachts: 'Yachts',
|
|
|
|
|
companies: 'Companies',
|
|
|
|
|
memberships: 'Company Memberships',
|
|
|
|
|
reservations: 'Reservations',
|
2026-04-08 15:47:11 -04:00
|
|
|
admin: 'Administration',
|
feat(admin): inquiry inbox, send log, email-template overrides, reports dashboard, recommender keys, role-editor coverage; replace placeholder pages
Closes the bulk of audit-pass-#1 admin gaps in one batch.
New admin pages:
- /admin/inquiries reads website_submissions with filter chips for
berth/residence/contact + payload viewer per row.
- /admin/sends reads document_sends with sent/failed filter chips and
expandable body markdown; failures surface errorReason and any
fallback-to-link reason from the SMTP retry.
- /admin/email-templates lets per-port admins override the subject of
each transactional template (8 templates catalogued in
template-catalog.ts). Body editing is a follow-on; portal_activation
+ portal_reset are wired to honor the override via loadSubjectOverride.
- /admin/reports replaces the "Coming in Layer 3" placeholder with a
KPI dashboard: 4 KPI tiles, pipeline funnel bars, berth occupancy
donut-bars, conversion %, refresh every 60s.
- backup/import/onboarding admin pages replace placeholders with
actionable guidance: backup posture + planned features, available CLI
imports + planned UI, ordered onboarding checklist linking to admin
pages.
Existing pages widened:
- settings-manager exposes the 9 berth-recommender tunables that were
previously code-only (recommender_*, heat_weight_*, fallthrough_*,
tier_ladder_hide_late_stage).
- role-form covers all 19 RolePermissions schema groups; previously
missing yachts/companies/memberships/reservations + missing
documents.edit + files.edit checkboxes. snake_case residential
labels replaced with friendly text.
portal-auth.service.ts now also writes audit_log rows for portal
invite, resend, activate, password-reset request, and reset (closes one
more audit-pass-#2 gap while we were touching the file).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 14:58:17 +02:00
|
|
|
residential_clients: 'Residential Clients',
|
|
|
|
|
residential_interests: 'Residential Interests',
|
2026-04-08 15:47:11 -04:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
function formatAction(action: string): string {
|
|
|
|
|
return action.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface RoleFormProps {
|
|
|
|
|
open: boolean;
|
|
|
|
|
onOpenChange: (open: boolean) => void;
|
|
|
|
|
role?: {
|
|
|
|
|
id: string;
|
|
|
|
|
name: string;
|
|
|
|
|
description: string | null;
|
|
|
|
|
isSystem: boolean;
|
|
|
|
|
permissions: Record<string, Record<string, boolean>>;
|
|
|
|
|
} | null;
|
|
|
|
|
onSuccess: () => void;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function RoleForm({ open, onOpenChange, role, onSuccess }: RoleFormProps) {
|
|
|
|
|
const [name, setName] = useState('');
|
|
|
|
|
const [description, setDescription] = useState('');
|
|
|
|
|
const [permissions, setPermissions] = useState<Record<string, Record<string, boolean>>>(
|
|
|
|
|
structuredClone(DEFAULT_PERMISSIONS),
|
|
|
|
|
);
|
|
|
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
|
|
|
|
|
|
const isEdit = !!role;
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (open) {
|
|
|
|
|
if (role) {
|
|
|
|
|
setName(role.name);
|
|
|
|
|
setDescription(role.description ?? '');
|
|
|
|
|
// Merge role permissions over defaults to fill any missing keys
|
|
|
|
|
const merged = structuredClone(DEFAULT_PERMISSIONS);
|
|
|
|
|
for (const [group, actions] of Object.entries(role.permissions)) {
|
|
|
|
|
if (merged[group]) {
|
|
|
|
|
for (const [action, value] of Object.entries(actions as Record<string, boolean>)) {
|
|
|
|
|
merged[group]![action] = value;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
setPermissions(merged);
|
|
|
|
|
} else {
|
|
|
|
|
setName('');
|
|
|
|
|
setDescription('');
|
|
|
|
|
setPermissions(structuredClone(DEFAULT_PERMISSIONS));
|
|
|
|
|
}
|
|
|
|
|
setError(null);
|
|
|
|
|
}
|
|
|
|
|
}, [open, role]);
|
|
|
|
|
|
|
|
|
|
function togglePermission(group: string, action: string) {
|
|
|
|
|
setPermissions((prev) => {
|
|
|
|
|
const next = structuredClone(prev);
|
|
|
|
|
next[group]![action] = !next[group]![action];
|
|
|
|
|
return next;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function toggleGroup(group: string, value: boolean) {
|
|
|
|
|
setPermissions((prev) => {
|
|
|
|
|
const next = structuredClone(prev);
|
|
|
|
|
for (const action of Object.keys(next[group]!)) {
|
|
|
|
|
next[group]![action] = value;
|
|
|
|
|
}
|
|
|
|
|
return next;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function isGroupAllChecked(group: string): boolean {
|
|
|
|
|
return Object.values(permissions[group]!).every(Boolean);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function isGroupPartial(group: string): boolean {
|
|
|
|
|
const vals = Object.values(permissions[group]!);
|
|
|
|
|
return vals.some(Boolean) && !vals.every(Boolean);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function handleSubmit(e: React.FormEvent) {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
setError(null);
|
|
|
|
|
setLoading(true);
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
if (isEdit) {
|
|
|
|
|
await apiFetch(`/api/v1/admin/roles/${role.id}`, {
|
|
|
|
|
method: 'PATCH',
|
|
|
|
|
body: { name, description: description || null, permissions },
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
await apiFetch('/api/v1/admin/roles', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
body: { name, description: description || undefined, permissions },
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
onSuccess();
|
|
|
|
|
onOpenChange(false);
|
|
|
|
|
} catch (err: unknown) {
|
fix(audit-tier-2-routes): manual NextResponse.json error sweep + admin form banners
Two final waves of error-surface hygiene closing the audit's MED §12 +
HIGH §15 + HIGH §17 findings:
* 50 route files swept (61 sites): manual NextResponse.json({error,
status: 4xx|5xx}) early-returns replaced by typed throws +
errorResponse(err) at the catch.
- Super-admin gates (13 sites) use new requireSuperAdmin(ctx, action)
helper from src/lib/api/helpers.ts so denials hit the audit log.
- Path-param + body validation 400s become ValidationError throws.
- 404s become NotFoundError or CodedError('NOT_FOUND') for AI
feature-flag paths.
- 11 manual 5xx returns now re-throw so error_events captures the
request-id (the admin error inspector becomes usable from real
incidents).
- website-analytics 200-with-error anti-pattern flipped to 409 +
UMAMI_NOT_CONFIGURED. 502 upstream paths use UMAMI_UPSTREAM_ERROR.
- 11 sites intentionally preserved: storage/[token] anti-enumeration
token-failure paths, webhook-secret 401, "Unknown port" 400 in
public intake.
* 7 admin forms (roles, users, ports, webhooks, custom-fields,
document-templates, tags) gain a formatErrorBanner() helper from
src/lib/api/toast-error.ts that builds a multi-line "Error code / Reference ID"
banner — the rep can copy the request id when reporting a failed
save. Banners get whitespace-pre-line so newlines render.
Test status: 1168/1168 vitest, tsc clean.
Refs: docs/audit-comprehensive-2026-05-05.md MED §12 (auditor-F Issue 1)
+ HIGH §15 (auditor-F Issue 2) + HIGH §17 (auditor-H Issue 2).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 20:36:59 +02:00
|
|
|
const message = formatErrorBanner(err);
|
2026-04-08 15:47:11 -04:00
|
|
|
setError(message);
|
|
|
|
|
} finally {
|
|
|
|
|
setLoading(false);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<Sheet open={open} onOpenChange={onOpenChange}>
|
|
|
|
|
<SheetContent className="w-[500px] sm:max-w-[500px]">
|
|
|
|
|
<SheetHeader>
|
|
|
|
|
<SheetTitle>{isEdit ? 'Edit Role' : 'New Role'}</SheetTitle>
|
|
|
|
|
</SheetHeader>
|
|
|
|
|
|
|
|
|
|
<form onSubmit={handleSubmit} className="mt-6 flex flex-col h-[calc(100vh-140px)]">
|
|
|
|
|
<div className="space-y-4 mb-4">
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<Label htmlFor="role-name">Name</Label>
|
|
|
|
|
<Input
|
|
|
|
|
id="role-name"
|
|
|
|
|
value={name}
|
|
|
|
|
onChange={(e) => setName(e.target.value)}
|
|
|
|
|
placeholder="e.g. Sales Manager"
|
|
|
|
|
required
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<Label htmlFor="role-description">Description</Label>
|
|
|
|
|
<Textarea
|
|
|
|
|
id="role-description"
|
|
|
|
|
value={description}
|
|
|
|
|
onChange={(e) => setDescription(e.target.value)}
|
|
|
|
|
placeholder="What this role is for..."
|
|
|
|
|
rows={2}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<Label className="mb-2">Permissions</Label>
|
|
|
|
|
<ScrollArea className="flex-1 rounded-md border">
|
|
|
|
|
<Accordion type="multiple" className="px-3">
|
|
|
|
|
{Object.entries(permissions).map(([group, actions]) => (
|
|
|
|
|
<AccordionItem key={group} value={group}>
|
|
|
|
|
<AccordionTrigger className="text-sm">
|
|
|
|
|
<div className="flex items-center gap-3">
|
|
|
|
|
<Checkbox
|
|
|
|
|
checked={isGroupAllChecked(group)}
|
|
|
|
|
ref={(el) => {
|
|
|
|
|
if (el) {
|
|
|
|
|
(el as unknown as HTMLInputElement).indeterminate =
|
|
|
|
|
isGroupPartial(group);
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
onCheckedChange={(checked) => toggleGroup(group, checked === true)}
|
|
|
|
|
onClick={(e) => e.stopPropagation()}
|
|
|
|
|
/>
|
|
|
|
|
<span>{GROUP_LABELS[group] ?? group}</span>
|
|
|
|
|
</div>
|
|
|
|
|
</AccordionTrigger>
|
|
|
|
|
<AccordionContent>
|
|
|
|
|
<div className="grid grid-cols-2 gap-2 pl-8 pb-2">
|
|
|
|
|
{Object.entries(actions).map(([action, checked]) => (
|
|
|
|
|
<label
|
|
|
|
|
key={action}
|
|
|
|
|
className="flex items-center gap-2 text-sm cursor-pointer"
|
|
|
|
|
>
|
|
|
|
|
<Checkbox
|
|
|
|
|
checked={checked}
|
|
|
|
|
onCheckedChange={() => togglePermission(group, action)}
|
|
|
|
|
/>
|
|
|
|
|
{formatAction(action)}
|
|
|
|
|
</label>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</AccordionContent>
|
|
|
|
|
</AccordionItem>
|
|
|
|
|
))}
|
|
|
|
|
</Accordion>
|
|
|
|
|
</ScrollArea>
|
|
|
|
|
|
fix(audit-tier-2-routes): manual NextResponse.json error sweep + admin form banners
Two final waves of error-surface hygiene closing the audit's MED §12 +
HIGH §15 + HIGH §17 findings:
* 50 route files swept (61 sites): manual NextResponse.json({error,
status: 4xx|5xx}) early-returns replaced by typed throws +
errorResponse(err) at the catch.
- Super-admin gates (13 sites) use new requireSuperAdmin(ctx, action)
helper from src/lib/api/helpers.ts so denials hit the audit log.
- Path-param + body validation 400s become ValidationError throws.
- 404s become NotFoundError or CodedError('NOT_FOUND') for AI
feature-flag paths.
- 11 manual 5xx returns now re-throw so error_events captures the
request-id (the admin error inspector becomes usable from real
incidents).
- website-analytics 200-with-error anti-pattern flipped to 409 +
UMAMI_NOT_CONFIGURED. 502 upstream paths use UMAMI_UPSTREAM_ERROR.
- 11 sites intentionally preserved: storage/[token] anti-enumeration
token-failure paths, webhook-secret 401, "Unknown port" 400 in
public intake.
* 7 admin forms (roles, users, ports, webhooks, custom-fields,
document-templates, tags) gain a formatErrorBanner() helper from
src/lib/api/toast-error.ts that builds a multi-line "Error code / Reference ID"
banner — the rep can copy the request id when reporting a failed
save. Banners get whitespace-pre-line so newlines render.
Test status: 1168/1168 vitest, tsc clean.
Refs: docs/audit-comprehensive-2026-05-05.md MED §12 (auditor-F Issue 1)
+ HIGH §15 (auditor-F Issue 2) + HIGH §17 (auditor-H Issue 2).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 20:36:59 +02:00
|
|
|
{error && <p className="mt-2 whitespace-pre-line text-sm text-destructive">{error}</p>}
|
2026-04-08 15:47:11 -04:00
|
|
|
|
|
|
|
|
<SheetFooter className="mt-4">
|
|
|
|
|
<Button
|
|
|
|
|
type="button"
|
|
|
|
|
variant="outline"
|
|
|
|
|
onClick={() => onOpenChange(false)}
|
|
|
|
|
disabled={loading}
|
|
|
|
|
>
|
|
|
|
|
Cancel
|
|
|
|
|
</Button>
|
|
|
|
|
<Button type="submit" disabled={loading || !name.trim()}>
|
|
|
|
|
{loading ? 'Saving...' : isEdit ? 'Save Changes' : 'Create Role'}
|
|
|
|
|
</Button>
|
|
|
|
|
</SheetFooter>
|
|
|
|
|
</form>
|
|
|
|
|
</SheetContent>
|
|
|
|
|
</Sheet>
|
|
|
|
|
);
|
|
|
|
|
}
|