From 85d39e996e987988a12b2aacad10e588e2489e9b Mon Sep 17 00:00:00 2001 From: StuartFerguson Date: Sun, 27 Sep 2026 10:11:33 +0100 Subject: [PATCH] make call more secure by verifying estate in header against claims --- .../ControllerTestsBase.cs | 1 + .../CustomWebApplicationFactory.cs | 36 +++++- .../EstateEndpointTests.cs | 109 ++++++++++++++++++ .../TenantContextEndpointTests.cs | 56 +++++++++ .../Bootstrapper/MiddlewareRegistry.cs | 6 +- EstateReportingAPI/Common/EstateContext.cs | 74 ++++++++++++ .../Handlers/CalenderHandler.cs | 5 +- .../Handlers/ContractHandler.cs | 13 ++- EstateReportingAPI/Handlers/EstateHandler.cs | 9 +- .../Handlers/FileImportHandler.cs | 17 +-- .../Handlers/MerchantHandler.cs | 37 +++--- .../Handlers/OperatorHandler.cs | 9 +- .../Handlers/SettlementHandler.cs | 5 +- .../Handlers/TransactionHandler.cs | 47 ++++---- EstateReportingAPI/Startup.cs | 8 +- 15 files changed, 358 insertions(+), 74 deletions(-) create mode 100644 EstateReportingAPI.IntegrationTests/TenantContextEndpointTests.cs create mode 100644 EstateReportingAPI/Common/EstateContext.cs diff --git a/EstateReportingAPI.IntegrationTests/ControllerTestsBase.cs b/EstateReportingAPI.IntegrationTests/ControllerTestsBase.cs index 1577b67..b901d50 100644 --- a/EstateReportingAPI.IntegrationTests/ControllerTestsBase.cs +++ b/EstateReportingAPI.IntegrationTests/ControllerTestsBase.cs @@ -46,6 +46,7 @@ public virtual async ValueTask InitializeAsync() String dbConnString = GetLocalConnectionString($"TransactionProcessorReadModel-{this.TestId}"); this.factory = new CustomWebApplicationFactory(dbConnString); + this.factory.DefaultEstateId = this.TestId; this.Client = this.factory.CreateClient(); this.context = new EstateManagementContext(dbConnString); diff --git a/EstateReportingAPI.IntegrationTests/CustomWebApplicationFactory.cs b/EstateReportingAPI.IntegrationTests/CustomWebApplicationFactory.cs index 4339b0b..8b38112 100644 --- a/EstateReportingAPI.IntegrationTests/CustomWebApplicationFactory.cs +++ b/EstateReportingAPI.IntegrationTests/CustomWebApplicationFactory.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Net.Http.Headers; using System.Threading; using System.Threading.Tasks; using Microsoft.Data.SqlClient; @@ -32,6 +33,7 @@ public CustomWebApplicationFactory(string databaseConnectionString) } public string DefaultUserId { get; set; } = "1"; + public Guid DefaultEstateId { get; set; } protected override void ConfigureWebHost(IWebHostBuilder builder) { @@ -67,7 +69,11 @@ protected override void ConfigureWebHost(IWebHostBuilder builder) containerBuilder.AddSingleton(manager); - containerBuilder.Configure(options => options.DefaultUserId = DefaultUserId); + containerBuilder.Configure(options => + { + options.DefaultUserId = DefaultUserId; + options.DefaultEstateId = DefaultEstateId; + }); containerBuilder.AddAuthentication(TestAuthHandler.AuthenticationScheme) .AddScheme(TestAuthHandler.AuthenticationScheme, options => { }); @@ -82,14 +88,19 @@ protected override void ConfigureWebHost(IWebHostBuilder builder) public class TestAuthHandlerOptions : AuthenticationSchemeOptions { public string DefaultUserId { get; set; } = null!; + public Guid DefaultEstateId { get; set; } } public class TestAuthHandler : AuthenticationHandler { public const string UserId = "UserId"; + public const string OmitEstateClaim = "Test-Omit-Estate-Claim"; + public const string EstateClaim = "Test-Estate-Claim"; + public const string EstateClaimType = "Test-Estate-Claim-Type"; public const string AuthenticationScheme = "Test"; private readonly string _defaultUserId; + private readonly Guid _defaultEstateId; public TestAuthHandler( IOptionsMonitor options, @@ -98,10 +109,18 @@ public TestAuthHandler( ISystemClock clock) : base(options, logger, encoder, clock) { _defaultUserId = options.CurrentValue.DefaultUserId; + _defaultEstateId = options.CurrentValue.DefaultEstateId; } protected override Task HandleAuthenticateAsync() { + if (Context.Request.Headers.TryGetValue("Authorization", out var authorization) == false || + AuthenticationHeaderValue.TryParse(authorization.ToString(), out AuthenticationHeaderValue? authenticationHeader) == false || + String.Equals(authenticationHeader.Scheme, AuthenticationScheme, StringComparison.OrdinalIgnoreCase) == false) + { + return Task.FromResult(AuthenticateResult.NoResult()); + } + var claims = new List { new Claim(ClaimTypes.Name, "Test user") }; // Extract User ID from the request headers if it exists, @@ -115,6 +134,19 @@ protected override Task HandleAuthenticateAsync() claims.Add(new Claim(ClaimTypes.NameIdentifier, _defaultUserId)); } + if (Context.Request.Headers.ContainsKey(OmitEstateClaim) == false) + { + string estateClaim = Context.Request.Headers.TryGetValue(EstateClaim, out var requestedEstateClaim) + ? requestedEstateClaim.ToString() + : _defaultEstateId.ToString(); + + string estateClaimType = Context.Request.Headers.TryGetValue(EstateClaimType, out var requestedEstateClaimType) + ? requestedEstateClaimType.ToString() + : "estateId"; + + claims.Add(new Claim(estateClaimType, estateClaim)); + } + // TODO: Add as many claims as you need here var identity = new ClaimsIdentity(claims, AuthenticationScheme); @@ -125,4 +157,4 @@ protected override Task HandleAuthenticateAsync() return Task.FromResult(result); } -} \ No newline at end of file +} diff --git a/EstateReportingAPI.IntegrationTests/EstateEndpointTests.cs b/EstateReportingAPI.IntegrationTests/EstateEndpointTests.cs index 006a644..b6c7b48 100644 --- a/EstateReportingAPI.IntegrationTests/EstateEndpointTests.cs +++ b/EstateReportingAPI.IntegrationTests/EstateEndpointTests.cs @@ -1,6 +1,9 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; using System.Threading; using System.Threading.Tasks; using EstateReportingAPI.DataTransferObjects; @@ -54,6 +57,112 @@ public async Task EstateEndpoint_GetEstateOperator_EstateOperatorsReturned() { estateOperators.Single(e => e.Name == "Voucher").OperatorId.ShouldBe(this.context.Operators.Single(o => o.Name == "Voucher").OperatorId); } + [Fact] + public async Task EstateEndpoint_MatchingEstateClaimAndHeader_ReturnsEstate() + { + await this.helper.AddEstate("Test Estate", "Ref1"); + + using HttpResponseMessage response = await SendEstateRequest(this.TestId.ToString()); + + response.StatusCode.ShouldBe(HttpStatusCode.OK); + } + + [Fact] + public async Task EstateEndpoint_ValidEstateClaimWithoutHeader_ReturnsEstate() + { + await this.helper.AddEstate("Test Estate", "Ref1"); + + using HttpResponseMessage response = await SendEstateRequest(); + + response.StatusCode.ShouldBe(HttpStatusCode.OK); + } + + [Fact] + public async Task EstateEndpoint_DifferentEstateHeader_ReturnsForbidden() + { + using HttpResponseMessage response = await SendEstateRequest(Guid.NewGuid().ToString()); + + response.StatusCode.ShouldBe(HttpStatusCode.Forbidden); + } + + [Fact] + public async Task EstateEndpoint_MalformedEstateHeader_ReturnsForbidden() + { + using HttpResponseMessage response = await SendEstateRequest("not-a-guid"); + + response.StatusCode.ShouldBe(HttpStatusCode.Forbidden); + } + + [Fact] + public async Task EstateEndpoint_MissingEstateClaim_ReturnsForbidden() + { + using HttpResponseMessage response = await SendEstateRequest(omitEstateClaim: true); + + response.StatusCode.ShouldBe(HttpStatusCode.Forbidden); + } + + [Fact] + public async Task EstateEndpoint_MalformedEstateClaim_ReturnsForbidden() + { + using HttpResponseMessage response = await SendEstateRequest(estateClaim: "not-a-guid"); + + response.StatusCode.ShouldBe(HttpStatusCode.Forbidden); + } + + [Fact] + public async Task EstateEndpoint_EmptyEstateClaim_ReturnsForbidden() + { + using HttpResponseMessage response = await SendEstateRequest(estateClaim: Guid.Empty.ToString()); + + response.StatusCode.ShouldBe(HttpStatusCode.Forbidden); + } + + [Fact] + public async Task EstateEndpoint_EstateIdClaim_ReturnsEstate() + { + await this.helper.AddEstate("Test Estate", "Ref1"); + + using HttpResponseMessage response = await SendEstateRequest( + estateHeader: this.TestId.ToString(), + estateClaimType: "estateId"); + + response.StatusCode.ShouldBe(HttpStatusCode.OK); + } + + [Fact] + public async Task EstateEndpoint_MissingAuthentication_ReturnsUnauthorized() + { + using HttpRequestMessage request = new(HttpMethod.Get, this.BaseRoute); + + using HttpResponseMessage response = await this.Client.SendAsync(request); + + response.StatusCode.ShouldBe(HttpStatusCode.Unauthorized); + } + + private async Task SendEstateRequest( + string? estateHeader = null, + bool omitEstateClaim = false, + string? estateClaim = null, + string? estateClaimType = null) + { + using HttpRequestMessage request = new(HttpMethod.Get, this.BaseRoute); + request.Headers.Authorization = new AuthenticationHeaderValue("Test"); + + if (estateHeader != null) + request.Headers.Add("estateId", estateHeader); + + if (omitEstateClaim) + request.Headers.Add(TestAuthHandler.OmitEstateClaim, "true"); + + if (estateClaim != null) + request.Headers.Add(TestAuthHandler.EstateClaim, estateClaim); + + if (estateClaimType != null) + request.Headers.Add(TestAuthHandler.EstateClaimType, estateClaimType); + + return await this.Client.SendAsync(request); + } + protected override async Task ClearStandingData() { } diff --git a/EstateReportingAPI.IntegrationTests/TenantContextEndpointTests.cs b/EstateReportingAPI.IntegrationTests/TenantContextEndpointTests.cs new file mode 100644 index 0000000..a369b40 --- /dev/null +++ b/EstateReportingAPI.IntegrationTests/TenantContextEndpointTests.cs @@ -0,0 +1,56 @@ +using System; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading.Tasks; +using Xunit; +using Shouldly; + +namespace EstateReportingAPI.IntegrationTests; + +public sealed class TenantContextEndpointTests : ControllerTestsBase +{ + [Theory] + [InlineData("api/calendars/comparisondates")] + [InlineData("api/contracts")] + [InlineData("api/estates")] + [InlineData("api/fileimportlogs")] + [InlineData("api/fileprofiles")] + [InlineData("api/merchants")] + [InlineData("api/operators")] + [InlineData("api/settlements/todayssettlements")] + [InlineData("api/transactions/todayssales")] + public async Task ProtectedEndpoint_DifferentEstateHeader_ReturnsForbidden(string route) + { + using HttpRequestMessage request = new(HttpMethod.Get, route); + request.Headers.Authorization = new AuthenticationHeaderValue("Test"); + request.Headers.Add("estateId", Guid.NewGuid().ToString()); + + using HttpResponseMessage response = await this.Client.SendAsync(request); + + response.StatusCode.ShouldBe(HttpStatusCode.Forbidden); + } + + [Theory] + [InlineData("health")] + [InlineData("healthui")] + public async Task HealthEndpoint_DoesNotRequireEstateContext(string route) + { + using HttpRequestMessage request = new(HttpMethod.Get, route); + + using HttpResponseMessage response = await this.Client.SendAsync(request); + + ((int)response.StatusCode).ShouldNotBe((int)HttpStatusCode.Unauthorized); + ((int)response.StatusCode).ShouldNotBe((int)HttpStatusCode.Forbidden); + } + + protected override Task ClearStandingData() + { + return Task.CompletedTask; + } + + protected override Task SetupStandingData() + { + return Task.CompletedTask; + } +} diff --git a/EstateReportingAPI/Bootstrapper/MiddlewareRegistry.cs b/EstateReportingAPI/Bootstrapper/MiddlewareRegistry.cs index 73814c5..ff57258 100644 --- a/EstateReportingAPI/Bootstrapper/MiddlewareRegistry.cs +++ b/EstateReportingAPI/Bootstrapper/MiddlewareRegistry.cs @@ -2,6 +2,7 @@ using Shared.Middleware; namespace EstateReportingAPI.Bootstrapper{ + using EstateReportingAPI.Common; using Lamar; using Microsoft.Extensions.Diagnostics.HealthChecks; using OpenIddict.Validation.AspNetCore; @@ -21,6 +22,8 @@ public MiddlewareRegistry() this.ConfigureMiddlewareLogging(); this.ConfigureJsonOptions(); this.AddHealthMonitoringRegistration(Startup.Configuration); + this.AddScoped(); + this.AddScoped(sp => sp.GetRequiredService()); } @@ -55,7 +58,6 @@ private void ConfigureAuthentication(){ { options.DefaultScheme = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme; }); - this.AddOpenIddict() .AddValidation(options => { @@ -140,4 +142,4 @@ public static void ConfigureMinimalApi(JsonSerializerOptions serializerOptions) serializerOptions.Converters.Add(new DateTimeSpaceConverter()); } } -} \ No newline at end of file +} diff --git a/EstateReportingAPI/Common/EstateContext.cs b/EstateReportingAPI/Common/EstateContext.cs new file mode 100644 index 0000000..017e624 --- /dev/null +++ b/EstateReportingAPI/Common/EstateContext.cs @@ -0,0 +1,74 @@ +using Microsoft.AspNetCore.Authorization; + +namespace EstateReportingAPI.Common +{ + public interface IEstateContext + { + Guid EstateId { get; } + } + + public sealed class EstateContext : IEstateContext + { + public Guid EstateId { get; private set; } + + internal void SetEstate(Guid estateId) + { + if (estateId == Guid.Empty) + throw new ArgumentException("Estate ID cannot be empty.", nameof(estateId)); + + EstateId = estateId; + } + } + + public sealed class EstateContextMiddleware + { + private readonly RequestDelegate _next; + + public EstateContextMiddleware(RequestDelegate next) + { + _next = next; + } + + public async Task InvokeAsync(HttpContext httpContext,EstateContext estateContext) + { + // Health endpoints or anonymous endpoints do not need estate context. + if (httpContext.GetEndpoint()?.Metadata + .GetMetadata() != null) + { + await _next(httpContext); + return; + } + + if (httpContext.User.Identity?.IsAuthenticated != true) + { + httpContext.Response.StatusCode = StatusCodes.Status401Unauthorized; + return; + } + + string? estateClaim = httpContext.User.FindFirst("estateId")?.Value; + + if (!Guid.TryParse(estateClaim, out Guid authenticatedEstateId) || + authenticatedEstateId == Guid.Empty) + { + httpContext.Response.StatusCode = StatusCodes.Status403Forbidden; + return; + } + + // Backwards compatibility: if the header is supplied, it must agree + // with the authenticated claim. + if (httpContext.Request.Headers.TryGetValue("estateId", out var headerValue)) + { + if (!Guid.TryParse(headerValue.SingleOrDefault(), out Guid headerEstateId) || + headerEstateId != authenticatedEstateId) + { + httpContext.Response.StatusCode = StatusCodes.Status403Forbidden; + return; + } + } + + estateContext.SetEstate(authenticatedEstateId); + + await _next(httpContext); + } + } +} diff --git a/EstateReportingAPI/Handlers/CalenderHandler.cs b/EstateReportingAPI/Handlers/CalenderHandler.cs index 723ad05..a66deee 100644 --- a/EstateReportingAPI/Handlers/CalenderHandler.cs +++ b/EstateReportingAPI/Handlers/CalenderHandler.cs @@ -1,4 +1,5 @@ using EstateReportingAPI.BusinessLogic.Queries; +using EstateReportingAPI.Common; using EstateReportingAPI.DataTrasferObjects; using EstateReportingAPI.Models; using MediatR; @@ -9,10 +10,10 @@ namespace EstateReportingAPI.Handlers; public static class CalenderHandler { - public static async Task GetCalendarComparisonDates([FromHeader] Guid estateId, + public static async Task GetCalendarComparisonDates(IEstateContext estateContext, IMediator mediator, CancellationToken cancellationToken) { - CalendarQueries.GetComparisonDatesQuery query = new CalendarQueries.GetComparisonDatesQuery(estateId); + CalendarQueries.GetComparisonDatesQuery query = new CalendarQueries.GetComparisonDatesQuery(estateContext.EstateId); Result> result = await mediator.Send(query, cancellationToken); return ResponseFactory.FromResult(result, r => { diff --git a/EstateReportingAPI/Handlers/ContractHandler.cs b/EstateReportingAPI/Handlers/ContractHandler.cs index 68db54e..e062460 100644 --- a/EstateReportingAPI/Handlers/ContractHandler.cs +++ b/EstateReportingAPI/Handlers/ContractHandler.cs @@ -1,4 +1,5 @@ using EstateReportingAPI.BusinessLogic.Queries; +using EstateReportingAPI.Common; using EstateReportingAPI.Models; using MediatR; using Microsoft.AspNetCore.Mvc; @@ -8,11 +9,11 @@ namespace EstateReportingAPI.Handlers; public static class ContractHandler { - public static async Task GetRecentContracts([FromHeader] Guid estateId, + public static async Task GetRecentContracts(IEstateContext estateContext, IMediator mediator, CancellationToken cancellationToken) { - ContractQueries.GetRecentContractsQuery query = new ContractQueries.GetRecentContractsQuery(estateId); + ContractQueries.GetRecentContractsQuery query = new ContractQueries.GetRecentContractsQuery(estateContext.EstateId); Result> result = await mediator.Send(query, cancellationToken); return ResponseFactory.FromResult(result, r => r.Select(m => new DataTransferObjects.Contract { @@ -27,11 +28,11 @@ public static async Task GetRecentContracts([FromHeader] Guid estateId, }).ToList()); } - public static async Task GetContracts([FromHeader] Guid estateId, + public static async Task GetContracts(IEstateContext estateContext, IMediator mediator, CancellationToken cancellationToken) { - ContractQueries.GetContractsQuery query = new ContractQueries.GetContractsQuery(estateId); + ContractQueries.GetContractsQuery query = new ContractQueries.GetContractsQuery(estateContext.EstateId); Result> result = await mediator.Send(query, cancellationToken); return ResponseFactory.FromResult(result, r => r.Select(m => new DataTransferObjects.Contract { @@ -64,12 +65,12 @@ public static async Task GetContracts([FromHeader] Guid estateId, }).ToList()); } - public static async Task GetContract([FromHeader] Guid estateId, + public static async Task GetContract(IEstateContext estateContext, [FromRoute] Guid contractId, IMediator mediator, CancellationToken cancellationToken) { - ContractQueries.GetContractQuery query = new ContractQueries.GetContractQuery(estateId, contractId); + ContractQueries.GetContractQuery query = new ContractQueries.GetContractQuery(estateContext.EstateId, contractId); Result result = await mediator.Send(query, cancellationToken); return ResponseFactory.FromResult(result, r => new DataTransferObjects.Contract { diff --git a/EstateReportingAPI/Handlers/EstateHandler.cs b/EstateReportingAPI/Handlers/EstateHandler.cs index ff8584e..df13a99 100644 --- a/EstateReportingAPI/Handlers/EstateHandler.cs +++ b/EstateReportingAPI/Handlers/EstateHandler.cs @@ -1,4 +1,5 @@ using EstateReportingAPI.BusinessLogic.Queries; +using EstateReportingAPI.Common; using EstateReportingAPI.Models; using MediatR; using Microsoft.AspNetCore.Mvc; @@ -8,11 +9,11 @@ namespace EstateReportingAPI.Handlers; public static class EstateHandler { - public static async Task GetEstate([FromHeader] Guid estateId, + public static async Task GetEstate(IEstateContext estateContext, IMediator mediator, CancellationToken cancellationToken) { - EstateQueries.GetEstateQuery query = new EstateQueries.GetEstateQuery(estateId); + EstateQueries.GetEstateQuery query = new EstateQueries.GetEstateQuery(estateContext.EstateId); Result result = await mediator.Send(query, cancellationToken); return ResponseFactory.FromResult(result, r => { @@ -60,11 +61,11 @@ public static async Task GetEstate([FromHeader] Guid estateId, }); } - public static async Task GetOperators([FromHeader] Guid estateId, + public static async Task GetOperators(IEstateContext estateContext, IMediator mediator, CancellationToken cancellationToken) { - EstateQueries.GetEstateOperatorsQuery query = new EstateQueries.GetEstateOperatorsQuery(estateId); + EstateQueries.GetEstateOperatorsQuery query = new EstateQueries.GetEstateOperatorsQuery(estateContext.EstateId); Result> result = await mediator.Send(query, cancellationToken); return ResponseFactory.FromResult(result, r => { diff --git a/EstateReportingAPI/Handlers/FileImportHandler.cs b/EstateReportingAPI/Handlers/FileImportHandler.cs index 6b29f5c..f61e09b 100644 --- a/EstateReportingAPI/Handlers/FileImportHandler.cs +++ b/EstateReportingAPI/Handlers/FileImportHandler.cs @@ -1,4 +1,5 @@ using EstateReportingAPI.BusinessLogic.Queries; +using EstateReportingAPI.Common; using EstateReportingAPI.Models; using MediatR; using Microsoft.AspNetCore.Mvc; @@ -9,11 +10,11 @@ namespace EstateReportingAPI.Handlers; public static class FileProfileConfigurationHandler { - public static async Task GetFileProfileConfigurationList([FromHeader] Guid estateId, + public static async Task GetFileProfileConfigurationList(IEstateContext estateContext, IMediator mediator, CancellationToken cancellationToken) { - FileProfileConfigurationQueries.GetFileProfileConfigurationListQuery query = new(estateId); + FileProfileConfigurationQueries.GetFileProfileConfigurationListQuery query = new(estateContext.EstateId); Result> result = await mediator.Send(query, cancellationToken); return ResponseFactory.FromResult(result, r => r.Select(m => new DataTransferObjects.FileProfileConfiguration() @@ -31,14 +32,14 @@ public static async Task GetFileProfileConfigurationList([FromHeader] G public static class FileImportHandler { - public static async Task GetFileImportLogList([FromHeader] Guid estateId, + public static async Task GetFileImportLogList(IEstateContext estateContext, [FromQuery] Guid? merchantId, [FromQuery] DateTime startDate, [FromQuery] DateTime endDate, IMediator mediator, CancellationToken cancellationToken) { - FileImportLogQueries.GetFileImportLogListQuery query = new(estateId, merchantId, startDate, endDate); + FileImportLogQueries.GetFileImportLogListQuery query = new(estateContext.EstateId, merchantId, startDate, endDate); Result> result = await mediator.Send(query, cancellationToken); return ResponseFactory.FromResult(result, r => r.Select(m => new DataTransferObjects.FileImportLog @@ -63,13 +64,13 @@ public static async Task GetFileImportLogList([FromHeader] Guid estateI }).ToList()); } - public static async Task GetFileImportLog([FromHeader] Guid estateId, - [FromRoute] Guid fileImportLogId, + public static async Task GetFileImportLog(IEstateContext estateContext, + [FromRoute] Guid fileImportLogId, [FromQuery] Guid? merchantId, IMediator mediator, CancellationToken cancellationToken) { - FileImportLogQueries.GetFileImportLogQuery query = new(estateId, merchantId, fileImportLogId); + FileImportLogQueries.GetFileImportLogQuery query = new(estateContext.EstateId, merchantId, fileImportLogId); Result result = await mediator.Send(query, cancellationToken); return ResponseFactory.FromResult(result, r => new DataTransferObjects.FileImportLog @@ -95,4 +96,4 @@ public static async Task GetFileImportLog([FromHeader] Guid estateId, }).ToList() }); } -} \ No newline at end of file +} diff --git a/EstateReportingAPI/Handlers/MerchantHandler.cs b/EstateReportingAPI/Handlers/MerchantHandler.cs index 40524ad..a0e3819 100644 --- a/EstateReportingAPI/Handlers/MerchantHandler.cs +++ b/EstateReportingAPI/Handlers/MerchantHandler.cs @@ -1,4 +1,5 @@ using EstateReportingAPI.BusinessLogic.Queries; +using EstateReportingAPI.Common; using EstateReportingAPI.Models; using MediatR; using Microsoft.AspNetCore.Mvc; @@ -8,18 +9,18 @@ namespace EstateReportingAPI.Handlers; public static class MerchantHandler { - public static async Task GetMerchantKpis([FromHeader] Guid estateId, + public static async Task GetMerchantKpis(IEstateContext estateContext, IMediator mediator, CancellationToken cancellationToken) { - MerchantQueries.GetTransactionKpisQuery query = new(estateId); + MerchantQueries.GetTransactionKpisQuery query = new(estateContext.EstateId); Result result = await mediator.Send(query, cancellationToken); return ResponseFactory.FromResult(result, r => new DataTransferObjects.MerchantKpi() { MerchantsWithNoSaleInLast7Days = r.MerchantsWithNoSaleInLast7Days, MerchantsWithNoSaleToday = r.MerchantsWithNoSaleToday, MerchantsWithSaleInLastHour = r.MerchantsWithSaleInLastHour }); } - public static async Task GetRecentMerchants([FromHeader] Guid estateId, + public static async Task GetRecentMerchants(IEstateContext estateContext, IMediator mediator, CancellationToken cancellationToken) { - MerchantQueries.GetRecentMerchantsQuery query = new(estateId); + MerchantQueries.GetRecentMerchantsQuery query = new(estateContext.EstateId); Result> result = await mediator.Send(query, cancellationToken); return ResponseFactory.FromResult(result, r => r.Select(m => new EstateReportingAPI.DataTransferObjects.Merchant { MerchantReportingId = m.MerchantReportingId, @@ -43,7 +44,7 @@ public static async Task GetRecentMerchants([FromHeader] Guid estateId, }).OrderByDescending(m => m.CreatedDateTime).ToList()); } - public static async Task GetMerchants([FromHeader] Guid estateId, + public static async Task GetMerchants(IEstateContext estateContext, [FromQuery] String? name, [FromQuery] String? reference, [FromQuery] Int32? settlementSchedule, @@ -53,7 +54,7 @@ public static async Task GetMerchants([FromHeader] Guid estateId, CancellationToken cancellationToken) { // Build the query options MerchantQueries.MerchantQueryOptions queryOptions = new(name, reference, settlementSchedule, region, postCode); - MerchantQueries.GetMerchantsQuery query = new(estateId, queryOptions); + MerchantQueries.GetMerchantsQuery query = new(estateContext.EstateId, queryOptions); Result> result = await mediator.Send(query, cancellationToken); return ResponseFactory.FromResult(result, r => r.Select(m => new EstateReportingAPI.DataTransferObjects.Merchant { @@ -78,12 +79,12 @@ public static async Task GetMerchants([FromHeader] Guid estateId, }).OrderByDescending(m => m.CreatedDateTime).ToList()); } - public static async Task GetMerchant([FromHeader] Guid estateId, + public static async Task GetMerchant(IEstateContext estateContext, [FromRoute] Guid merchantId, IMediator mediator, CancellationToken cancellationToken) { // Build the query options - MerchantQueries.GetMerchantQuery query = new(estateId, merchantId); + MerchantQueries.GetMerchantQuery query = new(estateContext.EstateId, merchantId); Result result = await mediator.Send(query, cancellationToken); return ResponseFactory.FromResult(result, r => new EstateReportingAPI.DataTransferObjects.Merchant { @@ -108,11 +109,11 @@ public static async Task GetMerchant([FromHeader] Guid estateId, }); } - public static async Task GetMerchantOperators([FromHeader] Guid estateId, + public static async Task GetMerchantOperators(IEstateContext estateContext, [FromRoute] Guid merchantId, IMediator mediator, CancellationToken cancellationToken) { - MerchantQueries.GetMerchantOperatorsQuery query = new(estateId, merchantId); + MerchantQueries.GetMerchantOperatorsQuery query = new(estateContext.EstateId, merchantId); Result> result = await mediator.Send(query, cancellationToken); return ResponseFactory.FromResult(result, r => r.Select(o => new DataTransferObjects.MerchantOperator() { @@ -127,11 +128,11 @@ public static async Task GetMerchantOperators([FromHeader] Guid estateI - public static async Task GetMerchantContracts([FromHeader] Guid estateId, + public static async Task GetMerchantContracts(IEstateContext estateContext, [FromRoute] Guid merchantId, IMediator mediator, CancellationToken cancellationToken) { - MerchantQueries.GetMerchantContractsQuery query = new(estateId, merchantId); + MerchantQueries.GetMerchantContractsQuery query = new(estateContext.EstateId, merchantId); Result> result = await mediator.Send(query, cancellationToken); return ResponseFactory.FromResult(result, r => r.Select(c => new DataTransferObjects.MerchantContract() { @@ -150,32 +151,32 @@ public static async Task GetMerchantContracts([FromHeader] Guid estateI }).ToList()); } - public static async Task GetMerchantDevices([FromHeader] Guid estateId, + public static async Task GetMerchantDevices(IEstateContext estateContext, [FromRoute] Guid merchantId, IMediator mediator, CancellationToken cancellationToken) { - MerchantQueries.GetMerchantDevicesQuery query = new(estateId, merchantId); + MerchantQueries.GetMerchantDevicesQuery query = new(estateContext.EstateId, merchantId); Result> result = await mediator.Send(query, cancellationToken); return ResponseFactory.FromResult(result, r => r.Select(c => new DataTransferObjects.MerchantDevice() { IsDeleted = c.IsDeleted, MerchantId = c.MerchantId, DeviceId = c.DeviceId, DeviceIdentifier = c.DeviceIdentifier }).ToList()); } - public static async Task GetMerchantOpeningHours([FromHeader] Guid estateId, + public static async Task GetMerchantOpeningHours(IEstateContext estateContext, [FromRoute] Guid merchantId, IMediator mediator, CancellationToken cancellationToken) { - MerchantQueries.GetMerchantOpeningHoursQuery query = new(estateId, merchantId); + MerchantQueries.GetMerchantOpeningHoursQuery query = new(estateContext.EstateId, merchantId); Result> result = await mediator.Send(query, cancellationToken); return ResponseFactory.FromResult(result, r => r.Select(c => new DataTransferObjects.MerchantOpeningHour() { MerchantId = c.MerchantId, DayOfWeek = c.DayOfWeek, OpeningTime = c.OpeningTime, ClosingTime = c.ClosingTime }).ToList()); } - public static async Task GetMerchantSchedule([FromHeader] Guid estateId, + public static async Task GetMerchantSchedule(IEstateContext estateContext, [FromRoute] Guid merchantId, [FromRoute] Int32 year, IMediator mediator, CancellationToken cancellationToken) { - MerchantQueries.GetMerchantScheduleQuery query = new(estateId, merchantId, year); + MerchantQueries.GetMerchantScheduleQuery query = new(estateContext.EstateId, merchantId, year); Result result = await mediator.Send(query, cancellationToken); return ResponseFactory.FromResult(result, r => new DataTransferObjects.MerchantScheduleResponse() { Year = r.Year, Months = r.Months.Select(m => new DataTransferObjects.MerchantScheduleMonthResponse() { Month = m.Month, ClosedDays = m.ClosedDays }).ToList() }); diff --git a/EstateReportingAPI/Handlers/OperatorHandler.cs b/EstateReportingAPI/Handlers/OperatorHandler.cs index 84ce179..a632ea5 100644 --- a/EstateReportingAPI/Handlers/OperatorHandler.cs +++ b/EstateReportingAPI/Handlers/OperatorHandler.cs @@ -1,4 +1,5 @@ using EstateReportingAPI.BusinessLogic.Queries; +using EstateReportingAPI.Common; using EstateReportingAPI.Models; using MediatR; using Microsoft.AspNetCore.Mvc; @@ -8,11 +9,11 @@ namespace EstateReportingAPI.Handlers; public static class OperatorHandler { - public static async Task GetOperators([FromHeader] Guid estateId, + public static async Task GetOperators(IEstateContext estateContext, IMediator mediator, CancellationToken cancellationToken) { - OperatorQueries.GetOperatorsQuery query = new(estateId); + OperatorQueries.GetOperatorsQuery query = new(estateContext.EstateId); Result> result = await mediator.Send(query, cancellationToken); return ResponseFactory.FromResult(result, r => r.Select(m => new DataTransferObjects.Operator { @@ -25,12 +26,12 @@ public static async Task GetOperators([FromHeader] Guid estateId, }).ToList()); } - public static async Task GetOperator([FromHeader] Guid estateId, + public static async Task GetOperator(IEstateContext estateContext, [FromRoute] Guid operatorId, IMediator mediator, CancellationToken cancellationToken) { - OperatorQueries.GetOperatorQuery query = new(estateId, operatorId); + OperatorQueries.GetOperatorQuery query = new(estateContext.EstateId, operatorId); Result result = await mediator.Send(query, cancellationToken); return ResponseFactory.FromResult(result, r => new DataTransferObjects.Operator { diff --git a/EstateReportingAPI/Handlers/SettlementHandler.cs b/EstateReportingAPI/Handlers/SettlementHandler.cs index 044fba6..b25f87b 100644 --- a/EstateReportingAPI/Handlers/SettlementHandler.cs +++ b/EstateReportingAPI/Handlers/SettlementHandler.cs @@ -1,4 +1,5 @@ using EstateReportingAPI.BusinessLogic.Queries; +using EstateReportingAPI.Common; using EstateReportingAPI.DataTransferObjects; using MediatR; using Microsoft.AspNetCore.Mvc; @@ -7,11 +8,11 @@ namespace EstateReportingAPI.Handlers; public static class SettlementHandler { - public static async Task TodaysSettlements([FromHeader] Guid estateId, + public static async Task TodaysSettlements(IEstateContext estateContext, [FromQuery] DateTime comparisonDate, IMediator mediator, CancellationToken cancellationToken) { - var query = new SettlementQueries.TodaysSettlementQuery(estateId, comparisonDate); + var query = new SettlementQueries.TodaysSettlementQuery(estateContext.EstateId, comparisonDate); var result = await mediator.Send(query, cancellationToken); return ResponseFactory.FromResult(result, r => new TodaysSettlement() { diff --git a/EstateReportingAPI/Handlers/TransactionHandler.cs b/EstateReportingAPI/Handlers/TransactionHandler.cs index 2e29eee..39f5215 100644 --- a/EstateReportingAPI/Handlers/TransactionHandler.cs +++ b/EstateReportingAPI/Handlers/TransactionHandler.cs @@ -1,4 +1,5 @@ using EstateReportingAPI.BusinessLogic.Queries; +using EstateReportingAPI.Common; using EstateReportingAPI.DataTransferObjects; using MediatR; using Microsoft.AspNetCore.Mvc; @@ -10,14 +11,14 @@ namespace EstateReportingAPI.Handlers; public static class TransactionHandler { - public static async Task TodaysSales([FromHeader] Guid estateId, + public static async Task TodaysSales(IEstateContext estateContext, [FromQuery] int? merchantReportingId, [FromQuery] int? operatorReportingId, [FromQuery] DateTime comparisonDate, IMediator mediator, CancellationToken cancellationToken) { - var query = new TransactionQueries.TodaysSalesQuery(estateId, merchantReportingId.GetValueOrDefault(), operatorReportingId.GetValueOrDefault(), comparisonDate); + var query = new TransactionQueries.TodaysSalesQuery(estateContext.EstateId, merchantReportingId.GetValueOrDefault(), operatorReportingId.GetValueOrDefault(), comparisonDate); var result = await mediator.Send(query, cancellationToken); return ResponseFactory.FromResult(result, r => new TodaysSales @@ -31,13 +32,13 @@ public static async Task TodaysSales([FromHeader] Guid estateId, }); } - public static async Task TodaysFailedSales([FromHeader] Guid estateId, + public static async Task TodaysFailedSales(IEstateContext estateContext, [FromQuery] DateTime comparisonDate, [FromQuery] string responseCode, IMediator mediator, CancellationToken cancellationToken) { - var query = new TransactionQueries.TodaysFailedSales(estateId, comparisonDate, responseCode); + var query = new TransactionQueries.TodaysFailedSales(estateContext.EstateId, comparisonDate, responseCode); var result = await mediator.Send(query, cancellationToken); return ResponseFactory.FromResult(result, r => new TodaysSales @@ -51,7 +52,7 @@ public static async Task TodaysFailedSales([FromHeader] Guid estateId, }); } - public static async Task TransactionDetailReport([FromHeader] Guid estateId, + public static async Task TransactionDetailReport(IEstateContext estateContext, [FromBody] TransactionDetailReportRequest request, IMediator mediator, CancellationToken cancellationToken) { Models.TransactionDetailReportRequest queryRequest = new() { @@ -62,7 +63,7 @@ public static async Task TransactionDetailReport([FromHeader] Guid esta EndDate = request.EndDate }; - var query = new TransactionQueries.TransactionDetailReportQuery(estateId, queryRequest); + var query = new TransactionQueries.TransactionDetailReportQuery(estateContext.EstateId, queryRequest); var result = await mediator.Send(query, cancellationToken); TransactionDetailReportResponse SuccessFactory (Models.TransactionDetailReportResponse r) => @@ -94,7 +95,7 @@ TransactionDetailReportResponse SuccessFactory (Models.TransactionDetailReportRe return ResponseFactory.FromResult(result, SuccessFactory); } - public static async Task TransactionSummaryByMerchantReport([FromHeader] Guid estateId, + public static async Task TransactionSummaryByMerchantReport(IEstateContext estateContext, [FromBody] TransactionSummaryByMerchantRequest request, IMediator mediator, CancellationToken cancellationToken) { Models.TransactionSummaryByMerchantRequest queryRequest = new() { @@ -103,7 +104,7 @@ public static async Task TransactionSummaryByMerchantReport([FromHeader StartDate = request.StartDate, EndDate = request.EndDate }; - var query = new TransactionQueries.TransactionSummaryByMerchantQuery(estateId, queryRequest); + var query = new TransactionQueries.TransactionSummaryByMerchantQuery(estateContext.EstateId, queryRequest); var result = await mediator.Send(query, cancellationToken); TransactionSummaryByMerchantResponse SuccessFactory (Models.TransactionSummaryByMerchantResponse r) => new TransactionSummaryByMerchantResponse { @@ -124,12 +125,12 @@ TransactionSummaryByMerchantResponse SuccessFactory (Models.TransactionSummaryBy return ResponseFactory.FromResult(result, SuccessFactory); } - public static async Task ProductPerformanceReport([FromHeader] Guid estateId, + public static async Task ProductPerformanceReport(IEstateContext estateContext, [FromQuery] DateTime startDate, [FromQuery] DateTime endDate, IMediator mediator, CancellationToken cancellationToken) { - var query = new TransactionQueries.ProductPerformanceQuery(estateId, startDate, endDate); + var query = new TransactionQueries.ProductPerformanceQuery(estateContext.EstateId, startDate, endDate); var result = await mediator.Send(query, cancellationToken); ProductPerformanceResponse SuccessFactory(Models.ProductPerformanceResponse r) => new ProductPerformanceResponse @@ -151,7 +152,7 @@ ProductPerformanceResponse SuccessFactory(Models.ProductPerformanceResponse r) = return ResponseFactory.FromResult(result, SuccessFactory); } - public static async Task TransactionSummaryByOperatorReport([FromHeader] Guid estateId, + public static async Task TransactionSummaryByOperatorReport(IEstateContext estateContext, [FromBody] TransactionSummaryByOperatorRequest request, IMediator mediator, CancellationToken cancellationToken) { @@ -162,7 +163,7 @@ public static async Task TransactionSummaryByOperatorReport([FromHeader StartDate = request.StartDate, EndDate = request.EndDate }; - var query = new TransactionQueries.TransactionSummaryByOperatorQuery(estateId, queryRequest); + var query = new TransactionQueries.TransactionSummaryByOperatorQuery(estateContext.EstateId, queryRequest); var result = await mediator.Send(query, cancellationToken); TransactionSummaryByOperatorResponse SuccessFactory(Models.TransactionSummaryByOperatorResponse r) => new TransactionSummaryByOperatorResponse @@ -185,7 +186,7 @@ TransactionSummaryByOperatorResponse SuccessFactory(Models.TransactionSummaryByO return ResponseFactory.FromResult(result, SuccessFactory); } - public static async Task TransactionMixSummary([FromHeader] Guid estateId, + public static async Task TransactionMixSummary(IEstateContext estateContext, [FromBody] TransactionMixSummaryRequest request, IMediator mediator, CancellationToken cancellationToken) @@ -203,7 +204,7 @@ public static async Task TransactionMixSummary([FromHeader] Guid estate TopN = request.TopN }; - var query = new TransactionQueries.TransactionMixSummaryQuery(estateId, queryRequest); + var query = new TransactionQueries.TransactionMixSummaryQuery(estateContext.EstateId, queryRequest); var result = await mediator.Send(query, cancellationToken); TransactionMixSummaryResponse SuccessFactory(Models.TransactionMixSummaryResponse r) => @@ -247,7 +248,7 @@ TransactionMixSummaryResponse SuccessFactory(Models.TransactionMixSummaryRespons return ResponseFactory.FromResult(result, SuccessFactory); } - public static async Task RecentActivityReceiptReport([FromHeader] Guid estateId, + public static async Task RecentActivityReceiptReport(IEstateContext estateContext, [FromBody] GetRecentActivityReceiptReportRequest request, IMediator mediator, CancellationToken cancellationToken) @@ -261,7 +262,7 @@ public static async Task RecentActivityReceiptReport([FromHeader] Guid PageSize = request.PageSize }; - var query = new TransactionQueries.GetRecentActivityReceiptReportQuery(estateId, queryRequest); + var query = new TransactionQueries.GetRecentActivityReceiptReportQuery(estateContext.EstateId, queryRequest); var result = await mediator.Send(query, cancellationToken); GetRecentActivityReceiptReportResponse SuccessFactory(Models.GetRecentActivityReceiptReportResponse r) => @@ -287,12 +288,12 @@ GetRecentActivityReceiptReportResponse SuccessFactory(Models.GetRecentActivityRe return ResponseFactory.FromResult(result, SuccessFactory); } - public static async Task TodaysSalesByHour([FromHeader] Guid estateId, - [FromQuery] DateTime comparisonDate, - IMediator mediator, - CancellationToken cancellationToken) + public static async Task TodaysSalesByHour(IEstateContext estateContext, + [FromQuery] DateTime comparisonDate, + IMediator mediator, + CancellationToken cancellationToken) { - var query = new TransactionQueries.TodaysSalesByHour(estateId, comparisonDate); + var query = new TransactionQueries.TodaysSalesByHour(estateContext.EstateId, comparisonDate); var result = await mediator.Send(query, cancellationToken); List SuccessFactory(List r) => @@ -309,11 +310,11 @@ public static async Task TodaysSalesByHour([FromHeader] Guid estateId, return ResponseFactory.FromResult(result, SuccessFactory); } - public static async Task MerchantDailyPerformanceSummary([FromHeader] Guid estateId, + public static async Task MerchantDailyPerformanceSummary(IEstateContext estateContext, [FromBody] MerchantDailyPerformanceSummaryRequest request, IMediator mediator, CancellationToken cancellationToken) { - TransactionQueries.MerchantDailyPerformanceSummaryQuery query = new(estateId, new Models.MerchantDailyPerformanceSummaryRequest { + TransactionQueries.MerchantDailyPerformanceSummaryQuery query = new(estateContext.EstateId, new Models.MerchantDailyPerformanceSummaryRequest { EndDate = request.EndDate, MerchantReportingId = request.MerchantReportingId, StartDate = request.StartDate diff --git a/EstateReportingAPI/Startup.cs b/EstateReportingAPI/Startup.cs index 1fb8232..ff9ff10 100644 --- a/EstateReportingAPI/Startup.cs +++ b/EstateReportingAPI/Startup.cs @@ -10,6 +10,7 @@ namespace EstateReportingAPI { + using EstateReportingAPI.Common; using Shared.Middleware; using Shared.Serialisation; @@ -61,6 +62,7 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerF app.UseRouting(); app.UseAuthentication(); + app.UseMiddleware(); app.UseAuthorization(); app.UseEndpoints(endpoints => @@ -79,14 +81,14 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerF { Predicate = _ => true, ResponseWriter = Shared.HealthChecks.HealthCheckMiddleware.WriteResponse - }); + }).AllowAnonymous(); endpoints.MapHealthChecks("healthui", new HealthCheckOptions() { Predicate = _ => true, ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse - }); + }).AllowAnonymous(); }); } } -} \ No newline at end of file +}