From a7cd86962e04d3ebede3f2f756632db28d32b0ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20Forc=C3=A9n=20Mu=C3=B1oz?= Date: Tue, 11 Feb 2025 21:16:39 +0100 Subject: [PATCH] Added membership for users --- extract.go | 2 +- models/models.go | 16 ++--- models/plans.go | 82 +++++++++++++++++++++++- models/users.go | 8 +++ planner.go | 158 +++++++++++++++++++++++++++++++++++++++++------ 5 files changed, 238 insertions(+), 28 deletions(-) diff --git a/extract.go b/extract.go index d8156ac..2b5e709 100644 --- a/extract.go +++ b/extract.go @@ -7,7 +7,7 @@ import ( "planner/models" ) -func extract_user(orm *gorm.DB, c *gin.Context) *models.User { +func ExtractUser(orm *gorm.DB, c *gin.Context) *models.User { username, _, ok := c.Request.BasicAuth() if !ok { c.Status(http.StatusUnauthorized) diff --git a/models/models.go b/models/models.go index e2778db..fa7e415 100644 --- a/models/models.go +++ b/models/models.go @@ -9,13 +9,15 @@ type User struct { } type Member struct { - ID uint `gorm:"primaryKey;autoIncrement:true"` - PlanID uint - Plan Plan - Type string `gorm:"check:type in ('member','non-member')"` - Name string `gorm:"check:type=='member' OR name IS NOT NULL" json:"name"` - UserID string - User User `gorm:"foreignKey:UserID"` + ID uint `gorm:"primaryKey;autoIncrement:true" json:"-"` + PlanID uint `json:"-"` + Plan Plan `json:"-"` + Type string `gorm:"check:type in ('user','non-user')" json:"type"` + Name string `gorm:"check:type=='member' OR name IS NOT NULL" json:"name"` + Status string `gorm:"check:status in ('pending','ready')" json:"status"` + JoinCode string `gorm:"uniqueIndex;check:type=='non-member' OR join_code IS NOT NULL" json:"join_code,omitempty"` + UserID string `json:"username"` + User User `gorm:"foreignKey:UserID" json:"-"` } // CREATE TABLE plans(id INTEGER PRIMARY KEY AUTOINCREMENT, name STRING, owner STRING, FOREIGN KEY(owner) REFERENCES users(username)) diff --git a/models/plans.go b/models/plans.go index 1e045bf..cae2bc4 100644 --- a/models/plans.go +++ b/models/plans.go @@ -1,9 +1,12 @@ package models import ( + "crypto/rand" + "encoding/base64" "errors" - "gorm.io/gorm" . "planner/errors" + + "gorm.io/gorm" ) func GetPlan(orm *gorm.DB, user User, id uint) (*Plan, error) { @@ -48,3 +51,80 @@ func (p *Plan) IsMember(orm *gorm.DB, u *User) (bool, error) { } return member_count == 1, nil } + +func (p *Plan) GetMember(orm *gorm.DB, u *User) (*Member, error) { + var m Member + result := orm. + Table("members"). + Where("user_id=? AND plan_id=?", u.Username, p.ID). + Take(&m).Error + if result != nil { + return nil, ErrNotMember + } + return &m, nil +} + +func (p *Plan) HasNonUser(orm *gorm.DB, name string) (bool, error) { + var member_count int64 + result := orm. + Table("members"). + Where("plan_id=? AND name=?", p.ID, name). + Count(&member_count).Error + if result != nil { + return false, result + } + return member_count == 1, nil +} + +func (p *Plan) AddMember(orm *gorm.DB, new_member *Member) error { + if new_member == nil { + return errors.New("Member is nil") + } + new_member.PlanID = p.ID + if new_member.Type == "non-user" { + found, err := p.HasNonUser(orm, new_member.Name) + if err != nil { + return nil + } + if found { + return errors.New("Non user name taken") + } + new_member.Status = "ready" + return orm.Create(&new_member).Error + + } else if new_member.Type == "user" { + user, err := GetUser(orm, new_member.UserID) + if err != nil { + return err + } + + found, err := p.IsMember(orm, &user) + if err != nil { + return nil + } + if found { + return errors.New("User already is member") + } + + new_member.Status = "pending" + var res error + for retries := 3; retries >= 0; retries -= 1 { + join_code := make([]byte, 32) + _, err = rand.Read(join_code) + if err != nil { + return err + } + new_member.JoinCode = base64.URLEncoding.EncodeToString(join_code) + res = orm.Create(&new_member).Error + + if res == nil { + break + } else if !errors.Is(res, gorm.ErrDuplicatedKey) { + return res + } + } + return res + } else { + return errors.New("Invalid type for user") + } +} diff --git a/models/users.go b/models/users.go index cf25601..a4e72cb 100644 --- a/models/users.go +++ b/models/users.go @@ -13,3 +13,11 @@ func (u *User) GetPlans(orm *gorm.DB) ([]Plan, error) { Find(&plans) return plans, err.Error } + +func GetUser(orm *gorm.DB, username string) (User, error) { + user := User{Username: username} + if err := orm.Take(&user).Error; err != nil { + return user, err + } + return user, nil +} diff --git a/planner.go b/planner.go index 617745e..ecbddfb 100644 --- a/planner.go +++ b/planner.go @@ -5,6 +5,7 @@ import ( "fmt" "log" "net/http" + "strconv" . "planner/errors" "planner/models" @@ -59,7 +60,7 @@ func main() { } }) - r.GET("/users", func(c *gin.Context) { + r.GET("/user", func(c *gin.Context) { var q struct { Name string `form:"name"` } @@ -73,7 +74,7 @@ func main() { } }) - r.POST("/users", func(c *gin.Context) { + r.POST("/user", func(c *gin.Context) { var u User if c.ShouldBind(&u) == nil { orm.Create(&u) @@ -83,30 +84,47 @@ func main() { } }) - r.POST("/plans", func(c *gin.Context) { - username, _, ok := c.Request.BasicAuth() - if !ok { - c.Status(http.StatusUnauthorized) + r.GET("/users/:id", func(c *gin.Context) { + u := ExtractUser(orm, c) + if u == nil { return } - u := User{ - Username: username, + + requested := c.Param("id") + if requested == "" { + c.Status(http.StatusBadRequest) + return } - if result := orm.Take(&u); result.Error != nil { - c.String(http.StatusNotFound, "Unable to find user "+username) + other := User{Username: requested} + res := orm.Take(&other) + if res.Error != nil { + c.String(http.StatusInternalServerError, res.Error.Error()) + return + } + c.JSON(http.StatusOK, other) + }) + + r.POST("/plans", func(c *gin.Context) { + u := ExtractUser(orm, c) + if u == nil { return } var plan_req struct { - Name string `json:"name"` + Name string `json:"name" form:"name"` } c.Bind(&plan_req) var plan Plan = Plan{ Name: plan_req.Name, Owner: u.Username, Members: []Member{ - {UserID: u.Username}, + { + UserID: u.Username, + Type: "user", + Status: "ready", + JoinCode: "owner", + }, }, } result := orm.Create(&plan) @@ -119,7 +137,7 @@ func main() { }) r.GET("/plans", func(c *gin.Context) { - u := extract_user(orm, c) + u := ExtractUser(orm, c) if u == nil { return } @@ -132,7 +150,7 @@ func main() { }) r.GET("/plans/:id", func(c *gin.Context) { - user := extract_user(orm, c) + user := ExtractUser(orm, c) if user == nil { return } @@ -158,8 +176,110 @@ func main() { } }) + r.GET("/plans/:id/members", func(c *gin.Context) { + user := ExtractUser(orm, c) + if user == nil { + return + } + + plan_id, err := strconv.Atoi(c.Param("id")) + if err != nil { + c.Status(http.StatusBadRequest) + return + } + + plan, err := GetPlan(orm, *user, uint(plan_id)) + members, err := plan.GetAllUsers(orm) + + if err == nil { + c.JSON(http.StatusOK, members) + } else if errors.Is(err, ErrNotMember) { + c.Status(http.StatusForbidden) + } else if errors.Is(err, ErrNotFound) { + c.Status(http.StatusNotFound) + } else { + c.String(http.StatusInternalServerError, err.Error()) + } + }) + + r.POST("/plans/:id/members", func(c *gin.Context) { + user := ExtractUser(orm, c) + if user == nil { + return + } + + plan_id, err := strconv.Atoi(c.Param("id")) + if err != nil { + c.Status(http.StatusBadRequest) + return + } + + plan, err := GetPlan(orm, *user, uint(plan_id)) + + var new_member Member + if err := c.ShouldBind(&new_member); err != nil { + c.String(http.StatusInternalServerError, err.Error()) + return + } + + err = plan.AddMember(orm, &new_member) + + if err == nil { + c.JSON(http.StatusOK, new_member) + } else if errors.Is(err, ErrNotMember) { + c.Status(http.StatusForbidden) + } else if errors.Is(err, ErrNotFound) { + c.Status(http.StatusNotFound) + } else { + c.String(http.StatusInternalServerError, err.Error()) + } + }) + + r.GET("/plans/:id/join", func(c *gin.Context) { + user := ExtractUser(orm, c) + if user == nil { + return + } + + plan_id, err := strconv.Atoi(c.Param("id")) + if err != nil { + c.Status(http.StatusBadRequest) + return + } + plan, err := GetPlan(orm, *user, uint(plan_id)) + if err != nil { + c.Status(http.StatusInternalServerError) + return + } + + member, err := plan.GetMember(orm, user) + var query struct { + JoinCode string `form:"code"` + } + if c.ShouldBindQuery(&query) != nil || query.JoinCode == "" { + c.Status(http.StatusBadRequest) + return + } + if member.Status != "pending" { + c.String(http.StatusConflict, "User is not pending") + return + } + if member.JoinCode == query.JoinCode { + member.Status = "ready" + err := orm.Model(&member).Update("status", member.Status).Error + if err != nil { + c.String(http.StatusInternalServerError, err.Error()) + return + } + } else { + c.String(http.StatusConflict, "Invalid join code") + return + } + c.Status(http.StatusOK) + }) + r.POST("/plans/:id/polls", func(c *gin.Context) { - user := extract_user(orm, c) + user := ExtractUser(orm, c) if user == nil { return } @@ -204,7 +324,7 @@ func main() { }) r.GET("/plans/:id/polls", func(c *gin.Context) { - user := extract_user(orm, c) + user := ExtractUser(orm, c) if user == nil { return } @@ -225,7 +345,7 @@ func main() { }) r.GET("/polls/:poll_id", func(c *gin.Context) { - user := extract_user(orm, c) + user := ExtractUser(orm, c) if user == nil { return } @@ -246,7 +366,7 @@ func main() { }) r.GET("/polls/:poll_id/votes", func(c *gin.Context) { - user := extract_user(orm, c) + user := ExtractUser(orm, c) if user == nil { return } @@ -267,7 +387,7 @@ func main() { }) r.POST("/polls/:poll_id/votes", func(c *gin.Context) { - user := extract_user(orm, c) + user := ExtractUser(orm, c) if user == nil { return }