go-duck-cli 1.0.8 → 1.1.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 (70) hide show
  1. package/README.md +30 -15
  2. package/generators/ai_docs.js +130 -0
  3. package/generators/broker.js +63 -0
  4. package/generators/config.js +149 -7
  5. package/generators/devops.js +210 -43
  6. package/generators/docs.js +23 -4
  7. package/generators/elasticsearch.js +263 -0
  8. package/generators/kratos.js +229 -41
  9. package/generators/metering.js +280 -48
  10. package/generators/migrations.js +92 -198
  11. package/generators/mqtt.js +2 -39
  12. package/generators/multitenancy.js +274 -71
  13. package/generators/nats.js +39 -0
  14. package/generators/outbox.js +171 -0
  15. package/generators/postgrest.js +7 -3
  16. package/generators/postman.js +405 -0
  17. package/generators/repository.js +27 -0
  18. package/generators/router.js +27 -0
  19. package/generators/security.js +95 -14
  20. package/generators/serverless.js +147 -0
  21. package/generators/storage.js +589 -0
  22. package/generators/swagger.js +84 -60
  23. package/generators/telemetry.js +23 -32
  24. package/generators/websocket.js +55 -21
  25. package/index.js +481 -116
  26. package/package.json +6 -4
  27. package/parser/gdl.js +163 -24
  28. package/templates/docs/index.html.hbs +5 -5
  29. package/templates/docs/layout.hbs +221 -62
  30. package/templates/docs/pages/audit.hbs +83 -35
  31. package/templates/docs/pages/cli.hbs +18 -0
  32. package/templates/docs/pages/configuration.hbs +241 -0
  33. package/templates/docs/pages/datadog.hbs +46 -0
  34. package/templates/docs/pages/elasticsearch.hbs +121 -0
  35. package/templates/docs/pages/federation.hbs +241 -0
  36. package/templates/docs/pages/gdl-advanced.hbs +91 -0
  37. package/templates/docs/pages/gdl-annotations.hbs +137 -0
  38. package/templates/docs/pages/gdl-entities.hbs +134 -0
  39. package/templates/docs/pages/gdl-relationships.hbs +80 -0
  40. package/templates/docs/pages/gdl.hbs +60 -204
  41. package/templates/docs/pages/graphql.hbs +58 -44
  42. package/templates/docs/pages/grpc.hbs +53 -90
  43. package/templates/docs/pages/hybrid-store.hbs +127 -0
  44. package/templates/docs/pages/index.hbs +418 -149
  45. package/templates/docs/pages/keycloak.hbs +43 -0
  46. package/templates/docs/pages/legend.hbs +116 -0
  47. package/templates/docs/pages/mosquitto.hbs +39 -0
  48. package/templates/docs/pages/multitenancy.hbs +139 -71
  49. package/templates/docs/pages/otel.hbs +40 -0
  50. package/templates/docs/pages/realtime.hbs +38 -12
  51. package/templates/docs/pages/redis.hbs +40 -0
  52. package/templates/docs/pages/rest.hbs +120 -202
  53. package/templates/docs/pages/saga.hbs +94 -0
  54. package/templates/docs/pages/security.hbs +150 -44
  55. package/templates/docs/pages/serverless.hbs +157 -0
  56. package/templates/docs/pages/storage.hbs +127 -0
  57. package/templates/docs/pages/wizard.hbs +683 -0
  58. package/templates/docs/triple_identity_registry.png +0 -0
  59. package/templates/go/controller.go.hbs +287 -283
  60. package/templates/go/entity.go.hbs +17 -15
  61. package/templates/go/main.go.hbs +47 -180
  62. package/templates/go/migrator.go.hbs +65 -0
  63. package/templates/go/router.go.hbs +272 -0
  64. package/templates/graphql/resolver.go.hbs +53 -34
  65. package/templates/graphql/schema.graphql.hbs +17 -5
  66. package/templates/kratos/service.go.hbs +169 -34
  67. package/templates/proto/entity.proto.hbs +10 -14
  68. package/test_nested.gdl +21 -0
  69. package/templates/docs/intro.mp4 +0 -0
  70. package/test_parser.js +0 -9
