Files
SRCmail/lib/admin/vncdirectory-config.ts
T
Bernd Rodler 13ec05da83 feat: P2.9 VNCtalk + P2.10 Collabora + P2.11 Calendar Enhancements + P2.13 VNCdirectory Admin
- P2.9: VNCtalk video meeting — create/update meeting from event modal,
  'Join Meeting' link in event detail. Admin config vnctalkServerUrl.
- P2.10: Collabora online editing — 'Edit with Collabora' for office files,
  WOPI discovery + edit URL. Admin config collaboraServerUrl.
- P2.11: Calendar enhancements — clickable links in descriptions,
  participant contact popover, Reply/Reply All from event, timezone picker,
  map links for locations.
- P2.13: VNCdirectory IDP admin panel — Connection, SAML/IDP, LDAP,
  Authentication, Federated Apps configuration. Secret masking on display.
2026-08-07 13:38:12 +02:00

118 lines
3.4 KiB
TypeScript

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<string, string>;
}
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<Record<string, unknown> | 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<string, unknown>): Promise<void> {
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<VncDirectoryConfig> {
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<VncDirectoryConfig>,
): Promise<void> {
const current = await getVncDirectoryConfig();
const merged: Record<string, unknown> = {};
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<boolean> {
const cfg = await getVncDirectoryConfig();
return cfg.enabled;
}