mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-02-20 13:34:37 +01:00
Can invite user to manage your store
This commit is contained in:
parent
3b2cf2f1de
commit
39b34ff4ed
19 changed files with 514 additions and 174 deletions
|
@ -56,10 +56,12 @@ namespace BTCPayServer.Tests
|
|||
|
||||
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" });
|
||||
StoreId = store.CreatedStoreId;
|
||||
return store;
|
||||
var store2 = parent.PayTester.GetController<StoresController>(UserId);
|
||||
store2.CreatedStoreId = store.CreatedStoreId;
|
||||
return store2;
|
||||
}
|
||||
|
||||
public BTCPayNetwork SupportedNetwork { get; set; }
|
||||
|
|
|
@ -417,7 +417,7 @@ namespace BTCPayServer.Controllers
|
|||
if (stores.Count() == 0)
|
||||
{
|
||||
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 });
|
||||
}
|
||||
|
@ -434,9 +434,18 @@ namespace BTCPayServer.Controllers
|
|||
return View(model);
|
||||
}
|
||||
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)
|
||||
{
|
||||
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
|
||||
{
|
||||
storeId = store.Id
|
||||
|
|
|
@ -15,6 +15,7 @@ namespace BTCPayServer.Controllers
|
|||
{
|
||||
public partial class StoresController
|
||||
{
|
||||
|
||||
[HttpGet]
|
||||
[Route("{storeId}/lightning/{cryptoCode}")]
|
||||
public async Task<IActionResult> AddLightningNode(string storeId, string cryptoCode)
|
||||
|
|
|
@ -27,10 +27,11 @@ namespace BTCPayServer.Controllers
|
|||
{
|
||||
[Route("stores")]
|
||||
[Authorize(AuthenticationSchemes = "Identity.Application")]
|
||||
[Authorize(Policy = "CanAccessStore")]
|
||||
[Authorize(Policy = StorePolicies.OwnStore)]
|
||||
[AutoValidateAntiforgeryToken]
|
||||
public partial class StoresController : Controller
|
||||
{
|
||||
public string CreatedStoreId { get; set; }
|
||||
public StoresController(
|
||||
NBXplorerDashboard dashboard,
|
||||
IServiceProvider serviceProvider,
|
||||
|
@ -83,32 +84,6 @@ namespace BTCPayServer.Controllers
|
|||
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]
|
||||
[Route("{storeId}/wallet/{cryptoCode}")]
|
||||
public async Task<IActionResult> Wallet(string storeId, string cryptoCode)
|
||||
|
@ -125,79 +100,84 @@ namespace BTCPayServer.Controllers
|
|||
private string GetStoreUrl(string storeId)
|
||||
{
|
||||
return HttpContext.Request.GetAbsoluteRoot() + "/stores/" + storeId + "/";
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("{storeId}/users")]
|
||||
public async Task<IActionResult> StoreUsers(string storeId)
|
||||
{
|
||||
StoreUsersViewModel vm = new StoreUsersViewModel();
|
||||
await FillUsers(storeId, vm);
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
private async Task FillUsers(string storeId, StoreUsersViewModel vm)
|
||||
{
|
||||
var users = await _Repo.GetStoreUsers(storeId);
|
||||
vm.StoreId = storeId;
|
||||
vm.Users = users.Select(u => new StoreUsersViewModel.StoreUserViewModel()
|
||||
{
|
||||
Email = u.Email,
|
||||
Id = u.Id,
|
||||
Role = u.Role
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("{storeId}/users")]
|
||||
public async Task<IActionResult> StoreUsers(string storeId, StoreUsersViewModel vm)
|
||||
{
|
||||
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]
|
||||
public async Task<IActionResult> ListStores()
|
||||
[Route("{storeId}/users/{userId}/delete")]
|
||||
public async Task<IActionResult> DeleteStoreUser(string storeId, string userId)
|
||||
{
|
||||
StoresViewModel result = new StoresViewModel();
|
||||
result.StatusMessage = StatusMessage;
|
||||
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,
|
||||
Balances = balances[i].Select(t => t.Result).ToArray()
|
||||
});
|
||||
}
|
||||
return View(result);
|
||||
}
|
||||
|
||||
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 "--";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("{storeId}/delete")]
|
||||
public async Task<IActionResult> DeleteStore(string storeId)
|
||||
{
|
||||
var store = await _Repo.FindStore(storeId, GetUserId());
|
||||
StoreUsersViewModel vm = new StoreUsersViewModel();
|
||||
var store = await _Repo.FindStore(storeId, userId);
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
var user = await _UserManager.FindByIdAsync(userId);
|
||||
if (user == null)
|
||||
return NotFound();
|
||||
return View("Confirm", new ConfirmModel()
|
||||
{
|
||||
Title = "Delete store " + store.StoreName,
|
||||
Description = "This store will still be accessible to users sharing it",
|
||||
Title = $"Remove store user",
|
||||
Description = $"Are you sure to remove access to remove {store.Role} access to {user.Email}?",
|
||||
Action = "Delete"
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("{storeId}/delete")]
|
||||
public async Task<IActionResult> DeleteStorePost(string storeId)
|
||||
[Route("{storeId}/users/{userId}/delete")]
|
||||
public async Task<IActionResult> DeleteStoreUserPost(string storeId, string userId)
|
||||
{
|
||||
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));
|
||||
await _Repo.RemoveStoreUser(storeId, userId);
|
||||
StatusMessage = "User removed successfully";
|
||||
return RedirectToAction(nameof(StoreUsers), new { storeId = storeId, userId = userId });
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
|
@ -403,15 +383,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 ?? storeId;
|
||||
var userId = GetUserId();
|
||||
if (userId == null)
|
||||
return Unauthorized();
|
||||
var store = await _Repo.FindStore(storeId, userId);
|
||||
if (store == null)
|
||||
return Unauthorized();
|
||||
if (store.Role != StoreRoles.Owner)
|
||||
{
|
||||
storeId = model.StoreId;
|
||||
var userId = GetUserId();
|
||||
if (userId == null)
|
||||
return Unauthorized();
|
||||
var store = await _Repo.FindStore(storeId, userId);
|
||||
if (store == null)
|
||||
return Unauthorized();
|
||||
StatusMessage = "Error: You need to be owner of this store to request pairing codes";
|
||||
return RedirectToAction(nameof(UserStoresController.ListStores), "UserStores");
|
||||
}
|
||||
|
||||
var tokenRequest = new TokenRequest()
|
||||
|
@ -491,11 +473,13 @@ namespace BTCPayServer.Controllers
|
|||
[Route("/api-access-request")]
|
||||
public async Task<IActionResult> RequestPairing(string pairingCode, string selectedStore = null)
|
||||
{
|
||||
if (pairingCode == null)
|
||||
return NotFound();
|
||||
var pairing = await _TokenRepository.GetPairingAsync(pairingCode);
|
||||
if (pairing == null)
|
||||
{
|
||||
StatusMessage = "Unknown pairing code";
|
||||
return RedirectToAction(nameof(ListStores));
|
||||
return RedirectToAction(nameof(UserStoresController.ListStores), "UserStores");
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -517,7 +501,7 @@ namespace BTCPayServer.Controllers
|
|||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("api-access-request")]
|
||||
[Route("/api-access-request")]
|
||||
public async Task<IActionResult> Pair(string pairingCode, string selectedStore)
|
||||
{
|
||||
if (pairingCode == null)
|
||||
|
@ -527,6 +511,12 @@ namespace BTCPayServer.Controllers
|
|||
if (store == null || pairing == null)
|
||||
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);
|
||||
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 =>
|
||||
{
|
||||
o.AddPolicy("CanAccessStore", builder =>
|
||||
o.AddPolicy(StorePolicies.CanAccessStores, builder =>
|
||||
{
|
||||
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 string StatusMessage
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public List<StoreViewModel> Stores
|
||||
{
|
||||
get; set;
|
||||
} = new List<StoreViewModel>();
|
||||
|
||||
public class StoreViewModel
|
||||
{
|
||||
public string Name
|
||||
|
@ -32,6 +29,11 @@ namespace BTCPayServer.Models.StoreViewModels
|
|||
{
|
||||
get; set;
|
||||
}
|
||||
public bool IsOwner
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public string[] Balances
|
||||
{
|
||||
get; set;
|
||||
|
|
|
@ -66,6 +66,10 @@ namespace BTCPayServer.Payments.Lightning
|
|||
{
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
using (var ctx = _ContextFactory.CreateContext())
|
||||
{
|
||||
return await ctx.UserStore
|
||||
return (await ctx.UserStore
|
||||
.Where(u => u.ApplicationUserId == userId)
|
||||
.Select(u => u.StoreData)
|
||||
.ToArrayAsync();
|
||||
.Select(u => new { u.StoreData, u.Role })
|
||||
.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="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="Manage" asp-action="Index" title="Manage" class="nav-link js-scroll-trigger">My settings</a>
|
||||
|
|
|
@ -13,31 +13,39 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<table class="table">
|
||||
<tr>
|
||||
<th>Label</th>
|
||||
<td>@Model.Label</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Facade</th>
|
||||
<td>@Model.Facade</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>SIN</th>
|
||||
<td>@Model.SIN</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div class="col-md-4"></div>
|
||||
<div class="col-md-4">
|
||||
<table class="table">
|
||||
<tr>
|
||||
<th>Label</th>
|
||||
<td style="text-align:right;">@Model.Label</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Facade</th>
|
||||
<td style="text-align:right;">@Model.Facade</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>SIN</th>
|
||||
<td style="text-align:right;">@Model.SIN</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="col-md-4"></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<form asp-action="Pair" method="post">
|
||||
<div class="form-group">
|
||||
<label asp-for="SelectedStore"></label>
|
||||
<select asp-for="SelectedStore" asp-items="@(new SelectList(Model.Stores,"Id","Name"))" class="form-control"></select>
|
||||
<span asp-validation-for="SelectedStore" class="text-danger"></span>
|
||||
</div>
|
||||
<input type="hidden" name="pairingCode" value="@Model.Id" />
|
||||
<button type="submit" class="btn btn-info" title="Approve this pairing demand">Approve</button>
|
||||
</form>
|
||||
<div class="col-md-4"></div>
|
||||
<div class="col-md-4">
|
||||
<form asp-action="Pair" method="post">
|
||||
<div class="form-group">
|
||||
<label asp-for="SelectedStore"></label>
|
||||
<select asp-for="SelectedStore" asp-items="@(new SelectList(Model.Stores,"Id","Name"))" class="form-control"></select>
|
||||
<span asp-validation-for="SelectedStore" class="text-danger"></span>
|
||||
</div>
|
||||
<input type="hidden" name="pairingCode" value="@Model.Id" />
|
||||
<button type="submit" class="btn btn-info" title="Approve this pairing demand">Approve</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="col-md-4"></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
@ -14,7 +14,8 @@ namespace BTCPayServer.Views.Stores
|
|||
|
||||
|
||||
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 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,42 +29,44 @@
|
|||
<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 class="col-md-6">
|
||||
<form id="sendform" style="display:none;">
|
||||
<input type="hidden" id="cryptoCode" asp-for="CryptoCurrency" />
|
||||
<div class="form-group">
|
||||
<label>Destination</label>
|
||||
<input id="destination-textbox" name="Destination" class="form-control" type="text" />
|
||||
<span id="Destination-Error" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Amount</label>
|
||||
<input id="amount-textbox" name="Amount" class="form-control" type="text" />
|
||||
<span id="Amount-Error" class="text-danger"></span>
|
||||
<p class="form-text text-muted crypto-info" style="display: none;">
|
||||
Your current balance is <a id="crypto-balance-link" href="#"><span id="crypto-balance"></span></a> <span id="crypto-code"></span>.
|
||||
</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Fee rate (satoshi per byte)</label>
|
||||
<input id="fee-textbox" name="FeeRate" class="form-control" type="text" />
|
||||
<span id="FeeRate-Error" class="text-danger"></span>
|
||||
<p class="form-text text-muted crypto-info" style="display: none;">
|
||||
The recommended value is <a id="crypto-fee-link" href="#"><span id="crypto-fee"></span></a> satoshi per byte.
|
||||
</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Subtract fees from amount</label>
|
||||
<input id="substract-checkbox" name="SubstractFees" class="form-check" type="checkbox" />
|
||||
</div>
|
||||
<button id="confirm-button" name="command" type="submit" class="btn btn-success">Confirm</button>
|
||||
</form>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<form id="sendform" style="display:none;">
|
||||
<input type="hidden" id="cryptoCode" asp-for="CryptoCurrency" />
|
||||
<div class="form-group">
|
||||
<label>Destination</label>
|
||||
<input id="destination-textbox" name="Destination" class="form-control" type="text" />
|
||||
<span id="Destination-Error" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Amount</label>
|
||||
<input id="amount-textbox" name="Amount" class="form-control" type="text" />
|
||||
<span id="Amount-Error" class="text-danger"></span>
|
||||
<p class="form-text text-muted crypto-info" style="display: none;">
|
||||
Your current balance is <a id="crypto-balance-link" href="#"><span id="crypto-balance"></span></a> <span id="crypto-code"></span>.
|
||||
</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Fee rate (satoshi per byte)</label>
|
||||
<input id="fee-textbox" name="FeeRate" class="form-control" type="text" />
|
||||
<span id="FeeRate-Error" class="text-danger"></span>
|
||||
<p class="form-text text-muted crypto-info" style="display: none;">
|
||||
The recommended value is <a id="crypto-fee-link" href="#"><span id="crypto-fee"></span></a> satoshi per byte.
|
||||
</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Subtract fees from amount</label>
|
||||
<input id="substract-checkbox" name="SubstractFees" class="form-check" type="checkbox" />
|
||||
</div>
|
||||
<button id="confirm-button" name="command" type="submit" class="btn btn-success">Confirm</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@section Scripts
|
||||
{
|
||||
<script type="text/javascript">
|
||||
@section Scripts
|
||||
{
|
||||
<script type="text/javascript">
|
||||
@Model.ToJSVariableModel("srvModel")
|
||||
</script>
|
||||
<script src="~/js/ledgerwebsocket.js" type="text/javascript" defer="defer"></script>
|
||||
<script src="~/js/StoreWallet.js" type="text/javascript" defer="defer"></script>
|
||||
}
|
||||
</script>
|
||||
<script src="~/js/ledgerwebsocket.js" type="text/javascript" defer="defer"></script>
|
||||
<script src="~/js/StoreWallet.js" type="text/javascript" defer="defer"></script>
|
||||
}
|
||||
|
|
|
@ -4,5 +4,6 @@
|
|||
<ul class="nav nav-pills nav-stacked">
|
||||
<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.UsersNavClass(ViewContext)"><a asp-action="StoreUsers">Users</a></li>
|
||||
</ul>
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
<div class="row">
|
||||
<div class="col-lg-12 text-center">
|
||||
@Html.Partial("_StatusMessage", Model.StatusMessage)
|
||||
@Html.Partial("_StatusMessage", TempData["TempDataProperty-StatusMessage"])
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -27,7 +27,6 @@
|
|||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Website</th>
|
||||
<th>Balances</th>
|
||||
<th style="text-align:right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
@ -52,7 +51,13 @@
|
|||
}
|
||||
}
|
||||
</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>
|
||||
}
|
||||
</tbody>
|
Loading…
Add table
Reference in a new issue