BTCPay-infrastructure-recovery
This commit is contained in:
180
LittleShop/Controllers/BTCPayWebhookController.cs
Normal file
180
LittleShop/Controllers/BTCPayWebhookController.cs
Normal file
@@ -0,0 +1,180 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using LittleShop.DTOs;
|
||||
using LittleShop.Services;
|
||||
using LittleShop.Enums;
|
||||
|
||||
namespace LittleShop.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/btcpay")]
|
||||
public class BTCPayWebhookController : ControllerBase
|
||||
{
|
||||
private readonly ICryptoPaymentService _cryptoPaymentService;
|
||||
private readonly IBTCPayServerService _btcPayService;
|
||||
private readonly IConfiguration _configuration;
|
||||
private readonly ILogger<BTCPayWebhookController> _logger;
|
||||
|
||||
public BTCPayWebhookController(
|
||||
ICryptoPaymentService cryptoPaymentService,
|
||||
IBTCPayServerService btcPayService,
|
||||
IConfiguration configuration,
|
||||
ILogger<BTCPayWebhookController> logger)
|
||||
{
|
||||
_cryptoPaymentService = cryptoPaymentService;
|
||||
_btcPayService = btcPayService;
|
||||
_configuration = configuration;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
[HttpPost("webhook")]
|
||||
public async Task<IActionResult> ProcessWebhook()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Read the raw request body
|
||||
using var reader = new StreamReader(Request.Body);
|
||||
var requestBody = await reader.ReadToEndAsync();
|
||||
|
||||
// Get webhook signature from headers
|
||||
var signature = Request.Headers["BTCPAY-SIG"].FirstOrDefault();
|
||||
|
||||
if (string.IsNullOrEmpty(signature))
|
||||
{
|
||||
_logger.LogWarning("Webhook received without signature");
|
||||
return BadRequest("Missing webhook signature");
|
||||
}
|
||||
|
||||
// Validate webhook signature
|
||||
var webhookSecret = _configuration["BTCPayServer:WebhookSecret"];
|
||||
if (string.IsNullOrEmpty(webhookSecret))
|
||||
{
|
||||
_logger.LogError("BTCPay webhook secret not configured");
|
||||
return StatusCode(500, "Webhook validation not configured");
|
||||
}
|
||||
|
||||
if (!ValidateWebhookSignature(requestBody, signature, webhookSecret))
|
||||
{
|
||||
_logger.LogWarning("Invalid webhook signature");
|
||||
return BadRequest("Invalid webhook signature");
|
||||
}
|
||||
|
||||
// Parse webhook data
|
||||
var webhookData = JsonSerializer.Deserialize<BTCPayWebhookDto>(requestBody, new JsonSerializerOptions
|
||||
{
|
||||
PropertyNameCaseInsensitive = true
|
||||
});
|
||||
|
||||
if (webhookData == null)
|
||||
{
|
||||
_logger.LogWarning("Unable to parse webhook data");
|
||||
return BadRequest("Invalid webhook data");
|
||||
}
|
||||
|
||||
_logger.LogInformation("Processing BTCPay webhook: Type={Type}, InvoiceId={InvoiceId}, StoreId={StoreId}",
|
||||
webhookData.Type, webhookData.InvoiceId, webhookData.StoreId);
|
||||
|
||||
// Process the webhook based on event type
|
||||
var success = await ProcessWebhookEvent(webhookData);
|
||||
|
||||
if (!success)
|
||||
{
|
||||
return BadRequest("Failed to process webhook");
|
||||
}
|
||||
|
||||
return Ok();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error processing BTCPay webhook");
|
||||
return StatusCode(500, "Internal server error");
|
||||
}
|
||||
}
|
||||
|
||||
private bool ValidateWebhookSignature(string payload, string signature, string secret)
|
||||
{
|
||||
try
|
||||
{
|
||||
// BTCPay Server uses HMAC-SHA256 with format "sha256=<hex>"
|
||||
if (!signature.StartsWith("sha256="))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var expectedHash = signature.Substring(7); // Remove "sha256=" prefix
|
||||
var secretBytes = Encoding.UTF8.GetBytes(secret);
|
||||
var payloadBytes = Encoding.UTF8.GetBytes(payload);
|
||||
|
||||
using var hmac = new HMACSHA256(secretBytes);
|
||||
var computedHash = hmac.ComputeHash(payloadBytes);
|
||||
var computedHashHex = Convert.ToHexString(computedHash).ToLowerInvariant();
|
||||
|
||||
return expectedHash.Equals(computedHashHex, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error validating webhook signature");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<bool> ProcessWebhookEvent(BTCPayWebhookDto webhookData)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Map BTCPay webhook event types to our payment status
|
||||
var paymentStatus = MapWebhookEventToPaymentStatus(webhookData.Type);
|
||||
|
||||
if (!paymentStatus.HasValue)
|
||||
{
|
||||
_logger.LogInformation("Ignoring webhook event type: {Type}", webhookData.Type);
|
||||
return true; // Not an error, just not a status we care about
|
||||
}
|
||||
|
||||
// Extract payment details
|
||||
var amount = webhookData.Payment?.PaymentMethodPaid ?? 0;
|
||||
var transactionHash = webhookData.Payment?.TransactionData?.TransactionHash;
|
||||
|
||||
// Process the payment update
|
||||
var success = await _cryptoPaymentService.ProcessPaymentWebhookAsync(
|
||||
webhookData.InvoiceId,
|
||||
paymentStatus.Value,
|
||||
amount,
|
||||
transactionHash);
|
||||
|
||||
if (success)
|
||||
{
|
||||
_logger.LogInformation("Successfully processed webhook for invoice {InvoiceId} with status {Status}",
|
||||
webhookData.InvoiceId, paymentStatus.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("Failed to process webhook for invoice {InvoiceId}", webhookData.InvoiceId);
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error processing webhook event for invoice {InvoiceId}", webhookData.InvoiceId);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static PaymentStatus? MapWebhookEventToPaymentStatus(string eventType)
|
||||
{
|
||||
return eventType switch
|
||||
{
|
||||
"InvoiceCreated" => PaymentStatus.Pending,
|
||||
"InvoiceReceivedPayment" => PaymentStatus.Processing,
|
||||
"InvoicePaymentSettled" => PaymentStatus.Completed,
|
||||
"InvoiceProcessing" => PaymentStatus.Processing,
|
||||
"InvoiceExpired" => PaymentStatus.Expired,
|
||||
"InvoiceSettled" => PaymentStatus.Completed,
|
||||
"InvoiceInvalid" => PaymentStatus.Failed,
|
||||
_ => null // Unknown event type
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user