Rename login to credentials, fixed warnings and bugs (#58)

This commit is contained in:
Leendert de Borst
2024-07-01 15:43:32 +02:00
parent f1bc79a9a4
commit 48abe09415
31 changed files with 271 additions and 223 deletions

View File

@@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
<PropertyGroup>
<RootNamespace>AliasVault.WebApp</RootNamespace>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
@@ -60,12 +60,6 @@
</ItemGroup>
<ItemGroup>
<AdditionalFiles Include="Pages\Logins\AddEdit.razor" />
<AdditionalFiles Include="Pages\Logins\Delete.razor" />
<AdditionalFiles Include="Pages\Logins\View.razor" />
<Content Update="Layout\DbStatusIndicator.razor">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
</Content>
<Content Update="wwwroot\appsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>

View File

@@ -9,9 +9,9 @@ namespace AliasVault.WebApp.Auth.Pages.Base;
using System.Net.Http.Json;
using System.Text.Json;
using AliasVault.Shared.Models.WebApi;
using AliasVault.Shared.Models.WebApi.Auth;
using AliasVault.WebApp.Auth.Services;
using AliasVault.WebApp.Components;
using AliasVault.WebApp.Services;
using AliasVault.WebApp.Services.Database;
using Blazored.LocalStorage;
@@ -75,14 +75,34 @@ public class LoginBase : OwningComponentBase
[Inject]
public ILocalStorageService LocalStorage { get; set; } = null!;
/// <summary>
/// Parses the response content and displays the server validation errors.
/// </summary>
/// <param name="responseContent">Response content.</param>
/// <returns>List of errors if something went wrong.</returns>
public static List<string> ParseResponse(string responseContent)
{
var returnErrors = new List<string>();
var errorResponse = System.Text.Json.JsonSerializer.Deserialize<ServerValidationErrorResponse>(responseContent);
if (errorResponse is not null)
{
foreach (var error in errorResponse.Errors)
{
returnErrors.AddRange(error.Value);
}
}
return returnErrors;
}
/// <summary>
/// Gets the username from the authentication state asynchronously.
/// </summary>
/// <param name="email">Email address.</param>
/// <param name="password">Password.</param>
/// <param name="serverValidationErrors">ServerValidationErrors Blazor component reference.</param>
/// <returns>The username.</returns>
protected async Task ProcessLoginAsync(string email, string password, ServerValidationErrors serverValidationErrors)
/// <returns>List of errors if something went wrong.</returns>
protected async Task<List<string>> ProcessLoginAsync(string email, string password)
{
// Send request to server with email to get server ephemeral public key.
var result = await Http.PostAsJsonAsync("api/v1/Auth/login", new LoginRequest(email));
@@ -90,15 +110,16 @@ public class LoginBase : OwningComponentBase
if (!result.IsSuccessStatusCode)
{
serverValidationErrors.ParseResponse(responseContent);
return;
return ParseResponse(responseContent);
}
var loginResponse = JsonSerializer.Deserialize<LoginResponse>(responseContent);
if (loginResponse == null)
{
serverValidationErrors.AddError("An error occurred while processing the login request.");
return;
return new List<string>
{
"An error occurred while processing the login request.",
};
}
// 3. Client derives shared session key.
@@ -120,15 +141,16 @@ public class LoginBase : OwningComponentBase
if (!result.IsSuccessStatusCode)
{
serverValidationErrors.ParseResponse(responseContent);
return;
return ParseResponse(responseContent);
}
var validateLoginResponse = JsonSerializer.Deserialize<ValidateLoginResponse>(responseContent);
if (validateLoginResponse == null)
{
serverValidationErrors.AddError("An error occurred while processing the login request.");
return;
return new List<string>
{
"An error occurred while processing the login request.",
};
}
// 5. Client verifies proof.
@@ -155,5 +177,7 @@ public class LoginBase : OwningComponentBase
{
NavigationManager.NavigateTo("/");
}
return new List<string>();
}
}

View File

@@ -64,7 +64,11 @@
try
{
await ProcessLoginAsync(_loginModel.Email, _loginModel.Password, _serverValidationErrors);
var errors = await ProcessLoginAsync(_loginModel.Email, _loginModel.Password);
foreach (var error in errors)
{
_serverValidationErrors.AddError(error);
}
}
#if DEBUG
catch (Exception ex)

View File

@@ -8,6 +8,7 @@
@using System.Text.Json
@using AliasVault.Shared.Models
@using AliasVault.WebApp.Auth.Components
@using AliasVault.WebApp.Auth.Pages.Base
@using AliasVault.WebApp.Auth.Services
@using Cryptography
@using SecureRemotePassword
@@ -77,7 +78,7 @@
if (!result.IsSuccessStatusCode)
{
_serverValidationErrors.ParseResponse(responseContent);
LoginBase.ParseResponse(responseContent);
StateHasChanged();
return;
}

View File

@@ -75,7 +75,7 @@
try
{
await ProcessLoginAsync(Email, _unlockModel.Password, _serverValidationErrors);
await ProcessLoginAsync(Email, _unlockModel.Password);
StateHasChanged();
}
#if DEBUG

View File

