diff --git a/BTCPayServer.Client/BTCPayServerClient.Apps.cs b/BTCPayServer.Client/BTCPayServerClient.Apps.cs index 4f537258c..d24006e6e 100644 --- a/BTCPayServer.Client/BTCPayServerClient.Apps.cs +++ b/BTCPayServer.Client/BTCPayServerClient.Apps.cs @@ -19,5 +19,25 @@ namespace BTCPayServer.Client method: HttpMethod.Post), token); return await HandleResponse(response); } + + public virtual async Task GetApp(string appId, CancellationToken token = default) + { + if (appId == null) + throw new ArgumentNullException(nameof(appId)); + var response = await _httpClient.SendAsync( + CreateHttpRequest($"api/v1/apps/{appId}", + method: HttpMethod.Get), token); + return await HandleResponse(response); + } + + public virtual async Task DeleteApp(string appId, CancellationToken token = default) + { + if (appId == null) + throw new ArgumentNullException(nameof(appId)); + var response = await _httpClient.SendAsync( + CreateHttpRequest($"api/v1/apps/{appId}", + method: HttpMethod.Delete), token); + await HandleResponse(response); + } } } diff --git a/BTCPayServer.Tests/GreenfieldAPITests.cs b/BTCPayServer.Tests/GreenfieldAPITests.cs index 96e2f57f8..724391355 100644 --- a/BTCPayServer.Tests/GreenfieldAPITests.cs +++ b/BTCPayServer.Tests/GreenfieldAPITests.cs @@ -189,18 +189,42 @@ namespace BTCPayServer.Tests [Fact(Timeout = TestTimeout)] [Trait("Integration", "Integration")] - public async Task CanCreatePointOfSaleAppViaAPI() + public async Task CanCreateReadAndDeletePointOfSaleApp() { using var tester = CreateServerTester(); await tester.StartAsync(); var user = tester.NewAccount(); await user.RegisterDerivationSchemeAsync("BTC"); var client = await user.CreateClient(); + + // Test creating a POS app var app = await client.CreatePointOfSaleApp(user.StoreId, new CreatePointOfSaleAppRequest() { AppName = "test app from API" }); - Assert.Equal("test app from API", app.Name); Assert.Equal(user.StoreId, app.StoreId); Assert.Equal("PointOfSale", app.AppType); + + // Make sure we return a 404 if we try to get an app that doesn't exist + await AssertHttpError(404, async () => { + await client.GetApp("some random ID lol"); + }); + + // Test that we can retrieve the app data + var retrievedApp = await client.GetApp(app.Id); + Assert.Equal(app.Name, retrievedApp.Name); + Assert.Equal(app.StoreId, retrievedApp.StoreId); + Assert.Equal(app.AppType, retrievedApp.AppType); + + // Make sure we return a 404 if we try to delete an app that doesn't exist + await AssertHttpError(404, async () => + { + await client.DeleteApp("some random ID lol"); + }); + + // Test deleting the newly created app + await client.DeleteApp(retrievedApp.Id); + await AssertHttpError(404, async () => { + await client.GetApp(retrievedApp.Id); + }); } [Fact(Timeout = TestTimeout)] diff --git a/BTCPayServer/Controllers/GreenField/GreenfieldAppsController.cs b/BTCPayServer/Controllers/GreenField/GreenfieldAppsController.cs index 035397c82..ae54f896b 100644 --- a/BTCPayServer/Controllers/GreenField/GreenfieldAppsController.cs +++ b/BTCPayServer/Controllers/GreenField/GreenfieldAppsController.cs @@ -86,6 +86,38 @@ namespace BTCPayServer.Controllers.Greenfield return Ok(ToModel(appData)); } + [HttpGet("~/api/v1/apps/{appId}")] + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)] + public async Task GetApp(string appId) + { + var app = await _appService.GetApp(appId, AppType.PointOfSale); + if (app == null) + { + return AppNotFound(); + } + + return Ok(ToModel(app)); + } + + [HttpDelete("~/api/v1/apps/{appId}")] + public async Task DeleteApp(string appId) + { + var app = await _appService.GetApp(appId, null); + if (app == null) + { + return AppNotFound(); + } + + await _appService.DeleteApp(app); + + return Ok(); + } + + private IActionResult AppNotFound() + { + return this.CreateAPIError(404, "app-not-found", "The app with specified ID was not found"); + } + private PointOfSaleAppData ToModel(AppData appData) { return new PointOfSaleAppData diff --git a/BTCPayServer/Controllers/GreenField/LocalBTCPayServerClient.cs b/BTCPayServer/Controllers/GreenField/LocalBTCPayServerClient.cs index 918ca12da..15546b6bd 100644 --- a/BTCPayServer/Controllers/GreenField/LocalBTCPayServerClient.cs +++ b/BTCPayServer/Controllers/GreenField/LocalBTCPayServerClient.cs @@ -20,11 +20,9 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using NBitcoin; using NBXplorer.Models; -using Newtonsoft.Json; using Newtonsoft.Json.Linq; using InvoiceData = BTCPayServer.Client.Models.InvoiceData; using Language = BTCPayServer.Client.Models.Language; @@ -75,6 +73,8 @@ namespace BTCPayServer.Controllers.Greenfield private readonly GreenfieldStoreAutomatedLightningPayoutProcessorsController _greenfieldStoreAutomatedLightningPayoutProcessorsController; + private readonly GreenfieldAppsController _greenFieldAppsController; + private readonly IServiceProvider _serviceProvider; public BTCPayServerClientFactory(StoreRepository storeRepository, @@ -106,6 +106,7 @@ namespace BTCPayServer.Controllers.Greenfield greenfieldStoreAutomatedOnChainPayoutProcessorsController, GreenfieldStoreAutomatedLightningPayoutProcessorsController greenfieldStoreAutomatedLightningPayoutProcessorsController, + GreenfieldAppsController greenFieldAppsController, IServiceProvider serviceProvider) { _storeRepository = storeRepository; @@ -137,6 +138,7 @@ namespace BTCPayServer.Controllers.Greenfield greenfieldStoreAutomatedOnChainPayoutProcessorsController; _greenfieldStoreAutomatedLightningPayoutProcessorsController = greenfieldStoreAutomatedLightningPayoutProcessorsController; + _greenFieldAppsController = greenFieldAppsController; _serviceProvider = serviceProvider; } @@ -214,6 +216,7 @@ namespace BTCPayServer.Controllers.Greenfield _greenfieldPayoutProcessorsController, _greenfieldStoreAutomatedOnChainPayoutProcessorsController, _greenfieldStoreAutomatedLightningPayoutProcessorsController, + _greenFieldAppsController, new LocalHttpContextAccessor() {HttpContext = context} ); } @@ -259,6 +262,8 @@ namespace BTCPayServer.Controllers.Greenfield private readonly GreenfieldStoreUsersController _greenfieldStoreUsersController; + private readonly GreenfieldAppsController _greenFieldAppsController; + public LocalBTCPayServerClient( IServiceProvider serviceProvider, GreenfieldStoreOnChainPaymentMethodsController chainPaymentMethodsController, @@ -287,6 +292,7 @@ namespace BTCPayServer.Controllers.Greenfield greenfieldStoreAutomatedOnChainPayoutProcessorsController, GreenfieldStoreAutomatedLightningPayoutProcessorsController greenfieldStoreAutomatedLightningPayoutProcessorsController, + GreenfieldAppsController greenFieldAppsController, IHttpContextAccessor httpContextAccessor) : base(new Uri("https://dummy.local"), "", "") { _chainPaymentMethodsController = chainPaymentMethodsController; @@ -315,6 +321,7 @@ namespace BTCPayServer.Controllers.Greenfield greenfieldStoreAutomatedOnChainPayoutProcessorsController; _greenfieldStoreAutomatedLightningPayoutProcessorsController = greenfieldStoreAutomatedLightningPayoutProcessorsController; + _greenFieldAppsController = greenFieldAppsController; var controllers = new[] { @@ -1278,5 +1285,24 @@ namespace BTCPayServer.Controllers.Greenfield await _greenfieldPullPaymentController .GetStorePayouts(storeId, includeCancelled)); } + + public override async Task CreatePointOfSaleApp( + string storeId, + CreatePointOfSaleAppRequest request, CancellationToken token = default) + { + return GetFromActionResult( + await _greenFieldAppsController.CreatePointOfSaleApp(storeId, request)); + } + + public override async Task GetApp(string appId, CancellationToken token = default) + { + return GetFromActionResult( + await _greenFieldAppsController.GetApp(appId)); + } + + public override async Task DeleteApp(string appId, CancellationToken token = default) + { + HandleActionResult(await _greenFieldAppsController.DeleteApp(appId)); + } } } diff --git a/BTCPayServer/wwwroot/swagger/v1/swagger.template.apps.json b/BTCPayServer/wwwroot/swagger/v1/swagger.template.apps.json index f6f4c0996..135b63e72 100644 --- a/BTCPayServer/wwwroot/swagger/v1/swagger.template.apps.json +++ b/BTCPayServer/wwwroot/swagger/v1/swagger.template.apps.json @@ -170,11 +170,77 @@ } ] } + }, + "/api/v1/apps/{appId}": { + "get": { + "tags": [ + "Apps" + ], + "operationId": "Apps_GetPointOfSaleApp", + "summary": "Get basic app data", + "description": "Returns basic app data shared between all types of apps", + "responses": { + "200": { + "description": "Basic app data", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BasicAppData" + } + } + } + }, + "404": { + "description": "App with specified ID was not found" + } + }, + "security": [ + { + "API_Key": [ + "btcpay.store.canmodifystoresettings" + ], + "Basic": [] + } + ] + }, + "delete": { + "tags": [ + "Apps" + ], + "operationId": "Apps_DeletePointOfSaleApp", + "summary": "Delete app", + "description": "Deletes apps with specified ID", + "responses": { + "200": { + "description": "App was deleted" + }, + "404": { + "description": "App with specified ID was not found" + } + }, + "security": [ + { + "API_Key": [ + "btcpay.store.canmodifystoresettings" + ], + "Basic": [] + } + ] + } } }, "components": { "schemas": { "PointOfSaleAppData": { + "allOf": [ + { + "$ref": "#/components/schemas/BasicAppData" + }, + { + } + ] + }, + "BasicAppData": { "type": "object", "properties": { "id": { @@ -200,7 +266,7 @@ "appType": { "type": "string", "example": "PointOfSale", - "description": "Type of the app which was created (will always \"PointOfSale\" in this case" + "description": "Type of the app which was created" } } }