Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

User Localization settings #13181

Merged
merged 48 commits into from
Feb 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
249482d
User Localization settings
Skrypt Jan 31, 2023
78156fd
React to comment
Skrypt Jan 31, 2023
b8974b9
Something that works
Skrypt Jan 31, 2023
4ed3b3b
await NullProviderCultureResult;
Skrypt Jan 31, 2023
2b9b926
return default
Skrypt Feb 1, 2023
4260675
Set the user default culture to the localization default culture if n…
hishamco Jan 12, 2024
619834f
Set the user default time zone to the site settings default time zone…
hishamco Jan 12, 2024
1a9d823
UserLocalizationStartup -> Startup
hishamco Jan 12, 2024
125f7be
Refactoring
hishamco Jan 12, 2024
357fe3a
UserTimeZoneStartup -> Startup
hishamco Jan 12, 2024
e582c31
Refactoring
hishamco Jan 12, 2024
cbb4d6f
Use file-scope namespace
hishamco Jan 12, 2024
15e3a77
Add TimeZones to UserTimeZoneViewModel
hishamco Jan 12, 2024
7a38792
Remove extra whitespace
hishamco Jan 12, 2024
f6f789b
Merge branch 'main' into skrypt/admin-user-localization-setting
Piedone Jan 14, 2024
245bc8b
Add release note
hishamco Jan 15, 2024
a599188
Fix indentation
hishamco Jan 15, 2024
376b00a
Refactoring
hishamco Jan 15, 2024
a103b5f
CultureClaimType as constant
hishamco Jan 15, 2024
2a04a1b
Add comment on ConfigureOrder
hishamco Jan 15, 2024
64102fd
Add GetCulture() extension method
hishamco Jan 15, 2024
362e1a5
Address the feedback
hishamco Jan 15, 2024
6fc7e32
Remove unnecessary Configure() method
hishamco Jan 16, 2024
4d5cd8b
Set the configuration order on OrchardCore.Users.Localization
hishamco Jan 16, 2024
f6da84b
Remove ConfigureOrder from OrchardCore.Users.Localization
hishamco Jan 16, 2024
2b6a88a
Revert changes from OC.Localization
hishamco Jan 16, 2024
98eba6d
Merge branch 'main' into skrypt/admin-user-localization-setting
hishamco Jan 19, 2024
69f1afa
Merge branch 'main' into skrypt/admin-user-localization-setting
hishamco Jan 23, 2024
f7436d5
Set the User Localization feature priority
hishamco Jan 23, 2024
67afc77
Fix issue with default culture not selected
Skrypt Jan 25, 2024
265f94b
Merge branch 'main' into skrypt/admin-user-localization-setting
Skrypt Jan 25, 2024
7df0224
Revert Timezone changes
Skrypt Jan 25, 2024
089b172
Revert changes timezone
Skrypt Jan 25, 2024
a835a88
Simplify
Skrypt Jan 26, 2024
6ace93d
cleanup using
Skrypt Jan 26, 2024
839721b
Try to support Invariant Culture
Skrypt Jan 29, 2024
9d397fb
Merge branch 'main' into skrypt/admin-user-localization-setting
Skrypt Jan 29, 2024
f50d427
Support InvariatCulture
Skrypt Jan 30, 2024
d90704d
Constants
Skrypt Jan 30, 2024
0eb60da
Merge branch 'main' into skrypt/admin-user-localization-setting
Skrypt Jan 30, 2024
6cfc324
Update src/docs/releases/1.9.0.md
Skrypt Feb 1, 2024
98f21f4
Constants => UserLocalizationConstants
Skrypt Feb 1, 2024
2714dc2
Merge branch 'skrypt/admin-user-localization-setting' of https://gith…
Skrypt Feb 1, 2024
979584e
Add doc
Skrypt Feb 1, 2024
f64587c
Merge branch 'main' into skrypt/admin-user-localization-setting
Skrypt Feb 12, 2024
1eff31d
Update src/docs/reference/modules/Users/README.md
Skrypt Feb 12, 2024
3b986b1
Update src/OrchardCore.Modules/OrchardCore.Users/Localization/ViewMod…
Skrypt Feb 12, 2024
bf7246c
Update src/OrchardCore.Modules/OrchardCore.Users/Localization/Drivers…
Skrypt Feb 12, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.Extensions.Localization;
using OrchardCore.DisplayManagement.Entities;
using OrchardCore.DisplayManagement.Handlers;
using OrchardCore.DisplayManagement.ModelBinding;
using OrchardCore.DisplayManagement.Views;
using OrchardCore.Localization;
using OrchardCore.Users.Localization.Models;
using OrchardCore.Users.Localization.ViewModels;
using OrchardCore.Users.Models;

