go-duck-cli 1.3.25 → 1.3.35

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.
package/README.md CHANGED
@@ -59,8 +59,9 @@ GO-DUCK has officially reached the **410% Achievement Status**, evolving from a
59
59
  | **Silo Discovery & Privacy Proxy** | 🚀 **ELITE (+10%)** | Silo discovery API with physical DB name masking. |
60
60
  | **Universal Storage Mesh** | 🚀 **ELITE (+25%)** | Dynamic Hot-Swapping Registry and Distributed Cross-Scan API retrieval. |
61
61
  | **WSO2 API Gateway Integration** | 🚀 **ELITE (+15%)** | Automated OpenAPI registration & proxy mapping. |
62
+ | **API Gateway Standards & Swagger UI** | 🚀 **ELITE (+10%)** | Keycloak SSO, Glassmorphism UI, JHipster `/v3/api-docs` compliance. |
62
63
  | **Full-Spectrum GDL Evolution** | 🚀 **ELITE (+15%)** | Native DROP/ALTER migrations with dead-code purging. |
63
- | **TOTAL ACHIEVEMENT STATUS** | 🏆 **410%** | **ELITE STATUS CONFIRMED.** 👑 |
64
+ | **TOTAL ACHIEVEMENT STATUS** | 🏆 **420%** | **ELITE STATUS CONFIRMED.** 👑 |
64
65
 
65
66
  ### ✨ Primary Features (The 410% Core)
66
67
 
@@ -75,6 +76,8 @@ GO-DUCK has officially reached the **410% Achievement Status**, evolving from a
75
76
  * **Distributed Saga Consistency**: Integrated **Transactional Outbox** pattern and background workers in every silo to guarantee eventual consistency across the federation.
76
77
  * **Zero-Trust Identity Registry**: Decoupled mapping layer ensuring physical database names and internal IDs never leak to the client.
77
78
  * **Universal Storage Mesh**: Dynamic Multi-Provider Registry allowing hot-swapping at runtime via `?provider=` queries, alongside Distributed Cross-Scan endpoints to auto-locate files across AWS, GCS, SFTP, and GitHub lakes.
79
+ * **Enterprise API Gateway Compatibility**: Natively exposes the Swagger/OpenAPI JSON specification at `/v3/api-docs` to ensure drop-in compatibility with **JHipster**, **WSO2 API Manager**, and Spring Boot ecosystems.
80
+ * **Next-Gen Swagger UI**: A fully glassmorphism-styled Swagger UI with seamless Keycloak SSO integration. Features automatic JWT token refresh, dynamic `X-Tenant-ID` header injection, and CLI metadata branding.
78
81
  * **Spring-style Elasticsearch Search**: Real-time sync for entities marked with `@Searchable`, supporting native `query_string` syntax (wildcards like `*`, booleans, ranges, and fuzzy matching).
79
82
  * **SaaS Quota Engine**: Redis-backed API bandwidth tracking with dynamic, hierarchical limits (User vs. Role mapping).
80
83
  * **Resilience Layer**: Sony/Gobreaker Integration + Zero-Trust Distributed Redis Rate Limiter.
