mirror of
https://github.com/siteboon/claudecodeui.git
synced 2025-12-13 13:49:43 +00:00
Update package version to 1.1.3, add new dependencies for authentication and database management, and implement user authentication features including registration and login. Enhance API routes for protected access and integrate WebSocket authentication.
This commit is contained in:
100
server/database/db.js
Normal file
100
server/database/db.js
Normal file
@@ -0,0 +1,100 @@
|
||||
const sqlite3 = require('sqlite3').verbose();
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
const DB_PATH = path.join(__dirname, 'auth.db');
|
||||
const INIT_SQL_PATH = path.join(__dirname, 'init.sql');
|
||||
|
||||
// Create database connection
|
||||
const db = new sqlite3.Database(DB_PATH, (err) => {
|
||||
if (err) {
|
||||
console.error('Error opening database:', err.message);
|
||||
} else {
|
||||
console.log('Connected to SQLite database');
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize database with schema
|
||||
const initializeDatabase = async () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
const initSQL = fs.readFileSync(INIT_SQL_PATH, 'utf8');
|
||||
db.exec(initSQL, (err) => {
|
||||
if (err) {
|
||||
console.error('Error initializing database:', err.message);
|
||||
reject(err);
|
||||
} else {
|
||||
console.log('Database initialized successfully');
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error reading init SQL file:', error);
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// User database operations
|
||||
const userDb = {
|
||||
// Check if any users exist
|
||||
hasUsers: () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.get('SELECT COUNT(*) as count FROM users', (err, row) => {
|
||||
if (err) reject(err);
|
||||
else resolve(row.count > 0);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
// Create a new user
|
||||
createUser: (username, passwordHash) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const stmt = db.prepare('INSERT INTO users (username, password_hash) VALUES (?, ?)');
|
||||
stmt.run(username, passwordHash, function(err) {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve({ id: this.lastID, username });
|
||||
}
|
||||
});
|
||||
stmt.finalize();
|
||||
});
|
||||
},
|
||||
|
||||
// Get user by username
|
||||
getUserByUsername: (username) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.get('SELECT * FROM users WHERE username = ? AND is_active = 1', [username], (err, row) => {
|
||||
if (err) reject(err);
|
||||
else resolve(row);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
// Update last login time
|
||||
updateLastLogin: (userId) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.run('UPDATE users SET last_login = CURRENT_TIMESTAMP WHERE id = ?', [userId], (err) => {
|
||||
if (err) reject(err);
|
||||
else resolve();
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
// Get user by ID
|
||||
getUserById: (userId) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.get('SELECT id, username, created_at, last_login FROM users WHERE id = ? AND is_active = 1', [userId], (err, row) => {
|
||||
if (err) reject(err);
|
||||
else resolve(row);
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
db,
|
||||
initializeDatabase,
|
||||
userDb
|
||||
};
|
||||
16
server/database/init.sql
Normal file
16
server/database/init.sql
Normal file
@@ -0,0 +1,16 @@
|
||||
-- Initialize authentication database
|
||||
PRAGMA foreign_keys = ON;
|
||||
|
||||
-- Users table (single user system)
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
username TEXT UNIQUE NOT NULL,
|
||||
password_hash TEXT NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
last_login DATETIME,
|
||||
is_active BOOLEAN DEFAULT 1
|
||||
);
|
||||
|
||||
-- Indexes for performance
|
||||
CREATE INDEX IF NOT EXISTS idx_users_username ON users(username);
|
||||
CREATE INDEX IF NOT EXISTS idx_users_active ON users(is_active);
|
||||
100
server/index.js
100
server/index.js
@@ -33,6 +33,9 @@ const fetch = require('node-fetch');
|
||||
const { getProjects, getSessions, getSessionMessages, renameProject, deleteSession, deleteProject, addProjectManually, extractProjectDirectory, clearProjectDirectoryCache } = require('./projects');
|
||||
const { spawnClaude, abortClaudeSession } = require('./claude-cli');
|
||||
const gitRoutes = require('./routes/git');
|
||||
const authRoutes = require('./routes/auth');
|
||||
const { initializeDatabase } = require('./database/db');
|
||||
const { validateApiKey, authenticateToken, authenticateWebSocket } = require('./middleware/auth');
|
||||
|
||||
// File system watcher for projects folder
|
||||
let projectsWatcher = null;
|
||||
@@ -142,19 +145,43 @@ const wss = new WebSocketServer({
|
||||
server,
|
||||
verifyClient: (info) => {
|
||||
console.log('WebSocket connection attempt to:', info.req.url);
|
||||
return true; // Accept all connections for now
|
||||
|
||||
// Extract token from query parameters or headers
|
||||
const url = new URL(info.req.url, 'http://localhost');
|
||||
const token = url.searchParams.get('token') ||
|
||||
info.req.headers.authorization?.split(' ')[1];
|
||||
|
||||
// Verify token
|
||||
const user = authenticateWebSocket(token);
|
||||
if (!user) {
|
||||
console.log('❌ WebSocket authentication failed');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Store user info in the request for later use
|
||||
info.req.user = user;
|
||||
console.log('✅ WebSocket authenticated for user:', user.username);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
app.use(cors());
|
||||
app.use(express.json());
|
||||
|
||||
// Optional API key validation (if configured)
|
||||
app.use('/api', validateApiKey);
|
||||
|
||||
// Authentication routes (public)
|
||||
app.use('/api/auth', authRoutes);
|
||||
|
||||
// Git API Routes (protected)
|
||||
app.use('/api/git', authenticateToken, gitRoutes);
|
||||
|
||||
// Static files served after API routes
|
||||
app.use(express.static(path.join(__dirname, '../dist')));
|
||||
|
||||
// Git API Routes
|
||||
app.use('/api/git', gitRoutes);
|
||||
|
||||
// API Routes
|
||||
app.get('/api/config', (req, res) => {
|
||||
// API Routes (protected)
|
||||
app.get('/api/config', authenticateToken, (req, res) => {
|
||||
// Always use the server's actual IP and port for WebSocket connections
|
||||
const serverIP = getServerIP();
|
||||
const host = `${serverIP}:${PORT}`;
|
||||
@@ -168,7 +195,7 @@ app.get('/api/config', (req, res) => {
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/api/projects', async (req, res) => {
|
||||
app.get('/api/projects', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const projects = await getProjects();
|
||||
res.json(projects);
|
||||
@@ -177,7 +204,7 @@ app.get('/api/projects', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/projects/:projectName/sessions', async (req, res) => {
|
||||
app.get('/api/projects/:projectName/sessions', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const { limit = 5, offset = 0 } = req.query;
|
||||
const result = await getSessions(req.params.projectName, parseInt(limit), parseInt(offset));
|
||||
@@ -188,7 +215,7 @@ app.get('/api/projects/:projectName/sessions', async (req, res) => {
|
||||
});
|
||||
|
||||
// Get messages for a specific session
|
||||
app.get('/api/projects/:projectName/sessions/:sessionId/messages', async (req, res) => {
|
||||
app.get('/api/projects/:projectName/sessions/:sessionId/messages', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const { projectName, sessionId } = req.params;
|
||||
const messages = await getSessionMessages(projectName, sessionId);
|
||||
@@ -199,7 +226,7 @@ app.get('/api/projects/:projectName/sessions/:sessionId/messages', async (req, r
|
||||
});
|
||||
|
||||
// Rename project endpoint
|
||||
app.put('/api/projects/:projectName/rename', async (req, res) => {
|
||||
app.put('/api/projects/:projectName/rename', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const { displayName } = req.body;
|
||||
await renameProject(req.params.projectName, displayName);
|
||||
@@ -210,7 +237,7 @@ app.put('/api/projects/:projectName/rename', async (req, res) => {
|
||||
});
|
||||
|
||||
// Delete session endpoint
|
||||
app.delete('/api/projects/:projectName/sessions/:sessionId', async (req, res) => {
|
||||
app.delete('/api/projects/:projectName/sessions/:sessionId', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const { projectName, sessionId } = req.params;
|
||||
await deleteSession(projectName, sessionId);
|
||||
@@ -221,7 +248,7 @@ app.delete('/api/projects/:projectName/sessions/:sessionId', async (req, res) =>
|
||||
});
|
||||
|
||||
// Delete project endpoint (only if empty)
|
||||
app.delete('/api/projects/:projectName', async (req, res) => {
|
||||
app.delete('/api/projects/:projectName', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const { projectName } = req.params;
|
||||
await deleteProject(projectName);
|
||||
@@ -232,7 +259,7 @@ app.delete('/api/projects/:projectName', async (req, res) => {
|
||||
});
|
||||
|
||||
// Create project endpoint
|
||||
app.post('/api/projects/create', async (req, res) => {
|
||||
app.post('/api/projects/create', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const { path: projectPath } = req.body;
|
||||
|
||||
@@ -249,7 +276,7 @@ app.post('/api/projects/create', async (req, res) => {
|
||||
});
|
||||
|
||||
// Read file content endpoint
|
||||
app.get('/api/projects/:projectName/file', async (req, res) => {
|
||||
app.get('/api/projects/:projectName/file', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const { projectName } = req.params;
|
||||
const { filePath } = req.query;
|
||||
@@ -278,7 +305,7 @@ app.get('/api/projects/:projectName/file', async (req, res) => {
|
||||
});
|
||||
|
||||
// Serve binary file content endpoint (for images, etc.)
|
||||
app.get('/api/projects/:projectName/files/content', async (req, res) => {
|
||||
app.get('/api/projects/:projectName/files/content', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const { projectName } = req.params;
|
||||
const { path: filePath } = req.query;
|
||||
@@ -324,7 +351,7 @@ app.get('/api/projects/:projectName/files/content', async (req, res) => {
|
||||
});
|
||||
|
||||
// Save file content endpoint
|
||||
app.put('/api/projects/:projectName/file', async (req, res) => {
|
||||
app.put('/api/projects/:projectName/file', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const { projectName } = req.params;
|
||||
const { filePath, content } = req.body;
|
||||
@@ -371,7 +398,7 @@ app.put('/api/projects/:projectName/file', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/projects/:projectName/files', async (req, res) => {
|
||||
app.get('/api/projects/:projectName/files', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
|
||||
const fs = require('fs').promises;
|
||||
@@ -409,12 +436,16 @@ wss.on('connection', (ws, request) => {
|
||||
const url = request.url;
|
||||
console.log('🔗 Client connected to:', url);
|
||||
|
||||
if (url === '/shell') {
|
||||
// Parse URL to get pathname without query parameters
|
||||
const urlObj = new URL(url, 'http://localhost');
|
||||
const pathname = urlObj.pathname;
|
||||
|
||||
if (pathname === '/shell') {
|
||||
handleShellConnection(ws);
|
||||
} else if (url === '/ws') {
|
||||
} else if (pathname === '/ws') {
|
||||
handleChatConnection(ws);
|
||||
} else {
|
||||
console.log('❌ Unknown WebSocket path:', url);
|
||||
console.log('❌ Unknown WebSocket path:', pathname);
|
||||
ws.close();
|
||||
}
|
||||
});
|
||||
@@ -629,7 +660,7 @@ function handleShellConnection(ws) {
|
||||
});
|
||||
}
|
||||
// Audio transcription endpoint
|
||||
app.post('/api/transcribe', async (req, res) => {
|
||||
app.post('/api/transcribe', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const multer = require('multer');
|
||||
const upload = multer({ storage: multer.memoryStorage() });
|
||||
@@ -835,9 +866,24 @@ async function getFileTree(dirPath, maxDepth = 3, currentDepth = 0, showHidden =
|
||||
}
|
||||
|
||||
const PORT = process.env.PORT || 3000;
|
||||
server.listen(PORT, '0.0.0.0', () => {
|
||||
console.log(`Claude Code UI server running on http://0.0.0.0:${PORT}`);
|
||||
|
||||
// Start watching the projects folder for changes
|
||||
setupProjectsWatcher();
|
||||
});
|
||||
|
||||
// Initialize database and start server
|
||||
async function startServer() {
|
||||
try {
|
||||
// Initialize authentication database
|
||||
await initializeDatabase();
|
||||
console.log('✅ Database initialized successfully');
|
||||
|
||||
server.listen(PORT, '0.0.0.0', () => {
|
||||
console.log(`Claude Code UI server running on http://0.0.0.0:${PORT}`);
|
||||
|
||||
// Start watching the projects folder for changes
|
||||
setupProjectsWatcher();
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to start server:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
startServer();
|
||||
80
server/middleware/auth.js
Normal file
80
server/middleware/auth.js
Normal file
@@ -0,0 +1,80 @@
|
||||
const jwt = require('jsonwebtoken');
|
||||
const { userDb } = require('../database/db');
|
||||
|
||||
// Get JWT secret from environment or use default (for development)
|
||||
const JWT_SECRET = process.env.JWT_SECRET || 'claude-ui-dev-secret-change-in-production';
|
||||
|
||||
// Optional API key middleware
|
||||
const validateApiKey = (req, res, next) => {
|
||||
// Skip API key validation if not configured
|
||||
if (!process.env.API_KEY) {
|
||||
return next();
|
||||
}
|
||||
|
||||
const apiKey = req.headers['x-api-key'];
|
||||
if (apiKey !== process.env.API_KEY) {
|
||||
return res.status(401).json({ error: 'Invalid API key' });
|
||||
}
|
||||
next();
|
||||
};
|
||||
|
||||
// JWT authentication middleware
|
||||
const authenticateToken = async (req, res, next) => {
|
||||
const authHeader = req.headers['authorization'];
|
||||
const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN
|
||||
|
||||
if (!token) {
|
||||
return res.status(401).json({ error: 'Access denied. No token provided.' });
|
||||
}
|
||||
|
||||
try {
|
||||
const decoded = jwt.verify(token, JWT_SECRET);
|
||||
|
||||
// Verify user still exists and is active
|
||||
const user = await userDb.getUserById(decoded.userId);
|
||||
if (!user) {
|
||||
return res.status(401).json({ error: 'Invalid token. User not found.' });
|
||||
}
|
||||
|
||||
req.user = user;
|
||||
next();
|
||||
} catch (error) {
|
||||
console.error('Token verification error:', error);
|
||||
return res.status(403).json({ error: 'Invalid token' });
|
||||
}
|
||||
};
|
||||
|
||||
// Generate JWT token (never expires)
|
||||
const generateToken = (user) => {
|
||||
return jwt.sign(
|
||||
{
|
||||
userId: user.id,
|
||||
username: user.username
|
||||
},
|
||||
JWT_SECRET
|
||||
// No expiration - token lasts forever
|
||||
);
|
||||
};
|
||||
|
||||
// WebSocket authentication function
|
||||
const authenticateWebSocket = (token) => {
|
||||
if (!token) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const decoded = jwt.verify(token, JWT_SECRET);
|
||||
return decoded;
|
||||
} catch (error) {
|
||||
console.error('WebSocket token verification error:', error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
validateApiKey,
|
||||
authenticateToken,
|
||||
generateToken,
|
||||
authenticateWebSocket,
|
||||
JWT_SECRET
|
||||
};
|
||||
125
server/routes/auth.js
Normal file
125
server/routes/auth.js
Normal file
@@ -0,0 +1,125 @@
|
||||
const express = require('express');
|
||||
const bcrypt = require('bcrypt');
|
||||
const { userDb } = require('../database/db');
|
||||
const { generateToken, authenticateToken } = require('../middleware/auth');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// Check auth status and setup requirements
|
||||
router.get('/status', async (req, res) => {
|
||||
try {
|
||||
const hasUsers = await userDb.hasUsers();
|
||||
res.json({
|
||||
needsSetup: !hasUsers,
|
||||
isAuthenticated: false // Will be overridden by frontend if token exists
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Auth status error:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// User registration (setup) - only allowed if no users exist
|
||||
router.post('/register', async (req, res) => {
|
||||
try {
|
||||
const { username, password } = req.body;
|
||||
|
||||
// Validate input
|
||||
if (!username || !password) {
|
||||
return res.status(400).json({ error: 'Username and password are required' });
|
||||
}
|
||||
|
||||
if (username.length < 3 || password.length < 6) {
|
||||
return res.status(400).json({ error: 'Username must be at least 3 characters, password at least 6 characters' });
|
||||
}
|
||||
|
||||
// Check if users already exist (only allow one user)
|
||||
const hasUsers = await userDb.hasUsers();
|
||||
if (hasUsers) {
|
||||
return res.status(403).json({ error: 'User already exists. This is a single-user system.' });
|
||||
}
|
||||
|
||||
// Hash password
|
||||
const saltRounds = 12;
|
||||
const passwordHash = await bcrypt.hash(password, saltRounds);
|
||||
|
||||
// Create user
|
||||
const user = await userDb.createUser(username, passwordHash);
|
||||
|
||||
// Generate token
|
||||
const token = generateToken(user);
|
||||
|
||||
// Update last login
|
||||
await userDb.updateLastLogin(user.id);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
user: { id: user.id, username: user.username },
|
||||
token
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Registration error:', error);
|
||||
if (error.code === 'SQLITE_CONSTRAINT_UNIQUE') {
|
||||
res.status(409).json({ error: 'Username already exists' });
|
||||
} else {
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// User login
|
||||
router.post('/login', async (req, res) => {
|
||||
try {
|
||||
const { username, password } = req.body;
|
||||
|
||||
// Validate input
|
||||
if (!username || !password) {
|
||||
return res.status(400).json({ error: 'Username and password are required' });
|
||||
}
|
||||
|
||||
// Get user from database
|
||||
const user = await userDb.getUserByUsername(username);
|
||||
if (!user) {
|
||||
return res.status(401).json({ error: 'Invalid username or password' });
|
||||
}
|
||||
|
||||
// Verify password
|
||||
const isValidPassword = await bcrypt.compare(password, user.password_hash);
|
||||
if (!isValidPassword) {
|
||||
return res.status(401).json({ error: 'Invalid username or password' });
|
||||
}
|
||||
|
||||
// Generate token
|
||||
const token = generateToken(user);
|
||||
|
||||
// Update last login
|
||||
await userDb.updateLastLogin(user.id);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
user: { id: user.id, username: user.username },
|
||||
token
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Login error:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Get current user (protected route)
|
||||
router.get('/user', authenticateToken, (req, res) => {
|
||||
res.json({
|
||||
user: req.user
|
||||
});
|
||||
});
|
||||
|
||||
// Logout (client-side token removal, but this endpoint can be used for logging)
|
||||
router.post('/logout', authenticateToken, (req, res) => {
|
||||
// In a simple JWT system, logout is mainly client-side
|
||||
// This endpoint exists for consistency and potential future logging
|
||||
res.json({ success: true, message: 'Logged out successfully' });
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
Reference in New Issue
Block a user