320 lines
9.1 KiB
Go
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)
|
|
}
|