package main import ( "context" "net/http" "os" "os/signal" "syscall" "time" "github.com/gin-gonic/gin" "github.com/mattnite/forgejo-tickets/internal/auth" "github.com/mattnite/forgejo-tickets/internal/config" "github.com/mattnite/forgejo-tickets/internal/database" "github.com/mattnite/forgejo-tickets/internal/email" "github.com/mattnite/forgejo-tickets/internal/forgejo" adminhandlers "github.com/mattnite/forgejo-tickets/internal/handlers/admin" publichandlers "github.com/mattnite/forgejo-tickets/internal/handlers/public" "github.com/mattnite/forgejo-tickets/internal/templates" "github.com/rs/zerolog" "github.com/rs/zerolog/log" ) func main() { zerolog.TimeFieldFormat = zerolog.TimeFormatUnix gin.SetMode(gin.ReleaseMode) cfg, err := config.Load() if err != nil { log.Fatal().Msgf("failed to load config: %v", err) } db, err := database.Connect(cfg.DatabaseURL) if err != nil { log.Fatal().Msgf("failed to connect to database: %v", err) } if err := database.RunMigrations(db); err != nil { log.Fatal().Msgf("failed to run migrations: %v", err) } renderer, err := templates.NewRenderer() if err != nil { log.Fatal().Msgf("failed to initialize templates: %v", err) } emailClient := email.NewClient(cfg.PostmarkServerToken, cfg.PostmarkFromEmail, cfg.BaseURL) forgejoClient := forgejo.NewClient(cfg.ForgejoURL, cfg.ForgejoAPIToken) // Discover the bot's username for comment attribution if err := forgejoClient.InitBotLogin(); err != nil { log.Warn().Err(err).Msg("failed to initialize bot login (comment attribution may not work)") } else { log.Info().Str("bot_login", forgejoClient.BotLogin).Msg("forgejo bot login initialized") } sessionStore := auth.NewPGStore(db, []byte(cfg.SessionSecret)) authService := auth.NewService(db, sessionStore, emailClient) ctx, cancel := context.WithCancel(context.Background()) defer cancel() go sessionStore.Cleanup(ctx, 30*time.Minute) publicRouter := publichandlers.NewRouter(publichandlers.Dependencies{ DB: db, Renderer: renderer, Auth: authService, SessionStore: sessionStore, EmailClient: emailClient, ForgejoClient: forgejoClient, Config: cfg, }) adminRouter := adminhandlers.NewRouter(adminhandlers.Dependencies{ DB: db, Renderer: renderer, Auth: authService, EmailClient: emailClient, ForgejoClient: forgejoClient, Config: cfg, }) publicServer := &http.Server{ Addr: cfg.PublicAddr, Handler: publicRouter, ReadTimeout: 15 * time.Second, WriteTimeout: 15 * time.Second, IdleTimeout: 60 * time.Second, } adminServer := &http.Server{ Addr: cfg.AdminAddr, Handler: adminRouter, ReadTimeout: 15 * time.Second, WriteTimeout: 15 * time.Second, IdleTimeout: 60 * time.Second, } go func() { log.Info().Msgf("Public server listening on %s", cfg.PublicAddr) if err := publicServer.ListenAndServe(); err != nil && err != http.ErrServerClosed { log.Fatal().Msgf("public server error: %v", err) } }() go func() { log.Info().Msgf("Admin server listening on %s", cfg.AdminAddr) if err := adminServer.ListenAndServe(); err != nil && err != http.ErrServerClosed { log.Fatal().Msgf("admin server error: %v", err) } }() quit := make(chan os.Signal, 1) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) <-quit log.Info().Msg("Shutting down servers...") shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 30*time.Second) defer shutdownCancel() if err := publicServer.Shutdown(shutdownCtx); err != nil { log.Error().Err(err).Msg("public server shutdown error") } if err := adminServer.Shutdown(shutdownCtx); err != nil { log.Error().Err(err).Msg("admin server shutdown error") } cancel() log.Info().Msg("Servers stopped") }