c24bdde603
- approve: validates token, checks deliverable ownership via phase→client join, sets status=approved + approved_at=now() only if approved_at is currently null (CLAUDE.md immutability rule enforced) - comment: validates token, checks entity ownership (task or deliverable) via phase→client chain, inserts comment with author='client' - both routes return 404 on invalid token or unknown entity - neither route references quote_items (CLAUDE.md constraint enforced) - Zod validation on comment body: min 1 char, max 2000 chars (T-02-20 DoS mitigation)
58 lines
2.1 KiB
TypeScript
58 lines
2.1 KiB
TypeScript
import { NextRequest, NextResponse } from "next/server";
|
|
import { eq, and } from "drizzle-orm";
|
|
import { db } from "@/db";
|
|
import { clients, deliverables, tasks, phases } from "@/db/schema";
|
|
|
|
export async function POST(request: NextRequest) {
|
|
try {
|
|
const body = await request.json();
|
|
const { token, deliverableId } = body as { token?: string; deliverableId?: string };
|
|
|
|
if (!token || !deliverableId) {
|
|
return NextResponse.json({ error: "token e deliverableId richiesti" }, { status: 400 });
|
|
}
|
|
|
|
// Validate token — find the client
|
|
const clientRows = await db
|
|
.select({ id: clients.id })
|
|
.from(clients)
|
|
.where(eq(clients.token, token))
|
|
.limit(1);
|
|
|
|
if (clientRows.length === 0) {
|
|
return NextResponse.json({ error: "Token non valido" }, { status: 404 });
|
|
}
|
|
|
|
const clientId = clientRows[0].id;
|
|
|
|
// Verify deliverable belongs to this client (prevents cross-client approval)
|
|
// deliverable → task → phase → client
|
|
const ownershipCheck = await db
|
|
.select({ deliverable_id: deliverables.id, approved_at: deliverables.approved_at })
|
|
.from(deliverables)
|
|
.innerJoin(tasks, eq(deliverables.task_id, tasks.id))
|
|
.innerJoin(phases, and(eq(tasks.phase_id, phases.id), eq(phases.client_id, clientId)))
|
|
.where(eq(deliverables.id, deliverableId))
|
|
.limit(1);
|
|
|
|
if (ownershipCheck.length === 0) {
|
|
return NextResponse.json({ error: "Deliverable non trovato" }, { status: 404 });
|
|
}
|
|
|
|
// IMMUTABILITY RULE (CLAUDE.md): if approved_at is already set, this is a no-op
|
|
if (ownershipCheck[0].approved_at !== null) {
|
|
return NextResponse.json({ approved: true, message: "Già approvato" }, { status: 200 });
|
|
}
|
|
|
|
// Set approved — approved_at is immutable once set, client cannot unset it
|
|
await db
|
|
.update(deliverables)
|
|
.set({ status: "approved", approved_at: new Date() })
|
|
.where(eq(deliverables.id, deliverableId));
|
|
|
|
return NextResponse.json({ approved: true }, { status: 200 });
|
|
} catch (err) {
|
|
console.error("/api/client/approve error:", err);
|
|
return NextResponse.json({ error: "Errore interno" }, { status: 500 });
|
|
}
|
|
} |