Browse Source

feat: Optimize Tenant Selection Component

pull/1538/head
colin 2 days ago
parent
commit
580ffbecab
  1. 1
      aspnet-core/LINGYUN.MicroService.All.slnx
  2. 1
      aspnet-core/LINGYUN.MicroService.SingleProject.slnx
  3. 2
      aspnet-core/aspire/LINGYUN.Abp.MicroService.AuthServer/AuthServerModule.cs
  4. 36
      aspnet-core/aspire/LINGYUN.Abp.MicroService.AuthServer/Pages/Abp/MultiTenancy/TenantSwitchModal.cshtml
  5. 118
      aspnet-core/aspire/LINGYUN.Abp.MicroService.AuthServer/Pages/Abp/MultiTenancy/TenantSwitchModal.cshtml.cs
  6. 3
      aspnet-core/aspire/LINGYUN.Abp.MicroService.AuthServer/Pages/Abp/MultiTenancy/_ViewImports.cshtml
  7. 6
      aspnet-core/modules/account/LINGYUN.Abp.Account.Web.IdentityServer/Pages/Account/IdentityServerLoginModel.cs
  8. 2
      aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Pages/Account/LinkLogged.cshtml
  9. 2
      aspnet-core/modules/identity/LINGYUN.Abp.Identity.Application.Contracts/LINGYUN/Abp/Identity/Dto/IdentitySessionDto.cs
  10. 137
      aspnet-core/modules/identity/LINGYUN.Abp.Identity.AspNetCore/LINGYUN/Abp/Identity/AspNetCore/TotpService.cs
  11. 3
      aspnet-core/services/LY.AIO.Applications.Single/MicroServiceApplicationsSingleModule.Configure.cs
  12. 4
      aspnet-core/services/LY.AIO.Applications.Single/TenantHeaderParamter.cs
  13. 1
      aspnet-core/services/LY.MicroService.Applications.Single/Dockerfile
  14. 1
      aspnet-core/services/LY.MicroService.Applications.Single/LY.MicroService.Applications.Single.csproj
  15. 5
      aspnet-core/services/LY.MicroService.Applications.Single/MicroServiceApplicationsSingleModule.cs
  16. 36
      aspnet-core/services/LY.MicroService.Applications.Single/Pages/Abp/MultiTenancy/TenantSwitchModal.cshtml
  17. 112
      aspnet-core/services/LY.MicroService.Applications.Single/Pages/Abp/MultiTenancy/TenantSwitchModal.cshtml.cs
  18. 3
      aspnet-core/services/LY.MicroService.Applications.Single/Pages/Abp/MultiTenancy/_ViewImports.cshtml
  19. 8
      aspnet-core/services/LY.MicroService.AuthServer/AuthServerModule.Configure.cs
  20. 3
      aspnet-core/services/LY.MicroService.AuthServer/AuthServerModule.cs
  21. 1
      aspnet-core/services/LY.MicroService.AuthServer/LY.MicroService.AuthServer.csproj
  22. 36
      aspnet-core/services/LY.MicroService.AuthServer/Pages/Abp/MultiTenancy/TenantSwitchModal.cshtml
  23. 118
      aspnet-core/services/LY.MicroService.AuthServer/Pages/Abp/MultiTenancy/TenantSwitchModal.cshtml.cs
  24. 3
      aspnet-core/services/LY.MicroService.AuthServer/Pages/Abp/MultiTenancy/_ViewImports.cshtml

1
aspnet-core/LINGYUN.MicroService.All.slnx

@ -396,6 +396,7 @@
<Project Path="modules/openIddict/LINGYUN.Abp.OpenIddict.Dapr.Client/LINGYUN.Abp.OpenIddict.Dapr.Client.csproj" />
<Project Path="modules/openIddict/LINGYUN.Abp.OpenIddict.HttpApi.Client/LINGYUN.Abp.OpenIddict.HttpApi.Client.csproj" />
<Project Path="modules/openIddict/LINGYUN.Abp.OpenIddict.HttpApi/LINGYUN.Abp.OpenIddict.HttpApi.csproj" />
<Project Path="modules/openIddict/LINGYUN.Abp.OpenIddict.Impersonation/LINGYUN.Abp.OpenIddict.Impersonation.csproj" />
<Project Path="modules/openIddict/LINGYUN.Abp.OpenIddict.LinkUser/LINGYUN.Abp.OpenIddict.LinkUser.csproj" />
<Project Path="modules/openIddict/LINGYUN.Abp.OpenIddict.Portal/LINGYUN.Abp.OpenIddict.Portal.csproj" />
<Project Path="modules/openIddict/LINGYUN.Abp.OpenIddict.Sms/LINGYUN.Abp.OpenIddict.Sms.csproj" />

1
aspnet-core/LINGYUN.MicroService.SingleProject.slnx

