Files
MyClub/internal/routes/routes.go
T
Tomas Dvorak e9a63073e5 dev day #63
2025-10-17 17:39:11 +02:00

491 lines
21 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package routes
import (
"fotbal-club/internal/config"
"fotbal-club/internal/controllers"
"fotbal-club/internal/middleware"
"fotbal-club/internal/services"
"fotbal-club/pkg/email"
"time"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
// Global newsletter automation instance
var newsletterAutomation *services.NewsletterAutomation
// SetNewsletterAutomation stores the newsletter automation instance
func SetNewsletterAutomation(na *services.NewsletterAutomation) {
newsletterAutomation = na
}
// GetNewsletterAutomation returns the newsletter automation instance
func GetNewsletterAutomation() *services.NewsletterAutomation {
return newsletterAutomation
}
// SetupRoutes configures all the routes for the application
func SetupRoutes(api *gin.RouterGroup, db *gorm.DB) {
emailService := email.NewEmailService(config.AppConfig, db)
// Initialize controllers
baseController := &controllers.BaseController{DB: db}
authController := controllers.NewAuthController(db)
facrController := controllers.NewFACRController(db)
contactController := controllers.NewContactController(db, emailService)
passwordController := controllers.NewPasswordController(db, emailService)
aiController := controllers.NewAIController(db)
scoreboardController := controllers.NewScoreboardController(db)
youtubeController := controllers.NewYouTubeController(db)
aboutController := controllers.NewAboutController(db)
galleryController := controllers.NewGalleryController(db)
umamiController := controllers.NewUmamiController()
filesController := &controllers.FilesController{DB: db}
notificationsController := controllers.NewNotificationsController(db, emailService)
emailController := controllers.NewEmailController(db)
prefetchController := controllers.NewPrefetchController()
seoController := controllers.NewSEOController(db)
navigationController := controllers.NewNavigationController(db)
pollController := controllers.NewPollController(db)
clothingController := controllers.NewClothingController(db)
pageElementConfigController := controllers.NewPageElementConfigController(db)
// API v1 group
{
// Health check
api.GET("/health", baseController.HealthCheck)
// Image proxy (public) to work around CORS when reading images in Canvas on the frontend
api.GET("/proxy/image", baseController.ProxyImage)
// Public SEO: metadata
api.GET("/seo", seoController.GetPublicSEO)
// Public navigation endpoints
api.GET("/navigation", navigationController.GetNavigationItems)
api.GET("/social-links", navigationController.GetSocialLinks)
// Public page element configurations
api.GET("/page-elements", pageElementConfigController.GetPageElementConfigs)
// Email tracking (public)
api.GET("/email/open.gif", emailController.OpenPixel)
api.GET("/email/click", emailController.ClickRedirect)
api.GET("/email/spam", emailController.MarkSpam)
api.GET("/email/unsubscribe", emailController.Unsubscribe)
// Initial setup (public)
api.GET("/setup/status", baseController.SetupStatus)
api.POST("/setup/initialize", baseController.SetupInitialize)
// SMTP validation (public during setup; does not send email, only connects)
api.POST("/setup/validate-smtp", baseController.ValidateSMTP)
// Auth routes
auth := api.Group("/auth")
{
// Limit login attempts to mitigate brute force
auth.POST("/login", middleware.RateLimit(15, time.Minute), authController.Login)
// Logout clears auth cookie (stateless)
auth.POST("/logout", authController.Logout)
// Limit registration bursts
auth.POST("/register", middleware.RateLimit(5, time.Hour), authController.Register)
auth.GET("/check-email", authController.CheckEmail)
// Password reset (public)
auth.POST("/initiate-password-reset", passwordController.InitiatePasswordReset)
auth.POST("/verify-reset-code", passwordController.VerifyResetCode)
auth.POST("/complete-password-reset", passwordController.CompletePasswordReset)
// Legacy password reset endpoints (deprecated)
auth.POST("/forgot-password", passwordController.ForgotPassword)
auth.POST("/forgot-password-admin", passwordController.AdminSendReset)
auth.POST("/reset-password", passwordController.ResetPassword)
auth.GET("/me", middleware.JWTAuth(db), authController.GetCurrentUser)
// Initial admin setup helpers
auth.GET("/admin/exists", authController.AdminExists)
auth.POST("/make-admin", middleware.JWTAuth(db), authController.MakeAdmin)
}
// Event routes (public)
eventController := &controllers.EventController{DB: db}
events := api.Group("/events")
{
events.GET("", eventController.GetEvents)
events.GET("/upcoming", eventController.GetUpcomingEvents)
events.GET("/:id", eventController.GetEventByID)
}
// Protected routes (require authentication)
protected := api.Group("")
protected.Use(middleware.JWTAuth(db))
{
// AI endpoints (protected)
ai := protected.Group("/ai")
{
ai.POST("/blog/generate", aiController.GenerateBlog)
ai.POST("/about/generate", aiController.GenerateAboutPage)
}
// User profile
protected.GET("/me", authController.GetCurrentUser)
// Uploads are registered as a public endpoint below so the handler can
// allow unauthenticated uploads during initial setup (when no admin exists).
// The protected group remains for other authenticated endpoints.
// Events (protected)
protectedEvents := protected.Group("/events")
{
protectedEvents.POST("", eventController.CreateEvent)
protectedEvents.PUT("/:id", eventController.UpdateEvent)
protectedEvents.DELETE("/:id", eventController.DeleteEvent)
}
// Articles (protected - accessible by editors and admins)
articles := protected.Group("/articles")
{
articles.POST("", baseController.CreateArticle)
articles.PUT("/:id", baseController.UpdateArticle)
articles.DELETE("/:id", baseController.DeleteArticle)
// Link article to FACR match
articles.POST("/:id/match-link", baseController.PutArticleMatchLink)
articles.DELETE("/:id/match-link", baseController.DeleteArticleMatchLink)
}
// Teams (protected)
teams := protected.Group("/teams")
{
teams.POST("", baseController.CreateTeam)
teams.PUT("/:id", baseController.UpdateTeam)
teams.DELETE("/:id", baseController.DeleteTeam)
}
// Players (protected)
players := protected.Group("/players")
{
players.POST("", baseController.CreatePlayer)
players.PUT("/:id", baseController.UpdatePlayer)
players.DELETE("/:id", baseController.DeletePlayer)
}
// Sponsors (protected CRUD)
sponsors := protected.Group("/sponsors")
{
sponsors.POST("", baseController.CreateSponsor)
sponsors.PUT("/:id", baseController.UpdateSponsor)
sponsors.DELETE("/:id", baseController.DeleteSponsor)
}
// Admin routes (single consolidated group)
admin := protected.Group("/admin")
admin.Use(middleware.RoleAuth("admin"))
{
// Admin-only endpoints for managing sponsors, etc. (user CRUD removed; no handlers defined)
// Competition aliases (admin)
admin.GET("/competition-aliases", baseController.GetCompetitionAliases)
admin.PUT("/competition-aliases/:code", baseController.PutCompetitionAlias)
admin.DELETE("/competition-aliases/:code", baseController.DeleteCompetitionAlias)
admin.POST("/competition-aliases/reorder", baseController.ReorderCompetitionAliases)
// Categories (admin)
admin.POST("/categories", baseController.CreateCategory)
admin.PUT("/categories/:id", baseController.UpdateCategory)
admin.DELETE("/categories/:id", baseController.DeleteCategory)
// Settings (singleton)
admin.GET("/settings", baseController.GetSettings)
admin.PUT("/settings", baseController.UpdateSettings)
// About page (singleton)
admin.GET("/about", aboutController.GetAdminAboutPage)
admin.PUT("/about", aboutController.UpsertAboutPage)
admin.DELETE("/about", aboutController.DeleteAboutPage)
// Scoreboard (singleton)
admin.GET("/scoreboard", scoreboardController.GetAdmin)
admin.PUT("/scoreboard", scoreboardController.PutAdmin)
// Scoreboard timer controls
admin.POST("/scoreboard/timer/start", scoreboardController.StartTimer)
admin.POST("/scoreboard/timer/pause", scoreboardController.PauseTimer)
admin.POST("/scoreboard/timer/reset", scoreboardController.ResetTimer)
// Scoreboard advanced actions
admin.POST("/scoreboard/swap-sides", scoreboardController.SwapSides)
admin.POST("/scoreboard/second-half", scoreboardController.StartSecondHalf)
// Presets: save/list/load
admin.POST("/scoreboard/save", scoreboardController.SaveState)
admin.GET("/scoreboard/saves", scoreboardController.ListSaves)
admin.POST("/scoreboard/load", scoreboardController.LoadSaved)
// Scoreboard sponsors & QR (admin-only)
admin.GET("/scoreboard/sponsors", scoreboardController.ListSponsors)
admin.POST("/scoreboard/sponsors/upload", scoreboardController.UploadSponsors)
admin.DELETE("/scoreboard/sponsors", scoreboardController.DeleteSponsor)
admin.GET("/scoreboard/qr", scoreboardController.GetQR)
admin.POST("/scoreboard/qr", scoreboardController.UploadQR)
// Users (admin)
admin.GET("/users", authController.ListUsers)
// Create/Update/Delete users
admin.POST("/users", authController.AdminCreateUser)
admin.PUT("/users/:id", authController.AdminUpdateUser)
admin.DELETE("/users/:id", authController.AdminDeleteUser)
// Admin: send password reset email using special SMTP override
admin.POST("/users/send-reset", passwordController.AdminSendReset)
// Admin: reset password for a specific user ID
admin.POST("/users/:id/reset-password", passwordController.AdminSendResetByID)
// Admin matches merged with overrides
admin.GET("/matches", baseController.GetAdminMatches)
// Match & Team Logo Overrides
overrides := admin.Group("")
{
// Match overrides
overrides.GET("/match-overrides", baseController.GetMatchOverrides)
overrides.PUT("/match-overrides/:external_match_id", baseController.PutMatchOverride)
overrides.PATCH("/match-overrides/:external_match_id", baseController.PatchMatchOverride)
// Team logo overrides
overrides.GET("/team-logo-overrides", baseController.GetTeamLogoOverrides)
overrides.PUT("/team-logo-overrides/:external_team_id", baseController.PutTeamLogoOverride)
overrides.PATCH("/team-logo-overrides/:external_team_id", baseController.PatchTeamLogoOverride)
}
// Contact messages management
contactMessages := admin.Group("/contact-messages")
{
contactMessages.GET("", contactController.GetContactMessages)
contactMessages.GET("/:id", contactController.GetContactMessage)
contactMessages.PATCH("/:id/read", contactController.MarkMessageAsRead)
contactMessages.POST("/:id/forward", contactController.ForwardContactMessage)
contactMessages.POST("/forward-all", contactController.ForwardAllContactMessages)
contactMessages.DELETE("/:id", contactController.DeleteContactMessage)
contactMessages.DELETE("", contactController.DeleteContactMessages) // Bulk delete
}
// Newsletter management
admin.GET("/newsletter/subscribers", contactController.GetNewsletterSubscribers)
admin.POST("/newsletter/send", contactController.SendNewsletter)
admin.POST("/newsletter/preview", contactController.PreviewNewsletter)
admin.POST("/newsletter/test", contactController.SendNewsletterTest)
// New: send prebuilt digest by type and toggle automation
admin.POST("/newsletter/send-digest", contactController.SendNewsletterDigest)
admin.PATCH("/newsletter/enable", contactController.UpdateNewsletterAutomation)
// Removed deprecated SMTP test route (use /newsletter/test instead)
admin.GET("/newsletter/status", contactController.GetNewsletterStatus)
admin.GET("/newsletter/stats/recent", emailController.GetRecentEmailStats)
admin.GET("/newsletter/stats/:id/events", emailController.GetEmailEventsForLog)
admin.PATCH("/newsletter/subscribers/:id/preferences", contactController.UpdateNewsletterSubscriberPreferences)
admin.DELETE("/newsletter/subscribers/:id", contactController.DeleteNewsletterSubscriber)
admin.PATCH("/newsletter/subscribers/:id/status", contactController.UpdateNewsletterSubscriberStatus)
// Notifications (admin)
notifications := admin.Group("/notifications")
{
notifications.POST("/competition", notificationsController.SendCompetitionNotification)
notifications.POST("/match", notificationsController.SendMatchNotification)
}
// Prefetch management (admin)
prefetch := admin.Group("/prefetch")
{
prefetch.GET("/status", prefetchController.Status)
prefetch.POST("/trigger", prefetchController.Trigger)
}
// Cache RAW viewer (admin)
cache := admin.Group("/cache")
{
cache.GET("/list", baseController.GetAdminCacheList)
cache.GET("/file", baseController.GetAdminCacheFile)
}
// Gallery management (admin)
gallery := admin.Group("/gallery")
{
gallery.GET("/profile", galleryController.GetGalleryProfile) // Get Zonerama profile
gallery.POST("/albums/fetch", galleryController.FetchAlbum) // Fetch single album
gallery.DELETE("/albums/:id", galleryController.DeleteAlbum) // Delete album
gallery.POST("/refresh", galleryController.RefreshFromZonerama) // Refresh from Zonerama
}
// Alias endpoint for saving a single Zonerama album (keeps older frontend code working)
admin.POST("/zonerama/save-album", galleryController.FetchAlbum)
// Save or update a chosen Zonerama pick (photo) in unified cache
admin.POST("/zonerama/pick", baseController.PutZoneramaPick)
// SEO admin
admin.GET("/seo", seoController.GetSEOSettings)
admin.PUT("/seo", seoController.UpdateSEOSettings)
// Files management (admin)
files := admin.Group("/files")
{
files.GET("", filesController.GetAllFiles)
files.GET("/unused", filesController.GetUnusedFiles)
files.GET("/duplicates", filesController.GetDuplicateFiles)
files.GET("/:id/usages", filesController.GetFileUsages)
files.DELETE("/:id", filesController.DeleteFile)
files.POST("/scan", filesController.ScanAndSyncFiles)
files.POST("/refresh-tracking", filesController.RefreshFileTracking)
}
// Navigation management (admin)
navigation := admin.Group("/navigation")
{
navigation.GET("", navigationController.GetAllNavigationItems)
navigation.POST("", navigationController.CreateNavigationItem)
navigation.PUT("/:id", navigationController.UpdateNavigationItem)
navigation.DELETE("/:id", navigationController.DeleteNavigationItem)
navigation.POST("/reorder", navigationController.ReorderNavigationItems)
navigation.POST("/seed", navigationController.SeedDefaultNavigation)
}
// Social links management (admin)
socialLinks := admin.Group("/social-links")
{
socialLinks.GET("", navigationController.GetAllSocialLinks)
socialLinks.POST("", navigationController.CreateSocialLink)
socialLinks.PUT("/:id", navigationController.UpdateSocialLink)
socialLinks.DELETE("/:id", navigationController.DeleteSocialLink)
socialLinks.POST("/reorder", navigationController.ReorderSocialLinks)
}
// Clothing management (admin)
clothing := admin.Group("/clothing")
{
clothing.GET("", clothingController.GetClothingAdmin)
clothing.GET("/:id", clothingController.GetClothingByID)
clothing.POST("", clothingController.CreateClothing)
clothing.PUT("/:id", clothingController.UpdateClothing)
clothing.DELETE("/:id", clothingController.DeleteClothing)
clothing.POST("/reorder", clothingController.UpdateClothingOrder)
}
// Polls management (admin)
polls := admin.Group("/polls")
{
polls.GET("", pollController.GetPolls)
polls.GET("/:id", pollController.GetPoll)
polls.POST("", pollController.CreatePoll)
polls.PUT("/:id", pollController.UpdatePoll)
polls.DELETE("/:id", pollController.DeletePoll)
polls.GET("/:id/stats", pollController.GetPollStats)
}
// Page element configurations management (admin)
pageElements := admin.Group("/page-elements")
{
pageElements.GET("", pageElementConfigController.GetAllPageElementConfigs)
pageElements.POST("", pageElementConfigController.CreateOrUpdatePageElementConfig)
pageElements.PUT("/:id", pageElementConfigController.UpdatePageElementConfig)
pageElements.DELETE("/:id", pageElementConfigController.DeletePageElementConfig)
pageElements.POST("/batch", pageElementConfigController.BatchUpdatePageElementConfigs)
}
}
}
// Register analytics routes (public tracking + admin endpoints)
RegisterAnalyticsRoutes(api, db)
// Umami analytics routes
api.GET("/umami/config", umamiController.GetUmamiConfig)
// Public setup endpoint (no auth required - called during initial setup)
api.POST("/umami/initialize-setup", umamiController.InitializeUmamiSetup)
umami := api.Group("/admin/umami")
umami.Use(middleware.JWTAuth(db))
umami.Use(middleware.RoleAuth("admin"))
{
umami.POST("/initialize", umamiController.InitializeUmami)
umami.GET("/stats", umamiController.GetStats)
umami.GET("/metrics/:type", umamiController.GetMetrics)
umami.GET("/pageviews", umamiController.GetPageviews)
}
// Register contact info routes (public + admin endpoints)
RegisterContactInfoRoutes(api, db)
// Public API routes
// Allow uploads publicly so initial setup can upload a club logo before an admin exists.
api.POST("/upload", middleware.RateLimit(30, time.Minute), baseController.UploadImage)
// Public scoreboard
api.GET("/scoreboard", scoreboardController.GetPublic)
api.GET("/scoreboard/colors/derive", scoreboardController.DeriveColors)
// Public core endpoints
api.GET("/settings", baseController.GetPublicSettings)
api.GET("/competition-aliases", baseController.GetPublicCompetitionAliases)
api.GET("/public/team-logo-overrides", baseController.GetPublicTeamLogoOverrides)
// Articles (public)
api.GET("/articles/featured", baseController.GetFeaturedArticles)
api.GET("/articles", baseController.GetArticles)
api.GET("/articles/:id", baseController.GetArticle)
api.GET("/articles/slug/:slug", baseController.GetArticleBySlug)
api.POST("/articles/:id/read", baseController.IncrementArticleRead)
api.POST("/articles/:id/track-view", baseController.TrackArticleView)
// Public read-only access to article-match link
api.GET("/articles/:id/match-link", baseController.GetArticleMatchLink)
// Public categories
api.GET("/categories", baseController.GetCategories)
// Public YouTube cached videos
api.GET("/youtube/videos", youtubeController.GetYouTubeVideos)
// Public About page
api.GET("/about", aboutController.GetPublicAboutPage)
api.GET("/teams", baseController.GetTeams)
api.GET("/teams/:id", baseController.GetTeam)
api.GET("/players", baseController.GetPlayers)
api.GET("/players/:id", baseController.GetPlayer)
api.GET("/sponsors", baseController.GetSponsors)
api.GET("/matches", baseController.GetMatches)
api.GET("/matches/history", baseController.GetMatchesHistory)
api.GET("/standings", baseController.GetStandings)
// Gallery (public): albums and photos
api.GET("/gallery/albums", galleryController.GetGalleryAlbums) // Get all albums
api.GET("/gallery/albums/:id", galleryController.GetGalleryAlbum) // Get single album with photos
api.GET("/gallery/proxy-image", galleryController.ProxyImage) // Proxy Zonerama images to avoid CORS
// Legacy Zonerama endpoints (keep for backwards compatibility)
api.GET("/zonerama/album", baseController.GetZoneramaAlbum)
// Alias to support hyphenated path used by some frontend calls
api.GET("/zonerama-album", baseController.GetZoneramaAlbum)
api.GET("/zonerama/picks", baseController.GetZoneramaPicks)
// Clothing (public) - active items only
api.GET("/clothing", clothingController.GetClothing)
// Polls (public)
api.GET("/polls", pollController.GetPolls)
api.GET("/polls/:id", pollController.GetPoll)
api.POST("/polls/:id/vote", middleware.RateLimit(10, time.Minute), pollController.Vote)
api.GET("/polls/:id/results", pollController.GetPollResults)
// Contact form and newsletter endpoints (public) rate limited to prevent abuse
api.POST("/contact", middleware.RateLimit(10, time.Minute), contactController.SubmitContactForm)
api.POST("/newsletter/subscribe", middleware.RateLimit(30, time.Minute), contactController.SubscribeToNewsletter)
api.POST("/newsletter/setup", middleware.RateLimit(30, time.Minute), contactController.SetupNewsletterPreferences)
api.POST("/newsletter/unsubscribe/:email", middleware.RateLimit(30, time.Minute), contactController.UnsubscribeFromNewsletter)
// Token-based management (no auth)
api.GET("/newsletter/preferences", contactController.GetNewsletterPreferencesByToken)
api.POST("/newsletter/preferences", contactController.SaveNewsletterPreferencesByToken)
api.POST("/newsletter/unsubscribe-token", contactController.UnsubscribeByToken)
}
// FACR scraper endpoints (integrated): /api/v1/facr/*
facr := api.Group("/facr")
{
facr.GET("/club/search", facrController.SearchClubs)
facr.GET("/club/:type/:id", facrController.GetClubInfo)
facr.GET("/club/:type/:id/table", facrController.GetClubTables)
}
}
// SetupRootRoutes registers endpoints at the root (no /api prefix)
func SetupRootRoutes(r *gin.Engine, db *gorm.DB) {
seoController := controllers.NewSEOController(db)
r.GET("/robots.txt", seoController.GetRobotsTXT)
r.GET("/sitemap.xml", seoController.GetSitemapXML)
}