mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2024-11-19 01:43:50 +01:00
Store Custom Roles (#4940)
This commit is contained in:
parent
6b7fb55658
commit
783e4ccb35
@ -6,7 +6,8 @@ using Microsoft.Extensions.Logging;
|
|||||||
|
|
||||||
namespace BTCPayServer.Abstractions.TagHelpers;
|
namespace BTCPayServer.Abstractions.TagHelpers;
|
||||||
|
|
||||||
[HtmlTargetElement(Attributes = nameof(Permission))]
|
[HtmlTargetElement(Attributes = "[permission]")]
|
||||||
|
[HtmlTargetElement(Attributes = "[not-permission]" )]
|
||||||
public class PermissionTagHelper : TagHelper
|
public class PermissionTagHelper : TagHelper
|
||||||
{
|
{
|
||||||
private readonly IAuthorizationService _authorizationService;
|
private readonly IAuthorizationService _authorizationService;
|
||||||
@ -21,16 +22,19 @@ public class PermissionTagHelper : TagHelper
|
|||||||
}
|
}
|
||||||
|
|
||||||
public string Permission { get; set; }
|
public string Permission { get; set; }
|
||||||
|
public string NotPermission { get; set; }
|
||||||
public string PermissionResource { get; set; }
|
public string PermissionResource { get; set; }
|
||||||
|
|
||||||
|
|
||||||
public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
|
public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(Permission))
|
if (string.IsNullOrEmpty(Permission) && string.IsNullOrEmpty(NotPermission))
|
||||||
return;
|
return;
|
||||||
if (_httpContextAccessor.HttpContext is null)
|
if (_httpContextAccessor.HttpContext is null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var key = $"{Permission}_{PermissionResource}";
|
var expectedResult = !string.IsNullOrEmpty(Permission);
|
||||||
|
var key = $"{Permission??NotPermission}_{PermissionResource}";
|
||||||
if (!_httpContextAccessor.HttpContext.Items.TryGetValue(key, out var o) ||
|
if (!_httpContextAccessor.HttpContext.Items.TryGetValue(key, out var o) ||
|
||||||
o is not AuthorizationResult res)
|
o is not AuthorizationResult res)
|
||||||
{
|
{
|
||||||
@ -39,7 +43,7 @@ public class PermissionTagHelper : TagHelper
|
|||||||
Permission);
|
Permission);
|
||||||
_httpContextAccessor.HttpContext.Items.Add(key, res);
|
_httpContextAccessor.HttpContext.Items.Add(key, res);
|
||||||
}
|
}
|
||||||
if (!res.Succeeded)
|
if (expectedResult != res.Succeeded)
|
||||||
{
|
{
|
||||||
output.SuppressOutput();
|
output.SuppressOutput();
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using BTCPayServer.Client.Models;
|
using BTCPayServer.Client.Models;
|
||||||
@ -11,5 +12,11 @@ namespace BTCPayServer.Client
|
|||||||
var response = await _httpClient.SendAsync(CreateHttpRequest("api/v1/server/info"), token);
|
var response = await _httpClient.SendAsync(CreateHttpRequest("api/v1/server/info"), token);
|
||||||
return await HandleResponse<ServerInfoData>(response);
|
return await HandleResponse<ServerInfoData>(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public virtual async Task<List<RoleData>> GetServerRoles(CancellationToken token = default)
|
||||||
|
{
|
||||||
|
using var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/server/roles"), token);
|
||||||
|
return await HandleResponse<List<RoleData>>(response);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,13 @@ namespace BTCPayServer.Client
|
|||||||
{
|
{
|
||||||
public partial class BTCPayServerClient
|
public partial class BTCPayServerClient
|
||||||
{
|
{
|
||||||
|
public virtual async Task<List<RoleData>> GetStoreRoles(string storeId,
|
||||||
|
CancellationToken token = default)
|
||||||
|
{
|
||||||
|
using var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{storeId}/roles"), token);
|
||||||
|
return await HandleResponse<List<RoleData>>(response);
|
||||||
|
}
|
||||||
|
|
||||||
public virtual async Task<IEnumerable<StoreUserData>> GetStoreUsers(string storeId,
|
public virtual async Task<IEnumerable<StoreUserData>> GetStoreUsers(string storeId,
|
||||||
CancellationToken token = default)
|
CancellationToken token = default)
|
||||||
{
|
{
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace BTCPayServer.Client.Models
|
namespace BTCPayServer.Client.Models
|
||||||
{
|
{
|
||||||
public class StoreData : StoreBaseData
|
public class StoreData : StoreBaseData
|
||||||
@ -17,4 +19,12 @@ namespace BTCPayServer.Client.Models
|
|||||||
|
|
||||||
public string Role { get; set; }
|
public string Role { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class RoleData
|
||||||
|
{
|
||||||
|
public string Id { get; set; }
|
||||||
|
public List<string> Permissions { get; set; }
|
||||||
|
public string Role { get; set; }
|
||||||
|
public bool IsServerRole { get; set; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Linq.Expressions;
|
||||||
|
|
||||||
namespace BTCPayServer.Client
|
namespace BTCPayServer.Client
|
||||||
{
|
{
|
||||||
@ -134,7 +136,7 @@ namespace BTCPayServer.Client
|
|||||||
{
|
{
|
||||||
static Permission()
|
static Permission()
|
||||||
{
|
{
|
||||||
Init();
|
PolicyMap = Init();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Permission Create(string policy, string scope = null)
|
public static Permission Create(string policy, string scope = null)
|
||||||
@ -235,11 +237,13 @@ namespace BTCPayServer.Client
|
|||||||
return subPolicies.Contains(subpolicy) || subPolicies.Any(s => ContainsPolicy(s, subpolicy));
|
return subPolicies.Contains(subpolicy) || subPolicies.Any(s => ContainsPolicy(s, subpolicy));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Dictionary<string, HashSet<string>> PolicyMap = new();
|
public static ReadOnlyDictionary<string, HashSet<string>> PolicyMap { get; private set; }
|
||||||
|
|
||||||
|
|
||||||
private static void Init()
|
private static ReadOnlyDictionary<string, HashSet<string>> Init()
|
||||||
{
|
{
|
||||||
PolicyHasChild(Policies.CanModifyStoreSettings,
|
var policyMap = new Dictionary<string, HashSet<string>>();
|
||||||
|
PolicyHasChild(policyMap, Policies.CanModifyStoreSettings,
|
||||||
Policies.CanManageCustodianAccounts,
|
Policies.CanManageCustodianAccounts,
|
||||||
Policies.CanManagePullPayments,
|
Policies.CanManagePullPayments,
|
||||||
Policies.CanModifyInvoices,
|
Policies.CanModifyInvoices,
|
||||||
@ -248,25 +252,42 @@ namespace BTCPayServer.Client
|
|||||||
Policies.CanModifyPaymentRequests,
|
Policies.CanModifyPaymentRequests,
|
||||||
Policies.CanUseLightningNodeInStore);
|
Policies.CanUseLightningNodeInStore);
|
||||||
|
|
||||||
PolicyHasChild(Policies.CanManageUsers, Policies.CanCreateUser);
|
PolicyHasChild(policyMap,Policies.CanManageUsers, Policies.CanCreateUser);
|
||||||
PolicyHasChild(Policies.CanManagePullPayments, Policies.CanCreatePullPayments);
|
PolicyHasChild(policyMap,Policies.CanManagePullPayments, Policies.CanCreatePullPayments);
|
||||||
PolicyHasChild(Policies.CanCreatePullPayments, Policies.CanCreateNonApprovedPullPayments);
|
PolicyHasChild(policyMap,Policies.CanCreatePullPayments, Policies.CanCreateNonApprovedPullPayments);
|
||||||
PolicyHasChild(Policies.CanModifyPaymentRequests, Policies.CanViewPaymentRequests);
|
PolicyHasChild(policyMap,Policies.CanModifyPaymentRequests, Policies.CanViewPaymentRequests);
|
||||||
PolicyHasChild(Policies.CanModifyProfile, Policies.CanViewProfile);
|
PolicyHasChild(policyMap,Policies.CanModifyProfile, Policies.CanViewProfile);
|
||||||
PolicyHasChild(Policies.CanUseLightningNodeInStore, Policies.CanViewLightningInvoiceInStore, Policies.CanCreateLightningInvoiceInStore);
|
PolicyHasChild(policyMap,Policies.CanUseLightningNodeInStore, Policies.CanViewLightningInvoiceInStore, Policies.CanCreateLightningInvoiceInStore);
|
||||||
PolicyHasChild(Policies.CanManageNotificationsForUser, Policies.CanViewNotificationsForUser);
|
PolicyHasChild(policyMap,Policies.CanManageNotificationsForUser, Policies.CanViewNotificationsForUser);
|
||||||
PolicyHasChild(Policies.CanModifyServerSettings,
|
PolicyHasChild(policyMap,Policies.CanModifyServerSettings,
|
||||||
Policies.CanUseInternalLightningNode,
|
Policies.CanUseInternalLightningNode,
|
||||||
Policies.CanManageUsers);
|
Policies.CanManageUsers);
|
||||||
PolicyHasChild(Policies.CanUseInternalLightningNode, Policies.CanCreateLightningInvoiceInternalNode, Policies.CanViewLightningInvoiceInternalNode);
|
PolicyHasChild(policyMap, Policies.CanUseInternalLightningNode, Policies.CanCreateLightningInvoiceInternalNode, Policies.CanViewLightningInvoiceInternalNode);
|
||||||
PolicyHasChild(Policies.CanManageCustodianAccounts, Policies.CanViewCustodianAccounts);
|
PolicyHasChild(policyMap, Policies.CanManageCustodianAccounts, Policies.CanViewCustodianAccounts);
|
||||||
PolicyHasChild(Policies.CanModifyInvoices, Policies.CanViewInvoices, Policies.CanCreateInvoice, Policies.CanCreateLightningInvoiceInStore);
|
PolicyHasChild(policyMap, Policies.CanModifyInvoices, Policies.CanViewInvoices, Policies.CanCreateInvoice, Policies.CanCreateLightningInvoiceInStore);
|
||||||
PolicyHasChild(Policies.CanViewStoreSettings, Policies.CanViewInvoices, Policies.CanViewPaymentRequests);
|
PolicyHasChild(policyMap, Policies.CanViewStoreSettings, Policies.CanViewInvoices, Policies.CanViewPaymentRequests);
|
||||||
|
|
||||||
|
var missingPolicies = Policies.AllPolicies.ToHashSet();
|
||||||
|
//recurse through the tree to see which policies are not included in the tree
|
||||||
|
foreach (var policy in policyMap)
|
||||||
|
{
|
||||||
|
missingPolicies.Remove(policy.Key);
|
||||||
|
foreach (var subPolicy in policy.Value)
|
||||||
|
{
|
||||||
|
missingPolicies.Remove(subPolicy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var missingPolicy in missingPolicies)
|
||||||
|
{
|
||||||
|
policyMap.Add(missingPolicy, new HashSet<string>());
|
||||||
|
}
|
||||||
|
return new ReadOnlyDictionary<string, HashSet<string>>(policyMap);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void PolicyHasChild(string policy, params string[] subPolicies)
|
private static void PolicyHasChild(Dictionary<string, HashSet<string>>policyMap, string policy, params string[] subPolicies)
|
||||||
{
|
{
|
||||||
if (PolicyMap.TryGetValue(policy, out var existingSubPolicies))
|
if (policyMap.TryGetValue(policy, out var existingSubPolicies))
|
||||||
{
|
{
|
||||||
foreach (string subPolicy in subPolicies)
|
foreach (string subPolicy in subPolicies)
|
||||||
{
|
{
|
||||||
@ -275,7 +296,7 @@ namespace BTCPayServer.Client
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
PolicyMap.Add(policy, subPolicies.ToHashSet());
|
policyMap.Add(policy, subPolicies.ToHashSet());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -64,6 +64,7 @@ namespace BTCPayServer.Data
|
|||||||
public DbSet<U2FDevice> U2FDevices { get; set; }
|
public DbSet<U2FDevice> U2FDevices { get; set; }
|
||||||
public DbSet<Fido2Credential> Fido2Credentials { get; set; }
|
public DbSet<Fido2Credential> Fido2Credentials { get; set; }
|
||||||
public DbSet<UserStore> UserStore { get; set; }
|
public DbSet<UserStore> UserStore { get; set; }
|
||||||
|
public DbSet<StoreRole> StoreRoles { get; set; }
|
||||||
[Obsolete]
|
[Obsolete]
|
||||||
public DbSet<WalletData> Wallets { get; set; }
|
public DbSet<WalletData> Wallets { get; set; }
|
||||||
public DbSet<WalletObjectData> WalletObjects { get; set; }
|
public DbSet<WalletObjectData> WalletObjects { get; set; }
|
||||||
@ -129,6 +130,7 @@ namespace BTCPayServer.Data
|
|||||||
PayoutProcessorData.OnModelCreating(builder, Database);
|
PayoutProcessorData.OnModelCreating(builder, Database);
|
||||||
WebhookData.OnModelCreating(builder, Database);
|
WebhookData.OnModelCreating(builder, Database);
|
||||||
FormData.OnModelCreating(builder, Database);
|
FormData.OnModelCreating(builder, Database);
|
||||||
|
StoreRole.OnModelCreating(builder, Database);
|
||||||
|
|
||||||
|
|
||||||
if (Database.IsSqlite() && !_designTime)
|
if (Database.IsSqlite() && !_designTime)
|
||||||
|
@ -1,13 +1,10 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using BTCPayServer.Client;
|
|
||||||
using BTCPayServer.Client.Models;
|
using BTCPayServer.Client.Models;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
using PayoutProcessorData = BTCPayServer.Data.PayoutProcessorData;
|
|
||||||
|
|
||||||
namespace BTCPayServer.Data
|
namespace BTCPayServer.Data
|
||||||
{
|
{
|
||||||
@ -37,8 +34,6 @@ namespace BTCPayServer.Data
|
|||||||
|
|
||||||
public byte[] StoreCertificate { get; set; }
|
public byte[] StoreCertificate { get; set; }
|
||||||
|
|
||||||
[NotMapped] public string Role { get; set; }
|
|
||||||
|
|
||||||
public string StoreBlob { get; set; }
|
public string StoreBlob { get; set; }
|
||||||
|
|
||||||
[Obsolete("Use GetDefaultPaymentId instead")]
|
[Obsolete("Use GetDefaultPaymentId instead")]
|
||||||
@ -52,6 +47,7 @@ namespace BTCPayServer.Data
|
|||||||
public IEnumerable<CustodianAccountData> CustodianAccounts { get; set; }
|
public IEnumerable<CustodianAccountData> CustodianAccounts { get; set; }
|
||||||
public IEnumerable<StoreSettingData> Settings { get; set; }
|
public IEnumerable<StoreSettingData> Settings { get; set; }
|
||||||
public IEnumerable<FormData> Forms { get; set; }
|
public IEnumerable<FormData> Forms { get; set; }
|
||||||
|
public IEnumerable<StoreRole> StoreRoles { get; set; }
|
||||||
|
|
||||||
internal static void OnModelCreating(ModelBuilder builder, DatabaseFacade databaseFacade)
|
internal static void OnModelCreating(ModelBuilder builder, DatabaseFacade databaseFacade)
|
||||||
{
|
{
|
||||||
|
50
BTCPayServer.Data/Data/StoreRole.cs
Normal file
50
BTCPayServer.Data/Data/StoreRole.cs
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
using System.Linq;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.ChangeTracking;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Data;
|
||||||
|
|
||||||
|
public class StoreRole
|
||||||
|
{
|
||||||
|
public string Id { get; set; }
|
||||||
|
public string StoreDataId { get; set; }
|
||||||
|
public string Role { get; set; }
|
||||||
|
public List<string> Permissions { get; set; }
|
||||||
|
public List<UserStore> Users { get; set; }
|
||||||
|
public StoreData StoreData { get; set; }
|
||||||
|
|
||||||
|
internal static void OnModelCreating(ModelBuilder builder, DatabaseFacade databaseFacade)
|
||||||
|
{
|
||||||
|
builder.Entity<StoreRole>(entity =>
|
||||||
|
{
|
||||||
|
entity.HasOne(e => e.StoreData)
|
||||||
|
.WithMany(s => s.StoreRoles)
|
||||||
|
.HasForeignKey(e => e.StoreDataId)
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired(false);
|
||||||
|
|
||||||
|
entity.HasIndex(entity => new {entity.StoreDataId, entity.Role}).IsUnique();
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if (!databaseFacade.IsNpgsql())
|
||||||
|
{
|
||||||
|
builder.Entity<StoreRole>()
|
||||||
|
.Property(o => o.Permissions)
|
||||||
|
.HasConversion(
|
||||||
|
v => JsonConvert.SerializeObject(v),
|
||||||
|
v => JsonConvert.DeserializeObject<List<string>>(v)?? new List<string>(),
|
||||||
|
new ValueComparer<List<string>>(
|
||||||
|
(c1, c2) => c1 ==c2 || c1 != null && c2 != null && c1.SequenceEqual(c2),
|
||||||
|
c => c.Aggregate(0, (a, v) => HashCode.Combine(a, v.GetHashCode())),
|
||||||
|
c => c.ToList()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,3 +1,4 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace BTCPayServer.Data
|
namespace BTCPayServer.Data
|
||||||
@ -9,7 +10,10 @@ namespace BTCPayServer.Data
|
|||||||
|
|
||||||
public string StoreDataId { get; set; }
|
public string StoreDataId { get; set; }
|
||||||
public StoreData StoreData { get; set; }
|
public StoreData StoreData { get; set; }
|
||||||
public string Role { get; set; }
|
[Column("Role")]
|
||||||
|
public string StoreRoleId { get; set; }
|
||||||
|
public StoreRole StoreRole { get; set; }
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
internal static void OnModelCreating(ModelBuilder builder)
|
internal static void OnModelCreating(ModelBuilder builder)
|
||||||
@ -32,6 +36,10 @@ namespace BTCPayServer.Data
|
|||||||
.HasOne(pt => pt.StoreData)
|
.HasOne(pt => pt.StoreData)
|
||||||
.WithMany(t => t.UserStores)
|
.WithMany(t => t.UserStores)
|
||||||
.HasForeignKey(pt => pt.StoreDataId);
|
.HasForeignKey(pt => pt.StoreDataId);
|
||||||
|
|
||||||
|
builder.Entity<UserStore>().HasOne(e => e.StoreRole)
|
||||||
|
.WithMany(role => role.Users)
|
||||||
|
.HasForeignKey(e => e.StoreRoleId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
106
BTCPayServer.Data/Migrations/20230504125505_StoreRoles.cs
Normal file
106
BTCPayServer.Data/Migrations/20230504125505_StoreRoles.cs
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
using System;
|
||||||
|
using BTCPayServer.Data;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using NBitcoin;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace BTCPayServer.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(ApplicationDbContext))]
|
||||||
|
[Migration("20230504125505_StoreRoles")]
|
||||||
|
public partial class StoreRoles : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
var permissionsType = migrationBuilder.IsNpgsql() ? "TEXT[]" : "TEXT";
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "StoreRoles",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<string>(type: "TEXT", nullable: false),
|
||||||
|
StoreDataId = table.Column<string>(type: "TEXT", nullable: true),
|
||||||
|
Role = table.Column<string>(type: "TEXT", nullable: false),
|
||||||
|
Permissions = table.Column<string>(type: permissionsType, nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_StoreRoles", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_StoreRoles_Stores_StoreDataId",
|
||||||
|
column: x => x.StoreDataId,
|
||||||
|
principalTable: "Stores",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_StoreRoles_StoreDataId_Role",
|
||||||
|
table: "StoreRoles",
|
||||||
|
columns: new[] { "StoreDataId", "Role" },
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
object GetPermissionsData(string[] permissions)
|
||||||
|
{
|
||||||
|
if (migrationBuilder.IsNpgsql())
|
||||||
|
return permissions;
|
||||||
|
return JsonConvert.SerializeObject(permissions);
|
||||||
|
}
|
||||||
|
|
||||||
|
migrationBuilder.InsertData(
|
||||||
|
"StoreRoles",
|
||||||
|
columns: new[] { "Id", "Role", "Permissions" },
|
||||||
|
columnTypes: new[] { "TEXT", "TEXT", permissionsType },
|
||||||
|
values: new object[,]
|
||||||
|
{
|
||||||
|
{
|
||||||
|
"Owner", "Owner", GetPermissionsData(new[]
|
||||||
|
{
|
||||||
|
"btcpay.store.canmodifystoresettings",
|
||||||
|
"btcpay.store.cantradecustodianaccount",
|
||||||
|
"btcpay.store.canwithdrawfromcustodianaccount",
|
||||||
|
"btcpay.store.candeposittocustodianaccount"
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Guest", "Guest", GetPermissionsData(new[]
|
||||||
|
{
|
||||||
|
"btcpay.store.canviewstoresettings",
|
||||||
|
"btcpay.store.canmodifyinvoices",
|
||||||
|
"btcpay.store.canviewcustodianaccounts",
|
||||||
|
"btcpay.store.candeposittocustodianaccount"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.SupportAddForeignKey(migrationBuilder.ActiveProvider))
|
||||||
|
{
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_UserStore_StoreRoles_Role",
|
||||||
|
table: "UserStore",
|
||||||
|
column: "Role",
|
||||||
|
principalTable: "StoreRoles",
|
||||||
|
principalColumn: "Id");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
|
||||||
|
if (this.SupportDropForeignKey(migrationBuilder.ActiveProvider))
|
||||||
|
{
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_UserStore_StoreRoles_Role",
|
||||||
|
table: "UserStore");
|
||||||
|
}
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "StoreRoles");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -214,56 +214,6 @@ namespace BTCPayServer.Migrations
|
|||||||
b.ToTable("CustodianAccount");
|
b.ToTable("CustodianAccount");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("BTCPayServer.Data.FormData", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("Config")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<bool>("Public")
|
|
||||||
.HasColumnType("INTEGER");
|
|
||||||
|
|
||||||
b.Property<string>("StoreId")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("StoreId");
|
|
||||||
|
|
||||||
b.ToTable("Forms");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("BTCPayServer.Data.PayoutProcessorData", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<byte[]>("Blob")
|
|
||||||
.HasColumnType("BLOB");
|
|
||||||
|
|
||||||
b.Property<string>("PaymentMethod")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("Processor")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("StoreId")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("StoreId");
|
|
||||||
|
|
||||||
b.ToTable("PayoutProcessors");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("BTCPayServer.Data.Fido2Credential", b =>
|
modelBuilder.Entity("BTCPayServer.Data.Fido2Credential", b =>
|
||||||
{
|
{
|
||||||
b.Property<string>("Id")
|
b.Property<string>("Id")
|
||||||
@ -292,6 +242,31 @@ namespace BTCPayServer.Migrations
|
|||||||
b.ToTable("Fido2Credentials");
|
b.ToTable("Fido2Credentials");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BTCPayServer.Data.FormData", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Config")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("Public")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("StoreId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("StoreId");
|
||||||
|
|
||||||
|
b.ToTable("Forms");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("BTCPayServer.Data.InvoiceData", b =>
|
modelBuilder.Entity("BTCPayServer.Data.InvoiceData", b =>
|
||||||
{
|
{
|
||||||
b.Property<string>("Id")
|
b.Property<string>("Id")
|
||||||
@ -655,6 +630,34 @@ namespace BTCPayServer.Migrations
|
|||||||
b.ToTable("Payouts");
|
b.ToTable("Payouts");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BTCPayServer.Data.PayoutProcessorData", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<byte[]>("Blob")
|
||||||
|
.HasColumnType("BLOB");
|
||||||
|
|
||||||
|
b.Property<string>("Blob2")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("PaymentMethod")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Processor")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("StoreId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("StoreId");
|
||||||
|
|
||||||
|
b.ToTable("PayoutProcessors");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("BTCPayServer.Data.PendingInvoiceData", b =>
|
modelBuilder.Entity("BTCPayServer.Data.PendingInvoiceData", b =>
|
||||||
{
|
{
|
||||||
b.Property<string>("Id")
|
b.Property<string>("Id")
|
||||||
@ -802,6 +805,28 @@ namespace BTCPayServer.Migrations
|
|||||||
b.ToTable("Files");
|
b.ToTable("Files");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BTCPayServer.Data.StoreRole", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Permissions")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Role")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("StoreDataId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("StoreDataId", "Role")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("StoreRoles");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("BTCPayServer.Data.StoreSettingData", b =>
|
modelBuilder.Entity("BTCPayServer.Data.StoreSettingData", b =>
|
||||||
{
|
{
|
||||||
b.Property<string>("StoreId")
|
b.Property<string>("StoreId")
|
||||||
@ -878,13 +903,16 @@ namespace BTCPayServer.Migrations
|
|||||||
b.Property<string>("StoreDataId")
|
b.Property<string>("StoreDataId")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
b.Property<string>("Role")
|
b.Property<string>("StoreRoleId")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("Role");
|
||||||
|
|
||||||
b.HasKey("ApplicationUserId", "StoreDataId");
|
b.HasKey("ApplicationUserId", "StoreDataId");
|
||||||
|
|
||||||
b.HasIndex("StoreDataId");
|
b.HasIndex("StoreDataId");
|
||||||
|
|
||||||
|
b.HasIndex("StoreRoleId");
|
||||||
|
|
||||||
b.ToTable("UserStore");
|
b.ToTable("UserStore");
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -1188,26 +1216,6 @@ namespace BTCPayServer.Migrations
|
|||||||
b.Navigation("StoreData");
|
b.Navigation("StoreData");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("BTCPayServer.Data.FormData", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("BTCPayServer.Data.StoreData", "Store")
|
|
||||||
.WithMany("Forms")
|
|
||||||
.HasForeignKey("StoreId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade);
|
|
||||||
|
|
||||||
b.Navigation("Store");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("BTCPayServer.Data.PayoutProcessorData", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("BTCPayServer.Data.StoreData", "Store")
|
|
||||||
.WithMany("PayoutProcessors")
|
|
||||||
.HasForeignKey("StoreId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade);
|
|
||||||
|
|
||||||
b.Navigation("Store");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("BTCPayServer.Data.Fido2Credential", b =>
|
modelBuilder.Entity("BTCPayServer.Data.Fido2Credential", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("BTCPayServer.Data.ApplicationUser", "ApplicationUser")
|
b.HasOne("BTCPayServer.Data.ApplicationUser", "ApplicationUser")
|
||||||
@ -1218,6 +1226,16 @@ namespace BTCPayServer.Migrations
|
|||||||
b.Navigation("ApplicationUser");
|
b.Navigation("ApplicationUser");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BTCPayServer.Data.FormData", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("BTCPayServer.Data.StoreData", "Store")
|
||||||
|
.WithMany("Forms")
|
||||||
|
.HasForeignKey("StoreId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
|
||||||
|
b.Navigation("Store");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("BTCPayServer.Data.InvoiceData", b =>
|
modelBuilder.Entity("BTCPayServer.Data.InvoiceData", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
|
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
|
||||||
@ -1343,6 +1361,16 @@ namespace BTCPayServer.Migrations
|
|||||||
b.Navigation("StoreData");
|
b.Navigation("StoreData");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BTCPayServer.Data.PayoutProcessorData", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("BTCPayServer.Data.StoreData", "Store")
|
||||||
|
.WithMany("PayoutProcessors")
|
||||||
|
.HasForeignKey("StoreId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
|
||||||
|
b.Navigation("Store");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("BTCPayServer.Data.PendingInvoiceData", b =>
|
modelBuilder.Entity("BTCPayServer.Data.PendingInvoiceData", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
|
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
|
||||||
@ -1392,6 +1420,16 @@ namespace BTCPayServer.Migrations
|
|||||||
b.Navigation("ApplicationUser");
|
b.Navigation("ApplicationUser");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BTCPayServer.Data.StoreRole", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
|
||||||
|
.WithMany("StoreRoles")
|
||||||
|
.HasForeignKey("StoreDataId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
|
||||||
|
b.Navigation("StoreData");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("BTCPayServer.Data.StoreSettingData", b =>
|
modelBuilder.Entity("BTCPayServer.Data.StoreSettingData", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("BTCPayServer.Data.StoreData", "Store")
|
b.HasOne("BTCPayServer.Data.StoreData", "Store")
|
||||||
@ -1446,9 +1484,15 @@ namespace BTCPayServer.Migrations
|
|||||||
.OnDelete(DeleteBehavior.Cascade)
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
.IsRequired();
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("BTCPayServer.Data.StoreRole", "StoreRole")
|
||||||
|
.WithMany("Users")
|
||||||
|
.HasForeignKey("StoreRoleId");
|
||||||
|
|
||||||
b.Navigation("ApplicationUser");
|
b.Navigation("ApplicationUser");
|
||||||
|
|
||||||
b.Navigation("StoreData");
|
b.Navigation("StoreData");
|
||||||
|
|
||||||
|
b.Navigation("StoreRole");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("BTCPayServer.Data.WalletObjectLinkData", b =>
|
modelBuilder.Entity("BTCPayServer.Data.WalletObjectLinkData", b =>
|
||||||
@ -1606,9 +1650,16 @@ namespace BTCPayServer.Migrations
|
|||||||
|
|
||||||
b.Navigation("Settings");
|
b.Navigation("Settings");
|
||||||
|
|
||||||
|
b.Navigation("StoreRoles");
|
||||||
|
|
||||||
b.Navigation("UserStores");
|
b.Navigation("UserStores");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BTCPayServer.Data.StoreRole", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Users");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("BTCPayServer.Data.WalletData", b =>
|
modelBuilder.Entity("BTCPayServer.Data.WalletData", b =>
|
||||||
{
|
{
|
||||||
b.Navigation("WalletTransactions");
|
b.Navigation("WalletTransactions");
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using BTCPayServer.Client;
|
||||||
using BTCPayServer.Client.Models;
|
using BTCPayServer.Client.Models;
|
||||||
using BTCPayServer.Controllers;
|
using BTCPayServer.Controllers;
|
||||||
using BTCPayServer.Data;
|
using BTCPayServer.Data;
|
||||||
@ -55,7 +56,7 @@ namespace BTCPayServer.Tests
|
|||||||
Assert.Empty(appList2.Apps);
|
Assert.Empty(appList2.Apps);
|
||||||
Assert.Equal("test", appList.Apps[0].AppName);
|
Assert.Equal("test", appList.Apps[0].AppName);
|
||||||
Assert.Equal(apps.CreatedAppId, appList.Apps[0].Id);
|
Assert.Equal(apps.CreatedAppId, appList.Apps[0].Id);
|
||||||
Assert.True(appList.Apps[0].IsOwner);
|
Assert.True(appList.Apps[0].Role.ToPermissionSet(appList.Apps[0].StoreId).Contains(Policies.CanModifyStoreSettings, appList.Apps[0].StoreId));
|
||||||
Assert.Equal(user.StoreId, appList.Apps[0].StoreId);
|
Assert.Equal(user.StoreId, appList.Apps[0].StoreId);
|
||||||
Assert.IsType<NotFoundResult>(apps2.DeleteApp(appList.Apps[0].Id));
|
Assert.IsType<NotFoundResult>(apps2.DeleteApp(appList.Apps[0].Id));
|
||||||
Assert.IsType<ViewResult>(apps.DeleteApp(appList.Apps[0].Id));
|
Assert.IsType<ViewResult>(apps.DeleteApp(appList.Apps[0].Id));
|
||||||
|
@ -21,6 +21,7 @@ using BTCPayServer.Services;
|
|||||||
using BTCPayServer.Services.Custodian.Client.MockCustodian;
|
using BTCPayServer.Services.Custodian.Client.MockCustodian;
|
||||||
using BTCPayServer.Services.Notifications;
|
using BTCPayServer.Services.Notifications;
|
||||||
using BTCPayServer.Services.Notifications.Blobs;
|
using BTCPayServer.Services.Notifications.Blobs;
|
||||||
|
using BTCPayServer.Services.Stores;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
@ -228,7 +229,7 @@ namespace BTCPayServer.Tests
|
|||||||
await Assert.ThrowsAsync<GreenfieldAPIException>(() => newUserClient.GetInvoices(store.Id));
|
await Assert.ThrowsAsync<GreenfieldAPIException>(() => newUserClient.GetInvoices(store.Id));
|
||||||
|
|
||||||
// if user is a guest or owner, then it should be ok
|
// if user is a guest or owner, then it should be ok
|
||||||
await unrestricted.AddStoreUser(store.Id, new StoreUserData() { UserId = newUser.Id, Role = "Guest" });
|
await unrestricted.AddStoreUser(store.Id, new StoreUserData() { UserId = newUser.Id});
|
||||||
await newUserClient.GetInvoices(store.Id);
|
await newUserClient.GetInvoices(store.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1319,7 +1320,8 @@ namespace BTCPayServer.Tests
|
|||||||
// We strip the user's Owner right, so the key should not work
|
// We strip the user's Owner right, so the key should not work
|
||||||
using var ctx = tester.PayTester.GetService<Data.ApplicationDbContextFactory>().CreateContext();
|
using var ctx = tester.PayTester.GetService<Data.ApplicationDbContextFactory>().CreateContext();
|
||||||
var storeEntity = await ctx.UserStore.SingleAsync(u => u.ApplicationUserId == user.UserId && u.StoreDataId == newStore.Id);
|
var storeEntity = await ctx.UserStore.SingleAsync(u => u.ApplicationUserId == user.UserId && u.StoreDataId == newStore.Id);
|
||||||
storeEntity.Role = "Guest";
|
var roleId = (await tester.PayTester.GetService<StoreRepository>().GetStoreRoles(null)).Single(r => r.Role == "Guest").Id;
|
||||||
|
storeEntity.StoreRoleId = roleId;
|
||||||
await ctx.SaveChangesAsync();
|
await ctx.SaveChangesAsync();
|
||||||
await AssertHttpError(403, async () => await client.UpdateStore(newStore.Id, new UpdateStoreRequest() { Name = "B" }));
|
await AssertHttpError(403, async () => await client.UpdateStore(newStore.Id, new UpdateStoreRequest() { Name = "B" }));
|
||||||
|
|
||||||
@ -3365,11 +3367,16 @@ namespace BTCPayServer.Tests
|
|||||||
|
|
||||||
var client = await user.CreateClient(Policies.CanModifyStoreSettings, Policies.CanModifyServerSettings);
|
var client = await user.CreateClient(Policies.CanModifyStoreSettings, Policies.CanModifyServerSettings);
|
||||||
|
|
||||||
|
var roles = await client.GetServerRoles();
|
||||||
|
Assert.Equal(2,roles.Count);
|
||||||
|
#pragma warning disable CS0618
|
||||||
|
var ownerRole = roles.Single(data => data.Role == StoreRoles.Owner);
|
||||||
|
var guestRole = roles.Single(data => data.Role == StoreRoles.Guest);
|
||||||
|
#pragma warning restore CS0618
|
||||||
var users = await client.GetStoreUsers(user.StoreId);
|
var users = await client.GetStoreUsers(user.StoreId);
|
||||||
var storeuser = Assert.Single(users);
|
var storeuser = Assert.Single(users);
|
||||||
Assert.Equal(user.UserId, storeuser.UserId);
|
Assert.Equal(user.UserId, storeuser.UserId);
|
||||||
Assert.Equal(StoreRoles.Owner, storeuser.Role);
|
Assert.Equal(ownerRole.Id, storeuser.Role);
|
||||||
|
|
||||||
var user2 = tester.NewAccount();
|
var user2 = tester.NewAccount();
|
||||||
await user2.GrantAccessAsync(false);
|
await user2.GrantAccessAsync(false);
|
||||||
|
|
||||||
@ -3380,7 +3387,7 @@ namespace BTCPayServer.Tests
|
|||||||
await AssertPermissionError(Policies.CanModifyStoreSettings, async () => await user2Client.AddStoreUser(user.StoreId, new StoreUserData()));
|
await AssertPermissionError(Policies.CanModifyStoreSettings, async () => await user2Client.AddStoreUser(user.StoreId, new StoreUserData()));
|
||||||
await AssertPermissionError(Policies.CanModifyStoreSettings, async () => await user2Client.RemoveStoreUser(user.StoreId, user.UserId));
|
await AssertPermissionError(Policies.CanModifyStoreSettings, async () => await user2Client.RemoveStoreUser(user.StoreId, user.UserId));
|
||||||
|
|
||||||
await client.AddStoreUser(user.StoreId, new StoreUserData() { Role = StoreRoles.Guest, UserId = user2.UserId });
|
await client.AddStoreUser(user.StoreId, new StoreUserData() { Role = guestRole.Id, UserId = user2.UserId });
|
||||||
|
|
||||||
//test no access to api when only a guest
|
//test no access to api when only a guest
|
||||||
await AssertPermissionError(Policies.CanModifyStoreSettings, async () => await user2Client.GetStoreUsers(user.StoreId));
|
await AssertPermissionError(Policies.CanModifyStoreSettings, async () => await user2Client.GetStoreUsers(user.StoreId));
|
||||||
@ -3394,10 +3401,10 @@ namespace BTCPayServer.Tests
|
|||||||
await user2Client.GetStore(user.StoreId));
|
await user2Client.GetStore(user.StoreId));
|
||||||
|
|
||||||
|
|
||||||
await client.AddStoreUser(user.StoreId, new StoreUserData() { Role = StoreRoles.Owner, UserId = user2.UserId });
|
await client.AddStoreUser(user.StoreId, new StoreUserData() { Role = ownerRole.Id, UserId = user2.UserId });
|
||||||
await AssertAPIError("duplicate-store-user-role", async () =>
|
await AssertAPIError("duplicate-store-user-role", async () =>
|
||||||
await client.AddStoreUser(user.StoreId,
|
await client.AddStoreUser(user.StoreId,
|
||||||
new StoreUserData() { Role = StoreRoles.Owner, UserId = user2.UserId }));
|
new StoreUserData() { Role = ownerRole.Id, UserId = user2.UserId }));
|
||||||
await user2Client.RemoveStoreUser(user.StoreId, user.UserId);
|
await user2Client.RemoveStoreUser(user.StoreId, user.UserId);
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Buffers;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
@ -1741,11 +1742,12 @@ namespace BTCPayServer.Tests
|
|||||||
{
|
{
|
||||||
s.Driver.Navigate().Refresh();
|
s.Driver.Navigate().Refresh();
|
||||||
Assert.Contains("transaction-label", s.Driver.PageSource);
|
Assert.Contains("transaction-label", s.Driver.PageSource);
|
||||||
|
var labels = s.Driver.FindElements(By.CssSelector("#WalletTransactionsList tr:first-child div.transaction-label"));
|
||||||
|
Assert.Equal(2, labels.Count);
|
||||||
|
Assert.Contains(labels, element => element.Text == "payout");
|
||||||
|
Assert.Contains(labels, element => element.Text == "pull-payment");
|
||||||
});
|
});
|
||||||
var labels = s.Driver.FindElements(By.CssSelector("#WalletTransactionsList tr:first-child div.transaction-label"));
|
|
||||||
Assert.Equal(2, labels.Count);
|
|
||||||
Assert.Contains(labels, element => element.Text == "payout");
|
|
||||||
Assert.Contains(labels, element => element.Text == "pull-payment");
|
|
||||||
|
|
||||||
s.GoToStore(s.StoreId, StoreNavPages.Payouts);
|
s.GoToStore(s.StoreId, StoreNavPages.Payouts);
|
||||||
s.Driver.FindElement(By.Id($"{PayoutState.InProgress}-view")).Click();
|
s.Driver.FindElement(By.Id($"{PayoutState.InProgress}-view")).Click();
|
||||||
@ -2468,6 +2470,145 @@ retry:
|
|||||||
Assert.StartsWith(s.ServerUri.ToString(), s.Driver.Url);
|
Assert.StartsWith(s.ServerUri.ToString(), s.Driver.Url);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
[Fact]
|
||||||
|
[Trait("Selenium", "Selenium")]
|
||||||
|
public async Task CanUseRoleManager()
|
||||||
|
{
|
||||||
|
using var s = CreateSeleniumTester(newDb: true);
|
||||||
|
await s.StartAsync();
|
||||||
|
var user = s.RegisterNewUser(true);
|
||||||
|
s.GoToServer(ServerNavPages.Roles);
|
||||||
|
var existingServerRoles = s.Driver.FindElement(By.CssSelector("table")).FindElements(By.CssSelector("tr"));
|
||||||
|
Assert.Equal(3, existingServerRoles.Count);
|
||||||
|
IWebElement ownerRow = null;
|
||||||
|
IWebElement guestRow = null;
|
||||||
|
foreach (var roleItem in existingServerRoles)
|
||||||
|
{
|
||||||
|
if (roleItem.Text.Contains("owner", StringComparison.InvariantCultureIgnoreCase))
|
||||||
|
{
|
||||||
|
ownerRow = roleItem;
|
||||||
|
}
|
||||||
|
else if (roleItem.Text.Contains("guest", StringComparison.InvariantCultureIgnoreCase))
|
||||||
|
{
|
||||||
|
guestRow = roleItem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.NotNull(ownerRow);
|
||||||
|
Assert.NotNull(guestRow);
|
||||||
|
|
||||||
|
var ownerBadges = ownerRow.FindElements(By.CssSelector(".badge"));
|
||||||
|
Assert.Contains(ownerBadges, element => element.Text.Equals("Default", StringComparison.InvariantCultureIgnoreCase));
|
||||||
|
Assert.Contains(ownerBadges, element => element.Text.Equals("Server-wide", StringComparison.InvariantCultureIgnoreCase));
|
||||||
|
|
||||||
|
var guestBadges = guestRow.FindElements(By.CssSelector(".badge"));
|
||||||
|
Assert.DoesNotContain(guestBadges, element => element.Text.Equals("Default", StringComparison.InvariantCultureIgnoreCase));
|
||||||
|
Assert.Contains(guestBadges, element => element.Text.Equals("Server-wide", StringComparison.InvariantCultureIgnoreCase));
|
||||||
|
guestRow.FindElement(By.Id("SetDefault")).Click();
|
||||||
|
s.FindAlertMessage();
|
||||||
|
|
||||||
|
existingServerRoles = s.Driver.FindElement(By.CssSelector("table")).FindElements(By.CssSelector("tr"));
|
||||||
|
foreach (var roleItem in existingServerRoles)
|
||||||
|
{
|
||||||
|
if (roleItem.Text.Contains("owner", StringComparison.InvariantCultureIgnoreCase))
|
||||||
|
{
|
||||||
|
ownerRow = roleItem;
|
||||||
|
}
|
||||||
|
else if (roleItem.Text.Contains("guest", StringComparison.InvariantCultureIgnoreCase))
|
||||||
|
{
|
||||||
|
guestRow = roleItem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
guestBadges = guestRow.FindElements(By.CssSelector(".badge"));
|
||||||
|
Assert.Contains(guestBadges, element => element.Text.Equals("Default", StringComparison.InvariantCultureIgnoreCase));
|
||||||
|
|
||||||
|
ownerBadges = ownerRow.FindElements(By.CssSelector(".badge"));
|
||||||
|
Assert.DoesNotContain(ownerBadges, element => element.Text.Equals("Default", StringComparison.InvariantCultureIgnoreCase));
|
||||||
|
ownerRow.FindElement(By.Id("SetDefault")).Click();
|
||||||
|
s.FindAlertMessage();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
s.CreateNewStore();
|
||||||
|
s.GoToStore(StoreNavPages.Roles);
|
||||||
|
var existingStoreRoles = s.Driver.FindElement(By.CssSelector("table")).FindElements(By.CssSelector("tr"));
|
||||||
|
Assert.Equal(3, existingStoreRoles.Count);
|
||||||
|
Assert.Equal(2, existingStoreRoles.Count(element => element.Text.Contains("Server-wide", StringComparison.InvariantCultureIgnoreCase)));
|
||||||
|
|
||||||
|
foreach (var roleItem in existingStoreRoles)
|
||||||
|
{
|
||||||
|
if (roleItem.Text.Contains("owner", StringComparison.InvariantCultureIgnoreCase))
|
||||||
|
{
|
||||||
|
ownerRow = roleItem;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ownerRow.FindElement(By.LinkText("Remove")).Click();
|
||||||
|
Assert.DoesNotContain("ConfirmContinue", s.Driver.PageSource);
|
||||||
|
s.Driver.Navigate().Back();
|
||||||
|
existingStoreRoles = s.Driver.FindElement(By.CssSelector("table")).FindElements(By.CssSelector("tr"));
|
||||||
|
foreach (var roleItem in existingStoreRoles)
|
||||||
|
{
|
||||||
|
if (roleItem.Text.Contains("guest", StringComparison.InvariantCultureIgnoreCase))
|
||||||
|
{
|
||||||
|
guestRow = roleItem;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
guestRow.FindElement(By.LinkText("Remove")).Click();
|
||||||
|
s.Driver.FindElement(By.Id("ConfirmContinue")).Click();
|
||||||
|
s.FindAlertMessage();
|
||||||
|
|
||||||
|
s.GoToStore(StoreNavPages.Roles);
|
||||||
|
s.Driver.FindElement(By.Id("CreateRole")).Click();
|
||||||
|
|
||||||
|
Assert.Contains("Create role", s.Driver.PageSource);
|
||||||
|
s.Driver.FindElement(By.Id("Save")).Click();
|
||||||
|
s.Driver.FindElement(By.Id("Role")).SendKeys("store role");
|
||||||
|
s.Driver.FindElement(By.Id("Save")).Click();
|
||||||
|
s.FindAlertMessage();
|
||||||
|
|
||||||
|
existingStoreRoles = s.Driver.FindElement(By.CssSelector("table")).FindElements(By.CssSelector("tr"));
|
||||||
|
foreach (var roleItem in existingStoreRoles)
|
||||||
|
{
|
||||||
|
if (roleItem.Text.Contains("store role", StringComparison.InvariantCultureIgnoreCase))
|
||||||
|
{
|
||||||
|
guestRow = roleItem;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
guestBadges = guestRow.FindElements(By.CssSelector(".badge"));
|
||||||
|
Assert.DoesNotContain(guestBadges, element => element.Text.Equals("server-wide", StringComparison.InvariantCultureIgnoreCase));
|
||||||
|
s.GoToStore(StoreNavPages.Users);
|
||||||
|
var options = s.Driver.FindElements(By.CssSelector("#Role option"));
|
||||||
|
Assert.Equal(2, options.Count);
|
||||||
|
Assert.Contains(options, element => element.Text.Equals("store role", StringComparison.InvariantCultureIgnoreCase));
|
||||||
|
s.CreateNewStore();
|
||||||
|
s.GoToStore(StoreNavPages.Roles);
|
||||||
|
existingStoreRoles = s.Driver.FindElement(By.CssSelector("table")).FindElements(By.CssSelector("tr"));
|
||||||
|
Assert.Equal(2, existingStoreRoles.Count);
|
||||||
|
Assert.Equal(1, existingStoreRoles.Count(element => element.Text.Contains("Server-wide", StringComparison.InvariantCultureIgnoreCase)));
|
||||||
|
Assert.Equal(0, existingStoreRoles.Count(element => element.Text.Contains("store role", StringComparison.InvariantCultureIgnoreCase)));
|
||||||
|
s.GoToStore(StoreNavPages.Users);
|
||||||
|
options = s.Driver.FindElements(By.CssSelector("#Role option"));
|
||||||
|
Assert.Single(options);
|
||||||
|
Assert.DoesNotContain(options, element => element.Text.Equals("store role", StringComparison.InvariantCultureIgnoreCase));
|
||||||
|
|
||||||
|
|
||||||
|
s.GoToStore(StoreNavPages.Roles);
|
||||||
|
s.Driver.FindElement(By.Id("CreateRole")).Click();
|
||||||
|
s.Driver.FindElement(By.Id("Role")).SendKeys("Malice");
|
||||||
|
|
||||||
|
s.Driver.ExecuteJavaScript($"document.getElementById('Policies')['{Policies.CanModifyServerSettings}']=new Option('{Policies.CanModifyServerSettings}', '{Policies.CanModifyServerSettings}', true,true);");
|
||||||
|
|
||||||
|
s.Driver.FindElement(By.Id("Save")).Click();
|
||||||
|
s.FindAlertMessage();
|
||||||
|
Assert.Contains("Malice",s.Driver.PageSource);
|
||||||
|
Assert.DoesNotContain(Policies.CanModifyServerSettings,s.Driver.PageSource);
|
||||||
|
}
|
||||||
|
|
||||||
private static void CanBrowseContent(SeleniumTester s)
|
private static void CanBrowseContent(SeleniumTester s)
|
||||||
{
|
{
|
||||||
|
@ -470,7 +470,10 @@ namespace BTCPayServer.Tests
|
|||||||
var req = await _server.GetNextRequest(cancellation);
|
var req = await _server.GetNextRequest(cancellation);
|
||||||
var bytes = await req.Request.Body.ReadBytesAsync((int)req.Request.Headers.ContentLength);
|
var bytes = await req.Request.Body.ReadBytesAsync((int)req.Request.Headers.ContentLength);
|
||||||
var callback = Encoding.UTF8.GetString(bytes);
|
var callback = Encoding.UTF8.GetString(bytes);
|
||||||
_webhookEvents.Add(JsonConvert.DeserializeObject<WebhookInvoiceEvent>(callback));
|
lock (_webhookEvents)
|
||||||
|
{
|
||||||
|
_webhookEvents.Add(JsonConvert.DeserializeObject<WebhookInvoiceEvent>(callback));
|
||||||
|
}
|
||||||
req.Response.StatusCode = 200;
|
req.Response.StatusCode = 200;
|
||||||
_server.Done();
|
_server.Done();
|
||||||
}
|
}
|
||||||
@ -487,18 +490,21 @@ namespace BTCPayServer.Tests
|
|||||||
{
|
{
|
||||||
int retry = 0;
|
int retry = 0;
|
||||||
retry:
|
retry:
|
||||||
foreach (var evt in WebhookEvents)
|
lock (WebhookEvents)
|
||||||
{
|
{
|
||||||
if (evt.Type == eventType)
|
foreach (var evt in WebhookEvents)
|
||||||
{
|
{
|
||||||
var typedEvt = evt.ReadAs<TEvent>();
|
if (evt.Type == eventType)
|
||||||
try
|
|
||||||
{
|
|
||||||
assert(typedEvt);
|
|
||||||
return typedEvt;
|
|
||||||
}
|
|
||||||
catch (XunitException)
|
|
||||||
{
|
{
|
||||||
|
var typedEvt = evt.ReadAs<TEvent>();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
assert(typedEvt);
|
||||||
|
return typedEvt;
|
||||||
|
}
|
||||||
|
catch (XunitException)
|
||||||
|
{
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -540,12 +546,12 @@ retry:
|
|||||||
public async Task AddGuest(string userId)
|
public async Task AddGuest(string userId)
|
||||||
{
|
{
|
||||||
var repo = this.parent.PayTester.GetService<StoreRepository>();
|
var repo = this.parent.PayTester.GetService<StoreRepository>();
|
||||||
await repo.AddStoreUser(StoreId, userId, "Guest");
|
await repo.AddStoreUser(StoreId, userId, StoreRoleId.Guest);
|
||||||
}
|
}
|
||||||
public async Task AddOwner(string userId)
|
public async Task AddOwner(string userId)
|
||||||
{
|
{
|
||||||
var repo = this.parent.PayTester.GetService<StoreRepository>();
|
var repo = this.parent.PayTester.GetService<StoreRepository>();
|
||||||
await repo.AddStoreUser(StoreId, userId, "Owner");
|
await repo.AddStoreUser(StoreId, userId, StoreRoleId.Owner);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1962,7 +1962,8 @@ namespace BTCPayServer.Tests
|
|||||||
Assert.Empty(appList2.Apps);
|
Assert.Empty(appList2.Apps);
|
||||||
Assert.Equal("test", appList.Apps[0].AppName);
|
Assert.Equal("test", appList.Apps[0].AppName);
|
||||||
Assert.Equal(apps.CreatedAppId, appList.Apps[0].Id);
|
Assert.Equal(apps.CreatedAppId, appList.Apps[0].Id);
|
||||||
Assert.True(app.IsOwner);
|
|
||||||
|
Assert.True(app.Role.ToPermissionSet(app.StoreId).Contains(Policies.CanModifyStoreSettings, app.StoreId));
|
||||||
Assert.Equal(user.StoreId, appList.Apps[0].StoreId);
|
Assert.Equal(user.StoreId, appList.Apps[0].StoreId);
|
||||||
Assert.IsType<NotFoundResult>(apps2.DeleteApp(appList.Apps[0].Id));
|
Assert.IsType<NotFoundResult>(apps2.DeleteApp(appList.Apps[0].Id));
|
||||||
Assert.IsType<ViewResult>(apps.DeleteApp(appList.Apps[0].Id));
|
Assert.IsType<ViewResult>(apps.DeleteApp(appList.Apps[0].Id));
|
||||||
|
@ -72,7 +72,6 @@ namespace BTCPayServer.Components.MainNav
|
|||||||
vm.Apps = apps.Select(a => new StoreApp
|
vm.Apps = apps.Select(a => new StoreApp
|
||||||
{
|
{
|
||||||
Id = a.Id,
|
Id = a.Id,
|
||||||
IsOwner = a.IsOwner,
|
|
||||||
AppName = a.AppName,
|
AppName = a.AppName,
|
||||||
AppType = a.AppType
|
AppType = a.AppType
|
||||||
}).ToList();
|
}).ToList();
|
||||||
|
@ -20,6 +20,5 @@ namespace BTCPayServer.Components.MainNav
|
|||||||
public string Id { get; set; }
|
public string Id { get; set; }
|
||||||
public string AppName { get; set; }
|
public string AppName { get; set; }
|
||||||
public string AppType { get; set; }
|
public string AppType { get; set; }
|
||||||
public bool IsOwner { get; set; }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
||||||
@using BTCPayServer.Abstractions.Extensions
|
@using BTCPayServer.Abstractions.Extensions
|
||||||
@using BTCPayServer.Abstractions.Contracts
|
@using BTCPayServer.Abstractions.Contracts
|
||||||
|
@using BTCPayServer.Client
|
||||||
@using BTCPayServer.Services
|
@using BTCPayServer.Services
|
||||||
@inject SignInManager<ApplicationUser> SignInManager
|
@inject SignInManager<ApplicationUser> SignInManager
|
||||||
@inject BTCPayServerEnvironment Env
|
@inject BTCPayServerEnvironment Env
|
||||||
@ -29,13 +30,11 @@
|
|||||||
{
|
{
|
||||||
<a asp-controller="UIHome" asp-action="Index" id="StoreSelectorHome" class="navbar-brand py-2">@{await LogoContent();}</a>
|
<a asp-controller="UIHome" asp-action="Index" id="StoreSelectorHome" class="navbar-brand py-2">@{await LogoContent();}</a>
|
||||||
}
|
}
|
||||||
else if (Model.CurrentStoreIsOwner)
|
|
||||||
{
|
|
||||||
<a asp-controller="UIStores" asp-action="Dashboard" asp-route-storeId="@Model.CurrentStoreId" id="StoreSelectorHome" class="navbar-brand py-2">@{await LogoContent();}</a>
|
|
||||||
}
|
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<a asp-controller="UIInvoice" asp-action="ListInvoices" asp-route-storeId="@Model.CurrentStoreId" id="StoreSelectorHome" class="navbar-brand py-2">@{await LogoContent();}</a>
|
<a asp-controller="UIStores" asp-action="Dashboard" permission="@Policies.CanModifyStoreSettings" asp-route-storeId="@Model.CurrentStoreId" id="StoreSelectorHome" class="navbar-brand py-2">@{await LogoContent();}</a>
|
||||||
|
|
||||||
|
<a asp-controller="UIInvoice" asp-action="ListInvoices" not-permission="@Policies.CanModifyStoreSettings" asp-route-storeId="@Model.CurrentStoreId" id="StoreSelectorHome" class="navbar-brand py-2">@{await LogoContent();}</a>
|
||||||
}
|
}
|
||||||
@if (Model.Options.Any())
|
@if (Model.Options.Any())
|
||||||
{
|
{
|
||||||
|
@ -43,7 +43,6 @@ namespace BTCPayServer.Components.StoreSelector
|
|||||||
Text = store.StoreName,
|
Text = store.StoreName,
|
||||||
Value = store.Id,
|
Value = store.Id,
|
||||||
Selected = store.Id == currentStore?.Id,
|
Selected = store.Id == currentStore?.Id,
|
||||||
IsOwner = store.Role == StoreRoles.Owner,
|
|
||||||
WalletId = walletId
|
WalletId = walletId
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
@ -57,7 +56,6 @@ namespace BTCPayServer.Components.StoreSelector
|
|||||||
Options = options,
|
Options = options,
|
||||||
CurrentStoreId = currentStore?.Id,
|
CurrentStoreId = currentStore?.Id,
|
||||||
CurrentDisplayName = currentStore?.StoreName,
|
CurrentDisplayName = currentStore?.StoreName,
|
||||||
CurrentStoreIsOwner = currentStore?.Role == StoreRoles.Owner,
|
|
||||||
CurrentStoreLogoFileId = blob?.LogoFileId
|
CurrentStoreLogoFileId = blob?.LogoFileId
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -8,7 +8,6 @@ namespace BTCPayServer.Components.StoreSelector
|
|||||||
public string CurrentStoreId { get; set; }
|
public string CurrentStoreId { get; set; }
|
||||||
public string CurrentStoreLogoFileId { get; set; }
|
public string CurrentStoreLogoFileId { get; set; }
|
||||||
public string CurrentDisplayName { get; set; }
|
public string CurrentDisplayName { get; set; }
|
||||||
public bool CurrentStoreIsOwner { get; set; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public class StoreSelectorOption
|
public class StoreSelectorOption
|
||||||
|
@ -0,0 +1,42 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using BTCPayServer.Abstractions.Constants;
|
||||||
|
using BTCPayServer.Abstractions.Extensions;
|
||||||
|
using BTCPayServer.Client;
|
||||||
|
using BTCPayServer.Client.Models;
|
||||||
|
using BTCPayServer.Services.Stores;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Cors;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Controllers.Greenfield;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||||
|
[EnableCors(CorsPolicies.All)]
|
||||||
|
public class GreenfieldServerRolesController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly StoreRepository _storeRepository;
|
||||||
|
|
||||||
|
public GreenfieldServerRolesController(StoreRepository storeRepository)
|
||||||
|
{
|
||||||
|
_storeRepository = storeRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Authorize(Policy = Policies.CanModifyServerSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||||
|
[HttpGet("~/api/v1/server/roles")]
|
||||||
|
public async Task<IActionResult> GetServerRoles()
|
||||||
|
{
|
||||||
|
return Ok(FromModel(await _storeRepository.GetStoreRoles(null, false, false)));
|
||||||
|
}
|
||||||
|
private List<RoleData> FromModel(StoreRepository.StoreRole[] data)
|
||||||
|
{
|
||||||
|
return data.Select(r => new RoleData() {Role = r.Role, Id = r.Id, Permissions = r.Permissions, IsServerRole = true}).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private IActionResult StoreNotFound()
|
||||||
|
{
|
||||||
|
return this.CreateAPIError(404, "store-not-found", "The store was not found");
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,48 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using BTCPayServer.Abstractions.Constants;
|
||||||
|
using BTCPayServer.Abstractions.Extensions;
|
||||||
|
using BTCPayServer.Client;
|
||||||
|
using BTCPayServer.Client.Models;
|
||||||
|
using BTCPayServer.Services.Stores;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Cors;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Controllers.Greenfield
|
||||||
|
{
|
||||||
|
[ApiController]
|
||||||
|
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||||
|
[EnableCors(CorsPolicies.All)]
|
||||||
|
public class GreenfieldStoreRolesController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly StoreRepository _storeRepository;
|
||||||
|
|
||||||
|
public GreenfieldStoreRolesController(StoreRepository storeRepository)
|
||||||
|
{
|
||||||
|
_storeRepository = storeRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||||
|
[HttpGet("~/api/v1/stores/{storeId}/roles")]
|
||||||
|
public async Task<IActionResult> GetStoreRoles(string storeId)
|
||||||
|
{
|
||||||
|
var store = HttpContext.GetStoreData();
|
||||||
|
return store == null
|
||||||
|
? StoreNotFound()
|
||||||
|
: Ok(FromModel(await _storeRepository.GetStoreRoles(storeId, false, false)));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private List<RoleData> FromModel(StoreRepository.StoreRole[] data)
|
||||||
|
{
|
||||||
|
return data.Select(r => new RoleData() {Role = r.Role, Id = r.Id, Permissions = r.Permissions, IsServerRole = r.IsServerRole}).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private IActionResult StoreNotFound()
|
||||||
|
{
|
||||||
|
return this.CreateAPIError(404, "store-not-found", "The store was not found");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -63,8 +63,19 @@ namespace BTCPayServer.Controllers.Greenfield
|
|||||||
{
|
{
|
||||||
return StoreNotFound();
|
return StoreNotFound();
|
||||||
}
|
}
|
||||||
//we do not need to validate the role string as any value other than `StoreRoles.Owner` is currently treated like a guest
|
StoreRoleId roleId = null;
|
||||||
if (await _storeRepository.AddStoreUser(storeId, request.UserId, request.Role))
|
|
||||||
|
if (request.Role is not null)
|
||||||
|
{
|
||||||
|
roleId = await _storeRepository.ResolveStoreRoleId(storeId, request.Role);
|
||||||
|
if (roleId is null)
|
||||||
|
ModelState.AddModelError(nameof(request.Role), "The role id provided does not exist");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ModelState.IsValid)
|
||||||
|
return this.CreateValidationError(ModelState);
|
||||||
|
|
||||||
|
if (await _storeRepository.AddStoreUser(storeId, request.UserId, roleId))
|
||||||
{
|
{
|
||||||
return Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
@ -74,7 +85,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
|||||||
|
|
||||||
private IEnumerable<StoreUserData> FromModel(Data.StoreData data)
|
private IEnumerable<StoreUserData> FromModel(Data.StoreData data)
|
||||||
{
|
{
|
||||||
return data.UserStores.Select(store => new StoreUserData() { UserId = store.ApplicationUserId, Role = store.Role });
|
return data.UserStores.Select(store => new StoreUserData() { UserId = store.ApplicationUserId, Role = store.StoreRoleId });
|
||||||
}
|
}
|
||||||
private IActionResult StoreNotFound()
|
private IActionResult StoreNotFound()
|
||||||
{
|
{
|
||||||
|
@ -1319,5 +1319,27 @@ namespace BTCPayServer.Controllers.Greenfield
|
|||||||
{
|
{
|
||||||
return GetFromActionResult<CrowdfundAppData>(await GetController<GreenfieldAppsController>().GetCrowdfundApp(appId));
|
return GetFromActionResult<CrowdfundAppData>(await GetController<GreenfieldAppsController>().GetCrowdfundApp(appId));
|
||||||
}
|
}
|
||||||
|
public override async Task<PullPaymentData> RefundInvoice(string storeId, string invoiceId, RefundInvoiceRequest request, CancellationToken token = default)
|
||||||
|
{
|
||||||
|
return GetFromActionResult<PullPaymentData>(await GetController<GreenfieldInvoiceController>().RefundInvoice(storeId, invoiceId, request, token));
|
||||||
|
}
|
||||||
|
public override async Task RevokeAPIKey(string userId, string apikey, CancellationToken token = default)
|
||||||
|
{
|
||||||
|
HandleActionResult(await GetController<GreenfieldApiKeysController>().RevokeAPIKey(userId, apikey));
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<ApiKeyData> CreateAPIKey(string userId, CreateApiKeyRequest request, CancellationToken token = default)
|
||||||
|
{
|
||||||
|
return GetFromActionResult<ApiKeyData>(await GetController<GreenfieldApiKeysController>().CreateUserAPIKey(userId, request));
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<List<RoleData>> GetServerRoles(CancellationToken token = default)
|
||||||
|
{
|
||||||
|
return GetFromActionResult<List<RoleData>>(await GetController<GreenfieldServerRolesController>().GetServerRoles());
|
||||||
|
}
|
||||||
|
public override async Task<List<RoleData>> GetStoreRoles(string storeId, CancellationToken token = default)
|
||||||
|
{
|
||||||
|
return GetFromActionResult<List<RoleData>>(await GetController<GreenfieldStoreRolesController>().GetStoreRoles(storeId));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
@ -9,30 +8,18 @@ using BTCPayServer.Abstractions.Constants;
|
|||||||
using BTCPayServer.Abstractions.Contracts;
|
using BTCPayServer.Abstractions.Contracts;
|
||||||
using BTCPayServer.Abstractions.Extensions;
|
using BTCPayServer.Abstractions.Extensions;
|
||||||
using BTCPayServer.Client;
|
using BTCPayServer.Client;
|
||||||
using BTCPayServer.Components.StoreSelector;
|
|
||||||
using BTCPayServer.Data;
|
using BTCPayServer.Data;
|
||||||
using BTCPayServer.Filters;
|
using BTCPayServer.Filters;
|
||||||
using BTCPayServer.HostedServices;
|
|
||||||
using BTCPayServer.Models;
|
using BTCPayServer.Models;
|
||||||
using BTCPayServer.Models.StoreViewModels;
|
using BTCPayServer.Models.StoreViewModels;
|
||||||
using BTCPayServer.Payments;
|
|
||||||
using BTCPayServer.Payments.Lightning;
|
|
||||||
using BTCPayServer.Security;
|
|
||||||
using BTCPayServer.Services;
|
using BTCPayServer.Services;
|
||||||
using BTCPayServer.Services.Apps;
|
|
||||||
using BTCPayServer.Services.Stores;
|
using BTCPayServer.Services.Stores;
|
||||||
using ExchangeSharp;
|
|
||||||
using Google.Apis.Auth.OAuth2;
|
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Hosting;
|
using Microsoft.AspNetCore.Hosting;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.Extensions.FileProviders;
|
using Microsoft.Extensions.FileProviders;
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using NBitcoin;
|
|
||||||
using NBitcoin.Payment;
|
|
||||||
using NBitpayClient;
|
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
@ -92,13 +79,13 @@ namespace BTCPayServer.Controllers
|
|||||||
var store = await _storeRepository.FindStore(storeId, userId);
|
var store = await _storeRepository.FindStore(storeId, userId);
|
||||||
if (store != null)
|
if (store != null)
|
||||||
{
|
{
|
||||||
return RedirectToStore(store);
|
return RedirectToStore(userId, store);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var stores = await _storeRepository.GetStoresByUserId(userId);
|
var stores = await _storeRepository.GetStoresByUserId(userId);
|
||||||
return stores.Any()
|
return stores.Any()
|
||||||
? RedirectToStore(stores.First())
|
? RedirectToStore(userId, stores.First())
|
||||||
: RedirectToAction(nameof(UIUserStoresController.CreateStore), "UIUserStores");
|
: RedirectToAction(nameof(UIUserStoresController.CreateStore), "UIUserStores");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -211,9 +198,9 @@ namespace BTCPayServer.Controllers
|
|||||||
return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
|
return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
|
||||||
}
|
}
|
||||||
|
|
||||||
public RedirectToActionResult RedirectToStore(StoreData store)
|
public RedirectToActionResult RedirectToStore(string userId, StoreData store)
|
||||||
{
|
{
|
||||||
return store.HasPermission(Policies.CanModifyStoreSettings)
|
return store.HasPermission(userId, Policies.CanModifyStoreSettings)
|
||||||
? RedirectToAction("Dashboard", "UIStores", new { storeId = store.Id })
|
? RedirectToAction("Dashboard", "UIStores", new { storeId = store.Id })
|
||||||
: RedirectToAction("ListInvoices", "UIInvoice", new { storeId = store.Id });
|
: RedirectToAction("ListInvoices", "UIInvoice", new { storeId = store.Id });
|
||||||
}
|
}
|
||||||
|
@ -638,7 +638,7 @@ namespace BTCPayServer.Controllers
|
|||||||
}
|
}
|
||||||
if (explorer is null)
|
if (explorer is null)
|
||||||
return NotSupported("This feature is only available to BTC wallets");
|
return NotSupported("This feature is only available to BTC wallets");
|
||||||
if (this.GetCurrentStore().Role != StoreRoles.Owner)
|
if (!GetCurrentStore().HasPermission(GetUserId(), Policies.CanModifyStoreSettings))
|
||||||
return Forbid();
|
return Forbid();
|
||||||
|
|
||||||
var derivationScheme = (this.GetCurrentStore().GetDerivationSchemeSettings(_NetworkProvider, network.CryptoCode))?.AccountDerivation;
|
var derivationScheme = (this.GetCurrentStore().GetDerivationSchemeSettings(_NetworkProvider, network.CryptoCode))?.AccountDerivation;
|
||||||
|
185
BTCPayServer/Controllers/UIServerController.Roles.cs
Normal file
185
BTCPayServer/Controllers/UIServerController.Roles.cs
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Amazon.S3.Transfer;
|
||||||
|
using BTCPayServer.Abstractions.Constants;
|
||||||
|
using BTCPayServer.Abstractions.Extensions;
|
||||||
|
using BTCPayServer.Abstractions.Models;
|
||||||
|
using BTCPayServer.Models.ServerViewModels;
|
||||||
|
using BTCPayServer.Services.Stores;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Controllers
|
||||||
|
{
|
||||||
|
public partial class UIServerController
|
||||||
|
{
|
||||||
|
[Route("server/roles")]
|
||||||
|
public async Task<IActionResult> ListRoles(
|
||||||
|
[FromServices] StoreRepository storeRepository,
|
||||||
|
RolesViewModel model,
|
||||||
|
string sortOrder = null
|
||||||
|
)
|
||||||
|
{
|
||||||
|
model ??= new RolesViewModel();
|
||||||
|
|
||||||
|
model.DefaultRole = (await storeRepository.GetDefaultRole()).Role;
|
||||||
|
var roles = await storeRepository.GetStoreRoles(null);
|
||||||
|
|
||||||
|
if (sortOrder != null)
|
||||||
|
{
|
||||||
|
switch (sortOrder)
|
||||||
|
{
|
||||||
|
case "desc":
|
||||||
|
ViewData["NextRoleSortOrder"] = "asc";
|
||||||
|
roles = roles.OrderByDescending(user => user.Role).ToArray();
|
||||||
|
break;
|
||||||
|
case "asc":
|
||||||
|
roles = roles.OrderBy(user => user.Role).ToArray();
|
||||||
|
ViewData["NextRoleSortOrder"] = "desc";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
model.Roles = roles.Skip(model.Skip).Take(model.Count).ToList();
|
||||||
|
|
||||||
|
return View(model);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("server/roles/{role}")]
|
||||||
|
public async Task<IActionResult> CreateOrEditRole(
|
||||||
|
[FromServices] StoreRepository storeRepository,
|
||||||
|
string role)
|
||||||
|
{
|
||||||
|
if (role == "create")
|
||||||
|
{
|
||||||
|
ModelState.Remove(nameof(role));
|
||||||
|
return View(new UpdateRoleViewModel());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var roleData = await storeRepository.GetStoreRole(new StoreRoleId(role));
|
||||||
|
if (roleData == null)
|
||||||
|
return NotFound();
|
||||||
|
return View(new UpdateRoleViewModel()
|
||||||
|
{
|
||||||
|
Policies = roleData.Permissions,
|
||||||
|
Role = roleData.Role
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
[HttpPost("server/roles/{role}")]
|
||||||
|
public async Task<IActionResult> CreateOrEditRole(
|
||||||
|
|
||||||
|
[FromServices] StoreRepository storeRepository,
|
||||||
|
[FromRoute] string role, UpdateRoleViewModel viewModel)
|
||||||
|
{
|
||||||
|
string successMessage = null;
|
||||||
|
if (role == "create")
|
||||||
|
{
|
||||||
|
successMessage = "Role created";
|
||||||
|
role = viewModel.Role;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
successMessage = "Role updated";
|
||||||
|
var storeRole = await storeRepository.GetStoreRole(new StoreRoleId(role));
|
||||||
|
if (storeRole == null)
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ModelState.IsValid)
|
||||||
|
{
|
||||||
|
return View(viewModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
var r = await storeRepository.AddOrUpdateStoreRole(new StoreRoleId(role), viewModel.Policies);
|
||||||
|
if (r is null)
|
||||||
|
{
|
||||||
|
TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||||
|
{
|
||||||
|
Severity = StatusMessageModel.StatusSeverity.Error,
|
||||||
|
Message = "Role could not be updated"
|
||||||
|
});
|
||||||
|
return View(viewModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||||
|
{
|
||||||
|
Severity = StatusMessageModel.StatusSeverity.Success,
|
||||||
|
Message = successMessage
|
||||||
|
});
|
||||||
|
|
||||||
|
return RedirectToAction(nameof(ListRoles));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[HttpGet("server/roles/{role}/delete")]
|
||||||
|
public async Task<IActionResult> DeleteRole(
|
||||||
|
[FromServices] StoreRepository storeRepository,
|
||||||
|
string role)
|
||||||
|
{
|
||||||
|
var roleData = await storeRepository.GetStoreRole(new StoreRoleId(role), true);
|
||||||
|
if (roleData == null)
|
||||||
|
return NotFound();
|
||||||
|
|
||||||
|
return View("Confirm",
|
||||||
|
roleData.IsUsed is true
|
||||||
|
? new ConfirmModel("Delete role",
|
||||||
|
$"Unable to proceed: The role <strong>{Html.Encode(roleData.Role)}</strong> is currently assigned to one or more users, it cannot be removed.")
|
||||||
|
: new ConfirmModel("Delete role",
|
||||||
|
$"The role <strong>{Html.Encode(roleData.Role)}</strong> will be permanently deleted. Are you sure?",
|
||||||
|
"Delete"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("server/roles/{role}/delete")]
|
||||||
|
public async Task<IActionResult> DeleteRolePost(
|
||||||
|
[FromServices] StoreRepository storeRepository,
|
||||||
|
string role)
|
||||||
|
{
|
||||||
|
var roleId = new StoreRoleId(role);
|
||||||
|
var roleData = await storeRepository.GetStoreRole(roleId, true);
|
||||||
|
if (roleData == null)
|
||||||
|
return NotFound();
|
||||||
|
if (roleData.IsUsed is true)
|
||||||
|
{
|
||||||
|
return BadRequest();
|
||||||
|
}
|
||||||
|
|
||||||
|
var errorMessage = await storeRepository.RemoveStoreRole(roleId);
|
||||||
|
if (errorMessage is null)
|
||||||
|
{
|
||||||
|
|
||||||
|
TempData[WellKnownTempData.SuccessMessage] = "Role deleted";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
TempData[WellKnownTempData.ErrorMessage] = errorMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
return RedirectToAction(nameof(ListRoles));
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("server/roles/{role}/default")]
|
||||||
|
public async Task<IActionResult> SetDefaultRole(
|
||||||
|
[FromServices] StoreRepository storeRepository,
|
||||||
|
string role)
|
||||||
|
{
|
||||||
|
await storeRepository.SetDefaultRole(role);
|
||||||
|
|
||||||
|
TempData[WellKnownTempData.SuccessMessage] = "Role set default";
|
||||||
|
|
||||||
|
return RedirectToAction(nameof(ListRoles));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public class UpdateRoleViewModel
|
||||||
|
{
|
||||||
|
[Required]
|
||||||
|
[Display(Name = "Role")]
|
||||||
|
public string Role { get; set; }
|
||||||
|
|
||||||
|
[Display(Name = "Policies")] public List<string> Policies { get; set; } = new();
|
||||||
|
}
|
164
BTCPayServer/Controllers/UIStoresController.Roles.cs
Normal file
164
BTCPayServer/Controllers/UIStoresController.Roles.cs
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.Data;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Amazon.S3.Transfer;
|
||||||
|
using BTCPayServer.Abstractions.Constants;
|
||||||
|
using BTCPayServer.Abstractions.Extensions;
|
||||||
|
using BTCPayServer.Abstractions.Models;
|
||||||
|
using BTCPayServer.Models.ServerViewModels;
|
||||||
|
using BTCPayServer.Services.Stores;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Controllers
|
||||||
|
{
|
||||||
|
public partial class UIStoresController
|
||||||
|
{
|
||||||
|
[Route("{storeId}/roles")]
|
||||||
|
public async Task<IActionResult> ListRoles(
|
||||||
|
string storeId,
|
||||||
|
[FromServices] StoreRepository storeRepository,
|
||||||
|
RolesViewModel model,
|
||||||
|
string sortOrder = null
|
||||||
|
)
|
||||||
|
{
|
||||||
|
model ??= new RolesViewModel();
|
||||||
|
|
||||||
|
model.DefaultRole = (await storeRepository.GetDefaultRole()).Role;
|
||||||
|
var roles = await storeRepository.GetStoreRoles(storeId, false, false);
|
||||||
|
|
||||||
|
if (sortOrder != null)
|
||||||
|
{
|
||||||
|
switch (sortOrder)
|
||||||
|
{
|
||||||
|
case "desc":
|
||||||
|
ViewData["NextRoleSortOrder"] = "asc";
|
||||||
|
roles = roles.OrderByDescending(user => user.Role).ToArray();
|
||||||
|
break;
|
||||||
|
case "asc":
|
||||||
|
roles = roles.OrderBy(user => user.Role).ToArray();
|
||||||
|
ViewData["NextRoleSortOrder"] = "desc";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
model.Roles = roles.Skip(model.Skip).Take(model.Count).ToList();
|
||||||
|
|
||||||
|
return View(model);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("{storeId}/roles/{role}")]
|
||||||
|
public async Task<IActionResult> CreateOrEditRole(
|
||||||
|
string storeId,
|
||||||
|
[FromServices] StoreRepository storeRepository,
|
||||||
|
string role)
|
||||||
|
{
|
||||||
|
if (role == "create")
|
||||||
|
{
|
||||||
|
ModelState.Remove(nameof(role));
|
||||||
|
return View(new UpdateRoleViewModel());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var roleData = await storeRepository.GetStoreRole(new StoreRoleId(storeId, role));
|
||||||
|
if (roleData == null)
|
||||||
|
return NotFound();
|
||||||
|
return View(new UpdateRoleViewModel()
|
||||||
|
{
|
||||||
|
Policies = roleData.Permissions,
|
||||||
|
Role = roleData.Role
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
[HttpPost("{storeId}/roles/{role}")]
|
||||||
|
public async Task<IActionResult> CreateOrEditRole(
|
||||||
|
string storeId,
|
||||||
|
[FromServices] StoreRepository storeRepository,
|
||||||
|
[FromRoute] string role, UpdateRoleViewModel viewModel)
|
||||||
|
{
|
||||||
|
string successMessage = null;
|
||||||
|
StoreRoleId roleId;
|
||||||
|
if (role == "create")
|
||||||
|
{
|
||||||
|
successMessage = "Role created";
|
||||||
|
role = viewModel.Role;
|
||||||
|
roleId = new StoreRoleId(storeId, role);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
successMessage = "Role updated";
|
||||||
|
roleId = new StoreRoleId(storeId, role);
|
||||||
|
var storeRole = await storeRepository.GetStoreRole(roleId);
|
||||||
|
if (storeRole == null)
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ModelState.IsValid)
|
||||||
|
{
|
||||||
|
return View(viewModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
var r = await storeRepository.AddOrUpdateStoreRole(roleId, viewModel.Policies);
|
||||||
|
if (r is null)
|
||||||
|
{
|
||||||
|
TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||||
|
{
|
||||||
|
Severity = StatusMessageModel.StatusSeverity.Error,
|
||||||
|
Message = "Role could not be updated"
|
||||||
|
});
|
||||||
|
return View(viewModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||||
|
{
|
||||||
|
Severity = StatusMessageModel.StatusSeverity.Success,
|
||||||
|
Message = successMessage
|
||||||
|
});
|
||||||
|
|
||||||
|
return RedirectToAction(nameof(ListRoles), new { storeId });
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[HttpGet("{storeId}/roles/{role}/delete")]
|
||||||
|
public async Task<IActionResult> DeleteRole(
|
||||||
|
string storeId,
|
||||||
|
[FromServices] StoreRepository storeRepository,
|
||||||
|
string role)
|
||||||
|
{
|
||||||
|
var roleData = await storeRepository.GetStoreRole(new StoreRoleId(storeId, role), true);;
|
||||||
|
if (roleData == null)
|
||||||
|
return NotFound();
|
||||||
|
|
||||||
|
return View("Confirm",
|
||||||
|
roleData.IsUsed is true
|
||||||
|
? new ConfirmModel("Delete role",
|
||||||
|
$"Unable to proceed: The role <strong>{Html.Encode(roleData.Role)}</strong> is currently assigned to one or more users, it cannot be removed.")
|
||||||
|
: new ConfirmModel("Delete role",
|
||||||
|
$"The role <strong>{Html.Encode(roleData.Role)}</strong> will be permanently deleted. Are you sure?",
|
||||||
|
"Delete"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("{storeId}/roles/{roleId}/delete")]
|
||||||
|
public async Task<IActionResult> DeleteRolePost(
|
||||||
|
string storeId,
|
||||||
|
[FromServices] StoreRepository storeRepository,
|
||||||
|
string role)
|
||||||
|
{
|
||||||
|
var roleId = new StoreRoleId(storeId, role);
|
||||||
|
var roleData = await storeRepository.GetStoreRole(roleId, true);
|
||||||
|
if (roleData == null)
|
||||||
|
return NotFound();
|
||||||
|
if (roleData.IsUsed is true)
|
||||||
|
{
|
||||||
|
return BadRequest();
|
||||||
|
}
|
||||||
|
await storeRepository.RemoveStoreRole(roleId);
|
||||||
|
|
||||||
|
TempData[WellKnownTempData.SuccessMessage] = "Role deleted";
|
||||||
|
return RedirectToAction(nameof(ListRoles), new { storeId });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -130,6 +130,7 @@ namespace BTCPayServer.Controllers
|
|||||||
public async Task<IActionResult> StoreUsers()
|
public async Task<IActionResult> StoreUsers()
|
||||||
{
|
{
|
||||||
StoreUsersViewModel vm = new StoreUsersViewModel();
|
StoreUsersViewModel vm = new StoreUsersViewModel();
|
||||||
|
vm.Role = StoreRoleId.Guest.Role;
|
||||||
await FillUsers(vm);
|
await FillUsers(vm);
|
||||||
return View(vm);
|
return View(vm);
|
||||||
}
|
}
|
||||||
@ -142,7 +143,7 @@ namespace BTCPayServer.Controllers
|
|||||||
{
|
{
|
||||||
Email = u.Email,
|
Email = u.Email,
|
||||||
Id = u.Id,
|
Id = u.Id,
|
||||||
Role = u.Role
|
Role = u.StoreRole.Role
|
||||||
}).ToList();
|
}).ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -150,7 +151,7 @@ namespace BTCPayServer.Controllers
|
|||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
[Route("{storeId}/users")]
|
[Route("{storeId}/users")]
|
||||||
public async Task<IActionResult> StoreUsers(StoreUsersViewModel vm)
|
public async Task<IActionResult> StoreUsers(string storeId, StoreUsersViewModel vm)
|
||||||
{
|
{
|
||||||
await FillUsers(vm);
|
await FillUsers(vm);
|
||||||
if (!ModelState.IsValid)
|
if (!ModelState.IsValid)
|
||||||
@ -163,12 +164,16 @@ namespace BTCPayServer.Controllers
|
|||||||
ModelState.AddModelError(nameof(vm.Email), "User not found");
|
ModelState.AddModelError(nameof(vm.Email), "User not found");
|
||||||
return View(vm);
|
return View(vm);
|
||||||
}
|
}
|
||||||
if (!StoreRoles.AllRoles.Contains(vm.Role))
|
|
||||||
|
var roles = await _Repo.GetStoreRoles(CurrentStore.Id);
|
||||||
|
if (roles.All(role => role.Id != vm.Role))
|
||||||
{
|
{
|
||||||
ModelState.AddModelError(nameof(vm.Role), "Invalid role");
|
ModelState.AddModelError(nameof(vm.Role), "Invalid role");
|
||||||
return View(vm);
|
return View(vm);
|
||||||
}
|
}
|
||||||
if (!await _Repo.AddStoreUser(CurrentStore.Id, user.Id, vm.Role))
|
var roleId = await _Repo.ResolveStoreRoleId(storeId, vm.Role);
|
||||||
|
|
||||||
|
if (!await _Repo.AddStoreUser(CurrentStore.Id, user.Id, roleId))
|
||||||
{
|
{
|
||||||
ModelState.AddModelError(nameof(vm.Email), "The user already has access to this store");
|
ModelState.AddModelError(nameof(vm.Email), "The user already has access to this store");
|
||||||
return View(vm);
|
return View(vm);
|
||||||
@ -938,8 +943,9 @@ namespace BTCPayServer.Controllers
|
|||||||
ViewBag.HidePublicKey = true;
|
ViewBag.HidePublicKey = true;
|
||||||
ViewBag.ShowStores = true;
|
ViewBag.ShowStores = true;
|
||||||
ViewBag.ShowMenu = false;
|
ViewBag.ShowMenu = false;
|
||||||
var stores = await _Repo.GetStoresByUserId(userId);
|
var stores = (await _Repo.GetStoresByUserId(userId)).Where(data => data.HasPermission(userId, Policies.CanModifyStoreSettings)).ToArray();
|
||||||
model.Stores = new SelectList(stores.Where(s => s.Role == StoreRoles.Owner), nameof(CurrentStore.Id), nameof(CurrentStore.StoreName));
|
|
||||||
|
model.Stores = new SelectList(stores, nameof(CurrentStore.Id), nameof(CurrentStore.StoreName));
|
||||||
if (!model.Stores.Any())
|
if (!model.Stores.Any())
|
||||||
{
|
{
|
||||||
TempData[WellKnownTempData.ErrorMessage] = "You need to be owner of at least one store before pairing";
|
TempData[WellKnownTempData.ErrorMessage] = "You need to be owner of at least one store before pairing";
|
||||||
@ -1004,14 +1010,14 @@ namespace BTCPayServer.Controllers
|
|||||||
return RedirectToAction(nameof(UIHomeController.Index), "UIHome");
|
return RedirectToAction(nameof(UIHomeController.Index), "UIHome");
|
||||||
}
|
}
|
||||||
|
|
||||||
var stores = await _Repo.GetStoresByUserId(userId);
|
var stores = (await _Repo.GetStoresByUserId(userId)).Where(data => data.HasPermission(userId, Policies.CanModifyStoreSettings)).ToArray();
|
||||||
return View(new PairingModel
|
return View(new PairingModel
|
||||||
{
|
{
|
||||||
Id = pairing.Id,
|
Id = pairing.Id,
|
||||||
Label = pairing.Label,
|
Label = pairing.Label,
|
||||||
SIN = pairing.SIN ?? "Server-Initiated Pairing",
|
SIN = pairing.SIN ?? "Server-Initiated Pairing",
|
||||||
StoreId = selectedStore ?? stores.FirstOrDefault()?.Id,
|
StoreId = selectedStore ?? stores.FirstOrDefault()?.Id,
|
||||||
Stores = stores.Where(u => u.Role == StoreRoles.Owner).Select(s => new PairingModel.StoreViewModel
|
Stores = stores.Select(s => new PairingModel.StoreViewModel
|
||||||
{
|
{
|
||||||
Id = s.Id,
|
Id = s.Id,
|
||||||
Name = string.IsNullOrEmpty(s.StoreName) ? s.Id : s.StoreName
|
Name = string.IsNullOrEmpty(s.StoreName) ? s.Id : s.StoreName
|
||||||
|
@ -189,11 +189,7 @@ namespace BTCPayServer.Controllers
|
|||||||
ListWalletsViewModel.WalletViewModel walletVm = new ListWalletsViewModel.WalletViewModel();
|
ListWalletsViewModel.WalletViewModel walletVm = new ListWalletsViewModel.WalletViewModel();
|
||||||
wallets.Wallets.Add(walletVm);
|
wallets.Wallets.Add(walletVm);
|
||||||
walletVm.Balance = await wallet.Balance + " " + wallet.Wallet.Network.CryptoCode;
|
walletVm.Balance = await wallet.Balance + " " + wallet.Wallet.Network.CryptoCode;
|
||||||
walletVm.IsOwner = wallet.Store.Role == StoreRoles.Owner;
|
|
||||||
if (!walletVm.IsOwner)
|
|
||||||
{
|
|
||||||
walletVm.Balance = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
walletVm.CryptoCode = wallet.Network.CryptoCode;
|
walletVm.CryptoCode = wallet.Network.CryptoCode;
|
||||||
walletVm.StoreId = wallet.Store.Id;
|
walletVm.StoreId = wallet.Store.Id;
|
||||||
|
@ -74,6 +74,7 @@ namespace BTCPayServer.Data.Payouts.LightningLike
|
|||||||
.Include(data => data.PullPaymentData)
|
.Include(data => data.PullPaymentData)
|
||||||
.ThenInclude(data => data.StoreData)
|
.ThenInclude(data => data.StoreData)
|
||||||
.ThenInclude(data => data.UserStores)
|
.ThenInclude(data => data.UserStores)
|
||||||
|
.ThenInclude(data => data.StoreRole)
|
||||||
.Where(data =>
|
.Where(data =>
|
||||||
payoutIds.Contains(data.Id) &&
|
payoutIds.Contains(data.Id) &&
|
||||||
data.State == PayoutState.AwaitingPayment &&
|
data.State == PayoutState.AwaitingPayment &&
|
||||||
@ -84,7 +85,7 @@ namespace BTCPayServer.Data.Payouts.LightningLike
|
|||||||
if (approvedStores.TryGetValue(payout.PullPaymentData.StoreId, out var value))
|
if (approvedStores.TryGetValue(payout.PullPaymentData.StoreId, out var value))
|
||||||
return value;
|
return value;
|
||||||
value = payout.PullPaymentData.StoreData.UserStores
|
value = payout.PullPaymentData.StoreData.UserStores
|
||||||
.Any(store => store.Role == StoreRoles.Owner && store.ApplicationUserId == userId);
|
.Any(store => store.ApplicationUserId == userId && store.StoreRole.Permissions.Contains(Policies.CanModifyStoreSettings));
|
||||||
approvedStores.Add(payout.PullPaymentData.StoreId, value);
|
approvedStores.Add(payout.PullPaymentData.StoreId, value);
|
||||||
return value;
|
return value;
|
||||||
}).ToList();
|
}).ToList();
|
||||||
|
@ -16,32 +16,6 @@ namespace BTCPayServer.Data
|
|||||||
{
|
{
|
||||||
public static class StoreDataExtensions
|
public static class StoreDataExtensions
|
||||||
{
|
{
|
||||||
public static PermissionSet GetPermissionSet(this StoreData store)
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(store);
|
|
||||||
if (store.Role is null)
|
|
||||||
return new PermissionSet();
|
|
||||||
return new PermissionSet(store.Role == StoreRoles.Owner
|
|
||||||
? new[]
|
|
||||||
{
|
|
||||||
Permission.Create(Policies.CanModifyStoreSettings, store.Id),
|
|
||||||
Permission.Create(Policies.CanTradeCustodianAccount, store.Id),
|
|
||||||
Permission.Create(Policies.CanWithdrawFromCustodianAccounts, store.Id),
|
|
||||||
Permission.Create(Policies.CanDepositToCustodianAccounts, store.Id)
|
|
||||||
}
|
|
||||||
: new[]
|
|
||||||
{
|
|
||||||
Permission.Create(Policies.CanViewStoreSettings, store.Id),
|
|
||||||
Permission.Create(Policies.CanModifyInvoices, store.Id),
|
|
||||||
Permission.Create(Policies.CanViewCustodianAccounts, store.Id),
|
|
||||||
Permission.Create(Policies.CanDepositToCustodianAccounts, store.Id)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
public static bool HasPermission(this StoreData store, string permission)
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(store);
|
|
||||||
return store.GetPermissionSet().Contains(permission, store.Id);
|
|
||||||
}
|
|
||||||
|
|
||||||
#pragma warning disable CS0618
|
#pragma warning disable CS0618
|
||||||
public static PaymentMethodId? GetDefaultPaymentId(this StoreData storeData)
|
public static PaymentMethodId? GetDefaultPaymentId(this StoreData storeData)
|
||||||
|
@ -1,11 +1,40 @@
|
|||||||
#nullable enable
|
#nullable enable
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using BTCPayServer.Client;
|
||||||
using BTCPayServer.Data;
|
using BTCPayServer.Data;
|
||||||
|
|
||||||
namespace BTCPayServer
|
namespace BTCPayServer
|
||||||
{
|
{
|
||||||
public static class StoreExtensions
|
public static class StoreExtensions
|
||||||
{
|
{
|
||||||
|
public static StoreRole? GetStoreRoleOfUser(this StoreData store, string userId)
|
||||||
|
{
|
||||||
|
return store.UserStores.FirstOrDefault(r => r.ApplicationUserId == userId)?.StoreRole;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static PermissionSet GetPermissionSet(this StoreRole storeRole, string storeId)
|
||||||
|
{
|
||||||
|
return new PermissionSet(storeRole.Permissions
|
||||||
|
.Select(s => Permission.TryCreatePermission(s, storeId, out var permission) ? permission : null)
|
||||||
|
.Where(s => s != null).ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static PermissionSet GetPermissionSet(this StoreData store, string userId)
|
||||||
|
{
|
||||||
|
return store.GetStoreRoleOfUser(userId)?.GetPermissionSet(store.Id)?? new PermissionSet();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool HasPermission(this StoreData store, string userId, string permission)
|
||||||
|
{
|
||||||
|
return GetPermissionSet(store, userId).HasPermission(permission, store.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool HasPermission(this PermissionSet permissionSet, string permission, string storeId)
|
||||||
|
{
|
||||||
|
return permissionSet.Contains(permission, storeId);
|
||||||
|
}
|
||||||
|
|
||||||
public static DerivationSchemeSettings? GetDerivationSchemeSettings(this StoreData store, BTCPayNetworkProvider networkProvider, string cryptoCode)
|
public static DerivationSchemeSettings? GetDerivationSchemeSettings(this StoreData store, BTCPayNetworkProvider networkProvider, string cryptoCode)
|
||||||
{
|
{
|
||||||
var paymentMethod = store
|
var paymentMethod = store
|
||||||
|
@ -6,12 +6,12 @@ using System.Linq;
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using BTCPayServer.Abstractions.Contracts;
|
using BTCPayServer.Abstractions.Contracts;
|
||||||
|
using BTCPayServer.Client;
|
||||||
using BTCPayServer.Client.Models;
|
using BTCPayServer.Client.Models;
|
||||||
using BTCPayServer.Configuration;
|
using BTCPayServer.Configuration;
|
||||||
using BTCPayServer.Data;
|
using BTCPayServer.Data;
|
||||||
using BTCPayServer.Fido2;
|
using BTCPayServer.Fido2;
|
||||||
using BTCPayServer.Fido2.Models;
|
using BTCPayServer.Fido2.Models;
|
||||||
using BTCPayServer.Logging;
|
|
||||||
using BTCPayServer.Payments;
|
using BTCPayServer.Payments;
|
||||||
using BTCPayServer.Payments.Lightning;
|
using BTCPayServer.Payments.Lightning;
|
||||||
using BTCPayServer.Plugins.Crowdfund;
|
using BTCPayServer.Plugins.Crowdfund;
|
||||||
@ -40,7 +40,6 @@ namespace BTCPayServer.Hosting
|
|||||||
{
|
{
|
||||||
public class MigrationStartupTask : IStartupTask
|
public class MigrationStartupTask : IStartupTask
|
||||||
{
|
{
|
||||||
public Logs Logs { get; }
|
|
||||||
|
|
||||||
private readonly ApplicationDbContextFactory _DBContextFactory;
|
private readonly ApplicationDbContextFactory _DBContextFactory;
|
||||||
private readonly StoreRepository _StoreRepository;
|
private readonly StoreRepository _StoreRepository;
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using BTCPayServer.Data;
|
using BTCPayServer.Data;
|
||||||
|
using BTCPayServer.Services.Stores;
|
||||||
|
|
||||||
|
|
||||||
namespace BTCPayServer.Models.AppViewModels
|
namespace BTCPayServer.Models.AppViewModels
|
||||||
@ -14,12 +15,12 @@ namespace BTCPayServer.Models.AppViewModels
|
|||||||
public string AppName { get; set; }
|
public string AppName { get; set; }
|
||||||
public string AppType { get; set; }
|
public string AppType { get; set; }
|
||||||
public string ViewStyle { get; set; }
|
public string ViewStyle { get; set; }
|
||||||
public bool IsOwner { get; set; }
|
|
||||||
|
|
||||||
public string UpdateAction { get { return "Update" + AppType; } }
|
public string UpdateAction { get { return "Update" + AppType; } }
|
||||||
public string ViewAction { get { return "View" + AppType; } }
|
public string ViewAction { get { return "View" + AppType; } }
|
||||||
public DateTimeOffset Created { get; set; }
|
public DateTimeOffset Created { get; set; }
|
||||||
public AppData App { get; set; }
|
public AppData App { get; set; }
|
||||||
|
public StoreRepository.StoreRole Role { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public ListAppViewModel[] Apps { get; set; }
|
public ListAppViewModel[] Apps { get; set; }
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using BTCPayServer.Services.Stores;
|
||||||
|
|
||||||
namespace BTCPayServer.Models.ServerViewModels
|
namespace BTCPayServer.Models.ServerViewModels
|
||||||
{
|
{
|
||||||
@ -20,5 +21,11 @@ namespace BTCPayServer.Models.ServerViewModels
|
|||||||
public override int CurrentPageCount => Users.Count;
|
public override int CurrentPageCount => Users.Count;
|
||||||
public Dictionary<string, string> Roles { get; set; }
|
public Dictionary<string, string> Roles { get; set; }
|
||||||
}
|
}
|
||||||
|
public class RolesViewModel : BasePagingViewModel
|
||||||
|
{
|
||||||
|
public List<StoreRepository.StoreRole> Roles { get; set; } = new List<StoreRepository.StoreRole>();
|
||||||
|
public string DefaultRole { get; set; }
|
||||||
|
public override int CurrentPageCount => Roles.Count;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -11,10 +11,6 @@ namespace BTCPayServer.Models.StoreViewModels
|
|||||||
public string Role { get; set; }
|
public string Role { get; set; }
|
||||||
public string Id { get; set; }
|
public string Id { get; set; }
|
||||||
}
|
}
|
||||||
public StoreUsersViewModel()
|
|
||||||
{
|
|
||||||
Role = StoreRoles.Guest;
|
|
||||||
}
|
|
||||||
[Required]
|
[Required]
|
||||||
[EmailAddress]
|
[EmailAddress]
|
||||||
public string Email { get; set; }
|
public string Email { get; set; }
|
||||||
|
@ -12,7 +12,6 @@ namespace BTCPayServer.Models.WalletViewModels
|
|||||||
public string StoreId { get; set; }
|
public string StoreId { get; set; }
|
||||||
public string CryptoCode { get; set; }
|
public string CryptoCode { get; set; }
|
||||||
public string Balance { get; set; }
|
public string Balance { get; set; }
|
||||||
public bool IsOwner { get; set; }
|
|
||||||
public WalletId Id { get; set; }
|
public WalletId Id { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,6 +3,7 @@ using System.Collections.Generic;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using BTCPayServer.Abstractions.Contracts;
|
using BTCPayServer.Abstractions.Contracts;
|
||||||
|
using BTCPayServer.Client;
|
||||||
using BTCPayServer.Client.Models;
|
using BTCPayServer.Client.Models;
|
||||||
using BTCPayServer.Configuration;
|
using BTCPayServer.Configuration;
|
||||||
using BTCPayServer.Data;
|
using BTCPayServer.Data;
|
||||||
@ -57,7 +58,7 @@ public class LightningAutomatedPayoutProcessor : BaseAutomatedPayoutProcessor<Au
|
|||||||
var lightningSupportedPaymentMethod = (LightningSupportedPaymentMethod)paymentMethod;
|
var lightningSupportedPaymentMethod = (LightningSupportedPaymentMethod)paymentMethod;
|
||||||
if (lightningSupportedPaymentMethod.IsInternalNode &&
|
if (lightningSupportedPaymentMethod.IsInternalNode &&
|
||||||
!(await Task.WhenAll((await _storeRepository.GetStoreUsers(_PayoutProcesserSettings.StoreId))
|
!(await Task.WhenAll((await _storeRepository.GetStoreUsers(_PayoutProcesserSettings.StoreId))
|
||||||
.Where(user => user.Role == StoreRoles.Owner).Select(user => user.Id)
|
.Where(user => user.StoreRole.ToPermissionSet( _PayoutProcesserSettings.StoreId).Contains(Policies.CanModifyStoreSettings, _PayoutProcesserSettings.StoreId)).Select(user => user.Id)
|
||||||
.Select(s => _userService.IsAdminUser(s)))).Any(b => b))
|
.Select(s => _userService.IsAdminUser(s)))).Any(b => b))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
|
@ -141,7 +141,7 @@ namespace BTCPayServer.Security
|
|||||||
{
|
{
|
||||||
if (store is not null)
|
if (store is not null)
|
||||||
{
|
{
|
||||||
if (store.HasPermission(policy))
|
if (store.HasPermission(userId,policy))
|
||||||
{
|
{
|
||||||
success = true;
|
success = true;
|
||||||
}
|
}
|
||||||
|
@ -98,7 +98,7 @@ namespace BTCPayServer.Security.Greenfield
|
|||||||
var store = await _storeRepository.FindStore(storeId, userid);
|
var store = await _storeRepository.FindStore(storeId, userid);
|
||||||
if (store == null)
|
if (store == null)
|
||||||
break;
|
break;
|
||||||
if (!store.HasPermission(policy))
|
if (!store.HasPermission(userid, policy))
|
||||||
break;
|
break;
|
||||||
success = true;
|
success = true;
|
||||||
_httpContext.SetStoreData(store);
|
_httpContext.SetStoreData(store);
|
||||||
|
@ -5,6 +5,7 @@ using System.Globalization;
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using BTCPayServer.Client;
|
||||||
using BTCPayServer.Data;
|
using BTCPayServer.Data;
|
||||||
using BTCPayServer.Models.AppViewModels;
|
using BTCPayServer.Models.AppViewModels;
|
||||||
using BTCPayServer.Plugins.Crowdfund;
|
using BTCPayServer.Plugins.Crowdfund;
|
||||||
@ -236,15 +237,6 @@ namespace BTCPayServer.Services.Apps
|
|||||||
return invoices;
|
return invoices;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<StoreData[]> GetOwnedStores(string userId)
|
|
||||||
{
|
|
||||||
await using var ctx = _ContextFactory.CreateContext();
|
|
||||||
return await ctx.UserStore
|
|
||||||
.Where(us => us.ApplicationUserId == userId && us.Role == StoreRoles.Owner)
|
|
||||||
.Select(u => u.StoreData)
|
|
||||||
.ToArrayAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<bool> DeleteApp(AppData appData)
|
public async Task<bool> DeleteApp(AppData appData)
|
||||||
{
|
{
|
||||||
await using var ctx = _ContextFactory.CreateContext();
|
await using var ctx = _ContextFactory.CreateContext();
|
||||||
@ -256,25 +248,25 @@ namespace BTCPayServer.Services.Apps
|
|||||||
public async Task<ListAppsViewModel.ListAppViewModel[]> GetAllApps(string? userId, bool allowNoUser = false, string? storeId = null)
|
public async Task<ListAppsViewModel.ListAppViewModel[]> GetAllApps(string? userId, bool allowNoUser = false, string? storeId = null)
|
||||||
{
|
{
|
||||||
await using var ctx = _ContextFactory.CreateContext();
|
await using var ctx = _ContextFactory.CreateContext();
|
||||||
var listApps = await ctx.UserStore
|
var listApps = (await ctx.UserStore
|
||||||
.Where(us =>
|
.Where(us =>
|
||||||
(allowNoUser && string.IsNullOrEmpty(userId) || us.ApplicationUserId == userId) &&
|
(allowNoUser && string.IsNullOrEmpty(userId) || us.ApplicationUserId == userId) &&
|
||||||
(storeId == null || us.StoreDataId == storeId))
|
(storeId == null || us.StoreDataId == storeId))
|
||||||
.Join(ctx.Apps, us => us.StoreDataId, app => app.StoreDataId,
|
.Include(store => store.StoreRole)
|
||||||
(us, app) =>
|
.Include(store => store.StoreData)
|
||||||
new ListAppsViewModel.ListAppViewModel
|
.Join(ctx.Apps, us => us.StoreDataId, app => app.StoreDataId, (us, app) => new { us, app })
|
||||||
{
|
.OrderBy(b => b.app.Created)
|
||||||
IsOwner = us.Role == StoreRoles.Owner,
|
.ToArrayAsync()).Select(arg => new ListAppsViewModel.ListAppViewModel
|
||||||
StoreId = us.StoreDataId,
|
{
|
||||||
StoreName = us.StoreData.StoreName,
|
Role = StoreRepository.ToStoreRole(arg.us.StoreRole),
|
||||||
AppName = app.Name,
|
StoreId = arg.us.StoreDataId,
|
||||||
AppType = app.AppType,
|
StoreName = arg.us.StoreData.StoreName,
|
||||||
Id = app.Id,
|
AppName = arg.app.Name,
|
||||||
Created = app.Created,
|
AppType = arg.app.AppType,
|
||||||
App = app
|
Id = arg.app.Id,
|
||||||
})
|
Created = arg.app.Created,
|
||||||
.OrderBy(b => b.Created)
|
App = arg.app
|
||||||
.ToArrayAsync();
|
}).ToArray();
|
||||||
|
|
||||||
// allowNoUser can lead to apps being included twice, unify them with distinct
|
// allowNoUser can lead to apps being included twice, unify them with distinct
|
||||||
if (allowNoUser)
|
if (allowNoUser)
|
||||||
@ -368,7 +360,8 @@ namespace BTCPayServer.Services.Apps
|
|||||||
return null;
|
return null;
|
||||||
await using var ctx = _ContextFactory.CreateContext();
|
await using var ctx = _ContextFactory.CreateContext();
|
||||||
var app = await ctx.UserStore
|
var app = await ctx.UserStore
|
||||||
.Where(us => us.ApplicationUserId == userId && us.Role == StoreRoles.Owner)
|
.Include(store => store.StoreRole)
|
||||||
|
.Where(us => us.ApplicationUserId == userId && us.StoreRole.Permissions.Contains(Policies.CanModifyStoreSettings))
|
||||||
.SelectMany(us => us.StoreData.Apps.Where(a => a.Id == appId))
|
.SelectMany(us => us.StoreData.Apps.Where(a => a.Id == appId))
|
||||||
.FirstOrDefaultAsync();
|
.FirstOrDefaultAsync();
|
||||||
if (app == null)
|
if (app == null)
|
||||||
|
@ -52,6 +52,9 @@ namespace BTCPayServer.Services
|
|||||||
public List<DomainToAppMappingItem> DomainToAppMapping { get; set; } = new List<DomainToAppMappingItem>();
|
public List<DomainToAppMappingItem> DomainToAppMapping { get; set; } = new List<DomainToAppMappingItem>();
|
||||||
[Display(Name = "Enable experimental features")]
|
[Display(Name = "Enable experimental features")]
|
||||||
public bool Experimental { get; set; }
|
public bool Experimental { get; set; }
|
||||||
|
|
||||||
|
[Display(Name = "Default role for users on a new store")]
|
||||||
|
public string DefaultRole { get; set; }
|
||||||
|
|
||||||
public class BlockExplorerOverrideItem
|
public class BlockExplorerOverrideItem
|
||||||
{
|
{
|
||||||
|
@ -4,6 +4,7 @@ using System.Collections.Generic;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using BTCPayServer.Abstractions.Contracts;
|
using BTCPayServer.Abstractions.Contracts;
|
||||||
|
using BTCPayServer.Client;
|
||||||
using BTCPayServer.Data;
|
using BTCPayServer.Data;
|
||||||
using BTCPayServer.Events;
|
using BTCPayServer.Events;
|
||||||
using BTCPayServer.Migrations;
|
using BTCPayServer.Migrations;
|
||||||
@ -18,6 +19,7 @@ namespace BTCPayServer.Services.Stores
|
|||||||
{
|
{
|
||||||
private readonly ApplicationDbContextFactory _ContextFactory;
|
private readonly ApplicationDbContextFactory _ContextFactory;
|
||||||
private readonly EventAggregator _eventAggregator;
|
private readonly EventAggregator _eventAggregator;
|
||||||
|
private readonly SettingsRepository _settingsRepository;
|
||||||
|
|
||||||
public JsonSerializerSettings SerializerSettings { get; }
|
public JsonSerializerSettings SerializerSettings { get; }
|
||||||
|
|
||||||
@ -25,10 +27,11 @@ namespace BTCPayServer.Services.Stores
|
|||||||
{
|
{
|
||||||
return _ContextFactory.CreateContext();
|
return _ContextFactory.CreateContext();
|
||||||
}
|
}
|
||||||
public StoreRepository(ApplicationDbContextFactory contextFactory, JsonSerializerSettings serializerSettings, EventAggregator eventAggregator)
|
public StoreRepository(ApplicationDbContextFactory contextFactory, JsonSerializerSettings serializerSettings, EventAggregator eventAggregator, SettingsRepository settingsRepository)
|
||||||
{
|
{
|
||||||
_ContextFactory = contextFactory ?? throw new ArgumentNullException(nameof(contextFactory));
|
_ContextFactory = contextFactory ?? throw new ArgumentNullException(nameof(contextFactory));
|
||||||
_eventAggregator = eventAggregator;
|
_eventAggregator = eventAggregator;
|
||||||
|
_settingsRepository = settingsRepository;
|
||||||
SerializerSettings = serializerSettings;
|
SerializerSettings = serializerSettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -45,56 +48,168 @@ namespace BTCPayServer.Services.Stores
|
|||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(userId);
|
ArgumentNullException.ThrowIfNull(userId);
|
||||||
await using var ctx = _ContextFactory.CreateContext();
|
await using var ctx = _ContextFactory.CreateContext();
|
||||||
return (await ctx
|
return await ctx
|
||||||
.UserStore
|
.UserStore
|
||||||
.Where(us => us.ApplicationUserId == userId && us.StoreDataId == storeId)
|
.Where(us => us.ApplicationUserId == userId && us.StoreDataId == storeId)
|
||||||
.Include(store => store.StoreData.UserStores)
|
.Include(store => store.StoreData.UserStores)
|
||||||
.Select(us => new
|
.ThenInclude(store => store.StoreRole)
|
||||||
{
|
.Select(us => us.StoreData).FirstOrDefaultAsync();
|
||||||
Store = us.StoreData,
|
|
||||||
Role = us.Role
|
|
||||||
}).ToArrayAsync())
|
|
||||||
.Select(us =>
|
|
||||||
{
|
|
||||||
us.Store.Role = us.Role;
|
|
||||||
return us.Store;
|
|
||||||
}).FirstOrDefault();
|
|
||||||
}
|
}
|
||||||
#nullable disable
|
#nullable disable
|
||||||
public class StoreUser
|
public class StoreUser
|
||||||
{
|
{
|
||||||
public string Id { get; set; }
|
public string Id { get; set; }
|
||||||
public string Email { get; set; }
|
public string Email { get; set; }
|
||||||
|
public StoreRole StoreRole { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class StoreRole
|
||||||
|
{
|
||||||
|
public PermissionSet ToPermissionSet(string storeId)
|
||||||
|
{
|
||||||
|
return new PermissionSet(Permissions
|
||||||
|
.Select(s => Permission.TryCreatePermission(s, storeId, out var permission) ? permission : null)
|
||||||
|
.Where(s => s != null).ToArray());
|
||||||
|
}
|
||||||
public string Role { get; set; }
|
public string Role { get; set; }
|
||||||
|
public List<string> Permissions { get; set; }
|
||||||
|
public bool IsServerRole { get; set; }
|
||||||
|
public string Id { get; set; }
|
||||||
|
public bool? IsUsed { get; set; }
|
||||||
}
|
}
|
||||||
#nullable enable
|
#nullable enable
|
||||||
|
public async Task<StoreRole[]> GetStoreRoles(string? storeId, bool includeUsers = false, bool storeOnly = false)
|
||||||
|
{
|
||||||
|
await using var ctx = _ContextFactory.CreateContext();
|
||||||
|
var query = ctx.StoreRoles.Where(u => (storeOnly && u.StoreDataId == storeId) || (!storeOnly && (u.StoreDataId == null || u.StoreDataId == storeId)));
|
||||||
|
if (includeUsers)
|
||||||
|
{
|
||||||
|
query = query.Include(u => u.Users);
|
||||||
|
}
|
||||||
|
return (await query.ToArrayAsync()).Select(role => ToStoreRole(role)).ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<StoreRoleId> GetDefaultRole()
|
||||||
|
{
|
||||||
|
var r = (await _settingsRepository.GetSettingAsync<PoliciesSettings>())?.DefaultRole;
|
||||||
|
if (r is not null)
|
||||||
|
return new StoreRoleId(r);
|
||||||
|
return StoreRoleId.Owner;
|
||||||
|
}
|
||||||
|
public async Task SetDefaultRole(string role)
|
||||||
|
{
|
||||||
|
var s = (await _settingsRepository.GetSettingAsync<PoliciesSettings>()) ?? new PoliciesSettings();
|
||||||
|
s.DefaultRole = role;
|
||||||
|
await _settingsRepository.UpdateSetting(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<StoreRole?> GetStoreRole(StoreRoleId role, bool includeUsers = false)
|
||||||
|
{
|
||||||
|
await using var ctx = _ContextFactory.CreateContext();
|
||||||
|
var query = ctx.StoreRoles.AsQueryable();
|
||||||
|
if (includeUsers)
|
||||||
|
{
|
||||||
|
query = query.Include(u => u.Users);
|
||||||
|
}
|
||||||
|
var match = await query.SingleOrDefaultAsync(r => r.Id == role.Id);
|
||||||
|
if (match == null)
|
||||||
|
return null;
|
||||||
|
if (match.StoreDataId != role.StoreId)
|
||||||
|
// Should never happen because the roleId include the storeId
|
||||||
|
throw new InvalidOperationException("Bug 03991: This should never happen");
|
||||||
|
return ToStoreRole(match);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string?> RemoveStoreRole(StoreRoleId role)
|
||||||
|
{
|
||||||
|
await using var ctx = _ContextFactory.CreateContext();
|
||||||
|
if (await GetDefaultRole() == role)
|
||||||
|
{
|
||||||
|
return "Cannot remove the default role";
|
||||||
|
}
|
||||||
|
|
||||||
|
var match = await ctx.StoreRoles.FindAsync(role.Id);
|
||||||
|
if (match != null && match.StoreDataId == role.StoreId)
|
||||||
|
{
|
||||||
|
if (role.StoreId is null && match.Permissions.Contains(Policies.CanModifyStoreSettings) &&
|
||||||
|
await ctx.StoreRoles.CountAsync(role =>
|
||||||
|
role.StoreDataId == null && role.Permissions.Contains(Policies.CanModifyStoreSettings)) == 1)
|
||||||
|
return "This is the last role that allows to modify store settings, you cannot remove it";
|
||||||
|
ctx.StoreRoles.Remove(match);
|
||||||
|
await ctx.SaveChangesAsync();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return "Role not found";
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<StoreRole?> AddOrUpdateStoreRole(StoreRoleId role, List<string> policies)
|
||||||
|
{
|
||||||
|
policies = policies.Where(s => Policies.IsValidPolicy(s) && Policies.IsStorePolicy(s)).ToList();
|
||||||
|
await using var ctx = _ContextFactory.CreateContext();
|
||||||
|
Data.StoreRole? match = await ctx.StoreRoles.FindAsync(role.Id);
|
||||||
|
if (match is null)
|
||||||
|
{
|
||||||
|
match = new Data.StoreRole() { Id = role.Id, StoreDataId = role.StoreId, Role = role.Role };
|
||||||
|
ctx.StoreRoles.Add(match);
|
||||||
|
}
|
||||||
|
match.Permissions = policies;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await ctx.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
catch (DbUpdateException)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return ToStoreRole(match);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public async Task<StoreUser[]> GetStoreUsers(string storeId)
|
public async Task<StoreUser[]> GetStoreUsers(string storeId)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(storeId);
|
ArgumentNullException.ThrowIfNull(storeId);
|
||||||
using var ctx = _ContextFactory.CreateContext();
|
await using var ctx = _ContextFactory.CreateContext();
|
||||||
return await ctx
|
return (await
|
||||||
.UserStore
|
ctx
|
||||||
.Where(u => u.StoreDataId == storeId)
|
.UserStore
|
||||||
.Select(u => new StoreUser()
|
.Where(u => u.StoreDataId == storeId)
|
||||||
{
|
.Include(u => u.StoreRole)
|
||||||
Id = u.ApplicationUserId,
|
.Select(u => new
|
||||||
Email = u.ApplicationUser.Email,
|
{
|
||||||
Role = u.Role
|
Id = u.ApplicationUserId,
|
||||||
}).ToArrayAsync();
|
u.ApplicationUser.Email,
|
||||||
|
u.StoreRole
|
||||||
|
}).ToArrayAsync()).Select(arg => new StoreUser()
|
||||||
|
{
|
||||||
|
StoreRole = ToStoreRole(arg.StoreRole),
|
||||||
|
Id = arg.Id,
|
||||||
|
Email = arg.Email
|
||||||
|
}).ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static StoreRole ToStoreRole(Data.StoreRole storeRole)
|
||||||
|
{
|
||||||
|
return new StoreRole()
|
||||||
|
{
|
||||||
|
Id = storeRole.Id,
|
||||||
|
Role = storeRole.Role,
|
||||||
|
Permissions = storeRole.Permissions,
|
||||||
|
IsServerRole = storeRole.StoreDataId == null,
|
||||||
|
IsUsed = storeRole.Users?.Any()
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<StoreData[]> GetStoresByUserId(string userId, IEnumerable<string>? storeIds = null)
|
public async Task<StoreData[]> GetStoresByUserId(string userId, IEnumerable<string>? storeIds = null)
|
||||||
{
|
{
|
||||||
using var ctx = _ContextFactory.CreateContext();
|
await using var ctx = _ContextFactory.CreateContext();
|
||||||
return (await ctx.UserStore
|
return (await ctx.UserStore
|
||||||
.Where(u => u.ApplicationUserId == userId && (storeIds == null || storeIds.Contains(u.StoreDataId)))
|
.Where(u => u.ApplicationUserId == userId && (storeIds == null || storeIds.Contains(u.StoreDataId)))
|
||||||
.Select(u => new { u.StoreData, u.Role })
|
.Include(store => store.StoreData)
|
||||||
.ToArrayAsync())
|
.ThenInclude(data => data.UserStores)
|
||||||
.Select(u =>
|
.ThenInclude(data => data.StoreRole)
|
||||||
{
|
.Select(store => store.StoreData)
|
||||||
u.StoreData.Role = u.Role;
|
.ToArrayAsync());
|
||||||
return u.StoreData;
|
|
||||||
}).ToArray();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<StoreData?> GetStoreByInvoiceId(string invoiceId)
|
public async Task<StoreData?> GetStoreByInvoiceId(string invoiceId)
|
||||||
@ -105,29 +220,68 @@ namespace BTCPayServer.Services.Stores
|
|||||||
return matched?.StoreData;
|
return matched?.StoreData;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> AddStoreUser(string storeId, string userId, string role)
|
/// <summary>
|
||||||
|
/// `role` can be passed in two format:
|
||||||
|
/// STOREID::ROLE or ROLE.
|
||||||
|
/// If the first case, this method make sure the storeId is same as <paramref name="storeId"/>.
|
||||||
|
/// In the second case, we interprete ROLE as a server level roleId first, then if it does not exist, check if there is a store level role.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="storeId"></param>
|
||||||
|
/// <param name="role"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public async Task<StoreRoleId?> ResolveStoreRoleId(string storeId, string? role)
|
||||||
{
|
{
|
||||||
using var ctx = _ContextFactory.CreateContext();
|
if (string.IsNullOrWhiteSpace(role))
|
||||||
var userStore = new UserStore() { StoreDataId = storeId, ApplicationUserId = userId, Role = role };
|
return null;
|
||||||
|
if (role.Contains("::", StringComparison.OrdinalIgnoreCase) || storeId.Contains("::", StringComparison.OrdinalIgnoreCase))
|
||||||
|
return null;
|
||||||
|
var roleId = StoreRoleId.Parse(role);
|
||||||
|
if (roleId.StoreId != null && roleId.StoreId != storeId)
|
||||||
|
return null;
|
||||||
|
if ((await GetStoreRole(roleId)) != null)
|
||||||
|
return roleId;
|
||||||
|
if (roleId.IsServerRole)
|
||||||
|
roleId = new StoreRoleId(storeId, role);
|
||||||
|
if ((await GetStoreRole(roleId)) != null)
|
||||||
|
return roleId;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> AddStoreUser(string storeId, string userId, StoreRoleId? roleId = null)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(storeId);
|
||||||
|
AssertStoreRoleIfNeeded(storeId, roleId);
|
||||||
|
roleId ??= await GetDefaultRole();
|
||||||
|
await using var ctx = _ContextFactory.CreateContext();
|
||||||
|
var userStore = new UserStore() { StoreDataId = storeId, ApplicationUserId = userId, StoreRoleId = roleId.Id };
|
||||||
ctx.UserStore.Add(userStore);
|
ctx.UserStore.Add(userStore);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await ctx.SaveChangesAsync();
|
await ctx.SaveChangesAsync();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
catch (Microsoft.EntityFrameworkCore.DbUpdateException)
|
catch (DbUpdateException)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void AssertStoreRoleIfNeeded(string storeId, StoreRoleId? roleId)
|
||||||
|
{
|
||||||
|
if (roleId?.StoreId != null && storeId != roleId.StoreId)
|
||||||
|
throw new ArgumentException("The roleId doesn't belong to this storeId", nameof(roleId));
|
||||||
|
}
|
||||||
|
|
||||||
public async Task CleanUnreachableStores()
|
public async Task CleanUnreachableStores()
|
||||||
{
|
{
|
||||||
await using var ctx = _ContextFactory.CreateContext();
|
await using var ctx = _ContextFactory.CreateContext();
|
||||||
if (!ctx.Database.SupportDropForeignKey())
|
if (!ctx.Database.SupportDropForeignKey())
|
||||||
return;
|
return;
|
||||||
var events = new List<Events.StoreRemovedEvent>();
|
var events = new List<Events.StoreRemovedEvent>();
|
||||||
foreach (var store in await ctx.Stores.Where(s => s.UserStores.All(u => u.Role != StoreRoles.Owner)).ToArrayAsync())
|
foreach (var store in await ctx.Stores.Include(data => data.UserStores)
|
||||||
|
.ThenInclude(store => store.StoreRole).Where(s =>
|
||||||
|
s.UserStores.All(u => !u.StoreRole.Permissions.Contains(Policies.CanModifyStoreSettings)))
|
||||||
|
.ToArrayAsync())
|
||||||
{
|
{
|
||||||
ctx.Stores.Remove(store);
|
ctx.Stores.Remove(store);
|
||||||
events.Add(new Events.StoreRemovedEvent(store.Id));
|
events.Add(new Events.StoreRemovedEvent(store.Id));
|
||||||
@ -139,8 +293,8 @@ namespace BTCPayServer.Services.Stores
|
|||||||
public async Task<bool> RemoveStoreUser(string storeId, string userId)
|
public async Task<bool> RemoveStoreUser(string storeId, string userId)
|
||||||
{
|
{
|
||||||
await using var ctx = _ContextFactory.CreateContext();
|
await using var ctx = _ContextFactory.CreateContext();
|
||||||
if (!await ctx.UserStore.AnyAsync(store =>
|
if (!await ctx.UserStore.Include(store => store.StoreRole).AnyAsync(store =>
|
||||||
store.StoreDataId == storeId && store.Role == StoreRoles.Owner &&
|
store.StoreDataId == storeId && store.StoreRole.Permissions.Contains(Policies.CanModifyStoreSettings) &&
|
||||||
userId != store.ApplicationUserId))
|
userId != store.ApplicationUserId))
|
||||||
return false;
|
return false;
|
||||||
var userStore = new UserStore() { StoreDataId = storeId, ApplicationUserId = userId };
|
var userStore = new UserStore() { StoreDataId = storeId, ApplicationUserId = userId };
|
||||||
@ -156,7 +310,7 @@ namespace BTCPayServer.Services.Stores
|
|||||||
await using var ctx = _ContextFactory.CreateContext();
|
await using var ctx = _ContextFactory.CreateContext();
|
||||||
if (ctx.Database.SupportDropForeignKey())
|
if (ctx.Database.SupportDropForeignKey())
|
||||||
{
|
{
|
||||||
if (!await ctx.UserStore.Where(u => u.StoreDataId == storeId && u.Role == StoreRoles.Owner).AnyAsync())
|
if (!await ctx.UserStore.Where(u => u.StoreDataId == storeId && u.StoreRole.Permissions.Contains(Policies.CanModifyStoreSettings)).AnyAsync())
|
||||||
{
|
{
|
||||||
var store = await ctx.Stores.FindAsync(storeId);
|
var store = await ctx.Stores.FindAsync(storeId);
|
||||||
if (store != null)
|
if (store != null)
|
||||||
@ -168,20 +322,23 @@ namespace BTCPayServer.Services.Stores
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
public async Task CreateStore(string ownerId, StoreData storeData)
|
public async Task CreateStore(string ownerId, StoreData storeData, StoreRoleId? roleId = null)
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrEmpty(storeData.Id))
|
if (!string.IsNullOrEmpty(storeData.Id))
|
||||||
throw new ArgumentException("id should be empty", nameof(storeData.StoreName));
|
throw new ArgumentException("id should be empty", nameof(storeData.StoreName));
|
||||||
if (string.IsNullOrEmpty(storeData.StoreName))
|
if (string.IsNullOrEmpty(storeData.StoreName))
|
||||||
throw new ArgumentException("name should not be empty", nameof(storeData.StoreName));
|
throw new ArgumentException("name should not be empty", nameof(storeData.StoreName));
|
||||||
ArgumentNullException.ThrowIfNull(ownerId);
|
ArgumentNullException.ThrowIfNull(ownerId);
|
||||||
using var ctx = _ContextFactory.CreateContext();
|
AssertStoreRoleIfNeeded(storeData.Id, roleId);
|
||||||
|
await using var ctx = _ContextFactory.CreateContext();
|
||||||
storeData.Id = Encoders.Base58.EncodeData(RandomUtils.GetBytes(32));
|
storeData.Id = Encoders.Base58.EncodeData(RandomUtils.GetBytes(32));
|
||||||
|
roleId ??= await GetDefaultRole();
|
||||||
|
|
||||||
var userStore = new UserStore
|
var userStore = new UserStore
|
||||||
{
|
{
|
||||||
StoreDataId = storeData.Id,
|
StoreDataId = storeData.Id,
|
||||||
ApplicationUserId = ownerId,
|
ApplicationUserId = ownerId,
|
||||||
Role = StoreRoles.Owner,
|
StoreRoleId = roleId.Id,
|
||||||
};
|
};
|
||||||
|
|
||||||
ctx.Add(storeData);
|
ctx.Add(storeData);
|
||||||
@ -234,7 +391,7 @@ namespace BTCPayServer.Services.Stores
|
|||||||
.Where(s => s.StoreId == storeId && s.WebhookId == webhookId)
|
.Where(s => s.StoreId == storeId && s.WebhookId == webhookId)
|
||||||
.SelectMany(s => s.Webhook.Deliveries)
|
.SelectMany(s => s.Webhook.Deliveries)
|
||||||
.OrderByDescending(s => s.Timestamp);
|
.OrderByDescending(s => s.Timestamp);
|
||||||
if (count is int c)
|
if (count is { } c)
|
||||||
req = req.Take(c);
|
req = req.Take(c);
|
||||||
return await req
|
return await req
|
||||||
.ToArrayAsync();
|
.ToArrayAsync();
|
||||||
@ -431,4 +588,66 @@ retry:
|
|||||||
return ctx.Database.SupportDropForeignKey();
|
return ctx.Database.SupportDropForeignKey();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public record StoreRoleId
|
||||||
|
{
|
||||||
|
public static StoreRoleId Parse(string str)
|
||||||
|
{
|
||||||
|
var i = str.IndexOf("::");
|
||||||
|
string? storeId = null;
|
||||||
|
string role;
|
||||||
|
if (i == -1)
|
||||||
|
{
|
||||||
|
role = str;
|
||||||
|
return new StoreRoleId(role);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
role = str[0..i];
|
||||||
|
storeId = str[(i + 2)..];
|
||||||
|
return new StoreRoleId(storeId, role);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public StoreRoleId(string role)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(role))
|
||||||
|
throw new ArgumentException("Role shouldn't be null or empty", nameof(role));
|
||||||
|
if (role.Contains("::", StringComparison.OrdinalIgnoreCase))
|
||||||
|
throw new ArgumentException("Role shouldn't contains '::'", nameof(role));
|
||||||
|
Role = role;
|
||||||
|
}
|
||||||
|
public StoreRoleId(string storeId, string role)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(role))
|
||||||
|
throw new ArgumentException("Role shouldn't be null or empty", nameof(role));
|
||||||
|
if (string.IsNullOrWhiteSpace(storeId))
|
||||||
|
throw new ArgumentException("StoreId shouldn't be null or empty", nameof(storeId));
|
||||||
|
if (role.Contains("::", StringComparison.OrdinalIgnoreCase))
|
||||||
|
throw new ArgumentException("Role shouldn't contains '::'", nameof(role));
|
||||||
|
if (storeId.Contains("::", StringComparison.OrdinalIgnoreCase))
|
||||||
|
throw new ArgumentException("StoreId shouldn't contains '::'", nameof(storeId));
|
||||||
|
StoreId = storeId;
|
||||||
|
Role = role;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static StoreRoleId Owner { get; } = new StoreRoleId("Owner");
|
||||||
|
public static StoreRoleId Guest { get; } = new StoreRoleId("Guest");
|
||||||
|
public string? StoreId { get; }
|
||||||
|
public string Role { get; }
|
||||||
|
public string Id
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (StoreId is null)
|
||||||
|
return Role;
|
||||||
|
return $"{StoreId}::{Role}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsServerRole => StoreId is null;
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return Id;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,19 +1,13 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace BTCPayServer
|
namespace BTCPayServer
|
||||||
{
|
{
|
||||||
public class StoreRoles
|
public class StoreRoles
|
||||||
{
|
{
|
||||||
|
[Obsolete("You should check authorization policies instead of roles")]
|
||||||
public const string Owner = "Owner";
|
public const string Owner = "Owner";
|
||||||
|
[Obsolete("You should check authorization policies instead of roles")]
|
||||||
public const string Guest = "Guest";
|
public const string Guest = "Guest";
|
||||||
public static IEnumerable<String> AllRoles
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
yield return Owner;
|
|
||||||
yield return Guest;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
119
BTCPayServer/Views/Shared/CreateOrEditRole.cshtml
Normal file
119
BTCPayServer/Views/Shared/CreateOrEditRole.cshtml
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
@using BTCPayServer.Client
|
||||||
|
@using BTCPayServer.Views.Server
|
||||||
|
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
||||||
|
@using BTCPayServer.Abstractions.Extensions
|
||||||
|
@using BTCPayServer.Abstractions.TagHelpers
|
||||||
|
@using BTCPayServer.Controllers
|
||||||
|
@using BTCPayServer.Views.Stores
|
||||||
|
@model UpdateRoleViewModel
|
||||||
|
@{
|
||||||
|
Layout = "_NavLayout.cshtml";
|
||||||
|
var role = Context.GetRouteValue("role") as string;
|
||||||
|
|
||||||
|
if (role == "create")
|
||||||
|
role = null;
|
||||||
|
|
||||||
|
var storeId = Context.GetRouteValue("storeId") as string;
|
||||||
|
var controller = ViewContext.RouteData.Values["controller"].ToString().TrimEnd("Controller", StringComparison.InvariantCultureIgnoreCase);
|
||||||
|
if (storeId is null)
|
||||||
|
ViewData.SetActivePage(ServerNavPages.Roles, role is null ? "Create role" : "Update role");
|
||||||
|
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ViewData.SetActivePage(StoreNavPages.Roles, role is null ? "Create role" : "Update role");
|
||||||
|
}
|
||||||
|
var storePolicies = Policies.AllPolicies.Where(Policies.IsStorePolicy).ToArray();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
<h3 class="mb-4">@ViewData["Title"]</h3>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-xxl-constrain">
|
||||||
|
<form method="post">
|
||||||
|
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
|
||||||
|
|
||||||
|
<div class="form-group" style="max-width:320px">
|
||||||
|
<label asp-for="Role" class="form-label"></label>
|
||||||
|
@if (role == null)
|
||||||
|
{
|
||||||
|
<input asp-for="Role" required="required" class="form-control" />
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<input asp-for="Role" required="required" class="form-control" readonly />
|
||||||
|
}
|
||||||
|
<span asp-validation-for="Role" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h4 class="mt-4 mb-3">Permissions</h4>
|
||||||
|
<select multiple="multiple" asp-for="Policies" class="form-select hide-when-js">
|
||||||
|
@foreach (var policy in storePolicies)
|
||||||
|
{
|
||||||
|
<option value="@policy" class="text-truncate" asp-selected="@(Model.Policies?.Contains(policy) ?? false)">@policy</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
<div class="list-group mb-2">
|
||||||
|
@{
|
||||||
|
var storePolicyMap = Permission.PolicyMap.Where(pair => Policies.IsStorePolicy(pair.Key)).ToArray();
|
||||||
|
var topMostPolicies = storePolicyMap.Where(pair => !storePolicyMap.Any(valuePair => valuePair.Value.Contains(pair.Key)));
|
||||||
|
@foreach (var policy in topMostPolicies)
|
||||||
|
{
|
||||||
|
RenderTree(policy, storePolicyMap, Model.Policies.Contains(policy.Key));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<span asp-validation-for="Policies" class="text-danger"></span>
|
||||||
|
<button id="Save" type="submit" class="btn btn-primary" name="command" value="Save">Save</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@{
|
||||||
|
void RenderTree(KeyValuePair<string, HashSet<string>> policy, KeyValuePair<string, HashSet<string>>[] storePolicyMap, bool isChecked)
|
||||||
|
{
|
||||||
|
<div class="form-check mb-0">
|
||||||
|
<input type="checkbox" class="form-check-input policy-cb" checked="@isChecked" value="@policy.Key" id="Policy-@policy.Key.Replace(".", "_")" />
|
||||||
|
<label class="h5 fw-semibold form-check-label mb-1" for="Policy-@policy.Key.Replace(".", "_")" data-bs-toggle="tooltip" title="@policy.Key">
|
||||||
|
@UIManageController.AddApiKeyViewModel.PermissionValueItem.PermissionDescriptions[policy.Key].Title
|
||||||
|
</label>
|
||||||
|
<p class="text-muted">@UIManageController.AddApiKeyViewModel.PermissionValueItem.PermissionDescriptions[policy.Key].Description</p>
|
||||||
|
@if (policy.Value?.Any() is true)
|
||||||
|
{
|
||||||
|
<div class="list-group">
|
||||||
|
@foreach (var subPolicy in policy.Value)
|
||||||
|
{
|
||||||
|
var match = storePolicyMap.SingleOrDefault(pair => pair.Key == subPolicy);
|
||||||
|
RenderTree(match.Key is not null ? match : new KeyValuePair<string, HashSet<string>>(subPolicy, null), storePolicyMap, !isChecked && Model.Policies.Contains(subPolicy));
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
<script>
|
||||||
|
function handleCheckboxChange(element) {
|
||||||
|
const { checked, value: policy } = element;
|
||||||
|
const policySelect = document.getElementById('Policies');
|
||||||
|
const subPolicies = element.parentElement.querySelectorAll(`.list-group .policy-cb:not([value="${policy}"])`);
|
||||||
|
|
||||||
|
policySelect.querySelector(`option[value="${policy}"]`).selected = checked;
|
||||||
|
subPolicies.forEach(subPolicy => {
|
||||||
|
subPolicy.checked = checked? false : subPolicy.checked;
|
||||||
|
if (checked){
|
||||||
|
subPolicy.setAttribute("disabled", "disabled");
|
||||||
|
} else {
|
||||||
|
subPolicy.removeAttribute("disabled");
|
||||||
|
}
|
||||||
|
policySelect.querySelector(`option[value="${subPolicy.value}"]`).selected = subPolicy.checked;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
document.querySelectorAll(".policy-cb:checked").forEach(handleCheckboxChange);
|
||||||
|
|
||||||
|
delegate('change', '.policy-cb', event => {
|
||||||
|
handleCheckboxChange(event.target);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
147
BTCPayServer/Views/Shared/ListRoles.cshtml
Normal file
147
BTCPayServer/Views/Shared/ListRoles.cshtml
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
@using BTCPayServer.Components
|
||||||
|
@using BTCPayServer.Views.Server
|
||||||
|
@using BTCPayServer.Views.Stores
|
||||||
|
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
||||||
|
@using BTCPayServer.Abstractions.Extensions
|
||||||
|
@using BTCPayServer.Client
|
||||||
|
@model BTCPayServer.Models.ServerViewModels.RolesViewModel
|
||||||
|
@{
|
||||||
|
Layout = "_NavLayout.cshtml";
|
||||||
|
var storeId = Context.GetRouteValue("storeId") as string;
|
||||||
|
var controller = ViewContext.RouteData.Values["controller"].ToString().TrimEnd("Controller", StringComparison.InvariantCultureIgnoreCase);
|
||||||
|
if (storeId is null)
|
||||||
|
ViewData.SetActivePage(ServerNavPages.Roles);
|
||||||
|
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ViewData.SetActivePage(StoreNavPages.Roles);
|
||||||
|
}
|
||||||
|
var nextRoleSortOrder = (string) ViewData["NextRoleSortOrder"];
|
||||||
|
String roleSortOrder = null;
|
||||||
|
switch (nextRoleSortOrder)
|
||||||
|
{
|
||||||
|
case "asc":
|
||||||
|
roleSortOrder = "desc";
|
||||||
|
break;
|
||||||
|
case "desc":
|
||||||
|
roleSortOrder = "asc";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
var sortIconClass = "fa-sort";
|
||||||
|
if (roleSortOrder != null)
|
||||||
|
{
|
||||||
|
sortIconClass = $"fa-sort-alpha-{roleSortOrder}";
|
||||||
|
}
|
||||||
|
|
||||||
|
var sortByDesc = "Sort by descending...";
|
||||||
|
var sortByAsc = "Sort by ascending...";
|
||||||
|
|
||||||
|
var showInUseColumn = !Model.Roles.Any(r => r.IsUsed is null);
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="d-flex align-items-center justify-content-between mb-3">
|
||||||
|
<h3 class="mb-0">@ViewData["Title"]</h3>
|
||||||
|
<a asp-action="CreateOrEditRole" asp-route-storeId="@storeId" class="btn btn-primary" role="button" id="CreateRole" asp-route-role="create"
|
||||||
|
asp-controller="@controller">
|
||||||
|
<span class="fa fa-plus"></span>
|
||||||
|
Add Role
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>
|
||||||
|
<a
|
||||||
|
asp-controller="@controller"
|
||||||
|
asp-action="ListRoles"
|
||||||
|
asp-route-storeId="@storeId"
|
||||||
|
asp-route-sortOrder="@(nextRoleSortOrder ?? "asc")"
|
||||||
|
class="text-nowrap"
|
||||||
|
title="@(nextRoleSortOrder == "desc" ? sortByAsc : sortByDesc)">
|
||||||
|
Role
|
||||||
|
<span class="fa @(sortIconClass)" />
|
||||||
|
</a>
|
||||||
|
</th>
|
||||||
|
<th >Permissions</th>
|
||||||
|
@if (showInUseColumn)
|
||||||
|
{
|
||||||
|
<th>In use</th>
|
||||||
|
}
|
||||||
|
<th class="text-end">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@foreach (var role in Model.Roles)
|
||||||
|
{
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<div class="d-flex flex-wrap align-items-center gap-2">
|
||||||
|
<span class="me-1">@role.Role</span>
|
||||||
|
@if (role.IsServerRole)
|
||||||
|
{
|
||||||
|
<span class="badge bg-dark">
|
||||||
|
Server-wide
|
||||||
|
</span>
|
||||||
|
@if (Model.DefaultRole == role.Id)
|
||||||
|
{
|
||||||
|
<span class="badge bg-info">
|
||||||
|
Default
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
@if (!role.Permissions.Any())
|
||||||
|
{
|
||||||
|
<span class="text-warning">No policies</span>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
@foreach (var policy in role.Permissions)
|
||||||
|
{
|
||||||
|
<code class="d-block text-break">@policy</code>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
@if (showInUseColumn)
|
||||||
|
{
|
||||||
|
<td class="text-center">
|
||||||
|
@if (role.IsUsed is true)
|
||||||
|
{
|
||||||
|
<span class="text-success fa fa-check"></span>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<span class="text-danger fa fa-times"></span>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
}
|
||||||
|
<td class="text-end">
|
||||||
|
<a permission="@(role.IsServerRole ? Policies.CanModifyServerSettings : Policies.CanModifyStoreSettings)" asp-action="CreateOrEditRole" asp-route-storeId="@storeId" asp-route-role="@role.Role"
|
||||||
|
asp-controller="@(role.IsServerRole ? "UIServer" : "UIStores")">
|
||||||
|
Edit
|
||||||
|
</a> -
|
||||||
|
<a permission="@(role.IsServerRole ? Policies.CanModifyServerSettings : Policies.CanModifyStoreSettings)" asp-action="DeleteRole" asp-route-storeId="@storeId" asp-route-role="@role.Role"
|
||||||
|
asp-controller="@(role.IsServerRole ? "UIServer" : "UIStores")">
|
||||||
|
Remove
|
||||||
|
</a>
|
||||||
|
@if (role.IsServerRole && Model.DefaultRole != role.Id)
|
||||||
|
{
|
||||||
|
|
||||||
|
<a permission="@Policies.CanModifyServerSettings" asp-action="SetDefaultRole" asp-route-role="@role.Role"
|
||||||
|
asp-controller="UIServer" id="SetDefault">
|
||||||
|
- Set as default
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<vc:pager view-model="Model"></vc:pager>
|
@ -1,5 +1,6 @@
|
|||||||
@using BTCPayServer.Services.Apps
|
@using BTCPayServer.Services.Apps
|
||||||
@using BTCPayServer.Abstractions.Models
|
@using BTCPayServer.Abstractions.Models
|
||||||
|
@using BTCPayServer.Client
|
||||||
@model ListAppsViewModel
|
@model ListAppsViewModel
|
||||||
@inject AppService AppService
|
@inject AppService AppService
|
||||||
@{
|
@{
|
||||||
@ -79,14 +80,11 @@
|
|||||||
{
|
{
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
@if (app.IsOwner)
|
<span permission="@Policies.CanModifyStoreSettings">
|
||||||
{
|
<a asp-controller="UIStores" asp-action="GeneralSettings" asp-route-storeId="@app.StoreId">@app.StoreName</a>
|
||||||
<span><a asp-controller="UIStores" asp-action="GeneralSettings" asp-route-storeId="@app.StoreId">@app.StoreName</a></span>
|
</span>
|
||||||
}
|
|
||||||
else
|
<span not-permission="@Policies.CanModifyStoreSettings">@app.StoreName</span>
|
||||||
{
|
|
||||||
<span>@app.StoreName</span>
|
|
||||||
}
|
|
||||||
</td>
|
</td>
|
||||||
<td>@app.AppName</td>
|
<td>@app.AppName</td>
|
||||||
<td>
|
<td>
|
||||||
@ -100,14 +98,15 @@
|
|||||||
<span>@viewStyle</span>
|
<span>@viewStyle</span>
|
||||||
}
|
}
|
||||||
</td>
|
</td>
|
||||||
<td class="text-end">
|
<td class="text-end" permission="@Policies.CanModifyStoreSettings">
|
||||||
@if (app.IsOwner)
|
|
||||||
{
|
|
||||||
<a asp-action="@app.UpdateAction" asp-controller="UIApps" asp-route-appId="@app.Id" asp-route-storeId="@app.StoreId">Settings</a>
|
<a asp-action="@app.UpdateAction" asp-controller="UIApps" asp-route-appId="@app.Id" asp-route-storeId="@app.StoreId">Settings</a>
|
||||||
<span> - </span>
|
<span> - </span>
|
||||||
}
|
|
||||||
<a asp-action="DeleteApp" asp-route-appId="@app.Id" data-bs-toggle="modal" data-bs-target="#ConfirmModal" data-description="The app <strong>@Html.Encode(app.AppName)</strong> and its settings will be permanently deleted from your store <strong>@Html.Encode(app.StoreName)</strong>." data-confirm-input="DELETE">Delete</a>
|
<a asp-action="DeleteApp" asp-route-appId="@app.Id" data-bs-toggle="modal" data-bs-target="#ConfirmModal" data-description="The app <strong>@Html.Encode(app.AppName)</strong> and its settings will be permanently deleted from your store <strong>@Html.Encode(app.StoreName)</strong>." data-confirm-input="DELETE">Delete</a>
|
||||||
</td>
|
</td>
|
||||||
|
<td class="text-end" no-permission="@Policies.CanModifyStoreSettings">
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
}
|
}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
@ -2,6 +2,7 @@ namespace BTCPayServer.Views.Server
|
|||||||
{
|
{
|
||||||
public enum ServerNavPages
|
public enum ServerNavPages
|
||||||
{
|
{
|
||||||
Index, Users, Emails, Policies, Theme, Services, Maintenance, Logs, Files, Plugins
|
Index, Users, Emails, Policies, Theme, Services, Maintenance, Logs, Files, Plugins,
|
||||||
|
Roles
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@
|
|||||||
<nav id="SectionNav">
|
<nav id="SectionNav">
|
||||||
<div class="nav">
|
<div class="nav">
|
||||||
<a asp-controller="UIServer" id="SectionNav-@ServerNavPages.Users" class="nav-link @ViewData.IsActivePage(ServerNavPages.Users)" asp-action="ListUsers">Users</a>
|
<a asp-controller="UIServer" id="SectionNav-@ServerNavPages.Users" class="nav-link @ViewData.IsActivePage(ServerNavPages.Users)" asp-action="ListUsers">Users</a>
|
||||||
|
<a asp-controller="UIServer" id="SectionNav-@ServerNavPages.Roles" class="nav-link @ViewData.IsActivePage(ServerNavPages.Roles)" asp-action="ListRoles">Roles</a>
|
||||||
<a asp-controller="UIServer" id="SectionNav-@ServerNavPages.Emails" class="nav-link @ViewData.IsActivePage(ServerNavPages.Emails)" asp-action="Emails">Email</a>
|
<a asp-controller="UIServer" id="SectionNav-@ServerNavPages.Emails" class="nav-link @ViewData.IsActivePage(ServerNavPages.Emails)" asp-action="Emails">Email</a>
|
||||||
<a asp-controller="UIServer" id="SectionNav-@ServerNavPages.Policies" class="nav-link @ViewData.IsActivePage(ServerNavPages.Policies)" asp-action="Policies">Policies</a>
|
<a asp-controller="UIServer" id="SectionNav-@ServerNavPages.Policies" class="nav-link @ViewData.IsActivePage(ServerNavPages.Policies)" asp-action="Policies">Policies</a>
|
||||||
<a asp-controller="UIServer" id="SectionNav-@ServerNavPages.Services" class="nav-link @ViewData.IsActivePage(ServerNavPages.Services)" asp-action="Services">Services</a>
|
<a asp-controller="UIServer" id="SectionNav-@ServerNavPages.Services" class="nav-link @ViewData.IsActivePage(ServerNavPages.Services)" asp-action="Services">Services</a>
|
||||||
|
@ -24,6 +24,7 @@ namespace BTCPayServer.Views.Stores
|
|||||||
[Obsolete("Use StoreNavPages.Plugins instead")]
|
[Obsolete("Use StoreNavPages.Plugins instead")]
|
||||||
Integrations,
|
Integrations,
|
||||||
Emails,
|
Emails,
|
||||||
Forms
|
Forms,
|
||||||
|
Roles
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,13 @@
|
|||||||
@using BTCPayServer.Abstractions.Models
|
@using BTCPayServer.Abstractions.Models
|
||||||
|
@using BTCPayServer.Services.Stores
|
||||||
|
@using BTCPayServer.Abstractions.Contracts
|
||||||
@model StoreUsersViewModel
|
@model StoreUsersViewModel
|
||||||
|
@inject IScopeProvider ScopeProvider
|
||||||
|
@inject StoreRepository StoreRepository
|
||||||
@{
|
@{
|
||||||
Layout = "../Shared/_NavLayout.cshtml";
|
Layout = "../Shared/_NavLayout.cshtml";
|
||||||
ViewData.SetActivePage(StoreNavPages.Users, "Store Users", Context.GetStoreData().Id);
|
ViewData.SetActivePage(StoreNavPages.Users, "Store Users", Context.GetStoreData().Id);
|
||||||
|
var roles = new SelectList(await StoreRepository.GetStoreRoles(ScopeProvider.GetCurrentStoreId()), nameof(StoreRepository.StoreRole.Id), nameof(StoreRepository.StoreRole.Role), Model.Role);
|
||||||
}
|
}
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@ -24,9 +29,7 @@
|
|||||||
<input asp-for="Email" type="text" class="form-control" placeholder="user@example.com">
|
<input asp-for="Email" type="text" class="form-control" placeholder="user@example.com">
|
||||||
</div>
|
</div>
|
||||||
<div class="ms-3">
|
<div class="ms-3">
|
||||||
<select asp-for="Role" class="form-select">
|
<select asp-for="Role" class="form-select" asp-items="roles">
|
||||||
<option value="@StoreRoles.Owner">Owner</option>
|
|
||||||
<option value="@StoreRoles.Guest">Guest</option>
|
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="ms-3">
|
<div class="ms-3">
|
||||||
|
@ -14,6 +14,7 @@
|
|||||||
<a permission="@Policies.CanModifyStoreSettings" id="SectionNav-@(nameof(StoreNavPages.CheckoutAppearance))" class="nav-link @ViewData.IsActivePage(StoreNavPages.CheckoutAppearance)" asp-controller="UIStores" asp-action="CheckoutAppearance" asp-route-storeId="@storeId">Checkout Appearance</a>
|
<a permission="@Policies.CanModifyStoreSettings" id="SectionNav-@(nameof(StoreNavPages.CheckoutAppearance))" class="nav-link @ViewData.IsActivePage(StoreNavPages.CheckoutAppearance)" asp-controller="UIStores" asp-action="CheckoutAppearance" asp-route-storeId="@storeId">Checkout Appearance</a>
|
||||||
<a permission="@Policies.CanModifyStoreSettings" id="SectionNav-@(nameof(StoreNavPages.Tokens))" class="nav-link @ViewData.IsActivePage(StoreNavPages.Tokens)" asp-controller="UIStores" asp-action="ListTokens" asp-route-storeId="@storeId">Access Tokens</a>
|
<a permission="@Policies.CanModifyStoreSettings" id="SectionNav-@(nameof(StoreNavPages.Tokens))" class="nav-link @ViewData.IsActivePage(StoreNavPages.Tokens)" asp-controller="UIStores" asp-action="ListTokens" asp-route-storeId="@storeId">Access Tokens</a>
|
||||||
<a permission="@Policies.CanModifyStoreSettings" id="SectionNav-@(nameof(StoreNavPages.Users))" class="nav-link @ViewData.IsActivePage(StoreNavPages.Users)" asp-controller="UIStores" asp-action="StoreUsers" asp-route-storeId="@storeId">Users</a>
|
<a permission="@Policies.CanModifyStoreSettings" id="SectionNav-@(nameof(StoreNavPages.Users))" class="nav-link @ViewData.IsActivePage(StoreNavPages.Users)" asp-controller="UIStores" asp-action="StoreUsers" asp-route-storeId="@storeId">Users</a>
|
||||||
|
<a permission="@Policies.CanModifyStoreSettings" id="SectionNav-@(nameof(StoreNavPages.Roles))" class="nav-link @ViewData.IsActivePage(StoreNavPages.Roles)" asp-controller="UIStores" asp-action="ListRoles" asp-route-storeId="@storeId">Roles</a>
|
||||||
<a permission="@Policies.CanModifyStoreSettings" id="SectionNav-@(nameof(StoreNavPages.Webhooks))" class="nav-link @ViewData.IsActivePage(StoreNavPages.Webhooks)" asp-controller="UIStores" asp-action="Webhooks" asp-route-storeId="@storeId">Webhooks</a>
|
<a permission="@Policies.CanModifyStoreSettings" id="SectionNav-@(nameof(StoreNavPages.Webhooks))" class="nav-link @ViewData.IsActivePage(StoreNavPages.Webhooks)" asp-controller="UIStores" asp-action="Webhooks" asp-route-storeId="@storeId">Webhooks</a>
|
||||||
<a permission="@Policies.CanModifyStoreSettings" id="SectionNav-@(nameof(StoreNavPages.PayoutProcessors))" class="nav-link @ViewData.IsActivePage(StoreNavPages.PayoutProcessors)" asp-controller="UIPayoutProcessors" asp-action="ConfigureStorePayoutProcessors" asp-route-storeId="@storeId">Payout Processors</a>
|
<a permission="@Policies.CanModifyStoreSettings" id="SectionNav-@(nameof(StoreNavPages.PayoutProcessors))" class="nav-link @ViewData.IsActivePage(StoreNavPages.PayoutProcessors)" asp-controller="UIPayoutProcessors" asp-action="ConfigureStorePayoutProcessors" asp-route-storeId="@storeId">Payout Processors</a>
|
||||||
<a permission="@Policies.CanModifyStoreSettings" id="SectionNav-@(nameof(StoreNavPages.Emails))" class="nav-link @ViewData.IsActivePage(StoreNavPages.Emails)" asp-controller="UIStores" asp-action="StoreEmailSettings" asp-route-storeId="@storeId">Emails</a>
|
<a permission="@Policies.CanModifyStoreSettings" id="SectionNav-@(nameof(StoreNavPages.Emails))" class="nav-link @ViewData.IsActivePage(StoreNavPages.Emails)" asp-controller="UIStores" asp-action="StoreEmailSettings" asp-route-storeId="@storeId">Emails</a>
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
@model BTCPayServer.Models.WalletViewModels.ListWalletsViewModel
|
@using BTCPayServer.Client
|
||||||
|
@model BTCPayServer.Models.WalletViewModels.ListWalletsViewModel
|
||||||
@{
|
@{
|
||||||
ViewData.SetActivePage(WalletsNavPages.Index, "Wallets");
|
ViewData.SetActivePage(WalletsNavPages.Index, "Wallets");
|
||||||
}
|
}
|
||||||
@ -42,16 +43,18 @@
|
|||||||
@foreach (var wallet in Model.Wallets)
|
@foreach (var wallet in Model.Wallets)
|
||||||
{
|
{
|
||||||
<tr>
|
<tr>
|
||||||
@if (wallet.IsOwner)
|
|
||||||
{
|
<td>
|
||||||
<td><a asp-action="GeneralSettings" asp-controller="UIStores" asp-route-storeId="@wallet.StoreId">@wallet.StoreName</a></td>
|
<a
|
||||||
}
|
permission="@Policies.CanModifyStoreSettings"
|
||||||
else
|
asp-action="GeneralSettings" asp-controller="UIStores" asp-route-storeId="@wallet.StoreId">
|
||||||
{
|
@wallet.StoreName
|
||||||
<td>@wallet.StoreName</td>
|
</a>
|
||||||
}
|
<span not-permission="@Policies.CanModifyStoreSettings">@wallet.StoreName</span>
|
||||||
|
</td>
|
||||||
|
|
||||||
<td>@wallet.CryptoCode</td>
|
<td>@wallet.CryptoCode</td>
|
||||||
<td>@wallet.Balance</td>
|
<td><span permission="@Policies.CanModifyStoreSettings">@wallet.Balance</span></td>
|
||||||
<td class="text-end">
|
<td class="text-end">
|
||||||
<a asp-action="WalletTransactions" asp-route-walletId="@wallet.Id">Manage</a>
|
<a asp-action="WalletTransactions" asp-route-walletId="@wallet.Id">Manage</a>
|
||||||
</td>
|
</td>
|
||||||
|
@ -47,6 +47,42 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"/api/v1/server/roles": {
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"ServerInfo"
|
||||||
|
],
|
||||||
|
"summary": "Get store's roles",
|
||||||
|
"description": "View information about the store's roles at the server's scope",
|
||||||
|
"operationId": "Server_GetStoreRoles",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "The user roles available at the server's scope",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/RoleData"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"description": "If you are authenticated but forbidden to get the store's roles"
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Store not found"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"API_Key": [
|
||||||
|
"btcpay.server.canmodifyserversettings"
|
||||||
|
],
|
||||||
|
"Basic": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"components": {
|
"components": {
|
||||||
|
@ -254,10 +254,92 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"/api/v1/stores/{storeId}/roles": {
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"Stores"
|
||||||
|
],
|
||||||
|
"summary": "Get store's roles",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "storeId",
|
||||||
|
"in": "path",
|
||||||
|
"required": true,
|
||||||
|
"description": "The store to fetch",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "View information about the specified store's roles",
|
||||||
|
"operationId": "Stores_GetStoreRoles",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "The user roles available for this store",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/RoleData"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"description": "If you are authenticated but forbidden to get the store's roles"
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Store not found"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"API_Key": [
|
||||||
|
"btcpay.store.canmodifystoresettings"
|
||||||
|
],
|
||||||
|
"Basic": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"components": {
|
"components": {
|
||||||
"schemas": {
|
"schemas": {
|
||||||
|
"RoleData": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"description": "The role's Id (Same as role if the role is created at server level, if the role is created at the store level the format is `STOREID::ROLE`)",
|
||||||
|
"type": "string",
|
||||||
|
"nullable": false,
|
||||||
|
"example": "Owner"
|
||||||
|
},
|
||||||
|
"role": {
|
||||||
|
"description": "The role's name",
|
||||||
|
"type": "string",
|
||||||
|
"nullable": false,
|
||||||
|
"example": "Owner"
|
||||||
|
},
|
||||||
|
"permissions": {
|
||||||
|
"description": "The permissions attached to this role",
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"example": [
|
||||||
|
"btcpay.store.canmodifystoresettings",
|
||||||
|
"btcpay.store.cantradecustodianaccount",
|
||||||
|
"btcpay.store.canwithdrawfromcustodianaccount",
|
||||||
|
"btcpay.store.candeposittocustodianaccount"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"isServerRole": {
|
||||||
|
"description": "Whether this role is at the scope of the store or scope of the server",
|
||||||
|
"type": "boolean",
|
||||||
|
"example": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"StoreDataList": {
|
"StoreDataList": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
@ -463,7 +545,7 @@
|
|||||||
"nullable": true,
|
"nullable": true,
|
||||||
"items": {
|
"items": {
|
||||||
"$ref": "#/components/schemas/PaymentMethodCriteriaData"
|
"$ref": "#/components/schemas/PaymentMethodCriteriaData"
|
||||||
},
|
},
|
||||||
"description": "The criteria required to activate specific payment methods."
|
"description": "The criteria required to activate specific payment methods."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user