From 5b3b96b372ef3c2e9537fab27eb069ff29bb713c Mon Sep 17 00:00:00 2001 From: Andrew Camilleri Date: Tue, 19 May 2020 19:59:23 +0200 Subject: [PATCH] GreenField: Payment Requests CRUD (#1430) * GreenField: Payment Requests CRUD * fixes * fix swagger * fix swag * rebase fixes * Add new permissions for payment requests * Adapt PR to archive * fix tst * add to contains policxy * make decimals returned as string due to avoid shitty language parsing issues * do not register decimal json converter as global * fix cultureinfo for json covnerter * pr changes * add json convertet test * fix json test * fix rebase --- .../BTCPayServerClient.PaymentRequests.cs | 59 +++ .../DecimalStringJsonConverter.cs | 39 ++ .../Models/CreatePaymentRequestRequest.cs | 6 + .../Models/CreateStoreRequest.cs | 2 +- .../Models/PaymentRequestBaseData.cs | 21 + .../Models/PaymentRequestData.cs | 19 + .../Models/UpdatePaymentRequestRequest.cs | 6 + BTCPayServer.Client/Permissions.cs | 24 +- BTCPayServer.Data/BTCPayServer.Data.csproj | 3 + BTCPayServer.Data/Data/PaymentRequestData.cs | 29 +- BTCPayServer.Tests/GreenfieldAPITests.cs | 279 +++++++++---- .../GreenField/PaymentRequestsController.cs | 164 ++++++++ .../Controllers/ManageController.APIKeys.cs | 6 +- .../Data/PaymentRequestDataExtensions.cs | 12 +- BTCPayServer/Hosting/Startup.cs | 1 + .../ListPaymentRequestsViewModel.cs | 8 +- .../PaymentRequest/PaymentRequestHub.cs | 6 +- .../PaymentRequest/PaymentRequestService.cs | 6 +- .../GreenFieldAuthorizationHandler.cs | 2 + .../PaymentRequestRepository.cs | 18 +- .../v1/swagger.template.payment-requests.json | 370 ++++++++++++++++++ 21 files changed, 953 insertions(+), 127 deletions(-) create mode 100644 BTCPayServer.Client/BTCPayServerClient.PaymentRequests.cs create mode 100644 BTCPayServer.Client/JsonConverters/DecimalStringJsonConverter.cs create mode 100644 BTCPayServer.Client/Models/CreatePaymentRequestRequest.cs create mode 100644 BTCPayServer.Client/Models/PaymentRequestBaseData.cs create mode 100644 BTCPayServer.Client/Models/PaymentRequestData.cs create mode 100644 BTCPayServer.Client/Models/UpdatePaymentRequestRequest.cs create mode 100644 BTCPayServer/Controllers/GreenField/PaymentRequestsController.cs create mode 100644 BTCPayServer/wwwroot/swagger/v1/swagger.template.payment-requests.json diff --git a/BTCPayServer.Client/BTCPayServerClient.PaymentRequests.cs b/BTCPayServer.Client/BTCPayServerClient.PaymentRequests.cs new file mode 100644 index 000000000..a54edcb12 --- /dev/null +++ b/BTCPayServer.Client/BTCPayServerClient.PaymentRequests.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using BTCPayServer.Client.Models; + +namespace BTCPayServer.Client +{ + public partial class BTCPayServerClient + { + public virtual async Task> GetPaymentRequests(string storeId, + CancellationToken token = default) + { + var response = + await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{storeId}/payment-requests"), token); + return await HandleResponse>(response); + } + + public virtual async Task GetPaymentRequest(string storeId, string paymentRequestId, + CancellationToken token = default) + { + var response = await _httpClient.SendAsync( + CreateHttpRequest($"api/v1/stores/{storeId}/payment-requests/{paymentRequestId}"), token); + return await HandleResponse(response); + } + + public virtual async Task ArchivePaymentRequest(string storeId, string paymentRequestId, + CancellationToken token = default) + { + var response = await _httpClient.SendAsync( + CreateHttpRequest($"api/v1/stores/{storeId}/payment-requests/{paymentRequestId}", + method: HttpMethod.Delete), token); + HandleResponse(response); + } + + public virtual async Task CreatePaymentRequest(string storeId, + CreatePaymentRequestRequest request, CancellationToken token = default) + { + if (request == null) + throw new ArgumentNullException(nameof(request)); + var response = await _httpClient.SendAsync( + CreateHttpRequest($"api/v1/stores/{storeId}/payment-requests", bodyPayload: request, + method: HttpMethod.Post), token); + return await HandleResponse(response); + } + + public virtual async Task UpdatePaymentRequest(string storeId, string paymentRequestId, + UpdatePaymentRequestRequest request, CancellationToken token = default) + { + if (request == null) + throw new ArgumentNullException(nameof(request)); + var response = await _httpClient.SendAsync( + CreateHttpRequest($"api/v1/stores/{storeId}/payment-requests/{paymentRequestId}", bodyPayload: request, + method: HttpMethod.Put), token); + return await HandleResponse(response); + } + } +} diff --git a/BTCPayServer.Client/JsonConverters/DecimalStringJsonConverter.cs b/BTCPayServer.Client/JsonConverters/DecimalStringJsonConverter.cs new file mode 100644 index 000000000..2e29590d8 --- /dev/null +++ b/BTCPayServer.Client/JsonConverters/DecimalStringJsonConverter.cs @@ -0,0 +1,39 @@ +using System; +using System.Globalization; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace BTCPayServer.JsonConverters +{ + public class DecimalStringJsonConverter : JsonConverter + { + public override bool CanConvert(Type objectType) + { + return (objectType == typeof(decimal) || objectType == typeof(decimal?)); + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, + JsonSerializer serializer) + { + JToken token = JToken.Load(reader); + switch (token.Type) + { + case JTokenType.Float: + case JTokenType.Integer: + case JTokenType.String: + return decimal.Parse(token.ToString(), CultureInfo.InvariantCulture); + case JTokenType.Null when objectType == typeof(decimal?): + return null; + default: + throw new JsonSerializationException("Unexpected token type: " + + token.Type); + } + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + if (value != null) + writer.WriteValue(((decimal)value).ToString(CultureInfo.InvariantCulture)); + } + } +} diff --git a/BTCPayServer.Client/Models/CreatePaymentRequestRequest.cs b/BTCPayServer.Client/Models/CreatePaymentRequestRequest.cs new file mode 100644 index 000000000..3e8adce7d --- /dev/null +++ b/BTCPayServer.Client/Models/CreatePaymentRequestRequest.cs @@ -0,0 +1,6 @@ +namespace BTCPayServer.Client.Models +{ + public class CreatePaymentRequestRequest : PaymentRequestBaseData + { + } +} \ No newline at end of file diff --git a/BTCPayServer.Client/Models/CreateStoreRequest.cs b/BTCPayServer.Client/Models/CreateStoreRequest.cs index cbba444f8..eac719c5f 100644 --- a/BTCPayServer.Client/Models/CreateStoreRequest.cs +++ b/BTCPayServer.Client/Models/CreateStoreRequest.cs @@ -1,6 +1,6 @@ namespace BTCPayServer.Client.Models { - public class CreateStoreRequest : StoreBaseData + public class CreateStoreRequest: StoreBaseData { } } diff --git a/BTCPayServer.Client/Models/PaymentRequestBaseData.cs b/BTCPayServer.Client/Models/PaymentRequestBaseData.cs new file mode 100644 index 000000000..ec4bd8436 --- /dev/null +++ b/BTCPayServer.Client/Models/PaymentRequestBaseData.cs @@ -0,0 +1,21 @@ +using System; +using BTCPayServer.JsonConverters; +using Newtonsoft.Json; + +namespace BTCPayServer.Client.Models +{ + public class PaymentRequestBaseData + { + [JsonProperty(ItemConverterType = typeof(DecimalStringJsonConverter))] + public decimal Amount { get; set; } + public string Currency { get; set; } + public DateTime? ExpiryDate { get; set; } + public string Title { get; set; } + public string Description { get; set; } + public string Email { get; set; } + + public string EmbeddedCSS { get; set; } + public string CustomCSSLink { get; set; } + public bool AllowCustomPaymentAmounts { get; set; } + } +} diff --git a/BTCPayServer.Client/Models/PaymentRequestData.cs b/BTCPayServer.Client/Models/PaymentRequestData.cs new file mode 100644 index 000000000..3fc0f655d --- /dev/null +++ b/BTCPayServer.Client/Models/PaymentRequestData.cs @@ -0,0 +1,19 @@ +using System; + +namespace BTCPayServer.Client.Models +{ + public class PaymentRequestData : PaymentRequestBaseData + { + public PaymentRequestData.PaymentRequestStatus Status { get; set; } + public DateTimeOffset Created { get; set; } + public string Id { get; set; } + public bool Archived { get; set; } + + public enum PaymentRequestStatus + { + Pending = 0, + Completed = 1, + Expired = 2 + } + } +} \ No newline at end of file diff --git a/BTCPayServer.Client/Models/UpdatePaymentRequestRequest.cs b/BTCPayServer.Client/Models/UpdatePaymentRequestRequest.cs new file mode 100644 index 000000000..76f16118c --- /dev/null +++ b/BTCPayServer.Client/Models/UpdatePaymentRequestRequest.cs @@ -0,0 +1,6 @@ +namespace BTCPayServer.Client.Models +{ + public class UpdatePaymentRequestRequest : PaymentRequestBaseData + { + } +} \ No newline at end of file diff --git a/BTCPayServer.Client/Permissions.cs b/BTCPayServer.Client/Permissions.cs index 8f4ce19cc..2b937cd85 100644 --- a/BTCPayServer.Client/Permissions.cs +++ b/BTCPayServer.Client/Permissions.cs @@ -10,6 +10,8 @@ namespace BTCPayServer.Client public const string CanModifyStoreSettings = "btcpay.store.canmodifystoresettings"; public const string CanViewStoreSettings = "btcpay.store.canviewstoresettings"; public const string CanCreateInvoice = "btcpay.store.cancreateinvoice"; + public const string CanViewPaymentRequests = "btcpay.store.canviewpaymentrequests"; + public const string CanModifyPaymentRequests = "btcpay.store.canmodifypaymentrequests"; public const string CanModifyProfile = "btcpay.user.canmodifyprofile"; public const string CanViewProfile = "btcpay.user.canviewprofile"; public const string CanCreateUser = "btcpay.server.cancreateuser"; @@ -22,6 +24,8 @@ namespace BTCPayServer.Client yield return CanModifyServerSettings; yield return CanModifyStoreSettings; yield return CanViewStoreSettings; + yield return CanViewPaymentRequests; + yield return CanModifyPaymentRequests; yield return CanModifyProfile; yield return CanViewProfile; yield return CanCreateUser; @@ -135,13 +139,19 @@ namespace BTCPayServer.Client return true; if (this.Policy == subpolicy) return true; - if (subpolicy == Policies.CanViewStoreSettings && this.Policy == Policies.CanModifyStoreSettings) - return true; - if (subpolicy == Policies.CanCreateInvoice && this.Policy == Policies.CanModifyStoreSettings) - return true; - if (subpolicy == Policies.CanViewProfile && this.Policy == Policies.CanModifyProfile) - return true; - return false; + switch (subpolicy) + { + case Policies.CanViewStoreSettings when this.Policy == Policies.CanModifyStoreSettings: + case Policies.CanCreateInvoice when this.Policy == Policies.CanModifyStoreSettings: + case Policies.CanViewProfile when this.Policy == Policies.CanModifyProfile: + case Policies.CanModifyPaymentRequests when this.Policy == Policies.CanModifyStoreSettings: + case Policies.CanViewPaymentRequests when this.Policy == Policies.CanModifyStoreSettings: + case Policies.CanViewPaymentRequests when this.Policy == Policies.CanViewStoreSettings: + case Policies.CanViewPaymentRequests when this.Policy == Policies.CanModifyPaymentRequests: + return true; + default: + return false; + } } public string StoreId { get; } diff --git a/BTCPayServer.Data/BTCPayServer.Data.csproj b/BTCPayServer.Data/BTCPayServer.Data.csproj index 00bfa3e97..f73a1d6da 100644 --- a/BTCPayServer.Data/BTCPayServer.Data.csproj +++ b/BTCPayServer.Data/BTCPayServer.Data.csproj @@ -9,4 +9,7 @@ + + + diff --git a/BTCPayServer.Data/Data/PaymentRequestData.cs b/BTCPayServer.Data/Data/PaymentRequestData.cs index 78380f678..ca3513eb2 100644 --- a/BTCPayServer.Data/Data/PaymentRequestData.cs +++ b/BTCPayServer.Data/Data/PaymentRequestData.cs @@ -1,6 +1,7 @@ -using System; +using System; using System.Collections.Generic; using System.Text; +using BTCPayServer.Client.Models; namespace BTCPayServer.Data { @@ -16,31 +17,9 @@ namespace BTCPayServer.Data public StoreData StoreData { get; set; } - public PaymentRequestStatus Status { get; set; } + public Client.Models.PaymentRequestData.PaymentRequestStatus Status { get; set; } public byte[] Blob { get; set; } - - public class PaymentRequestBlob - { - public decimal Amount { get; set; } - public string Currency { get; set; } - - public DateTime? ExpiryDate { get; set; } - - public string Title { get; set; } - public string Description { get; set; } - public string Email { get; set; } - - public string EmbeddedCSS { get; set; } - public string CustomCSSLink { get; set; } - public bool AllowCustomPaymentAmounts { get; set; } - } - - public enum PaymentRequestStatus - { - Pending = 0, - Completed = 1, - Expired = 2 - } + } } diff --git a/BTCPayServer.Tests/GreenfieldAPITests.cs b/BTCPayServer.Tests/GreenfieldAPITests.cs index 511cbafd1..62da14a8a 100644 --- a/BTCPayServer.Tests/GreenfieldAPITests.cs +++ b/BTCPayServer.Tests/GreenfieldAPITests.cs @@ -1,14 +1,19 @@ using System; +using System.IO; using System.Linq; using System.Net.Http; using System.Threading.Tasks; using BTCPayServer.Client; using BTCPayServer.Client.Models; +using BTCPayServer.JsonConverters; using BTCPayServer.Services; using BTCPayServer.Tests.Logging; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using Xunit; using Xunit.Abstractions; using CreateApplicationUserRequest = BTCPayServer.Client.Models.CreateApplicationUserRequest; +using JsonReader = Newtonsoft.Json.JsonReader; namespace BTCPayServer.Tests { @@ -20,7 +25,7 @@ namespace BTCPayServer.Tests public GreenfieldAPITests(ITestOutputHelper helper) { - Logs.Tester = new XUnitLog(helper) { Name = "Tests" }; + Logs.Tester = new XUnitLog(helper) {Name = "Tests"}; Logs.LogProvider = new XUnitLogProvider(helper); } @@ -41,10 +46,10 @@ namespace BTCPayServer.Tests Assert.NotNull(apiKeyData); Assert.Equal(client.APIKey, apiKeyData.ApiKey); Assert.Single(apiKeyData.Permissions); - + //a client using Basic Auth has no business here await AssertHttpError(401, async () => await clientBasic.GetCurrentAPIKeyInfo()); - + //revoke current api key await client.RevokeCurrentAPIKeyInfo(); await AssertHttpError(401, async () => await client.GetCurrentAPIKeyInfo()); @@ -52,6 +57,7 @@ namespace BTCPayServer.Tests await AssertHttpError(401, async () => await clientBasic.RevokeCurrentAPIKeyInfo()); } } + [Fact(Timeout = TestTimeout)] [Trait("Integration", "Integration")] public async Task CanCreateAndDeleteAPIKeyViaAPI() @@ -65,18 +71,19 @@ namespace BTCPayServer.Tests var apiKey = await unrestricted.CreateAPIKey(new CreateApiKeyRequest() { Label = "Hello world", - Permissions = new Permission[] { Permission.Create(Policies.CanViewProfile) } + Permissions = new Permission[] {Permission.Create(Policies.CanViewProfile)} }); Assert.Equal("Hello world", apiKey.Label); var p = Assert.Single(apiKey.Permissions); Assert.Equal(Policies.CanViewProfile, p.Policy); var restricted = acc.CreateClientFromAPIKey(apiKey.ApiKey); - await AssertHttpError(403, async () => await restricted.CreateAPIKey(new CreateApiKeyRequest() - { - Label = "Hello world2", - Permissions = new Permission[] { Permission.Create(Policies.CanViewProfile) } - })); + await AssertHttpError(403, + async () => await restricted.CreateAPIKey(new CreateApiKeyRequest() + { + Label = "Hello world2", + Permissions = new Permission[] {Permission.Create(Policies.CanViewProfile)} + })); await unrestricted.RevokeAPIKey(apiKey.ApiKey); await AssertHttpError(404, async () => await unrestricted.RevokeAPIKey(apiKey.ApiKey)); @@ -92,35 +99,54 @@ namespace BTCPayServer.Tests tester.PayTester.DisableRegistration = true; await tester.StartAsync(); var unauthClient = new BTCPayServerClient(tester.PayTester.ServerUri); - await AssertHttpError(400, async () => await unauthClient.CreateUser(new CreateApplicationUserRequest())); - await AssertHttpError(400, async () => await unauthClient.CreateUser(new CreateApplicationUserRequest() { Email = "test@gmail.com" })); + await AssertHttpError(400, + async () => await unauthClient.CreateUser(new CreateApplicationUserRequest())); + await AssertHttpError(400, + async () => await unauthClient.CreateUser( + new CreateApplicationUserRequest() {Email = "test@gmail.com"})); // Pass too simple - await AssertHttpError(400, async () => await unauthClient.CreateUser(new CreateApplicationUserRequest() { Email = "test3@gmail.com", Password = "a" })); + await AssertHttpError(400, + async () => await unauthClient.CreateUser( + new CreateApplicationUserRequest() {Email = "test3@gmail.com", Password = "a"})); // We have no admin, so it should work - var user1 = await unauthClient.CreateUser(new CreateApplicationUserRequest() { Email = "test@gmail.com", Password = "abceudhqw" }); + var user1 = await unauthClient.CreateUser( + new CreateApplicationUserRequest() {Email = "test@gmail.com", Password = "abceudhqw"}); // We have no admin, so it should work - var user2 = await unauthClient.CreateUser(new CreateApplicationUserRequest() { Email = "test2@gmail.com", Password = "abceudhqw" }); + var user2 = await unauthClient.CreateUser( + new CreateApplicationUserRequest() {Email = "test2@gmail.com", Password = "abceudhqw"}); // Duplicate email - await AssertHttpError(400, async () => await unauthClient.CreateUser(new CreateApplicationUserRequest() { Email = "test2@gmail.com", Password = "abceudhqw" })); + await AssertHttpError(400, + async () => await unauthClient.CreateUser( + new CreateApplicationUserRequest() {Email = "test2@gmail.com", Password = "abceudhqw"})); // Let's make an admin - var admin = await unauthClient.CreateUser(new CreateApplicationUserRequest() { Email = "admin@gmail.com", Password = "abceudhqw", IsAdministrator = true }); + var admin = await unauthClient.CreateUser(new CreateApplicationUserRequest() + { + Email = "admin@gmail.com", Password = "abceudhqw", IsAdministrator = true + }); // 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, async () => await unauthClient.CreateUser(new CreateApplicationUserRequest() { Email = "test3@gmail.com", Password = "afewfoiewiou" })); + await AssertHttpError(401, + async () => await unauthClient.CreateUser( + new CreateApplicationUserRequest() {Email = "test3@gmail.com", Password = "afewfoiewiou"})); // But should be ok with subscriptions unlocked var settings = tester.PayTester.GetService(); - await settings.UpdateSetting(new PoliciesSettings() { LockSubscription = false }); - await unauthClient.CreateUser(new CreateApplicationUserRequest() { Email = "test3@gmail.com", Password = "afewfoiewiou" }); + await settings.UpdateSetting(new PoliciesSettings() {LockSubscription = false}); + await unauthClient.CreateUser( + new CreateApplicationUserRequest() {Email = "test3@gmail.com", Password = "afewfoiewiou"}); // But it should be forbidden to create an admin without being authenticated - await AssertHttpError(403, async () => await unauthClient.CreateUser(new CreateApplicationUserRequest() { Email = "admin2@gmail.com", Password = "afewfoiewiou", IsAdministrator = true })); - await settings.UpdateSetting(new PoliciesSettings() { LockSubscription = true }); + await AssertHttpError(403, + async () => await unauthClient.CreateUser(new CreateApplicationUserRequest() + { + Email = "admin2@gmail.com", Password = "afewfoiewiou", IsAdministrator = true + })); + await settings.UpdateSetting(new PoliciesSettings() {LockSubscription = true}); var adminAcc = tester.NewAccount(); adminAcc.UserId = admin.Id; @@ -128,32 +154,49 @@ namespace BTCPayServer.Tests var adminClient = await adminAcc.CreateClient(Policies.CanModifyProfile); // We should be forbidden to create a new user without proper admin permissions - await AssertHttpError(403, async () => await adminClient.CreateUser(new CreateApplicationUserRequest() { Email = "test4@gmail.com", Password = "afewfoiewiou" })); - await AssertHttpError(403, async () => await adminClient.CreateUser(new CreateApplicationUserRequest() { Email = "test4@gmail.com", Password = "afewfoiewiou", IsAdministrator = true })); + await AssertHttpError(403, + async () => await adminClient.CreateUser( + new CreateApplicationUserRequest() {Email = "test4@gmail.com", Password = "afewfoiewiou"})); + await AssertHttpError(403, + async () => await adminClient.CreateUser(new CreateApplicationUserRequest() + { + Email = "test4@gmail.com", Password = "afewfoiewiou", IsAdministrator = true + })); // However, should be ok with the unrestricted permissions of an admin adminClient = await adminAcc.CreateClient(Policies.Unrestricted); - await adminClient.CreateUser(new CreateApplicationUserRequest() { Email = "test4@gmail.com", Password = "afewfoiewiou" }); + await adminClient.CreateUser( + new CreateApplicationUserRequest() {Email = "test4@gmail.com", Password = "afewfoiewiou"}); // Even creating new admin should be ok - await adminClient.CreateUser(new CreateApplicationUserRequest() { Email = "admin4@gmail.com", Password = "afewfoiewiou", IsAdministrator = true }); + await adminClient.CreateUser(new CreateApplicationUserRequest() + { + Email = "admin4@gmail.com", Password = "afewfoiewiou", IsAdministrator = true + }); var user1Acc = tester.NewAccount(); user1Acc.UserId = user1.Id; user1Acc.IsAdmin = false; var user1Client = await user1Acc.CreateClient(Policies.CanModifyServerSettings); - + // User1 trying to get server management would still fail to create user - await AssertHttpError(403, async () => await user1Client.CreateUser(new CreateApplicationUserRequest() { Email = "test8@gmail.com", Password = "afewfoiewiou" })); + await AssertHttpError(403, + async () => await user1Client.CreateUser( + new CreateApplicationUserRequest() {Email = "test8@gmail.com", Password = "afewfoiewiou"})); // User1 should be able to create user if subscription unlocked - await settings.UpdateSetting(new PoliciesSettings() { LockSubscription = false }); - await user1Client.CreateUser(new CreateApplicationUserRequest() { Email = "test8@gmail.com", Password = "afewfoiewiou" }); + await settings.UpdateSetting(new PoliciesSettings() {LockSubscription = false}); + await user1Client.CreateUser( + new CreateApplicationUserRequest() {Email = "test8@gmail.com", Password = "afewfoiewiou"}); // But not an admin - await AssertHttpError(403, async () => await user1Client.CreateUser(new CreateApplicationUserRequest() { Email = "admin8@gmail.com", Password = "afewfoiewiou", IsAdministrator = true })); + await AssertHttpError(403, + async () => await user1Client.CreateUser(new CreateApplicationUserRequest() + { + Email = "admin8@gmail.com", Password = "afewfoiewiou", IsAdministrator = true + })); } } - + [Fact(Timeout = TestTimeout)] [Trait("Integration", "Integration")] public async Task StoresControllerTests() @@ -165,15 +208,15 @@ namespace BTCPayServer.Tests user.GrantAccess(); await user.MakeAdmin(); var client = await user.CreateClient(Policies.Unrestricted); - + //create store var newStore = await client.CreateStore(new CreateStoreRequest() {Name = "A"}); - + //update store var updatedStore = await client.UpdateStore(newStore.Id, new UpdateStoreRequest() {Name = "B"}); Assert.Equal("B", updatedStore.Name); Assert.Equal("B", (await client.GetStore(newStore.Id)).Name); - + //list stores var stores = await client.GetStores(); var storeIds = stores.Select(data => data.Id); @@ -185,9 +228,9 @@ namespace BTCPayServer.Tests //get store var store = await client.GetStore(user.StoreId); - Assert.Equal(user.StoreId,store.Id); - Assert.Contains(store.Name,storeNames); - + Assert.Equal(user.StoreId, store.Id); + Assert.Contains(store.Name, storeNames); + //remove store await client.RemoveStore(newStore.Id); await AssertHttpError(403, async () => @@ -195,13 +238,14 @@ namespace BTCPayServer.Tests await client.GetStore(newStore.Id); }); Assert.Single(await client.GetStores()); - + newStore = await client.CreateStore(new CreateStoreRequest() {Name = "A"}); - var scopedClient = await user.CreateClient(Permission.Create(Policies.CanViewStoreSettings, user.StoreId).ToString()); + var scopedClient = + await user.CreateClient(Permission.Create(Policies.CanViewStoreSettings, user.StoreId).ToString()); Assert.Single(await scopedClient.GetStores()); } } - + private async Task AssertHttpError(int code, Func act) { var ex = await Assert.ThrowsAsync(act); @@ -235,45 +279,40 @@ namespace BTCPayServer.Tests await clientProfile.GetCurrentUser(); await clientBasic.GetCurrentUser(); - await Assert.ThrowsAsync(async () => await clientInsufficient.CreateUser(new CreateApplicationUserRequest() - { - Email = $"{Guid.NewGuid()}@g.com", - Password = Guid.NewGuid().ToString() - })); + await Assert.ThrowsAsync(async () => + await clientInsufficient.CreateUser(new CreateApplicationUserRequest() + { + Email = $"{Guid.NewGuid()}@g.com", Password = Guid.NewGuid().ToString() + })); var newUser = await clientServer.CreateUser(new CreateApplicationUserRequest() { - Email = $"{Guid.NewGuid()}@g.com", - Password = Guid.NewGuid().ToString() + Email = $"{Guid.NewGuid()}@g.com", Password = Guid.NewGuid().ToString() }); Assert.NotNull(newUser); var newUser2 = await clientBasic.CreateUser(new CreateApplicationUserRequest() { - Email = $"{Guid.NewGuid()}@g.com", - Password = Guid.NewGuid().ToString() + Email = $"{Guid.NewGuid()}@g.com", Password = Guid.NewGuid().ToString() }); Assert.NotNull(newUser2); - await Assert.ThrowsAsync(async () => await clientServer.CreateUser(new CreateApplicationUserRequest() - { - Email = $"{Guid.NewGuid()}", - Password = Guid.NewGuid().ToString() - })); + await Assert.ThrowsAsync(async () => + await clientServer.CreateUser(new CreateApplicationUserRequest() + { + Email = $"{Guid.NewGuid()}", Password = Guid.NewGuid().ToString() + })); - await Assert.ThrowsAsync(async () => await clientServer.CreateUser(new CreateApplicationUserRequest() - { - Email = $"{Guid.NewGuid()}@g.com", - })); - - await Assert.ThrowsAsync(async () => await clientServer.CreateUser(new CreateApplicationUserRequest() - { - Password = Guid.NewGuid().ToString() - })); + await Assert.ThrowsAsync(async () => + await clientServer.CreateUser( + new CreateApplicationUserRequest() {Email = $"{Guid.NewGuid()}@g.com",})); + await Assert.ThrowsAsync(async () => + await clientServer.CreateUser( + new CreateApplicationUserRequest() {Password = Guid.NewGuid().ToString()})); } } - + [Fact(Timeout = TestTimeout)] [Trait("Integration", "Integration")] public async Task HealthControllerTests() @@ -288,7 +327,7 @@ namespace BTCPayServer.Tests Assert.True(apiHealthData.Synchronized); } } - + [Fact(Timeout = TestTimeout)] [Trait("Integration", "Integration")] public async Task ServerInfoControllerTests() @@ -303,19 +342,115 @@ namespace BTCPayServer.Tests user.GrantAccess(); var clientBasic = await user.CreateClient(); var serverInfoData = await clientBasic.GetServerInfo(); - Assert.NotNull(serverInfoData); - Assert.NotNull(serverInfoData.Version); - Assert.NotNull(serverInfoData.Onion); Assert.NotNull(serverInfoData.Status); - Assert.True(serverInfoData.Status.FullySynched); Assert.Contains("BTC", serverInfoData.SupportedPaymentMethods); Assert.Contains("BTC_LightningLike", serverInfoData.SupportedPaymentMethods); - - Assert.NotNull(serverInfoData.Status.SyncStatus); - Assert.Single(serverInfoData.Status.SyncStatus.Select(s => s.CryptoCode == "BTC")); } } + + [Fact(Timeout = TestTimeout)] + [Trait("Integration", "Integration")] + public async Task PaymentControllerTests() + { + using (var tester = ServerTester.Create()) + { + await tester.StartAsync(); + var user = tester.NewAccount(); + user.GrantAccess(); + await user.MakeAdmin(); + var client = await user.CreateClient(Policies.Unrestricted); + var viewOnly = await user.CreateClient(Policies.CanViewPaymentRequests); + + //create payment request + + //validation errors + await AssertHttpError(400, async () => + { + await client.CreatePaymentRequest(user.StoreId, new CreatePaymentRequestRequest() {Title = "A"}); + }); + await AssertHttpError(400, async () => + { + await client.CreatePaymentRequest(user.StoreId, + new CreatePaymentRequestRequest() {Title = "A", Currency = "BTC", Amount = 0}); + }); + await AssertHttpError(400, async () => + { + await client.CreatePaymentRequest(user.StoreId, + new CreatePaymentRequestRequest() {Title = "A", Currency = "helloinvalid", Amount = 1}); + }); + await AssertHttpError(403, async () => + { + await viewOnly.CreatePaymentRequest(user.StoreId, + new CreatePaymentRequestRequest() {Title = "A", Currency = "helloinvalid", Amount = 1}); + }); + var newPaymentRequest = await client.CreatePaymentRequest(user.StoreId, + new CreatePaymentRequestRequest() {Title = "A", Currency = "USD", Amount = 1}); + + //list payment request + var paymentRequests = await viewOnly.GetPaymentRequests(user.StoreId); + + Assert.NotNull(paymentRequests); + Assert.Single(paymentRequests); + Assert.Equal(newPaymentRequest.Id, paymentRequests.First().Id); + + //get payment request + var paymentRequest = await viewOnly.GetPaymentRequest(user.StoreId, newPaymentRequest.Id); + Assert.Equal(newPaymentRequest.Title, paymentRequest.Title); + + //update payment request + var updateRequest = JObject.FromObject(paymentRequest).ToObject(); + updateRequest.Title = "B"; + await AssertHttpError(403, async () => + { + await viewOnly.UpdatePaymentRequest(user.StoreId, paymentRequest.Id, updateRequest); + }); + await client.UpdatePaymentRequest(user.StoreId, paymentRequest.Id, updateRequest); + paymentRequest = await client.GetPaymentRequest(user.StoreId, newPaymentRequest.Id); + Assert.Equal(updateRequest.Title, paymentRequest.Title); + + //archive payment request + await AssertHttpError(403, async () => + { + await viewOnly.ArchivePaymentRequest(user.StoreId, paymentRequest.Id); + }); + + await client.ArchivePaymentRequest(user.StoreId, paymentRequest.Id); + Assert.DoesNotContain(paymentRequest.Id, + (await client.GetPaymentRequests(user.StoreId)).Select(data => data.Id)); + } + } + + [Fact(Timeout = TestTimeout)] + [Trait("Fast", "Fast")] + public async Task DecimalStringJsonConverterTests() + { + JsonReader Get(string val) + { + return new JsonTextReader(new StringReader(val)); + } + + var jsonConverter = new DecimalStringJsonConverter(); + Assert.True(jsonConverter.CanConvert(typeof(decimal))); + Assert.True(jsonConverter.CanConvert(typeof(decimal?))); + Assert.False(jsonConverter.CanConvert(typeof(double))); + Assert.False(jsonConverter.CanConvert(typeof(float))); + Assert.False(jsonConverter.CanConvert(typeof(int))); + Assert.False(jsonConverter.CanConvert(typeof(string))); + + var numberJson = "1"; + var numberDecimalJson = "1.2"; + var stringJson = "\"1.2\""; + Assert.Equal(1m, jsonConverter.ReadJson(Get(numberJson), typeof(decimal), null, null)); + Assert.Equal(1.2m, jsonConverter.ReadJson(Get(numberDecimalJson), typeof(decimal), null, null)); + Assert.Null(jsonConverter.ReadJson(Get("null"), typeof(decimal?), null, null)); + Assert.Throws(() => + { + jsonConverter.ReadJson(Get("null"), typeof(decimal), null, null); + }); + Assert.Equal(1.2m, jsonConverter.ReadJson(Get(stringJson), typeof(decimal), null, null)); + Assert.Equal(1.2m, jsonConverter.ReadJson(Get(stringJson), typeof(decimal?), null, null)); + } } } diff --git a/BTCPayServer/Controllers/GreenField/PaymentRequestsController.cs b/BTCPayServer/Controllers/GreenField/PaymentRequestsController.cs new file mode 100644 index 000000000..1dc79cef7 --- /dev/null +++ b/BTCPayServer/Controllers/GreenField/PaymentRequestsController.cs @@ -0,0 +1,164 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using BTCPayServer.Client; +using BTCPayServer.Client.Models; +using BTCPayServer.Data; +using BTCPayServer.Security; +using BTCPayServer.Services.PaymentRequests; +using BTCPayServer.Services.Rates; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using PaymentRequestData = BTCPayServer.Data.PaymentRequestData; + +namespace BTCPayServer.Controllers.GreenField +{ + [ApiController] + [Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield)] + public class GreenFieldPaymentRequestsController : ControllerBase + { + private readonly PaymentRequestRepository _paymentRequestRepository; + private readonly CurrencyNameTable _currencyNameTable; + + public GreenFieldPaymentRequestsController(PaymentRequestRepository paymentRequestRepository, + CurrencyNameTable currencyNameTable) + { + _paymentRequestRepository = paymentRequestRepository; + _currencyNameTable = currencyNameTable; + } + + [Authorize(Policy = Policies.CanViewPaymentRequests, AuthenticationSchemes = AuthenticationSchemes.Greenfield)] + [HttpGet("~/api/v1/stores/{storeId}/payment-requests")] + public async Task>> GetPaymentRequests(string storeId) + { + var prs = await _paymentRequestRepository.FindPaymentRequests( + new PaymentRequestQuery() {StoreId = storeId, IncludeArchived = false}); + return Ok(prs.Items.Select(FromModel)); + } + + [Authorize(Policy = Policies.CanViewPaymentRequests, AuthenticationSchemes = AuthenticationSchemes.Greenfield)] + [HttpGet("~/api/v1/stores/{storeId}/payment-requests/{paymentRequestId}")] + public async Task> GetPaymentRequest(string storeId, string paymentRequestId) + { + var pr = await _paymentRequestRepository.FindPaymentRequests( + new PaymentRequestQuery() {StoreId = storeId, Ids = new[] {paymentRequestId}}); + + if (pr.Total == 0) + { + return NotFound(); + } + + return Ok(FromModel(pr.Items.First())); + } + + [Authorize(Policy = Policies.CanModifyPaymentRequests, + AuthenticationSchemes = AuthenticationSchemes.Greenfield)] + [HttpDelete("~/api/v1/stores/{storeId}/payment-requests/{paymentRequestId}")] + public async Task ArchivePaymentRequest(string storeId, string paymentRequestId) + { + var pr = await _paymentRequestRepository.FindPaymentRequests( + new PaymentRequestQuery() {StoreId = storeId, Ids = new[] {paymentRequestId}, IncludeArchived = false}); + if (pr.Total == 0) + { + return NotFound(); + } + + var updatedPr = pr.Items.First(); + updatedPr.Archived = true; + await _paymentRequestRepository.CreateOrUpdatePaymentRequest(updatedPr); + return Ok(); + } + + [HttpPost("~/api/v1/stores/{storeId}/payment-requests")] + [Authorize(Policy = Policies.CanModifyPaymentRequests, + AuthenticationSchemes = AuthenticationSchemes.Greenfield)] + public async Task CreatePaymentRequest(string storeId, + CreatePaymentRequestRequest request) + { + var validationResult = Validate(request); + if (validationResult != null) + { + return validationResult; + } + + var pr = new PaymentRequestData() + { + StoreDataId = storeId, + Status = Client.Models.PaymentRequestData.PaymentRequestStatus.Pending, + Created = DateTimeOffset.Now + }; + pr.SetBlob(request); + pr = await _paymentRequestRepository.CreateOrUpdatePaymentRequest(pr); + return Ok(FromModel(pr)); + } + + [HttpPut("~/api/v1/stores/{storeId}/payment-requests/{paymentRequestId}")] + [Authorize(Policy = Policies.CanModifyPaymentRequests, + AuthenticationSchemes = AuthenticationSchemes.Greenfield)] + public async Task UpdatePaymentRequest(string storeId, + string paymentRequestId, [FromBody] UpdatePaymentRequestRequest request) + { + var validationResult = Validate(request); + if (validationResult != null) + { + return validationResult; + } + + var pr = await _paymentRequestRepository.FindPaymentRequests( + new PaymentRequestQuery() {StoreId = storeId, Ids = new[] {paymentRequestId}}); + if (pr.Total == 0) + { + return NotFound(); + } + + var updatedPr = pr.Items.First(); + updatedPr.SetBlob(request); + + return Ok(FromModel(await _paymentRequestRepository.CreateOrUpdatePaymentRequest(updatedPr))); + } + + private IActionResult Validate(PaymentRequestBaseData data) + { + if (data is null) + return BadRequest(); + if (data.Amount <= 0) + { + ModelState.AddModelError(nameof(data.Amount), "Please provide an amount greater than 0"); + } + + if (string.IsNullOrEmpty(data.Currency) || + _currencyNameTable.GetCurrencyData(data.Currency, false) == null) + ModelState.AddModelError(nameof(data.Currency), "Invalid currency"); + + if (string.IsNullOrEmpty(data.Title)) + ModelState.AddModelError(nameof(data.Title), "Title is required"); + + if (!string.IsNullOrEmpty(data.CustomCSSLink) && data.CustomCSSLink.Length > 500) + ModelState.AddModelError(nameof(data.CustomCSSLink), "CustomCSSLink is 500 chars max"); + + return !ModelState.IsValid ? BadRequest(new ValidationProblemDetails(ModelState)) : null; + } + + private static Client.Models.PaymentRequestData FromModel(PaymentRequestData data) + { + var blob = data.GetBlob(); + return new Client.Models.PaymentRequestData() + { + Created = data.Created, + Id = data.Id, + Status = data.Status, + Archived = data.Archived, + Amount = blob.Amount, + Currency = blob.Currency, + Description = blob.Description, + Title = blob.Title, + ExpiryDate = blob.ExpiryDate, + Email = blob.Email, + AllowCustomPaymentAmounts = blob.AllowCustomPaymentAmounts, + EmbeddedCSS = blob.EmbeddedCSS, + CustomCSSLink = blob.CustomCSSLink + }; + } + } +} diff --git a/BTCPayServer/Controllers/ManageController.APIKeys.cs b/BTCPayServer/Controllers/ManageController.APIKeys.cs index 6bdf4a19b..ec1c5d2cb 100644 --- a/BTCPayServer/Controllers/ManageController.APIKeys.cs +++ b/BTCPayServer/Controllers/ManageController.APIKeys.cs @@ -355,7 +355,7 @@ namespace BTCPayServer.Controllers { {BTCPayServer.Client.Policies.Unrestricted, ("Unrestricted access", "The app will have unrestricted access to your account.")}, {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 create, view and modify, delete and create new invoices on the 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.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.")}, @@ -364,6 +364,10 @@ namespace BTCPayServer.Controllers {BTCPayServer.Client.Policies.CanModifyProfile, ("Manage your profile", "The app will be able to view and modify your user profile.")}, {BTCPayServer.Client.Policies.CanCreateInvoice, ("Create an invoice", "The app will be able to create new invoices.")}, {$"{BTCPayServer.Client.Policies.CanCreateInvoice}:", ("Create an invoice", "The app will be able to create new invoices on the selected stores.")}, + {BTCPayServer.Client.Policies.CanModifyPaymentRequests, ("Modify your payment requests", "The app will be able to view, modify, delete and create new payment requests on all your stores.")}, + {$"{BTCPayServer.Client.Policies.CanModifyPaymentRequests}:", ("Manage selected stores' payment requests", "The app will be able to view, modify, delete and create new payment requests on the selected stores.")}, + {BTCPayServer.Client.Policies.CanViewPaymentRequests, ("View your payment requests", "The app will be able to view payment requests.")}, + {$"{BTCPayServer.Client.Policies.CanViewPaymentRequests}:", ("View your payment requests", "The app will be able to view the selected stores' payment requests.")}, }; public string Title { diff --git a/BTCPayServer/Data/PaymentRequestDataExtensions.cs b/BTCPayServer/Data/PaymentRequestDataExtensions.cs index 987c1015d..e88e535fc 100644 --- a/BTCPayServer/Data/PaymentRequestDataExtensions.cs +++ b/BTCPayServer/Data/PaymentRequestDataExtensions.cs @@ -1,22 +1,20 @@ -using NBitcoin; +using BTCPayServer.Client.Models; using NBXplorer; -using NBXplorer.DerivationStrategy; using Newtonsoft.Json.Linq; -using static BTCPayServer.Data.PaymentRequestData; namespace BTCPayServer.Data { public static class PaymentRequestDataExtensions { - public static PaymentRequestBlob GetBlob(this PaymentRequestData paymentRequestData) + public static PaymentRequestBaseData GetBlob(this PaymentRequestData paymentRequestData) { var result = paymentRequestData.Blob == null - ? new PaymentRequestBlob() - : JObject.Parse(ZipUtils.Unzip(paymentRequestData.Blob)).ToObject(); + ? new PaymentRequestBaseData() + : JObject.Parse(ZipUtils.Unzip(paymentRequestData.Blob)).ToObject(); return result; } - public static bool SetBlob(this PaymentRequestData paymentRequestData, PaymentRequestBlob blob) + public static bool SetBlob(this PaymentRequestData paymentRequestData, PaymentRequestBaseData blob) { var original = new Serializer(null).ToString(paymentRequestData.GetBlob()); var newBlob = new Serializer(null).ToString(blob); diff --git a/BTCPayServer/Hosting/Startup.cs b/BTCPayServer/Hosting/Startup.cs index b797bfc97..dd642b7df 100644 --- a/BTCPayServer/Hosting/Startup.cs +++ b/BTCPayServer/Hosting/Startup.cs @@ -20,6 +20,7 @@ using BTCPayServer.Security; using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.Net.Http.Headers; using System.Net; +using BTCPayServer.JsonConverters; using BTCPayServer.PaymentRequest; using BTCPayServer.Services.Apps; using BTCPayServer.Storage; diff --git a/BTCPayServer/Models/PaymentRequestViewModels/ListPaymentRequestsViewModel.cs b/BTCPayServer/Models/PaymentRequestViewModels/ListPaymentRequestsViewModel.cs index 145a31ef9..e6e56b3da 100644 --- a/BTCPayServer/Models/PaymentRequestViewModels/ListPaymentRequestsViewModel.cs +++ b/BTCPayServer/Models/PaymentRequestViewModels/ListPaymentRequestsViewModel.cs @@ -1,10 +1,12 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; +using BTCPayServer.Client.Models; using BTCPayServer.Data; using BTCPayServer.Services.PaymentRequests; using BTCPayServer.Services.Rates; using Microsoft.AspNetCore.Mvc.Rendering; +using PaymentRequestData = BTCPayServer.Data.PaymentRequestData; namespace BTCPayServer.Models.PaymentRequestViewModels { @@ -98,14 +100,14 @@ namespace BTCPayServer.Models.PaymentRequestViewModels EmbeddedCSS = $""; switch (data.Status) { - case PaymentRequestData.PaymentRequestStatus.Pending: + case Client.Models.PaymentRequestData.PaymentRequestStatus.Pending: Status = ExpiryDate.HasValue ? $"Expires on {ExpiryDate.Value:g}" : "Pending"; IsPending = true; break; - case PaymentRequestData.PaymentRequestStatus.Completed: + case Client.Models.PaymentRequestData.PaymentRequestStatus.Completed: Status = "Settled"; break; - case PaymentRequestData.PaymentRequestStatus.Expired: + case Client.Models.PaymentRequestData.PaymentRequestStatus.Expired: Status = "Expired"; break; default: diff --git a/BTCPayServer/PaymentRequest/PaymentRequestHub.cs b/BTCPayServer/PaymentRequest/PaymentRequestHub.cs index 57d412929..661abf127 100644 --- a/BTCPayServer/PaymentRequest/PaymentRequestHub.cs +++ b/BTCPayServer/PaymentRequest/PaymentRequestHub.cs @@ -16,6 +16,7 @@ using BTCPayServer.Data; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Builder; +using PaymentRequestData = BTCPayServer.Client.Models.PaymentRequestData; namespace BTCPayServer.PaymentRequest { @@ -123,7 +124,7 @@ namespace BTCPayServer.PaymentRequest Logs.PayServer.LogInformation("Starting payment request expiration watcher"); var (total, items) = await _PaymentRequestRepository.FindPaymentRequests(new PaymentRequestQuery() { - Status = new[] {PaymentRequestData.PaymentRequestStatus.Pending} + Status = new[] {Client.Models.PaymentRequestData.PaymentRequestStatus.Pending} }, cancellationToken); Logs.PayServer.LogInformation($"{total} pending payment requests being checked since last run"); @@ -172,7 +173,8 @@ namespace BTCPayServer.PaymentRequest await InfoUpdated(updated.PaymentRequestId); var expiry = updated.Data.GetBlob().ExpiryDate; - if (updated.Data.Status == PaymentRequestData.PaymentRequestStatus.Pending && + if (updated.Data.Status == + PaymentRequestData.PaymentRequestStatus.Pending && expiry.HasValue) { QueueExpiryTask( diff --git a/BTCPayServer/PaymentRequest/PaymentRequestService.cs b/BTCPayServer/PaymentRequest/PaymentRequestService.cs index 4ddda8c07..a17d00f76 100644 --- a/BTCPayServer/PaymentRequest/PaymentRequestService.cs +++ b/BTCPayServer/PaymentRequest/PaymentRequestService.cs @@ -48,16 +48,16 @@ namespace BTCPayServer.PaymentRequest if (blob.ExpiryDate.HasValue) { if (blob.ExpiryDate.Value <= DateTimeOffset.UtcNow) - currentStatus = PaymentRequestData.PaymentRequestStatus.Expired; + currentStatus = Client.Models.PaymentRequestData.PaymentRequestStatus.Expired; } - else if (pr.Status == PaymentRequestData.PaymentRequestStatus.Pending) + else if (pr.Status == Client.Models.PaymentRequestData.PaymentRequestStatus.Pending) { var rateRules = pr.StoreData.GetStoreBlob().GetRateRules(_BtcPayNetworkProvider); var invoices = await _PaymentRequestRepository.GetInvoicesForPaymentRequest(pr.Id); var contributions = _AppService.GetContributionsByPaymentMethodId(blob.Currency, invoices, true); if (contributions.TotalCurrency >= blob.Amount) { - currentStatus = PaymentRequestData.PaymentRequestStatus.Completed; + currentStatus = Client.Models.PaymentRequestData.PaymentRequestStatus.Completed; } } diff --git a/BTCPayServer/Security/GreenField/GreenFieldAuthorizationHandler.cs b/BTCPayServer/Security/GreenField/GreenFieldAuthorizationHandler.cs index 9a0fd3196..ae325524f 100644 --- a/BTCPayServer/Security/GreenField/GreenFieldAuthorizationHandler.cs +++ b/BTCPayServer/Security/GreenField/GreenFieldAuthorizationHandler.cs @@ -42,6 +42,8 @@ namespace BTCPayServer.Security.GreenField success = context.HasPermission(Permission.Create(requirement.Policy)); break; + case Policies.CanViewPaymentRequests: + case Policies.CanModifyPaymentRequests: case Policies.CanViewStoreSettings: case Policies.CanModifyStoreSettings: var storeId = _HttpContext.GetImplicitStoreId(); diff --git a/BTCPayServer/Services/PaymentRequests/PaymentRequestRepository.cs b/BTCPayServer/Services/PaymentRequests/PaymentRequestRepository.cs index ce5058d8b..b68bb0eb6 100644 --- a/BTCPayServer/Services/PaymentRequests/PaymentRequestRepository.cs +++ b/BTCPayServer/Services/PaymentRequests/PaymentRequestRepository.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -77,7 +76,7 @@ namespace BTCPayServer.Services.PaymentRequests } } - public async Task UpdatePaymentRequestStatus(string paymentRequestId, PaymentRequestData.PaymentRequestStatus status, CancellationToken cancellationToken = default) + public async Task UpdatePaymentRequestStatus(string paymentRequestId, Client.Models.PaymentRequestData.PaymentRequestStatus status, CancellationToken cancellationToken = default) { using (var context = _ContextFactory.CreateContext()) { @@ -102,7 +101,7 @@ namespace BTCPayServer.Services.PaymentRequests if (!string.IsNullOrEmpty(query.StoreId)) { queryable = queryable.Where(data => - data.StoreDataId.Equals(query.StoreId, StringComparison.InvariantCulture)); + data.StoreDataId == query.StoreId); } if (query.Status != null && query.Status.Any()) @@ -110,7 +109,13 @@ namespace BTCPayServer.Services.PaymentRequests queryable = queryable.Where(data => query.Status.Contains(data.Status)); } - + + if (query.Ids != null && query.Ids.Any()) + { + queryable = queryable.Where(data => + query.Ids.Contains(data.Id)); + } + if (!string.IsNullOrEmpty(query.UserId)) { queryable = queryable.Where(i => @@ -181,10 +186,11 @@ namespace BTCPayServer.Services.PaymentRequests public class PaymentRequestQuery { public string StoreId { get; set; } - public bool IncludeArchived { get; set; } = true; - public PaymentRequestData.PaymentRequestStatus[] Status{ get; set; } + public bool IncludeArchived { get; set; } = true; + public Client.Models.PaymentRequestData.PaymentRequestStatus[] Status{ get; set; } public string UserId { get; set; } public int? Skip { get; set; } public int? Count { get; set; } + public string[] Ids { get; set; } } } diff --git a/BTCPayServer/wwwroot/swagger/v1/swagger.template.payment-requests.json b/BTCPayServer/wwwroot/swagger/v1/swagger.template.payment-requests.json new file mode 100644 index 000000000..96987c86a --- /dev/null +++ b/BTCPayServer/wwwroot/swagger/v1/swagger.template.payment-requests.json @@ -0,0 +1,370 @@ +{ + "paths": { + "/api/v1/stores/{storeId}/payment-requests": { + "get": { + "tags": [ + "Payment Requests" + ], + "summary": "Get payment requests", + "parameters": [ + { + "name": "storeId", + "in": "path", + "required": true, + "description": "The store to query", + "schema": { "type": "string" } + } + ], + "description": "View information about the existing payment requests", + "operationId": "PaymentRequests_GetPaymentRequests", + "responses": { + "200": { + "description": "list of payment requests", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PaymentRequestDataList" + } + } + } + } + }, + "security": [ + { + "API Key": [ + "btcpay.store.canviewpaymentrequests" + ], + "Basic": [] + } + ] + }, + "post": { + "tags": [ + "Payment Requests" + ], + "summary": "Create a new payment request", + "parameters": [ + { + "name": "storeId", + "in": "path", + "required": true, + "description": "The store to query", + "schema": { "type": "string" } + } + ], + "description": "Create a new payment request", + "operationId": "PaymentRequests_CreatePaymentRequest", + "responses": { + "200": { + "description": "Information about the new payment request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PaymentRequestData" + } + } + } + }, + "400": { + "description": "A list of errors that occurred when creating the payment request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ValidationProblemDetails" + } + } + } + }, + "403": { + "description": "If you are authenticated but forbidden to add new payment requests" + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PaymentRequestBaseData" + } + } + } + }, + "security": [ + { + "API Key": [ + "btcpay.store.canmodifypaymentrequests" + ], + "Basic": [] + } + ] + } + }, + "/api/v1/stores/{storeId}/payment-requests/{paymentRequestId}": { + "get": { + "tags": [ + "Payment Requests" + ], + "summary": "Get payment request", + "parameters": [ + { + "name": "storeId", + "in": "path", + "required": true, + "description": "The store to fetch", + "schema": { "type": "string" } + }, + { + "name": "paymentRequestId", + "in": "path", + "required": true, + "description": "The payment request to fetch", + "schema": { "type": "string" } + } + ], + "description": "View information about the specified payment request", + "operationId": "PaymentRequests_GetPaymentRequests", + "responses": { + "200": { + "description": "specified payment request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PaymentRequestData" + } + } + } + }, + "403": { + "description": "If you are authenticated but forbidden to view the specified payment request" + }, + "404": { + "description": "The key is not found for this payment request" + } + }, + "security": [ + { + "API Key": [ + "btcpay.store.canviewpaymentrequests" + ], + "Basic": [] + } + ] + }, + "delete": { + "tags": [ + "Payment Requests" + ], + "summary": "Archive payment request", + "description": "Archives the specified payment request.", + "operationId": "PaymentRequests_ArchivePaymentRequest", + "parameters": [ + { + "name": "storeId", + "in": "path", + "required": true, + "description": "The store the payment request belongs to", + "schema": { "type": "string" } + }, + { + "name": "paymentRequestId", + "in": "path", + "required": true, + "description": "The payment request to remove", + "schema": { "type": "string" } + } + ], + "responses": { + "200": { + "description": "The payment request has been archived" + }, + "400": { + "description": "A list of errors that occurred when archiving the payment request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ValidationProblemDetails" + } + } + } + }, + "403": { + "description": "If you are authenticated but forbidden to archive the specified payment request" + }, + "404": { + "description": "The key is not found for this payment request" + } + }, + "security": [ + { + "API Key": [ "btcpay.store.canmodifypaymentrequests"], + "Basic": [] + } + ] + }, + "put": { + "tags": [ + "Payment Requests" + ], + "summary": "Update payment request", + "parameters": [ + { + "name": "storeId", + "in": "path", + "required": true, + "description": "The store to query", + "schema": { "type": "string" } + }, + { + "name": "paymentRequestId", + "in": "path", + "required": true, + "description": "The payment request to remove", + "schema": { "type": "string" } + } + ], + "description": "Update a payment request", + "operationId": "PaymentRequests_UpdatePaymentRequest", + "responses": { + "200": { + "description": "The updated payment request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PaymentRequestData" + } + } + } + }, + "400": { + "description": "A list of errors that occurred when updating the payment request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ValidationProblemDetails" + } + } + } + }, + "403": { + "description": "If you are authenticated but forbidden to update the payment request" + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PaymentRequestBaseData" + } + } + } + }, + "security": [ + { + "API Key": [ + "btcpay.store.canmodifypaymentrequests" + ], + "Basic": [] + } + ] + } + } + }, + "components": { + "schemas": { + "PaymentRequestDataList": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PaymentRequestData" + } + }, + "PaymentRequestData": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/PaymentRequestBaseData" + }, + "properties": { + "id": { + "type": "string", + "description": "The id of the payment request", + "nullable": false + }, + "status": { + "type": "string", + "enum": [ "Pending", "Completed" ,"Expired"], + "description": "The status of the payment request", + "nullable": false + }, + "created": { + "type": "string", + "description": "The creation date of the payment request", + "nullable": false, + "format": "date-time" + } + } + }, + "PaymentRequestBaseData":{ + "type": "object", + "additionalProperties": false, + "properties": { + "amount": { + "type": "string", + "format": "decimal", + "minimum": 0, + "exclusiveMinimum": true, + "description": "The amount of the payment request", + "nullable": false + }, + + "currency": { + "type": "string", + "format": "ISO 4217 Currency code(BTC, EUR, USD, etc)", + "description": "The currency of the payment request", + "nullable": false + }, + "email": { + "type": "string", + "description": "The email used in invoices generated by the payment request", + "nullable": true, + "format": "email" + }, + "description": { + "type": "string", + "description": "The description of the payment request", + "nullable": true, + "format": "html" + }, + "expiryDate": { + "type": "string", + "description": "The expiry date of the payment request", + "nullable": true, + "format": "date-time" + }, + "embeddedCSS": { + "type": "string", + "description": "Custom CSS styling for the payment request", + "nullable": true, + "format": "css", + "maximum": 500 + }, + "customCSSLink": { + "type": "string", + "description": "Custom CSS link for styling the payment request", + "nullable": true, + "format": "uri" + }, + "allowCustomPaymentAmounts": { + "type": "boolean", + "description": "Whether to allow users to create invoices that partially pay the payment request ", + "nullable": true + } + } + } + } + }, + "tags": [ + { + "name": "Payment Requests" + } + ] +}