From 47b838684437b653c2f93e19b974893f97af24df Mon Sep 17 00:00:00 2001 From: Hayden Hargreaves Date: Wed, 3 Sep 2025 22:22:23 -0700 Subject: [PATCH 1/3] (FEAT): Errors can now be displayed using the RenderErrorBanner fx. This is found in the components domain. Make sure only HTMX routes call it. Although, I think I put this into the page handlers WHICH IS WRONG. However, it does work, ish. But it does not load into the DOM properly. But it seems to display just fine. --- internal/app/handlers/engagement_handler.go | 2 + internal/app/handlers/page_handler.go | 23 ++- internal/templates/components/error.templ | 58 ++++++++ internal/templates/components/error_templ.go | 137 ++++++++++++++++++ internal/templates/layouts/app_layout.templ | 36 +++-- .../templates/layouts/app_layout_templ.go | 6 +- web/static/css/tailwind.css | 35 +++++ 7 files changed, 270 insertions(+), 27 deletions(-) create mode 100644 internal/templates/components/error.templ create mode 100644 internal/templates/components/error_templ.go diff --git a/internal/app/handlers/engagement_handler.go b/internal/app/handlers/engagement_handler.go index 9e35182..1c75b7c 100644 --- a/internal/app/handlers/engagement_handler.go +++ b/internal/app/handlers/engagement_handler.go @@ -7,6 +7,7 @@ import ( "github.com/gin-gonic/gin" domain "github.com/haydenhargreaves/Potion/internal/domain/server" + "github.com/haydenhargreaves/Potion/internal/templates/components" ) func EngagementViewRecipe(ctx *gin.Context) { @@ -101,6 +102,7 @@ func EngagementFavoriteRecipe(ctx *gin.Context) { 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(), diff --git a/internal/app/handlers/page_handler.go b/internal/app/handlers/page_handler.go index a29b705..8c84971 100755 --- a/internal/app/handlers/page_handler.go +++ b/internal/app/handlers/page_handler.go @@ -11,6 +11,7 @@ import ( domainRecipe "github.com/haydenhargreaves/Potion/internal/domain/recipe" domain "github.com/haydenhargreaves/Potion/internal/domain/server" domainServer "github.com/haydenhargreaves/Potion/internal/domain/server" + "github.com/haydenhargreaves/Potion/internal/templates/components" layouts "github.com/haydenhargreaves/Potion/internal/templates/layouts" pages "github.com/haydenhargreaves/Potion/internal/templates/pages" templates "github.com/haydenhargreaves/Potion/internal/templates/pages" @@ -41,6 +42,7 @@ func HomePage(ctx *gin.Context) { 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()), @@ -49,9 +51,10 @@ func HomePage(ctx *gin.Context) { } 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 made recipes. %s\n", err.Error()), + "message": fmt.Sprintf("Error getting viewed recipes. %s\n", err.Error()), }) return } @@ -59,9 +62,10 @@ func HomePage(ctx *gin.Context) { // 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 made recipes. %s\n", err.Error()), + "message": fmt.Sprintf("Error getting recipe of the week. %s\n", err.Error()), }) return } @@ -82,9 +86,10 @@ func HomePage(ctx *gin.Context) { // 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 made recipes. %s\n", err.Error()), + "message": fmt.Sprintf("Error getting recipe of the week. %s\n", err.Error()), }) return } @@ -178,6 +183,7 @@ func ProfilePage(ctx *gin.Context) { 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()), @@ -187,9 +193,10 @@ func ProfilePage(ctx *gin.Context) { 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 recipes. %s\n", err.Error()), + "message": fmt.Sprintf("Error getting favorite recipes. %s\n", err.Error()), }) return } @@ -197,6 +204,7 @@ func ProfilePage(ctx *gin.Context) { // 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()), @@ -217,7 +225,6 @@ func ListPage(ctx *gin.Context) { ctx.HTML(http.StatusOK, "", layouts.AppLayout(title, page)) } -// TODO: Figure out how to handle errors, think we just need a simple display. func RecipePage(ctx *gin.Context) { // Call recipe service to get via ID deps := ctx.MustGet("deps").(*domainServer.InjectedDependencies) @@ -226,7 +233,7 @@ func RecipePage(ctx *gin.Context) { // Parse ID parsed, err := strconv.Atoi(id) if err != nil { - fmt.Printf("ERROR: %s\n", err.Error()) + components.RenderErrorBanner(ctx, fmt.Sprintf("ERROR: %s", err.Error())) ctx.JSON(400, err.Error()) return } @@ -251,7 +258,7 @@ func RecipePage(ctx *gin.Context) { // Get recipe recipe, err := deps.RecipeService.GetRecipe(parsed, userId) if err != nil { - fmt.Printf("ERROR: %s\n", err.Error()) + components.RenderErrorBanner(ctx, fmt.Sprintf("ERROR: %s", err.Error())) ctx.JSON(400, err.Error()) return } @@ -259,7 +266,7 @@ func RecipePage(ctx *gin.Context) { // Get user (owner) user, err := deps.UserService.GetUser(recipe.UserId) if err != nil { - fmt.Printf("ERROR: %s\n", err.Error()) + components.RenderErrorBanner(ctx, fmt.Sprintf("ERROR: %s", err.Error())) ctx.JSON(400, err.Error()) return } diff --git a/internal/templates/components/error.templ b/internal/templates/components/error.templ new file mode 100644 index 0000000..7c75b50 --- /dev/null +++ b/internal/templates/components/error.templ @@ -0,0 +1,58 @@ +package components + +import "github.com/gin-gonic/gin" + +templ errorIcon() { + + + +} + +templ closeButton() { + +} + +templ errorBanner(message string) { +
+
+ @errorIcon() +

Error

+ @closeButton() +
+
+

+ { message } +

+
+
+} + +// RenderErrorBanner renders the error banner. However, this function must ONLY be called by an +// HTMX route. Otherwise, there is no promise it will work (may result in undefined behavior). +// Just writes a piece of content to the response. +func RenderErrorBanner(ctx *gin.Context, message string) { + ctx.Writer.Header().Set("Content-Type", "text/html") + errorBanner(message).Render(ctx.Request.Context(), ctx.Writer) +} diff --git a/internal/templates/components/error_templ.go b/internal/templates/components/error_templ.go new file mode 100644 index 0000000..607c8e9 --- /dev/null +++ b/internal/templates/components/error_templ.go @@ -0,0 +1,137 @@ +// Code generated by templ - DO NOT EDIT. + +// templ: version: v0.3.937 +package components + +//lint:file-ignore SA4006 This context is only used if a nested component is present. + +import "github.com/a-h/templ" +import templruntime "github.com/a-h/templ/runtime" + +import "github.com/gin-gonic/gin" + +func errorIcon() templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var1 := templ.GetChildren(ctx) + if templ_7745c5c3_Var1 == nil { + templ_7745c5c3_Var1 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +func closeButton() templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var2 := templ.GetChildren(ctx) + if templ_7745c5c3_Var2 == nil { + templ_7745c5c3_Var2 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +func errorBanner(message string) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var3 := templ.GetChildren(ctx) + if templ_7745c5c3_Var3 == nil { + templ_7745c5c3_Var3 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = errorIcon().Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "

