mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-02-20 13:34:37 +01:00
Init
This commit is contained in:
commit
b5c6ed3860
228 changed files with 63977 additions and 0 deletions
289
.gitignore
vendored
Normal file
289
.gitignore
vendored
Normal file
|
@ -0,0 +1,289 @@
|
|||
## Ignore Visual Studio temporary files, build results, and
|
||||
## files generated by popular Visual Studio add-ons.
|
||||
##
|
||||
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
|
||||
|
||||
# User-specific files
|
||||
*.suo
|
||||
*.user
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
.vs/
|
||||
|
||||
# User-specific files (MonoDevelop/Xamarin Studio)
|
||||
*.userprefs
|
||||
|
||||
# Build results
|
||||
[Dd]ebug/
|
||||
[Dd]ebugPublic/
|
||||
[Rr]elease/
|
||||
[Rr]eleases/
|
||||
x64/
|
||||
x86/
|
||||
bld/
|
||||
[Bb]in/
|
||||
[Oo]bj/
|
||||
[Ll]og/
|
||||
|
||||
# Visual Studio 2015 cache/options directory
|
||||
.vs/
|
||||
# Uncomment if you have tasks that create the project's static files in wwwroot
|
||||
#wwwroot/
|
||||
|
||||
# MSTest test Results
|
||||
[Tt]est[Rr]esult*/
|
||||
[Bb]uild[Ll]og.*
|
||||
|
||||
# NUNIT
|
||||
*.VisualState.xml
|
||||
TestResult.xml
|
||||
|
||||
# Build Results of an ATL Project
|
||||
[Dd]ebugPS/
|
||||
[Rr]eleasePS/
|
||||
dlldata.c
|
||||
|
||||
# .NET Core
|
||||
project.lock.json
|
||||
project.fragment.lock.json
|
||||
artifacts/
|
||||
**/Properties/launchSettings.json
|
||||
|
||||
*_i.c
|
||||
*_p.c
|
||||
*_i.h
|
||||
*.ilk
|
||||
*.meta
|
||||
*.obj
|
||||
*.pch
|
||||
*.pdb
|
||||
*.pgc
|
||||
*.pgd
|
||||
*.rsp
|
||||
*.sbr
|
||||
*.tlb
|
||||
*.tli
|
||||
*.tlh
|
||||
*.tmp
|
||||
*.tmp_proj
|
||||
*.log
|
||||
*.vspscc
|
||||
*.vssscc
|
||||
.builds
|
||||
*.pidb
|
||||
*.svclog
|
||||
*.scc
|
||||
|
||||
# Chutzpah Test files
|
||||
_Chutzpah*
|
||||
|
||||
# Visual C++ cache files
|
||||
ipch/
|
||||
*.aps
|
||||
*.ncb
|
||||
*.opendb
|
||||
*.opensdf
|
||||
*.sdf
|
||||
*.cachefile
|
||||
*.VC.db
|
||||
*.VC.VC.opendb
|
||||
|
||||
# Visual Studio profiler
|
||||
*.psess
|
||||
*.vsp
|
||||
*.vspx
|
||||
*.sap
|
||||
|
||||
# TFS 2012 Local Workspace
|
||||
$tf/
|
||||
|
||||
# Guidance Automation Toolkit
|
||||
*.gpState
|
||||
|
||||
# ReSharper is a .NET coding add-in
|
||||
_ReSharper*/
|
||||
*.[Rr]e[Ss]harper
|
||||
*.DotSettings.user
|
||||
|
||||
# JustCode is a .NET coding add-in
|
||||
.JustCode
|
||||
|
||||
# TeamCity is a build add-in
|
||||
_TeamCity*
|
||||
|
||||
# DotCover is a Code Coverage Tool
|
||||
*.dotCover
|
||||
|
||||
# Visual Studio code coverage results
|
||||
*.coverage
|
||||
*.coveragexml
|
||||
|
||||
# NCrunch
|
||||
_NCrunch_*
|
||||
.*crunch*.local.xml
|
||||
nCrunchTemp_*
|
||||
|
||||
# MightyMoose
|
||||
*.mm.*
|
||||
AutoTest.Net/
|
||||
|
||||
# Web workbench (sass)
|
||||
.sass-cache/
|
||||
|
||||
# Installshield output folder
|
||||
[Ee]xpress/
|
||||
|
||||
# DocProject is a documentation generator add-in
|
||||
DocProject/buildhelp/
|
||||
DocProject/Help/*.HxT
|
||||
DocProject/Help/*.HxC
|
||||
DocProject/Help/*.hhc
|
||||
DocProject/Help/*.hhk
|
||||
DocProject/Help/*.hhp
|
||||
DocProject/Help/Html2
|
||||
DocProject/Help/html
|
||||
|
||||
# Click-Once directory
|
||||
publish/
|
||||
|
||||
# Publish Web Output
|
||||
*.[Pp]ublish.xml
|
||||
*.azurePubxml
|
||||
# TODO: Comment the next line if you want to checkin your web deploy settings
|
||||
# but database connection strings (with potential passwords) will be unencrypted
|
||||
*.pubxml
|
||||
*.publishproj
|
||||
|
||||
# Microsoft Azure Web App publish settings. Comment the next line if you want to
|
||||
# checkin your Azure Web App publish settings, but sensitive information contained
|
||||
# in these scripts will be unencrypted
|
||||
PublishScripts/
|
||||
|
||||
# NuGet Packages
|
||||
*.nupkg
|
||||
# The packages folder can be ignored because of Package Restore
|
||||
**/packages/*
|
||||
# except build/, which is used as an MSBuild target.
|
||||
!**/packages/build/
|
||||
# Uncomment if necessary however generally it will be regenerated when needed
|
||||
#!**/packages/repositories.config
|
||||
# NuGet v3's project.json files produces more ignorable files
|
||||
*.nuget.props
|
||||
*.nuget.targets
|
||||
|
||||
# Microsoft Azure Build Output
|
||||
csx/
|
||||
*.build.csdef
|
||||
|
||||
# Microsoft Azure Emulator
|
||||
ecf/
|
||||
rcf/
|
||||
|
||||
# Windows Store app package directories and files
|
||||
AppPackages/
|
||||
BundleArtifacts/
|
||||
Package.StoreAssociation.xml
|
||||
_pkginfo.txt
|
||||
|
||||
# Visual Studio cache files
|
||||
# files ending in .cache can be ignored
|
||||
*.[Cc]ache
|
||||
# but keep track of directories ending in .cache
|
||||
!*.[Cc]ache/
|
||||
|
||||
# Others
|
||||
ClientBin/
|
||||
~$*
|
||||
*~
|
||||
*.dbmdl
|
||||
*.dbproj.schemaview
|
||||
*.jfm
|
||||
*.pfx
|
||||
*.publishsettings
|
||||
orleans.codegen.cs
|
||||
|
||||
# Since there are multiple workflows, uncomment next line to ignore bower_components
|
||||
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
|
||||
#bower_components/
|
||||
|
||||
# RIA/Silverlight projects
|
||||
Generated_Code/
|
||||
|
||||
# Backup & report files from converting an old project file
|
||||
# to a newer Visual Studio version. Backup files are not needed,
|
||||
# because we have git ;-)
|
||||
_UpgradeReport_Files/
|
||||
Backup*/
|
||||
UpgradeLog*.XML
|
||||
UpgradeLog*.htm
|
||||
|
||||
# SQL Server files
|
||||
*.mdf
|
||||
*.ldf
|
||||
*.ndf
|
||||
|
||||
# Business Intelligence projects
|
||||
*.rdl.data
|
||||
*.bim.layout
|
||||
*.bim_*.settings
|
||||
|
||||
# Microsoft Fakes
|
||||
FakesAssemblies/
|
||||
|
||||
# GhostDoc plugin setting file
|
||||
*.GhostDoc.xml
|
||||
|
||||
# Node.js Tools for Visual Studio
|
||||
.ntvs_analysis.dat
|
||||
node_modules/
|
||||
|
||||
# Typescript v1 declaration files
|
||||
typings/
|
||||
|
||||
# Visual Studio 6 build log
|
||||
*.plg
|
||||
|
||||
# Visual Studio 6 workspace options file
|
||||
*.opt
|
||||
|
||||
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
|
||||
*.vbw
|
||||
|
||||
# Visual Studio LightSwitch build output
|
||||
**/*.HTMLClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/ModelManifest.xml
|
||||
**/*.Server/GeneratedArtifacts
|
||||
**/*.Server/ModelManifest.xml
|
||||
_Pvt_Extensions
|
||||
|
||||
# Paket dependency manager
|
||||
.paket/paket.exe
|
||||
paket-files/
|
||||
|
||||
# FAKE - F# Make
|
||||
.fake/
|
||||
|
||||
# JetBrains Rider
|
||||
.idea/
|
||||
*.sln.iml
|
||||
|
||||
# CodeRush
|
||||
.cr/
|
||||
|
||||
# Python Tools for Visual Studio (PTVS)
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
# Cake - Uncomment if you are using it
|
||||
# tools/**
|
||||
# !tools/packages.config
|
||||
|
||||
# Telerik's JustMock configuration file
|
||||
*.jmconfig
|
||||
|
||||
# BizTalk build output
|
||||
*.btp.cs
|
||||
*.btm.cs
|
||||
*.odx.cs
|
||||
*.xsd.cs
|
20
BTCPayServer.Tests/BTCPayServer.Tests.csproj
Normal file
20
BTCPayServer.Tests/BTCPayServer.Tests.csproj
Normal file
|
@ -0,0 +1,20 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp2.0</TargetFramework>
|
||||
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.3.0-preview-20170628-02" />
|
||||
<PackageReference Include="NBitcoin.TestFramework" Version="1.4.4" />
|
||||
<PackageReference Include="xunit" Version="2.2.0" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.2.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\BTCPayServer\BTCPayServer.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
141
BTCPayServer.Tests/BTCPayServerTester.cs
Normal file
141
BTCPayServer.Tests/BTCPayServerTester.cs
Normal file
|
@ -0,0 +1,141 @@
|
|||
using BTCPayServer.Configuration;
|
||||
using BTCPayServer.Hosting;
|
||||
using BTCPayServer.Invoicing;
|
||||
using BTCPayServer.RateProvider;
|
||||
using BTCPayServer.Tests.Logging;
|
||||
using BTCPayServer.Tests.Mocks;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http.Internal;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Controllers;
|
||||
using Microsoft.AspNetCore.Mvc.Routing;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NBitcoin;
|
||||
using NBitcoin.Tests;
|
||||
using NBXplorer;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Security.Claims;
|
||||
using System.Security.Principal;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using Xunit;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
public class BTCPayServerTester : IDisposable
|
||||
{
|
||||
private string _Directory;
|
||||
|
||||
public BTCPayServerTester(string scope)
|
||||
{
|
||||
this._Directory = scope ?? throw new ArgumentNullException(nameof(scope));
|
||||
}
|
||||
|
||||
public Uri NBXplorerUri
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public string CookieFile
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public Uri ServerUri
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public ExtKey HDPrivateKey
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
IWebHost _Host;
|
||||
public void Start()
|
||||
{
|
||||
if(!Directory.Exists(_Directory))
|
||||
Directory.CreateDirectory(_Directory);
|
||||
|
||||
HDPrivateKey = new ExtKey();
|
||||
var port = Utils.FreeTcpPort();
|
||||
StringBuilder config = new StringBuilder();
|
||||
config.AppendLine($"regtest=1");
|
||||
config.AppendLine($"port={port}");
|
||||
config.AppendLine($"explorer.url={NBXplorerUri.AbsoluteUri}");
|
||||
config.AppendLine($"explorer.cookiefile={CookieFile}");
|
||||
config.AppendLine($"hdpubkey={HDPrivateKey.Neuter().ToString(Network.RegTest)}");
|
||||
File.WriteAllText(Path.Combine(_Directory, "settings.config"), config.ToString());
|
||||
|
||||
ServerUri = new Uri("http://127.0.0.1:" + port + "/");
|
||||
|
||||
BTCPayServerOptions options = new BTCPayServerOptions();
|
||||
options.LoadArgs(new TextFileConfiguration(new string[] { "-datadir", _Directory }));
|
||||
|
||||
_Host = new WebHostBuilder()
|
||||
.ConfigureServices(s =>
|
||||
{
|
||||
s.AddSingleton<IRateProvider>(new MockRateProvider(new Rate("USD", 5000m)));
|
||||
s.AddLogging(l =>
|
||||
{
|
||||
l.SetMinimumLevel(LogLevel.Information)
|
||||
.AddFilter("Microsoft", LogLevel.Error)
|
||||
.AddProvider(Logs.LogProvider);
|
||||
});
|
||||
})
|
||||
.AddPayServer(options)
|
||||
.UseKestrel()
|
||||
.UseStartup<Startup>()
|
||||
.Build();
|
||||
_Host.Start();
|
||||
Runtime = (BTCPayServerRuntime)_Host.Services.GetService(typeof(BTCPayServerRuntime));
|
||||
var watcher = (InvoiceWatcher)_Host.Services.GetService(typeof(InvoiceWatcher));
|
||||
watcher.PollInterval = TimeSpan.FromMilliseconds(50);
|
||||
}
|
||||
|
||||
public BTCPayServerRuntime Runtime
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public T GetController<T>(string userId = null) where T : Controller
|
||||
{
|
||||
var context = new DefaultHttpContext();
|
||||
context.Request.Host = new HostString("127.0.0.1");
|
||||
context.Request.Scheme = "http";
|
||||
context.Request.Protocol = "http";
|
||||
if(userId != null)
|
||||
{
|
||||
context.User = new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(ClaimTypes.NameIdentifier, userId) }));
|
||||
}
|
||||
var scope = (IServiceScopeFactory)_Host.Services.GetService(typeof(IServiceScopeFactory));
|
||||
var provider = scope.CreateScope().ServiceProvider;
|
||||
context.RequestServices = provider;
|
||||
|
||||
var httpAccessor = provider.GetRequiredService<IHttpContextAccessor>();
|
||||
httpAccessor.HttpContext = context;
|
||||
|
||||
var controller = (T)ActivatorUtilities.CreateInstance(provider, typeof(T));
|
||||
controller.Url = new UrlHelperMock();
|
||||
controller.ControllerContext = new ControllerContext()
|
||||
{
|
||||
HttpContext = context
|
||||
};
|
||||
return controller;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if(_Host != null)
|
||||
_Host.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
89
BTCPayServer.Tests/Logging/Logs.cs
Normal file
89
BTCPayServer.Tests/Logging/Logs.cs
Normal file
|
@ -0,0 +1,89 @@
|
|||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace BTCPayServer.Tests.Logging
|
||||
{
|
||||
public interface ILog
|
||||
{
|
||||
void LogInformation(string msg);
|
||||
}
|
||||
|
||||
public class XUnitLogProvider : ILoggerProvider
|
||||
{
|
||||
ITestOutputHelper _Helper;
|
||||
public XUnitLogProvider(ITestOutputHelper helper)
|
||||
{
|
||||
_Helper = helper;
|
||||
}
|
||||
public ILogger CreateLogger(string categoryName)
|
||||
{
|
||||
return new XUnitLog(_Helper) { Name = categoryName };
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
public class XUnitLog : ILog, ILogger, IDisposable
|
||||
{
|
||||
ITestOutputHelper _Helper;
|
||||
public XUnitLog(ITestOutputHelper helper)
|
||||
{
|
||||
_Helper = helper;
|
||||
}
|
||||
|
||||
public string Name
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public IDisposable BeginScope<TState>(TState state)
|
||||
{
|
||||
return this;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public bool IsEnabled(LogLevel logLevel)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
|
||||
{
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.Append(formatter(state, exception));
|
||||
if(exception != null)
|
||||
{
|
||||
builder.AppendLine();
|
||||
builder.Append(exception.ToString());
|
||||
}
|
||||
LogInformation(builder.ToString());
|
||||
}
|
||||
|
||||
public void LogInformation(string msg)
|
||||
{
|
||||
if(msg != null)
|
||||
_Helper.WriteLine(Name + ": " + msg);
|
||||
}
|
||||
}
|
||||
public class Logs
|
||||
{
|
||||
public static ILog Tester
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public static XUnitLogProvider LogProvider
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
}
|
||||
}
|
38
BTCPayServer.Tests/Mocks/UrlHelperMock.cs
Normal file
38
BTCPayServer.Tests/Mocks/UrlHelperMock.cs
Normal file
|
@ -0,0 +1,38 @@
|
|||
using Microsoft.AspNetCore.Mvc;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using Microsoft.AspNetCore.Mvc.Routing;
|
||||
|
||||
namespace BTCPayServer.Tests.Mocks
|
||||
{
|
||||
public class UrlHelperMock : IUrlHelper
|
||||
{
|
||||
public ActionContext ActionContext => throw new NotImplementedException();
|
||||
|
||||
public string Action(UrlActionContext actionContext)
|
||||
{
|
||||
return "http://127.0.0.1/mock";
|
||||
}
|
||||
|
||||
public string Content(string contentPath)
|
||||
{
|
||||
return "http://127.0.0.1/mock";
|
||||
}
|
||||
|
||||
public bool IsLocalUrl(string url)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public string Link(string routeName, object values)
|
||||
{
|
||||
return "http://127.0.0.1/mock";
|
||||
}
|
||||
|
||||
public string RouteUrl(UrlRouteContext routeContext)
|
||||
{
|
||||
return "http://127.0.0.1/mock";
|
||||
}
|
||||
}
|
||||
}
|
106
BTCPayServer.Tests/NBXplorerTester.cs
Normal file
106
BTCPayServer.Tests/NBXplorerTester.cs
Normal file
|
@ -0,0 +1,106 @@
|
|||
using NBitcoin;
|
||||
using NBitcoin.Tests;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using Xunit;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
public class NBXplorerTester : IDisposable
|
||||
{
|
||||
private string _Directory;
|
||||
|
||||
public NBXplorerTester(string scope)
|
||||
{
|
||||
if(scope == null)
|
||||
throw new ArgumentNullException(nameof(scope));
|
||||
this._Directory = scope;
|
||||
}
|
||||
|
||||
Process _Process;
|
||||
|
||||
public CoreNode Node
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public void Start()
|
||||
{
|
||||
ProcessLauncher launcher = new ProcessLauncher();
|
||||
launcher.GoTo("Repositories", true);
|
||||
if(!launcher.GoTo(new[] { "nxbplorer", "NBXplorer" }))
|
||||
{
|
||||
launcher.Run("git", "clone https://github.com/dgarage/NBXplorer nxbplorer");
|
||||
Assert.True(launcher.GoTo(new[] { "nxbplorer", "NBXplorer" }), "Could not clone nxbplorer");
|
||||
}
|
||||
launcher.PushDirectory();
|
||||
if(!launcher.GoTo(new[] { "bin", "Release", "netcoreapp2.0" }) || !launcher.Exists("NBXplorer.dll"))
|
||||
{
|
||||
launcher.PopDirectory();
|
||||
launcher.Run("git", "pull");
|
||||
launcher.Run("git", "checkout master");
|
||||
|
||||
launcher.Run("dotnet", "build /p:Configuration=Release");
|
||||
Assert.True(launcher.GoTo(new[] { "bin", "Release", "netcoreapp2.0" }), "Could not build NBXplorer");
|
||||
|
||||
launcher.AssertExists("NBXplorer.dll");
|
||||
}
|
||||
|
||||
var port = Utils.FreeTcpPort();
|
||||
var launcher2 = new ProcessLauncher();
|
||||
launcher2.GoTo(_Directory, true);
|
||||
launcher2.GoTo("nbxplorer-datadir", true);
|
||||
StringBuilder config = new StringBuilder();
|
||||
config.AppendLine($"regtest=1");
|
||||
config.AppendLine($"port={port}");
|
||||
config.AppendLine($"rpc.url={Node.RPCUri.AbsoluteUri}");
|
||||
config.AppendLine($"rpc.auth={Node.AuthenticationString}");
|
||||
config.AppendLine($"node.endpoint={Node.NodeEndpoint.Address}:{Node.NodeEndpoint.Port}");
|
||||
File.WriteAllText(Path.Combine(launcher2.CurrentDirectory, "settings.config"), config.ToString());
|
||||
_Process = launcher.Start("dotnet", $"NBXplorer.dll -datadir \"{launcher2.CurrentDirectory}\"");
|
||||
ExplorerClient = new NBXplorer.ExplorerClient(Node.Network, new Uri($"http://127.0.0.1:{port}/"));
|
||||
CookieFile = Path.Combine(launcher2.CurrentDirectory, ".cookie");
|
||||
File.Create(CookieFile).Close(); //Will be wipedout when the client starts
|
||||
ExplorerClient.SetCookieFile(CookieFile);
|
||||
try
|
||||
{
|
||||
var cancellationSource = new CancellationTokenSource(10000);
|
||||
ExplorerClient.WaitServerStarted(cancellationSource.Token);
|
||||
}
|
||||
catch(OperationCanceledException)
|
||||
{
|
||||
Assert.False(_Process.HasExited, "NBXplorer failed to launch");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public NBXplorer.ExplorerClient ExplorerClient
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public string CookieFile
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public static NBXplorerTester Create([CallerMemberNameAttribute] string scope = null)
|
||||
{
|
||||
return new NBXplorerTester(scope);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if(_Process != null && !_Process.HasExited)
|
||||
_Process.Kill();
|
||||
}
|
||||
}
|
||||
}
|
112
BTCPayServer.Tests/ProcessLauncher.cs
Normal file
112
BTCPayServer.Tests/ProcessLauncher.cs
Normal file
|
@ -0,0 +1,112 @@
|
|||
using BTCPayServer.Tests.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using Xunit;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
public class ProcessLauncher
|
||||
{
|
||||
string _CurrentDirectory;
|
||||
public string CurrentDirectory
|
||||
{
|
||||
get
|
||||
{
|
||||
return _CurrentDirectory;
|
||||
}
|
||||
}
|
||||
public ProcessLauncher()
|
||||
{
|
||||
_CurrentDirectory = Directory.GetCurrentDirectory();
|
||||
}
|
||||
public bool GoTo(string[] directories, bool createIfNotExists = false)
|
||||
{
|
||||
var original = _CurrentDirectory;
|
||||
foreach(var dir in directories)
|
||||
{
|
||||
var newDirectory = Path.Combine(_CurrentDirectory, dir);
|
||||
if(!Directory.Exists(newDirectory))
|
||||
{
|
||||
if(createIfNotExists)
|
||||
Directory.CreateDirectory(newDirectory);
|
||||
else
|
||||
{
|
||||
_CurrentDirectory = original;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
_CurrentDirectory = newDirectory;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
Stack<string> _Directories = new Stack<string>();
|
||||
public void PushDirectory()
|
||||
{
|
||||
_Directories.Push(_CurrentDirectory);
|
||||
}
|
||||
|
||||
public void PopDirectory()
|
||||
{
|
||||
_CurrentDirectory = _Directories.Pop();
|
||||
}
|
||||
|
||||
public bool GoTo(string directory, bool createIfNotExists = false)
|
||||
{
|
||||
return GoTo(new[] { directory }, createIfNotExists);
|
||||
}
|
||||
|
||||
public void Run(string processName, string args)
|
||||
{
|
||||
Start(processName, args).WaitForExit();
|
||||
}
|
||||
|
||||
public Process Start(string processName, string args)
|
||||
{
|
||||
Logs.Tester.LogInformation($"Running [{processName} {args}] from {_CurrentDirectory}");
|
||||
StringBuilder builder = new StringBuilder();
|
||||
var process = new Process()
|
||||
{
|
||||
StartInfo = new ProcessStartInfo
|
||||
{
|
||||
WorkingDirectory = _CurrentDirectory,
|
||||
FileName = processName,
|
||||
Arguments = args,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true
|
||||
}
|
||||
};
|
||||
try
|
||||
{
|
||||
process.OutputDataReceived += (s, e) =>
|
||||
{
|
||||
Logs.Tester.LogInformation(e.Data);
|
||||
};
|
||||
process.ErrorDataReceived += (s, e) =>
|
||||
{
|
||||
Logs.Tester.LogInformation(e.Data);
|
||||
};
|
||||
process.Start();
|
||||
process.BeginErrorReadLine();
|
||||
process.BeginOutputReadLine();
|
||||
}
|
||||
catch(Exception ex) { throw new Exception($"You need to install {processName} for this test (info : {ex.Message})"); }
|
||||
return process;
|
||||
}
|
||||
|
||||
public void AssertExists(string file)
|
||||
{
|
||||
var path = Path.Combine(_CurrentDirectory, file);
|
||||
Assert.True(File.Exists(path), $"The file {path} should exist");
|
||||
}
|
||||
|
||||
public bool Exists(string file)
|
||||
{
|
||||
var path = Path.Combine(_CurrentDirectory, file);
|
||||
return File.Exists(path);
|
||||
}
|
||||
}
|
||||
}
|
89
BTCPayServer.Tests/ServerTester.cs
Normal file
89
BTCPayServer.Tests/ServerTester.cs
Normal file
|
@ -0,0 +1,89 @@
|
|||
using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Models.AccountViewModels;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NBitcoin;
|
||||
using NBitcoin.Tests;
|
||||
using NBitpayClient;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
public class ServerTester : IDisposable
|
||||
{
|
||||
public static ServerTester Create([CallerMemberNameAttribute]string scope = null)
|
||||
{
|
||||
return new ServerTester(scope);
|
||||
}
|
||||
|
||||
string _Directory;
|
||||
NodeBuilder _Builder;
|
||||
|
||||
public ServerTester(string scope)
|
||||
{
|
||||
_Directory = scope;
|
||||
}
|
||||
|
||||
public void Start()
|
||||
{
|
||||
if(Directory.Exists(_Directory))
|
||||
Utils.DeleteDirectory(_Directory);
|
||||
if(!Directory.Exists(_Directory))
|
||||
Directory.CreateDirectory(_Directory);
|
||||
_Builder = NodeBuilder.Create(_Directory, "0.14.2");
|
||||
ExplorerNode = _Builder.CreateNode(false);
|
||||
ExplorerNode.WhiteBind = true;
|
||||
ExplorerNode.Start();
|
||||
ExplorerNode.CreateRPCClient().Generate(101);
|
||||
ExplorerTester = NBXplorerTester.Create(Path.Combine(_Directory, "explorer"));
|
||||
ExplorerTester.Node = ExplorerNode;
|
||||
ExplorerTester.Start();
|
||||
|
||||
PayTester = new BTCPayServerTester(Path.Combine(_Directory, "pay"))
|
||||
{
|
||||
NBXplorerUri = ExplorerTester.ExplorerClient.Address,
|
||||
CookieFile = ExplorerTester.CookieFile
|
||||
};
|
||||
PayTester.Start();
|
||||
}
|
||||
|
||||
public TestAccount CreateAccount()
|
||||
{
|
||||
return new TestAccount(this);
|
||||
}
|
||||
|
||||
public CoreNode ExplorerNode
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public BTCPayServerTester PayTester
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public NBXplorerTester ExplorerTester
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public Network Network
|
||||
{
|
||||
get;
|
||||
set;
|
||||
} = Network.RegTest;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if(PayTester != null)
|
||||
PayTester.Dispose();
|
||||
if(ExplorerTester != null)
|
||||
ExplorerTester.Dispose();
|
||||
if(_Builder != null)
|
||||
_Builder.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
65
BTCPayServer.Tests/TestAccount.cs
Normal file
65
BTCPayServer.Tests/TestAccount.cs
Normal file
|
@ -0,0 +1,65 @@
|
|||
using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Invoicing;
|
||||
using BTCPayServer.Models.AccountViewModels;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NBitcoin;
|
||||
using NBitpayClient;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Xunit;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
public class TestAccount
|
||||
{
|
||||
ServerTester parent;
|
||||
public TestAccount(ServerTester parent)
|
||||
{
|
||||
this.parent = parent;
|
||||
BitPay = new Bitpay(new Key(), parent.PayTester.ServerUri);
|
||||
}
|
||||
|
||||
public void GrantAccess()
|
||||
{
|
||||
GrantAccessAsync().GetAwaiter().GetResult();
|
||||
}
|
||||
public async Task GrantAccessAsync()
|
||||
{
|
||||
var extKey = new ExtKey().GetWif(parent.Network);
|
||||
var pairingCode = BitPay.RequestClientAuthorization("test", Facade.Merchant);
|
||||
var account = parent.PayTester.GetController<AccountController>();
|
||||
await account.Register(new RegisterViewModel()
|
||||
{
|
||||
Email = "Bob@toto.com",
|
||||
ConfirmPassword = "Kitten0@",
|
||||
Password = "Kitten0@",
|
||||
});
|
||||
UserId = account.RegisteredUserId;
|
||||
StoreId = account.RegisteredStoreId;
|
||||
var manage = parent.PayTester.GetController<ManageController>(account.RegisteredUserId);
|
||||
await manage.Index(new Models.ManageViewModels.IndexViewModel()
|
||||
{
|
||||
ExtPubKey = extKey.Neuter().ToString(),
|
||||
SpeedPolicy = SpeedPolicy.MediumSpeed
|
||||
});
|
||||
Assert.IsType<ViewResult>(await manage.AskPairing(pairingCode.ToString()));
|
||||
await manage.Pairs(pairingCode.ToString());
|
||||
}
|
||||
|
||||
public Bitpay BitPay
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public string UserId
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public string StoreId
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
}
|
||||
}
|
255
BTCPayServer.Tests/UnitTest1.cs
Normal file
255
BTCPayServer.Tests/UnitTest1.cs
Normal file
|
@ -0,0 +1,255 @@
|
|||
using BTCPayServer.Tests.Logging;
|
||||
using System.Linq;
|
||||
using NBitcoin;
|
||||
using NBitcoin.DataEncoders;
|
||||
using NBitcoin.Payment;
|
||||
using NBitpayClient;
|
||||
using System;
|
||||
using System.Threading;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
using Xunit.Sdk;
|
||||
using BTCPayServer.Invoicing;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
public class UnitTest1
|
||||
{
|
||||
public UnitTest1(ITestOutputHelper helper)
|
||||
{
|
||||
Logs.Tester = new XUnitLog(helper) { Name = "Tests" };
|
||||
Logs.LogProvider = new XUnitLogProvider(helper);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanCalculateCryptoDue()
|
||||
{
|
||||
var entity = new InvoiceEntity();
|
||||
entity.TxFee = Money.Coins(0.1m);
|
||||
entity.Rate = 5000;
|
||||
entity.Payments = new System.Collections.Generic.List<PaymentEntity>();
|
||||
entity.ProductInformation = new ProductInformation() { Price = 5000 };
|
||||
|
||||
Assert.Equal(Money.Coins(1.1m), entity.GetCryptoDue());
|
||||
Assert.Equal(Money.Coins(1.1m), entity.GetTotalCryptoDue());
|
||||
|
||||
entity.Payments.Add(new PaymentEntity() { Output = new TxOut(Money.Coins(0.5m), new Key()) });
|
||||
|
||||
//Since we need to spend one more txout, it should be 1.1 - 0,5 + 0.1
|
||||
Assert.Equal(Money.Coins(0.7m), entity.GetCryptoDue());
|
||||
Assert.Equal(Money.Coins(1.2m), entity.GetTotalCryptoDue());
|
||||
|
||||
entity.Payments.Add(new PaymentEntity() { Output = new TxOut(Money.Coins(0.2m), new Key()) });
|
||||
Assert.Equal(Money.Coins(0.6m), entity.GetCryptoDue());
|
||||
Assert.Equal(Money.Coins(1.3m), entity.GetTotalCryptoDue());
|
||||
|
||||
entity.Payments.Add(new PaymentEntity() { Output = new TxOut(Money.Coins(0.6m), new Key()) });
|
||||
|
||||
Assert.Equal(Money.Zero, entity.GetCryptoDue());
|
||||
Assert.Equal(Money.Coins(1.3m), entity.GetTotalCryptoDue());
|
||||
|
||||
entity.Payments.Add(new PaymentEntity() { Output = new TxOut(Money.Coins(0.2m), new Key()) });
|
||||
|
||||
Assert.Equal(Money.Zero, entity.GetCryptoDue());
|
||||
Assert.Equal(Money.Coins(1.3m), entity.GetTotalCryptoDue());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanPayUsingBIP70()
|
||||
{
|
||||
using(var tester = ServerTester.Create())
|
||||
{
|
||||
tester.Start();
|
||||
var user = tester.CreateAccount();
|
||||
user.GrantAccess();
|
||||
var invoice = user.BitPay.CreateInvoice(new Invoice()
|
||||
{
|
||||
Price = 5000.0,
|
||||
Currency = "USD",
|
||||
PosData = "posData",
|
||||
OrderId = "orderId",
|
||||
//RedirectURL = redirect + "redirect",
|
||||
//NotificationURL = CallbackUri + "/notification",
|
||||
ItemDesc = "Some description",
|
||||
FullNotifications = true
|
||||
}, Facade.Merchant);
|
||||
|
||||
Assert.False(invoice.Refundable);
|
||||
|
||||
var url = new BitcoinUrlBuilder(invoice.PaymentUrls.BIP72);
|
||||
var request = url.GetPaymentRequest();
|
||||
var payment = request.CreatePayment();
|
||||
|
||||
Transaction tx = new Transaction();
|
||||
tx.Outputs.AddRange(request.Details.Outputs.Select(o => new TxOut(o.Amount, o.Script)));
|
||||
var cashCow = tester.ExplorerNode.CreateRPCClient();
|
||||
tx = cashCow.FundRawTransaction(tx).Transaction;
|
||||
tx = cashCow.SignRawTransaction(tx);
|
||||
|
||||
payment.Transactions.Add(tx);
|
||||
|
||||
payment.RefundTo.Add(new PaymentOutput(Money.Coins(1.0m), new Key().ScriptPubKey));
|
||||
var ack = payment.SubmitPayment();
|
||||
Assert.NotNull(ack);
|
||||
|
||||
Eventually(() =>
|
||||
{
|
||||
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
|
||||
Assert.Equal("paid", localInvoice.Status);
|
||||
Assert.True(localInvoice.Refundable);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InvoiceFlowThroughDifferentStatesCorrectly()
|
||||
{
|
||||
using(var tester = ServerTester.Create())
|
||||
{
|
||||
tester.Start();
|
||||
var user = tester.CreateAccount();
|
||||
Assert.False(user.BitPay.TestAccess(Facade.Merchant));
|
||||
user.GrantAccess();
|
||||
Assert.True(user.BitPay.TestAccess(Facade.Merchant));
|
||||
var invoice = user.BitPay.CreateInvoice(new Invoice()
|
||||
{
|
||||
Price = 5000.0,
|
||||
Currency = "USD",
|
||||
PosData = "posData",
|
||||
OrderId = "orderId",
|
||||
//RedirectURL = redirect + "redirect",
|
||||
//NotificationURL = CallbackUri + "/notification",
|
||||
ItemDesc = "Some description",
|
||||
FullNotifications = true
|
||||
}, Facade.Merchant);
|
||||
|
||||
var textSearchResult = tester.PayTester.Runtime.InvoiceRepository.GetInvoices(new Invoicing.InvoiceQuery()
|
||||
{
|
||||
StoreId = user.StoreId,
|
||||
TextSearch = invoice.OrderId
|
||||
}).GetAwaiter().GetResult();
|
||||
|
||||
Assert.Equal(1, textSearchResult.Length);
|
||||
|
||||
textSearchResult = tester.PayTester.Runtime.InvoiceRepository.GetInvoices(new Invoicing.InvoiceQuery()
|
||||
{
|
||||
StoreId = user.StoreId,
|
||||
TextSearch = invoice.Id
|
||||
}).GetAwaiter().GetResult();
|
||||
|
||||
Assert.Equal(1, textSearchResult.Length);
|
||||
|
||||
invoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
|
||||
Assert.Equal(Money.Coins(0), invoice.BtcPaid);
|
||||
Assert.Equal("new", invoice.Status);
|
||||
Assert.Equal("false", invoice.ExceptionStatus);
|
||||
|
||||
Assert.Equal(1, user.BitPay.GetInvoices(invoice.InvoiceTime.DateTime).Length);
|
||||
Assert.Equal(0, user.BitPay.GetInvoices(invoice.InvoiceTime.DateTime + TimeSpan.FromDays(1)).Length);
|
||||
Assert.Equal(1, user.BitPay.GetInvoices(invoice.InvoiceTime.DateTime - TimeSpan.FromDays(5)).Length);
|
||||
Assert.Equal(1, user.BitPay.GetInvoices(invoice.InvoiceTime.DateTime - TimeSpan.FromDays(5), invoice.InvoiceTime.DateTime).Length);
|
||||
Assert.Equal(0, user.BitPay.GetInvoices(invoice.InvoiceTime.DateTime - TimeSpan.FromDays(5), invoice.InvoiceTime.DateTime - TimeSpan.FromDays(1)).Length);
|
||||
|
||||
|
||||
var firstPayment = Money.Coins(0.04m);
|
||||
|
||||
var txFee = Money.Zero;
|
||||
|
||||
var rate = user.BitPay.GetRates();
|
||||
|
||||
var cashCow = tester.ExplorerNode.CreateRPCClient();
|
||||
var invoiceAddress = BitcoinAddress.Create(invoice.BitcoinAddress, cashCow.Network);
|
||||
cashCow.SendToAddress(invoiceAddress, firstPayment);
|
||||
|
||||
Money secondPayment = Money.Zero;
|
||||
|
||||
Eventually(() =>
|
||||
{
|
||||
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
|
||||
Assert.Equal("paidPartial", localInvoice.Status);
|
||||
Assert.Equal(firstPayment, localInvoice.BtcPaid);
|
||||
txFee = localInvoice.BtcDue - invoice.BtcDue;
|
||||
Assert.Equal("paidPartial", localInvoice.ExceptionStatus);
|
||||
secondPayment = localInvoice.BtcDue;
|
||||
});
|
||||
|
||||
cashCow.SendToAddress(invoiceAddress, secondPayment);
|
||||
|
||||
Eventually(() =>
|
||||
{
|
||||
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
|
||||
Assert.Equal("paid", localInvoice.Status);
|
||||
Assert.Equal(firstPayment + secondPayment, localInvoice.BtcPaid);
|
||||
Assert.Equal(Money.Zero, localInvoice.BtcDue);
|
||||
Assert.Equal("false", localInvoice.ExceptionStatus);
|
||||
});
|
||||
|
||||
cashCow.Generate(1); //The user has medium speed settings, so 1 conf is enough to be confirmed
|
||||
|
||||
Eventually(() =>
|
||||
{
|
||||
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
|
||||
Assert.Equal("confirmed", localInvoice.Status);
|
||||
});
|
||||
|
||||
cashCow.Generate(5); //Now should be complete
|
||||
|
||||
Eventually(() =>
|
||||
{
|
||||
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
|
||||
Assert.Equal("complete", localInvoice.Status);
|
||||
});
|
||||
|
||||
invoice = user.BitPay.CreateInvoice(new Invoice()
|
||||
{
|
||||
Price = 5000.0,
|
||||
Currency = "USD",
|
||||
PosData = "posData",
|
||||
OrderId = "orderId",
|
||||
//RedirectURL = redirect + "redirect",
|
||||
//NotificationURL = CallbackUri + "/notification",
|
||||
ItemDesc = "Some description",
|
||||
FullNotifications = true
|
||||
}, Facade.Merchant);
|
||||
invoiceAddress = BitcoinAddress.Create(invoice.BitcoinAddress, cashCow.Network);
|
||||
|
||||
cashCow.SendToAddress(invoiceAddress, invoice.BtcDue + Money.Coins(1));
|
||||
|
||||
Eventually(() =>
|
||||
{
|
||||
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
|
||||
Assert.Equal("paidOver", localInvoice.Status);
|
||||
Assert.Equal(Money.Zero, localInvoice.BtcDue);
|
||||
Assert.Equal("paidOver", localInvoice.ExceptionStatus);
|
||||
});
|
||||
|
||||
cashCow.Generate(1);
|
||||
|
||||
Eventually(() =>
|
||||
{
|
||||
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
|
||||
Assert.Equal("confirmed", localInvoice.Status);
|
||||
Assert.Equal(Money.Zero, localInvoice.BtcDue);
|
||||
Assert.Equal("paidOver", localInvoice.ExceptionStatus);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void Eventually(Action act)
|
||||
{
|
||||
CancellationTokenSource cts = new CancellationTokenSource(10000);
|
||||
while(true)
|
||||
{
|
||||
try
|
||||
{
|
||||
act();
|
||||
break;
|
||||
}
|
||||
catch(XunitException) when(!cts.Token.IsCancellationRequested)
|
||||
{
|
||||
cts.Token.WaitHandle.WaitOne(500);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
63
BTCPayServer.Tests/Utils.cs
Normal file
63
BTCPayServer.Tests/Utils.cs
Normal file
|
@ -0,0 +1,63 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
public class Utils
|
||||
{
|
||||
public static int FreeTcpPort()
|
||||
{
|
||||
TcpListener l = new TcpListener(IPAddress.Loopback, 0);
|
||||
l.Start();
|
||||
int port = ((IPEndPoint)l.LocalEndpoint).Port;
|
||||
l.Stop();
|
||||
return port;
|
||||
}
|
||||
|
||||
// http://stackoverflow.com/a/14933880/2061103
|
||||
public static void DeleteDirectory(string destinationDir)
|
||||
{
|
||||
const int magicDust = 10;
|
||||
for(var gnomes = 1; gnomes <= magicDust; gnomes++)
|
||||
{
|
||||
try
|
||||
{
|
||||
Directory.Delete(destinationDir, true);
|
||||
}
|
||||
catch(DirectoryNotFoundException)
|
||||
{
|
||||
return; // good!
|
||||
}
|
||||
catch(IOException)
|
||||
{
|
||||
if(gnomes == magicDust)
|
||||
throw;
|
||||
// System.IO.IOException: The directory is not empty
|
||||
System.Diagnostics.Debug.WriteLine("Gnomes prevent deletion of {0}! Applying magic dust, attempt #{1}.", destinationDir, gnomes);
|
||||
|
||||
// see http://stackoverflow.com/questions/329355/cannot-delete-directory-with-directory-deletepath-true for more magic
|
||||
Thread.Sleep(100);
|
||||
continue;
|
||||
}
|
||||
catch(UnauthorizedAccessException)
|
||||
{
|
||||
if(gnomes == magicDust)
|
||||
throw;
|
||||
// Wait, maybe another software make us authorized a little later
|
||||
System.Diagnostics.Debug.WriteLine("Gnomes prevent deletion of {0}! Applying magic dust, attempt #{1}.", destinationDir, gnomes);
|
||||
|
||||
// see http://stackoverflow.com/questions/329355/cannot-delete-directory-with-directory-deletepath-true for more magic
|
||||
Thread.Sleep(100);
|
||||
continue;
|
||||
}
|
||||
return;
|
||||
}
|
||||
// depending on your use case, consider throwing an exception here
|
||||
}
|
||||
}
|
||||
}
|
35
BTCPayServer/Authentication/BitIdentity.cs
Normal file
35
BTCPayServer/Authentication/BitIdentity.cs
Normal file
|
@ -0,0 +1,35 @@
|
|||
using NBitcoin;
|
||||
using NBitcoin.DataEncoders;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Security.Principal;
|
||||
using System.Text;
|
||||
|
||||
namespace BTCPayServer.Authentication
|
||||
{
|
||||
public class BitIdentity : IIdentity
|
||||
{
|
||||
public BitIdentity(PubKey key)
|
||||
{
|
||||
PubKey = key;
|
||||
_Name = Encoders.Base58Check.EncodeData(Encoders.Hex.DecodeData("0f02" + key.Hash.ToString()));
|
||||
SIN = NBitpayClient.Extensions.BitIdExtensions.GetBitIDSIN(key);
|
||||
}
|
||||
string _Name;
|
||||
|
||||
public string SIN
|
||||
{
|
||||
get;
|
||||
}
|
||||
public PubKey PubKey
|
||||
{
|
||||
get;
|
||||
}
|
||||
|
||||
public string AuthenticationType => "BitID";
|
||||
|
||||
public bool IsAuthenticated => true;
|
||||
|
||||
public string Name => _Name;
|
||||
}
|
||||
}
|
44
BTCPayServer/Authentication/BitToken.cs
Normal file
44
BTCPayServer/Authentication/BitToken.cs
Normal file
|
@ -0,0 +1,44 @@
|
|||
using NBitcoin;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace BTCPayServer.Authentication
|
||||
{
|
||||
public class BitTokenEntity
|
||||
{
|
||||
public string Name
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public string Value
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public DateTimeOffset DateCreated
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public bool Active
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public string PairedId
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public string Label
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public DateTimeOffset PairingTime
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public string SIN
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
}
|
||||
}
|
50
BTCPayServer/Authentication/PairingCodeEntity.cs
Normal file
50
BTCPayServer/Authentication/PairingCodeEntity.cs
Normal file
|
@ -0,0 +1,50 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace BTCPayServer.Authentication
|
||||
{
|
||||
public class PairingCodeEntity
|
||||
{
|
||||
public string Id
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public string Facade
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public string Label
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public string SIN
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public DateTimeOffset PairingTime
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public DateTimeOffset PairingExpiration
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public string Token
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public bool IsExpired()
|
||||
{
|
||||
return DateTimeOffset.UtcNow > PairingExpiration;
|
||||
}
|
||||
}
|
||||
}
|
179
BTCPayServer/Authentication/TokenRepository.cs
Normal file
179
BTCPayServer/Authentication/TokenRepository.cs
Normal file
|
@ -0,0 +1,179 @@
|
|||
using DBreeze;
|
||||
using NBitcoin;
|
||||
using NBitcoin.DataEncoders;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Authentication
|
||||
{
|
||||
public class TokenRepository
|
||||
{
|
||||
public TokenRepository(DBreezeEngine engine)
|
||||
{
|
||||
_Engine = engine;
|
||||
}
|
||||
|
||||
|
||||
private readonly DBreezeEngine _Engine;
|
||||
public DBreezeEngine Engine
|
||||
{
|
||||
get
|
||||
{
|
||||
return _Engine;
|
||||
}
|
||||
}
|
||||
|
||||
public Task<BitTokenEntity[]> GetTokens(string sin)
|
||||
{
|
||||
List<BitTokenEntity> tokens = new List<BitTokenEntity>();
|
||||
using(var tx = _Engine.GetTransaction())
|
||||
{
|
||||
tx.ValuesLazyLoadingIsOn = false;
|
||||
foreach(var row in tx.SelectForward<string, byte[]>($"T_{sin}"))
|
||||
{
|
||||
var token = ToObject<BitTokenEntity>(row.Value);
|
||||
tokens.Add(token);
|
||||
}
|
||||
}
|
||||
return Task.FromResult(tokens.ToArray());
|
||||
}
|
||||
|
||||
public Task<BitTokenEntity> CreateToken(string sin, string tokenName)
|
||||
{
|
||||
var token = new BitTokenEntity
|
||||
{
|
||||
Name = tokenName,
|
||||
Value = Encoders.Base58.EncodeData(RandomUtils.GetBytes(32)),
|
||||
DateCreated = DateTimeOffset.UtcNow
|
||||
};
|
||||
using(var tx = _Engine.GetTransaction())
|
||||
{
|
||||
tx.Insert<string, byte[]>($"T_{sin}", token.Name, ToBytes(token));
|
||||
tx.Commit();
|
||||
}
|
||||
return Task.FromResult(token);
|
||||
}
|
||||
|
||||
public Task<bool> PairWithAsync(string pairingCode, string pairedId)
|
||||
{
|
||||
if(pairedId == null)
|
||||
throw new ArgumentNullException(nameof(pairedId));
|
||||
using(var tx = _Engine.GetTransaction())
|
||||
{
|
||||
var row = tx.Select<string, byte[]>("PairingCodes", pairingCode);
|
||||
if(row == null || !row.Exists)
|
||||
return Task.FromResult(false);
|
||||
tx.RemoveKey<string>("PairingCodes", pairingCode);
|
||||
try
|
||||
{
|
||||
var pairingEntity = ToObject<PairingCodeEntity>(row.Value);
|
||||
if(pairingEntity.IsExpired())
|
||||
return Task.FromResult(false);
|
||||
row = tx.Select<string, byte[]>($"T_{pairingEntity.SIN}", pairingEntity.Facade);
|
||||
if(row == null || !row.Exists)
|
||||
return Task.FromResult(false);
|
||||
var token = ToObject<BitTokenEntity>(row.Value);
|
||||
if(token.Active)
|
||||
return Task.FromResult(false);
|
||||
token.Active = true;
|
||||
token.PairedId = pairedId;
|
||||
token.SIN = pairingEntity.SIN;
|
||||
token.Label = pairingEntity.Label;
|
||||
token.PairingTime = DateTimeOffset.UtcNow;
|
||||
tx.Insert($"TbP_{pairedId}", token.Value, ToBytes(token));
|
||||
tx.Insert($"T_{pairingEntity.SIN}", pairingEntity.Facade, ToBytes(token));
|
||||
}
|
||||
finally
|
||||
{
|
||||
tx.Commit();
|
||||
}
|
||||
}
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
|
||||
public Task<BitTokenEntity[]> GetTokensByPairedIdAsync(string pairedId)
|
||||
{
|
||||
List<BitTokenEntity> tokens = new List<BitTokenEntity>();
|
||||
using(var tx = _Engine.GetTransaction())
|
||||
{
|
||||
tx.ValuesLazyLoadingIsOn = false;
|
||||
foreach(var row in tx.SelectForward<string, byte[]>($"TbP_{pairedId}"))
|
||||
{
|
||||
tokens.Add(ToObject<BitTokenEntity>(row.Value));
|
||||
}
|
||||
}
|
||||
return Task.FromResult(tokens.ToArray());
|
||||
}
|
||||
|
||||
public Task<PairingCodeEntity> GetPairingAsync(string pairingCode)
|
||||
{
|
||||
using(var tx = _Engine.GetTransaction())
|
||||
{
|
||||
var row = tx.Select<string, byte[]>("PairingCodes", pairingCode);
|
||||
if(row == null || !row.Exists)
|
||||
return Task.FromResult<PairingCodeEntity>(null);
|
||||
var pairingEntity = ToObject<PairingCodeEntity>(row.Value);
|
||||
if(pairingEntity.IsExpired())
|
||||
return Task.FromResult<PairingCodeEntity>(null);
|
||||
return Task.FromResult(pairingEntity);
|
||||
}
|
||||
}
|
||||
public Task<PairingCodeEntity> AddPairingCodeAsync(PairingCodeEntity pairingCodeEntity)
|
||||
{
|
||||
pairingCodeEntity = Clone(pairingCodeEntity);
|
||||
pairingCodeEntity.Id = Encoders.Base58.EncodeData(RandomUtils.GetBytes(6));
|
||||
using(var tx = _Engine.GetTransaction())
|
||||
{
|
||||
tx.Insert("PairingCodes", pairingCodeEntity.Id, ToBytes(pairingCodeEntity));
|
||||
tx.Commit();
|
||||
}
|
||||
return Task.FromResult(pairingCodeEntity);
|
||||
}
|
||||
|
||||
private byte[] ToBytes<T>(T obj)
|
||||
{
|
||||
return ZipUtils.Zip(JsonConvert.SerializeObject(obj));
|
||||
}
|
||||
private T ToObject<T>(byte[] value)
|
||||
{
|
||||
return JsonConvert.DeserializeObject<T>(ZipUtils.Unzip(value));
|
||||
}
|
||||
|
||||
private T Clone<T>(T obj)
|
||||
{
|
||||
return ToObject<T>(ToBytes(obj));
|
||||
}
|
||||
|
||||
|
||||
public async Task DeleteToken(string sin, string tokenName)
|
||||
{
|
||||
var token = await GetToken(sin, tokenName);
|
||||
using(var tx = _Engine.GetTransaction())
|
||||
{
|
||||
tx.RemoveKey<string>($"T_{sin}", tokenName);
|
||||
if(token.PairedId != null)
|
||||
tx.RemoveKey<string>($"TbP_" + token.PairedId, token.Value);
|
||||
tx.Commit();
|
||||
}
|
||||
}
|
||||
|
||||
public Task<BitTokenEntity> GetToken(string sin, string tokenName)
|
||||
{
|
||||
using(var tx = _Engine.GetTransaction())
|
||||
{
|
||||
tx.ValuesLazyLoadingIsOn = true;
|
||||
var row = tx.Select<string, byte[]>($"T_{sin}", tokenName);
|
||||
if(row == null || !row.Exists)
|
||||
return Task.FromResult<BitTokenEntity>(null);
|
||||
var token = ToObject<BitTokenEntity>(row.Value);
|
||||
if(!token.Active)
|
||||
return Task.FromResult<BitTokenEntity>(null);
|
||||
return Task.FromResult(token);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
89
BTCPayServer/BTCPayServer.csproj
Normal file
89
BTCPayServer/BTCPayServer.csproj
Normal file
|
@ -0,0 +1,89 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>netcoreapp2.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<None Remove="Currencies.txt" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Currencies.txt" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Filter" Version="1.1.2" />
|
||||
<PackageReference Include="NBitcoin" Version="4.0.0.34" />
|
||||
<PackageReference Include="NBitpayClient" Version="1.0.0.6" />
|
||||
<PackageReference Include="DBreeze" Version="1.87.0" />
|
||||
<PackageReference Include="NBXplorer.Client" Version="1.0.0.7" />
|
||||
<PackageReference Include="System.ValueTuple" Version="4.4.0" />
|
||||
<PackageReference Include="System.Xml.XmlSerializer" Version="4.0.11" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="2.0.0" PrivateAssets="All" />
|
||||
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="2.0.0" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<DotNetCliToolReference Include="Microsoft.EntityFrameworkCore.Tools.DotNet" Version="2.0.0" />
|
||||
<DotNetCliToolReference Include="Microsoft.Extensions.SecretManager.Tools" Version="2.0.0" />
|
||||
<DotNetCliToolReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Tools" Version="2.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Migrations\" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="wwwroot\img\bitcoin-symbol.svg" />
|
||||
<None Include="wwwroot\js\core.js" />
|
||||
<None Include="wwwroot\js\creative.js" />
|
||||
<None Include="wwwroot\js\creative.min.js" />
|
||||
<None Include="wwwroot\js\site.js" />
|
||||
<None Include="wwwroot\js\site.min.js" />
|
||||
<None Include="wwwroot\vendor\bootstrap\js\bootstrap.js" />
|
||||
<None Include="wwwroot\vendor\bootstrap\js\bootstrap.min.js" />
|
||||
<None Include="wwwroot\vendor\font-awesome\fonts\fontawesome-webfont.svg" />
|
||||
<None Include="wwwroot\vendor\font-awesome\fonts\fontawesome-webfont.woff2" />
|
||||
<None Include="wwwroot\vendor\font-awesome\less\animated.less" />
|
||||
<None Include="wwwroot\vendor\font-awesome\less\bordered-pulled.less" />
|
||||
<None Include="wwwroot\vendor\font-awesome\less\core.less" />
|
||||
<None Include="wwwroot\vendor\font-awesome\less\fixed-width.less" />
|
||||
<None Include="wwwroot\vendor\font-awesome\less\font-awesome.less" />
|
||||
<None Include="wwwroot\vendor\font-awesome\less\icons.less" />
|
||||
<None Include="wwwroot\vendor\font-awesome\less\larger.less" />
|
||||
<None Include="wwwroot\vendor\font-awesome\less\list.less" />
|
||||
<None Include="wwwroot\vendor\font-awesome\less\mixins.less" />
|
||||
<None Include="wwwroot\vendor\font-awesome\less\path.less" />
|
||||
<None Include="wwwroot\vendor\font-awesome\less\rotated-flipped.less" />
|
||||
<None Include="wwwroot\vendor\font-awesome\less\screen-reader.less" />
|
||||
<None Include="wwwroot\vendor\font-awesome\less\stacked.less" />
|
||||
<None Include="wwwroot\vendor\font-awesome\less\variables.less" />
|
||||
<None Include="wwwroot\vendor\font-awesome\scss\font-awesome.scss" />
|
||||
<None Include="wwwroot\vendor\font-awesome\scss\_animated.scss" />
|
||||
<None Include="wwwroot\vendor\font-awesome\scss\_bordered-pulled.scss" />
|
||||
<None Include="wwwroot\vendor\font-awesome\scss\_core.scss" />
|
||||
<None Include="wwwroot\vendor\font-awesome\scss\_fixed-width.scss" />
|
||||
<None Include="wwwroot\vendor\font-awesome\scss\_icons.scss" />
|
||||
<None Include="wwwroot\vendor\font-awesome\scss\_larger.scss" />
|
||||
<None Include="wwwroot\vendor\font-awesome\scss\_list.scss" />
|
||||
<None Include="wwwroot\vendor\font-awesome\scss\_mixins.scss" />
|
||||
<None Include="wwwroot\vendor\font-awesome\scss\_path.scss" />
|
||||
<None Include="wwwroot\vendor\font-awesome\scss\_rotated-flipped.scss" />
|
||||
<None Include="wwwroot\vendor\font-awesome\scss\_screen-reader.scss" />
|
||||
<None Include="wwwroot\vendor\font-awesome\scss\_stacked.scss" />
|
||||
<None Include="wwwroot\vendor\font-awesome\scss\_variables.scss" />
|
||||
<None Include="wwwroot\vendor\jquery-easing\jquery.easing.compatibility.js" />
|
||||
<None Include="wwwroot\vendor\jquery-easing\jquery.easing.js" />
|
||||
<None Include="wwwroot\vendor\jquery-easing\jquery.easing.min.js" />
|
||||
<None Include="wwwroot\vendor\jquery\jquery.js" />
|
||||
<None Include="wwwroot\vendor\jquery\jquery.min.js" />
|
||||
<None Include="wwwroot\vendor\magnific-popup\jquery.magnific-popup.js" />
|
||||
<None Include="wwwroot\vendor\magnific-popup\jquery.magnific-popup.min.js" />
|
||||
<None Include="wwwroot\vendor\popper\popper.js" />
|
||||
<None Include="wwwroot\vendor\popper\popper.min.js" />
|
||||
<None Include="wwwroot\vendor\scrollreveal\scrollreveal.js" />
|
||||
<None Include="wwwroot\vendor\scrollreveal\scrollreveal.min.js" />
|
||||
</ItemGroup>
|
||||
</Project>
|
18
BTCPayServer/BitpayHttpException.cs
Normal file
18
BTCPayServer/BitpayHttpException.cs
Normal file
|
@ -0,0 +1,18 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
public class BitpayHttpException : Exception
|
||||
{
|
||||
public BitpayHttpException(int code, string message) : base(message)
|
||||
{
|
||||
StatusCode = code;
|
||||
}
|
||||
public int StatusCode
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
}
|
||||
}
|
205
BTCPayServer/Configuration/BTCPayServerOptions.cs
Normal file
205
BTCPayServer/Configuration/BTCPayServerOptions.cs
Normal file
|
@ -0,0 +1,205 @@
|
|||
using BTCPayServer.Logging;
|
||||
using System.Linq;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NBitcoin;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
|
||||
namespace BTCPayServer.Configuration
|
||||
{
|
||||
public class BTCPayServerOptions
|
||||
{
|
||||
public Network Network
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public Uri Explorer
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public string CookieFile
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public string ConfigurationFile
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
public string DataDir
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
public List<IPEndPoint> Listen
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public void LoadArgs(TextFileConfiguration consoleConfig)
|
||||
{
|
||||
ConfigurationFile = consoleConfig.GetOrDefault<string>("conf", null);
|
||||
DataDir = consoleConfig.GetOrDefault<string>("datadir", null);
|
||||
if(DataDir != null && ConfigurationFile != null)
|
||||
{
|
||||
var isRelativePath = Path.GetFullPath(ConfigurationFile).Length > ConfigurationFile.Length;
|
||||
if(isRelativePath)
|
||||
{
|
||||
ConfigurationFile = Path.Combine(DataDir, ConfigurationFile);
|
||||
}
|
||||
}
|
||||
|
||||
Network = consoleConfig.GetOrDefault<bool>("testnet", false) ? Network.TestNet :
|
||||
consoleConfig.GetOrDefault<bool>("regtest", false) ? Network.RegTest :
|
||||
null;
|
||||
|
||||
if(DataDir != null && ConfigurationFile == null)
|
||||
{
|
||||
ConfigurationFile = GetDefaultConfigurationFile(Network != null);
|
||||
}
|
||||
|
||||
if(ConfigurationFile != null)
|
||||
{
|
||||
AssetConfigFileExists();
|
||||
var configTemp = TextFileConfiguration.Parse(File.ReadAllText(ConfigurationFile));
|
||||
Network = Network ?? (configTemp.GetOrDefault<bool>("testnet", false) ? Network.TestNet :
|
||||
configTemp.GetOrDefault<bool>("regtest", false) ? Network.RegTest :
|
||||
null);
|
||||
}
|
||||
|
||||
Network = Network ?? Network.Main;
|
||||
if(DataDir == null)
|
||||
{
|
||||
DataDir = DefaultDataDirectory.GetDefaultDirectory("BTCPayServer", Network, true);
|
||||
ConfigurationFile = GetDefaultConfigurationFile(true);
|
||||
}
|
||||
|
||||
if(!Directory.Exists(DataDir))
|
||||
throw new ConfigurationException("Data directory does not exists");
|
||||
|
||||
var config = TextFileConfiguration.Parse(File.ReadAllText(ConfigurationFile));
|
||||
consoleConfig.MergeInto(config, true);
|
||||
|
||||
Logs.Configuration.LogInformation("Network: " + Network);
|
||||
Logs.Configuration.LogInformation("Data directory set to " + DataDir);
|
||||
Logs.Configuration.LogInformation("Configuration file set to " + ConfigurationFile);
|
||||
|
||||
var defaultPort = config.GetOrDefault<int>("port", GetDefaultPort(Network));
|
||||
Listen = config
|
||||
.GetAll("bind")
|
||||
.Select(p => ConvertToEndpoint(p, defaultPort))
|
||||
.ToList();
|
||||
if(Listen.Count == 0)
|
||||
{
|
||||
Listen.Add(new IPEndPoint(IPAddress.Parse("127.0.0.1"), defaultPort));
|
||||
}
|
||||
|
||||
Explorer = config.GetOrDefault<Uri>("explorer.url", GetDefaultNXplorerUri());
|
||||
CookieFile = config.GetOrDefault<string>("explorer.cookiefile", GetExplorerDefaultCookiePath());
|
||||
ExternalUrl = config.GetOrDefault<Uri>("externalurl", null);
|
||||
if(ExternalUrl == null)
|
||||
{
|
||||
var ip = Listen.Where(u => !u.Address.ToString().Equals("0.0.0.0", StringComparison.OrdinalIgnoreCase)).FirstOrDefault()
|
||||
?? new IPEndPoint(IPAddress.Parse("127.0.0.1"), defaultPort);
|
||||
ExternalUrl = new Uri($"http://{ip.Address}:{ip.Port}/");
|
||||
}
|
||||
}
|
||||
|
||||
public Uri ExternalUrl
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
private Uri GetDefaultNXplorerUri()
|
||||
{
|
||||
return new Uri("http://localhost:" + GetNXplorerDefaultPort(Network));
|
||||
}
|
||||
|
||||
|
||||
public string[] GetUrls()
|
||||
{
|
||||
return Listen.Select(b => "http://" + b + "/").ToArray();
|
||||
}
|
||||
|
||||
private void AssetConfigFileExists()
|
||||
{
|
||||
if(!File.Exists(ConfigurationFile))
|
||||
throw new ConfigurationException("Configuration file does not exists");
|
||||
}
|
||||
|
||||
public static IPEndPoint ConvertToEndpoint(string str, int defaultPort)
|
||||
{
|
||||
var portOut = defaultPort;
|
||||
var hostOut = "";
|
||||
int colon = str.LastIndexOf(':');
|
||||
// if a : is found, and it either follows a [...], or no other : is in the string, treat it as port separator
|
||||
bool fHaveColon = colon != -1;
|
||||
bool fBracketed = fHaveColon && (str[0] == '[' && str[colon - 1] == ']'); // if there is a colon, and in[0]=='[', colon is not 0, so in[colon-1] is safe
|
||||
bool fMultiColon = fHaveColon && (str.LastIndexOf(':', colon - 1) != -1);
|
||||
if(fHaveColon && (colon == 0 || fBracketed || !fMultiColon))
|
||||
{
|
||||
int n;
|
||||
if(int.TryParse(str.Substring(colon + 1), out n) && n > 0 && n < 0x10000)
|
||||
{
|
||||
str = str.Substring(0, colon);
|
||||
portOut = n;
|
||||
}
|
||||
}
|
||||
if(str.Length > 0 && str[0] == '[' && str[str.Length - 1] == ']')
|
||||
hostOut = str.Substring(1, str.Length - 2);
|
||||
else
|
||||
hostOut = str;
|
||||
return new IPEndPoint(IPAddress.Parse(hostOut), portOut);
|
||||
}
|
||||
|
||||
const string DefaultConfigFile = "settings.config";
|
||||
private string GetDefaultConfigurationFile(bool createIfNotExist)
|
||||
{
|
||||
var config = Path.Combine(DataDir, DefaultConfigFile);
|
||||
Logs.Configuration.LogInformation("Configuration file set to " + config);
|
||||
if(createIfNotExist && !File.Exists(config))
|
||||
{
|
||||
Logs.Configuration.LogInformation("Creating configuration file");
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.AppendLine("### Global settings ###");
|
||||
builder.AppendLine("#testnet=0");
|
||||
builder.AppendLine("#regtest=0");
|
||||
builder.AppendLine("#Put here the xpub key of your hardware wallet");
|
||||
builder.AppendLine("#hdpubkey=xpub...");
|
||||
builder.AppendLine();
|
||||
builder.AppendLine("### Server settings ###");
|
||||
builder.AppendLine("#port=" + GetDefaultPort(Network));
|
||||
builder.AppendLine("#bind=127.0.0.1");
|
||||
builder.AppendLine("#externalurl=http://127.0.0.1/");
|
||||
builder.AppendLine();
|
||||
builder.AppendLine("### NBXplorer settings ###");
|
||||
builder.AppendLine("#explorer.url=" + GetDefaultNXplorerUri());
|
||||
builder.AppendLine("#explorer.cookiefile=" + GetExplorerDefaultCookiePath());
|
||||
File.WriteAllText(config, builder.ToString());
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
private string GetExplorerDefaultCookiePath()
|
||||
{
|
||||
return Path.Combine(DefaultDataDirectory.GetDefaultDirectory("NBXplorer", Network, false), ".cookie");
|
||||
}
|
||||
|
||||
private int GetNXplorerDefaultPort(Network network)
|
||||
{
|
||||
return network == Network.Main ? 24444 :
|
||||
network == Network.TestNet ? 24445 : 24446;
|
||||
}
|
||||
|
||||
private int GetDefaultPort(Network network)
|
||||
{
|
||||
return network == Network.Main ? 23000 :
|
||||
network == Network.TestNet ? 23001 : 23002;
|
||||
}
|
||||
}
|
||||
}
|
114
BTCPayServer/Configuration/BTCPayServerRuntime.cs
Normal file
114
BTCPayServer/Configuration/BTCPayServerRuntime.cs
Normal file
|
@ -0,0 +1,114 @@
|
|||
using BTCPayServer.Authentication;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using BTCPayServer.Invoicing;
|
||||
using BTCPayServer.Logging;
|
||||
using BTCPayServer.Wallet;
|
||||
using DBreeze;
|
||||
using NBitcoin;
|
||||
using NBXplorer;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using BTCPayServer.Data;
|
||||
|
||||
namespace BTCPayServer.Configuration
|
||||
{
|
||||
public class BTCPayServerRuntime : IDisposable
|
||||
{
|
||||
public ExplorerClient Explorer
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public void Configure(BTCPayServerOptions opts)
|
||||
{
|
||||
ConfigureAsync(opts).GetAwaiter().GetResult();
|
||||
}
|
||||
public async Task ConfigureAsync(BTCPayServerOptions opts)
|
||||
{
|
||||
Network = opts.Network;
|
||||
Explorer = new ExplorerClient(opts.Network, opts.Explorer);
|
||||
Explorer.SetCookieFile(opts.CookieFile);
|
||||
CancellationTokenSource cts = new CancellationTokenSource(5000);
|
||||
try
|
||||
{
|
||||
Logs.Configuration.LogInformation("Trying to connect to explorer " + Explorer.Address.AbsoluteUri);
|
||||
await Explorer.WaitServerStartedAsync(cts.Token).ConfigureAwait(false);
|
||||
Logs.Configuration.LogInformation("Connection successfull");
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
throw new ConfigurationException($"Could not connect to NBXplorer, {ex.Message}");
|
||||
}
|
||||
DBreezeEngine db = new DBreezeEngine(CreateDBPath(opts, "TokensDB"));
|
||||
_Resources.Add(db);
|
||||
TokenRepository = new TokenRepository(db);
|
||||
|
||||
db = new DBreezeEngine(CreateDBPath(opts, "InvoiceDB"));
|
||||
_Resources.Add(db);
|
||||
|
||||
var dbContext = new ApplicationDbContextFactory(Path.Combine(opts.DataDir, "sqllite.db"));
|
||||
DBFactory = dbContext;
|
||||
InvoiceRepository = new InvoiceRepository(dbContext, db, Network);
|
||||
|
||||
|
||||
db = new DBreezeEngine(CreateDBPath(opts, "AddressMapping"));
|
||||
_Resources.Add(db);
|
||||
Wallet = new BTCPayWallet(Explorer, db);
|
||||
}
|
||||
|
||||
private static string CreateDBPath(BTCPayServerOptions opts, string name)
|
||||
{
|
||||
var dbpath = Path.Combine(opts.DataDir, name);
|
||||
if(!Directory.Exists(dbpath))
|
||||
Directory.CreateDirectory(dbpath);
|
||||
return dbpath;
|
||||
}
|
||||
|
||||
List<IDisposable> _Resources = new List<IDisposable>();
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
lock(_Resources)
|
||||
{
|
||||
foreach(var r in _Resources)
|
||||
{
|
||||
r.Dispose();
|
||||
}
|
||||
_Resources.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
public Network Network
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
public TokenRepository TokenRepository
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public InvoiceRepository InvoiceRepository
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public BTCPayWallet Wallet
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public ApplicationDbContextFactory DBFactory
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
52
BTCPayServer/Configuration/DefaultDataDirectory.cs
Normal file
52
BTCPayServer/Configuration/DefaultDataDirectory.cs
Normal file
|
@ -0,0 +1,52 @@
|
|||
using BTCPayServer.Logging;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NBitcoin;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
|
||||
namespace BTCPayServer.Configuration
|
||||
{
|
||||
public class DefaultDataDirectory
|
||||
{
|
||||
public static string GetDefaultDirectory(string appName, Network network, bool createDirectory)
|
||||
{
|
||||
string directory = null;
|
||||
var home = Environment.GetEnvironmentVariable("HOME");
|
||||
if(!string.IsNullOrEmpty(home))
|
||||
{
|
||||
if(createDirectory)
|
||||
Logs.Configuration.LogInformation("Using HOME environment variable for initializing application data");
|
||||
directory = home;
|
||||
directory = Path.Combine(directory, "." + appName.ToLowerInvariant());
|
||||
}
|
||||
else
|
||||
{
|
||||
var localAppData = Environment.GetEnvironmentVariable("APPDATA");
|
||||
if(!string.IsNullOrEmpty(localAppData))
|
||||
{
|
||||
if(createDirectory)
|
||||
Logs.Configuration.LogInformation("Using APPDATA environment variable for initializing application data");
|
||||
directory = localAppData;
|
||||
directory = Path.Combine(directory, appName);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new DirectoryNotFoundException("Could not find suitable datadir");
|
||||
}
|
||||
}
|
||||
if(!Directory.Exists(directory) && createDirectory)
|
||||
{
|
||||
Directory.CreateDirectory(directory);
|
||||
}
|
||||
directory = Path.Combine(directory, network.Name);
|
||||
if(!Directory.Exists(directory) && createDirectory)
|
||||
{
|
||||
Logs.Configuration.LogInformation("Creating data directory");
|
||||
Directory.CreateDirectory(directory);
|
||||
}
|
||||
return directory;
|
||||
}
|
||||
}
|
||||
}
|
221
BTCPayServer/Configuration/TextFileConfiguration.cs
Normal file
221
BTCPayServer/Configuration/TextFileConfiguration.cs
Normal file
|
@ -0,0 +1,221 @@
|
|||
using NBitcoin;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
public class ConfigurationException : Exception
|
||||
{
|
||||
public ConfigurationException(string message) : base(message)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public class TextFileConfiguration
|
||||
{
|
||||
private Dictionary<string, List<string>> _Args;
|
||||
|
||||
public TextFileConfiguration(string[] args)
|
||||
{
|
||||
_Args = new Dictionary<string, List<string>>();
|
||||
string noValueParam = null;
|
||||
Action flushNoValueParam = () =>
|
||||
{
|
||||
if(noValueParam != null)
|
||||
{
|
||||
Add(noValueParam, "1", false);
|
||||
noValueParam = null;
|
||||
}
|
||||
};
|
||||
|
||||
foreach(var arg in args)
|
||||
{
|
||||
bool isParamName = arg.StartsWith("-", StringComparison.Ordinal);
|
||||
if(isParamName)
|
||||
{
|
||||
var splitted = arg.Split('=');
|
||||
if(splitted.Length > 1)
|
||||
{
|
||||
var value = String.Join("=", splitted.Skip(1).ToArray());
|
||||
flushNoValueParam();
|
||||
Add(splitted[0], value, false);
|
||||
}
|
||||
else
|
||||
{
|
||||
flushNoValueParam();
|
||||
noValueParam = splitted[0];
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if(noValueParam != null)
|
||||
{
|
||||
Add(noValueParam, arg, false);
|
||||
noValueParam = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
flushNoValueParam();
|
||||
}
|
||||
|
||||
private void Add(string key, string value, bool sourcePriority)
|
||||
{
|
||||
key = NormalizeKey(key);
|
||||
List<string> list;
|
||||
if(!_Args.TryGetValue(key, out list))
|
||||
{
|
||||
list = new List<string>();
|
||||
_Args.Add(key, list);
|
||||
}
|
||||
if(sourcePriority)
|
||||
list.Insert(0, value);
|
||||
else
|
||||
list.Add(value);
|
||||
}
|
||||
|
||||
private static string NormalizeKey(string key)
|
||||
{
|
||||
key = key.ToLowerInvariant();
|
||||
while(key.Length > 0 && key[0] == '-')
|
||||
{
|
||||
key = key.Substring(1);
|
||||
}
|
||||
key = key.Replace(".", "");
|
||||
return key;
|
||||
}
|
||||
|
||||
public void MergeInto(TextFileConfiguration destination, bool sourcePriority)
|
||||
{
|
||||
foreach(var kv in _Args)
|
||||
{
|
||||
foreach(var v in kv.Value)
|
||||
destination.Add(kv.Key, v, sourcePriority);
|
||||
}
|
||||
}
|
||||
|
||||
public TextFileConfiguration(Dictionary<string, List<string>> args)
|
||||
{
|
||||
_Args = args;
|
||||
}
|
||||
|
||||
public static TextFileConfiguration Parse(string data)
|
||||
{
|
||||
Dictionary<string, List<string>> result = new Dictionary<string, List<string>>();
|
||||
var lines = data.Split(new[] { "\r\n", "\n" }, StringSplitOptions.RemoveEmptyEntries);
|
||||
int lineCount = -1;
|
||||
foreach(var l in lines)
|
||||
{
|
||||
lineCount++;
|
||||
var line = l.Trim();
|
||||
if(line.StartsWith("#", StringComparison.Ordinal))
|
||||
continue;
|
||||
var split = line.Split('=');
|
||||
if(split.Length == 0)
|
||||
continue;
|
||||
if(split.Length == 1)
|
||||
throw new FormatException("Line " + lineCount + ": No value are set");
|
||||
|
||||
var key = split[0];
|
||||
key = NormalizeKey(key);
|
||||
List<string> values;
|
||||
if(!result.TryGetValue(key, out values))
|
||||
{
|
||||
values = new List<string>();
|
||||
result.Add(key, values);
|
||||
}
|
||||
var value = String.Join("=", split.Skip(1).ToArray());
|
||||
values.Add(value);
|
||||
}
|
||||
return new TextFileConfiguration(result);
|
||||
}
|
||||
|
||||
public bool Contains(string key)
|
||||
{
|
||||
List<string> values;
|
||||
return _Args.TryGetValue(key, out values);
|
||||
}
|
||||
public string[] GetAll(string key)
|
||||
{
|
||||
List<string> values;
|
||||
if(!_Args.TryGetValue(key, out values))
|
||||
return new string[0];
|
||||
return values.ToArray();
|
||||
}
|
||||
|
||||
private List<Tuple<string, string>> _Aliases = new List<Tuple<string, string>>();
|
||||
|
||||
public void AddAlias(string from, string to)
|
||||
{
|
||||
from = NormalizeKey(from);
|
||||
to = NormalizeKey(to);
|
||||
_Aliases.Add(Tuple.Create(from, to));
|
||||
}
|
||||
public T GetOrDefault<T>(string key, T defaultValue)
|
||||
{
|
||||
key = NormalizeKey(key);
|
||||
|
||||
var aliases = _Aliases
|
||||
.Where(a => a.Item1 == key || a.Item2 == key)
|
||||
.Select(a => a.Item1 == key ? a.Item2 : a.Item1)
|
||||
.ToList();
|
||||
aliases.Insert(0, key);
|
||||
|
||||
foreach(var alias in aliases)
|
||||
{
|
||||
List<string> values;
|
||||
if(!_Args.TryGetValue(alias, out values))
|
||||
continue;
|
||||
if(values.Count == 0)
|
||||
continue;
|
||||
try
|
||||
{
|
||||
return ConvertValue<T>(values[0]);
|
||||
}
|
||||
catch(FormatException) { throw new ConfigurationException("Key " + key + " should be of type " + typeof(T).Name); }
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
private T ConvertValue<T>(string str)
|
||||
{
|
||||
if(typeof(T) == typeof(bool))
|
||||
{
|
||||
var trueValues = new[] { "1", "true" };
|
||||
var falseValues = new[] { "0", "false" };
|
||||
if(trueValues.Contains(str, StringComparer.OrdinalIgnoreCase))
|
||||
return (T)(object)true;
|
||||
if(falseValues.Contains(str, StringComparer.OrdinalIgnoreCase))
|
||||
return (T)(object)false;
|
||||
throw new FormatException();
|
||||
}
|
||||
else if(typeof(T) == typeof(Uri))
|
||||
return (T)(object)new Uri(str, UriKind.Absolute);
|
||||
else if(typeof(T) == typeof(string))
|
||||
return (T)(object)str;
|
||||
else if(typeof(T) == typeof(IPEndPoint))
|
||||
{
|
||||
var separator = str.LastIndexOf(":");
|
||||
if(separator == -1)
|
||||
throw new FormatException();
|
||||
var ip = str.Substring(0, separator);
|
||||
var port = str.Substring(separator + 1);
|
||||
return (T)(object)new IPEndPoint(IPAddress.Parse(ip), int.Parse(port));
|
||||
}
|
||||
else if(typeof(T) == typeof(int))
|
||||
{
|
||||
return (T)(object)int.Parse(str, CultureInfo.InvariantCulture);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new NotSupportedException("Configuration value does not support time " + typeof(T).Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
62
BTCPayServer/Controllers/AccessTokenController.cs
Normal file
62
BTCPayServer/Controllers/AccessTokenController.cs
Normal file
|
@ -0,0 +1,62 @@
|
|||
using BTCPayServer.Authentication;
|
||||
using BTCPayServer.Filters;
|
||||
using BTCPayServer.Models;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NBitcoin.DataEncoders;
|
||||
using NBitpayClient;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public class AccessTokenController : Controller
|
||||
{
|
||||
TokenRepository _TokenRepository;
|
||||
public AccessTokenController(TokenRepository tokenRepository)
|
||||
{
|
||||
_TokenRepository = tokenRepository ?? throw new ArgumentNullException(nameof(tokenRepository));
|
||||
}
|
||||
[HttpGet]
|
||||
[Route("tokens")]
|
||||
public async Task<GetTokensResponse> GetTokens()
|
||||
{
|
||||
var tokens = await _TokenRepository.GetTokens(this.GetBitIdentity().SIN);
|
||||
return new GetTokensResponse(tokens);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("tokens")]
|
||||
public async Task<DataWrapper<List<PairingCodeResponse>>> GetPairingCode([FromBody] PairingCodeRequest token)
|
||||
{
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var pairingEntity = new PairingCodeEntity()
|
||||
{
|
||||
Facade = token.Facade,
|
||||
Label = token.Label,
|
||||
SIN = token.Id,
|
||||
PairingTime = now,
|
||||
PairingExpiration = now + TimeSpan.FromMinutes(15)
|
||||
};
|
||||
var grantedToken = await _TokenRepository.CreateToken(token.Id, token.Facade);
|
||||
pairingEntity.Token = grantedToken.Name;
|
||||
pairingEntity = await _TokenRepository.AddPairingCodeAsync(pairingEntity);
|
||||
|
||||
var pairingCodes = new List<PairingCodeResponse>
|
||||
{
|
||||
new PairingCodeResponse()
|
||||
{
|
||||
PairingCode = pairingEntity.Id,
|
||||
PairingExpiration = pairingEntity.PairingExpiration,
|
||||
DateCreated = pairingEntity.PairingTime,
|
||||
Facade = grantedToken.Name,
|
||||
Token = grantedToken.Value,
|
||||
Label = pairingEntity.Label
|
||||
}
|
||||
};
|
||||
return DataWrapper.Create(pairingCodes);
|
||||
}
|
||||
}
|
||||
}
|
486
BTCPayServer/Controllers/AccountController.cs
Normal file
486
BTCPayServer/Controllers/AccountController.cs
Normal file
|
@ -0,0 +1,486 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.AccountViewModels;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Stores;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
[Authorize]
|
||||
[Route("[controller]/[action]")]
|
||||
public class AccountController : Controller
|
||||
{
|
||||
private readonly UserManager<ApplicationUser> _userManager;
|
||||
private readonly SignInManager<ApplicationUser> _signInManager;
|
||||
private readonly IEmailSender _emailSender;
|
||||
private readonly ILogger _logger;
|
||||
StoreRepository storeRepository;
|
||||
|
||||
|
||||
public AccountController(
|
||||
UserManager<ApplicationUser> userManager,
|
||||
StoreRepository storeRepository,
|
||||
SignInManager<ApplicationUser> signInManager,
|
||||
IEmailSender emailSender,
|
||||
ILogger<AccountController> logger)
|
||||
{
|
||||
this.storeRepository = storeRepository;
|
||||
_userManager = userManager;
|
||||
_signInManager = signInManager;
|
||||
_emailSender = emailSender;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
[TempData]
|
||||
public string ErrorMessage { get; set; }
|
||||
|
||||
[HttpGet]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> Login(string returnUrl = null)
|
||||
{
|
||||
// Clear the existing external cookie to ensure a clean login process
|
||||
await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme);
|
||||
|
||||
ViewData["ReturnUrl"] = returnUrl;
|
||||
return View();
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[AllowAnonymous]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> Login(LoginViewModel model, string returnUrl = null)
|
||||
{
|
||||
ViewData["ReturnUrl"] = returnUrl;
|
||||
if (ModelState.IsValid)
|
||||
{
|
||||
// This doesn't count login failures towards account lockout
|
||||
// To enable password failures to trigger account lockout, set lockoutOnFailure: true
|
||||
var result = await _signInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, lockoutOnFailure: false);
|
||||
if (result.Succeeded)
|
||||
{
|
||||
_logger.LogInformation("User logged in.");
|
||||
return RedirectToLocal(returnUrl);
|
||||
}
|
||||
if (result.RequiresTwoFactor)
|
||||
{
|
||||
return RedirectToAction(nameof(LoginWith2fa), new { returnUrl, model.RememberMe });
|
||||
}
|
||||
if (result.IsLockedOut)
|
||||
{
|
||||
_logger.LogWarning("User account locked out.");
|
||||
return RedirectToAction(nameof(Lockout));
|
||||
}
|
||||
else
|
||||
{
|
||||
ModelState.AddModelError(string.Empty, "Invalid login attempt.");
|
||||
return View(model);
|
||||
}
|
||||
}
|
||||
|
||||
// If we got this far, something failed, redisplay form
|
||||
return View(model);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> LoginWith2fa(bool rememberMe, string returnUrl = null)
|
||||
{
|
||||
// Ensure the user has gone through the username & password screen first
|
||||
var user = await _signInManager.GetTwoFactorAuthenticationUserAsync();
|
||||
|
||||
if (user == null)
|
||||
{
|
||||
throw new ApplicationException($"Unable to load two-factor authentication user.");
|
||||
}
|
||||
|
||||
var model = new LoginWith2faViewModel { RememberMe = rememberMe };
|
||||
ViewData["ReturnUrl"] = returnUrl;
|
||||
|
||||
return View(model);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[AllowAnonymous]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> LoginWith2fa(LoginWith2faViewModel model, bool rememberMe, string returnUrl = null)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(model);
|
||||
}
|
||||
|
||||
var user = await _signInManager.GetTwoFactorAuthenticationUserAsync();
|
||||
if (user == null)
|
||||
{
|
||||
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
|
||||
var authenticatorCode = model.TwoFactorCode.Replace(" ", string.Empty).Replace("-", string.Empty);
|
||||
|
||||
var result = await _signInManager.TwoFactorAuthenticatorSignInAsync(authenticatorCode, rememberMe, model.RememberMachine);
|
||||
|
||||
if (result.Succeeded)
|
||||
{
|
||||
_logger.LogInformation("User with ID {UserId} logged in with 2fa.", user.Id);
|
||||
return RedirectToLocal(returnUrl);
|
||||
}
|
||||
else if (result.IsLockedOut)
|
||||
{
|
||||
_logger.LogWarning("User with ID {UserId} account locked out.", user.Id);
|
||||
return RedirectToAction(nameof(Lockout));
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("Invalid authenticator code entered for user with ID {UserId}.", user.Id);
|
||||
ModelState.AddModelError(string.Empty, "Invalid authenticator code.");
|
||||
return View();
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> LoginWithRecoveryCode(string returnUrl = null)
|
||||
{
|
||||
// Ensure the user has gone through the username & password screen first
|
||||
var user = await _signInManager.GetTwoFactorAuthenticationUserAsync();
|
||||
if (user == null)
|
||||
{
|
||||
throw new ApplicationException($"Unable to load two-factor authentication user.");
|
||||
}
|
||||
|
||||
ViewData["ReturnUrl"] = returnUrl;
|
||||
|
||||
return View();
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[AllowAnonymous]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> LoginWithRecoveryCode(LoginWithRecoveryCodeViewModel model, string returnUrl = null)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(model);
|
||||
}
|
||||
|
||||
var user = await _signInManager.GetTwoFactorAuthenticationUserAsync();
|
||||
if (user == null)
|
||||
{
|
||||
throw new ApplicationException($"Unable to load two-factor authentication user.");
|
||||
}
|
||||
|
||||
var recoveryCode = model.RecoveryCode.Replace(" ", string.Empty);
|
||||
|
||||
var result = await _signInManager.TwoFactorRecoveryCodeSignInAsync(recoveryCode);
|
||||
|
||||
if (result.Succeeded)
|
||||
{
|
||||
_logger.LogInformation("User with ID {UserId} logged in with a recovery code.", user.Id);
|
||||
return RedirectToLocal(returnUrl);
|
||||
}
|
||||
if (result.IsLockedOut)
|
||||
{
|
||||
_logger.LogWarning("User with ID {UserId} account locked out.", user.Id);
|
||||
return RedirectToAction(nameof(Lockout));
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("Invalid recovery code entered for user with ID {UserId}", user.Id);
|
||||
ModelState.AddModelError(string.Empty, "Invalid recovery code entered.");
|
||||
return View();
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[AllowAnonymous]
|
||||
public IActionResult Lockout()
|
||||
{
|
||||
return View();
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[AllowAnonymous]
|
||||
public IActionResult Register(string returnUrl = null)
|
||||
{
|
||||
ViewData["ReturnUrl"] = returnUrl;
|
||||
return View();
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[AllowAnonymous]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> Register(RegisterViewModel model, string returnUrl = null)
|
||||
{
|
||||
ViewData["ReturnUrl"] = returnUrl;
|
||||
if (ModelState.IsValid)
|
||||
{
|
||||
var user = new ApplicationUser { UserName = model.Email, Email = model.Email };
|
||||
var result = await _userManager.CreateAsync(user, model.Password);
|
||||
if (result.Succeeded)
|
||||
{
|
||||
_logger.LogInformation("User created a new account with password.");
|
||||
|
||||
var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
|
||||
var callbackUrl = Url.EmailConfirmationLink(user.Id, code, Request.Scheme);
|
||||
RegisteredUserId = user.Id;
|
||||
var store = await storeRepository.CreateStore(user.Id);
|
||||
RegisteredStoreId = store.Id;
|
||||
await _emailSender.SendEmailConfirmationAsync(model.Email, callbackUrl);
|
||||
await _signInManager.SignInAsync(user, isPersistent: false);
|
||||
_logger.LogInformation("User created a new account with password.");
|
||||
return RedirectToLocal(returnUrl);
|
||||
}
|
||||
AddErrors(result);
|
||||
}
|
||||
|
||||
// If we got this far, something failed, redisplay form
|
||||
return View(model);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test property
|
||||
/// </summary>
|
||||
public string RegisteredUserId
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test property
|
||||
/// </summary>
|
||||
public string RegisteredStoreId
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> Logout()
|
||||
{
|
||||
await _signInManager.SignOutAsync();
|
||||
_logger.LogInformation("User logged out.");
|
||||
return RedirectToAction(nameof(HomeController.Index), "Home");
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[AllowAnonymous]
|
||||
[ValidateAntiForgeryToken]
|
||||
public IActionResult ExternalLogin(string provider, string returnUrl = null)
|
||||
{
|
||||
// Request a redirect to the external login provider.
|
||||
var redirectUrl = Url.Action(nameof(ExternalLoginCallback), "Account", new { returnUrl });
|
||||
var properties = _signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl);
|
||||
return Challenge(properties, provider);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> ExternalLoginCallback(string returnUrl = null, string remoteError = null)
|
||||
{
|
||||
if (remoteError != null)
|
||||
{
|
||||
ErrorMessage = $"Error from external provider: {remoteError}";
|
||||
return RedirectToAction(nameof(Login));
|
||||
}
|
||||
var info = await _signInManager.GetExternalLoginInfoAsync();
|
||||
if (info == null)
|
||||
{
|
||||
return RedirectToAction(nameof(Login));
|
||||
}
|
||||
|
||||
// Sign in the user with this external login provider if the user already has a login.
|
||||
var result = await _signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, isPersistent: false, bypassTwoFactor: true);
|
||||
if (result.Succeeded)
|
||||
{
|
||||
_logger.LogInformation("User logged in with {Name} provider.", info.LoginProvider);
|
||||
return RedirectToLocal(returnUrl);
|
||||
}
|
||||
if (result.IsLockedOut)
|
||||
{
|
||||
return RedirectToAction(nameof(Lockout));
|
||||
}
|
||||
else
|
||||
{
|
||||
// If the user does not have an account, then ask the user to create an account.
|
||||
ViewData["ReturnUrl"] = returnUrl;
|
||||
ViewData["LoginProvider"] = info.LoginProvider;
|
||||
var email = info.Principal.FindFirstValue(ClaimTypes.Email);
|
||||
return View("ExternalLogin", new ExternalLoginViewModel { Email = email });
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[AllowAnonymous]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> ExternalLoginConfirmation(ExternalLoginViewModel model, string returnUrl = null)
|
||||
{
|
||||
if (ModelState.IsValid)
|
||||
{
|
||||
// Get the information about the user from the external login provider
|
||||
var info = await _signInManager.GetExternalLoginInfoAsync();
|
||||
if (info == null)
|
||||
{
|
||||
throw new ApplicationException("Error loading external login information during confirmation.");
|
||||
}
|
||||
var user = new ApplicationUser { UserName = model.Email, Email = model.Email };
|
||||
var result = await _userManager.CreateAsync(user);
|
||||
if (result.Succeeded)
|
||||
{
|
||||
result = await _userManager.AddLoginAsync(user, info);
|
||||
if (result.Succeeded)
|
||||
{
|
||||
await _signInManager.SignInAsync(user, isPersistent: false);
|
||||
_logger.LogInformation("User created an account using {Name} provider.", info.LoginProvider);
|
||||
return RedirectToLocal(returnUrl);
|
||||
}
|
||||
}
|
||||
AddErrors(result);
|
||||
}
|
||||
|
||||
ViewData["ReturnUrl"] = returnUrl;
|
||||
return View(nameof(ExternalLogin), model);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> ConfirmEmail(string userId, string code)
|
||||
{
|
||||
if (userId == null || code == null)
|
||||
{
|
||||
return RedirectToAction(nameof(HomeController.Index), "Home");
|
||||
}
|
||||
var user = await _userManager.FindByIdAsync(userId);
|
||||
if (user == null)
|
||||
{
|
||||
throw new ApplicationException($"Unable to load user with ID '{userId}'.");
|
||||
}
|
||||
var result = await _userManager.ConfirmEmailAsync(user, code);
|
||||
return View(result.Succeeded ? "ConfirmEmail" : "Error");
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[AllowAnonymous]
|
||||
public IActionResult ForgotPassword()
|
||||
{
|
||||
return View();
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[AllowAnonymous]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> ForgotPassword(ForgotPasswordViewModel model)
|
||||
{
|
||||
if (ModelState.IsValid)
|
||||
{
|
||||
var user = await _userManager.FindByEmailAsync(model.Email);
|
||||
if (user == null || !(await _userManager.IsEmailConfirmedAsync(user)))
|
||||
{
|
||||
// Don't reveal that the user does not exist or is not confirmed
|
||||
return RedirectToAction(nameof(ForgotPasswordConfirmation));
|
||||
}
|
||||
|
||||
// For more information on how to enable account confirmation and password reset please
|
||||
// visit https://go.microsoft.com/fwlink/?LinkID=532713
|
||||
var code = await _userManager.GeneratePasswordResetTokenAsync(user);
|
||||
var callbackUrl = Url.ResetPasswordCallbackLink(user.Id, code, Request.Scheme);
|
||||
await _emailSender.SendEmailAsync(model.Email, "Reset Password",
|
||||
$"Please reset your password by clicking here: <a href='{callbackUrl}'>link</a>");
|
||||
return RedirectToAction(nameof(ForgotPasswordConfirmation));
|
||||
}
|
||||
|
||||
// If we got this far, something failed, redisplay form
|
||||
return View(model);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[AllowAnonymous]
|
||||
public IActionResult ForgotPasswordConfirmation()
|
||||
{
|
||||
return View();
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[AllowAnonymous]
|
||||
public IActionResult ResetPassword(string code = null)
|
||||
{
|
||||
if (code == null)
|
||||
{
|
||||
throw new ApplicationException("A code must be supplied for password reset.");
|
||||
}
|
||||
var model = new ResetPasswordViewModel { Code = code };
|
||||
return View(model);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[AllowAnonymous]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> ResetPassword(ResetPasswordViewModel model)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(model);
|
||||
}
|
||||
var user = await _userManager.FindByEmailAsync(model.Email);
|
||||
if (user == null)
|
||||
{
|
||||
// Don't reveal that the user does not exist
|
||||
return RedirectToAction(nameof(ResetPasswordConfirmation));
|
||||
}
|
||||
var result = await _userManager.ResetPasswordAsync(user, model.Code, model.Password);
|
||||
if (result.Succeeded)
|
||||
{
|
||||
return RedirectToAction(nameof(ResetPasswordConfirmation));
|
||||
}
|
||||
AddErrors(result);
|
||||
return View();
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[AllowAnonymous]
|
||||
public IActionResult ResetPasswordConfirmation()
|
||||
{
|
||||
return View();
|
||||
}
|
||||
|
||||
|
||||
[HttpGet]
|
||||
public IActionResult AccessDenied()
|
||||
{
|
||||
return View();
|
||||
}
|
||||
|
||||
#region Helpers
|
||||
|
||||
private void AddErrors(IdentityResult result)
|
||||
{
|
||||
foreach (var error in result.Errors)
|
||||
{
|
||||
ModelState.AddModelError(string.Empty, error.Description);
|
||||
}
|
||||
}
|
||||
|
||||
private IActionResult RedirectToLocal(string returnUrl)
|
||||
{
|
||||
if (Url.IsLocalUrl(returnUrl))
|
||||
{
|
||||
return Redirect(returnUrl);
|
||||
}
|
||||
else
|
||||
{
|
||||
return RedirectToAction(nameof(HomeController.Index), "Home");
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
37
BTCPayServer/Controllers/HomeController.cs
Normal file
37
BTCPayServer/Controllers/HomeController.cs
Normal file
|
@ -0,0 +1,37 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using BTCPayServer.Models;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public class HomeController : Controller
|
||||
{
|
||||
public IActionResult Index()
|
||||
{
|
||||
return View();
|
||||
}
|
||||
|
||||
public IActionResult About()
|
||||
{
|
||||
ViewData["Message"] = "Your application description page.";
|
||||
|
||||
return View();
|
||||
}
|
||||
|
||||
public IActionResult Contact()
|
||||
{
|
||||
ViewData["Message"] = "Your contact page.";
|
||||
|
||||
return View();
|
||||
}
|
||||
|
||||
public IActionResult Error()
|
||||
{
|
||||
return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
|
||||
}
|
||||
}
|
||||
}
|
104
BTCPayServer/Controllers/InvoiceController.API.cs
Normal file
104
BTCPayServer/Controllers/InvoiceController.API.cs
Normal file
|
@ -0,0 +1,104 @@
|
|||
using BTCPayServer.Authentication;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using BTCPayServer.Filters;
|
||||
using BTCPayServer.Invoicing;
|
||||
using BTCPayServer.Logging;
|
||||
using BTCPayServer.Models;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NBitpayClient;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public partial class InvoiceController
|
||||
{
|
||||
[HttpPost]
|
||||
[Route("invoices")]
|
||||
[MediaTypeConstraint("application/json")]
|
||||
[BitpayAPIConstraint]
|
||||
public async Task<DataWrapper<InvoiceResponse>> CreateInvoice([FromBody] Invoice invoice)
|
||||
{
|
||||
var bitToken = await CheckTokenPermissionAsync(Facade.Merchant, invoice.Token);
|
||||
var store = await FindStore(bitToken);
|
||||
return await CreateInvoiceCore(invoice, store);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("invoices/{id}")]
|
||||
[BitpayAPIConstraint]
|
||||
public async Task<DataWrapper<InvoiceResponse>> GetInvoice(string id, string token)
|
||||
{
|
||||
var bitToken = await CheckTokenPermissionAsync(Facade.Merchant, token);
|
||||
var store = await FindStore(bitToken);
|
||||
var invoice = await _InvoiceRepository.GetInvoice(store.Id, id);
|
||||
if(invoice == null)
|
||||
throw new BitpayHttpException(404, "Object not found");
|
||||
|
||||
var resp = EntityToDTO(invoice);
|
||||
return new DataWrapper<InvoiceResponse>(resp);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("invoices")]
|
||||
[BitpayAPIConstraint]
|
||||
public async Task<DataWrapper<InvoiceResponse[]>> GetInvoices(
|
||||
string token,
|
||||
DateTimeOffset? dateStart = null,
|
||||
DateTimeOffset? dateEnd = null,
|
||||
string orderId = null,
|
||||
string itemCode = null,
|
||||
string status = null,
|
||||
int? limit = null,
|
||||
int? offset = null)
|
||||
{
|
||||
if(dateEnd != null)
|
||||
dateEnd = dateEnd.Value + TimeSpan.FromDays(1); //Should include the end day
|
||||
var bitToken = await CheckTokenPermissionAsync(Facade.Merchant, token);
|
||||
var store = await FindStore(bitToken);
|
||||
var query = new InvoiceQuery()
|
||||
{
|
||||
Count = limit,
|
||||
Skip = offset,
|
||||
EndDate = dateEnd,
|
||||
StartDate = dateStart,
|
||||
OrderId = orderId,
|
||||
ItemCode = itemCode,
|
||||
Status = status,
|
||||
StoreId = store.Id
|
||||
};
|
||||
|
||||
|
||||
var entities = (await _InvoiceRepository.GetInvoices(query))
|
||||
.Select(EntityToDTO).ToArray();
|
||||
|
||||
return DataWrapper.Create(entities);
|
||||
}
|
||||
|
||||
private async Task<BitTokenEntity> CheckTokenPermissionAsync(Facade facade, string exptectedToken)
|
||||
{
|
||||
if(facade == null)
|
||||
throw new ArgumentNullException(nameof(facade));
|
||||
|
||||
var actualToken = await _TokenRepository.GetToken(this.GetBitIdentity().SIN, facade.ToString());
|
||||
if(exptectedToken == null || actualToken == null || !actualToken.Value.Equals(exptectedToken, StringComparison.Ordinal))
|
||||
{
|
||||
Logs.PayServer.LogDebug($"No token found for facade {facade} for SIN {this.GetBitIdentity().SIN}");
|
||||
throw new BitpayHttpException(401, "This endpoint does not support the `user` facade");
|
||||
}
|
||||
return actualToken;
|
||||
}
|
||||
|
||||
private async Task<StoreData> FindStore(BitTokenEntity bitToken)
|
||||
{
|
||||
var store = await _StoreRepository.FindStore(bitToken.PairedId);
|
||||
if(store == null)
|
||||
throw new BitpayHttpException(401, "Unknown store");
|
||||
return store;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
using BTCPayServer.Filters;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using BTCPayServer.Logging;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NBitcoin;
|
||||
using NBitcoin.Payment;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public partial class InvoiceController
|
||||
{
|
||||
[HttpGet]
|
||||
[Route("i/{invoiceId}")]
|
||||
[AcceptMediaTypeConstraint("application/bitcoin-paymentrequest")]
|
||||
public async Task<IActionResult> GetInvoiceRequest(string invoiceId)
|
||||
{
|
||||
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
|
||||
if(invoice == null || invoice.IsExpired())
|
||||
return NotFound();
|
||||
|
||||
var dto = EntityToDTO(invoice);
|
||||
PaymentRequest request = new PaymentRequest
|
||||
{
|
||||
DetailsVersion = 1
|
||||
};
|
||||
request.Details.Expires = invoice.ExpirationTime;
|
||||
request.Details.Memo = invoice.ProductInformation.ItemDesc;
|
||||
request.Details.Network = _Network;
|
||||
request.Details.Outputs.Add(new PaymentOutput() { Amount = dto.BTCDue, Script = BitcoinAddress.Create(dto.BitcoinAddress, _Network).ScriptPubKey });
|
||||
request.Details.MerchantData = Encoding.UTF8.GetBytes(invoice.Id);
|
||||
request.Details.Time = DateTimeOffset.UtcNow;
|
||||
request.Details.PaymentUrl = new Uri(_ExternalUrl.GetAbsolute($"i/{invoice.Id}"), UriKind.Absolute);
|
||||
|
||||
var store = await _StoreRepository.FindStore(invoice.StoreId);
|
||||
if(store == null)
|
||||
throw new BitpayHttpException(401, "Unknown store");
|
||||
|
||||
if(store.StoreCertificate != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
request.Sign(store.StoreCertificate, PKIType.X509SHA256);
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
Logs.PayServer.LogWarning(ex, "Error while signing payment request");
|
||||
}
|
||||
}
|
||||
|
||||
return new PaymentRequestActionResult(request);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("i/{invoiceId}", Order = 99)]
|
||||
[MediaTypeConstraint("application/bitcoin-payment")]
|
||||
public async Task<IActionResult> PostPayment(string invoiceId)
|
||||
{
|
||||
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
|
||||
if(invoice == null || invoice.IsExpired())
|
||||
return NotFound();
|
||||
var payment = PaymentMessage.Load(Request.Body);
|
||||
var unused = _Wallet.BroadcastTransactionsAsync(payment.Transactions);
|
||||
await _InvoiceRepository.AddRefundsAsync(invoiceId, payment.RefundTo.Select(p => new TxOut(p.Amount, p.Script)).ToArray());
|
||||
return new PaymentAckActionResult(payment.CreateACK(invoiceId + " is currently processing, thanks for your purchase..."));
|
||||
}
|
||||
}
|
||||
}
|
185
BTCPayServer/Controllers/InvoiceController.UI.cs
Normal file
185
BTCPayServer/Controllers/InvoiceController.UI.cs
Normal file
|
@ -0,0 +1,185 @@
|
|||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Filters;
|
||||
using BTCPayServer.Invoicing;
|
||||
using BTCPayServer.Models.InvoicingModels;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NBitcoin;
|
||||
using NBitpayClient;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public partial class InvoiceController
|
||||
{
|
||||
|
||||
[HttpGet]
|
||||
[Route("i/{invoiceId}")]
|
||||
[AcceptMediaTypeConstraint("application/bitcoin-paymentrequest", false)]
|
||||
public async Task<IActionResult> Payment(string invoiceId)
|
||||
{
|
||||
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
|
||||
if(invoice == null)
|
||||
return NotFound();
|
||||
var store = await _StoreRepository.FindStore(invoice.StoreId);
|
||||
var dto = EntityToDTO(invoice);
|
||||
|
||||
var model = new PaymentModel()
|
||||
{
|
||||
OrderId = invoice.OrderId,
|
||||
InvoiceId = invoice.Id,
|
||||
BTCAddress = invoice.DepositAddress.ToString(),
|
||||
BTCAmount = (invoice.GetTotalCryptoDue() - invoice.TxFee).ToString(),
|
||||
BTCTotalDue = invoice.GetTotalCryptoDue().ToString(),
|
||||
BTCDue = invoice.GetCryptoDue().ToString(),
|
||||
CustomerEmail = invoice.RefundMail,
|
||||
ExpirationSeconds = Math.Max(0, (int)(invoice.ExpirationTime - DateTimeOffset.UtcNow).TotalSeconds),
|
||||
MaxTimeSeconds = (int)(invoice.ExpirationTime - invoice.InvoiceTime).TotalSeconds,
|
||||
ItemDesc = invoice.ProductInformation.ItemDesc,
|
||||
Rate = invoice.Rate.ToString(),
|
||||
RedirectUrl = invoice.RedirectURL,
|
||||
StoreName = store.StoreName,
|
||||
TxFees = invoice.TxFee.ToString(),
|
||||
InvoiceBitcoinUrl = dto.PaymentUrls.BIP72,
|
||||
TxCount = invoice.Payments.Count + 1,
|
||||
Status = invoice.Status
|
||||
};
|
||||
|
||||
var expiration = TimeSpan.FromSeconds((double)model.ExpirationSeconds);
|
||||
model.TimeLeft = PrettyPrint(expiration);
|
||||
return View(model);
|
||||
}
|
||||
|
||||
private string PrettyPrint(TimeSpan expiration)
|
||||
{
|
||||
StringBuilder builder = new StringBuilder();
|
||||
if(expiration.Days >= 1)
|
||||
builder.Append(expiration.Days.ToString());
|
||||
if(expiration.Hours >= 1)
|
||||
builder.Append(expiration.Hours.ToString("00"));
|
||||
builder.Append($"{expiration.Minutes.ToString("00")}:{expiration.Seconds.ToString("00")}");
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("i/{invoiceId}/status")]
|
||||
public async Task<IActionResult> GetStatus(string invoiceId)
|
||||
{
|
||||
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
|
||||
if(invoice == null)
|
||||
return NotFound();
|
||||
return Content(invoice.Status);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("i/{invoiceId}/UpdateCustomer")]
|
||||
public async Task<IActionResult> UpdateCustomer(string invoiceId, [FromBody]UpdateCustomerModel data)
|
||||
{
|
||||
if(!ModelState.IsValid)
|
||||
{
|
||||
return BadRequest(ModelState);
|
||||
}
|
||||
await _InvoiceRepository.UpdateInvoice(invoiceId, data).ConfigureAwait(false);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("Invoices")]
|
||||
[BitpayAPIConstraint(false)]
|
||||
public async Task<IActionResult> Index(string searchTerm = null, int skip = 0, int count = 20)
|
||||
{
|
||||
var store = await FindStore(User);
|
||||
var model = new InvoicesModel();
|
||||
foreach(var invoice in await _InvoiceRepository.GetInvoices(new InvoiceQuery()
|
||||
{
|
||||
TextSearch = searchTerm,
|
||||
Count = count,
|
||||
Skip = skip,
|
||||
StoreId = store.Id
|
||||
}))
|
||||
{
|
||||
model.SearchTerm = searchTerm;
|
||||
model.Invoices.Add(new InvoiceModel()
|
||||
{
|
||||
Status = invoice.Status,
|
||||
Date = invoice.InvoiceTime,
|
||||
InvoiceId = invoice.Id,
|
||||
AmountCurrency = $"{invoice.ProductInformation.Price.ToString(CultureInfo.InvariantCulture)} {invoice.ProductInformation.Currency}"
|
||||
});
|
||||
}
|
||||
model.Skip = skip;
|
||||
model.Count = count;
|
||||
model.StatusMessage = StatusMessage;
|
||||
return View(model);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("Invoices/Create")]
|
||||
[BitpayAPIConstraint(false)]
|
||||
public IActionResult CreateInvoice()
|
||||
{
|
||||
return View();
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("Invoices/Create")]
|
||||
[BitpayAPIConstraint(false)]
|
||||
public async Task<IActionResult> CreateInvoice(CreateInvoiceModel model)
|
||||
{
|
||||
if(!ModelState.IsValid)
|
||||
{
|
||||
return View(model);
|
||||
}
|
||||
var store = await FindStore(User);
|
||||
var result = await CreateInvoiceCore(new Invoice()
|
||||
{
|
||||
Price = model.Amount.Value,
|
||||
Currency = "USD",
|
||||
PosData = model.PosData,
|
||||
OrderId = model.OrderId,
|
||||
//RedirectURL = redirect + "redirect",
|
||||
//NotificationURL = CallbackUri + "/notification",
|
||||
ItemDesc = model.ItemDesc,
|
||||
FullNotifications = true,
|
||||
BuyerEmail = model.BuyerEmail
|
||||
}, store);
|
||||
|
||||
StatusMessage = $"Invoice {result.Data.Id} just created!";
|
||||
return RedirectToAction("Index");
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[BitpayAPIConstraint(false)]
|
||||
public IActionResult SearchInvoice(InvoicesModel invoices)
|
||||
{
|
||||
return RedirectToAction("Index", new
|
||||
{
|
||||
searchTerm = invoices.SearchTerm,
|
||||
skip = invoices.Skip,
|
||||
count = invoices.Count,
|
||||
});
|
||||
}
|
||||
|
||||
[TempData]
|
||||
public string StatusMessage
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
private async Task<StoreData> FindStore(ClaimsPrincipal user)
|
||||
{
|
||||
var usr = await _UserManager.GetUserAsync(User);
|
||||
if(user == null)
|
||||
{
|
||||
throw new ApplicationException($"Unable to load user with ID '{_UserManager.GetUserId(User)}'.");
|
||||
}
|
||||
return await _StoreRepository.GetStore(usr.Id);
|
||||
}
|
||||
}
|
||||
}
|
165
BTCPayServer/Controllers/InvoiceController.cs
Normal file
165
BTCPayServer/Controllers/InvoiceController.cs
Normal file
|
@ -0,0 +1,165 @@
|
|||
using BTCPayServer.Authentication;
|
||||
using System.Reflection;
|
||||
using System.Linq;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using BTCPayServer.Logging;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NBitpayClient;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Models;
|
||||
using Newtonsoft.Json;
|
||||
using BTCPayServer.Invoicing;
|
||||
using BTCPayServer.Wallet;
|
||||
using System.Globalization;
|
||||
using NBitcoin;
|
||||
using NBitcoin.DataEncoders;
|
||||
using BTCPayServer.RateProvider;
|
||||
using BTCPayServer.Filters;
|
||||
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
|
||||
using System.Net;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using NBitcoin.Payment;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Stores;
|
||||
using BTCPayServer.Models.InvoicingModels;
|
||||
using System.Security.Claims;
|
||||
using BTCPayServer.Services;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public partial class InvoiceController : Controller
|
||||
{
|
||||
TokenRepository _TokenRepository;
|
||||
InvoiceRepository _InvoiceRepository;
|
||||
IExternalUrlProvider _ExternalUrl;
|
||||
BTCPayWallet _Wallet;
|
||||
IRateProvider _RateProvider;
|
||||
private InvoiceWatcher _Watcher;
|
||||
StoreRepository _StoreRepository;
|
||||
Network _Network;
|
||||
UserManager<ApplicationUser> _UserManager;
|
||||
IFeeProvider _FeeProvider;
|
||||
|
||||
public InvoiceController(
|
||||
Network network,
|
||||
InvoiceRepository invoiceRepository,
|
||||
UserManager<ApplicationUser> userManager,
|
||||
TokenRepository tokenRepository,
|
||||
BTCPayWallet wallet,
|
||||
IExternalUrlProvider externalUrl,
|
||||
IRateProvider rateProvider,
|
||||
StoreRepository storeRepository,
|
||||
InvoiceWatcher watcher,
|
||||
IFeeProvider feeProvider)
|
||||
{
|
||||
_StoreRepository = storeRepository ?? throw new ArgumentNullException(nameof(storeRepository));
|
||||
_Network = network ?? throw new ArgumentNullException(nameof(network));
|
||||
_TokenRepository = tokenRepository ?? throw new ArgumentNullException(nameof(tokenRepository));
|
||||
_InvoiceRepository = invoiceRepository ?? throw new ArgumentNullException(nameof(invoiceRepository));
|
||||
_ExternalUrl = externalUrl;
|
||||
_Wallet = wallet ?? throw new ArgumentNullException(nameof(wallet));
|
||||
_RateProvider = rateProvider ?? throw new ArgumentNullException(nameof(rateProvider));
|
||||
_Watcher = watcher ?? throw new ArgumentNullException(nameof(watcher));
|
||||
_UserManager = userManager;
|
||||
_FeeProvider = feeProvider ?? throw new ArgumentNullException(nameof(feeProvider));
|
||||
}
|
||||
|
||||
static Regex _Email;
|
||||
bool IsEmail(string str)
|
||||
{
|
||||
if(String.IsNullOrWhiteSpace(str))
|
||||
return false;
|
||||
if(_Email == null)
|
||||
_Email = new Regex("^((([a-z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\u00A0-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFEF])+(\\.([a-z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\u00A0-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFEF])+)*)|((\\x22)((((\\x20|\\x09)*(\\x0d\\x0a))?(\\x20|\\x09)+)?(([\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]|\\x21|[\\x23-\\x5b]|[\\x5d-\\x7e]|[\\u00A0-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFEF])|(\\\\([\\x01-\\x09\\x0b\\x0c\\x0d-\\x7f]|[\\u00A0-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFEF]))))*(((\\x20|\\x09)*(\\x0d\\x0a))?(\\x20|\\x09)+)?(\\x22)))@((([a-z]|\\d|[\\u00A0-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFEF])|(([a-z]|\\d|[\\u00A0-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFEF])([a-z]|\\d|-|\\.|_|~|[\\u00A0-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFEF])*([a-z]|\\d|[\\u00A0-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFEF])))\\.)+(([a-z]|[\\u00A0-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFEF])|(([a-z]|[\\u00A0-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFEF])([a-z]|\\d|-|\\.|_|~|[\\u00A0-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFEF])*([a-z]|[\\u00A0-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFEF])))\\.?$", RegexOptions.IgnoreCase | RegexOptions.ExplicitCapture | RegexOptions.Compiled, TimeSpan.FromSeconds(2.0));
|
||||
return _Email.IsMatch(str);
|
||||
}
|
||||
|
||||
private async Task<DataWrapper<InvoiceResponse>> CreateInvoiceCore(Invoice invoice, StoreData store)
|
||||
{
|
||||
var derivationStrategy = store.DerivationStrategy;
|
||||
var entity = new InvoiceEntity
|
||||
{
|
||||
InvoiceTime = DateTimeOffset.UtcNow,
|
||||
DerivationStrategy = derivationStrategy ?? throw new BitpayHttpException(400, "This user has not configured his derivation strategy")
|
||||
};
|
||||
EmailAddressAttribute emailValidator = new EmailAddressAttribute();
|
||||
entity.ExpirationTime = entity.InvoiceTime + TimeSpan.FromMinutes(15.0);
|
||||
entity.BuyerInformation = Map<Invoice, BuyerInformation>(invoice);
|
||||
entity.RefundMail = IsEmail(entity?.BuyerInformation?.BuyerEmail) ? entity.BuyerInformation.BuyerEmail : null;
|
||||
entity.ProductInformation = Map<Invoice, ProductInformation>(invoice);
|
||||
entity.RedirectURL = invoice.RedirectURL ?? store.StoreWebsite;
|
||||
entity.Status = "new";
|
||||
entity.SpeedPolicy = store.SpeedPolicy;
|
||||
entity.TxFee = (await _FeeProvider.GetFeeRateAsync()).GetFee(100); // assume price for 100 bytes
|
||||
entity.Rate = (double)await _RateProvider.GetRateAsync(invoice.Currency);
|
||||
entity.PosData = invoice.PosData;
|
||||
entity.DepositAddress = await _Wallet.ReserveAddressAsync(derivationStrategy);
|
||||
|
||||
entity = await _InvoiceRepository.CreateInvoiceAsync(store.Id, entity);
|
||||
await _Wallet.MapAsync(entity.DepositAddress, entity.Id);
|
||||
await _Watcher.WatchAsync(entity.Id);
|
||||
var resp = EntityToDTO(entity);
|
||||
return new DataWrapper<InvoiceResponse>(resp) { Facade = "pos/invoice" };
|
||||
}
|
||||
|
||||
private InvoiceResponse EntityToDTO(InvoiceEntity entity)
|
||||
{
|
||||
InvoiceResponse dto = new InvoiceResponse
|
||||
{
|
||||
Id = entity.Id,
|
||||
OrderId = entity.OrderId,
|
||||
PosData = entity.PosData,
|
||||
CurrentTime = DateTimeOffset.UtcNow,
|
||||
InvoiceTime = entity.InvoiceTime,
|
||||
ExpirationTime = entity.ExpirationTime,
|
||||
BTCPrice = Money.Coins((decimal)(1.0 / entity.Rate)).ToString(),
|
||||
Status = entity.Status,
|
||||
Url = _ExternalUrl.GetAbsolute("invoice?id=" + entity.Id),
|
||||
Currency = entity.ProductInformation.Currency,
|
||||
Flags = new Flags() { Refundable = entity.Refundable }
|
||||
};
|
||||
Populate(entity.ProductInformation, dto);
|
||||
Populate(entity.BuyerInformation, dto);
|
||||
dto.ExRates = new Dictionary<string, double>
|
||||
{
|
||||
{ entity.ProductInformation.Currency, entity.Rate }
|
||||
};
|
||||
dto.PaymentUrls = new InvoicePaymentUrls()
|
||||
{
|
||||
BIP72 = $"bitcoin:{entity.DepositAddress}?amount={entity.GetCryptoDue()}&r={_ExternalUrl.GetAbsolute($"i/{entity.Id}")}",
|
||||
BIP72b = $"bitcoin:?r={_ExternalUrl.GetAbsolute($"i/{entity.Id}")}",
|
||||
BIP73 = _ExternalUrl.GetAbsolute($"i/{entity.Id}"),
|
||||
BIP21 = $"bitcoin:{entity.DepositAddress}?amount={entity.GetCryptoDue()}",
|
||||
};
|
||||
dto.BitcoinAddress = entity.DepositAddress.ToString();
|
||||
dto.Token = Encoders.Base58.EncodeData(RandomUtils.GetBytes(16)); //No idea what it is useful for
|
||||
dto.Guid = Guid.NewGuid().ToString();
|
||||
|
||||
var paid = entity.Payments.Select(p => p.Output.Value).Sum();
|
||||
dto.BTCPaid = paid.ToString();
|
||||
dto.BTCDue = entity.GetCryptoDue().ToString();
|
||||
dto.ExceptionStatus = entity.ExceptionStatus == null ? new JValue(false) : new JValue(entity.ExceptionStatus);
|
||||
return dto;
|
||||
}
|
||||
|
||||
private TDest Map<TFrom, TDest>(TFrom data)
|
||||
{
|
||||
return JsonConvert.DeserializeObject<TDest>(JsonConvert.SerializeObject(data));
|
||||
}
|
||||
private void Populate<TFrom, TDest>(TFrom from, TDest dest)
|
||||
{
|
||||
var str = JsonConvert.SerializeObject(from);
|
||||
JsonConvert.PopulateObject(str, dest);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
678
BTCPayServer/Controllers/ManageController.cs
Normal file
678
BTCPayServer/Controllers/ManageController.cs
Normal file
|
@ -0,0 +1,678 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.ManageViewModels;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Authentication;
|
||||
using BTCPayServer.Wallet;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using NBitpayClient;
|
||||
using NBitcoin;
|
||||
using BTCPayServer.Stores;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
[Authorize]
|
||||
[Route("[controller]/[action]")]
|
||||
public class ManageController : Controller
|
||||
{
|
||||
private readonly UserManager<ApplicationUser> _userManager;
|
||||
private readonly SignInManager<ApplicationUser> _signInManager;
|
||||
private readonly IEmailSender _emailSender;
|
||||
private readonly ILogger _logger;
|
||||
private readonly UrlEncoder _urlEncoder;
|
||||
TokenRepository _TokenRepository;
|
||||
private readonly BTCPayWallet _Wallet;
|
||||
IHostingEnvironment _Env;
|
||||
IExternalUrlProvider _UrlProvider;
|
||||
StoreRepository _StoreRepository;
|
||||
|
||||
|
||||
private const string AuthenicatorUriFormat = "otpauth://totp/{0}:{1}?secret={2}&issuer={0}&digits=6";
|
||||
|
||||
public ManageController(
|
||||
UserManager<ApplicationUser> userManager,
|
||||
SignInManager<ApplicationUser> signInManager,
|
||||
IEmailSender emailSender,
|
||||
ILogger<ManageController> logger,
|
||||
UrlEncoder urlEncoder,
|
||||
TokenRepository tokenRepository,
|
||||
BTCPayWallet wallet,
|
||||
StoreRepository storeRepository,
|
||||
IHostingEnvironment env,
|
||||
IExternalUrlProvider urlProvider)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_signInManager = signInManager;
|
||||
_emailSender = emailSender;
|
||||
_logger = logger;
|
||||
_urlEncoder = urlEncoder;
|
||||
_TokenRepository = tokenRepository;
|
||||
_Wallet = wallet;
|
||||
_Env = env;
|
||||
_UrlProvider = urlProvider;
|
||||
_StoreRepository = storeRepository;
|
||||
}
|
||||
|
||||
[TempData]
|
||||
public string StatusMessage
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> Index()
|
||||
{
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if(user == null)
|
||||
{
|
||||
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
var store = await _StoreRepository.GetStore(user.Id);
|
||||
|
||||
var model = new IndexViewModel
|
||||
{
|
||||
Username = user.UserName,
|
||||
Email = user.Email,
|
||||
PhoneNumber = user.PhoneNumber,
|
||||
IsEmailConfirmed = user.EmailConfirmed,
|
||||
StatusMessage = StatusMessage,
|
||||
ExtPubKey = store.DerivationStrategy,
|
||||
StoreWebsite = store.StoreWebsite,
|
||||
StoreName = store.StoreName,
|
||||
SpeedPolicy = store.SpeedPolicy
|
||||
};
|
||||
return View(model);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> Index(IndexViewModel model)
|
||||
{
|
||||
if(!ModelState.IsValid)
|
||||
{
|
||||
return View(model);
|
||||
}
|
||||
|
||||
bool needUpdate = false;
|
||||
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if(user == null)
|
||||
{
|
||||
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
var store = await _StoreRepository.GetStore(user.Id);
|
||||
|
||||
if(model.ExtPubKey != store.DerivationStrategy)
|
||||
{
|
||||
store.DerivationStrategy = model.ExtPubKey;
|
||||
await _Wallet.TrackAsync(store.DerivationStrategy);
|
||||
needUpdate = true;
|
||||
}
|
||||
|
||||
if(model.SpeedPolicy != store.SpeedPolicy)
|
||||
{
|
||||
store.SpeedPolicy = model.SpeedPolicy;
|
||||
needUpdate = true;
|
||||
}
|
||||
|
||||
if(model.StoreName != store.StoreName)
|
||||
{
|
||||
store.StoreName = model.StoreName;
|
||||
needUpdate = true;
|
||||
}
|
||||
|
||||
if(model.StoreWebsite != store.StoreWebsite)
|
||||
{
|
||||
store.StoreWebsite = model.StoreWebsite;
|
||||
needUpdate = true;
|
||||
}
|
||||
|
||||
var email = user.Email;
|
||||
if(model.Email != email)
|
||||
{
|
||||
var setEmailResult = await _userManager.SetEmailAsync(user, model.Email);
|
||||
if(!setEmailResult.Succeeded)
|
||||
{
|
||||
throw new ApplicationException($"Unexpected error occurred setting email for user with ID '{user.Id}'.");
|
||||
}
|
||||
}
|
||||
|
||||
var phoneNumber = user.PhoneNumber;
|
||||
if(model.PhoneNumber != phoneNumber)
|
||||
{
|
||||
var setPhoneResult = await _userManager.SetPhoneNumberAsync(user, model.PhoneNumber);
|
||||
if(!setPhoneResult.Succeeded)
|
||||
{
|
||||
throw new ApplicationException($"Unexpected error occurred setting phone number for user with ID '{user.Id}'.");
|
||||
}
|
||||
}
|
||||
|
||||
if(needUpdate)
|
||||
{
|
||||
var result = await _userManager.UpdateAsync(user);
|
||||
await _StoreRepository.UpdateStore(store);
|
||||
if(!result.Succeeded)
|
||||
{
|
||||
throw new ApplicationException($"Unexpected error occurred updating user with ID '{user.Id}'.");
|
||||
}
|
||||
}
|
||||
|
||||
StatusMessage = "Your profile has been updated";
|
||||
return RedirectToAction(nameof(Index));
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> SendVerificationEmail(IndexViewModel model)
|
||||
{
|
||||
if(!ModelState.IsValid)
|
||||
{
|
||||
return View(model);
|
||||
}
|
||||
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if(user == null)
|
||||
{
|
||||
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
|
||||
var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
|
||||
var callbackUrl = Url.EmailConfirmationLink(user.Id, code, Request.Scheme);
|
||||
var email = user.Email;
|
||||
await _emailSender.SendEmailConfirmationAsync(email, callbackUrl);
|
||||
|
||||
StatusMessage = "Verification email sent. Please check your email.";
|
||||
return RedirectToAction(nameof(Index));
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> ChangePassword()
|
||||
{
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if(user == null)
|
||||
{
|
||||
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
|
||||
var hasPassword = await _userManager.HasPasswordAsync(user);
|
||||
if(!hasPassword)
|
||||
{
|
||||
return RedirectToAction(nameof(SetPassword));
|
||||
}
|
||||
|
||||
var model = new ChangePasswordViewModel { StatusMessage = StatusMessage };
|
||||
return View(model);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> ChangePassword(ChangePasswordViewModel model)
|
||||
{
|
||||
if(!ModelState.IsValid)
|
||||
{
|
||||
return View(model);
|
||||
}
|
||||
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if(user == null)
|
||||
{
|
||||
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
|
||||
var changePasswordResult = await _userManager.ChangePasswordAsync(user, model.OldPassword, model.NewPassword);
|
||||
if(!changePasswordResult.Succeeded)
|
||||
{
|
||||
AddErrors(changePasswordResult);
|
||||
return View(model);
|
||||
}
|
||||
|
||||
await _signInManager.SignInAsync(user, isPersistent: false);
|
||||
_logger.LogInformation("User changed their password successfully.");
|
||||
StatusMessage = "Your password has been changed.";
|
||||
|
||||
return RedirectToAction(nameof(ChangePassword));
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> SetPassword()
|
||||
{
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if(user == null)
|
||||
{
|
||||
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
|
||||
var hasPassword = await _userManager.HasPasswordAsync(user);
|
||||
|
||||
if(hasPassword)
|
||||
{
|
||||
return RedirectToAction(nameof(ChangePassword));
|
||||
}
|
||||
|
||||
var model = new SetPasswordViewModel { StatusMessage = StatusMessage };
|
||||
return View(model);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> SetPassword(SetPasswordViewModel model)
|
||||
{
|
||||
if(!ModelState.IsValid)
|
||||
{
|
||||
return View(model);
|
||||
}
|
||||
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if(user == null)
|
||||
{
|
||||
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
|
||||
var addPasswordResult = await _userManager.AddPasswordAsync(user, model.NewPassword);
|
||||
if(!addPasswordResult.Succeeded)
|
||||
{
|
||||
AddErrors(addPasswordResult);
|
||||
return View(model);
|
||||
}
|
||||
|
||||
await _signInManager.SignInAsync(user, isPersistent: false);
|
||||
StatusMessage = "Your password has been set.";
|
||||
|
||||
return RedirectToAction(nameof(SetPassword));
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> ExternalLogins()
|
||||
{
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if(user == null)
|
||||
{
|
||||
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
|
||||
var model = new ExternalLoginsViewModel { CurrentLogins = await _userManager.GetLoginsAsync(user) };
|
||||
model.OtherLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync())
|
||||
.Where(auth => model.CurrentLogins.All(ul => auth.Name != ul.LoginProvider))
|
||||
.ToList();
|
||||
model.ShowRemoveButton = await _userManager.HasPasswordAsync(user) || model.CurrentLogins.Count > 1;
|
||||
model.StatusMessage = StatusMessage;
|
||||
|
||||
return View(model);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> LinkLogin(string provider)
|
||||
{
|
||||
// Clear the existing external cookie to ensure a clean login process
|
||||
await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme);
|
||||
|
||||
// Request a redirect to the external login provider to link a login for the current user
|
||||
var redirectUrl = Url.Action(nameof(LinkLoginCallback));
|
||||
var properties = _signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl, _userManager.GetUserId(User));
|
||||
return new ChallengeResult(provider, properties);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> LinkLoginCallback()
|
||||
{
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if(user == null)
|
||||
{
|
||||
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
|
||||
var info = await _signInManager.GetExternalLoginInfoAsync(user.Id);
|
||||
if(info == null)
|
||||
{
|
||||
throw new ApplicationException($"Unexpected error occurred loading external login info for user with ID '{user.Id}'.");
|
||||
}
|
||||
|
||||
var result = await _userManager.AddLoginAsync(user, info);
|
||||
if(!result.Succeeded)
|
||||
{
|
||||
throw new ApplicationException($"Unexpected error occurred adding external login for user with ID '{user.Id}'.");
|
||||
}
|
||||
|
||||
// Clear the existing external cookie to ensure a clean login process
|
||||
await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme);
|
||||
|
||||
StatusMessage = "The external login was added.";
|
||||
return RedirectToAction(nameof(ExternalLogins));
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("/api-access-request")]
|
||||
public async Task<IActionResult> AskPairing(string pairingCode)
|
||||
{
|
||||
var pairing = await _TokenRepository.GetPairingAsync(pairingCode);
|
||||
if(pairing == null)
|
||||
{
|
||||
StatusMessage = "Unknown pairing code";
|
||||
return RedirectToAction(nameof(Pairs));
|
||||
}
|
||||
else
|
||||
{
|
||||
return View(new PairingModel()
|
||||
{
|
||||
Id = pairing.Id,
|
||||
Facade = pairing.Facade,
|
||||
Label = pairing.Label,
|
||||
SIN = pairing.SIN
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> Pairs(string pairingCode)
|
||||
{
|
||||
var store = await _StoreRepository.GetStore(_userManager.GetUserId(User));
|
||||
if(pairingCode != null && await _TokenRepository.PairWithAsync(pairingCode, store.Id))
|
||||
{
|
||||
StatusMessage = "Pairing is successfull";
|
||||
return RedirectToAction(nameof(Tokens));
|
||||
}
|
||||
else
|
||||
{
|
||||
StatusMessage = "Pairing failed";
|
||||
return RedirectToAction(nameof(Tokens));
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> RemoveLogin(RemoveLoginViewModel model)
|
||||
{
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if(user == null)
|
||||
{
|
||||
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
|
||||
var result = await _userManager.RemoveLoginAsync(user, model.LoginProvider, model.ProviderKey);
|
||||
if(!result.Succeeded)
|
||||
{
|
||||
throw new ApplicationException($"Unexpected error occurred removing external login for user with ID '{user.Id}'.");
|
||||
}
|
||||
|
||||
await _signInManager.SignInAsync(user, isPersistent: false);
|
||||
StatusMessage = "The external login was removed.";
|
||||
return RedirectToAction(nameof(ExternalLogins));
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> TwoFactorAuthentication()
|
||||
{
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if(user == null)
|
||||
{
|
||||
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
|
||||
var model = new TwoFactorAuthenticationViewModel
|
||||
{
|
||||
HasAuthenticator = await _userManager.GetAuthenticatorKeyAsync(user) != null,
|
||||
Is2faEnabled = user.TwoFactorEnabled,
|
||||
RecoveryCodesLeft = await _userManager.CountRecoveryCodesAsync(user),
|
||||
};
|
||||
|
||||
return View(model);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> Disable2faWarning()
|
||||
{
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if(user == null)
|
||||
{
|
||||
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
|
||||
if(!user.TwoFactorEnabled)
|
||||
{
|
||||
throw new ApplicationException($"Unexpected error occured disabling 2FA for user with ID '{user.Id}'.");
|
||||
}
|
||||
|
||||
return View(nameof(Disable2fa));
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> Disable2fa()
|
||||
{
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if(user == null)
|
||||
{
|
||||
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
|
||||
var disable2faResult = await _userManager.SetTwoFactorEnabledAsync(user, false);
|
||||
if(!disable2faResult.Succeeded)
|
||||
{
|
||||
throw new ApplicationException($"Unexpected error occured disabling 2FA for user with ID '{user.Id}'.");
|
||||
}
|
||||
|
||||
_logger.LogInformation("User with ID {UserId} has disabled 2fa.", user.Id);
|
||||
return RedirectToAction(nameof(TwoFactorAuthentication));
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> EnableAuthenticator()
|
||||
{
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if(user == null)
|
||||
{
|
||||
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
|
||||
var unformattedKey = await _userManager.GetAuthenticatorKeyAsync(user);
|
||||
if(string.IsNullOrEmpty(unformattedKey))
|
||||
{
|
||||
await _userManager.ResetAuthenticatorKeyAsync(user);
|
||||
unformattedKey = await _userManager.GetAuthenticatorKeyAsync(user);
|
||||
}
|
||||
|
||||
var model = new EnableAuthenticatorViewModel
|
||||
{
|
||||
SharedKey = FormatKey(unformattedKey),
|
||||
AuthenticatorUri = GenerateQrCodeUri(user.Email, unformattedKey)
|
||||
};
|
||||
|
||||
return View(model);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> EnableAuthenticator(EnableAuthenticatorViewModel model)
|
||||
{
|
||||
if(!ModelState.IsValid)
|
||||
{
|
||||
return View(model);
|
||||
}
|
||||
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if(user == null)
|
||||
{
|
||||
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
|
||||
// Strip spaces and hypens
|
||||
var verificationCode = model.Code.Replace(" ", string.Empty).Replace("-", string.Empty);
|
||||
|
||||
var is2faTokenValid = await _userManager.VerifyTwoFactorTokenAsync(
|
||||
user, _userManager.Options.Tokens.AuthenticatorTokenProvider, verificationCode);
|
||||
|
||||
if(!is2faTokenValid)
|
||||
{
|
||||
ModelState.AddModelError("model.Code", "Verification code is invalid.");
|
||||
return View(model);
|
||||
}
|
||||
|
||||
await _userManager.SetTwoFactorEnabledAsync(user, true);
|
||||
_logger.LogInformation("User with ID {UserId} has enabled 2FA with an authenticator app.", user.Id);
|
||||
return RedirectToAction(nameof(GenerateRecoveryCodes));
|
||||
}
|
||||
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> AddToken(AddTokenViewModel model)
|
||||
{
|
||||
if(!ModelState.IsValid)
|
||||
{
|
||||
return View(model);
|
||||
}
|
||||
string storeId = await GetStoreId();
|
||||
|
||||
var url = new Uri(_UrlProvider.GetAbsolute(""));
|
||||
var bitpay = new Bitpay(new NBitcoin.Key(), url);
|
||||
var pairing = await bitpay.RequestClientAuthorizationAsync(model.Label, new Facade(model.Facade));
|
||||
var link = pairing.CreateLink(url).ToString();
|
||||
await _TokenRepository.PairWithAsync(pairing.ToString(), storeId);
|
||||
StatusMessage = "New access token paired to this store";
|
||||
return RedirectToAction("Tokens");
|
||||
}
|
||||
|
||||
private async Task<string> GetStoreId()
|
||||
{
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if(user == null)
|
||||
{
|
||||
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
return (await _StoreRepository.GetStore(user.Id)).Id;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public IActionResult AddToken()
|
||||
{
|
||||
var model = new AddTokenViewModel();
|
||||
model.Facade = "merchant";
|
||||
if(_Env.IsDevelopment())
|
||||
{
|
||||
model.PublicKey = new Key().PubKey.ToHex();
|
||||
}
|
||||
return View(model);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> DeleteToken(string name, string sin)
|
||||
{
|
||||
await _TokenRepository.DeleteToken(sin, name);
|
||||
StatusMessage = "Token revoked";
|
||||
return RedirectToAction("Tokens");
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> Tokens()
|
||||
{
|
||||
var model = new TokensViewModel();
|
||||
var tokens = await _TokenRepository.GetTokensByPairedIdAsync(await GetStoreId());
|
||||
model.StatusMessage = StatusMessage;
|
||||
model.Tokens = tokens.Select(t => new TokenViewModel()
|
||||
{
|
||||
Facade = t.Name,
|
||||
Label = t.Label,
|
||||
SIN = t.SIN,
|
||||
Id = t.Value
|
||||
}).ToArray();
|
||||
return View(model);
|
||||
}
|
||||
|
||||
|
||||
[HttpGet]
|
||||
public IActionResult ResetAuthenticatorWarning()
|
||||
{
|
||||
return View(nameof(ResetAuthenticator));
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> ResetAuthenticator()
|
||||
{
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if(user == null)
|
||||
{
|
||||
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
|
||||
await _userManager.SetTwoFactorEnabledAsync(user, false);
|
||||
await _userManager.ResetAuthenticatorKeyAsync(user);
|
||||
_logger.LogInformation("User with id '{UserId}' has reset their authentication app key.", user.Id);
|
||||
|
||||
return RedirectToAction(nameof(EnableAuthenticator));
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> GenerateRecoveryCodes()
|
||||
{
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if(user == null)
|
||||
{
|
||||
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
|
||||
if(!user.TwoFactorEnabled)
|
||||
{
|
||||
throw new ApplicationException($"Cannot generate recovery codes for user with ID '{user.Id}' as they do not have 2FA enabled.");
|
||||
}
|
||||
|
||||
var recoveryCodes = await _userManager.GenerateNewTwoFactorRecoveryCodesAsync(user, 10);
|
||||
var model = new GenerateRecoveryCodesViewModel { RecoveryCodes = recoveryCodes.ToArray() };
|
||||
|
||||
_logger.LogInformation("User with ID {UserId} has generated new 2FA recovery codes.", user.Id);
|
||||
|
||||
return View(model);
|
||||
}
|
||||
|
||||
#region Helpers
|
||||
|
||||
private void AddErrors(IdentityResult result)
|
||||
{
|
||||
foreach(var error in result.Errors)
|
||||
{
|
||||
ModelState.AddModelError(string.Empty, error.Description);
|
||||
}
|
||||
}
|
||||
|
||||
private string FormatKey(string unformattedKey)
|
||||
{
|
||||
var result = new StringBuilder();
|
||||
int currentPosition = 0;
|
||||
while(currentPosition + 4 < unformattedKey.Length)
|
||||
{
|
||||
result.Append(unformattedKey.Substring(currentPosition, 4)).Append(" ");
|
||||
currentPosition += 4;
|
||||
}
|
||||
if(currentPosition < unformattedKey.Length)
|
||||
{
|
||||
result.Append(unformattedKey.Substring(currentPosition));
|
||||
}
|
||||
|
||||
return result.ToString().ToLowerInvariant();
|
||||
}
|
||||
|
||||
private string GenerateQrCodeUri(string email, string unformattedKey)
|
||||
{
|
||||
return string.Format(
|
||||
AuthenicatorUriFormat,
|
||||
_urlEncoder.Encode("BTCPayServer"),
|
||||
_urlEncoder.Encode(email),
|
||||
unformattedKey);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
40
BTCPayServer/Controllers/PaymentRequestActionResult.cs
Normal file
40
BTCPayServer/Controllers/PaymentRequestActionResult.cs
Normal file
|
@ -0,0 +1,40 @@
|
|||
using Microsoft.AspNetCore.Mvc;
|
||||
using NBitcoin.Payment;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public class PaymentRequestActionResult : IActionResult
|
||||
{
|
||||
PaymentRequest req;
|
||||
public PaymentRequestActionResult(PaymentRequest req)
|
||||
{
|
||||
this.req = req;
|
||||
}
|
||||
public Task ExecuteResultAsync(ActionContext context)
|
||||
{
|
||||
context.HttpContext.Response.Headers["Content-Transfer-Encoding"] = "binary";
|
||||
context.HttpContext.Response.ContentType = "application/bitcoin-paymentrequest";
|
||||
req.WriteTo(context.HttpContext.Response.Body);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
public class PaymentAckActionResult : IActionResult
|
||||
{
|
||||
PaymentACK req;
|
||||
public PaymentAckActionResult(PaymentACK req)
|
||||
{
|
||||
this.req = req;
|
||||
}
|
||||
public Task ExecuteResultAsync(ActionContext context)
|
||||
{
|
||||
context.HttpContext.Response.Headers["Content-Transfer-Encoding"] = "binary";
|
||||
context.HttpContext.Response.ContentType = "application/bitcoin-paymentack";
|
||||
req.WriteTo(context.HttpContext.Response.Body);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
40
BTCPayServer/Controllers/RateController.cs
Normal file
40
BTCPayServer/Controllers/RateController.cs
Normal file
|
@ -0,0 +1,40 @@
|
|||
using BTCPayServer.Models;
|
||||
using BTCPayServer.RateProvider;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Filters;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public class RateController : Controller
|
||||
{
|
||||
IRateProvider _RateProvider;
|
||||
CurrencyNameTable _CurrencyNameTable;
|
||||
public RateController(IRateProvider rateProvider, CurrencyNameTable currencyNameTable)
|
||||
{
|
||||
_RateProvider = rateProvider ?? throw new ArgumentNullException(nameof(rateProvider));
|
||||
_CurrencyNameTable = currencyNameTable ?? throw new ArgumentNullException(nameof(currencyNameTable));
|
||||
}
|
||||
|
||||
[Route("rates")]
|
||||
[HttpGet]
|
||||
[BitpayAPIConstraint]
|
||||
public async Task<DataWrapper<NBitpayClient.Rate[]>> GetRates()
|
||||
{
|
||||
var allRates = (await _RateProvider.GetRatesAsync());
|
||||
return new DataWrapper<NBitpayClient.Rate[]>
|
||||
(allRates.Select(r =>
|
||||
new NBitpayClient.Rate()
|
||||
{
|
||||
Code = r.Currency,
|
||||
Name = _CurrencyNameTable.GetCurrencyData(r.Currency)?.Name,
|
||||
Value = r.Value
|
||||
}).Where(n => n.Name != null).ToArray());
|
||||
|
||||
}
|
||||
}
|
||||
}
|
269
BTCPayServer/Currencies.txt
Normal file
269
BTCPayServer/Currencies.txt
Normal file
|
@ -0,0 +1,269 @@
|
|||
Afghani AFN 2
|
||||
Bitcoin BTC 8 Ƀ
|
||||
Euro EUR 2 €
|
||||
Lek ALL 2
|
||||
Algerian Dinar DZD 2
|
||||
US Dollar USD 2 $
|
||||
Euro EUR 2
|
||||
Kwanza AOA 2
|
||||
East Caribbean Dollar XCD 2
|
||||
No universal currency
|
||||
East Caribbean Dollar XCD 2
|
||||
Argentine Peso ARS 2
|
||||
Armenian Dram AMD 2
|
||||
Aruban Florin AWG 2
|
||||
Australian Dollar AUD 2 A$
|
||||
Euro EUR 2
|
||||
Azerbaijanian Manat AZN 2
|
||||
Bahamian Dollar BSD 2
|
||||
Bahraini Dinar BHD 3
|
||||
Taka BDT 2
|
||||
Barbados Dollar BBD 2
|
||||
Belarusian Ruble BYN 2
|
||||
Belarusian Ruble BYR 0
|
||||
Euro EUR 2
|
||||
Belize Dollar BZD 2
|
||||
CFA Franc BCEAO XOF 0
|
||||
Bermudian Dollar BMD 2
|
||||
Indian Rupee INR 2
|
||||
Ngultrum BTN 2
|
||||
Boliviano BOB 2
|
||||
Mvdol BOV 2
|
||||
US Dollar USD 2
|
||||
Convertible Mark BAM 2
|
||||
Pula BWP 2
|
||||
Norwegian Krone NOK 2
|
||||
Brazilian Real BRL 2
|
||||
US Dollar USD 2
|
||||
Brunei Dollar BND 2
|
||||
Bulgarian Lev BGN 2
|
||||
CFA Franc BCEAO XOF 0
|
||||
Burundi Franc BIF 0
|
||||
Cabo Verde Escudo CVE 2
|
||||
Riel KHR 2
|
||||
CFA Franc BEAC XAF 0
|
||||
Canadian Dollar CAD 2 CA$
|
||||
Cayman Islands Dollar KYD 2
|
||||
CFA Franc BEAC XAF 0
|
||||
CFA Franc BEAC XAF 0
|
||||
Chilean Peso CLP 0
|
||||
Unidad de Fomento CLF 4
|
||||
Yuan Renminbi CNY 2 CN¥
|
||||
Australian Dollar AUD 2 A$
|
||||
Colombian Peso COP 2
|
||||
Unidad de Valor Real COU 2
|
||||
Comoro Franc KMF 0
|
||||
Congolese Franc CDF 2
|
||||
CFA Franc BEAC XAF 0
|
||||
New Zealand Dollar NZD 2 NZ$
|
||||
Costa Rican Colon CRC 2
|
||||
CFA Franc BCEAO XOF 0
|
||||
Kuna HRK 2
|
||||
Cuban Peso CUP 2
|
||||
Peso Convertible CUC 2
|
||||
Netherlands Antillean Guilder ANG 2
|
||||
Euro EUR 2
|
||||
Czech Koruna CZK 2
|
||||
Danish Krone DKK 2
|
||||
Djibouti Franc DJF 0
|
||||
East Caribbean Dollar XCD 2
|
||||
Dominican Peso DOP 2
|
||||
US Dollar USD 2
|
||||
Egyptian Pound EGP 2
|
||||
El Salvador Colon SVC 2
|
||||
US Dollar USD 2
|
||||
CFA Franc BEAC XAF 0
|
||||
Nakfa ERN 2
|
||||
Euro EUR 2
|
||||
Ethiopian Birr ETB 2
|
||||
Euro EUR 2
|
||||
Falkland Islands Pound FKP 2
|
||||
Danish Krone DKK 2
|
||||
Fiji Dollar FJD 2
|
||||
Euro EUR 2
|
||||
Euro EUR 2
|
||||
Euro EUR 2
|
||||
CFP Franc XPF 0
|
||||
Euro EUR 2
|
||||
CFA Franc BEAC XAF 0
|
||||
Dalasi GMD 2
|
||||
Lari GEL 2
|
||||
Euro EUR 2
|
||||
Ghana Cedi GHS 2
|
||||
Gibraltar Pound GIP 2
|
||||
Euro EUR 2
|
||||
Danish Krone DKK 2
|
||||
East Caribbean Dollar XCD 2
|
||||
Euro EUR 2
|
||||
US Dollar USD 2
|
||||
Quetzal GTQ 2
|
||||
Pound Sterling GBP 2 £
|
||||
Guinea Franc GNF 0
|
||||
CFA Franc BCEAO XOF 0
|
||||
Guyana Dollar GYD 2
|
||||
Gourde HTG 2
|
||||
US Dollar USD 2
|
||||
Australian Dollar AUD 2
|
||||
Euro EUR 2
|
||||
Lempira HNL 2
|
||||
Hong Kong Dollar HKD 2
|
||||
Forint HUF 2
|
||||
Iceland Krona ISK 0
|
||||
Indian Rupee INR 2
|
||||
Rupiah IDR 2
|
||||
SDR (Special Drawing Right) XDR N.A.
|
||||
Iranian Rial IRR 2
|
||||
Iraqi Dinar IQD 3
|
||||
Euro EUR 2
|
||||
Pound Sterling GBP 2
|
||||
New Israeli Sheqel ILS 2
|
||||
Euro EUR 2
|
||||
Jamaican Dollar JMD 2
|
||||
Yen JPY 0 ¥
|
||||
Pound Sterling GBP 2
|
||||
Jordanian Dinar JOD 3
|
||||
Tenge KZT 2
|
||||
Kenyan Shilling KES 2
|
||||
Australian Dollar AUD 2
|
||||
North Korean Won KPW 2
|
||||
Won KRW 0 ₩
|
||||
Kuwaiti Dinar KWD 3
|
||||
Som KGS 2
|
||||
Kip LAK 2
|
||||
Euro EUR 2
|
||||
Lebanese Pound LBP 2
|
||||
Loti LSL 2
|
||||
Rand ZAR 2
|
||||
Liberian Dollar LRD 2
|
||||
Libyan Dinar LYD 3
|
||||
Swiss Franc CHF 2
|
||||
Euro EUR 2
|
||||
Euro EUR 2
|
||||
Pataca MOP 2
|
||||
Denar MKD 2
|
||||
Malagasy Ariary MGA 2
|
||||
Malawi Kwacha MWK 2
|
||||
Malaysian Ringgit MYR 2
|
||||
Rufiyaa MVR 2
|
||||
CFA Franc BCEAO XOF 0
|
||||
Euro EUR 2
|
||||
US Dollar USD 2
|
||||
Euro EUR 2
|
||||
Ouguiya MRO 2
|
||||
Mauritius Rupee MUR 2
|
||||
Euro EUR 2
|
||||
ADB Unit of Account XUA N.A.
|
||||
Mexican Peso MXN 2
|
||||
Mexican Unidad de Inversion (UDI) MXV 2
|
||||
US Dollar USD 2
|
||||
Moldovan Leu MDL 2
|
||||
Euro EUR 2
|
||||
Tugrik MNT 2
|
||||
Euro EUR 2
|
||||
East Caribbean Dollar XCD 2
|
||||
Moroccan Dirham MAD 2
|
||||
Mozambique Metical MZN 2
|
||||
Kyat MMK 2
|
||||
Namibia Dollar NAD 2
|
||||
Rand ZAR 2
|
||||
Australian Dollar AUD 2
|
||||
Nepalese Rupee NPR 2
|
||||
Euro EUR 2
|
||||
CFP Franc XPF 0
|
||||
New Zealand Dollar NZD 2
|
||||
Cordoba Oro NIO 2
|
||||
CFA Franc BCEAO XOF 0
|
||||
Naira NGN 2
|
||||
New Zealand Dollar NZD 2
|
||||
Australian Dollar AUD 2
|
||||
US Dollar USD 2
|
||||
Norwegian Krone NOK 2
|
||||
Rial Omani OMR 3
|
||||
Pakistan Rupee PKR 2
|
||||
US Dollar USD 2
|
||||
No universal currency
|
||||
Balboa PAB 2
|
||||
US Dollar USD 2
|
||||
Kina PGK 2
|
||||
Guarani PYG 0
|
||||
Sol PEN 2
|
||||
Philippine Peso PHP 2
|
||||
New Zealand Dollar NZD 2
|
||||
Zloty PLN 2
|
||||
Euro EUR 2
|
||||
US Dollar USD 2
|
||||
Qatari Rial QAR 2
|
||||
Euro EUR 2
|
||||
Romanian Leu RON 2
|
||||
Russian Ruble RUB 2
|
||||
Rwanda Franc RWF 0
|
||||
Euro EUR 2
|
||||
Saint Helena Pound SHP 2
|
||||
East Caribbean Dollar XCD 2
|
||||
East Caribbean Dollar XCD 2
|
||||
Euro EUR 2
|
||||
Euro EUR 2
|
||||
East Caribbean Dollar XCD 2
|
||||
Tala WST 2
|
||||
Euro EUR 2
|
||||
Dobra STD 2
|
||||
Saudi Riyal SAR 2
|
||||
CFA Franc BCEAO XOF 0
|
||||
Serbian Dinar RSD 2
|
||||
Seychelles Rupee SCR 2
|
||||
Leone SLL 2
|
||||
Singapore Dollar SGD 2
|
||||
Netherlands Antillean Guilder ANG 2
|
||||
Sucre XSU N.A.
|
||||
Euro EUR 2
|
||||
Euro EUR 2
|
||||
Solomon Islands Dollar SBD 2
|
||||
Somali Shilling SOS 2
|
||||
Rand ZAR 2
|
||||
No universal currency
|
||||
South Sudanese Pound SSP 2
|
||||
Euro EUR 2
|
||||
Sri Lanka Rupee LKR 2
|
||||
Sudanese Pound SDG 2
|
||||
Surinam Dollar SRD 2
|
||||
Norwegian Krone NOK 2
|
||||
Lilangeni SZL 2
|
||||
Swedish Krona SEK 2
|
||||
Swiss Franc CHF 2
|
||||
WIR Euro CHE 2
|
||||
WIR Franc CHW 2
|
||||
Syrian Pound SYP 2
|
||||
New Taiwan Dollar TWD 2
|
||||
Somoni TJS 2
|
||||
Tanzanian Shilling TZS 2
|
||||
Baht THB 2
|
||||
US Dollar USD 2
|
||||
CFA Franc BCEAO XOF 0
|
||||
New Zealand Dollar NZD 2
|
||||
Pa’anga TOP 2
|
||||
Trinidad and Tobago Dollar TTD 2
|
||||
Tunisian Dinar TND 3
|
||||
Turkish Lira TRY 2
|
||||
Turkmenistan New Manat TMT 2
|
||||
US Dollar USD 2
|
||||
Australian Dollar AUD 2
|
||||
Uganda Shilling UGX 0
|
||||
Hryvnia UAH 2
|
||||
UAE Dirham AED 2
|
||||
Pound Sterling GBP 2
|
||||
US Dollar USD 2
|
||||
US Dollar USD 2
|
||||
US Dollar (Next day) USN 2
|
||||
Peso Uruguayo UYU 2
|
||||
Uruguay Peso en Unidades Indexadas (URUIURUI) UYI 0
|
||||
Uzbekistan Sum UZS 2
|
||||
Vatu VUV 0
|
||||
Bolívar VEF 2
|
||||
Dong VND 0
|
||||
US Dollar USD 2
|
||||
US Dollar USD 2
|
||||
CFP Franc XPF 0
|
||||
Moroccan Dirham MAD 2
|
||||
Yemeni Rial YER 2
|
||||
Zambian Kwacha ZMW 2
|
||||
Zimbabwe Dollar ZWL 2
|
79
BTCPayServer/Data/ApplicationDbContext.cs
Normal file
79
BTCPayServer/Data/ApplicationDbContext.cs
Normal file
|
@ -0,0 +1,79 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using BTCPayServer.Models;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure.Internal;
|
||||
|
||||
namespace BTCPayServer.Data
|
||||
{
|
||||
public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
|
||||
{
|
||||
public ApplicationDbContext()
|
||||
{
|
||||
|
||||
}
|
||||
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
|
||||
: base(options)
|
||||
{
|
||||
}
|
||||
|
||||
public DbSet<InvoiceData> Invoices
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public DbSet<RefundAddressesData> RefundAddresses
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public DbSet<PaymentData> Payments
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public DbSet<StoreData> Stores
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||
{
|
||||
var options = optionsBuilder.Options.FindExtension<SqliteOptionsExtension>();
|
||||
if(options?.ConnectionString == null)
|
||||
optionsBuilder.UseSqlite("Data Source=temp.db");
|
||||
}
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder builder)
|
||||
{
|
||||
base.OnModelCreating(builder);
|
||||
builder.Entity<InvoiceData>()
|
||||
.HasIndex(o => o.StoreDataId);
|
||||
|
||||
builder.Entity<PaymentData>()
|
||||
.HasIndex(o => o.InvoiceDataId);
|
||||
|
||||
builder.Entity<RefundAddressesData>()
|
||||
.HasIndex(o => o.InvoiceDataId);
|
||||
|
||||
builder.Entity<UserStore>()
|
||||
.HasKey(t => new {
|
||||
t.ApplicationUserId,
|
||||
t.StoreDataId
|
||||
});
|
||||
|
||||
builder.Entity<UserStore>()
|
||||
.HasOne(pt => pt.ApplicationUser)
|
||||
.WithMany(p => p.UserStores)
|
||||
.HasForeignKey(pt => pt.ApplicationUserId);
|
||||
|
||||
builder.Entity<UserStore>()
|
||||
.HasOne(pt => pt.StoreData)
|
||||
.WithMany(t => t.UserStores)
|
||||
.HasForeignKey(pt => pt.StoreDataId);
|
||||
}
|
||||
}
|
||||
}
|
24
BTCPayServer/Data/ApplicationDbContextFactory.cs
Normal file
24
BTCPayServer/Data/ApplicationDbContextFactory.cs
Normal file
|
@ -0,0 +1,24 @@
|
|||
using Microsoft.EntityFrameworkCore;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Data
|
||||
{
|
||||
public class ApplicationDbContextFactory
|
||||
{
|
||||
string _Path;
|
||||
public ApplicationDbContextFactory(string path)
|
||||
{
|
||||
_Path = path ?? throw new ArgumentNullException(nameof(path));
|
||||
}
|
||||
|
||||
public ApplicationDbContext CreateContext()
|
||||
{
|
||||
var builder = new DbContextOptionsBuilder<ApplicationDbContext>();
|
||||
builder.UseSqlite("Data Source=" + _Path);
|
||||
return new ApplicationDbContext(builder.Options);
|
||||
}
|
||||
}
|
||||
}
|
70
BTCPayServer/Data/InvoiceData.cs
Normal file
70
BTCPayServer/Data/InvoiceData.cs
Normal file
|
@ -0,0 +1,70 @@
|
|||
using BTCPayServer.Models;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Data
|
||||
{
|
||||
public class InvoiceData
|
||||
{
|
||||
public string StoreDataId
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public StoreData StoreData
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public string Id
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public DateTimeOffset Created
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public List<PaymentData> Payments
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public List<RefundAddressesData> RefundAddresses
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public byte[] Blob
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public string ItemCode
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public string OrderId
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public string Status
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public string ExceptionStatus
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public string CustomerEmail
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
}
|
||||
}
|
29
BTCPayServer/Data/PaymentData.cs
Normal file
29
BTCPayServer/Data/PaymentData.cs
Normal file
|
@ -0,0 +1,29 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Data
|
||||
{
|
||||
public class PaymentData
|
||||
{
|
||||
public string Id
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public string InvoiceDataId
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public InvoiceData InvoiceData
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public byte[] Blob
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
}
|
||||
}
|
27
BTCPayServer/Data/RefundData.cs
Normal file
27
BTCPayServer/Data/RefundData.cs
Normal file
|
@ -0,0 +1,27 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Data
|
||||
{
|
||||
public class RefundAddressesData
|
||||
{
|
||||
public string Id
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public string InvoiceDataId
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public InvoiceData InvoiceData
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public byte[] Blob
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
}
|
||||
}
|
48
BTCPayServer/Data/StoreData.cs
Normal file
48
BTCPayServer/Data/StoreData.cs
Normal file
|
@ -0,0 +1,48 @@
|
|||
using BTCPayServer.Invoicing;
|
||||
using BTCPayServer.Models;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Data
|
||||
{
|
||||
public class StoreData
|
||||
{
|
||||
public string Id
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public List<UserStore> UserStores
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public string DerivationStrategy
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public string StoreName
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public SpeedPolicy SpeedPolicy
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public string StoreWebsite
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public byte[] StoreCertificate
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
}
|
||||
}
|
29
BTCPayServer/Data/UserStore.cs
Normal file
29
BTCPayServer/Data/UserStore.cs
Normal file
|
@ -0,0 +1,29 @@
|
|||
using BTCPayServer.Models;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Data
|
||||
{
|
||||
public class UserStore
|
||||
{
|
||||
public string ApplicationUserId
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public ApplicationUser ApplicationUser
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public string StoreDataId
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public StoreData StoreData
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
}
|
||||
}
|
18
BTCPayServer/Extensions.cs
Normal file
18
BTCPayServer/Extensions.cs
Normal file
|
@ -0,0 +1,18 @@
|
|||
using BTCPayServer.Authentication;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
public static class Extensions
|
||||
{
|
||||
public static BitIdentity GetBitIdentity(this Controller controller)
|
||||
{
|
||||
if(!(controller.User.Identity is BitIdentity))
|
||||
throw new UnauthorizedAccessException("no-bitid");
|
||||
return (BitIdentity)controller.User.Identity;
|
||||
}
|
||||
}
|
||||
}
|
18
BTCPayServer/Extensions/EmailSenderExtensions.cs
Normal file
18
BTCPayServer/Extensions/EmailSenderExtensions.cs
Normal file
|
@ -0,0 +1,18 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Services;
|
||||
|
||||
namespace BTCPayServer.Services
|
||||
{
|
||||
public static class EmailSenderExtensions
|
||||
{
|
||||
public static Task SendEmailConfirmationAsync(this IEmailSender emailSender, string email, string link)
|
||||
{
|
||||
return emailSender.SendEmailAsync(email, "Confirm your email",
|
||||
$"Please confirm your account by clicking this link: <a href='{HtmlEncoder.Default.Encode(link)}'>link</a>");
|
||||
}
|
||||
}
|
||||
}
|
29
BTCPayServer/Extensions/UrlHelperExtensions.cs
Normal file
29
BTCPayServer/Extensions/UrlHelperExtensions.cs
Normal file
|
@ -0,0 +1,29 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Controllers;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc
|
||||
{
|
||||
public static class UrlHelperExtensions
|
||||
{
|
||||
public static string EmailConfirmationLink(this IUrlHelper urlHelper, string userId, string code, string scheme)
|
||||
{
|
||||
return urlHelper.Action(
|
||||
action: nameof(AccountController.ConfirmEmail),
|
||||
controller: "Account",
|
||||
values: new { userId, code },
|
||||
protocol: scheme);
|
||||
}
|
||||
|
||||
public static string ResetPasswordCallbackLink(this IUrlHelper urlHelper, string userId, string code, string scheme)
|
||||
{
|
||||
return urlHelper.Action(
|
||||
action: nameof(AccountController.ResetPassword),
|
||||
controller: "Account",
|
||||
values: new { userId, code },
|
||||
protocol: scheme);
|
||||
}
|
||||
}
|
||||
}
|
98
BTCPayServer/ExternalUrlProvider.cs
Normal file
98
BTCPayServer/ExternalUrlProvider.cs
Normal file
|
@ -0,0 +1,98 @@
|
|||
using Microsoft.AspNetCore.Http;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using Microsoft.AspNetCore.Http.Extensions;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
public interface IExternalUrlProvider
|
||||
{
|
||||
string GetEncodedUrl();
|
||||
string GetAbsolute(string path);
|
||||
}
|
||||
|
||||
public class DefaultExternalUrlProvider : IExternalUrlProvider
|
||||
{
|
||||
IHttpContextAccessor _ContextAccessor;
|
||||
public DefaultExternalUrlProvider(IHttpContextAccessor contextAccessor)
|
||||
{
|
||||
if(contextAccessor == null)
|
||||
throw new ArgumentNullException(nameof(contextAccessor));
|
||||
_ContextAccessor = contextAccessor;
|
||||
}
|
||||
public string GetAbsolute(string path)
|
||||
{
|
||||
var request = _ContextAccessor.HttpContext.Request;
|
||||
var builder = new UriBuilder()
|
||||
{
|
||||
Scheme = request.Scheme,
|
||||
Host = request.Host.Host,
|
||||
};
|
||||
if(request.Host.Port.HasValue)
|
||||
builder.Port = request.Host.Port.Value;
|
||||
return builder.Uri.AbsoluteUri + path;
|
||||
}
|
||||
|
||||
public string GetEncodedUrl()
|
||||
{
|
||||
var request = _ContextAccessor.HttpContext.Request;
|
||||
return request.GetEncodedUrl();
|
||||
}
|
||||
}
|
||||
|
||||
public class FixedExternalUrlProvider : IExternalUrlProvider
|
||||
{
|
||||
string _Url;
|
||||
IHttpContextAccessor _ContextAccessor;
|
||||
public FixedExternalUrlProvider(Uri url, IHttpContextAccessor contextAccessor)
|
||||
{
|
||||
if(url == null)
|
||||
throw new ArgumentNullException(nameof(url));
|
||||
if(contextAccessor == null)
|
||||
throw new ArgumentNullException(nameof(contextAccessor));
|
||||
_ContextAccessor = contextAccessor;
|
||||
_Url = url.AbsoluteUri;
|
||||
}
|
||||
|
||||
public string GetAbsolute(string path)
|
||||
{
|
||||
var uri = new Uri(_Url, UriKind.Absolute);
|
||||
var builder = new UriBuilder()
|
||||
{
|
||||
Scheme = uri.Scheme,
|
||||
Host = uri.Host,
|
||||
};
|
||||
if(!uri.IsDefaultPort)
|
||||
builder.Port = uri.Port;
|
||||
return builder.Uri.AbsoluteUri + path;
|
||||
}
|
||||
|
||||
public string GetEncodedUrl()
|
||||
{
|
||||
var req = _ContextAccessor.HttpContext.Request;
|
||||
return BuildAbsolute(req.Path, req.QueryString); ;
|
||||
}
|
||||
|
||||
private string BuildAbsolute(PathString path = new PathString(),
|
||||
QueryString query = new QueryString(),
|
||||
FragmentString fragment = new FragmentString())
|
||||
{
|
||||
|
||||
var combinedPath = path.HasValue ? path.Value.Substring(1) : "";
|
||||
|
||||
var encodedQuery = query.ToString();
|
||||
var encodedFragment = fragment.ToString();
|
||||
|
||||
// PERF: Calculate string length to allocate correct buffer size for StringBuilder.
|
||||
var length = _Url.Length + combinedPath.Length + encodedQuery.Length + encodedFragment.Length;
|
||||
|
||||
return new StringBuilder(length)
|
||||
.Append(_Url)
|
||||
.Append(combinedPath)
|
||||
.Append(encodedQuery)
|
||||
.Append(encodedFragment)
|
||||
.ToString();
|
||||
}
|
||||
}
|
||||
}
|
76
BTCPayServer/Filters/OnlyMediaTypeAttribute.cs
Normal file
76
BTCPayServer/Filters/OnlyMediaTypeAttribute.cs
Normal file
|
@ -0,0 +1,76 @@
|
|||
using Microsoft.AspNetCore.Mvc.ActionConstraints;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace BTCPayServer.Filters
|
||||
{
|
||||
public class MediaTypeConstraintAttribute : Attribute, IActionConstraint
|
||||
{
|
||||
public MediaTypeConstraintAttribute(string mediaType)
|
||||
{
|
||||
MediaType = mediaType ?? throw new ArgumentNullException(nameof(mediaType));
|
||||
}
|
||||
|
||||
public string MediaType
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public int Order => 100;
|
||||
|
||||
public bool Accept(ActionConstraintContext context)
|
||||
{
|
||||
var match = context.RouteContext.HttpContext.Request.ContentType?.StartsWith(MediaType, StringComparison.Ordinal);
|
||||
return match.HasValue && match.Value;
|
||||
}
|
||||
}
|
||||
|
||||
public class BitpayAPIConstraintAttribute : Attribute, IActionConstraint
|
||||
{
|
||||
public BitpayAPIConstraintAttribute(bool isBitpayAPI = true)
|
||||
{
|
||||
IsBitpayAPI = isBitpayAPI;
|
||||
}
|
||||
|
||||
public bool IsBitpayAPI
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public int Order => 100;
|
||||
|
||||
public bool Accept(ActionConstraintContext context)
|
||||
{
|
||||
return context.RouteContext.HttpContext.Request.Headers["x-accept-version"].Where(h => h == "2.0.0").Any() == IsBitpayAPI;
|
||||
}
|
||||
}
|
||||
|
||||
public class AcceptMediaTypeConstraintAttribute : Attribute, IActionConstraint
|
||||
{
|
||||
public AcceptMediaTypeConstraintAttribute(string mediaType, bool expectedValue = true)
|
||||
{
|
||||
MediaType = mediaType ?? throw new ArgumentNullException(nameof(mediaType));
|
||||
ExpectedValue = expectedValue;
|
||||
}
|
||||
|
||||
public bool ExpectedValue
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public string MediaType
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public int Order => 100;
|
||||
|
||||
public bool Accept(ActionConstraintContext context)
|
||||
{
|
||||
var match = context.RouteContext.HttpContext.Request.Headers["Accept"].FirstOrDefault()?.StartsWith(MediaType, StringComparison.Ordinal);
|
||||
return (match.HasValue && match.Value) == ExpectedValue;
|
||||
}
|
||||
}
|
||||
}
|
92
BTCPayServer/Hosting/BTCPayServerServices.cs
Normal file
92
BTCPayServer/Hosting/BTCPayServerServices.cs
Normal file
|
@ -0,0 +1,92 @@
|
|||
using BTCPayServer.Configuration;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using BTCPayServer.Wallet;
|
||||
using BTCPayServer.RateProvider;
|
||||
using NBitpayClient;
|
||||
using NBitcoin;
|
||||
using BTCPayServer.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.IO;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using BTCPayServer.Invoicing;
|
||||
using NBXplorer;
|
||||
using BTCPayServer.Stores;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using BTCPayServer.Services;
|
||||
|
||||
namespace BTCPayServer.Hosting
|
||||
{
|
||||
public static class BTCPayServerServices
|
||||
{
|
||||
public static IWebHostBuilder AddPayServer(this IWebHostBuilder builder, BTCPayServerOptions options)
|
||||
{
|
||||
return
|
||||
builder
|
||||
.ConfigureServices(c =>
|
||||
{
|
||||
c.AddDbContext<ApplicationDbContext>(o =>
|
||||
{
|
||||
var path = Path.Combine(options.DataDir, "sqllite.db");
|
||||
o.UseSqlite("Data Source=" + path);
|
||||
});
|
||||
c.AddSingleton(options);
|
||||
c.AddSingleton<BTCPayServerRuntime>(o =>
|
||||
{
|
||||
var runtime = new BTCPayServerRuntime();
|
||||
runtime.Configure(options);
|
||||
return runtime;
|
||||
});
|
||||
c.AddSingleton<Network>(options.Network);
|
||||
c.AddSingleton(o => o.GetRequiredService<BTCPayServerRuntime>().TokenRepository);
|
||||
c.AddSingleton(o => o.GetRequiredService<BTCPayServerRuntime>().InvoiceRepository);
|
||||
c.AddSingleton<ApplicationDbContextFactory>(o => o.GetRequiredService<BTCPayServerRuntime>().DBFactory);
|
||||
c.AddSingleton<StoreRepository>();
|
||||
c.AddSingleton(o => o.GetRequiredService<BTCPayServerRuntime>().Wallet);
|
||||
c.AddSingleton<CurrencyNameTable>();
|
||||
c.AddSingleton<IFeeProvider>(o => new NBXplorerFeeProvider()
|
||||
{
|
||||
Fallback = new FeeRate(100, 1),
|
||||
BlockTarget = 20,
|
||||
ExplorerClient = o.GetRequiredService<ExplorerClient>()
|
||||
});
|
||||
c.AddSingleton<ExplorerClient>(o =>
|
||||
{
|
||||
var runtime = o.GetRequiredService<BTCPayServerRuntime>();
|
||||
return runtime.Explorer;
|
||||
});
|
||||
c.AddSingleton<Bitpay>(o =>
|
||||
{
|
||||
if(options.Network == Network.Main)
|
||||
return new Bitpay(new Key(), new Uri("https://bitpay.com/"));
|
||||
else
|
||||
return new Bitpay(new Key(), new Uri("https://test.bitpay.com/"));
|
||||
});
|
||||
c.TryAddSingleton<IRateProvider, BitpayRateProvider>();
|
||||
c.AddSingleton<InvoiceWatcher>();
|
||||
c.AddSingleton<IHostedService>(o => o.GetRequiredService<InvoiceWatcher>());
|
||||
c.AddScoped<IHttpContextAccessor, HttpContextAccessor>();
|
||||
c.AddSingleton<IExternalUrlProvider>(o => new FixedExternalUrlProvider(options.ExternalUrl, o.GetRequiredService<IHttpContextAccessor>()));
|
||||
})
|
||||
.UseUrls(options.GetUrls());
|
||||
}
|
||||
|
||||
public static IApplicationBuilder UsePayServer(this IApplicationBuilder app)
|
||||
{
|
||||
using(var scope = app.ApplicationServices.GetService<IServiceScopeFactory>().CreateScope())
|
||||
{
|
||||
scope.ServiceProvider.GetRequiredService<ApplicationDbContext>().Database.Migrate();
|
||||
}
|
||||
app.UseMiddleware<BTCPayMiddleware>();
|
||||
return app;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
102
BTCPayServer/Hosting/BTCpayMiddleware.cs
Normal file
102
BTCPayServer/Hosting/BTCpayMiddleware.cs
Normal file
|
@ -0,0 +1,102 @@
|
|||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using NBitcoin;
|
||||
using NBitcoin.Crypto;
|
||||
using NBitcoin.DataEncoders;
|
||||
using Microsoft.AspNetCore.Http.Internal;
|
||||
using System.IO;
|
||||
using BTCPayServer.Authentication;
|
||||
using System.Security.Principal;
|
||||
using NBitpayClient.Extensions;
|
||||
using BTCPayServer.Logging;
|
||||
using Newtonsoft.Json;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Configuration;
|
||||
|
||||
namespace BTCPayServer.Hosting
|
||||
{
|
||||
public class BTCPayMiddleware
|
||||
{
|
||||
TokenRepository _TokenRepository;
|
||||
RequestDelegate _Next;
|
||||
IExternalUrlProvider _ExternalUrl;
|
||||
public BTCPayMiddleware(RequestDelegate next, TokenRepository tokenRepo, IExternalUrlProvider externalUrl)
|
||||
{
|
||||
_ExternalUrl = externalUrl ?? throw new ArgumentNullException(nameof(externalUrl));
|
||||
_TokenRepository = tokenRepo ?? throw new ArgumentNullException(nameof(tokenRepo));
|
||||
_Next = next ?? throw new ArgumentNullException(nameof(next));
|
||||
}
|
||||
|
||||
public async Task Invoke(HttpContext httpContext)
|
||||
{
|
||||
httpContext.Request.Headers.TryGetValue("x-signature", out StringValues values);
|
||||
var sig = values.FirstOrDefault();
|
||||
httpContext.Request.Headers.TryGetValue("x-identity", out values);
|
||||
var id = values.FirstOrDefault();
|
||||
if(!string.IsNullOrEmpty(sig) && !string.IsNullOrEmpty(id))
|
||||
{
|
||||
httpContext.Request.EnableRewind();
|
||||
|
||||
string body = string.Empty;
|
||||
if(httpContext.Request.ContentLength != 0 && httpContext.Request.Body != null)
|
||||
{
|
||||
using(StreamReader reader = new StreamReader(httpContext.Request.Body, Encoding.UTF8, true, 1024, true))
|
||||
{
|
||||
body = reader.ReadToEnd();
|
||||
}
|
||||
httpContext.Request.Body.Position = 0;
|
||||
}
|
||||
|
||||
var url = _ExternalUrl.GetEncodedUrl();
|
||||
try
|
||||
{
|
||||
var key = new PubKey(id);
|
||||
if(BitIdExtensions.CheckBitIDSignature(key, sig, url, body))
|
||||
{
|
||||
var bitid = new BitIdentity(key);
|
||||
httpContext.User = new GenericPrincipal(bitid, new string[0]);
|
||||
Logs.PayServer.LogDebug($"BitId signature check success for SIN {bitid.SIN}");
|
||||
}
|
||||
}
|
||||
catch(FormatException) { }
|
||||
if(!(httpContext.User.Identity is BitIdentity))
|
||||
Logs.PayServer.LogDebug("BitId signature check failed");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await _Next(httpContext);
|
||||
}
|
||||
catch(UnauthorizedAccessException ex)
|
||||
{
|
||||
await HandleBitpayHttpException(httpContext, new BitpayHttpException(401, ex.Message));
|
||||
}
|
||||
catch(BitpayHttpException ex)
|
||||
{
|
||||
await HandleBitpayHttpException(httpContext, ex);
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
Logs.PayServer.LogCritical(new EventId(), ex, "Unhandled exception in BTCPayMiddleware");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task HandleBitpayHttpException(HttpContext httpContext, BitpayHttpException ex)
|
||||
{
|
||||
httpContext.Response.StatusCode = ex.StatusCode;
|
||||
using(var writer = new StreamWriter(httpContext.Response.Body, Encoding.UTF8, 1024, true))
|
||||
{
|
||||
var result = JsonConvert.SerializeObject(new BitpayErrorsModel(ex));
|
||||
writer.Write(result);
|
||||
await writer.FlushAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
70
BTCPayServer/Hosting/Startup.cs
Normal file
70
BTCPayServer/Hosting/Startup.cs
Normal file
|
@ -0,0 +1,70 @@
|
|||
using Microsoft.AspNetCore.Hosting;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NBitpayClient;
|
||||
using BTCPayServer.Authentication;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using BTCPayServer.Filters;
|
||||
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Models;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using BTCPayServer.Data;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using BTCPayServer.Logging;
|
||||
|
||||
namespace BTCPayServer.Hosting
|
||||
{
|
||||
public class Startup
|
||||
{
|
||||
public void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
|
||||
services.AddIdentity<ApplicationUser, IdentityRole>()
|
||||
.AddEntityFrameworkStores<ApplicationDbContext>()
|
||||
.AddDefaultTokenProviders();
|
||||
|
||||
// Add application services.
|
||||
services.AddTransient<IEmailSender, EmailSender>();
|
||||
|
||||
//services.AddSingleton<IObjectModelValidator, NoObjectModelValidator>();
|
||||
services.AddMvcCore(o =>
|
||||
{
|
||||
//o.Filters.Add(new NBXplorerExceptionFilter());
|
||||
o.OutputFormatters.Clear();
|
||||
o.InputFormatters.Clear();
|
||||
})
|
||||
.AddJsonFormatters()
|
||||
.AddFormatterMappings();
|
||||
services.AddMvc();
|
||||
}
|
||||
public void Configure(
|
||||
IApplicationBuilder app,
|
||||
IHostingEnvironment env,
|
||||
ILoggerFactory loggerFactory)
|
||||
{
|
||||
if(env.IsDevelopment())
|
||||
{
|
||||
app.UseDeveloperExceptionPage();
|
||||
app.UseBrowserLink();
|
||||
}
|
||||
Logs.Configure(loggerFactory);
|
||||
app.UsePayServer();
|
||||
app.UseStaticFiles();
|
||||
app.UseAuthentication();
|
||||
|
||||
app.UseMvc(routes =>
|
||||
{
|
||||
routes.MapRoute(
|
||||
name: "default",
|
||||
template: "{controller=Home}/{action=Index}/{id?}");
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
245
BTCPayServer/Invoicing/InvoiceEntity.cs
Normal file
245
BTCPayServer/Invoicing/InvoiceEntity.cs
Normal file
|
@ -0,0 +1,245 @@
|
|||
using NBitcoin;
|
||||
using System.Linq;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace BTCPayServer.Invoicing
|
||||
{
|
||||
public class BuyerInformation
|
||||
{
|
||||
[JsonProperty(PropertyName = "buyerName")]
|
||||
public string BuyerName
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
[JsonProperty(PropertyName = "buyerEmail")]
|
||||
public string BuyerEmail
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
[JsonProperty(PropertyName = "buyerCountry")]
|
||||
public string BuyerCountry
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
[JsonProperty(PropertyName = "buyerZip")]
|
||||
public string BuyerZip
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
[JsonProperty(PropertyName = "buyerState")]
|
||||
public string BuyerState
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
[JsonProperty(PropertyName = "buyerCity")]
|
||||
public string BuyerCity
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
[JsonProperty(PropertyName = "buyerAddress2")]
|
||||
public string BuyerAddress2
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
[JsonProperty(PropertyName = "buyerAddress1")]
|
||||
public string BuyerAddress1
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
[JsonProperty(PropertyName = "buyerPhone")]
|
||||
public string BuyerPhone
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
}
|
||||
|
||||
public class ProductInformation
|
||||
{
|
||||
[JsonProperty(PropertyName = "itemDesc")]
|
||||
public string ItemDesc
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
[JsonProperty(PropertyName = "itemCode")]
|
||||
public string ItemCode
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
[JsonProperty(PropertyName = "physical")]
|
||||
public bool Physical
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
[JsonProperty(PropertyName = "price")]
|
||||
public double Price
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
[JsonProperty(PropertyName = "currency")]
|
||||
public string Currency
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
}
|
||||
|
||||
public enum SpeedPolicy
|
||||
{
|
||||
HighSpeed = 0,
|
||||
MediumSpeed = 1,
|
||||
LowSpeed = 2
|
||||
}
|
||||
public class InvoiceEntity
|
||||
{
|
||||
public string Id
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public string StoreId
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public string OrderId
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public Money GetTotalCryptoDue()
|
||||
{
|
||||
return Calculate().TotalDue;
|
||||
}
|
||||
|
||||
private (Money TotalDue, Money Paid) Calculate()
|
||||
{
|
||||
var totalDue = Money.Coins((decimal)(ProductInformation.Price / Rate)) + TxFee;
|
||||
var paid = Money.Zero;
|
||||
var payments =
|
||||
Payments
|
||||
.OrderByDescending(p => p.ReceivedTime)
|
||||
.Select(_ =>
|
||||
{
|
||||
paid += _.Output.Value;
|
||||
return _;
|
||||
})
|
||||
.TakeWhile(_ =>
|
||||
{
|
||||
var paidEnough = totalDue <= paid;
|
||||
if(!paidEnough)
|
||||
totalDue += TxFee;
|
||||
return !paidEnough;
|
||||
})
|
||||
.ToArray();
|
||||
return (totalDue, paid);
|
||||
}
|
||||
|
||||
public Money GetTotalPaid()
|
||||
{
|
||||
return Calculate().Paid;
|
||||
}
|
||||
public Money GetCryptoDue()
|
||||
{
|
||||
var o = Calculate();
|
||||
var v = o.TotalDue - o.Paid;
|
||||
return v < Money.Zero ? Money.Zero : v;
|
||||
}
|
||||
|
||||
public SpeedPolicy SpeedPolicy
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public double Rate
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public DateTimeOffset InvoiceTime
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public DateTimeOffset ExpirationTime
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public BitcoinAddress DepositAddress
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public ProductInformation ProductInformation
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public BuyerInformation BuyerInformation
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public string PosData
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public string DerivationStrategy
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public string Status
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public string ExceptionStatus
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public List<PaymentEntity> Payments
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public bool Refundable
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public string RefundMail
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public string RedirectURL
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public Money TxFee
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public bool IsExpired()
|
||||
{
|
||||
return DateTimeOffset.UtcNow > ExpirationTime;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public class PaymentEntity
|
||||
{
|
||||
public DateTimeOffset ReceivedTime
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public OutPoint Outpoint
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public TxOut Output
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
}
|
||||
}
|
382
BTCPayServer/Invoicing/InvoiceRepository.cs
Normal file
382
BTCPayServer/Invoicing/InvoiceRepository.cs
Normal file
|
@ -0,0 +1,382 @@
|
|||
using DBreeze;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using NBitpayClient;
|
||||
using Newtonsoft.Json;
|
||||
using System.Linq;
|
||||
using NBitcoin;
|
||||
using NBitcoin.DataEncoders;
|
||||
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Query;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure.Internal;
|
||||
using BTCPayServer.Models;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using System.Globalization;
|
||||
using BTCPayServer.Models.InvoicingModels;
|
||||
|
||||
namespace BTCPayServer.Invoicing
|
||||
{
|
||||
public class InvoiceRepository
|
||||
{
|
||||
|
||||
|
||||
private readonly DBreezeEngine _Engine;
|
||||
public DBreezeEngine Engine
|
||||
{
|
||||
get
|
||||
{
|
||||
return _Engine;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Network _Network;
|
||||
public Network Network
|
||||
{
|
||||
get
|
||||
{
|
||||
return _Network;
|
||||
}
|
||||
set
|
||||
{
|
||||
_Network = value;
|
||||
}
|
||||
}
|
||||
|
||||
private ApplicationDbContextFactory _ContextFactory;
|
||||
public InvoiceRepository(ApplicationDbContextFactory contextFactory, DBreezeEngine engine, Network network)
|
||||
{
|
||||
_Engine = engine;
|
||||
_Network = network;
|
||||
_ContextFactory = contextFactory;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public Task AddPendingInvoice(string invoiceId)
|
||||
{
|
||||
using(var tx = _Engine.GetTransaction())
|
||||
{
|
||||
tx.Insert<string, byte[]>("T-Pending", invoiceId, new byte[0]);
|
||||
tx.Commit();
|
||||
}
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
|
||||
public Task RemovePendingInvoice(string invoiceId)
|
||||
{
|
||||
using(var tx = _Engine.GetTransaction())
|
||||
{
|
||||
tx.RemoveKey("T-Pending", invoiceId);
|
||||
tx.Commit();
|
||||
}
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
|
||||
|
||||
public string[] GetPendingInvoices()
|
||||
{
|
||||
List<string> pending = new List<string>();
|
||||
using(var tx = _Engine.GetTransaction())
|
||||
{
|
||||
foreach(var row in tx.SelectForward<string, byte[]>("T-Pending"))
|
||||
{
|
||||
pending.Add(row.Key);
|
||||
}
|
||||
}
|
||||
return pending.ToArray();
|
||||
}
|
||||
|
||||
public async Task UpdateInvoice(string invoiceId, UpdateCustomerModel data)
|
||||
{
|
||||
using(var ctx = _ContextFactory.CreateContext())
|
||||
{
|
||||
var invoiceData = await ctx.Invoices.FindAsync(invoiceId).ConfigureAwait(false);
|
||||
if(invoiceData == null)
|
||||
return;
|
||||
if(invoiceData.CustomerEmail == null && data.Email != null)
|
||||
{
|
||||
invoiceData.CustomerEmail = data.Email;
|
||||
}
|
||||
await ctx.SaveChangesAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<InvoiceEntity> CreateInvoiceAsync(string storeId, InvoiceEntity invoice)
|
||||
{
|
||||
invoice = Clone(invoice);
|
||||
invoice.Id = Encoders.Base58.EncodeData(RandomUtils.GetBytes(16));
|
||||
invoice.Payments = new List<PaymentEntity>();
|
||||
invoice.StoreId = storeId;
|
||||
using(var context = _ContextFactory.CreateContext())
|
||||
{
|
||||
await context.AddAsync(new InvoiceData()
|
||||
{
|
||||
StoreDataId = storeId,
|
||||
Id = invoice.Id,
|
||||
Created = invoice.InvoiceTime,
|
||||
Blob = ToBytes(invoice),
|
||||
OrderId = invoice.OrderId,
|
||||
Status = invoice.Status,
|
||||
ItemCode = invoice.ProductInformation.ItemCode,
|
||||
CustomerEmail = invoice.RefundMail
|
||||
}).ConfigureAwait(false);
|
||||
await context.SaveChangesAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
AddToTextSearch(invoice.Id,
|
||||
invoice.Id,
|
||||
invoice.InvoiceTime.ToString(CultureInfo.InvariantCulture),
|
||||
invoice.ProductInformation.Price.ToString(CultureInfo.InvariantCulture),
|
||||
invoice.GetTotalCryptoDue().ToString(),
|
||||
invoice.OrderId,
|
||||
ToString(invoice.BuyerInformation),
|
||||
ToString(invoice.ProductInformation),
|
||||
invoice.StoreId
|
||||
);
|
||||
|
||||
return invoice;
|
||||
}
|
||||
|
||||
|
||||
private string[] SearchInvoice(string searchTerms)
|
||||
{
|
||||
using(var tx = _Engine.GetTransaction())
|
||||
{
|
||||
return tx.TextSearch("InvoiceSearch").Block(searchTerms)
|
||||
.GetDocumentIDs()
|
||||
.Select(id => Encoders.Base58.EncodeData(id))
|
||||
.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
void AddToTextSearch(string invoiceId, params string[] terms)
|
||||
{
|
||||
using(var tx = _Engine.GetTransaction())
|
||||
{
|
||||
tx.TextInsert("InvoiceSearch", Encoders.Base58.DecodeData(invoiceId), string.Join(" ", terms.Where(t => !String.IsNullOrWhiteSpace(t))));
|
||||
tx.Commit();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task UpdateInvoiceStatus(string invoiceId, string status, string exceptionStatus)
|
||||
{
|
||||
using(var context = _ContextFactory.CreateContext())
|
||||
{
|
||||
var invoiceData = await context.FindAsync<InvoiceData>(invoiceId).ConfigureAwait(false);
|
||||
if(invoiceData == null)
|
||||
return;
|
||||
invoiceData.Status = status;
|
||||
invoiceData.ExceptionStatus = exceptionStatus;
|
||||
await context.SaveChangesAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<InvoiceEntity> GetInvoice(string storeId, string id)
|
||||
{
|
||||
using(var context = _ContextFactory.CreateContext())
|
||||
{
|
||||
IQueryable<InvoiceData> query =
|
||||
context
|
||||
.Invoices
|
||||
.Include(o => o.Payments)
|
||||
.Include(o => o.RefundAddresses)
|
||||
.Where(i => i.Id == id);
|
||||
|
||||
if(storeId != null)
|
||||
query = query.Where(i => i.StoreDataId == storeId);
|
||||
|
||||
var invoice = await query.FirstOrDefaultAsync().ConfigureAwait(false);
|
||||
if(invoice == null)
|
||||
return null;
|
||||
|
||||
return ToEntity(invoice);
|
||||
}
|
||||
}
|
||||
|
||||
private InvoiceEntity ToEntity(InvoiceData invoice)
|
||||
{
|
||||
var entity = ToObject<InvoiceEntity>(invoice.Blob);
|
||||
entity.Payments = invoice.Payments.Select(p => ToObject<PaymentEntity>(p.Blob)).ToList();
|
||||
entity.ExceptionStatus = invoice.ExceptionStatus;
|
||||
entity.Status = invoice.Status;
|
||||
entity.RefundMail = invoice.CustomerEmail;
|
||||
entity.Refundable = invoice.RefundAddresses.Count != 0;
|
||||
return entity;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public async Task<InvoiceEntity[]> GetInvoices(InvoiceQuery queryObject)
|
||||
{
|
||||
using(var context = _ContextFactory.CreateContext())
|
||||
{
|
||||
|
||||
IQueryable<InvoiceData> query = context
|
||||
.Invoices
|
||||
.Include(o => o.Payments)
|
||||
.Include(o => o.RefundAddresses);
|
||||
|
||||
if(!string.IsNullOrEmpty(queryObject.StoreId))
|
||||
{
|
||||
query = query.Where(i => i.StoreDataId == queryObject.StoreId);
|
||||
}
|
||||
|
||||
if(!string.IsNullOrEmpty(queryObject.TextSearch))
|
||||
{
|
||||
var ids = new HashSet<string>(SearchInvoice(queryObject.TextSearch));
|
||||
if(ids.Count == 0)
|
||||
return new InvoiceEntity[0];
|
||||
query = query.Where(i => ids.Contains(i.Id));
|
||||
}
|
||||
|
||||
if(queryObject.StartDate != null)
|
||||
query = query.Where(i => queryObject.StartDate.Value <= i.Created);
|
||||
|
||||
if(queryObject.EndDate != null)
|
||||
query = query.Where(i => i.Created <= queryObject.EndDate.Value);
|
||||
|
||||
if(queryObject.ItemCode != null)
|
||||
query = query.Where(i => i.ItemCode == queryObject.ItemCode);
|
||||
|
||||
if(queryObject.OrderId != null)
|
||||
query = query.Where(i => i.OrderId == queryObject.OrderId);
|
||||
|
||||
if(queryObject.Status != null)
|
||||
query = query.Where(i => i.Status == queryObject.Status);
|
||||
|
||||
query = query.OrderByDescending(q => q.Created);
|
||||
|
||||
if(queryObject.Skip != null)
|
||||
query = query.Skip(queryObject.Skip.Value);
|
||||
|
||||
if(queryObject.Count != null)
|
||||
query = query.Take(queryObject.Count.Value);
|
||||
|
||||
var data = await query.ToArrayAsync().ConfigureAwait(false);
|
||||
|
||||
return data.Select(ToEntity).ToArray();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public async Task AddRefundsAsync(string invoiceId, TxOut[] outputs)
|
||||
{
|
||||
if(outputs.Length == 0)
|
||||
return;
|
||||
outputs = outputs.Take(10).ToArray();
|
||||
using(var context = _ContextFactory.CreateContext())
|
||||
{
|
||||
int i = 0;
|
||||
foreach(var output in outputs)
|
||||
{
|
||||
await context.RefundAddresses.AddAsync(new RefundAddressesData()
|
||||
{
|
||||
Id = invoiceId + "-" + i,
|
||||
InvoiceDataId = invoiceId,
|
||||
Blob = ToBytes(output)
|
||||
}).ConfigureAwait(false);
|
||||
i++;
|
||||
}
|
||||
await context.SaveChangesAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
var addresses = outputs.Select(o => o.ScriptPubKey.GetDestinationAddress(_Network)).Where(a => a != null).ToArray();
|
||||
AddToTextSearch(invoiceId, addresses.Select(a => a.ToString()).ToArray());
|
||||
}
|
||||
|
||||
public async Task<PaymentEntity> AddPayment(string invoiceId, Coin receivedCoin)
|
||||
{
|
||||
using(var context = _ContextFactory.CreateContext())
|
||||
{
|
||||
PaymentEntity entity = new PaymentEntity
|
||||
{
|
||||
Outpoint = receivedCoin.Outpoint,
|
||||
Output = receivedCoin.TxOut,
|
||||
ReceivedTime = DateTime.UtcNow
|
||||
};
|
||||
|
||||
PaymentData data = new PaymentData
|
||||
{
|
||||
Id = receivedCoin.Outpoint.ToString(),
|
||||
Blob = ToBytes(entity),
|
||||
InvoiceDataId = invoiceId
|
||||
};
|
||||
|
||||
await context.Payments.AddAsync(data).ConfigureAwait(false);
|
||||
|
||||
await context.SaveChangesAsync().ConfigureAwait(false);
|
||||
return entity;
|
||||
}
|
||||
}
|
||||
|
||||
private T ToObject<T>(byte[] value)
|
||||
{
|
||||
return NBitcoin.JsonConverters.Serializer.ToObject<T>(ZipUtils.Unzip(value), Network);
|
||||
}
|
||||
|
||||
private byte[] ToBytes<T>(T obj)
|
||||
{
|
||||
return ZipUtils.Zip(NBitcoin.JsonConverters.Serializer.ToString(obj));
|
||||
}
|
||||
|
||||
private T Clone<T>(T invoice)
|
||||
{
|
||||
return NBitcoin.JsonConverters.Serializer.ToObject<T>(ToString(invoice), Network);
|
||||
}
|
||||
|
||||
private string ToString<T>(T data)
|
||||
{
|
||||
return NBitcoin.JsonConverters.Serializer.ToString(data, Network);
|
||||
}
|
||||
}
|
||||
|
||||
public class InvoiceQuery
|
||||
{
|
||||
public string StoreId
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public string TextSearch
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public DateTimeOffset? StartDate
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public DateTimeOffset? EndDate
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public int? Skip
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public int? Count
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public string OrderId
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public string ItemCode
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public string Status
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
}
|
||||
}
|
270
BTCPayServer/Invoicing/InvoiceWatcher.cs
Normal file
270
BTCPayServer/Invoicing/InvoiceWatcher.cs
Normal file
|
@ -0,0 +1,270 @@
|
|||
using NBXplorer;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
using NBXplorer.Models;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using NBitcoin;
|
||||
using BTCPayServer.Logging;
|
||||
using System.Threading;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace BTCPayServer.Invoicing
|
||||
{
|
||||
public class InvoiceWatcher : IHostedService
|
||||
{
|
||||
InvoiceRepository _InvoiceRepository;
|
||||
ExplorerClient _ExplorerClient;
|
||||
DerivationStrategyFactory _DerivationFactory;
|
||||
|
||||
public InvoiceWatcher(ExplorerClient explorerClient, InvoiceRepository invoiceRepository)
|
||||
{
|
||||
_ExplorerClient = explorerClient ?? throw new ArgumentNullException(nameof(explorerClient));
|
||||
_DerivationFactory = new DerivationStrategyFactory(_ExplorerClient.Network);
|
||||
_InvoiceRepository = invoiceRepository ?? throw new ArgumentNullException(nameof(invoiceRepository));
|
||||
}
|
||||
|
||||
private async Task StartWatchInvoice(string invoiceId)
|
||||
{
|
||||
Logs.PayServer.LogInformation("Watching invoice " + invoiceId);
|
||||
UTXOChanges changes = null;
|
||||
while(true)
|
||||
{
|
||||
try
|
||||
{
|
||||
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId).ConfigureAwait(false);
|
||||
if(invoice == null)
|
||||
break;
|
||||
var stateBefore = invoice.Status;
|
||||
var result = await UpdateInvoice(changes, invoice).ConfigureAwait(false);
|
||||
changes = result.Changes;
|
||||
if(result.NeedSave)
|
||||
await _InvoiceRepository.UpdateInvoiceStatus(invoice.Id, invoice.Status, invoice.ExceptionStatus).ConfigureAwait(false);
|
||||
|
||||
if(stateBefore != invoice.Status)
|
||||
{
|
||||
Logs.PayServer.LogInformation($"Invoice {invoice.Id}: {stateBefore} => {invoice.Status}");
|
||||
}
|
||||
|
||||
if(invoice.Status == "complete" || invoice.Status == "invalid")
|
||||
{
|
||||
await _InvoiceRepository.RemovePendingInvoice(invoice.Id).ConfigureAwait(false);
|
||||
Logs.PayServer.LogInformation("Stopped watching invoice " + invoiceId);
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch(OperationCanceledException) when(_Cts.Token.IsCancellationRequested)
|
||||
{
|
||||
break;
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
Logs.PayServer.LogError(ex, "Unhandled error on watching invoice " + invoiceId);
|
||||
await Task.Delay(10000, _Cts.Token).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private async Task<(bool NeedSave, UTXOChanges Changes)> UpdateInvoice(UTXOChanges changes, InvoiceEntity invoice)
|
||||
{
|
||||
if(invoice.Status == "invalid" && (invoice.Status == "new" || invoice.Status == "paidPartial"))
|
||||
{
|
||||
return (false, changes);
|
||||
}
|
||||
bool needSave = false;
|
||||
bool shouldWait = true;
|
||||
|
||||
if(invoice.ExpirationTime < DateTimeOffset.UtcNow)
|
||||
{
|
||||
needSave = true;
|
||||
invoice.Status = "invalid";
|
||||
}
|
||||
|
||||
if(invoice.Status == "new" || invoice.Status == "paidPartial")
|
||||
{
|
||||
var strategy = _DerivationFactory.Parse(invoice.DerivationStrategy);
|
||||
changes = await _ExplorerClient.SyncAsync(strategy, changes, false, _Cts.Token).ConfigureAwait(false);
|
||||
shouldWait = false; //should not wait, Sync is blocking call
|
||||
|
||||
List<Coin> receivedCoins = new List<Coin>();
|
||||
foreach(var received in changes.Confirmed.UTXOs.Concat(changes.Unconfirmed.UTXOs))
|
||||
if(received.Output.ScriptPubKey == invoice.DepositAddress.ScriptPubKey)
|
||||
receivedCoins.Add(new Coin(received.Outpoint, received.Output));
|
||||
|
||||
var alreadyAccounted = new HashSet<OutPoint>(invoice.Payments.Select(p => p.Outpoint));
|
||||
foreach(var coin in receivedCoins.Where(c => !alreadyAccounted.Contains(c.Outpoint)))
|
||||
{
|
||||
var payment = await _InvoiceRepository.AddPayment(invoice.Id, coin).ConfigureAwait(false);
|
||||
invoice.Payments.Add(payment);
|
||||
if(invoice.Status == "new")
|
||||
{
|
||||
invoice.Status = "paidPartial";
|
||||
needSave = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(invoice.Status == "paidPartial")
|
||||
{
|
||||
var totalPaid = invoice.Payments.Select(p => p.Output.Value).Sum();
|
||||
if(totalPaid == invoice.GetTotalCryptoDue())
|
||||
{
|
||||
invoice.Status = "paid";
|
||||
invoice.ExceptionStatus = null;
|
||||
needSave = true;
|
||||
}
|
||||
|
||||
if(totalPaid > invoice.GetTotalCryptoDue())
|
||||
{
|
||||
invoice.Status = "paidOver";
|
||||
invoice.ExceptionStatus = "paidOver";
|
||||
needSave = true;
|
||||
}
|
||||
|
||||
if(totalPaid < invoice.GetTotalCryptoDue() && invoice.ExceptionStatus == null)
|
||||
{
|
||||
invoice.ExceptionStatus = "paidPartial";
|
||||
needSave = true;
|
||||
}
|
||||
}
|
||||
|
||||
if(invoice.Status == "paid" || invoice.Status == "paidOver")
|
||||
{
|
||||
var getTransactions = invoice.Payments.Select(o => o.Outpoint.Hash).Select(o => _ExplorerClient.GetTransactionAsync(o, _Cts.Token)).ToArray();
|
||||
await Task.WhenAll(getTransactions).ConfigureAwait(false);
|
||||
var transactions = getTransactions.Select(c => c.GetAwaiter().GetResult()).ToArray();
|
||||
|
||||
bool confirmed = false;
|
||||
var minConf = transactions.Select(t => t.Confirmations).Min();
|
||||
if(invoice.SpeedPolicy == SpeedPolicy.HighSpeed)
|
||||
{
|
||||
if(minConf > 0)
|
||||
confirmed = true;
|
||||
else
|
||||
confirmed = !transactions.Any(t => t.Transaction.RBF);
|
||||
}
|
||||
else if(invoice.SpeedPolicy == SpeedPolicy.MediumSpeed)
|
||||
{
|
||||
confirmed = minConf >= 1;
|
||||
}
|
||||
else if(invoice.SpeedPolicy == SpeedPolicy.LowSpeed)
|
||||
{
|
||||
confirmed = minConf >= 6;
|
||||
}
|
||||
|
||||
if(confirmed)
|
||||
{
|
||||
invoice.Status = "confirmed";
|
||||
needSave = true;
|
||||
}
|
||||
}
|
||||
|
||||
if(invoice.Status == "confirmed")
|
||||
{
|
||||
var getTransactions = invoice.Payments.Select(o => o.Outpoint.Hash).Select(o => _ExplorerClient.GetTransactionAsync(o, _Cts.Token)).ToArray();
|
||||
await Task.WhenAll(getTransactions).ConfigureAwait(false);
|
||||
var transactions = getTransactions.Select(c => c.GetAwaiter().GetResult()).ToArray();
|
||||
var minConf = transactions.Select(t => t.Confirmations).Min();
|
||||
if(minConf >= 6)
|
||||
{
|
||||
invoice.Status = "complete";
|
||||
needSave = true;
|
||||
}
|
||||
}
|
||||
|
||||
shouldWait = shouldWait && !needSave;
|
||||
|
||||
if(shouldWait)
|
||||
{
|
||||
await Task.Delay(PollInterval, _Cts.Token).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return (needSave, changes);
|
||||
}
|
||||
|
||||
public TimeSpan PollInterval
|
||||
{
|
||||
get; set;
|
||||
} = TimeSpan.FromSeconds(10);
|
||||
|
||||
public async Task WatchAsync(string invoiceId)
|
||||
{
|
||||
await _InvoiceRepository.AddPendingInvoice(invoiceId).ConfigureAwait(false);
|
||||
_WatchRequests.Add(invoiceId);
|
||||
}
|
||||
|
||||
BlockingCollection<string> _WatchRequests = new BlockingCollection<string>(new ConcurrentQueue<string>());
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_Cts.Cancel();
|
||||
}
|
||||
|
||||
|
||||
Thread _Thread;
|
||||
TaskCompletionSource<bool> _RunningTask;
|
||||
CancellationTokenSource _Cts;
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
foreach(var pending in _InvoiceRepository.GetPendingInvoices())
|
||||
{
|
||||
_WatchRequests.Add(pending);
|
||||
}
|
||||
_RunningTask = new TaskCompletionSource<bool>();
|
||||
_Cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
_Thread = new Thread(Run) { Name = "InvoiceWatcher" };
|
||||
_Thread.Start();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
void Run()
|
||||
{
|
||||
Logs.PayServer.LogInformation("Start watching invoices");
|
||||
List<Task> watching = new List<Task>();
|
||||
try
|
||||
{
|
||||
foreach(var item in _WatchRequests.GetConsumingEnumerable(_Cts.Token))
|
||||
{
|
||||
watching.Add(StartWatchInvoice(item));
|
||||
foreach(var task in watching.ToList())
|
||||
{
|
||||
if(task.Status != TaskStatus.Running)
|
||||
{
|
||||
watching.Remove(task);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(OperationCanceledException) when(_Cts.Token.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
Task.WaitAll(watching.ToArray());
|
||||
}
|
||||
catch(AggregateException) { }
|
||||
_RunningTask.TrySetResult(true);
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
_Cts.Cancel();
|
||||
_RunningTask.TrySetException(ex);
|
||||
Logs.PayServer.LogCritical(ex, "Error in the InvoiceWatcher loop");
|
||||
}
|
||||
finally
|
||||
{
|
||||
Logs.PayServer.LogInformation("Stop watching invoices");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_Cts.Cancel();
|
||||
return Task.WhenAny(_RunningTask.Task, Task.Delay(-1, cancellationToken));
|
||||
}
|
||||
}
|
||||
}
|
400
BTCPayServer/Logging/ConsoleLogger.cs
Normal file
400
BTCPayServer/Logging/ConsoleLogger.cs
Normal file
|
@ -0,0 +1,400 @@
|
|||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Console;
|
||||
using Microsoft.Extensions.Logging.Console.Internal;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Logging
|
||||
{
|
||||
public class CustomConsoleLogProvider : ILoggerProvider
|
||||
{
|
||||
ConsoleLoggerProcessor _Processor = new ConsoleLoggerProcessor();
|
||||
public ILogger CreateLogger(string categoryName)
|
||||
{
|
||||
return new CustomConsoleLogger(categoryName, (a,b) => true, false, _Processor);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A variant of ASP.NET Core ConsoleLogger which does not make new line for the category
|
||||
/// </summary>
|
||||
public class CustomConsoleLogger : ILogger
|
||||
{
|
||||
private static readonly string _loglevelPadding = ": ";
|
||||
private static readonly string _messagePadding;
|
||||
private static readonly string _newLineWithMessagePadding;
|
||||
|
||||
// ConsoleColor does not have a value to specify the 'Default' color
|
||||
private readonly ConsoleColor? DefaultConsoleColor = null;
|
||||
|
||||
private readonly ConsoleLoggerProcessor _queueProcessor;
|
||||
private Func<string, LogLevel, bool> _filter;
|
||||
|
||||
[ThreadStatic]
|
||||
private static StringBuilder _logBuilder;
|
||||
|
||||
static CustomConsoleLogger()
|
||||
{
|
||||
var logLevelString = GetLogLevelString(LogLevel.Information);
|
||||
_messagePadding = new string(' ', logLevelString.Length + _loglevelPadding.Length);
|
||||
_newLineWithMessagePadding = Environment.NewLine + _messagePadding;
|
||||
}
|
||||
|
||||
public CustomConsoleLogger(string name, Func<string, LogLevel, bool> filter, bool includeScopes, ConsoleLoggerProcessor loggerProcessor)
|
||||
{
|
||||
Name = name ?? throw new ArgumentNullException(nameof(name));
|
||||
Filter = filter ?? ((category, logLevel) => true);
|
||||
IncludeScopes = includeScopes;
|
||||
|
||||
_queueProcessor = loggerProcessor;
|
||||
|
||||
if(RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
Console = new WindowsLogConsole();
|
||||
}
|
||||
else
|
||||
{
|
||||
Console = new AnsiLogConsole(new AnsiSystemConsole());
|
||||
}
|
||||
}
|
||||
|
||||
public IConsole Console
|
||||
{
|
||||
get
|
||||
{
|
||||
return _queueProcessor.Console;
|
||||
}
|
||||
set
|
||||
{
|
||||
_queueProcessor.Console = value ?? throw new ArgumentNullException(nameof(value));
|
||||
}
|
||||
}
|
||||
|
||||
public Func<string, LogLevel, bool> Filter
|
||||
{
|
||||
get
|
||||
{
|
||||
return _filter;
|
||||
}
|
||||
set
|
||||
{
|
||||
_filter = value ?? throw new ArgumentNullException(nameof(value));
|
||||
}
|
||||
}
|
||||
|
||||
public bool IncludeScopes
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public string Name
|
||||
{
|
||||
get;
|
||||
}
|
||||
|
||||
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
|
||||
{
|
||||
if(!IsEnabled(logLevel))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if(formatter == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(formatter));
|
||||
}
|
||||
|
||||
var message = formatter(state, exception);
|
||||
|
||||
if(!string.IsNullOrEmpty(message) || exception != null)
|
||||
{
|
||||
WriteMessage(logLevel, Name, eventId.Id, message, exception);
|
||||
}
|
||||
}
|
||||
|
||||
public virtual void WriteMessage(LogLevel logLevel, string logName, int eventId, string message, Exception exception)
|
||||
{
|
||||
var logBuilder = _logBuilder;
|
||||
_logBuilder = null;
|
||||
|
||||
if(logBuilder == null)
|
||||
{
|
||||
logBuilder = new StringBuilder();
|
||||
}
|
||||
|
||||
var logLevelColors = default(ConsoleColors);
|
||||
var logLevelString = string.Empty;
|
||||
|
||||
// Example:
|
||||
// INFO: ConsoleApp.Program[10]
|
||||
// Request received
|
||||
|
||||
logLevelColors = GetLogLevelConsoleColors(logLevel);
|
||||
logLevelString = GetLogLevelString(logLevel);
|
||||
// category and event id
|
||||
var lenBefore = logBuilder.ToString().Length;
|
||||
logBuilder.Append(_loglevelPadding);
|
||||
logBuilder.Append(logName);
|
||||
logBuilder.Append(": ");
|
||||
var lenAfter = logBuilder.ToString().Length;
|
||||
while(lenAfter++ < 18)
|
||||
logBuilder.Append(" ");
|
||||
// scope information
|
||||
if(IncludeScopes)
|
||||
{
|
||||
GetScopeInformation(logBuilder);
|
||||
}
|
||||
|
||||
if(!string.IsNullOrEmpty(message))
|
||||
{
|
||||
// message
|
||||
//logBuilder.Append(_messagePadding);
|
||||
|
||||
var len = logBuilder.Length;
|
||||
logBuilder.AppendLine(message);
|
||||
logBuilder.Replace(Environment.NewLine, _newLineWithMessagePadding, len, message.Length);
|
||||
}
|
||||
|
||||
// Example:
|
||||
// System.InvalidOperationException
|
||||
// at Namespace.Class.Function() in File:line X
|
||||
if(exception != null)
|
||||
{
|
||||
// exception message
|
||||
logBuilder.AppendLine(exception.ToString());
|
||||
}
|
||||
|
||||
if(logBuilder.Length > 0)
|
||||
{
|
||||
var hasLevel = !string.IsNullOrEmpty(logLevelString);
|
||||
// Queue log message
|
||||
_queueProcessor.EnqueueMessage(new LogMessageEntry()
|
||||
{
|
||||
Message = logBuilder.ToString(),
|
||||
MessageColor = DefaultConsoleColor,
|
||||
LevelString = hasLevel ? logLevelString : null,
|
||||
LevelBackground = hasLevel ? logLevelColors.Background : null,
|
||||
LevelForeground = hasLevel ? logLevelColors.Foreground : null
|
||||
});
|
||||
}
|
||||
|
||||
logBuilder.Clear();
|
||||
if(logBuilder.Capacity > 1024)
|
||||
{
|
||||
logBuilder.Capacity = 1024;
|
||||
}
|
||||
_logBuilder = logBuilder;
|
||||
}
|
||||
|
||||
public bool IsEnabled(LogLevel logLevel)
|
||||
{
|
||||
return Filter(Name, logLevel);
|
||||
}
|
||||
|
||||
public IDisposable BeginScope<TState>(TState state)
|
||||
{
|
||||
if(state == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(state));
|
||||
}
|
||||
|
||||
return ConsoleLogScope.Push(Name, state);
|
||||
}
|
||||
|
||||
private static string GetLogLevelString(LogLevel logLevel)
|
||||
{
|
||||
switch(logLevel)
|
||||
{
|
||||
case LogLevel.Trace:
|
||||
return "trce";
|
||||
case LogLevel.Debug:
|
||||
return "dbug";
|
||||
case LogLevel.Information:
|
||||
return "info";
|
||||
case LogLevel.Warning:
|
||||
return "warn";
|
||||
case LogLevel.Error:
|
||||
return "fail";
|
||||
case LogLevel.Critical:
|
||||
return "crit";
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(logLevel));
|
||||
}
|
||||
}
|
||||
|
||||
private ConsoleColors GetLogLevelConsoleColors(LogLevel logLevel)
|
||||
{
|
||||
// We must explicitly set the background color if we are setting the foreground color,
|
||||
// since just setting one can look bad on the users console.
|
||||
switch(logLevel)
|
||||
{
|
||||
case LogLevel.Critical:
|
||||
return new ConsoleColors(ConsoleColor.White, ConsoleColor.Red);
|
||||
case LogLevel.Error:
|
||||
return new ConsoleColors(ConsoleColor.Black, ConsoleColor.Red);
|
||||
case LogLevel.Warning:
|
||||
return new ConsoleColors(ConsoleColor.Yellow, ConsoleColor.Black);
|
||||
case LogLevel.Information:
|
||||
return new ConsoleColors(ConsoleColor.DarkGreen, ConsoleColor.Black);
|
||||
case LogLevel.Debug:
|
||||
return new ConsoleColors(ConsoleColor.Gray, ConsoleColor.Black);
|
||||
case LogLevel.Trace:
|
||||
return new ConsoleColors(ConsoleColor.Gray, ConsoleColor.Black);
|
||||
default:
|
||||
return new ConsoleColors(DefaultConsoleColor, DefaultConsoleColor);
|
||||
}
|
||||
}
|
||||
|
||||
private void GetScopeInformation(StringBuilder builder)
|
||||
{
|
||||
var current = ConsoleLogScope.Current;
|
||||
string scopeLog = string.Empty;
|
||||
var length = builder.Length;
|
||||
|
||||
while(current != null)
|
||||
{
|
||||
if(length == builder.Length)
|
||||
{
|
||||
scopeLog = $"=> {current}";
|
||||
}
|
||||
else
|
||||
{
|
||||
scopeLog = $"=> {current} ";
|
||||
}
|
||||
|
||||
builder.Insert(length, scopeLog);
|
||||
current = current.Parent;
|
||||
}
|
||||
if(builder.Length > length)
|
||||
{
|
||||
builder.Insert(length, _messagePadding);
|
||||
builder.AppendLine();
|
||||
}
|
||||
}
|
||||
|
||||
private struct ConsoleColors
|
||||
{
|
||||
public ConsoleColors(ConsoleColor? foreground, ConsoleColor? background)
|
||||
{
|
||||
Foreground = foreground;
|
||||
Background = background;
|
||||
}
|
||||
|
||||
public ConsoleColor? Foreground
|
||||
{
|
||||
get;
|
||||
}
|
||||
|
||||
public ConsoleColor? Background
|
||||
{
|
||||
get;
|
||||
}
|
||||
}
|
||||
|
||||
private class AnsiSystemConsole : IAnsiSystemConsole
|
||||
{
|
||||
public void Write(string message)
|
||||
{
|
||||
System.Console.Write(message);
|
||||
}
|
||||
|
||||
public void WriteLine(string message)
|
||||
{
|
||||
System.Console.WriteLine(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class ConsoleLoggerProcessor : IDisposable
|
||||
{
|
||||
private const int _maxQueuedMessages = 1024;
|
||||
|
||||
private readonly BlockingCollection<LogMessageEntry> _messageQueue = new BlockingCollection<LogMessageEntry>(_maxQueuedMessages);
|
||||
private readonly Task _outputTask;
|
||||
|
||||
public IConsole Console;
|
||||
|
||||
public ConsoleLoggerProcessor()
|
||||
{
|
||||
// Start Console message queue processor
|
||||
_outputTask = Task.Factory.StartNew(
|
||||
ProcessLogQueue,
|
||||
this,
|
||||
TaskCreationOptions.LongRunning);
|
||||
}
|
||||
|
||||
public virtual void EnqueueMessage(LogMessageEntry message)
|
||||
{
|
||||
if(!_messageQueue.IsAddingCompleted)
|
||||
{
|
||||
try
|
||||
{
|
||||
_messageQueue.Add(message);
|
||||
return;
|
||||
}
|
||||
catch(InvalidOperationException) { }
|
||||
}
|
||||
|
||||
// Adding is completed so just log the message
|
||||
WriteMessage(message);
|
||||
}
|
||||
|
||||
// for testing
|
||||
internal virtual void WriteMessage(LogMessageEntry message)
|
||||
{
|
||||
if(message.LevelString != null)
|
||||
{
|
||||
Console.Write(message.LevelString, message.LevelBackground, message.LevelForeground);
|
||||
}
|
||||
|
||||
Console.Write(message.Message, message.MessageColor, message.MessageColor);
|
||||
Console.Flush();
|
||||
}
|
||||
|
||||
private void ProcessLogQueue()
|
||||
{
|
||||
foreach(var message in _messageQueue.GetConsumingEnumerable())
|
||||
{
|
||||
WriteMessage(message);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ProcessLogQueue(object state)
|
||||
{
|
||||
var consoleLogger = (ConsoleLoggerProcessor)state;
|
||||
|
||||
consoleLogger.ProcessLogQueue();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_messageQueue.CompleteAdding();
|
||||
|
||||
try
|
||||
{
|
||||
_outputTask.Wait(1500); // with timeout in-case Console is locked by user input
|
||||
}
|
||||
catch(TaskCanceledException) { }
|
||||
catch(AggregateException ex) when(ex.InnerExceptions.Count == 1 && ex.InnerExceptions[0] is TaskCanceledException) { }
|
||||
}
|
||||
}
|
||||
|
||||
public struct LogMessageEntry
|
||||
{
|
||||
public string LevelString;
|
||||
public ConsoleColor? LevelBackground;
|
||||
public ConsoleColor? LevelForeground;
|
||||
public ConsoleColor? MessageColor;
|
||||
public string Message;
|
||||
}
|
||||
|
||||
}
|
54
BTCPayServer/Logging/Logs.cs
Normal file
54
BTCPayServer/Logging/Logs.cs
Normal file
|
@ -0,0 +1,54 @@
|
|||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Logging
|
||||
{
|
||||
public class Logs
|
||||
{
|
||||
static Logs()
|
||||
{
|
||||
Configure(new FuncLoggerFactory(n => NullLogger.Instance));
|
||||
}
|
||||
public static void Configure(ILoggerFactory factory)
|
||||
{
|
||||
Configuration = factory.CreateLogger("Configuration");
|
||||
PayServer = factory.CreateLogger("PayServer");
|
||||
}
|
||||
public static ILogger Configuration
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public static ILogger PayServer
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public const int ColumnLength = 16;
|
||||
}
|
||||
|
||||
public class FuncLoggerFactory : ILoggerFactory
|
||||
{
|
||||
private Func<string, ILogger> createLogger;
|
||||
public FuncLoggerFactory(Func<string, ILogger> createLogger)
|
||||
{
|
||||
this.createLogger = createLogger;
|
||||
}
|
||||
public void AddProvider(ILoggerProvider provider)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public ILogger CreateLogger(string categoryName)
|
||||
{
|
||||
return createLogger(categoryName);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
355
BTCPayServer/Migrations/20170901023716_Init.Designer.cs
generated
Normal file
355
BTCPayServer/Migrations/20170901023716_Init.Designer.cs
generated
Normal file
|
@ -0,0 +1,355 @@
|
|||
// <auto-generated />
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Invoicing;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage;
|
||||
using Microsoft.EntityFrameworkCore.Storage.Internal;
|
||||
using System;
|
||||
|
||||
namespace BTCPayServer.Migrations
|
||||
{
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
[Migration("20170901023716_Init")]
|
||||
partial class Init
|
||||
{
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "2.0.0-rtm-26452");
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.InvoiceData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<byte[]>("Blob");
|
||||
|
||||
b.Property<DateTimeOffset>("Created");
|
||||
|
||||
b.Property<string>("CustomerEmail");
|
||||
|
||||
b.Property<string>("ExceptionStatus");
|
||||
|
||||
b.Property<string>("ItemCode");
|
||||
|
||||
b.Property<string>("OrderId");
|
||||
|
||||
b.Property<string>("Status");
|
||||
|
||||
b.Property<string>("StoreDataId");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("StoreDataId");
|
||||
|
||||
b.ToTable("Invoices");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.PaymentData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<byte[]>("Blob");
|
||||
|
||||
b.Property<string>("InvoiceDataId");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("InvoiceDataId");
|
||||
|
||||
b.ToTable("Payments");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.RefundAddressesData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<byte[]>("Blob");
|
||||
|
||||
b.Property<string>("InvoiceDataId");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("InvoiceDataId");
|
||||
|
||||
b.ToTable("RefundAddresses");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.StoreData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<string>("DerivationStrategy");
|
||||
|
||||
b.Property<int>("SpeedPolicy");
|
||||
|
||||
b.Property<byte[]>("StoreCertificate");
|
||||
|
||||
b.Property<string>("StoreName");
|
||||
|
||||
b.Property<string>("StoreWebsite");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Stores");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.UserStore", b =>
|
||||
{
|
||||
b.Property<string>("ApplicationUserId");
|
||||
|
||||
b.Property<string>("StoreDataId");
|
||||
|
||||
b.HasKey("ApplicationUserId", "StoreDataId");
|
||||
|
||||
b.HasIndex("StoreDataId");
|
||||
|
||||
b.ToTable("UserStore");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Models.ApplicationUser", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<int>("AccessFailedCount");
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.IsConcurrencyToken();
|
||||
|
||||
b.Property<string>("Email")
|
||||
.HasMaxLength(256);
|
||||
|
||||
b.Property<bool>("EmailConfirmed");
|
||||
|
||||
b.Property<bool>("LockoutEnabled");
|
||||
|
||||
b.Property<DateTimeOffset?>("LockoutEnd");
|
||||
|
||||
b.Property<string>("NormalizedEmail")
|
||||
.HasMaxLength(256);
|
||||
|
||||
b.Property<string>("NormalizedUserName")
|
||||
.HasMaxLength(256);
|
||||
|
||||
b.Property<string>("PasswordHash");
|
||||
|
||||
b.Property<string>("PhoneNumber");
|
||||
|
||||
b.Property<bool>("PhoneNumberConfirmed");
|
||||
|
||||
b.Property<string>("SecurityStamp");
|
||||
|
||||
b.Property<bool>("TwoFactorEnabled");
|
||||
|
||||
b.Property<string>("UserName")
|
||||
.HasMaxLength(256);
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("NormalizedEmail")
|
||||
.HasName("EmailIndex");
|
||||
|
||||
b.HasIndex("NormalizedUserName")
|
||||
.IsUnique()
|
||||
.HasName("UserNameIndex");
|
||||
|
||||
b.ToTable("AspNetUsers");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.IsConcurrencyToken();
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasMaxLength(256);
|
||||
|
||||
b.Property<string>("NormalizedName")
|
||||
.HasMaxLength(256);
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("NormalizedName")
|
||||
.IsUnique()
|
||||
.HasName("RoleNameIndex");
|
||||
|
||||
b.ToTable("AspNetRoles");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<string>("ClaimType");
|
||||
|
||||
b.Property<string>("ClaimValue");
|
||||
|
||||
b.Property<string>("RoleId")
|
||||
.IsRequired();
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("RoleId");
|
||||
|
||||
b.ToTable("AspNetRoleClaims");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<string>("ClaimType");
|
||||
|
||||
b.Property<string>("ClaimValue");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.IsRequired();
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("AspNetUserClaims");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||
{
|
||||
b.Property<string>("LoginProvider");
|
||||
|
||||
b.Property<string>("ProviderKey");
|
||||
|
||||
b.Property<string>("ProviderDisplayName");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.IsRequired();
|
||||
|
||||
b.HasKey("LoginProvider", "ProviderKey");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("AspNetUserLogins");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
||||
{
|
||||
b.Property<string>("UserId");
|
||||
|
||||
b.Property<string>("RoleId");
|
||||
|
||||
b.HasKey("UserId", "RoleId");
|
||||
|
||||
b.HasIndex("RoleId");
|
||||
|
||||
b.ToTable("AspNetUserRoles");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||
{
|
||||
b.Property<string>("UserId");
|
||||
|
||||
b.Property<string>("LoginProvider");
|
||||
|
||||
b.Property<string>("Name");
|
||||
|
||||
b.Property<string>("Value");
|
||||
|
||||
b.HasKey("UserId", "LoginProvider", "Name");
|
||||
|
||||
b.ToTable("AspNetUserTokens");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.InvoiceData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
|
||||
.WithMany()
|
||||
.HasForeignKey("StoreDataId");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.PaymentData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
|
||||
.WithMany("Payments")
|
||||
.HasForeignKey("InvoiceDataId");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.RefundAddressesData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
|
||||
.WithMany("RefundAddresses")
|
||||
.HasForeignKey("InvoiceDataId");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.UserStore", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Models.ApplicationUser", "ApplicationUser")
|
||||
.WithMany("UserStores")
|
||||
.HasForeignKey("ApplicationUserId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
|
||||
.WithMany("UserStores")
|
||||
.HasForeignKey("StoreDataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole")
|
||||
.WithMany()
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Models.ApplicationUser")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Models.ApplicationUser")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole")
|
||||
.WithMany()
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.HasOne("BTCPayServer.Models.ApplicationUser")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Models.ApplicationUser")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
356
BTCPayServer/Migrations/20170901023716_Init.cs
Normal file
356
BTCPayServer/Migrations/20170901023716_Init.cs
Normal file
|
@ -0,0 +1,356 @@
|
|||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace BTCPayServer.Migrations
|
||||
{
|
||||
public partial class Init : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "AspNetRoles",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<string>(type: "TEXT", nullable: false),
|
||||
ConcurrencyStamp = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Name = table.Column<string>(type: "TEXT", maxLength: 256, nullable: true),
|
||||
NormalizedName = table.Column<string>(type: "TEXT", maxLength: 256, nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_AspNetRoles", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "AspNetUsers",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<string>(type: "TEXT", nullable: false),
|
||||
AccessFailedCount = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
ConcurrencyStamp = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Email = table.Column<string>(type: "TEXT", maxLength: 256, nullable: true),
|
||||
EmailConfirmed = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
LockoutEnabled = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
LockoutEnd = table.Column<DateTimeOffset>(type: "TEXT", nullable: true),
|
||||
NormalizedEmail = table.Column<string>(type: "TEXT", maxLength: 256, nullable: true),
|
||||
NormalizedUserName = table.Column<string>(type: "TEXT", maxLength: 256, nullable: true),
|
||||
PasswordHash = table.Column<string>(type: "TEXT", nullable: true),
|
||||
PhoneNumber = table.Column<string>(type: "TEXT", nullable: true),
|
||||
PhoneNumberConfirmed = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
SecurityStamp = table.Column<string>(type: "TEXT", nullable: true),
|
||||
TwoFactorEnabled = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
UserName = table.Column<string>(type: "TEXT", maxLength: 256, nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_AspNetUsers", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Stores",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<string>(type: "TEXT", nullable: false),
|
||||
DerivationStrategy = table.Column<string>(type: "TEXT", nullable: true),
|
||||
SpeedPolicy = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
StoreCertificate = table.Column<byte[]>(type: "BLOB", nullable: true),
|
||||
StoreName = table.Column<string>(type: "TEXT", nullable: true),
|
||||
StoreWebsite = table.Column<string>(type: "TEXT", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Stores", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "AspNetRoleClaims",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
ClaimType = table.Column<string>(type: "TEXT", nullable: true),
|
||||
ClaimValue = table.Column<string>(type: "TEXT", nullable: true),
|
||||
RoleId = table.Column<string>(type: "TEXT", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_AspNetRoleClaims", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_AspNetRoleClaims_AspNetRoles_RoleId",
|
||||
column: x => x.RoleId,
|
||||
principalTable: "AspNetRoles",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "AspNetUserClaims",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
ClaimType = table.Column<string>(type: "TEXT", nullable: true),
|
||||
ClaimValue = table.Column<string>(type: "TEXT", nullable: true),
|
||||
UserId = table.Column<string>(type: "TEXT", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_AspNetUserClaims", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_AspNetUserClaims_AspNetUsers_UserId",
|
||||
column: x => x.UserId,
|
||||
principalTable: "AspNetUsers",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "AspNetUserLogins",
|
||||
columns: table => new
|
||||
{
|
||||
LoginProvider = table.Column<string>(type: "TEXT", nullable: false),
|
||||
ProviderKey = table.Column<string>(type: "TEXT", nullable: false),
|
||||
ProviderDisplayName = table.Column<string>(type: "TEXT", nullable: true),
|
||||
UserId = table.Column<string>(type: "TEXT", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_AspNetUserLogins", x => new { x.LoginProvider, x.ProviderKey });
|
||||
table.ForeignKey(
|
||||
name: "FK_AspNetUserLogins_AspNetUsers_UserId",
|
||||
column: x => x.UserId,
|
||||
principalTable: "AspNetUsers",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "AspNetUserRoles",
|
||||
columns: table => new
|
||||
{
|
||||
UserId = table.Column<string>(type: "TEXT", nullable: false),
|
||||
RoleId = table.Column<string>(type: "TEXT", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_AspNetUserRoles", x => new { x.UserId, x.RoleId });
|
||||
table.ForeignKey(
|
||||
name: "FK_AspNetUserRoles_AspNetRoles_RoleId",
|
||||
column: x => x.RoleId,
|
||||
principalTable: "AspNetRoles",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_AspNetUserRoles_AspNetUsers_UserId",
|
||||
column: x => x.UserId,
|
||||
principalTable: "AspNetUsers",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "AspNetUserTokens",
|
||||
columns: table => new
|
||||
{
|
||||
UserId = table.Column<string>(type: "TEXT", nullable: false),
|
||||
LoginProvider = table.Column<string>(type: "TEXT", nullable: false),
|
||||
Name = table.Column<string>(type: "TEXT", nullable: false),
|
||||
Value = table.Column<string>(type: "TEXT", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_AspNetUserTokens", x => new { x.UserId, x.LoginProvider, x.Name });
|
||||
table.ForeignKey(
|
||||
name: "FK_AspNetUserTokens_AspNetUsers_UserId",
|
||||
column: x => x.UserId,
|
||||
principalTable: "AspNetUsers",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Invoices",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<string>(type: "TEXT", nullable: false),
|
||||
Blob = table.Column<byte[]>(type: "BLOB", nullable: true),
|
||||
Created = table.Column<DateTimeOffset>(type: "TEXT", nullable: false),
|
||||
CustomerEmail = table.Column<string>(type: "TEXT", nullable: true),
|
||||
ExceptionStatus = table.Column<string>(type: "TEXT", nullable: true),
|
||||
ItemCode = table.Column<string>(type: "TEXT", nullable: true),
|
||||
OrderId = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Status = table.Column<string>(type: "TEXT", nullable: true),
|
||||
StoreDataId = table.Column<string>(type: "TEXT", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Invoices", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_Invoices_Stores_StoreDataId",
|
||||
column: x => x.StoreDataId,
|
||||
principalTable: "Stores",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "UserStore",
|
||||
columns: table => new
|
||||
{
|
||||
ApplicationUserId = table.Column<string>(type: "TEXT", nullable: false),
|
||||
StoreDataId = table.Column<string>(type: "TEXT", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_UserStore", x => new { x.ApplicationUserId, x.StoreDataId });
|
||||
table.ForeignKey(
|
||||
name: "FK_UserStore_AspNetUsers_ApplicationUserId",
|
||||
column: x => x.ApplicationUserId,
|
||||
principalTable: "AspNetUsers",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_UserStore_Stores_StoreDataId",
|
||||
column: x => x.StoreDataId,
|
||||
principalTable: "Stores",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Payments",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<string>(type: "TEXT", nullable: false),
|
||||
Blob = table.Column<byte[]>(type: "BLOB", nullable: true),
|
||||
InvoiceDataId = table.Column<string>(type: "TEXT", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Payments", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_Payments_Invoices_InvoiceDataId",
|
||||
column: x => x.InvoiceDataId,
|
||||
principalTable: "Invoices",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "RefundAddresses",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<string>(type: "TEXT", nullable: false),
|
||||
Blob = table.Column<byte[]>(type: "BLOB", nullable: true),
|
||||
InvoiceDataId = table.Column<string>(type: "TEXT", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_RefundAddresses", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_RefundAddresses_Invoices_InvoiceDataId",
|
||||
column: x => x.InvoiceDataId,
|
||||
principalTable: "Invoices",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_AspNetRoleClaims_RoleId",
|
||||
table: "AspNetRoleClaims",
|
||||
column: "RoleId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "RoleNameIndex",
|
||||
table: "AspNetRoles",
|
||||
column: "NormalizedName",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_AspNetUserClaims_UserId",
|
||||
table: "AspNetUserClaims",
|
||||
column: "UserId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_AspNetUserLogins_UserId",
|
||||
table: "AspNetUserLogins",
|
||||
column: "UserId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_AspNetUserRoles_RoleId",
|
||||
table: "AspNetUserRoles",
|
||||
column: "RoleId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "EmailIndex",
|
||||
table: "AspNetUsers",
|
||||
column: "NormalizedEmail");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "UserNameIndex",
|
||||
table: "AspNetUsers",
|
||||
column: "NormalizedUserName",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Invoices_StoreDataId",
|
||||
table: "Invoices",
|
||||
column: "StoreDataId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Payments_InvoiceDataId",
|
||||
table: "Payments",
|
||||
column: "InvoiceDataId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_RefundAddresses_InvoiceDataId",
|
||||
table: "RefundAddresses",
|
||||
column: "InvoiceDataId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_UserStore_StoreDataId",
|
||||
table: "UserStore",
|
||||
column: "StoreDataId");
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "AspNetRoleClaims");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "AspNetUserClaims");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "AspNetUserLogins");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "AspNetUserRoles");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "AspNetUserTokens");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Payments");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "RefundAddresses");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "UserStore");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "AspNetRoles");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Invoices");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "AspNetUsers");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Stores");
|
||||
}
|
||||
}
|
||||
}
|
354
BTCPayServer/Migrations/ApplicationDbContextModelSnapshot.cs
Normal file
354
BTCPayServer/Migrations/ApplicationDbContextModelSnapshot.cs
Normal file
|
@ -0,0 +1,354 @@
|
|||
// <auto-generated />
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Invoicing;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage;
|
||||
using Microsoft.EntityFrameworkCore.Storage.Internal;
|
||||
using System;
|
||||
|
||||
namespace BTCPayServer.Migrations
|
||||
{
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
partial class ApplicationDbContextModelSnapshot : ModelSnapshot
|
||||
{
|
||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "2.0.0-rtm-26452");
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.InvoiceData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<byte[]>("Blob");
|
||||
|
||||
b.Property<DateTimeOffset>("Created");
|
||||
|
||||
b.Property<string>("CustomerEmail");
|
||||
|
||||
b.Property<string>("ExceptionStatus");
|
||||
|
||||
b.Property<string>("ItemCode");
|
||||
|
||||
b.Property<string>("OrderId");
|
||||
|
||||
b.Property<string>("Status");
|
||||
|
||||
b.Property<string>("StoreDataId");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("StoreDataId");
|
||||
|
||||
b.ToTable("Invoices");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.PaymentData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<byte[]>("Blob");
|
||||
|
||||
b.Property<string>("InvoiceDataId");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("InvoiceDataId");
|
||||
|
||||
b.ToTable("Payments");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.RefundAddressesData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<byte[]>("Blob");
|
||||
|
||||
b.Property<string>("InvoiceDataId");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("InvoiceDataId");
|
||||
|
||||
b.ToTable("RefundAddresses");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.StoreData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<string>("DerivationStrategy");
|
||||
|
||||
b.Property<int>("SpeedPolicy");
|
||||
|
||||
b.Property<byte[]>("StoreCertificate");
|
||||
|
||||
b.Property<string>("StoreName");
|
||||
|
||||
b.Property<string>("StoreWebsite");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Stores");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.UserStore", b =>
|
||||
{
|
||||
b.Property<string>("ApplicationUserId");
|
||||
|
||||
b.Property<string>("StoreDataId");
|
||||
|
||||
b.HasKey("ApplicationUserId", "StoreDataId");
|
||||
|
||||
b.HasIndex("StoreDataId");
|
||||
|
||||
b.ToTable("UserStore");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Models.ApplicationUser", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<int>("AccessFailedCount");
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.IsConcurrencyToken();
|
||||
|
||||
b.Property<string>("Email")
|
||||
.HasMaxLength(256);
|
||||
|
||||
b.Property<bool>("EmailConfirmed");
|
||||
|
||||
b.Property<bool>("LockoutEnabled");
|
||||
|
||||
b.Property<DateTimeOffset?>("LockoutEnd");
|
||||
|
||||
b.Property<string>("NormalizedEmail")
|
||||
.HasMaxLength(256);
|
||||
|
||||
b.Property<string>("NormalizedUserName")
|
||||
.HasMaxLength(256);
|
||||
|
||||
b.Property<string>("PasswordHash");
|
||||
|
||||
b.Property<string>("PhoneNumber");
|
||||
|
||||
b.Property<bool>("PhoneNumberConfirmed");
|
||||
|
||||
b.Property<string>("SecurityStamp");
|
||||
|
||||
b.Property<bool>("TwoFactorEnabled");
|
||||
|
||||
b.Property<string>("UserName")
|
||||
.HasMaxLength(256);
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("NormalizedEmail")
|
||||
.HasName("EmailIndex");
|
||||
|
||||
b.HasIndex("NormalizedUserName")
|
||||
.IsUnique()
|
||||
.HasName("UserNameIndex");
|
||||
|
||||
b.ToTable("AspNetUsers");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.IsConcurrencyToken();
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasMaxLength(256);
|
||||
|
||||
b.Property<string>("NormalizedName")
|
||||
.HasMaxLength(256);
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("NormalizedName")
|
||||
.IsUnique()
|
||||
.HasName("RoleNameIndex");
|
||||
|
||||
b.ToTable("AspNetRoles");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<string>("ClaimType");
|
||||
|
||||
b.Property<string>("ClaimValue");
|
||||
|
||||
b.Property<string>("RoleId")
|
||||
.IsRequired();
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("RoleId");
|
||||
|
||||
b.ToTable("AspNetRoleClaims");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<string>("ClaimType");
|
||||
|
||||
b.Property<string>("ClaimValue");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.IsRequired();
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("AspNetUserClaims");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||
{
|
||||
b.Property<string>("LoginProvider");
|
||||
|
||||
b.Property<string>("ProviderKey");
|
||||
|
||||
b.Property<string>("ProviderDisplayName");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.IsRequired();
|
||||
|
||||
b.HasKey("LoginProvider", "ProviderKey");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("AspNetUserLogins");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
||||
{
|
||||
b.Property<string>("UserId");
|
||||
|
||||
b.Property<string>("RoleId");
|
||||
|
||||
b.HasKey("UserId", "RoleId");
|
||||
|
||||
b.HasIndex("RoleId");
|
||||
|
||||
b.ToTable("AspNetUserRoles");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||
{
|
||||
b.Property<string>("UserId");
|
||||
|
||||
b.Property<string>("LoginProvider");
|
||||
|
||||
b.Property<string>("Name");
|
||||
|
||||
b.Property<string>("Value");
|
||||
|
||||
b.HasKey("UserId", "LoginProvider", "Name");
|
||||
|
||||
b.ToTable("AspNetUserTokens");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.InvoiceData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
|
||||
.WithMany()
|
||||
.HasForeignKey("StoreDataId");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.PaymentData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
|
||||
.WithMany("Payments")
|
||||
.HasForeignKey("InvoiceDataId");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.RefundAddressesData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
|
||||
.WithMany("RefundAddresses")
|
||||
.HasForeignKey("InvoiceDataId");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.UserStore", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Models.ApplicationUser", "ApplicationUser")
|
||||
.WithMany("UserStores")
|
||||
.HasForeignKey("ApplicationUserId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
|
||||
.WithMany("UserStores")
|
||||
.HasForeignKey("StoreDataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole")
|
||||
.WithMany()
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Models.ApplicationUser")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Models.ApplicationUser")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole")
|
||||
.WithMany()
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.HasOne("BTCPayServer.Models.ApplicationUser")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Models.ApplicationUser")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Models.AccountViewModels
|
||||
{
|
||||
public class ExternalLoginViewModel
|
||||
{
|
||||
[Required]
|
||||
[EmailAddress]
|
||||
public string Email { get; set; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Models.AccountViewModels
|
||||
{
|
||||
public class ForgotPasswordViewModel
|
||||
{
|
||||
[Required]
|
||||
[EmailAddress]
|
||||
public string Email { get; set; }
|
||||
}
|
||||
}
|
22
BTCPayServer/Models/AccountViewModels/LoginViewModel.cs
Normal file
22
BTCPayServer/Models/AccountViewModels/LoginViewModel.cs
Normal file
|
@ -0,0 +1,22 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Models.AccountViewModels
|
||||
{
|
||||
public class LoginViewModel
|
||||
{
|
||||
[Required]
|
||||
[EmailAddress]
|
||||
public string Email { get; set; }
|
||||
|
||||
[Required]
|
||||
[DataType(DataType.Password)]
|
||||
public string Password { get; set; }
|
||||
|
||||
[Display(Name = "Remember me?")]
|
||||
public bool RememberMe { get; set; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Models.AccountViewModels
|
||||
{
|
||||
public class LoginWith2faViewModel
|
||||
{
|
||||
[Required]
|
||||
[StringLength(7, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
|
||||
[DataType(DataType.Text)]
|
||||
[Display(Name = "Authenticator code")]
|
||||
public string TwoFactorCode { get; set; }
|
||||
|
||||
[Display(Name = "Remember this machine")]
|
||||
public bool RememberMachine { get; set; }
|
||||
|
||||
public bool RememberMe { get; set; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Models.AccountViewModels
|
||||
{
|
||||
public class LoginWithRecoveryCodeViewModel
|
||||
{
|
||||
[Required]
|
||||
[DataType(DataType.Text)]
|
||||
[Display(Name = "Recovery Code")]
|
||||
public string RecoveryCode { get; set; }
|
||||
}
|
||||
}
|
27
BTCPayServer/Models/AccountViewModels/RegisterViewModel.cs
Normal file
27
BTCPayServer/Models/AccountViewModels/RegisterViewModel.cs
Normal file
|
@ -0,0 +1,27 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Models.AccountViewModels
|
||||
{
|
||||
public class RegisterViewModel
|
||||
{
|
||||
[Required]
|
||||
[EmailAddress]
|
||||
[Display(Name = "Email")]
|
||||
public string Email { get; set; }
|
||||
|
||||
[Required]
|
||||
[StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
|
||||
[DataType(DataType.Password)]
|
||||
[Display(Name = "Password")]
|
||||
public string Password { get; set; }
|
||||
|
||||
[DataType(DataType.Password)]
|
||||
[Display(Name = "Confirm password")]
|
||||
[Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]
|
||||
public string ConfirmPassword { get; set; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Models.AccountViewModels
|
||||
{
|
||||
public class ResetPasswordViewModel
|
||||
{
|
||||
[Required]
|
||||
[EmailAddress]
|
||||
public string Email { get; set; }
|
||||
|
||||
[Required]
|
||||
[StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
|
||||
[DataType(DataType.Password)]
|
||||
public string Password { get; set; }
|
||||
|
||||
[DataType(DataType.Password)]
|
||||
[Display(Name = "Confirm password")]
|
||||
[Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]
|
||||
public string ConfirmPassword { get; set; }
|
||||
|
||||
public string Code { get; set; }
|
||||
}
|
||||
}
|
20
BTCPayServer/Models/ApplicationUser.cs
Normal file
20
BTCPayServer/Models/ApplicationUser.cs
Normal file
|
@ -0,0 +1,20 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using BTCPayServer.Invoicing;
|
||||
using BTCPayServer.Data;
|
||||
|
||||
namespace BTCPayServer.Models
|
||||
{
|
||||
// Add profile data for application users by adding properties to the ApplicationUser class
|
||||
public class ApplicationUser : IdentityUser
|
||||
{
|
||||
public List<UserStore> UserStores
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
}
|
||||
}
|
39
BTCPayServer/Models/BitpayErrorsModel.cs
Normal file
39
BTCPayServer/Models/BitpayErrorsModel.cs
Normal file
|
@ -0,0 +1,39 @@
|
|||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace BTCPayServer.Models
|
||||
{
|
||||
public class BitpayErrorsModel
|
||||
{
|
||||
public BitpayErrorsModel()
|
||||
{
|
||||
|
||||
}
|
||||
public BitpayErrorsModel(BitpayHttpException ex)
|
||||
{
|
||||
Error = ex.Message;
|
||||
}
|
||||
|
||||
[JsonProperty("errors", DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
public BitpayErrorModel[] Errors
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
[JsonProperty("error", DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
public string Error
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
}
|
||||
|
||||
public class BitpayErrorModel
|
||||
{
|
||||
[JsonProperty("error")]
|
||||
public string Error
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
}
|
||||
}
|
37
BTCPayServer/Models/DataWrapper.cs
Normal file
37
BTCPayServer/Models/DataWrapper.cs
Normal file
|
@ -0,0 +1,37 @@
|
|||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace BTCPayServer.Models
|
||||
{
|
||||
public class DataWrapper
|
||||
{
|
||||
public static DataWrapper<T> Create<T>(T obj)
|
||||
{
|
||||
return new DataWrapper<T>(obj);
|
||||
}
|
||||
}
|
||||
public class DataWrapper<T>
|
||||
{
|
||||
public DataWrapper()
|
||||
{
|
||||
|
||||
}
|
||||
public DataWrapper(T data)
|
||||
{
|
||||
Data = data;
|
||||
}
|
||||
|
||||
[JsonProperty("facade", NullValueHandling = NullValueHandling.Ignore)]
|
||||
public string Facade
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
[JsonProperty("data")]
|
||||
public T Data
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
}
|
||||
}
|
11
BTCPayServer/Models/ErrorViewModel.cs
Normal file
11
BTCPayServer/Models/ErrorViewModel.cs
Normal file
|
@ -0,0 +1,11 @@
|
|||
using System;
|
||||
|
||||
namespace BTCPayServer.Models
|
||||
{
|
||||
public class ErrorViewModel
|
||||
{
|
||||
public string RequestId { get; set; }
|
||||
|
||||
public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
|
||||
}
|
||||
}
|
54
BTCPayServer/Models/GetTokensResponse.cs
Normal file
54
BTCPayServer/Models/GetTokensResponse.cs
Normal file
|
@ -0,0 +1,54 @@
|
|||
using Microsoft.AspNetCore.Mvc;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Authentication;
|
||||
using NBitcoin.DataEncoders;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using System.IO;
|
||||
|
||||
namespace BTCPayServer.Models
|
||||
{
|
||||
//{"data":[{"pos":"FfZ6WCa8TunAvPCpQZXkdBsoH4Yo18FyPaJ5X5qjrVVY"},{"pos/invoice":"H1pwwh2tMeSCri9rh5VvHWEHokGdf2EGtghfZkUEbeZv"},{"merchant":"89zEBr9orAc6wgybAABp8ioGcjYeFrUaZgMzjxNuqYty"},{"merchant/invoice":"8e7ijDxGfJsWXWgJuKXjjNgxnX1xpsBM8cTZCFnU7ehj"}]}
|
||||
public class GetTokensResponse : IActionResult
|
||||
{
|
||||
BitTokenEntity[] _Tokens;
|
||||
public GetTokensResponse(BitTokenEntity[] tokens)
|
||||
{
|
||||
if(tokens == null)
|
||||
throw new ArgumentNullException(nameof(tokens));
|
||||
this._Tokens = tokens;
|
||||
}
|
||||
|
||||
[JsonProperty(PropertyName = "data")]
|
||||
//{"pos":"FfZ6WCa8TunAvPCpQZXkdBsoH4Yo18FyPaJ5X5qjrVVY"}
|
||||
public JArray Data
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public async Task ExecuteResultAsync(ActionContext context)
|
||||
{
|
||||
JObject jobj = new JObject();
|
||||
JArray jarray = new JArray();
|
||||
jobj.Add("data", jarray);
|
||||
foreach(var token in _Tokens)
|
||||
{
|
||||
JObject item = new JObject();
|
||||
jarray.Add(item);
|
||||
JProperty jProp = new JProperty(token.Name);
|
||||
item.Add(jProp);
|
||||
jProp.Value = token.Value;
|
||||
}
|
||||
context.HttpContext.Response.Headers.Add("Content-Type", new Microsoft.Extensions.Primitives.StringValues("application/json"));
|
||||
var str = JsonConvert.SerializeObject(jobj);
|
||||
using(var writer = new StreamWriter(context.HttpContext.Response.Body, Encoding.UTF8, 1024 * 10, true))
|
||||
{
|
||||
await writer.WriteLineAsync(str);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
225
BTCPayServer/Models/InvoiceResponse.cs
Normal file
225
BTCPayServer/Models/InvoiceResponse.cs
Normal file
|
@ -0,0 +1,225 @@
|
|||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace BTCPayServer.Models
|
||||
{
|
||||
class DateTimeJsonConverter : JsonConverter
|
||||
{
|
||||
public override bool CanConvert(Type objectType)
|
||||
{
|
||||
return objectType == typeof(DateTimeOffset);
|
||||
}
|
||||
|
||||
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
|
||||
{
|
||||
var v = (long)reader.Value;
|
||||
Check(v);
|
||||
return unixRef + TimeSpan.FromMilliseconds((long)v);
|
||||
}
|
||||
|
||||
static DateTimeOffset unixRef = new DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan.Zero);
|
||||
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
|
||||
{
|
||||
var date = ((DateTimeOffset)value).ToUniversalTime();
|
||||
long v = (long)(date - unixRef).TotalMilliseconds;
|
||||
Check(v);
|
||||
writer.WriteValue(v);
|
||||
}
|
||||
|
||||
private static void Check(long v)
|
||||
{
|
||||
if(v < 0)
|
||||
throw new FormatException("Invalid datetime (less than 1/1/1970)");
|
||||
}
|
||||
}
|
||||
|
||||
//{"facade":"pos/invoice","data":{,}}
|
||||
public class InvoiceResponse
|
||||
{
|
||||
//"url":"https://test.bitpay.com/invoice?id=9saCHtp1zyPcNoi3rDdBu8"
|
||||
[JsonProperty("url")]
|
||||
public string Url
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
//"posData":"posData"
|
||||
[JsonProperty("posData")]
|
||||
public string PosData
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
//status":"new"
|
||||
[JsonProperty("status")]
|
||||
public string Status
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
//"btcPrice":"0.001157"
|
||||
[JsonProperty("btcPrice")]
|
||||
public string BTCPrice
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
//"btcDue":"0.001160"
|
||||
[JsonProperty("btcDue")]
|
||||
public string BTCDue
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
//"price":5
|
||||
[JsonProperty("price")]
|
||||
public double Price
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
//"currency":"USD"
|
||||
[JsonProperty("currency")]
|
||||
public string Currency
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
//"exRates":{"USD":4320.02}
|
||||
[JsonProperty("exRates")]
|
||||
public Dictionary<string, double> ExRates
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
//"buyerTotalBtcAmount":"0.001160"
|
||||
[JsonProperty("buyerTotalBtcAmount")]
|
||||
public string BuyerTotalBtcAmount
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
//"itemDesc":"Some description"
|
||||
[JsonProperty("itemDesc")]
|
||||
public string ItemDesc
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
//"orderId":"orderId"
|
||||
[JsonProperty("orderId")]
|
||||
public string OrderId
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
//"guid":"e238ce2a-06da-47e9-aefd-2588d4aa5f8d"
|
||||
[JsonProperty("guid")]
|
||||
public string Guid
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
//"id":"9saCHtp1zyPcNoi3rDdBu8"
|
||||
[JsonProperty("id")]
|
||||
public string Id
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
[JsonConverter(typeof(DateTimeJsonConverter))]
|
||||
[JsonProperty("invoiceTime")]
|
||||
public DateTimeOffset InvoiceTime
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
[JsonConverter(typeof(DateTimeJsonConverter))]
|
||||
[JsonProperty("expirationTime")]
|
||||
public DateTimeOffset ExpirationTime
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
[JsonConverter(typeof(DateTimeJsonConverter))]
|
||||
[JsonProperty("currentTime")]
|
||||
public DateTimeOffset CurrentTime
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
//"lowFeeDetected":false
|
||||
[JsonProperty("lowFeeDetected")]
|
||||
public bool LowFeeDetected
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
//"btcPaid":"0.000000"
|
||||
[JsonProperty("btcPaid")]
|
||||
public string BTCPaid
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
//"rate":4320.02
|
||||
[JsonProperty("rate")]
|
||||
public double Rate
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
//"exceptionStatus":false
|
||||
//Can be `paidPartial`, `paidOver`, or false
|
||||
[JsonProperty("exceptionStatus")]
|
||||
public JToken ExceptionStatus
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
//"paymentUrls":{"BIP21":"bitcoin:muFQCEbfRJohcds3bkfv1sRFj8uVTfv2wv?amount=0.001160","BIP72":"bitcoin:muFQCEbfRJohcds3bkfv1sRFj8uVTfv2wv?amount=0.001160&r=https://test.bitpay.com/i/9saCHtp1zyPcNoi3rDdBu8","BIP72b":"bitcoin:?r=https://test.bitpay.com/i/9saCHtp1zyPcNoi3rDdBu8","BIP73":"https://test.bitpay.com/i/9saCHtp1zyPcNoi3rDdBu8"}
|
||||
[JsonProperty("paymentUrls")]
|
||||
public NBitpayClient.InvoicePaymentUrls PaymentUrls
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
//"refundAddressRequestPending":false
|
||||
[JsonProperty("refundAddressRequestPending")]
|
||||
public bool RefundAddressRequestPending
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
//"buyerPaidBtcMinerFee":"0.000003"
|
||||
[JsonProperty("buyerPaidBtcMinerFee")]
|
||||
public string BuyerPaidBtcMinerFee
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
//"bitcoinAddress":"muFQCEbfRJohcds3bkfv1sRFj8uVTfv2wv"
|
||||
[JsonProperty("bitcoinAddress")]
|
||||
public string BitcoinAddress
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
//"token":"9jF3TU7A8inKHDRQXFrKcRnMkLXWGQ2yKf7pnjMKGHEfpwTNV35HytrD9FXDBy25Li"
|
||||
[JsonProperty("token")]
|
||||
public string Token
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
[JsonProperty("flags")]
|
||||
public Flags Flags
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
}
|
||||
public class Flags
|
||||
{
|
||||
[JsonProperty("refundable")]
|
||||
public bool Refundable
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
}
|
||||
}
|
38
BTCPayServer/Models/InvoicingModels/CreateInvoiceModel.cs
Normal file
38
BTCPayServer/Models/InvoicingModels/CreateInvoiceModel.cs
Normal file
|
@ -0,0 +1,38 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Models.InvoicingModels
|
||||
{
|
||||
public class CreateInvoiceModel
|
||||
{
|
||||
[Required]
|
||||
public double? Amount
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public string OrderId
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public string ItemDesc
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public string PosData
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
[EmailAddress]
|
||||
public string BuyerEmail
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
}
|
||||
}
|
59
BTCPayServer/Models/InvoicingModels/InvoicesModel.cs
Normal file
59
BTCPayServer/Models/InvoicingModels/InvoicesModel.cs
Normal file
|
@ -0,0 +1,59 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Models.InvoicingModels
|
||||
{
|
||||
public class InvoicesModel
|
||||
{
|
||||
public int Skip
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public int Count
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public string SearchTerm
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public List<InvoiceModel> Invoices
|
||||
{
|
||||
get; set;
|
||||
} = new List<InvoiceModel>();
|
||||
public string StatusMessage
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
}
|
||||
|
||||
public class InvoiceModel
|
||||
{
|
||||
public DateTimeOffset Date
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public string InvoiceId
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public string Status
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public string AmountCurrency
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public string StatusMessage
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
}
|
||||
}
|
103
BTCPayServer/Models/InvoicingModels/PaymentModel.cs
Normal file
103
BTCPayServer/Models/InvoicingModels/PaymentModel.cs
Normal file
|
@ -0,0 +1,103 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Models.InvoicingModels
|
||||
{
|
||||
public class PaymentModel
|
||||
{
|
||||
public string InvoiceId
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public string OrderId
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public string BTCAddress
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public string BTCDue
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public string CustomerEmail
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public int ExpirationSeconds
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public int MaxTimeSeconds
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public string TimeLeft
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public string RedirectUrl
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
|
||||
public string StoreName
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public string ItemDesc
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public string Rate
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public string BTCAmount
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public string TxFees
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public string InvoiceBitcoinUrl
|
||||
{
|
||||
get;
|
||||
internal set;
|
||||
}
|
||||
public string BTCTotalDue
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public int TxCount
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public string StoreEmail
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public string Status
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
}
|
||||
}
|
18
BTCPayServer/Models/InvoicingModels/UpdateCustomerModel.cs
Normal file
18
BTCPayServer/Models/InvoicingModels/UpdateCustomerModel.cs
Normal file
|
@ -0,0 +1,18 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Models.InvoicingModels
|
||||
{
|
||||
public class UpdateCustomerModel
|
||||
{
|
||||
[EmailAddress]
|
||||
[Required]
|
||||
public string Email
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Models.ManageViewModels
|
||||
{
|
||||
public class ChangePasswordViewModel
|
||||
{
|
||||
[Required]
|
||||
[DataType(DataType.Password)]
|
||||
[Display(Name = "Current password")]
|
||||
public string OldPassword { get; set; }
|
||||
|
||||
[Required]
|
||||
[StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
|
||||
[DataType(DataType.Password)]
|
||||
[Display(Name = "New password")]
|
||||
public string NewPassword { get; set; }
|
||||
|
||||
[DataType(DataType.Password)]
|
||||
[Display(Name = "Confirm new password")]
|
||||
[Compare("NewPassword", ErrorMessage = "The new password and confirmation password do not match.")]
|
||||
public string ConfirmPassword { get; set; }
|
||||
|
||||
public string StatusMessage { get; set; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Models.ManageViewModels
|
||||
{
|
||||
public class EnableAuthenticatorViewModel
|
||||
{
|
||||
[Required]
|
||||
[StringLength(7, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
|
||||
[DataType(DataType.Text)]
|
||||
[Display(Name = "Verification Code")]
|
||||
public string Code { get; set; }
|
||||
|
||||
[ReadOnly(true)]
|
||||
public string SharedKey { get; set; }
|
||||
|
||||
public string AuthenticatorUri { get; set; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
namespace BTCPayServer.Models.ManageViewModels
|
||||
{
|
||||
public class ExternalLoginsViewModel
|
||||
{
|
||||
public IList<UserLoginInfo> CurrentLogins { get; set; }
|
||||
|
||||
public IList<AuthenticationScheme> OtherLogins { get; set; }
|
||||
|
||||
public bool ShowRemoveButton { get; set; }
|
||||
|
||||
public string StatusMessage { get; set; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Models.ManageViewModels
|
||||
{
|
||||
public class GenerateRecoveryCodesViewModel
|
||||
{
|
||||
public string[] RecoveryCodes { get; set; }
|
||||
}
|
||||
}
|
53
BTCPayServer/Models/ManageViewModels/IndexViewModel.cs
Normal file
53
BTCPayServer/Models/ManageViewModels/IndexViewModel.cs
Normal file
|
@ -0,0 +1,53 @@
|
|||
using BTCPayServer.Invoicing;
|
||||
using BTCPayServer.Validations;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Models.ManageViewModels
|
||||
{
|
||||
public class IndexViewModel
|
||||
{
|
||||
public string Username { get; set; }
|
||||
|
||||
public bool IsEmailConfirmed { get; set; }
|
||||
|
||||
[Required]
|
||||
[EmailAddress]
|
||||
[MaxLength(50)]
|
||||
public string Email { get; set; }
|
||||
|
||||
[ExtPubKeyValidator]
|
||||
public string ExtPubKey { get; set; }
|
||||
|
||||
[Display(Name = "Store Name")]
|
||||
[MaxLength(50)]
|
||||
public string StoreName
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
[Display(Name = "Consider the invoice confirmed when the payment transaction...")]
|
||||
public SpeedPolicy SpeedPolicy
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
[Phone]
|
||||
[Display(Name = "Phone number")]
|
||||
[MaxLength(50)]
|
||||
public string PhoneNumber { get; set; }
|
||||
|
||||
public string StatusMessage { get; set; }
|
||||
|
||||
[Url]
|
||||
[Display(Name = "Store Website")]
|
||||
public string StoreWebsite
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
}
|
||||
}
|
27
BTCPayServer/Models/ManageViewModels/PairingModel.cs
Normal file
27
BTCPayServer/Models/ManageViewModels/PairingModel.cs
Normal file
|
@ -0,0 +1,27 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Models.ManageViewModels
|
||||
{
|
||||
public class PairingModel
|
||||
{
|
||||
public string Id
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public string Label
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public string Facade
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public string SIN
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
}
|
||||
}
|
14
BTCPayServer/Models/ManageViewModels/RemoveLoginViewModel.cs
Normal file
14
BTCPayServer/Models/ManageViewModels/RemoveLoginViewModel.cs
Normal file
|
@ -0,0 +1,14 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Models.ManageViewModels
|
||||
{
|
||||
public class RemoveLoginViewModel
|
||||
{
|
||||
public string LoginProvider { get; set; }
|
||||
public string ProviderKey { get; set; }
|
||||
}
|
||||
}
|
24
BTCPayServer/Models/ManageViewModels/SetPasswordViewModel.cs
Normal file
24
BTCPayServer/Models/ManageViewModels/SetPasswordViewModel.cs
Normal file
|
@ -0,0 +1,24 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Models.ManageViewModels
|
||||
{
|
||||
public class SetPasswordViewModel
|
||||
{
|
||||
[Required]
|
||||
[StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
|
||||
[DataType(DataType.Password)]
|
||||
[Display(Name = "New password")]
|
||||
public string NewPassword { get; set; }
|
||||
|
||||
[DataType(DataType.Password)]
|
||||
[Display(Name = "Confirm new password")]
|
||||
[Compare("NewPassword", ErrorMessage = "The new password and confirmation password do not match.")]
|
||||
public string ConfirmPassword { get; set; }
|
||||
|
||||
public string StatusMessage { get; set; }
|
||||
}
|
||||
}
|
60
BTCPayServer/Models/ManageViewModels/TokensViewModel.cs
Normal file
60
BTCPayServer/Models/ManageViewModels/TokensViewModel.cs
Normal file
|
@ -0,0 +1,60 @@
|
|||
using BTCPayServer.Validations;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Models.ManageViewModels
|
||||
{
|
||||
public class AddTokenViewModel
|
||||
{
|
||||
[PubKeyValidatorAttribute]
|
||||
public string PublicKey
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
[Required]
|
||||
public string Label
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
[Required]
|
||||
public string Facade
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
}
|
||||
public class TokenViewModel
|
||||
{
|
||||
public string Id
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public string Label
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public string SIN
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public string Facade
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
}
|
||||
public class TokensViewModel
|
||||
{
|
||||
public TokenViewModel[] Tokens
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public string StatusMessage
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Models.ManageViewModels
|
||||
{
|
||||
public class TwoFactorAuthenticationViewModel
|
||||
{
|
||||
public bool HasAuthenticator { get; set; }
|
||||
|
||||
public int RecoveryCodesLeft { get; set; }
|
||||
|
||||
public bool Is2faEnabled { get; set; }
|
||||
}
|
||||
}
|
82
BTCPayServer/Models/TokenRequest.cs
Normal file
82
BTCPayServer/Models/TokenRequest.cs
Normal file
|
@ -0,0 +1,82 @@
|
|||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayServer.Models
|
||||
{
|
||||
public class PairingCodeRequest
|
||||
{
|
||||
[JsonProperty(PropertyName = "id")]
|
||||
public string Id
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
[JsonProperty(PropertyName = "guid")]
|
||||
public string Guid
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
[JsonProperty(PropertyName = "facade")]
|
||||
public string Facade
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
[JsonProperty(PropertyName = "count")]
|
||||
public int Count
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
[JsonProperty(PropertyName = "label")]
|
||||
public string Label
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
}
|
||||
|
||||
public class PairingCodeResponse
|
||||
{
|
||||
[JsonProperty(PropertyName = "pairingCode")]
|
||||
public string PairingCode
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
[JsonProperty(PropertyName = "pairingExpiration")]
|
||||
[JsonConverter(typeof(DateTimeJsonConverter))]
|
||||
public DateTimeOffset PairingExpiration
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
[JsonProperty(PropertyName = "dateCreated")]
|
||||
[JsonConverter(typeof(DateTimeJsonConverter))]
|
||||
public DateTimeOffset DateCreated
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
[JsonProperty(PropertyName = "facade")]
|
||||
public string Facade
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[JsonProperty(PropertyName = "token")]
|
||||
public string Token
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[JsonProperty(PropertyName = "label")]
|
||||
public string Label
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
}
|
||||
}
|
88
BTCPayServer/Program.cs
Normal file
88
BTCPayServer/Program.cs
Normal file
|
@ -0,0 +1,88 @@
|
|||
using BTCPayServer.Configuration;
|
||||
using BTCPayServer.Logging;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using BTCPayServer.Hosting;
|
||||
using NBitcoin;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Linq;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
class Program
|
||||
{
|
||||
static void Main(string[] args)
|
||||
{
|
||||
ServicePointManager.DefaultConnectionLimit = 100;
|
||||
IWebHost host = null;
|
||||
try
|
||||
{
|
||||
var conf = new BTCPayServerOptions();
|
||||
conf.LoadArgs(new TextFileConfiguration(args));
|
||||
|
||||
host = new WebHostBuilder()
|
||||
.AddPayServer(conf)
|
||||
.UseKestrel()
|
||||
.UseIISIntegration()
|
||||
.UseContentRoot(Directory.GetCurrentDirectory())
|
||||
.ConfigureServices(services =>
|
||||
{
|
||||
services.AddLogging(l =>
|
||||
{
|
||||
l.AddFilter("Microsoft", LogLevel.Error);
|
||||
l.AddProvider(new CustomConsoleLogProvider());
|
||||
});
|
||||
})
|
||||
.UseStartup<Startup>()
|
||||
.Build();
|
||||
var running = host.RunAsync();
|
||||
OpenBrowser(conf.GetUrls().Select(url => url.Replace("0.0.0.0", "127.0.0.1")).First());
|
||||
running.GetAwaiter().GetResult();
|
||||
}
|
||||
catch(ConfigurationException ex)
|
||||
{
|
||||
if(!string.IsNullOrEmpty(ex.Message))
|
||||
Logs.Configuration.LogError(ex.Message);
|
||||
}
|
||||
catch(Exception exception)
|
||||
{
|
||||
Logs.PayServer.LogError("Exception thrown while running the server");
|
||||
Logs.PayServer.LogError(exception.ToString());
|
||||
}
|
||||
finally
|
||||
{
|
||||
if(host != null)
|
||||
host.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
public static void OpenBrowser(string url)
|
||||
{
|
||||
try
|
||||
{
|
||||
if(RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
Process.Start(new ProcessStartInfo("cmd", $"/c start {url}")); // Works ok on windows
|
||||
}
|
||||
else if(RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||
{
|
||||
Process.Start("xdg-open", url); // Works ok on linux
|
||||
}
|
||||
else if(RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
|
||||
{
|
||||
Process.Start("open", url); // Not tested
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
}
|
36
BTCPayServer/RateProvider/BitpayRateProvider.cs
Normal file
36
BTCPayServer/RateProvider/BitpayRateProvider.cs
Normal file
|
@ -0,0 +1,36 @@
|
|||
using NBitpayClient;
|
||||
using System.Linq;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.RateProvider
|
||||
{
|
||||
public class BitpayRateProvider : IRateProvider
|
||||
{
|
||||
Bitpay _Bitpay;
|
||||
public BitpayRateProvider(Bitpay bitpay)
|
||||
{
|
||||
if(bitpay == null)
|
||||
throw new ArgumentNullException(nameof(bitpay));
|
||||
_Bitpay = bitpay;
|
||||
}
|
||||
public async Task<decimal> GetRateAsync(string currency)
|
||||
{
|
||||
var rates = await _Bitpay.GetRatesAsync().ConfigureAwait(false);
|
||||
var rate = rates.GetRate(currency);
|
||||
if(rate == 0m)
|
||||
throw new RateUnavailableException(currency);
|
||||
return (decimal)rate;
|
||||
}
|
||||
|
||||
public async Task<ICollection<Rate>> GetRatesAsync()
|
||||
{
|
||||
return (await _Bitpay.GetRatesAsync().ConfigureAwait(false))
|
||||
.AllRates
|
||||
.Select(r => new Rate() { Currency = r.Code, Value = r.Value })
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
}
|
83
BTCPayServer/RateProvider/CurrencyNameTable.cs
Normal file
83
BTCPayServer/RateProvider/CurrencyNameTable.cs
Normal file
|
@ -0,0 +1,83 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace BTCPayServer.RateProvider
|
||||
{
|
||||
public class CurrencyData
|
||||
{
|
||||
public string Name
|
||||
{
|
||||
get;
|
||||
internal set;
|
||||
}
|
||||
public string Code
|
||||
{
|
||||
get;
|
||||
internal set;
|
||||
}
|
||||
public int Divisibility
|
||||
{
|
||||
get;
|
||||
internal set;
|
||||
}
|
||||
public string Symbol
|
||||
{
|
||||
get;
|
||||
internal set;
|
||||
}
|
||||
}
|
||||
public class CurrencyNameTable
|
||||
{
|
||||
public CurrencyNameTable()
|
||||
{
|
||||
_Currencies = LoadCurrency().ToDictionary(k => k.Code);
|
||||
}
|
||||
|
||||
|
||||
Dictionary<string, CurrencyData> _Currencies;
|
||||
|
||||
static CurrencyData[] LoadCurrency()
|
||||
{
|
||||
var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("BTCPayServer.Currencies.txt");
|
||||
string content = null;
|
||||
using(var reader = new StreamReader(stream, Encoding.UTF8))
|
||||
{
|
||||
content = reader.ReadToEnd();
|
||||
}
|
||||
var currencies = content.Split(new[] { "\r\n", "\n" }, StringSplitOptions.RemoveEmptyEntries);
|
||||
Dictionary<string, CurrencyData> dico = new Dictionary<string, CurrencyData>();
|
||||
foreach(var currency in currencies)
|
||||
{
|
||||
var splitted = currency.Split(new[] { '\t' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
if(splitted.Length < 3)
|
||||
continue;
|
||||
CurrencyData info = new CurrencyData();
|
||||
info.Name = splitted[0];
|
||||
info.Code = splitted[1];
|
||||
int divisibility;
|
||||
if(!int.TryParse(splitted[2], out divisibility))
|
||||
continue;
|
||||
info.Divisibility = divisibility;
|
||||
if(!dico.ContainsKey(info.Code))
|
||||
dico.Add(info.Code, info);
|
||||
if(splitted.Length >= 4)
|
||||
{
|
||||
info.Symbol = splitted[3];
|
||||
}
|
||||
}
|
||||
return dico.Values.ToArray();
|
||||
}
|
||||
|
||||
public CurrencyData GetCurrencyData(string currency)
|
||||
{
|
||||
CurrencyData result;
|
||||
_Currencies.TryGetValue(currency.ToUpperInvariant(), out result);
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
33
BTCPayServer/RateProvider/IRateProvider.cs
Normal file
33
BTCPayServer/RateProvider/IRateProvider.cs
Normal file
|
@ -0,0 +1,33 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.RateProvider
|
||||
{
|
||||
public class Rate
|
||||
{
|
||||
public Rate()
|
||||
{
|
||||
|
||||
}
|
||||
public Rate(string currency, decimal value)
|
||||
{
|
||||
Value = value;
|
||||
Currency = currency;
|
||||
}
|
||||
public string Currency
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public decimal Value
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
}
|
||||
public interface IRateProvider
|
||||
{
|
||||
Task<decimal> GetRateAsync(string currency);
|
||||
Task<ICollection<Rate>> GetRatesAsync();
|
||||
}
|
||||
}
|
35
BTCPayServer/RateProvider/MockRateProvider.cs
Normal file
35
BTCPayServer/RateProvider/MockRateProvider.cs
Normal file
|
@ -0,0 +1,35 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.RateProvider
|
||||
{
|
||||
public class MockRateProvider : IRateProvider
|
||||
{
|
||||
List<Rate> _Rates;
|
||||
|
||||
public MockRateProvider(params Rate[] rates)
|
||||
{
|
||||
_Rates = new List<Rate>(rates);
|
||||
}
|
||||
public MockRateProvider(List<Rate> rates)
|
||||
{
|
||||
_Rates = rates;
|
||||
}
|
||||
public Task<decimal> GetRateAsync(string currency)
|
||||
{
|
||||
var rate = _Rates.FirstOrDefault(r => r.Currency.Equals(currency, StringComparison.OrdinalIgnoreCase));
|
||||
if(rate == null)
|
||||
throw new RateUnavailableException(currency);
|
||||
return Task.FromResult(rate.Value);
|
||||
}
|
||||
|
||||
public Task<ICollection<Rate>> GetRatesAsync()
|
||||
{
|
||||
ICollection<Rate> rates = _Rates;
|
||||
return Task.FromResult(rates);
|
||||
}
|
||||
}
|
||||
}
|
21
BTCPayServer/RateProvider/RateUnavailableException.cs
Normal file
21
BTCPayServer/RateProvider/RateUnavailableException.cs
Normal file
|
@ -0,0 +1,21 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace BTCPayServer.RateProvider
|
||||
{
|
||||
public class RateUnavailableException : Exception
|
||||
{
|
||||
public RateUnavailableException(string currency) : base("Rate unavailable for currency " + currency)
|
||||
{
|
||||
if(currency == null)
|
||||
throw new ArgumentNullException(nameof(currency));
|
||||
Currency = currency;
|
||||
}
|
||||
|
||||
public string Currency
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
}
|
||||
}
|
17
BTCPayServer/Services/EmailSender.cs
Normal file
17
BTCPayServer/Services/EmailSender.cs
Normal file
|
@ -0,0 +1,17 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Services
|
||||
{
|
||||
// This class is used by the application to send email for account confirmation and password reset.
|
||||
// For more details see https://go.microsoft.com/fwlink/?LinkID=532713
|
||||
public class EmailSender : IEmailSender
|
||||
{
|
||||
public Task SendEmailAsync(string email, string subject, string message)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
12
BTCPayServer/Services/IEmailSender.cs
Normal file
12
BTCPayServer/Services/IEmailSender.cs
Normal file
|
@ -0,0 +1,12 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Services
|
||||
{
|
||||
public interface IEmailSender
|
||||
{
|
||||
Task SendEmailAsync(string email, string subject, string message);
|
||||
}
|
||||
}
|
60
BTCPayServer/Services/IFeeProvider.cs
Normal file
60
BTCPayServer/Services/IFeeProvider.cs
Normal file
|
@ -0,0 +1,60 @@
|
|||
using NBitcoin;
|
||||
using NBXplorer;
|
||||
using NBXplorer.Models;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Services
|
||||
{
|
||||
public interface IFeeProvider
|
||||
{
|
||||
Task<FeeRate> GetFeeRateAsync();
|
||||
}
|
||||
|
||||
public class NBXplorerFeeProvider : IFeeProvider
|
||||
{
|
||||
public ExplorerClient ExplorerClient
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public FeeRate Fallback
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public int BlockTarget
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public async Task<FeeRate> GetFeeRateAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
return (await ExplorerClient.GetFeeRateAsync(BlockTarget).ConfigureAwait(false)).FeeRate;
|
||||
}
|
||||
catch(NBXplorerException ex) when( ex.Error.HttpCode == 400 && ex.Error.Code == "fee-estimation-unavailable")
|
||||
{
|
||||
return Fallback;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class FixedFeeProvider : IFeeProvider
|
||||
{
|
||||
public FixedFeeProvider(FeeRate feeRate)
|
||||
{
|
||||
FeeRate = feeRate;
|
||||
}
|
||||
|
||||
public FeeRate FeeRate
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public Task<FeeRate> GetFeeRateAsync()
|
||||
{
|
||||
return Task.FromResult(FeeRate);
|
||||
}
|
||||
}
|
||||
}
|
70
BTCPayServer/Stores/StoreRepository.cs
Normal file
70
BTCPayServer/Stores/StoreRepository.cs
Normal file
|
@ -0,0 +1,70 @@
|
|||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Models;
|
||||
using NBitcoin;
|
||||
using NBitcoin.DataEncoders;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace BTCPayServer.Stores
|
||||
{
|
||||
public class StoreRepository
|
||||
{
|
||||
private ApplicationDbContextFactory _ContextFactory;
|
||||
public StoreRepository(ApplicationDbContextFactory contextFactory)
|
||||
{
|
||||
_ContextFactory = contextFactory ?? throw new ArgumentNullException(nameof(contextFactory));
|
||||
}
|
||||
|
||||
public async Task<StoreData> FindStore(string storeId)
|
||||
{
|
||||
using(var ctx = _ContextFactory.CreateContext())
|
||||
{
|
||||
return await ctx.FindAsync<StoreData>(storeId).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<StoreData> CreateStore(string userId)
|
||||
{
|
||||
using(var ctx = _ContextFactory.CreateContext())
|
||||
{
|
||||
StoreData store = new StoreData
|
||||
{
|
||||
Id = Encoders.Base58.EncodeData(RandomUtils.GetBytes(32))
|
||||
};
|
||||
var userStore = new UserStore
|
||||
{
|
||||
StoreDataId = store.Id,
|
||||
ApplicationUserId = userId
|
||||
};
|
||||
await ctx.AddAsync(store).ConfigureAwait(false);
|
||||
await ctx.AddAsync(userStore).ConfigureAwait(false);
|
||||
await ctx.SaveChangesAsync().ConfigureAwait(false);
|
||||
return store;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<StoreData> GetStore(string userId)
|
||||
{
|
||||
using(var ctx = _ContextFactory.CreateContext())
|
||||
{
|
||||
return await ctx
|
||||
.Stores
|
||||
.Where(s => s.UserStores.Any(us => us.ApplicationUserId == userId))
|
||||
.FirstOrDefaultAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task UpdateStore(StoreData store)
|
||||
{
|
||||
using(var ctx = _ContextFactory.CreateContext())
|
||||
{
|
||||
var existing = await ctx.FindAsync<StoreData>(store.Id);
|
||||
ctx.Entry(existing).CurrentValues.SetValues(store);
|
||||
await ctx.SaveChangesAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
31
BTCPayServer/Validations/ExtPubKeyValidator.cs
Normal file
31
BTCPayServer/Validations/ExtPubKeyValidator.cs
Normal file
|
@ -0,0 +1,31 @@
|
|||
using NBitcoin;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text;
|
||||
|
||||
namespace BTCPayServer.Validations
|
||||
{
|
||||
public class ExtPubKeyValidatorAttribute : ValidationAttribute
|
||||
{
|
||||
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
|
||||
{
|
||||
if(value == null)
|
||||
{
|
||||
return ValidationResult.Success;
|
||||
}
|
||||
var network = (Network)validationContext.GetService(typeof(Network));
|
||||
if(network == null)
|
||||
return new ValidationResult("No Network specified");
|
||||
try
|
||||
{
|
||||
new BitcoinExtPubKey((string)value, network);
|
||||
return ValidationResult.Success;
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
return new ValidationResult(ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
28
BTCPayServer/Validations/PubKeyValidator.cs
Normal file
28
BTCPayServer/Validations/PubKeyValidator.cs
Normal file
|
@ -0,0 +1,28 @@
|
|||
using NBitcoin;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text;
|
||||
|
||||
namespace BTCPayServer.Validations
|
||||
{
|
||||
public class PubKeyValidatorAttribute : ValidationAttribute
|
||||
{
|
||||
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
|
||||
{
|
||||
if(value == null)
|
||||
{
|
||||
return ValidationResult.Success;
|
||||
}
|
||||
try
|
||||
{
|
||||
new PubKey((string)value);
|
||||
return ValidationResult.Success;
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
return new ValidationResult(ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
8
BTCPayServer/Views/Account/AccessDenied.cshtml
Normal file
8
BTCPayServer/Views/Account/AccessDenied.cshtml
Normal file
|
@ -0,0 +1,8 @@
|
|||
@{
|
||||
ViewData["Title"] = "Access denied";
|
||||
}
|
||||
|
||||
<header>
|
||||
<h2 class="text-danger">ViewData["Title"]</h2>
|
||||
<p class="text-danger">You do not have access to this resource.</p>
|
||||
</header>
|
10
BTCPayServer/Views/Account/ConfirmEmail.cshtml
Normal file
10
BTCPayServer/Views/Account/ConfirmEmail.cshtml
Normal file
|
@ -0,0 +1,10 @@
|
|||
@{
|
||||
ViewData["Title"] = "Confirm email";
|
||||
}
|
||||
|
||||
<h2>@ViewData["Title"]</h2>
|
||||
<div>
|
||||
<p>
|
||||
Thank you for confirming your email.
|
||||
</p>
|
||||
</div>
|
32
BTCPayServer/Views/Account/ExternalLogin.cshtml
Normal file
32
BTCPayServer/Views/Account/ExternalLogin.cshtml
Normal file
|
@ -0,0 +1,32 @@
|
|||
@model ExternalLoginViewModel
|
||||
@{
|
||||
ViewData["Title"] = "Register";
|
||||
}
|
||||
|
||||
<h2>@ViewData["Title"]</h2>
|
||||
<h4>Associate your @ViewData["LoginProvider"] account.</h4>
|
||||
<hr />
|
||||
|
||||
<p class="text-info">
|
||||
You've successfully authenticated with <strong>@ViewData["LoginProvider"]</strong>.
|
||||
Please enter an email address for this site below and click the Register button to finish
|
||||
logging in.
|
||||
</p>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<form asp-action="ExternalLoginConfirmation" asp-route-returnurl="@ViewData["ReturnUrl"]" method="post">
|
||||
<div asp-validation-summary="All" class="text-danger"></div>
|
||||
<div class="form-group">
|
||||
<label asp-for="Email"></label>
|
||||
<input asp-for="Email" class="form-control" />
|
||||
<span asp-validation-for="Email" class="text-danger"></span>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-default">Register</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
@await Html.PartialAsync("_ValidationScriptsPartial")
|
||||
}
|
25
BTCPayServer/Views/Account/ForgotPassword.cshtml
Normal file
25
BTCPayServer/Views/Account/ForgotPassword.cshtml
Normal file
|
@ -0,0 +1,25 @@
|
|||
@model ForgotPasswordViewModel
|
||||
@{
|
||||
ViewData["Title"] = "Forgot your password?";
|
||||
}
|
||||
|
||||
<h2>@ViewData["Title"]</h2>
|
||||
<h4>Enter your email.</h4>
|
||||
<hr />
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<form asp-action="ForgotPassword" method="post">
|
||||
<div asp-validation-summary="All" class="text-danger"></div>
|
||||
<div class="form-group">
|
||||
<label asp-for="Email"></label>
|
||||
<input asp-for="Email" class="form-control" />
|
||||
<span asp-validation-for="Email" class="text-danger"></span>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-default">Submit</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
@await Html.PartialAsync("_ValidationScriptsPartial")
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
@{
|
||||
ViewData["Title"] = "Forgot password confirmation";
|
||||
}
|
||||
|
||||
<h2>@ViewData["Title"]</h2>
|
||||
<p>
|
||||
Please check your email to reset your password.
|
||||
</p>
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue