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) contactController := controllers.NewContactController(db, emailService) passwordController := controllers.NewPasswordController(db, emailService) aiController := controllers.NewAIController(db) scoreboardController := controllers.NewScoreboardController(db) aboutController := controllers.NewAboutController(db) galleryController := controllers.NewGalleryController(db) 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) sweepstakesController := controllers.NewSweepstakesController(db, emailService) clothingController := controllers.NewClothingController(db) pageElementConfigController := controllers.NewPageElementConfigController(db) articleController := controllers.NewArticleController(db) myuibrixController := &controllers.MyUIbrixController{DB: db} editorPreviewController := controllers.NewEditorPreviewController(db) shortLinkController := controllers.NewShortLinkController(db) commentController := controllers.NewCommentController(db) engagementController := controllers.NewEngagementController(db, emailService) // API v1 group { // Health check api.GET("/health", baseController.HealthCheck) // CSRF token for cookie-based clients api.GET("/csrf-token", middleware.GetCSRFToken) // 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) api.GET("/clothing", clothingController.GetClothing) // Public shortlink creation for visitors (same-site only) api.POST("/shortlinks/public", middleware.RateLimit(30, time.Minute), shortLinkController.PublicCreateShortLink) // 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) } // Comments (public list) api.GET("/comments", commentController.GetComments) // Engagement (public + protected) api.GET("/engagement/rewards", engagementController.GetRewards) // Protected routes (require authentication) protected := api.Group("") protected.Use(middleware.JWTAuth(db)) // CSRF protect state-changing requests when relying on cookies (Bearer tokens are auto-exempt) protected.Use(middleware.CSRFProtection()) { // Sweepstakes (protected) protected.POST("/sweepstakes/:id/enter", middleware.RateLimit(30, time.Minute), sweepstakesController.Enter) protected.POST("/sweepstakes/:id/played", sweepstakesController.MarkVisualPlayed) protected.GET("/sweepstakes/my-winnings", sweepstakesController.MyWinnings) // Engagement leaderboard (auth) protected.GET("/engagement/leaderboard", engagementController.GetLeaderboard) // Engagement profile & actions protected.GET("/engagement/profile", engagementController.GetProfile) protected.PATCH("/engagement/profile", engagementController.PatchProfile) protected.PATCH("/engagement/avatar", engagementController.PatchAvatar) protected.POST("/engagement/redeem", middleware.RateLimit(5, time.Hour), engagementController.Redeem) protected.GET("/engagement/achievements", engagementController.GetAchievements) protected.GET("/engagement/transactions", engagementController.GetMyTransactions) // Engagement new actions protected.POST("/engagement/checkin", middleware.RateLimit(10, time.Minute), engagementController.Checkin) protected.POST("/engagement/article-read", middleware.RateLimit(60, time.Minute), engagementController.ArticleRead) // Comments (create/update/delete) protected.POST("/comments", middleware.RateLimit(20, time.Minute), commentController.CreateComment) protected.PUT("/comments/:id", commentController.UpdateComment) protected.DELETE("/comments/:id", commentController.DeleteComment) // Comment reactions and unban request protected.POST("/comments/:id/react", middleware.RateLimit(60, time.Minute), commentController.React) protected.DELETE("/comments/:id/react", commentController.Unreact) protected.POST("/comments/unban-request", middleware.RateLimit(5, time.Hour), commentController.CreateUnbanRequest) protected.POST("/comments/:id/report", middleware.RateLimit(10, time.Hour), commentController.ReportComment) // Editor preview endpoints (authenticated editors) editor := protected.Group("/editor") editor.Use(middleware.RoleAuth("editor")) { // Real-time preview state editor.GET("/preview/:session_id", editorPreviewController.GetPreviewState) editor.POST("/preview/:session_id", editorPreviewController.UpdatePreviewState) editor.POST("/preview/:session_id/apply", editorPreviewController.ApplyPreviewChanges) editor.DELETE("/preview/:session_id", editorPreviewController.DiscardPreviewChanges) // Validation and variants editor.POST("/preview/validate", editorPreviewController.ValidatePreviewConfig) editor.GET("/variants/:element_name", editorPreviewController.GetAvailableVariants) } // Newsletter preferences token for current user protected.GET("/newsletter/token/me", contactController.GetNewsletterTokenForUser) // User profile update protected.PUT("/me", authController.UpdateCurrentUser) // AI endpoints (protected) ai := protected.Group("/ai") { ai.POST("/blog/generate", aiController.GenerateBlog) ai.POST("/about/generate", aiController.GenerateAboutPage) ai.POST("/css/generate", aiController.GenerateCSS) ai.POST("/instagram/generate", aiController.GenerateInstagram) } // 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.Use(middleware.RoleAuth("editor")) { protectedEvents.POST("", eventController.CreateEvent) protectedEvents.PUT("/:id", eventController.UpdateEvent) protectedEvents.DELETE("/:id", eventController.DeleteEvent) } // Shortlinks (protected for editors) - create/list protectedShortlinks := protected.Group("/shortlinks") protectedShortlinks.Use(middleware.RoleAuth("editor")) { protectedShortlinks.POST("", shortLinkController.CreateShortLink) protectedShortlinks.GET("", shortLinkController.ListShortLinks) } // Articles (protected - accessible by editors and admins) articles := protected.Group("/articles") articles.Use(middleware.RoleAuth("editor")) { articles.POST("", articleController.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.Use(middleware.RoleAuth("admin")) { teams.POST("", baseController.CreateTeam) teams.PUT("/:id", baseController.UpdateTeam) teams.DELETE("/:id", baseController.DeleteTeam) } // Players (protected) players := protected.Group("/players") players.Use(middleware.RoleAuth("admin")) { players.POST("", baseController.CreatePlayer) players.PUT("/:id", baseController.UpdatePlayer) players.DELETE("/:id", baseController.DeletePlayer) } // Sponsors (protected CRUD) sponsors := protected.Group("/sponsors") sponsors.Use(middleware.RoleAuth("admin")) { sponsors.POST("", baseController.CreateSponsor) sponsors.PUT("/:id", baseController.UpdateSponsor) sponsors.DELETE("/:id", baseController.DeleteSponsor) } // Banners (protected CRUD) banners := protected.Group("/banners") banners.Use(middleware.RoleAuth("admin")) { banners.POST("", baseController.CreateBanner) banners.PUT("/:id", baseController.UpdateBanner) banners.DELETE("/:id", baseController.DeleteBanner) } // Admin routes (single consolidated group) admin := protected.Group("/admin") admin.Use(middleware.RoleAuth("admin")) { // Comments commentsAdmin := admin.Group("/comments") { commentsAdmin.GET("", commentController.AdminList) commentsAdmin.PATCH("/:id/status", commentController.AdminUpdateStatus) commentsAdmin.POST("/ban", commentController.AdminBanUser) commentsAdmin.GET("/bans", commentController.AdminListBans) commentsAdmin.POST("/bans/:id/lift", commentController.AdminLiftBan) commentsAdmin.GET("/unban-requests", commentController.AdminListUnban) commentsAdmin.POST("/unban-requests/:id/resolve", commentController.AdminResolveUnban) } // 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.POST("/newsletter/smtp-test", contactController.AdminSmtpTest) 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("/usage", filesController.GetStorageUsage) 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) polls.GET("/:id/votes", pollController.AdminListVotes) } // Engagement management (admin) engagement := admin.Group("/engagement") { engagement.GET("/rewards", engagementController.AdminListRewards) engagement.POST("/rewards", engagementController.AdminCreateReward) engagement.PUT("/rewards/:id", engagementController.AdminUpdateReward) engagement.DELETE("/rewards/:id", engagementController.AdminDeleteReward) engagement.GET("/redemptions", engagementController.AdminListRedemptions) engagement.PATCH("/redemptions/:id", engagementController.AdminUpdateRedemptionStatus) engagement.GET("/leaderboard", engagementController.AdminGetLeaderboard) engagement.GET("/transactions", engagementController.AdminListTransactions) engagement.POST("/adjust", engagementController.AdminAdjustPoints) engagement.GET("/profile/:user_id", engagementController.AdminGetUserProfile) } // 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) } // MyUIbrix optimization and validation endpoints (admin) myuibrix := admin.Group("/myuibrix") { myuibrix.POST("/validate", myuibrixController.ValidateElementConfig) myuibrix.POST("/validate-batch", myuibrixController.BatchValidateConfigs) myuibrix.GET("/preview", myuibrixController.GetElementPreview) myuibrix.GET("/optimize-layout", myuibrixController.OptimizePageLayout) } // Short links (admin) shortlinks := admin.Group("/shortlinks") { shortlinks.POST("", shortLinkController.CreateShortLink) shortlinks.GET("", shortLinkController.ListShortLinks) shortlinks.GET("/:id/stats", shortLinkController.GetShortLinkStats) } // Sweepstakes management (admin) sw := admin.Group("/sweepstakes") { sw.GET("", sweepstakesController.AdminList) sw.POST("", sweepstakesController.AdminCreate) sw.PUT("/:id", sweepstakesController.AdminUpdate) sw.DELETE("/:id", sweepstakesController.AdminDelete) sw.GET("/:id/entries", sweepstakesController.AdminEntries) sw.GET("/:id/winners", sweepstakesController.AdminWinners) sw.PATCH("/:id/winners/:winner_id", sweepstakesController.AdminUpdateWinner) sw.PATCH("/:id/winners/:winner_id/prize", sweepstakesController.AdminSetWinnerPrize) sw.GET("/:id/visual", sweepstakesController.AdminVisualData) // Prizes sw.GET("/:id/prizes", sweepstakesController.AdminListPrizes) sw.POST("/:id/prizes", sweepstakesController.AdminCreatePrize) sw.PUT("/:id/prizes/:prize_id", sweepstakesController.AdminUpdatePrize) sw.DELETE("/:id/prizes/:prize_id", sweepstakesController.AdminDeletePrize) sw.POST("/:id/prizes/reorder", sweepstakesController.AdminReorderPrizes) // Finalize (draw winners) sw.POST("/:id/finalize", sweepstakesController.AdminFinalize) } } // Protected routes end } // Public sweepstakes endpoints api.GET("/sweepstakes/current", sweepstakesController.GetCurrent) api.GET("/sweepstakes/:id/visual", sweepstakesController.PublicVisualData) } } // SetupRootRoutes registers endpoints at the root (no /api prefix) func SetupRootRoutes(r *gin.Engine, db *gorm.DB) { seoController := controllers.NewSEOController(db) shortLinkController := controllers.NewShortLinkController(db) r.GET("/robots.txt", seoController.GetRobotsTXT) r.GET("/sitemap.xml", seoController.GetSitemapXML) // Public short-link redirects and generic tracked redirect r.GET("/s/:code", shortLinkController.RedirectShort) r.GET("/r", shortLinkController.RedirectAndTrack) }