// main.go — auth-gate entry point // // This file wires together configuration loading, the HTTP server, and the // background processes (permanent-whitelist file watcher, temporary-whitelist // cleanup). // // Why this structure? // // - Main keeps the wiring thin. Each component (config, auth, cleanup, watcher) // owns its own logic so we can test and reason about them independently. // - We don't embed the HTTP server logic inside main; instead main calls // createAuthHandler(config) and server.Serve(). This makes the handler // functionally pure (aside from config reads) and easier to mock. // - The background goroutines are started explicitly so we can stop them on // SIGTERM without leaking resources. package main import ( "context" "log" "net/http" "os" "os/signal" "syscall" "time" ) func main() { cfg := loadConfig() // Load the permanent whitelist from disk once at startup. // We reload it periodically via watcher (see watcher.go). loadPermanentWhitelist(cfg.PermanentWhitelistFile) // Start the background watcher that reloads the permanent whitelist file. // It only reloads when the file actually changes (mtime comparison), so // frequent touch-operations don't cause unnecessary restarts. w := newPermanentWhitelistWatcher(cfg.PermanentWhitelistFile, cfg.WatchInterval, cfg.PermanentWhitelist) w.start() // Start the cleanup goroutine that expires temporary whitelisted IPs // whose TTL has elapsed. It runs every cfg.CleanupInterval. go cleanupLoop(cfg.TemporaryWhitelist, cfg.CleanupInterval) // Build the HTTP server. addr := ":" + cfg.Port server := newAuthServer(cfg, addr) // Start serving in a goroutine so we can do graceful shutdown. go func() { log.Printf("auth-gate listening on %s", addr) if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { log.Fatalf("server error: %v", err) } }() // Graceful shutdown on SIGTERM / SIGINT. // // We wait for a signal, then call server.Shutdown(ctx) which: // 1. Stops accepting new connections // 2. Closes idle connections // 3. Waits for in-flight requests to complete (up to 30s) // // This is important for Docker deployments: without it the container // would be killed mid-request and clients would see connection errors. quit := make(chan os.Signal, 1) signal.Notify(quit, syscall.SIGTERM, syscall.SIGINT) <-quit log.Println("shutting down...") ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() server.Shutdown(ctx) }