Compare commits

...

4 Commits

Author SHA1 Message Date
1e4cf8f922 Merge pull request '(FEAT): Errors can now be displayed using the RenderErrorBanner fx.' (#44) from feature/errors into master
All checks were successful
Deploy application with Docker / build_and_deploy (push) Successful in 1m56s
Reviewed-on: #44
2025-09-04 20:26:05 -07:00
Hayden Hargreaves
1e6a06e8ed (FEAT): Added errors to each of the handlers.
I think this is really the only place we need them. For now at least.
2025-09-04 20:25:12 -07:00
Hayden Hargreaves
6b030adf02 (DOC): Updated documentation 2025-09-03 22:25:13 -07:00
Hayden Hargreaves
47b8386844 (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.
2025-09-03 22:22:23 -07:00
12 changed files with 295 additions and 50 deletions

View File

@ -6,6 +6,7 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
domain "github.com/haydenhargreaves/Potion/internal/domain/server" 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 // 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 // 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. // 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. // We do not need to return all of this data, it is just for testing.
func GoogleCallback(ctx *gin.Context) { func GoogleCallback(ctx *gin.Context) {
deps := ctx.MustGet("deps").(*domain.InjectedDependencies) deps := ctx.MustGet("deps").(*domain.InjectedDependencies)
@ -32,15 +31,11 @@ func GoogleCallback(ctx *gin.Context) {
code string = ctx.Query("code") code string = ctx.Query("code")
) )
// TODO: Do something real, not just return data if jwt, err := deps.AuthService.GoogleAuthSuccess(state, code); err != nil {
if jwt, dbUser, googleUserInfo, err := deps.AuthService.GoogleAuthSuccess(state, code); err != nil { components.RenderErrorBanner(ctx, err.Error())
ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
} else { } else {
domain.SetCookie(ctx, "jwt_token", jwt, time.Hour*24*7) 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, "/") ctx.Redirect(http.StatusSeeOther, "/")
} }
} }

View File

