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:
@@ -1,14 +1,14 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
|
||||
import { withAuth, withPermission } from '@/lib/api/helpers';
|
||||
import { errorResponse } from '@/lib/errors';
|
||||
import { errorResponse, ValidationError } from '@/lib/errors';
|
||||
import { clearDuplicate } from '@/lib/services/expense-dedup.service';
|
||||
|
||||
export const POST = withAuth(
|
||||
withPermission('expenses', 'edit', async (_req, ctx, params) => {
|
||||
try {
|
||||
const id = params.id;
|
||||
if (!id) return NextResponse.json({ error: 'Missing id' }, { status: 400 });
|
||||
if (!id) throw new ValidationError('id is required');
|
||||
await clearDuplicate(id, ctx.portId);
|
||||
return NextResponse.json({ ok: true });
|
||||
} catch (error) {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { z } from 'zod';
|
||||
|
||||
import { withAuth, withPermission } from '@/lib/api/helpers';
|
||||
import { parseBody } from '@/lib/api/route-helpers';
|
||||
import { errorResponse } from '@/lib/errors';
|
||||
import { errorResponse, ValidationError } from '@/lib/errors';
|
||||
import { mergeDuplicate } from '@/lib/services/expense-dedup.service';
|
||||
|
||||
const mergeSchema = z.object({
|
||||
@@ -15,9 +15,7 @@ export const POST = withAuth(
|
||||
withPermission('expenses', 'edit', async (req, ctx, params) => {
|
||||
try {
|
||||
const sourceId = params.id;
|
||||
if (!sourceId) {
|
||||
return NextResponse.json({ error: 'Missing id' }, { status: 400 });
|
||||
}
|
||||
if (!sourceId) throw new ValidationError('id is required');
|
||||
const body = await parseBody(req, mergeSchema);
|
||||
await mergeDuplicate(sourceId, body.targetId, ctx.portId);
|
||||
return NextResponse.json({ ok: true });
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
|
||||
import { withAuth } from '@/lib/api/helpers';
|
||||
import { requireSuperAdmin, withAuth } from '@/lib/api/helpers';
|
||||
import { errorResponse } from '@/lib/errors';
|
||||
import { exportParentCompany } from '@/lib/services/expense-export';
|
||||
import { listExpensesSchema } from '@/lib/validators/expenses';
|
||||
|
||||
export const POST = withAuth(async (req, ctx) => {
|
||||
try {
|
||||
if (!ctx.isSuperAdmin) {
|
||||
return NextResponse.json({ error: 'Insufficient permissions' }, { status: 403 });
|
||||
}
|
||||
requireSuperAdmin(ctx, 'expenses.export.parent-company');
|
||||
|
||||
const body = await req.json().catch(() => ({}));
|
||||
const query = listExpensesSchema.parse(body);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
|
||||
import { withAuth, withPermission, withRateLimit } from '@/lib/api/helpers';
|
||||
import { errorResponse } from '@/lib/errors';
|
||||
import { errorResponse, ValidationError } from '@/lib/errors';
|
||||
import { logger } from '@/lib/logger';
|
||||
import { getResolvedOcrConfig } from '@/lib/services/ocr-config.service';
|
||||
import {
|
||||
@@ -29,9 +29,7 @@ export const POST = withAuth(
|
||||
try {
|
||||
const formData = await req.formData();
|
||||
const file = formData.get('file') as File | null;
|
||||
if (!file) {
|
||||
return NextResponse.json({ error: 'No file provided' }, { status: 400 });
|
||||
}
|
||||
if (!file) throw new ValidationError('A file is required');
|
||||
const buffer = Buffer.from(await file.arrayBuffer());
|
||||
const mimeType = file.type || 'image/jpeg';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user