mirror of
https://github.com/aljazceru/btcpayserver.git
synced 2025-12-18 14:34:23 +01:00
Refactoring of Webhooks and Email Rules (#6954)
This commit is contained in:
@@ -0,0 +1,201 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Plugins.Webhooks.Controllers
|
||||
{
|
||||
[ApiController]
|
||||
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield,
|
||||
Policy = Policies.CanModifyWebhooks)]
|
||||
[EnableCors(CorsPolicies.All)]
|
||||
public class GreenfieldStoreWebhooksController : ControllerBase
|
||||
{
|
||||
public GreenfieldStoreWebhooksController(StoreRepository storeRepository, WebhookSender webhookSender)
|
||||
{
|
||||
StoreRepository = storeRepository;
|
||||
WebhookSender = webhookSender;
|
||||
}
|
||||
|
||||
public StoreRepository StoreRepository { get; }
|
||||
public WebhookSender WebhookSender { get; }
|
||||
|
||||
[HttpGet("~/api/v1/stores/{storeId}/webhooks/{webhookId?}")]
|
||||
public async Task<IActionResult> ListWebhooks(string storeId, string webhookId)
|
||||
{
|
||||
if (webhookId is null)
|
||||
{
|
||||
return Ok((await StoreRepository.GetWebhooks(CurrentStoreId))
|
||||
.Select(o => FromModel(o, false))
|
||||
.ToArray());
|
||||
}
|
||||
else
|
||||
{
|
||||
var w = await StoreRepository.GetWebhook(CurrentStoreId, webhookId);
|
||||
if (w is null)
|
||||
return WebhookNotFound();
|
||||
return Ok(FromModel(w, false));
|
||||
}
|
||||
}
|
||||
|
||||
string CurrentStoreId
|
||||
{
|
||||
get
|
||||
{
|
||||
return this.HttpContext.GetStoreData()?.Id;
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost("~/api/v1/stores/{storeId}/webhooks")]
|
||||
public async Task<IActionResult> CreateWebhook(string storeId, Client.Models.CreateStoreWebhookRequest create)
|
||||
{
|
||||
ValidateWebhookRequest(create);
|
||||
if (!ModelState.IsValid)
|
||||
return this.CreateValidationError(ModelState);
|
||||
var webhookId = await StoreRepository.CreateWebhook(CurrentStoreId, ToModel(create));
|
||||
var w = await StoreRepository.GetWebhook(CurrentStoreId, webhookId);
|
||||
if (w is null)
|
||||
return WebhookNotFound();
|
||||
return Ok(FromModel(w, true));
|
||||
}
|
||||
|
||||
private void ValidateWebhookRequest(StoreWebhookBaseData create)
|
||||
{
|
||||
if (!Uri.TryCreate(create?.Url, UriKind.Absolute, out _))
|
||||
ModelState.AddModelError(nameof(Url), "Invalid Url");
|
||||
}
|
||||
|
||||
[HttpPut("~/api/v1/stores/{storeId}/webhooks/{webhookId}")]
|
||||
public async Task<IActionResult> UpdateWebhook(string storeId, string webhookId, Client.Models.UpdateStoreWebhookRequest update)
|
||||
{
|
||||
ValidateWebhookRequest(update);
|
||||
if (!ModelState.IsValid)
|
||||
return this.CreateValidationError(ModelState);
|
||||
var w = await StoreRepository.GetWebhook(CurrentStoreId, webhookId);
|
||||
if (w is null)
|
||||
return WebhookNotFound();
|
||||
await StoreRepository.UpdateWebhook(storeId, webhookId, ToModel(update));
|
||||
return await ListWebhooks(storeId, webhookId);
|
||||
}
|
||||
[HttpDelete("~/api/v1/stores/{storeId}/webhooks/{webhookId}")]
|
||||
public async Task<IActionResult> DeleteWebhook(string storeId, string webhookId)
|
||||
{
|
||||
var w = await StoreRepository.GetWebhook(CurrentStoreId, webhookId);
|
||||
if (w is null)
|
||||
return WebhookNotFound();
|
||||
await StoreRepository.DeleteWebhook(CurrentStoreId, webhookId);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
IActionResult WebhookNotFound()
|
||||
{
|
||||
return this.CreateAPIError(404, "webhook-not-found", "The webhook was not found");
|
||||
}
|
||||
IActionResult WebhookDeliveryNotFound()
|
||||
{
|
||||
return this.CreateAPIError(404, "webhookdelivery-not-found", "The webhook delivery was not found");
|
||||
}
|
||||
private WebhookBlob ToModel(StoreWebhookBaseData create)
|
||||
{
|
||||
return new WebhookBlob()
|
||||
{
|
||||
Active = create.Enabled,
|
||||
Url = create.Url,
|
||||
Secret = create.Secret,
|
||||
AuthorizedEvents = create.AuthorizedEvents is Client.Models.StoreWebhookBaseData.AuthorizedEventsData aed ?
|
||||
new AuthorizedWebhookEvents()
|
||||
{
|
||||
Everything = aed.Everything,
|
||||
SpecificEvents = aed.SpecificEvents
|
||||
} :
|
||||
new AuthorizedWebhookEvents() { Everything = true },
|
||||
AutomaticRedelivery = create.AutomaticRedelivery,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
[HttpGet("~/api/v1/stores/{storeId}/webhooks/{webhookId}/deliveries/{deliveryId?}")]
|
||||
public async Task<IActionResult> ListDeliveries(string storeId, string webhookId, string deliveryId, int? count = null)
|
||||
{
|
||||
if (deliveryId is null)
|
||||
{
|
||||
return Ok((await StoreRepository.GetWebhookDeliveries(CurrentStoreId, webhookId, count))
|
||||
.Select(o => FromModel(o))
|
||||
.ToList());
|
||||
}
|
||||
else
|
||||
{
|
||||
var delivery = await StoreRepository.GetWebhookDelivery(CurrentStoreId, webhookId, deliveryId);
|
||||
if (delivery is null)
|
||||
return WebhookDeliveryNotFound();
|
||||
return Ok(FromModel(delivery));
|
||||
}
|
||||
}
|
||||
[HttpPost("~/api/v1/stores/{storeId}/webhooks/{webhookId}/deliveries/{deliveryId}/redeliver")]
|
||||
public async Task<IActionResult> RedeliverWebhook(string storeId, string webhookId, string deliveryId)
|
||||
{
|
||||
var delivery = await StoreRepository.GetWebhookDelivery(CurrentStoreId, webhookId, deliveryId);
|
||||
if (delivery is null)
|
||||
return WebhookDeliveryNotFound();
|
||||
if (delivery.GetBlob().IsPruned())
|
||||
return WebhookDeliveryPruned();
|
||||
return this.Ok(new JValue(await WebhookSender.Redeliver(deliveryId)));
|
||||
}
|
||||
|
||||
private IActionResult WebhookDeliveryPruned()
|
||||
{
|
||||
return this.CreateAPIError(409, "webhookdelivery-pruned", "This webhook delivery has been pruned, so it can't be redelivered");
|
||||
}
|
||||
|
||||
[HttpGet("~/api/v1/stores/{storeId}/webhooks/{webhookId}/deliveries/{deliveryId}/request")]
|
||||
public async Task<IActionResult> GetDeliveryRequest(string storeId, string webhookId, string deliveryId)
|
||||
{
|
||||
var delivery = await StoreRepository.GetWebhookDelivery(CurrentStoreId, webhookId, deliveryId);
|
||||
if (delivery is null)
|
||||
return WebhookDeliveryNotFound();
|
||||
if (delivery.GetBlob().IsPruned())
|
||||
return WebhookDeliveryPruned();
|
||||
return File(delivery.GetBlob().Request, "application/json");
|
||||
}
|
||||
|
||||
private Client.Models.WebhookDeliveryData FromModel(Data.WebhookDeliveryData data)
|
||||
{
|
||||
var b = data.GetBlob();
|
||||
return new Client.Models.WebhookDeliveryData()
|
||||
{
|
||||
Id = data.Id,
|
||||
Timestamp = data.Timestamp,
|
||||
Status = b.Status,
|
||||
ErrorMessage = b.ErrorMessage,
|
||||
HttpCode = b.HttpCode
|
||||
};
|
||||
}
|
||||
|
||||
Client.Models.StoreWebhookData FromModel(Data.WebhookData data, bool includeSecret)
|
||||
{
|
||||
var b = data.GetBlob();
|
||||
return new Client.Models.StoreWebhookData()
|
||||
{
|
||||
Id = data.Id,
|
||||
Url = b.Url,
|
||||
Enabled = b.Active,
|
||||
Secret = includeSecret ? b.Secret : null,
|
||||
AutomaticRedelivery = b.AutomaticRedelivery,
|
||||
AuthorizedEvents = new Client.Models.StoreWebhookData.AuthorizedEventsData()
|
||||
{
|
||||
Everything = b.AuthorizedEvents.Everything,
|
||||
SpecificEvents = b.AuthorizedEvents.SpecificEvents
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
#nullable enable
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
using BTCPayServer.Abstractions.Models;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Plugins.Webhooks.Views;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Localization;
|
||||
using NBitcoin;
|
||||
using NBitcoin.DataEncoders;
|
||||
|
||||
namespace BTCPayServer.Plugins.Webhooks.Controllers;
|
||||
|
||||
[Route("stores")]
|
||||
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
[Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
[AutoValidateAntiforgeryToken]
|
||||
[Area(WebhooksPlugin.Area)]
|
||||
public class UIStoreWebhooksController(
|
||||
StoreRepository storeRepo,
|
||||
IStringLocalizer stringLocalizer,
|
||||
WebhookSender webhookSender) : Controller
|
||||
{
|
||||
public Data.StoreData CurrentStore => HttpContext.GetStoreData();
|
||||
public IStringLocalizer StringLocalizer { get; set; } = stringLocalizer;
|
||||
private async Task<Data.WebhookDeliveryData?> LastDeliveryForWebhook(string webhookId)
|
||||
{
|
||||
return (await storeRepo.GetWebhookDeliveries(CurrentStore.Id, webhookId, 1)).FirstOrDefault();
|
||||
}
|
||||
|
||||
[HttpGet("{storeId}/webhooks")]
|
||||
public async Task<IActionResult> Webhooks()
|
||||
{
|
||||
var webhooks = await storeRepo.GetWebhooks(CurrentStore.Id);
|
||||
return View(nameof(Webhooks), new WebhooksViewModel
|
||||
{
|
||||
Webhooks = webhooks.Select(async w =>
|
||||
{
|
||||
var lastDelivery = await LastDeliveryForWebhook(w.Id);
|
||||
var lastDeliveryBlob = lastDelivery?.GetBlob();
|
||||
|
||||
return new WebhooksViewModel.WebhookViewModel()
|
||||
{
|
||||
Id = w.Id,
|
||||
Url = w.GetBlob().Url,
|
||||
LastDeliveryErrorMessage = lastDeliveryBlob?.ErrorMessage,
|
||||
LastDeliveryTimeStamp = lastDelivery?.Timestamp,
|
||||
LastDeliverySuccessful = lastDeliveryBlob == null ? true : lastDeliveryBlob.Status == WebhookDeliveryStatus.HttpSuccess,
|
||||
};
|
||||
}
|
||||
).Select(t => t.Result).ToArray()
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet("{storeId}/webhooks/new")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public IActionResult NewWebhook()
|
||||
{
|
||||
return View(nameof(ModifyWebhook), new EditWebhookViewModel
|
||||
{
|
||||
Active = true,
|
||||
Everything = true,
|
||||
IsNew = true,
|
||||
Secret = Encoders.Base58.EncodeData(RandomUtils.GetBytes(20))
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost("{storeId}/webhooks/{webhookId}/remove")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> DeleteWebhook(string webhookId)
|
||||
{
|
||||
var webhook = await storeRepo.GetWebhook(CurrentStore.Id, webhookId);
|
||||
if (webhook is null)
|
||||
return NotFound();
|
||||
|
||||
await storeRepo.DeleteWebhook(CurrentStore.Id, webhookId);
|
||||
TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["Webhook successfully deleted"].Value;
|
||||
return RedirectToAction(nameof(Webhooks), new { storeId = CurrentStore.Id });
|
||||
}
|
||||
|
||||
[HttpPost("{storeId}/webhooks/new")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> NewWebhook(string storeId, EditWebhookViewModel viewModel)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
return View(nameof(ModifyWebhook), viewModel);
|
||||
|
||||
await storeRepo.CreateWebhook(CurrentStore.Id, viewModel.CreateBlob());
|
||||
TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["The webhook has been created"].Value;
|
||||
return RedirectToAction(nameof(Webhooks), new { storeId });
|
||||
}
|
||||
|
||||
[HttpGet("{storeId}/webhooks/{webhookId}")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> ModifyWebhook(string webhookId)
|
||||
{
|
||||
var webhook = await storeRepo.GetWebhook(CurrentStore.Id, webhookId);
|
||||
if (webhook is null)
|
||||
return NotFound();
|
||||
|
||||
var blob = webhook.GetBlob();
|
||||
var deliveries = await storeRepo.GetWebhookDeliveries(CurrentStore.Id, webhookId, 20);
|
||||
return View(nameof(ModifyWebhook), new EditWebhookViewModel(blob)
|
||||
{
|
||||
Deliveries = deliveries
|
||||
.Select(s => new DeliveryViewModel(s)).ToList()
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost("{storeId}/webhooks/{webhookId}")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> ModifyWebhook(string webhookId, EditWebhookViewModel viewModel)
|
||||
{
|
||||
var webhook = await storeRepo.GetWebhook(CurrentStore.Id, webhookId);
|
||||
if (webhook is null)
|
||||
return NotFound();
|
||||
if (!ModelState.IsValid)
|
||||
return View(nameof(ModifyWebhook), viewModel);
|
||||
|
||||
await storeRepo.UpdateWebhook(CurrentStore.Id, webhookId, viewModel.CreateBlob());
|
||||
TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["The webhook has been updated"].Value;
|
||||
return RedirectToAction(nameof(Webhooks), new { storeId = CurrentStore.Id });
|
||||
}
|
||||
|
||||
[HttpPost("{storeId}/webhooks/{webhookId}/deliveries/{deliveryId}/redeliver")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> RedeliverWebhook(string webhookId, string deliveryId)
|
||||
{
|
||||
var delivery = await storeRepo.GetWebhookDelivery(CurrentStore.Id, webhookId, deliveryId);
|
||||
if (delivery is null)
|
||||
return NotFound();
|
||||
|
||||
var newDeliveryId = await webhookSender.Redeliver(deliveryId);
|
||||
if (newDeliveryId is null)
|
||||
return NotFound();
|
||||
|
||||
TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["Successfully planned a redelivery"].Value;
|
||||
return RedirectToAction(nameof(ModifyWebhook),
|
||||
new
|
||||
{
|
||||
storeId = CurrentStore.Id,
|
||||
webhookId
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet("{storeId}/webhooks/{webhookId}/deliveries/{deliveryId}/request")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> WebhookDelivery(string webhookId, string deliveryId)
|
||||
{
|
||||
var delivery = await storeRepo.GetWebhookDelivery(CurrentStore.Id, webhookId, deliveryId);
|
||||
if (delivery is null)
|
||||
return NotFound();
|
||||
|
||||
return File(delivery.GetBlob().Request, "application/json");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user