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 e7a038fd57..87c599ea4f 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,7 @@ "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.", "Volo.Abp.Identity:010001": "You can not delete your own account!", "Permission:IdentityManagement": "Identity management", "Permission:RoleManagement": "Role management", 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 8b8b936c0a..cfc58d4dad 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,7 @@ "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.", "Volo.Abp.Identity:010001": "Kendi hesabınızı silemezsiniz!", "Permission:IdentityManagement": "Kimlik yönetimi", "Permission:RoleManagement": "Rol yönetimi", 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 f57029eb9e..b80c820f98 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/Organizations/OrganizationUnitConsts.cs b/modules/identity/src/Volo.Abp.Identity.Domain.Shared/Volo/Abp/Identity/Organizations/OrganizationUnitConsts.cs new file mode 100644 index 0000000000..a704e6bb05 --- /dev/null +++ b/modules/identity/src/Volo.Abp.Identity.Domain.Shared/Volo/Abp/Identity/Organizations/OrganizationUnitConsts.cs @@ -0,0 +1,25 @@ +namespace Volo.Abp.Identity.Organizations +{ + public 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/Volo/Abp/Identity/Organizations/IOrganizationUnitRepository.cs b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/Organizations/IOrganizationUnitRepository.cs new file mode 100644 index 0000000000..0136d9ef4c --- /dev/null +++ b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/Organizations/IOrganizationUnitRepository.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Volo.Abp.Domain.Repositories; + +namespace Volo.Abp.Identity.Organizations +{ + public interface IOrganizationUnitRepository : IBasicRepository + { + Task> GetChildrenAsync(Guid? parentId); + + Task> GetAllChildrenWithParentCodeAsync(string code, Guid? parentId); + } +} diff --git a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/Organizations/OrganizationUnit.cs b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/Organizations/OrganizationUnit.cs new file mode 100644 index 0000000000..0955aa248a --- /dev/null +++ b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/Organizations/OrganizationUnit.cs @@ -0,0 +1,178 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Volo.Abp.Domain.Entities.Auditing; +using Volo.Abp.MultiTenancy; + +namespace Volo.Abp.Identity.Organizations +{ + /// + /// Represents an organization unit (OU). + /// + public class OrganizationUnit : FullAuditedAggregateRoot, IMultiTenant + { + public virtual Guid? TenantId { get; protected set; } + + /// + /// Parent Id. + /// Null, if this OU is root. + /// + public virtual Guid? ParentId { get; set; } + + /// + /// Hierarchical Code of this organization unit. + /// Example: "00001.00042.00005". + /// This is a unique code for a Tenant. + /// It's changeable if OU hierarch is changed. + /// + public virtual string Code { get; set; } + + /// + /// Display name of this role. + /// + public virtual string DisplayName { get; set; } + + /// + /// Children of this OU. + /// + public virtual ICollection Children { get; 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? tenantId, string displayName, Guid? parentId = null) + { + TenantId = tenantId; + DisplayName = displayName; + ParentId = parentId; + } + + /// + /// 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("."); + } + } +} diff --git a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/Organizations/OrganizationUnitManager.cs b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/Organizations/OrganizationUnitManager.cs new file mode 100644 index 0000000000..88aad4d66a --- /dev/null +++ b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/Organizations/OrganizationUnitManager.cs @@ -0,0 +1,138 @@ +using Microsoft.Extensions.Localization; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Volo.Abp.Domain.Services; +using Volo.Abp.Identity.Localization; +using Volo.Abp.Uow; + +namespace Volo.Abp.Identity.Organizations +{ + /// + /// Performs domain logic for Organization Units. + /// + public class OrganizationUnitManager : DomainService + { + protected IOrganizationUnitRepository _organizationUnitRepository { get; private set; } + + private readonly IStringLocalizer _localizer; + + public OrganizationUnitManager( + IOrganizationUnitRepository organizationUnitRepository, + IStringLocalizer localizer) + { + _organizationUnitRepository = organizationUnitRepository; + _localizer = localizer; + } + + [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) + { + var parentCode = parentId != null ? await GetCodeOrDefaultAsync(parentId.Value) : null; + return OrganizationUnit.AppendCode(parentCode, OrganizationUnit.CreateCode(1)); + } + + return OrganizationUnit.CalculateNextCode(lastChild.Code); + } + + 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(); + } + + var code = await GetCodeOrDefaultAsync(parentId.Value); + + return await _organizationUnitRepository.GetAllChildrenWithParentCodeAsync(code, parentId); + } + } +} diff --git a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/Organizations/OrganizationUnitRole.cs b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/Organizations/OrganizationUnitRole.cs new file mode 100644 index 0000000000..cd9d9d3ac4 --- /dev/null +++ b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/Organizations/OrganizationUnitRole.cs @@ -0,0 +1,54 @@ +using System; +using Volo.Abp.Domain.Entities.Auditing; +using Volo.Abp.MultiTenancy; + +namespace Volo.Abp.Identity.Organizations +{ + /// + /// Represents membership of a User to an OU. + /// + public class OrganizationUnitRole : CreationAuditedEntity, IMultiTenant, ISoftDelete + { + /// + /// TenantId of this entity. + /// + public virtual Guid? TenantId { get; protected set; } + + /// + /// Id of the Role. + /// + public virtual Guid RoleId { get; set; } + + /// + /// Id of the . + /// + public virtual Guid OrganizationUnitId { get; set; } + + /// + /// Specifies if the organization is soft deleted or not. + /// + public virtual bool IsDeleted { get; set; } + + /// + /// Initializes a new instance of the class. + /// + public OrganizationUnitRole() + { + + } + + /// + /// Initializes a new instance of the class. + /// + /// TenantId + /// Id of the User. + /// Id of the . + public OrganizationUnitRole(Guid? tenantId, Guid roleId, Guid organizationUnitId) + { + TenantId = tenantId; + RoleId = roleId; + OrganizationUnitId = organizationUnitId; + } + + } +} diff --git a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/Organizations/OrganizationUnitUser.cs b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/Organizations/OrganizationUnitUser.cs new file mode 100644 index 0000000000..3ab7a1c711 --- /dev/null +++ b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/Organizations/OrganizationUnitUser.cs @@ -0,0 +1,54 @@ +using System; +using Volo.Abp.Domain.Entities.Auditing; +using Volo.Abp.MultiTenancy; + +namespace Volo.Abp.Identity.Organizations +{ + /// + /// Represents membership of a User to an OU. + /// + public class OrganizationUnitUser : CreationAuditedEntity, IMultiTenant, ISoftDelete + { + + /// + /// TenantId of this entity. + /// + public virtual Guid? TenantId { get; set; } + + /// + /// Id of the User. + /// + public virtual Guid UserId { get; set; } + + /// + /// Id of the . + /// + public virtual Guid OrganizationUnitId { get; set; } + + /// + /// Specifies if the organization is soft deleted or not. + /// + public virtual bool IsDeleted { get; set; } + + /// + /// Initializes a new instance of the class. + /// + public OrganizationUnitUser() + { + + } + + /// + /// Initializes a new instance of the class. + /// + /// TenantId + /// Id of the User. + /// Id of the . + public OrganizationUnitUser(Guid? tenantId, Guid userId, Guid organizationUnitId) + { + TenantId = tenantId; + UserId = userId; + OrganizationUnitId = organizationUnitId; + } + } +} 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 945905d75f..994a3a3985 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 @@ -14,8 +14,8 @@ - + 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..f24adad3f7 --- /dev/null +++ b/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/EfCoreOrganizationUnitRepository.cs @@ -0,0 +1,32 @@ +using Microsoft.EntityFrameworkCore; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Volo.Abp.Domain.Repositories.EntityFrameworkCore; +using Volo.Abp.EntityFrameworkCore; +using Volo.Abp.Identity.Organizations; + +namespace Volo.Abp.Identity.EntityFrameworkCore +{ + public class EfCoreOrganizationUnitRepository : EfCoreRepository, IOrganizationUnitRepository + { + public EfCoreOrganizationUnitRepository(IDbContextProvider dbContextProvider) + : base(dbContextProvider) + { + } + + public async Task> GetChildrenAsync(Guid? parentId) + { + return await DbSet.Where(x => x.ParentId == parentId) + .ToListAsync(); + } + + public async Task> GetAllChildrenWithParentCodeAsync(string code, Guid? parentId) + { + return await DbSet.Where(ou => ou.Code.StartsWith(code) && ou.Id != parentId.Value) + .ToListAsync(); + } + } +} 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..b5d7007767 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 @@ -1,6 +1,7 @@ using Microsoft.EntityFrameworkCore; using Volo.Abp.Data; using Volo.Abp.EntityFrameworkCore; +using Volo.Abp.Identity.Organizations; namespace Volo.Abp.Identity.EntityFrameworkCore { @@ -12,5 +13,11 @@ namespace Volo.Abp.Identity.EntityFrameworkCore DbSet Roles { get; set; } DbSet ClaimTypes { get; set; } + + DbSet OrganizationUnits { get; set; } + + DbSet OrganizationUnitRoles { get; set; } + + DbSet OrganizationUnitUsers { 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..576a5d2347 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 @@ -1,6 +1,7 @@ using Microsoft.EntityFrameworkCore; using Volo.Abp.Data; using Volo.Abp.EntityFrameworkCore; +using Volo.Abp.Identity.Organizations; namespace Volo.Abp.Identity.EntityFrameworkCore { @@ -16,6 +17,12 @@ namespace Volo.Abp.Identity.EntityFrameworkCore public DbSet ClaimTypes { get; set; } + public DbSet OrganizationUnits { get; set; } + + public DbSet OrganizationUnitRoles { get; set; } + + public DbSet OrganizationUnitUsers { get; set; } + public IdentityDbContext(DbContextOptions options) : base(options) {