Compare commits
6 Commits
5db803d033
...
3177a4d089
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3177a4d089 | ||
|
|
1749a91bf9 | ||
|
|
25ea3fcfd7 | ||
|
|
7df879b04a | ||
|
|
34b0cc4199 | ||
|
|
281fd673d3 |
@ -1,47 +0,0 @@
|
||||
package handlers
|
||||
|
||||
// GoogleLogin directs the user to Googles select user login page. Once the user has selected an
|
||||
// account, they will be directed to the GoogleCallback handler where the main logic resides.
|
||||
//
|
||||
// DEPRECATED: As of September 4th, 2025.
|
||||
// func GoogleLogin(ctx *gin.Context) {
|
||||
// deps := ctx.MustGet("deps").(*domain.InjectedDependencies)
|
||||
// url := deps.AuthService.GetGoogleAuthUrl()
|
||||
//
|
||||
// ctx.Redirect(http.StatusSeeOther, url)
|
||||
// }
|
||||
|
||||
// GoogleCallback is the callback handler when the user successfully logs in with their Google
|
||||
// account. They will be directed here and a JWT is generated. This JWT is stored in the users
|
||||
// cookies and will be used by protected routes to validate their login status.
|
||||
//
|
||||
// We do not need to return all of this data, it is just for testing.
|
||||
//
|
||||
// DEPRECATED: As of September 4th, 2025.
|
||||
// func GoogleCallback(ctx *gin.Context) {
|
||||
// deps := ctx.MustGet("deps").(*domain.InjectedDependencies)
|
||||
//
|
||||
// var (
|
||||
// state string = ctx.Query("state")
|
||||
// code string = ctx.Query("code")
|
||||
// )
|
||||
//
|
||||
// if jwt, err := deps.AuthService.GoogleAuthSuccess(state, code); err != nil {
|
||||
// components.RenderErrorBanner(ctx, err.Error())
|
||||
// ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
// } else {
|
||||
// domain.SetCookie(ctx, "jwt_token", jwt, time.Hour*24*7)
|
||||
// ctx.Redirect(http.StatusSeeOther, "/")
|
||||
// }
|
||||
// }
|
||||
|
||||
// Logout removes the token from the user's browser. Effectively "logging them out." Routes that
|
||||
// require authentication will require the user to sign back in before accessing them again.
|
||||
// This route will direct the user back to the home page.
|
||||
//
|
||||
// DEPRECATED: As of September 4th, 2025.
|
||||
// func Logout(ctx *gin.Context) {
|
||||
// domain.SetCookie(ctx, "jwt_token", "", -1)
|
||||
// domain.SetCookie(ctx, "search-filters", "", -1)
|
||||
// ctx.Redirect(http.StatusSeeOther, domain.WEB_HOME)
|
||||
// }
|
||||
@ -1,142 +0,0 @@
|
||||
package handlers
|
||||
|
||||
// DEPRECATED: As of September 4th, 2025.
|
||||
// func EngagementViewRecipe(ctx *gin.Context) {
|
||||
// deps := ctx.MustGet("deps").(*domain.InjectedDependencies)
|
||||
// recipeId, _ := strconv.Atoi(ctx.Param("id"))
|
||||
//
|
||||
// // Ensure user is logged in with a valid account
|
||||
// user := deps.UserService.GetAuthenicatedUser(ctx)
|
||||
// if user == nil {
|
||||
// // Log (stale) user out
|
||||
// domain.SetCookie(ctx, "jwt_token", "", -1)
|
||||
// domain.SetCookie(ctx, "search-filters", "", -1)
|
||||
// }
|
||||
//
|
||||
// if !domain.IsLoggedIn(ctx) || user == nil {
|
||||
// if _, err := deps.EngagementService.ViewRecipe(recipeId); err != nil {
|
||||
// components.RenderErrorBanner(ctx, err.Error())
|
||||
// ctx.JSON(http.StatusInternalServerError, gin.H{
|
||||
// "status": http.StatusInternalServerError,
|
||||
// "message": err.Error(),
|
||||
// })
|
||||
// } else {
|
||||
// ctx.Header("HX-Redirect", fmt.Sprintf(domain.WEB_RECIPE, recipeId))
|
||||
// ctx.Status(http.StatusOK)
|
||||
// }
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// // We caught nil already, we can assume the user exists
|
||||
// if _, err := deps.EngagementService.UserViewRecipe(user.Id, recipeId); err != nil {
|
||||
// components.RenderErrorBanner(ctx, err.Error())
|
||||
// ctx.JSON(http.StatusInternalServerError, gin.H{
|
||||
// "status": http.StatusInternalServerError,
|
||||
// "message": err.Error(),
|
||||
// })
|
||||
// } else {
|
||||
// ctx.Header("HX-Redirect", fmt.Sprintf(domain.WEB_RECIPE, recipeId))
|
||||
// ctx.Status(http.StatusOK)
|
||||
// }
|
||||
// }
|
||||
|
||||
// DEPRECATED: As of September 4th, 2025.
|
||||
// func EngagementShareRecipe(ctx *gin.Context) {
|
||||
// deps := ctx.MustGet("deps").(*domain.InjectedDependencies)
|
||||
// recipeId, _ := strconv.Atoi(ctx.Param("id"))
|
||||
//
|
||||
// // Ensure user is logged in with a valid account
|
||||
// user := deps.UserService.GetAuthenicatedUser(ctx)
|
||||
// if user == nil {
|
||||
// // Log (stale) user out
|
||||
// domain.SetCookie(ctx, "jwt_token", "", -1)
|
||||
// domain.SetCookie(ctx, "search-filters", "", -1)
|
||||
// }
|
||||
//
|
||||
// if !domain.IsLoggedIn(ctx) || user == nil {
|
||||
// if _, err := deps.EngagementService.ShareRecipe(recipeId); err != nil {
|
||||
// components.RenderErrorBanner(ctx, err.Error())
|
||||
// ctx.JSON(http.StatusInternalServerError, gin.H{
|
||||
// "status": http.StatusInternalServerError,
|
||||
// "message": err.Error(),
|
||||
// })
|
||||
// } else {
|
||||
// ctx.Status(http.StatusNoContent)
|
||||
// }
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// if _, err := deps.EngagementService.UserShareRecipe(user.Id, recipeId); err != nil {
|
||||
// components.RenderErrorBanner(ctx, err.Error())
|
||||
// ctx.JSON(http.StatusInternalServerError, gin.H{
|
||||
// "status": http.StatusInternalServerError,
|
||||
// "message": err.Error(),
|
||||
// })
|
||||
// } else {
|
||||
// ctx.Status(http.StatusNoContent)
|
||||
// }
|
||||
// }
|
||||
|
||||
// DEPRECATED: As of September 4th, 2025.
|
||||
// func EngagementFavoriteRecipe(ctx *gin.Context) {
|
||||
// deps := ctx.MustGet("deps").(*domain.InjectedDependencies)
|
||||
//
|
||||
// // Ensure user is logged in with a valid account
|
||||
// user := deps.UserService.GetAuthenicatedUser(ctx)
|
||||
// if user == nil {
|
||||
// // Log (stale) user out
|
||||
// domain.SetCookie(ctx, "jwt_token", "", -1)
|
||||
// domain.SetCookie(ctx, "search-filters", "", -1)
|
||||
// }
|
||||
//
|
||||
// if !domain.IsLoggedIn(ctx) || user == nil {
|
||||
// ctx.Header("HX-Redirect", domain.WEB_LOGIN)
|
||||
// ctx.Status(http.StatusOK)
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// id := ctx.Param("id")
|
||||
// recipeId, _ := strconv.Atoi(id)
|
||||
//
|
||||
// if _, err := deps.EngagementService.UserFavoriteRecipe(user.Id, recipeId); err != nil {
|
||||
// components.RenderErrorBanner(ctx, fmt.Sprintf("Something went wrong. %s.", err.Error()))
|
||||
// ctx.JSON(http.StatusInternalServerError, gin.H{
|
||||
// "status": http.StatusInternalServerError,
|
||||
// "message": err.Error(),
|
||||
// })
|
||||
// } else {
|
||||
// ctx.Status(http.StatusNoContent)
|
||||
// }
|
||||
// }
|
||||
|
||||
// DEPRECATED: As of September 4th, 2025.
|
||||
// func EngagementMakeRecipe(ctx *gin.Context) {
|
||||
// deps := ctx.MustGet("deps").(*domain.InjectedDependencies)
|
||||
//
|
||||
// // Ensure user is logged in with a valid account
|
||||
// user := deps.UserService.GetAuthenicatedUser(ctx)
|
||||
// if user == nil {
|
||||
// // Log (stale) user out
|
||||
// domain.SetCookie(ctx, "jwt_token", "", -1)
|
||||
// domain.SetCookie(ctx, "search-filters", "", -1)
|
||||
// }
|
||||
//
|
||||
// if !domain.IsLoggedIn(ctx) || user == nil {
|
||||
// ctx.Header("HX-Redirect", domain.WEB_LOGIN)
|
||||
// ctx.Status(http.StatusOK)
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// id := ctx.Param("id")
|
||||
// recipeId, _ := strconv.Atoi(id)
|
||||
//
|
||||
// if _, err := deps.EngagementService.UserMakeRecipe(user.Id, recipeId); err != nil {
|
||||
// components.RenderErrorBanner(ctx, err.Error())
|
||||
// ctx.JSON(http.StatusInternalServerError, gin.H{
|
||||
// "status": http.StatusInternalServerError,
|
||||
// "message": err.Error(),
|
||||
// })
|
||||
// } else {
|
||||
// ctx.Status(http.StatusNoContent)
|
||||
// }
|
||||
// }
|
||||
@ -1,296 +0,0 @@
|
||||
package handlers
|
||||
|
||||
// DEPRECATED: As of September 4th, 2025.
|
||||
// func LoginPage(ctx *gin.Context) {
|
||||
// title := "Potion - Login"
|
||||
// page := pages.LoginPage()
|
||||
//
|
||||
// ctx.HTML(http.StatusOK, "", layouts.AppLayout(title, page))
|
||||
// }
|
||||
|
||||
// DEPRECATED: As of September 4th, 2025.
|
||||
// func HomePage(ctx *gin.Context) {
|
||||
// deps := ctx.MustGet("deps").(*domainServer.InjectedDependencies)
|
||||
//
|
||||
// loggedIn := domain.IsLoggedIn(ctx)
|
||||
//
|
||||
// // Ensure user is logged in with a valid account
|
||||
// if user := deps.UserService.GetAuthenicatedUser(ctx); user == nil {
|
||||
// // Log (stale) user out
|
||||
// domain.SetCookie(ctx, "jwt_token", "", -1)
|
||||
// domain.SetCookie(ctx, "search-filters", "", -1)
|
||||
// loggedIn = false
|
||||
// }
|
||||
//
|
||||
// var page templ.Component
|
||||
// if loggedIn {
|
||||
// userId := ctx.MustGet("userId").(int)
|
||||
// madeRecipes, err := deps.RecipeService.GetUserMadeRecipes(userId, 6)
|
||||
// if err != nil {
|
||||
// components.RenderErrorBanner(ctx, fmt.Sprintf("Error getting made recipes. %s\n", err.Error()))
|
||||
// ctx.JSON(http.StatusInternalServerError, gin.H{
|
||||
// "status": http.StatusInternalServerError,
|
||||
// "message": fmt.Sprintf("Error getting made recipes. %s\n", err.Error()),
|
||||
// })
|
||||
// return
|
||||
// }
|
||||
// viewedRecipes, err := deps.RecipeService.GetUserViewedRecipes(userId, 6)
|
||||
// if err != nil {
|
||||
// components.RenderErrorBanner(ctx, fmt.Sprintf("Error getting viewed recipes. %s\n", err.Error()))
|
||||
// ctx.JSON(http.StatusInternalServerError, gin.H{
|
||||
// "status": http.StatusInternalServerError,
|
||||
// "message": fmt.Sprintf("Error getting viewed recipes. %s\n", err.Error()),
|
||||
// })
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// // Get the recipe of the week
|
||||
// recipeOfTheWeek, err := deps.RecipeService.GetRecipeOfTheWeek(&userId)
|
||||
// if err != nil {
|
||||
// components.RenderErrorBanner(ctx, fmt.Sprintf("Error getting recipe of the week. %s\n", err.Error()))
|
||||
// ctx.JSON(http.StatusInternalServerError, gin.H{
|
||||
// "status": http.StatusInternalServerError,
|
||||
// "message": fmt.Sprintf("Error getting recipe of the week. %s\n", err.Error()),
|
||||
// })
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// if bytes, err := ctx.Cookie("search-filters"); err != nil {
|
||||
// fmt.Printf("ERROR: Failed to get search-filter cookie. %s\n", err.Error())
|
||||
// page = templates.HomePage(true, viewedRecipes, madeRecipes, recipeOfTheWeek, nil)
|
||||
// } else {
|
||||
// var filters domainRecipe.SearchFilters
|
||||
// if err := json.Unmarshal([]byte(bytes), &filters); err != nil {
|
||||
// fmt.Printf("ERROR: Failed to unmarshal search-filter cookie. %s\n", err.Error())
|
||||
// page = templates.HomePage(true, viewedRecipes, madeRecipes, recipeOfTheWeek, nil)
|
||||
// } else {
|
||||
// page = templates.HomePage(true, viewedRecipes, madeRecipes, recipeOfTheWeek, &filters)
|
||||
// }
|
||||
// }
|
||||
// } else {
|
||||
// // Get the recipe of the week
|
||||
// recipeOfTheWeek, err := deps.RecipeService.GetRecipeOfTheWeek(nil)
|
||||
// if err != nil {
|
||||
// components.RenderErrorBanner(ctx, fmt.Sprintf("Error getting recipe of the week. %s\n", err.Error()))
|
||||
// ctx.JSON(http.StatusInternalServerError, gin.H{
|
||||
// "status": http.StatusInternalServerError,
|
||||
// "message": fmt.Sprintf("Error getting recipe of the week. %s\n", err.Error()),
|
||||
// })
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// if bytes, err := ctx.Cookie("search-filters"); err != nil {
|
||||
// fmt.Printf("ERROR: Failed to get search-filter cookie. %s\n", err.Error())
|
||||
// page = templates.HomePage(false, nil, nil, recipeOfTheWeek, nil)
|
||||
// } else {
|
||||
// var filters domainRecipe.SearchFilters
|
||||
// if err := json.Unmarshal([]byte(bytes), &filters); err != nil {
|
||||
// fmt.Printf("ERROR: Failed to unmarshal search-filter cookie. %s\n", err.Error())
|
||||
// page = templates.HomePage(false, nil, nil, recipeOfTheWeek, nil)
|
||||
// } else {
|
||||
// page = templates.HomePage(false, nil, nil, recipeOfTheWeek, &filters)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// title := "Potion - Home"
|
||||
//
|
||||
// ctx.HTML(http.StatusOK, "", layouts.AppLayout(title, page))
|
||||
// }
|
||||
|
||||
// DEPRECATED: As of September 4th, 2025.
|
||||
// func FavoritesPage(ctx *gin.Context) {
|
||||
// // If not logged in, direct to the login page
|
||||
// if !domainServer.IsLoggedIn(ctx) {
|
||||
// ctx.Redirect(http.StatusSeeOther, domainServer.WEB_LOGIN)
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// title := "Potion - Favorites"
|
||||
// var page templ.Component
|
||||
//
|
||||
// // Get filters from cookies
|
||||
// if bytes, err := ctx.Cookie("search-filters"); err != nil {
|
||||
// fmt.Printf("ERROR: Failed to get search-filter cookie. %s\n", err.Error())
|
||||
// page = pages.FavoritesPage(nil)
|
||||
// } else {
|
||||
// var filters domainRecipe.SearchFilters
|
||||
// if err := json.Unmarshal([]byte(bytes), &filters); err != nil {
|
||||
// fmt.Printf("ERROR: Failed to unmarshal search-filter cookie. %s\n", err.Error())
|
||||
// page = pages.FavoritesPage(nil)
|
||||
// } else {
|
||||
// page = pages.FavoritesPage(&filters)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// ctx.HTML(http.StatusOK, "", layouts.AppLayout(title, page))
|
||||
// }
|
||||
//
|
||||
// func CreatePage(ctx *gin.Context) {
|
||||
// deps := ctx.MustGet("deps").(*domainServer.InjectedDependencies)
|
||||
//
|
||||
// // If not logged in, direct to the login page
|
||||
// if !domainServer.IsLoggedIn(ctx) {
|
||||
// ctx.Redirect(http.StatusSeeOther, domainServer.WEB_LOGIN)
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// // Ensure user is logged in with a valid account
|
||||
// if user := deps.UserService.GetAuthenicatedUser(ctx); user == nil {
|
||||
// // Log (stale) user out
|
||||
// domain.SetCookie(ctx, "jwt_token", "", -1)
|
||||
// domain.SetCookie(ctx, "search-filters", "", -1)
|
||||
//
|
||||
// ctx.Redirect(http.StatusSeeOther, domainServer.WEB_LOGIN)
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// title := "Potion - Create"
|
||||
// page := pages.CreatePage()
|
||||
//
|
||||
// ctx.HTML(http.StatusOK, "", layouts.AppLayout(title, page))
|
||||
// }
|
||||
|
||||
// DEPRECATED: As of September 4th, 2025.
|
||||
// func ProfilePage(ctx *gin.Context) {
|
||||
// // If not logged in, direct to the login page
|
||||
// if !domainServer.IsLoggedIn(ctx) {
|
||||
// ctx.Redirect(http.StatusSeeOther, domainServer.WEB_LOGIN)
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// // Else, get the user data
|
||||
// deps := ctx.MustGet("deps").(*domainServer.InjectedDependencies)
|
||||
// user := deps.UserService.GetAuthenicatedUser(ctx)
|
||||
// if user == nil {
|
||||
// // User is failing to be found, direct to the login page
|
||||
// ctx.Redirect(http.StatusSeeOther, domainServer.WEB_LOGIN)
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// recipes, err := deps.RecipeService.GetUserRecipes(user.Id)
|
||||
// if err != nil {
|
||||
// components.RenderErrorBanner(ctx, fmt.Sprintf("Error getting recipes. %s\n", err.Error()))
|
||||
// ctx.JSON(http.StatusInternalServerError, gin.H{
|
||||
// "status": http.StatusInternalServerError,
|
||||
// "message": fmt.Sprintf("Error getting recipes. %s\n", err.Error()),
|
||||
// })
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// favorites, err := deps.RecipeService.GetUserFavoriteRecipes(user.Id)
|
||||
// if err != nil {
|
||||
// components.RenderErrorBanner(ctx, fmt.Sprintf("Error getting favorite recipes. %s\n", err.Error()))
|
||||
// ctx.JSON(http.StatusInternalServerError, gin.H{
|
||||
// "status": http.StatusInternalServerError,
|
||||
// "message": fmt.Sprintf("Error getting favorite recipes. %s\n", err.Error()),
|
||||
// })
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// // Get the engagement data, not sure what will happen when errors occur
|
||||
// engagements, err := deps.EngagementService.GetUserEngagement(user.Id, 6)
|
||||
// if err != nil {
|
||||
// components.RenderErrorBanner(ctx, fmt.Sprintf("Error getting user engagements. %s\n", err.Error()))
|
||||
// ctx.JSON(http.StatusInternalServerError, gin.H{
|
||||
// "status": http.StatusInternalServerError,
|
||||
// "message": fmt.Sprintf("Error getting user engagements. %s\n", err.Error()),
|
||||
// })
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// title := "Potion - Profile"
|
||||
// page := pages.ProfilePage(*user, recipes, favorites, engagements)
|
||||
//
|
||||
// ctx.HTML(http.StatusOK, "", layouts.AppLayout(title, page))
|
||||
// }
|
||||
|
||||
// DEPRECATED: As of September 4th, 2025.
|
||||
// func ListPage(ctx *gin.Context) {
|
||||
// title := "Potion - Shopping List"
|
||||
// page := pages.ListPage()
|
||||
//
|
||||
// ctx.HTML(http.StatusOK, "", layouts.AppLayout(title, page))
|
||||
// }
|
||||
|
||||
// DEPRECATED: As of September 4th, 2025.
|
||||
// func RecipePage(ctx *gin.Context) {
|
||||
// // Call recipe service to get via ID
|
||||
// deps := ctx.MustGet("deps").(*domainServer.InjectedDependencies)
|
||||
// id := ctx.Param("id")
|
||||
//
|
||||
// // Parse ID
|
||||
// parsed, err := strconv.Atoi(id)
|
||||
// if err != nil {
|
||||
// components.RenderErrorBanner(ctx, fmt.Sprintf("ERROR: %s", err.Error()))
|
||||
// ctx.JSON(400, err.Error())
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// // Get signed in user, if they exist
|
||||
// var userId *int = nil
|
||||
// var loggedIn = domainServer.IsLoggedIn(ctx)
|
||||
//
|
||||
// // Ensure user is logged in with a valid account
|
||||
// if user := deps.UserService.GetAuthenicatedUser(ctx); user == nil {
|
||||
// // Log (stale) user out
|
||||
// domain.SetCookie(ctx, "jwt_token", "", -1)
|
||||
// domain.SetCookie(ctx, "search-filters", "", -1)
|
||||
// loggedIn = false
|
||||
// }
|
||||
//
|
||||
// if loggedIn {
|
||||
// storeId := ctx.MustGet("userId").(int)
|
||||
// userId = &storeId
|
||||
// }
|
||||
//
|
||||
// // Get recipe
|
||||
// recipe, err := deps.RecipeService.GetRecipe(parsed, userId)
|
||||
// if err != nil {
|
||||
// components.RenderErrorBanner(ctx, fmt.Sprintf("ERROR: %s", err.Error()))
|
||||
// ctx.JSON(400, err.Error())
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// // Get user (owner)
|
||||
// user, err := deps.UserService.GetUser(recipe.UserId)
|
||||
// if err != nil {
|
||||
// components.RenderErrorBanner(ctx, fmt.Sprintf("ERROR: %s", err.Error()))
|
||||
// ctx.JSON(400, err.Error())
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// title := "Potion - View Recipe"
|
||||
// page := pages.RecipePage(*recipe, *user, loggedIn, deps.EnvironmentConfig.Domain)
|
||||
//
|
||||
// ctx.HTML(http.StatusOK, "", layouts.AppLayout(title, page))
|
||||
// }
|
||||
//
|
||||
// func SearchPage(ctx *gin.Context) {
|
||||
// var page templ.Component
|
||||
// // Get filters from cookies
|
||||
// if bytes, err := ctx.Cookie("search-filters"); err != nil {
|
||||
// fmt.Printf("ERROR: Failed to get search-filter cookie. %s\n", err.Error())
|
||||
// page = pages.SearchPage(nil, false)
|
||||
// } else {
|
||||
// var filters domainRecipe.SearchFilters
|
||||
// if err := json.Unmarshal([]byte(bytes), &filters); err != nil {
|
||||
// fmt.Printf("ERROR: Failed to unmarshal search-filter cookie. %s\n", err.Error())
|
||||
// page = pages.SearchPage(nil, false)
|
||||
// } else {
|
||||
// page = pages.SearchPage(&filters, true)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// title := "Potion - Recipe Search"
|
||||
//
|
||||
// ctx.HTML(http.StatusOK, "", layouts.AppLayout(title, page))
|
||||
// }
|
||||
|
||||
// DEPRECATED: As of September 4th, 2025.
|
||||
// func NotFoundPage(ctx *gin.Context) {
|
||||
// title := "Potion - Not Found"
|
||||
// page := pages.NotFoundPage()
|
||||
//
|
||||
// ctx.HTML(http.StatusOK, "", layouts.AppLayout(title, page))
|
||||
// }
|
||||
@ -1,136 +0,0 @@
|
||||
package handlers
|
||||
|
||||
// DEPRECATED: As of September 4th, 2025.
|
||||
// const CREATE_ERROR_HTML = `
|
||||
// <p id="response" class="text-sm text-red-500 px-4 py-1 bg-red-100 rounded-full w-fit">
|
||||
// Uh oh! Something went wrong when creating your recipe. Please try again. %s
|
||||
// </p>
|
||||
// `
|
||||
|
||||
// DEPRECATED: As of September 4th, 2025.
|
||||
// func CreateRecipe(ctx *gin.Context) {
|
||||
// deps := ctx.MustGet("deps").(*domain.InjectedDependencies)
|
||||
//
|
||||
// recipe, err := deps.RecipeService.CreateRecipe(ctx)
|
||||
// if err != nil {
|
||||
// components.RenderErrorBanner(ctx, err.Error())
|
||||
// ctx.String(http.StatusOK, CREATE_ERROR_HTML, err.Error())
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// // Send HTMX redirection
|
||||
// url := fmt.Sprintf(domain.WEB_RECIPE, recipe.Id)
|
||||
// ctx.Header("HX-Redirect", url)
|
||||
// ctx.Status(http.StatusCreated)
|
||||
// }
|
||||
|
||||
// toBits converts an array of stringified numbers into a single summed value
|
||||
//
|
||||
// DEPRECATED: As of September 4th, 2025.
|
||||
// func toBits(arr []string) (bits int) {
|
||||
// for _, x := range arr {
|
||||
// num, _ := strconv.Atoi(x)
|
||||
// bits += num
|
||||
// }
|
||||
// return
|
||||
// }
|
||||
|
||||
// DEPRECATED: As of September 4th, 2025.
|
||||
// func SearchRecipes(ctx *gin.Context) {
|
||||
// deps := ctx.MustGet("deps").(*domain.InjectedDependencies)
|
||||
//
|
||||
// // create filters
|
||||
// filters := domainRecipe.SearchFilters{
|
||||
// Search: ctx.PostForm("search"), // string, search query for titles
|
||||
// MealType: toBits(ctx.PostFormArray("meal")),
|
||||
// Time: toBits(ctx.PostFormArray("time")),
|
||||
// Difficulty: toBits(ctx.PostFormArray("difficulty")),
|
||||
// ServingSize: toBits(ctx.PostFormArray("serving")),
|
||||
// }
|
||||
//
|
||||
// // Set the filters into the cookies, so they can be reloaded
|
||||
// if bytes, err := json.Marshal(filters); err == nil {
|
||||
// domain.SetCookie(ctx, "search-filters", string(bytes), time.Hour*24)
|
||||
// // ctx.SetCookie(
|
||||
// // "search-filters",
|
||||
// // string(bytes),
|
||||
// // int(time.Now().Add(24*time.Hour).Sub(time.Now()).Seconds()),
|
||||
// // "/",
|
||||
// // true,
|
||||
// // )
|
||||
// }
|
||||
//
|
||||
// redirect := ctx.PostForm("redirect")
|
||||
// if redirect == "true" {
|
||||
// ctx.Header("HX-Redirect", domain.WEB_SEARCH)
|
||||
// ctx.Status(http.StatusOK)
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// // Get user if logged in, so we can get favorite status
|
||||
// var userId *int = nil
|
||||
// if domain.IsLoggedIn(ctx) {
|
||||
// id := ctx.MustGet("userId").(int)
|
||||
// userId = &id
|
||||
// }
|
||||
//
|
||||
// // TODO: Not sure if we need to ensure the user is valid here
|
||||
//
|
||||
// // We don't care about favorite status, so use false
|
||||
// recipes, err := deps.RecipeService.SearchRecipes(filters, userId, false)
|
||||
// if err != nil {
|
||||
// components.RenderErrorBanner(ctx, err.Error())
|
||||
// ctx.JSON(http.StatusOK, gin.H{"error": err.Error()})
|
||||
// }
|
||||
//
|
||||
// // Render content as the response
|
||||
// ctx.Status(200)
|
||||
// templates.ResultList(recipes).Render(ctx.Request.Context(), ctx.Writer)
|
||||
// }
|
||||
|
||||
// DEPRECATED: As of September 4th, 2025.
|
||||
// func SearchRecipesFavorites(ctx *gin.Context) {
|
||||
// deps := ctx.MustGet("deps").(*domain.InjectedDependencies)
|
||||
//
|
||||
// // create filters
|
||||
// filters := domainRecipe.SearchFilters{
|
||||
// Search: ctx.PostForm("search"), // string, search query for titles
|
||||
// MealType: toBits(ctx.PostFormArray("meal")),
|
||||
// Time: toBits(ctx.PostFormArray("time")),
|
||||
// Difficulty: toBits(ctx.PostFormArray("difficulty")),
|
||||
// ServingSize: toBits(ctx.PostFormArray("serving")),
|
||||
// }
|
||||
//
|
||||
// // Set the filters into the cookies, so they can be reloaded
|
||||
// if bytes, err := json.Marshal(filters); err == nil {
|
||||
// domain.SetCookie(ctx, "search-filters", string(bytes), time.Hour*24)
|
||||
// // ctx.SetCookie(
|
||||
// // "search-filters",
|
||||
// // string(bytes),
|
||||
// // int(time.Now().Add(24*time.Hour).Sub(time.Now()).Seconds()),
|
||||
// // "/",
|
||||
// // "", // TODO: Need an actual domain
|
||||
// // false, // TODO: True in prod
|
||||
// // true,
|
||||
// // )
|
||||
// }
|
||||
//
|
||||
// // TODO: Error here if they're not logged in?
|
||||
// // Get user data (they should be logged in)
|
||||
// if !domain.IsLoggedIn(ctx) {
|
||||
// components.RenderErrorBanner(ctx, "User is not logged in. User will be nil.")
|
||||
// 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)
|
||||
// if err != nil {
|
||||
// components.RenderErrorBanner(ctx, err.Error())
|
||||
// ctx.JSON(http.StatusOK, gin.H{"error": err.Error()})
|
||||
// }
|
||||
//
|
||||
// // Render content as the response
|
||||
// ctx.Status(200)
|
||||
// templates.FavoriteList(recipes).Render(ctx.Request.Context(), ctx.Writer)
|
||||
// }
|
||||
@ -1,71 +0,0 @@
|
||||
package handlers
|
||||
|
||||
// DEPRECATED: As of September 4th, 2025.
|
||||
// const TAG_HTML = `
|
||||
// <li
|
||||
// hx-post="%s"
|
||||
// hx-trigger="click"
|
||||
// hx-target="#tag-list"
|
||||
// hx-swap="innerHTML"
|
||||
// hx-include="#tag-list"
|
||||
// hx-vals='{"target": "%s"}'
|
||||
// class="flex text-xs items-center bg-blue-100 w-fit px-2 py-1 rounded-full gap-x-1 select-none cursor-pointer hover:bg-blue-200 duration-300">
|
||||
// × %s
|
||||
// </li>
|
||||
// `
|
||||
|
||||
// DEPRECATED: As of September 4th, 2025.
|
||||
// const TAG_LIST_HTML = `
|
||||
// <input
|
||||
// hx-swap-oob="outerHTML"
|
||||
// type="hidden"
|
||||
// name="tags"
|
||||
// id="tags"
|
||||
// value="%s"
|
||||
// />
|
||||
// `
|
||||
|
||||
// DEPRECATED: As of September 4th, 2025.
|
||||
// func NewTag(ctx *gin.Context) {
|
||||
// tag := strings.ToLower(ctx.PostForm("tag"))
|
||||
// tags := strings.Split(ctx.PostForm("tags"), ",")
|
||||
//
|
||||
// tags = append([]string{tag}, tags...)
|
||||
//
|
||||
// var html string
|
||||
// var cleaned_tags []string
|
||||
// for _, tag := range tags {
|
||||
// if tag != "" {
|
||||
// html += fmt.Sprintf(TAG_HTML, domain.STATE_TAGS_DELETE, tag, tag)
|
||||
//
|
||||
// // Ensure that the list provided does not contain blank spaces.
|
||||
// // This is another measure to ensure this state is bulletproof.
|
||||
// cleaned_tags = append(cleaned_tags, tag)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Execute OOB swap for the tags
|
||||
// html += fmt.Sprintf(TAG_LIST_HTML, strings.Join(cleaned_tags, ","))
|
||||
//
|
||||
// ctx.String(http.StatusOK, html)
|
||||
// }
|
||||
|
||||
// DEPRECATED: As of September 4th, 2025.
|
||||
// func DeleteTag(ctx *gin.Context) {
|
||||
// tags := strings.Split(ctx.PostForm("tags"), ",")
|
||||
// target := ctx.PostForm("target")
|
||||
//
|
||||
// var html string
|
||||
// var new_tags []string
|
||||
// for _, tag := range tags {
|
||||
// if tag != target && tag != "" {
|
||||
// html += fmt.Sprintf(TAG_HTML, domain.STATE_TAGS_DELETE ,tag, tag)
|
||||
// new_tags = append(new_tags, tag)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Execute OOB swap for the tags
|
||||
// html += fmt.Sprintf(TAG_LIST_HTML, strings.Join(new_tags, ","))
|
||||
//
|
||||
// ctx.String(http.StatusOK, html)
|
||||
// }
|
||||
@ -1,83 +0,0 @@
|
||||
package handlers
|
||||
|
||||
// DEPRECATED: As of September 4th, 2025.
|
||||
// func GetUserRecipes(ctx *gin.Context) {
|
||||
// deps := ctx.MustGet("deps").(*domain.InjectedDependencies)
|
||||
//
|
||||
// // Ensure user is logged in with a valid account
|
||||
// user := deps.UserService.GetAuthenicatedUser(ctx)
|
||||
// if user == nil {
|
||||
// // Log (stale) user out
|
||||
// domain.SetCookie(ctx, "jwt_token", "", -1)
|
||||
// domain.SetCookie(ctx, "search-filters", "", -1)
|
||||
// }
|
||||
//
|
||||
// // Ensure logged in
|
||||
// if !domain.IsLoggedIn(ctx) || user == nil {
|
||||
// components.RenderErrorBanner(ctx, "User is not authorized to access this endpoint. Please login to continue.")
|
||||
// ctx.JSON(http.StatusUnauthorized, gin.H{
|
||||
// "status": http.StatusUnauthorized,
|
||||
// "message": "User is not authorized to access this endpoint. Please login to continue.",
|
||||
// "recipes": nil,
|
||||
// })
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// recipes, err := deps.RecipeService.GetUserRecipes(user.Id)
|
||||
// if err != nil {
|
||||
// components.RenderErrorBanner(ctx, fmt.Sprintf("Could not get user recipes. %s", err.Error()))
|
||||
// ctx.JSON(http.StatusBadRequest, gin.H{
|
||||
// "status": http.StatusBadRequest,
|
||||
// "message": fmt.Sprintf("Could not get user recipes. %s", err.Error()),
|
||||
// "recipes": nil,
|
||||
// })
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// ctx.JSON(http.StatusOK, gin.H{
|
||||
// "status": http.StatusOK,
|
||||
// "message": "User recipes successfully retrieved.",
|
||||
// "recipes": recipes,
|
||||
// })
|
||||
// }
|
||||
|
||||
// DEPRECATED: As of September 4th, 2025.
|
||||
// func GetUserFavoriteRecipes(ctx *gin.Context) {
|
||||
// deps := ctx.MustGet("deps").(*domain.InjectedDependencies)
|
||||
//
|
||||
// // Ensure user is logged in with a valid account
|
||||
// user := deps.UserService.GetAuthenicatedUser(ctx)
|
||||
// if user == nil {
|
||||
// // Log (stale) user out
|
||||
// domain.SetCookie(ctx, "jwt_token", "", -1)
|
||||
// domain.SetCookie(ctx, "search-filters", "", -1)
|
||||
// }
|
||||
//
|
||||
// // Ensure logged in
|
||||
// if !domain.IsLoggedIn(ctx) || user == nil {
|
||||
// components.RenderErrorBanner(ctx, "User is not authorized to access this endpoint. Please login to continue.")
|
||||
// ctx.JSON(http.StatusUnauthorized, gin.H{
|
||||
// "status": http.StatusUnauthorized,
|
||||
// "message": "User is not authorized to access this endpoint. Please login to continue.",
|
||||
// "recipes": nil,
|
||||
// })
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// recipes, err := deps.RecipeService.GetUserFavoriteRecipes(user.Id)
|
||||
// if err != nil {
|
||||
// components.RenderErrorBanner(ctx, fmt.Sprintf("Could not get favorite recipes. %s", err.Error()))
|
||||
// ctx.JSON(http.StatusBadRequest, gin.H{
|
||||
// "status": http.StatusBadRequest,
|
||||
// "message": fmt.Sprintf("Could not get favorite recipes. %s", err.Error()),
|
||||
// "recipes": nil,
|
||||
// })
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// ctx.JSON(http.StatusOK, gin.H{
|
||||
// "status": http.StatusOK,
|
||||
// "message": "User recipes successfully retrieved.",
|
||||
// "recipes": recipes,
|
||||
// })
|
||||
// }
|
||||
47
internal/app/server/auth_handler_v2.go
Normal file
47
internal/app/server/auth_handler_v2.go
Normal file
@ -0,0 +1,47 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// GetGoogleAuthUrlHandlerV2 fetches a Google authentication URl and returns it.
|
||||
// This function is atomic and cannot fail.
|
||||
func (s *Server) GetGoogleAuthUrlHandlerV2(ctx *gin.Context) {
|
||||
url := s.deps.AuthService.GetGoogleAuthUrl()
|
||||
ctx.JSON(http.StatusOK, gin.H{
|
||||
"status": http.StatusOK,
|
||||
"message": "[OK] Successfully retrieved Google auth URL.",
|
||||
"url": url,
|
||||
})
|
||||
}
|
||||
|
||||
// GoogleCallbackHandlerV2 reads the data from the Google redirection and uses it
|
||||
// to generate a JWT which is sent back to the UI via a URL query parameter. If an
|
||||
// error occurs the user will be directed to the login page with an error query param.
|
||||
func (s *Server) GoogleCallbackHandlerV2(ctx *gin.Context) {
|
||||
var (
|
||||
state string = ctx.Query("state")
|
||||
code string = ctx.Query("code")
|
||||
)
|
||||
|
||||
if jwt, err := s.deps.AuthService.GoogleAuthSuccess(state, code); err != nil {
|
||||
url := fmt.Sprintf("http://localhost:5173/v2/web/login?error=%s", url.QueryEscape(err.Error()))
|
||||
ctx.Redirect(http.StatusSeeOther, url)
|
||||
} else {
|
||||
url := "http://localhost:5173/v2/web/home"
|
||||
s.SetCookie(ctx, "jwt_token", jwt, time.Hour*24*7)
|
||||
ctx.Redirect(http.StatusSeeOther, url)
|
||||
}
|
||||
}
|
||||
|
||||
// BUG: This is not working, not yet sure why
|
||||
func (s *Server) LogoutHandlerV2(ctx *gin.Context) {
|
||||
s.SetCookie(ctx, "jwt_token", "", -1)
|
||||
// s.SetCookie(ctx, "search-filters", "", -1) // TODO: This was copied, might function differently now
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
@ -18,10 +18,10 @@ import (
|
||||
func (s *Server) SetCookie(ctx *gin.Context, name, value string, duration time.Duration) {
|
||||
var (
|
||||
path string = "/"
|
||||
httpOnly bool = true
|
||||
httpOnly bool = false // NOTE: Should use false so React can see it!
|
||||
maxAge int
|
||||
secure bool
|
||||
domain string
|
||||
secure bool = false
|
||||
domain string = ""
|
||||
)
|
||||
|
||||
if duration < 0 {
|
||||
@ -32,7 +32,7 @@ func (s *Server) SetCookie(ctx *gin.Context, name, value string, duration time.D
|
||||
maxAge = 0
|
||||
} else {
|
||||
// Normal calculation
|
||||
maxAge = int(time.Now().Add(duration).Sub(time.Now()).Seconds())
|
||||
maxAge = int(time.Until(time.Now().Add(duration)).Seconds())
|
||||
}
|
||||
|
||||
if s.deps.EnvironmentConfig.Environment == "prod" {
|
||||
@ -41,12 +41,8 @@ func (s *Server) SetCookie(ctx *gin.Context, name, value string, duration time.D
|
||||
|
||||
} else if s.deps.EnvironmentConfig.Environment == "dev" {
|
||||
secure = false
|
||||
domain = s.deps.EnvironmentConfig.Domain
|
||||
|
||||
} else {
|
||||
// Defaults
|
||||
secure = false
|
||||
domain = ""
|
||||
// domain = s.deps.EnvironmentConfig.Domain
|
||||
domain = "localhost"
|
||||
}
|
||||
|
||||
ctx.SetCookie(name, value, maxAge, path, domain, secure, httpOnly)
|
||||
|
||||
66
internal/app/server/middleware_v2.go
Normal file
66
internal/app/server/middleware_v2.go
Normal file
@ -0,0 +1,66 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
domain "github.com/haydenhargreaves/Potion/internal/domain/server"
|
||||
)
|
||||
|
||||
// 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
|
||||
// middleware is successful, it will set the 'userId' and 'userEmail' fields and pass
|
||||
// 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) {
|
||||
tokenString, err := ctx.Cookie("jwt_token")
|
||||
fmt.Println(tokenString)
|
||||
if err != nil {
|
||||
ctx.JSON(http.StatusUnauthorized, gin.H{
|
||||
"status": http.StatusUnauthorized,
|
||||
"message": fmt.Sprintf("[UNAUTHORIZED] Failed to get token from cookie. %s", err.Error()),
|
||||
})
|
||||
ctx.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
claims := &domain.JwtClaims{}
|
||||
|
||||
token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) {
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
||||
}
|
||||
return jwtSecretKey, nil
|
||||
})
|
||||
|
||||
// Error occurred when parsing
|
||||
if err != nil {
|
||||
ctx.JSON(http.StatusUnauthorized, gin.H{
|
||||
"status": http.StatusUnauthorized,
|
||||
"message": fmt.Sprintf("[UNAUTHORIZED] Error parsing cooking. %s", err.Error()),
|
||||
})
|
||||
ctx.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// Token is invalid
|
||||
if !token.Valid {
|
||||
ctx.JSON(http.StatusUnauthorized, gin.H{
|
||||
"status": http.StatusUnauthorized,
|
||||
"message": "[UNAUTHORIZED] Token is invalid.",
|
||||
})
|
||||
ctx.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// Found: Set the values
|
||||
ctx.Set("userId", claims.UserId)
|
||||
ctx.Set("userEmail", claims.Email)
|
||||
ctx.Next()
|
||||
}
|
||||
}
|
||||
35
internal/app/server/recipe_handler_v2.go
Normal file
35
internal/app/server/recipe_handler_v2.go
Normal file
@ -0,0 +1,35 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
|
||||
// GetRecipeOfTheWeekHandler fetchs the current recipe of the week and returns it.
|
||||
// If an error occurs, it will be returned and a recipe will not be returned.
|
||||
//
|
||||
// Until auth is reimplemented, there is no way to determine what user is making the
|
||||
// call.
|
||||
func (s *Server) GetRecipeOfTheWeekHandlerV2(ctx *gin.Context) {
|
||||
// BUG: This needs to be different, not hard coded
|
||||
userId := 1
|
||||
|
||||
recipe, err := s.deps.RecipeService.GetRecipeOfTheWeek(&userId)
|
||||
|
||||
if err != nil {
|
||||
ctx.JSON(http.StatusBadRequest, gin.H{
|
||||
"status": http.StatusBadRequest,
|
||||
"message": fmt.Sprintf("[ERROR] Failed to get recipe of the week. %s", err.Error()),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, gin.H{
|
||||
"status": http.StatusOK,
|
||||
"message": "[OK] Successfully retrieved recipe of the week.",
|
||||
"recipe": recipe,
|
||||
})
|
||||
}
|
||||
@ -42,7 +42,11 @@ func Init(port int) *Server {
|
||||
server.Router.SetTrustedProxies(nil)
|
||||
|
||||
// Setup the CORS settings and active them
|
||||
server.config.AllowAllOrigins = true
|
||||
// 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
|
||||
@ -74,7 +78,8 @@ func (s *Server) Setup() *Server {
|
||||
|
||||
// SETUP GOOGLE AUTH
|
||||
var (
|
||||
redirectUrl string = fmt.Sprintf("%s%s", cfg.Domain, domain.API_AUTH_CALLBACK)
|
||||
// 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{
|
||||
@ -120,13 +125,15 @@ func (s *Server) Setup() *Server {
|
||||
|
||||
// Apply middleware
|
||||
s.Router.Use(RecoveryMiddleware())
|
||||
s.Router.Use(JwtAuthMiddleWare(jwtSecret))
|
||||
// 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)
|
||||
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)
|
||||
@ -179,7 +186,7 @@ func (s *Server) Setup() *Server {
|
||||
path := ctx.Request.URL.Path
|
||||
|
||||
// TODO: Use constants for errors?
|
||||
if strings.HasPrefix(path, domain.VERSION+domain.API) {
|
||||
if strings.HasPrefix(path, domain.VERSION_1+domain.API) {
|
||||
ctx.JSON(http.StatusNotFound, gin.H{
|
||||
"status": http.StatusNotFound,
|
||||
"error": "API_NOT_FOUND",
|
||||
@ -192,5 +199,18 @@ func (s *Server) Setup() *Server {
|
||||
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("/protected", JwtAuthMiddlewareV2([]byte(cfg.JwtSecret)), func(ctx *gin.Context) {
|
||||
ctx.JSON(http.StatusOK, gin.H{"msg": "YAY"})
|
||||
})
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
25
internal/app/server/user_handler_v2.go
Normal file
25
internal/app/server/user_handler_v2.go
Normal file
@ -0,0 +1,25 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
|
||||
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.",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, gin.H{
|
||||
"status": http.StatusOK,
|
||||
"message": "[OK] Successfully retrieved authenticated user.",
|
||||
"user": user,
|
||||
})
|
||||
}
|
||||
@ -1,36 +1,38 @@
|
||||
package domain
|
||||
|
||||
// Sub-routes
|
||||
const VERSION = "/v1"
|
||||
const VERSION_1 = "/v1"
|
||||
const VERSION_2 = "/v2"
|
||||
const WEB = "/web"
|
||||
const API = "/api"
|
||||
const STATE = "/state"
|
||||
|
||||
// Web prefixed routes
|
||||
const WEB_LOGIN = VERSION + WEB + "/login"
|
||||
const WEB_INDEX = VERSION + WEB
|
||||
const WEB_HOME = VERSION + WEB + "/home"
|
||||
const WEB_FAVORITES = VERSION + WEB + "/favorites"
|
||||
const WEB_CREATE = VERSION + WEB + "/create"
|
||||
const WEB_PROFIlE = VERSION + WEB + "/profile"
|
||||
const WEB_LIST = VERSION + WEB + "/list"
|
||||
const WEB_RECIPE = VERSION + WEB + "/recipe/%d"
|
||||
const WEB_SEARCH = VERSION + WEB + "/search"
|
||||
const WEB_NOT_FOUND = VERSION + WEB + "/404"
|
||||
const WEB_LOGIN = VERSION_1 + WEB + "/login"
|
||||
const WEB_INDEX = VERSION_1 + WEB
|
||||
const WEB_HOME = VERSION_1 + WEB + "/home"
|
||||
const WEB_FAVORITES = VERSION_1 + WEB + "/favorites"
|
||||
const WEB_CREATE = VERSION_1 + WEB + "/create"
|
||||
const WEB_PROFIlE = VERSION_1 + WEB + "/profile"
|
||||
const WEB_LIST = VERSION_1 + WEB + "/list"
|
||||
const WEB_RECIPE = VERSION_1 + WEB + "/recipe/%d"
|
||||
const WEB_SEARCH = VERSION_1 + WEB + "/search"
|
||||
const WEB_NOT_FOUND = VERSION_1 + WEB + "/404"
|
||||
|
||||
// API prefixed routes
|
||||
const API_AUTH_LOGIN = VERSION + API + "/auth/login"
|
||||
const API_AUTH_CALLBACK = VERSION + API + "/auth/callback"
|
||||
const API_AUTH_LOGOUT = VERSION + API + "/auth/logout"
|
||||
const API_CREATE_RECIPE = VERSION + API + "/recipe"
|
||||
const API_SEARCH_RECIPES = VERSION + API + "/recipe/search"
|
||||
const API_SEARCH_FAVORITES = VERSION + API + "/recipe/search/favorites"
|
||||
const API_AUTH_LOGIN = VERSION_1 + API + "/auth/login"
|
||||
const API_AUTH_CALLBACK = VERSION_1 + API + "/auth/callback"
|
||||
const API_AUTH_CALLBACK_V2 = VERSION_2 + API + "/auth/callback"
|
||||
const API_AUTH_LOGOUT = VERSION_1 + API + "/auth/logout"
|
||||
const API_CREATE_RECIPE = VERSION_1 + API + "/recipe"
|
||||
const API_SEARCH_RECIPES = VERSION_1 + API + "/recipe/search"
|
||||
const API_SEARCH_FAVORITES = VERSION_1 + API + "/recipe/search/favorites"
|
||||
|
||||
const API_ENGAGEMENT_VIEW = VERSION + API + "/engagement/view/%d"
|
||||
const API_ENGAGEMENT_SHARE = VERSION + API + "/engagement/share/%d"
|
||||
const API_ENGAGEMENT_FAVORITE = VERSION + API + "/engagement/favorite/%d"
|
||||
const API_ENGAGEMENT_MAKE = VERSION + API + "/engagement/make/%d"
|
||||
const API_ENGAGEMENT_VIEW = VERSION_1 + API + "/engagement/view/%d"
|
||||
const API_ENGAGEMENT_SHARE = VERSION_1 + API + "/engagement/share/%d"
|
||||
const API_ENGAGEMENT_FAVORITE = VERSION_1 + API + "/engagement/favorite/%d"
|
||||
const API_ENGAGEMENT_MAKE = VERSION_1 + API + "/engagement/make/%d"
|
||||
|
||||
// State prefixed routes
|
||||
const STATE_TAGS_CREATE = VERSION + WEB + STATE + "/tags"
|
||||
const STATE_TAGS_DELETE = VERSION + WEB + STATE + "/tags/delete"
|
||||
const STATE_TAGS_CREATE = VERSION_1 + WEB + STATE + "/tags"
|
||||
const STATE_TAGS_DELETE = VERSION_1 + WEB + STATE + "/tags/delete"
|
||||
|
||||
@ -1,73 +1,4 @@
|
||||
# React + TypeScript + Vite
|
||||
|
||||
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||
# IF BACKEND CANNOT GET COOKIE
|
||||
|
||||
Currently, two official plugins are available:
|
||||
|
||||
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
|
||||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
||||
|
||||
## React Compiler
|
||||
|
||||
The React Compiler is currently not compatible with SWC. See [this issue](https://github.com/vitejs/vite-plugin-react/issues/428) for tracking the progress.
|
||||
|
||||
## Expanding the ESLint configuration
|
||||
|
||||
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
|
||||
|
||||
```js
|
||||
export default defineConfig([
|
||||
globalIgnores(['dist']),
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
extends: [
|
||||
// Other configs...
|
||||
|
||||
// Remove tseslint.configs.recommended and replace with this
|
||||
tseslint.configs.recommendedTypeChecked,
|
||||
// Alternatively, use this for stricter rules
|
||||
tseslint.configs.strictTypeChecked,
|
||||
// Optionally, add this for stylistic rules
|
||||
tseslint.configs.stylisticTypeChecked,
|
||||
|
||||
// Other configs...
|
||||
],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
// other options...
|
||||
},
|
||||
},
|
||||
])
|
||||
```
|
||||
|
||||
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
|
||||
|
||||
```js
|
||||
// eslint.config.js
|
||||
import reactX from 'eslint-plugin-react-x'
|
||||
import reactDom from 'eslint-plugin-react-dom'
|
||||
|
||||
export default defineConfig([
|
||||
globalIgnores(['dist']),
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
extends: [
|
||||
// Other configs...
|
||||
// Enable lint rules for React
|
||||
reactX.configs['recommended-typescript'],
|
||||
// Enable lint rules for React DOM
|
||||
reactDom.configs.recommended,
|
||||
],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
// other options...
|
||||
},
|
||||
},
|
||||
])
|
||||
```
|
||||
Do not forget to send the axios request with the `{ withCredentials: true }` flags.
|
||||
|
||||
333
web/package-lock.json
generated
333
web/package-lock.json
generated
@ -9,9 +9,11 @@
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@tailwindcss/vite": "^4.1.16",
|
||||
"axios": "^1.13.2",
|
||||
"eslint-plugin-react-dom": "^2.2.4",
|
||||
"eslint-plugin-react-x": "^2.2.4",
|
||||
"react": "^19.1.1",
|
||||
"react-cookie": "^8.0.1",
|
||||
"react-dom": "^19.1.1",
|
||||
"react-router-dom": "^7.9.5",
|
||||
"tailwindcss": "^4.1.16"
|
||||
@ -1604,6 +1606,18 @@
|
||||
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/hoist-non-react-statics": {
|
||||
"version": "3.3.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.7.tgz",
|
||||
"integrity": "sha512-PQTyIulDkIDro8P+IHbKCsw7U2xxBYflVzW/FgWdCAePD9xGSidgA76/GeJ6lBKoblyhf9pBY763gbrN+1dI8g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"hoist-non-react-statics": "^3.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/json-schema": {
|
||||
"version": "7.0.15",
|
||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
|
||||
@ -1624,7 +1638,6 @@
|
||||
"version": "19.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz",
|
||||
"integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"csstype": "^3.0.2"
|
||||
@ -1963,6 +1976,23 @@
|
||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
||||
"license": "Python-2.0"
|
||||
},
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.13.2",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz",
|
||||
"integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.4",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
@ -1997,6 +2027,19 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bind-apply-helpers": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/callsites": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
|
||||
@ -2040,6 +2083,18 @@
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/compare-versions": {
|
||||
"version": "6.1.1",
|
||||
"resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-6.1.1.tgz",
|
||||
@ -2079,7 +2134,6 @@
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/debug": {
|
||||
@ -2105,6 +2159,15 @@
|
||||
"integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/detect-libc": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||
@ -2114,6 +2177,20 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"gopd": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/enhanced-resolve": {
|
||||
"version": "5.18.3",
|
||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz",
|
||||
@ -2127,6 +2204,51 @@
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/es-define-property": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-errors": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-object-atoms": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
||||
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-set-tostringtag": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
||||
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.6",
|
||||
"has-tostringtag": "^1.0.2",
|
||||
"hasown": "^2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.25.11",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.11.tgz",
|
||||
@ -2518,6 +2640,42 @@
|
||||
"integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.11",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
|
||||
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"debug": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
|
||||
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"es-set-tostringtag": "^2.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"mime-types": "^2.1.12"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
@ -2532,6 +2690,52 @@
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/get-intrinsic": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.2",
|
||||
"es-define-property": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"es-object-atoms": "^1.1.1",
|
||||
"function-bind": "^1.1.2",
|
||||
"get-proto": "^1.0.1",
|
||||
"gopd": "^1.2.0",
|
||||
"has-symbols": "^1.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"math-intrinsics": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/get-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dunder-proto": "^1.0.1",
|
||||
"es-object-atoms": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/glob-parent": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
|
||||
@ -2557,6 +2761,18 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/gopd": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/graceful-fs": {
|
||||
"version": "4.2.11",
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||
@ -2579,6 +2795,54 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/has-symbols": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/has-tostringtag": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
||||
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"has-symbols": "^1.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/hasown": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/hoist-non-react-statics": {
|
||||
"version": "3.3.2",
|
||||
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
|
||||
"integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"react-is": "^16.7.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ignore": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
||||
@ -3004,6 +3268,15 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.5.5"
|
||||
}
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/merge2": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
|
||||
@ -3026,6 +3299,27 @@
|
||||
"node": ">=8.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-db": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-types": {
|
||||
"version": "2.1.35",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-db": "1.52.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
@ -3200,6 +3494,12 @@
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/punycode": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||
@ -3238,6 +3538,20 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-cookie": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/react-cookie/-/react-cookie-8.0.1.tgz",
|
||||
"integrity": "sha512-QNdAd0MLuAiDiLcDU/2s/eyKmmfMHtjPUKJ2dZ/5CcQ9QKUium4B3o61/haq6PQl/YWFqC5PO8GvxeHKhy3GFA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/hoist-non-react-statics": "^3.3.6",
|
||||
"hoist-non-react-statics": "^3.3.2",
|
||||
"universal-cookie": "^8.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">= 16.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-dom": {
|
||||
"version": "19.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz",
|
||||
@ -3250,6 +3564,12 @@
|
||||
"react": "^19.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-is": {
|
||||
"version": "16.13.1",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react-router": {
|
||||
"version": "7.9.5",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.9.5.tgz",
|
||||
@ -3639,6 +3959,15 @@
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/universal-cookie": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/universal-cookie/-/universal-cookie-8.0.1.tgz",
|
||||
"integrity": "sha512-B6ks9FLLnP1UbPPcveOidfvB9pHjP+wekP2uRYB9YDfKVpvcjKgy1W5Zj+cEXJ9KTPnqOKGfVDQBmn8/YCQfRg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cookie": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/uri-js": {
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
|
||||
|
||||
@ -11,9 +11,11 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@tailwindcss/vite": "^4.1.16",
|
||||
"axios": "^1.13.2",
|
||||
"eslint-plugin-react-dom": "^2.2.4",
|
||||
"eslint-plugin-react-x": "^2.2.4",
|
||||
"react": "^19.1.1",
|
||||
"react-cookie": "^8.0.1",
|
||||
"react-dom": "^19.1.1",
|
||||
"react-router-dom": "^7.9.5",
|
||||
"tailwindcss": "^4.1.16"
|
||||
|
||||
@ -8,19 +8,40 @@ import Create from './pages/Create';
|
||||
import Favorites from './pages/Favorites';
|
||||
import Profile from './pages/Profile';
|
||||
import ShoppingList from './pages/ShoppingList';
|
||||
import LoginPage from './pages/Login';
|
||||
import { use, type ReactNode } from 'react';
|
||||
import { AuthContext } from './context/AuthContext';
|
||||
|
||||
function ProtectedRoute({ children }: { children: ReactNode }) {
|
||||
const { isLoggedIn } = use(AuthContext)
|
||||
// Wait until the value is set
|
||||
if (isLoggedIn === undefined) {
|
||||
// Still checking auth state: don't render anything yet, or show a spinner if desired
|
||||
return null; // or <Loading />
|
||||
}
|
||||
|
||||
if (isLoggedIn) return children;
|
||||
|
||||
// Redirect to login page if not authenicated
|
||||
return <Navigate to="/v2/web/login" replace />
|
||||
}
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route path="/" element={<Navigate to={ROUTE_CONSTANTS.Home} replace />} />
|
||||
|
||||
{/* Login page does not inherit WebLayout */}
|
||||
<Route path="/v2/web/login" element={<LoginPage />} />
|
||||
|
||||
<Route path="/v2/web" element={<WebLayout />}>
|
||||
<Route index element={<Navigate to={ROUTE_CONSTANTS.Home} replace />} />
|
||||
<Route path="home" element={<Home />} />
|
||||
<Route path="favorites" element={<Favorites />} />
|
||||
<Route path="create" element={<Create />} />
|
||||
<Route path="profile" element={<Profile />} />
|
||||
<Route path="list" element={<ShoppingList />} />
|
||||
<Route path="favorites" element={<ProtectedRoute><Favorites /></ProtectedRoute>} />
|
||||
<Route path="create" element={<ProtectedRoute><Create /></ProtectedRoute>} />
|
||||
<Route path="profile" element={<ProtectedRoute><Profile /></ProtectedRoute>} />
|
||||
<Route path="list" element={<ProtectedRoute><ShoppingList /></ProtectedRoute>} />
|
||||
|
||||
{/* <Route path="recipe/:id" element={<Home />} /> */}
|
||||
|
||||
|
||||
@ -11,7 +11,7 @@ export default function Navigation() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<nav className="block md:fixed w-full z-10">
|
||||
<nav className="block md:fixed w-full z-20">
|
||||
<div
|
||||
className="relative w-full px-8 md:px-44 p-4 border-b border-gray-300 shadow-sm shadow-gray-300 bg-white flex justify-between items-center"
|
||||
>
|
||||
|
||||
13
web/src/context/AuthContext.tsx
Normal file
13
web/src/context/AuthContext.tsx
Normal file
@ -0,0 +1,13 @@
|
||||
import { createContext } from "react";
|
||||
|
||||
interface AuthContextType {
|
||||
isLoggedIn: boolean | undefined;
|
||||
setIsLoggedIn: (state: boolean) => void;
|
||||
getJwt: () => string;
|
||||
}
|
||||
|
||||
export const AuthContext = createContext<AuthContextType>({
|
||||
isLoggedIn: undefined,
|
||||
setIsLoggedIn: () => { return },
|
||||
getJwt: () => ""
|
||||
});
|
||||
34
web/src/context/AuthProvider.tsx
Normal file
34
web/src/context/AuthProvider.tsx
Normal file
@ -0,0 +1,34 @@
|
||||
import { useEffect, useState, type ReactNode } from "react";
|
||||
import { AuthContext } from "./AuthContext";
|
||||
import { useCookies } from 'react-cookie';
|
||||
|
||||
// BUG: The rerender issue is ridiclious, and needs to be updated. Maybe using a global
|
||||
// state management tool instead of a context
|
||||
//
|
||||
// BUG: We do not want to have access to these cookies in the UI, for security reasons.
|
||||
// Instead, we should implement an api `/auth/status` can be requested to validate
|
||||
// a token. Not sure how often this should get called, but it should be implemented.
|
||||
export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
const [cookies] = useCookies(["jwt_token"]);
|
||||
const [isLoggedIn, setIsLoggedIn] = useState<boolean | undefined>(cookies.jwt_token !== undefined);
|
||||
|
||||
const getJwt = (): string => {
|
||||
if (!cookies.jwt_token) return "";
|
||||
return cookies.jwt_token as string;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setIsLoggedIn(cookies.jwt_token !== undefined);
|
||||
}, [cookies]);
|
||||
|
||||
// NOTE: Display some loading page, maybe...
|
||||
// if (isLoggedIn === undefined) {
|
||||
// return <div>Loading authentication...</div>; // or null for no flicker
|
||||
// }
|
||||
|
||||
return (
|
||||
<AuthContext value={{ isLoggedIn, setIsLoggedIn, getJwt }}>
|
||||
{children}
|
||||
</AuthContext>
|
||||
);
|
||||
}
|
||||
2
web/src/context/ProtectedRoute.tsx
Normal file
2
web/src/context/ProtectedRoute.tsx
Normal file
@ -0,0 +1,2 @@
|
||||
|
||||
export function ProtectedRoute
|
||||
@ -2,9 +2,15 @@ import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import './index.css'
|
||||
import App from './App.tsx'
|
||||
import { AuthProvider } from './context/AuthProvider.tsx'
|
||||
import { CookiesProvider } from 'react-cookie'
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
<CookiesProvider>
|
||||
<AuthProvider>
|
||||
<App />
|
||||
</AuthProvider>
|
||||
</CookiesProvider>
|
||||
</StrictMode>,
|
||||
)
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { use, useEffect, useState } from "react";
|
||||
import SalmonVideo from "../assets/videos/salmon_video.mp4";
|
||||
import Banner from "../components/Banner";
|
||||
import ROUTE_CONSTANTS from "../types/routes";
|
||||
@ -8,14 +8,22 @@ import type { Recipe } from "../types/recipe";
|
||||
import RecipeCardSmall from "../components/cards/RecipeCardSmall";
|
||||
import ContentCardSmall from "../components/cards/ContentCardSmall";
|
||||
import RecipeSearchBar from "../components/inputs/RecipeSearchBar";
|
||||
import { GetRecipeOfTheWeek } from "../services/RecipeService";
|
||||
import { isApiError, type ApiError } from "../types/api/error";
|
||||
import { AuthContext } from "../context/AuthContext";
|
||||
|
||||
export default function Home() {
|
||||
const [loggedIn, isLoggedIn] = useState<boolean>(false);
|
||||
// Context
|
||||
const { isLoggedIn } = use(AuthContext);
|
||||
|
||||
// Page state
|
||||
const [recipeOfTheWeek, setRecipeOfTheWeek] = useState<Recipe | null>(null);
|
||||
|
||||
const [madeRecipes, setMadeRecipes] = useState<Recipe[]>([]);
|
||||
const [viewedRecipes, setViewedRecipes] = useState<Recipe[]>([]);
|
||||
|
||||
const [error, setError] = useState<string>("");
|
||||
|
||||
|
||||
// BUG: Remove these
|
||||
useEffect(() => {
|
||||
@ -97,7 +105,6 @@ export default function Home() {
|
||||
Favorite: true
|
||||
};
|
||||
|
||||
isLoggedIn(true);
|
||||
setRecipeOfTheWeek(recipe);
|
||||
|
||||
const recipes: Recipe[] = [recipe, recipe2];
|
||||
@ -105,6 +112,26 @@ export default function Home() {
|
||||
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;
|
||||
}
|
||||
setRecipeOfTheWeek(result);
|
||||
}
|
||||
void fetch();
|
||||
}, []);
|
||||
|
||||
// BUG: Prob remove
|
||||
useEffect(() => {
|
||||
if (error)
|
||||
console.error(error);
|
||||
}, [error]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Intro Section */}
|
||||
@ -155,7 +182,7 @@ export default function Home() {
|
||||
<Banner content="Take Another Look." />
|
||||
<div className="w-full">
|
||||
<h3 className="text-lg mt-8 mx-4">Recently viewed</h3>
|
||||
{loggedIn ?
|
||||
{isLoggedIn ?
|
||||
<div className="flex overflow-x-auto gap-x-4 mx-4 my-4">
|
||||
{viewedRecipes && viewedRecipes.length > 0 ? (
|
||||
<>
|
||||
@ -176,7 +203,7 @@ export default function Home() {
|
||||
</div>
|
||||
}
|
||||
<h3 className="text-lg mt-8 mx-4">Make again</h3>
|
||||
{loggedIn ?
|
||||
{isLoggedIn ?
|
||||
<div className="flex overflow-x-auto gap-x-4 mx-4 my-4">
|
||||
{madeRecipes && madeRecipes.length > 0 ? (
|
||||
<>
|
||||
|
||||
75
web/src/pages/Login.tsx
Normal file
75
web/src/pages/Login.tsx
Normal file
@ -0,0 +1,75 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { GetGoogleAuthUrl } from "../services/AuthService"
|
||||
import { isApiError, type ApiError } from "../types/api/error"
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
|
||||
|
||||
export default function LoginPage() {
|
||||
const [error, setError] = useState<string>("");
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
|
||||
const clickHandler = async (): Promise<void> => {
|
||||
const result: string | ApiError = await GetGoogleAuthUrl();
|
||||
|
||||
if (isApiError(result)) {
|
||||
setError(result.message);
|
||||
return;
|
||||
}
|
||||
|
||||
window.location.href = result;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (error)
|
||||
console.error(error);
|
||||
}, [error]);
|
||||
|
||||
useEffect(() => {
|
||||
if (searchParams.has("error")) {
|
||||
const error: string = searchParams.get("error")!;
|
||||
setError(error);
|
||||
}
|
||||
}, [searchParams]);
|
||||
|
||||
// TODO: Implement an error display!
|
||||
return <>
|
||||
<div className="h-screen w-full grid place-items-center bg-gray-100">
|
||||
<div className="w-3/4 sm:w-3/4 md:w-1/2 lg:w-2/7 bg-white border border-gray-200 rounded-xl shadow-2xs">
|
||||
<div className="p-4 sm:p-7">
|
||||
<div className="">
|
||||
<h1 className="block text-2xl font-bold text-gray-800">
|
||||
Sign in to Continue
|
||||
</h1>
|
||||
<p className="mt-2 text-sm text-gray-600">
|
||||
You need to sign in to continue. Don't have an account? Signing in will
|
||||
create one for you!
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-5">
|
||||
<button onClick={() => { void clickHandler(); }} className="w-full py-3 px-4 inline-flex justify-center items-center gap-x-2 text-sm font-medium rounded-lg border border-gray-200 bg-white text-gray-800 shadow-2xs hover:bg-gray-50 focus:outline-hidden focus:bg-gray-50 disabled:opacity-50 disabled:pointer-events-none cursor-pointer">
|
||||
<svg className="w-4 h-auto" width="46" height="47" viewBox="0 0 46 47" fill="none">
|
||||
<path
|
||||
d="M46 24.0287C46 22.09 45.8533 20.68 45.5013 19.2112H23.4694V27.9356H36.4069C36.1429 30.1094 34.7347 33.37 31.5957 35.5731L31.5663 35.8669L38.5191 41.2719L38.9885 41.3306C43.4477 37.2181 46 31.1669 46 24.0287Z"
|
||||
fill="#4285F4"
|
||||
></path>
|
||||
<path
|
||||
d="M23.4694 47C29.8061 47 35.1161 44.9144 39.0179 41.3012L31.625 35.5437C29.6301 36.9244 26.9898 37.8937 23.4987 37.8937C17.2793 37.8937 12.0281 33.7812 10.1505 28.1412L9.88649 28.1706L2.61097 33.7812L2.52296 34.0456C6.36608 41.7125 14.287 47 23.4694 47Z"
|
||||
fill="#34A853"
|
||||
></path>
|
||||
<path
|
||||
d="M10.1212 28.1413C9.62245 26.6725 9.32908 25.1156 9.32908 23.5C9.32908 21.8844 9.62245 20.3275 10.0918 18.8588V18.5356L2.75765 12.8369L2.52296 12.9544C0.909439 16.1269 0 19.7106 0 23.5C0 27.2894 0.909439 30.8731 2.49362 34.0456L10.1212 28.1413Z"
|
||||
fill="#FBBC05"
|
||||
></path>
|
||||
<path
|
||||
d="M23.4694 9.07688C27.8699 9.07688 30.8622 10.9863 32.5344 12.5725L39.1645 6.11C35.0867 2.32063 29.8061 0 23.4694 0C14.287 0 6.36607 5.2875 2.49362 12.9544L10.0918 18.8588C11.9987 13.1894 17.25 9.07688 23.4694 9.07688Z"
|
||||
fill="#EB4335"
|
||||
></path>
|
||||
</svg>
|
||||
Sign in with Google
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
@ -1,16 +1,29 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { use, useEffect, useState } from "react";
|
||||
import type { User } from "../types/user";
|
||||
import type { Recipe } from "../types/recipe";
|
||||
import RecipeListItem from "../components/results/RecipeListItem";
|
||||
import type { Engagement } from "../types/engagement";
|
||||
import ActivityListItem from "../components/results/ActivityListItem";
|
||||
import { AuthContext } from "../context/AuthContext";
|
||||
import { GetAuthenticatedUser } from "../services/UserService";
|
||||
import { isApiError, type ApiError } from "../types/api/error";
|
||||
import { Logout } from "../services/AuthService";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
export default function Profile() {
|
||||
// Context
|
||||
const { getJwt } = use(AuthContext);
|
||||
const navigate = useNavigate();
|
||||
|
||||
// Page state
|
||||
const [error, setError] = useState<string>("");
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
const [recipes, setRecipes] = useState<Recipe[]>([]);
|
||||
const [favorites, setFavorites] = useState<Recipe[]>([]);
|
||||
const [activity, setActivity] = useState<Engagement[]>([]);
|
||||
const [jwt, setJwt] = useState<string>("");
|
||||
|
||||
// BUG: Remove this, used for testing
|
||||
useEffect(() => {
|
||||
const recipe: Recipe = {
|
||||
Id: 1,
|
||||
@ -99,23 +112,43 @@ export default function Profile() {
|
||||
Created: new Date(),
|
||||
};
|
||||
|
||||
const user: User = {
|
||||
Id: 1,
|
||||
GoogleId: "a",
|
||||
Name: "Hayden Hargreaves",
|
||||
Email: "hhargreaves2006@gmail.com",
|
||||
ImageUrl: "https://lh3.googleusercontent.com/a/ACg8ocLeT6ltjQIkiBy1MgMJDbQxtBfMVfn8sP4e1t7d0bCJeHFdpcea=s96-c",
|
||||
GoogleRefreshToken: "a",
|
||||
Created: new Date(),
|
||||
};
|
||||
|
||||
setUser(user);
|
||||
|
||||
setRecipes([recipe, recipe2, recipe, recipe, recipe, recipe, recipe]);
|
||||
setRecipes([recipe, recipe2]);
|
||||
setFavorites([recipe, recipe2]);
|
||||
setActivity([eng]);
|
||||
}, []);
|
||||
|
||||
// Log the user out and direct to the home page
|
||||
const logoutHandler = (): void => {
|
||||
void Logout();
|
||||
void navigate("/v2/web/home");
|
||||
}
|
||||
|
||||
// Get the JWT from the cookies
|
||||
useEffect(() => {
|
||||
setJwt(getJwt());
|
||||
}, [getJwt]);
|
||||
|
||||
// Get the user when the JWTS change
|
||||
useEffect(() => {
|
||||
// No jwt, we can't get a user
|
||||
if (!jwt) return;
|
||||
|
||||
async function fetch() {
|
||||
const result: User | ApiError = await GetAuthenticatedUser();
|
||||
if (isApiError(result)) {
|
||||
setError(result.message);
|
||||
return;
|
||||
}
|
||||
setUser(result);
|
||||
}
|
||||
void fetch();
|
||||
}, [jwt]);
|
||||
|
||||
useEffect(() => {
|
||||
if (error)
|
||||
console.log("@error", error);
|
||||
}, [error]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* User Details Section */}
|
||||
@ -195,9 +228,9 @@ export default function Profile() {
|
||||
|
||||
{/* Logout Section TODO: Click event*/}
|
||||
<section className="w-full flex flex-col justify-center items-center py-8 border-t border-gray-300 mt-auto">
|
||||
<a href="" className="text-center border border-red-500 text-red-500 w-9/10 md:w-1/3 py-2 rounded-lg hover:cursor-pointer hover:bg-red-100 duration-300">
|
||||
<button onClick={logoutHandler} className="text-center border border-red-500 text-red-500 w-9/10 md:w-1/3 py-2 rounded-lg hover:cursor-pointer hover:bg-red-100 duration-300">
|
||||
Logout
|
||||
</a>
|
||||
</button>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
|
||||
@ -1,7 +1,10 @@
|
||||
export default function ShoppingList() {
|
||||
return (
|
||||
<>
|
||||
<p>ShoppingList</p>
|
||||
<div className="flex flex-col items-center justify-center min-h-[90vh] h-full gap-y-2">
|
||||
<h1 className="text-4xl text-gray-800 font-semibold text-center">Page Under Construction </h1>
|
||||
<p className="text-gray-700">Sit tight, this page is coming soon!</p>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
26
web/src/services/AuthService.ts
Normal file
26
web/src/services/AuthService.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import axios from "axios";
|
||||
import type { GetGoogleAuthUrlResponse, LogoutResponse } from "../types/api/auth";
|
||||
import type { ApiError } from "../types/api/error";
|
||||
|
||||
|
||||
export async function GetGoogleAuthUrl (): Promise<string | ApiError> {
|
||||
const response = await axios.get<GetGoogleAuthUrlResponse>("http://localhost:3000/v2/api/auth/login");
|
||||
|
||||
if (response.status !== 200) {
|
||||
const err: ApiError = {
|
||||
status: response.status,
|
||||
message: "[FAIL] Something went wrong."
|
||||
};
|
||||
return err;
|
||||
}
|
||||
|
||||
return response.data.url;
|
||||
}
|
||||
|
||||
export async function Logout (): Promise<void> {
|
||||
const response = await axios.get<LogoutResponse>("http://localhost:3000/v2/api/auth/logout");
|
||||
|
||||
// This should never happen
|
||||
if (response.status !== 204)
|
||||
console.error("LOGOUT FAILED");
|
||||
}
|
||||
19
web/src/services/RecipeService.ts
Normal file
19
web/src/services/RecipeService.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import axios from "axios";
|
||||
import type { GetRecipeOfTheWeekResponse } from "../types/api/recipe";
|
||||
import type { Recipe } from "../types/recipe";
|
||||
import type { ApiError } from "../types/api/error";
|
||||
|
||||
|
||||
export async function GetRecipeOfTheWeek (): Promise<Recipe | ApiError> {
|
||||
const response = await axios.get<GetRecipeOfTheWeekResponse>("http://localhost:3000/v2/api/recipe/of-the-week");
|
||||
|
||||
if (response.status !== 200 || response.data.recipe === undefined) {
|
||||
const err: ApiError = {
|
||||
status: response.data.status,
|
||||
message: response.data.message
|
||||
};
|
||||
return err;
|
||||
}
|
||||
|
||||
return response.data.recipe;
|
||||
}
|
||||
19
web/src/services/UserService.ts
Normal file
19
web/src/services/UserService.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import axios from "axios";
|
||||
import type { ApiError } from "../types/api/error";
|
||||
import type { User } from "../types/user";
|
||||
import type { GetAuthenticateUserResponse } from "../types/api/user";
|
||||
|
||||
|
||||
export async function GetAuthenticatedUser(): Promise<User | ApiError> {
|
||||
const response = await axios.get<GetAuthenticateUserResponse>("http://localhost:3000/v2/api/user", { withCredentials: true });
|
||||
|
||||
if (response.data.status !== 200 || response.data.user === undefined){
|
||||
const err: ApiError = {
|
||||
status: response.data.status,
|
||||
message: response.data.message
|
||||
};
|
||||
return err;
|
||||
}
|
||||
|
||||
return response.data.user;
|
||||
}
|
||||
10
web/src/types/api/auth.ts
Normal file
10
web/src/types/api/auth.ts
Normal file
@ -0,0 +1,10 @@
|
||||
|
||||
export interface GetGoogleAuthUrlResponse {
|
||||
status: number;
|
||||
message: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface LogoutResponse {
|
||||
stauts: number;
|
||||
}
|
||||
16
web/src/types/api/error.ts
Normal file
16
web/src/types/api/error.ts
Normal file
@ -0,0 +1,16 @@
|
||||
|
||||
export function isApiError(obj: unknown): obj is ApiError {
|
||||
return (
|
||||
typeof obj === "object" &&
|
||||
obj !== null &&
|
||||
"status" in obj &&
|
||||
typeof (obj as { status?: unknown }).status === "number" &&
|
||||
"message" in obj &&
|
||||
typeof (obj as { message?: unknown }).message === "string"
|
||||
);
|
||||
}
|
||||
|
||||
export interface ApiError {
|
||||
status: number;
|
||||
message: string;
|
||||
}
|
||||
7
web/src/types/api/recipe.ts
Normal file
7
web/src/types/api/recipe.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import type { Recipe } from "../recipe";
|
||||
|
||||
export interface GetRecipeOfTheWeekResponse {
|
||||
status: number;
|
||||
message: string;
|
||||
recipe?: Recipe;
|
||||
}
|
||||
7
web/src/types/api/user.ts
Normal file
7
web/src/types/api/user.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import type { User } from "../user";
|
||||
|
||||
export interface GetAuthenticateUserResponse {
|
||||
status: number;
|
||||
message: string;
|
||||
user?: User;
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user