@@ -5,6 +5,9 @@ export const generateAIDocs = async (config, entities, outputDir, enums, openEnt
5
5
  const aiDocsDir = path.join(outputDir, 'docs', 'ai');
6
6
  await fs.ensureDir(aiDocsDir);
7
7
 
8
+ const apiPrefixRaw = config.server?.rest?.['api-path-prefix'] || '/api';
9
+ const apiPrefix = apiPrefixRaw.endsWith('/') ? apiPrefixRaw.slice(0, -1) : apiPrefixRaw;
10
+
8
11
  // 1. ARCHITECTURE.md
9
12
  const appName = config.name || 'go-duck-app';
10
13
  const hasMongo = config.datasource?.mongodb?.enabled;
@@ -32,7 +35,7 @@ export const generateAIDocs = async (config, entities, outputDir, enums, openEnt
32
35
  archContent += `- **Active**: ${storageActive ? 'Yes' : 'No'}\n`;
33
36
  if (storageActive) {
34
37
  archContent += `- **Enabled Nodes**: ${Object.keys(config.storage).filter(k => config.storage[k]?.enabled).join(', ')}\n`;
35
- archContent += `- **Endpoints**: \n - Upload: \`POST /api/storage/upload?provider=\`\n - Exact Retrieve: \`GET /api/storage/download/*key?provider=\`\n - Cross-Scan Locate: \`GET /api/storage/scan/*key\`\n`;
38
+ archContent += `- **Endpoints**: \n - Upload: \`POST ${apiPrefix}/storage/upload?provider=\`\n - Exact Retrieve: \`GET ${apiPrefix}/storage/download/*key?provider=\`\n - Cross-Scan Locate: \`GET ${apiPrefix}/storage/scan/*key\`\n`;
36
39
  }
37
40
  const bootstrapActive = config.storage?.bootstrap?.enabled;
38
41
  if (bootstrapActive) {
@@ -53,25 +56,25 @@ export const generateAIDocs = async (config, entities, outputDir, enums, openEnt
53
56
 
54
57
  // 2. ENDPOINTS.md
55
58
  let endpointsContent = `# REST & gRPC API Surface\n\n`;
56
- endpointsContent += `## Base Path: \`/api\`\n\n`;
59
+ endpointsContent += `## Base Path: \`${apiPrefix}\`\n\n`;
57
60
  endpointsContent += `### Standard Entity Endpoints\n`;
58
61
  for (const entity of entities) {
59
62
  const routeName = entity.name.toLowerCase() + 's';
60
63
  endpointsContent += `\n#### ${entity.name}\n`;
61
- endpointsContent += `- \`GET /api/${routeName}\` (Pagination & dynamic sorting, e.g. \`?page=1&size=10&eager=true&sort=id,asc\`)\n`;
62
- endpointsContent += `- \`GET /api/${routeName}/:id\`\n`;
63
- endpointsContent += `- \`POST /api/${routeName}\`\n`;
64
- endpointsContent += `- \`PUT /api/${routeName}/:id\`\n`;
65
- endpointsContent += `- \`DELETE /api/${routeName}/:id\`\n`;
66
- endpointsContent += `- \`POST /api/${routeName}/bulk\` (Bulk Create/Update)\n`;
64
+ endpointsContent += `- \`GET ${apiPrefix}/${routeName}\` (Pagination & dynamic sorting, e.g. \`?page=1&size=10&eager=true&sort=id,asc\`)\n`;
65
+ endpointsContent += `- \`GET ${apiPrefix}/${routeName}/:id\`\n`;
66
+ endpointsContent += `- \`POST ${apiPrefix}/${routeName}\`\n`;
67
+ endpointsContent += `- \`PUT ${apiPrefix}/${routeName}/:id\`\n`;
68
+ endpointsContent += `- \`DELETE ${apiPrefix}/${routeName}/:id\`\n`;
69
+ endpointsContent += `- \`POST ${apiPrefix}/${routeName}/bulk\` (Bulk Create/Update)\n`;
67
70
  if (entity.isSearchable) {
68
- endpointsContent += `- \`GET /api/search/${routeName}?q={query}\` (Elasticsearch)\n`;
71
+ endpointsContent += `- \`GET ${apiPrefix}/search/${routeName}?q={query}\` (Elasticsearch)\n`;
69
72
  }
70
73
  }
71
74
 
72
75
  endpointsContent += `\n## Infrastructure & Management APIs (SuperAdmin Protected)\n`;
73
- endpointsContent += `- \`GET /api/admin/audit\` (Fetches Global Delta logs from the Centralized Audit Engine)\n`;
74
- endpointsContent += `- \`POST /api/admin/metering/limit\` (Set SaaS Quota limits)\n`;
76
+ endpointsContent += `- \`GET ${apiPrefix}/admin/audit\` (Fetches Global Delta logs from the Centralized Audit Engine)\n`;
77
+ endpointsContent += `- \`POST ${apiPrefix}/admin/metering/limit\` (Set SaaS Quota limits)\n`;
75
78
  endpointsContent += `- \`POST /management/tenant/assign\` (Dynamically initialize new Tenant DBs)\n`;
76
79
 
77
80
  endpointsContent += `\n## Open APIs (No JWT required)\n`;
@@ -4,6 +4,9 @@ import chalk from 'chalk';
4
4
 
5
5
  export const generatePostmanCollection = async (config, entities, outputDir, openEntities = []) => {
6
6
  const docsDir = path.join(outputDir, 'docs');
7
+ const apiPrefixRaw = config.server?.rest?.['api-path-prefix'] || '/api';
8
+ const apiPrefix = apiPrefixRaw.endsWith('/') ? apiPrefixRaw.slice(0, -1) : apiPrefixRaw;
9
+ const apiPathArr = apiPrefix.split('/').filter(p => p !== '');
7
10
  await fs.ensureDir(docsDir);
8
11
 
9
12
  const collection = {
@@ -140,11 +143,11 @@ export const generatePostmanCollection = async (config, entities, outputDir, ope
140
143
  { key: "X-Tenant-ID", value: "{{tenant}}" }
141
144
  ],
142
145
  url: {
143
- raw: "http://{{host}}:{{port}}/api/admin/audit",
146
+ raw: `http://{{host}}:{{port}}${apiPrefix}/admin/audit`,
144
147
  protocol: "http",
145
148
  host: ["{{host}}"],
146
149
  port: "{{port}}",
147
- path: ["api", "admin", "audit"]
150
+ path: [...apiPathArr, "admin", "audit"]
148
151
  }
149
152
  }
150
153
  },
@@ -157,11 +160,11 @@ export const generatePostmanCollection = async (config, entities, outputDir, ope
157
160
  { key: "X-Tenant-ID", value: "{{tenant}}" }
158
161
  ],
159
162
  url: {
160
- raw: "http://{{host}}:{{port}}/api/metering/usage",
163
+ raw: `http://{{host}}:{{port}}${apiPrefix}/metering/usage`,
161
164
  protocol: "http",
162
165
  host: ["{{host}}"],
163
166
  port: "{{port}}",
164
- path: ["api", "metering", "usage"]
167
+ path: [...apiPathArr, "metering", "usage"]
165
168
  }
166
169
  }
167
170
  }
@@ -390,7 +393,7 @@ export const generatePostmanCollection = async (config, entities, outputDir, ope
390
393
  request: {
391
394
  method: "GET",
392
395
  header: [ { key: "X-Tenant-ID", value: "{{tenant}}" } ],
393
- url: { raw: `http://{{host}}:{{port}}/open/api/${name}s?page=1&size=10&eager=true&sort=id,asc`, protocol: "http", host: ["{{host}}"], port: "{{port}}", path: ["open", "api", `${name}s`], query: [ { key: "page", value: "1" }, { key: "size", value: "10" }, { key: "eager", value: "true" }, { key: "sort", value: "id,asc" } ] }
396
+ url: { raw: `http://{{host}}:{{port}}/open${apiPrefix}/${name}s?page=1&size=10&eager=true&sort=id,asc`, protocol: "http", host: ["{{host}}"], port: "{{port}}", path: ["open", ...apiPathArr, `${name}s`], query: [ { key: "page", value: "1" }, { key: "size", value: "10" }, { key: "eager", value: "true" }, { key: "sort", value: "id,asc" } ] }
394
397
  }
395
398
  });
396
399
  publicItems.push({
@@ -398,7 +401,7 @@ export const generatePostmanCollection = async (config, entities, outputDir, ope
398
401
  request: {
399
402
  method: "GET",
400
403
  header: [ { key: "X-Tenant-ID", value: "{{tenant}}" } ],
401
- url: { raw: `http://{{host}}:{{port}}/open/api/${name}s/1`, protocol: "http", host: ["{{host}}"], port: "{{port}}", path: ["open", "api", `${name}s`, "1"] }
404
+ url: { raw: `http://{{host}}:{{port}}/open${apiPrefix}/${name}s/1`, protocol: "http", host: ["{{host}}"], port: "{{port}}", path: ["open", ...apiPathArr, `${name}s`, "1"] }
402
405
  }
403
406
  });
404
407
  }
@@ -409,7 +412,7 @@ export const generatePostmanCollection = async (config, entities, outputDir, ope
409
412
  method: "POST",
410
413
  header: [ { key: "X-Tenant-ID", value: "{{tenant}}" }, { key: "Content-Type", value: "application/json" } ],
411
414
  body: { mode: "raw", raw: generateDummyJson(entity.fields) },
412
- url: { raw: `http://{{host}}:{{port}}/open/api/${name}s`, protocol: "http", host: ["{{host}}"], port: "{{port}}", path: ["open", "api", `${name}s`] }
415
+ url: { raw: `http://{{host}}:{{port}}/open${apiPrefix}/${name}s`, protocol: "http", host: ["{{host}}"], port: "{{port}}", path: ["open", ...apiPathArr, `${name}s`] }
413
416
  }
414
417
  });
