Initial YakPanel commit
This commit is contained in:
257
YakPanel-server/frontend/src/pages/FtpPage.tsx
Normal file
257
YakPanel-server/frontend/src/pages/FtpPage.tsx
Normal file
@@ -0,0 +1,257 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { apiRequest, updateFtpPassword } from '../api/client'
|
||||
import { Plus, Trash2, Key } from 'lucide-react'
|
||||
|
||||
interface FtpAccount {
|
||||
id: number
|
||||
name: string
|
||||
path: string
|
||||
ps: string
|
||||
}
|
||||
|
||||
export function FtpPage() {
|
||||
const [accounts, setAccounts] = useState<FtpAccount[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState('')
|
||||
const [showCreate, setShowCreate] = useState(false)
|
||||
const [creating, setCreating] = useState(false)
|
||||
const [creatingError, setCreatingError] = useState('')
|
||||
const [changePwId, setChangePwId] = useState<number | null>(null)
|
||||
const [pwError, setPwError] = useState('')
|
||||
|
||||
const loadAccounts = () => {
|
||||
setLoading(true)
|
||||
apiRequest<FtpAccount[]>('/ftp/list')
|
||||
.then(setAccounts)
|
||||
.catch((err) => setError(err.message))
|
||||
.finally(() => setLoading(false))
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadAccounts()
|
||||
}, [])
|
||||
|
||||
const handleCreate = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault()
|
||||
const form = e.currentTarget
|
||||
const name = (form.elements.namedItem('name') as HTMLInputElement).value.trim()
|
||||
const password = (form.elements.namedItem('password') as HTMLInputElement).value
|
||||
const path = (form.elements.namedItem('path') as HTMLInputElement).value.trim()
|
||||
const ps = (form.elements.namedItem('ps') as HTMLInputElement).value.trim()
|
||||
|
||||
if (!name || !password || !path) {
|
||||
setCreatingError('Name, password and path are required')
|
||||
return
|
||||
}
|
||||
setCreating(true)
|
||||
setCreatingError('')
|
||||
apiRequest<{ status: boolean; msg: string }>('/ftp/create', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ name, password, path, ps }),
|
||||
})
|
||||
.then(() => {
|
||||
setShowCreate(false)
|
||||
form.reset()
|
||||
loadAccounts()
|
||||
})
|
||||
.catch((err) => setCreatingError(err.message))
|
||||
.finally(() => setCreating(false))
|
||||
}
|
||||
|
||||
const handleChangePassword = (e: React.FormEvent, id: number) => {
|
||||
e.preventDefault()
|
||||
const form = e.currentTarget
|
||||
const password = (form.elements.namedItem('new_password') as HTMLInputElement).value
|
||||
const confirm = (form.elements.namedItem('confirm_password') as HTMLInputElement).value
|
||||
if (!password || password.length < 6) {
|
||||
setPwError('Password must be at least 6 characters')
|
||||
return
|
||||
}
|
||||
if (password !== confirm) {
|
||||
setPwError('Passwords do not match')
|
||||
return
|
||||
}
|
||||
setPwError('')
|
||||
updateFtpPassword(id, password)
|
||||
.then(() => setChangePwId(null))
|
||||
.catch((err) => setPwError(err.message))
|
||||
}
|
||||
|
||||
const handleDelete = (id: number, name: string) => {
|
||||
if (!confirm(`Delete FTP account "${name}"?`)) return
|
||||
apiRequest<{ status: boolean }>(`/ftp/${id}`, { method: 'DELETE' })
|
||||
.then(loadAccounts)
|
||||
.catch((err) => setError(err.message))
|
||||
}
|
||||
|
||||
if (loading) return <div className="text-gray-500">Loading...</div>
|
||||
if (error) return <div className="p-4 rounded bg-red-100 text-red-700">{error}</div>
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h1 className="text-2xl font-bold text-gray-800 dark:text-white">FTP</h1>
|
||||
<button
|
||||
onClick={() => setShowCreate(true)}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg font-medium"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
Add FTP
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 p-3 rounded-lg bg-gray-100 dark:bg-gray-700/50 text-sm text-gray-600 dark:text-gray-400">
|
||||
FTP accounts use Pure-FTPd (pure-pw). Path must be under www root. Install: <code className="font-mono">apt install pure-ftpd pure-ftpd-common</code>
|
||||
</div>
|
||||
|
||||
{showCreate && (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-xl p-6 w-full max-w-md">
|
||||
<h2 className="text-xl font-bold mb-4 text-gray-800 dark:text-white">Create FTP Account</h2>
|
||||
<form onSubmit={handleCreate} className="space-y-4">
|
||||
{creatingError && (
|
||||
<div className="p-2 rounded bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-300 text-sm">
|
||||
{creatingError}
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Username</label>
|
||||
<input
|
||||
name="name"
|
||||
type="text"
|
||||
placeholder="ftpuser"
|
||||
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Password</label>
|
||||
<input
|
||||
name="password"
|
||||
type="password"
|
||||
placeholder="••••••••"
|
||||
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Path</label>
|
||||
<input
|
||||
name="path"
|
||||
type="text"
|
||||
placeholder="/www/wwwroot"
|
||||
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Note (optional)</label>
|
||||
<input
|
||||
name="ps"
|
||||
type="text"
|
||||
placeholder="My FTP"
|
||||
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-2 justify-end pt-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowCreate(false)}
|
||||
className="px-4 py-2 text-gray-600 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={creating}
|
||||
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 disabled:bg-blue-400 text-white rounded-lg font-medium"
|
||||
>
|
||||
{creating ? 'Creating...' : 'Create'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow overflow-hidden">
|
||||
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<thead className="bg-gray-50 dark:bg-gray-700">
|
||||
<tr>
|
||||
<th className="px-4 py-2 text-left text-sm font-medium text-gray-700 dark:text-gray-300">Name</th>
|
||||
<th className="px-4 py-2 text-left text-sm font-medium text-gray-700 dark:text-gray-300">Path</th>
|
||||
<th className="px-4 py-2 text-left text-sm font-medium text-gray-700 dark:text-gray-300">Note</th>
|
||||
<th className="px-4 py-2 text-right text-sm font-medium text-gray-700 dark:text-gray-300">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
{accounts.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={4} className="px-4 py-8 text-center text-gray-500">
|
||||
No FTP accounts. Click "Add FTP" to create one.
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
accounts.map((a) => (
|
||||
<tr key={a.id}>
|
||||
<td className="px-4 py-2 text-gray-900 dark:text-white">{a.name}</td>
|
||||
<td className="px-4 py-2 text-gray-600 dark:text-gray-400">{a.path}</td>
|
||||
<td className="px-4 py-2 text-gray-600 dark:text-gray-400">{a.ps || '-'}</td>
|
||||
<td className="px-4 py-2 text-right">
|
||||
<span className="flex gap-1 justify-end">
|
||||
<button
|
||||
onClick={() => setChangePwId(a.id)}
|
||||
className="p-2 text-amber-600 hover:bg-amber-50 dark:hover:bg-amber-900/20 rounded"
|
||||
title="Change password"
|
||||
>
|
||||
<Key className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDelete(a.id, a.name)}
|
||||
className="p-2 text-red-600 hover:bg-red-50 dark:hover:bg-red-900/20 rounded"
|
||||
title="Delete"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{changePwId && (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-xl p-6 w-full max-w-md">
|
||||
<h2 className="text-xl font-bold mb-4 text-gray-800 dark:text-white">Change FTP Password</h2>
|
||||
<form onSubmit={(e) => handleChangePassword(e, changePwId)} className="space-y-4">
|
||||
{pwError && (
|
||||
<div className="p-2 rounded bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-300 text-sm">
|
||||
{pwError}
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">New Password</label>
|
||||
<input name="new_password" type="password" minLength={6} className="w-full px-4 py-2 border rounded-lg bg-white dark:bg-gray-700" required />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Confirm Password</label>
|
||||
<input name="confirm_password" type="password" minLength={6} className="w-full px-4 py-2 border rounded-lg bg-white dark:bg-gray-700" required />
|
||||
</div>
|
||||
<div className="flex gap-2 justify-end">
|
||||
<button type="button" onClick={() => setChangePwId(null)} className="px-4 py-2 text-gray-600 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg">
|
||||
Cancel
|
||||
</button>
|
||||
<button type="submit" className="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg">
|
||||
Update
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user