littleshop/LittleShop/Controllers/BTCPayWebhookController.cs
2025-09-04 21:28:47 +01:00

180 lines
6.3 KiB
C#

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
};
}
}