mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-13 18:15:23 +00:00
Merge pull request #174 from kaitranntt/kai/fix/add-cliproxy-account
fix(api): resolve CLIProxy account management errors
This commit is contained in:
10 files changed
+636
-350
No files matched your search
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* Account Routes - CRUD operations for Claude accounts (profiles.json)
|
||||
*
|
||||
* Separated from profile-routes.ts to avoid dual-mounting conflicts.
|
||||
*/
|
||||
|
||||
import { Router, Request, Response } from 'express';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { getCcsDir } from '../../utils/config-manager';
|
||||
|
||||
const router = Router();
|
||||
|
||||
/**
|
||||
* GET /api/accounts - List accounts from profiles.json
|
||||
*/
|
||||
router.get('/', (_req: Request, res: Response): void => {
|
||||
try {
|
||||
const profilesPath = path.join(getCcsDir(), 'profiles.json');
|
||||
|
||||
if (!fs.existsSync(profilesPath)) {
|
||||
res.json({ accounts: [], default: null });
|
||||
return;
|
||||
}
|
||||
|
||||
const data = JSON.parse(fs.readFileSync(profilesPath, 'utf8'));
|
||||
const accounts = Object.entries(data.profiles || {}).map(([name, meta]) => {
|
||||
const metadata = meta as Record<string, unknown>;
|
||||
return {
|
||||
name,
|
||||
...metadata,
|
||||
};
|
||||
});
|
||||
|
||||
res.json({ accounts, default: data.default || null });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: (error as Error).message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/accounts/default - Set default account
|
||||
*/
|
||||
router.post('/default', (req: Request, res: Response): void => {
|
||||
try {
|
||||
const { name } = req.body;
|
||||
|
||||
if (!name) {
|
||||
res.status(400).json({ error: 'Missing required field: name' });
|
||||
return;
|
||||
}
|
||||
|
||||
const profilesPath = path.join(getCcsDir(), 'profiles.json');
|
||||
|
||||
const data = fs.existsSync(profilesPath)
|
||||
? JSON.parse(fs.readFileSync(profilesPath, 'utf8'))
|
||||
: { profiles: {} };
|
||||
|
||||
data.default = name;
|
||||
fs.writeFileSync(profilesPath, JSON.stringify(data, null, 2) + '\n');
|
||||
|
||||
res.json({ default: name });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: (error as Error).message });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -32,57 +32,61 @@ const validProviders: CLIProxyProvider[] = ['gemini', 'codex', 'agy', 'qwen', 'i
|
||||
* GET /api/cliproxy/auth - Get auth status for built-in CLIProxy profiles
|
||||
* Also fetches CLIProxyAPI stats to update lastUsedAt for active providers
|
||||
*/
|
||||
router.get('/', async (_req: Request, res: Response) => {
|
||||
// Initialize accounts from existing tokens on first request
|
||||
initializeAccounts();
|
||||
router.get('/', async (_req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
// Initialize accounts from existing tokens on first request
|
||||
initializeAccounts();
|
||||
|
||||
// Fetch CLIProxyAPI usage stats to determine active providers
|
||||
const stats = await fetchCliproxyStats();
|
||||
// Fetch CLIProxyAPI usage stats to determine active providers
|
||||
const stats = await fetchCliproxyStats();
|
||||
|
||||
// Map CLIProxyAPI provider names to our internal provider names
|
||||
const statsProviderMap: Record<string, CLIProxyProvider> = {
|
||||
gemini: 'gemini',
|
||||
antigravity: 'agy',
|
||||
codex: 'codex',
|
||||
qwen: 'qwen',
|
||||
iflow: 'iflow',
|
||||
};
|
||||
// Map CLIProxyAPI provider names to our internal provider names
|
||||
const statsProviderMap: Record<string, CLIProxyProvider> = {
|
||||
gemini: 'gemini',
|
||||
antigravity: 'agy',
|
||||
codex: 'codex',
|
||||
qwen: 'qwen',
|
||||
iflow: 'iflow',
|
||||
};
|
||||
|
||||
// Update lastUsedAt for providers with recent activity
|
||||
if (stats?.requestsByProvider) {
|
||||
for (const [statsProvider, requestCount] of Object.entries(stats.requestsByProvider)) {
|
||||
if (requestCount > 0) {
|
||||
const provider = statsProviderMap[statsProvider.toLowerCase()];
|
||||
if (provider) {
|
||||
// Touch the default account for this provider (or all accounts)
|
||||
const accounts = getProviderAccounts(provider);
|
||||
for (const account of accounts) {
|
||||
// Only touch if this is the default account (most likely being used)
|
||||
if (account.isDefault) {
|
||||
touchAccount(provider, account.id);
|
||||
// Update lastUsedAt for providers with recent activity
|
||||
if (stats?.requestsByProvider) {
|
||||
for (const [statsProvider, requestCount] of Object.entries(stats.requestsByProvider)) {
|
||||
if (requestCount > 0) {
|
||||
const provider = statsProviderMap[statsProvider.toLowerCase()];
|
||||
if (provider) {
|
||||
// Touch the default account for this provider (or all accounts)
|
||||
const accounts = getProviderAccounts(provider);
|
||||
for (const account of accounts) {
|
||||
// Only touch if this is the default account (most likely being used)
|
||||
if (account.isDefault) {
|
||||
touchAccount(provider, account.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const statuses = getAllAuthStatus();
|
||||
|
||||
const authStatus = statuses.map((status) => {
|
||||
const oauthConfig = getOAuthConfig(status.provider);
|
||||
return {
|
||||
provider: status.provider,
|
||||
displayName: oauthConfig.displayName,
|
||||
authenticated: status.authenticated,
|
||||
lastAuth: status.lastAuth?.toISOString() || null,
|
||||
tokenFiles: status.tokenFiles.length,
|
||||
accounts: status.accounts,
|
||||
defaultAccount: status.defaultAccount,
|
||||
};
|
||||
});
|
||||
|
||||
res.json({ authStatus });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: (error as Error).message });
|
||||
}
|
||||
|
||||
const statuses = getAllAuthStatus();
|
||||
|
||||
const authStatus = statuses.map((status) => {
|
||||
const oauthConfig = getOAuthConfig(status.provider);
|
||||
return {
|
||||
provider: status.provider,
|
||||
displayName: oauthConfig.displayName,
|
||||
authenticated: status.authenticated,
|
||||
lastAuth: status.lastAuth?.toISOString() || null,
|
||||
tokenFiles: status.tokenFiles.length,
|
||||
accounts: status.accounts,
|
||||
defaultAccount: status.defaultAccount,
|
||||
};
|
||||
});
|
||||
|
||||
res.json({ authStatus });
|
||||
});
|
||||
|
||||
// ==================== Account Management ====================
|
||||
@@ -91,11 +95,16 @@ router.get('/', async (_req: Request, res: Response) => {
|
||||
* GET /api/cliproxy/accounts - Get all accounts across all providers
|
||||
*/
|
||||
router.get('/accounts', (_req: Request, res: Response) => {
|
||||
// Initialize accounts from existing tokens
|
||||
initializeAccounts();
|
||||
try {
|
||||
// Initialize accounts from existing tokens
|
||||
initializeAccounts();
|
||||
|
||||
const accounts = getAllAccountsSummary();
|
||||
res.json({ accounts });
|
||||
const accounts = getAllAccountsSummary();
|
||||
res.json({ accounts });
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to list accounts';
|
||||
res.status(500).json({ error: message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -110,8 +119,13 @@ router.get('/accounts/:provider', (req: Request, res: Response): void => {
|
||||
return;
|
||||
}
|
||||
|
||||
const accounts = getProviderAccounts(provider as CLIProxyProvider);
|
||||
res.json({ provider, accounts });
|
||||
try {
|
||||
const accounts = getProviderAccounts(provider as CLIProxyProvider);
|
||||
res.json({ provider, accounts });
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to get provider accounts';
|
||||
res.status(500).json({ error: message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -132,12 +146,19 @@ router.post('/accounts/:provider/default', (req: Request, res: Response): void =
|
||||
return;
|
||||
}
|
||||
|
||||
const success = setDefaultAccountFn(provider as CLIProxyProvider, accountId);
|
||||
try {
|
||||
const success = setDefaultAccountFn(provider as CLIProxyProvider, accountId);
|
||||
|
||||
if (success) {
|
||||
res.json({ provider, defaultAccount: accountId });
|
||||
} else {
|
||||
res.status(404).json({ error: 'Account not found' });
|
||||
if (success) {
|
||||
res.json({ provider, defaultAccount: accountId });
|
||||
} else {
|
||||
res
|
||||
.status(404)
|
||||
.json({ error: `Account '${accountId}' not found for provider '${provider}'` });
|
||||
}
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to set default account';
|
||||
res.status(500).json({ error: message });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -153,12 +174,19 @@ router.delete('/accounts/:provider/:accountId', (req: Request, res: Response): v
|
||||
return;
|
||||
}
|
||||
|
||||
const success = removeAccountFn(provider as CLIProxyProvider, accountId);
|
||||
try {
|
||||
const success = removeAccountFn(provider as CLIProxyProvider, accountId);
|
||||
|
||||
if (success) {
|
||||
res.json({ provider, accountId, deleted: true });
|
||||
} else {
|
||||
res.status(404).json({ error: 'Account not found' });
|
||||
if (success) {
|
||||
res.json({ provider, accountId, deleted: true });
|
||||
} else {
|
||||
res
|
||||
.status(404)
|
||||
.json({ error: `Account '${accountId}' not found for provider '${provider}'` });
|
||||
}
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to remove account';
|
||||
res.status(500).json({ error: message });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
*/
|
||||
|
||||
import { Router, Request, Response } from 'express';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import {
|
||||
fetchCliproxyStats,
|
||||
@@ -11,7 +12,11 @@ import {
|
||||
fetchCliproxyErrorLogs,
|
||||
fetchCliproxyErrorLogContent,
|
||||
} 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 { ensureCliproxyService } from '../../cliproxy/service-manager';
|
||||
import { checkCliproxyUpdate } from '../../cliproxy/binary-manager';
|
||||
@@ -19,10 +24,9 @@ import { checkCliproxyUpdate } from '../../cliproxy/binary-manager';
|
||||
const router = Router();
|
||||
|
||||
/**
|
||||
* GET /api/cliproxy/stats - Get CLIProxyAPI usage statistics
|
||||
* Returns: CliproxyStats or error if proxy not running
|
||||
* Shared handler for stats/usage endpoint
|
||||
*/
|
||||
router.get('/stats', async (_req: Request, res: Response): Promise<void> => {
|
||||
const handleStatsRequest = async (_req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
// Check if proxy is running first
|
||||
const running = await isCliproxyRunning();
|
||||
@@ -48,7 +52,18 @@ router.get('/stats', async (_req: Request, res: Response): Promise<void> => {
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: (error as Error).message });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* GET /api/cliproxy/stats - Get CLIProxyAPI usage statistics
|
||||
* Returns: CliproxyStats or error if proxy not running
|
||||
*/
|
||||
router.get('/stats', handleStatsRequest);
|
||||
|
||||
/**
|
||||
* GET /api/cliproxy/usage - Alias for /stats (frontend compatibility)
|
||||
*/
|
||||
router.get('/usage', handleStatsRequest);
|
||||
|
||||
/**
|
||||
* GET /api/cliproxy/status - Check CLIProxyAPI running status
|
||||
@@ -249,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;
|
||||
@@ -24,12 +24,16 @@ const router = Router();
|
||||
/**
|
||||
* GET /api/config/format - Return current config format and migration status
|
||||
*/
|
||||
router.get('/format', (_req: Request, res: Response) => {
|
||||
res.json({
|
||||
format: getConfigFormat(),
|
||||
migrationNeeded: needsMigration(),
|
||||
backups: getBackupDirectories(),
|
||||
});
|
||||
router.get('/format', (_req: Request, res: Response): void => {
|
||||
try {
|
||||
res.json({
|
||||
format: getConfigFormat(),
|
||||
migrationNeeded: needsMigration(),
|
||||
backups: getBackupDirectories(),
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: (error as Error).message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -89,25 +93,33 @@ router.put('/', (req: Request, res: Response): void => {
|
||||
/**
|
||||
* POST /api/config/migrate - Trigger migration from JSON to YAML
|
||||
*/
|
||||
router.post('/migrate', async (req: Request, res: Response) => {
|
||||
const dryRun = req.query.dryRun === 'true';
|
||||
const result = await migrate(dryRun);
|
||||
res.json(result);
|
||||
router.post('/migrate', async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const dryRun = req.query.dryRun === 'true';
|
||||
const result = await migrate(dryRun);
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: (error as Error).message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/config/rollback - Rollback migration to JSON format
|
||||
*/
|
||||
router.post('/rollback', async (req: Request, res: Response): Promise<void> => {
|
||||
const { backupPath } = req.body;
|
||||
try {
|
||||
const { backupPath } = req.body;
|
||||
|
||||
if (!backupPath || typeof backupPath !== 'string') {
|
||||
res.status(400).json({ error: 'Missing required field: backupPath' });
|
||||
return;
|
||||
if (!backupPath || typeof backupPath !== 'string') {
|
||||
res.status(400).json({ error: 'Missing required field: backupPath' });
|
||||
return;
|
||||
}
|
||||
|
||||
const success = await rollback(backupPath);
|
||||
res.json({ success });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: (error as Error).message });
|
||||
}
|
||||
|
||||
const success = await rollback(backupPath);
|
||||
res.json({ success });
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -10,22 +10,30 @@ const router = Router();
|
||||
/**
|
||||
* GET /api/health - Run health checks
|
||||
*/
|
||||
router.get('/', async (_req: Request, res: Response) => {
|
||||
const report = await runHealthChecks();
|
||||
res.json(report);
|
||||
router.get('/', async (_req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const report = await runHealthChecks();
|
||||
res.json(report);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: (error as Error).message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/health/fix/:checkId - Fix a health issue
|
||||
*/
|
||||
router.post('/fix/:checkId', (req: Request, res: Response): void => {
|
||||
const { checkId } = req.params;
|
||||
const result = fixHealthIssue(checkId);
|
||||
try {
|
||||
const { checkId } = req.params;
|
||||
const result = fixHealthIssue(checkId);
|
||||
|
||||
if (result.success) {
|
||||
res.json({ success: true, message: result.message });
|
||||
} else {
|
||||
res.status(400).json({ success: false, message: result.message });
|
||||
if (result.success) {
|
||||
res.json({ success: true, message: result.message });
|
||||
} else {
|
||||
res.status(400).json({ success: false, message: result.message });
|
||||
}
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: (error as Error).message });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import { Router } from 'express';
|
||||
|
||||
// Import domain routers
|
||||
import profileRoutes from './profile-routes';
|
||||
import accountRoutes from './account-routes';
|
||||
import configRoutes from './config-routes';
|
||||
import healthRoutes from './health-routes';
|
||||
import providerRoutes from './provider-routes';
|
||||
@@ -28,7 +29,7 @@ export const apiRoutes = Router();
|
||||
// Profile CRUD, settings management, presets, accounts
|
||||
apiRoutes.use('/profiles', profileRoutes);
|
||||
apiRoutes.use('/settings', settingsRoutes);
|
||||
apiRoutes.use('/accounts', profileRoutes);
|
||||
apiRoutes.use('/accounts', accountRoutes);
|
||||
|
||||
// ==================== Unified Config ====================
|
||||
// Config format, migration
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
/**
|
||||
* Profile Routes - CRUD operations for user profiles and accounts
|
||||
* Profile Routes - CRUD operations for user profiles
|
||||
*
|
||||
* Uses unified config (config.yaml) when available, falls back to legacy (config.json).
|
||||
* Note: Account routes have been moved to account-routes.ts
|
||||
*/
|
||||
|
||||
import { Router, Request, Response } from 'express';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { getCcsDir } from '../../utils/config-manager';
|
||||
import { isReservedName, RESERVED_PROFILE_NAMES } from '../../config/reserved-names';
|
||||
import { createApiProfile, removeApiProfile } from '../../api/services/profile-writer';
|
||||
import { apiProfileExists, listApiProfiles } from '../../api/services/profile-reader';
|
||||
@@ -20,15 +18,19 @@ const router = Router();
|
||||
/**
|
||||
* GET /api/profiles - List all profiles
|
||||
*/
|
||||
router.get('/', (_req: Request, res: Response) => {
|
||||
const result = listApiProfiles();
|
||||
// Map isConfigured -> configured for UI compatibility
|
||||
const profiles = result.profiles.map((p) => ({
|
||||
name: p.name,
|
||||
settingsPath: p.settingsPath,
|
||||
configured: p.isConfigured,
|
||||
}));
|
||||
res.json({ profiles });
|
||||
router.get('/', (_req: Request, res: Response): void => {
|
||||
try {
|
||||
const result = listApiProfiles();
|
||||
// Map isConfigured -> configured for UI compatibility
|
||||
const profiles = result.profiles.map((p) => ({
|
||||
name: p.name,
|
||||
settingsPath: p.settingsPath,
|
||||
configured: p.isConfigured,
|
||||
}));
|
||||
res.json({ profiles });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: (error as Error).message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -117,52 +119,4 @@ router.delete('/:name', (req: Request, res: Response): void => {
|
||||
res.json({ name, deleted: true });
|
||||
});
|
||||
|
||||
// ==================== Accounts ====================
|
||||
|
||||
/**
|
||||
* GET /api/accounts - List accounts from profiles.json
|
||||
*/
|
||||
router.get('/accounts', (_req: Request, res: Response): void => {
|
||||
const profilesPath = path.join(getCcsDir(), 'profiles.json');
|
||||
|
||||
if (!fs.existsSync(profilesPath)) {
|
||||
res.json({ accounts: [], default: null });
|
||||
return;
|
||||
}
|
||||
|
||||
const data = JSON.parse(fs.readFileSync(profilesPath, 'utf8'));
|
||||
const accounts = Object.entries(data.profiles || {}).map(([name, meta]) => {
|
||||
const metadata = meta as Record<string, unknown>;
|
||||
return {
|
||||
name,
|
||||
...metadata,
|
||||
};
|
||||
});
|
||||
|
||||
res.json({ accounts, default: data.default || null });
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/accounts/default - Set default account
|
||||
*/
|
||||
router.post('/accounts/default', (req: Request, res: Response): void => {
|
||||
const { name } = req.body;
|
||||
|
||||
if (!name) {
|
||||
res.status(400).json({ error: 'Missing required field: name' });
|
||||
return;
|
||||
}
|
||||
|
||||
const profilesPath = path.join(getCcsDir(), 'profiles.json');
|
||||
|
||||
const data = fs.existsSync(profilesPath)
|
||||
? JSON.parse(fs.readFileSync(profilesPath, 'utf8'))
|
||||
: { profiles: {} };
|
||||
|
||||
data.default = name;
|
||||
fs.writeFileSync(profilesPath, JSON.stringify(data, null, 2) + '\n');
|
||||
|
||||
res.json({ default: name });
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -32,103 +32,115 @@ function maskApiKeys(settings: Settings): Settings {
|
||||
* GET /api/settings/:profile - Get settings with masked API keys
|
||||
*/
|
||||
router.get('/:profile', (req: Request, res: Response): void => {
|
||||
const { profile } = req.params;
|
||||
const ccsDir = getCcsDir();
|
||||
const settingsPath = path.join(ccsDir, `${profile}.settings.json`);
|
||||
try {
|
||||
const { profile } = req.params;
|
||||
const ccsDir = getCcsDir();
|
||||
const settingsPath = path.join(ccsDir, `${profile}.settings.json`);
|
||||
|
||||
if (!fs.existsSync(settingsPath)) {
|
||||
res.status(404).json({ error: 'Settings not found' });
|
||||
return;
|
||||
if (!fs.existsSync(settingsPath)) {
|
||||
res.status(404).json({ error: 'Settings not found' });
|
||||
return;
|
||||
}
|
||||
|
||||
const stat = fs.statSync(settingsPath);
|
||||
const settings = loadSettings(settingsPath);
|
||||
const masked = maskApiKeys(settings);
|
||||
|
||||
res.json({
|
||||
profile,
|
||||
settings: masked,
|
||||
mtime: stat.mtime.getTime(),
|
||||
path: settingsPath,
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: (error as Error).message });
|
||||
}
|
||||
|
||||
const stat = fs.statSync(settingsPath);
|
||||
const settings = loadSettings(settingsPath);
|
||||
const masked = maskApiKeys(settings);
|
||||
|
||||
res.json({
|
||||
profile,
|
||||
settings: masked,
|
||||
mtime: stat.mtime.getTime(),
|
||||
path: settingsPath,
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/settings/:profile/raw - Get full settings (for editing)
|
||||
*/
|
||||
router.get('/:profile/raw', (req: Request, res: Response): void => {
|
||||
const { profile } = req.params;
|
||||
const ccsDir = getCcsDir();
|
||||
const settingsPath = path.join(ccsDir, `${profile}.settings.json`);
|
||||
try {
|
||||
const { profile } = req.params;
|
||||
const ccsDir = getCcsDir();
|
||||
const settingsPath = path.join(ccsDir, `${profile}.settings.json`);
|
||||
|
||||
if (!fs.existsSync(settingsPath)) {
|
||||
res.status(404).json({ error: 'Settings not found' });
|
||||
return;
|
||||
if (!fs.existsSync(settingsPath)) {
|
||||
res.status(404).json({ error: 'Settings not found' });
|
||||
return;
|
||||
}
|
||||
|
||||
const stat = fs.statSync(settingsPath);
|
||||
const settings = loadSettings(settingsPath);
|
||||
|
||||
res.json({
|
||||
profile,
|
||||
settings,
|
||||
mtime: stat.mtime.getTime(),
|
||||
path: settingsPath,
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: (error as Error).message });
|
||||
}
|
||||
|
||||
const stat = fs.statSync(settingsPath);
|
||||
const settings = loadSettings(settingsPath);
|
||||
|
||||
res.json({
|
||||
profile,
|
||||
settings,
|
||||
mtime: stat.mtime.getTime(),
|
||||
path: settingsPath,
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* PUT /api/settings/:profile - Update settings with conflict detection and backup
|
||||
*/
|
||||
router.put('/:profile', (req: Request, res: Response): void => {
|
||||
const { profile } = req.params;
|
||||
const { settings, expectedMtime } = req.body;
|
||||
const ccsDir = getCcsDir();
|
||||
const settingsPath = path.join(ccsDir, `${profile}.settings.json`);
|
||||
try {
|
||||
const { profile } = req.params;
|
||||
const { settings, expectedMtime } = req.body;
|
||||
const ccsDir = getCcsDir();
|
||||
const settingsPath = path.join(ccsDir, `${profile}.settings.json`);
|
||||
|
||||
const fileExists = fs.existsSync(settingsPath);
|
||||
const fileExists = fs.existsSync(settingsPath);
|
||||
|
||||
// Only check conflict if file exists and expectedMtime was provided
|
||||
if (fileExists && expectedMtime) {
|
||||
const stat = fs.statSync(settingsPath);
|
||||
if (stat.mtime.getTime() !== expectedMtime) {
|
||||
res.status(409).json({
|
||||
error: 'File modified externally',
|
||||
currentMtime: stat.mtime.getTime(),
|
||||
});
|
||||
return;
|
||||
// Only check conflict if file exists and expectedMtime was provided
|
||||
if (fileExists && expectedMtime) {
|
||||
const stat = fs.statSync(settingsPath);
|
||||
if (stat.mtime.getTime() !== expectedMtime) {
|
||||
res.status(409).json({
|
||||
error: 'File modified externally',
|
||||
currentMtime: stat.mtime.getTime(),
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create backup only if file exists
|
||||
let backupPath: string | undefined;
|
||||
if (fileExists) {
|
||||
const backupDir = path.join(ccsDir, 'backups');
|
||||
if (!fs.existsSync(backupDir)) {
|
||||
fs.mkdirSync(backupDir, { recursive: true });
|
||||
// Create backup only if file exists
|
||||
let backupPath: string | undefined;
|
||||
if (fileExists) {
|
||||
const backupDir = path.join(ccsDir, 'backups');
|
||||
if (!fs.existsSync(backupDir)) {
|
||||
fs.mkdirSync(backupDir, { recursive: true });
|
||||
}
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||
backupPath = path.join(backupDir, `${profile}.${timestamp}.settings.json`);
|
||||
fs.copyFileSync(settingsPath, backupPath);
|
||||
}
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||
backupPath = path.join(backupDir, `${profile}.${timestamp}.settings.json`);
|
||||
fs.copyFileSync(settingsPath, backupPath);
|
||||
|
||||
// Ensure directory exists for new files
|
||||
if (!fileExists) {
|
||||
fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
|
||||
}
|
||||
|
||||
// Write new settings atomically
|
||||
const tempPath = settingsPath + '.tmp';
|
||||
fs.writeFileSync(tempPath, JSON.stringify(settings, null, 2) + '\n');
|
||||
fs.renameSync(tempPath, settingsPath);
|
||||
|
||||
const newStat = fs.statSync(settingsPath);
|
||||
res.json({
|
||||
profile,
|
||||
mtime: newStat.mtime.getTime(),
|
||||
backupPath,
|
||||
created: !fileExists,
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: (error as Error).message });
|
||||
}
|
||||
|
||||
// Ensure directory exists for new files
|
||||
if (!fileExists) {
|
||||
fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
|
||||
}
|
||||
|
||||
// Write new settings atomically
|
||||
const tempPath = settingsPath + '.tmp';
|
||||
fs.writeFileSync(tempPath, JSON.stringify(settings, null, 2) + '\n');
|
||||
fs.renameSync(tempPath, settingsPath);
|
||||
|
||||
const newStat = fs.statSync(settingsPath);
|
||||
res.json({
|
||||
profile,
|
||||
mtime: newStat.mtime.getTime(),
|
||||
backupPath,
|
||||
created: !fileExists,
|
||||
});
|
||||
});
|
||||
|
||||
// ==================== Presets ====================
|
||||
@@ -137,85 +149,97 @@ router.put('/:profile', (req: Request, res: Response): void => {
|
||||
* GET /api/settings/:profile/presets - Get saved presets for a provider
|
||||
*/
|
||||
router.get('/:profile/presets', (req: Request, res: Response): void => {
|
||||
const { profile } = req.params;
|
||||
const ccsDir = getCcsDir();
|
||||
const settingsPath = path.join(ccsDir, `${profile}.settings.json`);
|
||||
try {
|
||||
const { profile } = req.params;
|
||||
const ccsDir = getCcsDir();
|
||||
const settingsPath = path.join(ccsDir, `${profile}.settings.json`);
|
||||
|
||||
if (!fs.existsSync(settingsPath)) {
|
||||
res.json({ presets: [] });
|
||||
return;
|
||||
if (!fs.existsSync(settingsPath)) {
|
||||
res.json({ presets: [] });
|
||||
return;
|
||||
}
|
||||
|
||||
const settings = loadSettings(settingsPath);
|
||||
res.json({ presets: settings.presets || [] });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: (error as Error).message });
|
||||
}
|
||||
|
||||
const settings = loadSettings(settingsPath);
|
||||
res.json({ presets: settings.presets || [] });
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/settings/:profile/presets - Create a new preset
|
||||
*/
|
||||
router.post('/:profile/presets', (req: Request, res: Response): void => {
|
||||
const { profile } = req.params;
|
||||
const { name, default: defaultModel, opus, sonnet, haiku } = req.body;
|
||||
try {
|
||||
const { profile } = req.params;
|
||||
const { name, default: defaultModel, opus, sonnet, haiku } = req.body;
|
||||
|
||||
if (!name || !defaultModel) {
|
||||
res.status(400).json({ error: 'Missing required fields: name, default' });
|
||||
return;
|
||||
if (!name || !defaultModel) {
|
||||
res.status(400).json({ error: 'Missing required fields: name, default' });
|
||||
return;
|
||||
}
|
||||
|
||||
const ccsDir = getCcsDir();
|
||||
const settingsPath = path.join(ccsDir, `${profile}.settings.json`);
|
||||
|
||||
// Create settings file if it doesn't exist
|
||||
if (!fs.existsSync(settingsPath)) {
|
||||
fs.writeFileSync(settingsPath, JSON.stringify({ env: {}, presets: [] }, null, 2) + '\n');
|
||||
}
|
||||
|
||||
const settings = loadSettings(settingsPath);
|
||||
settings.presets = settings.presets || [];
|
||||
|
||||
// Check for duplicate name
|
||||
if (settings.presets.some((p) => p.name === name)) {
|
||||
res.status(409).json({ error: 'Preset with this name already exists' });
|
||||
return;
|
||||
}
|
||||
|
||||
const preset = {
|
||||
name,
|
||||
default: defaultModel,
|
||||
opus: opus || defaultModel,
|
||||
sonnet: sonnet || defaultModel,
|
||||
haiku: haiku || defaultModel,
|
||||
};
|
||||
|
||||
settings.presets.push(preset);
|
||||
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
||||
|
||||
res.status(201).json({ preset });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: (error as Error).message });
|
||||
}
|
||||
|
||||
const ccsDir = getCcsDir();
|
||||
const settingsPath = path.join(ccsDir, `${profile}.settings.json`);
|
||||
|
||||
// Create settings file if it doesn't exist
|
||||
if (!fs.existsSync(settingsPath)) {
|
||||
fs.writeFileSync(settingsPath, JSON.stringify({ env: {}, presets: [] }, null, 2) + '\n');
|
||||
}
|
||||
|
||||
const settings = loadSettings(settingsPath);
|
||||
settings.presets = settings.presets || [];
|
||||
|
||||
// Check for duplicate name
|
||||
if (settings.presets.some((p) => p.name === name)) {
|
||||
res.status(409).json({ error: 'Preset with this name already exists' });
|
||||
return;
|
||||
}
|
||||
|
||||
const preset = {
|
||||
name,
|
||||
default: defaultModel,
|
||||
opus: opus || defaultModel,
|
||||
sonnet: sonnet || defaultModel,
|
||||
haiku: haiku || defaultModel,
|
||||
};
|
||||
|
||||
settings.presets.push(preset);
|
||||
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
||||
|
||||
res.status(201).json({ preset });
|
||||
});
|
||||
|
||||
/**
|
||||
* DELETE /api/settings/:profile/presets/:name - Delete a preset
|
||||
*/
|
||||
router.delete('/:profile/presets/:name', (req: Request, res: Response): void => {
|
||||
const { profile, name } = req.params;
|
||||
const ccsDir = getCcsDir();
|
||||
const settingsPath = path.join(ccsDir, `${profile}.settings.json`);
|
||||
try {
|
||||
const { profile, name } = req.params;
|
||||
const ccsDir = getCcsDir();
|
||||
const settingsPath = path.join(ccsDir, `${profile}.settings.json`);
|
||||
|
||||
if (!fs.existsSync(settingsPath)) {
|
||||
res.status(404).json({ error: 'Settings not found' });
|
||||
return;
|
||||
if (!fs.existsSync(settingsPath)) {
|
||||
res.status(404).json({ error: 'Settings not found' });
|
||||
return;
|
||||
}
|
||||
|
||||
const settings = loadSettings(settingsPath);
|
||||
if (!settings.presets || !settings.presets.some((p) => p.name === name)) {
|
||||
res.status(404).json({ error: 'Preset not found' });
|
||||
return;
|
||||
}
|
||||
|
||||
settings.presets = settings.presets.filter((p) => p.name !== name);
|
||||
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
||||
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: (error as Error).message });
|
||||
}
|
||||
|
||||
const settings = loadSettings(settingsPath);
|
||||
if (!settings.presets || !settings.presets.some((p) => p.name === name)) {
|
||||
res.status(404).json({ error: 'Preset not found' });
|
||||
return;
|
||||
}
|
||||
|
||||
settings.presets = settings.presets.filter((p) => p.name !== name);
|
||||
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
||||
|
||||
res.json({ success: true });
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -78,82 +78,90 @@ router.post('/', (req: Request, res: Response): void => {
|
||||
* PUT /api/cliproxy/:name - Update cliproxy variant
|
||||
*/
|
||||
router.put('/:name', (req: Request, res: Response): void => {
|
||||
const { name } = req.params;
|
||||
const { provider, account, model } = req.body;
|
||||
try {
|
||||
const { name } = req.params;
|
||||
const { provider, account, model } = req.body;
|
||||
|
||||
const config = readConfigSafe();
|
||||
const config = readConfigSafe();
|
||||
|
||||
if (!config.cliproxy?.[name]) {
|
||||
res.status(404).json({ error: 'Variant not found' });
|
||||
return;
|
||||
}
|
||||
|
||||
const variant = config.cliproxy[name];
|
||||
|
||||
// Update fields if provided
|
||||
if (provider) {
|
||||
variant.provider = provider;
|
||||
}
|
||||
if (account !== undefined) {
|
||||
if (account) {
|
||||
variant.account = account;
|
||||
} else {
|
||||
delete variant.account; // Remove account to use default
|
||||
if (!config.cliproxy?.[name]) {
|
||||
res.status(404).json({ error: 'Variant not found' });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Update model in settings file if provided
|
||||
if (model !== undefined) {
|
||||
const settingsPath = path.join(getCcsDir(), `${name}.settings.json`);
|
||||
if (fs.existsSync(settingsPath)) {
|
||||
const settings = loadSettings(settingsPath);
|
||||
if (model) {
|
||||
settings.env = settings.env || {};
|
||||
settings.env.ANTHROPIC_MODEL = model;
|
||||
} else if (settings.env) {
|
||||
delete settings.env.ANTHROPIC_MODEL;
|
||||
const variant = config.cliproxy[name];
|
||||
|
||||
// Update fields if provided
|
||||
if (provider) {
|
||||
variant.provider = provider;
|
||||
}
|
||||
if (account !== undefined) {
|
||||
if (account) {
|
||||
variant.account = account;
|
||||
} else {
|
||||
delete variant.account; // Remove account to use default
|
||||
}
|
||||
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
||||
}
|
||||
|
||||
// Update model in settings file if provided
|
||||
if (model !== undefined) {
|
||||
const settingsPath = path.join(getCcsDir(), `${name}.settings.json`);
|
||||
if (fs.existsSync(settingsPath)) {
|
||||
const settings = loadSettings(settingsPath);
|
||||
if (model) {
|
||||
settings.env = settings.env || {};
|
||||
settings.env.ANTHROPIC_MODEL = model;
|
||||
} else if (settings.env) {
|
||||
delete settings.env.ANTHROPIC_MODEL;
|
||||
}
|
||||
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
||||
}
|
||||
}
|
||||
|
||||
writeConfig(config);
|
||||
|
||||
res.json({
|
||||
name,
|
||||
provider: variant.provider,
|
||||
account: variant.account || 'default',
|
||||
settings: variant.settings,
|
||||
updated: true,
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: (error as Error).message });
|
||||
}
|
||||
|
||||
writeConfig(config);
|
||||
|
||||
res.json({
|
||||
name,
|
||||
provider: variant.provider,
|
||||
account: variant.account || 'default',
|
||||
settings: variant.settings,
|
||||
updated: true,
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* DELETE /api/cliproxy/:name - Delete cliproxy variant
|
||||
*/
|
||||
router.delete('/:name', (req: Request, res: Response): void => {
|
||||
const { name } = req.params;
|
||||
try {
|
||||
const { name } = req.params;
|
||||
|
||||
const config = readConfigSafe();
|
||||
const config = readConfigSafe();
|
||||
|
||||
if (!config.cliproxy?.[name]) {
|
||||
res.status(404).json({ error: 'Variant not found' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Never delete settings files for reserved provider names (safety guard)
|
||||
if (!isReservedName(name)) {
|
||||
// Only delete settings file for non-reserved variant names
|
||||
const settingsPath = path.join(getCcsDir(), `${name}.settings.json`);
|
||||
if (fs.existsSync(settingsPath)) {
|
||||
fs.unlinkSync(settingsPath);
|
||||
if (!config.cliproxy?.[name]) {
|
||||
res.status(404).json({ error: 'Variant not found' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Never delete settings files for reserved provider names (safety guard)
|
||||
if (!isReservedName(name)) {
|
||||
// Only delete settings file for non-reserved variant names
|
||||
const settingsPath = path.join(getCcsDir(), `${name}.settings.json`);
|
||||
if (fs.existsSync(settingsPath)) {
|
||||
fs.unlinkSync(settingsPath);
|
||||
}
|
||||
}
|
||||
|
||||
delete config.cliproxy[name];
|
||||
writeConfig(config);
|
||||
|
||||
res.json({ name, deleted: true });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: (error as Error).message });
|
||||
}
|
||||
|
||||
delete config.cliproxy[name];
|
||||
writeConfig(config);
|
||||
|
||||
res.json({ name, deleted: true });
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -307,16 +307,18 @@ export const api = {
|
||||
|
||||
// Multi-account management
|
||||
accounts: {
|
||||
list: () => request<{ accounts: ProviderAccountsMap }>('/cliproxy/accounts'),
|
||||
list: () => request<{ accounts: ProviderAccountsMap }>('/cliproxy/auth/accounts'),
|
||||
listByProvider: (provider: string) =>
|
||||
request<{ provider: string; accounts: OAuthAccount[] }>(`/cliproxy/accounts/${provider}`),
|
||||
request<{ provider: string; accounts: OAuthAccount[] }>(
|
||||
`/cliproxy/auth/accounts/${provider}`
|
||||
),
|
||||
setDefault: (provider: string, accountId: string) =>
|
||||
request(`/cliproxy/accounts/${provider}/default`, {
|
||||
request(`/cliproxy/auth/accounts/${provider}/default`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ accountId }),
|
||||
}),
|
||||
remove: (provider: string, accountId: string) =>
|
||||
request(`/cliproxy/accounts/${provider}/${accountId}`, { method: 'DELETE' }),
|
||||
request(`/cliproxy/auth/accounts/${provider}/${accountId}`, { method: 'DELETE' }),
|
||||
},
|
||||
// OAuth flow
|
||||
auth: {
|
||||
|
||||
Reference in new issue
Block a user