Art website – site management

The primary reason to build this website, was to give my wife a way to maintain her own site, add new images of her paintings and drawings, add or modify meta data like name, year of creation, dimensions etc.

Creating management pages

Luckily, creating management pages for a data model is very easy in MVC because of its Razor templates. I started of with creating a new controller, called PaintingsController, which I gave an action called Manage. I generated the associated view from a strongly typed list template using Painting as the model. It displayed all the public properties of the Painting class in a grid with Edit, Delete, Move up and Move down action links next to each painting. Then, I implemented the Add action on the PaintingsController. It consists of two methods. The first one is invoked on a GET request and displays the view in which the user can enter data about the new painting in a HTML form. It also includes a browse button to allow the selection of the image file to upload.

   1: public class PaintingsController : Controller
   2: {
   3:     private IArtCollection _artCollection;
   4:  
   5:     public PaintingsController(IArtCollection artcollection)
   6:     {
   7:         _artCollection = artcollection;
   8:     }
   9:  
  10:     [HttpGet]
  11:     public ActionResult Manage()
  12:     {
  13:         var paintings = _artCollection.Paintings;
  14:         return View(paintings);
  15:     }
  16:  
  17:     [HttpGet]
  18:     public ActionResult Add()
  19:     {
  20:         return View();
  21:     }
  22:  
  23:     [HttpPost]
  24:     public ActionResult Add(Painting painting)
  25:     { 
  26:         foreach (string file in Request.Files)
  27:         {
  28:             var postedFile = Request.Files[file] as HttpPostedFileBase;
  29:  
  30:             if (postedFile.ContentLength == 0)
  31:                 continue;
  32:  
  33:             string filename = Path.GetFileName(postedFile.FileName);
  34:             string imagesFolder = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "images");
  35:             string savedFileName = Path.Combine(imagesFolder, filename);
  36:             postedFile.SaveAs(savedFileName);
  37:  
  38:             painting.ImageUrl = "/images/" + filename;
  39:         }
  40:  
  41:         _artCollection.AddPainting(painting);
  42:         return RedirectToAction("Manage", "Paintings");
  43:     }
  44:  
  45:     [HttpGet]
  46:     public ActionResult Edit(int id)
  47:     {
  48:         var painting = (from p in _artCollection.Paintings
  49:                         where p.Id == id
  50:                         select p).First();
  51:         return View(painting);
  52:     }
  53:  
  54:     [HttpPost]
  55:     public ActionResult Edit(Painting painting)
  56:     {
  57:         if (ModelState.IsValid)
  58:         {
  59:             _artCollection.DeletePainting(painting.Id);
  60:             _artCollection.AddPainting(painting);
  61:         }
  62:         return RedirectToAction("Manage", "Paintings");
  63:     }
  64:  
  65:     [HttpGet]
  66:     public ActionResult Delete(int id)
  67:     {
  68:         var painting = (from p in _artCollection.Paintings
  69:                         where p.Id == id
  70:                         select p).First();
  71:         
  72:         return View(painting);
  73:     }
  74:  
  75:     [HttpPost]
  76:     public ActionResult Delete(int id, Painting painting)
  77:     {
  78:         _artCollection.DeletePainting(id);
  79:         return RedirectToAction("Manage", "Paintings");
  80:     }
  81:  
  82:     public ActionResult MoveUp(int id)
  83:     {
  84:         _artCollection.PromotePainting(id);
  85:  
  86:         return RedirectToAction("Manage", "Paintings");
  87:     }
  88:  
  89:     public ActionResult MoveDown(int id)
  90:     {
  91:         _artCollection.DemotePainting(id);
  92:  
  93:         return RedirectToAction("Manage", "Paintings");
  94:     }
  95: }

The second method is invoked on the POST request when the form is submitted. It reads the posted file from the POST request and saves it in a local folder. It also creates a thumbnail image and adds the new painting to the art collection XML file. The same controller has methods for editing and deleting paintings, and for moving them up or down in the display order.

   1: @model IEnumerable<Website.Models.Painting>
   2: @{
   3:     ViewBag.Title = "Manage";
   4: }
   5: <h2>Manage</h2>
   6: <p>
   7:     @Html.ActionLink("New Painting", "Add")
   8: </p>
   9: <table>
  10:     <tr>
  11:         <th>
  12:             @Html.DisplayNameFor(model => model.Name)
  13:         </th>
  14:         <th>
  15:             @Html.DisplayNameFor(model => model.Year)
  16:         </th>
  17:         <th>
  18:             @Html.DisplayNameFor(model => model.ImageUrl)
  19:         </th>
  20:         <th>
  21:             @Html.DisplayNameFor(model => model.ThumbUrl)
  22:         </th>
  23:         <th></th>
  24:     </tr>
  25:  
  26: @foreach (var item in Model) {
  27:     <tr>
  28:         <td>
  29:             @Html.DisplayFor(modelItem => item.Name)
  30:         </td>
  31:         <td>
  32:             @Html.DisplayFor(modelItem => item.Year)
  33:         </td>
  34:         <td>
  35:             @Html.DisplayFor(modelItem => item.ImageUrl)
  36:         </td>
  37:         <td>
  38:             @Html.DisplayFor(modelItem => item.ThumbUrl)
  39:         </td>
  40:         <td>
  41:             @Html.ActionLink("Edit", "Edit", new { id=item.Id }) |
  42:             @Html.ActionLink("Delete", "Delete", new { id=item.Id })
  43:             @Html.ActionLink("Up", "MoveUp", new { id=item.Id })
  44:             @Html.ActionLink("Down", "MoveDown", new { id=item.Id })
  45:         </td>
  46:     </tr>
  47: }
  48: </table>

