463 lines
12 KiB
Go
463 lines
12 KiB
Go
package handlers
|
|
|
|
import (
|
|
"errors"
|
|
"net/http"
|
|
"strconv"
|
|
|
|
"veza-backend-api/internal/core/social"
|
|
"veza-backend-api/internal/utils"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/google/uuid"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
// GroupHandler handles social group operations
|
|
type GroupHandler struct {
|
|
service *social.Service
|
|
commonHandler *CommonHandler
|
|
}
|
|
|
|
// NewGroupHandler creates a new GroupHandler
|
|
func NewGroupHandler(service *social.Service, logger *zap.Logger) *GroupHandler {
|
|
return &GroupHandler{
|
|
service: service,
|
|
commonHandler: NewCommonHandler(logger),
|
|
}
|
|
}
|
|
|
|
// CreateGroupRequest is the DTO for group creation
|
|
type CreateGroupRequest struct {
|
|
Name string `json:"name" binding:"required,min=1,max=255" validate:"required,min=1,max=255"`
|
|
Description string `json:"description" binding:"max=2000" validate:"max=2000"`
|
|
IsPublic *bool `json:"is_public"`
|
|
}
|
|
|
|
// CreateGroup creates a new social group
|
|
func (h *GroupHandler) CreateGroup(c *gin.Context) {
|
|
userID, ok := GetUserIDUUID(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
var req CreateGroupRequest
|
|
if appErr := h.commonHandler.BindAndValidateJSON(c, &req); appErr != nil {
|
|
RespondWithAppError(c, appErr)
|
|
return
|
|
}
|
|
|
|
req.Name = utils.SanitizeText(req.Name, 255)
|
|
req.Description = utils.SanitizeText(req.Description, 2000)
|
|
|
|
isPublic := true
|
|
if req.IsPublic != nil {
|
|
isPublic = *req.IsPublic
|
|
}
|
|
|
|
group, err := h.service.CreateGroup(c.Request.Context(), userID, req.Name, req.Description, isPublic)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create group"})
|
|
return
|
|
}
|
|
|
|
RespondSuccess(c, http.StatusCreated, group)
|
|
}
|
|
|
|
// ListGroups returns all public groups
|
|
func (h *GroupHandler) ListGroups(c *gin.Context) {
|
|
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "20"))
|
|
offset, _ := strconv.Atoi(c.DefaultQuery("offset", "0"))
|
|
if limit < 1 {
|
|
limit = 20
|
|
}
|
|
if limit > 100 {
|
|
limit = 100
|
|
}
|
|
if offset < 0 {
|
|
offset = 0
|
|
}
|
|
|
|
groups, total, err := h.service.ListGroups(c.Request.Context(), limit, offset)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to list groups"})
|
|
return
|
|
}
|
|
|
|
RespondSuccess(c, http.StatusOK, gin.H{
|
|
"groups": groups,
|
|
"total": total,
|
|
})
|
|
}
|
|
|
|
// GetGroup returns a group by ID. When authenticated, includes user_status (is_member, role, has_pending_request).
|
|
func (h *GroupHandler) GetGroup(c *gin.Context) {
|
|
groupID, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid group ID"})
|
|
return
|
|
}
|
|
|
|
group, err := h.service.GetGroup(c.Request.Context(), groupID)
|
|
if err != nil {
|
|
if errors.Is(err, social.ErrGroupNotFound) {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "Group not found"})
|
|
return
|
|
}
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get group"})
|
|
return
|
|
}
|
|
|
|
resp := gin.H{
|
|
"id": group.ID,
|
|
"name": group.Name,
|
|
"description": group.Description,
|
|
"creator_id": group.CreatorID,
|
|
"avatar_url": group.AvatarURL,
|
|
"is_public": group.IsPublic,
|
|
"member_count": group.MemberCount,
|
|
"created_at": group.CreatedAt,
|
|
"updated_at": group.UpdatedAt,
|
|
}
|
|
|
|
if userID, ok := GetUserIDUUID(c); ok {
|
|
isMember, role, hasPendingRequest, err := h.service.GetGroupMemberStatus(c.Request.Context(), userID, groupID)
|
|
if err == nil {
|
|
resp["user_status"] = gin.H{
|
|
"is_member": isMember,
|
|
"role": role,
|
|
"has_pending_request": hasPendingRequest,
|
|
}
|
|
}
|
|
}
|
|
|
|
RespondSuccess(c, http.StatusOK, resp)
|
|
}
|
|
|
|
// JoinGroup adds the authenticated user to a group
|
|
func (h *GroupHandler) JoinGroup(c *gin.Context) {
|
|
userID, ok := GetUserIDUUID(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
groupID, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid group ID"})
|
|
return
|
|
}
|
|
|
|
if err := h.service.JoinGroup(c.Request.Context(), userID, groupID); err != nil {
|
|
if errors.Is(err, social.ErrGroupNotFound) {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "Group not found"})
|
|
return
|
|
}
|
|
if errors.Is(err, social.ErrAlreadyMember) {
|
|
c.JSON(http.StatusConflict, gin.H{"error": "Already a member"})
|
|
return
|
|
}
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to join group"})
|
|
return
|
|
}
|
|
|
|
RespondSuccess(c, http.StatusOK, gin.H{"message": "Joined group successfully"})
|
|
}
|
|
|
|
// LeaveGroup removes the authenticated user from a group
|
|
func (h *GroupHandler) LeaveGroup(c *gin.Context) {
|
|
userID, ok := GetUserIDUUID(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
groupID, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid group ID"})
|
|
return
|
|
}
|
|
|
|
if err := h.service.LeaveGroup(c.Request.Context(), userID, groupID); err != nil {
|
|
if errors.Is(err, social.ErrGroupNotFound) {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "Group not found"})
|
|
return
|
|
}
|
|
if errors.Is(err, social.ErrNotMember) {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Not a member of this group"})
|
|
return
|
|
}
|
|
if errors.Is(err, social.ErrCannotLeaveOwned) {
|
|
c.JSON(http.StatusForbidden, gin.H{"error": "Creator cannot leave group"})
|
|
return
|
|
}
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to leave group"})
|
|
return
|
|
}
|
|
|
|
RespondSuccess(c, http.StatusOK, gin.H{"message": "Left group successfully"})
|
|
}
|
|
|
|
// RequestToJoin creates a join request for private groups (S2.1)
|
|
func (h *GroupHandler) RequestToJoin(c *gin.Context) {
|
|
userID, ok := GetUserIDUUID(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
groupID, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid group ID"})
|
|
return
|
|
}
|
|
|
|
req, err := h.service.RequestToJoin(c.Request.Context(), userID, groupID)
|
|
if err != nil {
|
|
if errors.Is(err, social.ErrGroupNotFound) {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "Group not found"})
|
|
return
|
|
}
|
|
if errors.Is(err, social.ErrAlreadyMember) {
|
|
c.JSON(http.StatusConflict, gin.H{"error": "Already a member"})
|
|
return
|
|
}
|
|
if errors.Is(err, social.ErrRequestAlreadyExist) {
|
|
c.JSON(http.StatusConflict, gin.H{"error": "Join request already pending"})
|
|
return
|
|
}
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to request join"})
|
|
return
|
|
}
|
|
|
|
if req == nil {
|
|
RespondSuccess(c, http.StatusOK, gin.H{"message": "Joined group successfully"})
|
|
return
|
|
}
|
|
RespondSuccess(c, http.StatusCreated, req)
|
|
}
|
|
|
|
// ListJoinRequests returns pending join requests (admin/modo only) (S2.1)
|
|
func (h *GroupHandler) ListJoinRequests(c *gin.Context) {
|
|
userID, ok := GetUserIDUUID(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
groupID, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid group ID"})
|
|
return
|
|
}
|
|
|
|
requests, err := h.service.ListJoinRequests(c.Request.Context(), groupID, userID)
|
|
if err != nil {
|
|
if errors.Is(err, social.ErrForbidden) {
|
|
c.JSON(http.StatusForbidden, gin.H{"error": "Forbidden"})
|
|
return
|
|
}
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to list requests"})
|
|
return
|
|
}
|
|
|
|
RespondSuccess(c, http.StatusOK, gin.H{"requests": requests})
|
|
}
|
|
|
|
// ApproveJoinRequest approves a join request (S2.1)
|
|
func (h *GroupHandler) ApproveJoinRequest(c *gin.Context) {
|
|
userID, ok := GetUserIDUUID(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
requestID, err := uuid.Parse(c.Param("request_id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request ID"})
|
|
return
|
|
}
|
|
|
|
if err := h.service.ApproveJoinRequest(c.Request.Context(), requestID, userID); err != nil {
|
|
if errors.Is(err, social.ErrRequestNotFound) {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "Request not found"})
|
|
return
|
|
}
|
|
if errors.Is(err, social.ErrForbidden) {
|
|
c.JSON(http.StatusForbidden, gin.H{"error": "Forbidden"})
|
|
return
|
|
}
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to approve request"})
|
|
return
|
|
}
|
|
|
|
RespondSuccess(c, http.StatusOK, gin.H{"message": "Request approved"})
|
|
}
|
|
|
|
// RejectJoinRequest rejects a join request (S2.1)
|
|
func (h *GroupHandler) RejectJoinRequest(c *gin.Context) {
|
|
userID, ok := GetUserIDUUID(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
requestID, err := uuid.Parse(c.Param("request_id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request ID"})
|
|
return
|
|
}
|
|
|
|
if err := h.service.RejectJoinRequest(c.Request.Context(), requestID, userID); err != nil {
|
|
if errors.Is(err, social.ErrRequestNotFound) {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "Request not found"})
|
|
return
|
|
}
|
|
if errors.Is(err, social.ErrForbidden) {
|
|
c.JSON(http.StatusForbidden, gin.H{"error": "Forbidden"})
|
|
return
|
|
}
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to reject request"})
|
|
return
|
|
}
|
|
|
|
RespondSuccess(c, http.StatusOK, gin.H{"message": "Request rejected"})
|
|
}
|
|
|
|
// InviteMemberRequest is the DTO for inviting a member
|
|
type InviteMemberRequest struct {
|
|
Email *string `json:"email"`
|
|
UserID *uuid.UUID `json:"user_id"`
|
|
}
|
|
|
|
// InviteMember invites a user to join a group (S2.2)
|
|
func (h *GroupHandler) InviteMember(c *gin.Context) {
|
|
userID, ok := GetUserIDUUID(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
groupID, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid group ID"})
|
|
return
|
|
}
|
|
|
|
var req InviteMemberRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"})
|
|
return
|
|
}
|
|
|
|
var inviteeID uuid.UUID
|
|
if req.UserID != nil {
|
|
inviteeID = *req.UserID
|
|
} else if req.Email != nil {
|
|
inviteeID, err = h.service.ResolveInviteeByEmailOrID(c.Request.Context(), *req.Email)
|
|
if err != nil {
|
|
if errors.Is(err, social.ErrUserNotFound) {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
|
|
return
|
|
}
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to resolve user"})
|
|
return
|
|
}
|
|
} else {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "email or user_id required"})
|
|
return
|
|
}
|
|
|
|
if err := h.service.InviteMember(c.Request.Context(), userID, groupID, inviteeID); err != nil {
|
|
if errors.Is(err, social.ErrForbidden) {
|
|
c.JSON(http.StatusForbidden, gin.H{"error": "Forbidden"})
|
|
return
|
|
}
|
|
if errors.Is(err, social.ErrAlreadyMember) {
|
|
c.JSON(http.StatusConflict, gin.H{"error": "User is already a member"})
|
|
return
|
|
}
|
|
if errors.Is(err, social.ErrUserNotFound) {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
|
|
return
|
|
}
|
|
if err.Error() == "invitation already pending" {
|
|
c.JSON(http.StatusConflict, gin.H{"error": "Invitation already pending"})
|
|
return
|
|
}
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to invite member"})
|
|
return
|
|
}
|
|
|
|
RespondSuccess(c, http.StatusCreated, gin.H{"message": "Invitation sent"})
|
|
}
|
|
|
|
// UpdateMemberRoleRequest is the DTO for updating member role
|
|
type UpdateMemberRoleRequest struct {
|
|
Role string `json:"role" binding:"required,oneof=admin moderator member"`
|
|
}
|
|
|
|
// UpdateMemberRole updates a member's role (S2.3)
|
|
func (h *GroupHandler) UpdateMemberRole(c *gin.Context) {
|
|
userID, ok := GetUserIDUUID(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
groupID, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid group ID"})
|
|
return
|
|
}
|
|
|
|
targetUserID, err := uuid.Parse(c.Param("user_id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid user ID"})
|
|
return
|
|
}
|
|
|
|
var req UpdateMemberRoleRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"})
|
|
return
|
|
}
|
|
|
|
if err := h.service.UpdateMemberRole(c.Request.Context(), userID, groupID, targetUserID, req.Role); err != nil {
|
|
if errors.Is(err, social.ErrForbidden) {
|
|
c.JSON(http.StatusForbidden, gin.H{"error": "Forbidden"})
|
|
return
|
|
}
|
|
if errors.Is(err, social.ErrNotMember) {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "User is not a member"})
|
|
return
|
|
}
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update role"})
|
|
return
|
|
}
|
|
|
|
RespondSuccess(c, http.StatusOK, gin.H{"message": "Role updated"})
|
|
}
|
|
|
|
// ListMyGroups returns groups the user is a member of (S2.5)
|
|
func (h *GroupHandler) ListMyGroups(c *gin.Context) {
|
|
userID, ok := GetUserIDUUID(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "20"))
|
|
offset, _ := strconv.Atoi(c.DefaultQuery("offset", "0"))
|
|
if limit < 1 {
|
|
limit = 20
|
|
}
|
|
if limit > 100 {
|
|
limit = 100
|
|
}
|
|
if offset < 0 {
|
|
offset = 0
|
|
}
|
|
|
|
groups, total, err := h.service.ListMyGroups(c.Request.Context(), userID, limit, offset)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to list groups"})
|
|
return
|
|
}
|
|
|
|
RespondSuccess(c, http.StatusOK, gin.H{
|
|
"groups": groups,
|
|
"total": total,
|
|
})
|
|
}
|