Compare commits
23 Commits
v2.0.4
...
v2.0.5-pay
Author | SHA1 | Date | |
---|---|---|---|
54dd1df55f | |||
6ae36825d5 | |||
f1719ed3d2 | |||
79c5ff9ed0 | |||
76a880a30e | |||
08835895e9 | |||
4ee12b41b1 | |||
4fb43cbbad | |||
adce1dffb1 | |||
0f049eee1b | |||
cfc2b9c236 | |||
637c06fc01 | |||
1ef177ba0f | |||
8acf1c2d62 | |||
44dc6499cd | |||
f5a420a272 | |||
d24e0cd1a2 | |||
b3bc11c19d | |||
fe3bccf3ce | |||
7829a93251 | |||
d4b76823a2 | |||
00cc16455c | |||
6e222c573b |
@ -47,7 +47,6 @@ jobs:
|
||||
docker buildx create --use
|
||||
DOCKER_BUILDX_OPTS="--platform linux/amd64,linux/arm64,linux/arm/v7 --build-arg GIT_COMMIT=${GIT_COMMIT} --push"
|
||||
docker buildx build $DOCKER_BUILDX_OPTS -t $DOCKERHUB_REPO:$LATEST_TAG .
|
||||
docker buildx build $DOCKER_BUILDX_OPTS -t $DOCKERHUB_REPO:$LATEST_TAG-altcoins --build-arg CONFIGURATION_NAME=Altcoins-Release .
|
||||
workflows:
|
||||
version: 2
|
||||
build_and_test:
|
||||
|
@ -106,8 +106,8 @@ public static class HttpRequestExtensions
|
||||
|
||||
/// <summary>
|
||||
/// Will return an absolute URL.
|
||||
/// If `relativeOrAsbolute` is absolute, returns it.
|
||||
/// If `relativeOrAsbolute` is relative, send absolute url based on the HOST of this request (without PathBase)
|
||||
/// If `relativeOrAbsolute` is absolute, returns it.
|
||||
/// If `relativeOrAbsolute` is relative, send absolute url based on the HOST of this request (without PathBase)
|
||||
/// </summary>
|
||||
/// <param name="request"></param>
|
||||
/// <param name="relativeOrAbsolte"></param>
|
||||
|
@ -10,7 +10,7 @@ namespace BTCPayServer.Services.Rates
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
|
||||
public RateSourceInfo RateSourceInfo => new("barebitcoin", "Bare Bitcoin", "https://api.bb.no/price");
|
||||
public RateSourceInfo RateSourceInfo => new("barebitcoin", "Bare Bitcoin", "https://api.bb.no/v1/price/nok");
|
||||
|
||||
public BareBitcoinRateProvider(HttpClient httpClient)
|
||||
{
|
||||
@ -24,16 +24,15 @@ namespace BTCPayServer.Services.Rates
|
||||
|
||||
var jobj = await response.Content.ReadAsAsync<JObject>(cancellationToken);
|
||||
|
||||
// Extract market and otc prices
|
||||
var market = jobj["market"].Value<decimal>();
|
||||
var buy = jobj["buy"].Value<decimal>();
|
||||
var sell = jobj["sell"].Value<decimal>();
|
||||
// Extract bid/ask prices from JSON response
|
||||
var bid = (decimal)jobj["bid"];
|
||||
var ask = (decimal)jobj["ask"];
|
||||
|
||||
// Create currency pair for BTC/NOK
|
||||
var pair = new CurrencyPair("BTC", "NOK");
|
||||
|
||||
// Return single pair rate with sell/buy as bid/ask
|
||||
return new[] { new PairRate(pair, new BidAsk(sell, buy)) };
|
||||
// Return single pair rate with bid/ask
|
||||
return new[] { new PairRate(pair, new BidAsk(bid, ask)) };
|
||||
}
|
||||
}
|
||||
}
|
@ -2,6 +2,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Controllers;
|
||||
@ -802,5 +803,65 @@ g:
|
||||
Assert.Equal("new", topupInvoice.Status);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
[Fact]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanUsePoSAppJsonEndpoint()
|
||||
{
|
||||
using var tester = CreateServerTester();
|
||||
await tester.StartAsync();
|
||||
var user = tester.NewAccount();
|
||||
await user.GrantAccessAsync();
|
||||
user.RegisterDerivationScheme("BTC");
|
||||
var apps = user.GetController<UIAppsController>();
|
||||
var pos = user.GetController<UIPointOfSaleController>();
|
||||
var vm = Assert.IsType<CreateAppViewModel>(Assert.IsType<ViewResult>(apps.CreateApp(user.StoreId)).Model);
|
||||
var appType = PointOfSaleAppType.AppType;
|
||||
vm.AppName = "test";
|
||||
vm.SelectedAppType = appType;
|
||||
var redirect = Assert.IsType<RedirectResult>(apps.CreateApp(user.StoreId, vm).Result);
|
||||
Assert.EndsWith("/settings/pos", redirect.Url);
|
||||
var appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps(user.StoreId).Result).Model);
|
||||
var app = appList.Apps[0];
|
||||
var appData = new AppData { Id = app.Id, StoreDataId = app.StoreId, Name = app.AppName, AppType = appType };
|
||||
apps.HttpContext.SetAppData(appData);
|
||||
pos.HttpContext.SetAppData(appData);
|
||||
var vmpos = await pos.UpdatePointOfSale(app.Id).AssertViewModelAsync<UpdatePointOfSaleViewModel>();
|
||||
vmpos.Title = "App POS";
|
||||
vmpos.Currency = "EUR";
|
||||
Assert.IsType<RedirectToActionResult>(pos.UpdatePointOfSale(app.Id, vmpos).Result);
|
||||
|
||||
// Failing requests
|
||||
var (invoiceId1, error1) = await PosJsonRequest(tester, app.Id, "amount=-21&discount=10&tip=2");
|
||||
Assert.Null(invoiceId1);
|
||||
Assert.Equal("Negative amount is not allowed", error1);
|
||||
var (invoiceId2, error2) = await PosJsonRequest(tester, app.Id, "amount=21&discount=-10&tip=-2");
|
||||
Assert.Null(invoiceId2);
|
||||
Assert.Equal("Negative tip or discount is not allowed", error2);
|
||||
|
||||
// Successful request
|
||||
var (invoiceId3, error3) = await PosJsonRequest(tester, app.Id, "amount=21");
|
||||
Assert.NotNull(invoiceId3);
|
||||
Assert.Null(error3);
|
||||
|
||||
// Check generated invoice
|
||||
var invoices = await user.BitPay.GetInvoicesAsync();
|
||||
var invoice = invoices.First();
|
||||
Assert.Equal(invoiceId3, invoice.Id);
|
||||
Assert.Equal(21.00m, invoice.Price);
|
||||
Assert.Equal("EUR", invoice.Currency);
|
||||
}
|
||||
|
||||
private async Task<(string invoiceId, string error)> PosJsonRequest(ServerTester tester, string appId, string query)
|
||||
{
|
||||
var uriBuilder = new UriBuilder(tester.PayTester.ServerUri) { Path = $"/apps/{appId}/pos/light", Query = query };
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, uriBuilder.Uri);
|
||||
request.Headers.Add("Accept", "application/json");
|
||||
var response = await tester.PayTester.HttpClient.SendAsync(request);
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
var json = JObject.Parse(content);
|
||||
return (json["invoiceId"]?.Value<string>(), json["error"]?.Value<string>());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -65,6 +65,11 @@ namespace BTCPayServer.Tests
|
||||
Assert.Equal($"bitcoin:{address}", clipboard);
|
||||
Assert.Equal($"bitcoin:{address.ToUpperInvariant()}", qrValue);
|
||||
s.Driver.ElementDoesNotExist(By.Id("Lightning_BTC-CHAIN"));
|
||||
|
||||
// Contact option
|
||||
var contactLink = s.Driver.FindElement(By.Id("ContactLink"));
|
||||
Assert.Equal("Contact us", contactLink.Text);
|
||||
Assert.Matches(supportUrl.Replace("{InvoiceId}", invoiceId), contactLink.GetAttribute("href"));
|
||||
|
||||
// Details should show exchange rate
|
||||
s.Driver.ToggleCollapse("PaymentDetails");
|
||||
@ -138,7 +143,6 @@ namespace BTCPayServer.Tests
|
||||
Assert.Contains("resubmit a payment", expiredSection.Text);
|
||||
Assert.DoesNotContain("This invoice expired with partial payment", expiredSection.Text);
|
||||
});
|
||||
Assert.True(s.Driver.ElementDoesNotExist(By.Id("ContactLink")));
|
||||
Assert.True(s.Driver.ElementDoesNotExist(By.Id("ReceiptLink")));
|
||||
Assert.Equal(storeUrl, s.Driver.FindElement(By.Id("StoreLink")).GetAttribute("href"));
|
||||
|
||||
@ -172,9 +176,6 @@ namespace BTCPayServer.Tests
|
||||
Assert.Contains("This invoice expired with partial payment", expiredSection.Text);
|
||||
Assert.DoesNotContain("resubmit a payment", expiredSection.Text);
|
||||
});
|
||||
var contactLink = s.Driver.FindElement(By.Id("ContactLink"));
|
||||
Assert.Equal("Contact us", contactLink.Text);
|
||||
Assert.Matches(supportUrl.Replace("{InvoiceId}", invoiceId), contactLink.GetAttribute("href"));
|
||||
Assert.True(s.Driver.ElementDoesNotExist(By.Id("ReceiptLink")));
|
||||
Assert.Equal(storeUrl, s.Driver.FindElement(By.Id("StoreLink")).GetAttribute("href"));
|
||||
|
||||
@ -243,7 +244,6 @@ namespace BTCPayServer.Tests
|
||||
});
|
||||
s.Driver.FindElement(By.Id("confetti"));
|
||||
s.Driver.FindElement(By.Id("ReceiptLink"));
|
||||
Assert.True(s.Driver.ElementDoesNotExist(By.Id("ContactLink")));
|
||||
Assert.Equal(storeUrl, s.Driver.FindElement(By.Id("StoreLink")).GetAttribute("href"));
|
||||
|
||||
// BIP21
|
||||
|
@ -3029,17 +3029,18 @@ namespace BTCPayServer.Tests
|
||||
var info = await client.GetLightningNodeInfo(user.StoreId, "BTC");
|
||||
Assert.Single(info.NodeURIs);
|
||||
Assert.NotEqual(0, info.BlockHeight);
|
||||
|
||||
// balance
|
||||
await TestUtils.EventuallyAsync(async () =>
|
||||
{
|
||||
var balance = await client.GetLightningNodeBalance(user.StoreId, "BTC");
|
||||
var histogram = await client.GetLightningNodeHistogram(user.StoreId, "BTC");
|
||||
var localBalance = balance.OffchainBalance.Local.ToDecimal(LightMoneyUnit.BTC);
|
||||
Assert.Equal(histogram.Balance, histogram.Series.Last());
|
||||
Assert.Equal(localBalance, histogram.Balance);
|
||||
Assert.Equal(localBalance, histogram.Series.Last());
|
||||
});
|
||||
|
||||
// Disable for now see #6518
|
||||
//// balance
|
||||
//await TestUtils.EventuallyAsync(async () =>
|
||||
//{
|
||||
// var balance = await client.GetLightningNodeBalance(user.StoreId, "BTC");
|
||||
// var localBalance = balance.OffchainBalance.Local.ToDecimal(LightMoneyUnit.BTC);
|
||||
// var histogram = await client.GetLightningNodeHistogram(user.StoreId, "BTC");
|
||||
// Assert.Equal(histogram.Balance, histogram.Series.Last());
|
||||
// Assert.Equal(localBalance, histogram.Balance);
|
||||
// Assert.Equal(localBalance, histogram.Series.Last());
|
||||
//});
|
||||
|
||||
// As admin, can use the internal node through our store.
|
||||
await user.MakeAdmin(true);
|
||||
@ -3829,7 +3830,6 @@ namespace BTCPayServer.Tests
|
||||
|
||||
await tester.WaitForEvent<NewBlockEvent>(async () =>
|
||||
{
|
||||
|
||||
await tester.ExplorerNode.GenerateAsync(1);
|
||||
}, bevent => bevent.PaymentMethodId == PaymentTypes.CHAIN.GetPaymentMethodId("BTC"));
|
||||
|
||||
|
@ -15,7 +15,7 @@ namespace BTCPayServer.Tests.Mocks
|
||||
|
||||
public string Action(UrlActionContext actionContext)
|
||||
{
|
||||
return $"{_BaseUrl}mock";
|
||||
return $"/mock";
|
||||
}
|
||||
|
||||
public string Content(string contentPath)
|
||||
|
@ -2175,6 +2175,56 @@ namespace BTCPayServer.Tests
|
||||
Assert.Contains("PP1 Edited", s.Driver.PageSource);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Selenium", "Selenium")]
|
||||
public async Task CanUseAwaitProgressForInProgressPayout()
|
||||
{
|
||||
using var s = CreateSeleniumTester();
|
||||
await s.StartAsync();
|
||||
s.RegisterNewUser(true);
|
||||
s.CreateNewStore();
|
||||
s.GenerateWallet(isHotWallet: true);
|
||||
await s.FundStoreWallet(denomination: 50.0m);
|
||||
|
||||
s.GoToStore(s.StoreId, StoreNavPages.PayoutProcessors);
|
||||
s.Driver.FindElement(By.Id("Configure-BTC-CHAIN")).Click();
|
||||
s.Driver.SetCheckbox(By.Id("ProcessNewPayoutsInstantly"), true);
|
||||
s.ClickPagePrimary();
|
||||
|
||||
s.GoToStore(s.StoreId, StoreNavPages.PullPayments);
|
||||
s.ClickPagePrimary();
|
||||
s.Driver.FindElement(By.Id("Name")).SendKeys("PP1");
|
||||
s.Driver.FindElement(By.Id("Amount")).Clear();
|
||||
s.Driver.FindElement(By.Id("Amount")).SendKeys("99.0");
|
||||
s.Driver.SetCheckbox(By.Id("AutoApproveClaims"), true);
|
||||
s.ClickPagePrimary();
|
||||
|
||||
s.Driver.FindElement(By.LinkText("View")).Click();
|
||||
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.Last());
|
||||
|
||||
var address = await s.Server.ExplorerNode.GetNewAddressAsync();
|
||||
s.Driver.FindElement(By.Id("Destination")).SendKeys(address.ToString() + Keys.Enter);
|
||||
s.GoToStore(s.StoreId, StoreNavPages.Payouts);
|
||||
s.Driver.FindElement(By.Id("InProgress-view")).Click();
|
||||
|
||||
// Waiting for the payment processor to process the payment
|
||||
int i = 0;
|
||||
while (!s.Driver.PageSource.Contains("mass-action-select-all"))
|
||||
{
|
||||
s.Driver.Navigate().Refresh();
|
||||
i++;
|
||||
Thread.Sleep(1000);
|
||||
if (i > 10)
|
||||
break;
|
||||
}
|
||||
s.Driver.TakeScreenshot().SaveAsFile("C:\\Users\\NicolasDorier\\AppData\\Local\\Temp\\1109191644\\1.png");
|
||||
s.Driver.FindElement(By.ClassName("mass-action-select-all")).Click();
|
||||
|
||||
s.Driver.FindElement(By.Id("InProgress-mark-awaiting-payment")).Click();
|
||||
s.Driver.FindElement(By.Id("AwaitingPayment-view")).Click();
|
||||
Assert.Contains("PP1", s.Driver.PageSource);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Selenium", "Selenium")]
|
||||
[Trait("Lightning", "Lightning")]
|
||||
|
@ -152,13 +152,10 @@
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@if (Model.Series != null)
|
||||
{
|
||||
<div class="ct-chart"></div>
|
||||
<template>
|
||||
@Safe.Json(Model)
|
||||
</template>
|
||||
}
|
||||
<div class="ct-chart"></div>
|
||||
<template>
|
||||
@Safe.Json(Model)
|
||||
</template>
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -36,7 +36,7 @@
|
||||
<tr>
|
||||
<th class="w-125px" text-translate="true">Date</th>
|
||||
<th text-translate="true">Transaction</th>
|
||||
<th text-translate="true">Labels</th>
|
||||
<th style="min-width:125px" text-translate="true">Labels</th>
|
||||
<th class="text-end" text-translate="true">Amount</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
@ -48,9 +48,7 @@
|
||||
else
|
||||
{
|
||||
<p>
|
||||
We would like to show you a chart of your balance.
|
||||
Please <a href="https://github.com/dgarage/NBXplorer/blob/master/docs/Postgres-Migration.md" target="_blank" rel="noreferrer noopener">migrate to the new NBXplorer backend</a>
|
||||
for that data to become available.
|
||||
We would like to show you a chart of your balance. Please set <a href="https://docs.btcpayserver.org/Deployment/ManualDeploymentExtended/#3-create-a-configuration-file" target="_blank" rel="noreferrer noopener">NBXPlorer's PostgreSQL connection string</a> to make this data available.
|
||||
</p>
|
||||
}
|
||||
<script>
|
||||
|
@ -195,30 +195,17 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
}
|
||||
|
||||
[HttpGet("~/api/v1/apps/pos/{appId}")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
public async Task<IActionResult> GetPosApp(string appId)
|
||||
{
|
||||
var app = await _appService.GetApp(appId, PointOfSaleAppType.AppType, includeArchived: true);
|
||||
if (app == null)
|
||||
{
|
||||
return AppNotFound();
|
||||
}
|
||||
|
||||
return Ok(ToPointOfSaleModel(app));
|
||||
return app == null ? AppNotFound() : Ok(ToPointOfSaleModel(app));
|
||||
}
|
||||
|
||||
[HttpGet("~/api/v1/apps/crowdfund/{appId}")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
public async Task<IActionResult> GetCrowdfundApp(string appId)
|
||||
{
|
||||
var app = await _appService.GetApp(appId, CrowdfundAppType.AppType, includeArchived: true);
|
||||
if (app == null)
|
||||
{
|
||||
return AppNotFound();
|
||||
}
|
||||
|
||||
var model = await ToCrowdfundModel(app);
|
||||
return Ok(model);
|
||||
return app == null ? AppNotFound() : Ok(await ToCrowdfundModel(app));
|
||||
}
|
||||
|
||||
[HttpDelete("~/api/v1/apps/{appId}")]
|
||||
|
@ -12,10 +12,8 @@ using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Bitcoin;
|
||||
using BTCPayServer.Payouts;
|
||||
using BTCPayServer.Rating;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Security.Greenfield;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
@ -25,9 +23,6 @@ using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using NBitcoin;
|
||||
using NBitpayClient;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using CreateInvoiceRequest = BTCPayServer.Client.Models.CreateInvoiceRequest;
|
||||
using InvoiceData = BTCPayServer.Client.Models.InvoiceData;
|
||||
@ -600,12 +595,13 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
{
|
||||
statuses.Add(InvoiceStatus.Settled);
|
||||
}
|
||||
|
||||
if (state.CanMarkInvalid())
|
||||
{
|
||||
statuses.Add(InvoiceStatus.Invalid);
|
||||
}
|
||||
return new InvoiceData()
|
||||
var store = request?.HttpContext.GetStoreData();
|
||||
var receipt = store == null ? entity.ReceiptOptions : InvoiceDataBase.ReceiptOptions.Merge(store.GetStoreBlob().ReceiptOptions, entity.ReceiptOptions);
|
||||
return new InvoiceData
|
||||
{
|
||||
StoreId = entity.StoreId,
|
||||
ExpirationTime = entity.ExpirationTime,
|
||||
@ -621,7 +617,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
Archived = entity.Archived,
|
||||
Metadata = entity.Metadata.ToJObject(),
|
||||
AvailableStatusesForManualMarking = statuses.ToArray(),
|
||||
Checkout = new CreateInvoiceRequest.CheckoutOptions()
|
||||
Checkout = new InvoiceDataBase.CheckoutOptions
|
||||
{
|
||||
Expiration = entity.ExpirationTime - entity.InvoiceTime,
|
||||
Monitoring = entity.MonitoringExpiration - entity.ExpirationTime,
|
||||
@ -634,7 +630,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
RedirectAutomatically = entity.RedirectAutomatically,
|
||||
RedirectURL = entity.RedirectURLTemplate
|
||||
},
|
||||
Receipt = entity.ReceiptOptions
|
||||
Receipt = receipt
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -171,9 +171,9 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
var allowedPayjoin = derivationScheme.IsHotWallet && Store.GetStoreBlob().PayJoinEnabled;
|
||||
if (allowedPayjoin)
|
||||
{
|
||||
bip21.QueryParams.Add(PayjoinClient.BIP21EndpointKey,
|
||||
Request.GetAbsoluteUri(Url.Action(nameof(PayJoinEndpointController.Submit), "PayJoinEndpoint",
|
||||
new { network.CryptoCode })));
|
||||
var endpoint = Url.ActionAbsolute(Request, nameof(PayJoinEndpointController.Submit), "PayJoinEndpoint",
|
||||
new { network.CryptoCode }).ToString();
|
||||
bip21.QueryParams.Add(PayjoinClient.BIP21EndpointKey, endpoint);
|
||||
}
|
||||
|
||||
return Ok(new OnChainWalletAddressData()
|
||||
|
@ -1,11 +1,11 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
@ -22,11 +22,16 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
{
|
||||
private readonly StoreRepository _storeRepository;
|
||||
private readonly UserManager<ApplicationUser> _userManager;
|
||||
private readonly UriResolver _uriResolver;
|
||||
|
||||
public GreenfieldStoreUsersController(StoreRepository storeRepository, UserManager<ApplicationUser> userManager)
|
||||
public GreenfieldStoreUsersController(
|
||||
StoreRepository storeRepository,
|
||||
UserManager<ApplicationUser> userManager,
|
||||
UriResolver uriResolver)
|
||||
{
|
||||
_storeRepository = storeRepository;
|
||||
_userManager = userManager;
|
||||
_uriResolver = uriResolver;
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
@ -95,8 +100,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
Role = storeUser.StoreRoleId,
|
||||
Email = user?.Email,
|
||||
Name = blob?.Name,
|
||||
ImageUrl = blob?.ImageUrl,
|
||||
|
||||
ImageUrl = blob?.ImageUrl == null ? null : await _uriResolver.Resolve(Request.GetAbsoluteRootUri(), UnresolvedUri.Create(blob.ImageUrl))
|
||||
});
|
||||
}
|
||||
return storeUsers;
|
||||
|
@ -37,6 +37,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
private readonly RoleManager<IdentityRole> _roleManager;
|
||||
private readonly SettingsRepository _settingsRepository;
|
||||
private readonly EventAggregator _eventAggregator;
|
||||
private readonly CallbackGenerator _callbackGenerator;
|
||||
private readonly IPasswordValidator<ApplicationUser> _passwordValidator;
|
||||
private readonly IRateLimitService _throttleService;
|
||||
private readonly BTCPayServerOptions _options;
|
||||
@ -50,6 +51,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
SettingsRepository settingsRepository,
|
||||
PoliciesSettings policiesSettings,
|
||||
EventAggregator eventAggregator,
|
||||
CallbackGenerator callbackGenerator,
|
||||
IPasswordValidator<ApplicationUser> passwordValidator,
|
||||
IRateLimitService throttleService,
|
||||
BTCPayServerOptions options,
|
||||
@ -65,6 +67,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
_settingsRepository = settingsRepository;
|
||||
PoliciesSettings = policiesSettings;
|
||||
_eventAggregator = eventAggregator;
|
||||
_callbackGenerator = callbackGenerator;
|
||||
_passwordValidator = passwordValidator;
|
||||
_throttleService = throttleService;
|
||||
_options = options;
|
||||
@ -113,7 +116,8 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
|
||||
if (user.RequiresApproval)
|
||||
{
|
||||
return await _userService.SetUserApproval(user.Id, request.Approved, Request.GetAbsoluteRootUri())
|
||||
var loginLink = _callbackGenerator.ForLogin(user, Request);
|
||||
return await _userService.SetUserApproval(user.Id, request.Approved, loginLink)
|
||||
? Ok()
|
||||
: this.CreateAPIError("invalid-state", $"User is already {(request.Approved ? "approved" : "unapproved")}");
|
||||
}
|
||||
@ -219,6 +223,10 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
ModelState.AddModelError(string.Empty, error.Description);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_eventAggregator.Publish(new UserEvent.Updated(user));
|
||||
}
|
||||
}
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
@ -255,7 +263,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
blob.ImageUrl = fileIdUri.ToString();
|
||||
user.SetBlob(blob);
|
||||
await _userManager.UpdateAsync(user);
|
||||
|
||||
_eventAggregator.Publish(new UserEvent.Updated(user));
|
||||
var model = await FromModel(user);
|
||||
return Ok(model);
|
||||
}
|
||||
@ -280,6 +288,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
blob.ImageUrl = null;
|
||||
user.SetBlob(blob);
|
||||
await _userManager.UpdateAsync(user);
|
||||
_eventAggregator.Publish(new UserEvent.Updated(user));
|
||||
}
|
||||
return Ok();
|
||||
}
|
||||
@ -399,18 +408,11 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
await _settingsRepository.FirstAdminRegistered(policies, _options.UpdateUrl != null, _options.DisableRegistration, Logs);
|
||||
}
|
||||
}
|
||||
|
||||
var currentUser = await _userManager.GetUserAsync(User);
|
||||
var userEvent = new UserRegisteredEvent
|
||||
var userEvent = currentUser switch
|
||||
{
|
||||
RequestUri = Request.GetAbsoluteRootUri(),
|
||||
Admin = isNewAdmin,
|
||||
User = user
|
||||
};
|
||||
if (currentUser is not null)
|
||||
{
|
||||
userEvent.Kind = UserRegisteredEventKind.Invite;
|
||||
userEvent.InvitedByUser = currentUser;
|
||||
{ } invitedBy => await UserEvent.Invited.Create(user, invitedBy, _callbackGenerator, Request, true),
|
||||
_ => await UserEvent.Registered.Create(user, _callbackGenerator, Request)
|
||||
};
|
||||
_eventAggregator.Publish(userEvent);
|
||||
|
||||
@ -444,6 +446,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
|
||||
// Ok, this user is an admin but there are other admins as well so safe to delete
|
||||
await _userService.DeleteUserAndAssociatedData(user);
|
||||
_eventAggregator.Publish(new UserEvent.Deleted(user));
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
@ -41,7 +41,7 @@ namespace BTCPayServer.Controllers
|
||||
readonly SettingsRepository _SettingsRepository;
|
||||
private readonly Fido2Service _fido2Service;
|
||||
private readonly LnurlAuthService _lnurlAuthService;
|
||||
private readonly LinkGenerator _linkGenerator;
|
||||
private readonly CallbackGenerator _callbackGenerator;
|
||||
private readonly UserLoginCodeService _userLoginCodeService;
|
||||
private readonly EventAggregator _eventAggregator;
|
||||
readonly ILogger _logger;
|
||||
@ -64,7 +64,7 @@ namespace BTCPayServer.Controllers
|
||||
UserLoginCodeService userLoginCodeService,
|
||||
LnurlAuthService lnurlAuthService,
|
||||
EmailSenderFactory emailSenderFactory,
|
||||
LinkGenerator linkGenerator,
|
||||
CallbackGenerator callbackGenerator,
|
||||
IStringLocalizer stringLocalizer,
|
||||
Logs logs)
|
||||
{
|
||||
@ -78,8 +78,8 @@ namespace BTCPayServer.Controllers
|
||||
_fido2Service = fido2Service;
|
||||
_lnurlAuthService = lnurlAuthService;
|
||||
EmailSenderFactory = emailSenderFactory;
|
||||
_linkGenerator = linkGenerator;
|
||||
_userLoginCodeService = userLoginCodeService;
|
||||
_callbackGenerator = callbackGenerator;
|
||||
_userLoginCodeService = userLoginCodeService;
|
||||
_eventAggregator = eventAggregator;
|
||||
_logger = logs.PayServer;
|
||||
Logs = logs;
|
||||
@ -297,10 +297,7 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
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) ?? string.Empty)
|
||||
LNURLEndpoint = new Uri(_callbackGenerator.ForLNUrlAuth(user, r, Request))
|
||||
};
|
||||
}
|
||||
return null;
|
||||
@ -627,12 +624,7 @@ namespace BTCPayServer.Controllers
|
||||
RegisteredAdmin = true;
|
||||
}
|
||||
|
||||
_eventAggregator.Publish(new UserRegisteredEvent
|
||||
{
|
||||
RequestUri = Request.GetAbsoluteRootUri(),
|
||||
User = user,
|
||||
Admin = RegisteredAdmin
|
||||
});
|
||||
_eventAggregator.Publish(await UserEvent.Registered.Create(user, _callbackGenerator, Request));
|
||||
RegisteredUserId = user.Id;
|
||||
|
||||
TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["Account created."].Value;
|
||||
@ -699,11 +691,8 @@ namespace BTCPayServer.Controllers
|
||||
var result = await _userManager.ConfirmEmailAsync(user, code);
|
||||
if (result.Succeeded)
|
||||
{
|
||||
_eventAggregator.Publish(new UserConfirmedEmailEvent
|
||||
{
|
||||
User = user,
|
||||
RequestUri = Request.GetAbsoluteRootUri()
|
||||
});
|
||||
var approvalLink = _callbackGenerator.ForApproval(user, Request);
|
||||
_eventAggregator.Publish(new UserEvent.ConfirmedEmail(user, approvalLink));
|
||||
|
||||
var hasPassword = await _userManager.HasPasswordAsync(user);
|
||||
if (hasPassword)
|
||||
@ -749,11 +738,8 @@ namespace BTCPayServer.Controllers
|
||||
// Don't reveal that the user does not exist or is not confirmed
|
||||
return RedirectToAction(nameof(ForgotPasswordConfirmation));
|
||||
}
|
||||
_eventAggregator.Publish(new UserPasswordResetRequestedEvent
|
||||
{
|
||||
User = user,
|
||||
RequestUri = Request.GetAbsoluteRootUri()
|
||||
});
|
||||
var callbackUri = await _callbackGenerator.ForPasswordReset(user, Request);
|
||||
_eventAggregator.Publish(new UserEvent.PasswordResetRequested(user, callbackUri));
|
||||
return RedirectToAction(nameof(ForgotPasswordConfirmation));
|
||||
}
|
||||
|
||||
@ -889,11 +875,10 @@ namespace BTCPayServer.Controllers
|
||||
private async Task FinalizeInvitationIfApplicable(ApplicationUser user)
|
||||
{
|
||||
if (!_userManager.HasInvitationToken<ApplicationUser>(user)) return;
|
||||
_eventAggregator.Publish(new UserInviteAcceptedEvent
|
||||
{
|
||||
User = user,
|
||||
RequestUri = Request.GetAbsoluteRootUri()
|
||||
});
|
||||
|
||||
// This is a placeholder, the real storeIds will be set by the UserEventHostedService
|
||||
var storeUsersLink = _callbackGenerator.StoreUsersLink("{0}", Request);
|
||||
_eventAggregator.Publish(new UserEvent.InviteAccepted(user, storeUsersLink));
|
||||
// unset used token
|
||||
await _userManager.UnsetInvitationTokenAsync<ApplicationUser>(user.Id);
|
||||
}
|
||||
|
@ -56,8 +56,8 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
var invoice = (await _InvoiceRepository.GetInvoices(new InvoiceQuery
|
||||
{
|
||||
InvoiceId = new[] { invoiceId },
|
||||
UserId = GetUserId()
|
||||
InvoiceId = [invoiceId],
|
||||
UserId = GetUserIdForInvoiceQuery()
|
||||
})).FirstOrDefault();
|
||||
if (invoice is null)
|
||||
return NotFound();
|
||||
@ -71,11 +71,11 @@ namespace BTCPayServer.Controllers
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> RedeliverWebhook(string storeId, string invoiceId, string deliveryId)
|
||||
{
|
||||
var invoice = (await _InvoiceRepository.GetInvoices(new InvoiceQuery()
|
||||
var invoice = (await _InvoiceRepository.GetInvoices(new InvoiceQuery
|
||||
{
|
||||
InvoiceId = new[] { invoiceId },
|
||||
StoreId = new[] { storeId },
|
||||
UserId = GetUserId()
|
||||
InvoiceId = [invoiceId],
|
||||
StoreId = [storeId],
|
||||
UserId = GetUserIdForInvoiceQuery()
|
||||
})).FirstOrDefault();
|
||||
if (invoice is null)
|
||||
return NotFound();
|
||||
@ -100,8 +100,8 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
var invoice = (await _InvoiceRepository.GetInvoices(new InvoiceQuery
|
||||
{
|
||||
InvoiceId = new[] { invoiceId },
|
||||
UserId = GetUserId(),
|
||||
InvoiceId = [invoiceId],
|
||||
UserId = GetUserIdForInvoiceQuery(),
|
||||
IncludeAddresses = true,
|
||||
IncludeArchived = true,
|
||||
IncludeRefunds = true,
|
||||
@ -599,8 +599,8 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
var invoice = (await _InvoiceRepository.GetInvoices(new InvoiceQuery
|
||||
{
|
||||
InvoiceId = new[] { invoiceId },
|
||||
UserId = GetUserId(),
|
||||
InvoiceId = [invoiceId],
|
||||
UserId = GetUserIdForInvoiceQuery(),
|
||||
IncludeAddresses = false,
|
||||
IncludeArchived = true,
|
||||
})).FirstOrDefault();
|
||||
@ -1116,7 +1116,7 @@ namespace BTCPayServer.Controllers
|
||||
return new InvoiceQuery
|
||||
{
|
||||
TextSearch = textSearch,
|
||||
UserId = GetUserId(),
|
||||
UserId = GetUserIdForInvoiceQuery(),
|
||||
Unusual = fs.GetFilterBool("unusual"),
|
||||
IncludeArchived = fs.GetFilterBool("includearchived") ?? false,
|
||||
Status = fs.GetFilterArray("status"),
|
||||
@ -1257,8 +1257,8 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
var invoice = (await _InvoiceRepository.GetInvoices(new InvoiceQuery
|
||||
{
|
||||
InvoiceId = new[] { invoiceId },
|
||||
UserId = GetUserId()
|
||||
InvoiceId = [invoiceId],
|
||||
UserId = GetUserIdForInvoiceQuery()
|
||||
})).FirstOrDefault();
|
||||
var model = new InvoiceStateChangeModel();
|
||||
if (invoice == null)
|
||||
@ -1292,6 +1292,9 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
private string GetUserId() => _UserManager.GetUserId(User)!;
|
||||
|
||||
// Let server admin lookup invoices from users, see #6489
|
||||
private string? GetUserIdForInvoiceQuery() => User.IsInRole(Roles.ServerAdmin) ? null : GetUserId();
|
||||
|
||||
private SelectList GetPaymentMethodsSelectList(StoreData store)
|
||||
{
|
||||
return new SelectList(store.GetPaymentMethodConfigs(_handlers, true)
|
||||
|
@ -4,9 +4,9 @@ using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
using BTCPayServer.Abstractions.Contracts;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Abstractions.Models;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Events;
|
||||
using BTCPayServer.Fido2;
|
||||
using BTCPayServer.Models.ManageViewModels;
|
||||
using BTCPayServer.Security.Greenfield;
|
||||
@ -36,11 +36,12 @@ namespace BTCPayServer.Controllers
|
||||
private readonly APIKeyRepository _apiKeyRepository;
|
||||
private readonly IAuthorizationService _authorizationService;
|
||||
private readonly Fido2Service _fido2Service;
|
||||
private readonly LinkGenerator _linkGenerator;
|
||||
private readonly CallbackGenerator _callbackGenerator;
|
||||
private readonly IHtmlHelper Html;
|
||||
private readonly UserService _userService;
|
||||
private readonly UriResolver _uriResolver;
|
||||
private readonly IFileService _fileService;
|
||||
private readonly EventAggregator _eventAggregator;
|
||||
readonly StoreRepository _StoreRepository;
|
||||
public IStringLocalizer StringLocalizer { get; }
|
||||
|
||||
@ -55,13 +56,13 @@ namespace BTCPayServer.Controllers
|
||||
APIKeyRepository apiKeyRepository,
|
||||
IAuthorizationService authorizationService,
|
||||
Fido2Service fido2Service,
|
||||
LinkGenerator linkGenerator,
|
||||
CallbackGenerator callbackGenerator,
|
||||
UserService userService,
|
||||
UriResolver uriResolver,
|
||||
IFileService fileService,
|
||||
IStringLocalizer stringLocalizer,
|
||||
IHtmlHelper htmlHelper
|
||||
)
|
||||
IHtmlHelper htmlHelper,
|
||||
EventAggregator eventAggregator)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_signInManager = signInManager;
|
||||
@ -72,8 +73,9 @@ namespace BTCPayServer.Controllers
|
||||
_apiKeyRepository = apiKeyRepository;
|
||||
_authorizationService = authorizationService;
|
||||
_fido2Service = fido2Service;
|
||||
_linkGenerator = linkGenerator;
|
||||
_callbackGenerator = callbackGenerator;
|
||||
Html = htmlHelper;
|
||||
_eventAggregator = eventAggregator;
|
||||
_userService = userService;
|
||||
_uriResolver = uriResolver;
|
||||
_fileService = fileService;
|
||||
@ -189,9 +191,9 @@ namespace BTCPayServer.Controllers
|
||||
return View(model);
|
||||
}
|
||||
|
||||
if (needUpdate is true)
|
||||
if (needUpdate && await _userManager.UpdateAsync(user) is { Succeeded: true })
|
||||
{
|
||||
needUpdate = await _userManager.UpdateAsync(user) is { Succeeded: true };
|
||||
_eventAggregator.Publish(new UserEvent.Updated(user));
|
||||
TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["Your profile has been updated"].Value;
|
||||
}
|
||||
else
|
||||
@ -217,8 +219,7 @@ namespace BTCPayServer.Controllers
|
||||
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
|
||||
var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
|
||||
var callbackUrl = _linkGenerator.EmailConfirmationLink(user.Id, code, Request.Scheme, Request.Host, Request.PathBase);
|
||||
var callbackUrl = await _callbackGenerator.ForEmailConfirmation(user, Request);
|
||||
(await _EmailSenderFactory.GetEmailSender()).SendEmailConfirmation(user.GetMailboxAddress(), callbackUrl);
|
||||
TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["Verification email sent. Please check your email."].Value;
|
||||
return RedirectToAction(nameof(Index));
|
||||
@ -320,7 +321,7 @@ namespace BTCPayServer.Controllers
|
||||
return RedirectToAction(nameof(SetPassword));
|
||||
}
|
||||
|
||||
[HttpPost()]
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> DeleteUserPost()
|
||||
{
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
@ -330,12 +331,12 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
await _userService.DeleteUserAndAssociatedData(user);
|
||||
_eventAggregator.Publish(new UserEvent.Deleted(user));
|
||||
TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["Account successfully deleted."].Value;
|
||||
await _signInManager.SignOutAsync();
|
||||
return RedirectToAction(nameof(UIAccountController.Login), "UIAccount");
|
||||
}
|
||||
|
||||
|
||||
#region Helpers
|
||||
|
||||
private void AddErrors(IdentityResult result)
|
||||
|
@ -70,8 +70,7 @@ namespace BTCPayServer.Controllers
|
||||
InvitationUrl =
|
||||
string.IsNullOrEmpty(blob?.InvitationToken)
|
||||
? null
|
||||
: _linkGenerator.InvitationLink(u.Id, blob.InvitationToken, Request.Scheme,
|
||||
Request.Host, Request.PathBase),
|
||||
: _callbackGenerator.ForInvitation(u, blob.InvitationToken, Request),
|
||||
EmailConfirmed = u.RequiresEmailConfirmation ? u.EmailConfirmed : null,
|
||||
Approved = u.RequiresApproval ? u.Approved : null,
|
||||
Created = u.Created,
|
||||
@ -98,7 +97,7 @@ namespace BTCPayServer.Controllers
|
||||
Id = user.Id,
|
||||
Email = user.Email,
|
||||
Name = blob?.Name,
|
||||
InvitationUrl = string.IsNullOrEmpty(blob?.InvitationToken) ? null : _linkGenerator.InvitationLink(user.Id, blob.InvitationToken, Request.Scheme, Request.Host, Request.PathBase),
|
||||
InvitationUrl = string.IsNullOrEmpty(blob?.InvitationToken) ? null : _callbackGenerator.ForInvitation(user, blob.InvitationToken, Request),
|
||||
ImageUrl = string.IsNullOrEmpty(blob?.ImageUrl) ? null : await _uriResolver.Resolve(Request.GetAbsoluteRootUri(), UnresolvedUri.Create(blob.ImageUrl)),
|
||||
EmailConfirmed = user.RequiresEmailConfirmation ? user.EmailConfirmed : null,
|
||||
Approved = user.RequiresApproval ? user.Approved : null,
|
||||
@ -120,7 +119,8 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
if (user.RequiresApproval && viewModel.Approved.HasValue && user.Approved != viewModel.Approved.Value)
|
||||
{
|
||||
approvalStatusChanged = await _userService.SetUserApproval(user.Id, viewModel.Approved.Value, Request.GetAbsoluteRootUri());
|
||||
var loginLink = _callbackGenerator.ForLogin(user, Request);
|
||||
approvalStatusChanged = await _userService.SetUserApproval(user.Id, viewModel.Approved.Value, loginLink);
|
||||
}
|
||||
if (user.RequiresEmailConfirmation && viewModel.EmailConfirmed.HasValue && user.EmailConfirmed != viewModel.EmailConfirmed)
|
||||
{
|
||||
@ -260,31 +260,21 @@ namespace BTCPayServer.Controllers
|
||||
if (model.IsAdmin && !(await _UserManager.AddToRoleAsync(user, Roles.ServerAdmin)).Succeeded)
|
||||
model.IsAdmin = false;
|
||||
|
||||
var tcs = new TaskCompletionSource<Uri>();
|
||||
var currentUser = await _UserManager.GetUserAsync(HttpContext.User);
|
||||
var sendEmail = model.SendInvitationEmail && ViewData["CanSendEmail"] is true;
|
||||
|
||||
_eventAggregator.Publish(new UserRegisteredEvent
|
||||
{
|
||||
RequestUri = Request.GetAbsoluteRootUri(),
|
||||
Kind = UserRegisteredEventKind.Invite,
|
||||
User = user,
|
||||
InvitedByUser = currentUser,
|
||||
SendInvitationEmail = sendEmail,
|
||||
Admin = model.IsAdmin,
|
||||
CallbackUrlGenerated = tcs
|
||||
});
|
||||
|
||||
var callbackUrl = await tcs.Task;
|
||||
var evt = await UserEvent.Invited.Create(user, currentUser, _callbackGenerator, Request, sendEmail);
|
||||
_eventAggregator.Publish(evt);
|
||||
|
||||
var info = sendEmail
|
||||
? "An invitation email has been sent. You may alternatively"
|
||||
: "An invitation email has not been sent. You need to";
|
||||
|
||||
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Success,
|
||||
AllowDismiss = false,
|
||||
Html = $"Account successfully created. {info} share this link with them:<br/>{callbackUrl}"
|
||||
Html = $"Account successfully created. {info} share this link with them:<br/>{evt.InvitationLink}"
|
||||
});
|
||||
return RedirectToAction(nameof(User), new { userId = user.Id });
|
||||
}
|
||||
@ -387,7 +377,8 @@ namespace BTCPayServer.Controllers
|
||||
if (user == null)
|
||||
return NotFound();
|
||||
|
||||
await _userService.SetUserApproval(userId, approved, Request.GetAbsoluteRootUri());
|
||||
var loginLink = _callbackGenerator.ForLogin(user, Request);
|
||||
await _userService.SetUserApproval(userId, approved, loginLink);
|
||||
|
||||
TempData[WellKnownTempData.SuccessMessage] = approved
|
||||
? StringLocalizer["User approved"].Value
|
||||
@ -414,8 +405,7 @@ namespace BTCPayServer.Controllers
|
||||
throw new ApplicationException($"Unable to load user with ID '{userId}'.");
|
||||
}
|
||||
|
||||
var code = await _UserManager.GenerateEmailConfirmationTokenAsync(user);
|
||||
var callbackUrl = _linkGenerator.EmailConfirmationLink(user.Id, code, Request.Scheme, Request.Host, Request.PathBase);
|
||||
var callbackUrl = await _callbackGenerator.ForEmailConfirmation(user, Request);
|
||||
|
||||
(await _emailSenderFactory.GetEmailSender()).SendEmailConfirmation(user.GetMailboxAddress(), callbackUrl);
|
||||
|
||||
|
@ -64,7 +64,7 @@ namespace BTCPayServer.Controllers
|
||||
private readonly StoredFileRepository _StoredFileRepository;
|
||||
private readonly IFileService _fileService;
|
||||
private readonly IEnumerable<IStorageProviderService> _StorageProviderServices;
|
||||
private readonly LinkGenerator _linkGenerator;
|
||||
private readonly CallbackGenerator _callbackGenerator;
|
||||
private readonly UriResolver _uriResolver;
|
||||
private readonly EmailSenderFactory _emailSenderFactory;
|
||||
private readonly TransactionLinkProviders _transactionLinkProviders;
|
||||
@ -90,7 +90,7 @@ namespace BTCPayServer.Controllers
|
||||
EventAggregator eventAggregator,
|
||||
IOptions<ExternalServicesOptions> externalServiceOptions,
|
||||
Logs logs,
|
||||
LinkGenerator linkGenerator,
|
||||
CallbackGenerator callbackGenerator,
|
||||
UriResolver uriResolver,
|
||||
EmailSenderFactory emailSenderFactory,
|
||||
IHostApplicationLifetime applicationLifetime,
|
||||
@ -119,7 +119,7 @@ namespace BTCPayServer.Controllers
|
||||
_eventAggregator = eventAggregator;
|
||||
_externalServiceOptions = externalServiceOptions;
|
||||
Logs = logs;
|
||||
_linkGenerator = linkGenerator;
|
||||
_callbackGenerator = callbackGenerator;
|
||||
_uriResolver = uriResolver;
|
||||
_emailSenderFactory = emailSenderFactory;
|
||||
ApplicationLifetime = applicationLifetime;
|
||||
@ -697,7 +697,7 @@ namespace BTCPayServer.Controllers
|
||||
var lnConfig = _LnConfigProvider.GetConfig(configKey);
|
||||
if (lnConfig != null)
|
||||
{
|
||||
model.QRCodeLink = Request.GetAbsoluteUri(Url.Action(nameof(GetLNDConfig), new { configKey = configKey }));
|
||||
model.QRCodeLink = Url.ActionAbsolute(Request, nameof(GetLNDConfig), new { configKey }).ToString();
|
||||
model.QRCode = $"config={model.QRCodeLink}";
|
||||
}
|
||||
}
|
||||
|
@ -439,7 +439,28 @@ namespace BTCPayServer.Controllers
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case "mark-awaiting-payment":
|
||||
await using (var context = _dbContextFactory.CreateContext())
|
||||
{
|
||||
var payouts = (await PullPaymentHostedService.GetPayouts(new PullPaymentHostedService.PayoutQuery()
|
||||
{
|
||||
States = new[] { PayoutState.InProgress },
|
||||
Stores = new[] { storeId },
|
||||
PayoutIds = payoutIds
|
||||
}, context));
|
||||
foreach (var payout in payouts)
|
||||
{
|
||||
payout.State = PayoutState.AwaitingPayment;
|
||||
payout.Proof = null;
|
||||
}
|
||||
await context.SaveChangesAsync();
|
||||
}
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
{
|
||||
Message = "Payout payments have been marked as awaiting payment",
|
||||
Severity = StatusMessageModel.StatusSeverity.Success
|
||||
});
|
||||
break;
|
||||
case "cancel":
|
||||
await _pullPaymentService.Cancel(
|
||||
new PullPaymentHostedService.CancelRequest(payoutIds, new[] { storeId }));
|
||||
|
@ -9,6 +9,7 @@ using BTCPayServer.Client;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Events;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Mails;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
@ -58,28 +59,18 @@ public partial class UIStoresController
|
||||
Created = DateTimeOffset.UtcNow
|
||||
};
|
||||
|
||||
var result = await _userManager.CreateAsync(user);
|
||||
if (result.Succeeded)
|
||||
var currentUser = await _userManager.GetUserAsync(HttpContext.User);
|
||||
if (currentUser is not null &&
|
||||
(await _userManager.CreateAsync(user)) is { Succeeded: true } result)
|
||||
{
|
||||
var invitationEmail = await _emailSenderFactory.IsComplete();
|
||||
var tcs = new TaskCompletionSource<Uri>();
|
||||
var currentUser = await _userManager.GetUserAsync(HttpContext.User);
|
||||
var evt = await UserEvent.Invited.Create(user, currentUser, _callbackGenerator, Request, invitationEmail);
|
||||
_eventAggregator.Publish(evt);
|
||||
|
||||
_eventAggregator.Publish(new UserRegisteredEvent
|
||||
{
|
||||
RequestUri = Request.GetAbsoluteRootUri(),
|
||||
Kind = UserRegisteredEventKind.Invite,
|
||||
User = user,
|
||||
InvitedByUser = currentUser,
|
||||
SendInvitationEmail = invitationEmail,
|
||||
CallbackUrlGenerated = tcs
|
||||
});
|
||||
|
||||
var callbackUrl = await tcs.Task;
|
||||
var info = invitationEmail
|
||||
? "An invitation email has been sent.<br/>You may alternatively"
|
||||
: "An invitation email has not been sent, because the server does not have an email server configured.<br/> You need to";
|
||||
successInfo = $"{info} share this link with them: <a class='alert-link' href='{callbackUrl}'>{callbackUrl}</a>";
|
||||
successInfo = $"{info} share this link with them: <a class='alert-link' href='{evt.InvitationLink}'>{evt.InvitationLink}</a>";
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -59,6 +59,7 @@ public partial class UIStoresController : Controller
|
||||
EmailSenderFactory emailSenderFactory,
|
||||
WalletFileParsers onChainWalletParsers,
|
||||
UIUserStoresController userStoresController,
|
||||
CallbackGenerator callbackGenerator,
|
||||
UriResolver uriResolver,
|
||||
CurrencyNameTable currencyNameTable,
|
||||
IStringLocalizer stringLocalizer,
|
||||
@ -86,6 +87,7 @@ public partial class UIStoresController : Controller
|
||||
_emailSenderFactory = emailSenderFactory;
|
||||
_onChainWalletParsers = onChainWalletParsers;
|
||||
_userStoresController = userStoresController;
|
||||
_callbackGenerator = callbackGenerator;
|
||||
_uriResolver = uriResolver;
|
||||
_currencyNameTable = currencyNameTable;
|
||||
_eventAggregator = eventAggregator;
|
||||
@ -121,6 +123,7 @@ public partial class UIStoresController : Controller
|
||||
private readonly EmailSenderFactory _emailSenderFactory;
|
||||
private readonly WalletFileParsers _onChainWalletParsers;
|
||||
private readonly UIUserStoresController _userStoresController;
|
||||
private readonly CallbackGenerator _callbackGenerator;
|
||||
private readonly UriResolver _uriResolver;
|
||||
private readonly EventAggregator _eventAggregator;
|
||||
private readonly IHtmlHelper _html;
|
||||
|
@ -404,9 +404,9 @@ namespace BTCPayServer.Controllers
|
||||
var bip21 = network.GenerateBIP21(address?.ToString(), null);
|
||||
if (allowedPayjoin)
|
||||
{
|
||||
bip21.QueryParams.Add(PayjoinClient.BIP21EndpointKey,
|
||||
Request.GetAbsoluteUri(Url.Action(nameof(PayJoinEndpointController.Submit), "PayJoinEndpoint",
|
||||
new { cryptoCode = walletId.CryptoCode })));
|
||||
var endpoint = Url.ActionAbsolute(Request, nameof(PayJoinEndpointController.Submit), "PayJoinEndpoint",
|
||||
new { cryptoCode = walletId.CryptoCode }).ToString();
|
||||
bip21.QueryParams.Add(PayjoinClient.BIP21EndpointKey, endpoint);
|
||||
}
|
||||
|
||||
string[]? labels = null;
|
||||
|
@ -339,61 +339,22 @@ public class BitcoinLikePayoutHandler : IPayoutHandler, IHasNetwork
|
||||
{
|
||||
var proof = ParseProof(payout) as PayoutTransactionOnChainBlob;
|
||||
var payoutBlob = payout.GetBlob(this._jsonSerializerSettings);
|
||||
if (proof is null || proof.Accounted is false)
|
||||
{
|
||||
if (proof?.Accounted is not true)
|
||||
continue;
|
||||
}
|
||||
foreach (var txid in proof.Candidates.ToList())
|
||||
{
|
||||
var explorer = _explorerClientProvider.GetExplorerClient(Network.CryptoCode);
|
||||
var tx = await explorer.GetTransactionAsync(txid);
|
||||
if (tx is null)
|
||||
{
|
||||
proof.Candidates.Remove(txid);
|
||||
}
|
||||
else if (tx.Confirmations >= payoutBlob.MinimumConfirmation)
|
||||
continue;
|
||||
if (tx.Confirmations >= payoutBlob.MinimumConfirmation)
|
||||
{
|
||||
payout.State = PayoutState.Completed;
|
||||
proof.TransactionId = tx.TransactionHash;
|
||||
updatedPayouts.Add(payout);
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
var rebroadcasted = await explorer.BroadcastAsync(tx.Transaction);
|
||||
if (rebroadcasted.RPCCode == RPCErrorCode.RPC_TRANSACTION_ERROR ||
|
||||
rebroadcasted.RPCCode == RPCErrorCode.RPC_TRANSACTION_REJECTED)
|
||||
{
|
||||
proof.Candidates.Remove(txid);
|
||||
}
|
||||
else
|
||||
{
|
||||
payout.State = PayoutState.InProgress;
|
||||
proof.TransactionId = tx.TransactionHash;
|
||||
updatedPayouts.Add(payout);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (proof.TransactionId is not null && !proof.Candidates.Contains(proof.TransactionId))
|
||||
{
|
||||
proof.TransactionId = null;
|
||||
}
|
||||
|
||||
if (proof.Candidates.Count == 0)
|
||||
{
|
||||
if (payout.State != PayoutState.AwaitingPayment)
|
||||
{
|
||||
updatedPayouts.Add(payout);
|
||||
}
|
||||
payout.State = PayoutState.AwaitingPayment;
|
||||
}
|
||||
else if (proof.TransactionId is null)
|
||||
{
|
||||
proof.TransactionId = proof.Candidates.First();
|
||||
}
|
||||
|
||||
if (payout.State == PayoutState.Completed)
|
||||
proof.Candidates = null;
|
||||
SetProofBlob(payout, proof);
|
||||
@ -402,7 +363,7 @@ public class BitcoinLikePayoutHandler : IPayoutHandler, IHasNetwork
|
||||
await ctx.SaveChangesAsync();
|
||||
foreach (PayoutData payoutData in updatedPayouts)
|
||||
{
|
||||
_eventAggregator.Publish(new PayoutEvent(PayoutEvent.PayoutEventType.Updated,payoutData));
|
||||
_eventAggregator.Publish(new PayoutEvent(PayoutEvent.PayoutEventType.Updated, payoutData));
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
37
BTCPayServer/Events/AppEvent.cs
Normal file
37
BTCPayServer/Events/AppEvent.cs
Normal file
@ -0,0 +1,37 @@
|
||||
#nullable enable
|
||||
using BTCPayServer.Data;
|
||||
|
||||
namespace BTCPayServer.Events;
|
||||
|
||||
public class AppEvent(AppData app, string? detail = null)
|
||||
{
|
||||
public class Created(AppData app, string? detail = null) : AppEvent(app, detail ?? app.AppType)
|
||||
{
|
||||
protected override string ToString()
|
||||
{
|
||||
return $"{base.ToString()} has been created";
|
||||
}
|
||||
}
|
||||
public class Deleted(AppData app, string? detail = null) : AppEvent(app, detail ?? app.AppType)
|
||||
{
|
||||
protected override string ToString()
|
||||
{
|
||||
return $"{base.ToString()} has been deleted";
|
||||
}
|
||||
}
|
||||
public class Updated(AppData app, string? detail = null) : AppEvent(app, detail ?? app.AppType)
|
||||
{
|
||||
protected override string ToString()
|
||||
{
|
||||
return $"{base.ToString()} has been updated";
|
||||
}
|
||||
}
|
||||
public string AppId { get; } = app.Id;
|
||||
public string StoreId { get; } = app.StoreDataId;
|
||||
public string? Detail { get; } = detail;
|
||||
|
||||
protected new virtual string ToString()
|
||||
{
|
||||
return $"AppEvent: App \"{app.Name}\" ({StoreId})";
|
||||
}
|
||||
}
|
@ -5,9 +5,11 @@ namespace BTCPayServer.Events
|
||||
public class NewBlockEvent
|
||||
{
|
||||
public PaymentMethodId PaymentMethodId { get; set; }
|
||||
public object AdditionalInfo { get; set; }
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{PaymentMethodId}: New block";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
50
BTCPayServer/Events/StoreEvent.cs
Normal file
50
BTCPayServer/Events/StoreEvent.cs
Normal file
@ -0,0 +1,50 @@
|
||||
#nullable enable
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using BTCPayServer.Data;
|
||||
|
||||
namespace BTCPayServer.Events;
|
||||
|
||||
public class StoreEvent(StoreData store, string? detail = null)
|
||||
{
|
||||
public class Created(StoreData store, string? detail = null) : StoreEvent(store, detail)
|
||||
{
|
||||
protected override string ToString()
|
||||
{
|
||||
return $"{base.ToString()} has been created";
|
||||
}
|
||||
}
|
||||
public class Removed(StoreData store, string? detail = null) : StoreEvent(store, detail)
|
||||
{
|
||||
protected override string ToString()
|
||||
{
|
||||
return $"{base.ToString()} has been removed";
|
||||
}
|
||||
}
|
||||
public class Updated(StoreData store, string? detail = null) : StoreEvent(store, detail)
|
||||
{
|
||||
protected override string ToString()
|
||||
{
|
||||
return $"{base.ToString()} has been updated";
|
||||
}
|
||||
}
|
||||
public string StoreId { get; } = store.Id;
|
||||
public string? Detail { get; } = detail;
|
||||
|
||||
public IEnumerable<StoreUser>? StoreUsers { get; } = store.UserStores?.Select(userStore => new StoreUser
|
||||
{
|
||||
UserId = userStore.ApplicationUserId,
|
||||
RoleId = userStore.StoreRoleId
|
||||
});
|
||||
|
||||
protected new virtual string ToString()
|
||||
{
|
||||
return $"StoreEvent: Store \"{store.StoreName}\" ({store.Id})";
|
||||
}
|
||||
|
||||
public class StoreUser
|
||||
{
|
||||
public string UserId { get; init; } = null!;
|
||||
public string RoleId { get; set; } = null!;
|
||||
}
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
namespace BTCPayServer.Events;
|
||||
|
||||
public class StoreRemovedEvent
|
||||
{
|
||||
public StoreRemovedEvent(string storeId)
|
||||
{
|
||||
StoreId = storeId;
|
||||
}
|
||||
public string StoreId { get; set; }
|
||||
public override string ToString()
|
||||
{
|
||||
return $"Store {StoreId} has been removed";
|
||||
}
|
||||
}
|
36
BTCPayServer/Events/StoreRoleEvent.cs
Normal file
36
BTCPayServer/Events/StoreRoleEvent.cs
Normal file
@ -0,0 +1,36 @@
|
||||
#nullable enable
|
||||
|
||||
namespace BTCPayServer.Events;
|
||||
|
||||
public abstract class StoreRoleEvent(string storeId, string roleId)
|
||||
{
|
||||
public string StoreId { get; } = storeId;
|
||||
public string RoleId { get; } = roleId;
|
||||
|
||||
public class Added(string storeId, string roleId) : StoreRoleEvent(storeId, roleId)
|
||||
{
|
||||
protected override string ToString()
|
||||
{
|
||||
return $"{base.ToString()} has been added";
|
||||
}
|
||||
}
|
||||
public class Removed(string storeId, string roleId) : StoreRoleEvent(storeId, roleId)
|
||||
{
|
||||
protected override string ToString()
|
||||
{
|
||||
return $"{base.ToString()} has been removed";
|
||||
}
|
||||
}
|
||||
public class Updated(string storeId, string roleId) : StoreRoleEvent(storeId, roleId)
|
||||
{
|
||||
protected override string ToString()
|
||||
{
|
||||
return $"{base.ToString()} has been updated";
|
||||
}
|
||||
}
|
||||
|
||||
protected new virtual string ToString()
|
||||
{
|
||||
return $"StoreRoleEvent: Store {StoreId}, Role {RoleId}";
|
||||
}
|
||||
}
|
38
BTCPayServer/Events/StoreUserEvent.cs
Normal file
38
BTCPayServer/Events/StoreUserEvent.cs
Normal file
@ -0,0 +1,38 @@
|
||||
#nullable enable
|
||||
|
||||
namespace BTCPayServer.Events;
|
||||
|
||||
public abstract class StoreUserEvent(string storeId, string userId)
|
||||
{
|
||||
public class Added(string storeId, string userId, string roleId) : StoreUserEvent(storeId, userId)
|
||||
{
|
||||
public string RoleId { get; } = roleId;
|
||||
protected override string ToString()
|
||||
{
|
||||
return $"{base.ToString()} has been added";
|
||||
}
|
||||
}
|
||||
public class Removed(string storeId, string userId) : StoreUserEvent(storeId, userId)
|
||||
{
|
||||
protected override string ToString()
|
||||
{
|
||||
return $"{base.ToString()} has been removed";
|
||||
}
|
||||
}
|
||||
public class Updated(string storeId, string userId, string roleId) : StoreUserEvent(storeId, userId)
|
||||
{
|
||||
public string RoleId { get; } = roleId;
|
||||
protected override string ToString()
|
||||
{
|
||||
return $"{base.ToString()} has been updated";
|
||||
}
|
||||
}
|
||||
|
||||
public string StoreId { get; } = storeId;
|
||||
public string UserId { get; } = userId;
|
||||
|
||||
protected new virtual string ToString()
|
||||
{
|
||||
return $"StoreUserEvent: User {UserId}, Store {StoreId}";
|
||||
}
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
using System;
|
||||
using BTCPayServer.Data;
|
||||
|
||||
namespace BTCPayServer.Events;
|
||||
|
||||
public class UserApprovedEvent
|
||||
{
|
||||
public ApplicationUser User { get; set; }
|
||||
public Uri RequestUri { get; set; }
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
using System;
|
||||
using BTCPayServer.Data;
|
||||
|
||||
namespace BTCPayServer.Events;
|
||||
|
||||
public class UserConfirmedEmailEvent
|
||||
{
|
||||
public ApplicationUser User { get; set; }
|
||||
public Uri RequestUri { get; set; }
|
||||
}
|
87
BTCPayServer/Events/UserEvent.cs
Normal file
87
BTCPayServer/Events/UserEvent.cs
Normal file
@ -0,0 +1,87 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Services;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace BTCPayServer.Events;
|
||||
|
||||
public class UserEvent(ApplicationUser user)
|
||||
{
|
||||
public class Deleted(ApplicationUser user) : UserEvent(user)
|
||||
{
|
||||
protected override string ToString()
|
||||
{
|
||||
return $"{base.ToString()} has been deleted";
|
||||
}
|
||||
}
|
||||
public class InviteAccepted(ApplicationUser user, string storeUsersLink) : UserEvent(user)
|
||||
{
|
||||
public string StoreUsersLink { get; set; } = storeUsersLink;
|
||||
}
|
||||
public class PasswordResetRequested(ApplicationUser user, string resetLink) : UserEvent(user)
|
||||
{
|
||||
public string ResetLink { get; } = resetLink;
|
||||
}
|
||||
public class Registered(ApplicationUser user, string approvalLink, string confirmationEmail) : UserEvent(user)
|
||||
{
|
||||
public string ApprovalLink { get; } = approvalLink;
|
||||
public string ConfirmationEmailLink { get; set; } = confirmationEmail;
|
||||
public static async Task<Registered> Create(ApplicationUser user, CallbackGenerator callbackGenerator, HttpRequest request)
|
||||
{
|
||||
var approvalLink = callbackGenerator.ForApproval(user, request);
|
||||
var confirmationEmail = await callbackGenerator.ForEmailConfirmation(user, request);
|
||||
return new Registered(user, approvalLink, confirmationEmail);
|
||||
}
|
||||
}
|
||||
public class Invited(ApplicationUser user, ApplicationUser invitedBy, string invitationLink, string approvalLink, string confirmationEmail) : Registered(user, approvalLink, confirmationEmail)
|
||||
{
|
||||
public bool SendInvitationEmail { get; set; }
|
||||
public ApplicationUser InvitedByUser { get; } = invitedBy;
|
||||
public string InvitationLink { get; } = invitationLink;
|
||||
|
||||
public static async Task<Invited> Create(ApplicationUser user, ApplicationUser currentUser, CallbackGenerator callbackGenerator, HttpRequest request, bool sendEmail)
|
||||
{
|
||||
var invitationLink = await callbackGenerator.ForInvitation(user, request);
|
||||
var approvalLink = callbackGenerator.ForApproval(user, request);
|
||||
var confirmationEmail = await callbackGenerator.ForEmailConfirmation(user, request);
|
||||
return new Invited(user, currentUser, invitationLink, approvalLink, confirmationEmail)
|
||||
{
|
||||
SendInvitationEmail = sendEmail
|
||||
};
|
||||
}
|
||||
}
|
||||
public class Updated(ApplicationUser user) : UserEvent(user)
|
||||
{
|
||||
protected override string ToString()
|
||||
{
|
||||
return $"{base.ToString()} has been updated";
|
||||
}
|
||||
}
|
||||
public class Approved(ApplicationUser user, string loginLink) : UserEvent(user)
|
||||
{
|
||||
public string LoginLink { get; set; } = loginLink;
|
||||
protected override string ToString()
|
||||
{
|
||||
return $"{base.ToString()} has been approved";
|
||||
}
|
||||
}
|
||||
|
||||
public class ConfirmedEmail(ApplicationUser user, string approvalLink): UserEvent(user)
|
||||
{
|
||||
public string ApprovalLink { get; set; } = approvalLink;
|
||||
|
||||
protected override string ToString()
|
||||
{
|
||||
return $"{base.ToString()} has email confirmed";
|
||||
}
|
||||
}
|
||||
|
||||
public ApplicationUser User { get; } = user;
|
||||
|
||||
protected new virtual string ToString()
|
||||
{
|
||||
return $"UserEvent: User \"{User.Email}\" ({User.Id})";
|
||||
}
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
using System;
|
||||
using BTCPayServer.Data;
|
||||
|
||||
namespace BTCPayServer.Events;
|
||||
|
||||
public class UserInviteAcceptedEvent
|
||||
{
|
||||
public ApplicationUser User { get; set; }
|
||||
public Uri RequestUri { get; set; }
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
|
||||
namespace BTCPayServer.Events
|
||||
{
|
||||
public class UserPasswordResetRequestedEvent
|
||||
{
|
||||
public ApplicationUser User { get; set; }
|
||||
public Uri RequestUri { get; set; }
|
||||
public TaskCompletionSource<Uri> CallbackUrlGenerated;
|
||||
}
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
|
||||
namespace BTCPayServer.Events;
|
||||
|
||||
public class UserRegisteredEvent
|
||||
{
|
||||
public ApplicationUser User { get; set; }
|
||||
public bool Admin { get; set; }
|
||||
public UserRegisteredEventKind Kind { get; set; } = UserRegisteredEventKind.Registration;
|
||||
public Uri RequestUri { get; set; }
|
||||
public ApplicationUser InvitedByUser { get; set; }
|
||||
public bool SendInvitationEmail { get; set; } = true;
|
||||
public TaskCompletionSource<Uri> CallbackUrlGenerated;
|
||||
}
|
||||
|
||||
public enum UserRegisteredEventKind
|
||||
{
|
||||
Registration,
|
||||
Invite
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
|
||||
using System;
|
||||
using BTCPayServer;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Controllers;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
@ -11,6 +12,12 @@ namespace Microsoft.AspNetCore.Mvc
|
||||
public static class UrlHelperExtensions
|
||||
{
|
||||
#nullable enable
|
||||
public static Uri ActionAbsolute(this IUrlHelper helper, HttpRequest request, string? action, string? controller, object? values)
|
||||
=> request.GetAbsoluteUriNoPathBase(new Uri(helper.Action(action, controller, values) ?? "", UriKind.Relative));
|
||||
public static Uri ActionAbsolute(this IUrlHelper helper, HttpRequest request, string? action, string? controller)
|
||||
=> request.GetAbsoluteUriNoPathBase(new Uri(helper.Action(action, controller) ?? "", UriKind.Relative));
|
||||
public static Uri ActionAbsolute(this IUrlHelper helper, HttpRequest request, string? action, object? values)
|
||||
=> request.GetAbsoluteUriNoPathBase(new Uri(helper.Action(action, values) ?? "", UriKind.Relative));
|
||||
public static string? EnsureLocal(this IUrlHelper helper, string? url, HttpRequest? httpRequest = null)
|
||||
{
|
||||
if (url is null || helper.IsLocalUrl(url))
|
||||
@ -23,52 +30,11 @@ namespace Microsoft.AspNetCore.Mvc
|
||||
}
|
||||
#nullable restore
|
||||
|
||||
public static string UserDetailsLink(this LinkGenerator urlHelper, string userId, string scheme, HostString host, string pathbase)
|
||||
{
|
||||
return urlHelper.GetUriByAction(nameof(UIServerController.User), "UIServer",
|
||||
new { userId }, scheme, host, pathbase);
|
||||
}
|
||||
|
||||
public static string StoreUsersLink(this LinkGenerator urlHelper, string storeId, string scheme, HostString host, string pathbase)
|
||||
{
|
||||
return urlHelper.GetUriByAction(nameof(UIStoresController.StoreUsers), "UIStores",
|
||||
new { storeId }, scheme, host, pathbase);
|
||||
}
|
||||
|
||||
public static string InvitationLink(this LinkGenerator urlHelper, string userId, string code, string scheme, HostString host, string pathbase)
|
||||
{
|
||||
return urlHelper.GetUriByAction(nameof(UIAccountController.AcceptInvite), "UIAccount",
|
||||
new { userId, code }, scheme, host, pathbase);
|
||||
}
|
||||
|
||||
public static string EmailConfirmationLink(this LinkGenerator urlHelper, string userId, string code, string scheme, HostString host, string pathbase)
|
||||
{
|
||||
return urlHelper.GetUriByAction(nameof(UIAccountController.ConfirmEmail), "UIAccount",
|
||||
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 LoginCodeLink(this LinkGenerator urlHelper, string loginCode, string returnUrl, string scheme, HostString host, string pathbase)
|
||||
{
|
||||
return urlHelper.GetUriByAction(nameof(UIAccountController.LoginUsingCode), "UIAccount", new { loginCode, returnUrl }, scheme, host, pathbase);
|
||||
}
|
||||
|
||||
public static string ResetPasswordLink(this LinkGenerator urlHelper, string userId, string code, string scheme, HostString host, string pathbase)
|
||||
{
|
||||
return urlHelper.GetUriByAction(
|
||||
action: nameof(UIAccountController.SetPassword),
|
||||
controller: "UIAccount",
|
||||
values: new { userId, code },
|
||||
scheme: scheme,
|
||||
host: host,
|
||||
pathBase: pathbase
|
||||
);
|
||||
}
|
||||
|
||||
public static string PaymentRequestLink(this LinkGenerator urlHelper, string paymentRequestId, string scheme, HostString host, string pathbase)
|
||||
{
|
||||
return urlHelper.GetUriByAction(
|
||||
|
@ -1,4 +1,3 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
@ -9,10 +8,7 @@ using BTCPayServer.Services.Mails;
|
||||
using BTCPayServer.Services.Notifications;
|
||||
using BTCPayServer.Services.Notifications.Blobs;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace BTCPayServer.HostedServices;
|
||||
@ -20,41 +16,41 @@ namespace BTCPayServer.HostedServices;
|
||||
public class UserEventHostedService(
|
||||
EventAggregator eventAggregator,
|
||||
UserManager<ApplicationUser> userManager,
|
||||
CallbackGenerator callbackGenerator,
|
||||
EmailSenderFactory emailSenderFactory,
|
||||
NotificationSender notificationSender,
|
||||
StoreRepository storeRepository,
|
||||
LinkGenerator generator,
|
||||
Logs logs)
|
||||
: EventHostedServiceBase(eventAggregator, logs)
|
||||
{
|
||||
public UserManager<ApplicationUser> UserManager { get; } = userManager;
|
||||
public CallbackGenerator CallbackGenerator { get; } = callbackGenerator;
|
||||
|
||||
protected override void SubscribeToEvents()
|
||||
{
|
||||
Subscribe<UserRegisteredEvent>();
|
||||
Subscribe<UserApprovedEvent>();
|
||||
Subscribe<UserConfirmedEmailEvent>();
|
||||
Subscribe<UserPasswordResetRequestedEvent>();
|
||||
Subscribe<UserInviteAcceptedEvent>();
|
||||
Subscribe<UserEvent.Registered>();
|
||||
Subscribe<UserEvent.Invited>();
|
||||
Subscribe<UserEvent.Approved>();
|
||||
Subscribe<UserEvent.ConfirmedEmail>();
|
||||
Subscribe<UserEvent.PasswordResetRequested>();
|
||||
Subscribe<UserEvent.InviteAccepted>();
|
||||
}
|
||||
|
||||
protected override async Task ProcessEvent(object evt, CancellationToken cancellationToken)
|
||||
{
|
||||
string code;
|
||||
string callbackUrl;
|
||||
Uri uri;
|
||||
HostString host;
|
||||
ApplicationUser user;
|
||||
ApplicationUser user = (evt as UserEvent).User;
|
||||
IEmailSender emailSender;
|
||||
switch (evt)
|
||||
{
|
||||
case UserRegisteredEvent ev:
|
||||
user = ev.User;
|
||||
uri = ev.RequestUri;
|
||||
host = new HostString(uri.Host, uri.Port);
|
||||
|
||||
case UserEvent.Registered ev:
|
||||
// can be either a self-registration or by invite from another user
|
||||
var isInvite = ev.Kind == UserRegisteredEventKind.Invite;
|
||||
var type = ev.Admin ? "admin" : "user";
|
||||
var info = isInvite ? ev.InvitedByUser != null ? $"invited by {ev.InvitedByUser.Email}" : "invited" : "registered";
|
||||
var type = await UserManager.IsInRoleAsync(user, Roles.ServerAdmin) ? "admin" : "user";
|
||||
var info = ev switch
|
||||
{
|
||||
UserEvent.Invited { InvitedByUser: { } invitedBy } => $"invited by {invitedBy.Email}",
|
||||
UserEvent.Invited => "invited",
|
||||
_ => "registered"
|
||||
};
|
||||
var requiresApproval = user.RequiresApproval && !user.Approved;
|
||||
var requiresEmailConfirmation = user.RequiresEmailConfirmation && !user.EmailConfirmed;
|
||||
|
||||
@ -66,84 +62,55 @@ public class UserEventHostedService(
|
||||
// inform admins only about qualified users and not annoy them with bot registrations.
|
||||
if (requiresApproval && !requiresEmailConfirmation)
|
||||
{
|
||||
await NotifyAdminsAboutUserRequiringApproval(user, uri, newUserInfo);
|
||||
await NotifyAdminsAboutUserRequiringApproval(user, ev.ApprovalLink, newUserInfo);
|
||||
}
|
||||
|
||||
// set callback result and send email to user
|
||||
emailSender = await emailSenderFactory.GetEmailSender();
|
||||
if (isInvite)
|
||||
if (ev is UserEvent.Invited invited)
|
||||
{
|
||||
code = await userManager.GenerateInvitationTokenAsync<ApplicationUser>(user.Id);
|
||||
callbackUrl = generator.InvitationLink(user.Id, code, uri.Scheme, host, uri.PathAndQuery);
|
||||
ev.CallbackUrlGenerated?.SetResult(new Uri(callbackUrl));
|
||||
|
||||
if (ev.SendInvitationEmail)
|
||||
emailSender.SendInvitation(user.GetMailboxAddress(), callbackUrl);
|
||||
if (invited.SendInvitationEmail)
|
||||
emailSender.SendInvitation(user.GetMailboxAddress(), invited.InvitationLink);
|
||||
}
|
||||
else if (requiresEmailConfirmation)
|
||||
{
|
||||
code = await userManager.GenerateEmailConfirmationTokenAsync(user);
|
||||
callbackUrl = generator.EmailConfirmationLink(user.Id, code, uri.Scheme, host, uri.PathAndQuery);
|
||||
ev.CallbackUrlGenerated?.SetResult(new Uri(callbackUrl));
|
||||
|
||||
emailSender.SendEmailConfirmation(user.GetMailboxAddress(), callbackUrl);
|
||||
}
|
||||
else
|
||||
{
|
||||
ev.CallbackUrlGenerated?.SetResult(null);
|
||||
emailSender.SendEmailConfirmation(user.GetMailboxAddress(), ev.ConfirmationEmailLink);
|
||||
}
|
||||
break;
|
||||
|
||||
case UserPasswordResetRequestedEvent pwResetEvent:
|
||||
user = pwResetEvent.User;
|
||||
uri = pwResetEvent.RequestUri;
|
||||
host = new HostString(uri.Host, uri.Port);
|
||||
code = await userManager.GeneratePasswordResetTokenAsync(user);
|
||||
callbackUrl = generator.ResetPasswordLink(user.Id, code, uri.Scheme, host, uri.PathAndQuery);
|
||||
pwResetEvent.CallbackUrlGenerated?.SetResult(new Uri(callbackUrl));
|
||||
case UserEvent.PasswordResetRequested pwResetEvent:
|
||||
Logs.PayServer.LogInformation("User {Email} requested a password reset", user.Email);
|
||||
emailSender = await emailSenderFactory.GetEmailSender();
|
||||
emailSender.SendResetPassword(user.GetMailboxAddress(), callbackUrl);
|
||||
emailSender.SendResetPassword(user.GetMailboxAddress(), pwResetEvent.ResetLink);
|
||||
break;
|
||||
|
||||
case UserApprovedEvent approvedEvent:
|
||||
user = approvedEvent.User;
|
||||
case UserEvent.Approved approvedEvent:
|
||||
if (!user.Approved) break;
|
||||
uri = approvedEvent.RequestUri;
|
||||
host = new HostString(uri.Host, uri.Port);
|
||||
callbackUrl = generator.LoginLink(uri.Scheme, host, uri.PathAndQuery);
|
||||
emailSender = await emailSenderFactory.GetEmailSender();
|
||||
emailSender.SendApprovalConfirmation(user.GetMailboxAddress(), callbackUrl);
|
||||
emailSender.SendApprovalConfirmation(user.GetMailboxAddress(), approvedEvent.LoginLink);
|
||||
break;
|
||||
|
||||
case UserConfirmedEmailEvent confirmedEvent:
|
||||
user = confirmedEvent.User;
|
||||
case UserEvent.ConfirmedEmail confirmedEvent:
|
||||
if (!user.EmailConfirmed) break;
|
||||
uri = confirmedEvent.RequestUri;
|
||||
var confirmedUserInfo = $"User {user.Email} confirmed their email address";
|
||||
Logs.PayServer.LogInformation(confirmedUserInfo);
|
||||
if (!user.RequiresApproval || user.Approved) return;
|
||||
await NotifyAdminsAboutUserRequiringApproval(user, uri, confirmedUserInfo);
|
||||
await NotifyAdminsAboutUserRequiringApproval(user, confirmedEvent.ApprovalLink, confirmedUserInfo);
|
||||
break;
|
||||
|
||||
case UserInviteAcceptedEvent inviteAcceptedEvent:
|
||||
user = inviteAcceptedEvent.User;
|
||||
uri = inviteAcceptedEvent.RequestUri;
|
||||
case UserEvent.InviteAccepted inviteAcceptedEvent:
|
||||
Logs.PayServer.LogInformation("User {Email} accepted the invite", user.Email);
|
||||
await NotifyAboutUserAcceptingInvite(user, uri);
|
||||
await NotifyAboutUserAcceptingInvite(user, inviteAcceptedEvent.StoreUsersLink);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task NotifyAdminsAboutUserRequiringApproval(ApplicationUser user, Uri uri, string newUserInfo)
|
||||
private async Task NotifyAdminsAboutUserRequiringApproval(ApplicationUser user, string approvalLink, string newUserInfo)
|
||||
{
|
||||
if (!user.RequiresApproval || user.Approved) return;
|
||||
// notification
|
||||
await notificationSender.SendNotification(new AdminScope(), new NewUserRequiresApprovalNotification(user));
|
||||
// email
|
||||
var admins = await userManager.GetUsersInRoleAsync(Roles.ServerAdmin);
|
||||
var host = new HostString(uri.Host, uri.Port);
|
||||
var approvalLink = generator.UserDetailsLink(user.Id, uri.Scheme, host, uri.PathAndQuery);
|
||||
var admins = await UserManager.GetUsersInRoleAsync(Roles.ServerAdmin);
|
||||
var emailSender = await emailSenderFactory.GetEmailSender();
|
||||
foreach (var admin in admins)
|
||||
{
|
||||
@ -151,7 +118,7 @@ public class UserEventHostedService(
|
||||
}
|
||||
}
|
||||
|
||||
private async Task NotifyAboutUserAcceptingInvite(ApplicationUser user, Uri uri)
|
||||
private async Task NotifyAboutUserAcceptingInvite(ApplicationUser user, string storeUsersLink)
|
||||
{
|
||||
var stores = await storeRepository.GetStoresByUserId(user.Id);
|
||||
var notifyRoles = new[] { StoreRoleId.Owner, StoreRoleId.Manager };
|
||||
@ -161,15 +128,14 @@ public class UserEventHostedService(
|
||||
await notificationSender.SendNotification(new StoreScope(store.Id, notifyRoles), new InviteAcceptedNotification(user, store));
|
||||
// email
|
||||
var notifyUsers = await storeRepository.GetStoreUsers(store.Id, notifyRoles);
|
||||
var host = new HostString(uri.Host, uri.Port);
|
||||
var storeUsersLink = generator.StoreUsersLink(store.Id, uri.Scheme, host, uri.PathAndQuery);
|
||||
var link = string.Format(storeUsersLink, store.Id);
|
||||
var emailSender = await emailSenderFactory.GetEmailSender(store.Id);
|
||||
foreach (var storeUser in notifyUsers)
|
||||
{
|
||||
if (storeUser.Id == user.Id) continue; // do not notify the user itself (if they were added as owner or manager)
|
||||
var notifyUser = await userManager.FindByIdOrEmail(storeUser.Id);
|
||||
var notifyUser = await UserManager.FindByIdOrEmail(storeUser.Id);
|
||||
var info = $"User {user.Email} accepted the invite to {store.StoreName}";
|
||||
emailSender.SendUserInviteAcceptedInfo(notifyUser.GetMailboxAddress(), info, storeUsersLink);
|
||||
emailSender.SendUserInviteAcceptedInfo(notifyUser.GetMailboxAddress(), info, link);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -86,6 +86,7 @@ namespace BTCPayServer.Hosting
|
||||
}
|
||||
public static IServiceCollection AddBTCPayServer(this IServiceCollection services, IConfiguration configuration, Logs logs)
|
||||
{
|
||||
services.TryAddSingleton<CallbackGenerator>();
|
||||
services.TryAddSingleton<IStringLocalizerFactory, LocalizerFactory>();
|
||||
services.TryAddSingleton<IHtmlLocalizerFactory, LocalizerFactory>();
|
||||
services.TryAddSingleton<LocalizerService>();
|
||||
|
@ -154,7 +154,7 @@ namespace BTCPayServer.Payments.Bitcoin
|
||||
{
|
||||
case NBXplorer.Models.NewBlockEvent evt:
|
||||
await UpdatePaymentStates(wallet);
|
||||
_Aggregator.Publish(new Events.NewBlockEvent() { PaymentMethodId = pmi });
|
||||
_Aggregator.Publish(new Events.NewBlockEvent() { PaymentMethodId = pmi, AdditionalInfo = evt });
|
||||
break;
|
||||
case NBXplorer.Models.NewTransactionEvent evt:
|
||||
if (evt.DerivationStrategy != null)
|
||||
|
@ -3,11 +3,9 @@ using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Channels;
|
||||
using System.Threading.Tasks;
|
||||
using Amazon.Runtime.Internal;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Configuration;
|
||||
using BTCPayServer.Data;
|
||||
@ -18,13 +16,11 @@ using BTCPayServer.Payments.Bitcoin;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using NBitcoin;
|
||||
using NBitpayClient;
|
||||
using NBXplorer;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
@ -263,15 +259,24 @@ retry:
|
||||
{
|
||||
lock (_InstanceListeners)
|
||||
{
|
||||
foreach ((_, var instance) in _InstanceListeners.ToArray())
|
||||
foreach (var key in _InstanceListeners.Keys)
|
||||
{
|
||||
instance.RemoveExpiredInvoices();
|
||||
if (!instance.Empty)
|
||||
instance.EnsureListening(_Cts.Token);
|
||||
CheckConnection(key.Item1, key.Item2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void CheckConnection(string cryptoCode, string connStr)
|
||||
{
|
||||
if (_InstanceListeners.TryGetValue((cryptoCode, connStr), out var instance))
|
||||
{
|
||||
|
||||
instance.RemoveExpiredInvoices();
|
||||
if (!instance.Empty)
|
||||
instance.EnsureListening(_Cts.Token);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CreateNewLNInvoiceForBTCPayInvoice(InvoiceEntity invoice)
|
||||
{
|
||||
var paymentMethods = GetLightningPrompts(invoice).ToArray();
|
||||
|
@ -160,17 +160,25 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
|
||||
if (await Throttle(appId))
|
||||
return new TooManyRequestsResult(ZoneLimits.PublicInvoices);
|
||||
|
||||
// Distinguish JSON requests coming via the mobile app
|
||||
var wantsJson = Request.Headers.Accept.FirstOrDefault()?.StartsWith("application/json") is true;
|
||||
|
||||
var app = await _appService.GetApp(appId, PointOfSaleAppType.AppType);
|
||||
if (app == null)
|
||||
return wantsJson
|
||||
? Json(new { error = "App not found" })
|
||||
: NotFound();
|
||||
|
||||
// not allowing negative tips or discounts
|
||||
if (tip < 0 || discount < 0)
|
||||
return RedirectToAction(nameof(ViewPointOfSale), new { appId });
|
||||
return wantsJson
|
||||
? Json(new { error = "Negative tip or discount is not allowed" })
|
||||
: RedirectToAction(nameof(ViewPointOfSale), new { appId });
|
||||
|
||||
if (string.IsNullOrEmpty(choiceKey) && amount <= 0)
|
||||
return RedirectToAction(nameof(ViewPointOfSale), new { appId });
|
||||
|
||||
if (app == null)
|
||||
return NotFound();
|
||||
return wantsJson
|
||||
? Json(new { error = "Negative amount is not allowed" })
|
||||
: RedirectToAction(nameof(ViewPointOfSale), new { appId });
|
||||
|
||||
var settings = app.GetSettings<PointOfSaleSettings>();
|
||||
settings.DefaultView = settings.EnableShoppingCart ? PosViewType.Cart : settings.DefaultView;
|
||||
@ -180,6 +188,7 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
|
||||
{
|
||||
return RedirectToAction(nameof(ViewPointOfSale), new { appId, viewType });
|
||||
}
|
||||
|
||||
var jposData = TryParseJObject(posData);
|
||||
string title;
|
||||
decimal? price;
|
||||
@ -235,9 +244,10 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
|
||||
switch (itemChoice.Inventory)
|
||||
{
|
||||
case <= 0:
|
||||
return RedirectToAction(nameof(ViewPointOfSale), new { appId });
|
||||
case { } inventory when inventory < cartItem.Count:
|
||||
return RedirectToAction(nameof(ViewPointOfSale), new { appId });
|
||||
return wantsJson
|
||||
? Json(new { error = $"Inventory for {itemChoice.Title} exhausted: {itemChoice.Inventory} available" })
|
||||
: RedirectToAction(nameof(ViewPointOfSale), new { appId });
|
||||
}
|
||||
}
|
||||
|
||||
@ -262,8 +272,9 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
|
||||
var store = await _appService.GetStore(app);
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
var posFormId = settings.FormId;
|
||||
var formData = await FormDataService.GetForm(posFormId);
|
||||
|
||||
// skip forms feature for JSON requests (from the app)
|
||||
var formData = wantsJson ? null : await FormDataService.GetForm(posFormId);
|
||||
JObject formResponseJObject = null;
|
||||
switch (formData)
|
||||
{
|
||||
@ -337,7 +348,7 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
|
||||
RedirectAutomatically = settings.RedirectAutomatically,
|
||||
RedirectURL = !string.IsNullOrEmpty(redirectUrl) ? redirectUrl
|
||||
: !string.IsNullOrEmpty(settings.RedirectUrl) ? settings.RedirectUrl
|
||||
: Request.GetAbsoluteUri(Url.Action(nameof(ViewPointOfSale), "UIPointOfSale", new { appId, viewType })),
|
||||
: Url.ActionAbsolute(Request, nameof(ViewPointOfSale), "UIPointOfSale", new { appId, viewType }).ToString(),
|
||||
PaymentMethods = paymentMethods?.Where(p => p.Value.Enabled).Select(p => p.Key).ToArray()
|
||||
},
|
||||
AdditionalSearchTerms = new[] { AppService.GetAppSearchTerm(app) }
|
||||
@ -411,14 +422,16 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
|
||||
meta.Merge(formResponseJObject);
|
||||
entity.Metadata = InvoiceMetadata.FromJObject(meta);
|
||||
});
|
||||
var data = new { invoiceId = invoice.Id };
|
||||
if (wantsJson)
|
||||
return Json(data);
|
||||
if (price is 0 && storeBlob.ReceiptOptions?.Enabled is true)
|
||||
{
|
||||
return RedirectToAction(nameof(UIInvoiceController.InvoiceReceipt), "UIInvoice", new { invoiceId = invoice.Id });
|
||||
}
|
||||
return RedirectToAction(nameof(UIInvoiceController.Checkout), "UIInvoice", new { invoiceId = invoice.Id });
|
||||
return RedirectToAction(nameof(UIInvoiceController.InvoiceReceipt), "UIInvoice", data);
|
||||
return RedirectToAction(nameof(UIInvoiceController.Checkout), "UIInvoice", data);
|
||||
}
|
||||
catch (BitpayHttpException e)
|
||||
{
|
||||
if (wantsJson) return Json(new { error = e.Message });
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
{
|
||||
Html = e.Message.Replace("\n", "<br />", StringComparison.OrdinalIgnoreCase),
|
||||
@ -521,7 +534,7 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
|
||||
{
|
||||
var controller = nameof(UIPointOfSaleController).TrimEnd("Controller", StringComparison.InvariantCulture);
|
||||
var redirectUrl =
|
||||
Request.GetAbsoluteUri(Url.Action(nameof(ViewPointOfSale), controller, new { appId, viewType }));
|
||||
Url.ActionAbsolute(Request, nameof(ViewPointOfSale), controller, new { appId, viewType }).ToString();
|
||||
formParameters.Add("formResponse", FormDataService.GetValues(form).ToString());
|
||||
return View("PostRedirect", new PostRedirectViewModel
|
||||
{
|
||||
|
@ -6,7 +6,9 @@ using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Events;
|
||||
using BTCPayServer.Models.AppViewModels;
|
||||
using BTCPayServer.Plugins.Crowdfund;
|
||||
using BTCPayServer.Plugins.PointOfSale;
|
||||
@ -28,7 +30,7 @@ namespace BTCPayServer.Services.Apps
|
||||
private readonly Dictionary<string, AppBaseType> _appTypes;
|
||||
static AppService()
|
||||
{
|
||||
_defaultSerializer = new JsonSerializerSettings()
|
||||
_defaultSerializer = new JsonSerializerSettings
|
||||
{
|
||||
ContractResolver = new Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver(),
|
||||
Formatting = Formatting.None
|
||||
@ -40,8 +42,8 @@ namespace BTCPayServer.Services.Apps
|
||||
readonly ApplicationDbContextFactory _ContextFactory;
|
||||
private readonly InvoiceRepository _InvoiceRepository;
|
||||
readonly CurrencyNameTable _Currencies;
|
||||
private readonly DisplayFormatter _displayFormatter;
|
||||
private readonly StoreRepository _storeRepository;
|
||||
private readonly EventAggregator _eventAggregator;
|
||||
public CurrencyNameTable Currencies => _Currencies;
|
||||
private readonly string[] _paidStatuses = [
|
||||
InvoiceStatus.Processing.ToString(),
|
||||
@ -53,15 +55,15 @@ namespace BTCPayServer.Services.Apps
|
||||
ApplicationDbContextFactory contextFactory,
|
||||
InvoiceRepository invoiceRepository,
|
||||
CurrencyNameTable currencies,
|
||||
DisplayFormatter displayFormatter,
|
||||
StoreRepository storeRepository)
|
||||
StoreRepository storeRepository,
|
||||
EventAggregator eventAggregator)
|
||||
{
|
||||
_appTypes = apps.ToDictionary(a => a.Type, a => a);
|
||||
_ContextFactory = contextFactory;
|
||||
_InvoiceRepository = invoiceRepository;
|
||||
_Currencies = currencies;
|
||||
_storeRepository = storeRepository;
|
||||
_displayFormatter = displayFormatter;
|
||||
_eventAggregator = eventAggregator;
|
||||
}
|
||||
#nullable enable
|
||||
public Dictionary<string, string> GetAvailableAppTypes()
|
||||
@ -231,6 +233,7 @@ namespace BTCPayServer.Services.Apps
|
||||
await using var ctx = _ContextFactory.CreateContext();
|
||||
ctx.Apps.Add(appData);
|
||||
ctx.Entry(appData).State = EntityState.Deleted;
|
||||
_eventAggregator.Publish(new AppEvent.Deleted(appData));
|
||||
return await ctx.SaveChangesAsync() == 1;
|
||||
}
|
||||
|
||||
@ -239,6 +242,7 @@ namespace BTCPayServer.Services.Apps
|
||||
await using var ctx = _ContextFactory.CreateContext();
|
||||
appData.Archived = archived;
|
||||
ctx.Entry(appData).State = EntityState.Modified;
|
||||
_eventAggregator.Publish(new AppEvent.Updated(appData));
|
||||
return await ctx.SaveChangesAsync() == 1;
|
||||
}
|
||||
|
||||
@ -446,7 +450,8 @@ retry:
|
||||
public async Task UpdateOrCreateApp(AppData app)
|
||||
{
|
||||
await using var ctx = _ContextFactory.CreateContext();
|
||||
if (string.IsNullOrEmpty(app.Id))
|
||||
var newApp = string.IsNullOrEmpty(app.Id);
|
||||
if (newApp)
|
||||
{
|
||||
app.Id = Encoders.Base58.EncodeData(RandomUtils.GetBytes(20));
|
||||
app.Created = DateTimeOffset.UtcNow;
|
||||
@ -460,6 +465,10 @@ retry:
|
||||
ctx.Entry(app).Property(data => data.AppType).IsModified = false;
|
||||
}
|
||||
await ctx.SaveChangesAsync();
|
||||
if (newApp)
|
||||
_eventAggregator.Publish(new AppEvent.Created(app));
|
||||
else
|
||||
_eventAggregator.Publish(new AppEvent.Updated(app));
|
||||
}
|
||||
|
||||
private static bool TryParseJson(string json, [MaybeNullWhen(false)] out JObject result)
|
||||
|
84
BTCPayServer/Services/CallbackGenerator.cs
Normal file
84
BTCPayServer/Services/CallbackGenerator.cs
Normal file
@ -0,0 +1,84 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Reflection.Emit;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NBitcoin.Altcoins.ArgoneumInternals;
|
||||
using BTCPayServer.Controllers;
|
||||
using Microsoft.AspNetCore.Mvc.Routing;
|
||||
using Microsoft.AspNetCore.Http.Extensions;
|
||||
using NBitcoin.DataEncoders;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace BTCPayServer.Services
|
||||
{
|
||||
public class CallbackGenerator(LinkGenerator linkGenerator, UserManager<ApplicationUser> userManager)
|
||||
{
|
||||
public LinkGenerator LinkGenerator { get; } = linkGenerator;
|
||||
public UserManager<ApplicationUser> UserManager { get; } = userManager;
|
||||
|
||||
public string ForLNUrlAuth(ApplicationUser user, byte[] r, HttpRequest request)
|
||||
{
|
||||
return 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) ?? throw Bug();
|
||||
}
|
||||
|
||||
public string StoreUsersLink(string storeId, HttpRequest request)
|
||||
{
|
||||
return LinkGenerator.GetUriByAction(nameof(UIStoresController.StoreUsers), "UIStores",
|
||||
new { storeId }, request.Scheme, request.Host, request.PathBase) ?? throw Bug();
|
||||
}
|
||||
|
||||
public async Task<string> ForEmailConfirmation(ApplicationUser user, HttpRequest request)
|
||||
{
|
||||
var code = await UserManager.GenerateEmailConfirmationTokenAsync(user);
|
||||
return LinkGenerator.GetUriByAction(nameof(UIAccountController.ConfirmEmail), "UIAccount",
|
||||
new { userId = user.Id, code }, request.Scheme, request.Host, request.PathBase) ?? throw Bug();
|
||||
}
|
||||
public async Task<string> ForInvitation(ApplicationUser user, HttpRequest request)
|
||||
{
|
||||
var code = await UserManager.GenerateInvitationTokenAsync<ApplicationUser>(user.Id) ?? throw Bug();
|
||||
return ForInvitation(user, code, request);
|
||||
}
|
||||
public string ForInvitation(ApplicationUser user, string code, HttpRequest request)
|
||||
{
|
||||
return LinkGenerator.GetUriByAction(nameof(UIAccountController.AcceptInvite), "UIAccount",
|
||||
new { userId = user.Id, code }, request.Scheme, request.Host, request.PathBase) ?? throw Bug();
|
||||
}
|
||||
public async Task<string> ForPasswordReset(ApplicationUser user, HttpRequest request)
|
||||
{
|
||||
var code = await UserManager.GeneratePasswordResetTokenAsync(user);
|
||||
return LinkGenerator.GetUriByAction(
|
||||
action: nameof(UIAccountController.SetPassword),
|
||||
controller: "UIAccount",
|
||||
values: new { userId = user.Id, code },
|
||||
scheme: request.Scheme,
|
||||
host: request.Host,
|
||||
pathBase: request.PathBase
|
||||
) ?? throw Bug();
|
||||
}
|
||||
|
||||
public string ForApproval(ApplicationUser user, HttpRequest request)
|
||||
{
|
||||
return LinkGenerator.GetUriByAction(nameof(UIServerController.User), "UIServer",
|
||||
new { userId = user.Id }, request.Scheme, request.Host, request.PathBase) ?? throw Bug();
|
||||
}
|
||||
public string ForLogin(ApplicationUser user, HttpRequest request)
|
||||
{
|
||||
return LinkGenerator.GetUriByAction(nameof(UIAccountController.Login), "UIAccount", new { email = user.Email }, request.Scheme, request.Host, request.PathBase) ?? throw Bug();
|
||||
}
|
||||
|
||||
private Exception Bug([CallerMemberName] string? name = null) => new InvalidOperationException($"Error generating link for {name} (Report this bug to BTCPay Server github repository)");
|
||||
}
|
||||
}
|
@ -27,52 +27,9 @@ public class LightningHistogramService
|
||||
var ticks = (to - from).Ticks;
|
||||
var interval = TimeSpan.FromTicks(ticks / pointCount);
|
||||
|
||||
try
|
||||
{
|
||||
// general balance
|
||||
var lnBalance = await lightningClient.GetBalance(cancellationToken);
|
||||
var total = lnBalance.OffchainBalance.Local;
|
||||
var totalBtc = total.ToDecimal(LightMoneyUnit.BTC);
|
||||
// prepare transaction data
|
||||
var lnInvoices = await lightningClient.ListInvoices(cancellationToken);
|
||||
var lnPayments = await lightningClient.ListPayments(cancellationToken);
|
||||
var lnTransactions = lnInvoices
|
||||
.Where(inv => inv.Status == LightningInvoiceStatus.Paid && inv.PaidAt >= from)
|
||||
.Select(inv => new LnTx { Amount = inv.Amount.ToDecimal(LightMoneyUnit.BTC), Settled = inv.PaidAt.GetValueOrDefault() })
|
||||
.Concat(lnPayments
|
||||
.Where(pay => pay.Status == LightningPaymentStatus.Complete && pay.CreatedAt >= from)
|
||||
.Select(pay => new LnTx { Amount = pay.Amount.ToDecimal(LightMoneyUnit.BTC) * -1, Settled = pay.CreatedAt.GetValueOrDefault() }))
|
||||
.OrderByDescending(tx => tx.Settled)
|
||||
.ToList();
|
||||
// assemble graph data going backwards
|
||||
var series = new List<decimal>(pointCount);
|
||||
var labels = new List<DateTimeOffset>(pointCount);
|
||||
var balance = totalBtc;
|
||||
for (var i = pointCount; i > 0; i--)
|
||||
{
|
||||
var txs = lnTransactions.Where(t =>
|
||||
t.Settled.Ticks >= from.Ticks + interval.Ticks * i &&
|
||||
t.Settled.Ticks < from.Ticks + interval.Ticks * (i + 1));
|
||||
var sum = txs.Sum(tx => tx.Amount);
|
||||
balance -= sum;
|
||||
series.Add(balance);
|
||||
labels.Add(from + interval * (i - 1));
|
||||
}
|
||||
// reverse the lists
|
||||
series.Reverse();
|
||||
labels.Reverse();
|
||||
return new HistogramData
|
||||
{
|
||||
Type = type,
|
||||
Balance = totalBtc,
|
||||
Series = series,
|
||||
Labels = labels
|
||||
};
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
// TODO: We can't just list all invoices and payments, we need to filter them by date
|
||||
// but the client doesn't support that yet so let's just disable this for now. See #6518
|
||||
return null;
|
||||
}
|
||||
|
||||
private class LnTx
|
||||
|
@ -2,19 +2,16 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection.Metadata;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Contracts;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Events;
|
||||
using BTCPayServer.Migrations;
|
||||
using Dapper;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NBitcoin;
|
||||
using NBitcoin.DataEncoders;
|
||||
using Newtonsoft.Json;
|
||||
using static BTCPayServer.Services.Stores.StoreRepository;
|
||||
|
||||
namespace BTCPayServer.Services.Stores
|
||||
{
|
||||
@ -157,6 +154,7 @@ namespace BTCPayServer.Services.Stores
|
||||
return "This is the last role that allows to modify store settings, you cannot remove it";
|
||||
ctx.StoreRoles.Remove(match);
|
||||
await ctx.SaveChangesAsync();
|
||||
_eventAggregator.Publish(new StoreRoleEvent.Removed(role.StoreId!, role.Id));
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -168,15 +166,21 @@ namespace BTCPayServer.Services.Stores
|
||||
policies = policies.Where(s => Policies.IsValidPolicy(s) && Policies.IsStorePolicy(s)).ToList();
|
||||
await using var ctx = _ContextFactory.CreateContext();
|
||||
Data.StoreRole? match = await ctx.StoreRoles.FindAsync(role.Id);
|
||||
var added = false;
|
||||
if (match is null)
|
||||
{
|
||||
match = new Data.StoreRole() { Id = role.Id, StoreDataId = role.StoreId, Role = role.Role };
|
||||
match = new Data.StoreRole { Id = role.Id, StoreDataId = role.StoreId, Role = role.Role };
|
||||
ctx.StoreRoles.Add(match);
|
||||
added = true;
|
||||
}
|
||||
match.Permissions = policies;
|
||||
try
|
||||
{
|
||||
await ctx.SaveChangesAsync();
|
||||
StoreRoleEvent evt = added
|
||||
? new StoreRoleEvent.Added(role.StoreId!, role.Id)
|
||||
: new StoreRoleEvent.Updated(role.StoreId!, role.Id);
|
||||
_eventAggregator.Publish(evt);
|
||||
}
|
||||
catch (DbUpdateException)
|
||||
{
|
||||
@ -301,6 +305,7 @@ namespace BTCPayServer.Services.Stores
|
||||
try
|
||||
{
|
||||
await ctx.SaveChangesAsync();
|
||||
_eventAggregator.Publish(new StoreUserEvent.Added(storeId, userId, roleId.Id));
|
||||
return true;
|
||||
}
|
||||
catch (DbUpdateException)
|
||||
@ -316,10 +321,12 @@ namespace BTCPayServer.Services.Stores
|
||||
roleId ??= await GetDefaultRole();
|
||||
await using var ctx = _ContextFactory.CreateContext();
|
||||
var userStore = await ctx.UserStore.FindAsync(userId, storeId);
|
||||
var added = false;
|
||||
if (userStore is null)
|
||||
{
|
||||
userStore = new UserStore { StoreDataId = storeId, ApplicationUserId = userId };
|
||||
ctx.UserStore.Add(userStore);
|
||||
added = true;
|
||||
}
|
||||
|
||||
if (userStore.StoreRoleId == roleId.Id)
|
||||
@ -329,6 +336,10 @@ namespace BTCPayServer.Services.Stores
|
||||
try
|
||||
{
|
||||
await ctx.SaveChangesAsync();
|
||||
StoreUserEvent evt = added
|
||||
? new StoreUserEvent.Added(storeId, userId, userStore.StoreRoleId)
|
||||
: new StoreUserEvent.Updated(storeId, userId, userStore.StoreRoleId);
|
||||
_eventAggregator.Publish(evt);
|
||||
return true;
|
||||
}
|
||||
catch (DbUpdateException)
|
||||
@ -343,22 +354,6 @@ namespace BTCPayServer.Services.Stores
|
||||
throw new ArgumentException("The roleId doesn't belong to this storeId", nameof(roleId));
|
||||
}
|
||||
|
||||
public async Task CleanUnreachableStores()
|
||||
{
|
||||
await using var ctx = _ContextFactory.CreateContext();
|
||||
var events = new List<Events.StoreRemovedEvent>();
|
||||
foreach (var store in await ctx.Stores.Include(data => data.UserStores)
|
||||
.ThenInclude(store => store.StoreRole).Where(s =>
|
||||
s.UserStores.All(u => !u.StoreRole.Permissions.Contains(Policies.CanModifyStoreSettings)))
|
||||
.ToArrayAsync())
|
||||
{
|
||||
ctx.Stores.Remove(store);
|
||||
events.Add(new Events.StoreRemovedEvent(store.Id));
|
||||
}
|
||||
await ctx.SaveChangesAsync();
|
||||
events.ForEach(e => _eventAggregator.Publish(e));
|
||||
}
|
||||
|
||||
public async Task<bool> RemoveStoreUser(string storeId, string userId)
|
||||
{
|
||||
await using var ctx = _ContextFactory.CreateContext();
|
||||
@ -370,8 +365,8 @@ namespace BTCPayServer.Services.Stores
|
||||
ctx.UserStore.Add(userStore);
|
||||
ctx.Entry(userStore).State = EntityState.Deleted;
|
||||
await ctx.SaveChangesAsync();
|
||||
_eventAggregator.Publish(new StoreUserEvent.Removed(storeId, userId));
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
private async Task DeleteStoreIfOrphan(string storeId)
|
||||
@ -384,7 +379,7 @@ namespace BTCPayServer.Services.Stores
|
||||
{
|
||||
ctx.Stores.Remove(store);
|
||||
await ctx.SaveChangesAsync();
|
||||
_eventAggregator.Publish(new StoreRemovedEvent(store.Id));
|
||||
_eventAggregator.Publish(new StoreEvent.Removed(store));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -410,6 +405,8 @@ namespace BTCPayServer.Services.Stores
|
||||
ctx.Add(storeData);
|
||||
ctx.Add(userStore);
|
||||
await ctx.SaveChangesAsync();
|
||||
_eventAggregator.Publish(new StoreUserEvent.Added(storeData.Id, userStore.ApplicationUserId, roleId.Id));
|
||||
_eventAggregator.Publish(new StoreEvent.Created(storeData));
|
||||
}
|
||||
|
||||
public async Task<WebhookData[]> GetWebhooks(string storeId)
|
||||
@ -558,6 +555,7 @@ namespace BTCPayServer.Services.Stores
|
||||
{
|
||||
ctx.Entry(existing).CurrentValues.SetValues(store);
|
||||
await ctx.SaveChangesAsync().ConfigureAwait(false);
|
||||
_eventAggregator.Publish(new StoreEvent.Updated(store));
|
||||
}
|
||||
}
|
||||
|
||||
@ -579,6 +577,8 @@ retry:
|
||||
try
|
||||
{
|
||||
await ctx.SaveChangesAsync();
|
||||
if (store != null)
|
||||
_eventAggregator.Publish(new StoreEvent.Removed(store));
|
||||
}
|
||||
catch (DbUpdateException ex) when (IsDeadlock(ex) && retry < 5)
|
||||
{
|
||||
|
@ -108,7 +108,7 @@ namespace BTCPayServer.Services
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<bool> SetUserApproval(string userId, bool approved, Uri requestUri)
|
||||
public async Task<bool> SetUserApproval(string userId, bool approved, string loginLink)
|
||||
{
|
||||
using var scope = _serviceProvider.CreateScope();
|
||||
var userManager = scope.ServiceProvider.GetRequiredService<UserManager<ApplicationUser>>();
|
||||
@ -123,7 +123,7 @@ namespace BTCPayServer.Services
|
||||
if (succeeded)
|
||||
{
|
||||
_logger.LogInformation("User {Email} is now {Status}", user.Email, approved ? "approved" : "unapproved");
|
||||
_eventAggregator.Publish(new UserApprovedEvent { User = user, RequestUri = requestUri });
|
||||
_eventAggregator.Publish(new UserEvent.Approved(user, loginLink));
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -55,7 +55,7 @@ Vue.component("lnurl-withdraw-checkout", {
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
url: @Safe.Json(Context.Request.GetAbsoluteUri(Url.Action("SubmitLNURLWithdrawForInvoice", "NFC"))),
|
||||
url: @Safe.Json(Url.ActionAbsolute(Context.Request, "SubmitLNURLWithdrawForInvoice", "NFC").ToString()),
|
||||
amount: 0,
|
||||
submitting: false
|
||||
}
|
||||
|
File diff suppressed because one or more lines are too long
@ -18,6 +18,7 @@
|
||||
Create Form
|
||||
</a>
|
||||
</div>
|
||||
<partial name="_StatusMessage" />
|
||||
|
||||
<div class="row">
|
||||
<div class="col-xxl-constrain col-xl-10">
|
||||
|
@ -215,6 +215,7 @@
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<partial name="_StatusMessage" />
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
@if (!ViewContext.ModelState.IsValid)
|
||||
|
@ -3,7 +3,6 @@
|
||||
@inject LanguageService LangService
|
||||
@inject BTCPayServerEnvironment Env
|
||||
@inject IEnumerable<IUIExtension> UiExtensions
|
||||
@inject PaymentMethodHandlerDictionary PaymentMethodHandlerDictionary
|
||||
@inject BTCPayServer.Security.ContentSecurityPolicies Csp
|
||||
@model CheckoutModel
|
||||
@{
|
||||
@ -12,7 +11,6 @@
|
||||
ViewData["StoreBranding"] = Model.StoreBranding;
|
||||
Csp.UnsafeEval();
|
||||
var hasPaymentPlugins = UiExtensions.Any(extension => extension.Location == "checkout-payment-method");
|
||||
var checkoutLink = Url.Action("Checkout", new { invoiceId = Model.InvoiceId });
|
||||
}
|
||||
@functions {
|
||||
private string ToJsValue(object value)
|
||||
@ -93,16 +91,15 @@
|
||||
<div v-if="displayedPaymentMethods.length > 1 || @Safe.Json(hasPaymentPlugins)" class="mt-3 mb-2">
|
||||
<h6 class="text-center mb-3" v-t="'pay_with'"></h6>
|
||||
<div class="btcpay-pills d-flex flex-wrap align-items-center justify-content-center gap-2 pb-2">
|
||||
<a
|
||||
v-for="crypto in displayedPaymentMethods"
|
||||
:href="@ToJsValue(checkoutLink) + '/' + crypto.paymentMethodId"
|
||||
<a v-for="crypto in displayedPaymentMethods"
|
||||
asp-action="Checkout" asp-route-invoiceId="@Model.InvoiceId"
|
||||
class="btcpay-pill m-0 payment-method"
|
||||
:class="{ active: srvModel.paymentMethodId === crypto.paymentMethodId }"
|
||||
:class="{ active: pmId === crypto.paymentMethodId }"
|
||||
v-on:click.prevent="changePaymentMethod(crypto.paymentMethodId)"
|
||||
v-text="crypto.paymentMethodName">
|
||||
</a>
|
||||
@await Component.InvokeAsync("UiExtensionPoint", new { location = "checkout-payment-method", model = Model })
|
||||
</div>
|
||||
@await Component.InvokeAsync("UiExtensionPoint", new { location = "checkout-payment-method", model = Model })
|
||||
</div>
|
||||
<component v-if="paymentMethodComponent" :is="paymentMethodComponent"
|
||||
:model="srvModel"
|
||||
@ -222,7 +219,6 @@
|
||||
<p class="text-center mt-3" v-html="replaceNewlines($t(isPaidPartial ? 'invoice_paidpartial_body' : 'invoice_expired_body', { storeName: srvModel.storeName, minutes: srvModel.maxTimeMinutes }))"></p>
|
||||
</div>
|
||||
<div class="buttons" v-if="(isPaidPartial && srvModel.storeSupportUrl) || storeLink || isModal">
|
||||
<a v-if="isPaidPartial && srvModel.storeSupportUrl" class="btn btn-primary rounded-pill w-100" :href="srvModel.storeSupportUrl" v-t="'contact_us'" id="ContactLink"></a>
|
||||
<a v-if="storeLink" class="btn btn-primary rounded-pill w-100" :href="storeLink" :target="isModal ? '_top' : null" v-html="$t('return_to_store', { storeName: srvModel.storeName })" id="StoreLink"></a>
|
||||
<button v-else-if="isModal" class="btn btn-primary rounded-pill w-100" v-on:click="close" v-t="'Close'"></button>
|
||||
</div>
|
||||
@ -234,6 +230,7 @@
|
||||
<checkout-cheating invoice-id="@Model.InvoiceId" :due="due" :is-settled="isSettled" :is-processing="isProcessing" :payment-method-id="pmId" :crypto-code="srvModel.paymentMethodCurrency"></checkout-cheating>
|
||||
}
|
||||
<footer class="store-footer">
|
||||
<a v-if="srvModel.storeSupportUrl" class="mb-4" :href="srvModel.storeSupportUrl" v-t="'contact_us'" id="ContactLink"></a>
|
||||
<a class="store-powered-by" href="https://btcpayserver.org" target="_blank" rel="noreferrer noopener">
|
||||
{{$t("powered_by")}} <partial name="_StoreFooterLogo" />
|
||||
</a>
|
||||
@ -289,6 +286,7 @@
|
||||
</dl>
|
||||
</script>
|
||||
<script>
|
||||
const checkoutBaseUrl = @Safe.Json(Url.Action("Checkout", new { invoiceId = Model.InvoiceId, paymentMethodId = string.Empty }));
|
||||
const i18nUrl = @Safe.Json($"{Model.RootPath}misc/translations/checkout/{{{{lng}}}}?v={Env.Version}");
|
||||
const statusUrl = @Safe.Json(Url.Action("GetStatus", new { invoiceId = Model.InvoiceId }));
|
||||
const statusWsUrl = @Safe.Json(Url.Action("GetStatusWebSocket", new { invoiceId = Model.InvoiceId }));
|
||||
|
@ -39,11 +39,11 @@
|
||||
<td class="actions-col" permission="@Policies.CanModifyStoreSettings">
|
||||
@if (conf.Value is null)
|
||||
{
|
||||
<a href="@processorsView.Factory.ConfigureLink(storeId, conf.Key, Context.Request)" text-translate="true">Configure</a>
|
||||
<a id="Configure-@conf.Key" href="@processorsView.Factory.ConfigureLink(storeId, conf.Key, Context.Request)" text-translate="true">Configure</a>
|
||||
}
|
||||
else
|
||||
{
|
||||
<a href="@processorsView.Factory.ConfigureLink(storeId, conf.Key, Context.Request)" text-translate="true">Modify</a>
|
||||
<a id="Configure-@conf.Key" href="@processorsView.Factory.ConfigureLink(storeId, conf.Key, Context.Request)" text-translate="true">Modify</a>
|
||||
|
||||
@if (await processorsView.Factory.CanRemove())
|
||||
{
|
||||
|
@ -33,6 +33,9 @@
|
||||
stateActions.Add(("cancel", StringLocalizer["Cancel"]));
|
||||
stateActions.Add(("mark-paid", StringLocalizer["Mark as already paid"]));
|
||||
break;
|
||||
case PayoutState.InProgress:
|
||||
stateActions.Add(("mark-awaiting-payment", StringLocalizer["Mark as awaiting payment"]));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -21,6 +21,8 @@
|
||||
@section PageHeadContent {
|
||||
<style>
|
||||
.tooltip-inner { text-align: left; }
|
||||
.actions-col .btn[data-clipboard] { color: var(--btcpay-body-link); }
|
||||
.actions-col .btn[data-clipboard]:hover { color: var(--btcpay-body-link-accent); }
|
||||
</style>
|
||||
}
|
||||
|
||||
@ -101,7 +103,7 @@
|
||||
</th>
|
||||
<th text-translate="true" scope="col">Name</th>
|
||||
<th text-translate="true" scope="col">Automatically Approved</th>
|
||||
<th text-translate="true" scope="col">Refunded</th>
|
||||
<th text-translate="true" scope="col">Claimed</th>
|
||||
<th scope="col" class="actions-col"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
@ -134,6 +136,12 @@
|
||||
</td>
|
||||
<td class="actions-col">
|
||||
<div class="d-inline-flex align-items-center gap-3">
|
||||
<button type="button" data-clipboard="@Url.ActionAbsolute(Context.Request, "ViewPullPayment", "UIPullPayment", new { pullPaymentId = pp.Id })"
|
||||
permission="@Policies.CanViewPullPayments"
|
||||
class="only-for-js btn btn-link p-0"
|
||||
text-translate="true">
|
||||
Copy Link
|
||||
</button>
|
||||
<a asp-action="ViewPullPayment"
|
||||
permission="@Policies.CanViewPullPayments"
|
||||
asp-controller="UIPullPayment"
|
||||
|
@ -219,7 +219,7 @@
|
||||
<label asp-for="SupportUrl" class="form-label"></label>
|
||||
<input asp-for="SupportUrl" class="form-control" />
|
||||
<span asp-validation-for="SupportUrl" class="text-danger"></span>
|
||||
<div class="form-text" html-translate="true">For support requests related to partially paid invoices. A "Contact Us" button with this link will be shown on the invoice expired page. Can contain the placeholders <code>{OrderId}</code> and <code>{InvoiceId}</code>. Can be any valid URI, such as a website, email, and Nostr.</div>
|
||||
<div class="form-text" html-translate="true">A "Contact Us" button with this link will be shown on the checkout page. Can contain the placeholders <code>{OrderId}</code> and <code>{InvoiceId}</code>. Can be any valid URI, such as a website, email, and Nostr.</div>
|
||||
</div>
|
||||
<div class="d-flex my-3">
|
||||
<input asp-for="LazyPaymentMethods" type="checkbox" class="btcpay-toggle me-3" />
|
||||
|
@ -41,11 +41,15 @@
|
||||
async fetchRate(currencyPair) {
|
||||
const storeId = @Safe.Json(Context.GetRouteValue("storeId"));
|
||||
const pathBase = @Safe.Json(Context.Request.PathBase);
|
||||
const response = await fetch(`${pathBase}/api/rates?storeId=${storeId}¤cyPairs=${currencyPair}`);
|
||||
const json = await response.json();
|
||||
const rate = json[0] && json[0].rate;
|
||||
if (rate) return rate;
|
||||
else console.warn(`Fetching rate for ${currencyPair} failed.`);
|
||||
try {
|
||||
const response = await fetch(`${pathBase}/api/rates?storeId=${storeId}¤cyPairs=${currencyPair}`);
|
||||
const json = await response.json();
|
||||
const rate = json[0] && json[0].rate;
|
||||
if (rate) return rate;
|
||||
else console.warn(`Fetching rate for ${currencyPair} failed.`);
|
||||
} catch (e) {
|
||||
console.error(`Fetching rate for ${currencyPair} failed: ${e}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
@ -232,7 +232,7 @@
|
||||
</button>
|
||||
</div>
|
||||
</th>
|
||||
<th text-translate="true" class="text-start">Label</th>
|
||||
<th text-translate="true" style="min-width:125px">Label</th>
|
||||
<th text-translate="true">Transaction</th>
|
||||
<th text-translate="true" class="amount-col">Amount</th>
|
||||
<th></th>
|
||||
|
@ -259,6 +259,10 @@ function initApp() {
|
||||
if (this.pmId !== id) {
|
||||
this.paymentMethodId = id;
|
||||
this.fetchData();
|
||||
// update url
|
||||
const url = new URL(window.location.href);
|
||||
url.pathname = checkoutBaseUrl + '/' + id;
|
||||
history.pushState({}, "", url);
|
||||
}
|
||||
},
|
||||
changeLanguage (e) {
|
||||
|
@ -126,12 +126,6 @@
|
||||
height="100"
|
||||
id="page314"
|
||||
inkscape:label="acinq" /><inkscape:page
|
||||
x="326.31506"
|
||||
y="1.2204317"
|
||||
width="150"
|
||||
height="100"
|
||||
id="page266"
|
||||
inkscape:label="bailliegifford" /><inkscape:page
|
||||
x="0.85967809"
|
||||
y="110.56508"
|
||||
width="150"
|
||||
|
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 70 KiB |
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 11 KiB |
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 23 KiB |
@ -135,15 +135,7 @@
|
||||
"404": {
|
||||
"description": "POS app with specified ID was not found"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"API_Key": [
|
||||
"btcpay.store.canmodifystoresettings"
|
||||
],
|
||||
"Basic": []
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/apps/crowdfund/{appId}": {
|
||||
@ -174,15 +166,7 @@
|
||||
"404": {
|
||||
"description": "Crowdfund app with specified ID was not found"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"API_Key": [
|
||||
"btcpay.store.canmodifystoresettings"
|
||||
],
|
||||
"Basic": []
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/stores/{storeId}/apps/crowdfund": {
|
||||
|
@ -1,5 +1,5 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<Version>2.0.4</Version>
|
||||
<Version>2.0.5</Version>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
|
21
Changelog.md
21
Changelog.md
@ -1,5 +1,26 @@
|
||||
# Changelog
|
||||
|
||||
## 2.0.5
|
||||
|
||||
### Bug fixes
|
||||
|
||||
* Invoices: Allow admin to see users' invoices (#6517) @dennisreimann
|
||||
* UI: Fix inconsistent responsiveness of labels (#6508, #6501) @dennisreimann
|
||||
* Greenfield: Receipt options from the GetInvoice route were not reflecting the store's settings (#6483) @dennisreimann
|
||||
* Checkout: Fix regression affecting the UI of the SideShift, FixedFloat, and Trocador plugins (#6481) @dennisreimann
|
||||
* Fix several incorrectly generated links when `BTCPAY_ROOTPATH` is used (#6506)
|
||||
|
||||
### Improvements
|
||||
|
||||
* Checkout: Add support link to footer (#6511, #6495) @dennisreimann
|
||||
* Pull Payment: Add "Copy Link" button to the action column (#6516, #6515) @dennisreimann
|
||||
* Greenfield: Remove authorization requirement for PoS data (#6499) @dennisreimann
|
||||
* Greenfield: Resolve store user's image URL @dennisreimann
|
||||
|
||||
### Feature removed
|
||||
|
||||
* Remove the Lightning Balance histogram from the dashboard (too slow on large instances).
|
||||
|
||||
## 2.0.4
|
||||
|
||||
### New features
|
||||
|
@ -215,7 +215,6 @@ The BTCPay Server Project is proudly supported by these entities through the [BT
|
||||
|
||||
[](https://spiral.xyz)
|
||||
[](https://opensats.org)
|
||||
[](https://www.bailliegifford.com)
|
||||
[](https://tether.to)
|
||||
[](https://hrf.org)
|
||||
[](https://lunanode.com)
|
||||
|
Reference in New Issue
Block a user