feat(sase): VIN list management view
A user-list-style table at /projects/sase/vin-decode/vins for inspecting
and managing decoded VINs. Pairs with sase.tr#34 (cache-clear + delete
endpoints).
Repo (lib/sase/vin-list.ts)
- listVinDecodes(filter, sort, page) — raw SQL with dynamic WHERE for
search (VIN/email), provider IN, success boolean, brand IDs, date
range. Joins users + brands, computes EXISTS(vehicles) per row so the
action menu knows whether DB delete is meaningful.
- KNOWN_PROVIDERS export.
Admin SDK
- clearVinCache({ vin, reason, founderId }) — POST cache-clear
- deleteVehicleByVin({ vin, reason, founderId }) — DELETE vehicle
- Result types: VinCacheClearResult, VinDeleteResult.
- SASE_ADMIN_ENDPOINTS list updated.
Panel routes
- POST /api/sase/vins/[vin]/cache-clear — auth + spoke-wired + reason
≥ 5 chars. Audit on both paths.
- DELETE /api/sase/vins/[vin] — same shape.
UI (/projects/sase/vin-decode/vins)
- URL-driven filters (VIN/email search, provider multi-select pills,
success/fail toggle) — sharable links.
- Sortable columns: Tarih (createdAt) and RT (responseTimeMs).
- Per-row cells: timestamp · VIN (mono) · brand slug+name · user email
(link to user detail) · provider badge (link to provider drill-down)
· ok/fail badge (fail title = errorMessage) · responseTimeMs ·
cacheSource from timings jsonb · action menu.
- Per-row action menu (gated on saseAdminWired):
[Cache] — opens reason modal, POSTs cache-clear
[DB] — only when vehicles row exists; opens reason modal,
destructive variant, DELETE
- Pager with prefix/postfix range + ←/→ links preserving filters.
Dashboard header gets a "VIN list →" pill next to Business/Trends.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
56
apps/web/src/app/api/sase/vins/[vin]/cache-clear/route.ts
Normal file
56
apps/web/src/app/api/sase/vins/[vin]/cache-clear/route.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { headers } from "next/headers";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { createSaseAdmin, saseAdminWired } from "@/lib/admin-sdk/sase";
|
||||
import { writeAudit } from "@/lib/audit";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export async function POST(
|
||||
req: Request,
|
||||
ctx: { params: Promise<{ vin: string }> },
|
||||
) {
|
||||
const session = await auth.api.getSession({ headers: await headers() });
|
||||
if (!session) {
|
||||
return NextResponse.json({ ok: false, error: "unauthenticated" }, { status: 401 });
|
||||
}
|
||||
if (!saseAdminWired()) {
|
||||
return NextResponse.json({ ok: false, error: "spoke_not_wired" }, { status: 503 });
|
||||
}
|
||||
|
||||
const { vin } = await ctx.params;
|
||||
const body = (await req.json().catch(() => ({}))) as { reason?: string };
|
||||
const reason = (body.reason ?? "").trim();
|
||||
if (reason.length < 5) {
|
||||
return NextResponse.json({ ok: false, error: "reason_required" }, { status: 400 });
|
||||
}
|
||||
|
||||
const endpoint = `/api/sase/vins/${vin}/cache-clear`;
|
||||
try {
|
||||
const sdk = createSaseAdmin();
|
||||
const result = await sdk.clearVinCache({
|
||||
vin,
|
||||
reason,
|
||||
founderId: session.user.id,
|
||||
});
|
||||
await writeAudit({
|
||||
projectKey: "sase",
|
||||
endpoint,
|
||||
method: "POST",
|
||||
requestPayload: { vin, reasonLen: reason.length },
|
||||
responseStatus: 200,
|
||||
});
|
||||
return NextResponse.json({ ok: true, result });
|
||||
} catch (err) {
|
||||
const status = (err as { status?: number } | undefined)?.status ?? 500;
|
||||
const message = err instanceof Error ? err.message : "unknown";
|
||||
await writeAudit({
|
||||
projectKey: "sase",
|
||||
endpoint,
|
||||
method: "POST",
|
||||
requestPayload: { vin, reasonLen: reason.length },
|
||||
responseStatus: status,
|
||||
});
|
||||
return NextResponse.json({ ok: false, error: message }, { status });
|
||||
}
|
||||
}
|
||||
57
apps/web/src/app/api/sase/vins/[vin]/route.ts
Normal file
57
apps/web/src/app/api/sase/vins/[vin]/route.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { headers } from "next/headers";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { createSaseAdmin, saseAdminWired } from "@/lib/admin-sdk/sase";
|
||||
import { writeAudit } from "@/lib/audit";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
// Hard-delete the shared vehicle row for this VIN.
|
||||
export async function DELETE(
|
||||
req: Request,
|
||||
ctx: { params: Promise<{ vin: string }> },
|
||||
) {
|
||||
const session = await auth.api.getSession({ headers: await headers() });
|
||||
if (!session) {
|
||||
return NextResponse.json({ ok: false, error: "unauthenticated" }, { status: 401 });
|
||||
}
|
||||
if (!saseAdminWired()) {
|
||||
return NextResponse.json({ ok: false, error: "spoke_not_wired" }, { status: 503 });
|
||||
}
|
||||
|
||||
const { vin } = await ctx.params;
|
||||
const body = (await req.json().catch(() => ({}))) as { reason?: string };
|
||||
const reason = (body.reason ?? "").trim();
|
||||
if (reason.length < 5) {
|
||||
return NextResponse.json({ ok: false, error: "reason_required" }, { status: 400 });
|
||||
}
|
||||
|
||||
const endpoint = `/api/sase/vins/${vin}`;
|
||||
try {
|
||||
const sdk = createSaseAdmin();
|
||||
const result = await sdk.deleteVehicleByVin({
|
||||
vin,
|
||||
reason,
|
||||
founderId: session.user.id,
|
||||
});
|
||||
await writeAudit({
|
||||
projectKey: "sase",
|
||||
endpoint,
|
||||
method: "DELETE",
|
||||
requestPayload: { vin, reasonLen: reason.length },
|
||||
responseStatus: 200,
|
||||
});
|
||||
return NextResponse.json({ ok: true, result });
|
||||
} catch (err) {
|
||||
const status = (err as { status?: number } | undefined)?.status ?? 500;
|
||||
const message = err instanceof Error ? err.message : "unknown";
|
||||
await writeAudit({
|
||||
projectKey: "sase",
|
||||
endpoint,
|
||||
method: "DELETE",
|
||||
requestPayload: { vin, reasonLen: reason.length },
|
||||
responseStatus: status,
|
||||
});
|
||||
return NextResponse.json({ ok: false, error: message }, { status });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user