Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions UIs/Merchants/Comanda.Merchants.WebUI.sln
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 18
VisualStudioVersion = 18.3.11312.210
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Source", "Source", "{B8EFCA5F-814F-285C-A8CB-F00F14650265}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Comanda.Merchants.WebUI", "Source\Comanda.Merchants.WebUI.csproj", "{C835E518-C45B-4575-93EC-78103FD20F9B}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{C835E518-C45B-4575-93EC-78103FD20F9B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C835E518-C45B-4575-93EC-78103FD20F9B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C835E518-C45B-4575-93EC-78103FD20F9B}.Debug|x64.ActiveCfg = Debug|Any CPU
{C835E518-C45B-4575-93EC-78103FD20F9B}.Debug|x64.Build.0 = Debug|Any CPU
{C835E518-C45B-4575-93EC-78103FD20F9B}.Debug|x86.ActiveCfg = Debug|Any CPU
{C835E518-C45B-4575-93EC-78103FD20F9B}.Debug|x86.Build.0 = Debug|Any CPU
{C835E518-C45B-4575-93EC-78103FD20F9B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C835E518-C45B-4575-93EC-78103FD20F9B}.Release|Any CPU.Build.0 = Release|Any CPU
{C835E518-C45B-4575-93EC-78103FD20F9B}.Release|x64.ActiveCfg = Release|Any CPU
{C835E518-C45B-4575-93EC-78103FD20F9B}.Release|x64.Build.0 = Release|Any CPU
{C835E518-C45B-4575-93EC-78103FD20F9B}.Release|x86.ActiveCfg = Release|Any CPU
{C835E518-C45B-4575-93EC-78103FD20F9B}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{C835E518-C45B-4575-93EC-78103FD20F9B} = {B8EFCA5F-814F-285C-A8CB-F00F14650265}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {B38CFDDE-933A-4367-B105-C867F91B05BD}
EndGlobalSection
EndGlobal
80 changes: 80 additions & 0 deletions UIs/Merchants/Source/App.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
@inject IProfilesClient profilesClient
@inject IPrincipalProvider principalProvider
@inject IStoreClient storeClient
@inject ISessionManager sessionManager
@inject IIdentityClient identityClient
@inject ILocalStorageGateway localStorage
@inject AuthenticationStateProvider authenticationStateProvider
@inject NavigationManager navigationManager

<CascadingAuthenticationState>
<Router AppAssembly="@typeof(App).Assembly" NotFoundPage="typeof(Pages.NotFound)">
<Found Context="routeData">
<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)">
<NotAuthorized>
<NotAuthorized />
</NotAuthorized>
</AuthorizeRouteView>
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
</Found>
</Router>
</CascadingAuthenticationState>

@code {
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();

await InitializeMockSessionAsync();
await SetPrincipalAsync();

var session = await authenticationStateProvider.GetAuthenticationStateAsync();
var principal = session.User;

if (principal?.Identity?.IsAuthenticated is true)
{
var identifier = principal.FindFirst(ClaimTypes.NameIdentifier)?.Value;
var owners = await profilesClient.GetOwnersAsync(new() { UserId = identifier! });

var owner = owners?.Data?.Items.FirstOrDefault();
if (owner is null)
return;

var establishments = await storeClient.GetEstablishmentsAsync(new() { OwnerId = owner.Identifier });
var establishment = establishments?.Data?.Items.FirstOrDefault();

if (establishment is null)
{
navigationManager.NavigateTo("/onboarding");
return;
}

await localStorage.SetAsync<EstablishmentScheme>(Storage.Establishment, establishment);
await localStorage.SetAsync<OwnerScheme>(Storage.Merchant, owner);
}

}

private async Task SetPrincipalAsync()
{
var principal = await principalProvider.GetPrincipalAsync();
if (principal.IsFailure || principal.Data is null)
return;

await localStorage.SetAsync<PrincipalScheme>(Storage.Principal, principal.Data);
}