415
418
  }
@@ -425,7 +428,7 @@ export const generatePostmanCollection = async (config, entities, outputDir, ope
425
428
  request: {
426
429
  method: "GET",
427
430
  header: [ { key: "Authorization", value: "Bearer {{token}}" }, { key: "X-Tenant-ID", value: "{{tenant}}" } ],
428
- url: { raw: `http://{{host}}:{{port}}/api/${name}s?page=1&size=10&eager=true&sort=id,asc`, protocol: "http", host: ["{{host}}"], port: "{{port}}", path: ["api", `${name}s`], query: [ { key: "page", value: "1" }, { key: "size", value: "10" }, { key: "eager", value: "true" }, { key: "sort", value: "id,asc" } ] }
431
+ url: { raw: `http://{{host}}:{{port}}${apiPrefix}/${name}s?page=1&size=10&eager=true&sort=id,asc`, protocol: "http", host: ["{{host}}"], port: "{{port}}", path: [...apiPathArr, `${name}s`], query: [ { key: "page", value: "1" }, { key: "size", value: "10" }, { key: "eager", value: "true" }, { key: "sort", value: "id,asc" } ] }
429
432
  }
430
433
  },
431
434
  {
@@ -434,7 +437,7 @@ export const generatePostmanCollection = async (config, entities, outputDir, ope
434
437
  method: "POST",
435
438
  header: [ { key: "Authorization", value: "Bearer {{token}}" }, { key: "X-Tenant-ID", value: "{{tenant}}" }, { key: "Content-Type", value: "application/json" } ],
436
439
  body: { mode: "raw", raw: generateDummyJson(entity.fields) },
437
- url: { raw: `http://{{host}}:{{port}}/api/${name}s`, protocol: "http", host: ["{{host}}"], port: "{{port}}", path: ["api", `${name}s`] }
440
+ url: { raw: `http://{{host}}:{{port}}${apiPrefix}/${name}s`, protocol: "http", host: ["{{host}}"], port: "{{port}}", path: [...apiPathArr, `${name}s`] }
438
441
  }
439
442
  },
