Skip to content

Commit f7e6043

Browse files
Merge pull request #5 from https-richardy/feature/implements-merchants-webui
feature: this commit introduces a Blazor WebAssembly project, and ini…
2 parents e6e9143 + 54d6c10 commit f7e6043

59 files changed

Lines changed: 2890 additions & 0 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
2+
Microsoft Visual Studio Solution File, Format Version 12.00
3+
# Visual Studio Version 18
4+
VisualStudioVersion = 18.3.11312.210
5+
MinimumVisualStudioVersion = 10.0.40219.1
6+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Source", "Source", "{B8EFCA5F-814F-285C-A8CB-F00F14650265}"
7+
EndProject
8+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Comanda.Merchants.WebUI", "Source\Comanda.Merchants.WebUI.csproj", "{C835E518-C45B-4575-93EC-78103FD20F9B}"
9+
EndProject
10+
Global
11+
GlobalSection(SolutionConfigurationPlatforms) = preSolution
12+
Debug|Any CPU = Debug|Any CPU
13+
Debug|x64 = Debug|x64
14+
Debug|x86 = Debug|x86
15+
Release|Any CPU = Release|Any CPU
16+
Release|x64 = Release|x64
17+
Release|x86 = Release|x86
18+
EndGlobalSection
19+
GlobalSection(ProjectConfigurationPlatforms) = postSolution
20+
{C835E518-C45B-4575-93EC-78103FD20F9B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
21+
{C835E518-C45B-4575-93EC-78103FD20F9B}.Debug|Any CPU.Build.0 = Debug|Any CPU
22+
{C835E518-C45B-4575-93EC-78103FD20F9B}.Debug|x64.ActiveCfg = Debug|Any CPU
23+
{C835E518-C45B-4575-93EC-78103FD20F9B}.Debug|x64.Build.0 = Debug|Any CPU
24+
{C835E518-C45B-4575-93EC-78103FD20F9B}.Debug|x86.ActiveCfg = Debug|Any CPU
25+
{C835E518-C45B-4575-93EC-78103FD20F9B}.Debug|x86.Build.0 = Debug|Any CPU
26+
{C835E518-C45B-4575-93EC-78103FD20F9B}.Release|Any CPU.ActiveCfg = Release|Any CPU
27+
{C835E518-C45B-4575-93EC-78103FD20F9B}.Release|Any CPU.Build.0 = Release|Any CPU
28+
{C835E518-C45B-4575-93EC-78103FD20F9B}.Release|x64.ActiveCfg = Release|Any CPU
29+
{C835E518-C45B-4575-93EC-78103FD20F9B}.Release|x64.Build.0 = Release|Any CPU
30+
{C835E518-C45B-4575-93EC-78103FD20F9B}.Release|x86.ActiveCfg = Release|Any CPU
31+
{C835E518-C45B-4575-93EC-78103FD20F9B}.Release|x86.Build.0 = Release|Any CPU
32+
EndGlobalSection
33+
GlobalSection(SolutionProperties) = preSolution
34+
HideSolutionNode = FALSE
35+
EndGlobalSection
36+
GlobalSection(NestedProjects) = preSolution
37+
{C835E518-C45B-4575-93EC-78103FD20F9B} = {B8EFCA5F-814F-285C-A8CB-F00F14650265}
38+
EndGlobalSection
39+
GlobalSection(ExtensibilityGlobals) = postSolution
40+
SolutionGuid = {B38CFDDE-933A-4367-B105-C867F91B05BD}
41+
EndGlobalSection
42+
EndGlobal

UIs/Merchants/Source/App.razor

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
@inject IProfilesClient profilesClient
2+
@inject IPrincipalProvider principalProvider
3+
@inject IStoreClient storeClient
4+
@inject ISessionManager sessionManager
5+
@inject IIdentityClient identityClient
6+
@inject ILocalStorageGateway localStorage
7+
@inject AuthenticationStateProvider authenticationStateProvider
8+
@inject NavigationManager navigationManager
9+
10+
<CascadingAuthenticationState>
11+
<Router AppAssembly="@typeof(App).Assembly" NotFoundPage="typeof(Pages.NotFound)">
12+
<Found Context="routeData">
13+
<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)">
14+
<NotAuthorized>
15+
<NotAuthorized />
16+
</NotAuthorized>
17+
</AuthorizeRouteView>
18+
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
19+
</Found>
20+
</Router>
21+
</CascadingAuthenticationState>
22+
23+
@code {
24+
protected override async Task OnInitializedAsync()
25+
{
26+
await base.OnInitializedAsync();
27+
28+
await InitializeMockSessionAsync();
29+
await SetPrincipalAsync();
30+
31+
var session = await authenticationStateProvider.GetAuthenticationStateAsync();
32+
var principal = session.User;
33+
34+
if (principal?.Identity?.IsAuthenticated is true)
35+
{
36+
var identifier = principal.FindFirst(ClaimTypes.NameIdentifier)?.Value;
37+
var owners = await profilesClient.GetOwnersAsync(new() { UserId = identifier! });
38+
39+
var owner = owners?.Data?.Items.FirstOrDefault();
40+
if (owner is null)
41+
return;
42+
43+
var establishments = await storeClient.GetEstablishmentsAsync(new() { OwnerId = owner.Identifier });
44+
var establishment = establishments?.Data?.Items.FirstOrDefault();
45+
46+
if (establishment is null)
47+
{
48+
navigationManager.NavigateTo("/onboarding");
49+
return;
50+
}
51+
52+
await localStorage.SetAsync<EstablishmentScheme>(Storage.Establishment, establishment);
53+
await localStorage.SetAsync<OwnerScheme>(Storage.Merchant, owner);
54+
}
55+
56+
}
57+
58+
private async Task SetPrincipalAsync()
59+
{
60+
var principal = await principalProvider.GetPrincipalAsync();
61+
if (principal.IsFailure || principal.Data is null)
62+
return;
63+
64+
await localStorage.SetAsync<PrincipalScheme>(Storage.Principal, principal.Data);
65+
}
66+
67+
private async Task InitializeMockSessionAsync()
68+
{
69+
var authentication = await identityClient.AuthenticateAsync(new()
70+
{
71+
Username = "jane.doe@email.com",
72+
Password = "password"
73+
});
74+
75+
if (authentication.IsFailure || authentication.Data is null)
76+
return;
77+
78+
await sessionManager.SignInAsync(authentication.Data.AccessToken, authentication.Data.RefreshToken);
79+
}
80+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
namespace Comanda.Merchants.WebUI.Authentication;
2+
3+
public sealed class JwtAuthenticationStateProvider(ILocalStorageGateway localStorage) :
4+
AuthenticationStateProvider
5+
{
6+
private readonly JwtSecurityTokenHandler tokenHandler = new();
7+
private readonly ClaimsPrincipal anonymous =
8+
new(new ClaimsIdentity());
9+
10+
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
11+
{
12+
var tokenString = await localStorage.GetAsStringAsync(Storage.SecurityToken);
13+
if (string.IsNullOrWhiteSpace(tokenString))
14+
{
15+
return new AuthenticationState(anonymous);
16+
}
17+
18+
var token = tokenHandler.ReadJwtToken(tokenString);
19+
if (token.ValidTo < DateTime.UtcNow)
20+
{
21+
return new AuthenticationState(anonymous);
22+
}
23+
24+
var identity = new ClaimsIdentity(token.Claims, "https://www.rfc-editor.org/rfc/rfc7519", nameType: "preferred_username", roleType: "role");
25+
var principal = new ClaimsPrincipal(identity);
26+
27+
return new AuthenticationState(principal);
28+
}
29+
30+
public void NotifyAuthenticationStateChanged()
31+
{
32+
NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
33+
}
34+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
namespace Comanda.Merchants.WebUI.Authentication;
2+
3+
public sealed class PrincipalProvider(HttpClient httpClient) : IPrincipalProvider
4+
{
5+
private readonly JsonSerializerOptions serializerOptions = new()
6+
{
7+
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
8+
PropertyNameCaseInsensitive = true
9+
};
10+
11+
public async Task<Result<PrincipalScheme>> GetPrincipalAsync(CancellationToken cancellation = default)
12+
{
13+
var response = await httpClient.GetAsync("/api/v1/identity/principal", cancellation);
14+
var content = await response.Content.ReadAsStringAsync(cancellation);
15+
16+
// we prefer explicit boolean comparisons for readability
17+
// https://rules.sonarsource.com/csharp/RSPEC-1125/
18+
19+
#pragma warning disable S1125
20+
if (response.IsSuccessStatusCode is false)
21+
{
22+
return Result<PrincipalScheme>.Failure(UserErrors.UserDoesNotExist);
23+
}
24+
25+
var result = JsonSerializer.Deserialize<PrincipalScheme>(content, serializerOptions);
26+
if (result is null)
27+
{
28+
return Result<PrincipalScheme>.Failure(UserErrors.UserDoesNotExist);
29+
}
30+
31+
return Result<PrincipalScheme>.Success(result);
32+
}
33+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
namespace Comanda.Merchants.WebUI.Authentication;
2+
3+
public sealed class SessionManager(ILocalStorageGateway localStorage, IIdentityClient identityClient, AuthenticationStateProvider provider) : ISessionManager
4+
{
5+
public async Task SignInAsync(string token, string refreshToken)
6+
{
7+
await localStorage.SetAsStringAsync(Storage.SecurityToken, token);
8+
await localStorage.SetAsStringAsync(Storage.RefreshToken, refreshToken);
9+
10+
// inform provider of signin because blazor does not automatically detect localstorage updates
11+
if (provider is JwtAuthenticationStateProvider authenticationState)
12+
authenticationState.NotifyAuthenticationStateChanged();
13+
}
14+
15+
public async Task SignOutAsync()
16+
{
17+
var refreshToken = await localStorage.GetAsStringAsync(Storage.RefreshToken);
18+
if (string.IsNullOrWhiteSpace(refreshToken))
19+
return;
20+
21+
await localStorage.RemoveAsync(Storage.SecurityToken);
22+
await identityClient.InvalidateSessionAsync(new() { RefreshToken = refreshToken });
23+
24+
// notify provider of logout because blazor does not detect localstorage changes automatically
25+
if (provider is JwtAuthenticationStateProvider authenticationState)
26+
authenticationState.NotifyAuthenticationStateChanged();
27+
}
28+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net10.0</TargetFramework>
5+
<Nullable>enable</Nullable>
6+
<ImplicitUsings>enable</ImplicitUsings>
7+
<OverrideHtmlAssetPlaceholders>true</OverrideHtmlAssetPlaceholders>
8+
</PropertyGroup>
9+
10+
<ItemGroup>
11+
<PackageReference Include="Microsoft.AspNetCore.Components.Authorization" Version="10.0.1" />
12+
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.1" />
13+
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="10.0.1" PrivateAssets="all" />
14+
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.1" />
15+
16+
<PackageReference Include="MudBlazor" Version="8.15.0" />
17+
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.15.0" />
18+
<PackageReference Include="Comanda.Internal.Contracts" Version="1.0.8" />
19+
<PackageReference Include="HttpsRichardy.Federation.Sdk.Contracts" Version="1.0.2" />
20+
</ItemGroup>
21+
22+
</Project>

0 commit comments

Comments
 (0)