246 lines
13 KiB
TypeScript
246 lines
13 KiB
TypeScript
|
|
import { useEffect, useState } from 'react'
|
||
|
|
import { apiRequest, changePassword, testEmail } from '../api/client'
|
||
|
|
import { Save, Key, Mail } from 'lucide-react'
|
||
|
|
|
||
|
|
interface PanelConfig {
|
||
|
|
panel_port: number
|
||
|
|
www_root: string
|
||
|
|
setup_path: string
|
||
|
|
webserver_type: string
|
||
|
|
mysql_root_set: boolean
|
||
|
|
app_name: string
|
||
|
|
app_version: string
|
||
|
|
}
|
||
|
|
|
||
|
|
export function ConfigPage() {
|
||
|
|
const [config, setConfig] = useState<PanelConfig | null>(null)
|
||
|
|
const [configKeys, setConfigKeys] = useState<Record<string, string>>({})
|
||
|
|
const [loading, setLoading] = useState(true)
|
||
|
|
const [error, setError] = useState('')
|
||
|
|
const [saved, setSaved] = useState(false)
|
||
|
|
const [pwSaved, setPwSaved] = useState(false)
|
||
|
|
const [pwError, setPwError] = useState('')
|
||
|
|
const [testEmailResult, setTestEmailResult] = useState<string | null>(null)
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
Promise.all([
|
||
|
|
apiRequest<PanelConfig>('/config/panel'),
|
||
|
|
apiRequest<Record<string, string>>('/config/keys'),
|
||
|
|
])
|
||
|
|
.then(([cfg, keys]) => {
|
||
|
|
setConfig(cfg)
|
||
|
|
setConfigKeys(keys || {})
|
||
|
|
})
|
||
|
|
.catch((err) => setError(err.message))
|
||
|
|
.finally(() => setLoading(false))
|
||
|
|
}, [])
|
||
|
|
|
||
|
|
const handleChangePassword = (e: React.FormEvent) => {
|
||
|
|
e.preventDefault()
|
||
|
|
setPwSaved(false)
|
||
|
|
setPwError('')
|
||
|
|
const form = e.currentTarget
|
||
|
|
const oldPw = (form.elements.namedItem('old_password') as HTMLInputElement).value
|
||
|
|
const newPw = (form.elements.namedItem('new_password') as HTMLInputElement).value
|
||
|
|
const confirmPw = (form.elements.namedItem('confirm_password') as HTMLInputElement).value
|
||
|
|
if (!oldPw || !newPw) {
|
||
|
|
setPwError('All fields required')
|
||
|
|
return
|
||
|
|
}
|
||
|
|
if (newPw !== confirmPw) {
|
||
|
|
setPwError('New passwords do not match')
|
||
|
|
return
|
||
|
|
}
|
||
|
|
if (newPw.length < 6) {
|
||
|
|
setPwError('Password must be at least 6 characters')
|
||
|
|
return
|
||
|
|
}
|
||
|
|
changePassword(oldPw, newPw)
|
||
|
|
.then(() => {
|
||
|
|
setPwSaved(true)
|
||
|
|
form.reset()
|
||
|
|
})
|
||
|
|
.catch((err) => setPwError(err.message))
|
||
|
|
}
|
||
|
|
|
||
|
|
const handleTestEmail = () => {
|
||
|
|
setTestEmailResult(null)
|
||
|
|
testEmail()
|
||
|
|
.then(() => setTestEmailResult('Test email sent!'))
|
||
|
|
.catch((err) => setTestEmailResult(`Failed: ${err.message}`))
|
||
|
|
}
|
||
|
|
|
||
|
|
const handleSave = (e: React.FormEvent) => {
|
||
|
|
e.preventDefault()
|
||
|
|
setSaved(false)
|
||
|
|
const form = e.currentTarget
|
||
|
|
const port = (form.elements.namedItem('panel_port') as HTMLInputElement).value
|
||
|
|
const wwwRoot = (form.elements.namedItem('www_root') as HTMLInputElement).value
|
||
|
|
const setupPath = (form.elements.namedItem('setup_path') as HTMLInputElement).value
|
||
|
|
const webserver = (form.elements.namedItem('webserver_type') as HTMLSelectElement).value
|
||
|
|
const mysqlRoot = (form.elements.namedItem('mysql_root') as HTMLInputElement).value
|
||
|
|
const emailTo = (form.elements.namedItem('email_to') as HTMLInputElement)?.value || ''
|
||
|
|
const smtpServer = (form.elements.namedItem('smtp_server') as HTMLInputElement)?.value || ''
|
||
|
|
const smtpPort = (form.elements.namedItem('smtp_port') as HTMLInputElement)?.value || '587'
|
||
|
|
const smtpUser = (form.elements.namedItem('smtp_user') as HTMLInputElement)?.value || ''
|
||
|
|
const smtpPassword = (form.elements.namedItem('smtp_password') as HTMLInputElement)?.value || ''
|
||
|
|
|
||
|
|
const promises: Promise<unknown>[] = [
|
||
|
|
apiRequest('/config/set', { method: 'POST', body: JSON.stringify({ key: 'panel_port', value: port }) }),
|
||
|
|
apiRequest('/config/set', { method: 'POST', body: JSON.stringify({ key: 'www_root', value: wwwRoot }) }),
|
||
|
|
apiRequest('/config/set', { method: 'POST', body: JSON.stringify({ key: 'setup_path', value: setupPath }) }),
|
||
|
|
apiRequest('/config/set', { method: 'POST', body: JSON.stringify({ key: 'webserver_type', value: webserver }) }),
|
||
|
|
]
|
||
|
|
if (mysqlRoot) {
|
||
|
|
promises.push(apiRequest('/config/set', { method: 'POST', body: JSON.stringify({ key: 'mysql_root', value: mysqlRoot }) }))
|
||
|
|
}
|
||
|
|
promises.push(
|
||
|
|
apiRequest('/config/set', { method: 'POST', body: JSON.stringify({ key: 'email_to', value: emailTo }) }),
|
||
|
|
apiRequest('/config/set', { method: 'POST', body: JSON.stringify({ key: 'smtp_server', value: smtpServer }) }),
|
||
|
|
apiRequest('/config/set', { method: 'POST', body: JSON.stringify({ key: 'smtp_port', value: smtpPort }) }),
|
||
|
|
apiRequest('/config/set', { method: 'POST', body: JSON.stringify({ key: 'smtp_user', value: smtpUser }) }),
|
||
|
|
)
|
||
|
|
if (smtpPassword) {
|
||
|
|
promises.push(apiRequest('/config/set', { method: 'POST', body: JSON.stringify({ key: 'smtp_password', value: smtpPassword }) }))
|
||
|
|
}
|
||
|
|
Promise.all(promises)
|
||
|
|
.then(() => setSaved(true))
|
||
|
|
.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>
|
||
|
|
if (!config) return null
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div>
|
||
|
|
<h1 className="text-2xl font-bold mb-6 text-gray-800 dark:text-white">Settings</h1>
|
||
|
|
|
||
|
|
<form onSubmit={handleSave} className="max-w-xl space-y-6">
|
||
|
|
<div className="bg-white dark:bg-gray-800 rounded-lg shadow border border-gray-200 dark:border-gray-700 p-6">
|
||
|
|
<h2 className="text-lg font-semibold mb-4 text-gray-800 dark:text-white">Panel</h2>
|
||
|
|
<div className="space-y-4">
|
||
|
|
<div>
|
||
|
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Panel Port</label>
|
||
|
|
<input name="panel_port" type="number" defaultValue={config.panel_port} className="w-full px-4 py-2 border rounded-lg bg-white dark:bg-gray-700" />
|
||
|
|
</div>
|
||
|
|
<div>
|
||
|
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">WWW Root</label>
|
||
|
|
<input name="www_root" type="text" defaultValue={config.www_root} className="w-full px-4 py-2 border rounded-lg bg-white dark:bg-gray-700" />
|
||
|
|
</div>
|
||
|
|
<div>
|
||
|
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Setup Path</label>
|
||
|
|
<input name="setup_path" type="text" defaultValue={config.setup_path} className="w-full px-4 py-2 border rounded-lg bg-white dark:bg-gray-700" />
|
||
|
|
</div>
|
||
|
|
<div>
|
||
|
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Webserver</label>
|
||
|
|
<select name="webserver_type" defaultValue={config.webserver_type} className="w-full px-4 py-2 border rounded-lg bg-white dark:bg-gray-700">
|
||
|
|
<option value="nginx">Nginx</option>
|
||
|
|
<option value="apache">Apache</option>
|
||
|
|
<option value="openlitespeed">OpenLiteSpeed</option>
|
||
|
|
</select>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="bg-white dark:bg-gray-800 rounded-lg shadow border border-gray-200 dark:border-gray-700 p-6">
|
||
|
|
<h2 className="text-lg font-semibold mb-4 text-gray-800 dark:text-white">Notifications (Email)</h2>
|
||
|
|
<div className="space-y-4">
|
||
|
|
<div>
|
||
|
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Email To</label>
|
||
|
|
<input name="email_to" type="email" defaultValue={configKeys.email_to} placeholder="admin@example.com" className="w-full px-4 py-2 border rounded-lg bg-white dark:bg-gray-700" />
|
||
|
|
</div>
|
||
|
|
<div>
|
||
|
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">SMTP Server</label>
|
||
|
|
<input name="smtp_server" type="text" defaultValue={configKeys.smtp_server} placeholder="smtp.gmail.com" className="w-full px-4 py-2 border rounded-lg bg-white dark:bg-gray-700" />
|
||
|
|
</div>
|
||
|
|
<div>
|
||
|
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">SMTP Port</label>
|
||
|
|
<input name="smtp_port" type="number" defaultValue={configKeys.smtp_port || '587'} className="w-full px-4 py-2 border rounded-lg bg-white dark:bg-gray-700" />
|
||
|
|
</div>
|
||
|
|
<div>
|
||
|
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">SMTP User</label>
|
||
|
|
<input name="smtp_user" type="text" defaultValue={configKeys.smtp_user} className="w-full px-4 py-2 border rounded-lg bg-white dark:bg-gray-700" />
|
||
|
|
</div>
|
||
|
|
<div>
|
||
|
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">SMTP Password</label>
|
||
|
|
<input name="smtp_password" type="password" placeholder={configKeys.smtp_password ? '•••••••• (leave blank to keep)' : 'Optional'} className="w-full px-4 py-2 border rounded-lg bg-white dark:bg-gray-700" />
|
||
|
|
</div>
|
||
|
|
<p className="text-xs text-gray-500">Used for panel alerts (e.g. backup completion, security warnings).</p>
|
||
|
|
<button
|
||
|
|
type="button"
|
||
|
|
onClick={handleTestEmail}
|
||
|
|
className="flex items-center gap-2 px-4 py-2 bg-amber-600 hover:bg-amber-700 text-white rounded-lg text-sm"
|
||
|
|
>
|
||
|
|
<Mail className="w-4 h-4" />
|
||
|
|
Send Test Email
|
||
|
|
</button>
|
||
|
|
{testEmailResult && (
|
||
|
|
<p className={`text-sm ${testEmailResult.startsWith('Failed') ? 'text-red-600' : 'text-green-600'}`}>
|
||
|
|
{testEmailResult}
|
||
|
|
</p>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="bg-white dark:bg-gray-800 rounded-lg shadow border border-gray-200 dark:border-gray-700 p-6">
|
||
|
|
<h2 className="text-lg font-semibold mb-4 text-gray-800 dark:text-white">MySQL</h2>
|
||
|
|
<div className="space-y-4">
|
||
|
|
<div>
|
||
|
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Root Password</label>
|
||
|
|
<input
|
||
|
|
name="mysql_root"
|
||
|
|
type="password"
|
||
|
|
placeholder={config.mysql_root_set ? '•••••••• (leave blank to keep)' : 'Required for real DB creation'}
|
||
|
|
className="w-full px-4 py-2 border rounded-lg bg-white dark:bg-gray-700"
|
||
|
|
/>
|
||
|
|
<p className="mt-1 text-xs text-gray-500">Used to create/drop MySQL databases. Leave blank to keep current.</p>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="flex items-center gap-4">
|
||
|
|
<button type="submit" className="flex items-center gap-2 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg font-medium">
|
||
|
|
<Save className="w-4 h-4" />
|
||
|
|
Save
|
||
|
|
</button>
|
||
|
|
{saved && <span className="text-green-600 dark:text-green-400 text-sm">Saved</span>}
|
||
|
|
</div>
|
||
|
|
</form>
|
||
|
|
|
||
|
|
<div className="mt-8 bg-white dark:bg-gray-800 rounded-lg shadow border border-gray-200 dark:border-gray-700 p-6 max-w-xl">
|
||
|
|
<h2 className="text-lg font-semibold mb-4 text-gray-800 dark:text-white">Change Password</h2>
|
||
|
|
<form onSubmit={handleChangePassword} className="space-y-4 max-w-md">
|
||
|
|
{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">Current Password</label>
|
||
|
|
<input name="old_password" type="password" 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">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 New 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>
|
||
|
|
<button type="submit" className="flex items-center gap-2 px-4 py-2 bg-amber-600 hover:bg-amber-700 text-white rounded-lg font-medium">
|
||
|
|
<Key className="w-4 h-4" />
|
||
|
|
Change Password
|
||
|
|
</button>
|
||
|
|
{pwSaved && <span className="text-green-600 dark:text-green-400 text-sm ml-2">Password changed</span>}
|
||
|
|
</form>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="mt-8 p-4 bg-gray-100 dark:bg-gray-700/50 rounded-lg text-sm text-gray-600 dark:text-gray-400">
|
||
|
|
<p><strong>App:</strong> {config.app_name} v{config.app_version}</p>
|
||
|
|
<p className="mt-2">Note: Some settings require a panel restart to take effect.</p>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
)
|
||
|
|
}
|