@ -274,6 +274,7 @@
<Project Path="modules/identity/LINGYUN.Abp.Identity.Application/LINGYUN.Abp.Identity.Application.csproj" />
<Project Path="modules/identity/LINGYUN.Abp.Identity.AspNetCore.QrCode/LINGYUN.Abp.Identity.AspNetCore.QrCode.csproj" />
<Project Path="modules/identity/LINGYUN.Abp.Identity.AspNetCore.Session/LINGYUN.Abp.Identity.AspNetCore.Session.csproj" />
<Project Path="modules/identity/LINGYUN.Abp.Identity.AspNetCore/LINGYUN.Abp.Identity.AspNetCore.csproj" />
<Project Path="modules/identity/LINGYUN.Abp.Identity.Domain.Shared/LINGYUN.Abp.Identity.Domain.Shared.csproj" />
<Project Path="modules/identity/LINGYUN.Abp.Identity.Domain/LINGYUN.Abp.Identity.Domain.csproj" />
<Project Path="modules/identity/LINGYUN.Abp.Identity.EntityFrameworkCore/LINGYUN.Abp.Identity.EntityFrameworkCore.csproj" />

2
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),

36
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<AbpUiMultiTenancyResource> L
@{
Layout = null;
}
<abp-modal>
<abp-modal-header title="@L["SwitchTenant"].Value"></abp-modal-header>
<abp-modal-body>
<form method="post" id="tenantSwitchForm" asp-page="/Abp/MultiTenancy/TenantSwitchModal">
@if (Model.Input is SelectTenantInfoModel selectModel && selectModel.AvailableTenants.Count > 0)
{
<abp-select asp-for="Input.Name" label="@L["SwitchTenantHint"].Value" asp-items="selectModel.AvailableTenants" />
}
else
{
<abp-input asp-for="Input.Name" label="@L["SwitchTenantHint"].Value" />
}
</form>
</abp-modal-body>
<abp-modal-footer buttons="@(AbpModalButtons.Cancel | AbpModalButtons.Save)"></abp-modal-footer>
</abp-modal>
<script>
$(function() {
$('.modal-footer .btn-primary').click(function(e) {
e.preventDefault();
$('#tenantSwitchForm').submit();
});
});
</script>

118
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<SelectListItem> 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<AbpAspNetCoreMultiTenancyOptions> 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<SelectListItem> AvailableTenants { get; set; } = new();
[SelectItems(nameof(AvailableTenants))]
public override string? Name { get; set; }
}
}
#nullable disable

3
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

6
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<IActionResult> 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<IActionResult> 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);
}
}

2
aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Pages/Account/LinkLogged.cshtml

@ -20,7 +20,7 @@
<input asp-for="LinkTenantId">
<input asp-for="LinkUserId">
<div class="gap-2 d-grid">
<abp-button class="btn-secondary">@L["LogInUsingYourProviderAccount", Model.LinkTenantAndUserName] &rarr;</abp-button>
<abp-button class="btn-secondary">@L["LogInUsingYourProviderAccount", @Model.LinkTenantAndUserName] &rarr;</abp-button>
</div>
</form>
</div>

2
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<Guid>
public class IdentitySessionDto : ExtensibleEntityDto<Guid>
{
public string SessionId { get; set; }

137
aspnet-core/modules/identity/LINGYUN.Abp.Identity.AspNetCore/LINGYUN/Abp/Identity/AspNetCore/TotpService.cs

@ -1,137 +0,0 @@
using System;
using System.Diagnostics;
using System.Net;
using System.Security.Cryptography;
using System.Text;
namespace LINGYUN.Abp.Identity.AspNetCore;
/// <summary>
/// 参考
/// Microsoft: https://github.com/dotnet/dotnet/blob/main/src/aspnetcore/src/Identity/Extensions.Core/src/Rfc6238AuthenticationService.cs
/// </summary>
#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<byte> 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<byte> modifierCombinedBytes = timestepAsBytes;
if (modifierBytes is not null)
{
modifierCombinedBytes = ApplyModifier(timestepAsBytes, modifierBytes);
}
Span<byte> 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<byte> 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

3
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;

4
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<OpenApiParameter>();
operation.Parameters ??= new List<IOpenApiParameter>();
operation.Parameters.Add(new OpenApiParameter
{
Name = _aspNetCoreMultiTenancyOptions.TenantKey,

1
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" ]

1
aspnet-core/services/LY.MicroService.Applications.Single/LY.MicroService.Applications.Single.csproj

@ -188,6 +188,7 @@
<ProjectReference Include="..\..\modules\identity\LINGYUN.Abp.Identity.Application.Contracts\LINGYUN.Abp.Identity.Application.Contracts.csproj" />
<ProjectReference Include="..\..\modules\identity\LINGYUN.Abp.Identity.Application\LINGYUN.Abp.Identity.Application.csproj" />
<ProjectReference Include="..\..\modules\identity\LINGYUN.Abp.Identity.AspNetCore.Session\LINGYUN.Abp.Identity.AspNetCore.Session.csproj" />
<ProjectReference Include="..\..\modules\identity\LINGYUN.Abp.Identity.AspNetCore\LINGYUN.Abp.Identity.AspNetCore.csproj" />
<ProjectReference Include="..\..\modules\identity\LINGYUN.Abp.Identity.Domain.Shared\LINGYUN.Abp.Identity.Domain.Shared.csproj" />
<ProjectReference Include="..\..\modules\identity\LINGYUN.Abp.Identity.Domain\LINGYUN.Abp.Identity.Domain.csproj" />
<ProjectReference Include="..\..\modules\identity\LINGYUN.Abp.Identity.EntityFrameworkCore\LINGYUN.Abp.Identity.EntityFrameworkCore.csproj" />

5
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),

36
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<AbpUiMultiTenancyResource> L
@{
Layout = null;
}
<abp-modal>
<abp-modal-header title="@L["SwitchTenant"].Value"></abp-modal-header>
<abp-modal-body>
<form method="post" id="tenantSwitchForm" asp-page="/Abp/MultiTenancy/TenantSwitchModal">
@if (Model.Input is SelectTenantInfoModel selectModel && selectModel.AvailableTenants.Count > 0)
{
<abp-select asp-for="Input.Name" label="@L["SwitchTenantHint"].Value" asp-items="selectModel.AvailableTenants" />
}
else
{
<abp-input asp-for="Input.Name" label="@L["SwitchTenantHint"].Value" />
}
</form>
</abp-modal-body>
<abp-modal-footer buttons="@(AbpModalButtons.Cancel | AbpModalButtons.Save)"></abp-modal-footer>
</abp-modal>
<script>
$(function() {
$('.modal-footer .btn-primary').click(function(e) {
e.preventDefault();
$('#tenantSwitchForm').submit();
});
});
</script>

112
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<SelectListItem> 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<AbpAspNetCoreMultiTenancyOptions> 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<SelectListItem> AvailableTenants { get; set; } = new();
[SelectItems(nameof(AvailableTenants))]
public override string? Name { get; set; }
}
}
#nullable disable

