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

Integration of Webpushr for Real-Time Notifications #17

Merged
merged 23 commits into from
Dec 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions .github/workflows/docker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,8 @@ jobs:


- name: Build and push CleanAspire.ClientApp image
working-directory: ./src/CleanAspire.ClientApp
run: |
docker build -t ${{ secrets.DOCKER_USERNAME }}/cleanaspire-clientapp:${{ steps.version.outputs.version }} .
docker build -t ${{ secrets.DOCKER_USERNAME }}/cleanaspire-clientapp:${{ steps.version.outputs.version }} -f src/CleanAspire.ClientApp/Dockerfile .
docker push ${{ secrets.DOCKER_USERNAME }}/cleanaspire-clientapp:${{ steps.version.outputs.version }}

- name: Build and push CleanAspire.Api image
Expand Down
76 changes: 65 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,12 @@ 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. **Integrated CI/CD Pipelines**
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.


Expand All @@ -62,7 +67,7 @@ With a focus on **Clean Architecture** and **extreme code simplicity**, CleanAsp
version: '3.8'
services:
apiservice:
image: blazordevlab/cleanaspire-api:0.0.45
image: blazordevlab/cleanaspire-api:0.0.47
environment:
- ASPNETCORE_ENVIRONMENT=Development
- AllowedHosts=*
Expand All @@ -74,13 +79,16 @@ services:
- AllowedCorsOrigins=https://cleanaspire.blazorserver.com,https://localhost:7123
- SendGrid__ApiKey=<your API key>
- SendGrid__DefaultFromEmail=<your email>
- Webpushr__Token=<your-webpushr-token>
- Webpushr__ApiKey=<your-webpushr-api-keys>
- Webpushr__PublicKey=<your-webpushr-public-key>
ports:
- "8019:80"
- "8018:443"


webfrontend:
image: blazordevlab/cleanaspire-clientapp:0.0.45
image: blazordevlab/cleanaspire-clientapp:0.0.47
ports:
- "8016:80"
- "8017:443"
Expand Down Expand Up @@ -109,30 +117,76 @@ services:
4. **Access the Application**:
Open your browser and go to `https://localhost:5001` to see the Blazor WebAssembly PWA in action.

### How to Register and Configure Webpushr

