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.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
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)
|
||||
{
|
||||
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,
|
||||
|
||||
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 CanModifyServerSettings = "btcpay.server.canmodifyserversettings";
|
||||
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 CanViewStoreSettings = "btcpay.store.canviewstoresettings";
|
||||
public const string CanViewInvoices = "btcpay.store.canviewinvoices";
|
||||
@@ -29,6 +30,7 @@ namespace BTCPayServer.Client
|
||||
{
|
||||
yield return CanViewInvoices;
|
||||
yield return CanCreateInvoice;
|
||||
yield return CanModifyStoreWebhooks;
|
||||
yield return CanModifyServerSettings;
|
||||
yield return CanModifyStoreSettings;
|
||||
yield return CanViewStoreSettings;
|
||||
@@ -156,6 +158,7 @@ namespace BTCPayServer.Client
|
||||
switch (subpolicy)
|
||||
{
|
||||
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.CanViewStoreSettings 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(
|
||||
new CreateApplicationUserRequest() { Email = "test@gmail.com", Password = "abceudhqw" });
|
||||
Assert.Empty(user1.Roles);
|
||||
|
||||
|
||||
// We have no admin, so it should work
|
||||
var user2 = await unauthClient.CreateUser(
|
||||
new CreateApplicationUserRequest() { Email = "test2@gmail.com", Password = "abceudhqw" });
|
||||
Assert.Empty(user2.Roles);
|
||||
|
||||
|
||||
// Duplicate email
|
||||
await AssertValidationError(new[] { "Email" },
|
||||
async () => await unauthClient.CreateUser(
|
||||
@@ -170,7 +170,7 @@ namespace BTCPayServer.Tests
|
||||
Assert.Contains("ServerAdmin", admin.Roles);
|
||||
Assert.NotNull(admin.Created);
|
||||
Assert.True((DateTimeOffset.Now - admin.Created).Value.Seconds < 10);
|
||||
|
||||
|
||||
// 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
|
||||
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)]
|
||||
[Trait("Integration", "Integration")]
|
||||
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("Lightning", "Lightning")]
|
||||
public async Task CanUseLightningAPI()
|
||||
@@ -907,7 +999,7 @@ namespace BTCPayServer.Tests
|
||||
var info = await client.GetLightningNodeInfo("BTC");
|
||||
Assert.Single(info.NodeURIs);
|
||||
Assert.NotEqual(0, info.BlockHeight);
|
||||
|
||||
|
||||
var err = await Assert.ThrowsAsync<HttpRequestException>(async () => await client.GetLightningNodeChannels("BTC"));
|
||||
Assert.Contains("503", err.Message);
|
||||
// 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");
|
||||
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")).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")).SendKeys("HelloWorld");
|
||||
s.Driver.FindElement(By.Name("update")).Click();
|
||||
@@ -663,26 +663,26 @@ namespace BTCPayServer.Tests
|
||||
|
||||
s.Driver.FindElement(By.Name("update")).Click();
|
||||
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");
|
||||
s.GoToStore(store.storeId);
|
||||
s.AddDerivationScheme();
|
||||
s.CreateInvoice(store.storeName);
|
||||
var request = await server.GetNextRequest();
|
||||
var headers = request.HttpContext.Request.Headers;
|
||||
var headers = request.Request.Headers;
|
||||
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))}";
|
||||
Assert.Equal(expectedSig, actualSig);
|
||||
request.HttpContext.Response.StatusCode = 200;
|
||||
request.Complete();
|
||||
request.Response.StatusCode = 200;
|
||||
server.Done();
|
||||
|
||||
Logs.Tester.LogInformation("Let's make a failed event");
|
||||
s.CreateInvoice(store.storeName);
|
||||
request = await server.GetNextRequest();
|
||||
request.HttpContext.Response.StatusCode = 404;
|
||||
request.Complete();
|
||||
request.Response.StatusCode = 404;
|
||||
server.Done();
|
||||
|
||||
// The delivery is done asynchronously, so small wait here
|
||||
await Task.Delay(500);
|
||||
@@ -695,8 +695,8 @@ namespace BTCPayServer.Tests
|
||||
elements[0].Click();
|
||||
s.AssertHappyMessage();
|
||||
request = await server.GetNextRequest();
|
||||
request.HttpContext.Response.StatusCode = 404;
|
||||
request.Complete();
|
||||
request.Response.StatusCode = 404;
|
||||
server.Done();
|
||||
|
||||
Logs.Tester.LogInformation("Can we browse the json content?");
|
||||
CanBrowseContent(s);
|
||||
@@ -708,8 +708,8 @@ namespace BTCPayServer.Tests
|
||||
element.Click();
|
||||
s.AssertHappyMessage();
|
||||
request = await server.GetNextRequest();
|
||||
request.HttpContext.Response.StatusCode = 404;
|
||||
request.Complete();
|
||||
request.Response.StatusCode = 404;
|
||||
server.Done();
|
||||
|
||||
Logs.Tester.LogInformation("Let's see if we can delete store with some webhooks inside");
|
||||
s.GoToStore(store.storeId);
|
||||
|
||||
@@ -251,5 +251,5 @@
|
||||
<_ContentIncludedByDefault Remove="Views\Components\NotificationsDropdown\Default.cshtml" />
|
||||
</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>
|
||||
|
||||
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.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.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 the selected stores' settings.")},
|
||||
{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
|
||||
{
|
||||
public WebhookEvent WebhookEvent;
|
||||
public WebhookDeliveryData Delivery;
|
||||
public Data.WebhookDeliveryData Delivery;
|
||||
public WebhookBlob WebhookBlob;
|
||||
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;
|
||||
WebhookEvent = webhookEvent;
|
||||
@@ -130,7 +130,7 @@ namespace BTCPayServer.HostedServices
|
||||
continue;
|
||||
if (!ShouldDeliver(webhookEventType, webhookBlob))
|
||||
continue;
|
||||
WebhookDeliveryData delivery = NewDelivery();
|
||||
Data.WebhookDeliveryData delivery = NewDelivery();
|
||||
delivery.WebhookId = webhook.Id;
|
||||
var webhookEvent = new WebhookInvoiceEvent();
|
||||
webhookEvent.InvoiceId = invoiceEvent.InvoiceId;
|
||||
@@ -293,9 +293,9 @@ namespace BTCPayServer.HostedServices
|
||||
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.Timestamp = DateTimeOffset.UtcNow;
|
||||
return delivery;
|
||||
|
||||
@@ -15,7 +15,7 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||
{
|
||||
|
||||
}
|
||||
public DeliveryViewModel(WebhookDeliveryData s)
|
||||
public DeliveryViewModel(Data.WebhookDeliveryData s)
|
||||
{
|
||||
var blob = s.GetBlob();
|
||||
Id = s.Id;
|
||||
|
||||
@@ -19,6 +19,9 @@ namespace BTCPayServer.Payments.PayJoin
|
||||
services.AddHttpClient(PayjoinClient.PayjoinOnionNamedClient)
|
||||
.ConfigureHttpClient(h => h.DefaultRequestHeaders.ConnectionClose = true)
|
||||
.ConfigurePrimaryHttpMessageHandler<Socks5HttpClientHandler>();
|
||||
services.AddHttpClient(WebhookNotificationManager.OnionNamedClient)
|
||||
.ConfigureHttpClient(h => h.DefaultRequestHeaders.ConnectionClose = true)
|
||||
.ConfigurePrimaryHttpMessageHandler<Socks5HttpClientHandler>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ retry:
|
||||
_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();
|
||||
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();
|
||||
return await ctx.InvoiceWebhookDeliveries
|
||||
|
||||
@@ -230,14 +230,16 @@ namespace BTCPayServer.Services.Stores
|
||||
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();
|
||||
return await ctx.StoreWebhooks
|
||||
IQueryable<WebhookDeliveryData> req = ctx.StoreWebhooks
|
||||
.Where(s => s.StoreId == storeId && s.WebhookId == webhookId)
|
||||
.SelectMany(s => s.Webhook.Deliveries)
|
||||
.OrderByDescending(s => s.Timestamp)
|
||||
.Take(count)
|
||||
.OrderByDescending(s => s.Timestamp);
|
||||
if (count is int c)
|
||||
req = req.Take(c);
|
||||
return await req
|
||||
.ToArrayAsync();
|
||||
}
|
||||
|
||||
@@ -246,6 +248,8 @@ namespace BTCPayServer.Services.Stores
|
||||
using var ctx = _ContextFactory.CreateContext();
|
||||
WebhookData data = new WebhookData();
|
||||
data.Id = Encoders.Base58.EncodeData(RandomUtils.GetBytes(16));
|
||||
if (string.IsNullOrEmpty(blob.Secret))
|
||||
blob.Secret = Encoders.Base58.EncodeData(RandomUtils.GetBytes(16));
|
||||
data.SetBlob(blob);
|
||||
StoreWebhookData storeWebhook = new StoreWebhookData();
|
||||
storeWebhook.StoreId = storeId;
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
"securitySchemes": {
|
||||
"API Key": {
|
||||
"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",
|
||||
"in": "header",
|
||||
"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