feat: nginx reverse proxy, Spotify import, overlay system, UI overhaul

- Add nginx as single entry point: /api/* → backend, /* → web
- NEXT_PUBLIC_API_URL="" so all API calls are relative (go through nginx)
- Add Spotify playlist import (Client Credentials OAuth, up to 500 tracks)
- Add Yandex/Spotify tabbed import UI on /playlists
- Add stream overlay system (SSE + polling fallback, 9 styles)
- Reorganize pages into (main) route group
- Add QueuePanel, VersionsPanel, Toaster components
- Add overlay settings tab in /settings

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-28 00:45:53 +03:00
parent 87ba7a0ecf
commit 428548a620
55 changed files with 5934 additions and 2052 deletions

View File

@@ -2,22 +2,29 @@ package router
import (
"net/http"
"strings"
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
"github.com/toyffee/party-mix/internal/config"
"github.com/toyffee/party-mix/internal/handlers"
"gorm.io/gorm"
)
func New(db *gorm.DB, jwtSecret string) *gin.Engine {
func New(db *gorm.DB, cfg *config.Config) *gin.Engine {
r := gin.Default()
origins := strings.Split(cfg.AllowedOrigins, ",")
for i, o := range origins {
origins[i] = strings.TrimSpace(o)
}
r.Use(cors.New(cors.Config{
AllowAllOrigins: true,
AllowOrigins: origins,
AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
AllowHeaders: []string{"Origin", "Content-Type", "Authorization"},
ExposeHeaders: []string{"Content-Length", "Content-Range", "Accept-Ranges"},
AllowCredentials: false,
AllowCredentials: true,
}))
r.GET("/health", func(c *gin.Context) {
@@ -31,25 +38,27 @@ func New(db *gorm.DB, jwtSecret string) *gin.Engine {
proxy.GET("/img", handlers.ImgProxyHandler)
proxy.GET("/mp3", handlers.MP3ProxyHandler)
proxy.GET("/yandex-playlist", handlers.YandexPlaylistHandler)
proxy.GET("/spotify-playlist", handlers.SpotifyPlaylistHandler)
}
auth := r.Group("/api/auth")
{
auth.POST("/register", handlers.Register(db))
auth.POST("/login", handlers.Login(db, jwtSecret))
auth.GET("/me", handlers.AuthRequired(jwtSecret), handlers.Me(db))
auth.POST("/login", handlers.Login(db, cfg.JWTSecret, cfg.CookieSecure))
auth.POST("/logout", handlers.Logout(cfg.CookieSecure))
auth.GET("/me", handlers.AuthRequired(cfg.JWTSecret), handlers.Me(db))
}
r.GET("/api/playlists/public", handlers.GetPublicPlaylists(db))
versions := r.Group("/api/versions", handlers.AuthRequired(jwtSecret))
versions := r.Group("/api/versions", handlers.AuthRequired(cfg.JWTSecret))
{
versions.GET("", handlers.GetVersions(db))
versions.POST("", handlers.SaveVersion(db))
versions.DELETE("", handlers.DeleteVersion(db))
}
playlists := r.Group("/api/playlists", handlers.AuthRequired(jwtSecret))
playlists := r.Group("/api/playlists", handlers.AuthRequired(cfg.JWTSecret))
{
playlists.GET("", handlers.GetPlaylists(db))
playlists.POST("", handlers.CreatePlaylist(db))
@@ -59,6 +68,13 @@ func New(db *gorm.DB, jwtSecret string) *gin.Engine {
playlists.POST("/:id/tracks", handlers.AddTrackToPlaylist(db))
}
overlay := r.Group("/api/overlay")
{
overlay.PUT("/state", handlers.AuthRequired(cfg.JWTSecret), handlers.PushOverlayState)
overlay.GET("/:token/state", handlers.GetOverlayState)
overlay.GET("/:token/stream", handlers.StreamOverlayState)
}
remote := r.Group("/api/remote")
{
remote.POST("", handlers.CreateRemoteRoom)