Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
113 commits
Select commit Hold shift + click to select a range
c26943f
feat: add tenant foundation contracts
weslleymurdock Aug 17, 2026
83ee3df
feat: associate identity users with tenants
weslleymurdock Aug 17, 2026
ec74abf
feat: add tenant catalog entity
weslleymurdock Aug 17, 2026
9979631
feat: add tenant setup state
weslleymurdock Aug 17, 2026
4b63bd4
feat: add tenant setup db context
weslleymurdock Aug 17, 2026
7b2428a
feat: add tenant catalog persistence
weslleymurdock Aug 17, 2026
9b5c1da
feat: implement tenant provisioning and setup
weslleymurdock Aug 17, 2026
7d66c82
feat: register tenant infrastructure namespaces
weslleymurdock Aug 17, 2026
6691c27
feat: extend identity contract for tenant authorization
weslleymurdock Aug 17, 2026
dc126ff
feat: integrate tenant roles into identity setup
weslleymurdock Aug 17, 2026
d2bca05
feat: enforce tenant-aware identity login
weslleymurdock Aug 17, 2026
108f74d
feat: include tenant context in login command
weslleymurdock Aug 17, 2026
3e209c6
feat: enforce tenant scope during identity login
weslleymurdock Aug 17, 2026
fc69ade
feat: persist master database setup configuration
weslleymurdock Aug 17, 2026
666bde5
feat: register setup configuration services
weslleymurdock Aug 17, 2026
f5ceace
feat: enforce tenant isolation and setup gate
weslleymurdock Aug 17, 2026
df01342
feat: compose tenant-aware identity and middleware
weslleymurdock Aug 17, 2026
0c3e0aa
feat: move identity endpoints under tenant API route
weslleymurdock Aug 17, 2026
69d405e
feat: expose tenancy contracts to web host
weslleymurdock Aug 17, 2026
3fe0c90
feat: make master database configuration part of setup
weslleymurdock Aug 17, 2026
d52268d
feat: add system tenant provisioning controller
weslleymurdock Aug 17, 2026
a1cd144
feat: add per-tenant setup controller
weslleymurdock Aug 17, 2026
c4e056d
feat: add system setup razor page
weslleymurdock Aug 17, 2026
6f2ff8e
feat: add tenant setup razor page
weslleymurdock Aug 17, 2026
fa06ad7
feat: scope home page to tenant route
weslleymurdock Aug 17, 2026
462b42f
feat: scope counter page to tenant route
weslleymurdock Aug 17, 2026
b303d16
feat: scope weather page to tenant route
weslleymurdock Aug 17, 2026
9ac97b1
fix: resolve master connection store through dependency injection
weslleymurdock Aug 17, 2026
674f2ff
fix: register tenant middleware namespaces
weslleymurdock Aug 17, 2026
c71008d
fix: harden tenant setup route detection
weslleymurdock Aug 17, 2026
709d94a
fix: enforce setup before anonymous endpoints
weslleymurdock Aug 17, 2026
7dd76b6
feat: initialize master schema during setup
weslleymurdock Aug 17, 2026
36900c8
fix: allow setup controller to initialize a selected database
weslleymurdock Aug 17, 2026
ce280d8
fix: initialize the selected master database before identity setup
weslleymurdock Aug 17, 2026
a1b49f3
fix: bind tenant scope to refresh tokens
weslleymurdock Aug 17, 2026
6ca4736
fix: validate tenant binding on refresh tokens
weslleymurdock Aug 17, 2026
71e7d55
feat: add tenant-bound refresh identity operation
weslleymurdock Aug 17, 2026
43cc127
feat: include tenant scope in refresh command
weslleymurdock Aug 17, 2026
5402932
feat: enforce tenant binding in refresh handler
weslleymurdock Aug 17, 2026
a032ba3
fix: bind refresh endpoint to tenant route
weslleymurdock Aug 17, 2026
d4d3b29
feat: implement tenant-bound refresh token service
weslleymurdock Aug 17, 2026
ade245e
test: cover tenant identity isolation and refresh binding
weslleymurdock Aug 17, 2026
c0da9c6
fix: include filesystem APIs required by setup persistence
weslleymurdock Aug 17, 2026
1da884b
fix: exempt Blazor infrastructure requests from tenant gate
weslleymurdock Aug 17, 2026
29d2adc
feat: persist system setup state
weslleymurdock Aug 17, 2026
fa432eb
feat: persist installation setup state in master database
weslleymurdock Aug 17, 2026
11c894c
fix: gate requests on persisted system setup state
weslleymurdock Aug 17, 2026
decb8d2
refactor: remove unused identity dependency from tenant gate
weslleymurdock Aug 17, 2026
771f2ca
feat: mark persisted system setup complete after initialization
weslleymurdock Aug 17, 2026
2461b65
fix: import tenant contracts into identity tests
weslleymurdock Aug 17, 2026
7eaffbf
fix: lock system setup mutations after initialization
weslleymurdock Aug 17, 2026
ee787e7
fix: build and tests compile and pass
weslleymurdock Aug 17, 2026
e927bcc
feat: improve program.cs and DI composition extensions
weslleymurdock Aug 17, 2026
f5f5840
fix: tests
weslleymurdock Aug 17, 2026
1038d24
Merge branch 'main' into feat/foundation
weslleymurdock Aug 17, 2026
3be518b
fix: global usings
weslleymurdock Aug 17, 2026
42be5fc
fix: warnings
weslleymurdock Aug 17, 2026
4d1b046
add enforcement steps for line and branch minimum coverage
weslleymurdock Aug 17, 2026
91ada13
feat: major refactor
weslleymurdock Aug 17, 2026
fbb4873
feat: document auditable entity contracts
weslleymurdock Aug 17, 2026
d41eaca
feat: add persistence audit record
weslleymurdock Aug 17, 2026
3ec029b
feat: map audit history to dedicated schema
weslleymurdock Aug 17, 2026
1985c0e
chore: import audit persistence dependencies
weslleymurdock Aug 17, 2026
18e17d6
feat: add entity audit history to persistence save pipeline
weslleymurdock Aug 17, 2026
870544d
fix: avoid auditing non-auditable audit records
weslleymurdock Aug 17, 2026
42294eb
feat: make users auditable
weslleymurdock Aug 17, 2026
ff02f17
feat: make roles auditable
weslleymurdock Aug 17, 2026
59d481a
feat: make tenants auditable
weslleymurdock Aug 17, 2026
61b9cd6
feat: make system setup state auditable
weslleymurdock Aug 17, 2026
97fafe2
feat: expose request actor to persistence auditing
weslleymurdock Aug 17, 2026
0a5d2d9
refactor: use MudBlazor for application layout
weslleymurdock Aug 17, 2026
6cf3048
refactor: replace default navigation with MudBlazor
weslleymurdock Aug 17, 2026
832ebbf
refactor: preserve tenant-scoped navigation in MudBlazor menu
weslleymurdock Aug 17, 2026
5926522
fix: simplify Razor tenant navigation expressions
weslleymurdock Aug 17, 2026
1df3aca
refactor: migrate home page to MudBlazor
weslleymurdock Aug 17, 2026
aa5b417
refactor: migrate counter page to MudBlazor
weslleymurdock Aug 17, 2026
f8a3cdc
refactor: migrate weather page to MudBlazor
weslleymurdock Aug 17, 2026
3d587c6
refactor: migrate error page to MudBlazor
weslleymurdock Aug 17, 2026
5da8e74
refactor: migrate not-found page to MudBlazor
weslleymurdock Aug 17, 2026
7044e0e
refactor: migrate system setup page to MudBlazor
weslleymurdock Aug 17, 2026
7ff44a6
refactor: migrate tenant setup page to MudBlazor
weslleymurdock Aug 17, 2026
db62146
refactor: style reconnect component with MudBlazor
weslleymurdock Aug 17, 2026
c2d5025
refactor: remove bootstrap styling from MudBlazor host
weslleymurdock Aug 17, 2026
34dda56
fix: enforce actual line and branch coverage rates
weslleymurdock Aug 17, 2026
b10865f
test: cover auditable persistence history and actor tracking
weslleymurdock Aug 17, 2026
065a007
fix: use shared JSON global import
weslleymurdock Aug 17, 2026
90d365c
chore: import audit and HTTP test dependencies
weslleymurdock Aug 17, 2026
93ffd60
fix: make tenant menu links Razor-safe
weslleymurdock Aug 17, 2026
b842e0c
fix: classify soft deletes as deleted audit operations
weslleymurdock Aug 17, 2026
502b162
test: verify soft-delete audit operation and state transition
weslleymurdock Aug 17, 2026
7221969
fix: prepare audit entries once per save operation
weslleymurdock Aug 17, 2026
6b452b3
test: cover tenant access middleware isolation and setup branches
weslleymurdock Aug 17, 2026
8418a87
fix: import tenant middleware namespace for tests
weslleymurdock Aug 17, 2026
e1f4a17
fix: use MudBlazor-compatible accessibility attribute
weslleymurdock Aug 17, 2026
9203925
chore: import authorization, setup and constant contracts for middlew…
weslleymurdock Aug 17, 2026
9d6a7ac
fix: stabilize tenant middleware test invocation tracking
weslleymurdock Aug 17, 2026
0496042
test: cover tenant contracts handlers and setup validators
weslleymurdock Aug 17, 2026
48f0ac4
chore: import application tenant and system handler contracts
weslleymurdock Aug 17, 2026
da1792e
fix: avoid invoking protected validator member from tests
weslleymurdock Aug 17, 2026
5b09887
test: cover setup configuration persistence branches
weslleymurdock Aug 17, 2026
f9214e8
test: cover validation middleware lifecycle and null handling
weslleymurdock Aug 17, 2026
cf87e01
fix: align tenant middleware test with setup gate response contract
weslleymurdock Aug 17, 2026
7781794
test: cover tenant identity null-scope and tenant-admin failure branches
weslleymurdock Aug 17, 2026
663a10a
test: cover audit actor claim fallbacks
weslleymurdock Aug 17, 2026
47d5447
test: cover tenant provisioning validation branches
weslleymurdock Aug 17, 2026
09d6bff
fix: align application contract tests with current request signatures
weslleymurdock Aug 17, 2026
2d471e1
chore: import IO and JWT contracts for tests
weslleymurdock Aug 17, 2026
1f2f39f
fix: use current IdentityUserManager CreateAsync overload
weslleymurdock Aug 17, 2026
8eafaa3
fix: use command contract without generated tenant id
weslleymurdock Aug 17, 2026
2cce776
fix: provide tenant request identifier in validation tests
weslleymurdock Aug 17, 2026
d81a75b
test: cover identity handler tenant and failure branches
weslleymurdock Aug 17, 2026
a9460c2
fix: align tenant handler assertion with source value casing
weslleymurdock Aug 17, 2026
0ff09b7
test: cover hard-delete audit branch for non-soft-deletable entities
weslleymurdock Aug 17, 2026
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
23 changes: 9 additions & 14 deletions .github/workflows/CI-CT-CD.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: CI - CT - CD
name: Continuous Integration - Testing - Deploy

