Skip to content

Commit

Permalink
Add Blazor WebAssembly Admin Page (dotnet-architecture#426)
Browse files Browse the repository at this point in the history
* 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 (dotnet-architecture#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 (dotnet-architecture#420)

* Blazor login with MVC

* return back the PasswordSignInAsync in Login page

* CRUD added (dotnet-architecture#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… (dotnet-architecture#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>
  • Loading branch information
ardalis and ShadyNagy authored Jul 24, 2020
1 parent 4253660 commit 8d3ac69
Show file tree
Hide file tree
Showing 86 changed files with 3,266 additions and 80 deletions.
16 changes: 15 additions & 1 deletion eShopOnWeb.sln
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,11 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
EndProject
Project("{E53339B2-1760-4266-BCC7-CA923CBCF16C}") = "docker-compose", "docker-compose.dcproj", "{1FCBE191-34FE-4B2E-8915-CA81553958AD}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PublicApi", "src\PublicApi\PublicApi.csproj", "{B5E4F33C-4667-4A55-AF6A-740F84C4CF3A}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PublicApi", "src\PublicApi\PublicApi.csproj", "{B5E4F33C-4667-4A55-AF6A-740F84C4CF3A}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazorAdmin", "src\BlazorAdmin\BlazorAdmin.csproj", "{71368733-80A4-4869-B215-3A7001878577}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Shared", "src\Shared\Shared.csproj", "{7BDB419E-FAC1-4D43-8AA9-FB61EBE31BB8}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Expand Down Expand Up @@ -70,6 +74,14 @@ Global
{B5E4F33C-4667-4A55-AF6A-740F84C4CF3A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B5E4F33C-4667-4A55-AF6A-740F84C4CF3A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B5E4F33C-4667-4A55-AF6A-740F84C4CF3A}.Release|Any CPU.Build.0 = Release|Any CPU
{71368733-80A4-4869-B215-3A7001878577}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{71368733-80A4-4869-B215-3A7001878577}.Debug|Any CPU.Build.0 = Debug|Any CPU
{71368733-80A4-4869-B215-3A7001878577}.Release|Any CPU.ActiveCfg = Release|Any CPU
{71368733-80A4-4869-B215-3A7001878577}.Release|Any CPU.Build.0 = Release|Any CPU
{7BDB419E-FAC1-4D43-8AA9-FB61EBE31BB8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7BDB419E-FAC1-4D43-8AA9-FB61EBE31BB8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7BDB419E-FAC1-4D43-8AA9-FB61EBE31BB8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7BDB419E-FAC1-4D43-8AA9-FB61EBE31BB8}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -82,6 +94,8 @@ Global
{0F576306-7E2D-49B7-87B1-EB5D94CFD5FC} = {15EA4737-125B-4E6E-A806-E13B7EBCDCCF}
{7EFB5482-F942-4C3D-94B0-9B70596E6D0A} = {15EA4737-125B-4E6E-A806-E13B7EBCDCCF}
{B5E4F33C-4667-4A55-AF6A-740F84C4CF3A} = {419A6ACE-0419-4315-A6FB-B0E63D39432E}
{71368733-80A4-4869-B215-3A7001878577} = {419A6ACE-0419-4315-A6FB-B0E63D39432E}
{7BDB419E-FAC1-4D43-8AA9-FB61EBE31BB8} = {419A6ACE-0419-4315-A6FB-B0E63D39432E}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {49813262-5DA3-4D61-ABD3-493C74CE8C2B}
Expand Down
30 changes: 30 additions & 0 deletions src/BlazorAdmin/App.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<CascadingAuthenticationState>
<Router AppAssembly="@typeof(Program).Assembly">
<Found Context="routeData">
<AuthorizeRouteView RouteData="@routeData"
DefaultLayout="@typeof(MainLayout)">
<NotAuthorized>
@if (!context.User.Identity.IsAuthenticated)
{
<RedirectToLogin />
}
else
{
<h2>Not Authorized</h2>
<p>
You are not authorized to access
this resource.

<a href="/">Return to eShop</a>
</p>
}
</NotAuthorized>
</AuthorizeRouteView>
</Found>
<NotFound>
<LayoutView Layout="@typeof(MainLayout)">
<p>Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>
</CascadingAuthenticationState>
37 changes: 37 additions & 0 deletions src/BlazorAdmin/BlazorAdmin.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
<RazorLangVersion>3.0</RazorLangVersion>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Blazored.LocalStorage" Version="2.1.6" />
<PackageReference Include="Microsoft.AspNetCore.Components.Authorization" Version="3.1.5" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="3.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="3.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Build" Version="3.2.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="3.2.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.Extensions.Identity.Core" Version="3.1.5" />
<PackageReference Include="Newtonsoft.Json" Version="9.0.1" />
<PackageReference Include="System.Net.Http.Json" Version="3.2.0" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\ApplicationCore\ApplicationCore.csproj" />
<ProjectReference Include="..\Shared\Shared.csproj" />
</ItemGroup>

<ItemGroup>
<Compile Update="Services\CatalogItem\Delete.EditCatalogItemResult.cs">
<DependentUpon>Delete.cs</DependentUpon>
</Compile>
<Compile Update="Services\CatalogItem\GetById.EditCatalogItemResult.cs">
<DependentUpon>GetById.cs</DependentUpon>
</Compile>
<Compile Update="Services\CatalogItem\Edit.CreateCatalogItemResult.cs">
<DependentUpon>Edit.cs</DependentUpon>
</Compile>
</ItemGroup>

</Project>
7 changes: 7 additions & 0 deletions src/BlazorAdmin/Constants.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace BlazorAdmin
{
public class Constants
{
public const string API_URL = "https://localhost:5099/api/";
}
}
83 changes: 83 additions & 0 deletions src/BlazorAdmin/CustomAuthStateProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
using System;
using System.Net.Http;
using System.Net.Http.Json;
using BlazorAdmin.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Components.Authorization;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components;
using Microsoft.eShopWeb;
using Microsoft.Extensions.Logging;
using Shared.Authorization;

namespace BlazorAdmin
{
public class CustomAuthStateProvider : AuthenticationStateProvider
{
private static readonly TimeSpan UserCacheRefreshInterval = TimeSpan.FromSeconds(60);

private readonly AuthService _authService;
private readonly ILogger<CustomAuthStateProvider> _logger;

private DateTimeOffset _userLastCheck = DateTimeOffset.FromUnixTimeSeconds(0);
private ClaimsPrincipal _cachedUser = new ClaimsPrincipal(new ClaimsIdentity());

public CustomAuthStateProvider(AuthService authService, ILogger<CustomAuthStateProvider> logger)
{
_authService = authService;
_logger = logger;
}

public override async Task<AuthenticationState> GetAuthenticationStateAsync() =>
new AuthenticationState(await GetUser(useCache: true));

private async ValueTask<ClaimsPrincipal> GetUser(bool useCache = false)
{
var now = DateTimeOffset.Now;
if (useCache && now < _userLastCheck + UserCacheRefreshInterval)
{
return _cachedUser;
}

_cachedUser = await FetchUser();
_userLastCheck = now;

return _cachedUser;
}

private async Task<ClaimsPrincipal> FetchUser()
{
UserInfo user = null;

try
{
user = await _authService.GetTokenFromController();
}
catch (Exception exc)
{
_logger.LogWarning(exc, "Fetching user failed.");
}

if (user == null || !user.IsAuthenticated)
{
return new ClaimsPrincipal(new ClaimsIdentity());
}

var identity = new ClaimsIdentity(
nameof(CustomAuthStateProvider),
user.NameClaimType,
user.RoleClaimType);

if (user.Claims != null)
{
foreach (var claim in user.Claims)
{
identity.AddClaim(new Claim(claim.Type, claim.Value));
}
}

return new ClaimsPrincipal(identity);
}
}
}
26 changes: 26 additions & 0 deletions src/BlazorAdmin/Helpers/BlazorComponent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using Microsoft.AspNetCore.Components;

namespace BlazorAdmin.Helpers
{
public class BlazorComponent : ComponentBase
{
private readonly RefreshBroadcast _refresh = RefreshBroadcast.Instance;

protected override void OnInitialized()
{
_refresh.RefreshRequested += DoRefresh;
base.OnInitialized();
}

public void CallRequestRefresh()
{
_refresh.CallRequestRefresh();
}

private void DoRefresh()
{
StateHasChanged();
}

}
}
27 changes: 27 additions & 0 deletions src/BlazorAdmin/Helpers/BlazorLayoutComponent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using System.Threading.Tasks;
using BlazorAdmin.Services;
using Microsoft.AspNetCore.Components;

namespace BlazorAdmin.Helpers
{
public class BlazorLayoutComponent : LayoutComponentBase
{
private readonly RefreshBroadcast _refresh = RefreshBroadcast.Instance;

protected override void OnInitialized()
{
_refresh.RefreshRequested += DoRefresh;
base.OnInitialized();
}

public void CallRequestRefresh()
{
_refresh.CallRequestRefresh();
}

private void DoRefresh()
{
StateHasChanged();
}
}
}
24 changes: 24 additions & 0 deletions src/BlazorAdmin/Helpers/RefreshBroadcast.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using System;

namespace BlazorAdmin.Helpers
{
internal sealed class RefreshBroadcast
{
private static readonly Lazy<RefreshBroadcast>
Lazy =
new Lazy<RefreshBroadcast>
(() => new RefreshBroadcast());

public static RefreshBroadcast Instance => Lazy.Value;

private RefreshBroadcast()
{
}

public event Action RefreshRequested;
public void CallRequestRefresh()
{
RefreshRequested?.Invoke();
}
}
}
29 changes: 29 additions & 0 deletions src/BlazorAdmin/JavaScript/Cookies.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;

namespace BlazorAdmin.JavaScript
{
public class Cookies
{
private readonly IJSRuntime _jsRuntime;

public Cookies(IJSRuntime jsRuntime)
{
_jsRuntime = jsRuntime;
}

public async Task DeleteCookie(string name)
{
await _jsRuntime.InvokeAsync<string>("deleteCookie", name);
}

public async Task<string> GetCookie(string name)
{
return await _jsRuntime.InvokeAsync<string>("getCookie", name);
}
}
}
24 changes: 24 additions & 0 deletions src/BlazorAdmin/JavaScript/Route.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;

namespace BlazorAdmin.JavaScript
{
public class Route
{
private readonly IJSRuntime _jsRuntime;

public Route(IJSRuntime jsRuntime)
{
_jsRuntime = jsRuntime;
}

public async Task RouteOutside(string path)
{
await _jsRuntime.InvokeAsync<string>("routeOutside", path);
}
}
}
37 changes: 37 additions & 0 deletions src/BlazorAdmin/Network/SecureHttpClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
using System.Collections.Generic;
using System.Net.Http;
using System.Net.Http.Json;
using System.Threading.Tasks;
using BlazorAdmin.Services.CatalogBrandService;

namespace BlazorAdmin.Network
{
public class SecureHttpClient
{
private readonly HttpClient client;

public SecureHttpClient(HttpClient client)
{
this.client = client;

this.client.DefaultRequestHeaders.Add("Authorization", $"Bearer ");
}

public async Task<List<CatalogBrand>> GetCatalogBrandsAsync()
{
var brands = new List<CatalogBrand>();

try
{
brands = (await client.GetFromJsonAsync<CatalogBrandResult>($"{Constants.API_URL}catalog-brands")).CatalogBrands;
}
catch (AccessTokenNotAvailableException exception)
{
exception.Redirect();
}

return brands;
}
}
}
Loading

0 comments on commit 8d3ac69

Please sign in to comment.