@siteboon/claude-code-ui 1.9.0 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +44 -2
- package/dist/api-docs.html +857 -0
- package/dist/assets/index-BHQThXog.css +32 -0
- package/dist/assets/index-DPk7rbtA.js +902 -0
- package/dist/assets/{vendor-codemirror-D2k1L1JZ.js → vendor-codemirror-B7BYDWj-.js} +17 -17
- package/dist/index.html +3 -3
- package/package.json +8 -4
- package/server/claude-sdk.js +5 -0
- package/server/cursor-cli.js +5 -0
- package/server/database/db.js +180 -3
- package/server/database/init.sql +34 -1
- package/server/index.js +36 -15
- package/server/projects.js +8 -4
- package/server/routes/agent.js +1161 -0
- package/server/routes/git.js +264 -54
- package/server/routes/settings.js +178 -0
- package/dist/assets/index-Bmo7Hu70.css +0 -32
- package/dist/assets/index-D3NZxyU6.js +0 -793
package/server/routes/git.js
CHANGED
|
@@ -4,6 +4,8 @@ import { promisify } from 'util';
|
|
|
4
4
|
import path from 'path';
|
|
5
5
|
import { promises as fs } from 'fs';
|
|
6
6
|
import { extractProjectDirectory } from '../projects.js';
|
|
7
|
+
import { queryClaudeSDK } from '../claude-sdk.js';
|
|
8
|
+
import { spawnCursor } from '../cursor-cli.js';
|
|
7
9
|
|
|
8
10
|
const router = express.Router();
|
|
9
11
|
const execAsync = promisify(exec);
|
|
@@ -19,6 +21,35 @@ async function getActualProjectPath(projectName) {
|
|
|
19
21
|
}
|
|
20
22
|
}
|
|
21
23
|
|
|
24
|
+
// Helper function to strip git diff headers
|
|
25
|
+
function stripDiffHeaders(diff) {
|
|
26
|
+
if (!diff) return '';
|
|
27
|
+
|
|
28
|
+
const lines = diff.split('\n');
|
|
29
|
+
const filteredLines = [];
|
|
30
|
+
let startIncluding = false;
|
|
31
|
+
|
|
32
|
+
for (const line of lines) {
|
|
33
|
+
// Skip all header lines including diff --git, index, file mode, and --- / +++ file paths
|
|
34
|
+
if (line.startsWith('diff --git') ||
|
|
35
|
+
line.startsWith('index ') ||
|
|
36
|
+
line.startsWith('new file mode') ||
|
|
37
|
+
line.startsWith('deleted file mode') ||
|
|
38
|
+
line.startsWith('---') ||
|
|
39
|
+
line.startsWith('+++')) {
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Start including lines from @@ hunk headers onwards
|
|
44
|
+
if (line.startsWith('@@') || startIncluding) {
|
|
45
|
+
startIncluding = true;
|
|
46
|
+
filteredLines.push(line);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return filteredLines.join('\n');
|
|
51
|
+
}
|
|
52
|
+
|
|
22
53
|
// Helper function to validate git repository
|
|
23
54
|
async function validateGitRepository(projectPath) {
|
|
24
55
|
try {
|
|
@@ -122,32 +153,39 @@ router.get('/diff', async (req, res) => {
|
|
|
122
153
|
// Validate git repository
|
|
123
154
|
await validateGitRepository(projectPath);
|
|
124
155
|
|
|
125
|
-
// Check if file is untracked
|
|
156
|
+
// Check if file is untracked or deleted
|
|
126
157
|
const { stdout: statusOutput } = await execAsync(`git status --porcelain "${file}"`, { cwd: projectPath });
|
|
127
158
|
const isUntracked = statusOutput.startsWith('??');
|
|
128
|
-
|
|
159
|
+
const isDeleted = statusOutput.trim().startsWith('D ') || statusOutput.trim().startsWith(' D');
|
|
160
|
+
|
|
129
161
|
let diff;
|
|
130
162
|
if (isUntracked) {
|
|
131
163
|
// For untracked files, show the entire file content as additions
|
|
132
164
|
const fileContent = await fs.readFile(path.join(projectPath, file), 'utf-8');
|
|
133
165
|
const lines = fileContent.split('\n');
|
|
134
|
-
diff = `--- /dev/null\n+++ b/${file}\n@@ -0,0 +1,${lines.length} @@\n` +
|
|
166
|
+
diff = `--- /dev/null\n+++ b/${file}\n@@ -0,0 +1,${lines.length} @@\n` +
|
|
135
167
|
lines.map(line => `+${line}`).join('\n');
|
|
168
|
+
} else if (isDeleted) {
|
|
169
|
+
// For deleted files, show the entire file content from HEAD as deletions
|
|
170
|
+
const { stdout: fileContent } = await execAsync(`git show HEAD:"${file}"`, { cwd: projectPath });
|
|
171
|
+
const lines = fileContent.split('\n');
|
|
172
|
+
diff = `--- a/${file}\n+++ /dev/null\n@@ -1,${lines.length} +0,0 @@\n` +
|
|
173
|
+
lines.map(line => `-${line}`).join('\n');
|
|
136
174
|
} else {
|
|
137
175
|
// Get diff for tracked files
|
|
138
176
|
// First check for unstaged changes (working tree vs index)
|
|
139
177
|
const { stdout: unstagedDiff } = await execAsync(`git diff -- "${file}"`, { cwd: projectPath });
|
|
140
|
-
|
|
178
|
+
|
|
141
179
|
if (unstagedDiff) {
|
|
142
180
|
// Show unstaged changes if they exist
|
|
143
|
-
diff = unstagedDiff;
|
|
181
|
+
diff = stripDiffHeaders(unstagedDiff);
|
|
144
182
|
} else {
|
|
145
183
|
// If no unstaged changes, check for staged changes (index vs HEAD)
|
|
146
184
|
const { stdout: stagedDiff } = await execAsync(`git diff --cached -- "${file}"`, { cwd: projectPath });
|
|
147
|
-
diff = stagedDiff || '';
|
|
185
|
+
diff = stripDiffHeaders(stagedDiff) || '';
|
|
148
186
|
}
|
|
149
187
|
}
|
|
150
|
-
|
|
188
|
+
|
|
151
189
|
res.json({ diff });
|
|
152
190
|
} catch (error) {
|
|
153
191
|
console.error('Git diff error:', error);
|
|
@@ -155,6 +193,61 @@ router.get('/diff', async (req, res) => {
|
|
|
155
193
|
}
|
|
156
194
|
});
|
|
157
195
|
|
|
196
|
+
// Get file content with diff information for CodeEditor
|
|
197
|
+
router.get('/file-with-diff', async (req, res) => {
|
|
198
|
+
const { project, file } = req.query;
|
|
199
|
+
|
|
200
|
+
if (!project || !file) {
|
|
201
|
+
return res.status(400).json({ error: 'Project name and file path are required' });
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
try {
|
|
205
|
+
const projectPath = await getActualProjectPath(project);
|
|
206
|
+
|
|
207
|
+
// Validate git repository
|
|
208
|
+
await validateGitRepository(projectPath);
|
|
209
|
+
|
|
210
|
+
// Check file status
|
|
211
|
+
const { stdout: statusOutput } = await execAsync(`git status --porcelain "${file}"`, { cwd: projectPath });
|
|
212
|
+
const isUntracked = statusOutput.startsWith('??');
|
|
213
|
+
const isDeleted = statusOutput.trim().startsWith('D ') || statusOutput.trim().startsWith(' D');
|
|
214
|
+
|
|
215
|
+
let currentContent = '';
|
|
216
|
+
let oldContent = '';
|
|
217
|
+
|
|
218
|
+
if (isDeleted) {
|
|
219
|
+
// For deleted files, get content from HEAD
|
|
220
|
+
const { stdout: headContent } = await execAsync(`git show HEAD:"${file}"`, { cwd: projectPath });
|
|
221
|
+
oldContent = headContent;
|
|
222
|
+
currentContent = headContent; // Show the deleted content in editor
|
|
223
|
+
} else {
|
|
224
|
+
// Get current file content
|
|
225
|
+
currentContent = await fs.readFile(path.join(projectPath, file), 'utf-8');
|
|
226
|
+
|
|
227
|
+
if (!isUntracked) {
|
|
228
|
+
// Get the old content from HEAD for tracked files
|
|
229
|
+
try {
|
|
230
|
+
const { stdout: headContent } = await execAsync(`git show HEAD:"${file}"`, { cwd: projectPath });
|
|
231
|
+
oldContent = headContent;
|
|
232
|
+
} catch (error) {
|
|
233
|
+
// File might be newly added to git (staged but not committed)
|
|
234
|
+
oldContent = '';
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
res.json({
|
|
240
|
+
currentContent,
|
|
241
|
+
oldContent,
|
|
242
|
+
isDeleted,
|
|
243
|
+
isUntracked
|
|
244
|
+
});
|
|
245
|
+
} catch (error) {
|
|
246
|
+
console.error('Git file-with-diff error:', error);
|
|
247
|
+
res.json({ error: error.message });
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
|
|
158
251
|
// Commit changes
|
|
159
252
|
router.post('/commit', async (req, res) => {
|
|
160
253
|
const { project, message, files } = req.body;
|
|
@@ -343,19 +436,24 @@ router.get('/commit-diff', async (req, res) => {
|
|
|
343
436
|
}
|
|
344
437
|
});
|
|
345
438
|
|
|
346
|
-
// Generate commit message based on staged changes
|
|
439
|
+
// Generate commit message based on staged changes using AI
|
|
347
440
|
router.post('/generate-commit-message', async (req, res) => {
|
|
348
|
-
const { project, files } = req.body;
|
|
349
|
-
|
|
441
|
+
const { project, files, provider = 'claude' } = req.body;
|
|
442
|
+
|
|
350
443
|
if (!project || !files || files.length === 0) {
|
|
351
444
|
return res.status(400).json({ error: 'Project name and files are required' });
|
|
352
445
|
}
|
|
353
446
|
|
|
447
|
+
// Validate provider
|
|
448
|
+
if (!['claude', 'cursor'].includes(provider)) {
|
|
449
|
+
return res.status(400).json({ error: 'provider must be "claude" or "cursor"' });
|
|
450
|
+
}
|
|
451
|
+
|
|
354
452
|
try {
|
|
355
453
|
const projectPath = await getActualProjectPath(project);
|
|
356
|
-
|
|
454
|
+
|
|
357
455
|
// Get diff for selected files
|
|
358
|
-
let
|
|
456
|
+
let diffContext = '';
|
|
359
457
|
for (const file of files) {
|
|
360
458
|
try {
|
|
361
459
|
const { stdout } = await execAsync(
|
|
@@ -363,17 +461,30 @@ router.post('/generate-commit-message', async (req, res) => {
|
|
|
363
461
|
{ cwd: projectPath }
|
|
364
462
|
);
|
|
365
463
|
if (stdout) {
|
|
366
|
-
|
|
464
|
+
diffContext += `\n--- ${file} ---\n${stdout}`;
|
|
367
465
|
}
|
|
368
466
|
} catch (error) {
|
|
369
467
|
console.error(`Error getting diff for ${file}:`, error);
|
|
370
468
|
}
|
|
371
469
|
}
|
|
372
|
-
|
|
373
|
-
//
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
470
|
+
|
|
471
|
+
// If no diff found, might be untracked files
|
|
472
|
+
if (!diffContext.trim()) {
|
|
473
|
+
// Try to get content of untracked files
|
|
474
|
+
for (const file of files) {
|
|
475
|
+
try {
|
|
476
|
+
const filePath = path.join(projectPath, file);
|
|
477
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
478
|
+
diffContext += `\n--- ${file} (new file) ---\n${content.substring(0, 1000)}\n`;
|
|
479
|
+
} catch (error) {
|
|
480
|
+
console.error(`Error reading file ${file}:`, error);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// Generate commit message using AI
|
|
486
|
+
const message = await generateCommitMessageWithAI(files, diffContext, provider, projectPath);
|
|
487
|
+
|
|
377
488
|
res.json({ message });
|
|
378
489
|
} catch (error) {
|
|
379
490
|
console.error('Generate commit message error:', error);
|
|
@@ -381,46 +492,145 @@ router.post('/generate-commit-message', async (req, res) => {
|
|
|
381
492
|
}
|
|
382
493
|
});
|
|
383
494
|
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
//
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
495
|
+
/**
|
|
496
|
+
* Generates a commit message using AI (Claude SDK or Cursor CLI)
|
|
497
|
+
* @param {Array<string>} files - List of changed files
|
|
498
|
+
* @param {string} diffContext - Git diff content
|
|
499
|
+
* @param {string} provider - 'claude' or 'cursor'
|
|
500
|
+
* @param {string} projectPath - Project directory path
|
|
501
|
+
* @returns {Promise<string>} Generated commit message
|
|
502
|
+
*/
|
|
503
|
+
async function generateCommitMessageWithAI(files, diffContext, provider, projectPath) {
|
|
504
|
+
// Create the prompt
|
|
505
|
+
const prompt = `You are a git commit message generator. Based on the following file changes and diffs, generate a commit message in conventional commit format.
|
|
506
|
+
|
|
507
|
+
REQUIREMENTS:
|
|
508
|
+
- Use conventional commit format: type(scope): subject
|
|
509
|
+
- Include a body that explains what changed and why
|
|
510
|
+
- Valid types: feat, fix, docs, style, refactor, perf, test, build, ci, chore
|
|
511
|
+
- Keep subject line under 50 characters
|
|
512
|
+
- Wrap body at 72 characters
|
|
513
|
+
- Be specific and descriptive
|
|
514
|
+
- Return ONLY the commit message, nothing else - no markdown, no explanations, no code blocks
|
|
515
|
+
|
|
516
|
+
FILES CHANGED:
|
|
517
|
+
${files.map(f => `- ${f}`).join('\n')}
|
|
518
|
+
|
|
519
|
+
DIFFS:
|
|
520
|
+
${diffContext.substring(0, 4000)}
|
|
521
|
+
|
|
522
|
+
Generate the commit message now:`;
|
|
523
|
+
|
|
524
|
+
try {
|
|
525
|
+
// Create a simple writer that collects the response
|
|
526
|
+
let responseText = '';
|
|
527
|
+
const writer = {
|
|
528
|
+
send: (data) => {
|
|
529
|
+
try {
|
|
530
|
+
const parsed = typeof data === 'string' ? JSON.parse(data) : data;
|
|
531
|
+
console.log('🔍 Writer received message type:', parsed.type);
|
|
532
|
+
|
|
533
|
+
// Handle different message formats from Claude SDK and Cursor CLI
|
|
534
|
+
// Claude SDK sends: {type: 'claude-response', data: {message: {content: [...]}}}
|
|
535
|
+
if (parsed.type === 'claude-response' && parsed.data) {
|
|
536
|
+
const message = parsed.data.message || parsed.data;
|
|
537
|
+
console.log('📦 Claude response message:', JSON.stringify(message, null, 2).substring(0, 500));
|
|
538
|
+
if (message.content && Array.isArray(message.content)) {
|
|
539
|
+
// Extract text from content array
|
|
540
|
+
for (const item of message.content) {
|
|
541
|
+
if (item.type === 'text' && item.text) {
|
|
542
|
+
console.log('✅ Extracted text chunk:', item.text.substring(0, 100));
|
|
543
|
+
responseText += item.text;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
// Cursor CLI sends: {type: 'cursor-output', output: '...'}
|
|
549
|
+
else if (parsed.type === 'cursor-output' && parsed.output) {
|
|
550
|
+
console.log('✅ Cursor output:', parsed.output.substring(0, 100));
|
|
551
|
+
responseText += parsed.output;
|
|
552
|
+
}
|
|
553
|
+
// Also handle direct text messages
|
|
554
|
+
else if (parsed.type === 'text' && parsed.text) {
|
|
555
|
+
console.log('✅ Direct text:', parsed.text.substring(0, 100));
|
|
556
|
+
responseText += parsed.text;
|
|
557
|
+
}
|
|
558
|
+
} catch (e) {
|
|
559
|
+
// Ignore parse errors
|
|
560
|
+
console.error('Error parsing writer data:', e);
|
|
561
|
+
}
|
|
562
|
+
},
|
|
563
|
+
setSessionId: () => {}, // No-op for this use case
|
|
564
|
+
};
|
|
565
|
+
|
|
566
|
+
console.log('🚀 Calling AI agent with provider:', provider);
|
|
567
|
+
console.log('📝 Prompt length:', prompt.length);
|
|
568
|
+
|
|
569
|
+
// Call the appropriate agent
|
|
570
|
+
if (provider === 'claude') {
|
|
571
|
+
await queryClaudeSDK(prompt, {
|
|
572
|
+
cwd: projectPath,
|
|
573
|
+
permissionMode: 'bypassPermissions',
|
|
574
|
+
model: 'sonnet'
|
|
575
|
+
}, writer);
|
|
576
|
+
} else if (provider === 'cursor') {
|
|
577
|
+
await spawnCursor(prompt, {
|
|
578
|
+
cwd: projectPath,
|
|
579
|
+
skipPermissions: true
|
|
580
|
+
}, writer);
|
|
416
581
|
}
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
582
|
+
|
|
583
|
+
console.log('📊 Total response text collected:', responseText.length, 'characters');
|
|
584
|
+
console.log('📄 Response preview:', responseText.substring(0, 200));
|
|
585
|
+
|
|
586
|
+
// Clean up the response
|
|
587
|
+
const cleanedMessage = cleanCommitMessage(responseText);
|
|
588
|
+
console.log('🧹 Cleaned message:', cleanedMessage.substring(0, 200));
|
|
589
|
+
|
|
590
|
+
return cleanedMessage || 'chore: update files';
|
|
591
|
+
} catch (error) {
|
|
592
|
+
console.error('Error generating commit message with AI:', error);
|
|
593
|
+
// Fallback to simple message
|
|
594
|
+
return `chore: update ${files.length} file${files.length !== 1 ? 's' : ''}`;
|
|
421
595
|
}
|
|
422
596
|
}
|
|
423
597
|
|
|
598
|
+
/**
|
|
599
|
+
* Cleans the AI-generated commit message by removing markdown, code blocks, and extra formatting
|
|
600
|
+
* @param {string} text - Raw AI response
|
|
601
|
+
* @returns {string} Clean commit message
|
|
602
|
+
*/
|
|
603
|
+
function cleanCommitMessage(text) {
|
|
604
|
+
if (!text || !text.trim()) {
|
|
605
|
+
return '';
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
let cleaned = text.trim();
|
|
609
|
+
|
|
610
|
+
// Remove markdown code blocks
|
|
611
|
+
cleaned = cleaned.replace(/```[a-z]*\n/g, '');
|
|
612
|
+
cleaned = cleaned.replace(/```/g, '');
|
|
613
|
+
|
|
614
|
+
// Remove markdown headers
|
|
615
|
+
cleaned = cleaned.replace(/^#+\s*/gm, '');
|
|
616
|
+
|
|
617
|
+
// Remove leading/trailing quotes
|
|
618
|
+
cleaned = cleaned.replace(/^["']|["']$/g, '');
|
|
619
|
+
|
|
620
|
+
// If there are multiple lines, take everything (subject + body)
|
|
621
|
+
// Just clean up extra blank lines
|
|
622
|
+
cleaned = cleaned.replace(/\n{3,}/g, '\n\n');
|
|
623
|
+
|
|
624
|
+
// Remove any explanatory text before the actual commit message
|
|
625
|
+
// Look for conventional commit pattern and start from there
|
|
626
|
+
const conventionalCommitMatch = cleaned.match(/(feat|fix|docs|style|refactor|perf|test|build|ci|chore)(\(.+?\))?:.+/s);
|
|
627
|
+
if (conventionalCommitMatch) {
|
|
628
|
+
cleaned = cleaned.substring(cleaned.indexOf(conventionalCommitMatch[0]));
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
return cleaned.trim();
|
|
632
|
+
}
|
|
633
|
+
|
|
424
634
|
// Get remote status (ahead/behind commits with smart remote detection)
|
|
425
635
|
router.get('/remote-status', async (req, res) => {
|
|
426
636
|
const { project } = req.query;
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import { apiKeysDb, credentialsDb } from '../database/db.js';
|
|
3
|
+
|
|
4
|
+
const router = express.Router();
|
|
5
|
+
|
|
6
|
+
// ===============================
|
|
7
|
+
// API Keys Management
|
|
8
|
+
// ===============================
|
|
9
|
+
|
|
10
|
+
// Get all API keys for the authenticated user
|
|
11
|
+
router.get('/api-keys', async (req, res) => {
|
|
12
|
+
try {
|
|
13
|
+
const apiKeys = apiKeysDb.getApiKeys(req.user.id);
|
|
14
|
+
// Don't send the full API key in the list for security
|
|
15
|
+
const sanitizedKeys = apiKeys.map(key => ({
|
|
16
|
+
...key,
|
|
17
|
+
api_key: key.api_key.substring(0, 10) + '...'
|
|
18
|
+
}));
|
|
19
|
+
res.json({ apiKeys: sanitizedKeys });
|
|
20
|
+
} catch (error) {
|
|
21
|
+
console.error('Error fetching API keys:', error);
|
|
22
|
+
res.status(500).json({ error: 'Failed to fetch API keys' });
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// Create a new API key
|
|
27
|
+
router.post('/api-keys', async (req, res) => {
|
|
28
|
+
try {
|
|
29
|
+
const { keyName } = req.body;
|
|
30
|
+
|
|
31
|
+
if (!keyName || !keyName.trim()) {
|
|
32
|
+
return res.status(400).json({ error: 'Key name is required' });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const result = apiKeysDb.createApiKey(req.user.id, keyName.trim());
|
|
36
|
+
res.json({
|
|
37
|
+
success: true,
|
|
38
|
+
apiKey: result
|
|
39
|
+
});
|
|
40
|
+
} catch (error) {
|
|
41
|
+
console.error('Error creating API key:', error);
|
|
42
|
+
res.status(500).json({ error: 'Failed to create API key' });
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Delete an API key
|
|
47
|
+
router.delete('/api-keys/:keyId', async (req, res) => {
|
|
48
|
+
try {
|
|
49
|
+
const { keyId } = req.params;
|
|
50
|
+
const success = apiKeysDb.deleteApiKey(req.user.id, parseInt(keyId));
|
|
51
|
+
|
|
52
|
+
if (success) {
|
|
53
|
+
res.json({ success: true });
|
|
54
|
+
} else {
|
|
55
|
+
res.status(404).json({ error: 'API key not found' });
|
|
56
|
+
}
|
|
57
|
+
} catch (error) {
|
|
58
|
+
console.error('Error deleting API key:', error);
|
|
59
|
+
res.status(500).json({ error: 'Failed to delete API key' });
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// Toggle API key active status
|
|
64
|
+
router.patch('/api-keys/:keyId/toggle', async (req, res) => {
|
|
65
|
+
try {
|
|
66
|
+
const { keyId } = req.params;
|
|
67
|
+
const { isActive } = req.body;
|
|
68
|
+
|
|
69
|
+
if (typeof isActive !== 'boolean') {
|
|
70
|
+
return res.status(400).json({ error: 'isActive must be a boolean' });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const success = apiKeysDb.toggleApiKey(req.user.id, parseInt(keyId), isActive);
|
|
74
|
+
|
|
75
|
+
if (success) {
|
|
76
|
+
res.json({ success: true });
|
|
77
|
+
} else {
|
|
78
|
+
res.status(404).json({ error: 'API key not found' });
|
|
79
|
+
}
|
|
80
|
+
} catch (error) {
|
|
81
|
+
console.error('Error toggling API key:', error);
|
|
82
|
+
res.status(500).json({ error: 'Failed to toggle API key' });
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// ===============================
|
|
87
|
+
// Generic Credentials Management
|
|
88
|
+
// ===============================
|
|
89
|
+
|
|
90
|
+
// Get all credentials for the authenticated user (optionally filtered by type)
|
|
91
|
+
router.get('/credentials', async (req, res) => {
|
|
92
|
+
try {
|
|
93
|
+
const { type } = req.query;
|
|
94
|
+
const credentials = credentialsDb.getCredentials(req.user.id, type || null);
|
|
95
|
+
// Don't send the actual credential values for security
|
|
96
|
+
res.json({ credentials });
|
|
97
|
+
} catch (error) {
|
|
98
|
+
console.error('Error fetching credentials:', error);
|
|
99
|
+
res.status(500).json({ error: 'Failed to fetch credentials' });
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// Create a new credential
|
|
104
|
+
router.post('/credentials', async (req, res) => {
|
|
105
|
+
try {
|
|
106
|
+
const { credentialName, credentialType, credentialValue, description } = req.body;
|
|
107
|
+
|
|
108
|
+
if (!credentialName || !credentialName.trim()) {
|
|
109
|
+
return res.status(400).json({ error: 'Credential name is required' });
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (!credentialType || !credentialType.trim()) {
|
|
113
|
+
return res.status(400).json({ error: 'Credential type is required' });
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (!credentialValue || !credentialValue.trim()) {
|
|
117
|
+
return res.status(400).json({ error: 'Credential value is required' });
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const result = credentialsDb.createCredential(
|
|
121
|
+
req.user.id,
|
|
122
|
+
credentialName.trim(),
|
|
123
|
+
credentialType.trim(),
|
|
124
|
+
credentialValue.trim(),
|
|
125
|
+
description?.trim() || null
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
res.json({
|
|
129
|
+
success: true,
|
|
130
|
+
credential: result
|
|
131
|
+
});
|
|
132
|
+
} catch (error) {
|
|
133
|
+
console.error('Error creating credential:', error);
|
|
134
|
+
res.status(500).json({ error: 'Failed to create credential' });
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// Delete a credential
|
|
139
|
+
router.delete('/credentials/:credentialId', async (req, res) => {
|
|
140
|
+
try {
|
|
141
|
+
const { credentialId } = req.params;
|
|
142
|
+
const success = credentialsDb.deleteCredential(req.user.id, parseInt(credentialId));
|
|
143
|
+
|
|
144
|
+
if (success) {
|
|
145
|
+
res.json({ success: true });
|
|
146
|
+
} else {
|
|
147
|
+
res.status(404).json({ error: 'Credential not found' });
|
|
148
|
+
}
|
|
149
|
+
} catch (error) {
|
|
150
|
+
console.error('Error deleting credential:', error);
|
|
151
|
+
res.status(500).json({ error: 'Failed to delete credential' });
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// Toggle credential active status
|
|
156
|
+
router.patch('/credentials/:credentialId/toggle', async (req, res) => {
|
|
157
|
+
try {
|
|
158
|
+
const { credentialId } = req.params;
|
|
159
|
+
const { isActive } = req.body;
|
|
160
|
+
|
|
161
|
+
if (typeof isActive !== 'boolean') {
|
|
162
|
+
return res.status(400).json({ error: 'isActive must be a boolean' });
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const success = credentialsDb.toggleCredential(req.user.id, parseInt(credentialId), isActive);
|
|
166
|
+
|
|
167
|
+
if (success) {
|
|
168
|
+
res.json({ success: true });
|
|
169
|
+
} else {
|
|
170
|
+
res.status(404).json({ error: 'Credential not found' });
|
|
171
|
+
}
|
|
172
|
+
} catch (error) {
|
|
173
|
+
console.error('Error toggling credential:', error);
|
|
174
|
+
res.status(500).json({ error: 'Failed to toggle credential' });
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
export default router;
|