Add active sessions component to client (#80)

This commit is contained in:
Leendert de Borst
2024-08-30 16:28:22 +02:00
parent 4f8ab5da28
commit 1945b15e2e
4 changed files with 224 additions and 56 deletions

View File

@@ -48,7 +48,7 @@ public class SecurityController(IDbContextFactory<AliasServerDbContext> dbContex
ExpireDate = x.ExpireDate,
CreatedAt = x.CreatedAt,
})
.OrderBy(x => x.CreatedAt)
.OrderByDescending(x => x.CreatedAt)
.ToListAsync();
return Ok(refreshTokenList);

View File

@@ -0,0 +1,109 @@
@using AliasVault.Shared.Models.WebApi.Security
@inject HttpClient Http
@inject GlobalNotificationService GlobalNotificationService
<div class="p-4 mb-4 mx-4 bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 sm:p-6 dark:bg-gray-800">
<h3 class="mb-2 text-lg font-medium text-gray-900 dark:text-white">Active Sessions</h3>
<p class="text-sm text-gray-600 dark:text-gray-400 mb-4">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.</p>
@if (IsLoading)
{
<LoadingIndicator />
}
else
{
@if (!Sessions.Any())
{
<p class="text-sm text-gray-600 dark:text-gray-400">No active sessions found.</p>
}
else
{
<div class="overflow-x-auto">
<table class="w-full text-sm text-left text-gray-500 dark:text-gray-400">
<thead class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400">
<tr>
<th scope="col" class="px-6 py-3">Device</th>
<th scope="col" class="px-6 py-3">Last active</th>
<th scope="col" class="px-6 py-3">Expires</th>
<th scope="col" class="px-6 py-3">Action</th>
</tr>
</thead>
<tbody>
@foreach (var session in Sessions)
{
<tr class="bg-white border-b dark:bg-gray-800 dark:border-gray-700">
<td class="px-6 py-4">@session.DeviceIdentifier</td>
<td class="px-6 py-4">@session.CreatedAt.ToLocalTime().ToString("g")</td>
<td class="px-6 py-4">@session.ExpireDate.ToLocalTime().ToString("g")</td>
<td class="px-6 py-4">
<button @onclick="() => RevokeSession(session.Id)"
class="font-medium text-red-600 dark:text-red-500 hover:underline">Revoke (logout)</button>
</td>
</tr>
}
</tbody>
</table>
</div>
}
}
</div>
@code {
/// <summary>
/// Gets or sets the list of active sessions (refresh tokens) for the current user.
/// </summary>
[Parameter]
public List<RefreshTokenModel> Sessions { get; set; } = new List<RefreshTokenModel>();
/// <summary>
/// Event callback that is invoked when the list of active sessions changes.
/// </summary>
[Parameter]
public EventCallback OnSessionsChanged { get; set; }
private bool IsLoading { get; set; } = true;
/// <summary>
/// Loads the active sessions data from the server.
/// </summary>
/// <returns>A task representing the asynchronous operation.</returns>
public async Task LoadData()
{
IsLoading = true;
StateHasChanged();
var sessionsResponse = await Http.GetFromJsonAsync<List<RefreshTokenModel>>("api/v1/Security/sessions");
if (sessionsResponse is not null)
{
Sessions = sessionsResponse;
}
IsLoading = false;
StateHasChanged();
}
/// <summary>
/// Revokes a specific session (refresh token) for the current user.
/// </summary>
/// <param name="id">The unique identifier of the session to revoke.</param>
/// <returns>A task representing the asynchronous operation.</returns>
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);
}
}
}

View File

