diff --git a/auth/register.go b/auth/register.go index e59a6d4..9398f2b 100644 --- a/auth/register.go +++ b/auth/register.go @@ -97,6 +97,7 @@ func (auth *AuthMiddleware) RegisterPost(w http.ResponseWriter, r *http.Request) _, err = db.CreateUser(username, hashedPasswordBytes) if err != nil { + log.Println("Registration error:", err) props.GeneralError = "Something went wrong. Error code: Ozai" auth.RedirectWithFlash(w, r, "/register", "register_props", &props) return diff --git a/context/group_page.go b/context/group_page.go new file mode 100644 index 0000000..d4beb22 --- /dev/null +++ b/context/group_page.go @@ -0,0 +1,59 @@ +package context + +import ( + "net/http" + + "lishwist/db" + "lishwist/error" + "lishwist/templates" +) + +type GroupProps struct { + Name string + Members []db.User + CurrentUsername string +} + +func (ctx *Context) GroupPage(w http.ResponseWriter, r *http.Request) { + user := ctx.Auth.ExpectUser(r) + groupReference := r.PathValue("groupReference") + group, err := user.GetGroupByReference(groupReference) + if err != nil { + error.Page(w, "An error occurred while fetching this group :(", http.StatusInternalServerError, err) + return + } + if group == nil { + error.Page(w, "Group not found. (It might be because you're not a member)", http.StatusNotFound, nil) + return + } + peers, err := user.GetPeers(group.Id) + if err != nil { + error.Page(w, "An error occurred while fetching this group :(", http.StatusInternalServerError, err) + return + } + p := GroupProps{ + Name: group.Name, + Members: peers, + CurrentUsername: user.Name, + } + templates.Execute(w, "group_page.gotmpl", p) +} + +func (ctx *Context) PublicGroupPage(w http.ResponseWriter, r *http.Request) { + 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 + } + members, err := group.GetMembers() + if err != nil { + error.Page(w, "An error occurred while fetching this group :(", http.StatusInternalServerError, err) + return + } + p := GroupProps{ + Name: group.Name, + Members: members, + } + templates.Execute(w, "public_group_page.gotmpl", p) +} diff --git a/context/home.go b/context/home.go index 1f76836..1fb15b2 100644 --- a/context/home.go +++ b/context/home.go @@ -15,6 +15,7 @@ type HomeProps struct { Todo []db.Gift Reference string HostUrl string + Groups []db.Group } func (ctx *Context) Home(w http.ResponseWriter, r *http.Request) { @@ -29,7 +30,12 @@ func (ctx *Context) Home(w http.ResponseWriter, r *http.Request) { error.Page(w, "An error occurred while fetching your wishlist :(", http.StatusInternalServerError, err) return } - p := HomeProps{Username: user.Name, Gifts: gifts, Todo: todo, Reference: user.Reference, HostUrl: env.HostUrl.String()} + groups, err := user.GetGroups() + if err != nil { + error.Page(w, "An error occurred while fetching your wishlist :(", http.StatusInternalServerError, err) + return + } + p := HomeProps{Username: user.Name, Gifts: gifts, Todo: todo, Reference: user.Reference, HostUrl: env.HostUrl.String(), Groups: groups} templates.Execute(w, "home.gotmpl", p) } diff --git a/db/group.go b/db/group.go new file mode 100644 index 0000000..fc17647 --- /dev/null +++ b/db/group.go @@ -0,0 +1,61 @@ +package db + +import "database/sql" + +type Group struct { + Id string + Name string + Reference string +} + +func queryForGroup(query string, args ...any) (*Group, error) { + var id string + var name string + var reference string + err := database.QueryRow(query, args...).Scan(&id, &name, &reference) + if err == sql.ErrNoRows { + return nil, nil + } else if err != nil { + return nil, err + } + group := Group{ + Id: id, + Name: name, + Reference: reference, + } + return &group, nil +} + +func GetGroupByReference(reference string) (*Group, error) { + stmt := "SELECT [group].id, [group].name, [group].reference FROM [group] WHERE [group].reference = ?" + return queryForGroup(stmt, reference) +} + +func (g *Group) GetMembers() ([]User, error) { + stmt := "SELECT user.id, user.name, user.reference FROM user JOIN group_member ON group_member.user_id = user.id JOIN [group] ON [group].id = group_member.group_id WHERE [group].id = ?" + rows, err := database.Query(stmt, g.Id) + users := []User{} + if err != nil { + return users, err + } + defer rows.Close() + for rows.Next() { + var id string + var name string + var reference string + err := rows.Scan(&id, &name, &reference) + if err != nil { + return users, err + } + users = append(users, User{ + Id: id, + Name: name, + Reference: reference, + }) + } + err = rows.Err() + if err != nil { + return users, err + } + return users, nil +} diff --git a/db/init.sql b/db/init.sql index b5581eb..d3b70d7 100644 --- a/db/init.sql +++ b/db/init.sql @@ -19,4 +19,17 @@ CREATE TABLE IF NOT EXISTS "gift" ( FOREIGN KEY("creator_id") REFERENCES "user"("id"), FOREIGN KEY("claimant_id") REFERENCES "user"("id") ); +CREATE TABLE IF NOT EXISTS "group" ( + "id" INTEGER NOT NULL UNIQUE, + "name" TEXT NOT NULL UNIQUE, + "reference" TEXT NOT NULL UNIQUE, + PRIMARY KEY("id" AUTOINCREMENT) +); +CREATE TABLE IF NOT EXISTS "group_member" ( + "group_id" INTEGER NOT NULL, + "user_id" INTEGER NOT NULL, + UNIQUE("user_id","group_id"), + FOREIGN KEY("group_id") REFERENCES "group"("id"), + FOREIGN KEY("user_id") REFERENCES "user"("id") +); COMMIT; diff --git a/db/user.go b/db/user.go index f7538b2..97bf4cd 100644 --- a/db/user.go +++ b/db/user.go @@ -365,3 +365,66 @@ func (u *User) AddGiftToUser(otherUserReference string, giftName string) error { } return nil } + +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 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 +} + +func (u *User) GetPeers(groupId string) ([]User, error) { + stmt := "SELECT user.id, user.name, user.reference FROM user JOIN group_member ON group_member.user_id = user.id JOIN [group] ON [group].id = group_member.group_id WHERE [group].id = ? AND user.id != ?" + rows, err := database.Query(stmt, groupId, u.Id) + if err != nil { + return nil, err + } + defer rows.Close() + users := []User{} + for rows.Next() { + var id string + var name string + var reference string + err := rows.Scan(&id, &name, &reference) + if err != nil { + return nil, err + } + users = append(users, User{ + Id: id, + Name: name, + Reference: reference, + }) + } + err = rows.Err() + if err != nil { + return nil, err + } + return users, nil +} + +func (u *User) GetGroupByReference(reference string) (*Group, error) { + stmt := "SELECT [group].id, [group].name, [group].reference FROM [group] JOIN group_member ON [group].id == group_member.group_id WHERE [group].reference = ? AND group_member.user_id = ?" + return queryForGroup(stmt, reference, u.Id) +} diff --git a/error/page.go b/error/page.go index 8fab823..898c927 100644 --- a/error/page.go +++ b/error/page.go @@ -11,7 +11,9 @@ type pageProps struct { } func Page(w http.ResponseWriter, publicMessage string, status int, err error) { - log.Printf("%s --- %s\n", publicMessage, err) + if err != nil { + log.Printf("%s --- %s\n", publicMessage, err) + } templates.Execute(w, "error_page.gotmpl", pageProps{publicMessage}) http.Error(w, "", http.StatusInternalServerError) } diff --git a/main.go b/main.go index 5ceca5e..c5cb1c4 100644 --- a/main.go +++ b/main.go @@ -34,11 +34,13 @@ func main() { publicMux.HandleFunc("GET /", authMiddleware.Login) publicMux.HandleFunc("POST /", authMiddleware.LoginPost) publicMux.HandleFunc("GET /list/{userReference}", ctx.PublicWishlist) + publicMux.HandleFunc("GET /group/{groupReference}", ctx.PublicGroupPage) protectedMux.HandleFunc("GET /{$}", ctx.Home) protectedMux.HandleFunc("POST /{$}", ctx.HomePost) protectedMux.HandleFunc("GET /list/{userReference}", ctx.ForeignWishlist) protectedMux.HandleFunc("POST /list/{userReference}", ctx.ForeignWishlistPost) + protectedMux.HandleFunc("GET /group/{groupReference}", ctx.GroupPage) protectedMux.HandleFunc("POST /logout", authMiddleware.LogoutPost) http.Handle("/", authMiddleware) diff --git a/templates/group_page.gotmpl b/templates/group_page.gotmpl new file mode 100644 index 0000000..5649ea9 --- /dev/null +++ b/templates/group_page.gotmpl @@ -0,0 +1,50 @@ +{{define "navbar"}} + +
+ +{{end}} + +{{define "body"}} +
+
+
+
+

{{.Name}} group members

+ {{with .Members}} + + {{else}} +

There's nobody else in this group.

+ {{end}} +
+
+
+
+{{end}} + diff --git a/templates/home.gotmpl b/templates/home.gotmpl index 985fc67..6a87056 100644 --- a/templates/home.gotmpl +++ b/templates/home.gotmpl @@ -54,7 +54,7 @@ -
+

Your todo list

{{with .Todo}} @@ -91,6 +91,23 @@ {{end}}
+ +
+
+

Your groups

+ {{with .Groups}} + + {{else}} +

You don't belong to any groups

+ {{end}} +
+
{{end}} diff --git a/templates/public_group_page.gotmpl b/templates/public_group_page.gotmpl new file mode 100644 index 0000000..9d63da1 --- /dev/null +++ b/templates/public_group_page.gotmpl @@ -0,0 +1,37 @@ +{{define "navbar"}} + +{{end}} + +{{define "login_prompt"}} +Login or register +{{end}} + +{{define "body"}} +
+
+
+
+

{{.Name}} group members

+

{{template "login_prompt"}} to see your groups

+ {{with .Members}} +
    + {{range .}} +
  • + {{.Name}} +
  • + {{end}} +
+ {{else}} +

There's nobody else in this group.

+ {{end}} +
+
+
+
+{{end}} \ No newline at end of file diff --git a/templates/templates.go b/templates/templates.go index f36b24c..10ee0ed 100644 --- a/templates/templates.go +++ b/templates/templates.go @@ -32,6 +32,8 @@ func loadTemplates() map[string]*template.Template { foreignTmpl := template.Must(template.ParseFiles("templates/base.gotmpl", "templates/foreign_wishlist.gotmpl")) publicWishlistTmpl := template.Must(template.ParseFiles("templates/base.gotmpl", "templates/public_foreign_wishlist.gotmpl")) errorTmpl := template.Must(template.ParseFiles("templates/base.gotmpl", "templates/error_page.gotmpl")) + groupTmpl := template.Must(template.ParseFiles("templates/base.gotmpl", "templates/group_page.gotmpl")) + publicGroupTmpl := template.Must(template.ParseFiles("templates/base.gotmpl", "templates/public_group_page.gotmpl")) return map[string]*template.Template{ "home.gotmpl": homeTmpl, "login.gotmpl": loginTmpl, @@ -39,5 +41,7 @@ func loadTemplates() map[string]*template.Template { "foreign_wishlist.gotmpl": foreignTmpl, "public_foreign_wishlist.gotmpl": publicWishlistTmpl, "error_page.gotmpl": errorTmpl, + "group_page.gotmpl": groupTmpl, + "public_group_page.gotmpl": publicGroupTmpl, } }