mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-05-06 22:36:27 -04:00
Merge pull request #91 from lanedirt/74-add-versioning-support-to-local-sqlite-implementation-with-local-upgrade-paths
Add versioning support to local sqlite implementation with local upgrade paths
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>
|
||||
@@ -70,6 +73,9 @@
|
||||
<Link>.dockerignore</Link>
|
||||
</Content>
|
||||
<AdditionalFiles Include="..\stylecop.json" Link="stylecop.json" />
|
||||
<Content Update="wwwroot\appsettings.Development.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -86,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>
|
||||
|
||||
@@ -77,7 +77,10 @@
|
||||
|
||||
if (!result.IsSuccessStatusCode)
|
||||
{
|
||||
LoginBase.ParseResponse(responseContent);
|
||||
foreach (var error in LoginBase.ParseResponse(responseContent))
|
||||
{
|
||||
_serverValidationErrors.AddError(error);
|
||||
}
|
||||
StateHasChanged();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -75,7 +75,11 @@
|
||||
|
||||
try
|
||||
{
|
||||
await ProcessLoginAsync(Email, _unlockModel.Password);
|
||||
var errors = await ProcessLoginAsync(Email, _unlockModel.Password);
|
||||
foreach (var error in errors)
|
||||
{
|
||||
_serverValidationErrors.AddError(error);
|
||||
}
|
||||
StateHasChanged();
|
||||
}
|
||||
#if DEBUG
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
@inject NavigationManager NavigationManager
|
||||
|
||||
<div @onclick="ShowDetails" class="p-4 space-y-2 bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700
|
||||
<div @onclick="ShowDetails" class="credential-card p-4 space-y-2 bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700
|
||||
dark:bg-gray-800 cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors duration-200">
|
||||
<div class="px-4 py-2 text-gray-400 rounded text-center flex flex-col items-center">
|
||||
<DisplayFavicon faviconBytes="@Obj.Logo"></DisplayFavicon>
|
||||
|
||||
@@ -34,17 +34,12 @@ else
|
||||
private async void OnDatabaseStateChanged(object? sender, DbServiceState.DatabaseState newState)
|
||||
{
|
||||
await InvokeAsync(StateHasChanged);
|
||||
if (newState.Status == DbServiceState.DatabaseStatus.Saving)
|
||||
if (newState.Status == DbServiceState.DatabaseStatus.SavingToServer)
|
||||
{
|
||||
// Show loading indicator for at least 0.5 seconds even if the save operation is faster.
|
||||
Message = "Saving...";
|
||||
await ShowLoadingIndicatorAsync();
|
||||
}
|
||||
else if (newState.Status == DbServiceState.DatabaseStatus.Loading)
|
||||
{
|
||||
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,10 +1,4 @@
|
||||
@inherits LayoutComponentBase
|
||||
@implements IDisposable
|
||||
@inject DbService DbService
|
||||
@inject AuthenticationStateProvider AuthStateProvider
|
||||
@inject NavigationManager NavigationManager
|
||||
@using Microsoft.AspNetCore.Components.Authorization
|
||||
@using AliasVault.WebApp.Providers
|
||||
|
||||
<CascadingAuthenticationState>
|
||||
<AuthorizeView>
|
||||
@@ -12,28 +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
|
||||
{
|
||||
<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>
|
||||
}
|
||||
<main>
|
||||
@Body
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
</div>
|
||||
</Authorized>
|
||||
@@ -46,94 +22,5 @@
|
||||
</CascadingAuthenticationState>
|
||||
|
||||
@code {
|
||||
private bool IsDbInitialized { get; set; } = false;
|
||||
private const int MinimumLoadingTimeMs = 800;
|
||||
|
||||
[CascadingParameter] private Task<AuthenticationState>? AuthState { get; set; }
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await base.OnInitializedAsync();
|
||||
|
||||
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.Initialized)
|
||||
{
|
||||
IsDbInitialized = 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.Initialized)
|
||||
{
|
||||
IsDbInitialized = 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);
|
||||
}
|
||||
|
||||
IsDbInitialized = true;
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,7 @@ using Microsoft.JSInterop;
|
||||
/// </summary>
|
||||
public class MainBase : OwningComponentBase
|
||||
{
|
||||
private const string ReturnUrlKey = "returnUrl";
|
||||
private bool _parametersInitialSet;
|
||||
|
||||
/// <summary>
|
||||
@@ -91,6 +92,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(ReturnUrlKey, currentUrl);
|
||||
|
||||
NavigationManager.NavigateTo("/sync");
|
||||
while (true)
|
||||
{
|
||||
await Task.Delay(200);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -106,6 +120,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(ReturnUrlKey, currentUrl);
|
||||
|
||||
NavigationManager.NavigateTo("/sync");
|
||||
while (true)
|
||||
{
|
||||
await Task.Delay(200);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -152,13 +179,13 @@ public class MainBase : OwningComponentBase
|
||||
if (!AuthService.IsEncryptionKeySet())
|
||||
{
|
||||
// If returnUrl is not set and current URL is not unlock page, set it to the current URL.
|
||||
var localStorageReturnUrl = await LocalStorage.GetItemAsync<string>("returnUrl");
|
||||
var localStorageReturnUrl = await LocalStorage.GetItemAsync<string>(ReturnUrlKey);
|
||||
if (string.IsNullOrEmpty(localStorageReturnUrl))
|
||||
{
|
||||
var currentUrl = NavigationManager.Uri;
|
||||
if (!currentUrl.Contains("unlock"))
|
||||
{
|
||||
await LocalStorage.SetItemAsync("returnUrl", currentUrl);
|
||||
await LocalStorage.SetItemAsync(ReturnUrlKey, currentUrl);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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">
|
||||
<div class="space-y-4">
|
||||
<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="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(1500);
|
||||
|
||||
// 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>
|
||||
126
src/AliasVault.WebApp/Main/Pages/Sync/Sync.razor
Normal file
126
src/AliasVault.WebApp/Main/Pages/Sync/Sync.razor
Normal file
@@ -0,0 +1,126 @@
|
||||
@page "/sync"
|
||||
@layout EmptyLayout
|
||||
@implements IDisposable
|
||||
@using AliasVault.WebApp.Main.Pages.Sync.StatusMessages
|
||||
@inject ILocalStorageService LocalStorage
|
||||
@inject DbService DbService
|
||||
@inject AuthService AuthService
|
||||
@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 const string ReturnUrlKey = "returnUrl";
|
||||
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;
|
||||
|
||||
// Check that encryption key is set. If not, redirect to unlock screen.
|
||||
if (!AuthService.IsEncryptionKeySet())
|
||||
{
|
||||
await LocalStorage.SetItemAsync(ReturnUrlKey, NavigationManager.Uri);
|
||||
NavigationManager.NavigateTo("/unlock");
|
||||
}
|
||||
|
||||
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>(ReturnUrlKey);
|
||||
if (!string.IsNullOrEmpty(localStorageReturnUrl) && localStorageReturnUrl != "/sync")
|
||||
{
|
||||
await LocalStorage.RemoveItemAsync(ReturnUrlKey);
|
||||
NavigationManager.NavigateTo(localStorageReturnUrl);
|
||||
}
|
||||
else
|
||||
{
|
||||
NavigationManager.NavigateTo("/");
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
DbService.GetState().StateChanged -= OnDatabaseStateChanged;
|
||||
}
|
||||
}
|
||||
@@ -163,19 +163,19 @@ public class AuthService(HttpClient httpClient, ILocalStorageService localStorag
|
||||
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
|
||||
public async Task RemoveTokensAsync()
|
||||
{
|
||||
await localStorage.RemoveItemAsync(AccessTokenKey);
|
||||
await localStorage.RemoveItemAsync(RefreshTokenKey);
|
||||
|
||||
// If the remote call fails we catch the exception and ignore it.
|
||||
// This is because the user is already logged out and we don't want to trigger another refresh token request.
|
||||
// Revoke the tokens from the server by calling the webapi.
|
||||
try
|
||||
{
|
||||
await RevokeTokenAsync();
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Ignore the exception
|
||||
// If an exception occurs we ignore it and continue with removing the tokens from local storage.
|
||||
}
|
||||
|
||||
// Remove the tokens from local storage.
|
||||
await localStorage.RemoveItemAsync(AccessTokenKey);
|
||||
await localStorage.RemoveItemAsync(RefreshTokenKey);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -13,6 +13,7 @@ using AliasClientDb;
|
||||
using AliasVault.Shared.Models.WebApi;
|
||||
using AliasVault.WebApp.Services.Auth;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.JSInterop;
|
||||
|
||||
/// <summary>
|
||||
@@ -72,22 +73,14 @@ public class DbService : IDisposable
|
||||
return;
|
||||
}
|
||||
|
||||
_state.UpdateState(DbServiceState.DatabaseStatus.Loading);
|
||||
|
||||
// Ensure the in-memory database representation is created and has the necessary tables.
|
||||
await _dbContext.Database.EnsureCreatedAsync();
|
||||
|
||||
// Attempt to fill the local database with a previously saved database stored on the server.
|
||||
var loaded = await LoadDatabaseFromServerAsync();
|
||||
if (loaded)
|
||||
{
|
||||
_isSuccessfullyInitialized = true;
|
||||
_state.UpdateState(DbServiceState.DatabaseStatus.Initialized);
|
||||
Console.WriteLine("Database succesfully loaded from server.");
|
||||
_retryCount = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
_state.UpdateState(DbServiceState.DatabaseStatus.Error);
|
||||
Console.WriteLine("Failed to load database from server.");
|
||||
}
|
||||
}
|
||||
@@ -122,7 +115,7 @@ public class DbService : IDisposable
|
||||
public async Task SaveDatabaseAsync()
|
||||
{
|
||||
// Set the initial state of the database service.
|
||||
_state.UpdateState(DbServiceState.DatabaseStatus.Saving);
|
||||
_state.UpdateState(DbServiceState.DatabaseStatus.SavingToServer);
|
||||
|
||||
// Save the actual dbContext.
|
||||
await _dbContext.SaveChangesAsync();
|
||||
@@ -136,13 +129,13 @@ public class DbService : IDisposable
|
||||
var success = await SaveToServerAsync(encryptedBase64String);
|
||||
if (success)
|
||||
{
|
||||
Console.WriteLine("Database succesfully saved to server.");
|
||||
_state.UpdateState(DbServiceState.DatabaseStatus.Initialized);
|
||||
Console.WriteLine("Database successfully saved to server.");
|
||||
_state.UpdateState(DbServiceState.DatabaseStatus.Ready);
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine("Failed to save database to server.");
|
||||
_state.UpdateState(DbServiceState.DatabaseStatus.Error);
|
||||
_state.UpdateState(DbServiceState.DatabaseStatus.OperationError);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -173,13 +166,86 @@ public class DbService : IDisposable
|
||||
return base64String;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Migrate the database structure to the latest version.
|
||||
/// </summary>
|
||||
/// <returns>Bool which indicates if migration was succesful.</returns>
|
||||
public async Task<bool> MigrateDatabaseAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
await _dbContext.Database.MigrateAsync();
|
||||
_isSuccessfullyInitialized = true;
|
||||
_state.UpdateState(DbServiceState.DatabaseStatus.Ready);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine(ex.Message);
|
||||
return false;
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
@@ -189,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);
|
||||
@@ -234,9 +302,13 @@ public class DbService : IDisposable
|
||||
|
||||
using (var command = _sqlConnection.CreateCommand())
|
||||
{
|
||||
// Disable foreign key constraints
|
||||
command.CommandText = "PRAGMA foreign_keys = OFF;";
|
||||
await command.ExecuteNonQueryAsync();
|
||||
|
||||
// Drop all tables in the original database
|
||||
command.CommandText = @"
|
||||
SELECT 'DELETE FROM ' || name || ';'
|
||||
SELECT 'DROP TABLE IF EXISTS ' || name || ';'
|
||||
FROM sqlite_master
|
||||
WHERE type = 'table' AND name NOT LIKE 'sqlite_%';";
|
||||
var dropTableCommands = new List<string>();
|
||||
@@ -254,14 +326,36 @@ public class DbService : IDisposable
|
||||
await command.ExecuteNonQueryAsync();
|
||||
}
|
||||
|
||||
// Attach the imported database and copy tables
|
||||
// Attach the imported database
|
||||
command.CommandText = "ATTACH DATABASE @fileName AS importDb";
|
||||
command.Parameters.Add(new SqliteParameter("@fileName", tempFileName));
|
||||
await command.ExecuteNonQueryAsync();
|
||||
|
||||
// Get CREATE TABLE statements from the imported database
|
||||
command.CommandText = @"
|
||||
SELECT sql
|
||||
FROM importDb.sqlite_master
|
||||
WHERE type = 'table' AND name NOT LIKE 'sqlite_%';";
|
||||
var createTableCommands = new List<string>();
|
||||
using (var reader = await command.ExecuteReaderAsync())
|
||||
{
|
||||
while (await reader.ReadAsync())
|
||||
{
|
||||
createTableCommands.Add(reader.GetString(0));
|
||||
}
|
||||
}
|
||||
|
||||
// Create tables in the main database
|
||||
foreach (var createTableCommand in createTableCommands)
|
||||
{
|
||||
command.CommandText = createTableCommand;
|
||||
await command.ExecuteNonQueryAsync();
|
||||
}
|
||||
|
||||
// Copy data from imported database to main database
|
||||
command.CommandText = @"
|
||||
SELECT 'INSERT INTO main.' || name || ' SELECT * FROM importDb.' || name || ';'
|
||||
FROM sqlite_master
|
||||
FROM importDb.sqlite_master
|
||||
WHERE type = 'table' AND name NOT LIKE 'sqlite_%';";
|
||||
var tableInsertCommands = new List<string>();
|
||||
using (var reader = await command.ExecuteReaderAsync())
|
||||
@@ -278,8 +372,13 @@ public class DbService : IDisposable
|
||||
await command.ExecuteNonQueryAsync();
|
||||
}
|
||||
|
||||
// Detach the imported database
|
||||
command.CommandText = "DETACH DATABASE importDb";
|
||||
await command.ExecuteNonQueryAsync();
|
||||
|
||||
// Re-enable foreign key constraints
|
||||
command.CommandText = "PRAGMA foreign_keys = ON;";
|
||||
await command.ExecuteNonQueryAsync();
|
||||
}
|
||||
|
||||
File.Delete(tempFileName);
|
||||
@@ -291,6 +390,8 @@ public class DbService : IDisposable
|
||||
/// <returns>Task.</returns>
|
||||
private async Task<bool> LoadDatabaseFromServerAsync()
|
||||
{
|
||||
_state.UpdateState(DbServiceState.DatabaseStatus.Loading);
|
||||
|
||||
// Load from webapi.
|
||||
try
|
||||
{
|
||||
@@ -301,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;
|
||||
}
|
||||
|
||||
@@ -336,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,25 +29,40 @@ public class DbServiceState
|
||||
/// </summary>
|
||||
Uninitialized,
|
||||
|
||||
/// <summary>
|
||||
/// Database is initialized but no task is currently in progress.
|
||||
/// </summary>
|
||||
Initialized,
|
||||
|
||||
/// <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>
|
||||
DecryptionFailed,
|
||||
|
||||
/// <summary>
|
||||
/// Database has been decrypted but has pending migrations and needs to be updated.
|
||||
/// </summary>
|
||||
PendingMigrations,
|
||||
|
||||
/// <summary>
|
||||
/// Database is ready but no task is currently in progress.
|
||||
/// </summary>
|
||||
Ready,
|
||||
|
||||
/// <summary>
|
||||
/// Database is saving to server.
|
||||
/// </summary>
|
||||
Saving,
|
||||
SavingToServer,
|
||||
|
||||
/// <summary>
|
||||
/// An error occurred during a database operation.
|
||||
/// </summary>
|
||||
Error,
|
||||
OperationError,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -123,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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"ApiUrl": "http://localhost:5092",
|
||||
"UseDebugEncryptionKey": "false"
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -32,14 +32,14 @@ public class Alias
|
||||
/// </summary>
|
||||
[StringLength(255)]
|
||||
[Column(TypeName = "VARCHAR")]
|
||||
public string? FirstName { get; set; } = null!;
|
||||
public string? FirstName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the last name.
|
||||
/// </summary>
|
||||
[StringLength(255)]
|
||||
[Column(TypeName = "VARCHAR")]
|
||||
public string? LastName { get; set; } = null!;
|
||||
public string? LastName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the nickname.
|
||||
|
||||
@@ -42,4 +42,8 @@
|
||||
</AdditionalFiles>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Migrations\" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
278
src/Databases/AliasClientDb/Migrations/20240708094944_1.0.0-InitialMigration.Designer.cs
generated
Normal file
278
src/Databases/AliasClientDb/Migrations/20240708094944_1.0.0-InitialMigration.Designer.cs
generated
Normal file
@@ -0,0 +1,278 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using AliasClientDb;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace AliasClientDb.Migrations
|
||||
{
|
||||
[DbContext(typeof(AliasClientDbContext))]
|
||||
[Migration("20240708094944_1.0.0-InitialMigration")]
|
||||
partial class _100InitialMigration
|
||||
{
|
||||
/// <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("AliasClientDb.Alias", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("AddressCity")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("VARCHAR");
|
||||
|
||||
b.Property<string>("AddressCountry")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("VARCHAR");
|
||||
|
||||
b.Property<string>("AddressState")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("VARCHAR");
|
||||
|
||||
b.Property<string>("AddressStreet")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("VARCHAR");
|
||||
|
||||
b.Property<string>("AddressZipCode")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("VARCHAR");
|
||||
|
||||
b.Property<string>("BankAccountIBAN")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("BirthDate")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("EmailPrefix")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("FirstName")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("VARCHAR");
|
||||
|
||||
b.Property<string>("Gender")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("VARCHAR");
|
||||
|
||||
b.Property<string>("Hobbies")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("LastName")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("VARCHAR");
|
||||
|
||||
b.Property<string>("NickName")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("VARCHAR");
|
||||
|
||||
b.Property<string>("PhoneMobile")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Aliases");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasClientDb.Attachment", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<byte[]>("Blob")
|
||||
.IsRequired()
|
||||
.HasColumnType("BLOB");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("CredentialId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Filename")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CredentialId");
|
||||
|
||||
b.ToTable("Attachment");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasClientDb.Credential", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("AliasId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Notes")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("ServiceId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AliasId");
|
||||
|
||||
b.HasIndex("ServiceId");
|
||||
|
||||
b.ToTable("Credentials");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasClientDb.Password", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("CredentialId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CredentialId");
|
||||
|
||||
b.ToTable("Passwords");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasClientDb.Service", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<byte[]>("Logo")
|
||||
.HasColumnType("BLOB");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Url")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Services");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasClientDb.Attachment", b =>
|
||||
{
|
||||
b.HasOne("AliasClientDb.Credential", "Credential")
|
||||
.WithMany("Attachments")
|
||||
.HasForeignKey("CredentialId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Credential");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasClientDb.Credential", b =>
|
||||
{
|
||||
b.HasOne("AliasClientDb.Alias", "Alias")
|
||||
.WithMany("Credentials")
|
||||
.HasForeignKey("AliasId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("AliasClientDb.Service", "Service")
|
||||
.WithMany("Credentials")
|
||||
.HasForeignKey("ServiceId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Alias");
|
||||
|
||||
b.Navigation("Service");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasClientDb.Password", b =>
|
||||
{
|
||||
b.HasOne("AliasClientDb.Credential", "Credential")
|
||||
.WithMany("Passwords")
|
||||
.HasForeignKey("CredentialId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Credential");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasClientDb.Alias", b =>
|
||||
{
|
||||
b.Navigation("Credentials");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasClientDb.Credential", b =>
|
||||
{
|
||||
b.Navigation("Attachments");
|
||||
|
||||
b.Navigation("Passwords");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasClientDb.Service", b =>
|
||||
{
|
||||
b.Navigation("Credentials");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
// <auto-generated/>
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace AliasClientDb.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class _100InitialMigration : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Aliases",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "TEXT", nullable: false),
|
||||
Gender = table.Column<string>(type: "VARCHAR", maxLength: 255, nullable: true),
|
||||
FirstName = table.Column<string>(type: "VARCHAR", maxLength: 255, nullable: true),
|
||||
LastName = table.Column<string>(type: "VARCHAR", maxLength: 255, nullable: true),
|
||||
NickName = table.Column<string>(type: "VARCHAR", maxLength: 255, nullable: true),
|
||||
BirthDate = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||
AddressStreet = table.Column<string>(type: "VARCHAR", maxLength: 255, nullable: true),
|
||||
AddressCity = table.Column<string>(type: "VARCHAR", maxLength: 255, nullable: true),
|
||||
AddressState = table.Column<string>(type: "VARCHAR", maxLength: 255, nullable: true),
|
||||
AddressZipCode = table.Column<string>(type: "VARCHAR", maxLength: 255, nullable: true),
|
||||
AddressCountry = table.Column<string>(type: "VARCHAR", maxLength: 255, nullable: true),
|
||||
Hobbies = table.Column<string>(type: "TEXT", maxLength: 255, nullable: true),
|
||||
EmailPrefix = table.Column<string>(type: "TEXT", maxLength: 255, nullable: true),
|
||||
PhoneMobile = table.Column<string>(type: "TEXT", maxLength: 255, nullable: true),
|
||||
BankAccountIBAN = table.Column<string>(type: "TEXT", maxLength: 255, nullable: true),
|
||||
CreatedAt = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||
UpdatedAt = table.Column<DateTime>(type: "TEXT", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Aliases", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Services",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "TEXT", nullable: false),
|
||||
Name = table.Column<string>(type: "TEXT", maxLength: 255, nullable: true),
|
||||
Url = table.Column<string>(type: "TEXT", maxLength: 255, nullable: true),
|
||||
Logo = table.Column<byte[]>(type: "BLOB", nullable: true),
|
||||
CreatedAt = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||
UpdatedAt = table.Column<DateTime>(type: "TEXT", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Services", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Credentials",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "TEXT", nullable: false),
|
||||
AliasId = table.Column<Guid>(type: "TEXT", nullable: false),
|
||||
Notes = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Username = table.Column<string>(type: "TEXT", nullable: false),
|
||||
CreatedAt = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||
UpdatedAt = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||
ServiceId = table.Column<Guid>(type: "TEXT", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Credentials", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_Credentials_Aliases_AliasId",
|
||||
column: x => x.AliasId,
|
||||
principalTable: "Aliases",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_Credentials_Services_ServiceId",
|
||||
column: x => x.ServiceId,
|
||||
principalTable: "Services",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Attachment",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "TEXT", nullable: false),
|
||||
Filename = table.Column<string>(type: "TEXT", maxLength: 255, nullable: false),
|
||||
Blob = table.Column<byte[]>(type: "BLOB", nullable: false),
|
||||
CreatedAt = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||
UpdatedAt = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||
CredentialId = table.Column<Guid>(type: "TEXT", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Attachment", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_Attachment_Credentials_CredentialId",
|
||||
column: x => x.CredentialId,
|
||||
principalTable: "Credentials",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Passwords",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "TEXT", nullable: false),
|
||||
Value = table.Column<string>(type: "TEXT", maxLength: 255, nullable: true),
|
||||
CreatedAt = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||
UpdatedAt = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||
CredentialId = table.Column<Guid>(type: "TEXT", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Passwords", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_Passwords_Credentials_CredentialId",
|
||||
column: x => x.CredentialId,
|
||||
principalTable: "Credentials",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Attachment_CredentialId",
|
||||
table: "Attachment",
|
||||
column: "CredentialId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Credentials_AliasId",
|
||||
table: "Credentials",
|
||||
column: "AliasId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Credentials_ServiceId",
|
||||
table: "Credentials",
|
||||
column: "ServiceId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Passwords_CredentialId",
|
||||
table: "Passwords",
|
||||
column: "CredentialId");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "Attachment");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Passwords");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Credentials");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Aliases");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Services");
|
||||
}
|
||||
}
|
||||
}
|
||||
278
src/Databases/AliasClientDb/Migrations/20240708224522_1.0.1-EmptyTestMigration.Designer.cs
generated
Normal file
278
src/Databases/AliasClientDb/Migrations/20240708224522_1.0.1-EmptyTestMigration.Designer.cs
generated
Normal file
@@ -0,0 +1,278 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using AliasClientDb;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace AliasClientDb.Migrations
|
||||
{
|
||||
[DbContext(typeof(AliasClientDbContext))]
|
||||
[Migration("20240708224522_1.0.1-EmptyTestMigration")]
|
||||
partial class _101EmptyTestMigration
|
||||
{
|
||||
/// <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("AliasClientDb.Alias", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("AddressCity")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("VARCHAR");
|
||||
|
||||
b.Property<string>("AddressCountry")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("VARCHAR");
|
||||
|
||||
b.Property<string>("AddressState")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("VARCHAR");
|
||||
|
||||
b.Property<string>("AddressStreet")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("VARCHAR");
|
||||
|
||||
b.Property<string>("AddressZipCode")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("VARCHAR");
|
||||
|
||||
b.Property<string>("BankAccountIBAN")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("BirthDate")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("EmailPrefix")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("FirstName")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("VARCHAR");
|
||||
|
||||
b.Property<string>("Gender")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("VARCHAR");
|
||||
|
||||
b.Property<string>("Hobbies")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("LastName")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("VARCHAR");
|
||||
|
||||
b.Property<string>("NickName")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("VARCHAR");
|
||||
|
||||
b.Property<string>("PhoneMobile")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Aliases");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasClientDb.Attachment", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<byte[]>("Blob")
|
||||
.IsRequired()
|
||||
.HasColumnType("BLOB");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("CredentialId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Filename")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CredentialId");
|
||||
|
||||
b.ToTable("Attachment");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasClientDb.Credential", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("AliasId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Notes")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("ServiceId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AliasId");
|
||||
|
||||
b.HasIndex("ServiceId");
|
||||
|
||||
b.ToTable("Credentials");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasClientDb.Password", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("CredentialId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CredentialId");
|
||||
|
||||
b.ToTable("Passwords");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasClientDb.Service", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<byte[]>("Logo")
|
||||
.HasColumnType("BLOB");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Url")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Services");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasClientDb.Attachment", b =>
|
||||
{
|
||||
b.HasOne("AliasClientDb.Credential", "Credential")
|
||||
.WithMany("Attachments")
|
||||
.HasForeignKey("CredentialId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Credential");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasClientDb.Credential", b =>
|
||||
{
|
||||
b.HasOne("AliasClientDb.Alias", "Alias")
|
||||
.WithMany("Credentials")
|
||||
.HasForeignKey("AliasId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("AliasClientDb.Service", "Service")
|
||||
.WithMany("Credentials")
|
||||
.HasForeignKey("ServiceId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Alias");
|
||||
|
||||
b.Navigation("Service");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasClientDb.Password", b =>
|
||||
{
|
||||
b.HasOne("AliasClientDb.Credential", "Credential")
|
||||
.WithMany("Passwords")
|
||||
.HasForeignKey("CredentialId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Credential");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasClientDb.Alias", b =>
|
||||
{
|
||||
b.Navigation("Credentials");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasClientDb.Credential", b =>
|
||||
{
|
||||
b.Navigation("Attachments");
|
||||
|
||||
b.Navigation("Passwords");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasClientDb.Service", b =>
|
||||
{
|
||||
b.Navigation("Credentials");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
// <auto-generated/>
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace AliasClientDb.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class _101EmptyTestMigration : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,275 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using AliasClientDb;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace AliasClientDb.Migrations
|
||||
{
|
||||
[DbContext(typeof(AliasClientDbContext))]
|
||||
partial class AliasClientDbContextModelSnapshot : ModelSnapshot
|
||||
{
|
||||
protected override void BuildModel(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("AliasClientDb.Alias", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("AddressCity")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("VARCHAR");
|
||||
|
||||
b.Property<string>("AddressCountry")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("VARCHAR");
|
||||
|
||||
b.Property<string>("AddressState")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("VARCHAR");
|
||||
|
||||
b.Property<string>("AddressStreet")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("VARCHAR");
|
||||
|
||||
b.Property<string>("AddressZipCode")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("VARCHAR");
|
||||
|
||||
b.Property<string>("BankAccountIBAN")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("BirthDate")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("EmailPrefix")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("FirstName")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("VARCHAR");
|
||||
|
||||
b.Property<string>("Gender")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("VARCHAR");
|
||||
|
||||
b.Property<string>("Hobbies")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("LastName")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("VARCHAR");
|
||||
|
||||
b.Property<string>("NickName")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("VARCHAR");
|
||||
|
||||
b.Property<string>("PhoneMobile")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Aliases");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasClientDb.Attachment", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<byte[]>("Blob")
|
||||
.IsRequired()
|
||||
.HasColumnType("BLOB");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("CredentialId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Filename")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CredentialId");
|
||||
|
||||
b.ToTable("Attachment");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasClientDb.Credential", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("AliasId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Notes")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("ServiceId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AliasId");
|
||||
|
||||
b.HasIndex("ServiceId");
|
||||
|
||||
b.ToTable("Credentials");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasClientDb.Password", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("CredentialId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CredentialId");
|
||||
|
||||
b.ToTable("Passwords");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasClientDb.Service", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<byte[]>("Logo")
|
||||
.HasColumnType("BLOB");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Url")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Services");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasClientDb.Attachment", b =>
|
||||
{
|
||||
b.HasOne("AliasClientDb.Credential", "Credential")
|
||||
.WithMany("Attachments")
|
||||
.HasForeignKey("CredentialId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Credential");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasClientDb.Credential", b =>
|
||||
{
|
||||
b.HasOne("AliasClientDb.Alias", "Alias")
|
||||
.WithMany("Credentials")
|
||||
.HasForeignKey("AliasId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("AliasClientDb.Service", "Service")
|
||||
.WithMany("Credentials")
|
||||
.HasForeignKey("ServiceId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Alias");
|
||||
|
||||
b.Navigation("Service");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasClientDb.Password", b =>
|
||||
{
|
||||
b.HasOne("AliasClientDb.Credential", "Credential")
|
||||
.WithMany("Passwords")
|
||||
.HasForeignKey("CredentialId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Credential");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasClientDb.Alias", b =>
|
||||
{
|
||||
b.Navigation("Credentials");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasClientDb.Credential", b =>
|
||||
{
|
||||
b.Navigation("Attachments");
|
||||
|
||||
b.Navigation("Passwords");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasClientDb.Service", b =>
|
||||
{
|
||||
b.Navigation("Credentials");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
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>
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
namespace AliasVault.E2ETests.Common;
|
||||
|
||||
using AliasServerDb;
|
||||
using AliasVault.Shared.Providers.Time;
|
||||
using Microsoft.Playwright;
|
||||
|
||||
@@ -42,12 +43,12 @@ public class PlaywrightTest
|
||||
/// <summary>
|
||||
/// Gets or sets random unique account email that is used for the test.
|
||||
/// </summary>
|
||||
protected string TestUserEmail { get; set; } = string.Empty;
|
||||
protected virtual string TestUserEmail { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets random unique account password that is used for the test.
|
||||
/// </summary>
|
||||
protected string TestUserPassword { get; set; } = string.Empty;
|
||||
protected virtual string TestUserPassword { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the Playwright browser instance.
|
||||
@@ -69,6 +70,11 @@ public class PlaywrightTest
|
||||
/// </summary>
|
||||
protected PlaywrightInputHelper InputHelper { get; private set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the db context for the WebAPI project.
|
||||
/// </summary>
|
||||
protected AliasServerDbContext ApiDbContext => _apiFactory.GetDbContext();
|
||||
|
||||
/// <summary>
|
||||
/// One time setup for the Playwright test which runs before all tests in the class.
|
||||
/// </summary>
|
||||
@@ -228,9 +234,11 @@ public class PlaywrightTest
|
||||
/// <returns>Async task.</returns>
|
||||
private async Task Register()
|
||||
{
|
||||
// Generate random email and password
|
||||
TestUserEmail = $"{Guid.NewGuid()}@test.com";
|
||||
TestUserPassword = Guid.NewGuid().ToString();
|
||||
// If email is not set by test explicitly, generate a random email.
|
||||
TestUserEmail = TestUserEmail.Length > 0 ? TestUserEmail : $"{Guid.NewGuid()}@test.com";
|
||||
|
||||
// If password is not set by test explicitly, generate a random password.
|
||||
TestUserPassword = TestUserPassword.Length > 0 ? TestUserPassword : Guid.NewGuid().ToString();
|
||||
|
||||
// Check that we get redirected to /user/login when accessing the root URL and not authenticated.
|
||||
await Page.GotoAsync(AppBaseUrl);
|
||||
@@ -241,7 +249,7 @@ public class PlaywrightTest
|
||||
await registerButton.ClickAsync();
|
||||
await WaitForURLAsync("**/user/register");
|
||||
|
||||
// Try to login with test credentials.
|
||||
// Try to register an account with the generated test credentials.
|
||||
var emailField = Page.Locator("input[id='email']");
|
||||
var passwordField = Page.Locator("input[id='password']");
|
||||
var password2Field = Page.Locator("input[id='password2']");
|
||||
@@ -255,11 +263,10 @@ public class PlaywrightTest
|
||||
|
||||
// Check if we get redirected when clicking on the register button.
|
||||
var submitButton = Page.Locator("button[type='submit']");
|
||||
Console.WriteLine(submitButton);
|
||||
await submitButton.ClickAsync();
|
||||
|
||||
// Check if we get redirected to the root URL after registration which means we are logged in.
|
||||
await WaitForURLAsync(AppBaseUrl);
|
||||
await WaitForURLAsync(AppBaseUrl, "Find all of your credentials below");
|
||||
}
|
||||
|
||||
private async Task SetupEnvironment()
|
||||
@@ -278,11 +285,11 @@ public class PlaywrightTest
|
||||
|
||||
// Start WebAPI in-memory.
|
||||
_apiFactory.HostUrl = "http://localhost:" + apiPort;
|
||||
var apiClient = _apiFactory.CreateDefaultClient();
|
||||
_apiFactory.CreateDefaultClient();
|
||||
|
||||
// Start Blazor WASM app out-of-process.
|
||||
_wasmFactory.HostUrl = "http://localhost:" + appPort;
|
||||
var wasmClient = _wasmFactory.CreateDefaultClient();
|
||||
_wasmFactory.CreateDefaultClient();
|
||||
|
||||
// Set Playwright headless mode true if not in debug mode.
|
||||
bool isDebugMode = System.Diagnostics.Debugger.IsAttached;
|
||||
|
||||
@@ -24,6 +24,16 @@ using Microsoft.Extensions.Hosting;
|
||||
public class WebApplicationApiFactoryFixture<TEntryPoint> : WebApplicationFactory<TEntryPoint>
|
||||
where TEntryPoint : class
|
||||
{
|
||||
/// <summary>
|
||||
/// The DbContext instance that is created for the test.
|
||||
/// </summary>
|
||||
private AliasServerDbContext? _dbContext;
|
||||
|
||||
/// <summary>
|
||||
/// The DbConnection instance that is created for the test.
|
||||
/// </summary>
|
||||
private DbConnection? _dbConnection;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the URL the web application host will listen on.
|
||||
/// </summary>
|
||||
@@ -34,6 +44,24 @@ public class WebApplicationApiFactoryFixture<TEntryPoint> : WebApplicationFactor
|
||||
/// </summary>
|
||||
public TestTimeProvider TimeProvider { get; private set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Returns the DbContext instance for the test. This can be used to seed the database with test data.
|
||||
/// </summary>
|
||||
/// <returns>AliasServerDbContext instance.</returns>
|
||||
public AliasServerDbContext GetDbContext()
|
||||
{
|
||||
if (_dbContext == null)
|
||||
{
|
||||
var options = new DbContextOptionsBuilder<AliasServerDbContext>()
|
||||
.UseSqlite(_dbConnection!)
|
||||
.Options;
|
||||
|
||||
_dbContext = new AliasServerDbContext(options);
|
||||
}
|
||||
|
||||
return _dbContext;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void ConfigureWebHost(IWebHostBuilder builder)
|
||||
{
|
||||
@@ -88,10 +116,10 @@ public class WebApplicationApiFactoryFixture<TEntryPoint> : WebApplicationFactor
|
||||
// Create a new DbConnection and AliasServerDbContext with an in-memory database.
|
||||
services.AddSingleton<DbConnection>(container =>
|
||||
{
|
||||
var connection = new SqliteConnection("DataSource=:memory:");
|
||||
connection.Open();
|
||||
_dbConnection = new SqliteConnection("DataSource=:memory:");
|
||||
_dbConnection.Open();
|
||||
|
||||
return connection;
|
||||
return _dbConnection;
|
||||
});
|
||||
|
||||
services.AddDbContext<AliasServerDbContext>((container, options) =>
|
||||
@@ -114,7 +142,7 @@ public class WebApplicationApiFactoryFixture<TEntryPoint> : WebApplicationFactor
|
||||
|
||||
// This delay prevents "ERR_CONNECTION_REFUSED" errors
|
||||
// which happened like 1 out of 10 times when running tests.
|
||||
Thread.Sleep(50);
|
||||
Thread.Sleep(100);
|
||||
|
||||
return dummyHost;
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ public class WebApplicationWasmFactoryFixture<TEntryPoint> : WebApplicationFacto
|
||||
|
||||
// This delay prevents "ERR_CONNECTION_REFUSED" errors
|
||||
// which happened like 1 out of 10 times when running tests.
|
||||
Thread.Sleep(50);
|
||||
Thread.Sleep(100);
|
||||
|
||||
return dummyHost;
|
||||
}
|
||||
|
||||
@@ -31,6 +31,45 @@ public class AuthTests : PlaywrightTest
|
||||
await Login();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test if registering an account with the same email address as an existing account shows a warning.
|
||||
/// </summary>
|
||||
/// <returns>Async task.</returns>
|
||||
[Test]
|
||||
public async Task RegisterFormWarning()
|
||||
{
|
||||
// Logout.
|
||||
await NavigateUsingBlazorRouter("user/logout");
|
||||
await WaitForURLAsync("**/user/logout", "AliasVault");
|
||||
|
||||
// Wait and check if we get redirected to /user/login.
|
||||
await WaitForURLAsync("**/user/login");
|
||||
|
||||
// Try to register a new account.
|
||||
var registerButton = Page.Locator("a[href='/user/register']");
|
||||
await registerButton.ClickAsync();
|
||||
await WaitForURLAsync("**/user/register");
|
||||
|
||||
// Register account with same test credentials as used in the initial registration bootstrap method.
|
||||
var emailField = Page.Locator("input[id='email']");
|
||||
var passwordField = Page.Locator("input[id='password']");
|
||||
var password2Field = Page.Locator("input[id='password2']");
|
||||
await emailField.FillAsync(TestUserEmail);
|
||||
await passwordField.FillAsync(TestUserPassword);
|
||||
await password2Field.FillAsync(TestUserPassword);
|
||||
|
||||
// Check the terms of service checkbox
|
||||
var termsCheckbox = Page.Locator("input[id='terms']");
|
||||
await termsCheckbox.CheckAsync();
|
||||
|
||||
// Check if we get a visible warning when trying to register.
|
||||
var submitButton = Page.Locator("button[type='submit']");
|
||||
await submitButton.ClickAsync();
|
||||
|
||||
var warning = await Page.TextContentAsync("div[role='alert']");
|
||||
Assert.That(warning, Does.Contain("is already taken."), "No visible warning when registering with existing email address.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test if logging in works.
|
||||
/// </summary>
|
||||
@@ -52,7 +91,7 @@ public class AuthTests : PlaywrightTest
|
||||
// Check if we get redirected when clicking on the login button.
|
||||
var loginButton = Page.Locator("button[type='submit']");
|
||||
await loginButton.ClickAsync();
|
||||
await WaitForURLAsync(AppBaseUrl);
|
||||
await WaitForURLAsync(AppBaseUrl, "Find all of your credentials below");
|
||||
|
||||
// Check if the login was successful by verifying content.
|
||||
var pageContent = await Page.TextContentAsync("body");
|
||||
|
||||
98
src/Tests/AliasVault.E2ETests/Tests/DbUpgradeTest.cs
Normal file
98
src/Tests/AliasVault.E2ETests/Tests/DbUpgradeTest.cs
Normal file
File diff suppressed because one or more lines are too long
@@ -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