← back to Norma

app/api/integrations/route.ts

187 lines

import { NextRequest, NextResponse } from 'next/server';
import { query } from '@/lib/db';
import { requireRole } from '@/lib/require-role';

/**
 * GET /api/integrations
 * List all connected services with status.
 */
export async function GET(request: NextRequest) {
  const auth = requireRole(request, 'admin', 'staff');
  if (auth instanceof NextResponse) return auth;

  const result = await query(
    `SELECT id, service_id, name, category, status, config, metadata,
            connected_by, connected_at, last_synced_at, error_message, updated_at
     FROM connected_services
     ORDER BY
       CASE status WHEN 'connected' THEN 0 WHEN 'partial' THEN 1 ELSE 2 END,
       name`
  );

  const connected = result.rows.filter((r: { status: string }) => r.status === 'connected').length;

  return NextResponse.json({
    services: result.rows,
    total: result.rows.length,
    connected,
  });
}

/**
 * PATCH /api/integrations
 * Update a service's config, status, or metadata.
 */
export async function PATCH(request: NextRequest) {
  const auth = requireRole(request, 'admin');
  if (auth instanceof NextResponse) return auth;

  const body = await request.json();
  const { service_id, status, config, metadata } = body;
  if (!service_id) return NextResponse.json({ error: 'service_id required' }, { status: 400 });

  const sets: string[] = ['updated_at = NOW()'];
  const values: unknown[] = [];
  let idx = 1;

  if (status) {
    sets.push(`status = $${idx++}`);
    values.push(status);
    if (status === 'connected') {
      sets.push(`connected_at = NOW()`);
      sets.push(`connected_by = 'admin'`);
      sets.push(`error_message = NULL`);
    }
  }
  if (config) {
    sets.push(`config = config || $${idx++}::jsonb`);
    values.push(JSON.stringify(config));
  }
  if (metadata) {
    sets.push(`metadata = metadata || $${idx++}::jsonb`);
    values.push(JSON.stringify(metadata));
  }

  values.push(service_id);
  const result = await query(
    `UPDATE connected_services SET ${sets.join(', ')} WHERE service_id = $${idx} RETURNING *`,
    values
  );

  if (result.rows.length === 0) {
    return NextResponse.json({ error: 'Service not found' }, { status: 404 });
  }

  return NextResponse.json({ service: result.rows[0] });
}

/**
 * POST /api/integrations
 * Test a service connection.
 */
export async function POST(request: NextRequest) {
  const auth = requireRole(request, 'admin');
  if (auth instanceof NextResponse) return auth;

  const { service_id, action } = await request.json();
  if (!service_id || action !== 'test') {
    return NextResponse.json({ error: 'service_id and action="test" required' }, { status: 400 });
  }

  // Service-specific connection tests
  const tests: Record<string, () => Promise<{ ok: boolean; message: string }>> = {
    slack: async () => {
      const webhookUrl = process.env.SLACK_WEBHOOK_URL;
      if (!webhookUrl) return { ok: false, message: 'SLACK_WEBHOOK_URL not set in .env.local' };
      try {
        const res = await fetch(webhookUrl, {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ text: 'Norma connection test - all systems go!' }),
        });
        return res.ok
          ? { ok: true, message: 'Message sent to Slack successfully' }
          : { ok: false, message: `Slack returned ${res.status}` };
      } catch (err) {
        return { ok: false, message: `Connection failed: ${(err as Error).message}` };
      }
    },
    clickup: async () => {
      const apiKey = process.env.CLICKUP_API_KEY;
      if (!apiKey) return { ok: false, message: 'CLICKUP_API_KEY not set in .env.local' };
      try {
        const res = await fetch('https://api.clickup.com/api/v2/user', {
          headers: { Authorization: apiKey },
        });
        if (res.ok) {
          const data = await res.json();
          return { ok: true, message: `Connected as ${data.user?.username || 'unknown'}` };
        }
        return { ok: false, message: `ClickUp returned ${res.status}` };
      } catch (err) {
        return { ok: false, message: `Connection failed: ${(err as Error).message}` };
      }
    },
    action_network: async () => {
      const apiKey = process.env.ACTION_NETWORK_API_KEY;
      if (!apiKey) return { ok: false, message: 'ACTION_NETWORK_API_KEY not set in .env.local' };
      try {
        const res = await fetch('https://actionnetwork.org/api/v2/petitions?page=1', {
          headers: { 'OSDI-API-Token': apiKey },
        });
        if (res.ok) {
          const data = await res.json();
          const count = data._embedded?.['osdi:petitions']?.length || 0;
          return { ok: true, message: `Connected — ${count} petitions on page 1` };
        }
        return { ok: false, message: `Action Network returned ${res.status}` };
      } catch (err) {
        return { ok: false, message: `Connection failed: ${(err as Error).message}` };
      }
    },
    fred_api: async () => {
      const apiKey = process.env.FRED_API_KEY;
      if (!apiKey) return { ok: false, message: 'FRED_API_KEY not set (free at stlouisfed.org)' };
      try {
        const res = await fetch(
          `https://api.stlouisfed.org/fred/series?series_id=SLOAS&api_key=${apiKey}&file_type=json`
        );
        if (res.ok) return { ok: true, message: 'Connected to St. Louis Fed FRED API' };
        return { ok: false, message: `FRED returned ${res.status}` };
      } catch (err) {
        return { ok: false, message: `Connection failed: ${(err as Error).message}` };
      }
    },
    census_api: async () => {
      const apiKey = process.env.CENSUS_API_KEY;
      if (!apiKey) return { ok: false, message: 'CENSUS_API_KEY not set (free at api.census.gov)' };
      try {
        const res = await fetch(
          `https://api.census.gov/data/2022/acs/acs5?get=NAME&for=state:06&key=${apiKey}`
        );
        if (res.ok) return { ok: true, message: 'Connected to US Census Bureau API' };
        return { ok: false, message: `Census returned ${res.status}` };
      } catch (err) {
        return { ok: false, message: `Connection failed: ${(err as Error).message}` };
      }
    },
  };

  const testFn = tests[service_id];
  if (!testFn) {
    return NextResponse.json({ ok: true, message: `${service_id} marked as connected (no test available)` });
  }

  const result = await testFn();

  // Update service status based on test result
  await query(
    `UPDATE connected_services
     SET status = $1, error_message = $2, last_synced_at = NOW(), updated_at = NOW()
     WHERE service_id = $3`,
    [result.ok ? 'connected' : 'error', result.ok ? null : result.message, service_id]
  );

  return NextResponse.json(result);
}