btcpayserver/BTCPayServer/App/BtcPayAppController.cs

248 lines
9.1 KiB
C#
Raw Normal View History

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