package main import ( "context" "errors" "log" "net/http" "os" "os/signal" "syscall" "time" "gitea.hostxtra.co.uk/mrhid6/vantage/admin/internal/api" "gitea.hostxtra.co.uk/mrhid6/vantage/admin/internal/auth" "gitea.hostxtra.co.uk/mrhid6/vantage/admin/internal/billing" "gitea.hostxtra.co.uk/mrhid6/vantage/admin/internal/config" "gitea.hostxtra.co.uk/mrhid6/vantage/admin/internal/db" "gitea.hostxtra.co.uk/mrhid6/vantage/admin/internal/hqsync" "gitea.hostxtra.co.uk/mrhid6/vantage/admin/internal/inject" "gitea.hostxtra.co.uk/mrhid6/vantage/admin/internal/licensing" "gitea.hostxtra.co.uk/mrhid6/vantage/admin/internal/lifecycle" "gitea.hostxtra.co.uk/mrhid6/vantage/admin/internal/mail" "gitea.hostxtra.co.uk/mrhid6/vantage/admin/internal/models" "gitea.hostxtra.co.uk/mrhid6/vantage/admin/internal/paddle" sharedmail "gitea.hostxtra.co.uk/mrhid6/vantage/shared/mail" "github.com/joho/godotenv" ) func main() { godotenv.Load() cfg, err := config.Load() if err != nil { log.Fatalf("configuration error: %v", err) } licensing.SetSigningKey(cfg.SigningKey) api.SetAppLoginURL(cfg.AppLoginURL) if _, err := paddle.Init(cfg.PaddleAPIKey, cfg.PaddleEnv); err != nil { log.Fatalf("paddle init: %v", err) } mail.Init(sharedmail.Sender{ Host: cfg.SMTPHost, Port: cfg.SMTPPort, From: cfg.SMTPFrom, Username: cfg.SMTPUsername, Password: cfg.SMTPPassword, PublicURL: cfg.PublicURL, }) if !mail.Enabled() { log.Println("warning: SMTP not configured; verification and licence emails will fail") } auth.InitRedis(cfg.RedisAddr, cfg.RedisUsername, cfg.RedisPassword) pingCtx, pingCancel := context.WithTimeout(context.Background(), 10*time.Second) if err := auth.Ping(pingCtx); err != nil { pingCancel() log.Fatalf("redis: %v", err) } pingCancel() ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) if err := db.Connect(ctx, cfg); err != nil { cancel() log.Fatalf("database: %v", err) } cancel() log.Printf("connected: admin=%s control=%s", cfg.AdminDBName, cfg.ControlDBName) idxCtx, idxCancel := context.WithTimeout(context.Background(), 30*time.Second) if err := db.EnsureIndexes(idxCtx); err != nil { idxCancel() log.Fatalf("indexes: %v", err) } // Legacy plans are re-keyed BEFORE the seed, so the seed's fresh // (self_hosted, professional) row cannot collide with the legacy self_hosted // row's rename on deployment_tier_unique. if err := models.MigrateLegacyPlans(idxCtx); err != nil { idxCancel() log.Fatalf("migrate legacy plans: %v", err) } if err := models.SeedPlans(idxCtx); err != nil { idxCancel() log.Fatalf("plan seed: %v", err) } if err := models.SeedCatalogue(idxCtx); err != nil { idxCancel() log.Fatalf("seed catalogue: %v", err) } if err := models.Backfill(idxCtx); err != nil { idxCancel() log.Fatalf("backfill: %v", err) } idxCancel() reconcileCtx, stopReconcile := context.WithCancel(context.Background()) defer stopReconcile() inject.StartReconciler(reconcileCtx) billing.StartPlaceholderReconciler(reconcileCtx) hqsync.Start(reconcileCtx) lifecycle.SetPortalURL(cfg.PublicURL) lifecycle.Start(reconcileCtx, cfg.ReapAfter) srv := &http.Server{ Addr: cfg.Addr, Handler: api.Routes(cfg), ReadHeaderTimeout: 10 * time.Second, ReadTimeout: 20 * time.Second, WriteTimeout: 30 * time.Second, IdleTimeout: 60 * time.Second, } go func() { log.Printf("admin listening on %s", cfg.Addr) if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { log.Fatalf("server error: %v", err) } }() stopCtx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) defer stop() <-stopCtx.Done() shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 15*time.Second) defer shutdownCancel() _ = srv.Shutdown(shutdownCtx) log.Println("admin stopped") os.Exit(0) }