Files
notely/backend/internal/application/services/space_service.go
2026-03-26 16:27:14 +00:00

320 lines
9.1 KiB
Go

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)
}