Compare commits

...

13 Commits

16 changed files with 177 additions and 51 deletions

View File

@ -19,6 +19,12 @@ Once you want to stop
docker-compose down
```
If you want to stop, and remove all existing data
```
docker-compose down -v
```
You can run the tests inside a container by running
```

View File

@ -118,7 +118,7 @@ namespace BTCPayServer.Tests
var acc = tester.NewAccount();
acc.Register();
acc.CreateStore();
var controller = tester.PayTester.GetController<StoresController>(acc.UserId);
var token = (RedirectToActionResult)controller.CreateToken(acc.StoreId, new Models.StoreViewModels.CreateTokenViewModel()
{

View File

@ -58,7 +58,13 @@ namespace BTCPayServer.Authentication
public async Task<string> CreatePairingCodeAsync()
{
string pairingCodeId = Encoders.Base58.EncodeData(RandomUtils.GetBytes(6));
string pairingCodeId = null;
while(true)
{
pairingCodeId = Encoders.Base58.EncodeData(RandomUtils.GetBytes(6));
if(pairingCodeId.Length == 7) // woocommerce plugin check for exactly 7 digits
break;
}
using(var ctx = _Factory.CreateContext())
{
var now = DateTime.UtcNow;

View File

@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.0</TargetFramework>
<Version>1.0.0.13</Version>
<Version>1.0.0.17</Version>
</PropertyGroup>
<ItemGroup>
<Compile Remove="Build\dockerfiles\**" />

View File

@ -58,6 +58,12 @@ namespace BTCPayServer.Controllers
pairingEntity = await _TokenRepository.GetPairingAsync(request.PairingCode);
pairingEntity.SIN = sin;
if(string.IsNullOrEmpty(pairingEntity.Label) && !string.IsNullOrEmpty(request.Label))
{
pairingEntity.Label = request.Label;
await _TokenRepository.UpdatePairingCode(pairingEntity);
}
var result = await _TokenRepository.PairWithSINAsync(request.PairingCode, sin);
if(result != PairingResult.Complete && result != PairingResult.Partial)
throw new BitpayHttpException(400, $"Error while pairing ({result})");

View File

@ -92,6 +92,27 @@ namespace BTCPayServer.Controllers
return View(model);
}
static Dictionary<string, CultureInfo> _CurrencyProviders = new Dictionary<string, CultureInfo>();
private IFormatProvider GetCurrencyProvider(string currency)
{
lock(_CurrencyProviders)
{
if(_CurrencyProviders.Count == 0)
{
foreach(var culture in CultureInfo.GetCultures(CultureTypes.AllCultures).Where(c => !c.IsNeutralCulture))
{
try
{
_CurrencyProviders.TryAdd(new RegionInfo(culture.LCID).ISOCurrencySymbol, culture);
}
catch { }
}
}
return _CurrencyProviders.TryGet(currency);
}
}
[HttpGet]
[Route("i/{invoiceId}")]
[Route("invoice")]
@ -112,6 +133,7 @@ namespace BTCPayServer.Controllers
var model = new PaymentModel()
{
ServerUrl = HttpContext.Request.GetAbsoluteRoot(),
OrderId = invoice.OrderId,
InvoiceId = invoice.Id,
BTCAddress = invoice.DepositAddress.ToString(),
@ -122,7 +144,7 @@ namespace BTCPayServer.Controllers
ExpirationSeconds = Math.Max(0, (int)(invoice.ExpirationTime - DateTimeOffset.UtcNow).TotalSeconds),
MaxTimeSeconds = (int)(invoice.ExpirationTime - invoice.InvoiceTime).TotalSeconds,
ItemDesc = invoice.ProductInformation.ItemDesc,
Rate = invoice.Rate.ToString(),
Rate = invoice.Rate.ToString("C", GetCurrencyProvider(invoice.ProductInformation.Currency)),
RedirectUrl = invoice.RedirectURL,
StoreName = store.StoreName,
TxFees = invoice.TxFee.ToString(),

View File

@ -1,4 +1,5 @@
using BTCPayServer.Authentication;
using BTCPayServer.Data;
using BTCPayServer.Models;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Services.Stores;
@ -8,6 +9,7 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using NBitcoin;
using NBitpayClient;
using NBXplorer.DerivationStrategy;
@ -21,6 +23,7 @@ namespace BTCPayServer.Controllers
[Route("stores")]
[Authorize(AuthenticationSchemes = "Identity.Application")]
[Authorize(Policy = "CanAccessStore")]
[AutoValidateAntiforgeryToken]
public class StoresController : Controller
{
public StoresController(
@ -123,7 +126,6 @@ namespace BTCPayServer.Controllers
}
[HttpPost]
[ValidateAntiForgeryToken]
[Route("{storeId}")]
public async Task<IActionResult> UpdateStore(string storeId, StoreViewModel model, string command)
{
@ -220,7 +222,7 @@ namespace BTCPayServer.Controllers
}
[HttpPost]
[ValidateAntiForgeryToken]
[Route("/api-tokens")]
[Route("{storeId}/Tokens/Create")]
public async Task<IActionResult> CreateToken(string storeId, CreateTokenViewModel model)
{
@ -228,6 +230,17 @@ namespace BTCPayServer.Controllers
{
return View(model);
}
model.Label = model.Label ?? String.Empty;
if(storeId == null) // Permissions are not checked by Policy if the storeId is not passed by url
{
storeId = model.StoreId;
var userId = GetUserId();
if(userId == null)
return Unauthorized();
var store = await _Repo.FindStore(storeId, userId);
if(store == null)
return Unauthorized();
}
var tokenRequest = new TokenRequest()
{
@ -262,16 +275,29 @@ namespace BTCPayServer.Controllers
}
[HttpGet]
[Route("/api-tokens")]
[Route("{storeId}/Tokens/Create")]
public IActionResult CreateToken()
public async Task<IActionResult> CreateToken(string storeId)
{
var userId = GetUserId();
if(string.IsNullOrWhiteSpace(userId))
return Unauthorized();
var model = new CreateTokenViewModel();
model.Facade = "merchant";
ViewBag.HidePublicKey = storeId == null;
ViewBag.ShowStores = storeId == null;
ViewBag.ShowMenu = storeId != null;
model.StoreId = storeId;
if(storeId == null)
{
model.Stores = new SelectList(await _Repo.GetStoresByUserId(userId), nameof(StoreData.Id), nameof(StoreData.StoreName), storeId);
}
return View(model);
}
[HttpPost]
[ValidateAntiForgeryToken]
[Route("{storeId}/Tokens/Delete")]
public async Task<IActionResult> DeleteToken(string storeId, string tokenId)
{
@ -316,7 +342,6 @@ namespace BTCPayServer.Controllers
}
[HttpPost]
[ValidateAntiForgeryToken]
[Route("api-access-request")]
public async Task<IActionResult> Pair(string pairingCode, string selectedStore)
{

View File

@ -69,12 +69,16 @@ namespace BTCPayServer.Hosting
object storeId = null;
if(!((Microsoft.AspNetCore.Mvc.ActionContext)context.Resource).RouteData.Values.TryGetValue("storeId", out storeId))
context.Succeed(requirement);
else
else if(storeId != null)
{
var store = await _StoreRepository.FindStore((string)storeId, _UserManager.GetUserId(((Microsoft.AspNetCore.Mvc.ActionContext)context.Resource).HttpContext.User));
if(store != null)
if(requirement.Role == null || requirement.Role == store.Role)
context.Succeed(requirement);
var user = _UserManager.GetUserId(((Microsoft.AspNetCore.Mvc.ActionContext)context.Resource).HttpContext.User);
if(user != null)
{
var store = await _StoreRepository.FindStore((string)storeId, user);
if(store != null)
if(requirement.Role == null || requirement.Role == store.Role)
context.Succeed(requirement);
}
}
}
}

View File

@ -11,7 +11,10 @@ namespace BTCPayServer.Models.InvoicingModels
{
get; set;
}
public string ServerUrl
{
get; set;
}
public string OrderId
{
get; set;

View File

@ -1,4 +1,5 @@
using BTCPayServer.Validations;
using Microsoft.AspNetCore.Mvc.Rendering;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
@ -14,7 +15,7 @@ namespace BTCPayServer.Models.StoreViewModels
{
get; set;
}
[Required]
public string Label
{
get; set;
@ -25,6 +26,17 @@ namespace BTCPayServer.Models.StoreViewModels
{
get; set;
}
[Required]
public string StoreId
{
get; set;
}
public SelectList Stores
{
get; set;
}
}
public class TokenViewModel
{

View File

@ -27,6 +27,8 @@
crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery.qrcode/1.0/jquery.qrcode.min.js"></script>
<script type="text/javascript">
var serverUrl = "@Model.ServerUrl";
var invoiceId = "@Model.InvoiceId";
var btcAddress = "@Model.BTCAddress";
var btcDue = "@Model.BTCDue"; //must be a string
var customerEmail = "@Model.CustomerEmail"; // Place holder
@ -128,8 +130,6 @@
<div class="single-item-order__right">
<div class="single-item-order__right__btc-price clickable" id="buyerTotalBtcAmount">
<span>@Model.BTCTotalDue</span>
<!---->
<img class="single-item-order__right__btc-price__chevron" src="~/img/chevron.svg">
</div>
<!---->
<div class="single-item-order__right__ex-rate">
@ -178,23 +178,23 @@
<div adjust-height="" class="payment-box">
<div class="bp-view payment scan" id="scan" style="opacity: 1;">
<div class="payment__scan">
<div class="payment__details__instruction__open-wallet hidden-sm-up">
<!---->
<a class="payment__details__instruction__open-wallet__btn action-button action-button--secondary">
<span i18n="">Show QR code</span>
<img class="m-qr-code-icon" src="~/img/qr-code.svg">
</a>
<div class="m-qr-code-container hidden-sm-up hide">
<p class="m-qr-code-header" i18n="">
Hide QR code
<img class="m-qr-code-expand" src="~/img/chevron.svg">
</p>
@*<div class="payment__details__instruction__open-wallet hidden-sm-up">
<!---->
<div class="qr-codes"></div>
</div>
</div>
<a class="payment__details__instruction__open-wallet__btn action-button action-button--secondary">
<span i18n="">Show QR code</span>
<img class="m-qr-code-icon" src="~/img/qr-code.svg">
</a>
<div class="m-qr-code-container hidden-sm-up hide">
<p class="m-qr-code-header" i18n="">
Hide QR code
<img class="m-qr-code-expand" src="~/img/chevron.svg">
</p>
<!---->
<div class="qr-codes"></div>
</div>
</div>*@
<!---->
<div class="qr-codes hidden-xs-down"></div>
<div class="qr-codes"></div>
</div>
<div class="payment__details__instruction__open-wallet">
<a class="payment__details__instruction__open-wallet__btn action-button" href="@Model.InvoiceBitcoinUrl">
@ -379,7 +379,7 @@
<div class="manual-box__amount__label label" i18n="">Amount</div>
<!---->
<div class="manual-box__amount__value copy-cursor" ngxclipboard="">
<span>@Model.BTCDue BTC</span>
<span>@Model.BTCDue</span> BTC
<div class="copied-label">
<span i18n="">Copied</span>
</div>
@ -426,6 +426,11 @@
<!---->
<div class="success-message" i18n="">This invoice has been paid.</div>
<!---->
<button class="action-button" style="margin-top: 0px;">
<bp-done-text>
<span i18n="" class="i18n-return-to-merchant">Return to @Model.StoreName</span>
</bp-done-text>
</button>
</div>
</div>
<!---->

View File

@ -1,5 +1,6 @@
@{
Layout = "/Views/Shared/_Layout.cshtml";
ViewBag.ShowMenu = ViewBag.ShowMenu ?? true;
}
@ -15,7 +16,10 @@
<div>
<div class="row">
<div class="col-md-3">
@await Html.PartialAsync("_Nav")
@if(ViewBag.ShowMenu)
{
@await Html.PartialAsync("_Nav")
}
</div>
<div class="col-md-9">
@RenderBody()

View File

@ -3,24 +3,33 @@
Layout = "../Shared/_NavLayout.cshtml";
ViewData["Title"] = "Create a new token";
ViewData.AddActivePage(StoreNavPages.Tokens);
ViewBag.HidePublicKey = ViewBag.HidePublicKey ?? false;
ViewBag.ShowStores = ViewBag.ShowStores ?? false;
}
<h4>@ViewData["Title"]</h4>
<div class="row">
<div class="col-md-6">
<form asp-action="CreateToken" method="post">
<form method="post">
<div class="form-group">
<label asp-for="Label"></label>
@if(ViewBag.HidePublicKey)
{
<small class="text-muted">optional</small>
}
<input asp-for="Label" class="form-control" />
<span asp-validation-for="Label" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="PublicKey"></label>
<small class="text-muted">Keep empty for server-initiated pairing</small>
<input asp-for="PublicKey" class="form-control" />
<span asp-validation-for="PublicKey" class="text-danger"></span>
</div>
@if(!ViewBag.HidePublicKey)
{
<div class="form-group">
<label asp-for="PublicKey"></label>
<small class="text-muted">Keep empty for server-initiated pairing</small>
<input asp-for="PublicKey" class="form-control" />
<span asp-validation-for="PublicKey" class="text-danger"></span>
</div>
}
<div class="form-group">
<label asp-for="Facade"></label>
<select asp-for="Facade" class="form-control">
@ -29,6 +38,19 @@
</select>
<span asp-validation-for="Facade" class="text-danger"></span>
</div>
@if(ViewBag.ShowStores)
{
<div class="form-group">
<label asp-for="StoreId" class="control-label"></label>
<select asp-for="StoreId" asp-items="Model.Stores" class="form-control"></select>
<span asp-validation-for="StoreId" class="text-danger"></span>
</div>
}
else
{
<input type="hidden" asp-for="StoreId" />
}
<div class="form-group">
<input type="submit" value="Request pairing" class="btn btn-default" />
</div>

View File

@ -8461,7 +8461,6 @@ strong {
.single-item-order__right__ex-rate {
font-style: italic;
font-size: 11px;
margin-right: 16px;
}
.single-item-order__right__btc-price {

View File

@ -69,7 +69,7 @@ function emailForm() {
// Push the email to a server, once the reception is confirmed move on
customerEmail = emailAddress;
var path = window.location.pathname + "/UpdateCustomer";
var path = serverUrl + "/i/" + invoiceId + "/UpdateCustomer";
$.ajax({
url: path,
@ -155,9 +155,15 @@ $("#copy-tab").click(function () {
// Should connect using webhook ?
// If notification received
var oldStatus = status;
updateState(status);
function updateState(status) {
if (oldStatus != status)
{
oldStatus = status;
window.parent.postMessage({ "invoiceId": invoiceId, "status": status }, "*");
}
if (status == "complete" ||
status == "paidOver" ||
status == "confirmed" ||
@ -165,6 +171,17 @@ function updateState(status) {
if ($(".modal-dialog").hasClass("expired")) {
$(".modal-dialog").removeClass("expired");
}
if (merchantRefLink != "") {
$(".action-button").click(function () {
window.location.href = merchantRefLink;
});
}
else
{
$(".action-button").hide();
}
$(".modal-dialog").addClass("paid");
if ($("#scan").hasClass("active")) {
@ -186,7 +203,7 @@ function updateState(status) {
}
var watcher = setInterval(function () {
var path = window.location.pathname + "/status";
var path = serverUrl + "/i/" + invoiceId + "/status";
$.ajax({
url: path,
type: "GET"
@ -207,11 +224,6 @@ $(".menu__item").click(function () {
// function to load contents in different language should go there
});
// Redirect
$("#expired .action-button").click(function () {
window.location.href = merchantRefLink;
});
// Validate Email address
function validateEmail(email) {
var re = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;

View File

@ -1,7 +1,7 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.26730.3
VisualStudioVersion = 15.0.26730.16
MinimumVisualStudioVersion = 15.0.26124.0
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BTCPayServer", "BTCPayServer\BTCPayServer.csproj", "{949A0870-8D8C-4DE5-8845-DDD560489177}"
EndProject