diff --git a/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/AbpAccountAuthenticationTypes.cs b/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/AbpAccountAuthenticationTypes.cs new file mode 100644 index 000000000..4d85a1cf0 --- /dev/null +++ b/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/AbpAccountAuthenticationTypes.cs @@ -0,0 +1,6 @@ +namespace LINGYUN.Abp.Account.Web; + +public static class AbpAccountAuthenticationTypes +{ + public const string ShouldChangePassword = "Abp.Account.ShouldChangePassword"; +} 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 f1d791cc6..0b2b446b2 100644 --- a/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/AbpAccountWebModule.cs +++ b/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/AbpAccountWebModule.cs @@ -1,14 +1,21 @@ using LINGYUN.Abp.Account.Emailing; +using LINGYUN.Abp.Account.Web.Bundling; +using LINGYUN.Abp.Account.Web.Pages.Account; using LINGYUN.Abp.Account.Web.ProfileManagement; using LINGYUN.Abp.Identity; using LINGYUN.Abp.Identity.AspNetCore.QrCode; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.DependencyInjection; +using System; using Volo.Abp.Account.Localization; using Volo.Abp.Account.Web.Pages.Account; using Volo.Abp.Account.Web.ProfileManagement; using Volo.Abp.AspNetCore.Mvc.Localization; using Volo.Abp.AspNetCore.Mvc.UI.Bundling; using Volo.Abp.AspNetCore.Mvc.UI.Packages.QRCode; +using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Bundling; using Volo.Abp.AutoMapper; using Volo.Abp.Modularity; using Volo.Abp.Sms; @@ -53,17 +60,35 @@ public class AbpAccountWebModule : AbpModule { options.AddMaps(validate: true); }); + + context.Services + .AddAuthentication() + .AddCookie(AbpAccountAuthenticationTypes.ShouldChangePassword, options => + { + options.LoginPath = new PathString("/Account/Login"); + options.ExpireTimeSpan = TimeSpan.FromMinutes(5.0); + options.Events = new CookieAuthenticationEvents + { + OnValidatePrincipal = SecurityStampValidator.ValidatePrincipalAsync + }; + }); } private void ConfigureProfileManagementPage() { Configure(options => { - options.Contributors.Add(new SessionManagementPageContributor()); + options.Contributors.Add(new ProfileManagementPageContributor()); }); Configure(options => { + options.StyleBundles + .Add(AccountBundles.Styles.UserLoginLink, bundle => + { + bundle.AddContributors(typeof(UserLoginLinkStyleContributor)); + }); + options.ScriptBundles .Configure(typeof(ManageModel).FullName, configuration => @@ -86,6 +111,11 @@ public class AbpAccountWebModule : AbpModule // QrCode configuration.AddContributors(typeof(QRCodeScriptContributor)); }); + options.ScriptBundles + .Configure(AccountBundles.Scripts.ChangePassword, bundle => + { + bundle.AddContributors(typeof(ChangePasswordScriptContributor)); + }); }); } } diff --git a/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Bundling/ChangePasswordScriptContributor.cs b/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Bundling/ChangePasswordScriptContributor.cs new file mode 100644 index 000000000..779b22580 --- /dev/null +++ b/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Bundling/ChangePasswordScriptContributor.cs @@ -0,0 +1,17 @@ +using System.Threading.Tasks; +using Volo.Abp.AspNetCore.Mvc.UI.Bundling; +using Volo.Abp.AspNetCore.Mvc.UI.Packages.JQuery; +using Volo.Abp.Modularity; + +namespace LINGYUN.Abp.Account.Web.Bundling; + +[DependsOn(typeof(JQueryScriptContributor))] +public class ChangePasswordScriptContributor : BundleContributor +{ + public override Task ConfigureBundleAsync(BundleConfigurationContext context) + { + context.Files.Add("/Pages/Account/ChangePassword.js"); + + return Task.CompletedTask; + } +} diff --git a/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Pages/Account/ChangePassword.cshtml b/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Pages/Account/ChangePassword.cshtml new file mode 100644 index 000000000..09526ceeb --- /dev/null +++ b/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Pages/Account/ChangePassword.cshtml @@ -0,0 +1,46 @@ +@page +@using Volo.Abp.Account.Localization +@using Volo.Abp.Identity +@using Volo.Abp.Users +@using Microsoft.AspNetCore.Mvc.Localization +@using LINGYUN.Abp.Account.Web.Bundling; +@using LINGYUN.Abp.Account.Web.Pages.Account +@inject IHtmlLocalizer L +@model LINGYUN.Abp.Account.Web.Pages.Account.ChangePasswordModel + +
+
+

@L["ChangePassword"]

+
+
+ @if (!Model.HideOldPasswordInput) + { + +
+ + +
+ + +
+ } + +
+ + +
+
+ + +
+ + +
+ +
+ + +
+
+ + \ No newline at end of file diff --git a/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Pages/Account/ChangePassword.cshtml.cs b/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Pages/Account/ChangePassword.cshtml.cs new file mode 100644 index 000000000..e90aa16f6 --- /dev/null +++ b/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Pages/Account/ChangePassword.cshtml.cs @@ -0,0 +1,169 @@ +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; +using System; +using System.ComponentModel.DataAnnotations; +using System.Security.Principal; +using System.Threading.Tasks; +using Volo.Abp.Account.Web.Pages.Account; +using Volo.Abp.Auditing; +using Volo.Abp.Identity; +using Volo.Abp.Identity.AspNetCore; +using Volo.Abp.MultiTenancy; +using Volo.Abp.Validation; + +namespace LINGYUN.Abp.Account.Web.Pages.Account; + +public class UserInfoModel : IMultiTenant +{ + public Guid Id { get; set; } + + public Guid? TenantId { get; set; } +} + +public class ChangePasswordInputModel +{ + [Required] + [DynamicStringLength(typeof(IdentityUserConsts), nameof(IdentityUserConsts.MaxPasswordLength))] + [Display(Name = "DisplayName:CurrentPassword")] + [DataType(DataType.Password)] + [DisableAuditing] + public string CurrentPassword { get; set; } + + [Required] + [DynamicStringLength(typeof(IdentityUserConsts), nameof(IdentityUserConsts.MaxPasswordLength))] + [Display(Name = "DisplayName:NewPassword")] + [DataType(DataType.Password)] + [DisableAuditing] + public string NewPassword { get; set; } + + [Required] + [DynamicStringLength(typeof(IdentityUserConsts), nameof(IdentityUserConsts.MaxPasswordLength))] + [Display(Name = "DisplayName:NewPasswordConfirm")] + [DataType(DataType.Password)] + [DisableAuditing] + public string NewPasswordConfirm { get; set; } +} + +public class ChangePasswordModel : AccountPageModel +{ + [BindProperty] + public UserInfoModel UserInfo { get; set; } + + [BindProperty] + public ChangePasswordInputModel Input { get; set; } + + [BindProperty(SupportsGet = true)] + public string ReturnUrl { get; set; } + + [BindProperty(SupportsGet = true)] + public string ReturnUrlHash { get; set; } + + [BindProperty(SupportsGet = true)] + public bool RememberMe { get; set; } + + public bool HideOldPasswordInput { get; set; } + + public AbpSignInManager AbpSignInManager => LazyServiceProvider.LazyGetRequiredService(); + public IdentityDynamicClaimsPrincipalContributorCache IdentityDynamicClaimsPrincipalContributorCache => LazyServiceProvider.LazyGetRequiredService(); + + public async virtual Task OnGetAsync() + { + Input = new ChangePasswordInputModel(); + UserInfo = await GetCurrentUser(); + + if (UserInfo == null || UserInfo.TenantId != CurrentTenant.Id) + { + await HttpContext.SignOutAsync(AbpAccountAuthenticationTypes.ShouldChangePassword); + return RedirectToPage("/Login", new { ReturnUrl, ReturnUrlHash }); + } + + HideOldPasswordInput = (await UserManager.GetByIdAsync(UserInfo.Id)).PasswordHash == null; + return Page(); + } + + public async virtual Task OnPostAsync() + { + if (Input.CurrentPassword == Input.NewPassword) + { + Alerts.Warning(L["NewPasswordSameAsOld"]); + return Page(); + } + + var userInfo = await GetCurrentUser(); + if (userInfo != null) + { + if (userInfo.TenantId == CurrentTenant.Id) + { + try + { + await IdentityOptions.SetAsync(); + var user = await UserManager.GetByIdAsync(userInfo.Id); + if (user.PasswordHash == null) + { + (await UserManager.AddPasswordAsync(user, Input.NewPassword)).CheckErrors(); + } + else + { + (await UserManager.ChangePasswordAsync(user, Input.CurrentPassword, Input.NewPassword)).CheckErrors(); + } + + await IdentitySecurityLogManager.SaveAsync(new IdentitySecurityLogContext() + { + Identity = IdentitySecurityLogIdentityConsts.Identity, + Action = IdentitySecurityLogActionConsts.ChangePassword + }); + user.SetShouldChangePasswordOnNextLogin(false); + (await UserManager.UpdateAsync(user)).CheckErrors(); + + await HttpContext.SignOutAsync(AbpAccountAuthenticationTypes.ShouldChangePassword); + + await SignInManager.SignInAsync(user, RememberMe); + + await IdentitySecurityLogManager.SaveAsync(new IdentitySecurityLogContext() + { + Identity = IdentitySecurityLogIdentityConsts.IdentityExternal, + Action = IdentitySecurityLogActionConsts.LoginSucceeded, + UserName = user.UserName + }); + await IdentityDynamicClaimsPrincipalContributorCache.ClearAsync(user.Id, user.TenantId); + return await RedirectSafelyAsync(ReturnUrl, ReturnUrlHash); + } + catch (Exception ex) + { + Alerts.Warning(GetLocalizeExceptionMessage(ex)); + return Page(); + } + } + } + + await HttpContext.SignOutAsync(AbpAccountAuthenticationTypes.ShouldChangePassword); + + return RedirectToPage("/Login", new { ReturnUrl, ReturnUrlHash }); + } + + protected async virtual Task GetCurrentUser() + { + var result = await HttpContext.AuthenticateAsync(AbpAccountAuthenticationTypes.ShouldChangePassword); + + var userId = result?.Principal?.FindUserId(); + if (!userId.HasValue) + { + return null; + } + + var tenantId = result.Principal.FindTenantId(); + using (CurrentTenant.Change(tenantId, null)) + { + var identityUser = await UserManager.FindByIdAsync(userId.Value.ToString()); + return identityUser == null + ? null + : new UserInfoModel() + { + Id = identityUser.Id, + TenantId = identityUser.TenantId + }; + } + } +} diff --git a/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Pages/Account/ChangePassword.js b/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Pages/Account/ChangePassword.js new file mode 100644 index 000000000..24804e155 --- /dev/null +++ b/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Pages/Account/ChangePassword.js @@ -0,0 +1,21 @@ +$(function () { + $(".password-visibility-button").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"); + } + }); +}); 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 new file mode 100644 index 000000000..f1e346cad --- /dev/null +++ b/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Pages/Account/Login.cshtml @@ -0,0 +1,92 @@ +@page +@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.Theming; +@using Volo.Abp.Identity; +@using Volo.Abp.Settings +@model LINGYUN.Abp.Account.Web.Pages.Account.LoginModel +@inject IHtmlLocalizer L +@inject IThemeManager ThemeManager +@inject Volo.Abp.Settings.ISettingProvider SettingProvider + +@{ + Layout = ThemeManager.CurrentTheme.GetAccountLayout(); +} + +@section scripts +{ + + + +} + +
+
+

@L["Login"]

+ @if (await SettingProvider.IsTrueAsync(AccountSettingNames.IsSelfRegistrationEnabled)) + { + + @L["AreYouANewUser"] + @L["Register"] + + } + @if (Model.EnableLocalLogin) + { +
+
+ + + +
+ +
+ +
+ + +
+ +
+ + + + + + @L["ForgotPassword"] + + +
+ @L["Login"] + @if (Model.ShowCancelButton) + { + @L["Cancel"] + } +
+
+ } + + @if (Model.VisibleExternalProviders.Any()) + { +
+
@L["OrLoginWith"]
+
+ @foreach (var provider in Model.VisibleExternalProviders) + { + + } +
+
+ } + + @if (!Model.EnableLocalLogin && !Model.VisibleExternalProviders.Any()) + { +
+ @L["InvalidLoginRequest"] + @L["ThereAreNoLoginSchemesConfiguredForThisClient"] +
+ } + +
+
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 new file mode 100644 index 000000000..4c8c28d64 --- /dev/null +++ b/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Pages/Account/Login.cshtml.cs @@ -0,0 +1,166 @@ +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using Volo.Abp.Account.Settings; +using Volo.Abp.Account.Web; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Identity; +using Volo.Abp.Identity.AspNetCore; +using Volo.Abp.Security.Claims; +using Volo.Abp.Settings; +using IdentityUser = Volo.Abp.Identity.IdentityUser; + +namespace LINGYUN.Abp.Account.Web.Pages.Account; + +[ExposeServices(typeof(Volo.Abp.Account.Web.Pages.Account.LoginModel))] +public class LoginModel : Volo.Abp.Account.Web.Pages.Account.LoginModel +{ + public LoginModel( + IAuthenticationSchemeProvider schemeProvider, + IOptions accountOptions, + IOptions identityOptions, + IdentityDynamicClaimsPrincipalContributorCache identityDynamicClaimsPrincipalContributorCache) + : base(schemeProvider, accountOptions, identityOptions, identityDynamicClaimsPrincipalContributorCache) + { + } + + public async override Task OnPostAsync(string action) + { + await CheckLocalLoginAsync(); + + ValidateModel(); + + ExternalProviders = await GetExternalProviders(); + + EnableLocalLogin = await SettingProvider.IsTrueAsync(AccountSettingNames.EnableLocalLogin); + + await ReplaceEmailToUsernameOfInputIfNeeds(); + + await IdentityOptions.SetAsync(); + + var result = await SignInManager.PasswordSignInAsync( + LoginInput.UserNameOrEmailAddress, + LoginInput.Password, + LoginInput.RememberMe, + true + ); + + await IdentitySecurityLogManager.SaveAsync(new IdentitySecurityLogContext() + { + Identity = IdentitySecurityLogIdentityConsts.Identity, + Action = result.ToIdentitySecurityLogAction(), + UserName = LoginInput.UserNameOrEmailAddress + }); + + if (result.RequiresTwoFactor) + { + return await TwoFactorLoginResultAsync(); + } + + if (result.IsLockedOut) + { + Alerts.Warning(L["UserLockedOutMessage"]); + return Page(); + } + + if (result.IsNotAllowed) + { + var notAllowedUser = await GetIdentityUserAsync(LoginInput.UserNameOrEmailAddress); + if (await UserManager.CheckPasswordAsync(notAllowedUser, LoginInput.Password)) + { + // Óû§±ØÐëÐÞ¸ÄÃÜÂë + if (notAllowedUser.ShouldChangePasswordOnNextLogin || await UserManager.ShouldPeriodicallyChangePasswordAsync(notAllowedUser)) + { + var changePwdIdentity = new ClaimsIdentity(AbpAccountAuthenticationTypes.ShouldChangePassword); + changePwdIdentity.AddClaim(new Claim(AbpClaimTypes.UserId, notAllowedUser.Id.ToString())); + if (notAllowedUser.TenantId.HasValue) + { + changePwdIdentity.AddClaim(new Claim(AbpClaimTypes.TenantId, notAllowedUser.TenantId.ToString())); + } + + await HttpContext.SignInAsync(AbpAccountAuthenticationTypes.ShouldChangePassword, new ClaimsPrincipal(changePwdIdentity)); + + return RedirectToPage("ChangePassword", new + { + returnUrl = ReturnUrl, + returnUrlHash = ReturnUrlHash, + rememberMe = LoginInput.RememberMe + }); + } + } + + Alerts.Warning(L["LoginIsNotAllowed"]); + return Page(); + } + + if (!result.Succeeded) + { + Alerts.Danger(L["InvalidUserNameOrPassword"]); + return Page(); + } + + //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); + + Debug.Assert(user != null, nameof(user) + " != null"); + + // Clear the dynamic claims cache. + await IdentityDynamicClaimsPrincipalContributorCache.ClearAsync(user.Id, user.TenantId); + + return await RedirectSafelyAsync(ReturnUrl, ReturnUrlHash); + } + + protected override Task TwoFactorLoginResultAsync() + { + // ÖØ¶¨ÏòË«ÒòËØÈÏÖ¤Ò³Ãæ + return Task.FromResult(RedirectToPage("SendCode", new + { + returnUrl = ReturnUrl, + returnUrlHash = ReturnUrlHash, + rememberMe = LoginInput.RememberMe + })); + } + + protected virtual async Task GetIdentityUserAsync(string userNameOrEmailAddress) + { + return await UserManager.FindByNameAsync(LoginInput.UserNameOrEmailAddress) ?? + await UserManager.FindByEmailAsync(LoginInput.UserNameOrEmailAddress); + } + + protected async override Task> GetExternalProviders() + { + var schemes = await SchemeProvider.GetAllSchemesAsync(); + + var providers = schemes + .Where(x => x.DisplayName != null || x.Name.Equals(AccountOptions.WindowsAuthenticationSchemeName, StringComparison.OrdinalIgnoreCase)) + .Select(x => new ExternalProviderModel + { + DisplayName = x.DisplayName, + AuthenticationScheme = x.Name + }) + .ToList(); + + foreach (var provider in providers) + { + var localizedDisplayName = L[provider.DisplayName]; + if (localizedDisplayName.ResourceNotFound) + { + localizedDisplayName = L["AuthenticationScheme:" + provider.DisplayName]; + } + + if (!localizedDisplayName.ResourceNotFound) + { + provider.DisplayName = localizedDisplayName.Value; + } + } + + return providers; + } +} diff --git a/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/ProfileManagement/SessionManagementPageContributor.cs b/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/ProfileManagement/ProfileManagementPageContributor.cs similarity index 96% rename from aspnet-core/modules/account/LINGYUN.Abp.Account.Web/ProfileManagement/SessionManagementPageContributor.cs rename to aspnet-core/modules/account/LINGYUN.Abp.Account.Web/ProfileManagement/ProfileManagementPageContributor.cs index b653d8bf9..beb3200cf 100644 --- a/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/ProfileManagement/SessionManagementPageContributor.cs +++ b/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/ProfileManagement/ProfileManagementPageContributor.cs @@ -10,7 +10,7 @@ using Volo.Abp.Account.Web.ProfileManagement; namespace LINGYUN.Abp.Account.Web.ProfileManagement; -public class SessionManagementPageContributor : IProfileManagementPageContributor +public class ProfileManagementPageContributor : IProfileManagementPageContributor { public virtual Task ConfigureAsync(ProfileManagementPageCreationContext context) {