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>
This commit is contained in:
@@ -5,33 +5,24 @@ import type { AuthContext } from '@/lib/api/helpers';
|
||||
import { parseBody } from '@/lib/api/route-helpers';
|
||||
import { db } from '@/lib/db';
|
||||
import { savedViews } from '@/lib/db/schema';
|
||||
import { errorResponse } from '@/lib/errors';
|
||||
import { errorResponse, ForbiddenError, NotFoundError } from '@/lib/errors';
|
||||
import { savedViewsService } from '@/lib/services/saved-views.service';
|
||||
import { updateSavedViewSchema } from '@/lib/validators/saved-views';
|
||||
|
||||
/**
|
||||
* Resolves the view and enforces ownership before mutating.
|
||||
*
|
||||
* Returns a 404 when the view does not exist (or lives in a different port)
|
||||
* and a 403 when it belongs to a different user. The 404-before-403 split
|
||||
* matches the rest of the API and avoids leaking the existence of another
|
||||
* user's saved view via timing or status code.
|
||||
* Throws NotFoundError when the view does not exist (or lives in a different
|
||||
* port) and ForbiddenError when it belongs to a different user. The 404-
|
||||
* before-403 split matches the rest of the API and avoids leaking the
|
||||
* existence of another user's saved view via timing or status code.
|
||||
*/
|
||||
async function assertViewOwner(
|
||||
id: string,
|
||||
portId: string,
|
||||
userId: string,
|
||||
): Promise<NextResponse | null> {
|
||||
async function assertViewOwner(id: string, portId: string, userId: string): Promise<void> {
|
||||
const view = await db.query.savedViews.findFirst({
|
||||
where: and(eq(savedViews.id, id), eq(savedViews.portId, portId)),
|
||||
});
|
||||
if (!view) {
|
||||
return NextResponse.json({ error: 'Not found' }, { status: 404 });
|
||||
}
|
||||
if (view.userId !== userId) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
return null;
|
||||
if (!view) throw new NotFoundError('saved view');
|
||||
if (view.userId !== userId) throw new ForbiddenError('That saved view belongs to someone else.');
|
||||
}
|
||||
|
||||
export async function patchHandler(
|
||||
@@ -41,8 +32,7 @@ export async function patchHandler(
|
||||
): Promise<NextResponse> {
|
||||
try {
|
||||
const id = params.id ?? '';
|
||||
const denied = await assertViewOwner(id, ctx.portId, ctx.userId);
|
||||
if (denied) return denied;
|
||||
await assertViewOwner(id, ctx.portId, ctx.userId);
|
||||
const body = await parseBody(req as never, updateSavedViewSchema);
|
||||
const view = await savedViewsService.update(ctx.portId, ctx.userId, id, body);
|
||||
return NextResponse.json({ data: view });
|
||||
@@ -58,8 +48,7 @@ export async function deleteHandler(
|
||||
): Promise<NextResponse> {
|
||||
try {
|
||||
const id = params.id ?? '';
|
||||
const denied = await assertViewOwner(id, ctx.portId, ctx.userId);
|
||||
if (denied) return denied;
|
||||
await assertViewOwner(id, ctx.portId, ctx.userId);
|
||||
await savedViewsService.delete(ctx.portId, ctx.userId, id);
|
||||
return NextResponse.json({ data: null }, { status: 200 });
|
||||
} catch (error) {
|
||||
|
||||
Reference in New Issue
Block a user