diff --git a/.gitignore b/.gitignore index acf0731..793cf78 100644 --- a/.gitignore +++ b/.gitignore @@ -166,3 +166,4 @@ pip-log.txt */Logs/ *.log *.log.* +Notes/papercut/*.eml diff --git a/Notes/papercut/Papercut.exe b/Notes/papercut/Papercut.exe new file mode 100644 index 0000000..13d5812 Binary files /dev/null and b/Notes/papercut/Papercut.exe differ diff --git a/Notes/papercut/Papercut.exe.config b/Notes/papercut/Papercut.exe.config new file mode 100644 index 0000000..c744dc2 --- /dev/null +++ b/Notes/papercut/Papercut.exe.config @@ -0,0 +1,50 @@ + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + Any + + + 25 + + + False + + + + + + + + + + + + + \ No newline at end of file diff --git a/Notes/papercut/log4net.dll b/Notes/papercut/log4net.dll new file mode 100644 index 0000000..ffc57e1 Binary files /dev/null and b/Notes/papercut/log4net.dll differ diff --git a/Web/Content/Account.Login.css b/Web/Content/Account.Login.css index e63a490..d3ca7a1 100644 --- a/Web/Content/Account.Login.css +++ b/Web/Content/Account.Login.css @@ -38,6 +38,10 @@ h2 { margin-bottom: 20px; } +.rememberMe label.checkbox { + padding-left: 10px; +} + legend { border-bottom: 0; margin-bottom: 0; diff --git a/Web/Controllers/AccountController.cs b/Web/Controllers/AccountController.cs index 3943673..856a9a1 100644 --- a/Web/Controllers/AccountController.cs +++ b/Web/Controllers/AccountController.cs @@ -2,6 +2,8 @@ using System.Web.Mvc; using System.Web.Security; using MileageTraker.Web.DAL; +using MileageTraker.Web.Email; +using MileageTraker.Web.Utility; using MileageTraker.Web.ViewModels.Account; namespace MileageTraker.Web.Controllers @@ -10,10 +12,10 @@ namespace MileageTraker.Web.Controllers public class AccountController : ControllerBase { [AllowAnonymous] - public ActionResult Login(string returnUrl) + public ActionResult Login(string returnUrl, string username) { ViewBag.ReturnUrl = returnUrl; - return View(); + return View(new LoginViewModel{Username = username}); } [HttpPost] @@ -40,7 +42,12 @@ namespace MileageTraker.Web.Controllers } catch (UserLockedOutException) { - ModelState.AddModelError("", "Too many failed password attempts for " + model.Username + ". Account is locked."); + ModelState.AddModelError("", + "Too many failed password attempts for " + + model.Username + ". Account is locked. " + + @"Use 'Forgot Password' or contact " + + "administrator to unlock." + ); } } @@ -58,37 +65,7 @@ namespace MileageTraker.Web.Controllers return RedirectToAction("Index", "CreateLog"); } - - //[AllowAnonymous] - //public ActionResult Register() - //{ - // return View(); - //} - - //[HttpPost] - //[AllowAnonymous] - //[ValidateAntiForgeryToken] - //public ActionResult Register(RegisterModel model) - //{ - // if (ModelState.IsValid) - // { - // // Attempt to register the user - // try - // { - // WebSecurity.CreateUserAndAccount(model.UserName, model.Password); - // WebSecurity.Login(model.UserName, model.Password); - // return RedirectToAction("Login"); - // } - // catch (MembershipCreateUserException e) - // { - // ModelState.AddModelError("", ErrorCodeToString(e.StatusCode)); - // } - // } - - // // If we got this far, something failed, redisplay form - // return View(model); - //} - + public ActionResult Manage(ManageMessageId? message) { ViewBag.StatusMessage = @@ -130,7 +107,7 @@ namespace MileageTraker.Web.Controllers // If we got this far, something failed, redisplay form return View(model); } - + private ActionResult RedirectToLocal(string returnUrl) { if (Url.IsLocalUrl(returnUrl)) @@ -145,5 +122,98 @@ namespace MileageTraker.Web.Controllers ChangePasswordSuccess, SetPasswordSuccess, } + + #region Reset Password + + /// + /// View the Reset Password form + /// + [AllowAnonymous] + [AcceptVerbs(HttpVerbs.Get)] + public ViewResult ResetPassword(string username) + { + return View(new ResetPasswordViewModel{Username = username}); + } + + /// + /// Begins the Reset Password process + /// + [AllowAnonymous] + [AcceptVerbs(HttpVerbs.Post)] + [ValidateAntiForgeryToken] + public ActionResult ResetPassword(ResetPasswordViewModel viewModel) + { + var user = DataService.FindUserByUsername(viewModel.Username); + if (user != null && Request.Url != null) + { + var passwordResetToken = Algorithms.GenerateToken(); + var url = Request.Url.Scheme + + @"://" + + Request.Url.Authority + + Url.Action("NewPassword", "Account", + new NewPasswordViewModel + { + UserId = user.UserId, + PasswordResetToken = passwordResetToken + }); + user.PasswordResetToken = passwordResetToken; + DataService.UpdateUser(user); + var email = new EmailNotificationService(); + email.NotifyResetPassword(user,url); + } + + TempData["StatusMessage"] = "Please check your email - we have sent a request for you to reset the password."; + // even when if not successful, let the user think they're getting a cookie + return RedirectToAction("Login"); + } + + /// + /// Action users are sent to when they reset their password. + /// + [AllowAnonymous] + [AcceptVerbs(HttpVerbs.Get)] + public ActionResult NewPassword(Guid userId, string passwordResetToken) + { + var user = DataService.GetUser(userId); + if (user != null && user.PasswordResetToken == passwordResetToken) + { + var newPasswordViewModel + = new NewPasswordViewModel + { + UserId = user.UserId, + Username = user.Username, + PasswordResetToken = passwordResetToken + }; + return View(newPasswordViewModel); + } + + return HttpNotFound(); + } + + /// + /// Set a new password. + /// + /// The view model. + [AllowAnonymous] + [AcceptVerbs(HttpVerbs.Post)] + [ValidateAntiForgeryToken] + public ActionResult NewPassword(NewPasswordViewModel viewModel) + { + if (ModelState.IsValid) + { + var user = DataService.GetUser(viewModel.UserId); + if (user != null && user.PasswordResetToken == viewModel.PasswordResetToken) + { + DataService.UpdateUserPassword(viewModel.UserId, viewModel.NewPassword); + TempData["StatusMessage"] = "Password set for " + viewModel.Username; + return RedirectToAction("Login", new {username = viewModel.Username}); + } + } + + // If we got this far, something failed, redisplay form + return View(viewModel); + } + + #endregion } } diff --git a/Web/Controllers/UserController.cs b/Web/Controllers/UserController.cs index 684a2d2..b5ab0af 100644 --- a/Web/Controllers/UserController.cs +++ b/Web/Controllers/UserController.cs @@ -132,9 +132,8 @@ namespace MileageTraker.Web.Controllers { var user = Membership.GetUser(id); if (user == null) - { return HttpNotFound(); - } + var viewModel = new SetPasswordViewModel {UserId = id, Username = user.UserName}; return View(viewModel); } @@ -144,16 +143,9 @@ namespace MileageTraker.Web.Controllers { if (ModelState.IsValid) { - try - { - DataService.UpdateUserPassword(viewModel.UserId, viewModel.NewPassword); - TempData["StatusMessage"] = "Password set for " + viewModel.Username; - return RedirectToAction("Details", new { id = viewModel.UserId}); - } - catch (Exception) - { - ModelState.AddModelError("", "The new password is invalid."); - } + DataService.UpdateUserPassword(viewModel.UserId, viewModel.NewPassword); + TempData["StatusMessage"] = "Password set for " + viewModel.Username; + return RedirectToAction("Details", new { id = viewModel.UserId}); } // If we got this far, something failed, redisplay form diff --git a/Web/DAL/DataService.cs b/Web/DAL/DataService.cs index cfa580a..14f7196 100644 --- a/Web/DAL/DataService.cs +++ b/Web/DAL/DataService.cs @@ -477,6 +477,7 @@ namespace MileageTraker.Web.DAL user.LastPasswordChangedDate = DateTime.Now; user.IsLockedOut = false; user.PasswordFailuresSinceLastSuccess = 0; + user.PasswordResetToken = null; UpdateUser(user); } diff --git a/Web/Email/EmailNotification.cs b/Web/Email/EmailNotification.cs new file mode 100644 index 0000000..dd9a71e --- /dev/null +++ b/Web/Email/EmailNotification.cs @@ -0,0 +1,45 @@ +using System.Configuration; +using System.Net.Mail; +using MileageTraker.Web.Models; +using log4net; + +namespace MileageTraker.Web.Email +{ + /// + /// Email Notification + /// + public class EmailNotificationService + { + //private static readonly ILog _logger = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); + + private readonly string _resetPasswordBody; + private readonly string _resetPasswordFromAddress; + private readonly string _resetPasswordSubject; + + private readonly SmtpClient _smtpClient; + + /// + /// Initializes a new instance of the class. + /// + /// The SMTP client. + public EmailNotificationService() + { + _smtpClient = new SmtpClient(); + + _resetPasswordBody = ConfigurationManager.AppSettings["ResetPasswordBody"]; + _resetPasswordFromAddress = ConfigurationManager.AppSettings["ResetPasswordFromAddress"]; + _resetPasswordSubject = ConfigurationManager.AppSettings["ResetPasswordSubject"]; + } + + /// + /// Sends the reset password email. + /// + /// To this user. + /// Reset url + public void NotifyResetPassword(User user, string url) + { + var body = string.Format(_resetPasswordBody, url); + _smtpClient.Send(new MailMessage(_resetPasswordFromAddress, user.Email, _resetPasswordSubject, body)); + } + } +} \ No newline at end of file diff --git a/Web/ViewModels/Account/NewPasswordViewModel.cs b/Web/ViewModels/Account/NewPasswordViewModel.cs new file mode 100644 index 0000000..0e4753d --- /dev/null +++ b/Web/ViewModels/Account/NewPasswordViewModel.cs @@ -0,0 +1,32 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.Web.Mvc; + +namespace MileageTraker.Web.ViewModels.Account +{ + /// + /// ViewModel for Verifying a Reset Password + /// + public class NewPasswordViewModel + { + [HiddenInput] + public Guid UserId { get; set; } + + [HiddenInput] + public string PasswordResetToken { get; set; } + + [HiddenInput] + public string Username { get; set; } + + [Required] + [StringLength(100, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 6)] + [DataType(DataType.Password)] + [Display(Name = "New password")] + public string NewPassword { get; set; } + + [DataType(DataType.Password)] + [Display(Name = "Confirm new password")] + [Compare("NewPassword", ErrorMessage = "The new password and confirmation password do not match.")] + public string ConfirmPassword { get; set; } + } +} \ No newline at end of file diff --git a/Web/ViewModels/Account/ResetPasswordViewModel.cs b/Web/ViewModels/Account/ResetPasswordViewModel.cs new file mode 100644 index 0000000..9d734be --- /dev/null +++ b/Web/ViewModels/Account/ResetPasswordViewModel.cs @@ -0,0 +1,11 @@ +using System.ComponentModel.DataAnnotations; + +namespace MileageTraker.Web.ViewModels.Account +{ + public class ResetPasswordViewModel + { + [Required] + [RegularExpression("[^@]*", ErrorMessage = "Enter just your username, not your email")] + public string Username { get; set; } + } +} \ No newline at end of file diff --git a/Web/Views/Account/Login.cshtml b/Web/Views/Account/Login.cshtml index cc47368..9e6b166 100644 --- a/Web/Views/Account/Login.cshtml +++ b/Web/Views/Account/Login.cshtml @@ -24,5 +24,6 @@ @Html.EditorForModel() + @Html.ActionLink("Forgot Password?", "ResetPassword", new { Model.Username }, new { @class = "pull-right" }) } diff --git a/Web/Views/Account/NewPassword.cshtml b/Web/Views/Account/NewPassword.cshtml new file mode 100644 index 0000000..659c6da --- /dev/null +++ b/Web/Views/Account/NewPassword.cshtml @@ -0,0 +1,28 @@ +@model MileageTraker.Web.ViewModels.Account.NewPasswordViewModel + +@{ + ViewBag.Title = "New Password"; + Layout = "~/Views/Shared/_Layout.login.cshtml"; +} + +@section Styles +{ + +} + +@using (Html.BeginForm("NewPassword", "Account", FormMethod.Post, new {@class = "form-login"})) { + +
+ +

@ViewBag.Title

+ + @Html.AntiForgeryToken() + @Html.Partial("_ValidationSummary") + +
+ + @Html.EditorForModel() + +
+} + diff --git a/Web/Views/Account/ResetPassword.cshtml b/Web/Views/Account/ResetPassword.cshtml new file mode 100644 index 0000000..abf4333 --- /dev/null +++ b/Web/Views/Account/ResetPassword.cshtml @@ -0,0 +1,34 @@ +@model MileageTraker.Web.ViewModels.Account.ResetPasswordViewModel + +@{ + ViewBag.Title = "Forgot password?"; + Layout = "~/Views/Shared/_Layout.login.cshtml"; +} +@section Styles +{ + +} + +@using (Html.BeginForm("ResetPassword", "Account", FormMethod.Post, new {@class = "form-login"})) { + + @Html.Partial("_StatusMessage") + +
+ +

@ViewBag.Title

+ +

Enter your username to + begin resetting your password. An email will + be sent to you with instructions for completing the process. +

+ + @Html.AntiForgeryToken() + @Html.Partial("_ValidationSummary") + +
+ + @Html.EditorForModel() + + @Html.ActionLink("Login", "Login", new { Model.Username }, new { @class = "pull-right" }) +
+} diff --git a/Web/Web.Release.config b/Web/Web.Release.config index 1288a60..829060f 100644 --- a/Web/Web.Release.config +++ b/Web/Web.Release.config @@ -33,4 +33,11 @@ + + + + + + + \ No newline at end of file diff --git a/Web/Web.config b/Web/Web.config index a5e31c2..c745656 100644 --- a/Web/Web.config +++ b/Web/Web.config @@ -13,7 +13,17 @@ + + + + + + + + + + diff --git a/Web/Web.csproj b/Web/Web.csproj index ed1bd8c..c22d45f 100644 --- a/Web/Web.csproj +++ b/Web/Web.csproj @@ -126,6 +126,7 @@ + 201204181847082_InitialMigration.cs @@ -139,11 +140,13 @@ 201212261822498_AddMembership.cs + + @@ -280,6 +283,8 @@ + +