This commit is contained in:
2026-08-22 02:59:16 +02:00
commit 6a5bb1d699
100 changed files with 17409 additions and 0 deletions
+153
View File
@@ -0,0 +1,153 @@
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))
})
}