@@ -0,0 +1,405 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+ import chalk from 'chalk';
4
+
5
+ export const generatePostmanCollection = async (config, entities, outputDir, openEntities = []) => {
6
+ const docsDir = path.join(outputDir, 'docs');
7
+ await fs.ensureDir(docsDir);
8
+
9
+ const collection = {
10
+ info: {
11
+ name: `${config.name} Complete Postman API Suite`,
12
+ description: `Auto-generated comprehensive testing suite for all REST, GraphQL, WebSocket, RPC, and Management APIs of the ${config.name} microservice.`,
13
+ schema: "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
14
+ },
15
+ item: [],
16
+ variable: [
17
+ { key: "host", value: "localhost", type: "string" },
18
+ { key: "port", value: "8080", type: "string" },
19
+ { key: "tenant", value: "tenant_1", type: "string" },
20
+ { key: "token", value: "", type: "string" },
21
+ { key: "keycloak_url", value: "http://localhost:8180", type: "string" },
22
+ { key: "realm", value: config.security?.['keycloak-realm'] || 'master', type: "string" },
23
+ { key: "app_client_id", value: `${config.name}-app`, type: "string" },
24
+ { key: "service_client_id", value: `${config.name}-service`, type: "string" },
25
+ { key: "service_secret", value: "service-secret-123", type: "string" },
26
+ { key: "username", value: "admin", type: "string" },
27
+ { key: "password", value: "admin", type: "string" }
28
+ ]
29
+ };
30
+
31
+ // 1. Auth Folder
32
+ collection.item.push({
33
+ name: "1. Authentication (Zero-Trust)",
34
+ description: "Fetch OIDC Tokens from Keycloak",
35
+ item: [
36
+ {
37
+ name: "1. Public App Login (User Login)",
38
+ event: [
39
+ {
40
+ listen: "test",
41
+ script: {
42
+ exec: [
43
+ "var jsonData = pm.response.json();",
44
+ "if (jsonData.access_token) {",
45
+ " pm.collectionVariables.set(\"token\", jsonData.access_token);",
46
+ "}"
47
+ ],
48
+ type: "text/javascript"
49
+ }
50
+ }
51
+ ],
52
+ request: {
53
+ method: "POST",
54
+ header: [{ key: "Content-Type", value: "application/x-www-form-urlencoded" }],
55
+ body: {
56
+ mode: "urlencoded",
57
+ urlencoded: [
58
+ { key: "client_id", value: "{{app_client_id}}", type: "text" },
59
+ { key: "username", value: "{{username}}", type: "text" },
60
+ { key: "password", value: "{{password}}", type: "text" },
61
+ { key: "grant_type", value: "password", type: "text" }
62
+ ]
63
+ },
64
+ url: {
65
+ raw: "{{keycloak_url}}/realms/{{realm}}/protocol/openid-connect/token",
66
+ host: ["{{keycloak_url}}"],
67
+ path: ["realms", "{{realm}}", "protocol", "openid-connect", "token"]
68
+ }
69
+ }
70
+ },
71
+ {
72
+ name: "2. Confidential Service Login (Service Account)",
73
+ event: [
74
+ {
75
+ listen: "test",
76
+ script: {
77
+ exec: [
78
+ "var jsonData = pm.response.json();",
79
+ "if (jsonData.access_token) {",
80
+ " pm.collectionVariables.set(\"token\", jsonData.access_token);",
81
+ "}"
82
+ ],
83
+ type: "text/javascript"
84
+ }
85
+ }
86
+ ],
87
+ request: {
88
+ method: "POST",
89
+ header: [{ key: "Content-Type", value: "application/x-www-form-urlencoded" }],
90
+ body: {
91
+ mode: "urlencoded",
92
+ urlencoded: [
93
+ { key: "client_id", value: "{{service_client_id}}", type: "text" },
94
+ { key: "client_secret", value: "{{service_secret}}", type: "text" },
95
+ { key: "grant_type", value: "client_credentials", type: "text" }
96
+ ]
97
+ },
98
+ url: {
99
+ raw: "{{keycloak_url}}/realms/{{realm}}/protocol/openid-connect/token",
100
+ host: ["{{keycloak_url}}"],
101
+ path: ["realms", "{{realm}}", "protocol", "openid-connect", "token"]
102
+ }
103
+ }
104
+ }
105
+ ]
106
+ });
107
+
108
+ // 2. Management & Zero-Trust
109
+ collection.item.push({
110
+ name: "2. Architecture Management Metrics",
111
+ item: [
112
+ {
113
+ name: "1. Provision & Assign Tenant DB",
114
+ request: {
115
+ method: "POST",
116
+ header: [
117
+ { key: "Authorization", value: "Bearer {{token}}" },
118
+ { key: "X-Tenant-ID", value: "master_internal" },
119
+ { key: "Content-Type", value: "application/json" }
120
+ ],
121
+ body: {
122
+ mode: "raw",
123
+ raw: "{\n \"roleName\": \"tenant_1_admin\",\n \"dbName\": \"tenant_1\"\n}"
124
+ },
125
+ url: {
126
+ raw: "http://{{host}}:{{port}}/management/tenant/assign",
127
+ protocol: "http",
128
+ host: ["{{host}}"],
129
+ port: "{{port}}",
130
+ path: ["management", "tenant", "assign"]
131
+ }
132
+ }
133
+ },
134
+ {
135
+ name: "2. Fetch Central Audit Logs",
136
+ request: {
137
+ method: "GET",
138
+ header: [
139
+ { key: "Authorization", value: "Bearer {{token}}" },
140
+ { key: "X-Tenant-ID", value: "{{tenant}}" }
141
+ ],
142
+ url: {
143
+ raw: "http://{{host}}:{{port}}/api/audit",
144
+ protocol: "http",
145
+ host: ["{{host}}"],
146
+ port: "{{port}}",
147
+ path: ["api", "audit"]
148
+ }
149
+ }
150
+ },
151
+ {
152
+ name: "3. Monitor API Quotas",
153
+ request: {
154
+ method: "GET",
155
+ header: [
156
+ { key: "Authorization", value: "Bearer {{token}}" },
157
+ { key: "X-Tenant-ID", value: "{{tenant}}" }
158
+ ],
159
+ url: {
160
+ raw: "http://{{host}}:{{port}}/api/metering/usage",
161
+ protocol: "http",
162
+ host: ["{{host}}"],
163
+ port: "{{port}}",
164
+ path: ["api", "metering", "usage"]
165
+ }
166
+ }
167
+ }
168
+ ]
169
+ });
170
+
171
+ // 3. GraphQL
172
+ collection.item.push({
173
+ name: "3. GraphQL Federation Layer",
174
+ item: [
175
+ {
176
+ name: "1. Unified GraphQL Engine",
177
+ request: {
178
+ method: "POST",
179
+ header: [
180
+ { key: "Authorization", value: "Bearer {{token}}" },
181
+ { key: "X-Tenant-ID", value: "{{tenant}}" },
182
+ { key: "Content-Type", value: "application/json" }
183
+ ],
184
+ body: {
185
+ mode: "graphql",
186
+ graphql: {
187
+ query: `query {\n ${entities.length > 0 ? 'list' + entities[0].name.charAt(0).toUpperCase() + entities[0].name.slice(1) + 's' : 'test'} {\n id\n }\n}`,
188
+ variables: ""
189
+ }
190
+ },
191
+ url: {
192
+ raw: "http://{{host}}:{{port}}/graphql",
193
+ protocol: "http",
194
+ host: ["{{host}}"],
195
+ port: "{{port}}",
196
+ path: ["graphql"]
197
+ }
198
+ }
199
+ }
200
+ ]
201
+ });
202
+
203
+ // 4. REST Entities
204
+ const restFolder = { name: "4. Standard REST & Deep Search", item: [] };
205
+
206
+ const generateDummyJson = (fields) => {
207
+ const obj = {};
208
+ for (const f of fields) {
209
+ if (f.type === 'String' || f.type === 'Text') obj[f.name] = "Sample " + f.name;
210
+ else if (f.type === 'Integer' || f.type === 'Long') obj[f.name] = 100;
211
+ else if (f.type === 'Float' || f.type === 'BigDecimal') obj[f.name] = 99.99;
212
+ else if (f.type === 'Boolean') obj[f.name] = true;
213
+ else if (f.type === 'LocalDate') obj[f.name] = "2024-01-01";
214
+ else if (f.type === 'Instant') obj[f.name] = "2024-01-01T12:00:00Z";
215
+ else if (f.type === 'JSON' || f.type === 'JSONB') obj[f.name] = {"attribute": "example_value"};
216
+ else obj[f.name] = "test";
217
+ }
218
+ return JSON.stringify(obj, null, 2);
219
+ };
220
+
221
+ const isOpen = (entityName, action) => {
222
+ if (!openEntities || !Array.isArray(openEntities)) return false;
223
+ const wildcard = openEntities.find(e => e.name === '*');
224
+ if (wildcard && wildcard.actions.includes(action.toLowerCase())) return true;
225
+ const entry = openEntities.find(e => e.name.toLowerCase() === entityName.toLowerCase());
226
+ if (entry && entry.actions.includes(action.toLowerCase())) return true;
227
+ return false;
228
+ };
229
+
230
+ for (const entity of entities) {
231
+ const name = entity.name.toLowerCase();
232
+ const capitalized = entity.name.charAt(0).toUpperCase() + entity.name.slice(1);
233
+ const folder = { name: `${capitalized} APIs`, item: [] };
234
+
235
+ // 1. PUBLIC API Folder (If Open)
236
+ const publicItems = [];
237
+ if (isOpen(capitalized, 'read')) {
238
+ publicItems.push({
239
+ name: `[PUBLIC] List ${capitalized}s`,
240
+ request: {
241
+ method: "GET",
242
+ header: [ { key: "X-Tenant-ID", value: "{{tenant}}" } ],
243
+ url: { raw: `http://{{host}}:{{port}}/open/api/${name}s?page=1&size=10&eager=true`, protocol: "http", host: ["{{host}}"], port: "{{port}}", path: ["open", "api", `${name}s`], query: [ { key: "page", value: "1" }, { key: "size", value: "10" }, { key: "eager", value: "true" } ] }
244
+ }
245
+ });
246
+ publicItems.push({
247
+ name: `[PUBLIC] Get ${capitalized} by ID`,
248
+ request: {
249
+ method: "GET",
250
+ header: [ { key: "X-Tenant-ID", value: "{{tenant}}" } ],
251
+ url: { raw: `http://{{host}}:{{port}}/open/api/${name}s/1`, protocol: "http", host: ["{{host}}"], port: "{{port}}", path: ["open", "api", `${name}s`, "1"] }
252
+ }
253
+ });
254
+ }
255
+ if (isOpen(capitalized, 'create')) {
256
+ publicItems.push({
257
+ name: `[PUBLIC] Create ${capitalized}`,
258
+ request: {
259
+ method: "POST",
260
+ header: [ { key: "X-Tenant-ID", value: "{{tenant}}" }, { key: "Content-Type", value: "application/json" } ],
261
+ body: { mode: "raw", raw: generateDummyJson(entity.fields) },
262
+ url: { raw: `http://{{host}}:{{port}}/open/api/${name}s`, protocol: "http", host: ["{{host}}"], port: "{{port}}", path: ["open", "api", `${name}s`] }
263
+ }
264
+ });
265
+ }
266
+
267
+ if (publicItems.length > 0) {
268
+ folder.item.push({ name: "Public (Zero-Auth) Access", item: publicItems });
269
+ }
270
+
271
+ // 2. PRIVATE API Folder (Secured)
272
+ const privateItems = [
273
+ {
274
+ name: `List ${capitalized}s (REST)`,
275
+ request: {
276
+ method: "GET",
277
+ header: [ { key: "Authorization", value: "Bearer {{token}}" }, { key: "X-Tenant-ID", value: "{{tenant}}" } ],
278
+ url: { raw: `http://{{host}}:{{port}}/api/${name}s?page=1&size=10&eager=true`, protocol: "http", host: ["{{host}}"], port: "{{port}}", path: ["api", `${name}s`], query: [ { key: "page", value: "1" }, { key: "size", value: "10" }, { key: "eager", value: "true" } ] }
279
+ }
280
+ },
281
+ {
282
+ name: `Create ${capitalized} (REST)`,
283
+ request: {
284
+ method: "POST",
285
+ header: [ { key: "Authorization", value: "Bearer {{token}}" }, { key: "X-Tenant-ID", value: "{{tenant}}" }, { key: "Content-Type", value: "application/json" } ],
286
+ body: { mode: "raw", raw: generateDummyJson(entity.fields) },
287
+ url: { raw: `http://{{host}}:{{port}}/api/${name}s`, protocol: "http", host: ["{{host}}"], port: "{{port}}", path: ["api", `${name}s`] }
288
+ }
289
+ },
290
+ {
291
+ name: `Get ${capitalized} by ID (REST)`,
292
+ request: {
293
+ method: "GET",
294
+ header: [ { key: "Authorization", value: "Bearer {{token}}" }, { key: "X-Tenant-ID", value: "{{tenant}}" } ],
295
+ url: { raw: `http://{{host}}:{{port}}/api/${name}s/1`, protocol: "http", host: ["{{host}}"], port: "{{port}}", path: ["api", `${name}s`, "1"] }
296
+ }
297
+ },
298
+ {
299
+ name: `RPC Deep JSON Search (${capitalized})`,
300
+ request: {
301
+ method: "GET",
302
+ header: [ { key: "Authorization", value: "Bearer {{token}}" }, { key: "X-Tenant-ID", value: "{{tenant}}" } ],
303
+ 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" } ] }
304
+ }
305
+ }
306
+ ];
307
+
308
+ folder.item.push({ name: "Private (Auth Required) CRUD", item: privateItems });
309
+ restFolder.item.push(folder);
310
+ }
311
+ collection.item.push(restFolder);
312
+
313
+ // 5. WebSocket
314
+ collection.item.push({
315
+ name: "5. Real-Time Distributed WebSockets",
316
+ item: [
317
+ {
318
+ name: "1. REST-over-WS Streaming Link",
319
+ request: {
320
+ method: "GET",
321
+ header: [],
322
+ url: {
323
+ raw: "ws://{{host}}:{{port}}/ws?token={{token}}",
324
+ protocol: "ws",
325
+ host: ["{{host}}"],
326
+ port: "{{port}}",
327
+ path: ["ws"],
328
+ query: [{ key: "token", value: "{{token}}" }]
329
+ }
330
+ }
331
+ }
332
+ ]
333
+ });
334
+
335
+ // 6. Storage Mesh
336
+ collection.item.push({
337
+ name: "6. Universal Storage Mesh",
338
+ item: [
339
+ {
340
+ name: "1. Upload File to Active Lake",
341
+ request: {
342
+ method: "POST",
343
+ header: [
344
+ { key: "Authorization", value: "Bearer {{token}}" },
345
+ { key: "X-Tenant-ID", value: "{{tenant}}" }
346
+ ],
347
+ body: {
348
+ mode: "formdata",
349
+ formdata: [
350
+ { key: "file", type: "file", src: [] },
351
+ { key: "folder", value: "farm/animals", type: "text" }
352
+ ]
353
+ },
354
+ url: {
355
+ raw: "http://{{host}}:{{port}}/api/storage/upload?provider=sftp",
356
+ protocol: "http",
357
+ host: ["{{host}}"],
358
+ port: "{{port}}",
359
+ path: ["api", "storage", "upload"],
360
+ query: [{ key: "provider", value: "sftp" }]
361
+ }
362
+ }
363
+ },
364
+ {
365
+ name: "2. Download File (Provider Targeted)",
366
+ request: {
367
+ method: "GET",
368
+ header: [
369
+ { key: "Authorization", value: "Bearer {{token}}" },
370
+ { key: "X-Tenant-ID", value: "{{tenant}}" }
371
+ ],
372
+ url: {
373
+ raw: "http://{{host}}:{{port}}/api/storage/download/farm/animals/photo.jpg?provider=sftp",
374
+ protocol: "http",
375
+ host: ["{{host}}"],
376
+ port: "{{port}}",
377
+ path: ["api", "storage", "download", "farm", "animals", "photo.jpg"],
378
+ query: [{ key: "provider", value: "sftp" }]
379
+ }
380
+ }
381
+ },
382
+ {
383
+ name: "3. Distributed Cross-Scan Retrieve",
384
+ request: {
385
+ method: "GET",
386
+ header: [
387
+ { key: "Authorization", value: "Bearer {{token}}" },
388
+ { key: "X-Tenant-ID", value: "{{tenant}}" }
389
+ ],
390
+ url: {
391
+ raw: "http://{{host}}:{{port}}/api/storage/scan/farm/animals/photo.jpg",
392
+ protocol: "http",
393
+ host: ["{{host}}"],
394
+ port: "{{port}}",
395
+ path: ["api", "storage", "scan", "farm", "animals", "photo.jpg"]
396
+ }
397
+ }
398
+ }
399
+ ]
400
+ });
401
+
402
+ const filePath = path.join(docsDir, 'postman_collection.json');
403
+ await fs.writeJson(filePath, collection, { spaces: 2 });
404
+ console.log(chalk.gray(' Generated Postman Collection: postman_collection.json'));
405
+ };
@@ -9,6 +9,8 @@ export const generateRepositoryCode = async (outputDir) => {
9
9
  const repoGo = `package repository
10
10
 
11
11
  import (
12
+ "context"
13
+ "go.mongodb.org/mongo-driver/mongo"
12
14
  "gorm.io/gorm"
13
15
  )
14
16
 
@@ -21,6 +23,31 @@ func NewRepository(db *gorm.DB) *Repository {
21
23
  DB: db,
22
24
  }
23
25
  }
26
+
27
+ func (r *Repository) GetDB(ctx context.Context) *gorm.DB {
28
+ // Kratos/Gin shared context value
29
+ if tdb, ok := ctx.Value("tenantDBConn").(*gorm.DB); ok {
30
+ return tdb
31
+ }
32
+ return r.DB
33
+ }
34
+
35
+ type MongoRepository struct {
36
+ Client *mongo.Client
37
+ }
38
+
39
+ func NewMongoRepository(client *mongo.Client) *MongoRepository {
40
+ return &MongoRepository{
41
+ Client: client,
42
+ }
43
+ }
44
+
45
+ func (r *MongoRepository) GetMongoDB(ctx context.Context) *mongo.Database {
46
+ if mdb, ok := ctx.Value("tenantMongoDB").(*mongo.Database); ok {
47
+ return mdb
48
+ }
49
+ return nil
50
+ }
24
51
  `;
25
52
 
26
53
  await fs.writeFile(path.join(repoDir, 'repository.go'), repoGo);
@@ -0,0 +1,27 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+ import Handlebars from 'handlebars';
4
+ import chalk from 'chalk';
5
+
6
+ export const generateRouterCode = async (outputDir, config, entities, openEntities) => {
7
+ const routerDir = path.join(outputDir, 'router');
8
+ await fs.ensureDir(routerDir);
9
+
10
+ const templatePath = path.resolve(path.dirname(import.meta.url.replace('file://', '')), '../templates/go/router.go.hbs');
11
+ if (!await fs.pathExists(templatePath)) {
12
+ console.error(chalk.red(`❌ Router template not found: ${templatePath}`));
13
+ return;
14
+ }
15
+
16
+ const templateSource = await fs.readFile(templatePath, 'utf8');
17
+ const template = Handlebars.compile(templateSource);
18
+
19
+ const content = template({
20
+ app_name: config.name,
21
+ entities,
22
+ openEntities
23
+ });
24
+
25
+ await fs.writeFile(path.join(routerDir, 'router.go'), content);
26
+ console.log(chalk.gray(' Generated Reusable Router Package: router/router.go'));
27
+ };
@@ -10,11 +10,13 @@ export const generateSecurityMiddleware = async (config, outputDir) => {
10
10
  package middleware
11
11
 
12
12
  import (
13
+ "fmt"
13
14
  "net/http"
14
15
  "strings"
15
16
 
16
17
  "github.com/gin-gonic/gin"
17
18
  "github.com/golang-jwt/jwt/v4"
19
+ "{{app_name}}/config"
18
20
  )
19
21
 
20
22
  // JWTMiddleware validates Keycloak JWTs
@@ -34,9 +36,15 @@ func JWTMiddleware() gin.HandlerFunc {
34
36
 
35
37
  tokenString := parts[1]
36
38
 
37
- token, _, err := new(jwt.Parser).ParseUnverified(tokenString, jwt.MapClaims{})
38
- if err != nil {
39
- c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Could not parse token"})
39
+ token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
40
+ if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
41
+ return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
42
+ }
43
+ return []byte(config.GetConfig().GoDuck.Security.KeycloakAppClientSecret), nil
44
+ })
45
+
46
+ if err != nil || !token.Valid {
47
+ c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Invalid or expired token"})
40
48
  return
41
49
  }
42
50
 
@@ -59,14 +67,58 @@ func GetUserID(c *gin.Context) string {
59
67
  }
60
68
  return "anonymous"
61
69
  }
