package main import ( "context" "errors" "log/slog" "net/http" "os" "os/signal" "strings" "syscall" "time" "mixmaker/backend/internal/adapter/httpapi" "mixmaker/backend/internal/adapter/postgres" "mixmaker/backend/internal/application" "mixmaker/backend/internal/realtime" ) func main() { ctx := context.Background() databaseURL := required("DATABASE_URL") if len(os.Args) > 1 && os.Args[1] == "migrate" { dir := env("MIGRATIONS_DIR", "migrations") if err := postgres.Migrate(ctx, databaseURL, dir); err != nil { slog.Error("migration failed", "error", err) os.Exit(1) } slog.Info("migrations applied") return } store, err := postgres.Open(ctx, databaseURL) if err != nil { slog.Error("database connection failed", "error", err) os.Exit(1) } defer store.Close() hub := realtime.New() service := application.New(store, hub) cfg := httpapi.Config{ DiscordClientID: required("DISCORD_CLIENT_ID"), DiscordClientSecret: required("DISCORD_CLIENT_SECRET"), DiscordRedirectURL: required("DISCORD_REDIRECT_URL"), FrontendURL: env("FRONTEND_URL", "/"), CookieName: env("SESSION_COOKIE_NAME", "mixmaker_session"), SecureCookies: env("COOKIE_SECURE", "true") == "true", SessionTTL: durationEnv("SESSION_TTL", 7*24*time.Hour), AdminDiscordIDs: csvSet(os.Getenv("ADMIN_DISCORD_IDS")), } server := &http.Server{ Addr: env("HTTP_ADDR", ":8080"), Handler: httpapi.New(service, store, hub, cfg), ReadHeaderTimeout: 5 * time.Second, IdleTimeout: 60 * time.Second, } go func() { slog.Info("API listening", "address", server.Addr) if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { slog.Error("server failed", "error", err) os.Exit(1) } }() stop, cancel := signal.NotifyContext(ctx, syscall.SIGINT, syscall.SIGTERM) defer cancel() <-stop.Done() shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 10*time.Second) defer shutdownCancel() if err := server.Shutdown(shutdownCtx); err != nil { slog.Error("graceful shutdown failed", "error", err) } } func required(key string) string { value := os.Getenv(key) if value == "" { slog.Error("required environment variable missing", "key", key) os.Exit(2) } return value } func env(key, fallback string) string { if value := os.Getenv(key); value != "" { return value } return fallback } func csvSet(value string) map[string]bool { out := make(map[string]bool) for _, item := range strings.Split(value, ",") { if item = strings.TrimSpace(item); item != "" { out[item] = true } } return out } func durationEnv(key string, fallback time.Duration) time.Duration { value := os.Getenv(key) if value == "" { return fallback } parsed, err := time.ParseDuration(value) if err != nil || parsed <= 0 { slog.Error("invalid duration environment variable", "key", key, "value", value) os.Exit(2) } return parsed }