© Lee Naylor 2016

Lee Naylor, ASP.NET MVC with Entity Framework and CSS , 10.1007/978-1-4842-2137-2_7

7. Authentication and Authorization Using ASP.NET Identity

Lee Naylor

(1)Newton-le-Willows, Merseyside, UK

We now have a web site where users can create, edit, and search for products and manage images. At the moment, though, anyone can just open the site and edit, create, and delete products and images. This chapter shows you how to add some authentication so users can log in and how to add authorization based on roles to determine what tasks users can perform. The code in this chapter uses Microsoft ASP.NET Identity v2 combined with SQL LocalDb. Throughout the code in this chapter, you will see references to OWIN, which stands for Open Web Interface for .NET. The idea behind OWIN is that it acts as a layer of abstraction between a web application and the hosting environment. More information about OWIN is available at http://owin.org/ .

Note

If you want to follow along with the code in this chapter, you must have completed Chapter 6 or download Chapter 6’s source code from www.apress.com as a starting point.

To keep the examples as simple as possible and so that you can follow this in your own projects, much of the code is based on the Microsoft sample Identity 2.x code. If you want to view this sample code, open a new empty ASP.NET project and install this package into it. Do not install the Microsoft sample identity code into the BabyStore project. You can find this sample code by choosing Project ➤ Manage NuGet Packages, checking the Include Prerelease box, and then searching for “identity samples” in the NuGet Package Manager window. Figure 7-1 shows the Microsoft.AspNet.Identity.Sample package.

A419071_1_En_7_Fig1_HTML.jpg
Figure 7-1. Locating the Microsoft Identity Samples package via NuGet. Never install this package into an existing project. It should only be installed into an empty ASP.NET project as shown in the package description

Examining the Automatically Created BabyStore Project Identity Code and Database

When we initially created the BabyStore project in Chapter 1, we set the Authentication option to Individual User Accounts. Setting this option meant that the project was created with some Identity code to cater for the basic user registration and management of some user properties. During the project creation a database context was created and a database connection string was added to the Web.Config file. The database context is named ApplicationDbContext and inherits from IdentityDbContext<ApplicationUser>. IdentityDbContext inherits from DbContext. ApplicationDbContext can be found in the ModelsIdentityModels.cs file. The default code for the context is shown here.

public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
{
    public ApplicationDbContext()
        : base("DefaultConnection", throwIfV1Schema: false)
    {
    }


    public static ApplicationDbContext Create()
    {
        return new ApplicationDbContext();
    }
}

This context will use the connection string DefaultConnection to connect to the database to be used for storing Identity details. The Web.Config file contains this connection string. The following code shows the DefaultConnection connectionString as listed in the BabyStore project’s Web.Config file:

<connectionStrings>
  <add name="DefaultConnection" connectionString="Data Source=(LocalDb)MSSQLLocalDB;AttachDbFilename=|DataDirectory|aspnet-BabyStore-20160217123618.mdf;Initial Catalog=aspnet-BabyStore-20160217123618;Integrated Security=True" providerName="System.Data.SqlClient" />
  <add name="StoreContext" connectionString="Data Source=(LocalDB)MSSQLLocalDB;AttachDbFilename=|DataDirectory|BabyStore3.mdf;Initial Catalog=BabyStore3;Integrated Security=True" providerName="System.Data.SqlClient" />
</connectionStrings>

When a request is made that requires identity (e.g. a user tries to log in), the Identity database is created in the App_Data directory. The database name is generated automatically by Visual Studio; in this case, the database file is aspnet-BabyStore-20160217123618.mdf. Your filename will differ based on the date and time that the connection string is created.

To see the current database state, try to log in to the web site using the username [email protected] and any password. The attempt will fail and you should receive an error from the login page, as shown in Figure 7-2.

A419071_1_En_7_Fig2_HTML.jpg
Figure 7-2. An invalid login attempt

If you expand the App_Data folder in Visual Studio’s Solution Explorer window, you should see the new database file that will be used for identity purposes, as shown in Figure 7-3. If you don’t see the new database, try clicking the Refresh button in Solution Explorer.

A419071_1_En_7_Fig3_HTML.jpg
Figure 7-3. The newly created identity database (highlighted) in the App_Data folder

Next, open the database in Server Explorer by right-clicking it and choosing Open from the menu. Note that when the database opens in Server Explorer, it appears under the DefaultConnection item rather than StoreContext; this is because it uses the ConnectionString DefaultConnection in Web.Config.

The database contains six tables, as shown in Figure 7-4.

A419071_1_En_7_Fig4_HTML.jpg
Figure 7-4. The default tables in the Identity database
  • _MigrationHistory is used by Entity Framework to keep a log of all the migrations relating to the database.

  • AspNetRoles holds the roles that users can belong to.

  • AspNetUserClaims holds claims data. A claim is information about a user that can be used as an alternative to a role for authorization purposes.

  • AspNetUserLogins is used to hold data about third-party or external logins, for example, users logging in via Google or Facebook.

  • AspNetUserRoles is used to map users to roles.

  • AspNetUsers holds user data.

In this chapter, we’ll work with users and roles and the related tables.

Note

I don’t cover claims or external logins in this book. If you want to learn more about those topics, they are covered in Adam Freeman’s Pro ASP.NET MVC 5 Platform book.

The current Identity code in the project is quite basic and works only for allowing individual user logins. During the rest of the chapter, we are going to expand this code and the database in order to:

  • Create the ability to manage different user roles and users.

  • Assign users to roles.

  • Create an initial Admin user when the project first runs.

  • Add some extra user fields.

Working with Roles

Before looking at users, we are going to work with roles so that when we do come to work with users, we can include code for assigning them to roles during registration, edit the roles they belong to, and display roles in the user details when the administrator views them.

Adding a Role Manager

The first thing required when working with roles is to create a role manager. To start, add the following class inside the BabyStore namespace at the end of the AppStart/IdentityConfig.cs file.

public class ApplicationRoleManager : RoleManager<IdentityRole>
{
    public ApplicationRoleManager(IRoleStore<IdentityRole, string> roleStore)
        : base(roleStore)
    {
    }


    public static ApplicationRoleManager Create(IdentityFactoryOptions<ApplicationRoleManager>  
         options, IOwinContext context)
    {
        return new ApplicationRoleManager(new
        RoleStore<IdentityRole>(context.Get<ApplicationDbContext>()));
    }
}

This code adds a new class named ApplicationRoleManager deriving from the strongly typed class RoleManager<T> for accessing and managing roles. In the code implemented above, RoleManager<IdentityRole> is used. IdentityRole is the class Entity Framework uses for modeling a role and is a simple class consisting of an ID, a name, and a list of users belonging to the role. The Create method returns a new ApplicationRoleManager instance using ApplicationDbContext.

Next, to create a role manager, add the following new line of code to the ConfigureAuth method after the last app.CreateOwinContext call in the App_StartStartUp.Auth.cs file:

app.CreatePerOwinContext<ApplicationRoleManager>(ApplicationRoleManager.Create);

This additional code will now create an ApplicationRoleManager instance whenever the application is started. The ConfigureAuth method is called from the project’s Startup.cs file and that runs every time the web site starts.

Creating an Admin User and Admin Role Whenever the Identity Database Is Created: Using a Database Initializer

Next we are going to create an Admin user and Admin role to act as an administrator for the web site. Start by adding the following code to the App_StartIdentityConfig.cs file:

// This example shows you how to create a new database if the Model changes
public class ApplicationDbInitializer : DropCreateDatabaseIfModelChanges<ApplicationDbContext>
{
    protected override void Seed(ApplicationDbContext context)
    {
        InitializeIdentityForEF(context);
        base.Seed(context);
    }


    //Create [email protected] with [email protected] in the Admin role        
    public static void InitializeIdentityForEF(ApplicationDbContext db)
    {
        var userManager =
        HttpContext.Current.GetOwinContext().GetUserManager<ApplicationUserManager>();
        var roleManager = HttpContext.Current.GetOwinContext().Get<ApplicationRoleManager>();
        const string name = "[email protected]";
        const string password = "[email protected]";
        const string roleName = "Admin";


        //Create Role Admin if it does not exist
        var role = roleManager.FindByName(roleName);
        if (role == null)
        {
            role = new IdentityRole(roleName);
            var roleresult = roleManager.Create(role);
        }


        var user = userManager.FindByName(name);
        if (user == null)
        {
            user = new ApplicationUser
            {
                UserName = name,
                Email = name
            };
            var result = userManager.Create(user, password);
            result = userManager.SetLockoutEnabled(user.Id, false);
        }


        // Add user admin to Role Admin if not already added
        var rolesForUser = userManager.GetRoles(user.Id);
        if (!rolesForUser.Contains(role.Name))
        {
            var result = userManager.AddToRole(user.Id, role.Name);
        }
    }
}
Note

For an alternative way of seeding the database using Code First Migrations, see the opening pages of Chapter 13.

This code adds a new class to initialize the Identity database with an Admin user with the username [email protected] and the password [email protected] and assigns them to a new role named Admin. This class demonstrates an alternative way to seed a database with test data as opposed to using migrations. The ApplicationDbInitializer class inherits from DropCreateDatabaseIfModelChanges<T>, meaning that it will take effect only if the database is new or if the model has changed. Alternatives include always dropping and recreating the database using DropCreateDatabaseAlways<T> or the default behavior, which is CreateDatabaseIfNotExists<T>.

The InitializeIdentityForEF method is called by the Seed method and works by first obtaining the application user manager and role manager. Some constants are used to set the admin username and password and the Admin role name. The code then uses the role manager to check if the Admin role exists. If it does not, it creates it by calling the Create method of the role manager. The user manager is then used to search for the Admin user. If it does not exist, it is created using the user manager’s Create method. Finally, the code checks to see if the Admin user belongs to any roles. If it does not, it is added to the Admin role.

Note

I don’t recommend hard-coding usernames and plain text passwords into code and have done this only for demonstration purposes. You should never do this in a real commercial project.

Next, we need a way to call the ApplicationDbInitializer class, so add a static constructor to the ApplicationDbContext class in the ModelsIdentityModel.cs file, as highlighted:

public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
{
    public ApplicationDbContext()
        : base("DefaultConnection", throwIfV1Schema: false)
    {
    }


    static ApplicationDbContext()
    {
        // Set the database initializer which is run once during application start
        // This seeds the database with admin user credentials and admin role
        Database.SetInitializer<ApplicationDbContext>(new ApplicationDbInitializer());
    }


    public static ApplicationDbContext Create()
    {
        return new ApplicationDbContext();
    }
}

Logging In as the Admin User

To be able to log in as the new Admin user, we need to force the database to recreate. To do this, change the DefaultConnection connectionString property in the Web.Config file as follows (make sure it’s all on one line in Web.Config):

<add name="DefaultConnection" connectionString="Data Source=(LocalDb)MSSQLLocalDB;AttachDbFilename=|DataDirectory|aspnet-BabyStore-Identity.mdf;Initial Catalog=aspnet-BabyStore-Identity;Integrated Security=True" providerName="System.Data.SqlClient" />

Now start the web site without debugging and attempt to log in as the user [email protected] using the password [email protected]. You should be logged into the web site as the Admin user, as shown in Figure 7-5.

A419071_1_En_7_Fig5_HTML.jpg
Figure 7-5. Logging into the web site as the new Admin user

The Admin user and role have now been created in the Identity database. To view the user data, expand the DefaultConnection connection in Server Explorer and view the data of the AspNetUsers table. You will see the new admin user data, as shown in Figure 7-6.

A419071_1_En_7_Fig6_HTML.jpg
Figure 7-6. The admin user data in the AspNetUsers table

Next, view the newly created Admin role by viewing the data of the AspNetRoles table, as shown in Figure 7-7. You will see the auto-generated ID field along with the role name of Admin.

A419071_1_En_7_Fig7_HTML.jpg
Figure 7-7. The newly created Admin role in the AspNetRoles database table

Finally, view the data of the AspNetUserRoles table. This table is used to map users to roles. You should see something similar to Figure 7-8, with a row containing the IDs of the new Admin user and Admin role.

A419071_1_En_7_Fig8_HTML.jpg
Figure 7-8. The AspNetUserRoles mapping table showing the mapping between the Admin role and Admin user

Adding a Roles View Model and RolesAdminController

This first thing we are going to do to allow users to view and manage roles is create a view model for a role. We’re going to deviate slightly from the way the Microsoft sample code does this and put this class in its own file. To create the view model, add a new file named AdminViewModel to the ViewModels folder and update the file to add a RoleViewModel class as follows:

using System.ComponentModel.DataAnnotations;

namespace BabyStore.ViewModels
{
    public class RoleViewModel
    {
        public string Id { get; set; }
        [Required(AllowEmptyStrings = false)]
        [Display(Name = "Role Name")]
        public string Name { get; set; }
    }
}

Next, add a controller by right-clicking the Controllers folder and choosing Add ➤ Controller from the menu. In the Add Scaffold window, choose the option MVC5 Controller with read/write actions, as shown in Figure 7-9. This controller will handle all the requests for managing roles.

A419071_1_En_7_Fig9_HTML.jpg
Figure 7-9. Creating an MVC5 Controller with read/write actions

Click the Add button and name the controller RolesAdminController. Click the Add button and the RolesAdminController code will appear with basic outline methods for Index, Details, Create, Edit, and Delete.

First of all, add this statement to the top of the ControllersRolesAdminController.cs file: using Microsoft.AspNet.Identity.Owin;.

Now add the following constructors and properties to the RolesAdminController class in order to obtain the RoleManager and UserManager instances for use in the controller:

public class RolesAdminController : Controller
{
    public RolesAdminController()
    {
    }


    public RolesAdminController(ApplicationUserManager userManager, ApplicationRoleManager
        roleManager)
    {
        UserManager = userManager;
        RoleManager = roleManager;
    }


    private ApplicationUserManager _userManager;
    public ApplicationUserManager UserManager
    {
        get
        {
            return _userManager ??
               HttpContext.GetOwinContext().GetUserManager<ApplicationUserManager>();
        }
        set
        {
            _userManager = value;
        }
    }


    private ApplicationRoleManager _roleManager;
    public ApplicationRoleManager RoleManager
    {
        get
        {
            return _roleManager ?? HttpContext.GetOwinContext().Get<ApplicationRoleManager>();
        }
        private set
        {
            _roleManager = value;
        }
    }


    // GET: RolesAdmin
    public ActionResult Index()
    {
        return View();
    }
...rest of code omitted for brevity...

Displaying All Roles

Now that RoleManager and RolesAdminController are available, we can use them to work with roles. The first change we are going to make to the RolesAdminController methods is to update the Index method to return a list of all the roles in the system. This is very straightforward; you simply update the Index method in the ControllersRolesAdminController.cs file, as shown here, to return all the roles:

public ActionResult Index()
{
    return View(RoleManager.Roles);
}

Next, create a view for display the roles by right-clicking on the Index method and create a view named Index with the template set to Empty (without model). Ensure that Use a Layout Page is checked.

Add the following code to the new empty view (ViewsRolesAdminIndex.cshtml) to create a list of roles based on the model IEnumerable<Microsoft.AspNet.Identity.EntityFramework.IdentityRole> with the standard Edit, Details, and Delete links to other views. (Note that to keep this as close as possible to the sample code, we have used the model IEnumerable<Microsoft.AspNet.Identity.EntityFramework.IdentityRole> rather than the RoleViewModel we created earlier).

@model IEnumerable<Microsoft.AspNet.Identity.EntityFramework.IdentityRole>

@{
    ViewBag.Title = "Roles";
}


<h2>@ViewBag.Title</h2>

<p>
    @Html.ActionLink("Create New", "Create")
</p>
<table class="table">
    <tr>
        <th>
            @Html.DisplayNameFor(model => model.Name)
        </th>
        <th>
        </th>
    </tr>