Error

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = closeButton().Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var4 string + templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(message) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/components/error.templ`, Line: 46, Col: 13} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +// RenderErrorBanner renders the error banner. However, this function must ONLY be called by an +// HTMX route. Otherwise, there is no promise it will work (may result in undefined behavior). +// Just writes a piece of content to the response. +func RenderErrorBanner(ctx *gin.Context, message string) { + ctx.Writer.Header().Set("Content-Type", "text/html") + errorBanner(message).Render(ctx.Request.Context(), ctx.Writer) +} + +var _ = templruntime.GeneratedTemplate diff --git a/internal/templates/layouts/app_layout.templ b/internal/templates/layouts/app_layout.templ index 45f9a04..6076747 100644 --- a/internal/templates/layouts/app_layout.templ +++ b/internal/templates/layouts/app_layout.templ @@ -3,20 +3,24 @@ package templates // AppLayout is the main application layout, this does not contain any content other than // meta data, links, scripts and whatever is passed into it as a component. templ AppLayout(title string, child templ.Component) { - - - - - - - { title } - - - - - - @child - - - + + + + + + { title } + + + + +
+ @child + + + } diff --git a/internal/templates/layouts/app_layout_templ.go b/internal/templates/layouts/app_layout_templ.go index 8e40f3e..2bacf07 100644 --- a/internal/templates/layouts/app_layout_templ.go +++ b/internal/templates/layouts/app_layout_templ.go @@ -38,13 +38,13 @@ func AppLayout(title string, child templ.Component) templ.Component { var templ_7745c5c3_Var2 string templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(title) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/layouts/app_layout.templ`, Line: 12, Col: 16} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/layouts/app_layout.templ`, Line: 11, Col: 17} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -52,7 +52,7 @@ func AppLayout(title string, child templ.Component) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/web/static/css/tailwind.css b/web/static/css/tailwind.css index f7bd052..b2e20b3 100644 --- a/web/static/css/tailwind.css +++ b/web/static/css/tailwind.css @@ -8,6 +8,7 @@ --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace; --color-red-100: oklch(93.6% 0.032 17.717); + --color-red-200: oklch(88.5% 0.062 18.334); --color-red-500: oklch(63.7% 0.237 25.331); --color-green-500: oklch(72.3% 0.219 149.579); --color-blue-50: oklch(97% 0.014 254.604); @@ -235,18 +236,27 @@ .absolute { position: absolute; } + .fixed { + position: fixed; + } .relative { position: relative; } .static { position: static; } + .top-0 { + top: calc(var(--spacing) * 0); + } .top-1\/2 { top: calc(1/2 * 100%); } .top-\[100\%\] { top: 100%; } + .right-0 { + right: calc(var(--spacing) * 0); + } .left-0 { left: calc(var(--spacing) * 0); } @@ -262,6 +272,9 @@ .z-20 { z-index: 20; } + .m-4 { + margin: calc(var(--spacing) * 4); + } .mx-2 { margin-inline: calc(var(--spacing) * 2); } @@ -337,6 +350,12 @@ .mb-16 { margin-bottom: calc(var(--spacing) * 16); } + .ml-8 { + margin-left: calc(var(--spacing) * 8); + } + .ml-auto { + margin-left: auto; + } .block { display: block; } @@ -694,6 +713,9 @@ --tw-gradient-to: var(--color-purple-200); --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); } + .p-1 { + padding: calc(var(--spacing) * 1); + } .p-2 { padding: calc(var(--spacing) * 2); } @@ -939,6 +961,12 @@ --tw-shadow-color: color-mix(in oklab, var(--color-gray-300) var(--tw-shadow-alpha), transparent); } } + .shadow-red-200 { + --tw-shadow-color: oklch(88.5% 0.062 18.334); + @supports (color: color-mix(in lab, red, red)) { + --tw-shadow-color: color-mix(in oklab, var(--color-red-200) var(--tw-shadow-alpha), transparent); + } + } .outline { outline-style: var(--tw-outline-style); outline-width: 1px; @@ -1141,6 +1169,13 @@ } } } + .hover\:bg-red-200 { + &:hover { + @media (hover: hover) { + background-color: var(--color-red-200); + } + } + } .hover\:text-blue-400 { &:hover { @media (hover: hover) { From 6b030adf02dd2dfbe722527eeb302e53b529bd87 Mon Sep 17 00:00:00 2001 From: Hayden Hargreaves Date: Wed, 3 Sep 2025 22:25:13 -0700 Subject: [PATCH 2/3] (DOC): Updated documentation --- internal/templates/components/error.templ | 72 +++++++++----------- internal/templates/components/error_templ.go | 6 +- 2 files changed, 37 insertions(+), 41 deletions(-) diff --git a/internal/templates/components/error.templ b/internal/templates/components/error.templ index 7c75b50..7df6acf 100644 --- a/internal/templates/components/error.templ +++ b/internal/templates/components/error.templ @@ -3,56 +3,48 @@ package components import "github.com/gin-gonic/gin" templ errorIcon() { - - - + + + } templ closeButton() { - + } templ errorBanner(message string) { -
-
- @errorIcon() -

Error

- @closeButton() -
-
-

- { message } -

-
-
+
+
+ @errorIcon() +

Error

+ @closeButton() +
+
+

+ { message } +

+
+
} // RenderErrorBanner renders the error banner. However, this function must ONLY be called by an // HTMX route. Otherwise, there is no promise it will work (may result in undefined behavior). // Just writes a piece of content to the response. +// +// If this is called from an NON-HTMX request, it will display (and functionality seems to work) +// but it will be loaded at an unknown position in the DOM. It will not swap with the proper +// element. func RenderErrorBanner(ctx *gin.Context, message string) { - ctx.Writer.Header().Set("Content-Type", "text/html") - errorBanner(message).Render(ctx.Request.Context(), ctx.Writer) +ctx.Writer.Header().Set("Content-Type", "text/html") +errorBanner(message).Render(ctx.Request.Context(), ctx.Writer) } diff --git a/internal/templates/components/error_templ.go b/internal/templates/components/error_templ.go index 607c8e9..5f93b89 100644 --- a/internal/templates/components/error_templ.go +++ b/internal/templates/components/error_templ.go @@ -112,7 +112,7 @@ func errorBanner(message string) templ.Component { var templ_7745c5c3_Var4 string templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(message) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/components/error.templ`, Line: 46, Col: 13} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/components/error.templ`, Line: 34, Col: 15} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) if templ_7745c5c3_Err != nil { @@ -129,6 +129,10 @@ func errorBanner(message string) templ.Component { // RenderErrorBanner renders the error banner. However, this function must ONLY be called by an // HTMX route. Otherwise, there is no promise it will work (may result in undefined behavior). // Just writes a piece of content to the response. +// +// If this is called from an NON-HTMX request, it will display (and functionality seems to work) +// but it will be loaded at an unknown position in the DOM. It will not swap with the proper +// element. func RenderErrorBanner(ctx *gin.Context, message string) { ctx.Writer.Header().Set("Content-Type", "text/html") errorBanner(message).Render(ctx.Request.Context(), ctx.Writer) From 1e6a06e8edd1d76caa83c0f01cb0c9b48bb1cfca Mon Sep 17 00:00:00 2001 From: Hayden Hargreaves Date: Thu, 4 Sep 2025 20:25:12 -0700 Subject: [PATCH 3/3] (FEAT): Added errors to each of the handlers. I think this is really the only place we need them. For now at least. --- internal/app/handlers/auth_handler.go | 11 +++-------- internal/app/handlers/engagement_handler.go | 5 +++++ internal/app/handlers/recipe_handler.go | 5 +++++ internal/app/handlers/user_handler.go | 7 ++++++- internal/app/service/auth_service.go | 18 +++++++++--------- internal/domain/auth/service.go | 6 +----- 6 files changed, 29 insertions(+), 23 deletions(-) diff --git a/internal/app/handlers/auth_handler.go b/internal/app/handlers/auth_handler.go index 4fa0a66..3c37be0 100644 --- a/internal/app/handlers/auth_handler.go +++ b/internal/app/handlers/auth_handler.go @@ -6,6 +6,7 @@ import ( "github.com/gin-gonic/gin" domain "github.com/haydenhargreaves/Potion/internal/domain/server" + "github.com/haydenhargreaves/Potion/internal/templates/components" ) // GoogleLogin directs the user to Googles select user login page. Once the user has selected an @@ -21,8 +22,6 @@ func GoogleLogin(ctx *gin.Context) { // 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. // -// TODO: This route does not do the proper handling, need to work on the redirection or handling. -// // We do not need to return all of this data, it is just for testing. func GoogleCallback(ctx *gin.Context) { deps := ctx.MustGet("deps").(*domain.InjectedDependencies) @@ -32,15 +31,11 @@ func GoogleCallback(ctx *gin.Context) { code string = ctx.Query("code") ) - // TODO: Do something real, not just return data - if jwt, dbUser, googleUserInfo, err := deps.AuthService.GoogleAuthSuccess(state, code); err != nil { + 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.JSON(http.StatusOK, gin.H{"jwt": jwt, "googleUserInfo": googleUserInfo, "dbUser": dbUser}) - _ = dbUser - _ = googleUserInfo - ctx.Redirect(http.StatusSeeOther, "/") } } diff --git a/internal/app/handlers/engagement_handler.go b/internal/app/handlers/engagement_handler.go index 1c75b7c..571d6a3 100644 --- a/internal/app/handlers/engagement_handler.go +++ b/internal/app/handlers/engagement_handler.go @@ -24,6 +24,7 @@ func EngagementViewRecipe(ctx *gin.Context) { 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(), @@ -37,6 +38,7 @@ func EngagementViewRecipe(ctx *gin.Context) { // 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(), @@ -61,6 +63,7 @@ func EngagementShareRecipe(ctx *gin.Context) { 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(), @@ -72,6 +75,7 @@ func EngagementShareRecipe(ctx *gin.Context) { } 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(), @@ -133,6 +137,7 @@ func EngagementMakeRecipe(ctx *gin.Context) { 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(), diff --git a/internal/app/handlers/recipe_handler.go b/internal/app/handlers/recipe_handler.go index 2d24556..770c25c 100644 --- a/internal/app/handlers/recipe_handler.go +++ b/internal/app/handlers/recipe_handler.go @@ -10,6 +10,7 @@ import ( "github.com/gin-gonic/gin" domainRecipe "github.com/haydenhargreaves/Potion/internal/domain/recipe" domain "github.com/haydenhargreaves/Potion/internal/domain/server" + "github.com/haydenhargreaves/Potion/internal/templates/components" templates "github.com/haydenhargreaves/Potion/internal/templates/pages" ) @@ -24,6 +25,7 @@ func CreateRecipe(ctx *gin.Context) { recipe, err := deps.RecipeService.CreateRecipe(ctx) if err != nil { + components.RenderErrorBanner(ctx, err.Error()) ctx.String(http.StatusOK, CREATE_ERROR_HTML, err.Error()) return } @@ -89,6 +91,7 @@ func SearchRecipes(ctx *gin.Context) { // 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()}) } @@ -126,6 +129,7 @@ func SearchRecipesFavorites(ctx *gin.Context) { // TODO: Error here if they're not logged in? // Get user data (they should be logged in) if !domain.IsLoggedIn(ctx) { + 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."}) } @@ -133,6 +137,7 @@ func SearchRecipesFavorites(ctx *gin.Context) { 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()}) } diff --git a/internal/app/handlers/user_handler.go b/internal/app/handlers/user_handler.go index e19cd18..4561f87 100644 --- a/internal/app/handlers/user_handler.go +++ b/internal/app/handlers/user_handler.go @@ -6,6 +6,7 @@ import ( "github.com/gin-gonic/gin" domain "github.com/haydenhargreaves/Potion/internal/domain/server" + "github.com/haydenhargreaves/Potion/internal/templates/components" ) func GetUserRecipes(ctx *gin.Context) { @@ -21,6 +22,7 @@ func GetUserRecipes(ctx *gin.Context) { // 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.", @@ -31,6 +33,7 @@ func GetUserRecipes(ctx *gin.Context) { 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()), @@ -59,6 +62,7 @@ func GetUserFavoriteRecipes(ctx *gin.Context) { // 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.", @@ -69,9 +73,10 @@ func GetUserFavoriteRecipes(ctx *gin.Context) { 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 user recipes. %s", err.Error()), + "message": fmt.Sprintf("Could not get favorite recipes. %s", err.Error()), "recipes": nil, }) return diff --git a/internal/app/service/auth_service.go b/internal/app/service/auth_service.go index bd0bf98..41d77a8 100644 --- a/internal/app/service/auth_service.go +++ b/internal/app/service/auth_service.go @@ -60,48 +60,48 @@ func (s *AuthService) GetGoogleAuthUrl() string { // GoogleAuthSuccess accepts the data from the Google login endpoint and uses it to fetch the users // data. The data is then used to log the user in or create an account. -func (s *AuthService) GoogleAuthSuccess(state, code string) (string, domain.User, domain.GoogleUserInfo, error) { +func (s *AuthService) GoogleAuthSuccess(state, code string) (string, error) { // Ensure the state matches, prevents M.I.T.M. attacks if state != "randomstate" { - return "", domain.User{}, domain.GoogleUserInfo{}, fmt.Errorf("States don't match, received %s", state) + return "", fmt.Errorf("States don't match, received %s", state) } // Get access token from Google token, err := auth.GoogleAuthConfig.Exchange(context.Background(), code) if err != nil { - return "", domain.User{}, domain.GoogleUserInfo{}, fmt.Errorf("Code exchange failed: %s", err.Error()) + return "", fmt.Errorf("Code exchange failed: %s", err.Error()) } // Use the access token to get user data googleUserInfo, err := auth.GetUserData(token.AccessToken) if err != nil { - return "", domain.User{}, domain.GoogleUserInfo{}, err + return "", err } // Attempt to get the user, user is nil when they don't exit user, err := s.userRepository.GetGoogleUser(googleUserInfo.Id) if err != nil { - return "", domain.User{}, domain.GoogleUserInfo{}, fmt.Errorf("Failed to get db user: %s", err) + return "", fmt.Errorf("Failed to get db user: %s", err) } // A user was found if user != nil { jwt, err := generateJwt(user.Id, user.Email, s.jwtSecret) - return jwt, *user, googleUserInfo, err + return jwt, err } // user did not exist, need to create one newUser, err := s.userRepository.CreateGoogleUser(&googleUserInfo, token.RefreshToken) if err != nil { - return "", domain.User{}, domain.GoogleUserInfo{}, fmt.Errorf("Repository failed to create user: %s", err.Error()) + return "", fmt.Errorf("Repository failed to create user: %s", err.Error()) } jwt, err := generateJwt(newUser.Id, newUser.Email, s.jwtSecret) - return jwt, newUser, googleUserInfo, err + return jwt, err } // generateJwt requires user data and returns a JSON web token which can be stored in the browsers -// cookies. This token is used to log a user into the application and allow access to protected +// cookies. This token is used to log a user into the application and allow access to protected // routes. func generateJwt(userId int, email string, jwtSecret []byte) (string, error) { expiration := time.Now().Add(7 * 24 * time.Hour) diff --git a/internal/domain/auth/service.go b/internal/domain/auth/service.go index 936ed7f..81421fe 100644 --- a/internal/domain/auth/service.go +++ b/internal/domain/auth/service.go @@ -1,10 +1,6 @@ package domain -import ( - domain "github.com/haydenhargreaves/Potion/internal/domain/user" -) - type AuthService interface { GetGoogleAuthUrl() string - GoogleAuthSuccess(state, code string) (string, domain.User, domain.GoogleUserInfo, error) + GoogleAuthSuccess(state, code string) (string, error) }