From 90a4c17a7dab2e0836a4950c91909a785c01be5a Mon Sep 17 00:00:00 2001 From: colin Date: Thu, 5 Jun 2025 20:17:35 +0800 Subject: [PATCH 1/4] fix(account): fixed user login link style - Align the login link with the language switch bar --- .../Bundling/AccountBundles.cs | 18 ++++++++++++++++++ .../Bundling/UserLoginLinkStyleContributor.cs | 14 ++++++++++++++ .../LINGYUN.Abp.Account.Web.csproj | 5 ++++- .../Toolbar/UserLoginLink/Default.cshtml | 8 ++++++++ .../Modules/_ViewImports.cshtml | 4 ++++ .../styles/user-login-link/fix-style.css | 3 +++ 6 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Bundling/AccountBundles.cs create mode 100644 aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Bundling/UserLoginLinkStyleContributor.cs create mode 100644 aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Modules/Account/Components/Toolbar/UserLoginLink/Default.cshtml create mode 100644 aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Modules/_ViewImports.cshtml create mode 100644 aspnet-core/modules/account/LINGYUN.Abp.Account.Web/wwwroot/styles/user-login-link/fix-style.css diff --git a/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Bundling/AccountBundles.cs b/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Bundling/AccountBundles.cs new file mode 100644 index 000000000..9e72afeb9 --- /dev/null +++ b/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Bundling/AccountBundles.cs @@ -0,0 +1,18 @@ +namespace LINGYUN.Abp.Account.Web.Bundling; + +public static class AccountBundles +{ + public static class Scripts + { + public const string Global = "Abp.Account"; + + public const string ChangePassword = Global + ".ChangePassword"; + } + + public static class Styles + { + public const string Global = "Abp.Account"; + + public const string UserLoginLink = Global + ".UserLoginLink"; + } +} diff --git a/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Bundling/UserLoginLinkStyleContributor.cs b/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Bundling/UserLoginLinkStyleContributor.cs new file mode 100644 index 000000000..79c159a85 --- /dev/null +++ b/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Bundling/UserLoginLinkStyleContributor.cs @@ -0,0 +1,14 @@ +using System.Threading.Tasks; +using Volo.Abp.AspNetCore.Mvc.UI.Bundling; + +namespace LINGYUN.Abp.Account.Web.Bundling; + +public class UserLoginLinkStyleContributor : BundleContributor +{ + public override Task ConfigureBundleAsync(BundleConfigurationContext context) + { + context.Files.Add("/styles/user-login-link/fix-style.css"); + + return Task.CompletedTask; + } +} diff --git a/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/LINGYUN.Abp.Account.Web.csproj b/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/LINGYUN.Abp.Account.Web.csproj index 61556a1ac..6b5fb6bfd 100644 --- a/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/LINGYUN.Abp.Account.Web.csproj +++ b/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/LINGYUN.Abp.Account.Web.csproj @@ -38,7 +38,6 @@ - @@ -48,4 +47,8 @@ + + + + diff --git a/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Modules/Account/Components/Toolbar/UserLoginLink/Default.cshtml b/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Modules/Account/Components/Toolbar/UserLoginLink/Default.cshtml new file mode 100644 index 000000000..5efd868b3 --- /dev/null +++ b/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Modules/Account/Components/Toolbar/UserLoginLink/Default.cshtml @@ -0,0 +1,8 @@ +@using Localization.Resources.AbpUi +@using Microsoft.AspNetCore.Mvc.Localization +@using LINGYUN.Abp.Account.Web.Bundling; +@inject IHtmlLocalizer L + + + +@L["Login"] diff --git a/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Modules/_ViewImports.cshtml b/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Modules/_ViewImports.cshtml new file mode 100644 index 000000000..c1da1f5f1 --- /dev/null +++ b/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Modules/_ViewImports.cshtml @@ -0,0 +1,4 @@ +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@addTagHelper *, Volo.Abp.AspNetCore.Mvc.UI +@addTagHelper *, Volo.Abp.AspNetCore.Mvc.UI.Bootstrap +@addTagHelper *, Volo.Abp.AspNetCore.Mvc.UI.Bundling \ No newline at end of file diff --git a/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/wwwroot/styles/user-login-link/fix-style.css b/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/wwwroot/styles/user-login-link/fix-style.css new file mode 100644 index 000000000..2347184f4 --- /dev/null +++ b/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/wwwroot/styles/user-login-link/fix-style.css @@ -0,0 +1,3 @@ +.fix-margin { + margin: 10px 0; +} From 18343b9f9ddebe8bf08b2f92d762c7d680b2a6e8 Mon Sep 17 00:00:00 2001 From: colin Date: Thu, 5 Jun 2025 20:20:10 +0800 Subject: [PATCH 2/4] feat(account): Rewrite the login view page - Add the view of modifying password during login - Add modification of password authentication policy for temporary storage of sessions - Change the name of the contributor to the personal information page --- .../AbpAccountAuthenticationTypes.cs | 6 + .../AbpAccountWebModule.cs | 32 +++- .../ChangePasswordScriptContributor.cs | 17 ++ .../Pages/Account/ChangePassword.cshtml | 46 +++++ .../Pages/Account/ChangePassword.cshtml.cs | 169 ++++++++++++++++++ .../Pages/Account/ChangePassword.js | 21 +++ .../Pages/Account/Login.cshtml | 92 ++++++++++ .../Pages/Account/Login.cshtml.cs | 166 +++++++++++++++++ ...cs => ProfileManagementPageContributor.cs} | 2 +- 9 files changed, 549 insertions(+), 2 deletions(-) create mode 100644 aspnet-core/modules/account/LINGYUN.Abp.Account.Web/AbpAccountAuthenticationTypes.cs create mode 100644 aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Bundling/ChangePasswordScriptContributor.cs create mode 100644 aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Pages/Account/ChangePassword.cshtml create mode 100644 aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Pages/Account/ChangePassword.cshtml.cs create mode 100644 aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Pages/Account/ChangePassword.js create mode 100644 aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Pages/Account/Login.cshtml create mode 100644 aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Pages/Account/Login.cshtml.cs rename aspnet-core/modules/account/LINGYUN.Abp.Account.Web/ProfileManagement/{SessionManagementPageContributor.cs => ProfileManagementPageContributor.cs} (96%) 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) { From c2d078ca8737e2da62a6023bce53f7db2be7e8b5 Mon Sep 17 00:00:00 2001 From: colin Date: Thu, 5 Jun 2025 20:22:08 +0800 Subject: [PATCH 3/4] fix(openiddict): Fixed openiddict login model - Modify base class source --- .../Account/TwoFactorSupportedLoginModel.cs | 53 ++++++++----------- 1 file changed, 23 insertions(+), 30 deletions(-) diff --git a/aspnet-core/modules/account/LINGYUN.Abp.Account.Web.OpenIddict/Pages/Account/TwoFactorSupportedLoginModel.cs b/aspnet-core/modules/account/LINGYUN.Abp.Account.Web.OpenIddict/Pages/Account/TwoFactorSupportedLoginModel.cs index 376372223..41ae2c17e 100644 --- a/aspnet-core/modules/account/LINGYUN.Abp.Account.Web.OpenIddict/Pages/Account/TwoFactorSupportedLoginModel.cs +++ b/aspnet-core/modules/account/LINGYUN.Abp.Account.Web.OpenIddict/Pages/Account/TwoFactorSupportedLoginModel.cs @@ -1,64 +1,57 @@ using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; -using System.Collections.Generic; +using System; using System.Threading.Tasks; using Volo.Abp.Account.Web; -using Volo.Abp.Account.Web.Pages.Account; using Volo.Abp.DependencyInjection; using Volo.Abp.Identity; +using Volo.Abp.MultiTenancy; using Volo.Abp.OpenIddict; using IdentityOptions = Microsoft.AspNetCore.Identity.IdentityOptions; namespace LINGYUN.Abp.Account.Web.OpenIddict.Pages.Account { - /// - /// 重写登录模型,实现双因素登录 - /// [Dependency(ReplaceServices = true)] - [ExposeServices(typeof(LoginModel), typeof(OpenIddictSupportedLoginModel))] - public class TwoFactorSupportedLoginModel : OpenIddictSupportedLoginModel + [ExposeServices( + typeof(LINGYUN.Abp.Account.Web.Pages.Account.LoginModel), + typeof(TwoFactorSupportedLoginModel))] + public class TwoFactorSupportedLoginModel : LINGYUN.Abp.Account.Web.Pages.Account.LoginModel { + protected AbpOpenIddictRequestHelper OpenIddictRequestHelper { get; } public TwoFactorSupportedLoginModel( IAuthenticationSchemeProvider schemeProvider, IOptions accountOptions, IOptions identityOptions, IdentityDynamicClaimsPrincipalContributorCache identityDynamicClaimsPrincipalContributorCache, AbpOpenIddictRequestHelper openIddictRequestHelper) - : base(schemeProvider, accountOptions, identityOptions, identityDynamicClaimsPrincipalContributorCache, openIddictRequestHelper) + : base(schemeProvider, accountOptions, identityOptions, identityDynamicClaimsPrincipalContributorCache) { + OpenIddictRequestHelper = openIddictRequestHelper; } - protected async override Task> GetExternalProviders() + public async override Task OnGetAsync() { - var providers = await base.GetExternalProviders(); + LoginInput = new LoginInputModel(); - foreach (var provider in providers) + var request = await OpenIddictRequestHelper.GetFromReturnUrlAsync(ReturnUrl); + if (request?.ClientId != null) { - var localizedDisplayName = L[provider.DisplayName]; - if (localizedDisplayName.ResourceNotFound) - { - localizedDisplayName = L["AuthenticationScheme:" + provider.DisplayName]; - } + // TODO: Find a proper cancel way. + // ShowCancelButton = true; - if (!localizedDisplayName.ResourceNotFound) + LoginInput.UserNameOrEmailAddress = request.LoginHint; + + //TODO: Reference AspNetCore MultiTenancy module and use options to get the tenant key! + var tenant = request.GetParameter(TenantResolverConsts.DefaultTenantKey)?.ToString(); + if (!string.IsNullOrEmpty(tenant)) { - provider.DisplayName = localizedDisplayName.Value; + CurrentTenant.Change(Guid.Parse(tenant)); + Response.Cookies.Append(TenantResolverConsts.DefaultTenantKey, tenant); } } - return providers; - } - - protected override Task TwoFactorLoginResultAsync() - { - // 重定向双因素认证页面 - return Task.FromResult(RedirectToPage("SendCode", new - { - returnUrl = ReturnUrl, - returnUrlHash = ReturnUrlHash, - rememberMe = LoginInput.RememberMe - })); + return await base.OnGetAsync(); } } } From 65134dd9ec63ca59aa3406eff7dd2e0f32b55aa6 Mon Sep 17 00:00:00 2001 From: colin Date: Thu, 5 Jun 2025 20:28:36 +0800 Subject: [PATCH 4/4] feat(vben5): Add authority server center login - add `oidc-client-ts` package - add `onlyOidc` environment variable - add oidc login and token refresh functions to `useAuthStore` - If `onlyOidc` is enabled to guide users to jump to the certification authority server - third-party login only provides certification center server --- apps/vben5/apps/app-antd/.env.development | 7 +++ apps/vben5/apps/app-antd/package.json | 1 + .../app-antd/src/adapter/request/index.ts | 21 ++----- .../apps/app-antd/src/auth/authService.ts | 54 +++++++++++++++++ .../src/locales/langs/en-US/page.json | 5 +- .../src/locales/langs/zh-CN/page.json | 5 +- .../apps/app-antd/src/router/routes/core.ts | 11 ++++ apps/vben5/apps/app-antd/src/store/auth.ts | 60 +++++++++++++++---- .../src/views/_core/authentication/login.vue | 31 +++++++++- .../authentication/third-party-login.vue | 26 ++++---- .../views/_core/fallback/login-callback.vue | 15 +++++ .../effects/hooks/src/use-app-config.ts | 2 + apps/vben5/packages/types/global.d.ts | 2 + apps/vben5/pnpm-workspace.yaml | 1 + 14 files changed, 196 insertions(+), 45 deletions(-) create mode 100644 apps/vben5/apps/app-antd/src/auth/authService.ts create mode 100644 apps/vben5/apps/app-antd/src/views/_core/fallback/login-callback.vue diff --git a/apps/vben5/apps/app-antd/.env.development b/apps/vben5/apps/app-antd/.env.development index d7204a13d..2e7b0f3d5 100644 --- a/apps/vben5/apps/app-antd/.env.development +++ b/apps/vben5/apps/app-antd/.env.development @@ -15,6 +15,13 @@ VITE_DEVTOOLS=false # 是否注入全局loading VITE_INJECT_APP_LOADING=true +# 是否仅允许OIDC登录 +VITE_GLOB_ONLY_OIDC=false +# 认证服务器 +VITE_GLOB_AUTHORITY="http://127.0.0.1:30001" +# 授权范围 VITE_GLOB_AUDIENCE="openid email address phone profile offline_access lingyun-abp-application" +# 客户端Id VITE_GLOB_CLIENT_ID=vue-admin-client +# 客户端密钥【生产环境请勿设置此值,建议启用仅允许OIDC登录,将使用授权码类型登录】 VITE_GLOB_CLIENT_SECRET=1q2w3e* diff --git a/apps/vben5/apps/app-antd/package.json b/apps/vben5/apps/app-antd/package.json index 392c09fdc..0f267f1d4 100644 --- a/apps/vben5/apps/app-antd/package.json +++ b/apps/vben5/apps/app-antd/package.json @@ -64,6 +64,7 @@ "@vueuse/core": "catalog:", "ant-design-vue": "catalog:", "dayjs": "catalog:", + "oidc-client-ts": "catalog:", "pinia": "catalog:", "vue": "catalog:", "vue-router": "catalog:" diff --git a/apps/vben5/apps/app-antd/src/adapter/request/index.ts b/apps/vben5/apps/app-antd/src/adapter/request/index.ts index ddafbf93c..e0c13d6a5 100644 --- a/apps/vben5/apps/app-antd/src/adapter/request/index.ts +++ b/apps/vben5/apps/app-antd/src/adapter/request/index.ts @@ -5,7 +5,7 @@ import { } from '@vben/request'; import { useAccessStore } from '@vben/stores'; -import { useOAuthError, useTokenApi } from '@abp/account'; +import { useOAuthError } from '@abp/account'; import { useAbpStore } from '@abp/core'; import { requestClient, useWrapperResult } from '@abp/request'; import { message } from 'ant-design-vue'; @@ -13,7 +13,6 @@ import { message } from 'ant-design-vue'; import { useAuthStore } from '#/store'; export function initRequestClient() { - const { refreshTokenApi } = useTokenApi(); /** * 重新认证逻辑 */ @@ -36,19 +35,11 @@ export function initRequestClient() { * 刷新token逻辑 */ async function doRefreshToken() { - const accessStore = useAccessStore(); - if (accessStore.refreshToken) { - try { - const { accessToken, tokenType, refreshToken } = await refreshTokenApi({ - refreshToken: accessStore.refreshToken, - }); - const newToken = `${tokenType} ${accessToken}`; - accessStore.setAccessToken(newToken); - accessStore.setRefreshToken(refreshToken); - return newToken; - } catch { - console.warn('The refresh token has expired or is unavailable.'); - } + const authStore = useAuthStore(); + try { + return await authStore.refreshSession(); + } catch { + console.warn('The refresh token has expired or is unavailable.'); } return ''; } diff --git a/apps/vben5/apps/app-antd/src/auth/authService.ts b/apps/vben5/apps/app-antd/src/auth/authService.ts new file mode 100644 index 000000000..110620bcd --- /dev/null +++ b/apps/vben5/apps/app-antd/src/auth/authService.ts @@ -0,0 +1,54 @@ +import { useAppConfig } from '@vben/hooks'; + +import { UserManager, WebStorageStateStore } from 'oidc-client-ts'; + +const { authority, audience, clientId, clientSecret } = useAppConfig( + import.meta.env, + import.meta.env.PROD, +); + +const userManager = new UserManager({ + authority, + client_id: clientId, + client_secret: clientSecret, + redirect_uri: `${window.location.origin}/signin-callback`, + response_type: 'code', + scope: audience, + post_logout_redirect_uri: `${window.location.origin}/`, + silent_redirect_uri: `${window.location.origin}/silent-renew.html`, + automaticSilentRenew: true, + loadUserInfo: true, + userStore: new WebStorageStateStore({ store: window.localStorage }), +}); + +export default { + async login() { + return userManager.signinRedirect(); + }, + + async logout() { + return userManager.signoutRedirect(); + }, + + async refreshToken() { + return userManager.signinSilent(); + }, + + async getAccessToken() { + const user = await userManager.getUser(); + return user?.access_token; + }, + + async isAuthenticated() { + const user = await userManager.getUser(); + return !!user && !user.expired; + }, + + async handleCallback() { + return userManager.signinRedirectCallback(); + }, + + async getUser() { + return userManager.getUser(); + }, +}; diff --git a/apps/vben5/apps/app-antd/src/locales/langs/en-US/page.json b/apps/vben5/apps/app-antd/src/locales/langs/en-US/page.json index 618a258c0..03b44db1f 100644 --- a/apps/vben5/apps/app-antd/src/locales/langs/en-US/page.json +++ b/apps/vben5/apps/app-antd/src/locales/langs/en-US/page.json @@ -4,7 +4,10 @@ "register": "Register", "codeLogin": "Code Login", "qrcodeLogin": "Qr Code Login", - "forgetPassword": "Forget Password" + "forgetPassword": "Forget Password", + "oidcLogin": "Open Connect", + "oidcLoginMessage": "Please click \"OK\" to jump to the Certification center for login.", + "processingLogin": "Processing Login..." }, "dashboard": { "title": "Dashboard", diff --git a/apps/vben5/apps/app-antd/src/locales/langs/zh-CN/page.json b/apps/vben5/apps/app-antd/src/locales/langs/zh-CN/page.json index 4cb67081c..2b3f24782 100644 --- a/apps/vben5/apps/app-antd/src/locales/langs/zh-CN/page.json +++ b/apps/vben5/apps/app-antd/src/locales/langs/zh-CN/page.json @@ -4,7 +4,10 @@ "register": "注册", "codeLogin": "验证码登录", "qrcodeLogin": "二维码登录", - "forgetPassword": "忘记密码" + "forgetPassword": "忘记密码", + "oidcLogin": "认证中心登录", + "oidcLoginMessage": "请点击确定跳转认证中心登录", + "processingLogin": "登录成功,正在跳转中..." }, "dashboard": { "title": "概览", diff --git a/apps/vben5/apps/app-antd/src/router/routes/core.ts b/apps/vben5/apps/app-antd/src/router/routes/core.ts index 7218da228..5952b5926 100644 --- a/apps/vben5/apps/app-antd/src/router/routes/core.ts +++ b/apps/vben5/apps/app-antd/src/router/routes/core.ts @@ -21,6 +21,17 @@ const fallbackNotFoundRoute: RouteRecordRaw = { /** 基本路由,这些路由是必须存在的 */ const coreRoutes: RouteRecordRaw[] = [ + { + component: () => import('#/views/_core/fallback/login-callback.vue'), + meta: { + hideInBreadcrumb: true, + hideInMenu: true, + hideInTab: true, + title: 'Processing login', + }, + name: 'OidcFallback', + path: '/signin-callback', + }, /** * 根路由 * 使用基础布局,作为所有页面的父级容器,子级就不必配置BasicLayout。 diff --git a/apps/vben5/apps/app-antd/src/store/auth.ts b/apps/vben5/apps/app-antd/src/store/auth.ts index 1f0c9c822..6773557c0 100644 --- a/apps/vben5/apps/app-antd/src/store/auth.ts +++ b/apps/vben5/apps/app-antd/src/store/auth.ts @@ -2,7 +2,7 @@ import type { TokenResult } from '@abp/account'; import type { Recordable, UserInfo } from '@vben/types'; -import { ref, watch } from 'vue'; +import { ref } from 'vue'; import { useRouter } from 'vue-router'; import { DEFAULT_HOME_PATH, LOGIN_PATH } from '@vben/constants'; @@ -20,11 +20,12 @@ import { notification } from 'ant-design-vue'; import { defineStore } from 'pinia'; import { useAbpConfigApi } from '#/api/core/useAbpConfigApi'; +import authService from '#/auth/authService'; import { $t } from '#/locales'; export const useAuthStore = defineStore('auth', () => { const { publish } = useEventBus(); - const { loginApi } = useTokenApi(); + const { loginApi, refreshTokenApi } = useTokenApi(); const { loginApi: qrcodeLoginApi } = useQrCodeLoginApi(); const { loginApi: phoneLoginApi } = usePhoneLoginApi(); const { getUserInfoApi } = useUserInfoApi(); @@ -37,16 +38,45 @@ export const useAuthStore = defineStore('auth', () => { const loginLoading = ref(false); - watch( - () => accessStore.accessToken, - (accessToken) => { - if (accessToken && !loginLoading.value) { - loginLoading.value = true; - fetchUserInfo(); - loginLoading.value = false; + async function refreshSession() { + if (await authService.getAccessToken()) { + const user = await authService.refreshToken(); + const newToken = `${user?.token_type} ${user?.access_token}`; + accessStore.setAccessToken(newToken); + if (user?.refresh_token) { + accessStore.setRefreshToken(user.refresh_token); } - }, - ); + return newToken; + } else { + const { accessToken, tokenType, refreshToken } = await refreshTokenApi({ + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + refreshToken: accessStore.refreshToken!, + }); + const newToken = `${tokenType} ${accessToken}`; + accessStore.setAccessToken(newToken); + accessStore.setRefreshToken(refreshToken); + return newToken; + } + } + + async function oidcLogin() { + await authService.login(); + } + + async function oidcCallback() { + try { + const user = await authService.handleCallback(); + return await _loginSuccess({ + accessToken: user.access_token, + tokenType: user.token_type, + refreshToken: user.refresh_token ?? '', + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + expiresIn: user.expires_in!, + }); + } finally { + loginLoading.value = false; + } + } async function qrcodeLogin( key: string, @@ -96,7 +126,10 @@ export const useAuthStore = defineStore('auth', () => { async function logout(redirect: boolean = true) { try { - // await logoutApi(); + if (await authService.getAccessToken()) { + accessStore.setAccessToken(null); + await authService.logout(); + } } catch { // 不做任何处理 } @@ -200,8 +233,11 @@ export const useAuthStore = defineStore('auth', () => { authLogin, phoneLogin, qrcodeLogin, + oidcLogin, + oidcCallback, fetchUserInfo, loginLoading, logout, + refreshSession, }; }); diff --git a/apps/vben5/apps/app-antd/src/views/_core/authentication/login.vue b/apps/vben5/apps/app-antd/src/views/_core/authentication/login.vue index 5550ce382..157db329a 100644 --- a/apps/vben5/apps/app-antd/src/views/_core/authentication/login.vue +++ b/apps/vben5/apps/app-antd/src/views/_core/authentication/login.vue @@ -7,9 +7,11 @@ import type { Recordable } from '@vben/types'; import { computed, nextTick, onMounted, useTemplateRef } from 'vue'; import { AuthenticationLogin, useVbenModal, z } from '@vben/common-ui'; +import { useAppConfig } from '@vben/hooks'; import { $t } from '@vben/locales'; import { useAbpStore, useSettings } from '@abp/core'; +import { Modal } from 'ant-design-vue'; import { useAbpConfigApi } from '#/api/core/useAbpConfigApi'; import { useAuthStore } from '#/store'; @@ -24,6 +26,8 @@ interface LoginInstance { defineOptions({ name: 'Login' }); +const { onlyOidc } = useAppConfig(import.meta.env, import.meta.env.PROD); + const abpStore = useAbpStore(); const authStore = useAuthStore(); @@ -34,6 +38,9 @@ const { getConfigApi } = useAbpConfigApi(); const login = useTemplateRef('login'); const formSchema = computed((): VbenFormSchema[] => { + if (onlyOidc) { + return []; + } let schemas: VbenFormSchema[] = [ { component: 'Input', @@ -75,6 +82,24 @@ const [ShouldChangePasswordModal, changePasswordModalApi] = useVbenModal({ connectedComponent: ShouldChangePassword, }); async function onInit() { + if (onlyOidc === true) { + setTimeout(() => { + Modal.confirm({ + centered: true, + title: $t('page.auth.oidcLogin'), + content: $t('page.auth.oidcLoginMessage'), + maskClosable: false, + closable: false, + cancelButtonProps: { + disabled: true, + }, + async onOk() { + await authStore.oidcLogin(); + }, + }); + }, 300); + return; + } const abpConfig = await getConfigApi(); abpStore.setApplication(abpConfig); nextTick(() => { @@ -83,6 +108,10 @@ async function onInit() { }); } async function onLogin(params: Recordable) { + if (onlyOidc === true) { + await authStore.oidcLogin(); + return; + } try { await authStore.authLogin(params); } catch (error) { @@ -115,7 +144,7 @@ onMounted(onInit);