- 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.
118 lines
3.4 KiB
TypeScript
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;
|
|
}
|