diff --git a/Web/Content/Site.css b/Web/Content/Site.css index a2c973d..048c793 100644 --- a/Web/Content/Site.css +++ b/Web/Content/Site.css @@ -6,7 +6,6 @@ body { .center-content { display: block; max-width: 400px; - padding: 10px; margin: 0 auto 20px; } @@ -78,10 +77,6 @@ footer { padding: 4px; } -.alert .close { - position: static; -} - .qtip-content dt { line-height: 10px; width: 110px; @@ -211,7 +206,6 @@ dl.inline { } .center-content { - padding: 5px; margin: 0 auto 10px; } diff --git a/Web/Controllers/AccountController.cs b/Web/Controllers/AccountController.cs index 87ba7c7..3943673 100644 --- a/Web/Controllers/AccountController.cs +++ b/Web/Controllers/AccountController.cs @@ -1,6 +1,7 @@ using System; using System.Web.Mvc; using System.Web.Security; +using MileageTraker.Web.DAL; using MileageTraker.Web.ViewModels.Account; namespace MileageTraker.Web.Controllers @@ -22,17 +23,28 @@ namespace MileageTraker.Web.Controllers { if (ModelState.IsValid) { - var success = Membership.ValidateUser(model.Username, model.Password); - if (success) + try { - FormsAuthentication.SetAuthCookie(model.Username, model.RememberMe); - TempData["StatusMessage"] = "Logged in as " + model.Username; - return RedirectToLocal(returnUrl); + var success = Membership.ValidateUser(model.Username, model.Password); + if (success) + { + FormsAuthentication.SetAuthCookie(model.Username, model.RememberMe); + TempData["StatusMessage"] = "Logged in as " + model.Username; + return RedirectToLocal(returnUrl); + } + ModelState.AddModelError("", "The user name or password provided is incorrect."); + } + catch (UserAccountDisabledException) + { + ModelState.AddModelError("", "Account is disabled for " + model.Username + "."); + } + catch (UserLockedOutException) + { + ModelState.AddModelError("", "Too many failed password attempts for " + model.Username + ". Account is locked."); } } // If we got this far, something failed, redisplay form - ModelState.AddModelError("", "The user name or password provided is incorrect."); return View(model); } @@ -40,9 +52,10 @@ namespace MileageTraker.Web.Controllers [ValidateAntiForgeryToken] public ActionResult LogOff() { + TempData["StatusMessage"] = User.Identity.Name + " logged off"; + FormsAuthentication.SignOut(); - TempData["StatusMessage"] = "Logged off"; return RedirectToAction("Index", "CreateLog"); } @@ -81,7 +94,7 @@ namespace MileageTraker.Web.Controllers ViewBag.StatusMessage = message == ManageMessageId.ChangePasswordSuccess ? "Your password has been changed." : message == ManageMessageId.SetPasswordSuccess ? "Your password has been set." - : ""; + : null; ViewBag.ReturnUrl = Url.Action("Manage"); return View(); } @@ -108,7 +121,7 @@ namespace MileageTraker.Web.Controllers if (changePasswordSucceeded) { - TempData["StatusMessage"] = ManageMessageId.ChangePasswordSuccess; + TempData["StatusMessage"] = "Your password has been changed."; return RedirectToAction("Manage"); } ModelState.AddModelError("", "The current password is incorrect or the new password is invalid."); diff --git a/Web/Controllers/CreateLogController.cs b/Web/Controllers/CreateLogController.cs index 4dbe450..888838b 100644 --- a/Web/Controllers/CreateLogController.cs +++ b/Web/Controllers/CreateLogController.cs @@ -45,7 +45,7 @@ namespace MileageTraker.Web.Controllers [HttpParamAction] [HttpPost] [ActionLog] - public ViewResult Confirm(CreateLogViewModel model) + public ActionResult Confirm(CreateLogViewModel model) { if (ModelState.IsValid) { @@ -59,7 +59,16 @@ namespace MileageTraker.Web.Controllers log.UserHostAddress = HttpContext.Request.UserHostAddress; log.UserAgent = HttpContext.Request.UserAgent; DataService.AddLog(log); - return View("Success", model); + TempData["StatusMessage-Type"] = "alert-success"; + TempData["StatusMessage"] = + @"You've successfully created an entry + for " + model.EmployeeName + @" + traveling to " + model.CityName + @" + on " + model.Date.ToShortDateString() + @" + in Vehicle Id " + model.VehicleId + @" + ending in " + model.EndOdometer + @" + miles on the odometer."; + return RedirectToAction("Index"); } return View("Index", model); diff --git a/Web/Controllers/UserController.cs b/Web/Controllers/UserController.cs index 8e4a69a..684a2d2 100644 --- a/Web/Controllers/UserController.cs +++ b/Web/Controllers/UserController.cs @@ -24,6 +24,18 @@ namespace MileageTraker.Web.Controllers return View(DataService.GetUser(id)); } + public JsonResult UsernameAvailable(string username) + { + var user = DataService.FindUserByUsername(username); + return Json(user == null, JsonRequestBehavior.AllowGet); + } + + public JsonResult EmailAvailable(string email) + { + var user = DataService.FindUserByEmail(email); + return Json(user == null, JsonRequestBehavior.AllowGet); + } + public ActionResult Create() { var vm = new CreateUserViewModel @@ -111,7 +123,7 @@ namespace MileageTraker.Web.Controllers } TempData["StatusMessage"] = "Changes saved for " + user.Username; - return RedirectToAction("Index"); + return RedirectToAction("Details", new { id = viewModel.UserId}); } return View(viewModel); } @@ -132,7 +144,6 @@ namespace MileageTraker.Web.Controllers { if (ModelState.IsValid) { - // ChangePassword will throw an exception rather than return false in certain failure scenarios. try { DataService.UpdateUserPassword(viewModel.UserId, viewModel.NewPassword); @@ -149,6 +160,58 @@ namespace MileageTraker.Web.Controllers return View(viewModel); } + public ActionResult DisableUser(Guid id) + { + var user = DataService.GetUser(id); + if (user == null) + { + return HttpNotFound(); + } + user.IsApproved = false; + DataService.UpdateUser(user); + + TempData["StatusMessage"] = user.Username + " disabled"; + + if (Request.UrlReferrer != null) + return Redirect(Request.UrlReferrer.AbsolutePath); + return RedirectToAction("Index"); + } + + public ActionResult EnableUser(Guid id) + { + var user = DataService.GetUser(id); + if (user == null) + { + return HttpNotFound(); + } + user.IsApproved = true; + DataService.UpdateUser(user); + + TempData["StatusMessage"] = user.Username + " enabled"; + + if (Request.UrlReferrer != null) + return Redirect(Request.UrlReferrer.AbsolutePath); + return RedirectToAction("Index"); + } + + public ActionResult UnlockUser(Guid id) + { + var user = DataService.GetUser(id); + if (user == null) + { + return HttpNotFound(); + } + user.IsLockedOut = false; + user.PasswordFailuresSinceLastSuccess = 0; + DataService.UpdateUser(user); + + TempData["StatusMessage"] = user.Username + " unlocked"; + + if (Request.UrlReferrer != null) + return Redirect(Request.UrlReferrer.AbsolutePath); + return RedirectToAction("Index"); + } + private static string ErrorCodeToString(MembershipCreateStatus createStatus) { // See http://go.microsoft.com/fwlink/?LinkID=177550 for diff --git a/Web/Controllers/VehicleController.cs b/Web/Controllers/VehicleController.cs index 2a2045c..a120081 100644 --- a/Web/Controllers/VehicleController.cs +++ b/Web/Controllers/VehicleController.cs @@ -62,7 +62,8 @@ namespace MileageTraker.Web.Controllers } return View(vehicle); } - + + [AllowAnonymous] public JsonResult Exists(string vehicleId) { var vehicle = DataService.GetVehicle(vehicleId); diff --git a/Web/DAL/CodeFirstMembershipProvider.cs b/Web/DAL/CodeFirstMembershipProvider.cs index 1dd6b0d..2d28b26 100644 --- a/Web/DAL/CodeFirstMembershipProvider.cs +++ b/Web/DAL/CodeFirstMembershipProvider.cs @@ -136,11 +136,11 @@ namespace MileageTraker.Web.DAL } if (!user.IsApproved) { - return false; + throw new UserAccountDisabledException(); } if (user.IsLockedOut) { - return false; + throw new UserLockedOutException(); } var hashedPassword = user.Password; var verificationSucceeded = (hashedPassword != null && Crypto.VerifyHashedPassword(hashedPassword, password)); diff --git a/Web/DAL/DataService.cs b/Web/DAL/DataService.cs index 98c21d5..cfa580a 100644 --- a/Web/DAL/DataService.cs +++ b/Web/DAL/DataService.cs @@ -475,6 +475,8 @@ namespace MileageTraker.Web.DAL var user = GetUser(userId); user.Password = Crypto.HashPassword(password); user.LastPasswordChangedDate = DateTime.Now; + user.IsLockedOut = false; + user.PasswordFailuresSinceLastSuccess = 0; UpdateUser(user); } diff --git a/Web/DAL/UserAccountDisabledException.cs b/Web/DAL/UserAccountDisabledException.cs new file mode 100644 index 0000000..214e7ce --- /dev/null +++ b/Web/DAL/UserAccountDisabledException.cs @@ -0,0 +1,8 @@ +using System; + +namespace MileageTraker.Web.DAL +{ + public class UserAccountDisabledException : Exception + { + } +} \ No newline at end of file diff --git a/Web/DAL/UserLockedOutException.cs b/Web/DAL/UserLockedOutException.cs new file mode 100644 index 0000000..eda2c71 --- /dev/null +++ b/Web/DAL/UserLockedOutException.cs @@ -0,0 +1,8 @@ +using System; + +namespace MileageTraker.Web.DAL +{ + public class UserLockedOutException : Exception + { + } +} \ No newline at end of file diff --git a/Web/Scripts/Shared/Site.js b/Web/Scripts/Shared/Site.js index 41836c9..c0e870f 100644 --- a/Web/Scripts/Shared/Site.js +++ b/Web/Scripts/Shared/Site.js @@ -253,7 +253,8 @@ $(function () { }, unhighlight: function (element) { $(element).closest(".control-group").removeClass("error"); - } + }, + debug: true }); }); diff --git a/Web/Utility/CustomExtensions.cs b/Web/Utility/CustomExtensions.cs index 88b79c7..874d80d 100644 --- a/Web/Utility/CustomExtensions.cs +++ b/Web/Utility/CustomExtensions.cs @@ -6,7 +6,6 @@ using System.Linq; using System.Reflection; using System.Text; using System.Web.Mvc; -using MileageTraker.Web.Models; namespace MileageTraker.Web.Utility { @@ -182,11 +181,5 @@ namespace MileageTraker.Web.Utility Convert.ToDouble( System.Web.Security.Membership.UserIsOnlineTimeWindow))); } - - public static bool IsOnline(this User user) - { - return user.LastActivityDate != null - && user.LastActivityDate > UserOnlineThreshold(); - } } } \ No newline at end of file diff --git a/Web/ViewModels/User/CreateUserViewModel.cs b/Web/ViewModels/User/CreateUserViewModel.cs index fbc3052..6da2c9b 100644 --- a/Web/ViewModels/User/CreateUserViewModel.cs +++ b/Web/ViewModels/User/CreateUserViewModel.cs @@ -1,4 +1,5 @@ using System.ComponentModel.DataAnnotations; +using System.Web.Mvc; using AutoMapper; using MileageTraker.Web.Attributes; @@ -9,12 +10,14 @@ namespace MileageTraker.Web.ViewModels.User [Required] [StringLength(64)] [InputSize("small")] + [Remote("UsernameAvailable", "User", ErrorMessage = "Username already in use")] public string Username { get; set; } [Required] [DataType(DataType.EmailAddress)] [RegularExpression(@"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}", ErrorMessage = "Must be an email address")] [InputSize("large")] + [Remote("EmailAvailable", "User", ErrorMessage = "Email already in use")] public string Email { get; set; } [Required] diff --git a/Web/Views/Account/Login.cshtml b/Web/Views/Account/Login.cshtml index f49f92d..cc47368 100644 --- a/Web/Views/Account/Login.cshtml +++ b/Web/Views/Account/Login.cshtml @@ -10,7 +10,9 @@ } @using (Html.BeginForm("Login", "Account", new { ViewBag.ReturnUrl }, FormMethod.Post, new {@class = "form-login"})) { - + + @Html.Partial("_StatusMessage") +
- You've successfully created an entry - for @Html.DisplayTextFor(m => m.EmployeeName) - traveling to @Html.DisplayTextFor(m => m.CityName) - on @Html.Encode(Model.Date.ToShortDateString()) - in Vehicle Id @Html.DisplayTextFor(m => m.VehicleId) - ending in @Html.DisplayTextFor(m => m.EndOdometer) - miles on the odometer. -
-- @Html.ActionLink("Create another", "Index", null, new { @class = "btn" }) -
diff --git a/Web/Views/Log/Create.cshtml b/Web/Views/Log/Create.cshtml index 0394840..353b669 100644 --- a/Web/Views/Log/Create.cshtml +++ b/Web/Views/Log/Create.cshtml @@ -6,6 +6,8 @@ @{ Html.RenderPartial("BackToLogs"); } +@Html.Partial("_StatusMessage") ++
- @TempData["StatusMessage"] + @Html.Raw(TempData["StatusMessage"])
} else if (ViewBag.StatusMessage != null) { -+
@ViewBag.StatusMessage
diff --git a/Web/Views/User/Details.cshtml b/Web/Views/User/Details.cshtml index 6c3acc3..ab6592b 100644 --- a/Web/Views/User/Details.cshtml +++ b/Web/Views/User/Details.cshtml @@ -12,9 +12,20 @@- Locked out on @Html.DisplayTextFor(m => m.LastLockoutDate) -
+