← 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 });
}
}