Skip to content

Commit

Permalink
Merge pull request #32 from neozhu/refactoring/stage1
Browse files Browse the repository at this point in the history
IndexedDB-based Local Caching for Performance Enhancement
  • Loading branch information
neozhu authored Jan 5, 2025
2 parents ff8f3a9 + 667f552 commit 7391f65
Show file tree
Hide file tree
Showing 31 changed files with 282 additions and 250 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ By incorporating robust offline capabilities, CleanAspire empowers developers to
version: '3.8'
services:
apiservice:
image: blazordevlab/cleanaspire-api:0.0.59
image: blazordevlab/cleanaspire-api:0.0.61
environment:
- ASPNETCORE_ENVIRONMENT=Development
- AllowedHosts=*
Expand All @@ -110,7 +110,7 @@ services:


blazorweb:
image: blazordevlab/cleanaspire-webapp:0.0.59
image: blazordevlab/cleanaspire-webapp:0.0.61
environment:
- ASPNETCORE_ENVIRONMENT=Production
- AllowedHosts=*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,11 +73,15 @@ public async ValueTask<bool> TryHandleAsync(HttpContext httpContext, Exception e
Detail = "A numeric value caused an overflow.",
Instance = $"{httpContext.Request.Method} {httpContext.Request.Path}",
},
ReferenceConstraintException => new ProblemDetails
ReferenceConstraintException e => new ProblemDetails
{
Status = StatusCodes.Status400BadRequest,
Title = "Reference Constraint Violation",
Detail = "A foreign key reference constraint was violated.",
Detail = e.ConstraintName != null && e.ConstraintProperties != null && e.ConstraintProperties.Any()
? $"Foreign key reference constraint {e.ConstraintName} violated. Involved columns: {string.Join(", ", e.ConstraintProperties)}."
: e.ConstraintName != null
? $"Foreign key reference constraint {e.ConstraintName} violated."
: "A reference constraint violation occurred.",
Instance = $"{httpContext.Request.Method} {httpContext.Request.Path}",
},
DbUpdateException => new ProblemDetails
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -373,7 +373,9 @@ public static IEndpointRouteBuilder MapIdentityApiAdditionalEndpoints<TUser>(thi
throw new NotSupportedException($"{nameof(MapIdentityApiAdditionalEndpoints)} requires a user store with email support.");
}
if (user is not ApplicationUser appUser)
{
throw new InvalidCastException($"The provided user must be of type {nameof(ApplicationUser)}.");
}

