feat: groups

This commit is contained in:
Teajey 2024-10-27 16:49:49 +09:00
parent 04dc7e9376
commit e86fed2f00
Signed by: Teajey
GPG Key ID: 970E790FE834A713
12 changed files with 318 additions and 3 deletions

View File

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

59
context/group_page.go Normal file
View File

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

View File

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

61
db/group.go Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,50 @@
{{define "navbar"}}
<nav>
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link" href="/">Home</a>
</li>
</ul>
</nav>
<div class="flex-grow-1"></div>
<ul class="navbar-nav">
<li class="nav-item">
<div class="dropdown">
<button class="nav-link dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
Logged in as '{{.CurrentUsername}}'
</button>
<ul class="dropdown-menu">
<li>
<form class="d-contents" method="post" action="/logout">
<button class="dropdown-item" type="submit">Logout</button>
</form>
</li>
</ul>
</div>
</li>
</ul>
{{end}}
{{define "body"}}
<div class="overflow-y-scroll flex-grow-1">
<div class="container py-5">
<section class="card">
<div class="card-body">
<h2><em>{{.Name}}</em> group members</h2>
{{with .Members}}
<ul class="list-group">
{{range .}}
<li class="list-group-item">
<a href="/list/{{.Reference}}">{{.Name}}</a>
</li>
{{end}}
</ul>
{{else}}
<p>There's nobody else in this group.</p>
{{end}}
</div>
</section>
</div>
</div>
{{end}}

View File

@ -54,7 +54,7 @@
</div>
</section>
<section class="card">
<section class="card mb-4">
<div class="card-body">
<h2>Your todo list</h2>
{{with .Todo}}
@ -91,6 +91,23 @@
{{end}}
</div>
</section>
<section class="card">
<div class="card-body">
<h2>Your groups</h2>
{{with .Groups}}
<ul class="list-group">
{{range .}}
<li class="list-group-item">
<a href="/group/{{.Reference}}">{{.Name}}</a>
</li>
{{end}}
</ul>
{{else}}
<p>You don't belong to any groups</p>
{{end}}
</div>
</section>
</div>
</div>
{{end}}

View File

@ -0,0 +1,37 @@
{{define "navbar"}}
<nav>
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link" href="/">Home</a>
</li>
</ul>
</nav>
{{end}}
{{define "login_prompt"}}
<a href="/">Login</a> or <a href="/register">register</a>
{{end}}
{{define "body"}}
<div class="overflow-y-scroll flex-grow-1">
<div class="container py-5">
<section class="card">
<div class="card-body">
<h2><em>{{.Name}}</em> group members</h2>
<p>{{template "login_prompt"}} to see your groups</p>
{{with .Members}}
<ul class="list-group">
{{range .}}
<li class="list-group-item">
{{.Name}}
</li>
{{end}}
</ul>
{{else}}
<p>There's nobody else in this group.</p>
{{end}}
</div>
</section>
</div>
</div>
{{end}}

View File

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