'use client'; import { useEffect, useState } from 'react'; import { useTranslations } from 'next-intl'; import { Save, Loader2, Plus, X } from 'lucide-react'; import { apiFetch } from '@/lib/browser-navigation'; import { toast } from '@/stores/toast-store'; interface VncDirectoryFormData { enabled: boolean; apiUrl: string; apiKey: string; samlEnabled: boolean; samlIdpUrl: string; samlSpCert: string; samlIssuer: string; ldapEnabled: boolean; ldapUri: string; ldapBindDn: string; ldapBindPassword: string; ldapSearchBase: string; ldapType: 'openldap' | 'ms-ad'; tfaEnabled: boolean; oidcEnabled: boolean; oidcClientId: string; oidcDiscoveryUrl: string; sessionTtl: number; federatedApps: Record; } const BLANK_FORM: VncDirectoryFormData = { enabled: false, apiUrl: '', apiKey: '', samlEnabled: false, samlIdpUrl: '', samlSpCert: '', samlIssuer: '', ldapEnabled: false, ldapUri: '', ldapBindDn: '', ldapBindPassword: '', ldapSearchBase: '', ldapType: 'openldap', tfaEnabled: false, oidcEnabled: false, oidcClientId: '', oidcDiscoveryUrl: '', sessionTtl: 28800, federatedApps: {}, }; export function VncDirectoryTab() { const t = useTranslations('admin.vncdirectory'); const [config, setConfig] = useState({ ...BLANK_FORM }); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null); const [dirty, setDirty] = useState(false); useEffect(() => { fetchConfig(); }, []); async function fetchConfig() { setLoading(true); try { const res = await apiFetch('/api/admin/vncdirectory'); if (res.ok) { const data = await res.json(); setConfig(data); } } finally { setLoading(false); } } function updateField(key: K, value: VncDirectoryFormData[K]) { setConfig((prev) => ({ ...prev, [key]: value })); setDirty(true); setMessage(null); } function toggleBool(key: keyof VncDirectoryFormData) { setConfig((prev) => ({ ...prev, [key]: !prev[key] })); setDirty(true); setMessage(null); } function setFederatedApp(name: string, url: string) { setConfig((prev) => ({ ...prev, federatedApps: { ...prev.federatedApps, [name]: url }, })); setDirty(true); setMessage(null); } function removeFederatedApp(name: string) { setConfig((prev) => { const next = { ...prev.federatedApps }; delete next[name]; return { ...prev, federatedApps: next }; }); setDirty(true); setMessage(null); } async function handleSave() { setSaving(true); setMessage(null); try { const res = await apiFetch('/api/admin/vncdirectory', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(config), }); if (res.ok) { setMessage({ type: 'success', text: t('saved') }); setDirty(false); await fetchConfig(); } else { const data = await res.json(); setMessage({ type: 'error', text: data.error || t('save_error') }); } } catch (err) { const msg = err instanceof Error ? err.message : t('save_error'); setMessage({ type: 'error', text: msg }); toast.error(msg); } setSaving(false); } if (loading) { return (
{t('loading')}
); } const federatedAppsList = Object.entries(config.federatedApps); return (

{t('title')}

{t('description')}

{dirty && ( )}
{message && (
{message.text}
)}
{t('enabled')}

{t('enabled_description')}

{config.enabled && ( <>
updateField('apiUrl', v)} placeholder={t('url_placeholder')} /> updateField('apiKey', v)} placeholder={t('api_key_placeholder')} />
toggleBool('samlEnabled')} /> {config.samlEnabled && ( <> updateField('samlIdpUrl', v)} placeholder={t('idp_url_placeholder')} /> updateField('samlIssuer', v)} placeholder={t('issuer_placeholder')} />