diff --git a/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/IAccountAppService.cs b/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/IAccountAppService.cs index 74d6bb3f7a..4365969dc6 100644 --- a/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/IAccountAppService.cs +++ b/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/IAccountAppService.cs @@ -7,5 +7,9 @@ namespace Volo.Abp.Account public interface IAccountAppService : IApplicationService { Task RegisterAsync(RegisterDto input); + + Task SendPasswordResetCodeAsync(SendPasswordResetCodeDto input); + + Task ResetPasswordAsync(ResetPasswordDto input); } -} \ No newline at end of file +} diff --git a/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/en.json b/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/en.json index 2940fff63b..e9215d3f5a 100644 --- a/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/en.json +++ b/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/en.json @@ -44,6 +44,15 @@ "LoggedOutTitle": "Signed Out", "LoggedOutText": "You have been signed out and you will be redirected soon.", "ReturnToText": "Click here to redirect to {0}", - "OrLoginWith": "Or login with;" + "OrLoginWith": "Or login with;", + "ForgotPassword": "Forgot password?", + "SendPasswordResetLink_Information": "A password reset link will be sent to your email to reset your password. If you don't get an email within a few minutes, please re-try.", + "PasswordResetMailSentMessage": "Account recovery email sent to your e-mail address. If you don't see this email in your inbox within 15 minutes, look for it in your junk mail folder. If you find it there, please mark it as -Not Junk-. ", + "ResetPassword": "Reset Password", + "ConfirmPassword": "Confirm (repeat) the password", + "ResetPassword_Information": "Please enter your new password.", + "YourPasswordIsSuccessfullyReset": "Your password is successfully reset.", + "GoToTheApplication": "Go to the application", + "BackToLogin": "Back to login" } } diff --git a/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/sl.json b/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/sl.json index d95e591f8a..747cc752c7 100644 --- a/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/sl.json +++ b/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/sl.json @@ -40,6 +40,14 @@ "DisplayName:Abp.Account.IsSelfRegistrationEnabled": "Je lastna registracija uporabnika omogočena", "Description:Abp.Account.IsSelfRegistrationEnabled": "Ali lahko uporabnik sam registrira račun.", "DisplayName:Abp.Account.EnableLocalLogin": "Avtenticirajte se z lokalnim računom", - "Description:Abp.Account.EnableLocalLogin": "Označuje, ali bo strežnik uporabnikom omogočil avtentikacijo z lokalnim računom." + "Description:Abp.Account.EnableLocalLogin": "Označuje, ali bo strežnik uporabnikom omogočil avtentikacijo z lokalnim računom.", + "ForgotPassword": "Ste pozabili geslo?", + "SendPasswordResetLink_Information": "Na vaš e-poštni naslov bo poslana povezava za ponastavitev gesla za ponastavitev gesla. Če v nekaj minutah ne dobite e-poštnega sporočila, poskusite znova.", + "PasswordResetMailSentMessage": "E-poštno sporočilo za obnovitev računa je bilo poslano na vaš e-poštni naslov. Če v 15 minutah tega e-poštnega sporočila ne vidite v mapi »Prejeto«, ga poiščite v mapi z neželeno pošto. Če ga najdete tam, ga prosimo označite kot -Ni neželeno-. ", + "ResetPassword": "Ponastavitev gesla", + "ConfirmPassword": "Potrditev (ponovitev) gesla", + "ResetPassword_Information": "Prosimo vnesite vaše novo geslo.", + "YourPasswordIsSuccessfullyReset": "Vaše geslo je uspešno ponastavljeno.", + "BackToLogin": "Nazaj na prijavo" } } diff --git a/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/tr.json b/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/tr.json index 3938a34984..bbb2ac3638 100644 --- a/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/tr.json +++ b/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/tr.json @@ -43,6 +43,15 @@ "Description:Abp.Account.EnableLocalLogin": "Sunucunun, kullanıcıların yerel bir hesapla kimlik doğrulamasına izin verip vermeyeceğini belirtir.", "LoggedOutTitle": "Çıkış Yaptınız", "LoggedOutText": "Çıkış yaptınız ve birazdan yönlendirileceksiniz.", - "ReturnToText": "{0} uygulamasına dönmek için tıklayın." + "ReturnToText": "{0} uygulamasına dönmek için tıklayın.", + "ForgotPassword": "Şifremi unuttum", + "SendPasswordResetLink_Information": "E-posta adresinize bir şifre sıfırlama bağlantısı gönderilecektir. Birkaç dakika içerisinde bir e-posta almazsanız lütfen tekrar deneyin.", + "PasswordResetMailSentMessage": "E-posta adresinize bir şifre sıfırlama bağlantısı gönderilmiştir. Lütfen e-posta adresinizi kontrol ediniz. Eğer 15 dakika içinde, bu e-postayı gelen kutusunda bulamazsanız, gereksiz veya istenmeyen e-posta kutularına bakınız.", + "ResetPassword": "Şifre Yenileme", + "ConfirmPassword": "Şifre (tekrar)", + "ResetPassword_Information": "Lütfen yeni şifrenizi belirleyin.", + "YourPasswordIsSuccessfullyReset": "Şifreniz başarıyla sıfırlandı.", + "GoToTheApplication": "Uygulamaya git", + "BackToLogin": "Girişe dön" } } diff --git a/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/zh-Hans.json b/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/zh-Hans.json index 49a4604372..a7be7fd0b8 100644 --- a/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/zh-Hans.json +++ b/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/zh-Hans.json @@ -43,6 +43,15 @@ "Description:Abp.Account.EnableLocalLogin": "服务器是否将允许用户使用本地帐户进行身份验证。", "LoggedOutTitle": "注销", "LoggedOutText": "你已成功注销并将马上返回.", - "ReturnToText": "点击此处返回到 {0}" + "ReturnToText": "点击此处返回到 {0}", + "ForgotPassword": "忘记密码?", + "SendPasswordResetLink_Information": "密码重置链接将发送到您的电子邮件以重置密码. 如果您在几分钟内没有收到电子邮件,请重试.", + "PasswordResetMailSentMessage": "帐户恢复电子邮件已发送到您的电子邮件地址. 如果您在15分钟内未在收件箱中看到此电子邮件,请检查垃圾邮件,并标记为非垃圾邮件.", + "ResetPassword": "重设密码", + "ConfirmPassword": "确认(重复)密码", + "ResetPassword_Information": "请输入您的新密码.", + "YourPasswordIsSuccessfullyReset": "您的密码已经被重置成功.", + "GoToTheApplication": "转到应用程序", + "BackToLogin": "返回登录" } } diff --git a/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/ResetPasswordDto.cs b/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/ResetPasswordDto.cs new file mode 100644 index 0000000000..2423e6c778 --- /dev/null +++ b/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/ResetPasswordDto.cs @@ -0,0 +1,18 @@ +using System; +using System.ComponentModel.DataAnnotations; +using Volo.Abp.Auditing; + +namespace Volo.Abp.Account +{ + public class ResetPasswordDto + { + public Guid UserId { get; set; } + + [Required] + public string ResetToken { get; set; } + + [Required] + [DisableAuditing] + public string Password { get; set; } + } +} diff --git a/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/SendPasswordResetCodeDto.cs b/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/SendPasswordResetCodeDto.cs new file mode 100644 index 0000000000..c4fcea4eec --- /dev/null +++ b/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/SendPasswordResetCodeDto.cs @@ -0,0 +1,21 @@ +using System.ComponentModel.DataAnnotations; +using Volo.Abp.Identity; +using Volo.Abp.Validation; + +namespace Volo.Abp.Account +{ + public class SendPasswordResetCodeDto + { + [Required] + [EmailAddress] + [DynamicStringLength(typeof(IdentityUserConsts), nameof(IdentityUserConsts.MaxEmailLength))] + public string Email { get; set; } + + [Required] + public string AppName { get; set; } + + public string ReturnUrl { get; set; } + + public string ReturnUrlHash { get; set; } + } +} diff --git a/modules/account/src/Volo.Abp.Account.Application/Volo.Abp.Account.Application.csproj b/modules/account/src/Volo.Abp.Account.Application/Volo.Abp.Account.Application.csproj index bdcf0c9e8d..5ee4aa8697 100644 --- a/modules/account/src/Volo.Abp.Account.Application/Volo.Abp.Account.Application.csproj +++ b/modules/account/src/Volo.Abp.Account.Application/Volo.Abp.Account.Application.csproj @@ -11,11 +11,17 @@ + + + + + + diff --git a/modules/account/src/Volo.Abp.Account.Application/Volo/Abp/Account/AbpAccountApplicationModule.cs b/modules/account/src/Volo.Abp.Account.Application/Volo/Abp/Account/AbpAccountApplicationModule.cs index 1f2004ae7e..2130c52a12 100644 --- a/modules/account/src/Volo.Abp.Account.Application/Volo/Abp/Account/AbpAccountApplicationModule.cs +++ b/modules/account/src/Volo.Abp.Account.Application/Volo/Abp/Account/AbpAccountApplicationModule.cs @@ -1,7 +1,8 @@ -using Volo.Abp.Identity; +using Volo.Abp.Emailing; +using Volo.Abp.Identity; using Volo.Abp.Modularity; -using Volo.Abp.UI.Navigation.Urls; using Volo.Abp.UI.Navigation; +using Volo.Abp.UI.Navigation.Urls; using Volo.Abp.VirtualFileSystem; namespace Volo.Abp.Account @@ -9,7 +10,8 @@ namespace Volo.Abp.Account [DependsOn( typeof(AbpAccountApplicationContractsModule), typeof(AbpIdentityApplicationModule), - typeof(AbpUiNavigationModule) + typeof(AbpUiNavigationModule), + typeof(AbpEmailingModule) )] public class AbpAccountApplicationModule : AbpModule { @@ -19,6 +21,11 @@ namespace Volo.Abp.Account { options.FileSets.AddEmbedded(); }); + + Configure(options => + { + options.Applications["MVC"].Urls[AccountUrlNames.PasswordReset] = "Account/ResetPassword"; + }); } } -} \ No newline at end of file +} diff --git a/modules/account/src/Volo.Abp.Account.Application/Volo/Abp/Account/AccountAppService.cs b/modules/account/src/Volo.Abp.Account.Application/Volo/Abp/Account/AccountAppService.cs index e2e4206b81..066ce47f64 100644 --- a/modules/account/src/Volo.Abp.Account.Application/Volo/Abp/Account/AccountAppService.cs +++ b/modules/account/src/Volo.Abp.Account.Application/Volo/Abp/Account/AccountAppService.cs @@ -1,6 +1,7 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Identity; +using Volo.Abp.Account.Emailing; using Volo.Abp.Account.Settings; using Volo.Abp.Application.Services; using Volo.Abp.Identity; @@ -12,12 +13,18 @@ namespace Volo.Abp.Account { protected IIdentityRoleRepository RoleRepository { get; } protected IdentityUserManager UserManager { get; } + protected IAccountEmailer AccountEmailer { get; } + protected IdentitySecurityLogManager IdentitySecurityLogManager { get; } public AccountAppService( IdentityUserManager userManager, - IIdentityRoleRepository roleRepository) + IIdentityRoleRepository roleRepository, + IAccountEmailer accountEmailer, + IdentitySecurityLogManager identitySecurityLogManager) { RoleRepository = roleRepository; + AccountEmailer = accountEmailer; + IdentitySecurityLogManager = identitySecurityLogManager; UserManager = userManager; } @@ -35,6 +42,37 @@ namespace Volo.Abp.Account return ObjectMapper.Map(user); } + public virtual async Task SendPasswordResetCodeAsync(SendPasswordResetCodeDto input) + { + var user = await GetUserByEmail(input.Email); + var resetToken = await UserManager.GeneratePasswordResetTokenAsync(user); + await AccountEmailer.SendPasswordResetLinkAsync(user, resetToken, input.AppName, input.ReturnUrl, input.ReturnUrlHash); + } + + public virtual async Task ResetPasswordAsync(ResetPasswordDto input) + { + var user = await UserManager.GetByIdAsync(input.UserId); + (await UserManager.ResetPasswordAsync(user, input.ResetToken, input.Password)).CheckErrors(); + + await IdentitySecurityLogManager.SaveAsync(new IdentitySecurityLogContext + { + Identity = IdentitySecurityLogIdentityConsts.Identity, + Action = IdentitySecurityLogActionConsts.ChangePassword + }); + } + + protected virtual async Task GetUserByEmail(string email) + { + var user = await UserManager.FindByEmailAsync(email); + if (user == null) + { + throw new BusinessException("Volo.Account:InvalidEmailAddress") + .WithData("Email", email); + } + + return user; + } + protected virtual async Task CheckSelfRegistrationAsync() { if (!await SettingProvider.IsTrueAsync(AccountSettingNames.IsSelfRegistrationEnabled)) @@ -43,4 +81,4 @@ namespace Volo.Abp.Account } } } -} \ No newline at end of file +} diff --git a/modules/account/src/Volo.Abp.Account.Application/Volo/Abp/Account/AccountUrlNames.cs b/modules/account/src/Volo.Abp.Account.Application/Volo/Abp/Account/AccountUrlNames.cs new file mode 100644 index 0000000000..9ea1050ae4 --- /dev/null +++ b/modules/account/src/Volo.Abp.Account.Application/Volo/Abp/Account/AccountUrlNames.cs @@ -0,0 +1,7 @@ +namespace Volo.Abp.Account +{ + public static class AccountUrlNames + { + public const string PasswordReset = "Abp.Account.PasswordReset"; + } +} diff --git a/modules/account/src/Volo.Abp.Account.Application/Volo/Abp/Account/Emailing/AccountEmailer.cs b/modules/account/src/Volo.Abp.Account.Application/Volo/Abp/Account/Emailing/AccountEmailer.cs new file mode 100644 index 0000000000..baa762c840 --- /dev/null +++ b/modules/account/src/Volo.Abp.Account.Application/Volo/Abp/Account/Emailing/AccountEmailer.cs @@ -0,0 +1,106 @@ +using System; +using System.Diagnostics; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using System.Web; +using Microsoft.Extensions.Localization; +using Volo.Abp.Account.Emailing.Templates; +using Volo.Abp.Account.Localization; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Emailing; +using Volo.Abp.Identity; +using Volo.Abp.MultiTenancy; +using Volo.Abp.TextTemplating; +using Volo.Abp.UI.Navigation.Urls; + +namespace Volo.Abp.Account.Emailing +{ + public class AccountEmailer : IAccountEmailer, ITransientDependency + { + protected ITemplateRenderer TemplateRenderer { get; } + protected IEmailSender EmailSender { get; } + protected IStringLocalizer StringLocalizer { get; } + protected IAppUrlProvider AppUrlProvider { get; } + protected ICurrentTenant CurrentTenant { get; } + + public AccountEmailer( + IEmailSender emailSender, + ITemplateRenderer templateRenderer, + IStringLocalizer stringLocalizer, + IAppUrlProvider appUrlProvider, + ICurrentTenant currentTenant) + { + EmailSender = emailSender; + StringLocalizer = stringLocalizer; + AppUrlProvider = appUrlProvider; + CurrentTenant = currentTenant; + TemplateRenderer = templateRenderer; + } + + public virtual async Task SendPasswordResetLinkAsync( + IdentityUser user, + string resetToken, + string appName, + string returnUrl = null, + string returnUrlHash = null) + { + Debug.Assert(CurrentTenant.Id == user.TenantId, "This method can only work for current tenant!"); + + var url = await AppUrlProvider.GetResetPasswordUrlAsync(appName); + + var link = $"{url}?userId={user.Id}&tenantId={user.TenantId}&resetToken={UrlEncoder.Default.Encode(resetToken)}"; + + if (!returnUrl.IsNullOrEmpty()) + { + link += "&returnUrl=" + NormalizeReturnUrl(returnUrl); + } + + if (!returnUrlHash.IsNullOrEmpty()) + { + link += "&returnUrlHash=" + returnUrlHash; + } + + var emailContent = await TemplateRenderer.RenderAsync( + AccountEmailTemplates.PasswordResetLink, + new { link = link } + ); + + await EmailSender.SendAsync( + user.Email, + StringLocalizer["PasswordReset"], + emailContent + ); + } + + private string NormalizeReturnUrl(string returnUrl) + { + if (returnUrl.IsNullOrEmpty()) + { + return returnUrl; + } + + //Handling openid connect login + if (returnUrl.StartsWith("/connect/authorize/callback", StringComparison.OrdinalIgnoreCase)) + { + if (returnUrl.Contains("?")) + { + var queryPart = returnUrl.Split('?')[1]; + var queryParameters = queryPart.Split('&'); + foreach (var queryParameter in queryParameters) + { + if (queryParameter.Contains("=")) + { + var queryParam = queryParameter.Split('='); + if (queryParam[0] == "redirect_uri") + { + return HttpUtility.UrlDecode(queryParam[1]); + } + } + } + } + } + + return returnUrl; + } + } +} diff --git a/modules/account/src/Volo.Abp.Account.Application/Volo/Abp/Account/Emailing/AppUrlProviderAccountExtensions.cs b/modules/account/src/Volo.Abp.Account.Application/Volo/Abp/Account/Emailing/AppUrlProviderAccountExtensions.cs new file mode 100644 index 0000000000..571232824c --- /dev/null +++ b/modules/account/src/Volo.Abp.Account.Application/Volo/Abp/Account/Emailing/AppUrlProviderAccountExtensions.cs @@ -0,0 +1,13 @@ +using System.Threading.Tasks; +using Volo.Abp.UI.Navigation.Urls; + +namespace Volo.Abp.Account.Emailing +{ + public static class AppUrlProviderAccountExtensions + { + public static Task GetResetPasswordUrlAsync(this IAppUrlProvider appUrlProvider, string appName) + { + return appUrlProvider.GetUrlAsync(appName, AccountUrlNames.PasswordReset); + } + } +} diff --git a/modules/account/src/Volo.Abp.Account.Application/Volo/Abp/Account/Emailing/IAccountEmailer.cs b/modules/account/src/Volo.Abp.Account.Application/Volo/Abp/Account/Emailing/IAccountEmailer.cs new file mode 100644 index 0000000000..46644f3872 --- /dev/null +++ b/modules/account/src/Volo.Abp.Account.Application/Volo/Abp/Account/Emailing/IAccountEmailer.cs @@ -0,0 +1,16 @@ +using System.Threading.Tasks; +using Volo.Abp.Identity; + +namespace Volo.Abp.Account.Emailing +{ + public interface IAccountEmailer + { + Task SendPasswordResetLinkAsync( + IdentityUser user, + string resetToken, + string appName, + string returnUrl = null, + string returnUrlHash = null + ); + } +} diff --git a/modules/account/src/Volo.Abp.Account.Application/Volo/Abp/Account/Emailing/Templates/AccountEmailTemplateDefinitionProvider.cs b/modules/account/src/Volo.Abp.Account.Application/Volo/Abp/Account/Emailing/Templates/AccountEmailTemplateDefinitionProvider.cs new file mode 100644 index 0000000000..6db5ad9e69 --- /dev/null +++ b/modules/account/src/Volo.Abp.Account.Application/Volo/Abp/Account/Emailing/Templates/AccountEmailTemplateDefinitionProvider.cs @@ -0,0 +1,22 @@ +using Volo.Abp.Account.Localization; +using Volo.Abp.Emailing.Templates; +using Volo.Abp.Localization; +using Volo.Abp.TextTemplating; + +namespace Volo.Abp.Account.Emailing.Templates +{ + public class AccountEmailTemplateDefinitionProvider : TemplateDefinitionProvider + { + public override void Define(ITemplateDefinitionContext context) + { + context.Add( + new TemplateDefinition( + AccountEmailTemplates.PasswordResetLink, + displayName: LocalizableString.Create($"TextTemplate:{AccountEmailTemplates.PasswordResetLink}"), + layout: StandardEmailTemplates.Layout, + localizationResource: typeof(AccountResource) + ).WithVirtualFilePath("/Volo/Abp/Account/Emailing/Templates/PasswordResetLink.tpl", true) + ); + } + } +} diff --git a/modules/account/src/Volo.Abp.Account.Application/Volo/Abp/Account/Emailing/Templates/AccountEmailTemplates.cs b/modules/account/src/Volo.Abp.Account.Application/Volo/Abp/Account/Emailing/Templates/AccountEmailTemplates.cs new file mode 100644 index 0000000000..676064f0d6 --- /dev/null +++ b/modules/account/src/Volo.Abp.Account.Application/Volo/Abp/Account/Emailing/Templates/AccountEmailTemplates.cs @@ -0,0 +1,7 @@ +namespace Volo.Abp.Account.Emailing.Templates +{ + public static class AccountEmailTemplates + { + public const string PasswordResetLink = "Abp.Account.PasswordResetLink"; + } +} diff --git a/modules/account/src/Volo.Abp.Account.Application/Volo/Abp/Account/Emailing/Templates/PasswordResetLink.tpl b/modules/account/src/Volo.Abp.Account.Application/Volo/Abp/Account/Emailing/Templates/PasswordResetLink.tpl new file mode 100644 index 0000000000..05f21baf5b --- /dev/null +++ b/modules/account/src/Volo.Abp.Account.Application/Volo/Abp/Account/Emailing/Templates/PasswordResetLink.tpl @@ -0,0 +1,7 @@ +

{{L "PasswordReset"}}

+ +

{{L "PasswordResetInfoInEmail"}}

+ + \ No newline at end of file diff --git a/modules/account/src/Volo.Abp.Account.HttpApi/Volo/Abp/Account/AccountController.cs b/modules/account/src/Volo.Abp.Account.HttpApi/Volo/Abp/Account/AccountController.cs index fa479568aa..d1f22587e8 100644 --- a/modules/account/src/Volo.Abp.Account.HttpApi/Volo/Abp/Account/AccountController.cs +++ b/modules/account/src/Volo.Abp.Account.HttpApi/Volo/Abp/Account/AccountController.cs @@ -23,5 +23,19 @@ namespace Volo.Abp.Account { return AccountAppService.RegisterAsync(input); } + + [HttpPost] + [Route("send-password-reset-code")] + public virtual Task SendPasswordResetCodeAsync(SendPasswordResetCodeDto input) + { + return AccountAppService.SendPasswordResetCodeAsync(input); + } + + [HttpPost] + [Route("reset-password")] + public virtual Task ResetPasswordAsync(ResetPasswordDto input) + { + return AccountAppService.ResetPasswordAsync(input); + } } -} \ No newline at end of file +} 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 7e79442bd7..5fd2124f49 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 @@ -12,6 +12,7 @@ namespace Volo.Abp.Account.Web.Pages.Account { public abstract class AccountPageModel : AbpPageModel { + public IAccountAppService AccountAppService { get; set; } public SignInManager SignInManager { get; set; } public IdentityUserManager UserManager { get; set; } public IdentitySecurityLogManager IdentitySecurityLogManager { get; set; } diff --git a/modules/account/src/Volo.Abp.Account.Web/Pages/Account/ForgotPassword.cshtml b/modules/account/src/Volo.Abp.Account.Web/Pages/Account/ForgotPassword.cshtml new file mode 100644 index 0000000000..3702369fae --- /dev/null +++ b/modules/account/src/Volo.Abp.Account.Web/Pages/Account/ForgotPassword.cshtml @@ -0,0 +1,21 @@ +@page +@inject IHtmlLocalizer L +@using Microsoft.AspNetCore.Mvc.Localization +@using Volo.Abp.Account.Localization +@model Volo.Abp.Account.Web.Pages.Account.ForgotPasswordModel +@inject Volo.Abp.AspNetCore.Mvc.UI.Layout.IPageLayout PageLayout +@{ + PageLayout.Content.Title = L["ForgotPassword"].Value; +} + diff --git a/modules/account/src/Volo.Abp.Account.Web/Pages/Account/ForgotPassword.cshtml.cs b/modules/account/src/Volo.Abp.Account.Web/Pages/Account/ForgotPassword.cshtml.cs new file mode 100644 index 0000000000..1f426fa80f --- /dev/null +++ b/modules/account/src/Volo.Abp.Account.Web/Pages/Account/ForgotPassword.cshtml.cs @@ -0,0 +1,51 @@ +using System.ComponentModel.DataAnnotations; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Volo.Abp.Identity; +using Volo.Abp.Validation; + +namespace Volo.Abp.Account.Web.Pages.Account +{ + public class ForgotPasswordModel : AccountPageModel + { + [Required] + [EmailAddress] + [DynamicStringLength(typeof(IdentityUserConsts), nameof(IdentityUserConsts.MaxEmailLength))] + [BindProperty] + public string Email { get; set; } + + [HiddenInput] + [BindProperty(SupportsGet = true)] + public string ReturnUrl { get; set; } + + [HiddenInput] + [BindProperty(SupportsGet = true)] + public string ReturnUrlHash { get; set; } + + public virtual Task OnGetAsync() + { + return Task.FromResult(Page()); + } + + public virtual async Task OnPostAsync() + { + await AccountAppService.SendPasswordResetCodeAsync( + new SendPasswordResetCodeDto + { + Email = Email, + AppName = "MVC", //TODO: Const! + ReturnUrl = ReturnUrl, + ReturnUrlHash = ReturnUrlHash + } + ); + + return RedirectToPage( + "./PasswordResetLinkSent", + new + { + returnUrl = ReturnUrl, + returnUrlHash = ReturnUrlHash + }); + } + } +} diff --git a/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml b/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml index 41986c3856..056e69e895 100644 --- a/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml +++ b/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml @@ -31,12 +31,14 @@ -
- -
+ + + + + + @L["ForgotPassword"] + + @L["Login"] @if (Model.ShowCancelButton) { diff --git a/modules/account/src/Volo.Abp.Account.Web/Pages/Account/PasswordResetLinkSent.cshtml b/modules/account/src/Volo.Abp.Account.Web/Pages/Account/PasswordResetLinkSent.cshtml new file mode 100644 index 0000000000..d970af73e3 --- /dev/null +++ b/modules/account/src/Volo.Abp.Account.Web/Pages/Account/PasswordResetLinkSent.cshtml @@ -0,0 +1,13 @@ +@page +@model Volo.Abp.Account.Web.Pages.Account.PasswordResetLinkSentModel +@inject Volo.Abp.AspNetCore.Mvc.UI.Layout.IPageLayout PageLayout +@using Microsoft.AspNetCore.Mvc.Localization +@using Volo.Abp.Account.Localization +@inject IHtmlLocalizer L +@{ + PageLayout.Content.Title = L["ForgotPassword"].Value; +} +

@L["PasswordResetMailSentMessage"]

+ diff --git a/modules/account/src/Volo.Abp.Account.Web/Pages/Account/PasswordResetLinkSent.cshtml.cs b/modules/account/src/Volo.Abp.Account.Web/Pages/Account/PasswordResetLinkSent.cshtml.cs new file mode 100644 index 0000000000..ebf9c1b295 --- /dev/null +++ b/modules/account/src/Volo.Abp.Account.Web/Pages/Account/PasswordResetLinkSent.cshtml.cs @@ -0,0 +1,24 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; + +namespace Volo.Abp.Account.Web.Pages.Account +{ + public class PasswordResetLinkSentModel : AccountPageModel + { + [BindProperty(SupportsGet = true)] + public string ReturnUrl { get; set; } + + [BindProperty(SupportsGet = true)] + public string ReturnUrlHash { get; set; } + + public virtual Task OnGetAsync() + { + return Task.FromResult(Page()); + } + + public virtual Task OnPostAsync() + { + return Task.FromResult(Page()); + } + } +} diff --git a/modules/account/src/Volo.Abp.Account.Web/Pages/Account/ResetPassword.cshtml b/modules/account/src/Volo.Abp.Account.Web/Pages/Account/ResetPassword.cshtml new file mode 100644 index 0000000000..4373be552e --- /dev/null +++ b/modules/account/src/Volo.Abp.Account.Web/Pages/Account/ResetPassword.cshtml @@ -0,0 +1,22 @@ +@page +@inject IHtmlLocalizer L +@using Microsoft.AspNetCore.Mvc.Localization +@using Volo.Abp.Account.Localization +@model Volo.Abp.Account.Web.Pages.Account.ResetPasswordModel +@inject Volo.Abp.AspNetCore.Mvc.UI.Layout.IPageLayout PageLayout +@{ + PageLayout.Content.Title = L["ResetPassword"].Value; +} +
+

@L["ResetPassword_Information"]

+ + + + + + + + + @L["Cancel"] + + diff --git a/modules/account/src/Volo.Abp.Account.Web/Pages/Account/ResetPassword.cshtml.cs b/modules/account/src/Volo.Abp.Account.Web/Pages/Account/ResetPassword.cshtml.cs new file mode 100644 index 0000000000..13370f5fbd --- /dev/null +++ b/modules/account/src/Volo.Abp.Account.Web/Pages/Account/ResetPassword.cshtml.cs @@ -0,0 +1,110 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Volo.Abp.Auditing; +using Volo.Abp.Identity; +using Volo.Abp.MultiTenancy; +using Volo.Abp.Validation; + +namespace Volo.Abp.Account.Web.Pages.Account +{ + //TODO: Implement live password complexity check on the razor view! + + public class ResetPasswordModel : AccountPageModel + { + [HiddenInput] + [BindProperty(SupportsGet = true)] + public Guid? TenantId { get; set; } + + [Required] + [HiddenInput] + [BindProperty(SupportsGet = true)] + public Guid UserId { get; set; } + + [Required] + [HiddenInput] + [BindProperty(SupportsGet = true)] + public string ResetToken { get; set; } + + [HiddenInput] + [BindProperty(SupportsGet = true)] + public string ReturnUrl { get; set; } + + [HiddenInput] + [BindProperty(SupportsGet = true)] + public string ReturnUrlHash { get; set; } + + [Required] + [BindProperty] + [DataType(DataType.Password)] + [DynamicStringLength(typeof(IdentityUserConsts), nameof(IdentityUserConsts.MaxPasswordLength))] + [DisableAuditing] + public string Password { get; set; } + + [Required] + [BindProperty] + [DataType(DataType.Password)] + [DynamicStringLength(typeof(IdentityUserConsts), nameof(IdentityUserConsts.MaxPasswordLength))] + [DisableAuditing] + public string ConfirmPassword { get; set; } + + protected virtual ITenantResolveResultAccessor TenantResolveResultAccessor { get; } + + public ResetPasswordModel(ITenantResolveResultAccessor tenantResolveResultAccessor) + { + TenantResolveResultAccessor = tenantResolveResultAccessor; + } + + public virtual Task OnGetAsync() + { + //TODO: It would be good to try to switch tenant if needed + CheckCurrentTenant(TenantId); + return Task.FromResult(Page()); + } + + public virtual async Task OnPostAsync() + { + ValidateModel(); + + try + { + await AccountAppService.ResetPasswordAsync( + new ResetPasswordDto + { + UserId = UserId, + ResetToken = ResetToken, + Password = Password + } + ); + } + catch (AbpIdentityResultException e) + { + if (!string.IsNullOrWhiteSpace(e.Message)) + { + Alerts.Warning(e.Message); + return Page(); + } + + throw; + } + + //TODO: Try to automatically login! + return RedirectToPage("./ResetPasswordConfirmation", new + { + returnUrl = ReturnUrl, + returnUrlHash = ReturnUrlHash + }); + } + + protected override void ValidateModel() + { + if (!Equals(Password, ConfirmPassword)) + { + ModelState.AddModelError("ConfirmPassword", L["'{0}' and '{1}' do not match.", "ConfirmPassword", "Password"]); + } + + base.ValidateModel(); + } + } +} diff --git a/modules/account/src/Volo.Abp.Account.Web/Pages/Account/ResetPasswordConfirmation.cshtml b/modules/account/src/Volo.Abp.Account.Web/Pages/Account/ResetPasswordConfirmation.cshtml new file mode 100644 index 0000000000..f29e6620f5 --- /dev/null +++ b/modules/account/src/Volo.Abp.Account.Web/Pages/Account/ResetPasswordConfirmation.cshtml @@ -0,0 +1,11 @@ +@page +@model Volo.Abp.Account.Web.Pages.Account.ResetPasswordConfirmationModel +@inject Volo.Abp.AspNetCore.Mvc.UI.Layout.IPageLayout PageLayout +@using Microsoft.AspNetCore.Mvc.Localization +@using Volo.Abp.Account.Localization +@inject IHtmlLocalizer L +@{ + PageLayout.Content.Title = L["ResetPassword"].Value; +} +

@L["YourPasswordIsSuccessfullyReset"]

+@L["GoToTheApplication"] diff --git a/modules/account/src/Volo.Abp.Account.Web/Pages/Account/ResetPasswordConfirmation.cshtml.cs b/modules/account/src/Volo.Abp.Account.Web/Pages/Account/ResetPasswordConfirmation.cshtml.cs new file mode 100644 index 0000000000..2b713c1577 --- /dev/null +++ b/modules/account/src/Volo.Abp.Account.Web/Pages/Account/ResetPasswordConfirmation.cshtml.cs @@ -0,0 +1,24 @@ +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Volo.Abp.Account.Web.Pages.Account +{ + [AllowAnonymous] + public class ResetPasswordConfirmationModel : AccountPageModel + { + [BindProperty(SupportsGet = true)] + public string ReturnUrl { get; set; } + + [BindProperty(SupportsGet = true)] + public string ReturnUrlHash { get; set; } + + public virtual Task OnGetAsync() + { + ReturnUrl = GetRedirectUrl(ReturnUrl, ReturnUrlHash); + + return Task.FromResult(Page()); + } + } +} diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Application/MyProjectNameApplicationModule.cs b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Application/MyProjectNameApplicationModule.cs index e9941ee6c3..f435cd0482 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Application/MyProjectNameApplicationModule.cs +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Application/MyProjectNameApplicationModule.cs @@ -1,5 +1,8 @@ -using Volo.Abp.Account; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Volo.Abp.Account; using Volo.Abp.AutoMapper; +using Volo.Abp.Emailing; using Volo.Abp.FeatureManagement; using Volo.Abp.Identity; using Volo.Abp.Modularity; @@ -25,6 +28,10 @@ namespace MyCompanyName.MyProjectName { options.AddMaps(); }); + +#if DEBUG + context.Services.Replace(ServiceDescriptor.Singleton()); +#endif } } }