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/haydenhargreaves/Potion/internal/infrastructure/logging" "github.com/haydenhargreaves/Potion/internal/infrastructure/logging/loggers" _ "github.com/lib/pq" ) type Server struct { port int Router *gin.Engine config cors.Config DB *sql.DB deps domain.InjectedDependencies logs []logging.Logger } // 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.New(), // Not default anymore, to allow for custom logger port: port, config: cors.DefaultConfig(), logs: []logging.Logger{}, } // Default logger which logs everything server.logs = append(server.logs, loggers.NewConsoleLogger(logging.LogLevelTrace)) // Some stuff for templ rendering // TODO: Remove this 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", "https://potion.gophernest.net"} 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() { logging.LogAll(s.logs, logging.LogLevelDebug, "Server started on :%d\n", s.port) s.Router.Run(fmt.Sprintf(":%d", s.port)) } // TODO: (9/4/2025) Abstract these functions and cleanup. This is fucking messy... // TODO: (1/26/2026) Abstract these functions and cleanup. This is fucking messy... still func (s *Server) Setup() *Server { // SETUP THE ENVIRONMENT CONFIGURATION cfg, err := domain.LoadEnvironment(s.logs) if err != nil { logging.LogAll(s.logs, logging.LogLevelFatal, err.Error()) panic(err.Error()) } if cfg == nil { logging.LogAll(s.logs, logging.LogLevelFatal, "Environment configuration is nil, crashing.") panic("Environment configuration is nil, crashing.") } // TODO: Using release on them all? Def need to clean this shitty environment up if cfg.Environment == "dev" { gin.SetMode(gin.ReleaseMode) } 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 // TODO: Implement environment here for logging file path := "./logs.log" fileLogger, cleanup, err := loggers.NewFileLogger(path, logging.LogLevelDebug) if err != nil { logging.LogAll(s.logs, logging.LogLevelWarning, "Failed to create file logger. %s\n", err.Error()) } else { s.logs = append(s.logs, fileLogger) defer cleanup() } databaseLogger, err := loggers.NewDatabaseLogger(s.DB, "logs", logging.LogLevelInfo) if err != nil { logging.LogAll(s.logs, logging.LogLevelWarning, "Failed to create database logger. %s\n", err.Error()) } else { s.logs = append(s.logs, databaseLogger) } // 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, s.logs) 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 // TODO: Review the recovery middleware s.Router.Use(gin.Recovery(), RecoveryMiddleware(s.logs), LoggingMiddleware(s.logs)) // 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/:id", JwtOptionalAuthMiddlewareV2([]byte(cfg.JwtSecret)), s.GetRecipeHandlerV2) router_api_v2.GET("/recipe/of-the-week", JwtOptionalAuthMiddlewareV2([]byte(cfg.JwtSecret)), s.GetRecipeOfTheWeekHandlerV2) router_api_v2.POST("/recipe/search", JwtOptionalAuthMiddlewareV2([]byte(cfg.JwtSecret)), s.SearchRecipeHandlerV2) router_api_v2.POST("/recipe", JwtAuthMiddlewareV2([]byte(cfg.JwtSecret)), s.CreateRecipeHandlerV2) 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/:id", s.GetUserV2) 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("/user/recipes/made", JwtAuthMiddlewareV2([]byte(cfg.JwtSecret)), s.GetAuthenicatedUserMadeRecipesV2) router_api_v2.GET("/user/recipes/viewed", JwtAuthMiddlewareV2([]byte(cfg.JwtSecret)), s.GetAuthenicatedUserViewedRecipesV2) router_api_v2.POST("/engagement/view/:id", JwtOptionalAuthMiddlewareV2([]byte(cfg.JwtSecret)), s.EngagementViewRecipeHandlerV2) router_api_v2.POST("/engagement/share/:id", JwtOptionalAuthMiddlewareV2([]byte(cfg.JwtSecret)), s.EngagementShareRecipeHandlerV2) router_api_v2.POST("/engagement/favorite/:id", JwtAuthMiddlewareV2([]byte(cfg.JwtSecret)), s.EngagementFavoriteRecipeHandlerV2) router_api_v2.POST("/engagement/make/:id", JwtAuthMiddlewareV2([]byte(cfg.JwtSecret)), s.EngagementMakeRecipeHandlerV2) if cfg.Environment == "dev" { s.debugDisplayRoutes() } return s } func (s *Server) debugDisplayRoutes() { for _, route := range s.Router.Routes() { format := "%-8s %s" logging.LogAll(s.logs, logging.LogLevelDebug, format, route.Method, route.Path) } }