import { useEffect, useState } from 'react' import api from '../api/client' import { useAuthStore } from '../stores/auth' import { copyToClipboard } from '../utils/clipboard' import { Plus, Trash2, KeyRound, ShieldAlert, Copy, Check, Key, Save, Settings, QrCode, ShieldCheck, ShieldOff } from 'lucide-react' import { useSettingsStore } from '../stores/settings' export default function SettingsPage() { const [users, setUsers] = useState([]) const [showAdd, setShowAdd] = useState(false) const [form, setForm] = useState({ username: '', password: '', display_name: '' }) const [pwChange, setPwChange] = useState(null) const [pwForm, setPwForm] = useState({ password: '', confirm: '' }) const [pwMsg, setPwMsg] = useState('') const { user: currentUser } = useAuthStore() const reload = () => api.getUsers().then((u) => setUsers(u || [])) useEffect(() => { reload() }, []) const handleAdd = async () => { if (!form.username || !form.password) return await api.createUser(form) setForm({ username: '', password: '', display_name: '' }) setShowAdd(false) reload() } const handleDelete = async (id) => { if (!confirm('Benutzer wirklich loeschen?')) return await api.deleteUser(id) reload() } const handlePasswordChange = async () => { setPwMsg('') if (!pwForm.password || pwForm.password.length < 6) { setPwMsg('Mindestens 6 Zeichen') return } if (pwForm.password !== pwForm.confirm) { setPwMsg('Passwoerter stimmen nicht ueberein') return } try { await api.changePassword(pwChange, pwForm.password) setPwMsg('Passwort geaendert') setTimeout(() => { setPwChange(null); setPwMsg(''); setPwForm({ password: '', confirm: '' }) }, 1500) } catch (e) { setPwMsg('Fehler: ' + e.message) } } return (

Einstellungen

{/* Users */}
Benutzer
{showAdd && (
setForm({ ...form, username: e.target.value })} className="w-full bg-gray-800 border border-gray-700 rounded px-2 py-1 text-white text-sm focus:outline-none focus:border-orange-500" />
setForm({ ...form, password: e.target.value })} className="w-full bg-gray-800 border border-gray-700 rounded px-2 py-1 text-white text-sm focus:outline-none focus:border-orange-500" />
setForm({ ...form, display_name: e.target.value })} className="w-full bg-gray-800 border border-gray-700 rounded px-2 py-1 text-white text-sm focus:outline-none focus:border-orange-500" />
)}
{users.map((u) => (
{u.username} {u.display_name && ( ({u.display_name}) )} {u.role} {currentUser?.username === u.username && ( (Du) )}
{u.last_login ? `Login: ${new Date(u.last_login).toLocaleString('de-DE')}` : 'Nie eingeloggt'}
{/* Password Change */} {pwChange === u.id && (
setPwForm({ ...pwForm, password: e.target.value })} className="w-full bg-gray-900 border border-gray-700 rounded px-2 py-1 text-white text-sm focus:outline-none focus:border-orange-500" autoFocus />
setPwForm({ ...pwForm, confirm: e.target.value })} onKeyDown={(e) => e.key === 'Enter' && handlePasswordChange()} className="w-full bg-gray-900 border border-gray-700 rounded px-2 py-1 text-white text-sm focus:outline-none focus:border-orange-500" />
{pwMsg && ( {pwMsg} )}
)}
))}
{/* API Keys */} {/* 2FA */} {/* System Settings */}
) } function APIKeysSection() { const [keys, setKeys] = useState([]) const [showAdd, setShowAdd] = useState(false) const [newName, setNewName] = useState('') const [newKey, setNewKey] = useState(null) const [copied, setCopied] = useState(null) const [loading, setLoading] = useState(true) const reload = () => { setLoading(true) api.getAPIKeys() .then(k => setKeys(k || [])) .finally(() => setLoading(false)) } useEffect(() => { reload() }, []) const handleCreate = async () => { if (!newName.trim()) return try { const result = await api.createAPIKey(newName.trim()) setNewKey(result) setNewName('') reload() } catch (e) { alert('Fehler: ' + e.message) } } const handleDelete = async (id, name) => { if (!confirm(`API-Key "${name}" wirklich loeschen? Alle Agents mit diesem Key verlieren den Zugang.`)) return await api.deleteAPIKey(id) reload() } const copyText = (text, id) => { copyToClipboard(text) setCopied(id) setTimeout(() => setCopied(null), 2000) } return (
API-Keys ({keys.length})
{/* Neuer Key Dialog */} {showAdd && !newKey && (
setNewName(e.target.value)} placeholder="z.B. Agent-Key Kunde XY" className="w-full bg-gray-800 border border-gray-700 rounded px-2 py-1 text-white text-sm focus:outline-none focus:border-orange-500" autoFocus onKeyDown={(e) => e.key === 'Enter' && handleCreate()} />
)} {/* Neuer Key Anzeige */} {newKey && (
API-Key erstellt — jetzt kopieren, er wird nur einmal angezeigt!
{newKey.key}
)} {/* Key Liste */} {loading ? (
Laden...
) : keys.length === 0 ? (
Keine API-Keys vorhanden
) : (
{keys.map((k) => (
{k.name}
{k.key.substring(0, 8)}...{k.key.substring(k.key.length - 4)} Erstellt: {new Date(k.created_at).toLocaleDateString('de-DE')} {k.last_used && ( Letzter Zugriff: {new Date(k.last_used).toLocaleString('de-DE')} )}
))}
)}
) } function TwoFactorSection() { const { user } = useAuthStore() const [step, setStep] = useState('idle') // idle | setup | confirm | disable const [setupData, setSetupData] = useState(null) const [code, setCode] = useState('') const [password, setPassword] = useState('') const [msg, setMsg] = useState('') const [error, setError] = useState('') const totpEnabled = user?.totp_enabled const handleSetup = async () => { setError(''); setMsg('') try { const data = await api.setupTOTP() setSetupData(data) setStep('setup') } catch (e) { setError(e.message) } } const handleConfirm = async () => { setError('') try { await api.confirmTOTP(code) setMsg('2FA aktiviert') setStep('idle') setCode('') setSetupData(null) // user neu laden await useAuthStore.getState().checkAuth() } catch (e) { setError(e.message) } } const handleDisable = async () => { setError('') try { await api.disableTOTP(password, code) setMsg('2FA deaktiviert') setStep('idle') setCode(''); setPassword('') await useAuthStore.getState().checkAuth() } catch (e) { setError(e.message) } } return (
Zwei-Faktor-Authentifizierung (2FA) {totpEnabled ? Aktiv : Inaktiv }
{error &&
{error}
} {msg &&
{msg}
} {step === 'idle' && (
{totpEnabled ? 'TOTP-Authentifizierung ist aktiv. Jeder Login erfordert einen 6-stelligen Code.' : 'Schütze deinen Account mit einer Authenticator-App (z.B. Google Authenticator, Aegis).'}
{totpEnabled ? : }
)} {step === 'setup' && setupData && (
1. Scanne den QR-Code mit deiner Authenticator-App oder gib den Secret manuell ein.
Secret (manuell)
{setupData.secret}
OTP URI (für QR-Code-Scanner)
{setupData.uri}
2. Gib den ersten Code ein um die Einrichtung zu bestätigen:
setCode(e.target.value.replace(/\D/g, ''))} placeholder="000000" className="w-40 bg-gray-800 border border-gray-700 rounded px-3 py-2 text-white text-center text-lg tracking-widest focus:outline-none focus:border-orange-500" />
)} {step === 'disable' && (
Zur Deaktivierung: Passwort + aktuellen 2FA-Code eingeben.
setPassword(e.target.value)} className="w-full mt-1 bg-gray-800 border border-gray-700 rounded px-3 py-2 text-white text-sm focus:outline-none focus:border-orange-500" />
setCode(e.target.value.replace(/\D/g, ''))} placeholder="000000" className="w-full mt-1 bg-gray-800 border border-gray-700 rounded px-3 py-2 text-white text-sm text-center tracking-widest focus:outline-none focus:border-orange-500" />
)}
) } function SystemSettingsSection() { const { settings, fetchSettings } = useSettingsStore() const [form, setForm] = useState({}) const [saving, setSaving] = useState(false) const [msg, setMsg] = useState('') const [copied, setCopied] = useState(null) useEffect(() => { fetchSettings() }, []) useEffect(() => { setForm({ backend_url_internal: settings.backend_url_internal || '', backend_url_external: settings.backend_url_external || '', api_key: settings.api_key || '', }) }, [settings]) const handleSave = async () => { setSaving(true) setMsg('') try { await api.updateSettings(form) await fetchSettings() setMsg('Gespeichert') setTimeout(() => setMsg(''), 3000) } catch (e) { setMsg('Fehler: ' + e.message) } finally { setSaving(false) } } const copyText = (text, id) => { copyToClipboard(text) setCopied(id) setTimeout(() => setCopied(null), 2000) } return (
System-Einstellungen
setForm({ ...form, backend_url_internal: e.target.value })} placeholder="https://192.168.85.13:8443" className="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2 text-white text-sm focus:outline-none focus:border-orange-500 mt-1" />
Fuer Tunnel-Zugriff und lokale Verbindungen
setForm({ ...form, backend_url_external: e.target.value })} placeholder="https://example.dyndns.org:8443" className="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2 text-white text-sm focus:outline-none focus:border-orange-500 mt-1" />
Fuer Remote-Firewalls in der Installationsanleitung
setForm({ ...form, api_key: e.target.value })} className="flex-1 bg-gray-800 border border-gray-700 rounded px-3 py-2 text-white text-sm font-mono focus:outline-none focus:border-orange-500" />
Wird in Installationsanleitungen angezeigt
{msg && ( {msg} )}
) }