package main import ( "context" "log" "net/http" "os" "os/signal" "strings" "syscall" "time" "github.com/example/sndit/backend/internal/auth" "github.com/example/sndit/backend/internal/config" "github.com/example/sndit/backend/internal/db" devtools "github.com/example/sndit/backend/internal/dev" "github.com/example/sndit/backend/internal/downloads" "github.com/example/sndit/backend/internal/galleries" "github.com/example/sndit/backend/internal/gifts" "github.com/example/sndit/backend/internal/media" "github.com/example/sndit/backend/internal/storage" ) func main() { cfg := config.Load() ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer stop() database, err := db.New(ctx, cfg.DBDriver, cfg.DatabaseDSN()) if err != nil { log.Fatalf("database unavailable: %v", err) } defer database.Close() objectStorage, err := storage.NewMinIO(storage.Config{ Endpoint: cfg.StorageEndpoint, AccessKey: cfg.StorageAccessKey, SecretKey: cfg.StorageSecretKey, Bucket: cfg.StorageBucket, UseSSL: cfg.StorageUseSSL, CORSOrigins: cfg.CORSOrigin, }) if err != nil { log.Fatalf("storage unavailable: %v", err) } if err := objectStorage.EnsureBucket(ctx); err != nil { log.Fatalf("storage unavailable: %v", err) } authRepository := auth.NewRepository(database) authService, err := auth.NewService(authRepository, cfg.SessionSecret, cfg.CookieSecure) if err != nil { log.Fatalf("authentication unavailable: %v", err) } galleryRepository := galleries.NewRepository(database) mediaRepository := media.NewRepository(database) mediaProcessor := media.NewProcessor(mediaRepository, objectStorage, 2) defer mediaProcessor.Close() downloadRepository := downloads.NewRepository(database) downloadService := downloads.NewService(downloadRepository, mediaRepository, objectStorage, 1) defer downloadService.Close() authHandler := auth.NewHandler(authService) galleryHandler := galleries.NewHandler(galleryRepository, mediaRepository, objectStorage, authService) mediaHandler := media.NewHandler(mediaRepository, objectStorage, mediaProcessor, authService) downloadHandler := downloads.NewHandler(galleryRepository, mediaRepository, objectStorage, authService, downloadService) devHandler := devtools.NewHandler(database, objectStorage, authService, devtools.Config{ DBDriver: cfg.DBDriver, StorageEndpoint: cfg.StorageEndpoint, StorageBucket: cfg.StorageBucket, StorageUseSSL: cfg.StorageUseSSL, CORSOrigins: cfg.CORSOrigin, CookieSecure: cfg.CookieSecure, }) mux := http.NewServeMux() mux.HandleFunc("GET /health", health) authHandler.RegisterRoutes(mux) galleryHandler.RegisterProtectedRoutes(mux, authService.Require) mediaHandler.RegisterRoutes(mux, authService.Require) galleryHandler.RegisterPublicRoutes(mux) downloadHandler.RegisterRoutes(mux) devHandler.RegisterRoutes(mux, authService.Require) // Keep the original public gift endpoint available while the gallery product // uses /api/public/galleries/:slug. legacyGifts := gifts.NewHandler(gifts.NewService(gifts.NewRepository(database))) mux.Handle("/api/gifts/", legacyGifts.Routes()) server := &http.Server{ Addr: ":" + cfg.Port, Handler: withCORS(withLogging(mux), cfg.CORSOrigin), ReadHeaderTimeout: 5 * time.Second, ReadTimeout: 10 * time.Second, WriteTimeout: 10 * time.Second, IdleTimeout: 60 * time.Second, } go func() { log.Printf("API listening on http://localhost%s", server.Addr) if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { log.Fatalf("server failed: %v", err) } }() <-ctx.Done() shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() if err := server.Shutdown(shutdownCtx); err != nil { log.Printf("server shutdown failed: %v", err) } } func health(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", "application/json; charset=utf-8") w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(`{"status":"ok"}` + "\n")) } func withCORS(next http.Handler, allowedOrigin string) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { origin := r.Header.Get("Origin") if origin != "" && originAllowed(origin, allowedOrigin) { w.Header().Set("Access-Control-Allow-Origin", origin) w.Header().Set("Access-Control-Allow-Credentials", "true") w.Header().Set("Vary", "Origin") } if r.Method == http.MethodOptions { w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PATCH, DELETE, OPTIONS") w.Header().Set("Access-Control-Allow-Headers", "Content-Type") w.WriteHeader(http.StatusNoContent) return } next.ServeHTTP(w, r) }) } func originAllowed(origin, configured string) bool { for _, allowed := range strings.Split(configured, ",") { if strings.TrimSpace(allowed) == "*" || strings.TrimSpace(allowed) == origin { return true } } return false } func withLogging(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { started := time.Now() next.ServeHTTP(w, r) log.Printf("%s %s %s", r.Method, r.URL.Path, time.Since(started).Round(time.Millisecond)) }) }