login user with pos qr code and set to cashier mode - issue #212

This commit is contained in:
Ghander 2025-06-06 08:40:35 -05:00
parent ccd160c4e0
commit 4dbab4038c
11 changed files with 170 additions and 45 deletions

View File

@ -55,11 +55,11 @@ public class AuthStateProvider(
}
}
public BTCPayAppClient GetClient(string? baseUri = null)
public BTCPayAppClient GetClient(string? baseUri = null, string? token = null)
{
if (string.IsNullOrEmpty(baseUri) && string.IsNullOrEmpty(Account?.BaseUri))
throw new ArgumentException("No base URI present or provided.", nameof(baseUri));
var token = Account?.ModeToken ?? Account?.OwnerToken;
token ??= Account?.ModeToken ?? Account?.OwnerToken;
return new BTCPayAppClient(baseUri ?? Account!.BaseUri, token, clientFactory.CreateClient());
}
@ -199,7 +199,7 @@ public class AuthStateProvider(
await CheckAuthenticated(true);
store = GetUserStore(store.Id)!;
}
catch (Exception)
catch (Exception ex)
{
// ignored
}
@ -278,13 +278,19 @@ public class AuthStateProvider(
}
}
public async Task<FormResult> LoginWithCode(string serverUrl, string email, string code, CancellationToken? cancellation = default)
public async Task<FormResult> LoginWithCode(string serverUrl, string? email, string code, CancellationToken? cancellation = default)
{
try
{
var client = GetClient(serverUrl);
var response = await client.Login(code, cancellation.GetValueOrDefault());
if (string.IsNullOrEmpty(response.AccessToken)) throw new Exception("Did not obtain valid API token.");
if (string.IsNullOrEmpty(email))
{
var clientWithToken = GetClient(serverUrl, response.AccessToken);
var userInfo = await clientWithToken.GetUserInfo();
email = userInfo?.Email!;
}
var account = new BTCPayAccount(serverUrl, email, response.AccessToken);
await SetAccount(account);
return new FormResult(true);

View File

@ -9,7 +9,7 @@ public interface IAccountManager
public BTCPayAccount? Account { get; }
public AppUserInfo? UserInfo { get; }
public AppUserStoreInfo? CurrentStore { get; }
public BTCPayAppClient GetClient(string? baseUri = null);
public BTCPayAppClient GetClient(string? baseUri = null, string? token = null);
public Task<string?> GetEncryptionKey();
public Task SetEncryptionKey(string value);
public Task<bool> CheckAuthenticated(bool refreshUser = false);
@ -18,7 +18,7 @@ public interface IAccountManager
public Task<FormResult<AcceptInviteResult>> AcceptInvite(string inviteUrl, CancellationToken? cancellation = default);
public Task<FormResult<LoginInfoResult>> LoginInfo(string serverUrl, string email, CancellationToken? cancellation = default);
public Task<FormResult> Login(string serverUrl, string email, string password, string? otp, CancellationToken? cancellation = default);
public Task<FormResult> LoginWithCode(string serverUrl, string email, string code, CancellationToken? cancellation = default);
public Task<FormResult> LoginWithCode(string serverUrl, string? email, string code, CancellationToken? cancellation = default);
public Task<FormResult> Register(string serverUrl, string email, string password, CancellationToken? cancellation = default);
public Task<FormResult> ResetPassword(string serverUrl, string email, string? resetCode, string? newPassword, CancellationToken? cancellation = default);
public Task<FormResult<ApplicationUserData>> ChangePassword(string currentPassword, string newPassword, CancellationToken? cancellation = default);

View File

@ -5,4 +5,5 @@ public static class Constants
public const string LoginCodeSeparator = ";";
public const string EncryptionKeySeparator = "*";
public const string InviteSeparator = "/invite/";
public const string POSQRLoginSeparator = "loginCode";
}

View File

@ -35,7 +35,7 @@
{
<LightningNodeStateAlert NodeState="State.Value.LightningNodeState" ConnectionState="State.Value.ConnectionState" />
}
@if (State.Value.LightningNodeState is LightningNodeState.NotConfigured)
@if (State.Value.LightningNodeState is LightningNodeState.NotConfigured && AppSettings.AllowWalletGeneration)
{
<button class="btn btn-primary" @onclick="ConfigureLNWallet">Configure Lightning Wallet</button>
}

View File

@ -390,22 +390,22 @@
</li>
</ul>
</div>
<h2>Logout</h2>
<div class="box">
@switch (State.Value.ConnectionState)
{
case BTCPayConnectionState.ConnectedAsPrimary:
<p>This device is currently connected as the primary device for communication with the BTCPay Server.</p>
<p>Please note that when you sign out of the account on this device, your Lightning node will go offline, and you will not be able to receive any payments.</p>
break;
case BTCPayConnectionState.ConnectedAsSecondary:
<p>This device is currently connected as an additional device for communication with the BTCPay Server.</p>
<p>Please ensure that your primary device is still connected to the BTCPay Server, because otherwise you will not be able to receive any payments.</p>
break;
}
<button class="btn btn-outline-danger w-100 mt-2" type="button" @onclick="Logout">Logout</button>
</div>
</AuthorizeView>
<h2>Logout</h2>
<div class="box">
@switch (State.Value.ConnectionState)
{
case BTCPayConnectionState.ConnectedAsPrimary:
<p>This device is currently connected as the primary device for communication with the BTCPay Server.</p>
<p>Please note that when you sign out of the account on this device, your Lightning node will go offline, and you will not be able to receive any payments.</p>
break;
case BTCPayConnectionState.ConnectedAsSecondary:
<p>This device is currently connected as an additional device for communication with the BTCPay Server.</p>
<p>Please ensure that your primary device is still connected to the BTCPay Server, because otherwise you will not be able to receive any payments.</p>
break;
}
<button class="btn btn-outline-danger w-100 mt-2" type="button" @onclick="Logout">Logout</button>
</div>
</section>
}