on:
push:
Expand Down Expand Up @@ -91,6 +91,7 @@ jobs:

if ($coverageFiles.Count -eq 0) {
"## Coverage gate`n`nNo Coverlet XML was found under the test-results GUID subdirectories." | Out-File $env:GITHUB_STEP_SUMMARY -Append -Encoding utf8
Write-Error "Coverage data was not produced."
return
}

Expand All @@ -99,27 +100,21 @@ jobs:
$lineRate = [double]$coverage.coverage.'line-rate' * 100
$branchRate = [double]$coverage.coverage.'branch-rate' * 100

@"
@"
## Coverage gate
| Metric | Result | Minimum | Status |
| --- | ---: | ---: | --- |
| Line coverage | $([math]::Round($lineRate, 2))% | 80% | $(if ($lineRate -ge 80) { '✅' } else { '❌' }) |
| Branch coverage | $([math]::Round($branchRate, 2))% | 80% | $(if ($branchRate -ge 80) { '✅' } else { '❌' }) |
"@ | Out-File $env:GITHUB_STEP_SUMMARY -Append -Encoding utf8

"LINE_COVERAGE=$([math]::Round($lineRate, 2))" | Out-File artifacts/line-rate.txt -Encoding utf8
"BRANCH_COVERAGE=$([math]::Round($branchRate, 2))" | Out-File artifacts/branch-rate.txt -Encoding utf8

