diff --git a/server.js b/server.js index a8559ee..5dec6c4 100644 --- a/server.js +++ b/server.js @@ -282,6 +282,75 @@ function dockerLogs(container, tail = 160) { }); } +function execContainerCommand(containerName, cmd) { + return new Promise(async (resolve, reject) => { + try { + const bodyStr = JSON.stringify({ + AttachStdout: true, + AttachStderr: true, + Cmd: cmd + }); + + const createResult = await dockerRequest(`/containers/${encodeURIComponent(containerName)}/exec`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Content-Length': Buffer.byteLength(bodyStr) + }, + body: bodyStr + }); + + const execId = createResult?.Id; + if (!execId) { + reject(new Error(`Failed to create exec instance for container ${containerName}`)); + return; + } + + const startBody = JSON.stringify({ Detach: false, Tty: false }); + const req = http.request({ + socketPath: DOCKER_SOCKET, + path: `/exec/${execId}/start`, + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Content-Length': Buffer.byteLength(startBody) + } + }, (res) => { + const chunks = []; + res.on('data', (chunk) => chunks.push(chunk)); + res.on('end', () => { + if (res.statusCode < 200 || res.statusCode >= 300) { + reject(new Error(`Docker exec start returned status ${res.statusCode}`)); + return; + } + const raw = Buffer.concat(chunks); + let cleanText = ''; + let offset = 0; + while (offset + 8 <= raw.length) { + const size = raw.readUInt32BE(offset + 4); + const start = offset + 8; + const end = start + size; + if (end > raw.length) break; + cleanText += raw.slice(start, end).toString('utf8'); + offset = end; + } + if (!cleanText && raw.length > 0) { + cleanText = raw.toString('utf8'); + } + resolve(cleanText.trim()); + }); + }); + + req.on('error', reject); + req.write(startBody); + req.end(); + } catch (error) { + reject(error); + } + }); +} + + async function getContainers() { const containers = await dockerRequest('/containers/json?all=0'); return containers.map((container) => ({ @@ -1016,9 +1085,85 @@ const server = http.createServer(async (req, res) => { await serveStatic(req, res); }); +async function syncKeycloakSessionSettings(retries = 12, delayMs = 15000) { + for (let attempt = 1; attempt <= retries; attempt++) { + try { + console.log(`[Keycloak Sync] SSO session duration automation check (Attempt ${attempt}/${retries})...`); + + const inspect = await dockerRequest('/containers/auth/json').catch(() => null); + if (!inspect) { + console.log('[Keycloak Sync] "auth" container not found or Docker socket inaccessible. Skipping settings automation.'); + return; + } + + const state = inspect.State?.Status || inspect.state; + if (state !== 'running') { + console.log(`[Keycloak Sync] "auth" container is in status "${state}". Waiting for it to run...`); + if (attempt < retries) { + await new Promise((r) => setTimeout(r, delayMs)); + continue; + } + return; + } + + const env = inspect.Config?.Env || []; + const adminUser = env.find(e => e.startsWith('KC_BOOTSTRAP_ADMIN_USERNAME='))?.split('=')[1] + || env.find(e => e.startsWith('KEYCLOAK_ADMIN='))?.split('=')[1] + || 'admin'; + const adminPass = env.find(e => e.startsWith('KC_BOOTSTRAP_ADMIN_PASSWORD='))?.split('=')[1] + || env.find(e => e.startsWith('KEYCLOAK_ADMIN_PASSWORD='))?.split('=')[1]; + + if (!adminPass) { + console.log('[Keycloak Sync] Could not extract admin password from container environment. Skipping settings automation.'); + return; + } + + console.log('[Keycloak Sync] Authenticating kcadm.sh inside Keycloak container...'); + const loginCmd = [ + '/opt/keycloak/bin/kcadm.sh', + 'config', + 'credentials', + '--server', 'http://localhost:8080', + '--realm', 'master', + '--user', adminUser, + '--password', adminPass + ]; + await execContainerCommand('auth', loginCmd); + + console.log('[Keycloak Sync] Updating session settings in the "mischlabs" realm...'); + const updateCmd = [ + '/opt/keycloak/bin/kcadm.sh', + 'update', + 'realms/mischlabs', + '-s', 'ssoSessionIdleTimeout=2592000', // 30 days + '-s', 'ssoSessionMaxLifespan=7776000', // 90 days + '-s', 'ssoSessionIdleTimeoutRememberMe=2592000', // 30 days + '-s', 'ssoSessionMaxLifespanRememberMe=7776000', // 90 days + '-s', 'offlineSessionIdleTimeout=7776000', // 90 days + '-s', 'offlineSessionMaxLifespan=15552000', // 180 days + '-s', 'offlineSessionMaxLifespanEnabled=true' + ]; + const updateOutput = await execContainerCommand('auth', updateCmd); + console.log('[Keycloak Sync] Realm settings updated successfully:', updateOutput); + return; + } catch (error) { + console.warn(`[Keycloak Sync] Attempt ${attempt} failed: ${error.message}`); + if (attempt < retries) { + console.log(`[Keycloak Sync] Retrying in ${delayMs / 1000} seconds...`); + await new Promise((r) => setTimeout(r, delayMs)); + } else { + console.error('[Keycloak Sync] All attempts to automate Keycloak session settings failed.'); + } + } + } +} + server.listen(PORT, () => { console.log(`MischLabs dashboard listening on ${PORT}`); cleanupManualWatchtowerContainers().catch((error) => { console.warn(`Manual Watchtower cleanup failed: ${error.message}`); }); + syncKeycloakSessionSettings().catch((error) => { + console.warn(`Keycloak session synchronization failed: ${error.message}`); + }); });