Skip to content

Commit d8bc634

Browse files
authored
Merge pull request #29 from siteboon/login
Login
2 parents b277027 + ac32026 commit d8bc634

21 files changed

+2677
-98
lines changed

package-lock.json

Lines changed: 1686 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "claude-code-ui",
3-
"version": "1.1.3",
3+
"version": "1.1.4",
44
"description": "A web-based UI for Claude Code CLI",
55
"main": "server/index.js",
66
"scripts": {
@@ -33,11 +33,13 @@
3333
"@uiw/react-codemirror": "^4.23.13",
3434
"@xterm/addon-clipboard": "^0.1.0",
3535
"@xterm/addon-webgl": "^0.18.0",
36+
"bcrypt": "^6.0.0",
3637
"chokidar": "^4.0.3",
3738
"class-variance-authority": "^0.7.1",
3839
"clsx": "^2.1.1",
3940
"cors": "^2.8.5",
4041
"express": "^4.18.2",
42+
"jsonwebtoken": "^9.0.2",
4143
"lucide-react": "^0.515.0",
4244
"mime-types": "^3.0.1",
4345
"node-fetch": "^2.7.0",
@@ -46,6 +48,7 @@
4648
"react-dom": "^18.2.0",
4749
"react-markdown": "^10.1.0",
4850
"react-router-dom": "^6.8.1",
51+
"sqlite3": "^5.1.7",
4952
"tailwind-merge": "^3.3.1",
5053
"ws": "^8.14.2",
5154
"xterm": "^5.3.0",

server/database/db.js

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
const sqlite3 = require('sqlite3').verbose();
2+
const path = require('path');
3+
const fs = require('fs');
4+
5+
const DB_PATH = path.join(__dirname, 'auth.db');
6+
const INIT_SQL_PATH = path.join(__dirname, 'init.sql');
7+
8+
// Create database connection
9+
const db = new sqlite3.Database(DB_PATH, (err) => {
10+
if (err) {
11+
console.error('Error opening database:', err.message);
12+
} else {
13+
console.log('Connected to SQLite database');
14+
}
15+
});
16+
17+
// Initialize database with schema
18+
const initializeDatabase = async () => {
19+
return new Promise((resolve, reject) => {
20+
try {
21+
const initSQL = fs.readFileSync(INIT_SQL_PATH, 'utf8');
22+
db.exec(initSQL, (err) => {
23+
if (err) {
24+
console.error('Error initializing database:', err.message);
25+
reject(err);
26+
} else {
27+
console.log('Database initialized successfully');
28+
resolve();
29+
}
30+
});
31+
} catch (error) {
32+
console.error('Error reading init SQL file:', error);
33+
reject(error);
34+
}
35+
});
36+
};
37+
38+
// User database operations
39+
const userDb = {
40+
// Check if any users exist
41+
hasUsers: () => {
42+
return new Promise((resolve, reject) => {
43+
db.get('SELECT COUNT(*) as count FROM users', (err, row) => {
44+
if (err) reject(err);
45+
else resolve(row.count > 0);
46+
});
47+
});
48+
},
49+
50+
// Create a new user
51+
createUser: (username, passwordHash) => {
52+
return new Promise((resolve, reject) => {
53+
const stmt = db.prepare('INSERT INTO users (username, password_hash) VALUES (?, ?)');
54+
stmt.run(username, passwordHash, function(err) {
55+
if (err) {
56+
reject(err);
57+
} else {
58+
resolve({ id: this.lastID, username });
59+
}
60+
});
61+
stmt.finalize();
62+
});
63+
},
64+
65+
// Get user by username
66+
getUserByUsername: (username) => {
67+
return new Promise((resolve, reject) => {
68+
db.get('SELECT * FROM users WHERE username = ? AND is_active = 1', [username], (err, row) => {
69+
if (err) reject(err);
70+
else resolve(row);
71+
});
72+
});
73+
},
74+
75+
// Update last login time
76+
updateLastLogin: (userId) => {
77+
return new Promise((resolve, reject) => {
78+
db.run('UPDATE users SET last_login = CURRENT_TIMESTAMP WHERE id = ?', [userId], (err) => {
79+
if (err) reject(err);
80+
else resolve();
81+
});
82+
});
83+
},
84+
85+
// Get user by ID
86+
getUserById: (userId) => {
87+
return new Promise((resolve, reject) => {
88+
db.get('SELECT id, username, created_at, last_login FROM users WHERE id = ? AND is_active = 1', [userId], (err, row) => {
89+
if (err) reject(err);
90+
else resolve(row);
91+
});
92+
});
93+
}
94+
};
95+
96+
module.exports = {
97+
db,
98+
initializeDatabase,
99+
userDb
100+
};

server/database/init.sql

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
-- Initialize authentication database
2+
PRAGMA foreign_keys = ON;
3+
4+
-- Users table (single user system)
5+
CREATE TABLE IF NOT EXISTS users (
6+
id INTEGER PRIMARY KEY AUTOINCREMENT,
7+
username TEXT UNIQUE NOT NULL,
8+
password_hash TEXT NOT NULL,
9+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
10+
last_login DATETIME,
11+
is_active BOOLEAN DEFAULT 1
12+
);
13+
14+
-- Indexes for performance
15+
CREATE INDEX IF NOT EXISTS idx_users_username ON users(username);
16+
CREATE INDEX IF NOT EXISTS idx_users_active ON users(is_active);

server/index.js

Lines changed: 73 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ const fetch = require('node-fetch');
3333
const { getProjects, getSessions, getSessionMessages, renameProject, deleteSession, deleteProject, addProjectManually, extractProjectDirectory, clearProjectDirectoryCache } = require('./projects');
3434
const { spawnClaude, abortClaudeSession } = require('./claude-cli');
3535
const gitRoutes = require('./routes/git');
36+
const authRoutes = require('./routes/auth');
37+
const { initializeDatabase } = require('./database/db');
38+
const { validateApiKey, authenticateToken, authenticateWebSocket } = require('./middleware/auth');
3639

3740
// File system watcher for projects folder
3841
let projectsWatcher = null;
@@ -142,19 +145,43 @@ const wss = new WebSocketServer({
142145
server,
143146
verifyClient: (info) => {
144147
console.log('WebSocket connection attempt to:', info.req.url);
145-
return true; // Accept all connections for now
148+
149+
// Extract token from query parameters or headers
150+
const url = new URL(info.req.url, 'http://localhost');
151+
const token = url.searchParams.get('token') ||
152+
info.req.headers.authorization?.split(' ')[1];
153+
154+
// Verify token
155+
const user = authenticateWebSocket(token);
156+
if (!user) {
157+
console.log('❌ WebSocket authentication failed');
158+
return false;
159+
}
160+
161+
// Store user info in the request for later use
162+
info.req.user = user;
163+
console.log('✅ WebSocket authenticated for user:', user.username);
164+
return true;
146165
}
147166
});
148167

149168
app.use(cors());
150169
app.use(express.json());
151-
app.use(express.static(path.join(__dirname, '../dist')));
152170

153-
// Git API Routes
154-
app.use('/api/git', gitRoutes);
171+
// Optional API key validation (if configured)
172+
app.use('/api', validateApiKey);
155173

156-
// API Routes
157-
app.get('/api/config', (req, res) => {
174+
// Authentication routes (public)
175+
app.use('/api/auth', authRoutes);
176+
177+
// Git API Routes (protected)
178+
app.use('/api/git', authenticateToken, gitRoutes);
179+
180+
// Static files served after API routes
181+
app.use(express.static(path.join(__dirname, '../dist')));
182+
183+
// API Routes (protected)
184+
app.get('/api/config', authenticateToken, (req, res) => {
158185
// Always use the server's actual IP and port for WebSocket connections
159186
const serverIP = getServerIP();
160187
const host = `${serverIP}:${PORT}`;
@@ -168,7 +195,7 @@ app.get('/api/config', (req, res) => {
168195
});
169196
});
170197

171-
app.get('/api/projects', async (req, res) => {
198+
app.get('/api/projects', authenticateToken, async (req, res) => {
172199
try {
173200
const projects = await getProjects();
174201
res.json(projects);
@@ -177,7 +204,7 @@ app.get('/api/projects', async (req, res) => {
177204
}
178205
});
179206

180-
app.get('/api/projects/:projectName/sessions', async (req, res) => {
207+
app.get('/api/projects/:projectName/sessions', authenticateToken, async (req, res) => {
181208
try {
182209
const { limit = 5, offset = 0 } = req.query;
183210
const result = await getSessions(req.params.projectName, parseInt(limit), parseInt(offset));
@@ -188,7 +215,7 @@ app.get('/api/projects/:projectName/sessions', async (req, res) => {
188215
});
189216

190217
// Get messages for a specific session
191-
app.get('/api/projects/:projectName/sessions/:sessionId/messages', async (req, res) => {
218+
app.get('/api/projects/:projectName/sessions/:sessionId/messages', authenticateToken, async (req, res) => {
192219
try {
193220
const { projectName, sessionId } = req.params;
194221
const messages = await getSessionMessages(projectName, sessionId);
@@ -199,7 +226,7 @@ app.get('/api/projects/:projectName/sessions/:sessionId/messages', async (req, r
199226
});
200227

201228
// Rename project endpoint
202-
app.put('/api/projects/:projectName/rename', async (req, res) => {
229+
app.put('/api/projects/:projectName/rename', authenticateToken, async (req, res) => {
203230
try {
204231
const { displayName } = req.body;
205232
await renameProject(req.params.projectName, displayName);
@@ -210,7 +237,7 @@ app.put('/api/projects/:projectName/rename', async (req, res) => {
210237
});
211238

212239
// Delete session endpoint
213-
app.delete('/api/projects/:projectName/sessions/:sessionId', async (req, res) => {
240+
app.delete('/api/projects/:projectName/sessions/:sessionId', authenticateToken, async (req, res) => {
214241
try {
215242
const { projectName, sessionId } = req.params;
216243
await deleteSession(projectName, sessionId);
@@ -221,7 +248,7 @@ app.delete('/api/projects/:projectName/sessions/:sessionId', async (req, res) =>
221248
});
222249

223250
// Delete project endpoint (only if empty)
224-
app.delete('/api/projects/:projectName', async (req, res) => {
251+
app.delete('/api/projects/:projectName', authenticateToken, async (req, res) => {
225252
try {
226253
const { projectName } = req.params;
227254
await deleteProject(projectName);
@@ -232,7 +259,7 @@ app.delete('/api/projects/:projectName', async (req, res) => {
232259
});
233260

234261
// Create project endpoint
235-
app.post('/api/projects/create', async (req, res) => {
262+
app.post('/api/projects/create', authenticateToken, async (req, res) => {
236263
try {
237264
const { path: projectPath } = req.body;
238265

@@ -249,7 +276,7 @@ app.post('/api/projects/create', async (req, res) => {
249276
});
250277

251278
// Read file content endpoint
252-
app.get('/api/projects/:projectName/file', async (req, res) => {
279+
app.get('/api/projects/:projectName/file', authenticateToken, async (req, res) => {
253280
try {
254281
const { projectName } = req.params;
255282
const { filePath } = req.query;
@@ -278,7 +305,7 @@ app.get('/api/projects/:projectName/file', async (req, res) => {
278305
});
279306

280307
// Serve binary file content endpoint (for images, etc.)
281-
app.get('/api/projects/:projectName/files/content', async (req, res) => {
308+
app.get('/api/projects/:projectName/files/content', authenticateToken, async (req, res) => {
282309
try {
283310
const { projectName } = req.params;
284311
const { path: filePath } = req.query;
@@ -324,7 +351,7 @@ app.get('/api/projects/:projectName/files/content', async (req, res) => {
324351
});
325352

326353
// Save file content endpoint
327-
app.put('/api/projects/:projectName/file', async (req, res) => {
354+
app.put('/api/projects/:projectName/file', authenticateToken, async (req, res) => {
328355
try {
329356
const { projectName } = req.params;
330357
const { filePath, content } = req.body;
@@ -371,7 +398,7 @@ app.put('/api/projects/:projectName/file', async (req, res) => {
371398
}
372399
});
373400

374-
app.get('/api/projects/:projectName/files', async (req, res) => {
401+
app.get('/api/projects/:projectName/files', authenticateToken, async (req, res) => {
375402
try {
376403

377404
const fs = require('fs').promises;
@@ -409,12 +436,16 @@ wss.on('connection', (ws, request) => {
409436
const url = request.url;
410437
console.log('🔗 Client connected to:', url);
411438

412-
if (url === '/shell') {
439+
// Parse URL to get pathname without query parameters
440+
const urlObj = new URL(url, 'http://localhost');
441+
const pathname = urlObj.pathname;
442+
443+
if (pathname === '/shell') {
413444
handleShellConnection(ws);
414-
} else if (url === '/ws') {
445+
} else if (pathname === '/ws') {
415446
handleChatConnection(ws);
416447
} else {
417-
console.log('❌ Unknown WebSocket path:', url);
448+
console.log('❌ Unknown WebSocket path:', pathname);
418449
ws.close();
419450
}
420451
});
@@ -629,7 +660,7 @@ function handleShellConnection(ws) {
629660
});
630661
}
631662
// Audio transcription endpoint
632-
app.post('/api/transcribe', async (req, res) => {
663+
app.post('/api/transcribe', authenticateToken, async (req, res) => {
633664
try {
634665
const multer = require('multer');
635666
const upload = multer({ storage: multer.memoryStorage() });
@@ -835,9 +866,24 @@ async function getFileTree(dirPath, maxDepth = 3, currentDepth = 0, showHidden =
835866
}
836867

837868
const PORT = process.env.PORT || 3000;
838-
server.listen(PORT, '0.0.0.0', () => {
839-
console.log(`Claude Code UI server running on http://0.0.0.0:${PORT}`);
840-
841-
// Start watching the projects folder for changes
842-
setupProjectsWatcher();
843-
});
869+
870+
// Initialize database and start server
871+
async function startServer() {
872+
try {
873+
// Initialize authentication database
874+
await initializeDatabase();
875+
console.log('✅ Database initialized successfully');
876+
877+
server.listen(PORT, '0.0.0.0', () => {
878+
console.log(`Claude Code UI server running on http://0.0.0.0:${PORT}`);
879+
880+
// Start watching the projects folder for changes
881+
setupProjectsWatcher();
882+
});
883+
} catch (error) {
884+
console.error('❌ Failed to start server:', error);
885+
process.exit(1);
886+
}
887+
}
888+
889+
startServer();

0 commit comments

Comments
 (0)