package bootstrap import ( "context" "fmt" "log" "net" "net/http" "time" "github.com/gin-gonic/gin" "github.com/pocket-id/pocket-id/backend/internal/common" "github.com/pocket-id/pocket-id/backend/internal/controller" "github.com/pocket-id/pocket-id/backend/internal/job" "github.com/pocket-id/pocket-id/backend/internal/middleware" "github.com/pocket-id/pocket-id/backend/internal/service" "github.com/pocket-id/pocket-id/backend/internal/utils/systemd" "golang.org/x/time/rate" "gorm.io/gorm" ) // This is used to register additional controllers for tests var registerTestControllers []func(apiGroup *gin.RouterGroup, db *gorm.DB, appConfigService *service.AppConfigService, jwtService *service.JwtService) func initRouter(ctx context.Context, db *gorm.DB, appConfigService *service.AppConfigService) { err := initRouterInternal(ctx, db, appConfigService) if err != nil { log.Fatalf("failed to init router: %v", err) } } func initRouterInternal(ctx context.Context, db *gorm.DB, appConfigService *service.AppConfigService) error { // Set the appropriate Gin mode based on the environment switch common.EnvConfig.AppEnv { case "production": gin.SetMode(gin.ReleaseMode) case "development": gin.SetMode(gin.DebugMode) case "test": gin.SetMode(gin.TestMode) } r := gin.Default() r.Use(gin.Logger()) // Initialize services emailService, err := service.NewEmailService(appConfigService, db) if err != nil { return fmt.Errorf("unable to create email service: %w", err) } geoLiteService := service.NewGeoLiteService(ctx) auditLogService := service.NewAuditLogService(db, appConfigService, emailService, geoLiteService) jwtService := service.NewJwtService(appConfigService) webauthnService := service.NewWebAuthnService(db, jwtService, auditLogService, appConfigService) userService := service.NewUserService(db, jwtService, auditLogService, emailService, appConfigService) customClaimService := service.NewCustomClaimService(db) oidcService := service.NewOidcService(db, jwtService, appConfigService, auditLogService, customClaimService) userGroupService := service.NewUserGroupService(db, appConfigService) ldapService := service.NewLdapService(db, appConfigService, userService, userGroupService) apiKeyService := service.NewApiKeyService(db, emailService) rateLimitMiddleware := middleware.NewRateLimitMiddleware() // Setup global middleware r.Use(middleware.NewCorsMiddleware().Add()) r.Use(middleware.NewErrorHandlerMiddleware().Add()) r.Use(rateLimitMiddleware.Add(rate.Every(time.Second), 60)) scheduler, err := job.NewScheduler() if err != nil { return fmt.Errorf("failed to create job scheduler: %w", err) } err = scheduler.RegisterLdapJobs(ctx, ldapService, appConfigService) if err != nil { return fmt.Errorf("failed to register LDAP jobs in scheduler: %w", err) } err = scheduler.RegisterDbCleanupJobs(ctx, db) if err != nil { return fmt.Errorf("failed to register DB cleanup jobs in scheduler: %w", err) } err = scheduler.RegisterFileCleanupJobs(ctx, db) if err != nil { return fmt.Errorf("failed to register file cleanup jobs in scheduler: %w", err) } err = scheduler.RegisterApiKeyExpiryJob(ctx, apiKeyService, appConfigService) if err != nil { return fmt.Errorf("failed to register API key expiration jobs in scheduler: %w", err) } // Run the scheduler in a background goroutine, until the context is canceled go scheduler.Run(ctx) // Initialize middleware for specific routes authMiddleware := middleware.NewAuthMiddleware(apiKeyService, userService, jwtService) fileSizeLimitMiddleware := middleware.NewFileSizeLimitMiddleware() // Set up API routes apiGroup := r.Group("/api") controller.NewApiKeyController(apiGroup, authMiddleware, apiKeyService) controller.NewWebauthnController(apiGroup, authMiddleware, middleware.NewRateLimitMiddleware(), webauthnService, appConfigService) controller.NewOidcController(apiGroup, authMiddleware, fileSizeLimitMiddleware, oidcService, jwtService) controller.NewUserController(apiGroup, authMiddleware, middleware.NewRateLimitMiddleware(), userService, appConfigService) controller.NewAppConfigController(apiGroup, authMiddleware, appConfigService, emailService, ldapService) controller.NewAuditLogController(apiGroup, auditLogService, authMiddleware) controller.NewUserGroupController(apiGroup, authMiddleware, userGroupService) controller.NewCustomClaimController(apiGroup, authMiddleware, customClaimService) // Add test controller in non-production environments if common.EnvConfig.AppEnv != "production" { for _, f := range registerTestControllers { f(apiGroup, db, appConfigService, jwtService) } } // Set up base routes baseGroup := r.Group("/") controller.NewWellKnownController(baseGroup, jwtService) // Set up the server srv := &http.Server{ Addr: net.JoinHostPort(common.EnvConfig.Host, common.EnvConfig.Port), MaxHeaderBytes: 1 << 20, ReadHeaderTimeout: 10 * time.Second, Handler: r, } // Set up the listener listener, err := net.Listen("tcp", srv.Addr) if err != nil { return fmt.Errorf("failed to create TCP listener: %w", err) } log.Printf("Server listening on %s", srv.Addr) // Notify systemd that we are ready err = systemd.SdNotifyReady() if err != nil { log.Printf("[WARN] Unable to notify systemd that the service is ready: %v", err) // continue to serve anyway since it's not that important } // Start the server in a background goroutine go func() { defer listener.Close() // Next call blocks until the server is shut down srvErr := srv.Serve(listener) if srvErr != http.ErrServerClosed { log.Fatalf("Error starting app server: %v", srvErr) } }() // Block until the context is canceled <-ctx.Done() // Handle graceful shutdown // Note we use the background context here as ctx has been canceled already shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 5*time.Second) shutdownErr := srv.Shutdown(shutdownCtx) //nolint:contextcheck shutdownCancel() if shutdownErr != nil { // Log the error only (could be context canceled) log.Printf("[WARN] App server shutdown error: %v", shutdownErr) } return nil }