@@ -0,0 +1,92 @@
@inject HttpClient Http
@inject NavigationManager NavigationManager
<div class="p-4 mb-4 mx-4 bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 sm:p-6 dark:bg-gray-800">
<h3 class="mb-2 text-lg font-medium text-gray-900 dark:text-white">Two-factor authentication</h3>
@if (IsLoading)
{
<LoadingIndicator />
}
else
{
@if (TwoFactorEnabled)
{
<div class="mb-3 text-sm text-gray-600 dark:text-gray-400">Two factor authentication is currently enabled.</div>
<button @onclick="DisableTwoFactor"
class="bg-red-500 text-white py-2 px-4 rounded-md hover:bg-red-600 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 transition duration-150 ease-in-out">
Disable Two-Factor Authentication
</button>
}
else
{
<div class="mb-3 text-sm text-gray-600 dark:text-gray-400">Two factor authentication is currently disabled. In order to improve your account security we advise you to enable it.</div>
<button @onclick="EnableTwoFactor"
class="bg-green-500 text-white py-2 px-4 rounded-md hover:bg-green-600 focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2 transition duration-150 ease-in-out">
Enable Two-Factor Authentication
</button>
}
}
</div>
@code {
/// <summary>
/// Gets or sets a value indicating whether Two-Factor Authentication is enabled.
/// </summary>
[Parameter]
public bool TwoFactorEnabled { get; set; }
/// <summary>
/// Event callback that is invoked when the Two-Factor Authentication status changes.
/// </summary>
[Parameter]
public EventCallback OnStatusChanged { get; set; }
private bool IsLoading { get; set; } = true;
/// <summary>
/// Loads the Two-Factor Authentication status from the server.
/// </summary>
/// <returns>A task representing the asynchronous operation.</returns>
public async Task LoadData()
{
IsLoading = true;
StateHasChanged();
var twoFactorResponse = await Http.GetFromJsonAsync<TwoFactorEnabledResult>("api/v1/TwoFactorAuth/status");
if (twoFactorResponse is not null)
{
TwoFactorEnabled = twoFactorResponse.TwoFactorEnabled;
}
IsLoading = false;
StateHasChanged();
}
/// <summary>
/// Navigates to the Enable Two-Factor Authentication page.
/// </summary>
private void EnableTwoFactor()
{
NavigationManager.NavigateTo("settings/security/enable-2fa");
}
/// <summary>
/// Navigates to the Disable Two-Factor Authentication page.
/// </summary>
private void DisableTwoFactor()
{
NavigationManager.NavigateTo("settings/security/disable-2fa");
}
/// <summary>
/// Represents the result of the Two-Factor Authentication status check.
/// </summary>
private sealed class TwoFactorEnabledResult
{
/// <summary>
/// Gets or sets a value indicating whether Two-Factor Authentication is enabled.
/// </summary>
public required bool TwoFactorEnabled { get; init; } = false;
}
}

View File

@@ -1,48 +1,26 @@
@page "/settings/security"
@using AliasVault.Client.Main.Pages.Settings.Security.Components
@inherits MainBase
@inject HttpClient Http
<LayoutPageTitle>Security settings</LayoutPageTitle>
@if (IsLoading)
{
<LoadingIndicator />
}
else
{
<div class="grid grid-cols-1 px-4 pt-6 xl:grid-cols-3 xl:gap-4 dark:bg-gray-900">
<div class="mb-4 col-span-full xl:mb-2">
<Breadcrumb BreadcrumbItems="BreadcrumbItems"/>
<div class="grid grid-cols-1 px-4 pt-6 xl:grid-cols-3 xl:gap-4 dark:bg-gray-900">
<div class="mb-4 col-span-full xl:mb-2">
<Breadcrumb BreadcrumbItems="BreadcrumbItems"/>
<div class="flex items-center justify-between">
<h1 class="text-xl font-semibold text-gray-900 sm:text-2xl dark:text-white">Security settings</h1>
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">Configure security settings.</p>
<RefreshButton OnRefresh="LoadData" ButtonText="Refresh" />
</div>
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">Configure security settings.</p>
</div>
</div>
<div class="p-4 mb-4 mx-4 bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 sm:p-6 dark:bg-gray-800">
<h3 class="mb-4 text-lg font-medium text-gray-900 dark:text-white">Two-factor authentication</h3>
@if (TwoFactorEnabled)
{
<div class="mb-3 text-sm text-gray-600 dark:text-gray-400">Two factor authentication is currently enabled.</div>
<button @onclick="DisableTwoFactor"
class="bg-red-500 text-white py-2 px-4 rounded-md hover:bg-red-600 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 transition duration-150 ease-in-out">
Disable Two-Factor Authentication
</button>
}
else
{
<div class="mb-3 text-sm text-gray-600 dark:text-gray-400">Two factor authentication is currently disabled. In order to improve your account security we advise you to enable it.</div>
<button @onclick="EnableTwoFactor"
class="bg-green-500 text-white py-2 px-4 rounded-md hover:bg-green-600 focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2 transition duration-150 ease-in-out">
Enable Two-Factor Authentication
</button>
}
</div>
}
<TwoFactorAuthenticationSection @ref="TwoFactorSection" OnStatusChanged="LoadData" />
<ActiveSessionsSection @ref="SessionsSection" OnSessionsChanged="LoadData" />
@code {
private bool TwoFactorEnabled { get; set; }
private bool IsLoading { get; set; } = true;
private TwoFactorAuthenticationSection? TwoFactorSection;
private ActiveSessionsSection? SessionsSection;
/// <inheritdoc />
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<TwoFactorEnabledResult>("api/v1/TwoFactorAuth/status");
if (response is not null)
{
TwoFactorEnabled = response.TwoFactorEnabled;
}
IsLoading = false;
StateHasChanged();
await LoadData();
}
}
private void EnableTwoFactor()
/// <summary>
/// Loads data for both the Two-Factor Authentication and Active Sessions sections concurrently.
/// </summary>
/// <returns>A task representing the asynchronous operation.</returns>
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()
);
}
}