Skip to content

Commit

Permalink
Account refactoring
Browse files Browse the repository at this point in the history
Closes #111.
  • Loading branch information
dennisreimann committed Jan 24, 2025
1 parent b46bbae commit 515a765
Show file tree
Hide file tree
Showing 26 changed files with 314 additions and 360 deletions.
218 changes: 90 additions & 128 deletions BTCPayApp.Core/Auth/AuthStateProvider.cs

Large diffs are not rendered by default.

12 changes: 3 additions & 9 deletions BTCPayApp.Core/Auth/IAccountManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,8 @@ namespace BTCPayApp.Core.Auth;

public interface IAccountManager
{
public BTCPayAccount? GetAccount();
public Task<IEnumerable<BTCPayAccount>> GetAccounts(string? hostFilter = null);
public AppUserInfo? GetUserInfo();
public BTCPayAccount? Account { get; }
public AppUserInfo? UserInfo { get; }
public BTCPayAppClient GetClient(string? baseUri = null);
public Task<bool> CheckAuthenticated(bool refreshUser = false);
public Task<bool> IsAuthorized(string policy, object? resource = null);
Expand All @@ -19,17 +18,12 @@ public interface IAccountManager
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);
public Task<FormResult<ApplicationUserData>> ChangeAccountInfo(string email, string? name, string? imageUrl, CancellationToken? cancellation = default);
public Task<FormResult> SetCurrentStoreId(string storeId);
public Task UnsetCurrentStore();
public Task<FormResult> SetCurrentStoreId(string? storeId);
public AppUserStoreInfo? GetCurrentStore();
public AppUserStoreInfo? GetUserStore(string storeId);
public Task<AppUserStoreInfo> EnsureStorePos(AppUserStoreInfo store, bool? forceCreate = false);
public Task Logout();
public Task UpdateAccount(BTCPayAccount account);
public Task RemoveAccount(BTCPayAccount account);
public AsyncEventHandler<BTCPayAccount?>? OnBeforeAccountChange { get; set; }
public AsyncEventHandler<BTCPayAccount?>? OnAfterAccountChange { get; set; }
public AsyncEventHandler<BTCPayAccount?>? OnAccountInfoChange { get; set; }
public AsyncEventHandler<AppUserInfo?>? OnUserInfoChange { get; set; }
public AsyncEventHandler<AppUserStoreInfo?>? OnBeforeStoreChange { get; set; }
public AsyncEventHandler<AppUserStoreInfo?>? OnAfterStoreChange { get; set; }
Expand Down
20 changes: 4 additions & 16 deletions BTCPayApp.Core/BTCPayAccount.cs
Original file line number Diff line number Diff line change
@@ -1,24 +1,12 @@
namespace BTCPayApp.Core;

