package main import ( "context" "log" "time" "github.com/gofiber/fiber/v3" "github.com/gofiber/fiber/v3/middleware/adaptor" appq "knowfoolery/backend/services/question-bank-service/internal/application/question" qcache "knowfoolery/backend/services/question-bank-service/internal/infra/cache" qbconfig "knowfoolery/backend/services/question-bank-service/internal/infra/config" qent "knowfoolery/backend/services/question-bank-service/internal/infra/persistence/ent" httpapi "knowfoolery/backend/services/question-bank-service/internal/interfaces/http" "knowfoolery/backend/shared/infra/auth/zitadel" sharedredis "knowfoolery/backend/shared/infra/database/redis" "knowfoolery/backend/shared/infra/observability/logging" sharedmetrics "knowfoolery/backend/shared/infra/observability/metrics" "knowfoolery/backend/shared/infra/observability/tracing" "knowfoolery/backend/shared/infra/utils/serviceboot" "knowfoolery/backend/shared/infra/utils/validation" ) func main() { cfg := qbconfig.FromEnv() logger := logging.NewLogger(cfg.Logging) metrics := sharedmetrics.NewMetrics(cfg.Metrics) tracer, err := tracing.NewTracer(cfg.Tracing) if err != nil { logger.Fatal("failed to initialize tracer") } defer func() { _ = tracer.Shutdown(context.Background()) }() ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() persistence, err := qent.NewClient(ctx, cfg.Postgres) if err != nil { logger.WithError(err).Fatal("failed to initialize postgres client") } defer persistence.Close() repo := qent.NewQuestionRepository(persistence) if err := repo.EnsureSchema(ctx); err != nil { logger.WithError(err).Fatal("failed to ensure schema") } var redisClient *sharedredis.Client if c, redisErr := sharedredis.NewClient(cfg.Redis); redisErr == nil { redisClient = c defer func() { _ = redisClient.Close() }() } else { logger.WithError(redisErr).Warn("redis unavailable; running with local fallback cache") } randomCache := qcache.NewRandomQuestionCache(redisClient) service := appq.NewService(repo, randomCache, cfg.CacheTTL, cfg.MaxExclude) validator := validation.NewValidator() handler := httpapi.NewHandler(service, validator, logger, metrics, cfg.BulkMax) bootCfg := serviceboot.Config{ AppName: cfg.AppName, ServiceSlug: "question-bank", PortEnv: "QUESTION_BANK_PORT", DefaultPort: cfg.Port, } app := serviceboot.NewFiberApp(bootCfg) serviceboot.RegisterHealth(app, bootCfg.ServiceSlug) registerReadiness(app, persistence, redisClient) app.Get("/metrics", adaptor.HTTPHandler(sharedmetrics.Handler())) adminMiddleware := buildAdminMiddleware(cfg) httpapi.RegisterRoutes(app, handler, adminMiddleware) addr := serviceboot.ListenAddress(bootCfg.PortEnv, bootCfg.DefaultPort) log.Fatal(serviceboot.Run(app, addr)) } func buildAdminMiddleware(cfg qbconfig.Config) fiber.Handler { if cfg.ZitadelBaseURL == "" { return nil } client := zitadel.NewClient(zitadel.Config{ BaseURL: cfg.ZitadelBaseURL, ClientID: cfg.ZitadelClientID, ClientSecret: cfg.ZitadelSecret, Issuer: cfg.ZitadelIssuer, Audience: cfg.ZitadelAudience, Timeout: 10 * time.Second, }) return zitadel.JWTMiddleware(zitadel.JWTMiddlewareConfig{ Client: client, Issuer: cfg.ZitadelIssuer, Audience: cfg.ZitadelAudience, RequiredClaims: []string{"sub"}, AdminEndpoints: []string{"/admin"}, }) } func registerReadiness(app *fiber.App, persistence *qent.Client, redisClient *sharedredis.Client) { app.Get("/ready", func(c fiber.Ctx) error { ctx, cancel := context.WithTimeout(c.Context(), 2*time.Second) defer cancel() checks := map[string]string{"postgres": "ok", "redis": "ok"} if err := persistence.Pool.Ping(ctx); err != nil { checks["postgres"] = "down" } if redisClient != nil { if err := redisClient.HealthCheck(ctx); err != nil { checks["redis"] = "down" } } status := fiber.StatusOK if checks["postgres"] != "ok" { status = fiber.StatusServiceUnavailable } return c.Status(status).JSON(fiber.Map{ "status": "ready", "checks": checks, }) }) }