2 Commits

Author SHA1 Message Date
simos
6219c273a2 small fix 2025-11-17 08:48:23 +01:00
simos
33834d808b feature:show auth status on settings 2025-11-17 08:48:15 +01:00
3 changed files with 365 additions and 40 deletions

View File

@@ -70,6 +70,7 @@ import commandsRoutes from './routes/commands.js';
import settingsRoutes from './routes/settings.js';
import agentRoutes from './routes/agent.js';
import projectsRoutes from './routes/projects.js';
import cliAuthRoutes from './routes/cli-auth.js';
import { initializeDatabase } from './database/db.js';
import { validateApiKey, authenticateToken, authenticateWebSocket } from './middleware/auth.js';
@@ -250,6 +251,9 @@ app.use('/api/commands', authenticateToken, commandsRoutes);
// Settings API Routes (protected)
app.use('/api/settings', authenticateToken, settingsRoutes);
// CLI Authentication API Routes (protected)
app.use('/api/cli', authenticateToken, cliAuthRoutes);
// Agent API Routes (uses API key authentication)
app.use('/api/agent', agentRoutes);

179
server/routes/cli-auth.js Normal file
View 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;

View File

@@ -81,10 +81,23 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'tools' }) {
const [newCursorDisallowedCommand, setNewCursorDisallowedCommand] = useState('');
const [cursorMcpServers, setCursorMcpServers] = useState([]);
// Login modal states
const [showLoginModal, setShowLoginModal] = useState(false);
const [loginProvider, setLoginProvider] = useState(''); // 'claude' or 'cursor'
const [loginProvider, setLoginProvider] = useState('');
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
const commonTools = [
'Bash(git log:*)',
@@ -344,7 +357,8 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'tools' }) {
useEffect(() => {
if (isOpen) {
loadSettings();
// Set the active tab when the modal opens
checkClaudeAuthStatus();
checkCursorAuthStatus();
setActiveTab(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 = () => {
setLoginProvider('claude');
setSelectedProject(projects?.[0] || { name: 'default', fullPath: process.cwd() });
@@ -440,10 +526,14 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'tools' }) {
const handleLoginComplete = (exitCode) => {
if (exitCode === 0) {
// Login successful - could show a success message here
setSaveStatus('success');
if (loginProvider === 'claude') {
checkClaudeAuthStatus();
} else if (loginProvider === 'cursor') {
checkCursorAuthStatus();
}
}
// Modal will close itself via the LoginModal component
};
const saveSettings = () => {
@@ -1030,7 +1120,6 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'tools' }) {
</div>
</div>
{/* Claude Login */}
<div className="space-y-4">
<div className="flex items-center gap-3">
<LogIn className="w-5 h-5 text-blue-500" />
@@ -1039,13 +1128,39 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'tools' }) {
</h3>
</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="space-y-3">
<div className="flex items-center gap-2">
{claudeAuthStatus.loading ? (
<span className="text-sm text-blue-700 dark:text-blue-300">
Checking authentication...
</span>
) : claudeAuthStatus.authenticated ? (
<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">
Sign in to your Claude account to enable AI features
{claudeAuthStatus.authenticated
? 'Re-authenticate or switch accounts'
: 'Sign in to your Claude account to enable AI features'}
</div>
</div>
<Button
@@ -1059,6 +1174,7 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'tools' }) {
</div>
</div>
</div>
</div>
{/* Allowed Tools */}
<div className="space-y-4">
@@ -1790,7 +1906,6 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'tools' }) {
</div>
</div>
{/* Cursor Login */}
<div className="space-y-4">
<div className="flex items-center gap-3">
<LogIn className="w-5 h-5 text-purple-500" />
@@ -1799,13 +1914,39 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'tools' }) {
</h3>
</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="space-y-3">
<div className="flex items-center gap-2">
{cursorAuthStatus.loading ? (
<span className="text-sm text-purple-700 dark:text-purple-300">
Checking authentication...
</span>
) : cursorAuthStatus.authenticated ? (
<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">
Sign in to your Cursor account to enable AI features
{cursorAuthStatus.authenticated
? 'Re-authenticate or switch accounts'
: 'Sign in to your Cursor account to enable AI features'}
</div>
</div>
<Button
@@ -1819,6 +1960,7 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'tools' }) {
</div>
</div>
</div>
</div>
{/* Allowed Shell Commands */}
<div className="space-y-4">