go-duck-cli 1.0.0
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 +130 -0
- package/generators/cache.js +107 -0
- package/generators/config.js +173 -0
- package/generators/devops.js +212 -0
- package/generators/docs.js +74 -0
- package/generators/graphql.js +38 -0
- package/generators/kratos.js +157 -0
- package/generators/logger.js +68 -0
- package/generators/metering.js +143 -0
- package/generators/migrations.js +240 -0
- package/generators/mqtt.js +87 -0
- package/generators/multitenancy.js +130 -0
- package/generators/postgrest.js +115 -0
- package/generators/repository.js +28 -0
- package/generators/resilience.js +69 -0
- package/generators/security.js +168 -0
- package/generators/swagger.js +145 -0
- package/generators/telemetry.js +121 -0
- package/generators/websocket.js +162 -0
- package/index.js +592 -0
- package/package.json +23 -0
- package/parser/gdl.js +162 -0
- package/templates/application.yml.hbs +18 -0
- package/templates/docs/gin_bottle.png +0 -0
- package/templates/docs/index.html.hbs +226 -0
- package/templates/docs/intro.mp4 +0 -0
- package/templates/docs/kratos_mark.png +0 -0
- package/templates/docs/layout.hbs +106 -0
- package/templates/docs/logo.png +0 -0
- package/templates/docs/pages/audit.hbs +39 -0
- package/templates/docs/pages/cli.hbs +83 -0
- package/templates/docs/pages/gdl.hbs +223 -0
- package/templates/docs/pages/graphql.hbs +51 -0
- package/templates/docs/pages/grpc.hbs +100 -0
- package/templates/docs/pages/index.hbs +181 -0
- package/templates/docs/pages/integrations.hbs +83 -0
- package/templates/docs/pages/observability.hbs +34 -0
- package/templates/docs/pages/realtime.hbs +43 -0
- package/templates/docs/pages/rest.hbs +149 -0
- package/templates/docs/pages/security.hbs +31 -0
- package/templates/go/controller.go.hbs +236 -0
- package/templates/go/entity.go.hbs +34 -0
- package/templates/go/enum.go.hbs +7 -0
- package/templates/go/main.go.hbs +186 -0
- package/templates/graphql/resolver.go.hbs +50 -0
- package/templates/graphql/schema.graphql.hbs +64 -0
- package/templates/kratos/service.go.hbs +104 -0
- package/templates/proto/entity.proto.hbs +95 -0
- package/test_parser.js +9 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
|
|
5
|
+
export const generateMQTTCode = async (config, outputDir) => {
|
|
6
|
+
const messagingDir = path.join(outputDir, 'messaging');
|
|
7
|
+
await fs.ensureDir(messagingDir);
|
|
8
|
+
|
|
9
|
+
const mqttClientGo = `
|
|
10
|
+
package messaging
|
|
11
|
+
|
|
12
|
+
import (
|
|
13
|
+
"encoding/json"
|
|
14
|
+
"fmt"
|
|
15
|
+
"log"
|
|
16
|
+
"time"
|
|
17
|
+
|
|
18
|
+
"{{app_name}}/config"
|
|
19
|
+
mq "github.com/eclipse/paho.mqtt.golang"
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
var MQTTClient mq.Client
|
|
23
|
+
|
|
24
|
+
type EventMessage struct {
|
|
25
|
+
Action string \`json:"action"\`
|
|
26
|
+
Entity string \`json:"entity"\`
|
|
27
|
+
EventTime time.Time \`json:"event_time"\`
|
|
28
|
+
Payload interface{} \`json:"payload"\`
|
|
29
|
+
PreviousValue interface{} \`json:"previous_value,omitempty"\`
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
func InitMQTT(cfg *config.Config) {
|
|
33
|
+
if !cfg.GoDuck.Messaging.MQTT.Enabled {
|
|
34
|
+
log.Println("MQTT Messaging is disabled.")
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
opts := mq.NewClientOptions()
|
|
39
|
+
opts.AddBroker(cfg.GoDuck.Messaging.MQTT.Broker)
|
|
40
|
+
opts.SetClientID(cfg.GoDuck.Messaging.MQTT.ClientID)
|
|
41
|
+
|
|
42
|
+
if cfg.GoDuck.Messaging.MQTT.Username != "" {
|
|
43
|
+
opts.SetUsername(cfg.GoDuck.Messaging.MQTT.Username)
|
|
44
|
+
opts.SetPassword(cfg.GoDuck.Messaging.MQTT.Password)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
opts.OnConnect = func(c mq.Client) {
|
|
48
|
+
log.Printf("Connected to MQTT Broker: %s", cfg.GoDuck.Messaging.MQTT.Broker)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
client := mq.NewClient(opts)
|
|
52
|
+
if token := client.Connect(); token.Wait() && token.Error() != nil {
|
|
53
|
+
log.Printf("Failed to connect to MQTT: %v", token.Error())
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
MQTTClient = client
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
func PublishEvent(topicPrefix string, action string, entity string, payload interface{}, prev interface{}) {
|
|
61
|
+
if MQTTClient == nil || !MQTTClient.IsConnected() {
|
|
62
|
+
return
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
msg := EventMessage{
|
|
66
|
+
Action: action,
|
|
67
|
+
Entity: entity,
|
|
68
|
+
EventTime: time.Now(),
|
|
69
|
+
Payload: payload,
|
|
70
|
+
PreviousValue: prev,
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
data, err := json.Marshal(msg)
|
|
74
|
+
if err != nil {
|
|
75
|
+
log.Printf("Error marshaling MQTT message: %v", err)
|
|
76
|
+
return
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
topic := fmt.Sprintf("%s/%s/%s", topicPrefix, entity, action)
|
|
80
|
+
token := MQTTClient.Publish(topic, 0, false, data)
|
|
81
|
+
token.Wait()
|
|
82
|
+
}
|
|
83
|
+
`;
|
|
84
|
+
|
|
85
|
+
await fs.writeFile(path.join(messagingDir, 'mqtt.go'), mqttClientGo.replace(/{{app_name}}/g, config.name));
|
|
86
|
+
console.log(chalk.gray(' Generated MQTT Messaging Package'));
|
|
87
|
+
};
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
|
|
5
|
+
export const generateMultitenancy = async (config, outputDir) => {
|
|
6
|
+
|
|
7
|
+
const multitenancyTemplate = `
|
|
8
|
+
package middleware
|
|
9
|
+
|
|
10
|
+
import (
|
|
11
|
+
"fmt"
|
|
12
|
+
"net/http"
|
|
13
|
+
|
|
14
|
+
"github.com/gin-gonic/gin"
|
|
15
|
+
"gorm.io/gorm"
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
func TenantMiddleware(db *gorm.DB) gin.HandlerFunc {
|
|
19
|
+
return func(c *gin.Context) {
|
|
20
|
+
// 1. Get roles from JWT (previously set by JWTMiddleware)
|
|
21
|
+
userRolesInterface, exists := c.Get("UserRoles")
|
|
22
|
+
if !exists {
|
|
23
|
+
c.JSON(http.StatusUnauthorized, gin.H{"error": "No roles found in token"})
|
|
24
|
+
c.Abort()
|
|
25
|
+
return
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
roles, ok := userRolesInterface.([]interface{})
|
|
29
|
+
if !ok {
|
|
30
|
+
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid roles format"})
|
|
31
|
+
c.Abort()
|
|
32
|
+
return
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// 2. Lookup DB name for the roles in tenant_roles table
|
|
36
|
+
var dbName string
|
|
37
|
+
err := db.Raw("SELECT db_name FROM tenant_roles WHERE role_name IN ? LIMIT 1", roles).Scan(&dbName).Error
|
|
38
|
+
|
|
39
|
+
if err != nil || dbName == "" {
|
|
40
|
+
// Fallback to default DB if no role match (optional behavior)
|
|
41
|
+
dbName = "go-duck"
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// 3. Store tenant info for downstream use
|
|
45
|
+
c.Set("tenantDB", dbName)
|
|
46
|
+
c.Next()
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
`;
|
|
50
|
+
|
|
51
|
+
const dbApiTemplate = `
|
|
52
|
+
package management
|
|
53
|
+
|
|
54
|
+
import (
|
|
55
|
+
"fmt"
|
|
56
|
+
"net/http"
|
|
57
|
+
"os/exec"
|
|
58
|
+
"{{app_name}}/config"
|
|
59
|
+
|
|
60
|
+
"github.com/gin-gonic/gin"
|
|
61
|
+
"gorm.io/gorm"
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
type DatabaseRequest struct {
|
|
65
|
+
Role string \`json:"role" binding:"required"\`
|
|
66
|
+
DBName string \`json:"db_name" binding:"required"\`
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
func CreateDatabaseAndMigrate(masterDB *gorm.DB) gin.HandlerFunc {
|
|
70
|
+
return func(c *gin.Context) {
|
|
71
|
+
var req DatabaseRequest
|
|
72
|
+
if err := c.ShouldBindJSON(&req); err != nil {
|
|
73
|
+
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
74
|
+
return
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// 1. CREATE DATABASE
|
|
78
|
+
// Note: CREATE DATABASE cannot be run in a transaction.
|
|
79
|
+
// We use the masterDB connection.
|
|
80
|
+
if err := masterDB.Exec(fmt.Sprintf("CREATE DATABASE %s", req.DBName)).Error; err != nil {
|
|
81
|
+
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create database: " + err.Error()})
|
|
82
|
+
return
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// 2. Insert into roles mapping table
|
|
86
|
+
if err := masterDB.Exec("INSERT INTO tenant_roles (role_name, db_name) VALUES (?, ?)", req.Role, req.DBName).Error; err != nil {
|
|
87
|
+
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to map role: " + err.Error()})
|
|
88
|
+
return
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// 3. Start Liquibase Migration for the new tenant
|
|
92
|
+
appConfig, _ := config.LoadConfig()
|
|
93
|
+
ds := appConfig.GoDuck.Datasource
|
|
94
|
+
|
|
95
|
+
// Construct JDBC URL for the new database
|
|
96
|
+
jdbcUrl := fmt.Sprintf("jdbc:postgresql://%s:%d/%s", ds.Host, ds.Port, req.DBName)
|
|
97
|
+
|
|
98
|
+
fmt.Printf("Migrating new tenant DB: %s\\n", req.DBName)
|
|
99
|
+
|
|
100
|
+
cmd := exec.Command("liquibase",
|
|
101
|
+
"--url=" + jdbcUrl,
|
|
102
|
+
"--username=" + ds.Username,
|
|
103
|
+
"--password=" + ds.Password,
|
|
104
|
+
"--changeLogFile=migrations/master.xml",
|
|
105
|
+
"update")
|
|
106
|
+
|
|
107
|
+
if err := cmd.Run(); err != nil {
|
|
108
|
+
fmt.Printf("Liquibase Error: %v\\n", err)
|
|
109
|
+
// We don't fail the whole request because the DB is created,
|
|
110
|
+
// but we warn the admin.
|
|
111
|
+
c.JSON(http.StatusOK, gin.H{"message": "Database created but migration failed to auto-start. Please run manually.", "error": err.Error()})
|
|
112
|
+
return
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
c.JSON(http.StatusOK, gin.H{"message": "Database created, role mapped, and migration completed for " + req.Role})
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
`;
|
|
119
|
+
|
|
120
|
+
const middlewarePath = path.join(outputDir, 'middleware/tenant_middleware.go');
|
|
121
|
+
const dbApiPath = path.join(outputDir, 'management/db_controller.go');
|
|
122
|
+
|
|
123
|
+
await fs.ensureDir(path.join(outputDir, 'middleware'));
|
|
124
|
+
await fs.ensureDir(path.join(outputDir, 'management'));
|
|
125
|
+
|
|
126
|
+
await fs.writeFile(middlewarePath, multitenancyTemplate);
|
|
127
|
+
await fs.writeFile(dbApiPath, dbApiTemplate.replace('{{app_name}}', config.name));
|
|
128
|
+
|
|
129
|
+
console.log(chalk.gray(' Generated Multitenancy Middleware & Management API'));
|
|
130
|
+
};
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
|
|
5
|
+
export const generatePostgRESTCode = async (config, outputDir) => {
|
|
6
|
+
const controllersDir = path.join(outputDir, 'controllers');
|
|
7
|
+
await fs.ensureDir(controllersDir);
|
|
8
|
+
|
|
9
|
+
const postgrestController = `
|
|
10
|
+
package controllers
|
|
11
|
+
|
|
12
|
+
import (
|
|
13
|
+
"fmt"
|
|
14
|
+
"net/http"
|
|
15
|
+
"strings"
|
|
16
|
+
|
|
17
|
+
"github.com/gin-gonic/gin"
|
|
18
|
+
"gorm.io/gorm"
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
type SearchController struct {
|
|
22
|
+
DB *gorm.DB
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// GenericSearch handles PostgREST-like queries
|
|
26
|
+
// Syntax: /api/search/:table?age=gt.20&order=id.desc&limit=10&offset=0
|
|
27
|
+
func (sc *SearchController) GenericSearch(c *gin.Context) {
|
|
28
|
+
tableName := c.Param("table")
|
|
29
|
+
query := sc.DB.Table(tableName)
|
|
30
|
+
|
|
31
|
+
// Apply Filters
|
|
32
|
+
params := c.Request.URL.Query()
|
|
33
|
+
for key, values := range params {
|
|
34
|
+
if key == "order" || key == "limit" || key == "offset" || key == "select" {
|
|
35
|
+
continue
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
for _, val := range values {
|
|
39
|
+
parts := strings.SplitN(val, ".", 2)
|
|
40
|
+
if len(parts) < 2 {
|
|
41
|
+
// Default to equality
|
|
42
|
+
query = query.Where(fmt.Sprintf("%s = ?", key), val)
|
|
43
|
+
continue
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
op := parts[0]
|
|
47
|
+
target := parts[1]
|
|
48
|
+
|
|
49
|
+
switch op {
|
|
50
|
+
case "eq":
|
|
51
|
+
query = query.Where(fmt.Sprintf("%s = ?", key), target)
|
|
52
|
+
case "neq":
|
|
53
|
+
query = query.Where(fmt.Sprintf("%s <> ?", key), target)
|
|
54
|
+
case "gt":
|
|
55
|
+
query = query.Where(fmt.Sprintf("%s > ?", key), target)
|
|
56
|
+
case "gte":
|
|
57
|
+
query = query.Where(fmt.Sprintf("%s >= ?", key), target)
|
|
58
|
+
case "lt":
|
|
59
|
+
query = query.Where(fmt.Sprintf("%s < ?", key), target)
|
|
60
|
+
case "lte":
|
|
61
|
+
query = query.Where(fmt.Sprintf("%s <= ?", key), target)
|
|
62
|
+
case "like":
|
|
63
|
+
query = query.Where(fmt.Sprintf("%s LIKE ?", key), "%"+target+"%")
|
|
64
|
+
case "ilike":
|
|
65
|
+
query = query.Where(fmt.Sprintf("%s ILIKE ?", key), "%"+target+"%")
|
|
66
|
+
case "in":
|
|
67
|
+
list := strings.Split(target, ",")
|
|
68
|
+
query = query.Where(fmt.Sprintf("%s IN ?", key), list)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Apply Sorting
|
|
74
|
+
if order := c.Query("order"); order != "" {
|
|
75
|
+
parts := strings.SplitN(order, ".", 2)
|
|
76
|
+
field := parts[0]
|
|
77
|
+
direction := "asc"
|
|
78
|
+
if len(parts) > 1 {
|
|
79
|
+
direction = parts[1]
|
|
80
|
+
}
|
|
81
|
+
query = query.Order(fmt.Sprintf("%s %s", field, direction))
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Apply Pagination
|
|
85
|
+
if limit := c.Query("limit"); limit != "" {
|
|
86
|
+
query = query.Limit(parseInt(limit, 10))
|
|
87
|
+
} else {
|
|
88
|
+
query = query.Limit(100) // Default limit
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if offset := c.Query("offset"); offset != "" {
|
|
92
|
+
query = query.Offset(parseInt(offset, 0))
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
var results []map[string]interface{}
|
|
96
|
+
if err := query.Find(&results).Error; err != nil {
|
|
97
|
+
c.JSON(http.StatusInternalServerError, gin.H{"error": "Search failed: " + err.Error()})
|
|
98
|
+
return
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
c.JSON(http.StatusOK, results)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
func parseInt(s string, def int) int {
|
|
105
|
+
var val int
|
|
106
|
+
if _, err := fmt.Sscanf(s, "%d", &val); err != nil {
|
|
107
|
+
return def
|
|
108
|
+
}
|
|
109
|
+
return val
|
|
110
|
+
}
|
|
111
|
+
`;
|
|
112
|
+
|
|
113
|
+
await fs.writeFile(path.join(controllersDir, 'search_controller.go'), postgrestController);
|
|
114
|
+
console.log(chalk.gray(' Generated PostgREST-like Search Controller'));
|
|
115
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
|
|
5
|
+
export const generateRepositoryCode = async (outputDir) => {
|
|
6
|
+
const repoDir = path.join(outputDir, 'internal', 'repository');
|
|
7
|
+
await fs.ensureDir(repoDir);
|
|
8
|
+
|
|
9
|
+
const repoGo = `package repository
|
|
10
|
+
|
|
11
|
+
import (
|
|
12
|
+
"gorm.io/gorm"
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
type Repository struct {
|
|
16
|
+
DB *gorm.DB
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
func NewRepository(db *gorm.DB) *Repository {
|
|
20
|
+
return &Repository{
|
|
21
|
+
DB: db,
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
`;
|
|
25
|
+
|
|
26
|
+
await fs.writeFile(path.join(repoDir, 'repository.go'), repoGo);
|
|
27
|
+
console.log(chalk.gray(' Generated Internal Repository Layer'));
|
|
28
|
+
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
|
|
5
|
+
export const generateResilienceCode = async (config, outputDir) => {
|
|
6
|
+
const resilienceDir = path.join(outputDir, 'resilience');
|
|
7
|
+
await fs.ensureDir(resilienceDir);
|
|
8
|
+
|
|
9
|
+
const circuitBreakerGo = `
|
|
10
|
+
package resilience
|
|
11
|
+
|
|
12
|
+
import (
|
|
13
|
+
"fmt"
|
|
14
|
+
"log"
|
|
15
|
+
"time"
|
|
16
|
+
|
|
17
|
+
"{{app_name}}/config"
|
|
18
|
+
"github.com/sony/gobreaker"
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
var (
|
|
22
|
+
CB *gobreaker.CircuitBreaker
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
// InitResilience initializes circuit breaker settings
|
|
26
|
+
func InitResilience(cfg *config.Config) {
|
|
27
|
+
if !cfg.GoDuck.Resilience.CircuitBreaker.Enabled {
|
|
28
|
+
log.Println("Circuit Breaker is disabled.")
|
|
29
|
+
return
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
st := gobreaker.Settings{
|
|
33
|
+
Name: "Global Breaker",
|
|
34
|
+
Interval: time.Minute,
|
|
35
|
+
Timeout: cfg.GoDuck.Resilience.CircuitBreaker.Timeout,
|
|
36
|
+
MaxRequests: cfg.GoDuck.Resilience.CircuitBreaker.SuccessThreshold,
|
|
37
|
+
ReadyToTrip: func(counts gobreaker.Counts) bool {
|
|
38
|
+
return counts.ConsecutiveFailures >= cfg.GoDuck.Resilience.CircuitBreaker.FailureThreshold
|
|
39
|
+
},
|
|
40
|
+
OnStateChange: func(name string, from gobreaker.State, to gobreaker.State) {
|
|
41
|
+
log.Printf("Circuit Breaker %s: State Changed from %s to %s", name, from.String(), to.String())
|
|
42
|
+
},
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
CB = gobreaker.NewCircuitBreaker(st)
|
|
46
|
+
log.Println("Circuit Breaker initialized.")
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Execute wraps any function call in a circuit breaker
|
|
50
|
+
func Execute(f func() (interface{}, error)) (interface{}, error) {
|
|
51
|
+
if CB == nil {
|
|
52
|
+
return f()
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
result, err := CB.Execute(f)
|
|
56
|
+
if err != nil {
|
|
57
|
+
if err == gobreaker.ErrOpenState {
|
|
58
|
+
return nil, fmt.Errorf("circuit breaker is OPEN")
|
|
59
|
+
}
|
|
60
|
+
return nil, err
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return result, nil
|
|
64
|
+
}
|
|
65
|
+
`;
|
|
66
|
+
|
|
67
|
+
await fs.writeFile(path.join(resilienceDir, 'circuit_breaker.go'), circuitBreakerGo.replace(/{{app_name}}/g, config.name));
|
|
68
|
+
console.log(chalk.gray(' Generated Circuit Breaker Resilience Package'));
|
|
69
|
+
};
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
|
|
5
|
+
export const generateSecurityMiddleware = async (config, outputDir) => {
|
|
6
|
+
const middlewareDir = path.join(outputDir, 'middleware');
|
|
7
|
+
await fs.ensureDir(middlewareDir);
|
|
8
|
+
|
|
9
|
+
const jwtMiddleware = `
|
|
10
|
+
package middleware
|
|
11
|
+
|
|
12
|
+
import (
|
|
13
|
+
"net/http"
|
|
14
|
+
"strings"
|
|
15
|
+
|
|
16
|
+
"github.com/gin-gonic/gin"
|
|
17
|
+
"github.com/golang-jwt/jwt/v4"
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
// JWTMiddleware validates Keycloak JWTs
|
|
21
|
+
func JWTMiddleware() gin.HandlerFunc {
|
|
22
|
+
return func(c *gin.Context) {
|
|
23
|
+
authHeader := c.GetHeader("Authorization")
|
|
24
|
+
if authHeader == "" {
|
|
25
|
+
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Authorization header required"})
|
|
26
|
+
return
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
parts := strings.Split(authHeader, " ")
|
|
30
|
+
if len(parts) != 2 || parts[0] != "Bearer" {
|
|
31
|
+
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Invalid token format"})
|
|
32
|
+
return
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
tokenString := parts[1]
|
|
36
|
+
|
|
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"})
|
|
40
|
+
return
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if claims, ok := token.Claims.(jwt.MapClaims); ok {
|
|
44
|
+
c.Set("KeycloakID", claims["sub"])
|
|
45
|
+
c.Set("UserEmail", claims["email"])
|
|
46
|
+
if ra, ok := claims["realm_access"].(map[string]interface{}); ok {
|
|
47
|
+
c.Set("UserRoles", ra["roles"])
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
c.Next()
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
func GetUserID(c *gin.Context) string {
|
|
56
|
+
val, _ := c.Get("KeycloakID")
|
|
57
|
+
if str, ok := val.(string); ok {
|
|
58
|
+
return str
|
|
59
|
+
}
|
|
60
|
+
return "anonymous"
|
|
61
|
+
}
|
|
62
|
+
`;
|
|
63
|
+
|
|
64
|
+
const rateLimitMiddleware = `
|
|
65
|
+
package middleware
|
|
66
|
+
|
|
67
|
+
import (
|
|
68
|
+
"net/http"
|
|
69
|
+
"sync"
|
|
70
|
+
"{{app_name}}/config"
|
|
71
|
+
|
|
72
|
+
"github.com/gin-gonic/gin"
|
|
73
|
+
"golang.org/x/time/rate"
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
var (
|
|
77
|
+
limiters = make(map[string]*rate.Limiter)
|
|
78
|
+
mu sync.Mutex
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
// RateLimitMiddleware provides burst protection based on configuration
|
|
82
|
+
func RateLimitMiddleware(cfg *config.Config) gin.HandlerFunc {
|
|
83
|
+
return func(c *gin.Context) {
|
|
84
|
+
ip := c.ClientIP()
|
|
85
|
+
|
|
86
|
+
mu.Lock()
|
|
87
|
+
limiter, exists := limiters[ip]
|
|
88
|
+
if !exists {
|
|
89
|
+
rps := cfg.GoDuck.Security.RateLimit.RPS
|
|
90
|
+
burst := cfg.GoDuck.Security.RateLimit.Burst
|
|
91
|
+
limiter = rate.NewLimiter(rate.Limit(rps), burst)
|
|
92
|
+
limiters[ip] = limiter
|
|
93
|
+
}
|
|
94
|
+
mu.Unlock()
|
|
95
|
+
|
|
96
|
+
if !limiter.Allow() {
|
|
97
|
+
c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{
|
|
98
|
+
"error": "Rate limit exceeded. Please try again later.",
|
|
99
|
+
})
|
|
100
|
+
return
|
|
101
|
+
}
|
|
102
|
+
c.Next()
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
`;
|
|
106
|
+
|
|
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));
|
|
109
|
+
|
|
110
|
+
const corsMiddleware = `
|
|
111
|
+
package middleware
|
|
112
|
+
|
|
113
|
+
import (
|
|
114
|
+
"{{app_name}}/config"
|
|
115
|
+
"github.com/gin-gonic/gin"
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
func CORSMiddleware(cfg *config.Config) gin.HandlerFunc {
|
|
119
|
+
return func(c *gin.Context) {
|
|
120
|
+
origins := cfg.GoDuck.Server.CORS.AllowOrigins
|
|
121
|
+
methods := cfg.GoDuck.Server.CORS.AllowMethods
|
|
122
|
+
headers := cfg.GoDuck.Server.CORS.AllowHeaders
|
|
123
|
+
|
|
124
|
+
origin := c.Request.Header.Get("Origin")
|
|
125
|
+
allowOrigin := ""
|
|
126
|
+
for _, o := range origins {
|
|
127
|
+
if o == "*" || o == origin {
|
|
128
|
+
allowOrigin = origin
|
|
129
|
+
if o == "*" {
|
|
130
|
+
allowOrigin = "*"
|
|
131
|
+
}
|
|
132
|
+
break
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if allowOrigin != "" {
|
|
137
|
+
c.Writer.Header().Set("Access-Control-Allow-Origin", allowOrigin)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
|
|
141
|
+
|
|
142
|
+
headerString := ""
|
|
143
|
+
for i, h := range headers {
|
|
144
|
+
if i > 0 { headerString += ", " }
|
|
145
|
+
headerString += h
|
|
146
|
+
}
|
|
147
|
+
c.Writer.Header().Set("Access-Control-Allow-Headers", headerString)
|
|
148
|
+
|
|
149
|
+
methodString := ""
|
|
150
|
+
for i, m := range methods {
|
|
151
|
+
if i > 0 { methodString += ", " }
|
|
152
|
+
methodString += m
|
|
153
|
+
}
|
|
154
|
+
c.Writer.Header().Set("Access-Control-Allow-Methods", methodString)
|
|
155
|
+
|
|
156
|
+
if c.Request.Method == "OPTIONS" {
|
|
157
|
+
c.AbortWithStatus(204)
|
|
158
|
+
return
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
c.Next()
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
`;
|
|
165
|
+
|
|
166
|
+
await fs.writeFile(path.join(middlewareDir, 'cors_middleware.go'), corsMiddleware.replace('{{app_name}}', config.name));
|
|
167
|
+
console.log(chalk.gray(' Generated Advanced Security & CORS Middleware'));
|
|
168
|
+
};
|