package server import ( "database/sql" "fmt" "net/http" "strings" "github.com/a-h/templ/examples/integration-gin/gintemplrenderer" "github.com/gin-contrib/cors" "github.com/gin-gonic/gin" "github.com/haydenhargreaves/Potion/internal/app/service" domain "github.com/haydenhargreaves/Potion/internal/domain/server" "github.com/haydenhargreaves/Potion/internal/infrastructure/auth" "github.com/haydenhargreaves/Potion/internal/infrastructure/database/repository" _ "github.com/lib/pq" ) type Server struct { port int Router *gin.Engine config cors.Config DB *sql.DB deps domain.InjectedDependencies } // Init initializes the server with the provided port. CORS settings are defined here. // A pointer to a server object is returned which allows for method chaining. func Init(port int) *Server { server := &Server{ Router: gin.Default(), port: port, config: cors.DefaultConfig(), } // Some stuff for templ rendering htmlRenderer := server.Router.HTMLRender server.Router.HTMLRender = &gintemplrenderer.HTMLTemplRenderer{FallbackHtmlRenderer: htmlRenderer} // Disable proxy warnings server.Router.SetTrustedProxies(nil) // Setup the CORS settings and active them // server.config.AllowAllOrigins = true server.config.AllowOrigins = []string{"http://localhost:5173"} server.config.AllowMethods = []string{"GET", "POST", "PUT", "DELETE"} server.config.AllowHeaders = []string{"Origin", "Content-Type", "Authorization"} server.config.AllowCredentials = true server.Router.Use(cors.New(server.config)) return server } // Start starts the server on the port provided when the server was initialized func (s *Server) Start() { s.Router.Run(fmt.Sprintf(":%d", s.port)) } // TODO: (9/4/2025) Abstract these functions and cleanup. This is fucking messy... func (s *Server) Setup() *Server { // SETUP THE ENVIRONMENT CONFIGURATION cfg, err := domain.LoadEnvironment() if err != nil { panic(err.Error()) } if cfg == nil { panic("Environment configuration is nil, crashing.") } if cfg.Environment == "dev" { gin.SetMode(gin.DebugMode) } else if cfg.Environment == "prod" { gin.SetMode(gin.ReleaseMode) } else { gin.SetMode(gin.TestMode) } // SETUP GOOGLE AUTH var ( // NOTE: USING V2 NOW redirectUrl string = fmt.Sprintf("%s%s", cfg.Domain, domain.API_AUTH_CALLBACK_V2) clientId string = cfg.GoogleClientId clientSecret string = cfg.GoogleClientSecret scope []string = []string{ "https://www.googleapis.com/auth/userinfo.email", "https://www.googleapis.com/auth/userinfo.profile", } ) // Setup Google OAuth auth.NewGoogleConfig(redirectUrl, clientId, clientSecret, scope) // SETUP DATABASE db, err := sql.Open("postgres", cfg.DatabaseUrl) if err != nil { panic("Could not connect to database: " + err.Error()) } if err = db.Ping(); err != nil { panic("Error pinging database: " + err.Error()) } s.DB = db // SETUP JWT jwtSecret := []byte(cfg.JwtSecret) // Initialize and inject dependencies userRepo := repository.NewUserRepository(s.DB) recipeRepo := repository.NewRecipeRepository(s.DB) engagementRepo := repository.NewEngagementRepository(s.DB) userService := service.NewUserService(userRepo) authService := service.NewAuthService(userRepo, jwtSecret) recipeService := service.NewRecipeService(recipeRepo, engagementRepo) engagementService := service.NewEngagementService(engagementRepo, recipeRepo) s.deps = domain.InjectedDependencies{ UserService: userService, AuthService: authService, RecipeService: recipeService, EngagementService: engagementService, EnvironmentConfig: *cfg, } // Apply middleware s.Router.Use(RecoveryMiddleware()) // NOTE: No longer running on every connection! // s.Router.Use(JwtAuthMiddleWare(jwtSecret)) // Redirect index to home page: Update this as needed s.Router.GET("/", func(ctx *gin.Context) { ctx.Redirect(http.StatusSeeOther, domain.WEB_HOME) }) // Wrap all routes with a version router_v1 := s.Router.Group(domain.VERSION_1) router_v2 := s.Router.Group(domain.VERSION_2) // Domain specific routers router_web := router_v1.Group(domain.WEB) router_api := router_v1.Group(domain.API) router_state := router_web.Group("state") // Static routes router_web.Static("/static", "./web/static") // API router endpoints router_api.GET("/", func(ctx *gin.Context) { ctx.JSON(200, gin.H{"message": "Server is active."}) }) // WEB router endpoints router_web.GET("/", func(ctx *gin.Context) { ctx.Redirect(http.StatusSeeOther, domain.WEB_HOME) }) router_web.GET("/login", s.LoginPageHandler) router_web.GET("/home", s.HomePageHandler) router_web.GET("/favorites", s.FavoritesPageHandler) router_web.GET("/create", s.CreatePageHandler) router_web.GET("/profile", s.ProfilePageHandler) router_web.GET("/list", s.ListPageHandler) router_web.GET("/recipe/:id", s.RecipePageHandler) router_web.GET("/search", s.SearchPageHandler) router_web.GET("/404", s.NotFoundPageHandler) // WEB state endpoints router_state.POST("/tags", s.NewTagHandler) router_state.POST("/tags/delete", s.DeleteTagHandler) // Authentication router_api.GET("/auth/login", s.GoogleLoginHandler) router_api.GET("/auth/callback", s.GoogleCallbackHandler) router_api.GET("/auth/logout", s.LogoutHandler) // Recipe endpoints router_api.POST("/recipe", s.CreateRecipeHandler) router_api.POST("/recipe/search", s.SearchRecipesHandler) router_api.POST("/recipe/search/favorites", s.SearchRecipesFavoritesHandler) router_api.GET("/user/recipes", s.GetUserFavoriteRecipesHandler) router_api.GET("/user/favorites", s.GetUserFavoriteRecipesHandler) // Engagement endpoints router_api.POST("/engagement/view/:id", s.EngagementViewRecipeHandler) router_api.POST("/engagement/share/:id", s.EngagementShareRecipeHandler) router_api.POST("/engagement/favorite/:id", s.EngagementFavoriteRecipeHandler) router_api.POST("/engagement/make/:id", s.EngagementMakeRecipeHandler) // Catch un-routed URLS s.Router.NoRoute(func(ctx *gin.Context) { path := ctx.Request.URL.Path // TODO: Use constants for errors? if strings.HasPrefix(path, domain.VERSION_1+domain.API) { ctx.JSON(http.StatusNotFound, gin.H{ "status": http.StatusNotFound, "error": "API_NOT_FOUND", "message": "The request endpoint does not exist.", "path": path, }) return } ctx.Redirect(http.StatusSeeOther, domain.WEB_NOT_FOUND) }) // ---- VERSION 2 ROUTES ---- // router_api_v2 := router_v2.Group(domain.API) router_api_v2.GET("/recipe/of-the-week", s.GetRecipeOfTheWeekHandlerV2) router_api_v2.GET("/auth/login", s.GetGoogleAuthUrlHandlerV2) router_api_v2.GET("/auth/callback", s.GoogleCallbackHandlerV2) router_api_v2.GET("/auth/logout", s.LogoutHandlerV2) router_api_v2.GET("/user", JwtAuthMiddlewareV2([]byte(cfg.JwtSecret)), s.GetAuthenticatedUserHandlerV2) router_api_v2.GET("/user/recipes", JwtAuthMiddlewareV2([]byte(cfg.JwtSecret)), s.GetAuthenicatedUserRecipesV2) router_api_v2.GET("/user/favorites", JwtAuthMiddlewareV2([]byte(cfg.JwtSecret)), s.GetAuthenicatedUserFavoritesV2) router_api_v2.GET("/user/engagement", JwtAuthMiddlewareV2([]byte(cfg.JwtSecret)), s.GetAuthenicatedUserEngagementV2) router_api_v2.GET("/protected", JwtAuthMiddlewareV2([]byte(cfg.JwtSecret)), func(ctx *gin.Context) { ctx.JSON(http.StatusOK, gin.H{"msg": "YAY"}) }) return s }