first commit

This commit is contained in:
domrichardson
2026-03-24 16:03:04 +00:00
commit df40cc57e1
80 changed files with 16766 additions and 0 deletions

View File

@@ -0,0 +1,313 @@
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
}