diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..fe1152b
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,30 @@
+**/.classpath
+**/.dockerignore
+**/.env
+**/.git
+**/.gitignore
+**/.project
+**/.settings
+**/.toolstarget
+**/.vs
+**/.vscode
+**/*.*proj.user
+**/*.dbmdl
+**/*.jfm
+**/azds.yaml
+**/bin
+**/charts
+**/docker-compose*
+**/Dockerfile*
+**/node_modules
+**/npm-debug.log
+**/obj
+**/secrets.dev.yaml
+**/values.dev.yaml
+LICENSE
+README.md
+!**/.gitignore
+!.git/HEAD
+!.git/config
+!.git/packed-refs
+!.git/refs/heads/**
\ No newline at end of file
diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml
index e09d1e6..0edcb7a 100644
--- a/.github/workflows/docker.yml
+++ b/.github/workflows/docker.yml
@@ -32,10 +32,10 @@ jobs:
password: ${{ secrets.DOCKER_PASSWORD }}
- - name: Build and push CleanAspire.ClientApp image
+ - name: Build and push CleanAspire.WebApp image
run: |
- 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 }}
+ docker build -t ${{ secrets.DOCKER_USERNAME }}/cleanaspire-webapp:${{ steps.version.outputs.version }} -f src/CleanAspire.WebApp/Dockerfile .
+ docker push ${{ secrets.DOCKER_USERNAME }}/cleanaspire-webapp:${{ steps.version.outputs.version }}
- name: Build and push CleanAspire.Api image
run: |
diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml
index 371c628..1ee9a8e 100644
--- a/.github/workflows/dotnet.yml
+++ b/.github/workflows/dotnet.yml
@@ -12,11 +12,20 @@ jobs:
runs-on: ubuntu-latest
steps:
+ # Install CA certificates to ensure SSL trust
+ - name: Install CA Certificates
+ run: sudo apt-get update && sudo apt-get install -y ca-certificates
+
- uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 9.0.x
+
+ # Trust the ASP.NET Core development certificate
+ - name: Trust ASP.NET Core HTTPS Development Certificate
+ run: dotnet dev-certs https --trust
+
- name: Restore dependencies
run: dotnet restore CleanAspire.sln
- name: Build
diff --git a/CleanAspire.sln b/CleanAspire.sln
index 9ae4386..094f84b 100644
--- a/CleanAspire.sln
+++ b/CleanAspire.sln
@@ -35,6 +35,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{0AB3BF05
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CleanAspire.Tests", "tests\CleanAspire.Tests\CleanAspire.Tests.csproj", "{184DD222-E87D-65E3-4E4F-ADC8680E2D81}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CleanAspire.WebApp", "src\CleanAspire.WebApp\CleanAspire.WebApp.csproj", "{E29307F2-485B-47B4-9CA7-A7EA6949134B}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -85,6 +87,10 @@ Global
{184DD222-E87D-65E3-4E4F-ADC8680E2D81}.Debug|Any CPU.Build.0 = Debug|Any CPU
{184DD222-E87D-65E3-4E4F-ADC8680E2D81}.Release|Any CPU.ActiveCfg = Release|Any CPU
{184DD222-E87D-65E3-4E4F-ADC8680E2D81}.Release|Any CPU.Build.0 = Release|Any CPU
+ {E29307F2-485B-47B4-9CA7-A7EA6949134B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {E29307F2-485B-47B4-9CA7-A7EA6949134B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {E29307F2-485B-47B4-9CA7-A7EA6949134B}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {E29307F2-485B-47B4-9CA7-A7EA6949134B}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -102,6 +108,7 @@ Global
{7D6E47CC-90F1-4C19-AF96-1DE7EED7928C} = {C983071D-42D7-4326-A379-CE622E29D307}
{67226F2C-5D75-4B76-B5F8-E42A4BC1BBB1} = {C983071D-42D7-4326-A379-CE622E29D307}
{184DD222-E87D-65E3-4E4F-ADC8680E2D81} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
+ {E29307F2-485B-47B4-9CA7-A7EA6949134B} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {C379C278-2AFA-4DD5-96F5-34D17AAE1188}
diff --git a/README.md b/README.md
index 3214ddc..ef4ff1b 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.50
+ image: blazordevlab/cleanaspire-api:0.0.51
environment:
- ASPNETCORE_ENVIRONMENT=Development
- AllowedHosts=*
@@ -107,11 +107,17 @@ services:
- "8018:443"
- webfrontend:
- image: blazordevlab/cleanaspire-clientapp:0.0.50
+ blazorweb:
+ image: blazordevlab/cleanaspire-webapp:0.0.51
+ environment:
+ - ASPNETCORE_ENVIRONMENT=Production
+ - AllowedHosts=*
+ - ASPNETCORE_URLS=http://+:80;https://+:443
+ - ASPNETCORE_HTTP_PORTS=80
+ - ASPNETCORE_HTTPS_PORTS=443
ports:
- - "8016:80"
- - "8017:443"
+ - "8015:80"
+ - "8014:443"
diff --git a/src/CleanAspire.Api/CleanAspire.Api.csproj b/src/CleanAspire.Api/CleanAspire.Api.csproj
index 187a1f4..5b72650 100644
--- a/src/CleanAspire.Api/CleanAspire.Api.csproj
+++ b/src/CleanAspire.Api/CleanAspire.Api.csproj
@@ -16,7 +16,7 @@
-
+
diff --git a/src/CleanAspire.Api/appsettings.json b/src/CleanAspire.Api/appsettings.json
index 1e2bc1b..47f17a8 100644
--- a/src/CleanAspire.Api/appsettings.json
+++ b/src/CleanAspire.Api/appsettings.json
@@ -6,7 +6,7 @@
"Microsoft.AspNetCore": "Warning"
}
},
- "AllowedCorsOrigins": "https://localhost:7341,https://localhost:7123",
+ "AllowedCorsOrigins": "https://localhost:7341,https://localhost:7123,https://localhost:7114",
"ClientBaseUrl": "https://localhost:7123",
"DatabaseSettings": {
"DBProvider": "sqlite",
diff --git a/src/CleanAspire.AppHost/CleanAspire.AppHost.csproj b/src/CleanAspire.AppHost/CleanAspire.AppHost.csproj
index 9438a76..8443b97 100644
--- a/src/CleanAspire.AppHost/CleanAspire.AppHost.csproj
+++ b/src/CleanAspire.AppHost/CleanAspire.AppHost.csproj
@@ -1,4 +1,4 @@
-
+
@@ -15,8 +15,8 @@
+
-
diff --git a/src/CleanAspire.AppHost/Program.cs b/src/CleanAspire.AppHost/Program.cs
index f508ce0..c29c626 100644
--- a/src/CleanAspire.AppHost/Program.cs
+++ b/src/CleanAspire.AppHost/Program.cs
@@ -2,7 +2,7 @@
var apiService = builder.AddProject("apiservice");
-builder.AddProject("webfrontend")
+builder.AddProject("blazorweb")
.WithExternalHttpEndpoints()
.WithReference(apiService)
.WaitFor(apiService);
diff --git a/src/CleanAspire.ClientApp/CleanAspire.ClientApp.csproj b/src/CleanAspire.ClientApp/CleanAspire.ClientApp.csproj
index 62198d0..c79c91b 100644
--- a/src/CleanAspire.ClientApp/CleanAspire.ClientApp.csproj
+++ b/src/CleanAspire.ClientApp/CleanAspire.ClientApp.csproj
@@ -4,6 +4,8 @@
net9.0enableenable
+ true
+ Defaultservice-worker-assets.jsCleanAspire.ClientAppCleanAspire.ClientApp
diff --git a/src/CleanAspire.ClientApp/DependencyInjection.cs b/src/CleanAspire.ClientApp/DependencyInjection.cs
index 2b60731..c01e3fe 100644
--- a/src/CleanAspire.ClientApp/DependencyInjection.cs
+++ b/src/CleanAspire.ClientApp/DependencyInjection.cs
@@ -10,6 +10,39 @@ namespace CleanAspire.ClientApp;
public static class DependencyInjection
{
+ public static void TryAddScopedMudBlazor(this IServiceCollection services, IConfiguration config)
+ {
+ #region register MudBlazor.Services
+ services.AddMudServices(config =>
+ {
+ MudGlobal.InputDefaults.ShrinkLabel = true;
+ config.SnackbarConfiguration.PositionClass = Defaults.Classes.Position.BottomCenter;
+ config.SnackbarConfiguration.NewestOnTop = false;
+ config.SnackbarConfiguration.ShowCloseIcon = true;
+ config.SnackbarConfiguration.VisibleStateDuration = 3000;
+ config.SnackbarConfiguration.HideTransitionDuration = 500;
+ config.SnackbarConfiguration.ShowTransitionDuration = 500;
+ config.SnackbarConfiguration.SnackbarVariant = Variant.Filled;
+
+ // we're currently planning on deprecating `PreventDuplicates`, at least to the end dev. however,
+ // we may end up wanting to instead set it as internal because the docs project relies on it
+ // to ensure that the Snackbar always allows duplicates. disabling the warning for now because
+ // the project is set to treat warnings as errors.
+#pragma warning disable 0618
+ config.SnackbarConfiguration.PreventDuplicates = false;
+#pragma warning restore 0618
+ });
+ services.AddMudPopoverService();
+ services.AddMudBlazorSnackbar();
+ services.AddMudBlazorDialog();
+ services.AddMudLocalization();
+ services.AddBlazoredLocalStorage();
+ services.AddScoped();
+ services.AddScoped();
+ services.AddScoped();
+ services.AddScoped();
+ #endregion
+ }
public static void TryAddMudBlazor(this IServiceCollection services, IConfiguration config)
{
#region register MudBlazor.Services
diff --git a/src/CleanAspire.ClientApp/Dockerfile b/src/CleanAspire.ClientApp/Dockerfile
deleted file mode 100644
index f54f7bd..0000000
--- a/src/CleanAspire.ClientApp/Dockerfile
+++ /dev/null
@@ -1,42 +0,0 @@
-# Stage 1: Build the Blazor Client Application
-FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
-WORKDIR /src
-
-# Install Python for AOT compilation
-RUN apt-get update && apt-get install -y python3 python3-pip && ln -s /usr/bin/python3 /usr/bin/python
-
-# Copy the project files and restore dependencies
-COPY ["src/CleanAspire.ClientApp/CleanAspire.ClientApp.csproj", "src/CleanAspire.ClientApp/"]
-RUN dotnet restore "src/CleanAspire.ClientApp/CleanAspire.ClientApp.csproj"
-
-# Install wasm-tools for AOT
-RUN dotnet workload install wasm-tools --skip-manifest-update
-RUN dotnet workload update
-
-# Copy the entire source code and build the application in Release mode
-COPY . .
-RUN dotnet publish -c Release -o /app/publish
-
-# Stage 2: Serve the Blazor Client Application using Nginx
-FROM nginx:alpine AS final
-WORKDIR /usr/share/nginx/html
-
-# Install OpenSSL to create a self-signed certificate
-RUN apk add --no-cache openssl && \
- openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /etc/ssl/private/nginx-selfsigned.key -out /etc/ssl/certs/nginx-selfsigned.crt -subj "/CN=localhost"
-
-# Clean the default nginx content
-RUN rm -rf ./*
-
-# Copy the build output from the previous stage
-COPY --from=build /app/publish/wwwroot .
-
-# Copy the generated self-signed certificate and configure Nginx for HTTPS
-COPY src/CleanAspire.ClientApp/nginx.conf /etc/nginx/nginx.conf
-
-# Expose port 80 for HTTP traffic and 443 for HTTPS traffic
-EXPOSE 80
-EXPOSE 443
-
-# Start Nginx
-CMD ["nginx", "-g", "daemon off;"]
diff --git a/src/CleanAspire.ClientApp/Layout/MainLayout.razor b/src/CleanAspire.ClientApp/Layout/MainLayout.razor
index 0f13f01..d7f5d2e 100644
--- a/src/CleanAspire.ClientApp/Layout/MainLayout.razor
+++ b/src/CleanAspire.ClientApp/Layout/MainLayout.razor
@@ -21,8 +21,8 @@
{
LayoutService.MajorUpdateOccurred += LayoutServiceOnMajorUpdateOccured;
}
- OnlineStatusInterop.Initialize();
- await OfflineModeState.InitializeAsync();
+
+
}
protected override async Task OnAfterRenderAsync(bool firstRender)
@@ -31,6 +31,8 @@
if (firstRender)
{
+ OnlineStatusInterop.Initialize();
+ await OfflineModeState.InitializeAsync();
await ApplyUserPreferences();
if (_mudThemeProvider != null)
{
diff --git a/src/CleanAspire.ClientApp/Program.cs b/src/CleanAspire.ClientApp/Program.cs
index 1b442f3..c281006 100644
--- a/src/CleanAspire.ClientApp/Program.cs
+++ b/src/CleanAspire.ClientApp/Program.cs
@@ -18,8 +18,8 @@
var builder = WebAssemblyHostBuilder.CreateDefault(args);
-builder.RootComponents.Add("#app");
-builder.RootComponents.Add("head::after");
+//builder.RootComponents.Add("#app");
+//builder.RootComponents.Add("head::after");
// register the cookie handler
builder.Services.AddTransient();
@@ -81,10 +81,10 @@
builder.Configuration.Bind("Local", options.ProviderOptions);
});
// register the custom state provider
-builder.Services.AddScoped();
+builder.Services.AddSingleton();
// register the account management interface
-builder.Services.AddScoped(
+builder.Services.AddSingleton(
sp => (ISignInManagement)sp.GetRequiredService());
diff --git a/src/CleanAspire.ClientApp/Routes.razor b/src/CleanAspire.ClientApp/Routes.razor
new file mode 100644
index 0000000..6fd3ed1
--- /dev/null
+++ b/src/CleanAspire.ClientApp/Routes.razor
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+ Not found
+
+
Sorry, there's nothing at this address.
+
+
+
diff --git a/src/CleanAspire.ClientApp/Services/Identity/CookieAuthenticationStateProvider.cs b/src/CleanAspire.ClientApp/Services/Identity/CookieAuthenticationStateProvider.cs
index 475d8eb..be41576 100644
--- a/src/CleanAspire.ClientApp/Services/Identity/CookieAuthenticationStateProvider.cs
+++ b/src/CleanAspire.ClientApp/Services/Identity/CookieAuthenticationStateProvider.cs
@@ -25,13 +25,13 @@ public override async Task GetAuthenticationStateAsync()
var indexedDb = serviceProvider.GetRequiredService();
var onlineStatusInterop = serviceProvider.GetRequiredService();
var offlineState = serviceProvider.GetRequiredService();
- bool enableOffline = offlineState.Enabled;
authenticated = false;
// default to not authenticated
var user = unauthenticated;
ProfileResponse? profileResponse = null;
try
{
+ bool enableOffline = offlineState.Enabled;
var isOnline = await onlineStatusInterop.GetOnlineStatusAsync();
if (isOnline)
{
@@ -78,9 +78,9 @@ public async Task LoginAsync(LoginRequest request, bool remember = true, Cancell
var indexedDb = serviceProvider.GetRequiredService();
var onlineStatusInterop = serviceProvider.GetRequiredService();
var offlineState = serviceProvider.GetRequiredService();
- bool offlineModel = offlineState.Enabled;
try
{
+ bool offlineModel = offlineState.Enabled;
var isOnline = await onlineStatusInterop.GetOnlineStatusAsync();
if (isOnline)
{
diff --git a/src/CleanAspire.ClientApp/nginx.conf b/src/CleanAspire.ClientApp/nginx.conf
deleted file mode 100644
index 213d968..0000000
--- a/src/CleanAspire.ClientApp/nginx.conf
+++ /dev/null
@@ -1,31 +0,0 @@
-worker_processes auto;
-
-events {
- worker_connections 1024;
-}
-
-http {
- include /etc/nginx/mime.types;
- default_type application/octet-stream;
-
- # Define a server block here
- server {
- listen 80;
- listen 443 ssl;
-
- ssl_certificate /etc/ssl/certs/nginx-selfsigned.crt;
- ssl_certificate_key /etc/ssl/private/nginx-selfsigned.key;
-
- server_name localhost;
-
- location / {
- root /usr/share/nginx/html;
- index index.html index.htm;
- try_files $uri $uri/ /index.html;
- }
-
- error_page 404 /404.html;
- location = /40x.html {
- }
- }
-}
diff --git a/src/CleanAspire.ClientApp/wwwroot/appsettings.Development.json b/src/CleanAspire.ClientApp/wwwroot/appsettings.Development.json
index fa07868..b4ab680 100644
--- a/src/CleanAspire.ClientApp/wwwroot/appsettings.Development.json
+++ b/src/CleanAspire.ClientApp/wwwroot/appsettings.Development.json
@@ -7,7 +7,7 @@
},
"ClientAppSettings": {
"AppName": "Progressive Web Application",
- "Version": "v0.0.50",
+ "Version": "v0.0.51",
"ServiceBaseUrl": "https://localhost:7341"
}
}
diff --git a/src/CleanAspire.ClientApp/wwwroot/appsettings.json b/src/CleanAspire.ClientApp/wwwroot/appsettings.json
index 89d5b6b..2349092 100644
--- a/src/CleanAspire.ClientApp/wwwroot/appsettings.json
+++ b/src/CleanAspire.ClientApp/wwwroot/appsettings.json
@@ -7,7 +7,7 @@
},
"ClientAppSettings": {
"AppName": "Progressive Web Application",
- "Version": "v0.0.50",
+ "Version": "v0.0.51",
"ServiceBaseUrl": "https://apiservice.blazorserver.com"
}
}
diff --git a/src/CleanAspire.ClientApp/wwwroot/index.html b/src/CleanAspire.ClientApp/wwwroot/index.html
index 8758b71..94ac834 100644
--- a/src/CleanAspire.ClientApp/wwwroot/index.html
+++ b/src/CleanAspire.ClientApp/wwwroot/index.html
@@ -8,7 +8,6 @@
-
diff --git a/src/CleanAspire.ClientApp/wwwroot/service-worker.published.js b/src/CleanAspire.ClientApp/wwwroot/service-worker.published.js
index aae59f0..4cd9291 100644
--- a/src/CleanAspire.ClientApp/wwwroot/service-worker.published.js
+++ b/src/CleanAspire.ClientApp/wwwroot/service-worker.published.js
@@ -24,6 +24,11 @@ async function onInstall(event) {
.filter(asset => offlineAssetsInclude.some(pattern => pattern.test(asset.url)))
.filter(asset => !offlineAssetsExclude.some(pattern => pattern.test(asset.url)))
.map(asset => new Request(asset.url, { integrity: asset.hash, cache: 'no-cache' }));
+
+ // Also cache the host HTML and blazor.web.js
+ assetsRequests.push(new Request(baseUrl, { cache: 'no-cache' }));
+ assetsRequests.push(new Request(new URL('_framework/blazor.web.js', baseUrl).href, { cache: 'no-cache' }));
+
await caches.open(cacheName).then(cache => cache.addAll(assetsRequests));
}
@@ -40,13 +45,13 @@ async function onActivate(event) {
async function onFetch(event) {
let cachedResponse = null;
if (event.request.method === 'GET') {
- // For all navigation requests, try to serve index.html from cache,
+ // For all navigation requests, try to serve the host HTML from cache,
// unless that request is for an offline resource.
// If you need some URLs to be server-rendered, edit the following check to exclude those URLs
- const shouldServeIndexHtml = event.request.mode === 'navigate'
+ const shouldServeHostHtml = event.request.mode === 'navigate'
&& !manifestUrlList.some(url => url === event.request.url);
- const request = shouldServeIndexHtml ? 'index.html' : event.request;
+ const request = shouldServeHostHtml ? baseUrl : event.request;
const cache = await caches.open(cacheName);
cachedResponse = await cache.match(request);
}
diff --git a/src/CleanAspire.WebApp/CleanAspire.WebApp.csproj b/src/CleanAspire.WebApp/CleanAspire.WebApp.csproj
new file mode 100644
index 0000000..e6786b3
--- /dev/null
+++ b/src/CleanAspire.WebApp/CleanAspire.WebApp.csproj
@@ -0,0 +1,22 @@
+
+
+
+ net9.0
+ enable
+ enable
+ aa04e12f-2328-4d88-a3b5-5b0dfc063bbe
+ Linux
+ ..\..
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/CleanAspire.WebApp/Components/App.razor b/src/CleanAspire.WebApp/Components/App.razor
new file mode 100644
index 0000000..d5cd9a0
--- /dev/null
+++ b/src/CleanAspire.WebApp/Components/App.razor
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/CleanAspire.WebApp/Components/Pages/Error.razor b/src/CleanAspire.WebApp/Components/Pages/Error.razor
new file mode 100644
index 0000000..576cc2d
--- /dev/null
+++ b/src/CleanAspire.WebApp/Components/Pages/Error.razor
@@ -0,0 +1,36 @@
+@page "/Error"
+@using System.Diagnostics
+
+Error
+
+
Error.
+
An error occurred while processing your request.
+
+@if (ShowRequestId)
+{
+
+ Request ID:@RequestId
+
+}
+
+
Development Mode
+
+ Swapping to Development environment will display more detailed information about the error that occurred.
+
+
+ The Development environment shouldn't be enabled for deployed applications.
+ It can result in displaying sensitive information from exceptions to end users.
+ For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development
+ and restarting the app.
+
+
+@code{
+ [CascadingParameter]
+ private HttpContext? HttpContext { get; set; }
+
+ private string? RequestId { get; set; }
+ private bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
+
+ protected override void OnInitialized() =>
+ RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier;
+}
diff --git a/src/CleanAspire.WebApp/Components/_Imports.razor b/src/CleanAspire.WebApp/Components/_Imports.razor
new file mode 100644
index 0000000..b5146a8
--- /dev/null
+++ b/src/CleanAspire.WebApp/Components/_Imports.razor
@@ -0,0 +1,11 @@
+@using System.Net.Http
+@using System.Net.Http.Json
+@using Microsoft.AspNetCore.Components.Forms
+@using Microsoft.AspNetCore.Components.Routing
+@using Microsoft.AspNetCore.Components.Web
+@using static Microsoft.AspNetCore.Components.Web.RenderMode
+@using Microsoft.AspNetCore.Components.Web.Virtualization
+@using Microsoft.JSInterop
+@using CleanAspire.WebApp
+@using CleanAspire.ClientApp
+@using CleanAspire.WebApp.Components
diff --git a/src/CleanAspire.WebApp/Dockerfile b/src/CleanAspire.WebApp/Dockerfile
new file mode 100644
index 0000000..823d083
--- /dev/null
+++ b/src/CleanAspire.WebApp/Dockerfile
@@ -0,0 +1,54 @@
+# See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging.
+
+# This stage is used to build the service project
+FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
+ARG BUILD_CONFIGURATION=Release
+WORKDIR /src
+COPY ["src/CleanAspire.WebApp/CleanAspire.WebApp.csproj", "src/CleanAspire.WebApp/"]
+COPY ["src/CleanAspire.ClientApp/CleanAspire.ClientApp.csproj", "src/CleanAspire.ClientApp/"]
+COPY ["src/CleanAspire.ServiceDefaults/CleanAspire.ServiceDefaults.csproj", "src/CleanAspire.ServiceDefaults/"]
+RUN dotnet restore "./src/CleanAspire.WebApp/CleanAspire.WebApp.csproj"
+COPY . .
+WORKDIR "/src/src/CleanAspire.WebApp"
+RUN dotnet build "./CleanAspire.WebApp.csproj" -c $BUILD_CONFIGURATION -o /app/build
+
+# This stage is used to publish the service project to be copied to the final stage
+FROM build AS publish
+ARG BUILD_CONFIGURATION=Release
+RUN dotnet publish "./CleanAspire.WebApp.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
+
+# This stage is used in production or when running from VS in regular mode (Default when not using the Debug configuration)
+FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS final
+WORKDIR /app
+COPY --from=publish /app/publish .
+
+# Install OpenSSL
+RUN apt-get update && apt-get install -y openssl
+
+# Generate a self-signed certificate
+RUN mkdir -p /app/https && \
+ openssl req -x509 -newkey rsa:4096 -sha256 -days 3650 -nodes \
+ -keyout /app/https/private.key -out /app/https/certificate.crt \
+ -subj "/C=US/ST=State/L=City/O=Organization/CN=localhost" && \
+ openssl pkcs12 -export -out /app/https/aspnetapp.pfx \
+ -inkey /app/https/private.key -in /app/https/certificate.crt \
+ -password pass:CREDENTIAL_PLACEHOLDER
+
+
+# Setup environment variables for the application to find the certificate
+ENV ASPNETCORE_URLS=http://+:80;https://+:443
+ENV ASPNETCORE_Kestrel__Certificates__Default__Password="CREDENTIAL_PLACEHOLDER"
+ENV ASPNETCORE_Kestrel__Certificates__Default__Path="/app/https/aspnetapp.pfx"
+
+
+# Expose ports
+EXPOSE 80 443
+
+# Set the environment variable for ASP.NET Core to use Production settings
+ENV ASPNETCORE_ENVIRONMENT=Production
+
+# Enable Service Worker for PWA Installation
+# COPY src/CleanAspire.ClientApp/wwwroot/service-worker.published.js wwwroot/service-worker.js
+# COPY src/CleanAspire.ClientApp/wwwroot/manifest.json wwwroot/manifest.json
+
+ENTRYPOINT ["dotnet", "CleanAspire.WebApp.dll"]
\ No newline at end of file
diff --git a/src/CleanAspire.WebApp/Program.cs b/src/CleanAspire.WebApp/Program.cs
new file mode 100644
index 0000000..b2808eb
--- /dev/null
+++ b/src/CleanAspire.WebApp/Program.cs
@@ -0,0 +1,128 @@
+// 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 CleanAspire.Api.Client;
+using CleanAspire.ClientApp.Configurations;
+using CleanAspire.ClientApp.Services;
+using CleanAspire.ClientApp.Services.Identity;
+using CleanAspire.ClientApp.Services.JsInterop;
+using CleanAspire.ClientApp.Services.Proxies;
+using Microsoft.AspNetCore.Components.Authorization;
+using Microsoft.Kiota.Abstractions;
+using Microsoft.Kiota.Abstractions.Authentication;
+using Microsoft.Kiota.Http.HttpClientLibrary;
+using Microsoft.Kiota.Serialization.Form;
+using Microsoft.Kiota.Serialization.Json;
+using Microsoft.Kiota.Serialization.Multipart;
+using Microsoft.Kiota.Serialization.Text;
+using CleanAspire.ClientApp;
+
+
+var builder = WebApplication.CreateBuilder(args);
+builder.AddServiceDefaults();
+
+
+
+// Add services to the container.
+builder.Services.AddRazorComponents()
+ .AddInteractiveServerComponents()
+ .AddInteractiveWebAssemblyComponents();
+
+
+
+// register the cookie handler
+builder.Services.AddTransient();
+builder.Services.AddTransient();
+builder.Services.AddScoped();
+builder.Services.AddScoped();
+builder.Services.AddScoped();
+builder.Services.AddScoped();
+builder.Services.AddScoped();
+builder.Services.AddScoped();
+
+var clientAppSettings = builder.Configuration.GetSection(ClientAppSettings.KEY).Get();
+builder.Services.AddSingleton(clientAppSettings!);
+
+builder.Services.TryAddScopedMudBlazor(builder.Configuration);
+
+var httpClientBuilder = builder.Services.AddHttpClient("apiservice", (sp, options) =>
+{
+ var settings = sp.GetRequiredService();
+ options.BaseAddress = new Uri(settings.ServiceBaseUrl);
+
+}).AddHttpMessageHandler();
+
+builder.Services.AddScoped(sp =>
+{
+ ApiClientBuilder.RegisterDefaultSerializer();
+ ApiClientBuilder.RegisterDefaultSerializer();
+ ApiClientBuilder.RegisterDefaultSerializer();
+ ApiClientBuilder.RegisterDefaultSerializer();
+ ApiClientBuilder.RegisterDefaultDeserializer();
+ ApiClientBuilder.RegisterDefaultDeserializer();
+ ApiClientBuilder.RegisterDefaultDeserializer();
+ var settings = sp.GetRequiredService();
+ var httpClientFactory = sp.GetRequiredService();
+ var httpClient = httpClientFactory.CreateClient("apiservice");
+ var authProvider = new AnonymousAuthenticationProvider();
+ var requestAdapter = new HttpClientRequestAdapter(authProvider, httpClient: httpClient);
+ var apiClient = new ApiClient(requestAdapter);
+ if (!string.IsNullOrEmpty(settings.ServiceBaseUrl))
+ {
+ requestAdapter.BaseUrl = settings.ServiceBaseUrl;
+ }
+ return apiClient;
+
+});
+builder.Services.AddHttpClient("Webpushr", client =>
+{
+ client.BaseAddress = new Uri("https://api.webpushr.com");
+}).AddHttpMessageHandler();
+builder.Services.AddScoped();
+
+builder.Services.AddScoped();
+builder.Services.AddAuthorizationCore();
+builder.Services.AddCascadingAuthenticationState();
+builder.Services.AddOidcAuthentication(options =>
+{
+ // Configure your authentication provider options here.
+ // For more information, see https://aka.ms/blazor-standalone-auth
+ builder.Configuration.Bind("Local", options.ProviderOptions);
+});
+// register the custom state provider
+builder.Services.AddScoped();
+
+// register the account management interface
+builder.Services.AddScoped(
+ sp => (ISignInManagement)sp.GetRequiredService());
+
+
+builder.Services.AddLocalization(options => options.ResourcesPath = "Resources");
+
+var app = builder.Build();
+
+// Configure the HTTP request pipeline.
+if (app.Environment.IsDevelopment())
+{
+ app.UseWebAssemblyDebugging();
+}
+else
+{
+ app.UseExceptionHandler("/Error", createScopeForErrors: true);
+ // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
+ app.UseHsts();
+}
+
+app.UseHttpsRedirection();
+
+
+app.UseAntiforgery();
+
+app.MapStaticAssets();
+app.MapRazorComponents()
+ .AddInteractiveServerRenderMode()
+ .AddInteractiveWebAssemblyRenderMode()
+ .AddAdditionalAssemblies(typeof(CleanAspire.ClientApp._Imports).Assembly);
+
+app.Run();
diff --git a/src/CleanAspire.WebApp/Properties/launchSettings.json b/src/CleanAspire.WebApp/Properties/launchSettings.json
new file mode 100644
index 0000000..637b835
--- /dev/null
+++ b/src/CleanAspire.WebApp/Properties/launchSettings.json
@@ -0,0 +1,36 @@
+{
+ "profiles": {
+ "http": {
+ "commandName": "Project",
+ "launchBrowser": true,
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ },
+ "dotnetRunMessages": true,
+ "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
+ "applicationUrl": "http://localhost:5252"
+ },
+ "https": {
+ "commandName": "Project",
+ "launchBrowser": true,
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ },
+ "dotnetRunMessages": true,
+ "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
+ "applicationUrl": "https://localhost:7114;http://localhost:5252"
+ },
+ "Container (Dockerfile)": {
+ "commandName": "Docker",
+ "launchBrowser": true,
+ "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}",
+ "environmentVariables": {
+ "ASPNETCORE_HTTPS_PORTS": "8081",
+ "ASPNETCORE_HTTP_PORTS": "8080"
+ },
+ "publishAllPorts": true,
+ "useSSL": true
+ }
+ },
+ "$schema": "https://json.schemastore.org/launchsettings.json"
+}
\ No newline at end of file
diff --git a/src/CleanAspire.WebApp/appsettings.Development.json b/src/CleanAspire.WebApp/appsettings.Development.json
new file mode 100644
index 0000000..b4ab680
--- /dev/null
+++ b/src/CleanAspire.WebApp/appsettings.Development.json
@@ -0,0 +1,13 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "ClientAppSettings": {
+ "AppName": "Progressive Web Application",
+ "Version": "v0.0.51",
+ "ServiceBaseUrl": "https://localhost:7341"
+ }
+}
diff --git a/src/CleanAspire.WebApp/appsettings.json b/src/CleanAspire.WebApp/appsettings.json
new file mode 100644
index 0000000..7bcc835
--- /dev/null
+++ b/src/CleanAspire.WebApp/appsettings.json
@@ -0,0 +1,14 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*",
+ "ClientAppSettings": {
+ "AppName": "Progressive Web Application",
+ "Version": "v0.0.51",
+ "ServiceBaseUrl": "https://apiservice.blazorserver.com"
+ }
+}
diff --git a/src/CleanAspire.WebApp/wwwroot/app.css b/src/CleanAspire.WebApp/wwwroot/app.css
new file mode 100644
index 0000000..771f127
--- /dev/null
+++ b/src/CleanAspire.WebApp/wwwroot/app.css
@@ -0,0 +1,39 @@
+
+h1:focus {
+ outline: none;
+}
+
+.valid.modified:not([type=checkbox]) {
+ outline: 1px solid #26b050;
+}
+
+.invalid {
+ outline: 1px solid #e50000;
+}
+
+.validation-message {
+ color: #e50000;
+}
+
+.blazor-error-boundary {
+ background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
+ padding: 1rem 1rem 1rem 3.7rem;
+ color: white;
+}
+
+ .blazor-error-boundary::after {
+ content: "An error has occurred."
+ }
+
+.darker-border-checkbox.form-check-input {
+ border-color: #929292;
+}
+
+.form-floating > .form-control-plaintext::placeholder, .form-floating > .form-control::placeholder {
+ color: var(--bs-secondary-color);
+ text-align: end;
+}
+
+.form-floating > .form-control-plaintext:focus::placeholder, .form-floating > .form-control:focus::placeholder {
+ text-align: start;
+}
\ No newline at end of file
diff --git a/tests/CleanAspire.Tests/WebTests.cs b/tests/CleanAspire.Tests/WebTests.cs
index 7e01632..94d257c 100644
--- a/tests/CleanAspire.Tests/WebTests.cs
+++ b/tests/CleanAspire.Tests/WebTests.cs
@@ -1,4 +1,4 @@
-namespace CleanAspire.Tests;
+namespace CleanAspire.Tests;
public class WebTests
{
@@ -17,8 +17,8 @@ public async Task GetWebResourceRootReturnsOkStatusCode()
await app.StartAsync();
// Act
- var httpClient = app.CreateHttpClient("webfrontend");
- await resourceNotificationService.WaitForResourceAsync("webfrontend", KnownResourceStates.Running).WaitAsync(TimeSpan.FromSeconds(30));
+ var httpClient = app.CreateHttpClient("blazorweb");
+ await resourceNotificationService.WaitForResourceAsync("blazorweb", KnownResourceStates.Running).WaitAsync(TimeSpan.FromSeconds(30));
var response = await httpClient.GetAsync("/");
// Assert