    @foreach (var item in Model)
    {
        <tr>
            <td>
                @Html.DisplayFor(modelItem => item.Name)
            </td>
            <td>
                @Html.ActionLink("Edit", "Edit", new { id = item.Id }) |
                @Html.ActionLink("Details", "Details", new { id = item.Id }) |
                @Html.ActionLink("Delete", "Delete", new { id = item.Id })
            </td>
        </tr>
    }
</table>

Close your existing browser and ensure that you are logged out of the site. Right-click in the view file and choose the View in Browser option. The new view will display the list of roles (just Admin at the moment), as shown in Figure 7-10.

A419071_1_En_7_Fig10_HTML.jpg
Figure 7-10. The RolesAdmin Index page viewed without logging in

Adding Authorization at a Controller Class Level

At the moment, anybody can anonymously view the roles available in the web site by navigating to the URL /RolesAdmin/Index. To change this so that only an Admin user can see this URL and anything else to do with administering roles, we are going to add some authorization to the RolesAdminController class using an attribute. To enable the authorization, add an Authorize attribute above the class declaration, as highlighted in the following code:

[Authorize(Roles = "Admin")]                
public class RolesAdminController : Controller
{
...

Now build the solution and try opening the Index view again. You will be asked to log in, as shown in Figure 7-11. Log in as the user [email protected] with the password [email protected] and you will be redirected to the list of roles. I cover more about redirection after login later in the chapter.

A419071_1_En_7_Fig11_HTML.jpg
Figure 7-11. Login prompt appearing when user now tries to access the RolesAdmin Index page

Displaying Role Details

Next, to display details for a role, including the users allocated to the role, add the following using statements to the top of the ControllersRolesAdminController.cs file:

using System.Net;
using System.Threading.Tasks;
using BabyStore.Models;

Next, update the Details method as follows:

public async Task<ActionResult> Details(string id)
{
    if (id == null)
    {
        return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
    }
    var role = await RoleManager.FindByIdAsync(id);
    // Get the list of Users in this Role
    var users = new List<ApplicationUser>();


    // Get the list of Users in this Role
    foreach (var user in UserManager.Users.ToList())
    {
        if (await UserManager.IsInRoleAsync(user.Id, role.Name))
        {
            users.Add(user);
        }
    }


    ViewBag.Users = users;
    ViewBag.UserCount = users.Count();
    return View(role);
}

The Details method now becomes an asynchronous method. I cover asynchronous database access later in the book, but what this means in simple terms is that the program can continue with other actions while an asynchronous task is running. In this case, the asynchronous calls are made to the RoleManager.FindByIdAsync and UserManager.IsInRoleAsync methods. These methods are the key to this Details method and are used to find a role by ID (ID is a string), and to determine if a user is in a role based on input parameters of a user’s ID and a role name.

The Details method has been updated to take a string named id as an input parameter and attempts to find a role based on this ID. The code then loops through all the users in the application and finds all the ones that are in the role. It then adds the users to the ViewBag, along with the number of users so that this information can be shown in the view. I have kept this code in line with the Microsoft sample code so that you can follow it easily; normally I recommend using a view model, as we have done in other chapters, rather than the ViewBag.

Now add a view by right-clicking on the Details method and choosing Add View from the menu. Add a view named Details with the template set to Empty (without model) and ensure that Use a Layout Page is checked.

Add the following code to the new ViewsRolesAdminDetails.cshtml file:

@model Microsoft.AspNet.Identity.EntityFramework.IdentityRole

@{
    ViewBag.Title = "Role Details";
}


<h2>@ViewBag.Title </h2>

<div>
    <hr />
    <dl class="dl-horizontal">
        <dt>
            @Html.DisplayNameFor(model => model.Name)
        </dt>
        <dd>
            @Html.DisplayFor(model => model.Name)
        </dd>
    </dl>
</div>
<h4>List of users in this role</h4>
@if (ViewBag.UserCount == 0)
{
    <hr />
    <p>No users found in this role.</p>
}


<table class="table">

