package main import ( "context" "errors" "log/slog" "net/http" "os" "os/signal" "strconv" "strings" "syscall" "time" "github.com/b1tsblog/license-platform/internal/platform" ) func main() { if len(os.Args) > 1 && os.Args[1] == "--healthcheck" { healthcheck() return } logger := slog.New(slog.NewJSONHandler(os.Stdout, nil)) store, err := platform.OpenStore(env("LICENSE_DATA_FILE", "./data/platform.json")) if err != nil { logger.Error("open data store", "error", err) os.Exit(1) } app, err := platform.New(platform.Config{ Brand: env("LICENSE_BRAND", "License Platform"), Address: env("LICENSE_ADDRESS", ":8090"), PublicURL: env("LICENSE_PUBLIC_URL", "http://localhost:8090"), DataFile: env("LICENSE_DATA_FILE", "./data/platform.json"), MasterKey: secretEnv("LICENSE_MASTER_KEY"), BootstrapUsername: env("LICENSE_BOOTSTRAP_ADMIN_USER", "admin"), BootstrapPassword: secretEnv("LICENSE_BOOTSTRAP_ADMIN_PASSWORD"), BootstrapName: env("LICENSE_BOOTSTRAP_ADMIN_NAME", "Administrator"), AdminAPIToken: firstSecret("LICENSE_ADMIN_API_TOKEN", "LICENSE_SERVER_ADMIN_TOKEN"), SessionTTL: durationEnv("LICENSE_SESSION_TTL", 12*time.Hour), DefaultLeaseTTL: durationEnv("LICENSE_DEFAULT_LEASE_TTL", time.Hour), MaxLeaseTTL: durationEnv("LICENSE_MAX_LEASE_TTL", 24*time.Hour), SecureCookies: boolEnv("LICENSE_SECURE_COOKIES", false), }, store, logger) if err != nil { logger.Error("create license platform", "error", err) os.Exit(1) } address := env("LICENSE_ADDRESS", ":8090") server := &http.Server{Addr: address, Handler: app.Handler(), ReadHeaderTimeout: 5 * time.Second, ReadTimeout: 15 * time.Second, WriteTimeout: 20 * time.Second, IdleTimeout: 60 * time.Second, MaxHeaderBytes: 1 << 20} ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) defer stop() go func() { logger.Info("license platform started", "address", address, "public_url", env("LICENSE_PUBLIC_URL", "http://localhost:8090")) if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { logger.Error("license platform failed", "error", err) os.Exit(1) } }() <-ctx.Done() shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() _ = server.Shutdown(shutdownCtx) } func env(key, fallback string) string { if value := strings.TrimSpace(os.Getenv(key)); value != "" { return value } return fallback } func secretEnv(key string) string { if value := strings.TrimSpace(os.Getenv(key)); value != "" { return value } path := strings.TrimSpace(os.Getenv(key + "_FILE")) if path == "" { return "" } data, err := os.ReadFile(path) if err != nil { return "" } return strings.TrimSpace(string(data)) } func durationEnv(key string, fallback time.Duration) time.Duration { value := strings.TrimSpace(os.Getenv(key)) if value == "" { return fallback } if duration, err := time.ParseDuration(value); err == nil { return duration } if seconds, err := strconv.ParseInt(value, 10, 64); err == nil { return time.Duration(seconds) * time.Second } return fallback } func boolEnv(key string, fallback bool) bool { value := strings.TrimSpace(os.Getenv(key)) if value == "" { return fallback } parsed, err := strconv.ParseBool(value) if err != nil { return fallback } return parsed } func healthcheck() { address := env("LICENSE_ADDRESS", ":8090") port := address if index := strings.LastIndex(address, ":"); index >= 0 { port = address[index:] } if !strings.HasPrefix(port, ":") { port = ":" + port } client := &http.Client{Timeout: 3 * time.Second} resp, err := client.Get("http://127.0.0.1" + port + "/healthz") if err != nil || resp.StatusCode != http.StatusOK { os.Exit(1) } _ = resp.Body.Close() } func firstSecret(keys ...string) string { for _, key := range keys { if value := secretEnv(key); value != "" { return value } } return "" }