diff --git a/cmd/web/main.go b/cmd/web/main.go index 8f3951d..56c437e 100644 --- a/cmd/web/main.go +++ b/cmd/web/main.go @@ -5,7 +5,7 @@ import "github.com/haydenhargreaves/Potion/internal/app/server" const PORT = 3000 func main() { - s := server.Init(PORT).ConfigureAuth().ConnectDatabase().Setup() + s := server.Init(PORT).Setup() defer s.DB.Close() s.Start() diff --git a/internal/app/handlers/auth_handler.go b/internal/app/handlers/auth_handler.go index 2da9eff..ebdf7bc 100644 --- a/internal/app/handlers/auth_handler.go +++ b/internal/app/handlers/auth_handler.go @@ -61,5 +61,7 @@ func GoogleCallback(ctx *gin.Context) { func Logout(ctx *gin.Context) { // TODO: Use same values as the GoogleCallback function ctx.SetCookie("jwt_token", "", -1, "/", "", false, true) // TODO: Update settings + ctx.SetCookie("search-filters", "", -1, "/", "", false, true) + ctx.Redirect(http.StatusSeeOther, domain.WEB_HOME) } diff --git a/internal/app/handlers/page_handler.go b/internal/app/handlers/page_handler.go index e9e0f7a..61dfe22 100644 --- a/internal/app/handlers/page_handler.go +++ b/internal/app/handlers/page_handler.go @@ -82,19 +82,6 @@ func FavoritesPage(ctx *gin.Context) { } } - // Else, get the user's favorites - // BUG: Depreciated, not displaying a list, using search to drive this page as well - // deps := ctx.MustGet("deps").(*domainServer.InjectedDependencies) - // userId := ctx.MustGet("userId").(int) - // recipes, err := deps.RecipeService.GetUserFavoriteRecipes(userId) - // if err != nil { - // ctx.JSON(http.StatusInternalServerError, gin.H{ - // "status": http.StatusInternalServerError, - // "message": fmt.Sprintf("Error getting favorites. %s\n", err.Error()), - // }) - // return - // } - ctx.HTML(http.StatusOK, "", layouts.AppLayout(title, page)) } @@ -200,24 +187,8 @@ func RecipePage(ctx *gin.Context) { return } - // Add engagement - // BUG: Don't want to do this here - // if loggedIn { - // if _, err = deps.EngagementService.UserViewRecipe(*userId, recipe.Id); err != nil { - // fmt.Printf("ERROR: %s\n", err.Error()) - // ctx.JSON(400, err.Error()) - // return - // } - // } else { - // if _, err = deps.EngagementService.ViewRecipe(recipe.Id); err != nil { - // fmt.Printf("ERROR: %s\n", err.Error()) - // ctx.JSON(400, err.Error()) - // return - // } - // } - title := "Potion - View Recipe" - page := pages.RecipePage(*recipe, *user, loggedIn) + page := pages.RecipePage(*recipe, *user, loggedIn, deps.EnvironmentConfig.Domain) ctx.HTML(http.StatusOK, "", layouts.AppLayout(title, page)) } diff --git a/internal/app/handlers/recipe_handler.go b/internal/app/handlers/recipe_handler.go index a37ffc2..681e36f 100644 --- a/internal/app/handlers/recipe_handler.go +++ b/internal/app/handlers/recipe_handler.go @@ -121,6 +121,10 @@ func SearchRecipesFavorites(ctx *gin.Context) { // TODO: Error here if they're not logged in? // Get user data (they should be logged in) + if !domain.IsLoggedIn(ctx) { + ctx.JSON(http.StatusOK, gin.H{"error": "User is not logged in. User will be nil."}) + } + userId := ctx.MustGet("userId").(int) recipes, err := deps.RecipeService.SearchRecipes(filters, &userId, true) diff --git a/internal/app/server/middleware.go b/internal/app/server/middleware.go index 4eb7d6e..a22acdd 100644 --- a/internal/app/server/middleware.go +++ b/internal/app/server/middleware.go @@ -2,6 +2,9 @@ package server import ( "fmt" + "log" + "net/http" + "runtime/debug" "github.com/gin-gonic/gin" "github.com/golang-jwt/jwt/v5" @@ -71,3 +74,45 @@ func JwtAuthMiddleWare(jwtSecretKey []byte) gin.HandlerFunc { ctx.Next() } } + +func RecoveryMiddleware() gin.HandlerFunc { + + return func(ctx *gin.Context) { + defer func() { + if r := recover(); r != nil { + // Log the panic with stack trace + err := fmt.Errorf("panic recovered: %v\n%s", r, debug.Stack()) + log.Printf("[PANIC RECOVERY] %s", err) + + ctx.JSON(http.StatusOK, gin.H{ + "status": http.StatusOK, + "error": "API_PANIC_RECOVERED", + "message": err.Error(), + }) + + // Determine the content type of the request for appropriate response + // acceptHeader := ctx.Request.Header.Get("Accept") + // + // // Customize the response based on the request type (e.g., HTML vs. JSON) + // if strings.Contains(acceptHeader, "text/html") { + // // For browser requests (HTML), redirect to an error page or render a template + // ctx.HTML(http.StatusInternalServerError, "error.html", gin.H{ + // "title": "Something went wrong", + // "message": "An unexpected error occurred. Please try again later.", + // }) + // } else if strings.Contains(acceptHeader, "application/json") || ctx.Request.Method == http.MethodPost { + // // For API requests (JSON), return a JSON error response + // ctx.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{ + // "error": "An internal server error occurred.", + // "message": "We're working to fix the problem. Please try again later.", + // // You might include a unique error ID here for tracking + // }) + // } else { + // // Fallback for other content types + // ctx.AbortWithStatus(http.StatusInternalServerError) + // } + } + }() + ctx.Next() + } +} diff --git a/internal/app/server/server.go b/internal/app/server/server.go index 9863e17..233c031 100644 --- a/internal/app/server/server.go +++ b/internal/app/server/server.go @@ -4,7 +4,6 @@ import ( "database/sql" "fmt" "net/http" - "os" "strings" "github.com/a-h/templ/examples/integration-gin/gintemplrenderer" @@ -15,7 +14,6 @@ import ( 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/joho/godotenv" _ "github.com/lib/pq" ) @@ -53,18 +51,26 @@ func Init(port int) *Server { return server } -func (s *Server) ConfigureAuth() *Server { - err := godotenv.Load(".env") +// 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)) +} + +func (s *Server) Setup() *Server { + // SETUP THE ENVIRONMENT CONFIGURATION + cfg, err := domain.LoadEnvironment() if err != nil { - fmt.Printf("No .env file found or error loading .env: %v. Relying on system environment variables.", err) + panic(err.Error()) + } + if cfg == nil { + panic("Environment configuration is nil, crashing.") } - redirect_domain := os.Getenv("DOMAIN") - + // SETUP GOOGLE AUTH var ( - redirectUrl string = fmt.Sprintf("%s%s", redirect_domain, domain.API_AUTH_CALLBACK) - clientId string = os.Getenv("GOOGLE_CLIENT_ID") - clientSecret string = os.Getenv("GOOGLE_CLIENT_SECRET") + redirectUrl string = fmt.Sprintf("%s%s", cfg.Domain, domain.API_AUTH_CALLBACK) + 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", @@ -74,18 +80,8 @@ func (s *Server) ConfigureAuth() *Server { // Setup Google OAuth auth.NewGoogleConfig(redirectUrl, clientId, clientSecret, scope) - return s -} - -func (s *Server) ConnectDatabase() *Server { - err := godotenv.Load(".env") - if err != nil { - fmt.Printf("No .env file found or error loading .env: %v. Relying on system environment variables.", err) - } - - var connUrl string = os.Getenv("DATABASE_URL") - - db, err := sql.Open("postgres", connUrl) + // SETUP DATABASE + db, err := sql.Open("postgres", cfg.DatabaseUrl) if err != nil { panic("Could not connect to database: " + err.Error()) } @@ -96,21 +92,8 @@ func (s *Server) ConnectDatabase() *Server { s.DB = db - return s -} - -// 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)) -} - -func (s *Server) Setup() *Server { - err := godotenv.Load(".env") - if err != nil { - fmt.Printf("No .env file found or error loading .env: %v. Relying on system environment variables.", err) - } - - jwtSecret := []byte(os.Getenv("JWT_SECRET")) + // SETUP JWT + jwtSecret := []byte(cfg.JwtSecret) // Initialize and inject dependencies userRepo := repository.NewUserRepository(s.DB) @@ -126,9 +109,11 @@ func (s *Server) Setup() *Server { AuthService: authService, RecipeService: recipeService, EngagementService: engagementService, + EnvironmentConfig: *cfg, } // Apply middleware + s.Router.Use(RecoveryMiddleware()) s.Router.Use(DepedencyInjectionMiddleware(deps)) s.Router.Use(JwtAuthMiddleWare(jwtSecret)) @@ -149,15 +134,8 @@ func (s *Server) Setup() *Server { // API router endpoints router_api.GET("/", func(ctx *gin.Context) { ctx.JSON(200, gin.H{"message": "Server is active."}) }) router_api.GET("/tmp", func(ctx *gin.Context) { - if !domain.IsLoggedIn(ctx) { - ctx.JSON(200, gin.H{"error": "User is not logged in. Please login to continue"}) - return - } - - userId := ctx.MustGet("userId").(int) - userEmail := ctx.MustGet("userEmail").(string) - - ctx.JSON(200, gin.H{"id": userId, "email": userEmail}) + deps := ctx.MustGet("deps").(*domain.InjectedDependencies) + ctx.JSON(200, gin.H{"config": deps.EnvironmentConfig}) }) // WEB router endpoints @@ -188,22 +166,6 @@ func (s *Server) Setup() *Server { router_api.GET("/user/recipes", handlers.GetUserRecipes) router_api.GET("/user/favorites", handlers.GetUserFavoriteRecipes) - router_api.GET("/user/temp", func(ctx *gin.Context) { - recipes, err := recipeService.GetUserMadeRecipes(3, 6) - - if err != nil { - ctx.JSON(http.StatusBadRequest, gin.H{ - "recipes": recipes, - "error": err.Error(), - }) - } else { - ctx.JSON(http.StatusBadRequest, gin.H{ - "recipes": recipes, - "error": "", - }) - } - }) - // Engagement endpoints router_api.POST("/engagement/view/:id", handlers.EngagementViewRecipe) router_api.POST("/engagement/share/:id", handlers.EngagementShareRecipe) @@ -217,7 +179,7 @@ func (s *Server) Setup() *Server { // TODO: Use constants for errors? if strings.HasPrefix(path, domain.VERSION+domain.API) { ctx.JSON(http.StatusNotFound, gin.H{ - "status": 404, + "status": http.StatusNotFound, "error": "API_NOT_FOUND", "message": "The request endpoint does not exist.", "path": path, diff --git a/internal/domain/server/server.go b/internal/domain/server/server.go index a8861aa..e2d2712 100644 --- a/internal/domain/server/server.go +++ b/internal/domain/server/server.go @@ -1,14 +1,30 @@ package domain import ( + "fmt" + "os" + "github.com/gin-gonic/gin" "github.com/golang-jwt/jwt/v5" domainAuth "github.com/haydenhargreaves/Potion/internal/domain/auth" domainEngagement "github.com/haydenhargreaves/Potion/internal/domain/engagement" domainRecipe "github.com/haydenhargreaves/Potion/internal/domain/recipe" domainUser "github.com/haydenhargreaves/Potion/internal/domain/user" + "github.com/joho/godotenv" ) +// EnvironmentConfig stores the configuration of the environment. Anything loaded from the .env +// or docker environment will be stored here and can be accessed from the InjectedDependencies +// struct, which this is attached to. +type EnvironmentConfig struct { + GoogleClientId string + GoogleClientSecret string + JwtSecret string + DatabaseUrl string + Environment string + Domain string +} + // InjectedDependencies is a collection of dependencies that are injected into the application. They // are stored in the context and can be accessed by handlers via the context. type InjectedDependencies struct { @@ -16,6 +32,7 @@ type InjectedDependencies struct { AuthService domainAuth.AuthService RecipeService domainRecipe.RecipeService EngagementService domainEngagement.EngagementService + EnvironmentConfig EnvironmentConfig } // JwtClaims is the data stored in the JSON web token. All that is needed is the users ID and their @@ -32,3 +49,71 @@ func IsLoggedIn(ctx *gin.Context) bool { _, email := ctx.Get("userEmail") return id && email } + +func LoadEnvironment() (*EnvironmentConfig, error) { + err := godotenv.Load(".env") + if err != nil { + fmt.Printf("No .env file found or error loading .env: %v. Relying on system environment variables.", err) + } + + env := os.Getenv("ENVIRONMENT") + if env == "" { + return nil, fmt.Errorf("ENVIRONMENT environment variable is required.") + } + + googleClientId := os.Getenv("GOOGLE_CLIENT_ID") + if googleClientId == "" { + return nil, fmt.Errorf("GOOGLE_CLIENT_ID environment variable is required.") + } + + googleClientSecret := os.Getenv("GOOGLE_CLIENT_SECRET") + if googleClientSecret == "" { + return nil, fmt.Errorf("GOOGLE_CLIENT_SECRET environment variable is required.") + } + + jwtSecret := os.Getenv("JWT_SECRET") + if jwtSecret == "" { + return nil, fmt.Errorf("JWT_SECRET environment variable is required.") + } + + var domain string + if env == "dev" { + domain = os.Getenv("DOMAIN_DEV") + if domain == "" { + return nil, fmt.Errorf("DOMAIN_DEV environment variable is required when ENVIRONMENT is 'dev'.") + } + } else if env == "prod" { + domain = os.Getenv("DOMAIN_PROD") + if domain == "" { + return nil, fmt.Errorf("DOMAIN_PROD environment variable is required when ENVIRONMENT is 'prod'.") + } + } else { + return nil, fmt.Errorf("ENVIRONMENT environment variable is required and must be 'dev' or 'prod'.") + } + + var dbUrl string + if env == "dev" { + dbUrl = os.Getenv("DATABASE_URL_DEV") + if dbUrl == "" { + return nil, fmt.Errorf("DATABASE_URL_DEV environment variable is required when ENVIRONMENT is 'dev'.") + } + } else if env == "prod" { + dbUrl = os.Getenv("DATABASE_URL_PROD") + if dbUrl == "" { + return nil, fmt.Errorf("DATABASE_URL_PROD environment variable is required when ENVIRONMENT is 'prod'.") + } + } else { + return nil, fmt.Errorf("ENVIRONMENT environment variable is required and must be 'dev' or 'prod'.") + } + + cfg := &EnvironmentConfig{ + GoogleClientId: googleClientId, + GoogleClientSecret: googleClientSecret, + JwtSecret: jwtSecret, + DatabaseUrl: dbUrl, + Environment: env, + Domain: domain, + } + + return cfg, nil +} diff --git a/internal/infrastructure/database/repository/user_repository.go b/internal/infrastructure/database/repository/user_repository.go index 1bf0b97..b593d3d 100644 --- a/internal/infrastructure/database/repository/user_repository.go +++ b/internal/infrastructure/database/repository/user_repository.go @@ -2,6 +2,7 @@ package repository import ( "database/sql" + "fmt" domain "github.com/haydenhargreaves/Potion/internal/domain/user" _ "github.com/lib/pq" @@ -35,6 +36,10 @@ func (r *UserRepository) CreateGoogleUser(googleUserInfo *domain.GoogleUserInfo, return domain.User{}, err } + if googleUserInfo == nil { + return domain.User{}, fmt.Errorf("Google user info provided was nil") + } + var user domain.User query := `INSERT INTO users (GoogleId, Name, Email, ImageUrl, GoogleRefreshToken) diff --git a/internal/templates/components/navbar_templ.go b/internal/templates/components/navbar_templ.go index 0ae98db..9a3c7c2 100644 --- a/internal/templates/components/navbar_templ.go +++ b/internal/templates/components/navbar_templ.go @@ -211,7 +211,7 @@ func Navbar(current string) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "\">
Potion
Potion
No results
- } else { -End of results
- } -No results
+ } else { +End of results
+ } +{ recipe.Description }
-{ recipe.Description }
+