mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-03-20 07:39:07 -04:00
Refactor TOTP code to work view AddEdit/View mode (#181)
This commit is contained in:
committed by
Leendert de Borst
parent
e96cfa3940
commit
697abc6828
@@ -100,8 +100,12 @@
|
||||
if (OriginalAttachmentsIds.Contains(attachment.Id))
|
||||
{
|
||||
// If it was part of the original set, we soft delete it.
|
||||
attachment.IsDeleted = true;
|
||||
attachment.UpdatedAt = DateTime.UtcNow;
|
||||
var attachmentToDelete = Attachments.FirstOrDefault(a => a.Id == attachment.Id);
|
||||
if (attachmentToDelete is not null)
|
||||
{
|
||||
attachmentToDelete.IsDeleted = true;
|
||||
attachmentToDelete.UpdatedAt = DateTime.UtcNow;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
@inherits ComponentBase
|
||||
@inject TotpCodeService TotpCodeService
|
||||
@inject GlobalNotificationService GlobalNotificationService
|
||||
@inject DbService DbService
|
||||
@inject ConfirmModalService ConfirmModalService
|
||||
@using AliasVault.RazorComponents.Services
|
||||
@using TotpGenerator
|
||||
@implements IDisposable
|
||||
|
||||
<div class="p-4 mb-4 bg-white border border-gray-200 rounded-lg shadow-sm 2xl:col-span-2 dark:border-gray-700 sm:p-6 dark:bg-gray-800">
|
||||
@@ -9,10 +11,10 @@
|
||||
<div>
|
||||
<h3 class="mb-4 text-xl font-semibold dark:text-white">Two-factor authentication</h3>
|
||||
</div>
|
||||
@if (TotpCodeList.Count > 0)
|
||||
@if (TotpCodeList.Where(t => !t.IsDeleted).Any() && !IsAddFormVisible)
|
||||
{
|
||||
<div>
|
||||
<button @onclick="ShowAddTotpCodeModal" type="button" class="text-blue-700 hover:text-white border border-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-3 py-2 text-center dark:border-blue-500 dark:text-blue-500 dark:hover:text-white dark:hover:bg-blue-600 dark:focus:ring-blue-800">
|
||||
<button @onclick="ShowAddForm" type="button" class="text-blue-700 hover:text-white border border-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-3 py-2 text-center dark:border-blue-500 dark:text-blue-500 dark:hover:text-white dark:hover:bg-blue-600 dark:focus:ring-blue-800">
|
||||
Add TOTP Code
|
||||
</button>
|
||||
</div>
|
||||
@@ -23,16 +25,45 @@
|
||||
{
|
||||
<LoadingIndicator />
|
||||
}
|
||||
else if (TotpCodeList.Count == 0)
|
||||
else if ((TotpCodeList.Count == 0 || TotpCodeList.All(t => t.IsDeleted)) && !IsAddFormVisible)
|
||||
{
|
||||
<div class="flex flex-col justify-center">
|
||||
<p class="text-gray-500 dark:text-gray-400"><a @onclick="ShowAddTotpCodeModal" href="javascript:void(0)" class="text-blue-600 hover:text-blue-800 dark:text-blue-500 dark:hover:text-blue-400">Add a two-factor authenticator code</a></p>
|
||||
<p class="text-gray-500 dark:text-gray-400"><a @onclick="ShowAddForm" href="javascript:void(0)" class="text-blue-600 hover:text-blue-800 dark:text-blue-500 dark:hover:text-blue-400">Add a two-factor authenticator code</a></p>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
@if (IsAddFormVisible)
|
||||
{
|
||||
<div class="p-4 mb-4 bg-gray-50 border border-gray-200 rounded-lg dark:bg-gray-700 dark:border-gray-600">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h4 class="text-lg font-medium text-gray-900 dark:text-white">Add 2FA TOTP Code</h4>
|
||||
<button @onclick="HideAddForm" type="button" class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm w-8 h-8 ms-auto inline-flex justify-center items-center dark:hover:bg-gray-600 dark:hover:text-white">
|
||||
<svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
|
||||
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6"/>
|
||||
</svg>
|
||||
<span class="sr-only">Close form</span>
|
||||
</button>
|
||||
</div>
|
||||
<p class="mb-4 text-sm text-gray-500 dark:text-gray-400">If the website offers or requires 2FA for your account (such as Google Authenticator), you can use AliasVault instead to generate the codes for you.</p>
|
||||
<div class="mb-4">
|
||||
<label for="name" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Name</label>
|
||||
<input type="text" id="name" @bind="NewTotpCode.Name" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-600 dark:border-gray-500 dark:placeholder-gray-400 dark:text-white" placeholder="Service Name" required>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label for="secretKey" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Secret Key (32 characters)</label>
|
||||
<input type="text" id="secretKey" @bind="NewTotpCode.SecretKey" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-600 dark:border-gray-500 dark:placeholder-gray-400 dark:text-white" placeholder="Enter 32-character code" required>
|
||||
</div>
|
||||
<div class="flex justify-end">
|
||||
<button type="button" @onclick="AddTotpCode" class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800">
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="grid grid-cols-1 gap-4 mt-4">
|
||||
@foreach (var totpCode in TotpCodeList)
|
||||
@foreach (var totpCode in TotpCodeList.Where(t => !t.IsDeleted))
|
||||
{
|
||||
<div class="p-4 bg-gray-50 border border-gray-200 rounded-lg dark:bg-gray-700 dark:border-gray-600">
|
||||
<div class="flex justify-between items-center gap-4">
|
||||
@@ -41,13 +72,23 @@
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="flex flex-col items-end">
|
||||
<div class="text-2xl font-bold text-gray-900 dark:text-white">@GetTotpCode(totpCode.SecretKey)</div>
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400">@GetRemainingSeconds()s</div>
|
||||
@if (OriginalTotpCodeIds.Contains(totpCode.Id))
|
||||
{
|
||||
<div class="text-2xl font-bold text-gray-900 dark:text-white">@GetTotpCode(totpCode.SecretKey)</div>
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400">@GetRemainingSeconds()s</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="text-sm text-gray-500 dark:text-gray-400">Save to view code</div>
|
||||
}
|
||||
</div>
|
||||
<div class="w-1.5 h-8 bg-gray-200 rounded-full dark:bg-gray-600">
|
||||
<div class="bg-blue-600 rounded-full transition-all" style="height: @(GetRemainingPercentage())%; width: 100%"></div>
|
||||
@if (OriginalTotpCodeIds.Contains(totpCode.Id))
|
||||
{
|
||||
<div class="bg-blue-600 rounded-full transition-all" style="height: @(GetRemainingPercentage())%; width: 100%"></div>
|
||||
}
|
||||
</div>
|
||||
<button @onclick="() => DeleteTotpCode(totpCode.Id)" class="text-red-600 hover:text-red-800 dark:text-red-500 dark:hover:text-red-400">
|
||||
<button type="button" @onclick="() => DeleteTotpCode(totpCode)" class="text-red-600 hover:text-red-800 dark:text-red-500 dark:hover:text-red-400">
|
||||
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
@@ -60,82 +101,66 @@
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (IsAddTotpCodeModalVisible)
|
||||
{
|
||||
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50">
|
||||
<div class="relative p-4 w-full max-w-md max-h-full">
|
||||
<div class="relative bg-white rounded-lg shadow dark:bg-gray-700">
|
||||
<div class="flex items-center justify-between p-4 md:p-5 border-b rounded-t dark:border-gray-600">
|
||||
<h3 class="text-xl font-semibold text-gray-900 dark:text-white">
|
||||
Add 2FA TOTP Code
|
||||
</h3>
|
||||
<button @onclick="HideAddTotpCodeModal" type="button" class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm w-8 h-8 ms-auto inline-flex justify-center items-center dark:hover:bg-gray-600 dark:hover:text-white">
|
||||
<svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
|
||||
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6"/>
|
||||
</svg>
|
||||
<span class="sr-only">Close modal</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="p-4 md:p-5">
|
||||
<p class="mb-4 text-sm text-gray-500 dark:text-gray-400">If the website offers or requires 2FA for your account (such as Google Authenticator), you can use AliasVault instead to generate the codes for you.</p>
|
||||
<div class="mb-4">
|
||||
<label for="name" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Name</label>
|
||||
<input type="text" id="name" @bind="NewTotpCode.Name" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-600 dark:border-gray-500 dark:placeholder-gray-400 dark:text-white" placeholder="Service Name" required>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label for="secretKey" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Secret Key (32 characters)</label>
|
||||
<input type="text" id="secretKey" @bind="NewTotpCode.SecretKey" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-600 dark:border-gray-500 dark:placeholder-gray-400 dark:text-white" placeholder="Enter 32-character code" required>
|
||||
</div>
|
||||
<button @onclick="AddTotpCode" class="w-full text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800">
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@code {
|
||||
/// <summary>
|
||||
/// The credential ID.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public Guid CredentialId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The service name.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string ServiceName { get; set; } = string.Empty;
|
||||
|
||||
private List<TotpCode> TotpCodeList { get; set; } = new();
|
||||
/// <summary>
|
||||
/// The list of TOTP codes.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public List<TotpCode> TotpCodeList { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Event callback for when the TOTP codes list changes.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public EventCallback<List<TotpCode>> TotpCodesChanged { get; set; }
|
||||
|
||||
private bool IsLoading { get; set; } = true;
|
||||
private bool IsAddTotpCodeModalVisible { get; set; } = false;
|
||||
private bool IsAddFormVisible { get; set; } = false;
|
||||
private TotpCode NewTotpCode { get; set; } = new();
|
||||
private Timer? _refreshTimer;
|
||||
private Dictionary<string, string> _currentCodes = new();
|
||||
private List<Guid> OriginalTotpCodeIds { get; set; } = [];
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
{
|
||||
_refreshTimer?.Dispose();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await base.OnInitializedAsync();
|
||||
await LoadTotpCodesAsync();
|
||||
LoadTotpCodes();
|
||||
|
||||
// Start a timer to refresh the TOTP codes every second
|
||||
_refreshTimer = new Timer(async _ => await RefreshCodesAsync(), null, 0, 1000);
|
||||
}
|
||||
|
||||
private async Task LoadTotpCodesAsync()
|
||||
/// <inheritdoc/>
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
base.OnInitialized();
|
||||
|
||||
// Keep track of the original TOTP codes
|
||||
OriginalTotpCodeIds = TotpCodeList.Where(t => !t.IsDeleted).Select(t => t.Id).ToList();
|
||||
}
|
||||
|
||||
private void LoadTotpCodes()
|
||||
{
|
||||
IsLoading = true;
|
||||
StateHasChanged();
|
||||
|
||||
TotpCodeList = await TotpCodeService.GetTotpCodesAsync(CredentialId);
|
||||
|
||||
// Generate initial codes
|
||||
foreach (var code in TotpCodeList)
|
||||
foreach (var code in TotpCodeList.Where(t => !t.IsDeleted))
|
||||
{
|
||||
_currentCodes[code.SecretKey] = TotpCodeService.GenerateTotpCode(code.SecretKey);
|
||||
_currentCodes[code.SecretKey] = TotpGenerator.GenerateTotpCode(code.SecretKey);
|
||||
}
|
||||
|
||||
IsLoading = false;
|
||||
@@ -144,9 +169,9 @@
|
||||
|
||||
private async Task RefreshCodesAsync()
|
||||
{
|
||||
foreach (var code in TotpCodeList)
|
||||
foreach (var code in TotpCodeList.Where(t => !t.IsDeleted))
|
||||
{
|
||||
var newCode = TotpCodeService.GenerateTotpCode(code.SecretKey);
|
||||
var newCode = TotpGenerator.GenerateTotpCode(code.SecretKey);
|
||||
if (!_currentCodes.ContainsKey(code.SecretKey) || _currentCodes[code.SecretKey] != newCode)
|
||||
{
|
||||
_currentCodes[code.SecretKey] = newCode;
|
||||
@@ -164,7 +189,7 @@
|
||||
return code;
|
||||
}
|
||||
|
||||
var newCode = TotpCodeService.GenerateTotpCode(secretKey);
|
||||
var newCode = TotpGenerator.GenerateTotpCode(secretKey);
|
||||
_currentCodes[secretKey] = newCode;
|
||||
return newCode;
|
||||
}
|
||||
@@ -181,19 +206,18 @@
|
||||
return (int)(((30.0 - remaining) / 30.0) * 100);
|
||||
}
|
||||
|
||||
private void ShowAddTotpCodeModal()
|
||||
private void ShowAddForm()
|
||||
{
|
||||
NewTotpCode = new TotpCode
|
||||
{
|
||||
CredentialId = CredentialId,
|
||||
Name = ServiceName
|
||||
};
|
||||
IsAddTotpCodeModalVisible = true;
|
||||
IsAddFormVisible = true;
|
||||
}
|
||||
|
||||
private void HideAddTotpCodeModal()
|
||||
private void HideAddForm()
|
||||
{
|
||||
IsAddTotpCodeModalVisible = false;
|
||||
IsAddFormVisible = false;
|
||||
}
|
||||
|
||||
private async Task AddTotpCode()
|
||||
@@ -213,7 +237,7 @@
|
||||
try
|
||||
{
|
||||
// Validate the secret key by trying to generate a code
|
||||
TotpCodeService.GenerateTotpCode(NewTotpCode.SecretKey);
|
||||
TotpGenerator.GenerateTotpCode(NewTotpCode.SecretKey);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
@@ -221,38 +245,57 @@
|
||||
return;
|
||||
}
|
||||
|
||||
var result = await TotpCodeService.AddTotpCodeAsync(NewTotpCode);
|
||||
if (result != null)
|
||||
// Create a new TOTP code in memory
|
||||
var newTotpCode = new TotpCode
|
||||
{
|
||||
HideAddTotpCodeModal();
|
||||
GlobalNotificationService.AddSuccessMessage("TOTP code added successfully.", true);
|
||||
await LoadTotpCodesAsync();
|
||||
await DbService.SaveDatabaseAsync();
|
||||
Id = Guid.Empty,
|
||||
Name = NewTotpCode.Name,
|
||||
SecretKey = NewTotpCode.SecretKey,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
UpdatedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
// Add to the list
|
||||
TotpCodeList.Add(newTotpCode);
|
||||
|
||||
// Notify parent component
|
||||
await TotpCodesChanged.InvokeAsync(TotpCodeList);
|
||||
|
||||
HideAddForm();
|
||||
|
||||
// Refresh the codes
|
||||
_currentCodes[newTotpCode.SecretKey] = TotpGenerator.GenerateTotpCode(newTotpCode.SecretKey);
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private async Task DeleteTotpCode(TotpCode totpCode)
|
||||
{
|
||||
// Show confirmation modal.
|
||||
var result = await ConfirmModalService.ShowConfirmation("Delete TOTP code", "Are you sure you want to delete this TOTP code?");
|
||||
if (!result)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the TOTP code was part of the original set
|
||||
if (OriginalTotpCodeIds.Contains(totpCode.Id))
|
||||
{
|
||||
// If it was part of the original set, we soft delete it
|
||||
var totpCodeToDelete = TotpCodeList.FirstOrDefault(t => t.Id == totpCode.Id);
|
||||
if (totpCodeToDelete is not null)
|
||||
{
|
||||
totpCodeToDelete.IsDeleted = true;
|
||||
totpCodeToDelete.UpdatedAt = DateTime.UtcNow;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
GlobalNotificationService.AddErrorMessage("Failed to add TOTP code. Please try again.", true);
|
||||
// If it was not part of the original set, we hard delete it
|
||||
TotpCodeList.Remove(totpCode);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DeleteTotpCode(Guid totpCodeId)
|
||||
{
|
||||
var result = await TotpCodeService.DeleteTotpCodeAsync(totpCodeId);
|
||||
if (result)
|
||||
{
|
||||
GlobalNotificationService.AddSuccessMessage("TOTP code deleted successfully.", true);
|
||||
await LoadTotpCodesAsync();
|
||||
await DbService.SaveDatabaseAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
GlobalNotificationService.AddErrorMessage("Failed to delete TOTP code. Please try again.", true);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
{
|
||||
_refreshTimer?.Dispose();
|
||||
// Notify parent component
|
||||
await TotpCodesChanged.InvokeAsync(TotpCodeList);
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
121
src/AliasVault.Client/Main/Components/TotpCodes/TotpViewer.razor
Normal file
121
src/AliasVault.Client/Main/Components/TotpCodes/TotpViewer.razor
Normal file
@@ -0,0 +1,121 @@
|
||||
@inherits ComponentBase
|
||||
@inject TotpCodeService TotpCodeService
|
||||
@implements IDisposable
|
||||
@using TotpGenerator
|
||||
|
||||
<div class="p-4 mb-4 bg-white border border-gray-200 rounded-lg shadow-sm 2xl:col-span-2 dark:border-gray-700 sm:p-6 dark:bg-gray-800">
|
||||
<div class="flex justify-between">
|
||||
<div>
|
||||
<h3 class="mb-4 text-xl font-semibold dark:text-white">Two-factor authentication</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (IsLoading)
|
||||
{
|
||||
<LoadingIndicator />
|
||||
}
|
||||
else if (TotpCodeList.Count == 0)
|
||||
{
|
||||
<div class="flex flex-col justify-center">
|
||||
<p class="text-gray-500 dark:text-gray-400">No two-factor authenticator codes available</p>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="grid grid-cols-1 gap-4 mt-4">
|
||||
@foreach (var totpCode in TotpCodeList)
|
||||
{
|
||||
<div class="p-4 bg-gray-50 border border-gray-200 rounded-lg dark:bg-gray-700 dark:border-gray-600">
|
||||
<div class="flex justify-between items-center gap-4">
|
||||
<div class="flex items-center flex-1">
|
||||
<h4 class="text-lg font-medium text-gray-900 dark:text-white">@totpCode.Name</h4>
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="flex flex-col items-end">
|
||||
<div class="text-2xl font-bold text-gray-900 dark:text-white">@GetTotpCode(totpCode.SecretKey)</div>
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400">@GetRemainingSeconds()s</div>
|
||||
</div>
|
||||
<div class="w-1.5 h-8 bg-gray-200 rounded-full dark:bg-gray-600">
|
||||
<div class="bg-blue-600 rounded-full transition-all" style="height: @(GetRemainingPercentage())%; width: 100%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
/// <summary>
|
||||
/// The list of TOTP codes to display.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public required ICollection<TotpCode> TotpCodeList { get; set; }
|
||||
|
||||
private bool IsLoading { get; set; } = true;
|
||||
private Timer? _refreshTimer;
|
||||
private Dictionary<string, string> _currentCodes = new();
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
_refreshTimer?.Dispose();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await base.OnInitializedAsync();
|
||||
|
||||
// Generate initial codes
|
||||
foreach (var code in TotpCodeList)
|
||||
{
|
||||
_currentCodes[code.SecretKey] = TotpGenerator.GenerateTotpCode(code.SecretKey);
|
||||
}
|
||||
|
||||
// Start a timer to refresh the TOTP codes every second
|
||||
_refreshTimer = new Timer(async _ => await RefreshCodesAsync(), null, 0, 1000);
|
||||
|
||||
IsLoading = false;
|
||||
}
|
||||
|
||||
private async Task RefreshCodesAsync()
|
||||
{
|
||||
foreach (var code in TotpCodeList)
|
||||
{
|
||||
var newCode = TotpGenerator.GenerateTotpCode(code.SecretKey);
|
||||
if (!_currentCodes.ContainsKey(code.SecretKey) || _currentCodes[code.SecretKey] != newCode)
|
||||
{
|
||||
_currentCodes[code.SecretKey] = newCode;
|
||||
}
|
||||
}
|
||||
|
||||
// Always update the UI to refresh the progress bar
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
private string GetTotpCode(string secretKey)
|
||||
{
|
||||
if (_currentCodes.TryGetValue(secretKey, out var code))
|
||||
{
|
||||
return code;
|
||||
}
|
||||
|
||||
var newCode = TotpGenerator.GenerateTotpCode(secretKey);
|
||||
_currentCodes[secretKey] = newCode;
|
||||
return newCode;
|
||||
}
|
||||
|
||||
private int GetRemainingSeconds()
|
||||
{
|
||||
return TotpCodeService.GetRemainingSeconds();
|
||||
}
|
||||
|
||||
private int GetRemainingPercentage()
|
||||
{
|
||||
var remaining = GetRemainingSeconds();
|
||||
// Invert the percentage so it counts down instead of up
|
||||
return (int)(((30.0 - remaining) / 30.0) * 100);
|
||||
}
|
||||
}
|
||||
@@ -80,4 +80,9 @@ public sealed class CredentialEdit
|
||||
/// Gets or sets the Attachment list.
|
||||
/// </summary>
|
||||
public List<Attachment> Attachments { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the TOTP codes list.
|
||||
/// </summary>
|
||||
public List<TotpCode> TotpCodes { get; set; } = [];
|
||||
}
|
||||
|
||||
@@ -44,6 +44,19 @@ else
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (EditMode && Id.HasValue)
|
||||
{
|
||||
<div class="col-span-1 md:col-span-1 lg:col-span-1">
|
||||
<TotpCodes ServiceName="@Obj.ServiceName" TotpCodeList="@Obj.TotpCodes" TotpCodesChanged="HandleTotpCodesChanged" />
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="col-span-1 md:col-span-1 lg:col-span-1">
|
||||
<TotpCodes ServiceName="@Obj.ServiceName" TotpCodeList="@Obj.TotpCodes" TotpCodesChanged="HandleTotpCodesChanged" />
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="col-span-1 md:col-span-1 lg:col-span-1">
|
||||
<div class="p-4 mb-4 bg-white border border-gray-200 rounded-lg shadow-sm 2xl:col-span-2 dark:border-gray-700 sm:p-6 dark:bg-gray-800">
|
||||
<h3 class="mb-4 text-xl font-semibold dark:text-white">Notes</h3>
|
||||
@@ -218,6 +231,7 @@ else
|
||||
alias.Alias.Email = "@" + CredentialService.GetDefaultEmailDomain();
|
||||
alias.Service = new Service();
|
||||
alias.Passwords = new List<Password> { new Password() };
|
||||
alias.TotpCodes = new List<TotpCode>();
|
||||
|
||||
Obj = CredentialToCredentialEdit(alias);
|
||||
Obj.ServiceUrl = CredentialService.DefaultServiceUrl;
|
||||
@@ -257,6 +271,12 @@ else
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private void HandleTotpCodesChanged(List<TotpCode> updatedTotpCodes)
|
||||
{
|
||||
Obj.TotpCodes = updatedTotpCodes;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private async Task GenerateRandomAlias()
|
||||
{
|
||||
GlobalLoadingSpinner.Show();
|
||||
@@ -340,6 +360,7 @@ else
|
||||
Alias = aliasCopy.Alias,
|
||||
AliasBirthDate = aliasCopy.Alias.BirthDate.ToString("yyyy-MM-dd"),
|
||||
Attachments = aliasCopy.Attachments.ToList(),
|
||||
TotpCodes = aliasCopy.TotpCodes.ToList(),
|
||||
CreateDate = aliasCopy.CreatedAt,
|
||||
LastUpdate = aliasCopy.UpdatedAt
|
||||
};
|
||||
@@ -367,6 +388,7 @@ else
|
||||
},
|
||||
Alias = alias.Alias,
|
||||
Attachments = alias.Attachments,
|
||||
TotpCodes = alias.TotpCodes,
|
||||
};
|
||||
|
||||
if (string.IsNullOrWhiteSpace(alias.AliasBirthDate))
|
||||
|
||||
@@ -44,7 +44,12 @@ else
|
||||
</div>
|
||||
</div>
|
||||
<RecentEmails EmailAddress="@Alias.Alias.Email" />
|
||||
<TotpCodes CredentialId="@Id" ServiceName="@Alias.Service.Name" />
|
||||
|
||||
@if (Alias.TotpCodes.Count > 0)
|
||||
{
|
||||
<TotpViewer TotpCodeList="@Alias.TotpCodes" />
|
||||
}
|
||||
|
||||
@if (Alias.Notes != null && Alias.Notes.Length > 0)
|
||||
{
|
||||
<FormattedNote Notes="@Alias.Notes" />
|
||||
|
||||
@@ -182,6 +182,12 @@ public sealed class CredentialService(HttpClient httpClient, DbService dbService
|
||||
login.Attachments.Add(attachment);
|
||||
}
|
||||
|
||||
// Add TOTP codes
|
||||
foreach (var totpCode in loginObject.TotpCodes)
|
||||
{
|
||||
login.TotpCodes.Add(totpCode);
|
||||
}
|
||||
|
||||
context.Credentials.Add(login);
|
||||
|
||||
// Add password.
|
||||
@@ -253,8 +259,7 @@ public sealed class CredentialService(HttpClient httpClient, DbService dbService
|
||||
login.Service.UpdatedAt = DateTime.UtcNow;
|
||||
|
||||
// Remove attachments that are no longer in the list
|
||||
var attachmentsToRemove = login.Attachments.Where(existingAttachment =>
|
||||
!loginObject.Attachments.Any(a => a.Id == existingAttachment.Id)).ToList();
|
||||
var attachmentsToRemove = login.Attachments.Where(existingAttachment => !loginObject.Attachments.Any(a => a.Id == existingAttachment.Id)).ToList();
|
||||
foreach (var attachmentToRemove in attachmentsToRemove)
|
||||
{
|
||||
login.Attachments.Remove(attachmentToRemove);
|
||||
@@ -280,6 +285,33 @@ public sealed class CredentialService(HttpClient httpClient, DbService dbService
|
||||
}
|
||||
}
|
||||
|
||||
// Remove TOTP codes that are no longer in the list
|
||||
var totpCodesToRemove = login.TotpCodes.Where(existingTotp => !loginObject.TotpCodes.Any(t => t.Id == existingTotp.Id)).ToList();
|
||||
foreach (var totpToRemove in totpCodesToRemove)
|
||||
{
|
||||
login.TotpCodes.Remove(totpToRemove);
|
||||
context.Entry(totpToRemove).State = EntityState.Deleted;
|
||||
}
|
||||
|
||||
// Update existing TOTP codes and add new ones
|
||||
foreach (var totpCode in loginObject.TotpCodes)
|
||||
{
|
||||
if (totpCode.Id != Guid.Empty)
|
||||
{
|
||||
var existingTotpCode = login.TotpCodes.FirstOrDefault(t => t.Id == totpCode.Id);
|
||||
if (existingTotpCode != null)
|
||||
{
|
||||
// Update existing TOTP code
|
||||
context.Entry(existingTotpCode).CurrentValues.SetValues(totpCode);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Add new TOTP code
|
||||
login.TotpCodes.Add(totpCode);
|
||||
}
|
||||
}
|
||||
|
||||
// Save the database to the server.
|
||||
if (!await dbService.SaveDatabaseAsync())
|
||||
{
|
||||
@@ -300,14 +332,23 @@ public sealed class CredentialService(HttpClient httpClient, DbService dbService
|
||||
var context = await dbService.GetDbContextAsync();
|
||||
|
||||
var loginObject = await context.Credentials
|
||||
.Include(x => x.Passwords)
|
||||
.Include(x => x.Alias)
|
||||
.Include(x => x.Service)
|
||||
.Include(x => x.Attachments)
|
||||
.AsSplitQuery()
|
||||
.Where(x => x.Id == loginId)
|
||||
.Where(x => !x.IsDeleted)
|
||||
.FirstOrDefaultAsync();
|
||||
.Include(x => x.Passwords)
|
||||
.Include(x => x.Alias)
|
||||
.Include(x => x.Service)
|
||||
.Include(x => x.Attachments)
|
||||
.Include(x => x.TotpCodes)
|
||||
.AsSplitQuery()
|
||||
.Where(x => x.Id == loginId)
|
||||
.Where(x => !x.IsDeleted)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
if (loginObject != null)
|
||||
{
|
||||
// Filter out deleted items from collections after loading
|
||||
loginObject.Passwords = loginObject.Passwords.Where(p => !p.IsDeleted).ToList();
|
||||
loginObject.Attachments = loginObject.Attachments.Where(a => !a.IsDeleted).ToList();
|
||||
loginObject.TotpCodes = loginObject.TotpCodes.Where(t => !t.IsDeleted).ToList();
|
||||
}
|
||||
|
||||
return loginObject;
|
||||
}
|
||||
@@ -321,13 +362,14 @@ public sealed class CredentialService(HttpClient httpClient, DbService dbService
|
||||
var context = await dbService.GetDbContextAsync();
|
||||
|
||||
var loginObject = await context.Credentials
|
||||
.Include(x => x.Passwords)
|
||||
.Include(x => x.Alias)
|
||||
.Include(x => x.Service)
|
||||
.Include(x => x.Attachments)
|
||||
.AsSplitQuery()
|
||||
.Where(x => !x.IsDeleted)
|
||||
.ToListAsync();
|
||||
.Include(x => x.Passwords.Where(p => !p.IsDeleted))
|
||||
.Include(x => x.Alias)
|
||||
.Include(x => x.Service)
|
||||
.Include(x => x.Attachments.Where(a => !a.IsDeleted))
|
||||
.Include(x => x.TotpCodes.Where(t => !t.IsDeleted))
|
||||
.AsSplitQuery()
|
||||
.Where(x => !x.IsDeleted)
|
||||
.ToListAsync();
|
||||
|
||||
return loginObject;
|
||||
}
|
||||
|
||||
@@ -31,85 +31,6 @@ public class TotpCodeService
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all TOTP codes for a credential.
|
||||
/// </summary>
|
||||
/// <param name="credentialId">The credential ID.</param>
|
||||
/// <returns>A list of TOTP codes.</returns>
|
||||
public async Task<List<TotpCode>> GetTotpCodesAsync(Guid credentialId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var dbContext = await _dbService.GetDbContextAsync();
|
||||
return await dbContext.TotpCodes
|
||||
.Where(t => t.CredentialId == credentialId)
|
||||
.ToListAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error getting TOTP codes for credential {CredentialId}", credentialId);
|
||||
return new List<TotpCode>();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a new TOTP code.
|
||||
/// </summary>
|
||||
/// <param name="totpCode">The TOTP code to add.</param>
|
||||
/// <returns>The added TOTP code.</returns>
|
||||
public async Task<TotpCode?> AddTotpCodeAsync(TotpCode totpCode)
|
||||
{
|
||||
try
|
||||
{
|
||||
var dbContext = await _dbService.GetDbContextAsync();
|
||||
dbContext.TotpCodes.Add(totpCode);
|
||||
await dbContext.SaveChangesAsync();
|
||||
return totpCode;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error adding TOTP code for credential {CredentialId}", totpCode.CredentialId);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes a TOTP code.
|
||||
/// </summary>
|
||||
/// <param name="totpCodeId">The TOTP code ID.</param>
|
||||
/// <returns>True if the TOTP code was deleted, false otherwise.</returns>
|
||||
public async Task<bool> DeleteTotpCodeAsync(Guid totpCodeId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var dbContext = await _dbService.GetDbContextAsync();
|
||||
var totpCode = await dbContext.TotpCodes.FindAsync(totpCodeId);
|
||||
if (totpCode == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
dbContext.TotpCodes.Remove(totpCode);
|
||||
await dbContext.SaveChangesAsync();
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error deleting TOTP code {TotpCodeId}", totpCodeId);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a TOTP code for a given secret key.
|
||||
/// </summary>
|
||||
/// <param name="secretKey">The secret key.</param>
|
||||
/// <returns>The generated TOTP code.</returns>
|
||||
public string GenerateTotpCode(string secretKey)
|
||||
{
|
||||
return TotpGenerator.GenerateTotpCode(secretKey);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the remaining seconds until the TOTP code expires.
|
||||
/// </summary>
|
||||
|
||||
Reference in New Issue
Block a user