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:
simos
2025-07-09 18:25:58 +00:00
parent b27702797f
commit ec9ff3336a
21 changed files with 2670 additions and 91 deletions

View File

@@ -23,6 +23,7 @@ import ClaudeLogo from './ClaudeLogo.jsx';
import ClaudeStatus from './ClaudeStatus';
import { MicButton } from './MicButton.jsx';
import { api } from '../utils/api';
// Memoized message component to prevent unnecessary re-renders
const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFileOpen, onShowSettings, autoExpandTools, showRawParameters }) => {
@@ -949,7 +950,7 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
setIsLoadingSessionMessages(true);
try {
const response = await fetch(`/api/projects/${projectName}/sessions/${sessionId}/messages`);
const response = await api.sessionMessages(projectName, sessionId);
if (!response.ok) {
throw new Error('Failed to load session messages');
}
@@ -1451,7 +1452,7 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
const fetchProjectFiles = async () => {
try {
const response = await fetch(`/api/projects/${selectedProject.name}/files`);
const response = await api.getFiles(selectedProject.name);
if (response.ok) {
const files = await response.json();
// Flatten the file tree to get all file paths

View File

@@ -10,6 +10,7 @@ import { oneDark } from '@codemirror/theme-one-dark';
import { EditorView, Decoration } from '@codemirror/view';
import { StateField, StateEffect, RangeSetBuilder } from '@codemirror/state';
import { X, Save, Download, Maximize2, Minimize2, Eye, EyeOff } from 'lucide-react';
import { api } from '../utils/api';
function CodeEditor({ file, onClose, projectPath }) {
const [content, setContent] = useState('');
@@ -139,7 +140,7 @@ function CodeEditor({ file, onClose, projectPath }) {
try {
setLoading(true);
const response = await fetch(`/api/projects/${file.projectName}/file?filePath=${encodeURIComponent(file.path)}`);
const response = await api.readFile(file.projectName, file.path);
if (!response.ok) {
throw new Error(`Failed to load file: ${response.status} ${response.statusText}`);
@@ -176,16 +177,7 @@ function CodeEditor({ file, onClose, projectPath }) {
const handleSave = async () => {
setSaving(true);
try {
const response = await fetch(`/api/projects/${file.projectName}/file`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
filePath: file.path,
content: content
})
});
const response = await api.saveFile(file.projectName, file.path, content);
if (!response.ok) {
const errorData = await response.json();

View File

@@ -5,6 +5,7 @@ import { Folder, FolderOpen, File, FileText, FileCode } from 'lucide-react';
import { cn } from '../lib/utils';
import CodeEditor from './CodeEditor';
import ImageViewer from './ImageViewer';
import { api } from '../utils/api';
function FileTree({ selectedProject }) {
const [files, setFiles] = useState([]);
@@ -22,7 +23,7 @@ function FileTree({ selectedProject }) {
const fetchFiles = async () => {
setLoading(true);
try {
const response = await fetch(`/api/projects/${selectedProject.name}/files`);
const response = await api.getFiles(selectedProject.name);
if (!response.ok) {
const errorText = await response.text();

View File

@@ -1,6 +1,7 @@
import React, { useState, useEffect, useRef } from 'react';
import { GitBranch, GitCommit, Plus, Minus, RefreshCw, Check, X, ChevronDown, ChevronRight, Info, History, FileText, Mic, MicOff, Sparkles } from 'lucide-react';
import { MicButton } from './MicButton.jsx';
import { authenticatedFetch } from '../utils/api';
function GitPanel({ selectedProject, isMobile }) {
const [gitStatus, setGitStatus] = useState(null);
@@ -55,7 +56,7 @@ function GitPanel({ selectedProject, isMobile }) {
setIsLoading(true);
try {
const response = await fetch(`/api/git/status?project=${encodeURIComponent(selectedProject.name)}`);
const response = await authenticatedFetch(`/api/git/status?project=${encodeURIComponent(selectedProject.name)}`);
const data = await response.json();
console.log('Git status response:', data);
@@ -93,7 +94,7 @@ function GitPanel({ selectedProject, isMobile }) {
const fetchBranches = async () => {
try {
const response = await fetch(`/api/git/branches?project=${encodeURIComponent(selectedProject.name)}`);
const response = await authenticatedFetch(`/api/git/branches?project=${encodeURIComponent(selectedProject.name)}`);
const data = await response.json();
if (!data.error && data.branches) {
@@ -106,7 +107,7 @@ function GitPanel({ selectedProject, isMobile }) {
const switchBranch = async (branchName) => {
try {
const response = await fetch('/api/git/checkout', {
const response = await authenticatedFetch('/api/git/checkout', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
@@ -133,7 +134,7 @@ function GitPanel({ selectedProject, isMobile }) {
setIsCreatingBranch(true);
try {
const response = await fetch('/api/git/create-branch', {
const response = await authenticatedFetch('/api/git/create-branch', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
@@ -162,7 +163,7 @@ function GitPanel({ selectedProject, isMobile }) {
const fetchFileDiff = async (filePath) => {
try {
const response = await fetch(`/api/git/diff?project=${encodeURIComponent(selectedProject.name)}&file=${encodeURIComponent(filePath)}`);
const response = await authenticatedFetch(`/api/git/diff?project=${encodeURIComponent(selectedProject.name)}&file=${encodeURIComponent(filePath)}`);
const data = await response.json();
if (!data.error && data.diff) {
@@ -178,7 +179,7 @@ function GitPanel({ selectedProject, isMobile }) {
const fetchRecentCommits = async () => {
try {
const response = await fetch(`/api/git/commits?project=${encodeURIComponent(selectedProject.name)}&limit=10`);
const response = await authenticatedFetch(`/api/git/commits?project=${encodeURIComponent(selectedProject.name)}&limit=10`);
const data = await response.json();
if (!data.error && data.commits) {
@@ -191,7 +192,7 @@ function GitPanel({ selectedProject, isMobile }) {
const fetchCommitDiff = async (commitHash) => {
try {
const response = await fetch(`/api/git/commit-diff?project=${encodeURIComponent(selectedProject.name)}&commit=${commitHash}`);
const response = await authenticatedFetch(`/api/git/commit-diff?project=${encodeURIComponent(selectedProject.name)}&commit=${commitHash}`);
const data = await response.json();
if (!data.error && data.diff) {
@@ -208,7 +209,7 @@ function GitPanel({ selectedProject, isMobile }) {
const generateCommitMessage = async () => {
setIsGeneratingMessage(true);
try {
const response = await fetch('/api/git/generate-commit-message', {
const response = await authenticatedFetch('/api/git/generate-commit-message', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
@@ -275,7 +276,7 @@ function GitPanel({ selectedProject, isMobile }) {
setIsCommitting(true);
try {
const response = await fetch('/api/git/commit', {
const response = await authenticatedFetch('/api/git/commit', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({

View File

@@ -0,0 +1,108 @@
import React, { useState } from 'react';
import { useAuth } from '../contexts/AuthContext';
import ClaudeLogo from './ClaudeLogo';
const LoginForm = () => {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState('');
const { login } = useAuth();
const handleSubmit = async (e) => {
e.preventDefault();
setError('');
if (!username || !password) {
setError('Please enter both username and password');
return;
}
setIsLoading(true);
const result = await login(username, password);
if (!result.success) {
setError(result.error);
}
setIsLoading(false);
};
return (
<div className="min-h-screen bg-background flex items-center justify-center p-4">
<div className="w-full max-w-md">
<div className="bg-card rounded-lg shadow-lg border border-border p-8 space-y-6">
{/* Logo and Title */}
<div className="text-center">
<div className="flex justify-center mb-4">
<ClaudeLogo size={64} />
</div>
<h1 className="text-2xl font-bold text-foreground">Welcome Back</h1>
<p className="text-muted-foreground mt-2">
Sign in to your Claude Code UI account
</p>
</div>
{/* Login Form */}
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label htmlFor="username" className="block text-sm font-medium text-foreground mb-1">
Username
</label>
<input
type="text"
id="username"
value={username}
onChange={(e) => setUsername(e.target.value)}
className="w-full px-3 py-2 border border-border rounded-md bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="Enter your username"
required
disabled={isLoading}
/>
</div>
<div>
<label htmlFor="password" className="block text-sm font-medium text-foreground mb-1">
Password
</label>
<input
type="password"
id="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="w-full px-3 py-2 border border-border rounded-md bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="Enter your password"
required
disabled={isLoading}
/>
</div>
{error && (
<div className="p-3 bg-red-100 dark:bg-red-900/20 border border-red-300 dark:border-red-800 rounded-md">
<p className="text-sm text-red-700 dark:text-red-400">{error}</p>
</div>
)}
<button
type="submit"
disabled={isLoading}
className="w-full bg-blue-600 hover:bg-blue-700 disabled:bg-blue-400 text-white font-medium py-2 px-4 rounded-md transition-colors duration-200"
>
{isLoading ? 'Signing in...' : 'Sign In'}
</button>
</form>
<div className="text-center">
<p className="text-sm text-muted-foreground">
Enter your credentials to access Claude Code UI
</p>
</div>
</div>
</div>
</div>
);
};
export default LoginForm;

View File

@@ -0,0 +1,42 @@
import React from 'react';
import { useAuth } from '../contexts/AuthContext';
import SetupForm from './SetupForm';
import LoginForm from './LoginForm';
import ClaudeLogo from './ClaudeLogo';
const LoadingScreen = () => (
<div className="min-h-screen bg-background flex items-center justify-center p-4">
<div className="text-center">
<div className="flex justify-center mb-4">
<ClaudeLogo size={64} />
</div>
<h1 className="text-2xl font-bold text-foreground mb-2">Claude Code UI</h1>
<div className="flex items-center justify-center space-x-2">
<div className="w-2 h-2 bg-blue-500 rounded-full animate-bounce"></div>
<div className="w-2 h-2 bg-blue-500 rounded-full animate-bounce" style={{ animationDelay: '0.1s' }}></div>
<div className="w-2 h-2 bg-blue-500 rounded-full animate-bounce" style={{ animationDelay: '0.2s' }}></div>
</div>
<p className="text-muted-foreground mt-2">Loading...</p>
</div>
</div>
);
const ProtectedRoute = ({ children }) => {
const { user, isLoading, needsSetup } = useAuth();
if (isLoading) {
return <LoadingScreen />;
}
if (needsSetup) {
return <SetupForm />;
}
if (!user) {
return <LoginForm />;
}
return children;
};
export default ProtectedRoute;

View File

@@ -0,0 +1,135 @@
import React, { useState } from 'react';
import { useAuth } from '../contexts/AuthContext';
import ClaudeLogo from './ClaudeLogo';
const SetupForm = () => {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState('');
const { register } = useAuth();
const handleSubmit = async (e) => {
e.preventDefault();
setError('');
if (password !== confirmPassword) {
setError('Passwords do not match');
return;
}
if (username.length < 3) {
setError('Username must be at least 3 characters long');
return;
}
if (password.length < 6) {
setError('Password must be at least 6 characters long');
return;
}
setIsLoading(true);
const result = await register(username, password);
if (!result.success) {
setError(result.error);
}
setIsLoading(false);
};
return (
<div className="min-h-screen bg-background flex items-center justify-center p-4">
<div className="w-full max-w-md">
<div className="bg-card rounded-lg shadow-lg border border-border p-8 space-y-6">
{/* Logo and Title */}
<div className="text-center">
<div className="flex justify-center mb-4">
<ClaudeLogo size={64} />
</div>
<h1 className="text-2xl font-bold text-foreground">Welcome to Claude Code UI</h1>
<p className="text-muted-foreground mt-2">
Set up your account to get started
</p>
</div>
{/* Setup Form */}
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label htmlFor="username" className="block text-sm font-medium text-foreground mb-1">
Username
</label>
<input
type="text"
id="username"
value={username}
onChange={(e) => setUsername(e.target.value)}
className="w-full px-3 py-2 border border-border rounded-md bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="Enter your username"
required
disabled={isLoading}
/>
</div>
<div>
<label htmlFor="password" className="block text-sm font-medium text-foreground mb-1">
Password
</label>
<input
type="password"
id="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="w-full px-3 py-2 border border-border rounded-md bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="Enter your password"
required
disabled={isLoading}
/>
</div>
<div>
<label htmlFor="confirmPassword" className="block text-sm font-medium text-foreground mb-1">
Confirm Password
</label>
<input
type="password"
id="confirmPassword"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
className="w-full px-3 py-2 border border-border rounded-md bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="Confirm your password"
required
disabled={isLoading}
/>
</div>
{error && (
<div className="p-3 bg-red-100 dark:bg-red-900/20 border border-red-300 dark:border-red-800 rounded-md">
<p className="text-sm text-red-700 dark:text-red-400">{error}</p>
</div>
)}
<button
type="submit"
disabled={isLoading}
className="w-full bg-blue-600 hover:bg-blue-700 disabled:bg-blue-400 text-white font-medium py-2 px-4 rounded-md transition-colors duration-200"
>
{isLoading ? 'Setting up...' : 'Create Account'}
</button>
</form>
<div className="text-center">
<p className="text-sm text-muted-foreground">
This is a single-user system. Only one account can be created.
</p>
</div>
</div>
</div>
</div>
);
};
export default SetupForm;

View File

@@ -322,10 +322,21 @@ function Shell({ selectedProject, selectedSession, isActive }) {
if (isConnecting || isConnected) return;
try {
// Get authentication token
const token = localStorage.getItem('auth-token');
if (!token) {
console.error('No authentication token found for Shell WebSocket connection');
return;
}
// Fetch server configuration to get the correct WebSocket URL
let wsBaseUrl;
try {
const configResponse = await fetch('/api/config');
const configResponse = await fetch('/api/config', {
headers: {
'Authorization': `Bearer ${token}`
}
});
const config = await configResponse.json();
wsBaseUrl = config.wsUrl;
@@ -343,7 +354,8 @@ function Shell({ selectedProject, selectedSession, isActive }) {
wsBaseUrl = `${protocol}//${window.location.hostname}:${apiPort}`;
}
const wsUrl = `${wsBaseUrl}/shell`;
// Include token in WebSocket URL as query parameter
const wsUrl = `${wsBaseUrl}/shell?token=${encodeURIComponent(token)}`;
ws.current = new WebSocket(wsUrl);

View File

@@ -6,6 +6,7 @@ import { Input } from './ui/input';
import { FolderOpen, Folder, Plus, MessageSquare, Clock, ChevronDown, ChevronRight, Edit3, Check, X, Trash2, Settings, FolderPlus, RefreshCw, Sparkles, Edit2 } from 'lucide-react';
import { cn } from '../lib/utils';
import ClaudeLogo from './ClaudeLogo';
import { api } from '../utils/api';
// Move formatTimeAgo outside component to avoid recreation on every render
const formatTimeAgo = (dateString, currentTime) => {
@@ -132,13 +133,7 @@ function Sidebar({
const saveProjectName = async (projectName) => {
try {
const response = await fetch(`/api/projects/${projectName}/rename`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ displayName: editingName }),
});
const response = await api.renameProject(projectName, editingName);
if (response.ok) {
// Refresh projects to get updated data
@@ -164,9 +159,7 @@ function Sidebar({
}
try {
const response = await fetch(`/api/projects/${projectName}/sessions/${sessionId}`, {
method: 'DELETE',
});
const response = await api.deleteSession(projectName, sessionId);
if (response.ok) {
// Call parent callback if provided
@@ -189,9 +182,7 @@ function Sidebar({
}
try {
const response = await fetch(`/api/projects/${projectName}`, {
method: 'DELETE',
});
const response = await api.deleteProject(projectName);
if (response.ok) {
// Call parent callback if provided
@@ -218,15 +209,7 @@ function Sidebar({
setCreatingProject(true);
try {
const response = await fetch('/api/projects/create', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
path: newProjectPath.trim()
}),
});
const response = await api.createProject(newProjectPath.trim());
if (response.ok) {
const result = await response.json();