diff --git a/modules/identity/src/Volo.Abp.Identity.Application.Contracts/Volo/Abp/Identity/IdentityUserCreateOrUpdateDtoBase.cs b/modules/identity/src/Volo.Abp.Identity.Application.Contracts/Volo/Abp/Identity/IdentityUserCreateOrUpdateDtoBase.cs index ec8b4ef213..c18f2c16b5 100644 --- a/modules/identity/src/Volo.Abp.Identity.Application.Contracts/Volo/Abp/Identity/IdentityUserCreateOrUpdateDtoBase.cs +++ b/modules/identity/src/Volo.Abp.Identity.Application.Contracts/Volo/Abp/Identity/IdentityUserCreateOrUpdateDtoBase.cs @@ -1,4 +1,5 @@ -using System.ComponentModel.DataAnnotations; +using System; +using System.ComponentModel.DataAnnotations; using JetBrains.Annotations; using Volo.Abp.ObjectExtending; diff --git a/modules/identity/src/Volo.Abp.Identity.Application/Volo/Abp/Identity/IdentityUserAppService.cs b/modules/identity/src/Volo.Abp.Identity.Application/Volo/Abp/Identity/IdentityUserAppService.cs index 9d231b18d1..166d71fa5e 100644 --- a/modules/identity/src/Volo.Abp.Identity.Application/Volo/Abp/Identity/IdentityUserAppService.cs +++ b/modules/identity/src/Volo.Abp.Identity.Application/Volo/Abp/Identity/IdentityUserAppService.cs @@ -45,6 +45,8 @@ namespace Volo.Abp.Identity [Authorize(IdentityPermissions.Users.Default)] public virtual async Task> GetRolesAsync(Guid id) { + //TODO: Should also include roles of the related OUs. + var roles = await UserRepository.GetRolesAsync(id); return new ListResultDto( diff --git a/modules/identity/src/Volo.Abp.Identity.Application.Contracts/Volo/Abp/Identity/IdentityErrorCodes.cs b/modules/identity/src/Volo.Abp.Identity.Domain.Shared/Volo/Abp/Identity/IdentityErrorCodes.cs similarity index 53% rename from modules/identity/src/Volo.Abp.Identity.Application.Contracts/Volo/Abp/Identity/IdentityErrorCodes.cs rename to modules/identity/src/Volo.Abp.Identity.Domain.Shared/Volo/Abp/Identity/IdentityErrorCodes.cs index 4bdfc5d8e9..d77ee9f5a9 100644 --- a/modules/identity/src/Volo.Abp.Identity.Application.Contracts/Volo/Abp/Identity/IdentityErrorCodes.cs +++ b/modules/identity/src/Volo.Abp.Identity.Domain.Shared/Volo/Abp/Identity/IdentityErrorCodes.cs @@ -1,11 +1,8 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace Volo.Abp.Identity +namespace Volo.Abp.Identity { public static class IdentityErrorCodes { public const string UserSelfDeletion = "Volo.Abp.Identity:010001"; + public const string MaxAllowedOuMembership = "Volo.Abp.Identity:010002"; } } \ No newline at end of file diff --git a/modules/identity/src/Volo.Abp.Identity.Domain.Shared/Volo/Abp/Identity/Localization/en.json b/modules/identity/src/Volo.Abp.Identity.Domain.Shared/Volo/Abp/Identity/Localization/en.json index d6d6ca3983..8305ca9589 100644 --- a/modules/identity/src/Volo.Abp.Identity.Domain.Shared/Volo/Abp/Identity/Localization/en.json +++ b/modules/identity/src/Volo.Abp.Identity.Domain.Shared/Volo/Abp/Identity/Localization/en.json @@ -63,6 +63,8 @@ "Identity.PasswordConfirmationFailed": "Password does not match the confirm password.", "Identity.StaticRoleRenamingErrorMessage": "Static roles can not be renamed.", "Identity.StaticRoleDeletionErrorMessage": "Static roles can not be deleted.", + "Identity.OrganizationUnit.DuplicateDisplayNameWarning": "There is already an organization unit with name {0}. Two units with same name can not be created in same level.", + "Identity.OrganizationUnit.MaxUserMembershipCount": "Maximum allowed organization unit membership count for a user", "Volo.Abp.Identity:010001": "You can not delete your own account!", "Permission:IdentityManagement": "Identity management", "Permission:RoleManagement": "Role management", @@ -99,6 +101,7 @@ "Description:Abp.Identity.SignIn.EnablePhoneNumberConfirmation": "Whether the phoneNumber can be confirmed by the user.", "Description:Abp.Identity.SignIn.RequireConfirmedPhoneNumber": "Whether a confirmed telephone number is required to sign in.", "Description:Abp.Identity.User.IsUserNameUpdateEnabled": "Whether the username can be updated by the user.", - "Description:Abp.Identity.User.IsEmailUpdateEnabled": "Whether the email can be updated by the user." + "Description:Abp.Identity.User.IsEmailUpdateEnabled": "Whether the email can be updated by the user.", + "Volo.Abp.Identity:010002": "Can not set more than {MaxUserMembershipCount} organization unit for a user!" } } diff --git a/modules/identity/src/Volo.Abp.Identity.Domain.Shared/Volo/Abp/Identity/Localization/pt-BR.json b/modules/identity/src/Volo.Abp.Identity.Domain.Shared/Volo/Abp/Identity/Localization/pt-BR.json index 4df719fffe..1dda38b2a5 100644 --- a/modules/identity/src/Volo.Abp.Identity.Domain.Shared/Volo/Abp/Identity/Localization/pt-BR.json +++ b/modules/identity/src/Volo.Abp.Identity.Domain.Shared/Volo/Abp/Identity/Localization/pt-BR.json @@ -63,6 +63,7 @@ "Identity.PasswordConfirmationFailed": "A senha não confere com a confirmação de senha.", "Identity.StaticRoleRenamingErrorMessage": "Perfis estáticos não podem ser renomeados.", "Identity.StaticRoleDeletionErrorMessage": "Perfis estáticos não podem ser excluídos.", + "Identity.OrganizationUnit.DuplicateDisplayNameWarning": "Já existe uma unidade organizacional com o nome {0}. Duas unidades com o mesmo nome não pode ser criada no mesmo nível.", "Volo.Abp.Identity:010001": "Você não pode deletar sua própria conta!", "Permission:IdentityManagement": "Acessos", "Permission:RoleManagement": "Perfis", diff --git a/modules/identity/src/Volo.Abp.Identity.Domain.Shared/Volo/Abp/Identity/Localization/tr.json b/modules/identity/src/Volo.Abp.Identity.Domain.Shared/Volo/Abp/Identity/Localization/tr.json index 82efcf8f8a..0b17e51276 100644 --- a/modules/identity/src/Volo.Abp.Identity.Domain.Shared/Volo/Abp/Identity/Localization/tr.json +++ b/modules/identity/src/Volo.Abp.Identity.Domain.Shared/Volo/Abp/Identity/Localization/tr.json @@ -63,6 +63,8 @@ "Identity.UserNameNotFound": "{0} kullanıcısı bulunamadı.", "Identity.UserNotInRole": "Kullanıcı '{0}' rolünde değil.", "Identity.PasswordConfirmationFailed": "Yeni şifre ile onay şifresi uyuşmuyor.", + "Identity.OrganizationUnit.DuplicateDisplayNameWarning": "{0} isminde bir birim zaten var. Aynı seviyede aynı isimli iki birim olamaz.", + "Identity.OrganizationUnit.MaxUserMembershipCount": "Bir kullanıcı için izin verilen en fazla organizasyon birimi sayısı", "Volo.Abp.Identity:010001": "Kendi hesabınızı silemezsiniz!", "Permission:IdentityManagement": "Kimlik yönetimi", "Permission:RoleManagement": "Rol yönetimi", @@ -72,7 +74,8 @@ "Permission:ChangePermissions": "İzinleri değiştirme", "Permission:UserManagement": "Kullanıcı yönetimi", "Permission:UserLookup": "Kullanıcı sorgulama", - "DisplayName:Abp.Identity.Password.RequiredLength": "Uzunluk gerekli", + "Volo.Abp.Identity:010002": "Bir kullanıcı en fazla {MaxUserMembershipCount} organizasyon birimine üye olabilir!", + "DisplayName:Abp.Identity.Password.RequiredLength": "Uzunluk gerekli", "DisplayName:Abp.Identity.Password.RequiredUniqueChars": "Tekil karakter gerekli", "DisplayName:Abp.Identity.Password.RequireNonAlphanumeric": "Alfasayısal olmayan karakter gerekli", "DisplayName:Abp.Identity.Password.RequireLowercase": "Küçük harf gerekli", diff --git a/modules/identity/src/Volo.Abp.Identity.Domain.Shared/Volo/Abp/Identity/Localization/zh-Hans.json b/modules/identity/src/Volo.Abp.Identity.Domain.Shared/Volo/Abp/Identity/Localization/zh-Hans.json index 4908f170fa..ffeb4d0d40 100644 --- a/modules/identity/src/Volo.Abp.Identity.Domain.Shared/Volo/Abp/Identity/Localization/zh-Hans.json +++ b/modules/identity/src/Volo.Abp.Identity.Domain.Shared/Volo/Abp/Identity/Localization/zh-Hans.json @@ -63,6 +63,7 @@ "Identity.PasswordConfirmationFailed": "密码或确认密码不一致.", "Identity.StaticRoleRenamingErrorMessage": "无法重命名静态角色.", "Identity.StaticRoleDeletionErrorMessage": "无法删除静态角色.", + "Identity.OrganizationUnit.DuplicateDisplayNameWarning": "已存在名为 {0} 的组织单位. 无法在同一级别创建相同名称的组织单位.", "Volo.Abp.Identity:010001": "您无法删除自己的帐户!", "Permission:IdentityManagement": "身份标识管理", "Permission:RoleManagement": "角色管理", diff --git a/modules/identity/src/Volo.Abp.Identity.Domain.Shared/Volo/Abp/Identity/OrganizationUnitConsts.cs b/modules/identity/src/Volo.Abp.Identity.Domain.Shared/Volo/Abp/Identity/OrganizationUnitConsts.cs new file mode 100644 index 0000000000..ad6091a44e --- /dev/null +++ b/modules/identity/src/Volo.Abp.Identity.Domain.Shared/Volo/Abp/Identity/OrganizationUnitConsts.cs @@ -0,0 +1,25 @@ +namespace Volo.Abp.Identity +{ + public static class OrganizationUnitConsts + { + /// + /// Maximum length of the property. + /// + public const int MaxDisplayNameLength = 128; + + /// + /// Maximum depth of an OU hierarchy. + /// + public const int MaxDepth = 16; + + /// + /// Length of a code unit between dots. + /// + public const int CodeUnitLength = 5; + + /// + /// Maximum length of the property. + /// + public const int MaxCodeLength = MaxDepth * (CodeUnitLength + 1) - 1; + } +} diff --git a/modules/identity/src/Volo.Abp.Identity.Domain.Shared/Volo/Abp/Identity/Settings/IdentitySettingNames.cs b/modules/identity/src/Volo.Abp.Identity.Domain.Shared/Volo/Abp/Identity/Settings/IdentitySettingNames.cs index 9c07f6c55b..eb7a88407f 100644 --- a/modules/identity/src/Volo.Abp.Identity.Domain.Shared/Volo/Abp/Identity/Settings/IdentitySettingNames.cs +++ b/modules/identity/src/Volo.Abp.Identity.Domain.Shared/Volo/Abp/Identity/Settings/IdentitySettingNames.cs @@ -41,5 +41,12 @@ public const string IsUserNameUpdateEnabled = UserPrefix + ".IsUserNameUpdateEnabled"; public const string IsEmailUpdateEnabled = UserPrefix + ".IsEmailUpdateEnabled"; } + + public static class OrganizationUnit + { + private const string OrganizationUnitPrefix = Prefix + ".OrganizationUnit"; + + public const string MaxUserMembershipCount = OrganizationUnitPrefix + ".MaxUserMembershipCount"; + } } } \ No newline at end of file diff --git a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/AbpIdentitySettingDefinitionProvider.cs b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/AbpIdentitySettingDefinitionProvider.cs index 92847004ea..1fdd2dd451 100644 --- a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/AbpIdentitySettingDefinitionProvider.cs +++ b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/AbpIdentitySettingDefinitionProvider.cs @@ -11,88 +11,106 @@ namespace Volo.Abp.Identity { context.Add( new SettingDefinition( - IdentitySettingNames.Password.RequiredLength, - 6.ToString(), - L("DisplayName:Abp.Identity.Password.RequiredLength"), - L("Description:Abp.Identity.Password.RequiredLength"), + IdentitySettingNames.Password.RequiredLength, + 6.ToString(), + L("DisplayName:Abp.Identity.Password.RequiredLength"), + L("Description:Abp.Identity.Password.RequiredLength"), true), new SettingDefinition( - IdentitySettingNames.Password.RequiredUniqueChars, - 1.ToString(), - L("DisplayName:Abp.Identity.Password.RequiredUniqueChars"), - L("Description:Abp.Identity.Password.RequiredUniqueChars"), + IdentitySettingNames.Password.RequiredUniqueChars, + 1.ToString(), + L("DisplayName:Abp.Identity.Password.RequiredUniqueChars"), + L("Description:Abp.Identity.Password.RequiredUniqueChars"), true), new SettingDefinition( - IdentitySettingNames.Password.RequireNonAlphanumeric, - true.ToString(), - L("DisplayName:Abp.Identity.Password.RequireNonAlphanumeric"), - L("Description:Abp.Identity.Password.RequireNonAlphanumeric"), + IdentitySettingNames.Password.RequireNonAlphanumeric, + true.ToString(), + L("DisplayName:Abp.Identity.Password.RequireNonAlphanumeric"), + L("Description:Abp.Identity.Password.RequireNonAlphanumeric"), true), new SettingDefinition( - IdentitySettingNames.Password.RequireLowercase, - true.ToString(), L("DisplayName:Abp.Identity.Password.RequireLowercase"), - L("Description:Abp.Identity.Password.RequireLowercase"), + IdentitySettingNames.Password.RequireLowercase, + true.ToString(), + L("DisplayName:Abp.Identity.Password.RequireLowercase"), + L("Description:Abp.Identity.Password.RequireLowercase"), true), new SettingDefinition( - IdentitySettingNames.Password.RequireUppercase, - true.ToString(), L("DisplayName:Abp.Identity.Password.RequireUppercase"), - L("Description:Abp.Identity.Password.RequireUppercase"), + IdentitySettingNames.Password.RequireUppercase, + true.ToString(), + L("DisplayName:Abp.Identity.Password.RequireUppercase"), + L("Description:Abp.Identity.Password.RequireUppercase"), true), new SettingDefinition( - IdentitySettingNames.Password.RequireDigit, - true.ToString(), L("DisplayName:Abp.Identity.Password.RequireDigit"), - L("Description:Abp.Identity.Password.RequireDigit"), + IdentitySettingNames.Password.RequireDigit, + true.ToString(), + L("DisplayName:Abp.Identity.Password.RequireDigit"), + L("Description:Abp.Identity.Password.RequireDigit"), true), new SettingDefinition( - IdentitySettingNames.Lockout.AllowedForNewUsers, - true.ToString(), L("DisplayName:Abp.Identity.Lockout.AllowedForNewUsers"), - L("Description:Abp.Identity.Lockout.AllowedForNewUsers"), + IdentitySettingNames.Lockout.AllowedForNewUsers, + true.ToString(), + L("DisplayName:Abp.Identity.Lockout.AllowedForNewUsers"), + L("Description:Abp.Identity.Lockout.AllowedForNewUsers"), true), - + new SettingDefinition( - IdentitySettingNames.Lockout.LockoutDuration, - (5*60).ToString(), L("DisplayName:Abp.Identity.Lockout.LockoutDuration"), - L("Description:Abp.Identity.Lockout.LockoutDuration"), + IdentitySettingNames.Lockout.LockoutDuration, + (5 * 60).ToString(), + L("DisplayName:Abp.Identity.Lockout.LockoutDuration"), + L("Description:Abp.Identity.Lockout.LockoutDuration"), true), new SettingDefinition( - IdentitySettingNames.Lockout.MaxFailedAccessAttempts, - 5.ToString(), L("DisplayName:Abp.Identity.Lockout.MaxFailedAccessAttempts"), - L("Description:Abp.Identity.Lockout.MaxFailedAccessAttempts"), + IdentitySettingNames.Lockout.MaxFailedAccessAttempts, + 5.ToString(), + L("DisplayName:Abp.Identity.Lockout.MaxFailedAccessAttempts"), + L("Description:Abp.Identity.Lockout.MaxFailedAccessAttempts"), true), new SettingDefinition( - IdentitySettingNames.SignIn.RequireConfirmedEmail, - false.ToString(), L("DisplayName:Abp.Identity.SignIn.RequireConfirmedEmail"), - L("Description:Abp.Identity.SignIn.RequireConfirmedEmail"), + IdentitySettingNames.SignIn.RequireConfirmedEmail, + false.ToString(), + L("DisplayName:Abp.Identity.SignIn.RequireConfirmedEmail"), + L("Description:Abp.Identity.SignIn.RequireConfirmedEmail"), true), new SettingDefinition( IdentitySettingNames.SignIn.EnablePhoneNumberConfirmation, - true.ToString(), L("DisplayName:Abp.Identity.SignIn.EnablePhoneNumberConfirmation"), + true.ToString(), + L("DisplayName:Abp.Identity.SignIn.EnablePhoneNumberConfirmation"), L("Description:Abp.Identity.SignIn.EnablePhoneNumberConfirmation"), true), new SettingDefinition( IdentitySettingNames.SignIn.RequireConfirmedPhoneNumber, - false.ToString(), L("DisplayName:Abp.Identity.SignIn.RequireConfirmedPhoneNumber"), + false.ToString(), + L("DisplayName:Abp.Identity.SignIn.RequireConfirmedPhoneNumber"), L("Description:Abp.Identity.SignIn.RequireConfirmedPhoneNumber"), true), new SettingDefinition( - IdentitySettingNames.User.IsUserNameUpdateEnabled, - true.ToString(), L("DisplayName:Abp.Identity.User.IsUserNameUpdateEnabled"), - L("Description:Abp.Identity.User.IsUserNameUpdateEnabled"), + IdentitySettingNames.User.IsUserNameUpdateEnabled, + true.ToString(), + L("DisplayName:Abp.Identity.User.IsUserNameUpdateEnabled"), + L("Description:Abp.Identity.User.IsUserNameUpdateEnabled"), true), new SettingDefinition( - IdentitySettingNames.User.IsEmailUpdateEnabled, - true.ToString(), L("DisplayName:Abp.Identity.User.IsEmailUpdateEnabled"), - L("Description:Abp.Identity.User.IsEmailUpdateEnabled"), + IdentitySettingNames.User.IsEmailUpdateEnabled, + true.ToString(), + L("DisplayName:Abp.Identity.User.IsEmailUpdateEnabled"), + L("Description:Abp.Identity.User.IsEmailUpdateEnabled"), + true), + + new SettingDefinition( + IdentitySettingNames.OrganizationUnit.MaxUserMembershipCount, + int.MaxValue.ToString(), + L("Identity.OrganizationUnit.MaxUserMembershipCount"), + L("Identity.OrganizationUnit.MaxUserMembershipCount"), true) ); } diff --git a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IIdentityRoleRepository.cs b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IIdentityRoleRepository.cs index 3a344b3140..9596d34cfb 100644 --- a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IIdentityRoleRepository.cs +++ b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IIdentityRoleRepository.cs @@ -20,6 +20,10 @@ namespace Volo.Abp.Identity int skipCount = 0, bool includeDetails = false, CancellationToken cancellationToken = default + ); + Task> GetListAsync( + IEnumerable ids, + CancellationToken cancellationToken = default ); Task> GetDefaultOnesAsync( diff --git a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IIdentityUserRepository.cs b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IIdentityUserRepository.cs index 98269da005..3814ab96e3 100644 --- a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IIdentityUserRepository.cs +++ b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IIdentityUserRepository.cs @@ -21,6 +21,10 @@ namespace Volo.Abp.Identity CancellationToken cancellationToken = default ); + Task> GetRoleNamesInOrganizationUnitAsync( + Guid id, + CancellationToken cancellationToken = default); + Task FindByLoginAsync( [NotNull] string loginProvider, [NotNull] string providerKey, @@ -29,19 +33,19 @@ namespace Volo.Abp.Identity ); Task FindByNormalizedEmailAsync( - [NotNull] string normalizedEmail, + [NotNull] string normalizedEmail, bool includeDetails = true, CancellationToken cancellationToken = default ); Task> GetListByClaimAsync( - Claim claim, + Claim claim, bool includeDetails = false, CancellationToken cancellationToken = default ); Task> GetListByNormalizedRoleNameAsync( - string normalizedRoleName, + string normalizedRoleName, bool includeDetails = false, CancellationToken cancellationToken = default ); @@ -61,6 +65,25 @@ namespace Volo.Abp.Identity CancellationToken cancellationToken = default ); + Task> GetOrganizationUnitsAsync( + Guid id, + bool includeDetails = false, + CancellationToken cancellationToken = default); + + Task> GetUsersInOrganizationUnitAsync( + Guid organizationUnitId, + CancellationToken cancellationToken = default + ); + Task> GetUsersInOrganizationsListAsync( + List organizationUnitIds, + CancellationToken cancellationToken = default + ); + + Task> GetUsersInOrganizationUnitWithChildrenAsync( + string code, + CancellationToken cancellationToken = default + ); + Task GetCountAsync( string filter = null, CancellationToken cancellationToken = default diff --git a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IOrganizationUnitRepository.cs b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IOrganizationUnitRepository.cs new file mode 100644 index 0000000000..def41cdc8a --- /dev/null +++ b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IOrganizationUnitRepository.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Volo.Abp.Domain.Repositories; + +namespace Volo.Abp.Identity +{ + public interface IOrganizationUnitRepository : IBasicRepository + { + Task> GetChildrenAsync( + Guid? parentId, + bool includeDetails = false, + CancellationToken cancellationToken = default + ); + + Task> GetAllChildrenWithParentCodeAsync( + string code, + Guid? parentId, + bool includeDetails = false, + CancellationToken cancellationToken = default + ); + + Task GetAsync( + string displayName, + bool includeDetails = true, + CancellationToken cancellationToken = default + ); + + Task> GetListAsync( + string sorting = null, + int maxResultCount = int.MaxValue, + int skipCount = 0, + bool includeDetails = false, + CancellationToken cancellationToken = default + ); + + Task> GetListAsync( + IEnumerable ids, + bool includeDetails = false, + CancellationToken cancellationToken = default + ); + + Task> GetRolesAsync( + OrganizationUnit organizationUnit, + string sorting = null, + int maxResultCount = int.MaxValue, + int skipCount = 0, + bool includeDetails = false, + CancellationToken cancellationToken = default + ); + + Task GetRolesCountAsync( + OrganizationUnit organizationUnit, + CancellationToken cancellationToken = default + ); + + Task> GetMembersAsync( + OrganizationUnit organizationUnit, + string sorting = null, + int maxResultCount = int.MaxValue, + int skipCount = 0, + string filter = null, + bool includeDetails = false, + CancellationToken cancellationToken = default + ); + Task GetMembersCountAsync( + OrganizationUnit organizationUnit, + CancellationToken cancellationToken = default + ); + } +} diff --git a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityUser.cs b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityUser.cs index 51e15339ee..ea1f5ccf72 100644 --- a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityUser.cs +++ b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityUser.cs @@ -124,9 +124,13 @@ namespace Volo.Abp.Identity /// public virtual ICollection Tokens { get; protected set; } + /// + /// Navigation property for this organization units. + /// + public virtual ICollection OrganizationUnits { get; protected set; } + protected IdentityUser() { - ExtraProperties = new Dictionary(); } public IdentityUser(Guid id, [NotNull] string userName, [NotNull] string email, Guid? tenantId = null) @@ -147,6 +151,7 @@ namespace Volo.Abp.Identity Claims = new Collection(); Logins = new Collection(); Tokens = new Collection(); + OrganizationUnits = new Collection(); ExtraProperties = new Dictionary(); } @@ -276,6 +281,41 @@ namespace Volo.Abp.Identity Tokens.RemoveAll(t => t.LoginProvider == loginProvider && t.Name == name); } + public virtual void AddOrganizationUnit(Guid organizationUnitId) + { + if (IsInOrganizationUnit(organizationUnitId)) + { + return; + } + + OrganizationUnits.Add( + new IdentityUserOrganizationUnit( + Id, + organizationUnitId, + TenantId + ) + ); + } + + public virtual void RemoveOrganizationUnit(Guid organizationUnitId) + { + if (!IsInOrganizationUnit(organizationUnitId)) + { + return; + } + + OrganizationUnits.RemoveAll( + ou => ou.OrganizationUnitId == organizationUnitId + ); + } + + public virtual bool IsInOrganizationUnit(Guid organizationUnitId) + { + return OrganizationUnits.Any( + ou => ou.OrganizationUnitId == organizationUnitId + ); + } + public override string ToString() { return $"{base.ToString()}, UserName = {UserName}"; diff --git a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityUserManager.cs b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityUserManager.cs index 430a4822b4..f90694349a 100644 --- a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityUserManager.cs +++ b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityUserManager.cs @@ -11,6 +11,9 @@ using Volo.Abp.Domain.Entities; using Volo.Abp.Domain.Repositories; using Volo.Abp.Domain.Services; using Volo.Abp.Threading; +using Volo.Abp.Uow; +using Volo.Abp.Settings; +using Volo.Abp.Identity.Settings; namespace Volo.Abp.Identity { @@ -18,11 +21,13 @@ namespace Volo.Abp.Identity { protected IIdentityRoleRepository RoleRepository { get; } protected IIdentityUserRepository UserRepository { get; } + protected IOrganizationUnitRepository OrganizationUnitRepository { get; } + protected IIdentityUserRepository IdentityUserRepository { get; } + protected ISettingProvider SettingProvider { get; } + protected ICancellationTokenProvider CancellationTokenProvider { get; } protected override CancellationToken CancellationToken => CancellationTokenProvider.Token; - protected ICancellationTokenProvider CancellationTokenProvider { get; } - public IdentityUserManager( IdentityUserStore store, IIdentityRoleRepository roleRepository, @@ -35,7 +40,10 @@ namespace Volo.Abp.Identity IdentityErrorDescriber errors, IServiceProvider services, ILogger logger, - ICancellationTokenProvider cancellationTokenProvider) + ICancellationTokenProvider cancellationTokenProvider, + IOrganizationUnitRepository organizationUnitRepository, + IIdentityUserRepository identityUserRepository, + ISettingProvider settingProvider) : base( store, optionsAccessor, @@ -47,6 +55,9 @@ namespace Volo.Abp.Identity services, logger) { + OrganizationUnitRepository = organizationUnitRepository; + IdentityUserRepository = identityUserRepository; + SettingProvider = settingProvider; RoleRepository = roleRepository; UserRepository = userRepository; CancellationTokenProvider = cancellationTokenProvider; @@ -67,7 +78,7 @@ namespace Volo.Abp.Identity { Check.NotNull(user, nameof(user)); Check.NotNull(roleNames, nameof(roleNames)); - + var currentRoleNames = await GetRolesAsync(user); var result = await RemoveFromRolesAsync(user, currentRoleNames.Except(roleNames).Distinct()); @@ -85,10 +96,132 @@ namespace Volo.Abp.Identity return IdentityResult.Success; } + + public virtual async Task IsInOrganizationUnitAsync(Guid userId, Guid ouId) + { + var user = await IdentityUserRepository.GetAsync(userId, cancellationToken: CancellationToken); + return user.IsInOrganizationUnit(ouId); + } + + public virtual async Task IsInOrganizationUnitAsync(IdentityUser user, OrganizationUnit ou) + { + await IdentityUserRepository.EnsureCollectionLoadedAsync(user, u => u.OrganizationUnits, CancellationTokenProvider.Token); + return user.IsInOrganizationUnit(ou.Id); + } + + public virtual async Task AddToOrganizationUnitAsync(Guid userId, Guid ouId) + { + await AddToOrganizationUnitAsync( + await IdentityUserRepository.GetAsync(userId, cancellationToken: CancellationToken), + await OrganizationUnitRepository.GetAsync(ouId, cancellationToken: CancellationToken) + ); + } + + public virtual async Task AddToOrganizationUnitAsync(IdentityUser user, OrganizationUnit ou) + { + await IdentityUserRepository.EnsureCollectionLoadedAsync(user, u => u.OrganizationUnits, CancellationTokenProvider.Token); + + if (user.OrganizationUnits.Any(cou => cou.OrganizationUnitId == ou.Id)) + { + return; + } + + await CheckMaxUserOrganizationUnitMembershipCountAsync(user.OrganizationUnits.Count + 1); + + user.AddOrganizationUnit(ou.Id); + } + + public virtual async Task RemoveFromOrganizationUnitAsync(Guid userId, Guid ouId) + { + var user = await IdentityUserRepository.GetAsync(userId, cancellationToken: CancellationToken); + user.RemoveOrganizationUnit(ouId); + } + + public virtual async Task RemoveFromOrganizationUnitAsync(IdentityUser user, OrganizationUnit ou) + { + await IdentityUserRepository.EnsureCollectionLoadedAsync(user, u => u.OrganizationUnits, CancellationTokenProvider.Token); + + user.RemoveOrganizationUnit(ou.Id); + } + + public virtual async Task SetOrganizationUnitsAsync(Guid userId, params Guid[] organizationUnitIds) + { + await SetOrganizationUnitsAsync( + await IdentityUserRepository.GetAsync(userId, cancellationToken: CancellationToken), + organizationUnitIds + ); + } + + public virtual async Task SetOrganizationUnitsAsync(IdentityUser user, params Guid[] organizationUnitIds) + { + Check.NotNull(user, nameof(user)); + Check.NotNull(organizationUnitIds, nameof(organizationUnitIds)); + + await CheckMaxUserOrganizationUnitMembershipCountAsync(organizationUnitIds.Length); + + await IdentityUserRepository.EnsureCollectionLoadedAsync(user, u => u.OrganizationUnits, CancellationTokenProvider.Token); + + //Remove from removed OUs + foreach (var ouId in user.OrganizationUnits.Select(uou => uou.OrganizationUnitId).ToArray()) + { + if (!organizationUnitIds.Contains(ouId)) + { + user.RemoveOrganizationUnit(ouId); + } + } + + //Add to added OUs + foreach (var organizationUnitId in organizationUnitIds) + { + if (!user.IsInOrganizationUnit(organizationUnitId)) + { + user.AddOrganizationUnit(organizationUnitId); + } + } + } + + private async Task CheckMaxUserOrganizationUnitMembershipCountAsync(int requestedCount) + { + var maxCount = await SettingProvider.GetAsync(IdentitySettingNames.OrganizationUnit.MaxUserMembershipCount); + if (requestedCount > maxCount) + { + throw new BusinessException(IdentityErrorCodes.MaxAllowedOuMembership) + .WithData("MaxUserMembershipCount", maxCount); + } + } + + [UnitOfWork] + public virtual async Task> GetOrganizationUnitsAsync(IdentityUser user) + { + await IdentityUserRepository.EnsureCollectionLoadedAsync(user, u => u.OrganizationUnits, CancellationTokenProvider.Token); + + return await OrganizationUnitRepository.GetListAsync( + user.OrganizationUnits.Select(t => t.OrganizationUnitId), + cancellationToken: CancellationToken + ); + } + + [UnitOfWork] + public virtual async Task> GetUsersInOrganizationUnitAsync( + OrganizationUnit organizationUnit, + bool includeChildren = false) + { + if (includeChildren) + { + return await IdentityUserRepository + .GetUsersInOrganizationUnitWithChildrenAsync(organizationUnit.Code, CancellationToken); + } + else + { + return await IdentityUserRepository + .GetUsersInOrganizationUnitAsync(organizationUnit.Id, CancellationToken); + } + } + public virtual async Task AddDefaultRolesAsync([NotNull] IdentityUser user) { await UserRepository.EnsureCollectionLoadedAsync(user, u => u.Roles, CancellationToken); - + foreach (var role in await RoleRepository.GetDefaultOnesAsync(cancellationToken: CancellationToken)) { if (!user.IsInRole(role.Id)) @@ -96,7 +229,7 @@ namespace Volo.Abp.Identity user.AddRole(role.Id); } } - + return await UpdateUserAsync(user); } } diff --git a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityUserOrganizationUnit.cs b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityUserOrganizationUnit.cs new file mode 100644 index 0000000000..5bb0ed790d --- /dev/null +++ b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityUserOrganizationUnit.cs @@ -0,0 +1,44 @@ +using System; +using Volo.Abp.Domain.Entities.Auditing; +using Volo.Abp.MultiTenancy; + +namespace Volo.Abp.Identity +{ + /// + /// Represents membership of a User to an OU. + /// + public class IdentityUserOrganizationUnit : CreationAuditedEntity, IMultiTenant + { + /// + /// TenantId of this entity. + /// + public virtual Guid? TenantId { get; protected set; } + + /// + /// Id of the User. + /// + public virtual Guid UserId { get; protected set; } + + /// + /// Id of the related . + /// + public virtual Guid OrganizationUnitId { get; protected set; } + + protected IdentityUserOrganizationUnit() + { + + } + + public IdentityUserOrganizationUnit(Guid userId, Guid organizationUnitId, Guid? tenantId = null) + { + UserId = userId; + OrganizationUnitId = organizationUnitId; + TenantId = tenantId; + } + + public override object[] GetKeys() + { + return new object[] { UserId, OrganizationUnitId }; + } + } +} diff --git a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityUserStore.cs b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityUserStore.cs index ddf2dd4f4b..81269adf5a 100644 --- a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityUserStore.cs +++ b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityUserStore.cs @@ -53,6 +53,7 @@ namespace Volo.Abp.Identity protected IIdentityRoleRepository RoleRepository { get; } protected IGuidGenerator GuidGenerator { get; } protected ILogger Logger { get; } + protected ILookupNormalizer LookupNormalizer { get; } protected IIdentityUserRepository UserRepository { get; } public IdentityUserStore( @@ -60,12 +61,14 @@ namespace Volo.Abp.Identity IIdentityRoleRepository roleRepository, IGuidGenerator guidGenerator, ILogger logger, + ILookupNormalizer lookupNormalizer, IdentityErrorDescriber describer = null) { UserRepository = userRepository; RoleRepository = roleRepository; GuidGenerator = guidGenerator; Logger = logger; + LookupNormalizer = lookupNormalizer; ErrorDescriber = describer ?? new IdentityErrorDescriber(); } @@ -313,13 +316,17 @@ namespace Volo.Abp.Identity Check.NotNull(user, nameof(user)); Check.NotNull(normalizedRoleName, nameof(normalizedRoleName)); - var role = await RoleRepository.FindByNormalizedNameAsync(normalizedRoleName, cancellationToken: cancellationToken); + if (await IsInRoleAsync(user, normalizedRoleName, cancellationToken)) + { + return; + } + var role = await RoleRepository.FindByNormalizedNameAsync(normalizedRoleName, cancellationToken: cancellationToken); if (role == null) { throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, "Role {0} does not exist!", normalizedRoleName)); } - + await UserRepository.EnsureCollectionLoadedAsync(user, u => u.Roles, cancellationToken); user.AddRole(role.Id); @@ -337,11 +344,7 @@ namespace Volo.Abp.Identity cancellationToken.ThrowIfCancellationRequested(); Check.NotNull(user, nameof(user)); - - if (string.IsNullOrWhiteSpace(normalizedRoleName)) - { - throw new ArgumentException(nameof(normalizedRoleName) + " can not be null or whitespace"); - } + Check.NotNullOrWhiteSpace(normalizedRoleName, nameof(normalizedRoleName)); var role = await RoleRepository.FindByNormalizedNameAsync(normalizedRoleName, cancellationToken: cancellationToken); if (role == null) @@ -366,7 +369,13 @@ namespace Volo.Abp.Identity Check.NotNull(user, nameof(user)); - return await UserRepository.GetRoleNamesAsync(user.Id, cancellationToken: cancellationToken); + var userRoles = await UserRepository + .GetRoleNamesAsync(user.Id, cancellationToken: cancellationToken); + + var userOrganizationUnitRoles = await UserRepository + .GetRoleNamesInOrganizationUnitAsync(user.Id, cancellationToken: cancellationToken); + + return userRoles.Union(userOrganizationUnitRoles).ToList(); } /// @@ -377,26 +386,21 @@ namespace Volo.Abp.Identity /// The used to propagate notifications that the operation should be canceled. /// A containing a flag indicating if the specified user is a member of the given group. If the /// user is a member of the group the returned value with be true, otherwise it will be false. - public virtual async Task IsInRoleAsync([NotNull] IdentityUser user, [NotNull] string normalizedRoleName, CancellationToken cancellationToken = default) + public virtual async Task IsInRoleAsync( + [NotNull] IdentityUser user, + [NotNull] string normalizedRoleName, + CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); - + Check.NotNull(user, nameof(user)); + Check.NotNullOrWhiteSpace(normalizedRoleName, nameof(normalizedRoleName)); - if (string.IsNullOrWhiteSpace(normalizedRoleName)) - { - throw new ArgumentException(nameof(normalizedRoleName) + " can not be null or whitespace"); - } - - var role = await RoleRepository.FindByNormalizedNameAsync(normalizedRoleName, cancellationToken: cancellationToken); - if (role == null) - { - return false; - } - - await UserRepository.EnsureCollectionLoadedAsync(user, u => u.Roles, cancellationToken); + var roles = await GetRolesAsync(user, cancellationToken); - return user.IsInRole(role.Id); + return roles + .Select(r => LookupNormalizer.NormalizeName(r)) + .Contains(normalizedRoleName); } /// diff --git a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/OrganizationUnit.cs b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/OrganizationUnit.cs new file mode 100644 index 0000000000..3d1bbb0531 --- /dev/null +++ b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/OrganizationUnit.cs @@ -0,0 +1,212 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using Volo.Abp.Domain.Entities.Auditing; +using Volo.Abp.MultiTenancy; + +namespace Volo.Abp.Identity +{ + /// + /// Represents an organization unit (OU). + /// + public class OrganizationUnit : FullAuditedAggregateRoot, IMultiTenant + { + public virtual Guid? TenantId { get; protected set; } + + /// + /// Parent Id. + /// Null, if this OU is a root. + /// + public virtual Guid? ParentId { get; internal set; } + + /// + /// Hierarchical Code of this organization unit. + /// Example: "00001.00042.00005". + /// This is a unique code for a Tenant. + /// It's changeable if OU hierarchy is changed. + /// + public virtual string Code { get; internal set; } + + /// + /// Display name of this role. + /// + public virtual string DisplayName { get; set; } + + /// + /// Roles of this OU. + /// + public virtual ICollection Roles { get; protected set; } + + /// + /// Initializes a new instance of the class. + /// + public OrganizationUnit() + { + + } + + /// + /// Initializes a new instance of the class. + /// + /// Tenant's Id or null for host. + /// Display name. + /// Parent's Id or null if OU is a root. + public OrganizationUnit(Guid id, string displayName, Guid? parentId = null, Guid? tenantId = null) + { + Id = id; + TenantId = tenantId; + DisplayName = displayName; + ParentId = parentId; + Roles = new Collection(); + } + + /// + /// Creates code for given numbers. + /// Example: if numbers are 4,2 then returns "00004.00002"; + /// + /// Numbers + public static string CreateCode(params int[] numbers) + { + if (numbers.IsNullOrEmpty()) + { + return null; + } + + return numbers.Select(number => number.ToString(new string('0', OrganizationUnitConsts.CodeUnitLength))).JoinAsString("."); + } + + /// + /// Appends a child code to a parent code. + /// Example: if parentCode = "00001", childCode = "00042" then returns "00001.00042". + /// + /// Parent code. Can be null or empty if parent is a root. + /// Child code. + public static string AppendCode(string parentCode, string childCode) + { + if (childCode.IsNullOrEmpty()) + { + throw new ArgumentNullException(nameof(childCode), "childCode can not be null or empty."); + } + + if (parentCode.IsNullOrEmpty()) + { + return childCode; + } + + return parentCode + "." + childCode; + } + + /// + /// Gets relative code to the parent. + /// Example: if code = "00019.00055.00001" and parentCode = "00019" then returns "00055.00001". + /// + /// The code. + /// The parent code. + public static string GetRelativeCode(string code, string parentCode) + { + if (code.IsNullOrEmpty()) + { + throw new ArgumentNullException(nameof(code), "code can not be null or empty."); + } + + if (parentCode.IsNullOrEmpty()) + { + return code; + } + + if (code.Length == parentCode.Length) + { + return null; + } + + return code.Substring(parentCode.Length + 1); + } + + /// + /// Calculates next code for given code. + /// Example: if code = "00019.00055.00001" returns "00019.00055.00002". + /// + /// The code. + public static string CalculateNextCode(string code) + { + if (code.IsNullOrEmpty()) + { + throw new ArgumentNullException(nameof(code), "code can not be null or empty."); + } + + var parentCode = GetParentCode(code); + var lastUnitCode = GetLastUnitCode(code); + + return AppendCode(parentCode, CreateCode(Convert.ToInt32(lastUnitCode) + 1)); + } + + /// + /// Gets the last unit code. + /// Example: if code = "00019.00055.00001" returns "00001". + /// + /// The code. + public static string GetLastUnitCode(string code) + { + if (code.IsNullOrEmpty()) + { + throw new ArgumentNullException(nameof(code), "code can not be null or empty."); + } + + var splittedCode = code.Split('.'); + return splittedCode[splittedCode.Length - 1]; + } + + /// + /// Gets parent code. + /// Example: if code = "00019.00055.00001" returns "00019.00055". + /// + /// The code. + public static string GetParentCode(string code) + { + if (code.IsNullOrEmpty()) + { + throw new ArgumentNullException(nameof(code), "code can not be null or empty."); + } + + var splittedCode = code.Split('.'); + if (splittedCode.Length == 1) + { + return null; + } + + return splittedCode.Take(splittedCode.Length - 1).JoinAsString("."); + } + + public virtual void AddRole(Guid roleId) + { + Check.NotNull(roleId, nameof(roleId)); + + if (IsInRole(roleId)) + { + return; + } + + Roles.Add(new OrganizationUnitRole(roleId, Id, TenantId)); + } + + public virtual void RemoveRole(Guid roleId) + { + Check.NotNull(roleId, nameof(roleId)); + + if (!IsInRole(roleId)) + { + return; + } + + Roles.RemoveAll(r => r.RoleId == roleId); + } + + public virtual bool IsInRole(Guid roleId) + { + Check.NotNull(roleId, nameof(roleId)); + + return Roles.Any(r => r.RoleId == roleId); + } + } +} diff --git a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/OrganizationUnitManager.cs b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/OrganizationUnitManager.cs new file mode 100644 index 0000000000..bfaa522dd0 --- /dev/null +++ b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/OrganizationUnitManager.cs @@ -0,0 +1,188 @@ +using Microsoft.Extensions.Localization; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Volo.Abp.Domain.Services; +using Volo.Abp.Identity.Localization; +using Volo.Abp.Threading; +using Volo.Abp.Uow; + +namespace Volo.Abp.Identity +{ + /// + /// Performs domain logic for Organization Units. + /// + public class OrganizationUnitManager : DomainService + { + protected IOrganizationUnitRepository OrganizationUnitRepository { get; } + protected IStringLocalizer Localizer { get; } + protected IIdentityRoleRepository IdentityRoleRepository { get; } + protected ICancellationTokenProvider CancellationTokenProvider { get; } + + public OrganizationUnitManager( + IOrganizationUnitRepository organizationUnitRepository, + IStringLocalizer localizer, + IIdentityRoleRepository identityRoleRepository, + ICancellationTokenProvider cancellationTokenProvider) + { + OrganizationUnitRepository = organizationUnitRepository; + Localizer = localizer; + IdentityRoleRepository = identityRoleRepository; + CancellationTokenProvider = cancellationTokenProvider; + } + + [UnitOfWork] + public virtual async Task CreateAsync(OrganizationUnit organizationUnit) + { + organizationUnit.Code = await GetNextChildCodeAsync(organizationUnit.ParentId); + await ValidateOrganizationUnitAsync(organizationUnit); + await OrganizationUnitRepository.InsertAsync(organizationUnit); + } + + public virtual async Task UpdateAsync(OrganizationUnit organizationUnit) + { + await ValidateOrganizationUnitAsync(organizationUnit); + await OrganizationUnitRepository.UpdateAsync(organizationUnit); + } + + public virtual async Task GetNextChildCodeAsync(Guid? parentId) + { + var lastChild = await GetLastChildOrNullAsync(parentId); + if (lastChild != null) + { + return OrganizationUnit.CalculateNextCode(lastChild.Code); + } + + var parentCode = parentId != null + ? await GetCodeOrDefaultAsync(parentId.Value) + : null; + + return OrganizationUnit.AppendCode( + parentCode, + OrganizationUnit.CreateCode(1) + ); + } + + public virtual async Task GetLastChildOrNullAsync(Guid? parentId) + { + var children = await OrganizationUnitRepository.GetChildrenAsync(parentId); + return children.OrderBy(c => c.Code).LastOrDefault(); + } + + [UnitOfWork] + public virtual async Task DeleteAsync(Guid id) + { + var children = await FindChildrenAsync(id, true); + + foreach (var child in children) + { + await OrganizationUnitRepository.DeleteAsync(child); + } + + await OrganizationUnitRepository.DeleteAsync(id); + } + + [UnitOfWork] + public virtual async Task MoveAsync(Guid id, Guid? parentId) + { + var organizationUnit = await OrganizationUnitRepository.GetAsync(id); + if (organizationUnit.ParentId == parentId) + { + return; + } + + //Should find children before Code change + var children = await FindChildrenAsync(id, true); + + //Store old code of OU + var oldCode = organizationUnit.Code; + + //Move OU + organizationUnit.Code = await GetNextChildCodeAsync(parentId); + organizationUnit.ParentId = parentId; + + await ValidateOrganizationUnitAsync(organizationUnit); + + //Update Children Codes + foreach (var child in children) + { + child.Code = OrganizationUnit.AppendCode(organizationUnit.Code, OrganizationUnit.GetRelativeCode(child.Code, oldCode)); + } + } + + public virtual async Task GetCodeOrDefaultAsync(Guid id) + { + var ou = await OrganizationUnitRepository.GetAsync(id); + return ou?.Code; + } + + protected virtual async Task ValidateOrganizationUnitAsync(OrganizationUnit organizationUnit) + { + var siblings = (await FindChildrenAsync(organizationUnit.ParentId)) + .Where(ou => ou.Id != organizationUnit.Id) + .ToList(); + + if (siblings.Any(ou => ou.DisplayName == organizationUnit.DisplayName)) + { + throw new UserFriendlyException(Localizer["OrganizationUnitDuplicateDisplayNameWarning", organizationUnit.DisplayName]); + } + } + + public async Task> FindChildrenAsync(Guid? parentId, bool recursive = false) + { + if (!recursive) + { + return await OrganizationUnitRepository.GetChildrenAsync(parentId); + } + + if (!parentId.HasValue) + { + return await OrganizationUnitRepository.GetListAsync(includeDetails: true); + } + + var code = await GetCodeOrDefaultAsync(parentId.Value); + + return await OrganizationUnitRepository.GetAllChildrenWithParentCodeAsync(code, parentId); + } + + public virtual Task IsInOrganizationUnitAsync(IdentityUser user, OrganizationUnit ou) + { + return Task.FromResult(user.IsInOrganizationUnit(ou.Id)); + } + + public virtual async Task AddRoleToOrganizationUnitAsync(Guid roleId, Guid ouId) + { + await AddRoleToOrganizationUnitAsync( + await IdentityRoleRepository.GetAsync(roleId), + await OrganizationUnitRepository.GetAsync(ouId, true) + ); + } + + public virtual Task AddRoleToOrganizationUnitAsync(IdentityRole role, OrganizationUnit ou) + { + var currentRoles = ou.Roles; + + if (currentRoles.Any(r => r.OrganizationUnitId == ou.Id && r.RoleId == role.Id)) + { + return Task.FromResult(0); + } + ou.AddRole(role.Id); + return Task.FromResult(0); + } + + public virtual async Task RemoveRoleFromOrganizationUnitAsync(Guid roleId, Guid ouId) + { + await RemoveRoleFromOrganizationUnitAsync( + await IdentityRoleRepository.GetAsync(roleId), + await OrganizationUnitRepository.GetAsync(ouId, true) + ); + } + + public virtual Task RemoveRoleFromOrganizationUnitAsync(IdentityRole role, OrganizationUnit organizationUnit) + { + organizationUnit.RemoveRole(role.Id); + return Task.FromResult(0); + } + } +} diff --git a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/OrganizationUnitRole.cs b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/OrganizationUnitRole.cs new file mode 100644 index 0000000000..43aca1e272 --- /dev/null +++ b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/OrganizationUnitRole.cs @@ -0,0 +1,53 @@ +using System; +using Volo.Abp.Domain.Entities.Auditing; +using Volo.Abp.MultiTenancy; + +namespace Volo.Abp.Identity +{ + /// + /// Represents membership of a User to an OU. + /// + public class OrganizationUnitRole : CreationAuditedEntity, IMultiTenant + { + /// + /// TenantId of this entity. + /// + public virtual Guid? TenantId { get; protected set; } + + /// + /// Id of the Role. + /// + public virtual Guid RoleId { get; protected set; } + + /// + /// Id of the . + /// + public virtual Guid OrganizationUnitId { get; protected set; } + + /// + /// Initializes a new instance of the class. + /// + protected OrganizationUnitRole() + { + + } + + /// + /// Initializes a new instance of the class. + /// + /// TenantId + /// Id of the User. + /// Id of the . + public OrganizationUnitRole(Guid roleId, Guid organizationUnitId, Guid? tenantId = null) + { + RoleId = roleId; + OrganizationUnitId = organizationUnitId; + TenantId = tenantId; + } + + public override object[] GetKeys() + { + return new object[] { OrganizationUnitId, RoleId }; + } + } +} diff --git a/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo.Abp.Identity.EntityFrameworkCore.csproj b/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo.Abp.Identity.EntityFrameworkCore.csproj index fc647ad159..0f1e0d1c7e 100644 --- a/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo.Abp.Identity.EntityFrameworkCore.csproj +++ b/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo.Abp.Identity.EntityFrameworkCore.csproj @@ -15,8 +15,8 @@ - + diff --git a/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/AbpIdentityEntityFrameworkCoreModule.cs b/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/AbpIdentityEntityFrameworkCoreModule.cs index b3ff5c368b..f46d3b6e0b 100644 --- a/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/AbpIdentityEntityFrameworkCoreModule.cs +++ b/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/AbpIdentityEntityFrameworkCoreModule.cs @@ -16,6 +16,7 @@ namespace Volo.Abp.Identity.EntityFrameworkCore options.AddRepository(); options.AddRepository(); options.AddRepository(); + options.AddRepository(); }); } } diff --git a/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/EfCoreIdentityRoleRepository.cs b/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/EfCoreIdentityRoleRepository.cs index dfaaf51d91..8ebe3fd985 100644 --- a/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/EfCoreIdentityRoleRepository.cs +++ b/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/EfCoreIdentityRoleRepository.cs @@ -7,7 +7,6 @@ using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; using Volo.Abp.Domain.Repositories.EntityFrameworkCore; using Volo.Abp.EntityFrameworkCore; -using Volo.Abp.Guids; namespace Volo.Abp.Identity.EntityFrameworkCore { @@ -19,7 +18,7 @@ namespace Volo.Abp.Identity.EntityFrameworkCore } public virtual async Task FindByNormalizedNameAsync( - string normalizedRoleName, + string normalizedRoleName, bool includeDetails = true, CancellationToken cancellationToken = default) { @@ -29,10 +28,10 @@ namespace Volo.Abp.Identity.EntityFrameworkCore } public virtual async Task> GetListAsync( - string sorting = null, - int maxResultCount = int.MaxValue, - int skipCount = 0, - bool includeDetails = true, + string sorting = null, + int maxResultCount = int.MaxValue, + int skipCount = 0, + bool includeDetails = false, CancellationToken cancellationToken = default) { return await DbSet @@ -40,6 +39,15 @@ namespace Volo.Abp.Identity.EntityFrameworkCore .OrderBy(sorting ?? nameof(IdentityRole.Name)) .PageBy(skipCount, maxResultCount) .ToListAsync(GetCancellationToken(cancellationToken)); + } + + public virtual async Task> GetListAsync( + IEnumerable ids, + CancellationToken cancellationToken = default) + { + return await DbSet + .Where(t => ids.Contains(t.Id)) + .ToListAsync(GetCancellationToken(cancellationToken)); } public virtual async Task> GetDefaultOnesAsync( @@ -51,6 +59,6 @@ namespace Volo.Abp.Identity.EntityFrameworkCore public override IQueryable WithDetails() { return GetQueryable().IncludeDetails(); - } + } } } \ No newline at end of file diff --git a/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/EfCoreIdentityUserRepository.cs b/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/EfCoreIdentityUserRepository.cs index 97c2ebe779..c1bee0449a 100644 --- a/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/EfCoreIdentityUserRepository.cs +++ b/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/EfCoreIdentityUserRepository.cs @@ -6,6 +6,7 @@ using System.Security.Claims; using System.Threading; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Internal; using Volo.Abp.Domain.Repositories.EntityFrameworkCore; using Volo.Abp.EntityFrameworkCore; @@ -39,6 +40,22 @@ namespace Volo.Abp.Identity.EntityFrameworkCore join role in DbContext.Roles on userRole.RoleId equals role.Id where userRole.UserId == id select role.Name; + var organizationUnitIds = DbContext.Set().Where(q => q.UserId == id).Select(q => q.OrganizationUnitId).ToArray(); + var organizationRoleIds = DbContext.Set().Where(our => organizationUnitIds.Contains(our.OrganizationUnitId)).Select(r => r.RoleId).ToArray(); + var orgUnitRoleNameQuery = DbContext.Roles.Where(r => organizationRoleIds.Contains(r.Id)).Select(n => n.Name); + var resultQuery = query.Union(orgUnitRoleNameQuery); + return await resultQuery.ToListAsync(GetCancellationToken(cancellationToken)); + } + + public virtual async Task> GetRoleNamesInOrganizationUnitAsync( + Guid id, + CancellationToken cancellationToken = default) + { + var query = from userOu in DbContext.Set() + join roleOu in DbContext.Set() on userOu.OrganizationUnitId equals roleOu.OrganizationUnitId + join userOuRoles in DbContext.Roles on roleOu.RoleId equals userOuRoles.Id + where userOu.UserId == id + select userOuRoles.Name; return await query.ToListAsync(GetCancellationToken(cancellationToken)); } @@ -129,7 +146,21 @@ namespace Volo.Abp.Identity.EntityFrameworkCore where userRole.UserId == id select role; - return await query.ToListAsync(GetCancellationToken(cancellationToken)); + //TODO: Needs improvement + var userOrganizationsQuery = from userOrg in DbContext.Set() + join ou in DbContext.OrganizationUnits.IncludeDetails(includeDetails) on userOrg.OrganizationUnitId equals ou.Id + where userOrg.UserId == id + select ou; + + var orgUserRoleQuery = DbContext.Set().Where(q => userOrganizationsQuery.Select(t => t.Id).Contains(q.OrganizationUnitId)) + .Select(t => t.RoleId); + + var orgRoles = DbContext.Roles.Where(q => orgUserRoleQuery.Contains(q.Id)); + var resultQuery = query.Union(orgRoles); + + return await resultQuery.ToListAsync(GetCancellationToken(cancellationToken)); + + //return await query.ToListAsync(GetCancellationToken(cancellationToken)); } public virtual async Task GetCountAsync( @@ -147,9 +178,63 @@ namespace Volo.Abp.Identity.EntityFrameworkCore .LongCountAsync(GetCancellationToken(cancellationToken)); } + public virtual async Task> GetOrganizationUnitsAsync( + Guid id, + bool includeDetails = false, + CancellationToken cancellationToken = default) + { + var query = from userOU in DbContext.Set() + join ou in DbContext.OrganizationUnits.IncludeDetails(includeDetails) on userOU.OrganizationUnitId equals ou.Id + where userOU.UserId == id + select ou; + + return await query.ToListAsync(GetCancellationToken(cancellationToken)); + } + + public virtual async Task> GetUsersInOrganizationUnitAsync( + Guid organizationUnitId, + CancellationToken cancellationToken = default + ) + { + var query = from userOu in DbContext.Set() + join user in DbSet on userOu.UserId equals user.Id + where userOu.OrganizationUnitId == organizationUnitId + select user; + return await query.ToListAsync(GetCancellationToken(cancellationToken)); + } + + public async Task> GetUsersInOrganizationsListAsync( + List organizationUnitIds, + CancellationToken cancellationToken = default + ) + { + //var userIds = DbContext.Set() + // .Where(q => organizationUnitIds.Contains(q.OrganizationUnitId)) + // .Select(u => u.UserId); + //var query = DbContext.Users.Where(u => userIds.Contains(u.Id)); + var query = from userOu in DbContext.Set() + join user in DbSet on userOu.UserId equals user.Id + where organizationUnitIds.Contains(userOu.OrganizationUnitId) + select user; + return await query.ToListAsync(GetCancellationToken(cancellationToken)); + } + + public virtual async Task> GetUsersInOrganizationUnitWithChildrenAsync( + string code, + CancellationToken cancellationToken = default + ) + { + var query = from userOu in DbContext.Set() + join user in DbSet on userOu.UserId equals user.Id + join ou in DbContext.Set() on userOu.OrganizationUnitId equals ou.Id + where ou.Code.StartsWith(code) + select user; + return await query.ToListAsync(GetCancellationToken(cancellationToken)); + } + public override IQueryable WithDetails() { return GetQueryable().IncludeDetails(); - } + } } } diff --git a/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/EfCoreOrganizationUnitRepository.cs b/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/EfCoreOrganizationUnitRepository.cs new file mode 100644 index 0000000000..350f64ffe7 --- /dev/null +++ b/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/EfCoreOrganizationUnitRepository.cs @@ -0,0 +1,160 @@ +using Microsoft.EntityFrameworkCore; +using System; +using System.Collections.Generic; +using System.Linq.Dynamic.Core; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Volo.Abp.Domain.Repositories.EntityFrameworkCore; +using Volo.Abp.EntityFrameworkCore; + +namespace Volo.Abp.Identity.EntityFrameworkCore +{ + public class EfCoreOrganizationUnitRepository + : EfCoreRepository, + IOrganizationUnitRepository + { + public EfCoreOrganizationUnitRepository( + IDbContextProvider dbContextProvider) + : base(dbContextProvider) + { + } + + public virtual async Task> GetChildrenAsync( + Guid? parentId, + bool includeDetails = false, + CancellationToken cancellationToken = default) + { + return await DbSet + .IncludeDetails(includeDetails) + .Where(x => x.ParentId == parentId) + .ToListAsync(GetCancellationToken(cancellationToken)); + } + + public virtual async Task> GetAllChildrenWithParentCodeAsync( + string code, + Guid? parentId, + bool includeDetails = false, + CancellationToken cancellationToken = default) + { + return await DbSet + .IncludeDetails(includeDetails) + .Where(ou => ou.Code.StartsWith(code) && ou.Id != parentId.Value) + .ToListAsync(GetCancellationToken(cancellationToken)); + } + + public virtual async Task> GetListAsync( + string sorting = null, + int maxResultCount = int.MaxValue, + int skipCount = 0, + bool includeDetails = true, + CancellationToken cancellationToken = default) + { + return await DbSet + .IncludeDetails(includeDetails) + .OrderBy(sorting ?? nameof(OrganizationUnit.DisplayName)) + .PageBy(skipCount, maxResultCount) + .ToListAsync(GetCancellationToken(cancellationToken)); + } + public virtual async Task> GetListAsync( + IEnumerable ids, + bool includeDetails = false, + CancellationToken cancellationToken = default) + { + return await DbSet + .IncludeDetails(includeDetails) + .Where(t => ids.Contains(t.Id)) + .ToListAsync(GetCancellationToken(cancellationToken)); + } + + public virtual async Task GetAsync( + string displayName, + bool includeDetails = true, + CancellationToken cancellationToken = default) + { + return await DbSet + .IncludeDetails(includeDetails) + .FirstOrDefaultAsync( + ou => ou.DisplayName == displayName, + GetCancellationToken(cancellationToken) + ); + } + + public virtual async Task> GetRolesAsync( + OrganizationUnit organizationUnit, + string sorting = null, + int maxResultCount = int.MaxValue, + int skipCount = 0, + bool includeDetails = false, + CancellationToken cancellationToken = default) + { + var query = from organizationRole in DbContext.Set() + join role in DbContext.Roles.IncludeDetails(includeDetails) on organizationRole.RoleId equals role.Id + where organizationRole.OrganizationUnitId == organizationUnit.Id + select role; + query = query + .OrderBy(sorting ?? nameof(IdentityRole.Name)) + .PageBy(skipCount, maxResultCount); + + return await query.ToListAsync(GetCancellationToken(cancellationToken)); + } + + public virtual async Task GetRolesCountAsync( + OrganizationUnit organizationUnit, + CancellationToken cancellationToken = default) + { + var query = from organizationRole in DbContext.Set() + join role in DbContext.Roles on organizationRole.RoleId equals role.Id + where organizationRole.OrganizationUnitId == organizationUnit.Id + select role; + + return await query.CountAsync(GetCancellationToken(cancellationToken)); + } + + public virtual async Task> GetMembersAsync( + OrganizationUnit organizationUnit, + string sorting = null, + int maxResultCount = int.MaxValue, + int skipCount = 0, + string filter = null, + bool includeDetails = false, + CancellationToken cancellationToken = default + ) + { + var query = from userOu in DbContext.Set() + join user in DbContext.Users.IncludeDetails(includeDetails) on userOu.UserId equals user.Id + where userOu.OrganizationUnitId == organizationUnit.Id + select user; + + if (!filter.IsNullOrWhiteSpace()) + { + query = query.Where(u => + u.UserName.Contains(filter) || + u.Email.Contains(filter) || + (u.PhoneNumber != null && u.PhoneNumber.Contains(filter)) + ); + } + + return await query.OrderBy(sorting ?? nameof(IdentityUser.UserName)) + .PageBy(skipCount, maxResultCount) + .ToListAsync(GetCancellationToken(cancellationToken)); + } + + public virtual async Task GetMembersCountAsync( + OrganizationUnit organizationUnit, + CancellationToken cancellationToken = default) + { + var query = from userOu in DbContext.Set() + join user in DbContext.Users on userOu.UserId equals user.Id + where userOu.OrganizationUnitId == organizationUnit.Id + select user; + + return await query.CountAsync(GetCancellationToken(cancellationToken)); + } + + public override IQueryable WithDetails() + { + return GetQueryable().IncludeDetails(); + } + } +} diff --git a/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/IIdentityDbContext.cs b/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/IIdentityDbContext.cs index d3c560dca2..f27c9b534a 100644 --- a/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/IIdentityDbContext.cs +++ b/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/IIdentityDbContext.cs @@ -12,5 +12,7 @@ namespace Volo.Abp.Identity.EntityFrameworkCore DbSet Roles { get; set; } DbSet ClaimTypes { get; set; } + + DbSet OrganizationUnits { get; set; } } -} \ No newline at end of file +} diff --git a/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/IdentityDbContext.cs b/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/IdentityDbContext.cs index 05050159bc..811280de7b 100644 --- a/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/IdentityDbContext.cs +++ b/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/IdentityDbContext.cs @@ -16,6 +16,8 @@ namespace Volo.Abp.Identity.EntityFrameworkCore public DbSet ClaimTypes { get; set; } + public DbSet OrganizationUnits { get; set; } + public IdentityDbContext(DbContextOptions options) : base(options) { diff --git a/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/IdentityDbContextModelBuilderExtensions.cs b/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/IdentityDbContextModelBuilderExtensions.cs index 05b99e767e..7dfe662884 100644 --- a/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/IdentityDbContextModelBuilderExtensions.cs +++ b/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/IdentityDbContextModelBuilderExtensions.cs @@ -40,7 +40,8 @@ namespace Volo.Abp.Identity.EntityFrameworkCore b.HasMany(u => u.Logins).WithOne().HasForeignKey(ul => ul.UserId).IsRequired(); b.HasMany(u => u.Roles).WithOne().HasForeignKey(ur => ur.UserId).IsRequired(); b.HasMany(u => u.Tokens).WithOne().HasForeignKey(ur => ur.UserId).IsRequired(); - + b.HasMany(u => u.OrganizationUnits).WithOne().HasForeignKey(ur => ur.UserId).IsRequired(); + b.HasIndex(u => u.NormalizedUserName); b.HasIndex(u => u.NormalizedEmail); b.HasIndex(u => u.UserName); @@ -146,6 +147,47 @@ namespace Volo.Abp.Identity.EntityFrameworkCore b.Property(uc => uc.Description).HasMaxLength(IdentityClaimTypeConsts.MaxDescriptionLength); b.Property(uc => uc.ConcurrencyStamp).IsRequired().IsConcurrencyToken().HasMaxLength(IdentityClaimTypeConsts.MaxConcurrencyStampLength).HasColumnName(nameof(IdentityClaimType.ConcurrencyStamp)); }); + + builder.Entity(b => + { + b.ToTable(options.TablePrefix + "OrganizationUnits", options.Schema); + + b.ConfigureByConvention(); + + b.Property(ou => ou.Code).IsRequired().HasMaxLength(OrganizationUnitConsts.MaxCodeLength).HasColumnName(nameof(OrganizationUnit.Code)); + b.Property(ou => ou.DisplayName).IsRequired().HasMaxLength(OrganizationUnitConsts.MaxDisplayNameLength).HasColumnName(nameof(OrganizationUnit.DisplayName)); + + b.HasMany().WithOne().HasForeignKey(ou => ou.ParentId); + b.HasMany(ou => ou.Roles).WithOne().HasForeignKey(our => our.OrganizationUnitId).IsRequired(); + + b.HasIndex(ou => ou.Code); + }); + + builder.Entity(b => + { + b.ToTable(options.TablePrefix + "OrganizationUnitRoles", options.Schema); + + b.ConfigureByConvention(); + + b.HasKey(ou => new { ou.OrganizationUnitId, ou.RoleId }); + + b.HasOne().WithMany().HasForeignKey(ou => ou.RoleId).IsRequired(); + + b.HasIndex(ou => new { ou.RoleId, ou.OrganizationUnitId }); + }); + + builder.Entity(b => + { + b.ToTable(options.TablePrefix + "UserOrganizationUnits", options.Schema); + + b.ConfigureByConvention(); + + b.HasKey(ou => new { ou.OrganizationUnitId, ou.UserId }); + + b.HasOne().WithMany().HasForeignKey(ou => ou.OrganizationUnitId).IsRequired(); + + b.HasIndex(ou => new { ou.UserId, ou.OrganizationUnitId }); + }); } } } diff --git a/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/IdentityEfCoreQueryableExtensions.cs b/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/IdentityEfCoreQueryableExtensions.cs index 8cd598c673..5e4a0a2876 100644 --- a/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/IdentityEfCoreQueryableExtensions.cs +++ b/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/IdentityEfCoreQueryableExtensions.cs @@ -16,7 +16,8 @@ namespace Volo.Abp.Identity.EntityFrameworkCore .Include(x => x.Roles) .Include(x => x.Logins) .Include(x => x.Claims) - .Include(x => x.Tokens); + .Include(x => x.Tokens) + .Include(x => x.OrganizationUnits); } public static IQueryable IncludeDetails(this IQueryable queryable, bool include = true) @@ -29,5 +30,16 @@ namespace Volo.Abp.Identity.EntityFrameworkCore return queryable .Include(x => x.Claims); } + + public static IQueryable IncludeDetails(this IQueryable queryable, bool include = true) + { + if (!include) + { + return queryable; + } + + return queryable + .Include(x => x.Roles); + } } } \ No newline at end of file diff --git a/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/AbpIdentityMongoDbContext.cs b/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/AbpIdentityMongoDbContext.cs index 3db35866f5..3240ce1454 100644 --- a/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/AbpIdentityMongoDbContext.cs +++ b/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/AbpIdentityMongoDbContext.cs @@ -13,6 +13,8 @@ namespace Volo.Abp.Identity.MongoDB public IMongoCollection ClaimTypes => Collection(); + public IMongoCollection OrganizationUnits => Collection(); + protected override void CreateModel(IMongoModelBuilder modelBuilder) { base.CreateModel(modelBuilder); diff --git a/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/AbpIdentityMongoDbContextExtensions.cs b/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/AbpIdentityMongoDbContextExtensions.cs index dd0cc8d42e..bc303e0eb5 100644 --- a/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/AbpIdentityMongoDbContextExtensions.cs +++ b/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/AbpIdentityMongoDbContextExtensions.cs @@ -31,6 +31,11 @@ namespace Volo.Abp.Identity.MongoDB { b.CollectionName = options.CollectionPrefix + "ClaimTypes"; }); + + builder.Entity(b => + { + b.CollectionName = options.CollectionPrefix + "OrganizationUnits"; + }); } } } \ No newline at end of file diff --git a/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/AbpIdentityMongoDbModule.cs b/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/AbpIdentityMongoDbModule.cs index cfbc10adba..268c718b12 100644 --- a/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/AbpIdentityMongoDbModule.cs +++ b/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/AbpIdentityMongoDbModule.cs @@ -17,6 +17,7 @@ namespace Volo.Abp.Identity.MongoDB options.AddRepository(); options.AddRepository(); options.AddRepository(); + options.AddRepository(); }); } } diff --git a/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/IAbpIdentityMongoDbContext.cs b/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/IAbpIdentityMongoDbContext.cs index 36cdcbec3e..c903c5d96d 100644 --- a/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/IAbpIdentityMongoDbContext.cs +++ b/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/IAbpIdentityMongoDbContext.cs @@ -12,5 +12,7 @@ namespace Volo.Abp.Identity.MongoDB IMongoCollection Roles { get; } IMongoCollection ClaimTypes { get; } + + IMongoCollection OrganizationUnits { get; } } } \ No newline at end of file diff --git a/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/MongoIdentityRoleRepository.cs b/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/MongoIdentityRoleRepository.cs index 7acb9b4e4b..5311ffe094 100644 --- a/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/MongoIdentityRoleRepository.cs +++ b/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/MongoIdentityRoleRepository.cs @@ -7,30 +7,29 @@ using System.Linq.Dynamic.Core; using MongoDB.Driver; using MongoDB.Driver.Linq; using Volo.Abp.Domain.Repositories.MongoDB; -using Volo.Abp.Guids; using Volo.Abp.MongoDB; namespace Volo.Abp.Identity.MongoDB { public class MongoIdentityRoleRepository : MongoDbRepository, IIdentityRoleRepository { - public MongoIdentityRoleRepository(IMongoDbContextProvider dbContextProvider) + public MongoIdentityRoleRepository(IMongoDbContextProvider dbContextProvider) : base(dbContextProvider) { } public async Task FindByNormalizedNameAsync( - string normalizedRoleName, + string normalizedRoleName, bool includeDetails = true, CancellationToken cancellationToken = default) { return await GetMongoQueryable().FirstOrDefaultAsync(r => r.NormalizedName == normalizedRoleName, GetCancellationToken(cancellationToken)); } - public async Task> GetListAsync( - string sorting = null, - int maxResultCount = int.MaxValue, - int skipCount = 0, + public virtual async Task> GetListAsync( + string sorting = null, + int maxResultCount = int.MaxValue, + int skipCount = 0, bool includeDetails = false, CancellationToken cancellationToken = default) { @@ -41,10 +40,19 @@ namespace Volo.Abp.Identity.MongoDB .ToListAsync(GetCancellationToken(cancellationToken)); } + public virtual async Task> GetListAsync( + IEnumerable ids, + CancellationToken cancellationToken = default) + { + return await GetMongoQueryable() + .Where(t => ids.Contains(t.Id)) + .ToListAsync(GetCancellationToken(cancellationToken)); + } + public virtual async Task> GetDefaultOnesAsync( bool includeDetails = false, CancellationToken cancellationToken = default) { return await GetMongoQueryable().Where(r => r.IsDefault).ToListAsync(cancellationToken: GetCancellationToken(cancellationToken)); - } + } } } \ No newline at end of file diff --git a/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/MongoIdentityUserRepository.cs b/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/MongoIdentityUserRepository.cs index e87d6aacfc..b82fdf987c 100644 --- a/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/MongoIdentityUserRepository.cs +++ b/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/MongoIdentityUserRepository.cs @@ -8,7 +8,6 @@ using System.Threading.Tasks; using MongoDB.Driver; using MongoDB.Driver.Linq; using Volo.Abp.Domain.Repositories.MongoDB; -using Volo.Abp.Guids; using Volo.Abp.MongoDB; namespace Volo.Abp.Identity.MongoDB @@ -37,8 +36,41 @@ namespace Volo.Abp.Identity.MongoDB CancellationToken cancellationToken = default) { var user = await GetAsync(id, cancellationToken: GetCancellationToken(cancellationToken)); + var organizationUnitIds = user.OrganizationUnits + .Select(r => r.OrganizationUnitId) + .ToArray(); + var organizationUnits = DbContext.OrganizationUnits + .AsQueryable() + .Where(ou => organizationUnitIds.Contains(ou.Id)) + .ToArray(); + var orgUnitRoleIds = organizationUnits.SelectMany(x => x.Roles.Select(r => r.RoleId)).ToArray(); var roleIds = user.Roles.Select(r => r.RoleId).ToArray(); - return await DbContext.Roles.AsQueryable().Where(r => roleIds.Contains(r.Id)).Select(r => r.Name).ToListAsync(GetCancellationToken(cancellationToken)); + var allRoleIds = orgUnitRoleIds.Union(roleIds); + return await DbContext.Roles.AsQueryable().Where(r => allRoleIds.Contains(r.Id)).Select(r => r.Name).ToListAsync(GetCancellationToken(cancellationToken)); + } + + public async Task> GetRoleNamesInOrganizationUnitAsync( + Guid id, + CancellationToken cancellationToken = default) + { + var user = await GetAsync(id, cancellationToken: GetCancellationToken(cancellationToken)); + + var organizationUnitIds = user.OrganizationUnits + .Select(r => r.OrganizationUnitId) + .ToArray(); + + var organizationUnits = DbContext.OrganizationUnits + .AsQueryable() + .Where(ou => organizationUnitIds.Contains(ou.Id)) + .ToArray(); + + var roleIds = organizationUnits.SelectMany(x => x.Roles.Select(r => r.RoleId)).ToArray(); + + return await DbContext.Roles //TODO: Such usage suppress filters! + .AsQueryable() + .Where(r => roleIds.Contains(r.Id)) + .Select(r => r.Name) + .ToListAsync(GetCancellationToken(cancellationToken)); } public virtual async Task FindByLoginAsync( @@ -116,8 +148,30 @@ namespace Volo.Abp.Identity.MongoDB CancellationToken cancellationToken = default) { var user = await GetAsync(id, cancellationToken: GetCancellationToken(cancellationToken)); + var organizationUnitIds = user.OrganizationUnits + .Select(r => r.OrganizationUnitId) + .ToArray(); + var organizationUnits = DbContext.OrganizationUnits + .AsQueryable() + .Where(ou => organizationUnitIds.Contains(ou.Id)) + .ToArray(); + var orgUnitRoleIds = organizationUnits.SelectMany(x => x.Roles.Select(r => r.RoleId)).ToArray(); var roleIds = user.Roles.Select(r => r.RoleId).ToArray(); - return await DbContext.Roles.AsQueryable().Where(r => roleIds.Contains(r.Id)).ToListAsync(GetCancellationToken(cancellationToken)); + var allRoleIds = orgUnitRoleIds.Union(roleIds); + return await DbContext.Roles.AsQueryable().Where(r => allRoleIds.Contains(r.Id)).ToListAsync(GetCancellationToken(cancellationToken)); + } + + public async Task> GetOrganizationUnitsAsync( + Guid id, + bool includeDetails = false, + CancellationToken cancellationToken = default) + { + var user = await GetAsync(id, cancellationToken: GetCancellationToken(cancellationToken)); + var organizationUnitIds = user.OrganizationUnits.Select(r => r.OrganizationUnitId); + return await DbContext.OrganizationUnits.AsQueryable() + .Where(ou => organizationUnitIds.Contains(ou.Id)) + .ToListAsync(GetCancellationToken(cancellationToken)) + ; } public virtual async Task GetCountAsync( @@ -135,5 +189,43 @@ namespace Volo.Abp.Identity.MongoDB ) .LongCountAsync(GetCancellationToken(cancellationToken)); } + + public async Task> GetUsersInOrganizationUnitAsync( + Guid organizationUnitId, + CancellationToken cancellationToken = default) + { + var result = await GetMongoQueryable() + .Where(u => u.OrganizationUnits.Any(uou => uou.OrganizationUnitId == organizationUnitId)) + .ToListAsync(GetCancellationToken(cancellationToken)) + ; + return result; + } + + public async Task> GetUsersInOrganizationsListAsync( + List organizationUnitIds, + CancellationToken cancellationToken = default) + { + var result = await GetMongoQueryable() + .Where(u => u.OrganizationUnits.Any(uou => organizationUnitIds.Contains(uou.OrganizationUnitId))) + .ToListAsync(GetCancellationToken(cancellationToken)) + ; + return result; + } + + public async Task> GetUsersInOrganizationUnitWithChildrenAsync( + string code, + CancellationToken cancellationToken = default) + { + var organizationUnitIds = await DbContext.OrganizationUnits.AsQueryable() + .Where(ou => ou.Code.StartsWith(code)) + .Select(ou => ou.Id) + .ToListAsync(GetCancellationToken(cancellationToken)) + ; + + return await GetMongoQueryable() + .Where(u => u.OrganizationUnits.Any(uou => organizationUnitIds.Contains(uou.OrganizationUnitId))) + .ToListAsync(GetCancellationToken(cancellationToken)) + ; + } } } diff --git a/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/MongoOrganizationUnitRepository.cs b/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/MongoOrganizationUnitRepository.cs new file mode 100644 index 0000000000..ae735300fd --- /dev/null +++ b/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/MongoOrganizationUnitRepository.cs @@ -0,0 +1,138 @@ +using MongoDB.Driver; +using MongoDB.Driver.Linq; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Dynamic.Core; +using System.Threading; +using System.Threading.Tasks; +using Volo.Abp.Domain.Repositories.MongoDB; +using Volo.Abp.MongoDB; + +namespace Volo.Abp.Identity.MongoDB +{ + public class MongoOrganizationUnitRepository : MongoDbRepository, IOrganizationUnitRepository + { + public MongoOrganizationUnitRepository( + IMongoDbContextProvider dbContextProvider) + : base(dbContextProvider) + { + } + + public virtual async Task> GetChildrenAsync( + Guid? parentId, + bool includeDetails = false, + CancellationToken cancellationToken = default) + { + return await GetMongoQueryable() + .Where(ou => ou.ParentId == parentId) + .ToListAsync(GetCancellationToken(cancellationToken)); + } + + public virtual async Task> GetAllChildrenWithParentCodeAsync( + string code, + Guid? parentId, + bool includeDetails = false, + CancellationToken cancellationToken = default) + { + return await GetMongoQueryable() + .Where(ou => ou.Code.StartsWith(code) && ou.Id != parentId.Value) + .ToListAsync(GetCancellationToken(cancellationToken)); + } + + public virtual async Task> GetListAsync( + IEnumerable ids, + bool includeDetails = false, + CancellationToken cancellationToken = default) + { + return await GetMongoQueryable() + .Where(t => ids.Contains(t.Id)) + .ToListAsync(GetCancellationToken(cancellationToken)); + } + + public virtual async Task> GetListAsync( + string sorting = null, + int maxResultCount = int.MaxValue, + int skipCount = 0, + bool includeDetails = false, + CancellationToken cancellationToken = default) + { + return await GetMongoQueryable() + .OrderBy(sorting ?? nameof(OrganizationUnit.DisplayName)) + .As>() + .PageBy>(skipCount, maxResultCount) + .ToListAsync(GetCancellationToken(cancellationToken)); + } + + public virtual async Task GetAsync( + string displayName, + bool includeDetails = true, + CancellationToken cancellationToken = default) + { + return await GetMongoQueryable() + .FirstOrDefaultAsync( + ou => ou.DisplayName == displayName, + GetCancellationToken(cancellationToken) + ); + } + + public virtual async Task> GetRolesAsync( + OrganizationUnit organizationUnit, + string sorting = null, + int maxResultCount = int.MaxValue, + int skipCount = 0, + bool includeDetails = false, + CancellationToken cancellationToken = default) + { + var roleIds = organizationUnit.Roles.Select(r => r.RoleId).ToArray(); + return await DbContext.Roles.AsQueryable().Where(r => roleIds.Contains(r.Id)) + .OrderBy(sorting ?? nameof(IdentityRole.Name)) + .As>() + .PageBy>(skipCount, maxResultCount) + .ToListAsync(cancellationToken); + } + + public virtual async Task GetRolesCountAsync( + OrganizationUnit organizationUnit, + CancellationToken cancellationToken = default) + { + var roleIds = organizationUnit.Roles.Select(r => r.RoleId).ToArray(); + return await DbContext.Roles.AsQueryable().Where(r => roleIds.Contains(r.Id)) + .As>() + .CountAsync(cancellationToken); + } + + public virtual async Task> GetMembersAsync( + OrganizationUnit organizationUnit, + string sorting = null, + int maxResultCount = int.MaxValue, + int skipCount = 0, + string filter = null, + bool includeDetails = false, + CancellationToken cancellationToken = default) + { + return await DbContext.Users.AsQueryable() + .Where(u => u.OrganizationUnits.Any(uou => uou.OrganizationUnitId == organizationUnit.Id)) + .WhereIf>( + !filter.IsNullOrWhiteSpace(), + u => + u.UserName.Contains(filter) || + u.Email.Contains(filter) + ) + .OrderBy(sorting ?? nameof(IdentityUser.UserName)) + .As>() + .PageBy>(skipCount, maxResultCount) + .ToListAsync(GetCancellationToken(cancellationToken)); + } + + public virtual async Task GetMembersCountAsync( + OrganizationUnit organizationUnit, + CancellationToken cancellationToken = default) + { + return await DbContext.Users.AsQueryable() + .Where(u => u.OrganizationUnits.Any(uou => uou.OrganizationUnitId == organizationUnit.Id)) + .As>() + .CountAsync(GetCancellationToken(cancellationToken)); + } + } +} diff --git a/modules/identity/test/Volo.Abp.Identity.Application.Tests/Volo/Abp/Identity/IdentityUserAppService_Tests.cs b/modules/identity/test/Volo.Abp.Identity.Application.Tests/Volo/Abp/Identity/IdentityUserAppService_Tests.cs index 6fbf23ed73..a56631b2ae 100644 --- a/modules/identity/test/Volo.Abp.Identity.Application.Tests/Volo/Abp/Identity/IdentityUserAppService_Tests.cs +++ b/modules/identity/test/Volo.Abp.Identity.Application.Tests/Volo/Abp/Identity/IdentityUserAppService_Tests.cs @@ -189,9 +189,10 @@ namespace Volo.Abp.Identity //Assert - result.Items.Count.ShouldBe(2); + result.Items.Count.ShouldBe(3); result.Items.ShouldContain(r => r.Name == "moderator"); result.Items.ShouldContain(r => r.Name == "supporter"); + result.Items.ShouldContain(r => r.Name == "manager"); } [Fact] @@ -214,9 +215,10 @@ namespace Volo.Abp.Identity //Assert var roleNames = await _userRepository.GetRoleNamesAsync(johnNash.Id); - roleNames.Count.ShouldBe(2); + roleNames.Count.ShouldBe(3); roleNames.ShouldContain("admin"); roleNames.ShouldContain("moderator"); + roleNames.ShouldContain("manager"); } private static string CreateRandomEmail() diff --git a/modules/identity/test/Volo.Abp.Identity.Domain.Tests/Volo/Abp/Identity/IdentityUserManager_Tests.cs b/modules/identity/test/Volo.Abp.Identity.Domain.Tests/Volo/Abp/Identity/IdentityUserManager_Tests.cs index 32f344a32e..04f08ed266 100644 --- a/modules/identity/test/Volo.Abp.Identity.Domain.Tests/Volo/Abp/Identity/IdentityUserManager_Tests.cs +++ b/modules/identity/test/Volo.Abp.Identity.Domain.Tests/Volo/Abp/Identity/IdentityUserManager_Tests.cs @@ -1,11 +1,9 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Identity; using Shouldly; -using Volo.Abp.Castle.DynamicProxy; using Volo.Abp.Uow; using Xunit; @@ -16,6 +14,7 @@ namespace Volo.Abp.Identity private readonly IdentityUserManager _identityUserManager; private readonly IIdentityUserRepository _identityUserRepository; private readonly IIdentityRoleRepository _identityRoleRepository; + private readonly IOrganizationUnitRepository _organizationUnitRepository; private readonly ILookupNormalizer _lookupNormalizer; private readonly IUnitOfWorkManager _unitOfWorkManager; private readonly IdentityTestData _testData; @@ -25,6 +24,7 @@ namespace Volo.Abp.Identity _identityUserManager = GetRequiredService(); _identityUserRepository = GetRequiredService(); _identityRoleRepository = GetRequiredService(); + _organizationUnitRepository = GetRequiredService(); _lookupNormalizer = GetRequiredService(); _testData = GetRequiredService(); _unitOfWorkManager = GetRequiredService(); @@ -88,6 +88,35 @@ namespace Volo.Abp.Identity } } + [Fact] + public async Task SetOrganizationUnitsAsync() + { + using (var uow = _unitOfWorkManager.Begin()) + { + var user = await _identityUserRepository.FindByNormalizedUserNameAsync( + _lookupNormalizer.NormalizeName("david")); + user.ShouldNotBeNull(); + + var ou = await _organizationUnitRepository.GetAsync( + _lookupNormalizer.NormalizeName("OU11")); + ou.ShouldNotBeNull(); + + await _identityUserManager.SetOrganizationUnitsAsync(user, new Guid[] + { + ou.Id + }); + + user = await _identityUserRepository.FindByNormalizedUserNameAsync( + _lookupNormalizer.NormalizeName("david")); + user.OrganizationUnits.Count.ShouldBeGreaterThan(0); + user.OrganizationUnits.FirstOrDefault(uou => uou.OrganizationUnitId == ou.Id).ShouldNotBeNull(); + + await uow.CompleteAsync(); + + + } + } + [Fact] public async Task AddDefaultRolesAsync_In_Same_Uow() { @@ -111,6 +140,35 @@ namespace Volo.Abp.Identity role.IsDefault.ShouldBe(true); } + await uow.CompleteAsync(); + + } + } + + [Fact] + public async Task SetOrganizationUnits_Should_Remove() + { + using (var uow = _unitOfWorkManager.Begin()) + { + var ou = await _organizationUnitRepository.GetAsync( + _lookupNormalizer.NormalizeName("OU111")); + ou.ShouldNotBeNull(); + + var user = await _identityUserRepository.FindByNormalizedUserNameAsync( + _lookupNormalizer.NormalizeName("john.nash")); + user.ShouldNotBeNull(); + + var ouNew = await _organizationUnitRepository.GetAsync( + _lookupNormalizer.NormalizeName("OU2")); + ouNew.ShouldNotBeNull(); + + await _identityUserManager.SetOrganizationUnitsAsync(user, new Guid[] + { + ouNew.Id + }); + + user.OrganizationUnits.ShouldNotContain(x => x.OrganizationUnitId == ou.Id); + await uow.CompleteAsync(); } } diff --git a/modules/identity/test/Volo.Abp.Identity.Domain.Tests/Volo/Abp/Identity/OrganizationUnitManager_Tests.cs b/modules/identity/test/Volo.Abp.Identity.Domain.Tests/Volo/Abp/Identity/OrganizationUnitManager_Tests.cs new file mode 100644 index 0000000000..2f9ac794ac --- /dev/null +++ b/modules/identity/test/Volo.Abp.Identity.Domain.Tests/Volo/Abp/Identity/OrganizationUnitManager_Tests.cs @@ -0,0 +1,121 @@ +using Microsoft.AspNetCore.Identity; +using Shouldly; +using System.Linq; +using System.Threading.Tasks; +using Volo.Abp.Guids; +using Volo.Abp.Uow; +using Xunit; + +namespace Volo.Abp.Identity +{ + public class OrganizationUnitManager_Tests : AbpIdentityDomainTestBase + { + private readonly OrganizationUnitManager _organizationUnitManager; + private readonly IOrganizationUnitRepository _organizationUnitRepository; + private readonly IdentityTestData _testData; + private readonly IIdentityRoleRepository _identityRoleRepository; + private readonly ILookupNormalizer _lookupNormalizer; + private readonly IUnitOfWorkManager _unitOfWorkManager; + private readonly IGuidGenerator _guidGenerator; + public OrganizationUnitManager_Tests() + { + _organizationUnitManager = GetRequiredService(); + _organizationUnitRepository = GetRequiredService(); + _identityRoleRepository = GetRequiredService(); + _lookupNormalizer = GetRequiredService(); + _testData = GetRequiredService(); + _unitOfWorkManager = GetRequiredService(); + _guidGenerator = GetService(); + } + + [Fact] + public async Task CreateAsnyc() + { + await _organizationUnitManager.CreateAsync(new OrganizationUnit(_guidGenerator.Create(), "Root 1")); + + var root1 = await _organizationUnitRepository.GetAsync("Root 1"); + root1.ShouldNotBeNull(); + } + + [Fact] + public async Task UpdateAsync() + { + var ou = await _organizationUnitRepository.GetAsync("OU111"); + ou.DisplayName = "OU111 Updated"; + await _organizationUnitManager.UpdateAsync(ou); + + var ouAfterChange = await _organizationUnitRepository.GetAsync("OU111 Updated"); + ouAfterChange.DisplayName.ShouldContain("OU111 Updated"); + } + + [Fact] + public async Task DeleteAsync() + { + var ou = await _organizationUnitRepository.GetAsync("OU11"); + await _organizationUnitManager.DeleteAsync(ou.Id); + + (await _organizationUnitRepository.GetAsync("OU11")).ShouldBeNull(); + } + + [Fact] + public async Task MoveAsync() + { + var ou1 = await _organizationUnitRepository.GetAsync("OU1"); + var ou2 = await _organizationUnitRepository.GetAsync("OU2"); + + await _organizationUnitManager.MoveAsync(ou1.Id, ou2.Id); + + ou1 = await _organizationUnitRepository.GetAsync("OU1"); + ou1.ParentId.ShouldBe(ou2.Id); + ou1.Code.ShouldBe(OrganizationUnit.CreateCode(2, 2)); + + var ou11 = await _organizationUnitRepository.GetAsync("OU11"); + ou11.ParentId.ShouldBe(ou1.Id); + ou11.Code.ShouldBe(OrganizationUnit.CreateCode(2, 2, 1)); + + var ou111 = await _organizationUnitRepository.GetAsync("OU111"); + ou111.ParentId.ShouldBe(ou11.Id); + ou111.Code.ShouldBe(OrganizationUnit.CreateCode(2, 2, 1, 1)); + + var ou112 = await _organizationUnitRepository.GetAsync("OU112"); + ou112.ParentId.ShouldBe(ou11.Id); + ou112.Code.ShouldBe(OrganizationUnit.CreateCode(2, 2, 1, 2)); + + var ou12 = await _organizationUnitRepository.GetAsync("OU12"); + ou12.ParentId.ShouldBe(ou1.Id); + ou12.Code.ShouldBe(OrganizationUnit.CreateCode(2, 2, 2)); + } + + [Fact] + public async Task AddRoleToOrganizationUnitAsync() + { + OrganizationUnit ou = null; + IdentityRole adminRole = null; + + using (var uow = _unitOfWorkManager.Begin()) + { + ou = await _organizationUnitRepository.GetAsync("OU1", true); + adminRole = await _identityRoleRepository.FindByNormalizedNameAsync(_lookupNormalizer.NormalizeName("admin")); + await _organizationUnitManager.AddRoleToOrganizationUnitAsync(adminRole, ou); + await _organizationUnitRepository.UpdateAsync(ou); + + await uow.CompleteAsync(); + } + + ou = await _organizationUnitRepository.GetAsync("OU1", includeDetails: true); + ou.Roles.First().RoleId.ShouldBe(adminRole.Id); + } + + [Fact] + public async Task RemoveRoleFromOrganizationUnitAsync() + { + var ou = await _organizationUnitRepository.GetAsync("OU1", true); + var adminRole = await _identityRoleRepository.FindByNormalizedNameAsync(_lookupNormalizer.NormalizeName("admin")); + await _organizationUnitManager.AddRoleToOrganizationUnitAsync(adminRole.Id, ou.Id); + + await _organizationUnitManager.RemoveRoleFromOrganizationUnitAsync(adminRole.Id, ou.Id); + ou = await _organizationUnitRepository.GetAsync("OU1", includeDetails: true); + ou.Roles.FirstOrDefault(r => r.RoleId == adminRole.Id).ShouldBeNull(); + } + } +} diff --git a/modules/identity/test/Volo.Abp.Identity.EntityFrameworkCore.Tests/Volo/Abp/Identity/EntityFrameworkCore/OrganizationUnitRepository_Tests.cs b/modules/identity/test/Volo.Abp.Identity.EntityFrameworkCore.Tests/Volo/Abp/Identity/EntityFrameworkCore/OrganizationUnitRepository_Tests.cs new file mode 100644 index 0000000000..3e8de4f511 --- /dev/null +++ b/modules/identity/test/Volo.Abp.Identity.EntityFrameworkCore.Tests/Volo/Abp/Identity/EntityFrameworkCore/OrganizationUnitRepository_Tests.cs @@ -0,0 +1,6 @@ +namespace Volo.Abp.Identity.EntityFrameworkCore +{ + public class OrganizationUnitRepository_Tests : OrganizationUnitRepository_Tests + { + } +} diff --git a/modules/identity/test/Volo.Abp.Identity.MongoDB.Tests/Volo/Abp/Identity/MongoDB/OrganizationUnitRepository_Tests.cs b/modules/identity/test/Volo.Abp.Identity.MongoDB.Tests/Volo/Abp/Identity/MongoDB/OrganizationUnitRepository_Tests.cs new file mode 100644 index 0000000000..d60611b2b6 --- /dev/null +++ b/modules/identity/test/Volo.Abp.Identity.MongoDB.Tests/Volo/Abp/Identity/MongoDB/OrganizationUnitRepository_Tests.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Volo.Abp.Identity.MongoDB +{ + public class OrganizationUnitRepository_Tests : OrganizationUnitRepository_Tests + { + } +} diff --git a/modules/identity/test/Volo.Abp.Identity.TestBase/Volo/Abp/Identity/AbpIdentityTestDataBuilder.cs b/modules/identity/test/Volo.Abp.Identity.TestBase/Volo/Abp/Identity/AbpIdentityTestDataBuilder.cs index 69361ff5a8..9258be16af 100644 --- a/modules/identity/test/Volo.Abp.Identity.TestBase/Volo/Abp/Identity/AbpIdentityTestDataBuilder.cs +++ b/modules/identity/test/Volo.Abp.Identity.TestBase/Volo/Abp/Identity/AbpIdentityTestDataBuilder.cs @@ -1,4 +1,5 @@ -using System.Security.Claims; +using System; +using System.Security.Claims; using System.Threading.Tasks; using Microsoft.AspNetCore.Identity; using Volo.Abp.DependencyInjection; @@ -12,20 +13,27 @@ namespace Volo.Abp.Identity private readonly IIdentityUserRepository _userRepository; private readonly IIdentityClaimTypeRepository _identityClaimTypeRepository; private readonly IIdentityRoleRepository _roleRepository; + private readonly IOrganizationUnitRepository _organizationUnitRepository; private readonly ILookupNormalizer _lookupNormalizer; private readonly IdentityTestData _testData; + private readonly OrganizationUnitManager _organizationUnitManager; private IdentityRole _adminRole; - private IdentityRole _moderator; + private IdentityRole _moderatorRole; private IdentityRole _supporterRole; + private IdentityRole _managerRole; + private OrganizationUnit _ou111; + private OrganizationUnit _ou112; public AbpIdentityTestDataBuilder( IGuidGenerator guidGenerator, IIdentityUserRepository userRepository, IIdentityClaimTypeRepository identityClaimTypeRepository, IIdentityRoleRepository roleRepository, + IOrganizationUnitRepository organizationUnitRepository, ILookupNormalizer lookupNormalizer, - IdentityTestData testData) + IdentityTestData testData, + OrganizationUnitManager organizationUnitManager) { _guidGenerator = guidGenerator; _userRepository = userRepository; @@ -33,11 +41,14 @@ namespace Volo.Abp.Identity _roleRepository = roleRepository; _lookupNormalizer = lookupNormalizer; _testData = testData; + _organizationUnitRepository = organizationUnitRepository; + _organizationUnitManager = organizationUnitManager; } public async Task Build() { await AddRoles(); + await AddOrganizationUnits(); await AddUsers(); await AddClaimTypes(); } @@ -46,12 +57,41 @@ namespace Volo.Abp.Identity { _adminRole = await _roleRepository.FindByNormalizedNameAsync(_lookupNormalizer.NormalizeName("admin")); - _moderator = new IdentityRole(_testData.RoleModeratorId, "moderator"); - _moderator.AddClaim(_guidGenerator, new Claim("test-claim", "test-value")); - await _roleRepository.InsertAsync(_moderator); + _moderatorRole = new IdentityRole(_testData.RoleModeratorId, "moderator"); + _moderatorRole.AddClaim(_guidGenerator, new Claim("test-claim", "test-value")); + await _roleRepository.InsertAsync(_moderatorRole); _supporterRole = new IdentityRole(_guidGenerator.Create(), "supporter"); await _roleRepository.InsertAsync(_supporterRole); + + _managerRole = new IdentityRole(_guidGenerator.Create(), "manager"); + await _roleRepository.InsertAsync(_managerRole); + } + + /* Creates OU tree as shown below: + * + * - OU1 + * - OU11 + * - OU111 + * - OU112 + * - OU12 + * - OU2 + * - OU21 + */ + private async Task AddOrganizationUnits() + { + var ou1 = await CreateOU("OU1", OrganizationUnit.CreateCode(1)); + var ou11 = await CreateOU("OU11", OrganizationUnit.CreateCode(1, 1), ou1.Id); + _ou112 = await CreateOU("OU112", OrganizationUnit.CreateCode(1, 1, 2), ou11.Id); + var ou12 = await CreateOU("OU12", OrganizationUnit.CreateCode(1, 2), ou1.Id); + var ou2 = await CreateOU("OU2", OrganizationUnit.CreateCode(2)); + var ou21 = await CreateOU("OU21", OrganizationUnit.CreateCode(2, 1), ou2.Id); + + _ou111 = new OrganizationUnit(_guidGenerator.Create(), "OU111", ou11.Id); + _ou111.Code = OrganizationUnit.CreateCode(1, 1, 1); + _ou111.AddRole(_moderatorRole.Id); + _ou111.AddRole(_managerRole.Id); + await _organizationUnitRepository.InsertAsync(_ou111); } private async Task AddUsers() @@ -62,8 +102,10 @@ namespace Volo.Abp.Identity await _userRepository.InsertAsync(adminUser); var john = new IdentityUser(_testData.UserJohnId, "john.nash", "john.nash@abp.io"); - john.AddRole(_moderator.Id); + john.AddRole(_moderatorRole.Id); john.AddRole(_supporterRole.Id); + john.AddOrganizationUnit(_ou111.Id); + john.AddOrganizationUnit(_ou112.Id); john.AddLogin(new UserLoginInfo("github", "john", "John Nash")); john.AddLogin(new UserLoginInfo("twitter", "johnx", "John Nash")); john.AddClaim(_guidGenerator, new Claim("TestClaimType", "42")); @@ -71,20 +113,30 @@ namespace Volo.Abp.Identity await _userRepository.InsertAsync(john); var david = new IdentityUser(_testData.UserDavidId, "david", "david@abp.io"); + david.AddOrganizationUnit(_ou112.Id); await _userRepository.InsertAsync(david); var neo = new IdentityUser(_testData.UserNeoId, "neo", "neo@abp.io"); neo.AddRole(_supporterRole.Id); neo.AddClaim(_guidGenerator, new Claim("TestClaimType", "43")); + neo.AddOrganizationUnit(_ou111.Id); await _userRepository.InsertAsync(neo); } + private async Task AddClaimTypes() { - var ageClaim = new IdentityClaimType(_testData.AgeClaimId, "Age", false, false, null, null, null,IdentityClaimValueType.Int); + var ageClaim = new IdentityClaimType(_testData.AgeClaimId, "Age", false, false, null, null, null, IdentityClaimValueType.Int); await _identityClaimTypeRepository.InsertAsync(ageClaim); + var educationClaim = new IdentityClaimType(_testData.EducationClaimId, "Education", true, false, null, null, null); await _identityClaimTypeRepository.InsertAsync(educationClaim); } + + private async Task CreateOU(string displayName, string code, Guid? parentId = null) + { + var ou = await _organizationUnitRepository.InsertAsync(new OrganizationUnit(_guidGenerator.Create(), displayName, parentId) { Code = code }); + return ou; + } } } \ No newline at end of file diff --git a/modules/identity/test/Volo.Abp.Identity.TestBase/Volo/Abp/Identity/IdentityUserRepository_Tests.cs b/modules/identity/test/Volo.Abp.Identity.TestBase/Volo/Abp/Identity/IdentityUserRepository_Tests.cs index 4fa407a512..1d6728c0ae 100644 --- a/modules/identity/test/Volo.Abp.Identity.TestBase/Volo/Abp/Identity/IdentityUserRepository_Tests.cs +++ b/modules/identity/test/Volo.Abp.Identity.TestBase/Volo/Abp/Identity/IdentityUserRepository_Tests.cs @@ -15,11 +15,13 @@ namespace Volo.Abp.Identity { protected IIdentityUserRepository UserRepository { get; } protected ILookupNormalizer LookupNormalizer { get; } + protected IOrganizationUnitRepository OrganizationUnitRepository { get; } protected IdentityUserRepository_Tests() { UserRepository = ServiceProvider.GetRequiredService(); LookupNormalizer = ServiceProvider.GetRequiredService(); + OrganizationUnitRepository = ServiceProvider.GetRequiredService(); } [Fact] @@ -42,9 +44,10 @@ namespace Volo.Abp.Identity { var john = await UserRepository.FindByNormalizedUserNameAsync(LookupNormalizer.NormalizeName("john.nash")); var roles = await UserRepository.GetRoleNamesAsync(john.Id); - roles.Count.ShouldBe(2); + roles.Count.ShouldBe(3); roles.ShouldContain("moderator"); roles.ShouldContain("supporter"); + roles.ShouldContain("manager"); } [Fact] @@ -116,9 +119,10 @@ namespace Volo.Abp.Identity { var john = await UserRepository.FindByNormalizedUserNameAsync(LookupNormalizer.NormalizeName("john.nash")); var roles = await UserRepository.GetRolesAsync(john.Id); - roles.Count.ShouldBe(2); + roles.Count.ShouldBe(3); roles.ShouldContain(r => r.Name == "moderator"); roles.ShouldContain(r => r.Name == "supporter"); + roles.ShouldContain(r => r.Name == "manager"); } [Fact] @@ -128,6 +132,22 @@ namespace Volo.Abp.Identity (await UserRepository.GetCountAsync("undefined-username")).ShouldBe(0); } + [Fact] + public async Task GetUsersInOrganizationUnitAsync() + { + var users = await UserRepository.GetUsersInOrganizationUnitAsync((await GetOU("OU111")).Id); + users.ShouldNotBeNull(); + users.Count.ShouldBeGreaterThan(0); + } + + [Fact] + public async Task GetUsersInOrganizationUnitWithChildrenAsync() + { + var users = await UserRepository.GetUsersInOrganizationUnitWithChildrenAsync((await GetOU("OU111")).Code); + users.ShouldNotBeNull(); + users.Count.ShouldBeGreaterThan(0); + } + [Fact] public async Task Should_Eager_Load_User_Collections() { @@ -144,6 +164,16 @@ namespace Volo.Abp.Identity john.Tokens.ShouldNotBeNull(); john.Tokens.Any().ShouldBeTrue(); + + john.OrganizationUnits.ShouldNotBeNull(); + john.OrganizationUnits.Any().ShouldBeTrue(); + } + + private async Task GetOU(string diplayName) + { + var organizationUnit = await OrganizationUnitRepository.GetAsync(diplayName); + organizationUnit.ShouldNotBeNull(); + return organizationUnit; } } } diff --git a/modules/identity/test/Volo.Abp.Identity.TestBase/Volo/Abp/Identity/Identity_Repository_Resolve_Tests.cs b/modules/identity/test/Volo.Abp.Identity.TestBase/Volo/Abp/Identity/Identity_Repository_Resolve_Tests.cs index 8b748f10b7..6d541d5987 100644 --- a/modules/identity/test/Volo.Abp.Identity.TestBase/Volo/Abp/Identity/Identity_Repository_Resolve_Tests.cs +++ b/modules/identity/test/Volo.Abp.Identity.TestBase/Volo/Abp/Identity/Identity_Repository_Resolve_Tests.cs @@ -20,6 +20,8 @@ namespace Volo.Abp.Identity ServiceProvider.GetService>().ShouldNotBeNull(); ServiceProvider.GetService>().ShouldNotBeNull(); ServiceProvider.GetService().ShouldNotBeNull(); + + ServiceProvider.GetService().ShouldNotBeNull(); } } } diff --git a/modules/identity/test/Volo.Abp.Identity.TestBase/Volo/Abp/Identity/LazyLoading_Tests.cs b/modules/identity/test/Volo.Abp.Identity.TestBase/Volo/Abp/Identity/LazyLoading_Tests.cs index 1ae671dd4f..eae5ee5c1d 100644 --- a/modules/identity/test/Volo.Abp.Identity.TestBase/Volo/Abp/Identity/LazyLoading_Tests.cs +++ b/modules/identity/test/Volo.Abp.Identity.TestBase/Volo/Abp/Identity/LazyLoading_Tests.cs @@ -14,6 +14,7 @@ namespace Volo.Abp.Identity { protected IIdentityUserRepository UserRepository { get; } protected IIdentityRoleRepository RoleRepository { get; } + protected IOrganizationUnitRepository OrganizationUnitRepository { get; } protected ILookupNormalizer LookupNormalizer { get; } protected LazyLoading_Tests() @@ -21,6 +22,7 @@ namespace Volo.Abp.Identity UserRepository = ServiceProvider.GetRequiredService(); RoleRepository = ServiceProvider.GetRequiredService(); LookupNormalizer = ServiceProvider.GetRequiredService(); + OrganizationUnitRepository = ServiceProvider.GetRequiredService(); } [Fact] @@ -55,6 +57,22 @@ namespace Volo.Abp.Identity john.Tokens.ShouldNotBeNull(); john.Tokens.Any().ShouldBeTrue(); + john.OrganizationUnits.ShouldNotBeNull(); + john.OrganizationUnits.Any().ShouldBeTrue(); + + await uow.CompleteAsync(); + } + } + + [Fact] + public async Task Should_Lazy_Load_OrganizationUnit_Collections() + { + using (var uow = GetRequiredService().Begin()) + { + var ou = await OrganizationUnitRepository.GetAsync(LookupNormalizer.NormalizeName("OU111"), includeDetails: false); + ou.Roles.ShouldNotBeNull(); //? + ou.Roles.Any().ShouldBeTrue(); + await uow.CompleteAsync(); } } diff --git a/modules/identity/test/Volo.Abp.Identity.TestBase/Volo/Abp/Identity/OrganizationUnitRepository_Tests.cs b/modules/identity/test/Volo.Abp.Identity.TestBase/Volo/Abp/Identity/OrganizationUnitRepository_Tests.cs new file mode 100644 index 0000000000..6f6835396d --- /dev/null +++ b/modules/identity/test/Volo.Abp.Identity.TestBase/Volo/Abp/Identity/OrganizationUnitRepository_Tests.cs @@ -0,0 +1,155 @@ +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.DependencyInjection; +using Shouldly; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Volo.Abp.Guids; +using Volo.Abp.Modularity; +using Volo.Abp.Uow; +using Xunit; + +namespace Volo.Abp.Identity +{ + public abstract class OrganizationUnitRepository_Tests : AbpIdentityTestBase + where TStartupModule : IAbpModule + { + private readonly IOrganizationUnitRepository _organizationUnitRepository; + private readonly ILookupNormalizer _lookupNormalizer; + private readonly IdentityTestData _testData; + private readonly IGuidGenerator _guidGenerator; + private readonly OrganizationUnitManager _organizationUnitManager; + private readonly IIdentityRoleRepository _identityRoleRepository; + private readonly IUnitOfWorkManager _unitOfWorkManager; + private readonly IIdentityUserRepository _identityUserRepository; + + public OrganizationUnitRepository_Tests() + { + _organizationUnitRepository = ServiceProvider.GetRequiredService(); + _lookupNormalizer = ServiceProvider.GetRequiredService(); + _testData = GetRequiredService(); + _guidGenerator = GetRequiredService(); + _organizationUnitManager = GetRequiredService(); + _identityRoleRepository = GetRequiredService(); + _unitOfWorkManager = GetRequiredService(); + _identityUserRepository = GetRequiredService(); + } + + [Fact] + public async Task GetChildrenAsync() + { + (await _organizationUnitRepository.GetChildrenAsync(_testData.RoleModeratorId)).ShouldNotBeNull(); + } + + [Fact] + public async Task GetAllChildrenWithParentCodeAsync() + { + (await _organizationUnitRepository.GetAllChildrenWithParentCodeAsync(OrganizationUnit.CreateCode(0), _guidGenerator.Create())).ShouldNotBeNull(); + } + + [Fact] + public async Task GetListAsync() + { + var ouIds = (await _organizationUnitRepository.GetListAsync(includeDetails: true)) + .Select(ou => ou.Id).Take(2); + var ous = await _organizationUnitRepository.GetListAsync(ouIds); + ous.Count.ShouldBe(2); + ous.ShouldContain(ou => ou.Id == ouIds.First()); + } + + [Fact] + public async Task GetOrganizationUnitAsync() + { + var organizationUnit = await _organizationUnitRepository.GetAsync("OU111"); + organizationUnit.ShouldNotBeNull(); + } + + [Fact] + public async Task GetCountAsync() + { + (await _organizationUnitRepository.GetCountAsync()).ShouldBeGreaterThan(0); + } + + [Fact] + public async Task Should_Eager_Load_OrganizationUnit_Collections() + { + var ou = (await _organizationUnitRepository.GetListAsync(includeDetails: true)) + .FirstOrDefault(ou => ou.DisplayName == "OU111"); + ou.Roles.ShouldNotBeNull(); + ou.Roles.Any().ShouldBeTrue(); + } + [Fact] + public async Task GetOrganizationUnitRolesAsync() + { + OrganizationUnit ou = await _organizationUnitRepository.GetAsync("OU111", includeDetails: true); + + var ou111Roles = await _organizationUnitRepository.GetRolesAsync(ou, includeDetails: true); + ou111Roles.Count.ShouldBe(2); + ou111Roles.ShouldContain(n => n.Name == "manager"); + ou111Roles.ShouldContain(n => n.Name == "moderator"); + } + + [Fact] + public async Task GetOrganizationUnitRolesWithPagingAsync() + { + OrganizationUnit ou = await _organizationUnitRepository.GetAsync("OU111", includeDetails: true); + + var ou111Roles = await _organizationUnitRepository.GetRolesAsync(ou, sorting: "name desc", maxResultCount: 1, includeDetails: true); + ou111Roles.Count.ShouldBe(1); + ou111Roles.ShouldContain(n => n.Name == "moderator"); + } + + [Fact] + public async Task GetMembersInOrganizationUnitListAsync() + { + OrganizationUnit ou1 = await _organizationUnitRepository.GetAsync("OU111", true); + OrganizationUnit ou2 = await _organizationUnitRepository.GetAsync("OU112", true); + var users = await _identityUserRepository.GetUsersInOrganizationsListAsync(new List { ou1.Id, ou2.Id }); + users.Count.ShouldBeGreaterThan(0); + } + [Fact] + public async Task GetMembersInOrganizationUnitWithParamsAsync() + { + OrganizationUnit ou = await _organizationUnitRepository.GetAsync("OU111", true); + var users = await _organizationUnitRepository.GetMembersAsync(ou, "UserName DESC", 5, 0, "n"); + + users.Count.ShouldBeGreaterThan(1); + users.Count.ShouldBeLessThanOrEqualTo(5); + + //Filter check + users.ShouldAllBe(u => u.UserName.Contains("ne") || u.Email.Contains("n")); + + //Order check + for (var i = 0; i < users.Count - 1; i++) + { + string.Compare( + users[i].UserName, + users[i + 1].UserName, + StringComparison.OrdinalIgnoreCase + ).ShouldBeGreaterThan(0); + } + + users = await _organizationUnitRepository.GetMembersAsync(ou, null, 999, 0, "undefined-username"); + users.Count.ShouldBe(0); + } + + [Fact] + public async Task GetMembersCountOfOrganizationUnit() + { + OrganizationUnit ou = await _organizationUnitRepository.GetAsync("OU111", true); + var usersCount = await _organizationUnitRepository.GetMembersCountAsync(ou); + + usersCount.ShouldBeGreaterThan(1); + } + + [Fact] + public async Task GetRolesCountOfOrganizationUnit() + { + OrganizationUnit ou = await _organizationUnitRepository.GetAsync("OU111", true); + var rolesCount = await _organizationUnitRepository.GetRolesCountAsync(ou); + + rolesCount.ShouldBeGreaterThan(1); + } + } +}