package main import ( "context" "encoding/json" "errors" "log/slog" "net/http" "os" "os/signal" "syscall" "time" "github.com/alchemistkay/guestguard/internal/api" "github.com/alchemistkay/guestguard/internal/config" "github.com/alchemistkay/guestguard/internal/fraud" "github.com/alchemistkay/guestguard/internal/natspub" "github.com/alchemistkay/guestguard/internal/storage" ) func main() { if err := run(); err != nil { slog.Error("fatal", "err", err) os.Exit(1) } } func run() error { cfg, err := config.Load() if err != nil { return err } logger := newLogger(cfg.Env) slog.SetDefault(logger) rootCtx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) defer stop() logger.Info("connecting to database") db, err := storage.NewDB(rootCtx, cfg.DatabaseURL) if err != nil { return err } defer db.Close() logger.Info("running migrations") if err := db.Migrate(rootCtx); err != nil { return err } logger.Info("connecting to nats", "url", cfg.NATSURL) natsClient, err := natspub.Connect(rootCtx, cfg.NATSURL, logger) if err != nil { return err } defer natsClient.Close() logger.Info("dialing fraud engine", "addr", cfg.FraudGRPCAddr) fraudClient, err := fraud.Dial(rootCtx, cfg.FraudGRPCAddr, cfg.FraudGRPCTimeout, logger) if err != nil { return err } defer fraudClient.Close() hub := api.NewHub(logger) accessLogs := storage.NewAccessLogRepo(db) fraudSub, err := natspub.NewFraudScoredSubscriber( rootCtx, natsClient, "core-api-fraud-scored", func(ctx context.Context, evt natspub.FraudScored) error { if err := accessLogs.ApplyScore(ctx, storage.ApplyScoreParams{ AccessLogID: evt.AccessLogID, Score: evt.Score, Reasons: evt.Reasons, Flagged: evt.Score >= 60, }); err != nil { return err } payload, _ := json.Marshal(evt) hub.Broadcast(api.WSEvent{ Type: "fraud.scored", EventID: evt.EventID, Payload: payload, }) return nil }, logger, ) if err != nil { return err } fraudConsumeCtx, err := fraudSub.Start(rootCtx) if err != nil { return err } defer fraudConsumeCtx.Stop() rsvpSub, err := natspub.NewRSVPConfirmedSubscriber( rootCtx, natsClient, "core-api-rsvp-confirmed-ws", func(ctx context.Context, evt natspub.RSVPConfirmed) error { payload, _ := json.Marshal(evt) hub.Broadcast(api.WSEvent{ Type: "rsvp.confirmed", EventID: evt.EventID, Payload: payload, }) return nil }, logger, ) if err != nil { return err } rsvpConsumeCtx, err := rsvpSub.Start(rootCtx) if err != nil { return err } defer rsvpConsumeCtx.Stop() srv := &http.Server{ Addr: cfg.HTTPAddr, Handler: api.NewServer(api.ServerDeps{ Logger: logger, DB: db, Hub: hub, AccessPublisher: natsClient, RSVPPublisher: natsClient, FraudScorer: fraudClient, TokenTTL: cfg.TokenTTL, }).Handler(), ReadHeaderTimeout: 5 * time.Second, ReadTimeout: 30 * time.Second, WriteTimeout: 0, // 0 lets WS connections live; per-request handlers still bound by their own ctx IdleTimeout: 60 * time.Second, } errCh := make(chan error, 1) go func() { logger.Info("http server starting", "addr", cfg.HTTPAddr, "env", cfg.Env) if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { errCh <- err } close(errCh) }() select { case <-rootCtx.Done(): logger.Info("shutdown signal received") case err := <-errCh: if err != nil { return err } } shutdownCtx, cancel := context.WithTimeout(context.Background(), cfg.ShutdownTimeout) defer cancel() if err := srv.Shutdown(shutdownCtx); err != nil { logger.Error("graceful shutdown failed", "err", err) return err } logger.Info("shutdown complete") return nil } func newLogger(env string) *slog.Logger { level := slog.LevelInfo if env == "development" { level = slog.LevelDebug } return slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: level})) }