Skip to content

Commit 54d6c10

Browse files
feature: this commit introduces a Blazor WebAssembly project, and initial settings (appsettings, launchSettings, index.html, favicon, etc.). Implements JWT authentication, session providers, HTTP gateways, and local storage. Includes main layout, sidebar, navigation, WhatsApp support, and UI components for dashboard, products, kitchen, and settings. Adds forms, dialogs, validation schemas, and onboarding, error, and listing pages.
1 parent e6e9143 commit 54d6c10

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)