154 lines
5.2 KiB
Go
154 lines
5.2 KiB
Go
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))
|
|
})
|
|
}
|