Compare commits

...

73 Commits
u2f ... master

Author SHA1 Message Date
Juraj Bednar
bb8d10a0b9
Update README.md (#112)
@pavlenex said on btcpayserver chat that this is not maintained, probably a good idea to mention it in the documentation, since from docs it seems that it is alpha that is going to receive some care.
2024-10-21 15:51:03 +02:00
Kukks
b19665152d bump 2021-11-05 11:08:12 +01:00
Kukks
561c676758 use custom exchangesharp until they merge PR 2021-11-05 11:08:08 +01:00
Kukks
6699e73ed6 bump 2021-10-05 14:13:58 +02:00
Kukks
c5dfbf336e Bump ExchangeSharp 2021-10-05 14:13:38 +02:00
Kukks
e74a996424 bump 2021-07-12 08:44:52 +02:00
juandelcid
96c3747ad9 Send MarketSymbol on GetOrderDetailsAsync (#95)
Binance requires MarketSymbol when you query an order.
2021-07-12 08:44:00 +02:00
Kukks
c7fe58200f small fixes and adjustments around btcpay auth 2021-03-18 09:38:14 +01:00
Pavlenex
8e3051e61b
Merge pull request #83 from MaxHillebrand/patch-1
[typo] remove double word
2021-02-03 16:43:42 +01:00
Pavlenex
d84d4bdf7f
Merge pull request #72 from britttttk/readme-dca
Add DCA to readme
2021-02-03 16:43:31 +01:00
Max Hillebrand
003c261909
[typo] remove double word 2021-02-03 16:22:42 +01:00
Dennis Reimann
4dc82b81c5
Fix headline level (#80)
Noticed this while working on the docs search.
2020-12-04 10:26:52 +01:00
britttttk
d36ead30a7
fix case sensitive command and add link to FAQ (#71) 2020-10-13 09:00:54 +02:00
Britt Kelly
4cb2a23900 Add DCA to readme 2020-09-11 18:19:16 -06:00
ɹǝƃıǝ⅁ ɯo⊥
b3faa84cf3
Prevent model invalidation after sent email (#66)
Removed the confirmation message as it invalidates the model, preventing it from being saved.
2020-08-06 09:25:30 +02:00
Kukks
b413816797 set first user to admin even if from btcpay 2020-07-17 12:37:05 +02:00
Kukks
048b59c261 set default mode to list
closes #53
2020-07-17 12:36:49 +02:00
Kukks
7de6647e65 Refactor LoginWithBtcPay 2020-07-17 11:35:32 +02:00
Kukks
f4a837f36f fix login 2020-07-16 10:38:02 +02:00
Kukks
140d2c206a Login with BTCPay feature 2020-07-15 14:40:43 +02:00
Kukks
50c1651cba update 2020-06-21 14:07:47 +02:00
Kukks
a497084b34 fix build 2020-06-21 13:47:18 +02:00
Kukks
051a27fd86 bump version 2020-06-21 13:38:23 +02:00
Kukks
124982bd94 fix plenty of warnings 2020-06-21 13:37:51 +02:00
Kukks
97f682115c Add DCA guide 2020-06-21 13:28:03 +02:00
Kukks
39bd13bb25 use custom exchanegsharp until next release 2020-06-21 12:54:31 +02:00
Kukks
2ef1e353e7 Add DCA preset 2020-06-21 09:56:55 +02:00
Kukks
8334969f93 bump packages 2020-06-21 09:15:18 +02:00
Kukks
21ecf986f1 fix algolia autocomplete search dark theme color 2020-06-21 09:13:11 +02:00
Kukks
cb6fa66300 opt out telemetry 2020-06-21 08:52:08 +02:00
Dennis Reimann
32f21e5306
Fix links (#64)
Found these while working on a broken link check for the docs.
2020-06-18 07:28:11 +02:00
Dennis Reimann
5d0b771a36
Apply design guidelines (#62)
* Add design basics

* Minor footer improvement
2020-06-16 17:26:03 +02:00
Kukks
435ebf8909 bump packages 2020-05-27 09:17:49 +02:00
Kukks
323f7e8481 remove debug code 2020-05-27 08:46:52 +02:00
Kukks
2d7911bc95 bump 2020-05-26 12:08:58 +02:00
Kukks
ce7a70bb99 fix logs styling 2020-05-26 12:08:50 +02:00
Kukks
9b4fb02241 bump dynamic linq and set JObject type on receive webhook 2020-05-26 12:05:07 +02:00
Kukks
aa68a90224 Allow a webhook receive trigger to accept any http method 2020-05-26 12:04:36 +02:00
Kukks
236f416fc6 do not show individual actions or allow creation of them anymore 2020-05-26 10:46:12 +02:00
Kukks
36fbd57209 fix api stuff 2020-05-26 10:32:19 +02:00
Kukks
28c2bac049 fix badge styling 2020-05-26 09:57:56 +02:00
Kukks
2c6ace2a95 set basic auth to correct scheme 2020-05-25 09:13:51 +02:00
Kukks
a9d5a2fadb fix webhook receive 2020-05-24 15:54:17 +02:00
Kukks
215d311113 wip 2020-05-24 15:54:17 +02:00
Andrew Camilleri
e598cc10d2
Update config.yml 2020-05-14 17:40:26 +02:00
Dennis Reimann
25383e196b
Use relative asset links in docs (#61)
This way [Vuepress can handle them correctly](https://vuepress.vuejs.org/guide/assets.html#relative-urls) and they work on GitHub too.
2020-05-08 15:27:06 +02:00
Andrew Camilleri
d5c84fa491
Update config.yml 2020-04-30 20:03:13 +02:00
Andrew Camilleri
05b64186fb
Update config.yml 2020-04-30 19:15:11 +02:00
Andrew Camilleri
a75c275c10
test docs trigger 2020-04-30 19:14:00 +02:00
britttttk
1ca2119138
Add demo guide for Transmuter email receipt preset (#52)
* Add ticket demo guide

* Add Presets to index.md doc

* move new section to readme

Co-authored-by: Kukks <evilkukka@gmail.com>
2020-04-28 09:50:38 +02:00
Andrew Camilleri
0138d9712b
Merge pull request #58 from Eskyee/patch-1
fixed typo missing dot and whitespace
2020-04-26 13:20:26 +02:00
Esky33
19db0dff46
fixed typo missing dot and whitespace 2020-04-26 11:59:07 +01:00
Andrew Camilleri
f21a4ac701
Merge pull request #56 from 2pac1/master
Update README.md
2020-04-15 17:06:53 +02:00
2pac1
0295dd1e87
Update README.md 2020-04-15 16:54:32 +02:00
Kukks
1293f03182 Make basic auth always an api 2020-04-06 08:40:16 +02:00
Kukks
d56356d2f3 make the scheme use the same one as cookie 2020-04-05 16:53:05 +02:00
Kukks
ded2313463 recipe actions api 2020-04-05 10:28:14 +02:00
Kukks
e7c48e2db7 Make add action group a post action 2020-04-05 10:18:59 +02:00
Kukks
ddfc647300 newtonsoft json for mvc 2020-04-04 14:32:10 +02:00
Kukks
5902da4212 Add ReDoc and swagger 2020-04-04 14:27:27 +02:00
Kukks
774007e5c7 fix authorization for api 2020-04-04 14:05:13 +02:00
Kukks
c47efa0b43 Recipes API 2020-04-02 15:27:06 +02:00
Kukks
c8fee0e18f make cookie default scheme 2020-04-02 14:28:55 +02:00
Kukks
c3b73c7e4d fix auth selector 2020-04-02 13:43:40 +02:00
Kukks
0e7a7a5d70 Add role claims when authenticated 2020-04-02 12:05:14 +02:00
Kukks
08a3329eff Add Basic Auth option 2020-04-02 11:58:52 +02:00
Kukks
5b29c62fc6 Cleanup Identity pages 2020-04-02 11:58:46 +02:00
Kukks
8ad099bcce Add Blob to User 2020-04-02 10:51:10 +02:00
Kukks
2fba3da43e fixes #54 2020-03-10 12:35:13 +01:00
Kukks
9649294bbf fix issue with operators plugin 2020-03-02 12:56:08 +01:00
Kukks
c555df3054 bump 2020-02-27 11:38:05 +01:00
Kukks
78ca88b030 add fingerprint and acc key path to wallet gen helper 2020-02-27 11:37:07 +01:00
Kukks
7ffaa89f55 move and update docs 2020-02-27 11:13:54 +01:00
174 changed files with 10869 additions and 7410 deletions

View File

@ -80,12 +80,26 @@ jobs:
sudo docker manifest annotate $DOCKERHUB_REPO:$LATEST_TAG $DOCKERHUB_REPO:$LATEST_TAG-arm32v7 --os linux --arch arm --variant v7
sudo docker manifest annotate $DOCKERHUB_REPO:$LATEST_TAG $DOCKERHUB_REPO:$LATEST_TAG-arm64v8 --os linux --arch arm64 --variant v8
sudo docker manifest push $DOCKERHUB_REPO:$LATEST_TAG -p
trigger_docs_build:
machine:
enabled: true
image: circleci/classic:201808-01
steps:
- run:
command: |
curl -X POST -H "Authorization: token $GH_PAT" -H "Accept: application/vnd.github.everest-preview+json" -H "Content-Type: application/json" https://api.github.com/repos/btcpayserver/btcpayserver-doc/dispatches --data '{"event_type": "build_docs"}'
workflows:
version: 2
publish:
jobs:
- trigger_docs_build:
filters:
branches:
only: master
# only act on version tags
tags:
only: /v[0-9]+(\.[0-9]+)*/
- publish_docker_linuxamd64:
filters:
# ignore any commit on any branch by default

View File

@ -1,7 +1,7 @@
@inject BTCTransmuter.Extension.Tor.Services.TorServices TorServices
@if (TorServices.TransmuterTorService != null)
{
<li class="nav-item mr-3">
<a href="@TorServices.TransmuterTorService.OnionHost">Onion</a>
<li class="nav-item">
<a href="@TorServices.TransmuterTorService.OnionHost" class="nav-link">Onion</a>
</li>
}

View File

@ -7,12 +7,12 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="McMaster.NETCore.Plugins" Version="1.1.0" />
<PackageReference Include="McMaster.NETCore.Plugins.Mvc" Version="1.1.0" />
<PackageReference Include="Microsoft.Extensions.Identity.Stores" Version="3.1.1" />
<PackageReference Include="NetCore.AutoRegisterDi" Version="1.1.0" />
<PackageReference Include="McMaster.NETCore.Plugins" Version="1.3.0" />
<PackageReference Include="McMaster.NETCore.Plugins.Mvc" Version="1.3.0" />
<PackageReference Include="Microsoft.Extensions.Identity.Stores" Version="3.1.4" />
<PackageReference Include="NetCore.AutoRegisterDi" Version="2.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="System.Linq.Dynamic.Core" Version="1.0.20" />
<PackageReference Include="System.Linq.Dynamic.Core" Version="1.1.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\BtcTransmuter.Data\BtcTransmuter.Data.csproj" />

View File

@ -1,3 +1,5 @@
using System;
namespace BtcTransmuter
{
public interface IBtcTransmuterOptions
@ -7,5 +9,7 @@ namespace BtcTransmuter
string DataProtectionDir { get; set; }
DatabaseType DatabaseType { get; set; }
bool UseDatabaseColumnEncryption { get; set; }
bool DisableInternalAuth { get; set; }
Uri BTCPayAuthServer { get; set; }
}
}

View File

@ -0,0 +1,57 @@
using System.Collections.Generic;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
namespace BtcTransmuter.Abstractions.Extensions
{
public static class ControllerExtensions
{
public static bool IsApi(this Controller controller)
{
return controller.HttpContext.IsApi();
}
public static bool IsApi(this HttpContext context)
{
if (context.Items.TryGetValue("API", out var val))
{
return val is true;
}
return false;
}
public static void SetIsApi(this HttpContext context, bool val)
{
context.Items.TryAdd("API", val);
}
public static IActionResult ViewOrJson<T>(this Controller controller, string viewName, T payload)
{
if (controller.IsApi())
{
return controller.Json(payload);
}
return controller.View(viewName, payload);
}
public static IActionResult ViewOrJson<T>(this Controller controller, T payload)
{
if (controller.IsApi())
{
return controller.Json(payload);
}
return controller.View(payload);
}
public static IActionResult ViewOrBadRequest<T>(this Controller controller, T payload, bool usePayloadInBadRequest = false)
{
if (controller.IsApi())
{
return usePayloadInBadRequest ? controller.BadRequest(payload) : controller.BadRequest(controller.ModelState);
}
return controller.View(payload);
}
}
}

View File

@ -0,0 +1,16 @@
using Microsoft.AspNetCore.Http;
namespace BtcTransmuter.Abstractions.Extensions
{
public static class RequestExtensions
{
public static string GetAbsoluteRoot(this HttpRequest request)
{
return string.Concat(
request.Scheme,
"://",
request.Host.ToUriComponent(),
request.PathBase.ToUriComponent());
}
}
}

View File

@ -35,7 +35,7 @@ namespace BtcTransmuter.Abstractions.ExternalServices
{
return result.Error;
}
return View(await BuildViewModel(result.Data));
}

View File

@ -62,7 +62,7 @@ namespace BtcTransmuter.Abstractions.Helpers
return (e.Compile().DynamicInvoke(data.Values.ToArray()) ?? "").ToString();
}
catch (Exception exception)
catch (Exception)
{
return processed;
}

View File

@ -8,11 +8,11 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.DataProtection.Abstractions" Version="3.1.1" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="3.1.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="3.1.0" />
<PackageReference Include="Microsoft.AspNetCore.DataProtection.Abstractions" Version="3.1.4" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="3.1.4" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="3.1.4" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.4" />
</ItemGroup>
</Project>

View File

@ -1,10 +1,26 @@
using System.Collections.Generic;
using BtcTransmuter.Data.Encryption;
using BtcTransmuter.Data.Models;
using Microsoft.AspNetCore.Identity;
namespace BtcTransmuter.Data.Entities
{
public class User : IdentityUser
public class User : IdentityUser, IHasJsonData
{
public List<Recipe> Recipes { get; set; }
[Encrypted] public string DataJson { get; set; }
}
public class UserBlob
{
public bool BasicAuth { get; set; }
public BTCPayAuthDetails BTCPayAuthDetails { get; set; } = new BTCPayAuthDetails();
}
public class BTCPayAuthDetails
{
public string UserId { get; set; }
public string AccessToken { get; set; }
}
}

View File

@ -9,9 +9,9 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="3.1.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.4.0" />
<PackageReference Include="Moq" Version="4.13.1" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="3.1.4" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.6.1" />
<PackageReference Include="Moq" Version="4.14.1" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
</ItemGroup>

View File

@ -4,13 +4,13 @@
<AddRazorSupportForMvc>true</AddRazorSupportForMvc>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\BtcTransmuter.Abstractions\BtcTransmuter.Abstractions.csproj"/>
<ProjectReference Include="..\BtcTransmuter.Extension.DynamicServices\BtcTransmuter.Extension.DynamicServices.csproj"/>
<ProjectReference Include="..\BtcTransmuter.Abstractions\BtcTransmuter.Abstractions.csproj" />
<ProjectReference Include="..\BtcTransmuter.Extension.DynamicServices\BtcTransmuter.Extension.DynamicServices.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="3.1.1"/>
<PackageReference Include="NBitcoin" Version="5.0.13"/>
<PackageReference Include="NBitpayClient" Version="1.0.0.37"/>
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="3.1.4" />
<PackageReference Include="NBitcoin" Version="5.0.40" />
<PackageReference Include="NBitpayClient" Version="1.0.0.38" />
</ItemGroup>
</Project>

View File

@ -52,7 +52,7 @@ namespace BtcTransmuter.Extension.BtcPayServer.ExternalServices.BtcPayServer
var client = ConstructClient();
return client != null && await client.TestAccessAsync(Facade.Merchant);
}
catch (Exception e)
catch (Exception)
{
return false;
}

View File

@ -9,9 +9,9 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="3.1.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.4.0" />
<PackageReference Include="Moq" Version="4.13.1" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="3.1.4" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.6.1" />
<PackageReference Include="Moq" Version="4.14.1" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
</ItemGroup>

View File

@ -5,9 +5,9 @@
<OutputType>Library</OutputType>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\BtcTransmuter.Abstractions\BtcTransmuter.Abstractions.csproj"/>
<ProjectReference Include="..\BtcTransmuter.Abstractions\BtcTransmuter.Abstractions.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="3.1.1"/>
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="3.1.4" />
</ItemGroup>
</Project>

View File

@ -9,9 +9,9 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="3.1.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.4.0" />
<PackageReference Include="Moq" Version="4.13.1" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="3.1.4" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.6.1" />
<PackageReference Include="Moq" Version="4.14.1" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
</ItemGroup>

View File

@ -5,11 +5,11 @@
<OutputType>Library</OutputType>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\BtcTransmuter.Abstractions\BtcTransmuter.Abstractions.csproj"/>
<ProjectReference Include="..\BtcTransmuter.Extension.DynamicServices\BtcTransmuter.Extension.DynamicServices.csproj"/>
<ProjectReference Include="..\BtcTransmuter.Abstractions\BtcTransmuter.Abstractions.csproj" />
<ProjectReference Include="..\BtcTransmuter.Extension.DynamicServices\BtcTransmuter.Extension.DynamicServices.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="MailKit" Version="2.4.1"/>
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="3.1.1"/>
<PackageReference Include="MailKit" Version="2.6.0" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="3.1.4" />
</ItemGroup>
</Project>

View File

@ -51,7 +51,6 @@ namespace BtcTransmuter.Extension.Email.ExternalServices.Smtp
var error = await SendTestEmail(smtpService, viewModel.TestEmail);
if (string.IsNullOrEmpty(error))
{
ModelState.AddModelError(nameof(viewModel.TestEmail), "Email sent successfully, confirm that you received it");
viewModel.TestEmail = string.Empty;
}
else
@ -99,4 +98,4 @@ namespace BtcTransmuter.Extension.Email.ExternalServices.Smtp
[Display(Name = "Send test email from and to this address to check if your settings are valid")]
public string TestEmail { get; set; }
}
}
}

View File

@ -9,9 +9,9 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="3.1.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.4.0" />
<PackageReference Include="Moq" Version="4.13.1" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="3.1.4" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.6.1" />
<PackageReference Include="Moq" Version="4.14.1" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
</ItemGroup>

View File

@ -2,6 +2,7 @@ using System;
using BtcTransmuter.Data.Entities;
using BtcTransmuter.Data.Models;
using System.Linq;
using System.Threading.Tasks;
using BtcTransmuter.Extension.Exchange.ExternalServices.Exchange;
using BtcTransmuter.Tests.Base;
using Xunit;
@ -12,9 +13,9 @@ namespace BtcTransmuter.Extension.Exchange.Tests
public class ExchangeServiceTests:BaseExternalServiceTest<ExchangeService,ExchangeExternalServiceData >
{
[Fact]
public void ExchangeService_GetAvailableExchanges()
public async Task ExchangeService_GetAvailableExchanges()
{
Assert.True(ExchangeService.GetAvailableExchanges().Any());
Assert.True((await ExchangeService.GetAvailableExchanges()).Any());
}
[Fact]
@ -49,7 +50,7 @@ namespace BtcTransmuter.Extension.Exchange.Tests
}
[Fact]
public void ExchangeService_CanConstructClient()
public async Task ExchangeService_CanConstructClient()
{
var InvalidData = new ExchangeExternalServiceData()
{
@ -64,7 +65,7 @@ namespace BtcTransmuter.Extension.Exchange.Tests
externalServiceData.Set(InvalidData);
var exchangeService = GetExternalService(externalServiceData);
Assert.ThrowsAny<Exception>(() => exchangeService.ConstructClient());
await Assert.ThrowsAnyAsync<Exception>(async () => await exchangeService.ConstructClient());
var validData = new ExchangeExternalServiceData()

View File

@ -36,7 +36,7 @@ namespace BtcTransmuter.Extension.Exchange.Actions.GetExchangeBalance
var serviceData =
await _externalServiceManager.GetExternalServiceData(externalServiceId, GetUserId());
var exchangeService = new ExchangeService(serviceData);
var symbols = await exchangeService.ConstructClient().GetCurrenciesAsync();
var symbols = await (await exchangeService.ConstructClient()).GetCurrenciesAsync();
return symbols.Keys.ToArray();
}

View File

@ -28,7 +28,7 @@ namespace BtcTransmuter.Extension.Exchange.Actions.GetExchangeBalance
var externalService = await recipeAction.GetExternalService();
var exchangeService = new ExchangeService(externalService);
var client = exchangeService.ConstructClient();
var client = await exchangeService.ConstructClient();
var result = await client.GetAmountsAsync();

View File

@ -55,7 +55,7 @@ namespace BtcTransmuter.Extension.Exchange.Actions.GetExchangeRate
var serviceData =
await _externalServiceManager.GetExternalServiceData(viewModel.ExternalServiceId, GetUserId());
var exchangeService = new ExchangeService(serviceData);
var symbols = (await exchangeService.ConstructClient().GetMarketSymbolsAsync()).ToArray();
var symbols = (await (await exchangeService.ConstructClient()).GetMarketSymbolsAsync()).ToArray();
if (symbols.Contains(viewModel.MarketSymbol))
{
mainModel.ExternalServiceId = viewModel.ExternalServiceId;

View File

@ -26,7 +26,7 @@ namespace BtcTransmuter.Extension.Exchange.Actions.GetExchangeRate
{
var externalService = await recipeAction.GetExternalService();
var exchangeService = new ExchangeService(externalService);
var client = exchangeService.ConstructClient();
var client = await exchangeService.ConstructClient();
var result = await client.GetTickerAsync(actionData.MarketSymbol);

View File

@ -38,7 +38,7 @@ namespace BtcTransmuter.Extension.Exchange.Actions.PlaceOrder
var serviceData =
await _externalServiceManager.GetExternalServiceData(externalServiceId, GetUserId());
var exchangeService = new ExchangeService(serviceData);
return (await exchangeService.ConstructClient().GetMarketSymbolsAsync()).ToArray();
return (await (await exchangeService.ConstructClient()).GetMarketSymbolsAsync()).ToArray();
}
protected override async Task<PlaceOrderViewModel> BuildViewModel(RecipeAction from)
@ -79,7 +79,7 @@ namespace BtcTransmuter.Extension.Exchange.Actions.PlaceOrder
var serviceData =
await _externalServiceManager.GetExternalServiceData(viewModel.ExternalServiceId, GetUserId());
var exchangeService = new ExchangeService(serviceData);
var symbols = (await exchangeService.ConstructClient().GetMarketSymbolsAsync()).ToArray();
var symbols = (await (await exchangeService.ConstructClient()).GetMarketSymbolsAsync()).ToArray();
if (symbols.Contains(viewModel.MarketSymbol))
{
mainModel.ExternalServiceId = viewModel.ExternalServiceId;

View File

@ -26,7 +26,7 @@ namespace BtcTransmuter.Extension.Exchange.Actions.PlaceOrder
{
var externalService = await recipeAction.GetExternalService();
var exchangeService = new ExchangeService(externalService);
var client = exchangeService.ConstructClient();
var client = await exchangeService.ConstructClient();
var orderRequest = new ExchangeOrderRequest()
{
MarketSymbol = actionData.MarketSymbol,
@ -43,7 +43,7 @@ namespace BtcTransmuter.Extension.Exchange.Actions.PlaceOrder
{
var result = await client.PlaceOrderAsync(orderRequest);
System.Threading.Thread.Sleep(500);
result = await client.GetOrderDetailsAsync(result.OrderId);
result = await client.GetOrderDetailsAsync(result.OrderId, orderRequest.MarketSymbol);
return new TypedActionHandlerResult<ExchangeOrderResult>()
{
Executed = true,
@ -63,4 +63,4 @@ namespace BtcTransmuter.Extension.Exchange.Actions.PlaceOrder
}
}
}
}
}

View File

@ -4,9 +4,6 @@
<AddRazorSupportForMvc>true</AddRazorSupportForMvc>
<OutputType>Library</OutputType>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="DigitalRuby.ExchangeSharp" Version="0.6.4" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\BtcTransmuter.Abstractions\BtcTransmuter.Abstractions.csproj" />
<ProjectReference Include="..\BtcTransmuter.Extension.DynamicServices\BtcTransmuter.Extension.DynamicServices.csproj" />
@ -14,4 +11,7 @@
<ItemGroup>
<UpToDateCheckInput Remove="Views\SendEmail\EditData.cshtml" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Custom.DigitalRuby.ExchangeSharp" Version="0.9.0" />
</ItemGroup>
</Project>

View File

@ -25,10 +25,10 @@ namespace BtcTransmuter.Extension.Exchange.ExternalServices.Exchange
protected override string ExternalServiceType => ExchangeService.ExchangeServiceType;
protected override Task<EditExchangeExternalServiceDataViewModel> BuildViewModel(ExternalServiceData data)
protected override async Task<EditExchangeExternalServiceDataViewModel> BuildViewModel(ExternalServiceData data)
{
return Task.FromResult(new EditExchangeExternalServiceDataViewModel(new ExchangeService(data).GetData(),
ExchangeService.GetAvailableExchanges()));
return new EditExchangeExternalServiceDataViewModel(new ExchangeService(data).GetData(),
await ExchangeService.GetAvailableExchanges());
}
protected override async
@ -41,7 +41,7 @@ namespace BtcTransmuter.Extension.Exchange.ExternalServices.Exchange
if (!ModelState.IsValid)
{
return (null,
new EditExchangeExternalServiceDataViewModel(viewModel, ExchangeService.GetAvailableExchanges()));
new EditExchangeExternalServiceDataViewModel(viewModel, await ExchangeService.GetAvailableExchanges()));
}
//current External Service data
@ -54,7 +54,7 @@ namespace BtcTransmuter.Extension.Exchange.ExternalServices.Exchange
"Could not connect with current settings. Transmuter tests against fetching your balance amount from the exchange so you would need to enable that option if available");
return (null,
new EditExchangeExternalServiceDataViewModel(viewModel, ExchangeService.GetAvailableExchanges()));
new EditExchangeExternalServiceDataViewModel(viewModel, await ExchangeService.GetAvailableExchanges()));
}
return (externalServiceData, null);

View File

@ -25,17 +25,17 @@ namespace BtcTransmuter.Extension.Exchange.ExternalServices.Exchange
{
}
public static IExchangeAPI[] GetAvailableExchanges()
public static async Task<IExchangeAPI[]> GetAvailableExchanges()
{
return ExchangeAPI.GetExchangeAPIs();
return await ExchangeAPI.GetExchangeAPIsAsync();
}
public ExchangeAPI ConstructClient()
public async Task<ExchangeAPI> ConstructClient()
{
var data = GetData();
var result = ExchangeAPI.GetExchangeAPI(data.ExchangeName);
var result = await ExchangeAPI.GetExchangeAPIAsync(data.ExchangeName);
if (result is ExchangeAPI api)
{
if (!string.IsNullOrEmpty(data.OverrideUrl))
@ -52,7 +52,7 @@ namespace BtcTransmuter.Extension.Exchange.ExternalServices.Exchange
public async Task<bool> TestAccess()
{
var client = ConstructClient();
var client = await ConstructClient();
if (client == null)
{
return false;

View File

@ -52,7 +52,7 @@ namespace BtcTransmuter.Extension.NBXplorer.HostedServices
await Task.WhenAll(exchangeExternalServices.Select(async data =>
{
var exchangeService = new ExchangeService(data);
var client = exchangeService.ConstructClient();
var client = await exchangeService.ConstructClient();
var amounts = await client.GetAmountsAsync();
foreach (var keyValuePair in amounts)
{

View File

@ -37,7 +37,7 @@ namespace BtcTransmuter.Extension.Exchange.Triggers.CheckExchangeBalance
var serviceData =
await _externalServiceManager.GetExternalServiceData(externalServiceId, GetUserId());
var exchangeService = new ExchangeService(serviceData);
var symbols = await exchangeService.ConstructClient().GetCurrenciesAsync();
var symbols = await (await exchangeService.ConstructClient()).GetCurrenciesAsync();
return symbols.Keys.ToArray();
}

View File

@ -10,9 +10,9 @@
<ItemGroup>
<PackageReference Include="MaxKagamine.Moq.HttpClient" Version="1.1.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="3.1.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.4.0" />
<PackageReference Include="Moq" Version="4.13.1" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="3.1.4" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.6.1" />
<PackageReference Include="Moq" Version="4.14.1" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
</ItemGroup>

View File

@ -5,8 +5,8 @@
<OutputType>Library</OutputType>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.1.8" />
<PackageReference Include="NBitcoin" Version="5.0.13" />
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.1.15" />
<PackageReference Include="NBitcoin" Version="5.0.40" />
<ProjectReference Include="..\BtcTransmuter.Abstractions\BtcTransmuter.Abstractions.csproj" />
<ProjectReference Include="..\BtcTransmuter.Extension.DynamicServices\BtcTransmuter.Extension.DynamicServices.csproj" />
<ProjectReference Include="..\BtcTransmuter.Extension.NBXplorer\BtcTransmuter.Extension.NBXplorer.csproj" />

View File

@ -11,9 +11,9 @@
<ItemGroup>
<PackageReference Include="MaxKagamine.Moq.HttpClient" Version="1.1.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="3.1.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.4.0" />
<PackageReference Include="Moq" Version="4.13.1" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="3.1.4" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.6.1" />
<PackageReference Include="Moq" Version="4.14.1" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
</ItemGroup>

View File

@ -9,7 +9,7 @@
<ProjectReference Include="..\BtcTransmuter.Extension.DynamicServices\BtcTransmuter.Extension.DynamicServices.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="NBXplorer.Client" Version="3.0.2" />
<PackageReference Include="NBXplorer.Client" Version="3.0.15" />
</ItemGroup>
<ItemGroup>
<UpToDateCheckInput Remove="Views\NBXplorerCreatePSBT\EditData.cshtml" />

View File

@ -1,4 +1,3 @@
using System;
using System.Collections.Generic;
using System.Linq;
using BtcTransmuter.Extension.NBXplorer.Models;
@ -34,28 +33,30 @@ namespace BtcTransmuter.Extension.NBXplorer.Controllers
});
}
[HttpGet("{cryptoCode}/{mnemonic?}")]
public IActionResult GetWallet(string cryptoCode, string mnemonic)
[HttpPost("{cryptoCode}")]
public IActionResult GetWallet(string cryptoCode, [FromForm] string mnemonic)
{
if (string.IsNullOrEmpty(mnemonic))
{
return RedirectToAction("GetWallet", new
{
cryptoCode = cryptoCode,
mnemonic = new Mnemonic(Wordlist.English).ToString()
});
}
return ActionResult(cryptoCode, string.IsNullOrEmpty(mnemonic) ? null : new Mnemonic(mnemonic));
}
[HttpGet("{cryptoCode}")]
public IActionResult GetWallet(string cryptoCode)
{
return ActionResult(cryptoCode);
}
private IActionResult ActionResult(string cryptoCode, Mnemonic mnemonic = null)
{
var network = _nbXplorerClientProvider.GetClient(cryptoCode).Network;
var addressTypes = new Dictionary<ScriptPubKeyType, GetWalletViewModel.GetWalletViewModelAddressType>();
var mnemonicSeed = new Mnemonic(mnemonic);
var mnemonicSeed = mnemonic ?? new Mnemonic(Wordlist.English);
var extKey = mnemonicSeed.DeriveExtKey();
var wif = extKey.GetWif(network.NBitcoinNetwork);
var privateKey = extKey.PrivateKey;
var secret = privateKey.GetBitcoinSecret(network.NBitcoinNetwork);
if (network.NBitcoinNetwork.Consensus.SupportSegwit)
{
var segwitExtPubkey = extKey
@ -70,24 +71,26 @@ namespace BtcTransmuter.Extension.NBXplorer.Controllers
var segwit = _derivationSchemeParser.Parse(network.DerivationStrategyFactory, $"{segwitExtPubkey}");
var p2sh = _derivationSchemeParser.Parse(network.DerivationStrategyFactory, $"{p2shExtPubKey}-[p2sh]");
addressTypes.Add(ScriptPubKeyType.Segwit, new GetWalletViewModel.GetWalletViewModelAddressType()
{
Description = "BTCPay / BTCTransmuter compatible xpub for segwit addresses",
DerivationScheme = segwit.ToString(),
Addresses = GenerateAddresses(segwit, network),
RootKeyPath = NBXplorerPublicWallet.GetDerivationKeyPath(ScriptPubKeyType.Segwit, 0, network).ToString()
RootKeyPath ="m/" + NBXplorerPublicWallet.GetDerivationKeyPath(ScriptPubKeyType.Segwit, 0, network)
.ToString()
});
addressTypes.Add(ScriptPubKeyType.SegwitP2SH, new GetWalletViewModel.GetWalletViewModelAddressType()
{
Description = "BTCPay / BTCTransmuter compatible xpub for p2sh addresses",
DerivationScheme = p2sh.ToString(),
Addresses = GenerateAddresses(p2sh, network),
RootKeyPath = NBXplorerPublicWallet.GetDerivationKeyPath(ScriptPubKeyType.SegwitP2SH, 0, network).ToString()
RootKeyPath = "m/" + NBXplorerPublicWallet.GetDerivationKeyPath(ScriptPubKeyType.SegwitP2SH, 0, network)
.ToString()
});
}
var legacyExtPubkey = extKey
.Derive(NBXplorerPublicWallet.GetDerivationKeyPath(ScriptPubKeyType.Legacy, 0, network)).Neuter()
.ToString(network.NBitcoinNetwork);
@ -99,18 +102,19 @@ namespace BtcTransmuter.Extension.NBXplorer.Controllers
Description = "BTCPay / BTCTransmuter compatible xpub for legacy addresses",
DerivationScheme = legacy.ToString(),
Addresses = GenerateAddresses(legacy, network),
RootKeyPath = NBXplorerPublicWallet.GetDerivationKeyPath(ScriptPubKeyType.Legacy, 0, network).ToString()
RootKeyPath = "m/" +NBXplorerPublicWallet.GetDerivationKeyPath(ScriptPubKeyType.Legacy, 0, network).ToString()
});
return View(new GetWalletViewModel()
{
Mnemonic = mnemonic,
Mnemonic = mnemonicSeed.ToString(),
Network = network,
CryptoCode = cryptoCode,
CryptoCodes = _nbXplorerOptions.Cryptos,
PrivateKey = privateKey,
WIF = wif,
ExtPubKey = extKey.Neuter().ToString(network.NBitcoinNetwork),
Fingerprint = extKey.Neuter().PubKey.GetHDFingerPrint().ToString(),
AddressTypes = addressTypes,
Address = secret.GetAddress(ScriptPubKeyType.Legacy),
SegwitAddress = network.NBitcoinNetwork.Consensus.SupportSegwit
@ -118,7 +122,8 @@ namespace BtcTransmuter.Extension.NBXplorer.Controllers
: null,
P2SHAddress = network.NBitcoinNetwork.Consensus.SupportSegwit
? secret.GetAddress(ScriptPubKeyType.SegwitP2SH)
: null
: null,
});
}
@ -147,8 +152,8 @@ namespace BtcTransmuter.Extension.NBXplorer.Controllers
public Key PrivateKey { get; set; }
public BitcoinExtKey WIF { get; set; }
public string ExtPubKey { get; set; }
public Dictionary<ScriptPubKeyType,GetWalletViewModelAddressType> AddressTypes { get; set; }
public string Fingerprint { get; set; }
public Dictionary<ScriptPubKeyType, GetWalletViewModelAddressType> AddressTypes { get; set; }
public BitcoinAddress SegwitAddress { get; set; }
public BitcoinAddress Address { get; set; }
public BitcoinAddress P2SHAddress { get; set; }
@ -158,7 +163,7 @@ namespace BtcTransmuter.Extension.NBXplorer.Controllers
public string Description { get; set; }
public string DerivationScheme { get; set; }
public string RootKeyPath { get; set; }
public Dictionary<string , string> Addresses { get; set; }
public Dictionary<string, string> Addresses { get; set; }
}
}
}