@@ -107,6 +107,7 @@ public class AuthService(HttpClient httpClient, ILocalStorageService localStorag
/// <returns>Encryption key as base64 string.</returns>
public string GetEncryptionKeyAsBase64Async()
{
// Enable this line for debugging to skip unlock screen.
return Convert.ToBase64String(GetEncryptionKeyAsync());
}

View File

@@ -11,14 +11,14 @@
@code {
/// <summary>
/// Gets or sets the alias object to show.
/// Gets or sets the credentials object to show.
/// </summary>
[Parameter]
public AliasVault.Shared.Models.WebApi.AliasListEntry Obj { get; set; } = null!;
public AliasVault.WebApp.Models.CredentialListEntry Obj { get; set; } = null!;
private void ShowDetails()
{
// Redirect to view page instead for now.
NavigationManager.NavigateTo($"/login/{Obj.Id}");
NavigationManager.NavigateTo($"/credentials/{Obj.Id}");
}
}

View File

@@ -11,24 +11,6 @@
@code {
private readonly List<string> _errors = [];
/// <summary>
/// Parses the response content and displays the server validation errors.
/// </summary>
public void ParseResponse(string responseContent)
{
_errors.Clear();
var errorResponse = System.Text.Json.JsonSerializer.Deserialize<ServerValidationErrorResponse>(responseContent);
if (errorResponse is not null)
{
foreach (var error in errorResponse.Errors)
{
_errors.AddRange(error.Value);
}
}
StateHasChanged();
}
/// <summary>
/// Adds a server validation error.
/// </summary>

View File

@@ -15,8 +15,8 @@
<NavLink href="/" class="block rounded text-gray-700 hover:text-primary-700 dark:text-gray-400 dark:hover:text-white" ActiveClass="text-primary-700 dark:text-primary-500" Match="NavLinkMatch.All">
Home
</NavLink>
<NavLink href="/aliases" class="block text-gray-700 hover:text-primary-700 dark:text-gray-400 dark:hover:text-white" ActiveClass="text-primary-700 dark:text-primary-500" Match="NavLinkMatch.All">
Aliases
<NavLink href="/credentials" class="block text-gray-700 hover:text-primary-700 dark:text-gray-400 dark:hover:text-white" ActiveClass="text-primary-700 dark:text-primary-500" Match="NavLinkMatch.All">
Credentials
</NavLink>
</ul>
</div>
@@ -74,8 +74,8 @@
</NavLink>
</li>
<li class="block border-b dark:border-gray-700">
<NavLink href="/aliases" class="block py-3 px-4 text-gray-900 lg:py-0 dark:text-white lg:hover:underline lg:px-0" ActiveClass="text-primary-700 dark:text-primary-500" Match="NavLinkMatch.All">
Aliases
<NavLink href="/credentials" class="block py-3 px-4 text-gray-900 lg:py-0 dark:text-white lg:hover:underline lg:px-0" ActiveClass="text-primary-700 dark:text-primary-500" Match="NavLinkMatch.All">
Credentials
</NavLink>
</li>
</ul>

View File

@@ -1,5 +1,5 @@
//-----------------------------------------------------------------------
// <copyright file="LoginEdit.cs" company="lanedirt">
// <copyright file="CredentialEdit.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>
@@ -11,9 +11,9 @@ using System.ComponentModel.DataAnnotations;
using AliasClientDb;
/// <summary>
/// Login model.
/// Credential edit model.
/// </summary>
public class LoginEdit
public class CredentialEdit
{
/// <summary>
/// Gets or sets the Id of the login.

View File

@@ -0,0 +1,34 @@
//-----------------------------------------------------------------------
// <copyright file="CredentialListEntry.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.WebApp.Models;
/// <summary>
/// Alias list entry model. This model is used to represent an alias in a list with simplified properties.
/// </summary>
public class CredentialListEntry
{
/// <summary>
/// Gets or sets the alias id.
/// </summary>
public Guid Id { get; set; }
/// <summary>
/// Gets or sets the alias logo byte array.
/// </summary>
public byte[]? Logo { get; set; }
/// <summary>
/// Gets or sets the alias service name.
/// </summary>
public string Service { get; set; } = null!;
/// <summary>
/// Gets or sets the alias create date.
/// </summary>
public DateTime CreateDate { get; set; }
}

View File

@@ -1,21 +1,20 @@
@page "/add-login"
@page "/login/{id:guid}/edit"
@page "/add-credentials"
@page "/credentials/{id:guid}/edit"
@inherits PageBase
@inject NavigationManager Navigation
@inject AliasService AliasService
@inject CredentialService CredentialService
@using AliasGenerators.Implementations
@using AliasGenerators.Password.Implementations
@using AliasVault.WebApp.Models
@using Alias = AliasClientDb.Alias
@using Password = AliasClientDb.Password
@using Service = AliasClientDb.Service
@if (EditMode)
{
<LayoutPageTitle>Edit login</LayoutPageTitle>
<LayoutPageTitle>Edit credentials</LayoutPageTitle>
}
else {
<LayoutPageTitle>Add login</LayoutPageTitle>
<LayoutPageTitle>Add credentials</LayoutPageTitle>
}
<div class="grid grid-cols-1 px-4 pt-6 xl:grid-cols-3 xl:gap-4 dark:bg-gray-900">
@@ -24,18 +23,18 @@ else {
<div class="flex items-center justify-between">
@if (EditMode)
{
<h1 class="text-xl font-semibold text-gray-900 sm:text-2xl dark:text-white">Edit login</h1>
<h1 class="text-xl font-semibold text-gray-900 sm:text-2xl dark:text-white">Edit credentials</h1>
}
else {
<h1 class="text-xl font-semibold text-gray-900 sm:text-2xl dark:text-white">Add login</h1>
<h1 class="text-xl font-semibold text-gray-900 sm:text-2xl dark:text-white">Add credentials</h1>
}
</div>
@if (EditMode)
{
<p>Edit the existing login below.</p>
<p>Edit the existing credentials entry below.</p>
}
else {
<p>Create a new login below.</p>
<p>Create a new credentials entry below.</p>
}
</div>
</div>
@@ -68,7 +67,7 @@ else
<h3 class="mb-4 text-xl font-semibold dark:text-white">Login credentials</h3>
<div class="mb-4">
<button type="button" class="px-4 py-2 text-white bg-blue-600 rounded-lg hover:bg-blue-700 focus:ring-4 focus:ring-blue-300 dark:bg-blue-500 dark:hover:bg-blue-600 dark:focus:ring-blue-800" @onclick="GenerateRandomIdentity">Generate Random Identity</button>
<button type="submit" class="px-4 py-2 text-white bg-green-600 rounded-lg hover:bg-green-700 focus:ring-4 focus:ring-green-300 dark:bg-green-500 dark:hover:bg-green-600 dark:focus:ring-green-800">Save Login</button>
<button type="submit" class="px-4 py-2 text-white bg-green-600 rounded-lg hover:bg-green-700 focus:ring-4 focus:ring-green-300 dark:bg-green-500 dark:hover:bg-green-600 dark:focus:ring-green-800">Save Credentials</button>
@if (IsIdentityLoading)
{
<p>Loading...</p>
@@ -140,7 +139,7 @@ else
</div>
</div>
</div>
<button type="submit" class="px-4 py-2 text-white bg-green-600 rounded-lg hover:bg-green-700 focus:ring-4 focus:ring-green-300 dark:bg-green-500 dark:hover:bg-green-600 dark:focus:ring-green-800">Save Login</button>
<button type="submit" class="px-4 py-2 text-white bg-green-600 rounded-lg hover:bg-green-700 focus:ring-4 focus:ring-green-300 dark:bg-green-500 dark:hover:bg-green-600 dark:focus:ring-green-800">Save Credentials</button>
</div>
</EditForm>
@@ -153,14 +152,14 @@ else
@code {
/// <summary>
/// Gets or sets the login ID.
/// Gets or sets the Credentials ID.
/// </summary>
[Parameter]
public Guid? Id { get; set; }
private bool EditMode { get; set; }
private bool Loading { get; set; } = true;
private LoginEdit Obj { get; set; } = new();
private CredentialEdit Obj { get; set; } = new();
private bool IsIdentityLoading { get; set; }
private bool IsSaving { get; set; }
@@ -186,11 +185,11 @@ else
if (EditMode)
{
BreadcrumbItems.Add(new BreadcrumbItem { DisplayName = "Edit login" });
BreadcrumbItems.Add(new BreadcrumbItem { DisplayName = "Edit credential" });
}
else
{
BreadcrumbItems.Add(new BreadcrumbItem { DisplayName = "Add new login" });
BreadcrumbItems.Add(new BreadcrumbItem { DisplayName = "Add new credential" });
}
}
@@ -206,32 +205,32 @@ else
if (Id is null)
{
// Error loading alias.
GlobalNotificationService.AddErrorMessage("This login does not exist (anymore). Please try again.");
GlobalNotificationService.AddErrorMessage("This credential does not exist (anymore). Please try again.");
NavigationManager.NavigateTo("/", false, true);
return;
}
// Load existing Obj, retrieve from service
var alias = await AliasService.LoadEntryAsync(Id.Value);
var alias = await CredentialService.LoadEntryAsync(Id.Value);
if (alias is null)
{
// Error loading alias.
GlobalNotificationService.AddErrorMessage("This login does not exist (anymore). Please try again.");
GlobalNotificationService.AddErrorMessage("This credential does not exist (anymore). Please try again.");
NavigationManager.NavigateTo("/", false, true);
return;
}
Obj = LoginToLoginEdit(alias);
Obj = CredentialToCredentialEdit(alias);
}
else
{
// Create new Obj
var alias = new Login();
var alias = new Credential();
alias.Alias = new Alias();
alias.Service = new Service();
alias.Passwords = new List<Password> { new Password() };
Obj = LoginToLoginEdit(alias);
Obj = CredentialToCredentialEdit(alias);
}
// Hide loading spinner
@@ -247,7 +246,7 @@ else
StateHasChanged();
// Generate a random identity using the IIdentityGenerator implementation.
var identity = await AliasService.GenerateRandomIdentityAsync();
var identity = await CredentialService.GenerateRandomIdentityAsync();
// Generate random values for the Identity properties
Obj.Alias.FirstName = identity.FirstName;
@@ -297,12 +296,12 @@ else
{
if (Id is not null)
{
Id = await AliasService.UpdateLoginAsync(LoginEditToLogin(Obj));
Id = await CredentialService.UpdateEntryAsync(CredentialEditToCredential(Obj));
}
}
else
{
Id = await AliasService.InsertAliasAsync(LoginEditToLogin(Obj));
Id = await CredentialService.InsertEntryAsync(CredentialEditToCredential(Obj));
}
IsSaving = false;
@@ -311,40 +310,46 @@ else
if (Id is null || Id == Guid.Empty)
{
// Error saving.
GlobalNotificationService.AddErrorMessage("Error saving alias. Please try again.", true);
GlobalNotificationService.AddErrorMessage("Error saving credentials. Please try again.", true);
return;
}
// No error, add success message.
if (EditMode)
{
GlobalNotificationService.AddSuccessMessage("Login updated successfully.");
GlobalNotificationService.AddSuccessMessage("Credentials updated successfully.");
}
else
{
GlobalNotificationService.AddSuccessMessage("Login created successfully.");
GlobalNotificationService.AddSuccessMessage("Credentials created successfully.");
}
Navigation.NavigateTo("/login/" + Id);
Navigation.NavigateTo("/credentials/" + Id);
}
private LoginEdit LoginToLoginEdit(Login alias)
private CredentialEdit CredentialToCredentialEdit(Credential alias)
{
return new LoginEdit
Console.WriteLine("passwordCount: " + alias.Passwords.Count);
return new CredentialEdit
{
Id = alias.Id,
ServiceName = alias.Service.Name ?? string.Empty,
ServiceUrl = alias.Service.Url,
Password = alias.Passwords.FirstOrDefault() ?? new Password(),
Password = alias.Passwords.FirstOrDefault() ?? new Password
{
Value = string.Empty,
CreatedAt = DateTime.UtcNow,
UpdatedAt = DateTime.UtcNow,
},
Alias = alias.Alias,
CreateDate = alias.CreatedAt,
LastUpdate = alias.UpdatedAt
};
}
private Login LoginEditToLogin(LoginEdit alias)
private Credential CredentialEditToCredential(CredentialEdit alias)
{
return new Login()
return new Credential()
{
Id = alias.Id,
Service = new Service
@@ -354,12 +359,7 @@ else
},
Passwords = new List<Password>
{
new Password
{
Value = alias.Password.Value ?? string.Empty,
CreatedAt = alias.CreateDate,
UpdatedAt = alias.LastUpdate,
},
alias.Password,
},
Alias = alias.Alias,
};

View File

@@ -1,16 +1,16 @@
@page "/login/{id:guid}/delete"
@page "/credentials/{id:guid}/delete"
@inherits PageBase
@inject AliasService AliasService
@inject CredentialService CredentialService
<LayoutPageTitle>Delete login</LayoutPageTitle>
<LayoutPageTitle>Delete credentials entry</LayoutPageTitle>
<div class="grid grid-cols-1 px-4 pt-6 xl:grid-cols-3 xl:gap-4 dark:bg-gray-900">
<div class="mb-4 col-span-full xl:mb-2">
<Breadcrumb BreadcrumbItems="BreadcrumbItems" />
<div class="flex items-center justify-between">
<h1 class="text-xl font-semibold text-gray-900 sm:text-2xl dark:text-white">Delete login</h1>
<h1 class="text-xl font-semibold text-gray-900 sm:text-2xl dark:text-white">Delete credentials</h1>
</div>
<p>You can delete a login below.</p>
<p>You can delete a credentials entry below.</p>
</div>
</div>
@@ -22,7 +22,7 @@ else
{
<div class="p-4 mb-4 bg-white border border-gray-200 rounded-lg shadow-sm 2xl:col-span-2 dark:border-gray-700 sm:p-6 dark:bg-gray-800">
<AlertMessageError Message="Note: removing this login entry is permanent and cannot be undone." />
<h3 class="mb-4 text-xl font-semibold dark:text-white">Login</h3>
<h3 class="mb-4 text-xl font-semibold dark:text-white">Credential entry</h3>
<div class="mb-4">
<label class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Id</label>
<div>@Id</div>
@@ -49,14 +49,14 @@ else
public Guid Id { get; set; }
private bool IsLoading { get; set; } = true;
private Login? Obj { get; set; }
private Credential? Obj { get; set; }
/// <inheritdoc />
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
BreadcrumbItems.Add(new BreadcrumbItem { Url = "login/" + Id, DisplayName = "View Login" });
BreadcrumbItems.Add(new BreadcrumbItem { DisplayName = "Delete login" });
BreadcrumbItems.Add(new BreadcrumbItem { Url = "credentials/" + Id, DisplayName = "View Credentials Entry" });
BreadcrumbItems.Add(new BreadcrumbItem { DisplayName = "Delete credentials" });
}
/// <inheritdoc />
@@ -67,7 +67,7 @@ else
if (firstRender)
{
// Load existing Obj, retrieve from service
Obj = await AliasService.LoadEntryAsync(Id);
Obj = await CredentialService.LoadEntryAsync(Id);
// Hide loading spinner
IsLoading = false;
@@ -81,17 +81,17 @@ else
{
if (Obj is null)
{
GlobalNotificationService.AddErrorMessage("Error deleting. Login not found.", true);
GlobalNotificationService.AddErrorMessage("Error deleting. Credentials entry not found.", true);
return;
}
await AliasService.DeleteEntryAsync(Id);
GlobalNotificationService.AddSuccessMessage("Login successfully deleted.");
await CredentialService.DeleteEntryAsync(Id);
GlobalNotificationService.AddSuccessMessage("Credentials entry successfully deleted.");
NavigationManager.NavigateTo("/");
}
private void Cancel()
{
NavigationManager.NavigateTo("/login/" + Id);
NavigationManager.NavigateTo("/credentials/" + Id);
}
}

View File

@@ -1,9 +1,9 @@
@page "/login/{id:guid}"
@page "/credentials/{id:guid}"
@inherits PageBase
@using AliasVault.WebApp.Components.Email
@inject AliasService AliasService
@inject CredentialService CredentialService
<LayoutPageTitle>View login</LayoutPageTitle>
<LayoutPageTitle>View credentials</LayoutPageTitle>
@if (IsLoading || Alias == null)
{
@@ -17,13 +17,13 @@ else
<Breadcrumb BreadcrumbItems="BreadcrumbItems"/>
<div class="flex items-center justify-between">
<h1 class="text-xl font-semibold text-gray-900 sm:text-2xl dark:text-white">View login</h1>
<h1 class="text-xl font-semibold text-gray-900 sm:text-2xl dark:text-white">View credentials entry</h1>
<div class="flex">
<a href="/login/@Id/edit" class="mr-3 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">
Edit login
<a href="/credentials/@Id/edit" class="mr-3 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">
Edit credentials entry
</a>
<a href="/login/@Id/delete" class="px-4 py-2 text-white bg-red-600 rounded-lg hover:bg-red-700 focus:ring-4 focus:ring-red-300 dark:bg-red-500 dark:hover:bg-red-600 dark:focus:ring-red-800">
Delete login
<a href="/credentials/@Id/delete" class="px-4 py-2 text-white bg-red-600 rounded-lg hover:bg-red-700 focus:ring-4 focus:ring-red-300 dark:bg-red-500 dark:hover:bg-red-600 dark:focus:ring-red-800">
Delete credentials entry
</a>
</div>
</div>
@@ -119,19 +119,19 @@ else
@code {
/// <summary>
/// Gets or sets the login ID.
/// Gets or sets the credentials ID.
/// </summary>
[Parameter]
public Guid Id { get; set; }
private bool IsLoading { get; set; } = true;
private Login? Alias { get; set; } = new();
private Credential? Alias { get; set; } = new();
private string AliasEmail { get; set; } = string.Empty;
/// <inheritdoc />
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
BreadcrumbItems.Add(new BreadcrumbItem { DisplayName = "View login" });
BreadcrumbItems.Add(new BreadcrumbItem { DisplayName = "View credentials entry" });
}
/// <inheritdoc />
@@ -151,12 +151,12 @@ else
StateHasChanged();
// Load the aliases from the webapi via AliasService.
Alias = await AliasService.LoadEntryAsync(Id);
Alias = await CredentialService.LoadEntryAsync(Id);
if (Alias is null)
{
// Error loading alias.
GlobalNotificationService.AddErrorMessage("This login does not exist (anymore). Please try again.");
GlobalNotificationService.AddErrorMessage("This credentials entry does not exist (anymore). Please try again.");
NavigationManager.NavigateTo("/", false, true);
return;
}

View File

@@ -1,8 +1,8 @@
@page "/"
@page "/aliases"
@page "/credentials"
@inherits PageBase
@using AliasVault.WebApp.Components.Alias
@inject AliasService AliasService
@using AliasVault.WebApp.Components.Credentials
@inject CredentialService CredentialService
<LayoutPageTitle>Home</LayoutPageTitle>
@@ -10,12 +10,12 @@
<div class="mb-4 col-span-full xl:mb-2">
<Breadcrumb BreadcrumbItems="BreadcrumbItems" />
<div class="flex items-center justify-between">
<h1 class="text-xl font-semibold text-gray-900 sm:text-2xl dark:text-white">Logins</h1>
<a href="/add-login" 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">
+ Add new login
<h1 class="text-xl font-semibold text-gray-900 sm:text-2xl dark:text-white">Credentials</h1>
<a href="/add-credentials" 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">
+ Add new credential
</a>
</div>
<p>Find all of your logins below.</p>
<p>Find all of your credentials below.</p>
</div>
</div>
@@ -25,16 +25,16 @@
}
<div class="grid gap-4 px-4 mb-4 md:grid-cols-4 xl:grid-cols-6">
@foreach (var alias in Aliases)
@foreach (var credential in Credentials)
{
<Alias Obj="@alias"/>
<Credential Obj="@credential"/>
}
</div>
@code {
private bool IsLoading { get; set; } = true;
private List<Shared.Models.WebApi.AliasListEntry> Aliases { get; set; } = new();
private List<CredentialListEntry> Credentials { get; set; } = new();
/// <inheritdoc />
protected override async Task OnAfterRenderAsync(bool firstRender)
@@ -53,15 +53,15 @@
StateHasChanged();
// Load the aliases from the webapi via AliasService.
var aliasListEntries = await AliasService.GetListAsync();
if (aliasListEntries is null)
var credentialListEntries = await CredentialService.GetListAsync();
if (credentialListEntries is null)
{
// Error loading aliases.
GlobalNotificationService.AddErrorMessage("Failed to load aliases.", true);
GlobalNotificationService.AddErrorMessage("Failed to load credentials.", true);
return;
}
Aliases = aliasListEntries;
Credentials = credentialListEntries;
IsLoading = false;
StateHasChanged();
}

View File

@@ -43,7 +43,7 @@ builder.Services.AddScoped(sp =>
builder.Services.AddTransient<AliasVaultApiHandlerService>();
builder.Services.AddScoped<AuthService>();
builder.Services.AddScoped<AuthenticationStateProvider, AuthStateProvider>();
builder.Services.AddScoped<AliasService>();
builder.Services.AddScoped<CredentialService>();
builder.Services.AddScoped<DbService>();
builder.Services.AddScoped<GlobalNotificationService>();
builder.Services.AddSingleton<ClipboardCopyService>();

View File

@@ -1,5 +1,5 @@
//-----------------------------------------------------------------------
// <copyright file="AliasService.cs" company="lanedirt">
// <copyright file="CredentialService.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>
@@ -9,7 +9,7 @@ namespace AliasVault.WebApp.Services;
using System.Net.Http.Json;
using AliasClientDb;
using AliasVault.Shared.Models.WebApi;
using AliasVault.WebApp.Models;
using AliasVault.WebApp.Services.Database;
using Microsoft.EntityFrameworkCore;
using Identity = AliasGenerators.Identity.Models.Identity;
@@ -17,7 +17,7 @@ using Identity = AliasGenerators.Identity.Models.Identity;
/// <summary>
/// Service class for alias operations.
/// </summary>
public class AliasService(HttpClient httpClient, DbService dbService)
public class CredentialService(HttpClient httpClient, DbService dbService)
{
/// <summary>
/// Generate random identity by calling the IdentityGenerator API.
@@ -39,11 +39,11 @@ public class AliasService(HttpClient httpClient, DbService dbService)
/// </summary>
/// <param name="loginObject">Login object to insert.</param>
/// <returns>Guid of inserted entry.</returns>
public async Task<Guid> InsertAliasAsync(Login loginObject)
public async Task<Guid> InsertEntryAsync(Credential loginObject)
{
var context = await dbService.GetDbContextAsync();
var login = new Login
var login = new Credential
{
CreatedAt = DateTime.UtcNow,
UpdatedAt = DateTime.UtcNow,
@@ -76,19 +76,18 @@ public class AliasService(HttpClient httpClient, DbService dbService)
},
};
await context.Logins.AddAsync(login);
login.Passwords.Add(new Password()
{
Value = loginObject.Passwords.First().Value,
});
await context.Credentials.AddAsync(login);
await context.SaveChangesAsync();
Console.WriteLine("Inserted new alias without password.");
// Add password.
context.Passwords.Add(new AliasClientDb.Password()
{
Value = loginObject.Passwords.FirstOrDefault()?.Value,
CreatedAt = DateTime.UtcNow,
UpdatedAt = DateTime.UtcNow,
Login = login,
});
login.Passwords.Add(loginObject.Passwords.First());
await dbService.SaveDatabaseAsync();
@@ -102,12 +101,12 @@ public class AliasService(HttpClient httpClient, DbService dbService)
/// </summary>
/// <param name="loginObject">Login object to update.</param>
/// <returns>Guid of updated entry.</returns>
public async Task<Guid> UpdateLoginAsync(Login loginObject)
public async Task<Guid> UpdateEntryAsync(Credential loginObject)
{
var context = await dbService.GetDbContextAsync();
// Get the existing entry.
var login = await context.Logins
var login = await context.Credentials
.Include(x => x.Alias)
.Include(x => x.Service)
.Include(x => x.Passwords)
@@ -148,16 +147,16 @@ public class AliasService(HttpClient httpClient, DbService dbService)
/// </summary>
/// <param name="loginId">Id of login to load.</param>
/// <returns>Alias object.</returns>
public async Task<Login> LoadEntryAsync(Guid loginId)
public async Task<Credential?> LoadEntryAsync(Guid loginId)
{
var context = await dbService.GetDbContextAsync();
var loginObject = await context.Logins
var loginObject = await context.Credentials
.Include(x => x.Passwords)
.Include(x => x.Alias)
.Include(x => x.Service)
.Where(x => x.Id == loginId)
.FirstAsync();
.FirstOrDefaultAsync();
return loginObject;
}
@@ -165,16 +164,16 @@ public class AliasService(HttpClient httpClient, DbService dbService)
/// <summary>
/// Get list with all login entries.
/// </summary>
/// <returns>List of AliasListEntry objects.</returns>
public async Task<List<AliasListEntry>?> GetListAsync()
/// <returns>List of CredentialListEntry objects.</returns>
public async Task<List<CredentialListEntry>?> GetListAsync()
{
var context = await dbService.GetDbContextAsync();
// Retrieve all aliases from client DB.
return await context.Logins
return await context.Credentials
.Include(x => x.Alias)
.Include(x => x.Service)
.Select(x => new AliasListEntry
.Select(x => new CredentialListEntry
{
Id = x.Id,
Logo = x.Service.Logo,
@@ -193,11 +192,11 @@ public class AliasService(HttpClient httpClient, DbService dbService)
{
var context = await dbService.GetDbContextAsync();
var login = await context.Logins
var login = await context.Credentials
.Where(x => x.Id == id)
.FirstAsync();
context.Logins.Remove(login);
context.Credentials.Remove(login);
await context.SaveChangesAsync();
await dbService.SaveDatabaseAsync();
}

View File

@@ -15,6 +15,7 @@
@using AliasVault.WebApp.Components.Models
@using AliasVault.WebApp.Components.Alerts
@using AliasVault.WebApp.Components.Loading
@using AliasVault.WebApp.Models
@using AliasVault.WebApp.Pages.Base
@using AliasVault.WebApp.Services
@using AliasVault.WebApp.Services.Database

View File

@@ -121,4 +121,9 @@ public class Alias
/// Gets or sets the updated timestamp.
/// </summary>
public DateTime UpdatedAt { get; set; }
/// <summary>
/// Gets or sets the credential objects.
/// </summary>
public virtual ICollection<Credential> Credentials { get; set; } = new List<Credential>();
}

View File

@@ -46,27 +46,27 @@ public class AliasClientDbContext : DbContext
/// <summary>
/// Gets or sets the Alias DbSet.
/// </summary>
public DbSet<Alias> Aliases { get; set; }
public DbSet<Alias> Aliases { get; set; } = null!;
/// <summary>
/// Gets or sets the Attachment DbSet.
/// </summary>
public DbSet<Attachment> Attachment { get; set; }
public DbSet<Attachment> Attachment { get; set; } = null!;
/// <summary>
/// Gets or sets the Login DbSet.
/// Gets or sets the Credential DbSet.
/// </summary>
public DbSet<Login> Logins { get; set; }
public DbSet<Credential> Credentials { get; set; } = null!;
/// <summary>
/// Gets or sets the Password DbSet.
/// </summary>
public DbSet<Password> Passwords { get; set; }
public DbSet<Password> Passwords { get; set; } = null!;
/// <summary>
/// Gets or sets the Service DbSet.
/// </summary>
public DbSet<Service> Services { get; set; }
public DbSet<Service> Services { get; set; } = null!;
/// <summary>
/// The OnModelCreating method.
@@ -88,32 +88,32 @@ public class AliasClientDbContext : DbContext
}
}
// Configure Login - Alias relationship
modelBuilder.Entity<Login>()
// Configure Credential - Alias relationship
modelBuilder.Entity<Credential>()
.HasOne(l => l.Alias)
.WithMany()
.WithMany(c => c.Credentials)
.HasForeignKey(l => l.AliasId)
.OnDelete(DeleteBehavior.Cascade);
// Configure Login - Service relationship
modelBuilder.Entity<Login>()
// Configure Credential - Service relationship
modelBuilder.Entity<Credential>()
.HasOne(l => l.Service)
.WithMany()
.WithMany(c => c.Credentials)
.HasForeignKey(l => l.ServiceId)
.OnDelete(DeleteBehavior.Cascade);
// Configure Attachment - Login relationship
// Configure Attachment - Credential relationship
modelBuilder.Entity<Attachment>()
.HasOne(l => l.Login)
.WithMany()
.HasForeignKey(l => l.LoginId)
.HasOne(l => l.Credential)
.WithMany(c => c.Attachments)
.HasForeignKey(l => l.CredentialId)
.OnDelete(DeleteBehavior.Cascade);
// Configure Password - Login relationship
// Configure Password - Credential relationship
modelBuilder.Entity<Password>()
.HasOne(l => l.Login)
.WithMany()
.HasForeignKey(l => l.LoginId)
.HasOne(l => l.Credential)
.WithMany(c => c.Passwords)
.HasForeignKey(l => l.CredentialId)
.OnDelete(DeleteBehavior.Cascade);
}

View File

@@ -43,13 +43,13 @@ public class Attachment
public DateTime UpdatedAt { get; set; }
/// <summary>
/// Gets or sets the login foreign key.
/// Gets or sets the credential foreign key.
/// </summary>
public Guid LoginId { get; set; }
public Guid CredentialId { get; set; }
/// <summary>
/// Gets or sets the login navigation property.
/// Gets or sets the credential navigation property.
/// </summary>
[ForeignKey("LoginId")]
public virtual Login Login { get; set; } = null!;
[ForeignKey("CredentialId")]
public virtual Credential Credential { get; set; } = null!;
}

View File

@@ -1,5 +1,5 @@
//-----------------------------------------------------------------------
// <copyright file="Login.cs" company="lanedirt">
// <copyright file="Credential.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>
@@ -12,7 +12,7 @@ using System.ComponentModel.DataAnnotations.Schema;
/// <summary>
/// Login object.
/// </summary>
public class Login
public class Credential
{
/// <summary>
/// Gets or sets Login ID.

View File

@@ -38,13 +38,13 @@ public class Password
public DateTime UpdatedAt { get; set; }
/// <summary>
/// Gets or sets the login foreign key.
/// Gets or sets the credential foreign key.
/// </summary>
public Guid LoginId { get; set; }
public Guid CredentialId { get; set; }
/// <summary>
/// Gets or sets the login navigation property.
/// Gets or sets the credential navigation property.
/// </summary>
[ForeignKey("LoginId")]
public virtual Login Login { get; set; } = null!;
[ForeignKey("CredentialId")]
public virtual Credential Credential { get; set; } = null!;
}

View File

@@ -45,4 +45,9 @@ public class Service
/// Gets or sets the updated timestamp.
/// </summary>
public DateTime UpdatedAt { get; set; }
/// <summary>
/// Gets or sets the credential objects.
/// </summary>
public virtual ICollection<Credential> Credentials { get; set; } = new List<Credential>();
}

View File

@@ -56,6 +56,6 @@ public class AuthTests : PlaywrightTest
// Check if the login was successful by verifying content.
var pageContent = await Page.TextContentAsync("body");
Assert.That(pageContent, Does.Contain("Find all of your logins below"), "No index content after logging in.");
Assert.That(pageContent, Does.Contain("Find all of your credentials below"), "No index content after logging in.");
}
}

View File

@@ -1,5 +1,5 @@
//-----------------------------------------------------------------------
// <copyright file="AliasTests.cs" company="lanedirt">
// <copyright file="CredentialTests.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>
@@ -8,97 +8,95 @@
namespace AliasVault.E2ETests.Tests;
/// <summary>
/// End-to-end tests for the alias management.
/// End-to-end tests for the credential management.
/// </summary>
[Parallelizable(ParallelScope.Self)]
[TestFixture]
public class AliasTests : PlaywrightTest
public class CredentialTests : PlaywrightTest
{
private static readonly Random Random = new();
/// <summary>
/// Test if the alias listing index page works.
/// Test if the credential listing index page works.
/// </summary>
/// <returns>Async task.</returns>
[Test]
public async Task AliasListingTest()
public async Task CredentialListingTest()
{
await NavigateUsingBlazorRouter("aliases");
await WaitForURLAsync("**/aliases", "AliasVault");
await NavigateUsingBlazorRouter("credentials");
await WaitForURLAsync("**/credentials", "AliasVault");
// Check if the expected content is present.
var pageContent = await Page.TextContentAsync("body");
Assert.That(pageContent, Does.Contain("Find all of your logins below"), "No index content after logging in.");
Assert.That(pageContent, Does.Contain("Find all of your credentials below"), "No index content after logging in.");
}
/// <summary>
/// Test if creating a new alias works.
/// Test if creating a new credential entry works.
/// </summary>
/// <returns>Async task.</returns>
[Test]
public async Task CreateAliasTest()
public async Task CreateCredentialTest()
{
// Create a new alias with service name = "Test Service".
var serviceName = "Test Service";
await CreateAlias(new Dictionary<string, string>
await CreateCredentialEntry(new Dictionary<string, string>
{
{ "service-name", serviceName },
});
// Check that the service name is present in the content.
var pageContent = await Page.TextContentAsync("body");
Assert.That(pageContent, Does.Contain(serviceName), "Created alias service name does not appear on alias page.");
Assert.That(pageContent, Does.Contain(serviceName), "Created credential service name does not appear on alias page.");
}
/// <summary>
/// Test if editing a created alias works.
/// Test if editing a created credential entry works.
/// </summary>
/// <returns>Async task.</returns>
[Test]
public async Task EditAliasTest()
public async Task EditCredentialTest()
{
// Create a new alias with service name = "Alias service before".
var serviceNameBefore = "Login service before";
await CreateAlias(new Dictionary<string, string>
var serviceNameBefore = "Credential service before";
await CreateCredentialEntry(new Dictionary<string, string>
{
{ "service-name", serviceNameBefore },
});
// Check that the service name is present in the content.
var pageContent = await Page.TextContentAsync("body");
Assert.That(pageContent, Does.Contain(serviceNameBefore), "Created login service name does not appear on login page.");
Assert.That(pageContent, Does.Contain(serviceNameBefore), "Created credential service name does not appear on login page.");
// Click the edit button.
var editButton = Page.Locator("text=Edit login").First;
var editButton = Page.Locator("text=Edit credentials entry").First;
await editButton.ClickAsync();
await WaitForURLAsync("**/edit", "Save Login");
await WaitForURLAsync("**/edit", "Save Credentials");
// Replace the service name with "Alias service after".
var serviceNameAfter = "Login service after";
var serviceNameAfter = "Credential service after";
await InputHelper.FillInputFields(
fieldValues: new Dictionary<string, string>
{
{ "service-name", serviceNameAfter },
});
var submitButton = Page.Locator("text=Save Login").First;
var submitButton = Page.Locator("text=Save Credentials").First;
await submitButton.ClickAsync();
await WaitForURLAsync("**/login/**", "View Login");
await WaitForURLAsync("**/credentials/**", "View credentials entry");
pageContent = await Page.TextContentAsync("body");
Assert.That(pageContent, Does.Contain("Login updated"), "Login update confirmation message not shown.");
Assert.That(pageContent, Does.Contain(serviceNameAfter), "Login not updated correctly.");
Assert.That(pageContent, Does.Contain("Credentials updated"), "Credential update confirmation message not shown.");
Assert.That(pageContent, Does.Contain(serviceNameAfter), "Credential not updated correctly.");
}
/// <summary>
/// Create new alias.
/// Create new credential entry.
/// </summary>
/// <param name="formValues">Dictionary with html element ids and values to input as field value.</param>
/// <returns>Async task.</returns>
private async Task CreateAlias(Dictionary<string, string>? formValues = null)
private async Task CreateCredentialEntry(Dictionary<string, string>? formValues = null)
{
await NavigateUsingBlazorRouter("add-login");
await WaitForURLAsync("**/add-login", "Add login");
await NavigateUsingBlazorRouter("add-credentials");
await WaitForURLAsync("**/add-credentials", "Add credentials");
// Check if a button with text "Generate Random Identity" appears
var generateButton = Page.Locator("text=Generate Random Identity");
@@ -108,12 +106,12 @@ public class AliasTests : PlaywrightTest
await InputHelper.FillInputFields(formValues);
await InputHelper.FillEmptyInputFieldsWithRandom();
var submitButton = Page.Locator("text=Save Login").First;
var submitButton = Page.Locator("text=Save Credentials").First;
await submitButton.ClickAsync();
await WaitForURLAsync("**/login/**", "Login credentials");
await WaitForURLAsync("**/credentials/**", "Login credentials");
// Check if the alias was created
// Check if the credential was created
var pageContent = await Page.TextContentAsync("body");
Assert.That(pageContent, Does.Contain("Login credentials"), "Alias not created.");
Assert.That(pageContent, Does.Contain("Login credentials"), "Credential not created.");
}
}