diff --git a/BTCPayApp.Core/Auth/AuthStateProvider.cs b/BTCPayApp.Core/Auth/AuthStateProvider.cs index 5885726..c9ee87c 100644 --- a/BTCPayApp.Core/Auth/AuthStateProvider.cs +++ b/BTCPayApp.Core/Auth/AuthStateProvider.cs @@ -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 LoginWithCode(string serverUrl, string email, string code, CancellationToken? cancellation = default) + public async Task 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); diff --git a/BTCPayApp.Core/Auth/IAccountManager.cs b/BTCPayApp.Core/Auth/IAccountManager.cs index 4317695..b8cecec 100644 --- a/BTCPayApp.Core/Auth/IAccountManager.cs +++ b/BTCPayApp.Core/Auth/IAccountManager.cs @@ -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 GetEncryptionKey(); public Task SetEncryptionKey(string value); public Task CheckAuthenticated(bool refreshUser = false); @@ -18,7 +18,7 @@ public interface IAccountManager public Task> AcceptInvite(string inviteUrl, CancellationToken? cancellation = default); public Task> LoginInfo(string serverUrl, string email, CancellationToken? cancellation = default); public Task Login(string serverUrl, string email, string password, string? otp, CancellationToken? cancellation = default); - public Task LoginWithCode(string serverUrl, string email, string code, CancellationToken? cancellation = default); + public Task LoginWithCode(string serverUrl, string? email, string code, CancellationToken? cancellation = default); public Task Register(string serverUrl, string email, string password, CancellationToken? cancellation = default); public Task ResetPassword(string serverUrl, string email, string? resetCode, string? newPassword, CancellationToken? cancellation = default); public Task> ChangePassword(string currentPassword, string newPassword, CancellationToken? cancellation = default); diff --git a/BTCPayApp.Core/Constants.cs b/BTCPayApp.Core/Constants.cs index 64b703f..67a9aa9 100644 --- a/BTCPayApp.Core/Constants.cs +++ b/BTCPayApp.Core/Constants.cs @@ -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"; } diff --git a/BTCPayApp.UI/Pages/Lightning/SettingsPage.razor b/BTCPayApp.UI/Pages/Lightning/SettingsPage.razor index 63f9517..fa94702 100644 --- a/BTCPayApp.UI/Pages/Lightning/SettingsPage.razor +++ b/BTCPayApp.UI/Pages/Lightning/SettingsPage.razor @@ -35,7 +35,7 @@ { } - @if (State.Value.LightningNodeState is LightningNodeState.NotConfigured) + @if (State.Value.LightningNodeState is LightningNodeState.NotConfigured && AppSettings.AllowWalletGeneration) { } diff --git a/BTCPayApp.UI/Pages/Settings/IndexPage.razor b/BTCPayApp.UI/Pages/Settings/IndexPage.razor index 4fff1fd..0a7533e 100644 --- a/BTCPayApp.UI/Pages/Settings/IndexPage.razor +++ b/BTCPayApp.UI/Pages/Settings/IndexPage.razor @@ -390,22 +390,22 @@ -

Logout

-
- @switch (State.Value.ConnectionState) - { - case BTCPayConnectionState.ConnectedAsPrimary: -

This device is currently connected as the primary device for communication with the BTCPay Server.

-

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.

- break; - case BTCPayConnectionState.ConnectedAsSecondary: -

This device is currently connected as an additional device for communication with the BTCPay Server.

-

Please ensure that your primary device is still connected to the BTCPay Server, because otherwise you will not be able to receive any payments.

- break; - } - -
+

Logout

+
+ @switch (State.Value.ConnectionState) + { + case BTCPayConnectionState.ConnectedAsPrimary: +

This device is currently connected as the primary device for communication with the BTCPay Server.

+

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.

+ break; + case BTCPayConnectionState.ConnectedAsSecondary: +

This device is currently connected as an additional device for communication with the BTCPay Server.

+

Please ensure that your primary device is still connected to the BTCPay Server, because otherwise you will not be able to receive any payments.

+ break; + } + +
} diff --git a/BTCPayApp.UI/Pages/SignedOut/SignedOutBasePage.razor b/BTCPayApp.UI/Pages/SignedOut/SignedOutBasePage.razor index cf2671c..ba352ef 100644 --- a/BTCPayApp.UI/Pages/SignedOut/SignedOutBasePage.razor +++ b/BTCPayApp.UI/Pages/SignedOut/SignedOutBasePage.razor @@ -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 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 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 diff --git a/BTCPayApp.UI/Pages/Wallet/SettingsPage.razor b/BTCPayApp.UI/Pages/Wallet/SettingsPage.razor index b231de9..229f539 100644 --- a/BTCPayApp.UI/Pages/Wallet/SettingsPage.razor +++ b/BTCPayApp.UI/Pages/Wallet/SettingsPage.razor @@ -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() diff --git a/BTCPayApp.UI/StartupExtensions.cs b/BTCPayApp.UI/StartupExtensions.cs index 8970edc..4595d1a 100644 --- a/BTCPayApp.UI/StartupExtensions.cs +++ b/BTCPayApp.UI/StartupExtensions.cs @@ -27,3 +27,11 @@ public static class StartupExtensions return serviceCollection; } } + +public static class AppSettings +{ + public const bool AutoGenerateWallets = false; + public const bool AllowWalletGeneration = false; + + +} diff --git a/BTCPayApp.UI/StateMiddleware.cs b/BTCPayApp.UI/StateMiddleware.cs index c23290a..8570c2d 100644 --- a/BTCPayApp.UI/StateMiddleware.cs +++ b/BTCPayApp.UI/StateMiddleware.cs @@ -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(); } diff --git a/setup.ps1 b/setup.ps1 new file mode 100644 index 0000000..2ad8656 --- /dev/null +++ b/setup.ps1 @@ -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." diff --git a/submodules/btcpayserver b/submodules/btcpayserver index 21d609a..c6f960c 160000 --- a/submodules/btcpayserver +++ b/submodules/btcpayserver @@ -1 +1 @@ -Subproject commit 21d609aab09b6a24e49d509104839b3cc45d7760 +Subproject commit c6f960cef510932ce2abf9bc098f7e0ba3714428