diff --git a/src/AliasVault.Api/Controllers/Security/SecurityController.cs b/src/AliasVault.Api/Controllers/Security/SecurityController.cs index 5e5b8785c..07b3dcd9c 100644 --- a/src/AliasVault.Api/Controllers/Security/SecurityController.cs +++ b/src/AliasVault.Api/Controllers/Security/SecurityController.cs @@ -48,7 +48,7 @@ public class SecurityController(IDbContextFactory dbContex ExpireDate = x.ExpireDate, CreatedAt = x.CreatedAt, }) - .OrderBy(x => x.CreatedAt) + .OrderByDescending(x => x.CreatedAt) .ToListAsync(); return Ok(refreshTokenList); diff --git a/src/AliasVault.Client/Main/Pages/Settings/Security/Components/ActiveSessionsSection.razor b/src/AliasVault.Client/Main/Pages/Settings/Security/Components/ActiveSessionsSection.razor new file mode 100644 index 000000000..13c81eb3b --- /dev/null +++ b/src/AliasVault.Client/Main/Pages/Settings/Security/Components/ActiveSessionsSection.razor @@ -0,0 +1,109 @@ +@using AliasVault.Shared.Models.WebApi.Security +@inject HttpClient Http +@inject GlobalNotificationService GlobalNotificationService + +
+

Active Sessions

+

Below you can find an overview of devices that are currently logged in and/or have a valid session. You can revoke (logout) those sessions here.

