Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Offline Mode with IndexedDB Caching for Authentication #19

Merged
merged 33 commits into from
Dec 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
f388a94
add svg to assets
neozhu Dec 10, 2024
69ef9b7
test
neozhu Dec 10, 2024
b62ad05
Update README.md
neozhu Dec 10, 2024
1140dfd
offline login
neozhu Dec 11, 2024
020e7b3
add DisplayModeInterop
neozhu Dec 11, 2024
d60fab6
offline sign in
neozhu Dec 11, 2024
931bff6
add offline mode settings
neozhu Dec 11, 2024
741b9e9
add OfflineModeState
neozhu Dec 11, 2024
28f9d52
commit
neozhu Dec 11, 2024
229f9d0
add LocalWebpushrOptions
neozhu Dec 12, 2024
fcc0801
clean
neozhu Dec 12, 2024
ad0f28a
commit
neozhu Dec 12, 2024
3d26e92
Update service-worker.js
neozhu Dec 12, 2024
9991c89
commit
neozhu Dec 13, 2024
070e155
commit
neozhu Dec 13, 2024
2329759
remove Tavenem.Blazor.IndexedDB
neozhu Dec 14, 2024
4761739
commit
neozhu Dec 14, 2024
8b404c0
Update README.md
neozhu Dec 14, 2024
8eed865
add GetProductByIdAsync
neozhu Dec 15, 2024
77c7c60
Update IdentityApiAdditionalEndpointsExtensions.cs
neozhu Dec 15, 2024
e230e98
Fix code scanning alert no. 12: Log entries created from user input
neozhu Dec 15, 2024
b5a2b0d
add CreateProductAsync
neozhu Dec 15, 2024
9e4008b
Update Index.razor
neozhu Dec 15, 2024
4eb7c6b
Update Edit.razor
neozhu Dec 15, 2024
12f1c6b
Fix code scanning alert no. 14: Exposure of private information
neozhu Dec 15, 2024
cd91d26
add OfflineSyncService
neozhu Dec 16, 2024
3ab2beb
add OfflineSyncStatus.razor
neozhu Dec 16, 2024
248b158
test
neozhu Dec 16, 2024
4c62541
done
neozhu Dec 16, 2024
4bb9fc3
Update ProductServiceProxy.cs
neozhu Dec 16, 2024
9772b1e
add DeleteProductsAsync and clean data
neozhu Dec 16, 2024
3a56efc
Update README.md
neozhu Dec 16, 2024
6f71bd7
check online status
neozhu Dec 16, 2024
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
1 change: 1 addition & 0 deletions CleanAspire.slnx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<Solution>
<Folder Name="/Solution Items/">
<File Path=".editorconfig" />
<File Path="README.md" />
</Folder>
<Folder Name="/src/">
<Project Path="src/CleanAspire.Api/CleanAspire.Api.csproj" Id="ded5e19f-db6b-c4ee-e692-cffe0619c173" />
Expand Down
34 changes: 27 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,19 @@

With a focus on **Clean Architecture** and **extreme code simplicity**, CleanAspire provides developers with the tools to create responsive and maintainable web applications with minimal effort. The template also supports **Microsoft.Kiota** to simplify API client generation, ensuring consistency and productivity in every project.

### 🌐 Offline Support

CleanAspire fully supports **offline mode** through its integrated PWA capabilities, enabling your application to function seamlessly without an internet connection. By leveraging **Service Workers** and **browser caching**, the application can store essential resources and data locally, ensuring quick load times and uninterrupted access. Additionally, CleanAspire offers streamlined configuration options to help developers manage caching strategies and data synchronization effortlessly, guaranteeing that users receive the latest updates once the network is restored.

**Key Features of Offline Support:**

- **Service Workers Integration:** Efficiently handle caching and background synchronization to manage offline functionality.
- **Automatic Resource Caching:** Automatically caches essential assets and API responses, ensuring critical parts of the application are accessible offline.
- **Seamless Data Synchronization:** Maintains data consistency by synchronizing local changes with the server once the connection is reestablished.
- **User Experience Enhancements:** Provides fallback UI components and notifications to inform users about their offline status and any pending actions.