if ($lineRate -ge 80) {
"| Line coverage | $([math]::Round($lineRate, 2))% | 80% | ✅ |" | Out-File $env:GITHUB_STEP_SUMMARY -Append -Encoding utf8
} else {
"| Line coverage | $([math]::Round($lineRate, 2))% | 80% | ❌ |" | Out-File $env:GITHUB_STEP_SUMMARY -Append -Encoding utf8
if ($lineRate -lt 80 -or $branchRate -lt 80) {
Write-Error "Coverage gate failed. Line coverage: $([math]::Round($lineRate, 2))%; Branch coverage: $([math]::Round($branchRate, 2))%; minimum: 80%."
}

if ($branchRate -ge 80) {
"| Branch coverage | $([math]::Round($branchRate, 2))% | 80% | ✅ |" | Out-File $env:GITHUB_STEP_SUMMARY -Append -Encoding utf8
} else {
"| Branch coverage | $([math]::Round($branchRate, 2))% | 80% | ❌ |" | Out-File $env:GITHUB_STEP_SUMMARY -Append -Encoding utf8
}



- name: Append test execution report
if: always()
shell: pwsh
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
namespace RemoteTickets.Application.Common.System;

/// <summary>Represents the master database setup payload.</summary>
/// <param name="ConnectionString">The SQL Server connection string used by the tenant catalog.</param>
/// <param name="CommandTimeout">The value (in seconds) for use for operations timeout in SQL Server.</param>
public sealed record MasterDatabaseSetupRequest(
string ConnectionString,
int CommandTimeout);

/// <summary>Represents the first-time setup administrator credentials.</summary>
/// <param name="Email">The administrator email address.</param>
/// <param name="Password">The administrator password.</param>
public sealed record InitializeSetupRequest(string Email, string Password);

/// <summary>
/// Contract for System Service
/// </summary>
public interface ISystemService
{
/// <summary>
/// Performs the setup of master database
/// </summary>
/// <param name="connectionString">Connection string of master database</param>
/// <param name="commandTimeout">Timeout for operations on master database</param>
/// <param name="cancellationToken">Cancellation token for the operation</param>
/// <returns>An <see cref="MasterDatabaseSetupResponse" /> with the status of setup operation </returns>
Task<MasterDatabaseSetupResponse> PerformMasterDatabaseSetup(string connectionString, int commandTimeout, CancellationToken cancellationToken);
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
namespace RemoteTickets.Application.Common.Tenancy;

/// <summary>Contains the application roles used by the tenant-aware authorization model.</summary>
public static class TenantRoles
{
/// <summary>Gets the role that can administer the complete installation and every tenant.</summary>
public const string SysAdmin = "sysadmin";
/// <summary>Gets the role that can administer a single tenant.</summary>
public const string TenantAdmin = "tenadmin";
/// <summary>Gets the role that can operate the cash register and other non-configuration business functions.</summary>
public const string TenantOperator = "tenop";
/// <summary>Gets the role that can perform attendance and non-critical business operations.</summary>
public const string TenantAttendant = "tenat";
}

/// <summary>Contains claim names used by the tenant-aware security model.</summary>
public static class TenantClaimTypes
{
/// <summary>Gets the claim containing the tenant identifier assigned to a user.</summary>
public const string TenantId = "tenant_id";
}

/// <summary>Contains authorization policy names used by tenant-aware endpoints.</summary>
public static class TenantPolicies
{
/// <summary>Gets the policy requiring system administration privileges.</summary>
public const string SysAdmin = "tenant.sysadmin";
/// <summary>Gets the policy requiring tenant administration privileges.</summary>
public const string TenantAdmin = "tenant.admin";
/// <summary>Gets the policy requiring operator privileges.</summary>
public const string TenantOperator = "tenant.operator";
/// <summary>Gets the policy requiring attendant privileges.</summary>
public const string TenantAttendant = "tenant.attendant";
}

/// <summary>Represents the setup state of a tenant database.</summary>
/// <param name="IsSetupRequired">Indicates whether tenant setup is still required.</param>
/// <param name="IsSetupComplete">Indicates whether tenant setup has completed.</param>
public sealed record TenantSetupStatusResponse(bool IsSetupRequired, bool IsSetupComplete) : IResponse;

/// <summary>Represents a tenant exposed to system administrators.</summary>
/// <param name="Id">The stable tenant identifier used in routes.</param>
/// <param name="Name">The tenant display name.</param>
/// <param name="DatabaseName">The database name assigned to the tenant.</param>
/// <param name="IsActive">Indicates whether the tenant accepts normal operations.</param>
/// <param name="IsSetupComplete">Indicates whether tenant setup has completed.</param>
public sealed record TenantResponse(string Id, string Name, string DatabaseName, bool IsActive, bool IsSetupComplete) : IResponse;

/// <summary>Represents a request to provision a tenant and its first administrator.</summary>
/// <param name="Id">The tenant identifier.</param>
/// <param name="Name">The tenant display name.</param>
/// <param name="DatabaseName">The SQL Server database name.</param>
/// <param name="ConnectionString">The connection string for the tenant database.</param>
/// <param name="AdminEmail">The first tenant administrator email.</param>
/// <param name="AdminPassword">The first tenant administrator password.</param>
public sealed record CreateTenantRequest(string Id, string Name, string DatabaseName, string ConnectionString, string AdminEmail, string AdminPassword)
{
public CreateTenantCommand ToCommand()

Check warning on line 58 in src/server/RemoteTickets.Application/Common/Tenancy/TenancyContracts.cs

View workflow job for this annotation

GitHub Actions / Continuous Integration

Missing XML comment for publicly visible type or member 'CreateTenantRequest.ToCommand()'

Check warning on line 58 in src/server/RemoteTickets.Application/Common/Tenancy/TenancyContracts.cs

View workflow job for this annotation

GitHub Actions / Continuous Testing

Missing XML comment for publicly visible type or member 'CreateTenantRequest.ToCommand()'
=> new ((string.IsNullOrEmpty(Name) || string.IsNullOrWhiteSpace(Name) ? throw new ArgumentNullException(nameof(Name)) : Name),
(string.IsNullOrEmpty(DatabaseName) || string.IsNullOrWhiteSpace(DatabaseName)? throw new ArgumentNullException(nameof(DatabaseName)) : DatabaseName),
(string.IsNullOrEmpty(ConnectionString) || string.IsNullOrWhiteSpace(ConnectionString)? throw new ArgumentNullException(nameof(ConnectionString)) : ConnectionString),
(string.IsNullOrEmpty(AdminEmail) || string.IsNullOrWhiteSpace(AdminEmail)? throw new ArgumentNullException(nameof(AdminEmail)) : AdminEmail),
(string.IsNullOrEmpty(AdminPassword) || string.IsNullOrWhiteSpace(AdminPassword)? throw new ArgumentNullException(nameof(AdminPassword)) : AdminPassword));
}

/// <summary>Provides tenant registration and setup operations.</summary>
public interface ITenantManagementService
{
/// <summary>Gets a tenant by its route identifier.</summary>
/// <param name="tenantId">The tenant route identifier.</param>
/// <param name="cancellationToken">The token used to cancel the operation.</param>
/// <returns>The tenant, or <see langword="null"/> when it does not exist.</returns>
Task<TenantResponse?> GetAsync(string tenantId, CancellationToken cancellationToken);

/// <summary>Creates a tenant database and its first tenant administrator.</summary>
/// <param name="request">The tenant provisioning request.</param>
/// <param name="cancellationToken">The token used to cancel the operation.</param>
/// <returns>The provisioned tenant.</returns>
Task<TenantResponse> CreateAsync(CreateTenantRequest request, CancellationToken cancellationToken);

/// <summary>Gets the setup state stored in a tenant database.</summary>
/// <param name="tenantId">The tenant route identifier.</param>
/// <param name="cancellationToken">The token used to cancel the operation.</param>
/// <returns>The tenant setup state.</returns>
Task<TenantSetupStatusResponse> GetSetupStatusAsync(string tenantId, CancellationToken cancellationToken);

/// <summary>Completes tenant setup after all tenant initialization checks have succeeded.</summary>
/// <param name="tenantId">The tenant route identifier.</param>
/// <param name="cancellationToken">The token used to cancel the operation.</param>
/// <returns>The resulting setup state.</returns>
Task<TenantSetupStatusResponse> CompleteSetupAsync(string tenantId, CancellationToken cancellationToken);
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace RemoteTickets.Application.Common.Util;

public static class RemoteTicketsConstants

Check warning on line 3 in src/server/RemoteTickets.Application/Common/Util/RemoteTicketsConstants.cs

View workflow job for this annotation

GitHub Actions / Continuous Integration

Missing XML comment for publicly visible type or member 'RemoteTicketsConstants'

Check warning on line 3 in src/server/RemoteTickets.Application/Common/Util/RemoteTicketsConstants.cs

View workflow job for this annotation

GitHub Actions / Continuous Testing

Missing XML comment for publicly visible type or member 'RemoteTicketsConstants'
{
public const string SystemSetupId = "01a00d95-87a3-7d90-b45c-759bf28c5d50";

Check warning on line 5 in src/server/RemoteTickets.Application/Common/Util/RemoteTicketsConstants.cs

View workflow job for this annotation

GitHub Actions / Continuous Integration

Missing XML comment for publicly visible type or member 'RemoteTicketsConstants.SystemSetupId'

Check warning on line 5 in src/server/RemoteTickets.Application/Common/Util/RemoteTicketsConstants.cs

View workflow job for this annotation

GitHub Actions / Continuous Testing

Missing XML comment for publicly visible type or member 'RemoteTicketsConstants.SystemSetupId'
public const string TenantSetupId = "01a00e8b-ffe7-7186-aa29-46980b4cdbd7";

Check warning on line 6 in src/server/RemoteTickets.Application/Common/Util/RemoteTicketsConstants.cs

View workflow job for this annotation

GitHub Actions / Continuous Integration

Missing XML comment for publicly visible type or member 'RemoteTicketsConstants.TenantSetupId'

Check warning on line 6 in src/server/RemoteTickets.Application/Common/Util/RemoteTicketsConstants.cs

View workflow job for this annotation

GitHub Actions / Continuous Testing

Missing XML comment for publicly visible type or member 'RemoteTicketsConstants.TenantSetupId'
}
6 changes: 6 additions & 0 deletions src/server/RemoteTickets.Application/GlobalUsings.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
global using System;
global using System.Collections.Generic;
global using System.Data.Common;
global using System.Linq;
global using System.Security.Claims;
global using System.Threading;
global using System.Threading.Tasks;
global using FluentValidation;
global using FluentValidation.Results;
global using FluentValidation.Validators;
global using Mediator.Net.Context;
global using Mediator.Net.Contracts;
global using Mediator.Net.Pipeline;
global using RemoteTickets.Application.Common.Identity;
global using RemoteTickets.Application.Common.System;
global using RemoteTickets.Application.Common.Tenancy;
global using RemoteTickets.Application.Identity.Requests;
global using RemoteTickets.Application.System.Requests;
global using RemoteTickets.Application.Tenant.Requests;
Loading
Loading