public class BTCPayAccount(string baseUri, string email)
public class BTCPayAccount(string baseUri, string email, string? accessToken = null)
{
public static string GetId(string baseUri, string email) => $"{new Uri(baseUri).Host}:{email}";
public readonly string Id = GetId(baseUri, email);
public const string Key = "account";
public string Id { get; private set; } = $"{new Uri(baseUri).Host}:{email}";
public string BaseUri { get; private set; } = WithTrailingSlash(baseUri);
public string Email { get; private set; } = email;
public string? AccessToken { get; set; }
public string? Name { get; set; }
public string? ImageUrl { get; set; }

// TODO: Store this separately
public string? CurrentStoreId { get; set; }

public void SetInfo(string email, string? name, string? imageUrl)
{
Email = email;
Name = name;
ImageUrl = imageUrl;
}
public string? AccessToken { get; set; } = accessToken;

private static string WithTrailingSlash(string s)
{
Expand Down
3 changes: 2 additions & 1 deletion BTCPayApp.Core/BTCPayAppConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ namespace BTCPayApp.Core;

public class BTCPayAppConfig
{
public const string Key = "AppConfig";
public const string Key = "appconfig";
public string? Passcode { get; set; }
public string? CurrentStoreId { get; set; }
}
10 changes: 8 additions & 2 deletions BTCPayApp.Core/BTCPayServer/BTCPayConnectionManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ private async Task OnConnectionChanged(object? sender, (BTCPayConnectionState Ol
var newState = e.New;
try
{
var account = accountManager.GetAccount();
var account = accountManager.Account;
switch (e.New)
{
case BTCPayConnectionState.Init:
Expand Down Expand Up @@ -144,7 +144,7 @@ private async Task OnConnectionChanged(object? sender, (BTCPayConnectionState Ol
.WithUrl(url, options =>
{
options.AccessTokenProvider = () =>
Task.FromResult(accountManager.GetAccount()?.AccessToken);
Task.FromResult(accountManager.Account?.AccessToken);
options.HttpMessageHandlerFactory = serviceProvider
.GetService<Func<HttpMessageHandler, HttpMessageHandler>>();
options.WebSocketConfiguration =
Expand Down Expand Up @@ -202,6 +202,12 @@ private async Task OnConnectionChanged(object? sender, (BTCPayConnectionState Ol
await syncService.SyncToLocal();
}
newState = BTCPayConnectionState.ConnectedFinishedInitialSync;

var config = await configProvider.Get<BTCPayAppConfig>(BTCPayAppConfig.Key);
if (!string.IsNullOrEmpty(config?.CurrentStoreId))
{
await accountManager.SetCurrentStoreId(config.CurrentStoreId);
}
}
break;
case BTCPayConnectionState.ConnectedFinishedInitialSync:
Expand Down
2 changes: 1 addition & 1 deletion BTCPayApp.Core/Backup/SyncService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ await api.PutObjectAsync(new PutObjectRequest

private Task<IVSSAPI> GetUnencryptedVSSAPI()
{
var account = accountManager.GetAccount();
var account = accountManager.Account;
if (account is null)
throw new InvalidOperationException("Account not found");
var vssUri = new Uri(new Uri(account.BaseUri), "vss/");
Expand Down
2 changes: 1 addition & 1 deletion BTCPayApp.Core/Helpers/StoreHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public static class StoreHelpers
OnChainWalletManager onChainWalletManager, LightningNodeManager lightningNodeService, bool applyOnchain, bool applyLighting)
{
var storeId = accountManager.GetCurrentStore()?.Id;
var userId = accountManager.GetUserInfo()?.UserId;
var userId = accountManager.UserInfo?.UserId;
var config = await onChainWalletManager.GetConfig();
if (// are user and store present?
string.IsNullOrEmpty(userId) ||
Expand Down
4 changes: 2 additions & 2 deletions BTCPayApp.Tests/CoreTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ public async Task CanStartAppCore()
Assert.False(await node.AuthStateProvider.CheckAuthenticated());
Assert.True((await node.AccountManager.Login(btcpayUri.AbsoluteUri, username, username, null)).Succeeded);
Assert.True(await node.AuthStateProvider.CheckAuthenticated());
Assert.NotNull(node.AccountManager.GetAccount()?.AccessToken);
Assert.NotNull(node.AccountManager.Account?.AccessToken);

TestUtils.Eventually(() =>
Assert.Equal(BTCPayConnectionState.ConnectedAsMaster, node.ConnectionManager.ConnectionState), 30_000);
Expand All @@ -68,7 +68,7 @@ public async Task CanStartAppCore()
TestUtils.Eventually(() => Assert.Equal(LightningNodeState.WaitingForConnection, node.LNManager.State));
Assert.True((await node.AccountManager.Login(btcpayUri.AbsoluteUri, username, username, null)).Succeeded);
Assert.True(await node.AuthStateProvider.CheckAuthenticated());
Assert.NotNull(node.AccountManager.GetAccount()?.AccessToken);
Assert.NotNull(node.AccountManager.Account?.AccessToken);

TestUtils.Eventually(() => Assert.Equal(OnChainWalletState.NotConfigured, node.OnChainWalletManager.State));
TestUtils.Eventually(() => Assert.Equal(LightningNodeState.WaitingForConnection, node.LNManager.State));
Expand Down
5 changes: 3 additions & 2 deletions BTCPayApp.UI/App.razor
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
@using BTCPayApp.UI.Features
@using BTCPayApp.UI.Pages
@using BTCPayApp.Core.Contracts
@using BTCPayApp.UI.Pages.SignedOut
@inherits Fluxor.Blazor.Web.Components.FluxorComponent
@inject IDispatcher Dispatcher
@inject IAccountManager AccountManager
Expand Down Expand Up @@ -40,7 +41,7 @@
// signed in
if (AuthState == null) return;
var authState = await AuthState;
var account = AccountManager.GetAccount();
var account = AccountManager.Account;
if (authState.User.Identity?.IsAuthenticated is true && !string.IsNullOrEmpty(account?.BaseUri))
{
Dispatcher.Dispatch(new UIState.FetchInstanceInfo(account.BaseUri));
Expand All @@ -63,7 +64,7 @@
<RedirectToIndex/>
</NotAuthorized>
<Authorizing>
<LoadingPage Title="Loading"/>
<IndexPage />
</Authorizing>
</AuthorizeRouteView>
<FocusOnNavigate RouteData="@routeData" Selector="h1"/>
Expand Down
2 changes: 1 addition & 1 deletion BTCPayApp.UI/Components/SetupStatus.razor
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
}

public SetupState SetupStateAccount() {
return string.IsNullOrEmpty(AccountManager.GetAccount()?.CurrentStoreId) ? SetupState.Pending : SetupState.Completed;
return string.IsNullOrEmpty(AccountManager.GetCurrentStore()?.Id) ? SetupState.Pending : SetupState.Completed;
}

public SetupState SetupStateOnchain() {
Expand Down
152 changes: 63 additions & 89 deletions BTCPayApp.UI/Components/UserSwitch.razor
Original file line number Diff line number Diff line change
@@ -1,114 +1,88 @@
@using BTCPayApp.Core
@using BTCPayApp.Core.Auth
@using BTCPayApp.Core.Models
@using BTCPayApp.UI.Models
@using BTCPayServer.Client.Models
@inject IJSRuntime JS
@inject IAccountManager AccountManager
@inject NavigationManager NavigationManager
@implements IDisposable

<div class="d-inline-flex align-items-center p-0 gap-3">
@if (Users?.Count() > 1)
{
<button class="d-inline-flex align-items-center btn btn-link text-body dropdown-toggle dropdown-toggle-no-caret p-0 gap-2" type="button" data-bs-toggle="dropdown" aria-expanded="false">
<UserInfo Email="@_account!.Email" Name="@_account.Name" ImageUrl="@_account.ImageUrl" />
<Icon Symbol="caret-down" class="text-muted"/>
</button>
<ul class="dropdown-menu">
<li><h6 class="dropdown-header">Switch to user</h6></li>
@foreach (var user in Users)
<button class="d-inline-flex align-items-center btn btn-link text-body dropdown-toggle dropdown-toggle-no-caret p-0 gap-2" type="button" data-bs-toggle="dropdown" aria-expanded="false">
<UserInfo Email="@CurrentUser.Email" Name="@CurrentUser.Name" ImageUrl="@CurrentUser.ImageUrl" />
<Icon Symbol="caret-down" class="text-muted"/>
</button>
<ul class="dropdown-menu">
<li><h6 class="dropdown-header">Switch to user</h6></li>
@foreach (var user in Users)
{
if (user.Email == CurrentUser.Email) continue;
<li>
<button class="d-inline-flex align-items-center dropdown-item gap-2" type="button" @onclick="() => SwitchToUser(user)">
<UserInfo Email="@user.Email" Name="@user.Name" ImageUrl="@user.ImageUrl" />
</button>
</li>
}
</ul>
@*<div class="modal fade" id="UserSwitchModal" data-bs-backdrop="static" data-bs-keyboard="false" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
@if (Model != null)
{
if (user.Email == _account.Email) continue;
<li>
<button class="d-inline-flex align-items-center dropdown-item gap-2" type="button" @onclick="() => SwitchToUser(user)">
<UserInfo Email="@user.Email" Name="@user.Name" ImageUrl="@user.ImageUrl" />
</button>
</li>
}
</ul>
<div class="modal fade" id="UserSwitchModal" data-bs-backdrop="static" data-bs-keyboard="false" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
@if (Model != null)
{
<EditForm Model="Model" OnValidSubmit="HandleValidSubmit" FormName="SwitchAccount">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5">Switch to @Model.Email</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close">
<Icon Symbol="cross"/>
</button>
<EditForm Model="Model" OnValidSubmit="HandleValidSubmit" FormName="SwitchAccount">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5">Switch to @Model.Email</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close">
<Icon Symbol="cross"/>
</button>
</div>
<div class="modal-body">
@if (!string.IsNullOrEmpty(_errorMessage))
{
<Alert Type="danger">@_errorMessage</Alert>
}
<div class="form-group" hidden="@Model.RequireTwoFactor">
<label for="Password" class="form-label">Password</label>
<InputText @bind-Value="Model.Password" type="password" id="Password" autocomplete="current-password" class="form-control"/>
<ValidationMessage For="@(() => Model.Password)"/>
</div>
<div class="modal-body">
@if (!string.IsNullOrEmpty(_errorMessage))
@if (Model.RequireTwoFactor)
{
<div class="form-group">
<label for="TwoFactorCode" class="form-label">Two Factor Code</label>
<InputText @bind-Value="Model.TwoFactorCode" type="number" id="TwoFactorCode" autocomplete="one-time-code" class="form-control hide-number-spin"/>
<ValidationMessage For="@(() => Model.TwoFactorCode)"/>
</div>
}
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary w-100">
@if (_sending)
{
<Alert Type="danger">@_errorMessage</Alert>
<LoadingIndicator />
}
<div class="form-group" hidden="@Model.RequireTwoFactor">
<label for="Password" class="form-label">Password</label>
<InputText @bind-Value="Model.Password" type="password" id="Password" autocomplete="current-password" class="form-control"/>
<ValidationMessage For="@(() => Model.Password)"/>
</div>
@if (Model.RequireTwoFactor)
else
{
<div class="form-group">
<label for="TwoFactorCode" class="form-label">Two Factor Code</label>
<InputText @bind-Value="Model.TwoFactorCode" type="number" id="TwoFactorCode" autocomplete="one-time-code" class="form-control hide-number-spin"/>
<ValidationMessage For="@(() => Model.TwoFactorCode)"/>
</div>
<span>Sign in as @Model.Email</span>
}
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary w-100">
@if (_sending)
{
<LoadingIndicator />
}
else
{
<span>Sign in as @Model.Email</span>
}
</button>
</div>
</button>
</div>
</EditForm>
}
</div>
</div>
</EditForm>
}
</div>
}
else if (_account != null)
{
<UserInfo Email="@_account.Email" Name="@_account.Name" ImageUrl="@_account.ImageUrl" />
}
</div>*@
</div>

@code {
private BTCPayAccount? _account;
private bool _sending;
private string? _errorMessage;
private LoginModel? Model { get; set; }

[Parameter]
public IEnumerable<StoreUserData>? Users { get; set; }

protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();

AccountManager.OnAccountInfoChange += OnAccountInfoChange;

_account = AccountManager.GetAccount();
}

public void Dispose()
{
AccountManager.OnAccountInfoChange -= OnAccountInfoChange;
}
[Parameter, EditorRequired]
public required AppUserInfo CurrentUser { get; set; }

private async Task OnAccountInfoChange(object? sender, BTCPayAccount? account)
{
_account = account;
await InvokeAsync(StateHasChanged);
}
[Parameter, EditorRequired]
public required IEnumerable<StoreUserData> Users { get; set; }

private Task SwitchToUser(StoreUserData user)
{
Expand All @@ -118,7 +92,7 @@
//AccountManager.OnBeforeAccountChange += OnBeforeAccountChange;
}

private async Task OnBeforeAccountChange(object? sender, BTCPayAccount? previousAccount)
/*private async Task OnBeforeAccountChange(object? sender, BTCPayAccount? previousAccount)
{
await JS.InvokeVoidAsync("Interop.closeModal", "#UserSwitchModal");
}
Expand All @@ -144,5 +118,5 @@
? string.Join(",", result.Messages)
: "Invalid login attempt.";
}
}
}*/
}
4 changes: 1 addition & 3 deletions BTCPayApp.UI/Features/RootState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,10 @@ public class ConnectionEffects(NavigationManager navigationManager)
[EffectMethod]
public Task HandleConnectionStateUpdatedAction(RootState.ConnectionStateUpdatedAction action, IDispatcher dispatcher)
{
if(action.State == BTCPayConnectionState.WaitingForEncryptionKey)
if (action.State == BTCPayConnectionState.WaitingForEncryptionKey)
{

dispatcher.Dispatch(new GoAction(navigationManager.ToAbsoluteUri(Routes.EncryptionKey).ToString()));
}

return Task.CompletedTask;
}
}
Expand Down
Loading

0 comments on commit 515a765

Please sign in to comment.