Add Blazor WebAssembly Admin Page (#426)

* Added Blazor Client
Configured PublicAPI CORS to allow traffic from client

* Make admin page home page; remove extra pages
Add CatalogType list endpoint

* Wired up Types and Brands in the API and the admin list page

* Adding a custom HttpClient to talk securely to API

* Ardalis/blazor (#419)

* Login added

* AuthService will handel http request secure and not secure.

* Logout added

* CatalogBrandService in it is own service

* Get token from localstorage when refresh.

* used GetAsync

* Fixed Login and Logout switch.

* CatalogItemService added

* CatalogTypeService added & Auth for CatalogType.
using not used removed.

* Made BlazorComponent and BlazorLayoutComponent for refresh.
Index now small enough to be in one file.

* Removed the service from program main and use lazy singleton.

* used OnInitialized

* Refactoring and detecting login status in login.razor

* Refactoring login to redirect if user is already logged in

* Blazor login with MVC (#420)

* Blazor login with MVC

* return back the PasswordSignInAsync in Login page

* CRUD added (#422)

* CRUD added

* Unit Test changed to meet new redirect /admin

* CreateCatalogItemRequest added.

* Action caption added.

* Validation added for name and price.

* Updated port of api
Redirect to returnUrl from login

* Add username to /admin; link to my profile

* Working on authorization of /admin

* Working on custom auth locking down /admin page

* Microsoft authorize working.Login.razor removed.Login from SignInMana… (#425)

* Microsoft authorize working.Login.razor removed.Login from SignInManager and create token from it.unit test fixed.

* GetTokenFromController function used in CustomAuthStateProvider

* Cleaned up button styles
Refactored to use codebehind for List component
Updated Not Authorized view

Co-authored-by: Shady Nagy <shadynagi@gmail.com>
This commit is contained in:
Steve Smith
2020-07-24 12:36:47 -04:00
committed by GitHub
parent 4253660bc3
commit 8d3ac693d4
86 changed files with 3268 additions and 82 deletions

View File

@@ -3,8 +3,10 @@ using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
using BlazorAdmin.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
@@ -20,12 +22,16 @@ namespace Microsoft.eShopWeb.Web.Areas.Identity.Pages.Account
private readonly SignInManager<ApplicationUser> _signInManager;
private readonly ILogger<LoginModel> _logger;
private readonly IBasketService _basketService;
private readonly AuthService _authService;
private readonly ITokenClaimsService _tokenClaimsService;
public LoginModel(SignInManager<ApplicationUser> signInManager, ILogger<LoginModel> logger, IBasketService basketService)
public LoginModel(SignInManager<ApplicationUser> signInManager, ILogger<LoginModel> logger, IBasketService basketService, AuthService authService, ITokenClaimsService tokenClaimsService)
{
_signInManager = signInManager;
_logger = logger;
_basketService = basketService;
_authService = authService;
_tokenClaimsService = tokenClaimsService;
}
[BindProperty]
@@ -77,9 +83,13 @@ namespace Microsoft.eShopWeb.Web.Areas.Identity.Pages.Account
{
// This doesn't count login failures towards account lockout
// To enable password failures to trigger account lockout, set lockoutOnFailure: true
var result = await _signInManager.PasswordSignInAsync(Input.Email, Input.Password, Input.RememberMe, lockoutOnFailure: true);
//var result = await _signInManager.PasswordSignInAsync(Input.Email, Input.Password, Input.RememberMe, lockoutOnFailure: true);
var result = await _signInManager.PasswordSignInAsync(Input.Email, Input.Password, false, true);
if (result.Succeeded)
{
var token = await _tokenClaimsService.GetTokenAsync(Input.Email);
CreateAuthCookie(Input.Email, token);
_logger.LogInformation("User logged in.");
await TransferAnonymousBasketToUserAsync(Input.Email);
return LocalRedirect(returnUrl);
@@ -104,6 +114,14 @@ namespace Microsoft.eShopWeb.Web.Areas.Identity.Pages.Account
return Page();
}
private void CreateAuthCookie(string username, string token)
{
var cookieOptions = new CookieOptions();
cookieOptions.Expires = DateTime.Today.AddYears(10);
Response.Cookies.Append("token", token, cookieOptions);
Response.Cookies.Append("username", username, cookieOptions);
}
private async Task TransferAnonymousBasketToUserAsync(string userName)
{
if (Request.Cookies.ContainsKey(Constants.BASKET_COOKIENAME))

View File

@@ -12,7 +12,8 @@ namespace Microsoft.eShopWeb.Web.Configuration
services.Configure<CookiePolicyOptions>(options =>
{
// This lambda determines whether user consent for non-essential cookies is needed for a given request.
options.CheckConsentNeeded = context => true;
//TODO need to check that.
//options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
services.ConfigureApplicationCookie(options =>

View File

@@ -0,0 +1,63 @@
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Shared.Authorization;
namespace Microsoft.eShopWeb.Web.Controllers
{
[Route("[controller]")]
[ApiController]
public class UserController : ControllerBase
{
[HttpGet]
[Authorize]
[AllowAnonymous]
public IActionResult GetCurrentUser() =>
Ok(User.Identity.IsAuthenticated ? CreateUserInfo(User) : UserInfo.Anonymous);
private UserInfo CreateUserInfo(ClaimsPrincipal claimsPrincipal)
{
if (!claimsPrincipal.Identity.IsAuthenticated)
{
return UserInfo.Anonymous;
}
var userInfo = new UserInfo
{
IsAuthenticated = true
};
if (claimsPrincipal.Identity is ClaimsIdentity claimsIdentity)
{
userInfo.NameClaimType = claimsIdentity.NameClaimType;
userInfo.RoleClaimType = claimsIdentity.RoleClaimType;
}
else
{
userInfo.NameClaimType = "name";
userInfo.RoleClaimType = "role";
}
if (claimsPrincipal.Claims.Any())
{
var claims = new List<ClaimValue>();
var nameClaims = claimsPrincipal.FindAll(userInfo.NameClaimType);
foreach (var claim in nameClaims)
{
claims.Add(new ClaimValue(userInfo.NameClaimType, claim.Value));
}
foreach (var claim in claimsPrincipal.Claims.Except(nameClaims))
{
claims.Add(new ClaimValue(claim.Type, claim.Value));
}
userInfo.Claims = claims;
}
return userInfo;
}
}
}

View File

@@ -1,45 +1,67 @@
@page
@{
ViewData["Title"] = "Admin - Catalog";
@model IndexModel
}
<section class="esh-catalog-hero">
<div class="container">
<img class="esh-catalog-title" src="~/images/main_banner_text.png" />
</div>
</section>
<section class="esh-catalog-filters">
<div class="container">
<form method="get">
<label class="esh-catalog-label" data-title="brand">
<select asp-for="@Model.CatalogModel.BrandFilterApplied" asp-items="@Model.CatalogModel.Brands" class="esh-catalog-filter"></select>
</label>
<label class="esh-catalog-label" data-title="type">
<select asp-for="@Model.CatalogModel.TypesFilterApplied" asp-items="@Model.CatalogModel.Types" class="esh-catalog-filter"></select>
</label>
<input class="esh-catalog-send" type="image" src="images/arrow-right.svg" />
</form>
</div>
</section>
<div class="container">
@if (Model.CatalogModel.CatalogItems.Any())
{
<partial name="_pagination" for="CatalogModel.PaginationInfo" />
@using BlazorAdmin
<div class="esh-catalog-items row">
@foreach (var catalogItem in Model.CatalogModel.CatalogItems)
{
<div class="esh-catalog-item col-md-4">
<partial name="_editCatalog" for="@catalogItem" />
</div>
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@{
Layout = null;
ViewData["Title"] = "Admin - Catalog";
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<title>@ViewData["Title"] - Microsoft.eShopOnWeb</title>
<base href="~/" />
<environment include="Development">
<link rel="stylesheet" href="css/bootstrap/bootstrap.min.css" />
</environment>
<environment exclude="Development">
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"
asp-fallback-href="css/bootstrap/bootstrap.min.css"
asp-fallback-test-class="sr-only" asp-fallback-test-property="position" asp-fallback-test-value="absolute"
crossorigin="anonymous"
integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" />
</environment>
<link href="css/admin.css" rel="stylesheet" />
<script>
window.getCookie = (cname) => {
var name = cname + "=";
var decodedCookie = decodeURIComponent(document.cookie);
var ca = decodedCookie.split(';');
for (var i = 0; i < ca.length; i++) {
var c = ca[i];
while (c.charAt(0) == ' ') {
c = c.substring(1);
}
if (c.indexOf(name) == 0) {
return c.substring(name.length, c.length);
}
}
</div>
<partial name="_pagination" for="CatalogModel.PaginationInfo" />
}
else
{
<div class="esh-catalog-items row">
THERE ARE NO RESULTS THAT MATCH YOUR SEARCH
</div>
}
</div>
return "";
};
window.deleteCookie = (cname) => {
document.cookie = cname + "=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
};
window.routeOutside = (path) => {
window.location = path;
};
</script>
</head>
<body>
<admin>@(await Html.RenderComponentAsync<App>(RenderMode.ServerPrerendered))</admin>
<script src="_content/Microsoft.AspNetCore.Components.WebAssembly.Authentication/AuthenticationService.js"></script>
<script src="_framework/blazor.webassembly.js"></script>
</body>
</html>

View File

@@ -12,24 +12,9 @@ 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)
public IndexModel()
{
_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);
}
}
}

View File

@@ -12,7 +12,7 @@ namespace Microsoft.eShopWeb.Web
{
public class Program
{
public async static Task Main(string[] args)
public static async Task Main(string[] args)
{
var host = CreateHostBuilder(args)
.Build();

View File

@@ -11,6 +11,7 @@
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
@@ -18,6 +19,7 @@
"Web - PROD": {
"commandName": "Project",
"launchBrowser": true,
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Production"
},

View File

@@ -8,10 +8,6 @@ using Microsoft.AspNetCore.Mvc.ApplicationModels;
using Microsoft.EntityFrameworkCore;
using Microsoft.eShopWeb.Infrastructure.Data;
using Microsoft.eShopWeb.Infrastructure.Identity;
using Microsoft.eShopWeb.Infrastructure.Logging;
using Microsoft.eShopWeb.Infrastructure.Services;
using Microsoft.eShopWeb.Web.Interfaces;
using Microsoft.eShopWeb.Web.Services;
using Microsoft.eShopWeb.Web.Configuration;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
@@ -20,7 +16,14 @@ using Microsoft.Extensions.Hosting;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Mime;
using BlazorAdmin.Services;
using Blazored.LocalStorage;
using Microsoft.AspNetCore.Components;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.AspNetCore.Authorization;
using Microsoft.eShopWeb.ApplicationCore.Interfaces;
namespace Microsoft.eShopWeb.Web
{
@@ -87,6 +90,8 @@ namespace Microsoft.eShopWeb.Web
.AddEntityFrameworkStores<AppIdentityDbContext>()
.AddDefaultTokenProviders();
services.AddScoped<ITokenClaimsService, IdentityTokenClaimService>();
ConfigureCoreServices.Configure(services, Configuration);
ConfigureWebServices.Configure(services, Configuration);
@@ -104,11 +109,11 @@ namespace Microsoft.eShopWeb.Web
new SlugifyParameterTransformer()));
});
services.AddControllersWithViews();
services.AddRazorPages(options =>
{
options.Conventions.AuthorizePage("/Basket/Checkout");
});
services.AddControllersWithViews();
services.AddHttpContextAccessor();
services.AddHealthChecks();
services.Configure<ServiceConfig>(config =>
@@ -118,6 +123,22 @@ namespace Microsoft.eShopWeb.Web
config.Path = "/allservices";
});
// Blazor Admin Required Services for Prerendering
services.AddScoped<HttpClient>(s =>
{
var navigationManager = s.GetRequiredService<NavigationManager>();
return new HttpClient
{
//TODO need to do it well
BaseAddress = new Uri("https://localhost:44315/")
//BaseAddress = new Uri(navigationManager.BaseUri)
};
});
services.AddBlazoredLocalStorage();
services.AddServerSideBlazor();
services.AddScoped<AuthService>();
_services = services; // used to debug registered services
}
@@ -148,6 +169,7 @@ namespace Microsoft.eShopWeb.Web
app.UseDeveloperExceptionPage();
app.UseShowAllServicesMiddleware();
app.UseDatabaseErrorPage();
app.UseWebAssemblyDebugging();
}
else
{
@@ -156,10 +178,11 @@ namespace Microsoft.eShopWeb.Web
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseBlazorFrameworkFiles();
app.UseStaticFiles();
app.UseRouting();
app.UseHttpsRedirection();
app.UseCookiePolicy();
app.UseAuthentication();
app.UseAuthorization();
@@ -170,7 +193,10 @@ namespace Microsoft.eShopWeb.Web
endpoints.MapRazorPages();
endpoints.MapHealthChecks("home_page_health_check");
endpoints.MapHealthChecks("api_health_check");
//endpoints.MapBlazorHub("/admin");
endpoints.MapFallbackToFile("index.html");
});
}
}
}

View File

@@ -28,6 +28,7 @@
<PackageReference Include="MediatR" Version="8.0.2" />
<PackageReference Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="8.0.1" />
<PackageReference Include="BuildBundlerMinifier" Version="2.9.406" Condition="'$(Configuration)'=='Release'" PrivateAssets="All" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="3.2.0" />
<PackageReference Include="Microsoft.CodeAnalysis" Version="3.6.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="3.1.5" />
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="3.1.3" />
@@ -49,7 +50,9 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ApplicationCore\ApplicationCore.csproj" />
<ProjectReference Include="..\BlazorAdmin\BlazorAdmin.csproj" />
<ProjectReference Include="..\Infrastructure\Infrastructure.csproj" />
<ProjectReference Include="..\Shared\Shared.csproj" />
</ItemGroup>
<ItemGroup>
<None Include="compilerconfig.json" />