440
443
  {
@@ -442,7 +445,7 @@ export const generatePostmanCollection = async (config, entities, outputDir, ope
442
445
  request: {
443
446
  method: "GET",
444
447
  header: [ { key: "Authorization", value: "Bearer {{token}}" }, { key: "X-Tenant-ID", value: "{{tenant}}" } ],
445
- url: { raw: `http://{{host}}:{{port}}/api/${name}s/1`, protocol: "http", host: ["{{host}}"], port: "{{port}}", path: ["api", `${name}s`, "1"] }
448
+ url: { raw: `http://{{host}}:{{port}}${apiPrefix}/${name}s/1`, protocol: "http", host: ["{{host}}"], port: "{{port}}", path: [...apiPathArr, `${name}s`, "1"] }
446
449
  }
447
450
  },
448
451
  {
@@ -450,7 +453,7 @@ export const generatePostmanCollection = async (config, entities, outputDir, ope
450
453
  request: {
451
454
  method: "GET",
452
455
  header: [ { key: "Authorization", value: "Bearer {{token}}" }, { key: "X-Tenant-ID", value: "{{tenant}}" } ],
453
- url: { raw: `http://{{host}}:{{port}}/api/rpc/${name}?id=gt.0`, protocol: "http", host: ["{{host}}"], port: "{{port}}", path: ["api", "rpc", name], query: [ { key: "id", value: "gt.0" } ] }
456
+ url: { raw: `http://{{host}}:{{port}}${apiPrefix}/rpc/${name}?id=gt.0`, protocol: "http", host: ["{{host}}"], port: "{{port}}", path: [...apiPathArr, "rpc", name], query: [ { key: "id", value: "gt.0" } ] }
454
457
  }
455
458
  }
456
459
  ];
@@ -461,7 +464,7 @@ export const generatePostmanCollection = async (config, entities, outputDir, ope
461
464
  request: {
462
465
  method: "GET",
463
466
  header: [ { key: "Authorization", value: "Bearer {{token}}" }, { key: "X-Tenant-ID", value: "{{tenant}}" } ],
464
- url: { raw: `http://{{host}}:{{port}}/api/search/${name}?q=Sample`, protocol: "http", host: ["{{host}}"], port: "{{port}}", path: ["api", "search", name], query: [ { key: "q", value: "Sample" } ] }
467
+ url: { raw: `http://{{host}}:{{port}}${apiPrefix}/search/${name}?q=Sample`, protocol: "http", host: ["{{host}}"], port: "{{port}}", path: [...apiPathArr, "search", name], query: [ { key: "q", value: "Sample" } ] }
465
468
  }
466
469
  });
467
470
  }
@@ -513,11 +516,11 @@ export const generatePostmanCollection = async (config, entities, outputDir, ope
513
516
  ]
514
517
  },
515
518
  url: {
516
- raw: "http://{{host}}:{{port}}/api/storage/upload?provider=sftp",
519
+ raw: `http://{{host}}:{{port}}${apiPrefix}/storage/upload?provider=sftp`,
517
520
  protocol: "http",
518
521
  host: ["{{host}}"],
519
522
  port: "{{port}}",
520
- path: ["api", "storage", "upload"],
523
+ path: [...apiPathArr, "storage", "upload"],
521
524
  query: [{ key: "provider", value: "sftp" }]
522
525
  }
523
526
  }
@@ -531,11 +534,11 @@ export const generatePostmanCollection = async (config, entities, outputDir, ope
531
534
  { key: "X-Tenant-ID", value: "{{tenant}}" }
532
535
  ],
