Compare commits
57
Commits
c4f73fd999
...
1.59.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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;
|
||||
|
||||
public class CategorySeederService(CategoryService categoryService, ProductService productService, IFeatureManager features,
|
||||
public sealed class CategorySeederService(CategoryService categoryService, ProductService productService, IFeatureManager features,
|
||||
ILogger<CategorySeederService> logger) : BackgroundService
|
||||
{
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
namespace LiteCharms.Features.MidrandBooks.Seed.Configuration;
|
||||
|
||||
public class CdnSettings
|
||||
public sealed class CdnSettings
|
||||
{
|
||||
public string? BaseCdn { get; set; }
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ using LiteCharms.Features.MidrandBooks.Orders.Models;
|
||||
|
||||
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
|
||||
{
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
|
||||
@@ -5,7 +5,7 @@ using LiteCharms.Features.MidrandBooks.Seed.Configuration;
|
||||
|
||||
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
|
||||
{
|
||||
private readonly CdnSettings cdnSettings = options.Value;
|
||||
|
||||
@@ -5,7 +5,7 @@ using LiteCharms.Features.Models;
|
||||
|
||||
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>();
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using LiteCharms.Features.Extensions;
|
||||
using LiteCharms.Features.MidrandBooks.Abstractions;
|
||||
using LiteCharms.Features.MidrandBooks.Extensions;
|
||||
|
||||
namespace LiteCharms.Features.MidrandBooks.Tests.Common;
|
||||
|
||||
@@ -46,7 +46,6 @@ public static class Mappers
|
||||
MerchantId = entity.MerchantId,
|
||||
MerchantKey = entity.MerchantKey,
|
||||
Name = entity.Name,
|
||||
Passphrase = entity.Passphrase,
|
||||
Website = entity.Website,
|
||||
};
|
||||
|
||||
|
||||
@@ -15,6 +15,5 @@ public sealed class PaymentGatewayConfiguration : IEntityTypeConfiguration<Payme
|
||||
builder.Property(f => f.MerchantId).IsRequired();
|
||||
builder.Property(f => f.Enabled);
|
||||
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? Passphrase { get; set; }
|
||||
|
||||
public bool IsSandbox { get; set; }
|
||||
|
||||
public bool Enabled { get; set; }
|
||||
|
||||
@@ -174,7 +174,6 @@ public sealed class PaymentService(IDbContextFactory<MidrandBooksDbContext> cont
|
||||
MerchantKey = request.MerchantKey,
|
||||
Name = request.Name,
|
||||
Website = request.Website,
|
||||
Passphrase = "N/A",
|
||||
});
|
||||
|
||||
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()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Passphrase")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
using LiteCharms.Features.Extensions;
|
||||
using LiteCharms.Features.Hasher;
|
||||
using LiteCharms.Features.Models;
|
||||
using LiteCharms.Features.TechShop.Extensions;
|
||||
using LiteCharms.Features.TechShop.Leads.Models;
|
||||
using LiteCharms.Features.TechShop.Postgres;
|
||||
using static LiteCharms.Features.Extensions.Hash;
|
||||
|
||||
namespace LiteCharms.Features.TechShop.Leads;
|
||||
|
||||
@@ -29,7 +28,7 @@ public class LeadService(IDbContextFactory<ShopDbContext> contextFactory)
|
||||
FeedItemId = request.FeedItemId,
|
||||
Status = LeadStatus.New,
|
||||
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
|
||||
|
||||
@@ -26,6 +26,7 @@ public class Fixture : IDisposable
|
||||
.AddGarageS3(Configuration)
|
||||
.AddEmailServices(Configuration)
|
||||
.AddSingleton(Configuration)
|
||||
.AddHashServices(Configuration)
|
||||
.BuildServiceProvider();
|
||||
|
||||
Mediator = Services.GetRequiredService<IMediator>();
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
using LiteCharms.Features.Hasher;
|
||||
|
||||
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 incomingForm = new Dictionary<string, string>
|
||||
{
|
||||
{ "m_payment_id", paymentId },
|
||||
{ "amount", "350.00" },
|
||||
{ "item_name", "System Architecture Book" }
|
||||
};
|
||||
|
||||
var rawPayload = $"amount=350.00&item_name=System+Architecture+Book&m_payment_id={paymentId}&passphrase={payfastPassphrase}";
|
||||
var generatedSignature = HashService.ToMd5Hash(rawPayload).Value;
|
||||
|
||||
var result = hashService.VerifyPayfastWebhookSignature(incomingForm, 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 -->
|
||||
<ItemGroup>
|
||||
<Using Include="System.Net" />
|
||||
<Using Include="System.Text" />
|
||||
<Using Include="Mediator" />
|
||||
<Using Include="Xunit.Abstractions" />
|
||||
<Using Include="Microsoft.Extensions.DependencyInjection" />
|
||||
|
||||
@@ -7,25 +7,37 @@ public sealed partial class HashService(IHashids hasher, IOptions<HasherSettings
|
||||
{
|
||||
private readonly HasherSettings settings = options.Value;
|
||||
|
||||
[System.Text.RegularExpressions.GeneratedRegex(@"\A\b[0-9a-fA-F]+\b\Z")]
|
||||
private static partial System.Text.RegularExpressions.Regex HexHashRegex();
|
||||
[GeneratedRegex(@"\A\b[0-9a-fA-F]+\b\Z")]
|
||||
private static partial Regex HexHashRegex { get; }
|
||||
|
||||
public static readonly Func<string?, string?> StringToSha256Hash = (input) =>
|
||||
string.IsNullOrEmpty(input) ? null : Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(input)));
|
||||
[GeneratedRegex(@"\A[0-9a-fA-F]{32}\Z", RegexOptions.None, matchTimeoutMilliseconds: 100)]
|
||||
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));
|
||||
|
||||
public static readonly Func<byte[], string?> BytesToSha256Hash = (bytes) =>
|
||||
public static string? BytesToSha256Hash(byte[] 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))
|
||||
return Result.Fail<string>("Input content cannot be null or empty for MD5 processing.");
|
||||
|
||||
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)
|
||||
@@ -36,16 +48,16 @@ public sealed partial class HashService(IHashids hasher, IOptions<HasherSettings
|
||||
return Result.Fail<bool>("Validation failed: Missing signature string parameter.");
|
||||
|
||||
var sortedFields = incomingFormData
|
||||
.Where(field => field.Key != "signature")
|
||||
.OrderBy(field => field.Key)
|
||||
.Select(field => $"{field.Key}={Uri.EscapeDataString(field.Value).Replace("%20", "+")}");
|
||||
.Where(field => !string.Equals(field.Key, "signature", StringComparison.OrdinalIgnoreCase))
|
||||
.OrderBy(field => field.Key, StringComparer.Ordinal)
|
||||
.Select(field => $"{field.Key}={WebUtility.UrlEncode(field.Value)}");
|
||||
|
||||
string payload = string.Join("&", sortedFields);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(settings.PayfastPassphrase))
|
||||
payload += $"&passphrase={Uri.EscapeDataString(settings.PayfastPassphrase).Replace("%20", "+")}";
|
||||
payload += $"&passphrase={WebUtility.UrlEncode(settings.PayfastPassphrase)}";
|
||||
|
||||
var localHashResult = ComputeMd5Hash(payload);
|
||||
var localHashResult = ToMd5Hash(payload);
|
||||
|
||||
if (!localHashResult.IsSuccess)
|
||||
return Result.Fail<bool>(localHashResult.Errors);
|
||||
@@ -60,9 +72,9 @@ public sealed partial class HashService(IHashids hasher, IOptions<HasherSettings
|
||||
}
|
||||
}
|
||||
|
||||
public Result<string> HashEncodeHex(string input) => string.IsNullOrWhiteSpace(input) || !HexHashRegex().IsMatch(input)
|
||||
? Result.Fail<string>("Input must be a valid hexadecimal string.")
|
||||
: Result.Ok(hasher.EncodeHex(input));
|
||||
public Result<string> HashEncodeHex(string input) => string.IsNullOrWhiteSpace(input) || !HexHashRegex.IsMatch(input)
|
||||
? Result.Fail<string>("Input must be a valid hexadecimal string.")
|
||||
: Result.Ok(hasher.EncodeHex(input));
|
||||
|
||||
public Result<string> HashEncodeIntId(int id) => id < 0
|
||||
? Result.Fail<string>("Id cannot be negative.")
|
||||
|
||||
@@ -148,6 +148,8 @@
|
||||
<!-- Shared Usings -->
|
||||
<ItemGroup>
|
||||
<Using Include="HashidsNet" />
|
||||
<Using Include="System.Net" />
|
||||
<Using Include="System.Text.RegularExpressions" />
|
||||
<Using Include="System.Globalization" />
|
||||
<Using Include="Microsoft.AspNetCore.Builder" />
|
||||
<Using Include="Microsoft.Extensions.Hosting" />
|
||||
|
||||
@@ -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