Can invite user to manage your store

This commit is contained in:
nicolas.dorier
2018-03-23 16:24:57 +09:00
parent 3b2cf2f1de
commit 39b34ff4ed
19 changed files with 514 additions and 174 deletions

View File

@@ -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; }

View File

@@ -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

View File

@@ -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)

View File

@@ -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)
{ {

View 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);
}
}
}

View File

@@ -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));
}); });
}); });

View 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; }
}
}

View File

@@ -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;

View File

@@ -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})");

View File

@@ -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();
} }
} }

View 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;
}
}
}
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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);

View 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>

View File

@@ -29,7 +29,8 @@
<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="col-md-6"> <div class="row">
<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" />
<div class="form-group"> <div class="form-group">
@@ -59,12 +60,13 @@
</div> </div>
<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">
@Model.ToJSVariableModel("srvModel") @Model.ToJSVariableModel("srvModel")
</script> </script>
<script src="~/js/ledgerwebsocket.js" type="text/javascript" defer="defer"></script> <script src="~/js/ledgerwebsocket.js" type="text/javascript" defer="defer"></script>
<script src="~/js/StoreWallet.js" type="text/javascript" defer="defer"></script> <script src="~/js/StoreWallet.js" type="text/javascript" defer="defer"></script>
} }

View File

@@ -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>

View File

@@ -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>