mirror of https://github.com/abpframework/abp.git
12 changed files with 510 additions and 48 deletions
@ -0,0 +1,25 @@ |
|||
using Microsoft.AspNetCore.Components; |
|||
using Volo.Abp.AspNetCore.Components; |
|||
|
|||
namespace Volo.Abp.AspNetCore.Components.WebAssembly.Theming; |
|||
|
|||
/// <summary>
|
|||
/// Shared base for WASM theme <c>Authentication</c> pages.
|
|||
/// Provides a <see cref="GetCultureAwareHomeUrl"/> helper so the culture-aware
|
|||
/// home URL construction is not duplicated across theme packages.
|
|||
/// </summary>
|
|||
public abstract class CultureAwareAuthenticationBase : AbpComponentBase |
|||
{ |
|||
[Inject] |
|||
protected NavigationManager Navigation { get; set; } = default!; |
|||
|
|||
[Parameter] |
|||
public string? Culture { get; set; } |
|||
|
|||
protected virtual string GetCultureAwareHomeUrl() |
|||
{ |
|||
return string.IsNullOrEmpty(Culture) |
|||
? Navigation.BaseUri |
|||
: Navigation.BaseUri + Culture + "/"; |
|||
} |
|||
} |
|||
@ -0,0 +1,35 @@ |
|||
using System.Threading.Tasks; |
|||
using Microsoft.AspNetCore.Components; |
|||
using Microsoft.AspNetCore.Components.WebAssembly.Authentication; |
|||
using Microsoft.Extensions.Options; |
|||
using Volo.Abp.AspNetCore.Components.Web; |
|||
using Volo.Abp.AspNetCore.Components.WebAssembly; |
|||
|
|||
namespace Volo.Abp.AspNetCore.Components.WebAssembly.Theming; |
|||
|
|||
/// <summary>
|
|||
/// Provides the shared culture-aware login redirect logic for all WASM theme
|
|||
/// <c>WebAssemblyRedirectToLogin</c> components. Each theme must keep
|
|||
/// <c>@inherits RedirectToLogin</c> for ABP service-replacement assignability,
|
|||
/// so a common component base class is not feasible; this static helper
|
|||
/// centralises the logic instead.
|
|||
/// </summary>
|
|||
public static class CultureAwareRedirectToLoginHelper |
|||
{ |
|||
public static async Task RedirectAsync( |
|||
NavigationManager navigation, |
|||
string loginUrl, |
|||
IRouteBasedCultureUrlHelper cultureUrlHelper, |
|||
IOptions<AbpAspNetCoreComponentsWebOptions> webOptions) |
|||
{ |
|||
var cultureLoginUrl = await cultureUrlHelper.PrependCulturePrefixAsync(loginUrl); |
|||
if (webOptions.Value.IsBlazorWebApp) |
|||
{ |
|||
navigation.NavigateTo(cultureLoginUrl, forceLoad: true); |
|||
} |
|||
else |
|||
{ |
|||
navigation.NavigateToLogin(cultureLoginUrl); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,103 @@ |
|||
using System.Collections.Generic; |
|||
using System.Threading.Tasks; |
|||
using Microsoft.AspNetCore.Components; |
|||
using Shouldly; |
|||
using Volo.Abp.AspNetCore.Components.WebAssembly; |
|||
using Volo.Abp.Localization; |
|||
using Xunit; |
|||
|
|||
namespace Volo.Abp.AspNetCore.Mvc.Localization; |
|||
|
|||
public class RouteBasedCultureNavigationHelper_Tests |
|||
{ |
|||
private static readonly IEnumerable<LanguageInfo> AllLanguages = new[] |
|||
{ |
|||
new LanguageInfo("en"), |
|||
new LanguageInfo("tr"), |
|||
new LanguageInfo("zh-Hans"), |
|||
}; |
|||
|
|||
private readonly RouteBasedCultureNavigationHelper _helper = new(); |
|||
|
|||
[Fact] |
|||
public async Task Should_Replace_Culture_In_Simple_Path() |
|||
{ |
|||
var nav = new TestNavigationManager("https://example.com/", "https://example.com/tr/home"); |
|||
await _helper.NavigateToNewCultureAsync(nav, new LanguageInfo("en"), AllLanguages); |
|||
nav.LastNavigatedUri.ShouldBe("https://example.com/en/home"); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Replace_Culture_When_No_Path_After_Culture() |
|||
{ |
|||
var nav = new TestNavigationManager("https://example.com/", "https://example.com/tr"); |
|||
await _helper.NavigateToNewCultureAsync(nav, new LanguageInfo("en"), AllLanguages); |
|||
nav.LastNavigatedUri.ShouldBe("https://example.com/en"); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Replace_Culture_When_Query_String_Follows_Culture_Directly() |
|||
{ |
|||
// Regression: "tr?x=1" was being treated as a single segment "tr?x=1"
|
|||
// instead of culture="tr" + suffix="?x=1".
|
|||
var nav = new TestNavigationManager("https://example.com/", "https://example.com/tr?x=1"); |
|||
await _helper.NavigateToNewCultureAsync(nav, new LanguageInfo("en"), AllLanguages); |
|||
nav.LastNavigatedUri.ShouldBe("https://example.com/en?x=1"); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Replace_Culture_When_Fragment_Follows_Culture_Directly() |
|||
{ |
|||
var nav = new TestNavigationManager("https://example.com/", "https://example.com/tr#section"); |
|||
await _helper.NavigateToNewCultureAsync(nav, new LanguageInfo("en"), AllLanguages); |
|||
nav.LastNavigatedUri.ShouldBe("https://example.com/en#section"); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Replace_Culture_Preserving_Path_Query_And_Fragment() |
|||
{ |
|||
var nav = new TestNavigationManager("https://example.com/", "https://example.com/tr/about?ref=main#top"); |
|||
await _helper.NavigateToNewCultureAsync(nav, new LanguageInfo("zh-Hans"), AllLanguages); |
|||
nav.LastNavigatedUri.ShouldBe("https://example.com/zh-Hans/about?ref=main#top"); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Prepend_Culture_When_No_Existing_Culture_Prefix() |
|||
{ |
|||
var nav = new TestNavigationManager("https://example.com/", "https://example.com/identity/users"); |
|||
await _helper.NavigateToNewCultureAsync(nav, new LanguageInfo("zh-Hans"), AllLanguages); |
|||
nav.LastNavigatedUri.ShouldBe("https://example.com/zh-Hans/identity/users"); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Prepend_Culture_When_At_Root() |
|||
{ |
|||
var nav = new TestNavigationManager("https://example.com/", "https://example.com/"); |
|||
await _helper.NavigateToNewCultureAsync(nav, new LanguageInfo("tr"), AllLanguages); |
|||
nav.LastNavigatedUri.ShouldBe("https://example.com/tr/"); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Not_Navigate_When_Target_Culture_Matches_Current() |
|||
{ |
|||
var nav = new TestNavigationManager("https://example.com/", "https://example.com/tr/home"); |
|||
await _helper.NavigateToNewCultureAsync(nav, new LanguageInfo("tr"), AllLanguages); |
|||
// Already on /tr/home — no navigation should occur
|
|||
nav.LastNavigatedUri.ShouldBeNull(); |
|||
} |
|||
|
|||
private sealed class TestNavigationManager : NavigationManager |
|||
{ |
|||
public string? LastNavigatedUri { get; private set; } |
|||
|
|||
public TestNavigationManager(string baseUri, string uri) |
|||
{ |
|||
Initialize(baseUri, uri); |
|||
} |
|||
|
|||
protected override void NavigateToCore(string uri, bool forceLoad) |
|||
{ |
|||
LastNavigatedUri = uri; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,156 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Globalization; |
|||
using System.Threading.Tasks; |
|||
using NSubstitute; |
|||
using Shouldly; |
|||
using Volo.Abp.AspNetCore.Components.WebAssembly; |
|||
using Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations; |
|||
using Volo.Abp.AspNetCore.Mvc.Client; |
|||
using Volo.Abp.Localization; |
|||
using Xunit; |
|||
|
|||
namespace Volo.Abp.AspNetCore.Mvc.Localization; |
|||
|
|||
public class RouteBasedCultureUrlHelper_Tests |
|||
{ |
|||
private readonly ICachedApplicationConfigurationClient _configClient; |
|||
private readonly RouteBasedCultureUrlHelper _helper; |
|||
private readonly ApplicationConfigurationDto _config; |
|||
|
|||
public RouteBasedCultureUrlHelper_Tests() |
|||
{ |
|||
_config = new ApplicationConfigurationDto |
|||
{ |
|||
Localization = new ApplicationLocalizationConfigurationDto |
|||
{ |
|||
UseRouteBasedCulture = true, |
|||
Languages = new List<LanguageInfo> |
|||
{ |
|||
new LanguageInfo("en"), |
|||
new LanguageInfo("zh-Hans"), |
|||
new LanguageInfo("tr"), |
|||
new LanguageInfo("es-419"), |
|||
} |
|||
} |
|||
}; |
|||
|
|||
_configClient = Substitute.For<ICachedApplicationConfigurationClient>(); |
|||
_configClient.GetAsync().Returns(_config); |
|||
|
|||
_helper = new RouteBasedCultureUrlHelper(_configClient); |
|||
} |
|||
|
|||
[Theory] |
|||
[InlineData("https://auth-server.example.com/connect/authorize")] |
|||
[InlineData("http://example.com/login")] |
|||
public async Task Should_Not_Modify_Absolute_Urls(string url) |
|||
{ |
|||
using var _ = CultureScope("zh-Hans"); |
|||
var result = await _helper.PrependCulturePrefixAsync(url); |
|||
result.ShouldBe(url); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Not_Modify_Protocol_Relative_Url() |
|||
{ |
|||
using var _ = CultureScope("zh-Hans"); |
|||
var result = await _helper.PrependCulturePrefixAsync("//cdn.example.com/asset.js"); |
|||
result.ShouldBe("//cdn.example.com/asset.js"); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Prepend_Culture_To_Root_Relative_Url() |
|||
{ |
|||
using var _ = CultureScope("zh-Hans"); |
|||
var result = await _helper.PrependCulturePrefixAsync("/account/manage-profile"); |
|||
result.ShouldBe("/zh-Hans/account/manage-profile"); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Prepend_Culture_To_Tilde_Slash_Url() |
|||
{ |
|||
using var _ = CultureScope("tr"); |
|||
var result = await _helper.PrependCulturePrefixAsync("~/account/manage-profile"); |
|||
result.ShouldBe("~/tr/account/manage-profile"); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Prepend_Culture_To_Bare_Relative_Url() |
|||
{ |
|||
// Default auth URLs like "authentication/login" have no leading slash.
|
|||
using var _ = CultureScope("zh-Hans"); |
|||
var result = await _helper.PrependCulturePrefixAsync("authentication/login"); |
|||
result.ShouldBe("zh-Hans/authentication/login"); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Not_Modify_Url_When_Feature_Disabled() |
|||
{ |
|||
_config.Localization.UseRouteBasedCulture = false; |
|||
using var _ = CultureScope("zh-Hans"); |
|||
var result = await _helper.PrependCulturePrefixAsync("/home"); |
|||
result.ShouldBe("/home"); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Not_Modify_Url_When_Culture_Not_In_Language_List() |
|||
{ |
|||
using var _ = CultureScope("fr"); |
|||
var result = await _helper.PrependCulturePrefixAsync("/home"); |
|||
result.ShouldBe("/home"); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Return_Empty_String_Unchanged() |
|||
{ |
|||
var result = await _helper.PrependCulturePrefixAsync(string.Empty); |
|||
result.ShouldBe(string.Empty); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Support_Numeric_Region_Culture_Tag() |
|||
{ |
|||
using var _ = CultureScope("es-419"); |
|||
var result = await _helper.PrependCulturePrefixAsync("/home"); |
|||
result.ShouldBe("/es-419/home"); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Be_Idempotent_On_Root_Relative_Url() |
|||
{ |
|||
using var _ = CultureScope("zh-Hans"); |
|||
var result = await _helper.PrependCulturePrefixAsync("/zh-Hans/account/manage-profile"); |
|||
result.ShouldBe("/zh-Hans/account/manage-profile"); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Be_Idempotent_On_Tilde_Slash_Url() |
|||
{ |
|||
using var _ = CultureScope("tr"); |
|||
var result = await _helper.PrependCulturePrefixAsync("~/tr/account/manage-profile"); |
|||
result.ShouldBe("~/tr/account/manage-profile"); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Be_Idempotent_On_Bare_Relative_Url() |
|||
{ |
|||
using var _ = CultureScope("zh-Hans"); |
|||
var result = await _helper.PrependCulturePrefixAsync("zh-Hans/authentication/login"); |
|||
result.ShouldBe("zh-Hans/authentication/login"); |
|||
} |
|||
|
|||
private static IDisposable CultureScope(string cultureName) |
|||
{ |
|||
var previous = CultureInfo.CurrentCulture; |
|||
CultureInfo.CurrentCulture = new CultureInfo(cultureName); |
|||
return new DelegateDisposable(() => CultureInfo.CurrentCulture = previous); |
|||
} |
|||
|
|||
private sealed class DelegateDisposable : IDisposable |
|||
{ |
|||
private readonly System.Action _onDispose; |
|||
public DelegateDisposable(System.Action onDispose) => _onDispose = onDispose; |
|||
public void Dispose() => _onDispose(); |
|||
} |
|||
} |
|||
@ -1,25 +1,22 @@ |
|||
@inject NavigationManager Navigation |
|||
@using Volo.Abp.DependencyInjection |
|||
@using Volo.Abp.AspNetCore.Components.Web.BasicTheme.Themes.Basic |
|||
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication |
|||
@using Volo.Abp.AspNetCore.Components.WebAssembly.Theming |
|||
@using Microsoft.Extensions.Options |
|||
@using Volo.Abp.AspNetCore.Components.WebAssembly |
|||
@using Volo.Abp.AspNetCore.Components.Web |
|||
@inherits RedirectToLogin |
|||
@attribute [ExposeServices(typeof(RedirectToLogin))] |
|||
@attribute [Dependency(ReplaceServices = true)] |
|||
@inject IOptions<AuthenticationOptions> AuthenticationOptions |
|||
@inject NavigationManager Navigation |
|||
@inject IOptions<AuthenticationOptions> AuthOptions |
|||
@inject IRouteBasedCultureUrlHelper CultureUrlHelper |
|||
@inject IOptions<AbpAspNetCoreComponentsWebOptions> AbpAspNetCoreComponentsWebOptions |
|||
|
|||
@code { |
|||
protected override void OnInitialized() |
|||
protected override void OnInitialized() { } |
|||
|
|||
protected override Task OnInitializedAsync() |
|||
{ |
|||
if (AbpAspNetCoreComponentsWebOptions.Value.IsBlazorWebApp) |
|||
{ |
|||
Navigation.NavigateTo(AuthenticationOptions.Value.LoginUrl, forceLoad: true); |
|||
} |
|||
else |
|||
{ |
|||
Navigation.NavigateToLogin(AuthenticationOptions.Value.LoginUrl); |
|||
} |
|||
return CultureAwareRedirectToLoginHelper.RedirectAsync(Navigation, AuthOptions.Value.LoginUrl, CultureUrlHelper, AbpAspNetCoreComponentsWebOptions); |
|||
} |
|||
} |
|||
|
|||
Loading…
Reference in new issue