Compare commits
38 Commits
v1.12.4-rc
...
v1.12.6-r0
Author | SHA1 | Date | |
---|---|---|---|
7d67f729c8 | |||
f08608f766 | |||
2d370b8cea | |||
2c480f57c2 | |||
b1c171a5d9 | |||
692a13e0c8 | |||
d9b6e465c0 | |||
a4485b5377 | |||
4c0c2d2e94 | |||
89062fcb10 | |||
a2087ce722 | |||
312997c063 | |||
9380d4ca48 | |||
d44ec19663 | |||
12c871bfd8 | |||
f86f858499 | |||
7675dce000 | |||
b9ef41b8c3 | |||
18fe420b74 | |||
b7be93c569 | |||
cd01a7b727 | |||
b96e73a002 | |||
0bf22ddf29 | |||
1c4dc382a8 | |||
71c5566f2b | |||
6621859567 | |||
6437967e60 | |||
c5a926c50c | |||
85ab691b68 | |||
4d3e0ab599 | |||
02663a149e | |||
a8fdc4798d | |||
6290b0f3bf | |||
411e0334d0 | |||
b174977bc7 | |||
2111b67e2c | |||
b96cfcd14d | |||
95bf60c252 |
BTCPayServer.Client
BTCPayServer.Data
Data
Migrations
BTCPayServer.Rating
BTCPayServer.Tests
BTCPayServer.Tests.csprojBTCPayServerTester.csCheckoutv2Tests.csFastTests.csGreenfieldAPITests.csSeleniumTester.csSeleniumTests.csThirdPartyTests.csUnitTest1.cs
BTCPayServer
BTCPayServer.csproj
Components/StoreWalletBalance
Controllers
GreenField
GreenfieldPullPaymentController.csGreenfieldStoreOnChainWalletsController.csGreenfieldUsersController.csLocalBTCPayServerClient.cs
UIAccountController.csUIBoltcardController.csUILNURLController.csUIPullPaymentController.csUIServerController.Users.csUIStoresController.Email.csUIWalletsController.PSBT.csUIWalletsController.csData
Events
Extensions
HostedServices
Hosting
Models
PayoutProcessors/Lightning
Plugins
BoltcardBalance
BoltcardFactory
BoltcardTopUp
PointOfSale/Controllers
Security
Services
Views
Shared
UIAccount
ForgotPassword.cshtmlLockout.cshtmlLogin.cshtmlRegister.cshtmlSecondaryLogin.cshtmlSetPassword.cshtml
UIPullPayment
UIServer
UIStores
UIWallets
wwwroot
main
pos
swagger/v1
vendor/chartist
Build
Changelog.mdRELEASE-CYCLES.md@ -41,6 +41,14 @@ namespace BTCPayServer.Client
|
||||
return response.IsSuccessStatusCode;
|
||||
}
|
||||
|
||||
public virtual async Task<bool> ApproveUser(string idOrEmail, bool approved, CancellationToken token = default)
|
||||
{
|
||||
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/users/{idOrEmail}/approve", null,
|
||||
new ApproveUserRequest { Approved = approved }, HttpMethod.Post), token);
|
||||
await HandleResponse(response);
|
||||
return response.IsSuccessStatusCode;
|
||||
}
|
||||
|
||||
public virtual async Task<ApplicationUserData[]> GetUsers(CancellationToken token = default)
|
||||
{
|
||||
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/users/", null, HttpMethod.Get), token);
|
||||
|
@ -25,6 +25,16 @@ namespace BTCPayServer.Client.Models
|
||||
/// </summary>
|
||||
public bool RequiresEmailConfirmation { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the user was approved by an admin
|
||||
/// </summary>
|
||||
public bool Approved { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// whether the user needed approval on account creation
|
||||
/// </summary>
|
||||
public bool RequiresApproval { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// the roles of the user
|
||||
/// </summary>
|
||||
|
6
BTCPayServer.Client/Models/ApproveUserRequest.cs
Normal file
6
BTCPayServer.Client/Models/ApproveUserRequest.cs
Normal file
@ -0,0 +1,6 @@
|
||||
namespace BTCPayServer.Client;
|
||||
|
||||
public class ApproveUserRequest
|
||||
{
|
||||
public bool Approved { get; set; }
|
||||
}
|
@ -23,5 +23,7 @@ namespace BTCPayServer.Client.Models
|
||||
public DateTimeOffset? StartsAt { get; set; }
|
||||
public string[] PaymentMethods { get; set; }
|
||||
public bool AutoApproveClaims { get; set; }
|
||||
public string EmbeddedCSS { get; set; }
|
||||
public string CustomCSSLink { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ using System.Text;
|
||||
using NBitcoin.JsonConverters;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Client.Models
|
||||
{
|
||||
@ -14,11 +15,15 @@ namespace BTCPayServer.Client.Models
|
||||
}
|
||||
public class RegisterBoltcardRequest
|
||||
{
|
||||
[JsonProperty("LNURLW")]
|
||||
public string LNURLW { get; set; }
|
||||
[JsonConverter(typeof(HexJsonConverter))]
|
||||
[JsonProperty("UID")]
|
||||
public byte[] UID { get; set; }
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
public OnExistingBehavior? OnExisting { get; set; }
|
||||
[JsonExtensionData]
|
||||
public IDictionary<string, JToken> AdditionalData { get; set; } = new Dictionary<string, JToken>();
|
||||
}
|
||||
public class RegisterBoltcardResponse
|
||||
{
|
||||
|
@ -11,6 +11,8 @@ namespace BTCPayServer.Data
|
||||
public class ApplicationUser : IdentityUser, IHasBlob<UserBlob>
|
||||
{
|
||||
public bool RequiresEmailConfirmation { get; set; }
|
||||
public bool RequiresApproval { get; set; }
|
||||
public bool Approved { get; set; }
|
||||
public List<StoredFile> StoredFiles { get; set; }
|
||||
[Obsolete("U2F support has been replace with FIDO2")]
|
||||
public List<U2FDevice> U2FDevices { get; set; }
|
||||
|
@ -43,7 +43,7 @@ public class LightningAddressDataBlob
|
||||
public decimal? Max { get; set; }
|
||||
|
||||
public JObject InvoiceMetadata { get; set; }
|
||||
|
||||
public string PullPaymentId { get; set; }
|
||||
[JsonExtensionData] public Dictionary<string, JToken> AdditionalData { get; set; }
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,39 @@
|
||||
using BTCPayServer.Data;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace BTCPayServer.Migrations
|
||||
{
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
[Migration("20240104155620_AddApprovalToApplicationUser")]
|
||||
public partial class AddApprovalToApplicationUser : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "Approved",
|
||||
table: "AspNetUsers",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "RequiresApproval",
|
||||
table: "AspNetUsers",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Approved",
|
||||
table: "AspNetUsers");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "RequiresApproval",
|
||||
table: "AspNetUsers");
|
||||
}
|
||||
}
|
||||
}
|
@ -112,6 +112,9 @@ namespace BTCPayServer.Migrations
|
||||
b.Property<int>("AccessFailedCount")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("Approved")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<byte[]>("Blob")
|
||||
.HasColumnType("BLOB");
|
||||
|
||||
@ -158,6 +161,9 @@ namespace BTCPayServer.Migrations
|
||||
b.Property<bool>("PhoneNumberConfirmed")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("RequiresApproval")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("RequiresEmailConfirmation")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
[
|
||||
[
|
||||
{
|
||||
"name":"Afghan Afghani",
|
||||
"code":"AFN",
|
||||
@ -58,7 +58,7 @@
|
||||
{
|
||||
"name":"Argentine Peso",
|
||||
"code":"ARS",
|
||||
"divisibility":2,
|
||||
"divisibility":0,
|
||||
"symbol":null,
|
||||
"crypto":false
|
||||
},
|
||||
@ -289,7 +289,7 @@
|
||||
{
|
||||
"name":"Colombian Peso",
|
||||
"code":"COP",
|
||||
"divisibility":2,
|
||||
"divisibility":0,
|
||||
"symbol":null,
|
||||
"crypto":false
|
||||
},
|
||||
|
@ -77,7 +77,15 @@ namespace BTCPayServer.Services.Rates
|
||||
continue;
|
||||
try
|
||||
{
|
||||
_CurrencyProviders.TryAdd(new RegionInfo(culture.LCID).ISOCurrencySymbol, culture);
|
||||
var symbol = new RegionInfo(culture.LCID).ISOCurrencySymbol;
|
||||
var c = symbol switch
|
||||
{
|
||||
// ARS and COP are officially 2 digits, but due to depreciation,
|
||||
// nobody really use those anymore. (See https://github.com/btcpayserver/btcpayserver/issues/5708)
|
||||
"ARS" or "COP" => ModifyCurrencyDecimalDigit(culture, 0),
|
||||
_ => culture
|
||||
};
|
||||
_CurrencyProviders.TryAdd(symbol, c);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
@ -91,6 +99,15 @@ namespace BTCPayServer.Services.Rates
|
||||
}
|
||||
}
|
||||
|
||||
private CultureInfo ModifyCurrencyDecimalDigit(CultureInfo culture, int decimals)
|
||||
{
|
||||
var modifiedCulture = new CultureInfo(culture.Name);
|
||||
NumberFormatInfo modifiedNumberFormat = (NumberFormatInfo)modifiedCulture.NumberFormat.Clone();
|
||||
modifiedNumberFormat.CurrencyDecimalDigits = decimals;
|
||||
modifiedCulture.NumberFormat = modifiedNumberFormat;
|
||||
return modifiedCulture;
|
||||
}
|
||||
|
||||
private void AddCurrency(Dictionary<string, IFormatProvider> currencyProviders, string code, int divisibility, string symbol)
|
||||
{
|
||||
var culture = new CultureInfo("en-US");
|
||||
|
40
BTCPayServer.Rating/Providers/BitnobRateProvider.cs
Normal file
40
BTCPayServer.Rating/Providers/BitnobRateProvider.cs
Normal file
@ -0,0 +1,40 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Rating.Providers
|
||||
{
|
||||
public class BitnobRateProvider : IRateProvider
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
public BitnobRateProvider(HttpClient httpClient)
|
||||
{
|
||||
_httpClient = httpClient ?? new HttpClient();
|
||||
}
|
||||
public RateSourceInfo RateSourceInfo => new("bitnob", "Bitnob", "https://api.bitnob.co/api/v1/rates/bitcoin/price");
|
||||
|
||||
public async Task<PairRate[]> GetRatesAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
using var response = await _httpClient.GetAsync("https://api.bitnob.co/api/v1/rates/bitcoin/price", cancellationToken);
|
||||
JObject jobj = await response.Content.ReadAsAsync<JObject>(cancellationToken);
|
||||
var dataObject = jobj["data"] as JObject;
|
||||
|
||||
if (dataObject == null)
|
||||
{
|
||||
return Array.Empty<PairRate>();
|
||||
}
|
||||
var pairRates = new List<PairRate>();
|
||||
foreach (var property in dataObject.Properties())
|
||||
{
|
||||
string[] parts = property.Name.Split('_');
|
||||
decimal value = property.Value.Value<decimal>();
|
||||
pairRates.Add(new PairRate(new CurrencyPair("BTC", parts[1]), new BidAsk(value)));
|
||||
}
|
||||
return pairRates.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
@ -8,6 +8,7 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Rating;
|
||||
using ExchangeSharp;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
@ -16,7 +17,7 @@ namespace BTCPayServer.Services.Rates
|
||||
// Make sure that only one request is sent to kraken in general
|
||||
public class KrakenExchangeRateProvider : IRateProvider
|
||||
{
|
||||
public RateSourceInfo RateSourceInfo => new("kraken", "Kraken", "https://api.kraken.com/0/public/Ticker?pair=ATOMETH,ATOMEUR,ATOMUSD,ATOMXBT,BATETH,BATEUR,BATUSD,BATXBT,BCHEUR,BCHUSD,BCHXBT,DAIEUR,DAIUSD,DAIUSDT,DASHEUR,DASHUSD,DASHXBT,EOSETH,EOSXBT,ETHCHF,ETHDAI,ETHUSDC,ETHUSDT,GNOETH,GNOXBT,ICXETH,ICXEUR,ICXUSD,ICXXBT,LINKETH,LINKEUR,LINKUSD,LINKXBT,LSKETH,LSKEUR,LSKUSD,LSKXBT,NANOETH,NANOEUR,NANOUSD,NANOXBT,OMGETH,OMGEUR,OMGUSD,OMGXBT,PAXGETH,PAXGEUR,PAXGUSD,PAXGXBT,SCETH,SCEUR,SCUSD,SCXBT,USDCEUR,USDCUSD,USDCUSDT,USDTCAD,USDTEUR,USDTGBP,USDTZUSD,WAVESETH,WAVESEUR,WAVESUSD,WAVESXBT,XBTCHF,XBTDAI,XBTUSDC,XBTUSDT,XDGEUR,XDGUSD,XETCXETH,XETCXXBT,XETCZEUR,XETCZUSD,XETHXXBT,XETHZCAD,XETHZEUR,XETHZGBP,XETHZJPY,XETHZUSD,XLTCXXBT,XLTCZEUR,XLTCZUSD,XMLNXETH,XMLNXXBT,XMLNZEUR,XMLNZUSD,XREPXETH,XREPXXBT,XREPZEUR,XXBTZCAD,XXBTZEUR,XXBTZGBP,XXBTZJPY,XXBTZUSD,XXDGXXBT,XXLMXXBT,XXMRXXBT,XXMRZEUR,XXMRZUSD,XXRPXXBT,XXRPZEUR,XXRPZUSD,XZECXXBT,XZECZEUR,XZECZUSD");
|
||||
public RateSourceInfo RateSourceInfo => new("kraken", "Kraken", "https://api.kraken.com/0/public/Ticker");
|
||||
public HttpClient HttpClient
|
||||
{
|
||||
get
|
||||
@ -31,39 +32,6 @@ namespace BTCPayServer.Services.Rates
|
||||
|
||||
HttpClient _LocalClient;
|
||||
static readonly HttpClient _Client = new HttpClient();
|
||||
|
||||
// ExchangeSymbolToGlobalSymbol throws exception which would kill perf
|
||||
readonly ConcurrentDictionary<string, string> notFoundSymbols = new ConcurrentDictionary<string, string>(new Dictionary<string, string>()
|
||||
{
|
||||
{"ADAXBT","ADAXBT"},
|
||||
{ "BSVUSD","BSVUSD"},
|
||||
{ "QTUMEUR","QTUMEUR"},
|
||||
{ "QTUMXBT","QTUMXBT"},
|
||||
{ "EOSUSD","EOSUSD"},
|
||||
{ "XTZUSD","XTZUSD"},
|
||||
{ "XREPZUSD","XREPZUSD"},
|
||||
{ "ADAEUR","ADAEUR"},
|
||||
{ "ADAUSD","ADAUSD"},
|
||||
{ "GNOEUR","GNOEUR"},
|
||||
{ "XTZETH","XTZETH"},
|
||||
{ "XXRPZJPY","XXRPZJPY"},
|
||||
{ "XXRPZCAD","XXRPZCAD"},
|
||||
{ "XTZEUR","XTZEUR"},
|
||||
{ "QTUMETH","QTUMETH"},
|
||||
{ "XXLMZUSD","XXLMZUSD"},
|
||||
{ "QTUMCAD","QTUMCAD"},
|
||||
{ "QTUMUSD","QTUMUSD"},
|
||||
{ "XTZXBT","XTZXBT"},
|
||||
{ "GNOUSD","GNOUSD"},
|
||||
{ "ADAETH","ADAETH"},
|
||||
{ "ADACAD","ADACAD"},
|
||||
{ "XTZCAD","XTZCAD"},
|
||||
{ "BSVEUR","BSVEUR"},
|
||||
{ "XZECZJPY","XZECZJPY"},
|
||||
{ "XXLMZEUR","XXLMZEUR"},
|
||||
{"EOSEUR","EOSEUR"},
|
||||
{"BSVXBT","BSVXBT"}
|
||||
});
|
||||
string[] _Symbols = Array.Empty<string>();
|
||||
DateTimeOffset? _LastSymbolUpdate = null;
|
||||
readonly Dictionary<string, string> _TickerMapping = new Dictionary<string, string>()
|
||||
@ -76,47 +44,57 @@ namespace BTCPayServer.Services.Rates
|
||||
{ "ZEUR", "EUR" },
|
||||
{ "ZJPY", "JPY" },
|
||||
{ "ZCAD", "CAD" },
|
||||
{ "ZGBP", "GBP" }
|
||||
{ "ZGBP", "GBP" },
|
||||
{ "XXMR", "XMR" },
|
||||
{ "XETH", "ETH" },
|
||||
{ "USDC", "USDC" }, // On A=A purpose
|
||||
{ "XZEC", "ZEC" },
|
||||
{ "XLTC", "LTC" },
|
||||
{ "XXRP", "XRP" },
|
||||
};
|
||||
|
||||
string Normalize(string ticker)
|
||||
{
|
||||
_TickerMapping.TryGetValue(ticker, out var normalized);
|
||||
return normalized ?? ticker;
|
||||
}
|
||||
|
||||
readonly ConcurrentDictionary<string, CurrencyPair> CachedCurrencyPairs = new ConcurrentDictionary<string, CurrencyPair>();
|
||||
private CurrencyPair GetCurrencyPair(string symbol)
|
||||
{
|
||||
if (CachedCurrencyPairs.TryGetValue(symbol, out var pair))
|
||||
return pair;
|
||||
var found = _TickerMapping.Where(t => symbol.StartsWith(t.Key, StringComparison.OrdinalIgnoreCase))
|
||||
.Select(t => new { KrakenTicker = t.Key, PayTicker = t.Value }).FirstOrDefault();
|
||||
if (found is not null)
|
||||
{
|
||||
pair = new CurrencyPair(found.PayTicker, Normalize(symbol.Substring(found.KrakenTicker.Length)));
|
||||
}
|
||||
if (pair is null)
|
||||
{
|
||||
found = _TickerMapping.Where(t => symbol.EndsWith(t.Key, StringComparison.OrdinalIgnoreCase))
|
||||
.Select(t => new { KrakenTicker = t.Key, PayTicker = t.Value }).FirstOrDefault();
|
||||
if (found is not null)
|
||||
pair = new CurrencyPair(Normalize(symbol.Substring(0, symbol.Length - found.KrakenTicker.Length)), found.PayTicker);
|
||||
}
|
||||
if (pair is null)
|
||||
CurrencyPair.TryParse(symbol, out pair);
|
||||
CachedCurrencyPairs.TryAdd(symbol, pair);
|
||||
return pair;
|
||||
}
|
||||
public async Task<PairRate[]> GetRatesAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var result = new List<PairRate>();
|
||||
var symbols = await GetSymbolsAsync(cancellationToken);
|
||||
var helper = (ExchangeKrakenAPI)await ExchangeAPI.GetExchangeAPIAsync<ExchangeKrakenAPI>();
|
||||
var normalizedPairsList = symbols.Where(s => !notFoundSymbols.ContainsKey(s)).Select(s => helper.NormalizeMarketSymbol(s)).ToList();
|
||||
var csvPairsList = string.Join(",", normalizedPairsList);
|
||||
JToken apiTickers = await MakeJsonRequestAsync<JToken>("/0/public/Ticker", null, new Dictionary<string, object> { { "pair", csvPairsList } }, cancellationToken: cancellationToken);
|
||||
JToken apiTickers = await MakeJsonRequestAsync<JToken>("/0/public/Ticker", null, null, cancellationToken: cancellationToken);
|
||||
foreach (string symbol in symbols)
|
||||
{
|
||||
var ticker = ConvertToExchangeTicker(symbol, apiTickers[symbol]);
|
||||
if (ticker != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
string global = null;
|
||||
var mapped1 = _TickerMapping.Where(t => symbol.StartsWith(t.Key, StringComparison.OrdinalIgnoreCase))
|
||||
.Select(t => new { KrakenTicker = t.Key, PayTicker = t.Value }).SingleOrDefault();
|
||||
if (mapped1 != null)
|
||||
{
|
||||
var p2 = symbol.Substring(mapped1.KrakenTicker.Length);
|
||||
if (_TickerMapping.TryGetValue(p2, out var mapped2))
|
||||
p2 = mapped2;
|
||||
global = $"{mapped1.PayTicker}_{p2}";
|
||||
}
|
||||
else
|
||||
{
|
||||
global = await helper.ExchangeMarketSymbolToGlobalMarketSymbolAsync(symbol);
|
||||
}
|
||||
if (CurrencyPair.TryParse(global, out var pair))
|
||||
result.Add(new PairRate(pair, new BidAsk(ticker.Bid, ticker.Ask)));
|
||||
else
|
||||
notFoundSymbols.TryAdd(symbol, symbol);
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
notFoundSymbols.TryAdd(symbol, symbol);
|
||||
}
|
||||
var pair = GetCurrencyPair(symbol);
|
||||
if (pair is not null)
|
||||
result.Add(new PairRate(pair, new BidAsk(ticker.Bid, ticker.Ask)));
|
||||
}
|
||||
}
|
||||
return result.ToArray();
|
||||
|
@ -24,7 +24,7 @@
|
||||
<PackageReference Include="Newtonsoft.Json.Schema" Version="3.0.15" />
|
||||
<PackageReference Include="Selenium.Support" Version="4.1.1" />
|
||||
<PackageReference Include="Selenium.WebDriver" Version="4.1.1" />
|
||||
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="119.0.6045.10500" />
|
||||
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="121.0.6167.8500" />
|
||||
<PackageReference Include="xunit" Version="2.6.6" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.6">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
|
@ -245,6 +245,9 @@ namespace BTCPayServer.Tests
|
||||
rateProvider.Providers.Add("kraken", kraken);
|
||||
}
|
||||
|
||||
// reset test server policies
|
||||
var settings = GetService<SettingsRepository>();
|
||||
await settings.UpdateSetting(new PoliciesSettings { LockSubscription = false, RequiresUserApproval = false });
|
||||
|
||||
TestLogs.LogInformation("Waiting site is operational...");
|
||||
await WaitSiteIsOperational();
|
||||
|
@ -360,10 +360,13 @@ namespace BTCPayServer.Tests
|
||||
expirySeconds.SendKeys("5");
|
||||
s.Driver.FindElement(By.Id("Expire")).Click();
|
||||
|
||||
paymentInfo = s.Driver.WaitForElement(By.Id("PaymentInfo"));
|
||||
Assert.Contains("This invoice will expire in", paymentInfo.Text);
|
||||
Assert.Contains("00:0", paymentInfo.Text);
|
||||
Assert.DoesNotContain("Please send", paymentInfo.Text);
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
paymentInfo = s.Driver.WaitForElement(By.Id("PaymentInfo"));
|
||||
Assert.Contains("This invoice will expire in", paymentInfo.Text);
|
||||
Assert.Contains("00:0", paymentInfo.Text);
|
||||
Assert.DoesNotContain("Please send", paymentInfo.Text);
|
||||
});
|
||||
|
||||
// Configure countdown timer
|
||||
s.GoToHome();
|
||||
|
@ -790,13 +790,15 @@ namespace BTCPayServer.Tests
|
||||
(0.0005m, "0.0005 USD", "USD"), (0.001m, "0.001 USD", "USD"), (0.01m, "0.01 USD", "USD"),
|
||||
(0.1m, "0.10 USD", "USD"), (0.1m, "0,10 EUR", "EUR"), (1000m, "1,000 JPY", "JPY"),
|
||||
(1000.0001m, "1,000.00 INR", "INR"),
|
||||
(0.0m, "0.00 USD", "USD")
|
||||
(0.0m, "0.00 USD", "USD"), (1m, "1 COP", "COP"), (1m, "1 ARS", "ARS")
|
||||
})
|
||||
{
|
||||
var actual = displayFormatter.Currency(test.Item1, test.Item3);
|
||||
actual = actual.Replace("¥", "¥"); // Hack so JPY test pass on linux as well
|
||||
Assert.Equal(test.Item2, actual);
|
||||
}
|
||||
Assert.Equal(0, CurrencyNameTable.Instance.GetNumberFormatInfo("ARS").CurrencyDecimalDigits);
|
||||
Assert.Equal(0, CurrencyNameTable.Instance.GetNumberFormatInfo("COP").CurrencyDecimalDigits);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
@ -13,6 +13,7 @@ using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Events;
|
||||
using BTCPayServer.Lightning;
|
||||
using BTCPayServer.Models.InvoicingModels;
|
||||
using BTCPayServer.NTag424;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.PayoutProcessors;
|
||||
@ -24,6 +25,7 @@ using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NBitcoin;
|
||||
using NBitcoin.DataEncoders;
|
||||
using NBitpayClient;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
@ -694,14 +696,10 @@ namespace BTCPayServer.Tests
|
||||
// Try loading 1 user by email. Loading myself.
|
||||
await AssertHttpError(403, async () => await badClient.GetUserByIdOrEmail(badUser.Email));
|
||||
|
||||
|
||||
|
||||
|
||||
// Why is this line needed? I saw it in "CanDeleteUsersViaApi" as well. Is this part of the cleanup?
|
||||
tester.Stores.Remove(adminUser.StoreId);
|
||||
}
|
||||
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanCreateUsersViaAPI()
|
||||
@ -1119,6 +1117,35 @@ namespace BTCPayServer.Tests
|
||||
OnExisting = OnExistingBehavior.KeepVersion
|
||||
});
|
||||
Assert.Equal(card2.Version, card3.Version);
|
||||
var p = new byte[] { 0xc7 }.Concat(uid).Concat(new byte[8]).ToArray();
|
||||
var card4 = await client.RegisterBoltcard(test4.Id, new RegisterBoltcardRequest()
|
||||
{
|
||||
OnExisting = OnExistingBehavior.KeepVersion,
|
||||
LNURLW = card2.LNURLW + $"?p={Encoders.Hex.EncodeData(AESKey.Parse(card2.K1).Encrypt(p))}"
|
||||
});
|
||||
Assert.Equal(card2.Version, card4.Version);
|
||||
Assert.Equal(card2.K4, card4.K4);
|
||||
// Can't define both properties
|
||||
await AssertValidationError(["LNURLW"], () => client.RegisterBoltcard(test4.Id, new RegisterBoltcardRequest()
|
||||
{
|
||||
OnExisting = OnExistingBehavior.KeepVersion,
|
||||
UID = uid,
|
||||
LNURLW = card2.LNURLW + $"?p={Encoders.Hex.EncodeData(AESKey.Parse(card2.K1).Encrypt(p))}"
|
||||
}));
|
||||
// p is malformed
|
||||
await AssertValidationError(["LNURLW"], () => client.RegisterBoltcard(test4.Id, new RegisterBoltcardRequest()
|
||||
{
|
||||
OnExisting = OnExistingBehavior.KeepVersion,
|
||||
UID = uid,
|
||||
LNURLW = card2.LNURLW + $"?p=lol"
|
||||
}));
|
||||
// p is invalid
|
||||
p[0] = 0;
|
||||
await AssertValidationError(["LNURLW"], () => client.RegisterBoltcard(test4.Id, new RegisterBoltcardRequest()
|
||||
{
|
||||
OnExisting = OnExistingBehavior.KeepVersion,
|
||||
LNURLW = card2.LNURLW + $"?p={Encoders.Hex.EncodeData(AESKey.Parse(card2.K1).Encrypt(p))}"
|
||||
}));
|
||||
// Test with SATS denomination values
|
||||
var testSats = await client.CreatePullPayment(storeId, new Client.Models.CreatePullPaymentRequest()
|
||||
{
|
||||
@ -3571,6 +3598,78 @@ namespace BTCPayServer.Tests
|
||||
await newUserBasicClient.GetCurrentUser();
|
||||
}
|
||||
|
||||
[Fact(Timeout = 60 * 2 * 1000)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task ApproveUserTests()
|
||||
{
|
||||
using var tester = CreateServerTester();
|
||||
await tester.StartAsync();
|
||||
var admin = tester.NewAccount();
|
||||
await admin.GrantAccessAsync(true);
|
||||
var adminClient = await admin.CreateClient(Policies.Unrestricted);
|
||||
Assert.False((await adminClient.GetUserByIdOrEmail(admin.UserId)).RequiresApproval);
|
||||
Assert.Empty(await adminClient.GetNotifications());
|
||||
|
||||
// require approval
|
||||
var settings = tester.PayTester.GetService<SettingsRepository>();
|
||||
await settings.UpdateSetting(new PoliciesSettings { LockSubscription = false, RequiresUserApproval = true });
|
||||
|
||||
// new user needs approval
|
||||
var unapprovedUser = tester.NewAccount();
|
||||
await unapprovedUser.GrantAccessAsync();
|
||||
var unapprovedUserBasicAuthClient = await unapprovedUser.CreateClient();
|
||||
await AssertAPIError("unauthenticated", async () =>
|
||||
{
|
||||
await unapprovedUserBasicAuthClient.GetCurrentUser();
|
||||
});
|
||||
var unapprovedUserApiKeyClient = await unapprovedUser.CreateClient(Policies.Unrestricted);
|
||||
await AssertAPIError("unauthenticated", async () =>
|
||||
{
|
||||
await unapprovedUserApiKeyClient.GetCurrentUser();
|
||||
});
|
||||
Assert.True((await adminClient.GetUserByIdOrEmail(unapprovedUser.UserId)).RequiresApproval);
|
||||
Assert.False((await adminClient.GetUserByIdOrEmail(unapprovedUser.UserId)).Approved);
|
||||
Assert.Single(await adminClient.GetNotifications(false));
|
||||
|
||||
// approve
|
||||
Assert.True(await adminClient.ApproveUser(unapprovedUser.UserId, true, CancellationToken.None));
|
||||
Assert.True((await adminClient.GetUserByIdOrEmail(unapprovedUser.UserId)).Approved);
|
||||
Assert.True((await unapprovedUserApiKeyClient.GetCurrentUser()).Approved);
|
||||
Assert.True((await unapprovedUserBasicAuthClient.GetCurrentUser()).Approved);
|
||||
|
||||
// un-approve
|
||||
Assert.True(await adminClient.ApproveUser(unapprovedUser.UserId, false, CancellationToken.None));
|
||||
Assert.False((await adminClient.GetUserByIdOrEmail(unapprovedUser.UserId)).Approved);
|
||||
await AssertAPIError("unauthenticated", async () =>
|
||||
{
|
||||
await unapprovedUserApiKeyClient.GetCurrentUser();
|
||||
});
|
||||
await AssertAPIError("unauthenticated", async () =>
|
||||
{
|
||||
await unapprovedUserBasicAuthClient.GetCurrentUser();
|
||||
});
|
||||
|
||||
// reset policies to not require approval
|
||||
await settings.UpdateSetting(new PoliciesSettings { LockSubscription = false, RequiresUserApproval = false });
|
||||
|
||||
// new user does not need approval
|
||||
var newUser = tester.NewAccount();
|
||||
await newUser.GrantAccessAsync();
|
||||
var newUserBasicAuthClient = await newUser.CreateClient();
|
||||
var newUserApiKeyClient = await newUser.CreateClient(Policies.Unrestricted);
|
||||
Assert.False((await newUserApiKeyClient.GetCurrentUser()).RequiresApproval);
|
||||
Assert.False((await newUserApiKeyClient.GetCurrentUser()).Approved);
|
||||
Assert.False((await newUserBasicAuthClient.GetCurrentUser()).RequiresApproval);
|
||||
Assert.False((await newUserBasicAuthClient.GetCurrentUser()).Approved);
|
||||
Assert.Single(await adminClient.GetNotifications(false));
|
||||
|
||||
// try unapproving user which does not have the RequiresApproval flag
|
||||
await AssertAPIError("invalid-state", async () =>
|
||||
{
|
||||
await adminClient.ApproveUser(newUser.UserId, false, CancellationToken.None);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact(Timeout = 60 * 2 * 1000)]
|
||||
[Trait("Integration", "Integration")]
|
||||
[Trait("Lightning", "Lightning")]
|
||||
|
@ -8,6 +8,7 @@ using BTCPayServer.Abstractions.Models;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Lightning;
|
||||
using BTCPayServer.Lightning.CLightning;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Views.Manage;
|
||||
using BTCPayServer.Views.Server;
|
||||
using BTCPayServer.Views.Stores;
|
||||
@ -17,6 +18,7 @@ using NBitcoin;
|
||||
using NBitcoin.RPC;
|
||||
using OpenQA.Selenium;
|
||||
using OpenQA.Selenium.Chrome;
|
||||
using OpenQA.Selenium.Support.Extensions;
|
||||
using OpenQA.Selenium.Support.UI;
|
||||
using Xunit;
|
||||
|
||||
@ -76,6 +78,7 @@ namespace BTCPayServer.Tests
|
||||
// A bit less than test timeout
|
||||
TimeSpan.FromSeconds(50));
|
||||
}
|
||||
|
||||
ServerUri = Server.PayTester.ServerUri;
|
||||
Driver.Manage().Window.Maximize();
|
||||
|
||||
|
@ -16,6 +16,7 @@ using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Lightning;
|
||||
using BTCPayServer.Models.InvoicingModels;
|
||||
using BTCPayServer.NTag424;
|
||||
@ -405,6 +406,148 @@ namespace BTCPayServer.Tests
|
||||
Assert.Contains("/login", s.Driver.Url);
|
||||
}
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
public async Task CanRequireApprovalForNewAccounts()
|
||||
{
|
||||
using var s = CreateSeleniumTester();
|
||||
await s.StartAsync();
|
||||
|
||||
var settings = s.Server.PayTester.GetService<SettingsRepository>();
|
||||
var policies = await settings.GetSettingAsync<PoliciesSettings>() ?? new PoliciesSettings();
|
||||
Assert.True(policies.EnableRegistration);
|
||||
Assert.False(policies.RequiresUserApproval);
|
||||
|
||||
// Register admin and adapt policies
|
||||
s.RegisterNewUser(true);
|
||||
var admin = s.AsTestAccount();
|
||||
s.GoToHome();
|
||||
s.GoToServer(ServerNavPages.Policies);
|
||||
Assert.True(s.Driver.FindElement(By.Id("EnableRegistration")).Selected);
|
||||
Assert.False(s.Driver.FindElement(By.Id("RequiresUserApproval")).Selected);
|
||||
s.Driver.FindElement(By.Id("RequiresUserApproval")).Click();
|
||||
s.Driver.FindElement(By.Id("SaveButton")).Click();
|
||||
Assert.Contains("Policies updated successfully", s.FindAlertMessage().Text);
|
||||
Assert.True(s.Driver.FindElement(By.Id("RequiresUserApproval")).Selected);
|
||||
|
||||
// Check user create view has approval checkbox
|
||||
s.GoToServer(ServerNavPages.Users);
|
||||
s.Driver.FindElement(By.Id("CreateUser")).Click();
|
||||
Assert.False(s.Driver.FindElement(By.Id("Approved")).Selected);
|
||||
|
||||
// Ensure there is no unread notification yet
|
||||
s.Driver.ElementDoesNotExist(By.Id("NotificationsBadge"));
|
||||
s.Logout();
|
||||
|
||||
// Register user and try to log in
|
||||
s.GoToRegister();
|
||||
s.RegisterNewUser();
|
||||
s.Driver.AssertNoError();
|
||||
Assert.Contains("Account created. The new account requires approval by an admin before you can log in", s.FindAlertMessage().Text);
|
||||
Assert.Contains("/login", s.Driver.Url);
|
||||
|
||||
var unapproved = s.AsTestAccount();
|
||||
s.LogIn(unapproved.RegisterDetails.Email, unapproved.RegisterDetails.Password);
|
||||
Assert.Contains("Your user account requires approval by an admin before you can log in", s.FindAlertMessage(StatusMessageModel.StatusSeverity.Warning).Text);
|
||||
Assert.Contains("/login", s.Driver.Url);
|
||||
|
||||
// Login with admin
|
||||
s.GoToLogin();
|
||||
s.LogIn(admin.RegisterDetails.Email, admin.RegisterDetails.Password);
|
||||
s.GoToHome();
|
||||
|
||||
// Check notification
|
||||
TestUtils.Eventually(() => Assert.Equal("1", s.Driver.FindElement(By.Id("NotificationsBadge")).Text));
|
||||
s.Driver.FindElement(By.Id("NotificationsHandle")).Click();
|
||||
Assert.Matches($"New user {unapproved.RegisterDetails.Email} requires approval", s.Driver.FindElement(By.CssSelector("#NotificationsList .notification")).Text);
|
||||
s.Driver.FindElement(By.Id("NotificationsMarkAllAsSeen")).Click();
|
||||
|
||||
// Reset approval policy
|
||||
s.GoToServer(ServerNavPages.Policies);
|
||||
Assert.True(s.Driver.FindElement(By.Id("EnableRegistration")).Selected);
|
||||
Assert.True(s.Driver.FindElement(By.Id("RequiresUserApproval")).Selected);
|
||||
s.Driver.FindElement(By.Id("RequiresUserApproval")).Click();
|
||||
s.Driver.FindElement(By.Id("SaveButton")).Click();
|
||||
Assert.Contains("Policies updated successfully", s.FindAlertMessage().Text);
|
||||
Assert.False(s.Driver.FindElement(By.Id("RequiresUserApproval")).Selected);
|
||||
|
||||
// Check user create view does not have approval checkbox
|
||||
s.GoToServer(ServerNavPages.Users);
|
||||
s.Driver.FindElement(By.Id("CreateUser")).Click();
|
||||
s.Driver.ElementDoesNotExist(By.Id("Approved"));
|
||||
|
||||
s.Logout();
|
||||
|
||||
// Still requires approval for user who registered before
|
||||
s.GoToLogin();
|
||||
s.LogIn(unapproved.RegisterDetails.Email, unapproved.RegisterDetails.Password);
|
||||
Assert.Contains("Your user account requires approval by an admin before you can log in", s.FindAlertMessage(StatusMessageModel.StatusSeverity.Warning).Text);
|
||||
Assert.Contains("/login", s.Driver.Url);
|
||||
|
||||
// New user can register and gets in without approval
|
||||
s.GoToRegister();
|
||||
s.RegisterNewUser();
|
||||
s.Driver.AssertNoError();
|
||||
Assert.DoesNotContain("/login", s.Driver.Url);
|
||||
var autoApproved = s.AsTestAccount();
|
||||
s.CreateNewStore();
|
||||
s.Logout();
|
||||
|
||||
// Login with admin and check list
|
||||
s.GoToLogin();
|
||||
s.LogIn(admin.RegisterDetails.Email, admin.RegisterDetails.Password);
|
||||
s.GoToHome();
|
||||
|
||||
// No notification this time
|
||||
s.Driver.ElementDoesNotExist(By.Id("NotificationsBadge"));
|
||||
|
||||
// Check users list
|
||||
s.GoToServer(ServerNavPages.Users);
|
||||
var rows = s.Driver.FindElements(By.CssSelector("#UsersList tr"));
|
||||
Assert.True(rows.Count >= 3);
|
||||
|
||||
// Check user which didn't require approval
|
||||
s.Driver.FindElement(By.Id("SearchTerm")).Clear();
|
||||
s.Driver.FindElement(By.Id("SearchTerm")).SendKeys(autoApproved.RegisterDetails.Email);
|
||||
s.Driver.FindElement(By.Id("SearchTerm")).SendKeys(Keys.Enter);
|
||||
rows = s.Driver.FindElements(By.CssSelector("#UsersList tr"));
|
||||
Assert.Single(rows);
|
||||
Assert.Contains(autoApproved.RegisterDetails.Email, rows.First().Text);
|
||||
s.Driver.ElementDoesNotExist(By.CssSelector("#UsersList tr:first-child .user-approved"));
|
||||
// Edit view does not contain approve toggle
|
||||
s.Driver.FindElement(By.CssSelector("#UsersList tr:first-child .user-edit")).Click();
|
||||
s.Driver.ElementDoesNotExist(By.Id("Approved"));
|
||||
|
||||
// Check user which still requires approval
|
||||
s.GoToServer(ServerNavPages.Users);
|
||||
s.Driver.FindElement(By.Id("SearchTerm")).Clear();
|
||||
s.Driver.FindElement(By.Id("SearchTerm")).SendKeys(unapproved.RegisterDetails.Email);
|
||||
s.Driver.FindElement(By.Id("SearchTerm")).SendKeys(Keys.Enter);
|
||||
rows = s.Driver.FindElements(By.CssSelector("#UsersList tr"));
|
||||
Assert.Single(rows);
|
||||
Assert.Contains(unapproved.RegisterDetails.Email, rows.First().Text);
|
||||
Assert.Contains("Pending Approval", s.Driver.FindElement(By.CssSelector("#UsersList tr:first-child .user-status")).Text);
|
||||
// Approve user
|
||||
s.Driver.FindElement(By.CssSelector("#UsersList tr:first-child .user-edit")).Click();
|
||||
s.Driver.FindElement(By.Id("Approved")).Click();
|
||||
s.Driver.FindElement(By.Id("SaveUser")).Click();
|
||||
Assert.Contains("User successfully updated", s.FindAlertMessage().Text);
|
||||
// Check list again
|
||||
s.GoToServer(ServerNavPages.Users);
|
||||
Assert.Contains(unapproved.RegisterDetails.Email, s.Driver.FindElement(By.Id("SearchTerm")).GetAttribute("value"));
|
||||
rows = s.Driver.FindElements(By.CssSelector("#UsersList tr"));
|
||||
Assert.Single(rows);
|
||||
Assert.Contains(unapproved.RegisterDetails.Email, rows.First().Text);
|
||||
Assert.Contains("Active", s.Driver.FindElement(By.CssSelector("#UsersList tr:first-child .user-status")).Text);
|
||||
|
||||
// Finally, login user that needed approval
|
||||
s.Logout();
|
||||
s.GoToLogin();
|
||||
s.LogIn(unapproved.RegisterDetails.Email, unapproved.RegisterDetails.Password);
|
||||
s.Driver.AssertNoError();
|
||||
Assert.DoesNotContain("/login", s.Driver.Url);
|
||||
s.CreateNewStore();
|
||||
}
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
public async Task CanUseSSHService()
|
||||
{
|
||||
@ -468,8 +611,16 @@ namespace BTCPayServer.Tests
|
||||
s.RegisterNewUser(true);
|
||||
s.CreateNewStore();
|
||||
|
||||
// Ensure empty server settings
|
||||
s.Driver.Navigate().GoToUrl(s.Link("/server/emails"));
|
||||
s.Driver.FindElement(By.Id("Settings_Login")).Clear();
|
||||
s.Driver.FindElement(By.Id("Settings_Password")).Clear();
|
||||
s.Driver.FindElement(By.Id("Settings_From")).Clear();
|
||||
s.Driver.FindElement(By.Id("Save")).Submit();
|
||||
|
||||
// Store Emails without server fallback
|
||||
s.GoToStore(StoreNavPages.Emails);
|
||||
s.Driver.ElementDoesNotExist(By.Id("UseCustomSMTP"));
|
||||
s.Driver.FindElement(By.Id("ConfigureEmailRules")).Click();
|
||||
Assert.Contains("You need to configure email settings before this feature works", s.Driver.PageSource);
|
||||
|
||||
@ -481,13 +632,16 @@ namespace BTCPayServer.Tests
|
||||
s.FindAlertMessage();
|
||||
}
|
||||
CanSetupEmailCore(s);
|
||||
|
||||
|
||||
// Store Emails with server fallback
|
||||
s.GoToStore(StoreNavPages.Emails);
|
||||
Assert.False(s.Driver.FindElement(By.Id("UseCustomSMTP")).Selected);
|
||||
s.Driver.FindElement(By.Id("ConfigureEmailRules")).Click();
|
||||
Assert.Contains("Emails will be sent with the email settings of the server", s.Driver.PageSource);
|
||||
Assert.DoesNotContain("You need to configure email settings before this feature works", s.Driver.PageSource);
|
||||
|
||||
s.GoToStore(StoreNavPages.Emails);
|
||||
s.Driver.FindElement(By.Id("UseCustomSMTP")).Click();
|
||||
Thread.Sleep(250);
|
||||
CanSetupEmailCore(s);
|
||||
|
||||
// Store Email Rules
|
||||
@ -495,7 +649,6 @@ namespace BTCPayServer.Tests
|
||||
Assert.Contains("There are no rules yet.", s.Driver.PageSource);
|
||||
Assert.DoesNotContain("id=\"SaveEmailRules\"", s.Driver.PageSource);
|
||||
Assert.DoesNotContain("You need to configure email settings before this feature works", s.Driver.PageSource);
|
||||
Assert.DoesNotContain("Emails will be sent with the email settings of the server", s.Driver.PageSource);
|
||||
|
||||
s.Driver.FindElement(By.Id("CreateEmailRule")).Click();
|
||||
var select = new SelectElement(s.Driver.FindElement(By.Id("Rules_0__Trigger")));
|
||||
@ -506,6 +659,9 @@ namespace BTCPayServer.Tests
|
||||
s.Driver.FindElement(By.ClassName("note-editable")).SendKeys("Your invoice is settled");
|
||||
s.Driver.FindElement(By.Id("SaveEmailRules")).Click();
|
||||
Assert.Contains("Store email rules saved", s.FindAlertMessage().Text);
|
||||
|
||||
s.GoToStore(StoreNavPages.Emails);
|
||||
Assert.True(s.Driver.FindElement(By.Id("UseCustomSMTP")).Selected);
|
||||
}
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
@ -1841,6 +1997,7 @@ namespace BTCPayServer.Tests
|
||||
public async Task CanUsePullPaymentsViaUI()
|
||||
{
|
||||
using var s = CreateSeleniumTester();
|
||||
s.Server.DeleteStore = false;
|
||||
s.Server.ActivateLightning(LightningConnectionType.LndREST);
|
||||
await s.StartAsync();
|
||||
await s.Server.EnsureChannelsSetup();
|
||||
@ -1963,7 +2120,6 @@ namespace BTCPayServer.Tests
|
||||
});
|
||||
s.GoToHome();
|
||||
//offline/external payout test
|
||||
|
||||
var newStore = s.CreateNewStore();
|
||||
s.GenerateWallet("BTC", "", true, true);
|
||||
s.GoToStore(s.StoreId, StoreNavPages.PullPayments);
|
||||
@ -2153,6 +2309,22 @@ namespace BTCPayServer.Tests
|
||||
|
||||
// Simulate a boltcard
|
||||
{
|
||||
// LNURL Withdraw support check with BTC denomination
|
||||
s.GoToStore(s.StoreId, StoreNavPages.PullPayments);
|
||||
s.Driver.FindElement(By.Id("NewPullPayment")).Click();
|
||||
s.Driver.FindElement(By.Id("Name")).SendKeys("TopUpTest");
|
||||
s.Driver.SetCheckbox(By.Id("AutoApproveClaims"), true);
|
||||
s.Driver.FindElement(By.Id("Amount")).Clear();
|
||||
s.Driver.FindElement(By.Id("Amount")).SendKeys("100000");
|
||||
s.Driver.FindElement(By.Id("Currency")).Clear();
|
||||
s.Driver.FindElement(By.Id("Currency")).SendKeys("SATS" + Keys.Enter);
|
||||
s.FindAlertMessage();
|
||||
s.Driver.FindElement(By.LinkText("View")).Click();
|
||||
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.Last());
|
||||
|
||||
s.Driver.FindElement(By.CssSelector("#lnurlwithdraw-button")).Click();
|
||||
s.Driver.WaitForElement(By.Id("qr-code-data-input"));
|
||||
lnurl = new Uri(LNURL.LNURL.Parse(s.Driver.FindElement(By.Id("qr-code-data-input")).GetAttribute("value"), out _).ToString().Replace("https", "http"));
|
||||
var db = s.Server.PayTester.GetService<ApplicationDbContextFactory>();
|
||||
var ppid = lnurl.AbsoluteUri.Split("/").Last();
|
||||
var issuerKey = new IssuerKey(SettingsRepositoryExtensions.FixedKey());
|
||||
@ -2167,6 +2339,25 @@ namespace BTCPayServer.Tests
|
||||
// p and c should work so long as no bolt11 has been submitted
|
||||
info = (LNURLWithdrawRequest)await LNURL.LNURL.FetchInformation(boltcardUrl, s.Server.PayTester.HttpClient);
|
||||
info = (LNURLWithdrawRequest)await LNURL.LNURL.FetchInformation(boltcardUrl, s.Server.PayTester.HttpClient);
|
||||
Assert.NotNull(info.PayLink);
|
||||
Assert.StartsWith("lnurlp://", info.PayLink.AbsoluteUri);
|
||||
// Ignore certs issue
|
||||
info.PayLink = new Uri(info.PayLink.AbsoluteUri.Replace("lnurlp://", "http://"), UriKind.Absolute);
|
||||
var payReq = (LNURLPayRequest)await LNURL.LNURL.FetchInformation(info.PayLink, s.Server.PayTester.HttpClient);
|
||||
var callback = await payReq.SendRequest(LightMoney.Satoshis(100), Network.RegTest, s.Server.PayTester.HttpClient);
|
||||
Assert.NotNull(callback.Pr);
|
||||
var pr = BOLT11PaymentRequest.Parse(callback.Pr, Network.RegTest);
|
||||
Assert.Equal(LightMoney.Satoshis(100), pr.MinimumAmount);
|
||||
var res = await s.Server.CustomerLightningD.Pay(callback.Pr);
|
||||
Assert.Equal(PayResult.Ok, res.Result);
|
||||
var ppService = s.Server.PayTester.GetService<PullPaymentHostedService>();
|
||||
var serializer = s.Server.PayTester.GetService<BTCPayNetworkJsonSerializerSettings>();
|
||||
await TestUtils.EventuallyAsync(async () =>
|
||||
{
|
||||
var pp = await ppService.GetPullPayment(ppid, true);
|
||||
Assert.Contains(pp.Payouts.Select(p => p.GetBlob(serializer)), p => p.CryptoAmount == -LightMoney.Satoshis(100).ToUnit(LightMoneyUnit.BTC));
|
||||
});
|
||||
|
||||
var fakeBoltcardUrl = new Uri(Regex.Replace(boltcardUrl.AbsoluteUri, "p=([A-F0-9]{32})", $"p={RandomBytes(16)}"));
|
||||
await Assert.ThrowsAsync<LNUrlException>(() => LNURL.LNURL.FetchInformation(fakeBoltcardUrl, s.Server.PayTester.HttpClient));
|
||||
fakeBoltcardUrl = new Uri(Regex.Replace(boltcardUrl.AbsoluteUri, "c=([A-F0-9]{16})", $"c={RandomBytes(8)}"));
|
||||
@ -2332,10 +2523,27 @@ namespace BTCPayServer.Tests
|
||||
s.Server.ActivateLightning();
|
||||
await s.StartAsync();
|
||||
await s.Server.EnsureChannelsSetup();
|
||||
|
||||
// Create users
|
||||
var user = s.RegisterNewUser();
|
||||
var userAccount = s.AsTestAccount();
|
||||
s.GoToHome();
|
||||
s.Logout();
|
||||
s.GoToRegister();
|
||||
s.RegisterNewUser(true);
|
||||
|
||||
// Setup store and associate user
|
||||
s.CreateNewStore();
|
||||
s.GoToStore();
|
||||
s.AddLightningNode(LightningConnectionType.CLightning, false);
|
||||
s.GoToStore(StoreNavPages.Users);
|
||||
s.Driver.FindElement(By.Id("Email")).Clear();
|
||||
s.Driver.FindElement(By.Id("Email")).SendKeys(user);
|
||||
new SelectElement(s.Driver.FindElement(By.Id("Role"))).SelectByValue("Guest");
|
||||
s.Driver.FindElement(By.Id("AddUser")).Click();
|
||||
Assert.Contains("User added successfully", s.FindAlertMessage().Text);
|
||||
|
||||
// Setup POS
|
||||
s.Driver.FindElement(By.Id("StoreNav-CreatePointOfSale")).Click();
|
||||
s.Driver.FindElement(By.Id("AppName")).SendKeys(Guid.NewGuid().ToString());
|
||||
s.Driver.FindElement(By.Id("Create")).Click();
|
||||
@ -2360,6 +2568,8 @@ namespace BTCPayServer.Tests
|
||||
s.Driver.WaitForElement(By.ClassName("keypad"));
|
||||
|
||||
// basic checks
|
||||
var keypadUrl = s.Driver.Url;
|
||||
s.Driver.FindElement(By.Id("RecentTransactionsToggle"));
|
||||
Assert.Contains("EUR", s.Driver.FindElement(By.Id("Currency")).Text);
|
||||
Assert.Contains("0,00", s.Driver.FindElement(By.Id("Amount")).Text);
|
||||
Assert.Equal("", s.Driver.FindElement(By.Id("Calculation")).Text);
|
||||
@ -2405,6 +2615,19 @@ namespace BTCPayServer.Tests
|
||||
s.Driver.FindElement(By.Id("DetailsToggle")).Click();
|
||||
s.Driver.WaitForElement(By.Id("PaymentDetails-TotalFiat"));
|
||||
Assert.Contains("1 222,21 €", s.Driver.FindElement(By.Id("PaymentDetails-TotalFiat")).Text);
|
||||
|
||||
// Guest user can access recent transactions
|
||||
s.GoToHome();
|
||||
s.Logout();
|
||||
s.LogIn(user, userAccount.RegisterDetails.Password);
|
||||
s.GoToUrl(keypadUrl);
|
||||
s.Driver.FindElement(By.Id("RecentTransactionsToggle"));
|
||||
s.GoToHome();
|
||||
s.Logout();
|
||||
|
||||
// Unauthenticated user can't access recent transactions
|
||||
s.GoToUrl(keypadUrl);
|
||||
s.Driver.ElementDoesNotExist(By.Id("RecentTransactionsToggle"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@ -3127,15 +3350,13 @@ retry:
|
||||
|
||||
private static void CanSetupEmailCore(SeleniumTester s)
|
||||
{
|
||||
s.Driver.ScrollTo(By.Id("QuickFillDropdownToggle"));
|
||||
s.Driver.FindElement(By.Id("QuickFillDropdownToggle")).Click();
|
||||
s.Driver.FindElement(By.CssSelector("#quick-fill .dropdown-menu .dropdown-item:first-child")).Click();
|
||||
s.Driver.FindElement(By.Id("Settings_Login")).Clear();
|
||||
s.Driver.FindElement(By.Id("Settings_Login")).SendKeys("test@gmail.com");
|
||||
s.Driver.FindElement(By.CssSelector("button[value=\"Save\"]")).Submit();
|
||||
s.FindAlertMessage();
|
||||
s.Driver.FindElement(By.Id("Settings_Password")).Clear();
|
||||
s.Driver.FindElement(By.Id("Settings_Password")).SendKeys("mypassword");
|
||||
|
||||
s.Driver.FindElement(By.Id("Settings_From")).Clear();
|
||||
s.Driver.FindElement(By.Id("Settings_From")).SendKeys("Firstname Lastname <email@example.com>");
|
||||
s.Driver.FindElement(By.Id("Save")).SendKeys(Keys.Enter);
|
||||
|
@ -191,6 +191,12 @@ namespace BTCPayServer.Tests
|
||||
// Ripio keeps changing their pair, so anything is fine...
|
||||
Assert.NotEmpty(exchangeRates.ByExchange[name]);
|
||||
}
|
||||
else if (name == "bitnob")
|
||||
{
|
||||
Assert.Contains(exchangeRates.ByExchange[name],
|
||||
e => e.CurrencyPair == new CurrencyPair("BTC", "NGN") &&
|
||||
e.BidAsk.Bid > 1.0m); // 1 BTC will always be more than 1 NGN
|
||||
}
|
||||
else if (name == "cryptomarket")
|
||||
{
|
||||
Assert.Contains(exchangeRates.ByExchange[name],
|
||||
|
@ -2325,17 +2325,21 @@ namespace BTCPayServer.Tests
|
||||
using var tester = CreateServerTester(newDb: true);
|
||||
await tester.StartAsync();
|
||||
var f = tester.PayTester.GetService<ApplicationDbContextFactory>();
|
||||
const string id = "BTCPayServer.Services.PoliciesSettings";
|
||||
using (var ctx = f.CreateContext())
|
||||
{
|
||||
var setting = new SettingData() { Id = "BTCPayServer.Services.PoliciesSettings" };
|
||||
setting.Value = JObject.Parse("{\"RootAppId\": null, \"RootAppType\": 1, \"Experimental\": false, \"PluginSource\": null, \"LockSubscription\": false, \"DisableSSHService\": false, \"PluginPreReleases\": false, \"BlockExplorerLinks\": [],\"DomainToAppMapping\": [{\"AppId\": \"87kj5yKay8mB4UUZcJhZH5TqDKMD3CznjwLjiu1oYZXe\", \"Domain\": \"donate.nicolas-dorier.com\", \"AppType\": 0}], \"CheckForNewVersions\": false, \"AllowHotWalletForAll\": false, \"RequiresConfirmedEmail\": false, \"DiscourageSearchEngines\": false, \"DisableInstantNotifications\": false, \"DisableNonAdminCreateUserApi\": false, \"AllowHotWalletRPCImportForAll\": false, \"AllowLightningInternalNodeForAll\": false, \"DisableStoresToUseServerEmailSettings\": false}").ToString();
|
||||
// remove existing policies setting
|
||||
var setting = await ctx.Settings.FirstOrDefaultAsync(c => c.Id == id);
|
||||
if (setting != null) ctx.Settings.Remove(setting);
|
||||
// create legacy policies setting that needs migration
|
||||
setting = new SettingData { Id = id, Value = JObject.Parse("{\"RootAppId\": null, \"RootAppType\": 1, \"Experimental\": false, \"PluginSource\": null, \"LockSubscription\": false, \"DisableSSHService\": false, \"PluginPreReleases\": false, \"BlockExplorerLinks\": [],\"DomainToAppMapping\": [{\"AppId\": \"87kj5yKay8mB4UUZcJhZH5TqDKMD3CznjwLjiu1oYZXe\", \"Domain\": \"donate.nicolas-dorier.com\", \"AppType\": 0}], \"CheckForNewVersions\": false, \"AllowHotWalletForAll\": false, \"RequiresConfirmedEmail\": false, \"DiscourageSearchEngines\": false, \"DisableInstantNotifications\": false, \"DisableNonAdminCreateUserApi\": false, \"AllowHotWalletRPCImportForAll\": false, \"AllowLightningInternalNodeForAll\": false, \"DisableStoresToUseServerEmailSettings\": false}").ToString() };
|
||||
ctx.Settings.Add(setting);
|
||||
await ctx.SaveChangesAsync();
|
||||
}
|
||||
await RestartMigration(tester);
|
||||
using (var ctx = f.CreateContext())
|
||||
{
|
||||
var setting = await ctx.Settings.FirstOrDefaultAsync(c => c.Id == "BTCPayServer.Services.PoliciesSettings");
|
||||
var setting = await ctx.Settings.FirstOrDefaultAsync(c => c.Id == id);
|
||||
var o = JObject.Parse(setting.Value);
|
||||
Assert.Equal("Crowdfund", o["RootAppType"].Value<string>());
|
||||
o = (JObject)((JArray)o["DomainToAppMapping"])[0];
|
||||
|
@ -22,6 +22,18 @@
|
||||
<EmbeddedResource Remove="wwwroot\vendor\jquery-nice-select\**" />
|
||||
<None Remove="Build\**" />
|
||||
<None Remove="wwwroot\vendor\jquery-nice-select\**" />
|
||||
<Content Update="Plugins\BoltcardBalance\Views\ScanCard.cshtml">
|
||||
<Pack>$(IncludeRazorContentInPack)</Pack>
|
||||
</Content>
|
||||
<Content Update="Plugins\BoltcardFactory\Views\UpdateBoltcardFactory.cshtml">
|
||||
<Pack>false</Pack>
|
||||
</Content>
|
||||
<Content Update="Plugins\BoltcardFactory\Views\ViewBoltcardFactory.cshtml">
|
||||
<Pack>$(IncludeRazorContentInPack)</Pack>
|
||||
</Content>
|
||||
<Content Update="Plugins\BoltcardTopUp\Views\ScanCard.cshtml">
|
||||
<Pack>$(IncludeRazorContentInPack)</Pack>
|
||||
</Content>
|
||||
<Content Update="Views\UIStorePullPayments\NewPullPayment.cshtml">
|
||||
<Pack>false</Pack>
|
||||
</Content>
|
||||
@ -46,11 +58,11 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BTCPayServer.NTag424" Version="1.0.20" />
|
||||
<PackageReference Include="BTCPayServer.NTag424" Version="1.0.22" />
|
||||
<PackageReference Include="YamlDotNet" Version="8.0.0" />
|
||||
<PackageReference Include="BIP78.Sender" Version="0.2.2" />
|
||||
<PackageReference Include="BTCPayServer.Hwi" Version="2.0.2" />
|
||||
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.5.3" />
|
||||
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.5.4" />
|
||||
<PackageReference Include="CsvHelper" Version="15.0.5" />
|
||||
<PackageReference Include="Dapper" Version="2.1.28" />
|
||||
<PackageReference Include="Fido2" Version="2.0.2" />
|
||||
|
@ -64,13 +64,14 @@
|
||||
|
||||
const id = `StoreWalletBalance-${storeId}`;
|
||||
const baseUrl = @Safe.Json(Url.Action("WalletHistogram", "UIWallets", new { walletId = Model.WalletId, type = WalletHistogramType.Week }));
|
||||
const valueTransform = value => rate
|
||||
? DashboardUtils.displayDefaultCurrency(value, rate, defaultCurrency, divisibility).toString()
|
||||
: value
|
||||
const chartOpts = {
|
||||
fullWidth: true,
|
||||
showArea: true,
|
||||
axisY: {
|
||||
labelInterpolationFnc: value => rate
|
||||
? DashboardUtils.displayDefaultCurrency(value, rate, defaultCurrency, divisibility).toString()
|
||||
: value
|
||||
labelInterpolationFnc: valueTransform
|
||||
}
|
||||
};
|
||||
|
||||
@ -80,16 +81,22 @@
|
||||
document.querySelectorAll(`#${id} .currency`).forEach(c => c.innerText = currency)
|
||||
document.querySelectorAll(`#${id} [data-balance]`).forEach(c => {
|
||||
const value = Number.parseFloat(c.dataset.balance);
|
||||
c.innerText = rate
|
||||
? DashboardUtils.displayDefaultCurrency(value, rate, currency, divisibility)
|
||||
: value
|
||||
c.innerText = valueTransform(value)
|
||||
});
|
||||
if (!series) return;
|
||||
|
||||
const min = Math.min(...series);
|
||||
const max = Math.max(...series);
|
||||
const low = Math.max(min - ((max - min) / 5), 0);
|
||||
const renderOpts = Object.assign({}, chartOpts, { low });
|
||||
const tooltip = Chartist.plugins.tooltip2({
|
||||
template: '{{value}}',
|
||||
offset: {
|
||||
x: 0,
|
||||
y: -16
|
||||
},
|
||||
valueTransformFunction: valueTransform
|
||||
})
|
||||
const renderOpts = Object.assign({}, chartOpts, { low, plugins: [tooltip] });
|
||||
const chart = new Chartist.Line(`#${id} .ct-chart`, {
|
||||
labels,
|
||||
series: [series]
|
||||
|
@ -1,6 +1,8 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO.IsolatedStorage;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
@ -12,6 +14,7 @@ using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Logging;
|
||||
using BTCPayServer.NTag424;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Security;
|
||||
@ -22,8 +25,11 @@ using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NBitcoin.DataEncoders;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Org.BouncyCastle.Bcpg.OpenPgp;
|
||||
using MarkPayoutRequest = BTCPayServer.HostedServices.MarkPayoutRequest;
|
||||
|
||||
namespace BTCPayServer.Controllers.Greenfield
|
||||
@ -43,6 +49,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
private readonly IAuthorizationService _authorizationService;
|
||||
private readonly SettingsRepository _settingsRepository;
|
||||
private readonly BTCPayServerEnvironment _env;
|
||||
private readonly Logs _logs;
|
||||
|
||||
public GreenfieldPullPaymentController(PullPaymentHostedService pullPaymentService,
|
||||
LinkGenerator linkGenerator,
|
||||
@ -53,7 +60,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
BTCPayNetworkProvider btcPayNetworkProvider,
|
||||
IAuthorizationService authorizationService,
|
||||
SettingsRepository settingsRepository,
|
||||
BTCPayServerEnvironment env)
|
||||
BTCPayServerEnvironment env, Logs logs)
|
||||
{
|
||||
_pullPaymentService = pullPaymentService;
|
||||
_linkGenerator = linkGenerator;
|
||||
@ -65,6 +72,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
_authorizationService = authorizationService;
|
||||
_settingsRepository = settingsRepository;
|
||||
_env = env;
|
||||
_logs = logs;
|
||||
}
|
||||
|
||||
[HttpGet("~/api/v1/stores/{storeId}/pull-payments")]
|
||||
@ -153,20 +161,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
}
|
||||
if (!ModelState.IsValid)
|
||||
return this.CreateValidationError(ModelState);
|
||||
var ppId = await _pullPaymentService.CreatePullPayment(new CreatePullPayment()
|
||||
{
|
||||
StartsAt = request.StartsAt,
|
||||
ExpiresAt = request.ExpiresAt,
|
||||
Period = request.Period,
|
||||
BOLT11Expiration = request.BOLT11Expiration,
|
||||
Name = request.Name,
|
||||
Description = request.Description,
|
||||
Amount = request.Amount,
|
||||
Currency = request.Currency,
|
||||
StoreId = storeId,
|
||||
PaymentMethodIds = paymentMethods,
|
||||
AutoApproveClaims = request.AutoApproveClaims
|
||||
});
|
||||
var ppId = await _pullPaymentService.CreatePullPayment(storeId, request);
|
||||
var pp = await _pullPaymentService.GetPullPayment(ppId, false);
|
||||
return this.Ok(CreatePullPaymentData(pp));
|
||||
}
|
||||
@ -200,13 +195,39 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
[HttpPost]
|
||||
[Route("~/api/v1/pull-payments/{pullPaymentId}/boltcards")]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> RegisterBoltcard(string pullPaymentId, RegisterBoltcardRequest request)
|
||||
public async Task<IActionResult> RegisterBoltcard(string pullPaymentId, RegisterBoltcardRequest request, string? onExisting = null)
|
||||
{
|
||||
if (pullPaymentId is null)
|
||||
return PullPaymentNotFound();
|
||||
this._logs.PayServer.LogInformation($"RegisterBoltcard: onExisting queryParam: {onExisting}");
|
||||
this._logs.PayServer.LogInformation($"{JsonConvert.SerializeObject(request)}");
|
||||
var pp = await _pullPaymentService.GetPullPayment(pullPaymentId, false);
|
||||
if (pp is null)
|
||||
return PullPaymentNotFound();
|
||||
var issuerKey = await _settingsRepository.GetIssuerKey(_env);
|
||||
|
||||
// LNURLW is used by deeplinks
|
||||
if (request?.LNURLW is not null)
|
||||
{
|
||||
if (request.UID is not null)
|
||||
{
|
||||
ModelState.AddModelError(nameof(request.LNURLW), "You should pass either LNURLW or UID but not both");
|
||||
return this.CreateValidationError(ModelState);
|
||||
}
|
||||
var p = ExtractP(request.LNURLW);
|
||||
if (p is null)
|
||||
{
|
||||
ModelState.AddModelError(nameof(request.LNURLW), "The LNURLW should contains a 'p=' parameter");
|
||||
return this.CreateValidationError(ModelState);
|
||||
}
|
||||
if (issuerKey.TryDecrypt(p) is not BoltcardPICCData picc)
|
||||
{
|
||||
ModelState.AddModelError(nameof(request.LNURLW), "The LNURLW 'p=' parameter cannot be decrypted");
|
||||
return this.CreateValidationError(ModelState);
|
||||
}
|
||||
request.UID = picc.Uid;
|
||||
}
|
||||
|
||||
if (request?.UID is null || request.UID.Length != 7)
|
||||
{
|
||||
ModelState.AddModelError(nameof(request.UID), "The UID is required and should be 7 bytes");
|
||||
@ -217,15 +238,28 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
return this.CreateAPIError(400, "lnurl-not-supported", "This pull payment currency should be BTC or SATS and accept lightning");
|
||||
}
|
||||
|
||||
var issuerKey = await _settingsRepository.GetIssuerKey(_env);
|
||||
// Passing onExisting as a query parameter is used by deeplink
|
||||
request.OnExisting = onExisting switch
|
||||
{
|
||||
nameof(OnExistingBehavior.UpdateVersion) => OnExistingBehavior.UpdateVersion,
|
||||
nameof(OnExistingBehavior.KeepVersion) => OnExistingBehavior.KeepVersion,
|
||||
_ => request.OnExisting
|
||||
};
|
||||
|
||||
this._logs.PayServer.LogInformation($"After");
|
||||
this._logs.PayServer.LogInformation($"{JsonConvert.SerializeObject(request)}");
|
||||
|
||||
var version = await _dbContextFactory.LinkBoltcardToPullPayment(pullPaymentId, issuerKey, request.UID, request.OnExisting);
|
||||
this._logs.PayServer.LogInformation($"Version: " + version);
|
||||
this._logs.PayServer.LogInformation($"ID: " + Encoders.Hex.EncodeData(issuerKey.GetId(request.UID)));
|
||||
|
||||
var keys = issuerKey.CreatePullPaymentCardKey(request.UID, version, pullPaymentId).DeriveBoltcardKeys(issuerKey);
|
||||
|
||||
var boltcardUrl = Url.Action(nameof(UIBoltcardController.GetWithdrawRequest), "UIBoltcard");
|
||||
boltcardUrl = Request.GetAbsoluteUri(boltcardUrl);
|
||||
boltcardUrl = Regex.Replace(boltcardUrl, "^https?://", "lnurlw://");
|
||||
|
||||
return Ok(new RegisterBoltcardResponse()
|
||||
var resp = new RegisterBoltcardResponse()
|
||||
{
|
||||
LNURLW = boltcardUrl,
|
||||
Version = version,
|
||||
@ -234,7 +268,25 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
K2 = Encoders.Hex.EncodeData(keys.AuthenticationKey.ToBytes()).ToUpperInvariant(),
|
||||
K3 = Encoders.Hex.EncodeData(keys.K3.ToBytes()).ToUpperInvariant(),
|
||||
K4 = Encoders.Hex.EncodeData(keys.K4.ToBytes()).ToUpperInvariant(),
|
||||
});
|
||||
};
|
||||
this._logs.PayServer.LogInformation($"Response");
|
||||
this._logs.PayServer.LogInformation($"{JsonConvert.SerializeObject(resp)}");
|
||||
|
||||
return Ok(resp);
|
||||
}
|
||||
|
||||
private string? ExtractP(string? url)
|
||||
{
|
||||
if (url is null || !Uri.TryCreate(url, UriKind.Absolute, out var uri))
|
||||
return null;
|
||||
int num = uri.AbsoluteUri.IndexOf('?');
|
||||
if (num == -1)
|
||||
return null;
|
||||
string input = uri.AbsoluteUri.Substring(num);
|
||||
Match match = Regex.Match(input, "p=([a-f0-9A-F]{32})");
|
||||
if (!match.Success)
|
||||
return null;
|
||||
return match.Groups[1].Value;
|
||||
}
|
||||
|
||||
[HttpGet("~/api/v1/pull-payments/{pullPaymentId}")]
|
||||
|
@ -518,10 +518,6 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
Outputs = outputs,
|
||||
AlwaysIncludeNonWitnessUTXO = true,
|
||||
InputSelection = request.SelectedInputs?.Any() is true,
|
||||
AllowFeeBump =
|
||||
!request.RBF.HasValue ? WalletSendModel.ThreeStateBool.Maybe :
|
||||
request.RBF.Value ? WalletSendModel.ThreeStateBool.Yes :
|
||||
WalletSendModel.ThreeStateBool.No,
|
||||
FeeSatoshiPerByte = request.FeeRate?.SatoshiPerByte,
|
||||
NoChange = request.NoChange
|
||||
},
|
||||
|
@ -92,6 +92,26 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
$"{(request.Locked ? "Locking" : "Unlocking")} user failed");
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanModifyServerSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpPost("~/api/v1/users/{idOrEmail}/approve")]
|
||||
public async Task<IActionResult> ApproveUser(string idOrEmail, ApproveUserRequest request)
|
||||
{
|
||||
var user = await _userManager.FindByIdOrEmail(idOrEmail);
|
||||
if (user is null)
|
||||
{
|
||||
return this.UserNotFound();
|
||||
}
|
||||
|
||||
var success = false;
|
||||
if (user.RequiresApproval)
|
||||
{
|
||||
success = await _userService.SetUserApproval(user.Id, request.Approved, Request.GetAbsoluteRootUri());
|
||||
}
|
||||
|
||||
return success ? Ok() : this.CreateAPIError("invalid-state",
|
||||
$"{(request.Approved ? "Approving" : "Unapproving")} user failed");
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanViewUsers, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpGet("~/api/v1/users/")]
|
||||
public async Task<ActionResult<ApplicationUserData[]>> GetUsers()
|
||||
@ -163,7 +183,9 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
UserName = request.Email,
|
||||
Email = request.Email,
|
||||
RequiresEmailConfirmation = policies.RequiresConfirmedEmail,
|
||||
RequiresApproval = policies.RequiresUserApproval,
|
||||
Created = DateTimeOffset.UtcNow,
|
||||
Approved = !anyAdmin && isAdmin // auto-approve first admin
|
||||
};
|
||||
var passwordValidation = await this._passwordValidator.ValidateAsync(_userManager, user, request.Password);
|
||||
if (!passwordValidation.Succeeded)
|
||||
|
@ -1084,6 +1084,13 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
new LockUserRequest { Locked = disabled }));
|
||||
}
|
||||
|
||||
public override async Task<bool> ApproveUser(string idOrEmail, bool approved, CancellationToken token = default)
|
||||
{
|
||||
return GetFromActionResult<bool>(
|
||||
await GetController<GreenfieldUsersController>().ApproveUser(idOrEmail,
|
||||
new ApproveUserRequest { Approved = approved }));
|
||||
}
|
||||
|
||||
public override async Task<OnChainWalletTransactionData> PatchOnChainWalletTransaction(string storeId,
|
||||
string cryptoCode, string transactionId,
|
||||
PatchOnChainTransactionRequest request, bool force = false, CancellationToken token = default)
|
||||
|
@ -2,7 +2,6 @@ using System;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Security.Claims;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
@ -109,6 +108,7 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
if (User.Identity.IsAuthenticated && string.IsNullOrEmpty(returnUrl))
|
||||
return RedirectToLocal();
|
||||
|
||||
// Clear the existing external cookie to ensure a clean login process
|
||||
await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme);
|
||||
|
||||
@ -118,15 +118,13 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
ViewData["ReturnUrl"] = returnUrl;
|
||||
return View(nameof(Login), new LoginViewModel() { Email = email });
|
||||
return View(nameof(Login), new LoginViewModel { Email = email });
|
||||
}
|
||||
|
||||
|
||||
[HttpPost("/login/code")]
|
||||
[AllowAnonymous]
|
||||
[ValidateAntiForgeryToken]
|
||||
[RateLimitsFilter(ZoneLimits.Login, Scope = RateLimitsScope.RemoteAddress)]
|
||||
|
||||
public async Task<IActionResult> LoginWithCode(string loginCode, string returnUrl = null)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(loginCode))
|
||||
@ -134,17 +132,26 @@ namespace BTCPayServer.Controllers
|
||||
var userId = _userLoginCodeService.Verify(loginCode);
|
||||
if (userId is null)
|
||||
{
|
||||
ModelState.AddModelError(string.Empty,
|
||||
"Login code was invalid");
|
||||
return await Login(returnUrl, null);
|
||||
TempData[WellKnownTempData.ErrorMessage] = "Login code was invalid";
|
||||
return await Login(returnUrl);
|
||||
}
|
||||
var user = await _userManager.FindByIdAsync(userId);
|
||||
|
||||
_logger.LogInformation("User with ID {UserId} logged in with a login code.", user.Id);
|
||||
var user = await _userManager.FindByIdAsync(userId);
|
||||
if (!UserService.TryCanLogin(user, out var message))
|
||||
{
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
{
|
||||
Severity = user == null ? StatusMessageModel.StatusSeverity.Error : StatusMessageModel.StatusSeverity.Warning,
|
||||
Message = message
|
||||
});
|
||||
return await Login(returnUrl);
|
||||
}
|
||||
|
||||
_logger.LogInformation("User with ID {UserId} logged in with a login code", user!.Id);
|
||||
await _signInManager.SignInAsync(user, false, "LoginCode");
|
||||
return RedirectToLocal(returnUrl);
|
||||
}
|
||||
return await Login(returnUrl, null);
|
||||
return await Login(returnUrl);
|
||||
}
|
||||
|
||||
[HttpPost("/login")]
|
||||
@ -161,24 +168,20 @@ namespace BTCPayServer.Controllers
|
||||
ViewData["ReturnUrl"] = returnUrl;
|
||||
if (ModelState.IsValid)
|
||||
{
|
||||
// Require the user to have a confirmed email before they can log on.
|
||||
// Require the user to pass basic checks (approval, confirmed email, not disabled) before they can log on
|
||||
var user = await _userManager.FindByEmailAsync(model.Email);
|
||||
if (user != null)
|
||||
const string errorMessage = "Invalid login attempt.";
|
||||
if (!UserService.TryCanLogin(user, out var message))
|
||||
{
|
||||
if (user.RequiresEmailConfirmation && !await _userManager.IsEmailConfirmedAsync(user))
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
{
|
||||
ModelState.AddModelError(string.Empty,
|
||||
"You must have a confirmed email to log in.");
|
||||
return View(model);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ModelState.AddModelError(string.Empty, "Invalid login attempt.");
|
||||
Severity = user == null ? StatusMessageModel.StatusSeverity.Error : StatusMessageModel.StatusSeverity.Warning,
|
||||
Message = message
|
||||
});
|
||||
return View(model);
|
||||
}
|
||||
|
||||
var fido2Devices = await _fido2Service.HasCredentials(user.Id);
|
||||
var fido2Devices = await _fido2Service.HasCredentials(user!.Id);
|
||||
var lnurlAuthCredentials = await _lnurlAuthService.HasCredentials(user.Id);
|
||||
if (!await _userManager.IsLockedOutAsync(user) && (fido2Devices || lnurlAuthCredentials))
|
||||
{
|
||||
@ -196,33 +199,30 @@ namespace BTCPayServer.Controllers
|
||||
};
|
||||
}
|
||||
|
||||
return View("SecondaryLogin", new SecondaryLoginViewModel()
|
||||
return View("SecondaryLogin", new SecondaryLoginViewModel
|
||||
{
|
||||
LoginWith2FaViewModel = twoFModel,
|
||||
LoginWithFido2ViewModel = fido2Devices ? await BuildFido2ViewModel(model.RememberMe, user) : null,
|
||||
LoginWithLNURLAuthViewModel = lnurlAuthCredentials ? await BuildLNURLAuthViewModel(model.RememberMe, user) : null,
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
await _userManager.AccessFailedAsync(user);
|
||||
ModelState.AddModelError(string.Empty, "Invalid login attempt.");
|
||||
return View(model);
|
||||
|
||||
}
|
||||
await _userManager.AccessFailedAsync(user);
|
||||
ModelState.AddModelError(string.Empty, errorMessage!);
|
||||
return View(model);
|
||||
}
|
||||
|
||||
var result = await _signInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, lockoutOnFailure: true);
|
||||
if (result.Succeeded)
|
||||
{
|
||||
_logger.LogInformation($"User '{user.Id}' logged in.");
|
||||
_logger.LogInformation("User {UserId} logged in", user.Id);
|
||||
return RedirectToLocal(returnUrl);
|
||||
}
|
||||
if (result.RequiresTwoFactor)
|
||||
{
|
||||
return View("SecondaryLogin", new SecondaryLoginViewModel()
|
||||
return View("SecondaryLogin", new SecondaryLoginViewModel
|
||||
{
|
||||
LoginWith2FaViewModel = new LoginWith2faViewModel()
|
||||
LoginWith2FaViewModel = new LoginWith2faViewModel
|
||||
{
|
||||
RememberMe = model.RememberMe
|
||||
}
|
||||
@ -230,14 +230,12 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
if (result.IsLockedOut)
|
||||
{
|
||||
_logger.LogWarning($"User '{user.Id}' account locked out.");
|
||||
_logger.LogWarning("User {UserId} account locked out", user.Id);
|
||||
return RedirectToAction(nameof(Lockout), new { user.LockoutEnd });
|
||||
}
|
||||
else
|
||||
{
|
||||
ModelState.AddModelError(string.Empty, "Invalid login attempt.");
|
||||
return View(model);
|
||||
}
|
||||
|
||||
ModelState.AddModelError(string.Empty, errorMessage);
|
||||
return View(model);
|
||||
}
|
||||
|
||||
// If we got this far, something failed, redisplay form
|
||||
@ -253,7 +251,7 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return new LoginWithFido2ViewModel()
|
||||
return new LoginWithFido2ViewModel
|
||||
{
|
||||
Data = r,
|
||||
UserId = user.Id,
|
||||
@ -263,7 +261,6 @@ namespace BTCPayServer.Controllers
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
private async Task<LoginWithLNURLAuthViewModel> BuildLNURLAuthViewModel(bool rememberMe, ApplicationUser user)
|
||||
{
|
||||
if (_btcPayServerEnvironment.IsSecure(HttpContext))
|
||||
@ -273,15 +270,14 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return new LoginWithLNURLAuthViewModel()
|
||||
return new LoginWithLNURLAuthViewModel
|
||||
{
|
||||
|
||||
RememberMe = rememberMe,
|
||||
UserId = user.Id,
|
||||
LNURLEndpoint = new Uri(_linkGenerator.GetUriByAction(
|
||||
action: nameof(UILNURLAuthController.LoginResponse),
|
||||
controller: "UILNURLAuth",
|
||||
values: new { userId = user.Id, action = "login", tag = "login", k1 = Encoders.Hex.EncodeData(r) }, Request.Scheme, Request.Host, Request.PathBase))
|
||||
action: nameof(UILNURLAuthController.LoginResponse),
|
||||
controller: "UILNURLAuth",
|
||||
values: new { userId = user.Id, action = "login", tag = "login", k1 = Encoders.Hex.EncodeData(r) }, Request.Scheme, Request.Host, Request.PathBase) ?? string.Empty)
|
||||
};
|
||||
}
|
||||
return null;
|
||||
@ -298,14 +294,18 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
ViewData["ReturnUrl"] = returnUrl;
|
||||
var errorMessage = "Invalid login attempt.";
|
||||
var user = await _userManager.FindByIdAsync(viewModel.UserId);
|
||||
|
||||
if (user == null)
|
||||
if (!UserService.TryCanLogin(user, out var message))
|
||||
{
|
||||
return NotFound();
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
{
|
||||
Severity = user == null ? StatusMessageModel.StatusSeverity.Error : StatusMessageModel.StatusSeverity.Warning,
|
||||
Message = message
|
||||
});
|
||||
return RedirectToAction("Login");
|
||||
}
|
||||
|
||||
var errorMessage = string.Empty;
|
||||
try
|
||||
{
|
||||
var k1 = Encoders.Hex.DecodeData(viewModel.LNURLEndpoint.ParseQueryString().Get("k1"));
|
||||
@ -313,34 +313,33 @@ namespace BTCPayServer.Controllers
|
||||
storedk1.SequenceEqual(k1))
|
||||
{
|
||||
_lnurlAuthService.FinalLoginStore.TryRemove(viewModel.UserId, out _);
|
||||
await _signInManager.SignInAsync(user, viewModel.RememberMe, "FIDO2");
|
||||
_logger.LogInformation("User logged in.");
|
||||
await _signInManager.SignInAsync(user!, viewModel.RememberMe, "FIDO2");
|
||||
_logger.LogInformation("User logged in");
|
||||
return RedirectToLocal(returnUrl);
|
||||
}
|
||||
|
||||
errorMessage = "Invalid login attempt.";
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
errorMessage = e.Message;
|
||||
}
|
||||
|
||||
ModelState.AddModelError(string.Empty, errorMessage);
|
||||
return View("SecondaryLogin", new SecondaryLoginViewModel()
|
||||
if (!string.IsNullOrEmpty(errorMessage))
|
||||
{
|
||||
|
||||
LoginWithFido2ViewModel = (await _fido2Service.HasCredentials(user.Id)) ? await BuildFido2ViewModel(viewModel.RememberMe, user) : null,
|
||||
ModelState.AddModelError(string.Empty, errorMessage);
|
||||
}
|
||||
return View("SecondaryLogin", new SecondaryLoginViewModel
|
||||
{
|
||||
LoginWithFido2ViewModel = await _fido2Service.HasCredentials(user!.Id) ? await BuildFido2ViewModel(viewModel.RememberMe, user) : null,
|
||||
LoginWithLNURLAuthViewModel = viewModel,
|
||||
LoginWith2FaViewModel = !user.TwoFactorEnabled
|
||||
? null
|
||||
: new LoginWith2faViewModel()
|
||||
: new LoginWith2faViewModel
|
||||
{
|
||||
RememberMe = viewModel.RememberMe
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
[HttpPost("/login/fido2")]
|
||||
[AllowAnonymous]
|
||||
[ValidateAntiForgeryToken]
|
||||
@ -352,44 +351,50 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
ViewData["ReturnUrl"] = returnUrl;
|
||||
var errorMessage = "Invalid login attempt.";
|
||||
var user = await _userManager.FindByIdAsync(viewModel.UserId);
|
||||
|
||||
if (user == null)
|
||||
if (!UserService.TryCanLogin(user, out var message))
|
||||
{
|
||||
return NotFound();
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
{
|
||||
Severity = user == null ? StatusMessageModel.StatusSeverity.Error : StatusMessageModel.StatusSeverity.Warning,
|
||||
Message = message
|
||||
});
|
||||
return RedirectToAction("Login");
|
||||
}
|
||||
|
||||
var errorMessage = string.Empty;
|
||||
try
|
||||
{
|
||||
if (await _fido2Service.CompleteLogin(viewModel.UserId, JObject.Parse(viewModel.Response).ToObject<AuthenticatorAssertionRawResponse>()))
|
||||
{
|
||||
await _signInManager.SignInAsync(user, viewModel.RememberMe, "FIDO2");
|
||||
_logger.LogInformation("User logged in.");
|
||||
await _signInManager.SignInAsync(user!, viewModel.RememberMe, "FIDO2");
|
||||
_logger.LogInformation("User logged in");
|
||||
return RedirectToLocal(returnUrl);
|
||||
}
|
||||
|
||||
errorMessage = "Invalid login attempt.";
|
||||
}
|
||||
catch (Fido2VerificationException e)
|
||||
{
|
||||
errorMessage = e.Message;
|
||||
}
|
||||
|
||||
ModelState.AddModelError(string.Empty, errorMessage);
|
||||
if (!string.IsNullOrEmpty(errorMessage))
|
||||
{
|
||||
ModelState.AddModelError(string.Empty, errorMessage);
|
||||
}
|
||||
viewModel.Response = null;
|
||||
return View("SecondaryLogin", new SecondaryLoginViewModel()
|
||||
return View("SecondaryLogin", new SecondaryLoginViewModel
|
||||
{
|
||||
LoginWithFido2ViewModel = viewModel,
|
||||
LoginWithLNURLAuthViewModel = (await _lnurlAuthService.HasCredentials(user.Id)) ? await BuildLNURLAuthViewModel(viewModel.RememberMe, user) : null,
|
||||
LoginWithLNURLAuthViewModel = await _lnurlAuthService.HasCredentials(user!.Id) ? await BuildLNURLAuthViewModel(viewModel.RememberMe, user) : null,
|
||||
LoginWith2FaViewModel = !user.TwoFactorEnabled
|
||||
? null
|
||||
: new LoginWith2faViewModel()
|
||||
: new LoginWith2faViewModel
|
||||
{
|
||||
RememberMe = viewModel.RememberMe
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet("/login/2fa")]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> LoginWith2fa(bool rememberMe, string returnUrl = null)
|
||||
@ -401,7 +406,6 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
// Ensure the user has gone through the username & password screen first
|
||||
var user = await _signInManager.GetTwoFactorAuthenticationUserAsync();
|
||||
|
||||
if (user == null)
|
||||
{
|
||||
throw new ApplicationException($"Unable to load two-factor authentication user.");
|
||||
@ -409,11 +413,11 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
ViewData["ReturnUrl"] = returnUrl;
|
||||
|
||||
return View("SecondaryLogin", new SecondaryLoginViewModel()
|
||||
return View("SecondaryLogin", new SecondaryLoginViewModel
|
||||
{
|
||||
LoginWith2FaViewModel = new LoginWith2faViewModel { RememberMe = rememberMe },
|
||||
LoginWithFido2ViewModel = (await _fido2Service.HasCredentials(user.Id)) ? await BuildFido2ViewModel(rememberMe, user) : null,
|
||||
LoginWithLNURLAuthViewModel = (await _lnurlAuthService.HasCredentials(user.Id)) ? await BuildLNURLAuthViewModel(rememberMe, user) : null,
|
||||
LoginWithFido2ViewModel = await _fido2Service.HasCredentials(user.Id) ? await BuildFido2ViewModel(rememberMe, user) : null,
|
||||
LoginWithLNURLAuthViewModel = await _lnurlAuthService.HasCredentials(user.Id) ? await BuildLNURLAuthViewModel(rememberMe, user) : null,
|
||||
});
|
||||
}
|
||||
|
||||
@ -437,32 +441,32 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
if (!UserService.TryCanLogin(user, out var message))
|
||||
{
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Warning,
|
||||
Message = message
|
||||
});
|
||||
return View(model);
|
||||
}
|
||||
|
||||
var authenticatorCode = model.TwoFactorCode.Replace(" ", string.Empty, StringComparison.InvariantCulture).Replace("-", string.Empty, StringComparison.InvariantCulture);
|
||||
|
||||
var result = await _signInManager.TwoFactorAuthenticatorSignInAsync(authenticatorCode, rememberMe, model.RememberMachine);
|
||||
|
||||
if (result.Succeeded)
|
||||
{
|
||||
_logger.LogInformation("User with ID {UserId} logged in with 2fa.", user.Id);
|
||||
_logger.LogInformation("User with ID {UserId} logged in with 2fa", user.Id);
|
||||
return RedirectToLocal(returnUrl);
|
||||
}
|
||||
else if (result.IsLockedOut)
|
||||
|
||||
_logger.LogWarning("Invalid authenticator code entered for user with ID {UserId}", user.Id);
|
||||
ModelState.AddModelError(string.Empty, "Invalid authenticator code.");
|
||||
return View("SecondaryLogin", new SecondaryLoginViewModel
|
||||
{
|
||||
_logger.LogWarning("User with ID {UserId} account locked out.", user.Id);
|
||||
return RedirectToAction(nameof(Lockout), new { user.LockoutEnd });
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("Invalid authenticator code entered for user with ID {UserId}.", user.Id);
|
||||
ModelState.AddModelError(string.Empty, "Invalid authenticator code.");
|
||||
return View("SecondaryLogin", new SecondaryLoginViewModel()
|
||||
{
|
||||
LoginWith2FaViewModel = model,
|
||||
LoginWithFido2ViewModel = (await _fido2Service.HasCredentials(user.Id)) ? await BuildFido2ViewModel(rememberMe, user) : null,
|
||||
LoginWithLNURLAuthViewModel = (await _lnurlAuthService.HasCredentials(user.Id)) ? await BuildLNURLAuthViewModel(rememberMe, user) : null,
|
||||
});
|
||||
}
|
||||
LoginWith2FaViewModel = model,
|
||||
LoginWithFido2ViewModel = await _fido2Service.HasCredentials(user.Id) ? await BuildFido2ViewModel(rememberMe, user) : null,
|
||||
LoginWithLNURLAuthViewModel = await _lnurlAuthService.HasCredentials(user.Id) ? await BuildLNURLAuthViewModel(rememberMe, user) : null,
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet("/login/recovery-code")]
|
||||
@ -504,30 +508,35 @@ namespace BTCPayServer.Controllers
|
||||
var user = await _signInManager.GetTwoFactorAuthenticationUserAsync();
|
||||
if (user == null)
|
||||
{
|
||||
throw new ApplicationException($"Unable to load two-factor authentication user.");
|
||||
throw new ApplicationException("Unable to load two-factor authentication user.");
|
||||
}
|
||||
if (!UserService.TryCanLogin(user, out var message))
|
||||
{
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Warning,
|
||||
Message = message
|
||||
});
|
||||
return View(model);
|
||||
}
|
||||
|
||||
var recoveryCode = model.RecoveryCode.Replace(" ", string.Empty, StringComparison.InvariantCulture);
|
||||
|
||||
var result = await _signInManager.TwoFactorRecoveryCodeSignInAsync(recoveryCode);
|
||||
|
||||
if (result.Succeeded)
|
||||
{
|
||||
_logger.LogInformation("User with ID {UserId} logged in with a recovery code.", user.Id);
|
||||
_logger.LogInformation("User with ID {UserId} logged in with a recovery code", user.Id);
|
||||
return RedirectToLocal(returnUrl);
|
||||
}
|
||||
if (result.IsLockedOut)
|
||||
{
|
||||
_logger.LogWarning("User with ID {UserId} account locked out.", user.Id);
|
||||
_logger.LogWarning("User with ID {UserId} account locked out", user.Id);
|
||||
|
||||
return RedirectToAction(nameof(Lockout), new { user.LockoutEnd });
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("Invalid recovery code entered for user with ID {UserId}", user.Id);
|
||||
ModelState.AddModelError(string.Empty, "Invalid recovery code entered.");
|
||||
return View();
|
||||
}
|
||||
|
||||
_logger.LogWarning("Invalid recovery code entered for user with ID {UserId}", user.Id);
|
||||
ModelState.AddModelError(string.Empty, "Invalid recovery code entered.");
|
||||
return View();
|
||||
}
|
||||
|
||||
[HttpGet("/login/lockout")]
|
||||
@ -540,7 +549,7 @@ namespace BTCPayServer.Controllers
|
||||
[HttpGet("/register")]
|
||||
[AllowAnonymous]
|
||||
[RateLimitsFilter(ZoneLimits.Register, Scope = RateLimitsScope.RemoteAddress)]
|
||||
public IActionResult Register(string returnUrl = null, bool logon = true)
|
||||
public IActionResult Register(string returnUrl = null)
|
||||
{
|
||||
if (!CanLoginOrRegister())
|
||||
{
|
||||
@ -567,32 +576,36 @@ namespace BTCPayServer.Controllers
|
||||
var policies = await _SettingsRepository.GetSettingAsync<PoliciesSettings>() ?? new PoliciesSettings();
|
||||
if (policies.LockSubscription && !User.IsInRole(Roles.ServerAdmin))
|
||||
return RedirectToAction(nameof(UIHomeController.Index), "UIHome");
|
||||
|
||||
if (ModelState.IsValid)
|
||||
{
|
||||
var anyAdmin = (await _userManager.GetUsersInRoleAsync(Roles.ServerAdmin)).Any();
|
||||
var isFirstAdmin = !anyAdmin || (model.IsAdmin && _Options.CheatMode);
|
||||
var user = new ApplicationUser
|
||||
{
|
||||
UserName = model.Email,
|
||||
Email = model.Email,
|
||||
RequiresEmailConfirmation = policies.RequiresConfirmedEmail,
|
||||
Created = DateTimeOffset.UtcNow
|
||||
RequiresApproval = policies.RequiresUserApproval,
|
||||
Created = DateTimeOffset.UtcNow,
|
||||
Approved = isFirstAdmin // auto-approve first admin
|
||||
};
|
||||
var result = await _userManager.CreateAsync(user, model.Password);
|
||||
if (result.Succeeded)
|
||||
{
|
||||
var admin = await _userManager.GetUsersInRoleAsync(Roles.ServerAdmin);
|
||||
if (admin.Count == 0 || (model.IsAdmin && _Options.CheatMode))
|
||||
if (isFirstAdmin)
|
||||
{
|
||||
await _RoleManager.CreateAsync(new IdentityRole(Roles.ServerAdmin));
|
||||
await _userManager.AddToRoleAsync(user, Roles.ServerAdmin);
|
||||
var settings = await _SettingsRepository.GetSettingAsync<ThemeSettings>();
|
||||
var settings = await _SettingsRepository.GetSettingAsync<ThemeSettings>() ?? new ThemeSettings();
|
||||
settings.FirstRun = false;
|
||||
await _SettingsRepository.UpdateSetting<ThemeSettings>(settings);
|
||||
await _SettingsRepository.UpdateSetting(settings);
|
||||
|
||||
await _SettingsRepository.FirstAdminRegistered(policies, _Options.UpdateUrl != null, _Options.DisableRegistration, Logs);
|
||||
RegisteredAdmin = true;
|
||||
}
|
||||
|
||||
_eventAggregator.Publish(new UserRegisteredEvent()
|
||||
_eventAggregator.Publish(new UserRegisteredEvent
|
||||
{
|
||||
RequestUri = Request.GetAbsoluteRootUri(),
|
||||
User = user,
|
||||
@ -600,19 +613,29 @@ namespace BTCPayServer.Controllers
|
||||
});
|
||||
RegisteredUserId = user.Id;
|
||||
|
||||
if (!policies.RequiresConfirmedEmail)
|
||||
TempData[WellKnownTempData.SuccessMessage] = "Account created.";
|
||||
if (policies.RequiresConfirmedEmail)
|
||||
{
|
||||
if (logon)
|
||||
await _signInManager.SignInAsync(user, isPersistent: false);
|
||||
TempData[WellKnownTempData.SuccessMessage] += " Please confirm your email.";
|
||||
}
|
||||
if (policies.RequiresUserApproval)
|
||||
{
|
||||
TempData[WellKnownTempData.SuccessMessage] += " The new account requires approval by an admin before you can log in.";
|
||||
}
|
||||
if (policies.RequiresConfirmedEmail || policies.RequiresUserApproval)
|
||||
{
|
||||
return RedirectToAction(nameof(Login));
|
||||
}
|
||||
if (logon)
|
||||
{
|
||||
await _signInManager.SignInAsync(user, isPersistent: false);
|
||||
return RedirectToLocal(returnUrl);
|
||||
}
|
||||
else
|
||||
{
|
||||
TempData[WellKnownTempData.SuccessMessage] = "Account created, please confirm your email";
|
||||
return View();
|
||||
}
|
||||
}
|
||||
AddErrors(result);
|
||||
else
|
||||
{
|
||||
AddErrors(result);
|
||||
}
|
||||
}
|
||||
|
||||
// If we got this far, something failed, redisplay form
|
||||
@ -628,8 +651,8 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
await _signInManager.SignOutAsync();
|
||||
HttpContext.DeleteUserPrefsCookie();
|
||||
_logger.LogInformation("User logged out.");
|
||||
return RedirectToAction(nameof(UIAccountController.Login));
|
||||
_logger.LogInformation("User logged out");
|
||||
return RedirectToAction(nameof(Login));
|
||||
}
|
||||
|
||||
[HttpGet("/register/confirm-email")]
|
||||
@ -650,7 +673,7 @@ namespace BTCPayServer.Controllers
|
||||
if (!await _userManager.HasPasswordAsync(user))
|
||||
{
|
||||
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Info,
|
||||
Message = "Your email has been confirmed but you still need to set your password."
|
||||
@ -660,7 +683,7 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
if (result.Succeeded)
|
||||
{
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Success,
|
||||
Message = "Your email has been confirmed."
|
||||
@ -687,12 +710,12 @@ namespace BTCPayServer.Controllers
|
||||
if (ModelState.IsValid)
|
||||
{
|
||||
var user = await _userManager.FindByEmailAsync(model.Email);
|
||||
if (user == null || (user.RequiresEmailConfirmation && !(await _userManager.IsEmailConfirmedAsync(user))))
|
||||
if (!UserService.TryCanLogin(user, out _))
|
||||
{
|
||||
// Don't reveal that the user does not exist or is not confirmed
|
||||
return RedirectToAction(nameof(ForgotPasswordConfirmation));
|
||||
}
|
||||
_eventAggregator.Publish(new UserPasswordResetRequestedEvent()
|
||||
_eventAggregator.Publish(new UserPasswordResetRequestedEvent
|
||||
{
|
||||
User = user,
|
||||
RequestUri = Request.GetAbsoluteRootUri()
|
||||
@ -740,16 +763,16 @@ namespace BTCPayServer.Controllers
|
||||
return View(model);
|
||||
}
|
||||
var user = await _userManager.FindByEmailAsync(model.Email);
|
||||
if (user == null)
|
||||
if (!UserService.TryCanLogin(user, out _))
|
||||
{
|
||||
// Don't reveal that the user does not exist
|
||||
return RedirectToAction(nameof(Login));
|
||||
}
|
||||
|
||||
var result = await _userManager.ResetPasswordAsync(user, model.Code, model.Password);
|
||||
var result = await _userManager.ResetPasswordAsync(user!, model.Code, model.Password);
|
||||
if (result.Succeeded)
|
||||
{
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Success,
|
||||
Message = "Password successfully set."
|
||||
@ -800,7 +823,7 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
private void SetInsecureFlags()
|
||||
{
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Error,
|
||||
Message = "You cannot login over an insecure connection. Please use HTTPS or Tor."
|
||||
|
@ -14,11 +14,20 @@ using System.Threading;
|
||||
using System;
|
||||
using NBitcoin.DataEncoders;
|
||||
using System.Text.Json.Serialization;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using System.Collections.Generic;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Lightning;
|
||||
using System.Reflection.Metadata;
|
||||
|
||||
namespace BTCPayServer.Controllers;
|
||||
|
||||
public class UIBoltcardController : Controller
|
||||
{
|
||||
private readonly PullPaymentHostedService _ppService;
|
||||
private readonly StoreRepository _storeRepository;
|
||||
|
||||
public class BoltcardSettings
|
||||
{
|
||||
[JsonConverter(typeof(NBitcoin.JsonConverters.HexJsonConverter))]
|
||||
@ -28,11 +37,15 @@ public class UIBoltcardController : Controller
|
||||
UILNURLController lnUrlController,
|
||||
SettingsRepository settingsRepository,
|
||||
ApplicationDbContextFactory contextFactory,
|
||||
PullPaymentHostedService ppService,
|
||||
StoreRepository storeRepository,
|
||||
BTCPayServerEnvironment env)
|
||||
{
|
||||
LNURLController = lnUrlController;
|
||||
SettingsRepository = settingsRepository;
|
||||
ContextFactory = contextFactory;
|
||||
_ppService = ppService;
|
||||
_storeRepository = storeRepository;
|
||||
Env = env;
|
||||
}
|
||||
|
||||
@ -41,6 +54,64 @@ public class UIBoltcardController : Controller
|
||||
public ApplicationDbContextFactory ContextFactory { get; }
|
||||
public BTCPayServerEnvironment Env { get; }
|
||||
|
||||
[AllowAnonymous]
|
||||
[HttpGet("~/boltcard/pay")]
|
||||
public async Task<IActionResult> GetPayRequest([FromQuery] string? p, [FromQuery] long? amount = null)
|
||||
{
|
||||
var issuerKey = await SettingsRepository.GetIssuerKey(Env);
|
||||
var piccData = issuerKey.TryDecrypt(p);
|
||||
if (piccData is null)
|
||||
return BadRequest(new LNUrlStatusResponse { Status = "ERROR", Reason = "Invalid PICCData" });
|
||||
|
||||
piccData = new BoltcardPICCData(piccData.Uid, int.MaxValue - 10); // do not check the counter
|
||||
var registration = await ContextFactory.GetBoltcardRegistration(issuerKey, piccData, false);
|
||||
var pp = await _ppService.GetPullPayment(registration!.PullPaymentId, false);
|
||||
var store = await _storeRepository.FindStore(pp.StoreId);
|
||||
|
||||
var lnUrlMetadata = new Dictionary<string, string>();
|
||||
lnUrlMetadata.Add("text/plain", "Boltcard Top-Up");
|
||||
var payRequest = new LNURLPayRequest
|
||||
{
|
||||
Tag = "payRequest",
|
||||
MinSendable = LightMoney.Satoshis(1.0m),
|
||||
MaxSendable = LightMoney.FromUnit(6.12m, LightMoneyUnit.BTC),
|
||||
Callback = new Uri(GetPayLink(p, Request.Scheme), UriKind.Absolute),
|
||||
CommentAllowed = 0
|
||||
};
|
||||
payRequest.Metadata = Newtonsoft.Json.JsonConvert.SerializeObject(lnUrlMetadata.Select(kv => new[] { kv.Key, kv.Value }));
|
||||
if (amount is null)
|
||||
return Ok(payRequest);
|
||||
|
||||
var cryptoCode = "BTC";
|
||||
|
||||
var currency = "BTC";
|
||||
var invoiceAmount = LightMoney.FromUnit(amount.Value, LightMoneyUnit.MilliSatoshi).ToUnit(LightMoneyUnit.BTC);
|
||||
|
||||
if (pp.GetBlob().Currency == "SATS")
|
||||
{
|
||||
currency = "SATS";
|
||||
invoiceAmount = LightMoney.FromUnit(amount.Value, LightMoneyUnit.MilliSatoshi).ToUnit(LightMoneyUnit.Satoshi);
|
||||
}
|
||||
|
||||
LNURLController.ControllerContext.HttpContext = HttpContext;
|
||||
var result = await LNURLController.GetLNURLRequest(
|
||||
cryptoCode,
|
||||
store,
|
||||
store.GetStoreBlob(),
|
||||
new CreateInvoiceRequest()
|
||||
{
|
||||
Currency = currency,
|
||||
Amount = invoiceAmount
|
||||
},
|
||||
payRequest,
|
||||
lnUrlMetadata,
|
||||
[PullPaymentHostedService.GetInternalTag(pp.Id)]);
|
||||
if (result is not OkObjectResult ok || ok.Value is not LNURLPayRequest payRequest2)
|
||||
return result;
|
||||
payRequest = payRequest2;
|
||||
var invoiceId = payRequest.Callback.AbsoluteUri.Split('/').Last();
|
||||
return await LNURLController.GetLNURLForInvoice(invoiceId, cryptoCode, amount.Value, null);
|
||||
}
|
||||
[AllowAnonymous]
|
||||
[HttpGet("~/boltcard")]
|
||||
public async Task<IActionResult> GetWithdrawRequest([FromQuery] string? p, [FromQuery] string? c, [FromQuery] string? pr, [FromQuery] string? k1, CancellationToken cancellationToken)
|
||||
@ -65,6 +136,16 @@ public class UIBoltcardController : Controller
|
||||
if (!cardKey.CheckSunMac(c, piccData))
|
||||
return BadRequest(new LNUrlStatusResponse { Status = "ERROR", Reason = "Replayed or expired query" });
|
||||
LNURLController.ControllerContext.HttpContext = HttpContext;
|
||||
return await LNURLController.GetLNURLForPullPayment("BTC", registration.PullPaymentId, pr, $"{p}-{c}", cancellationToken);
|
||||
var res = await LNURLController.GetLNURLForPullPayment("BTC", registration.PullPaymentId, pr, $"{p}-{c}", cancellationToken);
|
||||
if (res is not OkObjectResult ok || ok.Value is not LNURLWithdrawRequest withdrawRequest)
|
||||
return res;
|
||||
var paylink = GetPayLink(p, "lnurlp");
|
||||
withdrawRequest.PayLink = new Uri(paylink, UriKind.Absolute);
|
||||
return res;
|
||||
}
|
||||
|
||||
private string GetPayLink(string? p, string scheme)
|
||||
{
|
||||
return Url.Action(nameof(GetPayRequest), "UIBoltcard", new { p }, scheme)!;
|
||||
}
|
||||
}
|
||||
|
@ -26,6 +26,7 @@ using BTCPayServer.Plugins.PointOfSale.Models;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.PaymentRequests;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using LNURL;
|
||||
@ -436,6 +437,13 @@ namespace BTCPayServer
|
||||
var store = await _storeRepository.FindStore(lightningAddressSettings.StoreDataId);
|
||||
if (store is null)
|
||||
return NotFound("Unknown username");
|
||||
List<string> additionalTags = new List<string>();
|
||||
if (blob?.PullPaymentId is not null)
|
||||
{
|
||||
var pp = await _pullPaymentHostedService.GetPullPayment(blob.PullPaymentId, false);
|
||||
if (pp != null)
|
||||
additionalTags.Add(PullPaymentHostedService.GetInternalTag(blob.PullPaymentId));
|
||||
}
|
||||
var result = await GetLNURLRequest(
|
||||
cryptoCode,
|
||||
store,
|
||||
@ -453,7 +461,7 @@ namespace BTCPayServer
|
||||
new Dictionary<string, string>
|
||||
{
|
||||
{ "text/identifier", $"{username}@{Request.Host}" }
|
||||
});
|
||||
}, additionalTags);
|
||||
if (result is not OkObjectResult ok || ok.Value is not LNURLPayRequest payRequest)
|
||||
return result;
|
||||
var invoiceId = payRequest.Callback.AbsoluteUri.Split('/').Last();
|
||||
@ -495,7 +503,7 @@ namespace BTCPayServer
|
||||
});
|
||||
}
|
||||
|
||||
private async Task<IActionResult> GetLNURLRequest(
|
||||
internal async Task<IActionResult> GetLNURLRequest(
|
||||
string cryptoCode,
|
||||
Data.StoreData store,
|
||||
Data.StoreBlob blob,
|
||||
|
@ -11,6 +11,7 @@ using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Abstractions.Models;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Controllers.Greenfield;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Lightning;
|
||||
@ -127,13 +128,27 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
if (_pullPaymentHostedService.SupportsLNURL(blob))
|
||||
{
|
||||
var url = Url.Action("GetLNURLForPullPayment", "UILNURL", new { cryptoCode = _networkProvider.DefaultNetwork.CryptoCode, pullPaymentId = vm.Id }, Request.Scheme, Request.Host.ToString());
|
||||
var url = Url.Action(nameof(UILNURLController.GetLNURLForPullPayment), "UILNURL", new { cryptoCode = _networkProvider.DefaultNetwork.CryptoCode, pullPaymentId = vm.Id }, Request.Scheme, Request.Host.ToString());
|
||||
vm.LnurlEndpoint = url != null ? new Uri(url) : null;
|
||||
vm.SetupDeepLink = $"boltcard://program?url={GetBoltcardDeeplinkUrl(vm, OnExistingBehavior.UpdateVersion)}";
|
||||
vm.ResetDeepLink = $"boltcard://reset?url={GetBoltcardDeeplinkUrl(vm, OnExistingBehavior.KeepVersion)}";
|
||||
}
|
||||
|
||||
return View(nameof(ViewPullPayment), vm);
|
||||
}
|
||||
|
||||
private string GetBoltcardDeeplinkUrl(ViewPullPaymentModel vm, OnExistingBehavior onExisting)
|
||||
{
|
||||
var registerUrl = Url.Action(nameof(GreenfieldPullPaymentController.RegisterBoltcard), "GreenfieldPullPayment",
|
||||
new
|
||||
{
|
||||
pullPaymentId = vm.Id,
|
||||
onExisting = onExisting.ToString()
|
||||
}, Request.Scheme, Request.Host.ToString());
|
||||
registerUrl = Uri.EscapeDataString(registerUrl);
|
||||
return registerUrl;
|
||||
}
|
||||
|
||||
[HttpGet("stores/{storeId}/pull-payments/edit/{pullPaymentId}")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> EditPullPayment(string storeId, string pullPaymentId)
|
||||
@ -261,7 +276,8 @@ namespace BTCPayServer.Controllers
|
||||
Destination = destination,
|
||||
PullPaymentId = pullPaymentId,
|
||||
Value = vm.ClaimedAmount,
|
||||
PaymentMethodId = paymentMethodId
|
||||
PaymentMethodId = paymentMethodId,
|
||||
StoreId = pp.StoreId
|
||||
});
|
||||
|
||||
if (result.Result != ClaimRequest.ClaimResult.Ok)
|
||||
|
@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
@ -8,25 +7,21 @@ using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Abstractions.Models;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Events;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.ServerViewModels;
|
||||
using BTCPayServer.Services;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using MimeKit;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public partial class UIServerController
|
||||
{
|
||||
[Route("server/users")]
|
||||
[HttpGet("server/users")]
|
||||
public async Task<IActionResult> ListUsers(
|
||||
[FromServices] RoleManager<IdentityRole> roleManager,
|
||||
UsersViewModel model,
|
||||
string sortOrder = null
|
||||
)
|
||||
UsersViewModel model,
|
||||
string sortOrder = null)
|
||||
{
|
||||
model = this.ParseListQuery(model ?? new UsersViewModel());
|
||||
|
||||
@ -64,7 +59,8 @@ namespace BTCPayServer.Controllers
|
||||
Name = u.UserName,
|
||||
Email = u.Email,
|
||||
Id = u.Id,
|
||||
Verified = u.EmailConfirmed || !u.RequiresEmailConfirmation,
|
||||
EmailConfirmed = u.RequiresEmailConfirmation ? u.EmailConfirmed : null,
|
||||
Approved = u.RequiresApproval ? u.Approved : null,
|
||||
Created = u.Created,
|
||||
Roles = u.UserRoles.Select(role => role.RoleId),
|
||||
Disabled = u.LockoutEnabled && u.LockoutEnd != null && DateTimeOffset.UtcNow < u.LockoutEnd.Value.UtcDateTime
|
||||
@ -74,44 +70,67 @@ namespace BTCPayServer.Controllers
|
||||
return View(model);
|
||||
}
|
||||
|
||||
[Route("server/users/{userId}")]
|
||||
[HttpGet("server/users/{userId}")]
|
||||
public new async Task<IActionResult> User(string userId)
|
||||
{
|
||||
var user = await _UserManager.FindByIdAsync(userId);
|
||||
if (user == null)
|
||||
return NotFound();
|
||||
var roles = await _UserManager.GetRolesAsync(user);
|
||||
var userVM = new UsersViewModel.UserViewModel
|
||||
var model = new UsersViewModel.UserViewModel
|
||||
{
|
||||
Id = user.Id,
|
||||
Email = user.Email,
|
||||
Verified = user.EmailConfirmed || !user.RequiresEmailConfirmation,
|
||||
EmailConfirmed = user.RequiresEmailConfirmation ? user.EmailConfirmed : null,
|
||||
Approved = user.RequiresApproval ? user.Approved : null,
|
||||
IsAdmin = Roles.HasServerAdmin(roles)
|
||||
};
|
||||
return View(userVM);
|
||||
return View(model);
|
||||
}
|
||||
|
||||
[Route("server/users/{userId}")]
|
||||
[HttpPost]
|
||||
[HttpPost("server/users/{userId}")]
|
||||
public new async Task<IActionResult> User(string userId, UsersViewModel.UserViewModel viewModel)
|
||||
{
|
||||
var user = await _UserManager.FindByIdAsync(userId);
|
||||
if (user == null)
|
||||
return NotFound();
|
||||
|
||||
bool? propertiesChanged = null;
|
||||
bool? adminStatusChanged = null;
|
||||
bool? approvalStatusChanged = null;
|
||||
|
||||
if (user.RequiresApproval && viewModel.Approved.HasValue)
|
||||
{
|
||||
approvalStatusChanged = await _userService.SetUserApproval(user.Id, viewModel.Approved.Value, Request.GetAbsoluteRootUri());
|
||||
}
|
||||
if (user.RequiresEmailConfirmation && viewModel.EmailConfirmed.HasValue && user.EmailConfirmed != viewModel.EmailConfirmed)
|
||||
{
|
||||
user.EmailConfirmed = viewModel.EmailConfirmed.Value;
|
||||
propertiesChanged = true;
|
||||
}
|
||||
|
||||
var admins = await _UserManager.GetUsersInRoleAsync(Roles.ServerAdmin);
|
||||
var roles = await _UserManager.GetRolesAsync(user);
|
||||
var wasAdmin = Roles.HasServerAdmin(roles);
|
||||
if (!viewModel.IsAdmin && admins.Count == 1 && wasAdmin)
|
||||
{
|
||||
TempData[WellKnownTempData.ErrorMessage] = "This is the only Admin, so their role can't be removed until another Admin is added.";
|
||||
return View(viewModel); // return
|
||||
return View(viewModel);
|
||||
}
|
||||
|
||||
if (viewModel.IsAdmin != wasAdmin)
|
||||
{
|
||||
var success = await _userService.SetAdminUser(user.Id, viewModel.IsAdmin);
|
||||
if (success)
|
||||
adminStatusChanged = await _userService.SetAdminUser(user.Id, viewModel.IsAdmin);
|
||||
}
|
||||
|
||||
if (propertiesChanged is true)
|
||||
{
|
||||
propertiesChanged = await _UserManager.UpdateAsync(user) is { Succeeded: true };
|
||||
}
|
||||
|
||||
if (propertiesChanged.HasValue || adminStatusChanged.HasValue || approvalStatusChanged.HasValue)
|
||||
{
|
||||
if (propertiesChanged is not false && adminStatusChanged is not false && approvalStatusChanged is not false)
|
||||
{
|
||||
TempData[WellKnownTempData.SuccessMessage] = "User successfully updated";
|
||||
}
|
||||
@ -121,23 +140,22 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
}
|
||||
|
||||
return RedirectToAction(nameof(User), new { userId = userId });
|
||||
return RedirectToAction(nameof(User), new { userId });
|
||||
}
|
||||
|
||||
[Route("server/users/new")]
|
||||
[HttpGet]
|
||||
[HttpGet("server/users/new")]
|
||||
public IActionResult CreateUser()
|
||||
{
|
||||
ViewData["AllowRequestApproval"] = _policiesSettings.RequiresUserApproval;
|
||||
ViewData["AllowRequestEmailConfirmation"] = _policiesSettings.RequiresConfirmedEmail;
|
||||
return View();
|
||||
}
|
||||
|
||||
[Route("server/users/new")]
|
||||
[HttpPost]
|
||||
[HttpPost("server/users/new")]
|
||||
public async Task<IActionResult> CreateUser(RegisterFromAdminViewModel model)
|
||||
{
|
||||
var requiresConfirmedEmail = _policiesSettings.RequiresConfirmedEmail;
|
||||
ViewData["AllowRequestEmailConfirmation"] = requiresConfirmedEmail;
|
||||
ViewData["AllowRequestApproval"] = _policiesSettings.RequiresUserApproval;
|
||||
ViewData["AllowRequestEmailConfirmation"] = _policiesSettings.RequiresConfirmedEmail;
|
||||
if (!_Options.CheatMode)
|
||||
model.IsAdmin = false;
|
||||
if (ModelState.IsValid)
|
||||
@ -148,7 +166,9 @@ namespace BTCPayServer.Controllers
|
||||
UserName = model.Email,
|
||||
Email = model.Email,
|
||||
EmailConfirmed = model.EmailConfirmed,
|
||||
RequiresEmailConfirmation = requiresConfirmedEmail,
|
||||
RequiresEmailConfirmation = _policiesSettings.RequiresConfirmedEmail,
|
||||
RequiresApproval = _policiesSettings.RequiresUserApproval,
|
||||
Approved = model.Approved,
|
||||
Created = DateTimeOffset.UtcNow
|
||||
};
|
||||
|
||||
@ -223,7 +243,6 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
if (await _userService.IsUserTheOnlyOneAdmin(user))
|
||||
{
|
||||
// return
|
||||
return View("Confirm", new ConfirmModel("Delete admin",
|
||||
$"Unable to proceed: As the user <strong>{Html.Encode(user.Email)}</strong> is the last enabled admin, it cannot be removed."));
|
||||
}
|
||||
@ -281,6 +300,29 @@ namespace BTCPayServer.Controllers
|
||||
return RedirectToAction(nameof(ListUsers));
|
||||
}
|
||||
|
||||
[HttpGet("server/users/{userId}/approve")]
|
||||
public async Task<IActionResult> ApproveUser(string userId, bool approved)
|
||||
{
|
||||
var user = userId == null ? null : await _UserManager.FindByIdAsync(userId);
|
||||
if (user == null)
|
||||
return NotFound();
|
||||
|
||||
return View("Confirm", new ConfirmModel($"{(approved ? "Approve" : "Unapprove")} user", $"The user <strong>{Html.Encode(user.Email)}</strong> will be {(approved ? "approved" : "unapproved")}. Are you sure?", (approved ? "Approve" : "Unapprove")));
|
||||
}
|
||||
|
||||
[HttpPost("server/users/{userId}/approve")]
|
||||
public async Task<IActionResult> ApproveUserPost(string userId, bool approved)
|
||||
{
|
||||
var user = userId == null ? null : await _UserManager.FindByIdAsync(userId);
|
||||
if (user == null)
|
||||
return NotFound();
|
||||
|
||||
await _userService.SetUserApproval(userId, approved, Request.GetAbsoluteRootUri());
|
||||
|
||||
TempData[WellKnownTempData.SuccessMessage] = $"User {(approved ? "approved" : "unapproved")}";
|
||||
return RedirectToAction(nameof(ListUsers));
|
||||
}
|
||||
|
||||
[HttpGet("server/users/{userId}/verification-email")]
|
||||
public async Task<IActionResult> SendVerificationEmail(string userId)
|
||||
{
|
||||
@ -332,5 +374,8 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
[Display(Name = "Email confirmed?")]
|
||||
public bool EmailConfirmed { get; set; }
|
||||
|
||||
[Display(Name = "User approved?")]
|
||||
public bool Approved { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -27,22 +27,20 @@ namespace BTCPayServer.Controllers
|
||||
return NotFound();
|
||||
|
||||
var blob = store.GetStoreBlob();
|
||||
var storeSetupComplete = blob.EmailSettings?.IsComplete() is true;
|
||||
if (!storeSetupComplete && !TempData.HasStatusMessage())
|
||||
if (blob.EmailSettings?.IsComplete() is not true && !TempData.HasStatusMessage())
|
||||
{
|
||||
var emailSender = await _emailSenderFactory.GetEmailSender(store.Id) as StoreEmailSender;
|
||||
var hasServerFallback = await IsSetupComplete(emailSender?.FallbackSender);
|
||||
var message = hasServerFallback
|
||||
? "Emails will be sent with the email settings of the server"
|
||||
: "You need to configure email settings before this feature works";
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
if (!await IsSetupComplete(emailSender?.FallbackSender))
|
||||
{
|
||||
Severity = hasServerFallback ? StatusMessageModel.StatusSeverity.Info : StatusMessageModel.StatusSeverity.Warning,
|
||||
Html = $"{message}. <a class='alert-link' href='{Url.Action("StoreEmailSettings", new { storeId })}'>Configure store email settings</a>."
|
||||
});
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Warning,
|
||||
Html = $"You need to configure email settings before this feature works. <a class='alert-link' href='{Url.Action("StoreEmailSettings", new { storeId })}'>Configure store email settings</a>."
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
var vm = new StoreEmailRuleViewModel { Rules = blob.EmailRules ?? new List<StoreEmailRule>() };
|
||||
var vm = new StoreEmailRuleViewModel { Rules = blob.EmailRules ?? [] };
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
@ -172,13 +170,20 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
[HttpGet("{storeId}/email-settings")]
|
||||
public IActionResult StoreEmailSettings()
|
||||
public async Task<IActionResult> StoreEmailSettings(string storeId)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
var data = store.GetStoreBlob().EmailSettings ?? new EmailSettings();
|
||||
return View(new EmailsViewModel(data));
|
||||
|
||||
var blob = store.GetStoreBlob();
|
||||
var data = blob.EmailSettings ?? new EmailSettings();
|
||||
var fallbackSettings = await _emailSenderFactory.GetEmailSender(store.Id) is StoreEmailSender { FallbackSender: not null } storeSender
|
||||
? await storeSender.FallbackSender.GetEmailSettings()
|
||||
: null;
|
||||
var vm = new EmailsViewModel(data, fallbackSettings);
|
||||
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
[HttpPost("{storeId}/email-settings")]
|
||||
@ -187,7 +192,13 @@ namespace BTCPayServer.Controllers
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
|
||||
|
||||
var emailSender = await _emailSenderFactory.GetEmailSender(store.Id) as StoreEmailSender;
|
||||
var fallbackSettings = await _emailSenderFactory.GetEmailSender(store.Id) is StoreEmailSender { FallbackSender: not null } storeSender
|
||||
? await storeSender.FallbackSender.GetEmailSettings()
|
||||
: null;
|
||||
model.FallbackSettings = fallbackSettings;
|
||||
|
||||
if (command == "Test")
|
||||
{
|
||||
try
|
||||
@ -230,7 +241,7 @@ namespace BTCPayServer.Controllers
|
||||
return View(model);
|
||||
}
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
if (new EmailsViewModel(storeBlob.EmailSettings).PasswordSet && storeBlob.EmailSettings != null)
|
||||
if (storeBlob.EmailSettings != null && new EmailsViewModel(storeBlob.EmailSettings, fallbackSettings).PasswordSet)
|
||||
{
|
||||
model.Settings.Password = storeBlob.EmailSettings.Password;
|
||||
}
|
||||
|
@ -42,15 +42,7 @@ namespace BTCPayServer.Controllers
|
||||
psbtDestination.Amount = Money.Coins(transactionOutput.Amount.Value);
|
||||
psbtDestination.SubstractFees = transactionOutput.SubtractFeesFromOutput;
|
||||
}
|
||||
|
||||
if (network.SupportRBF)
|
||||
{
|
||||
if (sendModel.AllowFeeBump is WalletSendModel.ThreeStateBool.Yes)
|
||||
psbtRequest.RBF = true;
|
||||
if (sendModel.AllowFeeBump is WalletSendModel.ThreeStateBool.No)
|
||||
psbtRequest.RBF = false;
|
||||
}
|
||||
|
||||
psbtRequest.RBF = network.SupportRBF ? true : null;
|
||||
psbtRequest.AlwaysIncludeNonWitnessUTXO = sendModel.AlwaysIncludeNonWitnessUTXO;
|
||||
|
||||
psbtRequest.FeePreference = new FeePreference();
|
||||
|
@ -513,8 +513,6 @@ namespace BTCPayServer.Controllers
|
||||
recommendedFees.Select(tuple => tuple.GetAwaiter().GetResult()).Where(option => option != null).ToList();
|
||||
|
||||
model.FeeSatoshiPerByte = recommendedFees[1].GetAwaiter().GetResult()?.FeeRate;
|
||||
model.SupportRBF = network.SupportRBF;
|
||||
|
||||
model.CryptoDivisibility = network.Divisibility;
|
||||
using (CancellationTokenSource cts = new CancellationTokenSource())
|
||||
{
|
||||
@ -570,7 +568,6 @@ namespace BTCPayServer.Controllers
|
||||
if (network == null || network.ReadonlyWallet)
|
||||
return NotFound();
|
||||
|
||||
vm.SupportRBF = network.SupportRBF;
|
||||
vm.NBXSeedAvailable = await GetSeed(walletId, network) != null;
|
||||
if (!string.IsNullOrEmpty(bip21))
|
||||
{
|
||||
|
@ -202,7 +202,8 @@ namespace BTCPayServer.Data
|
||||
{ "JPY", "bitbank" },
|
||||
{ "TRY", "btcturk" },
|
||||
{ "UGX", "yadio"},
|
||||
{ "RSD", "bitpay"}
|
||||
{ "RSD", "bitpay"},
|
||||
{ "NGN", "bitnob"}
|
||||
};
|
||||
|
||||
public string GetRecommendedExchange() =>
|
||||
|
12
BTCPayServer/Events/UserApprovedEvent.cs
Normal file
12
BTCPayServer/Events/UserApprovedEvent.cs
Normal file
@ -0,0 +1,12 @@
|
||||
using System;
|
||||
using BTCPayServer.Data;
|
||||
|
||||
namespace BTCPayServer.Events
|
||||
{
|
||||
public class UserApprovedEvent
|
||||
{
|
||||
public ApplicationUser User { get; set; }
|
||||
public bool Approved { get; set; }
|
||||
public Uri RequestUri { get; set; }
|
||||
}
|
||||
}
|
@ -1,11 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Configuration;
|
||||
using BTCPayServer.Logging;
|
||||
using BTCPayServer.NTag424;
|
||||
using BTCPayServer.Services;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
|
@ -23,6 +23,12 @@ namespace BTCPayServer.Services
|
||||
$"Please confirm your account by clicking this link: <a href='{HtmlEncoder.Default.Encode(link)}'>link</a>");
|
||||
}
|
||||
|
||||
public static void SendApprovalConfirmation(this IEmailSender emailSender, MailboxAddress address, string link)
|
||||
{
|
||||
emailSender.SendEmail(address, "Your account has been approved",
|
||||
$"Your account has been approved and you can now <a href='{HtmlEncoder.Default.Encode(link)}'>login here</a>");
|
||||
}
|
||||
|
||||
public static void SendSetPasswordConfirmation(this IEmailSender emailSender, MailboxAddress address, string link, bool newPassword)
|
||||
{
|
||||
var subject = $"{(newPassword ? "Set" : "Update")} Password";
|
||||
|
@ -29,6 +29,11 @@ namespace Microsoft.AspNetCore.Mvc
|
||||
new { userId, code }, scheme, host, pathbase);
|
||||
}
|
||||
|
||||
public static string LoginLink(this LinkGenerator urlHelper, string scheme, HostString host, string pathbase)
|
||||
{
|
||||
return urlHelper.GetUriByAction(nameof(UIAccountController.Login), "UIAccount", null , scheme, host, pathbase);
|
||||
}
|
||||
|
||||
public static string ResetPasswordCallbackLink(this LinkGenerator urlHelper, string userId, string code, string scheme, HostString host, string pathbase)
|
||||
{
|
||||
return urlHelper.GetUriByAction(
|
||||
|
@ -7,11 +7,13 @@ using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Events;
|
||||
using BTCPayServer.Logging;
|
||||
using BTCPayServer.Models.WalletViewModels;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Rating;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Notifications;
|
||||
using BTCPayServer.Services.Notifications.Blobs;
|
||||
using BTCPayServer.Services.Rates;
|
||||
@ -47,7 +49,7 @@ namespace BTCPayServer.HostedServices
|
||||
public class PullPaymentHostedService : BaseAsyncService
|
||||
{
|
||||
private readonly string[] _lnurlSupportedCurrencies = { "BTC", "SATS" };
|
||||
|
||||
|
||||
public class CancelRequest
|
||||
{
|
||||
public CancelRequest(string pullPaymentId)
|
||||
@ -108,6 +110,25 @@ namespace BTCPayServer.HostedServices
|
||||
}
|
||||
}
|
||||
|
||||
public Task<string> CreatePullPayment(string storeId, CreatePullPaymentRequest request)
|
||||
{
|
||||
return CreatePullPayment(new CreatePullPayment()
|
||||
{
|
||||
StartsAt = request.StartsAt,
|
||||
ExpiresAt = request.ExpiresAt,
|
||||
Period = request.Period,
|
||||
BOLT11Expiration = request.BOLT11Expiration,
|
||||
Name = request.Name,
|
||||
Description = request.Description,
|
||||
Amount = request.Amount,
|
||||
Currency = request.Currency,
|
||||
StoreId = storeId,
|
||||
PaymentMethodIds = request.PaymentMethods.Select(p => PaymentMethodId.Parse(p)).ToArray(),
|
||||
AutoApproveClaims = request.AutoApproveClaims,
|
||||
EmbeddedCSS = request.EmbeddedCSS,
|
||||
CustomCSSLink = request.CustomCSSLink
|
||||
});
|
||||
}
|
||||
public async Task<string> CreatePullPayment(CreatePullPayment create)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(create);
|
||||
@ -173,7 +194,15 @@ namespace BTCPayServer.HostedServices
|
||||
var query = ctx.Payouts.AsQueryable();
|
||||
if (payoutQuery.States is not null)
|
||||
{
|
||||
query = query.Where(data => payoutQuery.States.Contains(data.State));
|
||||
if (payoutQuery.States.Length == 1)
|
||||
{
|
||||
var state = payoutQuery.States[0];
|
||||
query = query.Where(data => data.State == state);
|
||||
}
|
||||
else
|
||||
{
|
||||
query = query.Where(data => payoutQuery.States.Contains(data.State));
|
||||
}
|
||||
}
|
||||
|
||||
if (payoutQuery.PullPayments is not null)
|
||||
@ -196,12 +225,28 @@ namespace BTCPayServer.HostedServices
|
||||
|
||||
if (payoutQuery.PaymentMethods is not null)
|
||||
{
|
||||
query = query.Where(data => payoutQuery.PaymentMethods.Contains(data.PaymentMethodId));
|
||||
if (payoutQuery.PaymentMethods.Length == 1)
|
||||
{
|
||||
var pm = payoutQuery.PaymentMethods[0];
|
||||
query = query.Where(data => pm == data.PaymentMethodId);
|
||||
}
|
||||
else
|
||||
{
|
||||
query = query.Where(data => payoutQuery.PaymentMethods.Contains(data.PaymentMethodId));
|
||||
}
|
||||
}
|
||||
|
||||
if (payoutQuery.Stores is not null)
|
||||
{
|
||||
query = query.Where(data => payoutQuery.Stores.Contains(data.StoreDataId));
|
||||
if (payoutQuery.Stores.Length == 1)
|
||||
{
|
||||
var store = payoutQuery.Stores[0];
|
||||
query = query.Where(data => store == data.StoreDataId);
|
||||
}
|
||||
else
|
||||
{
|
||||
query = query.Where(data => payoutQuery.Stores.Contains(data.StoreDataId));
|
||||
}
|
||||
}
|
||||
if (payoutQuery.IncludeStoreData)
|
||||
{
|
||||
@ -239,7 +284,7 @@ namespace BTCPayServer.HostedServices
|
||||
|
||||
return await query.FirstOrDefaultAsync(data => data.Id == pullPaymentId);
|
||||
}
|
||||
|
||||
record TopUpRequest(string PullPaymentId, InvoiceEntity InvoiceEntity);
|
||||
class PayoutRequest
|
||||
{
|
||||
public PayoutRequest(TaskCompletionSource<ClaimRequest.ClaimResponse> completionSource,
|
||||
@ -249,6 +294,8 @@ namespace BTCPayServer.HostedServices
|
||||
ArgumentNullException.ThrowIfNull(completionSource);
|
||||
Completion = completionSource;
|
||||
ClaimRequest = request;
|
||||
if (request.StoreId is null)
|
||||
throw new ArgumentNullException(nameof(request.StoreId));
|
||||
}
|
||||
|
||||
public TaskCompletionSource<ClaimRequest.ClaimResponse> Completion { get; set; }
|
||||
@ -299,10 +346,20 @@ namespace BTCPayServer.HostedServices
|
||||
{
|
||||
payoutHandler.StartBackgroundCheck(Subscribe);
|
||||
}
|
||||
|
||||
_eventAggregator.Subscribe<Events.InvoiceEvent>(TopUpInvoice);
|
||||
return new[] { Loop() };
|
||||
}
|
||||
|
||||
private void TopUpInvoice(InvoiceEvent evt)
|
||||
{
|
||||
if (evt.EventCode == InvoiceEventCode.Completed)
|
||||
{
|
||||
foreach (var pullPaymentId in evt.Invoice.GetInternalTags("PULLPAY#"))
|
||||
{
|
||||
_Channel.Writer.TryWrite(new TopUpRequest(pullPaymentId, evt.Invoice));
|
||||
}
|
||||
}
|
||||
}
|
||||
private void Subscribe(params Type[] events)
|
||||
{
|
||||
foreach (Type @event in events)
|
||||
@ -315,6 +372,10 @@ namespace BTCPayServer.HostedServices
|
||||
{
|
||||
await foreach (var o in _Channel.Reader.ReadAllAsync())
|
||||
{
|
||||
if (o is TopUpRequest topUp)
|
||||
{
|
||||
await HandleTopUp(topUp);
|
||||
}
|
||||
if (o is PayoutRequest req)
|
||||
{
|
||||
await HandleCreatePayout(req);
|
||||
@ -349,10 +410,40 @@ namespace BTCPayServer.HostedServices
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleTopUp(TopUpRequest topUp)
|
||||
{
|
||||
var pp = await this.GetPullPayment(topUp.PullPaymentId, false);
|
||||
using var ctx = _dbContextFactory.CreateContext();
|
||||
|
||||
var payout = new Data.PayoutData()
|
||||
{
|
||||
Id = Encoders.Base58.EncodeData(RandomUtils.GetBytes(20)),
|
||||
Date = DateTimeOffset.UtcNow,
|
||||
State = PayoutState.Completed,
|
||||
PullPaymentDataId = pp.Id,
|
||||
PaymentMethodId = new PaymentMethodId("BTC", PaymentTypes.LightningLike).ToString(),
|
||||
Destination = null,
|
||||
StoreDataId = pp.StoreId
|
||||
};
|
||||
var rate = topUp.InvoiceEntity.Rates["BTC"];
|
||||
var cryptoAmount = Math.Round(topUp.InvoiceEntity.PaidAmount.Net / rate, 11);
|
||||
|
||||
var payoutBlob = new PayoutBlob()
|
||||
{
|
||||
CryptoAmount = -cryptoAmount,
|
||||
Amount = -topUp.InvoiceEntity.PaidAmount.Net,
|
||||
Destination = null,
|
||||
Metadata = new JObject(),
|
||||
};
|
||||
payout.SetBlob(payoutBlob, _jsonSerializerSettings);
|
||||
await ctx.Payouts.AddAsync(payout);
|
||||
await ctx.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public bool SupportsLNURL(PullPaymentBlob blob)
|
||||
{
|
||||
var pms = blob.SupportedPaymentMethods.FirstOrDefault(id =>
|
||||
id.PaymentType == LightningPaymentType.Instance &&
|
||||
var pms = blob.SupportedPaymentMethods.FirstOrDefault(id =>
|
||||
id.PaymentType == LightningPaymentType.Instance &&
|
||||
_networkProvider.DefaultNetwork.CryptoCode == id.CryptoCode);
|
||||
return pms is not null && _lnurlSupportedCurrencies.Contains(blob.Currency);
|
||||
}
|
||||
@ -609,7 +700,7 @@ namespace BTCPayServer.HostedServices
|
||||
{
|
||||
Amount = claimed,
|
||||
Destination = req.ClaimRequest.Destination.ToString(),
|
||||
Metadata = req.ClaimRequest.Metadata?? new JObject(),
|
||||
Metadata = req.ClaimRequest.Metadata ?? new JObject(),
|
||||
};
|
||||
payout.SetBlob(payoutBlob, _jsonSerializerSettings);
|
||||
await ctx.Payouts.AddAsync(payout);
|
||||
@ -802,6 +893,10 @@ namespace BTCPayServer.HostedServices
|
||||
return time;
|
||||
}
|
||||
|
||||
public static string GetInternalTag(string id)
|
||||
{
|
||||
return $"PULLPAY#{id}";
|
||||
}
|
||||
|
||||
class InternalPayoutPaidRequest
|
||||
{
|
||||
@ -856,25 +951,25 @@ namespace BTCPayServer.HostedServices
|
||||
{
|
||||
null when destination.Amount is null && ppCurrency is null => ("Amount is not specified in destination or payout request", null),
|
||||
null when destination.Amount is null => (null, null),
|
||||
null when destination.Amount != null => (null,destination.Amount),
|
||||
not null when destination.Amount is null => (null,amount),
|
||||
null when destination.Amount != null => (null, destination.Amount),
|
||||
not null when destination.Amount is null => (null, amount),
|
||||
not null when destination.Amount != null && amount != destination.Amount &&
|
||||
destination.IsExplicitAmountMinimum &&
|
||||
payoutCurrency == "BTC" && ppCurrency == "SATS" &&
|
||||
new Money(amount.Value, MoneyUnit.Satoshi).ToUnit(MoneyUnit.BTC) < destination.Amount =>
|
||||
($"Amount is implied in both destination ({destination.Amount}) and payout request ({amount}), but the payout request amount is less than the destination amount",null),
|
||||
($"Amount is implied in both destination ({destination.Amount}) and payout request ({amount}), but the payout request amount is less than the destination amount", null),
|
||||
not null when destination.Amount != null && amount != destination.Amount &&
|
||||
destination.IsExplicitAmountMinimum &&
|
||||
!(payoutCurrency == "BTC" && ppCurrency == "SATS") &&
|
||||
amount < destination.Amount =>
|
||||
($"Amount is implied in both destination ({destination.Amount}) and payout request ({amount}), but the payout request amount is less than the destination amount",null),
|
||||
($"Amount is implied in both destination ({destination.Amount}) and payout request ({amount}), but the payout request amount is less than the destination amount", null),
|
||||
not null when destination.Amount != null && amount != destination.Amount &&
|
||||
!destination.IsExplicitAmountMinimum =>
|
||||
($"Amount is implied in destination ({destination.Amount}) that does not match the payout amount provided {amount})", null),
|
||||
_ => (null, amount)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
public static string GetErrorMessage(ClaimResult result)
|
||||
{
|
||||
switch (result)
|
||||
|
@ -6,6 +6,8 @@ using BTCPayServer.Events;
|
||||
using BTCPayServer.Logging;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Mails;
|
||||
using BTCPayServer.Services.Notifications;
|
||||
using BTCPayServer.Services.Notifications.Blobs;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
@ -19,20 +21,27 @@ namespace BTCPayServer.HostedServices
|
||||
{
|
||||
private readonly UserManager<ApplicationUser> _userManager;
|
||||
private readonly EmailSenderFactory _emailSenderFactory;
|
||||
private readonly NotificationSender _notificationSender;
|
||||
private readonly LinkGenerator _generator;
|
||||
|
||||
|
||||
public UserEventHostedService(EventAggregator eventAggregator, UserManager<ApplicationUser> userManager,
|
||||
EmailSenderFactory emailSenderFactory, LinkGenerator generator, Logs logs) : base(eventAggregator, logs)
|
||||
public UserEventHostedService(
|
||||
EventAggregator eventAggregator,
|
||||
UserManager<ApplicationUser> userManager,
|
||||
EmailSenderFactory emailSenderFactory,
|
||||
NotificationSender notificationSender,
|
||||
LinkGenerator generator,
|
||||
Logs logs) : base(eventAggregator, logs)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_emailSenderFactory = emailSenderFactory;
|
||||
_notificationSender = notificationSender;
|
||||
_generator = generator;
|
||||
}
|
||||
|
||||
protected override void SubscribeToEvents()
|
||||
{
|
||||
Subscribe<UserRegisteredEvent>();
|
||||
Subscribe<UserApprovedEvent>();
|
||||
Subscribe<UserPasswordResetRequestedEvent>();
|
||||
}
|
||||
|
||||
@ -40,30 +49,39 @@ namespace BTCPayServer.HostedServices
|
||||
{
|
||||
string code;
|
||||
string callbackUrl;
|
||||
Uri uri;
|
||||
HostString host;
|
||||
ApplicationUser user;
|
||||
MailboxAddress address;
|
||||
IEmailSender emailSender;
|
||||
UserPasswordResetRequestedEvent userPasswordResetRequestedEvent;
|
||||
switch (evt)
|
||||
{
|
||||
case UserRegisteredEvent userRegisteredEvent:
|
||||
user = userRegisteredEvent.User;
|
||||
Logs.PayServer.LogInformation(
|
||||
$"A new user just registered {userRegisteredEvent.User.Email} {(userRegisteredEvent.Admin ? "(admin)" : "")}");
|
||||
if (!userRegisteredEvent.User.EmailConfirmed && userRegisteredEvent.User.RequiresEmailConfirmation)
|
||||
$"A new user just registered {user.Email} {(userRegisteredEvent.Admin ? "(admin)" : "")}");
|
||||
if (user.RequiresApproval && !user.Approved)
|
||||
{
|
||||
code = await _userManager.GenerateEmailConfirmationTokenAsync(userRegisteredEvent.User);
|
||||
callbackUrl = _generator.EmailConfirmationLink(userRegisteredEvent.User.Id, code,
|
||||
userRegisteredEvent.RequestUri.Scheme,
|
||||
new HostString(userRegisteredEvent.RequestUri.Host, userRegisteredEvent.RequestUri.Port),
|
||||
userRegisteredEvent.RequestUri.PathAndQuery);
|
||||
await _notificationSender.SendNotification(new AdminScope(), new NewUserRequiresApprovalNotification(user));
|
||||
}
|
||||
if (!user.EmailConfirmed && user.RequiresEmailConfirmation)
|
||||
{
|
||||
uri = userRegisteredEvent.RequestUri;
|
||||
host = new HostString(uri.Host, uri.Port);
|
||||
code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
|
||||
callbackUrl = _generator.EmailConfirmationLink(user.Id, code, uri.Scheme, host, uri.PathAndQuery);
|
||||
userRegisteredEvent.CallbackUrlGenerated?.SetResult(new Uri(callbackUrl));
|
||||
address = userRegisteredEvent.User.GetMailboxAddress();
|
||||
(await _emailSenderFactory.GetEmailSender()).SendEmailConfirmation(address, callbackUrl);
|
||||
address = user.GetMailboxAddress();
|
||||
emailSender = await _emailSenderFactory.GetEmailSender();
|
||||
emailSender.SendEmailConfirmation(address, callbackUrl);
|
||||
}
|
||||
else if (!await _userManager.HasPasswordAsync(userRegisteredEvent.User))
|
||||
{
|
||||
userPasswordResetRequestedEvent = new UserPasswordResetRequestedEvent()
|
||||
userPasswordResetRequestedEvent = new UserPasswordResetRequestedEvent
|
||||
{
|
||||
CallbackUrlGenerated = userRegisteredEvent.CallbackUrlGenerated,
|
||||
User = userRegisteredEvent.User,
|
||||
User = user,
|
||||
RequestUri = userRegisteredEvent.RequestUri
|
||||
};
|
||||
goto passwordSetter;
|
||||
@ -72,22 +90,33 @@ namespace BTCPayServer.HostedServices
|
||||
{
|
||||
userRegisteredEvent.CallbackUrlGenerated?.SetResult(null);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case UserApprovedEvent userApprovedEvent:
|
||||
if (userApprovedEvent.Approved)
|
||||
{
|
||||
uri = userApprovedEvent.RequestUri;
|
||||
host = new HostString(uri.Host, uri.Port);
|
||||
address = userApprovedEvent.User.GetMailboxAddress();
|
||||
callbackUrl = _generator.LoginLink(uri.Scheme, host, uri.PathAndQuery);
|
||||
emailSender = await _emailSenderFactory.GetEmailSender();
|
||||
emailSender.SendApprovalConfirmation(address, callbackUrl);
|
||||
}
|
||||
break;
|
||||
|
||||
case UserPasswordResetRequestedEvent userPasswordResetRequestedEvent2:
|
||||
userPasswordResetRequestedEvent = userPasswordResetRequestedEvent2;
|
||||
passwordSetter:
|
||||
code = await _userManager.GeneratePasswordResetTokenAsync(userPasswordResetRequestedEvent.User);
|
||||
var newPassword = await _userManager.HasPasswordAsync(userPasswordResetRequestedEvent.User);
|
||||
callbackUrl = _generator.ResetPasswordCallbackLink(userPasswordResetRequestedEvent.User.Id, code,
|
||||
userPasswordResetRequestedEvent.RequestUri.Scheme,
|
||||
new HostString(userPasswordResetRequestedEvent.RequestUri.Host,
|
||||
userPasswordResetRequestedEvent.RequestUri.Port),
|
||||
userPasswordResetRequestedEvent.RequestUri.PathAndQuery);
|
||||
uri = userPasswordResetRequestedEvent.RequestUri;
|
||||
host = new HostString(uri.Host, uri.Port);
|
||||
user = userPasswordResetRequestedEvent.User;
|
||||
code = await _userManager.GeneratePasswordResetTokenAsync(user);
|
||||
var newPassword = await _userManager.HasPasswordAsync(user);
|
||||
callbackUrl = _generator.ResetPasswordCallbackLink(user.Id, code, uri.Scheme, host, uri.PathAndQuery);
|
||||
userPasswordResetRequestedEvent.CallbackUrlGenerated?.SetResult(new Uri(callbackUrl));
|
||||
address = userPasswordResetRequestedEvent.User.GetMailboxAddress();
|
||||
(await _emailSenderFactory.GetEmailSender())
|
||||
.SendSetPasswordConfirmation(address, callbackUrl, newPassword);
|
||||
address = user.GetMailboxAddress();
|
||||
emailSender = await _emailSenderFactory.GetEmailSender();
|
||||
emailSender.SendSetPasswordConfirmation(address, callbackUrl, newPassword);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -436,8 +436,8 @@ namespace BTCPayServer.Hosting
|
||||
services.AddScoped<IAuthorizationHandler, BitpayAuthorizationHandler>();
|
||||
|
||||
services.AddSingleton<INotificationHandler, NewVersionNotification.Handler>();
|
||||
services.AddSingleton<INotificationHandler, NewUserRequiresApprovalNotification.Handler>();
|
||||
services.AddSingleton<INotificationHandler, PluginUpdateNotification.Handler>();
|
||||
|
||||
services.AddSingleton<INotificationHandler, InvoiceEventNotification.Handler>();
|
||||
services.AddSingleton<INotificationHandler, PayoutNotification.Handler>();
|
||||
services.AddSingleton<INotificationHandler, ExternalPayoutTransactionNotification.Handler>();
|
||||
@ -551,6 +551,7 @@ namespace BTCPayServer.Hosting
|
||||
services.AddRateProvider<ByllsRateProvider>();
|
||||
services.AddRateProvider<BudaRateProvider>();
|
||||
services.AddRateProvider<BitbankRateProvider>();
|
||||
services.AddRateProvider<BitnobRateProvider>();
|
||||
services.AddRateProvider<BitpayRateProvider>();
|
||||
services.AddRateProvider<RipioExchangeProvider>();
|
||||
services.AddRateProvider<CryptoMarketExchangeRateProvider>();
|
||||
|
@ -6,25 +6,27 @@ namespace BTCPayServer.Models.ServerViewModels
|
||||
{
|
||||
public class EmailsViewModel
|
||||
{
|
||||
public EmailSettings Settings { get; set; }
|
||||
public EmailSettings FallbackSettings { get; set; }
|
||||
public bool PasswordSet { get; set; }
|
||||
|
||||
[MailboxAddress]
|
||||
[Display(Name = "Test Email")]
|
||||
public string TestEmail { get; set; }
|
||||
|
||||
public EmailsViewModel()
|
||||
{
|
||||
|
||||
}
|
||||
public EmailsViewModel(EmailSettings settings)
|
||||
|
||||
public EmailsViewModel(EmailSettings settings, EmailSettings fallbackSettings = null)
|
||||
{
|
||||
Settings = settings;
|
||||
FallbackSettings = fallbackSettings;
|
||||
PasswordSet = !string.IsNullOrEmpty(settings?.Password);
|
||||
}
|
||||
public EmailSettings Settings
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public bool PasswordSet { get; set; }
|
||||
[MailboxAddressAttribute]
|
||||
[Display(Name = "Test Email")]
|
||||
public string TestEmail
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public bool IsSetup() => Settings?.IsComplete() is true;
|
||||
public bool IsFallbackSetup() => FallbackSettings?.IsComplete() is true;
|
||||
public bool UsesFallback() => IsFallbackSetup() && Settings == FallbackSettings;
|
||||
}
|
||||
}
|
||||
|
@ -11,7 +11,8 @@ namespace BTCPayServer.Models.ServerViewModels
|
||||
public string Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string Email { get; set; }
|
||||
public bool Verified { get; set; }
|
||||
public bool? EmailConfirmed { get; set; }
|
||||
public bool? Approved { get; set; }
|
||||
public bool Disabled { get; set; }
|
||||
public bool IsAdmin { get; set; }
|
||||
public DateTimeOffset? Created { get; set; }
|
||||
|
@ -71,6 +71,9 @@ namespace BTCPayServer.Models
|
||||
|
||||
public PaymentMethodId[] PaymentMethods { get; set; }
|
||||
|
||||
public string SetupDeepLink { get; set; }
|
||||
public string ResetDeepLink { get; set; }
|
||||
|
||||
public string HubPath { get; set; }
|
||||
public string ResetIn { get; set; }
|
||||
public string Email { get; set; }
|
||||
|
@ -55,11 +55,8 @@ namespace BTCPayServer.Models.WalletViewModels
|
||||
public int CryptoDivisibility { get; set; }
|
||||
public string Fiat { get; set; }
|
||||
public string RateError { get; set; }
|
||||
public bool SupportRBF { get; set; }
|
||||
[Display(Name = "Always include non-witness UTXO if available")]
|
||||
public bool AlwaysIncludeNonWitnessUTXO { get; set; }
|
||||
[Display(Name = "Allow fee increase (RBF)")]
|
||||
public ThreeStateBool AllowFeeBump { get; set; }
|
||||
|
||||
public bool NBXSeedAvailable { get; set; }
|
||||
[Display(Name = "PayJoin BIP21")]
|
||||
|
@ -88,11 +88,11 @@ public class LightningAutomatedPayoutProcessor : BaseAutomatedPayoutProcessor<Li
|
||||
{
|
||||
continue;
|
||||
}
|
||||
failed = await TrypayBolt(client, blob, payoutData,
|
||||
failed = !await TrypayBolt(client, blob, payoutData,
|
||||
lnurlResult.Item1);
|
||||
break;
|
||||
case BoltInvoiceClaimDestination item1:
|
||||
failed = await TrypayBolt(client, blob, payoutData, item1.PaymentRequest);
|
||||
failed = !await TrypayBolt(client, blob, payoutData, item1.PaymentRequest);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,35 @@
|
||||
#nullable enable
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Contracts;
|
||||
using BTCPayServer.Abstractions.Models;
|
||||
using BTCPayServer.Abstractions.Services;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Configuration;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Plugins.BoltcardFactory.Controllers;
|
||||
using BTCPayServer.Plugins.PointOfSale;
|
||||
using BTCPayServer.Plugins.PointOfSale.Controllers;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace BTCPayServer.Plugins.BoltcardBalance
|
||||
{
|
||||
public class BoltcardBalancePlugin : BaseBTCPayServerPlugin
|
||||
{
|
||||
public const string ViewsDirectory = "/Plugins/BoltcardBalance/Views";
|
||||
public const string AppType = "BoltcardBalance";
|
||||
|
||||
public override string Identifier => "BTCPayServer.Plugins.BoltcardBalance";
|
||||
public override string Name => "BoltcardBalance";
|
||||
public override string Description => "Add ability to check the history and balance of a Boltcard";
|
||||
|
||||
public override void Execute(IServiceCollection services)
|
||||
{
|
||||
services.AddSingleton<IUIExtension>(new UIExtension($"{ViewsDirectory}/NavExtension.cshtml", "header-nav"));
|
||||
base.Execute(services);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,111 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Reflection.Metadata;
|
||||
using System.Threading.Tasks;
|
||||
using AngleSharp.Dom;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Plugins.BoltcardBalance.ViewModels;
|
||||
using BTCPayServer.Plugins.BoltcardFactory;
|
||||
using BTCPayServer.Services;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace BTCPayServer.Plugins.BoltcardBalance.Controllers
|
||||
{
|
||||
[AutoValidateAntiforgeryToken]
|
||||
public class UIBoltcardBalanceController : Controller
|
||||
{
|
||||
private readonly ApplicationDbContextFactory _dbContextFactory;
|
||||
private readonly SettingsRepository _settingsRepository;
|
||||
private readonly BTCPayServerEnvironment _env;
|
||||
private readonly BTCPayNetworkJsonSerializerSettings _serializerSettings;
|
||||
|
||||
public UIBoltcardBalanceController(
|
||||
ApplicationDbContextFactory dbContextFactory,
|
||||
SettingsRepository settingsRepository,
|
||||
BTCPayServerEnvironment env,
|
||||
BTCPayNetworkJsonSerializerSettings serializerSettings)
|
||||
{
|
||||
_dbContextFactory = dbContextFactory;
|
||||
_settingsRepository = settingsRepository;
|
||||
_env = env;
|
||||
_serializerSettings = serializerSettings;
|
||||
}
|
||||
[HttpGet("boltcards/balance")]
|
||||
public async Task<IActionResult> ScanCard([FromQuery] string p = null, [FromQuery] string c = null)
|
||||
{
|
||||
if (p is null || c is null)
|
||||
{
|
||||
return View($"{BoltcardBalancePlugin.ViewsDirectory}/ScanCard.cshtml");
|
||||
}
|
||||
|
||||
//return View($"{BoltcardBalancePlugin.ViewsDirectory}/BalanceView.cshtml", new BalanceViewModel()
|
||||
//{
|
||||
// AmountDue = 10000m,
|
||||
// Currency = "SATS",
|
||||
// Transactions = [new() { Date = DateTimeOffset.UtcNow, Balance = -3.0m }, new() { Date = DateTimeOffset.UtcNow, Balance = -5.0m }]
|
||||
//});
|
||||
|
||||
var issuerKey = await _settingsRepository.GetIssuerKey(_env);
|
||||
var boltData = issuerKey.TryDecrypt(p);
|
||||
if (boltData?.Uid is null)
|
||||
return NotFound();
|
||||
var id = issuerKey.GetId(boltData.Uid);
|
||||
var registration = await _dbContextFactory.GetBoltcardRegistration(issuerKey, boltData, true);
|
||||
if (registration is null)
|
||||
return NotFound();
|
||||
return await GetBalanceView(registration.PullPaymentId, p);
|
||||
}
|
||||
[NonAction]
|
||||
public async Task<IActionResult> GetBalanceView(string ppId, string p)
|
||||
{
|
||||
using var ctx = _dbContextFactory.CreateContext();
|
||||
var pp = await ctx.PullPayments.FindAsync(ppId);
|
||||
if (pp is null)
|
||||
return NotFound();
|
||||
var blob = pp.GetBlob();
|
||||
|
||||
var payouts = (await ctx.Payouts.GetPayoutInPeriod(pp)
|
||||
.OrderByDescending(o => o.Date)
|
||||
.ToListAsync())
|
||||
.Select(o => new
|
||||
{
|
||||
Entity = o,
|
||||
Blob = o.GetBlob(_serializerSettings)
|
||||
});
|
||||
|
||||
|
||||
var totalPaid = payouts.Where(p => p.Entity.State != PayoutState.Cancelled).Select(p => p.Blob.Amount).Sum();
|
||||
|
||||
var bech32LNUrl = new Uri(Url.Action(nameof(UIBoltcardController.GetPayRequest), "UIBoltcard", new { p }, Request.Scheme), UriKind.Absolute);
|
||||
bech32LNUrl = LNURL.LNURL.EncodeUri(bech32LNUrl, "payRequest", true);
|
||||
var vm = new BalanceViewModel()
|
||||
{
|
||||
Currency = blob.Currency,
|
||||
AmountDue = blob.Limit - totalPaid,
|
||||
LNUrlBech32 = bech32LNUrl.AbsoluteUri,
|
||||
LNUrlPay = Url.Action(nameof(UIBoltcardController.GetPayRequest), "UIBoltcard", new { p }, "lnurlp")
|
||||
};
|
||||
foreach (var payout in payouts)
|
||||
{
|
||||
vm.Transactions.Add(new BalanceViewModel.Transaction()
|
||||
{
|
||||
Date = payout.Entity.Date,
|
||||
Balance = -payout.Blob.Amount,
|
||||
Status = payout.Entity.State
|
||||
});
|
||||
}
|
||||
vm.Transactions.Add(new BalanceViewModel.Transaction()
|
||||
{
|
||||
Date = pp.StartDate,
|
||||
Balance = blob.Limit,
|
||||
Status = PayoutState.Completed
|
||||
});
|
||||
|
||||
return View($"{BoltcardBalancePlugin.ViewsDirectory}/BalanceView.cshtml", vm);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using BTCPayServer.Client.Models;
|
||||
|
||||
namespace BTCPayServer.Plugins.BoltcardBalance.ViewModels
|
||||
{
|
||||
public class BalanceViewModel
|
||||
{
|
||||
public class Transaction
|
||||
{
|
||||
public DateTimeOffset Date { get; set; }
|
||||
public bool Positive => Balance >= 0;
|
||||
public decimal Balance { get; set; }
|
||||
public PayoutState Status { get; internal set; }
|
||||
}
|
||||
public string Currency { get; set; }
|
||||
public decimal AmountDue { get; set; }
|
||||
public List<Transaction> Transactions { get; set; } = new List<Transaction>();
|
||||
public string LNUrlBech32 { get; set; }
|
||||
public string LNUrlPay { get; set; }
|
||||
|
||||
}
|
||||
}
|
@ -0,0 +1,73 @@
|
||||
@using BTCPayServer.Plugins.BoltcardBalance.ViewModels
|
||||
@using BTCPayServer.Services
|
||||
@inject DisplayFormatter DisplayFormatter
|
||||
|
||||
@model BalanceViewModel
|
||||
|
||||
@{
|
||||
Layout = null;
|
||||
}
|
||||
|
||||
<div class="col col-12 col-lg-12 mb-4">
|
||||
<div class="bg-tile h-100 m-0 p-3 p-sm-5 rounded">
|
||||
<nav id="wizard-navbar">
|
||||
@if (this.ViewData["NoCancelWizard"] is not true)
|
||||
{
|
||||
<button type="button" class="btn btn-secondary only-for-js mt-4" id="lnurlwithdraw-button">
|
||||
<span class="fa fa-qrcode fa-2x" title="Deposit"></span>
|
||||
</button>
|
||||
<a href="#" id="CancelWizard" class="cancel mt-4">
|
||||
<vc:icon symbol="close" />
|
||||
</a>
|
||||
}
|
||||
</nav>
|
||||
<div class="d-flex justify-content-center">
|
||||
<div class="d-flex flex-column justify-content-center align-items-center">
|
||||
<dl class="mb-0 mt-md-4">
|
||||
<div class="d-flex d-print-inline-block flex-column mb-4">
|
||||
<dt class="h4 fw-semibold text-nowrap text-primary text-print-default order-2 order-sm-1 mb-1">@DisplayFormatter.Currency(Model.AmountDue, Model.Currency)</dt>
|
||||
</div>
|
||||
</dl>
|
||||
@* <div class="lnurl-pay d-none">
|
||||
<vc:qr-code data="@Model.LNUrlPay" />
|
||||
</div>
|
||||
<div class="lnurl-pay d-flex gap-3 mt-3 mt-sm-0 d-none">
|
||||
<a class="btn btn-primary" target="_blank" href="@Model.LNUrlPay">Deposit from Wallet... (LNURLPay)</a>
|
||||
</div> *@
|
||||
<div class="lnurl-pay d-none">
|
||||
<vc:qr-code data="@Model.LNUrlBech32" />
|
||||
</div>
|
||||
<div class="lnurl-pay d-flex gap-3 mt-3 mt-sm-0 d-none">
|
||||
<a class="btn btn-primary" target="_blank" href="@Model.LNUrlBech32">Deposit from Wallet...</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (Model.Transactions.Any())
|
||||
{
|
||||
<div class="col col-12 col-lg-12 mb-4">
|
||||
<div class="bg-tile h-100 m-0 p-3 p-sm-5 rounded table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="date-col">Date</th>
|
||||
<th class="amount-col">Amount</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var tx in Model.Transactions)
|
||||
{
|
||||
<tr>
|
||||
<td class="date-col">@tx.Date.ToBrowserDate(ViewsRazor.DateDisplayFormat.Relative)</td>
|
||||
<td class="amount-col">
|
||||
<span data-sensitive class="text-@(tx.Positive ? "success" : "danger")">@DisplayFormatter.Currency(tx.Balance, Model.Currency, DisplayFormatter.CurrencyFormat.Code)</span>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
@using BTCPayServer.Client
|
||||
@using BTCPayServer.Plugins.BoltcardFactory
|
||||
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@using BTCPayServer.Views.Apps
|
||||
@using BTCPayServer.Abstractions.Extensions
|
||||
@using BTCPayServer.Abstractions.TagHelpers
|
||||
|
||||
|
||||
<li class="nav-item">
|
||||
<a asp-area="" asp-controller="UIBoltcardBalance" asp-action="ScanCard" class="nav-link">
|
||||
<vc:icon symbol="pay-button" />
|
||||
<span>Boltcard Balance</span>
|
||||
</a>
|
||||
</li>
|
161
BTCPayServer/Plugins/BoltcardBalance/Views/ScanCard.cshtml
Normal file
161
BTCPayServer/Plugins/BoltcardBalance/Views/ScanCard.cshtml
Normal file
@ -0,0 +1,161 @@
|
||||
@{
|
||||
ViewData["Title"] = "Boltcard Balances";
|
||||
ViewData["ShowFooter"] = false;
|
||||
Layout = "/Views/Shared/_LayoutWizard.cshtml";
|
||||
}
|
||||
|
||||
@section PageHeadContent
|
||||
{
|
||||
<style>
|
||||
.amount-col {
|
||||
text-align: right;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
}
|
||||
|
||||
<header class="text-center">
|
||||
<h1>Consult balance</h1>
|
||||
<p class="lead text-secondary mt-3" id="explanation">Scan your card for consulting the balance</p>
|
||||
</header>
|
||||
|
||||
<div id="body" class="my-4">
|
||||
<div id="actions" class="d-flex align-items-center justify-content-center d-none">
|
||||
<div class="d-flex gap-3 mt-3 mt-sm-0">
|
||||
<a id="start-scan-btn" class="btn btn-primary" href="#">Ask permission...</a>
|
||||
</div>
|
||||
</div>
|
||||
<div id="qr" class="d-flex flex-column align-items-center justify-content-center d-none">
|
||||
<div class="d-inline-flex flex-column" style="width:256px">
|
||||
<div class="qr-container mb-2">
|
||||
<vc:qr-code data="@Context.Request.GetCurrentUrl()" />
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-secondary">NFC not supported in this device</p>
|
||||
</div>
|
||||
<div id="scanning-btn" class="d-flex align-items-center justify-content-center d-none">
|
||||
<div class="d-flex gap-3 mt-3 mt-sm-0">
|
||||
<a id="scanning-btn-link" class="action-button" style="font-size: 50px;" ></a>
|
||||
</div>
|
||||
</div>
|
||||
<div id="balance" class="row">
|
||||
<div id="balance-table"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
var permissionGranted = false;
|
||||
var ndef = null;
|
||||
var abortController = null;
|
||||
function delay(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
async function showBalance(lnurlw) {
|
||||
setState("Submitting");
|
||||
await delay(1000);
|
||||
var url = window.location.href.replace("#", "");
|
||||
url = url.split("?")[0] + "?" + lnurlw.split("?")[1];
|
||||
// url = "https://testnet.demo.btcpayserver.org/boltcards/balance?p=...&c=..."
|
||||
|
||||
var xhttp = new XMLHttpRequest();
|
||||
xhttp.onreadystatechange = function () {
|
||||
if (this.readyState == 4 && this.status == 200 && this.responseText) {
|
||||
document.getElementById("balance-table").innerHTML = this.responseText;
|
||||
document.getElementById("CancelWizard").addEventListener("click", function (e) {
|
||||
e.preventDefault();
|
||||
setState("WaitingForCard");
|
||||
document.getElementById("balance-table").innerHTML = "";
|
||||
});
|
||||
|
||||
document.getElementById("lnurlwithdraw-button").addEventListener("click", function (e) {
|
||||
var el = document.getElementsByClassName("lnurl-pay");
|
||||
for (var i = 0; i < el.length; i++) {
|
||||
if (el[i].classList.contains("d-none"))
|
||||
el[i].classList.remove("d-none");
|
||||
else
|
||||
el[i].classList.add("d-none");
|
||||
}
|
||||
});
|
||||
|
||||
setState("ShowBalance");
|
||||
}
|
||||
else {
|
||||
setState("WaitingForCard");
|
||||
}
|
||||
};
|
||||
xhttp.open('GET', url, true);
|
||||
xhttp.send(new FormData());
|
||||
}
|
||||
|
||||
async function startScan() {
|
||||
if (!('NDEFReader' in window)) {
|
||||
return;
|
||||
}
|
||||
ndef = new NDEFReader();
|
||||
abortController = new AbortController();
|
||||
abortController.signal.onabort = () => setState("WaitingForCard");
|
||||
|
||||
await ndef.scan({ signal: abortController.signal })
|
||||
setState("WaitingForCard");
|
||||
ndef.onreading = async ({ message }) => {
|
||||
const record = message.records[0];
|
||||
const textDecoder = new TextDecoder('utf-8');
|
||||
const decoded = textDecoder.decode(record.data);
|
||||
await showBalance(decoded);
|
||||
};
|
||||
}
|
||||
|
||||
function setState(state)
|
||||
{
|
||||
document.getElementById("actions").classList.add("d-none");
|
||||
document.getElementById("qr").classList.add("d-none");
|
||||
document.getElementById("scanning-btn").classList.add("d-none");
|
||||
document.getElementById("balance").classList.add("d-none");
|
||||
|
||||
if (state === "NFCNotSupported")
|
||||
{
|
||||
document.getElementById("qr").classList.remove("d-none");
|
||||
}
|
||||
else if (state === "WaitingForPermission")
|
||||
{
|
||||
|
||||
document.getElementById("actions").classList.remove("d-none");
|
||||
}
|
||||
else if (state === "WaitingForCard")
|
||||
{
|
||||
document.getElementById("scanning-btn").classList.remove("d-none");
|
||||
document.getElementById("scanning-btn-link").innerHTML = "<i class=\"fa fa-wifi\"></i>";
|
||||
}
|
||||
else if (state == "Submitting")
|
||||
{
|
||||
document.getElementById("scanning-btn").classList.remove("d-none");
|
||||
document.getElementById("scanning-btn-link").innerHTML = "<i class=\"fa fa-spinner\"></i>"
|
||||
}
|
||||
else if (state == "ShowBalance") {
|
||||
document.getElementById("scanning-btn").classList.remove("d-none");
|
||||
document.getElementById("scanning-btn-link").innerHTML = "<i class=\"fa fa-wifi\"></i>";
|
||||
document.getElementById("balance").classList.remove("d-none");
|
||||
}
|
||||
}
|
||||
document.addEventListener("DOMContentLoaded", async () => {
|
||||
var nfcSupported = 'NDEFReader' in window;
|
||||
if (!nfcSupported) {
|
||||
setState("NFCNotSupported");
|
||||
}
|
||||
else {
|
||||
setState("WaitingForPermission");
|
||||
var granted = (await navigator.permissions.query({ name: 'nfc' })).state === 'granted';
|
||||
if (granted)
|
||||
{
|
||||
setState("WaitingForCard");
|
||||
startScan();
|
||||
}
|
||||
}
|
||||
delegate('click', "#start-scan-btn", startScan);
|
||||
//showBalance("lnurl://ewfw?p=test&c=test");
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
|
@ -0,0 +1,74 @@
|
||||
#nullable enable
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Contracts;
|
||||
using BTCPayServer.Abstractions.Models;
|
||||
using BTCPayServer.Abstractions.Services;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Configuration;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Plugins.BoltcardFactory.Controllers;
|
||||
using BTCPayServer.Plugins.PointOfSale;
|
||||
using BTCPayServer.Plugins.PointOfSale.Controllers;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace BTCPayServer.Plugins.BoltcardFactory
|
||||
{
|
||||
public class BoltcardFactoryPlugin : BaseBTCPayServerPlugin
|
||||
{
|
||||
public const string ViewsDirectory = "/Plugins/BoltcardFactory/Views";
|
||||
public const string AppType = "BoltcardFactory";
|
||||
|
||||
public override string Identifier => "BTCPayServer.Plugins.BoltcardFactory";
|
||||
public override string Name => "BoltcardFactory";
|
||||
public override string Description => "Allow the creation of a consequential number of Boltcards in an efficient way";
|
||||
|
||||
|
||||
internal class BoltcardFactoryAppType : AppBaseType
|
||||
{
|
||||
private readonly LinkGenerator _linkGenerator;
|
||||
private readonly IOptions<BTCPayServerOptions> _btcPayServerOptions;
|
||||
|
||||
public BoltcardFactoryAppType(
|
||||
LinkGenerator linkGenerator,
|
||||
IOptions<BTCPayServerOptions> btcPayServerOptions)
|
||||
{
|
||||
Type = AppType;
|
||||
Description = "Boltcard Factories";
|
||||
_linkGenerator = linkGenerator;
|
||||
_btcPayServerOptions = btcPayServerOptions;
|
||||
}
|
||||
public override Task<string> ConfigureLink(AppData app)
|
||||
{
|
||||
return Task.FromResult(_linkGenerator.GetPathByAction(nameof(UIBoltcardFactoryController.UpdateBoltcardFactory),
|
||||
"UIBoltcardFactory", new { appId = app.Id }, _btcPayServerOptions.Value.RootPath)!);
|
||||
}
|
||||
|
||||
public override Task<object?> GetInfo(AppData appData)
|
||||
{
|
||||
return Task.FromResult<object?>(null);
|
||||
}
|
||||
|
||||
public override Task SetDefaultSettings(AppData appData, string defaultCurrency)
|
||||
{
|
||||
appData.SetSettings(new CreatePullPaymentRequest());
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public override Task<string> ViewLink(AppData app)
|
||||
{
|
||||
return Task.FromResult(_linkGenerator.GetPathByAction(nameof(UIBoltcardFactoryController.ViewBoltcardFactory),
|
||||
"UIBoltcardFactory", new { appId = app.Id }, _btcPayServerOptions.Value.RootPath)!);
|
||||
}
|
||||
}
|
||||
public override void Execute(IServiceCollection services)
|
||||
{
|
||||
services.AddSingleton<IUIExtension>(new UIExtension($"{ViewsDirectory}/NavExtension.cshtml", "header-nav"));
|
||||
services.AddSingleton<AppBaseType, BoltcardFactoryAppType>();
|
||||
base.Execute(services);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,150 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.NTag424;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NBitcoin.DataEncoders;
|
||||
|
||||
namespace BTCPayServer.Plugins.BoltcardFactory.Controllers
|
||||
{
|
||||
[ApiController]
|
||||
[Route("apps")]
|
||||
public class APIBoltcardFactoryController : ControllerBase
|
||||
{
|
||||
private readonly ILogger<APIBoltcardFactoryController> _logger;
|
||||
private readonly AppService _appService;
|
||||
private readonly SettingsRepository _settingsRepository;
|
||||
private readonly BTCPayServerEnvironment _env;
|
||||
private readonly ApplicationDbContextFactory _dbContextFactory;
|
||||
private readonly PullPaymentHostedService _ppService;
|
||||
|
||||
public APIBoltcardFactoryController(
|
||||
ILogger<APIBoltcardFactoryController> logger,
|
||||
AppService appService,
|
||||
SettingsRepository settingsRepository,
|
||||
BTCPayServerEnvironment env,
|
||||
ApplicationDbContextFactory dbContextFactory,
|
||||
PullPaymentHostedService ppService)
|
||||
{
|
||||
_logger = logger;
|
||||
_appService = appService;
|
||||
_settingsRepository = settingsRepository;
|
||||
_env = env;
|
||||
_dbContextFactory = dbContextFactory;
|
||||
_ppService = ppService;
|
||||
}
|
||||
[HttpPost("{appId}/boltcards")]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> RegisterBoltcard(string appId, RegisterBoltcardRequest? request, string? onExisting = null)
|
||||
{
|
||||
var app = await _appService.GetApp(appId, BoltcardFactoryPlugin.AppType);
|
||||
if (app is null)
|
||||
return NotFound();
|
||||
var issuerKey = await _settingsRepository.GetIssuerKey(_env);
|
||||
|
||||
// LNURLW is used by deeplinks
|
||||
if (request?.LNURLW is not null)
|
||||
{
|
||||
if (request.UID is not null)
|
||||
{
|
||||
_logger.LogInformation("You should pass either LNURLW or UID but not both");
|
||||
ModelState.AddModelError(nameof(request.LNURLW), "You should pass either LNURLW or UID but not both");
|
||||
return this.CreateValidationError(ModelState);
|
||||
}
|
||||
var p = ExtractP(request.LNURLW);
|
||||
if (p is null)
|
||||
{
|
||||
_logger.LogInformation("The LNURLW should contains a 'p=' parameter");
|
||||
ModelState.AddModelError(nameof(request.LNURLW), "The LNURLW should contains a 'p=' parameter");
|
||||
return this.CreateValidationError(ModelState);
|
||||
}
|
||||
if (issuerKey.TryDecrypt(p) is not BoltcardPICCData picc)
|
||||
{
|
||||
_logger.LogInformation("The LNURLW 'p=' parameter cannot be decrypted");
|
||||
ModelState.AddModelError(nameof(request.LNURLW), "The LNURLW 'p=' parameter cannot be decrypted");
|
||||
return this.CreateValidationError(ModelState);
|
||||
}
|
||||
request.UID = picc.Uid;
|
||||
}
|
||||
|
||||
if (request?.UID is null || request.UID.Length != 7)
|
||||
{
|
||||
_logger.LogInformation("The UID is required and should be 7 bytes");
|
||||
ModelState.AddModelError(nameof(request.UID), "The UID is required and should be 7 bytes");
|
||||
return this.CreateValidationError(ModelState);
|
||||
}
|
||||
|
||||
// Passing onExisting as a query parameter is used by deeplink
|
||||
request.OnExisting = onExisting switch
|
||||
{
|
||||
nameof(OnExistingBehavior.UpdateVersion) => OnExistingBehavior.UpdateVersion,
|
||||
nameof(OnExistingBehavior.KeepVersion) => OnExistingBehavior.KeepVersion,
|
||||
_ => request.OnExisting
|
||||
};
|
||||
|
||||
int version;
|
||||
string ppId;
|
||||
var registration = await _dbContextFactory.GetBoltcardRegistration(issuerKey, request.UID);
|
||||
|
||||
if (request.OnExisting == OnExistingBehavior.UpdateVersion)
|
||||
{
|
||||
var req = app.GetSettings<CreatePullPaymentRequest>();
|
||||
ppId = await _ppService.CreatePullPayment(app.StoreDataId, req);
|
||||
version = await _dbContextFactory.LinkBoltcardToPullPayment(ppId, issuerKey, request.UID, request.OnExisting);
|
||||
}
|
||||
// If it's a reset, do not create a new pull payment
|
||||
else
|
||||
{
|
||||
if (registration?.PullPaymentId is null)
|
||||
{
|
||||
_logger.LogInformation("This card isn't registered");
|
||||
ModelState.AddModelError(nameof(request.UID), "This card isn't registered");
|
||||
return this.CreateValidationError(ModelState);
|
||||
}
|
||||
ppId = registration.PullPaymentId;
|
||||
version = registration.Version;
|
||||
}
|
||||
|
||||
var keys = issuerKey.CreatePullPaymentCardKey(request.UID, version, ppId).DeriveBoltcardKeys(issuerKey);
|
||||
var boltcardUrl = Url.Action(nameof(UIBoltcardController.GetWithdrawRequest), "UIBoltcard");
|
||||
boltcardUrl = Request.GetAbsoluteUri(boltcardUrl);
|
||||
boltcardUrl = Regex.Replace(boltcardUrl, "^https?://", "lnurlw://");
|
||||
|
||||
var resp = new RegisterBoltcardResponse()
|
||||
{
|
||||
LNURLW = boltcardUrl,
|
||||
Version = version,
|
||||
K0 = Encoders.Hex.EncodeData(keys.AppMasterKey.ToBytes()).ToUpperInvariant(),
|
||||
K1 = Encoders.Hex.EncodeData(keys.EncryptionKey.ToBytes()).ToUpperInvariant(),
|
||||
K2 = Encoders.Hex.EncodeData(keys.AuthenticationKey.ToBytes()).ToUpperInvariant(),
|
||||
K3 = Encoders.Hex.EncodeData(keys.K3.ToBytes()).ToUpperInvariant(),
|
||||
K4 = Encoders.Hex.EncodeData(keys.K4.ToBytes()).ToUpperInvariant(),
|
||||
};
|
||||
|
||||
return Ok(resp);
|
||||
}
|
||||
private string? ExtractP(string? url)
|
||||
{
|
||||
if (url is null || !Uri.TryCreate(url, UriKind.Absolute, out var uri))
|
||||
return null;
|
||||
int num = uri.AbsoluteUri.IndexOf('?');
|
||||
if (num == -1)
|
||||
return null;
|
||||
string input = uri.AbsoluteUri.Substring(num);
|
||||
Match match = Regex.Match(input, "p=([a-f0-9A-F]{32})");
|
||||
if (!match.Success)
|
||||
return null;
|
||||
return match.Groups[1].Value;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,220 @@
|
||||
#nullable enable
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
using BTCPayServer.Abstractions.Models;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Filters;
|
||||
using BTCPayServer.Models.WalletViewModels;
|
||||
using BTCPayServer.Plugins.PointOfSale;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
using BTCPayServer.Plugins.PointOfSale.Models;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using System.Text.RegularExpressions;
|
||||
using System;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Payments;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using BTCPayServer.Client.Models;
|
||||
using Org.BouncyCastle.Ocsp;
|
||||
using BTCPayServer.NTag424;
|
||||
using NBitcoin.DataEncoders;
|
||||
using Newtonsoft.Json;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.HostedServices;
|
||||
using System.Threading;
|
||||
using BTCPayServer.Plugins.BoltcardFactory.ViewModels;
|
||||
using BTCPayServer.Controllers.Greenfield;
|
||||
using BTCPayServer.Models;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace BTCPayServer.Plugins.BoltcardFactory.Controllers
|
||||
{
|
||||
[AutoValidateAntiforgeryToken]
|
||||
[Route("apps")]
|
||||
public class UIBoltcardFactoryController : Controller
|
||||
{
|
||||
private readonly IEnumerable<IPayoutHandler> _payoutHandlers;
|
||||
private readonly CurrencyNameTable _currencies;
|
||||
private readonly AppService _appService;
|
||||
private readonly StoreRepository _storeRepository;
|
||||
private readonly CurrencyNameTable _currencyNameTable;
|
||||
private readonly IAuthorizationService _authorizationService;
|
||||
|
||||
public UIBoltcardFactoryController(
|
||||
IEnumerable<IPayoutHandler> payoutHandlers,
|
||||
CurrencyNameTable currencies,
|
||||
AppService appService,
|
||||
StoreRepository storeRepository,
|
||||
CurrencyNameTable currencyNameTable,
|
||||
IAuthorizationService authorizationService)
|
||||
{
|
||||
_payoutHandlers = payoutHandlers;
|
||||
_currencies = currencies;
|
||||
_appService = appService;
|
||||
_storeRepository = storeRepository;
|
||||
_currencyNameTable = currencyNameTable;
|
||||
_authorizationService = authorizationService;
|
||||
}
|
||||
public Data.StoreData CurrentStore => HttpContext.GetStoreData();
|
||||
private AppData GetCurrentApp() => HttpContext.GetAppData();
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
[HttpGet("{appId}/settings/boltcardfactory")]
|
||||
public async Task<IActionResult> UpdateBoltcardFactory(string appId)
|
||||
{
|
||||
if (CurrentStore is null || GetCurrentApp() is null)
|
||||
return NotFound();
|
||||
|
||||
var paymentMethods = await _payoutHandlers.GetSupportedPaymentMethods(CurrentStore);
|
||||
if (!paymentMethods.Any())
|
||||
{
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
{
|
||||
Message = "You must enable at least one payment method before creating a pull payment.",
|
||||
Severity = StatusMessageModel.StatusSeverity.Error
|
||||
});
|
||||
return RedirectToAction(nameof(UIStoresController.Dashboard), "UIStores", new { storeId = CurrentStore.Id });
|
||||
}
|
||||
|
||||
var req = GetCurrentApp().GetSettings<CreatePullPaymentRequest>();
|
||||
return base.View($"{BoltcardFactoryPlugin.ViewsDirectory}/UpdateBoltcardFactory.cshtml", CreateViewModel(paymentMethods, req));
|
||||
}
|
||||
|
||||
private static NewPullPaymentModel CreateViewModel(List<PaymentMethodId> paymentMethods, CreatePullPaymentRequest req)
|
||||
{
|
||||
return new NewPullPaymentModel
|
||||
{
|
||||
Name = req.Name,
|
||||
Currency = req.Currency,
|
||||
Amount = req.Amount,
|
||||
AutoApproveClaims = req.AutoApproveClaims,
|
||||
Description = req.Description,
|
||||
PaymentMethods = req.PaymentMethods,
|
||||
BOLT11Expiration = req.BOLT11Expiration?.TotalDays is double v ? (long)v : 30,
|
||||
EmbeddedCSS = req.EmbeddedCSS,
|
||||
CustomCSSLink = req.CustomCSSLink,
|
||||
PaymentMethodItems =
|
||||
paymentMethods.Select(id => new SelectListItem(id.ToPrettyString(), id.ToString(), true))
|
||||
};
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
[HttpPost("{appId}/settings/boltcardfactory")]
|
||||
public async Task<IActionResult> UpdateBoltcardFactory(string appId, NewPullPaymentModel model)
|
||||
{
|
||||
if (CurrentStore is null)
|
||||
return NotFound();
|
||||
var storeId = CurrentStore.Id;
|
||||
var paymentMethodOptions = await _payoutHandlers.GetSupportedPaymentMethods(CurrentStore);
|
||||
model.PaymentMethodItems =
|
||||
paymentMethodOptions.Select(id => new SelectListItem(id.ToPrettyString(), id.ToString(), true));
|
||||
model.Name ??= string.Empty;
|
||||
model.Currency = model.Currency?.ToUpperInvariant()?.Trim() ?? String.Empty;
|
||||
model.PaymentMethods ??= new List<string>();
|
||||
|
||||
if (!model.PaymentMethods.Any())
|
||||
{
|
||||
// Since we assign all payment methods to be selected by default above we need to update
|
||||
// them here to reflect user's selection so that they can correct their mistake
|
||||
model.PaymentMethodItems =
|
||||
paymentMethodOptions.Select(id => new SelectListItem(id.ToPrettyString(), id.ToString(), false));
|
||||
ModelState.AddModelError(nameof(model.PaymentMethods), "You need at least one payment method");
|
||||
}
|
||||
if (_currencyNameTable.GetCurrencyData(model.Currency, false) is null)
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.Currency), "Invalid currency");
|
||||
}
|
||||
if (model.Amount <= 0.0m)
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.Amount), "The amount should be more than zero");
|
||||
}
|
||||
if (model.Name.Length > 50)
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.Name), "The name should be maximum 50 characters.");
|
||||
}
|
||||
|
||||
var selectedPaymentMethodIds = model.PaymentMethods.Select(PaymentMethodId.Parse).ToArray();
|
||||
if (!selectedPaymentMethodIds.All(id => selectedPaymentMethodIds.Contains(id)))
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.Name), "Not all payment methods are supported");
|
||||
}
|
||||
if (!ModelState.IsValid)
|
||||
return View(model);
|
||||
model.AutoApproveClaims = model.AutoApproveClaims && (await
|
||||
_authorizationService.AuthorizeAsync(User, CurrentStore.Id, Policies.CanCreatePullPayments)).Succeeded;
|
||||
|
||||
var req = new CreatePullPaymentRequest()
|
||||
{
|
||||
Name = model.Name,
|
||||
Description = model.Description,
|
||||
Currency = model.Currency,
|
||||
CustomCSSLink = model.CustomCSSLink,
|
||||
Amount = model.Amount,
|
||||
AutoApproveClaims = model.AutoApproveClaims,
|
||||
EmbeddedCSS = model.EmbeddedCSS,
|
||||
BOLT11Expiration = TimeSpan.FromDays(model.BOLT11Expiration),
|
||||
PaymentMethods = model.PaymentMethods.ToArray()
|
||||
};
|
||||
var app = GetCurrentApp();
|
||||
app.SetSettings(req);
|
||||
await _appService.UpdateOrCreateApp(app);
|
||||
var paymentMethods = await _payoutHandlers.GetSupportedPaymentMethods(CurrentStore);
|
||||
this.TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||
{
|
||||
Message = "Pull payment request created",
|
||||
Severity = StatusMessageModel.StatusSeverity.Success
|
||||
});
|
||||
return View($"{BoltcardFactoryPlugin.ViewsDirectory}/UpdateBoltcardFactory.cshtml", CreateViewModel(paymentMethods, req));
|
||||
}
|
||||
private async Task<string?> GetStoreDefaultCurrentIfEmpty(string storeId, string? currency)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(currency))
|
||||
{
|
||||
currency = (await _storeRepository.FindStore(storeId))?.GetStoreBlob()?.DefaultCurrency;
|
||||
}
|
||||
return currency?.Trim().ToUpperInvariant();
|
||||
}
|
||||
private int[] ListSplit(string list, string separator = ",")
|
||||
{
|
||||
if (string.IsNullOrEmpty(list))
|
||||
{
|
||||
return Array.Empty<int>();
|
||||
}
|
||||
|
||||
// Remove all characters except numeric and comma
|
||||
Regex charsToDestroy = new Regex(@"[^\d|\" + separator + "]");
|
||||
list = charsToDestroy.Replace(list, "");
|
||||
|
||||
return list.Split(separator, StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToArray();
|
||||
}
|
||||
[HttpGet("/apps/{appId}/boltcardfactory")]
|
||||
[DomainMappingConstraint(BoltcardFactoryPlugin.AppType)]
|
||||
[AllowAnonymous]
|
||||
public IActionResult ViewBoltcardFactory(string appId)
|
||||
{
|
||||
var vm = new ViewBoltcardFactoryViewModel();
|
||||
vm.SetupDeepLink = $"boltcard://program?url={GetBoltcardDeeplinkUrl(appId, OnExistingBehavior.UpdateVersion)}";
|
||||
vm.ResetDeepLink = $"boltcard://reset?url={GetBoltcardDeeplinkUrl(appId, OnExistingBehavior.KeepVersion)}";
|
||||
return View($"{BoltcardFactoryPlugin.ViewsDirectory}/ViewBoltcardFactory.cshtml", vm);
|
||||
}
|
||||
|
||||
private string GetBoltcardDeeplinkUrl(string appId, OnExistingBehavior onExisting)
|
||||
{
|
||||
var registerUrl = Url.Action(nameof(APIBoltcardFactoryController.RegisterBoltcard), "APIBoltcardFactory",
|
||||
new
|
||||
{
|
||||
appId = appId,
|
||||
onExisting = onExisting.ToString()
|
||||
}, Request.Scheme, Request.Host.ToString());
|
||||
registerUrl = Uri.EscapeDataString(registerUrl!);
|
||||
return registerUrl;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
namespace BTCPayServer.Plugins.BoltcardFactory.ViewModels;
|
||||
|
||||
public class ViewBoltcardFactoryViewModel
|
||||
{
|
||||
public string SetupDeepLink { get; set; }
|
||||
public string ResetDeepLink { get; set; }
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
@using BTCPayServer.Client
|
||||
@using BTCPayServer.Plugins.BoltcardFactory
|
||||
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@using BTCPayServer.Views.Apps
|
||||
@using BTCPayServer.Abstractions.Extensions
|
||||
@using BTCPayServer.Abstractions.TagHelpers
|
||||
@using BTCPayServer.Plugins.PointOfSale
|
||||
@using BTCPayServer.Services.Apps
|
||||
@inject AppService AppService;
|
||||
@model BTCPayServer.Components.MainNav.MainNavViewModel
|
||||
@{
|
||||
var store = Context.GetStoreData();
|
||||
}
|
||||
|
||||
@if (store != null)
|
||||
{
|
||||
var appType = AppService.GetAppType(BoltcardFactoryPlugin.AppType)!;
|
||||
<li class="nav-item" permission="@Policies.CanModifyStoreSettings">
|
||||
<a asp-area="" asp-controller="UIApps" asp-action="CreateApp" asp-route-storeId="@store.Id" asp-route-appType="@appType.Type" class="nav-link @ViewData.IsActivePage(AppsNavPages.Create, appType.Type)" id="@($"StoreNav-Create{appType.Type}")">
|
||||
<vc:icon symbol="pointofsale" />
|
||||
<span>@appType.Description</span>
|
||||
</a>
|
||||
</li>
|
||||
@foreach (var app in Model.Apps.Where(app => app.AppType == appType.Type))
|
||||
{
|
||||
<li class="nav-item nav-item-sub" permission="@Policies.CanModifyStoreSettings">
|
||||
<a asp-area="" asp-controller="UIBoltcardFactory" asp-action="UpdateBoltcardFactory" asp-route-appId="@app.Id" class="nav-link @ViewData.IsActivePage(AppsNavPages.Update, app.Id)" id="@($"StoreNav-App-{app.Id}")">
|
||||
<span>@app.AppName</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item nav-item-sub" not-permission="@Policies.CanModifyStoreSettings">
|
||||
<a asp-area="" asp-controller="UIBoltcardFactory" asp-action="ViewBoltcardFactory" asp-route-appId="@app.Id" class="nav-link">
|
||||
<span>@app.AppName</span>
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
}
|
@ -0,0 +1,130 @@
|
||||
@using BTCPayServer.Abstractions.Extensions
|
||||
@using BTCPayServer.Client
|
||||
@using BTCPayServer.Views.Stores
|
||||
@model BTCPayServer.Models.WalletViewModels.NewPullPaymentModel
|
||||
@{
|
||||
ViewData["Title"] = "Update Boltcard Factory";
|
||||
Layout = "/Views/Shared/_Layout.cshtml";
|
||||
}
|
||||
|
||||
@section PageHeadContent {
|
||||
<link href="~/vendor/summernote/summernote-bs5.css" rel="stylesheet" asp-append-version="true"/>
|
||||
}
|
||||
|
||||
@section PageFootContent {
|
||||
<partial name="_ValidationScriptsPartial"/>
|
||||
<script src="~/vendor/summernote/summernote-bs5.js" asp-append-version="true"></script>
|
||||
}
|
||||
|
||||
<form method="post" asp-route-appId="@Context.GetRouteValue("appId")" asp-action="UpdateBoltcardFactory">
|
||||
<div class="sticky-header d-flex align-items-center justify-content-between">
|
||||
<h2 class="mb-0">@ViewData["Title"]</h2>
|
||||
<div class="d-flex gap-3 mt-3 mt-sm-0">
|
||||
<a class="btn btn-secondary" asp-controller="UIBoltcardFactory" asp-action="ViewBoltcardFactory" asp-route-appId="@Context.GetRouteValue("appId")" id="ViewApp" target="_blank">View</a>
|
||||
<input type="submit" value="Save" class="btn btn-primary order-sm-1" id="Save" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<partial name="_StatusMessage"/>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label asp-for="Name" class="form-label"></label>
|
||||
<input asp-for="Name" class="form-control"/>
|
||||
<span asp-validation-for="Name" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="form-group col-8">
|
||||
<label asp-for="Amount" class="form-label" data-required></label>
|
||||
<input asp-for="Amount" class="form-control" inputmode="decimal"/>
|
||||
<span asp-validation-for="Amount" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group col-4">
|
||||
<label asp-for="Currency" class="form-label"></label>
|
||||
<input asp-for="Currency" class="form-control w-auto" currency-selection />
|
||||
<span asp-validation-for="Currency" class="text-danger"></span>
|
||||
</div>
|
||||
|
||||
<div class="form-group col-12" permission="@Policies.CanCreatePullPayments">
|
||||
<div class="form-check ">
|
||||
<input asp-for="AutoApproveClaims" type="checkbox" class="form-check-input"/>
|
||||
<label asp-for="AutoApproveClaims" class="form-check-label"></label>
|
||||
<span asp-validation-for="AutoApproveClaims" class="text-danger"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group mb-4">
|
||||
<label asp-for="PaymentMethods" class="form-label"></label>
|
||||
@foreach (var item in Model.PaymentMethodItems)
|
||||
{
|
||||
<div class="form-check mb-2">
|
||||
<label class="form-label">
|
||||
<input name="PaymentMethods" class="form-check-input" type="checkbox" value="@item.Value" @(item.Selected ? "checked" : "")>
|
||||
@item.Text
|
||||
</label>
|
||||
</div>
|
||||
}
|
||||
<span asp-validation-for="PaymentMethods" class="text-danger mt-0"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-9">
|
||||
<div class="form-group mb-4">
|
||||
<label asp-for="Description" class="form-label"></label>
|
||||
<textarea asp-for="Description" class="form-control richtext"></textarea>
|
||||
<span asp-validation-for="Description" class="text-danger"></span>
|
||||
</div>
|
||||
|
||||
<h4 class="mt-5 mb-2">Additional Options</h4>
|
||||
<div class="form-group">
|
||||
<div class="accordion" id="additional">
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header" id="additional-custom-css-header">
|
||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#additional-custom-css" aria-expanded="false" aria-controls="additional-custom-css">
|
||||
Custom CSS
|
||||
<vc:icon symbol="caret-down"/>
|
||||
</button>
|
||||
</h2>
|
||||
<div id="additional-custom-css" class="accordion-collapse collapse" aria-labelledby="additional-custom-css-header">
|
||||
<div class="accordion-body">
|
||||
<div class="form-group">
|
||||
<label asp-for="CustomCSSLink" class="form-label"></label>
|
||||
<a href="https://docs.btcpayserver.org/Development/Theme/#2-bootstrap-themes" target="_blank" rel="noreferrer noopener" title="More information...">
|
||||
<vc:icon symbol="info" />
|
||||
</a>
|
||||
<input asp-for="CustomCSSLink" class="form-control"/>
|
||||
<span asp-validation-for="CustomCSSLink" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="EmbeddedCSS" class="form-label"></label>
|
||||
<textarea asp-for="EmbeddedCSS" rows="10" cols="40" class="form-control"></textarea>
|
||||
<span asp-validation-for="EmbeddedCSS" class="text-danger"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header" id="additional-lightning-header">
|
||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#additional-lightning" aria-expanded="false" aria-controls="additional-lightning">
|
||||
Lightning network settings
|
||||
<vc:icon symbol="caret-down"/>
|
||||
</button>
|
||||
</h2>
|
||||
<div id="additional-lightning" class="accordion-collapse collapse" aria-labelledby="additional-lightning-header">
|
||||
<div class="accordion-body">
|
||||
<div class="form-group">
|
||||
<label asp-for="BOLT11Expiration" class="form-label"></label>
|
||||
<div class="input-group">
|
||||
<input inputmode="numeric" asp-for="BOLT11Expiration" class="form-control" style="max-width:12ch;"/>
|
||||
<span class="input-group-text">days</span>
|
||||
</div>
|
||||
<span asp-validation-for="BOLT11Expiration" class="text-danger"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
@ -0,0 +1,44 @@
|
||||
@model ViewBoltcardFactoryViewModel
|
||||
|
||||
@{
|
||||
ViewData["Title"] = "Boltcard factory";
|
||||
Layout = "/Views/Shared/_LayoutWizard.cshtml";
|
||||
}
|
||||
|
||||
<header class="text-center">
|
||||
<h1>Program Boltcards</h1>
|
||||
<p class="lead text-secondary mt-3" id="explanation">Using Boltcard NFC Programmer</p>
|
||||
</header>
|
||||
|
||||
<div id="body" class="my-4">
|
||||
<div id="actions" class="d-flex align-items-center justify-content-center" style="visibility:hidden">
|
||||
<div class="d-flex gap-3 mt-3 mt-sm-0">
|
||||
<a class="btn btn-primary" href="@Model.SetupDeepLink" target="_blank">Setup</a>
|
||||
<a class="btn btn-danger" href="@Model.ResetDeepLink" target="_blank">Reset</a>
|
||||
</div>
|
||||
</div>
|
||||
<div id="qr" class="d-flex align-items-center justify-content-center">
|
||||
<div class="d-inline-flex flex-column" style="width:256px" style="visibility:hidden">
|
||||
<div class="qr-container mb-2">
|
||||
<vc:qr-code data="@Context.Request.GetCurrentUrl()" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
var isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
|
||||
if (isMobile) {
|
||||
document.getElementById("actions").style.visibility = "visible";
|
||||
document.getElementById("qr").style.visibility = "hidden";
|
||||
}
|
||||
else {
|
||||
document.getElementById("actions").style.visibility = "hidden";
|
||||
document.getElementById("qr").style.visibility = "visible";
|
||||
document.getElementById("explanation").innerText = "Scan the QR code with your mobile device";
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
|
@ -0,0 +1 @@
|
||||
@using BTCPayServer.Plugins.BoltcardFactory.ViewModels;
|
22
BTCPayServer/Plugins/BoltcardTopUp/BoltcardTopUpPlugin.cs
Normal file
22
BTCPayServer/Plugins/BoltcardTopUp/BoltcardTopUpPlugin.cs
Normal file
@ -0,0 +1,22 @@
|
||||
using BTCPayServer.Abstractions.Contracts;
|
||||
using BTCPayServer.Abstractions.Models;
|
||||
using BTCPayServer.Abstractions.Services;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using static BTCPayServer.Plugins.BoltcardFactory.BoltcardFactoryPlugin;
|
||||
|
||||
namespace BTCPayServer.Plugins.BoltcardTopUp;
|
||||
|
||||
public class BoltcardTopUpPlugin : BaseBTCPayServerPlugin
|
||||
{
|
||||
public const string ViewsDirectory = "/Plugins/BoltcardTopUp/Views";
|
||||
public override string Identifier => "BTCPayServer.Plugins.BoltcardTopUp";
|
||||
public override string Name => "BoltcardTopUp";
|
||||
public override string Description => "Add the ability to Top-Up a Boltcard";
|
||||
|
||||
public override void Execute(IServiceCollection services)
|
||||
{
|
||||
services.AddSingleton<IUIExtension>(new UIExtension($"{ViewsDirectory}/NavExtension.cshtml", "header-nav"));
|
||||
base.Execute(services);
|
||||
}
|
||||
}
|
@ -0,0 +1,207 @@
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Filters;
|
||||
using BTCPayServer.Plugins.PointOfSale.Models;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.ModelBinders;
|
||||
using BTCPayServer.Plugins.BoltcardBalance;
|
||||
using System.Collections.Specialized;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.NTag424;
|
||||
using BTCPayServer.Services;
|
||||
using NBitcoin.DataEncoders;
|
||||
using NBitcoin;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Newtonsoft.Json;
|
||||
using Org.BouncyCastle.Ocsp;
|
||||
using System.Security.Claims;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Plugins.BoltcardBalance.Controllers;
|
||||
using BTCPayServer.HostedServices;
|
||||
|
||||
namespace BTCPayServer.Plugins.BoltcardTopUp.Controllers
|
||||
{
|
||||
public class UIBoltcardTopUpController : Controller
|
||||
{
|
||||
private readonly ApplicationDbContextFactory _dbContextFactory;
|
||||
private readonly SettingsRepository _settingsRepository;
|
||||
private readonly BTCPayServerEnvironment _env;
|
||||
private readonly BTCPayNetworkJsonSerializerSettings _jsonSerializerSettings;
|
||||
private readonly RateFetcher _rateFetcher;
|
||||
private readonly BTCPayNetworkProvider _networkProvider;
|
||||
private readonly UIBoltcardBalanceController _boltcardBalanceController;
|
||||
private readonly PullPaymentHostedService _ppService;
|
||||
|
||||
public UIBoltcardTopUpController(
|
||||
ApplicationDbContextFactory dbContextFactory,
|
||||
SettingsRepository settingsRepository,
|
||||
BTCPayServerEnvironment env,
|
||||
BTCPayNetworkJsonSerializerSettings jsonSerializerSettings,
|
||||
RateFetcher rateFetcher,
|
||||
BTCPayNetworkProvider networkProvider,
|
||||
UIBoltcardBalanceController boltcardBalanceController,
|
||||
PullPaymentHostedService ppService,
|
||||
CurrencyNameTable currencies)
|
||||
{
|
||||
_dbContextFactory = dbContextFactory;
|
||||
_settingsRepository = settingsRepository;
|
||||
_env = env;
|
||||
_jsonSerializerSettings = jsonSerializerSettings;
|
||||
_rateFetcher = rateFetcher;
|
||||
_networkProvider = networkProvider;
|
||||
_boltcardBalanceController = boltcardBalanceController;
|
||||
_ppService = ppService;
|
||||
Currencies = currencies;
|
||||
}
|
||||
|
||||
public CurrencyNameTable Currencies { get; }
|
||||
|
||||
[HttpGet("~/stores/{storeId}/boltcards/top-up")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
[XFrameOptions(XFrameOptionsAttribute.XFrameOptions.Unset)]
|
||||
[AutoValidateAntiforgeryToken]
|
||||
public async Task<IActionResult> Keypad(string storeId, string currency = null)
|
||||
{
|
||||
var settings = new PointOfSaleSettings
|
||||
{
|
||||
Title = "Boltcards Top-Up"
|
||||
};
|
||||
currency ??= this.HttpContext.GetStoreData().GetStoreBlob().DefaultCurrency;
|
||||
var numberFormatInfo = Currencies.GetNumberFormatInfo(currency);
|
||||
double step = Math.Pow(10, -numberFormatInfo.CurrencyDecimalDigits);
|
||||
//var store = new Data.StoreData();
|
||||
//var storeBlob = new StoreBlob();
|
||||
|
||||
return View($"{BoltcardTopUpPlugin.ViewsDirectory}/Keypad.cshtml", new ViewPointOfSaleViewModel
|
||||
{
|
||||
Title = settings.Title,
|
||||
//StoreName = store.StoreName,
|
||||
//BrandColor = storeBlob.BrandColor,
|
||||
//CssFileId = storeBlob.CssFileId,
|
||||
//LogoFileId = storeBlob.LogoFileId,
|
||||
Step = step.ToString(CultureInfo.InvariantCulture),
|
||||
//ViewType = BTCPayServer.Plugins.PointOfSale.PosViewType.Light,
|
||||
//ShowCustomAmount = settings.ShowCustomAmount,
|
||||
//ShowDiscount = settings.ShowDiscount,
|
||||
//ShowSearch = settings.ShowSearch,
|
||||
//ShowCategories = settings.ShowCategories,
|
||||
//EnableTips = settings.EnableTips,
|
||||
//CurrencyCode = settings.Currency,
|
||||
//CurrencySymbol = numberFormatInfo.CurrencySymbol,
|
||||
CurrencyCode = currency,
|
||||
CurrencyInfo = new ViewPointOfSaleViewModel.CurrencyInfoData
|
||||
{
|
||||
CurrencySymbol = string.IsNullOrEmpty(numberFormatInfo.CurrencySymbol) ? settings.Currency : numberFormatInfo.CurrencySymbol,
|
||||
Divisibility = numberFormatInfo.CurrencyDecimalDigits,
|
||||
DecimalSeparator = numberFormatInfo.CurrencyDecimalSeparator,
|
||||
ThousandSeparator = numberFormatInfo.NumberGroupSeparator,
|
||||
Prefixed = new[] { 0, 2 }.Contains(numberFormatInfo.CurrencyPositivePattern),
|
||||
SymbolSpace = new[] { 2, 3 }.Contains(numberFormatInfo.CurrencyPositivePattern)
|
||||
},
|
||||
//Items = AppService.Parse(settings.Template, false),
|
||||
//ButtonText = settings.ButtonText,
|
||||
//CustomButtonText = settings.CustomButtonText,
|
||||
//CustomTipText = settings.CustomTipText,
|
||||
//CustomTipPercentages = settings.CustomTipPercentages,
|
||||
//CustomCSSLink = settings.CustomCSSLink,
|
||||
//CustomLogoLink = storeBlob.CustomLogo,
|
||||
//AppId = "vouchers",
|
||||
StoreId = storeId,
|
||||
//Description = settings.Description,
|
||||
//EmbeddedCSS = settings.EmbeddedCSS,
|
||||
//RequiresRefundEmail = settings.RequiresRefundEmail
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost("~/stores/{storeId}/boltcards/top-up")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
[XFrameOptions(XFrameOptionsAttribute.XFrameOptions.Unset)]
|
||||
[AutoValidateAntiforgeryToken]
|
||||
public IActionResult Keypad(string storeId,
|
||||
[ModelBinder(typeof(InvariantDecimalModelBinder))] decimal amount, string currency)
|
||||
{
|
||||
return RedirectToAction(nameof(ScanCard),
|
||||
new
|
||||
{
|
||||
storeId = storeId,
|
||||
amount = amount,
|
||||
currency = currency
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet("~/stores/{storeId}/boltcards/top-up/scan")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
[XFrameOptions(XFrameOptionsAttribute.XFrameOptions.Unset)]
|
||||
[AutoValidateAntiforgeryToken]
|
||||
public async Task<IActionResult> ScanCard(string storeId,
|
||||
[ModelBinder(typeof(InvariantDecimalModelBinder))] decimal amount, string currency)
|
||||
{
|
||||
return View($"{BoltcardTopUpPlugin.ViewsDirectory}/ScanCard.cshtml");
|
||||
}
|
||||
|
||||
[HttpPost("~/stores/{storeId}/boltcards/top-up/scan")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
[XFrameOptions(XFrameOptionsAttribute.XFrameOptions.Unset)]
|
||||
public async Task<IActionResult> ScanCard(string storeId,
|
||||
[ModelBinder(typeof(InvariantDecimalModelBinder))] decimal amount, string currency, string p, string c)
|
||||
{
|
||||
//return View($"{BoltcardBalancePlugin.ViewsDirectory}/BalanceView.cshtml", new BoltcardBalance.ViewModels.BalanceViewModel()
|
||||
//{
|
||||
// AmountDue = 10000m,
|
||||
// Currency = "SATS",
|
||||
// Transactions = [new() { Date = DateTimeOffset.UtcNow, Balance = -3.0m }, new() { Date = DateTimeOffset.UtcNow, Balance = -5.0m }]
|
||||
//});
|
||||
|
||||
var issuerKey = await _settingsRepository.GetIssuerKey(_env);
|
||||
var boltData = issuerKey.TryDecrypt(p);
|
||||
if (boltData?.Uid is null)
|
||||
return NotFound();
|
||||
var id = issuerKey.GetId(boltData.Uid);
|
||||
var registration = await _dbContextFactory.GetBoltcardRegistration(issuerKey, boltData, true);
|
||||
if (registration is null)
|
||||
return NotFound();
|
||||
|
||||
var pp = await _ppService.GetPullPayment(registration.PullPaymentId, false);
|
||||
|
||||
var rules = this.HttpContext.GetStoreData().GetStoreBlob().GetRateRules(_networkProvider);
|
||||
var rateResult = await _rateFetcher.FetchRate(new Rating.CurrencyPair("BTC", currency), rules, default);
|
||||
var cryptoAmount = Math.Round(amount / rateResult.BidAsk.Bid, 11);
|
||||
|
||||
var ppCurrency = pp.GetBlob().Currency;
|
||||
rateResult = await _rateFetcher.FetchRate(new Rating.CurrencyPair(ppCurrency, currency), rules, default);
|
||||
var ppAmount = Math.Round(amount / rateResult.BidAsk.Bid, Currencies.GetNumberFormatInfo(ppCurrency).CurrencyDecimalDigits);
|
||||
|
||||
using var ctx = _dbContextFactory.CreateContext();
|
||||
var payout = new Data.PayoutData()
|
||||
{
|
||||
Id = Encoders.Base58.EncodeData(RandomUtils.GetBytes(20)),
|
||||
Date = DateTimeOffset.UtcNow,
|
||||
State = PayoutState.Completed,
|
||||
PullPaymentDataId = registration.PullPaymentId,
|
||||
PaymentMethodId = new PaymentMethodId("BTC", PaymentTypes.LightningLike).ToString(),
|
||||
Destination = null,
|
||||
StoreDataId = storeId
|
||||
};
|
||||
var payoutBlob = new PayoutBlob()
|
||||
{
|
||||
CryptoAmount = -cryptoAmount,
|
||||
Amount = -ppAmount,
|
||||
Destination = null,
|
||||
Metadata = new JObject(),
|
||||
};
|
||||
payout.SetBlob(payoutBlob, _jsonSerializerSettings);
|
||||
await ctx.Payouts.AddAsync(payout);
|
||||
await ctx.SaveChangesAsync();
|
||||
_boltcardBalanceController.ViewData["NoCancelWizard"] = true;
|
||||
return await _boltcardBalanceController.GetBalanceView(registration.PullPaymentId, p);
|
||||
}
|
||||
}
|
||||
}
|
48
BTCPayServer/Plugins/BoltcardTopUp/Views/Keypad.cshtml
Normal file
48
BTCPayServer/Plugins/BoltcardTopUp/Views/Keypad.cshtml
Normal file
@ -0,0 +1,48 @@
|
||||
@inject BTCPayServer.Security.ContentSecurityPolicies Csp
|
||||
@model BTCPayServer.Plugins.PointOfSale.Models.ViewPointOfSaleViewModel
|
||||
@{
|
||||
Layout = "PointOfSale/Public/_Layout";
|
||||
Csp.UnsafeEval();
|
||||
}
|
||||
@section PageHeadContent {
|
||||
<link href="~/pos/keypad.css" asp-append-version="true" rel="stylesheet" />
|
||||
}
|
||||
@section PageFootContent {
|
||||
<script>var srvModel = @Safe.Json(Model);</script>
|
||||
<script src="~/vendor/vuejs/vue.min.js" asp-append-version="true"></script>
|
||||
<script src="~/pos/common.js" asp-append-version="true"></script>
|
||||
<script src="~/pos/keypad.js" asp-append-version="true"></script>
|
||||
}
|
||||
<div id="PosKeypad" class="public-page-wrap">
|
||||
<partial name="_StatusMessage" />
|
||||
<partial name="_StoreHeader" model="(Model.Title, null as StoreBrandingViewModel)" />
|
||||
|
||||
<form id="app" method="post"
|
||||
asp-route-storeId="@Model.StoreId"
|
||||
asp-antiforgery="true" v-on:submit="handleFormSubmit" class="d-flex flex-column gap-4 my-auto" v-cloak>
|
||||
<input type="hidden" name="posdata" v-model="posdata" id="posdata">
|
||||
<input type="hidden" name="amount" v-model="totalNumeric">
|
||||
<input type="hidden" name="currency" v-model="currencyCode">
|
||||
<div ref="display" class="d-flex flex-column align-items-center px-4 mb-auto">
|
||||
<div class="fw-semibold text-muted" id="Currency">{{currencyCode}}</div>
|
||||
<div class="fw-bold lh-sm" ref="amount" v-bind:style="{ fontSize: `${fontSize}px` }" id="Amount">{{ formatCurrency(total, false) }}</div>
|
||||
<div class="text-muted text-center mt-2" id="Calculation">{{ calculation }}</div>
|
||||
</div>
|
||||
<div class="keypad">
|
||||
<button v-for="k in keys" :key="k" :disabled="k === '+' && mode !== 'amounts'" v-on:click.prevent="keyPressed(k)" v-on:dblclick.prevent="doubleClick(k)" type="button" class="btn btn-secondary btn-lg" :data-key="k">{{ k }}</button>
|
||||
</div>
|
||||
<button class="btn btn-lg btn-primary mx-3" type="submit" :disabled="payButtonLoading" id="pay-button">
|
||||
<div v-if="payButtonLoading" class="spinner-border spinner-border-sm" id="pay-button-spinner" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
<template v-else>Top-Up Card</template>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
|
||||
<footer class="store-footer">
|
||||
<a class="store-powered-by" href="https://btcpayserver.org" target="_blank" rel="noreferrer noopener">
|
||||
Powered by <partial name="_StoreFooterLogo" />
|
||||
</a>
|
||||
</footer>
|
||||
</div>
|
20
BTCPayServer/Plugins/BoltcardTopUp/Views/NavExtension.cshtml
Normal file
20
BTCPayServer/Plugins/BoltcardTopUp/Views/NavExtension.cshtml
Normal file
@ -0,0 +1,20 @@
|
||||
@using BTCPayServer.Client
|
||||
@using BTCPayServer.Plugins.BoltcardFactory
|
||||
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@using BTCPayServer.Views.Apps
|
||||
@using BTCPayServer.Abstractions.Extensions
|
||||
@using BTCPayServer.Abstractions.TagHelpers
|
||||
@using BTCPayServer
|
||||
|
||||
@{
|
||||
var storeId = Context.GetStoreData()?.Id;
|
||||
}
|
||||
@if (storeId != null)
|
||||
{
|
||||
<li class="nav-item">
|
||||
<a asp-area="" asp-controller="UIBoltcardTopUp" asp-action="Keypad" asp-route-storeId="@storeId" class="nav-link">
|
||||
<vc:icon symbol="pay-button" />
|
||||
<span>Boltcard Top-Up</span>
|
||||
</a>
|
||||
</li>
|
||||
}
|
150
BTCPayServer/Plugins/BoltcardTopUp/Views/ScanCard.cshtml
Normal file
150
BTCPayServer/Plugins/BoltcardTopUp/Views/ScanCard.cshtml
Normal file
@ -0,0 +1,150 @@
|
||||
@{
|
||||
ViewData["Title"] = "Boltcard TopUps";
|
||||
ViewData["ShowFooter"] = false;
|
||||
Layout = "/Views/Shared/_LayoutWizard.cshtml";
|
||||
}
|
||||
|
||||
@section PageHeadContent
|
||||
{
|
||||
<style>
|
||||
.amount-col {
|
||||
text-align: right;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
}
|
||||
|
||||
<header class="text-center">
|
||||
<h1>Boltcard Top-Up</h1>
|
||||
<p class="lead text-secondary mt-3" id="explanation">Scan your card to top it up</p>
|
||||
</header>
|
||||
|
||||
<div id="body" class="my-4">
|
||||
<div id="actions" class="d-flex align-items-center justify-content-center d-none">
|
||||
<div class="d-flex gap-3 mt-3 mt-sm-0">
|
||||
<a id="start-scan-btn" class="btn btn-primary" href="#">Ask permission...</a>
|
||||
</div>
|
||||
</div>
|
||||
<div id="qr" class="d-flex flex-column align-items-center justify-content-center d-none">
|
||||
<div class="d-inline-flex flex-column">
|
||||
<div class="qr-container mb-2">
|
||||
<vc:qr-code data="@Context.Request.GetCurrentUrl()" />
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-secondary">NFC not supported in this device</p>
|
||||
</div>
|
||||
<div id="scanning-btn" class="d-flex align-items-center justify-content-center d-none">
|
||||
<div class="d-flex gap-3 mt-3 mt-sm-0">
|
||||
<a id="scanning-btn-link" class="action-button" style="font-size: 50px;" ></a>
|
||||
</div>
|
||||
</div>
|
||||
<div id="balance" class="row">
|
||||
<div id="balance-table"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
var permissionGranted = false;
|
||||
var ndef = null;
|
||||
var abortController = null;
|
||||
var scanned = false;
|
||||
|
||||
function delay(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
async function showBalance(lnurlw) {
|
||||
setState("Submitting");
|
||||
await delay(1000);
|
||||
var url = window.location.href.replace("#", "");
|
||||
url = url.split("?")[0] + "?" + lnurlw.split("?")[1] + "&" + url.split("?")[1];
|
||||
// url = "https://testnet.demo.btcpayserver.org/boltcards/balance?p=...&c=..."
|
||||
|
||||
scanned = true;
|
||||
var xhttp = new XMLHttpRequest();
|
||||
xhttp.onreadystatechange = function () {
|
||||
if (this.readyState == 4 && this.status == 200 && this.responseText) {
|
||||
document.getElementById("balance-table").innerHTML = this.responseText;
|
||||
setState("ShowBalance");
|
||||
}
|
||||
else {
|
||||
scanned = false;
|
||||
setState("WaitingForCard");
|
||||
}
|
||||
};
|
||||
xhttp.open('POST', url, true);
|
||||
xhttp.send(new FormData());
|
||||
}
|
||||
async function startScan() {
|
||||
if (!('NDEFReader' in window)) {
|
||||
return;
|
||||
}
|
||||
ndef = new NDEFReader();
|
||||
abortController = new AbortController();
|
||||
abortController.signal.onabort = () => setState("WaitingForCard");
|
||||
|
||||
await ndef.scan({ signal: abortController.signal })
|
||||
setState("WaitingForCard");
|
||||
ndef.onreading = async ({ message }) => {
|
||||
const record = message.records[0];
|
||||
const textDecoder = new TextDecoder('utf-8');
|
||||
const decoded = textDecoder.decode(record.data);
|
||||
await showBalance(decoded);
|
||||
};
|
||||
}
|
||||
|
||||
function setState(state)
|
||||
{
|
||||
document.getElementById("actions").classList.add("d-none");
|
||||
document.getElementById("qr").classList.add("d-none");
|
||||
document.getElementById("scanning-btn").classList.add("d-none");
|
||||
document.getElementById("balance").classList.add("d-none");
|
||||
|
||||
if (state === "NFCNotSupported")
|
||||
{
|
||||
document.getElementById("qr").classList.remove("d-none");
|
||||
}
|
||||
else if (state === "WaitingForPermission")
|
||||
{
|
||||
|
||||
document.getElementById("actions").classList.remove("d-none");
|
||||
}
|
||||
else if (state === "WaitingForCard")
|
||||
{
|
||||
document.getElementById("scanning-btn").classList.remove("d-none");
|
||||
document.getElementById("scanning-btn-link").innerHTML = "<i class=\"fa fa-wifi\"></i>";
|
||||
}
|
||||
else if (state == "Submitting")
|
||||
{
|
||||
document.getElementById("scanning-btn").classList.remove("d-none");
|
||||
document.getElementById("scanning-btn-link").innerHTML = "<i class=\"fa fa-spinner\"></i>"
|
||||
}
|
||||
else if (state == "ShowBalance") {
|
||||
document.getElementById("explanation").classList.add("d-none");
|
||||
document.getElementById("scanning-btn").classList.remove("d-none");
|
||||
document.getElementById("scanning-btn-link").innerHTML = "<i class=\"fa fa-bitcoin\"></i>";
|
||||
document.getElementById("balance").classList.remove("d-none");
|
||||
}
|
||||
}
|
||||
document.addEventListener("DOMContentLoaded", async () => {
|
||||
var nfcSupported = 'NDEFReader' in window;
|
||||
if (!nfcSupported) {
|
||||
setState("NFCNotSupported");
|
||||
//setState("ShowBalance");
|
||||
}
|
||||
else {
|
||||
setState("WaitingForPermission");
|
||||
var granted = (await navigator.permissions.query({ name: 'nfc' })).state === 'granted';
|
||||
if (granted)
|
||||
{
|
||||
setState("WaitingForCard");
|
||||
startScan();
|
||||
}
|
||||
}
|
||||
delegate('click', "#start-scan-btn", startScan);
|
||||
//showBalance("lnurl://ewfw?p=test&c=test");
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
|
@ -92,6 +92,7 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
|
||||
EmbeddedCSS = settings.EmbeddedCSS,
|
||||
CustomCSSLink = settings.CustomCSSLink
|
||||
};
|
||||
// Check if the currency is COP or ARS (exclude decimal places)
|
||||
|
||||
return View($"PointOfSale/Public/{viewType}", new ViewPointOfSaleViewModel
|
||||
{
|
||||
|
@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
using BTCPayServer.Abstractions.Contracts;
|
||||
@ -74,10 +73,10 @@ namespace BTCPayServer.Security
|
||||
// resolve from app
|
||||
if (routeData.Values.TryGetValue("appId", out var vAppId) && vAppId is string appId)
|
||||
{
|
||||
app = await _appService.GetAppDataIfOwner(userId, appId);
|
||||
app = await _appService.GetAppData(userId, appId);
|
||||
if (storeId == null)
|
||||
{
|
||||
storeId = app?.StoreDataId ?? String.Empty;
|
||||
storeId = app?.StoreDataId ?? string.Empty;
|
||||
}
|
||||
else if (app?.StoreDataId != storeId)
|
||||
{
|
||||
@ -90,7 +89,7 @@ namespace BTCPayServer.Security
|
||||
paymentRequest = await _paymentRequestRepository.FindPaymentRequest(payReqId, userId);
|
||||
if (storeId == null)
|
||||
{
|
||||
storeId = paymentRequest?.StoreDataId ?? String.Empty;
|
||||
storeId = paymentRequest?.StoreDataId ?? string.Empty;
|
||||
}
|
||||
else if (paymentRequest?.StoreDataId != storeId)
|
||||
{
|
||||
@ -103,7 +102,7 @@ namespace BTCPayServer.Security
|
||||
invoice = await _invoiceRepository.GetInvoice(invoiceId);
|
||||
if (storeId == null)
|
||||
{
|
||||
storeId = invoice?.StoreId ?? String.Empty;
|
||||
storeId = invoice?.StoreId ?? string.Empty;
|
||||
}
|
||||
else if (invoice?.StoreId != storeId)
|
||||
{
|
||||
|
@ -7,6 +7,7 @@ using System.Text.Encodings.Web;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Services;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@ -58,14 +59,12 @@ namespace BTCPayServer.Security.Greenfield
|
||||
return AuthenticateResult.NoResult();
|
||||
|
||||
var key = await _apiKeyRepository.GetKey(apiKey, true);
|
||||
|
||||
if (key == null || await _userManager.IsLockedOutAsync(key.User))
|
||||
if (!UserService.TryCanLogin(key?.User, out var error))
|
||||
{
|
||||
return AuthenticateResult.Fail("ApiKey authentication failed");
|
||||
return AuthenticateResult.Fail($"ApiKey authentication failed: {error}");
|
||||
}
|
||||
List<Claim> claims = new List<Claim>();
|
||||
claims.Add(new Claim(_identityOptions.CurrentValue.ClaimsIdentity.UserIdClaimType, key.UserId));
|
||||
|
||||
var claims = new List<Claim> { new (_identityOptions.CurrentValue.ClaimsIdentity.UserIdClaimType, key.UserId) };
|
||||
claims.AddRange((await _userManager.GetRolesAsync(key.User)).Select(s => new Claim(_identityOptions.CurrentValue.ClaimsIdentity.RoleClaimType, s)));
|
||||
claims.AddRange(Permission.ToPermissions(key.GetBlob()?.Permissions ?? Array.Empty<string>()).Select(permission =>
|
||||
new Claim(GreenfieldConstants.ClaimTypes.Permission, permission.ToString())));
|
||||
|
@ -7,6 +7,7 @@ using System.Text.Encodings.Web;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Services;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
@ -66,6 +67,10 @@ namespace BTCPayServer.Security.Greenfield
|
||||
.FirstOrDefaultAsync(applicationUser =>
|
||||
applicationUser.NormalizedUserName == _userManager.NormalizeName(username));
|
||||
|
||||
if (!UserService.TryCanLogin(user, out var error))
|
||||
{
|
||||
return AuthenticateResult.Fail($"Basic authentication failed: {error}");
|
||||
}
|
||||
if (user.Fido2Credentials.Any())
|
||||
{
|
||||
return AuthenticateResult.Fail("Cannot use Basic authentication with multi-factor is enabled.");
|
||||
|
@ -371,10 +371,26 @@ namespace BTCPayServer.Services.Apps
|
||||
return null;
|
||||
await using var ctx = _ContextFactory.CreateContext();
|
||||
var app = await ctx.UserStore
|
||||
.Include(store => store.StoreRole)
|
||||
.Where(us => us.ApplicationUserId == userId && us.StoreRole.Permissions.Contains(Policies.CanModifyStoreSettings))
|
||||
.SelectMany(us => us.StoreData.Apps.Where(a => a.Id == appId))
|
||||
.FirstOrDefaultAsync();
|
||||
.Include(store => store.StoreRole)
|
||||
.Where(us => us.ApplicationUserId == userId && us.StoreRole.Permissions.Contains(Policies.CanModifyStoreSettings))
|
||||
.SelectMany(us => us.StoreData.Apps.Where(a => a.Id == appId))
|
||||
.FirstOrDefaultAsync();
|
||||
if (app == null)
|
||||
return null;
|
||||
if (type != null && type != app.AppType)
|
||||
return null;
|
||||
return app;
|
||||
}
|
||||
|
||||
public async Task<AppData?> GetAppData(string userId, string appId, string? type = null)
|
||||
{
|
||||
if (userId == null || appId == null)
|
||||
return null;
|
||||
await using var ctx = _ContextFactory.CreateContext();
|
||||
var app = await ctx.UserStore
|
||||
.Where(us => us.ApplicationUserId == userId && us.StoreData != null && us.StoreData.UserStores.Any(u => u.ApplicationUserId == userId))
|
||||
.SelectMany(us => us.StoreData.Apps.Where(a => a.Id == appId))
|
||||
.FirstOrDefaultAsync();
|
||||
if (app == null)
|
||||
return null;
|
||||
if (type != null && type != app.AppType)
|
||||
|
@ -0,0 +1,57 @@
|
||||
using BTCPayServer.Abstractions.Contracts;
|
||||
using BTCPayServer.Configuration;
|
||||
using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Data;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
|
||||
namespace BTCPayServer.Services.Notifications.Blobs;
|
||||
|
||||
internal class NewUserRequiresApprovalNotification : BaseNotification
|
||||
{
|
||||
private const string TYPE = "newuserrequiresapproval";
|
||||
public string UserId { get; set; }
|
||||
public string UserEmail { get; set; }
|
||||
public override string Identifier => TYPE;
|
||||
public override string NotificationType => TYPE;
|
||||
|
||||
public NewUserRequiresApprovalNotification()
|
||||
{
|
||||
}
|
||||
|
||||
public NewUserRequiresApprovalNotification(ApplicationUser user)
|
||||
{
|
||||
UserId = user.Id;
|
||||
UserEmail = user.Email;
|
||||
}
|
||||
|
||||
internal class Handler : NotificationHandler<NewUserRequiresApprovalNotification>
|
||||
{
|
||||
private readonly LinkGenerator _linkGenerator;
|
||||
private readonly BTCPayServerOptions _options;
|
||||
|
||||
public Handler(LinkGenerator linkGenerator, BTCPayServerOptions options)
|
||||
{
|
||||
_linkGenerator = linkGenerator;
|
||||
_options = options;
|
||||
}
|
||||
|
||||
public override string NotificationType => TYPE;
|
||||
public override (string identifier, string name)[] Meta
|
||||
{
|
||||
get
|
||||
{
|
||||
return [(TYPE, "New user requires approval")];
|
||||
}
|
||||
}
|
||||
|
||||
protected override void FillViewModel(NewUserRequiresApprovalNotification notification, NotificationViewModel vm)
|
||||
{
|
||||
vm.Identifier = notification.Identifier;
|
||||
vm.Type = notification.NotificationType;
|
||||
vm.Body = $"New user {notification.UserEmail} requires approval.";
|
||||
vm.ActionLink = _linkGenerator.GetPathByAction(nameof(UIServerController.User),
|
||||
"UIServer",
|
||||
new { userId = notification.UserId }, _options.RootPath);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Validation;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
@ -14,6 +14,18 @@ namespace BTCPayServer.Services
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
|
||||
[Display(Name = "Disable new user registration on the server")]
|
||||
public bool LockSubscription { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
[Display(Name = "Enable new user registration on the server")]
|
||||
public bool EnableRegistration
|
||||
{
|
||||
get => !LockSubscription;
|
||||
set { LockSubscription = !value; }
|
||||
}
|
||||
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
|
||||
[Display(Name = "Require new users to be approved by an admin after registration")]
|
||||
public bool RequiresUserApproval { get; set; }
|
||||
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
|
||||
[Display(Name = "Discourage search engines from indexing this site")]
|
||||
@ -30,8 +42,24 @@ namespace BTCPayServer.Services
|
||||
public bool DisableInstantNotifications { get; set; }
|
||||
[Display(Name = "Disable stores from using the server's email settings as backup")]
|
||||
public bool DisableStoresToUseServerEmailSettings { get; set; }
|
||||
[JsonIgnore]
|
||||
[Display(Name = "Allow stores to use the server's SMTP email settings as a default")]
|
||||
public bool EnableStoresToUseServerEmailSettings
|
||||
{
|
||||
get => !DisableStoresToUseServerEmailSettings;
|
||||
set { DisableStoresToUseServerEmailSettings = !value; }
|
||||
}
|
||||
|
||||
[Display(Name = "Disable non-admins access to the user creation API endpoint")]
|
||||
public bool DisableNonAdminCreateUserApi { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
[Display(Name = "Non-admins can access the user creation API endpoint")]
|
||||
public bool EnableNonAdminCreateUserApi
|
||||
{
|
||||
get => !DisableNonAdminCreateUserApi;
|
||||
set { DisableNonAdminCreateUserApi = !value; }
|
||||
}
|
||||
|
||||
public const string DefaultPluginSource = "https://plugin-builder.btcpayserver.org";
|
||||
[UriAttribute]
|
||||
|
@ -1,10 +1,13 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Events;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Storage.Services;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
@ -20,6 +23,7 @@ namespace BTCPayServer.Services
|
||||
private readonly StoredFileRepository _storedFileRepository;
|
||||
private readonly FileService _fileService;
|
||||
private readonly StoreRepository _storeRepository;
|
||||
private readonly EventAggregator _eventAggregator;
|
||||
private readonly ApplicationDbContextFactory _applicationDbContextFactory;
|
||||
private readonly ILogger<UserService> _logger;
|
||||
|
||||
@ -27,6 +31,7 @@ namespace BTCPayServer.Services
|
||||
IServiceProvider serviceProvider,
|
||||
StoredFileRepository storedFileRepository,
|
||||
FileService fileService,
|
||||
EventAggregator eventAggregator,
|
||||
StoreRepository storeRepository,
|
||||
ApplicationDbContextFactory applicationDbContextFactory,
|
||||
ILogger<UserService> logger)
|
||||
@ -34,6 +39,7 @@ namespace BTCPayServer.Services
|
||||
_serviceProvider = serviceProvider;
|
||||
_storedFileRepository = storedFileRepository;
|
||||
_fileService = fileService;
|
||||
_eventAggregator = eventAggregator;
|
||||
_storeRepository = storeRepository;
|
||||
_applicationDbContextFactory = applicationDbContextFactory;
|
||||
_logger = logger;
|
||||
@ -46,26 +52,89 @@ namespace BTCPayServer.Services
|
||||
(userRole, role) => role.Name).ToArray()))).ToListAsync();
|
||||
}
|
||||
|
||||
|
||||
public static ApplicationUserData FromModel(ApplicationUser data, string?[] roles)
|
||||
{
|
||||
return new ApplicationUserData()
|
||||
return new ApplicationUserData
|
||||
{
|
||||
Id = data.Id,
|
||||
Email = data.Email,
|
||||
EmailConfirmed = data.EmailConfirmed,
|
||||
RequiresEmailConfirmation = data.RequiresEmailConfirmation,
|
||||
Approved = data.Approved,
|
||||
RequiresApproval = data.RequiresApproval,
|
||||
Created = data.Created,
|
||||
Roles = roles,
|
||||
Disabled = data.LockoutEnabled && data.LockoutEnd is not null && DateTimeOffset.UtcNow < data.LockoutEnd.Value.UtcDateTime
|
||||
};
|
||||
}
|
||||
|
||||
private bool IsDisabled(ApplicationUser user)
|
||||
private static bool IsEmailConfirmed(ApplicationUser user)
|
||||
{
|
||||
return user.EmailConfirmed || !user.RequiresEmailConfirmation;
|
||||
}
|
||||
|
||||
private static bool IsApproved(ApplicationUser user)
|
||||
{
|
||||
return user.Approved || !user.RequiresApproval;
|
||||
}
|
||||
|
||||
private static bool IsDisabled(ApplicationUser user)
|
||||
{
|
||||
return user.LockoutEnabled && user.LockoutEnd is not null &&
|
||||
DateTimeOffset.UtcNow < user.LockoutEnd.Value.UtcDateTime;
|
||||
}
|
||||
|
||||
public static bool TryCanLogin([NotNullWhen(true)] ApplicationUser? user, [MaybeNullWhen(true)] out string error)
|
||||
{
|
||||
error = null;
|
||||
if (user == null)
|
||||
{
|
||||
error = "Invalid login attempt.";
|
||||
return false;
|
||||
}
|
||||
if (!IsEmailConfirmed(user))
|
||||
{
|
||||
error = "You must have a confirmed email to log in.";
|
||||
return false;
|
||||
}
|
||||
if (!IsApproved(user))
|
||||
{
|
||||
error = "Your user account requires approval by an admin before you can log in.";
|
||||
return false;
|
||||
}
|
||||
if (IsDisabled(user))
|
||||
{
|
||||
error = "Your user account is currently disabled.";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<bool> SetUserApproval(string userId, bool approved, Uri requestUri)
|
||||
{
|
||||
using var scope = _serviceProvider.CreateScope();
|
||||
var userManager = scope.ServiceProvider.GetRequiredService<UserManager<ApplicationUser>>();
|
||||
var user = await userManager.FindByIdAsync(userId);
|
||||
if (user is null || !user.RequiresApproval || user.Approved == approved)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
user.Approved = approved;
|
||||
var succeeded = await userManager.UpdateAsync(user) is { Succeeded: true };
|
||||
if (succeeded)
|
||||
{
|
||||
_logger.LogInformation("User {UserId} is now {Status}", user.Id, approved ? "approved" : "unapproved");
|
||||
_eventAggregator.Publish(new UserApprovedEvent { User = user, Approved = approved, RequestUri = requestUri });
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogError("Failed to {Action} user {UserId}", approved ? "approve" : "unapprove", user.Id);
|
||||
}
|
||||
|
||||
return succeeded;
|
||||
}
|
||||
|
||||
public async Task<bool?> ToggleUser(string userId, DateTimeOffset? lockedOutDeadline)
|
||||
{
|
||||
using var scope = _serviceProvider.CreateScope();
|
||||
@ -163,7 +232,6 @@ namespace BTCPayServer.Services
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public async Task<bool> IsUserTheOnlyOneAdmin(ApplicationUser user)
|
||||
{
|
||||
using var scope = _serviceProvider.CreateScope();
|
||||
@ -175,7 +243,7 @@ namespace BTCPayServer.Services
|
||||
}
|
||||
var adminUsers = await userManager.GetUsersInRoleAsync(Roles.ServerAdmin);
|
||||
var enabledAdminUsers = adminUsers
|
||||
.Where(applicationUser => !IsDisabled(applicationUser))
|
||||
.Where(applicationUser => !IsDisabled(applicationUser) && IsApproved(applicationUser))
|
||||
.Select(applicationUser => applicationUser.Id).ToList();
|
||||
|
||||
return enabledAdminUsers.Count == 1 && enabledAdminUsers.Contains(user.Id);
|
||||
|
@ -487,6 +487,7 @@ namespace BTCPayServer.Services
|
||||
|
||||
public static WalletObjectData NewWalletObjectData(WalletObjectId id, JObject? data = null)
|
||||
{
|
||||
|
||||
return new WalletObjectData()
|
||||
{
|
||||
WalletId = id.WalletId.ToString(),
|
||||
|
@ -49,7 +49,7 @@
|
||||
<div class="inputWithIcon _copyInput">
|
||||
<input type="text" class="checkoutTextbox" v-bind:value="srvModel.invoiceBitcoinUrl" readonly="readonly"/>
|
||||
<img v-bind:src="srvModel.cryptoImage" v-if="hasPayjoin"/>
|
||||
<i class="fa fa-user-secret" v-else/>
|
||||
<i class="fa fa-user-secret" v-else/>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
@ -1,27 +1,5 @@
|
||||
@model BTCPayServer.Models.ServerViewModels.EmailsViewModel
|
||||
|
||||
<div class="row">
|
||||
<div class="col-xl-10 col-xxl-constrain">
|
||||
<div class="d-flex flex-wrap gap-3 align-items-center justify-content-between mt-n1 mb-4">
|
||||
<h3 class="mb-0">Email Server</h3>
|
||||
<div class="d-flex">
|
||||
<div class="dropdown only-for-js" id="quick-fill">
|
||||
<button class="btn btn-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false" id="QuickFillDropdownToggle">
|
||||
Quick Fill
|
||||
</button>
|
||||
<div class="dropdown-menu" aria-labelledby="QuickFillDropdownToggle">
|
||||
<a class="dropdown-item" href="" data-server="smtp.gmail.com" data-port="587">Gmail.com</a>
|
||||
<a class="dropdown-item" href="" data-server="mail.yahoo.com" data-port="587">Yahoo.com</a>
|
||||
<a class="dropdown-item" href="" data-server="smtp.mailgun.org" data-port="587">Mailgun</a>
|
||||
<a class="dropdown-item" href="" data-server="smtp.office365.com" data-port="587">Office365</a>
|
||||
<a class="dropdown-item" href="" data-server="smtp.sendgrid.net" data-port="587">SendGrid</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="post" autocomplete="off">
|
||||
<div class="row">
|
||||
<div class="col-xl-10 col-xxl-constrain">
|
||||
@ -30,8 +8,22 @@
|
||||
<div asp-validation-summary="All"></div>
|
||||
}
|
||||
<div class="form-group">
|
||||
<label asp-for="Settings.Server" class="form-label">SMTP Server</label>
|
||||
<input asp-for="Settings.Server" data-fill="server" class="form-control"/>
|
||||
<div class="d-flex flex-wrap gap-2 align-items-center justify-content-between">
|
||||
<label asp-for="Settings.Server" class="form-label">SMTP Server</label>
|
||||
<div class="dropdown only-for-js mt-n2" id="quick-fill">
|
||||
<button class="btn btn-link p-0 dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false" id="QuickFillDropdownToggle">
|
||||
Quick Fill
|
||||
</button>
|
||||
<div class="dropdown-menu" aria-labelledby="QuickFillDropdownToggle">
|
||||
<a class="dropdown-item" href="" data-server="smtp.gmail.com" data-port="587">Gmail.com</a>
|
||||
<a class="dropdown-item" href="" data-server="mail.yahoo.com" data-port="587">Yahoo.com</a>
|
||||
<a class="dropdown-item" href="" data-server="smtp.mailgun.org" data-port="587">Mailgun</a>
|
||||
<a class="dropdown-item" href="" data-server="smtp.office365.com" data-port="587">Office365</a>
|
||||
<a class="dropdown-item" href="" data-server="smtp.sendgrid.net" data-port="587">SendGrid</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<input asp-for="Settings.Server" data-fill="server" class="form-control" />
|
||||
<span asp-validation-for="Settings.Server" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
@ -53,7 +45,6 @@
|
||||
<div class="form-group">
|
||||
@if (!Model.PasswordSet)
|
||||
{
|
||||
|
||||
<label asp-for="Settings.Password" class="form-label"></label>
|
||||
<input asp-for="Settings.Password" type="password" class="form-control"/>
|
||||
<span asp-validation-for="Settings.Password" class="text-danger"></span>
|
||||
|
@ -18,8 +18,11 @@
|
||||
@RenderBody()
|
||||
</div>
|
||||
</section>
|
||||
<partial name="_Footer"/>
|
||||
<partial name="LayoutFoot" />
|
||||
@await RenderSectionAsync("PageFootContent", false)
|
||||
@if (ViewData["ShowFooter"] is not false)
|
||||
{
|
||||
<partial name="_Footer"/>
|
||||
}
|
||||
<partial name="LayoutFoot" />
|
||||
@await RenderSectionAsync("PageFootContent", false)
|
||||
</body>
|
||||
</html>
|
||||
|
@ -12,6 +12,7 @@
|
||||
@await RenderSectionAsync("PageFootContent", false)
|
||||
}
|
||||
|
||||
|
||||
<nav id="wizard-navbar">
|
||||
@await RenderSectionAsync("Navbar", false)
|
||||
</nav>
|
||||
|
@ -2,8 +2,8 @@
|
||||
@using BTCPayServer.Abstractions.Contracts
|
||||
@model (string Title, StoreBrandingViewModel StoreBranding)
|
||||
@{
|
||||
var logoUrl = !string.IsNullOrEmpty(Model.StoreBranding.LogoFileId)
|
||||
? await FileService.GetFileUrl(Context.Request.GetAbsoluteRootUri(), Model.StoreBranding.LogoFileId)
|
||||
var logoUrl = !string.IsNullOrEmpty(Model.StoreBranding?.LogoFileId)
|
||||
? await FileService.GetFileUrl(Context.Request.GetAbsoluteRootUri(), Model.StoreBranding?.LogoFileId)
|
||||
: null;
|
||||
}
|
||||
<header class="store-header" v-pre>
|
||||
|
@ -10,7 +10,7 @@
|
||||
</p>
|
||||
|
||||
<form asp-action="ForgotPassword" method="post">
|
||||
<div asp-validation-summary="All"></div>
|
||||
<div asp-validation-summary="All" class="@(ViewContext.ModelState.ErrorCount.Equals(1) ? "no-marker" : "")"></div>
|
||||
<div class="form-group">
|
||||
<label asp-for="Email" class="form-label"></label>
|
||||
<input asp-for="Email" class="form-control" />
|
||||
|
@ -1,5 +1,4 @@
|
||||
@using BTCPayServer.Abstractions.Extensions
|
||||
@model DateTimeOffset?
|
||||
@model DateTimeOffset?
|
||||
@{
|
||||
ViewData["Title"] = "Account disabled";
|
||||
Layout = "_LayoutSignedOut";
|
||||
|
@ -9,16 +9,16 @@
|
||||
|
||||
<form asp-route-returnurl="@ViewData["ReturnUrl"]" method="post" id="login-form" asp-action="Login">
|
||||
<fieldset disabled="@(ViewData.ContainsKey("disabled") ? "disabled" : null)">
|
||||
<div asp-validation-summary="ModelOnly"></div>
|
||||
<div asp-validation-summary="ModelOnly" class="@(ViewContext.ModelState.ErrorCount.Equals(1) ? "no-marker" : "")"></div>
|
||||
<div class="form-group">
|
||||
<label asp-for="Email" class="form-label"></label>
|
||||
<input asp-for="Email" class="form-control" required autofocus/>
|
||||
<input asp-for="Email" class="form-control" required autofocus />
|
||||
<span asp-validation-for="Email" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="d-flex justify-content-between">
|
||||
<label asp-for="Password" class="form-label"></label>
|
||||
<a asp-action="ForgotPassword" >Forgot password?</a>
|
||||
<a asp-action="ForgotPassword" tabindex="-1">Forgot password?</a>
|
||||
</div>
|
||||
<div class="input-group d-flex">
|
||||
<input asp-for="Password" class="form-control" required />
|
||||
|
@ -8,7 +8,7 @@
|
||||
|
||||
<form asp-route-returnUrl="@ViewData["ReturnUrl"]" asp-route-logon="true" method="post">
|
||||
<fieldset disabled="@(ViewData.ContainsKey("disabled") ? "disabled" : null)" >
|
||||
<div asp-validation-summary="ModelOnly"></div>
|
||||
<div asp-validation-summary="ModelOnly" class="@(ViewContext.ModelState.ErrorCount.Equals(1) ? "no-marker" : "")"></div>
|
||||
<div class="form-group">
|
||||
<label asp-for="Email" class="form-label"></label>
|
||||
<input asp-for="Email" class="form-control" required autofocus />
|
||||
|
@ -13,9 +13,9 @@
|
||||
<partial name="_ValidationScriptsPartial" />
|
||||
}
|
||||
|
||||
@if (Model.LoginWith2FaViewModel != null && Model.LoginWithFido2ViewModel != null&& Model.LoginWithLNURLAuthViewModel != null)
|
||||
@if (Model.LoginWith2FaViewModel != null && Model.LoginWithFido2ViewModel != null && Model.LoginWithLNURLAuthViewModel != null)
|
||||
{
|
||||
<div asp-validation-summary="ModelOnly"></div>
|
||||
<div asp-validation-summary="ModelOnly" class="@(ViewContext.ModelState.ErrorCount.Equals(1) ? "no-marker" : "")"></div>
|
||||
}
|
||||
else if (Model.LoginWith2FaViewModel == null && Model.LoginWithFido2ViewModel == null && Model.LoginWithLNURLAuthViewModel == null)
|
||||
{
|
||||
|
@ -5,7 +5,7 @@
|
||||
}
|
||||
|
||||
<form method="post" asp-action="SetPassword">
|
||||
<div asp-validation-summary="All"></div>
|
||||
<div asp-validation-summary="All" class="@(ViewContext.ModelState.ErrorCount.Equals(1) ? "no-marker" : "")"></div>
|
||||
<input asp-for="Code" type="hidden"/>
|
||||
<input asp-for="EmailSetInternally" type="hidden"/>
|
||||
@if (Model.EmailSetInternally)
|
||||
|
@ -30,11 +30,6 @@
|
||||
<partial name="LayoutHead"/>
|
||||
<link href="~/vendor/bootstrap-vue/bootstrap-vue.min.css" asp-append-version="true" rel="stylesheet"/>
|
||||
<link href="~/vendor/vue-qrcode-reader/vue-qrcode-reader.css" rel="stylesheet" asp-append-version="true"/>
|
||||
<style>
|
||||
.no-marker > ul {
|
||||
list-style-type: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="min-vh-100">
|
||||
<div id="app" class="d-flex flex-column min-vh-100 pb-l">
|
||||
@ -207,15 +202,15 @@
|
||||
</p>
|
||||
@if (Model.LnurlEndpoint is not null)
|
||||
{
|
||||
<p>
|
||||
<a asp-action="SetupBoltcard" asp-controller="UIPullPayment" asp-route-pullPaymentId="@Model.Id" asp-route-command="configure-boltcard">
|
||||
Setup Boltcard
|
||||
</a>
|
||||
<span> | </span>
|
||||
<a asp-action="SetupBoltcard" asp-controller="UIPullPayment" asp-route-pullPaymentId="@Model.Id" asp-route-command="reset-boltcard">
|
||||
Reset Boltcard
|
||||
</a>
|
||||
</p>
|
||||
<p id="BoltcardActions" style="visibility:hidden">
|
||||
<a id="SetupBoltcard" asp-action="SetupBoltcard" asp-controller="UIPullPayment" asp-route-pullPaymentId="@Model.Id" asp-route-command="configure-boltcard">
|
||||
Setup Boltcard
|
||||
</a>
|
||||
<span> | </span>
|
||||
<a id="ResetBoltcard" asp-action="SetupBoltcard" asp-controller="UIPullPayment" asp-route-pullPaymentId="@Model.Id" asp-route-command="reset-boltcard">
|
||||
Reset Boltcard
|
||||
</a>
|
||||
</p>
|
||||
}
|
||||
<a class="store-powered-by" href="https://btcpayserver.org" target="_blank" rel="noreferrer noopener">
|
||||
Powered by <partial name="_StoreFooterLogo" />
|
||||
@ -231,6 +226,15 @@
|
||||
<script src="~/vendor/ur-registry/urlib.min.js" asp-append-version="true"></script>
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
var isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
|
||||
if (isMobile) {
|
||||
document.getElementById("SetupBoltcard").setAttribute('target', '_blank');
|
||||
document.getElementById("SetupBoltcard").setAttribute('href', @Safe.Json(@Model.SetupDeepLink));
|
||||
document.getElementById("ResetBoltcard").setAttribute('target', '_blank');
|
||||
document.getElementById("ResetBoltcard").setAttribute('href', @Safe.Json(@Model.ResetDeepLink));
|
||||
}
|
||||
document.getElementById("BoltcardActions").style.visibility = "visible";
|
||||
|
||||
window.qrApp = initQRShow({});
|
||||
delegate('click', 'button[page-qr]', event => {
|
||||
qrApp.title = "Pull Payment QR";
|
||||
|
@ -36,8 +36,6 @@
|
||||
<span asp-validation-for="IsAdmin" class="text-danger"></span>
|
||||
</div>
|
||||
}
|
||||
|
||||
|
||||
@if (ViewData["AllowRequestEmailConfirmation"] is true)
|
||||
{
|
||||
<div class="form-group form-check">
|
||||
@ -46,8 +44,16 @@
|
||||
<span asp-validation-for="EmailConfirmed" class="text-danger"></span>
|
||||
</div>
|
||||
}
|
||||
@if (ViewData["AllowRequestApproval"] is true)
|
||||
{
|
||||
<div class="form-group form-check">
|
||||
<input asp-for="Approved" type="checkbox" class="form-check-input"/>
|
||||
<label asp-for="Approved" class="form-check-label"></label>
|
||||
<span asp-validation-for="Approved" class="text-danger"></span>
|
||||
</div>
|
||||
}
|
||||
|
||||
<button id="Save" type="submit" class="btn btn-primary mt-2" name="command" value="Save">Create account</button>
|
||||
<button id="Save" type="submit" class="btn btn-primary mt-2" name="command" value="Save">Create account</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -3,6 +3,7 @@
|
||||
ViewData.SetActivePage(ServerNavPages.Emails, "Emails");
|
||||
}
|
||||
|
||||
<h3 class="mb-4">Email Server</h3>
|
||||
<partial name="EmailsBody" model="Model" />
|
||||
|
||||
@section PageFootContent {
|
||||
|
@ -3,16 +3,12 @@
|
||||
@{
|
||||
ViewData.SetActivePage(ServerNavPages.Users);
|
||||
var nextUserEmailSortOrder = (string)ViewData["NextUserEmailSortOrder"];
|
||||
String userEmailSortOrder = null;
|
||||
switch (nextUserEmailSortOrder)
|
||||
var userEmailSortOrder = nextUserEmailSortOrder switch
|
||||
{
|
||||
case "asc":
|
||||
userEmailSortOrder = "desc";
|
||||
break;
|
||||
case "desc":
|
||||
userEmailSortOrder = "asc";
|
||||
break;
|
||||
}
|
||||
"asc" => "desc",
|
||||
"desc" => "asc",
|
||||
_ => null
|
||||
};
|
||||
|
||||
var sortIconClass = "fa-sort";
|
||||
if (userEmailSortOrder != null)
|
||||
@ -20,8 +16,8 @@
|
||||
sortIconClass = $"fa-sort-alpha-{userEmailSortOrder}";
|
||||
}
|
||||
|
||||
var sortByDesc = "Sort by descending...";
|
||||
var sortByAsc = "Sort by ascending...";
|
||||
const string sortByDesc = "Sort by descending...";
|
||||
const string sortByAsc = "Sort by ascending...";
|
||||
}
|
||||
|
||||
<div class="d-flex align-items-center justify-content-between mb-3">
|
||||
@ -31,14 +27,8 @@
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<form asp-action="ListUsers" asp-route-sortOrder="@(userEmailSortOrder)" style="max-width:640px">
|
||||
<div class="input-group">
|
||||
<input asp-for="SearchTerm" class="form-control" placeholder="Search by email..." />
|
||||
<button type="submit" class="btn btn-secondary" title="Search by email">
|
||||
<span class="fa fa-search"></span> Search
|
||||
</button>
|
||||
</div>
|
||||
<span asp-validation-for="SearchTerm" class="text-danger"></span>
|
||||
<form asp-action="ListUsers" asp-route-sortOrder="@(userEmailSortOrder)" style="max-width:640px" method="get">
|
||||
<input asp-for="SearchTerm" class="form-control" placeholder="Search by email..." />
|
||||
</form>
|
||||
|
||||
<div class="table-responsive">
|
||||
@ -53,58 +43,52 @@
|
||||
title="@(nextUserEmailSortOrder == "desc" ? sortByAsc : sortByDesc)"
|
||||
>
|
||||
Email
|
||||
<span class="fa @(sortIconClass)" />
|
||||
<span class="fa @(sortIconClass)"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th >Created</th>
|
||||
<th class="text-center">Verified</th>
|
||||
<th class="text-center">Enabled</th>
|
||||
<th class="text-end">Actions</th>
|
||||
<th>Created</th>
|
||||
<th>Status</th>
|
||||
<th class="actions-col"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tbody id="UsersList">
|
||||
@foreach (var user in Model.Users)
|
||||
{
|
||||
var status = user switch
|
||||
{
|
||||
{ Disabled: true } => ("Disabled", "danger"),
|
||||
{ Approved: false } => ("Pending Approval", "warning"),
|
||||
{ EmailConfirmed: false } => ("Pending Email Verification", "warning"),
|
||||
_ => ("Active", "success")
|
||||
};
|
||||
<tr>
|
||||
<td class="d-flex align-items-center">
|
||||
<span class="me-2">@user.Email</span>
|
||||
<td class="d-flex align-items-center gap-2">
|
||||
<span class="user-email">@user.Email</span>
|
||||
@foreach (var role in user.Roles)
|
||||
{
|
||||
<span class="badge bg-info">@Model.Roles[role]</span>
|
||||
}
|
||||
</td>
|
||||
<td>@user.Created?.ToBrowserDate()</td>
|
||||
<td class="text-center">
|
||||
@if (user.Verified)
|
||||
{
|
||||
<span class="text-success fa fa-check"></span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="text-danger fa fa-times"></span>
|
||||
}
|
||||
<td>
|
||||
<span class="user-status badge bg-@status.Item2">@status.Item1</span>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
@if (!user.Disabled)
|
||||
{
|
||||
<span class="text-success fa fa-check" title="User is enabled"></span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="text-danger fa fa-times" title="User is disabled"></span>
|
||||
}
|
||||
</td>
|
||||
<td class="text-end">
|
||||
@if (!user.Verified && !user.Disabled) {
|
||||
<a asp-action="SendVerificationEmail" asp-route-userId="@user.Id" data-bs-toggle="modal" data-bs-target="#ConfirmModal" data-description="This will send a verification email to <strong>@Html.Encode(user.Email)</strong>.">Resend verification email</a>
|
||||
<span>-</span>
|
||||
}
|
||||
<a asp-action="User" asp-route-userId="@user.Id">Edit</a> <span> - </span> <a asp-action="DeleteUser" asp-route-userId="@user.Id">Remove</a>
|
||||
- <a asp-action="ToggleUser"
|
||||
asp-route-enable="@user.Disabled"
|
||||
asp-route-userId="@user.Id">
|
||||
@(user.Disabled ? "Enable" : "Disable")
|
||||
</a>
|
||||
<td class="actions-col">
|
||||
<div class="d-inline-flex align-items-center gap-3">
|
||||
@if (user is { EmailConfirmed: false, Disabled: false }) {
|
||||
<a asp-action="SendVerificationEmail" asp-route-userId="@user.Id" data-bs-toggle="modal" data-bs-target="#ConfirmModal" data-title="Send verification email" data-description="This will send a verification email to <strong>@Html.Encode(user.Email)</strong>." data-confirm="Send">Resend email</a>
|
||||
}
|
||||
else if (user is { Approved: false, Disabled: false })
|
||||
{
|
||||
<a asp-action="ApproveUser" asp-route-userId="@user.Id" asp-route-approved="true" data-bs-toggle="modal" data-bs-target="#ConfirmModal" data-title="Approve user" data-description="This will approve the user <strong>@Html.Encode(user.Email)</strong>." data-confirm="Approve">Approve</a>
|
||||
}
|
||||
else
|
||||
{
|
||||
<a asp-action="ToggleUser" asp-route-userId="@user.Id" asp-route-enable="@user.Disabled">@(user.Disabled ? "Enable" : "Disable")</a>
|
||||
}
|
||||
<a asp-action="User" asp-route-userId="@user.Id" class="user-edit">Edit</a>
|
||||
<a asp-action="DeleteUser" asp-route-userId="@user.Id">Remove</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
|
@ -11,15 +11,10 @@
|
||||
|
||||
@section PageHeadContent {
|
||||
<style>
|
||||
#AllowLightningInternalNodeForAll ~ .info-note,
|
||||
#AllowHotWalletRPCImportForAll ~ .info-note,
|
||||
#AllowHotWalletForAll ~ .info-note,
|
||||
#DisableNonAdminCreateUserApi:checked ~ .info-note,
|
||||
#LockSubscription:checked ~ .info-note { display: none; }
|
||||
|
||||
#AllowLightningInternalNodeForAll:checked ~ .info-note,
|
||||
#AllowHotWalletRPCImportForAll:checked ~ .info-note,
|
||||
#AllowHotWalletForAll:checked ~ .info-note { display: inline-flex; }
|
||||
input[type="checkbox"] ~ .info-note,
|
||||
input[type="checkbox"] ~ .subsettings { display: none; }
|
||||
input[type="checkbox"]:checked ~ .info-note { display: flex; max-width: 44em; }
|
||||
input[type="checkbox"]:checked ~ .subsettings { display: block; }
|
||||
</style>
|
||||
}
|
||||
|
||||
@ -31,10 +26,59 @@
|
||||
}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-xl-8 col-xxl-constrain">
|
||||
<div class="col-xl-10 col-xxl-constrain">
|
||||
<form method="post" class="d-flex flex-column">
|
||||
<div class="form-group mb-5">
|
||||
<h4 class="mb-3">Existing User Settings</h4>
|
||||
<h4 class="mb-3">Registration Settings</h4>
|
||||
<div class="form-check my-3">
|
||||
<input asp-for="EnableRegistration" type="checkbox" class="form-check-input"/>
|
||||
<label asp-for="EnableRegistration" class="form-check-label"></label>
|
||||
<span asp-validation-for="EnableRegistration" class="text-danger"></span>
|
||||
<div class="info-note mt-2 text-warning" role="alert">
|
||||
<vc:icon symbol="warning"/>
|
||||
Caution: Enabling public user registration means anyone can register to your server and may expose your BTCPay Server instance to potential security risks from unknown users.
|
||||
</div>
|
||||
<div class="subsettings">
|
||||
<div class="form-check my-3">
|
||||
@{
|
||||
var emailSettings = (await _SettingsRepository.GetSettingAsync<EmailSettings>()) ?? new EmailSettings();
|
||||
/* The "|| Model.RequiresConfirmedEmail" check is for the case when a user had checked
|
||||
the checkbox without first configuring the e-mail settings so that they can uncheck it. */
|
||||
var isEmailConfigured = emailSettings.IsComplete() || Model.RequiresConfirmedEmail;
|
||||
}
|
||||
<input asp-for="RequiresConfirmedEmail" type="checkbox" class="form-check-input" disabled="@(isEmailConfigured ? null : "disabled")"/>
|
||||
<label asp-for="RequiresConfirmedEmail" class="form-check-label"></label>
|
||||
<a href="https://docs.btcpayserver.org/FAQ/ServerSettings/#how-to-allow-registration-on-my-btcpay-server" target="_blank" rel="noreferrer noopener">
|
||||
<vc:icon symbol="info" />
|
||||
</a>
|
||||
<span asp-validation-for="RequiresConfirmedEmail" class="text-danger"></span>
|
||||
@if (!isEmailConfigured)
|
||||
{
|
||||
<div class="mb-2">
|
||||
<span class="text-secondary">Your email server has not been configured. <a asp-controller="UIServer" asp-action="Emails">Please configure it first.</a></span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="form-check my-3">
|
||||
<input asp-for="RequiresUserApproval" type="checkbox" class="form-check-input"/>
|
||||
<label asp-for="RequiresUserApproval" class="form-check-label"></label>
|
||||
<span asp-validation-for="RequiresUserApproval" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-check my-3">
|
||||
<input asp-for="EnableNonAdminCreateUserApi" type="checkbox" class="form-check-input"/>
|
||||
<label asp-for="EnableNonAdminCreateUserApi" class="form-check-label"></label>
|
||||
<span asp-validation-for="EnableNonAdminCreateUserApi" class="text-danger"></span>
|
||||
<div class="info-note mt-2 text-warning" role="alert">
|
||||
<vc:icon symbol="warning"/>
|
||||
Caution: Allowing non-admins to have access to API endpoints may expose your BTCPay Server instance to potential security risks from unknown users.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group mb-5">
|
||||
<h4 class="mb-3">User Settings</h4>
|
||||
<div class="form-check my-3">
|
||||
<input asp-for="AllowLightningInternalNodeForAll" type="checkbox" class="form-check-input"/>
|
||||
<label asp-for="AllowLightningInternalNodeForAll" class="form-check-label"></label>
|
||||
@ -71,45 +115,14 @@
|
||||
</div>
|
||||
|
||||
<div class="form-group mb-5">
|
||||
<h4 class="mb-3">New User Settings</h4>
|
||||
<h4 class="mb-3">Email Settings</h4>
|
||||
<div class="form-check my-3">
|
||||
@{
|
||||
var emailSettings = (await _SettingsRepository.GetSettingAsync<EmailSettings>()) ?? new EmailSettings();
|
||||
/* The "|| Model.RequiresConfirmedEmail" check is for the case when a user had checked
|
||||
the checkbox without first configuring the e-mail settings so that they can uncheck it. */
|
||||
var isEmailConfigured = emailSettings.IsComplete() || Model.RequiresConfirmedEmail;
|
||||
}
|
||||
<input asp-for="RequiresConfirmedEmail" type="checkbox" class="form-check-input" disabled="@(isEmailConfigured ? null : "disabled")"/>
|
||||
<label asp-for="RequiresConfirmedEmail" class="form-check-label"></label>
|
||||
<a href="https://docs.btcpayserver.org/FAQ/ServerSettings/#how-to-allow-registration-on-my-btcpay-server" target="_blank" rel="noreferrer noopener">
|
||||
<input asp-for="EnableStoresToUseServerEmailSettings" type="checkbox" class="form-check-input"/>
|
||||
<label asp-for="EnableStoresToUseServerEmailSettings" class="form-check-label"></label>
|
||||
<a href="https://docs.btcpayserver.org/Notifications/#server-emails" target="_blank" rel="noreferrer noopener">
|
||||
<vc:icon symbol="info" />
|
||||
</a>
|
||||
<span asp-validation-for="RequiresConfirmedEmail" class="text-danger"></span>
|
||||
@if (!isEmailConfigured)
|
||||
{
|
||||
<div class="mb-2">
|
||||
<span class="text-secondary">Your email server has not been configured. <a asp-controller="UIServer" asp-action="Emails">Please configure it first.</a></span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="form-check my-3">
|
||||
<input asp-for="LockSubscription" type="checkbox" class="form-check-input"/>
|
||||
<label asp-for="LockSubscription" class="form-check-label"></label>
|
||||
<span asp-validation-for="LockSubscription" class="text-danger"></span>
|
||||
<div class="info-note mt-2 text-warning" role="alert">
|
||||
<vc:icon symbol="warning"/>
|
||||
Caution: Enabling public user registration means anyone can register to your server and may expose your BTCPay Server instance to potential security risks from unknown users.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-check my-3">
|
||||
<input asp-for="DisableNonAdminCreateUserApi" type="checkbox" class="form-check-input"/>
|
||||
<label asp-for="DisableNonAdminCreateUserApi" class="form-check-label"></label>
|
||||
<span asp-validation-for="DisableNonAdminCreateUserApi" class="text-danger"></span>
|
||||
<div class="info-note mt-2 text-warning" role="alert">
|
||||
<vc:icon symbol="warning"/>
|
||||
Caution: Allowing non-admins to have access to API endpoints may expose your BTCPay Server instance to potential security risks from unknown users.
|
||||
</div>
|
||||
<span asp-validation-for="EnableStoresToUseServerEmailSettings" class="text-danger"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -123,14 +136,6 @@
|
||||
</a>
|
||||
<span asp-validation-for="DisableInstantNotifications" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-check my-3">
|
||||
<input asp-for="DisableStoresToUseServerEmailSettings" type="checkbox" class="form-check-input"/>
|
||||
<label asp-for="DisableStoresToUseServerEmailSettings" class="form-check-label"></label>
|
||||
<a href="https://docs.btcpayserver.org/Notifications/#server-emails" target="_blank" rel="noreferrer noopener">
|
||||
<vc:icon symbol="info" />
|
||||
</a>
|
||||
<span asp-validation-for="DisableStoresToUseServerEmailSettings" class="text-danger"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group mb-5">
|
||||
@ -177,7 +182,7 @@
|
||||
<h4 class="mt-5">Customization Settings</h4>
|
||||
<div class="form-group mb-5">
|
||||
<label asp-for="RootAppId" class="form-label"></label>
|
||||
<select asp-for="RootAppId" asp-items="@(new SelectList(ViewBag.AppsList, nameof(SelectListItem.Value), nameof(SelectListItem.Text), Model.RootAppId))" class="form-select w-auto"></select>
|
||||
<select asp-for="RootAppId" asp-items="@(new SelectList(ViewBag.AppsList, nameof(SelectListItem.Value), nameof(SelectListItem.Text), Model.RootAppId))" class="form-select"></select>
|
||||
@if (!Model.DomainToAppMapping.Any())
|
||||
{
|
||||
<button id="AddDomainButton" type="submit" name="command" value="add-domain" class="btn btn-link px-0">Map specific domains to specific apps</button>
|
||||
@ -210,7 +215,7 @@
|
||||
<label asp-for="DomainToAppMapping[index].AppId" class="form-label"></label>
|
||||
<select asp-for="DomainToAppMapping[index].AppId"
|
||||
asp-items="@(new SelectList(ViewBag.AppsList, nameof(SelectListItem.Value), nameof(SelectListItem.Text), Model.DomainToAppMapping[index].AppId))"
|
||||
class="form-select w-auto">
|
||||
class="form-select">
|
||||
</select>
|
||||
<span asp-validation-for="DomainToAppMapping[index].AppId" class="text-danger"></span>
|
||||
</div>
|
||||
|
@ -5,14 +5,26 @@
|
||||
|
||||
<h3 class="mb-4">@ViewData["Title"]</h3>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<form method="post">
|
||||
<div class="form-group form-check mb-4">
|
||||
<input asp-for="IsAdmin" type="checkbox" class="form-check-input" />
|
||||
<label asp-for="IsAdmin" class="form-check-label">Is admin</label>
|
||||
</div>
|
||||
<button name="command" type="submit" class="btn btn-primary" value="Save">Save</button>
|
||||
</form>
|
||||
<form method="post">
|
||||
<div class="form-check my-3">
|
||||
<input asp-for="IsAdmin" type="checkbox" class="form-check-input" />
|
||||
<label asp-for="IsAdmin" class="form-check-label">User is admin</label>
|
||||
</div>
|
||||
</div>
|
||||
@if (Model.Approved.HasValue)
|
||||
{
|
||||
<div class="form-check my-3">
|
||||
<input id="Approved" name="Approved" type="checkbox" value="true" class="form-check-input" @(Model.Approved.Value ? "checked" : "") />
|
||||
<label for="Approved" class="form-check-label">User is approved</label>
|
||||
</div>
|
||||
<input name="Approved" type="hidden" value="false">
|
||||
}
|
||||
@if (Model.EmailConfirmed.HasValue)
|
||||
{
|
||||
<div class="form-check my-3">
|
||||
<input id="EmailConfirmed" name="EmailConfirmed" value="true" type="checkbox" class="form-check-input" @(Model.EmailConfirmed.Value ? "checked" : "") />
|
||||
<label for="EmailConfirmed" class="form-check-label">Email address is confirmed</label>
|
||||
</div>
|
||||
<input name="EmailConfirmed" type="hidden" value="false">
|
||||
}
|
||||
<button name="command" type="submit" class="btn btn-primary mt-3" value="Save" id="SaveUser">Save</button>
|
||||
</form>
|
||||
|
@ -24,7 +24,9 @@
|
||||
{
|
||||
/* include chart library inline so that it instantly renders */
|
||||
<link rel="stylesheet" href="~/vendor/chartist/chartist.css" asp-append-version="true">
|
||||
<link rel="stylesheet" href="~/vendor/chartist/chartist-plugin-tooltip.css" asp-append-version="true">
|
||||
<script src="~/vendor/chartist/chartist.min.js" asp-append-version="true"></script>
|
||||
<script src="~/vendor/chartist/chartist-plugin-tooltip.js" asp-append-version="true"></script>
|
||||
<script>
|
||||
const DashboardUtils = {
|
||||
toDefaultCurrency(amount, rate) {
|
||||
|
@ -2,6 +2,7 @@
|
||||
@{
|
||||
Layout = "../Shared/_NavLayout.cshtml";
|
||||
ViewData.SetActivePage(StoreNavPages.Emails, "Emails", Context.GetStoreData().Id);
|
||||
var hasCustomSettings = Model.IsSetup() && !Model.UsesFallback();
|
||||
}
|
||||
|
||||
<div class="row mb-4">
|
||||
@ -19,7 +20,25 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<partial name="EmailsBody" model="Model" />
|
||||
<h3 class="mb-4">Email Server</h3>
|
||||
@if (Model.IsFallbackSetup())
|
||||
{
|
||||
<label class="d-flex align-items-center mb-4">
|
||||
<input type="checkbox" id="UseCustomSMTP" checked="@hasCustomSettings" class="btcpay-toggle me-3" data-bs-toggle="collapse" data-bs-target="#SmtpSettings" aria-expanded="@hasCustomSettings" aria-controls="SmtpSettings" />
|
||||
<div>
|
||||
<span>Use custom SMTP settings for this store</span>
|
||||
<div class="form-text">Otherwise, the server's SMTP settings will be used to send emails.</div>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<div class="checkout-settings collapse @(hasCustomSettings ? "show" : "")" id="SmtpSettings">
|
||||
<partial name="EmailsBody" model="Model" />
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<partial name="EmailsBody" model="Model" />
|
||||
}
|
||||
|
||||
@section PageFootContent {
|
||||
<partial name="_ValidationScriptsPartial" />
|
||||
|
@ -117,8 +117,7 @@
|
||||
<code>{Payout.Metadata}*</code>
|
||||
</td>
|
||||
</tr>
|
||||
<tr><td colspan="2">* These fields are JSON objects. You can access properties within them using <a href="https://www.newtonsoft.com/json/help/html/SelectToken.htm#SelectTokenJSONPath" rel="noreferrer noopener" target="_blank">this syntax</a>. One example is <code>{Invoice.Metadata.itemCode}</code>
|
||||
</td></tr>
|
||||
<tr><td colspan="2">* These fields are JSON objects. You can access properties within them using <a href="https://www.newtonsoft.com/json/help/html/SelectToken.htm#SelectTokenJSONPath" rel="noreferrer noopener" target="_blank">this syntax</a>. One example is <code>{Invoice.Metadata.itemCode}</code></td></tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -33,7 +33,7 @@
|
||||
</select>
|
||||
</div>
|
||||
<div class="ms-3">
|
||||
<button type="submit" role="button" class="btn btn-primary">Add User</button>
|
||||
<button type="submit" role="button" class="btn btn-primary" id="AddUser">Add User</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
@ -271,20 +271,7 @@
|
||||
<label asp-for="AlwaysIncludeNonWitnessUTXO" class="form-check-label"></label>
|
||||
</div>
|
||||
</div>
|
||||
@if (Model.SupportRBF)
|
||||
{
|
||||
<div class="form-group">
|
||||
<label asp-for="AllowFeeBump" class="form-label"></label>
|
||||
<a href="https://docs.btcpayserver.org/Wallet/#rbf-replace-by-fee" target="_blank" rel="noreferrer noopener">
|
||||
<vc:icon symbol="info" />
|
||||
</a>
|
||||
<select asp-for="AllowFeeBump" class="form-select w-auto">
|
||||
<option value="Maybe">Randomize for higher privacy</option>
|
||||
<option value="Yes">Yes</option>
|
||||
<option value="No">No</option>
|
||||
</select>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!string.IsNullOrEmpty(Model.PayJoinBIP21))
|
||||
{
|
||||
<div class="form-group">
|
||||
|
@ -42,6 +42,11 @@
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.no-marker > ul {
|
||||
list-style-type: none;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
/* General and site-wide Bootstrap modifications */
|
||||
p {
|
||||
margin-bottom: 1.5rem;
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user