By incorporating robust offline capabilities, CleanAspire empowers developers to build resilient applications that deliver a consistent and reliable user experience, regardless of network conditions.


### 🔑 Key Features

Expand All @@ -28,7 +41,7 @@ With a focus on **Clean Architecture** and **extreme code simplicity**, CleanAsp

4. **Blazor WebAssembly and PWA Integration**
- Combines the power of Blazor WebAssembly for interactive and lightweight client-side UIs.
- PWA capabilities ensure offline support and a seamless native-like experience.
- PWA capabilities ensure offline support and a seamless native-like experience, allowing users to access the application and data even when offline.

5. **Streamlined API Client Integration**
- Utilizes **Microsoft.Kiota** to automatically generate strongly-typed API clients, reducing development overhead.
Expand All @@ -40,14 +53,21 @@ With a focus on **Clean Architecture** and **extreme code simplicity**, CleanAsp
7. **Cloud-Ready with Docker**
- Preconfigured for Docker, enabling easy deployment to cloud platforms or local environments.

8. **Real-Time Web Push Notifications**
- Integrated **Webpushr** to deliver instant browser notifications.
- Keeps users informed and engaged with real-time updates.
- Fully customizable notifications with targeted delivery and analytics support.
8. **Real-Time Web Push Notifications**
- Integrated **Webpushr** to deliver instant browser notifications.
- Keeps users informed and engaged with real-time updates.
- Fully customizable notifications with targeted delivery and analytics support.

9. **Integrated CI/CD Pipelines**
- Includes GitHub Actions workflows for automated building, testing, and deployment.

10. **Offline Mode Support**
- **Offline mode enabled by default** to provide a seamless experience even without internet access.
- Uses **IndexedDB** to cache data locally, allowing the application to retrieve data and function offline.
- The system detects the online/offline status and fetches data from **IndexedDB** when offline, ensuring uninterrupted access to key features.




### 🌟 Why Choose CleanAspire?

Expand All @@ -67,7 +87,7 @@ With a focus on **Clean Architecture** and **extreme code simplicity**, CleanAsp
version: '3.8'
services:
apiservice:
image: blazordevlab/cleanaspire-api:0.0.47
image: blazordevlab/cleanaspire-api:0.0.49
environment:
- ASPNETCORE_ENVIRONMENT=Development
- AllowedHosts=*
Expand All @@ -88,7 +108,7 @@ services:


webfrontend:
image: blazordevlab/cleanaspire-clientapp:0.0.47
image: blazordevlab/cleanaspire-clientapp:0.0.49
ports:
- "8016:80"
- "8017:443"
Expand Down
2 changes: 1 addition & 1 deletion src/CleanAspire.Api/CleanAspire.Api.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
<PackageReference Include="Microsoft.Extensions.ApiDescription.Server" Version="9.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Identity.Core" Version="9.0.0" />
<PackageReference Include="Scalar.AspNetCore" Version="1.2.49" />
<PackageReference Include="Scalar.AspNetCore" Version="1.2.55" />
<PackageReference Include="Scrutor" Version="5.0.2" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.6" />
<PackageReference Include="StrongGrid" Version="0.110.0" />
Expand Down
7 changes: 7 additions & 0 deletions src/CleanAspire.Api/Endpoints/ProductEndpointRegistrar.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ public void RegisterRoutes(IEndpointRouteBuilder routes)
})
.Produces<IEnumerable<ProductDto>>(StatusCodes.Status200OK)
.ProducesProblem(StatusCodes.Status400BadRequest)
.ProducesProblem(StatusCodes.Status500InternalServerError)
.WithSummary("Get all products")
.WithDescription("Returns a list of all products in the system.");

Expand All @@ -33,6 +34,7 @@ public void RegisterRoutes(IEndpointRouteBuilder routes)
.Produces<ProductDto>(StatusCodes.Status200OK)
.ProducesProblem(StatusCodes.Status404NotFound)
.ProducesProblem(StatusCodes.Status400BadRequest)
.ProducesProblem(StatusCodes.Status500InternalServerError)
.WithSummary("Get product by ID")
.WithDescription("Returns the details of a specific product by its unique ID.");

