Closes checklist items F-01..F-06, D-01..D-28, S-01..S-10, C-01..C-07, B-01..B-07, R-01..R-02, O-03. Security (28 deletions + 10 auth gates): - Delete 28 unauthenticated debug/cursor/firebase/test routes - Gate ai/chat, ai/conversation, context/summarize, work-completed with withTenantProject/withAuth - Add HMAC-SHA256 signature verification to webhooks/coolify - Switch all admin secret comparisons to timingSafeStringEq Foundations (lib/server/*): - api-handler.ts: withAuth, withTenantProject, withWorkspace, withAdminSecret, withRateLimit - logger.ts: structured request-scoped logging with turnId - audit-log.ts: writeAuditLog helper + audit_log table - rate-limit.ts: Postgres sliding window rate limiter - coolify-webhook.ts: verifyCoolifySignature - timing-safe.ts: timingSafeStringEq Chat hardening (chat/route.ts): - MAX_TOOL_ROUNDS 15 → 8 (C-01) - Loop detection: hard-break at 3 identical fingerprints (was 5) (C-02) - Add 6-consecutive-tool-call hard-break (C-02) - Mode: respond first, act second prompt block (C-03) - SSE heartbeat every 25s via setInterval (C-04) - Per-tool 45s timeout via Promise.race (C-05) - turnId per-turn UUID for log correlation (C-06) - Recovery fires when roundsSinceText >= 4 (C-07) - SSE plan event on plan_task_add/edit (B-05) Beta features: - invites table + GET/POST /api/invites (P4.8) - invites/[token] validate + redeem (P4.8) - fs_project_dev_servers table + lib/server/dev-server-state.ts (P6.B1) - fs_project_secrets table + CRUD routes (P6.D2) - lib/integrations/brief-extract.ts (P3.7) Documentation: - app/api/ROUTES.md: full route map with auth + tenant
77 lines
2.3 KiB
TypeScript
77 lines
2.3 KiB
TypeScript
/**
|
|
* GET /api/ai/conversation?projectId=… — fetch saved conversation
|
|
* DELETE /api/ai/conversation?projectId=… — wipe saved conversation
|
|
*
|
|
* Closes S-02: was completely unauthenticated and accepted any projectId.
|
|
*/
|
|
import { NextResponse } from "next/server";
|
|
import { query } from "@/lib/db-postgres";
|
|
import { withTenantProject } from "@/lib/server/api-handler";
|
|
import { log } from "@/lib/server/logger";
|
|
|
|
const ENSURE_TABLE = `
|
|
CREATE TABLE IF NOT EXISTS chat_conversations (
|
|
project_id text PRIMARY KEY,
|
|
messages jsonb NOT NULL DEFAULT '[]',
|
|
updated_at timestamptz NOT NULL DEFAULT NOW()
|
|
)
|
|
`;
|
|
|
|
type StoredMessageRole = "user" | "assistant";
|
|
|
|
type ConversationMessage = {
|
|
role: StoredMessageRole;
|
|
content: string;
|
|
createdAt?: string;
|
|
};
|
|
|
|
type ConversationResponse = {
|
|
messages: ConversationMessage[];
|
|
};
|
|
|
|
export const GET = withTenantProject(
|
|
async (request, _ctx, { project }) => {
|
|
try {
|
|
await query(ENSURE_TABLE);
|
|
const rows = await query<{ messages: ConversationMessage[] }>(
|
|
`SELECT messages FROM chat_conversations WHERE project_id = $1`,
|
|
[project.id],
|
|
);
|
|
const messages: ConversationMessage[] = rows[0]?.messages ?? [];
|
|
const response: ConversationResponse = { messages };
|
|
return NextResponse.json(response);
|
|
} catch (err) {
|
|
log.error("ai/conversation GET failed", {
|
|
route: "api.ai.conversation",
|
|
projectId: project.id,
|
|
err: err instanceof Error ? err.message : String(err),
|
|
});
|
|
return NextResponse.json({ messages: [] });
|
|
}
|
|
},
|
|
{ source: "search", paramName: "projectId" },
|
|
);
|
|
|
|
export const DELETE = withTenantProject(
|
|
async (_request, _ctx, { project }) => {
|
|
try {
|
|
await query(ENSURE_TABLE);
|
|
await query(`DELETE FROM chat_conversations WHERE project_id = $1`, [
|
|
project.id,
|
|
]);
|
|
return NextResponse.json({ ok: true });
|
|
} catch (err) {
|
|
log.error("ai/conversation DELETE failed", {
|
|
route: "api.ai.conversation",
|
|
projectId: project.id,
|
|
err: err instanceof Error ? err.message : String(err),
|
|
});
|
|
return NextResponse.json(
|
|
{ error: "Failed to reset conversation" },
|
|
{ status: 500 },
|
|
);
|
|
}
|
|
},
|
|
{ source: "search", paramName: "projectId" },
|
|
);
|