+ + @if (IsLoading) + { + + } + else + { + @if (!Sessions.Any()) + { +

No active sessions found.

+ } + else + { +
+ + + + + + + + + + + @foreach (var session in Sessions) + { + + + + + + + } + +
DeviceLast activeExpiresAction
@session.DeviceIdentifier@session.CreatedAt.ToLocalTime().ToString("g")@session.ExpireDate.ToLocalTime().ToString("g") + +
+
+ } + } +
+ +@code { + /// + /// Gets or sets the list of active sessions (refresh tokens) for the current user. + /// + [Parameter] + public List Sessions { get; set; } = new List(); + + /// + /// Event callback that is invoked when the list of active sessions changes. + /// + [Parameter] + public EventCallback OnSessionsChanged { get; set; } + + private bool IsLoading { get; set; } = true; + + /// + /// Loads the active sessions data from the server. + /// + /// A task representing the asynchronous operation. + public async Task LoadData() + { + IsLoading = true; + StateHasChanged(); + var sessionsResponse = await Http.GetFromJsonAsync>("api/v1/Security/sessions"); + if (sessionsResponse is not null) + { + Sessions = sessionsResponse; + } + + IsLoading = false; + StateHasChanged(); + } + + /// + /// Revokes a specific session (refresh token) for the current user. + /// + /// The unique identifier of the session to revoke. + /// A task representing the asynchronous operation. + private async Task RevokeSession(Guid id) + { + try + { + var response = await Http.DeleteAsync($"api/v1/Security/sessions/{id}"); + if (response.IsSuccessStatusCode) + { + GlobalNotificationService.AddSuccessMessage("Session revoked successfully.", true); + await OnSessionsChanged.InvokeAsync(); + } + else + { + GlobalNotificationService.AddErrorMessage("Failed to revoke session.", true); + } + } + catch (Exception ex) + { + GlobalNotificationService.AddErrorMessage($"Failed to revoke session: {ex.Message}.", true); + } + } +} diff --git a/src/AliasVault.Client/Main/Pages/Settings/Security/Components/TwoFactorAuthenticationSection.razor b/src/AliasVault.Client/Main/Pages/Settings/Security/Components/TwoFactorAuthenticationSection.razor new file mode 100644 index 000000000..152249143 --- /dev/null +++ b/src/AliasVault.Client/Main/Pages/Settings/Security/Components/TwoFactorAuthenticationSection.razor @@ -0,0 +1,92 @@ +@inject HttpClient Http +@inject NavigationManager NavigationManager + +
+

Two-factor authentication

+ + @if (IsLoading) + { + + } + else + { + @if (TwoFactorEnabled) + { +
Two factor authentication is currently enabled.
+ + } + else + { +
Two factor authentication is currently disabled. In order to improve your account security we advise you to enable it.
+ + } + } +
+ +@code { + /// + /// Gets or sets a value indicating whether Two-Factor Authentication is enabled. + /// + [Parameter] + public bool TwoFactorEnabled { get; set; } + + /// + /// Event callback that is invoked when the Two-Factor Authentication status changes. + /// + [Parameter] + public EventCallback OnStatusChanged { get; set; } + + private bool IsLoading { get; set; } = true; + + /// + /// Loads the Two-Factor Authentication status from the server. + /// + /// A task representing the asynchronous operation. + public async Task LoadData() + { + IsLoading = true; + StateHasChanged(); + + var twoFactorResponse = await Http.GetFromJsonAsync("api/v1/TwoFactorAuth/status"); + if (twoFactorResponse is not null) + { + TwoFactorEnabled = twoFactorResponse.TwoFactorEnabled; + } + + IsLoading = false; + StateHasChanged(); + } + + /// + /// Navigates to the Enable Two-Factor Authentication page. + /// + private void EnableTwoFactor() + { + NavigationManager.NavigateTo("settings/security/enable-2fa"); + } + + /// + /// Navigates to the Disable Two-Factor Authentication page. + /// + private void DisableTwoFactor() + { + NavigationManager.NavigateTo("settings/security/disable-2fa"); + } + + /// + /// Represents the result of the Two-Factor Authentication status check. + /// + private sealed class TwoFactorEnabledResult + { + /// + /// Gets or sets a value indicating whether Two-Factor Authentication is enabled. + /// + public required bool TwoFactorEnabled { get; init; } = false; + } +} diff --git a/src/AliasVault.Client/Main/Pages/Settings/Security/Security.razor b/src/AliasVault.Client/Main/Pages/Settings/Security/Security.razor index e64004f7b..0be4cb1e7 100644 --- a/src/AliasVault.Client/Main/Pages/Settings/Security/Security.razor +++ b/src/AliasVault.Client/Main/Pages/Settings/Security/Security.razor @@ -1,48 +1,26 @@ @page "/settings/security" +@using AliasVault.Client.Main.Pages.Settings.Security.Components @inherits MainBase -@inject HttpClient Http Security settings -@if (IsLoading) -{ - -} -else -{ -
-
- +
+
+ +

Security settings

-

Configure security settings.

+
+

Configure security settings.

+
-
-

Two-factor authentication

- - @if (TwoFactorEnabled) - { -
Two factor authentication is currently enabled.
- - } - else - { -
Two factor authentication is currently disabled. In order to improve your account security we advise you to enable it.
- - } -
-} + + @code { - private bool TwoFactorEnabled { get; set; } - private bool IsLoading { get; set; } = true; + private TwoFactorAuthenticationSection? TwoFactorSection; + private ActiveSessionsSection? SessionsSection; /// protected override async Task OnInitializedAsync() @@ -57,32 +35,21 @@ else { await base.OnAfterRenderAsync(firstRender); - // Check on server if 2FA is enabled if (firstRender) { - var response = await Http.GetFromJsonAsync("api/v1/TwoFactorAuth/status"); - if (response is not null) - { - TwoFactorEnabled = response.TwoFactorEnabled; - } - - IsLoading = false; - StateHasChanged(); + await LoadData(); } } - private void EnableTwoFactor() + /// + /// Loads data for both the Two-Factor Authentication and Active Sessions sections concurrently. + /// + /// A task representing the asynchronous operation. + private async Task LoadData() { - NavigationManager.NavigateTo("settings/security/enable-2fa"); - } - - private void DisableTwoFactor() - { - NavigationManager.NavigateTo("settings/security/disable-2fa"); - } - - private sealed class TwoFactorEnabledResult - { - public required bool TwoFactorEnabled { get; init; } = false; + await Task.WhenAll( + TwoFactorSection!.LoadData(), + SessionsSection!.LoadData() + ); } }