mirror of
https://github.com/OrchardCMS/Orchard.git
synced 2025-10-15 19:54:57 +08:00
Adding UI to change password
- new Lost Password link in LogOng view - sends a reset link by mail Work Item: 16341 --HG-- branch : dev
This commit is contained in:
@@ -12,6 +12,7 @@ using Orchard.Users.Services;
|
||||
using Orchard.Users.ViewModels;
|
||||
using Orchard.ContentManagement;
|
||||
using Orchard.Users.Models;
|
||||
using Orchard.UI.Notify;
|
||||
|
||||
namespace Orchard.Users.Controllers {
|
||||
[HandleError, Themed]
|
||||
@@ -119,8 +120,8 @@ namespace Orchard.Users.Controllers {
|
||||
|
||||
if (user != null) {
|
||||
if ( user.As<UserPart>().EmailStatus == UserStatus.Pending ) {
|
||||
string challengeToken = _membershipService.GetEncryptedChallengeToken(user.As<UserPart>());
|
||||
_membershipService.SendChallengeEmail(user.As<UserPart>(), Url.AbsoluteAction(() => Url.Action("ChallengeEmail", "Account", new { Area = "Orchard.Users", token = challengeToken })));
|
||||
string challengeToken = _userService.GetNonce(user.As<UserPart>());
|
||||
_userService.SendChallengeEmail(user.As<UserPart>(), Url.AbsoluteAction(() => Url.Action("ChallengeEmail", "Account", new { Area = "Orchard.Users", token = challengeToken })));
|
||||
|
||||
return RedirectToAction("ChallengeEmailSent");
|
||||
}
|
||||
@@ -141,6 +142,36 @@ namespace Orchard.Users.Controllers {
|
||||
return Register();
|
||||
}
|
||||
|
||||
public ActionResult LostPassword() {
|
||||
return View();
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public ActionResult LostPassword(string username) {
|
||||
|
||||
if(String.IsNullOrWhiteSpace(username)){
|
||||
_orchardServices.Notifier.Error(T("Invalid username or E-mail"));
|
||||
return View();
|
||||
}
|
||||
|
||||
_userService.SendLostPasswordEmail(username, nonce => Url.AbsoluteAction(() => Url.Action("ValidateLostPassword", "Account", new { Area = "Orchard.Users", nonce = nonce })));
|
||||
|
||||
_orchardServices.Notifier.Information(T("Check your e-mail for the confirmation link."));
|
||||
|
||||
return RedirectToAction("LogOn");
|
||||
}
|
||||
|
||||
public ActionResult ValidateLostPassword(string nonce) {
|
||||
IUser user;
|
||||
if (null != (user = _userService.ValidateLostPassword(nonce))) {
|
||||
_authenticationService.SignIn(user, false);
|
||||
return RedirectToAction("ChangePassword");
|
||||
}
|
||||
else {
|
||||
return new RedirectResult("~/");
|
||||
}
|
||||
}
|
||||
|
||||
[Authorize]
|
||||
public ActionResult ChangePassword() {
|
||||
ViewData["PasswordLength"] = MinPasswordLength;
|
||||
@@ -150,32 +181,23 @@ namespace Orchard.Users.Controllers {
|
||||
|
||||
[Authorize]
|
||||
[HttpPost]
|
||||
[SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes",
|
||||
Justification = "Exceptions result in password not being changed.")]
|
||||
public ActionResult ChangePassword(string currentPassword, string newPassword, string confirmPassword) {
|
||||
ViewData["PasswordLength"] = MinPasswordLength;
|
||||
|
||||
if (!ValidateChangePassword(currentPassword, newPassword, confirmPassword)) {
|
||||
if (newPassword == null || newPassword.Length < MinPasswordLength) {
|
||||
ModelState.AddModelError("newPassword", T("You must specify a new password of {0} or more characters.", MinPasswordLength));
|
||||
}
|
||||
|
||||
if (!String.Equals(newPassword, confirmPassword, StringComparison.Ordinal)) {
|
||||
ModelState.AddModelError("_FORM", T("The new password and confirmation password do not match."));
|
||||
}
|
||||
|
||||
if (!ModelState.IsValid) {
|
||||
return View();
|
||||
}
|
||||
|
||||
try {
|
||||
var validated = _membershipService.ValidateUser(User.Identity.Name, currentPassword);
|
||||
|
||||
if (validated != null) {
|
||||
_membershipService.SetPassword(validated, newPassword);
|
||||
return RedirectToAction("ChangePasswordSuccess");
|
||||
}
|
||||
else {
|
||||
ModelState.AddModelError("_FORM",
|
||||
T("The current password is incorrect or the new password is invalid."));
|
||||
return ChangePassword();
|
||||
}
|
||||
}
|
||||
catch {
|
||||
ModelState.AddModelError("_FORM", T("The current password is incorrect or the new password is invalid."));
|
||||
return ChangePassword();
|
||||
}
|
||||
_membershipService.SetPassword(_orchardServices.WorkContext.CurrentUser, newPassword);
|
||||
return RedirectToAction("ChangePasswordSuccess");
|
||||
}
|
||||
|
||||
public ActionResult RegistrationPending() {
|
||||
@@ -199,7 +221,7 @@ namespace Orchard.Users.Controllers {
|
||||
}
|
||||
|
||||
public ActionResult ChallengeEmail(string token) {
|
||||
var user = _membershipService.ValidateChallengeToken(token);
|
||||
var user = _userService.ValidateChallenge(token);
|
||||
|
||||
if ( user != null ) {
|
||||
_authenticationService.SignIn(user, false /* createPersistentCookie */);
|
||||
@@ -217,21 +239,6 @@ namespace Orchard.Users.Controllers {
|
||||
|
||||
#region Validation Methods
|
||||
|
||||
private bool ValidateChangePassword(string currentPassword, string newPassword, string confirmPassword) {
|
||||
if (String.IsNullOrEmpty(currentPassword)) {
|
||||
ModelState.AddModelError("currentPassword", T("You must specify a current password."));
|
||||
}
|
||||
if (newPassword == null || newPassword.Length < MinPasswordLength) {
|
||||
ModelState.AddModelError("newPassword", T("You must specify a new password of {0} or more characters.", MinPasswordLength));
|
||||
}
|
||||
|
||||
if (!String.Equals(newPassword, confirmPassword, StringComparison.Ordinal)) {
|
||||
ModelState.AddModelError("_FORM", T("The new password and confirmation password do not match."));
|
||||
}
|
||||
|
||||
return ModelState.IsValid;
|
||||
}
|
||||
|
||||
private IUser ValidateLogOn(string userNameOrEmail, string password) {
|
||||
bool validate = true;
|
||||
|
||||
|
@@ -197,8 +197,8 @@ namespace Orchard.Users.Controllers {
|
||||
var user = Services.ContentManager.Get(id);
|
||||
|
||||
if ( user != null ) {
|
||||
string challengeToken = _membershipService.GetEncryptedChallengeToken(user.As<UserPart>());
|
||||
_membershipService.SendChallengeEmail(user.As<UserPart>(), Url.AbsoluteAction(() => Url.Action("ChallengeEmail", "Account", new {Area = "Orchard.Users", token = challengeToken})));
|
||||
string challengeToken = _userService.GetNonce(user.As<UserPart>());
|
||||
_userService.SendChallengeEmail(user.As<UserPart>(), Url.AbsoluteAction(() => Url.Action("ChallengeEmail", "Account", new {Area = "Orchard.Users", token = challengeToken})));
|
||||
}
|
||||
|
||||
Services.Notifier.Information(T("Challenge email sent"));
|
||||
|
@@ -29,11 +29,16 @@ namespace Orchard.Users.Handlers {
|
||||
context.MailMessage.Body = T("The following user account needs to be moderated: {0}", recipient.UserName).Text;
|
||||
}
|
||||
|
||||
if ( context.Type == MessageTypes.Validation ) {
|
||||
if (context.Type == MessageTypes.Validation) {
|
||||
context.MailMessage.Subject = T("User account validation").Text;
|
||||
context.MailMessage.Body = T("Dear {0}, please <a href=\"{1}\">click here</a> to validate you email address.", recipient.UserName, context.Properties["ChallengeUrl"]).Text;
|
||||
}
|
||||
|
||||
if (context.Type == MessageTypes.LostPassword) {
|
||||
context.MailMessage.Subject = T("Lost password").Text;
|
||||
context.MailMessage.Body = T("Dear {0}, please <a href=\"{1}\">click here</a> to change your password.", recipient.UserName, context.Properties["LostPasswordUrl"]).Text;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public void Sent(MessageContext context) {
|
||||
|
@@ -7,6 +7,7 @@ namespace Orchard.Users.Models {
|
||||
public static class MessageTypes {
|
||||
public const string Moderation = "ORCHARD_USERS_MODERATION";
|
||||
public const string Validation = "ORCHARD_USERS_VALIDATION";
|
||||
public const string LostPassword = "ORCHARD_USERS_RESETPASSWORD";
|
||||
|
||||
}
|
||||
}
|
@@ -123,6 +123,9 @@
|
||||
<Content Include="Views\EditorTemplates\Parts\User.Edit.cshtml" />
|
||||
<Content Include="Views\EditorTemplates\Parts\User.Create.cshtml" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Content Include="Views\Account\LostPassword.cshtml" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
|
||||
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v10.0\WebApplications\Microsoft.WebApplication.targets" />
|
||||
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
|
||||
|
@@ -1,6 +1,16 @@
|
||||
using Orchard.Security;
|
||||
using System;
|
||||
namespace Orchard.Users.Services {
|
||||
public interface IUserService : IDependency {
|
||||
string VerifyUserUnicity(string userName, string email);
|
||||
string VerifyUserUnicity(int id, string userName, string email);
|
||||
|
||||
void SendChallengeEmail(IUser user, string url);
|
||||
IUser ValidateChallenge(string challengeToken);
|
||||
|
||||
bool SendLostPasswordEmail(string usernameOrEmail, Func<string, string> createUrl);
|
||||
IUser ValidateLostPassword(string nonce);
|
||||
|
||||
string GetNonce(IUser user);
|
||||
}
|
||||
}
|
@@ -14,6 +14,7 @@ using Orchard.Users.Events;
|
||||
using Orchard.Users.Models;
|
||||
using Orchard.Messaging.Services;
|
||||
using System.Collections.Generic;
|
||||
using Orchard.Services;
|
||||
|
||||
namespace Orchard.Users.Services {
|
||||
[UsedImplicitly]
|
||||
@@ -22,11 +23,13 @@ namespace Orchard.Users.Services {
|
||||
private readonly IOrchardServices _orchardServices;
|
||||
private readonly IMessageManager _messageManager;
|
||||
private readonly IEnumerable<IUserEventHandler> _userEventHandlers;
|
||||
private readonly IClock _clock;
|
||||
|
||||
public MembershipService(IOrchardServices orchardServices, IMessageManager messageManager, IEnumerable<IUserEventHandler> userEventHandlers) {
|
||||
public MembershipService(IOrchardServices orchardServices, IMessageManager messageManager, IEnumerable<IUserEventHandler> userEventHandlers, IClock clock) {
|
||||
_orchardServices = orchardServices;
|
||||
_messageManager = messageManager;
|
||||
_userEventHandlers = userEventHandlers;
|
||||
_clock = clock;
|
||||
Logger = NullLogger.Instance;
|
||||
T = NullLocalizer.Instance;
|
||||
}
|
||||
@@ -87,54 +90,6 @@ namespace Orchard.Users.Services {
|
||||
return user;
|
||||
}
|
||||
|
||||
public void SendChallengeEmail(IUser user, string url) {
|
||||
_messageManager.Send(user.ContentItem.Record, MessageTypes.Validation, "email", new Dictionary<string, string> { { "ChallengeUrl", url } });
|
||||
}
|
||||
|
||||
public IUser ValidateChallengeToken(string challengeToken) {
|
||||
string username;
|
||||
DateTime validateByUtc;
|
||||
|
||||
if(!DecryptChallengeToken(challengeToken, out username, out validateByUtc)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ( validateByUtc < DateTime.UtcNow )
|
||||
return null;
|
||||
|
||||
var user = GetUser(username);
|
||||
if ( user == null )
|
||||
return null;
|
||||
|
||||
user.As<UserPart>().EmailStatus = UserStatus.Approved;
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
public string GetEncryptedChallengeToken(IUser user) {
|
||||
var challengeToken = new XElement("Token", new XAttribute("username", user.UserName), new XAttribute("validate-by-utc", DateTime.UtcNow.Add(DelayToValidate).ToString(CultureInfo.InvariantCulture))).ToString();
|
||||
var data = Encoding.UTF8.GetBytes(challengeToken);
|
||||
return MachineKey.Encode(data, MachineKeyProtection.All);
|
||||
}
|
||||
|
||||
private static bool DecryptChallengeToken(string challengeToken, out string username, out DateTime validateByUtc) {
|
||||
username = null;
|
||||
validateByUtc = DateTime.UtcNow;
|
||||
|
||||
try {
|
||||
var data = MachineKey.Decode(challengeToken, MachineKeyProtection.All);
|
||||
var xml = Encoding.UTF8.GetString(data);
|
||||
var element = XElement.Parse(xml);
|
||||
username = element.Attribute("username").Value;
|
||||
validateByUtc = DateTime.Parse(element.Attribute("validate-by-utc").Value, CultureInfo.InvariantCulture);
|
||||
return true;
|
||||
}
|
||||
catch {
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public IUser GetUser(string username) {
|
||||
var lowerName = username == null ? "" : username.ToLower();
|
||||
|
||||
|
@@ -1,17 +1,33 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using JetBrains.Annotations;
|
||||
using Orchard.Logging;
|
||||
using Orchard.ContentManagement;
|
||||
using Orchard.Users.Models;
|
||||
using Orchard.Security;
|
||||
using System.Xml.Linq;
|
||||
using Orchard.Services;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using System.Web.Security;
|
||||
using Orchard.Messaging.Services;
|
||||
|
||||
namespace Orchard.Users.Services {
|
||||
[UsedImplicitly]
|
||||
public class UserService : IUserService {
|
||||
private readonly IContentManager _contentManager;
|
||||
private static readonly TimeSpan DelayToValidate = new TimeSpan(7, 0, 0, 0); // one week to validate email
|
||||
|
||||
public UserService(IContentManager contentManager) {
|
||||
private readonly IContentManager _contentManager;
|
||||
private readonly IMembershipService _membershipService;
|
||||
private readonly IClock _clock;
|
||||
private readonly IMessageManager _messageManager;
|
||||
|
||||
public UserService(IContentManager contentManager, IMembershipService membershipService, IClock clock, IMessageManager messageManager) {
|
||||
_contentManager = contentManager;
|
||||
_membershipService = membershipService;
|
||||
_clock = clock;
|
||||
_messageManager = messageManager;
|
||||
Logger = NullLogger.Instance;
|
||||
}
|
||||
|
||||
@@ -46,5 +62,86 @@ namespace Orchard.Users.Services {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public string GetNonce(IUser user) {
|
||||
var challengeToken = new XElement("Token", new XAttribute("username", user.UserName), new XAttribute("validate-by-utc", _clock.UtcNow.Add(DelayToValidate).ToString(CultureInfo.InvariantCulture))).ToString();
|
||||
var data = Encoding.UTF8.GetBytes(challengeToken);
|
||||
return MachineKey.Encode(data, MachineKeyProtection.All);
|
||||
}
|
||||
|
||||
private bool DecryptNonce(string challengeToken, out string username, out DateTime validateByUtc) {
|
||||
username = null;
|
||||
validateByUtc = _clock.UtcNow;
|
||||
|
||||
try {
|
||||
var data = MachineKey.Decode(challengeToken, MachineKeyProtection.All);
|
||||
var xml = Encoding.UTF8.GetString(data);
|
||||
var element = XElement.Parse(xml);
|
||||
username = element.Attribute("username").Value;
|
||||
validateByUtc = DateTime.Parse(element.Attribute("validate-by-utc").Value, CultureInfo.InvariantCulture);
|
||||
return true;
|
||||
}
|
||||
catch {
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public IUser ValidateChallenge(string nonce) {
|
||||
string username;
|
||||
DateTime validateByUtc;
|
||||
|
||||
if (!DecryptNonce(nonce, out username, out validateByUtc)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (validateByUtc < _clock.UtcNow)
|
||||
return null;
|
||||
|
||||
var user = _membershipService.GetUser(username);
|
||||
if (user == null)
|
||||
return null;
|
||||
|
||||
user.As<UserPart>().EmailStatus = UserStatus.Approved;
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
public void SendChallengeEmail(IUser user, string url) {
|
||||
_messageManager.Send(user.ContentItem.Record, MessageTypes.Validation, "email", new Dictionary<string, string> { { "ChallengeUrl", url } });
|
||||
}
|
||||
|
||||
public bool SendLostPasswordEmail(string usernameOrEmail, Func<string, string> createUrl) {
|
||||
var lowerName = usernameOrEmail.ToLower();
|
||||
var user = _contentManager.Query<UserPart, UserPartRecord>().Where(u => u.NormalizedUserName == lowerName || u.Email == lowerName).List().FirstOrDefault();
|
||||
|
||||
if (user != null) {
|
||||
string nonce = GetNonce(user);
|
||||
string url = createUrl(nonce);
|
||||
|
||||
_messageManager.Send(user.ContentItem.Record, MessageTypes.LostPassword, "email", new Dictionary<string, string> { { "LostPasswordUrl", url } });
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public IUser ValidateLostPassword(string nonce) {
|
||||
string username;
|
||||
DateTime validateByUtc;
|
||||
|
||||
if (!DecryptNonce(nonce, out username, out validateByUtc)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (validateByUtc < _clock.UtcNow)
|
||||
return null;
|
||||
|
||||
var user = _membershipService.GetUser(username);
|
||||
if (user == null)
|
||||
return null;
|
||||
|
||||
return user;
|
||||
}
|
||||
}
|
||||
}
|
@@ -6,11 +6,6 @@
|
||||
@using (Html.BeginFormAntiForgeryPost()) {
|
||||
<fieldset>
|
||||
<legend>@T("Account Information")</legend>
|
||||
<div>
|
||||
<label for="currentPassword">@T("Current password:")</label>
|
||||
@Html.Password("currentPassword")
|
||||
@Html.ValidationMessage("currentPassword")
|
||||
</div>
|
||||
<div>
|
||||
<label for="newPassword">@T("New password:")</label>
|
||||
@Html.Password("newPassword")
|
||||
|
@@ -6,10 +6,14 @@
|
||||
}
|
||||
|
||||
<h1 class="page-title">@Html.TitleForPage(Model.Title)</h1>
|
||||
<p>@T("Please enter your username and password.") @if(userCanRegister) { <text> </text> @Html.ActionLink("Register", "Register") @T(" if you don't have an account.") }</p>
|
||||
<p>
|
||||
@T("Please enter your username and password.")
|
||||
@if(userCanRegister) { @Html.ActionLink(T("Register").Text, "Register") @T(" if you don't have an account.") }
|
||||
@Html.ActionLink(T("Lost your Password?").Text, "LostPassword")
|
||||
</p>
|
||||
@Html.ValidationSummary(T("Login was unsuccessful. Please correct the errors and try again.").ToString())
|
||||
|
||||
@using (Html.BeginFormAntiForgeryPost(Url.Action("LogOn", new {ReturnUrl = Request.QueryString["ReturnUrl"]}))) {
|
||||
@using (Html.BeginFormAntiForgeryPost(Url.Action("LogOn", new { ReturnUrl = Request.QueryString["ReturnUrl"] }))) {
|
||||
<fieldset class="login-form group">
|
||||
<legend>@T("Account Information")</legend>
|
||||
<ol>
|
||||
|
@@ -0,0 +1,15 @@
|
||||
<h1>@Html.TitleForPage(T("Lost Password").ToString())</h1>
|
||||
<p>@T("Please enter your username or email address. You will receive a link to create a new password via email.")</p>
|
||||
@using (Html.BeginFormAntiForgeryPost()) {
|
||||
<fieldset>
|
||||
<legend>@T("Account Information")</legend>
|
||||
<div>
|
||||
<label for="username">@T("Username or E-mail:")</label>
|
||||
@Html.TextBox("username")
|
||||
@Html.ValidationMessage("username")
|
||||
</div>
|
||||
<div>
|
||||
<button class="primaryAction" type="submit">@T("Send Request")</button>
|
||||
</div>
|
||||
</fieldset>
|
||||
}
|
@@ -8,8 +8,5 @@
|
||||
IUser ValidateUser(string userNameOrEmail, string password);
|
||||
void SetPassword(IUser user, string password);
|
||||
|
||||
IUser ValidateChallengeToken(string challengeToken);
|
||||
void SendChallengeEmail(IUser user, string url);
|
||||
string GetEncryptedChallengeToken(IUser user);
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user