package services import ( "context" "errors" "strings" "go.mongodb.org/mongo-driver/v2/bson" "github.com/noteapp/backend/internal/application/dto" "github.com/noteapp/backend/internal/domain/entities" "github.com/noteapp/backend/internal/domain/repositories" "github.com/noteapp/backend/internal/infrastructure/security" ) // AdminService handles admin-level operations type AdminService struct { userRepo repositories.UserRepository groupRepo repositories.GroupRepository spaceRepo repositories.SpaceRepository membershipRepo repositories.MembershipRepository noteRepo repositories.NoteRepository categoryRepo repositories.CategoryRepository featureFlagRepo repositories.FeatureFlagRepository permissionService *PermissionService encryptor *security.Encryptor } // NewAdminService creates a new AdminService func NewAdminService( userRepo repositories.UserRepository, groupRepo repositories.GroupRepository, spaceRepo repositories.SpaceRepository, membershipRepo repositories.MembershipRepository, noteRepo repositories.NoteRepository, categoryRepo repositories.CategoryRepository, featureFlagRepo repositories.FeatureFlagRepository, permissionService *PermissionService, encryptor *security.Encryptor, ) *AdminService { return &AdminService{ userRepo: userRepo, groupRepo: groupRepo, spaceRepo: spaceRepo, membershipRepo: membershipRepo, noteRepo: noteRepo, categoryRepo: categoryRepo, featureFlagRepo: featureFlagRepo, permissionService: permissionService, encryptor: encryptor, } } // ListUsers returns all users as admin DTOs func (s *AdminService) ListUsers(ctx context.Context) ([]*dto.AdminUserDTO, error) { users, err := s.userRepo.ListAllUsers(ctx) if err != nil { return nil, err } result := make([]*dto.AdminUserDTO, len(users)) for i, u := range users { if s.permissionService != nil { permissions, err := s.permissionService.GetUserEffectivePermissions(ctx, u) if err == nil { u.Permissions = permissions } } result[i] = dto.NewAdminUserDTO(u) } return result, nil } // ListGroups returns all permission groups. func (s *AdminService) ListGroups(ctx context.Context) ([]*dto.PermissionGroupDTO, error) { groups, err := s.groupRepo.ListGroups(ctx) if err != nil { return nil, err } result := make([]*dto.PermissionGroupDTO, len(groups)) for i, group := range groups { result[i] = dto.NewPermissionGroupDTO(group) } return result, nil } // CreateGroup creates a new permission group. func (s *AdminService) CreateGroup(ctx context.Context, req *dto.CreatePermissionGroupRequest) (*dto.PermissionGroupDTO, error) { name := strings.TrimSpace(req.Name) if name == "" { return nil, errors.New("group name is required") } group := &entities.PermissionGroup{ Name: name, Description: strings.TrimSpace(req.Description), Permissions: normalizePermissions(req.Permissions), IsSystem: false, } if err := s.groupRepo.CreateGroup(ctx, group); err != nil { return nil, err } return dto.NewPermissionGroupDTO(group), nil } // UpdateGroup updates a permission group. func (s *AdminService) UpdateGroup(ctx context.Context, groupID bson.ObjectID, req *dto.UpdatePermissionGroupRequest) (*dto.PermissionGroupDTO, error) { group, err := s.groupRepo.GetGroupByID(ctx, groupID) if err != nil { return nil, err } if group.IsSystem { return nil, errors.New("system groups cannot be modified") } if name := strings.TrimSpace(req.Name); name != "" { group.Name = name } group.Description = strings.TrimSpace(req.Description) group.Permissions = normalizePermissions(req.Permissions) if err := s.groupRepo.UpdateGroup(ctx, group); err != nil { return nil, err } if err := s.refreshAllUserPermissions(ctx); err != nil { return nil, err } return dto.NewPermissionGroupDTO(group), nil } // UpdateUserGroups assigns groups to a user. func (s *AdminService) UpdateUserGroups(ctx context.Context, userID bson.ObjectID, groupIDs []bson.ObjectID) (*dto.AdminUserDTO, error) { if s.permissionService == nil { return nil, errors.New("permission service unavailable") } user, err := s.permissionService.SetUserGroups(ctx, userID, groupIDs) if err != nil { return nil, err } return dto.NewAdminUserDTO(user), nil } func (s *AdminService) refreshAllUserPermissions(ctx context.Context) error { if s.permissionService == nil { return nil } users, err := s.userRepo.ListAllUsers(ctx) if err != nil { return err } for _, user := range users { if err := s.permissionService.UpdateUserEffectivePermissions(ctx, user); err != nil { return err } } return nil } func normalizePermissions(permissions []string) []string { unique := map[string]struct{}{} result := make([]string, 0, len(permissions)) for _, permission := range permissions { normalized := entities.NormalizePermission(permission) if normalized == "" { continue } if _, exists := unique[normalized]; exists { continue } unique[normalized] = struct{}{} result = append(result, normalized) } return result } // ListAllSpaces returns all spaces func (s *AdminService) ListAllSpaces(ctx context.Context) ([]*dto.SpaceDTO, error) { spaces, err := s.spaceRepo.GetAllSpaces(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 } // UpdateSpace updates all editable space fields func (s *AdminService) UpdateSpace(ctx context.Context, spaceID bson.ObjectID, req *dto.CreateSpaceRequest) (*dto.SpaceDTO, error) { space, err := s.spaceRepo.GetSpaceByID(ctx, spaceID) if err != nil { return nil, err } space.Name = req.Name space.Description = req.Description space.Icon = req.Icon space.IsPublic = req.IsPublic if err := s.spaceRepo.UpdateSpace(ctx, space); err != nil { return nil, err } return dto.NewSpaceDTO(space), nil } // SetSpaceVisibility sets the is_public flag on a space func (s *AdminService) SetSpaceVisibility(ctx context.Context, spaceID bson.ObjectID, isPublic bool) error { space, err := s.spaceRepo.GetSpaceByID(ctx, spaceID) if err != nil { return err } space.IsPublic = isPublic return s.spaceRepo.UpdateSpace(ctx, space) } // AddSpaceMember adds a member in a space if not already present. func (s *AdminService) AddSpaceMember(ctx context.Context, spaceID, userID bson.ObjectID) error { existing, err := s.membershipRepo.GetUserMembership(ctx, userID, spaceID) if err == nil && existing != nil { return nil } return s.membershipRepo.CreateMembership(ctx, &entities.Membership{ UserID: userID, SpaceID: spaceID, }) } // ListSpaceMembers returns all members for a space func (s *AdminService) ListSpaceMembers(ctx context.Context, spaceID bson.ObjectID) ([]*dto.SpaceMemberDTO, error) { 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 } // RemoveSpaceMember removes a member from a space. func (s *AdminService) RemoveSpaceMember(ctx context.Context, spaceID, userID bson.ObjectID) error { membership, err := s.membershipRepo.GetUserMembership(ctx, userID, spaceID) if err != nil { return err } return s.membershipRepo.DeleteMembership(ctx, membership.ID) } // DeleteSpace deletes a space and all associated data (admin, no permission check). func (s *AdminService) DeleteSpace(ctx context.Context, spaceID bson.ObjectID) error { 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) } // GetFeatureFlags returns current app-wide feature flags. func (s *AdminService) GetFeatureFlags(ctx context.Context) (*dto.FeatureFlagsDTO, error) { if s.featureFlagRepo == nil { return dto.NewFeatureFlagsDTO(nil), nil } flags, err := s.featureFlagRepo.GetFeatureFlags(ctx) if err != nil { return nil, err } return dto.NewFeatureFlagsDTO(flags), nil } // UpdateFeatureFlags updates app-wide feature flags. func (s *AdminService) UpdateFeatureFlags(ctx context.Context, req *dto.UpdateFeatureFlagsRequest) (*dto.FeatureFlagsDTO, error) { if s.featureFlagRepo == nil { return nil, errors.New("feature flags are unavailable") } // Load existing flags so we can preserve the encrypted S3 secret when not updated existing, err := s.featureFlagRepo.GetFeatureFlags(ctx) if err != nil { existing = entities.NewDefaultFeatureFlags() } flags := &entities.FeatureFlags{ RegistrationEnabled: req.RegistrationEnabled, ProviderLoginEnabled: req.ProviderLoginEnabled, PublicSharingEnabled: req.PublicSharingEnabled, FileExplorerEnabled: req.FileExplorerEnabled, S3Endpoint: strings.TrimSpace(req.S3Endpoint), S3Bucket: strings.TrimSpace(req.S3Bucket), S3Region: strings.TrimSpace(req.S3Region), S3AccessKey: strings.TrimSpace(req.S3AccessKey), S3SecretKey: existing.S3SecretKey, // keep encrypted secret by default } // Only re-encrypt if a new secret was supplied if s.encryptor != nil && strings.TrimSpace(req.S3SecretKey) != "" { encrypted, err := s.encryptor.Encrypt(strings.TrimSpace(req.S3SecretKey)) if err != nil { return nil, err } flags.S3SecretKey = encrypted } if err := s.featureFlagRepo.UpdateFeatureFlags(ctx, flags); err != nil { return nil, err } return dto.NewFeatureFlagsDTO(flags), nil }