diff --git a/src/ApplicationCore/Constants/AuthorizationConstants.cs b/src/ApplicationCore/Constants/AuthorizationConstants.cs new file mode 100644 index 0000000..7ec183b --- /dev/null +++ b/src/ApplicationCore/Constants/AuthorizationConstants.cs @@ -0,0 +1,12 @@ +namespace Microsoft.eShopWeb.ApplicationCore.Constants +{ + public class AuthorizationConstants + { + public static class Roles + { + public const string ADMINISTRATORS = "Administrators"; + } + + public const string DEFAULT_PASSWORD = "Pass@word1"; + } +} diff --git a/src/Infrastructure/Identity/AppIdentityDbContextSeed.cs b/src/Infrastructure/Identity/AppIdentityDbContextSeed.cs index cb0a6b3..3664fe1 100644 --- a/src/Infrastructure/Identity/AppIdentityDbContextSeed.cs +++ b/src/Infrastructure/Identity/AppIdentityDbContextSeed.cs @@ -1,14 +1,23 @@ using Microsoft.AspNetCore.Identity; +using Microsoft.eShopWeb.ApplicationCore.Constants; using System.Threading.Tasks; namespace Microsoft.eShopWeb.Infrastructure.Identity { public class AppIdentityDbContextSeed { - public static async Task SeedAsync(UserManager userManager) + public static async Task SeedAsync(UserManager userManager, RoleManager roleManager) { + await roleManager.CreateAsync(new IdentityRole(AuthorizationConstants.Roles.ADMINISTRATORS)); + var defaultUser = new ApplicationUser { UserName = "demouser@microsoft.com", Email = "demouser@microsoft.com" }; - await userManager.CreateAsync(defaultUser, "Pass@word1"); + await userManager.CreateAsync(defaultUser, AuthorizationConstants.DEFAULT_PASSWORD); + + string adminUserName = "admin@microsoft.com"; + var adminUser = new ApplicationUser { UserName = adminUserName, Email = adminUserName }; + await userManager.CreateAsync(adminUser, AuthorizationConstants.DEFAULT_PASSWORD); + adminUser = await userManager.FindByNameAsync(adminUserName); + await userManager.AddToRoleAsync(adminUser, AuthorizationConstants.Roles.ADMINISTRATORS); } } } diff --git a/src/Web/Extensions/CacheHelpers.cs b/src/Web/Extensions/CacheHelpers.cs new file mode 100644 index 0000000..e8d92f4 --- /dev/null +++ b/src/Web/Extensions/CacheHelpers.cs @@ -0,0 +1,25 @@ +using System; + +namespace Microsoft.eShopWeb.Web.Extensions +{ + public static class CacheHelpers + { + public static readonly TimeSpan DefaultCacheDuration = TimeSpan.FromSeconds(30); + private static readonly string _itemsKeyTemplate = "items-{0}-{1}-{2}-{3}"; + + public static string GenerateCatalogItemCacheKey(int pageIndex, int itemsPage, int? brandId, int? typeId) + { + return string.Format(_itemsKeyTemplate, pageIndex, itemsPage, brandId, typeId); + } + + public static string GenerateBrandsCacheKey() + { + return "brands"; + } + + public static string GenerateTypesCacheKey() + { + return "types"; + } + } +} diff --git a/src/Web/Interfaces/ICatalogItemViewModelService.cs b/src/Web/Interfaces/ICatalogItemViewModelService.cs new file mode 100644 index 0000000..00b7f26 --- /dev/null +++ b/src/Web/Interfaces/ICatalogItemViewModelService.cs @@ -0,0 +1,10 @@ +using Microsoft.eShopWeb.Web.ViewModels; +using System.Threading.Tasks; + +namespace Microsoft.eShopWeb.Web.Interfaces +{ + public interface ICatalogItemViewModelService + { + Task UpdateCatalogItem(CatalogItemViewModel viewModel); + } +} diff --git a/src/Web/Pages/Admin/EditCatalogItem.cshtml b/src/Web/Pages/Admin/EditCatalogItem.cshtml new file mode 100644 index 0000000..56b6db4 --- /dev/null +++ b/src/Web/Pages/Admin/EditCatalogItem.cshtml @@ -0,0 +1,38 @@ +@page +@{ + ViewData["Title"] = "Admin - Edit Catalog"; + @model EditCatalogItemModel +} + +
+
+
+ +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ +
+
+
+
+
+ + +@section Scripts { + +} \ No newline at end of file diff --git a/src/Web/Pages/Admin/EditCatalogItem.cshtml.cs b/src/Web/Pages/Admin/EditCatalogItem.cshtml.cs new file mode 100644 index 0000000..0dd4af2 --- /dev/null +++ b/src/Web/Pages/Admin/EditCatalogItem.cshtml.cs @@ -0,0 +1,39 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.eShopWeb.ApplicationCore.Constants; +using Microsoft.eShopWeb.Web.Interfaces; +using Microsoft.eShopWeb.Web.ViewModels; +using System.Threading.Tasks; + +namespace Microsoft.eShopWeb.Web.Pages.Admin +{ + [Authorize(Roles = AuthorizationConstants.Roles.ADMINISTRATORS)] + public class EditCatalogItemModel : PageModel + { + private readonly ICatalogItemViewModelService _catalogItemViewModelService; + + public EditCatalogItemModel(ICatalogItemViewModelService catalogItemViewModelService) + { + _catalogItemViewModelService = catalogItemViewModelService; + } + + [BindProperty] + public CatalogItemViewModel CatalogModel { get; set; } = new CatalogItemViewModel(); + + public async Task OnGet(CatalogItemViewModel catalogModel) + { + CatalogModel = catalogModel; + } + + public async Task OnPostAsync() + { + if (ModelState.IsValid) + { + await _catalogItemViewModelService.UpdateCatalogItem(CatalogModel); + } + + return RedirectToPage("/Admin/Index"); + } + } +} diff --git a/src/Web/Pages/Admin/Index.cshtml b/src/Web/Pages/Admin/Index.cshtml new file mode 100644 index 0000000..83ae4fa --- /dev/null +++ b/src/Web/Pages/Admin/Index.cshtml @@ -0,0 +1,45 @@ +@page +@{ + ViewData["Title"] = "Admin - Catalog"; + @model IndexModel +} +
+
+ +
+
+
+
+
+ + + +
+
+
+
+ @if (Model.CatalogModel.CatalogItems.Any()) + { + + +
+ @foreach (var catalogItem in Model.CatalogModel.CatalogItems) + { +
+ +
+ } +
+ + } + else + { +
+ THERE ARE NO RESULTS THAT MATCH YOUR SEARCH +
+ } +
diff --git a/src/Web/Pages/Admin/Index.cshtml.cs b/src/Web/Pages/Admin/Index.cshtml.cs new file mode 100644 index 0000000..2e172cf --- /dev/null +++ b/src/Web/Pages/Admin/Index.cshtml.cs @@ -0,0 +1,35 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.eShopWeb.ApplicationCore.Constants; +using Microsoft.eShopWeb.Web.Extensions; +using Microsoft.eShopWeb.Web.Services; +using Microsoft.eShopWeb.Web.ViewModels; +using Microsoft.Extensions.Caching.Memory; +using System.Threading.Tasks; + +namespace Microsoft.eShopWeb.Web.Pages.Admin +{ + [Authorize(Roles = AuthorizationConstants.Roles.ADMINISTRATORS)] + public class IndexModel : PageModel + { + private readonly ICatalogViewModelService _catalogViewModelService; + private readonly IMemoryCache _cache; + + public IndexModel(ICatalogViewModelService catalogViewModelService, IMemoryCache cache) + { + _catalogViewModelService = catalogViewModelService; + _cache = cache; + } + + public CatalogIndexViewModel CatalogModel { get; set; } = new CatalogIndexViewModel(); + + public async Task OnGet(CatalogIndexViewModel catalogModel, int? pageId) + { + var cacheKey = CacheHelpers.GenerateCatalogItemCacheKey(pageId.GetValueOrDefault(), Constants.ITEMS_PER_PAGE, catalogModel.BrandFilterApplied, catalogModel.TypesFilterApplied); + + _cache.Remove(cacheKey); + + CatalogModel = await _catalogViewModelService.GetCatalogItems(pageId.GetValueOrDefault(), Constants.ITEMS_PER_PAGE, catalogModel.BrandFilterApplied, catalogModel.TypesFilterApplied); + } + } +} diff --git a/src/Web/Pages/Index.cshtml.cs b/src/Web/Pages/Index.cshtml.cs index 5c52559..8fc4fe7 100644 --- a/src/Web/Pages/Index.cshtml.cs +++ b/src/Web/Pages/Index.cshtml.cs @@ -1,26 +1,24 @@ -using Microsoft.AspNetCore.Mvc.RazorPages; -using Microsoft.eShopWeb.Web.Services; -using Microsoft.eShopWeb.Web.ViewModels; -using System.Threading.Tasks; - -namespace Microsoft.eShopWeb.Web.Pages -{ - public class IndexModel : PageModel - { - private readonly ICatalogViewModelService _catalogViewModelService; - - public IndexModel(ICatalogViewModelService catalogViewModelService) - { - _catalogViewModelService = catalogViewModelService; - } - - public CatalogIndexViewModel CatalogModel { get; set; } = new CatalogIndexViewModel(); - - public async Task OnGet(CatalogIndexViewModel catalogModel, int? pageId) - { - CatalogModel = await _catalogViewModelService.GetCatalogItems(pageId ?? 0, Constants.ITEMS_PER_PAGE, catalogModel.BrandFilterApplied, catalogModel.TypesFilterApplied); - } - - - } -} +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.eShopWeb.Web.Services; +using Microsoft.eShopWeb.Web.ViewModels; +using System.Threading.Tasks; + +namespace Microsoft.eShopWeb.Web.Pages +{ + public class IndexModel : PageModel + { + private readonly ICatalogViewModelService _catalogViewModelService; + + public IndexModel(ICatalogViewModelService catalogViewModelService) + { + _catalogViewModelService = catalogViewModelService; + } + + public CatalogIndexViewModel CatalogModel { get; set; } = new CatalogIndexViewModel(); + + public async Task OnGet(CatalogIndexViewModel catalogModel, int? pageId) + { + CatalogModel = await _catalogViewModelService.GetCatalogItems(pageId ?? 0, Constants.ITEMS_PER_PAGE, catalogModel.BrandFilterApplied, catalogModel.TypesFilterApplied); + } + } +} diff --git a/src/Web/Pages/Shared/_editCatalog.cshtml b/src/Web/Pages/Shared/_editCatalog.cshtml new file mode 100644 index 0000000..f30b877 --- /dev/null +++ b/src/Web/Pages/Shared/_editCatalog.cshtml @@ -0,0 +1,15 @@ +@model CatalogItemViewModel + +
+
+ +
+ @Model.Name +
+ + + + + +
+
diff --git a/src/Web/Program.cs b/src/Web/Program.cs index 0703f42..ed7493a 100644 --- a/src/Web/Program.cs +++ b/src/Web/Program.cs @@ -27,7 +27,8 @@ namespace Microsoft.eShopWeb.Web await CatalogContextSeed.SeedAsync(catalogContext, loggerFactory); var userManager = services.GetRequiredService>(); - await AppIdentityDbContextSeed.SeedAsync(userManager); + var roleManager = services.GetRequiredService>(); + await AppIdentityDbContextSeed.SeedAsync(userManager, roleManager); } catch (Exception ex) { diff --git a/src/Web/Services/CachedCatalogViewModelService.cs b/src/Web/Services/CachedCatalogViewModelService.cs index 0aebeca..86463b1 100644 --- a/src/Web/Services/CachedCatalogViewModelService.cs +++ b/src/Web/Services/CachedCatalogViewModelService.cs @@ -3,7 +3,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.eShopWeb.Web.ViewModels; using Microsoft.Extensions.Caching.Memory; -using System; +using Microsoft.eShopWeb.Web.Extensions; namespace Microsoft.eShopWeb.Web.Services { @@ -11,10 +11,6 @@ namespace Microsoft.eShopWeb.Web.Services { private readonly IMemoryCache _cache; private readonly CatalogViewModelService _catalogViewModelService; - private static readonly string _brandsKey = "brands"; - private static readonly string _typesKey = "types"; - private static readonly string _itemsKeyTemplate = "items-{0}-{1}-{2}-{3}"; - private static readonly TimeSpan _defaultCacheDuration = TimeSpan.FromSeconds(30); public CachedCatalogViewModelService(IMemoryCache cache, CatalogViewModelService catalogViewModelService) @@ -25,28 +21,29 @@ namespace Microsoft.eShopWeb.Web.Services public async Task> GetBrands() { - return await _cache.GetOrCreateAsync(_brandsKey, async entry => + return await _cache.GetOrCreateAsync(CacheHelpers.GenerateBrandsCacheKey(), async entry => { - entry.SlidingExpiration = _defaultCacheDuration; + entry.SlidingExpiration = CacheHelpers.DefaultCacheDuration; return await _catalogViewModelService.GetBrands(); }); } public async Task GetCatalogItems(int pageIndex, int itemsPage, int? brandId, int? typeId) { - string cacheKey = String.Format(_itemsKeyTemplate, pageIndex, itemsPage, brandId, typeId); + var cacheKey = CacheHelpers.GenerateCatalogItemCacheKey(pageIndex, Constants.ITEMS_PER_PAGE, brandId, typeId); + return await _cache.GetOrCreateAsync(cacheKey, async entry => { - entry.SlidingExpiration = _defaultCacheDuration; + entry.SlidingExpiration = CacheHelpers.DefaultCacheDuration; return await _catalogViewModelService.GetCatalogItems(pageIndex, itemsPage, brandId, typeId); }); } public async Task> GetTypes() { - return await _cache.GetOrCreateAsync(_typesKey, async entry => + return await _cache.GetOrCreateAsync(CacheHelpers.GenerateTypesCacheKey(), async entry => { - entry.SlidingExpiration = _defaultCacheDuration; + entry.SlidingExpiration = CacheHelpers.DefaultCacheDuration; return await _catalogViewModelService.GetTypes(); }); } diff --git a/src/Web/Services/CatalogItemViewModelService.cs b/src/Web/Services/CatalogItemViewModelService.cs new file mode 100644 index 0000000..551b2cd --- /dev/null +++ b/src/Web/Services/CatalogItemViewModelService.cs @@ -0,0 +1,31 @@ +using Microsoft.eShopWeb.ApplicationCore.Entities; +using Microsoft.eShopWeb.ApplicationCore.Interfaces; +using Microsoft.eShopWeb.Web.Interfaces; +using Microsoft.eShopWeb.Web.ViewModels; +using System.Threading.Tasks; + +namespace Microsoft.eShopWeb.Web.Services +{ + public class CatalogItemViewModelService : ICatalogItemViewModelService + { + private readonly IAsyncRepository _catalogItemRepository; + + public CatalogItemViewModelService(IAsyncRepository catalogItemRepository) + { + _catalogItemRepository = catalogItemRepository; + } + + public async Task UpdateCatalogItem(CatalogItemViewModel viewModel) + { + //Get existing CatalogItem + var existingCatalogItem = await _catalogItemRepository.GetByIdAsync(viewModel.Id); + + //Build updated CatalogItem + var updatedCatalogItem = existingCatalogItem; + updatedCatalogItem.Name = viewModel.Name; + updatedCatalogItem.Price = viewModel.Price; + + await _catalogItemRepository.UpdateAsync(updatedCatalogItem); + } + } +} diff --git a/src/Web/Startup.cs b/src/Web/Startup.cs index 37077fd..cfcf603 100644 --- a/src/Web/Startup.cs +++ b/src/Web/Startup.cs @@ -92,6 +92,7 @@ namespace Microsoft.eShopWeb.Web services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.Configure(Configuration); services.AddSingleton(new UriComposer(Configuration.Get())); services.AddScoped(typeof(IAppLogger<>), typeof(LoggerAdapter<>)); diff --git a/src/Web/Views/Shared/_LoginPartial.cshtml b/src/Web/Views/Shared/_LoginPartial.cshtml index 5adb1af..4a72226 100644 --- a/src/Web/Views/Shared/_LoginPartial.cshtml +++ b/src/Web/Views/Shared/_LoginPartial.cshtml @@ -8,6 +8,13 @@
+ @if (User.IsInRole("Administrators")) + { + +
Admin
+
+ } @@ -18,8 +25,7 @@ asp-action="MyAccount">
My account
- +
Log Out
diff --git a/src/Web/wwwroot/css/shared/components/identity/identity.css b/src/Web/wwwroot/css/shared/components/identity/identity.css index 38629ad..f319c74 100644 --- a/src/Web/wwwroot/css/shared/components/identity/identity.css +++ b/src/Web/wwwroot/css/shared/components/identity/identity.css @@ -41,7 +41,7 @@ .esh-identity:hover .esh-identity-drop { border: 1px solid #EEEEEE; - height: 10rem; + height: 14rem; transition: height 0.35s; z-index: 10; } diff --git a/tests/FunctionalTests/Web/CustomWebApplicationFactory.cs b/tests/FunctionalTests/Web/CustomWebApplicationFactory.cs index 659abd9..6f17765 100644 --- a/tests/FunctionalTests/Web/CustomWebApplicationFactory.cs +++ b/tests/FunctionalTests/Web/CustomWebApplicationFactory.cs @@ -64,8 +64,8 @@ namespace Microsoft.eShopWeb.FunctionalTests.Web.Controllers // seed sample user data var userManager = scopedServices.GetRequiredService>(); - - AppIdentityDbContextSeed.SeedAsync(userManager).Wait(); + var roleManager = scopedServices.GetRequiredService>(); + AppIdentityDbContextSeed.SeedAsync(userManager, roleManager).Wait(); } catch (Exception ex) { diff --git a/tests/UnitTests/Web/Extensions/CacheHelpersTests/GenerateBrandsCacheKey_Should.cs b/tests/UnitTests/Web/Extensions/CacheHelpersTests/GenerateBrandsCacheKey_Should.cs new file mode 100644 index 0000000..52b03df --- /dev/null +++ b/tests/UnitTests/Web/Extensions/CacheHelpersTests/GenerateBrandsCacheKey_Should.cs @@ -0,0 +1,16 @@ +using Microsoft.eShopWeb.Web.Extensions; +using Xunit; + +namespace Microsoft.eShopWeb.UnitTests.Web.Extensions.CacheHelpersTests +{ + public class GenerateBrandsCacheKey_Should + { + [Fact] + public void ReturnBrandsCacheKey() + { + var result = CacheHelpers.GenerateBrandsCacheKey(); + + Assert.Equal("brands", result); + } + } +} diff --git a/tests/UnitTests/Web/Extensions/CacheHelpersTests/GenerateCatalogItemCacheKey_Should.cs b/tests/UnitTests/Web/Extensions/CacheHelpersTests/GenerateCatalogItemCacheKey_Should.cs new file mode 100644 index 0000000..025b5df --- /dev/null +++ b/tests/UnitTests/Web/Extensions/CacheHelpersTests/GenerateCatalogItemCacheKey_Should.cs @@ -0,0 +1,21 @@ +using Microsoft.eShopWeb.Web; +using Microsoft.eShopWeb.Web.Extensions; +using Xunit; + +namespace Microsoft.eShopWeb.UnitTests.Web.Extensions.CacheHelpersTests +{ + public class GenerateCatalogItemCacheKey_Should + { + [Fact] + public void ReturnCatalogItemCacheKey() + { + var pageIndex = 0; + int? brandId = null; + int? typeId = null; + + var result = CacheHelpers.GenerateCatalogItemCacheKey(pageIndex, Constants.ITEMS_PER_PAGE, brandId, typeId); + + Assert.Equal("items-0-10--", result); + } + } +} diff --git a/tests/UnitTests/Web/Extensions/CacheHelpersTests/GenerateTypesCacheKey_Should.cs b/tests/UnitTests/Web/Extensions/CacheHelpersTests/GenerateTypesCacheKey_Should.cs new file mode 100644 index 0000000..f7cce99 --- /dev/null +++ b/tests/UnitTests/Web/Extensions/CacheHelpersTests/GenerateTypesCacheKey_Should.cs @@ -0,0 +1,16 @@ +using Microsoft.eShopWeb.Web.Extensions; +using Xunit; + +namespace Microsoft.eShopWeb.UnitTests.Web.Extensions.CacheHelpersTests +{ + public class GenerateTypesCacheKey_Should + { + [Fact] + public void ReturnTypesCacheKey() + { + var result = CacheHelpers.GenerateTypesCacheKey(); + + Assert.Equal("types", result); + } + } +}