View File

@ -95,7 +95,7 @@ namespace BtcTransmuter.Extension.NBXplorer.Services
return txBuilder.BuildTransaction(true);
}
public async Task<TransactionBuilder> AddTxOutsToTransaction(TransactionBuilder transactionBuilder,
private Task<TransactionBuilder> AddTxOutsToTransaction(TransactionBuilder transactionBuilder,
IEnumerable<(Money amount, IDestination destination, bool subtractFee)> outgoing)
{
foreach (var tuple in outgoing)
@ -119,7 +119,7 @@ namespace BtcTransmuter.Extension.NBXplorer.Services
break;
}
return transactionBuilder;
return Task.FromResult(transactionBuilder);
}
public static ExtKey GetKeyFromDetails(PrivateKeyDetails privateKeyDetails, Network network)

View File

@ -2,8 +2,8 @@
{
if (User.IsInRole("Admin"))
{
<li class="nav-item mr-3">
<a asp-controller="NBXplorerStatus" asp-action="GetSummaries">NBXplorer Status</a>
<li class="nav-item">
<a asp-controller="NBXplorerStatus" asp-action="GetSummaries" class="nav-link">NBXplorer Status</a>
</li>
}
}
}

View File

@ -20,7 +20,12 @@
<div class="card-body">
<div class="form-group">
<label class="control-label">Mnemonic Seed</label>
<input readonly class="form-control" value="@Model.Mnemonic"/>
<form class="input-group" method="post" asp-action="GetWallet" asp-route-cryptoCode="@Model.CryptoCode">
<input asp-for="Mnemonic" class="form-control"/>
<div class="input-group-append">
<button type="submit" class="btn btn-secondary">Load</button>
</div>
</form>
</div>
<div class="form-group">
<label class="control-label">Master Private Key</label>
@ -34,13 +39,17 @@
<label class="control-label">Master Extended Public key</label>
<input readonly class="form-control" value="@Model.ExtPubKey"/>
</div>
<div class="form-group">
<label class="control-label">Root Fingerprint</label>
<input readonly class="form-control" value="@Model.Fingerprint"/>
</div>
<div class="card">
<nav>
<div class="nav nav-tabs nav-fill" id="nav-tab" role="tablist">
@for (int i = 0; i < Model.AddressTypes.Count; i++)
{
var item = Model.AddressTypes.ElementAt(i);
<a class="nav-item nav-link @(i == 0? "active": "")" href="#tab-hd-@item.Key" id="nav-hd-@item.Key" data-toggle="tab" h role="tab">@item.Key</a>
@for (int i = 0; i < Model.AddressTypes.Count; i++)
{
var item = Model.AddressTypes.ElementAt(i);
<a class="nav-item nav-link @(i == 0 ? "active" : "")" href="#tab-hd-@item.Key" id="nav-hd-@item.Key" data-toggle="tab" h role="tab">@item.Key</a>
}
<a class="nav-item nav-link" href="#tab-singular" id="nav-singular" data-toggle="tab" role="tab">Singular Addresses</a>
@ -50,11 +59,15 @@
@for (int i = 0; i < Model.AddressTypes.Count; i++)
{
var item = Model.AddressTypes.ElementAt(i);
<div class="tab-pane p-2 @(i == 0? "show active": "")" id="tab-hd-@item.Key" role="tabpanel" id="tab-hd-@item.Key">
<div class="tab-pane p-2 @(i == 0 ? "show active" : "")" id="tab-hd-@item.Key" role="tabpanel" id="tab-hd-@item.Key">
<div class="form-group">
<label class="control-label">@item.Value.Description</label>
<input readonly class="form-control" value="@item.Value.DerivationScheme"/>
</div>
<div class="form-group">
<label class="control-label">Account Key Path</label>
<input readonly class="form-control" value="@item.Value.RootKeyPath"/>
</div>
<div class="form-group">
<table class="table table-sm table-responsive-md">
<thead>
@ -67,14 +80,15 @@
@foreach (var sample in item.Value.Addresses)
{
<tr>
<td><span class="text-muted ">@item.Value.RootKeyPath/</span>@sample.Key</td>
<td>
<span class="text-muted ">@item.Value.RootKeyPath/</span>@sample.Key</td>
<td>@sample.Value</td>
</tr>
}
</tbody>
</table>
</div>
</div>
</div>
}
<div class="tab-pane p-2" id="tab-singular" role="tabpanel">

View File

@ -11,9 +11,9 @@
<ItemGroup>
<PackageReference Include="MaxKagamine.Moq.HttpClient" Version="1.1.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="3.1.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.4.0" />
<PackageReference Include="Moq" Version="4.13.1" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="3.1.4" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.6.1" />
<PackageReference Include="Moq" Version="4.14.1" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
</ItemGroup>

View File

@ -24,14 +24,14 @@ namespace BtcTransmuter.Extension.Operators.Actions.ChooseNumericValue
{
}
protected override async Task<ChooseNumericValueViewModel> BuildViewModel(RecipeAction from)
protected override Task<ChooseNumericValueViewModel> BuildViewModel(RecipeAction from)
{
var fromData = from.Get<ChooseNumericValueData>();
return new ChooseNumericValueViewModel
return Task.FromResult(new ChooseNumericValueViewModel
{
RecipeId = @from.RecipeId,
Items = fromData.Items
};
});
}
protected override async Task<(RecipeAction ToSave, ChooseNumericValueViewModel showViewModel)> BuildModel(

View File

@ -18,7 +18,7 @@ namespace BtcTransmuter.Extension.Operators.Actions.ChooseNumericValue
public override string ControllerName => "ChooseNumericValue";
protected override async Task<TypedActionHandlerResult<string>> Execute(Dictionary<string, object> data, RecipeAction recipeAction,
protected override Task<TypedActionHandlerResult<string>> Execute(Dictionary<string, object> data, RecipeAction recipeAction,
ChooseNumericValueData actionData)
{
ChooseNumericValueData.ChooseNumericValueDataItem selectedItem = null;
@ -53,12 +53,12 @@ namespace BtcTransmuter.Extension.Operators.Actions.ChooseNumericValue
}
});
return new TypedActionHandlerResult<string>()
return Task.FromResult(new TypedActionHandlerResult<string>()
{
TypedData = selectedItem.ValueToChoose,
Executed = true,
Result = $"chose {selectedItem.ValueToChoose}"
};
});
}
}
}

