diff --git a/aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/Localization/Resources/en.json b/aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/Localization/Resources/en.json index 4c6527801..b0f6e7cbc 100644 --- a/aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/Localization/Resources/en.json +++ b/aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/Localization/Resources/en.json @@ -51,6 +51,10 @@ "ProfileTab:Authenticator": "Authenticator", "ProfileTab:SecurityLog": "Security Log", "PhoneNumberChangedMessage": "Your mobile number has been successfully changed.", - "SecurityLogs": "Security Logs" + "SecurityLogs": "Security Logs", + "PasswordLogin": "Login with password", + "PhoneNumberLogin": "Login with phone", + "DisplayName:Abp.Account.EnablePhoneNumberLogin": "Authenticate with a phone number", + "Description:Abp.Account.EnablePhoneNumberLogin": "Indicates whether the server allows users to use mobile phone verification codes." } } \ No newline at end of file diff --git a/aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/Localization/Resources/zh-Hans.json b/aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/Localization/Resources/zh-Hans.json index 86f3b2b45..527f80541 100644 --- a/aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/Localization/Resources/zh-Hans.json +++ b/aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/Localization/Resources/zh-Hans.json @@ -51,6 +51,10 @@ "ProfileTab:Authenticator": "身份验证程序", "ProfileTab:SecurityLog": "安全日志", "PhoneNumberChangedMessage": "您的手机号码已成功更改.", - "SecurityLogs": "安全日志" + "SecurityLogs": "安全日志", + "PasswordLogin": "密码登录", + "PhoneNumberLogin": "验证码登录", + "DisplayName:Abp.Account.EnablePhoneNumberLogin": "使用手机验证码进行身份验证", + "Description:Abp.Account.EnablePhoneNumberLogin": "表示服务器是否允许用户使用手机验证码进行身份验证。" } } \ No newline at end of file diff --git a/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/AbpAccountWebModule.cs b/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/AbpAccountWebModule.cs index 0b2b446b2..1c2c05fc3 100644 --- a/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/AbpAccountWebModule.cs +++ b/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/AbpAccountWebModule.cs @@ -7,6 +7,7 @@ using LINGYUN.Abp.Identity.AspNetCore.QrCode; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using System; using Volo.Abp.Account.Localization; @@ -91,31 +92,37 @@ public class AbpAccountWebModule : AbpModule options.ScriptBundles .Configure(typeof(ManageModel).FullName, - configuration => + bundle => { // Client Proxies - configuration.AddFiles("/client-proxies/account-proxy.js"); + bundle.AddFiles("/client-proxies/account-proxy.js"); // Session - configuration.AddFiles("/Pages/Account/Components/ProfileManagementGroup/Session/Index.js"); + bundle.AddFiles("/Pages/Account/Components/ProfileManagementGroup/Session/Index.js"); // Authenticator - configuration.AddFiles("/Pages/Account/Components/ProfileManagementGroup/Authenticator/Index.js"); + bundle.AddFiles("/Pages/Account/Components/ProfileManagementGroup/Authenticator/Index.js"); // SecurityLog - configuration.AddFiles("/Pages/Account/Components/ProfileManagementGroup/SecurityLog/Index.js"); + bundle.AddFiles("/Pages/Account/Components/ProfileManagementGroup/SecurityLog/Index.js"); // TwoFactor - configuration.AddFiles("/Pages/Account/Components/ProfileManagementGroup/TwoFactor/Default.js"); + bundle.AddFiles("/Pages/Account/Components/ProfileManagementGroup/TwoFactor/Default.js"); // QrCode - configuration.AddContributors(typeof(QRCodeScriptContributor)); + bundle.AddContributors(typeof(QRCodeScriptContributor)); }); options.ScriptBundles .Configure(AccountBundles.Scripts.ChangePassword, bundle => { bundle.AddContributors(typeof(ChangePasswordScriptContributor)); }); + options.ScriptBundles + .Configure(typeof(Pages.Account.LoginModel).FullName, bundle => + { + bundle.AddFiles("/client-proxies/account-proxy.js"); + bundle.AddContributors(typeof(QRCodeScriptContributor)); + }); }); } } diff --git a/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Microsoft/AspNetCore/Mvc/ModelBinding/ModelStateExtensions.cs b/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Microsoft/AspNetCore/Mvc/ModelBinding/ModelStateExtensions.cs new file mode 100644 index 000000000..4f72500b8 --- /dev/null +++ b/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Microsoft/AspNetCore/Mvc/ModelBinding/ModelStateExtensions.cs @@ -0,0 +1,27 @@ +using System.Linq; + +namespace Microsoft.AspNetCore.Mvc.ModelBinding; + +public static class ModelStateExtensions +{ + public static void RemoveModelErrors(this ModelStateDictionary modelState, string modelName) + { + var keys = modelState.Keys + .Where(k => k.StartsWith(modelName + ".") || k == modelName) + .ToList(); + + foreach (var key in keys) + { + modelState.Remove(key); + } + } + + public static bool IsValidForModel(this ModelStateDictionary modelState, string modelName) + { + modelState.RemoveModelErrors(modelName); + + return modelState.Keys + .Where(k => k.StartsWith(modelName + ".") || k == modelName) + .All(key => modelState[key].ValidationState == ModelValidationState.Valid); + } +} diff --git a/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Pages/Account/Login.cshtml b/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Pages/Account/Login.cshtml index f69fe2973..3d9dd39fd 100644 --- a/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Pages/Account/Login.cshtml +++ b/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Pages/Account/Login.cshtml @@ -1,18 +1,21 @@ @page +@using LINGYUN.Abp.Account.Web.Pages.Account @using Microsoft.AspNetCore.Mvc.Localization @using Volo.Abp.Account.Localization @using Volo.Abp.Account.Settings -@using Volo.Abp.Account.Web.Pages.Account; +@using Volo.Abp.AspNetCore.Mvc.UI.Layout @using Volo.Abp.AspNetCore.Mvc.UI.Theming; @using Volo.Abp.Identity; -@using Volo.Abp.Settings +@using Volo.Abp.Settings; @model LINGYUN.Abp.Account.Web.Pages.Account.LoginModel @inject IHtmlLocalizer L @inject IThemeManager ThemeManager +@inject IPageLayout PageLayout @inject Volo.Abp.Settings.ISettingProvider SettingProvider @{ Layout = ThemeManager.CurrentTheme.GetAccountLayout(); + PageLayout.Content.Title = L["Login"].Value; } @section scripts @@ -22,84 +25,102 @@ } -
-
-

@L["Login"]

- @if (Model.EnableLocalLogin) - { -
-
- - - -
+
diff --git a/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Pages/Account/Login.cshtml.cs b/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Pages/Account/Login.cshtml.cs index eb331a83a..290362474 100644 --- a/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Pages/Account/Login.cshtml.cs +++ b/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Pages/Account/Login.cshtml.cs @@ -3,10 +3,12 @@ using LINGYUN.Abp.Account.Web.Models; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using System; using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.Diagnostics; using System.Linq; using System.Security.Claims; @@ -15,7 +17,7 @@ using Volo.Abp; using Volo.Abp.Account.Settings; using Volo.Abp.Account.Web; using Volo.Abp.Account.Web.Pages.Account; -using Volo.Abp.DependencyInjection; +using Volo.Abp.Auditing; using Volo.Abp.Identity; using Volo.Abp.Identity.AspNetCore; using Volo.Abp.Reflection; @@ -24,10 +26,10 @@ using Volo.Abp.Settings; using Volo.Abp.Validation; using static Volo.Abp.Account.Web.Pages.Account.LoginModel; using IdentityUser = Volo.Abp.Identity.IdentityUser; +using IIdentityUserRepository = LINGYUN.Abp.Identity.IIdentityUserRepository; namespace LINGYUN.Abp.Account.Web.Pages.Account; -//[ExposeServices(typeof(Volo.Abp.Account.Web.Pages.Account.LoginModel))] public class LoginModel : AccountPageModel { [HiddenInput] @@ -38,8 +40,15 @@ public class LoginModel : AccountPageModel [BindProperty(SupportsGet = true)] public string ReturnUrlHash { get; set; } - [BindProperty] - public LoginInputModel LoginInput { get; set; } + [HiddenInput] + [BindProperty(SupportsGet = true)] + public LoginType LoginType { get; set; } + + [BindProperty(Name = "PasswordLoginInput")] + public PasswordLoginInputModel PasswordLoginInput { get; set; } + + [BindProperty(Name = "PhoneLoginInput")] + public PhoneLoginInputModel PhoneLoginInput { get; set; } public bool EnableLocalLogin { get; set; } @@ -50,6 +59,7 @@ public class LoginModel : AccountPageModel public IEnumerable ExternalProviders { get; set; } public IEnumerable VisibleExternalProviders => ExternalProviders.Where(x => !x.DisplayName.IsNullOrWhiteSpace()); + protected IIdentityUserRepository UserRepository => LazyServiceProvider.LazyGetRequiredService(); protected IExternalProviderService ExternalProviderService { get; } protected IAuthenticationSchemeProvider SchemeProvider { get; } @@ -71,7 +81,9 @@ public class LoginModel : AccountPageModel public virtual async Task OnGetAsync() { - LoginInput = new LoginInputModel(); + LoginType = LoginType.Password; + PhoneLoginInput = new PhoneLoginInputModel(); + PasswordLoginInput = new PasswordLoginInputModel(); ExternalProviders = await GetExternalProviders(); @@ -85,24 +97,30 @@ public class LoginModel : AccountPageModel return Page(); } - public async virtual Task OnPostAsync(string action) + public async virtual Task OnPostPasswordLogin(string action) { - await CheckLocalLoginAsync(); + LoginType = LoginType.Password; - ValidateModel(); + await CheckLocalLoginAsync(); ExternalProviders = await GetExternalProviders(); EnableLocalLogin = await SettingProvider.IsTrueAsync(AccountSettingNames.EnableLocalLogin); + ModelState.RemoveModelErrors(nameof(PhoneLoginInput)); + if (!TryValidateModel(PasswordLoginInput, nameof(PasswordLoginInput))) + { + return Page(); + } + await ReplaceEmailToUsernameOfInputIfNeeds(); await IdentityOptions.SetAsync(); var result = await SignInManager.PasswordSignInAsync( - LoginInput.UserNameOrEmailAddress, - LoginInput.Password, - LoginInput.RememberMe, + PasswordLoginInput.UserNameOrEmailAddress, + PasswordLoginInput.Password, + PasswordLoginInput.RememberMe, true ); @@ -110,7 +128,7 @@ public class LoginModel : AccountPageModel { Identity = IdentitySecurityLogIdentityConsts.Identity, Action = result.ToIdentitySecurityLogAction(), - UserName = LoginInput.UserNameOrEmailAddress + UserName = PasswordLoginInput.UserNameOrEmailAddress }); if (result.RequiresTwoFactor) @@ -134,7 +152,7 @@ public class LoginModel : AccountPageModel } //TODO: Find a way of getting user's id from the logged in user and do not query it again like that! - var user = await GetIdentityUserAsync(LoginInput.UserNameOrEmailAddress); + var user = await GetIdentityUserAsync(PasswordLoginInput.UserNameOrEmailAddress); Debug.Assert(user != null, nameof(user) + " != null"); @@ -144,6 +162,54 @@ public class LoginModel : AccountPageModel return await RedirectSafelyAsync(ReturnUrl, ReturnUrlHash); } + public async virtual Task OnPostPhoneNumberLogin(string action) + { + LoginType = LoginType.PhoneNumber; + + await CheckLocalLoginAsync(); + + ExternalProviders = await GetExternalProviders(); + + EnableLocalLogin = await SettingProvider.IsTrueAsync(AccountSettingNames.EnableLocalLogin); + + ModelState.RemoveModelErrors(nameof(PasswordLoginInput)); + if (!TryValidateModel(PhoneLoginInput, nameof(PhoneLoginInput))) + { + return Page(); + } + + var user = await UserRepository.FindByPhoneNumberAsync(PhoneLoginInput.PhoneNumber); + if (user == null) + { + Logger.LogInformation("the user phone number is not registed!"); + Alerts.Danger(L["InvalidPhoneNumber"]); + return Page(); + } + + var result = await UserManager.VerifyTwoFactorTokenAsync(user, TokenOptions.DefaultPhoneProvider, PhoneLoginInput.Code); + if (!result) + { + Alerts.Danger(L["InvalidVerifyCode"]); + return Page(); + } + + await SignInManager.SignInAsync( + user, + PasswordLoginInput.RememberMe); + + await IdentitySecurityLogManager.SaveAsync(new IdentitySecurityLogContext() + { + Identity = IdentitySecurityLogIdentityConsts.IdentityTwoFactor, + Action = IdentitySecurityLogActionConsts.LoginSucceeded, + UserName = user.UserName + }); + + // Clear the dynamic claims cache. + await IdentityDynamicClaimsPrincipalContributorCache.ClearAsync(user.Id, user.TenantId); + + return await RedirectSafelyAsync(ReturnUrl, ReturnUrlHash); + } + public virtual async Task OnPostExternalLogin(string provider) { var redirectUrl = Url.Page("./Login", pageHandler: "ExternalLoginCallback", values: new { ReturnUrl, ReturnUrlHash }); @@ -267,15 +333,15 @@ public class LoginModel : AccountPageModel { returnUrl = ReturnUrl, returnUrlHash = ReturnUrlHash, - rememberMe = LoginInput.RememberMe + rememberMe = PasswordLoginInput.RememberMe })); } protected virtual async Task GetIdentityUserAsync(string userNameOrEmailAddress) { - return await UserManager.FindByNameAsync(LoginInput.UserNameOrEmailAddress) ?? - await UserManager.FindByEmailAsync(LoginInput.UserNameOrEmailAddress); + return await UserManager.FindByNameAsync(PasswordLoginInput.UserNameOrEmailAddress) ?? + await UserManager.FindByEmailAsync(PasswordLoginInput.UserNameOrEmailAddress); } protected async virtual Task> GetExternalProviders() @@ -316,24 +382,24 @@ public class LoginModel : AccountPageModel protected virtual async Task ReplaceEmailToUsernameOfInputIfNeeds() { - if (!ValidationHelper.IsValidEmailAddress(LoginInput.UserNameOrEmailAddress)) + if (!ValidationHelper.IsValidEmailAddress(PasswordLoginInput.UserNameOrEmailAddress)) { return; } - var userByUsername = await UserManager.FindByNameAsync(LoginInput.UserNameOrEmailAddress); + var userByUsername = await UserManager.FindByNameAsync(PasswordLoginInput.UserNameOrEmailAddress); if (userByUsername != null) { return; } - var userByEmail = await UserManager.FindByEmailAsync(LoginInput.UserNameOrEmailAddress); + var userByEmail = await UserManager.FindByEmailAsync(PasswordLoginInput.UserNameOrEmailAddress); if (userByEmail == null) { return; } - LoginInput.UserNameOrEmailAddress = userByEmail.UserName; + PasswordLoginInput.UserNameOrEmailAddress = userByEmail.UserName; } protected virtual async Task CheckLocalLoginAsync() @@ -352,8 +418,8 @@ public class LoginModel : AccountPageModel protected async virtual Task HandleUserNotAllowed() { - var notAllowedUser = await GetIdentityUserAsync(LoginInput.UserNameOrEmailAddress); - if (await UserManager.CheckPasswordAsync(notAllowedUser, LoginInput.Password)) + var notAllowedUser = await GetIdentityUserAsync(PasswordLoginInput.UserNameOrEmailAddress); + if (await UserManager.CheckPasswordAsync(notAllowedUser, PasswordLoginInput.Password)) { // û޸ if (notAllowedUser.ShouldChangePasswordOnNextLogin || await UserManager.ShouldPeriodicallyChangePasswordAsync(notAllowedUser)) @@ -371,7 +437,7 @@ public class LoginModel : AccountPageModel { returnUrl = ReturnUrl, returnUrlHash = ReturnUrlHash, - rememberMe = LoginInput.RememberMe + rememberMe = PasswordLoginInput.RememberMe }); } } @@ -385,3 +451,40 @@ public class LoginModel : AccountPageModel return Task.FromResult(Page()); } } + +public class PhoneLoginInputModel +{ + [Phone] + [Required] + [DynamicStringLength(typeof(IdentityUserConsts), nameof(IdentityUserConsts.MaxPhoneNumberLength))] + public string PhoneNumber { get; set; } + + [Required] + [StringLength(6)] + [Display(Name = "VerifyCode")] + public string Code { get; set; } + + public bool RememberMe { get; set; } +} + +public class PasswordLoginInputModel +{ + [Required] + [DynamicStringLength(typeof(IdentityUserConsts), nameof(IdentityUserConsts.MaxEmailLength))] + public string UserNameOrEmailAddress { get; set; } + + [Required] + [DynamicStringLength(typeof(IdentityUserConsts), nameof(IdentityUserConsts.MaxPasswordLength))] + [DataType(DataType.Password)] + [DisableAuditing] + public string Password { get; set; } + + public bool RememberMe { get; set; } +} + +public enum LoginType +{ + Password = 0, + PhoneNumber = 1, + QrCode = 2 +} diff --git a/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Pages/Account/Login.js b/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Pages/Account/Login.js new file mode 100644 index 000000000..efe347fb5 --- /dev/null +++ b/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Pages/Account/Login.js @@ -0,0 +1,54 @@ +$(function () { + let timer; + let countDown = 0; + var l = abp.localization.getResource('AbpAccount'); + var authService = labp.account.account; + + $("#SendVerifyCodeButton").click(function (e) { + const button = $(this); + e.preventDefault(); + + var isValid = $('#PhoneNumberForm').validate().element('#PhoneNumberInput'); + if (!isValid) { + return false; + } + + var input = $('#PhoneNumberForm').serializeFormToObject(); + + authService.sendPhoneSigninCode({ + phoneNumber: input.phoneLoginInput.phoneNumber, + }).then(function () { + countDown = 60; + timer = setInterval(function () { + button.prop('disabled', true); + button.text(`${countDown}`); + if (countDown === 0) { + clearInterval(timer); + button.prop('disabled', false); + button.text(l('SendVerifyCode')); + } + countDown--; + }, 1000); + }); + }); + + $("#PasswordVisibilityButton").click(function (e) { + let button = $(this); + let passwordInput = button.parent().find("input"); + if (!passwordInput) { + return; + } + + if (passwordInput.attr("type") === "password") { + passwordInput.attr("type", "text"); + } + else { + passwordInput.attr("type", "password"); + } + + let icon = button.find("i"); + if (icon) { + icon.toggleClass("fa-eye-slash").toggleClass("fa-eye"); + } + }); +});