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:
@@ -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))
|
||||
|
||||
@@ -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 =>
|
||||
|
||||
63
src/Web/Controllers/UserController.cs
Normal file
63
src/Web/Controllers/UserController.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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" />
|
||||
|
||||
Reference in New Issue
Block a user