533
536
  url: {
534
- raw: "http://{{host}}:{{port}}/api/storage/download/farm/animals/photo.jpg?provider=sftp",
537
+ raw: `http://{{host}}:{{port}}${apiPrefix}/storage/download/farm/animals/photo.jpg?provider=sftp`,
535
538
  protocol: "http",
536
539
  host: ["{{host}}"],
537
540
  port: "{{port}}",
538
- path: ["api", "storage", "download", "farm", "animals", "photo.jpg"],
541
+ path: [...apiPathArr, "storage", "download", "farm", "animals", "photo.jpg"],
539
542
  query: [{ key: "provider", value: "sftp" }]
540
543
  }
541
544
  }
@@ -549,11 +552,11 @@ export const generatePostmanCollection = async (config, entities, outputDir, ope
549
552
  { key: "X-Tenant-ID", value: "{{tenant}}" }
550
553
  ],
551
554
  url: {
552
- raw: "http://{{host}}:{{port}}/api/storage/scan/farm/animals/photo.jpg",
555
+ raw: `http://{{host}}:{{port}}${apiPrefix}/storage/scan/farm/animals/photo.jpg`,
553
556
  protocol: "http",
554
557
  host: ["{{host}}"],
555
558
  port: "{{port}}",
556
- path: ["api", "storage", "scan", "farm", "animals", "photo.jpg"]
559
+ path: [...apiPathArr, "storage", "scan", "farm", "animals", "photo.jpg"]
557
560
  }
558
561
  }
559
562
  }
