Compare commits

...

2 Commits

Author SHA1 Message Date
Thomas Williams 2940ffa915 Merge pull request 'feat: use rsvp module' (#6) from response-middleware-1 into main
Reviewed-on: #6
2024-12-05 01:05:30 +13:00
Teajey ca484e95a1
feat: use rsvp module 2024-12-04 20:52:56 +09:00
24 changed files with 466 additions and 438 deletions

1
.gitignore vendored
View File

@ -3,3 +3,4 @@ gin-bin
lishwist.db
.env*.local
server/db/init_sql.go
.ignored/

View File

@ -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
}

View File

@ -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
}

View File

@ -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) {

View File

@ -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})
}

View File

@ -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)

View File

@ -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),
}
}

View File

@ -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)
}
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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)
}
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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")
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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("/")
}

View File

@ -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)
}

View File

@ -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)
}

56
server/rsvp/handler.go Normal file
View File

@ -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)
}

42
server/rsvp/request.go Normal file
View File

@ -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
}

110
server/rsvp/response.go Normal file
View File

@ -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...)},
}
}

42
server/rsvp/session.go Normal file
View File

@ -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
}

View File

@ -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 {