← back to Norma

app/api/petitions/route.ts

208 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';

/**
 * GET /api/petitions
 * List petitions with optional filters:
 *   ?status=   — filter by status (active, closed, paused)
 *   ?platform= — filter by platform (moveon, actionnetwork, change_org, custom)
 *   ?category= — filter by category
 *   ?is_featured= — filter by featured flag (true/false)
 *   ?search=   — full-text search on title/description
 * Ordered by is_featured DESC, ai_urgency DESC, created_at DESC.
 */
export async function GET(request: NextRequest) {
  const auth = requireRole(request, 'admin', 'staff', 'pulse');
  if (auth instanceof NextResponse) return auth;

  try {
    const { searchParams } = new URL(request.url);
    const status = searchParams.get('status');
    const platform = searchParams.get('platform');
    const category = searchParams.get('category');
    const search = searchParams.get('search');

    const orgId = auth.role === 'admin' ? getOrgId(request) : auth.orgId;

    const conditions: string[] = [];
    const values: unknown[] = [];
    let paramIndex = 1;

    if (orgId) {
      conditions.push(`org_id = $${paramIndex}`);
      values.push(orgId);
      paramIndex++;
    }

    if (status) {
      conditions.push(`status = $${paramIndex}`);
      values.push(status);
      paramIndex++;
    }

    if (platform) {
      conditions.push(`platform = $${paramIndex}`);
      values.push(platform);
      paramIndex++;
    }

    if (category) {
      conditions.push(`category = $${paramIndex}`);
      values.push(category);
      paramIndex++;
    }

    const isFeatured = searchParams.get('is_featured');
    if (isFeatured !== null && isFeatured !== undefined && isFeatured !== '') {
      conditions.push(`is_featured = $${paramIndex}`);
      values.push(isFeatured === 'true');
      paramIndex++;
    }

    if (search) {
      conditions.push(
        `(title ILIKE $${paramIndex} OR description ILIKE $${paramIndex})`
      );
      values.push(`%${search}%`);
      paramIndex++;
    }

    let limit = parseInt(searchParams.get('limit') || '50', 10);
    if (isNaN(limit) || limit < 1) limit = 50;
    if (limit > 200) limit = 200;

    let offset = parseInt(searchParams.get('offset') || '0', 10);
    if (isNaN(offset) || offset < 0) offset = 0;

    const whereClause =
      conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';

    const countResult = await query(
      `SELECT COUNT(*)::int AS total FROM petitions ${whereClause}`,
      values
    );
    const total = countResult.rows[0]?.total ?? 0;

    const result = await query(
      `SELECT * FROM petitions
       ${whereClause}
       ORDER BY is_featured DESC, ai_urgency DESC NULLS LAST, created_at DESC
       LIMIT $${paramIndex} OFFSET $${paramIndex + 1}`,
      [...values, limit, offset]
    );

    return NextResponse.json({ petitions: result.rows, total, limit, offset });
  } catch (err) {
    console.error('[api/petitions] GET error:', (err as Error).message);
    return NextResponse.json({ error: 'Failed to fetch petitions' }, { status: 500 });
  }
}

/**
 * POST /api/petitions
 * Create a new petition.
 * Body: { title, description?, target?, category?, tags?, signature_count?,
 *         signature_goal?, ai_suggestion?, ai_urgency?, status?,
 *         url?, platform? }
 *
 * url + platform are optional — if omitted, defaults to 'custom' platform
 * and auto-generates a petition-site URL after insert.
 */
export async function POST(request: NextRequest) {
  const auth = requireRole(request, 'admin', 'staff');
  if (auth instanceof NextResponse) return auth;

  try {
    const body = await request.json();

    if (!body.title || typeof body.title !== 'string' || body.title.trim() === '') {
      return NextResponse.json({ error: 'title is required' }, { status: 400 });
    }

    const validPlatforms = ['moveon', 'actionnetwork', 'change_org', 'custom'];
    const platform = validPlatforms.includes(body.platform) ? body.platform : 'custom';

    // Accept any category string — normalize to lowercase
    const category = body.category
      ? String(body.category).toLowerCase().trim()
      : 'other';

    // Parse tags: accept either an array or a comma-separated string
    let tags: string[] = [];
    if (Array.isArray(body.tags)) {
      tags = body.tags.map((t: string) => String(t).trim()).filter(Boolean);
    } else if (typeof body.tags === 'string' && body.tags.trim()) {
      tags = body.tags
        .split(',')
        .map((t: string) => t.trim())
        .filter(Boolean);
    }

    const status = body.status === 'draft' ? 'draft' : 'active';
    const url = body.url?.trim() || 'pending'; // placeholder, updated after insert

    // Support 'body' field as petition content (stored in both body and description)
    const petitionBody = body.body || body.description || null;
    const talkingPoints = Array.isArray(body.talking_points)
      ? body.talking_points.map((t: string) => String(t).trim()).filter(Boolean)
      : [];

    const orgId = auth.role === 'admin' ? getOrgId(request) : auth.orgId;

    const result = await query(
      `INSERT INTO petitions
         (title, url, platform, description, body, target, category, tags,
          signature_count, signature_goal, ai_suggestion, ai_urgency, status,
          allow_signatures, talking_points, org_id)
       VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, true, $14, $15)
       RETURNING *`,
      [
        body.title.trim(),
        url,
        platform,
        petitionBody,
        petitionBody,
        body.target || null,
        category,
        tags,
        body.signature_count ? Number(body.signature_count) : 0,
        body.signature_goal ? Number(body.signature_goal) : 10000,
        body.ai_suggestion || null,
        body.ai_urgency ? Number(body.ai_urgency) : null,
        status,
        talkingPoints,
        orgId || null,
      ]
    );

    const petition = result.rows[0];

    // Auto-generate petition-site URL if none was provided
    if (!body.url || body.url === 'pending') {
      const petitionUrl = `http://45.61.58.125:7401/petition/${petition.id}`;
      await query(`UPDATE petitions SET url = $1 WHERE id = $2`, [petitionUrl, petition.id]);
      petition.url = petitionUrl;
    }

    const ip =
      request.headers.get('x-forwarded-for') ||
      request.headers.get('x-real-ip') ||
      undefined;
    await auditLog(
      'petition.created',
      'petition',
      petition.id,
      { title: petition.title, platform: petition.platform },
      ip
    );

    return NextResponse.json({ petition }, { status: 201 });
  } catch (err) {
    console.error('[api/petitions] POST error:', (err as Error).message);
    return NextResponse.json({ error: 'Failed to create petition' }, { status: 500 });
  }
}