Expand All @@ -41,6 +43,7 @@ public void RegisterRoutes(IEndpointRouteBuilder routes)
.Produces<ProductDto>(StatusCodes.Status201Created)
.ProducesValidationProblem(StatusCodes.Status422UnprocessableEntity)
.ProducesProblem(StatusCodes.Status400BadRequest)
.ProducesProblem(StatusCodes.Status500InternalServerError)
.WithSummary("Create a new product")
.WithDescription("Creates a new product with the provided details.");

Expand All @@ -50,6 +53,7 @@ public void RegisterRoutes(IEndpointRouteBuilder routes)
.ProducesValidationProblem(StatusCodes.Status422UnprocessableEntity)
.ProducesProblem(StatusCodes.Status400BadRequest)
.ProducesProblem(StatusCodes.Status404NotFound)
.ProducesProblem(StatusCodes.Status500InternalServerError)
.WithSummary("Update an existing product")
.WithDescription("Updates the details of an existing product.");

Expand All @@ -59,12 +63,15 @@ public void RegisterRoutes(IEndpointRouteBuilder routes)
.ProducesValidationProblem(StatusCodes.Status422UnprocessableEntity)
.ProducesProblem(StatusCodes.Status404NotFound)
.ProducesProblem(StatusCodes.Status400BadRequest)
.ProducesProblem(StatusCodes.Status500InternalServerError)
.WithSummary("Delete products by IDs")
.WithDescription("Deletes one or more products by their unique IDs.");

