From 580ffbecab4286147d773b8b6377b9c8b3eeaad4 Mon Sep 17 00:00:00 2001 From: colin Date: Fri, 7 Aug 2026 11:00:21 +0800 Subject: [PATCH] feat: Optimize Tenant Selection Component --- aspnet-core/LINGYUN.MicroService.All.slnx | 1 + .../LINGYUN.MicroService.SingleProject.slnx | 1 + .../AuthServerModule.cs | 2 +- .../Abp/MultiTenancy/TenantSwitchModal.cshtml | 36 +++++ .../MultiTenancy/TenantSwitchModal.cshtml.cs | 118 +++++++++++++++ .../Abp/MultiTenancy/_ViewImports.cshtml | 3 + .../Pages/Account/IdentityServerLoginModel.cs | 6 +- .../Pages/Account/LinkLogged.cshtml | 2 +- .../Abp/Identity/Dto/IdentitySessionDto.cs | 2 +- .../Abp/Identity/AspNetCore/TotpService.cs | 137 ------------------ ...rviceApplicationsSingleModule.Configure.cs | 3 - .../TenantHeaderParamter.cs | 4 +- .../Dockerfile | 1 + ...LY.MicroService.Applications.Single.csproj | 1 + .../MicroServiceApplicationsSingleModule.cs | 5 + .../Abp/MultiTenancy/TenantSwitchModal.cshtml | 36 +++++ .../MultiTenancy/TenantSwitchModal.cshtml.cs | 112 ++++++++++++++ .../Abp/MultiTenancy/_ViewImports.cshtml | 3 + .../AuthServerModule.Configure.cs | 8 + .../AuthServerModule.cs | 3 + .../LY.MicroService.AuthServer.csproj | 1 + .../Abp/MultiTenancy/TenantSwitchModal.cshtml | 36 +++++ .../MultiTenancy/TenantSwitchModal.cshtml.cs | 118 +++++++++++++++ .../Abp/MultiTenancy/_ViewImports.cshtml | 3 + 24 files changed, 494 insertions(+), 148 deletions(-) create mode 100644 aspnet-core/aspire/LINGYUN.Abp.MicroService.AuthServer/Pages/Abp/MultiTenancy/TenantSwitchModal.cshtml create mode 100644 aspnet-core/aspire/LINGYUN.Abp.MicroService.AuthServer/Pages/Abp/MultiTenancy/TenantSwitchModal.cshtml.cs create mode 100644 aspnet-core/aspire/LINGYUN.Abp.MicroService.AuthServer/Pages/Abp/MultiTenancy/_ViewImports.cshtml delete mode 100644 aspnet-core/modules/identity/LINGYUN.Abp.Identity.AspNetCore/LINGYUN/Abp/Identity/AspNetCore/TotpService.cs create mode 100644 aspnet-core/services/LY.MicroService.Applications.Single/Pages/Abp/MultiTenancy/TenantSwitchModal.cshtml create mode 100644 aspnet-core/services/LY.MicroService.Applications.Single/Pages/Abp/MultiTenancy/TenantSwitchModal.cshtml.cs create mode 100644 aspnet-core/services/LY.MicroService.Applications.Single/Pages/Abp/MultiTenancy/_ViewImports.cshtml create mode 100644 aspnet-core/services/LY.MicroService.AuthServer/Pages/Abp/MultiTenancy/TenantSwitchModal.cshtml create mode 100644 aspnet-core/services/LY.MicroService.AuthServer/Pages/Abp/MultiTenancy/TenantSwitchModal.cshtml.cs create mode 100644 aspnet-core/services/LY.MicroService.AuthServer/Pages/Abp/MultiTenancy/_ViewImports.cshtml diff --git a/aspnet-core/LINGYUN.MicroService.All.slnx b/aspnet-core/LINGYUN.MicroService.All.slnx index d70ae4710..60c1adda7 100644 --- a/aspnet-core/LINGYUN.MicroService.All.slnx +++ b/aspnet-core/LINGYUN.MicroService.All.slnx @@ -396,6 +396,7 @@ + diff --git a/aspnet-core/LINGYUN.MicroService.SingleProject.slnx b/aspnet-core/LINGYUN.MicroService.SingleProject.slnx index 2c995b9b7..ee04e035f 100644 --- a/aspnet-core/LINGYUN.MicroService.SingleProject.slnx +++ b/aspnet-core/LINGYUN.MicroService.SingleProject.slnx @@ -274,6 +274,7 @@ + diff --git a/aspnet-core/aspire/LINGYUN.Abp.MicroService.AuthServer/AuthServerModule.cs b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AuthServer/AuthServerModule.cs index 98eb4313f..f91aea66c 100644 --- a/aspnet-core/aspire/LINGYUN.Abp.MicroService.AuthServer/AuthServerModule.cs +++ b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AuthServer/AuthServerModule.cs @@ -62,9 +62,9 @@ namespace LINGYUN.Abp.MicroService.AuthServer; typeof(AbpOpenIddictSmsModule), typeof(AbpOpenIddictWeChatModule), typeof(AbpOpenIddictLinkUserModule), - typeof(AbpOpenIddictImpersonationModule), typeof(AbpOpenIddictPortalModule), typeof(AbpOpenIddictWeChatWorkModule), + typeof(AbpOpenIddictImpersonationModule), typeof(AbpIdentityOrganizaztionUnitsModule), typeof(AbpPermissionManagementDomainIdentityModule), typeof(AuthServerMigrationsEntityFrameworkCoreModule), diff --git a/aspnet-core/aspire/LINGYUN.Abp.MicroService.AuthServer/Pages/Abp/MultiTenancy/TenantSwitchModal.cshtml b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AuthServer/Pages/Abp/MultiTenancy/TenantSwitchModal.cshtml new file mode 100644 index 000000000..b749b83d7 --- /dev/null +++ b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AuthServer/Pages/Abp/MultiTenancy/TenantSwitchModal.cshtml @@ -0,0 +1,36 @@ +@page +@using Microsoft.AspNetCore.Mvc.Localization +@using LINGYUN.Abp.MicroService.AuthServer.Pages.Abp.MultiTenancy +@using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal +@using Volo.Abp.AspNetCore.Mvc.UI.MultiTenancy.Localization +@using static LINGYUN.Abp.MicroService.AuthServer.Pages.Abp.MultiTenancy.TenantSwitchModalModel +@model TenantSwitchModalModel +@inject IHtmlLocalizer L +@{ + Layout = null; +} + + + +
+ @if (Model.Input is SelectTenantInfoModel selectModel && selectModel.AvailableTenants.Count > 0) + { + + } + else + { + + } + +
+ +
+ + \ No newline at end of file diff --git a/aspnet-core/aspire/LINGYUN.Abp.MicroService.AuthServer/Pages/Abp/MultiTenancy/TenantSwitchModal.cshtml.cs b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AuthServer/Pages/Abp/MultiTenancy/TenantSwitchModal.cshtml.cs new file mode 100644 index 000000000..83b6b574f --- /dev/null +++ b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AuthServer/Pages/Abp/MultiTenancy/TenantSwitchModal.cshtml.cs @@ -0,0 +1,118 @@ +using LINGYUN.Platform.Portal; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.Extensions.Options; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Volo.Abp; +using Volo.Abp.AspNetCore.MultiTenancy; +using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form; +using Volo.Abp.AspNetCore.Mvc.UI.MultiTenancy.Localization; +using Volo.Abp.AspNetCore.Mvc.UI.RazorPages; +using Volo.Abp.MultiTenancy; + +#nullable enable +namespace LINGYUN.Abp.MicroService.AuthServer.Pages.Abp.MultiTenancy; + +public class TenantSwitchModalModel : AbpPageModel +{ + [BindProperty] + public TenantInfoModel Input { get; set; } = default!; + + public List AvailableTenants { get; set; } = new(); + + protected ITenantStore TenantStore { get; } + protected ITenantNormalizer TenantNormalizer { get; } + protected AbpAspNetCoreMultiTenancyOptions Options { get; } + protected IEnterpriseRepository EnterpriseRepository { get; } + public TenantSwitchModalModel( + ITenantStore tenantStore, + ITenantNormalizer tenantNormalizer, + IOptions options, + IEnterpriseRepository enterpriseRepository) + { + TenantStore = tenantStore; + TenantNormalizer = tenantNormalizer; + Options = options.Value; + EnterpriseRepository = enterpriseRepository; + + LocalizationResourceType = typeof(AbpUiMultiTenancyResource); + } + + public async virtual Task OnGetAsync() + { + await LoadAvailableTenants(); + if (AvailableTenants.Count > 0) + { + Input = new SelectTenantInfoModel + { + AvailableTenants = AvailableTenants + }; + } + else + { + Input = new TenantInfoModel(); + } + + if (CurrentTenant.IsAvailable) + { + var tenant = await TenantStore.FindAsync(CurrentTenant.GetId()); + Input.Name = tenant?.Name; + } + } + + public virtual async Task OnPostAsync() + { + Guid? tenantId = null; + if (!Input.Name.IsNullOrEmpty()) + { + var tenant = await TenantStore.FindAsync(TenantNormalizer.NormalizeName(Input.Name!)!); + if (tenant == null && Guid.TryParse(Input.Name, out var id)) + { + tenant = await TenantStore.FindAsync(id); + } + if (tenant == null) + { + throw new UserFriendlyException(L["GivenTenantIsNotExist", Input.Name!]); + } + + if (!tenant.IsActive) + { + throw new UserFriendlyException(L["GivenTenantIsNotAvailable", Input.Name!]); + } + + tenantId = tenant.Id; + } + + AbpMultiTenancyCookieHelper.SetTenantCookie(HttpContext, tenantId, Options.TenantKey); + } + private async Task LoadAvailableTenants() + { + var enterprises = await EnterpriseRepository.GetEnterprisesInTenantListAsync(maxResultCount: 25); + if (enterprises.Count > 0) + { + AvailableTenants = enterprises + .Select(enterprise => + new SelectListItem( + enterprise.Name, enterprise.TenantId?.ToString())) + .Union([new SelectListItem("Default", "")]) + .ToList(); + } + } + + public class TenantInfoModel + { + public virtual string? Name { get; set; } + } + + public class SelectTenantInfoModel : TenantInfoModel + { + public List AvailableTenants { get; set; } = new(); + + [SelectItems(nameof(AvailableTenants))] + public override string? Name { get; set; } + } +} +#nullable disable diff --git a/aspnet-core/aspire/LINGYUN.Abp.MicroService.AuthServer/Pages/Abp/MultiTenancy/_ViewImports.cshtml b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AuthServer/Pages/Abp/MultiTenancy/_ViewImports.cshtml new file mode 100644 index 000000000..225780c2c --- /dev/null +++ b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AuthServer/Pages/Abp/MultiTenancy/_ViewImports.cshtml @@ -0,0 +1,3 @@ +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@addTagHelper *, Volo.Abp.AspNetCore.Mvc.UI.Bootstrap +@addTagHelper *, Volo.Abp.AspNetCore.Mvc.UI.Bundling \ No newline at end of file diff --git a/aspnet-core/modules/account/LINGYUN.Abp.Account.Web.IdentityServer/Pages/Account/IdentityServerLoginModel.cs b/aspnet-core/modules/account/LINGYUN.Abp.Account.Web.IdentityServer/Pages/Account/IdentityServerLoginModel.cs index 90d83c431..7beb47dde 100644 --- a/aspnet-core/modules/account/LINGYUN.Abp.Account.Web.IdentityServer/Pages/Account/IdentityServerLoginModel.cs +++ b/aspnet-core/modules/account/LINGYUN.Abp.Account.Web.IdentityServer/Pages/Account/IdentityServerLoginModel.cs @@ -182,7 +182,7 @@ namespace LINGYUN.Abp.Account.Web.IdentityServer.Pages.Account // Clear the dynamic claims cache. await IdentityDynamicClaimsPrincipalContributorCache.ClearAsync(user.Id, user.TenantId); - return await RedirectSafelyAsync(PasswordLoginInput.ReturnUrl, PasswordLoginInput.ReturnUrlHash); + return await RedirectSafelyAsync(ReturnUrl, ReturnUrlHash); } public override async Task OnPostPhoneNumberLogin(string action) @@ -237,7 +237,7 @@ namespace LINGYUN.Abp.Account.Web.IdentityServer.Pages.Account // Clear the dynamic claims cache. await IdentityDynamicClaimsPrincipalContributorCache.ClearAsync(user.Id, user.TenantId); - return await RedirectSafelyAsync(PhoneLoginInput.ReturnUrl, PhoneLoginInput.ReturnUrlHash); + return await RedirectSafelyAsync(ReturnUrl, ReturnUrlHash); } public override async Task OnPostQrCodeLogin(string action) @@ -305,7 +305,7 @@ namespace LINGYUN.Abp.Account.Web.IdentityServer.Pages.Account // Clear the dynamic claims cache. await IdentityDynamicClaimsPrincipalContributorCache.ClearAsync(user.Id, user.TenantId); - return await RedirectSafelyAsync(QrCodeLoginInput.ReturnUrl, QrCodeLoginInput.ReturnUrlHash); + return await RedirectSafelyAsync(ReturnUrl, ReturnUrlHash); } } diff --git a/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Pages/Account/LinkLogged.cshtml b/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Pages/Account/LinkLogged.cshtml index 943d86377..4be781478 100644 --- a/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Pages/Account/LinkLogged.cshtml +++ b/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Pages/Account/LinkLogged.cshtml @@ -20,7 +20,7 @@
- @L["LogInUsingYourProviderAccount", Model.LinkTenantAndUserName] → + @L["LogInUsingYourProviderAccount", @Model.LinkTenantAndUserName] →
diff --git a/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Application.Contracts/LINGYUN/Abp/Identity/Dto/IdentitySessionDto.cs b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Application.Contracts/LINGYUN/Abp/Identity/Dto/IdentitySessionDto.cs index 77f8b7d63..da79e346a 100644 --- a/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Application.Contracts/LINGYUN/Abp/Identity/Dto/IdentitySessionDto.cs +++ b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Application.Contracts/LINGYUN/Abp/Identity/Dto/IdentitySessionDto.cs @@ -2,7 +2,7 @@ using Volo.Abp.Application.Dtos; namespace LINGYUN.Abp.Identity; -public class IdentitySessionDto : EntityDto +public class IdentitySessionDto : ExtensibleEntityDto { public string SessionId { get; set; } diff --git a/aspnet-core/modules/identity/LINGYUN.Abp.Identity.AspNetCore/LINGYUN/Abp/Identity/AspNetCore/TotpService.cs b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.AspNetCore/LINGYUN/Abp/Identity/AspNetCore/TotpService.cs deleted file mode 100644 index 6c6b38c89..000000000 --- a/aspnet-core/modules/identity/LINGYUN.Abp.Identity.AspNetCore/LINGYUN/Abp/Identity/AspNetCore/TotpService.cs +++ /dev/null @@ -1,137 +0,0 @@ -using System; -using System.Diagnostics; -using System.Net; -using System.Security.Cryptography; -using System.Text; - -namespace LINGYUN.Abp.Identity.AspNetCore; - -/// -/// 参考 -/// Microsoft: https://github.com/dotnet/dotnet/blob/main/src/aspnetcore/src/Identity/Extensions.Core/src/Rfc6238AuthenticationService.cs -/// -#nullable enable -internal class TotpService -{ - private static readonly TimeSpan _timestep = TimeSpan.FromMinutes(3); - private static readonly Encoding _encoding = new UTF8Encoding(false, true); -#if NETSTANDARD2_0 || NETFRAMEWORK - private static readonly DateTime _unixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); -#endif - - internal static int ComputeTotp( -#if NET6_0_OR_GREATER - byte[] key, -#else - HashAlgorithm hashAlgorithm, -#endif - ulong timestepNumber, - byte[]? modifierBytes) - { - // # of 0's = length of pin - const int Mod = 1000000; - - // See https://tools.ietf.org/html/rfc4226 - // We can add an optional modifier -#if NET6_0_OR_GREATER - Span timestepAsBytes = stackalloc byte[sizeof(long)]; - var res = BitConverter.TryWriteBytes(timestepAsBytes, IPAddress.HostToNetworkOrder((long)timestepNumber)); - Debug.Assert(res); -#else - var timestepAsBytes = BitConverter.GetBytes(IPAddress.HostToNetworkOrder((long)timestepNumber)); -#endif - -#if NET6_0_OR_GREATER - Span modifierCombinedBytes = timestepAsBytes; - if (modifierBytes is not null) - { - modifierCombinedBytes = ApplyModifier(timestepAsBytes, modifierBytes); - } - Span hash = stackalloc byte[HMACSHA1.HashSizeInBytes]; - res = HMACSHA1.TryHashData(key, modifierCombinedBytes, hash, out var written); - Debug.Assert(res); - Debug.Assert(written == hash.Length); -#else - var hash = hashAlgorithm.ComputeHash(ApplyModifier(timestepAsBytes, modifierBytes)); -#endif - - // Generate DT string - var offset = hash[hash.Length - 1] & 0xf; - Debug.Assert(offset + 4 < hash.Length); - var binaryCode = (hash[offset] & 0x7f) << 24 - | (hash[offset + 1] & 0xff) << 16 - | (hash[offset + 2] & 0xff) << 8 - | (hash[offset + 3] & 0xff); - - return binaryCode % Mod; - } - - private static byte[] ApplyModifier(Span input, byte[] modifierBytes) - { - var combined = new byte[checked(input.Length + modifierBytes.Length)]; - input.CopyTo(combined); - Buffer.BlockCopy(modifierBytes, 0, combined, input.Length, modifierBytes.Length); - return combined; - } - - // More info: https://tools.ietf.org/html/rfc6238#section-4 - private static ulong GetCurrentTimeStepNumber() - { -#if NETSTANDARD2_0 || NETFRAMEWORK - var delta = DateTime.UtcNow - _unixEpoch; -#else - var delta = DateTimeOffset.UtcNow - DateTimeOffset.UnixEpoch; -#endif - return (ulong)(delta.Ticks / _timestep.Ticks); - } - - public static int GenerateCode(byte[] securityToken, string? modifier = null) - { - ArgumentNullException.ThrowIfNull(securityToken); - - // Allow a variance of no greater than 9 minutes in either direction - var currentTimeStep = GetCurrentTimeStepNumber(); - - var modifierBytes = modifier is not null ? _encoding.GetBytes(modifier) : null; -#if NET6_0_OR_GREATER - return ComputeTotp(securityToken, currentTimeStep, modifierBytes); -#else - using (var hashAlgorithm = new HMACSHA1(securityToken)) - { - return ComputeTotp(hashAlgorithm, currentTimeStep, modifierBytes); - } -#endif - } - - public static bool ValidateCode(byte[] securityToken, int code, string? modifier = null) - { - ArgumentNullException.ThrowIfNull(securityToken); - - // Allow a variance of no greater than 9 minutes in either direction - var currentTimeStep = GetCurrentTimeStepNumber(); - -#if !NET6_0_OR_GREATER - using (var hashAlgorithm = new HMACSHA1(securityToken)) -#endif - { - var modifierBytes = modifier is not null ? _encoding.GetBytes(modifier) : null; - for (var i = -2; i <= 2; i++) - { -#if NET6_0_OR_GREATER - var computedTotp = ComputeTotp(securityToken, (ulong)((long)currentTimeStep + i), modifierBytes); -#else - var computedTotp = ComputeTotp(hashAlgorithm, (ulong)((long)currentTimeStep + i), modifierBytes); -#endif - if (computedTotp == code) - { - return true; - } - } - } - - // No match - return false; - } -} - -#nullable disable diff --git a/aspnet-core/services/LY.AIO.Applications.Single/MicroServiceApplicationsSingleModule.Configure.cs b/aspnet-core/services/LY.AIO.Applications.Single/MicroServiceApplicationsSingleModule.Configure.cs index 0e15f1592..9ec4e4262 100644 --- a/aspnet-core/services/LY.AIO.Applications.Single/MicroServiceApplicationsSingleModule.Configure.cs +++ b/aspnet-core/services/LY.AIO.Applications.Single/MicroServiceApplicationsSingleModule.Configure.cs @@ -2,7 +2,6 @@ using Elsa.Options; using LINGYUN.Abp.Aliyun.Localization; using LINGYUN.Abp.BackgroundTasks; -using LINGYUN.Abp.DataProtectionManagement; using LINGYUN.Abp.ExceptionHandling; using LINGYUN.Abp.ExceptionHandling.Emailing; using LINGYUN.Abp.Exporter.MiniExcel; @@ -43,8 +42,6 @@ using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.Extensions.Caching.StackExchangeRedis; using Microsoft.IdentityModel.Logging; using Microsoft.OpenApi; -using Microsoft.OpenApi.Models; -using MiniExcelLibs.Attributes; using OpenIddict.Server; using OpenIddict.Server.AspNetCore; using Quartz; diff --git a/aspnet-core/services/LY.AIO.Applications.Single/TenantHeaderParamter.cs b/aspnet-core/services/LY.AIO.Applications.Single/TenantHeaderParamter.cs index 9d80a45fd..1b66dd7c6 100644 --- a/aspnet-core/services/LY.AIO.Applications.Single/TenantHeaderParamter.cs +++ b/aspnet-core/services/LY.AIO.Applications.Single/TenantHeaderParamter.cs @@ -1,5 +1,5 @@ using Microsoft.Extensions.Options; -using Microsoft.OpenApi.Models; +using Microsoft.OpenApi; using Swashbuckle.AspNetCore.SwaggerGen; using Volo.Abp.AspNetCore.MultiTenancy; using Volo.Abp.MultiTenancy; @@ -22,7 +22,7 @@ public class TenantHeaderParamter : IOperationFilter { if (_multiTenancyOptions.IsEnabled) { - operation.Parameters = operation.Parameters ?? new List(); + operation.Parameters ??= new List(); operation.Parameters.Add(new OpenApiParameter { Name = _aspNetCoreMultiTenancyOptions.TenantKey, diff --git a/aspnet-core/services/LY.MicroService.Applications.Single/Dockerfile b/aspnet-core/services/LY.MicroService.Applications.Single/Dockerfile index aecb0c1af..886b6e334 100644 --- a/aspnet-core/services/LY.MicroService.Applications.Single/Dockerfile +++ b/aspnet-core/services/LY.MicroService.Applications.Single/Dockerfile @@ -10,6 +10,7 @@ RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo '$TZ' > /etc/timezone EXPOSE 80/tcp VOLUME [ "./app/blobs" ] +VOLUME [ "./app/certs" ] VOLUME [ "./app/Logs" ] VOLUME [ "./app/Modules" ] diff --git a/aspnet-core/services/LY.MicroService.Applications.Single/LY.MicroService.Applications.Single.csproj b/aspnet-core/services/LY.MicroService.Applications.Single/LY.MicroService.Applications.Single.csproj index fb0ab2aec..247c750a9 100644 --- a/aspnet-core/services/LY.MicroService.Applications.Single/LY.MicroService.Applications.Single.csproj +++ b/aspnet-core/services/LY.MicroService.Applications.Single/LY.MicroService.Applications.Single.csproj @@ -188,6 +188,7 @@ + diff --git a/aspnet-core/services/LY.MicroService.Applications.Single/MicroServiceApplicationsSingleModule.cs b/aspnet-core/services/LY.MicroService.Applications.Single/MicroServiceApplicationsSingleModule.cs index 318442de3..7325f337f 100644 --- a/aspnet-core/services/LY.MicroService.Applications.Single/MicroServiceApplicationsSingleModule.cs +++ b/aspnet-core/services/LY.MicroService.Applications.Single/MicroServiceApplicationsSingleModule.cs @@ -1,6 +1,8 @@ using LINGYUN.Abp.BlobManagement.MimeCheck; +using LINGYUN.Abp.Identity.AspNetCore; using LINGYUN.Abp.Identity.Jobs; using LINGYUN.Abp.Notifications.Calendar; +using Volo.Abp.AspNetCore.Mvc.UI.MultiTenancy; namespace LY.MicroService.Applications.Single; @@ -13,6 +15,8 @@ namespace LY.MicroService.Applications.Single; typeof(AbpSerilogEnrichersUniqueIdModule), // Serilog模块 typeof(AbpAspNetCoreSerilogModule), + // 身份认证模块 扩展UserToken验证 + typeof(AbpIdentityAspNetCoreModule), // 身份认证模块 会话管理集成 typeof(AbpIdentityAspNetCoreSessionModule), // 身份认证模块 会话中间件 @@ -398,6 +402,7 @@ namespace LY.MicroService.Applications.Single; typeof(AbpAspNetCoreMvcIdempotentWrapperModule), typeof(AbpAspNetCoreHttpOverridesModule), typeof(AbpHttpClientIdentityModelWebModule), + typeof(AbpAspNetCoreMvcUiMultiTenancyModule), typeof(AbpSwashbuckleModule), typeof(AbpMailKitModule), typeof(AbpAutofacModule), diff --git a/aspnet-core/services/LY.MicroService.Applications.Single/Pages/Abp/MultiTenancy/TenantSwitchModal.cshtml b/aspnet-core/services/LY.MicroService.Applications.Single/Pages/Abp/MultiTenancy/TenantSwitchModal.cshtml new file mode 100644 index 000000000..1555af5b6 --- /dev/null +++ b/aspnet-core/services/LY.MicroService.Applications.Single/Pages/Abp/MultiTenancy/TenantSwitchModal.cshtml @@ -0,0 +1,36 @@ +@page +@using Microsoft.AspNetCore.Mvc.Localization +@using LY.MicroService.Applications.Single.Pages.Abp.MultiTenancy +@using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal +@using Volo.Abp.AspNetCore.Mvc.UI.MultiTenancy.Localization +@using static LY.MicroService.Applications.Single.Pages.Abp.MultiTenancy.TenantSwitchModalModel +@model TenantSwitchModalModel +@inject IHtmlLocalizer L +@{ + Layout = null; +} + + + +
+ @if (Model.Input is SelectTenantInfoModel selectModel && selectModel.AvailableTenants.Count > 0) + { + + } + else + { + + } + +
+ +
+ + \ No newline at end of file diff --git a/aspnet-core/services/LY.MicroService.Applications.Single/Pages/Abp/MultiTenancy/TenantSwitchModal.cshtml.cs b/aspnet-core/services/LY.MicroService.Applications.Single/Pages/Abp/MultiTenancy/TenantSwitchModal.cshtml.cs new file mode 100644 index 000000000..a4a9302c5 --- /dev/null +++ b/aspnet-core/services/LY.MicroService.Applications.Single/Pages/Abp/MultiTenancy/TenantSwitchModal.cshtml.cs @@ -0,0 +1,112 @@ +using LINGYUN.Platform.Portal; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.Extensions.Options; +using Volo.Abp.AspNetCore.MultiTenancy; +using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form; +using Volo.Abp.AspNetCore.Mvc.UI.MultiTenancy.Localization; +using Volo.Abp.AspNetCore.Mvc.UI.RazorPages; + +#nullable enable +namespace LY.MicroService.Applications.Single.Pages.Abp.MultiTenancy; + +public class TenantSwitchModalModel : AbpPageModel +{ + [BindProperty] + public TenantInfoModel Input { get; set; } = default!; + + public List AvailableTenants { get; set; } = new(); + + protected ITenantStore TenantStore { get; } + protected ITenantNormalizer TenantNormalizer { get; } + protected AbpAspNetCoreMultiTenancyOptions Options { get; } + protected IEnterpriseRepository EnterpriseRepository { get; } + public TenantSwitchModalModel( + ITenantStore tenantStore, + ITenantNormalizer tenantNormalizer, + IOptions options, + IEnterpriseRepository enterpriseRepository) + { + TenantStore = tenantStore; + TenantNormalizer = tenantNormalizer; + Options = options.Value; + EnterpriseRepository = enterpriseRepository; + + LocalizationResourceType = typeof(AbpUiMultiTenancyResource); + } + + public async virtual Task OnGetAsync() + { + await LoadAvailableTenants(); + if (AvailableTenants.Count > 0) + { + Input = new SelectTenantInfoModel + { + AvailableTenants = AvailableTenants + }; + } + else + { + Input = new TenantInfoModel(); + } + + if (CurrentTenant.IsAvailable) + { + var tenant = await TenantStore.FindAsync(CurrentTenant.GetId()); + Input.Name = tenant?.Name; + } + } + + public virtual async Task OnPostAsync() + { + Guid? tenantId = null; + if (!Input.Name.IsNullOrEmpty()) + { + var tenant = await TenantStore.FindAsync(TenantNormalizer.NormalizeName(Input.Name!)!); + if (tenant == null && Guid.TryParse(Input.Name, out var id)) + { + tenant = await TenantStore.FindAsync(id); + } + if (tenant == null) + { + throw new UserFriendlyException(L["GivenTenantIsNotExist", Input.Name!]); + } + + if (!tenant.IsActive) + { + throw new UserFriendlyException(L["GivenTenantIsNotAvailable", Input.Name!]); + } + + tenantId = tenant.Id; + } + + AbpMultiTenancyCookieHelper.SetTenantCookie(HttpContext, tenantId, Options.TenantKey); + } + private async Task LoadAvailableTenants() + { + var enterprises = await EnterpriseRepository.GetEnterprisesInTenantListAsync(maxResultCount: 25); + if (enterprises.Count > 0) + { + AvailableTenants = enterprises + .Select(enterprise => + new SelectListItem( + enterprise.Name, enterprise.TenantId?.ToString())) + .Union([new SelectListItem("Default", "")]) + .ToList(); + } + } + + public class TenantInfoModel + { + public virtual string? Name { get; set; } + } + + public class SelectTenantInfoModel : TenantInfoModel + { + public List AvailableTenants { get; set; } = new(); + + [SelectItems(nameof(AvailableTenants))] + public override string? Name { get; set; } + } +} +#nullable disable diff --git a/aspnet-core/services/LY.MicroService.Applications.Single/Pages/Abp/MultiTenancy/_ViewImports.cshtml b/aspnet-core/services/LY.MicroService.Applications.Single/Pages/Abp/MultiTenancy/_ViewImports.cshtml new file mode 100644 index 000000000..225780c2c --- /dev/null +++ b/aspnet-core/services/LY.MicroService.Applications.Single/Pages/Abp/MultiTenancy/_ViewImports.cshtml @@ -0,0 +1,3 @@ +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@addTagHelper *, Volo.Abp.AspNetCore.Mvc.UI.Bootstrap +@addTagHelper *, Volo.Abp.AspNetCore.Mvc.UI.Bundling \ No newline at end of file diff --git a/aspnet-core/services/LY.MicroService.AuthServer/AuthServerModule.Configure.cs b/aspnet-core/services/LY.MicroService.AuthServer/AuthServerModule.Configure.cs index 70c4577c3..7e406a7f9 100644 --- a/aspnet-core/services/LY.MicroService.AuthServer/AuthServerModule.Configure.cs +++ b/aspnet-core/services/LY.MicroService.AuthServer/AuthServerModule.Configure.cs @@ -1,8 +1,10 @@ using DotNetCore.CAP; using LINGYUN.Abp.AspNetCore.MultiTenancy; +using LINGYUN.Abp.Identity; using LINGYUN.Abp.Localization.CultureMap; using LINGYUN.Abp.LocalizationManagement; using LINGYUN.Abp.OpenIddict.AspNetCore.Session; +using LINGYUN.Abp.OpenIddict.Impersonation; using LINGYUN.Abp.OpenIddict.LinkUser; using LINGYUN.Abp.OpenIddict.Portal; using LINGYUN.Abp.OpenIddict.Sms; @@ -399,6 +401,12 @@ public partial class AuthServerModule options.RefreshTokenReuseLeeway = lifetime.GetValue("RefreshTokenReuseLeeway", options.RefreshTokenReuseLeeway); options.UserCodeLifetime = lifetime.GetValue("UserCode", options.UserCodeLifetime); }); + + Configure(options => + { + options.ImpersonationPermission = IdentityPermissions.Users.Impersonation; + options.ImpersonationTenantPermission = "AbpSaas.Tenants.Impersonation"; + }); } private void ConfigureSecurity(IServiceCollection services, IConfiguration configuration, bool isDevelopment = false) { diff --git a/aspnet-core/services/LY.MicroService.AuthServer/AuthServerModule.cs b/aspnet-core/services/LY.MicroService.AuthServer/AuthServerModule.cs index 86093fdb6..b0eb45961 100644 --- a/aspnet-core/services/LY.MicroService.AuthServer/AuthServerModule.cs +++ b/aspnet-core/services/LY.MicroService.AuthServer/AuthServerModule.cs @@ -17,6 +17,7 @@ using LINGYUN.Abp.Identity.OrganizaztionUnits; using LINGYUN.Abp.Identity.Session.AspNetCore; using LINGYUN.Abp.Localization.CultureMap; using LINGYUN.Abp.OpenIddict.AspNetCore.Session; +using LINGYUN.Abp.OpenIddict.Impersonation; using LINGYUN.Abp.OpenIddict.LinkUser; using LINGYUN.Abp.OpenIddict.Portal; using LINGYUN.Abp.OpenIddict.Sms; @@ -27,6 +28,7 @@ using LINGYUN.Abp.Serilog.Enrichers.UniqueId; using LINGYUN.Abp.Sms.Platform; using LINGYUN.Abp.Telemetry.OpenTelemetry; using LINGYUN.Abp.Telemetry.SkyWalking; +using LINGYUN.Platform.EntityFrameworkCore; using LY.MicroService.AuthServer.EntityFrameworkCore; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; @@ -66,6 +68,7 @@ namespace LY.MicroService.AuthServer; typeof(AbpOpenIddictLinkUserModule), typeof(AbpOpenIddictPortalModule), typeof(AbpOpenIddictWeChatWorkModule), + typeof(AbpOpenIddictImpersonationModule), typeof(AbpIdentityOrganizaztionUnitsModule), typeof(AbpPermissionManagementDomainIdentityModule), typeof(AuthServerMigrationsEntityFrameworkCoreModule), diff --git a/aspnet-core/services/LY.MicroService.AuthServer/LY.MicroService.AuthServer.csproj b/aspnet-core/services/LY.MicroService.AuthServer/LY.MicroService.AuthServer.csproj index cdc357f83..f3b2708a7 100644 --- a/aspnet-core/services/LY.MicroService.AuthServer/LY.MicroService.AuthServer.csproj +++ b/aspnet-core/services/LY.MicroService.AuthServer/LY.MicroService.AuthServer.csproj @@ -72,6 +72,7 @@ + diff --git a/aspnet-core/services/LY.MicroService.AuthServer/Pages/Abp/MultiTenancy/TenantSwitchModal.cshtml b/aspnet-core/services/LY.MicroService.AuthServer/Pages/Abp/MultiTenancy/TenantSwitchModal.cshtml new file mode 100644 index 000000000..4be97216f --- /dev/null +++ b/aspnet-core/services/LY.MicroService.AuthServer/Pages/Abp/MultiTenancy/TenantSwitchModal.cshtml @@ -0,0 +1,36 @@ +@page +@using Microsoft.AspNetCore.Mvc.Localization +@using LY.MicroService.AuthServer.Pages.Abp.MultiTenancy +@using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal +@using Volo.Abp.AspNetCore.Mvc.UI.MultiTenancy.Localization +@using static LY.MicroService.AuthServer.Pages.Abp.MultiTenancy.TenantSwitchModalModel +@model TenantSwitchModalModel +@inject IHtmlLocalizer L +@{ + Layout = null; +} + + + +
+ @if (Model.Input is SelectTenantInfoModel selectModel && selectModel.AvailableTenants.Count > 0) + { + + } + else + { + + } + +
+ +
+ + \ No newline at end of file diff --git a/aspnet-core/services/LY.MicroService.AuthServer/Pages/Abp/MultiTenancy/TenantSwitchModal.cshtml.cs b/aspnet-core/services/LY.MicroService.AuthServer/Pages/Abp/MultiTenancy/TenantSwitchModal.cshtml.cs new file mode 100644 index 000000000..812e734a0 --- /dev/null +++ b/aspnet-core/services/LY.MicroService.AuthServer/Pages/Abp/MultiTenancy/TenantSwitchModal.cshtml.cs @@ -0,0 +1,118 @@ +using LINGYUN.Platform.Portal; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.Extensions.Options; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Volo.Abp; +using Volo.Abp.AspNetCore.MultiTenancy; +using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form; +using Volo.Abp.AspNetCore.Mvc.UI.MultiTenancy.Localization; +using Volo.Abp.AspNetCore.Mvc.UI.RazorPages; +using Volo.Abp.MultiTenancy; + +#nullable enable +namespace LY.MicroService.AuthServer.Pages.Abp.MultiTenancy; + +public class TenantSwitchModalModel : AbpPageModel +{ + [BindProperty] + public TenantInfoModel Input { get; set; } = default!; + + public List AvailableTenants { get; set; } = new(); + + protected ITenantStore TenantStore { get; } + protected ITenantNormalizer TenantNormalizer { get; } + protected AbpAspNetCoreMultiTenancyOptions Options { get; } + protected IEnterpriseRepository EnterpriseRepository { get; } + public TenantSwitchModalModel( + ITenantStore tenantStore, + ITenantNormalizer tenantNormalizer, + IOptions options, + IEnterpriseRepository enterpriseRepository) + { + TenantStore = tenantStore; + TenantNormalizer = tenantNormalizer; + Options = options.Value; + EnterpriseRepository = enterpriseRepository; + + LocalizationResourceType = typeof(AbpUiMultiTenancyResource); + } + + public async virtual Task OnGetAsync() + { + await LoadAvailableTenants(); + if (AvailableTenants.Count > 0) + { + Input = new SelectTenantInfoModel + { + AvailableTenants = AvailableTenants + }; + } + else + { + Input = new TenantInfoModel(); + } + + if (CurrentTenant.IsAvailable) + { + var tenant = await TenantStore.FindAsync(CurrentTenant.GetId()); + Input.Name = tenant?.Name; + } + } + + public virtual async Task OnPostAsync() + { + Guid? tenantId = null; + if (!Input.Name.IsNullOrEmpty()) + { + var tenant = await TenantStore.FindAsync(TenantNormalizer.NormalizeName(Input.Name!)!); + if (tenant == null && Guid.TryParse(Input.Name, out var id)) + { + tenant = await TenantStore.FindAsync(id); + } + if (tenant == null) + { + throw new UserFriendlyException(L["GivenTenantIsNotExist", Input.Name!]); + } + + if (!tenant.IsActive) + { + throw new UserFriendlyException(L["GivenTenantIsNotAvailable", Input.Name!]); + } + + tenantId = tenant.Id; + } + + AbpMultiTenancyCookieHelper.SetTenantCookie(HttpContext, tenantId, Options.TenantKey); + } + private async Task LoadAvailableTenants() + { + var enterprises = await EnterpriseRepository.GetEnterprisesInTenantListAsync(maxResultCount: 25); + if (enterprises.Count > 0) + { + AvailableTenants = enterprises + .Select(enterprise => + new SelectListItem( + enterprise.Name, enterprise.TenantId?.ToString())) + .Union([new SelectListItem("Default", "")]) + .ToList(); + } + } + + public class TenantInfoModel + { + public virtual string? Name { get; set; } + } + + public class SelectTenantInfoModel : TenantInfoModel + { + public List AvailableTenants { get; set; } = new(); + + [SelectItems(nameof(AvailableTenants))] + public override string? Name { get; set; } + } +} +#nullable disable diff --git a/aspnet-core/services/LY.MicroService.AuthServer/Pages/Abp/MultiTenancy/_ViewImports.cshtml b/aspnet-core/services/LY.MicroService.AuthServer/Pages/Abp/MultiTenancy/_ViewImports.cshtml new file mode 100644 index 000000000..225780c2c --- /dev/null +++ b/aspnet-core/services/LY.MicroService.AuthServer/Pages/Abp/MultiTenancy/_ViewImports.cshtml @@ -0,0 +1,3 @@ +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@addTagHelper *, Volo.Abp.AspNetCore.Mvc.UI.Bootstrap +@addTagHelper *, Volo.Abp.AspNetCore.Mvc.UI.Bundling \ No newline at end of file