3
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

8
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<OpenIddictImpersonationOptions>(options =>
{
options.ImpersonationPermission = IdentityPermissions.Users.Impersonation;
options.ImpersonationTenantPermission = "AbpSaas.Tenants.Impersonation";
});
}
private void ConfigureSecurity(IServiceCollection services, IConfiguration configuration, bool isDevelopment = false)
{

3
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),

1
aspnet-core/services/LY.MicroService.AuthServer/LY.MicroService.AuthServer.csproj

@ -72,6 +72,7 @@
<ProjectReference Include="..\..\modules\identity\LINGYUN.Abp.Identity.OrganizaztionUnits\LINGYUN.Abp.Identity.OrganizaztionUnits.csproj" />
<ProjectReference Include="..\..\modules\identity\LINGYUN.Abp.Identity.Session.AspNetCore\LINGYUN.Abp.Identity.Session.AspNetCore.csproj" />
<ProjectReference Include="..\..\modules\openIddict\LINGYUN.Abp.OpenIddict.AspNetCore.Session\LINGYUN.Abp.OpenIddict.AspNetCore.Session.csproj" />
<ProjectReference Include="..\..\modules\openIddict\LINGYUN.Abp.OpenIddict.Impersonation\LINGYUN.Abp.OpenIddict.Impersonation.csproj" />
<ProjectReference Include="..\..\modules\openIddict\LINGYUN.Abp.OpenIddict.LinkUser\LINGYUN.Abp.OpenIddict.LinkUser.csproj" />
<ProjectReference Include="..\..\modules\openIddict\LINGYUN.Abp.OpenIddict.Portal\LINGYUN.Abp.OpenIddict.Portal.csproj" />
<ProjectReference Include="..\..\modules\openIddict\LINGYUN.Abp.OpenIddict.Sms\LINGYUN.Abp.OpenIddict.Sms.csproj" />

36
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<AbpUiMultiTenancyResource> L
@{
Layout = null;
}
<abp-modal>
<abp-modal-header title="@L["SwitchTenant"].Value"></abp-modal-header>
<abp-modal-body>
<form method="post" id="tenantSwitchForm" asp-page="/Abp/MultiTenancy/TenantSwitchModal">
@if (Model.Input is SelectTenantInfoModel selectModel && selectModel.AvailableTenants.Count > 0)
{
<abp-select asp-for="Input.Name" label="@L["SwitchTenantHint"].Value" asp-items="selectModel.AvailableTenants" />
}
else
{
<abp-input asp-for="Input.Name" label="@L["SwitchTenantHint"].Value" />
}
</form>
</abp-modal-body>
<abp-modal-footer buttons="@(AbpModalButtons.Cancel | AbpModalButtons.Save)"></abp-modal-footer>
</abp-modal>
<script>
$(function() {
$('.modal-footer .btn-primary').click(function(e) {
e.preventDefault();
$('#tenantSwitchForm').submit();
});
});
</script>

118
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<SelectListItem> 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<AbpAspNetCoreMultiTenancyOptions> 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<SelectListItem> AvailableTenants { get; set; } = new();
[SelectItems(nameof(AvailableTenants))]
public override string? Name { get; set; }
}
}
#nullable disable

3
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
Loading…
Cancel
Save