// Get products with pagination and filtering
group.MapPost("/pagination", ([FromServices] IMediator mediator, [FromBody] ProductsWithPaginationQuery query) => mediator.Send(query))
.Produces<PaginatedResult<ProductDto>>(StatusCodes.Status200OK)
.ProducesProblem(StatusCodes.Status400BadRequest)
.ProducesProblem(StatusCodes.Status500InternalServerError)
.WithSummary("Get products with pagination")
.WithDescription("Returns a paginated list of products based on search keywords, page size, and sorting options.");
}
Expand Down
12 changes: 6 additions & 6 deletions src/CleanAspire.Api/ExceptionHandlers/ProblemExceptionHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,21 +41,21 @@ public async ValueTask<bool> TryHandleAsync(HttpContext httpContext, Exception e
g => g.Select(e => e.ErrorMessage).ToArray()
)
},
UniqueConstraintException => new ProblemDetails
UniqueConstraintException e => new ProblemDetails
{
Status = StatusCodes.Status400BadRequest,
Title = "Unique Constraint Violation",
Detail = "A unique constraint violation occurred.",
Detail = $"Unique constraint {e.ConstraintName} violated. Duplicate value for {e.ConstraintProperties[0]}",
Instance = $"{httpContext.Request.Method} {httpContext.Request.Path}",
},
CannotInsertNullException => new ProblemDetails
CannotInsertNullException e => new ProblemDetails
{
Status = StatusCodes.Status400BadRequest,
Title = "Null Value Error",
Detail = "A required field was null.",
Instance = $"{httpContext.Request.Method} {httpContext.Request.Path}",
},
MaxLengthExceededException => new ProblemDetails
MaxLengthExceededException e => new ProblemDetails
{
Status = StatusCodes.Status400BadRequest,
Title = "Max Length Exceeded",
Expand Down Expand Up @@ -92,8 +92,8 @@ public async ValueTask<bool> TryHandleAsync(HttpContext httpContext, Exception e
},
_ => new ProblemDetails
{
Status = StatusCodes.Status400BadRequest,
Title = "Unhandled Exception",
Status = StatusCodes.Status500InternalServerError,
Title = "Internal Server Error",
Detail = "An unexpected error occurred. Please try again later.",
Instance = $"{httpContext.Request.Method} {httpContext.Request.Path}"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ public static IEndpointRouteBuilder MapIdentityApiAdditionalEndpoints<TUser>(thi
{
return CreateValidationProblem(result);
}
logger.LogInformation("User signup request received: {@SignupRequest}", request);
logger.LogInformation("User signup successful.");
await SendConfirmationEmailAsync(user, userManager, context, request.Email);
return TypedResults.Created();
})
Expand Down
2 changes: 1 addition & 1 deletion src/CleanAspire.Application/CleanAspire.Application.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="ZiggyCreatures.FusionCache" Version="2.0.0-preview-2" />
<PackageReference Include="ZiggyCreatures.FusionCache" Version="2.0.0-preview-3" />
<ProjectReference Include="..\CleanAspire.Domain\CleanAspire.Domain.csproj" />
</ItemGroup>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ public GetProductByIdQueryHandler(IApplicationDbContext dbContext)
{
throw new KeyNotFoundException($"Product with Id '{request.Id}' was not found.");
}

return product;
}
}
12 changes: 6 additions & 6 deletions src/CleanAspire.ClientApp/CleanAspire.ClientApp.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,15 @@
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.0 " />
<PackageReference Include="Blazored.LocalStorage" Version="4.5.0" />
<PackageReference Include="Microsoft.Kiota.Abstractions" Version="1.15.2" />
<PackageReference Include="Microsoft.Kiota.Http.HttpClientLibrary" Version="1.15.2" />
<PackageReference Include="Microsoft.Kiota.Abstractions" Version="1.16.0" />
<PackageReference Include="Microsoft.Kiota.Http.HttpClientLibrary" Version="1.16.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="9.0.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="9.0.0" />
<PackageReference Include="Microsoft.Kiota.Serialization.Form" Version="1.15.2" />
<PackageReference Include="Microsoft.Kiota.Serialization.Json" Version="1.15.2" />
<PackageReference Include="Microsoft.Kiota.Serialization.Multipart" Version="1.15.2" />
<PackageReference Include="Microsoft.Kiota.Serialization.Text" Version="1.15.2" />
<PackageReference Include="Microsoft.Kiota.Serialization.Form" Version="1.16.0" />
<PackageReference Include="Microsoft.Kiota.Serialization.Json" Version="1.16.0" />
<PackageReference Include="Microsoft.Kiota.Serialization.Multipart" Version="1.16.0" />
<PackageReference Include="Microsoft.Kiota.Serialization.Text" Version="1.16.0" />
<PackageReference Include="MudBlazor" Version="8.0.0-preview.5" />
<PackageReference Include="OneOf" Version="3.0.271" />
</ItemGroup>
Expand Down
5 changes: 5 additions & 0 deletions src/CleanAspire.ClientApp/Client/.kiota/workspace.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"version": "1.0.0",
"clients": {},
"plugins": {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,6 @@
[Parameter]
public EventCallback OnPrintClick { get; set; }

private async Task GoBack()
{
await new HistoryGo(JS).GoBack();
}

private async Task Save()
{
if (OnSaveButtonClick.HasDelegate)
Expand Down
56 changes: 56 additions & 0 deletions src/CleanAspire.ClientApp/Components/OfflineSyncStatus.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
@inject OfflineSyncService OfflineSyncService

<div class="d-flex justify-center align-center flex-row gap-2">
@if (OfflineSyncService.CurrentStatus == SyncStatus.Idle)
{
<MudIconButton Icon="@Icons.Material.Outlined.MoreVert" Color="Color.Inherit" />
}
else if (OfflineSyncService.CurrentStatus == SyncStatus.Completed)
{
<MudText Typo="Typo.caption">@OfflineSyncService.StatusMessage</MudText>
<MudIcon>
<svg width="24" height="24" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<circle cx="50" cy="50" r="45" stroke="#2ecc71" stroke-width="9" fill="none">
<animate attributeName="stroke-dasharray" from="0, 283" to="283, 283" dur="0.6s" fill="freeze" />
</circle>
<path d="M30 50 L45 65 L70 35" stroke="#2ecc71" stroke-width="10" fill="none" stroke-linecap="round" stroke-linejoin="round"
stroke-dasharray="50, 50" stroke-dashoffset="50">
<animate attributeName="stroke-dashoffset" from="50" to="0" dur="0.4s" begin="0.6s" fill="freeze" />
</path>
</svg>
</MudIcon>
}else
{
<MudText Typo="Typo.caption">@OfflineSyncService.StatusMessage</MudText>
<MudIcon>
<svg width="24" height="24" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" transform="matrix(-1.8369701987210297e-16,-1,1,-1.8369701987210297e-16,0,0)">
<circle cx="50" cy="20" r="7" fill="#3498db">
<animateTransform attributeType="XML" attributeName="transform" type="rotate" from="0 50 50" to="360 50 50" dur="1.5s" repeatCount="indefinite"></animateTransform>
</circle>
<circle cx="80" cy="50" r="7" fill="#e74c3c">
<animateTransform attributeType="XML" attributeName="transform" type="rotate" from="0 50 50" to="360 50 50" dur="1.5s" begin="0.2s" repeatCount="indefinite"></animateTransform>
</circle>
<circle cx="50" cy="80" r="7" fill="#f1c40f">
<animateTransform attributeType="XML" attributeName="transform" type="rotate" from="0 50 50" to="360 50 50" dur="1.5s" begin="0.4s" repeatCount="indefinite"></animateTransform>
</circle>
<circle cx="20" cy="50" r="7" fill="#2ecc71">
<animateTransform attributeType="XML" attributeName="transform" type="rotate" from="0 50 50" to="360 50 50" dur="1.5s" begin="0.6s" repeatCount="indefinite"></animateTransform>
</circle>
</svg>
</MudIcon>
}
</div>

@code {
protected override void OnInitialized()
{
OfflineSyncService.OnSyncStateChanged += StateHasChanged;
}

public void Dispose()
{
OfflineSyncService.OnSyncStateChanged -= StateHasChanged;
}


}
22 changes: 7 additions & 15 deletions src/CleanAspire.ClientApp/Components/WebpushrSetup.razor
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,13 @@
{
if (firstRender)
{
var result = await ApiClientService.ExecuteAsync(() => ApiClient.Webpushr.Config.GetAsync());
result.Switch(
async ok =>
{
var webpushr = new Webpushr(JS);
await webpushr.SetupWebpushrAsync(ok.PublicKey!);
},
invalid =>
{
Snackbar.Add(L["Invalid configuration received. Please check the Webpushr settings."], Severity.Error);
},
error =>
{
Snackbar.Add(L["An error occurred while fetching the Webpushr configuration. Please try again later."], Severity.Error);
});
var online = await OnlineStatusInterop.GetOnlineStatusAsync();
if (online)
{
var publicKey = await WebpushrService.GetPublicKeyAsync();
var webpushr = new Webpushr(JS);
await webpushr.SetupWebpushrAsync(publicKey!);
}
}
}
}
4 changes: 2 additions & 2 deletions src/CleanAspire.ClientApp/DependencyInjection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@
using CleanAspire.ClientApp.Services.Interfaces;
using CleanAspire.ClientApp.Services.UserPreferences;
using CleanAspire.ClientApp.Services;

namespace CleanAspire.ClientApp;

public static class DependencyInjection
{

public static void TryAddMudBlazor(this IServiceCollection services, IConfiguration config)
{
#region register MudBlazor.Services
Expand Down Expand Up @@ -43,6 +43,6 @@ public static void TryAddMudBlazor(this IServiceCollection services, IConfigurat
services.AddScoped<DialogServiceHelper>();
#endregion
}

}

5 changes: 3 additions & 2 deletions src/CleanAspire.ClientApp/Layout/MainLayout.razor
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,14 @@
public LayoutService LayoutService { get; set; } = default!;
private MudThemeProvider _mudThemeProvider=default!;

protected override void OnInitialized()
protected override async Task OnInitializedAsync()
{
if (LayoutService != null)
{
LayoutService.MajorUpdateOccurred += LayoutServiceOnMajorUpdateOccured;
}
base.OnInitialized();
OnlineStatusInterop.Initialize();
await OfflineModeState.InitializeAsync();
}

protected override async Task OnAfterRenderAsync(bool firstRender)
Expand Down
2 changes: 1 addition & 1 deletion src/CleanAspire.ClientApp/Layout/Navbar.razor
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@
}

<MudSpacer />
<MudIconButton Icon="@Icons.Material.Outlined.MoreVert" Color="Color.Inherit" />
<OfflineSyncStatus></OfflineSyncStatus>
</MudToolBar>
</MudPaper>

Expand Down
Loading
Loading