From ca484e95a1f26590cc42739977a716513d7d5c6f Mon Sep 17 00:00:00 2001 From: Teajey <21069848+Teajey@users.noreply.github.com> Date: Wed, 4 Dec 2024 20:52:56 +0900 Subject: [PATCH] feat: use rsvp module --- .gitignore | 1 + server/api/login.go | 4 +- server/db/group.go | 10 +-- server/db/user.go | 40 ++-------- server/error/page.go | 19 ----- server/main.go | 44 +++++------ server/router/router.go | 39 ++------- server/routing/context.go | 34 +++----- server/routing/foreign_wishlist.go | 32 +++----- server/routing/groups.go | 87 ++++++++------------ server/routing/home.go | 34 +++----- server/routing/login.go | 56 ++++--------- server/routing/logout.go | 19 ++--- server/routing/not_found.go | 12 +-- server/routing/redirect_with_error.go | 17 ---- server/routing/register.go | 46 +++++------ server/routing/todo.go | 28 +++---- server/routing/users.go | 52 +++++------- server/routing/wishlist.go | 71 +++++++---------- server/rsvp/handler.go | 56 +++++++++++++ server/rsvp/request.go | 42 ++++++++++ server/rsvp/response.go | 110 ++++++++++++++++++++++++++ server/rsvp/session.go | 42 ++++++++++ server/templates/templates.go | 9 +-- 24 files changed, 466 insertions(+), 438 deletions(-) delete mode 100644 server/error/page.go delete mode 100644 server/routing/redirect_with_error.go create mode 100644 server/rsvp/handler.go create mode 100644 server/rsvp/request.go create mode 100644 server/rsvp/response.go create mode 100644 server/rsvp/session.go diff --git a/.gitignore b/.gitignore index 191c9ff..5a4c884 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ gin-bin lishwist.db .env*.local server/db/init_sql.go +.ignored/ diff --git a/server/api/login.go b/server/api/login.go index 4aed299..9e39d3a 100644 --- a/server/api/login.go +++ b/server/api/login.go @@ -9,8 +9,8 @@ import ( ) type LoginProps struct { - GeneralError string - SuccessfulRegistration bool + GeneralError string `json:",omitempty"` + SuccessfulRegistration bool `json:",omitempty"` Username templates.InputProps Password templates.InputProps } diff --git a/server/db/group.go b/server/db/group.go index 951a526..5b1efdc 100644 --- a/server/db/group.go +++ b/server/db/group.go @@ -40,27 +40,27 @@ func queryForGroup(query string, args ...any) (*Group, error) { func queryForGroups(query string, args ...any) ([]Group, error) { groups := []Group{} - rows, err := database.Query(query) + rows, err := database.Query(query, args...) if err != nil { - return groups, err + return groups, fmt.Errorf("Query failed: %w", err) } defer rows.Close() for rows.Next() { var group Group err := rows.Scan(&group.Id, &group.Name, &group.Reference) if err != nil { - return groups, err + return groups, fmt.Errorf("Failed to scan row: %w", err) } members, err := queryForGroupMembers(group.Id) if err != nil { - return groups, err + return groups, fmt.Errorf("Failed to query for group members: %w", err) } group.Members = members groups = append(groups, group) } err = rows.Err() if err != nil { - return groups, err + return groups, fmt.Errorf("Rows error: %w", err) } return groups, nil } diff --git a/server/db/user.go b/server/db/user.go index daca2f6..79975f9 100644 --- a/server/db/user.go +++ b/server/db/user.go @@ -18,14 +18,14 @@ type User struct { type Gift struct { Id string Name string - ClaimantId string - ClaimantName string + ClaimantId string `json:",omitempty"` + ClaimantName string `json:",omitempty"` Sent bool - RecipientId string - RecipientName string - RecipientRef string - CreatorId string - CreatorName string + RecipientId string `json:",omitempty"` + RecipientName string `json:",omitempty"` + RecipientRef string `json:",omitempty"` + CreatorId string `json:",omitempty"` + CreatorName string `json:",omitempty"` } func queryForUser(query string, args ...any) (*User, error) { @@ -406,31 +406,7 @@ func (u *User) AddGiftToUser(otherUserReference string, giftName string) error { func (u *User) GetGroups() ([]Group, error) { stmt := "SELECT [group].id, [group].name, [group].reference FROM [group] JOIN group_member ON group_member.group_id = [group].id JOIN v_user AS user ON user.id = group_member.user_id WHERE user.id = ?" - rows, err := database.Query(stmt, u.Id) - if err != nil { - return nil, err - } - defer rows.Close() - groups := []Group{} - for rows.Next() { - var id string - var name string - var reference string - err := rows.Scan(&id, &name, &reference) - if err != nil { - return nil, err - } - groups = append(groups, Group{ - Id: id, - Name: name, - Reference: reference, - }) - } - err = rows.Err() - if err != nil { - return nil, err - } - return groups, nil + return queryForGroups(stmt, u.Id) } func (u *User) GetGroupByReference(reference string) (*Group, error) { diff --git a/server/error/page.go b/server/error/page.go deleted file mode 100644 index 1a33b0d..0000000 --- a/server/error/page.go +++ /dev/null @@ -1,19 +0,0 @@ -package error - -import ( - "lishwist/templates" - "log" - "net/http" -) - -type pageProps struct { - Message string -} - -func Page(w http.ResponseWriter, publicMessage string, status int, err error) { - w.WriteHeader(status) - if err != nil { - log.Printf("%s --- %s\n", publicMessage, err) - } - templates.Execute(w, "error_page.gotmpl", pageProps{publicMessage}) -} diff --git a/server/main.go b/server/main.go index 20ee877..b0bb60a 100644 --- a/server/main.go +++ b/server/main.go @@ -35,32 +35,26 @@ func main() { r := router.New(store) - route := routing.NewContext(store) + r.Public.HandleFunc("GET /", routing.Login) + r.Public.HandleFunc("GET /group/{groupReference}", routing.PublicGroupPage) + r.Public.HandleFunc("GET /list/{userReference}", routing.PublicWishlist) + r.Public.HandleFunc("GET /register", routing.Register) + r.Public.HandleFunc("POST /", routing.LoginPost) + r.Public.HandleFunc("POST /register", routing.RegisterPost) - r.Html.Public.HandleFunc("GET /register", route.Register) - r.Html.Public.HandleFunc("POST /register", route.RegisterPost) - r.Html.Public.HandleFunc("GET /", route.Login) - r.Html.Public.HandleFunc("POST /", route.LoginPost) - r.Html.Public.HandleFunc("GET /list/{userReference}", route.PublicWishlist) - r.Html.Public.HandleFunc("GET /group/{groupReference}", route.PublicGroupPage) - - r.Html.Private.Handle("GET /{$}", route.ExpectUser(route.Home)) - r.Html.Private.Handle("POST /{$}", route.ExpectUser(route.HomePost)) - r.Html.Private.Handle("GET /list/{userReference}", route.ExpectUser(route.ForeignWishlist)) - r.Html.Private.Handle("POST /list/{userReference}", route.ExpectUser(route.ForeignWishlistPost)) - r.Html.Private.Handle("GET /group/{groupReference}", route.ExpectUser(route.GroupPage)) - r.Html.Private.HandleFunc("POST /logout", route.LogoutPost) - r.Html.Private.HandleFunc("GET /", routing.NotFound) - - r.Json.Public.HandleFunc("GET /", routing.NotFoundJson) - - r.Json.Private.Handle("GET /users", route.ExpectUser(route.UsersJson)) - r.Json.Private.Handle("GET /users/{userReference}", route.ExpectUser(route.User)) - r.Json.Private.Handle("POST /users/{userReference}", route.ExpectUser(route.UserPost)) - r.Json.Private.Handle("GET /groups", route.ExpectUser(route.GroupsJson)) - r.Json.Private.Handle("POST /groups/{groupReference}", route.ExpectUser(route.GroupPost)) - r.Json.Private.Handle("GET /groups/{groupReference}", route.ExpectUser(route.Group)) - r.Json.Private.HandleFunc("GET /", routing.NotFoundJson) + r.Private.HandleFunc("GET /", routing.NotFound) + r.Private.HandleFunc("GET /group/{groupReference}", routing.ExpectUser(routing.GroupPage)) + r.Private.HandleFunc("GET /groups", routing.ExpectUser(routing.GroupsJson)) + r.Private.HandleFunc("GET /groups/{groupReference}", routing.ExpectUser(routing.Group)) + r.Private.HandleFunc("GET /list/{userReference}", routing.ExpectUser(routing.ForeignWishlist)) + r.Private.HandleFunc("GET /users", routing.ExpectUser(routing.Users)) + r.Private.HandleFunc("GET /users/{userReference}", routing.ExpectUser(routing.User)) + r.Private.HandleFunc("GET /{$}", routing.ExpectUser(routing.Home)) + r.Private.HandleFunc("POST /groups/{groupReference}", routing.ExpectUser(routing.GroupPost)) + r.Private.HandleFunc("POST /list/{userReference}", routing.ExpectUser(routing.ForeignWishlistPost)) + r.Private.HandleFunc("POST /logout", routing.LogoutPost) + r.Private.HandleFunc("POST /users/{userReference}", routing.ExpectUser(routing.UserPost)) + r.Private.HandleFunc("POST /{$}", routing.ExpectUser(routing.HomePost)) http.Handle("/", r) diff --git a/server/router/router.go b/server/router/router.go index a90662b..56049d2 100644 --- a/server/router/router.go +++ b/server/router/router.go @@ -1,16 +1,16 @@ package router import ( + "lishwist/rsvp" "net/http" - "strings" "github.com/Teajey/sqlstore" ) type VisibilityRouter struct { Store *sqlstore.Store - Public *http.ServeMux - Private *http.ServeMux + Public *rsvp.ServeMux + Private *rsvp.ServeMux } func (s *VisibilityRouter) ServeHTTP(w http.ResponseWriter, r *http.Request) { @@ -24,33 +24,10 @@ func (s *VisibilityRouter) ServeHTTP(w http.ResponseWriter, r *http.Request) { } } -type Router struct { - Json VisibilityRouter - Html VisibilityRouter -} - -func (s *Router) ServeHTTP(w http.ResponseWriter, r *http.Request) { - accept := r.Header.Get("Accept") - - switch { - case strings.HasPrefix(accept, "application/json"): - s.Json.ServeHTTP(w, r) - default: - s.Html.ServeHTTP(w, r) - } -} - -func New(store *sqlstore.Store) *Router { - return &Router{ - Json: VisibilityRouter{ - Store: store, - Public: http.NewServeMux(), - Private: http.NewServeMux(), - }, - Html: VisibilityRouter{ - Store: store, - Public: http.NewServeMux(), - Private: http.NewServeMux(), - }, +func New(store *sqlstore.Store) *VisibilityRouter { + return &VisibilityRouter{ + Store: store, + Public: rsvp.NewServeMux(store), + Private: rsvp.NewServeMux(store), } } diff --git a/server/routing/context.go b/server/routing/context.go index b5c20b2..e8c9fb0 100644 --- a/server/routing/context.go +++ b/server/routing/context.go @@ -2,39 +2,23 @@ package routing import ( "lishwist/db" - "log" + "lishwist/rsvp" "net/http" - - "github.com/Teajey/sqlstore" ) -type Context struct { - store *sqlstore.Store -} - -func NewContext(store *sqlstore.Store) *Context { - return &Context{ - store, - } -} - -func (ctx *Context) ExpectUser(next func(*db.User, http.ResponseWriter, *http.Request)) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - session, _ := ctx.store.Get(r, "lishwist_user") - username, ok := session.Values["username"].(string) +func ExpectUser(next func(*db.User, http.Header, *rsvp.Request) rsvp.Response) rsvp.HandlerFunc { + return func(w http.Header, r *rsvp.Request) rsvp.Response { + session := r.GetSession() + username, ok := session.GetValue("username").(string) if !ok { - log.Println("Failed to get username") - http.Error(w, "", http.StatusInternalServerError) - return + return rsvp.Error(http.StatusInternalServerError, "Something went wrong.").Log("Failed to get username from session") } user, err := db.GetUserByName(username) if err != nil { - log.Printf("Failed to get user: %s\n", err) - http.Error(w, "", http.StatusInternalServerError) - return + return rsvp.Error(http.StatusInternalServerError, "Something went wrong.").Log("Failed to get user %q: %s", username, err) } - next(user, w, r) - }) + return next(user, w, r) + } } diff --git a/server/routing/foreign_wishlist.go b/server/routing/foreign_wishlist.go index 21ee556..99b8944 100644 --- a/server/routing/foreign_wishlist.go +++ b/server/routing/foreign_wishlist.go @@ -2,8 +2,7 @@ package routing import ( "lishwist/db" - "lishwist/error" - "lishwist/templates" + "lishwist/rsvp" "net/http" ) @@ -14,28 +13,24 @@ type foreignWishlistProps struct { Gifts []db.Gift } -func (ctx *Context) ForeignWishlist(currentUser *db.User, w http.ResponseWriter, r *http.Request) { +func ForeignWishlist(currentUser *db.User, h http.Header, r *rsvp.Request) rsvp.Response { userReference := r.PathValue("userReference") if currentUser.Reference == userReference { - http.Redirect(w, r, "/", http.StatusSeeOther) - return + return rsvp.SeeOther("/") } otherUser, err := db.GetUserByReference(userReference) if err != nil { - error.Page(w, "An error occurred while fetching this user :(", http.StatusInternalServerError, err) - return + return rsvp.Error(http.StatusInternalServerError, "An error occurred while fetching this user :(").Log("Couldn't get user by reference %q: %s", userReference, err) } if otherUser == nil { - error.Page(w, "User not found", http.StatusNotFound, err) - return + return rsvp.Error(http.StatusInternalServerError, "User not found") } gifts, err := currentUser.GetOtherUserGifts(userReference) if err != nil { - error.Page(w, "An error occurred while fetching this user's wishlist :(", http.StatusInternalServerError, err) - return + return rsvp.Error(http.StatusInternalServerError, "An error occurred while fetching this user :(").Log("%q couldn't get wishes of other user %q: %s", currentUser.Name, otherUser.Name, err) } p := foreignWishlistProps{CurrentUserId: currentUser.Id, CurrentUserName: currentUser.Name, Username: otherUser.Name, Gifts: gifts} - templates.Execute(w, "foreign_wishlist.gotmpl", p) + return rsvp.Data("foreign_wishlist.gotmpl", p) } type publicWishlistProps struct { @@ -43,22 +38,19 @@ type publicWishlistProps struct { GiftCount int } -func (ctx *Context) PublicWishlist(w http.ResponseWriter, r *http.Request) { +func PublicWishlist(h http.Header, r *rsvp.Request) rsvp.Response { userReference := r.PathValue("userReference") otherUser, err := db.GetUserByReference(userReference) if err != nil { - error.Page(w, "An error occurred while fetching this user :(", http.StatusInternalServerError, err) - return + return rsvp.Error(http.StatusInternalServerError, "An error occurred while fetching this user :(").Log("Couldn't get user by reference %q on public wishlist: %s", userReference, err) } if otherUser == nil { - error.Page(w, "User not found", http.StatusNotFound, err) - return + return rsvp.Error(http.StatusInternalServerError, "User not found") } giftCount, err := otherUser.CountGifts() if err != nil { - error.Page(w, "An error occurred while fetching data about this user :(", http.StatusInternalServerError, err) - return + return rsvp.Error(http.StatusInternalServerError, "An error occurred while fetching this user :(").Log("Couldn't get wishes of user %q on public wishlist: %s", otherUser.Name, err) } p := publicWishlistProps{Username: otherUser.Name, GiftCount: giftCount} - templates.Execute(w, "public_foreign_wishlist.gotmpl", p) + return rsvp.Data("public_foreign_wishlist.gotmpl", p) } diff --git a/server/routing/groups.go b/server/routing/groups.go index 5db3cc4..2bf1eaa 100644 --- a/server/routing/groups.go +++ b/server/routing/groups.go @@ -1,13 +1,11 @@ package routing import ( - "encoding/json" "net/http" "slices" "lishwist/db" - "lishwist/error" - "lishwist/templates" + "lishwist/rsvp" ) type GroupProps struct { @@ -15,16 +13,14 @@ type GroupProps struct { CurrentUsername string } -func (ctx *Context) GroupPage(currentUser *db.User, w http.ResponseWriter, r *http.Request) { +func GroupPage(currentUser *db.User, h http.Header, r *rsvp.Request) rsvp.Response { groupReference := r.PathValue("groupReference") group, err := currentUser.GetGroupByReference(groupReference) if err != nil { - error.Page(w, "An error occurred while fetching this group :(", http.StatusInternalServerError, err) - return + return rsvp.Error(http.StatusInternalServerError, "An error occurred while fetching this group :(").Log("Couldn't get group: %s", err) } if group == nil { - error.Page(w, "Group not found. (It might be because you're not a member)", http.StatusNotFound, nil) - return + return rsvp.Error(http.StatusNotFound, "Group not found. (It might be because you're not a member)").Log("Couldn't get group: %s", err) } index := group.MemberIndex(currentUser.Id) group.Members = slices.Delete(group.Members, index, index+1) @@ -32,68 +28,58 @@ func (ctx *Context) GroupPage(currentUser *db.User, w http.ResponseWriter, r *ht Group: group, CurrentUsername: currentUser.Name, } - templates.Execute(w, "group_page.gotmpl", p) + return rsvp.Data("group_page.gotmpl", p) } -func (ctx *Context) PublicGroupPage(w http.ResponseWriter, r *http.Request) { +func PublicGroupPage(h http.Header, r *rsvp.Request) rsvp.Response { groupReference := r.PathValue("groupReference") group, err := db.GetGroupByReference(groupReference) if err != nil { - error.Page(w, "An error occurred while fetching this group :(", http.StatusInternalServerError, err) - return + return rsvp.Error(http.StatusInternalServerError, "An error occurred while fetching this group :(").Log("Couldn't get group: %s", err) } p := GroupProps{ Group: group, } - templates.Execute(w, "public_group_page.gotmpl", p) + return rsvp.Data("public_group_page.gotmpl", p) } -func (ctx *Context) GroupPost(currentUser *db.User, w http.ResponseWriter, r *http.Request) { +func GroupPost(currentUser *db.User, h http.Header, r *rsvp.Request) rsvp.Response { if !currentUser.IsAdmin { - NotFoundJson(w, r) - return - } - if err := r.ParseForm(); err != nil { - writeGeneralErrorJson(w, http.StatusBadRequest, "Failed to parse form: "+err.Error()) - return + return NotFound(h, r) } + form := r.ParseForm() var group *db.Group reference := r.PathValue("groupReference") - name := r.Form.Get("name") - addUsers := r.Form["addUser"] - removeUsers := r.Form["removeUser"] + name := form.Get("name") + addUsers := form["addUser"] + removeUsers := form["removeUser"] if name != "" { createdGroup, err := db.CreateGroup(name, reference) if err != nil { - writeGeneralErrorJson(w, http.StatusBadRequest, "Failed to create group: "+err.Error()) - return + return rsvp.Error(http.StatusInternalServerError, "Failed to create group: %w", err) } group = createdGroup } else { existingGroup, err := db.GetGroupByReference(reference) if err != nil { - writeGeneralErrorJson(w, http.StatusBadRequest, "Failed to get group: "+err.Error()) - return + return rsvp.Error(http.StatusInternalServerError, "Failed to get group: %w", err) } if existingGroup == nil { - writeGeneralErrorJson(w, http.StatusNotFound, "Group not found") - return + return rsvp.Error(http.StatusNotFound, "Group not found", err) } group = existingGroup for _, userId := range removeUsers { index := group.MemberIndex(userId) if index == -1 { - writeGeneralErrorJson(w, http.StatusBadRequest, "Group %q does not contain a user with id %s", reference, userId) - return + return rsvp.Error(http.StatusBadRequest, "Group %q does not contain a user with id %s", reference, userId) } err = group.RemoveUser(userId) if err != nil { - writeGeneralErrorJson(w, http.StatusBadRequest, "On group %q failed to remove user with id %s: %s", reference, userId, err) - return + return rsvp.Error(http.StatusInternalServerError, "On group %q failed to remove user with id %s: %s", reference, userId, err) } group.Members = slices.Delete(group.Members, index, index+1) } @@ -102,54 +88,47 @@ func (ctx *Context) GroupPost(currentUser *db.User, w http.ResponseWriter, r *ht for _, userId := range addUsers { user, err := db.GetUser(userId) if err != nil { - writeGeneralErrorJson(w, http.StatusBadRequest, "Groups exists, but a user with id %s could not be fetched: %s", userId, err) - return + return rsvp.Error(http.StatusInternalServerError, "Groups exists, but a user with id %s could not be fetched: %s", userId, err) } if user == nil { - writeGeneralErrorJson(w, http.StatusBadRequest, "Groups exists, but a user with id %s does not exist", userId) - return + return rsvp.Error(http.StatusInternalServerError, "Groups exists, but a user with id %s does not exist", userId) } err = group.AddUser(user.Id) if err != nil { - writeGeneralErrorJson(w, http.StatusBadRequest, "Groups exists, but failed to add user with id %s: %s", userId, err) - return + return rsvp.Error(http.StatusInternalServerError, "Groups exists, but failed to add user with id %s: %s", userId, err) } group.Members = append(group.Members, *user) } - _ = json.NewEncoder(w).Encode(group) + return rsvp.Data("", group) } -func (ctx *Context) GroupsJson(currentUser *db.User, w http.ResponseWriter, r *http.Request) { +func GroupsJson(currentUser *db.User, h http.Header, r *rsvp.Request) rsvp.Response { if !currentUser.IsAdmin { - NotFoundJson(w, r) - return + return NotFound(h, r) } groups, err := db.GetAllGroups() if err != nil { - writeGeneralErrorJson(w, http.StatusBadRequest, "Failed to get groups: "+err.Error()) - return + return rsvp.Error(http.StatusInternalServerError, "Failed to get groups: %s", err) } - _ = json.NewEncoder(w).Encode(groups) + return rsvp.Data("", groups) } -func (ctx *Context) Group(currentUser *db.User, w http.ResponseWriter, r *http.Request) { +func Group(currentUser *db.User, h http.Header, r *rsvp.Request) rsvp.Response { if !currentUser.IsAdmin { - NotFoundJson(w, r) - return + return NotFound(h, r) } + groupReference := r.PathValue("groupReference") group, err := db.GetGroupByReference(groupReference) if err != nil { - writeGeneralErrorJson(w, http.StatusBadRequest, "Couldn't get group: %s", err) - return + return rsvp.Error(http.StatusInternalServerError, "Couldn't get group: %s", err) } if group == nil { - writeGeneralErrorJson(w, http.StatusNotFound, "Group not found.") - return + return rsvp.Error(http.StatusNotFound, "Group not found.") } - _ = json.NewEncoder(w).Encode(group) + return rsvp.Data("", group) } diff --git a/server/routing/home.go b/server/routing/home.go index 98e40d6..c2a5084 100644 --- a/server/routing/home.go +++ b/server/routing/home.go @@ -5,8 +5,7 @@ import ( "lishwist/db" "lishwist/env" - "lishwist/error" - "lishwist/templates" + "lishwist/rsvp" ) type HomeProps struct { @@ -18,40 +17,31 @@ type HomeProps struct { Groups []db.Group } -func (ctx *Context) Home(currentUser *db.User, w http.ResponseWriter, r *http.Request) { +func Home(currentUser *db.User, h http.Header, r *rsvp.Request) rsvp.Response { gifts, err := currentUser.GetGifts() if err != nil { - error.Page(w, "An error occurred while fetching your wishlist :(", http.StatusInternalServerError, err) - return + return rsvp.Error(http.StatusInternalServerError, "An error occurred while fetching your wishlist :(").Log("Failed to get gifts: %s", err) } todo, err := currentUser.GetTodo() if err != nil { - error.Page(w, "An error occurred while fetching your wishlist :(", http.StatusInternalServerError, err) - return + return rsvp.Error(http.StatusInternalServerError, "An error occurred while fetching your wishlist :(").Log("Failed to get todo: %s", err) } groups, err := currentUser.GetGroups() if err != nil { - error.Page(w, "An error occurred while fetching your wishlist :(", http.StatusInternalServerError, err) - return + return rsvp.Error(http.StatusInternalServerError, "An error occurred while fetching your wishlist :(").Log("Failed to get groups: %s", err) } p := HomeProps{Username: currentUser.Name, Gifts: gifts, Todo: todo, Reference: currentUser.Reference, HostUrl: env.HostUrl.String(), Groups: groups} - templates.Execute(w, "home.gotmpl", p) + return rsvp.Data("home.gotmpl", p) } -func (ctx *Context) HomePost(currentUser *db.User, w http.ResponseWriter, r *http.Request) { - if err := r.ParseForm(); err != nil { - http.Error(w, "Couldn't parse form", http.StatusBadRequest) - return - } - switch r.Form.Get("intent") { +func HomePost(currentUser *db.User, h http.Header, r *rsvp.Request) rsvp.Response { + form := r.ParseForm() + switch form.Get("intent") { case "add_idea": - ctx.WishlistAdd(currentUser, w, r) - return + return WishlistAdd(currentUser, h, r) case "delete_idea": - ctx.WishlistDelete(currentUser, w, r) - return + return WishlistDelete(currentUser, h, r) default: - ctx.TodoUpdate(currentUser, w, r) - return + return TodoUpdate(currentUser, h, r) } } diff --git a/server/routing/login.go b/server/routing/login.go index 27acdea..43cd32c 100644 --- a/server/routing/login.go +++ b/server/routing/login.go @@ -1,25 +1,17 @@ package routing import ( - "encoding/json" "lishwist/api" - sesh "lishwist/session" - "lishwist/templates" - "log" + "lishwist/rsvp" "net/http" ) -func (ctx *Context) Login(w http.ResponseWriter, r *http.Request) { - session, _ := ctx.store.Get(r, "lishwist_user") +func Login(h http.Header, r *rsvp.Request) rsvp.Response { + session := r.GetSession() props := api.NewLoginProps("", "") - flash, err := sesh.GetFirstFlash(w, r, session, "login_props") - if err != nil { - http.Error(w, "Something went wrong. Error code: Zuko", http.StatusInternalServerError) - return - } - + flash := session.FlashGet("login_props") flashProps, ok := flash.(*api.LoginProps) if ok { props.Username.Value = flashProps.Username.Value @@ -29,46 +21,30 @@ func (ctx *Context) Login(w http.ResponseWriter, r *http.Request) { props.Password.Error = flashProps.Password.Error } - flash, err = sesh.GetFirstFlash(w, r, session, "successful_registration") - if err != nil { - http.Error(w, "Something went wrong. Error code: Zuko", http.StatusInternalServerError) - return - } - + flash = session.FlashGet("successful_registration") successfulReg, _ := flash.(bool) if successfulReg { props.SuccessfulRegistration = true } - templates.Execute(w, "login.gotmpl", props) + return rsvp.Data("login.gotmpl", props).SaveSession(session) } -func (ctx *Context) LoginPost(w http.ResponseWriter, r *http.Request) { - if err := r.ParseForm(); err != nil { - http.Error(w, "Couldn't parse form", http.StatusBadRequest) - return - } +func LoginPost(h http.Header, r *rsvp.Request) rsvp.Response { + form := r.ParseForm() + session := r.GetSession() - username := r.Form.Get("username") - password := r.Form.Get("password") + username := form.Get("username") + password := form.Get("password") props := api.Login(username, password) if props != nil { - ctx.RedirectWithFlash(w, r, "/", "login_props", &props) - _ = json.NewEncoder(w).Encode(props) - return + return rsvp.SeeOther("/").SaveSession(session) } - // NOTE: Overwriting any existing cookie or session here. So we don't care if there's an error - session, _ := ctx.store.Get(r, "lishwist_user") - session.ID = "" - session.Values["authorized"] = true - session.Values["username"] = username - if err := session.Save(r, w); err != nil { - log.Println("Couldn't save session:", err) - http.Error(w, "Something went wrong. Error code: Zuko", http.StatusInternalServerError) - return - } + session.SetID("") + session.SetValue("authorized", true) + session.SetValue("username", username) - http.Redirect(w, r, r.URL.Path, http.StatusSeeOther) + return rsvp.SeeOther(r.URL().Path).SaveSession(session) } diff --git a/server/routing/logout.go b/server/routing/logout.go index 18b0ba8..5b4b19f 100644 --- a/server/routing/logout.go +++ b/server/routing/logout.go @@ -1,22 +1,15 @@ package routing import ( + "lishwist/rsvp" "net/http" ) -func (ctx *Context) LogoutPost(w http.ResponseWriter, r *http.Request) { - session, err := ctx.store.Get(r, "lishwist_user") - if err != nil { - http.Error(w, "Something went wrong. Error code: Iroh", http.StatusInternalServerError) - return - } +func LogoutPost(h http.Header, r *rsvp.Request) rsvp.Response { + session := r.GetSession() - session.Options.MaxAge = 0 - session.Values = nil - if err := session.Save(r, w); err != nil { - http.Error(w, "Something went wrong. Error code: Azula", http.StatusInternalServerError) - return - } + session.Options().MaxAge = 0 + session.ClearValues() - http.Redirect(w, r, "/", http.StatusSeeOther) + return rsvp.SeeOther("/").SaveSession(session) } diff --git a/server/routing/not_found.go b/server/routing/not_found.go index f8a6d35..6b2ed65 100644 --- a/server/routing/not_found.go +++ b/server/routing/not_found.go @@ -3,15 +3,9 @@ package routing import ( "net/http" - "lishwist/error" + "lishwist/rsvp" ) -func NotFoundJson(w http.ResponseWriter, r *http.Request) { - w.Header().Add("Content-Type", "application/json; charset=utf-8") - w.WriteHeader(http.StatusNotFound) - _, _ = w.Write([]byte(`{"GeneralError":"Not Found"}`)) -} - -func NotFound(w http.ResponseWriter, r *http.Request) { - error.Page(w, "404 -- Page not found", http.StatusNotFound, nil) +func NotFound(h http.Header, r *rsvp.Request) rsvp.Response { + return rsvp.Error(http.StatusNotFound, "Page not found") } diff --git a/server/routing/redirect_with_error.go b/server/routing/redirect_with_error.go deleted file mode 100644 index a19e244..0000000 --- a/server/routing/redirect_with_error.go +++ /dev/null @@ -1,17 +0,0 @@ -package routing - -import ( - "log" - "net/http" -) - -func (ctx *Context) RedirectWithFlash(w http.ResponseWriter, r *http.Request, url string, key string, flash any) { - session, _ := ctx.store.Get(r, "lishwist_user") - session.AddFlash(flash, key) - if err := session.Save(r, w); err != nil { - log.Println("Couldn't save session:", err) - http.Error(w, "Something went wrong. Error code: Zuko", http.StatusInternalServerError) - return - } - http.Redirect(w, r, url, http.StatusSeeOther) -} diff --git a/server/routing/register.go b/server/routing/register.go index 38c454c..33aded9 100644 --- a/server/routing/register.go +++ b/server/routing/register.go @@ -1,20 +1,19 @@ package routing import ( - "encoding/json" "lishwist/api" - "lishwist/templates" - "log" + "lishwist/rsvp" "net/http" ) -func (ctx *Context) Register(w http.ResponseWriter, r *http.Request) { +func Register(h http.Header, r *rsvp.Request) rsvp.Response { props := api.NewRegisterProps("", "", "") - session, _ := ctx.store.Get(r, "lishwist_user") - if flashes := session.Flashes("register_props"); len(flashes) > 0 { - log.Printf("Register found flashes: %#v\n", flashes) - flashProps, _ := flashes[0].(*api.RegisterProps) + session := r.GetSession() + flash := session.FlashGet("register_props") + + flashProps, _ := flash.(*api.RegisterProps) + if flashProps != nil { props.Username.Value = flashProps.Username.Value props.GeneralError = flashProps.GeneralError @@ -22,32 +21,25 @@ func (ctx *Context) Register(w http.ResponseWriter, r *http.Request) { props.ConfirmPassword.Error = flashProps.ConfirmPassword.Error } - if err := session.Save(r, w); err != nil { - log.Println("Couldn't save session:", err) - http.Error(w, "Something went wrong. Error code: Zuko", http.StatusInternalServerError) - return - } - - templates.Execute(w, "register.gotmpl", props) + return rsvp.Data("register.gotmpl", props).SaveSession(session) } -func (ctx *Context) RegisterPost(w http.ResponseWriter, r *http.Request) { - if err := r.ParseForm(); err != nil { - http.Error(w, "Couldn't parse form", http.StatusBadRequest) - return - } +func RegisterPost(h http.Header, r *rsvp.Request) rsvp.Response { + form := r.ParseForm() - username := r.Form.Get("username") - newPassword := r.Form.Get("newPassword") - confirmPassword := r.Form.Get("confirmPassword") + username := form.Get("username") + newPassword := form.Get("newPassword") + confirmPassword := form.Get("confirmPassword") props := api.Register(username, newPassword, confirmPassword) + s := r.GetSession() + if props != nil { - ctx.RedirectWithFlash(w, r, "/register", "register_props", &props) - _ = json.NewEncoder(w).Encode(props) - return + s.FlashSet(&props, "register_props") + return rsvp.SeeOther("/register").SaveSession(s) } - ctx.RedirectWithFlash(w, r, "/", "successful_registration", true) + s.FlashSet(true, "successful_registration") + return rsvp.SeeOther("/").SaveSession(s) } diff --git a/server/routing/todo.go b/server/routing/todo.go index c86e8f0..470aabf 100644 --- a/server/routing/todo.go +++ b/server/routing/todo.go @@ -2,34 +2,28 @@ package routing import ( "lishwist/db" - "log" + "lishwist/rsvp" "net/http" ) -func (ctx *Context) TodoUpdate(currentUser *db.User, w http.ResponseWriter, r *http.Request) { - if err := r.ParseForm(); err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - switch r.Form.Get("intent") { +func TodoUpdate(currentUser *db.User, h http.Header, r *rsvp.Request) rsvp.Response { + form := r.ParseForm() + + switch form.Get("intent") { case "unclaim_todo": - unclaims := r.Form["gift"] + unclaims := form["gift"] err := currentUser.ClaimGifts([]string{}, unclaims) if err != nil { - http.Error(w, "Failed to update claim...", http.StatusInternalServerError) - return + return rsvp.Error(http.StatusInternalServerError, "Failed to update claim...").LogError(err) } case "complete_todo": - claims := r.Form["gift"] + claims := form["gift"] err := currentUser.CompleteGifts(claims) if err != nil { - log.Printf("Failed to complete gifts: %s\n", err) - http.Error(w, "Failed to complete gifts...", http.StatusInternalServerError) - return + return rsvp.Error(http.StatusInternalServerError, "Failed to complete gifts...").LogError(err) } default: - http.Error(w, "Invalid intent", http.StatusBadRequest) - return + return rsvp.Error(http.StatusBadRequest, "Invalid intent") } - http.Redirect(w, r, "/", http.StatusSeeOther) + return rsvp.SeeOther("/") } diff --git a/server/routing/users.go b/server/routing/users.go index b108f5f..05fc35a 100644 --- a/server/routing/users.go +++ b/server/routing/users.go @@ -1,82 +1,70 @@ package routing import ( - "encoding/json" "lishwist/db" + "lishwist/rsvp" "net/http" ) -func (ctx *Context) UsersJson(currentUser *db.User, w http.ResponseWriter, r *http.Request) { +func Users(currentUser *db.User, h http.Header, r *rsvp.Request) rsvp.Response { if !currentUser.IsAdmin { - NotFoundJson(w, r) - return + return NotFound(h, r) } users, err := db.GetAllUsers() if err != nil { - writeGeneralErrorJson(w, http.StatusInternalServerError, "Failed to get users: "+err.Error()) - return + return rsvp.Error(http.StatusInternalServerError, "Failed to get users: %s", err) } - _ = json.NewEncoder(w).Encode(users) + return rsvp.Data("", users) } -func (ctx *Context) User(currentUser *db.User, w http.ResponseWriter, r *http.Request) { +func User(currentUser *db.User, h http.Header, r *rsvp.Request) rsvp.Response { if !currentUser.IsAdmin { - NotFoundJson(w, r) - return + return NotFound(h, r) } reference := r.PathValue("userReference") user, err := db.GetUserByReference(reference) if err != nil { - writeGeneralErrorJson(w, http.StatusInternalServerError, "Failed to get user: %s", err) - return + return rsvp.Error(http.StatusInternalServerError, "Failed to get user: %s", err) } if user == nil { - writeGeneralErrorJson(w, http.StatusNotFound, "User not found") - return + return rsvp.Error(http.StatusNotFound, "User not found") } - _ = json.NewEncoder(w).Encode(user) + return rsvp.Data("", user) } -func (ctx *Context) UserPost(currentUser *db.User, w http.ResponseWriter, r *http.Request) { +func UserPost(currentUser *db.User, h http.Header, r *rsvp.Request) rsvp.Response { if !currentUser.IsAdmin { - NotFoundJson(w, r) - return - } - if err := r.ParseForm(); err != nil { - writeGeneralErrorJson(w, http.StatusInternalServerError, "Failed to parse form: %s", err) - return + return NotFound(h, r) } + form := r.ParseForm() + reference := r.PathValue("userReference") if reference == currentUser.Reference { - writeGeneralErrorJson(w, http.StatusForbidden, "You cannot delete yourself.") - return + return rsvp.Error(http.StatusForbidden, "You cannot delete yourself.") } user, err := db.GetAnyUserByReference(reference) if err != nil { - writeGeneralErrorJson(w, http.StatusInternalServerError, "Failed to get user: %s", err) - return + return rsvp.Error(http.StatusInternalServerError, "Failed to get user: %s", err) } if user == nil { - writeGeneralErrorJson(w, http.StatusNotFound, "User not found") - return + return rsvp.Error(http.StatusNotFound, "User not found") } - intent := r.Form.Get("intent") + intent := form.Get("intent") if intent != "" { err = user.SetLive(intent != "delete") if err != nil { - writeGeneralErrorJson(w, http.StatusInternalServerError, "Failed to delete user: "+err.Error()) - return + return rsvp.Error(http.StatusInternalServerError, "Failed to delete user: %s", err) } } - _ = json.NewEncoder(w).Encode(user) + return rsvp.Data("", user) } diff --git a/server/routing/wishlist.go b/server/routing/wishlist.go index 394a5b7..798c6a5 100644 --- a/server/routing/wishlist.go +++ b/server/routing/wishlist.go @@ -2,82 +2,67 @@ package routing import ( "lishwist/db" - "lishwist/error" + "lishwist/rsvp" "net/http" ) -func (ctx *Context) WishlistAdd(currentUser *db.User, w http.ResponseWriter, r *http.Request) { - if err := r.ParseForm(); err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - newGiftName := r.Form.Get("gift_name") +func WishlistAdd(currentUser *db.User, h http.Header, r *rsvp.Request) rsvp.Response { + form := r.ParseForm() + newGiftName := form.Get("gift_name") err := currentUser.AddGift(newGiftName) if err != nil { - error.Page(w, "Failed to add gift.", http.StatusInternalServerError, err) - return + return rsvp.Error(http.StatusInternalServerError, "Failed to add gift.").LogError(err) } - http.Redirect(w, r, "/", http.StatusSeeOther) + return rsvp.SeeOther("/") } -func (ctx *Context) WishlistDelete(currentUser *db.User, w http.ResponseWriter, r *http.Request) { - if err := r.ParseForm(); err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - targets := r.Form["gift"] +func WishlistDelete(currentUser *db.User, h http.Header, r *rsvp.Request) rsvp.Response { + form := r.ParseForm() + targets := form["gift"] err := currentUser.RemoveGifts(targets...) if err != nil { - error.Page(w, "Failed to remove gifts.", http.StatusInternalServerError, err) - return + return rsvp.Error(http.StatusInternalServerError, "Failed to remove gifts.").LogError(err) } - http.Redirect(w, r, "/", http.StatusSeeOther) + return rsvp.SeeOther("/") } -func (ctx *Context) ForeignWishlistPost(currentUser *db.User, w http.ResponseWriter, r *http.Request) { - if err := r.ParseForm(); err != nil { - error.Page(w, "Failed to parse form...", http.StatusBadRequest, err) - return - } +func ForeignWishlistPost(currentUser *db.User, h http.Header, r *rsvp.Request) rsvp.Response { + form := r.ParseForm() userReference := r.PathValue("userReference") - switch r.Form.Get("intent") { + intent := form.Get("intent") + switch intent { case "claim": - claims := r.Form["unclaimed"] - unclaims := r.Form["claimed"] + claims := form["unclaimed"] + unclaims := form["claimed"] err := currentUser.ClaimGifts(claims, unclaims) if err != nil { - error.Page(w, "Failed to update claim...", http.StatusInternalServerError, err) - return + return rsvp.Error(http.StatusInternalServerError, "Failed to update claim...").LogError(err) } case "complete": - claims := r.Form["claimed"] + claims := form["claimed"] err := currentUser.CompleteGifts(claims) if err != nil { - error.Page(w, "Failed to complete gifts...", http.StatusInternalServerError, nil) - return + return rsvp.Error(http.StatusInternalServerError, "Failed to complete gifts...").LogError(err) } case "add": - giftName := r.Form.Get("gift_name") + giftName := form.Get("gift_name") if giftName == "" { - error.Page(w, "Gift name not provided", http.StatusBadRequest, nil) - return + return rsvp.Error(http.StatusBadRequest, "Gift name not provided") } err := currentUser.AddGiftToUser(userReference, giftName) if err != nil { - error.Page(w, "Failed to add gift idea to other user...", http.StatusInternalServerError, err) - return + return rsvp.Error(http.StatusInternalServerError, "Failed to add gift idea to other user...").LogError(err) } case "delete": - claims := r.Form["unclaimed"] - unclaims := r.Form["claimed"] + claims := form["unclaimed"] + unclaims := form["claimed"] gifts := append(claims, unclaims...) err := currentUser.RemoveGifts(gifts...) if err != nil { - error.Page(w, "Failed to remove gift idea for other user...", http.StatusInternalServerError, err) - return + return rsvp.Error(http.StatusInternalServerError, "Failed to remove gift idea for other user...").LogError(err) } default: - http.Error(w, "Invalid intent", http.StatusBadRequest) + return rsvp.Error(http.StatusBadRequest, "Invalid intent %q", intent) } - http.Redirect(w, r, "/list/"+userReference, http.StatusSeeOther) + return rsvp.SeeOther("/list/" + userReference) } diff --git a/server/rsvp/handler.go b/server/rsvp/handler.go new file mode 100644 index 0000000..c172834 --- /dev/null +++ b/server/rsvp/handler.go @@ -0,0 +1,56 @@ +package rsvp + +import ( + "log" + "net/http" + + "github.com/Teajey/sqlstore" +) + +type ServeMux struct { + inner *http.ServeMux + store *sqlstore.Store +} + +func NewServeMux(store *sqlstore.Store) *ServeMux { + return &ServeMux{ + inner: http.NewServeMux(), + store: store, + } +} + +func (m *ServeMux) ServeHTTP(w http.ResponseWriter, r *http.Request) { + m.inner.ServeHTTP(w, r) +} + +type Handler interface { + ServeHTTP(h http.Header, r *Request) Response +} + +type HandlerFunc func(h http.Header, r *Request) Response + +func (m *ServeMux) HandleFunc(pattern string, handler HandlerFunc) { + m.inner.HandleFunc(pattern, func(w http.ResponseWriter, stdReq *http.Request) { + r := wrapStdRequest(m.store, stdReq) + + response := handler(w.Header(), &r) + + err := response.Write(w, stdReq) + if err != nil { + response.Data = struct{ Message error }{err} + response.HtmlTemplateName = "error_page.gotmpl" + response.Status = http.StatusInternalServerError + } else { + return + } + err = response.Write(w, stdReq) + if err != nil { + log.Printf("Failed to write rsvp.Response to bytes: %s\n", err) + http.Error(w, "Failed to write response", http.StatusInternalServerError) + } + }) +} + +func (m *ServeMux) Handle(pattern string, handler Handler) { + m.HandleFunc(pattern, handler.ServeHTTP) +} diff --git a/server/rsvp/request.go b/server/rsvp/request.go new file mode 100644 index 0000000..7e387cd --- /dev/null +++ b/server/rsvp/request.go @@ -0,0 +1,42 @@ +package rsvp + +import ( + "log" + "net/http" + "net/url" + + "github.com/Teajey/sqlstore" +) + +type Request struct { + inner *http.Request + store *sqlstore.Store +} + +func wrapStdRequest(store *sqlstore.Store, r *http.Request) Request { + return Request{ + inner: r, + store: store, + } +} + +func (r *Request) GetSession() Session { + session, _ := r.store.Get(r.inner, "lishwist_user") + return Session{session} +} + +func (r *Request) ParseForm() url.Values { + err := r.inner.ParseForm() + if err != nil { + log.Printf("Failed to parse form: %s\n", err) + } + return r.inner.Form +} + +func (r *Request) PathValue(name string) string { + return r.inner.PathValue(name) +} + +func (r *Request) URL() *url.URL { + return r.inner.URL +} diff --git a/server/rsvp/response.go b/server/rsvp/response.go new file mode 100644 index 0000000..921e79e --- /dev/null +++ b/server/rsvp/response.go @@ -0,0 +1,110 @@ +package rsvp + +import ( + "bytes" + "encoding/json" + "fmt" + "lishwist/templates" + "log" + "net/http" + "strings" +) + +type Response struct { + HtmlTemplateName string + Data any + SeeOther string + Session *Session + Status int + LogMessage string +} + +func (res *Response) Write(w http.ResponseWriter, r *http.Request) error { + if res.Session != nil { + err := res.Session.inner.Save(r, w) + if err != nil { + return fmt.Errorf("Failed to write session: %w", err) + } + } + + if res.SeeOther != "" { + http.Redirect(w, r, res.SeeOther, http.StatusSeeOther) + return nil + } + + bodyBytes := bytes.NewBuffer([]byte{}) + accept := r.Header.Get("Accept") + + if res.LogMessage != "" { + log.Printf("%s --- %s\n", res.Data, res.LogMessage) + } + + if res.Status != 0 { + w.WriteHeader(res.Status) + } + + switch { + case strings.Contains(accept, "text/html"): + if res.HtmlTemplateName == "" { + err := json.NewEncoder(bodyBytes).Encode(res.Data) + if err != nil { + return err + } + } else { + err := templates.Execute(bodyBytes, res.HtmlTemplateName, res.Data) + if err != nil { + return err + } + } + case strings.Contains(accept, "application/json"): + err := json.NewEncoder(bodyBytes).Encode(res.Data) + if err != nil { + return err + } + default: + err := json.NewEncoder(bodyBytes).Encode(res.Data) + if err != nil { + return err + } + } + + _, err := w.Write(bodyBytes.Bytes()) + if err != nil { + log.Printf("Failed to write rsvp.Response to HTTP: %s\n", err) + } + return nil +} + +func Data(htmlTemplateName string, data any) Response { + return Response{ + HtmlTemplateName: htmlTemplateName, + Data: data, + } +} + +func (r Response) Log(format string, a ...any) Response { + r.LogMessage = fmt.Sprintf(format, a...) + return r +} + +func (r Response) LogError(err error) Response { + r.LogMessage = fmt.Sprintf("%s", err) + return r +} + +func (r Response) SaveSession(s Session) Response { + r.Session = &s + return r +} + +func SeeOther(url string) Response { + return Response{SeeOther: url} +} + +func Error(status int, format string, a ...any) Response { + return Response{ + Status: status, + HtmlTemplateName: "error_page.gotmpl", + Data: struct{ Message string }{fmt.Sprintf(format, a...)}, + } +} diff --git a/server/rsvp/session.go b/server/rsvp/session.go new file mode 100644 index 0000000..9761766 --- /dev/null +++ b/server/rsvp/session.go @@ -0,0 +1,42 @@ +package rsvp + +import ( + "github.com/gorilla/sessions" +) + +type Session struct { + inner *sessions.Session +} + +func (s *Session) FlashGet(key ...string) any { + list := s.inner.Flashes(key...) + if len(list) < 1 { + return nil + } else { + return list[0] + } +} + +func (s *Session) FlashSet(value any, key ...string) { + s.inner.AddFlash(value, key...) +} + +func (s *Session) SetID(value string) { + s.inner.ID = value +} + +func (s *Session) SetValue(key any, value any) { + s.inner.Values[key] = value +} + +func (s *Session) GetValue(key any) any { + return s.inner.Values[key] +} + +func (s *Session) ClearValues() { + s.inner.Values = nil +} + +func (s *Session) Options() *sessions.Options { + return s.inner.Options +} diff --git a/server/templates/templates.go b/server/templates/templates.go index 9dac3ff..4a77340 100644 --- a/server/templates/templates.go +++ b/server/templates/templates.go @@ -2,8 +2,7 @@ package templates import ( "fmt" - "log" - "net/http" + "io" "text/template" ) @@ -33,12 +32,12 @@ func (p *InputProps) Validate() bool { var tmpls map[string]*template.Template = loadTemplates() -func Execute(w http.ResponseWriter, name string, data any) { +func Execute(w io.Writer, name string, data any) error { err := tmpls[name].Execute(w, data) if err != nil { - log.Printf("Failed to execute '%s' template: %s\n", name, err) - return + return fmt.Errorf("Failed to execute '%s' template: %w\n", name, err) } + return nil } func loadTemplates() map[string]*template.Template { -- 2.40.1