Skip to content
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
118 changes: 64 additions & 54 deletions .github/workflows/pullrequest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@
branches:
- main

permissions:
contents: read
checks: write

jobs:
unittests:
name: "Build and Unit Test"
Expand Down Expand Up @@ -48,61 +52,67 @@
name: test-results
path: '**/TestResults/*.trx'
retention-days: 30


# chrometests:
# name: "Build and Test UI - Chrome"
# env:
# ASPNETCORE_ENVIRONMENT: "Production"
# NODE_VERSION: '14.x' # set this to the node version to use

# runs-on: ubuntu-latest

# steps:
# - uses: actions/checkout@v2.3.4

# - name: Trust Certificate
# run: |
# sudo apt-get install expect
# certPath="$GITHUB_WORKSPACE/Certificates/aspnetapp-root-cert.pfx"
# password="password"

# # Use expect to automate the passphrase input
# expect -c "
# spawn openssl pkcs12 -in \"$certPath\" -clcerts -nokeys -out certificate.crt -password pass:\"$password\"
# expect \"Enter Import Password:\" { send \"$password\n\" }
# expect eof
# "

# expect -c "
# spawn openssl pkcs12 -in \"$certPath\" -nocerts -out private.key -password pass:\"$password\"
# expect \"Enter Import Password:\" { send \"$password\n\" }
# expect eof
# "

# sudo cp certificate.crt /usr/local/share/ca-certificates/aspnetapp-root-cert.crt
# sudo update-ca-certificates

# sudo cp private.key /etc/ssl/private/aspnetapp-root-cert.key
# sudo chmod 400 /etc/ssl/private/aspnetapp-root-cert.key

chrometests:
name: "Build and Test UI - Chrome"
env:
ASPNETCORE_ENVIRONMENT: "Production"
NODE_VERSION: '14.x' # set this to the node version to use

runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v2.3.4

- name: Trust Certificate
run: |
sudo apt-get install expect
certPath="$GITHUB_WORKSPACE/Certificates/aspnetapp-root-cert.pfx"
password="password"

Check failure on line 71 in .github/workflows/pullrequest.yml

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

.github/workflows/pullrequest.yml#L71

A gitleaks hashicorp-tf-password was detected which attempts to identify hard-coded credentials.

# Use expect to automate the passphrase input
expect -c "
spawn openssl pkcs12 -in \"$certPath\" -clcerts -nokeys -out certificate.crt -password pass:\"$password\"
expect \"Enter Import Password:\" { send \"$password\n\" }
expect eof
"

expect -c "
spawn openssl pkcs12 -in \"$certPath\" -nocerts -out private.key -password pass:\"$password\"
expect \"Enter Import Password:\" { send \"$password\n\" }
expect eof
"

sudo cp certificate.crt /usr/local/share/ca-certificates/aspnetapp-root-cert.crt
sudo update-ca-certificates

sudo cp private.key /etc/ssl/private/aspnetapp-root-cert.key
sudo chmod 400 /etc/ssl/private/aspnetapp-root-cert.key

# - name: Build Docker Image
# run: docker build . --file EstateManagementUI/Dockerfile --tag estatemanagementui:latest

# - name: Restore Nuget Packages
# run: dotnet restore EstateManagementUI.sln --source ${{ secrets.PUBLICFEEDURL }} --source ${{ secrets.PRIVATEFEED_URL }}

# - name: Run Integration Tests (Chrome)
# env:
# Browser: Chrome
# IsCI: true
# run: dotnet test "EstateManagementUI.IntegrationTests\EstateManagementUI.IntegrationTests.csproj" --filter Category=PRTest
- name: Build Docker Image
run: docker build . --file EstateManagementUI.BlazorServer/Dockerfile --tag estatemanagementuiblazorserver:latest

- name: Restore Nuget Packages
run: dotnet restore EstateManagementUI.sln --source ${{ secrets.PUBLICFEEDURL }} --source ${{ secrets.PRIVATEFEED_URL }}

