← back to Norma
app/api/statements/[id]/route.ts
134 lines
import { NextRequest, NextResponse } from 'next/server';
import { query } from '@/lib/db';
import { requireRole } from '@/lib/require-role';
import { auditLog } from '@/lib/audit';
import { getOrgId } from '@/lib/orgId';
type RouteContext = { params: Promise<{ id: string }> };
/**
* GET /api/statements/[id]
*/
export async function GET(request: NextRequest, ctx: RouteContext) {
const auth = requireRole(request, 'admin', 'staff');
if (auth instanceof NextResponse) return auth;
const { id } = await ctx.params;
const orgId = auth.role === 'admin' ? getOrgId(request) : auth.orgId;
const orgFilter = orgId ? ' AND org_id = $2' : '';
const result = await query(`SELECT * FROM statements WHERE id = $1${orgFilter}`, orgId ? [id, orgId] : [id]);
if (!result.rows[0]) {
return NextResponse.json({ error: 'Statement not found' }, { status: 404 });
}
return NextResponse.json({ statement: result.rows[0] });
}
/**
* PUT /api/statements/[id]
* Update a statement. Saves previous version before overwriting.
*/
export async function PUT(request: NextRequest, ctx: RouteContext) {
const auth = requireRole(request, 'admin', 'staff');
if (auth instanceof NextResponse) return auth;
const { id } = await ctx.params;
try {
const data = await request.json();
const orgId = auth.role === 'admin' ? getOrgId(request) : auth.orgId;
const orgFilter = orgId ? ' AND org_id = $2' : '';
// Fetch current version to save history
const current = await query(`SELECT * FROM statements WHERE id = $1${orgFilter}`, orgId ? [id, orgId] : [id]);
if (!current.rows[0]) {
return NextResponse.json({ error: 'Statement not found' }, { status: 404 });
}
const prev = current.rows[0];
// Save version history
await query(
`INSERT INTO statement_versions (statement_id, version, title, body_html, body_text, quote, edited_by)
VALUES ($1, $2, $3, $4, $5, $6, $7)`,
[id, prev.version, prev.title, prev.body_html, prev.body_text, prev.quote, 'user'],
);
// Build dynamic update
const fields: string[] = [];
const values: unknown[] = [];
let idx = 1;
const allowedFields = [
'title', 'body_html', 'body_text', 'quote', 'quote_attribution',
'event_type', 'event_title', 'event_summary', 'event_url', 'event_date',
'tone', 'category', 'urgency', 'tags', 'status',
'reviewed_by', 'review_notes',
];
for (const field of allowedFields) {
if (data[field] !== undefined) {
fields.push(`${field} = $${idx++}`);
values.push(data[field]);
}
}
if (fields.length === 0) {
return NextResponse.json({ error: 'No fields to update' }, { status: 400 });
}
// Auto-set review timestamp when status changes to reviewed/approved
if (data.status === 'reviewed' || data.status === 'approved') {
fields.push(`reviewed_at = NOW()`);
}
if (data.status === 'published') {
fields.push(`published_at = NOW()`);
}
fields.push(`version = version + 1`);
fields.push(`updated_at = NOW()`);
values.push(id);
idx++;
let stmtOrgFilter = '';
if (orgId) {
values.push(orgId);
stmtOrgFilter = ` AND org_id = $${idx}`;
}
const result = await query(
`UPDATE statements SET ${fields.join(', ')} WHERE id = $${idx - 1}${stmtOrgFilter} RETURNING *`,
values,
);
const ip = request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip') || undefined;
await auditLog('statement.updated', 'statement', id, { fields: Object.keys(data) }, ip);
return NextResponse.json({ statement: result.rows[0] });
} catch (err) {
console.error('[api/statements/[id]] PUT error:', (err as Error).message);
return NextResponse.json({ error: 'Failed to update statement' }, { status: 500 });
}
}
/**
* DELETE /api/statements/[id]
*/
export async function DELETE(request: NextRequest, ctx: RouteContext) {
const auth = requireRole(request, 'admin', 'staff');
if (auth instanceof NextResponse) return auth;
const { id } = await ctx.params;
const orgId = auth.role === 'admin' ? getOrgId(request) : auth.orgId;
const orgFilter = orgId ? ' AND org_id = $2' : '';
const result = await query(`DELETE FROM statements WHERE id = $1${orgFilter} RETURNING id`, orgId ? [id, orgId] : [id]);
if (!result.rows[0]) {
return NextResponse.json({ error: 'Statement not found' }, { status: 404 });
}
const ip = request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip') || undefined;
await auditLog('statement.deleted', 'statement', id, {}, ip);
return NextResponse.json({ deleted: true });
}