View File

@ -8,6 +8,7 @@
@using BTCPayApp.Core.Models
@using BTCPayApp.UI.Features
@using Microsoft.Extensions.Logging
@using System.Web
@inject IJSRuntime JS
@inject IAccountManager AccountManager
@inject BTCPayConnectionManager ConnectionManager
@ -93,6 +94,10 @@
{
return await HandleLoginCode(urlOrLoginCode);
}
if (urlOrLoginCode.Contains(Constants.POSQRLoginSeparator))
{
return await HandlePOSQRLoginCode(urlOrLoginCode);
}
if (urlOrLoginCode.Contains(Constants.EncryptionKeySeparator))
{
return await HandleEncryptionKey(urlOrLoginCode);
@ -160,6 +165,42 @@
return false;
}
private async Task<bool> HandlePOSQRLoginCode(string url)
{
var uri = new Uri(url);
var baseUrl = uri.GetLeftPart(UriPartial.Authority);
var queryParams = HttpUtility.ParseQueryString(new Uri(url).Query);
if (queryParams is not null)
{
var loginCode = queryParams[Constants.POSQRLoginSeparator];
if(string.IsNullOrEmpty(loginCode))
{
ErrorMessage = "Invalid employee login code";
StateHasChanged();
return false;
}
ErrorMessage = null;
Sending = true;
StateHasChanged();
var result = await AccountManager.LoginWithCode(baseUrl, null, loginCode);
Sending = false;
if (result.Succeeded) return true;
ErrorMessage = result.Messages?.Contains("Failed") is false
? string.Join(",", result.Messages)
: "Invalid login attempt.";
}
else
{
ErrorMessage = "Invalid login code";
}
StateHasChanged();
return false;
}
private async Task<bool> HandleEncryptionKey(string code)
{
var parts = code.Split(Constants.EncryptionKeySeparator);
@ -206,25 +247,29 @@
switch (ConnectionManager.ConnectionState)
{
case BTCPayConnectionState.Connecting or BTCPayConnectionState.Syncing:
{
route = Routes.Index;
break;
}
{
route = Routes.Index;
break;
}
case BTCPayConnectionState.WaitingForEncryptionKey:
route = Routes.Pairing;
break;
default:
{
if (storeId == null)
{
route = AccountManager.UserInfo?.Stores?.Any() is true
? Routes.SelectStore
: Routes.CreateStore;
}
else if (current == Routes.Index)
{
route = Routes.PointOfSale;
}
if (storeId == null)
{
route = AccountManager.UserInfo?.Stores?.Any() is true
? Routes.SelectStore
: Routes.CreateStore;
}
else if (current == Routes.Index)
{
route = Routes.PointOfSale;
if(AccountManager.CurrentStore?.RoleId == "Employee")
{
await AccountManager.SwitchMode(storeId, "Cashier");
}
}
else
{
// do not redirect if the user is already on the correct page

View File

@ -139,7 +139,7 @@
{
Config = await OnChainWalletManager.GetConfig();
Derivation = Config?.Derivations.FirstOrDefault(d => d.Key == WalletDerivation.NativeSegwit).Value;
_canConfigureWallet = await OnChainWalletManager.CanConfigureWallet();
_canConfigureWallet = AppSettings.AllowWalletGeneration && await OnChainWalletManager.CanConfigureWallet();
}
private async Task SetStorePaymentMethod()

View File

@ -27,3 +27,11 @@ public static class StartupExtensions
return serviceCollection;
}
}
public static class AppSettings
{
public const bool AutoGenerateWallets = false;
public const bool AllowWalletGeneration = false;
}

View File

@ -22,6 +22,7 @@ public class StateMiddleware(
IDispatcher _dispatcher)
: Middleware
{
public const string UiStateConfigKey = "uistate";
private CancellationTokenSource? _ratesCts;
private bool _previouslyConnected;
@ -69,9 +70,11 @@ public class StateMiddleware(
dispatcher.Dispatch(new RootState.ConnectionStateUpdatedAction(btcPayConnectionManager.ConnectionState));
// initial wallet generation
if (onChainWalletManager is { State: OnChainWalletState.NotConfigured } && await onChainWalletManager.CanConfigureWallet())
if (AppSettings.AutoGenerateWallets &&
onChainWalletManager is { State: OnChainWalletState.NotConfigured } &&
await onChainWalletManager.CanConfigureWallet())
{
await onChainWalletManager.Generate();
await onChainWalletManager.Generate();
}
// refresh after returning from the background
@ -100,8 +103,8 @@ public class StateMiddleware(
_dispatcher.Dispatch(new StoreState.FetchOnchainHistogram(store.Id));
}
break;
case OnChainWalletState.NotConfigured when await onChainWalletManager.CanConfigureWallet():
await onChainWalletManager.Generate();
case OnChainWalletState.NotConfigured when await onChainWalletManager.CanConfigureWallet() && AppSettings.AutoGenerateWallets:
await onChainWalletManager.Generate();
break;
}
}
@ -119,11 +122,13 @@ public class StateMiddleware(
lightningNodeService.StateChanged += async (_, _) =>
{
dispatcher.Dispatch(new RootState.LightningNodeStateUpdatedAction(lightningNodeService.State));
if (lightningNodeService is {State: LightningNodeState.NotConfigured} && await lightningNodeService.CanConfigureLightningNode())
if (lightningNodeService is {State: LightningNodeState.NotConfigured} &&
await lightningNodeService.CanConfigureLightningNode() &&
AppSettings.AutoGenerateWallets)
{
try
{
await lightningNodeService.Generate();
// await lightningNodeService.Generate();
}
catch (Exception ex)
{
@ -241,7 +246,9 @@ public class StateMiddleware(
_ = RefreshRates(dispatcher, _ratesCts.Token);
// initial wallet generation
if (onChainWalletManager is { State: OnChainWalletState.NotConfigured } && await onChainWalletManager.CanConfigureWallet())
if (onChainWalletManager is { State: OnChainWalletState.NotConfigured } &&
await onChainWalletManager.CanConfigureWallet() &&
AppSettings.AutoGenerateWallets)
{
await onChainWalletManager.Generate();
}

58
setup.ps1 Normal file
View File

@ -0,0 +1,58 @@
# Check if not in a CI environment
if (-not (Test-Path Env:CI)) {
# Initialize the server submodule
Write-Host "Initializing and updating submodules..."
git submodule init
if ($LASTEXITCODE -eq 0) {
git submodule update --recursive
} else {
Write-Error "git submodule init failed."
exit 1
}
if ($LASTEXITCODE -ne 0) {
Write-Error "git submodule update --recursive failed."
exit 1
}
# Install the workloads
Write-Host "Restoring dotnet workloads..."
dotnet workload restore
if ($LASTEXITCODE -ne 0) {
Write-Error "dotnet workload restore failed."
exit 1
}
}
# Create appsettings file to include app plugin when running the server
$appsettings = "submodules/btcpayserver/BTCPayServer/appsettings.dev.json"
if (-not (Test-Path $appsettings -PathType Leaf)) {
Write-Host "Creating $appsettings..."
$content = '{ "DEBUG_PLUGINS": "../../../BTCPayServer.Plugins.App/bin/Debug/net8.0/BTCPayServer.Plugins.App.dll" }'
Set-Content -Path $appsettings -Value $content -Encoding UTF8
}
# Publish plugin to share its dependencies with the server
$originalLocation = Get-Location
$pluginDir = "BTCPayServer.Plugins.App"
if (Test-Path $pluginDir) {
Write-Host "Changing directory to $pluginDir..."
Set-Location $pluginDir
Write-Host "Publishing plugin..."
dotnet publish -c Debug -o "bin/Debug/net8.0"
if ($LASTEXITCODE -ne 0) {
Write-Error "dotnet publish failed."
Set-Location $originalLocation # Ensure we return to original location on error
exit 1
}
Write-Host "Returning to original directory..."
Set-Location $originalLocation
} else {
Write-Error "Plugin directory $pluginDir not found."
exit 1
}
Write-Host "Setup complete."

@ -1 +1 @@
Subproject commit 21d609aab09b6a24e49d509104839b3cc45d7760
Subproject commit c6f960cef510932ce2abf9bc098f7e0ba3714428