diff --git a/internal/app/server/authentication.go b/internal/app/server/authentication.go new file mode 100644 index 0000000..1e03839 --- /dev/null +++ b/internal/app/server/authentication.go @@ -0,0 +1,27 @@ +package server + +import ( + "net/http" + + "github.com/gin-gonic/gin" + domain "github.com/haydenhargreaves/Potion/internal/domain/user" +) + +// AuthenticatedFunc is a function that handles authenticated requests +type AuthenticatedFunc func(ctx *gin.Context, user *domain.User) + +// withAuthenticatedUser is a helper to run a handler only if user is authenticated. Otherwise +// the function will return an error with a 401 status. +func (s *Server) withAuthenticatedUser(ctx *gin.Context, handler AuthenticatedFunc) { + user := s.deps.UserService.GetAuthenicatedUser(ctx) + if user == nil { + ctx.JSON(http.StatusUnauthorized, gin.H{ + "status": http.StatusUnauthorized, + "message": "[UNAUTHORIZED] Could not fetch authenticated user.", + }) + return + } + handler(ctx, user) +} + +// TODO: Create a function to use for methods that CAN use a user, but sometimes don't. diff --git a/internal/app/server/middleware_v2.go b/internal/app/server/middleware_v2.go index c661adb..57c9375 100644 --- a/internal/app/server/middleware_v2.go +++ b/internal/app/server/middleware_v2.go @@ -10,18 +10,18 @@ import ( ) // JwtAuthMiddlewareV2 is responsible to protecting routes. Anything that may go wrong -// will be returned via JSON with a 'message' field and a 401 error code. When this +// will be returned via JSON with a 'message' field and a 401 error code. When this // middleware is successful, it will set the 'userId' and 'userEmail' fields and pass -// to the next function in the chain. +// to the next function in the chain. // // Functions that are called after this can assume that those values defined are always // set. func JwtAuthMiddlewareV2(jwtSecretKey []byte) gin.HandlerFunc { - return func(ctx *gin.Context) { + return func(ctx *gin.Context) { tokenString, err := ctx.Cookie("jwt_token") if err != nil { ctx.JSON(http.StatusUnauthorized, gin.H{ - "status": http.StatusUnauthorized, + "status": http.StatusUnauthorized, "message": fmt.Sprintf("[UNAUTHORIZED] Failed to get token from cookie. %s", err.Error()), }) ctx.Abort() @@ -40,7 +40,7 @@ func JwtAuthMiddlewareV2(jwtSecretKey []byte) gin.HandlerFunc { // Error occurred when parsing if err != nil { ctx.JSON(http.StatusUnauthorized, gin.H{ - "status": http.StatusUnauthorized, + "status": http.StatusUnauthorized, "message": fmt.Sprintf("[UNAUTHORIZED] Error parsing cooking. %s", err.Error()), }) ctx.Abort() @@ -50,7 +50,7 @@ func JwtAuthMiddlewareV2(jwtSecretKey []byte) gin.HandlerFunc { // Token is invalid if !token.Valid { ctx.JSON(http.StatusUnauthorized, gin.H{ - "status": http.StatusUnauthorized, + "status": http.StatusUnauthorized, "message": "[UNAUTHORIZED] Token is invalid.", }) ctx.Abort() diff --git a/internal/app/server/server.go b/internal/app/server/server.go index bf96b00..94cdf6f 100644 --- a/internal/app/server/server.go +++ b/internal/app/server/server.go @@ -211,6 +211,9 @@ func (s *Server) Setup() *Server { 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.GET("/protected", JwtAuthMiddlewareV2([]byte(cfg.JwtSecret)), func(ctx *gin.Context) { ctx.JSON(http.StatusOK, gin.H{"msg": "YAY"}) }) diff --git a/internal/app/server/user_handler_v2.go b/internal/app/server/user_handler_v2.go index 4143ee1..ae9f11a 100644 --- a/internal/app/server/user_handler_v2.go +++ b/internal/app/server/user_handler_v2.go @@ -5,99 +5,110 @@ import ( "net/http" "github.com/gin-gonic/gin" + domain "github.com/haydenhargreaves/Potion/internal/domain/user" ) func (s *Server) GetAuthenticatedUserHandlerV2(ctx *gin.Context) { - user := s.deps.UserService.GetAuthenicatedUser(ctx) - if user == nil { - ctx.JSON(http.StatusUnauthorized, gin.H{ - "status": http.StatusUnauthorized, - "message": "[UNAUTHORIZED] Could not fetch authenticated user.", + s.withAuthenticatedUser(ctx, func(ctx *gin.Context, user *domain.User) { + ctx.JSON(http.StatusOK, gin.H{ + "status": http.StatusOK, + "message": "[OK] Successfully retrieved authenticated user.", + "user": user, }) - return - } - - ctx.JSON(http.StatusOK, gin.H{ - "status": http.StatusOK, - "message": "[OK] Successfully retrieved authenticated user.", - "user": user, }) } func (s *Server) GetAuthenicatedUserRecipesV2(ctx *gin.Context) { - user := s.deps.UserService.GetAuthenicatedUser(ctx) - if user == nil { - ctx.JSON(http.StatusUnauthorized, gin.H{ - "status": http.StatusUnauthorized, - "message": "[UNAUTHORIZED] Could not fetch authenticated user.", - }) - return - } + s.withAuthenticatedUser(ctx, func(ctx *gin.Context, user *domain.User) { + recipes, err := s.deps.RecipeService.GetUserRecipes(user.Id) + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{ + "status": http.StatusBadRequest, + "message": fmt.Sprintf("[FAILED] Could not fetch authenticated users's recipes. %s", err.Error()), + }) + return + } - recipes, err := s.deps.RecipeService.GetUserRecipes(user.Id) - if err != nil { - ctx.JSON(http.StatusBadRequest, gin.H{ - "status": http.StatusBadRequest, - "message": fmt.Sprintf("[FAILED] Could not fetch authenticated users's recipes. %s", err.Error()), + ctx.JSON(http.StatusOK, gin.H{ + "status": http.StatusOK, + "message": "[OK] Successfully retrieved authenticated user's recipes.", + "recipes": recipes, }) - return - } - - ctx.JSON(http.StatusOK, gin.H{ - "status": http.StatusOK, - "message": "[OK] Successfully retrieved authenticated user's recipes.", - "recipes": recipes, }) } func (s *Server) GetAuthenicatedUserFavoritesV2(ctx *gin.Context) { - user := s.deps.UserService.GetAuthenicatedUser(ctx) - if user == nil { - ctx.JSON(http.StatusUnauthorized, gin.H{ - "status": http.StatusUnauthorized, - "message": "[UNAUTHORIZED] Could not fetch authenticated user.", - }) - return - } + s.withAuthenticatedUser(ctx, func(ctx *gin.Context, user *domain.User) { + favorites, err := s.deps.RecipeService.GetUserFavoriteRecipes(user.Id) + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{ + "status": http.StatusBadRequest, + "message": fmt.Sprintf("[FAILED] Could not fetch authenticated users's favorites. %s", err.Error()), + }) + return + } - favorites, err := s.deps.RecipeService.GetUserFavoriteRecipes(user.Id) - if err != nil { - ctx.JSON(http.StatusBadRequest, gin.H{ - "status": http.StatusBadRequest, - "message": fmt.Sprintf("[FAILED] Could not fetch authenticated users's favorites. %s", err.Error()), + ctx.JSON(http.StatusOK, gin.H{ + "status": http.StatusOK, + "message": "[OK] Successfully retrieved authenticated user's favorites.", + "favorites": favorites, }) - return - } - - ctx.JSON(http.StatusOK, gin.H{ - "status": http.StatusOK, - "message": "[OK] Successfully retrieved authenticated user's favorites.", - "favorites": favorites, }) } func (s *Server) GetAuthenicatedUserEngagementV2(ctx *gin.Context) { - user := s.deps.UserService.GetAuthenicatedUser(ctx) - if user == nil { - ctx.JSON(http.StatusUnauthorized, gin.H{ - "status": http.StatusUnauthorized, - "message": "[UNAUTHORIZED] Could not fetch authenticated user.", - }) - return - } + s.withAuthenticatedUser(ctx, func(ctx *gin.Context, user *domain.User) { + engagement, err := s.deps.EngagementService.GetUserEngagement(user.Id, 6) + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{ + "status": http.StatusBadRequest, + "message": fmt.Sprintf("[FAILED] Failed to get authenticated user engagement. %s", err.Error()), + }) + return + } - engagement, err := s.deps.EngagementService.GetUserEngagement(user.Id, 6) - if err != nil { - ctx.JSON(http.StatusBadRequest, gin.H{ - "status": http.StatusBadRequest, - "message": fmt.Sprintf("[FAILED] Failed to get authenticated user engagement. %s", err.Error()), + ctx.JSON(http.StatusOK, gin.H{ + "status": http.StatusOK, + "message": "[OK] Successfully retrieved authenticated user engagement.", + "engagement": engagement, + }) + }) +} + +func (s *Server) GetAuthenicatedUserMadeRecipesV2(ctx *gin.Context) { + s.withAuthenticatedUser(ctx, func(ctx *gin.Context, user *domain.User) { + recipes, err := s.deps.RecipeService.GetUserMadeRecipes(user.Id, 6) + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{ + "status": http.StatusBadRequest, + "message": fmt.Sprintf("[FAILED] Failed to get authenticated user's made recipes. %s", err.Error()), + }) + return + } + + ctx.JSON(http.StatusOK, gin.H{ + "status": http.StatusOK, + "message": "[OK] Successfully retrieved authenticated user's made recipes.", + "recipes": recipes, + }) + }) +} + +func (s *Server) GetAuthenicatedUserViewedRecipesV2(ctx *gin.Context) { + s.withAuthenticatedUser(ctx, func(ctx *gin.Context, user *domain.User) { + recipes, err := s.deps.RecipeService.GetUserViewedRecipes(user.Id, 6) + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{ + "status": http.StatusBadRequest, + "message": fmt.Sprintf("[FAILED] Failed to get authenticated user's viewed recipes. %s", err.Error()), + }) + return + } + + ctx.JSON(http.StatusOK, gin.H{ + "status": http.StatusOK, + "message": "[OK] Successfully retrieved authenticated user's viewed recipes.", + "recipes": recipes, }) - return - } - - ctx.JSON(http.StatusOK, gin.H{ - "status": http.StatusOK, - "message": "[OK] Successfully retrieved authenticated user engagement.", - "engagement": engagement, }) } diff --git a/web/src/pages/Home.tsx b/web/src/pages/Home.tsx index 3430f7a..c9ba7e8 100644 --- a/web/src/pages/Home.tsx +++ b/web/src/pages/Home.tsx @@ -11,6 +11,7 @@ import RecipeSearchBar from "../components/inputs/RecipeSearchBar"; import { GetRecipeOfTheWeek } from "../services/RecipeService"; import { isApiError, type ApiError } from "../types/api/error"; import { AuthContext } from "../context/AuthContext"; +import { GetAuthenticatedUserMadeRecipes, GetAuthenticateUserViewedRecipes } from "../services/UserService"; export default function Home() { // Context @@ -24,104 +25,33 @@ export default function Home() { const [error, setError] = useState(""); - - // BUG: Remove these - useEffect(() => { - const recipe: Recipe = { - Id: 1, - Title: "Classic Pancakes", - Description: "Fluffy and delicious pancakes perfect for breakfast.", - Instructions: [ - "In a bowl, mix all the dry ingredients.", - "In another bowl, whisk the wet ingredients.", - "Combine both mixes until smooth.", - "Heat a non-stick skillet and pour batter.", - "Cook until bubbles form, flip and cook the other side.", - "Serve warm with syrup." - ], - Serves: 4, - Difficulty: 2, // scale 1-5 (example) - Duration: { - Total: 20, - Prep: 5, - Cook: 15 - }, - Category: "breakfast", - Ingredients: [ - { Name: "Flour", Quantity: "2 cups" }, - { Name: "Milk", Quantity: "1.5 cups" }, - { Name: "Egg", Quantity: "1 large" }, - { Name: "Baking Powder", Quantity: "2 teaspoons" }, - { Name: "Salt", Quantity: "0.5 teaspoon" }, - { Name: "Sugar", Quantity: "1 tablespoon" } - ], - UserId: 101, - Modified: new Date("2025-10-30T09:00:00"), - Created: new Date("2025-10-01T08:30:00"), - Tags: [ - { Id: 1, Name: "easy", Created: new Date("2025-01-01T12:00:00") }, - { Id: 2, Name: "quick", Created: new Date("2025-01-02T12:00:00") }, - { Id: 3, Name: "breakfast", Created: new Date("2025-01-03T12:00:00") } - ], - Favorite: true - }; - - const recipe2: Recipe = { - Id: 2, - Title: "Classic Pancakes", - Description: "Fluffy and delicious pancakes perfect for breakfast.", - Instructions: [ - "In a bowl, mix all the dry ingredients.", - "In another bowl, whisk the wet ingredients.", - "Combine both mixes until smooth.", - "Heat a non-stick skillet and pour batter.", - "Cook until bubbles form, flip and cook the other side.", - "Serve warm with syrup." - ], - Serves: 4, - Difficulty: 2, // scale 1-5 (example) - Duration: { - Total: 20, - Prep: 5, - Cook: 15 - }, - Category: "breakfast", - Ingredients: [ - { Name: "Flour", Quantity: "2 cups" }, - { Name: "Milk", Quantity: "1.5 cups" }, - { Name: "Egg", Quantity: "1 large" }, - { Name: "Baking Powder", Quantity: "2 teaspoons" }, - { Name: "Salt", Quantity: "0.5 teaspoon" }, - { Name: "Sugar", Quantity: "1 tablespoon" } - ], - UserId: 101, - Modified: new Date("2025-10-30T09:00:00"), - Created: new Date("2025-10-01T08:30:00"), - Tags: [ - { Id: 1, Name: "easy", Created: new Date("2025-01-01T12:00:00") }, - { Id: 2, Name: "quick", Created: new Date("2025-01-02T12:00:00") }, - { Id: 3, Name: "breakfast", Created: new Date("2025-01-03T12:00:00") } - ], - Favorite: true - }; - - setRecipeOfTheWeek(recipe); - - const recipes: Recipe[] = [recipe, recipe2]; - setMadeRecipes(recipes); - setViewedRecipes(recipes); - }, []); - // TODO: Fetch other items when needed // Fetch the recipe of the week useEffect(() => { async function fetch() { - const result: Recipe | ApiError = await GetRecipeOfTheWeek(); - if (isApiError(result)) { - setError(result.message); - return; + const result_rotw: Recipe | ApiError = await GetRecipeOfTheWeek(); + if (isApiError(result_rotw)) { + setError(result_rotw.message); + } else { + setRecipeOfTheWeek(result_rotw); } - setRecipeOfTheWeek(result); + + if (isLoggedIn) { + const result_made: Recipe[] | ApiError = await GetAuthenticatedUserMadeRecipes(); + if (isApiError(result_made)) { + setError(result_made.message); + } else { + setMadeRecipes(result_made); + } + + const result_viewed: Recipe[] | ApiError = await GetAuthenticateUserViewedRecipes(); + if (isApiError(result_viewed)) { + setError(result_viewed.message); + } else { + setViewedRecipes(result_viewed); + } + } + } void fetch(); }, []); diff --git a/web/src/services/UserService.ts b/web/src/services/UserService.ts index a24e682..fc58d4a 100644 --- a/web/src/services/UserService.ts +++ b/web/src/services/UserService.ts @@ -1,7 +1,7 @@ import axios from "axios"; import type { ApiError } from "../types/api/error"; import type { User } from "../types/user"; -import type { GetAuthenticateUserEngagementResponse, GetAuthenticateUserFavoritesResponse, GetAuthenticateUserRecipesResponse, GetAuthenticateUserResponse } from "../types/api/user"; +import type { GetAuthenticateUserEngagementResponse, GetAuthenticateUserFavoritesResponse, GetAuthenticateUserMadeRecipesResponse, GetAuthenticateUserRecipesResponse, GetAuthenticateUserResponse, GetAuthenticateUserViewedRecipesResponse } from "../types/api/user"; import type { Recipe } from "../types/recipe"; import type { Engagement } from "../types/engagement"; @@ -61,3 +61,31 @@ export async function GetAuthenticatedUserEngagement(): Promise { + const response = await axios.get("http://localhost:3000/v2/api/user/recipes/made"); + + if (response.data.status !== 200 || response.data.recipes === undefined) { + const err: ApiError = { + status: response.data.status, + message: response.data.message + }; + return err; + } + + return response.data.recipes; +} + +export async function GetAuthenticateUserViewedRecipes(): Promise { + const response = await axios.get("http://localhost:3000/v2/api/user/recipes/viewed"); + + if (response.data.status !== 200 || response.data.recipes === undefined) { + const err: ApiError = { + status: response.data.status, + message: response.data.message + }; + return err; + } + + return response.data.recipes; +} diff --git a/web/src/types/api/user.ts b/web/src/types/api/user.ts index 0a0db86..5f784cd 100644 --- a/web/src/types/api/user.ts +++ b/web/src/types/api/user.ts @@ -25,3 +25,15 @@ export interface GetAuthenticateUserEngagementResponse { message: string; engagement?: Engagement[]; } + +export interface GetAuthenticateUserMadeRecipesResponse { + status: number; + message: string; + recipes?: Recipe[]; +} + +export interface GetAuthenticateUserViewedRecipesResponse { + status: number; + message: string; + recipes?: Recipe[]; +}