From c13dd9addbf89f716e4ef5cfdf1d673139ffcb68 Mon Sep 17 00:00:00 2001 From: Mo Tarbin Date: Sun, 30 Jun 2024 21:41:41 -0400 Subject: Move to Donetick Org, first commit --- internal/circle/handler.go | 442 +++++++++++++++++++++++++++++++++++++ internal/circle/model/model.go | 35 +++ internal/circle/repo/repository.go | 117 ++++++++++ 3 files changed, 594 insertions(+) create mode 100644 internal/circle/handler.go create mode 100644 internal/circle/model/model.go create mode 100644 internal/circle/repo/repository.go (limited to 'internal/circle') diff --git a/internal/circle/handler.go b/internal/circle/handler.go new file mode 100644 index 0000000..c15d322 --- /dev/null +++ b/internal/circle/handler.go @@ -0,0 +1,442 @@ +package circle + +import ( + "log" + + "strconv" + "time" + + auth "donetick.com/core/internal/authorization" + "donetick.com/core/internal/chore" + chRepo "donetick.com/core/internal/chore/repo" + cModel "donetick.com/core/internal/circle/model" + cRepo "donetick.com/core/internal/circle/repo" + uModel "donetick.com/core/internal/user/model" + uRepo "donetick.com/core/internal/user/repo" + "donetick.com/core/logging" + jwt "github.com/appleboy/gin-jwt/v2" + "github.com/gin-gonic/gin" +) + +type Handler struct { + circleRepo *cRepo.CircleRepository + userRepo *uRepo.UserRepository + choreRepo *chRepo.ChoreRepository +} + +func NewHandler(cr *cRepo.CircleRepository, ur *uRepo.UserRepository, c *chRepo.ChoreRepository) *Handler { + return &Handler{ + circleRepo: cr, + userRepo: ur, + choreRepo: c, + } +} + +func (h *Handler) GetCircleMembers(c *gin.Context) { + // Get the circle ID from the JWT + log := logging.FromContext(c) + currentUser, ok := auth.CurrentUser(c) + if !ok { + log.Error("Error getting current user") + c.JSON(500, gin.H{ + "error": "Error getting current user", + }) + return + } + + // Get all the members of the circle + members, err := h.circleRepo.GetCircleUsers(c, currentUser.CircleID) + if err != nil { + log.Error("Error getting circle members:", err) + c.JSON(500, gin.H{ + "error": "Error getting circle members", + }) + return + } + + c.JSON(200, gin.H{ + "res": members, + }) +} + +func (h *Handler) JoinCircle(c *gin.Context) { + // Get the circle ID from the JWT + log := logging.FromContext(c) + log.Debug("handlder.go: JoinCircle") + currentUser, ok := auth.CurrentUser(c) + + if !ok { + c.JSON(500, gin.H{ + "error": "Error getting current user", + }) + return + } + + requestedCircleID := c.Query("invite_code") + if requestedCircleID == "" { + c.JSON(400, gin.H{ + "error": "Invalid request", + }) + return + } + + circle, err := h.circleRepo.GetCircleByInviteCode(c, requestedCircleID) + + if circle.ID == currentUser.CircleID { + c.JSON(409, gin.H{ + "error": "You are already a member of this circle", + }) + return + } + + // Add the user to the circle + err = h.circleRepo.AddUserToCircle(c, &cModel.UserCircle{ + CircleID: circle.ID, + UserID: currentUser.ID, + Role: "member", + IsActive: false, + }) + + if err != nil { + log.Error("Error adding user to circle:", err) + c.JSON(500, gin.H{ + "error": "Error adding user to circle", + }) + return + } + + c.JSON(200, gin.H{ + "res": "User Requested to join circle successfully", + }) +} + +func (h *Handler) LeaveCircle(c *gin.Context) { + log := logging.FromContext(c) + log.Debug("handler.go: LeaveCircle") + currentUser, ok := auth.CurrentUser(c) + if !ok { + c.JSON(500, gin.H{ + "error": "Error getting current user", + }) + return + } + rawCircleID := c.Query("circle_id") + circleID, err := strconv.Atoi(rawCircleID) + if err != nil { + c.JSON(400, gin.H{ + "error": "Invalid request", + }) + return + } + + orginalCircleID, err := h.circleRepo.GetUserOriginalCircle(c, currentUser.ID) + if err != nil { + log.Error("Error getting user original circle:", err) + c.JSON(500, gin.H{ + "error": "Error getting user original circle", + }) + return + } + + // START : HANDLE USER LEAVING CIRCLE + // bulk update chores: + if err := handleUserLeavingCircle(h, c, currentUser, orginalCircleID); err != nil { + log.Error("Error handling user leaving circle:", err) + c.JSON(500, gin.H{ + "error": "Error handling user leaving circle", + }) + return + } + + // END: HANDLE USER LEAVING CIRCLE + + err = h.circleRepo.LeaveCircleByUserID(c, circleID, currentUser.ID) + if err != nil { + log.Error("Error leaving circle:", err) + c.JSON(500, gin.H{ + "error": "Error leaving circle", + }) + return + } + + if err := h.userRepo.UpdateUserCircle(c, currentUser.ID, orginalCircleID); err != nil { + log.Error("Error updating user circle:", err) + c.JSON(500, gin.H{ + "error": "Error updating user circle", + }) + return + } + c.JSON(200, gin.H{ + "res": "User left circle successfully", + }) +} + +func handleUserLeavingCircle(h *Handler, c *gin.Context, leavingUser *uModel.User, orginalCircleID int) error { + userAssignedCircleChores, err := h.choreRepo.GetChores(c, leavingUser.CircleID, leavingUser.ID) + if err != nil { + return err + } + for _, ch := range userAssignedCircleChores { + + if ch.CreatedBy == leavingUser.ID && ch.AssignedTo != leavingUser.ID { + ch.AssignedTo = leavingUser.ID + ch.UpdatedAt = time.Now() + ch.UpdatedBy = leavingUser.ID + ch.CircleID = orginalCircleID + } else if ch.CreatedBy != leavingUser.ID && ch.AssignedTo == leavingUser.ID { + chore.RemoveAssigneeAndReassign(ch, leavingUser.ID) + } + + } + + h.choreRepo.UpdateChores(c, userAssignedCircleChores) + h.choreRepo.RemoveChoreAssigneeByCircleID(c, leavingUser.ID, leavingUser.CircleID) + return nil +} + +func (h *Handler) DeleteCircleMember(c *gin.Context) { + log := logging.FromContext(c) + log.Debug("handler.go: DeleteCircleMember") + currentUser, ok := auth.CurrentUser(c) + if !ok { + c.JSON(500, gin.H{ + "error": "Error getting current user", + }) + return + } + rawCircleID := c.Param("id") + circleID, err := strconv.Atoi(rawCircleID) + if err != nil { + c.JSON(400, gin.H{ + "error": "Invalid request", + }) + return + } + rawMemeberIDToDeleted := c.Query("member_id") + memberIDToDeleted, err := strconv.Atoi(rawMemeberIDToDeleted) + if err != nil { + c.JSON(400, gin.H{ + "error": "Invalid request", + }) + return + } + admins, err := h.circleRepo.GetCircleAdmins(c, circleID) + if err != nil { + log.Error("Error getting circle admins:", err) + c.JSON(500, gin.H{ + "error": "Error getting circle admins", + }) + return + } + isAdmin := false + for _, admin := range admins { + if admin.UserID == currentUser.ID { + isAdmin = true + break + } + } + if !isAdmin { + c.JSON(403, gin.H{ + "error": "You are not an admin of this circle", + }) + return + } + orginalCircleID, err := h.circleRepo.GetUserOriginalCircle(c, memberIDToDeleted) + if handleUserLeavingCircle(h, c, &uModel.User{ID: memberIDToDeleted, CircleID: circleID}, orginalCircleID) != nil { + log.Error("Error handling user leaving circle:", err) + c.JSON(500, gin.H{ + "error": "Error handling user leaving circle", + }) + return + } + + err = h.circleRepo.DeleteMemberByID(c, circleID, memberIDToDeleted) + if err != nil { + log.Error("Error deleting circle member:", err) + c.JSON(500, gin.H{ + "error": "Error deleting circle member", + }) + return + } + c.JSON(200, gin.H{ + "res": "User deleted from circle successfully", + }) +} + +func (h *Handler) GetUserCircles(c *gin.Context) { + log := logging.FromContext(c) + currentUser, ok := auth.CurrentUser(c) + if !ok { + c.JSON(500, gin.H{ + "error": "Error getting current user", + }) + return + } + + circles, err := h.circleRepo.GetUserCircles(c, currentUser.ID) + if err != nil { + log.Error("Error getting user circles:", err) + c.JSON(500, gin.H{ + "error": "Error getting user circles", + }) + return + } + + c.JSON(200, gin.H{ + "res": circles, + }) +} + +func (h *Handler) GetPendingCircleMembers(c *gin.Context) { + log := logging.FromContext(c) + currentUser, ok := auth.CurrentUser(c) + if !ok { + c.JSON(500, gin.H{ + "error": "Error getting current user", + }) + return + } + + currentMemebers, err := h.circleRepo.GetCircleUsers(c, currentUser.CircleID) + if err != nil { + log.Error("Error getting circle members:", err) + c.JSON(500, gin.H{ + "error": "Error getting circle members", + }) + return + } + + // confirm that the current user is an admin: + isAdmin := false + for _, member := range currentMemebers { + if member.UserID == currentUser.ID && member.Role == "admin" { + isAdmin = true + break + } + } + if !isAdmin { + c.JSON(403, gin.H{ + "error": "You are not an admin of this circle", + }) + return + } + + members, err := h.circleRepo.GetPendingJoinRequests(c, currentUser.CircleID) + if err != nil { + log.Error("Error getting pending circle members:", err) + c.JSON(500, gin.H{ + "error": "Error getting pending circle members", + }) + return + } + + c.JSON(200, gin.H{ + "res": members, + }) +} + +func (h *Handler) AcceptJoinRequest(c *gin.Context) { + log := logging.FromContext(c) + currentUser, ok := auth.CurrentUser(c) + if !ok { + c.JSON(500, gin.H{ + "error": "Error getting current user", + }) + return + } + + rawRequestID := c.Query("requestId") + requestID, err := strconv.Atoi(rawRequestID) + if err != nil { + c.JSON(400, gin.H{ + "error": "Invalid request", + }) + return + } + + currentMemebers, err := h.circleRepo.GetCircleUsers(c, currentUser.CircleID) + if err != nil { + log.Error("Error getting circle members:", err) + c.JSON(500, gin.H{ + "error": "Error getting circle members", + }) + return + } + + // confirm that the current user is an admin: + isAdmin := false + for _, member := range currentMemebers { + if member.UserID == currentUser.ID && member.Role == "admin" { + isAdmin = true + break + } + } + if !isAdmin { + c.JSON(403, gin.H{ + "error": "You are not an admin of this circle", + }) + return + } + pendingRequests, err := h.circleRepo.GetPendingJoinRequests(c, currentUser.CircleID) + if err != nil { + log.Error("Error getting pending circle members:", err) + c.JSON(500, gin.H{ + "error": "Error getting pending circle members", + }) + return + } + isActiveRequest := false + var requestedCircle *cModel.UserCircleDetail + for _, request := range pendingRequests { + if request.ID == requestID { + requestedCircle = request + isActiveRequest = true + break + } + } + if !isActiveRequest { + c.JSON(400, gin.H{ + "error": "Invalid request", + }) + return + } + + err = h.circleRepo.AcceptJoinRequest(c, currentUser.CircleID, requestID) + if err != nil { + log.Error("Error accepting join request:", err) + c.JSON(500, gin.H{ + "error": "Error accepting join request", + }) + return + } + + if err := h.userRepo.UpdateUserCircle(c, requestedCircle.UserID, currentUser.CircleID); err != nil { + log.Error("Error updating user circle:", err) + c.JSON(500, gin.H{ + "error": "Error updating user circle", + }) + return + } + + c.JSON(200, gin.H{ + "res": "Join request accepted successfully", + }) +} + +func Routes(router *gin.Engine, h *Handler, auth *jwt.GinJWTMiddleware) { + log.Println("Registering routes") + + circleRoutes := router.Group("circles") + circleRoutes.Use(auth.MiddlewareFunc()) + { + circleRoutes.GET("/members", h.GetCircleMembers) + circleRoutes.GET("/members/requests", h.GetPendingCircleMembers) + circleRoutes.PUT("/members/requests/accept", h.AcceptJoinRequest) + circleRoutes.GET("/", h.GetUserCircles) + circleRoutes.POST("/join", h.JoinCircle) + circleRoutes.DELETE("/leave", h.LeaveCircle) + circleRoutes.DELETE("/:id/members/delete", h.DeleteCircleMember) + + } + +} diff --git a/internal/circle/model/model.go b/internal/circle/model/model.go new file mode 100644 index 0000000..bf26b34 --- /dev/null +++ b/internal/circle/model/model.go @@ -0,0 +1,35 @@ +package circle + +import "time" + +type Circle struct { + ID int `json:"id" gorm:"primary_key"` // Unique identifier + Name string `json:"name" gorm:"column:name"` // Full name + CreatedBy int `json:"created_by" gorm:"column:created_by"` // Created by + CreatedAt time.Time `json:"created_at" gorm:"column:created_at"` // Created at + UpdatedAt time.Time `json:"updated_at" gorm:"column:updated_at"` // Updated at + InviteCode string `json:"invite_code" gorm:"column:invite_code"` // Invite code + Disabled bool `json:"disabled" gorm:"column:disabled"` // Disabled +} + +type CircleDetail struct { + Circle + UserRole string `json:"userRole" gorm:"column:role"` +} + +type UserCircle struct { + ID int `json:"id" gorm:"primary_key"` // Unique identifier + UserID int `json:"userId" gorm:"column:user_id"` // User ID + CircleID int `json:"circleId" gorm:"column:circle_id"` // Circle ID + Role string `json:"role" gorm:"column:role"` // Role + IsActive bool `json:"isActive" gorm:"column:is_active;default:false"` + CreatedAt time.Time `json:"createdAt" gorm:"column:created_at"` // Created at + UpdatedAt time.Time `json:"updatedAt" gorm:"column:updated_at"` // Updated at +} + +type UserCircleDetail struct { + UserCircle + Username string `json:"username" gorm:"column:username"` + DisplayName string `json:"displayName" gorm:"column:display_name"` + ChatID int `json:"chatID" gorm:"column:chat_id"` +} diff --git a/internal/circle/repo/repository.go b/internal/circle/repo/repository.go new file mode 100644 index 0000000..712cc99 --- /dev/null +++ b/internal/circle/repo/repository.go @@ -0,0 +1,117 @@ +package repo + +import ( + "context" + + cModel "donetick.com/core/internal/circle/model" + "gorm.io/gorm" +) + +type ICircleRepository interface { + CreateCircle(circle *cModel.Circle) error + AddUserToCircle(circleUser *cModel.UserCircle) error + GetCircleUsers(circleID int) ([]*cModel.UserCircle, error) + GetUserCircles(userID int) ([]*cModel.Circle, error) + DeleteUserFromCircle(circleID, userID int) error + ChangeUserRole(circleID, userID int, role string) error + GetCircleByInviteCode(inviteCode string) (*cModel.Circle, error) + GetCircleByID(circleID int) (*cModel.Circle, error) +} + +type CircleRepository struct { + db *gorm.DB +} + +func NewCircleRepository(db *gorm.DB) *CircleRepository { + return &CircleRepository{db} +} + +func (r *CircleRepository) CreateCircle(c context.Context, circle *cModel.Circle) (*cModel.Circle, error) { + if err := r.db.WithContext(c).Save(&circle).Error; err != nil { + return nil, err + } + return circle, nil + +} + +func (r *CircleRepository) AddUserToCircle(c context.Context, circleUser *cModel.UserCircle) error { + return r.db.WithContext(c).Save(circleUser).Error +} + +func (r *CircleRepository) GetCircleUsers(c context.Context, circleID int) ([]*cModel.UserCircleDetail, error) { + var circleUsers []*cModel.UserCircleDetail + // join user table to get user details like username and display name: + if err := r.db.WithContext(c).Raw("SELECT * FROM user_circles LEFT JOIN users on users.id = user_circles.user_id WHERE user_circles.circle_id = ?", circleID).Scan(&circleUsers).Error; err != nil { + return nil, err + } + return circleUsers, nil +} + +func (r *CircleRepository) GetPendingJoinRequests(c context.Context, circleID int) ([]*cModel.UserCircleDetail, error) { + var pendingRequests []*cModel.UserCircleDetail + if err := r.db.WithContext(c).Raw("SELECT *, user_circles.id as id FROM user_circles LEFT JOIN users on users.id = user_circles.user_id WHERE user_circles.circle_id = ? AND user_circles.is_active = false", circleID).Scan(&pendingRequests).Error; err != nil { + return nil, err + } + return pendingRequests, nil +} + +func (r *CircleRepository) AcceptJoinRequest(c context.Context, circleID, requestID int) error { + + return r.db.WithContext(c).Model(&cModel.UserCircle{}).Where("circle_id = ? AND id = ?", circleID, requestID).Update("is_active", true).Error +} + +func (r *CircleRepository) GetUserCircles(c context.Context, userID int) ([]*cModel.CircleDetail, error) { + var circles []*cModel.CircleDetail + if err := r.db.WithContext(c).Raw("SELECT circles.*, user_circles.role as role, user_circles.created_at uc_created_at FROM circles Left JOIN user_circles on circles.id = user_circles.circle_id WHERE user_circles.user_id = ? ORDER BY uc_created_at desc", userID).Scan(&circles).Error; err != nil { + return nil, err + } + return circles, nil +} + +func (r *CircleRepository) DeleteUserFromCircle(c context.Context, circleID, userID int) error { + return r.db.WithContext(c).Where("circle_id = ? AND user_id = ?", circleID, userID).Delete(&cModel.UserCircle{}).Error +} + +func (r *CircleRepository) ChangeUserRole(c context.Context, circleID, userID int, role int) error { + return r.db.WithContext(c).Model(&cModel.UserCircle{}).Where("circle_id = ? AND user_id = ?", circleID, userID).Update("role", role).Error +} + +func (r *CircleRepository) GetCircleByInviteCode(c context.Context, inviteCode string) (*cModel.Circle, error) { + var circle cModel.Circle + if err := r.db.WithContext(c).Where("invite_code = ?", inviteCode).First(&circle).Error; err != nil { + return nil, err + } + return &circle, nil +} + +func (r *CircleRepository) GetCircleByID(c context.Context, circleID int) (*cModel.Circle, error) { + var circle cModel.Circle + if err := r.db.WithContext(c).First(&circle, circleID).Error; err != nil { + return nil, err + } + return &circle, nil +} + +func (r *CircleRepository) LeaveCircleByUserID(c context.Context, circleID, userID int) error { + return r.db.WithContext(c).Where("circle_id = ? AND user_id = ? AND role != 'admin'", circleID, userID).Delete(&cModel.UserCircle{}).Error +} + +func (r *CircleRepository) GetUserOriginalCircle(c context.Context, userID int) (int, error) { + var circleID int + if err := r.db.WithContext(c).Raw("SELECT circle_id FROM user_circles WHERE user_id = ? AND role = 'admin'", userID).Scan(&circleID).Error; err != nil { + return 0, err + } + return circleID, nil +} + +func (r *CircleRepository) DeleteMemberByID(c context.Context, circleID, userID int) error { + return r.db.WithContext(c).Where("circle_id = ? AND user_id = ?", circleID, userID).Delete(&cModel.UserCircle{}).Error +} + +func (r *CircleRepository) GetCircleAdmins(c context.Context, circleID int) ([]*cModel.UserCircleDetail, error) { + var circleAdmins []*cModel.UserCircleDetail + if err := r.db.WithContext(c).Raw("SELECT * FROM user_circles LEFT JOIN users on users.id = user_circles.user_id WHERE user_circles.circle_id = ? AND user_circles.role = 'admin'", circleID).Scan(&circleAdmins).Error; err != nil { + return nil, err + } + return circleAdmins, nil +} -- cgit