import { readFile, writeFile, rename } from 'node:fs/promises'; import { ensureStateDir, getStatePath } from '@/lib/admin/paths'; import { logger } from '@/lib/logger'; export interface VncDirectoryConfig { 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; } export const DEFAULT_VNCDIRECTORY_CONFIG: VncDirectoryConfig = { 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: {}, }; /** Keys that should be masked when returning config to clients */ export const VNCDIRECTORY_SENSITIVE_KEYS = new Set(['apiKey', 'ldapBindPassword']); function applyEnvOverrides(config: VncDirectoryConfig): VncDirectoryConfig { const envEnabled = process.env.VNCDIRECTORY_ENABLED; if (envEnabled !== undefined) { config.enabled = envEnabled === 'true' || envEnabled === '1'; } const envApiUrl = process.env.VNCDIRECTORY_API_URL; if (envApiUrl !== undefined) { config.apiUrl = envApiUrl; } const envSamlEnabled = process.env.VNCDIRECTORY_SAML_ENABLED; if (envSamlEnabled !== undefined) { config.samlEnabled = envSamlEnabled === 'true' || envSamlEnabled === '1'; } return config; } async function readJsonFile(filename: string): Promise | null> { const filePath = getStatePath(filename); try { const raw = await readFile(filePath, 'utf-8'); return JSON.parse(raw); } catch (error) { if ((error as NodeJS.ErrnoException).code === 'ENOENT') return null; logger.warn(`Failed to read ${filename} from state dir`, { error: error instanceof Error ? error.message : 'Unknown error', }); return null; } } async function writeJsonFile(filename: string, data: Record): Promise { await ensureStateDir(); const targetPath = getStatePath(filename); const tmpPath = targetPath + '.tmp'; await writeFile(tmpPath, JSON.stringify(data, null, 2), 'utf-8'); await rename(tmpPath, targetPath); } export async function getVncDirectoryConfig(): Promise { const fileConfig = await readJsonFile('vncdirectory.json'); const base = fileConfig ? { ...DEFAULT_VNCDIRECTORY_CONFIG, ...fileConfig } : { ...DEFAULT_VNCDIRECTORY_CONFIG }; return applyEnvOverrides(base); } export async function saveVncDirectoryConfig( config: Partial, ): Promise { const current = await getVncDirectoryConfig(); const merged: Record = {}; for (const key of Object.keys(DEFAULT_VNCDIRECTORY_CONFIG)) { const k = key as keyof VncDirectoryConfig; if (k in config) { merged[key] = config[k]; } else { merged[key] = current[k]; } } await writeJsonFile('vncdirectory.json', merged); } export async function isVncDirectoryEnabled(): Promise { const cfg = await getVncDirectoryConfig(); return cfg.enabled; }