Files
wijtransferen/backend/cmd/server/main.go
T
2026-08-22 17:27:55 +02:00

173 lines
5.5 KiB
Go

package main
import (
"context"
"log"
"net/http"
"os"
"os/signal"
"strings"
"syscall"
"time"
_ "github.com/example/sndit/backend/docs"
"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/media"
"github.com/example/sndit/backend/internal/storage"
"github.com/gin-gonic/gin"
swaggerFiles "github.com/swaggo/files"
ginSwagger "github.com/swaggo/gin-swagger"
)
// @title Noah Bianchi Studio API
// @version 1.0
// @description REST API for photographer galleries and client delivery.
// @schemes http https
// @BasePath /
// @securityDefinitions.apikey studioSession
// @in header
// @name Cookie
// @description Use the studio_session cookie as studio_session=<value>.
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,
})
router := gin.New()
router.Use(gin.Recovery(), withCORS(cfg.CORSOrigin), withLogging())
router.GET("/health", health)
router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler,
ginSwagger.DocExpansion("none"),
ginSwagger.PersistAuthorization(true),
))
require := authService.Require()
authHandler.RegisterRoutes(router)
authHandler.RegisterProtectedRoutes(router, require)
galleryHandler.RegisterProtectedRoutes(router, require)
mediaHandler.RegisterRoutes(router, require)
galleryHandler.RegisterPublicRoutes(router)
downloadHandler.RegisterRoutes(router)
devHandler.RegisterRoutes(router, require)
server := &http.Server{
Addr: ":" + cfg.Port,
Handler: router,
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)
}
}
// health godoc
// @Summary Check API health
// @Tags system
// @Produce json
// @Success 200 {object} map[string]string
// @Router /health [get]
func health(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"status": "ok"})
}
func withCORS(allowedOrigin string) gin.HandlerFunc {
return func(c *gin.Context) {
origin := c.GetHeader("Origin")
if origin != "" && originAllowed(origin, allowedOrigin) {
c.Header("Access-Control-Allow-Origin", origin)
c.Header("Access-Control-Allow-Credentials", "true")
c.Header("Vary", "Origin")
}
if c.Request.Method == http.MethodOptions {
c.Header("Access-Control-Allow-Methods", "GET, POST, PATCH, DELETE, OPTIONS")
c.Header("Access-Control-Allow-Headers", "Content-Type")
c.Status(http.StatusNoContent)
c.Abort()
return
}
c.Next()
}
}
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() gin.HandlerFunc {
return func(c *gin.Context) {
started := time.Now()
c.Next()
log.Printf("%s %s %d %s", c.Request.Method, c.Request.URL.Path, c.Writer.Status(), time.Since(started).Round(time.Millisecond))
}
}