namespace OrchardCore.Users.Localization.Drivers;

public class UserLocalizationDisplayDriver : SectionDisplayDriver<User, UserLocalizationSettings>
{
private readonly ILocalizationService _localizationService;
protected readonly IStringLocalizer S;

public UserLocalizationDisplayDriver(
ILocalizationService localizationService,
IStringLocalizer<UserLocalizationDisplayDriver> localizer)
{
_localizationService = localizationService;
S = localizer;
}

public override Task<IDisplayResult> EditAsync(UserLocalizationSettings section, BuildEditorContext context)
{
return Task.FromResult<IDisplayResult>(Initialize<UserLocalizationViewModel>("UserCulture_Edit", async model =>
{
var supportedCultures = await _localizationService.GetSupportedCulturesAsync();

var cultureList = supportedCultures.Select(culture =>
Skrypt marked this conversation as resolved.
Show resolved Hide resolved
new SelectListItem
{
Text = CultureInfo.GetCultureInfo(culture).DisplayName + " (" + culture + ")",
Value = culture
}).ToList();

cultureList.Insert(0, new SelectListItem() { Text = S["Use site's culture"], Value = "none" });

// If Invariant Culture is installed as a supported culture we bind it to a different culture code than String.Empty.
Skrypt marked this conversation as resolved.
Show resolved Hide resolved
var emptyCulture = cultureList.FirstOrDefault(c => c.Value == "");
if (emptyCulture != null)
{
emptyCulture.Value = UserLocalizationConstants.Invariant;
}

model.SelectedCulture = section.Culture;
model.CultureList = cultureList;
}).Location("Content:2"));
}

public override async Task<IDisplayResult> UpdateAsync(User model, UserLocalizationSettings section, IUpdateModel updater, BuildEditorContext context)
{
var viewModel = new UserLocalizationViewModel();

if (await context.Updater.TryUpdateModelAsync(viewModel, Prefix))
{
section.Culture = viewModel.SelectedCulture;
}

return await EditAsync(section, context);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using OrchardCore.Users.Localization.Providers;

namespace System.Security.Claims;

public static class ClaimsPrincipleExtensions
{
public static string GetCulture(this ClaimsPrincipal principal)
=> principal.FindFirstValue(UserLocalizationClaimsProvider.CultureClaimType);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace OrchardCore.Users.Localization.Models;

/// <summary>
/// Provides a model for the IEntity property.
/// </summary>
public class UserLocalizationSettings
{
public string Culture { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using System;
using System.Security.Claims;
using System.Threading.Tasks;
using OrchardCore.Entities;
using OrchardCore.Users.Localization.Models;
using OrchardCore.Users.Models;
using OrchardCore.Users.Services;

namespace OrchardCore.Users.Localization.Providers;

public class UserLocalizationClaimsProvider : IUserClaimsProvider
{
internal const string CultureClaimType = "culture";

public Task GenerateAsync(IUser user, ClaimsIdentity claims)
{
ArgumentNullException.ThrowIfNull(user);
ArgumentNullException.ThrowIfNull(claims);

if (user is not User currentUser)
{
return Task.CompletedTask;
}

if (currentUser.Has<UserLocalizationSettings>())
{
var localizationSetting = currentUser.As<UserLocalizationSettings>();

if (localizationSetting.Culture != "none")
{
claims.AddClaim(new Claim(CultureClaimType, localizationSetting.Culture == UserLocalizationConstants.Invariant ? "" : localizationSetting.Culture));
}
}

return Task.CompletedTask;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using System;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Localization;

namespace OrchardCore.Users.Localization.Providers;

public class UserLocalizationRequestCultureProvider : RequestCultureProvider
{
public override Task<ProviderCultureResult> DetermineProviderCultureResult(HttpContext httpContext)
{
ArgumentNullException.ThrowIfNull(httpContext);

if (httpContext?.User?.Identity?.IsAuthenticated == false)
{
return NullProviderCultureResult;
}

var userCulture = httpContext.User.GetCulture(); // String.Empty here means that it did not find the Culture Claim.

if (String.IsNullOrWhiteSpace(userCulture))
{
return NullProviderCultureResult;
}

return Task.FromResult(new ProviderCultureResult(userCulture == UserLocalizationConstants.Invariant ? "" : userCulture));
}
}
23 changes: 23 additions & 0 deletions src/OrchardCore.Modules/OrchardCore.Users/Localization/Startup.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using OrchardCore.DisplayManagement.Handlers;
using OrchardCore.Modules;
using OrchardCore.Users.Localization.Drivers;
using OrchardCore.Users.Localization.Providers;
using OrchardCore.Users.Models;
using OrchardCore.Users.Services;

namespace OrchardCore.Users.Localization;

[Feature("OrchardCore.Users.Localization")]
public class Startup : StartupBase
{
public override void ConfigureServices(IServiceCollection services)
{
services.AddScoped<IDisplayDriver<User>, UserLocalizationDisplayDriver>();
services.AddScoped<IUserClaimsProvider, UserLocalizationClaimsProvider>();

services.Configure<RequestLocalizationOptions>(options =>
options.AddInitialRequestCultureProvider(new UserLocalizationRequestCultureProvider()));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace OrchardCore.Users.Localization
{
public static class UserLocalizationConstants
{
public const string Invariant = "invariant";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.Rendering;

namespace OrchardCore.Users.Localization.ViewModels;

public class UserLocalizationViewModel
{
public string SelectedCulture { get; set; }

[BindNever]
public List<SelectListItem> CultureList { get; set; } = [];
}
9 changes: 9 additions & 0 deletions src/OrchardCore.Modules/OrchardCore.Users/Manifest.cs
hishamco marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,15 @@
Category = "Settings"
)]

[assembly: Feature(
Id = "OrchardCore.Users.Localization",
Name = "User Localization",
Description = "Provides a way to set the culture per user.",
Dependencies = new[] { "OrchardCore.Users", "OrchardCore.Localization" },
Category = "Settings",
Priority = "-1" // Added to avoid changing the order in the localization module.
)]

[assembly: Feature(
Id = "OrchardCore.Users.CustomUserSettings",
Name = "Custom User Settings",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
@using OrchardCore.Users.Localization.ViewModels
@model UserLocalizationViewModel

<div class="mb-3" asp-validation-class-for="SelectedCulture">
<label asp-for="SelectedCulture">@T["Default User Culture"]</label>
<select asp-for="SelectedCulture" asp-items="Model.CultureList" class="form-select">
</select>
<span asp-validation-for="SelectedCulture"></span>
<span class="hint">@T["Determines the default culture used by this user."]</span>
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -517,7 +517,7 @@ private static void AddAuthentication(OrchardCoreBuilder builder)
.Configure(app =>
{
app.UseAuthentication();
});
}, order: -150);
Skrypt marked this conversation as resolved.
Show resolved Hide resolved
}

/// <summary>
Expand Down
7 changes: 7 additions & 0 deletions src/docs/reference/modules/Users/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,18 @@ The module contains the following features apart from the base feature:
- Two-Factor Authentication Services: Provices Two-factor core services. This feature cannot be manually enabled or disable as it is enabled by dependency on demand.
- Two-Factor Email Method: Allows users to two-factor authenticate using an email.
- Two-Factor Authenticator App Method: Allows users to two-factor authenticate using any Authenticator App.
- User Localization: Allows ability to configure user culture per user from admin UI.

## Two-factor Authentication

Starting with version 1.7, OrchardCore is shipped with everything you need to secure your app with two-factor authentication. To use two-factor authentication, simply enable "Two-Factor Email Method" and/or "Two-Factor Authenticator App Method" features. You can configure the process based on your need by navigating to `Security` >> `Settings` >> `User Login`. Click on the "Two-Factor Authentication" tab and update the settings as needed.

## User Localization

The feature adds the ability to configure the culture per user from the admin UI.

This feature adds a `RequestCultureProvider` to retrieve the current user culture from its claims. This feature will set a new user claim with a `CultureClaimType` named "culture". It also has a culture option to fall back to other ASP.NET Request Culture Providers by simply setting the user culture to "Use site's culture" which will also be the selected default value.

## Custom Paths

If you want to specify custom paths to access the authentication related urls, you can change them by using this option in the appsettings.json:
Expand Down
5 changes: 5 additions & 0 deletions src/docs/releases/1.9.0.md
Skrypt marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -171,3 +171,8 @@ public class PersonController : Controller
```

In this example, (if the admin prefix remains the default "Admin") you can reach the Index action at `~/Admin/Person` (or by the route name `Person`), because its own action-level attribute took precedence. You can reach Create at `~/Admin/Person/Create` (route name `PersonCreate`) and Edit for the person whose identifier string is "john-doe" at `~/Admin/Person/john-doe` (route name `PersonEdit`).

## Users Module

Added a new User Localization feature that allows to be able to configure the culture per user from the admin UI.