private async Task InitializeMockSessionAsync()
{
var authentication = await identityClient.AuthenticateAsync(new()
{
Username = "jane.doe@email.com",
Password = "password"
});

if (authentication.IsFailure || authentication.Data is null)
return;

await sessionManager.SignInAsync(authentication.Data.AccessToken, authentication.Data.RefreshToken);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
namespace Comanda.Merchants.WebUI.Authentication;

public sealed class JwtAuthenticationStateProvider(ILocalStorageGateway localStorage) :
AuthenticationStateProvider
{
private readonly JwtSecurityTokenHandler tokenHandler = new();
private readonly ClaimsPrincipal anonymous =
new(new ClaimsIdentity());

public override async Task<AuthenticationState> GetAuthenticationStateAsync()
{
var tokenString = await localStorage.GetAsStringAsync(Storage.SecurityToken);
if (string.IsNullOrWhiteSpace(tokenString))
{
return new AuthenticationState(anonymous);
}

var token = tokenHandler.ReadJwtToken(tokenString);
if (token.ValidTo < DateTime.UtcNow)
{
return new AuthenticationState(anonymous);
}

var identity = new ClaimsIdentity(token.Claims, "https://www.rfc-editor.org/rfc/rfc7519", nameType: "preferred_username", roleType: "role");
var principal = new ClaimsPrincipal(identity);

return new AuthenticationState(principal);
}

public void NotifyAuthenticationStateChanged()
{
NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
}
}
33 changes: 33 additions & 0 deletions UIs/Merchants/Source/Authentication/PrincipalProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
namespace Comanda.Merchants.WebUI.Authentication;

public sealed class PrincipalProvider(HttpClient httpClient) : IPrincipalProvider
{
private readonly JsonSerializerOptions serializerOptions = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
PropertyNameCaseInsensitive = true
};

public async Task<Result<PrincipalScheme>> GetPrincipalAsync(CancellationToken cancellation = default)
{
var response = await httpClient.GetAsync("/api/v1/identity/principal", cancellation);
var content = await response.Content.ReadAsStringAsync(cancellation);

// we prefer explicit boolean comparisons for readability
// https://rules.sonarsource.com/csharp/RSPEC-1125/

#pragma warning disable S1125
if (response.IsSuccessStatusCode is false)
{
return Result<PrincipalScheme>.Failure(UserErrors.UserDoesNotExist);
}

var result = JsonSerializer.Deserialize<PrincipalScheme>(content, serializerOptions);
if (result is null)
{
return Result<PrincipalScheme>.Failure(UserErrors.UserDoesNotExist);
}

return Result<PrincipalScheme>.Success(result);
}
}
28 changes: 28 additions & 0 deletions UIs/Merchants/Source/Authentication/SessionManager.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
namespace Comanda.Merchants.WebUI.Authentication;

public sealed class SessionManager(ILocalStorageGateway localStorage, IIdentityClient identityClient, AuthenticationStateProvider provider) : ISessionManager
{
public async Task SignInAsync(string token, string refreshToken)
{
await localStorage.SetAsStringAsync(Storage.SecurityToken, token);
await localStorage.SetAsStringAsync(Storage.RefreshToken, refreshToken);

// inform provider of signin because blazor does not automatically detect localstorage updates
if (provider is JwtAuthenticationStateProvider authenticationState)
authenticationState.NotifyAuthenticationStateChanged();
}

public async Task SignOutAsync()
{
var refreshToken = await localStorage.GetAsStringAsync(Storage.RefreshToken);
if (string.IsNullOrWhiteSpace(refreshToken))
return;

await localStorage.RemoveAsync(Storage.SecurityToken);
await identityClient.InvalidateSessionAsync(new() { RefreshToken = refreshToken });

// notify provider of logout because blazor does not detect localstorage changes automatically
if (provider is JwtAuthenticationStateProvider authenticationState)
authenticationState.NotifyAuthenticationStateChanged();
}
}
22 changes: 22 additions & 0 deletions UIs/Merchants/Source/Comanda.Merchants.WebUI.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">

<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<OverrideHtmlAssetPlaceholders>true</OverrideHtmlAssetPlaceholders>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.Authorization" Version="10.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="10.0.1" PrivateAssets="all" />
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.1" />

<PackageReference Include="MudBlazor" Version="8.15.0" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.15.0" />
<PackageReference Include="Comanda.Internal.Contracts" Version="1.0.8" />
<PackageReference Include="HttpsRichardy.Federation.Sdk.Contracts" Version="1.0.2" />
</ItemGroup>

</Project>
Loading