181 lines
5.7 KiB
Go
181 lines
5.7 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,
|
|
PublicEndpoint: cfg.StoragePublicEndpoint,
|
|
AccessKey: cfg.StorageAccessKey,
|
|
SecretKey: cfg.StorageSecretKey,
|
|
Bucket: cfg.StorageBucket,
|
|
UseSSL: cfg.StorageUseSSL,
|
|
PublicUseSSL: true,
|
|
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,
|
|
APIURL: cfg.APIURL,
|
|
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() {
|
|
addr := cfg.APIURL
|
|
if addr == "" {
|
|
addr = "http://localhost" + server.Addr
|
|
}
|
|
log.Printf("API listening on %s", 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, ",") {
|
|
allowed = strings.TrimRight(strings.TrimSpace(allowed), "/")
|
|
if allowed == "*" || 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))
|
|
}
|
|
}
|