package main import ( "context" "fmt" "log" "net/http" "os" "os/signal" "syscall" "time" "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" sharedbootstrap "github.com/geo-platform/tenant-api/internal/shared/bootstrap" "github.com/geo-platform/tenant-api/internal/shared/cache" sharedconfig "github.com/geo-platform/tenant-api/internal/shared/config" "github.com/geo-platform/tenant-api/internal/shared/ipregion" "github.com/geo-platform/tenant-api/internal/shared/messaging/rabbitmq" "github.com/geo-platform/tenant-api/internal/shared/objectstorage" "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) var rabbitMQ *rabbitmq.Client if cfg.RabbitMQ.URL != "" { mq, mqErr := rabbitmq.New(ctx, cfg.RabbitMQ) if mqErr != nil { logger.Warn("rabbitmq unavailable during startup; job retry actions requiring queues are disabled", zap.Error(mqErr)) } else { rabbitMQ = mq defer func() { _ = rabbitMQ.Close() }() } } accountsRepo := repository.NewAccountRepository(pool) adminUsersRepo := repository.NewAdminUserRepository(pool) jobsRepo := repository.NewJobRepository(pool, monitoringPool) kolSubscriptionsRepo := repository.NewKolSubscriptionRepository(pool) auditsRepo := repository.NewAuditRepository(pool) siteDomainMappingsRepo := repository.NewSiteDomainMappingRepository(monitoringPool) desktopClientReleasesRepo := repository.NewDesktopClientReleaseRepository(pool) schedulerRepo := repository.NewSchedulerRepository(pool) ipRegionResolver, err := ipregion.NewResolver(cfg.IPRegion.V4XDBPath, cfg.IPRegion.V6XDBPath, logger) if err != nil { logger.Sugar().Warnf("ops-api ip2region disabled: %v", err) } if ipRegionResolver != nil { defer ipRegionResolver.Close() } auditSvc := app.NewAuditService(auditsRepo, logger).WithIPRegionResolver(ipRegionResolver) issuer := app.NewTokenIssuer(cfg.JWT.Secret, cfg.JWT.AccessTTL) var passwordCipher *sharedauth.PasswordCipher if cfg.Auth.PasswordCipher.Enabled { if cfg.Auth.PasswordCipher.PrivateKeyPEM == "" { passwordCipher, err = sharedauth.NewEphemeralPasswordCipher(cfg.Auth.PasswordCipher.KeyID) } else { passwordCipher, err = sharedauth.NewPasswordCipher(cfg.Auth.PasswordCipher.PrivateKeyPEM, cfg.Auth.PasswordCipher.KeyID) } if err != nil { logger.Sugar().Fatalf("ops-api init password cipher: %v", err) } } else { logger.Warn("ops auth password cipher disabled; login request payloads may expose plaintext passwords in browser developer tools") } loginGuard := sharedauth.NewLoginGuard(rdb, sharedauth.DefaultLoginGuardConfig("ops")) authSvc := app.NewAuthService(accountsRepo, auditSvc, issuer, loginGuard).WithPasswordCipher(passwordCipher) accountSvc := app.NewAccountService(accountsRepo, auditSvc) tenantLoginGuard := sharedauth.NewLoginGuard(rdb, sharedauth.DefaultLoginGuardConfig("tenant")) adminUserSvc := app.NewAdminUserService(adminUsersRepo, auditSvc, cfg.AdminUsers.DefaultPlanCode). WithLoginGuard(tenantLoginGuard). WithCache(appCache) jobSvc := app.NewJobService(jobsRepo, auditSvc, rabbitMQ) kolSubscriptionSvc := app.NewKolSubscriptionService(kolSubscriptionsRepo, auditSvc).WithCache(appCache) siteDomainMappingSvc := app.NewSiteDomainMappingService(siteDomainMappingsRepo, auditSvc) objectStorageClient := objectstorage.New(cfg.ObjectStorage, logger) desktopClientReleaseSvc := app.NewDesktopClientReleaseService(desktopClientReleasesRepo, objectStorageClient, auditSvc) objectStorageSvc := app.NewObjectStorageService(objectStorageClient, auditSvc).WithLogger(logger) complianceSvc := app.NewComplianceService(pool, auditSvc, logger) schedulerSvc := app.NewSchedulerService(schedulerRepo, auditSvc) mediaSupplySvc := app.NewMediaSupplyService(pool, auditSvc, func() sharedconfig.MediaSupplyConfig { if current := configStore.Current(); current != nil { return current.MediaSupply } return sharedconfig.MediaSupplyConfig{} }) 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)) } }() engine, err := sharedbootstrap.NewEngine(cfg.Server) if err != nil { logger.Sugar().Fatalf("ops-api init http engine: %v", err) } transport.RegisterRoutes(transport.Deps{ Engine: engine, Server: cfg.Server, Logger: logger, DB: pool, MonitoringDB: monitoringPool, Issuer: issuer, Auth: authSvc, Accounts: accountSvc, AdminUsers: adminUserSvc, Jobs: jobSvc, KolSubs: kolSubscriptionSvc, Audits: auditSvc, SiteDomains: siteDomainMappingSvc, Releases: desktopClientReleaseSvc, ObjectStorage: objectStorageSvc, Compliance: complianceSvc, Scheduler: schedulerSvc, MediaSupply: mediaSupplySvc, }) 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) } }