mirror of
https://github.com/aljazceru/btcpayserver.git
synced 2025-12-17 22:14:26 +01:00
Add Greenfield API
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace BTCPayServer.Client
|
namespace BTCPayServer.Client
|
||||||
{
|
{
|
||||||
|
|||||||
65
BTCPayServer.Client/BTCPayServerClient.Webhooks.cs
Normal file
65
BTCPayServer.Client/BTCPayServerClient.Webhooks.cs
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using BTCPayServer.Client.Models;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Client
|
||||||
|
{
|
||||||
|
public partial class BTCPayServerClient
|
||||||
|
{
|
||||||
|
public async Task<StoreWebhookData> CreateWebhook(string storeId, Client.Models.CreateStoreWebhookRequest create, CancellationToken token = default)
|
||||||
|
{
|
||||||
|
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{storeId}/webhooks", bodyPayload: create, method: HttpMethod.Post), token);
|
||||||
|
return await HandleResponse<StoreWebhookData>(response);
|
||||||
|
}
|
||||||
|
public async Task<StoreWebhookData> GetWebhook(string storeId, string webhookId, CancellationToken token = default)
|
||||||
|
{
|
||||||
|
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{storeId}/webhooks/{webhookId}"), token);
|
||||||
|
if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
|
||||||
|
return null;
|
||||||
|
return await HandleResponse<StoreWebhookData>(response);
|
||||||
|
}
|
||||||
|
public async Task<StoreWebhookData> UpdateWebhook(string storeId, string webhookId, Models.UpdateStoreWebhookRequest update, CancellationToken token = default)
|
||||||
|
{
|
||||||
|
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{storeId}/webhooks/{webhookId}", bodyPayload: update, method: HttpMethod.Put), token);
|
||||||
|
return await HandleResponse<StoreWebhookData>(response);
|
||||||
|
}
|
||||||
|
public async Task<bool> DeleteWebhook(string storeId, string webhookId, CancellationToken token = default)
|
||||||
|
{
|
||||||
|
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{storeId}/webhooks/{webhookId}", method: HttpMethod.Delete), token);
|
||||||
|
return response.IsSuccessStatusCode;
|
||||||
|
}
|
||||||
|
public async Task<StoreWebhookData[]> GetWebhooks(string storeId, CancellationToken token = default)
|
||||||
|
{
|
||||||
|
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{storeId}/webhooks"), token);
|
||||||
|
return await HandleResponse<StoreWebhookData[]>(response);
|
||||||
|
}
|
||||||
|
public async Task<WebhookDeliveryData[]> GetWebhookDeliveries(string storeId, string webhookId, CancellationToken token = default)
|
||||||
|
{
|
||||||
|
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{storeId}/webhooks/{webhookId}/deliveries"), token);
|
||||||
|
return await HandleResponse<WebhookDeliveryData[]>(response);
|
||||||
|
}
|
||||||
|
public async Task<WebhookDeliveryData> GetWebhookDelivery(string storeId, string webhookId, string deliveryId, CancellationToken token = default)
|
||||||
|
{
|
||||||
|
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{storeId}/webhooks/{webhookId}/deliveries/{deliveryId}"), token);
|
||||||
|
return await HandleResponse<WebhookDeliveryData>(response);
|
||||||
|
}
|
||||||
|
public async Task<string> RedeliverWebhook(string storeId, string webhookId, string deliveryId, CancellationToken token = default)
|
||||||
|
{
|
||||||
|
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{storeId}/webhooks/{webhookId}/deliveries/{deliveryId}/redeliver", null, HttpMethod.Post), token);
|
||||||
|
return await HandleResponse<string>(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<JObject> GetWebhookDeliveryRequest(string storeId, string webhookId, string deliveryId, CancellationToken token = default)
|
||||||
|
{
|
||||||
|
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{storeId}/webhooks/{webhookId}/deliveries/{deliveryId}/request"), token);
|
||||||
|
if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
|
||||||
|
return null;
|
||||||
|
return await HandleResponse<JObject>(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -65,7 +65,8 @@ namespace BTCPayServer.Client
|
|||||||
protected async Task<T> HandleResponse<T>(HttpResponseMessage message)
|
protected async Task<T> HandleResponse<T>(HttpResponseMessage message)
|
||||||
{
|
{
|
||||||
await HandleResponse(message);
|
await HandleResponse(message);
|
||||||
return JsonConvert.DeserializeObject<T>(await message.Content.ReadAsStringAsync());
|
var str = await message.Content.ReadAsStringAsync();
|
||||||
|
return JsonConvert.DeserializeObject<T>(str);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected virtual HttpRequestMessage CreateHttpRequest(string path,
|
protected virtual HttpRequestMessage CreateHttpRequest(string path,
|
||||||
|
|||||||
35
BTCPayServer.Client/Models/StoreWebhookData.cs
Normal file
35
BTCPayServer.Client/Models/StoreWebhookData.cs
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Client.Models
|
||||||
|
{
|
||||||
|
public class StoreWebhookBaseData
|
||||||
|
{
|
||||||
|
public class AuthorizedEventsData
|
||||||
|
{
|
||||||
|
public bool Everything { get; set; } = true;
|
||||||
|
|
||||||
|
[JsonProperty(ItemConverterType = typeof(Newtonsoft.Json.Converters.StringEnumConverter))]
|
||||||
|
public WebhookEventType[] SpecificEvents { get; set; } = Array.Empty<WebhookEventType>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Enabled { get; set; } = true;
|
||||||
|
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||||
|
public string Secret { get; set; }
|
||||||
|
public bool AutomaticRedelivery { get; set; } = true;
|
||||||
|
public string Url { get; set; }
|
||||||
|
public AuthorizedEventsData AuthorizedEvents { get; set; } = new AuthorizedEventsData();
|
||||||
|
}
|
||||||
|
public class UpdateStoreWebhookRequest : StoreWebhookBaseData
|
||||||
|
{
|
||||||
|
}
|
||||||
|
public class CreateStoreWebhookRequest : StoreWebhookBaseData
|
||||||
|
{
|
||||||
|
}
|
||||||
|
public class StoreWebhookData : StoreWebhookBaseData
|
||||||
|
{
|
||||||
|
public string Id { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
18
BTCPayServer.Client/Models/WebhookDeliveryData.cs
Normal file
18
BTCPayServer.Client/Models/WebhookDeliveryData.cs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Client.Models
|
||||||
|
{
|
||||||
|
public class WebhookDeliveryData
|
||||||
|
{
|
||||||
|
public string Id { get; set; }
|
||||||
|
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
|
||||||
|
public DateTimeOffset Timestamp { get; set; }
|
||||||
|
public int? HttpCode { get; set; }
|
||||||
|
public string ErrorMessage { get; set; }
|
||||||
|
[JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))]
|
||||||
|
public WebhookDeliveryStatus Status { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ namespace BTCPayServer.Client
|
|||||||
public const string CanUseLightningNodeInStore = "btcpay.store.canuselightningnode";
|
public const string CanUseLightningNodeInStore = "btcpay.store.canuselightningnode";
|
||||||
public const string CanModifyServerSettings = "btcpay.server.canmodifyserversettings";
|
public const string CanModifyServerSettings = "btcpay.server.canmodifyserversettings";
|
||||||
public const string CanModifyStoreSettings = "btcpay.store.canmodifystoresettings";
|
public const string CanModifyStoreSettings = "btcpay.store.canmodifystoresettings";
|
||||||
|
public const string CanModifyStoreWebhooks = "btcpay.store.webhooks.canmodifywebhooks";
|
||||||
public const string CanModifyStoreSettingsUnscoped = "btcpay.store.canmodifystoresettings:";
|
public const string CanModifyStoreSettingsUnscoped = "btcpay.store.canmodifystoresettings:";
|
||||||
public const string CanViewStoreSettings = "btcpay.store.canviewstoresettings";
|
public const string CanViewStoreSettings = "btcpay.store.canviewstoresettings";
|
||||||
public const string CanViewInvoices = "btcpay.store.canviewinvoices";
|
public const string CanViewInvoices = "btcpay.store.canviewinvoices";
|
||||||
@@ -29,6 +30,7 @@ namespace BTCPayServer.Client
|
|||||||
{
|
{
|
||||||
yield return CanViewInvoices;
|
yield return CanViewInvoices;
|
||||||
yield return CanCreateInvoice;
|
yield return CanCreateInvoice;
|
||||||
|
yield return CanModifyStoreWebhooks;
|
||||||
yield return CanModifyServerSettings;
|
yield return CanModifyServerSettings;
|
||||||
yield return CanModifyStoreSettings;
|
yield return CanModifyStoreSettings;
|
||||||
yield return CanViewStoreSettings;
|
yield return CanViewStoreSettings;
|
||||||
@@ -156,6 +158,7 @@ namespace BTCPayServer.Client
|
|||||||
switch (subpolicy)
|
switch (subpolicy)
|
||||||
{
|
{
|
||||||
case Policies.CanViewInvoices when this.Policy == Policies.CanModifyStoreSettings:
|
case Policies.CanViewInvoices when this.Policy == Policies.CanModifyStoreSettings:
|
||||||
|
case Policies.CanModifyStoreWebhooks when this.Policy == Policies.CanModifyStoreSettings:
|
||||||
case Policies.CanViewInvoices when this.Policy == Policies.CanViewStoreSettings:
|
case Policies.CanViewInvoices when this.Policy == Policies.CanViewStoreSettings:
|
||||||
case Policies.CanViewStoreSettings when this.Policy == Policies.CanModifyStoreSettings:
|
case Policies.CanViewStoreSettings when this.Policy == Policies.CanModifyStoreSettings:
|
||||||
case Policies.CanCreateInvoice when this.Policy == Policies.CanModifyStoreSettings:
|
case Policies.CanCreateInvoice when this.Policy == Policies.CanModifyStoreSettings:
|
||||||
|
|||||||
@@ -149,12 +149,12 @@ namespace BTCPayServer.Tests
|
|||||||
var user1 = await unauthClient.CreateUser(
|
var user1 = await unauthClient.CreateUser(
|
||||||
new CreateApplicationUserRequest() { Email = "test@gmail.com", Password = "abceudhqw" });
|
new CreateApplicationUserRequest() { Email = "test@gmail.com", Password = "abceudhqw" });
|
||||||
Assert.Empty(user1.Roles);
|
Assert.Empty(user1.Roles);
|
||||||
|
|
||||||
// We have no admin, so it should work
|
// We have no admin, so it should work
|
||||||
var user2 = await unauthClient.CreateUser(
|
var user2 = await unauthClient.CreateUser(
|
||||||
new CreateApplicationUserRequest() { Email = "test2@gmail.com", Password = "abceudhqw" });
|
new CreateApplicationUserRequest() { Email = "test2@gmail.com", Password = "abceudhqw" });
|
||||||
Assert.Empty(user2.Roles);
|
Assert.Empty(user2.Roles);
|
||||||
|
|
||||||
// Duplicate email
|
// Duplicate email
|
||||||
await AssertValidationError(new[] { "Email" },
|
await AssertValidationError(new[] { "Email" },
|
||||||
async () => await unauthClient.CreateUser(
|
async () => await unauthClient.CreateUser(
|
||||||
@@ -170,7 +170,7 @@ namespace BTCPayServer.Tests
|
|||||||
Assert.Contains("ServerAdmin", admin.Roles);
|
Assert.Contains("ServerAdmin", admin.Roles);
|
||||||
Assert.NotNull(admin.Created);
|
Assert.NotNull(admin.Created);
|
||||||
Assert.True((DateTimeOffset.Now - admin.Created).Value.Seconds < 10);
|
Assert.True((DateTimeOffset.Now - admin.Created).Value.Seconds < 10);
|
||||||
|
|
||||||
// Creating a new user without proper creds is now impossible (unauthorized)
|
// Creating a new user without proper creds is now impossible (unauthorized)
|
||||||
// Because if registration are locked and that an admin exists, we don't accept unauthenticated connection
|
// Because if registration are locked and that an admin exists, we don't accept unauthenticated connection
|
||||||
await AssertHttpError(401,
|
await AssertHttpError(401,
|
||||||
@@ -611,6 +611,98 @@ namespace BTCPayServer.Tests
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact(Timeout = TestTimeout)]
|
||||||
|
[Trait("Integration", "Integration")]
|
||||||
|
public async Task CanUseWebhooks()
|
||||||
|
{
|
||||||
|
void AssertHook(FakeServer fakeServer, Client.Models.StoreWebhookData hook)
|
||||||
|
{
|
||||||
|
Assert.True(hook.Enabled);
|
||||||
|
Assert.True(hook.AuthorizedEvents.Everything);
|
||||||
|
Assert.True(hook.AutomaticRedelivery);
|
||||||
|
Assert.Equal(fakeServer.ServerUri.AbsoluteUri, hook.Url);
|
||||||
|
}
|
||||||
|
using var tester = ServerTester.Create();
|
||||||
|
using var fakeServer = new FakeServer();
|
||||||
|
await fakeServer.Start();
|
||||||
|
await tester.StartAsync();
|
||||||
|
var user = tester.NewAccount();
|
||||||
|
user.GrantAccess();
|
||||||
|
user.RegisterDerivationScheme("BTC");
|
||||||
|
var clientProfile = await user.CreateClient(Policies.CanModifyStoreWebhooks, Policies.CanCreateInvoice);
|
||||||
|
var hook = await clientProfile.CreateWebhook(user.StoreId, new CreateStoreWebhookRequest()
|
||||||
|
{
|
||||||
|
Url = fakeServer.ServerUri.AbsoluteUri,
|
||||||
|
AutomaticRedelivery = false
|
||||||
|
});
|
||||||
|
Assert.NotNull(hook.Secret);
|
||||||
|
AssertHook(fakeServer, hook);
|
||||||
|
hook = await clientProfile.GetWebhook(user.StoreId, hook.Id);
|
||||||
|
AssertHook(fakeServer, hook);
|
||||||
|
var hooks = await clientProfile.GetWebhooks(user.StoreId);
|
||||||
|
hook = Assert.Single(hooks);
|
||||||
|
AssertHook(fakeServer, hook);
|
||||||
|
await clientProfile.CreateInvoice(user.StoreId,
|
||||||
|
new CreateInvoiceRequest() { Currency = "USD", Amount = 100 });
|
||||||
|
var req = await fakeServer.GetNextRequest();
|
||||||
|
req.Response.StatusCode = 200;
|
||||||
|
fakeServer.Done();
|
||||||
|
hook = await clientProfile.UpdateWebhook(user.StoreId, hook.Id, new UpdateStoreWebhookRequest()
|
||||||
|
{
|
||||||
|
Url = hook.Url,
|
||||||
|
Secret = "lol",
|
||||||
|
AutomaticRedelivery = false
|
||||||
|
});
|
||||||
|
Assert.Null(hook.Secret);
|
||||||
|
AssertHook(fakeServer, hook);
|
||||||
|
var deliveries = await clientProfile.GetWebhookDeliveries(user.StoreId, hook.Id);
|
||||||
|
var delivery = Assert.Single(deliveries);
|
||||||
|
delivery = await clientProfile.GetWebhookDelivery(user.StoreId, hook.Id, delivery.Id);
|
||||||
|
Assert.NotNull(delivery);
|
||||||
|
Assert.Equal(WebhookDeliveryStatus.HttpSuccess, delivery.Status);
|
||||||
|
|
||||||
|
var newDeliveryId = await clientProfile.RedeliverWebhook(user.StoreId, hook.Id, delivery.Id);
|
||||||
|
req = await fakeServer.GetNextRequest();
|
||||||
|
req.Response.StatusCode = 404;
|
||||||
|
fakeServer.Done();
|
||||||
|
await TestUtils.EventuallyAsync(async () =>
|
||||||
|
{
|
||||||
|
var newDelivery = await clientProfile.GetWebhookDelivery(user.StoreId, hook.Id, newDeliveryId);
|
||||||
|
Assert.NotNull(newDelivery);
|
||||||
|
Assert.Equal(404, newDelivery.HttpCode);
|
||||||
|
Assert.Equal(WebhookDeliveryStatus.HttpError, newDelivery.Status);
|
||||||
|
});
|
||||||
|
deliveries = await clientProfile.GetWebhookDeliveries(user.StoreId, hook.Id);
|
||||||
|
Assert.Equal(2, deliveries.Length);
|
||||||
|
Assert.Equal(newDeliveryId, deliveries[0].Id);
|
||||||
|
var jObj = await clientProfile.GetWebhookDeliveryRequest(user.StoreId, hook.Id, newDeliveryId);
|
||||||
|
Assert.NotNull(jObj);
|
||||||
|
|
||||||
|
Logs.Tester.LogInformation("Should not be able to access webhook without proper auth");
|
||||||
|
var unauthorized = await user.CreateClient(Policies.CanCreateInvoice);
|
||||||
|
await AssertHttpError(403, async () =>
|
||||||
|
{
|
||||||
|
await unauthorized.GetWebhookDeliveryRequest(user.StoreId, hook.Id, newDeliveryId);
|
||||||
|
});
|
||||||
|
|
||||||
|
Logs.Tester.LogInformation("Can use btcpay.store.canmodifystoresettings to query webhooks");
|
||||||
|
clientProfile = await user.CreateClient(Policies.CanModifyStoreSettings, Policies.CanCreateInvoice);
|
||||||
|
await clientProfile.GetWebhookDeliveryRequest(user.StoreId, hook.Id, newDeliveryId);
|
||||||
|
|
||||||
|
Logs.Tester.LogInformation("Testing corner cases");
|
||||||
|
Assert.Null(await clientProfile.GetWebhookDeliveryRequest(user.StoreId, "lol", newDeliveryId));
|
||||||
|
Assert.Null(await clientProfile.GetWebhookDeliveryRequest(user.StoreId, hook.Id, "lol"));
|
||||||
|
Assert.Null(await clientProfile.GetWebhookDeliveryRequest(user.StoreId, "lol", "lol"));
|
||||||
|
Assert.Null(await clientProfile.GetWebhook(user.StoreId, "lol"));
|
||||||
|
await AssertHttpError(404, async () =>
|
||||||
|
{
|
||||||
|
await clientProfile.UpdateWebhook(user.StoreId, "lol", new UpdateStoreWebhookRequest() { Url = hook.Url });
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.True(await clientProfile.DeleteWebhook(user.StoreId, hook.Id));
|
||||||
|
Assert.False(await clientProfile.DeleteWebhook(user.StoreId, hook.Id));
|
||||||
|
}
|
||||||
|
|
||||||
[Fact(Timeout = TestTimeout)]
|
[Fact(Timeout = TestTimeout)]
|
||||||
[Trait("Integration", "Integration")]
|
[Trait("Integration", "Integration")]
|
||||||
public async Task HealthControllerTests()
|
public async Task HealthControllerTests()
|
||||||
@@ -880,8 +972,8 @@ namespace BTCPayServer.Tests
|
|||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact(Timeout = 60 * 2 * 1000)]
|
[Fact(Timeout = 60 * 2 * 1000)]
|
||||||
[Trait("Integration", "Integration")]
|
[Trait("Integration", "Integration")]
|
||||||
[Trait("Lightning", "Lightning")]
|
[Trait("Lightning", "Lightning")]
|
||||||
public async Task CanUseLightningAPI()
|
public async Task CanUseLightningAPI()
|
||||||
@@ -907,7 +999,7 @@ namespace BTCPayServer.Tests
|
|||||||
var info = await client.GetLightningNodeInfo("BTC");
|
var info = await client.GetLightningNodeInfo("BTC");
|
||||||
Assert.Single(info.NodeURIs);
|
Assert.Single(info.NodeURIs);
|
||||||
Assert.NotEqual(0, info.BlockHeight);
|
Assert.NotEqual(0, info.BlockHeight);
|
||||||
|
|
||||||
var err = await Assert.ThrowsAsync<HttpRequestException>(async () => await client.GetLightningNodeChannels("BTC"));
|
var err = await Assert.ThrowsAsync<HttpRequestException>(async () => await client.GetLightningNodeChannels("BTC"));
|
||||||
Assert.Contains("503", err.Message);
|
Assert.Contains("503", err.Message);
|
||||||
// Not permission for the store!
|
// Not permission for the store!
|
||||||
|
|||||||
@@ -1,89 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Channels;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Microsoft.AspNetCore.Builder;
|
|
||||||
using Microsoft.AspNetCore.Hosting;
|
|
||||||
using Microsoft.AspNetCore.Hosting.Server.Features;
|
|
||||||
using Microsoft.AspNetCore.Http;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
|
|
||||||
namespace BTCPayServer.Tests
|
|
||||||
{
|
|
||||||
public class RawHttpServer : IDisposable
|
|
||||||
{
|
|
||||||
public class RawRequest
|
|
||||||
{
|
|
||||||
public RawRequest(TaskCompletionSource<bool> taskCompletion)
|
|
||||||
{
|
|
||||||
TaskCompletion = taskCompletion;
|
|
||||||
}
|
|
||||||
public HttpContext HttpContext { get; set; }
|
|
||||||
public TaskCompletionSource<bool> TaskCompletion { get; }
|
|
||||||
|
|
||||||
public void Complete()
|
|
||||||
{
|
|
||||||
TaskCompletion.SetResult(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
readonly IWebHost _Host = null;
|
|
||||||
readonly CancellationTokenSource _Closed = new CancellationTokenSource();
|
|
||||||
readonly Channel<RawRequest> _Requests = Channel.CreateUnbounded<RawRequest>();
|
|
||||||
public RawHttpServer()
|
|
||||||
{
|
|
||||||
var port = Utils.FreeTcpPort();
|
|
||||||
_Host = new WebHostBuilder()
|
|
||||||
.Configure(app =>
|
|
||||||
{
|
|
||||||
app.Run(req =>
|
|
||||||
{
|
|
||||||
var cts = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
|
||||||
_Requests.Writer.TryWrite(new RawRequest(cts)
|
|
||||||
{
|
|
||||||
HttpContext = req
|
|
||||||
});
|
|
||||||
return cts.Task;
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.UseKestrel()
|
|
||||||
.UseUrls("http://127.0.0.1:" + port)
|
|
||||||
.Build();
|
|
||||||
_Host.Start();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Uri GetUri()
|
|
||||||
{
|
|
||||||
return new Uri(_Host.ServerFeatures.Get<IServerAddressesFeature>().Addresses.First());
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<RawRequest> GetNextRequest()
|
|
||||||
{
|
|
||||||
using (CancellationTokenSource cancellation = new CancellationTokenSource(20 * 1000))
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
RawRequest req = null;
|
|
||||||
while (!await _Requests.Reader.WaitToReadAsync(cancellation.Token) ||
|
|
||||||
!_Requests.Reader.TryRead(out req))
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
return req;
|
|
||||||
}
|
|
||||||
catch (TaskCanceledException)
|
|
||||||
{
|
|
||||||
throw new Xunit.Sdk.XunitException("Callback to the webserver was expected, check if the callback url is accessible from internet");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
_Closed.Cancel();
|
|
||||||
_Host.Dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -639,10 +639,10 @@ namespace BTCPayServer.Tests
|
|||||||
Logs.Tester.LogInformation("Let's try to update one of them");
|
Logs.Tester.LogInformation("Let's try to update one of them");
|
||||||
s.Driver.FindElement(By.LinkText("Modify")).Click();
|
s.Driver.FindElement(By.LinkText("Modify")).Click();
|
||||||
|
|
||||||
using RawHttpServer server = new RawHttpServer();
|
using FakeServer server = new FakeServer();
|
||||||
|
await server.Start();
|
||||||
s.Driver.FindElement(By.Name("PayloadUrl")).Clear();
|
s.Driver.FindElement(By.Name("PayloadUrl")).Clear();
|
||||||
s.Driver.FindElement(By.Name("PayloadUrl")).SendKeys(server.GetUri().AbsoluteUri);
|
s.Driver.FindElement(By.Name("PayloadUrl")).SendKeys(server.ServerUri.AbsoluteUri);
|
||||||
s.Driver.FindElement(By.Name("Secret")).Clear();
|
s.Driver.FindElement(By.Name("Secret")).Clear();
|
||||||
s.Driver.FindElement(By.Name("Secret")).SendKeys("HelloWorld");
|
s.Driver.FindElement(By.Name("Secret")).SendKeys("HelloWorld");
|
||||||
s.Driver.FindElement(By.Name("update")).Click();
|
s.Driver.FindElement(By.Name("update")).Click();
|
||||||
@@ -663,26 +663,26 @@ namespace BTCPayServer.Tests
|
|||||||
|
|
||||||
s.Driver.FindElement(By.Name("update")).Click();
|
s.Driver.FindElement(By.Name("update")).Click();
|
||||||
s.AssertHappyMessage();
|
s.AssertHappyMessage();
|
||||||
Assert.Contains(server.GetUri().AbsoluteUri, s.Driver.PageSource);
|
Assert.Contains(server.ServerUri.AbsoluteUri, s.Driver.PageSource);
|
||||||
|
|
||||||
Logs.Tester.LogInformation("Let's see if we can generate an event");
|
Logs.Tester.LogInformation("Let's see if we can generate an event");
|
||||||
s.GoToStore(store.storeId);
|
s.GoToStore(store.storeId);
|
||||||
s.AddDerivationScheme();
|
s.AddDerivationScheme();
|
||||||
s.CreateInvoice(store.storeName);
|
s.CreateInvoice(store.storeName);
|
||||||
var request = await server.GetNextRequest();
|
var request = await server.GetNextRequest();
|
||||||
var headers = request.HttpContext.Request.Headers;
|
var headers = request.Request.Headers;
|
||||||
var actualSig = headers["BTCPay-Sig"].First();
|
var actualSig = headers["BTCPay-Sig"].First();
|
||||||
var bytes = await request.HttpContext.Request.Body.ReadBytesAsync((int)headers.ContentLength.Value);
|
var bytes = await request.Request.Body.ReadBytesAsync((int)headers.ContentLength.Value);
|
||||||
var expectedSig = $"sha256={Encoders.Hex.EncodeData(new HMACSHA256(Encoding.UTF8.GetBytes("HelloWorld")).ComputeHash(bytes))}";
|
var expectedSig = $"sha256={Encoders.Hex.EncodeData(new HMACSHA256(Encoding.UTF8.GetBytes("HelloWorld")).ComputeHash(bytes))}";
|
||||||
Assert.Equal(expectedSig, actualSig);
|
Assert.Equal(expectedSig, actualSig);
|
||||||
request.HttpContext.Response.StatusCode = 200;
|
request.Response.StatusCode = 200;
|
||||||
request.Complete();
|
server.Done();
|
||||||
|
|
||||||
Logs.Tester.LogInformation("Let's make a failed event");
|
Logs.Tester.LogInformation("Let's make a failed event");
|
||||||
s.CreateInvoice(store.storeName);
|
s.CreateInvoice(store.storeName);
|
||||||
request = await server.GetNextRequest();
|
request = await server.GetNextRequest();
|
||||||
request.HttpContext.Response.StatusCode = 404;
|
request.Response.StatusCode = 404;
|
||||||
request.Complete();
|
server.Done();
|
||||||
|
|
||||||
// The delivery is done asynchronously, so small wait here
|
// The delivery is done asynchronously, so small wait here
|
||||||
await Task.Delay(500);
|
await Task.Delay(500);
|
||||||
@@ -695,8 +695,8 @@ namespace BTCPayServer.Tests
|
|||||||
elements[0].Click();
|
elements[0].Click();
|
||||||
s.AssertHappyMessage();
|
s.AssertHappyMessage();
|
||||||
request = await server.GetNextRequest();
|
request = await server.GetNextRequest();
|
||||||
request.HttpContext.Response.StatusCode = 404;
|
request.Response.StatusCode = 404;
|
||||||
request.Complete();
|
server.Done();
|
||||||
|
|
||||||
Logs.Tester.LogInformation("Can we browse the json content?");
|
Logs.Tester.LogInformation("Can we browse the json content?");
|
||||||
CanBrowseContent(s);
|
CanBrowseContent(s);
|
||||||
@@ -708,8 +708,8 @@ namespace BTCPayServer.Tests
|
|||||||
element.Click();
|
element.Click();
|
||||||
s.AssertHappyMessage();
|
s.AssertHappyMessage();
|
||||||
request = await server.GetNextRequest();
|
request = await server.GetNextRequest();
|
||||||
request.HttpContext.Response.StatusCode = 404;
|
request.Response.StatusCode = 404;
|
||||||
request.Complete();
|
server.Done();
|
||||||
|
|
||||||
Logs.Tester.LogInformation("Let's see if we can delete store with some webhooks inside");
|
Logs.Tester.LogInformation("Let's see if we can delete store with some webhooks inside");
|
||||||
s.GoToStore(store.storeId);
|
s.GoToStore(store.storeId);
|
||||||
|
|||||||
@@ -251,5 +251,5 @@
|
|||||||
<_ContentIncludedByDefault Remove="Views\Components\NotificationsDropdown\Default.cshtml" />
|
<_ContentIncludedByDefault Remove="Views\Components\NotificationsDropdown\Default.cshtml" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ProjectExtensions><VisualStudio><UserProperties wwwroot_4swagger_4v1_4swagger_1template_1invoices_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" wwwroot_4swagger_4v1_4swagger_1template_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" wwwroot_4swagger_4v1_4swagger_1template_1pull-payments_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" wwwroot_4swagger_4v1_4swagger_1template_1serverinfo_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" wwwroot_4swagger_4v1_4swagger_1template_1stores_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" /></VisualStudio></ProjectExtensions>
|
<ProjectExtensions><VisualStudio><UserProperties wwwroot_4swagger_4v1_4swagger_1template_1invoices_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" wwwroot_4swagger_4v1_4swagger_1template_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" wwwroot_4swagger_4v1_4swagger_1template_1pull-payments_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" wwwroot_4swagger_4v1_4swagger_1template_1serverinfo_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" wwwroot_4swagger_4v1_4swagger_1template_1stores_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" wwwroot_4swagger_4v1_4swagger_1template_1users_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" wwwroot_4swagger_4v1_4swagger_1template_1webhooks_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" /></VisualStudio></ProjectExtensions>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
198
BTCPayServer/Controllers/GreenField/StoreWebhooksController.cs
Normal file
198
BTCPayServer/Controllers/GreenField/StoreWebhooksController.cs
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Amazon.Runtime;
|
||||||
|
using BTCPayServer.Client;
|
||||||
|
using BTCPayServer.Client.Models;
|
||||||
|
using BTCPayServer.Data;
|
||||||
|
using BTCPayServer.HostedServices;
|
||||||
|
using BTCPayServer.Security;
|
||||||
|
using BTCPayServer.Services.Stores;
|
||||||
|
using Google.Apis.Auth.OAuth2;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Cors;
|
||||||
|
using Microsoft.AspNetCore.Hosting;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
using Org.BouncyCastle.Bcpg.OpenPgp;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Controllers.GreenField
|
||||||
|
{
|
||||||
|
[ApiController]
|
||||||
|
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield,
|
||||||
|
Policy = Policies.CanModifyStoreWebhooks)]
|
||||||
|
[EnableCors(CorsPolicies.All)]
|
||||||
|
public class StoreWebhooksController : ControllerBase
|
||||||
|
{
|
||||||
|
public StoreWebhooksController(StoreRepository storeRepository, WebhookNotificationManager webhookNotificationManager)
|
||||||
|
{
|
||||||
|
StoreRepository = storeRepository;
|
||||||
|
WebhookNotificationManager = webhookNotificationManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public StoreRepository StoreRepository { get; }
|
||||||
|
public WebhookNotificationManager WebhookNotificationManager { get; }
|
||||||
|
|
||||||
|
[HttpGet("~/api/v1/stores/{storeId}/webhooks/{webhookId?}")]
|
||||||
|
public async Task<IActionResult> ListWebhooks(string storeId, string webhookId)
|
||||||
|
{
|
||||||
|
if (webhookId is null)
|
||||||
|
{
|
||||||
|
var store = HttpContext.GetStoreData();
|
||||||
|
if (store == null)
|
||||||
|
return NotFound();
|
||||||
|
return Ok((await StoreRepository.GetWebhooks(storeId))
|
||||||
|
.Select(o => FromModel(o, false))
|
||||||
|
.ToList());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var w = await StoreRepository.GetWebhook(storeId, webhookId);
|
||||||
|
if (w is null)
|
||||||
|
return NotFound();
|
||||||
|
return Ok(FromModel(w, false));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
[HttpPost("~/api/v1/stores/{storeId}/webhooks")]
|
||||||
|
public async Task<IActionResult> CreateWebhook(string storeId, Client.Models.CreateStoreWebhookRequest create)
|
||||||
|
{
|
||||||
|
var store = HttpContext.GetStoreData();
|
||||||
|
if (store == null)
|
||||||
|
return NotFound();
|
||||||
|
ValidateWebhookRequest(create);
|
||||||
|
if (!ModelState.IsValid)
|
||||||
|
return this.CreateValidationError(ModelState);
|
||||||
|
var webhookId = await StoreRepository.CreateWebhook(storeId, ToModel(create));
|
||||||
|
var w = await StoreRepository.GetWebhook(storeId, webhookId);
|
||||||
|
if (w is null)
|
||||||
|
return NotFound();
|
||||||
|
return Ok(FromModel(w, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ValidateWebhookRequest(StoreWebhookBaseData create)
|
||||||
|
{
|
||||||
|
if (!Uri.TryCreate(create?.Url, UriKind.Absolute, out var uri))
|
||||||
|
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 store = HttpContext.GetStoreData();
|
||||||
|
if (store == null)
|
||||||
|
return NotFound();
|
||||||
|
var w = await StoreRepository.GetWebhook(storeId, webhookId);
|
||||||
|
if (w is null)
|
||||||
|
return NotFound();
|
||||||
|
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 store = HttpContext.GetStoreData();
|
||||||
|
if (store == null)
|
||||||
|
return NotFound();
|
||||||
|
var w = await StoreRepository.GetWebhook(storeId, webhookId);
|
||||||
|
if (w is null)
|
||||||
|
return NotFound();
|
||||||
|
await StoreRepository.DeleteWebhook(storeId, webhookId);
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
var store = HttpContext.GetStoreData();
|
||||||
|
if (store == null)
|
||||||
|
return NotFound();
|
||||||
|
return Ok((await StoreRepository.GetWebhookDeliveries(storeId, webhookId, count))
|
||||||
|
.Select(o => FromModel(o))
|
||||||
|
.ToList());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var delivery = await StoreRepository.GetWebhookDelivery(storeId, webhookId, deliveryId);
|
||||||
|
if (delivery is null)
|
||||||
|
return NotFound();
|
||||||
|
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(HttpContext.GetStoreData().Id, webhookId, deliveryId);
|
||||||
|
if (delivery is null)
|
||||||
|
return NotFound();
|
||||||
|
return this.Ok(new JValue(await WebhookNotificationManager.Redeliver(deliveryId)));
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("~/api/v1/stores/{storeId}/webhooks/{webhookId}/deliveries/{deliveryId}/request")]
|
||||||
|
public async Task<IActionResult> GetDeliveryRequest(string storeId, string webhookId, string deliveryId)
|
||||||
|
{
|
||||||
|
var store = HttpContext.GetStoreData();
|
||||||
|
if (store == null)
|
||||||
|
return NotFound();
|
||||||
|
var delivery = await StoreRepository.GetWebhookDelivery(storeId, webhookId, deliveryId);
|
||||||
|
if (delivery is null)
|
||||||
|
return NotFound();
|
||||||
|
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
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -465,6 +465,8 @@ namespace BTCPayServer.Controllers
|
|||||||
{BTCPayServer.Client.Policies.CanCreateUser, ("Create new users", "The app will be able to create new users on this server.")},
|
{BTCPayServer.Client.Policies.CanCreateUser, ("Create new users", "The app will be able to create new users on this server.")},
|
||||||
{BTCPayServer.Client.Policies.CanModifyStoreSettings, ("Modify your stores", "The app will be able to view, modify, delete and create new invoices on all your stores.")},
|
{BTCPayServer.Client.Policies.CanModifyStoreSettings, ("Modify your stores", "The app will be able to view, modify, delete and create new invoices on all your stores.")},
|
||||||
{$"{BTCPayServer.Client.Policies.CanModifyStoreSettings}:", ("Manage selected stores", "The app will be able to view, modify, delete and create new invoices on the selected stores.")},
|
{$"{BTCPayServer.Client.Policies.CanModifyStoreSettings}:", ("Manage selected stores", "The app will be able to view, modify, delete and create new invoices on the selected stores.")},
|
||||||
|
{BTCPayServer.Client.Policies.CanModifyStoreWebhooks, ("Modify stores webhooks", "The app will be mofidy the webhooks of all your stores.")},
|
||||||
|
{$"{BTCPayServer.Client.Policies.CanModifyStoreWebhooks}:", ("Modify selected stores' webhooks", "The app will be mofidy the webhooks of the selected stores.")},
|
||||||
{BTCPayServer.Client.Policies.CanViewStoreSettings, ("View your stores", "The app will be able to view stores settings.")},
|
{BTCPayServer.Client.Policies.CanViewStoreSettings, ("View your stores", "The app will be able to view stores settings.")},
|
||||||
{$"{BTCPayServer.Client.Policies.CanViewStoreSettings}:", ("View your stores", "The app will be able to view the selected stores' settings.")},
|
{$"{BTCPayServer.Client.Policies.CanViewStoreSettings}:", ("View your stores", "The app will be able to view the selected stores' settings.")},
|
||||||
{BTCPayServer.Client.Policies.CanModifyServerSettings, ("Manage your server", "The app will have total control on the server settings of your server")},
|
{BTCPayServer.Client.Policies.CanModifyServerSettings, ("Manage your server", "The app will have total control on the server settings of your server")},
|
||||||
|
|||||||
@@ -57,10 +57,10 @@ namespace BTCPayServer.HostedServices
|
|||||||
class WebhookDeliveryRequest
|
class WebhookDeliveryRequest
|
||||||
{
|
{
|
||||||
public WebhookEvent WebhookEvent;
|
public WebhookEvent WebhookEvent;
|
||||||
public WebhookDeliveryData Delivery;
|
public Data.WebhookDeliveryData Delivery;
|
||||||
public WebhookBlob WebhookBlob;
|
public WebhookBlob WebhookBlob;
|
||||||
public string WebhookId;
|
public string WebhookId;
|
||||||
public WebhookDeliveryRequest(string webhookId, WebhookEvent webhookEvent, WebhookDeliveryData delivery, WebhookBlob webhookBlob)
|
public WebhookDeliveryRequest(string webhookId, WebhookEvent webhookEvent, Data.WebhookDeliveryData delivery, WebhookBlob webhookBlob)
|
||||||
{
|
{
|
||||||
WebhookId = webhookId;
|
WebhookId = webhookId;
|
||||||
WebhookEvent = webhookEvent;
|
WebhookEvent = webhookEvent;
|
||||||
@@ -130,7 +130,7 @@ namespace BTCPayServer.HostedServices
|
|||||||
continue;
|
continue;
|
||||||
if (!ShouldDeliver(webhookEventType, webhookBlob))
|
if (!ShouldDeliver(webhookEventType, webhookBlob))
|
||||||
continue;
|
continue;
|
||||||
WebhookDeliveryData delivery = NewDelivery();
|
Data.WebhookDeliveryData delivery = NewDelivery();
|
||||||
delivery.WebhookId = webhook.Id;
|
delivery.WebhookId = webhook.Id;
|
||||||
var webhookEvent = new WebhookInvoiceEvent();
|
var webhookEvent = new WebhookInvoiceEvent();
|
||||||
webhookEvent.InvoiceId = invoiceEvent.InvoiceId;
|
webhookEvent.InvoiceId = invoiceEvent.InvoiceId;
|
||||||
@@ -293,9 +293,9 @@ namespace BTCPayServer.HostedServices
|
|||||||
return bytes;
|
return bytes;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static WebhookDeliveryData NewDelivery()
|
private static Data.WebhookDeliveryData NewDelivery()
|
||||||
{
|
{
|
||||||
var delivery = new WebhookDeliveryData();
|
var delivery = new Data.WebhookDeliveryData();
|
||||||
delivery.Id = Encoders.Base58.EncodeData(RandomUtils.GetBytes(16));
|
delivery.Id = Encoders.Base58.EncodeData(RandomUtils.GetBytes(16));
|
||||||
delivery.Timestamp = DateTimeOffset.UtcNow;
|
delivery.Timestamp = DateTimeOffset.UtcNow;
|
||||||
return delivery;
|
return delivery;
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ namespace BTCPayServer.Models.StoreViewModels
|
|||||||
{
|
{
|
||||||
|
|
||||||
}
|
}
|
||||||
public DeliveryViewModel(WebhookDeliveryData s)
|
public DeliveryViewModel(Data.WebhookDeliveryData s)
|
||||||
{
|
{
|
||||||
var blob = s.GetBlob();
|
var blob = s.GetBlob();
|
||||||
Id = s.Id;
|
Id = s.Id;
|
||||||
|
|||||||
@@ -19,6 +19,9 @@ namespace BTCPayServer.Payments.PayJoin
|
|||||||
services.AddHttpClient(PayjoinClient.PayjoinOnionNamedClient)
|
services.AddHttpClient(PayjoinClient.PayjoinOnionNamedClient)
|
||||||
.ConfigureHttpClient(h => h.DefaultRequestHeaders.ConnectionClose = true)
|
.ConfigureHttpClient(h => h.DefaultRequestHeaders.ConnectionClose = true)
|
||||||
.ConfigurePrimaryHttpMessageHandler<Socks5HttpClientHandler>();
|
.ConfigurePrimaryHttpMessageHandler<Socks5HttpClientHandler>();
|
||||||
|
services.AddHttpClient(WebhookNotificationManager.OnionNamedClient)
|
||||||
|
.ConfigureHttpClient(h => h.DefaultRequestHeaders.ConnectionClose = true)
|
||||||
|
.ConfigurePrimaryHttpMessageHandler<Socks5HttpClientHandler>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ retry:
|
|||||||
_eventAggregator = eventAggregator;
|
_eventAggregator = eventAggregator;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<WebhookDeliveryData> GetWebhookDelivery(string invoiceId, string deliveryId)
|
public async Task<Data.WebhookDeliveryData> GetWebhookDelivery(string invoiceId, string deliveryId)
|
||||||
{
|
{
|
||||||
using var ctx = _ContextFactory.CreateContext();
|
using var ctx = _ContextFactory.CreateContext();
|
||||||
return await ctx.InvoiceWebhookDeliveries
|
return await ctx.InvoiceWebhookDeliveries
|
||||||
@@ -118,7 +118,7 @@ retry:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<List<WebhookDeliveryData>> GetWebhookDeliveries(string invoiceId)
|
public async Task<List<Data.WebhookDeliveryData>> GetWebhookDeliveries(string invoiceId)
|
||||||
{
|
{
|
||||||
using var ctx = _ContextFactory.CreateContext();
|
using var ctx = _ContextFactory.CreateContext();
|
||||||
return await ctx.InvoiceWebhookDeliveries
|
return await ctx.InvoiceWebhookDeliveries
|
||||||
|
|||||||
@@ -230,14 +230,16 @@ namespace BTCPayServer.Services.Stores
|
|||||||
await ctx.SaveChangesAsync();
|
await ctx.SaveChangesAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<WebhookDeliveryData[]> GetWebhookDeliveries(string storeId, string webhookId, int count)
|
public async Task<WebhookDeliveryData[]> GetWebhookDeliveries(string storeId, string webhookId, int? count)
|
||||||
{
|
{
|
||||||
using var ctx = _ContextFactory.CreateContext();
|
using var ctx = _ContextFactory.CreateContext();
|
||||||
return await ctx.StoreWebhooks
|
IQueryable<WebhookDeliveryData> req = ctx.StoreWebhooks
|
||||||
.Where(s => s.StoreId == storeId && s.WebhookId == webhookId)
|
.Where(s => s.StoreId == storeId && s.WebhookId == webhookId)
|
||||||
.SelectMany(s => s.Webhook.Deliveries)
|
.SelectMany(s => s.Webhook.Deliveries)
|
||||||
.OrderByDescending(s => s.Timestamp)
|
.OrderByDescending(s => s.Timestamp);
|
||||||
.Take(count)
|
if (count is int c)
|
||||||
|
req = req.Take(c);
|
||||||
|
return await req
|
||||||
.ToArrayAsync();
|
.ToArrayAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -246,6 +248,8 @@ namespace BTCPayServer.Services.Stores
|
|||||||
using var ctx = _ContextFactory.CreateContext();
|
using var ctx = _ContextFactory.CreateContext();
|
||||||
WebhookData data = new WebhookData();
|
WebhookData data = new WebhookData();
|
||||||
data.Id = Encoders.Base58.EncodeData(RandomUtils.GetBytes(16));
|
data.Id = Encoders.Base58.EncodeData(RandomUtils.GetBytes(16));
|
||||||
|
if (string.IsNullOrEmpty(blob.Secret))
|
||||||
|
blob.Secret = Encoders.Base58.EncodeData(RandomUtils.GetBytes(16));
|
||||||
data.SetBlob(blob);
|
data.SetBlob(blob);
|
||||||
StoreWebhookData storeWebhook = new StoreWebhookData();
|
StoreWebhookData storeWebhook = new StoreWebhookData();
|
||||||
storeWebhook.StoreId = storeId;
|
storeWebhook.StoreId = storeId;
|
||||||
|
|||||||
@@ -53,7 +53,7 @@
|
|||||||
"securitySchemes": {
|
"securitySchemes": {
|
||||||
"API Key": {
|
"API Key": {
|
||||||
"type": "apiKey",
|
"type": "apiKey",
|
||||||
"description": "BTCPay Server supports authenticating and authorizing users through an API Key that is generated by them. Send the API Key as a header value to Authorization with the format: `token {token}`. For a smoother experience, you can generate a url that redirects users to an API key creation screen.\n\n The following permissions applies to the context of the user creating the API Key:\n * `unrestricted`: Allow unrestricted access to your account.\n * `btcpay.server.canmodifyserversettings`: Allow total control on the server settings. (only if user is administrator)\n * `btcpay.server.cancreateuser`: Allow the creation of new users on this server. (only if user is an administrator)\n * `btcpay.user.canviewprofile`: Allow view access to your user profile.\n * `btcpay.user.canmodifyprofile`: Allow view and modification access to your user profile.\n\nThe following permissions applies to all stores of the user, you can limit to a specific store with the following format: `btcpay.store.cancreateinvoice:6HSHAEU4iYWtjxtyRs9KyPjM9GAQp8kw2T9VWbGG1FnZ`:\n * `btcpay.store.canviewstoresettings`: Allow view access to the stores settings. \n * `btcpay.store.canmodifystoresettings`: Allow view and modification access to the stores settings.\n * `btcpay.store.cancreateinvoice`: Allow invoice creation of the store.\n\nNote that API Keys only limits permission of a user and can never expand it. If an API Key has the permission `btcpay.server.canmodifyserversettings` but that the user account creating this API Key is not administrator, the API Key will not be able to modify the server settings.\n",
|
"description": "BTCPay Server supports authenticating and authorizing users through an API Key that is generated by them. Send the API Key as a header value to Authorization with the format: `token {token}`. For a smoother experience, you can generate a url that redirects users to an API key creation screen.\n\n The following permissions applies to the context of the user creating the API Key:\n * `unrestricted`: Allow unrestricted access to your account.\n * `btcpay.server.canmodifyserversettings`: Allow total control on the server settings. (only if user is administrator)\n * `btcpay.server.cancreateuser`: Allow the creation of new users on this server. (only if user is an administrator)\n * `btcpay.user.canviewprofile`: Allow view access to your user profile.\n * `btcpay.user.canmodifyprofile`: Allow view and modification access to your user profile.\n\nThe following permissions applies to all stores of the user, you can limit to a specific store with the following format: `btcpay.store.cancreateinvoice:6HSHAEU4iYWtjxtyRs9KyPjM9GAQp8kw2T9VWbGG1FnZ`:\n * `btcpay.store.canviewstoresettings`: Allow view access to the stores settings. \n * `btcpay.store.webhooks.canmodifywebhooks`: Allow modifications of webhooks in the store. \n * `btcpay.store.canmodifystoresettings`: Allow view and modification access to the stores settings and webhooks.\n * `btcpay.store.cancreateinvoice`: Allow invoice creation of the store.\n\nNote that API Keys only limits permission of a user and can never expand it. If an API Key has the permission `btcpay.server.canmodifyserversettings` but that the user account creating this API Key is not administrator, the API Key will not be able to modify the server settings.\n",
|
||||||
"name": "Authorization",
|
"name": "Authorization",
|
||||||
"in": "header",
|
"in": "header",
|
||||||
"scheme": "token"
|
"scheme": "token"
|
||||||
|
|||||||
593
BTCPayServer/wwwroot/swagger/v1/swagger.template.webhooks.json
Normal file
593
BTCPayServer/wwwroot/swagger/v1/swagger.template.webhooks.json
Normal file
@@ -0,0 +1,593 @@
|
|||||||
|
{
|
||||||
|
"paths": {
|
||||||
|
"/api/v1/stores/{storeId}/webhooks": {
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "storeId",
|
||||||
|
"in": "path",
|
||||||
|
"required": true,
|
||||||
|
"description": "The store id",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"Webhooks"
|
||||||
|
],
|
||||||
|
"summary": "Get webhooks of a store",
|
||||||
|
"description": "View webhooks of a store",
|
||||||
|
"operationId": "Webhokks_GetWebhooks",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "List of webhooks",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/WebhookDataList"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"API Key": [
|
||||||
|
"btcpay.store.webhooks.canmodifywebhooks"
|
||||||
|
],
|
||||||
|
"Basic": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"post": {
|
||||||
|
"tags": [ "Webhooks" ],
|
||||||
|
"summary": "Create a new webhook",
|
||||||
|
"description": "Create a new webhook",
|
||||||
|
"requestBody": {
|
||||||
|
"x-name": "request",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/WebhookDataCreate"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": true,
|
||||||
|
"x-position": 1
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Information about the new webhook",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/WebhookDataCreate"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "A list of errors that occurred when creating the webhook",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ValidationProblemDetails"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"API Key": [
|
||||||
|
"btcpay.store.webhooks.canmodifywebhooks"
|
||||||
|
],
|
||||||
|
"Basic": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/v1/stores/{storeId}/webhooks/{webhookId}": {
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "storeId",
|
||||||
|
"in": "path",
|
||||||
|
"required": true,
|
||||||
|
"description": "The store id",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "webhookId",
|
||||||
|
"in": "path",
|
||||||
|
"required": true,
|
||||||
|
"description": "The webhook id",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"Webhooks"
|
||||||
|
],
|
||||||
|
"summary": "Get a webhook of a store",
|
||||||
|
"description": "View webhook of a store",
|
||||||
|
"operationId": "Webhokks_GetWebhook",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "A webhook",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/WebhookData"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "The webhook has not been found"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"API Key": [
|
||||||
|
"btcpay.store.webhooks.canmodifywebhooks"
|
||||||
|
],
|
||||||
|
"Basic": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"put": {
|
||||||
|
"tags": [ "Webhooks" ],
|
||||||
|
"summary": "Update a webhook",
|
||||||
|
"description": "Update a webhook",
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/WebhookDataBase"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": true,
|
||||||
|
"x-position": 1
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Information about the updated webhook",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/WebhookData"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "A list of errors that occurred when creating the webhook",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ValidationProblemDetails"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"API Key": [
|
||||||
|
"btcpay.store.webhooks.canmodifywebhooks"
|
||||||
|
],
|
||||||
|
"Basic": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"tags": [ "Webhooks" ],
|
||||||
|
"summary": "Delete a webhook",
|
||||||
|
"description": "Delete a webhook",
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/WebhookDataBase"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": true,
|
||||||
|
"x-position": 1
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "The webhook has been deleted"
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "The webhook does not exist"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"API Key": [
|
||||||
|
"btcpay.store.webhooks.canmodifywebhooks"
|
||||||
|
],
|
||||||
|
"Basic": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/v1/stores/{storeId}/webhooks/{webhookId}/deliveries": {
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "storeId",
|
||||||
|
"in": "path",
|
||||||
|
"required": true,
|
||||||
|
"description": "The store id",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "webhookId",
|
||||||
|
"in": "path",
|
||||||
|
"required": true,
|
||||||
|
"description": "The webhook id",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"Webhooks"
|
||||||
|
],
|
||||||
|
"summary": "Get latest deliveries",
|
||||||
|
"description": "List the latest deliveries to the webhook, ordered from the most recent",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "count",
|
||||||
|
"in": "query",
|
||||||
|
"description": "The number of latest deliveries to fetch",
|
||||||
|
"required": false,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "List of deliveries",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/WebhookDeliveryList"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"API Key": [
|
||||||
|
"btcpay.store.webhooks.canmodifywebhooks"
|
||||||
|
],
|
||||||
|
"Basic": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/v1/stores/{storeId}/webhooks/{webhookId}/deliveries/{deliveryId}": {
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "storeId",
|
||||||
|
"in": "path",
|
||||||
|
"required": true,
|
||||||
|
"description": "The store id",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "webhookId",
|
||||||
|
"in": "path",
|
||||||
|
"required": true,
|
||||||
|
"description": "The webhook id",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "deliveryId",
|
||||||
|
"in": "path",
|
||||||
|
"required": true,
|
||||||
|
"description": "The id of the delivery",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"Webhooks"
|
||||||
|
],
|
||||||
|
"summary": "Get a webhook delivery",
|
||||||
|
"description": "Information about a webhook delivery",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Information about a delivery",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/WebhookDeliveryData"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "The delivery does not exists."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"API Key": [
|
||||||
|
"btcpay.store.webhooks.canmodifywebhooks"
|
||||||
|
],
|
||||||
|
"Basic": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/v1/stores/{storeId}/webhooks/{webhookId}/deliveries/{deliveryId}/request": {
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "storeId",
|
||||||
|
"in": "path",
|
||||||
|
"required": true,
|
||||||
|
"description": "The store id",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "webhookId",
|
||||||
|
"in": "path",
|
||||||
|
"required": true,
|
||||||
|
"description": "The webhook id",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "deliveryId",
|
||||||
|
"in": "path",
|
||||||
|
"required": true,
|
||||||
|
"description": "The id of the delivery",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"Webhooks"
|
||||||
|
],
|
||||||
|
"summary": "Get the delivery's request",
|
||||||
|
"description": "The delivery's JSON request sent to the endpoint",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "The delivery's JSON Request",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "The delivery does not exists."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"API Key": [
|
||||||
|
"btcpay.store.webhooks.canmodifywebhooks"
|
||||||
|
],
|
||||||
|
"Basic": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/v1/stores/{storeId}/webhooks/{webhookId}/deliveries/{deliveryId}/redeliver": {
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "storeId",
|
||||||
|
"in": "path",
|
||||||
|
"required": true,
|
||||||
|
"description": "The store id",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "webhookId",
|
||||||
|
"in": "path",
|
||||||
|
"required": true,
|
||||||
|
"description": "The webhook id",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "deliveryId",
|
||||||
|
"in": "path",
|
||||||
|
"required": true,
|
||||||
|
"description": "The id of the delivery",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"post": {
|
||||||
|
"tags": [
|
||||||
|
"Webhooks"
|
||||||
|
],
|
||||||
|
"summary": "Redeliver the delivery",
|
||||||
|
"description": "Redeliver the delivery",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "The new delivery id being broadcasted. (Broadcast happen asynchronously with this call)",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "The delivery does not exists."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"API Key": [
|
||||||
|
"btcpay.store.webhooks.canmodifywebhooks"
|
||||||
|
],
|
||||||
|
"Basic": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"components": {
|
||||||
|
"schemas": {
|
||||||
|
"WebhookDeliveryList": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/WebhookDeliveryData"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"WebhookDeliveryData": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The id of the delivery",
|
||||||
|
"nullable": false
|
||||||
|
},
|
||||||
|
"timestamp": {
|
||||||
|
"type": "number",
|
||||||
|
"format": "int64",
|
||||||
|
"nullable": false,
|
||||||
|
"description": "Timestamp of when the delivery got broadcasted"
|
||||||
|
},
|
||||||
|
"httpCode": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "HTTP code received by the remote service, if any.",
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
|
"errorMessage": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "User friendly error message, if any."
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Whether the delivery failed or not (possible values are: `Failed`, `HttpError`, `HttpSuccess`)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"WebhookDataList": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/WebhookData"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"WebhookData": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/WebhookDataBase"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The id of the webhook",
|
||||||
|
"nullable": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"WebhookDataCreate": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/WebhookData"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"secret": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Must be used by the callback receiver to ensure the delivery comes from BTCPay Server. BTCPay Server includes the `BTCPay-Sig` HTTP header, whose format is `sha256=[HMAC256(secret,bodyBytes)]`. The pattern to authenticate the webhook is similar to [how to secure webhooks in Github](https://developer.github.com/webhooks/securing/) but with sha256 instead of sha1.",
|
||||||
|
"nullable": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"WebhookDataBase": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The id of the webhook",
|
||||||
|
"nullable": false
|
||||||
|
},
|
||||||
|
"enabled": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Whether this webhook is enabled or not",
|
||||||
|
"nullable": false
|
||||||
|
},
|
||||||
|
"automaticRedelivery": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "If true, BTCPay Server will retry to redeliver any failed delivery after 10 seconds, 1 minutes and up to 6 times after 10 minutes.",
|
||||||
|
"nullable": false
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The endpoint where BTCPay Server will make the POST request with the webhook body",
|
||||||
|
"nullable": false
|
||||||
|
},
|
||||||
|
"authorizedEvents": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Which event should be received by this endpoint",
|
||||||
|
"properties": {
|
||||||
|
"everything": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "If true, the endpoint will receive all events related to the store.",
|
||||||
|
"nullable": false
|
||||||
|
},
|
||||||
|
"specificEvents": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "If `everything` is false, the specific events that the endpoint is interested in. Current events are: `InvoiceCreated`, `InvoiceReceivedPayment`, `InvoicePaidInFull`, `InvoiceExpired`, `InvoiceConfirmed`, `InvoiceInvalid`.",
|
||||||
|
"nullable": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
{
|
||||||
|
"name": "Webhooks"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user