package server import ( "music-server/cmd/web" "music-server/internal/logging" "music-server/internal/server/middleware" "net/http" "sort" "strings" "time" "github.com/a-h/templ" "github.com/labstack/echo/v5" echoMiddleware "github.com/labstack/echo/v5/middleware" echoSwagger "github.com/swaggo/echo-swagger/v2" "go.uber.org/zap" ) // @Title MusicServer API // @version 1.0 // @description API for the MusicServer application // @termsOfService http://sanplex.xyz/terms/ // @contact.name Sebastian Olsson // @contact.email zarnor91@gmail.com // @license.name MIT // @license.url http://opensource.org/licenses/MIT // @host localhost:8080 // @BasePath / func (s *Server) RegisterRoutes() http.Handler { e := echo.New() // Serve OpenAPI spec at /openapi e.GET("/openapi", echo.WrapHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") http.ServeFile(w, r, "cmd/docs/swagger.json") }))) e.Use(logging.RequestLogger()) e.Use(echoMiddleware.Recover()) e.Use(echoMiddleware.CORSWithConfig(echoMiddleware.CORSConfig{ AllowOrigins: []string{"https://*", "http://*"}, AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"}, AllowHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"}, AllowCredentials: true, MaxAge: 300, })) fileServer := http.FileServer(http.FS(web.Assets)) e.GET("/assets/*", echo.WrapHandler(fileServer)) e.GET("/search", echo.WrapHandler(templ.Handler(web.SearchForm()))) e.POST("/find", echo.WrapHandler(http.HandlerFunc(web.FindSoundtrackWebHandler))) e.Static("/", "/frontend") // Swagger UI e.GET("/swagger/*", echoSwagger.WrapHandler) // ============================================ // Legacy Endpoints (Deprecated - use /api/v1/ instead) // ============================================ deprecatedMiddleware := middleware.DeprecationMiddleware health := NewHealthHandler(s.db) e.GET("/health", deprecatedMiddleware(health.HealthCheck)) version := NewVersionHandler() e.GET("/version", deprecatedMiddleware(version.GetLatestVersion)) e.GET("/version/history", deprecatedMiddleware(version.GetVersionHistory)) character := NewCharacterHandler() e.GET("/character", deprecatedMiddleware(character.GetCharacter)) e.GET("/characters", deprecatedMiddleware(character.GetCharacterList)) download := NewDownloadHandler() e.GET("/download", deprecatedMiddleware(download.checkLatest)) e.GET("/download/list", deprecatedMiddleware(download.listAssetsOfLatest)) e.GET("/download/windows", deprecatedMiddleware(download.downloadLatestWindows)) e.GET("/download/linux", deprecatedMiddleware(download.downloadLatestLinux)) sync := NewSyncHandler() syncGroup := e.Group("/sync") syncGroup.GET("", deprecatedMiddleware(middleware.SyncCheckMiddleware(sync.SyncSoundtracksNewOnlyChanges))) syncGroup.GET("/progress", deprecatedMiddleware(sync.SyncProgress)) syncGroup.GET("/new", deprecatedMiddleware(middleware.SyncCheckMiddleware(sync.SyncSoundtracksNewOnlyChanges))) syncGroup.GET("/full", deprecatedMiddleware(middleware.SyncCheckMiddleware(sync.SyncSoundtracksNewFull))) syncGroup.GET("/new/full", deprecatedMiddleware(middleware.SyncCheckMiddleware(sync.SyncSoundtracksNewFull))) syncGroup.GET("/quick", deprecatedMiddleware(middleware.SyncCheckMiddleware(sync.SyncSoundtracksNewOnlyChanges))) syncGroup.GET("/reset", deprecatedMiddleware(middleware.SyncCheckMiddleware(sync.ResetDB))) music := NewMusicHandler() musicGroup := e.Group("/music") musicGroup.GET("", deprecatedMiddleware(middleware.SyncCheckMiddleware(music.GetSong))) musicGroup.GET("/soundTest", deprecatedMiddleware(middleware.SyncCheckMiddleware(music.GetSoundCheckSong))) musicGroup.GET("/reset", deprecatedMiddleware(middleware.SyncCheckMiddleware(music.ResetMusic))) musicGroup.GET("/rand", deprecatedMiddleware(middleware.SyncCheckMiddleware(music.GetRandomSong))) musicGroup.GET("/rand/low", deprecatedMiddleware(middleware.SyncCheckMiddleware(music.GetRandomSongLowChance))) musicGroup.GET("/rand/classic", deprecatedMiddleware(middleware.SyncCheckMiddleware(music.GetRandomSongClassic))) musicGroup.GET("/info", deprecatedMiddleware(music.GetSongInfo)) musicGroup.GET("/list", deprecatedMiddleware(music.GetPlayedSongs)) musicGroup.GET("/next", deprecatedMiddleware(middleware.SyncCheckMiddleware(music.GetNextSong))) musicGroup.GET("/previous", deprecatedMiddleware(middleware.SyncCheckMiddleware(music.GetPreviousSong))) musicGroup.GET("/all", deprecatedMiddleware(middleware.SyncCheckMiddleware(music.GetAllSoundtracksRandom))) musicGroup.GET("/all/order", deprecatedMiddleware(middleware.SyncCheckMiddleware(music.GetAllSoundtracks))) musicGroup.GET("/all/random", deprecatedMiddleware(middleware.SyncCheckMiddleware(music.GetAllSoundtracksRandom))) musicGroup.PUT("/played", deprecatedMiddleware(middleware.SyncCheckMiddleware(music.PutPlayed))) musicGroup.GET("/addQue", deprecatedMiddleware(middleware.SyncCheckMiddleware(music.AddLatestToQue))) musicGroup.GET("/addPlayed", deprecatedMiddleware(middleware.SyncCheckMiddleware(music.AddLatestPlayed))) // ============================================ // API v1 Routes with Token Authentication // ============================================ // Create /api/v1 group apiV1 := e.Group("/api/v1") // Create rate limiter for token creation: 10 requests per minute per IP tokenRateLimiter := middleware.NewRateLimiter(10, time.Minute) tokenRateLimitMiddleware := middleware.RateLimitMiddleware(tokenRateLimiter) // Token endpoints (public - no token required) tokenGroup := apiV1.Group("/token") tokenGroup.POST("", func(c *echo.Context) error { return s.tokenHandler.CreateTokenHandler(c) }, tokenRateLimitMiddleware) tokenGroup.DELETE("", func(c *echo.Context) error { return s.tokenHandler.DeleteTokenHandler(c) }) tokenGroup.POST("/cleanup", func(c *echo.Context) error { return s.tokenHandler.CleanupExpiredSessionsHandler(c) }) // Protected endpoints - require valid token // Create token auth middleware with pool access tokenAuthMiddleware := middleware.TokenAuthMiddleware(s.db.Pool) protectedV1 := apiV1.Group("", tokenAuthMiddleware) // Statistics endpoints (protected) statisticsHandler := s.statisticsHandler statsGroup := protectedV1.Group("/statistics") statsGroup.GET("/soundtracks/most-played", func(c *echo.Context) error { return statisticsHandler.GetMostPlayedSoundtracks(c) }) statsGroup.GET("/soundtracks/least-played", func(c *echo.Context) error { return statisticsHandler.GetLeastPlayedSoundtracks(c) }) statsGroup.GET("/soundtracks/never-played", func(c *echo.Context) error { return statisticsHandler.GetNeverPlayedSoundtracks(c) }) statsGroup.GET("/soundtracks/last-played", func(c *echo.Context) error { return statisticsHandler.GetLastPlayedSoundtracks(c) }) statsGroup.GET("/soundtracks/oldest-played", func(c *echo.Context) error { return statisticsHandler.GetOldestPlayedSoundtracks(c) }) statsGroup.GET("/songs/most-played", func(c *echo.Context) error { return statisticsHandler.GetMostPlayedSongs(c) }) statsGroup.GET("/songs/least-played", func(c *echo.Context) error { return statisticsHandler.GetLeastPlayedSongs(c) }) statsGroup.GET("/summary", func(c *echo.Context) error { return statisticsHandler.GetStatisticsSummary(c) }) // Version endpoints (protected) versionGroup := protectedV1.Group("/version") versionGroup.GET("", func(c *echo.Context) error { return version.GetLatestVersion(c) }) versionGroup.GET("/history", func(c *echo.Context) error { return version.GetVersionHistory(c) }) // Character endpoints (protected) characterGroup := protectedV1.Group("/character") characterGroup.GET("", func(c *echo.Context) error { return character.GetCharacter(c) }) characterGroup.GET("/list", func(c *echo.Context) error { return character.GetCharacterList(c) }) // Download endpoints (protected) downloadGroup := protectedV1.Group("/download") downloadGroup.GET("", func(c *echo.Context) error { return download.checkLatest(c) }) downloadGroup.GET("/list", func(c *echo.Context) error { return download.listAssetsOfLatest(c) }) downloadGroup.GET("/windows", func(c *echo.Context) error { return download.downloadLatestWindows(c) }) downloadGroup.GET("/linux", func(c *echo.Context) error { return download.downloadLatestLinux(c) }) // Sync endpoints (protected) protectedSyncGroup := protectedV1.Group("/sync") protectedSyncGroup.GET("", func(c *echo.Context) error { return sync.SyncSoundtracksNewOnlyChanges(c) }) protectedSyncGroup.GET("/progress", func(c *echo.Context) error { return sync.SyncProgress(c) }) protectedSyncGroup.GET("/new", func(c *echo.Context) error { return sync.SyncSoundtracksNewOnlyChanges(c) }) protectedSyncGroup.GET("/full", func(c *echo.Context) error { return sync.SyncSoundtracksNewFull(c) }) protectedSyncGroup.GET("/new/full", func(c *echo.Context) error { return sync.SyncSoundtracksNewFull(c) }) protectedSyncGroup.GET("/quick", func(c *echo.Context) error { return sync.SyncSoundtracksNewOnlyChanges(c) }) protectedSyncGroup.GET("/reset", func(c *echo.Context) error { return sync.ResetDB(c) }) // Music endpoints (protected) protectedMusicGroup := protectedV1.Group("/music") protectedMusicGroup.GET("", func(c *echo.Context) error { return music.GetSong(c) }) protectedMusicGroup.GET("/soundTest", func(c *echo.Context) error { return music.GetSoundCheckSong(c) }) protectedMusicGroup.GET("/reset", func(c *echo.Context) error { return music.ResetMusic(c) }) protectedMusicGroup.GET("/rand", func(c *echo.Context) error { return music.GetRandomSong(c) }) protectedMusicGroup.GET("/rand/low", func(c *echo.Context) error { return music.GetRandomSongLowChance(c) }) protectedMusicGroup.GET("/rand/classic", func(c *echo.Context) error { return music.GetRandomSongClassic(c) }) protectedMusicGroup.GET("/info", func(c *echo.Context) error { return music.GetSongInfo(c) }) protectedMusicGroup.GET("/list", func(c *echo.Context) error { return music.GetPlayedSongs(c) }) protectedMusicGroup.GET("/next", func(c *echo.Context) error { return music.GetNextSong(c) }) protectedMusicGroup.GET("/previous", func(c *echo.Context) error { return music.GetPreviousSong(c) }) protectedMusicGroup.GET("/all", func(c *echo.Context) error { return music.GetAllSoundtracksRandom(c) }) protectedMusicGroup.GET("/all/order", func(c *echo.Context) error { return music.GetAllSoundtracks(c) }) protectedMusicGroup.GET("/all/random", func(c *echo.Context) error { return music.GetAllSoundtracksRandom(c) }) protectedMusicGroup.PUT("/played", func(c *echo.Context) error { return music.PutPlayed(c) }) protectedMusicGroup.GET("/addQue", func(c *echo.Context) error { return music.AddLatestToQue(c) }) protectedMusicGroup.GET("/addPlayed", func(c *echo.Context) error { return music.AddLatestPlayed(c) }) routes := e.Router().Routes() sort.Slice(routes, func(i, j int) bool { return routes[i].Path < routes[j].Path }) for _, r := range routes { if (r.Method == "GET" || r.Method == "POST" || r.Method == "PUT" || r.Method == "DELETE") && !strings.Contains(r.Name, "github") { logging.GetLogger().Debug("Registered route", zap.String("method", r.Method), zap.String("path", r.Path)) } } return e }