@kernelminds/create-enclave 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,488 @@
1
+ package main
2
+
3
+ import (
4
+ "context"
5
+ "fmt"
6
+ "log"
7
+ "math/rand"
8
+ "net/http"
9
+ "os"
10
+ "path/filepath"
11
+ "strconv"
12
+ "strings"
13
+ "sync"
14
+ "time"
15
+
16
+ "github.com/gin-gonic/gin"
17
+ "github.com/go-redis/redis"
18
+ "github.com/scailo/go-sdk"
19
+ "google.golang.org/grpc"
20
+ "google.golang.org/grpc/credentials"
21
+ "google.golang.org/grpc/credentials/insecure"
22
+ "google.golang.org/grpc/metadata"
23
+ "google.golang.org/protobuf/proto"
24
+
25
+ "github.com/gorilla/securecookie"
26
+
27
+ "github.com/joho/godotenv"
28
+ _ "github.com/joho/godotenv/autoload"
29
+ )
30
+
31
+ // Config holds all necessary environment variables
32
+ type Config struct {
33
+ EnclaveName string
34
+ ScailoAPI string
35
+ Port int
36
+ Username string
37
+ Password string
38
+
39
+ RedisUsername string
40
+ RedisPassword string
41
+ RedisURL string
42
+
43
+ WorkflowEventsChannel string
44
+
45
+ CookieSignatureSecret string
46
+ }
47
+
48
+ // Global state variables
49
+ var (
50
+ GlobalConfig Config
51
+ AuthToken string
52
+ // Use this context for all Scailo API calls
53
+ ScailoAPICtx context.Context
54
+ // Mutex to protect shared state
55
+ mu sync.RWMutex
56
+ production bool = false
57
+ indexPage string // Cached version of index.html
58
+ enclavePrefix string
59
+ RedisClient *redis.Client
60
+ )
61
+
62
+ const (
63
+ // 12-hour interval (3600 * 12)
64
+ loginInterval = 3600 * 12 * time.Second
65
+ indexHTMLFile = "index.html"
66
+ )
67
+
68
+ func init() {
69
+ // Initialize the random number generator
70
+ // rand.Seed(time.Now().UnixNano())
71
+ }
72
+
73
+ // loadConfig reads and validates environment variables.
74
+ func loadConfig() {
75
+ // Try loading .env
76
+ godotenv.Load(".env")
77
+ // We'll respect the user's `production` flag.
78
+ if os.Getenv("PRODUCTION") == "true" {
79
+ production = true
80
+ } else {
81
+ // Assume development mode by default
82
+ production = false
83
+ }
84
+
85
+ // 1. Read environment variables
86
+ GlobalConfig.EnclaveName = os.Getenv("ENCLAVE_NAME")
87
+ GlobalConfig.ScailoAPI = os.Getenv("SCAILO_API")
88
+ GlobalConfig.Username = os.Getenv("USERNAME")
89
+ GlobalConfig.Password = os.Getenv("PASSWORD")
90
+
91
+ GlobalConfig.RedisUsername = os.Getenv("REDIS_USERNAME")
92
+ GlobalConfig.RedisPassword = os.Getenv("REDIS_PASSWORD")
93
+ GlobalConfig.RedisURL = os.Getenv("REDIS_URL")
94
+
95
+ GlobalConfig.WorkflowEventsChannel = os.Getenv("WORKFLOW_EVENTS_CHANNEL")
96
+ GlobalConfig.CookieSignatureSecret = os.Getenv("COOKIE_SIGNATURE_SECRET")
97
+
98
+ portStr := os.Getenv("PORT")
99
+ if portStr != "" {
100
+ p, err := strconv.Atoi(portStr)
101
+ if err == nil {
102
+ GlobalConfig.Port = p
103
+ }
104
+ }
105
+
106
+ // 2. Validate environment variables
107
+ var exitCode = 0
108
+ if GlobalConfig.EnclaveName == "" {
109
+ log.Println("ENCLAVE_NAME not set")
110
+ exitCode = 1
111
+ }
112
+ if GlobalConfig.ScailoAPI == "" {
113
+ log.Println("SCAILO_API not set")
114
+ exitCode = 1
115
+ }
116
+ if GlobalConfig.Port == 0 {
117
+ log.Println("PORT not set or is 0")
118
+ exitCode = 1
119
+ }
120
+ if GlobalConfig.Username == "" {
121
+ log.Println("USERNAME not set")
122
+ exitCode = 1
123
+ }
124
+ if GlobalConfig.Password == "" {
125
+ log.Println("PASSWORD not set")
126
+ exitCode = 1
127
+ }
128
+ if GlobalConfig.RedisURL == "" {
129
+ log.Println("REDIS_URL not set")
130
+ exitCode = 1
131
+ }
132
+ if GlobalConfig.WorkflowEventsChannel == "" {
133
+ log.Println("WORKFLOW_EVENTS_CHANNEL not set")
134
+ exitCode = 1
135
+ }
136
+ if GlobalConfig.CookieSignatureSecret == "" {
137
+ log.Println("COOKIE_SIGNATURE_SECRET not set")
138
+ exitCode = 1
139
+ }
140
+
141
+ enclavePrefix = getEnclavePrefix(GlobalConfig.EnclaveName)
142
+
143
+ if exitCode != 0 {
144
+ os.Exit(exitCode)
145
+ }
146
+ }
147
+
148
+ // loginToAPI logs into the Scailo API
149
+ func loginToAPI(conn *grpc.ClientConn) {
150
+ // This function uses a goroutine to run asynchronously and recursively.
151
+ RedisClient = redis.NewClient(&redis.Options{
152
+ Addr: GlobalConfig.RedisURL,
153
+ Password: GlobalConfig.RedisPassword,
154
+ MaxRetries: 5,
155
+ DialTimeout: 10 * time.Second,
156
+ ReadTimeout: 30 * time.Second,
157
+ WriteTimeout: 30 * time.Second,
158
+ PoolSize: 10,
159
+ MinIdleConns: 5,
160
+ PoolTimeout: 30 * time.Second,
161
+ IdleTimeout: 15 * time.Minute,
162
+ IdleCheckFrequency: 1 * time.Minute,
163
+ })
164
+ go handleWorkflowEvents(RedisClient)
165
+
166
+ // Create a Ticker for the recurring job (1 hour)
167
+ ticker := time.NewTicker(loginInterval)
168
+
169
+ // Start the initial login immediately, then wait for the ticker.
170
+ performLogin(conn)
171
+
172
+ // Wait for the ticker events in a separate goroutine
173
+ go func() {
174
+ for range ticker.C {
175
+ performLogin(conn)
176
+ }
177
+ }()
178
+ }
179
+
180
+ func getServerURL() string {
181
+ if strings.HasPrefix(GlobalConfig.ScailoAPI, "http") || strings.Contains(GlobalConfig.ScailoAPI, "//") {
182
+ var split = strings.Split(GlobalConfig.ScailoAPI, "//")
183
+ if len(split) > 1 {
184
+ return split[1]
185
+ }
186
+ }
187
+
188
+ return GlobalConfig.ScailoAPI
189
+ }
190
+
191
+ func handleWorkflowEvents(redisClient *redis.Client) error {
192
+ subscription := redisClient.Subscribe(GlobalConfig.WorkflowEventsChannel)
193
+ for message := range subscription.Channel() {
194
+ fmt.Println("Received message on workflow channel")
195
+ var workflowEvent = new(sdk.WorkflowEvent)
196
+
197
+ err := proto.Unmarshal([]byte(message.Payload), workflowEvent)
198
+ if err != nil {
199
+ fmt.Printf("Unable to unmarshal workflow event: %s\n", err.Error())
200
+ continue
201
+ }
202
+ fmt.Println("Unmarshalled the message with rule code: ", workflowEvent.RuleCode, " for service: ", workflowEvent.ServiceName.String())
203
+
204
+ // Sample code to handle a sales order related workflow rule
205
+ if workflowEvent.RuleCode == "" {
206
+ // Handle sales order
207
+ var salesorder = new(sdk.SalesOrder)
208
+ err = proto.Unmarshal(workflowEvent.TransactionPayload, salesorder)
209
+ if err != nil {
210
+ fmt.Println("Unable to unmarshal sales order: ", err)
211
+ continue
212
+ }
213
+ fmt.Println("Unmarshalled transaction payload for rule code: ", workflowEvent.RuleCode)
214
+ // Use the ScailoAPICtx to further process the sales order
215
+ }
216
+ }
217
+ return nil
218
+ }
219
+
220
+ func getGRPCConnection() *grpc.ClientConn {
221
+ var creds grpc.DialOption
222
+ if strings.HasPrefix(GlobalConfig.ScailoAPI, "http://") {
223
+ // Without TLS
224
+ creds = grpc.WithTransportCredentials(insecure.NewCredentials())
225
+ } else {
226
+ // With TLS
227
+ creds = grpc.WithTransportCredentials(credentials.NewClientTLSFromCert(nil, getServerURL()))
228
+ }
229
+
230
+ conn, err := grpc.NewClient(getServerURL(), creds)
231
+ if err != nil {
232
+ log.Fatalf("did not connect: %v", err)
233
+ }
234
+ return conn
235
+ }
236
+
237
+ func performLogin(conn *grpc.ClientConn) {
238
+ log.Println("About to login to API")
239
+
240
+ ctx := context.Background()
241
+
242
+ loginClient := sdk.NewLoginServiceClient(conn)
243
+ loginResp, err := loginClient.LoginAsEmployeePrimary(ctx, &sdk.UserLoginRequest{
244
+ Username: GlobalConfig.Username,
245
+ PlainTextPassword: GlobalConfig.Password,
246
+ })
247
+ if err != nil {
248
+ panic(err)
249
+ }
250
+
251
+ md := metadata.Pairs(
252
+ "auth_token", loginResp.AuthToken,
253
+ )
254
+
255
+ // 4. Create a new context with the metadata attached.
256
+ ScailoAPICtx = metadata.NewOutgoingContext(ctx, md)
257
+
258
+ mu.Lock()
259
+ AuthToken = loginResp.AuthToken
260
+ mu.Unlock()
261
+
262
+ log.Printf("Logged in with auth token: %s", AuthToken)
263
+ }
264
+
265
+ // replaceBundleCaches implements the cache-busting logic
266
+ func replaceBundleCaches(page string) string {
267
+ version := time.Now().Format("20060102150405") // YYYYMMDDhhmmss format
268
+
269
+ // Replace script preload
270
+ page = IndexPageReplacer(page,
271
+ fmt.Sprintf(`<link rel="preload" as="script" href="%s/resources/dist/js/bundle.src.min.js">`, enclavePrefix),
272
+ fmt.Sprintf(`<link rel="preload" as="script" href="%s/resources/dist/js/bundle.src.min.js?v=%s">`, enclavePrefix, version))
273
+
274
+ // Replace script src
275
+ page = IndexPageReplacer(page,
276
+ fmt.Sprintf(`<script src="%s/resources/dist/js/bundle.src.min.js"></script>`, enclavePrefix),
277
+ fmt.Sprintf(`<script src="%s/resources/dist/js/bundle.src.min.js?v=%s"></script>`, enclavePrefix, version))
278
+
279
+ // Replace stylesheet link
280
+ page = IndexPageReplacer(page,
281
+ fmt.Sprintf(`<link rel="stylesheet" href="%s/resources/dist/css/bundle.css">`, enclavePrefix),
282
+ fmt.Sprintf(`<link rel="stylesheet" href="%s/resources/dist/css/bundle.css?v=%s">`, enclavePrefix, version))
283
+
284
+ return page
285
+ }
286
+
287
+ // IndexPageReplacer is a helper to centralize string replacement with logging.
288
+ func IndexPageReplacer(s, old, new string) string {
289
+ return strings.ReplaceAll(s, old, new)
290
+ }
291
+
292
+ // indexHandler is the single handler for all root/SPA routes.
293
+ func indexHandler(c *gin.Context) {
294
+ // 1. Read index.html logic
295
+ if !production || indexPage == "" {
296
+ content, err := os.ReadFile(indexHTMLFile)
297
+ if err != nil {
298
+ log.Printf("Error reading index.html: %v", err)
299
+ c.String(http.StatusInternalServerError, "Index page not found.")
300
+ return
301
+ }
302
+ indexPage = string(content)
303
+ }
304
+
305
+ // 2. Cache busting logic
306
+ pageWithCache := replaceBundleCaches(indexPage)
307
+
308
+ // 3. Set headers and send response
309
+ c.Header("Content-Type", "text/html")
310
+ c.String(http.StatusOK, pageWithCache)
311
+ }
312
+
313
+ // AuthMiddleware is our "Gatekeeper"
314
+ func AuthMiddleware(cookieCoder *securecookie.SecureCookie, cookieAuthTokenName string) gin.HandlerFunc {
315
+ return func(c *gin.Context) {
316
+ val, err := c.Cookie(cookieAuthTokenName)
317
+ if err != nil {
318
+ c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
319
+ return
320
+ }
321
+
322
+ // Decode the value
323
+ var authToken string
324
+ cookieCoder.Decode(cookieAuthTokenName, val, &authToken)
325
+
326
+ if authToken == "" {
327
+ c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
328
+ return
329
+ }
330
+
331
+ // Store the unsigned value in the context for the next handler
332
+ c.Set(cookieAuthTokenName, authToken)
333
+ c.Next()
334
+ }
335
+ }
336
+
337
+ func getNewContextForAuthToken(authToken string) context.Context {
338
+ ctx := context.Background()
339
+ md := metadata.Pairs(
340
+ "auth_token", authToken,
341
+ )
342
+
343
+ // 4. Create a new context with the metadata attached.
344
+ return metadata.NewOutgoingContext(ctx, md)
345
+ }
346
+
347
+ // main entry point
348
+ func main() {
349
+ loadConfig()
350
+
351
+ conn := getGRPCConnection()
352
+ defer conn.Close()
353
+
354
+ var cookieCoder = securecookie.New([]byte(GlobalConfig.CookieSignatureSecret), nil)
355
+
356
+ // Start the recurring login process asynchronously
357
+ go loginToAPI(conn)
358
+
359
+ // Set Gin to release mode if in production
360
+ if production {
361
+ gin.SetMode(gin.ReleaseMode)
362
+ }
363
+
364
+ vaultClient := sdk.NewVaultServiceClient(conn)
365
+ purchaseOrdersClient := sdk.NewPurchasesOrdersServiceClient(conn)
366
+ vendorsClient := sdk.NewVendorsServiceClient(conn)
367
+
368
+ var cookieAuthTokenName = fmt.Sprintf("%s_auth_token", GlobalConfig.EnclaveName)
369
+
370
+ // Initialize Gin
371
+ router := gin.Default()
372
+
373
+ // --- 1. Register Static Routes ---
374
+ router.Static(fmt.Sprintf("%s/resources/dist", enclavePrefix), filepath.Join("resources", "dist"))
375
+
376
+ // --- 2. Health Checks ---
377
+ router.GET(fmt.Sprintf("%s/health/startup", enclavePrefix), func(c *gin.Context) {
378
+ c.JSON(http.StatusOK, gin.H{"status": "OK"})
379
+ })
380
+ router.GET(fmt.Sprintf("%s/health/liveliness", enclavePrefix), func(c *gin.Context) {
381
+ c.JSON(http.StatusOK, gin.H{"status": "OK"})
382
+ })
383
+ router.GET(fmt.Sprintf("%s/health/readiness", enclavePrefix), func(c *gin.Context) {
384
+ c.JSON(http.StatusOK, gin.H{"status": "OK"})
385
+ })
386
+
387
+ // --- 3. API Endpoint ---
388
+ // Using a parameter for enclaveName so it matches the route pattern exactly
389
+ router.GET(fmt.Sprintf("%s/api/random", enclavePrefix), func(c *gin.Context) {
390
+ // Generate a random float between 0.0 and 1.0 (like Math.random())
391
+ randomNumber := rand.Float64()
392
+ c.JSON(http.StatusOK, gin.H{"random": randomNumber})
393
+ })
394
+
395
+ // --- 4. Index Page / SPA Routes (all pointing to the same handler) ---
396
+ // Specific UI routes
397
+ uiPath1 := fmt.Sprintf("%s/ui", enclavePrefix)
398
+ uiPath2 := fmt.Sprintf("%s/ui/*path", enclavePrefix)
399
+
400
+ router.GET(uiPath1, indexHandler)
401
+ router.GET(uiPath2, indexHandler)
402
+
403
+ // Implicit redirect for entry_point_management = direct_url
404
+ router.GET("/", func(c *gin.Context) {
405
+ c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/ui", enclavePrefix))
406
+ })
407
+
408
+ // ------------------------------------------------------------------------------------------------------------------------
409
+ // Enclave Ingress handler
410
+ router.GET(fmt.Sprintf("%s/ingress/:token", enclavePrefix), func(c *gin.Context) {
411
+ var cookieToSet string
412
+ var expiresIn int
413
+ if !production {
414
+ // In dev, use the default auth token
415
+ expiresIn = 3600
416
+ cookieToSet = AuthToken
417
+ } else {
418
+ token := c.Param("token")
419
+ if token == "" {
420
+ c.JSON(http.StatusBadRequest, gin.H{"error": "Missing token"})
421
+ return
422
+ }
423
+ // Correctly verify the ingress token
424
+ ingress, err := vaultClient.VerifyEnclaveIngress(ScailoAPICtx, &sdk.VerifyEnclaveIngressRequest{Token: token})
425
+ if err != nil {
426
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
427
+ return
428
+ }
429
+ expiresIn = int(ingress.ExpiresAt) - int(time.Now().Unix())
430
+ cookieToSet = ingress.AuthToken
431
+
432
+ }
433
+ securedCookieValue, err := cookieCoder.Encode(cookieAuthTokenName, cookieToSet)
434
+ if err != nil {
435
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
436
+ return
437
+ }
438
+
439
+ c.SetCookie(cookieAuthTokenName, securedCookieValue, expiresIn, "/", "", false, true)
440
+ c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/ui", enclavePrefix))
441
+ })
442
+
443
+ // Protected routes
444
+ protected := router.Group(fmt.Sprintf("%s/protected", enclavePrefix))
445
+ protected.Use(AuthMiddleware(cookieCoder, cookieAuthTokenName))
446
+ {
447
+ protected.GET("/api/random", func(c *gin.Context) {
448
+ // Generate a random float between 0.0 and 1.0 (like Math.random())
449
+
450
+ authToken, exists := c.Get(cookieAuthTokenName)
451
+ if !exists {
452
+ c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
453
+ return
454
+ }
455
+
456
+ ctx := getNewContextForAuthToken(authToken.(string))
457
+ purchaseOrdersList, err := purchaseOrdersClient.Filter(ctx, &sdk.PurchasesOrdersServiceFilterReq{IsActive: sdk.BOOL_FILTER_BOOL_FILTER_TRUE, Count: -1})
458
+ if err != nil {
459
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
460
+ return
461
+ }
462
+ vendorsList, err := vendorsClient.Filter(ctx, &sdk.VendorsServiceFilterReq{IsActive: sdk.BOOL_FILTER_BOOL_FILTER_TRUE, Count: -1})
463
+ if err != nil {
464
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
465
+ return
466
+ }
467
+
468
+ randomNumber := rand.Float64()
469
+ c.JSON(http.StatusOK, gin.H{"random": randomNumber, "purchaseOrders": purchaseOrdersList.List, "vendors": vendorsList.List})
470
+ })
471
+ }
472
+
473
+ // ------------------------------------------------------------------------------------------------------------------------
474
+
475
+ // --- 5. Not Found Handler ---
476
+ router.NoRoute(func(c *gin.Context) {
477
+ // Only redirect if the request is not for a resource that failed to load (e.g., /resources/dist/404)
478
+ c.Redirect(http.StatusTemporaryRedirect, uiPath1)
479
+ })
480
+
481
+ // --- 6. Start Server ---
482
+ address := fmt.Sprintf("0.0.0.0:%d", GlobalConfig.Port)
483
+ log.Printf("Listening on address %s with Production: %t", address, production)
484
+
485
+ if err := router.Run(address); err != nil {
486
+ log.Fatalf("Server failed to start: %v", err)
487
+ }
488
+ }
@@ -0,0 +1,7 @@
1
+ package main
2
+
3
+ import "fmt"
4
+
5
+ func getEnclavePrefix(enclaveName string) string {
6
+ return fmt.Sprintf("/enclave/%s", enclaveName)
7
+ }