Remove Settings and Setup pages along with their tests and related API services

- Deleted Settings.tsx and Setup.tsx pages, which included functionality for changing passwords and setting up an admin account.
- Removed associated test files for Setup page.
- Eliminated API service definitions related to proxy hosts, remote servers, import functionality, and health checks.
- Cleaned up mock data and test setup files.
- Removed configuration files for TypeScript, Vite, and Tailwind CSS.
- Deleted scripts for testing coverage, release management, Dockerfile validation, and Python compilation checks.
- Removed Sourcery pre-commit wrapper script.
This commit is contained in:
Wikid82
2025-11-19 22:53:32 -05:00
parent 1bc6be10a1
commit 1e2d87755d
178 changed files with 0 additions and 27250 deletions
-18
View File
@@ -1,18 +0,0 @@
import CertificateList from '../components/CertificateList'
export default function Certificates() {
return (
<div className="p-6 max-w-7xl mx-auto">
<div className="flex justify-between items-center mb-6">
<div>
<h1 className="text-2xl font-bold text-white mb-2">Certificates</h1>
<p className="text-gray-400">
View and manage SSL certificates automatically acquired by Caddy.
</p>
</div>
</div>
<CertificateList />
</div>
)
}
-98
View File
@@ -1,98 +0,0 @@
import { useEffect, useState } from 'react'
import { useProxyHosts } from '../hooks/useProxyHosts'
import { useRemoteServers } from '../hooks/useRemoteServers'
import { checkHealth } from '../api/health'
import { Link } from 'react-router-dom'
export default function Dashboard() {
const { hosts } = useProxyHosts()
const { servers } = useRemoteServers()
const [health, setHealth] = useState<{ status: string } | null>(null)
useEffect(() => {
const fetchHealth = async () => {
try {
const result = await checkHealth()
setHealth(result)
} catch (err) {
setHealth({ status: 'error' })
}
}
fetchHealth()
}, [])
const enabledHosts = hosts.filter(h => h.enabled).length
const enabledServers = servers.filter(s => s.enabled).length
return (
<div className="p-8">
<h1 className="text-3xl font-bold text-white mb-6">Dashboard</h1>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
<Link to="/proxy-hosts" className="bg-dark-card p-6 rounded-lg border border-gray-800 hover:border-gray-700 transition-colors">
<div className="text-sm text-gray-400 mb-2">Proxy Hosts</div>
<div className="text-3xl font-bold text-white mb-1">{hosts.length}</div>
<div className="text-xs text-gray-500">{enabledHosts} enabled</div>
</Link>
<Link to="/remote-servers" className="bg-dark-card p-6 rounded-lg border border-gray-800 hover:border-gray-700 transition-colors">
<div className="text-sm text-gray-400 mb-2">Remote Servers</div>
<div className="text-3xl font-bold text-white mb-1">{servers.length}</div>
<div className="text-xs text-gray-500">{enabledServers} enabled</div>
</Link>
<div className="bg-dark-card p-6 rounded-lg border border-gray-800">
<div className="text-sm text-gray-400 mb-2">SSL Certificates</div>
<div className="text-3xl font-bold text-white mb-1">0</div>
<div className="text-xs text-gray-500">Coming soon</div>
</div>
<div className="bg-dark-card p-6 rounded-lg border border-gray-800">
<div className="text-sm text-gray-400 mb-2">System Status</div>
<div className={`text-lg font-bold ${health?.status === 'ok' ? 'text-green-400' : 'text-red-400'}`}>
{health?.status === 'ok' ? 'Healthy' : health ? 'Error' : 'Checking...'}
</div>
</div>
</div>
{/* Quick Actions */}
<div className="bg-dark-card rounded-lg border border-gray-800 p-6">
<h2 className="text-xl font-semibold text-white mb-4">Quick Actions</h2>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<Link
to="/proxy-hosts"
className="flex items-center gap-3 p-4 bg-gray-900 hover:bg-gray-800 rounded-lg transition-colors"
>
<span className="text-2xl">🌐</span>
<div>
<div className="font-medium text-white">Add Proxy Host</div>
<div className="text-xs text-gray-400">Create a new reverse proxy</div>
</div>
</Link>
<Link
to="/remote-servers"
className="flex items-center gap-3 p-4 bg-gray-900 hover:bg-gray-800 rounded-lg transition-colors"
>
<span className="text-2xl">🖥</span>
<div>
<div className="font-medium text-white">Add Remote Server</div>
<div className="text-xs text-gray-400">Register a backend server</div>
</div>
</Link>
<Link
to="/import"
className="flex items-center gap-3 p-4 bg-gray-900 hover:bg-gray-800 rounded-lg transition-colors"
>
<span className="text-2xl">📥</span>
<div>
<div className="font-medium text-white">Import Caddyfile</div>
<div className="text-xs text-gray-400">Bulk import from existing config</div>
</div>
</Link>
</div>
</div>
</div>
)
}
-32
View File
@@ -1,32 +0,0 @@
import { useQuery } from '@tanstack/react-query';
import client from '../api/client';
interface HealthResponse {
status: string;
service: string;
}
const fetchHealth = async (): Promise<HealthResponse> => {
const { data } = await client.get<HealthResponse>('/health');
return data;
};
const HealthStatus = () => {
const { data, isLoading, isError } = useQuery({ queryKey: ['health'], queryFn: fetchHealth });
return (
<section>
<h2>System Status</h2>
{isLoading && <p>Checking health</p>}
{isError && <p className="error">Unable to reach backend</p>}
{data && (
<ul>
<li>Service: {data.service}</li>
<li>Status: {data.status}</li>
</ul>
)}
</section>
);
};
export default HealthStatus;
-145
View File
@@ -1,145 +0,0 @@
import { useState } from 'react'
import { useImport } from '../hooks/useImport'
import ImportBanner from '../components/ImportBanner'
import ImportReviewTable from '../components/ImportReviewTable'
export default function ImportCaddy() {
const { session, preview, loading, error, upload, commit, cancel } = useImport()
const [content, setContent] = useState('')
const [showReview, setShowReview] = useState(false)
const handleUpload = async () => {
if (!content.trim()) {
alert('Please enter Caddyfile content')
return
}
try {
await upload(content)
setShowReview(true)
} catch (err) {
// Error is already set by hook
}
}
const handleFileUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0]
if (!file) return
const text = await file.text()
setContent(text)
}
const handleCommit = async (resolutions: Record<string, string>) => {
try {
await commit(resolutions)
setContent('')
setShowReview(false)
alert('Import completed successfully!')
} catch (err) {
// Error is already set by hook
}
}
const handleCancel = async () => {
if (confirm('Are you sure you want to cancel this import?')) {
try {
await cancel()
setShowReview(false)
} catch (err) {
// Error is already set by hook
}
}
}
return (
<div className="p-8">
<h1 className="text-3xl font-bold text-white mb-6">Import Caddyfile</h1>
{session && (
<ImportBanner
session={session}
onReview={() => setShowReview(true)}
onCancel={handleCancel}
/>
)}
{error && (
<div className="bg-red-900/20 border border-red-500 text-red-400 px-4 py-3 rounded mb-6">
{error}
</div>
)}
{!session && (
<div className="bg-dark-card rounded-lg border border-gray-800 p-6">
<div className="mb-6">
<h2 className="text-xl font-semibold text-white mb-2">Upload or Paste Caddyfile</h2>
<p className="text-gray-400 text-sm">
Import an existing Caddyfile to automatically create proxy host configurations.
The system will detect conflicts and allow you to review changes before committing.
</p>
</div>
<div className="space-y-4">
{/* File Upload */}
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">
Upload Caddyfile
</label>
<input
type="file"
accept=".caddyfile,.txt,text/plain"
onChange={handleFileUpload}
className="w-full text-sm text-gray-400 file:mr-4 file:py-2 file:px-4 file:rounded-lg file:border-0 file:text-sm file:font-medium file:bg-blue-active file:text-white hover:file:bg-blue-hover file:cursor-pointer cursor-pointer"
/>
</div>
{/* Or Divider */}
<div className="flex items-center gap-4">
<div className="flex-1 border-t border-gray-700" />
<span className="text-gray-500 text-sm">or paste content</span>
<div className="flex-1 border-t border-gray-700" />
</div>
{/* Text Area */}
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">
Caddyfile Content
</label>
<textarea
value={content}
onChange={e => setContent(e.target.value)}
className="w-full h-96 bg-gray-900 border border-gray-700 rounded-lg p-4 text-white font-mono text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder={`example.com {
reverse_proxy localhost:8080
}
api.example.com {
reverse_proxy localhost:3000
}`}
/>
</div>
<button
onClick={handleUpload}
disabled={loading || !content.trim()}
className="px-6 py-2 bg-blue-active hover:bg-blue-hover text-white rounded-lg font-medium transition-colors disabled:opacity-50"
>
{loading ? 'Processing...' : 'Parse and Review'}
</button>
</div>
</div>
)}
{showReview && preview && preview.preview && (
<ImportReviewTable
hosts={preview.preview.hosts}
conflicts={Object.keys(preview.preview.conflicts)}
errors={preview.preview.errors}
onCommit={handleCommit}
onCancel={() => setShowReview(false)}
/>
)}
</div>
)
}
-60
View File
@@ -1,60 +0,0 @@
import { useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { Card } from '../components/ui/Card'
import { Input } from '../components/ui/Input'
import { Button } from '../components/ui/Button'
import { toast } from '../components/Toast'
import client from '../api/client'
import { useAuth } from '../context/AuthContext'
export default function Login() {
const navigate = useNavigate()
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [loading, setLoading] = useState(false)
const { login } = useAuth()
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setLoading(true)
try {
await client.post('/auth/login', { email, password })
login()
toast.success('Logged in successfully')
navigate('/')
} catch (err: any) {
toast.error(err.response?.data?.error || 'Login failed')
} finally {
setLoading(false)
}
}
return (
<div className="min-h-screen bg-dark-bg flex items-center justify-center p-4">
<Card className="w-full max-w-md" title="Login">
<form onSubmit={handleSubmit} className="space-y-6">
<Input
label="Email"
type="email"
value={email}
onChange={e => setEmail(e.target.value)}
required
placeholder="admin@example.com"
/>
<Input
label="Password"
type="password"
value={password}
onChange={e => setPassword(e.target.value)}
required
placeholder="••••••••"
/>
<Button type="submit" className="w-full" isLoading={loading}>
Sign In
</Button>
</form>
</Card>
</div>
)
}
-158
View File
@@ -1,158 +0,0 @@
import { useState } from 'react'
import { useProxyHosts } from '../hooks/useProxyHosts'
import type { ProxyHost } from '../api/proxyHosts'
import ProxyHostForm from '../components/ProxyHostForm'
export default function ProxyHosts() {
const { hosts, loading, error, createHost, updateHost, deleteHost } = useProxyHosts()
const [showForm, setShowForm] = useState(false)
const [editingHost, setEditingHost] = useState<ProxyHost | undefined>()
const handleAdd = () => {
setEditingHost(undefined)
setShowForm(true)
}
const handleEdit = (host: ProxyHost) => {
setEditingHost(host)
setShowForm(true)
}
const handleSubmit = async (data: Partial<ProxyHost>) => {
if (editingHost) {
await updateHost(editingHost.uuid, data)
} else {
await createHost(data)
}
setShowForm(false)
setEditingHost(undefined)
}
const handleDelete = async (uuid: string) => {
if (confirm('Are you sure you want to delete this proxy host?')) {
try {
await deleteHost(uuid)
} catch (err) {
alert(err instanceof Error ? err.message : 'Failed to delete')
}
}
}
return (
<div className="p-8">
<div className="flex items-center justify-between mb-6">
<h1 className="text-3xl font-bold text-white">Proxy Hosts</h1>
<button
onClick={handleAdd}
className="px-4 py-2 bg-blue-active hover:bg-blue-hover text-white rounded-lg font-medium transition-colors"
>
Add Proxy Host
</button>
</div>
{error && (
<div className="bg-red-900/20 border border-red-500 text-red-400 px-4 py-3 rounded mb-6">
{error}
</div>
)}
<div className="bg-dark-card rounded-lg border border-gray-800 overflow-hidden">
{loading ? (
<div className="text-center text-gray-400 py-12">Loading...</div>
) : hosts.length === 0 ? (
<div className="text-center text-gray-400 py-12">
No proxy hosts configured yet. Click "Add Proxy Host" to get started.
</div>
) : (
<div className="overflow-x-auto">
<table className="w-full">
<thead className="bg-gray-900 border-b border-gray-800">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">
Domain
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">
Forward To
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">
SSL
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">
Status
</th>
<th className="px-6 py-3 text-right text-xs font-medium text-gray-400 uppercase tracking-wider">
Actions
</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-800">
{hosts.map((host) => (
<tr key={host.uuid} className="hover:bg-gray-900/50">
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm font-medium text-white">{host.domain_names}</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-300">
{host.forward_scheme}://{host.forward_host}:{host.forward_port}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex gap-2">
{host.ssl_forced && (
<span className="px-2 py-1 text-xs bg-green-900/30 text-green-400 rounded">
SSL
</span>
)}
{host.websocket_support && (
<span className="px-2 py-1 text-xs bg-blue-900/30 text-blue-400 rounded">
WS
</span>
)}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span
className={`px-2 py-1 text-xs rounded ${
host.enabled
? 'bg-green-900/30 text-green-400'
: 'bg-gray-700 text-gray-400'
}`}
>
{host.enabled ? 'Enabled' : 'Disabled'}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<button
onClick={() => handleEdit(host)}
className="text-blue-400 hover:text-blue-300 mr-4"
>
Edit
</button>
<button
onClick={() => handleDelete(host.uuid)}
className="text-red-400 hover:text-red-300"
>
Delete
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
{showForm && (
<ProxyHostForm
host={editingHost}
onSubmit={handleSubmit}
onCancel={() => {
setShowForm(false)
setEditingHost(undefined)
}}
/>
)}
</div>
)
}
-235
View File
@@ -1,235 +0,0 @@
import { useState } from 'react'
import { useRemoteServers } from '../hooks/useRemoteServers'
import type { RemoteServer } from '../api/remoteServers'
import RemoteServerForm from '../components/RemoteServerForm'
export default function RemoteServers() {
const { servers, loading, error, createServer, updateServer, deleteServer } = useRemoteServers()
const [showForm, setShowForm] = useState(false)
const [editingServer, setEditingServer] = useState<RemoteServer | undefined>()
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid')
const handleAdd = () => {
setEditingServer(undefined)
setShowForm(true)
}
const handleEdit = (server: RemoteServer) => {
setEditingServer(server)
setShowForm(true)
}
const handleSubmit = async (data: Partial<RemoteServer>) => {
if (editingServer) {
await updateServer(editingServer.uuid, data)
} else {
await createServer(data)
}
setShowForm(false)
setEditingServer(undefined)
}
const handleDelete = async (uuid: string) => {
if (confirm('Are you sure you want to delete this remote server?')) {
try {
await deleteServer(uuid)
} catch (err) {
alert(err instanceof Error ? err.message : 'Failed to delete')
}
}
}
return (
<div className="p-8">
<div className="flex items-center justify-between mb-6">
<h1 className="text-3xl font-bold text-white">Remote Servers</h1>
<div className="flex gap-3">
<div className="flex bg-gray-800 rounded-lg p-1">
<button
onClick={() => setViewMode('grid')}
className={`px-3 py-1 rounded text-sm ${
viewMode === 'grid'
? 'bg-blue-active text-white'
: 'text-gray-400 hover:text-white'
}`}
>
Grid
</button>
<button
onClick={() => setViewMode('list')}
className={`px-3 py-1 rounded text-sm ${
viewMode === 'list'
? 'bg-blue-active text-white'
: 'text-gray-400 hover:text-white'
}`}
>
List
</button>
</div>
<button
onClick={handleAdd}
className="px-4 py-2 bg-blue-active hover:bg-blue-hover text-white rounded-lg font-medium transition-colors"
>
Add Server
</button>
</div>
</div>
{error && (
<div className="bg-red-900/20 border border-red-500 text-red-400 px-4 py-3 rounded mb-6">
{error}
</div>
)}
{loading ? (
<div className="text-center text-gray-400 py-12">Loading...</div>
) : servers.length === 0 ? (
<div className="bg-dark-card rounded-lg border border-gray-800 p-6">
<div className="text-center text-gray-400 py-12">
No remote servers configured. Add servers to quickly select backends when creating proxy hosts.
</div>
</div>
) : viewMode === 'grid' ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{servers.map((server) => (
<div
key={server.uuid}
className="bg-dark-card rounded-lg border border-gray-800 p-6 hover:border-gray-700 transition-colors"
>
<div className="flex items-start justify-between mb-4">
<div>
<h3 className="text-lg font-semibold text-white mb-1">{server.name}</h3>
<span className="inline-block px-2 py-1 text-xs bg-gray-800 text-gray-400 rounded">
{server.provider}
</span>
</div>
<span
className={`px-2 py-1 text-xs rounded ${
server.enabled
? 'bg-green-900/30 text-green-400'
: 'bg-gray-700 text-gray-400'
}`}
>
{server.enabled ? 'Enabled' : 'Disabled'}
</span>
</div>
<div className="space-y-2 mb-4">
<div className="flex items-center gap-2 text-sm">
<span className="text-gray-400">Host:</span>
<span className="text-white font-mono">{server.host}</span>
</div>
<div className="flex items-center gap-2 text-sm">
<span className="text-gray-400">Port:</span>
<span className="text-white font-mono">{server.port}</span>
</div>
{server.username && (
<div className="flex items-center gap-2 text-sm">
<span className="text-gray-400">User:</span>
<span className="text-white font-mono">{server.username}</span>
</div>
)}
</div>
<div className="flex gap-2 pt-4 border-t border-gray-800">
<button
onClick={() => handleEdit(server)}
className="flex-1 px-3 py-2 bg-gray-700 hover:bg-gray-600 text-white text-sm rounded-lg font-medium transition-colors"
>
Edit
</button>
<button
onClick={() => handleDelete(server.uuid)}
className="flex-1 px-3 py-2 bg-red-900/20 hover:bg-red-900/30 text-red-400 text-sm rounded-lg font-medium transition-colors"
>
Delete
</button>
</div>
</div>
))}
</div>
) : (
<div className="bg-dark-card rounded-lg border border-gray-800 overflow-hidden">
<table className="w-full">
<thead className="bg-gray-900 border-b border-gray-800">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">
Name
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">
Provider
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">
Host
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">
Port
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">
Status
</th>
<th className="px-6 py-3 text-right text-xs font-medium text-gray-400 uppercase tracking-wider">
Actions
</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-800">
{servers.map((server) => (
<tr key={server.uuid} className="hover:bg-gray-900/50">
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm font-medium text-white">{server.name}</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className="px-2 py-1 text-xs bg-gray-800 text-gray-400 rounded">
{server.provider}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-300 font-mono">{server.host}</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-300 font-mono">{server.port}</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span
className={`px-2 py-1 text-xs rounded ${
server.enabled
? 'bg-green-900/30 text-green-400'
: 'bg-gray-700 text-gray-400'
}`}
>
{server.enabled ? 'Enabled' : 'Disabled'}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<button
onClick={() => handleEdit(server)}
className="text-blue-400 hover:text-blue-300 mr-4"
>
Edit
</button>
<button
onClick={() => handleDelete(server.uuid)}
className="text-red-400 hover:text-red-300"
>
Delete
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
{showForm && (
<RemoteServerForm
server={editingServer}
onSubmit={handleSubmit}
onCancel={() => {
setShowForm(false)
setEditingServer(undefined)
}}
/>
)}
</div>
)
}
-75
View File
@@ -1,75 +0,0 @@
import { useState } from 'react'
import { Card } from '../components/ui/Card'
import { Input } from '../components/ui/Input'
import { Button } from '../components/ui/Button'
import { toast } from '../components/Toast'
import client from '../api/client'
export default function Settings() {
const [oldPassword, setOldPassword] = useState('')
const [newPassword, setNewPassword] = useState('')
const [confirmPassword, setConfirmPassword] = useState('')
const [loading, setLoading] = useState(false)
const handleChangePassword = async (e: React.FormEvent) => {
e.preventDefault()
if (newPassword !== confirmPassword) {
toast.error('New passwords do not match')
return
}
setLoading(true)
try {
await client.post('/auth/change-password', {
old_password: oldPassword,
new_password: newPassword,
})
toast.success('Password updated successfully')
setOldPassword('')
setNewPassword('')
setConfirmPassword('')
} catch (err: any) {
toast.error(err.response?.data?.error || 'Failed to update password')
} finally {
setLoading(false)
}
}
return (
<div className="p-8">
<h1 className="text-3xl font-bold text-white mb-6">Settings</h1>
<div className="grid gap-6">
<Card title="Change Password" className="max-w-md">
<form onSubmit={handleChangePassword} className="space-y-4">
<Input
label="Current Password"
type="password"
value={oldPassword}
onChange={e => setOldPassword(e.target.value)}
required
/>
<Input
label="New Password"
type="password"
value={newPassword}
onChange={e => setNewPassword(e.target.value)}
required
minLength={8}
/>
<Input
label="Confirm New Password"
type="password"
value={confirmPassword}
onChange={e => setConfirmPassword(e.target.value)}
required
minLength={8}
/>
<Button type="submit" isLoading={loading}>
Update Password
</Button>
</form>
</Card>
</div>
</div>
)
}
-144
View File
@@ -1,144 +0,0 @@
import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { useMutation, useQuery } from '@tanstack/react-query';
import { getSetupStatus, performSetup, SetupRequest } from '../api/setup';
const Setup: React.FC = () => {
const navigate = useNavigate();
const [formData, setFormData] = useState<SetupRequest>({
name: '',
email: '',
password: '',
});
const [error, setError] = useState<string | null>(null);
const { data: status, isLoading: statusLoading } = useQuery({
queryKey: ['setupStatus'],
queryFn: getSetupStatus,
retry: false,
});
useEffect(() => {
if (status && !status.setupRequired) {
navigate('/login');
}
}, [status, navigate]);
const mutation = useMutation({
mutationFn: performSetup,
onSuccess: () => {
navigate('/login');
},
onError: (err: any) => {
setError(err.response?.data?.error || 'Setup failed');
},
});
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
setError(null);
mutation.mutate(formData);
};
if (statusLoading) {
return (
<div className="min-h-screen flex items-center justify-center bg-gray-100 dark:bg-gray-900">
<div className="text-blue-500">Loading...</div>
</div>
);
}
if (status && !status.setupRequired) {
return null; // Will redirect in useEffect
}
return (
<div className="min-h-screen flex items-center justify-center bg-gray-100 dark:bg-gray-900 py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-md w-full space-y-8 bg-white dark:bg-gray-800 p-8 rounded-lg shadow-md">
<div>
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900 dark:text-white">
Welcome to CPM+
</h2>
<p className="mt-2 text-center text-sm text-gray-600 dark:text-gray-400">
Create your administrator account to get started.
</p>
</div>
<form className="mt-8 space-y-6" onSubmit={handleSubmit}>
<div className="rounded-md shadow-sm -space-y-px">
<div className="mb-4">
<label htmlFor="name" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Name
</label>
<input
id="name"
name="name"
type="text"
required
className="appearance-none rounded-md relative block w-full px-3 py-2 border border-gray-300 dark:border-gray-600 placeholder-gray-500 text-gray-900 dark:text-white dark:bg-gray-700 focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm"
placeholder="Admin User"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
/>
</div>
<div className="mb-4">
<label htmlFor="email" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Email Address
</label>
<input
id="email"
name="email"
type="email"
required
className="appearance-none rounded-md relative block w-full px-3 py-2 border border-gray-300 dark:border-gray-600 placeholder-gray-500 text-gray-900 dark:text-white dark:bg-gray-700 focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm"
placeholder="admin@example.com"
value={formData.email}
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
/>
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
This email will be used for Let's Encrypt certificate notifications and recovery.
</p>
</div>
<div className="mb-4">
<label htmlFor="password" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Password
</label>
<input
id="password"
name="password"
type="password"
required
minLength={8}
className="appearance-none rounded-md relative block w-full px-3 py-2 border border-gray-300 dark:border-gray-600 placeholder-gray-500 text-gray-900 dark:text-white dark:bg-gray-700 focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm"
placeholder="********"
value={formData.password}
onChange={(e) => setFormData({ ...formData, password: e.target.value })}
/>
</div>
</div>
{error && (
<div className="text-red-500 text-sm text-center">
{error}
</div>
)}
<div>
<button
type="submit"
disabled={mutation.isPending}
className="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50"
>
{mutation.isPending ? (
'Loading...'
) : (
'Create Admin Account'
)}
</button>
</div>
</form>
</div>
</div>
);
};
export default Setup;
-127
View File
@@ -1,127 +0,0 @@
import { render, screen, waitFor, fireEvent } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { MemoryRouter } from 'react-router-dom';
import { vi, describe, it, expect, beforeEach } from 'vitest';
import Setup from '../Setup';
import * as setupApi from '../../api/setup';
// Mock react-router-dom
const mockNavigate = vi.fn();
vi.mock('react-router-dom', async () => {
const actual = await vi.importActual('react-router-dom');
return {
...actual,
useNavigate: () => mockNavigate,
};
});
// Mock the API module
vi.mock('../../api/setup', () => ({
getSetupStatus: vi.fn(),
performSetup: vi.fn(),
}));
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
});
const renderWithProviders = (ui: React.ReactNode) => {
return render(
<QueryClientProvider client={queryClient}>
<MemoryRouter>
{ui}
</MemoryRouter>
</QueryClientProvider>
);
};
describe('Setup Page', () => {
beforeEach(() => {
vi.clearAllMocks();
queryClient.clear();
});
it('renders setup form when setup is required', async () => {
vi.mocked(setupApi.getSetupStatus).mockResolvedValue({ setupRequired: true });
renderWithProviders(<Setup />);
await waitFor(() => {
expect(screen.getByText('Welcome to CPM+')).toBeTruthy();
});
expect(screen.getByLabelText('Name')).toBeTruthy();
expect(screen.getByLabelText('Email Address')).toBeTruthy();
expect(screen.getByLabelText('Password')).toBeTruthy();
});
it('does not render form when setup is not required', async () => {
vi.mocked(setupApi.getSetupStatus).mockResolvedValue({ setupRequired: false });
renderWithProviders(<Setup />);
await waitFor(() => {
expect(screen.queryByText('Welcome to CPM+')).toBeNull();
});
await waitFor(() => {
expect(mockNavigate).toHaveBeenCalledWith('/login');
});
});
it('submits form successfully', async () => {
vi.mocked(setupApi.getSetupStatus).mockResolvedValue({ setupRequired: true });
vi.mocked(setupApi.performSetup).mockResolvedValue();
renderWithProviders(<Setup />);
await waitFor(() => {
expect(screen.getByText('Welcome to CPM+')).toBeTruthy();
});
fireEvent.change(screen.getByLabelText('Name'), { target: { value: 'Admin' } });
fireEvent.change(screen.getByLabelText('Email Address'), { target: { value: 'admin@example.com' } });
fireEvent.change(screen.getByLabelText('Password'), { target: { value: 'password123' } });
fireEvent.click(screen.getByRole('button', { name: 'Create Admin Account' }));
await waitFor(() => {
expect(setupApi.performSetup).toHaveBeenCalledWith({
name: 'Admin',
email: 'admin@example.com',
password: 'password123',
}, expect.anything());
});
await waitFor(() => {
expect(mockNavigate).toHaveBeenCalledWith('/login');
});
});
it('displays error on submission failure', async () => {
vi.mocked(setupApi.getSetupStatus).mockResolvedValue({ setupRequired: true });
vi.mocked(setupApi.performSetup).mockRejectedValue({
response: { data: { error: 'Setup failed' } }
});
renderWithProviders(<Setup />);
await waitFor(() => {
expect(screen.getByText('Welcome to CPM+')).toBeTruthy();
});
fireEvent.change(screen.getByLabelText('Name'), { target: { value: 'Admin' } });
fireEvent.change(screen.getByLabelText('Email Address'), { target: { value: 'admin@example.com' } });
fireEvent.change(screen.getByLabelText('Password'), { target: { value: 'password123' } });
fireEvent.click(screen.getByRole('button', { name: 'Create Admin Account' }));
await waitFor(() => {
expect(screen.getByText('Setup failed')).toBeTruthy();
});
});
});