Enabling admin pages in MusicStore.Spa:

- Updated attribute routing so it works now
- Created a Pages folder and PageController for serving pages, I don't
  like views very much
- Worked around an EF issue
- Fixed ApiResult to use JsonResult.ExecuteResultAsync
- Made PagedList take the sort expression so it can be conditionally applied as calling Count on the query passed causes issues if it contains an OrderBy expression
- Made web server ports not conflict with non-SPA MusicStore
This commit is contained in:
DamianEdwards 2014-10-06 10:04:56 -07:00
parent e50cb5262a
commit 7055949e7b
12 changed files with 98 additions and 67 deletions

View File

@ -1,5 +1,4 @@
using System.Linq; using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNet.Mvc; using Microsoft.AspNet.Mvc;
using MusicStore.Infrastructure; using MusicStore.Infrastructure;
@ -23,8 +22,7 @@ namespace MusicStore.Apis
var albums = await _storeContext.Albums var albums = await _storeContext.Albums
//.Include(a => a.Genre) //.Include(a => a.Genre)
//.Include(a => a.Artist) //.Include(a => a.Artist)
.SortBy(sortBy, a => a.Title) .ToPagedListAsync(page, pageSize, sortBy, a => a.Title);
.ToPagedListAsync(page, pageSize);
return Json(albums); return Json(albums);
} }
@ -72,8 +70,7 @@ namespace MusicStore.Apis
} }
[HttpPost] [HttpPost]
//[Authorize(Roles = "Administrator")] [Authorize("ManageStore", "Allowed")]
[Authorize(ClaimTypes.Role, "Administrator")]
public async Task<ActionResult> CreateAlbum() public async Task<ActionResult> CreateAlbum()
{ {
var album = new Album(); var album = new Album();
@ -99,8 +96,7 @@ namespace MusicStore.Apis
} }
[HttpPut("{albumId:int}/update")] [HttpPut("{albumId:int}/update")]
//[Authorize(Roles = "Administrator")] [Authorize("ManageStore", "Allowed")]
[Authorize(ClaimTypes.Role, "Administrator")]
public async Task<ActionResult> UpdateAlbum(int albumId) public async Task<ActionResult> UpdateAlbum(int albumId)
{ {
var album = _storeContext.Albums.SingleOrDefault(a => a.AlbumId == albumId); var album = _storeContext.Albums.SingleOrDefault(a => a.AlbumId == albumId);
@ -133,11 +129,11 @@ namespace MusicStore.Apis
} }
[HttpDelete("{albumId:int}")] [HttpDelete("{albumId:int}")]
//[Authorize(Roles = "Administrator")] [Authorize("ManageStore", "Allowed")]
[Authorize(ClaimTypes.Role, "Administrator")]
public async Task<ActionResult> DeleteAlbum(int albumId) public async Task<ActionResult> DeleteAlbum(int albumId)
{ {
var album = await _storeContext.Albums.SingleOrDefaultAsync(a => a.AlbumId == albumId); //var album = await _storeContext.Albums.SingleOrDefaultAsync(a => a.AlbumId == albumId);
var album = _storeContext.Albums.SingleOrDefault(a => a.AlbumId == albumId);
if (album != null) if (album != null)
{ {

View File

@ -7,6 +7,7 @@ using MusicStore.Models;
namespace MusicStore.Controllers namespace MusicStore.Controllers
{ {
[Route("account")]
[Authorize] [Authorize]
public class AccountController : Controller public class AccountController : Controller
{ {
@ -20,20 +21,16 @@ namespace MusicStore.Controllers
public SignInManager<ApplicationUser> SignInManager { get; private set; } public SignInManager<ApplicationUser> SignInManager { get; private set; }
//
// GET: /Account/Login
[AllowAnonymous] [AllowAnonymous]
//Bug: https://github.com/aspnet/WebFx/issues/339 //Bug: https://github.com/aspnet/WebFx/issues/339
[HttpGet] [HttpGet("login")]
public IActionResult Login(string returnUrl) public IActionResult Login(string returnUrl)
{ {
ViewBag.ReturnUrl = returnUrl; ViewBag.ReturnUrl = returnUrl;
return View(); return View();
} }
// [HttpPost("login")]
// POST: /Account/Login
[HttpPost]
[AllowAnonymous] [AllowAnonymous]
[ValidateAntiForgeryToken] [ValidateAntiForgeryToken]
public async Task<IActionResult> Login(LoginViewModel model, string returnUrl) public async Task<IActionResult> Login(LoginViewModel model, string returnUrl)
@ -59,19 +56,15 @@ namespace MusicStore.Controllers
return View(model); return View(model);
} }
//
// GET: /Account/Register
[AllowAnonymous] [AllowAnonymous]
//Bug: https://github.com/aspnet/WebFx/issues/339 //Bug: https://github.com/aspnet/WebFx/issues/339
[HttpGet] [HttpGet("register")]
public IActionResult Register() public IActionResult Register()
{ {
return View(); return View();
} }
// [HttpPost("register")]
// POST: /Account/Register
[HttpPost]
[AllowAnonymous] [AllowAnonymous]
[ValidateAntiForgeryToken] [ValidateAntiForgeryToken]
public async Task<IActionResult> Register(RegisterViewModel model) public async Task<IActionResult> Register(RegisterViewModel model)
@ -84,7 +77,7 @@ namespace MusicStore.Controllers
if (result.Succeeded) if (result.Succeeded)
{ {
await SignInManager.SignInAsync(user, isPersistent: false); await SignInManager.SignInAsync(user, isPersistent: false);
return RedirectToAction("Index", "Home"); return RedirectToAction("Home", "Page");
} }
else else
{ {
@ -96,9 +89,7 @@ namespace MusicStore.Controllers
return View(model); return View(model);
} }
// [HttpGet("manage")]
// GET: /Account/Manage
[HttpGet]
public IActionResult Manage(ManageMessageId? message = null) public IActionResult Manage(ManageMessageId? message = null)
{ {
ViewBag.StatusMessage = ViewBag.StatusMessage =
@ -109,9 +100,7 @@ namespace MusicStore.Controllers
return View(); return View();
} }
// [HttpPost("manage")]
// POST: /Account/Manage
[HttpPost]
[ValidateAntiForgeryToken] [ValidateAntiForgeryToken]
public async Task<IActionResult> Manage(ManageUserViewModel model) public async Task<IActionResult> Manage(ManageUserViewModel model)
{ {
@ -135,14 +124,12 @@ namespace MusicStore.Controllers
return View(model); return View(model);
} }
// [HttpPost("logoff")]
// POST: /Account/LogOff
[HttpPost]
[ValidateAntiForgeryToken] [ValidateAntiForgeryToken]
public IActionResult LogOff() public IActionResult LogOff()
{ {
SignInManager.SignOut(); SignInManager.SignOut();
return RedirectToAction("Index", "Home"); return RedirectToAction("Home", "Page");
} }
#region Helpers #region Helpers

View File

@ -1,12 +0,0 @@
using Microsoft.AspNet.Mvc;
namespace MusicStore.Controllers
{
public class HomeController : Controller
{
public IActionResult Index()
{
return View();
}
}
}

View File

@ -0,0 +1,22 @@
using System.Security.Claims;
using Microsoft.AspNet.Mvc;
namespace MusicStore.Spa.Controllers
{
[Route("/")]
public class PageController : Controller
{
[HttpGet]
public IActionResult Home()
{
return View("/Pages/Home.cshtml");
}
[HttpGet("admin")]
[Authorize("ManageStore", "Allowed")]
public IActionResult Admin()
{
return View("/Pages/Admin.cshtml");
}
}
}

View File

@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNet.Mvc.ModelBinding; using Microsoft.AspNet.Mvc.ModelBinding;
using Newtonsoft.Json; using Newtonsoft.Json;
@ -40,7 +41,7 @@ namespace Microsoft.AspNet.Mvc
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)] [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public IEnumerable<ModelError> ModelErrors { get; set; } public IEnumerable<ModelError> ModelErrors { get; set; }
public override void ExecuteResult(ActionContext context) public override Task ExecuteResultAsync(ActionContext context)
{ {
if (StatusCode.HasValue) if (StatusCode.HasValue)
{ {
@ -48,7 +49,7 @@ namespace Microsoft.AspNet.Mvc
} }
var json = new JsonResult(this); var json = new JsonResult(this);
json.ExecuteResult(context); return json.ExecuteResultAsync(context);
} }
public class ModelError public class ModelError

View File

@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Linq.Expressions;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace MusicStore.Infrastructure namespace MusicStore.Infrastructure
@ -64,7 +65,8 @@ namespace MusicStore.Infrastructure
return new PagedList<T>(data, pagingConfig.Page, pagingConfig.PageSize, query.Count()); return new PagedList<T>(data, pagingConfig.Page, pagingConfig.PageSize, query.Count());
} }
public static async Task<IPagedList<T>> ToPagedListAsync<T>(this IQueryable<T> query, int page, int pageSize) public static async Task<IPagedList<TModel>> ToPagedListAsync<TModel, TProperty>(this IQueryable<TModel> query, int page, int pageSize, string sortExpression, Expression<Func<TModel, TProperty>> defaultSortExpression, SortDirection defaultSortDirection = SortDirection.Ascending)
where TModel : class
{ {
if (query == null) if (query == null)
{ {
@ -73,8 +75,15 @@ namespace MusicStore.Infrastructure
var pagingConfig = new PagingConfig(page, pageSize); var pagingConfig = new PagingConfig(page, pageSize);
var skipCount = ValidatePagePropertiesAndGetSkipCount(pagingConfig); var skipCount = ValidatePagePropertiesAndGetSkipCount(pagingConfig);
var dataQuery = query;
var data = await query if (defaultSortExpression != null)
{
dataQuery = dataQuery
.SortBy(sortExpression, defaultSortExpression);
}
var data = await dataQuery
.Skip(skipCount) .Skip(skipCount)
.Take(pagingConfig.PageSize) .Take(pagingConfig.PageSize)
.ToListAsync(); .ToListAsync();
@ -83,12 +92,14 @@ namespace MusicStore.Infrastructure
{ {
// Requested page has no records, just return the first page // Requested page has no records, just return the first page
pagingConfig.Page = 1; pagingConfig.Page = 1;
data = await query data = await dataQuery
.Take(pagingConfig.PageSize) .Take(pagingConfig.PageSize)
.ToListAsync(); .ToListAsync();
} }
return new PagedList<T>(data, pagingConfig.Page, pagingConfig.PageSize, await query.CountAsync()); var count = await query.CountAsync();
return new PagedList<TModel>(data, pagingConfig.Page, pagingConfig.PageSize, count);
} }
private static int ValidatePagePropertiesAndGetSkipCount(PagingConfig pagingConfig) private static int ValidatePagePropertiesAndGetSkipCount(PagingConfig pagingConfig)

View File

@ -56,22 +56,22 @@ namespace MusicStore.Models
private static async Task CreateAdminUser(IServiceProvider serviceProvider) private static async Task CreateAdminUser(IServiceProvider serviceProvider)
{ {
var options = serviceProvider.GetService<IOptionsAccessor<IdentityDbContextOptions>>().Options; var options = serviceProvider.GetService<IOptionsAccessor<IdentityDbContextOptions>>().Options;
//const string adminRole = "Administrator"; const string adminRole = "Administrator";
var userManager = serviceProvider.GetService<UserManager<ApplicationUser>>(); var userManager = serviceProvider.GetService<UserManager<ApplicationUser>>();
// TODO: Identity SQL does not support roles yet var roleManager = serviceProvider.GetService<RoleManager<IdentityRole>>();
//var roleManager = serviceProvider.GetService<ApplicationRoleManager>();
//if (!await roleManager.RoleExistsAsync(adminRole)) if (!await roleManager.RoleExistsAsync(adminRole))
//{ {
// await roleManager.CreateAsync(new IdentityRole(adminRole)); await roleManager.CreateAsync(new IdentityRole(adminRole));
//} }
var user = await userManager.FindByNameAsync(options.DefaultAdminUserName); var user = await userManager.FindByNameAsync(options.DefaultAdminUserName);
if (user == null) if (user == null)
{ {
user = new ApplicationUser { UserName = options.DefaultAdminUserName }; user = new ApplicationUser { UserName = options.DefaultAdminUserName };
await userManager.CreateAsync(user, options.DefaultAdminPassword); await userManager.CreateAsync(user, options.DefaultAdminPassword);
//await userManager.AddToRoleAsync(user, adminRole); await userManager.AddToRoleAsync(user, adminRole);
await userManager.AddClaimAsync(user, new Claim("ManageStore", "Allowed")); await userManager.AddClaimAsync(user, new Claim("ManageStore", "Allowed"));
} }
} }

View File

@ -15,7 +15,7 @@
</PropertyGroup> </PropertyGroup>
<PropertyGroup> <PropertyGroup>
<SchemaVersion>2.0</SchemaVersion> <SchemaVersion>2.0</SchemaVersion>
<DevelopmentServerPort>5001</DevelopmentServerPort> <DevelopmentServerPort>5101</DevelopmentServerPort>
</PropertyGroup> </PropertyGroup>
<Import Project="$(VSToolsPath)\AspNet\Microsoft.Web.AspNet.targets" Condition="'$(VSToolsPath)' != ''" /> <Import Project="$(VSToolsPath)\AspNet\Microsoft.Web.AspNet.targets" Condition="'$(VSToolsPath)' != ''" />
<ProjectExtensions> <ProjectExtensions>

View File

@ -0,0 +1,26 @@
@model IEnumerable<MusicStore.Models.Album>
@{
ViewBag.Title = "Store Manager";
ViewBag.ngApp = "MusicStore.Admin";
Layout = "/Views/Shared/_Layout.cshtml";
}
<h1>Store Manager</h1>
<div class="ng-view"></div>
@*@Html.InlineData("Lookup", "ArtistsApi")*@
@*@Html.InlineData("Lookup", "GenresApi")*@
@section Scripts {
<script src="~/js/angular.js"></script>
<script src="~/js/angular-route.js"></script>
<script src="~/js/ui-bootstrap.js"></script>
<script src="~/js/ui-bootstrap-tpls.js"></script>
@* TODO: This is currently all the compiled TypeScript, non-minified. Need to explore options
for alternate loading schemes, e.g. AMD loader of individual modules, min vs. non-min, etc. *@
<script src="~/js/@(ViewBag.ngApp).js"></script>
}

View File

@ -17,11 +17,11 @@
<span class="icon-bar"></span> <span class="icon-bar"></span>
<span class="icon-bar"></span> <span class="icon-bar"></span>
</button> </button>
@Html.ActionLink("ASP.NET MVC Music Store", "Index", "Home", null, new { @class = "navbar-brand" }) @Html.ActionLink("ASP.NET MVC Music Store", "Home", "Page", null, new { @class = "navbar-brand" })
</div> </div>
<div class="collapse navbar-collapse"> <div class="collapse navbar-collapse">
<ul class="nav navbar-nav"> <ul class="nav navbar-nav">
<li>@Html.ActionLink("Home", "Index", "Home")</li> <li>@Html.ActionLink("Home", "Home", "Page")</li>
@RenderSection("NavBarItems", required: false) @RenderSection("NavBarItems", required: false)
@ -35,8 +35,8 @@
@RenderBody() @RenderBody()
<hr /> <hr />
<footer class="navbar navbar-fixed-bottom navbar-default text-center"> <footer class="navbar navbar-fixed-bottom navbar-default text-center">
<p><a href="http://mvcmusicstore.codeplex.com">mvcmusicstore.codeplex.com</a></p> <p><a href="https://github.com/aspnet/musicstore">github.com/aspnet/musicstore</a></p>
<small>@Html.ActionLink("admin", "Index", "StoreManager")</small> <small>@Html.ActionLink("admin", "Admin", "Page")</small>
</footer> </footer>
</div> </div>

View File

@ -21,9 +21,9 @@
"Microsoft.Framework.ConfigurationModel.Json": "1.0.0-*" "Microsoft.Framework.ConfigurationModel.Json": "1.0.0-*"
}, },
"commands": { "commands": {
"WebListener": "Microsoft.AspNet.Hosting --server Microsoft.AspNet.Server.WebListener --server.urls http://localhost:5002", "WebListener": "Microsoft.AspNet.Hosting --server Microsoft.AspNet.Server.WebListener --server.urls http://localhost:5102",
"Kestrel": "Microsoft.AspNet.Hosting --server Kestrel --server.urls http://localhost:5004", "Kestrel": "Microsoft.AspNet.Hosting --server Kestrel --server.urls http://localhost:5104",
"run": "run server.urls=http://localhost:5003" "run": "run server.urls=http://localhost:5103"
}, },
"frameworks": { "frameworks": {
"aspnet50": { }, "aspnet50": { },