package main import ( "context" "fmt" "log/slog" "net/http" "os" "os/signal" "syscall" "time" "aiaa-notification-service/internal/adapter" "aiaa-notification-service/internal/cache" "aiaa-notification-service/internal/config" "aiaa-notification-service/internal/engine" "aiaa-notification-service/internal/handler" "aiaa-notification-service/internal/store" "github.com/gin-gonic/gin" ) func main() { logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo})) slog.SetDefault(logger) // Load config cfg, err := config.Load("config/config.yaml") if err != nil { slog.Error("failed to load config", "error", err) os.Exit(1) } // Connect MySQL st, err := store.NewStore(cfg.Database) if err != nil { slog.Error("failed to connect to MySQL", "error", err) os.Exit(1) } defer st.Close() // Connect Redis var redisCache *cache.Cache if cfg.Redis.Host != "" { redisCache, err = cache.NewCache(cfg.Redis) if err != nil { slog.Warn("redis connection failed, running without cache", "error", err) redisCache = nil } } else { slog.Warn("redis not configured, running without cache") } if redisCache != nil { defer redisCache.Close() } // Build engine matcher := engine.NewMatcher(st, redisCache) renderer := engine.NewRenderer() // Build sender factory senderFactory := func(channelType string) (adapter.ChannelSender, error) { return adapter.NewSender(channelType, &cfg.SMTP) } router := engine.NewRouter(st, redisCache, senderFactory) // Build handlers notifyH := handler.NewNotifyHandler(st, redisCache, matcher, renderer, router) sourceH := handler.NewSourceHandler(st, redisCache) templateH := handler.NewTemplateHandler(st, redisCache) channelH := handler.NewChannelHandler(st, redisCache) ruleH := handler.NewRuleHandler(st, redisCache) msgLogH := handler.NewMessageLogHandler(st) // Setup Gin gin.SetMode(gin.ReleaseMode) r := gin.New() r.Use(gin.Recovery()) // Health check r.GET("/health", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"status": "ok"}) }) api := r.Group("/api/v1") // Notify endpoint (source auth + rate limit) notifyGroup := api.Group("/notify") notifyGroup.Use(handler.SourceAuth(st)) if redisCache != nil { notifyGroup.Use(handler.RateLimit(redisCache, cfg.RateLimit.Default)) } notifyGroup.POST("", notifyH.Handle) // Management endpoints (admin auth) admin := api.Group("") admin.Use(handler.AdminAuth(cfg.Server.AdminKey)) // Sources admin.POST("/sources", sourceH.Create) admin.GET("/sources", sourceH.List) admin.GET("/sources/:id", sourceH.Get) admin.PUT("/sources/:id", sourceH.Update) admin.DELETE("/sources/:id", sourceH.Delete) // Templates admin.POST("/templates", templateH.Create) admin.GET("/templates", templateH.List) admin.GET("/templates/:id", templateH.Get) admin.PUT("/templates/:id", templateH.Update) admin.DELETE("/templates/:id", templateH.Delete) // Channels admin.POST("/channels", channelH.Create) admin.GET("/channels", channelH.List) admin.GET("/channels/:id", channelH.Get) admin.PUT("/channels/:id", channelH.Update) admin.DELETE("/channels/:id", channelH.Delete) // Rules admin.POST("/rules", ruleH.Create) admin.GET("/rules", ruleH.List) admin.GET("/rules/:id", ruleH.Get) admin.PUT("/rules/:id", ruleH.Update) admin.DELETE("/rules/:id", ruleH.Delete) admin.PATCH("/rules/:id/enable", ruleH.Enable) admin.PATCH("/rules/:id/disable", ruleH.Disable) admin.PATCH("/rules/:id/channels/:channel_id/enable", ruleH.EnableChannel) admin.PATCH("/rules/:id/channels/:channel_id/disable", ruleH.DisableChannel) // Message Logs admin.GET("/message-logs", msgLogH.List) // Start server srv := &http.Server{ Addr: fmt.Sprintf(":%d", cfg.Server.Port), Handler: r, } go func() { slog.Info("server starting", "port", cfg.Server.Port) if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { slog.Error("server error", "error", err) os.Exit(1) } }() // Graceful shutdown quit := make(chan os.Signal, 1) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) <-quit slog.Info("shutting down...") ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() if err := srv.Shutdown(ctx); err != nil { slog.Error("forced shutdown", "error", err) } slog.Info("server stopped") }