Files
pn-new-crm/src/app/api/v1/admin/storage/route.ts
Matt Ciaccio d3a6a9beef 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

69 lines
2.1 KiB
TypeScript

/**
* Admin storage status + connection test. Super-admin only.
*
* GET /api/v1/admin/storage — current backend + capacity stats
* POST /api/v1/admin/storage/test — exercise list/put/get/delete on s3
*/
import { NextResponse } from 'next/server';
import { requireSuperAdmin, withAuth } from '@/lib/api/helpers';
import { errorResponse } from '@/lib/errors';
import { TABLES_WITH_STORAGE_KEYS } from '@/lib/storage/migrate';
import { getStorageBackend } from '@/lib/storage';
import { S3Backend } from '@/lib/storage/s3';
import { db } from '@/lib/db';
import { sql } from 'drizzle-orm';
export const runtime = 'nodejs';
export const GET = withAuth(async (_req, ctx) => {
try {
requireSuperAdmin(ctx, 'admin.storage.read');
const backend = await getStorageBackend();
// Aggregate row count + total bytes across every storage-bearing table.
let fileCount = 0;
const totalBytes = 0;
for (const tbl of TABLES_WITH_STORAGE_KEYS) {
const result = await db.execute(
sql.raw(
`SELECT COUNT(*)::bigint AS n FROM ${tbl.table} WHERE ${tbl.keyColumn} IS NOT NULL`,
),
);
const rows = (
Array.isArray(result) ? result : ((result as { rows?: unknown[] }).rows ?? [])
) as Array<{ n: number | string }>;
fileCount += Number(rows[0]?.n ?? 0);
}
return NextResponse.json({
data: {
backend: backend.name,
fileCount,
totalBytes,
tablesTracked: TABLES_WITH_STORAGE_KEYS.map((t) => t.table),
},
});
} catch (error) {
return errorResponse(error);
}
});
export const POST = withAuth(async (_req, ctx) => {
try {
requireSuperAdmin(ctx, 'admin.storage.test');
const backend = await getStorageBackend();
if (!(backend instanceof S3Backend)) {
return NextResponse.json(
{ ok: false, error: 'Test connection only available for S3 backend' },
{ status: 400 },
);
}
const result = await backend.healthCheck();
return NextResponse.json(result);
} catch (error) {
return errorResponse(error);
}
});