import { NextRequest, NextResponse } from 'next/server'; import { requireAdminAuth, getClientIP } from '@/lib/admin/session'; import { auditLog } from '@/lib/admin/audit'; import { logger } from '@/lib/logger'; import { getVncDirectoryConfig, saveVncDirectoryConfig, DEFAULT_VNCDIRECTORY_CONFIG, VNCDIRECTORY_SENSITIVE_KEYS, type VncDirectoryConfig, } from '@/lib/admin/vncdirectory-config'; const VALID_LDAP_TYPES = new Set(['openldap', 'ms-ad']); const KNOWN_KEYS = new Set(Object.keys(DEFAULT_VNCDIRECTORY_CONFIG)); function maskConfigForClient(config: VncDirectoryConfig): Record { const result: Record = {}; for (const [key, value] of Object.entries(config)) { if (VNCDIRECTORY_SENSITIVE_KEYS.has(key)) { result[key] = typeof value === 'string' && value.length > 0 ? '••••••' : ''; } else { result[key] = value; } } return result; } export async function GET(request: NextRequest) { try { const result = await requireAdminAuth(request); if ('error' in result) return result.error; const config = await getVncDirectoryConfig(); return NextResponse.json(maskConfigForClient(config), { headers: { 'Cache-Control': 'no-store' }, }); } catch (error) { logger.error('VNCdirectory config read error', { error: error instanceof Error ? error.message : 'Unknown error', }); return NextResponse.json({ error: 'Internal server error' }, { status: 500 }); } } export async function POST(request: NextRequest) { try { const authResult = await requireAdminAuth(request); if ('error' in authResult) return authResult.error; const ip = getClientIP(request); const body = await request.json(); if (!body || typeof body !== 'object' || Array.isArray(body)) { return NextResponse.json({ error: 'Request body must be an object' }, { status: 400 }); } // Validate known keys only const unknownKeys = Object.keys(body).filter((k) => !KNOWN_KEYS.has(k)); if (unknownKeys.length > 0) { return NextResponse.json( { error: `Unknown config keys: ${unknownKeys.join(', ')}` }, { status: 400 }, ); } // Validate boolean fields const boolFields = ['enabled', 'samlEnabled', 'ldapEnabled', 'tfaEnabled', 'oidcEnabled']; for (const key of boolFields) { if (key in body && typeof body[key] !== 'boolean') { return NextResponse.json( { error: `${key} must be a boolean` }, { status: 400 }, ); } } // Validate sessionTtl if ('sessionTtl' in body) { const ttl = Number(body.sessionTtl); if (!Number.isFinite(ttl) || ttl < 0) { return NextResponse.json( { error: 'sessionTtl must be a non-negative number' }, { status: 400 }, ); } body.sessionTtl = ttl; } // Validate ldapType if ('ldapType' in body && !VALID_LDAP_TYPES.has(body.ldapType)) { return NextResponse.json( { error: `Invalid ldapType: ${body.ldapType}. Must be 'openldap' or 'ms-ad'.` }, { status: 400 }, ); } // Validate federatedApps if ('federatedApps' in body) { if (!body.federatedApps || typeof body.federatedApps !== 'object' || Array.isArray(body.federatedApps)) { return NextResponse.json( { error: 'federatedApps must be an object mapping app names to URLs' }, { status: 400 }, ); } for (const [appName, url] of Object.entries(body.federatedApps as Record)) { if (typeof url !== 'string') { return NextResponse.json( { error: `federatedApps.${appName} must be a string URL` }, { status: 400 }, ); } } } // If apiKey or ldapBindPassword are "••••••", preserve existing value const currentConfig = await getVncDirectoryConfig(); if (body.apiKey === '••••••') { body.apiKey = currentConfig.apiKey; } if (body.ldapBindPassword === '••••••') { body.ldapBindPassword = currentConfig.ldapBindPassword; } const changedKeys = Object.keys(body).filter((k) => { const currentVal = currentConfig[k as keyof VncDirectoryConfig]; const newVal = body[k]; if (k === 'federatedApps') { return JSON.stringify(currentVal) !== JSON.stringify(newVal); } return String(currentVal ?? '') !== String(newVal ?? ''); }); await saveVncDirectoryConfig(body as Partial); if (changedKeys.length > 0) { await auditLog('vncdirectory.update', { changedKeys }, ip); } return NextResponse.json({ ok: true }); } catch (error) { logger.error('VNCdirectory config update error', { error: error instanceof Error ? error.message : 'Unknown error', }); return NextResponse.json({ error: 'Internal server error' }, { status: 500 }); } }