mirror of
https://github.com/aljazceru/btcpayserver.git
synced 2025-12-18 22:44:29 +01:00
Can invite user to manage your store
This commit is contained in:
@@ -56,10 +56,12 @@ namespace BTCPayServer.Tests
|
|||||||
|
|
||||||
public async Task<StoresController> CreateStoreAsync()
|
public async Task<StoresController> CreateStoreAsync()
|
||||||
{
|
{
|
||||||
var store = parent.PayTester.GetController<StoresController>(UserId);
|
var store = parent.PayTester.GetController<UserStoresController>(UserId);
|
||||||
await store.CreateStore(new CreateStoreViewModel() { Name = "Test Store" });
|
await store.CreateStore(new CreateStoreViewModel() { Name = "Test Store" });
|
||||||
StoreId = store.CreatedStoreId;
|
StoreId = store.CreatedStoreId;
|
||||||
return store;
|
var store2 = parent.PayTester.GetController<StoresController>(UserId);
|
||||||
|
store2.CreatedStoreId = store.CreatedStoreId;
|
||||||
|
return store2;
|
||||||
}
|
}
|
||||||
|
|
||||||
public BTCPayNetwork SupportedNetwork { get; set; }
|
public BTCPayNetwork SupportedNetwork { get; set; }
|
||||||
|
|||||||
@@ -417,7 +417,7 @@ namespace BTCPayServer.Controllers
|
|||||||
if (stores.Count() == 0)
|
if (stores.Count() == 0)
|
||||||
{
|
{
|
||||||
StatusMessage = "Error: You need to create at least one store before creating a transaction";
|
StatusMessage = "Error: You need to create at least one store before creating a transaction";
|
||||||
return RedirectToAction(nameof(StoresController.ListStores), "Stores");
|
return RedirectToAction(nameof(UserStoresController.ListStores), "UserStores");
|
||||||
}
|
}
|
||||||
return View(new CreateInvoiceModel() { Stores = stores });
|
return View(new CreateInvoiceModel() { Stores = stores });
|
||||||
}
|
}
|
||||||
@@ -434,9 +434,18 @@ namespace BTCPayServer.Controllers
|
|||||||
return View(model);
|
return View(model);
|
||||||
}
|
}
|
||||||
var store = await _StoreRepository.FindStore(model.StoreId, GetUserId());
|
var store = await _StoreRepository.FindStore(model.StoreId, GetUserId());
|
||||||
|
StatusMessage = null;
|
||||||
|
if (store.Role != StoreRoles.Owner)
|
||||||
|
{
|
||||||
|
StatusMessage = "Error: You need to be owner of this store to create an invoice";
|
||||||
|
}
|
||||||
if (store.GetSupportedPaymentMethods(_NetworkProvider).Count() == 0)
|
if (store.GetSupportedPaymentMethods(_NetworkProvider).Count() == 0)
|
||||||
{
|
{
|
||||||
StatusMessage = "Error: You need to configure the derivation scheme in order to create an invoice";
|
StatusMessage = "Error: You need to configure the derivation scheme in order to create an invoice";
|
||||||
|
}
|
||||||
|
|
||||||
|
if(StatusMessage != null)
|
||||||
|
{
|
||||||
return RedirectToAction(nameof(StoresController.UpdateStore), "Stores", new
|
return RedirectToAction(nameof(StoresController.UpdateStore), "Stores", new
|
||||||
{
|
{
|
||||||
storeId = store.Id
|
storeId = store.Id
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ namespace BTCPayServer.Controllers
|
|||||||
{
|
{
|
||||||
public partial class StoresController
|
public partial class StoresController
|
||||||
{
|
{
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
[Route("{storeId}/lightning/{cryptoCode}")]
|
[Route("{storeId}/lightning/{cryptoCode}")]
|
||||||
public async Task<IActionResult> AddLightningNode(string storeId, string cryptoCode)
|
public async Task<IActionResult> AddLightningNode(string storeId, string cryptoCode)
|
||||||
|
|||||||
@@ -27,10 +27,11 @@ namespace BTCPayServer.Controllers
|
|||||||
{
|
{
|
||||||
[Route("stores")]
|
[Route("stores")]
|
||||||
[Authorize(AuthenticationSchemes = "Identity.Application")]
|
[Authorize(AuthenticationSchemes = "Identity.Application")]
|
||||||
[Authorize(Policy = "CanAccessStore")]
|
[Authorize(Policy = StorePolicies.OwnStore)]
|
||||||
[AutoValidateAntiforgeryToken]
|
[AutoValidateAntiforgeryToken]
|
||||||
public partial class StoresController : Controller
|
public partial class StoresController : Controller
|
||||||
{
|
{
|
||||||
|
public string CreatedStoreId { get; set; }
|
||||||
public StoresController(
|
public StoresController(
|
||||||
NBXplorerDashboard dashboard,
|
NBXplorerDashboard dashboard,
|
||||||
IServiceProvider serviceProvider,
|
IServiceProvider serviceProvider,
|
||||||
@@ -83,32 +84,6 @@ namespace BTCPayServer.Controllers
|
|||||||
get; set;
|
get; set;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet]
|
|
||||||
[Route("create")]
|
|
||||||
public IActionResult CreateStore()
|
|
||||||
{
|
|
||||||
return View();
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost]
|
|
||||||
[Route("create")]
|
|
||||||
public async Task<IActionResult> CreateStore(CreateStoreViewModel vm)
|
|
||||||
{
|
|
||||||
if (!ModelState.IsValid)
|
|
||||||
{
|
|
||||||
return View(vm);
|
|
||||||
}
|
|
||||||
var store = await _Repo.CreateStore(GetUserId(), vm.Name);
|
|
||||||
CreatedStoreId = store.Id;
|
|
||||||
StatusMessage = "Store successfully created";
|
|
||||||
return RedirectToAction(nameof(ListStores));
|
|
||||||
}
|
|
||||||
|
|
||||||
public string CreatedStoreId
|
|
||||||
{
|
|
||||||
get; set;
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
[Route("{storeId}/wallet/{cryptoCode}")]
|
[Route("{storeId}/wallet/{cryptoCode}")]
|
||||||
public async Task<IActionResult> Wallet(string storeId, string cryptoCode)
|
public async Task<IActionResult> Wallet(string storeId, string cryptoCode)
|
||||||
@@ -128,76 +103,81 @@ namespace BTCPayServer.Controllers
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
public async Task<IActionResult> ListStores()
|
[Route("{storeId}/users")]
|
||||||
|
public async Task<IActionResult> StoreUsers(string storeId)
|
||||||
{
|
{
|
||||||
StoresViewModel result = new StoresViewModel();
|
StoreUsersViewModel vm = new StoreUsersViewModel();
|
||||||
result.StatusMessage = StatusMessage;
|
await FillUsers(storeId, vm);
|
||||||
var stores = await _Repo.GetStoresByUserId(GetUserId());
|
return View(vm);
|
||||||
var balances = stores
|
|
||||||
.Select(s => s.GetSupportedPaymentMethods(_NetworkProvider)
|
|
||||||
.OfType<DerivationStrategy>()
|
|
||||||
.Select(d => ((Wallet: _WalletProvider.GetWallet(d.Network),
|
|
||||||
DerivationStrategy: d.DerivationStrategyBase)))
|
|
||||||
.Where(_ => _.Wallet != null)
|
|
||||||
.Select(async _ => (await GetBalanceString(_)) + " " + _.Wallet.Network.CryptoCode))
|
|
||||||
.ToArray();
|
|
||||||
|
|
||||||
await Task.WhenAll(balances.SelectMany(_ => _));
|
|
||||||
for (int i = 0; i < stores.Length; i++)
|
|
||||||
{
|
|
||||||
var store = stores[i];
|
|
||||||
result.Stores.Add(new StoresViewModel.StoreViewModel()
|
|
||||||
{
|
|
||||||
Id = store.Id,
|
|
||||||
Name = store.StoreName,
|
|
||||||
WebSite = store.StoreWebsite,
|
|
||||||
Balances = balances[i].Select(t => t.Result).ToArray()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return View(result);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task<string> GetBalanceString((BTCPayWallet Wallet, DerivationStrategyBase DerivationStrategy) _)
|
private async Task FillUsers(string storeId, StoreUsersViewModel vm)
|
||||||
{
|
{
|
||||||
using (CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)))
|
var users = await _Repo.GetStoreUsers(storeId);
|
||||||
|
vm.StoreId = storeId;
|
||||||
|
vm.Users = users.Select(u => new StoreUsersViewModel.StoreUserViewModel()
|
||||||
{
|
{
|
||||||
try
|
Email = u.Email,
|
||||||
{
|
Id = u.Id,
|
||||||
return (await _.Wallet.GetBalance(_.DerivationStrategy, cts.Token)).ToString();
|
Role = u.Role
|
||||||
|
}).ToList();
|
||||||
}
|
}
|
||||||
catch
|
|
||||||
|
[HttpPost]
|
||||||
|
[Route("{storeId}/users")]
|
||||||
|
public async Task<IActionResult> StoreUsers(string storeId, StoreUsersViewModel vm)
|
||||||
{
|
{
|
||||||
return "--";
|
await FillUsers(storeId, vm);
|
||||||
|
if(!ModelState.IsValid)
|
||||||
|
{
|
||||||
|
return View(vm);
|
||||||
}
|
}
|
||||||
|
var user = await _UserManager.FindByEmailAsync(vm.Email);
|
||||||
|
if(user == null)
|
||||||
|
{
|
||||||
|
ModelState.AddModelError(nameof(vm.Email), "User not found");
|
||||||
|
return View(vm);
|
||||||
}
|
}
|
||||||
|
if(!StoreRoles.AllRoles.Contains(vm.Role))
|
||||||
|
{
|
||||||
|
ModelState.AddModelError(nameof(vm.Role), "Invalid role");
|
||||||
|
return View(vm);
|
||||||
|
}
|
||||||
|
if(!await _Repo.AddStoreUser(storeId, user.Id, vm.Role))
|
||||||
|
{
|
||||||
|
ModelState.AddModelError(nameof(vm.Email), "The user already has access to this store");
|
||||||
|
return View(vm);
|
||||||
|
}
|
||||||
|
StatusMessage = "User added successfully";
|
||||||
|
return RedirectToAction(nameof(StoreUsers));
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
[Route("{storeId}/delete")]
|
[Route("{storeId}/users/{userId}/delete")]
|
||||||
public async Task<IActionResult> DeleteStore(string storeId)
|
public async Task<IActionResult> DeleteStoreUser(string storeId, string userId)
|
||||||
{
|
{
|
||||||
var store = await _Repo.FindStore(storeId, GetUserId());
|
StoreUsersViewModel vm = new StoreUsersViewModel();
|
||||||
|
var store = await _Repo.FindStore(storeId, userId);
|
||||||
if (store == null)
|
if (store == null)
|
||||||
return NotFound();
|
return NotFound();
|
||||||
|
var user = await _UserManager.FindByIdAsync(userId);
|
||||||
|
if (user == null)
|
||||||
|
return NotFound();
|
||||||
return View("Confirm", new ConfirmModel()
|
return View("Confirm", new ConfirmModel()
|
||||||
{
|
{
|
||||||
Title = "Delete store " + store.StoreName,
|
Title = $"Remove store user",
|
||||||
Description = "This store will still be accessible to users sharing it",
|
Description = $"Are you sure to remove access to remove {store.Role} access to {user.Email}?",
|
||||||
Action = "Delete"
|
Action = "Delete"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
[Route("{storeId}/delete")]
|
[Route("{storeId}/users/{userId}/delete")]
|
||||||
public async Task<IActionResult> DeleteStorePost(string storeId)
|
public async Task<IActionResult> DeleteStoreUserPost(string storeId, string userId)
|
||||||
{
|
{
|
||||||
var userId = GetUserId();
|
await _Repo.RemoveStoreUser(storeId, userId);
|
||||||
var store = await _Repo.FindStore(storeId, GetUserId());
|
StatusMessage = "User removed successfully";
|
||||||
if (store == null)
|
return RedirectToAction(nameof(StoreUsers), new { storeId = storeId, userId = userId });
|
||||||
return NotFound();
|
|
||||||
await _Repo.RemoveStore(storeId, userId);
|
|
||||||
StatusMessage = "Store removed successfully";
|
|
||||||
return RedirectToAction(nameof(ListStores));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
@@ -403,15 +383,17 @@ namespace BTCPayServer.Controllers
|
|||||||
return View(model);
|
return View(model);
|
||||||
}
|
}
|
||||||
model.Label = model.Label ?? String.Empty;
|
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 ?? storeId;
|
||||||
{
|
|
||||||
storeId = model.StoreId;
|
|
||||||
var userId = GetUserId();
|
var userId = GetUserId();
|
||||||
if (userId == null)
|
if (userId == null)
|
||||||
return Unauthorized();
|
return Unauthorized();
|
||||||
var store = await _Repo.FindStore(storeId, userId);
|
var store = await _Repo.FindStore(storeId, userId);
|
||||||
if (store == null)
|
if (store == null)
|
||||||
return Unauthorized();
|
return Unauthorized();
|
||||||
|
if (store.Role != StoreRoles.Owner)
|
||||||
|
{
|
||||||
|
StatusMessage = "Error: You need to be owner of this store to request pairing codes";
|
||||||
|
return RedirectToAction(nameof(UserStoresController.ListStores), "UserStores");
|
||||||
}
|
}
|
||||||
|
|
||||||
var tokenRequest = new TokenRequest()
|
var tokenRequest = new TokenRequest()
|
||||||
@@ -491,11 +473,13 @@ namespace BTCPayServer.Controllers
|
|||||||
[Route("/api-access-request")]
|
[Route("/api-access-request")]
|
||||||
public async Task<IActionResult> RequestPairing(string pairingCode, string selectedStore = null)
|
public async Task<IActionResult> RequestPairing(string pairingCode, string selectedStore = null)
|
||||||
{
|
{
|
||||||
|
if (pairingCode == null)
|
||||||
|
return NotFound();
|
||||||
var pairing = await _TokenRepository.GetPairingAsync(pairingCode);
|
var pairing = await _TokenRepository.GetPairingAsync(pairingCode);
|
||||||
if (pairing == null)
|
if (pairing == null)
|
||||||
{
|
{
|
||||||
StatusMessage = "Unknown pairing code";
|
StatusMessage = "Unknown pairing code";
|
||||||
return RedirectToAction(nameof(ListStores));
|
return RedirectToAction(nameof(UserStoresController.ListStores), "UserStores");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -517,7 +501,7 @@ namespace BTCPayServer.Controllers
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
[Route("api-access-request")]
|
[Route("/api-access-request")]
|
||||||
public async Task<IActionResult> Pair(string pairingCode, string selectedStore)
|
public async Task<IActionResult> Pair(string pairingCode, string selectedStore)
|
||||||
{
|
{
|
||||||
if (pairingCode == null)
|
if (pairingCode == null)
|
||||||
@@ -527,6 +511,12 @@ namespace BTCPayServer.Controllers
|
|||||||
if (store == null || pairing == null)
|
if (store == null || pairing == null)
|
||||||
return NotFound();
|
return NotFound();
|
||||||
|
|
||||||
|
if(store.Role != StoreRoles.Owner)
|
||||||
|
{
|
||||||
|
StatusMessage = "Error: You can't approve a pairing without being owner of the store";
|
||||||
|
return RedirectToAction(nameof(UserStoresController.ListStores), "UserStores");
|
||||||
|
}
|
||||||
|
|
||||||
var pairingResult = await _TokenRepository.PairWithStoreAsync(pairingCode, store.Id);
|
var pairingResult = await _TokenRepository.PairWithStoreAsync(pairingCode, store.Id);
|
||||||
if (pairingResult == PairingResult.Complete || pairingResult == PairingResult.Partial)
|
if (pairingResult == PairingResult.Complete || pairingResult == PairingResult.Partial)
|
||||||
{
|
{
|
||||||
|
|||||||
147
BTCPayServer/Controllers/UserStoresController.cs
Normal file
147
BTCPayServer/Controllers/UserStoresController.cs
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using BTCPayServer.Models;
|
||||||
|
using BTCPayServer.Models.StoreViewModels;
|
||||||
|
using BTCPayServer.Services.Stores;
|
||||||
|
using BTCPayServer.Services.Wallets;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using NBXplorer.DerivationStrategy;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Controllers
|
||||||
|
{
|
||||||
|
[Route("stores")]
|
||||||
|
[Authorize(AuthenticationSchemes = "Identity.Application")]
|
||||||
|
[AutoValidateAntiforgeryToken]
|
||||||
|
public partial class UserStoresController : Controller
|
||||||
|
{
|
||||||
|
private StoreRepository _Repo;
|
||||||
|
private BTCPayNetworkProvider _NetworkProvider;
|
||||||
|
private UserManager<ApplicationUser> _UserManager;
|
||||||
|
private BTCPayWalletProvider _WalletProvider;
|
||||||
|
|
||||||
|
public UserStoresController(
|
||||||
|
UserManager<ApplicationUser> userManager,
|
||||||
|
BTCPayNetworkProvider networkProvider,
|
||||||
|
BTCPayWalletProvider walletProvider,
|
||||||
|
StoreRepository storeRepository)
|
||||||
|
{
|
||||||
|
_Repo = storeRepository;
|
||||||
|
_NetworkProvider = networkProvider;
|
||||||
|
_UserManager = userManager;
|
||||||
|
_WalletProvider = walletProvider;
|
||||||
|
}
|
||||||
|
[HttpGet]
|
||||||
|
[Route("{storeId}/delete")]
|
||||||
|
public async Task<IActionResult> DeleteStore(string storeId)
|
||||||
|
{
|
||||||
|
var store = await _Repo.FindStore(storeId, GetUserId());
|
||||||
|
if (store == null)
|
||||||
|
return NotFound();
|
||||||
|
return View("Confirm", new ConfirmModel()
|
||||||
|
{
|
||||||
|
Title = "Delete store " + store.StoreName,
|
||||||
|
Description = "This store will still be accessible to users sharing it",
|
||||||
|
Action = "Delete"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
[Route("create")]
|
||||||
|
public IActionResult CreateStore()
|
||||||
|
{
|
||||||
|
return View();
|
||||||
|
}
|
||||||
|
|
||||||
|
public string CreatedStoreId
|
||||||
|
{
|
||||||
|
get; set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
[Route("{storeId}/delete")]
|
||||||
|
public async Task<IActionResult> DeleteStorePost(string storeId)
|
||||||
|
{
|
||||||
|
var userId = GetUserId();
|
||||||
|
var store = await _Repo.FindStore(storeId, GetUserId());
|
||||||
|
if (store == null)
|
||||||
|
return NotFound();
|
||||||
|
await _Repo.RemoveStore(storeId, userId);
|
||||||
|
StatusMessage = "Store removed successfully";
|
||||||
|
return RedirectToAction(nameof(ListStores));
|
||||||
|
}
|
||||||
|
|
||||||
|
[TempData]
|
||||||
|
public string StatusMessage { get; set; }
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<IActionResult> ListStores()
|
||||||
|
{
|
||||||
|
StoresViewModel result = new StoresViewModel();
|
||||||
|
var stores = await _Repo.GetStoresByUserId(GetUserId());
|
||||||
|
|
||||||
|
var balances = stores
|
||||||
|
.Select(s => s.GetSupportedPaymentMethods(_NetworkProvider)
|
||||||
|
.OfType<DerivationStrategy>()
|
||||||
|
.Select(d => ((Wallet: _WalletProvider.GetWallet(d.Network),
|
||||||
|
DerivationStrategy: d.DerivationStrategyBase)))
|
||||||
|
.Where(_ => _.Wallet != null)
|
||||||
|
.Select(async _ => (await GetBalanceString(_)) + " " + _.Wallet.Network.CryptoCode))
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
await Task.WhenAll(balances.SelectMany(_ => _));
|
||||||
|
for (int i = 0; i < stores.Length; i++)
|
||||||
|
{
|
||||||
|
var store = stores[i];
|
||||||
|
result.Stores.Add(new StoresViewModel.StoreViewModel()
|
||||||
|
{
|
||||||
|
Id = store.Id,
|
||||||
|
Name = store.StoreName,
|
||||||
|
WebSite = store.StoreWebsite,
|
||||||
|
IsOwner = store.Role == StoreRoles.Owner,
|
||||||
|
Balances = store.Role == StoreRoles.Owner ? balances[i].Select(t => t.Result).ToArray() : Array.Empty<string>()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return View(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
[Route("create")]
|
||||||
|
public async Task<IActionResult> CreateStore(CreateStoreViewModel vm)
|
||||||
|
{
|
||||||
|
if (!ModelState.IsValid)
|
||||||
|
{
|
||||||
|
return View(vm);
|
||||||
|
}
|
||||||
|
var store = await _Repo.CreateStore(GetUserId(), vm.Name);
|
||||||
|
CreatedStoreId = store.Id;
|
||||||
|
StatusMessage = "Store successfully created";
|
||||||
|
return RedirectToAction(nameof(ListStores));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<string> GetBalanceString((BTCPayWallet Wallet, DerivationStrategyBase DerivationStrategy) _)
|
||||||
|
{
|
||||||
|
using (CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return (await _.Wallet.GetBalance(_.DerivationStrategy, cts.Token)).ToString();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return "--";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private string GetUserId()
|
||||||
|
{
|
||||||
|
return _UserManager.GetUserId(User);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -173,14 +173,14 @@ namespace BTCPayServer.Hosting
|
|||||||
|
|
||||||
services.AddAuthorization(o =>
|
services.AddAuthorization(o =>
|
||||||
{
|
{
|
||||||
o.AddPolicy("CanAccessStore", builder =>
|
o.AddPolicy(StorePolicies.CanAccessStores, builder =>
|
||||||
{
|
{
|
||||||
builder.AddRequirements(new OwnStoreAuthorizationRequirement());
|
builder.AddRequirements(new OwnStoreAuthorizationRequirement());
|
||||||
});
|
});
|
||||||
|
|
||||||
o.AddPolicy("OwnStore", builder =>
|
o.AddPolicy(StorePolicies.OwnStore, builder =>
|
||||||
{
|
{
|
||||||
builder.AddRequirements(new OwnStoreAuthorizationRequirement("Owner"));
|
builder.AddRequirements(new OwnStoreAuthorizationRequirement(StoreRoles.Owner));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
28
BTCPayServer/Models/StoreViewModels/StoreUsersViewModel.cs
Normal file
28
BTCPayServer/Models/StoreViewModels/StoreUsersViewModel.cs
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Models.StoreViewModels
|
||||||
|
{
|
||||||
|
public class StoreUsersViewModel
|
||||||
|
{
|
||||||
|
public class StoreUserViewModel
|
||||||
|
{
|
||||||
|
public string Email { get; set; }
|
||||||
|
public string Role { get; set; }
|
||||||
|
public string Id { get; set; }
|
||||||
|
}
|
||||||
|
public StoreUsersViewModel()
|
||||||
|
{
|
||||||
|
Role = StoreRoles.Guest;
|
||||||
|
}
|
||||||
|
[Required]
|
||||||
|
[EmailAddress]
|
||||||
|
public string Email { get; set; }
|
||||||
|
public string StoreId { get; set; }
|
||||||
|
public string Role { get; set; }
|
||||||
|
public List<StoreUserViewModel> Users { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,14 +8,11 @@ namespace BTCPayServer.Models.StoreViewModels
|
|||||||
{
|
{
|
||||||
public class StoresViewModel
|
public class StoresViewModel
|
||||||
{
|
{
|
||||||
public string StatusMessage
|
|
||||||
{
|
|
||||||
get; set;
|
|
||||||
}
|
|
||||||
public List<StoreViewModel> Stores
|
public List<StoreViewModel> Stores
|
||||||
{
|
{
|
||||||
get; set;
|
get; set;
|
||||||
} = new List<StoreViewModel>();
|
} = new List<StoreViewModel>();
|
||||||
|
|
||||||
public class StoreViewModel
|
public class StoreViewModel
|
||||||
{
|
{
|
||||||
public string Name
|
public string Name
|
||||||
@@ -32,6 +29,11 @@ namespace BTCPayServer.Models.StoreViewModels
|
|||||||
{
|
{
|
||||||
get; set;
|
get; set;
|
||||||
}
|
}
|
||||||
|
public bool IsOwner
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
public string[] Balances
|
public string[] Balances
|
||||||
{
|
{
|
||||||
get; set;
|
get; set;
|
||||||
|
|||||||
@@ -66,6 +66,10 @@ namespace BTCPayServer.Payments.Lightning
|
|||||||
{
|
{
|
||||||
info = await client.GetInfo(cts.Token);
|
info = await client.GetInfo(cts.Token);
|
||||||
}
|
}
|
||||||
|
catch (OperationCanceledException) when (cts.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
throw new Exception($"The lightning node did not replied in a timely maner");
|
||||||
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
throw new Exception($"Error while connecting to the API ({ex.Message})");
|
throw new Exception($"Error while connecting to the API ({ex.Message})");
|
||||||
|
|||||||
@@ -48,14 +48,72 @@ namespace BTCPayServer.Services.Stores
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class StoreUser
|
||||||
|
{
|
||||||
|
public string Id { get; set; }
|
||||||
|
public string Email { get; set; }
|
||||||
|
public string Role { get; set; }
|
||||||
|
}
|
||||||
|
public async Task<StoreUser[]> GetStoreUsers(string storeId)
|
||||||
|
{
|
||||||
|
if (storeId == null)
|
||||||
|
throw new ArgumentNullException(nameof(storeId));
|
||||||
|
using (var ctx = _ContextFactory.CreateContext())
|
||||||
|
{
|
||||||
|
return await ctx
|
||||||
|
.UserStore
|
||||||
|
.Where(u => u.StoreDataId == storeId)
|
||||||
|
.Select(u => new StoreUser()
|
||||||
|
{
|
||||||
|
Id = u.ApplicationUserId,
|
||||||
|
Email = u.ApplicationUser.Email,
|
||||||
|
Role = u.Role
|
||||||
|
}).ToArrayAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<StoreData[]> GetStoresByUserId(string userId)
|
public async Task<StoreData[]> GetStoresByUserId(string userId)
|
||||||
{
|
{
|
||||||
using (var ctx = _ContextFactory.CreateContext())
|
using (var ctx = _ContextFactory.CreateContext())
|
||||||
{
|
{
|
||||||
return await ctx.UserStore
|
return (await ctx.UserStore
|
||||||
.Where(u => u.ApplicationUserId == userId)
|
.Where(u => u.ApplicationUserId == userId)
|
||||||
.Select(u => u.StoreData)
|
.Select(u => new { u.StoreData, u.Role })
|
||||||
.ToArrayAsync();
|
.ToArrayAsync())
|
||||||
|
.Select(u =>
|
||||||
|
{
|
||||||
|
u.StoreData.Role = u.Role;
|
||||||
|
return u.StoreData;
|
||||||
|
}).ToArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> AddStoreUser(string storeId, string userId, string role)
|
||||||
|
{
|
||||||
|
using (var ctx = _ContextFactory.CreateContext())
|
||||||
|
{
|
||||||
|
var userStore = new UserStore() { StoreDataId = storeId, ApplicationUserId = userId, Role = role };
|
||||||
|
ctx.UserStore.Add(userStore);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await ctx.SaveChangesAsync();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (DbUpdateException)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task RemoveStoreUser(string storeId, string userId)
|
||||||
|
{
|
||||||
|
using (var ctx = _ContextFactory.CreateContext())
|
||||||
|
{
|
||||||
|
var userStore = new UserStore() { StoreDataId = storeId, ApplicationUserId = userId };
|
||||||
|
ctx.UserStore.Add(userStore);
|
||||||
|
ctx.Entry<UserStore>(userStore).State = EntityState.Deleted;
|
||||||
|
await ctx.SaveChangesAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
26
BTCPayServer/StorePolicies.cs
Normal file
26
BTCPayServer/StorePolicies.cs
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace BTCPayServer
|
||||||
|
{
|
||||||
|
public class StorePolicies
|
||||||
|
{
|
||||||
|
public const string CanAccessStores = "CanAccessStore";
|
||||||
|
public const string OwnStore = "OwnStore";
|
||||||
|
}
|
||||||
|
public class StoreRoles
|
||||||
|
{
|
||||||
|
public const string Owner = "Owner";
|
||||||
|
public const string Guest = "Guest";
|
||||||
|
public static IEnumerable<String> AllRoles
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
yield return Owner;
|
||||||
|
yield return Guest;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -54,7 +54,7 @@
|
|||||||
{
|
{
|
||||||
<li class="nav-item"><a asp-area="" asp-controller="Server" asp-action="ListUsers" class="nav-link js-scroll-trigger">Server settings</a></li>
|
<li class="nav-item"><a asp-area="" asp-controller="Server" asp-action="ListUsers" class="nav-link js-scroll-trigger">Server settings</a></li>
|
||||||
}
|
}
|
||||||
<li class="nav-item"><a asp-area="" asp-controller="Stores" asp-action="ListStores" class="nav-link js-scroll-trigger">Stores</a></li>
|
<li class="nav-item"><a asp-area="" asp-controller="UserStores" asp-action="ListStores" class="nav-link js-scroll-trigger">Stores</a></li>
|
||||||
<li class="nav-item"><a asp-area="" asp-controller="Invoice" asp-action="ListInvoices" class="nav-link js-scroll-trigger">Invoices</a></li>
|
<li class="nav-item"><a asp-area="" asp-controller="Invoice" asp-action="ListInvoices" class="nav-link js-scroll-trigger">Invoices</a></li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a asp-area="" asp-controller="Manage" asp-action="Index" title="Manage" class="nav-link js-scroll-trigger">My settings</a>
|
<a asp-area="" asp-controller="Manage" asp-action="Index" title="Manage" class="nav-link js-scroll-trigger">My settings</a>
|
||||||
|
|||||||
@@ -13,22 +13,28 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
<div class="col-md-4"></div>
|
||||||
|
<div class="col-md-4">
|
||||||
<table class="table">
|
<table class="table">
|
||||||
<tr>
|
<tr>
|
||||||
<th>Label</th>
|
<th>Label</th>
|
||||||
<td>@Model.Label</td>
|
<td style="text-align:right;">@Model.Label</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Facade</th>
|
<th>Facade</th>
|
||||||
<td>@Model.Facade</td>
|
<td style="text-align:right;">@Model.Facade</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>SIN</th>
|
<th>SIN</th>
|
||||||
<td>@Model.SIN</td>
|
<td style="text-align:right;">@Model.SIN</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-md-4"></div>
|
||||||
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
<div class="col-md-4"></div>
|
||||||
|
<div class="col-md-4">
|
||||||
<form asp-action="Pair" method="post">
|
<form asp-action="Pair" method="post">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label asp-for="SelectedStore"></label>
|
<label asp-for="SelectedStore"></label>
|
||||||
@@ -39,5 +45,7 @@
|
|||||||
<button type="submit" class="btn btn-info" title="Approve this pairing demand">Approve</button>
|
<button type="submit" class="btn btn-info" title="Approve this pairing demand">Approve</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-md-4"></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ namespace BTCPayServer.Views.Stores
|
|||||||
|
|
||||||
|
|
||||||
public static string Tokens => "Tokens";
|
public static string Tokens => "Tokens";
|
||||||
|
public static string Users => "Users";
|
||||||
|
public static string UsersNavClass(ViewContext viewContext) => PageNavClass(viewContext, Users);
|
||||||
public static string TokensNavClass(ViewContext viewContext) => PageNavClass(viewContext, Tokens);
|
public static string TokensNavClass(ViewContext viewContext) => PageNavClass(viewContext, Tokens);
|
||||||
|
|
||||||
public static string IndexNavClass(ViewContext viewContext) => PageNavClass(viewContext, Index);
|
public static string IndexNavClass(ViewContext viewContext) => PageNavClass(viewContext, Index);
|
||||||
|
|||||||
56
BTCPayServer/Views/Stores/StoreUsers.cshtml
Normal file
56
BTCPayServer/Views/Stores/StoreUsers.cshtml
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
@model StoreUsersViewModel
|
||||||
|
@{
|
||||||
|
Layout = "../Shared/_NavLayout.cshtml";
|
||||||
|
ViewData["Title"] = "Manage users";
|
||||||
|
ViewData.AddActivePage(StoreNavPages.Users);
|
||||||
|
}
|
||||||
|
|
||||||
|
<h4>@ViewData["Title"]</h4>
|
||||||
|
@Html.Partial("_StatusMessage", TempData["TempDataProperty-StatusMessage"])
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div asp-validation-summary="All" class="text-danger"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<div class="form-group">
|
||||||
|
<h5>Users</h5>
|
||||||
|
<span>Add access to your store to other users (Guest will not be able to see and modify the store settings)</span>
|
||||||
|
</div>
|
||||||
|
<div class="form-inline">
|
||||||
|
<form method="post">
|
||||||
|
<input asp-for="Email" type="text" class="form-control" placeholder="user@example.com">
|
||||||
|
<select asp-for="Role" class="form-control">
|
||||||
|
<option value="@StoreRoles.Owner">Owner</option>
|
||||||
|
<option value="@StoreRoles.Guest">Guest</option>
|
||||||
|
</select>
|
||||||
|
<button type="submit" role="button" class="form-control btn btn-success"><span class="glyphicon glyphicon-plus"></span>Add user</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<table class="table">
|
||||||
|
<thead class="thead-inverse">
|
||||||
|
<tr>
|
||||||
|
<th>Email</th>
|
||||||
|
<th>Role</th>
|
||||||
|
<th style="text-align:right">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@foreach(var user in Model.Users)
|
||||||
|
{
|
||||||
|
<tr>
|
||||||
|
<td>@user.Email</td>
|
||||||
|
<td>@user.Role</td>
|
||||||
|
<td style="text-align:right">
|
||||||
|
<a asp-action="DeleteStoreUser" asp-route-storeId="@Model.StoreId" asp-route-userId="@user.Id">Remove</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -29,6 +29,7 @@
|
|||||||
<p id="check-success" style="display:none;"><span class="glyphicon glyphicon-ok-sign" style="color:green;"></span> <span class="check-label">Detecting hardware wallet...</span></p>
|
<p id="check-success" style="display:none;"><span class="glyphicon glyphicon-ok-sign" style="color:green;"></span> <span class="check-label">Detecting hardware wallet...</span></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="row">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<form id="sendform" style="display:none;">
|
<form id="sendform" style="display:none;">
|
||||||
<input type="hidden" id="cryptoCode" asp-for="CryptoCurrency" />
|
<input type="hidden" id="cryptoCode" asp-for="CryptoCurrency" />
|
||||||
@@ -60,6 +61,7 @@
|
|||||||
<button id="confirm-button" name="command" type="submit" class="btn btn-success">Confirm</button>
|
<button id="confirm-button" name="command" type="submit" class="btn btn-success">Confirm</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
@section Scripts
|
@section Scripts
|
||||||
{
|
{
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
|
|||||||
@@ -4,5 +4,6 @@
|
|||||||
<ul class="nav nav-pills nav-stacked">
|
<ul class="nav nav-pills nav-stacked">
|
||||||
<li class="@StoreNavPages.IndexNavClass(ViewContext)"><a asp-action="UpdateStore">Information</a></li>
|
<li class="@StoreNavPages.IndexNavClass(ViewContext)"><a asp-action="UpdateStore">Information</a></li>
|
||||||
<li class="@StoreNavPages.TokensNavClass(ViewContext)"><a asp-action="ListTokens">Access Tokens</a></li>
|
<li class="@StoreNavPages.TokensNavClass(ViewContext)"><a asp-action="ListTokens">Access Tokens</a></li>
|
||||||
|
<li class="@StoreNavPages.UsersNavClass(ViewContext)"><a asp-action="StoreUsers">Users</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-lg-12 text-center">
|
<div class="col-lg-12 text-center">
|
||||||
@Html.Partial("_StatusMessage", Model.StatusMessage)
|
@Html.Partial("_StatusMessage", TempData["TempDataProperty-StatusMessage"])
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -27,7 +27,6 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<th>Name</th>
|
<th>Name</th>
|
||||||
<th>Website</th>
|
<th>Website</th>
|
||||||
<th>Balances</th>
|
|
||||||
<th style="text-align:right">Actions</th>
|
<th style="text-align:right">Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -52,7 +51,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</td>
|
</td>
|
||||||
<td style="text-align:right"><a asp-action="UpdateStore" asp-route-storeId="@store.Id">Settings</a> - <a asp-action="DeleteStore" asp-route-storeId="@store.Id">Remove</a></td>
|
<td style="text-align:right">
|
||||||
|
@if(store.IsOwner)
|
||||||
|
{
|
||||||
|
<a asp-action="UpdateStore" asp-controller="Stores" asp-route-storeId="@store.Id">Settings</a><span> - </span>
|
||||||
|
}
|
||||||
|
<a asp-action="DeleteStore" asp-route-storeId="@store.Id">Remove</a>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
}
|
}
|
||||||
</tbody>
|
</tbody>
|
||||||
Reference in New Issue
Block a user