2023-06-23 15:10:44 +02:00
|
|
|
|
#nullable enable
|
2024-02-20 11:47:03 +01:00
|
|
|
|
using System;
|
2023-06-23 15:10:44 +02:00
|
|
|
|
using System.Linq;
|
|
|
|
|
using System.Threading.Tasks;
|
2023-07-03 09:56:00 +02:00
|
|
|
|
using BTCPayApp.CommonServer;
|
2024-02-20 11:47:03 +01:00
|
|
|
|
using BTCPayServer.Abstractions.Constants;
|
|
|
|
|
using BTCPayServer.Abstractions.Extensions;
|
2023-06-23 15:10:44 +02:00
|
|
|
|
using BTCPayServer.Client;
|
2024-02-20 11:47:03 +01:00
|
|
|
|
using BTCPayServer.Common;
|
|
|
|
|
using BTCPayServer.Controllers;
|
2023-06-23 15:10:44 +02:00
|
|
|
|
using BTCPayServer.Data;
|
2024-02-20 11:47:03 +01:00
|
|
|
|
using BTCPayServer.Events;
|
2023-06-23 15:10:44 +02:00
|
|
|
|
using BTCPayServer.Security.Greenfield;
|
2024-02-20 11:47:03 +01:00
|
|
|
|
using BTCPayServer.Services;
|
2024-04-04 11:24:56 +02:00
|
|
|
|
using BTCPayServer.Services.Invoices;
|
2023-06-23 15:10:44 +02:00
|
|
|
|
using BTCPayServer.Services.Stores;
|
2024-02-20 11:47:03 +01:00
|
|
|
|
using Microsoft.AspNetCore.Authentication.BearerToken;
|
|
|
|
|
using Microsoft.AspNetCore.Authorization;
|
|
|
|
|
using Microsoft.AspNetCore.Http;
|
|
|
|
|
using Microsoft.AspNetCore.Http.HttpResults;
|
|
|
|
|
using Microsoft.AspNetCore.Identity;
|
|
|
|
|
using Microsoft.AspNetCore.Identity.Data;
|
2023-06-23 15:10:44 +02:00
|
|
|
|
using Microsoft.AspNetCore.Mvc;
|
2024-02-20 11:47:03 +01:00
|
|
|
|
using Microsoft.Extensions.Options;
|
2023-06-23 15:10:44 +02:00
|
|
|
|
using NBitcoin;
|
|
|
|
|
using NBitcoin.DataEncoders;
|
|
|
|
|
using NBXplorer;
|
2024-02-20 11:47:03 +01:00
|
|
|
|
using NicolasDorier.RateLimits;
|
2023-06-23 15:10:44 +02:00
|
|
|
|
|
2024-02-20 11:47:03 +01:00
|
|
|
|
namespace BTCPayServer.App;
|
2023-06-23 15:10:44 +02:00
|
|
|
|
|
2024-02-20 11:47:03 +01:00
|
|
|
|
[ApiController]
|
|
|
|
|
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Bearer)]
|
2023-06-23 15:10:44 +02:00
|
|
|
|
[Route("btcpayapp")]
|
2024-02-20 11:47:03 +01:00
|
|
|
|
public class BtcPayAppController(
|
|
|
|
|
BtcPayAppService appService,
|
|
|
|
|
APIKeyRepository apiKeyRepository,
|
|
|
|
|
StoreRepository storeRepository,
|
|
|
|
|
BTCPayNetworkProvider btcPayNetworkProvider,
|
|
|
|
|
IExplorerClientProvider explorerClientProvider,
|
|
|
|
|
EventAggregator eventAggregator,
|
|
|
|
|
SignInManager<ApplicationUser> signInManager,
|
|
|
|
|
UserManager<ApplicationUser> userManager,
|
|
|
|
|
TimeProvider timeProvider,
|
2024-04-04 11:24:56 +02:00
|
|
|
|
PaymentMethodHandlerDictionary handlers,
|
2024-02-20 11:47:03 +01:00
|
|
|
|
IOptionsMonitor<BearerTokenOptions> bearerTokenOptions)
|
|
|
|
|
: Controller
|
2023-06-23 15:10:44 +02:00
|
|
|
|
{
|
2024-02-20 11:47:03 +01:00
|
|
|
|
[AllowAnonymous]
|
|
|
|
|
[HttpPost("login")]
|
|
|
|
|
[RateLimitsFilter(ZoneLimits.Login, Scope = RateLimitsScope.RemoteAddress)]
|
|
|
|
|
public async Task<Results<Ok<AccessTokenResponse>, EmptyHttpResult, ProblemHttpResult>> Login(LoginRequest login)
|
2023-06-23 15:10:44 +02:00
|
|
|
|
{
|
2024-02-20 11:47:03 +01:00
|
|
|
|
const string errorMessage = "Invalid login attempt.";
|
|
|
|
|
if (ModelState.IsValid)
|
|
|
|
|
{
|
|
|
|
|
// Require the user to pass basic checks (approval, confirmed email, not disabled) before they can log on
|
|
|
|
|
var user = await userManager.FindByEmailAsync(login.Email);
|
|
|
|
|
if (!UserService.TryCanLogin(user, out var message))
|
|
|
|
|
{
|
|
|
|
|
return TypedResults.Problem(message, statusCode: 401);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
signInManager.AuthenticationScheme = AuthenticationSchemes.Bearer;
|
|
|
|
|
var signInResult = await signInManager.PasswordSignInAsync(login.Email, login.Password, true, true);
|
|
|
|
|
if (signInResult.RequiresTwoFactor)
|
|
|
|
|
{
|
|
|
|
|
if (!string.IsNullOrEmpty(login.TwoFactorCode))
|
|
|
|
|
signInResult = await signInManager.TwoFactorAuthenticatorSignInAsync(login.TwoFactorCode, true, true);
|
|
|
|
|
else if (!string.IsNullOrEmpty(login.TwoFactorRecoveryCode))
|
|
|
|
|
signInResult = await signInManager.TwoFactorRecoveryCodeSignInAsync(login.TwoFactorRecoveryCode);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TODO: Add FIDO and LNURL Auth
|
|
|
|
|
|
|
|
|
|
return signInResult.Succeeded
|
|
|
|
|
? TypedResults.Empty
|
|
|
|
|
: TypedResults.Problem(signInResult.ToString(), statusCode: 401);
|
|
|
|
|
}
|
|
|
|
|
return TypedResults.Problem(errorMessage, statusCode: 401);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[AllowAnonymous]
|
|
|
|
|
[HttpPost("refresh")]
|
|
|
|
|
[RateLimitsFilter(ZoneLimits.Login, Scope = RateLimitsScope.RemoteAddress)]
|
|
|
|
|
public async Task<Results<Ok<AccessTokenResponse>, UnauthorizedHttpResult, SignInHttpResult, ChallengeHttpResult>> Refresh(RefreshRequest refresh)
|
|
|
|
|
{
|
|
|
|
|
const string scheme = AuthenticationSchemes.Bearer;
|
|
|
|
|
var authenticationTicket = bearerTokenOptions.Get(scheme).RefreshTokenProtector.Unprotect(refresh.RefreshToken);
|
|
|
|
|
var expiresUtc = authenticationTicket?.Properties.ExpiresUtc;
|
|
|
|
|
|
|
|
|
|
ApplicationUser? user = null;
|
|
|
|
|
int num;
|
|
|
|
|
if (expiresUtc.HasValue)
|
|
|
|
|
{
|
|
|
|
|
DateTimeOffset valueOrDefault = expiresUtc.GetValueOrDefault();
|
|
|
|
|
num = timeProvider.GetUtcNow() >= valueOrDefault ? 1 : 0;
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
num = 1;
|
|
|
|
|
bool flag = num != 0;
|
|
|
|
|
if (!flag)
|
|
|
|
|
{
|
|
|
|
|
signInManager.AuthenticationScheme = scheme;
|
|
|
|
|
user = await signInManager.ValidateSecurityStampAsync(authenticationTicket?.Principal);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return user != null
|
|
|
|
|
? TypedResults.SignIn(await signInManager.CreateUserPrincipalAsync(user), authenticationScheme: scheme)
|
|
|
|
|
: TypedResults.Challenge(authenticationSchemes: new[] { scheme });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[HttpPost("logout")]
|
|
|
|
|
public async Task<IResult> Logout()
|
|
|
|
|
{
|
|
|
|
|
var user = await userManager.GetUserAsync(User);
|
|
|
|
|
if (user != null)
|
|
|
|
|
{
|
|
|
|
|
await signInManager.SignOutAsync();
|
|
|
|
|
return Results.Ok();
|
|
|
|
|
}
|
|
|
|
|
return Results.Unauthorized();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[HttpGet("info")]
|
2024-04-09 20:45:21 +02:00
|
|
|
|
public async Task<Results<Ok<AppUserInfo>, ValidationProblem, NotFound>> Info()
|
2024-02-20 11:47:03 +01:00
|
|
|
|
{
|
|
|
|
|
var user = await userManager.GetUserAsync(User);
|
|
|
|
|
if (user == null) return TypedResults.NotFound();
|
|
|
|
|
|
|
|
|
|
var userStores = await storeRepository.GetStoresByUserId(user.Id);
|
2024-04-09 20:45:21 +02:00
|
|
|
|
return TypedResults.Ok(new AppUserInfo
|
2024-02-20 11:47:03 +01:00
|
|
|
|
{
|
|
|
|
|
UserId = user.Id,
|
|
|
|
|
Email = await userManager.GetEmailAsync(user),
|
|
|
|
|
Roles = await userManager.GetRolesAsync(user),
|
|
|
|
|
Stores = (from store in userStores
|
|
|
|
|
let userStore = store.UserStores.Find(us => us.ApplicationUserId == user.Id && us.StoreDataId == store.Id)!
|
|
|
|
|
select new AppUserStoreInfo
|
|
|
|
|
{
|
|
|
|
|
Id = store.Id,
|
|
|
|
|
Name = store.StoreName,
|
|
|
|
|
Archived = store.Archived,
|
|
|
|
|
RoleId = userStore.StoreRole.Id,
|
|
|
|
|
Permissions = userStore.StoreRole.Permissions
|
|
|
|
|
}).ToList()
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[AllowAnonymous]
|
|
|
|
|
[HttpPost("forgot-password")]
|
|
|
|
|
[RateLimitsFilter(ZoneLimits.ForgotPassword, Scope = RateLimitsScope.RemoteAddress)]
|
|
|
|
|
public async Task<IResult> ForgotPassword(ResetPasswordRequest resetRequest)
|
|
|
|
|
{
|
|
|
|
|
var user = await userManager.FindByEmailAsync(resetRequest.Email);
|
|
|
|
|
if (UserService.TryCanLogin(user, out _))
|
|
|
|
|
{
|
|
|
|
|
eventAggregator.Publish(new UserPasswordResetRequestedEvent
|
|
|
|
|
{
|
|
|
|
|
User = user,
|
|
|
|
|
RequestUri = Request.GetAbsoluteRootUri()
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
return TypedResults.Ok();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[AllowAnonymous]
|
|
|
|
|
[HttpPost("reset-password")]
|
|
|
|
|
public async Task<IResult> SetPassword(ResetPasswordRequest resetRequest)
|
|
|
|
|
{
|
|
|
|
|
var user = await userManager.FindByEmailAsync(resetRequest.Email);
|
|
|
|
|
if (!UserService.TryCanLogin(user, out _))
|
|
|
|
|
{
|
|
|
|
|
return TypedResults.Problem("Invalid account", statusCode: 401);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
IdentityResult result;
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
result = await userManager.ResetPasswordAsync(user, resetRequest.ResetCode, resetRequest.NewPassword);
|
|
|
|
|
}
|
|
|
|
|
catch (FormatException)
|
|
|
|
|
{
|
|
|
|
|
result = IdentityResult.Failed(userManager.ErrorDescriber.InvalidToken());
|
|
|
|
|
}
|
|
|
|
|
return result.Succeeded ? TypedResults.Ok() : TypedResults.Problem(result.ToString().Split(": ").Last(), statusCode: 401);
|
2023-06-23 15:10:44 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[HttpGet("pair/{code}")]
|
|
|
|
|
public async Task<IActionResult> StartPair(string code)
|
|
|
|
|
{
|
2024-02-20 11:47:03 +01:00
|
|
|
|
var res = appService.ConsumePairingCode(code);
|
2023-06-23 15:10:44 +02:00
|
|
|
|
if (res is null)
|
|
|
|
|
{
|
|
|
|
|
return Unauthorized();
|
|
|
|
|
}
|
|
|
|
|
|
2023-07-21 15:35:13 +02:00
|
|
|
|
StoreData? store = null;
|
|
|
|
|
if (res.StoreId is not null)
|
2023-06-23 15:10:44 +02:00
|
|
|
|
{
|
2024-02-20 11:47:03 +01:00
|
|
|
|
store = await storeRepository.FindStore(res.StoreId, res.UserId);
|
2023-07-21 15:35:13 +02:00
|
|
|
|
if (store is null)
|
|
|
|
|
{
|
|
|
|
|
return NotFound();
|
|
|
|
|
}
|
2023-06-23 15:10:44 +02:00
|
|
|
|
}
|
2023-07-21 15:35:13 +02:00
|
|
|
|
|
2024-04-04 11:24:56 +02:00
|
|
|
|
var key = new APIKeyData
|
2023-06-23 15:10:44 +02:00
|
|
|
|
{
|
|
|
|
|
Id = Encoders.Hex.EncodeData(RandomUtils.GetBytes(20)),
|
|
|
|
|
Type = APIKeyType.Permanent,
|
|
|
|
|
UserId = res.UserId,
|
|
|
|
|
Label = "BTCPay App Pairing"
|
|
|
|
|
};
|
2024-04-04 11:24:56 +02:00
|
|
|
|
key.SetBlob(new APIKeyBlob {Permissions = [Policies.Unrestricted] });
|
2024-02-20 11:47:03 +01:00
|
|
|
|
await apiKeyRepository.CreateKey(key);
|
2024-04-04 11:24:56 +02:00
|
|
|
|
|
|
|
|
|
var onchain = store?.GetDerivationSchemeSettings(handlers, "BTC");
|
2023-06-23 15:10:44 +02:00
|
|
|
|
string? onchainSeed = null;
|
|
|
|
|
if (onchain is not null)
|
|
|
|
|
{
|
2024-02-20 11:47:03 +01:00
|
|
|
|
var explorerClient = explorerClientProvider.GetExplorerClient("BTC");
|
2023-06-23 15:10:44 +02:00
|
|
|
|
onchainSeed = await GetSeed(explorerClient, onchain);
|
|
|
|
|
}
|
|
|
|
|
|
2024-04-04 11:24:56 +02:00
|
|
|
|
var nBitcoinNetwork = btcPayNetworkProvider.GetNetwork<BTCPayNetwork>("BTC").NBitcoinNetwork;
|
|
|
|
|
return Ok(new PairSuccessResult
|
2023-06-23 15:10:44 +02:00
|
|
|
|
{
|
|
|
|
|
Key = key.Id,
|
2023-07-21 15:35:13 +02:00
|
|
|
|
StoreId = store?.Id,
|
2023-06-23 15:10:44 +02:00
|
|
|
|
UserId = res.UserId,
|
2024-04-04 11:24:56 +02:00
|
|
|
|
ExistingWallet = onchain?.AccountDerivation?.GetExtPubKeys()?.FirstOrDefault()?.ToString(nBitcoinNetwork),
|
2023-06-28 13:15:50 +02:00
|
|
|
|
ExistingWalletSeed = onchainSeed,
|
2024-04-04 11:24:56 +02:00
|
|
|
|
Network = nBitcoinNetwork.Name
|
2023-06-23 15:10:44 +02:00
|
|
|
|
});
|
|
|
|
|
}
|
2023-07-03 09:56:00 +02:00
|
|
|
|
|
2023-06-23 15:10:44 +02:00
|
|
|
|
private async Task<string?> GetSeed(ExplorerClient client, DerivationSchemeSettings derivation)
|
|
|
|
|
{
|
|
|
|
|
return derivation.IsHotWallet &&
|
2023-07-03 09:56:00 +02:00
|
|
|
|
await client.GetMetadataAsync<string>(derivation.AccountDerivation, WellknownMetadataKeys.Mnemonic) is
|
|
|
|
|
{ } seed &&
|
|
|
|
|
!string.IsNullOrEmpty(seed)
|
|
|
|
|
? seed
|
|
|
|
|
: null;
|
2023-06-23 15:10:44 +02:00
|
|
|
|
}
|
|
|
|
|
}
|