package main import ( "context" "flag" "log" "net/http" "os" "os/signal" "path/filepath" "sync" "syscall" "time" "github.com/example/notify-gateway/internal/auth" "github.com/example/notify-gateway/internal/config" "github.com/example/notify-gateway/internal/divera" "github.com/example/notify-gateway/internal/gateway" "github.com/example/notify-gateway/internal/httpserver" "github.com/example/notify-gateway/internal/mailingress" "github.com/example/notify-gateway/internal/outbox" ) func main() { configPath := flag.String("config", "./data/config.json", "path to persistent config") outboxPath := flag.String("outbox", "", "SQLite outbox path (default: outbox.db next to config)") flag.Parse() logger := log.New(os.Stdout, "notify-gateway ", log.LstdFlags|log.LUTC) store, err := config.Open(*configPath) if err != nil { logger.Fatal(err) } cfg := store.Get() if err := config.Validate(cfg); err != nil { logger.Fatal(err) } if cfg.Server.AdminPasswordHash == "" { password := os.Getenv("GATEWAY_ADMIN_PASSWORD") generated := false if password == "" { password = randomPassword() generated = true } hash, err := auth.HashPassword(password) if err != nil { logger.Fatal(err) } if err := store.Update(func(c *config.Config) error { c.Server.AdminPasswordHash = hash; return nil }); err != nil { logger.Fatal(err) } if generated { logger.Printf("INITIAL ADMIN PASSWORD (shown once): %s", password) } else { logger.Printf("initialized admin password from GATEWAY_ADMIN_PASSWORD") } } client := divera.New(func() config.DiveraConfig { return store.Get().Divera }) dispatcher := gateway.New(store, client) server := httpserver.New(store, dispatcher, client, logger) if *outboxPath == "" { *outboxPath = filepath.Join(filepath.Dir(*configPath), "outbox.db") } db, err := outbox.Open(*outboxPath) if err != nil { logger.Fatal(err) } defer db.Close() queue := &gateway.Queue{Store: db, Dispatcher: dispatcher, Logger: logger} poller := &mailingress.Poller{Config: store, Queue: queue} server.UseQueue(queue, poller) workerCtx, cancelWorkers := context.WithCancel(context.Background()) defer cancelWorkers() var workers sync.WaitGroup workers.Add(2) go func() { defer workers.Done(); queue.Run(workerCtx) }() go func() { defer workers.Done(); poller.Run(workerCtx) }() cfg = store.Get() srv := &http.Server{Addr: cfg.Server.Listen, Handler: server.Handler(), ReadHeaderTimeout: 10 * time.Second, ReadTimeout: 20 * time.Second, WriteTimeout: 30 * time.Second, IdleTimeout: 60 * time.Second} logger.Printf("listening on %s (dry_run=%v)", cfg.Server.Listen, cfg.Divera.DryRun) stop, stopSignals := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer stopSignals() serverErr := make(chan error, 1) go func() { serverErr <- srv.ListenAndServe() }() select { case <-stop.Done(): case err := <-serverErr: if err != nil && err != http.ErrServerClosed { logger.Printf("HTTP server stopped: %v", err) } } shutdown, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() if err := srv.Shutdown(shutdown); err != nil { _ = srv.Close() } cancelWorkers() workers.Wait() } func randomPassword() string { return auth.RandomSecret() }