mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-03-18 22:57:01 -04:00
Add active sessions component to client (#80)
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user