1. **Register on Webpushr**
- Visit the [Webpushr website](https://www.webpushr.com/) and sign up for an account.
- Complete the registration process to access your dashboard.

2. **Obtain Required Keys**
- Navigate to the API configuration section in your Webpushr dashboard.
- Copy the following keys:
- **Token**
- **API Key**
- **Public Key**

3. **Add Configuration to `appsettings.json`**
Add the keys obtained from Webpushr into your application configuration file as follows:
```json
"Webpushr": {
"Token": "your-webpushr-token",
"APIKey": "your-webpushr-api-key",
"PublicKey": "your-webpushr-public-key"
}
```

4. **Integrate Webpushr in the Application**
- Use the `PublicKey` for initializing Webpushr on the client-side to enable browser notifications.
- Use the `Token` and `API Key` securely on the server-side for API communication with Webpushr.


### Architecture

CleanAspire is structured following the **Clean Architecture** approach, dividing the solution into distinct layers:

```
CleanAspire/
├── Solution Items/
│ ├── .editorconfig
├── src/
│ ├── CleanAspire.Api/ # API Layer - .NET Minimal API
│ │ └── CleanAspire.Api.csproj
│ ├── CleanAspire.AppHost/ # Hosting Layer - Application hosting and configuration
│ │ └── CleanAspire.AppHost.csproj
│ ├── CleanAspire.Application/ # Application Layer - Business Logic
│ ├── CleanAspire.WebAssembly/ # UI Layer - Blazor WebAssembly (PWA)
│ ├── CleanAspire.Infrastructure/ # Infrastructure Layer - Data Access, External Services
│ │ └── CleanAspire.Application.csproj
│ ├── CleanAspire.ClientApp/ # Client App Layer - Blazor WebAssembly or other client logic
│ │ └── CleanAspire.ClientApp.csproj
│ ├── CleanAspire.Domain/ # Domain Layer - Core Entities, including EF entities
│ ├── CleanAspire.AppHost/ # Hosting Layer - Application hosting and configuration
│ └── CleanAspire.ServiceDefaults/ # Service Defaults - Predefined configurations for services
│ │ └── CleanAspire.Domain.csproj
│ ├── CleanAspire.Infrastructure/ # Infrastructure Layer - Data Access, External Services
│ │ └── CleanAspire.Infrastructure.csproj
│ ├── CleanAspire.ServiceDefaults/ # Service Defaults - Predefined configurations for services
│ │ └── CleanAspire.ServiceDefaults.csproj
├── src/Migrators/
│ ├── Migrators.MSSQL/ # SQL Server Migration Scripts
│ │ └── Migrators.MSSQL.csproj
│ ├── Migrators.PostgreSQL/ # PostgreSQL Migration Scripts
│ │ └── Migrators.PostgreSQL.csproj
│ ├── Migrators.SQLite/ # SQLite Migration Scripts
│ │ └── Migrators.SQLite.csproj
├── tests/
│ ├── CleanAspire.Application.Tests/ # Unit Tests for Application Layer
│ ├── CleanAspire.Api.Tests/ # Integration Tests for API Layer
│ └── CleanAspire.WebAssembly.Tests/ # UI Tests for Blazor WebAssembly
│ ├── CleanAspire.Tests/ # Unit and Integration Tests
│ │ └── CleanAspire.Tests.csproj
├── README.md # Project README
├── LICENSE # License Information
└── CleanAspire.sln # Solution File for Visual Studio / VS Code
└── CleanAspire.sln # Solution File

```

### Contributions
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.45" />
<PackageReference Include="Scalar.AspNetCore" Version="1.2.49" />
<PackageReference Include="Scrutor" Version="5.0.2" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.6" />
<PackageReference Include="StrongGrid" Version="0.110.0" />
Expand Down
35 changes: 35 additions & 0 deletions src/CleanAspire.Api/Endpoints/WebpushrEndpointRegistrar.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.


using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using CleanAspire.Api.Webpushr;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;


namespace CleanAspire.Api.Endpoints;

public class WebpushrEndpointRegistrar : IEndpointRegistrar
{
public void RegisterRoutes(IEndpointRouteBuilder routes)
{
var webpushrOptions = routes.ServiceProvider.GetRequiredService<IOptions<WebpushrOptions>>().Value;
var group = routes.MapGroup("/webpushr").WithTags("webpushr").AllowAnonymous();

group.MapGet("/config", () =>
{
return TypedResults.Ok(webpushrOptions);
})
.Produces<WebpushrOptions>(StatusCodes.Status200OK)
.WithSummary("Retrieve current Webpushr configuration")
.WithDescription("Returns the Webpushr configuration details currently loaded from the application's configuration system. This information includes keys and tokens used for Webpushr push notifications.");
}
}


5 changes: 5 additions & 0 deletions src/CleanAspire.Api/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,18 @@
using CleanAspire.Infrastructure.Configurations;
using Microsoft.AspNetCore.Http.Features;
using CleanAspire.Api.ExceptionHandlers;
using CleanAspire.Api.Webpushr;
using System.Net.Http;


var builder = WebApplication.CreateBuilder(args);


builder.RegisterSerilog();

builder.Services.Configure<WebpushrOptions>(builder.Configuration.GetSection(WebpushrOptions.Key));



builder.Services.AddApplication();
builder.Services.AddInfrastructure(builder.Configuration);
Expand Down
13 changes: 13 additions & 0 deletions src/CleanAspire.Api/Webpushr/WebpushrOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

namespace CleanAspire.Api.Webpushr;

public class WebpushrOptions
{
public static string Key = "Webpushr";
public string Token { get; set; } = string.Empty;
public string ApiKey { get; set; } = string.Empty;
public string PublicKey { get; set; } = string.Empty;
}
5 changes: 5 additions & 0 deletions src/CleanAspire.Api/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@
"ApiKey": "your-sendgrid-api-key",
"DefaultFromEmail": "[email protected]"
},
"Webpushr": {
"Token": "your-webpushr-token",
"ApiKey": "your-webpushr-api-keys",
"PublicKey": "your-webpushr-public-key"
},
"Serilog": {
"MinimumLevel": {
"Default": "Debug",
Expand Down
61 changes: 34 additions & 27 deletions src/CleanAspire.ClientApp/CleanAspire.ClientApp.csproj
Original file line number Diff line number Diff line change
@@ -1,32 +1,39 @@
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">

<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<ServiceWorkerAssetsManifest>service-worker-assets.js</ServiceWorkerAssetsManifest>
<RootNamespace>CleanAspire.ClientApp</RootNamespace>
<AssemblyName>CleanAspire.ClientApp</AssemblyName>
</PropertyGroup>
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<ServiceWorkerAssetsManifest>service-worker-assets.js</ServiceWorkerAssetsManifest>
<RootNamespace>CleanAspire.ClientApp</RootNamespace>
<AssemblyName>CleanAspire.ClientApp</AssemblyName>
</PropertyGroup>
<!--
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
<RunAOTCompilation>true</RunAOTCompilation>
<BlazorWebAssemblyEnableLinker>true</BlazorWebAssemblyEnableLinker>
<PublishTrimmed>true</PublishTrimmed>
<WasmEnableInterpreter>true</WasmEnableInterpreter>
<InvariantGlobalization>true</InvariantGlobalization>
</PropertyGroup>-->
<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.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="MudBlazor" Version="8.0.0-preview.5" />
<PackageReference Include="OneOf" Version="3.0.271" />
</ItemGroup>

<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.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="MudBlazor" Version="8.0.0-preview.5" />
<PackageReference Include="OneOf" Version="3.0.271" />
</ItemGroup>

<ItemGroup>
<ServiceWorker Include="wwwroot\service-worker.js" PublishedContent="wwwroot\service-worker.published.js" />
</ItemGroup>
<ItemGroup>
<ServiceWorker Include="wwwroot\service-worker.js" PublishedContent="wwwroot\service-worker.published.js" />
</ItemGroup>

</Project>
6 changes: 6 additions & 0 deletions src/CleanAspire.ClientApp/Client/ApiClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
using CleanAspire.Api.Client.ResendConfirmationEmail;
using CleanAspire.Api.Client.ResetPassword;
using CleanAspire.Api.Client.Tenants;
using CleanAspire.Api.Client.Webpushr;
using Microsoft.Kiota.Abstractions.Extensions;
using Microsoft.Kiota.Abstractions;
using System.Collections.Generic;
Expand Down Expand Up @@ -86,6 +87,11 @@ public partial class ApiClient : BaseRequestBuilder
{
get => new global::CleanAspire.Api.Client.Tenants.TenantsRequestBuilder(PathParameters, RequestAdapter);
}
/// <summary>The webpushr property</summary>
public global::CleanAspire.Api.Client.Webpushr.WebpushrRequestBuilder Webpushr
{
get => new global::CleanAspire.Api.Client.Webpushr.WebpushrRequestBuilder(PathParameters, RequestAdapter);
}
/// <summary>
/// Instantiates a new <see cref="global::CleanAspire.Api.Client.ApiClient"/> and sets the default values.
/// </summary>
Expand Down
85 changes: 85 additions & 0 deletions src/CleanAspire.ClientApp/Client/Models/WebpushrOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// <auto-generated/>
#pragma warning disable CS0618
using Microsoft.Kiota.Abstractions.Extensions;
using Microsoft.Kiota.Abstractions.Serialization;
using System.Collections.Generic;
using System.IO;
using System;
namespace CleanAspire.Api.Client.Models
{
[global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")]
#pragma warning disable CS1591
public partial class WebpushrOptions : IAdditionalDataHolder, IParsable
#pragma warning restore CS1591
{
/// <summary>Stores additional data not described in the OpenAPI description found when deserializing. Can be used for serialization as well.</summary>
public IDictionary<string, object> AdditionalData { get; set; }
/// <summary>The apiKey property</summary>
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
#nullable enable
public string? ApiKey { get; set; }
#nullable restore
#else
public string ApiKey { get; set; }
#endif
/// <summary>The publicKey property</summary>
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
#nullable enable
public string? PublicKey { get; set; }
#nullable restore
#else
public string PublicKey { get; set; }
#endif
/// <summary>The token property</summary>
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
#nullable enable
public string? Token { get; set; }
#nullable restore
#else
public string Token { get; set; }
#endif
/// <summary>
/// Instantiates a new <see cref="global::CleanAspire.Api.Client.Models.WebpushrOptions"/> and sets the default values.
/// </summary>
public WebpushrOptions()
{
AdditionalData = new Dictionary<string, object>();
}
/// <summary>
/// Creates a new instance of the appropriate class based on discriminator value
/// </summary>
/// <returns>A <see cref="global::CleanAspire.Api.Client.Models.WebpushrOptions"/></returns>
/// <param name="parseNode">The parse node to use to read the discriminator value and create the object</param>
public static global::CleanAspire.Api.Client.Models.WebpushrOptions CreateFromDiscriminatorValue(IParseNode parseNode)
{
_ = parseNode ?? throw new ArgumentNullException(nameof(parseNode));
return new global::CleanAspire.Api.Client.Models.WebpushrOptions();
}
/// <summary>
/// The deserialization information for the current model
/// </summary>
/// <returns>A IDictionary&lt;string, Action&lt;IParseNode&gt;&gt;</returns>
public virtual IDictionary<string, Action<IParseNode>> GetFieldDeserializers()
{
return new Dictionary<string, Action<IParseNode>>
{
{ "apiKey", n => { ApiKey = n.GetStringValue(); } },
{ "publicKey", n => { PublicKey = n.GetStringValue(); } },
{ "token", n => { Token = n.GetStringValue(); } },
};
}
/// <summary>
/// Serializes information the current object
/// </summary>
/// <param name="writer">Serialization writer to use to serialize this model</param>
public virtual void Serialize(ISerializationWriter writer)
{
_ = writer ?? throw new ArgumentNullException(nameof(writer));
writer.WriteStringValue("apiKey", ApiKey);
writer.WriteStringValue("publicKey", PublicKey);
writer.WriteStringValue("token", Token);
writer.WriteAdditionalData(AdditionalData);
}
}
}
#pragma warning restore CS0618
Loading
Loading