import { NextResponse, type NextRequest } from 'next/server'; import { createClient } from '@supabase/supabase-js'; import { findViolations } from '@/lib/disallowed-phrases-guard'; export const dynamic = 'force-dynamic'; function slugify(input: string): string { return (input || 'submission') .toLowerCase() .replace(/[^a-z0-9\s-]/g, '') .trim() .replace(/\s+/g, '-') .replace(/-+/g, '-') .slice(0, 80) || 'submission'; } function randSuffix(): string { return Math.random().toString(36).slice(2, 8); } export async function POST(request: NextRequest) { const auth = request.headers.get('authorization') || ''; const expected = process.env.DISTRIBUTE_SHARED_SECRET; if (!expected) return NextResponse.json({ error: 'receiver not configured' }, { status: 500 }); if (!auth.startsWith('Bearer ') || auth.slice(7) !== expected) { return NextResponse.json({ error: 'unauthorized' }, { status: 401 }); } let body: any; try { body = await request.json(); } catch { return NextResponse.json({ error: 'invalid json' }, { status: 400 }); } const title = (body?.title || '').toString().trim(); const content = (body?.body || '').toString(); if (!title || !content) return NextResponse.json({ error: 'title and body are required' }, { status: 400 }); // Editorial guardrail: reject anything mentioning a disallowed phrase // (competitor publications, banned exhibitors, etc.) before it ever lands // in bb.native_articles. const violations = await findViolations(`${title}\n${content}`); if (violations.length > 0) { return NextResponse.json( { error: 'Submission rejected: contains disallowed phrases', violations }, { status: 422 }, ); } const contributor = body?.contributor || {}; const featuredImage = body?.featured_image_url || null; const attachments = Array.isArray(body?.attachments) ? body.attachments : []; const submissionId = body?.submission_id || null; const property = body?.property || 'broadcastbeat'; // Defense in depth: re-check the contributor's approval status against // distribute.users. distribute-rmp's submit route already gates, but a // belt-and-suspenders check here means even a leaked DISTRIBUTE_SHARED_SECRET // can't shove articles in from non-approved users. if (contributor?.id) { const url = process.env.NEXT_PUBLIC_SUPABASE_URL; const srKey = process.env.SUPABASE_SERVICE_ROLE_KEY; if (url && srKey) { try { const r = await fetch( `${url}/rest/v1/users?id=eq.${encodeURIComponent(contributor.id)}&select=status&limit=1`, { headers: { apikey: srKey, Authorization: `Bearer ${srKey}`, 'Accept-Profile': 'distribute', }, cache: 'no-store', }, ); const rows = (await r.json()) as Array<{ status: string }>; const status = Array.isArray(rows) && rows.length ? rows[0].status : null; if (status !== 'approved') { return NextResponse.json( { error: 'Contributor not approved', status }, { status: 403 }, ); } } catch { // If the lookup fails, fall back to letting the request through // (distribute-rmp side already gated). Don't break the pipeline on // a transient supabase glitch. } } } const url = process.env.NEXT_PUBLIC_SUPABASE_URL; const key = process.env.SUPABASE_SERVICE_ROLE_KEY; if (!url || !key) return NextResponse.json({ error: 'supabase not configured' }, { status: 500 }); const supabase = createClient(url, key, { auth: { persistSession: false } }); const baseSlug = slugify(title); const slug = `${baseSlug}-${randSuffix()}`; const excerpt = content .replace(/<[^>]+>/g, ' ') .replace(/\s+/g, ' ') .trim() .slice(0, 240); const { data: article, error: artErr } = await supabase .schema('bb') .from('native_articles') .insert({ slug, title, content, excerpt, author_name: contributor?.contact_name || contributor?.firm_name || 'PR Submission', category: 'latest', featured_image: featuredImage, status: 'pending_pr_rewrite', }) .select('id, slug') .single(); if (artErr) return NextResponse.json({ error: artErr.message, where: 'native_articles insert' }, { status: 500 }); const { error: subErr } = await supabase .schema('bb') .from('original_submissions') .insert({ submission_id: submissionId, property, title, body: content, featured_image_url: featuredImage, attachments, contributor, contributor_email: contributor?.email || null, raw_payload: body, native_article_id: article.id, }); if (subErr) return NextResponse.json({ error: subErr.message, where: 'original_submissions insert', article_id: article.id }, { status: 500 }); return NextResponse.json({ ok: true, article_id: article.id, slug: article.slug, status: 'pending_pr_rewrite' }, { status: 201 }); }