View File

@ -24,14 +24,14 @@ namespace BtcTransmuter.Extension.Operators.Actions.Condition
{
}
protected override async Task<ConditionViewModel> BuildViewModel(RecipeAction from)
protected override Task<ConditionViewModel> BuildViewModel(RecipeAction from)
{
var fromData = from.Get<ConditionData>();
return new ConditionViewModel
return Task.FromResult(new ConditionViewModel
{
RecipeId = @from.RecipeId,
Condition = fromData.Condition
};
});
}
protected override async Task<(RecipeAction ToSave, ConditionViewModel showViewModel)> BuildModel(

View File

@ -18,18 +18,17 @@ namespace BtcTransmuter.Extension.Operators.Actions.Condition
public override string ControllerName => "Condition";
protected override async Task<TypedActionHandlerResult<string>> Execute(Dictionary<string, object> data, RecipeAction recipeAction,
protected override Task<TypedActionHandlerResult<string>> Execute(Dictionary<string, object> data, RecipeAction recipeAction,
ConditionData actionData)
{
var condition = InterpolateString(actionData.Condition, data);
return new TypedActionHandlerResult<string>()
return Task.FromResult(new TypedActionHandlerResult<string>()
{
TypedData = condition,
Executed = condition.Equals("true", StringComparison.InvariantCultureIgnoreCase),
Result = $"Data value was: {condition}"
};
});
}
}
}

View File

@ -0,0 +1 @@
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

View File

@ -10,5 +10,6 @@
<ProjectReference Include="..\BtcTransmuter.Extension.Email\BtcTransmuter.Extension.Email.csproj" />
<ProjectReference Include="..\BtcTransmuter.Extension.Exchange\BtcTransmuter.Extension.Exchange.csproj" />
<ProjectReference Include="..\BtcTransmuter.Extension.NBXplorer\BtcTransmuter.Extension.NBXplorer.csproj" />
<ProjectReference Include="..\BtcTransmuter.Extension.Timer\BtcTransmuter.Extension.Timer.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,202 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using BtcTransmuter.Abstractions.Extensions;
using BtcTransmuter.Abstractions.ExternalServices;
using BtcTransmuter.Abstractions.Recipes;
using BtcTransmuter.Data.Entities;
using BtcTransmuter.Data.Models;
using BtcTransmuter.Extension.Exchange.Actions.PlaceOrder;
using BtcTransmuter.Extension.Exchange.ExternalServices.Exchange;
using BtcTransmuter.Extension.Timer.Triggers.Timer;
using ExchangeSharp;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Newtonsoft.Json;
namespace BtcTransmuter.Extension.Presets
{
[Route("presets-plugin/presets/dca")]
[Authorize]
public class DCAController : Controller, ITransmuterPreset
{
private readonly IExternalServiceManager _externalServiceManager;
private readonly UserManager<User> _userManager;
private readonly IRecipeManager _recipeManager;
public string Id { get; } = "DCA";
public string Name { get; } = "Dollar Cost Average";
public string Description { get; } = "Schedule daily purchases of Bitcoin!";
public DCAController(
IExternalServiceManager externalServiceManager,
UserManager<User> userManager,
IRecipeManager recipeManager)
{
_externalServiceManager = externalServiceManager;
_userManager = userManager;
_recipeManager = recipeManager;
}
public (string ControllerName, string ActionName) GetLink()
{
return (Id, nameof(Create));
}
[HttpGet("create")]
public async Task<IActionResult> Create()
{
var services = await GetServices();
return View(new CreateDCAViewModel()
{
ExchangeServices = new SelectList(services, nameof(ExternalServiceData.Id), nameof(ExternalServiceData.Name))
});
}
private async Task<IEnumerable<ExternalServiceData>> GetServices()
{
var services = await _externalServiceManager.GetExternalServicesData(new ExternalServicesDataQuery()
{
UserId = _userManager.GetUserId(User),
Type = new[] {Exchange.ExternalServices.Exchange.ExchangeService.ExchangeServiceType}
});
var exchangeServices = services.Where(data => data.Type == Exchange.ExternalServices.Exchange.ExchangeService.ExchangeServiceType);
return exchangeServices;
}
[HttpPost("create")]
public async Task<IActionResult> Create(CreateDCAViewModel viewModel)
{
var services = await GetServices();
viewModel.ExchangeServices = new SelectList(services, nameof(ExternalServiceData.Id),
nameof(ExternalServiceData.Name));
if (viewModel.FiatAmount <= 0)
{
ModelState.AddModelError(nameof(viewModel.FiatAmount), "Amount needs to be more than 0.");
}
if (ModelState.IsValid)
{
var serviceData =
await _externalServiceManager.GetExternalServiceData(viewModel.SelectedExchangeServiceId, GetUserId());
var exchangeService = new ExchangeService(serviceData);
var symbols = (await (await exchangeService.ConstructClient()).GetMarketSymbolsAsync()).ToArray();
if (!symbols.Contains(viewModel.MarketSymbol))
{
viewModel.AddModelError(nameof(viewModel.MarketSymbol), $"The market symbols you entered is invalid. Please choose from the following: {string.Join(",", symbols)}", ModelState);
}
}
if (!ModelState.IsValid)
{
return View(viewModel);
}
return await SetItUp(viewModel);
}
protected string GetUserId()
{
return _userManager.GetUserId(User);
}
private async Task<IActionResult> SetItUp(CreateDCAViewModel vm)
{
var presetName = $"Generated_DCA";
var recipe = new Recipe()
{
Name = presetName,
Description = "Generated from a preset",
UserId = _userManager.GetUserId(User),
Enabled = false
};
await _recipeManager.AddOrUpdateRecipe(recipe);
var recipeTrigger = new RecipeTrigger()
{
TriggerId = new TimerTrigger().Id,
RecipeId = recipe.Id
};
recipeTrigger.Set(new TimerTriggerParameters()
{
StartOn = vm.StartOn,
TriggerEvery = vm.TriggerEvery,
TriggerEveryAmount = vm.TriggerEveryAmount
});
await _recipeManager.AddOrUpdateRecipeTrigger(recipeTrigger);
var recipeActionGroup = new RecipeActionGroup()
{
RecipeId = recipe.Id
};
await _recipeManager.AddRecipeActionGroup(recipeActionGroup);
var tradeAction = new RecipeAction()
{
RecipeId = recipe.Id,
RecipeActionGroupId = recipeActionGroup.Id,
ActionId = new PlaceOrderDataActionHandler().ActionId,
ExternalServiceId = vm.SelectedExchangeServiceId,
Order = 0,
DataJson = JsonConvert.SerializeObject(new PlaceOrderData()
{
Amount = vm.FiatAmount.ToString(CultureInfo.InvariantCulture),
IsBuy = vm.IsBuy,
MarketSymbol = vm.MarketSymbol,
OrderType = OrderType.Market
})
};
await _recipeManager.AddOrUpdateRecipeAction(tradeAction);
return RedirectToAction("EditRecipe", "Recipes", new
{
id = recipe.Id,
statusMessage =
"Preset generated. Recipe is currently disabled for now. Please verify details are correct before enabling!"
});
}
}
public class CreateDCAViewModel
{
public SelectList ExchangeServices { get; set; }
[Display(Name = "Existing Exchange Store")]
[Required]
public string SelectedExchangeServiceId { get; set; }
[Display(Name = "The trading pair on the exchange")]
[Required]
public string MarketSymbol { get; set; }
[Display(Name = "Is it a buy market order?")]
[Required]
public bool IsBuy { get; set; } = true;
[Display(Name = "How much do you want to buy?")]
[Required]
public decimal FiatAmount { get; set; }
[Required]
[Display(Name = "Trigger every")]
public int TriggerEveryAmount { get; set; } = 1;
[Required]
public TimerTriggerParameters.TimerResetEvery TriggerEvery { get; set; } =
TimerTriggerParameters.TimerResetEvery.Day;
[Display(Name = "Start from")]
public DateTime? StartOn { get; set; }
}
}

