fix(api): complete error handling and add missing endpoints

- Add try-catch to all route handlers in settings, variant, config routes
- Implement 5 missing endpoints in cliproxy-stats-routes:
  - GET/PUT /config.yaml for YAML config management
  - GET /auth-files to list auth directory files
  - GET /auth-files/download for file download
  - PUT /models/:provider to update model settings
- All handlers now return proper error messages on failure
This commit is contained in:
kaitranntt committed 2025-12-21 18:18:46 -05:00
1 parent 85b0f17110
commit 3ed961fce9
4 files changed
+208 -1

No files matched your search

+172 -1
View File
@@ -3,6 +3,7 @@
*/ */
import { Router, Request, Response } from 'express'; import { Router, Request, Response } from 'express';
import * as fs from 'fs';
import * as path from 'path'; import * as path from 'path';
import { import {
fetchCliproxyStats, fetchCliproxyStats,
@@ -11,7 +12,11 @@ import {
fetchCliproxyErrorLogs, fetchCliproxyErrorLogs,
fetchCliproxyErrorLogContent, fetchCliproxyErrorLogContent,
} from '../../cliproxy/stats-fetcher'; } from '../../cliproxy/stats-fetcher';
import { getCliproxyWritablePath } from '../../cliproxy/config-generator'; import {
getCliproxyWritablePath,
getConfigPath,
getAuthDir,
} from '../../cliproxy/config-generator';
import { getProxyStatus as getProxyProcessStatus, stopProxy } from '../../cliproxy/session-tracker'; import { getProxyStatus as getProxyProcessStatus, stopProxy } from '../../cliproxy/session-tracker';
import { ensureCliproxyService } from '../../cliproxy/service-manager'; import { ensureCliproxyService } from '../../cliproxy/service-manager';
import { checkCliproxyUpdate } from '../../cliproxy/binary-manager'; import { checkCliproxyUpdate } from '../../cliproxy/binary-manager';
@@ -259,4 +264,170 @@ router.get('/error-logs/:name', async (req: Request, res: Response): Promise<voi
} }
}); });
// ==================== Config File ====================
/**
* GET /api/cliproxy/config.yaml - Get CLIProxy YAML config content
* Returns: plain text YAML content
*/
router.get('/config.yaml', async (_req: Request, res: Response): Promise<void> => {
try {
const configPath = getConfigPath();
if (!fs.existsSync(configPath)) {
res.status(404).json({ error: 'Config file not found' });
return;
}
const content = fs.readFileSync(configPath, 'utf8');
res.type('text/yaml').send(content);
} catch (error) {
res.status(500).json({ error: (error as Error).message });
}
});
/**
* PUT /api/cliproxy/config.yaml - Save CLIProxy YAML config content
* Body: { content: string }
* Returns: { success: true, path: string }
*/
router.put('/config.yaml', async (req: Request, res: Response): Promise<void> => {
try {
const { content } = req.body;
if (typeof content !== 'string') {
res.status(400).json({ error: 'Missing required field: content' });
return;
}
const configPath = getConfigPath();
// Ensure parent directory exists
const configDir = path.dirname(configPath);
if (!fs.existsSync(configDir)) {
fs.mkdirSync(configDir, { recursive: true });
}
// Write atomically
const tempPath = configPath + '.tmp';
fs.writeFileSync(tempPath, content);
fs.renameSync(tempPath, configPath);
res.json({ success: true, path: configPath });
} catch (error) {
res.status(500).json({ error: (error as Error).message });
}
});
// ==================== Auth Files ====================
/**
* GET /api/cliproxy/auth-files - List auth files in auth directory
* Returns: { files: Array<{ name, size, mtime }> }
*/
router.get('/auth-files', async (_req: Request, res: Response): Promise<void> => {
try {
const authDir = getAuthDir();
if (!fs.existsSync(authDir)) {
res.json({ files: [] });
return;
}
const entries = fs.readdirSync(authDir, { withFileTypes: true });
const files = entries
.filter((entry) => entry.isFile())
.map((entry) => {
const filePath = path.join(authDir, entry.name);
const stat = fs.statSync(filePath);
return {
name: entry.name,
size: stat.size,
mtime: stat.mtime.getTime(),
};
});
res.json({ files, directory: authDir });
} catch (error) {
res.status(500).json({ error: (error as Error).message });
}
});
/**
* GET /api/cliproxy/auth-files/download - Download auth file content
* Query: ?name=filename
* Returns: file content as octet-stream
*/
router.get('/auth-files/download', async (req: Request, res: Response): Promise<void> => {
try {
const { name } = req.query;
if (!name || typeof name !== 'string') {
res.status(400).json({ error: 'Missing required query parameter: name' });
return;
}
// Validate filename - prevent path traversal
if (name.includes('..') || name.includes('/') || name.includes('\\')) {
res.status(400).json({ error: 'Invalid filename' });
return;
}
const authDir = getAuthDir();
const filePath = path.join(authDir, name);
if (!fs.existsSync(filePath)) {
res.status(404).json({ error: 'Auth file not found' });
return;
}
const content = fs.readFileSync(filePath);
res.setHeader('Content-Disposition', `attachment; filename="${name}"`);
res.type('application/octet-stream').send(content);
} catch (error) {
res.status(500).json({ error: (error as Error).message });
}
});
// ==================== Model Updates ====================
/**
* PUT /api/cliproxy/models/:provider - Update model for a provider
* Body: { model: string }
* Returns: { success: true, provider, model }
*/
router.put('/models/:provider', async (req: Request, res: Response): Promise<void> => {
try {
const { provider } = req.params;
const { model } = req.body;
if (!model || typeof model !== 'string') {
res.status(400).json({ error: 'Missing required field: model' });
return;
}
// Get the settings file for this provider
const ccsDir = getCliproxyWritablePath();
const settingsPath = path.join(ccsDir, `${provider}.settings.json`);
if (!fs.existsSync(settingsPath)) {
res.status(404).json({ error: `Settings file not found for provider: ${provider}` });
return;
}
// Read and update settings
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
settings.env = settings.env || {};
settings.env.ANTHROPIC_MODEL = model;
// Write atomically
const tempPath = settingsPath + '.tmp';
fs.writeFileSync(tempPath, JSON.stringify(settings, null, 2) + '\n');
fs.renameSync(tempPath, settingsPath);
res.json({ success: true, provider, model });
} catch (error) {
res.status(500).json({ error: (error as Error).message });
}
});
export default router; export default router;
+4
View File
@@ -107,6 +107,7 @@ router.post('/migrate', async (req: Request, res: Response): Promise<void> => {
* POST /api/config/rollback - Rollback migration to JSON format * POST /api/config/rollback - Rollback migration to JSON format
*/ */
router.post('/rollback', async (req: Request, res: Response): Promise<void> => { router.post('/rollback', async (req: Request, res: Response): Promise<void> => {
try {
const { backupPath } = req.body; const { backupPath } = req.body;
if (!backupPath || typeof backupPath !== 'string') { if (!backupPath || typeof backupPath !== 'string') {
@@ -116,6 +117,9 @@ router.post('/rollback', async (req: Request, res: Response): Promise<void> => {
const success = await rollback(backupPath); const success = await rollback(backupPath);
res.json({ success }); res.json({ success });
} catch (error) {
res.status(500).json({ error: (error as Error).message });
}
}); });
export default router; export default router;
+24
View File
@@ -32,6 +32,7 @@ function maskApiKeys(settings: Settings): Settings {
* GET /api/settings/:profile - Get settings with masked API keys * GET /api/settings/:profile - Get settings with masked API keys
*/ */
router.get('/:profile', (req: Request, res: Response): void => { router.get('/:profile', (req: Request, res: Response): void => {
try {
const { profile } = req.params; const { profile } = req.params;
const ccsDir = getCcsDir(); const ccsDir = getCcsDir();
const settingsPath = path.join(ccsDir, `${profile}.settings.json`); const settingsPath = path.join(ccsDir, `${profile}.settings.json`);
@@ -51,12 +52,16 @@ router.get('/:profile', (req: Request, res: Response): void => {
mtime: stat.mtime.getTime(), mtime: stat.mtime.getTime(),
path: settingsPath, path: settingsPath,
}); });
} catch (error) {
res.status(500).json({ error: (error as Error).message });
}
}); });
/** /**
* GET /api/settings/:profile/raw - Get full settings (for editing) * GET /api/settings/:profile/raw - Get full settings (for editing)
*/ */
router.get('/:profile/raw', (req: Request, res: Response): void => { router.get('/:profile/raw', (req: Request, res: Response): void => {
try {
const { profile } = req.params; const { profile } = req.params;
const ccsDir = getCcsDir(); const ccsDir = getCcsDir();
const settingsPath = path.join(ccsDir, `${profile}.settings.json`); const settingsPath = path.join(ccsDir, `${profile}.settings.json`);
@@ -75,12 +80,16 @@ router.get('/:profile/raw', (req: Request, res: Response): void => {
mtime: stat.mtime.getTime(), mtime: stat.mtime.getTime(),
path: settingsPath, path: settingsPath,
}); });
} catch (error) {
res.status(500).json({ error: (error as Error).message });
}
}); });
/** /**
* PUT /api/settings/:profile - Update settings with conflict detection and backup * PUT /api/settings/:profile - Update settings with conflict detection and backup
*/ */
router.put('/:profile', (req: Request, res: Response): void => { router.put('/:profile', (req: Request, res: Response): void => {
try {
const { profile } = req.params; const { profile } = req.params;
const { settings, expectedMtime } = req.body; const { settings, expectedMtime } = req.body;
const ccsDir = getCcsDir(); const ccsDir = getCcsDir();
@@ -129,6 +138,9 @@ router.put('/:profile', (req: Request, res: Response): void => {
backupPath, backupPath,
created: !fileExists, created: !fileExists,
}); });
} catch (error) {
res.status(500).json({ error: (error as Error).message });
}
}); });
// ==================== Presets ==================== // ==================== Presets ====================
@@ -137,6 +149,7 @@ router.put('/:profile', (req: Request, res: Response): void => {
* GET /api/settings/:profile/presets - Get saved presets for a provider * GET /api/settings/:profile/presets - Get saved presets for a provider
*/ */
router.get('/:profile/presets', (req: Request, res: Response): void => { router.get('/:profile/presets', (req: Request, res: Response): void => {
try {
const { profile } = req.params; const { profile } = req.params;
const ccsDir = getCcsDir(); const ccsDir = getCcsDir();
const settingsPath = path.join(ccsDir, `${profile}.settings.json`); const settingsPath = path.join(ccsDir, `${profile}.settings.json`);
@@ -148,12 +161,16 @@ router.get('/:profile/presets', (req: Request, res: Response): void => {
const settings = loadSettings(settingsPath); const settings = loadSettings(settingsPath);
res.json({ presets: settings.presets || [] }); res.json({ presets: settings.presets || [] });
} catch (error) {
res.status(500).json({ error: (error as Error).message });
}
}); });
/** /**
* POST /api/settings/:profile/presets - Create a new preset * POST /api/settings/:profile/presets - Create a new preset
*/ */
router.post('/:profile/presets', (req: Request, res: Response): void => { router.post('/:profile/presets', (req: Request, res: Response): void => {
try {
const { profile } = req.params; const { profile } = req.params;
const { name, default: defaultModel, opus, sonnet, haiku } = req.body; const { name, default: defaultModel, opus, sonnet, haiku } = req.body;
@@ -191,12 +208,16 @@ router.post('/:profile/presets', (req: Request, res: Response): void => {
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n'); fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
res.status(201).json({ preset }); res.status(201).json({ preset });
} catch (error) {
res.status(500).json({ error: (error as Error).message });
}
}); });
/** /**
* DELETE /api/settings/:profile/presets/:name - Delete a preset * DELETE /api/settings/:profile/presets/:name - Delete a preset
*/ */
router.delete('/:profile/presets/:name', (req: Request, res: Response): void => { router.delete('/:profile/presets/:name', (req: Request, res: Response): void => {
try {
const { profile, name } = req.params; const { profile, name } = req.params;
const ccsDir = getCcsDir(); const ccsDir = getCcsDir();
const settingsPath = path.join(ccsDir, `${profile}.settings.json`); const settingsPath = path.join(ccsDir, `${profile}.settings.json`);
@@ -216,6 +237,9 @@ router.delete('/:profile/presets/:name', (req: Request, res: Response): void =>
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n'); fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
res.json({ success: true }); res.json({ success: true });
} catch (error) {
res.status(500).json({ error: (error as Error).message });
}
}); });
export default router; export default router;
+8
View File
@@ -78,6 +78,7 @@ router.post('/', (req: Request, res: Response): void => {
* PUT /api/cliproxy/:name - Update cliproxy variant * PUT /api/cliproxy/:name - Update cliproxy variant
*/ */
router.put('/:name', (req: Request, res: Response): void => { router.put('/:name', (req: Request, res: Response): void => {
try {
const { name } = req.params; const { name } = req.params;
const { provider, account, model } = req.body; const { provider, account, model } = req.body;
@@ -126,12 +127,16 @@ router.put('/:name', (req: Request, res: Response): void => {
settings: variant.settings, settings: variant.settings,
updated: true, updated: true,
}); });
} catch (error) {
res.status(500).json({ error: (error as Error).message });
}
}); });
/** /**
* DELETE /api/cliproxy/:name - Delete cliproxy variant * DELETE /api/cliproxy/:name - Delete cliproxy variant
*/ */
router.delete('/:name', (req: Request, res: Response): void => { router.delete('/:name', (req: Request, res: Response): void => {
try {
const { name } = req.params; const { name } = req.params;
const config = readConfigSafe(); const config = readConfigSafe();
@@ -154,6 +159,9 @@ router.delete('/:name', (req: Request, res: Response): void => {
writeConfig(config); writeConfig(config);
res.json({ name, deleted: true }); res.json({ name, deleted: true });
} catch (error) {
res.status(500).json({ error: (error as Error).message });
}
}); });
export default router; export default router;