← back to Norma

app/api/settings/intelligence/route.ts

141 lines

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

/** Mask a secret: show first 4 chars + dots, or empty if blank */
function maskValue(val: string | undefined): string {
  if (!val || !val.trim()) return '';
  if (val.length <= 6) return '••••••••';
  return val.slice(0, 4) + '••••••••';
}

/**
 * GET /api/settings/intelligence
 * Returns current intelligence settings from nonprofit_accounts.api_connections.
 * API keys are MASKED by default. Use ?reveal=key_name to get one unmasked key.
 * Uses X-Org-Id header to identify which org.
 */
export async function GET(request: NextRequest) {
  const auth = requireRole(request, 'admin');
  if (auth instanceof NextResponse) return auth;

  const orgId = request.headers.get('x-org-id');
  if (!orgId) {
    return NextResponse.json({ error: 'Missing X-Org-Id header' }, { status: 400 });
  }

  try {
    const result = await query(
      `SELECT api_connections, org_email FROM nonprofit_accounts WHERE id = $1`,
      [orgId]
    );

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

    const row = result.rows[0];
    const apiConnections = row.api_connections || {};
    const rawKeys = apiConnections.api_keys || {};
    const revealKey = new URL(request.url).searchParams.get('reveal');

    // Build masked keys with has_value flags
    const ALL_KEYS = ['grants_gov', 'congress_gov', 'regulations_gov', 'sam_gov', 'action_network', 'charity_navigator', 'open_states'];
    const maskedKeys: Record<string, string> = {};
    const hasValue: Record<string, boolean> = {};

    for (const k of ALL_KEYS) {
      const real = rawKeys[k] || '';
      hasValue[k] = !!real.trim();
      // If this specific key is being revealed, return the real value
      maskedKeys[k] = (revealKey === k) ? real : maskValue(real);
    }

    return NextResponse.json({
      intelligence: apiConnections.intelligence || {
        cron_enabled: false,
        digest_enabled: false,
        digest_day: 'wednesday',
        digest_email: row.org_email || '',
        last_cron_run: null,
      },
      api_keys: maskedKeys,
      api_keys_has_value: hasValue,
    });
  } catch (err) {
    console.error('[api/settings/intelligence] GET error:', (err as Error).message);
    return NextResponse.json({ error: 'Failed to fetch intelligence settings' }, { status: 500 });
  }
}

/**
 * PUT /api/settings/intelligence
 * Updates api_connections JSONB for the org (merge, not replace).
 * Body: { intelligence?: {...}, api_keys?: {...} }
 */
export async function PUT(request: NextRequest) {
  const auth = requireRole(request, 'admin');
  if (auth instanceof NextResponse) return auth;

  const orgId = request.headers.get('x-org-id');
  if (!orgId) {
    return NextResponse.json({ error: 'Missing X-Org-Id header' }, { status: 400 });
  }

  try {
    const body = await request.json();
    const { intelligence, api_keys } = body;

    // Fetch current api_connections
    const current = await query(
      `SELECT api_connections FROM nonprofit_accounts WHERE id = $1`,
      [orgId]
    );

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

    const existing = current.rows[0].api_connections || {};

    // Merge — do not replace the entire object
    const updated = { ...existing };

    if (intelligence !== undefined) {
      updated.intelligence = {
        ...(existing.intelligence || {}),
        ...intelligence,
      };
    }

    if (api_keys !== undefined) {
      const existingKeys = existing.api_keys || {};
      const mergedKeys = { ...existingKeys };
      for (const [k, v] of Object.entries(api_keys as Record<string, string>)) {
        // Skip masked placeholders — only save real user-entered values
        if (v && !v.includes('••')) {
          mergedKeys[k] = v;
        }
      }
      updated.api_keys = mergedKeys;
    }

    // Save back
    await query(
      `UPDATE nonprofit_accounts
       SET api_connections = $1, updated_at = now()
       WHERE id = $2`,
      [JSON.stringify(updated), orgId]
    );

    return NextResponse.json({
      success: true,
      intelligence: updated.intelligence || {},
      api_keys: updated.api_keys || {},
    });
  } catch (err) {
    console.error('[api/settings/intelligence] PUT error:', (err as Error).message);
    return NextResponse.json({ error: 'Failed to save intelligence settings' }, { status: 500 });
  }
}