package main import ( "context" "fmt" "log" "net/http" "os" "os/signal" "syscall" "time" "github.com/gin-gonic/gin" "go.uber.org/zap" "github.com/geo-platform/tenant-api/internal/ops/app" opsconfig "github.com/geo-platform/tenant-api/internal/ops/config" "github.com/geo-platform/tenant-api/internal/ops/repository" "github.com/geo-platform/tenant-api/internal/ops/transport" sharedauth "github.com/geo-platform/tenant-api/internal/shared/auth" "github.com/geo-platform/tenant-api/internal/shared/cache" "github.com/geo-platform/tenant-api/internal/shared/observability" "github.com/geo-platform/tenant-api/internal/shared/repository/postgres" "github.com/geo-platform/tenant-api/internal/shared/repository/redis" ) func main() { configPath := "configs/ops-config.yaml" if p := os.Getenv("CONFIG_PATH"); p != "" { configPath = p } configStore, err := opsconfig.NewStore(configPath) if err != nil { log.Fatalf("ops-api load config: %v", err) } defer configStore.Close() cfg := configStore.Current() logger, err := observability.NewLogger(cfg.Log.Level, cfg.Log.Format) if err != nil { log.Fatalf("ops-api init logger: %v", err) } defer func() { _ = logger.Sync() }() ctx := context.Background() pool, err := postgres.NewPool(ctx, cfg.Database) if err != nil { logger.Sugar().Fatalf("ops-api init postgres: %v", err) } defer pool.Close() monitoringPool, err := postgres.NewPool(ctx, cfg.MonitoringDatabase) if err != nil { logger.Sugar().Fatalf("ops-api init monitoring postgres: %v", err) } defer monitoringPool.Close() rdb, err := redis.NewClient(ctx, cfg.Redis) if err != nil { logger.Warn("redis unavailable during startup; continuing with degraded auth/cache fallback", zap.Error(err)) rdb = redis.NewLazyClient(cfg.Redis) } defer func() { _ = rdb.Close() }() appCache := cache.New(cfg.Cache.Driver, rdb) accountsRepo := repository.NewAccountRepository(pool) adminUsersRepo := repository.NewAdminUserRepository(pool) kolSubscriptionsRepo := repository.NewKolSubscriptionRepository(pool) auditsRepo := repository.NewAuditRepository(pool) siteDomainMappingsRepo := repository.NewSiteDomainMappingRepository(monitoringPool) ipRegionResolver, err := app.NewIPRegionResolver(cfg.IPRegion.V4XDBPath, cfg.IPRegion.V6XDBPath, logger) if err != nil { logger.Sugar().Warnf("ops-api ip2region disabled: %v", err) } defer ipRegionResolver.Close() auditSvc := app.NewAuditService(auditsRepo, logger).WithIPRegionResolver(ipRegionResolver) issuer := app.NewTokenIssuer(cfg.JWT.Secret, cfg.JWT.AccessTTL) loginGuard := sharedauth.NewLoginGuard(rdb, sharedauth.DefaultLoginGuardConfig("ops")) authSvc := app.NewAuthService(accountsRepo, auditSvc, issuer, loginGuard) accountSvc := app.NewAccountService(accountsRepo, auditSvc) tenantLoginGuard := sharedauth.NewLoginGuard(rdb, sharedauth.DefaultLoginGuardConfig("tenant")) adminUserSvc := app.NewAdminUserService(adminUsersRepo, auditSvc, cfg.AdminUsers.DefaultPlanCode).WithLoginGuard(tenantLoginGuard) kolSubscriptionSvc := app.NewKolSubscriptionService(kolSubscriptionsRepo, auditSvc).WithCache(appCache) siteDomainMappingSvc := app.NewSiteDomainMappingService(siteDomainMappingsRepo, auditSvc) complianceSvc := app.NewComplianceService(pool, auditSvc, logger) if err := app.SeedDefaultAdmin(ctx, accountsRepo, app.DefaultAdminSeed{ Username: cfg.DefaultAdmin.Username, Password: cfg.DefaultAdmin.Password, DisplayName: cfg.DefaultAdmin.DisplayName, Email: cfg.DefaultAdmin.Email, }, logger); err != nil { logger.Sugar().Fatalf("ops-api seed default admin: %v", err) } watcherCtx, watcherCancel := context.WithCancel(context.Background()) defer watcherCancel() go func() { if err := configStore.Watch(watcherCtx, logger, func(event opsconfig.ReloadEvent) { if event.Current == nil { return } issuer.Update(event.Current.JWT.Secret, event.Current.JWT.AccessTTL) adminUserSvc.UpdateDefaultPlanCode(event.Current.AdminUsers.DefaultPlanCode) if opsconfig.RestartRequired(event.Changes) { logger.Warn("ops config hot reload changed fields that require process restart") } }); err != nil { logger.Warn("ops config hot reload watcher stopped", zap.Error(err)) } }() if cfg.Server.Mode == "release" { gin.SetMode(gin.ReleaseMode) } engine := gin.New() transport.RegisterRoutes(transport.Deps{ Engine: engine, Logger: logger, DB: pool, MonitoringDB: monitoringPool, Issuer: issuer, Auth: authSvc, Accounts: accountSvc, AdminUsers: adminUserSvc, KolSubs: kolSubscriptionSvc, Audits: auditSvc, SiteDomains: siteDomainMappingSvc, Compliance: complianceSvc, }) addr := fmt.Sprintf(":%d", cfg.Server.Port) srv := &http.Server{ Addr: addr, Handler: engine, ReadHeaderTimeout: 10 * time.Second, } logger.Sugar().Infof("ops-api starting on %s", addr) go func() { if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { logger.Sugar().Fatalf("ops-api server: %v", err) } }() quit := make(chan os.Signal, 1) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) <-quit logger.Info("ops-api shutting down...") watcherCancel() shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() if err := srv.Shutdown(shutdownCtx); err != nil { logger.Sugar().Warnf("ops-api shutdown: %v", err) } }