    @foreach (var item in ViewBag.Users)
    {
        <tr>
            <td>
                @item.UserName
            </td>
        </tr>
    }
</table>
<p>
    @Html.ActionLink("Edit", "Edit", new { id = Model.Id }) |
    @Html.ActionLink("Back to List", "Index")
</p>

This view is relatively simple and displays the name of a role along with a list of users belonging to the role.

Start the project without debugging and navigate to the /RolesAdmin/Index URL. Click on the Details link for the Admin role. You will now see the details for the Admin role showing that the user [email protected] belongs to the role, as shown in Figure 7-12.

A419071_1_En_7_Fig12_HTML.jpg
Figure 7-12. The RolesAdmin Details page

Creating a Role

We’re going to start creating roles by adding a view for the GET version of the Create method in the RolesAdminController class. Right-click on this method in the RolesAdminController class and choose Add View from the menu. Then, in the Add View screen, set the View Name to Create. Set the template to Create and the model to RoleViewModel with Reference Script Libraries and Use a Layout Page both checked, as shown in Figure 7-13. Keep the Data Context Class blank.

A419071_1_En_7_Fig13_HTML.jpg
Figure 7-13. Adding the RolesAdmin Create view

When the new view is created, remove the line of code <h4>RoleViewModel</h4> and add a new h2 heading as highlighted in the following code:

@model BabyStore.ViewModels.RoleViewModel

@{
    ViewBag.Title = "Create Role";
}


<h2>@ViewBag.Title</h2>

@using (Html.BeginForm())
{
    @Html.AntiForgeryToken()


    <div class="form-horizontal">
        <hr />
        @Html.ValidationSummary(true, "", new { @class = "text-danger" })
...following code omitted for brevity

To update the HttpPost version of the Create method, first add the following using statements to the top of the ControllersRolesAdminContoroller.cs file:

using BabyStore.ViewModels;
using Microsoft.AspNet.Identity.EntityFramework;

Following this, update the HttpPost version of the Create method as follows:

[HttpPost]
public async Task<ActionResult> Create(RoleViewModel roleViewModel)
{
    if (ModelState.IsValid)
    {
        var role = new IdentityRole(roleViewModel.Name);
        var roleresult = await RoleManager.CreateAsync(role);
        if (!roleresult.Succeeded)
        {
            ModelState.AddModelError("", roleresult.Errors.First());
            return View();
        }
        return RedirectToAction("Index");
    }
    return View();
}

The key method in this code is the call to the RoleManager.CreateAsync method, which attempts to create a new role based on an IdentityRole object provided as an input parameter. Again, this is an asynchronous method and takes a RoleViewModel as an input parameter. The method checks if roleViewModel’s state is valid and if it is then it creates a new IdentityRole type variable and attempts to create this role using the CreateAysnc method of the RoleManager. If this is successful, then the Index action is called, otherwise, the Create view is returned to the user with an error message generated during the creation process.

Test the new Create methods and view by starting the site without debugging and navigating to the URL /RolesAdmin/Create. Create a new view named Users, as shown in Figure 7-14.

A419071_1_En_7_Fig14_HTML.jpg
Figure 7-14. Creating a new role named Users

Click the Create button and the new Users role will be created and displayed in the RolesAdmin Index page, as shown in Figure 7-15.

A419071_1_En_7_Fig15_HTML.jpg
Figure 7-15. The new Users role displayed in the RolesAdmin Index page

Fixing the Navigation Bar Style Issues

At the moment, the navigation bar has two issues. The first issue is that the text is split over two lines but more annoyingly this is now overlapping the rest of the content below it, as in Figure 7-15, where the heading “Roles” is not displayed. First, we’ll fix the overlapping issue, since sometimes the menu may appear over two lines.

To start, in the ViewsShared\_Layout.cshtml file, modify the div with the navbar class assigned to it as follows. Change the css class navbar-fixed-top to navbar-static-top:

<body>
    <div class="navbar navbar-inverse navbar-static-top">
        <div class="container">

This will make the navbar disappear off the screen when scrolling and pushes the rest of the content down below it so that the “Roles” heading text is now visible on the RolesAdmin Index page, as in Figure 7-16.

A419071_1_En_7_Fig16_HTML.jpg
Figure 7-16. Changing the navigation bar to use the navbar-static-top css class now displays the Roles heading

Having set the navigation bar to be fixed, there is now another issue that a blank whitespace has appeared above it. To fix this, update the Contentsite.css file to remove the padding-top: 50px; entry from the css body class so that it now reads as follows:

body {
    padding-bottom: 20px;
}

Finally, to make the nav-bar wide enough to accept all the text and not wrap over two lines, modify the Contentootstrap.css file to change the max-width property value of the container CSS class to 1300px inside the media-query for anything over 1200 pixels. This is a bit of a mouthful and I explain what CSS media queries are in part II of the book, but what this change means is that whenever the screen size is at least 1200 pixels, the container class will be set to a maximum of 1300 pixels wide if available. The container class is the CSS class that contains the contents of the navigation bar and hence restricts its width. Update the Contentootstrap.css file as highlighted:

@media (min-width: 1200px) {
  .container {
    max-width: 1300px;
  }

With these two changes in place, the navigation bar now appears as in Figure 7-17, with no whitespace and on one line.

A419071_1_En_7_Fig17_HTML.jpg
Figure 7-17. The navigation bar without the whitespace above it and text on one line only

Editing a Role

Next we’ll cover the editing of a role; in this trivial example, it will simply mean editing the role name. First of all, modify the GET version of the Edit method in the ControllersRolesAdminController.cs file as follows:

// GET: RolesAdmin/Edit/5
public async Task<ActionResult> Edit(string id)
{
    if (id == null)
    {
        return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
    }
    var role = await RoleManager.FindByIdAsync(id);
    if (role == null)
    {
        return HttpNotFound();
    }
    RoleViewModel roleModel = new RoleViewModel { Id = role.Id, Name = role.Name };
    return View(roleModel);
}

The input parameter type has been changed from int to string so that it can be used in the RoleManager.FindByIdAsync method. IDs used in Identity are long strings rather than integers. The program then searches for the role. If it’s found, its data is assigned to a new RoleViewModel object and passed to the view.

Next, add a new Edit view by right-clicking on the Edit method and choosing Add View from the menu. Set the view name to Edit, the template to Edit, the model class to RoleViewModel (BabyStore.ViewModels), and the Data Context Class to blank. Ensure that Reference Script Libraries and Use a Layout Page are checked. Figure 7-18 shows the correct options to set.

A419071_1_En_7_Fig18_HTML.jpg
Figure 7-18. Creating the RolesAdmin Edit view

Modify the generated view file to set the ViewBag.Title to Edit Role and use it inside the H2 heading tags. Also remove the <h4> heading as highlighted:

@model BabyStore.ViewModels.RoleViewModel

@{
    ViewBag.Title = "Edit Role";
}


<h2>@ViewBag.Title</h2>

@using (Html.BeginForm())
{
    @Html.AntiForgeryToken()


    <div class="form-horizontal">
        <hr />
        @Html.ValidationSummary(true, "", new { @class = "text-danger" })
        @Html.HiddenFor(model => model.Id)
...rest of code omitted for brevity

Next update the HttpPost version of the Edit method in the ControllersRolesAdminController.cs file as follows:

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Edit(RoleViewModel roleModel)
{
    if (ModelState.IsValid)
    {
        var role = await RoleManager.FindByIdAsync(roleModel.Id);
        role.Name = roleModel.Name;
        await RoleManager.UpdateAsync(role);
        return RedirectToAction("Index");
    }
    return View();
}

The key method in this code is the RoleManager.UpdateAsync method, which is used to update a role with any new property values assigned to it. This method takes a RoleViewModel as an input parameter and attempts to update the name of the role using the RoleManager.UpdateAsync method. It returns the user to the index view if everything is successful. Figures 7-19 and 7-20 show the Users group being renamed User.

A419071_1_En_7_Fig19_HTML.jpg
Figure 7-19. The RolesAdmin Edit view
A419071_1_En_7_Fig20_HTML.jpg
Figure 7-20. The Users role is renamed User

Deleting a Role

To enable the deleting of roles, first modify the GET version of the Delete method in the ControllersRolesAdminController.cs as file follows:

// GET: RolesAdmin/Delete/5
public async Task<ActionResult> Delete(string id)
{
    if (id == null)
    {
        return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
    }
    var role = await RoleManager.FindByIdAsync(id);
    if (role == null)
    {
        return HttpNotFound();
    }
    return View(role);
}

This code is very similar to the previous code for the GET version of the Edit method, but does not populate a view model to return to the view. Instead of using RoleViewModel, the view will be based on the IdentityRole class.

Add a Delete view by right-clicking on the Delete method and choosing Add View from the menu. Set the view name to Delete and the template to Empty (without model), with Use a Layout Page checked. In the generated view file, update the contents as follows:

@model Microsoft.AspNet.Identity.EntityFramework.IdentityRole

@{
    ViewBag.Title = "Delete Role";
}


<h2>Delete Role</h2>

<h3>Are you sure you want to delete this Role? </h3>
<p>Deleting this Role will remove all users from this role. It will not delete the users.</p>
<div>
    <hr />
    <dl class="dl-horizontal">
        <dt>
            @Html.DisplayNameFor(model => model.Name)
        </dt>


        <dd>
            @Html.DisplayFor(model => model.Name)
        </dd>
    </dl>
    @using (Html.BeginForm())
    {
        @Html.AntiForgeryToken()


        <div class="form-actions no-color">
            <input type="submit" value="Delete" class="btn btn-default" /> |
            @Html.ActionLink("Back to List", "Index")
        </div>
    }
</div>

The view has been modified to display the details of the role passed to the view and is based on the IdentityRole class. Figure 7-21 shows the new delete view for a new role I created named Test Delete:

A419071_1_En_7_Fig21_HTML.jpg
Figure 7-21. The RolesAdmin Delete view

To give this view a method to target, update the HttpPost version of the Delete method. First add the following using statement to the top of the ControllersRolesAdminController.cs file:

using Microsoft.AspNet.Identity;

Next update the HttpPost version of the Delete method renaming it to DeleteConfirmed and then update it as follows so that it finds a role based on an id and then deletes the role:

// POST: RolesAdmin/Delete/5
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public async Task<ActionResult> DeleteConfirmed(string id)
{
    if (ModelState.IsValid)
    {
        if (id == null)
        {
            return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
        }
        var role = await RoleManager.FindByIdAsync(id);
        if (role == null)
        {
            return HttpNotFound();
        }
        IdentityResult result = await RoleManager.DeleteAsync(role);
        if (!result.Succeeded)
        {
            ModelState.AddModelError("", result.Errors.First());
            return View();
        }
        return RedirectToAction("Index");
    }
    return View();
}

The key method call in the DeleteConfirmed method is the call to the RoleManager.DeleteAsync method, which takes an IdentityRole object as an input parameter and then attempts to delete the role.

The method has been renamed DeleteConfirmed because if the name was kept as Delete, it would have the same signature as the GET version of the Delete method (public async Task<ActionResult> Delete(string id)) and the compiler would not be able to differentiate between them, resulting in a compilation error. The method has been prefixed with [HttpPost, ActionName("Delete")] so that when a HttpPost request is made to target the Delete action, it actually targets this DeleteConfirmed method.

The method attempts to find a role based on the ID input parameter. Once it’s found it attempts to delete it and return the user to the index view. It adds an error to the ModelState and returns the user to the Delete view if it does not succeed.

Adding a Basic Admin Controller and View

At the moment, the web site has no links to the new RolesAdminController or associated views, and it already has a link to allow users to manage images. We are going to create a very simple admin page with links to all the index pages of the different controllers for managing images, roles, and users.

First create an AdminController class by right-clicking on the Controllers folder and choosing Add ➤ Controller from the menu. Then choose to add an MVC5 Controller Empty and name the new controller AdminController.

In the new AdminController.cs file, right-click the Index method and choose Add View from the menu. Set the view name to Index and the template to Empty (without model). Ensure Use a Layout Page is checked.

Update the contents of the new ViewsAdminIndex.cshtml file as follows to add links to the Index pages for ProductImages, RoleAdmin, and UserAdmin:

@{
    ViewBag.Title = "Admin";
}


<h2>@ViewBag.Title</h2>
<div class="container">
    <div class="row">
        @Html.ActionLink("Manage Images", "Index", "ProductImages")
    </div>
    <div class="row">
        @Html.ActionLink("Manage Roles", "Index", "RolesAdmin")
    </div>
    <div class="row">
        @Html.ActionLink("Manage Users", "Index", "UsersAdmin")
    </div>
</div>

Next, replace the Manage Images link in the ViewsShared\_Layout.cshtml file with a link to the new Admin Index page by replacing <li>@Html.ActionLink("Manage Images", "Index", "ProductImages")</li> with <li>@Html.ActionLink("Admin", "Index", "Admin")</li>.

Start the web site without debugging. In the home page, click the new Admin link in the navigation bar. The new Admin Index page should appear, as shown in Figure 7-22.

A419071_1_En_7_Fig22_HTML.jpg
Figure 7-22. The new Admin Index page with links to Manage Images, Roles, and Users

Adding Authorization to the Admin Controller and Admin Link

We have already covered adding authorization to a controller earlier in the chapter and will use this again in the AdminController. However, first we’re going to display the admin link only to the users in the Admin role. To make this change, update the ViewsShared\_Layout.cshml file as follows to add a check to see if the user is authenticated and in the admin group before displaying the Admin link:

@if (Request.IsAuthenticated && User.IsInRole("Admin"))            
{
    <li>@Html.ActionLink("Admin", "Index", "Admin")</li>
}
Note

If you need to perform more complex logic in a commercial project to validate a user, I recommend that you perform the logic in your controller classes and then set a Boolean value in your view model where possible. Because we are using pages generated by scaffolding, we’ve added the logic to check the user’s role to the view file directly.

Now if you click on the Logout link in the web site, the Admin link will no longer appear, as shown in Figure 7-23.

A419071_1_En_7_Fig23_HTML.jpg
Figure 7-23. The Admin link no longer appears when the user is not logged in

If you log in as the Admin user [email protected] with the [email protected] password, the Admin link should now appear as shown in Figure 7-24.

A419071_1_En_7_Fig24_HTML.jpg
Figure 7-24. The Admin link appears in the navigation bar when logged in as the [email protected] user

Users can still access the Admin Index page anonymously by simply entering the /Admin URL, so prevent this by modifying the AdminController.cs file as follows. This will add authorization to the AdminController class so it is only available to users in the Admin role:

[Authorize(Roles = "Admin")]                
public class AdminController : Controller
{

When the users try to access the URL /Admin anonymously, they will now be prompted to log in and must do so as a member of the Admin group in order to gain access.

Working with Users

This section covers working with users, including how to add new fields to the existing user class and how to work with two database contexts in one project. It also covers how to manage users as an administrator, user self-registration, viewing and editing personal details, and how users can reset their passwords using ASP.NET Identity. Just as everything to do with working with roles was based on the RoleManager class, working with users is based on the UserManager class.

When we covered working with roles earlier, it was necessary to create a new class named ApplicationRoleManager, which inherited from the RoleManager class. This time, when working with users, we do not need to create an ApplicationUserManager class, because when the project was created, this class was automatically created by setting Authentication to Individual User Accounts. The ApplicationUserManager class can be found in the App_StartIdentityConfig.cs file.

Adding Extra Properties for a User

If you expand the AspNetUsers database table, you will see the properties (displayed as database columns) currently stored for a user. These columns are generated from the IdentityUser class using Entity Framework. This class is provided by default when using Identity and is part of the Microsoft.AspNet.Identity.EntityFramework namespace. I’ve listed the contents of this class here so you can see the default properties it specifies for the users:

namespace Microsoft.AspNet.Identity.EntityFramework
{


    public class IdentityUser<TKey, TLogin, TRole, TClaim> : IUser<TKey>
        where TLogin : IdentityUserLogin<TKey>
        where TRole : IdentityUserRole<TKey>
        where TClaim : IdentityUserClaim<TKey>
    {


        public IdentityUser();
        public virtual int AccessFailedCount { get; set; }
        public virtual ICollection<TClaim> Claims { get; }
        public virtual string Email { get; set; }
        public virtual bool EmailConfirmed { get; set; }
        public virtual TKey Id { get; set; }
        public virtual bool LockoutEnabled { get; set; }
        public virtual DateTime? LockoutEndDateUtc { get; set; }
        public virtual ICollection<TLogin> Logins { get; }
        public virtual string PasswordHash { get; set; }
        public virtual string PhoneNumber { get; set; }
        public virtual bool PhoneNumberConfirmed { get; set; }
        public virtual ICollection<TRole> Roles { get; }
        public virtual string SecurityStamp { get; set; }
        public virtual bool TwoFactorEnabled { get; set; }
        public virtual string UserName { get; set; }
    }
}

We are not going to examine each property in this class in detail. The main reason for listing them is so that you can see what properties are currently defined for a user and crucially what is not defined. This class defines that a user will have an e-mail address, a phone number, and a username, but it does not have useful properties that we require for a shopping web site such as a user’s actual name, their date of birth, or address.

To add some new user fields, create a new class in the Models folder named Address and edit the contents as follows:

using System.ComponentModel.DataAnnotations;

namespace BabyStore.Models
{
    public class Address
    {
        [Required]
        [Display(Name = "Address Line 1")]
        public string AddressLine1 { get; set; }
        [Display(Name = "Address Line 2")]
        public string AddressLine2 { get; set; }
        [Required]
        public string Town { get; set; }
        [Required]
        public string County { get; set; }
        [Required]
        public string Postcode { get; set; }
    }
}

We are going to use an address more than once in this project for both users and orders, so it is defined as a distinct class. We’ve defined all the fields as required apart from the AddressLine2. This class will also be used to demonstrate how Entity Framework works with composition, whereby an object is composed of another object.

To add extra fields to a user, you need to update the ApplicationUser class. In order to add a first name, last name, date of birth, and address for a user, open the ModelsIdentityModel.cs file and update the ApplicationUser class as highlighted:

public class ApplicationUser : IdentityUser
{    
    [Required]
    [Display(Name = "First Name")]
    [StringLength(50)]
    public string FirstName { get; set; }
    [Required]
    [Display(Name = "Last Name")]
    [StringLength(50)]
    public string LastName { get; set; }


    [Required]
    [DataType(DataType.Date)]
    [Display(Name = "Date of birth")]
    [DisplayFormat(DataFormatString = "{0:d}")]
    public DateTime DateOfBirth { get; set; }


    public Address Address { get; set; }

    public async Task<ClaimsIdentity> GenerateUserIdentityAsync(UserManager<ApplicationUser>
    manager)
    {
        // Note the authenticationType must match the one defined in
               CookieAuthenticationOptions.AuthenticationType
        var userIdentity = await manager.CreateIdentityAsync(this,
               DefaultAuthenticationTypes.ApplicationCookie);
        // Add custom user claims here
        return userIdentity;
    }


}

Ensure you add using System; to the top of the file so that DateTime can be found and add using System.ComponentModel.DataAnnotations;. All the properties are required. For the DateOfBirth property, the DataType is set to date, so the time information stored in the field is not displayed. The DisplayFormat attribute is set to use the local server format (in this case, it’s the UK format mm/dd/yyyy).

Now that we have updated the ApplicationUser class, we need to apply the model changes to the Identity database. We’ll use Code First as we have done throughout the book, but first we have to reconfigure the project so that it can work with migrations for multiple database contexts.

Working with Two Database Contexts: Updating the Identity Database for the New User Properties

In order to enable migrations for the Identity database, we first need to update the existing migration files in the Migrations folder so that we can enable migrations for the ApplicationDbContext. Update the existing files in the Migrations folder as follows:

  • Change the name of the MigrationsConfiguration.cs file to MigrationsStoreConfiguration.cs

  • Update the class name and constructor in MigrationsStoreConfiguration.cs to StoreConfiguration as highlighted here (You can do this automatically by choosing yes when asked if you want to update all references when changing the class name):

    internal sealed class StoreConfiguration : DbMigrationsConfiguration<BabyStore.DAL.StoreContext>
    {
        public StoreConfiguration()
        {
            AutomaticMigrationsEnabled = false;
        }
  • Change the namespace in MigrationsStoreConfiguration.cs to BabyStore.Migrations.StoreConfiguration

  • Update the StoreContext connectionString property in the main Web.Config file to change the database name in order to create a new database to verify that migrations still work okay:

    <add name="StoreContext" connectionString="Data Source=(LocalDB)MSSQLLocalDB;AttachDbFilename=|DataDirectory|BabyStore4.mdf;Initial Catalog=BabyStore4;Integrated Security=True" providerName="System.Data.SqlClient" />
  • Create a new migration by running the following command in Package Manager Console. This will effectively reset the migrations and merge all the previous migrations into one:

    add-migration twocontextreset -configuration storeconfiguration
  • You should now see a new migration file with the following code:

    namespace BabyStore.Migrations.StoreConfiguration
    {
        using System;
        using System.Data.Entity.Migrations;


        public partial class twocontextreset : DbMigration
        {
            public override void Up()
            {
                CreateTable(
                    "dbo.Categories",
                    c => new
                        {
                            ID = c.Int(nullable: false, identity: true),
                            Name = c.String(nullable: false, maxLength: 50),
                        })
                    .PrimaryKey(t => t.ID);


                CreateTable(
                    "dbo.Products",
                    c => new
                        {
                            ID = c.Int(nullable: false, identity: true),
                            Name = c.String(nullable: false, maxLength: 50),
                            Description = c.String(nullable: false, maxLength: 200),
                            Price = c.Decimal(nullable: false, precision: 18, scale: 2),
                            CategoryID = c.Int(),
                        })
                    .PrimaryKey(t => t.ID)
                    .ForeignKey("dbo.Categories", t => t.CategoryID)
                    .Index(t => t.CategoryID);


                CreateTable(
                    "dbo.ProductImageMappings",
                    c => new
                        {
                            ID = c.Int(nullable: false, identity: true),
                            ImageNumber = c.Int(nullable: false),
                            ProductID = c.Int(nullable: false),
                            ProductImageID = c.Int(nullable: false),
                        })
                    .PrimaryKey(t => t.ID)
                    .ForeignKey("dbo.Products", t => t.ProductID, cascadeDelete: true)
                    .ForeignKey("dbo.ProductImages", t => t.ProductImageID, cascadeDelete: true)
                    .Index(t => t.ProductID)
                    .Index(t => t.ProductImageID);


                CreateTable(
                    "dbo.ProductImages",
                    c => new
                        {
                            ID = c.Int(nullable: false, identity: true),
                            FileName = c.String(maxLength: 100),
                        })
                    .PrimaryKey(t => t.ID)
                    .Index(t => t.FileName, unique: true);


            }

            public override void Down()
            {
                DropForeignKey("dbo.ProductImageMappings", "ProductImageID", "dbo.ProductImages");
                DropForeignKey("dbo.ProductImageMappings", "ProductID", "dbo.Products");
                DropForeignKey("dbo.Products", "CategoryID", "dbo.Categories");
                DropIndex("dbo.ProductImages", new[] { "FileName" });
                DropIndex("dbo.ProductImageMappings", new[] { "ProductImageID" });
                DropIndex("dbo.ProductImageMappings", new[] { "ProductID" });
                DropIndex("dbo.Products", new[] { "CategoryID" });
                DropTable("dbo.ProductImages");
                DropTable("dbo.ProductImageMappings");
                DropTable("dbo.Products");
                DropTable("dbo.Categories");
            }
        }
    }
    • Run the following command in Package Manager Console in order to create the BabyStore4 database:

      update-database -configuration storeconfiguration
  • Now delete the old migration files (apart from the _twocontextreset file) from the Migrations directory.

The BabyStore4 database will now be set up and ready to use with migrations in the BabyStore.Migrations.StoreConfiguration.StoreConfiguration namespace.

Note

There are several discussions on the web site such as Stack Overflow about how to work with two database contexts using Entity Framework. If you are introducing a new context and decide to change the namespace of your existing context as we have done, it is essential that you create a new migration as we did when creating twocontextreset . If you do not do this, everything will appear to be fine. However, as soon as you try to recreate your database, your old migrations will no longer be recognized, even if you place your existing migrations into the new namespace, and update the migrationhistory table in the database.

To enable migrations for the Identity database, open Package Manager Console and run the command:

enable-migrations –ContextTypeName BabyStore.Models.ApplicationDbContext

Next, add a new migration by running the following command in Package Manager Console:

add-migration update_user_fields -ConfigurationTypeName Configuration

Finally, update the database for the new ApplicationUser fields by running this command in Package Manager Console:

update-database -ConfigurationTypeName Configuration

The new fields for First Name, Last Name, and the Address columns should now appear in the DefaultConnection in the Server Explorer window. View the data of the AspNetUsers table. Figure 7-25 shows how the new columns are appended to the right side of the table.

A419071_1_En_7_Fig25_HTML.jpg
Figure 7-25. The new FirstName, LastName, and Address columns in the AspNetUsers database table
Note

When creating database fields for a class property composed of another class, Entity Framework creates the column prefixed with the property name. In this case, it has created all the Address fields prefixed with Address.

Updating the Admin User Creation Code for the New User Fields

Now that we added some extra required fields to the ApplicationUser class, we are going to update the code that creates the [email protected] user. Open the App_StartIdentityConfig.cs file and update the code inside the InitializeIdentityForEF method that creates a new ApplicationUser as highlighted. This code adds the FirstName, LastName, DateOfBirth, and Address values.

//Create [email protected] with [email protected] in the Admin role        
public static void InitializeIdentityForEF(ApplicationDbContext db)
{
    var userManager =
        HttpContext.Current.GetOwinContext().GetUserManager<ApplicationUserManager>();
    var roleManager = HttpContext.Current.GetOwinContext().Get<ApplicationRoleManager>();
    const string name = "[email protected]";
    const string password = "[email protected]";
    const string roleName = "Admin";


    //Create Role Admin if it does not exist
    var role = roleManager.FindByName(roleName);
    if (role == null)
    {
        role = new IdentityRole(roleName);
        var roleresult = roleManager.Create(role);
    }


    var user = userManager.FindByName(name);
    if (user == null)
    {
        user = new ApplicationUser
        {
            UserName = name,
            Email = name,
            FirstName = "Admin",
            LastName = "Admin",
            DateOfBirth = new DateTime(2015, 1, 1),
            Address = new Address
            {
                AddressLine1 = "1 Admin Street",
                Town = "Town",
                County = "County",
                Postcode = "PostCode"
            }
        };
        var result = userManager.Create(user, password);
        result = userManager.SetLockoutEnabled(user.Id, false);
    }


    // Add user admin to Role Admin if not already added
    var rolesForUser = userManager.GetRoles(user.Id);
    if (!rolesForUser.Contains(role.Name))
    {
        var result = userManager.AddToRole(user.Id, role.Name);
    }
}

Remember that it’s important to not hardcode the creation of Admin users inside code in a commercial project. This is done in this project for demonstration purposes only.

Creating a Users Role on Database Creation

Now that we’ve updated the code to add new data to the Admin user, we are going to recreate the database in order to recreate the Admin user with the new data. Before we do this, however, we are also going to create a new role named Users when the Identity database is created. Modify the InitializeIdentityForEF method in the App_StartIdentityConfig.cs file to create a Users role by adding the following highlighted code at the end of the method:

//Create [email protected] with [email protected] in the Admin role        
public static void InitializeIdentityForEF(ApplicationDbContext db)
{
    var userManager =
        HttpContext.Current.GetOwinContext().GetUserManager<ApplicationUserManager>();
    var roleManager = HttpContext.Current.GetOwinContext().Get<ApplicationRoleManager>();
    const string name = "[email protected]";
    const string password = "[email protected]";
    const string roleName = "Admin";


    //Create Role Admin if it does not exist
    var role = roleManager.FindByName(roleName);
    if (role == null)
    {
        role = new IdentityRole(roleName);
        var roleresult = roleManager.Create(role);
    }


    var user = userManager.FindByName(name);
    if (user == null)
    {
        user = new ApplicationUser
        {
            UserName = name,
            Email = name,
            FirstName = "Admin",
            LastName = "Admin",
            DateOfBirth = new DateTime(2015, 1, 1),
            Address = new Address
            {
                AddressLine1 = "1 Admin Street",
                Town = "Town",
                County = "County",
                Postcode = "PostCode"
            }
        };
        var result = userManager.Create(user, password);
        result = userManager.SetLockoutEnabled(user.Id, false);
    }


    // Add user admin to Role Admin if not already added
    var rolesForUser = userManager.GetRoles(user.Id);
    if (!rolesForUser.Contains(role.Name))
    {
        var result = userManager.AddToRole(user.Id, role.Name);
    }


    //Create users role
    const string userRoleName = "Users";
    role = roleManager.FindByName(userRoleName);
    if (role == null)
    {
        role = new IdentityRole(userRoleName);
        var roleresult = roleManager.Create(role);
    }
}

Next update the connection string for the DefaultConnection entry in the project’s main Web.Config file. Update the database name to aspnet-BabyStore-Identity2 as highlighted:

<add name="DefaultConnection" connectionString="Data Source=(LocalDb)MSSQLLocalDB;AttachDbFilename=|DataDirectory|aspnet-BabyStore-Identity2.mdf;Initial Catalog=aspnet-BabyStore-Identity2;Integrated Security=True" providerName="System.Data.SqlClient" />

This will create a new database for storing identity information the next time the project is built and any identity code is called.

Next, start the web site without debugging and log in as the Admin user, which is [email protected] with the [email protected] password. The Identity database will be recreated and in the AspNetUsers table, the [email protected] user will now have data in the FirstName, LastName, DateOfBirth, Address_AddressLine1, Address_Town, Address_County, and Address_Postcode fields, as shown in Figure 7-26.

A419071_1_En_7_Fig26_HTML.jpg
Figure 7-26. The Admin user now contains the extra data

In addition to the new user data, the AspNetRoles table will now also contain a Users role, as shown in Figure 7-27.

A419071_1_En_7_Fig27_HTML.jpg
Figure 7-27. The AspNetRoles table now contains an entry for a Users role

Adding a UsersAdminController

Now we’re going to create a UsersAdminController. This controller will handle all the requests from an administrator for managing and viewing user data, in the same way that a RolesAdminController was used to handle admin requests for viewing and managing roles.

Add a controller by right-clicking the Controllers folder and choosing Add ➤ Controller from the menu. In the Add Scaffold window, choose the option MVC5 Controller with read/write actions, as shown in Figure 7-9.

Click the Add button and name the controller UsersAdminController, then click the Add button and the UsersAdminController code will appear with basic outline methods for Index, Details, Create, Edit, and Delete.

Next add this statement to the top of the ControllersUsersAdminController.cs file: using Microsoft.AspNet.Identity.Owin;.

Then add the following bold line of code above the declaration of the UsersAdminController class so that it is limited only to users in the Admin role:

[Authorize(Roles = "Admin")]                
public class UsersAdminController : Controller

Now add the following constructors and properties to the UsersAdminController class in order to obtain the RoleManager and UserManager instances for use in the controller:

public class UsersAdminController : Controller
{
    public UsersAdminController()
    {
    }


    public UsersAdminController(ApplicationUserManager userManager, ApplicationRoleManager
        roleManager)
    {
        UserManager = userManager;
        RoleManager = roleManager;
    }


    private ApplicationUserManager _userManager;
    public ApplicationUserManager UserManager
    {
        get
        {
            return _userManager ??
               HttpContext.GetOwinContext().GetUserManager<ApplicationUserManager>();
        }
        private set
        {
            _userManager = value;
        }
    }


    private ApplicationRoleManager _roleManager;
    public ApplicationRoleManager RoleManager
    {
        get
        {
            return _roleManager ?? HttpContext.GetOwinContext().Get<ApplicationRoleManager>();
        }
        private set
        {
            _roleManager = value;
        }
    }


    // GET: UsersAdmin
    public ActionResult Index()
    {
        return View();
    }
...following code omitted for brevity...

Displaying All Users

To display all users, first update the Index method in the UsersAdminController class to return a list of all the users in the system, as shown in the following code.

public async Task<ActionResult> Index()
{
    return View(await UserManager.Users.ToListAsync());
}

Add the following using statements to the top of the ControllersUsersAdminController.cs file to ensure that the method compiles:

using System.Data.Entity;
using System.Threading.Tasks;

The Index method has been updated to be asynchronous, and it obtains the list of users from the UserManager asynchronously. I cover asynchronous database access later in the book.

Next, create a view for displaying the users by right-clicking on the Index method and choosing Add View. Create a view named Index with the template set to Empty (without model). Ensure Use a Layout Page is checked.

Add the following code to the new empty view to create a list of roles based on the model IEnumerable<BabyStore.Models.ApplicationUser>, with the standard Edit, Details, and Delete links to other views:

@model IEnumerable<BabyStore.Models.ApplicationUser>

@{
    ViewBag.Title = "Users";
}


<h2>@ViewBag.Title</h2>

<p>
    @Html.ActionLink("Create New", "Create")
</p>
<table class="table">
    <tr>
        <th>
            @Html.DisplayNameFor(model => model.UserName)
        </th>
        <th>
        </th>
    </tr>
    @foreach (var item in Model)
    {
        <tr>
            <td>
                @Html.DisplayFor(modelItem => item.UserName)
            </td>
            <td>
                @Html.ActionLink("Edit", "Edit", new { id = item.Id }) |
                @Html.ActionLink("Details", "Details", new { id = item.Id }) |
                @Html.ActionLink("Delete", "Delete", new { id = item.Id })
            </td>
        </tr>
    }
</table>

Right-click on Index.cshtml file and choose View in browser. You will be prompted to log in if you are not already logged in as the Admin user. Log in as the Admin user and you will see the index.cshtml page displaying a list of current web site users, as shown in Figure 7-28. At the moment, only the user [email protected] exists, so only that is displayed.

A419071_1_En_7_Fig28_HTML.jpg
Figure 7-28. The UserAdmin Index page displaying the current users in the site

Displaying User Details

To display details for a user, including the roles a user is allocated to, modify the Details method in the ControllersUsersAdminController.cs file as shown in bold:

public async Task<ActionResult> Details(string id)
{
    if (id == null)
    {
        return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
    }
    var user = await UserManager.FindByIdAsync(id);


    ViewBag.RoleNames = await UserManager.GetRolesAsync(user.Id);

    return View(user);
}

Add the statement using System.Net; to the top of the file so that the code compiles. The key methods in this code are UserManager.FindByIdAsync(id), which is used to find a user by ID, and UserManager.GetRolesAsync(user.Id), which is used to find the roles that a user is assigned to. The method attempts to find a user by ID and then finds the roles the user is assigned to. The roles are assigned to the ViewBag and the user is then passed to the view to display. We haven’t changed this code to use a view model in order to keep it as close as possible to the sample Microsoft code and make it is easier to follow.

Next we need to create the Details view. This view is going to be based on the ApplicationUser class, but creating it is not as straightforward as using the scaffolding process to create a Details view based on the ApplicationUser model. If you try this, the scaffolding process will throw an error with the message "The method or operation is not implemented". Therefore, to create this view, right-click on the method and choose Add View, then create a view named Details with the template set to Empty (without model). Ensure Use a Layout Page is checked.

Modify the new ViewsUsersAdminDetails.cshtml file so that it appears as follows. This code will display the user’s personal data, including name, address, e-mail, and date of birth. It also loops through the roles assigned to the ViewBag and lists them. We are not going to attempt to display all the address fields individually; instead, we’re going to rely on the HTML helper Html.DisplayFor to display all the fields in an Address object by simply calling @Html.DisplayFor(model => model.Address).

@model BabyStore.Models.ApplicationUser

@{
    ViewBag.Title = "User Details";
}


<h2>@ViewBag.Title</h2>

<div>
    <hr />
    <dl class="dl-horizontal">
        <dt>
            @Html.DisplayNameFor(model => model.FirstName)
        </dt>
        <dd>
            @Html.DisplayFor(model => model.FirstName)
        </dd>
        <dt>
            @Html.DisplayNameFor(model => model.LastName)
        </dt>
        <dd>
            @Html.DisplayFor(model => model.LastName)
        </dd>
        <dt>
            @Html.DisplayNameFor(model => model.Email)
        </dt>


        <dd>
            @Html.DisplayFor(model => model.Email)
        </dd>


        <dt>
            @Html.DisplayNameFor(model => model.DateOfBirth)
        </dt>


        <dd>
            @Html.DisplayFor(model => model.DateOfBirth)
        </dd>
        @Html.DisplayFor(model => model.Address)
    </dl>
</div>
<div class="container">
    <div class="row">
        <h4>Roles this user belongs to:</h4>
    </div>
    @if (ViewBag.RoleNames.Count == 0)
    {
        <hr />
        <p>No roles found for this user</p>
    }


    @foreach (var item in ViewBag.RoleNames)
    {
        <div class="row">
            @item
        </div>
    }
</div>
<p>
    @Html.ActionLink("Edit", "Edit", new { id = Model.Id }) |
    @Html.ActionLink("Back to List", "Index")
</p>

Once this code is completed, start the web site and navigate to the UsersAdmin Index page (/UsersAdmin). Click on the Details link of the [email protected] user. The user’s details should appear as shown in Figure 7-29. It shows the name, e-mail, date of birth, address, and role details.

A419071_1_En_7_Fig29_HTML.jpg
Figure 7-29. The details of the [email protected] user viewed via the UsersAdmin Details page

As you can see, using @Html.DisplayFor(model => model.Address) has shortened the code but displays the data from the user’s address differently from the other fields. This is because the HTML helper doesn’t have any instruction on how to display the Address model, so uses some default HTML. The effect of this is to attempt to display the address fields inside div elements, whereas the rest of the fields are formatted inside dl, dt, and dd tags as part of a definition list element. The following code snippet shows the generated HTML source for the first few address fields and labels compared with the date of birth field:

        <dt>
            Date of birth
        </dt>


        <dd>
            2015-01-01
        </dd>
<div class="display-label">Address Line 1</div>
<div class="display-field">1 Some Street</div>
<div class="display-label">Address Line 2</div>
<div class="display-field"></div>
<div class="display-label">Town</div>
<div class="display-field">Town</div>

You can see that date of birth is contained within different HTML tags to the address fields. I’m going to demonstrate how to fix this formatting issue using a feature of ASP.NET MVC known as display templates.

Using a Display Template

Display templates are a useful feature of ASP.NET MVC that allow you to specify a template to determine how a particular model will appear when used in the DisplayFor() helper method. We are going to introduce a display template to help display an address object correctly in the UsersAdmin Details view. Taking this approach allows you to reuse the same template every time you want to display an address.

To add a display template, add a new folder under the ViewsShared folder and name it DisplayTemplates . Next, add an empty view named Address using a Layout page to the new folder so that the ViewsSharedDisplayTemplatesAddress.cshtml file is created. Then update the contents of the file as follows:

@model BabyStore.Models.Address

<dt>
    @Html.DisplayNameFor(model => model.AddressLine1)
</dt>


<dd>
    @Html.DisplayFor(model => model.AddressLine1)
</dd>


<dt>
    @Html.DisplayNameFor(model => model.AddressLine2)
</dt>


<dd>
    @Html.DisplayFor(model => model.AddressLine2)
</dd>


<dt>
    @Html.DisplayNameFor(model => model.Town)
</dt>


<dd>
    @Html.DisplayFor(model => model.Town)
</dd>


<dt>
    @Html.DisplayNameFor(model => model.County)
</dt>


<dd>
    @Html.DisplayFor(model => model.County)
</dd>


<dt>
    @Html.DisplayNameFor(model => model.Postcode)
</dt>


<dd>
    @Html.DisplayFor(model => model.Postcode)
</dd>

Start the web site without debugging and then view the details of the [email protected] user. The address fields should be formatted as part of the definition list and appear consistently with the rest of the page, as shown in Figure 7-30.

A419071_1_En_7_Fig30_HTML.jpg
Figure 7-30. The UsersAdmin Detail page with address fields now formatted by a display template

Using a display template might seem a bit like overkill at the moment, but it will save code later when dealing with other areas of the site, which also displays addresses such as orders since the same display template can be reused with a single line of code.

Creating a New User as Admin

To create a new user as an Admin user, start by updating the GET version of the Create method in the ControllersUsersAdminController.cs file to assign the roles available in the database to the ViewBag as follows:

// GET: UsersAdmin/Create
public async Task<ActionResult> Create()
{
    //Get the list of Roles
    ViewBag.RoleId = new SelectList(await RoleManager.Roles.ToListAsync(), "Name", "Name");
    return View();
}

The SelectList will be used in the view to generate a list of check boxes, with one per role. Again, ViewBag has been used so that the code follows the Microsoft sample as closely as possible.

The view is going to be based on the RegisterViewModel class so this first needs to be updated to include the properties that we added to the ApplicationUser class. Modify the RegisterViewModel class in the ModelsAccountViewModels.cs file to add the properties in bold that were previously added to the ApplicationUser class:

public class RegisterViewModel
{
    [Required]
    [EmailAddress]
    [Display(Name = "Email")]
    public string Email { 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; }


    [Required]
    [Display(Name = "First Name")]
    [StringLength(50)]
    public string FirstName { get; set; }
    [Required]
    [Display(Name = "Last Name")]
    [StringLength(50)]
    public string LastName { get; set; }


    [Required]
    [DataType(DataType.Date)]
    [Display(Name = "Date of birth")]
    [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
    public DateTime DateOfBirth { get; set; }


    public Address Address { get; set; }
}

Ensure you add using System; to the top of the file so DateTime can be found. One difference between the files is that we have set the DataFormatString for DateOfBirth differently. This has been set differently because of a bug in Google Chrome that displays date formats incorrectly as the string "dd/mm/yyyy" when editing dates. This leads to an invalid date error when you’re editing an existing date.

Next right-click on the Create method in the UsersAdminController class and choose Add View. Add a view called Create using a Create template and the model class RegisterViewModel. Ensure Use a Layout Page and Reference Script Libraries are both checked. Figure 7-31 show the options.

A419071_1_En_7_Fig31_HTML.jpg
Figure 7-31. Adding a UsersAdminController Create view

The scaffolding process runs, but the process does not generate any code in the view for the address property because it is composed of another class. Update the new ViewsUsersAdminCreate.cshtml file to add an HTML EditorFor helper to display editable address fields, generate a set of check boxes for each item in the ViewBag.RoleId property, and update the <h2> heading. Then remove the <h4> heading. These changes are shown in bold in the following code:

@model BabyStore.Models.RegisterViewModel

@{
    ViewBag.Title = "Create User";
}


<h2>@ViewBag.Title</h2>

@using (Html.BeginForm())
{
    @Html.AntiForgeryToken()
    <div class="form-horizontal">
    <hr />
        @Html.ValidationSummary(true, "", new { @class = "text-danger" })
        <div class="form-group">
            @Html.LabelFor(model => model.Email, htmlAttributes: new { @class = "control-label
               col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.Email, new { htmlAttributes = new { @class =
                "form-control" } })
                @Html.ValidationMessageFor(model => model.Email, "", new { @class = "text-
                danger" })
            </div>
        </div>


        <div class="form-group">
            @Html.LabelFor(model => model.Password, htmlAttributes: new { @class = "control-
            label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.Password, new { htmlAttributes = new { @class =
                   "form-control" } })
                @Html.ValidationMessageFor(model => model.Password, "", new { @class = "text-
                   danger" })
            </div>
        </div>


        <div class="form-group">
            @Html.LabelFor(model => model.ConfirmPassword, htmlAttributes: new { @class =
               "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.ConfirmPassword, new { htmlAttributes = new {
                   @class = "form-control" } })
                @Html.ValidationMessageFor(model => model.ConfirmPassword, "", new { @class =
                   "text-danger" })
            </div>
        </div>


        <div class="form-group">
            @Html.LabelFor(model => model.FirstName, htmlAttributes: new { @class = "control-
               label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.FirstName, new { htmlAttributes = new { @class  
    ="form-control" } })
                @Html.ValidationMessageFor(model => model.FirstName, "", new { @class = "text-
                   danger" })
            </div>
        </div>


        <div class="form-group">
            @Html.LabelFor(model => model.LastName, htmlAttributes: new { @class = "control-
               label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.LastName, new { htmlAttributes = new { @class =
                   "form-control" } })
                @Html.ValidationMessageFor(model => model.LastName, "", new { @class = "text-
                   danger" })
            </div>
        </div>


        <div class="form-group">
            @Html.LabelFor(model => model.DateOfBirth, htmlAttributes: new { @class =
            "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.DateOfBirth, new { htmlAttributes = new {
                @class = "form-control" } })
                @Html.ValidationMessageFor(model => model.DateOfBirth, "", new { @class =
                "text-danger" })
            </div>
        </div>


        @Html.EditorFor(model => model.Address)

        <div class="form-group">
            <label class="col-md-2 control-label">
                Select User Role
            </label>
            <div class="col-md-10">
                @foreach (var item in (SelectList)ViewBag.RoleId)
                {
                    <input type="checkbox" name="SelectedRoles" value="@item.Value"  
                      class="checkbox-inline" />
                    @Html.Label(item.Value, new { @class = "control-label" })
                }
            </div>
        </div>


        <div class="form-group">
            <div class="col-md-offset-2 col-md-10">
                <input type="submit" value="Create" class="btn btn-default" />
            </div>
        </div>
    </div>
}


<div>
    @Html.ActionLink("Back to List", "Index")
</div>


@section Scripts {
    @Scripts.Render("∼/bundles/jqueryval")
}

Right-click the file and click View in Browser from the menu. The UsersAdmin Create HTML page should appear as shown in Figure 7-32, including the check boxes for each role.

A419071_1_En_7_Fig32_HTML.jpg
Figure 7-32. The UsersAdmin Create page

There is a similar issue with this page as there was with the Detail page. Using @Html.EditorFor(model => model.Address) has caused the Address fields to use some default HTML tags using the CSS classes editor-label and editor-field. The effect of this is that these fields are formatted differently than the rest of the page. ASP.NET MVC provides a feature similar to display templates called editor templates, and it can be used to specify how a model should appear when used with the EditFor() method.

Using an Editor Template

Editor template are a feature of ASP.NET MVC that allow you to specify a template to determine how a particular model will appear when used in the EditorFor() helper method. We’re going to introduce an editor template to display an address object correctly in the UsersAdmin Create view. Taking this approach allows you to reuse the same template every time you want to edit an address.

To add an editor template, add a new folder in the ViewsShared folder and name it EditorTemplates . Next add an empty view using a layout page named Address so that the ViewsSharedEditorTemplatesAddress.cshtml file is created. Then update the contents of the file as follows:

@model BabyStore.Models.Address

<div class="form-group">
    @Html.LabelFor(model => model.AddressLine1, htmlAttributes: new { @class = "control-label
    col-md-2" })
    <div class="col-md-10">
        @Html.EditorFor(model => model.AddressLine1, new { htmlAttributes = new { @class =
        "form-control" } })
        @Html.ValidationMessageFor(model => model.AddressLine1, "", new { @class = "text-
        danger" })
    </div>
</div>


<div class="form-group">
    @Html.LabelFor(model => model.AddressLine2, htmlAttributes: new { @class = "control-label
    col-md-2" })
    <div class="col-md-10">
        @Html.EditorFor(model => model.AddressLine2, new { htmlAttributes = new { @class =
        "form-control" } })
        @Html.ValidationMessageFor(model => model.AddressLine2, "", new { @class = "text-
        danger" })
    </div>
</div>


<div class="form-group">
    @Html.LabelFor(model => model.Town, htmlAttributes: new { @class = "control-label col-md-
        2" })
    <div class="col-md-10">
        @Html.EditorFor(model => model.Town, new { htmlAttributes = new { @class = "form-
        control" } })
        @Html.ValidationMessageFor(model => model.Town, "", new { @class = "text-danger" })
    </div>
</div>


<div class="form-group">
    @Html.LabelFor(model => model.County, htmlAttributes: new { @class = "control-label col-
    md-2" })
    <div class="col-md-10">
        @Html.EditorFor(model => model.County, new { htmlAttributes = new { @class = "form-
        control" } })
        @Html.ValidationMessageFor(model => model.County, "", new { @class = "text-danger" })
    </div>
</div>


<div class="form-group">
    @Html.LabelFor(model => model.Postcode, htmlAttributes: new { @class = "control-label col-
    md-2" })
    <div class="col-md-10">
        @Html.EditorFor(model => model.Postcode, new { htmlAttributes = new { @class = "form-
        control" } })
        @Html.ValidationMessageFor(model => model.Postcode, "", new { @class = "text-danger"  
        })
    </div>
</div>

Start the web site without debugging and then try to create a new user via the admin screen. The address fields should now be formatted like the rest of the page, as shown in Figure 7-33.

A419071_1_En_7_Fig33_HTML.jpg
Figure 7-33. The UsersAdmin Create page with address fields now formatted by an Editor Template

Next, the HttpPost version of the Create method in the UsersAdminController.cs file needs to be updated to process the data submitted by HTML form in the create view. Update the method to accept a RegisterViewModel and a collection of strings as input parameters, attempt to create a new user and assign it to the selected roles as follows:

// POST: UsersAdmin/Create
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Create(RegisterViewModel userViewModel, params string[]
    selectedRoles)
{
    if (ModelState.IsValid)
    {
        var user = new ApplicationUser
        {
            UserName = userViewModel.Email,
            Email = userViewModel.Email,
            DateOfBirth = userViewModel.DateOfBirth,
            FirstName = userViewModel.FirstName,
            LastName = userViewModel.LastName,
            Address = userViewModel.Address
        };
        var adminresult = await UserManager.CreateAsync(user, userViewModel.Password);


        //Add User to the selected Roles
        if (adminresult.Succeeded)
        {
            if (selectedRoles != null)
            {
                var result = await UserManager.AddToRolesAsync(user.Id, selectedRoles);
                if (!result.Succeeded)
                {
                    ModelState.AddModelError("", result.Errors.First());
                    ViewBag.RoleId = new SelectList(await RoleManager.Roles.ToListAsync(),
                      "Name", "Name");
                    return View();
                }
            }
        }
        else
        {
            ModelState.AddModelError("", adminresult.Errors.First());
            ViewBag.RoleId = new SelectList(RoleManager.Roles, "Name", "Name");
            return View();


        }
        return RedirectToAction("Index");
    }
    ViewBag.RoleId = new SelectList(RoleManager.Roles, "Name", "Name");
    return View();
}

After updating the method, ensure that you add using BabyStore.Models; to the top of the file. The Create method works by creating a new ApplicationUser based on the data passed in via the userViewModel input parameter, and then attempts to create a user based on this new ApplicationUser. If this succeeds, it adds the user to the roles contained in the selectedRoles input parameter. The key method calls in this method are UserManager.CreateAsync and UserManager.AddToRolesAsync. The UserManager.CreateAsync method attempts to create a user based on an ApplicationUser type and the password property passed into the method as part of the RegisterViewModel and UserManager.AddToRolesAsync adds a user to a number of roles taking a user ID and a collection of strings as input parameters.

Start the web site without debugging, log in as the Admin user, and navigate to the new UsersAdmin Create page. Add a new user to the Users role, with the e-mail address [email protected] and with the values as shown in Figure 7-34. Set the password field to P@ssw0rd.

A419071_1_En_7_Fig34_HTML.jpg
Figure 7-34. Values to use when creating [email protected]

Click the Create button. You should be returned to the Index page with the new user listed with the users, as shown in Figure 7-35.

A419071_1_En_7_Fig35_HTML.jpg
Figure 7-35. The newly created [email protected] in the UsersAdmin Index page

Click on the Details link for [email protected] and you will see the details entered when the user was created, as shown in Figure 7-36.

A419071_1_En_7_Fig36_HTML.jpg
Figure 7-36. Viewing the details of the new [email protected] user

Editing a User as Admin

To enable the editing of a user, start by adding a new view model class named EditUserViewModel to the ViewModelsAdminViewModel.cs file, as shown in bold:

using BabyStore.Models;                  
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Web.Mvc;


namespace BabyStore.ViewModels
{
    public class RoleViewModel
    {
        public string Id { get; set; }
        [Required(AllowEmptyStrings = false)]
        [Display(Name = "Role Name")]
        public string Name { get; set; }
    }


    public class EditUserViewModel
    {
        public string Id { get; set; }


        [Required(AllowEmptyStrings = false)]
        [Display(Name = "Email")]
        [EmailAddress]
        public string Email { get; set; }


        [Required]
        [Display(Name = "First Name")]
        [StringLength(50)]
        public string FirstName { get; set; }


        [Required]
        [Display(Name = "Last Name")]
        [StringLength(50)]
        public string LastName { get; set; }


        [Required]
        [DataType(DataType.Date)]    
        [Display(Name = "Date of birth")]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        public DateTime DateOfBirth { get; set; }


        public Address Address { get; set; }

        public IEnumerable<SelectListItem> RolesList { get; set; }
    }
}

This view model will be used in the Edit view. One thing to note is that it does not contain any fields regarding editing a password. Again, we set the DataFormatString for DateOfBirth to {0:yyyy-MM-dd} so that it works correctly in Google Chrome.

Next, update the GET version of the Edit method in ControllersUsersAdminController.cs file, as shown in bold, so that it finds a user and adds their details to a new instance of EditUserViewModel:

// GET: UsersAdmin/Edit/5
public async Task<ActionResult> Edit(string id)
{
    if (id == null)
    {
        return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
    }
    var user = await UserManager.FindByIdAsync(id);
    if (user == null)
    {
        return HttpNotFound();
    }


    var userRoles = await UserManager.GetRolesAsync(user.Id);

    return View(new EditUserViewModel()
    {
        Id = user.Id,
        Email = user.Email,
        DateOfBirth = user.DateOfBirth,
        FirstName = user.FirstName,
        LastName = user.LastName,
        Address = user.Address,
        RolesList = RoleManager.Roles.ToList().Select(x => new SelectListItem()
        {
            Selected = userRoles.Contains(x.Name),
            Text = x.Name,
            Value = x.Name
        })
    });
}

Ensure that you add the statement using BabyStore.ViewModels; to the top of the file. Next add a view by right-clicking on the Edit method and choosing Add View from the menu. Set the view name to Edit, the template to Edit, and the model class to EditUserViewModel. Then ensure that Reference Script Libraries and User a Layout Page are checked, as shown in Figure 7-37.

A419071_1_En_7_Fig37_HTML.jpg
Figure 7-37. Creating a UsersAdmin Edit view

In the generated view file, update the headings, add the ability to add an address, and add check boxes for the roles, as shown in bold in the following code:

@model EditUserViewModel

@{
    ViewBag.Title = "Edit User";
}


<h2>@ViewBag.Title</h2>

@using (Html.BeginForm())
{
    @Html.AntiForgeryToken()


    <div class="form-horizontal">
        <hr />
        @Html.ValidationSummary(true, "", new { @class = "text-danger" })
        @Html.HiddenFor(model => model.Id)


        <div class="form-group">
            @Html.LabelFor(model => model.Email, htmlAttributes: new { @class = "control-label
            col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.Email, new { htmlAttributes = new { @class =
                "form-control" } })
                @Html.ValidationMessageFor(model => model.Email, "", new { @class = "text-
                danger" })
            </div>
        </div>


        <div class="form-group">
            @Html.LabelFor(model => model.FirstName, htmlAttributes: new { @class = "control-
            label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.FirstName, new { htmlAttributes = new { @class
                = "form-control" } })
                @Html.ValidationMessageFor(model => model.FirstName, "", new { @class = "text-
                danger" })
            </div>
        </div>


        <div class="form-group">
            @Html.LabelFor(model => model.LastName, htmlAttributes: new { @class = "control-
            label col-md-2" })
            <div class="col-md-10">
            @Html.EditorFor(model => model.LastName, new { htmlAttributes = new { @class =
            "form-control" } })
                @Html.ValidationMessageFor(model => model.LastName, "", new { @class = "text-
                danger" })
            </div>
        </div>


        <div class="form-group">
            @Html.LabelFor(model => model.DateOfBirth, htmlAttributes: new { @class =
            "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.DateOfBirth, new { htmlAttributes = new {
                @class = "form-control" } })
                @Html.ValidationMessageFor(model => model.DateOfBirth, "", new { @class =
                "text-danger" })
            </div>
        </div>


        @Html.EditorFor(model => model.Address)

        <div class="form-group">
            @Html.Label("Roles", new { @class = "control-label col-md-2" })
            <span class="col-md-10">
                @foreach (var item in Model.RolesList)
                {
                    <input type="checkbox" name="SelectedRole" value="@item.Value"
                      checked="@item.Selected" class="checkbox-inline" />
                    @Html.Label(item.Value, new { @class = "control-label" })
                }
            </span>
        </div>


        <div class="form-group">
            <div class="col-md-offset-2 col-md-10">
                <input type="submit" value="Save" class="btn btn-default" />
            </div>
        </div>
    </div>
}


<div>
    @Html.ActionLink("Back to List", "Index")
</div>


@section Scripts {
    @Scripts.Render("∼/bundles/jqueryval")
}

Start the web site without debugging, log in as the Admin user, and navigate to the UsersAdmin Index page. Click the edit link for the [email protected] user. The new Edit page should appear as shown in Figure 7-38.

A419071_1_En_7_Fig38_HTML.jpg
Figure 7-38. The newly created UsersAdmin Edit page populated with details of [email protected]

One additional change we need to make to this view is to make the e-mail read only so it cannot be edited. We won’t bind this when updating the user anyway, but we also want to stop the users from thinking they can update it in the view. We could use the DisplayFor helper, but it renders the e-mail address as a link and we don’t want this. Neither do I like the way that it is aligned if we just use @Model.Email, so we’re going to use the readonly HTML attribute to continue to display the e-mail in a text box and ensure that the users cannot edit it.

Edit the EditorFor helper method for the model.Email property so that it now reads as follows:

@Html.EditorFor(model => model.Email, new { htmlAttributes = new { @class = "form-control",@readonly = "readonly" } }). The text box containing the user’s e-mail address will now be greyed out.

Next update the HttpPost version of the Edit method in the ControllersUsersAdminController.cs file so that it takes an EditUserViewModel and a collection of strings to represent the selected roles as input parameters and then uses these to update the user’s details and roles:

// POST: UsersAdmin/Edit/5
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Edit(EditUserViewModel editUser, params string[] selectedRole)
{
    if (ModelState.IsValid)
    {
        var user = await UserManager.FindByIdAsync(editUser.Id);
        if (user == null)
        {
            return HttpNotFound();
        }


        user.DateOfBirth = editUser.DateOfBirth;
        user.FirstName = editUser.FirstName;
        user.LastName = editUser.LastName;
        user.Address = editUser.Address;


        var userRoles = await UserManager.GetRolesAsync(user.Id);

        selectedRole = selectedRole ?? new string[] { };

        var result = await UserManager.AddToRolesAsync(user.Id,
            selectedRole.Except(userRoles).ToArray<string>());


        if (!result.Succeeded)
        {
            ModelState.AddModelError("", result.Errors.First());
            return View();
        }
        result = await UserManager.RemoveFromRolesAsync(user.Id,
            userRoles.Except(selectedRole).ToArray<string>());


        if (!result.Succeeded)
        {
            ModelState.AddModelError("", result.Errors.First());
            return View();
        }
        return RedirectToAction("Index");
    }
    ModelState.AddModelError("", "Something failed.");
    return View();
}

This method assigns the properties of the view model submitted from the HTML page (apart from the e-mail address) to the user and then finds the roles that the user belongs to. It then adds the user to any roles in the selectedRole input parameter that they do not already belong to and removes them from any that they do belong to but that are not in selectedRole. The only new method call that has not appeared in the previous methods is UserManager.RemoveFromRolesAsync().

Note

This code never calls UserManager.UpdateAsync(), yet the updated user details are saved. The changes to the rest of the user details are saved during the calls to UserManager.AddToRolesAsync() and UserManager.RemoveFromRolesAsync().

Start the web site without debugging and edit [email protected] by updating the address and date of birth. Add them to the Admin group and remove them from the Users group, as shown in Figure 7-39.

A419071_1_En_7_Fig39_HTML.jpg
Figure 7-39. Editing the user [email protected]

Click the Save button. The changes will save and you will be redirected to the Index page. Next, view the details for [email protected] and verify the changes you made, as shown in Figure 7-40.

A419071_1_En_7_Fig40_HTML.jpg
Figure 7-40. The details of the updated [email protected]

Dealing with Deleting Users

I am not going to include code for deleting users in this project because in almost every commercial project I have worked on, deleting users has never been a requirement. Regulatory requirements often specify that user accounts are always kept in the system so that data relating to any user can be found. For example, consider the scenario where a user places several orders in this web site, but the user is then deleted from the system. The company would have no way of knowing who placed the orders.

There are several scenarios for how to manage users who are no longer active or in some cases banned from using the system, including either setting some kind of archive or status field for a user indicating their current status. For example, on gaming web sites it is generally standard of the regulatory requirements that a user can be have many statuses, including registered but awaiting identity verification, approved to play, self-imposed ban, banned from playing by the company, banned from playing by the gaming authorities, and inactive (after a set limit of inactivity), but never deleted. If you do encounter a project where users cannot be deleted, it is also important to apply relevant security and audit trails to the database that holds the user details.

Should you want to delete users via a controller, then you can do this by passing the user as a parameter to the method UserManager.DeleteAsync(user).

If you are following along with the code examples, then remove the code @Html.ActionLink("Delete", "Delete", new { id = item.Id }) from the ViewsUsersAdminIndex.cshtml file and remove the Delete methods from the ControllersUsersAdminController.cs file.

User Self-Registration

So far we have allowed an Admin user to create users but provided no way for users to register themselves. We’ll address this issue by modifying the existing methods and views created when we initially created the project. The controllers that handle user self-management are the ControllersAccountController.cs and ControllersManageController.cs files.

Start by opening the ControllersAccountController.cs file and modifying the HttpPost version of the Register method so that all the fields in the RegisterViewModel are assigned during the user creation and assign the new user to the Users role, as follows:

[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Register(RegisterViewModel model)
{
    if (ModelState.IsValid)
    {
        var user = new ApplicationUser {
            UserName = model.Email,
            Email = model.Email,
            DateOfBirth = model.DateOfBirth,
            FirstName = model.FirstName,
            LastName = model.LastName,
            Address = model.Address
        };
        var result = await UserManager.CreateAsync(user, model.Password);
            if (result.Succeeded)
            {
                await UserManager.AddToRoleAsync(user.Id, "Users");
                await SignInManager.SignInAsync(user, isPersistent:false,
                rememberBrowser:false);
                    // For more information on how to enable account confirmation and password
                    //reset please visit http://go.microsoft.com/fwlink/?LinkID=320771
                // Send an email with this link
                // string code = await
                    ///UserManager.GenerateEmailConfirmationTokenAsync(user.Id);
                // var callbackUrl = Url.Action("ConfirmEmail", "Account", new { userId =
                    //user.Id, code = code }, protocol: Request.Url.Scheme);
                // await UserManager.SendEmailAsync(user.Id, "Confirm your account", "Please
                    //confirm your account by clicking <a href="" + callbackUrl + "">here</a>");


            return RedirectToAction("Index", "Home");
        }
        AddErrors(result);
    }


    // If we got this far, something failed, redisplay form
    return View(model);
}
Note

The AccountController has authorization applied to it at a class level by using the [Authorize] attribute above the class declaration. However, this is overridden for the Register method, because this needs to be accessed anonymously so users can register, by applying the attribute [AllowAnonymous]. This is applied to both versions of the Register and Login methods.

Next we are going to delete and recreate the Register view file to add the new fields we’ve added to RegisterViewModel. Delete the ViewsAccountRegister.cshtml file.

Right-click on the Register method in the AccountController and choose Add View from the menu. Name the view Register using the Create template and set the model class RegisterViewModel. Set the Data Context Class to blank and ensure that Reference Script Libraries and Use a Layout Page are checked, as shown in Figure 7-41.

A419071_1_En_7_Fig41_HTML.jpg
Figure 7-41. Recreating the Account Register view

Update the new file by setting the <h2> heading, removing the <h4> heading, and adding the address fields as follows:

@model BabyStore.Models.RegisterViewModel

@{
    ViewBag.Title = "Register";
}


<h2>@ViewBag.Title</h2>

@using (Html.BeginForm())
{
    @Html.AntiForgeryToken()
    <div class="form-horizontal">
        <hr />
        @Html.ValidationSummary(true, "", new { @class = "text-danger" })
        <div class="form-group">
            @Html.LabelFor(model => model.Email, htmlAttributes: new { @class = "control-label
            col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.Email, new { htmlAttributes = new { @class =
                "form-control" } })
                @Html.ValidationMessageFor(model => model.Email, "", new { @class = "text-
                danger" })
            </div>
        </div>


        <div class="form-group">
            @Html.LabelFor(model => model.Password, htmlAttributes: new { @class = "control-
            label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.Password, new { htmlAttributes = new { @class =
                "form-control" } })
                @Html.ValidationMessageFor(model => model.Password, "", new { @class = "text-
                danger" })
            </div>
        </div>


        <div class="form-group">
            @Html.LabelFor(model => model.ConfirmPassword, htmlAttributes: new { @class =
            "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.ConfirmPassword, new { htmlAttributes = new {
                @class = "form-control" } })
                @Html.ValidationMessageFor(model => model.ConfirmPassword, "", new { @class =
                "text-danger" })
            </div>
        </div>


        <div class="form-group">
            @Html.LabelFor(model => model.FirstName, htmlAttributes: new { @class = "control-
            label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.FirstName, new { htmlAttributes = new { @class
                ="form-control" } })
                @Html.ValidationMessageFor(model => model.FirstName, "", new { @class = "text-
                danger" })
            </div>
        </div>


        <div class="form-group">
            @Html.LabelFor(model => model.LastName, htmlAttributes: new { @class = "control-
            label col-md-2" })
            <div class="col-md-10">
             @Html.EditorFor(model => model.LastName, new { htmlAttributes = new { @class =
            "form-control" } })
                @Html.ValidationMessageFor(model => model.LastName, "", new { @class = "text-
                danger" })
            </div>
        </div>


        <div class="form-group">
            @Html.LabelFor(model => model.DateOfBirth, htmlAttributes: new { @class =  
            "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.DateOfBirth, new { htmlAttributes = new {
                @class= "form-control" } })
                @Html.ValidationMessageFor(model => model.DateOfBirth, "", new { @class =  
                "text-danger" })
            </div>
        </div>


        @Html.EditorFor(model => model.Address)

        <div class="form-group">
            <div class="col-md-offset-2 col-md-10">
                <input type="submit" value="Create" class="btn btn-default" />
            </div>
        </div>
    </div>
}


<div>
    @Html.ActionLink("Back to List", "Index")
</div>


@section Scripts {
    @Scripts.Render("∼/bundles/jqueryval")
}

There’s a lot of duplication between this code and the code in the ViewsUsersAdminCreate.cshtml file, so we’re going to create a partial view to contain the shared code. Right-click on the ViewsShared folder and choose Add View from the menu. Create a new empty view named _CreateRegisterUserPartial based on the model class RegisterViewModel, set the Data Context Class to blank, and ensure that Create as a Partial View is checked, as shown in Figure 7-42.

A419071_1_En_7_Fig42_HTML.jpg
Figure 7-42. Creating a partial view for user creation and registration

Update the contents of the new partial view as follows to add the shared fields:

@model BabyStore.Models.RegisterViewModel

<hr />
@Html.ValidationSummary(true, "", new { @class = "text-danger" })
<div class="form-group">
    @Html.LabelFor(model => model.Email, htmlAttributes: new { @class = "control-label col-md-
    2"})
    <div class="col-md-10">
        @Html.EditorFor(model => model.Email, new { htmlAttributes = new { @class = "form-
        control" } })
        @Html.ValidationMessageFor(model => model.Email, "", new { @class = "text-danger" })
    </div>
</div>


<div class="form-group">
    @Html.LabelFor(model => model.Password, htmlAttributes: new { @class = "control-label col-
    md-2" })
    <div class="col-md-10">
        @Html.EditorFor(model => model.Password, new { htmlAttributes = new { @class = "form-
        control" } })
        @Html.ValidationMessageFor(model => model.Password, "", new { @class = "text-danger" })
    </div>
</div>


<div class="form-group">
    @Html.LabelFor(model => model.ConfirmPassword, htmlAttributes: new { @class = "control-
    label col-md-2" })
    <div class="col-md-10">
        @Html.EditorFor(model => model.ConfirmPassword, new { htmlAttributes = new { @class =
        "form-control" } })
        @Html.ValidationMessageFor(model => model.ConfirmPassword, "", new { @class = "text-
        danger" })
    </div>
</div>


<div class="form-group">
    @Html.LabelFor(model => model.FirstName, htmlAttributes: new { @class = "control-label
    col-md-2" })
    <div class="col-md-10">
        @Html.EditorFor(model => model.FirstName, new { htmlAttributes = new { @class = "form-
        control" } })
        @Html.ValidationMessageFor(model => model.FirstName, "", new { @class = "text-danger"
            })
    </div>
</div>


<div class="form-group">
    @Html.LabelFor(model => model.LastName, htmlAttributes: new { @class = "control-label col-
    md-2" })
    <div class="col-md-10">
        @Html.EditorFor(model => model.LastName, new { htmlAttributes = new { @class = "form-
        control" } })
        @Html.ValidationMessageFor(model => model.LastName, "", new { @class = "text-danger"
        })
    </div>
</div>


<div class="form-group">
    @Html.LabelFor(model => model.DateOfBirth, htmlAttributes: new { @class = "control-label
    col-md-2" })
    <div class="col-md-10">
        @Html.EditorFor(model => model.DateOfBirth, new { htmlAttributes = new { @class =
        "form-control" } })
        @Html.ValidationMessageFor(model => model.DateOfBirth, "", new { @class = "text-
        danger"})
    </div>
</div>


@Html.EditorFor(model => model.Address)

Next update the ViewsUsersAdminCreate.cshtml file to use the new partial view as follows:

@model BabyStore.Models.RegisterViewModel

@{
    ViewBag.Title = "Create User";
}


<h2>@ViewBag.Title</h2>

@using (Html.BeginForm())
{
    @Html.AntiForgeryToken()
    <div class="form-horizontal">
        @Html.Partial("_CreateRegisterUserPartial", Model)
        <div class="form-group">
            <label class="col-md-2 control-label">
                Select User Role
            </label>
            <div class="col-md-10">
                @foreach (var item in (SelectList)ViewBag.RoleId)
                {
                    <input type="checkbox" name="SelectedRoles" value="@item.Value"
                      class="checkbox-inline" />
                    @Html.Label(item.Value, new { @class = "control-label" })
                }
            </div>
        </div>


        <div class="form-group">
            <div class="col-md-offset-2 col-md-10">
                <input type="submit" value="Create" class="btn btn-default" />
            </div>
        </div>
    </div>
}


<div>
    @Html.ActionLink("Back to List", "Index")
</div>


@section Scripts {
    @Scripts.Render("∼/bundles/jqueryval")
}

Then also update the ViewsAccountRegister.cshtml file to use the new partial view as follows:

@model BabyStore.Models.RegisterViewModel

@{
    ViewBag.Title = "Register";
}


<h2>@ViewBag.Title</h2>

@using (Html.BeginForm())
{
    @Html.AntiForgeryToken()
    <div class="form-horizontal">
        @Html.Partial("_CreateRegisterUserPartial", Model);
        <div class="form-group">
            <div class="col-md-offset-2 col-md-10">
                <input type="submit" value="Create" class="btn btn-default" />
            </div>
        </div>
    </div>
}


<div>
    @Html.ActionLink("Back to List", "Index")
</div>


@section Scripts {
    @Scripts.Render("∼/bundles/jqueryval")
}

Start the web site without debugging and ensure that you are logged out. Test the new user self-registration code by clicking on the Register link and registering as a new user with the e-mail address [email protected]. Use the values shown in Figure 7-43 and set the password fields to P@ssw0rd. When you click the Create button, the user should be created successfully, and you should be logged in as the new user and redirect to the home page.

A419071_1_En_7_Fig43_HTML.jpg
Figure 7-43. Registering as the new [email protected] user

Log out and log back into the web site as the Admin user [email protected] with the [email protected] password. Navigate through the Admin section to view the details of the new user [email protected]. You will see that the user belongs to the Users role, as shown in Figure 7-44.

A419071_1_En_7_Fig44_HTML.jpg
Figure 7-44. The details of the user [email protected]

Allowing a User to View Personal Details

To allow users to view their personal details, start by modifying the Index method of the ControllersManageController.cs file so that it reads as follows:

public async Task<ActionResult> Index()
{
    var userId = User.Identity.GetUserId();
    var user = await UserManager.FindByIdAsync(userId);
    return View(user);
}

This method uses the User.Identity.GetUserId code to get the current user’s ID, ensuring that a user cannot view another user’s details.

Next we need to create a new Index view. The current Index view does not contain anything that we want to show to the users, so delete it and add a new view similar to the UsersAdmin Details view.

First, delete the ViewsManageIndex.cshtml file and then right-click on the Index method in the ManageController class and click Add View. Create a new view named Index and set the template to Empty (without model). Ensure that Use a Layout Page is checked and then click the Add button. A new empty Index.cshtml file should be created. We’re going to base this view on the ApplicationUser model and, as I mentioned when creating the UsersAdmin Details view, this model does not work with the scaffolding process.

Before we code the view, we’re going to add a partial view, because just as there was with the registration process, there is also some overlap between this Index view and the UsersAdmin Details view.

Add a new partial view by right-clicking on the ViewsShared folder and choosing Add View. Set the view name to _UserDetailsPartial and the template to Empty (without model). Then ensure that Create as a Partial View is checked, as shown in Figure 7-45.

A419071_1_En_7_Fig45_HTML.jpg
Figure 7-45. Creating a partial view for user details

Modify the new _UserDetailsPartial.cshtml file contents as follows to base it on the ApplicationUser class and display common information between the UsersAdmin Details and the Manage Index views:

@model BabyStore.Models.ApplicationUser

<dt>
    @Html.DisplayNameFor(model => model.FirstName)
</dt>
<dd>
    @Html.DisplayFor(model => model.FirstName)
</dd>
<dt>
    @Html.DisplayNameFor(model => model.LastName)
</dt>
<dd>
    @Html.DisplayFor(model => model.LastName)
</dd>
<dt>
    @Html.DisplayNameFor(model => model.Email)
</dt>


<dd>
    @Html.DisplayFor(model => model.Email)
</dd>


<dt>
    @Html.DisplayNameFor(model => model.DateOfBirth)
</dt>


<dd>
    @Html.DisplayFor(model => model.DateOfBirth)
</dd>
@Html.DisplayFor(model => model.Address)

Next update the ViewsUsersAdminDetails.cshtml file to use the new partial view:

@model BabyStore.Models.ApplicationUser
@{
    ViewBag.Title = "User Details";
}


<h2>@ViewBag.Title</h2>

<div>
    <hr />
    <dl class="dl-horizontal">
        @Html.Partial("_UserDetailsPartial", Model)
    </dl>
</div>
<div class="container">
    <div class="row">
        <h4>Roles this user belongs to:</h4>
    </div>
    @if (ViewBag.RoleNames.Count == 0)
    {
        <hr />
        <p>No roles found for this user</p>
    }


    @foreach (var item in ViewBag.RoleNames)
    {
        <div class="row">
            @item
        </div>
    }
</div>
<p>
    @Html.ActionLink("Edit", "Edit", new { id = Model.Id }) |
    @Html.ActionLink("Back to List", "Index")
</p>

This view will work exactly as before when viewing a user’s details via the Admin menu. Next update the new ViewsManageIndex.cshtml file to be modeled on the ApplicationUser class and use the new partial view file as follows:

@model BabyStore.Models.ApplicationUser

@{
    ViewBag.Title = "My Details";
}


<h2>@ViewBag.Title</h2>

<div>
    <hr />
    <dl class="dl-horizontal">
        @Html.Partial("_UserDetailsPartial", Model)
        <dt>
            Password
        </dt>
        <dd>
            @Html.ActionLink("Change your password", "ChangePassword")
        </dd>
    </dl>
</div>


<p>
    @Html.ActionLink("Edit", "Edit", new { id = Model.Id })
</p>

To see the new ViewsManageIndex.cshtml file being used, start the web site without debugging and log in as the user [email protected] with the P@ssw0rd password. Next, click on the "Hello [email protected]" link. The My Details HTML page generated by the ViewsManageIndex.cshtml file will appear, as shown in Figure 7-46.

A419071_1_En_7_Fig46_HTML.jpg
Figure 7-46. The [email protected] user viewing their personal details

If you log out and then back in as the Admin user [email protected] with the [email protected] password and then navigate through the Admin section to view the details of the new user, you will see that the partial view being used has made no alteration to the HTML generated by the ViewsUsersAdminDetails.cshtml view file. It still appears as in Figure 7-44.

Allowing Users to Edit Personal Details

To complete the section, we’re going to cover allowing users to edit their details. This uses a similar process to an administrator editing a user’s details.

Start by adding a new Edit method to the ControllersManageController.cs file to find the details of the current user, and then return them to the view via a view model. We’re going to reuse the view model EditUserViewModel but won’t use the ID or RolesList properties. Add the following code to the ManageController class:

// GET: /Manage/Edit
public async Task<ActionResult> Edit()
{
    var userId = User.Identity.GetUserId();
    var user = await UserManager.FindByIdAsync(userId);
    var model = new EditUserViewModel
    {
        Email = user.Email,
        DateOfBirth = user.DateOfBirth,
        FirstName = user.FirstName,
        LastName = user.LastName,
        Address = user.Address
    };
    return View(model);
}

Ensure that you add the statement using BabyStore.ViewModels; to the top of the file. Next, add a new edit view based on an edit template using the EditUserViewModel model, and then ensure that Use a Layout Page and Reference Script Libraries are checked. Figure 7-47 shows the options to use for the new view.

A419071_1_En_7_Fig47_HTML.jpg
Figure 7-47. Adding a new Manage Edit view

As with the previous views we’ve created for users, we are going to create a partial view to share between this view and the UsersAdmin Edit view. Add a new partial view by right-clicking on the ViewsShared folder and choosing Add View. Add an empty partial view named _EditUserPartial based on the model class EditUserViewModel. The options to choose are shown in Figure 7-48.

A419071_1_En_7_Fig48_HTML.jpg
Figure 7-48. Add the _EditUserPartial partial view

Update the contents of the new view so that it contains the HTML common to both ViewsManageEdit.cshtml and ViewsUsersAdminEdit.cshtml.

@model BabyStore.ViewModels.EditUserViewModel

<hr />
@Html.ValidationSummary(true, "", new { @class = "text-danger" })


<div class="form-group">
    @Html.LabelFor(model => model.Email, htmlAttributes: new { @class = "control-label
    col-md-2"})
    <div class="col-md-10">
        @Html.EditorFor(model => model.Email, new { htmlAttributes = new { @class = "form-
        control", @readonly = "readonly" } })
        @Html.ValidationMessageFor(model => model.Email, "", new { @class = "text-danger" })
    </div>
</div>


<div class="form-group">
    @Html.LabelFor(model => model.FirstName, htmlAttributes: new { @class = "control-label
    col-md-2" })
    <div class="col-md-10">
        @Html.EditorFor(model => model.FirstName, new { htmlAttributes = new { @class = "form-
        control" } })
        @Html.ValidationMessageFor(model => model.FirstName, "", new { @class = "text-danger"
            })
    </div>
</div>


<div class="form-group">
    @Html.LabelFor(model => model.LastName, htmlAttributes: new { @class = "control-label col-
    md-2" })
    <div class="col-md-10">
        @Html.EditorFor(model => model.LastName, new { htmlAttributes = new { @class = "form-
        control" } })
        @Html.ValidationMessageFor(model => model.LastName, "", new { @class = "text-danger"
        })
    </div>
</div>


<div class="form-group">
    @Html.LabelFor(model => model.DateOfBirth, htmlAttributes: new { @class = "control-label
    col-md-2" })
    <div class="col-md-10">
        @Html.EditorFor(model => model.DateOfBirth, new { htmlAttributes = new { @class =
        "form-control" } })
        @Html.ValidationMessageFor(model => model.DateOfBirth, "", new { @class = "text-
        danger"})
    </div>
</div>


@Html.EditorFor(model => model.Address)

Following this, now update the ViewsUsersAdminEdit.cshtml file to use the new partial view:

@model EditUserViewModel

@{
    ViewBag.Title = "Edit User";
}


<h2>@ViewBag.Title</h2>

@using (Html.BeginForm())
{
    @Html.AntiForgeryToken()
    @Html.HiddenFor(model => model.Id)
    <div class="form-horizontal">
        @Html.Partial("_EditUserPartial", Model)
        <div class="form-group">
            @Html.Label("Roles", new { @class = "control-label col-md-2" })
            <span class=" col-md-10">
                @foreach (var item in Model.RolesList)
                {
                    <input type="checkbox" name="SelectedRole" value="@item.Value"
                      checked="@item.Selected" class="checkbox-inline" />
                    @Html.Label(item.Value, new { @class = "control-label" })
                }
            </span>
        </div>


        <div class="form-group">
            <div class="col-md-offset-2 col-md-10">
                <input type="submit" value="Save" class="btn btn-default" />
            </div>
        </div>
    </div>
}


<div>
    @Html.ActionLink("Back to List", "Index")
</div>


@section Scripts {
    @Scripts.Render("∼/bundles/jqueryval")
}

Finally, update the new ViewsManageEdit.cshtml file to use the new partial view and edit the headings as follows.

@model BabyStore.ViewModels.EditUserViewModel

@{
    ViewBag.Title = "Edit My Details";
}


<h2>@ViewBag.Title</h2>

@using (Html.BeginForm())
{
    @Html.AntiForgeryToken()


    <div class="form-horizontal">
        @Html.Partial("_EditUserPartial", Model)
        <div class="form-group">
            <div class="col-md-offset-2 col-md-10">
                <input type="submit" value="Save" class="btn btn-default" />
            </div>
        </div>
    </div>
}


<div>
    @Html.ActionLink("Back to List", "Index")
</div>


@section Scripts {
    @Scripts.Render("∼/bundles/jqueryval")
}

Start the web site without debugging and log in as [email protected] with the P@ssw0rd password. View the user’s details. Then click the Edit link. The new Edit My Details page generated from the ViewsManage/Edit.cshtml file should appear, as shown in Figure 7-49.

A419071_1_En_7_Fig49_HTML.jpg
Figure 7-49. The new User Edit page

To complete the process of allowing users to edit their details, you need an HttpPost version of the Edit method. This method is not going to take an ID as an input parameter because if it did then a user could edit another user’s details by spoofing the details in the HTML form. This is why we did not set the ID property of the EditUserViewModel in the GET version of the Edit method or use it in the ViewsManageEdit.cshtml file.

To process POST requests, add the following method to the ControllersManageController.cs file:

// POST: Manage/Edit/
// To protect from overposting attacks, please enable the specific properties you want to bind to, for
// more details see http://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost, ActionName("Edit")]
[ValidateAntiForgeryToken]
public async Task<ActionResult> EditPost()
{
    var userId = User.Identity.GetUserId();
    var userToUpdate = await UserManager.FindByIdAsync(userId);
    if (TryUpdateModel(userToUpdate, "", new string[] {
        "FirstName",
        "LastName",
        "DateOfBirth",
        "Address" }))
    {
        await UserManager.UpdateAsync(userToUpdate);
        return RedirectToAction("Index");
    }
    return View();
}

The method is named EditPost because it has the same signature as the previously created Edit method and so needs a different name. It finds the current user and then calls TryUpdateModel() to update the userToUpdate object. It’s then saved to the database by calling the UserManager.UpdateAsync() method.

To try the completed editing process, start the web site without debugging and log in as [email protected] with the P@ssw0rd password. View the user’s details. Then click the Edit link and change the user’s address as shown in Figure 7-50.

A419071_1_En_7_Fig50_HTML.jpg
Figure 7-50. Updating [email protected]'s address

Click the Save button. You will be returned to the Index page with the updated address displayed as shown in Figure 7-51.

A419071_1_En_7_Fig51_HTML.jpg
Figure 7-51. The updated user details of [email protected]

Allowing Users to Reset Their Passwords

At the moment the web site has no way for users to reset their passwords. The ASP.NET Identity code that enables users to reset their passwords works on the basis that a user can be e-mailed, but since we’re using fictional e-mail addresses, we’re going to cut this step out of the code while still explaining how this functionality works.

In a real production system, I recommend that you implement the code to send an external link to the user’s e-mail address and then allow them to click on a link back to the reset password page. The code to do this and a link with to a Microsoft tutorial can be found in the ForgotPassword method of the AccountController.

Start by modifying the ResetPassword method in the ControllersAccountController.cs file as follows. This change comments out the code that checks the value of the variable named code.

//
// GET: /Account/ResetPassword
[AllowAnonymous]
public ActionResult ResetPassword(string code)
{
    //return code == null ? View("Error") : View();
    return View();
}

If we were sending out e-mails, the code variable would be generated in the HttpPost version of the ForgotPassword function and sent out to the users as an e-mail containing a link to click on. You can view the code to perform this task currently commented out in the ForgotPassword method.

We are now going to generate the code variable at the point where it is used by modifying the HttpPost version of the ResetPassword method as follows:

//
// POST: /Account/ResetPassword
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> ResetPassword(ResetPasswordViewModel model)
{
    if (!ModelState.IsValid)
    {
        return View(model);
    }
    var user = await UserManager.FindByNameAsync(model.Email);
    if (user == null)
    {
        // Don't reveal that the user does not exist
        return RedirectToAction("ResetPasswordConfirmation", "Account");
    }
    string code = await UserManager.GeneratePasswordResetTokenAsync(user.Id);
    var result = await UserManager.ResetPasswordAsync(user.Id, code, model.Password);
    if (result.Succeeded)
    {
        return RedirectToAction("ResetPasswordConfirmation", "Account");
    }
    AddErrors(result);
    return View();
}
Caution

Because we are doing this for demonstration purposes and have bypassed sending confirmation e-mail, this code will allow you to alter the password for any user that you enter a valid e-mail address for. Therefore, never use this version of code in a real system. With the confirmation email in place, the code variable would be matched up to the e-mail address to ensure that the user is who they claim to be.

Next uncomment the "Forgot your password?" link in the Views/Account/Login.cshtml file and update it to allow the user to access the account ResetPassword view.

...previous code omitted for brevity
        <p>
            @Html.ActionLink("Register as a new user", "Register")
        </p>
        <p>
            @Html.ActionLink("Forgot your password?", "ResetPassword")
        </p>
    }
</section>
...following code omitted for brevity

Now start the web site without debugging and click on the Log In link. Then click on the Forgot Your Password? link. Use the Reset Password screen to change the password for [email protected] to P@ssw0rd1 and save your changes. The password will be changed and you can now use the new password to log in as [email protected].

Managing Password Complexity

Throughout this chapter we’ve been creating users with more complex “strong” passwords. To change the rules for password complexity, update the PasswordValidator code found in the App_StartIdentityConfig.cs file. This can be found in the Create method of the ApplicationUserManager class and we’ve listed the standard code for reference.

// Configure validation logic for passwords
manager.PasswordValidator = new PasswordValidator
{
    RequiredLength = 6,
    RequireNonLetterOrDigit = true,
    RequireDigit = true,
    RequireLowercase = true,
    RequireUppercase = true,
};

Adding Authorization for Product and Category Administration

We are now going to use the Admin role to manage the availability of the links and for admin features such as creating, deleting, and updating categories and products.

Adding Authorization to Categories

To add authorization to show or hide links for creating, editing, and deleting categories, modify the ViewsCategoriesIndex.cshtml file as highlighted in bold:

@model IEnumerable<BabyStore.Models.Category>

@{
    ViewBag.Title = "Categories";
}


<h2>@ViewBag.Title</h2>

@if (Request.IsAuthenticated && User.IsInRole("Admin"))
{
    <p>
        @Html.ActionLink("Create New", "Create")
    </p>
}
<table class="table">
    <tr>
        <th>
            @Html.DisplayNameFor(model => model.Name)
        </th>
        <th></th>
    </tr>


@foreach (var item in Model) {
    <tr>
        <td>
            @Html.ActionLink(item.Name, "Index", "Products", new { category = item.Name },
            null)
        </td>
        <td>
            @if (Request.IsAuthenticated && User.IsInRole("Admin"))
            {
                @Html.ActionLink("Edit", "Edit", new { id=item.ID })
                @Html.Raw(" | ")
                @Html.ActionLink("Delete", "Delete", new { id=item.ID })
            }
        </td>
    </tr>
}
</table>

In the modified code, checks were added to ensure that the links to create, edit, and delete categories are hidden if a user is not in the Admin role or not logged in, as shown in Figure 7-52. If the user is in the Admin role, then the links will be displayed, as shown in Figure 7-53.

A419071_1_En_7_Fig52_HTML.jpg
Figure 7-52. Links for creating, editing, and deleting a category are now hidden unless you’re logged in as an Admin
A419071_1_En_7_Fig53_HTML.jpg
Figure 7-53. When logged in as [email protected] the links to create, edit, and delete a category are visible

At the moment, a “non-admin” user can still manually enter URLs for the edit, delete, and create pages and access them. To prevent access to these methods, we need to modify the ControllersCategoriesController.cs file. In order to prevent anyone not in the Admin role from accessing these URLs, add the following line of code prior to the CategoriesController class declaration so it reads as follows:

[Authorize(Roles = "Admin")]                
public class CategoriesController : Controller

Now add the [AllowAnonymous] attribute before the Index method as follows, so that anyone can view the list of categories without logging in:

// GET: Categories
[AllowAnonymous]
public ActionResult Index()
{
    return View(db.Categories.OrderBy(c => c.Name).ToList());
}

Start the web site without debugging and without logging in. Ensure that you can access the Categories Index page by clicking on the Shop by Category link. Next try to manually enter the following URLs both without logging in and logged in as [email protected]:

  • Categories/Create

  • Categories/Edit/1

  • Categories/Delete/1

Each time you will be prompted to log in. If you then log in as a user in the Admin role, then you will be able to access these pages.

Finally, delete the ViewsCategoriesDetails.cshtml file and remove the Details method from the ControllersCategoriesController.cs file, as they are not needed.

Adding Authorization to Products

As we did for categories, we’re now going to use the Admin role to manage the availability of the links for the admin features of creating, deleting, and updating products.

To add authorization to show or hide links for creating, editing, and deleting products, modify the Views/Products/Index.cshtml file as follows:

@if (Request.IsAuthenticated && User.IsInRole("Admin"))            
{
    @Html.ActionLink("Create New", "Create")
}
...
@if (Request.IsAuthenticated && User.IsInRole("Admin"))
{
    @Html.ActionLink("Edit", "Edit", new { id = item.ID })
    @Html.Raw(" | ")
    @Html.ActionLink("Delete", "Delete", new { id = item.ID })
}

We’ve removed the details link since details are now accessed by clicking on the image.

Also for completeness you will need to update the ViewsProductsDetails.cshtml file to add authorization to the Edit link as follows:

<p>
    @if (Request.IsAuthenticated && User.IsInRole("Admin"))
    {
        @Html.ActionLink("Edit", "Edit", new { id = Model.ID })
        @Html.Raw(" | ")
    }
    @Html.ActionLink("Back to List", "Index")
</p>

To prevent users from manually entering the URLs to create, edit, and delete products, add authorization to the ControllersProductsController.cs file as follows.

Add an authentication attribute to the ProductsController class:

[Authorize(Roles = "Admin")]                
public class ProductsController : Controller

Now add the [AllowAnonymous] attribute before the Index and Details methods, as follows, so that anyone can view the list of products or the details of an individual product without logging in:

// GET: Products
[AllowAnonymous]
public ActionResult Index(string category, string search, string sortBy, int? page)
{...


// GET: Products/Details/5
[AllowAnonymous]
public ActionResult Details(int? id)
{

Start the web site without debugging and without logging in. Ensure that you can access the Products Index page by clicking on the View All Our Products link. Also click on a product’s image to verify you can view the product’s details.

Now try to manually enter the following URLs both without logging in and logged in as [email protected]:

  • Products/Create

  • Products/Edit/1

  • Products/Delete/1

Each time you will be prompted to log in. If you then log in as a user in the Admin role, you will be able to access these pages.

Finally, log in as the [email protected] user with the [email protected] password. Click on View All Our Products. You should see links to create, edit, and delete links and should be able to access each of the pages by clicking each link.

Note

If you need to perform more complex logic to validate a user, I recommend that you try to perform the logic in your controller classes and then set a Boolean value in your view model where possible. Because we are using pages generated by scaffolding, we’ve added the logic to check the user’s role to the view file directly.

Improving Redirection after Logging In or Registration

You might have noticed during this chapter that when you log in to the web site, you are not always redirected to the page you were on. If you are trying to access a page that requires authentication, for example to edit products, and you log in successfully the first time, the redirection works okay. However, under all other circumstances, such as you are just viewing products and log in, or you enter your credentials incorrectly, when you do log in then you are redirected back to the home page.

Redirecting Correctly After an Unsuccessful Then Successful Log In Attempt

To ensure that the URL a user was trying to access is stored even when a user makes an unsuccessful log in attempt, modify the HttpPost version of the Login method in the ControllersAccountController.cs file to populate the ViewBag.ReturnUrl property with the current returnUrl parameter as follows:

[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Login(LoginViewModel model, string returnUrl)
{
    if (!ModelState.IsValid)
    {
        return View(model);
    }


    // This doesn't count login failures towards account lockout
    // To enable password failures to trigger account lockout, change to shouldLockout: true
    var result = await SignInManager.PasswordSignInAsync(model.Email, model.Password,
        model.RememberMe, shouldLockout: false);
    switch (result)
    {
        case SignInStatus.Success:
            return RedirectToLocal(returnUrl);
        case SignInStatus.LockedOut:
            return View("Lockout");
        case SignInStatus.RequiresVerification:
            return RedirectToAction("SendCode", new { ReturnUrl = returnUrl, RememberMe =
               model.RememberMe });
        case SignInStatus.Failure:
        default:
            ModelState.AddModelError("", "Invalid login attempt.");
            ViewBag.ReturnUrl = returnUrl;
            return View(model);
    }
}

If you try to access one of the pages that requires authorization, for example to create a product, the returnUrl is set and displayed in the browser URL as /Account/Login?ReturnUrl=%2FProducts%2FCreate. The view login.cshtml uses the ViewBag.ReturnUrl value to decide where to redirect the user to. Adding this new line of code ensures that this value is maintained when a user’s login attempt fails.

See the effect of this change by attempting to access the /Products/Create URL anonymously and then, on your first login attempt, enter invalid credentials. On the second attempt, enter [email protected] with the [email protected] password. You will now be redirected to the Products Create page whereas previously you would have been redirected to the home page.

Always Redirecting to the Previous Page after Log In

By default, the redirectUrl variable is set only if the user was redirected to the login page by ASP.NET Identity; it does not get populated if the user manually opens the login page. This means that if a user is viewing a particular page and then chooses to log in, he is redirected to the home page rather than to the page he was on prior to the login page.

To improve this functionality so that users are always redirected to the page they were on prior to the login page, modify the Log In link generated in the ViewsShared\_LoginPartial.cshtml file as highlighted in bold. This link now has the returnURL set as a route value to the current path in the URL by using HttpContext.Current.Request.Url.AbsolutePath.

@using Microsoft.AspNet.Identity
@if (Request.IsAuthenticated)
{
    using (Html.BeginForm("LogOff", "Account", FormMethod.Post, new { id = "logoutForm",
        @class = "navbar-right" }))
    {
    @Html.AntiForgeryToken()


    <ul class="nav navbar-nav navbar-right">
        <li>
            @Html.ActionLink("Hello " + User.Identity.GetUserName() + "!", "Index", "Manage",
                 routeValues: null, htmlAttributes: new { title = "Manage" })
        </li>
        <li><a href="javascript:document.getElementById('logoutForm').submit()">Log
            off</a></li>
    </ul>
    }
}
else
{
    <ul class="nav navbar-nav navbar-right">
        <li>@Html.ActionLink("Register", "Register", "Account", routeValues: null,
            htmlAttributes: new { id = "registerLink" })</li>
        <li>@Html.ActionLink("Log in", "Login", "Account", new { returnUrl =
            HttpContext.Current.Request.Url.AbsolutePath }, htmlAttributes:
            new { id = "loginLink" })</li>
    </ul>
}

Users will now be redirected to the page they were on prior to making a login request. To see this code in action, start the web site without debugging, and click on the Shop by Category link. Then click on the Log In link and log in as [email protected] with the P@ssw0rd1 password. Once you’re logged in, you will be redirected to the Categories Index page rather than the home page.

Always Redirecting to the Previous Page After Registration

One remaining issue is that the registration process redirects the users to the home page once they have registered. To fix this, we need to use a similar process of setting a returnUrl variable in the Register link of the ViewsShared\_LoginPartial.cshtml file as follows:

@using Microsoft.AspNet.Identity
@if (Request.IsAuthenticated)
{
    using (Html.BeginForm("LogOff", "Account", FormMethod.Post, new { id = "logoutForm",
        @class = "navbar-right" }))
    {
    @Html.AntiForgeryToken()


    <ul class="nav navbar-nav navbar-right">
        <li>
            @Html.ActionLink("Hello " + User.Identity.GetUserName() + "!", "Index", "Manage",
                routeValues: null, htmlAttributes: new { title = "Manage" })
        </li>
        <li><a href="javascript:document.getElementById('logoutForm').submit()">Log
            off</a></li>
    </ul>
    }
}
else
{
    <ul class="nav navbar-nav navbar-right">
        <li>@Html.ActionLink("Register", "Register", "Account", new { returnUrl =
            HttpContext.Current.Request.Url.AbsolutePath },htmlAttributes: new { id =
            "registerLink" })</li>
        <li>@Html.ActionLink("Log in", "Login", "Account", new { returnUrl =
            HttpContext.Current.Request.Url.AbsolutePath }, htmlAttributes:
            new { id = "loginLink" })</li>
    </ul>
}

Next modify the GET version of the Register method in the ControllersAccountController.cs file as follows. This sets the returnUrl in the ViewBag in a similar manner to the Login method:

//
// GET: /Account/Register
[AllowAnonymous]
public ActionResult Register(string returnUrl)
{
    ViewBag.ReturnUrl = returnUrl;
    return View();
}

Then modify the HttpPost version of the Register method as follows so that it uses a returnUrl variable:

//
// POST: /Account/Register
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Register(RegisterViewModel model, string returnUrl)
{
    if (ModelState.IsValid)
    {
        var user = new ApplicationUser {
            UserName = model.Email,
            Email = model.Email,
            DateOfBirth = model.DateOfBirth,
            FirstName = model.FirstName,
            LastName = model.LastName,
            Address = model.Address
        };
        var result = await UserManager.CreateAsync(user, model.Password);
        if (result.Succeeded)
        {
            await UserManager.AddToRoleAsync(user.Id, "Users");
            await SignInManager.SignInAsync(user, isPersistent:false, rememberBrowser:false);


            // For more information on how to enable account confirmation and password reset  
               please visit http://go.microsoft.com/fwlink/?LinkID=320771
            // Send an email with this link
            // string code = await UserManager.GenerateEmailConfirmationTokenAsync(user.Id);
            // var callbackUrl = Url.Action("ConfirmEmail", "Account", new { userId = user.Id,
               code = code }, protocol: Request.Url.Scheme);
            // await UserManager.SendEmailAsync(user.Id, "Confirm your account", "Please
                 confirm your account by clicking <a href="" + callbackUrl + "">here</a>");


            // redirect the user back to the page they came from if it was local otherwise send  
            // them to home page
            return RedirectToLocal(returnUrl);
        }
        ViewBag.ReturnUrl = returnUrl;
        AddErrors(result);
    }


    // If we got this far, something failed, redisplay form
    return View(model);
}

Here we’ve passed in a new parameter named returnUrl and then use it to redirect the users to when they registered successfully. We also reset the ViewBag.ReturnUrl value if the registration fails so that it can be passed back into the method by the view again on subsequent attempts.

Next, to support the changes to the Register method, the ViewsAccountRegister.cshtml file needs to be altered to pass the ReturnUrl value into the HttpPost version of the Register method. To enable this change, update the Html.BeginForm method as follows:

@model BabyStore.Models.RegisterViewModel

@{
    ViewBag.Title = "Register";
}


<h2>@ViewBag.Title</h2>

@using (Html.BeginForm("Register", "Account", new { ReturnUrl = ViewBag.ReturnUrl },
    FormMethod.Post))
{
    @Html.AntiForgeryToken()


    <div class="form-horizontal">
        @Html.Partial("_CreateRegisterUserPartial", Model)
        <div class="form-group">
            <div class="col-md-offset-2 col-md-10">
                <input type="submit" value="Create" class="btn btn-default" />
            </div>
        </div>
    </div>
}


<div>
    @Html.ActionLink("Back to List", "Index")
</div>


@section Scripts {
    @Scripts.Render("∼/bundles/jqueryval")
}

Finally, we are going to add some code to deal with the scenario where a user has come from the login page and clicked the Register link. We don’t want to redirect the user to the login page after registering; we want to redirect them to the page they were on prior to the login page.

To deal with this scenario, simply change the link to the register page in the Views/Account/Login.cshtml file as follows:

@Html.ActionLink("Register as a new user", "Register", new { ReturnUrl = ViewBag.ReturnUrl })

These code changes enable users to register from any page in the web site and be redirected back to that page once registered.

To see this code in action, start the web site without debugging and click on the Shop by Category link. Then click on the Register link and register a new user called [email protected] with the P@ssw0rd password. Use the values shown in Figure 7-54.

A419071_1_En_7_Fig54_HTML.jpg
Figure 7-54. Registering the [email protected] user

When you click the Create button, the user will be registered and the web site will redirect you to the Categories Index page rather than to the home page.

Summary

This chapter started by introducing how to work with and manage roles using ASP.NET Identity via ASP.NET MVC. It also covered how to work with the Bootstrap styles to fix some styling issues with the site navigation. You then saw how to create an Admin controller and views to access administrative pages and how to apply authorization to restrict access to the Admin role.

The second half of the chapter covered working with users, including how to work with two database contexts to allow you to store identity data in a separate database and use Code First Migrations on two databases in one project. The chapter also covered how to apply authorization checks to display hyperlinks, and how to use authorization attributes to restrict or allow access to controllers and methods. Finally, the chapter closed by covering how to ensure that the web site always redirects users to the page they were on prior to attempting to log in or register.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset