314 lines
8.9 KiB
Go
314 lines
8.9 KiB
Go
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"
|
|
)
|
|
|
|
// 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
|
|
}
|
|
|
|
// 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,
|
|
) *AdminService {
|
|
return &AdminService{
|
|
userRepo: userRepo,
|
|
groupRepo: groupRepo,
|
|
spaceRepo: spaceRepo,
|
|
membershipRepo: membershipRepo,
|
|
noteRepo: noteRepo,
|
|
categoryRepo: categoryRepo,
|
|
featureFlagRepo: featureFlagRepo,
|
|
permissionService: permissionService,
|
|
}
|
|
}
|
|
|
|
// 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")
|
|
}
|
|
|
|
flags := &entities.FeatureFlags{
|
|
RegistrationEnabled: req.RegistrationEnabled,
|
|
ProviderLoginEnabled: req.ProviderLoginEnabled,
|
|
PublicSharingEnabled: req.PublicSharingEnabled,
|
|
}
|
|
|
|
if err := s.featureFlagRepo.UpdateFeatureFlags(ctx, flags); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return dto.NewFeatureFlagsDTO(flags), nil
|
|
}
|