mirror of
https://github.com/siteboon/claudecodeui.git
synced 2025-12-08 20:29:37 +00:00
Compare commits
2 Commits
abe8cd46a2
...
6219c273a2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6219c273a2 | ||
|
|
33834d808b |
@@ -70,6 +70,7 @@ import commandsRoutes from './routes/commands.js';
|
|||||||
import settingsRoutes from './routes/settings.js';
|
import settingsRoutes from './routes/settings.js';
|
||||||
import agentRoutes from './routes/agent.js';
|
import agentRoutes from './routes/agent.js';
|
||||||
import projectsRoutes from './routes/projects.js';
|
import projectsRoutes from './routes/projects.js';
|
||||||
|
import cliAuthRoutes from './routes/cli-auth.js';
|
||||||
import { initializeDatabase } from './database/db.js';
|
import { initializeDatabase } from './database/db.js';
|
||||||
import { validateApiKey, authenticateToken, authenticateWebSocket } from './middleware/auth.js';
|
import { validateApiKey, authenticateToken, authenticateWebSocket } from './middleware/auth.js';
|
||||||
|
|
||||||
@@ -250,6 +251,9 @@ app.use('/api/commands', authenticateToken, commandsRoutes);
|
|||||||
// Settings API Routes (protected)
|
// Settings API Routes (protected)
|
||||||
app.use('/api/settings', authenticateToken, settingsRoutes);
|
app.use('/api/settings', authenticateToken, settingsRoutes);
|
||||||
|
|
||||||
|
// CLI Authentication API Routes (protected)
|
||||||
|
app.use('/api/cli', authenticateToken, cliAuthRoutes);
|
||||||
|
|
||||||
// Agent API Routes (uses API key authentication)
|
// Agent API Routes (uses API key authentication)
|
||||||
app.use('/api/agent', agentRoutes);
|
app.use('/api/agent', agentRoutes);
|
||||||
|
|
||||||
|
|||||||
179
server/routes/cli-auth.js
Normal file
179
server/routes/cli-auth.js
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
import express from 'express';
|
||||||
|
import { spawn } from 'child_process';
|
||||||
|
import fs from 'fs/promises';
|
||||||
|
import path from 'path';
|
||||||
|
import os from 'os';
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
router.get('/claude/status', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const credentialsResult = await checkClaudeCredentials();
|
||||||
|
|
||||||
|
if (credentialsResult.authenticated) {
|
||||||
|
return res.json({
|
||||||
|
authenticated: true,
|
||||||
|
email: credentialsResult.email || 'Authenticated',
|
||||||
|
method: 'credentials_file'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.json({
|
||||||
|
authenticated: false,
|
||||||
|
email: null,
|
||||||
|
error: credentialsResult.error || 'Not authenticated'
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error checking Claude auth status:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
authenticated: false,
|
||||||
|
email: null,
|
||||||
|
error: error.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/cursor/status', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const result = await checkCursorStatus();
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
authenticated: result.authenticated,
|
||||||
|
email: result.email,
|
||||||
|
error: result.error
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error checking Cursor auth status:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
authenticated: false,
|
||||||
|
email: null,
|
||||||
|
error: error.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async function checkClaudeCredentials() {
|
||||||
|
try {
|
||||||
|
const credPath = path.join(os.homedir(), '.claude', '.credentials.json');
|
||||||
|
const content = await fs.readFile(credPath, 'utf8');
|
||||||
|
const creds = JSON.parse(content);
|
||||||
|
|
||||||
|
if (creds.accessToken) {
|
||||||
|
const isExpired = creds.expiresAt && Date.now() >= creds.expiresAt;
|
||||||
|
|
||||||
|
if (!isExpired) {
|
||||||
|
return {
|
||||||
|
authenticated: true,
|
||||||
|
email: creds.email || creds.user || null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
authenticated: false,
|
||||||
|
email: null
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
authenticated: false,
|
||||||
|
email: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkCursorStatus() {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
let processCompleted = false;
|
||||||
|
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
if (!processCompleted) {
|
||||||
|
processCompleted = true;
|
||||||
|
if (childProcess) {
|
||||||
|
childProcess.kill();
|
||||||
|
}
|
||||||
|
resolve({
|
||||||
|
authenticated: false,
|
||||||
|
email: null,
|
||||||
|
error: 'Command timeout'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
|
let childProcess;
|
||||||
|
try {
|
||||||
|
childProcess = spawn('cursor-agent', ['status']);
|
||||||
|
} catch (err) {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
processCompleted = true;
|
||||||
|
resolve({
|
||||||
|
authenticated: false,
|
||||||
|
email: null,
|
||||||
|
error: 'Cursor CLI not found or not installed'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let stdout = '';
|
||||||
|
let stderr = '';
|
||||||
|
|
||||||
|
childProcess.stdout.on('data', (data) => {
|
||||||
|
stdout += data.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
childProcess.stderr.on('data', (data) => {
|
||||||
|
stderr += data.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
childProcess.on('close', (code) => {
|
||||||
|
if (processCompleted) return;
|
||||||
|
processCompleted = true;
|
||||||
|
clearTimeout(timeout);
|
||||||
|
|
||||||
|
if (code === 0) {
|
||||||
|
const emailMatch = stdout.match(/Logged in as ([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/i);
|
||||||
|
|
||||||
|
if (emailMatch) {
|
||||||
|
resolve({
|
||||||
|
authenticated: true,
|
||||||
|
email: emailMatch[1],
|
||||||
|
output: stdout
|
||||||
|
});
|
||||||
|
} else if (stdout.includes('Logged in')) {
|
||||||
|
resolve({
|
||||||
|
authenticated: true,
|
||||||
|
email: 'Logged in',
|
||||||
|
output: stdout
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
resolve({
|
||||||
|
authenticated: false,
|
||||||
|
email: null,
|
||||||
|
error: 'Not logged in'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
resolve({
|
||||||
|
authenticated: false,
|
||||||
|
email: null,
|
||||||
|
error: stderr || 'Not logged in'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
childProcess.on('error', (err) => {
|
||||||
|
if (processCompleted) return;
|
||||||
|
processCompleted = true;
|
||||||
|
clearTimeout(timeout);
|
||||||
|
|
||||||
|
resolve({
|
||||||
|
authenticated: false,
|
||||||
|
email: null,
|
||||||
|
error: 'Cursor CLI not found or not installed'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default router;
|
||||||
@@ -81,10 +81,23 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'tools' }) {
|
|||||||
const [newCursorDisallowedCommand, setNewCursorDisallowedCommand] = useState('');
|
const [newCursorDisallowedCommand, setNewCursorDisallowedCommand] = useState('');
|
||||||
const [cursorMcpServers, setCursorMcpServers] = useState([]);
|
const [cursorMcpServers, setCursorMcpServers] = useState([]);
|
||||||
|
|
||||||
// Login modal states
|
|
||||||
const [showLoginModal, setShowLoginModal] = useState(false);
|
const [showLoginModal, setShowLoginModal] = useState(false);
|
||||||
const [loginProvider, setLoginProvider] = useState(''); // 'claude' or 'cursor'
|
const [loginProvider, setLoginProvider] = useState('');
|
||||||
const [selectedProject, setSelectedProject] = useState(null);
|
const [selectedProject, setSelectedProject] = useState(null);
|
||||||
|
|
||||||
|
const [claudeAuthStatus, setClaudeAuthStatus] = useState({
|
||||||
|
authenticated: false,
|
||||||
|
email: null,
|
||||||
|
loading: true,
|
||||||
|
error: null
|
||||||
|
});
|
||||||
|
const [cursorAuthStatus, setCursorAuthStatus] = useState({
|
||||||
|
authenticated: false,
|
||||||
|
email: null,
|
||||||
|
loading: true,
|
||||||
|
error: null
|
||||||
|
});
|
||||||
|
|
||||||
// Common tool patterns for Claude
|
// Common tool patterns for Claude
|
||||||
const commonTools = [
|
const commonTools = [
|
||||||
'Bash(git log:*)',
|
'Bash(git log:*)',
|
||||||
@@ -344,7 +357,8 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'tools' }) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
loadSettings();
|
loadSettings();
|
||||||
// Set the active tab when the modal opens
|
checkClaudeAuthStatus();
|
||||||
|
checkCursorAuthStatus();
|
||||||
setActiveTab(initialTab);
|
setActiveTab(initialTab);
|
||||||
}
|
}
|
||||||
}, [isOpen, initialTab]);
|
}, [isOpen, initialTab]);
|
||||||
@@ -425,7 +439,79 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'tools' }) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Login handlers
|
const checkClaudeAuthStatus = async () => {
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem('auth-token');
|
||||||
|
const response = await fetch('/api/cli/claude/status', {
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${token}`,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
setClaudeAuthStatus({
|
||||||
|
authenticated: data.authenticated,
|
||||||
|
email: data.email,
|
||||||
|
loading: false,
|
||||||
|
error: data.error || null
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setClaudeAuthStatus({
|
||||||
|
authenticated: false,
|
||||||
|
email: null,
|
||||||
|
loading: false,
|
||||||
|
error: 'Failed to check authentication status'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error checking Claude auth status:', error);
|
||||||
|
setClaudeAuthStatus({
|
||||||
|
authenticated: false,
|
||||||
|
email: null,
|
||||||
|
loading: false,
|
||||||
|
error: error.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const checkCursorAuthStatus = async () => {
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem('auth-token');
|
||||||
|
const response = await fetch('/api/cli/cursor/status', {
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${token}`,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
setCursorAuthStatus({
|
||||||
|
authenticated: data.authenticated,
|
||||||
|
email: data.email,
|
||||||
|
loading: false,
|
||||||
|
error: data.error || null
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setCursorAuthStatus({
|
||||||
|
authenticated: false,
|
||||||
|
email: null,
|
||||||
|
loading: false,
|
||||||
|
error: 'Failed to check authentication status'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error checking Cursor auth status:', error);
|
||||||
|
setCursorAuthStatus({
|
||||||
|
authenticated: false,
|
||||||
|
email: null,
|
||||||
|
loading: false,
|
||||||
|
error: error.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
const handleClaudeLogin = () => {
|
const handleClaudeLogin = () => {
|
||||||
setLoginProvider('claude');
|
setLoginProvider('claude');
|
||||||
setSelectedProject(projects?.[0] || { name: 'default', fullPath: process.cwd() });
|
setSelectedProject(projects?.[0] || { name: 'default', fullPath: process.cwd() });
|
||||||
@@ -440,10 +526,14 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'tools' }) {
|
|||||||
|
|
||||||
const handleLoginComplete = (exitCode) => {
|
const handleLoginComplete = (exitCode) => {
|
||||||
if (exitCode === 0) {
|
if (exitCode === 0) {
|
||||||
// Login successful - could show a success message here
|
|
||||||
setSaveStatus('success');
|
setSaveStatus('success');
|
||||||
|
|
||||||
|
if (loginProvider === 'claude') {
|
||||||
|
checkClaudeAuthStatus();
|
||||||
|
} else if (loginProvider === 'cursor') {
|
||||||
|
checkCursorAuthStatus();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Modal will close itself via the LoginModal component
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const saveSettings = () => {
|
const saveSettings = () => {
|
||||||
@@ -1030,7 +1120,6 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'tools' }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Claude Login */}
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<LogIn className="w-5 h-5 text-blue-500" />
|
<LogIn className="w-5 h-5 text-blue-500" />
|
||||||
@@ -1039,23 +1128,50 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'tools' }) {
|
|||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4">
|
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="space-y-3">
|
||||||
<div>
|
<div className="flex items-center gap-2">
|
||||||
<div className="font-medium text-blue-900 dark:text-blue-100">
|
{claudeAuthStatus.loading ? (
|
||||||
Claude CLI Login
|
<span className="text-sm text-blue-700 dark:text-blue-300">
|
||||||
</div>
|
Checking authentication...
|
||||||
<div className="text-sm text-blue-700 dark:text-blue-300">
|
</span>
|
||||||
Sign in to your Claude account to enable AI features
|
) : claudeAuthStatus.authenticated ? (
|
||||||
</div>
|
<div className="flex items-center gap-2">
|
||||||
|
<Badge variant="success" className="bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300">
|
||||||
|
✓ Logged in
|
||||||
|
</Badge>
|
||||||
|
{claudeAuthStatus.email && (
|
||||||
|
<span className="text-sm text-blue-700 dark:text-blue-300">
|
||||||
|
as {claudeAuthStatus.email}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Badge variant="secondary" className="bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-300">
|
||||||
|
Not authenticated
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<div className="font-medium text-blue-900 dark:text-blue-100">
|
||||||
|
Claude CLI Login
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-blue-700 dark:text-blue-300">
|
||||||
|
{claudeAuthStatus.authenticated
|
||||||
|
? 'Re-authenticate or switch accounts'
|
||||||
|
: 'Sign in to your Claude account to enable AI features'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
onClick={handleClaudeLogin}
|
||||||
|
className="bg-blue-600 hover:bg-blue-700 text-white"
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
<LogIn className="w-4 h-4 mr-2" />
|
||||||
|
Login
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
|
||||||
onClick={handleClaudeLogin}
|
|
||||||
className="bg-blue-600 hover:bg-blue-700 text-white"
|
|
||||||
size="sm"
|
|
||||||
>
|
|
||||||
<LogIn className="w-4 h-4 mr-2" />
|
|
||||||
Login
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1790,7 +1906,6 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'tools' }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Cursor Login */}
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<LogIn className="w-5 h-5 text-purple-500" />
|
<LogIn className="w-5 h-5 text-purple-500" />
|
||||||
@@ -1799,23 +1914,50 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'tools' }) {
|
|||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-purple-50 dark:bg-purple-900/20 border border-purple-200 dark:border-purple-800 rounded-lg p-4">
|
<div className="bg-purple-50 dark:bg-purple-900/20 border border-purple-200 dark:border-purple-800 rounded-lg p-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="space-y-3">
|
||||||
<div>
|
<div className="flex items-center gap-2">
|
||||||
<div className="font-medium text-purple-900 dark:text-purple-100">
|
{cursorAuthStatus.loading ? (
|
||||||
Cursor CLI Login
|
<span className="text-sm text-purple-700 dark:text-purple-300">
|
||||||
</div>
|
Checking authentication...
|
||||||
<div className="text-sm text-purple-700 dark:text-purple-300">
|
</span>
|
||||||
Sign in to your Cursor account to enable AI features
|
) : cursorAuthStatus.authenticated ? (
|
||||||
</div>
|
<div className="flex items-center gap-2">
|
||||||
|
<Badge variant="success" className="bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300">
|
||||||
|
✓ Logged in
|
||||||
|
</Badge>
|
||||||
|
{cursorAuthStatus.email && (
|
||||||
|
<span className="text-sm text-purple-700 dark:text-purple-300">
|
||||||
|
as {cursorAuthStatus.email}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Badge variant="secondary" className="bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-300">
|
||||||
|
Not authenticated
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<div className="font-medium text-purple-900 dark:text-purple-100">
|
||||||
|
Cursor CLI Login
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-purple-700 dark:text-purple-300">
|
||||||
|
{cursorAuthStatus.authenticated
|
||||||
|
? 'Re-authenticate or switch accounts'
|
||||||
|
: 'Sign in to your Cursor account to enable AI features'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
onClick={handleCursorLogin}
|
||||||
|
className="bg-purple-600 hover:bg-purple-700 text-white"
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
<LogIn className="w-4 h-4 mr-2" />
|
||||||
|
Login
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
|
||||||
onClick={handleCursorLogin}
|
|
||||||
className="bg-purple-600 hover:bg-purple-700 text-white"
|
|
||||||
size="sm"
|
|
||||||
>
|
|
||||||
<LogIn className="w-4 h-4 mr-2" />
|
|
||||||
Login
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user