Compare commits
47 Commits
v1.0.3.160
...
v1.0.3.162
Author | SHA1 | Date | |
---|---|---|---|
21c7bcca5a | |||
1df0fe9deb | |||
7038c28429 | |||
d9bdb46033 | |||
e0aad34105 | |||
a88f46e1ab | |||
ba480e40e6 | |||
ef52d6b4c7 | |||
99f47e2848 | |||
8046872315 | |||
b282a70534 | |||
991daefd85 | |||
2a0353b6ff | |||
304caaaf1d | |||
4f5f52b937 | |||
0b4760bc29 | |||
7f6d27cc5b | |||
f8520201ce | |||
efda8ff5bd | |||
27f964e2a1 | |||
56380a5fb3 | |||
a303e793b4 | |||
2934c27ee5 | |||
44d4673981 | |||
fca6b39681 | |||
c3bfce7656 | |||
c607696230 | |||
9eac33793a | |||
18aaa1a0c4 | |||
e7eea1036b | |||
48c21baee5 | |||
95b9884af7 | |||
d9ea9fbffd | |||
0c7f35b000 | |||
78f73132ed | |||
5a93857b4a | |||
b71fd1653e | |||
ec80787120 | |||
501c3241b5 | |||
0a8b303c11 | |||
fec5637040 | |||
5cbe61e2e0 | |||
023e64704d | |||
276a9a95f9 | |||
d16a4334cb | |||
fa51180dfa | |||
a3e7729c52 |
3
.github/ISSUE_TEMPLATE/bug_report.md
vendored
3
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -14,6 +14,9 @@ A clear and concise description of what the bug is.
|
||||
**Logs (if applicable)**
|
||||
Basic logs can be found in Server Settings > Logs.
|
||||
|
||||
**Setup Parameters**
|
||||
If you're reporting a deployment issue run `. btcpay-setup.sh -i` and paste your the paremeters by obscuring private information.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
|
@ -7,7 +7,6 @@
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="3.1.1" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="3.1.0" />
|
||||
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="3.1.0" />
|
||||
<PackageReference Include="OpenIddict.EntityFrameworkCore" Version="3.0.0-alpha1.20058.15" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="3.1.1" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
@ -1,6 +1,8 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
@ -11,15 +13,32 @@ namespace BTCPayServer.Data
|
||||
[MaxLength(50)]
|
||||
public string Id
|
||||
{
|
||||
get; set;
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[MaxLength(50)]
|
||||
public string StoreId
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
[MaxLength(50)] public string StoreId { get; set; }
|
||||
|
||||
[MaxLength(50)] public string UserId { get; set; }
|
||||
|
||||
public APIKeyType Type { get; set; } = APIKeyType.Legacy;
|
||||
public string Permissions { get; set; }
|
||||
|
||||
public StoreData StoreData { get; set; }
|
||||
public ApplicationUser User { get; set; }
|
||||
public string Label { get; set; }
|
||||
public string[] GetPermissions() { return Permissions?.Split(';') ?? new string[0]; }
|
||||
|
||||
public void SetPermissions(IEnumerable<string> permissions)
|
||||
{
|
||||
Permissions = string.Join(';',
|
||||
permissions?.Select(s => s.Replace(";", string.Empty)) ?? new string[0]);
|
||||
}
|
||||
}
|
||||
|
||||
public enum APIKeyType
|
||||
{
|
||||
Legacy,
|
||||
Permanent
|
||||
}
|
||||
}
|
||||
|
@ -4,7 +4,6 @@ using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Design;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using OpenIddict.EntityFrameworkCore.Models;
|
||||
|
||||
namespace BTCPayServer.Data
|
||||
{
|
||||
@ -160,6 +159,12 @@ namespace BTCPayServer.Data
|
||||
.HasOne(o => o.StoreData)
|
||||
.WithMany(i => i.APIKeys)
|
||||
.HasForeignKey(i => i.StoreId).OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
builder.Entity<APIKeyData>()
|
||||
.HasOne(o => o.User)
|
||||
.WithMany(i => i.APIKeys)
|
||||
.HasForeignKey(i => i.UserId).OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
builder.Entity<APIKeyData>()
|
||||
.HasIndex(o => o.StoreId);
|
||||
|
||||
@ -255,28 +260,6 @@ namespace BTCPayServer.Data
|
||||
.HasOne(o => o.WalletData)
|
||||
.WithMany(w => w.WalletTransactions).OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
builder.UseOpenIddict<BTCPayOpenIdClient, BTCPayOpenIdAuthorization, OpenIddictScope<string>, BTCPayOpenIdToken, string>();
|
||||
|
||||
if (Database.IsSqlite() && !_designTime)
|
||||
{
|
||||
// SQLite does not have proper support for DateTimeOffset via Entity Framework Core, see the limitations
|
||||
// here: https://docs.microsoft.com/en-us/ef/core/providers/sqlite/limitations#query-limitations
|
||||
// To work around this, when the Sqlite database provider is used, all model properties of type DateTimeOffset
|
||||
// use the DateTimeOffsetToBinaryConverter
|
||||
// Based on: https://github.com/aspnet/EntityFrameworkCore/issues/10784#issuecomment-415769754
|
||||
// This only supports millisecond precision, but should be sufficient for most use cases.
|
||||
foreach (var entityType in builder.Model.GetEntityTypes())
|
||||
{
|
||||
var properties = entityType.ClrType.GetProperties().Where(p => p.PropertyType == typeof(DateTimeOffset));
|
||||
foreach (var property in properties)
|
||||
{
|
||||
builder
|
||||
.Entity(entityType.Name)
|
||||
.Property(property.Name)
|
||||
.HasConversion(new Microsoft.EntityFrameworkCore.Storage.ValueConversion.DateTimeOffsetToBinaryConverter());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -20,9 +20,7 @@ namespace BTCPayServer.Data
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public List<BTCPayOpenIdClient> OpenIdClients { get; set; }
|
||||
|
||||
|
||||
public List<StoredFile> StoredFiles
|
||||
{
|
||||
get;
|
||||
@ -30,5 +28,6 @@ namespace BTCPayServer.Data
|
||||
}
|
||||
|
||||
public List<U2FDevice> U2FDevices { get; set; }
|
||||
public List<APIKeyData> APIKeys { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +0,0 @@
|
||||
using OpenIddict.EntityFrameworkCore.Models;
|
||||
|
||||
namespace BTCPayServer.Data
|
||||
{
|
||||
public class BTCPayOpenIdAuthorization : OpenIddictAuthorization<string, BTCPayOpenIdClient, BTCPayOpenIdToken> { }
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
using OpenIddict.EntityFrameworkCore.Models;
|
||||
|
||||
namespace BTCPayServer.Data
|
||||
{
|
||||
public class BTCPayOpenIdClient: OpenIddictApplication<string, BTCPayOpenIdAuthorization, BTCPayOpenIdToken>
|
||||
{
|
||||
public string ApplicationUserId { get; set; }
|
||||
public ApplicationUser ApplicationUser { get; set; }
|
||||
}
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
using OpenIddict.EntityFrameworkCore.Models;
|
||||
|
||||
namespace BTCPayServer.Data
|
||||
{
|
||||
public class BTCPayOpenIdToken : OpenIddictToken<string, BTCPayOpenIdClient, BTCPayOpenIdAuthorization> { }
|
||||
}
|
74
BTCPayServer.Data/Migrations/20200119130108_ExtendApiKeys.cs
Normal file
74
BTCPayServer.Data/Migrations/20200119130108_ExtendApiKeys.cs
Normal file
@ -0,0 +1,74 @@
|
||||
using BTCPayServer.Data;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
namespace BTCPayServer.Migrations
|
||||
{
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
[Migration("20200119130108_ExtendApiKeys")]
|
||||
public partial class ExtendApiKeys : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "Permissions",
|
||||
table: "ApiKeys",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "Type",
|
||||
table: "ApiKeys",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "UserId",
|
||||
table: "ApiKeys",
|
||||
maxLength: 50,
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ApiKeys_UserId",
|
||||
table: "ApiKeys",
|
||||
column: "UserId");
|
||||
if (this.SupportAddForeignKey(migrationBuilder.ActiveProvider))
|
||||
{
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_ApiKeys_AspNetUsers_UserId",
|
||||
table: "ApiKeys",
|
||||
column: "UserId",
|
||||
principalTable: "AspNetUsers",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
if (this.SupportDropForeignKey(migrationBuilder.ActiveProvider))
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_ApiKeys_AspNetUsers_UserId",
|
||||
table: "ApiKeys");
|
||||
}
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_ApiKeys_UserId",
|
||||
table: "ApiKeys");
|
||||
if (this.SupportDropColumn(migrationBuilder.ActiveProvider))
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Permissions",
|
||||
table: "ApiKeys");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Type",
|
||||
table: "ApiKeys");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "UserId",
|
||||
table: "ApiKeys");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
173
BTCPayServer.Data/Migrations/20200224134444_Remove_OpenIddict.cs
Normal file
173
BTCPayServer.Data/Migrations/20200224134444_Remove_OpenIddict.cs
Normal file
@ -0,0 +1,173 @@
|
||||
using System;
|
||||
using BTCPayServer.Data;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
namespace BTCPayServer.Migrations
|
||||
{
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
[Migration("20200224134444_Remove_OpenIddict")]
|
||||
public partial class Remove_OpenIddict : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "OpenIddictScopes");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "OpenIddictTokens");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "OpenIddictAuthorizations");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "OpenIddictApplications");
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
int? maxLength = this.IsMySql(migrationBuilder.ActiveProvider) ? (int?)255 : null;
|
||||
migrationBuilder.CreateTable(
|
||||
name: "OpenIddictApplications",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<string>(type: "TEXT", nullable: false, maxLength: maxLength),
|
||||
ApplicationUserId = table.Column<string>(type: "TEXT", nullable: true, maxLength: maxLength),
|
||||
ClientId = table.Column<string>(type: "TEXT", maxLength: 100, nullable: false),
|
||||
ClientSecret = table.Column<string>(type: "TEXT", nullable: true),
|
||||
ConcurrencyToken = table.Column<string>(type: "TEXT", maxLength: 50, nullable: true),
|
||||
ConsentType = table.Column<string>(type: "TEXT", nullable: true),
|
||||
DisplayName = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Permissions = table.Column<string>(type: "TEXT", nullable: true),
|
||||
PostLogoutRedirectUris = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Properties = table.Column<string>(type: "TEXT", nullable: true),
|
||||
RedirectUris = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Requirements = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Type = table.Column<string>(type: "TEXT", maxLength: 25, nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_OpenIddictApplications", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_OpenIddictApplications_AspNetUsers_ApplicationUserId",
|
||||
column: x => x.ApplicationUserId,
|
||||
principalTable: "AspNetUsers",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "OpenIddictScopes",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<string>(type: "TEXT", nullable: false, maxLength: maxLength),
|
||||
ConcurrencyToken = table.Column<string>(type: "TEXT", maxLength: 50, nullable: true),
|
||||
Description = table.Column<string>(type: "TEXT", nullable: true),
|
||||
DisplayName = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Name = table.Column<string>(type: "TEXT", maxLength: 200, nullable: false),
|
||||
Properties = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Resources = table.Column<string>(type: "TEXT", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_OpenIddictScopes", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "OpenIddictAuthorizations",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<string>(type: "TEXT", nullable: false, maxLength: maxLength),
|
||||
ApplicationId = table.Column<string>(type: "TEXT", nullable: true, maxLength: maxLength),
|
||||
ConcurrencyToken = table.Column<string>(type: "TEXT", maxLength: 50, nullable: true),
|
||||
Properties = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Scopes = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Status = table.Column<string>(type: "TEXT", maxLength: 25, nullable: false),
|
||||
Subject = table.Column<string>(type: "TEXT", maxLength: 450, nullable: true),
|
||||
Type = table.Column<string>(type: "TEXT", maxLength: 25, nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_OpenIddictAuthorizations", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_OpenIddictAuthorizations_OpenIddictApplications_ApplicationId",
|
||||
column: x => x.ApplicationId,
|
||||
principalTable: "OpenIddictApplications",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "OpenIddictTokens",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<string>(type: "TEXT", nullable: false, maxLength: maxLength),
|
||||
ApplicationId = table.Column<string>(type: "TEXT", nullable: true, maxLength: maxLength),
|
||||
AuthorizationId = table.Column<string>(type: "TEXT", nullable: true, maxLength: maxLength),
|
||||
ConcurrencyToken = table.Column<string>(type: "TEXT", maxLength: 50, nullable: true),
|
||||
CreationDate = table.Column<DateTimeOffset>(type: "TEXT", nullable: true),
|
||||
ExpirationDate = table.Column<DateTimeOffset>(type: "TEXT", nullable: true),
|
||||
Payload = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Properties = table.Column<string>(type: "TEXT", nullable: true),
|
||||
ReferenceId = table.Column<string>(type: "TEXT", maxLength: 100, nullable: true),
|
||||
Status = table.Column<string>(type: "TEXT", maxLength: 25, nullable: false),
|
||||
Subject = table.Column<string>(type: "TEXT", maxLength: 450, nullable: true),
|
||||
Type = table.Column<string>(type: "TEXT", maxLength: 25, nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_OpenIddictTokens", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_OpenIddictTokens_OpenIddictApplications_ApplicationId",
|
||||
column: x => x.ApplicationId,
|
||||
principalTable: "OpenIddictApplications",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "FK_OpenIddictTokens_OpenIddictAuthorizations_AuthorizationId",
|
||||
column: x => x.AuthorizationId,
|
||||
principalTable: "OpenIddictAuthorizations",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_OpenIddictApplications_ApplicationUserId",
|
||||
table: "OpenIddictApplications",
|
||||
column: "ApplicationUserId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_OpenIddictApplications_ClientId",
|
||||
table: "OpenIddictApplications",
|
||||
column: "ClientId",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_OpenIddictAuthorizations_ApplicationId_Status_Subject_Type",
|
||||
table: "OpenIddictAuthorizations",
|
||||
columns: new[] { "ApplicationId", "Status", "Subject", "Type" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_OpenIddictScopes_Name",
|
||||
table: "OpenIddictScopes",
|
||||
column: "Name",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_OpenIddictTokens_AuthorizationId",
|
||||
table: "OpenIddictTokens",
|
||||
column: "AuthorizationId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_OpenIddictTokens_ReferenceId",
|
||||
table: "OpenIddictTokens",
|
||||
column: "ReferenceId",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_OpenIddictTokens_ApplicationId_Status_Subject_Type",
|
||||
table: "OpenIddictTokens",
|
||||
columns: new[] { "ApplicationId", "Status", "Subject", "Type" });
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
using BTCPayServer.Data;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
namespace BTCPayServer.Migrations
|
||||
{
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
[Migration("20200225133433_AddApiKeyLabel")]
|
||||
public partial class AddApiKeyLabel : Migration
|
||||
{
|
||||
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "Label",
|
||||
table: "ApiKeys",
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Label",
|
||||
table: "ApiKeys");
|
||||
}
|
||||
}
|
||||
}
|
@ -22,14 +22,29 @@ namespace BTCPayServer.Migrations
|
||||
.HasColumnType("TEXT")
|
||||
.HasMaxLength(50);
|
||||
|
||||
b.Property<string>("Label")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Permissions")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("StoreId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasMaxLength(50);
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasMaxLength(50);
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("StoreId");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("ApiKeys");
|
||||
});
|
||||
|
||||
@ -148,164 +163,6 @@ namespace BTCPayServer.Migrations
|
||||
b.ToTable("AspNetUsers");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.BTCPayOpenIdAuthorization", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ApplicationId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ConcurrencyToken")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("TEXT")
|
||||
.HasMaxLength(50);
|
||||
|
||||
b.Property<string>("Properties")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Scopes")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasMaxLength(25);
|
||||
|
||||
b.Property<string>("Subject")
|
||||
.HasColumnType("TEXT")
|
||||
.HasMaxLength(450);
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasMaxLength(25);
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ApplicationId", "Status", "Subject", "Type");
|
||||
|
||||
b.ToTable("OpenIddictAuthorizations");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.BTCPayOpenIdClient", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ApplicationUserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ClientId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasMaxLength(100);
|
||||
|
||||
b.Property<string>("ClientSecret")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ConcurrencyToken")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("TEXT")
|
||||
.HasMaxLength(50);
|
||||
|
||||
b.Property<string>("ConsentType")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("DisplayName")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Permissions")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PostLogoutRedirectUris")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Properties")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("RedirectUris")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Requirements")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasMaxLength(25);
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ApplicationUserId");
|
||||
|
||||
b.HasIndex("ClientId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("OpenIddictApplications");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.BTCPayOpenIdToken", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ApplicationId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("AuthorizationId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ConcurrencyToken")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("TEXT")
|
||||
.HasMaxLength(50);
|
||||
|
||||
b.Property<DateTimeOffset?>("CreationDate")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset?>("ExpirationDate")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Payload")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Properties")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ReferenceId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasMaxLength(100);
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasMaxLength(25);
|
||||
|
||||
b.Property<string>("Subject")
|
||||
.HasColumnType("TEXT")
|
||||
.HasMaxLength(450);
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasMaxLength(25);
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AuthorizationId");
|
||||
|
||||
b.HasIndex("ReferenceId")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("ApplicationId", "Status", "Subject", "Type");
|
||||
|
||||
b.ToTable("OpenIddictTokens");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.HistoricalAddressInvoiceData", b =>
|
||||
{
|
||||
b.Property<string>("InvoiceDataId")
|
||||
@ -800,48 +657,17 @@ namespace BTCPayServer.Migrations
|
||||
b.ToTable("AspNetUserTokens");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictScope<string>", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ConcurrencyToken")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("TEXT")
|
||||
.HasMaxLength(50);
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("DisplayName")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasMaxLength(200);
|
||||
|
||||
b.Property<string>("Properties")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Resources")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Name")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("OpenIddictScopes");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.APIKeyData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
|
||||
.WithMany("APIKeys")
|
||||
.HasForeignKey("StoreId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.HasOne("BTCPayServer.Data.ApplicationUser", "User")
|
||||
.WithMany("APIKeys")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b =>
|
||||
@ -860,31 +686,6 @@ namespace BTCPayServer.Migrations
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.BTCPayOpenIdAuthorization", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.BTCPayOpenIdClient", "Application")
|
||||
.WithMany("Authorizations")
|
||||
.HasForeignKey("ApplicationId");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.BTCPayOpenIdClient", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.ApplicationUser", "ApplicationUser")
|
||||
.WithMany("OpenIdClients")
|
||||
.HasForeignKey("ApplicationUserId");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.BTCPayOpenIdToken", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.BTCPayOpenIdClient", "Application")
|
||||
.WithMany("Tokens")
|
||||
.HasForeignKey("ApplicationId");
|
||||
|
||||
b.HasOne("BTCPayServer.Data.BTCPayOpenIdAuthorization", "Authorization")
|
||||
.WithMany("Tokens")
|
||||
.HasForeignKey("AuthorizationId");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.HistoricalAddressInvoiceData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
|
||||
|
@ -11,7 +11,10 @@ namespace BTCPayServer.Migrations
|
||||
{
|
||||
return activeProvider != "Microsoft.EntityFrameworkCore.Sqlite";
|
||||
}
|
||||
|
||||
public static bool SupportAddForeignKey(this Microsoft.EntityFrameworkCore.Migrations.Migration migration, string activeProvider)
|
||||
{
|
||||
return activeProvider != "Microsoft.EntityFrameworkCore.Sqlite";
|
||||
}
|
||||
public static bool SupportDropForeignKey(this Microsoft.EntityFrameworkCore.Migrations.Migration migration, string activeProvider)
|
||||
{
|
||||
return activeProvider != "Microsoft.EntityFrameworkCore.Sqlite";
|
||||
|
@ -196,14 +196,8 @@ namespace BTCPayServer.Services.Rates
|
||||
throw new APIException(text);
|
||||
}
|
||||
api.ProcessResponse(new InternalHttpWebResponse(webHttpResponse));
|
||||
// local reference to handle delegate becoming null, extended discussion here:
|
||||
// https://github.com/btcpayserver/btcpayserver/commit/00747906849f093712c3907c99404c55b3defa66#r37022103
|
||||
var requestStateChanged = RequestStateChanged;
|
||||
if (requestStateChanged != null)
|
||||
{
|
||||
requestStateChanged(this, RequestMakerState.Finished, text);
|
||||
return text;
|
||||
}
|
||||
|
||||
RequestStateChanged?.Invoke(this, RequestMakerState.Finished, text);
|
||||
return text;
|
||||
}
|
||||
catch (Exception arg)
|
||||
|
300
BTCPayServer.Tests/ApiKeysTests.cs
Normal file
300
BTCPayServer.Tests/ApiKeysTests.cs
Normal file
@ -0,0 +1,300 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Security.APIKeys;
|
||||
using BTCPayServer.Tests.Logging;
|
||||
using BTCPayServer.Views.Manage;
|
||||
using ExchangeSharp;
|
||||
using Newtonsoft.Json;
|
||||
using OpenQA.Selenium;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
public class ApiKeysTests
|
||||
{
|
||||
public const int TestTimeout = TestUtils.TestTimeout;
|
||||
|
||||
public const string TestApiPath = "api/test/apikey";
|
||||
public ApiKeysTests(ITestOutputHelper helper)
|
||||
{
|
||||
Logs.Tester = new XUnitLog(helper) {Name = "Tests"};
|
||||
Logs.LogProvider = new XUnitLogProvider(helper);
|
||||
}
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Selenium", "Selenium")]
|
||||
public async Task CanCreateApiKeys()
|
||||
{
|
||||
//there are 2 ways to create api keys:
|
||||
//as a user through your profile
|
||||
//as an external application requesting an api key from a user
|
||||
|
||||
using (var s = SeleniumTester.Create())
|
||||
{
|
||||
await s.StartAsync();
|
||||
var tester = s.Server;
|
||||
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
|
||||
await user.CreateStoreAsync();
|
||||
s.GoToLogin();
|
||||
s.Login(user.RegisterDetails.Email, user.RegisterDetails.Password);
|
||||
s.GoToProfile(ManageNavPages.APIKeys);
|
||||
s.Driver.FindElement(By.Id("AddApiKey")).Click();
|
||||
if (!user.IsAdmin)
|
||||
{
|
||||
//not an admin, so this permission should not show
|
||||
Assert.DoesNotContain("ServerManagementPermission", s.Driver.PageSource);
|
||||
await user.MakeAdmin();
|
||||
s.Logout();
|
||||
s.GoToLogin();
|
||||
s.Login(user.RegisterDetails.Email, user.RegisterDetails.Password);
|
||||
s.GoToProfile(ManageNavPages.APIKeys);
|
||||
s.Driver.FindElement(By.Id("AddApiKey")).Click();
|
||||
}
|
||||
|
||||
//server management should show now
|
||||
s.SetCheckbox(s, "ServerManagementPermission", true);
|
||||
s.SetCheckbox(s, "StoreManagementPermission", true);
|
||||
s.Driver.FindElement(By.Id("Generate")).Click();
|
||||
var superApiKey = s.AssertHappyMessage().FindElement(By.TagName("code")).Text;
|
||||
|
||||
//this api key has access to everything
|
||||
await TestApiAgainstAccessToken(superApiKey, tester, user, APIKeyConstants.Permissions.ServerManagement,
|
||||
APIKeyConstants.Permissions.StoreManagement);
|
||||
|
||||
|
||||
s.Driver.FindElement(By.Id("AddApiKey")).Click();
|
||||
s.SetCheckbox(s, "ServerManagementPermission", true);
|
||||
s.Driver.FindElement(By.Id("Generate")).Click();
|
||||
var serverOnlyApiKey = s.AssertHappyMessage().FindElement(By.TagName("code")).Text;
|
||||
await TestApiAgainstAccessToken(serverOnlyApiKey, tester, user,
|
||||
APIKeyConstants.Permissions.ServerManagement);
|
||||
|
||||
|
||||
s.Driver.FindElement(By.Id("AddApiKey")).Click();
|
||||
s.SetCheckbox(s, "StoreManagementPermission", true);
|
||||
s.Driver.FindElement(By.Id("Generate")).Click();
|
||||
var allStoreOnlyApiKey = s.AssertHappyMessage().FindElement(By.TagName("code")).Text;
|
||||
await TestApiAgainstAccessToken(allStoreOnlyApiKey, tester, user,
|
||||
APIKeyConstants.Permissions.StoreManagement);
|
||||
|
||||
s.Driver.FindElement(By.Id("AddApiKey")).Click();
|
||||
s.Driver.FindElement(By.CssSelector("button[value=change-store-mode]")).Click();
|
||||
//there should be a store already by default in the dropdown
|
||||
var dropdown = s.Driver.FindElement(By.Name("SpecificStores[0]"));
|
||||
var option = dropdown.FindElement(By.TagName("option"));
|
||||
var storeId = option.GetAttribute("value");
|
||||
option.Click();
|
||||
s.Driver.FindElement(By.Id("Generate")).Click();
|
||||
var selectiveStoreApiKey = s.AssertHappyMessage().FindElement(By.TagName("code")).Text;
|
||||
await TestApiAgainstAccessToken(selectiveStoreApiKey, tester, user,
|
||||
APIKeyConstants.Permissions.GetStorePermission(storeId));
|
||||
|
||||
s.Driver.FindElement(By.Id("AddApiKey")).Click();
|
||||
s.Driver.FindElement(By.Id("Generate")).Click();
|
||||
var noPermissionsApiKey = s.AssertHappyMessage().FindElement(By.TagName("code")).Text;
|
||||
await TestApiAgainstAccessToken(noPermissionsApiKey, tester, user);
|
||||
|
||||
await Assert.ThrowsAnyAsync<HttpRequestException>(async () =>
|
||||
{
|
||||
await TestApiAgainstAccessToken<bool>("incorrect key", $"{TestApiPath}/me/id",
|
||||
tester.PayTester.HttpClient);
|
||||
});
|
||||
|
||||
|
||||
//let's test the authorized screen now
|
||||
//options for authorize are:
|
||||
//applicationName
|
||||
//redirect
|
||||
//permissions
|
||||
//strict
|
||||
//selectiveStores
|
||||
UriBuilder authorize = new UriBuilder(tester.PayTester.ServerUri);
|
||||
authorize.Path = "api-keys/authorize";
|
||||
|
||||
authorize.AppendPayloadToQuery(new Dictionary<string, object>()
|
||||
{
|
||||
{"redirect", "https://local.local/callback"},
|
||||
{"applicationName", "kukksappname"},
|
||||
{"strict", true},
|
||||
{"selectiveStores", false},
|
||||
{
|
||||
"permissions",
|
||||
new[]
|
||||
{
|
||||
APIKeyConstants.Permissions.StoreManagement,
|
||||
APIKeyConstants.Permissions.ServerManagement
|
||||
}
|
||||
},
|
||||
});
|
||||
var authUrl = authorize.ToString();
|
||||
var perms = new[]
|
||||
{
|
||||
APIKeyConstants.Permissions.StoreManagement, APIKeyConstants.Permissions.ServerManagement
|
||||
};
|
||||
authUrl = authUrl.Replace("permissions=System.String%5B%5D",
|
||||
string.Join("&", perms.Select(s1 => $"permissions={s1}")));
|
||||
s.Driver.Navigate().GoToUrl(authUrl);
|
||||
s.Driver.PageSource.Contains("kukksappname");
|
||||
Assert.NotNull(s.Driver.FindElement(By.Id("StoreManagementPermission")).GetAttribute("readonly"));
|
||||
Assert.True(s.Driver.FindElement(By.Id("StoreManagementPermission")).Selected);
|
||||
Assert.NotNull(s.Driver.FindElement(By.Id("ServerManagementPermission")).GetAttribute("readonly"));
|
||||
Assert.True(s.Driver.FindElement(By.Id("ServerManagementPermission")).Selected);
|
||||
Assert.DoesNotContain("change-store-mode", s.Driver.PageSource);
|
||||
s.Driver.FindElement(By.Id("consent-yes")).Click();
|
||||
var url = s.Driver.Url;
|
||||
IEnumerable<KeyValuePair<string, string>> results = url.Split("?").Last().Split("&")
|
||||
.Select(s1 => new KeyValuePair<string, string>(s1.Split("=")[0], s1.Split("=")[1]));
|
||||
|
||||
var apiKeyRepo = s.Server.PayTester.GetService<APIKeyRepository>();
|
||||
|
||||
await TestApiAgainstAccessToken(results.Single(pair => pair.Key == "key").Value, tester, user,
|
||||
(await apiKeyRepo.GetKey(results.Single(pair => pair.Key == "key").Value)).GetPermissions());
|
||||
|
||||
authorize = new UriBuilder(tester.PayTester.ServerUri);
|
||||
authorize.Path = "api-keys/authorize";
|
||||
authorize.AppendPayloadToQuery(new Dictionary<string, object>()
|
||||
{
|
||||
{"strict", false},
|
||||
{"selectiveStores", true},
|
||||
{
|
||||
"permissions",
|
||||
new[]
|
||||
{
|
||||
APIKeyConstants.Permissions.StoreManagement,
|
||||
APIKeyConstants.Permissions.ServerManagement
|
||||
}
|
||||
}
|
||||
});
|
||||
authUrl = authorize.ToString();
|
||||
perms = new[]
|
||||
{
|
||||
APIKeyConstants.Permissions.StoreManagement, APIKeyConstants.Permissions.ServerManagement
|
||||
};
|
||||
authUrl = authUrl.Replace("permissions=System.String%5B%5D",
|
||||
string.Join("&", perms.Select(s1 => $"permissions={s1}")));
|
||||
s.Driver.Navigate().GoToUrl(authUrl);
|
||||
Assert.DoesNotContain("kukksappname", s.Driver.PageSource);
|
||||
|
||||
Assert.Null(s.Driver.FindElement(By.Id("StoreManagementPermission")).GetAttribute("readonly"));
|
||||
Assert.True(s.Driver.FindElement(By.Id("StoreManagementPermission")).Selected);
|
||||
Assert.Null(s.Driver.FindElement(By.Id("ServerManagementPermission")).GetAttribute("readonly"));
|
||||
Assert.True(s.Driver.FindElement(By.Id("ServerManagementPermission")).Selected);
|
||||
|
||||
s.SetCheckbox(s, "ServerManagementPermission", false);
|
||||
Assert.Contains("change-store-mode", s.Driver.PageSource);
|
||||
s.Driver.FindElement(By.Id("consent-yes")).Click();
|
||||
url = s.Driver.Url;
|
||||
results = url.Split("?").Last().Split("&")
|
||||
.Select(s1 => new KeyValuePair<string, string>(s1.Split("=")[0], s1.Split("=")[1]));
|
||||
|
||||
await TestApiAgainstAccessToken(results.Single(pair => pair.Key == "key").Value, tester, user,
|
||||
(await apiKeyRepo.GetKey(results.Single(pair => pair.Key == "key").Value)).GetPermissions());
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
async Task TestApiAgainstAccessToken(string accessToken, ServerTester tester, TestAccount testAccount,
|
||||
params string[] permissions)
|
||||
{
|
||||
var resultUser =
|
||||
await TestApiAgainstAccessToken<string>(accessToken, $"{TestApiPath}/me/id",
|
||||
tester.PayTester.HttpClient);
|
||||
Assert.Equal(testAccount.UserId, resultUser);
|
||||
|
||||
//create a second user to see if any of its data gets messed upin our results.
|
||||
var secondUser = tester.NewAccount();
|
||||
secondUser.GrantAccess();
|
||||
|
||||
var selectiveStorePermissions = APIKeyConstants.Permissions.ExtractStorePermissionsIds(permissions);
|
||||
if (permissions.Contains(APIKeyConstants.Permissions.StoreManagement) || selectiveStorePermissions.Any())
|
||||
{
|
||||
var resultStores =
|
||||
await TestApiAgainstAccessToken<StoreData[]>(accessToken, $"{TestApiPath}/me/stores",
|
||||
tester.PayTester.HttpClient);
|
||||
|
||||
foreach (string selectiveStorePermission in selectiveStorePermissions)
|
||||
{
|
||||
Assert.True(await TestApiAgainstAccessToken<bool>(accessToken,
|
||||
$"{TestApiPath}/me/stores/{selectiveStorePermission}/can-edit",
|
||||
tester.PayTester.HttpClient));
|
||||
|
||||
Assert.Contains(resultStores,
|
||||
data => data.Id.Equals(selectiveStorePermission, StringComparison.InvariantCultureIgnoreCase));
|
||||
}
|
||||
|
||||
if (permissions.Contains(APIKeyConstants.Permissions.StoreManagement))
|
||||
{
|
||||
Assert.True(await TestApiAgainstAccessToken<bool>(accessToken,
|
||||
$"{TestApiPath}/me/stores/actions",
|
||||
tester.PayTester.HttpClient));
|
||||
|
||||
Assert.True(await TestApiAgainstAccessToken<bool>(accessToken,
|
||||
$"{TestApiPath}/me/stores/{testAccount.StoreId}/can-edit",
|
||||
tester.PayTester.HttpClient));
|
||||
Assert.Contains(resultStores,
|
||||
data => data.Id.Equals(testAccount.StoreId, StringComparison.InvariantCultureIgnoreCase));
|
||||
}
|
||||
else
|
||||
{
|
||||
await Assert.ThrowsAnyAsync<HttpRequestException>(async () =>
|
||||
{
|
||||
await TestApiAgainstAccessToken<bool>(accessToken,
|
||||
$"{TestApiPath}/me/stores/actions",
|
||||
tester.PayTester.HttpClient);
|
||||
});
|
||||
}
|
||||
|
||||
Assert.DoesNotContain(resultStores,
|
||||
data => data.Id.Equals(secondUser.StoreId, StringComparison.InvariantCultureIgnoreCase));
|
||||
}
|
||||
else
|
||||
{
|
||||
await Assert.ThrowsAnyAsync<HttpRequestException>(async () =>
|
||||
{
|
||||
await TestApiAgainstAccessToken<bool>(accessToken,
|
||||
$"{TestApiPath}/me/stores/{testAccount.StoreId}/can-edit",
|
||||
tester.PayTester.HttpClient);
|
||||
});
|
||||
}
|
||||
|
||||
await Assert.ThrowsAnyAsync<HttpRequestException>(async () =>
|
||||
{
|
||||
await TestApiAgainstAccessToken<bool>(accessToken, $"{TestApiPath}/me/stores/{secondUser.StoreId}/can-edit",
|
||||
tester.PayTester.HttpClient);
|
||||
});
|
||||
|
||||
if (permissions.Contains(APIKeyConstants.Permissions.ServerManagement))
|
||||
{
|
||||
Assert.True(await TestApiAgainstAccessToken<bool>(accessToken,
|
||||
$"{TestApiPath}/me/is-admin",
|
||||
tester.PayTester.HttpClient));
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<T> TestApiAgainstAccessToken<T>(string apikey, string url, HttpClient client)
|
||||
{
|
||||
var httpRequest = new HttpRequestMessage(HttpMethod.Get,
|
||||
new Uri(client.BaseAddress, url));
|
||||
httpRequest.Headers.Authorization = new AuthenticationHeaderValue("token", apikey);
|
||||
var result = await client.SendAsync(httpRequest);
|
||||
result.EnsureSuccessStatusCode();
|
||||
|
||||
var rawJson = await result.Content.ReadAsStringAsync();
|
||||
if (typeof(T).IsPrimitive || typeof(T) == typeof(string))
|
||||
{
|
||||
return (T)Convert.ChangeType(rawJson, typeof(T));
|
||||
}
|
||||
|
||||
return JsonConvert.DeserializeObject<T>(rawJson);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,427 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
using System.Security.Claims;
|
||||
using BTCPayServer.Tests.Logging;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using BTCPayServer.Data;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using OpenIddict.Abstractions;
|
||||
using OpenQA.Selenium;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
public class AuthenticationTests
|
||||
{
|
||||
public const int TestTimeout = TestUtils.TestTimeout;
|
||||
public AuthenticationTests(ITestOutputHelper helper)
|
||||
{
|
||||
Logs.Tester = new XUnitLog(helper) {Name = "Tests"};
|
||||
Logs.LogProvider = new XUnitLogProvider(helper);
|
||||
}
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task GetRedirectedToLoginPathOnChallenge()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
await tester.StartAsync();
|
||||
var client = tester.PayTester.HttpClient;
|
||||
//Wallets endpoint is protected
|
||||
var response = await client.GetAsync("wallets");
|
||||
var urlPath = response.RequestMessage.RequestUri.ToString()
|
||||
.Replace(tester.PayTester.ServerUri.ToString(), "");
|
||||
//Cookie Challenge redirects you to login page
|
||||
Assert.StartsWith("Account/Login", urlPath, StringComparison.InvariantCultureIgnoreCase);
|
||||
|
||||
var queryString = response.RequestMessage.RequestUri.ParseQueryString();
|
||||
|
||||
Assert.NotNull(queryString["ReturnUrl"]);
|
||||
Assert.Equal("/wallets", queryString["ReturnUrl"]);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanGetOpenIdConfiguration()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
await tester.StartAsync();
|
||||
using (var response =
|
||||
await tester.PayTester.HttpClient.GetAsync("/.well-known/openid-configuration"))
|
||||
{
|
||||
using (var streamToReadFrom = new StreamReader(await response.Content.ReadAsStreamAsync()))
|
||||
{
|
||||
var json = await streamToReadFrom.ReadToEndAsync();
|
||||
Assert.NotNull(json);
|
||||
JObject.Parse(json); // Should do more tests but good enough
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanUseNonInteractiveFlows()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
await tester.StartAsync();
|
||||
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
await user.MakeAdmin();
|
||||
var token = await RegisterPasswordClientAndGetAccessToken(user, null, tester);
|
||||
await TestApiAgainstAccessToken(token, tester, user);
|
||||
token = await RegisterPasswordClientAndGetAccessToken(user, "secret", tester);
|
||||
await TestApiAgainstAccessToken(token, tester, user);
|
||||
token = await RegisterClientCredentialsFlowAndGetAccessToken(user, "secret", tester);
|
||||
await TestApiAgainstAccessToken(token, tester, user);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Selenium", "Selenium")]
|
||||
public async Task CanUseImplicitFlow()
|
||||
{
|
||||
using (var s = SeleniumTester.Create())
|
||||
{
|
||||
await s.StartAsync();
|
||||
var tester = s.Server;
|
||||
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
await user.MakeAdmin();
|
||||
var id = Guid.NewGuid().ToString();
|
||||
var redirecturi = new Uri("http://127.0.0.1/oidc-callback");
|
||||
var openIdClient = await user.RegisterOpenIdClient(
|
||||
new OpenIddictApplicationDescriptor()
|
||||
{
|
||||
ClientId = id,
|
||||
DisplayName = id,
|
||||
Permissions = {OpenIddictConstants.Permissions.GrantTypes.Implicit},
|
||||
RedirectUris = {redirecturi},
|
||||
|
||||
});
|
||||
var implicitAuthorizeUrl = new Uri(tester.PayTester.ServerUri,
|
||||
$"connect/authorize?response_type=token&client_id={id}&redirect_uri={redirecturi.AbsoluteUri}&scope=openid server_management store_management&nonce={Guid.NewGuid().ToString()}");
|
||||
s.Driver.Navigate().GoToUrl(implicitAuthorizeUrl);
|
||||
s.Login(user.RegisterDetails.Email, user.RegisterDetails.Password);
|
||||
s.Driver.FindElement(By.Id("consent-yes")).Click();
|
||||
var url = s.Driver.Url;
|
||||
var results = url.Split("#").Last().Split("&")
|
||||
.ToDictionary(s1 => s1.Split("=")[0], s1 => s1.Split("=")[1]);
|
||||
await TestApiAgainstAccessToken(results["access_token"], tester, user);
|
||||
//in Implicit mode, you renew your token by hitting the same endpoint but adding prompt=none. If you are still logged in on the site, you will receive a fresh token.
|
||||
var implicitAuthorizeUrlSilentModel = new Uri($"{implicitAuthorizeUrl.OriginalString}&prompt=none");
|
||||
s.Driver.Navigate().GoToUrl(implicitAuthorizeUrlSilentModel);
|
||||
url = s.Driver.Url;
|
||||
results = url.Split("#").Last().Split("&").ToDictionary(s1 => s1.Split("=")[0], s1 => s1.Split("=")[1]);
|
||||
await TestApiAgainstAccessToken(results["access_token"], tester, user);
|
||||
|
||||
var stores = await TestApiAgainstAccessToken<StoreData[]>(results["access_token"],
|
||||
$"api/test/me/stores",
|
||||
tester.PayTester.HttpClient);
|
||||
Assert.NotEmpty(stores);
|
||||
|
||||
Assert.True(await TestApiAgainstAccessToken<bool>(results["access_token"],
|
||||
$"api/test/me/stores/{stores[0].Id}/can-edit",
|
||||
tester.PayTester.HttpClient));
|
||||
|
||||
//we dont ask for consent after acquiring it the first time for the same scopes.
|
||||
LogoutFlow(tester, id, s);
|
||||
s.Driver.Navigate().GoToUrl(implicitAuthorizeUrl);
|
||||
s.Login(user.RegisterDetails.Email, user.RegisterDetails.Password);
|
||||
s.Driver.AssertElementNotFound(By.Id("consent-yes"));
|
||||
|
||||
// Let's asks without scopes
|
||||
LogoutFlow(tester, id, s);
|
||||
id = Guid.NewGuid().ToString();
|
||||
openIdClient = await user.RegisterOpenIdClient(
|
||||
new OpenIddictApplicationDescriptor()
|
||||
{
|
||||
ClientId = id,
|
||||
DisplayName = id,
|
||||
Permissions = { OpenIddictConstants.Permissions.GrantTypes.Implicit },
|
||||
RedirectUris = { redirecturi },
|
||||
});
|
||||
implicitAuthorizeUrl = new Uri(tester.PayTester.ServerUri,
|
||||
$"connect/authorize?response_type=token&client_id={id}&redirect_uri={redirecturi.AbsoluteUri}&scope=openid&nonce={Guid.NewGuid().ToString()}");
|
||||
s.Driver.Navigate().GoToUrl(implicitAuthorizeUrl);
|
||||
s.Login(user.RegisterDetails.Email, user.RegisterDetails.Password);
|
||||
s.Driver.FindElement(By.Id("consent-yes")).Click();
|
||||
results = s.Driver.Url.Split("#").Last().Split("&")
|
||||
.ToDictionary(s1 => s1.Split("=")[0], s1 => s1.Split("=")[1]);
|
||||
|
||||
await Assert.ThrowsAnyAsync<HttpRequestException>(async () =>
|
||||
{
|
||||
await TestApiAgainstAccessToken<StoreData[]>(results["access_token"],
|
||||
$"api/test/me/stores",
|
||||
tester.PayTester.HttpClient);
|
||||
});
|
||||
await Assert.ThrowsAnyAsync<HttpRequestException>(async () =>
|
||||
{
|
||||
await TestApiAgainstAccessToken<bool>(results["access_token"],
|
||||
$"api/test/me/stores/{stores[0].Id}/can-edit",
|
||||
tester.PayTester.HttpClient);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void LogoutFlow(ServerTester tester, string clientId, SeleniumTester seleniumTester)
|
||||
{
|
||||
var logoutUrl = new Uri(tester.PayTester.ServerUri,
|
||||
$"connect/logout?response_type=token&client_id={clientId}");
|
||||
seleniumTester.Driver.Navigate().GoToUrl(logoutUrl);
|
||||
seleniumTester.GoToHome();
|
||||
Assert.Throws<NoSuchElementException>(() => seleniumTester.Driver.FindElement(By.Id("Logout")));
|
||||
|
||||
}
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Selenium", "Selenium")]
|
||||
public async Task CanUseCodeFlow()
|
||||
{
|
||||
using (var s = SeleniumTester.Create())
|
||||
{
|
||||
await s.StartAsync();
|
||||
var tester = s.Server;
|
||||
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
await user.MakeAdmin();
|
||||
var id = Guid.NewGuid().ToString();
|
||||
var redirecturi = new Uri("http://127.0.0.1/oidc-callback");
|
||||
var secret = "secret";
|
||||
var openIdClient = await user.RegisterOpenIdClient(
|
||||
new OpenIddictApplicationDescriptor()
|
||||
{
|
||||
ClientId = id,
|
||||
DisplayName = id,
|
||||
Permissions =
|
||||
{
|
||||
OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode,
|
||||
OpenIddictConstants.Permissions.GrantTypes.RefreshToken
|
||||
},
|
||||
RedirectUris = {redirecturi}
|
||||
}, secret);
|
||||
var authorizeUrl = new Uri(tester.PayTester.ServerUri,
|
||||
$"connect/authorize?response_type=code&client_id={id}&redirect_uri={redirecturi.AbsoluteUri}&scope=openid offline_access server_management store_management&state={Guid.NewGuid().ToString()}");
|
||||
s.Driver.Navigate().GoToUrl(authorizeUrl);
|
||||
s.Login(user.RegisterDetails.Email, user.RegisterDetails.Password);
|
||||
s.Driver.FindElement(By.Id("consent-yes")).Click();
|
||||
var url = s.Driver.Url;
|
||||
var results = url.Split("?").Last().Split("&")
|
||||
.ToDictionary(s1 => s1.Split("=")[0], s1 => s1.Split("=")[1]);
|
||||
|
||||
var httpClient = tester.PayTester.HttpClient;
|
||||
|
||||
var httpRequest = new HttpRequestMessage(HttpMethod.Post,
|
||||
new Uri(tester.PayTester.ServerUri, "/connect/token"))
|
||||
{
|
||||
Content = new FormUrlEncodedContent(new List<KeyValuePair<string, string>>()
|
||||
{
|
||||
new KeyValuePair<string, string>("grant_type",
|
||||
OpenIddictConstants.GrantTypes.AuthorizationCode),
|
||||
new KeyValuePair<string, string>("client_id", openIdClient.ClientId),
|
||||
new KeyValuePair<string, string>("client_secret", secret),
|
||||
new KeyValuePair<string, string>("code", results["code"]),
|
||||
new KeyValuePair<string, string>("redirect_uri", redirecturi.AbsoluteUri)
|
||||
})
|
||||
};
|
||||
|
||||
|
||||
var response = await httpClient.SendAsync(httpRequest);
|
||||
|
||||
Assert.True(response.IsSuccessStatusCode);
|
||||
|
||||
string content = await response.Content.ReadAsStringAsync();
|
||||
var result = System.Text.Json.JsonSerializer.Deserialize<OpenIddictResponse>(content);
|
||||
|
||||
await TestApiAgainstAccessToken(result.AccessToken, tester, user);
|
||||
|
||||
var refreshedAccessToken = await RefreshAnAccessToken(result.RefreshToken, httpClient, id, secret);
|
||||
|
||||
await TestApiAgainstAccessToken(refreshedAccessToken, tester, user);
|
||||
|
||||
LogoutFlow(tester, id, s);
|
||||
s.Driver.Navigate().GoToUrl(authorizeUrl);
|
||||
s.Login(user.RegisterDetails.Email, user.RegisterDetails.Password);
|
||||
|
||||
Assert.Throws<NoSuchElementException>(() => s.Driver.FindElement(By.Id("consent-yes")));
|
||||
results = url.Split("?").Last().Split("&")
|
||||
.ToDictionary(s1 => s1.Split("=")[0], s1 => s1.Split("=")[1]);
|
||||
Assert.True(results.ContainsKey("code"));
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<string> RefreshAnAccessToken(string refreshToken, HttpClient client, string clientId,
|
||||
string clientSecret = null)
|
||||
{
|
||||
var httpRequest = new HttpRequestMessage(HttpMethod.Post,
|
||||
new Uri(client.BaseAddress, "/connect/token"))
|
||||
{
|
||||
Content = new FormUrlEncodedContent(new List<KeyValuePair<string, string>>()
|
||||
{
|
||||
new KeyValuePair<string, string>("grant_type",
|
||||
OpenIddictConstants.GrantTypes.RefreshToken),
|
||||
new KeyValuePair<string, string>("client_id", clientId),
|
||||
new KeyValuePair<string, string>("client_secret", clientSecret),
|
||||
new KeyValuePair<string, string>("refresh_token", refreshToken)
|
||||
})
|
||||
};
|
||||
|
||||
var response = await client.SendAsync(httpRequest);
|
||||
|
||||
Assert.True(response.IsSuccessStatusCode);
|
||||
|
||||
string content = await response.Content.ReadAsStringAsync();
|
||||
var result = System.Text.Json.JsonSerializer.Deserialize<OpenIddictResponse>(content);
|
||||
Assert.NotEmpty(result.AccessToken);
|
||||
Assert.Null(result.Error);
|
||||
return result.AccessToken;
|
||||
}
|
||||
|
||||
private static async Task<string> RegisterClientCredentialsFlowAndGetAccessToken(TestAccount user,
|
||||
string secret,
|
||||
ServerTester tester)
|
||||
{
|
||||
var id = Guid.NewGuid().ToString();
|
||||
var openIdClient = await user.RegisterOpenIdClient(
|
||||
new OpenIddictApplicationDescriptor()
|
||||
{
|
||||
ClientId = id,
|
||||
DisplayName = id,
|
||||
Permissions = {OpenIddictConstants.Permissions.GrantTypes.ClientCredentials}
|
||||
}, secret);
|
||||
|
||||
|
||||
var httpClient = tester.PayTester.HttpClient;
|
||||
|
||||
var httpRequest = new HttpRequestMessage(HttpMethod.Post,
|
||||
new Uri(tester.PayTester.ServerUri, "/connect/token"))
|
||||
{
|
||||
Content = new FormUrlEncodedContent(new List<KeyValuePair<string, string>>()
|
||||
{
|
||||
new KeyValuePair<string, string>("grant_type",
|
||||
OpenIddictConstants.GrantTypes.ClientCredentials),
|
||||
new KeyValuePair<string, string>("client_id", openIdClient.ClientId),
|
||||
new KeyValuePair<string, string>("client_secret", secret),
|
||||
new KeyValuePair<string, string>("scope", "server_management store_management")
|
||||
})
|
||||
};
|
||||
|
||||
|
||||
var response = await httpClient.SendAsync(httpRequest);
|
||||
|
||||
Assert.True(response.IsSuccessStatusCode);
|
||||
|
||||
string content = await response.Content.ReadAsStringAsync();
|
||||
var result = System.Text.Json.JsonSerializer.Deserialize<OpenIddictResponse>(content);
|
||||
Assert.NotEmpty(result.AccessToken);
|
||||
Assert.Null(result.Error);
|
||||
return result.AccessToken;
|
||||
}
|
||||
|
||||
private static async Task<string> RegisterPasswordClientAndGetAccessToken(TestAccount user, string secret,
|
||||
ServerTester tester)
|
||||
{
|
||||
var id = Guid.NewGuid().ToString();
|
||||
var openIdClient = await user.RegisterOpenIdClient(
|
||||
new OpenIddictApplicationDescriptor()
|
||||
{
|
||||
ClientId = id,
|
||||
DisplayName = id,
|
||||
Permissions = {OpenIddictConstants.Permissions.GrantTypes.Password}
|
||||
}, secret);
|
||||
|
||||
|
||||
var httpClient = tester.PayTester.HttpClient;
|
||||
|
||||
var httpRequest = new HttpRequestMessage(HttpMethod.Post,
|
||||
new Uri(tester.PayTester.ServerUri, "/connect/token"))
|
||||
{
|
||||
Content = new FormUrlEncodedContent(new List<KeyValuePair<string, string>>()
|
||||
{
|
||||
new KeyValuePair<string, string>("grant_type", OpenIddictConstants.GrantTypes.Password),
|
||||
new KeyValuePair<string, string>("username", user.RegisterDetails.Email),
|
||||
new KeyValuePair<string, string>("password", user.RegisterDetails.Password),
|
||||
new KeyValuePair<string, string>("client_id", openIdClient.ClientId),
|
||||
new KeyValuePair<string, string>("client_secret", secret),
|
||||
new KeyValuePair<string, string>("scope", "server_management store_management")
|
||||
})
|
||||
};
|
||||
|
||||
|
||||
var response = await httpClient.SendAsync(httpRequest);
|
||||
|
||||
Assert.True(response.IsSuccessStatusCode);
|
||||
|
||||
string content = await response.Content.ReadAsStringAsync();
|
||||
var result = System.Text.Json.JsonSerializer.Deserialize<OpenIddictResponse>(content);
|
||||
Assert.NotEmpty(result.AccessToken);
|
||||
Assert.Null(result.Error);
|
||||
return result.AccessToken;
|
||||
}
|
||||
|
||||
async Task TestApiAgainstAccessToken(string accessToken, ServerTester tester, TestAccount testAccount)
|
||||
{
|
||||
var resultUser =
|
||||
await TestApiAgainstAccessToken<string>(accessToken, "api/test/me/id",
|
||||
tester.PayTester.HttpClient);
|
||||
Assert.Equal(testAccount.UserId, resultUser);
|
||||
|
||||
var secondUser = tester.NewAccount();
|
||||
secondUser.GrantAccess();
|
||||
|
||||
var resultStores =
|
||||
await TestApiAgainstAccessToken<StoreData[]>(accessToken, "api/test/me/stores",
|
||||
tester.PayTester.HttpClient);
|
||||
Assert.Contains(resultStores,
|
||||
data => data.Id.Equals(testAccount.StoreId, StringComparison.InvariantCultureIgnoreCase));
|
||||
Assert.DoesNotContain(resultStores,
|
||||
data => data.Id.Equals(secondUser.StoreId, StringComparison.InvariantCultureIgnoreCase));
|
||||
|
||||
Assert.True(await TestApiAgainstAccessToken<bool>(accessToken,
|
||||
$"api/test/me/stores/{testAccount.StoreId}/can-edit",
|
||||
tester.PayTester.HttpClient));
|
||||
|
||||
Assert.True(await TestApiAgainstAccessToken<bool>(accessToken,
|
||||
$"api/test/me/is-admin",
|
||||
tester.PayTester.HttpClient));
|
||||
|
||||
await Assert.ThrowsAnyAsync<HttpRequestException>(async () =>
|
||||
{
|
||||
await TestApiAgainstAccessToken<bool>(accessToken, $"api/test/me/stores/{secondUser.StoreId}/can-edit",
|
||||
tester.PayTester.HttpClient);
|
||||
});
|
||||
}
|
||||
|
||||
public async Task<T> TestApiAgainstAccessToken<T>(string accessToken, string url, HttpClient client)
|
||||
{
|
||||
var httpRequest = new HttpRequestMessage(HttpMethod.Get,
|
||||
new Uri(client.BaseAddress, url));
|
||||
httpRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
|
||||
var result = await client.SendAsync(httpRequest);
|
||||
result.EnsureSuccessStatusCode();
|
||||
|
||||
var rawJson = await result.Content.ReadAsStringAsync();
|
||||
if (typeof(T).IsPrimitive || typeof(T) == typeof(string))
|
||||
{
|
||||
return (T)Convert.ChangeType(rawJson, typeof(T));
|
||||
}
|
||||
|
||||
return JsonConvert.DeserializeObject<T>(rawJson);
|
||||
}
|
||||
}
|
||||
}
|
@ -32,7 +32,6 @@ using System.Security.Claims;
|
||||
using System.Security.Principal;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using OpenIddict.Abstractions;
|
||||
using Xunit;
|
||||
using BTCPayServer.Services;
|
||||
using System.Net.Http;
|
||||
@ -298,7 +297,7 @@ namespace BTCPayServer.Tests
|
||||
if (userId != null)
|
||||
{
|
||||
List<Claim> claims = new List<Claim>();
|
||||
claims.Add(new Claim(OpenIddictConstants.Claims.Subject, userId));
|
||||
claims.Add(new Claim(ClaimTypes.NameIdentifier, userId));
|
||||
if (isAdmin)
|
||||
claims.Add(new Claim(ClaimTypes.Role, Roles.ServerAdmin));
|
||||
context.User = new ClaimsPrincipal(new ClaimsIdentity(claims.ToArray(), AuthenticationSchemes.Cookie));
|
||||
|
@ -71,7 +71,7 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
catch { }
|
||||
|
||||
s.Driver.AssertElementNotFound(By.Id("emailAddressFormInput"));
|
||||
s.Driver.AssertElementNotFound(By.Id("emailAddressFormInput"));
|
||||
s.Driver.Navigate().Refresh();
|
||||
s.Driver.AssertElementNotFound(By.Id("emailAddressFormInput"));
|
||||
}
|
||||
@ -92,7 +92,7 @@ namespace BTCPayServer.Tests
|
||||
s.GoToInvoiceCheckout(invoiceId);
|
||||
Assert.True(s.Driver.FindElement(By.Id("DefaultLang")).FindElements(By.TagName("option")).Count > 1);
|
||||
var payWithTextEnglish = s.Driver.FindElement(By.Id("pay-with-text")).Text;
|
||||
|
||||
|
||||
var prettyDropdown = s.Driver.FindElement(By.Id("prettydropdown-DefaultLang"));
|
||||
prettyDropdown.Click();
|
||||
await Task.Delay(200);
|
||||
@ -100,13 +100,13 @@ namespace BTCPayServer.Tests
|
||||
await Task.Delay(1000);
|
||||
Assert.NotEqual(payWithTextEnglish, s.Driver.FindElement(By.Id("pay-with-text")).Text);
|
||||
s.Driver.Navigate().GoToUrl(s.Driver.Url + "?lang=da-DK");
|
||||
|
||||
|
||||
Assert.NotEqual(payWithTextEnglish, s.Driver.FindElement(By.Id("pay-with-text")).Text);
|
||||
|
||||
|
||||
s.Driver.Quit();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Altcoins", "Altcoins")]
|
||||
[Trait("Lightning", "Lightning")]
|
||||
@ -121,7 +121,7 @@ namespace BTCPayServer.Tests
|
||||
s.RegisterNewUser();
|
||||
var store = s.CreateNewStore();
|
||||
s.AddDerivationScheme("BTC");
|
||||
|
||||
|
||||
//check that there is no dropdown since only one payment method is set
|
||||
var invoiceId = s.CreateInvoice(store.storeName, 10, "USD", "a@g.com");
|
||||
s.GoToInvoiceCheckout(invoiceId);
|
||||
@ -129,33 +129,31 @@ namespace BTCPayServer.Tests
|
||||
s.GoToHome();
|
||||
s.GoToStore(store.storeId);
|
||||
s.AddDerivationScheme("LTC");
|
||||
s.AddLightningNode("BTC",LightningConnectionType.CLightning);
|
||||
s.AddLightningNode("BTC", LightningConnectionType.CLightning);
|
||||
//there should be three now
|
||||
invoiceId = s.CreateInvoice(store.storeName, 10, "USD", "a@g.com");
|
||||
s.GoToInvoiceCheckout(invoiceId);
|
||||
var currencyDropdownButton = s.Driver.FindElement(By.ClassName("payment__currencies"));
|
||||
var currencyDropdownButton = s.Driver.WaitForElement(By.ClassName("payment__currencies"));
|
||||
Assert.Contains("BTC", currencyDropdownButton.Text);
|
||||
currencyDropdownButton.Click();
|
||||
|
||||
|
||||
var elements = s.Driver.FindElement(By.ClassName("vex-content")).FindElements(By.ClassName("vexmenuitem"));
|
||||
Assert.Equal(3, elements.Count);
|
||||
elements.Single(element => element.Text.Contains("LTC")).Click();
|
||||
Thread.Sleep(1000);
|
||||
currencyDropdownButton = s.Driver.FindElement(By.ClassName("payment__currencies"));
|
||||
currencyDropdownButton = s.Driver.WaitForElement(By.ClassName("payment__currencies"));
|
||||
Assert.Contains("LTC", currencyDropdownButton.Text);
|
||||
currencyDropdownButton.Click();
|
||||
|
||||
elements = s.Driver.FindElement(By.ClassName("vex-content")).FindElements(By.ClassName("vexmenuitem"));
|
||||
elements.Single(element => element.Text.Contains("Lightning")).Click();
|
||||
Thread.Sleep(1000);
|
||||
currencyDropdownButton = s.Driver.FindElement(By.ClassName("payment__currencies"));
|
||||
|
||||
currencyDropdownButton = s.Driver.WaitForElement(By.ClassName("payment__currencies"));
|
||||
Assert.Contains("Lightning", currencyDropdownButton.Text);
|
||||
|
||||
|
||||
s.Driver.Quit();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Lightning", "Lightning")]
|
||||
public async Task CanUseLightningSatsFeature()
|
||||
@ -171,12 +169,12 @@ namespace BTCPayServer.Tests
|
||||
s.GoToStore(store.storeId, StoreNavPages.Checkout);
|
||||
s.SetCheckbox(s, "LightningAmountInSatoshi", true);
|
||||
var command = s.Driver.FindElement(By.Name("command"));
|
||||
|
||||
|
||||
command.ForceClick();
|
||||
var invoiceId = s.CreateInvoice(store.storeName, 10, "USD", "a@g.com");
|
||||
s.GoToInvoiceCheckout(invoiceId);
|
||||
Assert.Contains("Sats", s.Driver.FindElement(By.ClassName("payment__currencies_noborder")).Text);
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -218,4 +216,30 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class SeleniumExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Utility method to wait until timeout for element to be present (optionally displayed)
|
||||
/// </summary>
|
||||
/// <param name="context">Wait context</param>
|
||||
/// <param name="by">How we search for element</param>
|
||||
/// <param name="displayed">Flag to wait for element to be displayed or just present</param>
|
||||
/// <param name="timeout">How long to wait for element to be present/displayed</param>
|
||||
/// <returns>Element we were waiting for</returns>
|
||||
public static IWebElement WaitForElement(this IWebDriver context, By by, bool displayed = true, uint timeout = 3)
|
||||
{
|
||||
var wait = new DefaultWait<IWebDriver>(context);
|
||||
wait.Timeout = TimeSpan.FromSeconds(timeout);
|
||||
wait.IgnoreExceptionTypes(typeof(NoSuchElementException));
|
||||
return wait.Until(ctx =>
|
||||
{
|
||||
var elem = ctx.FindElement(by);
|
||||
if (displayed && !elem.Displayed)
|
||||
return null;
|
||||
|
||||
return elem;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
71
BTCPayServer.Tests/GreenfieldAPITests.cs
Normal file
71
BTCPayServer.Tests/GreenfieldAPITests.cs
Normal file
@ -0,0 +1,71 @@
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Controllers.RestApi.ApiKeys;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Security.APIKeys;
|
||||
using BTCPayServer.Tests.Logging;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
public class GreenfieldAPITests
|
||||
{
|
||||
public const int TestTimeout = TestUtils.TestTimeout;
|
||||
|
||||
public const string TestApiPath = "api/test/apikey";
|
||||
|
||||
public GreenfieldAPITests(ITestOutputHelper helper)
|
||||
{
|
||||
Logs.Tester = new XUnitLog(helper) {Name = "Tests"};
|
||||
Logs.LogProvider = new XUnitLogProvider(helper);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task ApiKeysControllerTests()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
await tester.StartAsync();
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
await user.MakeAdmin();
|
||||
string apiKey = await GenerateAPIKey(tester, user);
|
||||
|
||||
//Get current api key
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, "api/v1/api-keys/current");
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("token", apiKey);
|
||||
var result = await tester.PayTester.HttpClient.SendAsync(request);
|
||||
Assert.True(result.IsSuccessStatusCode);
|
||||
var apiKeyData = JObject.Parse(await result.Content.ReadAsStringAsync()).ToObject<ApiKeyData>();
|
||||
Assert.NotNull(apiKeyData);
|
||||
Assert.Equal(apiKey, apiKeyData.ApiKey);
|
||||
Assert.Equal(user.UserId, apiKeyData.UserId);
|
||||
Assert.Equal(2, apiKeyData.Permissions.Length);
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<string> GenerateAPIKey(ServerTester tester, TestAccount user)
|
||||
{
|
||||
var manageController = tester.PayTester.GetController<ManageController>(user.UserId, user.StoreId, user.IsAdmin);
|
||||
var x = Assert.IsType<RedirectToActionResult>(await manageController.AddApiKey(
|
||||
new ManageController.AddApiKeyViewModel()
|
||||
{
|
||||
ServerManagementPermission = true,
|
||||
StoreManagementPermission = true,
|
||||
StoreMode = ManageController.AddApiKeyViewModel.ApiKeyStoreMode.AllStores
|
||||
}));
|
||||
var statusMessage = manageController.TempData.GetStatusMessageModel();
|
||||
Assert.NotNull(statusMessage);
|
||||
var apiKey = statusMessage.Html.Substring(statusMessage.Html.IndexOf("<code>") + 6);
|
||||
apiKey = apiKey.Substring(0, apiKey.IndexOf("</code>") );
|
||||
return apiKey;
|
||||
}
|
||||
}
|
||||
}
|
@ -19,6 +19,7 @@ using System.Threading.Tasks;
|
||||
using BTCPayServer.Lightning;
|
||||
using BTCPayServer.Lightning.CLightning;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Views.Manage;
|
||||
using BTCPayServer.Views.Stores;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
@ -71,19 +72,20 @@ namespace BTCPayServer.Tests
|
||||
Driver.AssertNoError();
|
||||
}
|
||||
|
||||
internal void AssertHappyMessage(StatusMessageModel.StatusSeverity severity = StatusMessageModel.StatusSeverity.Success)
|
||||
internal IWebElement AssertHappyMessage(StatusMessageModel.StatusSeverity severity = StatusMessageModel.StatusSeverity.Success)
|
||||
{
|
||||
using var cts = new CancellationTokenSource(20_000);
|
||||
while (!cts.IsCancellationRequested)
|
||||
{
|
||||
var success = Driver.FindElements(By.ClassName($"alert-{StatusMessageModel.ToString(severity)}")).Any(el => el.Displayed);
|
||||
if (success)
|
||||
return;
|
||||
var result = Driver.FindElements(By.ClassName($"alert-{StatusMessageModel.ToString(severity)}")).Where(el => el.Displayed);
|
||||
if (result.Any())
|
||||
return result.First();
|
||||
Thread.Sleep(100);
|
||||
}
|
||||
Logs.Tester.LogInformation(this.Driver.PageSource);
|
||||
Assert.True(false, $"Should have shown {severity} message");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static readonly TimeSpan ImplicitWait = TimeSpan.FromSeconds(10);
|
||||
public string Link(string relativeLink)
|
||||
@ -271,6 +273,20 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
Driver.FindElement(By.Id("Invoices")).Click();
|
||||
}
|
||||
|
||||
public void GoToProfile(ManageNavPages navPages = ManageNavPages.Index)
|
||||
{
|
||||
Driver.FindElement(By.Id("MySettings")).Click();
|
||||
if (navPages != ManageNavPages.Index)
|
||||
{
|
||||
Driver.FindElement(By.Id(navPages.ToString())).Click();
|
||||
}
|
||||
}
|
||||
|
||||
public void GoToLogin()
|
||||
{
|
||||
Driver.Navigate().GoToUrl(new Uri(Server.PayTester.ServerUri, "Account/Login"));
|
||||
}
|
||||
|
||||
public void GoToCreateInvoicePage()
|
||||
{
|
||||
|
@ -53,6 +53,9 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
NBXplorerUri = ExplorerClient.Address,
|
||||
TestDatabase = Enum.Parse<TestDatabases>(GetEnvironment("TESTS_DB", TestDatabases.Postgres.ToString()), true),
|
||||
// TODO: The fact that we use same conn string as development database can cause huge problems with tests
|
||||
// since in dev we already can have some users / stores registered, while on CI database is being initalized
|
||||
// for the first time and first registered user gets admin status by default
|
||||
Postgres = GetEnvironment("TESTS_POSTGRES", "User ID=postgres;Host=127.0.0.1;Port=39372;Database=btcpayserver"),
|
||||
MySQL = GetEnvironment("TESTS_MYSQL", "User ID=root;Host=127.0.0.1;Port=33036;Database=btcpayserver")
|
||||
};
|
||||
|
@ -18,8 +18,6 @@ using BTCPayServer.Tests.Logging;
|
||||
using BTCPayServer.Lightning;
|
||||
using BTCPayServer.Lightning.CLightning;
|
||||
using BTCPayServer.Data;
|
||||
using OpenIddict.Abstractions;
|
||||
using OpenIddict.Core;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using NBXplorer.Models;
|
||||
|
||||
@ -148,6 +146,7 @@ namespace BTCPayServer.Tests
|
||||
};
|
||||
await account.Register(RegisterDetails);
|
||||
UserId = account.RegisteredUserId;
|
||||
IsAdmin = account.RegisteredAdmin;
|
||||
}
|
||||
|
||||
public RegisterViewModel RegisterDetails{ get; set; }
|
||||
@ -194,14 +193,5 @@ namespace BTCPayServer.Tests
|
||||
if (storeController.ModelState.ErrorCount != 0)
|
||||
Assert.False(true, storeController.ModelState.FirstOrDefault().Value.Errors[0].ErrorMessage);
|
||||
}
|
||||
|
||||
public async Task<BTCPayOpenIdClient> RegisterOpenIdClient(OpenIddictApplicationDescriptor descriptor, string secret = null)
|
||||
{
|
||||
var openIddictApplicationManager = parent.PayTester.GetService<OpenIddictApplicationManager<BTCPayOpenIdClient>>();
|
||||
var client = new BTCPayOpenIdClient { Id = Guid.NewGuid().ToString(), ApplicationUserId = UserId};
|
||||
await openIddictApplicationManager.PopulateAsync(client, descriptor);
|
||||
await openIddictApplicationManager.CreateAsync(client, secret);
|
||||
return client;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -48,6 +48,7 @@
|
||||
<PackageReference Include="NicolasDorier.CommandLine.Configuration" Version="1.0.0.3" />
|
||||
<PackageReference Include="NicolasDorier.RateLimits" Version="1.1.0" />
|
||||
<PackageReference Include="NicolasDorier.StandardConfiguration" Version="1.0.0.18" />
|
||||
<PackageReference Include="NSwag.AspNetCore" Version="13.2.2" />
|
||||
<PackageReference Include="Serilog" Version="2.9.0" />
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="3.2.0" />
|
||||
<PackageReference Include="Serilog.Sinks.File" Version="4.1.0" />
|
||||
@ -66,9 +67,6 @@
|
||||
<PackageReference Include="TwentyTwenty.Storage.Local" Version="2.12.1" />
|
||||
<PackageReference Include="U2F.Core" Version="1.0.4" />
|
||||
<PackageReference Include="YamlDotNet" Version="8.0.0" />
|
||||
<PackageReference Include="OpenIddict" Version="3.0.0-alpha1.20058.15" />
|
||||
<PackageReference Include="OpenIddict.Server.AspNetCore" Version="3.0.0-alpha1.20058.15"></PackageReference>
|
||||
<PackageReference Include="OpenIddict.Validation.AspNetCore" Version="3.0.0-alpha1.20058.15"></PackageReference>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="3.1.1" Condition="'$(Configuration)' == 'Debug'" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="3.1.1" />
|
||||
</ItemGroup>
|
||||
@ -220,4 +218,8 @@
|
||||
<Pack>$(IncludeRazorContentInPack)</Pack>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<_ContentIncludedByDefault Remove="Views\Authorization\Authorize.cshtml" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
@ -39,7 +39,7 @@ namespace BTCPayServer.Controllers
|
||||
SettingsRepository _SettingsRepository;
|
||||
Configuration.BTCPayServerOptions _Options;
|
||||
private readonly BTCPayServerEnvironment _btcPayServerEnvironment;
|
||||
public U2FService _u2FService;
|
||||
public U2FService _u2FService;
|
||||
ILogger _logger;
|
||||
|
||||
public AccountController(
|
||||
@ -75,7 +75,7 @@ namespace BTCPayServer.Controllers
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> Login(string returnUrl = null)
|
||||
{
|
||||
|
||||
|
||||
if (User.Identity.IsAuthenticated && string.IsNullOrEmpty(returnUrl))
|
||||
return RedirectToLocal();
|
||||
// Clear the existing external cookie to ensure a clean login process
|
||||
@ -85,7 +85,7 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
SetInsecureFlags();
|
||||
}
|
||||
|
||||
|
||||
ViewData["ReturnUrl"] = returnUrl;
|
||||
return View();
|
||||
}
|
||||
@ -126,7 +126,7 @@ namespace BTCPayServer.Controllers
|
||||
if (await _userManager.CheckPasswordAsync(user, model.Password))
|
||||
{
|
||||
LoginWith2faViewModel twoFModel = null;
|
||||
|
||||
|
||||
if (user.TwoFactorEnabled)
|
||||
{
|
||||
// we need to do an actual sign in attempt so that 2fa can function in next step
|
||||
@ -145,14 +145,14 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
else
|
||||
{
|
||||
var incrementAccessFailedResult = await _userManager.AccessFailedAsync(user);
|
||||
ModelState.AddModelError(string.Empty, "Invalid login attempt.");
|
||||
return View(model);
|
||||
|
||||
var incrementAccessFailedResult = await _userManager.AccessFailedAsync(user);
|
||||
ModelState.AddModelError(string.Empty, "Invalid login attempt.");
|
||||
return View(model);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
var result = await _signInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, lockoutOnFailure: true);
|
||||
if (result.Succeeded)
|
||||
{
|
||||
@ -215,7 +215,7 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
return RedirectToAction("Login");
|
||||
}
|
||||
|
||||
|
||||
ViewData["ReturnUrl"] = returnUrl;
|
||||
var user = await _userManager.FindByIdAsync(viewModel.UserId);
|
||||
|
||||
@ -276,7 +276,7 @@ namespace BTCPayServer.Controllers
|
||||
return View("SecondaryLogin", new SecondaryLoginViewModel()
|
||||
{
|
||||
LoginWith2FaViewModel = new LoginWith2faViewModel { RememberMe = rememberMe },
|
||||
LoginWithU2FViewModel = (await _u2FService.HasDevices(user.Id))? await BuildU2FViewModel(rememberMe, user): null
|
||||
LoginWithU2FViewModel = (await _u2FService.HasDevices(user.Id)) ? await BuildU2FViewModel(rememberMe, user) : null
|
||||
});
|
||||
}
|
||||
|
||||
@ -322,7 +322,7 @@ namespace BTCPayServer.Controllers
|
||||
return View("SecondaryLogin", new SecondaryLoginViewModel()
|
||||
{
|
||||
LoginWith2FaViewModel = model,
|
||||
LoginWithU2FViewModel = (await _u2FService.HasDevices(user.Id))? await BuildU2FViewModel(rememberMe, user): null
|
||||
LoginWithU2FViewModel = (await _u2FService.HasDevices(user.Id)) ? await BuildU2FViewModel(rememberMe, user) : null
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -447,12 +447,13 @@ namespace BTCPayServer.Controllers
|
||||
var settings = await _SettingsRepository.GetSettingAsync<ThemeSettings>();
|
||||
settings.FirstRun = false;
|
||||
await _SettingsRepository.UpdateSetting<ThemeSettings>(settings);
|
||||
if(_Options.DisableRegistration)
|
||||
if (_Options.DisableRegistration)
|
||||
{
|
||||
// Once the admin user has been created lock subsequent user registrations (needs to be disabled for unit tests that require multiple users).
|
||||
policies.LockSubscription = true;
|
||||
await _SettingsRepository.UpdateSetting(policies);
|
||||
}
|
||||
RegisteredAdmin = true;
|
||||
}
|
||||
|
||||
var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
|
||||
@ -462,7 +463,7 @@ namespace BTCPayServer.Controllers
|
||||
_EmailSenderFactory.GetEmailSender().SendEmailConfirmation(model.Email, callbackUrl);
|
||||
if (!policies.RequiresConfirmedEmail)
|
||||
{
|
||||
if(logon)
|
||||
if (logon)
|
||||
await _signInManager.SignInAsync(user, isPersistent: false);
|
||||
return RedirectToLocal(returnUrl);
|
||||
}
|
||||
@ -479,13 +480,9 @@ namespace BTCPayServer.Controllers
|
||||
return View(model);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test property
|
||||
/// </summary>
|
||||
public string RegisteredUserId
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
// Properties used by tests
|
||||
public string RegisteredUserId { get; set; }
|
||||
public bool RegisteredAdmin { get; set; }
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> Logout()
|
||||
@ -539,7 +536,7 @@ namespace BTCPayServer.Controllers
|
||||
var callbackUrl = Url.ResetPasswordCallbackLink(user.Id, code, Request.Scheme);
|
||||
_EmailSenderFactory.GetEmailSender().SendEmail(model.Email, "Reset Password",
|
||||
$"Please reset your password by clicking here: <a href='{callbackUrl}'>link</a>");
|
||||
|
||||
|
||||
return RedirectToAction(nameof(ForgotPasswordConfirmation));
|
||||
}
|
||||
|
||||
@ -625,8 +622,8 @@ namespace BTCPayServer.Controllers
|
||||
return RedirectToAction(nameof(HomeController.Index), "Home");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
private bool CanLoginOrRegister()
|
||||
{
|
||||
return _btcPayServerEnvironment.IsDevelopping || _btcPayServerEnvironment.IsSecure;
|
||||
@ -639,7 +636,7 @@ namespace BTCPayServer.Controllers
|
||||
Severity = StatusMessageModel.StatusSeverity.Error,
|
||||
Message = "You cannot login over an insecure connection. Please use HTTPS or Tor."
|
||||
});
|
||||
|
||||
|
||||
ViewData["disabled"] = true;
|
||||
}
|
||||
|
||||
|
@ -1,136 +0,0 @@
|
||||
/*
|
||||
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
|
||||
* See https://github.com/openiddict/openiddict-core for more information concerning
|
||||
* the license and the contributors participating to this project.
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Security.OpenId;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.Authorization;
|
||||
using BTCPayServer.Security;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.AspNetCore;
|
||||
using OpenIddict.Abstractions;
|
||||
using OpenIddict.Core;
|
||||
using OpenIddict.Server;
|
||||
using System.Security.Claims;
|
||||
using OpenIddict.Server.AspNetCore;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public class AuthorizationController : Controller
|
||||
{
|
||||
private readonly OpenIddictApplicationManager<BTCPayOpenIdClient> _applicationManager;
|
||||
private readonly SignInManager<ApplicationUser> _signInManager;
|
||||
private readonly OpenIddictAuthorizationManager<BTCPayOpenIdAuthorization> _authorizationManager;
|
||||
private readonly UserManager<ApplicationUser> _userManager;
|
||||
private readonly IOptions<IdentityOptions> _IdentityOptions;
|
||||
|
||||
public AuthorizationController(
|
||||
OpenIddictApplicationManager<BTCPayOpenIdClient> applicationManager,
|
||||
SignInManager<ApplicationUser> signInManager,
|
||||
OpenIddictAuthorizationManager<BTCPayOpenIdAuthorization> authorizationManager,
|
||||
UserManager<ApplicationUser> userManager,
|
||||
IOptions<IdentityOptions> identityOptions)
|
||||
{
|
||||
_applicationManager = applicationManager;
|
||||
_signInManager = signInManager;
|
||||
_authorizationManager = authorizationManager;
|
||||
_userManager = userManager;
|
||||
_IdentityOptions = identityOptions;
|
||||
}
|
||||
|
||||
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
[HttpGet("/connect/authorize")]
|
||||
public async Task<IActionResult> Authorize()
|
||||
{
|
||||
var request = HttpContext.GetOpenIddictServerRequest();
|
||||
// Retrieve the application details from the database.
|
||||
var application = await _applicationManager.FindByClientIdAsync(request.ClientId);
|
||||
|
||||
if (application == null)
|
||||
{
|
||||
return View("Error",
|
||||
new ErrorViewModel
|
||||
{
|
||||
Error = OpenIddictConstants.Errors.InvalidClient,
|
||||
ErrorDescription =
|
||||
"Details concerning the calling client application cannot be found in the database"
|
||||
});
|
||||
}
|
||||
|
||||
var userId = _userManager.GetUserId(User);
|
||||
if (!string.IsNullOrEmpty(
|
||||
await OpenIdExtensions.IsUserAuthorized(_authorizationManager, request, userId, application.Id)))
|
||||
{
|
||||
return await Authorize("YES", false);
|
||||
}
|
||||
|
||||
// Flow the request_id to allow OpenIddict to restore
|
||||
// the original authorization request from the cache.
|
||||
return View(new AuthorizeViewModel
|
||||
{
|
||||
ApplicationName = await _applicationManager.GetDisplayNameAsync(application),
|
||||
RequestId = request.RequestId,
|
||||
Scope = request.GetScopes()
|
||||
});
|
||||
}
|
||||
|
||||
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
[HttpPost("/connect/authorize")]
|
||||
public async Task<IActionResult> Authorize(string consent, bool createAuthorization = true)
|
||||
{
|
||||
var request = HttpContext.GetOpenIddictServerRequest();
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if (user == null)
|
||||
{
|
||||
return View("Error",
|
||||
new ErrorViewModel
|
||||
{
|
||||
Error = OpenIddictConstants.Errors.ServerError,
|
||||
ErrorDescription = "The specified user could not be found"
|
||||
});
|
||||
}
|
||||
|
||||
string type = null;
|
||||
switch (consent.ToUpperInvariant())
|
||||
{
|
||||
case "YESTEMPORARY":
|
||||
type = OpenIddictConstants.AuthorizationTypes.AdHoc;
|
||||
break;
|
||||
case "YES":
|
||||
type = OpenIddictConstants.AuthorizationTypes.Permanent;
|
||||
break;
|
||||
case "NO":
|
||||
default:
|
||||
// Notify OpenIddict that the authorization grant has been denied by the resource owner
|
||||
// to redirect the user agent to the client application using the appropriate response_mode.
|
||||
return Forbid(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
|
||||
}
|
||||
|
||||
|
||||
var principal = await _signInManager.CreateUserPrincipalAsync(user);
|
||||
principal = await _signInManager.CreateUserPrincipalAsync(user);
|
||||
principal.SetScopes(request.GetScopes().Restrict(principal));
|
||||
principal.SetDestinations(_IdentityOptions.Value);
|
||||
if (createAuthorization)
|
||||
{
|
||||
var application = await _applicationManager.FindByClientIdAsync(request.ClientId);
|
||||
var authorization = await _authorizationManager.CreateAsync(User, user.Id, application.Id,
|
||||
type, principal.GetScopes());
|
||||
principal.SetInternalAuthorizationId(authorization.Id);
|
||||
}
|
||||
|
||||
// Returning a SignInResult will ask OpenIddict to issue the appropriate access/identity tokens.
|
||||
return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
|
||||
}
|
||||
}
|
||||
}
|
@ -103,6 +103,7 @@ namespace BTCPayServer.Controllers
|
||||
cryptoPayment.Paid = _CurrencyNameTable.DisplayFormatCurrency(accounting.CryptoPaid.ToDecimal(MoneyUnit.BTC), paymentMethodId.CryptoCode);
|
||||
cryptoPayment.Overpaid = _CurrencyNameTable.DisplayFormatCurrency(accounting.OverpaidHelper.ToDecimal(MoneyUnit.BTC), paymentMethodId.CryptoCode);
|
||||
var paymentMethodDetails = data.GetPaymentMethodDetails();
|
||||
cryptoPayment.Address = paymentMethodDetails.GetPaymentDestination();
|
||||
cryptoPayment.Rate = ExchangeRate(data);
|
||||
model.CryptoPayments.Add(cryptoPayment);
|
||||
}
|
||||
|
@ -74,7 +74,11 @@ namespace BTCPayServer.Controllers
|
||||
var getAppsTaggingStore = _InvoiceRepository.GetAppsTaggingStore(store.Id);
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
EmailAddressAttribute emailValidator = new EmailAddressAttribute();
|
||||
entity.ExpirationTime = entity.InvoiceTime.AddMinutes(storeBlob.InvoiceExpiration);
|
||||
entity.ExpirationTime = invoice.ExpirationTime is DateTimeOffset v ? v : entity.InvoiceTime.AddMinutes(storeBlob.InvoiceExpiration);
|
||||
if (entity.ExpirationTime - TimeSpan.FromSeconds(30.0) < entity.InvoiceTime)
|
||||
{
|
||||
throw new BitpayHttpException(400, "The expirationTime is set too soon");
|
||||
}
|
||||
entity.MonitoringExpiration = entity.ExpirationTime + TimeSpan.FromMinutes(storeBlob.MonitoringExpiration);
|
||||
entity.OrderId = invoice.OrderId;
|
||||
entity.ServerUrl = serverUrl;
|
||||
|
332
BTCPayServer/Controllers/ManageController.APIKeys.cs
Normal file
332
BTCPayServer/Controllers/ManageController.APIKeys.cs
Normal file
@ -0,0 +1,332 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Hosting.OpenApi;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Security.APIKeys;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NSwag.Annotations;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public partial class ManageController
|
||||
{
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> APIKeys()
|
||||
{
|
||||
return View(new ApiKeysViewModel()
|
||||
{
|
||||
ApiKeyDatas = await _apiKeyRepository.GetKeys(new APIKeyRepository.APIKeyQuery()
|
||||
{
|
||||
UserId = new[] {_userManager.GetUserId(User)}
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
[HttpGet("api-keys/{id}/delete")]
|
||||
public async Task<IActionResult> RemoveAPIKey(string id)
|
||||
{
|
||||
var key = await _apiKeyRepository.GetKey(id);
|
||||
if (key == null || key.UserId != _userManager.GetUserId(User))
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
return View("Confirm", new ConfirmModel()
|
||||
{
|
||||
Title = "Delete API Key "+ ( string.IsNullOrEmpty(key.Label)? string.Empty: key.Label) + "("+key.Id+")",
|
||||
Description = "Any application using this api key will immediately lose access",
|
||||
Action = "Delete",
|
||||
ActionUrl = Request.GetCurrentUrl().Replace("RemoveAPIKey", "RemoveAPIKeyPost")
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost("api-keys/{id}/delete")]
|
||||
public async Task<IActionResult> RemoveAPIKeyPost(string id)
|
||||
{
|
||||
var key = await _apiKeyRepository.GetKey(id);
|
||||
if (key == null || key.UserId != _userManager.GetUserId(User))
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
await _apiKeyRepository.Remove(id, _userManager.GetUserId(User));
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Success,
|
||||
Message = "API Key removed"
|
||||
});
|
||||
return RedirectToAction("APIKeys");
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> AddApiKey()
|
||||
{
|
||||
if (!_btcPayServerEnvironment.IsSecure)
|
||||
{
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Error,
|
||||
Message = "Cannot generate api keys while not on https or tor"
|
||||
});
|
||||
return RedirectToAction("APIKeys");
|
||||
}
|
||||
|
||||
return View("AddApiKey", await SetViewModelValues(new AddApiKeyViewModel()));
|
||||
}
|
||||
|
||||
/// <param name="permissions">The permissions to request. Current permissions available: ServerManagement, StoreManagement</param>
|
||||
/// <param name="applicationName">The name of your application</param>
|
||||
/// <param name="strict">If permissions are specified, and strict is set to false, it will allow the user to reject some of permissions the application is requesting.</param>
|
||||
/// <param name="selectiveStores">If the application is requesting the CanModifyStoreSettings permission and selectiveStores is set to true, this allows the user to only grant permissions to selected stores under the user's control.</param>
|
||||
[HttpGet("~/api-keys/authorize")]
|
||||
[OpenApiTags("Authorization")]
|
||||
[OpenApiOperation("Authorize User",
|
||||
"Redirect the browser to this endpoint to request the user to generate an api-key with specific permissions")]
|
||||
[IncludeInOpenApiDocs]
|
||||
public async Task<IActionResult> AuthorizeAPIKey(string[] permissions, string applicationName = null,
|
||||
bool strict = true, bool selectiveStores = false)
|
||||
{
|
||||
if (!_btcPayServerEnvironment.IsSecure)
|
||||
{
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Error,
|
||||
Message = "Cannot generate api keys while not on https or tor"
|
||||
});
|
||||
return RedirectToAction("APIKeys");
|
||||
}
|
||||
|
||||
permissions ??= Array.Empty<string>();
|
||||
|
||||
var vm = await SetViewModelValues(new AuthorizeApiKeysViewModel()
|
||||
{
|
||||
Label = applicationName,
|
||||
ServerManagementPermission = permissions.Contains(APIKeyConstants.Permissions.ServerManagement),
|
||||
StoreManagementPermission = permissions.Contains(APIKeyConstants.Permissions.StoreManagement),
|
||||
PermissionsFormatted = permissions,
|
||||
ApplicationName = applicationName,
|
||||
SelectiveStores = selectiveStores,
|
||||
Strict = strict,
|
||||
});
|
||||
|
||||
vm.ServerManagementPermission = vm.ServerManagementPermission && vm.IsServerAdmin;
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
[HttpPost("~/api-keys/authorize")]
|
||||
public async Task<IActionResult> AuthorizeAPIKey([FromForm] AuthorizeApiKeysViewModel viewModel)
|
||||
{
|
||||
await SetViewModelValues(viewModel);
|
||||
var ar = HandleCommands(viewModel);
|
||||
|
||||
if (ar != null)
|
||||
{
|
||||
return ar;
|
||||
}
|
||||
|
||||
|
||||
if (viewModel.PermissionsFormatted.Contains(APIKeyConstants.Permissions.ServerManagement))
|
||||
{
|
||||
if (!viewModel.IsServerAdmin && viewModel.ServerManagementPermission)
|
||||
{
|
||||
viewModel.ServerManagementPermission = false;
|
||||
}
|
||||
|
||||
if (!viewModel.ServerManagementPermission && viewModel.Strict)
|
||||
{
|
||||
ModelState.AddModelError(nameof(viewModel.ServerManagementPermission),
|
||||
"This permission is required for this application.");
|
||||
}
|
||||
}
|
||||
|
||||
if (viewModel.PermissionsFormatted.Contains(APIKeyConstants.Permissions.StoreManagement))
|
||||
{
|
||||
if (!viewModel.SelectiveStores &&
|
||||
viewModel.StoreMode == AddApiKeyViewModel.ApiKeyStoreMode.Specific)
|
||||
{
|
||||
viewModel.StoreMode = AddApiKeyViewModel.ApiKeyStoreMode.AllStores;
|
||||
ModelState.AddModelError(nameof(viewModel.StoreManagementPermission),
|
||||
"This application does not allow selective store permissions.");
|
||||
}
|
||||
|
||||
if (!viewModel.StoreManagementPermission && !viewModel.SpecificStores.Any() && viewModel.Strict)
|
||||
{
|
||||
ModelState.AddModelError(nameof(viewModel.StoreManagementPermission),
|
||||
"This permission is required for this application.");
|
||||
}
|
||||
}
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(viewModel);
|
||||
}
|
||||
|
||||
switch (viewModel.Command.ToLowerInvariant())
|
||||
{
|
||||
case "no":
|
||||
return RedirectToAction("APIKeys");
|
||||
case "yes":
|
||||
var key = await CreateKey(viewModel);
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Success,
|
||||
Html = $"API key generated! <code>{key.Id}</code>"
|
||||
});
|
||||
return RedirectToAction("APIKeys", new { key = key.Id});
|
||||
default: return View(viewModel);
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> AddApiKey(AddApiKeyViewModel viewModel)
|
||||
{
|
||||
await SetViewModelValues(viewModel);
|
||||
|
||||
var ar = HandleCommands(viewModel);
|
||||
|
||||
if (ar != null)
|
||||
{
|
||||
return ar;
|
||||
}
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(viewModel);
|
||||
}
|
||||
|
||||
var key = await CreateKey(viewModel);
|
||||
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Success,
|
||||
Html = $"API key generated! <code>{key.Id}</code>"
|
||||
});
|
||||
return RedirectToAction("APIKeys");
|
||||
}
|
||||
private IActionResult HandleCommands(AddApiKeyViewModel viewModel)
|
||||
{
|
||||
switch (viewModel.Command)
|
||||
{
|
||||
case "change-store-mode":
|
||||
viewModel.StoreMode = viewModel.StoreMode == AddApiKeyViewModel.ApiKeyStoreMode.Specific
|
||||
? AddApiKeyViewModel.ApiKeyStoreMode.AllStores
|
||||
: AddApiKeyViewModel.ApiKeyStoreMode.Specific;
|
||||
|
||||
if (viewModel.StoreMode == AddApiKeyViewModel.ApiKeyStoreMode.Specific &&
|
||||
!viewModel.SpecificStores.Any() && viewModel.Stores.Any())
|
||||
{
|
||||
viewModel.SpecificStores.Add(null);
|
||||
}
|
||||
return View(viewModel);
|
||||
case "add-store":
|
||||
viewModel.SpecificStores.Add(null);
|
||||
return View(viewModel);
|
||||
|
||||
case string x when x.StartsWith("remove-store", StringComparison.InvariantCultureIgnoreCase):
|
||||
{
|
||||
ModelState.Clear();
|
||||
var index = int.Parse(
|
||||
viewModel.Command.Substring(
|
||||
viewModel.Command.IndexOf(":", StringComparison.InvariantCultureIgnoreCase) + 1),
|
||||
CultureInfo.InvariantCulture);
|
||||
viewModel.SpecificStores.RemoveAt(index);
|
||||
return View(viewModel);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private async Task<APIKeyData> CreateKey(AddApiKeyViewModel viewModel)
|
||||
{
|
||||
var key = new APIKeyData()
|
||||
{
|
||||
Id = Guid.NewGuid().ToString().Replace("-", string.Empty),
|
||||
Type = APIKeyType.Permanent,
|
||||
UserId = _userManager.GetUserId(User),
|
||||
Label = viewModel.Label
|
||||
};
|
||||
key.SetPermissions(GetPermissionsFromViewModel(viewModel));
|
||||
await _apiKeyRepository.CreateKey(key);
|
||||
return key;
|
||||
}
|
||||
|
||||
private IEnumerable<string> GetPermissionsFromViewModel(AddApiKeyViewModel viewModel)
|
||||
{
|
||||
var permissions = new List<string>();
|
||||
|
||||
if (viewModel.StoreMode == AddApiKeyViewModel.ApiKeyStoreMode.Specific)
|
||||
{
|
||||
permissions.AddRange(viewModel.SpecificStores.Select(APIKeyConstants.Permissions.GetStorePermission));
|
||||
}
|
||||
else if (viewModel.StoreManagementPermission)
|
||||
{
|
||||
permissions.Add(APIKeyConstants.Permissions.StoreManagement);
|
||||
}
|
||||
|
||||
if (viewModel.IsServerAdmin && viewModel.ServerManagementPermission)
|
||||
{
|
||||
permissions.Add(APIKeyConstants.Permissions.ServerManagement);
|
||||
}
|
||||
|
||||
return permissions;
|
||||
}
|
||||
|
||||
private async Task<T> SetViewModelValues<T>(T viewModel) where T : AddApiKeyViewModel
|
||||
{
|
||||
viewModel.Stores = await _StoreRepository.GetStoresByUserId(_userManager.GetUserId(User));
|
||||
viewModel.IsServerAdmin = (await _authorizationService.AuthorizeAsync(User, Policies.CanModifyServerSettings.Key)).Succeeded;
|
||||
return viewModel;
|
||||
}
|
||||
|
||||
public class AddApiKeyViewModel
|
||||
{
|
||||
public string Label { get; set; }
|
||||
public StoreData[] Stores { get; set; }
|
||||
public ApiKeyStoreMode StoreMode { get; set; }
|
||||
public List<string> SpecificStores { get; set; } = new List<string>();
|
||||
public bool IsServerAdmin { get; set; }
|
||||
public bool ServerManagementPermission { get; set; }
|
||||
public bool StoreManagementPermission { get; set; }
|
||||
public string Command { get; set; }
|
||||
|
||||
public enum ApiKeyStoreMode
|
||||
{
|
||||
AllStores,
|
||||
Specific
|
||||
}
|
||||
}
|
||||
|
||||
public class AuthorizeApiKeysViewModel : AddApiKeyViewModel
|
||||
{
|
||||
public string ApplicationName { get; set; }
|
||||
public bool Strict { get; set; }
|
||||
public bool SelectiveStores { get; set; }
|
||||
public string Permissions { get; set; }
|
||||
|
||||
public string[] PermissionsFormatted
|
||||
{
|
||||
get
|
||||
{
|
||||
return Permissions?.Split(";", StringSplitOptions.RemoveEmptyEntries)?? Array.Empty<string>();
|
||||
}
|
||||
set
|
||||
{
|
||||
Permissions = string.Join(';', value ?? Array.Empty<string>());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public class ApiKeysViewModel
|
||||
{
|
||||
public List<APIKeyData> ApiKeyDatas { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
@ -19,6 +19,8 @@ using System.Globalization;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.U2F;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Security.APIKeys;
|
||||
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
@ -34,6 +36,8 @@ namespace BTCPayServer.Controllers
|
||||
IWebHostEnvironment _Env;
|
||||
public U2FService _u2FService;
|
||||
private readonly BTCPayServerEnvironment _btcPayServerEnvironment;
|
||||
private readonly APIKeyRepository _apiKeyRepository;
|
||||
private readonly IAuthorizationService _authorizationService;
|
||||
StoreRepository _StoreRepository;
|
||||
|
||||
|
||||
@ -48,7 +52,10 @@ namespace BTCPayServer.Controllers
|
||||
StoreRepository storeRepository,
|
||||
IWebHostEnvironment env,
|
||||
U2FService u2FService,
|
||||
BTCPayServerEnvironment btcPayServerEnvironment)
|
||||
BTCPayServerEnvironment btcPayServerEnvironment,
|
||||
APIKeyRepository apiKeyRepository,
|
||||
IAuthorizationService authorizationService
|
||||
)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_signInManager = signInManager;
|
||||
@ -58,6 +65,8 @@ namespace BTCPayServer.Controllers
|
||||
_Env = env;
|
||||
_u2FService = u2FService;
|
||||
_btcPayServerEnvironment = btcPayServerEnvironment;
|
||||
_apiKeyRepository = apiKeyRepository;
|
||||
_authorizationService = authorizationService;
|
||||
_StoreRepository = storeRepository;
|
||||
}
|
||||
|
||||
|
@ -37,7 +37,6 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
[HttpPost]
|
||||
[Route("api/v1/invoices")]
|
||||
[MediaTypeAcceptConstraintAttribute("text/html")]
|
||||
[IgnoreAntiforgeryToken]
|
||||
[EnableCors(CorsPolicies.All)]
|
||||
public async Task<IActionResult> PayButtonHandle([FromForm]PayButtonViewModel model, CancellationToken cancellationToken)
|
||||
@ -78,6 +77,15 @@ namespace BTCPayServer.Controllers
|
||||
ModelState.AddModelError("Store", e.Message);
|
||||
return View();
|
||||
}
|
||||
|
||||
if (model.JsonResponse)
|
||||
{
|
||||
return Json(new
|
||||
{
|
||||
InvoiceId = invoice.Data.Id,
|
||||
InvoiceUrl = invoice.Data.Url
|
||||
});
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(model.CheckoutQueryString))
|
||||
{
|
||||
|
23
BTCPayServer/Controllers/RestApi/ApiKeys/ApiKeyData.cs
Normal file
23
BTCPayServer/Controllers/RestApi/ApiKeys/ApiKeyData.cs
Normal file
@ -0,0 +1,23 @@
|
||||
using BTCPayServer.Data;
|
||||
|
||||
namespace BTCPayServer.Controllers.RestApi.ApiKeys
|
||||
{
|
||||
public class ApiKeyData
|
||||
{
|
||||
public string ApiKey { get; set; }
|
||||
public string Label { get; set; }
|
||||
public string UserId { get; set; }
|
||||
public string[] Permissions { get; set; }
|
||||
|
||||
public static ApiKeyData FromModel(APIKeyData data)
|
||||
{
|
||||
return new ApiKeyData()
|
||||
{
|
||||
Permissions = data.GetPermissions(),
|
||||
ApiKey = data.Id,
|
||||
UserId = data.UserId,
|
||||
Label = data.Label
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Hosting.OpenApi;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Security.APIKeys;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NSwag.Annotations;
|
||||
|
||||
namespace BTCPayServer.Controllers.RestApi.ApiKeys
|
||||
{
|
||||
[ApiController]
|
||||
[IncludeInOpenApiDocs]
|
||||
[OpenApiTags("API Keys")]
|
||||
[Authorize(AuthenticationSchemes = AuthenticationSchemes.ApiKey)]
|
||||
public class ApiKeysController : ControllerBase
|
||||
{
|
||||
private readonly APIKeyRepository _apiKeyRepository;
|
||||
|
||||
public ApiKeysController(APIKeyRepository apiKeyRepository)
|
||||
{
|
||||
_apiKeyRepository = apiKeyRepository;
|
||||
}
|
||||
|
||||
[OpenApiOperation("Get current API Key information", "View information about the current API key")]
|
||||
[SwaggerResponse(StatusCodes.Status200OK, typeof(ApiKeyData),
|
||||
Description = "Information about the current api key")]
|
||||
[HttpGet("~/api/v1/api-keys/current")]
|
||||
[HttpGet("~/api/v1/users/me/api-keys/current")]
|
||||
public async Task<ActionResult<ApiKeyData>> GetKey()
|
||||
{
|
||||
ControllerContext.HttpContext.GetAPIKey(out var apiKey);
|
||||
var data = await _apiKeyRepository.GetKey(apiKey);
|
||||
return Ok(ApiKeyData.FromModel(data));
|
||||
}
|
||||
}
|
||||
}
|
@ -1,27 +1,26 @@
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Security.APIKeys;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using OpenIddict.Validation.AspNetCore;
|
||||
|
||||
namespace BTCPayServer.Controllers.RestApi
|
||||
{
|
||||
/// <summary>
|
||||
/// this controller serves as a testing endpoint for our OpenId unit tests
|
||||
/// this controller serves as a testing endpoint for our api key unit tests
|
||||
/// </summary>
|
||||
[Route("api/[controller]")]
|
||||
[Route("api/test/apikey")]
|
||||
[ApiController]
|
||||
[Authorize(AuthenticationSchemes = AuthenticationSchemes.OpenId)]
|
||||
public class TestController : ControllerBase
|
||||
[Authorize(AuthenticationSchemes = AuthenticationSchemes.ApiKey)]
|
||||
public class TestApiKeyController : ControllerBase
|
||||
{
|
||||
private readonly UserManager<ApplicationUser> _userManager;
|
||||
private readonly StoreRepository _storeRepository;
|
||||
|
||||
public TestController(UserManager<ApplicationUser> userManager, StoreRepository storeRepository)
|
||||
public TestApiKeyController(UserManager<ApplicationUser> userManager, StoreRepository storeRepository)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_storeRepository = storeRepository;
|
||||
@ -40,24 +39,32 @@ namespace BTCPayServer.Controllers.RestApi
|
||||
}
|
||||
|
||||
[HttpGet("me/is-admin")]
|
||||
[Authorize(Policy = Policies.CanModifyServerSettings.Key, AuthenticationSchemes = AuthenticationSchemes.OpenId)]
|
||||
[Authorize(Policy = Policies.CanModifyServerSettings.Key, AuthenticationSchemes = AuthenticationSchemes.ApiKey)]
|
||||
public bool AmIAnAdmin()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
[HttpGet("me/stores")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings.Key,
|
||||
AuthenticationSchemes = AuthenticationSchemes.OpenId)]
|
||||
[Authorize(Policy = Policies.CanListStoreSettings.Key,
|
||||
AuthenticationSchemes = AuthenticationSchemes.ApiKey)]
|
||||
public async Task<StoreData[]> GetCurrentUserStores()
|
||||
{
|
||||
return await _storeRepository.GetStoresByUserId(_userManager.GetUserId(User));
|
||||
return await User.GetStores(_userManager, _storeRepository);
|
||||
}
|
||||
|
||||
[HttpGet("me/stores/actions")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings.Key,
|
||||
AuthenticationSchemes = AuthenticationSchemes.ApiKey)]
|
||||
public bool CanDoNonImplicitStoreActions()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
[HttpGet("me/stores/{storeId}/can-edit")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings.Key,
|
||||
AuthenticationSchemes = AuthenticationSchemes.OpenId)]
|
||||
AuthenticationSchemes = AuthenticationSchemes.ApiKey)]
|
||||
public bool CanEdit(string storeId)
|
||||
{
|
||||
return true;
|
@ -141,8 +141,9 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("{storeId}/derivations/{cryptoCode}")]
|
||||
public async Task<IActionResult> AddDerivationScheme(string storeId, DerivationSchemeViewModel vm,
|
||||
[Route("{storeId}/derivations/{cryptoCode}")]
|
||||
[ApiExplorerSettings(IgnoreApi = true)]
|
||||
public async Task<IActionResult> AddDerivationScheme(string storeId, [FromForm] DerivationSchemeViewModel vm,
|
||||
string cryptoCode)
|
||||
{
|
||||
vm.CryptoCode = cryptoCode;
|
||||
|
@ -33,8 +33,6 @@ namespace BTCPayServer.Controllers
|
||||
vm.ChangellyMerchantId = existing.ChangellyMerchantId;
|
||||
vm.Enabled = existing.Enabled;
|
||||
vm.AmountMarkupPercentage = existing.AmountMarkupPercentage;
|
||||
vm.ShowFiat = existing.ShowFiat;
|
||||
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
@ -60,8 +58,7 @@ namespace BTCPayServer.Controllers
|
||||
ApiUrl = vm.ApiUrl,
|
||||
ChangellyMerchantId = vm.ChangellyMerchantId,
|
||||
Enabled = vm.Enabled,
|
||||
AmountMarkupPercentage = vm.AmountMarkupPercentage,
|
||||
ShowFiat = vm.ShowFiat
|
||||
AmountMarkupPercentage = vm.AmountMarkupPercentage
|
||||
};
|
||||
|
||||
switch (command)
|
||||
|
@ -117,7 +117,7 @@ namespace BTCPayServer.Controllers
|
||||
.GetMetadataAsync<string>(derivationScheme.AccountDerivation,
|
||||
WellknownMetadataKeys.MasterHDKey);
|
||||
|
||||
return await SignWithSeed(walletId,
|
||||
return SignWithSeed(walletId,
|
||||
new SignWithSeedViewModel() {SeedOrKey = extKey, PSBT = psbt.ToBase64()});
|
||||
}
|
||||
|
||||
|
@ -573,7 +573,7 @@ namespace BTCPayServer.Controllers
|
||||
var extKey = await ExplorerClientProvider.GetExplorerClient(network)
|
||||
.GetMetadataAsync<string>(derivationScheme.AccountDerivation, WellknownMetadataKeys.MasterHDKey, cancellation);
|
||||
|
||||
return await SignWithSeed(walletId, new SignWithSeedViewModel()
|
||||
return SignWithSeed(walletId, new SignWithSeedViewModel()
|
||||
{
|
||||
SeedOrKey = extKey,
|
||||
PSBT = psbt.PSBT.ToBase64()
|
||||
@ -645,10 +645,9 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
[HttpPost]
|
||||
[Route("{walletId}/vault")]
|
||||
public async Task<IActionResult> SubmitVault([ModelBinder(typeof(WalletIdModelBinder))]
|
||||
public IActionResult SubmitVault([ModelBinder(typeof(WalletIdModelBinder))]
|
||||
WalletId walletId, WalletSendVaultModel model)
|
||||
{
|
||||
|
||||
return RedirectToWalletPSBTReady(model.PSBT);
|
||||
}
|
||||
private IActionResult RedirectToWalletPSBTReady(string psbt, string signingKey= null, string signingKeyPath = null)
|
||||
@ -718,7 +717,7 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
[HttpPost]
|
||||
[Route("{walletId}/ledger")]
|
||||
public async Task<IActionResult> SubmitLedger([ModelBinder(typeof(WalletIdModelBinder))]
|
||||
public IActionResult SubmitLedger([ModelBinder(typeof(WalletIdModelBinder))]
|
||||
WalletId walletId, WalletSendLedgerModel model)
|
||||
{
|
||||
return RedirectToWalletPSBTReady(model.PSBT);
|
||||
@ -735,7 +734,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
[HttpPost("{walletId}/psbt/seed")]
|
||||
public async Task<IActionResult> SignWithSeed([ModelBinder(typeof(WalletIdModelBinder))]
|
||||
public IActionResult SignWithSeed([ModelBinder(typeof(WalletIdModelBinder))]
|
||||
WalletId walletId, SignWithSeedViewModel viewModel)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
|
@ -268,7 +268,9 @@ namespace BTCPayServer
|
||||
|
||||
public static bool IsOnion(this Uri uri)
|
||||
{
|
||||
return uri?.DnsSafeHost?.EndsWith(".onion", StringComparison.OrdinalIgnoreCase) is true;
|
||||
if (uri == null || !uri.IsAbsoluteUri)
|
||||
return false;
|
||||
return uri.DnsSafeHost.EndsWith(".onion", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
|
||||
|
@ -1,37 +0,0 @@
|
||||
using System.IO;
|
||||
using System.Security.Cryptography;
|
||||
using BTCPayServer.Configuration;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using NETCore.Encrypt.Extensions.Internal;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
public static class OpenIddictExtensions
|
||||
{
|
||||
public static SecurityKey GetSigningKey(IConfiguration configuration, string fileName)
|
||||
{
|
||||
|
||||
var file = Path.Combine(configuration.GetDataDir(), fileName);
|
||||
using var rsa = new RSACryptoServiceProvider(2048);
|
||||
if (File.Exists(file))
|
||||
{
|
||||
rsa.FromXmlString2(File.ReadAllText(file));
|
||||
}
|
||||
else
|
||||
{
|
||||
var contents = rsa.ToXmlString2(true);
|
||||
File.WriteAllText(file, contents);
|
||||
}
|
||||
return new RsaSecurityKey(rsa.ExportParameters(true));;
|
||||
}
|
||||
public static OpenIddictServerBuilder ConfigureSigningKey(this OpenIddictServerBuilder builder,
|
||||
IConfiguration configuration)
|
||||
{
|
||||
return builder
|
||||
.AddSigningKey(GetSigningKey(configuration, "signing.rsaparams"))
|
||||
.AddEncryptionKey(GetSigningKey(configuration, "encrypting.rsaparams"));
|
||||
}
|
||||
}
|
||||
}
|
@ -1,97 +0,0 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Security.Cryptography;
|
||||
using System.Xml;
|
||||
|
||||
namespace NETCore.Encrypt.Extensions.Internal
|
||||
{
|
||||
/// <summary>
|
||||
/// .net core's implementatiosn are still marked as unsupported because of stupid decisions( https://github.com/dotnet/corefx/issues/23686)
|
||||
/// </summary>
|
||||
internal static class RsaKeyExtensions
|
||||
{
|
||||
#region XML
|
||||
|
||||
public static void FromXmlString2(this RSA rsa, string xmlString)
|
||||
{
|
||||
RSAParameters parameters = new RSAParameters();
|
||||
|
||||
XmlDocument xmlDoc = new XmlDocument();
|
||||
xmlDoc.LoadXml(xmlString);
|
||||
|
||||
if (xmlDoc.DocumentElement.Name.Equals("RSAKeyValue", StringComparison.InvariantCulture))
|
||||
{
|
||||
foreach (XmlNode node in xmlDoc.DocumentElement.ChildNodes)
|
||||
{
|
||||
switch (node.Name)
|
||||
{
|
||||
case "Modulus":
|
||||
parameters.Modulus = (string.IsNullOrEmpty(node.InnerText)
|
||||
? null
|
||||
: Convert.FromBase64String(node.InnerText));
|
||||
break;
|
||||
case "Exponent":
|
||||
parameters.Exponent = (string.IsNullOrEmpty(node.InnerText)
|
||||
? null
|
||||
: Convert.FromBase64String(node.InnerText));
|
||||
break;
|
||||
case "P":
|
||||
parameters.P = (string.IsNullOrEmpty(node.InnerText)
|
||||
? null
|
||||
: Convert.FromBase64String(node.InnerText));
|
||||
break;
|
||||
case "Q":
|
||||
parameters.Q = (string.IsNullOrEmpty(node.InnerText)
|
||||
? null
|
||||
: Convert.FromBase64String(node.InnerText));
|
||||
break;
|
||||
case "DP":
|
||||
parameters.DP = (string.IsNullOrEmpty(node.InnerText)
|
||||
? null
|
||||
: Convert.FromBase64String(node.InnerText));
|
||||
break;
|
||||
case "DQ":
|
||||
parameters.DQ = (string.IsNullOrEmpty(node.InnerText)
|
||||
? null
|
||||
: Convert.FromBase64String(node.InnerText));
|
||||
break;
|
||||
case "InverseQ":
|
||||
parameters.InverseQ = (string.IsNullOrEmpty(node.InnerText)
|
||||
? null
|
||||
: Convert.FromBase64String(node.InnerText));
|
||||
break;
|
||||
case "D":
|
||||
parameters.D = (string.IsNullOrEmpty(node.InnerText)
|
||||
? null
|
||||
: Convert.FromBase64String(node.InnerText));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception("Invalid XML RSA key.");
|
||||
}
|
||||
|
||||
rsa.ImportParameters(parameters);
|
||||
}
|
||||
|
||||
public static string ToXmlString2(this RSA rsa, bool includePrivateParameters)
|
||||
{
|
||||
RSAParameters parameters = rsa.ExportParameters(includePrivateParameters);
|
||||
|
||||
return string.Format(CultureInfo.InvariantCulture,
|
||||
"<RSAKeyValue><Modulus>{0}</Modulus><Exponent>{1}</Exponent><P>{2}</P><Q>{3}</Q><DP>{4}</DP><DQ>{5}</DQ><InverseQ>{6}</InverseQ><D>{7}</D></RSAKeyValue>",
|
||||
parameters.Modulus != null ? Convert.ToBase64String(parameters.Modulus) : null,
|
||||
parameters.Exponent != null ? Convert.ToBase64String(parameters.Exponent) : null,
|
||||
parameters.P != null ? Convert.ToBase64String(parameters.P) : null,
|
||||
parameters.Q != null ? Convert.ToBase64String(parameters.Q) : null,
|
||||
parameters.DP != null ? Convert.ToBase64String(parameters.DP) : null,
|
||||
parameters.DQ != null ? Convert.ToBase64String(parameters.DQ) : null,
|
||||
parameters.InverseQ != null ? Convert.ToBase64String(parameters.InverseQ) : null,
|
||||
parameters.D != null ? Convert.ToBase64String(parameters.D) : null);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
@ -26,12 +26,14 @@ using System.Threading;
|
||||
using BTCPayServer.Services.Wallets;
|
||||
using BTCPayServer.Logging;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Hosting.OpenApi;
|
||||
using BTCPayServer.PaymentRequest;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Bitcoin;
|
||||
using BTCPayServer.Payments.Changelly;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Security.APIKeys;
|
||||
using BTCPayServer.Services.PaymentRequests;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
@ -40,17 +42,6 @@ using Npgsql;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.U2F;
|
||||
using BundlerMinifier.TagHelpers;
|
||||
using OpenIddict.EntityFrameworkCore.Models;
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Security.Claims;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Models;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using BTCPayServer.Security.Bitpay;
|
||||
using Serilog;
|
||||
@ -66,7 +57,6 @@ namespace BTCPayServer.Hosting
|
||||
{
|
||||
var factory = provider.GetRequiredService<ApplicationDbContextFactory>();
|
||||
factory.ConfigureBuilder(o);
|
||||
o.UseOpenIddict<BTCPayOpenIdClient, BTCPayOpenIdAuthorization, OpenIddictScope<string>, BTCPayOpenIdToken, string>();
|
||||
});
|
||||
services.AddHttpClient();
|
||||
services.AddHttpClient(nameof(ExplorerClientProvider), httpClient =>
|
||||
@ -220,7 +210,6 @@ namespace BTCPayServer.Hosting
|
||||
services.AddSingleton<IHostedService, WalletReceiveCacheUpdater>();
|
||||
services.AddSingleton<IBackgroundJobClient, BackgroundJobClient>();
|
||||
services.AddScoped<IAuthorizationHandler, CookieAuthorizationHandler>();
|
||||
services.AddScoped<IAuthorizationHandler, OpenIdAuthorizationHandler>();
|
||||
services.AddScoped<IAuthorizationHandler, BitpayAuthorizationHandler>();
|
||||
|
||||
services.TryAddSingleton<ExplorerClientProvider>();
|
||||
@ -241,11 +230,11 @@ namespace BTCPayServer.Hosting
|
||||
services.AddTransient<PaymentRequestController>();
|
||||
// Add application services.
|
||||
services.AddSingleton<EmailSenderFactory>();
|
||||
// bundling
|
||||
|
||||
services.AddBtcPayServerAuthenticationSchemes(configuration);
|
||||
|
||||
services.AddAPIKeyAuthentication();
|
||||
services.AddBtcPayServerAuthenticationSchemes();
|
||||
services.AddAuthorization(o => o.AddBTCPayPolicies());
|
||||
|
||||
// bundling
|
||||
services.AddSingleton<IBundleProvider, ResourceBundleProvider>();
|
||||
services.AddTransient<BundleOptions>(provider =>
|
||||
{
|
||||
@ -274,7 +263,7 @@ namespace BTCPayServer.Hosting
|
||||
}
|
||||
return rateLimits;
|
||||
});
|
||||
|
||||
services.AddBTCPayOpenApi();
|
||||
|
||||
services.AddLogging(logBuilder =>
|
||||
{
|
||||
@ -292,17 +281,18 @@ namespace BTCPayServer.Hosting
|
||||
return services;
|
||||
}
|
||||
private const long MAX_DEBUG_LOG_FILE_SIZE = 2000000; // If debug log is in use roll it every N MB.
|
||||
private static void AddBtcPayServerAuthenticationSchemes(this IServiceCollection services,
|
||||
IConfiguration configuration)
|
||||
private static void AddBtcPayServerAuthenticationSchemes(this IServiceCollection services)
|
||||
{
|
||||
services.AddAuthentication()
|
||||
.AddCookie()
|
||||
.AddBitpayAuthentication();
|
||||
.AddBitpayAuthentication()
|
||||
.AddAPIKeyAuthentication();
|
||||
}
|
||||
|
||||
public static IApplicationBuilder UsePayServer(this IApplicationBuilder app)
|
||||
{
|
||||
app.UseMiddleware<BTCPayMiddleware>();
|
||||
app.UseBTCPayOpenApi();
|
||||
return app;
|
||||
}
|
||||
public static IApplicationBuilder UseHeadersOverride(this IApplicationBuilder app)
|
||||
|
9
BTCPayServer/Hosting/OpenApi/IncludeInOpenApiDocs.cs
Normal file
9
BTCPayServer/Hosting/OpenApi/IncludeInOpenApiDocs.cs
Normal file
@ -0,0 +1,9 @@
|
||||
using System;
|
||||
|
||||
|
||||
namespace BTCPayServer.Hosting.OpenApi
|
||||
{
|
||||
public class IncludeInOpenApiDocs : Attribute
|
||||
{
|
||||
}
|
||||
}
|
96
BTCPayServer/Hosting/OpenApi/OpenApiExtensions.cs
Normal file
96
BTCPayServer/Hosting/OpenApi/OpenApiExtensions.cs
Normal file
@ -0,0 +1,96 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Security;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using NJsonSchema;
|
||||
using NJsonSchema.Generation.TypeMappers;
|
||||
using NSwag;
|
||||
using NSwag.Generation.Processors.Security;
|
||||
|
||||
namespace BTCPayServer.Hosting.OpenApi
|
||||
{
|
||||
public static class OpenApiExtensions
|
||||
{
|
||||
public static IServiceCollection AddBTCPayOpenApi(this IServiceCollection serviceCollection)
|
||||
{
|
||||
|
||||
return serviceCollection.AddOpenApiDocument(config =>
|
||||
{
|
||||
config.PostProcess = document =>
|
||||
{
|
||||
document.Info.Version = "v1";
|
||||
document.Info.Title = "BTCPay Greenfield API";
|
||||
document.Info.Description = "A full API to use your BTCPay Server";
|
||||
document.Info.TermsOfService = null;
|
||||
document.Info.Contact = new NSwag.OpenApiContact
|
||||
{
|
||||
Name = "BTCPay Server", Email = string.Empty, Url = "https://btcpayserver.org"
|
||||
};
|
||||
};
|
||||
config.AddOperationFilter(context =>
|
||||
{
|
||||
var methodInfo = context.MethodInfo;
|
||||
if (methodInfo != null)
|
||||
{
|
||||
return methodInfo.CustomAttributes.Any(data =>
|
||||
data.AttributeType == typeof(IncludeInOpenApiDocs)) ||
|
||||
methodInfo.DeclaringType.CustomAttributes.Any(data =>
|
||||
data.AttributeType == typeof(IncludeInOpenApiDocs));
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
config.AddSecurity("APIKey", Enumerable.Empty<string>(),
|
||||
new OpenApiSecurityScheme
|
||||
{
|
||||
Type = OpenApiSecuritySchemeType.ApiKey,
|
||||
Name = "Authorization",
|
||||
In = OpenApiSecurityApiKeyLocation.Header,
|
||||
Description =
|
||||
"BTCPay Server supports authenticating and authorizing users through an API Key that is generated by them. Send the API Key as a header value to Authorization with the format: token {token}. For a smoother experience, you can generate a url that redirects users to an API key creation screen."
|
||||
});
|
||||
|
||||
config.OperationProcessors.Add(
|
||||
new BTCPayPolicyOperationProcessor("APIKey", AuthenticationSchemes.ApiKey));
|
||||
|
||||
config.TypeMappers.Add(
|
||||
new PrimitiveTypeMapper(typeof(PaymentType), s => s.Type = JsonObjectType.String));
|
||||
config.TypeMappers.Add(new PrimitiveTypeMapper(typeof(PaymentMethodId),
|
||||
s => s.Type = JsonObjectType.String));
|
||||
});
|
||||
}
|
||||
|
||||
public static IApplicationBuilder UseBTCPayOpenApi(this IApplicationBuilder builder)
|
||||
{
|
||||
return builder.UseOpenApi()
|
||||
.UseReDoc(settings => settings.Path = "/docs");
|
||||
}
|
||||
|
||||
|
||||
class BTCPayPolicyOperationProcessor : AspNetCoreOperationSecurityScopeProcessor
|
||||
{
|
||||
private readonly string _authScheme;
|
||||
|
||||
public BTCPayPolicyOperationProcessor(string x, string authScheme) : base(x)
|
||||
{
|
||||
_authScheme = authScheme;
|
||||
}
|
||||
|
||||
protected override IEnumerable<string> GetScopes(IEnumerable<AuthorizeAttribute> authorizeAttributes)
|
||||
{
|
||||
var result = authorizeAttributes
|
||||
.Where(attribute => attribute?.AuthenticationSchemes != null && attribute.Policy != null &&
|
||||
attribute.AuthenticationSchemes.Equals(_authScheme,
|
||||
StringComparison.InvariantCultureIgnoreCase))
|
||||
.Select(attribute => attribute.Policy);
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,8 +1,7 @@
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.DataProtection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using OpenIddict.Validation.AspNetCore;
|
||||
using OpenIddict.Abstractions;
|
||||
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using System;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
@ -19,14 +18,10 @@ using System.IO;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using BTCPayServer.Security;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core;
|
||||
using OpenIddict.EntityFrameworkCore.Models;
|
||||
using System.Net;
|
||||
using BTCPayServer.Security.OpenId;
|
||||
using BTCPayServer.PaymentRequest;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Storage;
|
||||
using Microsoft.Extensions.Options;
|
||||
using OpenIddict.Core;
|
||||
|
||||
namespace BTCPayServer.Hosting
|
||||
{
|
||||
@ -57,8 +52,6 @@ namespace BTCPayServer.Hosting
|
||||
.AddEntityFrameworkStores<ApplicationDbContext>()
|
||||
.AddDefaultTokenProviders();
|
||||
|
||||
ConfigureOpenIddict(services);
|
||||
|
||||
services.AddBTCPayServer(Configuration);
|
||||
services.AddProviderStorage();
|
||||
services.AddSession();
|
||||
@ -95,12 +88,6 @@ namespace BTCPayServer.Hosting
|
||||
options.Lockout.MaxFailedAccessAttempts = 5;
|
||||
options.Lockout.AllowedForNewUsers = true;
|
||||
options.Password.RequireUppercase = false;
|
||||
// Configure Identity to use the same JWT claims as OpenIddict instead
|
||||
// of the legacy WS-Federation claims it uses by default (ClaimTypes),
|
||||
// which saves you from doing the mapping in your authorization controller.
|
||||
options.ClaimsIdentity.UserNameClaimType = OpenIddictConstants.Claims.Name;
|
||||
options.ClaimsIdentity.UserIdClaimType = OpenIddictConstants.Claims.Subject;
|
||||
options.ClaimsIdentity.RoleClaimType = OpenIddictConstants.Claims.Role;
|
||||
});
|
||||
// If the HTTPS certificate path is not set this logic will NOT be used and the default Kestrel binding logic will be.
|
||||
string httpsCertificateFilePath = Configuration.GetOrDefault<string>("HttpsCertificateFilePath", null);
|
||||
@ -143,67 +130,6 @@ namespace BTCPayServer.Hosting
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private DirectoryInfo GetDataDir()
|
||||
{
|
||||
return new DirectoryInfo(Configuration.GetDataDir(DefaultConfiguration.GetNetworkType(Configuration)));
|
||||
}
|
||||
|
||||
private void ConfigureOpenIddict(IServiceCollection services)
|
||||
{
|
||||
// Register the OpenIddict services.
|
||||
services.AddOpenIddict()
|
||||
.AddCore(options =>
|
||||
{
|
||||
// Configure OpenIddict to use the Entity Framework Core stores and entities.
|
||||
options.UseEntityFrameworkCore()
|
||||
.UseDbContext<ApplicationDbContext>()
|
||||
.ReplaceDefaultEntities<BTCPayOpenIdClient, BTCPayOpenIdAuthorization, OpenIddictScope<string>,
|
||||
BTCPayOpenIdToken, string>();
|
||||
})
|
||||
.AddServer(options =>
|
||||
{
|
||||
options.UseAspNetCore()
|
||||
.EnableStatusCodePagesIntegration()
|
||||
.EnableAuthorizationEndpointPassthrough()
|
||||
.EnableLogoutEndpointPassthrough()
|
||||
.EnableAuthorizationEndpointCaching()
|
||||
.DisableTransportSecurityRequirement();
|
||||
|
||||
// Enable the token endpoint (required to use the password flow).
|
||||
options.SetTokenEndpointUris("/connect/token");
|
||||
options.SetAuthorizationEndpointUris("/connect/authorize");
|
||||
options.SetLogoutEndpointUris("/connect/logout");
|
||||
|
||||
//we do not care about these granular controls for now
|
||||
options.IgnoreScopePermissions();
|
||||
options.IgnoreEndpointPermissions();
|
||||
// Allow client applications various flows
|
||||
options.AllowImplicitFlow();
|
||||
options.AllowClientCredentialsFlow();
|
||||
options.AllowRefreshTokenFlow();
|
||||
options.AllowPasswordFlow();
|
||||
options.AllowAuthorizationCodeFlow();
|
||||
options.UseRollingTokens();
|
||||
|
||||
options.RegisterScopes(
|
||||
OpenIddictConstants.Scopes.OpenId,
|
||||
BTCPayScopes.StoreManagement,
|
||||
BTCPayScopes.ServerManagement
|
||||
);
|
||||
options.AddEventHandler(PasswordGrantTypeEventHandler.Descriptor);
|
||||
options.AddEventHandler(OpenIdGrantHandlerCheckCanSignIn.Descriptor);
|
||||
options.AddEventHandler(ClientCredentialsGrantTypeEventHandler.Descriptor);
|
||||
options.AddEventHandler(LogoutEventHandler.Descriptor);
|
||||
options.ConfigureSigningKey(Configuration);
|
||||
})
|
||||
.AddValidation(options =>
|
||||
{
|
||||
options.UseLocalServer();
|
||||
options.UseAspNetCore();
|
||||
});
|
||||
}
|
||||
|
||||
public void Configure(
|
||||
IApplicationBuilder app,
|
||||
IWebHostEnvironment env,
|
||||
@ -224,6 +150,10 @@ namespace BTCPayServer.Hosting
|
||||
});
|
||||
}
|
||||
}
|
||||
private DirectoryInfo GetDataDir()
|
||||
{
|
||||
return new DirectoryInfo(Configuration.GetDataDir(DefaultConfiguration.GetNetworkType(Configuration)));
|
||||
}
|
||||
|
||||
private static void ConfigureCore(IApplicationBuilder app, IWebHostEnvironment env, IServiceProvider prov, ILoggerFactory loggerFactory, BTCPayServerOptions options)
|
||||
{
|
||||
|
@ -75,6 +75,7 @@ namespace BTCPayServer.Models.AppViewModels
|
||||
public bool DisplayPerksRanking { get; set; }
|
||||
public bool Enabled { get; set; }
|
||||
public string ResetEvery { get; set; }
|
||||
public Dictionary<string, CurrencyData> CurrencyDataPayments { get; set; }
|
||||
}
|
||||
|
||||
public class ContributeToCrowdfund
|
||||
|
@ -29,5 +29,6 @@ namespace BTCPayServer.Models
|
||||
get; set;
|
||||
}
|
||||
public string ButtonClass { get; set; } = "btn-danger";
|
||||
public string ActionUrl { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -38,6 +38,7 @@ namespace BTCPayServer.Models.InvoicingModels
|
||||
public string PaymentMethod { get; set; }
|
||||
public string Due { get; set; }
|
||||
public string Paid { get; set; }
|
||||
public string Address { get; internal set; }
|
||||
public string Rate { get; internal set; }
|
||||
public string PaymentUrl { get; internal set; }
|
||||
public string Overpaid { get; set; }
|
||||
|
@ -41,5 +41,7 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||
public List<string> CurrencyDropdown { get; set; }
|
||||
public string PayButtonImageUrl { get; set; }
|
||||
public string PayButtonText { get; set; }
|
||||
public bool UseModal { get; set; }
|
||||
public bool JsonResponse { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -17,9 +17,6 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||
[Display(Name = "Optional, Changelly Merchant Id")]
|
||||
public string ChangellyMerchantId { get; set; }
|
||||
|
||||
[Display(Name = "Show Fiat Currencies as option in conversion")]
|
||||
public bool ShowFiat { get; set; } = true;
|
||||
|
||||
[Required]
|
||||
[Range(0, 100)]
|
||||
[Display(Name =
|
||||
|
@ -16,13 +16,11 @@ namespace BTCPayServer.Payments.Changelly
|
||||
public class Changelly
|
||||
{
|
||||
private readonly string _apisecret;
|
||||
private readonly bool _showFiat;
|
||||
private readonly HttpClient _httpClient;
|
||||
|
||||
public Changelly(HttpClient httpClient, string apiKey, string apiSecret, string apiUrl, bool showFiat = true)
|
||||
public Changelly(HttpClient httpClient, string apiKey, string apiSecret, string apiUrl)
|
||||
{
|
||||
_apisecret = apiSecret;
|
||||
_showFiat = showFiat;
|
||||
_httpClient = httpClient;
|
||||
_httpClient.BaseAddress = new Uri(apiUrl);
|
||||
_httpClient.DefaultRequestHeaders.Add("api-key", apiKey);
|
||||
@ -62,36 +60,16 @@ namespace BTCPayServer.Payments.Changelly
|
||||
|
||||
public virtual async Task<IEnumerable<CurrencyFull>> GetCurrenciesFull()
|
||||
{
|
||||
const string message = @"{
|
||||
const string message = @"{
|
||||
""jsonrpc"": ""2.0"",
|
||||
""id"": 1,
|
||||
""method"": ""getCurrenciesFull"",
|
||||
""params"": []
|
||||
}";
|
||||
|
||||
var result = await PostToApi<IEnumerable<CurrencyFull>>(message);
|
||||
var appendedResult = _showFiat
|
||||
? result.Result.Concat(new[]
|
||||
{
|
||||
new CurrencyFull()
|
||||
{
|
||||
Enable = true,
|
||||
Name = "EUR",
|
||||
FullName = "Euro",
|
||||
PayInConfirmations = 0,
|
||||
ImageLink = "https://changelly.com/api/coins/eur.png"
|
||||
},
|
||||
new CurrencyFull()
|
||||
{
|
||||
Enable = true,
|
||||
Name = "USD",
|
||||
FullName = "US Dollar",
|
||||
PayInConfirmations = 0,
|
||||
ImageLink = "https://changelly.com/api/coins/usd.png"
|
||||
}
|
||||
})
|
||||
: result.Result;
|
||||
return appendedResult;
|
||||
var result = await PostToApi<IEnumerable<CurrencyFull>>(message);
|
||||
|
||||
return result.Result;
|
||||
}
|
||||
|
||||
public virtual async Task<decimal> GetExchangeAmount(string fromCurrency,
|
||||
|
@ -61,8 +61,9 @@ namespace BTCPayServer.Payments.Changelly
|
||||
throw new ChangellyException("Changelly not enabled for this store");
|
||||
}
|
||||
|
||||
var changelly = new Changelly(_httpClientFactory.CreateClient("Changelly"), changellySettings.ApiKey, changellySettings.ApiSecret,
|
||||
changellySettings.ApiUrl, changellySettings.ShowFiat);
|
||||
var changelly = new Changelly(_httpClientFactory.CreateClient("Changelly"), changellySettings.ApiKey,
|
||||
changellySettings.ApiSecret,
|
||||
changellySettings.ApiUrl);
|
||||
_clientCache.AddOrReplace(storeId, changelly);
|
||||
return changelly;
|
||||
}
|
||||
|
@ -8,7 +8,6 @@ namespace BTCPayServer.Payments.Changelly
|
||||
public bool Enabled { get; set; }
|
||||
public string ChangellyMerchantId { get; set; }
|
||||
public decimal AmountMarkupPercentage { get; set; }
|
||||
public bool ShowFiat { get; set; }
|
||||
|
||||
public bool IsConfigured()
|
||||
{
|
||||
|
@ -53,7 +53,6 @@ namespace BTCPayServer
|
||||
l.AddFilter("Microsoft", LogLevel.Error);
|
||||
l.AddFilter("System.Net.Http.HttpClient", LogLevel.Critical);
|
||||
l.AddFilter("Microsoft.AspNetCore.Antiforgery.Internal", LogLevel.Critical);
|
||||
l.AddFilter("OpenIddict.Server.OpenIddictServerProvider", LogLevel.Error);
|
||||
l.AddProvider(new CustomConsoleLogProvider(processor));
|
||||
})
|
||||
.UseStartup<Startup>()
|
||||
|
56
BTCPayServer/Security/APIKeys/APIKeyAuthenticationHandler.cs
Normal file
56
BTCPayServer/Security/APIKeys/APIKeyAuthenticationHandler.cs
Normal file
@ -0,0 +1,56 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Security.Bitpay;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace BTCPayServer.Security.APIKeys
|
||||
{
|
||||
public class APIKeyAuthenticationHandler : AuthenticationHandler<APIKeyAuthenticationOptions>
|
||||
{
|
||||
private readonly APIKeyRepository _apiKeyRepository;
|
||||
private readonly IOptionsMonitor<IdentityOptions> _identityOptions;
|
||||
|
||||
public APIKeyAuthenticationHandler(
|
||||
APIKeyRepository apiKeyRepository,
|
||||
IOptionsMonitor<IdentityOptions> identityOptions,
|
||||
IOptionsMonitor<APIKeyAuthenticationOptions> options,
|
||||
ILoggerFactory logger,
|
||||
UrlEncoder encoder,
|
||||
ISystemClock clock) : base(options, logger, encoder, clock)
|
||||
{
|
||||
_apiKeyRepository = apiKeyRepository;
|
||||
_identityOptions = identityOptions;
|
||||
}
|
||||
|
||||
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
|
||||
{
|
||||
if (!Context.Request.HttpContext.GetAPIKey(out var apiKey) || string.IsNullOrEmpty(apiKey))
|
||||
return AuthenticateResult.NoResult();
|
||||
|
||||
var key = await _apiKeyRepository.GetKey(apiKey);
|
||||
|
||||
if (key == null)
|
||||
{
|
||||
return AuthenticateResult.Fail("ApiKey authentication failed");
|
||||
}
|
||||
|
||||
List<Claim> claims = new List<Claim>();
|
||||
|
||||
claims.Add(new Claim(_identityOptions.CurrentValue.ClaimsIdentity.UserIdClaimType, key.UserId));
|
||||
claims.AddRange(key.GetPermissions()
|
||||
.Select(permission => new Claim(APIKeyConstants.ClaimTypes.Permissions, permission)));
|
||||
|
||||
return AuthenticateResult.Success(new AuthenticationTicket(
|
||||
new ClaimsPrincipal(new ClaimsIdentity(claims, APIKeyConstants.AuthenticationType)), APIKeyConstants.AuthenticationType));
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
|
||||
namespace BTCPayServer.Security.Bitpay
|
||||
{
|
||||
public class APIKeyAuthenticationOptions : AuthenticationSchemeOptions
|
||||
{
|
||||
}
|
||||
}
|
@ -1,50 +1,50 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using System.Security.Claims;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using static BTCPayServer.Security.OpenId.RestAPIPolicies;
|
||||
using OpenIddict.Abstractions;
|
||||
using BTCPayServer.Security.OpenId;
|
||||
|
||||
namespace BTCPayServer.Security
|
||||
namespace BTCPayServer.Security.APIKeys
|
||||
{
|
||||
public class OpenIdAuthorizationHandler : AuthorizationHandler<PolicyRequirement>
|
||||
public class APIKeyAuthorizationHandler : AuthorizationHandler<PolicyRequirement>
|
||||
|
||||
{
|
||||
private readonly HttpContext _HttpContext;
|
||||
private readonly UserManager<ApplicationUser> _userManager;
|
||||
private readonly StoreRepository _storeRepository;
|
||||
|
||||
public OpenIdAuthorizationHandler(IHttpContextAccessor httpContextAccessor,
|
||||
UserManager<ApplicationUser> userManager,
|
||||
StoreRepository storeRepository)
|
||||
public APIKeyAuthorizationHandler(IHttpContextAccessor httpContextAccessor,
|
||||
UserManager<ApplicationUser> userManager,
|
||||
StoreRepository storeRepository)
|
||||
{
|
||||
_HttpContext = httpContextAccessor.HttpContext;
|
||||
_userManager = userManager;
|
||||
_storeRepository = storeRepository;
|
||||
}
|
||||
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, PolicyRequirement requirement)
|
||||
|
||||
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context,
|
||||
PolicyRequirement requirement)
|
||||
{
|
||||
if (context.User.Identity.AuthenticationType != "AuthenticationTypes.Federation")
|
||||
if (context.User.Identity.AuthenticationType != APIKeyConstants.AuthenticationType)
|
||||
return;
|
||||
|
||||
bool success = false;
|
||||
switch (requirement.Policy)
|
||||
{
|
||||
case Policies.CanListStoreSettings.Key:
|
||||
var selectiveStorePermissions =
|
||||
APIKeyConstants.Permissions.ExtractStorePermissionsIds(context.GetPermissions());
|
||||
success = context.HasPermissions(APIKeyConstants.Permissions.StoreManagement) ||
|
||||
selectiveStorePermissions.Any();
|
||||
break;
|
||||
case Policies.CanModifyStoreSettings.Key:
|
||||
if (!context.HasScopes(BTCPayScopes.StoreManagement))
|
||||
break;
|
||||
// TODO: It should be possible to grant permission to a specific store
|
||||
// we can do this by adding saving a claim with the specific store id
|
||||
// to the access_token
|
||||
string storeId = _HttpContext.GetImplicitStoreId();
|
||||
if (!context.HasPermissions(APIKeyConstants.Permissions.StoreManagement) &&
|
||||
!context.HasPermissions(APIKeyConstants.Permissions.GetStorePermission(storeId)))
|
||||
break;
|
||||
|
||||
if (storeId == null)
|
||||
{
|
||||
success = true;
|
||||
@ -60,9 +60,10 @@ namespace BTCPayServer.Security
|
||||
success = true;
|
||||
_HttpContext.SetStoreData(store);
|
||||
}
|
||||
|
||||
break;
|
||||
case Policies.CanModifyServerSettings.Key:
|
||||
if (!context.HasScopes(BTCPayScopes.ServerManagement))
|
||||
if (!context.HasPermissions(APIKeyConstants.Permissions.ServerManagement))
|
||||
break;
|
||||
// For this authorization, we stil check in database because it is super sensitive.
|
||||
var user = await _userManager.GetUserAsync(context.User);
|
36
BTCPayServer/Security/APIKeys/APIKeyConstants.cs
Normal file
36
BTCPayServer/Security/APIKeys/APIKeyConstants.cs
Normal file
@ -0,0 +1,36 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
|
||||
namespace BTCPayServer.Security.APIKeys
|
||||
{
|
||||
public static class APIKeyConstants
|
||||
{
|
||||
public const string AuthenticationType = "APIKey";
|
||||
|
||||
public static class ClaimTypes
|
||||
{
|
||||
public const string Permissions = nameof(APIKeys) + "." + nameof(Permissions);
|
||||
}
|
||||
|
||||
public static class Permissions
|
||||
{
|
||||
public const string ServerManagement = nameof(ServerManagement);
|
||||
public const string StoreManagement = nameof(StoreManagement);
|
||||
|
||||
public static readonly Dictionary<string, (string Title, string Description)> PermissionDescriptions = new Dictionary<string, (string Title, string Description)>()
|
||||
{
|
||||
{StoreManagement, ("Manage your stores", "The app will be able to create, modify and delete all your stores.")},
|
||||
{$"{nameof(StoreManagement)}:", ("Manage selected stores", "The app will be able to modify and delete selected stores.")},
|
||||
{ServerManagement, ("Manage your server", "The app will have total control on your server")},
|
||||
};
|
||||
|
||||
public static string GetStorePermission(string storeId) => $"{nameof(StoreManagement)}:{storeId}";
|
||||
|
||||
public static IEnumerable<string> ExtractStorePermissionsIds(IEnumerable<string> permissions) => permissions
|
||||
.Where(s => s.StartsWith($"{nameof(StoreManagement)}:", StringComparison.InvariantCulture))
|
||||
.Select(s => s.Split(":")[1]);
|
||||
}
|
||||
}
|
||||
}
|
75
BTCPayServer/Security/APIKeys/APIKeyExtensions.cs
Normal file
75
BTCPayServer/Security/APIKeys/APIKeyExtensions.cs
Normal file
@ -0,0 +1,75 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Security.Bitpay;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
|
||||
namespace BTCPayServer.Security.APIKeys
|
||||
{
|
||||
public static class APIKeyExtensions
|
||||
{
|
||||
public static bool GetAPIKey(this HttpContext httpContext, out StringValues apiKey)
|
||||
{
|
||||
if (httpContext.Request.Headers.TryGetValue("Authorization", out var value) &&
|
||||
value.ToString().StartsWith("token ", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
apiKey = value.ToString().Substring("token ".Length);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static Task<StoreData[]> GetStores(this ClaimsPrincipal claimsPrincipal,
|
||||
UserManager<ApplicationUser> userManager, StoreRepository storeRepository)
|
||||
{
|
||||
var permissions =
|
||||
claimsPrincipal.Claims.Where(claim => claim.Type == APIKeyConstants.ClaimTypes.Permissions)
|
||||
.Select(claim => claim.Value).ToList();
|
||||
|
||||
if (permissions.Contains(APIKeyConstants.Permissions.StoreManagement))
|
||||
{
|
||||
return storeRepository.GetStoresByUserId(userManager.GetUserId(claimsPrincipal));
|
||||
}
|
||||
|
||||
var storeIds = APIKeyConstants.Permissions.ExtractStorePermissionsIds(permissions);
|
||||
return storeRepository.GetStoresByUserId(userManager.GetUserId(claimsPrincipal), storeIds);
|
||||
}
|
||||
|
||||
public static AuthenticationBuilder AddAPIKeyAuthentication(this AuthenticationBuilder builder)
|
||||
{
|
||||
builder.AddScheme<APIKeyAuthenticationOptions, APIKeyAuthenticationHandler>(AuthenticationSchemes.ApiKey,
|
||||
o => { });
|
||||
return builder;
|
||||
}
|
||||
|
||||
public static IServiceCollection AddAPIKeyAuthentication(this IServiceCollection serviceCollection)
|
||||
{
|
||||
serviceCollection.AddSingleton<APIKeyRepository>();
|
||||
serviceCollection.AddScoped<IAuthorizationHandler, APIKeyAuthorizationHandler>();
|
||||
return serviceCollection;
|
||||
}
|
||||
|
||||
public static string[] GetPermissions(this AuthorizationHandlerContext context)
|
||||
{
|
||||
return context.User.Claims.Where(c =>
|
||||
c.Type.Equals(APIKeyConstants.ClaimTypes.Permissions, StringComparison.InvariantCultureIgnoreCase))
|
||||
.Select(claim => claim.Value).ToArray();
|
||||
}
|
||||
|
||||
public static bool HasPermissions(this AuthorizationHandlerContext context, params string[] scopes)
|
||||
{
|
||||
return scopes.All(s => context.User.HasClaim(c =>
|
||||
c.Type.Equals(APIKeyConstants.ClaimTypes.Permissions, StringComparison.InvariantCultureIgnoreCase) &&
|
||||
c.Value.Split(' ').Contains(s)));
|
||||
}
|
||||
}
|
||||
}
|
72
BTCPayServer/Security/APIKeys/APIKeyRepository.cs
Normal file
72
BTCPayServer/Security/APIKeys/APIKeyRepository.cs
Normal file
@ -0,0 +1,72 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace BTCPayServer.Security.APIKeys
|
||||
{
|
||||
public class APIKeyRepository
|
||||
{
|
||||
private readonly ApplicationDbContextFactory _applicationDbContextFactory;
|
||||
|
||||
public APIKeyRepository(ApplicationDbContextFactory applicationDbContextFactory)
|
||||
{
|
||||
_applicationDbContextFactory = applicationDbContextFactory;
|
||||
}
|
||||
|
||||
public async Task<APIKeyData> GetKey(string apiKey)
|
||||
{
|
||||
using (var context = _applicationDbContextFactory.CreateContext())
|
||||
{
|
||||
return await EntityFrameworkQueryableExtensions.SingleOrDefaultAsync(context.ApiKeys,
|
||||
data => data.Id == apiKey && data.Type != APIKeyType.Legacy);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<List<APIKeyData>> GetKeys(APIKeyQuery query)
|
||||
{
|
||||
using (var context = _applicationDbContextFactory.CreateContext())
|
||||
{
|
||||
var queryable = context.ApiKeys.AsQueryable();
|
||||
if (query?.UserId != null && query.UserId.Any())
|
||||
{
|
||||
queryable = queryable.Where(data => query.UserId.Contains(data.UserId));
|
||||
}
|
||||
|
||||
return await queryable.ToListAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task CreateKey(APIKeyData key)
|
||||
{
|
||||
if (key.Type == APIKeyType.Legacy || !string.IsNullOrEmpty(key.StoreId) || string.IsNullOrEmpty(key.UserId))
|
||||
{
|
||||
throw new InvalidOperationException("cannot save a bitpay legacy api key with this repository");
|
||||
}
|
||||
|
||||
using (var context = _applicationDbContextFactory.CreateContext())
|
||||
{
|
||||
await context.ApiKeys.AddAsync(key);
|
||||
await context.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task Remove(string id, string getUserId)
|
||||
{
|
||||
using (var context = _applicationDbContextFactory.CreateContext())
|
||||
{
|
||||
var key = await EntityFrameworkQueryableExtensions.SingleOrDefaultAsync(context.ApiKeys,
|
||||
data => data.Id == id && data.UserId == getUserId);
|
||||
context.ApiKeys.Remove(key);
|
||||
await context.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public class APIKeyQuery
|
||||
{
|
||||
public string[] UserId { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
@ -1,16 +1,9 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using OpenIddict.Validation;
|
||||
using OpenIddict.Validation.AspNetCore;
|
||||
|
||||
namespace BTCPayServer.Security
|
||||
namespace BTCPayServer.Security
|
||||
{
|
||||
public class AuthenticationSchemes
|
||||
{
|
||||
public const string Cookie = "Identity.Application";
|
||||
public const string Bitpay = "Bitpay";
|
||||
public const string OpenId = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme;
|
||||
public const string ApiKey = "GreenfieldApiKey";
|
||||
}
|
||||
}
|
||||
|
@ -66,10 +66,10 @@ namespace BTCPayServer.Security.Bitpay
|
||||
|
||||
using (var ctx = _Factory.CreateContext())
|
||||
{
|
||||
var existing = await ctx.ApiKeys.Where(o => o.StoreId == storeId).FirstOrDefaultAsync();
|
||||
if (existing != null)
|
||||
var existing = await ctx.ApiKeys.Where(o => o.StoreId == storeId && o.Type == APIKeyType.Legacy).ToListAsync();
|
||||
if (existing.Any())
|
||||
{
|
||||
ctx.ApiKeys.Remove(existing);
|
||||
ctx.ApiKeys.RemoveRange(existing);
|
||||
}
|
||||
ctx.ApiKeys.Add(new APIKeyData() { Id = new string(generated), StoreId = storeId });
|
||||
await ctx.SaveChangesAsync().ConfigureAwait(false);
|
||||
@ -95,7 +95,7 @@ namespace BTCPayServer.Security.Bitpay
|
||||
{
|
||||
using (var ctx = _Factory.CreateContext())
|
||||
{
|
||||
return await ctx.ApiKeys.Where(o => o.StoreId == storeId).Select(c => c.Id).ToArrayAsync();
|
||||
return await ctx.ApiKeys.Where(o => o.StoreId == storeId && o.Type== APIKeyType.Legacy).Select(c => c.Id).ToArrayAsync();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,19 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using OpenIddict.Abstractions;
|
||||
|
||||
namespace BTCPayServer.Security.OpenId
|
||||
{
|
||||
public static class BTCPayScopes
|
||||
{
|
||||
public const string StoreManagement = "store_management";
|
||||
public const string ServerManagement = "server_management";
|
||||
}
|
||||
public static class RestAPIPolicies
|
||||
{
|
||||
|
||||
}
|
||||
}
|
@ -1,43 +0,0 @@
|
||||
using System.Threading.Tasks;
|
||||
using OpenIdConnectRequest = OpenIddict.Abstractions.OpenIddictRequest;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Models;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.Options;
|
||||
using OpenIddict.Core;
|
||||
using OpenIddict.Server;
|
||||
using System.Security.Claims;
|
||||
|
||||
namespace BTCPayServer.Security.OpenId
|
||||
{
|
||||
public abstract class BaseOpenIdGrantHandler<T> :
|
||||
IOpenIddictServerHandler<T>
|
||||
where T : OpenIddictServerEvents.BaseContext
|
||||
{
|
||||
private readonly OpenIddictApplicationManager<BTCPayOpenIdClient> _applicationManager;
|
||||
private readonly OpenIddictAuthorizationManager<BTCPayOpenIdAuthorization> _authorizationManager;
|
||||
protected readonly SignInManager<ApplicationUser> _signInManager;
|
||||
protected readonly IOptions<IdentityOptions> _identityOptions;
|
||||
|
||||
protected BaseOpenIdGrantHandler(
|
||||
OpenIddictApplicationManager<BTCPayOpenIdClient> applicationManager,
|
||||
OpenIddictAuthorizationManager<BTCPayOpenIdAuthorization> authorizationManager,
|
||||
SignInManager<ApplicationUser> signInManager,
|
||||
IOptions<IdentityOptions> identityOptions)
|
||||
{
|
||||
_applicationManager = applicationManager;
|
||||
_authorizationManager = authorizationManager;
|
||||
_signInManager = signInManager;
|
||||
_identityOptions = identityOptions;
|
||||
}
|
||||
|
||||
|
||||
protected Task<ClaimsPrincipal> CreateClaimsPrincipalAsync(OpenIdConnectRequest request, ApplicationUser user)
|
||||
{
|
||||
return OpenIdExtensions.CreateClaimsPrincipalAsync(_applicationManager, _authorizationManager,
|
||||
_identityOptions.Value, _signInManager, request, user);
|
||||
}
|
||||
public abstract ValueTask HandleAsync(T notification);
|
||||
}
|
||||
}
|
@ -1,61 +0,0 @@
|
||||
using System.Security.Claims;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Models;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.Options;
|
||||
using OpenIddict.Abstractions;
|
||||
using OpenIddict.Core;
|
||||
using OpenIddict.EntityFrameworkCore.Models;
|
||||
using OpenIddict.Server;
|
||||
|
||||
namespace BTCPayServer.Security.OpenId
|
||||
{
|
||||
public class ClientCredentialsGrantTypeEventHandler :
|
||||
BaseOpenIdGrantHandler<OpenIddictServerEvents.HandleTokenRequestContext>
|
||||
{
|
||||
public static OpenIddictServerHandlerDescriptor Descriptor { get; } =
|
||||
OpenIddictServerHandlerDescriptor.CreateBuilder<OpenIddictServerEvents.HandleTokenRequestContext>()
|
||||
.UseScopedHandler<ClientCredentialsGrantTypeEventHandler>()
|
||||
.Build();
|
||||
private readonly OpenIddictApplicationManager<BTCPayOpenIdClient> _applicationManager;
|
||||
|
||||
private readonly UserManager<ApplicationUser> _userManager;
|
||||
|
||||
public ClientCredentialsGrantTypeEventHandler(
|
||||
OpenIddictApplicationManager<BTCPayOpenIdClient> applicationManager,
|
||||
OpenIddictAuthorizationManager<BTCPayOpenIdAuthorization> authorizationManager,
|
||||
SignInManager<ApplicationUser> signInManager,
|
||||
IOptions<IdentityOptions> identityOptions,
|
||||
UserManager<ApplicationUser> userManager) : base(applicationManager, authorizationManager, signInManager,
|
||||
identityOptions)
|
||||
{
|
||||
_applicationManager = applicationManager;
|
||||
_userManager = userManager;
|
||||
}
|
||||
|
||||
public override async ValueTask HandleAsync(
|
||||
OpenIddictServerEvents.HandleTokenRequestContext notification)
|
||||
{
|
||||
var request = notification.Request;
|
||||
var context = notification;
|
||||
if (!request.IsClientCredentialsGrantType())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var application = await _applicationManager.FindByClientIdAsync(request.ClientId);
|
||||
if (application == null)
|
||||
{
|
||||
context.Reject(
|
||||
error: OpenIddictConstants.Errors.InvalidClient,
|
||||
description: "The client application was not found in the database.");
|
||||
return;
|
||||
}
|
||||
|
||||
var user = await _userManager.FindByIdAsync(application.ApplicationUserId);
|
||||
context.Principal = await CreateClaimsPrincipalAsync(request, user);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,34 +0,0 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Models;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.Options;
|
||||
using OpenIddict.Core;
|
||||
using OpenIddict.Server;
|
||||
using Microsoft.AspNetCore;
|
||||
using OpenIddict.Server.AspNetCore;
|
||||
|
||||
namespace BTCPayServer.Security.OpenId
|
||||
{
|
||||
public class LogoutEventHandler : IOpenIddictServerHandler<OpenIddictServerEvents.HandleLogoutRequestContext>
|
||||
{
|
||||
protected readonly SignInManager<ApplicationUser> _signInManager;
|
||||
public static OpenIddictServerHandlerDescriptor Descriptor { get; } =
|
||||
OpenIddictServerHandlerDescriptor.CreateBuilder<OpenIddictServerEvents.HandleLogoutRequestContext>()
|
||||
.UseScopedHandler<LogoutEventHandler>()
|
||||
.Build();
|
||||
public LogoutEventHandler(SignInManager<ApplicationUser> signInManager)
|
||||
{
|
||||
_signInManager = signInManager;
|
||||
}
|
||||
|
||||
public async ValueTask HandleAsync(
|
||||
OpenIddictServerEvents.HandleLogoutRequestContext notification)
|
||||
{
|
||||
await _signInManager.SignOutAsync();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,106 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Models;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using OpenIddict.Abstractions;
|
||||
using OpenIddict.Core;
|
||||
using OpenIddict.Server;
|
||||
|
||||
namespace BTCPayServer.Security.OpenId
|
||||
{
|
||||
public static class OpenIdExtensions
|
||||
{
|
||||
public static ImmutableHashSet<string> Restrict(this ImmutableArray<string> scopes, ClaimsPrincipal claimsPrincipal)
|
||||
{
|
||||
HashSet<string> restricted = new HashSet<string>();
|
||||
foreach (var scope in scopes)
|
||||
{
|
||||
if (scope == BTCPayScopes.ServerManagement && !claimsPrincipal.IsInRole(Roles.ServerAdmin))
|
||||
continue;
|
||||
restricted.Add(scope);
|
||||
}
|
||||
return restricted.ToImmutableHashSet();
|
||||
}
|
||||
public static async Task<ClaimsPrincipal> CreateClaimsPrincipalAsync(OpenIddictApplicationManager<BTCPayOpenIdClient> applicationManager,
|
||||
OpenIddictAuthorizationManager<BTCPayOpenIdAuthorization> authorizationManager,
|
||||
IdentityOptions identityOptions,
|
||||
SignInManager<ApplicationUser> signInManager,
|
||||
OpenIddictRequest request,
|
||||
ApplicationUser user)
|
||||
{
|
||||
var principal = await signInManager.CreateUserPrincipalAsync(user);
|
||||
if (!request.IsAuthorizationCodeGrantType() && !request.IsRefreshTokenGrantType())
|
||||
{
|
||||
principal.SetScopes(request.GetScopes().Restrict(principal));
|
||||
}
|
||||
else if (request.IsAuthorizationCodeGrantType() &&
|
||||
string.IsNullOrEmpty(principal.GetInternalAuthorizationId()))
|
||||
{
|
||||
var app = await applicationManager.FindByClientIdAsync(request.ClientId);
|
||||
var authorizationId = await IsUserAuthorized(authorizationManager, request, user.Id, app.Id);
|
||||
if (!string.IsNullOrEmpty(authorizationId))
|
||||
{
|
||||
principal.SetInternalAuthorizationId(authorizationId);
|
||||
}
|
||||
}
|
||||
|
||||
principal.SetDestinations(identityOptions);
|
||||
return principal;
|
||||
}
|
||||
|
||||
public static void SetDestinations(this ClaimsPrincipal principal, IdentityOptions identityOptions)
|
||||
{
|
||||
foreach (var claim in principal.Claims)
|
||||
{
|
||||
claim.SetDestinations(GetDestinations(identityOptions, claim, principal));
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<string> GetDestinations(IdentityOptions identityOptions, Claim claim,
|
||||
ClaimsPrincipal principal)
|
||||
{
|
||||
switch (claim.Type)
|
||||
{
|
||||
case OpenIddictConstants.Claims.Name:
|
||||
case OpenIddictConstants.Claims.Email:
|
||||
yield return OpenIddictConstants.Destinations.AccessToken;
|
||||
yield break;
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task<string> IsUserAuthorized(
|
||||
OpenIddictAuthorizationManager<BTCPayOpenIdAuthorization> authorizationManager,
|
||||
OpenIddictRequest request, string userId, string applicationId)
|
||||
{
|
||||
var authorizations = await authorizationManager.ListAsync(queryable =>
|
||||
queryable.Where(authorization =>
|
||||
authorization.Subject == userId &&
|
||||
authorization.Application.Id == applicationId &&
|
||||
authorization.Status == OpenIddictConstants.Statuses.Valid)).ToArrayAsync();
|
||||
|
||||
if (authorizations.Length > 0)
|
||||
{
|
||||
var scopeTasks = authorizations.Select(authorization =>
|
||||
(authorizationManager.GetScopesAsync(authorization).AsTask(), authorization.Id));
|
||||
await Task.WhenAll(scopeTasks.Select((tuple) => tuple.Item1));
|
||||
|
||||
var authorizationsWithSufficientScopes = scopeTasks
|
||||
.Select((tuple) => (Id: tuple.Id, Scopes: tuple.Item1.Result))
|
||||
.Where((tuple) => !request.GetScopes().Except(tuple.Scopes).Any());
|
||||
|
||||
if (authorizationsWithSufficientScopes.Any())
|
||||
{
|
||||
return authorizationsWithSufficientScopes.First().Id;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,70 +0,0 @@
|
||||
using System.Threading.Tasks;
|
||||
using OpenIddict.Abstractions;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Models;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.Options;
|
||||
using OpenIddict.Core;
|
||||
using OpenIddict.Server;
|
||||
using Microsoft.AspNetCore;
|
||||
using OpenIddict.Server.AspNetCore;
|
||||
|
||||
namespace BTCPayServer.Security.OpenId
|
||||
{
|
||||
public class OpenIdGrantHandlerCheckCanSignIn :
|
||||
BaseOpenIdGrantHandler<OpenIddictServerEvents.HandleTokenRequestContext>
|
||||
{
|
||||
public static OpenIddictServerHandlerDescriptor Descriptor { get; } =
|
||||
OpenIddictServerHandlerDescriptor.CreateBuilder<OpenIddictServerEvents.HandleTokenRequestContext>()
|
||||
.UseScopedHandler<OpenIdGrantHandlerCheckCanSignIn>()
|
||||
.Build();
|
||||
|
||||
private readonly UserManager<ApplicationUser> _userManager;
|
||||
|
||||
public OpenIdGrantHandlerCheckCanSignIn(
|
||||
OpenIddictApplicationManager<BTCPayOpenIdClient> applicationManager,
|
||||
OpenIddictAuthorizationManager<BTCPayOpenIdAuthorization> authorizationManager,
|
||||
SignInManager<ApplicationUser> signInManager,
|
||||
IOptions<IdentityOptions> identityOptions, UserManager<ApplicationUser> userManager) : base(
|
||||
applicationManager, authorizationManager, signInManager,
|
||||
identityOptions)
|
||||
{
|
||||
_userManager = userManager;
|
||||
}
|
||||
|
||||
public override async ValueTask HandleAsync(
|
||||
OpenIddictServerEvents.HandleTokenRequestContext notification)
|
||||
{
|
||||
var request = notification.Request;
|
||||
if (!request.IsRefreshTokenGrantType() && !request.IsAuthorizationCodeGrantType())
|
||||
{
|
||||
// Allow other handlers to process the event.
|
||||
return;
|
||||
}
|
||||
|
||||
var httpContext = notification.Transaction.GetHttpRequest().HttpContext;
|
||||
var authenticateResult = (await httpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme));
|
||||
|
||||
var user = await _userManager.GetUserAsync(authenticateResult.Principal);
|
||||
if (user == null)
|
||||
{
|
||||
notification.Reject(
|
||||
error: OpenIddictConstants.Errors.InvalidGrant,
|
||||
description: "The token is no longer valid.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure the user is still allowed to sign in.
|
||||
if (!await _signInManager.CanSignInAsync(user))
|
||||
{
|
||||
notification.Reject(
|
||||
error: OpenIddictConstants.Errors.InvalidGrant,
|
||||
description: "The user is no longer allowed to sign in.");
|
||||
return;
|
||||
}
|
||||
|
||||
notification.Principal = await this.CreateClaimsPrincipalAsync(request, user);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,65 +0,0 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.U2F;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.Options;
|
||||
using OpenIddict.Abstractions;
|
||||
using OpenIddict.Core;
|
||||
using OpenIddict.Server;
|
||||
using Microsoft.AspNetCore;
|
||||
|
||||
namespace BTCPayServer.Security.OpenId
|
||||
{
|
||||
public class PasswordGrantTypeEventHandler : BaseOpenIdGrantHandler<OpenIddictServerEvents.HandleTokenRequestContext>
|
||||
{
|
||||
private readonly UserManager<ApplicationUser> _userManager;
|
||||
private readonly NicolasDorier.RateLimits.RateLimitService _rateLimitService;
|
||||
private readonly U2FService _u2FService;
|
||||
|
||||
public PasswordGrantTypeEventHandler(
|
||||
OpenIddictApplicationManager<BTCPayOpenIdClient> applicationManager,
|
||||
OpenIddictAuthorizationManager<BTCPayOpenIdAuthorization> authorizationManager,
|
||||
SignInManager<ApplicationUser> signInManager,
|
||||
UserManager<ApplicationUser> userManager,
|
||||
NicolasDorier.RateLimits.RateLimitService rateLimitService,
|
||||
IOptions<IdentityOptions> identityOptions, U2FService u2FService) : base(applicationManager,
|
||||
authorizationManager, signInManager, identityOptions)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_rateLimitService = rateLimitService;
|
||||
_u2FService = u2FService;
|
||||
}
|
||||
|
||||
public static OpenIddictServerHandlerDescriptor Descriptor { get; } =
|
||||
OpenIddictServerHandlerDescriptor.CreateBuilder<OpenIddictServerEvents.HandleTokenRequestContext>()
|
||||
.UseScopedHandler<PasswordGrantTypeEventHandler>()
|
||||
.Build();
|
||||
|
||||
public override async ValueTask HandleAsync(
|
||||
OpenIddictServerEvents.HandleTokenRequestContext notification)
|
||||
{
|
||||
var request = notification.Request;
|
||||
if (!request.IsPasswordGrantType())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var httpContext = notification.Transaction.GetHttpRequest().HttpContext;
|
||||
await _rateLimitService.Throttle(ZoneLimits.Login, httpContext.Connection.RemoteIpAddress.ToString(), httpContext.RequestAborted);
|
||||
var user = await _userManager.FindByNameAsync(request.Username);
|
||||
if (user == null || await _u2FService.HasDevices(user.Id) ||
|
||||
!(await _signInManager.CheckPasswordSignInAsync(user, request.Password, lockoutOnFailure: true))
|
||||
.Succeeded)
|
||||
{
|
||||
notification.Reject(
|
||||
error: OpenIddictConstants.Errors.InvalidGrant,
|
||||
description: "The specified credentials are invalid.");
|
||||
return;
|
||||
}
|
||||
|
||||
notification.Principal = await CreateClaimsPrincipalAsync(request, user);
|
||||
}
|
||||
}
|
||||
}
|
@ -11,6 +11,7 @@ namespace BTCPayServer.Security
|
||||
public static AuthorizationOptions AddBTCPayPolicies(this AuthorizationOptions options)
|
||||
{
|
||||
options.AddPolicy(CanModifyStoreSettings.Key);
|
||||
options.AddPolicy(CanListStoreSettings.Key);
|
||||
options.AddPolicy(CanCreateInvoice.Key);
|
||||
options.AddPolicy(CanGetRates.Key);
|
||||
options.AddPolicy(CanModifyServerSettings.Key);
|
||||
@ -30,6 +31,10 @@ namespace BTCPayServer.Security
|
||||
{
|
||||
public const string Key = "btcpay.store.canmodifystoresettings";
|
||||
}
|
||||
public class CanListStoreSettings
|
||||
{
|
||||
public const string Key = "btcpay.store.canliststoresettings";
|
||||
}
|
||||
public class CanCreateInvoice
|
||||
{
|
||||
public const string Key = "btcpay.store.cancreateinvoice";
|
||||
|
@ -1,14 +1,8 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using OpenIddict.Abstractions;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
namespace BTCPayServer.Security
|
||||
{
|
||||
@ -16,7 +10,7 @@ namespace BTCPayServer.Security
|
||||
{
|
||||
public static bool HasScopes(this AuthorizationHandlerContext context, params string[] scopes)
|
||||
{
|
||||
return scopes.All(s => context.User.HasClaim(c => c.Type == OpenIddictConstants.Claims.Scope && c.Value.Split(' ').Contains(s)));
|
||||
return scopes.All(s => context.User.HasClaim(c => c.Type.Equals("scope", StringComparison.InvariantCultureIgnoreCase) && c.Value.Split(' ').Contains(s)));
|
||||
}
|
||||
public static string GetImplicitStoreId(this HttpContext httpContext)
|
||||
{
|
||||
|
@ -153,6 +153,10 @@ namespace BTCPayServer.Services.Apps
|
||||
Sounds = settings.Sounds,
|
||||
AnimationColors = settings.AnimationColors,
|
||||
CurrencyData = _Currencies.GetCurrencyData(settings.TargetCurrency, true),
|
||||
CurrencyDataPayments = currentPayments.Select(pair => pair.Key)
|
||||
.Concat(pendingPayments.Select(pair => pair.Key)).Distinct()
|
||||
.Select(id => _Currencies.GetCurrencyData(id.CryptoCode, true))
|
||||
.ToDictionary(data => data.Code, data => data),
|
||||
Info = new ViewCrowdfundViewModel.CrowdfundInfo()
|
||||
{
|
||||
TotalContributors = paidInvoices.Length,
|
||||
|
@ -78,12 +78,12 @@ namespace BTCPayServer.Services.Stores
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<StoreData[]> GetStoresByUserId(string userId)
|
||||
public async Task<StoreData[]> GetStoresByUserId(string userId, IEnumerable<string> storeIds = null)
|
||||
{
|
||||
using (var ctx = _ContextFactory.CreateContext())
|
||||
{
|
||||
return (await ctx.UserStore
|
||||
.Where(u => u.ApplicationUserId == userId)
|
||||
.Where(u => u.ApplicationUserId == userId && (storeIds == null || storeIds.Contains(u.StoreDataId)))
|
||||
.Select(u => new { u.StoreData, u.Role })
|
||||
.ToArrayAsync())
|
||||
.Select(u =>
|
||||
|
@ -128,7 +128,7 @@
|
||||
<b-tooltip target="crowdfund-body-raised-amount" v-if="paymentStats && paymentStats.length > 0">
|
||||
<ul class="p-0 text-uppercase">
|
||||
<li v-for="stat of paymentStats" class="list-unstyled">
|
||||
{{stat.label}} <span v-if="stat.lightning" class="fa fa-bolt"></span> {{stat.value.toFixed(srvModel.currencyData.divisibility)}}
|
||||
{{stat.label}} <span v-if="stat.lightning" class="fa fa-bolt"></span> {{stat.value}}
|
||||
</li>
|
||||
</ul>
|
||||
</b-tooltip>
|
||||
|
@ -1,57 +0,0 @@
|
||||
@using BTCPayServer.Security.OpenId
|
||||
@using OpenIddict.Abstractions
|
||||
@model BTCPayServer.Models.Authorization.AuthorizeViewModel
|
||||
@{
|
||||
|
||||
var scopeMappings = new Dictionary<string, (string Title, string Description)>()
|
||||
{
|
||||
{BTCPayScopes.StoreManagement, ("Manage your stores", "The app will be able to create, modify and delete all your stores.")},
|
||||
{BTCPayScopes.ServerManagement, ("Manage your server", "The app will have total control on your server")},
|
||||
};
|
||||
}
|
||||
<form method="post">
|
||||
<input type="hidden" name="request_id" value="@Model.RequestId"/>
|
||||
<section>
|
||||
<div class="card container">
|
||||
<div class="row">
|
||||
<div class="col-lg-12 section-heading">
|
||||
<h2>Authorization Request</h2>
|
||||
<hr class="primary">
|
||||
<p>@Model.ApplicationName is requesting access to your account.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
<div class="list-group list-group-flush">
|
||||
@foreach (var scope in Model.Scope)
|
||||
{
|
||||
@if (scopeMappings.TryGetValue(scope, out var text))
|
||||
{
|
||||
<li class="list-group-item">
|
||||
<h5 class="mb-1">@text.Title</h5>
|
||||
<p class="mb-1">@text.Description.</p>
|
||||
</li>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-2">
|
||||
<div class="col-lg-12 text-center">
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-primary" name="consent" id="consent-yes" type="submit" value="Yes">Authorize app</button>
|
||||
<button type="button" class="btn btn-primary dropdown-toggle dropdown-toggle-split" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="sr-only">Toggle Dropdown</span>
|
||||
</button>
|
||||
<div class="dropdown-menu">
|
||||
<button class="dropdown-item" name="consent" id="consent-yes-temporary" type="submit" value="YesTemporary">Authorize app until session ends</button>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-secondary" id="consent-no" name="consent" type="submit" value="No">Cancel</button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</form>
|
@ -55,7 +55,8 @@
|
||||
text: @Safe.Json(Model.BitcoinUri),
|
||||
width: 200,
|
||||
height: 200,
|
||||
useSVG: true
|
||||
useSVG: true,
|
||||
correctLevel : QRCode.CorrectLevel.M
|
||||
});
|
||||
$("#qrCode > img").css({ "margin": "auto" });
|
||||
</script>
|
||||
|
@ -183,7 +183,8 @@
|
||||
</div>
|
||||
}
|
||||
|
||||
<partial name="InvoicePaymentsPartial" model="Model" />
|
||||
<partial name="ListInvoicesPaymentsPartial" model="(Model, false)" />
|
||||
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
|
@ -274,7 +274,8 @@
|
||||
<tr id="invoice_@invoice.InvoiceId" style="display:none;">
|
||||
<td colspan="99">
|
||||
<div style="margin-left: 15px; margin-bottom: 0;">
|
||||
<partial name="InvoicePaymentsPartial" model="invoice.Details" />
|
||||
@* Leaving this as partial because it abstracts complexity of Invoice Payments *@
|
||||
<partial name="ListInvoicesPaymentsPartial" model="(invoice.Details, true)" />
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
@ -1,31 +1,42 @@
|
||||
@model InvoiceDetailsModel
|
||||
@model (InvoiceDetailsModel Invoice, bool ShowAddress)
|
||||
@{ var invoice = Model.Invoice; }
|
||||
@inject PaymentMethodHandlerDictionary PaymentMethodHandlerDictionary
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12 invoice-payments">
|
||||
<h3>Current status</h3>
|
||||
<h3>Paid summary</h3>
|
||||
<table class="table table-sm table-responsive-md">
|
||||
<thead class="thead-inverse">
|
||||
<tr>
|
||||
<th>Payment method</th>
|
||||
@if (Model.ShowAddress)
|
||||
{
|
||||
<th>Address</th>
|
||||
}
|
||||
<th class="text-right">Rate</th>
|
||||
<th class="text-right">Paid</th>
|
||||
<th class="text-right">Due</th>
|
||||
@if (Model.StatusException == InvoiceExceptionStatus.PaidOver)
|
||||
@if (invoice.StatusException == InvoiceExceptionStatus.PaidOver)
|
||||
{
|
||||
<th class="text-right">Overpaid</th>
|
||||
}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var payment in Model.CryptoPayments)
|
||||
@foreach (var payment in invoice.CryptoPayments)
|
||||
{
|
||||
<tr>
|
||||
<td>@payment.PaymentMethod</td>
|
||||
@if (Model.ShowAddress)
|
||||
{
|
||||
<td title="@payment.Address">
|
||||
<span class="text-truncate d-block" style="max-width: 400px">@payment.Address</span>
|
||||
</td>
|
||||
}
|
||||
<td class="text-right">@payment.Rate</td>
|
||||
<td class="text-right">@payment.Paid</td>
|
||||
<td class="text-right">@payment.Due</td>
|
||||
@if (Model.StatusException == InvoiceExceptionStatus.PaidOver)
|
||||
@if (invoice.StatusException == InvoiceExceptionStatus.PaidOver)
|
||||
{
|
||||
<td class="text-right">@payment.Overpaid</td>
|
||||
}
|
||||
@ -36,8 +47,7 @@
|
||||
</div>
|
||||
</div>
|
||||
@{
|
||||
var grouped = Model.Payments.GroupBy(payment => payment.GetPaymentMethodId().PaymentType);
|
||||
|
||||
var grouped = invoice.Payments.GroupBy(payment => payment.GetPaymentMethodId().PaymentType);
|
||||
}
|
||||
@foreach (var paymentGroup in grouped)
|
||||
{
|
52
BTCPayServer/Views/Manage/APIKeys.cshtml
Normal file
52
BTCPayServer/Views/Manage/APIKeys.cshtml
Normal file
@ -0,0 +1,52 @@
|
||||
@model BTCPayServer.Controllers.ManageController.ApiKeysViewModel
|
||||
@{
|
||||
ViewData.SetActivePageAndTitle(ManageNavPages.APIKeys, "Manage your API Keys");
|
||||
}
|
||||
|
||||
<partial name="_StatusMessage"/>
|
||||
<h4>API Keys</h4>
|
||||
<table class="table table-lg">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Label</th>
|
||||
<th>Key</th>
|
||||
<th>Permissions</th>
|
||||
<th class="text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var keyData in Model.ApiKeyDatas)
|
||||
{
|
||||
<tr>
|
||||
<td>@keyData.Label</td>
|
||||
<td>@keyData.Id</td>
|
||||
<td>
|
||||
@if (string.IsNullOrEmpty(keyData.Permissions))
|
||||
{
|
||||
<span>No permissions</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>@string.Join(", ", keyData.GetPermissions())</span>
|
||||
}
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<a asp-action="RemoveAPIKey" asp-route-id="@keyData.Id" asp-controller="Manage">Remove</a>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
@if (!Model.ApiKeyDatas.Any())
|
||||
{
|
||||
<tr>
|
||||
<td colspan="4" class="text-center h5 py-2">
|
||||
No API keys
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
<tr class="bg-gray">
|
||||
<td colspan="4">
|
||||
<a class="btn btn-primary" asp-action="AddApiKey" id="AddApiKey">Generate new key</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
128
BTCPayServer/Views/Manage/AddApiKey.cshtml
Normal file
128
BTCPayServer/Views/Manage/AddApiKey.cshtml
Normal file
@ -0,0 +1,128 @@
|
||||
@using BTCPayServer.Controllers
|
||||
@using BTCPayServer.Security.APIKeys
|
||||
@model BTCPayServer.Controllers.ManageController.AddApiKeyViewModel
|
||||
|
||||
@{
|
||||
ViewData.SetActivePageAndTitle(ManageNavPages.APIKeys, "Add API Key");
|
||||
|
||||
string GetDescription(string permission)
|
||||
{
|
||||
return APIKeyConstants.Permissions.PermissionDescriptions[permission].Description;
|
||||
}
|
||||
|
||||
string GetTitle(string permission)
|
||||
{
|
||||
return APIKeyConstants.Permissions.PermissionDescriptions[permission].Description;
|
||||
}
|
||||
}
|
||||
|
||||
<h4>@ViewData["Title"]</h4>
|
||||
<partial name="_StatusMessage"/>
|
||||
<p >
|
||||
Generate a new api key to use BTCPay through its API.
|
||||
</p>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<form method="post" asp-action="AddApiKey" class="list-group">
|
||||
|
||||
<input type="hidden" asp-for="StoreMode" value="@Model.StoreMode"/>
|
||||
<div asp-validation-summary="All" class="text-danger"></div>
|
||||
|
||||
<div class="list-group-item ">
|
||||
<div class="form-group">
|
||||
<label asp-for="Label"></label>
|
||||
<input asp-for="Label" class="form-control"/>
|
||||
<span asp-validation-for="Label" class="text-danger"></span>
|
||||
</div>
|
||||
</div>
|
||||
@if (Model.IsServerAdmin)
|
||||
{
|
||||
<div class="list-group-item form-group">
|
||||
<input asp-for="ServerManagementPermission" class="form-check-inline"/>
|
||||
<label asp-for="ServerManagementPermission" class="h5">@GetTitle(APIKeyConstants.Permissions.ServerManagement)</label>
|
||||
<span asp-validation-for="ServerManagementPermission" class="text-danger"></span>
|
||||
<p>@GetDescription(APIKeyConstants.Permissions.ServerManagement).</p>
|
||||
</div>
|
||||
}
|
||||
@if (Model.StoreMode == ManageController.AddApiKeyViewModel.ApiKeyStoreMode.AllStores)
|
||||
{
|
||||
<div class="list-group-item form-group">
|
||||
@Html.CheckBoxFor(model => model.StoreManagementPermission, new Dictionary<string, string>() {{"class", "form-check-inline"}})
|
||||
|
||||
<label asp-for="StoreManagementPermission" class="h5">@GetTitle(APIKeyConstants.Permissions.StoreManagement)</label>
|
||||
<span asp-validation-for="StoreManagementPermission" class="text-danger"></span>
|
||||
<p class="mb-0">@GetDescription(APIKeyConstants.Permissions.StoreManagement).</p>
|
||||
<button type="submit" class="btn btn-link" name="command" value="change-store-mode">Give permission to specific stores instead</button>
|
||||
</div>
|
||||
}
|
||||
else if (Model.StoreMode == ManageController.AddApiKeyViewModel.ApiKeyStoreMode.Specific)
|
||||
{
|
||||
<div class="list-group-item p-0 border-0 mb-2">
|
||||
<li class="list-group-item ">
|
||||
<h5 class="mb-1">@GetTitle(APIKeyConstants.Permissions.StoreManagement + ":")</h5>
|
||||
<p class="mb-0">@GetDescription(APIKeyConstants.Permissions.StoreManagement + ":").</p>
|
||||
<button type="submit" class="btn btn-link" name="command" value="change-store-mode">Give permission to all stores instead</button>
|
||||
</li>
|
||||
@if (!Model.Stores.Any())
|
||||
{
|
||||
<li class="list-group-item alert-warning">
|
||||
You currently have no stores configured.
|
||||
</li>
|
||||
}
|
||||
@for (var index = 0; index < Model.SpecificStores.Count; index++)
|
||||
{
|
||||
<div class="list-group-item transaction-output-form p-0 pl-lg-2">
|
||||
<div class="row">
|
||||
<div class="col-sm-12 col-md-12 col-lg-10 py-2 ">
|
||||
<div class="form-group my-0">
|
||||
@if (Model.SpecificStores[index] == null)
|
||||
{
|
||||
<select asp-for="SpecificStores[index]" class="form-control" asp-items="@(new SelectList(Model.Stores.Where(data => !Model.SpecificStores.Contains(data.Id)), nameof(StoreData.Id), nameof(StoreData.StoreName)))"></select>
|
||||
}
|
||||
else
|
||||
{
|
||||
var store = Model.Stores.SingleOrDefault(data => data.Id == Model.SpecificStores[index]);
|
||||
<select asp-for="SpecificStores[index]" class="form-control" asp-items="@(new SelectList(new[] {store}, nameof(StoreData.Id), nameof(StoreData.StoreName), store.Id))"></select>
|
||||
}
|
||||
|
||||
<span asp-validation-for="SpecificStores[index]" class="text-danger"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-12 col-md-12 col-lg-2 pull-right">
|
||||
<button type="submit" title="Remove Store Permission" name="command" value="@($"remove-store:{index}")"
|
||||
class="d-block d-lg-none d-xl-none btn btn-danger mb-2 ml-2">
|
||||
Remove
|
||||
</button>
|
||||
<button type="submit" title="Remove Store Permission" name="command" value="@($"remove-store:{index}")"
|
||||
class="d-none d-lg-block remove-btn text-decoration-none h-100 align-middle btn text-danger btn-link fa fa-times rounded-0 pull-right">
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@if (Model.SpecificStores.Count < Model.Stores.Length)
|
||||
{
|
||||
<div class="list-group-item">
|
||||
<button type="submit" name="command" value="add-store" class="ml-1 btn btn-secondary">Add another store </button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
<button type="submit" class="btn btn-primary" id="Generate">Generate API Key</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
@await Html.PartialAsync("_ValidationScriptsPartial")
|
||||
|
||||
<style>
|
||||
.remove-btn{
|
||||
font-size: 1.5rem;
|
||||
border-radius: 0;
|
||||
}
|
||||
.remove-btn:hover{
|
||||
background-color: #CCCCCC;
|
||||
}
|
||||
</style>
|
||||
}
|
150
BTCPayServer/Views/Manage/AuthorizeAPIKey.cshtml
Normal file
150
BTCPayServer/Views/Manage/AuthorizeAPIKey.cshtml
Normal file
@ -0,0 +1,150 @@
|
||||
@using BTCPayServer.Controllers
|
||||
@using BTCPayServer.Security.APIKeys
|
||||
@model BTCPayServer.Controllers.ManageController.AuthorizeApiKeysViewModel
|
||||
|
||||
@{
|
||||
Layout = "_Layout";
|
||||
ViewData["Title"] = $"Authorize {(Model.ApplicationName ?? "Application")}";
|
||||
|
||||
string GetDescription(string permission)
|
||||
{
|
||||
return APIKeyConstants.Permissions.PermissionDescriptions[permission].Description;
|
||||
}
|
||||
|
||||
string GetTitle(string permission)
|
||||
{
|
||||
return APIKeyConstants.Permissions.PermissionDescriptions[permission].Description;
|
||||
}
|
||||
}
|
||||
|
||||
<partial name="_StatusMessage"/>
|
||||
<form method="post" asp-action="AuthorizeAPIKey">
|
||||
<input type="hidden" asp-for="Permissions" value="@Model.Permissions"/>
|
||||
<input type="hidden" asp-for="Strict" value="@Model.Strict"/>
|
||||
<input type="hidden" asp-for="ApplicationName" value="@Model.ApplicationName"/>
|
||||
<input type="hidden" asp-for="SelectiveStores" value="@Model.SelectiveStores"/>
|
||||
<section>
|
||||
<div class="card container">
|
||||
<div class="row">
|
||||
<div class="col-lg-12 section-heading">
|
||||
<h2>Authorization Request</h2>
|
||||
<hr class="primary">
|
||||
<p class="mb-1">@(Model.ApplicationName ?? "An application") is requesting access to your account.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-lg-12 list-group px-2">
|
||||
<div asp-validation-summary="All" class="text-danger"></div>
|
||||
<div class="list-group-item ">
|
||||
<div class="form-group">
|
||||
<label asp-for="Label"></label>
|
||||
<input asp-for="Label" class="form-control"/>
|
||||
<span asp-validation-for="Label" class="text-danger"></span>
|
||||
</div>
|
||||
</div>
|
||||
@if (!Model.PermissionsFormatted.Any())
|
||||
{
|
||||
<div class="list-group-item form-group">
|
||||
<p >There are no associated permissions to the API key being requested here. The application cannot do anything with your BTCPay account other than validating your account exists.</p>
|
||||
</div>
|
||||
}
|
||||
@if (Model.PermissionsFormatted.Contains(APIKeyConstants.Permissions.ServerManagement) && (Model.IsServerAdmin || Model.Strict))
|
||||
{
|
||||
<div class="list-group-item form-group">
|
||||
<input asp-for="ServerManagementPermission" class="form-check-inline" readonly="@(Model.Strict || !Model.IsServerAdmin)"/>
|
||||
<label asp-for="ServerManagementPermission" class="h5">@GetTitle(APIKeyConstants.Permissions.ServerManagement)</label>
|
||||
@if (!Model.IsServerAdmin)
|
||||
{
|
||||
<span class="text-danger">
|
||||
The server management permission is being requested but your account is not an administrator
|
||||
</span>
|
||||
}
|
||||
|
||||
<span asp-validation-for="ServerManagementPermission" class="text-danger"></span>
|
||||
<p>@GetDescription(APIKeyConstants.Permissions.ServerManagement).</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (Model.PermissionsFormatted.Contains(APIKeyConstants.Permissions.StoreManagement))
|
||||
{
|
||||
@if (Model.StoreMode == ManageController.AddApiKeyViewModel.ApiKeyStoreMode.AllStores)
|
||||
{
|
||||
<div class="list-group-item form-group">
|
||||
<input type="checkbox" asp-for="StoreManagementPermission" class="form-check-inline" readonly="@Model.Strict"/>
|
||||
<label asp-for="StoreManagementPermission" class="h5">@GetTitle(APIKeyConstants.Permissions.StoreManagement)</label>
|
||||
<span asp-validation-for="StoreManagementPermission" class="text-danger"></span>
|
||||
<p class="mb-0">@GetDescription(APIKeyConstants.Permissions.StoreManagement).</p>
|
||||
@if (Model.SelectiveStores)
|
||||
{
|
||||
<button type="submit" class="btn btn-link" name="command" value="change-store-mode">Give permission to specific stores instead</button>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
else if (Model.StoreMode == ManageController.AddApiKeyViewModel.ApiKeyStoreMode.Specific)
|
||||
{
|
||||
<div class="list-group-item p-0 border-0 mb-2">
|
||||
<li class="list-group-item">
|
||||
<h5 class="mb-1">@GetTitle(APIKeyConstants.Permissions.StoreManagement + ":")</h5>
|
||||
<p class="mb-0">@GetDescription(APIKeyConstants.Permissions.StoreManagement + ":").</p>
|
||||
<button type="submit" class="btn btn-link" name="command" value="change-store-mode">Give permission to all stores instead</button>
|
||||
</li>
|
||||
@if (!Model.Stores.Any())
|
||||
{
|
||||
<li class="list-group-item alert-warning">
|
||||
You currently have no stores configured.
|
||||
</li>
|
||||
}
|
||||
@for (var index = 0; index < Model.SpecificStores.Count; index++)
|
||||
{
|
||||
<div class="list-group-item transaction-output-form p-0 pl-lg-2">
|
||||
<div class="row">
|
||||
<div class="col-sm-12 col-md-12 col-lg-10 py-2 ">
|
||||
<div class="form-group my-0">
|
||||
@if (Model.SpecificStores[index] == null)
|
||||
{
|
||||
<select asp-for="SpecificStores[index]" class="form-control" asp-items="@(new SelectList(Model.Stores.Where(data => !Model.SpecificStores.Contains(data.Id)), nameof(StoreData.Id), nameof(StoreData.StoreName)))"></select>
|
||||
}
|
||||
else
|
||||
{
|
||||
var store = Model.Stores.SingleOrDefault(data => data.Id == Model.SpecificStores[index]);
|
||||
<select asp-for="SpecificStores[index]" class="form-control" asp-items="@(new SelectList(new[] {store}, nameof(StoreData.Id), nameof(StoreData.StoreName), store.Id))"></select>
|
||||
}
|
||||
|
||||
<span asp-validation-for="SpecificStores[index]" class="text-danger"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-12 col-md-12 col-lg-2 pull-right">
|
||||
<button type="submit" title="Remove Store Permission" name="command" value="@($"remove-store:{index}")"
|
||||
class="d-block d-lg-none d-xl-none btn btn-danger mb-2 ml-2">
|
||||
Remove
|
||||
</button>
|
||||
<button type="submit" title="Remove Store Permission" name="command" value="@($"remove-store:{index}")"
|
||||
class="d-none d-lg-block remove-btn text-decoration-none h-100 align-middle btn text-danger btn-link fa fa-times rounded-0 pull-right">
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@if (Model.SpecificStores.Count < Model.Stores.Length)
|
||||
{
|
||||
<div class="list-group-item">
|
||||
<button type="submit" name="command" value="add-store" class="ml-1 btn btn-secondary">Add another store </button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row my-2">
|
||||
<div class="col-lg-12 text-center">
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-primary" name="command" id="consent-yes" type="submit" value="Yes">Authorize app</button>
|
||||
</div>
|
||||
<button class="btn btn-secondary" id="consent-no" name="command" type="submit" value="No">Cancel</button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</form>
|
@ -56,7 +56,8 @@
|
||||
text: @Safe.Json(Model.AuthenticatorUri),
|
||||
width: 200,
|
||||
height: 200,
|
||||
useSVG: true
|
||||
useSVG: true,
|
||||
correctLevel : QRCode.CorrectLevel.M
|
||||
});
|
||||
$("#qrCode > img").css({ "margin": "auto" });
|
||||
</script>
|
||||
|
@ -7,6 +7,6 @@ namespace BTCPayServer.Views.Manage
|
||||
{
|
||||
public enum ManageNavPages
|
||||
{
|
||||
Index, ChangePassword, TwoFactorAuthentication, U2F
|
||||
Index, ChangePassword, TwoFactorAuthentication, U2F, APIKeys
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,10 @@
|
||||
@inject SignInManager<ApplicationUser> SignInManager
|
||||
|
||||
<div class="nav flex-column nav-pills">
|
||||
<a class="nav-link @ViewData.IsActivePage(ManageNavPages.Index)" asp-action="Index">Profile</a>
|
||||
<a class="nav-link @ViewData.IsActivePage(ManageNavPages.ChangePassword)" asp-action="ChangePassword" id="ChangePassword">Password</a>
|
||||
<a class="nav-link @ViewData.IsActivePage(ManageNavPages.TwoFactorAuthentication)" asp-action="TwoFactorAuthentication">Two-factor authentication</a>
|
||||
<a class="nav-link @ViewData.IsActivePage(ManageNavPages.U2F)" asp-action="U2FAuthentication">U2F Authentication</a>
|
||||
<a id="@ManageNavPages.Index.ToString()" class="nav-link @ViewData.IsActivePage(ManageNavPages.Index)" asp-action="Index">Profile</a>
|
||||
<a id="@ManageNavPages.ChangePassword.ToString()" class="nav-link @ViewData.IsActivePage(ManageNavPages.ChangePassword)" asp-action="ChangePassword">Password</a>
|
||||
<a id="@ManageNavPages.TwoFactorAuthentication.ToString()" class="nav-link @ViewData.IsActivePage(ManageNavPages.TwoFactorAuthentication)" asp-action="TwoFactorAuthentication">Two-factor authentication</a>
|
||||
<a id="@ManageNavPages.U2F.ToString()" class="nav-link @ViewData.IsActivePage(ManageNavPages.U2F)" asp-action="U2FAuthentication">U2F Authentication</a>
|
||||
<a id="@ManageNavPages.APIKeys.ToString()" class="nav-link @ViewData.IsActivePage(ManageNavPages.APIKeys)" asp-action="APIKeys">API Keys</a>
|
||||
</div>
|
||||
|
||||
|
@ -158,7 +158,8 @@
|
||||
text: @Safe.Json(Model.QRCode),
|
||||
width: 200,
|
||||
height: 200,
|
||||
useSVG: true
|
||||
useSVG: true,
|
||||
correctLevel : QRCode.CorrectLevel.M
|
||||
});
|
||||
</script>
|
||||
}
|
||||
|
@ -73,7 +73,8 @@
|
||||
text: @Safe.Json(Model.ServiceLink),
|
||||
width: 200,
|
||||
height: 200,
|
||||
useSVG: true
|
||||
useSVG: true,
|
||||
correctLevel : QRCode.CorrectLevel.M
|
||||
});
|
||||
</script>
|
||||
}
|
||||
|
@ -187,7 +187,8 @@
|
||||
text: @Safe.Json(Model.QRCode),
|
||||
width: 200,
|
||||
height: 200,
|
||||
useSVG: true
|
||||
useSVG: true,
|
||||
correctLevel : QRCode.CorrectLevel.M
|
||||
});
|
||||
</script>
|
||||
}
|
||||
|
@ -101,7 +101,8 @@
|
||||
text: @Safe.Json(Model.ServiceLink),
|
||||
width: 200,
|
||||
height: 200,
|
||||
useSVG: true
|
||||
useSVG: true,
|
||||
correctLevel : QRCode.CorrectLevel.M
|
||||
});
|
||||
</script>
|
||||
}
|
||||
|
@ -98,7 +98,8 @@
|
||||
text: @Safe.Json(Model.ServiceLink),
|
||||
width: 200,
|
||||
height: 200,
|
||||
useSVG: true
|
||||
useSVG: true,
|
||||
correctLevel : QRCode.CorrectLevel.M
|
||||
});
|
||||
</script>
|
||||
}
|
||||
|
@ -27,7 +27,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="payment__details__instruction__open-wallet" v-if="srvModel.invoiceBitcoinUrl">
|
||||
<a class="payment__details__instruction__open-wallet__btn action-button" v-bind:href="srvModel.invoiceBitcoinUrl">
|
||||
<a class="payment__details__instruction__open-wallet__btn action-button" target="_top" v-bind:href="srvModel.invoiceBitcoinUrl">
|
||||
<span>{{$t("Open in wallet")}}</span>
|
||||
</a>
|
||||
</div>
|
||||
|
@ -26,7 +26,7 @@
|
||||
{
|
||||
<div class="row">
|
||||
<div class="col-lg-12 text-center">
|
||||
<form method="post">
|
||||
<form method="post" action="@Model.ActionUrl">
|
||||
<button id="continue" type="submit" class="btn @Model.ButtonClass w-25">@Model.Action</button>
|
||||
<button type="submit" class="btn btn-secondary w-25" onclick="history.back(); return false;">Go back</button>
|
||||
</form>
|
||||
|
@ -47,6 +47,11 @@
|
||||
<br />
|
||||
<div class="row">
|
||||
<div class="col-lg-7">
|
||||
<div class="form-group">
|
||||
<label>
|
||||
<input type="checkbox" v-model="srvModel.useModal" v-on:change="inputChanges" class="form-check-inline"/>Use Modal
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>
|
||||
<input type="checkbox" v-model="buttonInlineTextMode" v-on:change="inputChanges" class="form-check-inline"/> Customize text in button
|
||||
|
@ -38,11 +38,6 @@
|
||||
<input asp-for="AmountMarkupPercentage" class="form-control"/>
|
||||
<span asp-validation-for="AmountMarkupPercentage" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="ShowFiat"></label>
|
||||
<input asp-for="ShowFiat" class="form-check"/>
|
||||
<span asp-validation-for="ShowFiat" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="Enabled"></label>
|
||||
<input asp-for="Enabled" type="checkbox" class="form-check"/>
|
||||
|
@ -14,21 +14,39 @@
|
||||
</div>
|
||||
}
|
||||
<div class="row no-gutters">
|
||||
<div class="col-lg-6 mx-auto my-auto ">
|
||||
<div class="col-lg-7 mx-auto my-auto ">
|
||||
<form method="post" asp-action="WalletReceive" class="card text-center">
|
||||
@if (string.IsNullOrEmpty(Model.Address))
|
||||
{
|
||||
<div class="card-body">
|
||||
@if (string.IsNullOrEmpty(Model.Address))
|
||||
{
|
||||
|
||||
<h2 class="card-title">Receive @Model.CryptoCode</h2>
|
||||
<button class="btn btn-lg btn-primary m-2" type="submit" name="command" value="generate-new-address">Generate @Model.CryptoCode address</button>
|
||||
}
|
||||
else
|
||||
{
|
||||
<h2 class="card-title">Next available @Model.CryptoCode address</h2>
|
||||
<noscript>
|
||||
<div class="card-body m-sm-0 p-sm-0">
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control " readonly="readonly" asp-for="Address" id="address" />
|
||||
<h3 class="card-title mb-3">Receive @Model.CryptoCode</h3>
|
||||
<button class="btn btn-lg btn-primary" type="submit" name="command" value="generate-new-address">Generate @Model.CryptoCode address</button>
|
||||
}
|
||||
else
|
||||
{
|
||||
<h3 class="card-title mb-4">Next available @Model.CryptoCode address</h3>
|
||||
<noscript>
|
||||
<div class="card-body m-sm-0 p-sm-0">
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control " readonly="readonly" asp-for="Address" id="address" />
|
||||
<div class="input-group-append">
|
||||
<span class="input-group-text fa fa-copy"> </span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" name="command" value="unreserve-current-address" class="btn btn-link">Unreserve this address</button>
|
||||
|
||||
</div>
|
||||
</noscript>
|
||||
<div class="only-for-js card-body m-sm-0 p-sm-0" id="app">
|
||||
<div class="qr-container mb-4">
|
||||
<img v-bind:src="srvModel.cryptoImage" class="qr-icon" />
|
||||
<qrcode v-bind:value="srvModel.address" :options="{ width: 256, margin: 0, color: {dark:'#000', light:'#fff'} }" tag="svg">
|
||||
</qrcode>
|
||||
</div>
|
||||
<div class="input-group copy" data-clipboard-target="#vue-address">
|
||||
<input type="text" class=" form-control " readonly="readonly" :value="srvModel.address" id="vue-address" />
|
||||
<div class="input-group-append">
|
||||
<span class="input-group-text fa fa-copy"> </span>
|
||||
</div>
|
||||
@ -37,25 +55,8 @@
|
||||
<button type="submit" name="command" value="unreserve-current-address" class="btn btn-link">Unreserve this address</button>
|
||||
|
||||
</div>
|
||||
</noscript>
|
||||
<div class="only-for-js card-body m-sm-0 p-sm-0" id="app">
|
||||
<div class="qr-container mb-2">
|
||||
<img v-bind:src="srvModel.cryptoImage" class="qr-icon" />
|
||||
<qrcode v-bind:value="srvModel.address" :options="{ width: 256, margin: 0, color: {dark:'#000', light:'#fff'} }" tag="svg">
|
||||
</qrcode>
|
||||
</div>
|
||||
<div class="input-group copy" data-clipboard-target="#vue-address">
|
||||
<input type="text" class=" form-control " readonly="readonly" :value="srvModel.address" id="vue-address" />
|
||||
<div class="input-group-append">
|
||||
<span class="input-group-text fa fa-copy"> </span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" name="command" value="unreserve-current-address" class="btn btn-link">Unreserve this address</button>
|
||||
|
||||
</div>
|
||||
}
|
||||
|
||||
}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -109,11 +109,8 @@ addLoadEvent(function (ev) {
|
||||
return this.srvModel.targetCurrency.toUpperCase();
|
||||
},
|
||||
paymentStats: function(){
|
||||
var result= [];
|
||||
|
||||
var result= [];
|
||||
var combinedStats = {};
|
||||
|
||||
|
||||
var keys = Object.keys(this.srvModel.info.paymentStats);
|
||||
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
@ -135,20 +132,24 @@ addLoadEvent(function (ev) {
|
||||
}
|
||||
|
||||
keys = Object.keys(combinedStats);
|
||||
|
||||
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
var newItem = {key:keys[i], value: combinedStats[keys[i]], label: keys[i].replace("_","")};
|
||||
result.push(newItem);
|
||||
|
||||
}
|
||||
for (var i = 0; i < result.length; i++) {
|
||||
var current = result[i];
|
||||
if(current.label.endsWith("LightningLike")){
|
||||
current.label = current.label.substr(0,current.label.indexOf("LightningLike"));
|
||||
current.lightning = true;
|
||||
if(!combinedStats[keys[i]]){
|
||||
continue;
|
||||
}
|
||||
var paymentMethodId = keys[i].split("_");
|
||||
var value = combinedStats[keys[i]].toFixed(this.srvModel.currencyDataPayments[paymentMethodId[0]].divisibility);
|
||||
var newItem = {key:keys[i], value: value, label: paymentMethodId[0]};
|
||||
|
||||
if(paymentMethodId.length > 1 && paymentMethodId[1].endsWith("LightningLike")){
|
||||
newItem.lightning = true;
|
||||
}
|
||||
result.push(newItem);
|
||||
}
|
||||
|
||||
if(result.length === 1 && result[0].label === srvModel.targetCurrency){
|
||||
return [];
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
perks: function(){
|
||||
@ -255,7 +256,7 @@ addLoadEvent(function (ev) {
|
||||
} );
|
||||
});
|
||||
eventAggregator.$on("payment-received", function (amount, cryptoCode, type) {
|
||||
var onChain = type.toLowerCase() === "btclike";
|
||||
var onChain = type.toLowerCase() !== "lightninglike";
|
||||
if(self.sound) {
|
||||
playRandomSound();
|
||||
}
|
||||
|
@ -41,6 +41,34 @@ function getStyles (styles) {
|
||||
return document.getElementById(styles).innerHTML.trim().replace(/\s{2}/g, '') + '\n'
|
||||
}
|
||||
|
||||
function getScripts(srvModel) {
|
||||
return ""+
|
||||
"<script>" +
|
||||
"if(!window.btcpay){ " +
|
||||
" var head = document.getElementsByTagName('head')[0];" +
|
||||
" var script = document.createElement('script');" +
|
||||
" script.src='"+esc(srvModel.urlRoot)+"modal/btcpay.js';" +
|
||||
" script.type = 'text/javascript';" +
|
||||
" head.append(script);" +
|
||||
"}" +
|
||||
"function onBTCPayFormSubmit(event){" +
|
||||
" var xhttp = new XMLHttpRequest();" +
|
||||
" xhttp.onreadystatechange = function() {" +
|
||||
" if (this.readyState == 4 && this.status == 200) {" +
|
||||
" if(this.status == 200 && this.responseText){" +
|
||||
" var response = JSON.parse(this.responseText);" +
|
||||
" window.btcpay.showInvoice(response.invoiceId);" +
|
||||
" }" +
|
||||
" }" +
|
||||
" };" +
|
||||
" xhttp.open(\"POST\", event.target.getAttribute('action'), true);" +
|
||||
" xhttp.send(new FormData( event.target ));" +
|
||||
"}" +
|
||||
"</script>";
|
||||
}
|
||||
|
||||
|
||||
|
||||
function inputChanges(event, buttonSize) {
|
||||
if (buttonSize !== null && buttonSize !== undefined) {
|
||||
srvModel.buttonSize = buttonSize;
|
||||
@ -64,14 +92,16 @@ function inputChanges(event, buttonSize) {
|
||||
width = "209px";
|
||||
height = "57px";
|
||||
}
|
||||
|
||||
var html =
|
||||
//Scripts
|
||||
(srvModel.useModal? getScripts(srvModel) :"") +
|
||||
// Styles
|
||||
getStyles('template-paybutton-styles') + (isSlider ? getStyles('template-slider-styles') : '') +
|
||||
// Form
|
||||
'<form method="POST" action="' + esc(srvModel.urlRoot) + 'api/v1/invoices" class="btcpay-form btcpay-form--' + (srvModel.fitButtonInline ? 'inline' : 'block') +'">\n' +
|
||||
'<form method="POST" '+ ( srvModel.useModal? ' onsubmit="onBTCPayFormSubmit(event);return false" ' : '' )+' action="' + esc(srvModel.urlRoot) + 'api/v1/invoices" class="btcpay-form btcpay-form--' + (srvModel.fitButtonInline ? 'inline' : 'block') +'">\n' +
|
||||
addInput("storeId", srvModel.storeId);
|
||||
|
||||
if (srvModel.useModal) html += addInput("jsonResponse", true);
|
||||
if (srvModel.checkoutDesc) html += addInput("checkoutDesc", srvModel.checkoutDesc);
|
||||
|
||||
if (srvModel.orderId) html += addInput("orderId", srvModel.orderId);
|
||||
|
@ -1,5 +1,5 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<Version>1.0.3.160</Version>
|
||||
<Version>1.0.3.162</Version>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
|
@ -1,51 +0,0 @@
|
||||
# Developing and extending themes
|
||||
|
||||
The BTCPay Server user interface is built on a customized version of Bootstrap that supports [CSS custom properties](https://developer.mozilla.org/en-US/docs/Web/CSS/--*).
|
||||
This allows us to change theme related settings like fonts and colors without affecting the [`bootstrap.css`](#Notes-on-bootstrapcss).
|
||||
Also we can provide just the relevant customized parts instead of shipping a whole `bootstrap.css` file for each theme.
|
||||
|
||||
Take a look at the [predefined themes](../BTCPayServer/wwwroot/main/themes) to get an overview of this approach.
|
||||
|
||||
## Modifying existing themes
|
||||
|
||||
The custom property definitions in the `:root` selector are divided into several sections, that can be seen as a cascade:
|
||||
|
||||
- The first section contains general definitions (i.e. for custom brand and neutral colors).
|
||||
- The second section defines variables for specific purposes.
|
||||
Here you can map the general definitions or create additional ones.
|
||||
- The third section contains definitions for specific parts of the page, sections or components.
|
||||
Here you should try to reuse definitions from above as much as possible to provide a consistent look and feel.
|
||||
|
||||
The variables defined in a theme file get used in the [`site.css`](../BTCPayServer/wwwroot/main/site.css) and [`creative.css`](../BTCPayServer/wwwroot/main/bootstrap4-creativestart/creative.css) files.
|
||||
|
||||
### Overriding Bootstrap selectors
|
||||
|
||||
In addition to the variables you can also provide styles by directly adding CSS selectors to this file.
|
||||
This can be seen as a last resort in case there is no variable for something you want to change or some minor tweaking.
|
||||
|
||||
### Adding theme variables
|
||||
|
||||
In general it is a good idea to introduce specific variables for special purposes (like setting the link colors of a specific section).
|
||||
This allows us to address individual portions of the styles without affecting other parts which might be tight to a general variable.
|
||||
|
||||
For cases in which you want to introduce new variables that are used across all themes, add them to the `site.css` file.
|
||||
This file contains our modifications of the Bootstrap styles.
|
||||
Refrain from modifying `bootstrap.css` directly – see the [additional notes](#Notes-on-bootstrapcss) for the reasoning behind this.
|
||||
|
||||
## Adding a new theme
|
||||
|
||||
You should copy one of our predefined themes and change the variables to fit your needs.
|
||||
|
||||
To test and play around with the adjustments, you can also use the developer tools of the browser:
|
||||
Inspect the `<html>` element and modify the variables in the `:root` section of the styles inspector.
|
||||
|
||||
## Notes on bootstrap.css
|
||||
|
||||
The `bootstrap.css` file itself is generated based on what the original vendor `bootstrap.css` provides.
|
||||
|
||||
Right now [Bootstrap](https://getbootstrap.com/docs/4.3/getting-started/theming/) does not use custom properties, but in the future it is likely that they might switch to this approach as well.
|
||||
Until then we created a build script [in this repo](https://github.com/dennisreimann/btcpayserver-ui-prototype) which generates the `bootstrap.css` file we are using here.
|
||||
|
||||
The general approach should be to not modify the `bootstrap.css`, so that we can keep it easily updatable.
|
||||
The initial modifications of this file were made in order to allow for this themeing approach.
|
||||
Because bootstrap has colors spread all over the place we'd otherwise have to override mostly everything, that's why these general modifications are in the main `bootstrap.css` file.
|
@ -81,16 +81,17 @@ Contributors looking to do something a bit more challenging, before opening a pu
|
||||
|
||||
- [Setting up development environment on Windows](https://www.youtube.com/watch?v=ZePbMPSIvHM)
|
||||
- [Setting up development environment Linux (Ubuntu)](https://www.youtube.com/watch?v=j486T_Rk-yw&t)
|
||||
- [Setting up development environment MacOS](https://www.youtube.com/watch?v=GWR_CcMsEV0)
|
||||
|
||||
You also have an awesome video of our contributors which explains how to get started.[](https://www.youtube.com/embed/VNMnd-dX9Q8)
|
||||
|
||||
Here is some info about [how to extend the themes](Docs/Themes.md)
|
||||
Here is some info about [how to extend the themes](https://github.com/btcpayserver/btcpayserver-doc/blob/master/Theme.md)
|
||||
|
||||
## How to build
|
||||
|
||||
While the documentation advises to use docker-compose, you may want to build BTCPay yourself.
|
||||
While the documentation advises to use docker-compose, you may want to build BTCPay Server yourself.
|
||||
|
||||
First install .NET Core SDK v2.1.9 as specified by [Microsoft website](https://www.microsoft.com/net/download/dotnet-core/2.1).
|
||||
First install .NET Core SDK v3.1 as specified by [Microsoft website](https://dotnet.microsoft.com/download/dotnet-core/3.1).
|
||||
|
||||
On Powershell:
|
||||
```
|
||||
@ -118,7 +119,7 @@ On linux:
|
||||
|
||||
## How to debug
|
||||
|
||||
If you want to debug, use Visual Studio Code or Visual Studio 2017.
|
||||
If you want to debug, use Visual Studio Code or Visual Studio 2019.
|
||||
|
||||
You need to run the development time docker-compose as described [in the test guide](BTCPayServer.Tests/README.md).
|
||||
|
||||
|
Reference in New Issue
Block a user