Compare commits
61
Commits
c4f73fd999
...
1.61.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b369dad452 | ||
|
|
ac31c6ada8 | ||
|
|
c938bfec09 | ||
|
|
5eb6dbc8b2 | ||
|
|
adc14038db | ||
|
|
afc984f3bc | ||
|
|
61cb6c2228 | ||
|
|
f1ef614cbb | ||
|
|
8fe129e19c | ||
|
|
e7a798b5e9 | ||
|
|
41b6b71b31 | ||
|
|
ee6beef603 | ||
|
|
1c3f3eaf0d | ||
|
|
2e77666d9e | ||
|
|
1977b6b301 | ||
|
|
0ab14d8b63 | ||
|
|
466458e230 | ||
|
|
141d32f591 | ||
|
|
d9e7f225ae | ||
|
|
6ae63e2ad1 | ||
|
|
f5efdde37c | ||
|
|
1592d5dc8f | ||
|
|
50a8a59d92 | ||
|
|
b70d9559b0 | ||
|
|
81d5e8f07c | ||
|
|
20a53942b5 | ||
|
|
9edb2aa4aa | ||
|
|
6ed023f2cf | ||
|
|
2c9f5a846c | ||
|
|
41f7c05be3 | ||
|
|
1a03355e84 | ||
|
|
7743c3178e | ||
|
|
ab3d8e6e9a | ||
|
|
db4c348288 | ||
|
|
6683234642 | ||
|
|
6ddbb9479a | ||
|
|
6c7349a0f8 | ||
|
|
e97fd6cd3f | ||
|
|
184c7c252a | ||
|
|
bfe8c458d6 | ||
|
|
e6e0475db1 | ||
|
|
5090c60797 | ||
|
|
9432252e15 | ||
|
|
47111a1a3a | ||
|
|
6eb3d50375 | ||
|
|
4deb732804 | ||
|
|
20d9387d0b | ||
|
|
9f6d0ccaa0 | ||
|
|
1acbc4d213 | ||
|
|
8c99668fac | ||
|
|
ad44f46204 | ||
|
|
49d999c1e3 | ||
|
|
9ed4777a18 | ||
|
|
0cf44f68cc | ||
|
|
41ed5a4288 | ||
|
|
bbcba5e06c | ||
|
|
502cc326dd | ||
|
|
4675d4c5fc | ||
|
|
f80bb2fff9 | ||
|
|
6767906b0d | ||
|
|
a344af4498 |
@@ -3,7 +3,7 @@ using LiteCharms.Features.MidrandBooks.Products;
|
|||||||
|
|
||||||
namespace LiteCharms.Features.MidrandBooks.Seed;
|
namespace LiteCharms.Features.MidrandBooks.Seed;
|
||||||
|
|
||||||
public class CategorySeederService(CategoryService categoryService, ProductService productService, IFeatureManager features,
|
public sealed class CategorySeederService(CategoryService categoryService, ProductService productService, IFeatureManager features,
|
||||||
ILogger<CategorySeederService> logger) : BackgroundService
|
ILogger<CategorySeederService> logger) : BackgroundService
|
||||||
{
|
{
|
||||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
namespace LiteCharms.Features.MidrandBooks.Seed.Configuration;
|
namespace LiteCharms.Features.MidrandBooks.Seed.Configuration;
|
||||||
|
|
||||||
public class CdnSettings
|
public sealed class CdnSettings
|
||||||
{
|
{
|
||||||
public string? BaseCdn { get; set; }
|
public string? BaseCdn { get; set; }
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ using LiteCharms.Features.MidrandBooks.Orders.Models;
|
|||||||
|
|
||||||
namespace LiteCharms.Features.MidrandBooks.Seed;
|
namespace LiteCharms.Features.MidrandBooks.Seed;
|
||||||
|
|
||||||
public class CustomerSeederService(CustomerService customerService, OrderService orderService, IFeatureManager features,
|
public sealed class CustomerSeederService(CustomerService customerService, OrderService orderService, IFeatureManager features,
|
||||||
ILogger<CustomerSeederService> logger) : BackgroundService
|
ILogger<CustomerSeederService> logger) : BackgroundService
|
||||||
{
|
{
|
||||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ using LiteCharms.Features.MidrandBooks.Seed.Configuration;
|
|||||||
|
|
||||||
namespace LiteCharms.Features.MidrandBooks.Seed;
|
namespace LiteCharms.Features.MidrandBooks.Seed;
|
||||||
|
|
||||||
public class ProductsSeederService(ProductService productService, AuthorService authorService, BooksService booksService,
|
public sealed class ProductsSeederService(ProductService productService, AuthorService authorService, BooksService booksService,
|
||||||
IFeatureManager features, IOptions<CdnSettings> options, ILogger<ProductsSeederService> logger) : BackgroundService
|
IFeatureManager features, IOptions<CdnSettings> options, ILogger<ProductsSeederService> logger) : BackgroundService
|
||||||
{
|
{
|
||||||
private readonly CdnSettings cdnSettings = options.Value;
|
private readonly CdnSettings cdnSettings = options.Value;
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ using LiteCharms.Features.Models;
|
|||||||
|
|
||||||
namespace LiteCharms.Features.MidrandBooks.Tests;
|
namespace LiteCharms.Features.MidrandBooks.Tests;
|
||||||
|
|
||||||
public class AuthorServiceFeatureTests(Fixture fixture, ITestOutputHelper output) : IClassFixture<Fixture>
|
public class AuthorServiceFeatureTests(Fixture fixture) : IClassFixture<Fixture>
|
||||||
{
|
{
|
||||||
private readonly AuthorService authorService = fixture.Services.GetRequiredService<AuthorService>();
|
private readonly AuthorService authorService = fixture.Services.GetRequiredService<AuthorService>();
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
using LiteCharms.Features.Extensions;
|
using LiteCharms.Features.Extensions;
|
||||||
using LiteCharms.Features.MidrandBooks.Abstractions;
|
|
||||||
using LiteCharms.Features.MidrandBooks.Extensions;
|
using LiteCharms.Features.MidrandBooks.Extensions;
|
||||||
|
|
||||||
namespace LiteCharms.Features.MidrandBooks.Tests.Common;
|
namespace LiteCharms.Features.MidrandBooks.Tests.Common;
|
||||||
|
|||||||
@@ -46,7 +46,6 @@ public static class Mappers
|
|||||||
MerchantId = entity.MerchantId,
|
MerchantId = entity.MerchantId,
|
||||||
MerchantKey = entity.MerchantKey,
|
MerchantKey = entity.MerchantKey,
|
||||||
Name = entity.Name,
|
Name = entity.Name,
|
||||||
Passphrase = entity.Passphrase,
|
|
||||||
Website = entity.Website,
|
Website = entity.Website,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,5 @@ public sealed class PaymentGatewayConfiguration : IEntityTypeConfiguration<Payme
|
|||||||
builder.Property(f => f.MerchantId).IsRequired();
|
builder.Property(f => f.MerchantId).IsRequired();
|
||||||
builder.Property(f => f.Enabled);
|
builder.Property(f => f.Enabled);
|
||||||
builder.Property(f => f.Name).IsRequired();
|
builder.Property(f => f.Name).IsRequired();
|
||||||
builder.Property(f => f.Passphrase).IsRequired();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,8 +16,6 @@ public class PaymentGateway
|
|||||||
|
|
||||||
public string? MerchantKey { get; set; }
|
public string? MerchantKey { get; set; }
|
||||||
|
|
||||||
public string? Passphrase { get; set; }
|
|
||||||
|
|
||||||
public bool IsSandbox { get; set; }
|
public bool IsSandbox { get; set; }
|
||||||
|
|
||||||
public bool Enabled { get; set; }
|
public bool Enabled { get; set; }
|
||||||
|
|||||||
@@ -174,7 +174,6 @@ public sealed class PaymentService(IDbContextFactory<MidrandBooksDbContext> cont
|
|||||||
MerchantKey = request.MerchantKey,
|
MerchantKey = request.MerchantKey,
|
||||||
Name = request.Name,
|
Name = request.Name,
|
||||||
Website = request.Website,
|
Website = request.Website,
|
||||||
Passphrase = "N/A",
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return await context.SaveChangesAsync(cancellationToken) > 0
|
return await context.SaveChangesAsync(cancellationToken) > 0
|
||||||
|
|||||||
+1231
File diff suppressed because it is too large
Load Diff
+29
@@ -0,0 +1,29 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class RemovedPassphraseFromPaymentGateway : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "Passphrase",
|
||||||
|
table: "Gateways");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "Passphrase",
|
||||||
|
table: "Gateways",
|
||||||
|
type: "text",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
-4
@@ -604,10 +604,6 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
|
|||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("text");
|
.HasColumnType("text");
|
||||||
|
|
||||||
b.Property<string>("Passphrase")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTime?>("UpdatedAt")
|
b.Property<DateTime?>("UpdatedAt")
|
||||||
.HasColumnType("timestamp with time zone");
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
using LiteCharms.Features.Extensions;
|
using LiteCharms.Features.Hasher;
|
||||||
using LiteCharms.Features.Models;
|
using LiteCharms.Features.Models;
|
||||||
using LiteCharms.Features.TechShop.Extensions;
|
using LiteCharms.Features.TechShop.Extensions;
|
||||||
using LiteCharms.Features.TechShop.Leads.Models;
|
using LiteCharms.Features.TechShop.Leads.Models;
|
||||||
using LiteCharms.Features.TechShop.Postgres;
|
using LiteCharms.Features.TechShop.Postgres;
|
||||||
using static LiteCharms.Features.Extensions.Hash;
|
|
||||||
|
|
||||||
namespace LiteCharms.Features.TechShop.Leads;
|
namespace LiteCharms.Features.TechShop.Leads;
|
||||||
|
|
||||||
@@ -29,7 +28,7 @@ public class LeadService(IDbContextFactory<ShopDbContext> contextFactory)
|
|||||||
FeedItemId = request.FeedItemId,
|
FeedItemId = request.FeedItemId,
|
||||||
Status = LeadStatus.New,
|
Status = LeadStatus.New,
|
||||||
TargetId = request.TargetId,
|
TargetId = request.TargetId,
|
||||||
AttributionHash = StringToSha256Hash.Invoke($"{request.ClickId}{request.AppClickId}{request.WebClickId}")
|
AttributionHash = HashService.StringToSha256Hash($"{request.ClickId}{request.AppClickId}{request.WebClickId}")
|
||||||
});
|
});
|
||||||
|
|
||||||
return await context.SaveChangesAsync(cancellationToken) > 0
|
return await context.SaveChangesAsync(cancellationToken) > 0
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ public class Fixture : IDisposable
|
|||||||
.AddGarageS3(Configuration)
|
.AddGarageS3(Configuration)
|
||||||
.AddEmailServices(Configuration)
|
.AddEmailServices(Configuration)
|
||||||
.AddSingleton(Configuration)
|
.AddSingleton(Configuration)
|
||||||
|
.AddHashServices(Configuration)
|
||||||
.BuildServiceProvider();
|
.BuildServiceProvider();
|
||||||
|
|
||||||
Mediator = Services.GetRequiredService<IMediator>();
|
Mediator = Services.GetRequiredService<IMediator>();
|
||||||
|
|||||||
@@ -0,0 +1,155 @@
|
|||||||
|
using LiteCharms.Features.Hasher;
|
||||||
|
using LiteCharms.Features.Models;
|
||||||
|
using static LiteCharms.Features.Extensions.Hash;
|
||||||
|
|
||||||
|
namespace LiteCharms.Features.Tests;
|
||||||
|
|
||||||
|
public class HashServiceFeatureTests(Fixture fixture) : IClassFixture<Fixture>
|
||||||
|
{
|
||||||
|
private readonly HashService hashService = fixture.Services.GetRequiredService<HashService>();
|
||||||
|
private readonly string payfastPassphrase = fixture.Configuration.GetSection("HasherSettings:PayfastPassphrase").Value!;
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void StringToSha256Hash_Should_GenerateHash()
|
||||||
|
{
|
||||||
|
var input = "We are the best";
|
||||||
|
var expectedHash = "96E17275B53F6BEB7A0D1C4F789F226D3C71CBE398585F25B3028F2B432E78AB";
|
||||||
|
|
||||||
|
var result = HashService.StringToSha256Hash(input);
|
||||||
|
|
||||||
|
Assert.NotNull(result);
|
||||||
|
Assert.True(HashService.IsSha256Hash(result));
|
||||||
|
Assert.Equal(expectedHash, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void StreamToSha256Hash_Should_GenerateHash()
|
||||||
|
{
|
||||||
|
var input = "We are successful";
|
||||||
|
using var stream = new MemoryStream(Encoding.UTF8.GetBytes(input));
|
||||||
|
var expectedHash = "C27872EE494B09D72203C98FC858268F3CD3492D62AA7B766A873520C2C73AFB";
|
||||||
|
|
||||||
|
var result = HashService.StreamToSha256Hash(stream);
|
||||||
|
|
||||||
|
Assert.NotNull(result);
|
||||||
|
Assert.True(HashService.IsSha256Hash(result));
|
||||||
|
Assert.Equal(expectedHash, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void BytesToSha256Hash_Should_GenerateHash()
|
||||||
|
{
|
||||||
|
var inputBytes = Encoding.UTF8.GetBytes("We are wealthy");
|
||||||
|
var expectedHash = "3876BF98F6E4A8E42B22C40415687D6FF13F0E887F3F508B71852298FC665737";
|
||||||
|
|
||||||
|
var result = HashService.BytesToSha256Hash(inputBytes);
|
||||||
|
|
||||||
|
Assert.NotNull(result);
|
||||||
|
Assert.True(HashService.IsSha256Hash(result));
|
||||||
|
Assert.Equal(expectedHash, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ToMd5Hash_Should_GenerateHash()
|
||||||
|
{
|
||||||
|
var input = "We manifest our desired destiny";
|
||||||
|
var expectedMd5Lowercase = "6c7816869bcebe4634f7afe9c66dfa08";
|
||||||
|
|
||||||
|
var result = HashService.ToMd5Hash(input);
|
||||||
|
|
||||||
|
Assert.True(result.IsSuccess);
|
||||||
|
Assert.True(HashService.IsMd5Hash(result.Value));
|
||||||
|
Assert.Equal(expectedMd5Lowercase, result.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void VerifyPayfastWebhookSignature_Should_GenerateHash()
|
||||||
|
{
|
||||||
|
var paymentId = hashService.HashEncodeLongId(1001).Value;
|
||||||
|
|
||||||
|
var payload = new PayfastWebhookPayload
|
||||||
|
{
|
||||||
|
Amount = "350.00",
|
||||||
|
ItemName = "System Architecture Book",
|
||||||
|
MPaymentId = paymentId,
|
||||||
|
};
|
||||||
|
|
||||||
|
var rawPayload = payload.ToRawPayfastPayload(payfastPassphrase);
|
||||||
|
|
||||||
|
var generatedSignature = HashService.ToMd5Hash(rawPayload).Value;
|
||||||
|
|
||||||
|
var result = hashService.VerifyPayfastWebhookSignature(payload, generatedSignature);
|
||||||
|
|
||||||
|
Assert.True(result.IsSuccess);
|
||||||
|
Assert.True(result.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void HashEncodeHex_Should_GenerateHash()
|
||||||
|
{
|
||||||
|
var validHexInput = "DEADBEEF42";
|
||||||
|
|
||||||
|
var result = hashService.HashEncodeHex(validHexInput);
|
||||||
|
|
||||||
|
Assert.True(result.IsSuccess);
|
||||||
|
Assert.False(string.IsNullOrWhiteSpace(result.Value));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void HashEncodeIntId_Should_GenerateHash()
|
||||||
|
{
|
||||||
|
int targetId = 42;
|
||||||
|
|
||||||
|
var result = hashService.HashEncodeIntId(targetId);
|
||||||
|
|
||||||
|
Assert.True(result.IsSuccess);
|
||||||
|
Assert.True(result.Value.Length >= 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void HashEncodeLongId_Should_GenerateHash()
|
||||||
|
{
|
||||||
|
long targetId = 9904185012L;
|
||||||
|
|
||||||
|
var result = hashService.HashEncodeLongId(targetId);
|
||||||
|
|
||||||
|
Assert.True(result.IsSuccess);
|
||||||
|
Assert.True(result.Value.Length >= 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DecodeIntIdHash_Should_GenerateHash()
|
||||||
|
{
|
||||||
|
int originalId = 88041;
|
||||||
|
var hashedString = hashService.HashEncodeIntId(originalId).Value;
|
||||||
|
|
||||||
|
var result = hashService.DecodeIntIdHash(hashedString);
|
||||||
|
|
||||||
|
Assert.True(result.IsSuccess);
|
||||||
|
Assert.Equal(originalId, result.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DecodeLongIdHash_Should_GenerateHash()
|
||||||
|
{
|
||||||
|
long originalId = 9081230491823L;
|
||||||
|
var hashedString = hashService.HashEncodeLongId(originalId).Value;
|
||||||
|
|
||||||
|
var result = hashService.DecodeLongIdHash(hashedString);
|
||||||
|
|
||||||
|
Assert.True(result.IsSuccess);
|
||||||
|
Assert.Equal(originalId, result.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DecodeHexHash_Should_GenerateHash()
|
||||||
|
{
|
||||||
|
var originalHex = "ABCDEF12345";
|
||||||
|
var hashedString = hashService.HashEncodeHex(originalHex).Value;
|
||||||
|
|
||||||
|
var result = hashService.DecodeHexHash(hashedString);
|
||||||
|
|
||||||
|
Assert.True(result.IsSuccess);
|
||||||
|
Assert.Equal(originalHex, result.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -27,6 +27,8 @@
|
|||||||
|
|
||||||
<!-- Global Usings -->
|
<!-- Global Usings -->
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<Using Include="System.Net" />
|
||||||
|
<Using Include="System.Text" />
|
||||||
<Using Include="Mediator" />
|
<Using Include="Mediator" />
|
||||||
<Using Include="Xunit.Abstractions" />
|
<Using Include="Xunit.Abstractions" />
|
||||||
<Using Include="Microsoft.Extensions.DependencyInjection" />
|
<Using Include="Microsoft.Extensions.DependencyInjection" />
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using LiteCharms.Features.Hasher;
|
using LiteCharms.Features.Hasher;
|
||||||
using LiteCharms.Features.Hasher.Configuration;
|
using LiteCharms.Features.Hasher.Configuration;
|
||||||
|
using LiteCharms.Features.Models;
|
||||||
|
|
||||||
namespace LiteCharms.Features.Extensions;
|
namespace LiteCharms.Features.Extensions;
|
||||||
|
|
||||||
@@ -20,4 +21,82 @@ public static class Hash
|
|||||||
|
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static string ToRawPayfastPayload(this PayfastWebhookPayload input, string passphrase)
|
||||||
|
{
|
||||||
|
var parameters = new List<string>();
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(input.Amount))
|
||||||
|
parameters.Add($"amount={WebUtility.UrlEncode(input.Amount)}");
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(input.ItemName))
|
||||||
|
parameters.Add($"item_name={WebUtility.UrlEncode(input.ItemName)}");
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(input.MPaymentId))
|
||||||
|
parameters.Add($"m_payment_id={WebUtility.UrlEncode(input.MPaymentId)}");
|
||||||
|
|
||||||
|
string payload = string.Join("&", parameters);
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(passphrase))
|
||||||
|
payload += $"&passphrase={WebUtility.UrlEncode(passphrase)}";
|
||||||
|
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static (PayfastWebhookPayload Payload, string Passphrase) FromRawPayfastPayload(this string rawPayload)
|
||||||
|
{
|
||||||
|
string passphrase = string.Empty;
|
||||||
|
var payload = new PayfastWebhookPayload();
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(rawPayload)) return (payload, passphrase);
|
||||||
|
|
||||||
|
var segments = rawPayload.Split('&', StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
|
||||||
|
foreach (var segment in segments)
|
||||||
|
{
|
||||||
|
int delimiterIndex = segment.IndexOf('=');
|
||||||
|
if (delimiterIndex == -1)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
string key = segment[..delimiterIndex].Trim();
|
||||||
|
string rawValue = segment[(delimiterIndex + 1)..];
|
||||||
|
|
||||||
|
string decodedValue = WebUtility.UrlDecode(rawValue);
|
||||||
|
|
||||||
|
switch (key.ToLowerInvariant())
|
||||||
|
{
|
||||||
|
case "amount":
|
||||||
|
payload.Amount = decodedValue;
|
||||||
|
break;
|
||||||
|
case "item_name":
|
||||||
|
payload.ItemName = decodedValue;
|
||||||
|
break;
|
||||||
|
case "m_payment_id":
|
||||||
|
payload.MPaymentId = decodedValue;
|
||||||
|
break;
|
||||||
|
case "passphrase":
|
||||||
|
passphrase = decodedValue;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (payload, passphrase);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static (PayfastWebhookPayload Payload, string Passphrase) FromRawPayfastPayload(this IFormCollection form)
|
||||||
|
{
|
||||||
|
string passphrase = string.Empty;
|
||||||
|
var payload = new PayfastWebhookPayload();
|
||||||
|
|
||||||
|
if (form.IsNullOrEmpty()) return (payload, passphrase);
|
||||||
|
|
||||||
|
payload = new PayfastWebhookPayload
|
||||||
|
{
|
||||||
|
Amount = form.TryGetValue("amount", out var amountValues) ? amountValues.ToString() : null,
|
||||||
|
ItemName = form.TryGetValue("item_name", out var itemValues) ? itemValues.ToString() : null,
|
||||||
|
MPaymentId = form.TryGetValue("m_payment_id", out var paymentIdValues) ? paymentIdValues.ToString() : null,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (payload, passphrase);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
using LiteCharms.Features.Abstractions;
|
using LiteCharms.Features.Abstractions;
|
||||||
using LiteCharms.Features.Hasher.Configuration;
|
using LiteCharms.Features.Hasher.Configuration;
|
||||||
|
using LiteCharms.Features.Models;
|
||||||
|
|
||||||
namespace LiteCharms.Features.Hasher;
|
namespace LiteCharms.Features.Hasher;
|
||||||
|
|
||||||
@@ -7,45 +8,63 @@ public sealed partial class HashService(IHashids hasher, IOptions<HasherSettings
|
|||||||
{
|
{
|
||||||
private readonly HasherSettings settings = options.Value;
|
private readonly HasherSettings settings = options.Value;
|
||||||
|
|
||||||
[System.Text.RegularExpressions.GeneratedRegex(@"\A\b[0-9a-fA-F]+\b\Z")]
|
[GeneratedRegex(@"\A\b[0-9a-fA-F]+\b\Z")]
|
||||||
private static partial System.Text.RegularExpressions.Regex HexHashRegex();
|
private static partial Regex HexHashRegex { get; }
|
||||||
|
|
||||||
public static readonly Func<string?, string?> StringToSha256Hash = (input) =>
|
[GeneratedRegex(@"\A[0-9a-fA-F]{32}\Z", RegexOptions.None, matchTimeoutMilliseconds: 100)]
|
||||||
string.IsNullOrEmpty(input) ? null : Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(input)));
|
private static partial Regex Md5Regex { get; }
|
||||||
|
|
||||||
public static readonly Func<Stream, string?> StreamToSha256Hash = (stream) =>
|
[GeneratedRegex(@"\A[0-9a-fA-F]{64}\Z", RegexOptions.None, matchTimeoutMilliseconds: 100)]
|
||||||
|
private static partial Regex Sha256Regex { get; }
|
||||||
|
|
||||||
|
public static bool IsMd5Hash(string? value) =>
|
||||||
|
!string.IsNullOrWhiteSpace(value) && Md5Regex.IsMatch(value);
|
||||||
|
|
||||||
|
public static bool IsSha256Hash(string? value) =>
|
||||||
|
!string.IsNullOrWhiteSpace(value) && Sha256Regex.IsMatch(value);
|
||||||
|
|
||||||
|
public static string? StringToSha256Hash(string? input) =>
|
||||||
|
string.IsNullOrEmpty(input) ? null : Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(input)));
|
||||||
|
|
||||||
|
public static string? StreamToSha256Hash(Stream stream) =>
|
||||||
stream is null ? null : Convert.ToHexString(SHA256.HashData(stream));
|
stream is null ? null : Convert.ToHexString(SHA256.HashData(stream));
|
||||||
|
|
||||||
public static readonly Func<byte[], string?> BytesToSha256Hash = (bytes) =>
|
public static string? BytesToSha256Hash(byte[] bytes) =>
|
||||||
bytes is null ? null : Convert.ToHexString(SHA256.HashData(bytes));
|
bytes is null ? null : Convert.ToHexString(SHA256.HashData(bytes));
|
||||||
|
|
||||||
public static Result<string> ComputeMd5Hash(string input)
|
public static Result<string> ToMd5Hash(string input)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(input))
|
if (string.IsNullOrEmpty(input))
|
||||||
return Result.Fail<string>("Input content cannot be null or empty for MD5 processing.");
|
return Result.Fail<string>("Input content cannot be null or empty for MD5 processing.");
|
||||||
|
|
||||||
byte[] bytes = MD5.HashData(Encoding.UTF8.GetBytes(input));
|
byte[] bytes = MD5.HashData(Encoding.UTF8.GetBytes(input));
|
||||||
return Result.Ok(Convert.ToHexString(bytes).ToLowerInvariant());
|
return Result.Ok(Convert.ToHexString(bytes).ToLowerInvariant());
|
||||||
}
|
}
|
||||||
|
|
||||||
public Result<bool> VerifyPayfastWebhookSignature(IDictionary<string, string> incomingFormData, string incomingSignature)
|
public Result<bool> VerifyPayfastWebhookSignature(PayfastWebhookPayload payload, string incomingSignature)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(incomingSignature))
|
if (string.IsNullOrWhiteSpace(incomingSignature))
|
||||||
return Result.Fail<bool>("Validation failed: Missing signature string parameter.");
|
return Result.Fail<bool>("Validation failed: Missing signature string parameter.");
|
||||||
|
|
||||||
var sortedFields = incomingFormData
|
var parameters = new List<string>();
|
||||||
.Where(field => field.Key != "signature")
|
|
||||||
.OrderBy(field => field.Key)
|
|
||||||
.Select(field => $"{field.Key}={Uri.EscapeDataString(field.Value).Replace("%20", "+")}");
|
|
||||||
|
|
||||||
string payload = string.Join("&", sortedFields);
|
if (!string.IsNullOrWhiteSpace(payload.Amount))
|
||||||
|
parameters.Add($"amount={WebUtility.UrlEncode(payload.Amount)}");
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(payload.ItemName))
|
||||||
|
parameters.Add($"item_name={WebUtility.UrlEncode(payload.ItemName)}");
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(payload.MPaymentId))
|
||||||
|
parameters.Add($"m_payment_id={WebUtility.UrlEncode(payload.MPaymentId)}");
|
||||||
|
|
||||||
|
string signatureString = string.Join("&", parameters);
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(settings.PayfastPassphrase))
|
if (!string.IsNullOrWhiteSpace(settings.PayfastPassphrase))
|
||||||
payload += $"&passphrase={Uri.EscapeDataString(settings.PayfastPassphrase).Replace("%20", "+")}";
|
signatureString += $"&passphrase={WebUtility.UrlEncode(settings.PayfastPassphrase)}";
|
||||||
|
|
||||||
var localHashResult = ComputeMd5Hash(payload);
|
var localHashResult = ToMd5Hash(signatureString);
|
||||||
|
|
||||||
if (!localHashResult.IsSuccess)
|
if (!localHashResult.IsSuccess)
|
||||||
return Result.Fail<bool>(localHashResult.Errors);
|
return Result.Fail<bool>(localHashResult.Errors);
|
||||||
@@ -56,13 +75,13 @@ public sealed partial class HashService(IHashids hasher, IOptions<HasherSettings
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
return Result.Fail<bool>(new Error("An error occurred during MD5 verification loop.").CausedBy(ex));
|
return Result.Fail<bool>(new Error("An error occurred during Payfast MD5 verification.").CausedBy(ex));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Result<string> HashEncodeHex(string input) => string.IsNullOrWhiteSpace(input) || !HexHashRegex().IsMatch(input)
|
public Result<string> HashEncodeHex(string input) => string.IsNullOrWhiteSpace(input) || !HexHashRegex.IsMatch(input)
|
||||||
? Result.Fail<string>("Input must be a valid hexadecimal string.")
|
? Result.Fail<string>("Input must be a valid hexadecimal string.")
|
||||||
: Result.Ok(hasher.EncodeHex(input));
|
: Result.Ok(hasher.EncodeHex(input));
|
||||||
|
|
||||||
public Result<string> HashEncodeIntId(int id) => id < 0
|
public Result<string> HashEncodeIntId(int id) => id < 0
|
||||||
? Result.Fail<string>("Id cannot be negative.")
|
? Result.Fail<string>("Id cannot be negative.")
|
||||||
|
|||||||
@@ -147,7 +147,11 @@
|
|||||||
|
|
||||||
<!-- Shared Usings -->
|
<!-- Shared Usings -->
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<Using Include="Microsoft.IdentityModel.Tokens" />
|
||||||
|
<Using Include="Microsoft.AspNetCore.Http" />
|
||||||
<Using Include="HashidsNet" />
|
<Using Include="HashidsNet" />
|
||||||
|
<Using Include="System.Net" />
|
||||||
|
<Using Include="System.Text.RegularExpressions" />
|
||||||
<Using Include="System.Globalization" />
|
<Using Include="System.Globalization" />
|
||||||
<Using Include="Microsoft.AspNetCore.Builder" />
|
<Using Include="Microsoft.AspNetCore.Builder" />
|
||||||
<Using Include="Microsoft.Extensions.Hosting" />
|
<Using Include="Microsoft.Extensions.Hosting" />
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace LiteCharms.Features.Models;
|
||||||
|
|
||||||
|
public sealed class PayfastWebhookPayload
|
||||||
|
{
|
||||||
|
public string? Amount { get; set; }
|
||||||
|
public string? ItemName { get; set; }
|
||||||
|
public string? MPaymentId { get; set; }
|
||||||
|
}
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
namespace LiteCharms.Features.Models;
|
|
||||||
|
|
||||||
public class SearchState
|
|
||||||
{
|
|
||||||
public string Query { get; private set; } = string.Empty;
|
|
||||||
|
|
||||||
public event Action? OnSearchSubmitted;
|
|
||||||
|
|
||||||
public void UpdateQuery(string newQuery) => Query = newQuery;
|
|
||||||
|
|
||||||
public void SubmitSearch() => OnSearchSubmitted?.Invoke();
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user