- name: Run Integration Tests (Chrome)
env:
Browser: Chrome
IsCI: true
run: dotnet test "EstateManagementUI.IntegrationTests\EstateManagementUI.IntegrationTests.csproj" # --filter Category=PRTest

- name: Upload Playwright screenshots on failure
uses: actions/upload-artifact@v4.4.0
if: ${{ failure() }}
with:
name: chrometestresults
path: '**/TestResults/**'

# - uses: actions/upload-artifact@v4.4.0
# if: ${{ failure() }}
# with:
# name: chrometracelogs
# path: /home/txnproc/trace/
- uses: actions/upload-artifact@v4.4.0
if: ${{ failure() }}
with:
name: chrometracelogs
path: /home/txnproc/trace/

# edgetests:
# name: "Build and Test UI - Edge"
Expand Down Expand Up @@ -210,4 +220,4 @@
# if: ${{ failure() }}
# with:
# name: firefoxtracelogs
# path: /home/txnproc/trace/
# path: /home/txnproc/trace/
File renamed without changes.
16 changes: 11 additions & 5 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,24 @@
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="AspNetCore.HealthChecks.UI.Client" Version="9.0.0" />
<PackageVersion Include="AspNetCore.HealthChecks.Uris" Version="9.0.0" />
<PackageVersion Include="AspNetCore.HealthChecks.Uris" Version="9.0.0" />
<PackageVersion Include="bunit.web" Version="1.40.0" />
<PackageVersion Include="ClientProxyBase" Version="2026.5.6" />
<PackageVersion Include="ClientProxyBase" Version="2026.6.1" />
<PackageVersion Include="coverlet.collector" Version="10.0.1" />
<PackageVersion Include="coverlet.msbuild" Version="10.0.1" />
<PackageVersion Include="FileProcessor.Client" Version="2026.5.1" />
<PackageVersion Include="IdentityModel" Version="6.2.0" />
<PackageVersion Include="Lamar" Version="16.0.0" />
<PackageVersion Include="Lamar.Microsoft.DependencyInjection" Version="16.0.0" />
<PackageVersion Include="MediatR" Version="14.1.0" />
<PackageVersion Include="MessagingService.IntegrationTesting.Helpers" Version="2026.5.1" />
<PackageVersion Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="10.0.9" />
<PackageVersion Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="10.0.301" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.9" />
<PackageVersion Include="Microsoft.Extensions.Hosting.WindowsServices" Version="10.0.9" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.6.0" />
<PackageVersion Include="Microsoft.Playwright" Version="1.60.0" />
<PackageVersion Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.23.0" />
<PackageVersion Include="Moq" Version="4.20.72" />
<PackageVersion Include="NUnit" Version="4.6.1" />
<PackageVersion Include="NUnit3TestAdapter" Version="6.2.0" />
Expand All @@ -27,12 +30,15 @@
<PackageVersion Include="Reqnroll.Tools.MsBuild.Generation" Version="3.3.4" />
<PackageVersion Include="Roslynator.Analyzers" Version="4.15.0" />
<PackageVersion Include="SecurityService.Client" Version="2026.5.1" />
<PackageVersion Include="SecurityService.IntegrationTesting.Helpers" Version="2026.5.1" />
<PackageVersion Include="Sentry.AspNetCore" Version="6.6.0" />
<PackageVersion Include="Shared" Version="2026.5.6" />
<PackageVersion Include="Shared.Results" Version="2026.5.6" />
<PackageVersion Include="Shared" Version="2026.6.1" />
<PackageVersion Include="Shared.IntegrationTesting" Version="2026.6.1" />
<PackageVersion Include="Shared.Results" Version="2026.6.1" />
<PackageVersion Include="Shouldly" Version="4.3.0" />
<PackageVersion Include="SimpleResults" Version="4.0.0" />
<PackageVersion Include="TransactionProcessor.Client" Version="2026.5.1" />
<PackageVersion Include="TransactionProcessor.Client" Version="2026.5.2" />
<PackageVersion Include="TransactionProcessor.IntegrationTesting.Helpers" Version="2026.5.2" />
<PackageVersion Include="xunit.v3" Version="3.2.2" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" />
</ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,8 @@ public static WebApplicationBuilder ConfigureLiveAuthentication(this WebApplicat
{
OnRedirectToIdentityProvider = context =>
{
context.ProtocolMessage.IssuerAddress = $"{issuerAddress}/connect/authorize";

// Pass prompt parameter if specified in authentication properties
if (context.Properties.Items.TryGetValue("prompt", out var prompt))
{
Expand Down Expand Up @@ -273,4 +275,4 @@ public static WebApplicationBuilder RegisterSerialiser(this WebApplicationBuilde
builder.Services.AddSingleton(serialiserSettings);
return builder;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@
<!-- Sign In Section -->
<div class="mt-12 text-center">
<p class="text-gray-600 mb-4">Sign in to access all features</p>
<a id="loginButton" href="/login" class="inline-flex items-center px-8 py-3 bg-blue-600 hover:bg-blue-700 text-white font-semibold rounded-lg shadow-md transition-colors duration-150">
<a id="loginButton" href="/login" target="_top" rel="external" data-enhance-nav="false" onclick="window.location.href=this.href; return false;" class="inline-flex items-center px-8 py-3 bg-blue-600 hover:bg-blue-700 text-white font-semibold rounded-lg shadow-md transition-colors duration-150">
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 16l-4-4m0 0l4-4m-4 4h14m-5 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h7a3 3 0 013 3v1"></path>
</svg>
Expand Down
42 changes: 31 additions & 11 deletions EstateManagementUI.BlazorServer/Components/Pages/Home.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,46 +34,56 @@ protected override async Task OnAfterRenderAsync(bool firstRender)
}

// This runs once after the interactive circuit is established.
await this.LogToConsole("OnAfterRenderAsync FIRST RENDER (interactive) - performing interactive initialization");
await this.TraceDashboardAsync("OnAfterRenderAsync FIRST RENDER - starting interactive initialization");
await this.TraceDashboardAsync($"Current URI: {NavigationManager.Uri}");

try
{
await OnAfterRender(PermissionSection.Dashboard, PermissionFunction.View);
var permissionResult = await OnAfterRender(PermissionSection.Dashboard, PermissionFunction.View);
await this.TraceDashboardAsync($"Dashboard permission check result: {(permissionResult.IsFailed ? "FAILED" : "SUCCESS")}");

ClaimsPrincipal user = this.AuthState.User;
await this.TraceDashboardAsync($"Authentication state: authenticated={user.Identity?.IsAuthenticated}, name={user.Identity?.Name ?? "<null>"}, authType={user.Identity?.AuthenticationType ?? "<null>"}");
await this.TraceDashboardAsync($"Claims: {string.Join(", ", user.Claims.Select(c => $"{c.Type}={c.Value}"))}");

// Redirect unauthenticated users to entry screen
if (!user.Identity?.IsAuthenticated ?? true)
{
await this.TraceDashboardAsync("User is not authenticated - redirecting to entry page");
NavigationManager.NavigateToEntryPage();
return;
}

// Determine role and admin flag now that we're interactive
var role = await PermissionService.GetUserRoleAsync();
this.isAdministrator = role == "Administrator";
await this.LogToConsole($"User role: {role}, isAdministrator: {this.isAdministrator}");

CorrelationId correlationId = new CorrelationId(Guid.NewGuid());
var estateId = await this.GetEstateId();
await this.TraceDashboardAsync($"User role resolved to '{role}', isAdministrator={this.isAdministrator}");

// Only load dashboard data for non-admins
if (this.isAdministrator == false)
{
CorrelationId correlationId = new CorrelationId(Guid.NewGuid());
var estateId = await this.GetEstateId();
await this.TraceDashboardAsync($"Resolved estateId for dashboard data load: {estateId}");
await this.TraceDashboardAsync("Non-admin user - loading dashboard data");
var result = await this.LoadDashboardData(correlationId, estateId);
if (result.IsFailed)
{
await this.TraceDashboardAsync($"Dashboard data load failed: {result.Message ?? "<no error message>"}");
this.NavigationManager.NavigateToErrorPage();
return;
}
}
else
{
await this.TraceDashboardAsync("Administrator detected - skipping dashboard data load and rendering welcome panel");
this.isLoading = false;
this.StateHasChanged();
}
}
catch (Exception ex)
{
await this.TraceDashboardAsync($"Initialization exception: {ex}");
this.errorMessage = $"Initialization error: {ex.Message}";
this.isLoading = false;
this.StateHasChanged();
Expand All @@ -86,7 +96,7 @@ protected override async Task OnAfterRenderAsync(bool firstRender)

private async Task<Result> LoadDashboardData(CorrelationId correlationId, Guid estateId)
{
await this.LogToConsole($"LoadDashboardData START - selectedDate: {this._selectedComparisonDate}");
await this.TraceDashboardAsync($"LoadDashboardData START - selectedDate: {this._selectedComparisonDate}");
try {
this.isLoading = true;
this.errorMessage = null;
Expand Down Expand Up @@ -142,8 +152,10 @@ private async Task<Result> LoadDashboardData(CorrelationId correlationId, Guid e
private async Task<Result<DateTime>> LoadComparisonDates(CorrelationId correlationId, Guid estateId) {

if (this.comparisonDates == null || !this.comparisonDates.Any()) {
await this.TraceDashboardAsync($"Loading comparison dates for estateId={estateId}");
var comparisonDatesResult = await this.CalendarUiService.GetComparisonDates(correlationId, estateId);
if (comparisonDatesResult.IsFailed) {
await this.TraceDashboardAsync($"Comparison dates load failed: {comparisonDatesResult.Message ?? "<no error message>"}");
return ResultHelpers.CreateFailure(comparisonDatesResult);
}

Expand All @@ -165,28 +177,36 @@ private async Task<Result<DateTime>> LoadComparisonDates(CorrelationId correlati
private async Task OnComparisonDateChanged()
{
this.changeEventCounter++;
await this.LogToConsole($"🔥 OnComparisonDateChanged FIRED! Count: {this.changeEventCounter}, New value: {this._selectedComparisonDate}");
await this.TraceDashboardAsync($"OnComparisonDateChanged fired. Count={this.changeEventCounter}, New value={this._selectedComparisonDate}");

// This is called after _selectedComparisonDate is updated by @bind-Value
if (this.isAdministrator == false) {

CorrelationId correlationId = CorrelationIdHelper.New();
Guid estateId= await this.GetEstateId();

await this.LogToConsole($"Loading dashboard data for date: {this._selectedComparisonDate}");
await this.TraceDashboardAsync($"Loading dashboard data for date: {this._selectedComparisonDate}");
var loadResult = await this.LoadDashboardData(correlationId,estateId);
if (loadResult.IsFailed) {
await this.TraceDashboardAsync($"Dashboard refresh failed: {loadResult.Message ?? "<no error message>"}");
this.NavigationManager.NavigateToErrorPage();
return;
}
this.StateHasChanged();
await this.LogToConsole("Dashboard data reload complete");
await this.TraceDashboardAsync("Dashboard data reload complete");
}
else {
await this.LogToConsole("User is administrator - skipping data reload");
await this.TraceDashboardAsync("Administrator user - skipping dashboard data reload");
}
}

private async Task TraceDashboardAsync(string message)
{
var trace = $"[Home.razor {DateTime.Now:HH:mm:ss.fff}] {message}";
Console.WriteLine(trace);
await this.LogToConsole(message);
}

private async Task LogToConsole(string message)
{
try
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,10 @@ protected async Task<Result> RequirePermission(PermissionSection permissionSecti
{
// Do a permission check here
Boolean hasPermission = await this.PermissionService.HasPermissionAsync(permissionSection, permissionFunction);
Console.WriteLine($"[AuthorizedComponentBase {DateTime.Now:HH:mm:ss.fff}] RequirePermission section={permissionSection}, function={permissionFunction}, result={hasPermission}");
if (hasPermission == false)
{
Console.WriteLine($"[AuthorizedComponentBase {DateTime.Now:HH:mm:ss.fff}] Permission denied - navigating to access denied page");
this.NavigationManager.NavigateToAccessDeniedPage();
return Result.Unauthorized();
}
Expand All @@ -37,8 +39,13 @@ protected async Task<Result> RequirePermission(PermissionSection permissionSecti
protected async Task<Guid> GetEstateId() {
Result<Guid> estateIdResult = this.AuthState.GetEstateIdFromClaims();
if (estateIdResult.IsFailed) {
Console.WriteLine($"[AuthorizedComponentBase {DateTime.Now:HH:mm:ss.fff}] Failed to resolve estateId from claims");
this.NavigationManager.NavigateToErrorPage();
}
else
{
Console.WriteLine($"[AuthorizedComponentBase {DateTime.Now:HH:mm:ss.fff}] Resolved estateId={estateIdResult.Data}");
}
return estateIdResult.Data;
}

Expand All @@ -52,20 +59,29 @@ protected async Task<Result> OnAfterRender(PermissionSection section,
{
this.AuthState = await AuthenticationStateTask;
User = this.AuthState.User;
Console.WriteLine($"[AuthorizedComponentBase {DateTime.Now:HH:mm:ss.fff}] OnAfterRender authState authenticated={User.Identity?.IsAuthenticated}, name={User.Identity?.Name ?? "<null>"}, authType={User.Identity?.AuthenticationType ?? "<null>"}, claims={string.Join(", ", User.Claims.Select(c => $"{c.Type}={c.Value}"))}");

Result authResult = await RequirePermission(section, function);
if (authResult.IsFailed)
{
Console.WriteLine($"[AuthorizedComponentBase {DateTime.Now:HH:mm:ss.fff}] OnAfterRender stopped because permission check failed");
return Result.Failure();
}

if (loadFunc == null)
{
Console.WriteLine($"[AuthorizedComponentBase {DateTime.Now:HH:mm:ss.fff}] OnAfterRender completed without loadFunc");
return Result.Success();
}

Result result = await loadFunc();
if (result.IsFailed)
{
Console.WriteLine($"[AuthorizedComponentBase {DateTime.Now:HH:mm:ss.fff}] OnAfterRender loadFunc failed");
this.NavigationManager.NavigateToErrorPage();
return Result.Failure();
}
Console.WriteLine($"[AuthorizedComponentBase {DateTime.Now:HH:mm:ss.fff}] OnAfterRender loadFunc succeeded");
return Result.Success();
}
}
}
1 change: 1 addition & 0 deletions EstateManagementUI.BlazorServer/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ RUN curl -fsSL --insecure https://deb.nodesource.com/setup_20.x | bash - && \
COPY ["EstateManagementUI.BlazorServer/NuGet.Config", "."]
COPY ["EstateManagementUI.BlazorServer/Certificates/*.*", "Certificates/"]
COPY ["EstateManagementUI.BlazorServer/EstateManagementUI.BlazorServer.csproj", "EstateManagementUI.BlazorServer/"]
COPY ["Directory.Packages.props", "."]
RUN dotnet restore "./EstateManagementUI.BlazorServer/EstateManagementUI.BlazorServer.csproj"

COPY . .
Expand Down
Loading
Loading