70
+
71
+ // SuperAdminRoleMiddleware restricts access to endpoints to only users with the SuperAdminRole
72
+ func SuperAdminRoleMiddleware(cfg *config.Config) gin.HandlerFunc {
73
+ return func(c *gin.Context) {
74
+ superRole := cfg.GoDuck.Security.SuperAdminRole
75
+ if superRole == "" {
76
+ c.Next()
77
+ return
78
+ }
79
+
80
+ userRolesInterface, exists := c.Get("UserRoles")
81
+ if !exists {
82
+ c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "No roles found in security context"})
83
+ return
84
+ }
85
+
86
+ roles, ok := userRolesInterface.([]interface{})
87
+ if !ok {
88
+ c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "Invalid roles format"})
89
+ return
90
+ }
91
+
92
+ hasRole := false
93
+ for _, r := range roles {
94
+ if strings.ToLower(fmt.Sprintf("%v", r)) == strings.ToLower(superRole) {
95
+ hasRole = true
96
+ break
97
+ }
98
+ }
99
+
100
+ if !hasRole {
101
+ c.AbortWithStatusJSON(http.StatusForbidden, gin.H{
102
+ "error": "Insufficient permissions. This endpoint is restricted to " + superRole,
103
+ })
104
+ return
105
+ }
106
+
107
+ c.Next()
108
+ }
109
+ }
62
110
  `;
63
111
 
64
112
  const rateLimitMiddleware = `
