← back to Grant
app/api/outreach/route.ts
84 lines
import { NextRequest, NextResponse } from 'next/server';
import { query } from '@/lib/db';
import { verifyAuthWithOrg, resolveOrgId } from '@/lib/auth';
import { sanitizeProposalBody, bodyHtmlTooLarge } from '@/lib/sanitize';
/* ─── GET /api/outreach ───────────────────────────────────────────────────── */
export async function GET(request: NextRequest) {
const session = verifyAuthWithOrg(request);
if (!session) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
try {
const url = new URL(request.url);
const templateType = url.searchParams.get('template_type');
const targetType = url.searchParams.get('target_type');
const orgId = await resolveOrgId(session);
if (!orgId) return NextResponse.json({ rows: [] });
let sql = 'SELECT id, org_id, template_type, target_type, title, subject, body_html, body_text, is_ai_generated, version, created_at, updated_at FROM outreach_templates WHERE org_id = $1';
const params: unknown[] = [orgId];
let paramIdx = 2;
if (templateType && templateType !== 'all') {
sql += ` AND template_type = $${paramIdx}`;
params.push(templateType);
paramIdx++;
}
if (targetType && targetType !== 'all') {
sql += ` AND target_type = $${paramIdx}`;
params.push(targetType);
paramIdx++;
}
sql += ' ORDER BY created_at DESC';
const result = await query(sql, params);
return NextResponse.json({ rows: result.rows });
} catch (err) {
console.error('[outreach GET]', err);
return NextResponse.json({ error: 'Failed to fetch templates' }, { status: 500 });
}
}
/* ─── POST /api/outreach ──────────────────────────────────────────────────── */
export async function POST(request: NextRequest) {
const session = verifyAuthWithOrg(request);
if (!session) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
try {
const body = await request.json();
const orgId = await resolveOrgId(session);
if (!orgId) return NextResponse.json({ error: 'No organization found' }, { status: 400 });
// 2026-05-30 (audit P1-5): sanitize body_html at the storage boundary —
// outreach templates were stored raw while proposals were sanitized.
if (bodyHtmlTooLarge(body.body_html ?? '')) {
return NextResponse.json({ error: 'body_html too large or invalid' }, { status: 400 });
}
const cleanHtml = sanitizeProposalBody(body.body_html || '');
const result = await query(
`INSERT INTO outreach_templates (org_id, template_type, target_type, title, subject, body_html, body_text, is_ai_generated)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
RETURNING id, org_id, template_type, target_type, title, subject, body_html, body_text, is_ai_generated, version, created_at, updated_at`,
[
orgId,
body.template_type || 'introduction',
body.target_type || 'general',
body.title || 'Untitled Template',
body.subject || null,
cleanHtml,
body.body_text || '',
body.is_ai_generated !== undefined ? body.is_ai_generated : false,
],
);
return NextResponse.json(result.rows[0], { status: 201 });
} catch (err) {
console.error('[outreach POST]', err);
return NextResponse.json({ error: 'Failed to create template' }, { status: 500 });
}
}