diff --git a/modules/account/src/Volo.Abp.Account.Web/Pages/Account/AccountPageModel.cs b/modules/account/src/Volo.Abp.Account.Web/Pages/Account/AccountPageModel.cs index f229253c4a..ebd556f8e2 100644 --- a/modules/account/src/Volo.Abp.Account.Web/Pages/Account/AccountPageModel.cs +++ b/modules/account/src/Volo.Abp.Account.Web/Pages/Account/AccountPageModel.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Options; using Volo.Abp.Account.Localization; @@ -55,22 +54,4 @@ public abstract class AccountPageModel : AbpPageModel return exception.Message; } - - protected virtual async Task GetUserNameFromEmail(string email) - { - var userName = email.Split('@')[0]; - var existUser = await UserManager.FindByNameAsync(userName); - while (existUser != null) - { - var randomUserName = userName + RandomHelper.GetRandom(1000, 9999); - existUser = await UserManager.FindByNameAsync(randomUserName); - if (existUser == null) - { - userName = randomUserName; - break; - } - } - - return userName; - } } diff --git a/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Register.cshtml.cs b/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Register.cshtml.cs index 9e0a2c191b..c481715a16 100644 --- a/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Register.cshtml.cs +++ b/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Register.cshtml.cs @@ -102,7 +102,7 @@ public class RegisterModel : AccountPageModel return; } - var userName = await GetUserNameFromEmail(emailClaim.Value); + var userName = await UserManager.GetUserNameFromEmailAsync(emailClaim.Value); Input = new PostInput { UserName = userName, EmailAddress = emailClaim.Value }; } } @@ -128,7 +128,7 @@ public class RegisterModel : AccountPageModel } if (Input.UserName.IsNullOrWhiteSpace()) { - Input.UserName = await GetUserNameFromEmail(Input.EmailAddress); + Input.UserName = await UserManager.GetUserNameFromEmailAsync(Input.EmailAddress); } await RegisterExternalUserAsync(externalLoginInfo, Input.UserName, Input.EmailAddress); } diff --git a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityUserManager.cs b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityUserManager.cs index f8bfe5b603..a8e6a08ead 100644 --- a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityUserManager.cs +++ b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityUserManager.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Numerics; using System.Threading; using System.Threading.Tasks; using JetBrains.Annotations; @@ -82,7 +83,7 @@ public class IdentityUserManager : UserManager, IDomainService return await CreateAsync(user); } - + public async override Task DeleteAsync(IdentityUser user) { user.Claims.Clear(); @@ -397,4 +398,126 @@ public class IdentityUserManager : UserManager, IDomainService await UserRepository.UpdateOrganizationAsync(sourceOrganizationId, targetOrganizationId, CancellationToken); } + + public virtual async Task ValidateUserNameAsync(string userName, Guid? userId = null) + { + if (string.IsNullOrWhiteSpace(userName)) + { + return false; + } + + if (!string.IsNullOrEmpty(Options.User.AllowedUserNameCharacters) && userName.Any(c => !Options.User.AllowedUserNameCharacters.Contains(c))) + { + return false; + } + + var owner = await FindByNameAsync(userName); + if (owner != null && owner.Id != userId) + { + return false; + } + + return true; + } + + public virtual Task GetRandomUserNameAsync(int length) + { + var allowedUserNameCharacters = Options.User.AllowedUserNameCharacters; + if (allowedUserNameCharacters.IsNullOrWhiteSpace()) + { + allowedUserNameCharacters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+"; + } + + var randomUserName = string.Empty; + var random = new Random(); + while (randomUserName.Length < length) + { + randomUserName += allowedUserNameCharacters[random.Next(0, allowedUserNameCharacters.Length)]; + } + + return Task.FromResult(randomUserName); + } + + public virtual async Task GetUserNameFromEmailAsync(string email) + { + const int maxTryCount = 20; + var tryCount = 0; + + var userName = email.Split('@')[0]; + + if (await ValidateUserNameAsync(userName)) + { + // The username is valid. + return userName; + } + + if (Options.User.AllowedUserNameCharacters.IsNullOrWhiteSpace()) + { + // The AllowedUserNameCharacters is not set. So, we are generating a random username. + tryCount = 0; + do + { + var randomUserName = userName + RandomHelper.GetRandom(1000, 9999); + if ( await ValidateUserNameAsync(randomUserName)) + { + return randomUserName; + } + tryCount++; + } while (tryCount < maxTryCount); + } + else if (!userName.All(Options.User.AllowedUserNameCharacters.Contains)) + { + // The username contains not allowed characters. So, we are generating a random username. + do + { + var randomUserName = await GetRandomUserNameAsync(userName.Length); + if ( await ValidateUserNameAsync(randomUserName)) + { + return randomUserName; + } + tryCount++; + } while (tryCount < maxTryCount); + } + else if (Options.User.AllowedUserNameCharacters.Where(char.IsDigit).Distinct().Count() >= 4) + { + // The AllowedUserNameCharacters includes 4 numbers. So, we are generating 4 random numbers and appending to the username. + var numbers = Options.User.AllowedUserNameCharacters.Where(char.IsDigit).OrderBy(x => Guid.NewGuid()).Take(4).ToArray(); + var minArray = numbers.OrderBy(x => x).ToArray(); + if (minArray[0] == '0') + { + var secondItem = minArray[1]; + minArray[0] = secondItem; + minArray[1] = '0'; + } + var min = int.Parse(new string(minArray)); + var max = int.Parse(new string(numbers.OrderByDescending(x => x).ToArray())); + tryCount = 0; + do + { + var randomUserName = userName + RandomHelper.GetRandom(min, max); + if ( await ValidateUserNameAsync(randomUserName)) + { + return randomUserName; + } + tryCount++; + } while (tryCount < maxTryCount); + } + else + { + tryCount = 0; + do + { + // The AllowedUserNameCharacters does not include numbers. So, we are generating 4 random characters and appending to the username. + var randomUserName = userName + await GetRandomUserNameAsync(4); + if (await ValidateUserNameAsync(randomUserName)) + { + return randomUserName; + } + tryCount++; + } while (tryCount < maxTryCount); + } + + Logger.LogError($"Could not get a valid user name for the given email address: {email}, allowed characters: {Options.User.AllowedUserNameCharacters}, tried {maxTryCount} times."); + throw new AbpIdentityResultException(IdentityResult.Failed(new IdentityErrorDescriber().InvalidUserName(userName))); + } } diff --git a/modules/identity/test/Volo.Abp.Identity.Domain.Tests/Volo/Abp/Identity/IdentityUserManager_Tests.cs b/modules/identity/test/Volo.Abp.Identity.Domain.Tests/Volo/Abp/Identity/IdentityUserManager_Tests.cs index ec29d3d29a..da26de3e3d 100644 --- a/modules/identity/test/Volo.Abp.Identity.Domain.Tests/Volo/Abp/Identity/IdentityUserManager_Tests.cs +++ b/modules/identity/test/Volo.Abp.Identity.Domain.Tests/Volo/Abp/Identity/IdentityUserManager_Tests.cs @@ -2,8 +2,10 @@ using System.Collections.Generic; using System.Linq; using System.Security.Claims; +using System.Text.RegularExpressions; using System.Threading.Tasks; using Microsoft.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Options; @@ -378,6 +380,88 @@ public class IdentityUserManager_Tests : AbpIdentityDomainTestBase } } + [Fact] + public async Task ValidateUserNameAsync() + { + var result = await _identityUserManager.ValidateUserNameAsync("M_y+User-001@abp.io"); + result.ShouldBeTrue(); + + var user = CreateRandomUser(); + (await _identityUserManager.CreateAsync(user)).CheckErrors(); + + result = await _identityUserManager.ValidateUserNameAsync(user.UserName, user.Id); + result.ShouldBeTrue(); + + result = await _identityUserManager.ValidateUserNameAsync(user.UserName); + result.ShouldBeFalse(); + + result = await _identityUserManager.ValidateUserNameAsync("无效的字符"); + result.ShouldBeFalse(); + } + + [Fact] + public async Task GetRandomUserNameAsync() + { + _identityUserManager.Options.User.AllowedUserNameCharacters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+"; + var username = await _identityUserManager.GetRandomUserNameAsync(15); + username.Length.ShouldBe(15); + username.All(c => _identityUserManager.Options.User.AllowedUserNameCharacters.Contains(c)).ShouldBeTrue(); + + _identityUserManager.Options.User.AllowedUserNameCharacters = "abcdefghijklmnopqrstuvwxyz0123456789"; + username = await _identityUserManager.GetRandomUserNameAsync(15); + username.Length.ShouldBe(15); + username.All(c => _identityUserManager.Options.User.AllowedUserNameCharacters.Contains(c)).ShouldBeTrue(); + + _identityUserManager.Options.User.AllowedUserNameCharacters = "0123456789"; + username = await _identityUserManager.GetRandomUserNameAsync(15); + username.Length.ShouldBe(15); + username.All(c => _identityUserManager.Options.User.AllowedUserNameCharacters.Contains(c)).ShouldBeTrue(); + + _identityUserManager.Options.User.AllowedUserNameCharacters = null!; + username = await _identityUserManager.GetRandomUserNameAsync(15); + username.Length.ShouldBe(15); + username.All(c => "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+".Contains(c)).ShouldBeTrue(); + } + + [Fact] + public async Task GetUserNameFromEmailAsync() + { + _identityUserManager.Options.User.AllowedUserNameCharacters = "abcdefghijklmnopqrstuvwxyz0123456789"; + var username = await _identityUserManager.GetUserNameFromEmailAsync("Yönetici@abp.io"); + username.Length.ShouldBe("Yönetici".Length); //random username + username.All(c => "abcdefghijklmnopqrstuvwxyz0123456789".Contains(c)).ShouldBeTrue(); + + _identityUserManager.Options.User.AllowedUserNameCharacters = "abcdefghijklmnopqrstuvwxyz0123456789"; + username = await _identityUserManager.GetUserNameFromEmailAsync("admin@abp.io"); + username.Length.ShouldBe(9); //admin and random 4 numbers + username.ShouldContain("admin"); + Regex.IsMatch(username, @"\d{4}$").ShouldBeTrue(); + + _identityUserManager.Options.User.AllowedUserNameCharacters = "admin01234"; + username = await _identityUserManager.GetUserNameFromEmailAsync("admin@abp.io"); + username.Length.ShouldBe(9); //admin and random 4 numbers + username.ShouldContain("admin"); + Regex.IsMatch(username, @"[0-4]{3}$").ShouldBeTrue(); + + _identityUserManager.Options.User.AllowedUserNameCharacters = "abcdefghijklmnopqrstuvwxyz"; + username = await _identityUserManager.GetUserNameFromEmailAsync("admin@abp.io"); + username.Length.ShouldBe(9); //admin and random 4 characters + username.ShouldContain("admin"); + Regex.IsMatch(username, @"[a-z]{4}$").ShouldBeTrue(); + + _identityUserManager.Options.User.AllowedUserNameCharacters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + username = await _identityUserManager.GetUserNameFromEmailAsync("ADMIN@abp.io"); + username.Length.ShouldBe(9); //admin and random 4 characters + username.ShouldContain("ADMIN"); + Regex.IsMatch(username, @"[A-Z]{4}$").ShouldBeTrue(); + + _identityUserManager.Options.User.AllowedUserNameCharacters = null!; + username = await _identityUserManager.GetUserNameFromEmailAsync("admin@abp.io"); + username.Length.ShouldBe(9); //admin and random 4 numbers + username.ShouldContain("admin"); + Regex.IsMatch(username, @"[0-9]{4}$").ShouldBeTrue(); + } + private async Task CreateRandomDefaultRoleAsync() { await _identityRoleRepository.InsertAsync(