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")
+
+
+}
+
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")
+
+
+}
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 @@
+
+