@ -7,6 +7,7 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
domain "github.com/haydenhargreaves/Potion/internal/domain/server" domain "github.com/haydenhargreaves/Potion/internal/domain/server"
"github.com/haydenhargreaves/Potion/internal/templates/components"
) )
func EngagementViewRecipe(ctx *gin.Context) { func EngagementViewRecipe(ctx *gin.Context) {
@ -23,6 +24,7 @@ func EngagementViewRecipe(ctx *gin.Context) {
if !domain.IsLoggedIn(ctx) || user == nil { if !domain.IsLoggedIn(ctx) || user == nil {
if _, err := deps.EngagementService.ViewRecipe(recipeId); err != nil { if _, err := deps.EngagementService.ViewRecipe(recipeId); err != nil {
components.RenderErrorBanner(ctx, err.Error())
ctx.JSON(http.StatusInternalServerError, gin.H{ ctx.JSON(http.StatusInternalServerError, gin.H{
"status": http.StatusInternalServerError, "status": http.StatusInternalServerError,
"message": err.Error(), "message": err.Error(),
@ -36,6 +38,7 @@ func EngagementViewRecipe(ctx *gin.Context) {
// We caught nil already, we can assume the user exists // We caught nil already, we can assume the user exists
if _, err := deps.EngagementService.UserViewRecipe(user.Id, recipeId); err != nil { if _, err := deps.EngagementService.UserViewRecipe(user.Id, recipeId); err != nil {
components.RenderErrorBanner(ctx, err.Error())
ctx.JSON(http.StatusInternalServerError, gin.H{ ctx.JSON(http.StatusInternalServerError, gin.H{
"status": http.StatusInternalServerError, "status": http.StatusInternalServerError,
"message": err.Error(), "message": err.Error(),
@ -60,6 +63,7 @@ func EngagementShareRecipe(ctx *gin.Context) {
if !domain.IsLoggedIn(ctx) || user == nil { if !domain.IsLoggedIn(ctx) || user == nil {
if _, err := deps.EngagementService.ShareRecipe(recipeId); err != nil { if _, err := deps.EngagementService.ShareRecipe(recipeId); err != nil {
components.RenderErrorBanner(ctx, err.Error())
ctx.JSON(http.StatusInternalServerError, gin.H{ ctx.JSON(http.StatusInternalServerError, gin.H{
"status": http.StatusInternalServerError, "status": http.StatusInternalServerError,
"message": err.Error(), "message": err.Error(),
@ -71,6 +75,7 @@ func EngagementShareRecipe(ctx *gin.Context) {
} }
if _, err := deps.EngagementService.UserShareRecipe(user.Id, recipeId); err != nil { if _, err := deps.EngagementService.UserShareRecipe(user.Id, recipeId); err != nil {
components.RenderErrorBanner(ctx, err.Error())
ctx.JSON(http.StatusInternalServerError, gin.H{ ctx.JSON(http.StatusInternalServerError, gin.H{
"status": http.StatusInternalServerError, "status": http.StatusInternalServerError,
"message": err.Error(), "message": err.Error(),
@ -101,6 +106,7 @@ func EngagementFavoriteRecipe(ctx *gin.Context) {
recipeId, _ := strconv.Atoi(id) recipeId, _ := strconv.Atoi(id)
if _, err := deps.EngagementService.UserFavoriteRecipe(user.Id, recipeId); err != nil { 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{ ctx.JSON(http.StatusInternalServerError, gin.H{
"status": http.StatusInternalServerError, "status": http.StatusInternalServerError,
"message": err.Error(), "message": err.Error(),
@ -131,6 +137,7 @@ func EngagementMakeRecipe(ctx *gin.Context) {
recipeId, _ := strconv.Atoi(id) recipeId, _ := strconv.Atoi(id)
if _, err := deps.EngagementService.UserMakeRecipe(user.Id, recipeId); err != nil { if _, err := deps.EngagementService.UserMakeRecipe(user.Id, recipeId); err != nil {
components.RenderErrorBanner(ctx, err.Error())
ctx.JSON(http.StatusInternalServerError, gin.H{ ctx.JSON(http.StatusInternalServerError, gin.H{
"status": http.StatusInternalServerError, "status": http.StatusInternalServerError,
"message": err.Error(), "message": err.Error(),

View File

@ -11,6 +11,7 @@ import (
domainRecipe "github.com/haydenhargreaves/Potion/internal/domain/recipe" domainRecipe "github.com/haydenhargreaves/Potion/internal/domain/recipe"
domain "github.com/haydenhargreaves/Potion/internal/domain/server" domain "github.com/haydenhargreaves/Potion/internal/domain/server"
domainServer "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" layouts "github.com/haydenhargreaves/Potion/internal/templates/layouts"
pages "github.com/haydenhargreaves/Potion/internal/templates/pages" pages "github.com/haydenhargreaves/Potion/internal/templates/pages"
templates "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) userId := ctx.MustGet("userId").(int)
madeRecipes, err := deps.RecipeService.GetUserMadeRecipes(userId, 6) madeRecipes, err := deps.RecipeService.GetUserMadeRecipes(userId, 6)
if err != nil { if err != nil {
components.RenderErrorBanner(ctx, fmt.Sprintf("Error getting made recipes. %s\n", err.Error()))
ctx.JSON(http.StatusInternalServerError, gin.H{ ctx.JSON(http.StatusInternalServerError, gin.H{
"status": http.StatusInternalServerError, "status": http.StatusInternalServerError,
"message": fmt.Sprintf("Error getting made recipes. %s\n", err.Error()), "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) viewedRecipes, err := deps.RecipeService.GetUserViewedRecipes(userId, 6)
if err != nil { if err != nil {
components.RenderErrorBanner(ctx, fmt.Sprintf("Error getting viewed recipes. %s\n", err.Error()))
ctx.JSON(http.StatusInternalServerError, gin.H{ ctx.JSON(http.StatusInternalServerError, gin.H{
"status": http.StatusInternalServerError, "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 return
} }
@ -59,9 +62,10 @@ func HomePage(ctx *gin.Context) {
// Get the recipe of the week // Get the recipe of the week
recipeOfTheWeek, err := deps.RecipeService.GetRecipeOfTheWeek(&userId) recipeOfTheWeek, err := deps.RecipeService.GetRecipeOfTheWeek(&userId)
if err != 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{ ctx.JSON(http.StatusInternalServerError, gin.H{
"status": http.StatusInternalServerError, "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 return
} }
@ -82,9 +86,10 @@ func HomePage(ctx *gin.Context) {
// Get the recipe of the week // Get the recipe of the week
recipeOfTheWeek, err := deps.RecipeService.GetRecipeOfTheWeek(nil) recipeOfTheWeek, err := deps.RecipeService.GetRecipeOfTheWeek(nil)
if err != 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{ ctx.JSON(http.StatusInternalServerError, gin.H{
"status": http.StatusInternalServerError, "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 return
} }
@ -178,6 +183,7 @@ func ProfilePage(ctx *gin.Context) {
recipes, err := deps.RecipeService.GetUserRecipes(user.Id) recipes, err := deps.RecipeService.GetUserRecipes(user.Id)
if err != nil { if err != nil {
components.RenderErrorBanner(ctx, fmt.Sprintf("Error getting recipes. %s\n", err.Error()))
ctx.JSON(http.StatusInternalServerError, gin.H{ ctx.JSON(http.StatusInternalServerError, gin.H{
"status": http.StatusInternalServerError, "status": http.StatusInternalServerError,
"message": fmt.Sprintf("Error getting recipes. %s\n", err.Error()), "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) favorites, err := deps.RecipeService.GetUserFavoriteRecipes(user.Id)
if err != nil { if err != nil {
components.RenderErrorBanner(ctx, fmt.Sprintf("Error getting favorite recipes. %s\n", err.Error()))
ctx.JSON(http.StatusInternalServerError, gin.H{ ctx.JSON(http.StatusInternalServerError, gin.H{
"status": http.StatusInternalServerError, "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 return
} }
@ -197,6 +204,7 @@ func ProfilePage(ctx *gin.Context) {
// Get the engagement data, not sure what will happen when errors occur // Get the engagement data, not sure what will happen when errors occur
engagements, err := deps.EngagementService.GetUserEngagement(user.Id, 6) engagements, err := deps.EngagementService.GetUserEngagement(user.Id, 6)
if err != nil { if err != nil {
components.RenderErrorBanner(ctx, fmt.Sprintf("Error getting user engagements. %s\n", err.Error()))
ctx.JSON(http.StatusInternalServerError, gin.H{ ctx.JSON(http.StatusInternalServerError, gin.H{
"status": http.StatusInternalServerError, "status": http.StatusInternalServerError,
"message": fmt.Sprintf("Error getting user engagements. %s\n", err.Error()), "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)) 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) { func RecipePage(ctx *gin.Context) {
// Call recipe service to get via ID // Call recipe service to get via ID
deps := ctx.MustGet("deps").(*domainServer.InjectedDependencies) deps := ctx.MustGet("deps").(*domainServer.InjectedDependencies)
@ -226,7 +233,7 @@ func RecipePage(ctx *gin.Context) {
// Parse ID // Parse ID
parsed, err := strconv.Atoi(id) parsed, err := strconv.Atoi(id)
if err != nil { if err != nil {
fmt.Printf("ERROR: %s\n", err.Error()) components.RenderErrorBanner(ctx, fmt.Sprintf("ERROR: %s", err.Error()))
ctx.JSON(400, err.Error()) ctx.JSON(400, err.Error())
return return
} }
@ -251,7 +258,7 @@ func RecipePage(ctx *gin.Context) {
// Get recipe // Get recipe
recipe, err := deps.RecipeService.GetRecipe(parsed, userId) recipe, err := deps.RecipeService.GetRecipe(parsed, userId)
if err != nil { if err != nil {
fmt.Printf("ERROR: %s\n", err.Error()) components.RenderErrorBanner(ctx, fmt.Sprintf("ERROR: %s", err.Error()))
ctx.JSON(400, err.Error()) ctx.JSON(400, err.Error())
return return
} }
@ -259,7 +266,7 @@ func RecipePage(ctx *gin.Context) {
// Get user (owner) // Get user (owner)
user, err := deps.UserService.GetUser(recipe.UserId) user, err := deps.UserService.GetUser(recipe.UserId)
if err != nil { if err != nil {
fmt.Printf("ERROR: %s\n", err.Error()) components.RenderErrorBanner(ctx, fmt.Sprintf("ERROR: %s", err.Error()))
ctx.JSON(400, err.Error()) ctx.JSON(400, err.Error())
return return
} }

View File

@ -10,6 +10,7 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
domainRecipe "github.com/haydenhargreaves/Potion/internal/domain/recipe" domainRecipe "github.com/haydenhargreaves/Potion/internal/domain/recipe"
domain "github.com/haydenhargreaves/Potion/internal/domain/server" domain "github.com/haydenhargreaves/Potion/internal/domain/server"
"github.com/haydenhargreaves/Potion/internal/templates/components"
templates "github.com/haydenhargreaves/Potion/internal/templates/pages" templates "github.com/haydenhargreaves/Potion/internal/templates/pages"
) )
@ -24,6 +25,7 @@ func CreateRecipe(ctx *gin.Context) {
recipe, err := deps.RecipeService.CreateRecipe(ctx) recipe, err := deps.RecipeService.CreateRecipe(ctx)
if err != nil { if err != nil {
components.RenderErrorBanner(ctx, err.Error())
ctx.String(http.StatusOK, CREATE_ERROR_HTML, err.Error()) ctx.String(http.StatusOK, CREATE_ERROR_HTML, err.Error())
return return
} }
@ -89,6 +91,7 @@ func SearchRecipes(ctx *gin.Context) {
// We don't care about favorite status, so use false // We don't care about favorite status, so use false
recipes, err := deps.RecipeService.SearchRecipes(filters, userId, false) recipes, err := deps.RecipeService.SearchRecipes(filters, userId, false)
if err != nil { if err != nil {
components.RenderErrorBanner(ctx, err.Error())
ctx.JSON(http.StatusOK, gin.H{"error": 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? // TODO: Error here if they're not logged in?
// Get user data (they should be logged in) // Get user data (they should be logged in)
if !domain.IsLoggedIn(ctx) { 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."}) 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) recipes, err := deps.RecipeService.SearchRecipes(filters, &userId, true)
if err != nil { if err != nil {
components.RenderErrorBanner(ctx, err.Error())
ctx.JSON(http.StatusOK, gin.H{"error": err.Error()}) ctx.JSON(http.StatusOK, gin.H{"error": err.Error()})
} }

View File

@ -6,6 +6,7 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
domain "github.com/haydenhargreaves/Potion/internal/domain/server" domain "github.com/haydenhargreaves/Potion/internal/domain/server"
"github.com/haydenhargreaves/Potion/internal/templates/components"
) )
func GetUserRecipes(ctx *gin.Context) { func GetUserRecipes(ctx *gin.Context) {
@ -21,6 +22,7 @@ func GetUserRecipes(ctx *gin.Context) {
// Ensure logged in // Ensure logged in
if !domain.IsLoggedIn(ctx) || user == nil { 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{ ctx.JSON(http.StatusUnauthorized, gin.H{
"status": http.StatusUnauthorized, "status": http.StatusUnauthorized,
"message": "User is not authorized to access this endpoint. Please login to continue.", "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) recipes, err := deps.RecipeService.GetUserRecipes(user.Id)
if err != nil { if err != nil {
components.RenderErrorBanner(ctx, fmt.Sprintf("Could not get user recipes. %s", err.Error()))
ctx.JSON(http.StatusBadRequest, gin.H{ ctx.JSON(http.StatusBadRequest, gin.H{
"status": http.StatusBadRequest, "status": http.StatusBadRequest,
"message": fmt.Sprintf("Could not get user recipes. %s", err.Error()), "message": fmt.Sprintf("Could not get user recipes. %s", err.Error()),
@ -59,6 +62,7 @@ func GetUserFavoriteRecipes(ctx *gin.Context) {
// Ensure logged in // Ensure logged in
if !domain.IsLoggedIn(ctx) || user == nil { 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{ ctx.JSON(http.StatusUnauthorized, gin.H{
"status": http.StatusUnauthorized, "status": http.StatusUnauthorized,
"message": "User is not authorized to access this endpoint. Please login to continue.", "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) recipes, err := deps.RecipeService.GetUserFavoriteRecipes(user.Id)
if err != nil { if err != nil {
components.RenderErrorBanner(ctx, fmt.Sprintf("Could not get favorite recipes. %s", err.Error()))
ctx.JSON(http.StatusBadRequest, gin.H{ ctx.JSON(http.StatusBadRequest, gin.H{
"status": http.StatusBadRequest, "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, "recipes": nil,
}) })
return return

View File

@ -60,44 +60,44 @@ func (s *AuthService) GetGoogleAuthUrl() string {
// GoogleAuthSuccess accepts the data from the Google login endpoint and uses it to fetch the users // 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. // 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 // Ensure the state matches, prevents M.I.T.M. attacks
if state != "randomstate" { 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 // Get access token from Google
token, err := auth.GoogleAuthConfig.Exchange(context.Background(), code) token, err := auth.GoogleAuthConfig.Exchange(context.Background(), code)
if err != nil { 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 // Use the access token to get user data
googleUserInfo, err := auth.GetUserData(token.AccessToken) googleUserInfo, err := auth.GetUserData(token.AccessToken)
if err != nil { if err != nil {
return "", domain.User{}, domain.GoogleUserInfo{}, err return "", err
} }
// Attempt to get the user, user is nil when they don't exit // Attempt to get the user, user is nil when they don't exit
user, err := s.userRepository.GetGoogleUser(googleUserInfo.Id) user, err := s.userRepository.GetGoogleUser(googleUserInfo.Id)
if err != nil { 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 // A user was found
if user != nil { if user != nil {
jwt, err := generateJwt(user.Id, user.Email, s.jwtSecret) 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 // user did not exist, need to create one
newUser, err := s.userRepository.CreateGoogleUser(&googleUserInfo, token.RefreshToken) newUser, err := s.userRepository.CreateGoogleUser(&googleUserInfo, token.RefreshToken)
if err != nil { 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) 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 // generateJwt requires user data and returns a JSON web token which can be stored in the browsers

View File

@ -1,10 +1,6 @@
package domain package domain
import (
domain "github.com/haydenhargreaves/Potion/internal/domain/user"
)
type AuthService interface { type AuthService interface {
GetGoogleAuthUrl() string GetGoogleAuthUrl() string
GoogleAuthSuccess(state, code string) (string, domain.User, domain.GoogleUserInfo, error) GoogleAuthSuccess(state, code string) (string, error)
} }

View File

@ -0,0 +1,50 @@
package components
import "github.com/gin-gonic/gin"
templ errorIcon() {
<svg class="h-6 text-red-500" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M12 16.99V17M12 7V14M21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3C16.9706 3 21 7.02944 21 12Z"
stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path>
</svg>
}
templ closeButton() {
<button onclick="hideError();"
class="text-red-500 ml-auto hover:bg-red-200 p-1 rounded-sm transition-all duration-300 cursor-pointer">
<svg class="h-4" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd"
d="M19.207 6.207a1 1 0 0 0-1.414-1.414L12 10.586 6.207 4.793a1 1 0 0 0-1.414 1.414L10.586 12l-5.793 5.793a1 1 0 1 0 1.414 1.414L12 13.414l5.793 5.793a1 1 0 0 0 1.414-1.414L13.414 12l5.793-5.793z"
fill="currentColor"></path>
</svg>
</button>
}
templ errorBanner(message string) {
<div id="error-toast" hx-swap-oob="outerHTML"
class="fixed z-20 border border-red-500 rounded-sm right-0 top-0 m-4 p-4 bg-red-100 shadow shadow-red-200 transition-all duration-300 text-sm md:w-1/3">
<div class="flex items-center gap-x-2 pb-1">
@errorIcon()
<h1 class="text-red-500 font-semibold">Error</h1>
@closeButton()
</div>
<div class="ml-8">
<p class="text-red-500 text-sm">
{ message }
</p>
</div>
</div>
}
// 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)
}

View File

@ -0,0 +1,141 @@
// 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, "<svg class=\"h-6 text-red-500\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M12 16.99V17M12 7V14M21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3C16.9706 3 21 7.02944 21 12Z\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"></path></svg>")
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, "<button onclick=\"hideError();\" class=\"text-red-500 ml-auto hover:bg-red-200 p-1 rounded-sm transition-all duration-300 cursor-pointer\"><svg class=\"h-4\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\"><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M19.207 6.207a1 1 0 0 0-1.414-1.414L12 10.586 6.207 4.793a1 1 0 0 0-1.414 1.414L10.586 12l-5.793 5.793a1 1 0 1 0 1.414 1.414L12 13.414l5.793 5.793a1 1 0 0 0 1.414-1.414L13.414 12l5.793-5.793z\" fill=\"currentColor\"></path></svg></button>")
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, "<div id=\"error-toast\" hx-swap-oob=\"outerHTML\" class=\"fixed z-20 border border-red-500 rounded-sm right-0 top-0 m-4 p-4 bg-red-100 shadow shadow-red-200 transition-all duration-300 text-sm md:w-1/3\"><div class=\"flex items-center gap-x-2 pb-1\">")
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, "<h1 class=\"text-red-500 font-semibold\">Error</h1>")
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, "</div><div class=\"ml-8\"><p class=\"text-red-500 text-sm\">")
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: 34, Col: 15}
}
_, 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, "</p></div></div>")
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.
//
// 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)
}
var _ = templruntime.GeneratedTemplate

View File

@ -3,20 +3,24 @@ package templates
// AppLayout is the main application layout, this does not contain any content other than // 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. // meta data, links, scripts and whatever is passed into it as a component.
templ AppLayout(title string, child templ.Component) { templ AppLayout(title string, child templ.Component) {
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head>
<head> <meta charset="UTF-8"/>
<meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{ title }</title> <title>{ title }</title>
<link rel="stylesheet" href="/v1/web/static/css/tailwind.css" /> <link rel="stylesheet" href="/v1/web/static/css/tailwind.css"/>
<script src="https://unpkg.com/htmx.org@2.0.4"></script> <script src="https://unpkg.com/htmx.org@2.0.4"></script>
</head> </head>
<body class="bg-gray-100">
<body class="bg-gray-100"> <div id="error-toast"></div>
@child @child
</body> <script>
function hideError() {
</html> const err = document.getElementById("error-toast");
err.classList.add("hidden");
}
</script>
</body>
</html>
} }

View File

@ -38,13 +38,13 @@ func AppLayout(title string, child templ.Component) templ.Component {
var templ_7745c5c3_Var2 string var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(title) templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(title)
if templ_7745c5c3_Err != nil { 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)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</title><link rel=\"stylesheet\" href=\"/v1/web/static/css/tailwind.css\"><script src=\"https://unpkg.com/htmx.org@2.0.4\"></script></head><body class=\"bg-gray-100\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</title><link rel=\"stylesheet\" href=\"/v1/web/static/css/tailwind.css\"><script src=\"https://unpkg.com/htmx.org@2.0.4\"></script></head><body class=\"bg-gray-100\"><div id=\"error-toast\"></div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@ -52,7 +52,7 @@ func AppLayout(title string, child templ.Component) templ.Component {
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</body></html>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<script>\n function hideError() {\n const err = document.getElementById(\"error-toast\");\n err.classList.add(\"hidden\");\n }\n </script></body></html>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }

View File

@ -8,6 +8,7 @@
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New',
monospace; monospace;
--color-red-100: oklch(93.6% 0.032 17.717); --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-red-500: oklch(63.7% 0.237 25.331);
--color-green-500: oklch(72.3% 0.219 149.579); --color-green-500: oklch(72.3% 0.219 149.579);
--color-blue-50: oklch(97% 0.014 254.604); --color-blue-50: oklch(97% 0.014 254.604);
@ -235,18 +236,27 @@
.absolute { .absolute {
position: absolute; position: absolute;
} }
.fixed {
position: fixed;
}
.relative { .relative {
position: relative; position: relative;
} }
.static { .static {
position: static; position: static;
} }
.top-0 {
top: calc(var(--spacing) * 0);
}
.top-1\/2 { .top-1\/2 {
top: calc(1/2 * 100%); top: calc(1/2 * 100%);
} }
.top-\[100\%\] { .top-\[100\%\] {
top: 100%; top: 100%;
} }
.right-0 {
right: calc(var(--spacing) * 0);
}
.left-0 { .left-0 {
left: calc(var(--spacing) * 0); left: calc(var(--spacing) * 0);
} }
@ -262,6 +272,9 @@
.z-20 { .z-20 {
z-index: 20; z-index: 20;
} }
.m-4 {
margin: calc(var(--spacing) * 4);
}
.mx-2 { .mx-2 {
margin-inline: calc(var(--spacing) * 2); margin-inline: calc(var(--spacing) * 2);
} }
@ -337,6 +350,12 @@
.mb-16 { .mb-16 {
margin-bottom: calc(var(--spacing) * 16); margin-bottom: calc(var(--spacing) * 16);
} }
.ml-8 {
margin-left: calc(var(--spacing) * 8);
}
.ml-auto {
margin-left: auto;
}
.block { .block {
display: block; display: block;
} }
@ -694,6 +713,9 @@
--tw-gradient-to: var(--color-purple-200); --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)); --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 { .p-2 {
padding: calc(var(--spacing) * 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); --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 {
outline-style: var(--tw-outline-style); outline-style: var(--tw-outline-style);
outline-width: 1px; 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\:text-blue-400 {
&:hover { &:hover {
@media (hover: hover) { @media (hover: hover) {