import { useEffect, useState } from 'react' import Modal from 'react-bootstrap/Modal' import { apiRequest, updateFtpPassword } from '../api/client' import { PageHeader, AdminButton, AdminAlert, AdminTable, EmptyState } from '../components/admin' interface FtpAccount { id: number name: string path: string ps: string } export function FtpPage() { const [accounts, setAccounts] = useState([]) const [logPath, setLogPath] = useState(null) const [logContent, setLogContent] = useState('') const [logLoading, setLogLoading] = useState(false) const [logError, setLogError] = 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 loadFtpLogs = () => { setLogLoading(true) setLogError('') apiRequest<{ path: string | null; content: string }>('/ftp/logs?lines=400') .then((r) => { setLogPath(r.path) setLogContent(r.content || '') }) .catch((err) => setLogError(err.message)) .finally(() => setLogLoading(false)) } 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 ( <> setShowCreate(true)}> Add FTP } />
FTP accounts use Pure-FTPd (pure-pw). Path must be under www root. Install:{' '} apt install pure-ftpd pure-ftpd-common
FTP log (tail)
{logError ? {logError} : null} {logPath ? (

Source: {logPath}

) : null} {logContent ? (
              {logContent}
            
) : !logLoading && !logError ? (

Click "Load log" to tail common Pure-FTPd paths on this server.

) : null}
setShowCreate(false)} centered> Create FTP Account
{creatingError ? {creatingError} : null}
Name Path Note Actions {accounts.length === 0 ? ( ) : ( accounts.map((a) => ( {a.name} {a.path} {a.ps || '—'} )) )}
setChangePwId(null)} centered> Change FTP Password {changePwId != null ? (
handleChangePassword(e, changePwId)}> {pwError ? {pwError} : null}
) : null}
) }