65
113
  package middleware
66
114
 
67
115
  import (
116
+ "context"
117
+ "fmt"
68
118
  "net/http"
69
119
  "sync"
120
+ "time"
121
+ "{{app_name}}/cache"
70
122
  "{{app_name}}/config"
71
123
 
72
124
  "github.com/gin-gonic/gin"
@@ -76,26 +128,55 @@ import (
76
128
  var (
77
129
  limiters = make(map[string]*rate.Limiter)
78
130
  mu sync.Mutex
131
+ ctx = context.Background()
79
132
  )
80
133
 
81
- // RateLimitMiddleware provides burst protection based on configuration
134
+ // RateLimitMiddleware provides distributed burst protection using Redis (Zero-Trust fallback to local)
82
135
  func RateLimitMiddleware(cfg *config.Config) gin.HandlerFunc {
83
136
  return func(c *gin.Context) {
84
- ip := c.ClientIP()
85
-
137
+ rps := cfg.GoDuck.Security.RateLimit.RPS
138
+ burst := cfg.GoDuck.Security.RateLimit.Burst
139
+
140
+ // Try to identify by User (KeycloakID) instead of IP for Zero-Trust accuracy
141
+ identifier := c.ClientIP()
142
+ if uid, exists := c.Get("KeycloakID"); exists {
143
+ identifier = uid.(string)
144
+ }
145
+
146
+ // 1. Distributed Redis Architecture (Enterprise Grade)
147
+ if cache.RedisClient != nil {
148
+ // Fixed Window Counter per second
149
+ key := fmt.Sprintf("rate_limit:%s:%d", identifier, time.Now().Unix())
150
+ count, err := cache.RedisClient.Incr(ctx, key).Result()
151
+
152
+ if err == nil {
153
+ if count == 1 {
154
+ cache.RedisClient.Expire(ctx, key, 2*time.Second) // Small TTL for Sliding RPS
155
+ }
156
+ if count > int64(rps)+int64(burst) { // Allow burst
157
+ c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{
158
+ "error": "Distributed Rate limit exceeded. Please try again later.",
159
+ })
160
+ return
161
+ }
162
+ c.Next()
163
+ return
164
+ }
165
+ // If Redis fails, gracefully degrade to fallback
166
+ }
167
+
168
+ // 2. Local Fallback Architecture
86
169
  mu.Lock()
87
- limiter, exists := limiters[ip]
170
+ limiter, exists := limiters[identifier]
88
171
  if !exists {
89
- rps := cfg.GoDuck.Security.RateLimit.RPS
90
- burst := cfg.GoDuck.Security.RateLimit.Burst
91
172
  limiter = rate.NewLimiter(rate.Limit(rps), burst)
92
- limiters[ip] = limiter
173
+ limiters[identifier] = limiter
93
174
  }
94
175
  mu.Unlock()
95
176
 
96
177
  if !limiter.Allow() {
97
178
  c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{
98
- "error": "Rate limit exceeded. Please try again later.",
179
+ "error": "Internal Rate limit exceeded. Please try again later.",
99
180
  })
100
181
  return
101
182
  }
@@ -104,8 +185,8 @@ func RateLimitMiddleware(cfg *config.Config) gin.HandlerFunc {
104
185
  }
105
186
  `;
106
187
 
107
- await fs.writeFile(path.join(middlewareDir, 'jwt_middleware.go'), jwtMiddleware);
108
- await fs.writeFile(path.join(middlewareDir, 'rate_limit_middleware.go'), rateLimitMiddleware.replace('{{app_name}}', config.name));
188
+ await fs.writeFile(path.join(middlewareDir, 'jwt_middleware.go'), jwtMiddleware.replace(/{{app_name}}/g, config.name));
189
+ await fs.writeFile(path.join(middlewareDir, 'rate_limit_middleware.go'), rateLimitMiddleware.replace(/{{app_name}}/g, config.name));
109
190
 
110
191
  const corsMiddleware = `
111
192
  package middleware
@@ -163,6 +244,6 @@ func CORSMiddleware(cfg *config.Config) gin.HandlerFunc {
163
244
  }
164
245
  `;
165
246
 
166
- await fs.writeFile(path.join(middlewareDir, 'cors_middleware.go'), corsMiddleware.replace('{{app_name}}', config.name));
247
+ await fs.writeFile(path.join(middlewareDir, 'cors_middleware.go'), corsMiddleware.replace(/{{app_name}}/g, config.name));
167
248
  console.log(chalk.gray(' Generated Advanced Security & CORS Middleware'));
168
249
  };