← back to Norma
app/api/settings/credentials/route.ts
113 lines
import { NextRequest, NextResponse } from 'next/server';
import { query } from '@/lib/db';
import { requireRole } from '@/lib/require-role';
const SETTINGS_KEY = 'api_credentials';
/** Mask a secret: show first 4 chars + dots, or empty if blank */
function maskSecret(val: string | undefined): string {
if (!val || !val.trim()) return '';
if (val.length <= 6) return '••••••••';
return val.slice(0, 4) + '••••••••';
}
/**
* GET /api/settings/credentials
* Return stored API credentials with values MASKED.
* Use ?reveal=serviceId to get unmasked values for one service.
*/
export async function GET(request: NextRequest) {
const auth = requireRole(request, 'admin');
if (auth instanceof NextResponse) return auth;
try {
const result = await query(
`SELECT value FROM app_settings WHERE key = $1`,
[SETTINGS_KEY]
);
if (result.rows.length === 0) {
return NextResponse.json({ credentials: {}, has_values: {} });
}
const credentials = typeof result.rows[0].value === 'string'
? JSON.parse(result.rows[0].value)
: result.rows[0].value;
const revealService = new URL(request.url).searchParams.get('reveal');
// Mask all credential values except the revealed service
const masked: Record<string, Record<string, string>> = {};
const hasValues: Record<string, Record<string, boolean>> = {};
for (const [serviceId, serviceCreds] of Object.entries(credentials as Record<string, Record<string, string>>)) {
masked[serviceId] = {};
hasValues[serviceId] = {};
for (const [key, val] of Object.entries(serviceCreds || {})) {
hasValues[serviceId][key] = !!val?.trim();
masked[serviceId][key] = (revealService === serviceId) ? val : maskSecret(val);
}
}
return NextResponse.json({ credentials: masked, has_values: hasValues });
} catch (err) {
console.error('[api/settings/credentials] GET error:', (err as Error).message);
return NextResponse.json({ error: 'Failed to fetch credentials' }, { status: 500 });
}
}
/**
* PATCH /api/settings/credentials
* Update credentials for a single service.
* Body: { serviceId: string, credentials: { key: value, ... } }
* Skips masked placeholder values (containing ••) to prevent overwriting real secrets.
*/
export async function PATCH(request: NextRequest) {
const auth = requireRole(request, 'admin');
if (auth instanceof NextResponse) return auth;
try {
const body = await request.json();
const { serviceId, credentials: serviceCreds } = body;
if (!serviceId || !serviceCreds) {
return NextResponse.json({ error: 'Missing serviceId or credentials' }, { status: 400 });
}
// Fetch existing credentials
const result = await query(
`SELECT value FROM app_settings WHERE key = $1`,
[SETTINGS_KEY]
);
let allCredentials: Record<string, Record<string, string>> = {};
if (result.rows.length > 0) {
allCredentials = typeof result.rows[0].value === 'string'
? JSON.parse(result.rows[0].value)
: result.rows[0].value;
}
// Merge service credentials — skip masked placeholders
const existing = allCredentials[serviceId] || {};
for (const [k, v] of Object.entries(serviceCreds as Record<string, string>)) {
if (v && !v.includes('••')) {
existing[k] = v;
}
}
allCredentials[serviceId] = existing;
// Save back
await query(
`INSERT INTO app_settings (key, value, updated_at)
VALUES ($1, $2, now())
ON CONFLICT (key) DO UPDATE SET value = $2, updated_at = now()`,
[SETTINGS_KEY, JSON.stringify(allCredentials)]
);
return NextResponse.json({ success: true, serviceId });
} catch (err) {
console.error('[api/settings/credentials] PATCH error:', (err as Error).message);
return NextResponse.json({ error: 'Failed to save credentials' }, { status: 500 });
}
}