View File

@ -113,7 +113,7 @@ namespace BtcTransmuter.Extension.Presets
var serviceData =
await _externalServiceManager.GetExternalServiceData(condition.ExchangeServiceId, GetUserId());
var exchangeService = new ExchangeService(serviceData);
var symbols = (await exchangeService.ConstructClient().GetMarketSymbolsAsync()).ToArray();
var symbols = (await (await exchangeService.ConstructClient()).GetMarketSymbolsAsync()).ToArray();
if (!symbols.Contains(condition.MarketSymbol))
{
viewModel.AddModelError(

View File

@ -14,6 +14,7 @@ namespace BtcTransmuter.Extension.Presets
serviceCollection.AddTransient<ITransmuterPreset, PaymentForwarderController>();
serviceCollection.AddTransient<ITransmuterPreset, BTCPayEmailReceiptsController>();
serviceCollection.AddTransient<ITransmuterPreset, FiatExchangeConversionController>();
serviceCollection.AddTransient<ITransmuterPreset, DCAController>();
}
}
}

View File

@ -17,12 +17,12 @@ namespace BtcTransmuter.Extension.Presets
}
[HttpGet]
public async Task<IActionResult> ChoosePreset()
public Task<IActionResult> ChoosePreset()
{
return View(new ChoosePresetViewModel()
return Task.FromResult<IActionResult>(View(new ChoosePresetViewModel()
{
Presets = _transmuterPresets
});
}));
}
public class ChoosePresetViewModel
{

View File

@ -0,0 +1,126 @@
@using System.Globalization
@using BtcTransmuter.Extension.Exchange.ExternalServices.Exchange
@using BtcTransmuter.Extension.Timer.Triggers.Timer
@model BtcTransmuter.Extension.Presets.CreateDCAViewModel
@{
ViewData["Title"] = "Create automated Dollar Cost Averaging";
}
<h2>@ViewData["Title"]</h2>
<form method="post">
<div asp-validation-summary="All" class="text-danger"></div>
<div class="card mb-2">
<div class="card-body">
<h5 class="card-title">Choose your Exchange</h5>
<div class="form-group">
@if (!Model.ExchangeServices.Any())
{
<div class="list-group-item ">
<span class="text-danger">
There are no Exchange services connected to your Transmuter account.
<a asp-controller="ExternalServices" asp-action="CreateExternalService" asp-route-selectedType="@ExchangeService.ExchangeServiceType">Please create one first.</a>
</span>
</div>
}
else
{
<select asp-for="SelectedExchangeServiceId" asp-items="Model.ExchangeServices" class="form-control"></select>
<a asp-controller="ExternalServices" asp-action="CreateExternalService" asp-route-selectedType="@ExchangeService.ExchangeServiceType">Create</a>
}
<span asp-validation-for="SelectedExchangeServiceId" class="text-danger"></span>
</div>
</div>
</div>
<div class="card mb-2">
<div class="card-body">
<h5 class="card-title">When do you want to execute the trade?</h5>
<div class="form-group">
<label asp-for="StartOn" class="control-label"></label>
<div class="input-group">
<input type="datetime-local" asp-for="StartOn"
value="@(Model.StartOn?.ToString("u", CultureInfo.InvariantCulture))"
class="form-control flatdtpicker" placeholder="Start from"/>
<div class="input-group-append">
<button class="btn btn-secondary input-group-clear" type="button" title="Clear">
&times;
</button>
</div>
</div>
<span asp-validation-for="StartOn" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="TriggerEveryAmount" class="control-label"></label>
<div class="input-group">
<input type="number" asp-for="TriggerEveryAmount" placeholder="Amount" class="form-control">
<select class="custom-select" asp-for="TriggerEvery" asp-items="@Html.GetEnumSelectList(typeof(TimerTriggerParameters.TimerResetEvery))">
</select>
</div>
<span asp-validation-for="TriggerEveryAmount" class="text-danger"></span>
<span asp-validation-for="TriggerEvery" class="text-danger"></span>
</div>
</div>
</div>
<div class="card mb-2">
<div class="card-body">
<h5 class="card-title">Set up your trading conditions</h5>
<div class="form-group">
<label asp-for="MarketSymbol" class="control-label"></label>
<input asp-for="MarketSymbol" class="form-control autocomplete" data-datasrc="availableMarketSymbols" placeholder="Start typing to see a list of available trading pairs"/>
<span asp-validation-for="MarketSymbol" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="IsBuy" class="control-label"></label>
<input type="checkbox" asp-for="IsBuy" class="form-check"/>
<span asp-validation-for="IsBuy" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="FiatAmount" class="control-label"></label>
<input type="number" step="any" asp-for="FiatAmount" class="form-control"/>
<span asp-validation-for="FiatAmount" class="text-danger"></span>
</div>
</div>
</div>
<div class="mt-2">
<button type="submit" class="btn btn-primary">Save</button>
<a asp-action="GetServices" asp-controller="ExternalServices" class="btn btn-secondary">Back to recipe</a>
</div>
</form>
<script>
var actionUrlMapping = @Json.Serialize(Model.ExchangeServices.ToDictionary(item => item.Value, item => @Url.Action("GetAvailableMarketSymbols", "PlaceOrder", new {ExternalServiceId = item.Value})));;
var availableMarketSymbols = [];
$(document).ready(function(){
$("#SelectedExchangeServiceId").on("input", populateAvailableMarketSymbols);
function populateAvailableMarketSymbols(){
var value = $("#SelectedExchangeServiceId").val();
if(!value){
availableMarketSymbols = [];
}else{
$.ajax({
url: actionUrlMapping[value],
success: function(response){
availableMarketSymbols = response;
},
error: function(){
availableMarketSymbols = [];
}
});
}
}
populateAvailableMarketSymbols();
})
</script>

View File

@ -1,6 +1,6 @@
@if (User.Claims.Any())
{
<li class="nav-item mr-3">
<a asp-controller="Presets" asp-action="ChoosePreset">Presets</a>
<li class="nav-item">
<a asp-controller="Presets" asp-action="ChoosePreset" class="nav-link">Presets</a>
</li>
}
}

View File

@ -10,9 +10,9 @@
<ItemGroup>
<PackageReference Include="MaxKagamine.Moq.HttpClient" Version="1.1.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="3.1.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.4.0" />
<PackageReference Include="Moq" Version="4.13.1" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="3.1.4" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.6.1" />
<PackageReference Include="Moq" Version="4.14.1" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
</ItemGroup>

View File

@ -10,9 +10,9 @@
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="3.1.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.4.0" />
<PackageReference Include="Moq" Version="4.13.1" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="3.1.4" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.6.1" />
<PackageReference Include="Moq" Version="4.14.1" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
</ItemGroup>

View File

@ -11,9 +11,9 @@
<ItemGroup>
<PackageReference Include="MaxKagamine.Moq.HttpClient" Version="1.1.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="3.1.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.4.0" />
<PackageReference Include="Moq" Version="4.13.1" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="3.1.4" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.6.1" />
<PackageReference Include="Moq" Version="4.14.1" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
</ItemGroup>

View File

@ -8,6 +8,6 @@
<ProjectReference Include="..\BtcTransmuter.Abstractions\BtcTransmuter.Abstractions.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Http" Version="3.1.1" />
<PackageReference Include="Microsoft.Extensions.Http" Version="3.1.4" />
</ItemGroup>
</Project>

View File

@ -26,6 +26,7 @@ namespace BtcTransmuter.Extension.Webhook.Triggers.ReceiveWebRequest
public static readonly List<string> AllowedMethods = new List<string>()
{
"",
HttpMethod.Get.ToString(),
HttpMethod.Put.ToString(),
HttpMethod.Head.ToString(),
@ -74,6 +75,7 @@ namespace BtcTransmuter.Extension.Webhook.Triggers.ReceiveWebRequest
}
[Route("trigger/{relativeUrl?}")]
[AllowAnonymous]
public async Task<IActionResult> Trigger(string relativeUrl)
{
string body = null;

View File

@ -1,4 +1,4 @@
using System.Net.Http;
using Newtonsoft.Json.Linq;
namespace BtcTransmuter.Extension.Webhook.Triggers.ReceiveWebRequest
{
@ -8,7 +8,7 @@ namespace BtcTransmuter.Extension.Webhook.Triggers.ReceiveWebRequest
public string RelativeUrl { get; set; } = "";
public string Body { get; set; }= "";
public dynamic BodyJson { get; set; }
public JObject BodyJson { get; set; }
}
}

View File

@ -22,7 +22,7 @@ namespace BtcTransmuter.Extension.Webhook.Triggers.ReceiveWebRequest
ReceiveWebRequestTriggerData triggerData,
ReceiveWebRequestTriggerParameters parameters)
{
if (triggerData.Method != parameters.Method)
if (!string.IsNullOrEmpty(parameters.Method) && triggerData.Method != parameters.Method)
{
return Task.FromResult(false);
}

View File

@ -6,13 +6,19 @@
}
<div>
Receive a <kbd>@data.Method</kbd> HTTP request at
Receive a
@if (!string.IsNullOrEmpty(data.Method))
{
<kbd>@data.Method</kbd>
}
HTTP request at
<kbd>
@Url.Action("Trigger", "ReceiveWebRequest", new
{
relativeUrl = data.RelativeUrl
}, Context.Request.Scheme)</kbd>
@if (!string.IsNullOrEmpty(@data.Body))
}, Context.Request.Scheme)
</kbd>
@if (!string.IsNullOrEmpty(data.Body))
{
<span> with a body <kbd>@Enum.GetName(typeof(ReceiveWebRequestTriggerParameters.FieldComparer), data.BodyComparer) </kbd>@data.Body</span>
}

View File

