keycloak-api-manager 3.2.1 → 4.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/.env.example +27 -0
  2. package/Handlers/clientsHandler.js +240 -30
  3. package/Handlers/groupsHandler.js +16 -1
  4. package/Handlers/httpApiHelper.js +87 -0
  5. package/Handlers/realmsHandler.js +26 -4
  6. package/Handlers/usersHandler.js +43 -10
  7. package/README.md +341 -10
  8. package/index.js +159 -29
  9. package/package.json +3 -14
  10. package/test/.mocharc.json +4 -0
  11. package/test/TESTING.md +327 -0
  12. package/test/config/CONFIGURATION.md +170 -0
  13. package/test/config/default.json +36 -0
  14. package/test/config/local.json.example +7 -0
  15. package/test/config/secrets.json.example +7 -0
  16. package/test/diagnostic-protocol-mappers.js +189 -0
  17. package/test/docker-keycloak/DEPLOYMENT_GUIDE.md +262 -0
  18. package/test/docker-keycloak/certs/.gitkeep +7 -0
  19. package/test/docker-keycloak/docker-compose-https.yml +50 -0
  20. package/test/docker-keycloak/docker-compose.yml +59 -0
  21. package/test/docker-keycloak/setup-keycloak.js +501 -0
  22. package/test/enableServerFeatures.js +315 -0
  23. package/test/helpers/config.js +218 -0
  24. package/test/helpers/docker-helpers.js +513 -0
  25. package/test/helpers/setup.js +186 -0
  26. package/test/package.json +18 -0
  27. package/test/setup.js +194 -0
  28. package/test/specs/authenticationManagement.test.js +224 -0
  29. package/test/specs/clientCredentials.test.js +76 -0
  30. package/test/specs/clientScopes.test.js +388 -0
  31. package/test/specs/clients.test.js +791 -0
  32. package/test/specs/components.test.js +151 -0
  33. package/test/specs/debugClientLibrary.test.js +88 -0
  34. package/test/specs/groups.test.js +362 -0
  35. package/test/specs/identityProviders.test.js +292 -0
  36. package/test/specs/realms.test.js +390 -0
  37. package/test/specs/roles.test.js +322 -0
  38. package/test/specs/users.test.js +445 -0
  39. package/test/testConfig.js +69 -0
  40. package/.mocharc.json +0 -7
  41. package/docker-compose.yml +0 -27
  42. package/test/authenticationManagement.test.js +0 -329
  43. package/test/clientScopes.test.js +0 -256
  44. package/test/clients.test.js +0 -284
  45. package/test/components.test.js +0 -122
  46. package/test/config.js +0 -137
  47. package/test/docker-helpers.js +0 -111
  48. package/test/groups.test.js +0 -284
  49. package/test/identityProviders.test.js +0 -197
  50. package/test/mocha.env.js +0 -55
  51. package/test/realms.test.js +0 -349
  52. package/test/roles.test.js +0 -215
  53. package/test/users.test.js +0 -405
