mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-03-03 15:43:11 +08:00
- Implemented ConfigBackupService for backing up local configuration files. - Added ConfigSyncService to download configuration files from GitHub with remote-first conflict resolution. - Created VersionChecker to check application version against the latest GitHub release with caching. - Introduced security validation utilities for input validation to prevent common vulnerabilities. - Developed utility functions to start and stop Docusaurus documentation server.
324 lines
10 KiB
TypeScript
324 lines
10 KiB
TypeScript
/**
|
|
* Config Routes Module
|
|
* HTTP API endpoints for configuration backup and synchronization from GitHub
|
|
*
|
|
* Backup Endpoints:
|
|
* - POST /api/config/backup - Create backup
|
|
* - GET /api/config/backups - List backups
|
|
* - DELETE /api/config/backup/:name - Delete backup
|
|
* - POST /api/config/backup/:name/restore - Restore backup
|
|
*
|
|
* Sync Endpoints:
|
|
* - POST /api/config/sync - Sync config files from GitHub (remote-first)
|
|
* - GET /api/config/status - Get sync status (local vs remote comparison)
|
|
* - GET /api/config/remote - List available remote config files
|
|
*/
|
|
|
|
import type { RouteContext } from './types.js';
|
|
import { ConfigBackupService } from '../services/config-backup.js';
|
|
import { getConfigSyncService } from '../services/config-sync.js';
|
|
import { isValidBackupName, validateConfigDirs, validateGitHubParams } from '../../utils/security-validation.js';
|
|
|
|
/**
|
|
* Handle config routes
|
|
* @returns true if route was handled, false otherwise
|
|
*/
|
|
export async function handleConfigRoutes(ctx: RouteContext): Promise<boolean> {
|
|
const { pathname, req, res, handlePostRequest, broadcastToClients } = ctx;
|
|
|
|
// ========== CREATE BACKUP ==========
|
|
// POST /api/config/backup
|
|
if (pathname === '/api/config/backup' && req.method === 'POST') {
|
|
handlePostRequest(req, res, async (body: unknown) => {
|
|
try {
|
|
const { configDirs, backupName } = body as { configDirs?: string[]; backupName?: string };
|
|
|
|
// SECURITY: Validate inputs
|
|
if (backupName && !isValidBackupName(backupName)) {
|
|
return {
|
|
success: false,
|
|
error: 'Invalid backup name. Only alphanumeric, hyphen, underscore, and dot characters are allowed.'
|
|
};
|
|
}
|
|
|
|
if (configDirs) {
|
|
try {
|
|
validateConfigDirs(configDirs);
|
|
} catch (error) {
|
|
return {
|
|
success: false,
|
|
error: error instanceof Error ? error.message : String(error)
|
|
};
|
|
}
|
|
}
|
|
|
|
const backupService = new ConfigBackupService();
|
|
const result = await backupService.createBackup({ configDirs, backupName });
|
|
|
|
if (result.success) {
|
|
// Broadcast backup created event
|
|
broadcastToClients({
|
|
type: 'CONFIG_BACKUP_CREATED',
|
|
payload: {
|
|
backupPath: result.backupPath,
|
|
fileCount: result.fileCount,
|
|
timestamp: new Date().toISOString()
|
|
}
|
|
});
|
|
}
|
|
|
|
return result;
|
|
} catch (err) {
|
|
return { success: false, error: (err as Error).message, fileCount: 0 };
|
|
}
|
|
});
|
|
return true;
|
|
}
|
|
|
|
// ========== LIST BACKUPS ==========
|
|
// GET /api/config/backups
|
|
if (pathname === '/api/config/backups' && req.method === 'GET') {
|
|
try {
|
|
const backupService = new ConfigBackupService();
|
|
const backups = await backupService.listBackups();
|
|
|
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({ success: true, data: backups }));
|
|
} catch (err) {
|
|
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({ success: false, error: (err as Error).message }));
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// ========== DELETE BACKUP ==========
|
|
// DELETE /api/config/backup/:name
|
|
const deleteMatch = pathname.match(/^\/api\/config\/backup\/([^/]+)$/);
|
|
if (deleteMatch && req.method === 'DELETE') {
|
|
const backupName = deleteMatch[1];
|
|
|
|
// SECURITY: Validate backup name to prevent path traversal
|
|
if (!isValidBackupName(backupName)) {
|
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({
|
|
success: false,
|
|
error: 'Invalid backup name. Only alphanumeric, hyphen, underscore, and dot characters are allowed.'
|
|
}));
|
|
return true;
|
|
}
|
|
|
|
try {
|
|
const backupService = new ConfigBackupService();
|
|
const result = await backupService.deleteBackup(backupName);
|
|
|
|
if (result.success) {
|
|
// Broadcast backup deleted event
|
|
broadcastToClients({
|
|
type: 'CONFIG_BACKUP_DELETED',
|
|
payload: {
|
|
backupName,
|
|
timestamp: new Date().toISOString()
|
|
}
|
|
});
|
|
|
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify(result));
|
|
} else {
|
|
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify(result));
|
|
}
|
|
} catch (err) {
|
|
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({ success: false, error: (err as Error).message }));
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// ========== RESTORE BACKUP ==========
|
|
// POST /api/config/backup/:name/restore
|
|
const restoreMatch = pathname.match(/^\/api\/config\/backup\/([^/]+)\/restore$/);
|
|
if (restoreMatch && req.method === 'POST') {
|
|
const backupName = restoreMatch[1];
|
|
|
|
// SECURITY: Validate backup name to prevent path traversal
|
|
if (!isValidBackupName(backupName)) {
|
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({
|
|
success: false,
|
|
error: 'Invalid backup name. Only alphanumeric, hyphen, underscore, and dot characters are allowed.'
|
|
}));
|
|
return true;
|
|
}
|
|
|
|
try {
|
|
const backupService = new ConfigBackupService();
|
|
const result = await backupService.restoreBackup(backupName);
|
|
|
|
if (result.success) {
|
|
// Broadcast backup restored event
|
|
broadcastToClients({
|
|
type: 'CONFIG_BACKUP_RESTORED',
|
|
payload: {
|
|
backupName,
|
|
timestamp: new Date().toISOString()
|
|
}
|
|
});
|
|
|
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify(result));
|
|
} else {
|
|
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify(result));
|
|
}
|
|
} catch (err) {
|
|
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({ success: false, error: (err as Error).message }));
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// ========== SYNC CONFIG FROM GITHUB ==========
|
|
// POST /api/config/sync - Sync config files from GitHub (remote-first)
|
|
if (pathname === '/api/config/sync' && req.method === 'POST') {
|
|
handlePostRequest(req, res, async (body) => {
|
|
const { owner, repo, branch, configDirs, baseDir, overwrite } = body as {
|
|
owner?: string;
|
|
repo?: string;
|
|
branch?: string;
|
|
configDirs?: string[];
|
|
baseDir?: string;
|
|
overwrite?: boolean;
|
|
};
|
|
|
|
// SECURITY: Validate GitHub parameters (SSRF protection)
|
|
try {
|
|
validateGitHubParams({ owner, repo, branch });
|
|
|
|
// Validate config directories (path traversal protection)
|
|
if (configDirs) {
|
|
validateConfigDirs(configDirs);
|
|
}
|
|
} catch (error) {
|
|
return {
|
|
success: false,
|
|
error: error instanceof Error ? error.message : String(error)
|
|
};
|
|
}
|
|
|
|
const syncService = getConfigSyncService();
|
|
const result = await syncService.syncConfig({
|
|
owner,
|
|
repo,
|
|
branch,
|
|
configDirs,
|
|
baseDir,
|
|
overwrite: overwrite !== false, // default true
|
|
});
|
|
|
|
// Broadcast to connected dashboard clients on success
|
|
if (result.success && broadcastToClients) {
|
|
broadcastToClients({
|
|
type: 'CONFIG_SYNCED',
|
|
payload: {
|
|
syncedFiles: result.syncedFiles,
|
|
skippedFiles: result.skippedFiles,
|
|
timestamp: new Date().toISOString(),
|
|
},
|
|
});
|
|
}
|
|
|
|
return result;
|
|
});
|
|
return true;
|
|
}
|
|
|
|
// ========== GET SYNC STATUS ==========
|
|
// GET /api/config/status - Get sync status (local vs remote comparison)
|
|
if (pathname === '/api/config/status' && req.method === 'GET') {
|
|
try {
|
|
const url = ctx.url;
|
|
const owner = url.searchParams.get('owner') || undefined;
|
|
const repo = url.searchParams.get('repo') || undefined;
|
|
const branch = url.searchParams.get('branch') || undefined;
|
|
const configDirsParam = url.searchParams.get('configDirs');
|
|
const configDirs = configDirsParam ? configDirsParam.split(',') : undefined;
|
|
const baseDir = url.searchParams.get('baseDir') || undefined;
|
|
|
|
// SECURITY: Validate inputs
|
|
validateGitHubParams({ owner, repo, branch });
|
|
if (configDirs) {
|
|
validateConfigDirs(configDirs);
|
|
}
|
|
|
|
const syncService = getConfigSyncService();
|
|
const status = await syncService.getSyncStatus({
|
|
owner,
|
|
repo,
|
|
branch,
|
|
configDirs,
|
|
baseDir,
|
|
});
|
|
|
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({
|
|
success: true,
|
|
data: status,
|
|
timestamp: new Date().toISOString(),
|
|
}));
|
|
return true;
|
|
} catch (error: unknown) {
|
|
const message = error instanceof Error ? error.message : String(error);
|
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({
|
|
success: false,
|
|
error: message,
|
|
}));
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// ========== LIST REMOTE CONFIG FILES ==========
|
|
// GET /api/config/remote - List available remote config files
|
|
if (pathname === '/api/config/remote' && req.method === 'GET') {
|
|
try {
|
|
const url = ctx.url;
|
|
const owner = url.searchParams.get('owner') || undefined;
|
|
const repo = url.searchParams.get('repo') || undefined;
|
|
const branch = url.searchParams.get('branch') || undefined;
|
|
const configDir = url.searchParams.get('configDir') || '.claude';
|
|
|
|
// SECURITY: Validate inputs
|
|
validateGitHubParams({ owner, repo, branch });
|
|
validateConfigDirs([configDir]); // Single dir validation
|
|
|
|
const syncService = getConfigSyncService();
|
|
const files = await syncService.listRemoteFiles(configDir, {
|
|
owner,
|
|
repo,
|
|
branch,
|
|
});
|
|
|
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({
|
|
success: true,
|
|
data: {
|
|
configDir,
|
|
files,
|
|
},
|
|
timestamp: new Date().toISOString(),
|
|
}));
|
|
return true;
|
|
} catch (error: unknown) {
|
|
const message = error instanceof Error ? error.message : String(error);
|
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({
|
|
success: false,
|
|
error: message,
|
|
}));
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|