@ -0,0 +1,505 @@
// <auto-generated />
using System;
using BtcTransmuter.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace BtcTransmuter.Data.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20200402084229_AddUserBlob")]
partial class AddUserBlob
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "3.1.1");
modelBuilder.Entity("BtcTransmuter.Data.Entities.ExternalServiceData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("DataJson")
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<string>("Type")
.HasColumnType("TEXT");
b.Property<string>("UserId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("ExternalServices");
});
modelBuilder.Entity("BtcTransmuter.Data.Entities.Recipe", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("Description")
.HasColumnType("TEXT");
b.Property<bool>("Enabled")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<string>("UserId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("Recipes");
});
modelBuilder.Entity("BtcTransmuter.Data.Entities.RecipeAction", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("ActionId")
.HasColumnType("TEXT");
b.Property<string>("DataJson")
.HasColumnType("TEXT");
b.Property<string>("ExternalServiceId")
.HasColumnType("TEXT");
b.Property<int>("Order")
.HasColumnType("INTEGER");
b.Property<string>("RecipeActionGroupId")
.HasColumnType("TEXT");
b.Property<string>("RecipeId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("ExternalServiceId");
b.HasIndex("RecipeActionGroupId");
b.HasIndex("RecipeId");
b.ToTable("RecipeActions");
});
modelBuilder.Entity("BtcTransmuter.Data.Entities.RecipeActionGroup", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("RecipeId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("RecipeId");
b.ToTable("RecipeActionGroups");
});
modelBuilder.Entity("BtcTransmuter.Data.Entities.RecipeInvocation", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("ActionResult")
.HasColumnType("TEXT");
b.Property<string>("RecipeAction")
.HasColumnType("TEXT");
b.Property<string>("RecipeId")
.HasColumnType("TEXT");
b.Property<DateTime>("Timestamp")
.HasColumnType("TEXT");
b.Property<string>("TriggerDataJson")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("RecipeId");
b.ToTable("RecipeInvocations");
});
modelBuilder.Entity("BtcTransmuter.Data.Entities.RecipeTrigger", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("DataJson")
.HasColumnType("TEXT");
b.Property<string>("ExternalServiceId")
.HasColumnType("TEXT");
b.Property<string>("RecipeId")
.HasColumnType("TEXT");
b.Property<string>("TriggerId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("ExternalServiceId");
b.HasIndex("RecipeId")
.IsUnique();
b.ToTable("RecipeTriggers");
});
modelBuilder.Entity("BtcTransmuter.Data.Entities.Settings", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("DataJson")
.HasColumnType("TEXT");
b.Property<string>("Key")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("Key")
.IsUnique();
b.ToTable("Settings");
});
modelBuilder.Entity("BtcTransmuter.Data.Entities.User", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT");
b.Property<int>("AccessFailedCount")
.HasColumnType("INTEGER");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("TEXT");
b.Property<string>("DataJson")
.HasColumnType("TEXT");
b.Property<string>("Email")
.HasColumnType("TEXT")
.HasMaxLength(256);
b.Property<bool>("EmailConfirmed")
.HasColumnType("INTEGER");
b.Property<bool>("LockoutEnabled")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("TEXT");
b.Property<string>("NormalizedEmail")
.HasColumnType("TEXT")
.HasMaxLength(256);
b.Property<string>("NormalizedUserName")
.HasColumnType("TEXT")
.HasMaxLength(256);
b.Property<string>("PasswordHash")
.HasColumnType("TEXT");
b.Property<string>("PhoneNumber")
.HasColumnType("TEXT");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("INTEGER");
b.Property<string>("SecurityStamp")
.HasColumnType("TEXT");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("INTEGER");
b.Property<string>("UserName")
.HasColumnType("TEXT")
.HasMaxLength(256);
b.HasKey("Id");
b.HasIndex("NormalizedEmail")
.HasName("EmailIndex");
b.HasIndex("NormalizedUserName")
.IsUnique()
.HasName("UserNameIndex");
b.ToTable("AspNetUsers");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("TEXT")
.HasMaxLength(256);
b.Property<string>("NormalizedName")
.HasColumnType("TEXT")
.HasMaxLength(256);
b.HasKey("Id");
b.HasIndex("NormalizedName")
.IsUnique()
.HasName("RoleNameIndex");
b.ToTable("AspNetRoles");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("ClaimType")
.HasColumnType("TEXT");
b.Property<string>("ClaimValue")
.HasColumnType("TEXT");
b.Property<string>("RoleId")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("AspNetRoleClaims");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("ClaimType")
.HasColumnType("TEXT");
b.Property<string>("ClaimValue")
.HasColumnType("TEXT");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AspNetUserClaims");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.Property<string>("LoginProvider")
.HasColumnType("TEXT");
b.Property<string>("ProviderKey")
.HasColumnType("TEXT");
b.Property<string>("ProviderDisplayName")
.HasColumnType("TEXT");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("AspNetUserLogins");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("TEXT");
b.Property<string>("RoleId")
.HasColumnType("TEXT");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("AspNetUserRoles");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("TEXT");
b.Property<string>("LoginProvider")
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<string>("Value")
.HasColumnType("TEXT");
b.HasKey("UserId", "LoginProvider", "Name");
b.ToTable("AspNetUserTokens");
});
modelBuilder.Entity("BtcTransmuter.Data.Entities.ExternalServiceData", b =>
{
b.HasOne("BtcTransmuter.Data.Entities.User", "User")
.WithMany()
.HasForeignKey("UserId");
});
modelBuilder.Entity("BtcTransmuter.Data.Entities.Recipe", b =>
{
b.HasOne("BtcTransmuter.Data.Entities.User", null)
.WithMany("Recipes")
.HasForeignKey("UserId");
});
modelBuilder.Entity("BtcTransmuter.Data.Entities.RecipeAction", b =>
{
b.HasOne("BtcTransmuter.Data.Entities.ExternalServiceData", "ExternalService")
.WithMany("RecipeActions")
.HasForeignKey("ExternalServiceId");
b.HasOne("BtcTransmuter.Data.Entities.RecipeActionGroup", "RecipeActionGroup")
.WithMany("RecipeActions")
.HasForeignKey("RecipeActionGroupId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("BtcTransmuter.Data.Entities.Recipe", "Recipe")
.WithMany("RecipeActions")
.HasForeignKey("RecipeId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("BtcTransmuter.Data.Entities.RecipeActionGroup", b =>
{
b.HasOne("BtcTransmuter.Data.Entities.Recipe", "Recipe")
.WithMany("RecipeActionGroups")
.HasForeignKey("RecipeId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("BtcTransmuter.Data.Entities.RecipeInvocation", b =>
{
b.HasOne("BtcTransmuter.Data.Entities.Recipe", "Recipe")
.WithMany("RecipeInvocations")
.HasForeignKey("RecipeId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("BtcTransmuter.Data.Entities.RecipeTrigger", b =>
{
b.HasOne("BtcTransmuter.Data.Entities.ExternalServiceData", "ExternalService")
.WithMany("RecipeTriggers")
.HasForeignKey("ExternalServiceId");
b.HasOne("BtcTransmuter.Data.Entities.Recipe", "Recipe")
.WithOne("RecipeTrigger")
.HasForeignKey("BtcTransmuter.Data.Entities.RecipeTrigger", "RecipeId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.HasOne("BtcTransmuter.Data.Entities.User", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.HasOne("BtcTransmuter.Data.Entities.User", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("BtcTransmuter.Data.Entities.User", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.HasOne("BtcTransmuter.Data.Entities.User", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,22 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace BtcTransmuter.Data.Migrations
{
public partial class AddUserBlob : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "DataJson",
table: "AspNetUsers",
nullable: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "DataJson",
table: "AspNetUsers");
}
}
}

View File

@ -14,20 +14,25 @@ namespace BtcTransmuter.Data.Migrations
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "2.2.4-servicing-10062");
.HasAnnotation("ProductVersion", "3.1.1");
modelBuilder.Entity("BtcTransmuter.Data.Entities.ExternalServiceData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("DataJson");
b.Property<string>("DataJson")
.HasColumnType("TEXT");
b.Property<string>("Name");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<string>("Type");
b.Property<string>("Type")
.HasColumnType("TEXT");
b.Property<string>("UserId");
b.Property<string>("UserId")
.HasColumnType("TEXT");
b.HasKey("Id");
@ -39,15 +44,20 @@ namespace BtcTransmuter.Data.Migrations
modelBuilder.Entity("BtcTransmuter.Data.Entities.Recipe", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("Description");
b.Property<string>("Description")
.HasColumnType("TEXT");
b.Property<bool>("Enabled");
b.Property<bool>("Enabled")
.HasColumnType("INTEGER");
b.Property<string>("Name");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<string>("UserId");
b.Property<string>("UserId")
.HasColumnType("TEXT");
b.HasKey("Id");
@ -59,19 +69,26 @@ namespace BtcTransmuter.Data.Migrations
modelBuilder.Entity("BtcTransmuter.Data.Entities.RecipeAction", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("ActionId");
b.Property<string>("ActionId")
.HasColumnType("TEXT");
b.Property<string>("DataJson");
b.Property<string>("DataJson")
.HasColumnType("TEXT");
b.Property<string>("ExternalServiceId");
b.Property<string>("ExternalServiceId")
.HasColumnType("TEXT");
b.Property<int>("Order");
b.Property<int>("Order")
.HasColumnType("INTEGER");
b.Property<string>("RecipeActionGroupId");
b.Property<string>("RecipeActionGroupId")
.HasColumnType("TEXT");
b.Property<string>("RecipeId");
b.Property<string>("RecipeId")
.HasColumnType("TEXT");
b.HasKey("Id");
@ -87,9 +104,11 @@ namespace BtcTransmuter.Data.Migrations
modelBuilder.Entity("BtcTransmuter.Data.Entities.RecipeActionGroup", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("RecipeId");
b.Property<string>("RecipeId")
.HasColumnType("TEXT");
b.HasKey("Id");
@ -101,17 +120,23 @@ namespace BtcTransmuter.Data.Migrations
modelBuilder.Entity("BtcTransmuter.Data.Entities.RecipeInvocation", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("ActionResult");
b.Property<string>("ActionResult")
.HasColumnType("TEXT");
b.Property<string>("RecipeAction");
b.Property<string>("RecipeAction")
.HasColumnType("TEXT");
b.Property<string>("RecipeId");
b.Property<string>("RecipeId")
.HasColumnType("TEXT");
b.Property<DateTime>("Timestamp");
b.Property<DateTime>("Timestamp")
.HasColumnType("TEXT");
b.Property<string>("TriggerDataJson");
b.Property<string>("TriggerDataJson")
.HasColumnType("TEXT");
b.HasKey("Id");
@ -123,15 +148,20 @@ namespace BtcTransmuter.Data.Migrations
modelBuilder.Entity("BtcTransmuter.Data.Entities.RecipeTrigger", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("DataJson");
b.Property<string>("DataJson")
.HasColumnType("TEXT");
b.Property<string>("ExternalServiceId");
b.Property<string>("ExternalServiceId")
.HasColumnType("TEXT");
b.Property<string>("RecipeId");
b.Property<string>("RecipeId")
.HasColumnType("TEXT");
b.Property<string>("TriggerId");
b.Property<string>("TriggerId")
.HasColumnType("TEXT");
b.HasKey("Id");
@ -146,11 +176,14 @@ namespace BtcTransmuter.Data.Migrations
modelBuilder.Entity("BtcTransmuter.Data.Entities.Settings", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("DataJson");
b.Property<string>("DataJson")
.HasColumnType("TEXT");
b.Property<string>("Key");
b.Property<string>("Key")
.HasColumnType("TEXT");
b.HasKey("Id");
@ -163,39 +196,56 @@ namespace BtcTransmuter.Data.Migrations
modelBuilder.Entity("BtcTransmuter.Data.Entities.User", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
.HasColumnType("TEXT");
b.Property<int>("AccessFailedCount");
b.Property<int>("AccessFailedCount")
.HasColumnType("INTEGER");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken();
.IsConcurrencyToken()
.HasColumnType("TEXT");
b.Property<string>("DataJson")
.HasColumnType("TEXT");
b.Property<string>("Email")
.HasColumnType("TEXT")
.HasMaxLength(256);
b.Property<bool>("EmailConfirmed");
b.Property<bool>("EmailConfirmed")
.HasColumnType("INTEGER");
b.Property<bool>("LockoutEnabled");
b.Property<bool>("LockoutEnabled")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset?>("LockoutEnd");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("TEXT");
b.Property<string>("NormalizedEmail")
.HasColumnType("TEXT")
.HasMaxLength(256);
b.Property<string>("NormalizedUserName")
.HasColumnType("TEXT")
.HasMaxLength(256);
b.Property<string>("PasswordHash");
b.Property<string>("PasswordHash")
.HasColumnType("TEXT");
b.Property<string>("PhoneNumber");
b.Property<string>("PhoneNumber")
.HasColumnType("TEXT");
b.Property<bool>("PhoneNumberConfirmed");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("INTEGER");
b.Property<string>("SecurityStamp");
b.Property<string>("SecurityStamp")
.HasColumnType("TEXT");
b.Property<bool>("TwoFactorEnabled");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("INTEGER");
b.Property<string>("UserName")
.HasColumnType("TEXT")
.HasMaxLength(256);
b.HasKey("Id");
@ -213,15 +263,18 @@ namespace BtcTransmuter.Data.Migrations
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
.HasColumnType("TEXT");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken();
.IsConcurrencyToken()
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("TEXT")
.HasMaxLength(256);
b.Property<string>("NormalizedName")
.HasColumnType("TEXT")
.HasMaxLength(256);
b.HasKey("Id");
@ -236,14 +289,18 @@ namespace BtcTransmuter.Data.Migrations
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("ClaimType");
b.Property<string>("ClaimType")
.HasColumnType("TEXT");
b.Property<string>("ClaimValue");
b.Property<string>("ClaimValue")
.HasColumnType("TEXT");
b.Property<string>("RoleId")
.IsRequired();
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
@ -255,14 +312,18 @@ namespace BtcTransmuter.Data.Migrations
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("ClaimType");
b.Property<string>("ClaimType")
.HasColumnType("TEXT");
b.Property<string>("ClaimValue");
b.Property<string>("ClaimValue")
.HasColumnType("TEXT");
b.Property<string>("UserId")
.IsRequired();
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
@ -273,14 +334,18 @@ namespace BtcTransmuter.Data.Migrations
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.Property<string>("LoginProvider");
b.Property<string>("LoginProvider")
.HasColumnType("TEXT");
b.Property<string>("ProviderKey");
b.Property<string>("ProviderKey")
.HasColumnType("TEXT");
b.Property<string>("ProviderDisplayName");
b.Property<string>("ProviderDisplayName")
.HasColumnType("TEXT");
b.Property<string>("UserId")
.IsRequired();
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("LoginProvider", "ProviderKey");
@ -291,9 +356,11 @@ namespace BtcTransmuter.Data.Migrations
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.Property<string>("UserId");
b.Property<string>("UserId")
.HasColumnType("TEXT");
b.Property<string>("RoleId");
b.Property<string>("RoleId")
.HasColumnType("TEXT");
b.HasKey("UserId", "RoleId");
@ -304,13 +371,17 @@ namespace BtcTransmuter.Data.Migrations
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.Property<string>("UserId");
b.Property<string>("UserId")
.HasColumnType("TEXT");
b.Property<string>("LoginProvider");
b.Property<string>("LoginProvider")
.HasColumnType("TEXT");
b.Property<string>("Name");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<string>("Value");
b.Property<string>("Value")
.HasColumnType("TEXT");
b.HasKey("UserId", "LoginProvider", "Name");
@ -326,7 +397,7 @@ namespace BtcTransmuter.Data.Migrations
modelBuilder.Entity("BtcTransmuter.Data.Entities.Recipe", b =>
{
b.HasOne("BtcTransmuter.Data.Entities.User")
b.HasOne("BtcTransmuter.Data.Entities.User", null)
.WithMany("Recipes")
.HasForeignKey("UserId");
});
@ -378,47 +449,53 @@ namespace BtcTransmuter.Data.Migrations
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole")
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade);
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.HasOne("BtcTransmuter.Data.Entities.User")
b.HasOne("BtcTransmuter.Data.Entities.User", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.HasOne("BtcTransmuter.Data.Entities.User")
b.HasOne("BtcTransmuter.Data.Entities.User", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole")
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade);
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("BtcTransmuter.Data.Entities.User")
b.HasOne("BtcTransmuter.Data.Entities.User", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.HasOne("BtcTransmuter.Data.Entities.User")
b.HasOne("BtcTransmuter.Data.Entities.User", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
#pragma warning restore 612, 618
}

View File

@ -70,34 +70,30 @@ namespace BtcTransmuter.Services
public async Task<IEnumerable<RecipeInvocation>> GetRecipeInvocations(RecipeInvocationsQuery query)
{
using (var scope = _serviceScopeFactory.CreateScope())
using var scope = _serviceScopeFactory.CreateScope();
await using var context = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
var queryable = context.RecipeInvocations
.Include(invocation => invocation.Recipe)
.ThenInclude(recipe => recipe.RecipeTrigger)
.AsEnumerable();
if (query.OrderBy != null)
{
using (var context = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>())
switch (query.OrderBy.Field)
{
var queryable = context.RecipeInvocations
.Include(invocation => invocation.Recipe)
.ThenInclude(recipe => recipe.RecipeTrigger)
.AsEnumerable();
if (query.OrderBy != null)
{
switch (query.OrderBy.Field)
{
case RecipeInvocationsQuery.RecipeInvocationsQueryOrderBy.Timestamp:
queryable = query.OrderBy.Direction == OrderDirection.Ascending
? queryable.OrderBy(invocation => invocation.Timestamp)
: queryable.OrderByDescending(invocation => invocation.Timestamp);
break;
default:
throw new ArgumentOutOfRangeException();
}
}
return queryable
.Where(invocation =>
invocation.RecipeId.Equals(query.RecipeId, StringComparison.InvariantCultureIgnoreCase))
.Skip(query.Skip).Take(query.Take).ToList();
case RecipeInvocationsQuery.RecipeInvocationsQueryOrderBy.Timestamp:
queryable = query.OrderBy.Direction == OrderDirection.Ascending
? queryable.OrderBy(invocation => invocation.Timestamp)
: queryable.OrderByDescending(invocation => invocation.Timestamp);
break;
default:
throw new ArgumentOutOfRangeException();
}
}
return queryable
.Where(invocation =>
invocation.RecipeId.Equals(query.RecipeId, StringComparison.InvariantCultureIgnoreCase))
.Skip(query.Skip).Take(query.Take).ToList();
}
public async Task AddOrUpdateRecipe(Recipe recipe)
@ -150,6 +146,8 @@ namespace BtcTransmuter.Services
public async Task AddOrUpdateRecipeAction(RecipeAction action)
{
var oldES = action.ExternalService;
action.ExternalService = null;
using (var scope = _serviceScopeFactory.CreateScope())
{
using (var context = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>())
@ -160,10 +158,12 @@ namespace BtcTransmuter.Services
}
else
{
context.Entry(action).State = EntityState.Modified;
}
await context.SaveChangesAsync();
action.ExternalService = oldES;
}
}
}

View File

@ -9,9 +9,9 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="3.1.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.4.0" />
<PackageReference Include="Moq" Version="4.13.1" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="3.1.4" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.6.1" />
<PackageReference Include="Moq" Version="4.14.1" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
</ItemGroup>

View File

@ -9,8 +9,8 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.4.0" />
<PackageReference Include="Moq" Version="4.13.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.6.1" />
<PackageReference Include="Moq" Version="4.14.1" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
</ItemGroup>

View File

@ -9,7 +9,7 @@ namespace BtcTransmuter.Tests
public class InterpolationTests
{
[Fact]
public async Task CanInterpolate()
public void CanInterpolate()
{
Assert.Equal("hello world",InterpolationHelper.InterpolateString("{{Data1 +\" \" + Data2}}", new Dictionary<string, object>()
{

View File

@ -15,8 +15,7 @@ namespace BtcTransmuter.Areas.Identity
{
public void Configure(IWebHostBuilder builder)
{
builder.ConfigureServices((context, services) => {
});
builder.ConfigureServices((context, services) => { services.AddScoped<BTCPayAuthService>(); });
}
}
}

View File

@ -8,6 +8,7 @@
}
@inject ISettingsManager SettingsManager
@inject BtcTransmuterOptions Options;
@{
var settings = await SettingsManager.GetSettings<SystemSettings>(nameof(SystemSettings));
}
@ -17,37 +18,45 @@
<div class="col-md-4">
<section>
<form id="account" method="post">
<hr />
<div asp-validation-summary="All" class="text-danger"></div>
<div class="form-group">
<label asp-for="Input.Email"></label>
<input asp-for="Input.Email" class="form-control" />
<input asp-for="Input.Email" class="form-control"/>
<span asp-validation-for="Input.Email" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Input.Password"></label>
<input asp-for="Input.Password" class="form-control" />
<input asp-for="Input.Password" class="form-control"/>
<span asp-validation-for="Input.Password" class="text-danger"></span>
</div>
<div class="form-group">
<div class="checkbox">
<label asp-for="Input.RememberMe">
<input asp-for="Input.RememberMe" />
<input asp-for="Input.RememberMe"/>
@Html.DisplayNameFor(m => m.Input.RememberMe)
</label>
</div>
</div>
<div class="form-group">
<button type="submit" class="btn btn-primary">Log in</button>
<button type="submit" class="btn btn-primary">
Log in
@if (Options.DisableInternalAuth)
{
<span> with BTCPay Server account</span>
}
else if (Options.BTCPayAuthServer != null)
{
<span> (local account or BTCPay Server account)</span>
}
</button>
</div>
@if (!settings.DisableRegistration)
@if (!settings.DisableRegistration && !Options.DisableInternalAuth)
{
<div class="form-group">
@* <p> *@
@* <a id="forgot-password" asp-page="./ForgotPassword">Forgot your password?</a> *@
@* </p> *@
<p>
<a asp-page="./Register" asp-route-returnUrl="@Model.ReturnUrl">Register as a new user</a>
</p>
@ -56,37 +65,8 @@
</form>
</section>
</div>
@* <div class="col-md-6 col-md-offset-2"> *@
@* <section> *@
@* <h4>Use another service to log in.</h4> *@
@* <hr /> *@
@* @{ *@
@* if ((Model.ExternalLogins?.Count ?? 0) == 0) *@
@* { *@
@* <div> *@
@* <p> *@
@* Coming soon: Log in with Btcpayserver account *@
@* </p> *@
@* </div> *@
@* } *@
@* else *@
@* { *@
@* <form id="external-account" asp-page="./ExternalLogin" asp-route-returnUrl="@Model.ReturnUrl" method="post" class="form-horizontal"> *@
@* <div> *@
@* <p> *@
@* @foreach (var provider in Model.ExternalLogins) *@
@* { *@
@* <button type="submit" class="btn btn-primary" name="provider" value="@provider.Name" title="Log in using your @provider.DisplayName account">@provider.DisplayName</button> *@
@* } *@
@* </p> *@
@* </div> *@
@* </form> *@
@* } *@
@* } *@
@* </section> *@
@* </div> *@
</div>
@section Scripts {
<partial name="_ValidationScriptsPartial" />
}
<partial name="_ValidationScriptsPartial"/>
}

View File

@ -2,27 +2,41 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using BtcTransmuter.Data.Entities;
using BtcTransmuter.Data.Models;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace BtcTransmuter.Areas.Identity.Pages.Account
{
[AllowAnonymous]
public class LoginModel : PageModel
{
private readonly BTCPayAuthService _btcPayAuthService;
private readonly SignInManager<User> _signInManager;
private readonly ILogger<LoginModel> _logger;
private readonly IBtcTransmuterOptions _btcTransmuterOptions;
private readonly IHttpClientFactory _httpClientFactory;
private readonly UserManager<User> _userManager;
public LoginModel(SignInManager<User> signInManager, ILogger<LoginModel> logger)
public LoginModel(BTCPayAuthService btcPayAuthService,SignInManager<User> signInManager, ILogger<LoginModel> logger, IBtcTransmuterOptions btcTransmuterOptions, IHttpClientFactory httpClientFactory, UserManager<User> userManager)
{
_btcPayAuthService = btcPayAuthService;
_signInManager = signInManager;
_logger = logger;
_btcTransmuterOptions = btcTransmuterOptions;
_httpClientFactory = httpClientFactory;
_userManager = userManager;
}
[BindProperty]
@ -72,32 +86,66 @@ namespace BtcTransmuter.Areas.Identity.Pages.Account
if (ModelState.IsValid)
{
// This doesn't count login failures towards account lockout
// To enable password failures to trigger account lockout, set lockoutOnFailure: true
var result = await _signInManager.PasswordSignInAsync(Input.Email, Input.Password, Input.RememberMe, lockoutOnFailure: true);
if (result.Succeeded)
var user = await _btcPayAuthService.LoginAndRegisterIfNeeded(Input.Email, Input.Password);
if (user != null)
{
_logger.LogInformation("User logged in.");
await _signInManager.SignInAsync(user, Input.RememberMe);
_logger.LogInformation("User logged in using BTCPay.");
return LocalRedirect(returnUrl);
}
if (result.RequiresTwoFactor)
else if (!_btcTransmuterOptions.DisableInternalAuth)
{
return RedirectToPage("./LoginWith2fa", new { ReturnUrl = returnUrl, RememberMe = Input.RememberMe });
}
if (result.IsLockedOut)
{
_logger.LogWarning("User account locked out.");
return RedirectToPage("./Lockout");
}
else
{
ModelState.AddModelError(string.Empty, "Invalid login attempt.");
return Page();
var result = await _signInManager.PasswordSignInAsync(Input.Email, Input.Password, Input.RememberMe,
lockoutOnFailure: true);
if (result.Succeeded)
{
_logger.LogInformation("User logged in.");
return LocalRedirect(returnUrl);
}
if (result.RequiresTwoFactor)
{
return RedirectToPage("./LoginWith2fa",
new {ReturnUrl = returnUrl, RememberMe = Input.RememberMe});
}
if (result.IsLockedOut)
{
_logger.LogWarning("User account locked out.");
return RedirectToPage("./Lockout");
}
}
ModelState.AddModelError(string.Empty, "Invalid login attempt.");
}
// If we got this far, something failed, redisplay form
return Page();
}
}
public class GetCurrentUserResponse {
[JsonProperty("id")]
public string Id { get; set; }
[JsonProperty("email")]
public string Email { get; set; }
[JsonProperty("emailConfirmed")]
public bool EmailConfirmed { get; set; }
[JsonProperty("requiresEmailConfirmation")]
public bool RequiresEmailConfirmation { get; set; }
[JsonProperty("roles")]
public string[] Roles { get; set; }
public override string ToString()
{
return $"{Id}{Email}";
}
}
}

View File

@ -0,0 +1,27 @@
@page
@model BTCPayAccountLinkModel
@{
ViewData["Title"] = "BTCPay Account Link";
ViewData["ActivePage"] = ManageNavPages.BTCPayAccountLink;
}
<h4>@ViewData["Title"]</h4>
<partial name="_StatusMessage" for="StatusMessage" />
<div class="row">
<div class="col-md-6">
<form id="change-password-form" method="post">
<div asp-validation-summary="All" class="text-danger"></div>
<div class="form-group">
<label asp-for="Input.AccessToken"></label>
<input asp-for="Input.AccessToken" class="form-control" />
<span asp-validation-for="Input.AccessToken" class="text-danger"></span>
</div>
<button type="submit" class="btn btn-primary">Update</button>
</form>
</div>
</div>
@section Scripts {
<partial name="_ValidationScriptsPartial" />
}

View File

@ -0,0 +1,109 @@
using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks;
using BtcTransmuter.Data.Entities;
using BtcTransmuter.Data.Models;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace BtcTransmuter.Areas.Identity.Pages.Account.Manage
{
public class BTCPayAccountLinkModel : PageModel
{
private readonly UserManager<User> _userManager;
private readonly SignInManager<User> _signInManager;
private readonly IBtcTransmuterOptions _btcTransmuterOptions;
private readonly BTCPayAuthService _btcPayAuthService;
public BTCPayAccountLinkModel(
UserManager<User> userManager,
SignInManager<User> signInManager,
IBtcTransmuterOptions btcTransmuterOptions, BTCPayAuthService btcPayAuthService)
{
_userManager = userManager;
_signInManager = signInManager;
_btcTransmuterOptions = btcTransmuterOptions;
_btcPayAuthService = btcPayAuthService;
}
public bool CurrentTokenValid { get; set; }
[BindProperty] public InputModel Input { get; set; }
[TempData] public string StatusMessage { get; set; }
public class InputModel
{
[Display(Name = "Access Token")] public string AccessToken { get; set; }
}
public async Task<IActionResult> OnGetAsync()
{
if (_btcTransmuterOptions.BTCPayAuthServer == null)
{
return RedirectToPage("./ChangePassword");
}
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
var blob = user.Get<UserBlob>();
Input = new InputModel()
{
AccessToken = blob.BTCPayAuthDetails.AccessToken
};
if (!string.IsNullOrEmpty(Input.AccessToken))
{
var currentToken = await _btcPayAuthService.CheckToken(user);
CurrentTokenValid = currentToken != null && currentToken.Id == blob.BTCPayAuthDetails.UserId;
}
return Page();
}
public async Task<IActionResult> OnPostAsync()
{
if (_btcTransmuterOptions.BTCPayAuthServer == null)
{
return RedirectToPage("./ChangePassword");
}
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
var blob = user.Get<UserBlob>();
if (_btcTransmuterOptions.DisableInternalAuth && string.IsNullOrEmpty(Input.AccessToken))
{
ModelState.AddModelError("Input.AccessToken", "Access token is required.");
}
else if (_btcTransmuterOptions.DisableInternalAuth && !string.IsNullOrEmpty(Input.AccessToken))
{
var response = await _btcPayAuthService.CheckToken(user);
CurrentTokenValid = response != null;
if (CurrentTokenValid)
{
blob.BTCPayAuthDetails.AccessToken = Input.AccessToken;
blob.BTCPayAuthDetails.UserId = response.Id;
user.Set(blob);
await _userManager.UpdateAsync(user);
}
else if (!CurrentTokenValid && _btcTransmuterOptions.DisableInternalAuth)
{
ModelState.AddModelError("Input.AccessToken", "Invalid Access token.");
}
}
if (!ModelState.IsValid)
{
return Page();
}
return RedirectToPage();
}
}
}

View File

@ -1,13 +1,11 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks;
using BtcTransmuter.Data.Entities;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Logging;
namespace BtcTransmuter.Areas.Identity.Pages.Account.Manage
{
public class ChangePasswordModel : PageModel
@ -15,22 +13,22 @@ namespace BtcTransmuter.Areas.Identity.Pages.Account.Manage
private readonly UserManager<User> _userManager;
private readonly SignInManager<User> _signInManager;
private readonly ILogger<ChangePasswordModel> _logger;
private readonly IBtcTransmuterOptions _btcTransmuterOptions;
public ChangePasswordModel(
UserManager<User> userManager,
SignInManager<User> signInManager,
ILogger<ChangePasswordModel> logger)
ILogger<ChangePasswordModel> logger, IBtcTransmuterOptions btcTransmuterOptions)
{
_userManager = userManager;
_signInManager = signInManager;
_logger = logger;
_btcTransmuterOptions = btcTransmuterOptions;
}
[BindProperty]
public InputModel Input { get; set; }
[BindProperty] public InputModel Input { get; set; }
[TempData]
public string StatusMessage { get; set; }
[TempData] public string StatusMessage { get; set; }
public class InputModel
{
@ -40,7 +38,8 @@ namespace BtcTransmuter.Areas.Identity.Pages.Account.Manage
public string OldPassword { get; set; }
[Required]
[StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
[StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.",
MinimumLength = 6)]
[DataType(DataType.Password)]
[Display(Name = "New password")]
public string NewPassword { get; set; }
@ -53,6 +52,11 @@ namespace BtcTransmuter.Areas.Identity.Pages.Account.Manage
public async Task<IActionResult> OnGetAsync()
{
if (_btcTransmuterOptions.DisableInternalAuth)
{
return RedirectToPage("./BTCPayAccountLink");
}
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
@ -70,6 +74,11 @@ namespace BtcTransmuter.Areas.Identity.Pages.Account.Manage
public async Task<IActionResult> OnPostAsync()
{
if (_btcTransmuterOptions.DisableInternalAuth)
{
return RedirectToPage("./BTCPayAccountLink");
}
if (!ModelState.IsValid)
{
return Page();
@ -81,13 +90,15 @@ namespace BtcTransmuter.Areas.Identity.Pages.Account.Manage
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
var changePasswordResult = await _userManager.ChangePasswordAsync(user, Input.OldPassword, Input.NewPassword);
var changePasswordResult =
await _userManager.ChangePasswordAsync(user, Input.OldPassword, Input.NewPassword);
if (!changePasswordResult.Succeeded)
{
foreach (var error in changePasswordResult.Errors)
{
ModelState.AddModelError(string.Empty, error.Description);
}
return Page();
}
@ -98,4 +109,4 @@ namespace BtcTransmuter.Areas.Identity.Pages.Account.Manage
return RedirectToPage();
}
}
}
}

View File

@ -1,12 +0,0 @@
@page
@model DownloadPersonalDataModel
@{
ViewData["Title"] = "Download Your Data";
ViewData["ActivePage"] = ManageNavPages.PersonalData;
}
<h4>@ViewData["Title"]</h4>
@section Scripts {
<partial name="_ValidationScriptsPartial" />
}

View File

@ -1,51 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using BtcTransmuter.Data.Entities;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
namespace BtcTransmuter.Areas.Identity.Pages.Account.Manage
{
public class DownloadPersonalDataModel : PageModel
{
private readonly UserManager<User> _userManager;
private readonly ILogger<DownloadPersonalDataModel> _logger;
public DownloadPersonalDataModel(
UserManager<User> userManager,
ILogger<DownloadPersonalDataModel> logger)
{
_userManager = userManager;
_logger = logger;
}
public async Task<IActionResult> OnPostAsync()
{
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
_logger.LogInformation("User with ID '{UserId}' asked for their personal data.", _userManager.GetUserId(User));
// Only include personal data for download
var personalData = new Dictionary<string, string>();
var personalDataProps = typeof(User).GetProperties().Where(
prop => Attribute.IsDefined(prop, typeof(PersonalDataAttribute)));
foreach (var p in personalDataProps)
{
personalData.Add(p.Name, p.GetValue(user)?.ToString() ?? "null");
}
Response.Headers.Add("Content-Disposition", "attachment; filename=PersonalData.json");
return new FileContentResult(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(personalData)), "text/json");
}
}
}

View File

@ -6,35 +6,24 @@
}
<h4>@ViewData["Title"]</h4>
<partial name="_StatusMessage" for="StatusMessage" />
<partial name="_StatusMessage" for="StatusMessage"/>
<div class="row">
<div class="col-md-6">
<form id="profile-form" method="post">
<div asp-validation-summary="All" class="text-danger"></div>
<div class="form-group">
<label asp-for="Username"></label>
<input asp-for="Username" class="form-control" disabled />
<input asp-for="Username" class="form-control" disabled/>
</div>
<div class="form-group">
<label asp-for="Input.Email"></label>
@if (Model.IsEmailConfirmed)
{
<div class="input-group">
<input asp-for="Input.Email" class="form-control" />
<span class="input-group-addon" aria-hidden="true"><span class="glyphicon glyphicon-ok text-success"></span></span>
</div>
}
else
{
<input asp-for="Input.Email" class="form-control" />
@* <button id="email-verification" type="submit" asp-page-handler="SendVerificationEmail" class="btn btn-link">Send verification email</button> *@
}
<input asp-for="Input.Email" class="form-control"/>
<span asp-validation-for="Input.Email" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Input.PhoneNumber"></label>
<input asp-for="Input.PhoneNumber" class="form-control" />
<span asp-validation-for="Input.PhoneNumber" class="text-danger"></span>
<label asp-for="Input.AllowBasicAuth"></label>
<input asp-for="Input.AllowBasicAuth" class="form-check"/>
<span asp-validation-for="Input.AllowBasicAuth" class="text-danger"></span>
</div>
<button id="update-profile-button" type="submit" class="btn btn-primary">Save</button>
</form>
@ -42,5 +31,5 @@
</div>
@section Scripts {
<partial name="_ValidationScriptsPartial" />
<partial name="_ValidationScriptsPartial"/>
}

View File

@ -5,6 +5,7 @@ using System.Linq;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using BtcTransmuter.Data.Entities;
using BtcTransmuter.Data.Models;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.UI.Services;
using Microsoft.AspNetCore.Mvc;
@ -16,37 +17,27 @@ namespace BtcTransmuter.Areas.Identity.Pages.Account.Manage
{
private readonly UserManager<User> _userManager;
private readonly SignInManager<User> _signInManager;
private readonly IEmailSender _emailSender;
public IndexModel(
UserManager<User> userManager,
SignInManager<User> signInManager,
IEmailSender emailSender)
SignInManager<User> signInManager)
{
_userManager = userManager;
_signInManager = signInManager;
_emailSender = emailSender;
}
public string Username { get; set; }
public bool IsEmailConfirmed { get; set; }
[TempData] public string StatusMessage { get; set; }
[TempData]
public string StatusMessage { get; set; }
[BindProperty]
public InputModel Input { get; set; }
[BindProperty] public InputModel Input { get; set; }
public class InputModel
{
[Required]
[EmailAddress]
public string Email { get; set; }
[Required] [EmailAddress] public string Email { get; set; }
[Phone]
[Display(Name = "Phone number")]
public string PhoneNumber { get; set; }
[Display(Name = "Allow Basic Auth using this account")]
public bool AllowBasicAuth { get; set; }
}
public async Task<IActionResult> OnGetAsync()
@ -59,18 +50,15 @@ namespace BtcTransmuter.Areas.Identity.Pages.Account.Manage
var userName = await _userManager.GetUserNameAsync(user);
var email = await _userManager.GetEmailAsync(user);
var phoneNumber = await _userManager.GetPhoneNumberAsync(user);
Username = userName;
Input = new InputModel
{
Email = email,
PhoneNumber = phoneNumber
AllowBasicAuth = user.Get<UserBlob>().BasicAuth
};
IsEmailConfirmed = await _userManager.IsEmailConfirmedAsync(user);
return Page();
}
@ -82,6 +70,7 @@ namespace BtcTransmuter.Areas.Identity.Pages.Account.Manage
}
var user = await _userManager.GetUserAsync(User);
var blob = user.Get<UserBlob>();
if (user == null)
{
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
@ -94,18 +83,27 @@ namespace BtcTransmuter.Areas.Identity.Pages.Account.Manage
if (!setEmailResult.Succeeded)
{
var userId = await _userManager.GetUserIdAsync(user);
throw new InvalidOperationException($"Unexpected error occurred setting email for user with ID '{userId}'.");
throw new InvalidOperationException(
$"Unexpected error occurred setting email for user with ID '{userId}'.");
}
}
var phoneNumber = await _userManager.GetPhoneNumberAsync(user);
if (Input.PhoneNumber != phoneNumber)
var blobChanged = false;
if (Input.AllowBasicAuth != blob.BasicAuth)
{
var setPhoneResult = await _userManager.SetPhoneNumberAsync(user, Input.PhoneNumber);
if (!setPhoneResult.Succeeded)
blob.BasicAuth = Input.AllowBasicAuth;
blobChanged = true;
}
if (blobChanged)
{
user.Set(blob);
var updated = await _userManager.UpdateAsync(user);
if (!updated.Succeeded)
{
var userId = await _userManager.GetUserIdAsync(user);
throw new InvalidOperationException($"Unexpected error occurred setting phone number for user with ID '{userId}'.");
throw new InvalidOperationException(
$"Unexpected error occurred setting data for user with ID '{userId}'.");
}
}
@ -113,36 +111,5 @@ namespace BtcTransmuter.Areas.Identity.Pages.Account.Manage
StatusMessage = "Your profile has been updated";
return RedirectToPage();
}
public async Task<IActionResult> OnPostSendVerificationEmailAsync()
{
if (!ModelState.IsValid)
{
return Page();
}
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
var userId = await _userManager.GetUserIdAsync(user);
var email = await _userManager.GetEmailAsync(user);
var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
var callbackUrl = Url.Page(
"/Account/ConfirmEmail",
pageHandler: null,
values: new { userId = userId, code = code },
protocol: Request.Scheme);
await _emailSender.SendEmailAsync(
email,
"Confirm your email",
$"Please confirm your account by <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>.");
StatusMessage = "Verification email sent. Please check your email.";
return RedirectToPage();
}
}
}
}

View File

@ -8,6 +8,7 @@ namespace BtcTransmuter.Areas.Identity.Pages.Account.Manage
public static string Index => "Index";
public static string ChangePassword => "ChangePassword";
public static string BTCPayAccountLink => "BTCPayAccountLink";
public static string ExternalLogins => "ExternalLogins";
@ -24,6 +25,7 @@ namespace BtcTransmuter.Areas.Identity.Pages.Account.Manage
public static string PersonalDataNavClass(ViewContext viewContext) => PageNavClass(viewContext, PersonalData);
public static string TwoFactorAuthenticationNavClass(ViewContext viewContext) => PageNavClass(viewContext, TwoFactorAuthentication);
public static string BTCPayAccountLinkNavClass(ViewContext viewContext) => PageNavClass(viewContext, BTCPayAccountLink);
private static string PageNavClass(ViewContext viewContext, string page)
{

View File

@ -13,9 +13,6 @@
<p>
<strong>Deleting this data will permanently remove your account, and this cannot be recovered.</strong>
</p>
<form id="download-data" asp-page="DownloadPersonalData" method="post" class="form-group">
<button class="btn btn-primary" type="submit">Download</button>
</form>
<p>
<a id="delete" asp-page="DeletePersonalData" class="btn btn-primary">Delete</a>
</p>

View File

@ -3,21 +3,17 @@ using BtcTransmuter.Data.Entities;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Logging;
namespace BtcTransmuter.Areas.Identity.Pages.Account.Manage
{
public class PersonalDataModel : PageModel
{
private readonly UserManager<User> _userManager;
private readonly ILogger<PersonalDataModel> _logger;
public PersonalDataModel(
UserManager<User> userManager,
ILogger<PersonalDataModel> logger)
UserManager<User> userManager)
{
_userManager = userManager;
_logger = logger;
}
public async Task<IActionResult> OnGet()

View File

@ -1,12 +1,22 @@
@using BtcTransmuter.Data.Entities
@using Microsoft.AspNetCore.Identity
@inject SignInManager<User> SignInManager
@inject IBtcTransmuterOptions BtcTransmuterOptions;
@{
var hasExternalLogins = (await SignInManager.GetExternalAuthenticationSchemesAsync()).Any();
}
<ul class="nav nav-pills flex-column">
<li class="nav-item"><a class="nav-link @ManageNavPages.IndexNavClass(ViewContext)" id="profile" asp-page="./Index">Profile</a></li>
<li class="nav-item"><a class="nav-link @ManageNavPages.ChangePasswordNavClass(ViewContext)" id="change-password" asp-page="./ChangePassword">Password</a></li>
@if (!BtcTransmuterOptions.DisableInternalAuth)
{
<li class="nav-item"><a class="nav-link @ManageNavPages.ChangePasswordNavClass(ViewContext)" id="change-password" asp-page="./ChangePassword">Password</a></li>
}
@if (BtcTransmuterOptions.BTCPayAuthServer != null)
{
<li class="nav-item"><a class="nav-link @ManageNavPages.BTCPayAccountLinkNavClass(ViewContext)" id="btcpay-account-link" asp-page="./BTCPayAccountLink">BTCPay account link</a></li>
}
@if (hasExternalLogins)
{
<li id="external-logins" class="nav-item"><a id="external-login" class="nav-link @ManageNavPages.ExternalLoginsNavClass(ViewContext)" asp-page="./ExternalLogins">External logins</a></li>

View File

@ -4,13 +4,11 @@
ViewData["Title"] = "Register";
}
<h1>@ViewData["Title"]</h1>
<h1>Create a new account</h1>
<div class="row">
<div class="col-md-4">
<form asp-route-returnUrl="@Model.ReturnUrl" method="post">
<h4>Create a new account.</h4>
<hr />
<div asp-validation-summary="All" class="text-danger"></div>
<div class="form-group">
<label asp-for="Input.Email"></label>

View File

@ -0,0 +1,15 @@
using System.Net;
using Microsoft.AspNetCore.Authentication;
namespace BtcTransmuter.Auth
{
public static class BasicAuthenticationExtensions
{
public static AuthenticationBuilder AddBasicAuth(this AuthenticationBuilder builder)
{
return builder.AddScheme<BasicAuthenticationOptions, BasicAuthenticationHandler>(
nameof(AuthenticationSchemes.Basic),
o => { });
}
}
}

View File

@ -0,0 +1,76 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Security.Claims;
using System.Text;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using BtcTransmuter.Abstractions.Extensions;
using BtcTransmuter.Data.Entities;
using BtcTransmuter.Data.Models;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace BtcTransmuter.Auth
{
// ReSharper disable once ClassNeverInstantiated.Global
public class BasicAuthenticationHandler : AuthenticationHandler<BasicAuthenticationOptions>
{
private readonly IOptionsMonitor<IdentityOptions> _identityOptions;
private readonly SignInManager<User> _signInManager;
private readonly UserManager<User> _userManager;
public BasicAuthenticationHandler(
IOptionsMonitor<IdentityOptions> identityOptions,
IOptionsMonitor<BasicAuthenticationOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
ISystemClock clock,
SignInManager<User> signInManager,
UserManager<User> userManager) : base(options, logger, encoder, clock)
{
_identityOptions = identityOptions;
_signInManager = signInManager;
_userManager = userManager;
}
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
string authHeader = Context.Request.Headers["Authorization"];
if (authHeader == null || !authHeader.StartsWith("Basic ", StringComparison.OrdinalIgnoreCase))
return AuthenticateResult.NoResult();
var encodedUsernamePassword = authHeader.Split(' ', 2, StringSplitOptions.RemoveEmptyEntries)[1]?.Trim();
var decodedUsernamePassword =
Encoding.UTF8.GetString(Convert.FromBase64String(encodedUsernamePassword)).Split(':');
var username = decodedUsernamePassword[0];
var password = decodedUsernamePassword[1];
var result = await _signInManager.PasswordSignInAsync(username, password, true, true);
if (!result.Succeeded)
return AuthenticateResult.Fail(result.ToString());
var user = await _userManager.FindByNameAsync(username);
if (!user.Get<UserBlob>().BasicAuth)
{
return AuthenticateResult.Fail("This user does not have basic auth enabled.");
}
var claims = new List<Claim>()
{
new Claim(_identityOptions.CurrentValue.ClaimsIdentity.UserIdClaimType, user.Id)
};
var roles = await _userManager.GetRolesAsync(user);
claims.AddRange(roles.Select(s => new Claim(_identityOptions.CurrentValue.ClaimsIdentity.RoleClaimType, s) ));
Context.SetIsApi(true);
return AuthenticateResult.Success(new AuthenticationTicket(
new ClaimsPrincipal(new ClaimsIdentity(claims, nameof(AuthenticationSchemes.Basic))), nameof(AuthenticationSchemes.Basic)));
}
}
}

View File

@ -0,0 +1,8 @@
using Microsoft.AspNetCore.Authentication;
namespace BtcTransmuter.Auth
{
public class BasicAuthenticationOptions : AuthenticationSchemeOptions
{
}
}

View File

@ -0,0 +1,16 @@
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Server.HttpSys;
namespace BtcTransmuter.Auth
{
public class TransmuterSchemes
{
public const string AllSchemes = Local + "," + API;
public const string API = Basic;
public const string Basic = nameof(AuthenticationSchemes.Basic);
//IdentityConstants.ApplicationScheme
public const string Local = "Identity.Application" + "," + CookieAuthenticationDefaults.AuthenticationScheme;
}
}

View File

@ -0,0 +1,230 @@
using System;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
using BtcTransmuter.Areas.Identity.Pages.Account;
using BtcTransmuter.Data.Entities;
using BtcTransmuter.Data.Models;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace BtcTransmuter
{
public class BTCPayAuthService
{
private readonly UserManager<User> _userManager;
private readonly IBtcTransmuterOptions _btcTransmuterOptions;
private readonly IHttpClientFactory _httpClientFactory;
private readonly ILogger<BTCPayAuthService> _logger;
public BTCPayAuthService(
UserManager<User> userManager, IBtcTransmuterOptions btcTransmuterOptions,
IHttpClientFactory httpClientFactory, ILogger<BTCPayAuthService> logger)
{
_userManager = userManager;
_btcTransmuterOptions = btcTransmuterOptions;
_httpClientFactory = httpClientFactory;
_logger = logger;
}
public async Task<User> LoginAndRegisterIfNeeded(string user, string pass)
{
if (_btcTransmuterOptions.BTCPayAuthServer is null)
{
return null;
}
var response = await BasicAuthLogin(user, pass);
if (response == null)
{
return null;
}
var matchedUser = await FindUserByBTCPayUserId(response.Id);
if (matchedUser == null)
{
var key = await GenerateKey(user, pass);
if (string.IsNullOrEmpty(key))
{
return null;
}
//create account
matchedUser = new User()
{
Email = response.Email,
Id = response.Id,
UserName = response.Email,
};
matchedUser.Set(new UserBlob()
{
BTCPayAuthDetails = new BTCPayAuthDetails()
{
UserId = response.Id,
AccessToken = key
}
});
if ((await _userManager.CreateAsync(matchedUser)).Succeeded)
{
if (response.Roles.Contains("ServerAdmin") || await _userManager.Users.CountAsync() == 1)
{
await _userManager.AddToRoleAsync(matchedUser, "Admin");
}
}
else
{
return null;
}
}
else
{
var tokenResponse = await CheckToken(matchedUser);
if (!(tokenResponse?.ToString()?.Equals(response.ToString()) is true) &&
await GenerateKeyAndSet(user, pass, matchedUser))
{
await _userManager.UpdateAsync(matchedUser);
}
else if (!(tokenResponse?.ToString()?.Equals(response.ToString()) is true))
{
return null;
}
}
if (response.Roles.Contains("ServerAdmin"))
{
await _userManager.AddToRoleAsync(matchedUser, "Admin");
}
else if(!await _userManager.HasPasswordAsync(matchedUser))
{
await _userManager.RemoveFromRoleAsync(matchedUser, "Admin");
}
return matchedUser;
}
private async Task<bool> GenerateKeyAndSet(string user, string pass, User matchedUser)
{
var key = await GenerateKey(user, pass);
if (string.IsNullOrEmpty(key))
{
return false;
}
var blob = matchedUser.Get<UserBlob>();
blob.BTCPayAuthDetails.AccessToken = key;
if (_btcTransmuterOptions.DisableInternalAuth || !await _userManager.HasPasswordAsync(matchedUser))
{
matchedUser.Email = user;
matchedUser.UserName = user;
}
matchedUser.Set(blob);
return true;
}
public async Task<string> GenerateKey(string user, string pass)
{
var client = _httpClientFactory.CreateClient("BTCPayAuthServer");
var request = new HttpRequestMessage(HttpMethod.Post,
new Uri(_btcTransmuterOptions.BTCPayAuthServer, "/api/v1/api-keys"));
request.Headers.Authorization = new AuthenticationHeaderValue("Basic",
Convert.ToBase64String(Encoding.UTF8.GetBytes($"{user}:{pass}")));
request.Content = new StringContent(JsonConvert.SerializeObject(new
{
label = "transmuter login access token",
permissions = new[] {"btcpay.user.canmodifyprofile"}
}), Encoding.UTF8, "application/json");
var response = await client.SendAsync(request);
if (response.IsSuccessStatusCode)
{
var accessTokenResponse =
JsonConvert.DeserializeObject<JObject>((await response.Content.ReadAsStringAsync()));
return accessTokenResponse["apiKey"].Value<string>();
}
return null;
}
public async Task<User> FindUserByBTCPayUserId(string userId)
{
return _userManager.Users.AsEnumerable().SingleOrDefault(user =>
user.Get<UserBlob>().BTCPayAuthDetails.UserId == userId);
}
public async Task<GetCurrentUserResponse> BasicAuthLogin(string user, string pass)
{
if (_btcTransmuterOptions.BTCPayAuthServer is null)
{
return null;
}
try
{
var client = _httpClientFactory.CreateClient("BTCPayAuthServer");
var fetchUserId = new Uri(_btcTransmuterOptions.BTCPayAuthServer, "api/v1/users/me");
var request = new HttpRequestMessage(HttpMethod.Get, fetchUserId);
request.Headers.Authorization = new AuthenticationHeaderValue("Basic",
Convert.ToBase64String(Encoding.UTF8.GetBytes($"{user}:{pass}")));
var response = await client.SendAsync(request);
if (response.IsSuccessStatusCode)
{
return JsonConvert.DeserializeObject<GetCurrentUserResponse>(
await response.Content.ReadAsStringAsync());
}
}
catch (Exception e)
{
_logger.LogError(e, "error while attempting to authenticate with btcpay");
}
return null;
}
public async Task<GetCurrentUserResponse> CheckToken(User user)
{
if (user == null)
{
return null;
}
var blob = user.Get<UserBlob>();
return await CheckToken(blob.BTCPayAuthDetails.AccessToken);
}
public async Task<GetCurrentUserResponse> CheckToken(string token)
{
if (_btcTransmuterOptions.BTCPayAuthServer is null)
{
return null;
}
var client = _httpClientFactory.CreateClient("BTCPayAuthServer");
if (string.IsNullOrEmpty(token))
{
return null;
}
var fetchUserId = new Uri(_btcTransmuterOptions.BTCPayAuthServer, "api/v1/users/me");
var request = new HttpRequestMessage(HttpMethod.Get, fetchUserId);
request.Headers.Authorization = new AuthenticationHeaderValue("token", token);
var response = await client.SendAsync(request);
if (!response.IsSuccessStatusCode)
{
return null;
}
return JsonConvert.DeserializeObject<GetCurrentUserResponse>(
await response.Content.ReadAsStringAsync());
}
}
}

View File

@ -6,9 +6,9 @@
<AddRazorSupportForMvc>true</AddRazorSupportForMvc>
<UserSecretsId>aspnet-BtcTransmuter-065A2226-96CE-4371-A96D-97EFFCF0B153</UserSecretsId>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
<Version>0.0.49</Version>
<Version>0.0.59</Version>
<MvcRazorExcludeRefAssembliesFromPublish>false</MvcRazorExcludeRefAssembliesFromPublish>
<PackageVersion>0.0.49</PackageVersion>
<PackageVersion>0.0.59</PackageVersion>
</PropertyGroup>
@ -17,12 +17,13 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="3.1.1" />
<PackageReference Include="Microsoft.AspNetCore.Identity.UI" Version="3.1.1" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="3.1.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="3.1.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="3.1.1" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="3.1.0" />
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="3.1.4" />
<PackageReference Include="Microsoft.AspNetCore.Identity.UI" Version="3.1.4" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="3.1.4" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="3.1.4" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="3.1.4" />
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="3.1.5" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="3.1.3" />
</ItemGroup>
<ItemGroup>

View File

@ -1,9 +1,9 @@
using System;
using System.IO;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json.Linq;
using IHostingEnvironment = Microsoft.AspNetCore.Hosting.IHostingEnvironment;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace BtcTransmuter
@ -15,11 +15,19 @@ namespace BtcTransmuter
{
RootPath = configuration.GetValue("RootPath", "");
BTCPayAuthServer = configuration.GetValue<Uri>("BTCPayAuthServer", null);
DatabaseConnectionString = configuration.GetValue<string>("Database");
DataProtectionDir = configuration.GetValue<string>("DataProtectionDir");
DataProtectionApplicationName = configuration.GetValue<string>("DataProtectionApplicationName");
DatabaseType = configuration.GetValue<DatabaseType>("DatabaseType", DatabaseType.Sqlite);
UseDatabaseColumnEncryption = configuration.GetValue<bool>("UseDatabaseColumnEncryption", false);
DisableInternalAuth = configuration.GetValue<bool>("DisableInternalAuth", false);
if (DisableInternalAuth && BTCPayAuthServer == null)
{
DisableInternalAuth = false;
logger.LogWarning($"Cannot disable internal auth while not setting BTCPayAuthServer");
}
ExtensionsDir = configuration.GetValue<string>("ExtensionsDir",
Path.Combine(hostingEnvironment.ContentRootPath, "Extensions"));
@ -49,5 +57,9 @@ namespace BtcTransmuter
public DatabaseType DatabaseType { get; set; }
public bool UseDatabaseColumnEncryption { get; set; }
public bool DisableInternalAuth { get; set; }
public Uri BTCPayAuthServer { get; set; }
}
}

View File

@ -1,6 +1,7 @@
using System.Threading.Tasks;
using BtcTransmuter.Abstractions.Recipes;
using BtcTransmuter.Abstractions.Settings;
using BtcTransmuter.Auth;
using BtcTransmuter.Data.Entities;
using BtcTransmuter.Models;
using Microsoft.AspNetCore.Authorization;
@ -10,7 +11,7 @@ using Microsoft.EntityFrameworkCore;
namespace BtcTransmuter.Controllers
{
[Authorize(Roles = "Admin")]
[Authorize(Roles = "Admin", AuthenticationSchemes = TransmuterSchemes.Local)]
[Route("[controller]")]
public class AdminController : Controller
{

View File

@ -13,7 +13,6 @@ using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Hosting;
namespace BtcTransmuter.Controllers
{
@ -23,16 +22,14 @@ namespace BtcTransmuter.Controllers
private readonly IEnumerable<BtcTransmuterExtension> _btcTransmuterExtensions;
private readonly UserManager<User> _userManager;
private readonly IWebHostEnvironment _hostingEnvironment;
private readonly IHostApplicationLifetime _applicationLifetime;
public ExtensionsController(IEnumerable<BtcTransmuterExtension> btcTransmuterExtensions,
UserManager<User> userManager,
IWebHostEnvironment hostingEnvironment, IHostApplicationLifetime applicationLifetime)
IWebHostEnvironment hostingEnvironment)
{
_btcTransmuterExtensions = btcTransmuterExtensions;
_userManager = userManager;
_hostingEnvironment = hostingEnvironment;
_applicationLifetime = applicationLifetime;
}
[HttpGet("")]
@ -53,8 +50,7 @@ namespace BtcTransmuter.Controllers
public async Task<IActionResult> UploadExtension(List<IFormFile> files)
{
var dest = Path.Combine(_hostingEnvironment.ContentRootPath, "Extensions");
foreach (var formFile in files)
{
if (formFile.Length > 0)
@ -76,7 +72,6 @@ namespace BtcTransmuter.Controllers
}
}
// _applicationLifetime.StopApplication();
return RedirectToAction("Extensions", new
{
StatusMessage = "Files uploaded, restart server to load plugins"

View File

@ -8,7 +8,6 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.EntityFrameworkCore.Internal;
namespace BtcTransmuter.Controllers
{

View File

@ -1,18 +1,24 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Diagnostics;
using System.IO;
using System.Threading.Tasks;
using BtcTransmuter.Abstractions.Actions;
using BtcTransmuter.Abstractions.Triggers;
using BtcTransmuter.Abstractions.Extensions;
using Microsoft.AspNetCore.Mvc;
using BtcTransmuter.Models;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.FileProviders;
using Newtonsoft.Json.Linq;
namespace BtcTransmuter.Controllers
{
public class HomeController : Controller
{
private readonly IWebHostEnvironment _webHostEnvironment;
public HomeController(
IWebHostEnvironment webHostEnvironment)
{
_webHostEnvironment = webHostEnvironment;
}
public IActionResult Index()
{
@ -24,5 +30,28 @@ namespace BtcTransmuter.Controllers
{
return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
}
[Route("swagger/v1/swagger.json")]
public async Task<IActionResult> Swagger()
{
JObject json = new JObject();
var directoryContents = _webHostEnvironment.WebRootFileProvider.GetDirectoryContents("swagger/v1");
foreach (IFileInfo fi in directoryContents)
{
await using var stream = fi.CreateReadStream();
using var reader = new StreamReader(fi.CreateReadStream());
json.Merge(JObject.Parse(await reader.ReadToEndAsync()));
}
var servers = new JArray();
servers.Add(new JObject(new JProperty("url", HttpContext.Request.GetAbsoluteRoot())));
json["servers"] = servers;
return Json(json);
}
[Route("docs")]
public IActionResult SwaggerDocs()
{
return View();
}
}
}

View File

@ -2,6 +2,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BtcTransmuter.Abstractions.Actions;
using BtcTransmuter.Abstractions.Extensions;
using BtcTransmuter.Abstractions.Recipes;
using BtcTransmuter.Data.Entities;
using BtcTransmuter.Models;
@ -47,7 +48,7 @@ namespace BtcTransmuter.Controllers
}
}
return View(new EditRecipeActionViewModel()
return this.ViewOrJson(new EditRecipeActionViewModel()
{
RecipeId = id,
ActionId = recipeAction?.ActionId,
@ -80,7 +81,7 @@ namespace BtcTransmuter.Controllers
model.RecipeAction = recipeAction;
model.Actions = new SelectList(_actionDescriptors, nameof(IActionDescriptor.ActionId),
nameof(IActionDescriptor.Name), model.ActionId);
return View(model);
return this.ViewOrBadRequest(model);
}
if (string.IsNullOrEmpty(recipeActionId) || recipeAction.ActionId != model.ActionId)
@ -142,6 +143,10 @@ namespace BtcTransmuter.Controllers
}
await _recipeManager.RemoveRecipeAction(recipeActionId);
if (this.IsApi())
{
return Ok();
}
return RedirectToAction("EditRecipe", "Recipes", new
{
id,
@ -153,8 +158,12 @@ namespace BtcTransmuter.Controllers
});
}
private RedirectToActionResult GetNotFoundActionResult()
private ActionResult GetNotFoundActionResult()
{
if (this.IsApi())
{
return NotFound();
}
return RedirectToAction("GetRecipes", "Recipes", new
{
statusMessage = new StatusMessageModel()

View File

@ -1,6 +1,7 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using BtcTransmuter.Abstractions.Extensions;
using BtcTransmuter.Abstractions.Models;
using BtcTransmuter.Abstractions.Recipes;
using BtcTransmuter.Data.Entities;
@ -31,8 +32,7 @@ namespace BtcTransmuter.Controllers
{
UserId = GetUserId()
});
return View("GetRecipes",new GetRecipesViewModel()
return this.ViewOrJson("GetRecipes", new GetRecipesViewModel()
{
StatusMessage = statusMessage,
Recipes = recipes,
@ -65,6 +65,10 @@ namespace BtcTransmuter.Controllers
}
await _recipeManager.RemoveRecipe(id);
if (this.IsApi())
{
return Ok();
}
return RedirectToAction("GetRecipes", new
{
statusMessage = new StatusMessageModel()
@ -97,7 +101,7 @@ namespace BtcTransmuter.Controllers
}
});
return View(new GetRecipeLogsViewModel()
return this.ViewOrJson(new GetRecipeLogsViewModel()
{
Name = recipe.Name,
Id = id,
@ -113,6 +117,10 @@ namespace BtcTransmuter.Controllers
var result = await _recipeManager.CloneRecipe(id, enabled, name);
if (result == null)
{
if (this.IsApi())
{
return BadRequest();
}
return RedirectToAction(nameof(GetRecipes), new
{
statusMessage = new StatusMessageModel()
@ -123,6 +131,10 @@ namespace BtcTransmuter.Controllers
});
}
if (this.IsApi())
{
return await EditRecipe(result.Id, (string)null);
}
return RedirectToAction(nameof(EditRecipe), new
{
id = result.Id,
@ -148,7 +160,7 @@ namespace BtcTransmuter.Controllers
{
if (!ModelState.IsValid)
{
return View(viewModel);
return this.ViewOrBadRequest(viewModel);
}
var recipe = new Recipe()
@ -162,9 +174,13 @@ namespace BtcTransmuter.Controllers
if (string.IsNullOrEmpty(recipe.Id))
{
ModelState.AddModelError(string.Empty, "Could not save recipe");
return View(viewModel);
return this.ViewOrBadRequest(viewModel);
}
if (this.IsApi())
{
return await EditRecipe(recipe.Id, (string) null);
}
return RedirectToAction("EditRecipe", new {id = recipe.Id, statusMessage = "Recipe created"});
}
@ -182,7 +198,7 @@ namespace BtcTransmuter.Controllers
{
group.RecipeActions = group.RecipeActions.OrderBy(action => action.Order).ToList();
});
return View(new EditRecipeViewModel()
return this.ViewOrJson(new EditRecipeViewModel()
{
Id = id,
StatusMessage = statusMessage,
@ -215,10 +231,14 @@ namespace BtcTransmuter.Controllers
await _recipeManager.AddOrUpdateRecipe(recipe);
if (this.IsApi())
{
return await EditRecipe(recipe.Id, (string) null);
}
return RedirectToAction("EditRecipe", new {id = recipe.Id, statusMessage = "Recipe edited"});
}
[HttpGet("{id}/action-groups/add")]
[HttpPost("{id}/action-groups/add")]
public virtual async Task<IActionResult> AddRecipeActionGroup(string id)
{
var recipe = await _recipeManager.GetRecipe(id, GetUserId());
@ -231,6 +251,10 @@ namespace BtcTransmuter.Controllers
{
RecipeId = recipe.Id
});
if (this.IsApi())
{
return await EditRecipe(recipe.Id, (string) null);
}
return RedirectToAction("EditRecipe", new {id = recipe.Id, statusMessage = "Recipe Action group added"});
}
@ -246,6 +270,10 @@ namespace BtcTransmuter.Controllers
}
await _recipeManager.RemoveRecipeActionGroup(actionGroupId);
if (this.IsApi())
{
return await EditRecipe(recipe.Id, (string) null);
}
return RedirectToAction("EditRecipe", new {id = recipe.Id, statusMessage = "Recipe Action group removed"});
}
@ -262,13 +290,21 @@ namespace BtcTransmuter.Controllers
}
await _recipeManager.ReorderRecipeActionGroupActions(actionGroupId,
vm.UpdateActionGroupOrderItems?.ToDictionary(item => item.RecipeActionId, item => item.Order));
if (this.IsApi())
{
return await EditRecipe(recipe.Id, (string) null);
}
return RedirectToAction("EditRecipe",
new {id = recipe.Id, statusMessage = "Recipe Action group order updated"});
}
private RedirectToActionResult GetNotFoundActionResult()
private ActionResult GetNotFoundActionResult()
{
if (this.IsApi())
{
return NotFound();
}
return RedirectToAction("GetRecipes", new
{
statusMessage = new StatusMessageModel()

View File

@ -1,6 +1,7 @@
using System.ComponentModel.DataAnnotations;
using BtcTransmuter.Data.Entities;
using Microsoft.AspNetCore.Mvc.Rendering;
using Newtonsoft.Json;
namespace BtcTransmuter.Models
{
@ -10,6 +11,7 @@ namespace BtcTransmuter.Models
[Display(Name = "Action Type")]
public string ActionId { get; set; }
public SelectList Actions { get; set; }
[JsonIgnore]
public string StatusMessage { get; set; }
public RecipeAction RecipeAction { get; set; }
public string RecipeActionGroupId { get; set; }

View File

@ -1,4 +1,5 @@
using System.ComponentModel.DataAnnotations;
using System.Text.Json.Serialization;
using BtcTransmuter.Data.Entities;
using Microsoft.AspNetCore.Mvc.Rendering;
@ -10,6 +11,7 @@ namespace BtcTransmuter.Models
[Display(Name = "Trigger Type")]
[Required] public string TriggerId { get; set; }
public SelectList Triggers { get; set; }
[JsonIgnore]
public string StatusMessage { get; set; }
public RecipeTrigger RecipeTrigger { get; set; }
}

View File

@ -1,13 +1,16 @@
using System.Collections.Generic;
using System.Text.Json.Serialization;
using BtcTransmuter.Data.Entities;
namespace BtcTransmuter.Models
{
public class GetRecipesViewModel
{
[JsonIgnore]
public string StatusMessage { get; set; }
public IEnumerable<Recipe> Recipes { get; set; }
public ListMode ViewMode { get; set; } = ListMode.Cards;
[JsonIgnore]
public ListMode ViewMode { get; set; } = ListMode.List;
public enum ListMode
{

View File

@ -27,7 +27,9 @@
"NBXplorer_Cryptos":"btc;ltc",
"NBXplorer_Uri": "http://127.0.0.1:32838/",
"NBXplorer_NetworkType":"Regtest",
"NBXplorer_UseDefaultCookie": "1"
"NBXplorer_UseDefaultCookie": "1",
"BTCPayAuthServer": "http://localhost:14142",
"DisableInternalAuth": "false"
},
"applicationUrl": "https://localhost:5001;http://localhost:5000"
},

View File

@ -3,6 +3,7 @@ using System.IO;
using System.Linq;
using BtcTransmuter.Abstractions.Extensions;
using BtcTransmuter.Abstractions.Helpers;
using BtcTransmuter.Auth;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Hosting;
@ -11,11 +12,18 @@ using Microsoft.EntityFrameworkCore;
using BtcTransmuter.Data;
using BtcTransmuter.Data.Entities;
using BtcTransmuter.Services;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Server.HttpSys;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
namespace BtcTransmuter
@ -25,7 +33,7 @@ namespace BtcTransmuter
private readonly IWebHostEnvironment _hostingEnvironment;
private readonly ILogger _logger;
public Startup(IWebHostEnvironment hostingEnvironment, IConfiguration configuration, ILoggerFactory logFactory)
public Startup(IWebHostEnvironment hostingEnvironment, IConfiguration configuration, ILoggerFactory logFactory)
{
_hostingEnvironment = hostingEnvironment;
_logger = logFactory.CreateLogger(nameof(Startup));
@ -45,8 +53,9 @@ namespace BtcTransmuter
identityOptions.Password.RequiredUniqueChars = 0;
identityOptions.Password.RequireNonAlphanumeric = false;
});
services.Configure<SecurityStampValidatorOptions>(validatorOptions => validatorOptions.ValidationInterval = TimeSpan.FromSeconds(50));
services.Configure<SecurityStampValidatorOptions>(validatorOptions =>
validatorOptions.ValidationInterval = TimeSpan.FromSeconds(50));
services.AddHttpClient();
services.AddOptions();
@ -84,8 +93,9 @@ namespace BtcTransmuter
{
//new install, no keys
dataProtectionBuilder.SetApplicationName(options.DataProtectionApplicationName);
using (StreamWriter sw = File.CreateText(markerFile)) {}
}else if (existingFiles.Contains("appnamemarker"))
using (File.CreateText(markerFile)){ }
}
else if (existingFiles.Contains("appnamemarker"))
{
//marker was found, we can use the app name
dataProtectionBuilder.SetApplicationName(options.DataProtectionApplicationName);
@ -95,18 +105,38 @@ namespace BtcTransmuter
//keys found with no marker, stay with old way
}
}
services.ConfigureApplicationCookie(x => {
x.Cookie.Name = ".AspNet.Cookie.btctransmuter";
});
services.AddDefaultIdentity<User>()
.AddRoles<IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
services.AddAuthentication(sharedOptions =>
{
sharedOptions.DefaultScheme = "smart";
sharedOptions.DefaultChallengeScheme = "smart";
})
.AddPolicyScheme("smart", "", options =>
{
options.ForwardDefaultSelector = context =>
{
var authHeader = context.Request.Headers["Authorization"].FirstOrDefault();
if (authHeader?.StartsWith("Basic ") is true)
{
return nameof(AuthenticationSchemes.Basic);
}
var mvcBuilder = services.AddMvc(mvcOptions => mvcOptions.EnableEndpointRouting = false);
return IdentityConstants.ApplicationScheme;
};
})
.AddCookie().AddBasicAuth();
services.ConfigureApplicationCookie(authenticationOptions => {
authenticationOptions.Cookie.Name = ".AspNet.Cookie.btctransmuter";
});
var mvcBuilder = services.AddMvc(mvcOptions => { mvcOptions.EnableEndpointRouting = false; })
.AddNewtonsoftJson().AddRazorRuntimeCompilation();
services.AddExtensions(options.ExtensionsDir, mvcBuilder);
}
@ -120,7 +150,7 @@ namespace BtcTransmuter
using (var context = scope.ServiceProvider.GetService<ApplicationDbContext>())
{
if (context.Database.IsSqlite())
{
{
context.Database.EnsureCreated();
}
else
@ -155,6 +185,7 @@ namespace BtcTransmuter
app.UseStaticFiles(options.RootPath);
app.UsePathBase(options.RootPath);
app.UseAuthentication();
app.UseExtensions();
JsonConvert.DefaultSettings = () => new JsonSerializerSettings()
{
@ -163,15 +194,13 @@ namespace BtcTransmuter
.Where(extension => extension.JsonConverters != null)
.SelectMany(extension => extension.JsonConverters).ToList()
};
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
}
}
}

Some files were not shown because too many files have changed in this diff Show More