Completed token service
continuous-integration/drone/pr Build is passing

This commit is contained in:
Khwezi Mngoma
2026-06-12 20:48:12 +02:00
parent 3daf192ce9
commit 4d2b37ace7
8 changed files with 68 additions and 20 deletions
@@ -1,15 +1,16 @@
using LiteCharms.Features.Hasher; using LiteCharms.Features.Api.Configuration;
using LiteCharms.Features.Hasher.Configuration; using LiteCharms.Features.Hasher;
using LiteCharms.Features.Mediator; using LiteCharms.Features.Mediator;
using LiteCharms.Features.MidrandBooks.Orders; using LiteCharms.Features.MidrandBooks.Orders;
using LiteCharms.Features.MidrandBooks.Payments.Models; using LiteCharms.Features.MidrandBooks.Payments.Models;
namespace LiteCharms.Features.MidrandBooks.Payments.Events.Handlers; namespace LiteCharms.Features.MidrandBooks.Payments.Events.Handlers;
public sealed class PayfastPaymentConfirmationReceivedEventHandler(IServiceProvider services, IOptions<HasherSettings> hasherOptions, ILogger<PayfastPaymentConfirmationReceivedEvent> logger) : public sealed class PayfastPaymentConfirmationReceivedEventHandler(IServiceProvider services,
IOptions<PayfastSettings> payfastOptions, ILogger<PayfastPaymentConfirmationReceivedEvent> logger) :
INotificationHandler<PayfastPaymentConfirmationReceivedEvent> INotificationHandler<PayfastPaymentConfirmationReceivedEvent>
{ {
private readonly HasherSettings hasherSettings = hasherOptions.Value; private readonly PayfastSettings pasfastSettings = payfastOptions.Value;
public async ValueTask Handle(PayfastPaymentConfirmationReceivedEvent notification, CancellationToken cancellationToken) public async ValueTask Handle(PayfastPaymentConfirmationReceivedEvent notification, CancellationToken cancellationToken)
{ {
@@ -25,7 +26,7 @@ public sealed class PayfastPaymentConfirmationReceivedEventHandler(IServiceProvi
var payload = notification.Payload ?? throw new Exception("Payload metadata context context is null."); var payload = notification.Payload ?? throw new Exception("Payload metadata context context is null.");
var dict = payload.ToParamDictionary(); var dict = payload.ToParamDictionary();
var localSignature = PayfastService.GenerateSignature(dict, hasherSettings.PayfastPassphrase); var localSignature = PayfastService.GenerateSignature(dict, pasfastSettings.Passphrase);
if (localSignature.IsFailed) if (localSignature.IsFailed)
throw new Exception("Failed to generate local signature for incoming webhook payload."); throw new Exception("Failed to generate local signature for incoming webhook payload.");
@@ -159,6 +160,5 @@ public sealed class PayfastPaymentConfirmationReceivedEventHandler(IServiceProvi
logger.LogInformation("Webhook validation pipeline passed checks successfully, logged entry to ledger with status: {Status}", status); logger.LogInformation("Webhook validation pipeline passed checks successfully, logged entry to ledger with status: {Status}", status);
} }
activity?.SetStatus(ActivityStatusCode.Ok); activity?.SetStatus(ActivityStatusCode.Ok);
} }
} }
@@ -1,4 +1,5 @@
using LiteCharms.Features.Abstractions; using LiteCharms.Features.Abstractions;
using LiteCharms.Features.Api.Configuration;
using LiteCharms.Features.Hasher; using LiteCharms.Features.Hasher;
using LiteCharms.Features.MidrandBooks.Payments.Models; using LiteCharms.Features.MidrandBooks.Payments.Models;
using LiteCharms.Features.MidrandBooks.Postgres; using LiteCharms.Features.MidrandBooks.Postgres;
@@ -6,13 +7,11 @@ using LiteCharms.Features.MidrandBooks.Postgres;
namespace LiteCharms.Features.MidrandBooks.Payments; namespace LiteCharms.Features.MidrandBooks.Payments;
public sealed partial class PayfastService(IDbContextFactory<MidrandBooksDbContext> contextFactory, public sealed partial class PayfastService(IDbContextFactory<MidrandBooksDbContext> contextFactory,
ILogger<PayfastService> logger, IHttpClientFactory httpClientFactory, IConfiguration configuration) : IService IOptions<PayfastSettings> payfastOptions, ILogger<PayfastService> logger, IHttpClientFactory httpClientFactory) : IService
{ {
[GeneratedRegex(@"%[0-9A-Fa-f]{2}", RegexOptions.None, matchTimeoutMilliseconds: 1000)] [GeneratedRegex(@"%[0-9A-Fa-f]{2}", RegexOptions.None, matchTimeoutMilliseconds: 1000)]
public static partial Regex PercentEncodingRegex { get; } public static partial Regex PercentEncodingRegex { get; }
public readonly string[] ValidHosts = configuration.GetSection("ValidPayfastHosts").Get<string[]>() ?? [];
public async ValueTask<Result<long>> WriteLedgerEntryAsync(CreateGatewayLedgerEntry request, CancellationToken cancellationToken = default) public async ValueTask<Result<long>> WriteLedgerEntryAsync(CreateGatewayLedgerEntry request, CancellationToken cancellationToken = default)
{ {
try try
@@ -51,6 +50,9 @@ public sealed partial class PayfastService(IDbContextFactory<MidrandBooksDbConte
public async ValueTask<Result<bool>> ValidateReferrerIpAsync(string remoteIpAddress, bool allowLoopback = false, CancellationToken cancellationToken = default) public async ValueTask<Result<bool>> ValidateReferrerIpAsync(string remoteIpAddress, bool allowLoopback = false, CancellationToken cancellationToken = default)
{ {
if(payfastOptions.Value?.ValidHosts?.Length == 0)
return Result.Fail<bool>("Valid payfast hosts not configured.");
if (string.IsNullOrWhiteSpace(remoteIpAddress)) if (string.IsNullOrWhiteSpace(remoteIpAddress))
return Result.Fail<bool>("Remote IP address is null or whitespace."); return Result.Fail<bool>("Remote IP address is null or whitespace.");
@@ -58,7 +60,7 @@ public sealed partial class PayfastService(IDbContextFactory<MidrandBooksDbConte
{ {
var validIps = new HashSet<IPAddress>(); var validIps = new HashSet<IPAddress>();
foreach (var host in ValidHosts) foreach (var host in payfastOptions.Value!.ValidHosts!)
{ {
try try
{ {
@@ -36,6 +36,7 @@ public class Fixture : IDisposable
.AddHashServices(Configuration) .AddHashServices(Configuration)
.AddLiteCharmsApiSecurity(Configuration) .AddLiteCharmsApiSecurity(Configuration)
.AddSecurityApiSdk(Configuration) .AddSecurityApiSdk(Configuration)
.AddPayfastServices(Configuration)
.BuildServiceProvider(); ; .BuildServiceProvider(); ;
Mediator = Services.GetRequiredService<IMediator>(); Mediator = Services.GetRequiredService<IMediator>();
@@ -1,22 +1,25 @@
{ {
"PayfastSettings": {
"CheckoutUrl": "https://sandbox.payfast.co.za/eng/process",
"ValidHosts": [
"www.payfast.co.za",
"sandbox.payfast.co.za",
"w1w.payfast.co.za",
"w2w.payfast.co.za",
"ips.payfast.co.za",
"api.payfast.co.za",
"payment.payfast.io"
]
},
"LiteCharmsSettings": { "LiteCharmsSettings": {
"Authority": "https://sts.security.khongisa.co.za", "Authority": "https://sts.security.khongisa.co.za",
"Audience": "midrandbooks-api" "Audience": "midrandbooks-api"
}, },
"LiteCharmsClientSettings": { "LiteCharmsClientSettings": {
"Authority": "https://sts.security.khongisa.co.za", "Authority": "https://sts.security.khongisa.co.za",
"GrantType": "client_credentials", "GrantType": "client_credentials",
"Scope": "midrandbooks-api" "Scope": "midrandbooks-api"
}, },
"ValidPayfastHosts": [
"www.payfast.co.za",
"sandbox.payfast.co.za",
"w1w.payfast.co.za",
"w2w.payfast.co.za",
"ips.payfast.co.za",
"api.payfast.co.za",
"payment.payfast.io"
],
"HasherSettings": { "HasherSettings": {
"MinHashLength": 11 "MinHashLength": 11
}, },
@@ -31,6 +31,7 @@
<Using Include="System.Text" /> <Using Include="System.Text" />
<Using Include="Mediator" /> <Using Include="Mediator" />
<Using Include="Xunit.Abstractions" /> <Using Include="Xunit.Abstractions" />
<Using Include="Microsoft.Extensions.Options" />
<Using Include="Microsoft.Extensions.DependencyInjection" /> <Using Include="Microsoft.Extensions.DependencyInjection" />
<Using Include="Microsoft.Extensions.Configuration" /> <Using Include="Microsoft.Extensions.Configuration" />
</ItemGroup> </ItemGroup>
@@ -0,0 +1,18 @@
using LiteCharms.Features.Api.Configuration;
using LiteCharms.Features.Tests.Common;
namespace LiteCharms.Features.Tests;
public sealed class PayfastFeatureTests(Fixture fixture) : IClassFixture<Fixture>
{
private readonly PayfastSettings payfastSettings = fixture.Services.GetRequiredService<IOptions<PayfastSettings>>().Value;
[IntegrationFact]
public void PayfastSettings_ShouldFail_IfNotLoaded()
{
Assert.NotEmpty(payfastSettings.CheckoutUrl!);
Assert.NotEmpty(payfastSettings.MerchantId!);
Assert.NotEmpty(payfastSettings.MerchantKey!);
Assert.NotEmpty(payfastSettings.Passphrase!);
}
}
@@ -0,0 +1,14 @@
namespace LiteCharms.Features.Api.Configuration;
public sealed class PayfastSettings
{
public string? CheckoutUrl { get; set; }
public string? Passphrase { get; set; }
public string? MerchantId { get; set; }
public string? MerchantKey { get; set; }
public string[]? ValidHosts { get; set; }
}
+9
View File
@@ -9,6 +9,15 @@ public static class Api
{ {
public const string Books = nameof(Books); public const string Books = nameof(Books);
public const string Payments = nameof(Payments); public const string Payments = nameof(Payments);
public static IServiceCollection AddPayfastServices(this IServiceCollection services, IConfiguration configuration)
{
var configSection = configuration.GetSection(nameof(PayfastSettings));
services.Configure<PayfastSettings>(configSection);
return services;
}
public static IServiceCollection AddSecurityApiSdk(this IServiceCollection services, IConfiguration configuration) public static IServiceCollection AddSecurityApiSdk(this IServiceCollection services, IConfiguration configuration)
{ {