183 lines
5.8 KiB
Go
183 lines
5.8 KiB
Go
// monorepo/cloud/backend/internal/maplefile/service/collection/remove_member.go
|
|
package collection
|
|
|
|
import (
|
|
"context"
|
|
|
|
"go.uber.org/zap"
|
|
|
|
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
|
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config/constants"
|
|
dom_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/collection"
|
|
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
|
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/transaction"
|
|
"github.com/gocql/gocql"
|
|
)
|
|
|
|
type RemoveMemberRequestDTO struct {
|
|
CollectionID gocql.UUID `json:"collection_id"`
|
|
RecipientID gocql.UUID `json:"recipient_id"`
|
|
RemoveFromDescendants bool `json:"remove_from_descendants"`
|
|
}
|
|
|
|
type RemoveMemberResponseDTO struct {
|
|
Success bool `json:"success"`
|
|
Message string `json:"message"`
|
|
}
|
|
|
|
type RemoveMemberService interface {
|
|
Execute(ctx context.Context, req *RemoveMemberRequestDTO) (*RemoveMemberResponseDTO, error)
|
|
}
|
|
|
|
type removeMemberServiceImpl struct {
|
|
config *config.Configuration
|
|
logger *zap.Logger
|
|
repo dom_collection.CollectionRepository
|
|
}
|
|
|
|
func NewRemoveMemberService(
|
|
config *config.Configuration,
|
|
logger *zap.Logger,
|
|
repo dom_collection.CollectionRepository,
|
|
) RemoveMemberService {
|
|
logger = logger.Named("RemoveMemberService")
|
|
return &removeMemberServiceImpl{
|
|
config: config,
|
|
logger: logger,
|
|
repo: repo,
|
|
}
|
|
}
|
|
|
|
func (svc *removeMemberServiceImpl) Execute(ctx context.Context, req *RemoveMemberRequestDTO) (*RemoveMemberResponseDTO, error) {
|
|
//
|
|
// STEP 1: Validation
|
|
//
|
|
if req == nil {
|
|
svc.logger.Warn("Failed validation with nil request")
|
|
return nil, httperror.NewForBadRequestWithSingleField("non_field_error", "Remove member details are required")
|
|
}
|
|
|
|
e := make(map[string]string)
|
|
if req.CollectionID.String() == "" {
|
|
e["collection_id"] = "Collection ID is required"
|
|
}
|
|
if req.RecipientID.String() == "" {
|
|
e["recipient_id"] = "Recipient ID is required"
|
|
}
|
|
|
|
if len(e) != 0 {
|
|
svc.logger.Warn("Failed validation",
|
|
zap.Any("error", e))
|
|
return nil, httperror.NewForBadRequest(&e)
|
|
}
|
|
|
|
//
|
|
// STEP 2: Get user ID from context
|
|
//
|
|
userID, ok := ctx.Value(constants.SessionUserID).(gocql.UUID)
|
|
if !ok {
|
|
svc.logger.Error("Failed getting user ID from context")
|
|
return nil, httperror.NewForInternalServerErrorWithSingleField("message", "Authentication context error")
|
|
}
|
|
|
|
//
|
|
// STEP 3: Check if user has admin access to the collection
|
|
//
|
|
hasAccess, err := svc.repo.CheckAccess(ctx, req.CollectionID, userID, dom_collection.CollectionPermissionAdmin)
|
|
if err != nil {
|
|
svc.logger.Error("Failed to check access",
|
|
zap.Any("error", err),
|
|
zap.Any("collection_id", req.CollectionID),
|
|
zap.Any("user_id", userID))
|
|
return nil, err
|
|
}
|
|
|
|
// Collection owners and admin members can remove members
|
|
if !hasAccess {
|
|
isOwner, _ := svc.repo.IsCollectionOwner(ctx, req.CollectionID, userID)
|
|
|
|
if !isOwner {
|
|
svc.logger.Warn("Unauthorized member removal attempt",
|
|
zap.Any("user_id", userID),
|
|
zap.Any("collection_id", req.CollectionID))
|
|
return nil, httperror.NewForForbiddenWithSingleField("message", "You don't have permission to remove members from this collection")
|
|
}
|
|
}
|
|
|
|
//
|
|
// SAGA: Initialize distributed transaction manager
|
|
//
|
|
saga := transaction.NewSaga("remove-member", svc.logger)
|
|
|
|
//
|
|
// STEP 4: Retrieve the membership before removing (needed for compensation)
|
|
//
|
|
existingMembership, err := svc.repo.GetCollectionMembership(ctx, req.CollectionID, req.RecipientID)
|
|
if err != nil {
|
|
svc.logger.Error("Failed to get collection membership",
|
|
zap.Any("error", err),
|
|
zap.Any("collection_id", req.CollectionID),
|
|
zap.Any("recipient_id", req.RecipientID))
|
|
return nil, err
|
|
}
|
|
|
|
if existingMembership == nil {
|
|
svc.logger.Debug("Member not found in collection",
|
|
zap.Any("collection_id", req.CollectionID),
|
|
zap.Any("recipient_id", req.RecipientID))
|
|
return nil, httperror.NewForNotFoundWithSingleField("message", "Member not found in this collection")
|
|
}
|
|
|
|
//
|
|
// STEP 5: Remove the member
|
|
//
|
|
var err2 error
|
|
|
|
if req.RemoveFromDescendants {
|
|
err2 = svc.repo.RemoveMemberFromHierarchy(ctx, req.CollectionID, req.RecipientID)
|
|
} else {
|
|
err2 = svc.repo.RemoveMember(ctx, req.CollectionID, req.RecipientID)
|
|
}
|
|
|
|
if err2 != nil {
|
|
svc.logger.Error("Failed to remove member",
|
|
zap.Any("error", err2),
|
|
zap.Any("collection_id", req.CollectionID),
|
|
zap.Any("recipient_id", req.RecipientID),
|
|
zap.Bool("remove_from_descendants", req.RemoveFromDescendants))
|
|
saga.Rollback(ctx) // Rollback any previous operations
|
|
return nil, err2
|
|
}
|
|
|
|
//
|
|
// SAGA: Register compensation to re-add the member if needed
|
|
// IMPORTANT: Capture by value for closure
|
|
//
|
|
membershipCaptured := existingMembership
|
|
collectionIDCaptured := req.CollectionID
|
|
removeFromDescendantsCaptured := req.RemoveFromDescendants
|
|
|
|
saga.AddCompensation(func(ctx context.Context) error {
|
|
svc.logger.Warn("SAGA compensation: re-adding member to collection",
|
|
zap.String("collection_id", collectionIDCaptured.String()),
|
|
zap.String("recipient_id", membershipCaptured.RecipientID.String()),
|
|
zap.Bool("add_to_descendants", removeFromDescendantsCaptured))
|
|
|
|
if removeFromDescendantsCaptured {
|
|
// Re-add to hierarchy if it was removed from hierarchy
|
|
return svc.repo.AddMemberToHierarchy(ctx, collectionIDCaptured, membershipCaptured)
|
|
}
|
|
// Re-add to single collection if it was removed from single collection
|
|
return svc.repo.AddMember(ctx, collectionIDCaptured, membershipCaptured)
|
|
})
|
|
|
|
svc.logger.Info("Member removed successfully",
|
|
zap.Any("collection_id", req.CollectionID),
|
|
zap.Any("recipient_id", req.RecipientID),
|
|
zap.Bool("removed_from_descendants", req.RemoveFromDescendants))
|
|
|
|
return &RemoveMemberResponseDTO{
|
|
Success: true,
|
|
Message: "Member removed successfully",
|
|
}, nil
|
|
}
|