feat: automate Keycloak session settings config on dashboard startup
All checks were successful
Build & Push Docker Image to Gitea Registry / build-and-push (push) Successful in 11s

This commit is contained in:
Kroonk
2026-05-21 22:31:04 +02:00
parent 0d4377aed2
commit 60ed7d624e

145
server.js
View File

@@ -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() { async function getContainers() {
const containers = await dockerRequest('/containers/json?all=0'); const containers = await dockerRequest('/containers/json?all=0');
return containers.map((container) => ({ return containers.map((container) => ({
@@ -1016,9 +1085,85 @@ const server = http.createServer(async (req, res) => {
await serveStatic(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, () => { server.listen(PORT, () => {
console.log(`MischLabs dashboard listening on ${PORT}`); console.log(`MischLabs dashboard listening on ${PORT}`);
cleanupManualWatchtowerContainers().catch((error) => { cleanupManualWatchtowerContainers().catch((error) => {
console.warn(`Manual Watchtower cleanup failed: ${error.message}`); console.warn(`Manual Watchtower cleanup failed: ${error.message}`);
}); });
syncKeycloakSessionSettings().catch((error) => {
console.warn(`Keycloak session synchronization failed: ${error.message}`);
});
}); });