← back to Patty
app/api/settings/credentials/route.ts
147 lines
import { NextRequest, NextResponse } from 'next/server';
import { exec } from 'child_process';
import { promisify } from 'util';
import fs from 'fs';
import path from 'path';
import { query } from '@/lib/db';
import { verifyAuth } from '@/lib/auth';
const execAsync = promisify(exec);
const AGENTS_BASE = '/root/Projects/SDCC/agents';
const AGENT_ENV_MAP: Record<string, { dir: string; pm2Name: string; vars: string[] }> = {
twitter: {
dir: path.join(AGENTS_BASE, 'twitter-agent'),
pm2Name: 'sdcc-twitter',
vars: ['TWITTER_API_KEY', 'TWITTER_API_SECRET', 'TWITTER_ACCESS_TOKEN', 'TWITTER_ACCESS_SECRET', 'TWITTER_BEARER_TOKEN'],
},
reddit: {
dir: path.join(AGENTS_BASE, 'reddit-agent'),
pm2Name: 'sdcc-reddit',
vars: ['REDDIT_CLIENT_ID', 'REDDIT_CLIENT_SECRET', 'REDDIT_USERNAME', 'REDDIT_PASSWORD'],
},
discord: {
dir: path.join(AGENTS_BASE, 'discord-agent'),
pm2Name: 'sdcc-discord',
vars: ['DISCORD_BOT_TOKEN', 'DISCORD_DEFAULT_CHANNEL_ID'],
},
bluesky: {
dir: path.join(AGENTS_BASE, 'bluesky-agent'),
pm2Name: 'sdcc-bluesky',
vars: ['BSKY_HANDLE', 'BSKY_PASSWORD'],
},
};
function maskValue(val: string): string {
if (!val || val.length < 4) return '***';
return val.slice(0, 3) + '*'.repeat(Math.min(val.length - 3, 12));
}
function buildEnvFile(vars: Record<string, string>): string {
return Object.entries(vars)
.filter(([, v]) => v)
.map(([k, v]) => `${k}=${v}`)
.join('\n') + '\n';
}
/**
* GET /api/settings/credentials
* Returns masked credential status per platform.
*/
export async function GET(request: NextRequest) {
const user = verifyAuth(request);
if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
const result: Record<string, Record<string, string>> = {};
for (const [platform, cfg] of Object.entries(AGENT_ENV_MAP)) {
const envPath = path.join(cfg.dir, '.env');
result[platform] = {};
if (fs.existsSync(envPath)) {
const lines = fs.readFileSync(envPath, 'utf8').split('\n');
for (const line of lines) {
const [key, ...rest] = line.split('=');
const val = rest.join('=').trim();
if (key && cfg.vars.includes(key.trim())) {
result[platform][key.trim()] = val ? maskValue(val) : '';
}
}
}
// Ensure all expected vars appear (empty if not set)
for (const v of cfg.vars) {
if (!(v in result[platform])) result[platform][v] = '';
}
}
return NextResponse.json({ credentials: result });
}
/**
* POST /api/settings/credentials
* Saves credentials to .env files and restarts agents.
* Body: { platform: 'twitter'|'reddit'|'discord'|'bluesky', credentials: { KEY: 'val', ... } }
*/
export async function POST(request: NextRequest) {
const user = verifyAuth(request);
if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
try {
const body = await request.json();
const { platform, credentials } = body as { platform: string; credentials: Record<string, string> };
const cfg = AGENT_ENV_MAP[platform];
if (!cfg) {
return NextResponse.json({ error: `Unknown platform: ${platform}` }, { status: 400 });
}
// Merge with existing .env (only update provided non-empty keys)
const envPath = path.join(cfg.dir, '.env');
const existing: Record<string, string> = {};
if (fs.existsSync(envPath)) {
const lines = fs.readFileSync(envPath, 'utf8').split('\n');
for (const line of lines) {
const eq = line.indexOf('=');
if (eq > 0) {
existing[line.slice(0, eq).trim()] = line.slice(eq + 1).trim();
}
}
}
const merged: Record<string, string> = { ...existing };
for (const [k, v] of Object.entries(credentials)) {
if (v && v.trim()) merged[k] = v.trim();
}
fs.writeFileSync(envPath, buildEnvFile(merged), 'utf8');
// Restart the agent so it picks up new env vars
let restartResult = 'ok';
try {
await execAsync(`pm2 restart ${cfg.pm2Name}`);
} catch (err) {
restartResult = (err as Error).message;
}
// Log to DB audit
await query(
`INSERT INTO audit_events (event_type, entity_type, entity_id, metadata)
VALUES ('credentials.saved', 'platform', $1, $2)`,
[platform, JSON.stringify({ keys_set: Object.keys(credentials).filter(k => credentials[k]), restart: restartResult })]
).catch(() => {});
return NextResponse.json({
platform,
keys_saved: Object.keys(merged).length,
restarted: restartResult === 'ok',
restart_output: restartResult === 'ok' ? undefined : restartResult,
});
} catch (err) {
console.error('[credentials] POST error:', (err as Error).message);
return NextResponse.json({ error: 'Failed to save credentials' }, { status: 500 });
}
}