mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-04-10 17:47:51 -04:00
Improve client DB sync status indicators (#74)
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -377,6 +377,5 @@ src/AliasVault.WebApp/wwwroot/index.html
|
||||
# appsettings.Development.json is generated by the build process from appsettings.Development.template.json and therefore should be ignored
|
||||
src/AliasVault.WebApp/wwwroot/appsettings.Development.json
|
||||
|
||||
|
||||
# .env is generated by init.sh and therefore should be ignored
|
||||
.env
|
||||
|
||||
12
docs/misc/upgrade-ef-client-model.md
Normal file
12
docs/misc/upgrade-ef-client-model.md
Normal file
@@ -0,0 +1,12 @@
|
||||
To upgrade the AliasClientDb EF model, follow these steps:
|
||||
|
||||
1. Make changes to the AliasClientDb EF model in the `AliasClientDb` project.
|
||||
2. Create a new migration by running the following command in the `AliasClientDb` project:
|
||||
|
||||
```bash
|
||||
# Important: make sure the migration name is prefixed by the Semver version number of the release.
|
||||
# For example, if the release version is 1.0.0, the migration name should be `1.0.0-<migration-name>`.
|
||||
dotnet ef migrations add "1.0.0-<migration-name>"
|
||||
```
|
||||
4. On the next login of a user, they will be prompted (required) to upgrade their database schema to the latest version.
|
||||
Make sure to manually test this.
|
||||
@@ -28,13 +28,14 @@ public class VaultController(AliasServerDbContext context, UserManager<AliasVaul
|
||||
/// <summary>
|
||||
/// Default retention policy for vaults.
|
||||
/// </summary>
|
||||
private readonly RetentionPolicy _retentionPolicy = new RetentionPolicy
|
||||
private readonly RetentionPolicy _retentionPolicy = new()
|
||||
{
|
||||
Rules = new List<IRetentionRule>
|
||||
{
|
||||
new DailyRetentionRule() { DaysToKeep = 3 },
|
||||
new WeeklyRetentionRule() { WeeksToKeep = 1 },
|
||||
new MonthlyRetentionRule() { MonthsToKeep = 1 },
|
||||
new DailyRetentionRule { DaysToKeep = 3 },
|
||||
new WeeklyRetentionRule { WeeksToKeep = 1 },
|
||||
new MonthlyRetentionRule { MonthsToKeep = 1 },
|
||||
new VersionRetentionRule { VersionsToKeep = 3 },
|
||||
},
|
||||
};
|
||||
|
||||
@@ -61,10 +62,10 @@ public class VaultController(AliasServerDbContext context, UserManager<AliasVaul
|
||||
// as starting point.
|
||||
if (vault == null)
|
||||
{
|
||||
return Ok(new Shared.Models.WebApi.Vault(string.Empty, DateTime.MinValue, DateTime.MinValue));
|
||||
return Ok(new Shared.Models.WebApi.Vault(string.Empty, string.Empty, DateTime.MinValue, DateTime.MinValue));
|
||||
}
|
||||
|
||||
return Ok(new Shared.Models.WebApi.Vault(vault.VaultBlob, vault.CreatedAt, vault.UpdatedAt));
|
||||
return Ok(new Shared.Models.WebApi.Vault(vault.VaultBlob, vault.Version, vault.CreatedAt, vault.UpdatedAt));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -86,6 +87,7 @@ public class VaultController(AliasServerDbContext context, UserManager<AliasVaul
|
||||
{
|
||||
UserId = user.Id,
|
||||
VaultBlob = model.Blob,
|
||||
Version = model.Version,
|
||||
CreatedAt = timeProvider.UtcNow,
|
||||
UpdatedAt = timeProvider.UtcNow,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="VersionRetentionRule.cs" company="lanedirt">
|
||||
// Copyright (c) lanedirt. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE.md file in the project root for full license information.
|
||||
// </copyright>
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
namespace AliasVault.Api.Vault.RetentionRules;
|
||||
|
||||
using AliasServerDb;
|
||||
|
||||
/// <summary>
|
||||
/// Version retention rule that keeps the latest X unique versions of the vault.
|
||||
/// </summary>
|
||||
public class VersionRetentionRule : IRetentionRule
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets amount of versions to keep the vault.
|
||||
/// </summary>
|
||||
public int VersionsToKeep { get; set; }
|
||||
|
||||
/// <inheritdoc cref="IRetentionRule.ApplyRule"/>
|
||||
public IEnumerable<Vault> ApplyRule(List<Vault> vaults, DateTime now)
|
||||
{
|
||||
// For the specified amount of versions, take last vault per version.
|
||||
return vaults
|
||||
.GroupBy(x => x.Version)
|
||||
.Select(g => g.OrderByDescending(x => x.UpdatedAt).First())
|
||||
.OrderByDescending(x => x.UpdatedAt)
|
||||
.Take(VersionsToKeep);
|
||||
}
|
||||
}
|
||||
@@ -16,11 +16,13 @@ public class Vault
|
||||
/// Initializes a new instance of the <see cref="Vault"/> class.
|
||||
/// </summary>
|
||||
/// <param name="blob">Blob.</param>
|
||||
/// <param name="version">Version of the vault data model (migration).</param>
|
||||
/// <param name="createdAt">CreatedAt.</param>
|
||||
/// <param name="updatedAt">UpdatedAt.</param>
|
||||
public Vault(string blob, DateTime createdAt, DateTime updatedAt)
|
||||
public Vault(string blob, string version, DateTime createdAt, DateTime updatedAt)
|
||||
{
|
||||
Blob = blob;
|
||||
Version = version;
|
||||
CreatedAt = createdAt;
|
||||
UpdatedAt = updatedAt;
|
||||
}
|
||||
@@ -30,6 +32,11 @@ public class Vault
|
||||
/// </summary>
|
||||
public string Blob { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the vault version.
|
||||
/// </summary>
|
||||
public string Version { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the date and time of creation.
|
||||
/// </summary>
|
||||
|
||||
@@ -60,6 +60,9 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<AdditionalFiles Include="Main\Pages\Sync\StatusMessages\ErrorVaultDecrypt.razor" />
|
||||
<AdditionalFiles Include="Main\Pages\Sync\StatusMessages\PendingMigrations.razor" />
|
||||
<AdditionalFiles Include="Main\Pages\Sync\StatusMessages\VaultDecryptionProgress.razor" />
|
||||
<None Include="wwwroot\appsettings.Development.json" Condition="Exists('wwwroot\appsettings.Development.json')">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
@@ -89,4 +92,10 @@
|
||||
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<_ContentIncludedByDefault Remove="Main\Layout\StatusMessages\ErrorVaultDecrypt.razor" />
|
||||
<_ContentIncludedByDefault Remove="Main\Layout\StatusMessages\PendingMigrations.razor" />
|
||||
<_ContentIncludedByDefault Remove="Main\Layout\StatusMessages\VaultDecryptionProgress.razor" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -40,11 +40,6 @@ else
|
||||
Message = "Saving...";
|
||||
await ShowLoadingIndicatorAsync();
|
||||
}
|
||||
else if (newState.Status == DbServiceState.DatabaseStatus.LoadingFromServer)
|
||||
{
|
||||
Message = "Loading...";
|
||||
await ShowLoadingIndicatorAsync();
|
||||
}
|
||||
|
||||
LoadingIndicatorMessage = Message + " - " + newState.LastUpdated;
|
||||
}
|
||||
|
||||
16
src/AliasVault.WebApp/Main/Layout/EmptyLayout.razor
Normal file
16
src/AliasVault.WebApp/Main/Layout/EmptyLayout.razor
Normal file
@@ -0,0 +1,16 @@
|
||||
@inherits LayoutComponentBase
|
||||
|
||||
<CascadingAuthenticationState>
|
||||
<AuthorizeView>
|
||||
<Authorized>
|
||||
<main>
|
||||
@Body
|
||||
</main>
|
||||
</Authorized>
|
||||
<NotAuthorized>
|
||||
<main>
|
||||
@Body
|
||||
</main>
|
||||
</NotAuthorized>
|
||||
</AuthorizeView>
|
||||
</CascadingAuthenticationState>
|
||||
@@ -1,8 +1,4 @@
|
||||
@inherits LayoutComponentBase
|
||||
@implements IDisposable
|
||||
@inject DbService DbService
|
||||
@inject AuthenticationStateProvider AuthStateProvider
|
||||
@using AliasVault.WebApp.Main.Layout.StatusMessages
|
||||
|
||||
<CascadingAuthenticationState>
|
||||
<AuthorizeView>
|
||||
@@ -10,25 +6,10 @@
|
||||
<TopMenu />
|
||||
<div class="flex pt-16 overflow-hidden bg-gray-50 dark:bg-gray-900">
|
||||
<div id="main-content" class="relative w-full max-w-screen-2xl mx-auto h-full overflow-y-auto bg-gray-50 dark:bg-gray-900">
|
||||
@if (IsDbInitialized)
|
||||
{
|
||||
<main>
|
||||
@Body
|
||||
</main>
|
||||
<Footer />
|
||||
}
|
||||
else if(IsDbDecryptionError)
|
||||
{
|
||||
<ErrorVaultDecrypt />
|
||||
}
|
||||
else if(IsPendingMigrations)
|
||||
{
|
||||
<PendingMigrations />
|
||||
}
|
||||
else
|
||||
{
|
||||
<VaultDecryptionProgress />
|
||||
}
|
||||
<main>
|
||||
@Body
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
</div>
|
||||
</Authorized>
|
||||
@@ -41,119 +22,5 @@
|
||||
</CascadingAuthenticationState>
|
||||
|
||||
@code {
|
||||
private bool IsDbInitialized { get; set; } = false;
|
||||
private bool IsDbDecryptionError { get; set; } = false;
|
||||
private bool IsPendingMigrations { get; set; } = false;
|
||||
private const int MinimumLoadingTimeMs = 800;
|
||||
|
||||
[CascadingParameter] private Task<AuthenticationState>? AuthState { get; set; }
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await base.OnInitializedAsync();
|
||||
|
||||
// Reset local state
|
||||
IsDbInitialized = false;
|
||||
IsDbDecryptionError = false;
|
||||
IsPendingMigrations = false;
|
||||
|
||||
DbService.GetState().StateChanged += OnDatabaseStateChanged;
|
||||
AuthStateProvider.AuthenticationStateChanged += OnAuthenticationStateChanged;
|
||||
|
||||
await CheckAndInitializeDatabase();
|
||||
}
|
||||
|
||||
protected override async Task OnParametersSetAsync()
|
||||
{
|
||||
if (AuthState != null)
|
||||
{
|
||||
var authState = await AuthState;
|
||||
if (authState.User.Identity?.IsAuthenticated == true)
|
||||
{
|
||||
await CheckAndInitializeDatabase();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async void OnAuthenticationStateChanged(Task<AuthenticationState> task)
|
||||
{
|
||||
var authState = await task;
|
||||
if (authState.User.Identity?.IsAuthenticated == true)
|
||||
{
|
||||
await CheckAndInitializeDatabase();
|
||||
}
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private async Task CheckAndInitializeDatabase()
|
||||
{
|
||||
var currentState = DbService.GetState().CurrentState;
|
||||
if (currentState.Status == DbServiceState.DatabaseStatus.Uninitialized)
|
||||
{
|
||||
await InitializeDatabaseWithProgress();
|
||||
}
|
||||
else if (currentState.Status == DbServiceState.DatabaseStatus.Ready)
|
||||
{
|
||||
IsDbInitialized = true;
|
||||
StateHasChanged();
|
||||
}
|
||||
else if (currentState.Status == DbServiceState.DatabaseStatus.DecryptionFailed)
|
||||
{
|
||||
IsDbDecryptionError = true;
|
||||
StateHasChanged();
|
||||
}
|
||||
else if (currentState.Status == DbServiceState.DatabaseStatus.PendingMigrations)
|
||||
{
|
||||
IsPendingMigrations = true;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private async void OnDatabaseStateChanged(object? sender, DbServiceState.DatabaseState newState)
|
||||
{
|
||||
if (newState.Status == DbServiceState.DatabaseStatus.Uninitialized)
|
||||
{
|
||||
await InitializeDatabaseWithProgress();
|
||||
}
|
||||
else if (newState.Status == DbServiceState.DatabaseStatus.Ready)
|
||||
{
|
||||
IsDbInitialized = true;
|
||||
}
|
||||
else if (newState.Status == DbServiceState.DatabaseStatus.DecryptionFailed)
|
||||
{
|
||||
IsDbDecryptionError = true;
|
||||
}
|
||||
else if (newState.Status == DbServiceState.DatabaseStatus.PendingMigrations)
|
||||
{
|
||||
IsPendingMigrations = true;
|
||||
}
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private async Task InitializeDatabaseWithProgress()
|
||||
{
|
||||
IsDbInitialized = false;
|
||||
StateHasChanged();
|
||||
|
||||
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
|
||||
|
||||
await DbService.InitializeDatabaseAsync();
|
||||
|
||||
stopwatch.Stop();
|
||||
var elapsedMs = (int)stopwatch.ElapsedMilliseconds;
|
||||
|
||||
if (elapsedMs < MinimumLoadingTimeMs)
|
||||
{
|
||||
await Task.Delay(MinimumLoadingTimeMs - elapsedMs);
|
||||
}
|
||||
|
||||
await CheckAndInitializeDatabase();
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
DbService.GetState().StateChanged -= OnDatabaseStateChanged;
|
||||
AuthStateProvider.AuthenticationStateChanged -= OnAuthenticationStateChanged;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
.page {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
main {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%);
|
||||
}
|
||||
|
||||
.top-row {
|
||||
background-color: #f7f7f7;
|
||||
border-bottom: 1px solid #d6d5d5;
|
||||
justify-content: flex-end;
|
||||
height: 3.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.top-row ::deep a, .top-row ::deep .btn-link {
|
||||
white-space: nowrap;
|
||||
margin-left: 1.5rem;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.top-row ::deep a:hover, .top-row ::deep .btn-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.top-row ::deep a:first-child {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
@media (max-width: 640.98px) {
|
||||
.top-row {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.top-row ::deep a, .top-row ::deep .btn-link {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 641px) {
|
||||
.page {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
width: 250px;
|
||||
height: 100vh;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.top-row {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.top-row.auth ::deep a:first-child {
|
||||
flex: 1;
|
||||
text-align: right;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.top-row, article {
|
||||
padding-left: 2rem !important;
|
||||
padding-right: 1.5rem !important;
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
<div class="fixed inset-0 overflow-y-auto h-full w-full flex items-center justify-center">
|
||||
<div class="relative p-8 bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-md mx-auto">
|
||||
<div class="text-center">
|
||||
<h2 class="mt-4 text-xl font-semibold text-gray-900 dark:text-white">Vault decryption error.</h2>
|
||||
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">An error occured while locally decrypting your vault. Your data is not accessible at this moment. Please try again (later) or contact support.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,65 +0,0 @@
|
||||
@inject DbService DbService
|
||||
@inject GlobalNotificationService GlobalNotificationService
|
||||
|
||||
<div class="fixed inset-0 overflow-y-auto h-full w-full flex items-center justify-center">
|
||||
<div class="relative p-8 bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-md mx-auto">
|
||||
<div class="text-center">
|
||||
<h2 class="mt-4 text-xl font-semibold text-gray-900 dark:text-white">Vault needs to be upgraded.</h2>
|
||||
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">AliasVault has been updated which requires your vault to be upgraded in order to be compatible with the new datastructure.
|
||||
This upgrade should only take a few seconds.</p>
|
||||
|
||||
<div class="mt-4">
|
||||
@if (ErrorMessage.Length > 0)
|
||||
{
|
||||
<AlertMessageError Message="@ErrorMessage" />
|
||||
}
|
||||
|
||||
@if (IsPendingMigrations)
|
||||
{
|
||||
<svg class="mx-auto animate-spin h-12 w-12 text-primary-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
}
|
||||
else
|
||||
{
|
||||
<button @onclick="MigrateDatabase" type="button" class="px-4 py-2 text-white bg-primary-600 rounded-lg hover:bg-primary-700 focus:ring-4 focus:ring-primary-300 dark:bg-primary-500 dark:hover:bg-primary-600 dark:focus:ring-primary-800">
|
||||
Start upgrade process
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private bool IsPendingMigrations { get; set; } = false;
|
||||
private string ErrorMessage { get; set; } = string.Empty;
|
||||
|
||||
private async Task MigrateDatabase()
|
||||
{
|
||||
// Show loading indicator
|
||||
IsPendingMigrations = true;
|
||||
ErrorMessage = String.Empty;
|
||||
StateHasChanged();
|
||||
|
||||
// Simulate a delay.
|
||||
await Task.Delay(1000);
|
||||
|
||||
// Migrate the database
|
||||
if (await DbService.MigrateDatabaseAsync())
|
||||
{
|
||||
// Migration successful
|
||||
GlobalNotificationService.AddSuccessMessage("Database upgrade successful.", true);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Migration failed
|
||||
ErrorMessage = "Database upgrade failed. Please try again or contact support.";
|
||||
}
|
||||
|
||||
// Reset local state
|
||||
IsPendingMigrations = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
<div class="fixed inset-0 overflow-y-auto h-full w-full flex items-center justify-center">
|
||||
<div class="relative p-8 bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-md mx-auto">
|
||||
<div class="text-center">
|
||||
<svg class="mx-auto animate-spin h-12 w-12 text-primary-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
<h2 class="mt-4 text-xl font-semibold text-gray-900 dark:text-white">Vault decryption in progress</h2>
|
||||
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">Please wait while your vault is initialized. This may take a moment.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -91,6 +91,19 @@ public class MainBase : OwningComponentBase
|
||||
await Task.Delay(200);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if DB is initialized, if not, redirect to setup page.
|
||||
if (!DbService.GetState().CurrentState.IsInitialized())
|
||||
{
|
||||
var currentUrl = NavigationManager.Uri;
|
||||
await LocalStorage.SetItemAsync("returnUrl", currentUrl);
|
||||
|
||||
NavigationManager.NavigateTo("/sync");
|
||||
while (true)
|
||||
{
|
||||
await Task.Delay(200);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -106,6 +119,19 @@ public class MainBase : OwningComponentBase
|
||||
await Task.Delay(200);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if DB is initialized, if not, redirect to setup page.
|
||||
if (!DbService.GetState().CurrentState.IsInitialized())
|
||||
{
|
||||
var currentUrl = NavigationManager.Uri;
|
||||
await LocalStorage.SetItemAsync("returnUrl", currentUrl);
|
||||
|
||||
NavigationManager.NavigateTo("/sync");
|
||||
while (true)
|
||||
{
|
||||
await Task.Delay(200);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
@inject DbService DbService
|
||||
@inject GlobalNotificationService GlobalNotificationService
|
||||
|
||||
<div class="relative p-8 bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-md mx-auto">
|
||||
<div class="text-center">
|
||||
<svg class="mx-auto animate-spin h-12 w-12 text-primary-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
<div class="space-y-4">
|
||||
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">Welcome to AliasVault!</h2>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||
Your new encrypted vault is being created. This process may take a moment. Please wait.
|
||||
</p>
|
||||
|
||||
<div>
|
||||
@if (ErrorMessage.Length > 0)
|
||||
{
|
||||
<AlertMessageError Message="@ErrorMessage" />
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private string ErrorMessage { get; set; } = string.Empty;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await base.OnInitializedAsync();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
await base.OnAfterRenderAsync(firstRender);
|
||||
|
||||
if (firstRender)
|
||||
{
|
||||
// Start the database migration process
|
||||
await MigrateDatabase();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task MigrateDatabase()
|
||||
{
|
||||
// Simulate a delay.
|
||||
await Task.Delay(3000);
|
||||
|
||||
// Migrate the database
|
||||
if (await DbService.MigrateDatabaseAsync())
|
||||
{
|
||||
// Migration successful
|
||||
GlobalNotificationService.AddSuccessMessage("Vault successfully created.", true);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Migration failed
|
||||
ErrorMessage = "Vault creation failed. Please try again or contact support.";
|
||||
}
|
||||
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
<div class="relative p-8 bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-md mx-auto">
|
||||
<div class="text-center">
|
||||
<h2 class="mt-4 text-xl font-semibold text-gray-900 dark:text-white">Vault decryption error.</h2>
|
||||
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">An error occured while locally decrypting your vault. Your data is not accessible at this moment. Please try again (later) or contact support.</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,92 @@
|
||||
@inject DbService DbService
|
||||
@inject GlobalNotificationService GlobalNotificationService
|
||||
|
||||
<div class="relative p-8 bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-md mx-auto">
|
||||
<div class="text-center">
|
||||
<div class="space-y-4">
|
||||
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">Vault needs to be upgraded.</h2>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||
AliasVault has been updated. Your personal vault needs to be upgraded in order to be compatible with the new data model.
|
||||
This upgrade should only take a few seconds.
|
||||
</p>
|
||||
<div class="bg-gray-100 dark:bg-gray-800 p-4 rounded-lg shadow-sm">
|
||||
<h3 class="text-lg font-medium text-gray-700 dark:text-gray-300 mb-3">Version Information</h3>
|
||||
<div class="space-y-2">
|
||||
<p class="flex justify-between items-center">
|
||||
<span class="text-sm font-medium text-gray-600 dark:text-gray-400">Your vault:</span>
|
||||
<span class="text-base font-bold text-blue-600 dark:text-blue-400">@CurrentVersion</span>
|
||||
</p>
|
||||
<p class="flex justify-between items-center">
|
||||
<span class="text-sm font-medium text-gray-600 dark:text-gray-400">AliasVault latest version:</span>
|
||||
<span class="text-base font-bold text-green-600 dark:text-green-400">@LatestVersion</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@if (ErrorMessage.Length > 0)
|
||||
{
|
||||
<AlertMessageError Message="@ErrorMessage" />
|
||||
}
|
||||
|
||||
@if (IsPendingMigrations)
|
||||
{
|
||||
<svg class="mx-auto animate-spin h-12 w-12 text-primary-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
}
|
||||
else
|
||||
{
|
||||
<button @onclick="MigrateDatabase" type="button" class="px-4 py-2 text-white bg-primary-600 rounded-lg hover:bg-primary-700 focus:ring-4 focus:ring-primary-300 dark:bg-primary-500 dark:hover:bg-primary-600 dark:focus:ring-primary-800">
|
||||
Start upgrade process
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private bool IsPendingMigrations { get; set; } = false;
|
||||
private string ErrorMessage { get; set; } = string.Empty;
|
||||
private string CurrentVersion { get; set; } = string.Empty;
|
||||
private string LatestVersion { get; set; } = string.Empty;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await base.OnInitializedAsync();
|
||||
|
||||
// Get current and latest available database version
|
||||
CurrentVersion = await DbService.GetCurrentDatabaseVersionAsync();
|
||||
LatestVersion = await DbService.GetLatestDatabaseVersionAsync();
|
||||
}
|
||||
|
||||
private async Task MigrateDatabase()
|
||||
{
|
||||
// Show loading indicator
|
||||
IsPendingMigrations = true;
|
||||
ErrorMessage = String.Empty;
|
||||
StateHasChanged();
|
||||
|
||||
// Simulate a delay.
|
||||
await Task.Delay(1000);
|
||||
|
||||
// Migrate the database
|
||||
if (await DbService.MigrateDatabaseAsync())
|
||||
{
|
||||
// Migration successful
|
||||
GlobalNotificationService.AddSuccessMessage("Vault upgrade successful.", true);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Migration failed
|
||||
ErrorMessage = "Database upgrade failed. Please try again or contact support.";
|
||||
}
|
||||
|
||||
// Reset local state
|
||||
IsPendingMigrations = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<div class="relative p-8 bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-md mx-auto">
|
||||
<div class="text-center">
|
||||
<svg class="mx-auto animate-spin h-12 w-12 text-primary-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
<h2 class="mt-4 text-xl font-semibold text-gray-900 dark:text-white">Vault decryption in progress</h2>
|
||||
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">Please wait while your vault is initialized. This may take a moment.</p>
|
||||
</div>
|
||||
</div>
|
||||
117
src/AliasVault.WebApp/Main/Pages/Sync/Sync.razor
Normal file
117
src/AliasVault.WebApp/Main/Pages/Sync/Sync.razor
Normal file
@@ -0,0 +1,117 @@
|
||||
@page "/sync"
|
||||
@layout EmptyLayout
|
||||
@implements IDisposable
|
||||
@using AliasVault.WebApp.Main.Pages.Sync.StatusMessages
|
||||
@inject ILocalStorageService LocalStorage
|
||||
@inject DbService DbService
|
||||
@inject NavigationManager NavigationManager
|
||||
|
||||
<LayoutPageTitle>Sync</LayoutPageTitle>
|
||||
|
||||
<div class="fixed inset-0 overflow-y-auto h-full w-full flex flex-col items-center justify-center">
|
||||
@if (CurrentDbState.Status == DbServiceState.DatabaseStatus.DecryptionFailed)
|
||||
{
|
||||
<ErrorVaultDecrypt />
|
||||
}
|
||||
else if (CurrentDbState.Status == DbServiceState.DatabaseStatus.PendingMigrations)
|
||||
{
|
||||
<PendingMigrations />
|
||||
}
|
||||
else if (CurrentDbState.Status == DbServiceState.DatabaseStatus.Creating)
|
||||
{
|
||||
<Creating />
|
||||
}
|
||||
else
|
||||
{
|
||||
<VaultDecryptionProgress />
|
||||
}
|
||||
|
||||
<div class="text-sm font-medium text-gray-500 dark:text-gray-400 mt-6">
|
||||
Switch accounts? <a href="/user/logout" class="text-primary-700 hover:underline dark:text-primary-500">Logout</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@code {
|
||||
private DbServiceState.DatabaseState CurrentDbState { get; set; } = new();
|
||||
private const int MinimumLoadingTimeMs = 800;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await base.OnInitializedAsync();
|
||||
|
||||
DbService.GetState().StateChanged += OnDatabaseStateChanged;
|
||||
CurrentDbState = DbService.GetState().CurrentState;
|
||||
|
||||
await CheckAndInitializeDatabase();
|
||||
}
|
||||
|
||||
private async Task CheckAndInitializeDatabase()
|
||||
{
|
||||
CurrentDbState = DbService.GetState().CurrentState;
|
||||
if (CurrentDbState.Status == DbServiceState.DatabaseStatus.Uninitialized)
|
||||
{
|
||||
await InitializeDatabaseWithProgress();
|
||||
}
|
||||
else if (CurrentDbState.Status == DbServiceState.DatabaseStatus.Ready)
|
||||
{
|
||||
await RedirectBackToReturnUrl();
|
||||
}
|
||||
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private async void OnDatabaseStateChanged(object? sender, DbServiceState.DatabaseState newState)
|
||||
{
|
||||
CurrentDbState = DbService.GetState().CurrentState;
|
||||
if (CurrentDbState.Status == DbServiceState.DatabaseStatus.Uninitialized)
|
||||
{
|
||||
await InitializeDatabaseWithProgress();
|
||||
}
|
||||
else if (CurrentDbState.Status == DbServiceState.DatabaseStatus.Ready)
|
||||
{
|
||||
await RedirectBackToReturnUrl();
|
||||
}
|
||||
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private async Task InitializeDatabaseWithProgress()
|
||||
{
|
||||
StateHasChanged();
|
||||
|
||||
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
|
||||
|
||||
await DbService.InitializeDatabaseAsync();
|
||||
|
||||
stopwatch.Stop();
|
||||
var elapsedMs = (int)stopwatch.ElapsedMilliseconds;
|
||||
|
||||
if (elapsedMs < MinimumLoadingTimeMs)
|
||||
{
|
||||
await Task.Delay(MinimumLoadingTimeMs - elapsedMs);
|
||||
}
|
||||
|
||||
await CheckAndInitializeDatabase();
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private async Task RedirectBackToReturnUrl()
|
||||
{
|
||||
var localStorageReturnUrl = await LocalStorage.GetItemAsync<string>("returnUrl");
|
||||
if (!string.IsNullOrEmpty(localStorageReturnUrl) && localStorageReturnUrl != "/sync")
|
||||
{
|
||||
await LocalStorage.RemoveItemAsync("returnUrl");
|
||||
NavigationManager.NavigateTo(localStorageReturnUrl);
|
||||
}
|
||||
else
|
||||
{
|
||||
NavigationManager.NavigateTo("/");
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
DbService.GetState().StateChanged -= OnDatabaseStateChanged;
|
||||
}
|
||||
}
|
||||
@@ -73,32 +73,14 @@ public class DbService : IDisposable
|
||||
return;
|
||||
}
|
||||
|
||||
_state.UpdateState(DbServiceState.DatabaseStatus.LoadingFromServer);
|
||||
|
||||
// Ensure the in-memory database representation is created and has the necessary tables.
|
||||
await _dbContext.Database.MigrateAsync();
|
||||
|
||||
// Attempt to fill the local database with a previously saved database stored on the server.
|
||||
var loaded = await LoadDatabaseFromServerAsync();
|
||||
if (loaded)
|
||||
{
|
||||
Console.WriteLine("Database successfully loaded from server.");
|
||||
|
||||
// Check if database is up to date with migrations.
|
||||
var pendingMigrations = await _dbContext.Database.GetPendingMigrationsAsync();
|
||||
if (pendingMigrations.Any())
|
||||
{
|
||||
_state.UpdateState(DbServiceState.DatabaseStatus.PendingMigrations);
|
||||
}
|
||||
else
|
||||
{
|
||||
_isSuccessfullyInitialized = true;
|
||||
_state.UpdateState(DbServiceState.DatabaseStatus.Ready);
|
||||
}
|
||||
_retryCount = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
_state.UpdateState(DbServiceState.DatabaseStatus.DecryptionFailed);
|
||||
Console.WriteLine("Failed to load database from server.");
|
||||
}
|
||||
}
|
||||
@@ -147,7 +129,7 @@ public class DbService : IDisposable
|
||||
var success = await SaveToServerAsync(encryptedBase64String);
|
||||
if (success)
|
||||
{
|
||||
Console.WriteLine("Database succesfully saved to server.");
|
||||
Console.WriteLine("Database successfully saved to server.");
|
||||
_state.UpdateState(DbServiceState.DatabaseStatus.Ready);
|
||||
}
|
||||
else
|
||||
@@ -205,13 +187,65 @@ public class DbService : IDisposable
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the current version (applied migration) of the database that is loaded in memory.
|
||||
/// </summary>
|
||||
/// <returns>Version as string.</returns>
|
||||
public async Task<string> GetCurrentDatabaseVersionAsync()
|
||||
{
|
||||
var migrations = await _dbContext.Database.GetAppliedMigrationsAsync();
|
||||
var lastMigration = migrations.LastOrDefault();
|
||||
|
||||
// Convert migration Id in the form of "20240708094944_1.0.0-InitialMigration" to "1.0.0".
|
||||
if (lastMigration is not null)
|
||||
{
|
||||
var parts = lastMigration.Split('_');
|
||||
if (parts.Length > 1)
|
||||
{
|
||||
var versionPart = parts[1].Split('-')[0];
|
||||
if (Version.TryParse(versionPart, out _))
|
||||
{
|
||||
return versionPart;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the latest available version (EF migration) as defined in code.
|
||||
/// </summary>
|
||||
/// <returns>Version as string.</returns>
|
||||
public async Task<string> GetLatestDatabaseVersionAsync()
|
||||
{
|
||||
var migrations = await _dbContext.Database.GetPendingMigrationsAsync();
|
||||
var lastMigration = migrations.LastOrDefault();
|
||||
|
||||
// Convert migration Id in the form of "20240708094944_1.0.0-InitialMigration" to "1.0.0".
|
||||
if (lastMigration is not null)
|
||||
{
|
||||
var parts = lastMigration.Split('_');
|
||||
if (parts.Length > 1)
|
||||
{
|
||||
var versionPart = parts[1].Split('-')[0];
|
||||
if (Version.TryParse(versionPart, out _))
|
||||
{
|
||||
return versionPart;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears the database connection and creates a new one so that the database is empty.
|
||||
/// </summary>
|
||||
/// <returns>SqliteConnection and AliasClientDbContext.</returns>
|
||||
public (SqliteConnection SqliteConnection, AliasClientDbContext AliasClientDbContext) InitializeEmptyDatabase()
|
||||
{
|
||||
if (_isSuccessfullyInitialized && _sqlConnection.State == ConnectionState.Open)
|
||||
if (_sqlConnection is not null && _sqlConnection.State == ConnectionState.Open)
|
||||
{
|
||||
_sqlConnection.Close();
|
||||
}
|
||||
@@ -221,6 +255,8 @@ public class DbService : IDisposable
|
||||
|
||||
_dbContext = new AliasClientDbContext(_sqlConnection, log => Console.WriteLine(log));
|
||||
|
||||
// Reset the database state.
|
||||
_state.UpdateState(DbServiceState.DatabaseStatus.Uninitialized);
|
||||
_isSuccessfullyInitialized = false;
|
||||
|
||||
return (_sqlConnection, _dbContext);
|
||||
@@ -264,61 +300,8 @@ public class DbService : IDisposable
|
||||
var tempFileName = Path.GetRandomFileName();
|
||||
await File.WriteAllBytesAsync(tempFileName, bytes);
|
||||
|
||||
/*using (var command = _sqlConnection.CreateCommand())
|
||||
{
|
||||
// Empty all tables in the original database
|
||||
command.CommandText = @"
|
||||
SELECT 'DELETE FROM ' || name || ';'
|
||||
FROM sqlite_master
|
||||
WHERE type = 'table' AND name NOT LIKE 'sqlite_%';";
|
||||
var emptyTableCommands = new List<string>();
|
||||
using (var reader = await command.ExecuteReaderAsync())
|
||||
{
|
||||
while (await reader.ReadAsync())
|
||||
{
|
||||
emptyTableCommands.Add(reader.GetString(0));
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var emptyTableCommand in emptyTableCommands)
|
||||
{
|
||||
command.CommandText = emptyTableCommand;
|
||||
await command.ExecuteNonQueryAsync();
|
||||
}
|
||||
|
||||
// Attach the imported database and copy tables
|
||||
command.CommandText = "ATTACH DATABASE @fileName AS importDb";
|
||||
command.Parameters.Add(new SqliteParameter("@fileName", tempFileName));
|
||||
await command.ExecuteNonQueryAsync();
|
||||
|
||||
command.CommandText = @"
|
||||
SELECT 'INSERT INTO main.' || name || ' SELECT * FROM importDb.' || name || ';'
|
||||
FROM sqlite_master
|
||||
WHERE type = 'table' AND name NOT LIKE 'sqlite_%';";
|
||||
var tableInsertCommands = new List<string>();
|
||||
using (var reader = await command.ExecuteReaderAsync())
|
||||
{
|
||||
while (await reader.ReadAsync())
|
||||
{
|
||||
tableInsertCommands.Add(reader.GetString(0));
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var tableInsertCommand in tableInsertCommands)
|
||||
{
|
||||
command.CommandText = tableInsertCommand;
|
||||
await command.ExecuteNonQueryAsync();
|
||||
}
|
||||
|
||||
command.CommandText = "DETACH DATABASE importDb";
|
||||
await command.ExecuteNonQueryAsync();
|
||||
}
|
||||
*/
|
||||
|
||||
using (var command = _sqlConnection.CreateCommand())
|
||||
{
|
||||
Console.WriteLine("Dropping main tables..");
|
||||
|
||||
// Disable foreign key constraints
|
||||
command.CommandText = "PRAGMA foreign_keys = OFF;";
|
||||
await command.ExecuteNonQueryAsync();
|
||||
@@ -339,8 +322,6 @@ public class DbService : IDisposable
|
||||
|
||||
foreach (var dropTableCommand in dropTableCommands)
|
||||
{
|
||||
Console.WriteLine("Dropping table..");
|
||||
Console.WriteLine("Drop command: " + dropTableCommand);
|
||||
command.CommandText = dropTableCommand;
|
||||
await command.ExecuteNonQueryAsync();
|
||||
}
|
||||
@@ -350,8 +331,6 @@ public class DbService : IDisposable
|
||||
command.Parameters.Add(new SqliteParameter("@fileName", tempFileName));
|
||||
await command.ExecuteNonQueryAsync();
|
||||
|
||||
Console.WriteLine("Make create table statements from import db..");
|
||||
|
||||
// Get CREATE TABLE statements from the imported database
|
||||
command.CommandText = @"
|
||||
SELECT sql
|
||||
@@ -367,8 +346,6 @@ public class DbService : IDisposable
|
||||
}
|
||||
|
||||
// Create tables in the main database
|
||||
Console.WriteLine("Create tables in main db..");
|
||||
|
||||
foreach (var createTableCommand in createTableCommands)
|
||||
{
|
||||
command.CommandText = createTableCommand;
|
||||
@@ -376,8 +353,6 @@ public class DbService : IDisposable
|
||||
}
|
||||
|
||||
// Copy data from imported database to main database
|
||||
Console.WriteLine("Copy from import to main db.");
|
||||
|
||||
command.CommandText = @"
|
||||
SELECT 'INSERT INTO main.' || name || ' SELECT * FROM importDb.' || name || ';'
|
||||
FROM importDb.sqlite_master
|
||||
@@ -415,6 +390,8 @@ public class DbService : IDisposable
|
||||
/// <returns>Task.</returns>
|
||||
private async Task<bool> LoadDatabaseFromServerAsync()
|
||||
{
|
||||
_state.UpdateState(DbServiceState.DatabaseStatus.Loading);
|
||||
|
||||
// Load from webapi.
|
||||
try
|
||||
{
|
||||
@@ -425,28 +402,32 @@ public class DbService : IDisposable
|
||||
// on client is sufficient.
|
||||
if (string.IsNullOrEmpty(vault.Blob))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Attempt to decrypt the database blob.
|
||||
string decryptedBase64String = await _jsRuntime.InvokeAsync<string>("cryptoInterop.decrypt", vault.Blob, _authService.GetEncryptionKeyAsBase64Async());
|
||||
await ImportDbContextFromBase64Async(decryptedBase64String);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// If decryption fails it can indicate that the master password hash is not correct anymore,
|
||||
// so we logout the user just in case.
|
||||
Console.WriteLine(ex.Message);
|
||||
// Create the database structure from scratch to get an empty ready-to-use database.
|
||||
_state.UpdateState(DbServiceState.DatabaseStatus.Creating);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Attempt to decrypt the database blob.
|
||||
string decryptedBase64String = await _jsRuntime.InvokeAsync<string>("cryptoInterop.decrypt", vault.Blob, _authService.GetEncryptionKeyAsBase64Async());
|
||||
await ImportDbContextFromBase64Async(decryptedBase64String);
|
||||
|
||||
// Check if database is up to date with migrations.
|
||||
var pendingMigrations = await _dbContext.Database.GetPendingMigrationsAsync();
|
||||
if (pendingMigrations.Any())
|
||||
{
|
||||
_state.UpdateState(DbServiceState.DatabaseStatus.PendingMigrations);
|
||||
return false;
|
||||
}
|
||||
|
||||
_isSuccessfullyInitialized = true;
|
||||
_state.UpdateState(DbServiceState.DatabaseStatus.Ready);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine(ex.Message);
|
||||
_state.UpdateState(DbServiceState.DatabaseStatus.DecryptionFailed);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -460,7 +441,8 @@ public class DbService : IDisposable
|
||||
/// <returns>True if save action succeeded.</returns>
|
||||
private async Task<bool> SaveToServerAsync(string encryptedDatabase)
|
||||
{
|
||||
var vaultObject = new Vault(encryptedDatabase, DateTime.Now, DateTime.Now);
|
||||
var databaseVersion = await GetCurrentDatabaseVersionAsync();
|
||||
var vaultObject = new Vault(encryptedDatabase, databaseVersion, DateTime.Now, DateTime.Now);
|
||||
|
||||
try
|
||||
{
|
||||
|
||||
@@ -29,6 +29,16 @@ public class DbServiceState
|
||||
/// </summary>
|
||||
Uninitialized,
|
||||
|
||||
/// <summary>
|
||||
/// Database is loading from server.
|
||||
/// </summary>
|
||||
Loading,
|
||||
|
||||
/// <summary>
|
||||
/// Database is being created.
|
||||
/// </summary>
|
||||
Creating,
|
||||
|
||||
/// <summary>
|
||||
/// Database failed to decrypt. No data is accessible.
|
||||
/// </summary>
|
||||
@@ -44,11 +54,6 @@ public class DbServiceState
|
||||
/// </summary>
|
||||
Ready,
|
||||
|
||||
/// <summary>
|
||||
/// Database is loading from server.
|
||||
/// </summary>
|
||||
LoadingFromServer,
|
||||
|
||||
/// <summary>
|
||||
/// Database is saving to server.
|
||||
/// </summary>
|
||||
@@ -133,5 +138,22 @@ public class DbServiceState
|
||||
/// Gets or sets the last time the state was updated.
|
||||
/// </summary>
|
||||
public DateTime LastUpdated { get; set; } = DateTime.Now;
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the database state represents a initialized state.
|
||||
/// </summary>
|
||||
/// <returns>Bool.</returns>
|
||||
public bool IsInitialized()
|
||||
{
|
||||
if (Status == DatabaseStatus.Uninitialized ||
|
||||
Status == DatabaseStatus.PendingMigrations ||
|
||||
Status == DatabaseStatus.Loading ||
|
||||
Status == DatabaseStatus.DecryptionFailed)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1147,6 +1147,11 @@ video {
|
||||
background-color: rgb(255 255 255 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-gray-100 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(243 244 246 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-opacity-50 {
|
||||
--tw-bg-opacity: 0.5;
|
||||
}
|
||||
@@ -1403,6 +1408,16 @@ video {
|
||||
color: rgb(255 255 255 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.text-blue-600 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(37 99 235 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.text-green-600 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(22 163 74 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.opacity-0 {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
370
src/Databases/AliasServerDb/Migrations/20240708113743_AddVaultVersionColumn.Designer.cs
generated
Normal file
370
src/Databases/AliasServerDb/Migrations/20240708113743_AddVaultVersionColumn.Designer.cs
generated
Normal file
@@ -0,0 +1,370 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using AliasServerDb;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace AliasServerDb.Migrations
|
||||
{
|
||||
[DbContext(typeof(AliasServerDbContext))]
|
||||
[Migration("20240708113743_AddVaultVersionColumn")]
|
||||
partial class AddVaultVersionColumn
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "8.0.6")
|
||||
.HasAnnotation("Proxies:ChangeTracking", false)
|
||||
.HasAnnotation("Proxies:CheckEquality", false)
|
||||
.HasAnnotation("Proxies:LazyLoading", true);
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.AliasVaultUser", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("AccessFailedCount")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("EmailConfirmed")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("LockoutEnabled")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTimeOffset?>("LockoutEnd")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("NormalizedEmail")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("NormalizedUserName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PasswordHash")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PhoneNumber")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("PhoneNumberConfirmed")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Salt")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SecurityStamp")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("TwoFactorEnabled")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("UserName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Verifier")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("NormalizedEmail")
|
||||
.HasDatabaseName("EmailIndex");
|
||||
|
||||
b.HasIndex("NormalizedUserName")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("UserNameIndex");
|
||||
|
||||
b.ToTable("AspNetUsers", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.AspNetUserRefreshToken", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("DeviceIdentifier")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("ExpireDate")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("AspNetUserRefreshTokens");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.Vault", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("VaultBlob")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Version")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("Vaults");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("NormalizedName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("NormalizedName")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("RoleNameIndex");
|
||||
|
||||
b.ToTable("AspNetRoles", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("ClaimType")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ClaimValue")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("RoleId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("RoleId");
|
||||
|
||||
b.ToTable("AspNetRoleClaims", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("ClaimType")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ClaimValue")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("AspNetUserClaims", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||
{
|
||||
b.Property<string>("LoginProvider")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ProviderKey")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ProviderDisplayName")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("LoginProvider", "ProviderKey");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("AspNetUserLogins", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
||||
{
|
||||
b.Property<string>("UserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("RoleId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("UserId", "RoleId");
|
||||
|
||||
b.HasIndex("RoleId");
|
||||
|
||||
b.ToTable("AspNetUserRoles", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||
{
|
||||
b.Property<string>("UserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("LoginProvider")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("UserId", "LoginProvider", "Name");
|
||||
|
||||
b.ToTable("AspNetUserTokens", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.AspNetUserRefreshToken", b =>
|
||||
{
|
||||
b.HasOne("AliasServerDb.AliasVaultUser", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.Vault", b =>
|
||||
{
|
||||
b.HasOne("AliasServerDb.AliasVaultUser", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
||||
{
|
||||
b.HasOne("AliasServerDb.AliasVaultUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||
{
|
||||
b.HasOne("AliasServerDb.AliasVaultUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("AliasServerDb.AliasVaultUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||
{
|
||||
b.HasOne("AliasServerDb.AliasVaultUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace AliasServerDb.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddVaultVersionColumn : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "Version",
|
||||
table: "Vaults",
|
||||
type: "TEXT",
|
||||
maxLength: 255,
|
||||
nullable: false,
|
||||
defaultValue: "");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Version",
|
||||
table: "Vaults");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -149,6 +149,11 @@ namespace AliasServerDb.Migrations
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Version")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
@@ -37,6 +37,12 @@ public class Vault
|
||||
/// </summary>
|
||||
public string VaultBlob { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the vault data model version.
|
||||
/// </summary>
|
||||
[StringLength(255)]
|
||||
public string Version { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets created timestamp.
|
||||
/// </summary>
|
||||
|
||||
@@ -30,15 +30,15 @@ public class VaultRetentionManagerTests
|
||||
now = new DateTime(2023, 6, 1, 12, 0, 0); // Set a fixed "now" date for testing: June 1, 2023, 12:00 PM
|
||||
testVaults =
|
||||
[
|
||||
new Vault { UpdatedAt = new DateTime(2023, 5, 31, 12, 0, 0) },
|
||||
new Vault { UpdatedAt = new DateTime(2023, 5, 31, 4, 0, 0) },
|
||||
new Vault { UpdatedAt = new DateTime(2023, 5, 30, 12, 0, 0) }, // 2 days ago
|
||||
new Vault { UpdatedAt = new DateTime(2023, 5, 29, 12, 0, 0) }, // 3 days ago
|
||||
new Vault { UpdatedAt = new DateTime(2023, 5, 28, 12, 0, 0) }, // 4 days ago
|
||||
new Vault { UpdatedAt = new DateTime(2023, 5, 18, 12, 0, 0) }, // 2 weeks ago
|
||||
new Vault { UpdatedAt = new DateTime(2023, 5, 11, 12, 0, 0) }, // 3 weeks ago
|
||||
new Vault { UpdatedAt = new DateTime(2023, 5, 1, 12, 0, 0) }, // 1 month ago
|
||||
new Vault { UpdatedAt = new DateTime(2023, 4, 1, 12, 0, 0) }, // 2 months ago
|
||||
new Vault { Version = "1.1.0", UpdatedAt = new DateTime(2023, 5, 31, 12, 0, 0) },
|
||||
new Vault { Version = "1.1.0", UpdatedAt = new DateTime(2023, 5, 31, 4, 0, 0) },
|
||||
new Vault { Version = "1.1.0", UpdatedAt = new DateTime(2023, 5, 30, 12, 0, 0) }, // 2 days ago
|
||||
new Vault { Version = "1.1.0", UpdatedAt = new DateTime(2023, 5, 29, 12, 0, 0) }, // 3 days ago
|
||||
new Vault { Version = "1.0.3", UpdatedAt = new DateTime(2023, 5, 28, 12, 0, 0) }, // 4 days ago
|
||||
new Vault { Version = "1.0.3", UpdatedAt = new DateTime(2023, 5, 18, 12, 0, 0) }, // 2 weeks ago
|
||||
new Vault { Version = "1.0.3", UpdatedAt = new DateTime(2023, 5, 11, 12, 0, 0) }, // 3 weeks ago
|
||||
new Vault { Version = "1.0.2", UpdatedAt = new DateTime(2023, 5, 1, 12, 0, 0) }, // 1 month ago
|
||||
new Vault { Version = "1.0.1", UpdatedAt = new DateTime(2023, 4, 1, 12, 0, 0) }, // 2 months ago
|
||||
];
|
||||
}
|
||||
|
||||
@@ -101,6 +101,23 @@ public class VaultRetentionManagerTests
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test the VersionRetentionRule.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void VersionRetentionRuleTest()
|
||||
{
|
||||
var rule = new VersionRetentionRule { VersionsToKeep = 2 };
|
||||
var result = rule.ApplyRule(testVaults, now).ToList();
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(result, Has.Count.EqualTo(2));
|
||||
Assert.That(result[0].UpdatedAt, Is.EqualTo(new DateTime(2023, 5, 31, 12, 0, 0)));
|
||||
Assert.That(result[1].UpdatedAt, Is.EqualTo(new DateTime(2023, 5, 28, 12, 0, 0)));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test the RetentionPolicy object.
|
||||
/// </summary>
|
||||
@@ -114,6 +131,7 @@ public class VaultRetentionManagerTests
|
||||
new DailyRetentionRule { DaysToKeep = 2 },
|
||||
new WeeklyRetentionRule { WeeksToKeep = 2 },
|
||||
new MonthlyRetentionRule { MonthsToKeep = 1 },
|
||||
new VersionRetentionRule { VersionsToKeep = 3 },
|
||||
},
|
||||
};
|
||||
|
||||
@@ -130,11 +148,12 @@ public class VaultRetentionManagerTests
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(vaultsToKeep, Has.Count.EqualTo(3));
|
||||
Assert.That(vaultsToDelete, Has.Count.EqualTo(6));
|
||||
Assert.That(vaultsToKeep, Has.Count.EqualTo(4));
|
||||
Assert.That(vaultsToDelete, Has.Count.EqualTo(5));
|
||||
Assert.That(vaultsToKeep[0].UpdatedAt, Is.EqualTo(new DateTime(2023, 5, 31, 12, 0, 0)));
|
||||
Assert.That(vaultsToKeep[1].UpdatedAt, Is.EqualTo(new DateTime(2023, 5, 30, 12, 0, 0)));
|
||||
Assert.That(vaultsToKeep[2].UpdatedAt, Is.EqualTo(new DateTime(2023, 5, 28, 12, 0, 0)));
|
||||
Assert.That(vaultsToKeep[3].UpdatedAt, Is.EqualTo(new DateTime(2023, 5, 1, 12, 0, 0)));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user