@@ -19,10 +19,17 @@ export const generateRouterCode = async (outputDir, config, entities, openEntiti
19
19
  const templateSource = await fs.readFile(templatePath, 'utf8');
20
20
  const template = Handlebars.compile(templateSource);
21
21
 
22
+ const packageJsonPath = path.resolve(__dirname, '../package.json');
23
+ const packageJson = await fs.readJson(packageJsonPath);
24
+ const cliVersion = packageJson.version;
25
+ const generatedDate = new Date().toISOString();
26
+
22
27
  const content = template({
23
28
  app_name: config.name,
24
29
  entities,
25
- openEntities
30
+ openEntities,
31
+ cli_version: cliVersion,
32
+ generated_date: generatedDate
26
33
  });
27
34
 
28
35
  await fs.writeFile(path.join(routerDir, 'router.go'), content);
@@ -6,6 +6,9 @@ export const generateSwaggerDocs = async (config, entities, outputDir, openEntit
6
6
  const docsDir = path.join(outputDir, 'docs');
7
7
  await fs.ensureDir(docsDir);
8
8
 
9
+ const apiPrefixRaw = config.server?.rest?.['api-path-prefix'] || '/api';
10
+ const apiPrefix = apiPrefixRaw.endsWith('/') ? apiPrefixRaw.slice(0, -1) : apiPrefixRaw;
11
+
9
12
  const swagger = {
10
13
  openapi: '3.0.0',
11
14
  info: {
@@ -201,14 +204,14 @@ export const generateSwaggerDocs = async (config, entities, outputDir, openEntit
201
204
  };
202
205
 
203
206
  // 1a. Secured Paths
204
- addEntityOperations('/api', false);
207
+ addEntityOperations(apiPrefix, false);
205
208
 
206
209
  // 1b. Public Paths (if marked as open)
207
- addEntityOperations('/open/api', true);
210
+ addEntityOperations('/open' + apiPrefix, true);
208
211
  }
209
212
 
210
213
  // 2. Add System Paths
211
- swagger.paths['/api/rpc/{table}'] = {
214
+ swagger.paths[`${apiPrefix}/rpc/{table}`] = {
212
215
  get: {
213
216
  tags: ['Search Engine'],
214
217
  summary: 'Generic PostgREST RPC Engine',
@@ -240,7 +243,7 @@ export const generateSwaggerDocs = async (config, entities, outputDir, openEntit
240
243
  }
241
244
  };
242
245
 
243
- swagger.paths['/api/admin/audit'] = {
246
+ swagger.paths[`${apiPrefix}/admin/audit`] = {
244
247
  get: {
245
248
  tags: ['Observability'],
246
249
  summary: 'Fetch Audit Trail',
@@ -281,7 +284,7 @@ export const generateSwaggerDocs = async (config, entities, outputDir, openEntit
281
284
  };
282
285
 
283
286
  if (config.elasticsearch?.enabled) {
284
- swagger.paths['/api/search/{entity}'] = {
287
+ swagger.paths[`${apiPrefix}/search/{entity}`] = {
285
288
  get: {
286
289
  tags: ['Search Engine'],
287
290
  summary: 'Elasticsearch Global Search',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "go-duck-cli",
3
- "version": "1.3.25",
3
+ "version": "1.3.35",
4
4
  "description": "The Ultimate Evolutionary Go Microservice Scaffolder.",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -16,7 +16,7 @@
16
16
  <span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75"></span>
17
17
  <span class="relative inline-flex rounded-full h-3 w-3 bg-emerald-500"></span>
18
18
  </span>
19
- <p class="text-[11px] font-black text-indigo-900 uppercase tracking-[0.25em]">The 395% Elite Milestone Surpassed</p>
19
+ <p class="text-[11px] font-black text-indigo-900 uppercase tracking-[0.25em]">The 420% Elite Milestone Surpassed</p>
20
20
  </div>
21
21
 
22
22
  <!-- Heroic Logo Anchor (Enhanced) -->
@@ -121,6 +121,16 @@
121
121
  </div>
122
122
  </div>
123
123
 
124
+ <div class="lg:col-span-12 bg-white p-10 rounded-[2.5rem] border border-amber-100 shadow-sm hover:shadow-2xl transition-all group relative overflow-hidden cursor-crosshair bg-gradient-to-br from-white to-amber-50/30">
125
+ <p class="text-[9px] font-bold text-amber-600 uppercase tracking-widest mb-4">Elite Extension (+10%)</p>
126
+ <h4 class="text-2xl font-black text-slate-900 mb-3 tracking-tight italic">API Gateway Standards & Swagger UI</h4>
127
+ <p class="text-slate-600 leading-relaxed mb-8">Natively exposes OpenAPI JSON at the JHipster-standard <code>/v3/api-docs</code> endpoint. Features a completely re-engineered, glassmorphism Swagger UI deeply integrated with Keycloak SSO and automatic token refreshing.</p>
128
+ <div class="flex gap-3">
129
+ <span class="px-4 py-1.5 bg-amber-100 text-amber-700 text-[9px] font-black rounded-lg">JHIPSTER COMPATIBLE</span>
130
+ <span class="px-4 py-1.5 bg-amber-100 text-amber-700 text-[9px] font-black rounded-lg">KEYCLOAK SSO UI</span>
131
+ </div>
132
+ </div>
133
+
124
134
  <div class="lg:col-span-12 bg-white p-10 rounded-[2.5rem] border border-fuchsia-100 shadow-sm hover:shadow-2xl transition-all group relative overflow-hidden cursor-crosshair bg-gradient-to-br from-white to-fuchsia-50/30">
125
135
  <p class="text-[9px] font-bold text-fuchsia-600 uppercase tracking-widest mb-4">Elite Extension (+12%)</p>
126
136
  <h4 class="text-2xl font-black text-slate-900 mb-3 tracking-tight italic">K8s Network Isolation & NodePorts</h4>
@@ -436,7 +446,7 @@
436
446
  </div>
437
447
 
438
448
  <div class="mt-24 text-slate-500 font-mono text-[10px] uppercase tracking-[0.6em] font-black relative z-10">
439
- GO-DUCK &bull; THE 395% ELITE MILESTONE &bull; SOVEREIGN CODE ORCHESTRATION
449
+ GO-DUCK &bull; THE 420% ELITE MILESTONE &bull; SOVEREIGN CODE ORCHESTRATION
440
450
  </div>
441
451
  </div>
442
452
  </section>
@@ -86,8 +86,13 @@ class ApiProvider {
86
86
  <h2 class="text-2xl font-bold text-gray-800 mb-4 border-b pb-2">3. WSO2 API Manager Integration</h2>
87
87
  <p class="mb-4">GO-DUCK generated APIs can automatically register themselves with a <strong>WSO2 API Manager</strong> instance on startup. This uses the WSO2 Publisher REST API to import your dynamically generated OpenAPI 3.0 specs.</p>
88
88
 
89
+ <div class="bg-indigo-50 border-l-4 border-indigo-500 p-4 mb-6 rounded-r">
90
+ <h4 class="font-bold text-indigo-900 mb-1">Standard API Gateway Discovery</h4>
91
+ <p class="text-sm text-indigo-800">For external gateways (Kong, Apigee) and ecosystems like <strong>JHipster</strong> and <strong>Spring Boot</strong>, your microservice natively exposes its OpenAPI JSON at the standard <code>/v3/api-docs</code> endpoint. Legacy systems can still use <code>/swagger.json</code>.</p>
92
+ </div>
93
+
89
94
  <div class="bg-blue-50 border-l-4 border-blue-500 p-4 mb-6 rounded-r">
90
- <p class="text-blue-900"><strong>Configuration:</strong> Enable this by adding the <code>wso2</code> block to your <code>config.yaml</code>.</p>
95
+ <p class="text-blue-900"><strong>WSO2 Configuration:</strong> Enable this by adding the <code>wso2</code> block to your <code>config.yaml</code>.</p>
91
96
  </div>
92
97
 
93
98
  <pre><code class="language-yaml">go-duck:
@@ -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,238 @@ 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("/logo.png", "./docs/web/logo.png")
166
170
  r.GET("/swagger", func(c *gin.Context) {
167
- c.Data(http.StatusOK, "text/html; charset=utf-8", []byte(`<!DOCTYPE html>
171
+ swaggerHTML := `<!DOCTYPE html>
168
172
  <html lang="en">
169
173
  <head>
170
174
  <meta charset="UTF-8">
171
- <title>Swagger UI</title>
175
+ <title>Swagger UI (Secured)</title>
172
176
  <link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/4.15.5/swagger-ui.css" />
177
+ <script src="https://cdn.jsdelivr.net/npm/keycloak-js@24.0.4/dist/keycloak.min.js"></script>
173
178
  <style>
174
179
  html { box-sizing: border-box; overflow: -moz-scrollbars-vertical; overflow-y: scroll; }
175
180
  *, *:before, *:after { box-sizing: inherit; }
176
- body { margin:0; background: #fafafa; }
181
+ body { margin:0; background: #fafafa; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; }
182
+
183
+ .top-nav {
184
+ background: rgba(255, 255, 255, 0.8);
185
+ backdrop-filter: blur(10px);
186
+ -webkit-backdrop-filter: blur(10px);
187
+ border-bottom: 1px solid rgba(0, 0, 0, 0.1);
188
+ padding: 12px 24px;
189
+ display: flex;
190
+ justify-content: space-between;
191
+ align-items: center;
192
+ position: sticky;
193
+ top: 0;
194
+ z-index: 1000;
195
+ box-shadow: 0 4px 6px -1px rgba(0,0,0,0.05);
196
+ }
197
+
198
+ .nav-brand {
199
+ font-weight: 700;
200
+ font-size: 1.2rem;
201
+ color: #333;
202
+ display: flex;
203
+ align-items: center;
204
+ gap: 10px;
205
+ }
206
+
207
+ .nav-controls {
208
+ display: flex;
209
+ align-items: center;
210
+ gap: 16px;
211
+ }
212
+
213
+ .tenant-input {
214
+ padding: 8px 12px;
215
+ border: 1px solid #ddd;
216
+ border-radius: 6px;
217
+ font-size: 0.9rem;
218
+ outline: none;
219
+ transition: border-color 0.2s;
220
+ }
221
+
222
+ .tenant-input:focus {
223
+ border-color: #4a90e2;
224
+ }
225
+
226
+ .btn {
227
+ padding: 8px 16px;
228
+ border-radius: 6px;
229
+ border: none;
230
+ font-weight: 600;
231
+ cursor: pointer;
232
+ transition: all 0.2s;
233
+ font-size: 0.9rem;
234
+ }
235
+
236
+ .btn-login {
237
+ background: #4a90e2;
238
+ color: white;
239
+ }
240
+
241
+ .btn-login:hover {
242
+ background: #357abd;
243
+ }
244
+
245
+ .btn-logout {
246
+ background: #e74c3c;
247
+ color: white;
248
+ display: none;
249
+ }
250
+
251
+ .btn-logout:hover {
252
+ background: #c0392b;
253
+ }
254
+
255
+ .status-indicator {
256
+ display: flex;
257
+ align-items: center;
258
+ gap: 6px;
259
+ font-size: 0.85rem;
260
+ color: #666;
261
+ }
262
+
263
+ .dot {
264
+ width: 8px;
265
+ height: 8px;
266
+ border-radius: 50%;
267
+ background: #ccc;
268
+ }
269
+
270
+ .dot.active {
271
+ background: #2ecc71;
272
+ box-shadow: 0 0 8px #2ecc71;
273
+ }
177
274
  </style>
178
275
  </head>
179
276
  <body>
277
+ <div class="top-nav">
278
+ <div class="nav-brand">
279
+ <img src="/logo.png" alt="GO-DUCK Logo" style="height: 32px; width: auto;" />
280
+ <div style="display: flex; flex-direction: column; line-height: 1.2;">
281
+ <span>{{app_name}} API Explorer</span>
282
+ <span style="font-size: 0.65em; font-weight: normal; color: #94a3b8;">
283
+ GO-DUCK v{{cli_version}} • Generated: {{generated_date}}
284
+ </span>
285
+ </div>
286
+ </div>
287
+ <div class="nav-controls">
288
+ <div class="status-indicator">
289
+ <div class="dot" id="auth-dot"></div>
290
+ <span id="auth-status">Unauthenticated</span>
291
+ </div>
292
+ <input type="text" id="tenant-input" class="tenant-input" placeholder="X-Tenant-ID" value="tenant_1" title="Multi-tenant DB Target" />
293
+ <button id="login-btn" class="btn btn-login">Login with Keycloak</button>
294
+ <button id="logout-btn" class="btn btn-logout">Logout</button>
295
+ </div>
296
+ </div>
297
+
180
298
  <div id="swagger-ui"></div>
299
+
181
300
  <script src="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/4.15.5/swagger-ui-bundle.js"></script>
182
301
  <script src="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/4.15.5/swagger-ui-standalone-preset.js"></script>
183
302
  <script>
303
+ const KEYCLOAK_URL = 'KEYCLOAK_URL_PLACEHOLDER';
304
+ const KEYCLOAK_REALM = 'KEYCLOAK_REALM_PLACEHOLDER';
305
+ const KEYCLOAK_CLIENT = 'KEYCLOAK_CLIENT_PLACEHOLDER';
306
+
307
+ let keycloak = null;
308
+
309
+ function updateUI(authenticated) {
310
+ if (authenticated) {
311
+ document.getElementById('login-btn').style.display = 'none';
312
+ document.getElementById('logout-btn').style.display = 'block';
313
+ document.getElementById('auth-dot').classList.add('active');
314
+ document.getElementById('auth-status').innerText = 'Authenticated (' + (keycloak.tokenParsed?.preferred_username || 'User') + ')';
315
+ } else {
316
+ document.getElementById('login-btn').style.display = 'block';
317
+ document.getElementById('logout-btn').style.display = 'none';
318
+ document.getElementById('auth-dot').classList.remove('active');
319
+ document.getElementById('auth-status').innerText = 'Unauthenticated';
320
+ }
321
+ }
322
+
184
323
  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"
324
+ // Initialize Keycloak
325
+ keycloak = new Keycloak({
326
+ url: KEYCLOAK_URL,
327
+ realm: KEYCLOAK_REALM,
328
+ clientId: KEYCLOAK_CLIENT
329
+ });
330
+
331
+ keycloak.init({ onLoad: 'check-sso', checkLoginIframe: false }).then(authenticated => {
332
+ updateUI(authenticated);
333
+
334
+ document.getElementById('login-btn').onclick = () => keycloak.login();
335
+ document.getElementById('logout-btn').onclick = () => keycloak.logout();
336
+
337
+ if (authenticated) {
338
+ // Auto-refresh token logic
339
+ setInterval(() => {
340
+ keycloak.updateToken(70).then(refreshed => {
341
+ if (refreshed) {
342
+ console.log('Token refreshed automatically');
343
+ }
344
+ }).catch(() => {
345
+ console.error('Failed to refresh token');
346
+ updateUI(false);
347
+ });
348
+ }, 60000); // Check every minute
349
+ }
350
+
351
+ // 4. Initialize Swagger UI
352
+ const ui = SwaggerUIBundle({
353
+ url: "/v3/api-docs",
354
+ dom_id: '#swagger-ui',
355
+ deepLinking: true,
356
+ presets: [
357
+ SwaggerUIBundle.presets.apis,
358
+ SwaggerUIStandalonePreset
359
+ ],
360
+ layout: "StandaloneLayout",
361
+ requestInterceptor: (req) => {
362
+ if (keycloak && keycloak.token) {
363
+ req.headers['Authorization'] = 'Bearer ' + keycloak.token;
364
+ }
365
+ const tenantId = document.getElementById('tenant-input').value;
366
+ if (tenantId) {
367
+ req.headers['X-Tenant-ID'] = tenantId;
368
+ }
369
+ return req;
370
+ }
371
+ });
372
+ }).catch(err => {
373
+ console.error("Keycloak initialization failed", err);
374
+ document.getElementById('auth-status').innerText = 'Keycloak init failed';
375
+
376
+ // Still load swagger without auth interceptor
377
+ window.ui = SwaggerUIBundle({
378
+ url: "/v3/api-docs",
379
+ dom_id: '#swagger-ui',
380
+ deepLinking: true,
381
+ presets: [
382
+ SwaggerUIBundle.presets.apis,
383
+ SwaggerUIStandalonePreset
384
+ ],
385
+ layout: "StandaloneLayout"
386
+ });
194
387
  });
195
- window.ui = ui;
196
388
  };
197
389
  </script>
198
390
  </body>
199
- </html>`))
391
+ </html>`
392
+
393
+ htmlStr := strings.ReplaceAll(swaggerHTML, "KEYCLOAK_URL_PLACEHOLDER", appConfig.GoDuck.Security.KeycloakHost)
394
+ htmlStr = strings.ReplaceAll(htmlStr, "KEYCLOAK_REALM_PLACEHOLDER", appConfig.GoDuck.Security.KeycloakRealm)
395
+ htmlStr = strings.ReplaceAll(htmlStr, "KEYCLOAK_CLIENT_PLACEHOLDER", appConfig.GoDuck.Security.KeycloakAppClientID)
396
+
397
+ c.Data(http.StatusOK, "text/html; charset=utf-8", []byte(htmlStr))
200
398
  })
201
399
 
202
400
  // Management APIs (Run-time DB onboarding)