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([]) 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(null) const [pwError, setPwError] = useState('') const loadAccounts = () => { setLoading(true) apiRequest('/ftp/list') .then(setAccounts) .catch((err) => setError(err.message)) .finally(() => setLoading(false)) } useEffect(() => { loadAccounts() }, []) const handleCreate = (e: React.FormEvent) => { 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
Loading...
if (error) return
{error}
return (

FTP

FTP accounts use Pure-FTPd (pure-pw). Path must be under www root. Install: apt install pure-ftpd pure-ftpd-common
{showCreate && (

Create FTP Account

{creatingError && (
{creatingError}
)}
)}
{accounts.length === 0 ? ( ) : ( accounts.map((a) => ( )) )}
Name Path Note Actions
No FTP accounts. Click "Add FTP" to create one.
{a.name} {a.path} {a.ps || '-'}
{changePwId && (

Change FTP Password

handleChangePassword(e, changePwId)} className="space-y-4"> {pwError && (
{pwError}
)}
)}
) }