@@ -0,0 +1,50 @@
1
+ version: '3.8'
2
+
3
+ # Keycloak HTTPS Configuration
4
+ # Use this file with docker-compose -f docker-compose-https.yml for HTTPS deployments
5
+ #
6
+ # Environment Variables Required:
7
+ # KEYCLOAK_CERT_PATH: Path to directory containing keycloak.crt and keycloak.key
8
+ # KEYCLOAK_HOSTNAME: Hostname for Keycloak (e.g., smart-dell-sml.crs4.it)
9
+
10
+ services:
11
+ keycloak:
12
+ image: keycloak/keycloak:latest
13
+ container_name: keycloak-test
14
+ ports:
15
+ # HTTP port (for backward compatibility)
16
+ - "8080:8080"
17
+ # HTTPS port
18
+ - "8443:8443"
19
+ environment:
20
+ KEYCLOAK_ADMIN: admin # Master realm admin username
21
+ KEYCLOAK_ADMIN_PASSWORD: admin # Master realm admin password
22
+ KC_DB: dev-mem # In-memory database (development only)
23
+ KC_METRICS_ENABLED: 'false' # Disable metrics in dev
24
+ KC_HEALTH_ENABLED: 'true' # Enable health checks
25
+ KC_HOSTNAME: ${KEYCLOAK_HOSTNAME:-keycloak.local}
26
+ KC_SCHEME: https # Use HTTPS
27
+ KC_HTTP_PORT: 8080
28
+ KC_HTTPS_PORT: 8443
29
+ # HTTPS configuration
30
+ KC_HTTP_ENABLED: 'true' # Admin console accessible via HTTP too
31
+ KC_PROXY: reencrypt # Trust reverse proxy headers
32
+ volumes:
33
+ # Mount certificates for HTTPS
34
+ - "${KEYCLOAK_CERT_PATH}/keycloak.crt:/etc/keycloak/certs/keycloak.crt:ro"
35
+ - "${KEYCLOAK_CERT_PATH}/keycloak.key:/etc/keycloak/certs/keycloak.key:ro"
36
+ command:
37
+ - start-dev
38
+ - "--https-certificate-file=/etc/keycloak/certs/keycloak.crt"
39
+ - "--https-certificate-key-file=/etc/keycloak/certs/keycloak.key"
40
+ healthcheck:
41
+ test: ["CMD", "curl", "-fk", "https://localhost:8443/health/ready"]
42
+ interval: 5s
43
+ timeout: 5s
44
+ retries: 12 # Wait up to 60 seconds for Keycloak to be ready
45
+ networks:
46
+ - keycloak-network
47
+
48
+ networks:
49
+ keycloak-network:
50
+ driver: bridge
@@ -0,0 +1,59 @@
1
+ version: '3.8'
2
+
3
+ # Keycloak Test Server Configuration
4
+ # Supports both HTTP (local dev) and HTTPS (production-like setup)
5
+ #
6
+ # Usage:
7
+ # HTTP (local): docker-compose up -d
8
+ # HTTPS (remote): KEYCLOAK_HTTPS=true KEYCLOAK_CERT_PATH=/path/to/certs docker-compose up -d
9
+ #
10
+ # Environment Variables:
11
+ # KEYCLOAK_HTTPS: 'true' to enable HTTPS, 'false' for HTTP (default: false)
12
+ # KEYCLOAK_CERT_PATH: Path to directory containing certificate files (required if KEYCLOAK_HTTPS=true)
13
+ # KEYCLOAK_HOSTNAME: Hostname for Keycloak (default: localhost)
14
+ # KEYCLOAK_HTTPS_PORT: HTTPS port (default: 8443)
15
+
16
+ services:
17
+ keycloak:
18
+ image: keycloak/keycloak:latest
19
+ container_name: keycloak-test
20
+ ports:
21
+ # HTTP port (always enabled for admin console fallback)
22
+ - "8080:8080"
23
+ # HTTPS port (when enabled)
24
+ - "${KEYCLOAK_HTTPS_PORT:-8443}:8443"
25
+ environment:
26
+ KEYCLOAK_ADMIN: admin # Master realm admin username
27
+ KEYCLOAK_ADMIN_PASSWORD: admin # Master realm admin password
28
+ KC_DB: dev-mem # In-memory database (development only)
29
+ KC_METRICS_ENABLED: 'false' # Disable metrics in dev
30
+ KC_HEALTH_ENABLED: 'true' # Enable health checks
31
+ KC_HOSTNAME: ${KEYCLOAK_HOSTNAME:-localhost}
32
+ KC_SCHEME: ${KEYCLOAK_SCHEME:-http}
33
+ KC_HTTP_PORT: 8080
34
+ KC_HTTPS_PORT: 8443
35
+ # HTTPS configuration (only applied if KC_SCHEME=https)
36
+ KC_HTTP_ENABLED: 'true' # Admin console accessible via HTTP too
37
+ KC_PROXY: reencrypt # Trust reverse proxy headers
38
+ volumes:
39
+ # Mount certificates if HTTPS is enabled and path is provided
40
+ - "${KEYCLOAK_CERT_PATH:-./certs}:/etc/keycloak/certs:ro"
41
+ command:
42
+ - start-dev
43
+ # Note: To add HTTPS flags dynamically, use environment variable substitution
44
+ # For HTTPS mode, add these flags before start-dev:
45
+ # - --https-certificate-file=/etc/keycloak/certs/keycloak.crt
46
+ # - --https-certificate-key-file=/etc/keycloak/certs/keycloak.key
47
+ healthcheck:
48
+ test: ["CMD", "curl", "-f", "http://localhost:8080/health/ready"]
49
+ interval: 5s
50
+ timeout: 5s
51
+ retries: 12 # Wait up to 60 seconds for Keycloak to be ready
52
+ networks:
53
+ - keycloak-network
54
+
55
+ networks:
56
+ keycloak-network:
57
+ driver: bridge
58
+
59
+
@@ -0,0 +1,501 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Keycloak Container Setup Script
5
+ *
6
+ * Prompts user to choose local or remote deployment and configures Keycloak accordingly.
7
+ *
8
+ * Usage:
9
+ * npm run setup-keycloak
10
+ * node test/setup-keycloak.js
11
+ *
12
+ * Features:
13
+ * - Interactive prompts for deployment location
14
+ * - Local deployment with HTTP or HTTPS support
15
+ * - Remote SSH deployment with automatic docker-compose copying
16
+ * - Certificate configuration for HTTPS mode
17
+ */
18
+
19
+ const path = require('path');
20
+ const { spawn, exec } = require('child_process');
21
+ const fs = require('fs');
22
+ const os = require('os');
23
+ const readline = require('readline');
24
+
25
+ // ANSI color codes for terminal output
26
+ const colors = {
27
+ reset: '\x1b[0m',
28
+ bright: '\x1b[1m',
29
+ green: '\x1b[32m',
30
+ yellow: '\x1b[33m',
31
+ blue: '\x1b[36m',
32
+ red: '\x1b[31m',
33
+ };
34
+
35
+ // Detect docker compose command (docker-compose or docker compose)
36
+ function getDockerComposeCmdSync() {
37
+ try {
38
+ require('child_process').execSync('docker-compose --version', { stdio: 'ignore' });
39
+ return 'docker-compose';
40
+ } catch (err) {
41
+ try {
42
+ require('child_process').execSync('docker compose version', { stdio: 'ignore' });
43
+ return 'docker compose';
44
+ } catch (err2) {
45
+ return null; // Docker not available
46
+ }
47
+ }
48
+ }
49
+
50
+ let DOCKER_COMPOSE_CMD = getDockerComposeCmdSync();
51
+
52
+ let rl;
53
+
54
+ function initReadline() {
55
+ if (!rl) {
56
+ rl = readline.createInterface({
57
+ input: process.stdin,
58
+ output: process.stdout,
59
+ terminal: process.stdin.isTTY
60
+ });
61
+ }
62
+ return rl;
63
+ }
64
+
65
+ function log(message, color = 'reset') {
66
+ console.log(`${colors[color]}${message}${colors.reset}`);
67
+ }
68
+
69
+ async function prompt(question) {
70
+ return new Promise((resolve) => {
71
+ const interface = initReadline();
72
+ interface.question(question, (answer) => {
73
+ resolve(answer.trim());
74
+ });
75
+ });
76
+ }
77
+
78
+ async function askDeploymentLocation() {
79
+ log('\n=== Keycloak Container Deployment Setup ===\n', 'bright');
80
+ log('Choose deployment location:', 'blue');
81
+
82
+ const dockerAvailable = DOCKER_COMPOSE_CMD !== null;
83
+
84
+ if (dockerAvailable) {
85
+ log(' 1) Local machine (localhost:8080)', 'yellow');
86
+ log(' 2) Remote machine via SSH', 'yellow');
87
+ } else {
88
+ log(' ⚠ Docker not available locally', 'yellow');
89
+ log(' → Deploying to remote machine via SSH', 'yellow');
90
+ return 'remote';
91
+ }
92
+
93
+ let choice;
94
+ while (!['1', '2'].includes(choice)) {
95
+ choice = await prompt('\nEnter choice (1 or 2): ');
96
+ if (!['1', '2'].includes(choice)) {
97
+ log('Invalid choice. Please enter 1 or 2.', 'red');
98
+ }
99
+ }
100
+
101
+ return choice === '1' ? 'local' : 'remote';
102
+ }
103
+
104
+ async function askHttpsSetup() {
105
+ log('\nEnable HTTPS?', 'blue');
106
+ log(' 1) No, use HTTP (development)', 'yellow');
107
+ log(' 2) Yes, use HTTPS (production-like)', 'yellow');
108
+
109
+ let choice;
110
+ while (!['1', '2'].includes(choice)) {
111
+ choice = await prompt('\nEnter choice (1 or 2): ');
112
+ if (!['1', '2'].includes(choice)) {
113
+ log('Invalid choice. Please enter 1 or 2.', 'red');
114
+ }
115
+ }
116
+
117
+ return choice === '2';
118
+ }
119
+
120
+ async function askRemoteDetails() {
121
+ log('\nRemote Deployment Target', 'blue');
122
+ log(' Specify the user and machine where Keycloak will be deployed:', 'yellow');
123
+ log(' Format: username@hostname', 'yellow');
124
+ log(' Example: user@miodomino.it', 'yellow');
125
+
126
+ const host = await prompt('\nRemote host/IP (user@hostname): ');
127
+ if (!host) {
128
+ throw new Error('Host is required');
129
+ }
130
+
131
+ return { host };
132
+ }
133
+
134
+ async function askCertificatePath(isRemote = false) {
135
+ log('\nHTTPS requires certificate files.', 'blue');
136
+
137
+ // Check for default certificate files in test/docker-keycloak/certs
138
+ const defaultCertPath = path.join(__dirname, 'certs');
139
+ const defaultCertFile = path.join(defaultCertPath, 'keycloak.crt');
140
+ const defaultKeyFile = path.join(defaultCertPath, 'keycloak.key');
141
+
142
+ // For remote deployments, always use certificates from local test/docker-keycloak/certs
143
+ // and copy them to the remote server
144
+ if (isRemote) {
145
+ // Verify local certificates exist
146
+ if (!fs.existsSync(defaultCertFile) || !fs.existsSync(defaultKeyFile)) {
147
+ throw new Error(`Certificate files not found in ${defaultCertPath}\nExpected: keycloak.crt and keycloak.key`);
148
+ }
149
+ log(`✓ Using certificates from ${defaultCertPath}`, 'green');
150
+ log(' → Will be copied to remote server', 'yellow');
151
+ return { localPath: defaultCertPath, remotePath: null };
152
+ }
153
+
154
+ // Local deployment - check for defaults first
155
+ if (fs.existsSync(defaultCertFile) && fs.existsSync(defaultKeyFile)) {
156
+ log(`✓ Found default certificates in ${defaultCertPath}`, 'green');
157
+ return { localPath: defaultCertPath, remotePath: null };
158
+ }
159
+
160
+ // If defaults not found, ask user for custom path
161
+ log(` Default location: ${defaultCertPath}`, 'yellow');
162
+ log(' Certificate files needed: keycloak.crt and keycloak.key', 'yellow');
163
+
164
+ const certPath = await prompt('Certificate directory path (or press Enter for default): ');
165
+
166
+ // Use default if user just pressed Enter
167
+ if (!certPath) {
168
+ if (fs.existsSync(defaultCertFile) && fs.existsSync(defaultKeyFile)) {
169
+ log(`✓ Using default certificates from ${defaultCertPath}`, 'green');
170
+ return { localPath: defaultCertPath, remotePath: null };
171
+ }
172
+ throw new Error('Certificate path is required for HTTPS');
173
+ }
174
+
175
+ // Verify custom certificate files
176
+ const certFile = path.join(certPath, 'keycloak.crt');
177
+ const keyFile = path.join(certPath, 'keycloak.key');
178
+
179
+ if (!fs.existsSync(certFile) || !fs.existsSync(keyFile)) {
180
+ throw new Error(`Certificate files not found in ${certPath}\nExpected: keycloak.crt and keycloak.key`);
181
+ }
182
+
183
+ return { localPath: certPath, remotePath: null };
184
+ }
185
+
186
+ function executeCommand(command, cwd) {
187
+ return new Promise((resolve, reject) => {
188
+ const child = spawn('bash', ['-c', command], {
189
+ cwd: cwd || process.cwd(),
190
+ stdio: 'inherit',
191
+ });
192
+
193
+ child.on('close', (code) => {
194
+ if (code === 0) {
195
+ resolve();
196
+ } else {
197
+ reject(new Error(`Command failed with exit code ${code}: ${command}`));
198
+ }
199
+ });
200
+
201
+ child.on('error', (err) => {
202
+ reject(err);
203
+ });
204
+ });
205
+ }
206
+
207
+ function execSync(command, cwd) {
208
+ return new Promise((resolve, reject) => {
209
+ const options = {};
210
+ if (cwd) {
211
+ options.cwd = cwd;
212
+ }
213
+ exec(command, options, (error, stdout, stderr) => {
214
+ if (error) {
215
+ reject(error);
216
+ } else {
217
+ resolve(stdout.trim());
218
+ }
219
+ });
220
+ });
221
+ }
222
+
223
+ function updateTestBaseUrl(baseUrl) {
224
+ const configPath = path.join(__dirname, '..', 'config', 'default.json');
225
+ const raw = fs.readFileSync(configPath, 'utf8');
226
+ const config = JSON.parse(raw);
227
+ if (!config.test || !config.test.keycloak) {
228
+ throw new Error('test.keycloak not found in test/config/default.json');
229
+ }
230
+ config.test.keycloak.baseUrl = baseUrl;
231
+ fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`);
232
+ log(`✓ Updated test baseUrl: ${baseUrl}`, 'green');
233
+ }
234
+
235
+ async function deployLocal(useHttps, certPath) {
236
+ log('\n=== Local Deployment ===\n', 'bright');
237
+
238
+ const dockerComposeDir = __dirname;
239
+
240
+ try {
241
+ // Determine which compose file to use
242
+ const composeFile = useHttps ? 'docker-compose-https.yml' : 'docker-compose.yml';
243
+ const composeCmd = `docker-compose -f ${composeFile}`;
244
+
245
+ if (useHttps) {
246
+ log('Starting Keycloak with HTTPS...', 'blue');
247
+
248
+ // Write .env file for HTTPS
249
+ const finalCertPath = certPath && certPath.localPath ? certPath.localPath : certPath;
250
+ const envContent = `KEYCLOAK_CERT_PATH=${finalCertPath}
251
+ KEYCLOAK_HOSTNAME=localhost
252
+ `;
253
+
254
+ fs.writeFileSync(path.join(dockerComposeDir, '.env'), envContent);
255
+ log('Created .env file with HTTPS configuration', 'green');
256
+ } else {
257
+ log('Starting Keycloak with HTTP...', 'blue');
258
+
259
+ // Remove .env file for HTTP (use defaults)
260
+ const envFilePath = path.join(dockerComposeDir, '.env');
261
+ if (fs.existsSync(envFilePath)) {
262
+ fs.unlinkSync(envFilePath);
263
+ }
264
+ log('Using default HTTP configuration', 'green');
265
+ }
266
+
267
+ // Stop any existing containers
268
+ log('Stopping existing containers...', 'blue');
269
+ try {
270
+ await executeCommand(`${composeCmd} down 2>/dev/null || true`, dockerComposeDir);
271
+ } catch (err) {
272
+ // Ignore errors
273
+ }
274
+
275
+ // Start containers
276
+ log(`Starting Keycloak with ${useHttps ? 'HTTPS' : 'HTTP'}...`, 'blue');
277
+ await executeCommand(`${composeCmd} up -d`, dockerComposeDir);
278
+ log(`\n✓ Keycloak is starting locally...`, 'green');
279
+
280
+ log('\nWaiting for Keycloak to be ready...', 'blue');
281
+ let ready = false;
282
+ let attempts = 0;
283
+ const maxAttempts = 60;
284
+
285
+ // For HTTPS health check, we need to use -k flag to skip certificate validation
286
+ const healthCheckProtocol = useHttps ? 'https' : 'http';
287
+
288
+ while (!ready && attempts < maxAttempts) {
289
+ try {
290
+ // Health check always on localhost:8080 HTTP for simplicity
291
+ const health = await execSync(`curl -s http://localhost:8080/health/ready 2>/dev/null`);
292
+ if (health.includes('UP')) {
293
+ ready = true;
294
+ }
295
+ } catch (err) {
296
+ // Still waiting
297
+ }
298
+
299
+ if (!ready) {
300
+ attempts++;
301
+ process.stdout.write('.');
302
+ await new Promise(resolve => setTimeout(resolve, 1000));
303
+ }
304
+ }
305
+
306
+ if (ready) {
307
+ log('\n\n✓ Keycloak is ready!', 'green');
308
+ log('\nAccess Keycloak:', 'bright');
309
+ if (useHttps) {
310
+ log(` Admin Console: https://localhost:8443`, 'yellow');
311
+ log(` (self-signed cert - use -k with curl or add to browser exceptions)`, 'yellow');
312
+ } else {
313
+ log(` Admin Console: http://localhost:8080`, 'yellow');
314
+ }
315
+ log(' Credentials: admin / admin', 'yellow');
316
+ } else {
317
+ log('\n\n⚠ Keycloak is taking longer than expected. Check Docker logs:', 'yellow');
318
+ log(` ${composeCmd} logs -f`, 'yellow');
319
+ }
320
+
321
+ } catch (err) {
322
+ log(`\n✗ Error during local deployment: ${err.message}`, 'red');
323
+ throw err;
324
+ }
325
+ }
326
+
327
+ async function deployRemote(host, deployPath, useHttps, certPath) {
328
+ log('\n=== Remote Deployment ===\n', 'bright');
329
+
330
+ const dockerComposeDir = __dirname;
331
+ const dockerComposePath = path.join(dockerComposeDir, 'docker-compose.yml');
332
+ const dockerComposeHttpsPath = path.join(dockerComposeDir, 'docker-compose-https.yml');
333
+
334
+ try {
335
+ // Verify SSH connection
336
+ log(`Verifying SSH connection to ${host}...`, 'blue');
337
+ await execSync(`ssh -o ConnectTimeout=5 ${host} 'echo OK'`);
338
+ log('✓ SSH connection successful', 'green');
339
+
340
+ // Create deployment directory on remote
341
+ log(`Creating deployment directory on remote: ${deployPath}`, 'blue');
342
+ await execSync(`ssh ${host} 'mkdir -p "${deployPath}"'`);
343
+ log('✓ Directory created', 'green');
344
+
345
+ // Copy docker-compose files to remote
346
+ log('Copying docker-compose files to remote...', 'blue');
347
+ await execSync(`scp "${dockerComposePath}" "${host}:${deployPath}/docker-compose.yml"`);
348
+ await execSync(`scp "${dockerComposeHttpsPath}" "${host}:${deployPath}/docker-compose-https.yml"`);
349
+ log('✓ docker-compose files copied', 'green');
350
+
351
+ // Copy certificate files if HTTPS and certificates are on local machine
352
+ if (useHttps && certPath && certPath.localPath) {
353
+ log('Copying certificate files to remote...', 'blue');
354
+ const remoteDir = `${deployPath}/certs`;
355
+ await execSync(`ssh ${host} 'mkdir -p "${remoteDir}"'`);
356
+
357
+ const certFile = path.join(certPath.localPath, 'keycloak.crt');
358
+ const keyFile = path.join(certPath.localPath, 'keycloak.key');
359
+
360
+ await execSync(`scp "${certFile}" "${host}:${remoteDir}/"`);
361
+ await execSync(`scp "${keyFile}" "${host}:${remoteDir}/"`);
362
+ log('✓ Certificate files copied', 'green');
363
+ }
364
+
365
+ // Determine which compose file to use
366
+ const composeFile = useHttps ? 'docker-compose-https.yml' : 'docker-compose.yml';
367
+ const composeCmd = DOCKER_COMPOSE_CMD ? `${DOCKER_COMPOSE_CMD} -f ${composeFile}` : `docker compose -f ${composeFile}`;
368
+
369
+ // Create .env file on remote
370
+ log('Creating configuration on remote...', 'blue');
371
+
372
+ let envContent = '';
373
+ const hostname = host.includes('@') ? host.split('@')[1] : host;
374
+
375
+ if (useHttps) {
376
+ // Use remotePath if certificates are already on server, otherwise use copied path
377
+ const certPathForEnv = certPath.remotePath || `${deployPath}/certs`;
378
+ envContent = `KEYCLOAK_CERT_PATH=${certPathForEnv}
379
+ KEYCLOAK_HOSTNAME=${hostname}
380
+ `;
381
+ } else {
382
+ envContent = `KEYCLOAK_HOSTNAME=${hostname}
383
+ `;
384
+ }
385
+
386
+ const envBase64 = Buffer.from(envContent).toString('base64');
387
+ await execSync(`ssh ${host} 'echo "${envBase64}" | base64 -d > "${deployPath}/.env"'`);
388
+ log('✓ Configuration created', 'green');
389
+
390
+ // Stop any existing containers at the remote path
391
+ log('Stopping existing containers...', 'blue');
392
+ try {
393
+ await execSync(`ssh ${host} 'cd "${deployPath}" && ${DOCKER_COMPOSE_CMD} -f docker-compose.yml down 2>/dev/null || ${DOCKER_COMPOSE_CMD} -f docker-compose-https.yml down 2>/dev/null || true'`);
394
+ // Force remove the container in case down didn't work
395
+ await execSync(`ssh ${host} 'docker rm -f keycloak-test 2>/dev/null || true'`);
396
+ } catch (err) {
397
+ // Ignore errors if containers don't exist
398
+ }
399
+ log('✓ Old containers stopped', 'green');
400
+
401
+ // Start new containers
402
+ log(`Starting Keycloak container with ${useHttps ? 'HTTPS' : 'HTTP'}...`, 'blue');
403
+ await execSync(`ssh ${host} 'cd "${deployPath}" && ${composeCmd} up -d'`);
404
+ log('✓ Keycloak container started', 'green');
405
+
406
+ log('\nWaiting for Keycloak to be ready...', 'blue');
407
+ let ready = false;
408
+ let attempts = 0;
409
+ const maxAttempts = 60;
410
+
411
+ while (!ready && attempts < maxAttempts) {
412
+ try {
413
+ // Health check always uses HTTP on 8080 for simplicity
414
+ const checkCmd = `ssh ${host} 'curl -s http://localhost:8080/health/ready 2>/dev/null'`;
415
+ const health = await execSync(checkCmd);
416
+ if (health.includes('UP')) {
417
+ ready = true;
418
+ }
419
+ } catch (err) {
420
+ // Still waiting
421
+ }
422
+
423
+ if (!ready) {
424
+ attempts++;
425
+ process.stdout.write('.');
426
+ await new Promise(resolve => setTimeout(resolve, 1000));
427
+ }
428
+ }
429
+
430
+ if (ready) {
431
+ log('\n\n✓ Keycloak is ready!', 'green');
432
+ log('\nAccess Keycloak:', 'bright');
433
+ if (useHttps) {
434
+ log(` Admin Console: https://${hostname}:8443`, 'yellow');
435
+ log(` (self-signed cert - use -k with curl or add to browser exceptions)`, 'yellow');
436
+ } else {
437
+ log(` Admin Console: http://${hostname}:8080`, 'yellow');
438
+ }
439
+ log(' Credentials: admin / admin', 'yellow');
440
+ log(`\nDeployed at: ${host}:${deployPath}`, 'yellow');
441
+ } else {
442
+ log('\n\n⚠ Keycloak is taking longer than expected. Logs:', 'yellow');
443
+ log(` ssh ${host} 'cd ${deployPath} && ${composeCmd} logs -f'`, 'yellow');
444
+ }
445
+
446
+ } catch (err) {
447
+ log(`\n✗ Error during remote deployment: ${err.message}`, 'red');
448
+ throw err;
449
+ }
450
+ }
451
+
452
+ async function main() {
453
+ try {
454
+ const deployLocation = await askDeploymentLocation();
455
+ const useHttps = await askHttpsSetup();
456
+
457
+ let certPath = null;
458
+ if (useHttps) {
459
+ // For remote deployments, certificate path doesn't need to exist locally yet
460
+ const isRemote = deployLocation === 'remote';
461
+ certPath = await askCertificatePath(isRemote);
462
+ }
463
+
464
+ if (deployLocation === 'local') {
465
+ await deployLocal(useHttps, certPath);
466
+ const protocol = useHttps ? 'https' : 'http';
467
+ const port = useHttps ? 8443 : 8080;
468
+ updateTestBaseUrl(`${protocol}://localhost:${port}`);
469
+ } else {
470
+ const { host } = await askRemoteDetails();
471
+
472
+ // Extract username from host (format: user@host or just host)
473
+ let username = 'root';
474
+ if (host.includes('@')) {
475
+ username = host.split('@')[0];
476
+ }
477
+
478
+ // Automatically create deployment path: /home/<username>/docker-keycloak-api-manager-test
479
+ const deployPath = `/home/${username}/docker-keycloak-api-manager-test`;
480
+ log(`\n✓ Deployment path: ${deployPath}`, 'green');
481
+
482
+ await deployRemote(host, deployPath, useHttps, certPath);
483
+ const protocol = useHttps ? 'https' : 'http';
484
+ const port = useHttps ? 8443 : 8080;
485
+ const hostname = host.includes('@') ? host.split('@')[1] : host;
486
+ updateTestBaseUrl(`${protocol}://${hostname}:${port}`);
487
+ }
488
+
489
+ log('\n✓ Deployment complete!\n', 'green');
490
+
491
+ } catch (err) {
492
+ log(`\nSetup failed: ${err.message}\n`, 'red');
493
+ process.exit(1);
494
+ } finally {
495
+ if (rl) {
496
+ rl.close();
497
+ }
498
+ }
499
+ }
500
+
501
+ main();