diff --git a/app/(main)/admin/_tabs/vncdirectory.tsx b/app/(main)/admin/_tabs/vncdirectory.tsx new file mode 100644 index 00000000..911c167d --- /dev/null +++ b/app/(main)/admin/_tabs/vncdirectory.tsx @@ -0,0 +1,620 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { Save, Loader2, Plus, X } from 'lucide-react'; +import { apiFetch } from '@/lib/browser-navigation'; + +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 [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); + + 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: 'VNCdirectory configuration saved.' }); + setDirty(false); + await fetchConfig(); + } else { + const data = await res.json(); + setMessage({ type: 'error', text: data.error || 'Failed to save' }); + } + setSaving(false); + } + + if (loading) { + return ( +
+ Loading... +
+ ); + } + + const federatedAppsList = Object.entries(config.federatedApps); + + return ( +
+
+
+

VNCdirectory

+

+ Centralized identity and directory integration (SAML, LDAP, 2FA) +

+
+ {dirty && ( + + )} +
+ + {message && ( +
+ {message.text} +
+ )} + +
+
+
+ Enabled +

+ Turn on VNCdirectory integration for identity management, SSO, and directory services +

+
+ +
+
+ + {config.enabled && ( + <> +
+
+ updateField('apiUrl', v)} + placeholder="https://vncdirectory.example.com" + /> + updateField('apiKey', v)} + placeholder="Enter API key" + /> +
+
+ +
+
+ toggleBool('samlEnabled')} + /> + {config.samlEnabled && ( + <> + updateField('samlIdpUrl', v)} + placeholder="https://idp.example.com/saml2/idp" + /> + updateField('samlIssuer', v)} + placeholder="urn:example:vncmail" + /> +
+ +