var tenantId = dbcontext.Tenants.FirstOrDefault()?.Id;
appUser.TenantId = tenantId;
Expand All @@ -383,7 +385,7 @@ public static IEndpointRouteBuilder MapIdentityApiAdditionalEndpoints<TUser>(thi
appUser.Provider = "Google";
appUser.AvatarUrl = validatedUser.Picture;
appUser.LanguageCode = "en-US";
appUser.TimeZoneId = "UTC";
appUser.TimeZoneId = TimeZoneInfo.Local.Id;
appUser.EmailConfirmed = true;
appUser.RefreshToken = idTokenContent!.refresh_token;
appUser.RefreshTokenExpiryTime = DateTime.UtcNow.AddSeconds(idTokenContent.expires_in);
Expand Down Expand Up @@ -457,7 +459,7 @@ public static IEndpointRouteBuilder MapIdentityApiAdditionalEndpoints<TUser>(thi
.WithDescription("Generates a shared key and an Authenticator URI for a logged-in user. This endpoint is typically used to configure a TOTP authenticator app, such as Microsoft Authenticator or Google Authenticator.");


routeGroup.MapPost("/enable2fa", async Task<Results<Ok, ValidationProblem, NotFound, BadRequest<string>>>
routeGroup.MapPost("/enable2fa", async Task<Results<Ok, ValidationProblem, NotFound, BadRequest>>
(ClaimsPrincipal claimsPrincipal, HttpContext context, [FromBody] Enable2faRequest request) =>
{
var userManager = context.RequestServices.GetRequiredService<UserManager<TUser>>();
Expand Down Expand Up @@ -491,7 +493,7 @@ public static IEndpointRouteBuilder MapIdentityApiAdditionalEndpoints<TUser>(thi
}
else
{
return TypedResults.BadRequest("Invalid verification code");
return TypedResults.BadRequest();
}
}).RequireAuthorization()
.Produces(StatusCodes.Status200OK)
Expand Down
28 changes: 25 additions & 3 deletions src/CleanAspire.Api/OpenApiTransformersExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// See the LICENSE file in the project root for more information.

using Bogus;
using CleanAspire.Application.Features.Products.Commands;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.Data;
Expand Down Expand Up @@ -62,10 +63,10 @@ private class ExampleChemaTransformer : IOpenApiSchemaTransformer
{
private static readonly Faker _faker = new();
private static readonly Dictionary<Type, IOpenApiAny> _examples = [];

public ExampleChemaTransformer()
{
_examples[typeof(LoginRequest)]=new OpenApiObject
_examples[typeof(LoginRequest)] = new OpenApiObject
{
["email"] = new OpenApiString("administrator"),
["password"] = new OpenApiString("P@ssw0rd!")
Expand All @@ -82,9 +83,30 @@ public ExampleChemaTransformer()
["Nickname"] = new OpenApiString("exampleNickname"),
["Provider"] = new OpenApiString("Local"),
["TenantId"] = new OpenApiString("123e4567-e89b-47d3-a456-426614174000"),
["TimeZoneId"] = new OpenApiString("America/New_York"),
["TimeZoneId"] = new OpenApiString(TimeZoneInfo.Local.Id),
["LanguageCode"] = new OpenApiString("en-US")
};
_examples[typeof(CreateProductCommand)] = new OpenApiObject
{
["SKU"] = new OpenApiString("ABC123"),
["Name"] = new OpenApiString("Sample Product"),
["Category"] = new OpenApiString("Electronics"),
["Description"] = new OpenApiString("This is a sample product description."),
["Price"] = new OpenApiInteger(199),
["Currency"] = new OpenApiString("USD"),
["UOM"] = new OpenApiString("PCS")
};
_examples[typeof(UpdateProductCommand)] = new OpenApiObject
{
["Id"] = new OpenApiString(Guid.CreateVersion7().ToString()),
["SKU"] = new OpenApiString("ABC123"),
["Name"] = new OpenApiString("Sample Product"),
["Category"] = new OpenApiString("Electronics"),
["Description"] = new OpenApiString("This is a sample product description."),
["Price"] = new OpenApiInteger(199),
["Currency"] = new OpenApiString("USD"),
["UOM"] = new OpenApiString("PCS")
};

}
public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext context, CancellationToken cancellationToken)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

using CleanAspire.Api.Client;
using CleanAspire.Api.Client.Models;
using CleanAspire.ClientApp.Services;
using Microsoft.AspNetCore.Components;
using MudBlazor;

Expand All @@ -19,12 +20,13 @@ public MultiTenantAutocomplete()
}
public List<TenantDto>? Tenants { get; set; } = new();
[Inject] private ApiClient ApiClient { get; set; } = default!;
[Inject] private ApiClientServiceProxy ApiClientServiceProxy { get; set; } = default!;

protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
Tenants = await ApiClient.Tenants.GetAsync();
Tenants = await ApiClientServiceProxy.QueryAsync("multitenant", () => ApiClient.Tenants.GetAsync(), tags: null, expiration: TimeSpan.FromMinutes(60));
StateHasChanged(); // Trigger a re-render after the tenants are loaded
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/CleanAspire.ClientApp/Components/WebpushrSetup.razor
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,4 @@
await webpushr.SetupWebpushrAsync(publicKey!);
}
}
}
}
2 changes: 1 addition & 1 deletion src/CleanAspire.ClientApp/DependencyInjection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ public static void AddHttpClients(this IServiceCollection services, IConfigurati
});

// ApiClient Service
services.AddScoped<ApiClientService>();
services.AddScoped<ApiClientServiceProxy>();
}

public static void AddAuthenticationAndLocalization(this IServiceCollection services, IConfiguration configuration)
Expand Down
4 changes: 2 additions & 2 deletions src/CleanAspire.ClientApp/Pages/Account/ForgetPassword.razor
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,15 @@

