package main import ( "context" "log" "net/http" "os" "os/signal" "syscall" "time" "github.com/gin-gonic/gin" "go.uber.org/zap" "github.com/kms/api-key-service/internal/audit" "github.com/kms/api-key-service/internal/config" "github.com/kms/api-key-service/internal/database" "github.com/kms/api-key-service/internal/domain" "github.com/kms/api-key-service/internal/handlers" "github.com/kms/api-key-service/internal/metrics" "github.com/kms/api-key-service/internal/middleware" "github.com/kms/api-key-service/internal/repository/postgres" "github.com/kms/api-key-service/internal/services" ) func main() { // Initialize configuration cfg := config.NewConfig() if err := cfg.Validate(); err != nil { log.Fatal("Configuration validation failed:", err) } // Initialize logger logger := initLogger(cfg) defer logger.Sync() logger.Info("Starting API Key Management Service", zap.String("version", cfg.GetString("APP_VERSION")), zap.String("environment", cfg.GetString("APP_ENV")), ) // Initialize database logger.Info("Connecting to database", zap.String("dsn", cfg.GetDatabaseDSNForLogging())) db, err := database.NewPostgresProvider( cfg.GetDatabaseDSN(), cfg.GetInt("DB_MAX_OPEN_CONNS"), cfg.GetInt("DB_MAX_IDLE_CONNS"), cfg.GetString("DB_CONN_MAX_LIFETIME"), ) if err != nil { logger.Fatal("Failed to initialize database", zap.String("dsn", cfg.GetDatabaseDSNForLogging()), zap.Error(err)) } logger.Info("Database connection established successfully") // Database migrations are handled by PostgreSQL docker-entrypoint-initdb.d logger.Info("Database migrations are handled by PostgreSQL on container startup") // Initialize repositories appRepo := postgres.NewApplicationRepository(db) tokenRepo := postgres.NewStaticTokenRepository(db) permRepo := postgres.NewPermissionRepository(db) grantRepo := postgres.NewGrantedPermissionRepository(db) auditRepo := postgres.NewAuditRepository(db) // Initialize audit logger auditLogger := audit.NewAuditLogger(cfg, logger, auditRepo) // Initialize services appService := services.NewApplicationService(appRepo, auditRepo, logger) tokenService := services.NewTokenService(tokenRepo, appRepo, permRepo, grantRepo, cfg.GetString("INTERNAL_HMAC_KEY"), cfg, logger) authService := services.NewAuthenticationService(cfg, logger, permRepo) // Initialize handlers healthHandler := handlers.NewHealthHandler(db, logger) appHandler := handlers.NewApplicationHandler(appService, authService, logger) tokenHandler := handlers.NewTokenHandler(tokenService, authService, logger) authHandler := handlers.NewAuthHandler(authService, tokenService, cfg, logger) auditHandler := handlers.NewAuditHandler(auditLogger, authService, logger) testHandler := handlers.NewTestHandler(logger) // Set up router router := setupRouter(cfg, logger, healthHandler, appHandler, tokenHandler, authHandler, auditHandler, testHandler) // Create HTTP server srv := &http.Server{ Addr: cfg.GetServerAddress(), Handler: router, ReadTimeout: cfg.GetDuration("SERVER_READ_TIMEOUT"), WriteTimeout: cfg.GetDuration("SERVER_WRITE_TIMEOUT"), IdleTimeout: cfg.GetDuration("SERVER_IDLE_TIMEOUT"), } // Initialize bootstrap data logger.Info("Initializing bootstrap data") if err := initializeBootstrapData(context.Background(), appService, tokenService, cfg, logger); err != nil { logger.Fatal("Failed to initialize bootstrap data", zap.Error(err)) } // Start server in goroutine go func() { logger.Info("Starting HTTP server", zap.String("address", srv.Addr)) if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { logger.Fatal("Failed to start server", zap.Error(err)) } }() // Start metrics server if enabled var metricsSrv *http.Server if cfg.GetBool("METRICS_ENABLED") { metricsSrv = startMetricsServer(cfg, logger) } // Wait for interrupt signal to gracefully shutdown the server quit := make(chan os.Signal, 1) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) <-quit logger.Info("Shutting down server...") // Give outstanding requests time to complete ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() // Shutdown main server if err := srv.Shutdown(ctx); err != nil { logger.Error("Server forced to shutdown", zap.Error(err)) } // Shutdown metrics server if metricsSrv != nil { if err := metricsSrv.Shutdown(ctx); err != nil { logger.Error("Metrics server forced to shutdown", zap.Error(err)) } } logger.Info("Server exited") } func initLogger(cfg config.ConfigProvider) *zap.Logger { var logger *zap.Logger var err error if cfg.IsProduction() { logger, err = zap.NewProduction() } else { logger, err = zap.NewDevelopment() } if err != nil { log.Fatal("Failed to initialize logger:", err) } return logger } func setupRouter(cfg config.ConfigProvider, logger *zap.Logger, healthHandler *handlers.HealthHandler, appHandler *handlers.ApplicationHandler, tokenHandler *handlers.TokenHandler, authHandler *handlers.AuthHandler, auditHandler *handlers.AuditHandler, testHandler *handlers.TestHandler) *gin.Engine { // Set Gin mode based on environment if cfg.IsProduction() { gin.SetMode(gin.ReleaseMode) } router := gin.New() // Add middleware router.Use(middleware.Logger(logger)) router.Use(middleware.Recovery(logger)) router.Use(metrics.Middleware(logger)) router.Use(middleware.CORS()) router.Use(middleware.Security()) router.Use(middleware.ValidateContentType()) if cfg.GetBool("RATE_LIMIT_ENABLED") { router.Use(middleware.RateLimit(cfg.GetInt("RATE_LIMIT_RPS"), cfg.GetInt("RATE_LIMIT_BURST"))) } // Health check endpoint (no authentication required) router.GET("/health", healthHandler.Health) router.GET("/ready", healthHandler.Ready) // Development/Testing endpoints (no authentication required) if !cfg.IsProduction() { router.GET("/test/sso", testHandler.SSOTestPage) } // API routes api := router.Group("/api") { // Authentication endpoints (no prior auth required) api.GET("/login", authHandler.Login) // HTML page for browser access api.POST("/login", authHandler.Login) // JSON API for programmatic access api.POST("/verify", authHandler.Verify) api.POST("/renew", authHandler.Renew) // Protected routes (require authentication) protected := api.Group("/") protected.Use(middleware.Authentication(cfg, logger)) { // Application management protected.GET("/applications", appHandler.List) protected.POST("/applications", appHandler.Create) protected.GET("/applications/:id", appHandler.GetByID) protected.PUT("/applications/:id", appHandler.Update) protected.DELETE("/applications/:id", appHandler.Delete) // Token management protected.GET("/applications/:id/tokens", tokenHandler.ListByApp) protected.POST("/applications/:id/tokens", tokenHandler.Create) protected.DELETE("/tokens/:id", tokenHandler.Delete) // Audit management protected.GET("/audit/events", auditHandler.ListEvents) protected.GET("/audit/events/:id", auditHandler.GetEvent) protected.GET("/audit/stats", auditHandler.GetStats) // Documentation endpoint protected.GET("/docs", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{ "service": "API Key Management Service", "version": cfg.GetString("APP_VERSION"), "documentation": "See README.md and docs/ directory", "endpoints": map[string]interface{}{ "authentication": []string{ "POST /api/login", "POST /api/verify", "POST /api/renew", }, "applications": []string{ "GET /api/applications", "POST /api/applications", "GET /api/applications/:id", "PUT /api/applications/:id", "DELETE /api/applications/:id", }, "tokens": []string{ "GET /api/applications/:id/tokens", "POST /api/applications/:id/tokens", "DELETE /api/tokens/:id", }, "audit": []string{ "GET /api/audit/events", "GET /api/audit/events/:id", "GET /api/audit/stats", }, }, }) }) } } return router } func startMetricsServer(cfg config.ConfigProvider, logger *zap.Logger) *http.Server { mux := http.NewServeMux() // Prometheus metrics endpoint mux.HandleFunc("/metrics", metrics.PrometheusHandler()) // Health endpoint for metrics server mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) w.Write([]byte("OK")) }) srv := &http.Server{ Addr: cfg.GetMetricsAddress(), Handler: mux, } go func() { logger.Info("Starting metrics server", zap.String("address", srv.Addr)) if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { logger.Error("Failed to start metrics server", zap.Error(err)) } }() return srv } func initializeBootstrapData(ctx context.Context, appService services.ApplicationService, tokenService services.TokenService, cfg config.ConfigProvider, logger *zap.Logger) error { // Check if internal application already exists internalAppID := cfg.GetString("INTERNAL_APP_ID") _, err := appService.GetByID(ctx, internalAppID) if err == nil { logger.Info("Internal application already exists, skipping bootstrap") return nil } logger.Info("Creating internal application for bootstrap", zap.String("app_id", internalAppID)) // Create internal application for system operations internalAppReq := &domain.CreateApplicationRequest{ AppID: internalAppID, AppLink: "https://kms.internal/system", Type: []domain.ApplicationType{domain.ApplicationTypeStatic, domain.ApplicationTypeUser}, CallbackURL: "https://kms.internal/callback", TokenPrefix: "KMS", TokenRenewalDuration: domain.Duration{Duration: 365 * 24 * time.Hour}, // 1 year MaxTokenDuration: domain.Duration{Duration: 365 * 24 * time.Hour}, // 1 year Owner: domain.Owner{ Type: domain.OwnerTypeTeam, Name: "KMS System", Owner: "system@kms.internal", }, } app, err := appService.Create(ctx, internalAppReq, "system") if err != nil { logger.Error("Failed to create internal application", zap.Error(err)) return err } logger.Info("Internal application created successfully", zap.String("app_id", app.AppID), zap.String("hmac_key", app.HMACKey)) // Create a static token for internal system operations if needed internalTokenReq := &domain.CreateStaticTokenRequest{ AppID: internalAppID, Owner: domain.Owner{ Type: domain.OwnerTypeTeam, Name: "KMS System Token", Owner: "system@kms.internal", }, Permissions: []string{"internal.*", "app.*", "token.*", "audit.*"}, } token, err := tokenService.CreateStaticToken(ctx, internalTokenReq, "system") if err != nil { logger.Warn("Failed to create internal system token, continuing...", zap.Error(err)) } else { logger.Info("Internal system token created successfully", zap.String("token_id", token.ID.String())) } logger.Info("Bootstrap data initialization completed successfully") return nil }