diff --git a/Web.Tests/Membership/CryptoTests.cs b/Web.Tests/Membership/CryptoTests.cs new file mode 100644 index 0000000..d34ff2c --- /dev/null +++ b/Web.Tests/Membership/CryptoTests.cs @@ -0,0 +1,26 @@ +using MileageTraker.Web.Membership; +using NUnit.Framework; + +namespace Web.Tests.Membership +{ + [TestFixture] + public class CryptoTests + { + [Test] + public void HashPassword_Test() + { + const string password = "1da"; + var hashPassword = Crypto.HashPassword(password); + Assert.That(hashPassword, Is.Not.EqualTo(password)); + } + + [Test] + public void VerifyHashPassword_Test() + { + const string password = "1daFH2k1jv!3dsa"; + var hashPassword = Crypto.HashPassword(password); + bool verifyHashedPassword = Crypto.VerifyHashedPassword(hashPassword, password); + Assert.IsTrue(verifyHashedPassword); + } + } +} diff --git a/Web.Tests/Web.Tests.csproj b/Web.Tests/Web.Tests.csproj index 9811a0c..0e101e4 100644 --- a/Web.Tests/Web.Tests.csproj +++ b/Web.Tests/Web.Tests.csproj @@ -84,6 +84,7 @@ + diff --git a/Web/Controllers/AccountController.cs b/Web/Controllers/AccountController.cs new file mode 100644 index 0000000..7458469 --- /dev/null +++ b/Web/Controllers/AccountController.cs @@ -0,0 +1,173 @@ +using System; +using System.Web.Mvc; +using System.Web.Security; +using MileageTraker.Web.Membership; +using MileageTraker.Web.ViewModels; + +namespace MileageTraker.Web.Controllers +{ + [Authorize] + public class AccountController : Controller + { + [AllowAnonymous] + public ActionResult Login(string returnUrl) + { + ViewBag.ReturnUrl = returnUrl; + return View(); + } + + [HttpPost] + [AllowAnonymous] + [ValidateAntiForgeryToken] + public ActionResult Login(LoginViewModel model, string returnUrl) + { + if (ModelState.IsValid && WebSecurity.Login(model.EmailAddress, model.Password, model.RememberMe)) + { + // TODO: send notification to user + return RedirectToLocal(returnUrl); + } + + // If we got this far, something failed, redisplay form + ModelState.AddModelError("", "The user name or password provided is incorrect."); + return View(model); + } + + [HttpPost] + [ValidateAntiForgeryToken] + public ActionResult LogOff() + { + WebSecurity.Logout(); + + // TODO: send notification to user + return RedirectToAction("Login"); + } + + [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 = + message == ManageMessageId.ChangePasswordSuccess ? "Your password has been changed." + : message == ManageMessageId.SetPasswordSuccess ? "Your password has been set." + : message == ManageMessageId.RemoveLoginSuccess ? "The external login was removed." + : ""; + ViewBag.ReturnUrl = Url.Action("Manage"); + return View(); + } + + [HttpPost] + [ValidateAntiForgeryToken] + public ActionResult Manage(ChangePasswordViewModel model) + { + ViewBag.ReturnUrl = Url.Action("Manage"); + + if (ModelState.IsValid) + { + // ChangePassword will throw an exception rather than return false in certain failure scenarios. + bool changePasswordSucceeded; + try + { + changePasswordSucceeded = WebSecurity.ChangePassword(User.Identity.Name, model.OldPassword, model.NewPassword); + } + catch (Exception) + { + changePasswordSucceeded = false; + } + + if (changePasswordSucceeded) + { + return RedirectToAction("Manage", new { Message = ManageMessageId.ChangePasswordSuccess }); + } + else + { + ModelState.AddModelError("", "The current password is incorrect or the new password is invalid."); + } + } + + // If we got this far, something failed, redisplay form + return View(model); + } + + private ActionResult RedirectToLocal(string returnUrl) + { + if (Url.IsLocalUrl(returnUrl)) + { + return Redirect(returnUrl); + } + return RedirectToAction("Index", "Log"); + } + + public enum ManageMessageId + { + ChangePasswordSuccess, + SetPasswordSuccess, + RemoveLoginSuccess, + } + + private static string ErrorCodeToString(MembershipCreateStatus createStatus) + { + // See http://go.microsoft.com/fwlink/?LinkID=177550 for + // a full list of status codes. + switch (createStatus) + { + case MembershipCreateStatus.DuplicateUserName: + return "User name already exists. Please enter a different user name."; + + case MembershipCreateStatus.DuplicateEmail: + return "A user name for that e-mail address already exists. Please enter a different e-mail address."; + + case MembershipCreateStatus.InvalidPassword: + return "The password provided is invalid. Please enter a valid password value."; + + case MembershipCreateStatus.InvalidEmail: + return "The e-mail address provided is invalid. Please check the value and try again."; + + case MembershipCreateStatus.InvalidAnswer: + return "The password retrieval answer provided is invalid. Please check the value and try again."; + + case MembershipCreateStatus.InvalidQuestion: + return "The password retrieval question provided is invalid. Please check the value and try again."; + + case MembershipCreateStatus.InvalidUserName: + return "The user name provided is invalid. Please check the value and try again."; + + case MembershipCreateStatus.ProviderError: + return "The authentication provider returned an error. Please verify your entry and try again. If the problem persists, please contact your system administrator."; + + case MembershipCreateStatus.UserRejected: + return "The user creation request has been canceled. Please verify your entry and try again. If the problem persists, please contact your system administrator."; + + default: + return "An unknown error occurred. Please verify your entry and try again. If the problem persists, please contact your system administrator."; + } + } + } +} diff --git a/Web/Controllers/LogController.cs b/Web/Controllers/LogController.cs index 0ab3e73..b0326a9 100644 --- a/Web/Controllers/LogController.cs +++ b/Web/Controllers/LogController.cs @@ -8,6 +8,7 @@ using MileageTraker.Web.ViewModels; namespace MileageTraker.Web.Controllers { + [Authorize(Roles = "Administrator, Developer")] public class LogController : ControllerBase { private readonly DataService _dataService = new DataService(); diff --git a/Web/Controllers/VehicleController.cs b/Web/Controllers/VehicleController.cs index 5092645..7c27029 100644 --- a/Web/Controllers/VehicleController.cs +++ b/Web/Controllers/VehicleController.cs @@ -6,6 +6,7 @@ using MileageTraker.Web.ViewModels; namespace MileageTraker.Web.Controllers { + [Authorize(Roles = "Administrator, Developer")] public class VehicleController : ControllerBase { private readonly DataService _ds = new DataService(); diff --git a/Web/Membership/CodeFirstRoleProvider.cs b/Web/Membership/CodeFirstRoleProvider.cs index 72bfca4..83b0d5e 100644 --- a/Web/Membership/CodeFirstRoleProvider.cs +++ b/Web/Membership/CodeFirstRoleProvider.cs @@ -22,7 +22,7 @@ namespace MileageTraker.Web.Membership } using (var context = new MileageTrakerContext()) { - Role role = context.Roles.FirstOrDefault(rl => rl.RoleName == roleName); + var role = context.Roles.FirstOrDefault(rl => rl.RoleName == roleName); return role != null; } } @@ -39,8 +39,7 @@ namespace MileageTraker.Web.Membership } using (var context = new MileageTrakerContext()) { - User user; - user = context.Users.FirstOrDefault(usr => usr.Username == username); + var user = context.Users.FirstOrDefault(usr => usr.Username == username); if (user == null) { return false; @@ -70,7 +69,7 @@ namespace MileageTraker.Web.Membership } using (var context = new MileageTrakerContext()) { - Role role = context.Roles.FirstOrDefault(rl => rl.RoleName == roleName); + var role = context.Roles.FirstOrDefault(rl => rl.RoleName == roleName); if (role != null) { return role.Users.Select(usr => usr.Username).ToArray(); @@ -87,8 +86,7 @@ namespace MileageTraker.Web.Membership } using (var context = new MileageTrakerContext()) { - User user; - user = context.Users.FirstOrDefault(Usr => Usr.Username == username); + User user = context.Users.FirstOrDefault(Usr => Usr.Username == username); if (user != null) { return user.Roles.Select(rl => rl.RoleName).ToArray(); diff --git a/Web/Membership/Crypto.cs b/Web/Membership/Crypto.cs index 9b6a7ee..1fc6161 100644 --- a/Web/Membership/Crypto.cs +++ b/Web/Membership/Crypto.cs @@ -8,24 +8,11 @@ namespace MileageTraker.Web.Membership { public static class Crypto { - private const int TokenSizeInBytes = 16; private const int Pbkdf2Count = 1000; private const int Pbkdf2SubkeyLength = 256/8; private const int SaltSize = 128/8; - - public static string GenerateSalt(int byteLength = SaltSize) - { - var Buff = new byte[byteLength]; - using (var Prng = new RNGCryptoServiceProvider()) - { - Prng.GetBytes(Buff); - } - - return Convert.ToBase64String(Buff); - } - - public static string Hash(string input, string algorithm = "sha256") + private static string Hash(string input, string algorithm = "sha256") { if (input == null) { @@ -35,37 +22,24 @@ namespace MileageTraker.Web.Membership return Hash(Encoding.UTF8.GetBytes(input), algorithm); } - public static string Hash(byte[] input, string algorithm = "sha256") + private static string Hash(byte[] input, string algorithm = "sha256") { if (input == null) { throw new ArgumentNullException("input"); } - using (HashAlgorithm alg = HashAlgorithm.Create(algorithm)) + using (var alg = HashAlgorithm.Create(algorithm)) { if (alg != null) { - byte[] hashData = alg.ComputeHash(input); + var hashData = alg.ComputeHash(input); return BinaryToHex(hashData); } - else - { - throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "not supported hash alg", algorithm)); - } + throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "not supported hash alg", algorithm)); } } - public static string SHA1(string input) - { - return Hash(input, "sha1"); - } - - public static string SHA256(string input) - { - return Hash(input, "sha256"); - } - /* ======================= * HASHED PASSWORD FORMATS * ======================= @@ -79,9 +53,7 @@ namespace MileageTraker.Web.Membership public static string HashPassword(string password) { if (password == null) - { throw new ArgumentNullException("password"); - } byte[] salt; byte[] subkey; @@ -101,15 +73,11 @@ namespace MileageTraker.Web.Membership public static bool VerifyHashedPassword(string hashedPassword, string password) { if (hashedPassword == null) - { throw new ArgumentNullException("hashedPassword"); - } if (password == null) - { throw new ArgumentNullException("password"); - } - byte[] hashedPasswordBytes = Convert.FromBase64String(hashedPassword); + var hashedPasswordBytes = Convert.FromBase64String(hashedPassword); // Verify a version 0 (see comment above) password hash. @@ -132,7 +100,7 @@ namespace MileageTraker.Web.Membership return ByteArraysEqual(storedSubkey, generatedSubkey); } - internal static string BinaryToHex(byte[] data) + private static string BinaryToHex(byte[] data) { var hex = new char[data.Length*2]; diff --git a/Web/Membership/WebSecurity.cs b/Web/Membership/WebSecurity.cs index 316cbf0..f60d5b0 100644 --- a/Web/Membership/WebSecurity.cs +++ b/Web/Membership/WebSecurity.cs @@ -9,7 +9,7 @@ using MileageTraker.Web.Context; namespace MileageTraker.Web.Membership { - public sealed class WebSecurity + public static class WebSecurity { public static HttpContextBase Context { @@ -121,17 +121,17 @@ namespace MileageTraker.Web.Membership public static string CreateUserAndAccount(string userName, string password) { - return CreateUserAndAccount(userName, password, propertyValues: null, requireConfirmationToken: false); + return CreateUserAndAccount(userName, password, propertyValues: null); } public static string CreateUserAndAccount(string userName, string password, bool requireConfirmation) { - return CreateUserAndAccount(userName, password, propertyValues: null, requireConfirmationToken: requireConfirmation); + return CreateUserAndAccount(userName, password, null, requireConfirmation); } public static string CreateUserAndAccount(string userName, string password, IDictionary values) { - return CreateUserAndAccount(userName, password, propertyValues: values, requireConfirmationToken: false); + return CreateUserAndAccount(userName, password, values, requireConfirmationToken: false); } public static string CreateUserAndAccount(string userName, string password, object propertyValues = null, @@ -148,39 +148,30 @@ namespace MileageTraker.Web.Membership return codeFirstMembership.CreateUserAndAccount(userName, password, requireConfirmationToken, values); } - public static List FindUsersByEmail(string Email, int PageIndex, int PageSize) + public static List FindUsersByEmail(string email, int pageIndex, int pageSize) { int totalRecords; return - System.Web.Security.Membership.FindUsersByEmail(Email, PageIndex, PageSize, out totalRecords) + System.Web.Security.Membership.FindUsersByEmail(email, pageIndex, pageSize, out totalRecords) .Cast() .ToList(); } - public static List FindUsersByName(string Username, int PageIndex, int PageSize) + public static List FindUsersByName(string username, int pageIndex, int pageSize) { int totalRecords; return - System.Web.Security.Membership.FindUsersByName(Username, PageIndex, PageSize, out totalRecords) + System.Web.Security.Membership.FindUsersByName(username, pageIndex, pageSize, out totalRecords) .Cast() .ToList(); } - public static List GetAllUsers(int PageIndex, int PageSize) + public static List GetAllUsers(int pageIndex, int pageSize) { int totalRecords; return - System.Web.Security.Membership.GetAllUsers(PageIndex, PageSize, out totalRecords).Cast().ToList(); + System.Web.Security.Membership.GetAllUsers(pageIndex, pageSize, out totalRecords).Cast().ToList(); } - public static void InitializeDatabaseConnection(string connectionStringName, string userTableName, string userIdColumn, - string userNameColumn, bool autoCreateTables) - { - } - - public static void InitializeDatabaseConnection(string connectionString, string providerName, string userTableName, - string userIdColumn, string userNameColumn, bool autoCreateTables) - { - } } } \ No newline at end of file diff --git a/Web/Migrations/201212191949311_AddMembership.Designer.cs b/Web/Migrations/201212191949311_AddMembership.Designer.cs new file mode 100644 index 0000000..3ea79cf --- /dev/null +++ b/Web/Migrations/201212191949311_AddMembership.Designer.cs @@ -0,0 +1,24 @@ +// +namespace MileageTraker.Web.Migrations +{ + using System.Data.Entity.Migrations; + using System.Data.Entity.Migrations.Infrastructure; + + public sealed partial class AddMembership : IMigrationMetadata + { + string IMigrationMetadata.Id + { + get { return "201212191949311_AddMembership"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return "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"; } + } + } +} diff --git a/Web/Migrations/201212191949311_AddMembership.cs b/Web/Migrations/201212191949311_AddMembership.cs new file mode 100644 index 0000000..88168aa --- /dev/null +++ b/Web/Migrations/201212191949311_AddMembership.cs @@ -0,0 +1,130 @@ +namespace MileageTraker.Web.Migrations +{ + using System.Data.Entity.Migrations; + + public partial class AddMembership : DbMigration + { + public override void Up() + { + CreateTable( + "User", + c => new + { + UserId = c.Guid(nullable: false), + Username = c.String(nullable: false), + Email = c.String(), + Password = c.String(nullable: false), + FirstName = c.String(), + LastName = c.String(), + Comment = c.String(), + IsApproved = c.Boolean(nullable: false), + PasswordFailuresSinceLastSuccess = c.Int(nullable: false), + LastPasswordFailureDate = c.DateTime(), + LastActivityDate = c.DateTime(), + LastLockoutDate = c.DateTime(), + LastLoginDate = c.DateTime(), + ConfirmationToken = c.String(), + CreateDate = c.DateTime(), + IsLockedOut = c.Boolean(nullable: false), + LastPasswordChangedDate = c.DateTime(), + PasswordVerificationToken = c.String(), + PasswordVerificationTokenExpirationDate = c.DateTime(), + }) + .PrimaryKey(t => t.UserId); + + CreateTable( + "Role", + c => new + { + RoleId = c.Guid(nullable: false), + RoleName = c.String(nullable: false), + Description = c.String(), + }) + .PrimaryKey(t => t.RoleId); + + CreateTable( + "RoleUser", + c => new + { + Role_RoleId = c.Guid(nullable: false), + User_UserId = c.Guid(nullable: false), + }) + .PrimaryKey(t => new { t.Role_RoleId, t.User_UserId }) + .ForeignKey("Role", t => t.Role_RoleId, cascadeDelete: true) + .ForeignKey("User", t => t.User_UserId, cascadeDelete: true) + .Index(t => t.Role_RoleId) + .Index(t => t.User_UserId); + + Sql(@"INSERT INTO [Role] + ([RoleId] + ,[RoleName] + ,[Description]) + VALUES + ('f5bc8c1f-9e8b-4fd8-87d9-27404d8995d5' + ,'Administrator' + ,'Administrator'), + ('bb3bec25-2a4d-467a-b790-e24b53a54717' + ,'Developer' + ,'Developer')"); + + Sql(@"INSERT INTO [User] + ([UserId] + ,[Username] + ,[Email] + ,[Password] + ,[FirstName] + ,[LastName] + ,[Comment] + ,[IsApproved] + ,[PasswordFailuresSinceLastSuccess] + ,[LastPasswordFailureDate] + ,[LastActivityDate] + ,[LastLockoutDate] + ,[LastLoginDate] + ,[ConfirmationToken] + ,[CreateDate] + ,[IsLockedOut] + ,[LastPasswordChangedDate] + ,[PasswordVerificationToken] + ,[PasswordVerificationTokenExpirationDate]) + VALUES + ('33EA9A48-628A-458C-A9C9-FBB318C3CF0E' + ,'james.kolpack@gmail.com' + ,'james.kolpack@gmail.com' + ,'AH6VqEzZky4QfexgSwJIgg5gHrjaKGEk7pxUVBZz7X1Vkl31TlCUz9yTxmJN3/IDag==' + ,'James' + ,'Kolpack' + ,NULL + ,1 + ,0 + ,NULL + ,NULL + ,NULL + ,NULL + ,NULL + ,GETDATE() + ,0 + ,NULL + ,NULL + ,NULL)"); + + Sql(@"INSERT INTO [RoleUser] + ([Role_RoleId] + ,[User_UserId]) + VALUES + ('bb3bec25-2a4d-467a-b790-e24b53a54717' + ,'33EA9A48-628A-458C-A9C9-FBB318C3CF0E')"); + } + + public override void Down() + { + DropIndex("RoleUser", new[] { "User_UserId" }); + DropIndex("RoleUser", new[] { "Role_RoleId" }); + DropForeignKey("RoleUser", "User_UserId", "User"); + DropForeignKey("RoleUser", "Role_RoleId", "Role"); + DropTable("RoleUser"); + DropTable("Role"); + DropTable("User"); + } + } +} diff --git a/Web/ViewModels/AccountModels.cs b/Web/ViewModels/AccountModels.cs new file mode 100644 index 0000000..709274d --- /dev/null +++ b/Web/ViewModels/AccountModels.cs @@ -0,0 +1,57 @@ +using System.ComponentModel.DataAnnotations; +using System.Web.Mvc; + +namespace MileageTraker.Web.ViewModels +{ + public class ChangePasswordViewModel + { + [Required] + [DataType(DataType.Password)] + [Display(Name = "Current password")] + public string OldPassword { 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; } + } + + public class LoginViewModel + { + [Required] + [Display(Name = "Email Address")] + public string EmailAddress { get; set; } + + [Required] + [DataType(DataType.Password)] + [Display(Name = "Password")] + public string Password { get; set; } + + [Display(Name = "Remember me?")] + public bool RememberMe { get; set; } + } + + public class RegisterModel + { + [Required] + [Display(Name = "User name")] + 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 = "Password")] + public string Password { get; set; } + + [DataType(DataType.Password)] + [Display(Name = "Confirm password")] + [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")] + public string ConfirmPassword { get; set; } + } +} diff --git a/Web/Views/Account/Login.cshtml b/Web/Views/Account/Login.cshtml new file mode 100644 index 0000000..1577b45 --- /dev/null +++ b/Web/Views/Account/Login.cshtml @@ -0,0 +1,28 @@ +@model MileageTraker.Web.ViewModels.LoginViewModel + +@{ + ViewBag.Title = "Log in"; + Layout = "~/Views/Shared/_Layout.login.cshtml"; +} +@section Scripts +{ + + +} + +

@ViewBag.Title

+ +@using (Html.BeginForm("Login", "Account", new { ViewBag.ReturnUrl }, FormMethod.Post)) { + @Html.AntiForgeryToken() + @Html.ValidationSummary(true) + +
+ + @Html.EditorForModel() +
+
+ +
+
+
+} diff --git a/Web/Views/Account/Manage.cshtml b/Web/Views/Account/Manage.cshtml new file mode 100644 index 0000000..2fc5f58 --- /dev/null +++ b/Web/Views/Account/Manage.cshtml @@ -0,0 +1,14 @@ +@model MileageTraker.Web.ViewModels.ChangePasswordViewModel +@{ + ViewBag.Title = "Manage Account"; +} + +
+

@ViewBag.Title

+
+ +

@ViewBag.StatusMessage

+ +

You're logged in as @User.Identity.Name.

+ +@Html.Partial("_ChangePasswordPartial") diff --git a/Web/Views/Account/Register.cshtml b/Web/Views/Account/Register.cshtml new file mode 100644 index 0000000..0937e1a --- /dev/null +++ b/Web/Views/Account/Register.cshtml @@ -0,0 +1,33 @@ +@model MileageTraker.Web.ViewModels.RegisterModel +@{ + ViewBag.Title = "Register"; +} + +
+

@ViewBag.Title

+

Create a new account.

+
+ +@using (Html.BeginForm()) { + @Html.AntiForgeryToken() + @Html.ValidationSummary() + +
+ Registration Form +
    +
  1. + @Html.LabelFor(m => m.UserName) + @Html.TextBoxFor(m => m.UserName) +
  2. +
  3. + @Html.LabelFor(m => m.Password) + @Html.PasswordFor(m => m.Password) +
  4. +
  5. + @Html.LabelFor(m => m.ConfirmPassword) + @Html.PasswordFor(m => m.ConfirmPassword) +
  6. +
+ +
+} diff --git a/Web/Views/Account/_ChangePasswordPartial.cshtml b/Web/Views/Account/_ChangePasswordPartial.cshtml new file mode 100644 index 0000000..f28e70f --- /dev/null +++ b/Web/Views/Account/_ChangePasswordPartial.cshtml @@ -0,0 +1,27 @@ +@model MileageTraker.Web.ViewModels.ChangePasswordViewModel + +

Change password

+ +@using (Html.BeginForm("Manage", "Account")) { + @Html.AntiForgeryToken() + @Html.ValidationSummary() + +
+ Change Password Form +
    +
  1. + @Html.LabelFor(m => m.OldPassword) + @Html.PasswordFor(m => m.OldPassword) +
  2. +
  3. + @Html.LabelFor(m => m.NewPassword) + @Html.PasswordFor(m => m.NewPassword) +
  4. +
  5. + @Html.LabelFor(m => m.ConfirmPassword) + @Html.PasswordFor(m => m.ConfirmPassword) +
  6. +
+ +
+} diff --git a/Web/Views/Account/_SetPasswordPartial.cshtml b/Web/Views/Account/_SetPasswordPartial.cshtml new file mode 100644 index 0000000..6f1f78a --- /dev/null +++ b/Web/Views/Account/_SetPasswordPartial.cshtml @@ -0,0 +1,25 @@ +@model MileageTraker.Web.ViewModels.ChangePasswordViewModel + +

+ You do not have a password for this site. +

+ +@using (Html.BeginForm("Manage", "Account")) { + @Html.AntiForgeryToken() + @Html.ValidationSummary() + +
+ Set Password Form +
    +
  1. + @Html.LabelFor(m => m.NewPassword) + @Html.PasswordFor(m => m.NewPassword) +
  2. +
  3. + @Html.LabelFor(m => m.ConfirmPassword) + @Html.PasswordFor(m => m.ConfirmPassword) +
  4. +
+ +
+} diff --git a/Web/Views/Log/Edit.cshtml b/Web/Views/Log/Edit.cshtml index ddbe58b..d4d8f7f 100644 --- a/Web/Views/Log/Edit.cshtml +++ b/Web/Views/Log/Edit.cshtml @@ -22,6 +22,7 @@ { @Html.ValidationSummary(true)
+ @Html.EditorForModel() diff --git a/Web/Views/Shared/EditorTemplates/Boolean.cshtml b/Web/Views/Shared/EditorTemplates/Boolean.cshtml new file mode 100644 index 0000000..15f65c1 --- /dev/null +++ b/Web/Views/Shared/EditorTemplates/Boolean.cshtml @@ -0,0 +1,4 @@ + \ No newline at end of file diff --git a/Web/Views/Shared/EditorTemplates/Password.cshtml b/Web/Views/Shared/EditorTemplates/Password.cshtml new file mode 100644 index 0000000..99fcaa5 --- /dev/null +++ b/Web/Views/Shared/EditorTemplates/Password.cshtml @@ -0,0 +1,12 @@ +@{ + Layout = "~/Views/Shared/EditorTemplates/_FieldLayout.cshtml"; + var inputSize = (string)ViewData.ModelMetadata.AdditionalValues["InputSize"]; +} +@if (!string.IsNullOrEmpty(inputSize)) +{ + @Html.Password("", ViewData.TemplateInfo.FormattedModelValue, new { @class = inputSize}) +} +else +{ + @Html.Password("", ViewData.TemplateInfo.FormattedModelValue) +} diff --git a/Web/Views/Shared/_Layout.admin.cshtml b/Web/Views/Shared/_Layout.admin.cshtml index edd3100..2489fd3 100644 --- a/Web/Views/Shared/_Layout.admin.cshtml +++ b/Web/Views/Shared/_Layout.admin.cshtml @@ -17,6 +17,9 @@
  • @Html.ActionLink("Vehicles", "Index", "Vehicle")
  • @Html.ActionLink("Enter Mileage Log", "Index", "CreateLog")
  • +
    + @Html.Partial("_Layout.authentication.cshtml") +
    diff --git a/Web/Views/Shared/_Layout.authentication.cshtml.cshtml b/Web/Views/Shared/_Layout.authentication.cshtml.cshtml new file mode 100644 index 0000000..91655d3 --- /dev/null +++ b/Web/Views/Shared/_Layout.authentication.cshtml.cshtml @@ -0,0 +1,15 @@ +@if (Request.IsAuthenticated) { + +} \ No newline at end of file diff --git a/Web/Views/Shared/_Layout.login.cshtml b/Web/Views/Shared/_Layout.login.cshtml new file mode 100644 index 0000000..5b6a01d --- /dev/null +++ b/Web/Views/Shared/_Layout.login.cshtml @@ -0,0 +1,25 @@ + + + + Mileage Traker - ETHRA - @ViewBag.Title + + + + + @RenderSection("Styles", false) + + + + +
    + @RenderBody() +
    + + + + + + + @RenderSection("Scripts", false) + + diff --git a/Web/Web.config b/Web/Web.config index cac07aa..f809e2a 100644 --- a/Web/Web.config +++ b/Web/Web.config @@ -35,6 +35,9 @@ + + + @@ -42,7 +45,6 @@ - diff --git a/Web/Web.csproj b/Web/Web.csproj index a011428..5eb39e7 100644 --- a/Web/Web.csproj +++ b/Web/Web.csproj @@ -108,6 +108,7 @@ + @@ -123,11 +124,16 @@ 201212042021486_AddVehiclePreviousLog.cs + + + 201212191949311_AddMembership.cs + + @@ -238,6 +244,15 @@ + + + + + + + + + @@ -356,6 +371,7 @@ + 10.0 $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)