private async Task OnValidSubmit(EditContext context)
{
var result = await ApiClientService.ExecuteAsync(() => ApiClient.Account.ForgotPassword.PostAsync(new ForgotPasswordRequest() { Email = model.Email }));
var result = await ApiClientServiceProxy.ExecuteAsync(() => ApiClient.Account.ForgotPassword.PostAsync(new ForgotPasswordRequest() { Email = model.Email }));
result.Switch(
ok =>
{
Navigation.NavigateTo("/account/forgetpasswordsuccessful");
},
invalid =>
{
Snackbar.Add(L[invalid.Message ?? ""], Severity.Error);
Snackbar.Add(L[invalid.Detail ?? "Failed validation"], Severity.Error);
},
error =>
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@

private async Task OnValidSubmit(EditContext context)
{
var result = await ApiClientService.ExecuteAsync(() => ApiClient.ResetPassword.PostAsync(new ResetPasswordRequest() { Email = model.Email, NewPassword = model.PasswordConfirm, ResetCode = model.PasswordResetToken }));
var result = await ApiClientServiceProxy.ExecuteAsync(() => ApiClient.ResetPassword.PostAsync(new ResetPasswordRequest() { Email = model.Email, NewPassword = model.PasswordConfirm, ResetCode = model.PasswordResetToken }));
result.Switch(
ok =>
{
Expand All @@ -65,7 +65,7 @@
},
invalid =>
{
Snackbar.Add(L[invalid.Message ?? "Invalid reset request. Please check the provided information and try again."], Severity.Error);
Snackbar.Add(L[invalid.Detail ?? "Invalid reset request. Please check the provided information and try again."], Severity.Error);
},
error =>
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@
}
if (success)
{
var result = await ApiClientService.ExecuteAsync(async () =>
var result = await ApiClientServiceProxy.ExecuteAsync(async () =>
{
await ApiClient.Account.UpdateEmail.PostAsync(new UpdateEmailRequest()
{
Expand All @@ -101,7 +101,7 @@
},
invalid =>
{
Snackbar.Add(invalid.Message, Severity.Error);
Snackbar.Add(L[invalid.Detail ?? "Failed validation"], Severity.Error);
},
error =>
{
Expand All @@ -119,7 +119,7 @@
}
if (deleteSuccess)
{
var result = await ApiClientService.ExecuteAsync( async () =>
var result = await ApiClientServiceProxy.ExecuteAsync(async () =>
{
await ApiClient.Account.DeleteOwnerAccount.DeleteAsync(new DeleteUserRequest()
{
Expand All @@ -136,7 +136,7 @@
},
invalid =>
{
Snackbar.Add(invalid.Message, Severity.Error);
Snackbar.Add(L[invalid.Detail ?? "Failed validation"], Severity.Error);
},
error =>
{
Expand Down
4 changes: 2 additions & 2 deletions src/CleanAspire.ClientApp/Pages/Account/Profile/Profile.razor
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@
{
if (!string.IsNullOrEmpty(Code) && !string.IsNullOrEmpty(UserId) && !string.IsNullOrEmpty(ChangedEmail))
{
var result = await ApiClientService.ExecuteAsync(() => ApiClient.ConfirmEmail.GetAsync(requestConfiguration =>
var result = await ApiClientServiceProxy.ExecuteAsync(() => ApiClient.ConfirmEmail.GetAsync(requestConfiguration =>
{
requestConfiguration.QueryParameters.Code = Code;
requestConfiguration.QueryParameters.UserId = UserId;
Expand All @@ -95,7 +95,7 @@
},
invalid =>
{
Snackbar.Add(invalid.Message, Severity.Error);
Snackbar.Add(L[invalid.Detail ?? "Failed validation"], Severity.Error);
},
error =>
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@
Snackbar.Add(L["You are offline. Please check your internet connection."], Severity.Error);
return;
}
var result = await ApiClientService.ExecuteAsync(() => ApiClient.Account.Profile.PostAsync(new ProfileRequest()
var result = await ApiClientServiceProxy.ExecuteAsync(() => ApiClient.Account.Profile.PostAsync(new ProfileRequest()
{
AvatarUrl = model.AvatarUrl,
Email = model.Email,
Expand All @@ -124,7 +124,7 @@
},
invalid =>
{
Snackbar.Add(invalid.Message, Severity.Error);
Snackbar.Add(L[invalid.Detail ?? "Failed validation"], Severity.Error);
},
error =>
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@

@using Net.Codecrete.QrCodeGenerator
@using Net.Codecrete.QrCodeGenerator
<div class="d-flex flex-column gap-2">
<MudText Typo="Typo.h4">@L["Two-factor authentication"]</MudText>
<MudDivider />
Expand All @@ -22,10 +21,10 @@
<MudIcon Icon="@Icons.Material.Filled.Phone"></MudIcon> <MudText Color="Color.Primary">@L["Authenticator app"]</MudText>
</div>
<MudText Typo="Typo.body2" Class="mud-text-secondary">@L["Use an authentication app or browser extension to get two-factor authentication codes when prompted."]</MudText>
@if (UserProfileStore.Profile?.IsTwoFactorEnabled??false)
@if (UserProfileStore.Profile?.IsTwoFactorEnabled ?? false)
{
<div class="d-flex flex-row gap-2 align-content-center align-center">
<MudIcon Color="Color.Success" Icon="@Icons.Material.Filled.CheckCircle"> </MudIcon>
<MudIcon Color="Color.Success" Icon="@Icons.Material.Filled.CheckCircle"> </MudIcon>
<MudText Typo="Typo.body2" Class="mud-text-secondary"> @L["To disable, click here."] </MudText>
<MudButton Color="Color.Primary" OnClick="Disable2fa"><MudText>@L["Disable"]</MudText></MudButton>
</div>
Expand Down Expand Up @@ -73,7 +72,7 @@
</MudButton>
}
</div>

</div>
</div>
</MudListItem>
Expand Down Expand Up @@ -137,7 +136,7 @@
UserProfileStore.Set(profile);
Snackbar.Add(L["Two-factor authentication has been disabled successfully"], Severity.Success);
}
catch(ProblemDetails e)
catch (ProblemDetails e)
{
Snackbar.Add(e.Detail, Severity.Error);
}
Expand All @@ -156,7 +155,7 @@
}
if (value)
{
var response = await ApiClient.Account.GenerateAuthenticator.GetAsync(q=>q.QueryParameters.AppName= AppSettings.AppName);
var response = await ApiClient.Account.GenerateAuthenticator.GetAsync(q => q.QueryParameters.AppName = AppSettings.AppName);
if (response is not null)
{
_showConfigureAuthenticatorAppDialog = true;
Expand All @@ -167,7 +166,7 @@
}
}
}
public Task Close()
public Task Close()
{
_showConfigureAuthenticatorAppDialog = false;
return Task.CompletedTask;
Expand All @@ -183,11 +182,18 @@
_showConfigureAuthenticatorAppDialog = false;
Snackbar.Add(L["Two-factor authentication has been enabled successfully"], Severity.Success);
}
catch(ApiException e)
catch (ApiException e)
{
Snackbar.Add(L["Invalid verification code"], Severity.Error);
if (e.ResponseStatusCode == 404)
{
Snackbar.Add(L["User not found."], Severity.Error);
}
else
{
Snackbar.Add(L["Invalid verification code"], Severity.Error);
}
}

}
private async Task GenerateRecoveryCodes()
{
Expand Down
5 changes: 3 additions & 2 deletions src/CleanAspire.ClientApp/Pages/Account/SignUp.razor
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@
return;
}
waiting = true;
var result = await ApiClientService.ExecuteAsync(async () =>{
var result = await ApiClientServiceProxy.ExecuteAsync(async () =>
{
await ApiClient.Account.Signup.PostAsync(new SignupRequest() { Email = model.Email, Password = model.Password, LanguageCode = model.LanguageCode, Nickname = model.Nickname, Provider = model.Provider, TimeZoneId = model.TimeZoneId, TenantId = model.Tenant?.Id });
return true;
});
Expand All @@ -66,7 +67,7 @@
},
invalid =>
{
Snackbar.Add(invalid.Message, Severity.Error);
Snackbar.Add(L[invalid.Detail ?? "Failed validation"], Severity.Error);
waiting = false;
},
error =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
else
{

var result = await ApiClientService.ExecuteAsync(() => ApiClient.ConfirmEmail.GetAsync(requestConfiguration =>
var result = await ApiClientServiceProxy.ExecuteAsync(() => ApiClient.ConfirmEmail.GetAsync(requestConfiguration =>
{
requestConfiguration.QueryParameters.Code = Code;
requestConfiguration.QueryParameters.UserId = UserId;
Expand All @@ -50,7 +50,7 @@
},
invalid =>
{
message = invalid.Message;
message = L[invalid.Detail ?? "Failed validation"];
},
error =>
{
Expand Down
Loading

0 comments on commit 7391f65

Please sign in to comment.