← back to Norma
app/api/statements/route.ts
142 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';
import { getBrand } from '@/lib/brand';
/**
* GET /api/statements
* List statements with optional filters: status, event_type, urgency
*/
export async function GET(request: NextRequest) {
const auth = requireRole(request, 'admin', 'staff');
if (auth instanceof NextResponse) return auth;
const sp = request.nextUrl.searchParams;
const status = sp.get('status');
const eventType = sp.get('event_type');
const urgency = sp.get('urgency');
const limit = Math.min(parseInt(sp.get('limit') || '50', 10), 200);
const offset = parseInt(sp.get('offset') || '0', 10);
const orgId = auth.role === 'admin' ? getOrgId(request) : auth.orgId;
const conditions: string[] = [];
const params: unknown[] = [];
let idx = 1;
if (orgId) {
conditions.push(`org_id = $${idx++}`);
params.push(orgId);
}
if (status) {
conditions.push(`status = $${idx++}`);
params.push(status);
}
if (eventType) {
conditions.push(`event_type = $${idx++}`);
params.push(eventType);
}
if (urgency) {
conditions.push(`urgency = $${idx++}`);
params.push(urgency);
}
const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
const [countRes, rowsRes] = await Promise.all([
query(`SELECT COUNT(*) FROM statements ${where}`, params),
query(
`SELECT id, event_type, event_title, event_url, event_date, title, quote,
quote_attribution, tone, category, urgency, tags, status,
published_at, published_channels, ai_confidence, version,
reviewed_by, reviewed_at, created_at, updated_at
FROM statements ${where}
ORDER BY created_at DESC
LIMIT $${idx++} OFFSET $${idx++}`,
[...params, limit, offset],
),
]);
return NextResponse.json({
statements: rowsRes.rows,
total: parseInt(countRes.rows[0].count, 10),
limit,
offset,
});
}
/**
* POST /api/statements
* Manually create a statement (not AI-generated).
*/
export async function POST(request: NextRequest) {
const auth = requireRole(request, 'admin', 'staff');
if (auth instanceof NextResponse) return auth;
try {
const data = await request.json();
if (!data.title || !data.body_html || !data.event_title) {
return NextResponse.json(
{ error: 'title, body_html, and event_title are required' },
{ status: 400 },
);
}
// Strip HTML for plain text
const bodyText = (data.body_html as string)
.replace(/<br\s*\/?>/gi, '\n')
.replace(/<\/p>/gi, '\n\n')
.replace(/<li>/gi, '- ')
.replace(/<\/li>/gi, '\n')
.replace(/<[^>]+>/g, '')
.replace(/\n{3,}/g, '\n\n')
.trim();
const orgId = auth.role === 'admin' ? getOrgId(request) : auth.orgId;
const brand = await getBrand(orgId);
const defaultAttribution = brand.staff?.[0]
? `${brand.staff[0].name}, ${brand.staff[0].role}`
: brand.name;
const result = await query(
`INSERT INTO statements
(event_type, event_title, event_summary, event_url, event_date,
title, body_html, body_text, quote, quote_attribution,
tone, category, urgency, tags, status, news_item_id, org_id)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17)
RETURNING *`,
[
data.event_type || 'news',
data.event_title,
data.event_summary || null,
data.event_url || null,
data.event_date || null,
data.title,
data.body_html,
bodyText,
data.quote || null,
data.quote_attribution || defaultAttribution,
data.tone || 'press_release',
data.category || null,
data.urgency || 'standard',
data.tags || [],
data.status || 'draft',
data.news_item_id || null,
orgId || null,
],
);
const ip = request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip') || undefined;
await auditLog('statement.created', 'statement', result.rows[0].id, { event_title: data.event_title }, ip);
return NextResponse.json({ statement: result.rows[0] }, { status: 201 });
} catch (err) {
console.error('[api/statements] POST error:', (err as Error).message);
return NextResponse.json({ error: 'Failed to create statement' }, { status: 500 });
}
}