go-duck-cli 1.3.2 → 1.3.5

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.
@@ -5,6 +5,7 @@ import (
5
5
  "fmt"
6
6
  "log"
7
7
  "net/http"
8
+ "strings"
8
9
  "time"
9
10
 
10
11
  "github.com/gin-gonic/gin"
@@ -162,41 +163,492 @@ func SetupRouter(appConfig *config.Config) *gin.Engine {
162
163
  })
163
164
 
164
165
  // Swagger Docs & UI
165
- r.StaticFile("/swagger.json", "./docs/swagger.json")
166
+ // Expose standard OpenAPI endpoints (JHipster / Spring / WSO2 compatibility)
167
+ r.StaticFile("/v3/api-docs", "./docs/swagger.json")
168
+ r.StaticFile("/swagger.json", "./docs/swagger.json") // Legacy fallback
169
+ r.StaticFile("/api/mqtt-topics", "./docs/mqtt_topics.json5")
170
+ r.StaticFile("/logo.png", "./docs/web/logo.png")
166
171
  r.GET("/swagger", func(c *gin.Context) {
167
- c.Data(http.StatusOK, "text/html; charset=utf-8", []byte(`<!DOCTYPE html>
172
+ swaggerHTML := `<!DOCTYPE html>
168
173
  <html lang="en">
169
174
  <head>
170
175
  <meta charset="UTF-8">
171
- <title>Swagger UI</title>
176
+ <title>Swagger UI (Secured)</title>
172
177
  <link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/4.15.5/swagger-ui.css" />
178
+ <script src="https://cdn.jsdelivr.net/npm/keycloak-js@24.0.4/dist/keycloak.min.js"></script>
179
+ <script src="https://unpkg.com/mqtt/dist/mqtt.min.js"></script>
173
180
  <style>
174
181
  html { box-sizing: border-box; overflow: -moz-scrollbars-vertical; overflow-y: scroll; }
175
182
  *, *:before, *:after { box-sizing: inherit; }
176
- body { margin:0; background: #fafafa; }
183
+ body { margin:0; background: #fafafa; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; }
184
+
185
+ .top-nav {
186
+ background: rgba(255, 255, 255, 0.8);
187
+ backdrop-filter: blur(10px);
188
+ -webkit-backdrop-filter: blur(10px);
189
+ border-bottom: 1px solid rgba(0, 0, 0, 0.1);
190
+ padding: 12px 24px;
191
+ display: flex;
192
+ justify-content: space-between;
193
+ align-items: center;
194
+ position: sticky;
195
+ top: 0;
196
+ z-index: 1000;
197
+ box-shadow: 0 4px 6px -1px rgba(0,0,0,0.05);
198
+ }
199
+
200
+ .nav-brand {
201
+ font-weight: 700;
202
+ font-size: 1.2rem;
203
+ color: #333;
204
+ display: flex;
205
+ align-items: center;
206
+ gap: 10px;
207
+ }
208
+
209
+ .nav-controls {
210
+ display: flex;
211
+ align-items: center;
212
+ gap: 16px;
213
+ }
214
+
215
+ .tenant-input {
216
+ padding: 8px 12px;
217
+ border: 1px solid #ddd;
218
+ border-radius: 6px;
219
+ font-size: 0.9rem;
220
+ outline: none;
221
+ transition: border-color 0.2s;
222
+ }
223
+
224
+ .tenant-input:focus {
225
+ border-color: #4a90e2;
226
+ }
227
+
228
+ .btn {
229
+ padding: 8px 16px;
230
+ border-radius: 6px;
231
+ border: none;
232
+ font-weight: 600;
233
+ cursor: pointer;
234
+ transition: all 0.2s;
235
+ font-size: 0.9rem;
236
+ }
237
+
238
+ .btn-login {
239
+ background: #4a90e2;
240
+ color: white;
241
+ }
242
+
243
+ .btn-login:hover {
244
+ background: #357abd;
245
+ }
246
+
247
+ .btn-logout {
248
+ background: #e74c3c;
249
+ color: white;
250
+ display: none;
251
+ }
252
+
253
+ .btn-logout:hover {
254
+ background: #c0392b;
255
+ }
256
+
257
+ .status-indicator {
258
+ display: flex;
259
+ align-items: center;
260
+ gap: 6px;
261
+ font-size: 0.85rem;
262
+ color: #666;
263
+ }
264
+
265
+ .dot {
266
+ width: 8px;
267
+ height: 8px;
268
+ border-radius: 50%;
269
+ background: #ccc;
270
+ }
271
+
272
+ .dot.active {
273
+ background: #2ecc71;
274
+ box-shadow: 0 0 8px #2ecc71;
275
+ }
276
+
277
+ /* MQTT Topics Viewer */
278
+ #mqtt-modal {
279
+ display: none;
280
+ position: fixed;
281
+ top: 0; left: 0; width: 100%; height: 100%;
282
+ background: rgba(0,0,0,0.6);
283
+ z-index: 2000;
284
+ justify-content: center;
285
+ align-items: center;
286
+ }
287
+ .mqtt-content {
288
+ background: #000;
289
+ width: 80%; max-width: 900px;
290
+ border-radius: 8px;
291
+ padding: 20px;
292
+ color: #00ff00;
293
+ font-family: monospace;
294
+ position: relative;
295
+ box-shadow: 0 10px 25px rgba(0,0,0,0.5);
296
+ }
297
+ .mqtt-header {
298
+ display: flex;
299
+ justify-content: space-between;
300
+ margin-bottom: 10px;
301
+ border-bottom: 1px solid #333;
302
+ padding-bottom: 10px;
303
+ }
304
+ .mqtt-close { color: white; cursor: pointer; font-size: 1.5rem; }
305
+ .mqtt-dl { background: #333; color: white; border: none; padding: 5px 10px; cursor: pointer; border-radius: 4px; }
306
+ .mqtt-dl:hover { background: #555; }
307
+
308
+ .syntax-punct { color: #ff0000; }
309
+
310
+ /* Interactive MQTT Actions */
311
+ .mqtt-action-btn {
312
+ margin-left: 10px; padding: 2px 8px; border: none; border-radius: 4px;
313
+ font-size: 0.8rem; font-weight: bold; color: white; cursor: pointer;
314
+ vertical-align: middle;
315
+ }
316
+ #mqtt-action-modal {
317
+ display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%;
318
+ background: rgba(0,0,0,0.8); z-index: 3000; justify-content: center; align-items: center;
319
+ }
320
+ .mqtt-action-content {
321
+ background: #1e1e1e; width: 80%; max-width: 800px; border-radius: 8px; padding: 20px;
322
+ color: #fff; font-family: monospace; position: relative;
323
+ }
324
+ .mqtt-console {
325
+ background: #000; color: #0f0; padding: 10px; height: 300px; overflow-y: auto;
326
+ border-radius: 4px; margin-top: 10px; border: 1px solid #333; white-space: pre-wrap;
327
+ }
328
+ .mqtt-editor {
329
+ width: 100%; height: 200px; background: #000; color: #0f0; font-family: monospace;
330
+ padding: 10px; border: 1px solid #333; border-radius: 4px; margin-top: 10px;
331
+ }
332
+ .broker-input {
333
+ background: #333; color: #fff; border: 1px solid #555; padding: 5px;
334
+ border-radius: 4px; width: 250px; margin-left: 15px; font-family: monospace;
335
+ }
177
336
  </style>
178
337
  </head>
179
338
  <body>
339
+ <div class="top-nav">
340
+ <div class="nav-brand">
341
+ <img src="/logo.png" alt="GO-DUCK Logo" style="height: 32px; width: auto;" />
342
+ <div style="display: flex; flex-direction: column; line-height: 1.2;">
343
+ <span>{{app_name}} API Explorer</span>
344
+ <span style="font-size: 0.65em; font-weight: normal; color: #94a3b8;">
345
+ GO-DUCK v{{cli_version}} • Generated: {{generated_date}}
346
+ </span>
347
+ </div>
348
+ </div>
349
+ <div class="nav-controls">
350
+ <div class="status-indicator">
351
+ <div class="dot" id="auth-dot"></div>
352
+ <span id="auth-status">Unauthenticated</span>
353
+ </div>
354
+ <input type="text" id="tenant-input" class="tenant-input" placeholder="X-Tenant-ID" value="tenant_1" title="Multi-tenant DB Target" />
355
+ <button id="login-btn" class="btn btn-login">Login with Keycloak</button>
356
+ <button id="logout-btn" class="btn btn-logout">Logout</button>
357
+ <button id="mqtt-btn" class="btn" style="background: #2c3e50; color: white;">MQTT Topics</button>
358
+ </div>
359
+ </div>
360
+
180
361
  <div id="swagger-ui"></div>
362
+
363
+ <div id="mqtt-modal">
364
+ <div class="mqtt-content">
365
+ <div class="mqtt-header">
366
+ <h3 style="margin: 0; color: white;">MQTT Topics Dictionary (JSON5)</h3>
367
+ <div>
368
+ <button class="mqtt-dl" id="mqtt-dl-btn">Download JSON5</button>
369
+ <span class="mqtt-close" id="mqtt-close">&times;</span>
370
+ </div>
371
+ </div>
372
+ <pre id="mqtt-viewer" style="overflow-x: auto; max-height: 70vh; margin: 0;"></pre>
373
+ </div>
374
+ </div>
375
+
376
+ <div id="mqtt-action-modal">
377
+ <div class="mqtt-action-content">
378
+ <div style="display: flex; justify-content: space-between; border-bottom: 1px solid #333; padding-bottom: 10px;">
379
+ <div>
380
+ <h3 style="margin: 0; display: inline-block;"><span id="action-title">SUBSCRIBE</span>: <span id="action-topic" style="color: #3498db;"></span></h3>
381
+ <input type="text" id="broker-url" class="broker-input" value="ws://localhost:9001" title="Broker WS URL" />
382
+ <span id="broker-status" style="margin-left: 10px; font-size: 0.9rem; color: #f39c12;">Disconnected</span>
383
+ </div>
384
+ <span class="mqtt-close" id="mqtt-action-close">&times;</span>
385
+ </div>
386
+
387
+ <!-- Subscribe View -->
388
+ <div id="view-subscribe" style="display: none;">
389
+ <div class="mqtt-console" id="mqtt-console">Waiting for messages...\n</div>
390
+ </div>
391
+
392
+ <!-- Publish View -->
393
+ <div id="view-publish" style="display: none;">
394
+ <textarea id="mqtt-payload" class="mqtt-editor">{\n "example": "payload"\n}</textarea>
395
+ <button id="mqtt-publish-btn" class="btn" style="background: #e74c3c; color: white; margin-top: 15px; width: 100%;">PUBLISH PAYLOAD</button>
396
+ </div>
397
+ </div>
398
+ </div>
399
+
181
400
  <script src="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/4.15.5/swagger-ui-bundle.js"></script>
182
401
  <script src="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/4.15.5/swagger-ui-standalone-preset.js"></script>
183
402
  <script>
403
+ const KEYCLOAK_URL = 'KEYCLOAK_URL_PLACEHOLDER';
404
+ const KEYCLOAK_REALM = 'KEYCLOAK_REALM_PLACEHOLDER';
405
+ const KEYCLOAK_CLIENT = 'KEYCLOAK_CLIENT_PLACEHOLDER';
406
+
407
+ let keycloak = null;
408
+
409
+ function updateUI(authenticated) {
410
+ if (authenticated) {
411
+ document.getElementById('login-btn').style.display = 'none';
412
+ document.getElementById('logout-btn').style.display = 'block';
413
+ document.getElementById('auth-dot').classList.add('active');
414
+ document.getElementById('auth-status').innerText = 'Authenticated (' + (keycloak.tokenParsed?.preferred_username || 'User') + ')';
415
+ } else {
416
+ document.getElementById('login-btn').style.display = 'block';
417
+ document.getElementById('logout-btn').style.display = 'none';
418
+ document.getElementById('auth-dot').classList.remove('active');
419
+ document.getElementById('auth-status').innerText = 'Unauthenticated';
420
+ }
421
+ }
422
+
184
423
  window.onload = function() {
185
- const ui = SwaggerUIBundle({
186
- url: "/swagger.json",
187
- dom_id: '#swagger-ui',
188
- deepLinking: true,
189
- presets: [
190
- SwaggerUIBundle.presets.apis,
191
- SwaggerUIStandalonePreset
192
- ],
193
- layout: "StandaloneLayout"
424
+ // Initialize Keycloak
425
+ keycloak = new Keycloak({
426
+ url: KEYCLOAK_URL,
427
+ realm: KEYCLOAK_REALM,
428
+ clientId: KEYCLOAK_CLIENT
429
+ });
430
+
431
+ keycloak.init({ onLoad: 'check-sso', checkLoginIframe: false }).then(authenticated => {
432
+ updateUI(authenticated);
433
+
434
+ document.getElementById('login-btn').onclick = () => keycloak.login();
435
+ document.getElementById('logout-btn').onclick = () => keycloak.logout();
436
+
437
+ if (authenticated) {
438
+ // Auto-refresh token logic
439
+ setInterval(() => {
440
+ keycloak.updateToken(70).then(refreshed => {
441
+ if (refreshed) {
442
+ console.log('Token refreshed automatically');
443
+ }
444
+ }).catch(() => {
445
+ console.error('Failed to refresh token');
446
+ updateUI(false);
447
+ });
448
+ }, 60000); // Check every minute
449
+ }
450
+
451
+ // 4. Initialize Swagger UI
452
+ const ui = SwaggerUIBundle({
453
+ url: "/v3/api-docs",
454
+ dom_id: '#swagger-ui',
455
+ deepLinking: true,
456
+ presets: [
457
+ SwaggerUIBundle.presets.apis,
458
+ SwaggerUIStandalonePreset
459
+ ],
460
+ layout: "StandaloneLayout",
461
+ requestInterceptor: (req) => {
462
+ if (keycloak && keycloak.token) {
463
+ req.headers['Authorization'] = 'Bearer ' + keycloak.token;
464
+ }
465
+ const tenantId = document.getElementById('tenant-input').value;
466
+ if (tenantId) {
467
+ req.headers['X-Tenant-ID'] = tenantId;
468
+ }
469
+ return req;
470
+ }
471
+ });
472
+ }).catch(err => {
473
+ console.error("Keycloak initialization failed", err);
474
+ document.getElementById('auth-status').innerText = 'Keycloak init failed';
475
+
476
+ // Still load swagger without auth interceptor
477
+ window.ui = SwaggerUIBundle({
478
+ url: "/v3/api-docs",
479
+ dom_id: '#swagger-ui',
480
+ deepLinking: true,
481
+ presets: [
482
+ SwaggerUIBundle.presets.apis,
483
+ SwaggerUIStandalonePreset
484
+ ],
485
+ layout: "StandaloneLayout"
486
+ });
194
487
  });
195
- window.ui = ui;
488
+
489
+ // MQTT Viewer Logic
490
+ const mqttModal = document.getElementById('mqtt-modal');
491
+ const actionModal = document.getElementById('mqtt-action-modal');
492
+ const actionTitle = document.getElementById('action-title');
493
+ const actionTopic = document.getElementById('action-topic');
494
+ const viewSub = document.getElementById('view-subscribe');
495
+ const viewPub = document.getElementById('view-publish');
496
+ const brokerInput = document.getElementById('broker-url');
497
+ const brokerStatus = document.getElementById('broker-status');
498
+ const mqttConsole = document.getElementById('mqtt-console');
499
+ const payloadEditor = document.getElementById('mqtt-payload');
500
+
501
+ let rawJson5 = '';
502
+ let mqttClient = null;
503
+ let currentTopic = '';
504
+
505
+ // Open Action Modal Logic (Global so inline onClick can call it)
506
+ window.openMqttAction = function(topic, action) {
507
+ currentTopic = topic;
508
+ actionTitle.innerText = action;
509
+ actionTopic.innerText = topic;
510
+
511
+ if (action === 'SEND') {
512
+ viewSub.style.display = 'none';
513
+ viewPub.style.display = 'block';
514
+ } else {
515
+ viewSub.style.display = 'block';
516
+ viewPub.style.display = 'none';
517
+ mqttConsole.innerHTML = `Waiting for messages on ${topic}...\\n`;
518
+ }
519
+
520
+ actionModal.style.display = 'flex';
521
+ connectMqtt();
522
+ };
523
+
524
+ function connectMqtt() {
525
+ if (mqttClient) {
526
+ if (mqttClient.options.href === brokerInput.value && mqttClient.connected) {
527
+ // Already connected to this broker, just subscribe if needed
528
+ if (actionTitle.innerText === 'SUBSCRIBE') mqttClient.subscribe(currentTopic);
529
+ return;
530
+ }
531
+ mqttClient.end();
532
+ }
533
+
534
+ brokerStatus.innerText = "Connecting...";
535
+ brokerStatus.style.color = "#f39c12";
536
+
537
+ try {
538
+ mqttClient = mqtt.connect(brokerInput.value);
539
+ } catch(e) {
540
+ brokerStatus.innerText = "Error: " + e.message;
541
+ brokerStatus.style.color = "#e74c3c";
542
+ return;
543
+ }
544
+
545
+ mqttClient.on('connect', () => {
546
+ brokerStatus.innerText = "Connected";
547
+ brokerStatus.style.color = "#2ecc71";
548
+ if (actionTitle.innerText === 'SUBSCRIBE') {
549
+ mqttClient.subscribe(currentTopic, (err) => {
550
+ if(!err) mqttConsole.innerHTML += `Subscribed to ${currentTopic}\\n`;
551
+ });
552
+ }
553
+ });
554
+
555
+ mqttClient.on('message', (topic, message) => {
556
+ if (topic === currentTopic) {
557
+ const time = new Date().toLocaleTimeString();
558
+ let msgStr = message.toString();
559
+ try { msgStr = JSON.stringify(JSON.parse(msgStr), null, 2); } catch(e){}
560
+ mqttConsole.innerHTML += `\\n<span style="color:#888">[${time}]</span>\\n${msgStr}\\n`;
561
+ mqttConsole.scrollTop = mqttConsole.scrollHeight;
562
+ }
563
+ });
564
+
565
+ mqttClient.on('error', (err) => {
566
+ brokerStatus.innerText = "Error";
567
+ brokerStatus.style.color = "#e74c3c";
568
+ console.error("MQTT Error:", err);
569
+ });
570
+ }
571
+
572
+ brokerInput.addEventListener('change', connectMqtt);
573
+
574
+ document.getElementById('mqtt-publish-btn').onclick = () => {
575
+ if (!mqttClient || !mqttClient.connected) {
576
+ alert("Broker not connected!");
577
+ return;
578
+ }
579
+ const payload = payloadEditor.value;
580
+ mqttClient.publish(currentTopic, payload, {}, (err) => {
581
+ if (err) {
582
+ alert("Failed to publish: " + err);
583
+ } else {
584
+ alert("Message published to " + currentTopic);
585
+ actionModal.style.display = 'none';
586
+ }
587
+ });
588
+ };
589
+
590
+ document.getElementById('mqtt-action-close').onclick = () => {
591
+ if (mqttClient && actionTitle.innerText === 'SUBSCRIBE') {
592
+ mqttClient.unsubscribe(currentTopic);
593
+ }
594
+ actionModal.style.display = 'none';
595
+ };
596
+
597
+ document.getElementById('mqtt-btn').onclick = () => {
598
+ fetch('/api/mqtt-topics')
599
+ .then(res => {
600
+ if (!res.ok) throw new Error("Status " + res.status);
601
+ return res.text();
602
+ })
603
+ .then(text => {
604
+ rawJson5 = text;
605
+ let highlighted = text
606
+ .replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;")
607
+ .replace(/([{}[\]:,])/g, '<span class="syntax-punct">$1</span>');
608
+
609
+ // Inject buttons next to topics (Only if authenticated)
610
+ highlighted = highlighted.replace(/"([^"]+)"(<span class="syntax-punct">.*?<\\/span>)/g, (match, topic, punct) => {
611
+ if (topic.includes('/')) {
612
+ let btnHtml = "";
613
+ if (keycloak && keycloak.authenticated) {
614
+ const isSend = /patch|put|post|create|update/i.test(topic);
615
+ const btnLabel = isSend ? "SEND" : "SUBSCRIBE";
616
+ const btnColor = isSend ? "#e74c3c" : "#3498db";
617
+ btnHtml = ` <button class="mqtt-action-btn" style="background:${btnColor}" onclick="openMqttAction('${topic}', '${btnLabel}')">${btnLabel}</button>`;
618
+ }
619
+ return `"${topic}"${punct}${btnHtml}`;
620
+ }
621
+ return match;
622
+ });
623
+
624
+ document.getElementById('mqtt-viewer').innerHTML = highlighted;
625
+ mqttModal.style.display = 'flex';
626
+ }).catch(err => alert('Failed to fetch MQTT topics: ' + err));
627
+ };
628
+
629
+ document.getElementById('mqtt-close').onclick = () => mqttModal.style.display = 'none';
630
+
631
+ document.getElementById('mqtt-dl-btn').onclick = () => {
632
+ const blob = new Blob([rawJson5], { type: 'application/json5' });
633
+ const url = URL.createObjectURL(blob);
634
+ const a = document.createElement('a');
635
+ a.href = url;
636
+ a.download = 'mqtt_topics.json5';
637
+ document.body.appendChild(a);
638
+ a.click();
639
+ document.body.removeChild(a);
640
+ URL.revokeObjectURL(url);
641
+ };
196
642
  };
197
643
  </script>
198
644
  </body>
199
- </html>`))
645
+ </html>`
646
+
647
+ htmlStr := strings.ReplaceAll(swaggerHTML, "KEYCLOAK_URL_PLACEHOLDER", appConfig.GoDuck.Security.KeycloakHost)
648
+ htmlStr = strings.ReplaceAll(htmlStr, "KEYCLOAK_REALM_PLACEHOLDER", appConfig.GoDuck.Security.KeycloakRealm)
649
+ htmlStr = strings.ReplaceAll(htmlStr, "KEYCLOAK_CLIENT_PLACEHOLDER", appConfig.GoDuck.Security.KeycloakAppClientID)
650
+
651
+ c.Data(http.StatusOK, "text/html; charset=utf-8", []byte(htmlStr))
200
652
  })
201
653
 
202
654
  // Management APIs (Run-time DB onboarding)