Squashed commit of the following:
commit 70cb0b9de2bdbb6b564a7e6fb3a926a104e1e17c Author: Louis Knight-Webb <louis@bloop.ai> Date: Tue Jun 17 14:16:45 2025 -0400 Update API commit 36a5161b96b8f034daa91d08d648be77fbdcb30b Author: Louis Knight-Webb <louis@bloop.ai> Date: Tue Jun 17 14:14:33 2025 -0400 Further auth removal commit cba24ffd462a3de178658f26231011ed4d28a78b Author: Louis Knight-Webb <louis@bloop.ai> Date: Tue Jun 17 14:03:13 2025 -0400 Fully remove users commit cfb1aec9b984c3374e5cc0ffe182de2647caf85d Author: Louis Knight-Webb <louis@bloop.ai> Date: Tue Jun 17 11:51:20 2025 -0400 Start removing users
This commit is contained in:
@@ -1,43 +1,19 @@
|
||||
import { BrowserRouter, Routes, Route, useLocation } from 'react-router-dom'
|
||||
import { LoginForm } from '@/components/auth/login-form'
|
||||
import { Navbar } from '@/components/layout/navbar'
|
||||
import { HomePage } from '@/pages/home'
|
||||
import { Projects } from '@/pages/projects'
|
||||
import { ProjectTasks } from '@/pages/project-tasks'
|
||||
import { TaskDetailsPage } from '@/pages/task-details'
|
||||
import { TaskAttemptComparePage } from '@/pages/task-attempt-compare'
|
||||
import { Users } from '@/pages/users'
|
||||
import { AuthProvider, useAuth } from '@/contexts/auth-context'
|
||||
|
||||
|
||||
function AppContent() {
|
||||
const location = useLocation()
|
||||
const { isAuthenticated, isLoading, logout } = useAuth()
|
||||
const showNavbar = location.pathname !== '/' || isAuthenticated
|
||||
|
||||
const handleLogin = () => {
|
||||
// The actual login logic is handled by the LoginForm component
|
||||
// which will call the login method from useAuth()
|
||||
}
|
||||
|
||||
// Show loading while checking auth status
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="min-h-screen bg-background flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900 mx-auto mb-4"></div>
|
||||
<p className="text-muted-foreground">Checking authentication...</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return <LoginForm onSuccess={handleLogin} />
|
||||
}
|
||||
const showNavbar = true
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
{showNavbar && <Navbar onLogout={logout} />}
|
||||
{showNavbar && <Navbar />}
|
||||
<div className={showNavbar && location.pathname !== '/' ? "max-w-7xl mx-auto p-6 sm:p-8" : ""}>
|
||||
<Routes>
|
||||
<Route path="/" element={<HomePage />} />
|
||||
@@ -46,7 +22,7 @@ function AppContent() {
|
||||
<Route path="/projects/:projectId/tasks" element={<ProjectTasks />} />
|
||||
<Route path="/projects/:projectId/tasks/:taskId" element={<TaskDetailsPage />} />
|
||||
<Route path="/projects/:projectId/tasks/:taskId/attempts/:attemptId/compare" element={<TaskAttemptComparePage />} />
|
||||
<Route path="/users" element={<Users />} />
|
||||
|
||||
</Routes>
|
||||
</div>
|
||||
</div>
|
||||
@@ -56,9 +32,7 @@ function AppContent() {
|
||||
function App() {
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<AuthProvider>
|
||||
<AppContent />
|
||||
</AuthProvider>
|
||||
<AppContent />
|
||||
</BrowserRouter>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,118 +0,0 @@
|
||||
import { useState } from 'react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { LoginRequest, LoginResponse, ApiResponse } from 'shared/types'
|
||||
import { useAuth } from '@/contexts/auth-context'
|
||||
import { LogIn, AlertCircle } from 'lucide-react'
|
||||
|
||||
interface LoginFormProps {
|
||||
onSuccess?: () => void
|
||||
}
|
||||
|
||||
export function LoginForm({ onSuccess }: LoginFormProps) {
|
||||
const { login } = useAuth()
|
||||
const [email, setEmail] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
setError('')
|
||||
setLoading(true)
|
||||
|
||||
try {
|
||||
const loginData: LoginRequest = { email, password }
|
||||
const response = await fetch('/api/auth/login', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(loginData),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 401) {
|
||||
throw new Error('Invalid email or password')
|
||||
}
|
||||
throw new Error('Login failed')
|
||||
}
|
||||
|
||||
const data: ApiResponse<LoginResponse> = await response.json()
|
||||
|
||||
if (data.success && data.data) {
|
||||
login(data.data.user, data.data.token)
|
||||
onSuccess?.()
|
||||
} else {
|
||||
throw new Error('Login failed')
|
||||
}
|
||||
} catch (error) {
|
||||
setError(error instanceof Error ? error.message : 'An error occurred')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-background to-muted/20">
|
||||
<Card className="w-full max-w-md">
|
||||
<CardHeader className="text-center">
|
||||
<div className="mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-lg bg-primary/10">
|
||||
<LogIn className="h-6 w-6 text-primary" />
|
||||
</div>
|
||||
<CardTitle className="text-2xl">Welcome back</CardTitle>
|
||||
<CardDescription>
|
||||
Sign in to your account to continue
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder="Enter your email"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="password">Password</Label>
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder="Enter your password"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<Alert variant="destructive">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
{error}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Button type="submit" className="w-full" disabled={loading}>
|
||||
{loading ? 'Signing in...' : 'Sign in'}
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
<div className="mt-6 text-center text-sm text-muted-foreground">
|
||||
<p>Default admin credentials:</p>
|
||||
<p>Email: admin@example.com</p>
|
||||
<p>Password: Check your ADMIN_PASSWORD env var</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,21 +1,11 @@
|
||||
import { Link, useLocation } from 'react-router-dom'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { authStorage } from '@/lib/auth'
|
||||
import { ArrowLeft, FolderOpen, Users, LogOut } from 'lucide-react'
|
||||
import { ArrowLeft, FolderOpen, Users } from 'lucide-react'
|
||||
|
||||
interface NavbarProps {
|
||||
onLogout: () => void
|
||||
}
|
||||
|
||||
export function Navbar({ onLogout }: NavbarProps) {
|
||||
export function Navbar() {
|
||||
const location = useLocation()
|
||||
const currentUser = authStorage.getUser()
|
||||
const isHome = location.pathname === '/'
|
||||
|
||||
const handleLogout = () => {
|
||||
onLogout()
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="border-b">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
@@ -33,24 +23,19 @@ export function Navbar({ onLogout }: NavbarProps) {
|
||||
Projects
|
||||
</Link>
|
||||
</Button>
|
||||
{currentUser?.is_admin && (
|
||||
<Button
|
||||
asChild
|
||||
variant={location.pathname === '/users' ? 'default' : 'ghost'}
|
||||
size="sm"
|
||||
>
|
||||
<Link to="/users">
|
||||
<Users className="mr-2 h-4 w-4" />
|
||||
Users
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
asChild
|
||||
variant={location.pathname === '/users' ? 'default' : 'ghost'}
|
||||
size="sm"
|
||||
>
|
||||
<Link to="/users">
|
||||
<Users className="mr-2 h-4 w-4" />
|
||||
Users
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="text-sm text-muted-foreground">
|
||||
Welcome, {currentUser?.email}
|
||||
</div>
|
||||
{!isHome && (
|
||||
<Button asChild variant="ghost">
|
||||
<Link to="/">
|
||||
@@ -59,10 +44,6 @@ export function Navbar({ onLogout }: NavbarProps) {
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
<Button variant="ghost" onClick={handleLogout}>
|
||||
<LogOut className="mr-2 h-4 w-4" />
|
||||
Logout
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -6,8 +6,8 @@ import { Badge } from '@/components/ui/badge'
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert'
|
||||
import { Project, ApiResponse } from 'shared/types'
|
||||
import { ProjectForm } from './project-form'
|
||||
import { makeAuthenticatedRequest } from '@/lib/auth'
|
||||
import { ArrowLeft, Edit, Trash2, Calendar, Clock, User, AlertCircle, Loader2, CheckSquare } from 'lucide-react'
|
||||
import { makeRequest } from '@/lib/api'
|
||||
import { ArrowLeft, Edit, Trash2, Calendar, Clock, AlertCircle, Loader2, CheckSquare } from 'lucide-react'
|
||||
|
||||
interface ProjectDetailProps {
|
||||
projectId: string
|
||||
@@ -25,7 +25,7 @@ export function ProjectDetail({ projectId, onBack }: ProjectDetailProps) {
|
||||
setLoading(true)
|
||||
setError('')
|
||||
try {
|
||||
const response = await makeAuthenticatedRequest(`/api/projects/${projectId}`)
|
||||
const response = await makeRequest(`/api/projects/${projectId}`)
|
||||
const data: ApiResponse<Project> = await response.json()
|
||||
if (data.success && data.data) {
|
||||
setProject(data.data)
|
||||
@@ -45,7 +45,7 @@ export function ProjectDetail({ projectId, onBack }: ProjectDetailProps) {
|
||||
if (!confirm(`Are you sure you want to delete "${project.name}"? This action cannot be undone.`)) return
|
||||
|
||||
try {
|
||||
const response = await makeAuthenticatedRequest(`/api/projects/${projectId}`, {
|
||||
const response = await makeRequest(`/api/projects/${projectId}`, {
|
||||
method: 'DELETE',
|
||||
})
|
||||
if (response.ok) {
|
||||
@@ -166,13 +166,7 @@ export function ProjectDetail({ projectId, onBack }: ProjectDetailProps) {
|
||||
<span className="text-muted-foreground">Last Updated:</span>
|
||||
<span className="ml-2">{new Date(project.updated_at).toLocaleDateString()}</span>
|
||||
</div>
|
||||
<div className="flex items-center text-sm">
|
||||
<User className="mr-2 h-4 w-4 text-muted-foreground" />
|
||||
<span className="text-muted-foreground">Owner ID:</span>
|
||||
<code className="ml-2 text-xs bg-muted px-1 py-0.5 rounded">
|
||||
{project.owner_id.substring(0, 8)}...
|
||||
</code>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
import { FolderPicker } from "@/components/ui/folder-picker";
|
||||
import { Project, CreateProject, UpdateProject } from "shared/types";
|
||||
import { AlertCircle, Folder } from "lucide-react";
|
||||
import { makeAuthenticatedRequest } from "@/lib/auth";
|
||||
import { makeRequest } from "@/lib/api";
|
||||
|
||||
interface ProjectFormProps {
|
||||
open: boolean;
|
||||
@@ -76,7 +76,7 @@ export function ProjectForm({
|
||||
name,
|
||||
git_repo_path: finalGitRepoPath,
|
||||
};
|
||||
const response = await makeAuthenticatedRequest(
|
||||
const response = await makeRequest(
|
||||
`/api/projects/${project.id}`,
|
||||
{
|
||||
method: "PUT",
|
||||
@@ -98,7 +98,7 @@ export function ProjectForm({
|
||||
git_repo_path: finalGitRepoPath,
|
||||
use_existing_repo: repoMode === "existing",
|
||||
};
|
||||
const response = await makeAuthenticatedRequest("/api/projects", {
|
||||
const response = await makeRequest("/api/projects", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(createData),
|
||||
});
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Badge } from '@/components/ui/badge'
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert'
|
||||
import { Project, ApiResponse } from 'shared/types'
|
||||
import { ProjectForm } from './project-form'
|
||||
import { makeAuthenticatedRequest } from '@/lib/auth'
|
||||
import { makeRequest } from '@/lib/api'
|
||||
import { Plus, Edit, Trash2, Calendar, AlertCircle, Loader2, MoreHorizontal, ExternalLink } from 'lucide-react'
|
||||
import {
|
||||
DropdownMenu,
|
||||
@@ -27,7 +27,7 @@ export function ProjectList() {
|
||||
setLoading(true)
|
||||
setError('')
|
||||
try {
|
||||
const response = await makeAuthenticatedRequest('/api/projects')
|
||||
const response = await makeRequest('/api/projects')
|
||||
const data: ApiResponse<Project[]> = await response.json()
|
||||
if (data.success && data.data) {
|
||||
setProjects(data.data)
|
||||
@@ -46,7 +46,7 @@ export function ProjectList() {
|
||||
if (!confirm(`Are you sure you want to delete "${name}"? This action cannot be undone.`)) return
|
||||
|
||||
try {
|
||||
const response = await makeAuthenticatedRequest(`/api/projects/${id}`, {
|
||||
const response = await makeRequest(`/api/projects/${id}`, {
|
||||
method: 'DELETE',
|
||||
})
|
||||
if (response.ok) {
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { makeAuthenticatedRequest } from "@/lib/auth";
|
||||
import { makeRequest } from "@/lib/api";
|
||||
import type {
|
||||
TaskStatus,
|
||||
TaskAttempt,
|
||||
@@ -107,7 +107,7 @@ export function TaskDetailsDialog({
|
||||
const fetchTaskAttempts = async (taskId: string) => {
|
||||
try {
|
||||
setTaskAttemptsLoading(true);
|
||||
const response = await makeAuthenticatedRequest(
|
||||
const response = await makeRequest(
|
||||
`/api/projects/${projectId}/tasks/${taskId}/attempts`
|
||||
);
|
||||
|
||||
@@ -141,7 +141,7 @@ export function TaskDetailsDialog({
|
||||
|
||||
try {
|
||||
setActivitiesLoading(true);
|
||||
const response = await makeAuthenticatedRequest(
|
||||
const response = await makeRequest(
|
||||
`/api/projects/${projectId}/tasks/${task.id}/attempts/${attemptId}/activities`
|
||||
);
|
||||
|
||||
@@ -171,7 +171,7 @@ export function TaskDetailsDialog({
|
||||
|
||||
try {
|
||||
setSavingTask(true);
|
||||
const response = await makeAuthenticatedRequest(
|
||||
const response = await makeRequest(
|
||||
`/api/projects/${projectId}/tasks/${task.id}`,
|
||||
{
|
||||
method: "PUT",
|
||||
@@ -216,7 +216,7 @@ export function TaskDetailsDialog({
|
||||
setCreatingAttempt(true);
|
||||
const worktreePath = `/tmp/task-${task.id}-attempt-${Date.now()}`;
|
||||
|
||||
const response = await makeAuthenticatedRequest(
|
||||
const response = await makeRequest(
|
||||
`/api/projects/${projectId}/tasks/${task.id}/attempts`,
|
||||
{
|
||||
method: "POST",
|
||||
@@ -251,7 +251,7 @@ export function TaskDetailsDialog({
|
||||
|
||||
try {
|
||||
setStoppingAttempt(true);
|
||||
const response = await makeAuthenticatedRequest(
|
||||
const response = await makeRequest(
|
||||
`/api/projects/${projectId}/tasks/${task.id}/attempts/${selectedAttempt.id}/stop`,
|
||||
{
|
||||
method: "POST",
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Input } from '@/components/ui/input'
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert'
|
||||
import { Folder, FolderOpen, File, AlertCircle, Home, ChevronUp } from 'lucide-react'
|
||||
import { makeAuthenticatedRequest } from '@/lib/auth'
|
||||
import { makeRequest } from '@/lib/api'
|
||||
import { DirectoryEntry } from 'shared/types'
|
||||
|
||||
interface FolderPickerProps {
|
||||
@@ -43,7 +43,7 @@ export function FolderPicker({
|
||||
|
||||
try {
|
||||
const queryParam = path ? `?path=${encodeURIComponent(path)}` : ''
|
||||
const response = await makeAuthenticatedRequest(`/api/filesystem/list${queryParam}`)
|
||||
const response = await makeRequest(`/api/filesystem/list${queryParam}`)
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to load directory')
|
||||
|
||||
@@ -1,185 +0,0 @@
|
||||
import { useState } from 'react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert'
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||
import { User, CreateUser, UpdateUser } from 'shared/types'
|
||||
import { makeAuthenticatedRequest, authStorage } from '@/lib/auth'
|
||||
import { AlertCircle } from 'lucide-react'
|
||||
|
||||
interface UserFormProps {
|
||||
open: boolean
|
||||
onClose: () => void
|
||||
onSuccess: () => void
|
||||
user?: User | null
|
||||
}
|
||||
|
||||
export function UserForm({ open, onClose, onSuccess, user }: UserFormProps) {
|
||||
const [email, setEmail] = useState(user?.email || '')
|
||||
const [password, setPassword] = useState('')
|
||||
const [isAdmin, setIsAdmin] = useState(user?.is_admin || false)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
|
||||
const currentUser = authStorage.getUser()
|
||||
const isEditing = !!user
|
||||
const canEditAdminStatus = currentUser?.is_admin && currentUser.id !== user?.id
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
setError('')
|
||||
setLoading(true)
|
||||
|
||||
try {
|
||||
if (isEditing) {
|
||||
const updateData: UpdateUser = {
|
||||
email: email !== user.email ? email : null,
|
||||
password: password ? password : null,
|
||||
is_admin: canEditAdminStatus && isAdmin !== user.is_admin ? isAdmin : null
|
||||
}
|
||||
|
||||
// Remove null values
|
||||
Object.keys(updateData).forEach(key => {
|
||||
if (updateData[key as keyof UpdateUser] === null) {
|
||||
delete updateData[key as keyof UpdateUser]
|
||||
}
|
||||
})
|
||||
|
||||
const response = await makeAuthenticatedRequest(`/api/users/${user.id}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(updateData),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to update user')
|
||||
}
|
||||
} else {
|
||||
if (!password) {
|
||||
throw new Error('Password is required for new users')
|
||||
}
|
||||
|
||||
const createData: CreateUser = {
|
||||
email,
|
||||
password,
|
||||
is_admin: currentUser?.is_admin ? isAdmin : false
|
||||
}
|
||||
|
||||
const response = await makeAuthenticatedRequest('/api/users', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(createData),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 409) {
|
||||
throw new Error('A user with this email already exists')
|
||||
}
|
||||
throw new Error('Failed to create user')
|
||||
}
|
||||
}
|
||||
|
||||
onSuccess()
|
||||
resetForm()
|
||||
} catch (error) {
|
||||
setError(error instanceof Error ? error.message : 'An error occurred')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const resetForm = () => {
|
||||
setEmail(user?.email || '')
|
||||
setPassword('')
|
||||
setIsAdmin(user?.is_admin || false)
|
||||
setError('')
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
resetForm()
|
||||
onClose()
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={handleClose}>
|
||||
<DialogContent className="sm:max-w-[425px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{isEditing ? 'Edit User' : 'Create New User'}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{isEditing
|
||||
? 'Make changes to the user account here. Click save when you\'re done.'
|
||||
: 'Add a new user to the system. They will be able to log in with these credentials.'
|
||||
}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder="Enter email address"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="password">
|
||||
{isEditing ? 'New Password (leave blank to keep current)' : 'Password'}
|
||||
</Label>
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder={isEditing ? "Enter new password" : "Enter password"}
|
||||
required={!isEditing}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{canEditAdminStatus && (
|
||||
<div className="flex items-center space-x-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="isAdmin"
|
||||
checked={isAdmin}
|
||||
onChange={(e) => setIsAdmin(e.target.checked)}
|
||||
className="rounded border-gray-300"
|
||||
/>
|
||||
<Label htmlFor="isAdmin" className="text-sm font-medium">
|
||||
Administrator privileges
|
||||
</Label>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<Alert variant="destructive">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
{error}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={handleClose}
|
||||
disabled={loading}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" disabled={loading || !email.trim()}>
|
||||
{loading ? 'Saving...' : isEditing ? 'Save Changes' : 'Create User'}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
@@ -1,188 +0,0 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert'
|
||||
import { User, ApiResponse } from 'shared/types'
|
||||
import { UserForm } from './user-form'
|
||||
import { makeAuthenticatedRequest, authStorage } from '@/lib/auth'
|
||||
import { Plus, Edit, Trash2, Calendar, AlertCircle, Loader2, Shield, User as UserIcon } from 'lucide-react'
|
||||
|
||||
export function UserList() {
|
||||
const [users, setUsers] = useState<User[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [showForm, setShowForm] = useState(false)
|
||||
const [editingUser, setEditingUser] = useState<User | null>(null)
|
||||
const [error, setError] = useState('')
|
||||
const currentUser = authStorage.getUser()
|
||||
|
||||
const fetchUsers = async () => {
|
||||
setLoading(true)
|
||||
setError('')
|
||||
try {
|
||||
const response = await makeAuthenticatedRequest('/api/users')
|
||||
const data: ApiResponse<User[]> = await response.json()
|
||||
if (data.success && data.data) {
|
||||
setUsers(data.data)
|
||||
} else {
|
||||
setError('Failed to load users')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch users:', error)
|
||||
setError('Failed to connect to server')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDelete = async (id: string, email: string) => {
|
||||
if (!confirm(`Are you sure you want to delete user "${email}"? This action cannot be undone.`)) return
|
||||
|
||||
try {
|
||||
const response = await makeAuthenticatedRequest(`/api/users/${id}`, {
|
||||
method: 'DELETE',
|
||||
})
|
||||
if (response.ok) {
|
||||
fetchUsers()
|
||||
} else if (response.status === 403) {
|
||||
setError('You cannot delete this user')
|
||||
} else {
|
||||
setError('Failed to delete user')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to delete user:', error)
|
||||
setError('Failed to delete user')
|
||||
}
|
||||
}
|
||||
|
||||
const handleEdit = (user: User) => {
|
||||
setEditingUser(user)
|
||||
setShowForm(true)
|
||||
}
|
||||
|
||||
const handleFormSuccess = () => {
|
||||
setShowForm(false)
|
||||
setEditingUser(null)
|
||||
fetchUsers()
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchUsers()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-tight">Users</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Manage user accounts and permissions
|
||||
</p>
|
||||
</div>
|
||||
{currentUser?.is_admin && (
|
||||
<Button onClick={() => setShowForm(true)}>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
Add User
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<Alert variant="destructive">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
{error}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Loading users...
|
||||
</div>
|
||||
) : users.length === 0 ? (
|
||||
<Card>
|
||||
<CardContent className="py-12 text-center">
|
||||
<div className="mx-auto flex h-12 w-12 items-center justify-center rounded-lg bg-muted">
|
||||
<UserIcon className="h-6 w-6" />
|
||||
</div>
|
||||
<h3 className="mt-4 text-lg font-semibold">No users found</h3>
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
Get started by creating the first user account.
|
||||
</p>
|
||||
{currentUser?.is_admin && (
|
||||
<Button
|
||||
className="mt-4"
|
||||
onClick={() => setShowForm(true)}
|
||||
>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
Add your first user
|
||||
</Button>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
{users.map((user) => (
|
||||
<Card key={user.id} className="hover:shadow-md transition-shadow">
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-start justify-between">
|
||||
<CardTitle className="text-lg flex items-center">
|
||||
{user.is_admin ? (
|
||||
<Shield className="mr-2 h-4 w-4 text-orange-500" />
|
||||
) : (
|
||||
<UserIcon className="mr-2 h-4 w-4 text-blue-500" />
|
||||
)}
|
||||
{user.email}
|
||||
</CardTitle>
|
||||
<Badge variant={user.is_admin ? "default" : "secondary"}>
|
||||
{user.is_admin ? "Admin" : "User"}
|
||||
</Badge>
|
||||
</div>
|
||||
<CardDescription className="flex items-center">
|
||||
<Calendar className="mr-1 h-3 w-3" />
|
||||
Joined {user.created_at ? new Date(user.created_at).toLocaleDateString() : 'Unknown'}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleEdit(user)}
|
||||
className="h-8"
|
||||
>
|
||||
<Edit className="mr-1 h-3 w-3" />
|
||||
Edit
|
||||
</Button>
|
||||
{currentUser?.is_admin && currentUser.id !== user.id && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleDelete(user.id, user.email)}
|
||||
className="h-8 text-red-600 hover:text-red-700 hover:bg-red-50"
|
||||
>
|
||||
<Trash2 className="mr-1 h-3 w-3" />
|
||||
Delete
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<UserForm
|
||||
open={showForm}
|
||||
onClose={() => {
|
||||
setShowForm(false)
|
||||
setEditingUser(null)
|
||||
}}
|
||||
onSuccess={handleFormSuccess}
|
||||
user={editingUser}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
import { UserList } from './user-list'
|
||||
|
||||
export function UsersPage() {
|
||||
return <UserList />
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
import { createContext, useContext, useState, useEffect, ReactNode } from 'react'
|
||||
import { isAuthenticated, authStorage, makeAuthenticatedRequest } from '@/lib/auth'
|
||||
import { User } from 'shared/types'
|
||||
|
||||
interface AuthContextType {
|
||||
user: User | null
|
||||
isAuthenticated: boolean
|
||||
isLoading: boolean
|
||||
login: (user: User, token: string) => void
|
||||
logout: () => void
|
||||
refreshAuthStatus: () => Promise<void>
|
||||
}
|
||||
|
||||
const AuthContext = createContext<AuthContextType | undefined>(undefined)
|
||||
|
||||
interface AuthProviderProps {
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
export function AuthProvider({ children }: AuthProviderProps) {
|
||||
const [user, setUser] = useState<User | null>(null)
|
||||
const [isAuthenticatedState, setIsAuthenticated] = useState(false)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
|
||||
const checkAuthStatus = async (): Promise<boolean> => {
|
||||
const token = authStorage.getToken()
|
||||
if (!token) {
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await makeAuthenticatedRequest('/api/auth/status')
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
if (data.success && data.data?.authenticated) {
|
||||
// Update user data from server response
|
||||
if (data.data.user_id && data.data.email) {
|
||||
const userData: User = {
|
||||
id: data.data.user_id,
|
||||
email: data.data.email,
|
||||
is_admin: data.data.is_admin || false,
|
||||
created_at: new Date(),
|
||||
updated_at: new Date()
|
||||
}
|
||||
authStorage.setUser(userData)
|
||||
setUser(userData)
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// If we get here, the token is invalid
|
||||
return false
|
||||
} catch (error) {
|
||||
console.error('Auth status check failed:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const refreshAuthStatus = async () => {
|
||||
setIsLoading(true)
|
||||
|
||||
if (isAuthenticated()) {
|
||||
const isValid = await checkAuthStatus()
|
||||
if (isValid) {
|
||||
setIsAuthenticated(true)
|
||||
} else {
|
||||
// Clear invalid auth state
|
||||
authStorage.clear()
|
||||
setUser(null)
|
||||
setIsAuthenticated(false)
|
||||
}
|
||||
} else {
|
||||
setUser(null)
|
||||
setIsAuthenticated(false)
|
||||
}
|
||||
|
||||
setIsLoading(false)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
refreshAuthStatus()
|
||||
}, [])
|
||||
|
||||
const login = (userData: User, token: string) => {
|
||||
authStorage.setToken(token)
|
||||
authStorage.setUser(userData)
|
||||
setUser(userData)
|
||||
setIsAuthenticated(true)
|
||||
}
|
||||
|
||||
const logout = () => {
|
||||
authStorage.clear()
|
||||
setUser(null)
|
||||
setIsAuthenticated(false)
|
||||
window.location.href = '/'
|
||||
}
|
||||
|
||||
const value: AuthContextType = {
|
||||
user,
|
||||
isAuthenticated: isAuthenticatedState,
|
||||
isLoading,
|
||||
login,
|
||||
logout,
|
||||
refreshAuthStatus
|
||||
}
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={value}>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export function useAuth() {
|
||||
const context = useContext(AuthContext)
|
||||
if (context === undefined) {
|
||||
throw new Error('useAuth must be used within an AuthProvider')
|
||||
}
|
||||
return context
|
||||
}
|
||||
11
frontend/src/lib/api.ts
Normal file
11
frontend/src/lib/api.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export const makeRequest = async (url: string, options: RequestInit = {}) => {
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
...(options.headers || {})
|
||||
}
|
||||
|
||||
return fetch(url, {
|
||||
...options,
|
||||
headers
|
||||
})
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
import { User } from 'shared/types'
|
||||
|
||||
const TOKEN_KEY = 'auth_token'
|
||||
const USER_KEY = 'auth_user'
|
||||
|
||||
export const authStorage = {
|
||||
getToken: (): string | null => {
|
||||
return localStorage.getItem(TOKEN_KEY)
|
||||
},
|
||||
|
||||
setToken: (token: string): void => {
|
||||
localStorage.setItem(TOKEN_KEY, token)
|
||||
},
|
||||
|
||||
removeToken: (): void => {
|
||||
localStorage.removeItem(TOKEN_KEY)
|
||||
},
|
||||
|
||||
getUser: (): User | null => {
|
||||
const user = localStorage.getItem(USER_KEY)
|
||||
return user ? JSON.parse(user) : null
|
||||
},
|
||||
|
||||
setUser: (user: User): void => {
|
||||
localStorage.setItem(USER_KEY, JSON.stringify(user))
|
||||
},
|
||||
|
||||
removeUser: (): void => {
|
||||
localStorage.removeItem(USER_KEY)
|
||||
},
|
||||
|
||||
clear: (): void => {
|
||||
localStorage.removeItem(TOKEN_KEY)
|
||||
localStorage.removeItem(USER_KEY)
|
||||
}
|
||||
}
|
||||
|
||||
export const getAuthHeaders = (): Record<string, string> => {
|
||||
const token = authStorage.getToken()
|
||||
return token ? { Authorization: `Bearer ${token}` } : {}
|
||||
}
|
||||
|
||||
export const makeAuthenticatedRequest = async (url: string, options: RequestInit = {}) => {
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
...getAuthHeaders(),
|
||||
...(options.headers || {})
|
||||
}
|
||||
|
||||
return fetch(url, {
|
||||
...options,
|
||||
headers
|
||||
})
|
||||
}
|
||||
|
||||
export const isAuthenticated = (): boolean => {
|
||||
return !!authStorage.getToken()
|
||||
}
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { ApiResponse } from "shared/types";
|
||||
import { authStorage, makeAuthenticatedRequest } from "@/lib/auth";
|
||||
import { makeRequest } from "@/lib/api";
|
||||
import {
|
||||
Heart,
|
||||
Activity,
|
||||
@@ -20,7 +20,6 @@ import {
|
||||
CheckCircle,
|
||||
AlertCircle,
|
||||
Zap,
|
||||
Shield,
|
||||
} from "lucide-react";
|
||||
|
||||
export function HomePage() {
|
||||
@@ -30,12 +29,12 @@ export function HomePage() {
|
||||
);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const currentUser = authStorage.getUser();
|
||||
// Single user app, no need for user data
|
||||
|
||||
const checkHealth = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await makeAuthenticatedRequest("/api/health");
|
||||
const response = await makeRequest("/api/health");
|
||||
const data: ApiResponse<string> = await response.json();
|
||||
setMessage(data.message || "Health check completed");
|
||||
setMessageType("success");
|
||||
@@ -134,43 +133,37 @@ export function HomePage() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{currentUser?.is_admin && (
|
||||
<Card className="group hover:shadow-lg transition-all duration-200 border-muted/50 hover:border-muted">
|
||||
<CardHeader className="pb-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
<div className="rounded-lg bg-amber-500/10 p-2 mr-3 group-hover:bg-amber-500/20 transition-colors">
|
||||
<Users className="h-5 w-5 text-amber-600" />
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle className="text-lg flex items-center gap-2">
|
||||
Users
|
||||
<Badge variant="outline" className="text-xs">
|
||||
<Shield className="mr-1 h-3 w-3" />
|
||||
Admin Only
|
||||
</Badge>
|
||||
</CardTitle>
|
||||
<CardDescription className="mt-1">
|
||||
Manage user accounts and permissions
|
||||
</CardDescription>
|
||||
</div>
|
||||
<Card className="group hover:shadow-lg transition-all duration-200 border-muted/50 hover:border-muted">
|
||||
<CardHeader className="pb-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
<div className="rounded-lg bg-amber-500/10 p-2 mr-3 group-hover:bg-amber-500/20 transition-colors">
|
||||
<Users className="h-5 w-5 text-amber-600" />
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle className="text-lg flex items-center gap-2">
|
||||
Users
|
||||
</CardTitle>
|
||||
<CardDescription className="mt-1">
|
||||
Manage user accounts and permissions
|
||||
</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Button
|
||||
asChild
|
||||
className="group-hover:shadow-sm transition-shadow"
|
||||
size="sm"
|
||||
>
|
||||
<Link to="/users">
|
||||
<Users className="mr-2 h-4 w-4" />
|
||||
Manage Users
|
||||
</Link>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Button
|
||||
asChild
|
||||
className="group-hover:shadow-sm transition-shadow"
|
||||
size="sm"
|
||||
>
|
||||
<Link to="/users">
|
||||
<Users className="mr-2 h-4 w-4" />
|
||||
Manage Users
|
||||
</Link>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Status Alert */}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useParams, useNavigate } from 'react-router-dom'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent } from '@/components/ui/card'
|
||||
import { ArrowLeft, Plus } from 'lucide-react'
|
||||
import { makeAuthenticatedRequest } from '@/lib/auth'
|
||||
import { makeRequest } from '@/lib/api'
|
||||
import { TaskCreateDialog } from '@/components/tasks/TaskCreateDialog'
|
||||
import { TaskEditDialog } from '@/components/tasks/TaskEditDialog'
|
||||
|
||||
@@ -53,7 +53,7 @@ export function ProjectTasks() {
|
||||
|
||||
const fetchProject = async () => {
|
||||
try {
|
||||
const response = await makeAuthenticatedRequest(`/api/projects/${projectId}`)
|
||||
const response = await makeRequest(`/api/projects/${projectId}`)
|
||||
|
||||
if (response.ok) {
|
||||
const result: ApiResponse<Project> = await response.json()
|
||||
@@ -72,7 +72,7 @@ export function ProjectTasks() {
|
||||
const fetchTasks = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
const response = await makeAuthenticatedRequest(`/api/projects/${projectId}/tasks`)
|
||||
const response = await makeRequest(`/api/projects/${projectId}/tasks`)
|
||||
|
||||
if (response.ok) {
|
||||
const result: ApiResponse<Task[]> = await response.json()
|
||||
@@ -91,7 +91,7 @@ export function ProjectTasks() {
|
||||
|
||||
const handleCreateTask = async (title: string, description: string) => {
|
||||
try {
|
||||
const response = await makeAuthenticatedRequest(`/api/projects/${projectId}/tasks`, {
|
||||
const response = await makeRequest(`/api/projects/${projectId}/tasks`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
project_id: projectId,
|
||||
@@ -114,7 +114,7 @@ export function ProjectTasks() {
|
||||
if (!editingTask) return
|
||||
|
||||
try {
|
||||
const response = await makeAuthenticatedRequest(`/api/projects/${projectId}/tasks/${editingTask.id}`, {
|
||||
const response = await makeRequest(`/api/projects/${projectId}/tasks/${editingTask.id}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({
|
||||
title,
|
||||
@@ -138,7 +138,7 @@ export function ProjectTasks() {
|
||||
if (!confirm('Are you sure you want to delete this task?')) return
|
||||
|
||||
try {
|
||||
const response = await makeAuthenticatedRequest(`/api/projects/${projectId}/tasks/${taskId}`, {
|
||||
const response = await makeRequest(`/api/projects/${projectId}/tasks/${taskId}`, {
|
||||
method: 'DELETE',
|
||||
})
|
||||
|
||||
@@ -179,7 +179,7 @@ export function ProjectTasks() {
|
||||
))
|
||||
|
||||
try {
|
||||
const response = await makeAuthenticatedRequest(`/api/projects/${projectId}/tasks/${taskId}`, {
|
||||
const response = await makeRequest(`/api/projects/${projectId}/tasks/${taskId}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({
|
||||
title: task.title,
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useParams, useNavigate } from "react-router-dom";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ArrowLeft, FileText } from "lucide-react";
|
||||
import { makeAuthenticatedRequest } from "@/lib/auth";
|
||||
import { makeRequest } from "@/lib/api";
|
||||
import type { WorktreeDiff, DiffChunkType } from "shared/types";
|
||||
|
||||
interface ApiResponse<T> {
|
||||
@@ -37,7 +37,7 @@ export function TaskAttemptComparePage() {
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await makeAuthenticatedRequest(
|
||||
const response = await makeRequest(
|
||||
`/api/projects/${projectId}/tasks/${taskId}/attempts/${attemptId}/diff`
|
||||
);
|
||||
|
||||
@@ -67,7 +67,7 @@ export function TaskAttemptComparePage() {
|
||||
|
||||
try {
|
||||
setMerging(true);
|
||||
const response = await makeAuthenticatedRequest(
|
||||
const response = await makeRequest(
|
||||
`/api/projects/${projectId}/tasks/${taskId}/attempts/${attemptId}/merge`,
|
||||
{
|
||||
method: 'POST',
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
} from "@/components/ui/select";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { ArrowLeft, FileText } from "lucide-react";
|
||||
import { makeAuthenticatedRequest } from "@/lib/auth";
|
||||
import { makeRequest } from "@/lib/api";
|
||||
import type {
|
||||
TaskStatus,
|
||||
TaskAttempt,
|
||||
@@ -119,7 +119,7 @@ export function TaskDetailsPage() {
|
||||
|
||||
try {
|
||||
setTaskLoading(true);
|
||||
const response = await makeAuthenticatedRequest(
|
||||
const response = await makeRequest(
|
||||
`/api/projects/${projectId}/tasks/${taskId}`
|
||||
);
|
||||
|
||||
@@ -152,7 +152,7 @@ export function TaskDetailsPage() {
|
||||
setTaskAttemptsLoading(true);
|
||||
}
|
||||
|
||||
const response = await makeAuthenticatedRequest(
|
||||
const response = await makeRequest(
|
||||
`/api/projects/${projectId}/tasks/${taskId}/attempts`
|
||||
);
|
||||
|
||||
@@ -205,7 +205,7 @@ export function TaskDetailsPage() {
|
||||
setActivitiesLoading(true);
|
||||
}
|
||||
|
||||
const response = await makeAuthenticatedRequest(
|
||||
const response = await makeRequest(
|
||||
`/api/projects/${projectId}/tasks/${task.id}/attempts/${attemptId}/activities`
|
||||
);
|
||||
|
||||
@@ -237,7 +237,7 @@ export function TaskDetailsPage() {
|
||||
|
||||
try {
|
||||
setSavingTask(true);
|
||||
const response = await makeAuthenticatedRequest(
|
||||
const response = await makeRequest(
|
||||
`/api/projects/${projectId}/tasks/${task.id}`,
|
||||
{
|
||||
method: "PUT",
|
||||
@@ -287,7 +287,7 @@ export function TaskDetailsPage() {
|
||||
setCreatingAttempt(true);
|
||||
const worktreePath = `/tmp/task-${task.id}-attempt-${Date.now()}`;
|
||||
|
||||
const response = await makeAuthenticatedRequest(
|
||||
const response = await makeRequest(
|
||||
`/api/projects/${projectId}/tasks/${task.id}/attempts`,
|
||||
{
|
||||
method: "POST",
|
||||
@@ -322,7 +322,7 @@ export function TaskDetailsPage() {
|
||||
|
||||
try {
|
||||
setStoppingAttempt(true);
|
||||
const response = await makeAuthenticatedRequest(
|
||||
const response = await makeRequest(
|
||||
`/api/projects/${projectId}/tasks/${task.id}/attempts/${selectedAttempt.id}/stop`,
|
||||
{
|
||||
method: "POST",
|
||||
|
||||
@@ -1,188 +0,0 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert'
|
||||
import { User, ApiResponse } from 'shared/types'
|
||||
import { UserForm } from '@/components/users/user-form'
|
||||
import { makeAuthenticatedRequest, authStorage } from '@/lib/auth'
|
||||
import { Plus, Edit, Trash2, Calendar, AlertCircle, Loader2, Shield, User as UserIcon } from 'lucide-react'
|
||||
|
||||
export function Users() {
|
||||
const [users, setUsers] = useState<User[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [showForm, setShowForm] = useState(false)
|
||||
const [editingUser, setEditingUser] = useState<User | null>(null)
|
||||
const [error, setError] = useState('')
|
||||
const currentUser = authStorage.getUser()
|
||||
|
||||
const fetchUsers = async () => {
|
||||
setLoading(true)
|
||||
setError('')
|
||||
try {
|
||||
const response = await makeAuthenticatedRequest('/api/users')
|
||||
const data: ApiResponse<User[]> = await response.json()
|
||||
if (data.success && data.data) {
|
||||
setUsers(data.data)
|
||||
} else {
|
||||
setError('Failed to load users')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch users:', error)
|
||||
setError('Failed to connect to server')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDelete = async (id: string, email: string) => {
|
||||
if (!confirm(`Are you sure you want to delete user "${email}"? This action cannot be undone.`)) return
|
||||
|
||||
try {
|
||||
const response = await makeAuthenticatedRequest(`/api/users/${id}`, {
|
||||
method: 'DELETE',
|
||||
})
|
||||
if (response.ok) {
|
||||
fetchUsers()
|
||||
} else if (response.status === 403) {
|
||||
setError('You cannot delete this user')
|
||||
} else {
|
||||
setError('Failed to delete user')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to delete user:', error)
|
||||
setError('Failed to delete user')
|
||||
}
|
||||
}
|
||||
|
||||
const handleEdit = (user: User) => {
|
||||
setEditingUser(user)
|
||||
setShowForm(true)
|
||||
}
|
||||
|
||||
const handleFormSuccess = () => {
|
||||
setShowForm(false)
|
||||
setEditingUser(null)
|
||||
fetchUsers()
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchUsers()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-tight">Users</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Manage user accounts and permissions
|
||||
</p>
|
||||
</div>
|
||||
{currentUser?.is_admin && (
|
||||
<Button onClick={() => setShowForm(true)}>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
Add User
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<Alert variant="destructive">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
{error}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Loading users...
|
||||
</div>
|
||||
) : users.length === 0 ? (
|
||||
<Card>
|
||||
<CardContent className="py-12 text-center">
|
||||
<div className="mx-auto flex h-12 w-12 items-center justify-center rounded-lg bg-muted">
|
||||
<UserIcon className="h-6 w-6" />
|
||||
</div>
|
||||
<h3 className="mt-4 text-lg font-semibold">No users found</h3>
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
Get started by creating the first user account.
|
||||
</p>
|
||||
{currentUser?.is_admin && (
|
||||
<Button
|
||||
className="mt-4"
|
||||
onClick={() => setShowForm(true)}
|
||||
>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
Add your first user
|
||||
</Button>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
{users.map((user) => (
|
||||
<Card key={user.id} className="hover:shadow-md transition-shadow">
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-start justify-between">
|
||||
<CardTitle className="text-lg flex items-center">
|
||||
{user.is_admin ? (
|
||||
<Shield className="mr-2 h-4 w-4 text-orange-500" />
|
||||
) : (
|
||||
<UserIcon className="mr-2 h-4 w-4 text-blue-500" />
|
||||
)}
|
||||
{user.email}
|
||||
</CardTitle>
|
||||
<Badge variant={user.is_admin ? "default" : "secondary"}>
|
||||
{user.is_admin ? "Admin" : "User"}
|
||||
</Badge>
|
||||
</div>
|
||||
<CardDescription className="flex items-center">
|
||||
<Calendar className="mr-1 h-3 w-3" />
|
||||
Joined {user.created_at ? new Date(user.created_at).toLocaleDateString() : 'Unknown'}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleEdit(user)}
|
||||
className="h-8"
|
||||
>
|
||||
<Edit className="mr-1 h-3 w-3" />
|
||||
Edit
|
||||
</Button>
|
||||
{currentUser?.is_admin && currentUser.id !== user.id && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleDelete(user.id, user.email)}
|
||||
className="h-8 text-red-600 hover:text-red-700 hover:bg-red-50"
|
||||
>
|
||||
<Trash2 className="mr-1 h-3 w-3" />
|
||||
Delete
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<UserForm
|
||||
open={showForm}
|
||||
onClose={() => {
|
||||
setShowForm(false)
|
||||
setEditingUser(null)
|
||||
}}
|
||||
onSuccess={handleFormSuccess}
|
||||
user={editingUser}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -15,7 +15,6 @@ export default defineConfig({
|
||||
'/api': {
|
||||
target: 'http://localhost:3001',
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/api/, ''),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user