mirror of https://github.com/abpframework/abp.git
29 changed files with 626 additions and 19 deletions
@ -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; } |
|||
} |
|||
} |
|||
@ -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; } |
|||
} |
|||
} |
|||
@ -0,0 +1,7 @@ |
|||
namespace Volo.Abp.Account |
|||
{ |
|||
public static class AccountUrlNames |
|||
{ |
|||
public const string PasswordReset = "Abp.Account.PasswordReset"; |
|||
} |
|||
} |
|||
@ -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<AccountResource> StringLocalizer { get; } |
|||
protected IAppUrlProvider AppUrlProvider { get; } |
|||
protected ICurrentTenant CurrentTenant { get; } |
|||
|
|||
public AccountEmailer( |
|||
IEmailSender emailSender, |
|||
ITemplateRenderer templateRenderer, |
|||
IStringLocalizer<AccountResource> 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; |
|||
} |
|||
} |
|||
} |
|||
@ -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<string> GetResetPasswordUrlAsync(this IAppUrlProvider appUrlProvider, string appName) |
|||
{ |
|||
return appUrlProvider.GetUrlAsync(appName, AccountUrlNames.PasswordReset); |
|||
} |
|||
} |
|||
} |
|||
@ -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 |
|||
); |
|||
} |
|||
} |
|||
@ -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<AccountResource>($"TextTemplate:{AccountEmailTemplates.PasswordResetLink}"), |
|||
layout: StandardEmailTemplates.Layout, |
|||
localizationResource: typeof(AccountResource) |
|||
).WithVirtualFilePath("/Volo/Abp/Account/Emailing/Templates/PasswordResetLink.tpl", true) |
|||
); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,7 @@ |
|||
namespace Volo.Abp.Account.Emailing.Templates |
|||
{ |
|||
public static class AccountEmailTemplates |
|||
{ |
|||
public const string PasswordResetLink = "Abp.Account.PasswordResetLink"; |
|||
} |
|||
} |
|||
@ -0,0 +1,7 @@ |
|||
<h3>{{L "PasswordReset"}}</h3> |
|||
|
|||
<p>{{L "PasswordResetInfoInEmail"}}</p> |
|||
|
|||
<div> |
|||
<a href="{{model.link}}">{{L "ResetMyPassword"}}</a> |
|||
</div> |
|||
@ -0,0 +1,21 @@ |
|||
@page |
|||
@inject IHtmlLocalizer<AccountResource> 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; |
|||
} |
|||
<div class="account-module-form"> |
|||
<form method="post"> |
|||
<p>@L["SendPasswordResetLink_Information"]</p> |
|||
<input asp-for="ReturnUrl"/> |
|||
<input asp-for="ReturnUrlHash"/> |
|||
<abp-input asp-for="Email"/> |
|||
<abp-button button-type="Primary" size="Block" type="submit" class="mt-2 mb-3">@L["Submit"]</abp-button> |
|||
<a asp-page="./Login" asp-all-route-data="@(new Dictionary<string, string>{ {"returnUrl",Model.ReturnUrl}, {"returnUrlHash",Model.ReturnUrlHash} })"> |
|||
<i class="fa fa-long-arrow-left"></i> @L["Login"] |
|||
</a> |
|||
</form> |
|||
</div> |
|||
@ -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<IActionResult> OnGetAsync() |
|||
{ |
|||
return Task.FromResult<IActionResult>(Page()); |
|||
} |
|||
|
|||
public virtual async Task<IActionResult> OnPostAsync() |
|||
{ |
|||
await AccountAppService.SendPasswordResetCodeAsync( |
|||
new SendPasswordResetCodeDto |
|||
{ |
|||
Email = Email, |
|||
AppName = "MVC", //TODO: Const!
|
|||
ReturnUrl = ReturnUrl, |
|||
ReturnUrlHash = ReturnUrlHash |
|||
} |
|||
); |
|||
|
|||
return RedirectToPage( |
|||
"./PasswordResetLinkSent", |
|||
new |
|||
{ |
|||
returnUrl = ReturnUrl, |
|||
returnUrlHash = ReturnUrlHash |
|||
}); |
|||
} |
|||
} |
|||
} |
|||
@ -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<AccountResource> L |
|||
@{ |
|||
PageLayout.Content.Title = L["ForgotPassword"].Value; |
|||
} |
|||
<p>@L["PasswordResetMailSentMessage"]</p> |
|||
<div class="mt-4"> |
|||
<a abp-button="Primary" asp-page="./Login" asp-all-route-data="@(new Dictionary<string, string>{ {"returnUrl",Model.ReturnUrl}, {"returnUrlHash",Model.ReturnUrlHash} })">← @L["BackToLogin"]</a> |
|||
</div> |
|||
@ -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<IActionResult> OnGetAsync() |
|||
{ |
|||
return Task.FromResult<IActionResult>(Page()); |
|||
} |
|||
|
|||
public virtual Task<IActionResult> OnPostAsync() |
|||
{ |
|||
return Task.FromResult<IActionResult>(Page()); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,22 @@ |
|||
@page |
|||
@inject IHtmlLocalizer<AccountResource> 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; |
|||
} |
|||
<form method="post"> |
|||
<p>@L["ResetPassword_Information"]</p> |
|||
|
|||
<abp-input asp-for="ReturnUrl" /> |
|||
<abp-input asp-for="ReturnUrlHash" /> |
|||
<abp-input asp-for="UserId" /> |
|||
<abp-input asp-for="ResetToken" /> |
|||
<abp-input asp-for="Password" /> |
|||
<abp-input asp-for="ConfirmPassword" /> |
|||
|
|||
<a abp-button="Secondary" asp-page="./Login">@L["Cancel"]</a> |
|||
<abp-button type="submit" button-type="Primary" text="@L["Submit"].Value" /> |
|||
</form> |
|||
@ -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<IActionResult> OnGetAsync() |
|||
{ |
|||
//TODO: It would be good to try to switch tenant if needed
|
|||
CheckCurrentTenant(TenantId); |
|||
return Task.FromResult<IActionResult>(Page()); |
|||
} |
|||
|
|||
public virtual async Task<IActionResult> 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(); |
|||
} |
|||
} |
|||
} |
|||
@ -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<AccountResource> L |
|||
@{ |
|||
PageLayout.Content.Title = L["ResetPassword"].Value; |
|||
} |
|||
<p>@L["YourPasswordIsSuccessfullyReset"]</p> |
|||
<a abp-button="Primary" href="@Url.Content(Model.ReturnUrl)">@L["GoToTheApplication"]</a> |
|||
@ -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<IActionResult> OnGetAsync() |
|||
{ |
|||
ReturnUrl = GetRedirectUrl(ReturnUrl, ReturnUrlHash); |
|||
|
|||
return Task.FromResult<IActionResult>(Page()); |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue