From 518901e61c3000d70d4c4c8d90beb64884cffbe4 Mon Sep 17 00:00:00 2001 From: hualin Date: Thu, 2 Jan 2025 10:51:36 +0800 Subject: [PATCH 01/12] remove BadRequest --- .../IdentityApiAdditionalEndpointsExtensions.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/CleanAspire.Api/IdentityApiAdditionalEndpointsExtensions.cs b/src/CleanAspire.Api/IdentityApiAdditionalEndpointsExtensions.cs index 1f0f8af..335e2f0 100644 --- a/src/CleanAspire.Api/IdentityApiAdditionalEndpointsExtensions.cs +++ b/src/CleanAspire.Api/IdentityApiAdditionalEndpointsExtensions.cs @@ -457,7 +457,7 @@ public static IEndpointRouteBuilder MapIdentityApiAdditionalEndpoints(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>> + routeGroup.MapPost("/enable2fa", async Task> (ClaimsPrincipal claimsPrincipal, HttpContext context, [FromBody] Enable2faRequest request) => { var userManager = context.RequestServices.GetRequiredService>(); @@ -491,7 +491,7 @@ public static IEndpointRouteBuilder MapIdentityApiAdditionalEndpoints(thi } else { - return TypedResults.BadRequest("Invalid verification code"); + return TypedResults.BadRequest(); } }).RequireAuthorization() .Produces(StatusCodes.Status200OK) From 08b2d6b83941f4d399bd945bae01135847649a73 Mon Sep 17 00:00:00 2001 From: hualin Date: Thu, 2 Jan 2025 10:53:16 +0800 Subject: [PATCH 02/12] remove --- src/CleanAspire.ClientApp/Pages/Counter.razor | 19 ------ src/CleanAspire.ClientApp/Pages/Weather.razor | 58 ------------------- 2 files changed, 77 deletions(-) delete mode 100644 src/CleanAspire.ClientApp/Pages/Counter.razor delete mode 100644 src/CleanAspire.ClientApp/Pages/Weather.razor diff --git a/src/CleanAspire.ClientApp/Pages/Counter.razor b/src/CleanAspire.ClientApp/Pages/Counter.razor deleted file mode 100644 index 65d84b1..0000000 --- a/src/CleanAspire.ClientApp/Pages/Counter.razor +++ /dev/null @@ -1,19 +0,0 @@ -@page "/counter" - - -Counter - -

Counter

- -

Current count: @currentCount

- - - -@code { - private int currentCount = 0; - - private void IncrementCount() - { - currentCount++; - } -} diff --git a/src/CleanAspire.ClientApp/Pages/Weather.razor b/src/CleanAspire.ClientApp/Pages/Weather.razor deleted file mode 100644 index 205c9ca..0000000 --- a/src/CleanAspire.ClientApp/Pages/Weather.razor +++ /dev/null @@ -1,58 +0,0 @@ -@page "/weather" - -@inject HttpClient Http - -Weather - -

Weather

- -

This component demonstrates fetching data from the server.

- -@if (forecasts == null) -{ -

Loading...

-} -else -{ - - - - - - - - - - - @foreach (var forecast in forecasts) - { - - - - - - - } - -
DateTemp. (C)Temp. (F)Summary
@forecast.Date.ToShortDateString()@forecast.TemperatureC@forecast.TemperatureF@forecast.Summary
-} - -@code { - private WeatherForecast[]? forecasts; - - protected override async Task OnInitializedAsync() - { - forecasts = await Http.GetFromJsonAsync("sample-data/weather.json"); - } - - public class WeatherForecast - { - public DateOnly Date { get; set; } - - public int TemperatureC { get; set; } - - public string? Summary { get; set; } - - public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); - } -} From 60d718a44ac44f4ad1845d1474f94d831d8d5107 Mon Sep 17 00:00:00 2001 From: hualin Date: Thu, 2 Jan 2025 10:59:29 +0800 Subject: [PATCH 03/12] TimeZoneInfo.Local.Id --- .../IdentityApiAdditionalEndpointsExtensions.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/CleanAspire.Api/IdentityApiAdditionalEndpointsExtensions.cs b/src/CleanAspire.Api/IdentityApiAdditionalEndpointsExtensions.cs index 335e2f0..00dadae 100644 --- a/src/CleanAspire.Api/IdentityApiAdditionalEndpointsExtensions.cs +++ b/src/CleanAspire.Api/IdentityApiAdditionalEndpointsExtensions.cs @@ -373,7 +373,9 @@ public static IEndpointRouteBuilder MapIdentityApiAdditionalEndpoints(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; @@ -383,7 +385,7 @@ public static IEndpointRouteBuilder MapIdentityApiAdditionalEndpoints(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); From 211fdd8cf520f633f950c1c9e0419e5f08c247e3 Mon Sep 17 00:00:00 2001 From: hualin Date: Thu, 2 Jan 2025 11:08:50 +0800 Subject: [PATCH 04/12] add ExampleChemaTransformer --- README.md | 4 +-- .../OpenApiTransformersExtensions.cs | 28 +++++++++++++++++-- .../wwwroot/appsettings.json | 2 +- src/CleanAspire.WebApp/appsettings.json | 2 +- 4 files changed, 29 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 1044e74..be57dd1 100644 --- a/README.md +++ b/README.md @@ -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=* @@ -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=* diff --git a/src/CleanAspire.Api/OpenApiTransformersExtensions.cs b/src/CleanAspire.Api/OpenApiTransformersExtensions.cs index 99e9c71..2dc2343 100644 --- a/src/CleanAspire.Api/OpenApiTransformersExtensions.cs +++ b/src/CleanAspire.Api/OpenApiTransformersExtensions.cs @@ -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; @@ -62,10 +63,10 @@ private class ExampleChemaTransformer : IOpenApiSchemaTransformer { private static readonly Faker _faker = new(); private static readonly Dictionary _examples = []; - + public ExampleChemaTransformer() { - _examples[typeof(LoginRequest)]=new OpenApiObject + _examples[typeof(LoginRequest)] = new OpenApiObject { ["email"] = new OpenApiString("administrator"), ["password"] = new OpenApiString("P@ssw0rd!") @@ -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) diff --git a/src/CleanAspire.ClientApp/wwwroot/appsettings.json b/src/CleanAspire.ClientApp/wwwroot/appsettings.json index 13932dc..ad8d29d 100644 --- a/src/CleanAspire.ClientApp/wwwroot/appsettings.json +++ b/src/CleanAspire.ClientApp/wwwroot/appsettings.json @@ -7,7 +7,7 @@ }, "ClientAppSettings": { "AppName": "Blazor Aspire", - "Version": "v0.0.60", + "Version": "v0.0.61", "ServiceBaseUrl": "https://apiservice.blazorserver.com" } } diff --git a/src/CleanAspire.WebApp/appsettings.json b/src/CleanAspire.WebApp/appsettings.json index 5c14ffd..1f91fcd 100644 --- a/src/CleanAspire.WebApp/appsettings.json +++ b/src/CleanAspire.WebApp/appsettings.json @@ -8,7 +8,7 @@ "AllowedHosts": "*", "ClientAppSettings": { "AppName": "Blazor Aspire", - "Version": "v0.0.60", + "Version": "v0.0.61", "ServiceBaseUrl": "https://apiservice.blazorserver.com" } } From 860234090b8421f2454cb6c67ccd62331ac39627 Mon Sep 17 00:00:00 2001 From: hualin Date: Thu, 2 Jan 2025 11:15:57 +0800 Subject: [PATCH 05/12] Update ProblemExceptionHandler.cs --- .../ExceptionHandlers/ProblemExceptionHandler.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/CleanAspire.Api/ExceptionHandlers/ProblemExceptionHandler.cs b/src/CleanAspire.Api/ExceptionHandlers/ProblemExceptionHandler.cs index b7b18de..3aad564 100644 --- a/src/CleanAspire.Api/ExceptionHandlers/ProblemExceptionHandler.cs +++ b/src/CleanAspire.Api/ExceptionHandlers/ProblemExceptionHandler.cs @@ -73,11 +73,15 @@ public async ValueTask 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 From 85fc7507f401434b99a0fc5ddfa942133bd1b473 Mon Sep 17 00:00:00 2001 From: hualin Date: Thu, 2 Jan 2025 13:05:26 +0800 Subject: [PATCH 06/12] commit --- .../Account/Profile/TwofactorSetting.razor | 11 ++- .../Pages/Products/Edit.razor | 1 - .../Services/ApiClientService.cs | 39 ++++------ .../Services/Products/ProductServiceProxy.cs | 76 +++++++++++++++---- 4 files changed, 82 insertions(+), 45 deletions(-) diff --git a/src/CleanAspire.ClientApp/Pages/Account/Profile/TwofactorSetting.razor b/src/CleanAspire.ClientApp/Pages/Account/Profile/TwofactorSetting.razor index a05c40f..2b1042b 100644 --- a/src/CleanAspire.ClientApp/Pages/Account/Profile/TwofactorSetting.razor +++ b/src/CleanAspire.ClientApp/Pages/Account/Profile/TwofactorSetting.razor @@ -73,7 +73,7 @@ } - + @@ -185,7 +185,14 @@ } 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); + } } } diff --git a/src/CleanAspire.ClientApp/Pages/Products/Edit.razor b/src/CleanAspire.ClientApp/Pages/Products/Edit.razor index 0a742f7..e4adc39 100644 --- a/src/CleanAspire.ClientApp/Pages/Products/Edit.razor +++ b/src/CleanAspire.ClientApp/Pages/Products/Edit.razor @@ -67,7 +67,6 @@ [Parameter] public string? Id { get; set; } private bool _saving; - private bool _isOnline = true; bool success; string[] errors = { }; private UpdateProductCommand? model; diff --git a/src/CleanAspire.ClientApp/Services/ApiClientService.cs b/src/CleanAspire.ClientApp/Services/ApiClientService.cs index e2c9d1e..a270368 100644 --- a/src/CleanAspire.ClientApp/Services/ApiClientService.cs +++ b/src/CleanAspire.ClientApp/Services/ApiClientService.cs @@ -16,7 +16,7 @@ public ApiClientService(ILogger logger) { _logger = logger; } - public async Task> ExecuteAsync(Func> apiCall) + public async Task> ExecuteAsync(Func> apiCall) { try { @@ -27,45 +27,32 @@ public async Task> Ex { _logger.LogError(ex, ex.Message); - return new ApiClientValidationError(ex.Detail, ex); + return ex; } catch (ProblemDetails ex) { _logger.LogError(ex, ex.Message); - return new ApiClientError(ex.Detail, ex); + return ex; } catch (ApiException ex) { _logger.LogError(ex, ex.Message); - return new ApiClientError(ex.Message, ex); + return new ProblemDetails + { + Title = ex.Message, + Detail = ex.InnerException?.Message ?? ex.Message, + }; } catch (Exception ex) { _logger.LogError(ex, ex.Message); - return new ApiClientError(ex.Message, ex); + return new ProblemDetails + { + Title = ex.Message, + Detail = ex.InnerException?.Message ?? ex.Message, + }; } } } -public class ApiClientError -{ - public string? Message { get; } - public Exception Exception { get; } - public ApiClientError(string? message, Exception exception) - { - Message = message; - Exception = exception; - } -} -public class ApiClientValidationError -{ - public string? Message { get; } - public HttpValidationProblemDetails Errors { get; } - - public ApiClientValidationError(string? message, HttpValidationProblemDetails details) - { - Message = message; - Errors = details; - } -} diff --git a/src/CleanAspire.ClientApp/Services/Products/ProductServiceProxy.cs b/src/CleanAspire.ClientApp/Services/Products/ProductServiceProxy.cs index c4e94b6..4824bc3 100644 --- a/src/CleanAspire.ClientApp/Services/Products/ProductServiceProxy.cs +++ b/src/CleanAspire.ClientApp/Services/Products/ProductServiceProxy.cs @@ -107,7 +107,7 @@ public async Task> GetProductByIdAsync(s } } - public async Task> CreateProductAsync(CreateProductCommand command) + public async Task> CreateProductAsync(CreateProductCommand command) { var isOnline = await _onlineStatusInterop.GetOnlineStatusAsync(); if (isOnline) @@ -127,15 +127,27 @@ await _webpushrService.SendNotificationAsync( } catch (HttpValidationProblemDetails ex) { - return new ApiClientValidationError(ex.Detail, ex); + return ex; } catch (ProblemDetails ex) { - return new ApiClientError(ex.Detail, ex); + return ex; + } + catch (ApiException ex) + { + return new ProblemDetails + { + Title = ex.Message, + Detail = ex.Message + }; } catch (Exception ex) { - return new ApiClientError(ex.Message, ex); + return new ProblemDetails + { + Title = ex.Message, + Detail = ex.Message + }; } } else @@ -172,11 +184,15 @@ await _webpushrService.SendNotificationAsync( } else { - return new ApiClientError("Offline mode is disabled. Please enable offline mode to create products in offline mode.", new Exception("Offline mode is disabled.")); + return new ProblemDetails + { + Title = "Offline mode is disabled.", + Detail = "Offline mode is disabled. Please enable offline mode to create products in offline mode." + }; } } } - public async Task> UpdateProductAsync(UpdateProductCommand command) + public async Task> UpdateProductAsync(UpdateProductCommand command) { var isOnline = await _onlineStatusInterop.GetOnlineStatusAsync(); if (isOnline) @@ -188,19 +204,27 @@ public async Task> UpdateP } catch (HttpValidationProblemDetails ex) { - return new ApiClientValidationError(ex.Detail, ex); + return ex; } catch (ProblemDetails ex) { - return new ApiClientError(ex.Detail, ex); + return ex; } catch (ApiException ex) { - return new ApiClientError(ex.Message, ex); + return new ProblemDetails + { + Title = ex.Message, + Detail = ex.Message + }; } catch (Exception ex) { - return new ApiClientError(ex.Message, ex); + return new ProblemDetails + { + Title = ex.Message, + Detail = ex.Message + }; } } else if (_offlineModeState.Enabled) @@ -243,9 +267,13 @@ public async Task> UpdateP } return true; } - return new ApiClientError("Offline mode is disabled. Please enable offline mode to update products in offline mode.", new Exception("Offline mode is disabled.")); + return new ProblemDetails + { + Title = "Offline mode is disabled.", + Detail = "Offline mode is disabled. Please enable offline mode to update products in offline mode." + }; } - public async Task> DeleteProductsAsync(List productIds) + public async Task> DeleteProductsAsync(List productIds) { var isOnline = await _onlineStatusInterop.GetOnlineStatusAsync(); if (isOnline) @@ -256,13 +284,25 @@ public async Task> DeleteProductsAsync(List await _productCacheService.UpdateDeletedProductsAsync(productIds); return true; } - catch (ProblemDetails ex) + catch(ProblemDetails ex) { - return new ApiClientError(ex.Detail, ex); + return ex; } catch (ApiException ex) { - return new ApiClientError(ex.Message, ex); + return new ProblemDetails + { + Title = ex.Message, + Detail = ex.Message + }; + } + catch (Exception ex) + { + return new ProblemDetails + { + Title = ex.Message, + Detail = ex.Message + }; } } else if (_offlineModeState.Enabled) @@ -272,7 +312,11 @@ public async Task> DeleteProductsAsync(List await _productCacheService.UpdateDeletedProductsAsync(productIds); return true; } - return new ApiClientError("Offline mode is disabled. Please enable offline mode to delete products in offline mode.", new Exception("Offline mode is disabled.")); + return new ProblemDetails + { + Title = "Offline mode is disabled.", + Detail = "Offline mode is disabled. Please enable offline mode to delete products in offline mode." + }; } public async Task SyncOfflineCachedDataAsync() { From 5c016244be952222e620e6fb04f6f5f684cb78d9 Mon Sep 17 00:00:00 2001 From: hualin Date: Thu, 2 Jan 2025 13:17:31 +0800 Subject: [PATCH 07/12] commit --- src/CleanAspire.ClientApp/Pages/Account/ForgetPassword.razor | 2 +- .../Pages/Account/ForgetPasswordConfirm.razor | 2 +- .../Pages/Account/Profile/AccountSetting.razor | 4 ++-- src/CleanAspire.ClientApp/Pages/Account/Profile/Profile.razor | 2 +- .../Pages/Account/Profile/ProfileSetting.razor | 2 +- src/CleanAspire.ClientApp/Pages/Account/SignUp.razor | 2 +- .../Pages/Account/SignupConfirmation.razor | 2 +- .../Pages/Products/Components/NewProductDialog.razor | 2 +- src/CleanAspire.ClientApp/Pages/Products/Edit.razor | 2 +- 9 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/CleanAspire.ClientApp/Pages/Account/ForgetPassword.razor b/src/CleanAspire.ClientApp/Pages/Account/ForgetPassword.razor index 133b022..b61be52 100644 --- a/src/CleanAspire.ClientApp/Pages/Account/ForgetPassword.razor +++ b/src/CleanAspire.ClientApp/Pages/Account/ForgetPassword.razor @@ -34,7 +34,7 @@ }, invalid => { - Snackbar.Add(L[invalid.Message ?? ""], Severity.Error); + Snackbar.Add(L[invalid.Detail ?? "Failed validation"], Severity.Error); }, error => { diff --git a/src/CleanAspire.ClientApp/Pages/Account/ForgetPasswordConfirm.razor b/src/CleanAspire.ClientApp/Pages/Account/ForgetPasswordConfirm.razor index 764fac0..a1f5995 100644 --- a/src/CleanAspire.ClientApp/Pages/Account/ForgetPasswordConfirm.razor +++ b/src/CleanAspire.ClientApp/Pages/Account/ForgetPasswordConfirm.razor @@ -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 => { diff --git a/src/CleanAspire.ClientApp/Pages/Account/Profile/AccountSetting.razor b/src/CleanAspire.ClientApp/Pages/Account/Profile/AccountSetting.razor index eb7e201..d3c2164 100644 --- a/src/CleanAspire.ClientApp/Pages/Account/Profile/AccountSetting.razor +++ b/src/CleanAspire.ClientApp/Pages/Account/Profile/AccountSetting.razor @@ -101,7 +101,7 @@ }, invalid => { - Snackbar.Add(invalid.Message, Severity.Error); + Snackbar.Add(L[invalid.Detail ?? "Failed validation"], Severity.Error); }, error => { @@ -136,7 +136,7 @@ }, invalid => { - Snackbar.Add(invalid.Message, Severity.Error); + Snackbar.Add(L[invalid.Detail ?? "Failed validation"], Severity.Error); }, error => { diff --git a/src/CleanAspire.ClientApp/Pages/Account/Profile/Profile.razor b/src/CleanAspire.ClientApp/Pages/Account/Profile/Profile.razor index e495abd..3bd583c 100644 --- a/src/CleanAspire.ClientApp/Pages/Account/Profile/Profile.razor +++ b/src/CleanAspire.ClientApp/Pages/Account/Profile/Profile.razor @@ -95,7 +95,7 @@ }, invalid => { - Snackbar.Add(invalid.Message, Severity.Error); + Snackbar.Add(L[invalid.Detail ?? "Failed validation"], Severity.Error); }, error => { diff --git a/src/CleanAspire.ClientApp/Pages/Account/Profile/ProfileSetting.razor b/src/CleanAspire.ClientApp/Pages/Account/Profile/ProfileSetting.razor index e4dd8b6..8427e68 100644 --- a/src/CleanAspire.ClientApp/Pages/Account/Profile/ProfileSetting.razor +++ b/src/CleanAspire.ClientApp/Pages/Account/Profile/ProfileSetting.razor @@ -124,7 +124,7 @@ }, invalid => { - Snackbar.Add(invalid.Message, Severity.Error); + Snackbar.Add(L[invalid.Detail ?? "Failed validation"], Severity.Error); }, error => { diff --git a/src/CleanAspire.ClientApp/Pages/Account/SignUp.razor b/src/CleanAspire.ClientApp/Pages/Account/SignUp.razor index 7148133..e8ec363 100644 --- a/src/CleanAspire.ClientApp/Pages/Account/SignUp.razor +++ b/src/CleanAspire.ClientApp/Pages/Account/SignUp.razor @@ -66,7 +66,7 @@ }, invalid => { - Snackbar.Add(invalid.Message, Severity.Error); + Snackbar.Add(L[invalid.Detail ?? "Failed validation"], Severity.Error); waiting = false; }, error => diff --git a/src/CleanAspire.ClientApp/Pages/Account/SignupConfirmation.razor b/src/CleanAspire.ClientApp/Pages/Account/SignupConfirmation.razor index 94dd606..32757c8 100644 --- a/src/CleanAspire.ClientApp/Pages/Account/SignupConfirmation.razor +++ b/src/CleanAspire.ClientApp/Pages/Account/SignupConfirmation.razor @@ -50,7 +50,7 @@ }, invalid => { - message = invalid.Message; + message = L[invalid.Detail ?? "Failed validation"]; }, error => { diff --git a/src/CleanAspire.ClientApp/Pages/Products/Components/NewProductDialog.razor b/src/CleanAspire.ClientApp/Pages/Products/Components/NewProductDialog.razor index 336f943..809e537 100644 --- a/src/CleanAspire.ClientApp/Pages/Products/Components/NewProductDialog.razor +++ b/src/CleanAspire.ClientApp/Pages/Products/Components/NewProductDialog.razor @@ -81,7 +81,7 @@ }, invalid => { - Snackbar.Add(invalid.Message ?? L["Failed validation"], Severity.Error); + Snackbar.Add(L[invalid.Detail ?? "Failed validation"], Severity.Error); _saving = false; }, error => diff --git a/src/CleanAspire.ClientApp/Pages/Products/Edit.razor b/src/CleanAspire.ClientApp/Pages/Products/Edit.razor index e4adc39..3868bc8 100644 --- a/src/CleanAspire.ClientApp/Pages/Products/Edit.razor +++ b/src/CleanAspire.ClientApp/Pages/Products/Edit.razor @@ -115,7 +115,7 @@ }, invalid => { - Snackbar.Add(invalid.Message ?? L["Failed validation"], Severity.Error); + Snackbar.Add(@L[invalid.Detail], Severity.Error); return false; }, error => From 75fd33b807cda6870984335605191f9f628ac333 Mon Sep 17 00:00:00 2001 From: hualin Date: Thu, 2 Jan 2025 17:02:30 +0800 Subject: [PATCH 08/12] commit --- src/CleanAspire.ClientApp/Pages/Products/Edit.razor | 2 +- .../Services/Products/ProductServiceProxy.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/CleanAspire.ClientApp/Pages/Products/Edit.razor b/src/CleanAspire.ClientApp/Pages/Products/Edit.razor index 3868bc8..6d9205f 100644 --- a/src/CleanAspire.ClientApp/Pages/Products/Edit.razor +++ b/src/CleanAspire.ClientApp/Pages/Products/Edit.razor @@ -115,7 +115,7 @@ }, invalid => { - Snackbar.Add(@L[invalid.Detail], Severity.Error); + Snackbar.Add(@L[invalid.Detail ?? "Failed validation"], Severity.Error); return false; }, error => diff --git a/src/CleanAspire.ClientApp/Services/Products/ProductServiceProxy.cs b/src/CleanAspire.ClientApp/Services/Products/ProductServiceProxy.cs index 4824bc3..9b310a4 100644 --- a/src/CleanAspire.ClientApp/Services/Products/ProductServiceProxy.cs +++ b/src/CleanAspire.ClientApp/Services/Products/ProductServiceProxy.cs @@ -215,7 +215,7 @@ public async Task> Upd return new ProblemDetails { Title = ex.Message, - Detail = ex.Message + Detail = ex.InnerException?.Message?? ex.Message }; } catch (Exception ex) From 212db7520cb98953c94e0948891165eb9d4e5e0e Mon Sep 17 00:00:00 2001 From: hualin Date: Fri, 3 Jan 2025 11:24:17 +0800 Subject: [PATCH 09/12] add local caching --- .../Components/WebpushrSetup.razor | 2 +- .../DependencyInjection.cs | 2 +- .../Pages/Account/ForgetPassword.razor | 2 +- .../Pages/Account/ForgetPasswordConfirm.razor | 2 +- .../Account/Profile/AccountSetting.razor | 4 +-- .../Pages/Account/Profile/Profile.razor | 2 +- .../Account/Profile/ProfileSetting.razor | 2 +- .../Pages/Account/SignUp.razor | 3 +- .../Pages/Account/SignupConfirmation.razor | 2 +- ...entService.cs => ApiClientServiceProxy.cs} | 17 ++++------ .../CookieAuthenticationStateProvider.cs | 6 ++-- .../Services/JsInterop/IndexedDbCache.cs | 18 ++++++++-- .../Services/Products/ProductCacheService.cs | 24 +++++++++----- .../Services/Products/ProductServiceProxy.cs | 16 ++++++--- .../UserPreferences/UserPreferences.cs | 24 +++++++------- .../UserPreferences/UserPreferencesService.cs | 2 +- src/CleanAspire.ClientApp/_Imports.razor | 2 +- .../wwwroot/js/indexeddbstorage.js | 33 +++++++++++++++---- 18 files changed, 102 insertions(+), 61 deletions(-) rename src/CleanAspire.ClientApp/Services/{ApiClientService.cs => ApiClientServiceProxy.cs} (76%) diff --git a/src/CleanAspire.ClientApp/Components/WebpushrSetup.razor b/src/CleanAspire.ClientApp/Components/WebpushrSetup.razor index 5a7b7f8..f64543a 100644 --- a/src/CleanAspire.ClientApp/Components/WebpushrSetup.razor +++ b/src/CleanAspire.ClientApp/Components/WebpushrSetup.razor @@ -10,4 +10,4 @@ await webpushr.SetupWebpushrAsync(publicKey!); } } -} +} \ No newline at end of file diff --git a/src/CleanAspire.ClientApp/DependencyInjection.cs b/src/CleanAspire.ClientApp/DependencyInjection.cs index 00ed6bd..c1e2967 100644 --- a/src/CleanAspire.ClientApp/DependencyInjection.cs +++ b/src/CleanAspire.ClientApp/DependencyInjection.cs @@ -121,7 +121,7 @@ public static void AddHttpClients(this IServiceCollection services, IConfigurati }); // ApiClient Service - services.AddScoped(); + services.AddScoped(); } public static void AddAuthenticationAndLocalization(this IServiceCollection services, IConfiguration configuration) diff --git a/src/CleanAspire.ClientApp/Pages/Account/ForgetPassword.razor b/src/CleanAspire.ClientApp/Pages/Account/ForgetPassword.razor index b61be52..a2c0500 100644 --- a/src/CleanAspire.ClientApp/Pages/Account/ForgetPassword.razor +++ b/src/CleanAspire.ClientApp/Pages/Account/ForgetPassword.razor @@ -26,7 +26,7 @@ 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 => { diff --git a/src/CleanAspire.ClientApp/Pages/Account/ForgetPasswordConfirm.razor b/src/CleanAspire.ClientApp/Pages/Account/ForgetPasswordConfirm.razor index a1f5995..0781405 100644 --- a/src/CleanAspire.ClientApp/Pages/Account/ForgetPasswordConfirm.razor +++ b/src/CleanAspire.ClientApp/Pages/Account/ForgetPasswordConfirm.razor @@ -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 => { diff --git a/src/CleanAspire.ClientApp/Pages/Account/Profile/AccountSetting.razor b/src/CleanAspire.ClientApp/Pages/Account/Profile/AccountSetting.razor index d3c2164..e5696a3 100644 --- a/src/CleanAspire.ClientApp/Pages/Account/Profile/AccountSetting.razor +++ b/src/CleanAspire.ClientApp/Pages/Account/Profile/AccountSetting.razor @@ -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() { @@ -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() { diff --git a/src/CleanAspire.ClientApp/Pages/Account/Profile/Profile.razor b/src/CleanAspire.ClientApp/Pages/Account/Profile/Profile.razor index 3bd583c..5e10ef2 100644 --- a/src/CleanAspire.ClientApp/Pages/Account/Profile/Profile.razor +++ b/src/CleanAspire.ClientApp/Pages/Account/Profile/Profile.razor @@ -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; diff --git a/src/CleanAspire.ClientApp/Pages/Account/Profile/ProfileSetting.razor b/src/CleanAspire.ClientApp/Pages/Account/Profile/ProfileSetting.razor index 8427e68..6ffad6f 100644 --- a/src/CleanAspire.ClientApp/Pages/Account/Profile/ProfileSetting.razor +++ b/src/CleanAspire.ClientApp/Pages/Account/Profile/ProfileSetting.razor @@ -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, diff --git a/src/CleanAspire.ClientApp/Pages/Account/SignUp.razor b/src/CleanAspire.ClientApp/Pages/Account/SignUp.razor index e8ec363..31a9da8 100644 --- a/src/CleanAspire.ClientApp/Pages/Account/SignUp.razor +++ b/src/CleanAspire.ClientApp/Pages/Account/SignUp.razor @@ -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; }); diff --git a/src/CleanAspire.ClientApp/Pages/Account/SignupConfirmation.razor b/src/CleanAspire.ClientApp/Pages/Account/SignupConfirmation.razor index 32757c8..7e0ce87 100644 --- a/src/CleanAspire.ClientApp/Pages/Account/SignupConfirmation.razor +++ b/src/CleanAspire.ClientApp/Pages/Account/SignupConfirmation.razor @@ -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; diff --git a/src/CleanAspire.ClientApp/Services/ApiClientService.cs b/src/CleanAspire.ClientApp/Services/ApiClientServiceProxy.cs similarity index 76% rename from src/CleanAspire.ClientApp/Services/ApiClientService.cs rename to src/CleanAspire.ClientApp/Services/ApiClientServiceProxy.cs index a270368..c3d24f9 100644 --- a/src/CleanAspire.ClientApp/Services/ApiClientService.cs +++ b/src/CleanAspire.ClientApp/Services/ApiClientServiceProxy.cs @@ -3,19 +3,14 @@ // See the LICENSE file in the project root for more information. using CleanAspire.Api.Client.Models; +using CleanAspire.ClientApp.Services.JsInterop; using Microsoft.Kiota.Abstractions; using OneOf; namespace CleanAspire.ClientApp.Services; -public class ApiClientService +public class ApiClientServiceProxy(ILogger logger,IndexedDbCache cache) { - private readonly ILogger _logger; - - public ApiClientService(ILogger logger) - { - _logger = logger; - } public async Task> ExecuteAsync(Func> apiCall) { try @@ -26,17 +21,17 @@ public async Task catch (HttpValidationProblemDetails ex) { - _logger.LogError(ex, ex.Message); + logger.LogError(ex, ex.Message); return ex; } catch (ProblemDetails ex) { - _logger.LogError(ex, ex.Message); + logger.LogError(ex, ex.Message); return ex; } catch (ApiException ex) { - _logger.LogError(ex, ex.Message); + logger.LogError(ex, ex.Message); return new ProblemDetails { Title = ex.Message, @@ -45,7 +40,7 @@ public async Task } catch (Exception ex) { - _logger.LogError(ex, ex.Message); + logger.LogError(ex, ex.Message); return new ProblemDetails { Title = ex.Message, diff --git a/src/CleanAspire.ClientApp/Services/Identity/CookieAuthenticationStateProvider.cs b/src/CleanAspire.ClientApp/Services/Identity/CookieAuthenticationStateProvider.cs index 2185d61..dc011bb 100644 --- a/src/CleanAspire.ClientApp/Services/Identity/CookieAuthenticationStateProvider.cs +++ b/src/CleanAspire.ClientApp/Services/Identity/CookieAuthenticationStateProvider.cs @@ -143,17 +143,17 @@ await apiClient.Account.Google.SignIn.PostAsync(q=> }, cancellationToken); NotifyAuthenticationStateChanged(GetAuthenticationStateAsync()); } - catch(ProblemDetails ex) + catch(ProblemDetails) { // Log and re-throw problem details exception throw; } - catch(ApiException ex) + catch(ApiException) { // Log and re-throw API exception throw; } - catch(Exception ex) + catch(Exception) { // Log and re-throw general exception throw; diff --git a/src/CleanAspire.ClientApp/Services/JsInterop/IndexedDbCache.cs b/src/CleanAspire.ClientApp/Services/JsInterop/IndexedDbCache.cs index c9d2983..1927230 100644 --- a/src/CleanAspire.ClientApp/Services/JsInterop/IndexedDbCache.cs +++ b/src/CleanAspire.ClientApp/Services/JsInterop/IndexedDbCache.cs @@ -18,9 +18,23 @@ public IndexedDbCache(IJSRuntime jsRuntime) } // Save data to IndexedDB with optional tags - public async Task SaveDataAsync(string dbName, string key, T value, string[] tags = null) + public async Task SaveDataAsync(string dbName, string key, T value, string[]? tags = null, TimeSpan? expiration = null) { - await _jsRuntime.InvokeVoidAsync("indexedDbStorage.saveData", dbName, key, value, tags ?? Array.Empty()); + var expirationMs = expiration.HasValue ? (int)expiration.Value.TotalMilliseconds : (int?)null; + await _jsRuntime.InvokeVoidAsync("indexedDbStorage.saveData", dbName, key, value, tags ?? Array.Empty(), expirationMs); + } + // Get or set data in IndexedDB + public async Task GetOrSetAsync(string dbName, string key, Func> factory, string[]? tags = null, TimeSpan? expiration = null) + { + var existingData = await GetDataAsync(dbName, key); + if (existingData != null) + { + return existingData; + } + + var newData = await factory(); + await SaveDataAsync(dbName, key, newData, tags, expiration); + return newData; } // Get data from IndexedDB by key diff --git a/src/CleanAspire.ClientApp/Services/Products/ProductCacheService.cs b/src/CleanAspire.ClientApp/Services/Products/ProductCacheService.cs index 61db820..c9f5bdd 100644 --- a/src/CleanAspire.ClientApp/Services/Products/ProductCacheService.cs +++ b/src/CleanAspire.ClientApp/Services/Products/ProductCacheService.cs @@ -18,6 +18,7 @@ public class ProductCacheService private const string OFFLINE_DELETE_COMMAND_CACHE_KEY = "OfflineDeleteCommand:product"; private const string OFFLINE_PAGINATION_CACHE_TAG = "OfflinePagination:"; private const string PAGINATION_TAG = "products_pagination"; + private const string PAGINATION_CACHE_TAG = "products_pagination_cache"; private const string COMMANDS_TAG = "product_commands"; private const string OBJECT_TAG = "product"; @@ -38,20 +39,21 @@ public async Task SaveOrUpdateProductAsync(ProductDto product) return await _cache.GetDataAsync(DATABASENAME, productCacheKey); } - public async Task SaveOrUpdatePaginatedProductsAsync(ProductsWithPaginationQuery query, PaginatedResultOfProductDto data) - { - var cacheKey = GeneratePaginationCacheKey(query); - await _cache.SaveDataAsync(DATABASENAME, cacheKey, data, new[] { PAGINATION_TAG }); - } public async Task SaveOrUpdatePaginatedProductsAsync(string cacheKey, PaginatedResultOfProductDto data) { await _cache.SaveDataAsync(DATABASENAME, cacheKey, data, new[] { PAGINATION_TAG }); } - public async Task GetPaginatedProductsAsync(ProductsWithPaginationQuery query) + public async Task GetPaginatedProductsAsync(string cacheKey) { - var cacheKey = GeneratePaginationCacheKey(query); return await _cache.GetDataAsync(DATABASENAME, cacheKey); } + + public async Task GetOrSetAsync(string cacheKey, Func> factory, TimeSpan? expiration = null) + { + cacheKey = $"{PAGINATION_CACHE_TAG}{cacheKey}"; + return await _cache.GetOrSetAsync(DATABASENAME, cacheKey, factory, new[] { PAGINATION_CACHE_TAG }, expiration); + } + public async Task> GetAllCachedPaginatedResultsAsync() { var cachedData = await _cache.GetDataByTagsAsync( @@ -102,7 +104,7 @@ public async Task StoreOfflineCreateCommandAsync(CreateProductCommand command) ) ?? new List(); cached.Add(command); - await _cache.SaveDataAsync(DATABASENAME, OFFLINE_CREATE_COMMAND_CACHE_KEY, cached, new[] { COMMANDS_TAG }); + await _cache.SaveDataAsync(DATABASENAME, OFFLINE_CREATE_COMMAND_CACHE_KEY, cached, new[] { COMMANDS_TAG }); } public async Task StoreOfflineUpdateCommandAsync(UpdateProductCommand command) @@ -153,11 +155,15 @@ public async Task ClearCommands() await _cache.DeleteDataAsync(DATABASENAME, OFFLINE_UPDATE_COMMAND_CACHE_KEY); await _cache.DeleteDataAsync(DATABASENAME, OFFLINE_DELETE_COMMAND_CACHE_KEY); } + public async Task ClearPaginatedCache() + { + await _cache.DeleteDataByTagsAsync(DATABASENAME, new[] { PAGINATION_CACHE_TAG }); + } private string GenerateProductCacheKey(string productId) { return $"{nameof(ProductDto)}:{productId}"; } - private string GeneratePaginationCacheKey(ProductsWithPaginationQuery query) + public string GeneratePaginationCacheKey(ProductsWithPaginationQuery query) { return $"{nameof(ProductsWithPaginationQuery)}:{query.PageNumber}_{query.PageSize}_{query.Keywords}_{query.OrderBy}_{query.SortDirection}"; } diff --git a/src/CleanAspire.ClientApp/Services/Products/ProductServiceProxy.cs b/src/CleanAspire.ClientApp/Services/Products/ProductServiceProxy.cs index 9b310a4..82c9524 100644 --- a/src/CleanAspire.ClientApp/Services/Products/ProductServiceProxy.cs +++ b/src/CleanAspire.ClientApp/Services/Products/ProductServiceProxy.cs @@ -25,6 +25,9 @@ public class ProductServiceProxy private readonly OfflineModeState _offlineModeState; private readonly OfflineSyncService _offlineSyncService; private bool _previousOnlineStatus; + + private readonly TimeSpan _cacheExpiration = TimeSpan.FromSeconds(15); + public ProductServiceProxy(NavigationManager navigationManager, ProductCacheService productCacheService, IWebpushrService webpushrService, ApiClient apiClient, OnlineStatusInterop onlineStatusInterop, OfflineModeState offlineModeState, OfflineSyncService offlineSyncService) { _navigationManager = navigationManager; @@ -55,18 +58,18 @@ private async void OnOnlineStatusChanged(bool isOnline) public async Task GetPaginatedProductsAsync(ProductsWithPaginationQuery paginationQuery) { var isOnline = await _onlineStatusInterop.GetOnlineStatusAsync(); + var cacheKey = _productCacheService.GeneratePaginationCacheKey(paginationQuery); if (!isOnline) { - var cachedResult = await _productCacheService.GetPaginatedProductsAsync(paginationQuery); + var cachedResult = await _productCacheService.GetPaginatedProductsAsync(cacheKey); return cachedResult ?? new PaginatedResultOfProductDto(); } try { - var paginatedProducts = await _apiClient.Products.Pagination.PostAsync(paginationQuery); + var paginatedProducts = await _productCacheService.GetOrSetAsync(cacheKey, () => _apiClient.Products.Pagination.PostAsync(paginationQuery), _cacheExpiration); if (paginatedProducts != null && _offlineModeState.Enabled) { - await _productCacheService.SaveOrUpdatePaginatedProductsAsync(paginationQuery, paginatedProducts); - + await _productCacheService.SaveOrUpdatePaginatedProductsAsync(cacheKey, paginatedProducts); foreach (var productDto in paginatedProducts.Items) { await _productCacheService.SaveOrUpdateProductAsync(productDto); @@ -122,7 +125,7 @@ await _webpushrService.SendNotificationAsync( $"Our new product, {response.Name}, is now available. Click to learn more!", productUrl ); - + await _productCacheService.ClearPaginatedCache(); return response; } catch (HttpValidationProblemDetails ex) @@ -200,6 +203,7 @@ public async Task> Upd try { var response = await _apiClient.Products.PutAsync(command); + await _productCacheService.ClearPaginatedCache(); return true; } catch (HttpValidationProblemDetails ex) @@ -282,6 +286,7 @@ public async Task> DeleteProductsAsync(List { await _apiClient.Products.DeleteAsync(new DeleteProductCommand() { Ids = productIds }); await _productCacheService.UpdateDeletedProductsAsync(productIds); + await _productCacheService.ClearPaginatedCache(); return true; } catch(ProblemDetails ex) @@ -357,6 +362,7 @@ async Task ProcessCommandsAsync(IEnumerable commands, Func action await Task.Delay(1200); } await _productCacheService.ClearCommands(); + await _productCacheService.ClearPaginatedCache(); _offlineSyncService.SetSyncStatus(SyncStatus.Idle, "", 0, 0); } } diff --git a/src/CleanAspire.ClientApp/Services/UserPreferences/UserPreferences.cs b/src/CleanAspire.ClientApp/Services/UserPreferences/UserPreferences.cs index bdf1dc2..3c03fae 100644 --- a/src/CleanAspire.ClientApp/Services/UserPreferences/UserPreferences.cs +++ b/src/CleanAspire.ClientApp/Services/UserPreferences/UserPreferences.cs @@ -1,21 +1,21 @@ namespace CleanAspire.ClientApp.Services.UserPreferences; - public class UserPreferences - { - /// - /// Set the direction layout of the docs to RTL or LTR. If true RTL is used - /// - public bool RightToLeft { get; set; } +public class UserPreferences +{ + /// + /// Set the direction layout of the docs to RTL or LTR. If true RTL is used + /// + public bool RightToLeft { get; set; } - /// - /// The current dark light mode that is used - /// - public DarkLightMode DarkLightTheme { get; set; } - } + /// + /// The current dark light mode that is used + /// + public DarkLightMode DarkLightTheme { get; set; } +} public enum DarkLightMode { System = 0, Light = 1, Dark = 2 -} \ No newline at end of file +} diff --git a/src/CleanAspire.ClientApp/Services/UserPreferences/UserPreferencesService.cs b/src/CleanAspire.ClientApp/Services/UserPreferences/UserPreferencesService.cs index 184ac9d..e2d090a 100644 --- a/src/CleanAspire.ClientApp/Services/UserPreferences/UserPreferencesService.cs +++ b/src/CleanAspire.ClientApp/Services/UserPreferences/UserPreferencesService.cs @@ -31,7 +31,7 @@ public interface IUserPreferencesService public class UserPreferencesService : IUserPreferencesService { private readonly IStorageService _localStorage; - private const string Key = "userPreferences"; + private const string Key = "_userPreferences"; /// /// Initializes a new instance of the class. diff --git a/src/CleanAspire.ClientApp/_Imports.razor b/src/CleanAspire.ClientApp/_Imports.razor index 306a3b8..4dc2b75 100644 --- a/src/CleanAspire.ClientApp/_Imports.razor +++ b/src/CleanAspire.ClientApp/_Imports.razor @@ -31,7 +31,7 @@ @inject IStringLocalizer L @inject ILogger Logger @inject ApiClient ApiClient -@inject ApiClientService ApiClientService +@inject ApiClientServiceProxy ApiClientServiceProxy @inject UserProfileStore UserProfileStore @inject IWebpushrService WebpushrService @inject OnlineStatusInterop OnlineStatusInterop diff --git a/src/CleanAspire.ClientApp/wwwroot/js/indexeddbstorage.js b/src/CleanAspire.ClientApp/wwwroot/js/indexeddbstorage.js index bd3e9a3..0a219c1 100644 --- a/src/CleanAspire.ClientApp/wwwroot/js/indexeddbstorage.js +++ b/src/CleanAspire.ClientApp/wwwroot/js/indexeddbstorage.js @@ -34,14 +34,19 @@ }, // Save data to the cache store (with JSON serialization and optional tags) - saveData: function (dbName, key, value, tags = []) { + saveData: function (dbName, key, value, tags = [], expiration = null) { return this.open(dbName).then(db => { return new Promise((resolve, reject) => { const transaction = db.transaction('cache', 'readwrite'); const store = transaction.objectStore('cache'); const serializedValue = JSON.stringify(value); // Serialize the value - const request = store.put({ key: key, value: serializedValue, tags: tags }); // Include tags + const request = store.put({ + key: key, + value: serializedValue, + tags: tags, + expiration: expiration ? new Date(Date.now() + expiration).toISOString() : null // Add expiration + }); // Include tags request.onsuccess = () => resolve(); request.onerror = () => reject(request.error); @@ -58,6 +63,7 @@ const index = store.index('tags'); // Access the 'tags' index const results = []; + const deletePromises = []; // Fetch data for each tag const tagRequests = tags.map(tag => { return new Promise((tagResolve, tagReject) => { @@ -67,7 +73,13 @@ const entries = event.target.result; // Push key and deserialized value into the results array entries.forEach(entry => { - results.push({ key: entry.key, value: entry.value }); + // Check expiration + if (!entry.expiration || new Date(entry.expiration) >= new Date()) { + results.push({ key: entry.key, value: JSON.parse(entry.value) }); + } else { + // Add deletion of expired data + deletePromises.push(this.deleteData(dbName, entry.key)); + } }); tagResolve(); }; @@ -80,6 +92,7 @@ // Combine results for all tags Promise.all(tagRequests) + .then(() => Promise.all(deletePromises)) // Ensure expired data is deleted .then(() => resolve(results)) // Return the list of { key, value } .catch(reject); }); @@ -137,10 +150,16 @@ request.onsuccess = function (event) { const result = event.target.result; if (result) { - try { - resolve(JSON.parse(result.value)); // Deserialize the value - } catch (e) { - reject("Error parsing JSON"); + // Check expiration + if (result.expiration && new Date(result.expiration) < new Date()) { + window.indexedDbStorage.deleteData(dbName, key).then(() => resolve(null)) // Delete expired data + .catch(reject); + } else { + try { + resolve(JSON.parse(result.value)); // Deserialize the value + } catch (e) { + reject("Error parsing JSON"); + } } } else { resolve(null); // No data found From d4cd3a6e9a899fed7ac798c977566d89a71bc6d0 Mon Sep 17 00:00:00 2001 From: hualin Date: Fri, 3 Jan 2025 15:25:07 +0800 Subject: [PATCH 10/12] commit --- .../Components/Autocompletes/MultiTenantAutocomplete.cs | 4 +++- .../Services/ApiClientServiceProxy.cs | 7 ++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/CleanAspire.ClientApp/Components/Autocompletes/MultiTenantAutocomplete.cs b/src/CleanAspire.ClientApp/Components/Autocompletes/MultiTenantAutocomplete.cs index 59b553e..ab33dba 100644 --- a/src/CleanAspire.ClientApp/Components/Autocompletes/MultiTenantAutocomplete.cs +++ b/src/CleanAspire.ClientApp/Components/Autocompletes/MultiTenantAutocomplete.cs @@ -2,6 +2,7 @@ using CleanAspire.Api.Client; using CleanAspire.Api.Client.Models; +using CleanAspire.ClientApp.Services; using Microsoft.AspNetCore.Components; using MudBlazor; @@ -19,12 +20,13 @@ public MultiTenantAutocomplete() } public List? 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.Query("multitenant", () => ApiClient.Tenants.GetAsync(), tags: new[] { "multitenant" }, TimeSpan.FromMinutes(60)); StateHasChanged(); // Trigger a re-render after the tenants are loaded } } diff --git a/src/CleanAspire.ClientApp/Services/ApiClientServiceProxy.cs b/src/CleanAspire.ClientApp/Services/ApiClientServiceProxy.cs index c3d24f9..7d6a9e9 100644 --- a/src/CleanAspire.ClientApp/Services/ApiClientServiceProxy.cs +++ b/src/CleanAspire.ClientApp/Services/ApiClientServiceProxy.cs @@ -9,8 +9,13 @@ namespace CleanAspire.ClientApp.Services; -public class ApiClientServiceProxy(ILogger logger,IndexedDbCache cache) +public class ApiClientServiceProxy(ILogger logger, IndexedDbCache cache) { + public async Task Query(string cacheKey, Func> factory, string[]? tags = null, TimeSpan? expiration = null) + { + cacheKey = $"{cacheKey}"; + return await cache.GetOrSetAsync(IndexedDbCache.DATABASENAME, cacheKey, factory, tags, expiration); + } public async Task> ExecuteAsync(Func> apiCall) { try From 2b5f358d0ea43808849b35e0b6eee0cf1c074fb0 Mon Sep 17 00:00:00 2001 From: hualin Date: Fri, 3 Jan 2025 15:36:27 +0800 Subject: [PATCH 11/12] commit --- .../Components/Autocompletes/MultiTenantAutocomplete.cs | 2 +- src/CleanAspire.ClientApp/Services/ApiClientServiceProxy.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/CleanAspire.ClientApp/Components/Autocompletes/MultiTenantAutocomplete.cs b/src/CleanAspire.ClientApp/Components/Autocompletes/MultiTenantAutocomplete.cs index ab33dba..514fef0 100644 --- a/src/CleanAspire.ClientApp/Components/Autocompletes/MultiTenantAutocomplete.cs +++ b/src/CleanAspire.ClientApp/Components/Autocompletes/MultiTenantAutocomplete.cs @@ -26,7 +26,7 @@ protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) { - Tenants = await ApiClientServiceProxy.Query("multitenant", () => ApiClient.Tenants.GetAsync(), tags: new[] { "multitenant" }, TimeSpan.FromMinutes(60)); + Tenants = await ApiClientServiceProxy.QueryAsync("multitenant", () => ApiClient.Tenants.GetAsync(), tags: null, TimeSpan.FromMinutes(60)); StateHasChanged(); // Trigger a re-render after the tenants are loaded } } diff --git a/src/CleanAspire.ClientApp/Services/ApiClientServiceProxy.cs b/src/CleanAspire.ClientApp/Services/ApiClientServiceProxy.cs index 7d6a9e9..d22ec4a 100644 --- a/src/CleanAspire.ClientApp/Services/ApiClientServiceProxy.cs +++ b/src/CleanAspire.ClientApp/Services/ApiClientServiceProxy.cs @@ -11,7 +11,7 @@ namespace CleanAspire.ClientApp.Services; public class ApiClientServiceProxy(ILogger logger, IndexedDbCache cache) { - public async Task Query(string cacheKey, Func> factory, string[]? tags = null, TimeSpan? expiration = null) + public async Task QueryAsync(string cacheKey, Func> factory, string[]? tags = null, TimeSpan? expiration = null) { cacheKey = $"{cacheKey}"; return await cache.GetOrSetAsync(IndexedDbCache.DATABASENAME, cacheKey, factory, tags, expiration); From 667f552282934e75ef446c519ea00ebe4e1fc73e Mon Sep 17 00:00:00 2001 From: hualin Date: Fri, 3 Jan 2025 16:10:19 +0800 Subject: [PATCH 12/12] commit --- .../Autocompletes/MultiTenantAutocomplete.cs | 2 +- .../Account/Profile/TwofactorSetting.razor | 17 +++++++-------- .../Services/ApiClientServiceProxy.cs | 4 ++++ .../Services/Products/ProductCacheService.cs | 12 +---------- .../Services/Products/ProductServiceProxy.cs | 21 +++++++++++-------- 5 files changed, 26 insertions(+), 30 deletions(-) diff --git a/src/CleanAspire.ClientApp/Components/Autocompletes/MultiTenantAutocomplete.cs b/src/CleanAspire.ClientApp/Components/Autocompletes/MultiTenantAutocomplete.cs index 514fef0..1da89e8 100644 --- a/src/CleanAspire.ClientApp/Components/Autocompletes/MultiTenantAutocomplete.cs +++ b/src/CleanAspire.ClientApp/Components/Autocompletes/MultiTenantAutocomplete.cs @@ -26,7 +26,7 @@ protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) { - Tenants = await ApiClientServiceProxy.QueryAsync("multitenant", () => ApiClient.Tenants.GetAsync(), tags: null, TimeSpan.FromMinutes(60)); + Tenants = await ApiClientServiceProxy.QueryAsync("multitenant", () => ApiClient.Tenants.GetAsync(), tags: null, expiration: TimeSpan.FromMinutes(60)); StateHasChanged(); // Trigger a re-render after the tenants are loaded } } diff --git a/src/CleanAspire.ClientApp/Pages/Account/Profile/TwofactorSetting.razor b/src/CleanAspire.ClientApp/Pages/Account/Profile/TwofactorSetting.razor index 2b1042b..df221b4 100644 --- a/src/CleanAspire.ClientApp/Pages/Account/Profile/TwofactorSetting.razor +++ b/src/CleanAspire.ClientApp/Pages/Account/Profile/TwofactorSetting.razor @@ -1,5 +1,4 @@ - -@using Net.Codecrete.QrCodeGenerator +@using Net.Codecrete.QrCodeGenerator
@L["Two-factor authentication"] @@ -22,10 +21,10 @@ @L["Authenticator app"]
@L["Use an authentication app or browser extension to get two-factor authentication codes when prompted."] - @if (UserProfileStore.Profile?.IsTwoFactorEnabled??false) + @if (UserProfileStore.Profile?.IsTwoFactorEnabled ?? false) {
- + @L["To disable, click here."] @L["Disable"]
@@ -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); } @@ -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; @@ -167,7 +166,7 @@ } } } - public Task Close() + public Task Close() { _showConfigureAuthenticatorAppDialog = false; return Task.CompletedTask; @@ -183,7 +182,7 @@ _showConfigureAuthenticatorAppDialog = false; Snackbar.Add(L["Two-factor authentication has been enabled successfully"], Severity.Success); } - catch(ApiException e) + catch (ApiException e) { if (e.ResponseStatusCode == 404) { @@ -194,7 +193,7 @@ Snackbar.Add(L["Invalid verification code"], Severity.Error); } } - + } private async Task GenerateRecoveryCodes() { diff --git a/src/CleanAspire.ClientApp/Services/ApiClientServiceProxy.cs b/src/CleanAspire.ClientApp/Services/ApiClientServiceProxy.cs index d22ec4a..e599f2c 100644 --- a/src/CleanAspire.ClientApp/Services/ApiClientServiceProxy.cs +++ b/src/CleanAspire.ClientApp/Services/ApiClientServiceProxy.cs @@ -16,6 +16,10 @@ public class ApiClientServiceProxy(ILogger logger, Indexe cacheKey = $"{cacheKey}"; return await cache.GetOrSetAsync(IndexedDbCache.DATABASENAME, cacheKey, factory, tags, expiration); } + public async Task ClearCache(string[] tags) + { + await cache.DeleteDataByTagsAsync(IndexedDbCache.DATABASENAME, tags); + } public async Task> ExecuteAsync(Func> apiCall) { try diff --git a/src/CleanAspire.ClientApp/Services/Products/ProductCacheService.cs b/src/CleanAspire.ClientApp/Services/Products/ProductCacheService.cs index c9f5bdd..023d148 100644 --- a/src/CleanAspire.ClientApp/Services/Products/ProductCacheService.cs +++ b/src/CleanAspire.ClientApp/Services/Products/ProductCacheService.cs @@ -47,13 +47,6 @@ public async Task SaveOrUpdatePaginatedProductsAsync(string cacheKey, PaginatedR { return await _cache.GetDataAsync(DATABASENAME, cacheKey); } - - public async Task GetOrSetAsync(string cacheKey, Func> factory, TimeSpan? expiration = null) - { - cacheKey = $"{PAGINATION_CACHE_TAG}{cacheKey}"; - return await _cache.GetOrSetAsync(DATABASENAME, cacheKey, factory, new[] { PAGINATION_CACHE_TAG }, expiration); - } - public async Task> GetAllCachedPaginatedResultsAsync() { var cachedData = await _cache.GetDataByTagsAsync( @@ -155,10 +148,7 @@ public async Task ClearCommands() await _cache.DeleteDataAsync(DATABASENAME, OFFLINE_UPDATE_COMMAND_CACHE_KEY); await _cache.DeleteDataAsync(DATABASENAME, OFFLINE_DELETE_COMMAND_CACHE_KEY); } - public async Task ClearPaginatedCache() - { - await _cache.DeleteDataByTagsAsync(DATABASENAME, new[] { PAGINATION_CACHE_TAG }); - } + private string GenerateProductCacheKey(string productId) { return $"{nameof(ProductDto)}:{productId}"; diff --git a/src/CleanAspire.ClientApp/Services/Products/ProductServiceProxy.cs b/src/CleanAspire.ClientApp/Services/Products/ProductServiceProxy.cs index 82c9524..ab39b7a 100644 --- a/src/CleanAspire.ClientApp/Services/Products/ProductServiceProxy.cs +++ b/src/CleanAspire.ClientApp/Services/Products/ProductServiceProxy.cs @@ -19,20 +19,23 @@ public class ProductServiceProxy private readonly NavigationManager _navigationManager; private readonly ProductCacheService _productCacheService; private readonly IWebpushrService _webpushrService; + private readonly ApiClientServiceProxy _apiClientServiceProxy; private readonly ApiClient _apiClient; private readonly OnlineStatusInterop _onlineStatusInterop; private readonly OfflineModeState _offlineModeState; private readonly OfflineSyncService _offlineSyncService; + private readonly string[] _cacheTags = new[] { "caching" }; private bool _previousOnlineStatus; private readonly TimeSpan _cacheExpiration = TimeSpan.FromSeconds(15); - public ProductServiceProxy(NavigationManager navigationManager, ProductCacheService productCacheService, IWebpushrService webpushrService, ApiClient apiClient, OnlineStatusInterop onlineStatusInterop, OfflineModeState offlineModeState, OfflineSyncService offlineSyncService) + public ProductServiceProxy(NavigationManager navigationManager, ProductCacheService productCacheService, IWebpushrService webpushrService, ApiClientServiceProxy apiClientServiceProxy, ApiClient apiClient, OnlineStatusInterop onlineStatusInterop, OfflineModeState offlineModeState, OfflineSyncService offlineSyncService) { _navigationManager = navigationManager; _productCacheService = productCacheService; _webpushrService = webpushrService; + _apiClientServiceProxy = apiClientServiceProxy; _apiClient = apiClient; _onlineStatusInterop = onlineStatusInterop; _offlineModeState = offlineModeState; @@ -66,7 +69,7 @@ public async Task GetPaginatedProductsAsync(Product } try { - var paginatedProducts = await _productCacheService.GetOrSetAsync(cacheKey, () => _apiClient.Products.Pagination.PostAsync(paginationQuery), _cacheExpiration); + var paginatedProducts = await _apiClientServiceProxy.QueryAsync($"_{cacheKey}", () => _apiClient.Products.Pagination.PostAsync(paginationQuery), tags: _cacheTags, expiration: _cacheExpiration); if (paginatedProducts != null && _offlineModeState.Enabled) { await _productCacheService.SaveOrUpdatePaginatedProductsAsync(cacheKey, paginatedProducts); @@ -97,7 +100,7 @@ public async Task> GetProductByIdAsync(s } try { - var product = await _apiClient.Products[productId].GetAsync(); + var product = await _apiClientServiceProxy.QueryAsync($"_{productId}", () => _apiClient.Products[productId].GetAsync(), tags: _cacheTags, expiration: _cacheExpiration); if (product != null && _offlineModeState.Enabled) { await _productCacheService.SaveOrUpdateProductAsync(product); @@ -125,7 +128,7 @@ await _webpushrService.SendNotificationAsync( $"Our new product, {response.Name}, is now available. Click to learn more!", productUrl ); - await _productCacheService.ClearPaginatedCache(); + await _apiClientServiceProxy.ClearCache(_cacheTags); return response; } catch (HttpValidationProblemDetails ex) @@ -203,7 +206,7 @@ public async Task> Upd try { var response = await _apiClient.Products.PutAsync(command); - await _productCacheService.ClearPaginatedCache(); + await _apiClientServiceProxy.ClearCache(_cacheTags); return true; } catch (HttpValidationProblemDetails ex) @@ -219,7 +222,7 @@ public async Task> Upd return new ProblemDetails { Title = ex.Message, - Detail = ex.InnerException?.Message?? ex.Message + Detail = ex.InnerException?.Message ?? ex.Message }; } catch (Exception ex) @@ -286,10 +289,10 @@ public async Task> DeleteProductsAsync(List { await _apiClient.Products.DeleteAsync(new DeleteProductCommand() { Ids = productIds }); await _productCacheService.UpdateDeletedProductsAsync(productIds); - await _productCacheService.ClearPaginatedCache(); + await _apiClientServiceProxy.ClearCache(_cacheTags); return true; } - catch(ProblemDetails ex) + catch (ProblemDetails ex) { return ex; } @@ -362,7 +365,7 @@ async Task ProcessCommandsAsync(IEnumerable commands, Func action await Task.Delay(1200); } await _productCacheService.ClearCommands(); - await _productCacheService.ClearPaginatedCache(); + await _apiClientServiceProxy.ClearCache(_cacheTags); _offlineSyncService.SetSyncStatus(SyncStatus.Idle, "", 0, 0); } }