package services import ( "context" "errors" "time" "gitea.hostxtra.co.uk/mrhid6/notely/backend/internal/application/dto" "gitea.hostxtra.co.uk/mrhid6/notely/backend/internal/domain/entities" "gitea.hostxtra.co.uk/mrhid6/notely/backend/internal/domain/repositories" "go.mongodb.org/mongo-driver/v2/bson" ) // SpaceService handles space operations type SpaceService struct { spaceRepo repositories.SpaceRepository membershipRepo repositories.MembershipRepository noteRepo repositories.NoteRepository categoryRepo repositories.CategoryRepository userRepo repositories.UserRepository permissionService *PermissionService } // NewSpaceService creates a new space service func NewSpaceService( spaceRepo repositories.SpaceRepository, membershipRepo repositories.MembershipRepository, noteRepo repositories.NoteRepository, categoryRepo repositories.CategoryRepository, userRepo repositories.UserRepository, permissionService *PermissionService, ) *SpaceService { return &SpaceService{ spaceRepo: spaceRepo, membershipRepo: membershipRepo, noteRepo: noteRepo, categoryRepo: categoryRepo, userRepo: userRepo, permissionService: permissionService, } } // GetPublicSpace returns a single publicly accessible space func (s *SpaceService) GetPublicSpace(ctx context.Context, spaceID bson.ObjectID) (*dto.SpaceDTO, error) { space, err := s.spaceRepo.GetSpaceByID(ctx, spaceID) if err != nil { return nil, err } if !space.IsPublic { return nil, errors.New("space is not public") } return dto.NewSpaceDTO(space), nil } // GetPublicSpaces returns all publicly accessible spaces func (s *SpaceService) GetPublicSpaces(ctx context.Context) ([]*dto.SpaceDTO, error) { spaces, err := s.spaceRepo.GetPublicSpaces(ctx) if err != nil { return nil, err } result := make([]*dto.SpaceDTO, len(spaces)) for i, space := range spaces { result[i] = dto.NewSpaceDTO(space) } return result, nil } // CreateSpace creates a new space owned by the user func (s *SpaceService) CreateSpace(ctx context.Context, userID bson.ObjectID, req *dto.CreateSpaceRequest) (*dto.SpaceDTO, error) { if allowed, err := s.canCreateSpace(ctx, userID); err != nil { return nil, err } else if !allowed { return nil, errors.New("insufficient permissions") } space := &entities.Space{ Name: req.Name, Description: req.Description, Icon: req.Icon, OwnerID: userID, IsPublic: req.IsPublic, } if err := s.spaceRepo.CreateSpace(ctx, space); err != nil { return nil, err } // Add user as initial member membership := &entities.Membership{ UserID: userID, SpaceID: space.ID, JoinedAt: time.Now(), } if err := s.membershipRepo.CreateMembership(ctx, membership); err != nil { // Delete space if membership creation fails s.spaceRepo.DeleteSpace(ctx, space.ID) return nil, err } return dto.NewSpaceDTO(space), nil } // GetUserSpaces retrieves all spaces for a user func (s *SpaceService) GetUserSpaces(ctx context.Context, userID bson.ObjectID) ([]*dto.SpaceDTO, error) { // Get all memberships for the user memberships, err := s.membershipRepo.GetUserMemberships(ctx, userID) if err != nil { return nil, err } var spaceDTOs []*dto.SpaceDTO for _, membership := range memberships { space, err := s.spaceRepo.GetSpaceByID(ctx, membership.SpaceID) if err != nil { continue // Skip spaces that can't be loaded } spaceDTOs = append(spaceDTOs, dto.NewSpaceDTO(space)) } return spaceDTOs, nil } // GetSpaceByID gets a space by ID (with authorization check) func (s *SpaceService) GetSpaceByID(ctx context.Context, spaceID, userID bson.ObjectID) (*dto.SpaceDTO, error) { // Verify user has access to this space if _, err := s.membershipRepo.GetUserMembership(ctx, userID, spaceID); err != nil { return nil, errors.New("unauthorized") } space, err := s.spaceRepo.GetSpaceByID(ctx, spaceID) if err != nil { return nil, err } return dto.NewSpaceDTO(space), nil } // UpdateSpace updates a space (owner only) func (s *SpaceService) UpdateSpace(ctx context.Context, spaceID, userID bson.ObjectID, updates *dto.CreateSpaceRequest) (*dto.SpaceDTO, error) { hasPermission, err := s.hasGlobalOrSpacePermission(ctx, userID, spaceID, "space.edit", "settings.edit") if err != nil { return nil, err } if !hasPermission { return nil, errors.New("unauthorized") } space, err := s.spaceRepo.GetSpaceByID(ctx, spaceID) if err != nil { return nil, err } space.Name = updates.Name space.Description = updates.Description space.Icon = updates.Icon space.IsPublic = updates.IsPublic if err := s.spaceRepo.UpdateSpace(ctx, space); err != nil { return nil, err } return dto.NewSpaceDTO(space), nil } // DeleteSpace deletes a space (owner only) func (s *SpaceService) DeleteSpace(ctx context.Context, spaceID, userID bson.ObjectID) error { hasPermission, err := s.hasGlobalOrSpacePermission(ctx, userID, spaceID, "space.delete", "settings.delete") if err != nil { return err } if !hasPermission { return errors.New("unauthorized") } if err := s.noteRepo.DeleteNotesBySpaceID(ctx, spaceID); err != nil { return err } if err := s.categoryRepo.DeleteCategoriesBySpaceID(ctx, spaceID); err != nil { return err } if err := s.membershipRepo.DeleteMembershipsBySpaceID(ctx, spaceID); err != nil { return err } return s.spaceRepo.DeleteSpace(ctx, spaceID) } // AddMember adds a member to a space. func (s *SpaceService) AddMember(ctx context.Context, spaceID, userID, targetUserID bson.ObjectID) error { hasPermission, err := s.hasSpacePermission(ctx, userID, spaceID, "settings.member.manage") if err != nil { return err } if !hasPermission { return errors.New("unauthorized") } // Create membership for target user newMembership := &entities.Membership{ UserID: targetUserID, SpaceID: spaceID, JoinedAt: time.Now(), InvitedBy: userID, } return s.membershipRepo.CreateMembership(ctx, newMembership) } // RemoveMember removes a member from a space (owner only) func (s *SpaceService) RemoveMember(ctx context.Context, spaceID, userID, targetUserID bson.ObjectID) error { hasPermission, err := s.hasSpacePermission(ctx, userID, spaceID, "settings.member.manage") if err != nil { return err } if !hasPermission { return errors.New("unauthorized") } // Get target membership targetMembership, err := s.membershipRepo.GetUserMembership(ctx, targetUserID, spaceID) if err != nil { return err } return s.membershipRepo.DeleteMembership(ctx, targetMembership.ID) } // GetSpaceMembers returns all space members (owner only) func (s *SpaceService) GetSpaceMembers(ctx context.Context, spaceID, userID bson.ObjectID) ([]*dto.SpaceMemberDTO, error) { hasPermission, err := s.hasSpacePermission(ctx, userID, spaceID, "settings.member.view") if err != nil { return nil, err } if !hasPermission { return nil, errors.New("unauthorized") } memberships, err := s.membershipRepo.GetSpaceMembers(ctx, spaceID) if err != nil { return nil, err } result := make([]*dto.SpaceMemberDTO, 0, len(memberships)) for _, member := range memberships { username := member.UserID.Hex() if user, err := s.userRepo.GetUserByID(ctx, member.UserID); err == nil { username = user.Username } result = append(result, &dto.SpaceMemberDTO{ UserID: member.UserID.Hex(), Username: username, JoinedAt: member.JoinedAt.Format("2006-01-02T15:04:05Z"), }) } return result, nil } // ListAvailableUsers returns all users for member selection (owner only) func (s *SpaceService) ListAvailableUsers(ctx context.Context, spaceID, userID bson.ObjectID) ([]*dto.UserOptionDTO, error) { hasPermission, err := s.hasSpacePermission(ctx, userID, spaceID, "settings.member.manage") if err != nil { return nil, err } if !hasPermission { return nil, errors.New("unauthorized") } users, err := s.userRepo.ListAllUsers(ctx) if err != nil { return nil, err } result := make([]*dto.UserOptionDTO, 0, len(users)) for _, user := range users { result = append(result, &dto.UserOptionDTO{ ID: user.ID.Hex(), Username: user.Username, }) } return result, nil } func (s *SpaceService) canCreateSpace(ctx context.Context, userID bson.ObjectID) (bool, error) { if s.permissionService == nil { return false, errors.New("permission service unavailable") } hasPermission, err := s.permissionService.UserHasPermission(ctx, userID, "space.create") if err != nil { return false, err } return hasPermission, nil } func (s *SpaceService) hasSpacePermission(ctx context.Context, userID, spaceID bson.ObjectID, action string) (bool, error) { if s.permissionService == nil { return false, nil } return s.permissionService.HasSpacePermission(ctx, userID, spaceID, action) } func (s *SpaceService) hasGlobalOrSpacePermission(ctx context.Context, userID, spaceID bson.ObjectID, globalPermission, spaceAction string) (bool, error) { if s.permissionService == nil { return false, nil } hasGlobalPermission, err := s.permissionService.UserHasPermission(ctx, userID, globalPermission) if err != nil { return false, err } if hasGlobalPermission { return true, nil } return s.permissionService.HasSpacePermission(ctx, userID, spaceID, spaceAction) }