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.0 enable enable + true + Default service-worker-assets.js CleanAspire.ClientApp CleanAspire.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