Authorization

Now, obviously I did not want to allow just anyone to add or remove images from the website. Therefore I needed to protect the management pages from being accessed by anonymous users. In MVC, all that’s needed to to protect an action is the [Authorize] attribute decorating the method. Now, if the user is not logged in, MVC will return a 401 Unauthorized HTTP status. Putting the following authentication element in the web.config file will make MVC redirect the browser automatically to the Login action of the AccountController.

   1: <system.web>
   2:   <authentication mode="Forms">
   3:     <forms loginUrl="~/Account/Login" timeout="2880" />
   4:   </authentication>

So, now I needed a AccountController  with a Login action that redirects to Google to authenticate the user. Google allows 3rd party websites to authenticate users using their Google account. It uses the OpenId protocol to issue a claims token. As my wife has a Gmail email account, this would be a great option for her. When she clicks the manage icon, her browser is redirected to Google and if she’s not currently logged in, she’s asked for her credentials. If she is already logged in to her Google account, she’ll be presented (only the first time if the checkbox is checked) with the following dialog. After clicking ‘Allow’, she’s redirected back to her website and can now add or delete pictures of her art.

image

Next time she clicks the icon, she’ll only see some redirection happening in the address bar of the browser and she’ll be taken straight to the management pages of her website.

A good overview of integrating OpenId authentication providers into an MVC 4 website can be found here. However, the solution given there is much more than I needed. For this website, I only needed to authenticate a few users with a fixed provider: Google.

I started by using the NuGet package manager to install the DotNetOpenAuth library. I created a new controller class ‘AccountController’ and injected the IMembershipService and IAuthorizedUserManager interfaces. The latter interface abstracts which users have access to the management pages. It has only a single method called IsUserAllowed which takes an email address and the implementation of this interface reads the users from a text file with authorized users.

   1: public interface IAuthorizedUserManager
   2: {
   3:     bool IsAllowedUser(string email);
   4: }

The IMembershipService abstracts the interaction with the Google authentication provider.

   1: public interface IMembershipService
   2: {
   3:     IAuthenticationRequest CreateAuthenticationRequest(string identifier);
   4:     IUser GetUser();
   5:     HttpCookie CreateFormsAuthenticationCookie(IUser user);
   6: }

It has three methods. The first one creates the HTTP request to Google and is used from the Login action on the AccountController that is invoked on a POST HTTP request.

   1: [AllowAnonymous]
   2: [HttpPost]
   3: public ActionResult Login(string openid_identifier)
   4: {
   5:     var response = _service.CreateAuthenticationRequest(openid_identifier);
   6:  
   7:     if (response != null)
   8:     {
   9:         return response.RedirectingResponse.AsActionResult();
  10:     }
  11:  
  12:     return View();
  13: }

It redirects the browser to the Google authentication provider, where the user is prompted to login if not already authenticated. The authentication provider will redirect back to the /Account/Login action after the user logged on and will invoke the method responding to the GET request.

   1: [AllowAnonymous]
   2: public ActionResult Login()
   3: {
   4:     var user = _service.GetUser();
   5:  
   6:     if (user != null)
   7:     {
   8:         if (user.IsSignedByProvider && _authorizedUserManager.IsAllowedUser(user.Email))
   9:         {
  10:             var cookie = _service.CreateFormsAuthenticationCookie(user);
  11:             HttpContext.Response.Cookies.Add(cookie);
  12:  
  13:             return new RedirectResult(Request.Params["ReturnUrl"] ?? "/");
  14:         }
  15:     }
  16:  
  17:     // if user is not allowed to login, redirect back to home page
  18:     return new RedirectResult("/");
  19: }

This method will ask the service to retrieve the user. It does this by decoding the response from the provider and constructing an user object containing the claims sent by the provider. The Login method will then check if the email claim is in the list of authorized users and if it is, set an authentication cookie that will keep the user logged in for subsequent requests. After the cookie has been set, the browser is redirected back to the page that triggered the login process in the first place. But this time the authentication cookie is set and the request is routed to the authorized action on the controller.

The AccountController also has a LogOff action that calls the IMembershipService implementation SignOut method which just clears the authentication cookie.

Leave a comment