Merge pull request #284 from lanedirt/220-refactor-blazor-components-for-reuse

Refactor blazor components to reduce duplicated code
This commit is contained in:
Leendert de Borst
2024-10-07 20:58:54 +02:00
committed by GitHub
65 changed files with 1893 additions and 666 deletions

View File

@@ -1,52 +0,0 @@
@inherits ComponentBase
<nav class="flex mb-5">
<ol class="inline-flex items-center space-x-1 text-sm font-medium md:space-x-2">
<li class="inline-flex items-center">
<a href="/" class="inline-flex items-center text-gray-700 hover:text-primary-600 dark:text-gray-300 dark:hover:text-primary-500">
<svg class="w-5 h-5 mr-2.5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z"></path></svg>
Home
</a>
</li>
@foreach (var item in BreadcrumbItems)
{
@if (item.Url is not null)
{
<li>
<div class="flex items-center">
<svg class="w-6 h-6 text-gray-400" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd"></path></svg>
<a href="@item.Url" class="ml-1 text-gray-700 hover:text-primary-600 md:ml-2 dark:text-gray-300 dark:hover:text-primary-500">@item.DisplayName</a>
</div>
</li>
}
else
{
<li>
<div class="flex items-center">
<svg class="w-6 h-6 text-gray-400" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd"></path></svg>
@item.DisplayName
</div>
</li>
}
}
</ol>
</nav>
@code {
/// <summary>
/// Gets or sets the list of breadcrumb items.
/// </summary>
[Parameter]
public List<BreadcrumbItem> BreadcrumbItems { get; set; } = new();
/// <inheritdoc />
protected override void OnInitialized()
{
base.OnInitialized();
// Remove first item if it is the home page
if (BreadcrumbItems.Any() && BreadcrumbItems[0].DisplayName == "Home")
{
BreadcrumbItems.RemoveAt(0);
}
}
}

View File

@@ -29,9 +29,7 @@
<ValidationMessage For="() => Input.ConfirmPassword" class="mt-1 text-sm text-red-600 dark:text-red-400"/>
</div>
<div>
<button type="submit" class="w-full px-4 py-2 text-sm font-medium text-white bg-primary-600 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 rounded-md">
Update password
</button>
<SubmitButton>Update password</SubmitButton>
</div>
</EditForm>
</div>

View File

@@ -52,9 +52,7 @@ else
<ValidationMessage For="() => Input.Code" class="mt-1 text-sm text-red-600 dark:text-red-400"/>
</div>
<div>
<button type="submit" class="w-full px-4 py-2 text-sm font-medium text-white bg-primary-600 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 rounded-md dark:bg-primary-500 dark:hover:bg-primary-600">
Verify
</button>
<SubmitButton>Verify</SubmitButton>
</div>
<ValidationSummary class="text-red-600 dark:text-red-400" role="alert"/>
</EditForm>

View File

@@ -22,9 +22,7 @@
<ValidationMessage For="() => Input.PhoneNumber" class="mt-1 text-sm text-red-600 dark:text-red-400"/>
</div>
<div>
<button type="submit" class="w-full px-4 py-2 text-sm font-medium text-white bg-primary-600 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 rounded-md dark:bg-primary-500 dark:hover:bg-primary-600">
Save
</button>
<SubmitButton>Save</SubmitButton>
</div>
</EditForm>
</div>

View File

@@ -23,7 +23,7 @@
<div>
<form @formname="reset-authenticator" @onsubmit="OnSubmitAsync" method="post">
<AntiforgeryToken/>
<button class="bg-primary-600 hover:bg-primary-700 text-white font-bold py-2 px-4 rounded" type="submit">Reset authenticator key</button>
<SubmitButton>Reset authenticator key</SubmitButton>
</form>
</div>

View File

@@ -46,18 +46,12 @@
<div class="flex flex-col sm:flex-row space-y-2 sm:space-y-0 sm:space-x-2">
@if (!hasAuthenticator)
{
<a href="account/manage/enable-authenticator" class="inline-block px-4 py-2 bg-primary-600 hover:bg-primary-700 text-white font-medium rounded-lg text-sm text-center focus:ring-4 focus:outline-none focus:ring-primary-300 dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800">
Add authenticator app
</a>
<LinkButton Href="account/manage/enable-authenticator" Color="primary" Text="Add authenticator app" />
}
else
{
<a href="account/manage/enable-authenticator" class="inline-block px-4 py-2 bg-primary-600 hover:bg-primary-700 text-white font-medium rounded-lg text-sm text-center focus:ring-4 focus:outline-none focus:ring-primary-300 dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800">
Set up authenticator app
</a>
<a href="account/manage/reset-authenticator" class="inline-block px-4 py-2 bg-primary-600 hover:bg-primary-700 text-white font-medium rounded-lg text-sm text-center focus:ring-4 focus:outline-none focus:ring-primary-300 dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800">
Reset authenticator app
</a>
<LinkButton Href="account/manage/enable-authenticator" Color="primary" Text="Add authenticator app" />
<LinkButton Href="account/manage/reset-authenticator" Color="primary" Text="Reset authenticator app" />
}
</div>
</div>

View File

@@ -2,14 +2,11 @@
@using AliasVault.Admin.Main.Layout
@layout MainLayout
<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">
<div class="flex items-center justify-between">
<h1 class="text-xl font-semibold text-gray-900 sm:text-2xl dark:text-white">Manage account</h1>
</div>
<p>Manage your profile here.</p>
</div>
</div>
<PageHeader
BreadcrumbItems="@BreadcrumbItems"
Title="Manage account"
Description="Manage your profile here.">
</PageHeader>
<div class="container mx-auto px-4 py-8">
<hr class="mb-6 border-t border-gray-300"/>
@@ -22,3 +19,19 @@
</div>
</div>
</div>
@code {
/// <summary>
/// Gets the breadcrumb items for the page. A default set of breadcrumbs is added in the parent OnInitialized method.
/// </summary>
private List<BreadcrumbItem> BreadcrumbItems { get; } = new();
/// <inheritdoc />
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
// Add base breadcrumbs.
BreadcrumbItems.Add(new BreadcrumbItem { DisplayName = "Home", Url = "/" });
}
}

View File

@@ -1,18 +1,17 @@
@page "/emails"
@using AliasVault.RazorComponents.Tables
@inherits MainBase
<LayoutPageTitle>Emails</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">Emails</h1>
<RefreshButton OnClick="RefreshData" ButtonText="Refresh" />
</div>
<p>This page gives an overview of recently received mails by this AliasVault server.</p>
</div>
</div>
<PageHeader
BreadcrumbItems="@BreadcrumbItems"
Title="Emails"
Description="This page gives an overview of recently received mails by this AliasVault server. Note that all email fields except 'To' are encrypted with the public key of the user and cannot be decrypted by the server.">
<CustomActions>
<RefreshButton OnClick="RefreshData" ButtonText="Refresh" />
</CustomActions>
</PageHeader>
@if (IsLoading)
{
@@ -23,59 +22,66 @@ else
<div class="overflow-x-auto px-4">
<Paginator CurrentPage="CurrentPage" PageSize="PageSize" TotalRecords="TotalRecords" OnPageChanged="HandlePageChanged" />
<table class="w-full text-sm text-left text-gray-500 shadow rounded border mt-8">
<thead class="text-xs text-gray-700 uppercase bg-gray-50">
<tr>
<th scope="col" class="px-4 py-3">ID</th>
<th scope="col" class="px-4 py-3">Time</th>
<th scope="col" class="px-4 py-3">From</th>
<th scope="col" class="px-4 py-3">To</th>
<th scope="col" class="px-4 py-3">Subject</th>
<th scope="col" class="px-4 py-3">Preview</th>
<th scope="col" class="px-4 py-3">Attachments</th>
<SortableTable Columns="@_tableColumns" SortColumn="@SortColumn" SortDirection="@SortDirection" OnSortChanged="HandleSortChanged">
@foreach (var email in EmailList)
{
<tr class="bg-white border-b hover:bg-gray-50">
<td class="px-4 py-3 font-medium text-gray-900">
@email.Id
</td>
<td class="px-4 py-3">
@email.DateSystem.ToString("yyyy-MM-dd HH:mm")
</td>
<td class="px-4 py-3">
@(email.FromLocal.Length > 15 ? email.FromLocal.Substring(0, 15) : email.FromLocal)@@@(email.FromDomain.Length > 15 ? email.FromDomain.Substring(0, 15) : email.FromDomain)
</td>
<td class="px-4 py-3">
@email.ToLocal@@@email.ToDomain
</td>
<td class="px-4 py-3">
@(email.Subject.Length > 30 ? email.Subject.Substring(0, 30) : email.Subject)
</td>
<td class="px-4 py-3">
<span class="line-clamp-1">
@(email.MessagePreview?.Length > 30 ? email.MessagePreview.Substring(0, 30) : email.MessagePreview)
</span>
</td>
<td class="px-4 py-3">
@email.Attachments.Count
</td>
</tr>
</thead>
<tbody>
@foreach (var email in EmailList)
{
<tr class="bg-white border-b hover:bg-gray-50">
<td class="px-4 py-3 font-medium text-gray-900">
@email.Id
</td>
<td class="px-4 py-3">
@email.DateSystem.ToString("yyyy-MM-dd HH:mm")
</td>
<td class="px-4 py-3">
@(email.FromLocal.Length > 15 ? email.FromLocal.Substring(0, 15) : email.FromLocal)@@@(email.FromDomain.Length > 15 ? email.FromDomain.Substring(0, 15) : email.FromDomain)
</td>
<td class="px-4 py-3">
@email.ToLocal@@@email.ToDomain
</td>
<td class="px-4 py-3">
@(email.Subject.Length > 30 ? email.Subject.Substring(0, 30) : email.Subject)
</td>
<td class="px-4 py-3">
<span class="line-clamp-1">
@(email.MessagePreview?.Length > 30 ? email.MessagePreview.Substring(0, 30) : email.MessagePreview)
</span>
</td>
<td class="px-4 py-3">
@email.Attachments.Count
</td>
</tr>
}
</tbody>
</table>
}
</SortableTable>
</div>
}
@code {
private readonly List<TableColumn> _tableColumns = [
new TableColumn { Title = "ID", PropertyName = "Id" },
new TableColumn { Title = "Time", PropertyName = "DateSystem" },
new TableColumn { Title = "From", PropertyName = "From" },
new TableColumn { Title = "To", PropertyName = "To" },
new TableColumn { Title = "Subject", PropertyName = "Subject" },
new TableColumn { Title = "Preview", PropertyName = "MessagePreview" },
new TableColumn { Title = "Attachments", PropertyName = "Attachments" },
];
private List<Email> EmailList { get; set; } = [];
private bool IsLoading { get; set; } = true;
private int CurrentPage { get; set; } = 1;
private int PageSize { get; set; } = 50;
private int TotalRecords { get; set; }
private string SortColumn { get; set; } = "Id";
private SortDirection SortDirection { get; set; } = SortDirection.Descending;
private async Task HandleSortChanged((string column, SortDirection direction) sort)
{
SortColumn = sort.column;
SortDirection = sort.direction;
await RefreshData();
}
/// <inheritdoc />
protected override async Task OnAfterRenderAsync(bool firstRender)
{
@@ -96,9 +102,53 @@ else
IsLoading = true;
StateHasChanged();
TotalRecords = await DbContext.Emails.CountAsync();
EmailList = await DbContext.Emails
.OrderByDescending(x => x.DateSystem)
IQueryable<Email> query = DbContext.Emails;
// Apply sort
switch (SortColumn)
{
case "Id":
query = SortDirection == SortDirection.Ascending
? query.OrderBy(x => x.Id)
: query.OrderByDescending(x => x.Id);
break;
case "DateSystem":
query = SortDirection == SortDirection.Ascending
? query.OrderBy(x => x.DateSystem)
: query.OrderByDescending(x => x.DateSystem);
break;
case "From":
query = SortDirection == SortDirection.Ascending
? query.OrderBy(x => x.FromLocal + "@" + x.FromDomain)
: query.OrderByDescending(x => x.FromLocal + "@" + x.FromDomain);
break;
case "To":
query = SortDirection == SortDirection.Ascending
? query.OrderBy(x => x.ToLocal + "@" + x.ToDomain)
: query.OrderByDescending(x => x.ToLocal + "@" + x.ToDomain);
break;
case "Subject":
query = SortDirection == SortDirection.Ascending
? query.OrderBy(x => x.Subject)
: query.OrderByDescending(x => x.Subject);
break;
case "MessagePreview":
query = SortDirection == SortDirection.Ascending
? query.OrderBy(x => x.MessagePreview)
: query.OrderByDescending(x => x.MessagePreview);
break;
case "Attachments":
query = SortDirection == SortDirection.Ascending
? query.OrderBy(x => x.Attachments.Count)
: query.OrderByDescending(x => x.Attachments.Count);
break;
default:
query = query.OrderByDescending(x => x.DateSystem);
break;
}
TotalRecords = await query.CountAsync();
EmailList = await query
.Skip((CurrentPage - 1) * PageSize)
.Take(PageSize)
.ToListAsync();

View File

@@ -3,13 +3,8 @@
<LayoutPageTitle>Home</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">AliasVault Admin</h1>
</div>
<p>Welcome to the AliasVault admin portal.</p>
</div>
</div>
<PageHeader
BreadcrumbItems="@BreadcrumbItems"
Title="AliasVault Admin"
Description="Welcome to the AliasVault admin portal.">
</PageHeader>

View File

@@ -1,22 +1,19 @@
@page "/logging/auth"
@using AliasVault.RazorComponents.Tables
@using AliasVault.Shared.Models.Enums
@inherits MainBase
<LayoutPageTitle>Auth logs</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">Auth logs</h1>
<div class="flex items-end space-x-2">
<DeleteButton OnClick="DeleteLogsWithConfirmation" ButtonText="Delete all logs" />
<RefreshButton OnClick="RefreshData" ButtonText="Refresh" />
</div>
</div>
<p>This page gives an overview of recent auth attempts.</p>
</div>
</div>
<PageHeader
BreadcrumbItems="@BreadcrumbItems"
Title="Auth logs"
Description="This page gives an overview of recent auth attempts.">
<CustomActions>
<DeleteButton OnClick="DeleteLogsWithConfirmation" ButtonText="Delete all logs" />
<RefreshButton OnClick="RefreshData" ButtonText="Refresh" />
</CustomActions>
</PageHeader>
@if (IsLoading)
{
@@ -43,6 +40,21 @@ else
</div>
</div>
</div>
<SortableTable Columns="@_tableColumns" SortColumn="@SortColumn" SortDirection="@SortDirection" OnSortChanged="HandleSortChanged">
@foreach (var log in LogList)
{
<tr class="bg-white border-b hover:bg-gray-50">
<td class="px-4 py-3 font-medium text-gray-900">@log.Id</td>
<td class="px-4 py-3">@log.Timestamp.ToString("yyyy-MM-dd HH:mm")</td>
<td class="px-4 py-3">@log.Username</td>
<td class="px-4 py-3">@log.EventType</td>
<td class="px-4 py-3"><StatusPill Enabled="log.IsSuccess" TextTrue="Success" TextFalse="Failed" /></td>
<td class="px-4 py-3">@log.IpAddress</td>
</tr>
}
</SortableTable>
<table class="w-full text-sm text-left text-gray-500 shadow rounded border">
<thead class="text-xs text-gray-700 uppercase bg-gray-50">
<tr>
@@ -72,6 +84,15 @@ else
}
@code {
private readonly List<TableColumn> _tableColumns = [
new TableColumn { Title = "ID", PropertyName = "Id" },
new TableColumn { Title = "Time", PropertyName = "Timestamp" },
new TableColumn { Title = "Username", PropertyName = "Username" },
new TableColumn { Title = "Event", PropertyName = "EventType" },
new TableColumn { Title = "Success", PropertyName = "IsSuccess" },
new TableColumn { Title = "IP", PropertyName = "IpAddress" },
];
private List<AuthLog> LogList { get; set; } = [];
private bool IsLoading { get; set; } = true;
private int CurrentPage { get; set; } = 1;
@@ -106,6 +127,16 @@ else
}
}
private string SortColumn { get; set; } = "Id";
private SortDirection SortDirection { get; set; } = SortDirection.Descending;
private async Task HandleSortChanged((string column, SortDirection direction) sort)
{
SortColumn = sort.column;
SortDirection = sort.direction;
await RefreshData();
}
/// <inheritdoc />
protected override async Task OnAfterRenderAsync(bool firstRender)
{
@@ -142,9 +173,10 @@ else
}
}
query = ApplySort(query);
TotalRecords = await query.CountAsync();
LogList = await query
.OrderByDescending(x => x.Timestamp)
.Skip((CurrentPage - 1) * PageSize)
.Take(PageSize)
.ToListAsync();
@@ -153,6 +185,49 @@ else
StateHasChanged();
}
/// <summary>
/// Apply sort to the query.
/// </summary>
private IQueryable<AuthLog> ApplySort(IQueryable<AuthLog> query)
{
// Apply sort.
switch (SortColumn)
{
case "Timestamp":
query = SortDirection == SortDirection.Ascending
? query.OrderBy(x => x.Timestamp)
: query.OrderByDescending(x => x.Timestamp);
break;
case "Username":
query = SortDirection == SortDirection.Ascending
? query.OrderBy(x => x.Username)
: query.OrderByDescending(x => x.Username);
break;
case "EventType":
query = SortDirection == SortDirection.Ascending
? query.OrderBy(x => x.EventType)
: query.OrderByDescending(x => x.EventType);
break;
case "IsSuccess":
query = SortDirection == SortDirection.Ascending
? query.OrderBy(x => x.IsSuccess)
: query.OrderByDescending(x => x.IsSuccess);
break;
case "IpAddress":
query = SortDirection == SortDirection.Ascending
? query.OrderBy(x => x.IpAddress)
: query.OrderByDescending(x => x.IpAddress);
break;
default:
query = SortDirection == SortDirection.Ascending
? query.OrderBy(x => x.Id)
: query.OrderByDescending(x => x.Id);
break;
}
return query;
}
private async Task DeleteLogsWithConfirmation()
{
if (await ConfirmModalService.ShowConfirmation("Confirm Delete", "Are you sure you want to delete all logs? This action cannot be undone."))

View File

@@ -1,21 +1,18 @@
@page "/logging/general"
@using AliasVault.RazorComponents.Tables
@inherits MainBase
<LayoutPageTitle>System logs</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">General logs</h1>
<div class="flex items-end space-x-2">
<DeleteButton OnClick="DeleteLogsWithConfirmation" ButtonText="Delete all logs" />
<RefreshButton OnClick="RefreshData" ButtonText="Refresh" />
</div>
</div>
<p>This page gives an overview of recent system logs.</p>
</div>
</div>
<PageHeader
BreadcrumbItems="@BreadcrumbItems"
Title="General logs"
Description="This page gives an overview of recent system logs.">
<CustomActions>
<DeleteButton OnClick="DeleteLogsWithConfirmation" ButtonText="Delete all logs" />
<RefreshButton OnClick="RefreshData" ButtonText="Refresh" />
</CustomActions>
</PageHeader>
@if (IsLoading)
{
@@ -36,60 +33,57 @@ else
<option value="">All Services</option>
@foreach (var service in ServiceNames)
{
<option value="@service">@service</option>
<option value="@service">@service</option>
}
</select>
</div>
</div>
</div>
<table class="w-full text-sm text-left text-gray-500 shadow rounded border">
<thead class="text-xs text-gray-700 uppercase bg-gray-50">
<tr>
<th scope="col" class="px-4 py-3">ID</th>
<th scope="col" class="px-4 py-3">Time</th>
<th scope="col" class="px-4 py-3">Application</th>
<th scope="col" class="px-4 py-3">Level</th>
<th scope="col" class="px-4 py-3">Message</th>
</tr>
</thead>
<tbody>
@foreach (var log in LogList)
{
<tr class="bg-white border-b hover:bg-gray-50">
<td class="px-4 py-3 font-medium text-gray-900">@log.Id</td>
<td class="px-4 py-3">@log.TimeStamp.ToString("yyyy-MM-dd HH:mm")</td>
<td class="px-4 py-3">@log.Application</td>
@{
string bgColor = log.Level switch
{
"Information" => "bg-blue-500",
"Error" => "bg-red-500",
"Warning" => "bg-yellow-500",
"Debug" => "bg-green-500",
_ => "bg-gray-500"
};
<SortableTable Columns="@_tableColumns" SortColumn="@SortColumn" SortDirection="@SortDirection" OnSortChanged="HandleSortChanged">
@foreach (var log in LogList)
{
<tr class="bg-white border-b hover:bg-gray-50">
<td class="px-4 py-3 font-medium text-gray-900">@log.Id</td>
<td class="px-4 py-3">@log.TimeStamp.ToString("yyyy-MM-dd HH:mm")</td>
<td class="px-4 py-3">@log.Application</td>
@{
string bgColor = log.Level switch
{
"Information" => "bg-blue-500",
"Error" => "bg-red-500",
"Warning" => "bg-yellow-500",
"Debug" => "bg-green-500",
_ => "bg-gray-500"
};
}
<td class="px-4 py-3">
<span class="px-2 py-1 rounded-full text-white @bgColor">
@log.Level
</span>
</td>
<td class="px-4 py-3" title="@log.Exception">
@if (log.SourceContext.Length > 0)
{
<span>@log.SourceContext: </span>
}
<td class="px-4 py-3">
<span class="px-2 py-1 rounded-full text-white @bgColor">
@log.Level
</span>
</td>
<td class="px-4 py-3" title="@log.Exception">
@if (log.SourceContext.Length > 0)
{
<span>@log.SourceContext: </span>
}
@log.Message
</td>
</tr>
}
</tbody>
</table>
@log.Message
</td>
</tr>
}
</SortableTable>
</div>
}
@code {
private readonly List<TableColumn> _tableColumns = [
new TableColumn { Title = "ID", PropertyName = "Id" },
new TableColumn { Title = "Time", PropertyName = "Timestamp" },
new TableColumn { Title = "Application", PropertyName = "Application" },
new TableColumn { Title = "Level", PropertyName = "Level" },
new TableColumn { Title = "Message", PropertyName = "Message" },
];
private List<Log> LogList { get; set; } = [];
private bool IsLoading { get; set; } = true;
private int CurrentPage { get; set; } = 1;
@@ -126,6 +120,16 @@ else
private List<string> ServiceNames { get; set; } = [];
private string SortColumn { get; set; } = "Id";
private SortDirection SortDirection { get; set; } = SortDirection.Descending;
private async Task HandleSortChanged((string column, SortDirection direction) sort)
{
SortColumn = sort.column;
SortDirection = sort.direction;
await RefreshData();
}
/// <inheritdoc />
protected override async Task OnAfterRenderAsync(bool firstRender)
{
@@ -136,7 +140,6 @@ else
}
}
private void HandlePageChanged(int newPage)
{
CurrentPage = newPage;
@@ -162,9 +165,38 @@ else
query = query.Where(x => x.Application == SelectedServiceName);
}
// Apply sort.
switch (SortColumn)
{
case "Application":
query = SortDirection == SortDirection.Ascending
? query.OrderBy(x => x.Application)
: query.OrderByDescending(x => x.Application);
break;
case "Message":
query = SortDirection == SortDirection.Ascending
? query.OrderBy(x => x.Message)
: query.OrderByDescending(x => x.Message);
break;
case "Level":
query = SortDirection == SortDirection.Ascending
? query.OrderBy(x => x.Level)
: query.OrderByDescending(x => x.Level);
break;
case "Timestamp":
query = SortDirection == SortDirection.Ascending
? query.OrderBy(x => x.TimeStamp)
: query.OrderByDescending(x => x.TimeStamp);
break;
default:
query = SortDirection == SortDirection.Ascending
? query.OrderBy(x => x.Id)
: query.OrderByDescending(x => x.Id);
break;
}
TotalRecords = await query.CountAsync();
LogList = await query
.OrderByDescending(x => x.Id)
.Skip((CurrentPage - 1) * PageSize)
.Take(PageSize)
.ToListAsync();

View File

@@ -8,9 +8,9 @@
namespace AliasVault.Admin.Main.Pages;
using AliasServerDb;
using AliasVault.Admin.Main.Models;
using AliasVault.Admin.Services;
using AliasVault.Auth;
using AliasVault.RazorComponents.Models;
using AliasVault.RazorComponents.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Components;

View File

@@ -3,15 +3,11 @@
<LayoutPageTitle>Delete user</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 user</h1>
</div>
<p>You can delete the user below.</p>
</div>
</div>
<PageHeader
BreadcrumbItems="@BreadcrumbItems"
Title="Delete user"
Description="You can delete the user below.">
</PageHeader>
@if (IsLoading)
{
@@ -30,13 +26,10 @@ else
<label class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Username</label>
<div>@Obj?.UserName</div>
</div>
<button @onclick="DeleteConfirm" class="text-white bg-red-600 hover:bg-red-800 focus:ring-4 focus:ring-red-300 font-medium rounded-lg text-sm inline-flex items-center px-3 py-2.5 text-center mr-2 dark:focus:ring-red-900">
Yes, I'm sure
</button>
<button @onclick="Cancel" class="text-gray-900 bg-white hover:bg-gray-100 focus:ring-4 focus:ring-primary-300 border border-gray-200 font-medium inline-flex items-center rounded-lg text-sm px-3 py-2.5 text-center dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700 dark:focus:ring-gray-700" data-drawer-hide="drawer-delete-product-default">
No, cancel
</button>
<div class="flex space-x-3">
<Button Color="danger" OnClick="DeleteConfirm">Yes, I'm sure</Button>
<Button Color="secondary" OnClick="Cancel">No, cancel</Button>
</div>
</div>
}

View File

@@ -1,18 +1,17 @@
@page "/users"
@using AliasVault.RazorComponents.Tables
@inherits MainBase
<LayoutPageTitle>Users</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">Users</h1>
<RefreshButton OnClick="RefreshData" ButtonText="Refresh" />
</div>
<p>This page gives an overview of all registered users and the associated vaults.</p>
</div>
</div>
<PageHeader
BreadcrumbItems="@BreadcrumbItems"
Title="Users"
Description="This page gives an overview of all registered users and the associated vaults.">
<CustomActions>
<RefreshButton OnClick="RefreshData" ButtonText="Refresh" />
</CustomActions>
</PageHeader>
@if (IsLoading)
{
@@ -26,43 +25,40 @@ else
<div class="mb-4">
<input type="text" @bind-value="SearchTerm" @bind-value:event="oninput" id="search" placeholder="Search users..." class="w-full px-4 py-2 border rounded text-sm text-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
<table class="w-full text-sm text-left text-gray-500 shadow rounded border">
<thead class="text-xs text-gray-700 uppercase bg-gray-50">
<tr>
<th scope="col" class="px-4 py-3">ID</th>
<th scope="col" class="px-4 py-3">Registered</th>
<th scope="col" class="px-4 py-3">Username</th>
<th scope="col" class="px-4 py-3"># Vaults</th>
<th scope="col" class="px-4 py-3"># Email claims</th>
<th scope="col" class="px-4 py-3">Storage</th>
<th scope="col" class="px-4 py-3">2FA</th>
<th scope="col" class="px-4 py-3">Last vault update</th>
<th scope="col" class="px-4 py-3">Actions</th>
</tr>
</thead>
<tbody id="logTableBody">
@foreach (var user in UserList)
{
<tr class="bg-white border-b hover:bg-gray-50">
<td class="px-4 py-3 font-medium text-gray-900">@user.Id</td>
<td class="px-4 py-3">@user.CreatedAt.ToString("yyyy-MM-dd HH:mm")</td>
<td class="px-4 py-3">@user.UserName</td>
<td class="px-4 py-3">@user.VaultCount</td>
<td class="px-4 py-3">@user.EmailClaimCount</td>
<td class="px-4 py-3">@Math.Round((double)user.VaultStorageInKb / 1024, 1) MB</td>
<td class="px-4 py-3"><StatusPill Enabled="user.TwoFactorEnabled" /></td>
<td class="px-4 py-3">@user.LastVaultUpdate.ToString("yyyy-MM-dd HH:mm")</td>
<td class="px-4 py-3">
<a href="users/@user.Id" class="px-4 py-2 text-white bg-primary-600 rounded-lg hover:bg-primary-700 focus:ring-4 focus:ring-blue-300 dark:bg-primary-500 dark:hover:bg-primary-600 dark:focus:ring-primary-800">View</a>
</td>
</tr>
}
</tbody>
</table>
<SortableTable Columns="@_tableColumns" SortColumn="@SortColumn" SortDirection="@SortDirection" OnSortChanged="HandleSortChanged">
@foreach (var user in UserList)
{
<tr class="bg-white border-b hover:bg-gray-50">
<td class="px-4 py-3 font-medium text-gray-900">@user.Id</td>
<td class="px-4 py-3">@user.CreatedAt.ToString("yyyy-MM-dd HH:mm")</td>
<td class="px-4 py-3">@user.UserName</td>
<td class="px-4 py-3">@user.VaultCount</td>
<td class="px-4 py-3">@user.EmailClaimCount</td>
<td class="px-4 py-3">@Math.Round((double)user.VaultStorageInKb / 1024, 1) MB</td>
<td class="px-4 py-3"><StatusPill Enabled="user.TwoFactorEnabled" /></td>
<td class="px-4 py-3">@user.LastVaultUpdate.ToString("yyyy-MM-dd HH:mm")</td>
<td class="px-4 py-3">
<LinkButton Color="primary" Href="@($"users/{user.Id}")" Text="View" />
</td>
</tr>
}
</SortableTable>
</div>
}
@code {
private readonly List<TableColumn> _tableColumns = [
new TableColumn { Title = "ID", PropertyName = "Id" },
new TableColumn { Title = "Registered", PropertyName = "CreatedAt" },
new TableColumn { Title = "Username", PropertyName = "UserName" },
new TableColumn { Title = "# Vaults", PropertyName = "VaultCount" },
new TableColumn { Title = "# Email claims", PropertyName = "EmailClaimCount" },
new TableColumn { Title = "Storage", PropertyName = "VaultStorageInKb" },
new TableColumn { Title = "2FA", PropertyName = "TwoFactorEnabled" },
new TableColumn { Title = "LastVaultUpdate", PropertyName = "LastVaultUpdate" },
];
private List<UserViewModel> UserList { get; set; } = [];
private bool IsLoading { get; set; } = true;
private int CurrentPage { get; set; } = 1;
@@ -83,6 +79,16 @@ else
}
}
private string SortColumn { get; set; } = "CreatedAt";
private SortDirection SortDirection { get; set; } = SortDirection.Descending;
private async Task HandleSortChanged((string column, SortDirection direction) sort)
{
SortColumn = sort.column;
SortDirection = sort.direction;
await RefreshData();
}
/// <inheritdoc />
protected override async Task OnAfterRenderAsync(bool firstRender)
{
@@ -110,10 +116,11 @@ else
query = query.Where(x => EF.Functions.Like(x.UserName!.ToLower(), "%" + SearchTerm.ToLower() + "%"));
}
TotalRecords = await query.CountAsync();
// Apply sort.
query = ApplySort(query);
TotalRecords = await query.CountAsync();
var users = await query
.OrderBy(x => x.CreatedAt)
.Skip((CurrentPage - 1) * PageSize)
.Take(PageSize)
.Select(u => new
@@ -149,4 +156,63 @@ else
IsLoading = false;
StateHasChanged();
}
/// <summary>
/// Apply sort to the query.
/// </summary>
private IQueryable<AliasVaultUser> ApplySort(IQueryable<AliasVaultUser> query)
{
// Apply sort.
switch (SortColumn)
{
case "Id":
query = SortDirection == SortDirection.Ascending
? query.OrderBy(x => x.Id)
: query.OrderByDescending(x => x.Id);
break;
case "CreatedAt":
query = SortDirection == SortDirection.Ascending
? query.OrderBy(x => x.CreatedAt)
: query.OrderByDescending(x => x.CreatedAt);
break;
case "UserName":
query = SortDirection == SortDirection.Ascending
? query.OrderBy(x => x.UserName)
: query.OrderByDescending(x => x.UserName);
break;
case "VaultCount":
query = SortDirection == SortDirection.Ascending
? query.OrderBy(x => x.Vaults.Count)
: query.OrderByDescending(x => x.Vaults.Count);
break;
case "EmailClaimCount":
query = SortDirection == SortDirection.Ascending
? query.OrderBy(x => x.EmailClaims.Count)
: query.OrderByDescending(x => x.EmailClaims.Count);
break;
case "VaultStorageInKb":
query = SortDirection == SortDirection.Ascending
? query.OrderBy(x => x.Vaults.Sum(v => v.FileSize))
: query.OrderByDescending(x => x.Vaults.Sum(v => v.FileSize));
break;
case "TwoFactorEnabled":
query = SortDirection == SortDirection.Ascending
? query.OrderBy(x => x.TwoFactorEnabled)
: query.OrderByDescending(x => x.TwoFactorEnabled);
break;
case "LastVaultUpdate":
query = SortDirection == SortDirection.Ascending
? query.OrderBy(x => x.Vaults.Max(v => v.CreatedAt))
: query.OrderByDescending(x => x.Vaults.Max(v => v.CreatedAt));
break;
default:
query = SortDirection == SortDirection.Ascending
? query.OrderBy(x => x.Id)
: query.OrderByDescending(x => x.Id);
break;
}
return query;
}
}

View File

@@ -9,21 +9,17 @@
}
else
{
<PageHeader
BreadcrumbItems="@BreadcrumbItems"
Title="View user"
Description="View details of the user below.">
<CustomActions>
<RefreshButton OnClick="RefreshData" ButtonText="Refresh" />
<LinkButton Color="danger" Href="@($"/users/{Id}/delete")" Text="Delete user" />
</CustomActions>
</PageHeader>
<div class="grid grid-cols-2 px-4 pt-6 md:grid-cols-3 lg: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">View user</h1>
<div class="flex">
<a href="/users/@Id/delete" class="mr-2 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 user
</a>
<RefreshButton OnClick="LoadEntryAsync" ButtonText="Refresh"/>
</div>
</div>
</div>
<div class="col-span-full">
<div class="p-4 mb-4 bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 sm:p-6 dark:bg-gray-800">
<div class="items-center xl:block sm:space-x-4 xl:space-x-0 2xl:space-x-4">
@@ -40,20 +36,14 @@ else
<span>Authenticator key(s) active: @TwoFactorKeysCount</span>
@if (User.TwoFactorEnabled)
{
<button @onclick="DisableTwoFactor" class="px-3 py-2 text-xs font-medium text-center text-white bg-red-700 rounded-lg hover:bg-red-800 focus:ring-4 focus:outline-none focus:ring-red-300 dark:bg-red-600 dark:hover:bg-red-700 dark:focus:ring-red-800">
Disable 2FA
</button>
<Button Color="danger" OnClick="DisableTwoFactor">Disable 2FA</Button>
}
else
{
if (TwoFactorKeysCount > 0)
{
<button @onclick="EnableTwoFactor" class="px-3 py-2 text-xs font-medium text-center text-white bg-red-700 rounded-lg hover:bg-red-800 focus:ring-4 focus:outline-none focus:ring-red-300 dark:bg-red-600 dark:hover:bg-red-700 dark:focus:ring-red-800">
Enable 2FA
</button>
<button @onclick="ResetTwoFactor" class="px-3 py-2 text-xs font-medium text-center text-white bg-red-700 rounded-lg hover:bg-red-800 focus:ring-4 focus:outline-none focus:ring-red-300 dark:bg-red-600 dark:hover:bg-red-700 dark:focus:ring-red-800">
Remove 2FA keys
</button>
<Button Color="success" OnClick="EnableTwoFactor">Enable 2FA</Button>
<Button Color="danger" OnClick="ResetTwoFactor">Remove 2FA keys</Button>
}
}
</div>
@@ -68,60 +58,60 @@ else
<table class="w-full text-sm text-left text-gray-500 shadow rounded border">
<thead class="text-xs text-gray-700 uppercase bg-gray-50">
<tr>
<th scope="col" class="px-4 py-3">ID</th>
<th scope="col" class="px-4 py-3">Created</th>
<th scope="col" class="px-4 py-3">Updated</th>
<th scope="col" class="px-4 py-3">Filesize</th>
<th scope="col" class="px-4 py-3">DB version</th>
<th scope="col" class="px-4 py-3">Revision</th>
<th scope="col" class="px-4 py-3">Credentials</th>
<th scope="col" class="px-4 py-3">Email Claims</th>
<th scope="col" class="px-4 py-3">Status</th>
<th scope="col" class="px-4 py-3">Actions</th>
</tr>
<tr>
<th scope="col" class="px-4 py-3">ID</th>
<th scope="col" class="px-4 py-3">Created</th>
<th scope="col" class="px-4 py-3">Updated</th>
<th scope="col" class="px-4 py-3">Filesize</th>
<th scope="col" class="px-4 py-3">DB version</th>
<th scope="col" class="px-4 py-3">Revision</th>
<th scope="col" class="px-4 py-3">Credentials</th>
<th scope="col" class="px-4 py-3">Email Claims</th>
<th scope="col" class="px-4 py-3">Status</th>
<th scope="col" class="px-4 py-3">Actions</th>
</tr>
</thead>
<tbody>
@{
Vault? previousEntry = null;
}
@foreach (var entry in VaultList.OrderBy(e => e.UpdatedAt))
{
<tr class="bg-white border-b hover:bg-gray-50">
<td class="px-4 py-3 font-medium text-gray-900">@entry.Id</td>
<td class="px-4 py-3">@entry.CreatedAt.ToString("yyyy-MM-dd HH:mm")</td>
<td class="px-4 py-3">@entry.UpdatedAt.ToString("yyyy-MM-dd HH:mm")</td>
<td class="px-4 py-3">@Math.Round((double)entry.FileSize / 1024, 1) MB</td>
<td class="px-4 py-3">@entry.Version</td>
<td class="px-4 py-3">@entry.RevisionNumber</td>
<td class="px-4 py-3">@entry.CredentialsCount</td>
<td class="px-4 py-3">@entry.EmailClaimsCount</td>
<td class="px-4 py-3">
@if (entry == LatestVault)
{
<span class="bg-blue-100 text-blue-800 text-xs font-medium mr-2 px-2.5 py-0.5 rounded">Current</span>
}
@if (previousEntry != null && HasPasswordChanged(entry, previousEntry))
{
<span class="bg-green-100 text-green-800 text-xs font-medium mr-2 px-2.5 py-0.5 rounded">Password Changed</span>
}
</td>
<td class="px-4 py-3">
@if (entry != LatestVault)
{
<Button OnClick="() => MakeCurrentAsync(entry)">Make Current</Button>
}
</td>
</tr>
@{
Vault? previousEntry = null;
}
@foreach (var entry in VaultList.OrderBy(e => e.UpdatedAt))
{
<tr class="bg-white border-b hover:bg-gray-50">
<td class="px-4 py-3 font-medium text-gray-900">@entry.Id</td>
<td class="px-4 py-3">@entry.CreatedAt.ToString("yyyy-MM-dd HH:mm")</td>
<td class="px-4 py-3">@entry.UpdatedAt.ToString("yyyy-MM-dd HH:mm")</td>
<td class="px-4 py-3">@Math.Round((double)entry.FileSize / 1024, 1) MB</td>
<td class="px-4 py-3">@entry.Version</td>
<td class="px-4 py-3">@entry.RevisionNumber</td>
<td class="px-4 py-3">@entry.CredentialsCount</td>
<td class="px-4 py-3">@entry.EmailClaimsCount</td>
<td class="px-4 py-3">
@if (entry == LatestVault)
{
<span class="bg-blue-100 text-blue-800 text-xs font-medium mr-2 px-2.5 py-0.5 rounded">Current</span>
}
@if (previousEntry != null && HasPasswordChanged(entry, previousEntry))
{
<span class="bg-green-100 text-green-800 text-xs font-medium mr-2 px-2.5 py-0.5 rounded">Password Changed</span>
}
</td>
<td class="px-4 py-3">
@if (entry != LatestVault)
{
<Button OnClick="() => MakeCurrentAsync(entry)">Make Current</Button>
}
</td>
</tr>
previousEntry = entry;
}
previousEntry = entry;
}
</tbody>
</table>
</div>
</div>
</div>
<div class="p-4 mb-4 bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 sm:p-6 dark:bg-gray-800">
<div class="items-center">
<div>
@@ -148,7 +138,7 @@ else
<td class="px-4 py-3">@entry.CreatedAt.ToString("yyyy-MM-dd HH:mm")</td>
<td class="px-4 py-3">@entry.ExpireDate.ToString("yyyy-MM-dd HH:mm")</td>
<td class="px-4 py-3">
<button @onclick="() => RevokeRefreshToken(entry)" class="px-3 py-2 text-xs font-medium text-center text-white bg-red-700 rounded-lg hover:bg-red-800 focus:ring-4 focus:outline-none focus:ring-red-300 dark:bg-red-600 dark:hover:bg-red-700 dark:focus:ring-red-800">Revoke</button>
<Button Color="danger" OnClick="() => RevokeRefreshToken(entry)">Revoke</Button>
</td>
</tr>
}
@@ -219,11 +209,11 @@ else
if (firstRender)
{
await LoadEntryAsync();
await RefreshData();
}
}
private async Task LoadEntryAsync()
private async Task RefreshData()
{
IsLoading = true;
StateHasChanged();
@@ -294,7 +284,7 @@ else
{
DbContext.AliasVaultUserRefreshTokens.Remove(token);
await DbContext.SaveChangesAsync();
await LoadEntryAsync();
await RefreshData();
}
}
@@ -310,7 +300,7 @@ else
{
User.TwoFactorEnabled = true;
await DbContext.SaveChangesAsync();
await LoadEntryAsync();
await RefreshData();
}
}
@@ -327,7 +317,7 @@ else
{
User.TwoFactorEnabled = false;
await DbContext.SaveChangesAsync();
await LoadEntryAsync();
await RefreshData();
}
}
@@ -348,7 +338,7 @@ else
.ForEachAsync(x => DbContext.UserTokens.Remove(x));
await DbContext.SaveChangesAsync();
await LoadEntryAsync();
await RefreshData();
}
}
@@ -397,7 +387,7 @@ Do you want to proceed with the restoration?")) {
await DbContext.SaveChangesAsync();
// Reload the page.
await LoadEntryAsync();
await RefreshData();
}
}
}

View File

@@ -20,6 +20,8 @@
@using AliasVault.RazorComponents
@using AliasVault.RazorComponents.Alerts
@using AliasVault.RazorComponents.Buttons
@using AliasVault.RazorComponents.Headings
@using AliasVault.RazorComponents.Models
@using AliasVault.Admin.Main.Models
@using AliasVault.Admin.Main.Pages
@using AliasVault.Admin.Services

View File

@@ -4,6 +4,7 @@ module.exports = {
'./**/*.html',
'./**/*.razor',
'../Shared/AliasVault.RazorComponents/**/*.razor',
'../Shared/AliasVault.RazorComponents/**/*.cs',
],
safelist: [
'w-64',

View File

@@ -600,6 +600,10 @@ video {
border-width: 0;
}
.visible {
visibility: visible;
}
.invisible {
visibility: hidden;
}
@@ -675,10 +679,6 @@ video {
margin-bottom: 1rem;
}
.mb-5 {
margin-bottom: 1.25rem;
}
.mb-6 {
margin-bottom: 1.5rem;
}
@@ -687,10 +687,6 @@ video {
margin-bottom: 2rem;
}
.ml-1 {
margin-left: 0.25rem;
}
.ml-2 {
margin-left: 0.5rem;
}
@@ -751,6 +747,14 @@ video {
margin-top: 2rem;
}
.ms-1 {
margin-inline-start: 0.25rem;
}
.ml-1 {
margin-left: 0.25rem;
}
.line-clamp-1 {
overflow: hidden;
display: -webkit-box;
@@ -822,6 +826,10 @@ video {
height: 100%;
}
.h-3 {
height: 0.75rem;
}
.w-1\/2 {
width: 50%;
}
@@ -874,6 +882,10 @@ video {
width: 100%;
}
.w-3 {
width: 0.75rem;
}
.max-w-2xl {
max-width: 42rem;
}
@@ -952,10 +964,6 @@ video {
align-items: flex-start;
}
.items-end {
align-items: flex-end;
}
.items-center {
align-items: center;
}
@@ -988,6 +996,12 @@ video {
margin-left: calc(0.5rem * calc(1 - var(--tw-space-x-reverse)));
}
.space-x-3 > :not([hidden]) ~ :not([hidden]) {
--tw-space-x-reverse: 0;
margin-right: calc(0.75rem * var(--tw-space-x-reverse));
margin-left: calc(0.75rem * calc(1 - var(--tw-space-x-reverse)));
}
.space-x-4 > :not([hidden]) ~ :not([hidden]) {
--tw-space-x-reverse: 0;
margin-right: calc(1rem * var(--tw-space-x-reverse));
@@ -1024,6 +1038,12 @@ video {
margin-bottom: calc(1.5rem * var(--tw-space-y-reverse));
}
.space-y-3 > :not([hidden]) ~ :not([hidden]) {
--tw-space-y-reverse: 0;
margin-top: calc(0.75rem * calc(1 - var(--tw-space-y-reverse)));
margin-bottom: calc(0.75rem * var(--tw-space-y-reverse));
}
.divide-y > :not([hidden]) ~ :not([hidden]) {
--tw-divide-y-reverse: 0;
border-top-width: calc(1px * calc(1 - var(--tw-divide-y-reverse)));
@@ -1542,6 +1562,11 @@ video {
color: rgb(107 114 128 / var(--tw-text-opacity));
}
.text-gray-600 {
--tw-text-opacity: 1;
color: rgb(75 85 99 / var(--tw-text-opacity));
}
.text-gray-700 {
--tw-text-opacity: 1;
color: rgb(55 65 81 / var(--tw-text-opacity));
@@ -1719,11 +1744,6 @@ video {
background-color: rgb(154 93 38 / var(--tw-bg-opacity));
}
.hover\:bg-red-700:hover {
--tw-bg-opacity: 1;
background-color: rgb(185 28 28 / var(--tw-bg-opacity));
}
.hover\:bg-red-800:hover {
--tw-bg-opacity: 1;
background-color: rgb(153 27 27 / var(--tw-bg-opacity));
@@ -1780,11 +1800,6 @@ video {
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
}
.focus\:ring-blue-300:focus {
--tw-ring-opacity: 1;
--tw-ring-color: rgb(147 197 253 / var(--tw-ring-opacity));
}
.focus\:ring-blue-500:focus {
--tw-ring-opacity: 1;
--tw-ring-color: rgb(59 130 246 / var(--tw-ring-opacity));
@@ -1894,11 +1909,6 @@ video {
background-color: rgb(214 131 56 / var(--tw-bg-opacity));
}
.dark\:bg-red-500:is(.dark *) {
--tw-bg-opacity: 1;
background-color: rgb(239 68 68 / var(--tw-bg-opacity));
}
.dark\:bg-red-600:is(.dark *) {
--tw-bg-opacity: 1;
background-color: rgb(220 38 38 / var(--tw-bg-opacity));
@@ -2047,11 +2057,6 @@ video {
background-color: rgb(184 112 47 / var(--tw-bg-opacity));
}
.dark\:hover\:bg-red-600:hover:is(.dark *) {
--tw-bg-opacity: 1;
background-color: rgb(220 38 38 / var(--tw-bg-opacity));
}
.dark\:hover\:bg-red-700:hover:is(.dark *) {
--tw-bg-opacity: 1;
background-color: rgb(185 28 28 / var(--tw-bg-opacity));
@@ -2122,11 +2127,6 @@ video {
--tw-ring-color: rgb(153 27 27 / var(--tw-ring-opacity));
}
.dark\:focus\:ring-red-900:focus:is(.dark *) {
--tw-ring-opacity: 1;
--tw-ring-color: rgb(127 29 29 / var(--tw-ring-opacity));
}
@media (min-width: 640px) {
.sm\:flex {
display: flex;
@@ -2194,6 +2194,10 @@ video {
margin-right: 1.5rem;
}
.md\:inline {
display: inline;
}
.md\:flex {
display: flex;
}

View File

@@ -5,7 +5,7 @@
@implements IAsyncDisposable
<button @ref="buttonRef" @onclick="TogglePopup" id="quickIdentityButton" class="px-4 py-2 text-sm font-medium text-white bg-gradient-to-r from-primary-500 to-primary-600 hover:from-primary-600 hover:to-primary-700 focus:outline-none dark:from-primary-400 dark:to-primary-500 dark:hover:from-primary-500 dark:hover:to-primary-600 rounded-md shadow-sm transition duration-150 ease-in-out transform hover:scale-105 active:scale-95 focus:shadow-outline">
+ <span class="hidden md:inline">New identity</span>
+ <span class="hidden md:inline">New Alias</span>
</button>
@if (IsPopupVisible)
@@ -13,7 +13,7 @@
<ClickOutsideHandler OnClose="ClosePopup" ContentId="quickIdentityPopup,quickIdentityButton">
<div id="quickIdentityPopup" class="absolute z-50 mt-2 p-4 bg-white rounded-lg shadow-xl border border-gray-300"
style="@PopupStyle">
<h3 class="text-lg font-semibold mb-4">Create New Identity</h3>
<h3 class="text-lg font-semibold mb-4">Create New Alias</h3>
<EditForm Model="Model" OnValidSubmit="CreateIdentity">
<DataAnnotationsValidator />
<div class="mb-4">

View File

@@ -23,7 +23,7 @@
@code {
private static readonly string[] Quotes =
[
"Tip: Use the g+c (go create) keyboard shortcut to quickly create a new identity.",
"Tip: Use the g+c (go create) keyboard shortcut to quickly create a new alias.",
"Tip: Use the g+f (go find) keyboard shortcut to focus the search field.",
"Tip: Use the g+h (go home) keyboard shortcut to go to the homepage.",
"Tip: Use the g+l (go lock) keyboard shortcut to lock the vault.",

View File

@@ -11,39 +11,15 @@
@inject IJSRuntime JSRuntime
@implements IAsyncDisposable
@if (EditMode)
{
<LayoutPageTitle>Edit credentials</LayoutPageTitle>
}
else {
<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">
<div class="mb-4 col-span-full xl:mb-2">
<Breadcrumb BreadcrumbItems="BreadcrumbItems" />
<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 credentials</h1>
}
else {
<h1 class="text-xl font-semibold text-gray-900 sm:text-2xl dark:text-white">Add credentials</h1>
}
<div>
<button type="button" @onclick="TriggerFormSubmit" 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>
<button type="button" @onclick="Cancel" 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">Cancel</button>
</div>
</div>
@if (EditMode)
{
<p>Edit the existing credentials entry below.</p>
}
else {
<p>Create a new credentials entry below.</p>
}
</div>
</div>
<PageHeader
BreadcrumbItems="@BreadcrumbItems"
Title="@(EditMode ? "Edit credentials" : "Add credentials")"
Description="@(EditMode ? "Edit the existing credentials entry below." : "Create a new credentials entry below.")">
<CustomActions>
<ConfirmButton OnClick="TriggerFormSubmit">Save Credentials</ConfirmButton>
<CancelButton OnClick="Cancel">Cancel</CancelButton>
</CustomActions>
</PageHeader>
@if (Loading)
{
@@ -96,7 +72,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">
<h3 class="mb-4 text-xl font-semibold dark:text-white">Login credentials</h3>
<div class="mb-4">
<button type="button" @onclick="GenerateRandomAlias" 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">Generate Random Alias</button>
<Button OnClick="GenerateRandomAlias">Generate Random Alias</Button>
</div>
<div class="grid gap-6">
<div class="col-span-6 sm:col-span-3">
@@ -252,7 +228,7 @@ else
if (!EditMode)
{
// When creating a new identity: start with focus on the service name input.
// When creating a new alias: start with focus on the service name input.
await JsInteropService.FocusElementById("service-name");
}
}

View File

@@ -4,15 +4,11 @@
<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 credentials</h1>
</div>
<p>You can delete a credentials entry below.</p>
</div>
</div>
<PageHeader
BreadcrumbItems="@BreadcrumbItems"
Title="Delete credentials"
Description="You can delete a credentials entry below.">
</PageHeader>
@if (IsLoading)
{
@@ -55,8 +51,8 @@ else
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
BreadcrumbItems.Add(new BreadcrumbItem { Url = "credentials/" + Id, DisplayName = "View Credentials Entry" });
BreadcrumbItems.Add(new BreadcrumbItem { DisplayName = "Delete credentials" });
BreadcrumbItems.Add(new BreadcrumbItem { Url = "credentials/" + Id, DisplayName = "View credentials entry" });
BreadcrumbItems.Add(new BreadcrumbItem { DisplayName = "Delete credential" });
}
/// <inheritdoc />

View File

@@ -4,16 +4,14 @@
<LayoutPageTitle>Home</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">Credentials</h1>
<RefreshButton OnClick="LoadCredentialsAsync" ButtonText="Refresh" />
</div>
<p>Find all of your credentials below.</p>
</div>
</div>
<PageHeader
BreadcrumbItems="@BreadcrumbItems"
Title="Credentials"
Description="Find all of your credentials below.">
<CustomActions>
<RefreshButton OnClick="LoadCredentialsAsync" ButtonText="Refresh" />
</CustomActions>
</PageHeader>
@if (IsLoading)
{

View File

@@ -10,24 +10,24 @@
}
else
{
<PageHeader
BreadcrumbItems="@BreadcrumbItems"
Title="View credentials entry">
<CustomActions>
<LinkButton
Text="Edit"
AdditionalText="credentials entry"
Href="@($"/credentials/{Id}/edit")"
Color="primary" />
<LinkButton
Text="Delete"
AdditionalText="credentials entry"
Href="@($"/credentials/{Id}/delete")"
Color="danger" />
</CustomActions>
</PageHeader>
<div class="grid grid-cols-2 px-4 pt-6 md:grid-cols-3 lg: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">View credentials entry</h1>
<div class="flex">
<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 <span class="hidden md:inline">credentials entry</span>
</a>
<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 <span class="hidden md:inline">credentials entry</span>
</a>
</div>
</div>
</div>
<!-- Right Content -->
<div class="col-span-full lg:col-auto">
<div class="p-4 mb-4 bg-white border border-gray-200 rounded-lg shadow-sm 2xl:col-span-2 dark:border-gray-700 sm:p-6 dark:bg-gray-800">
<div class="items-center sm:flex xl:block 2xl:flex sm:space-x-4 xl:space-x-0 2xl:space-x-4">
@@ -48,7 +48,7 @@ else
<FormattedNote Notes="@Alias.Notes" />
}
@if (Alias.Attachments != null && Alias.Attachments.Count > 0)
@if (Alias.Attachments.Count > 0)
{
<AttachmentViewer Attachments="@Alias.Attachments" />
}

View File

@@ -17,16 +17,14 @@
<EmailModal Email="EmailModalEmail" IsSpamOk="false" OnClose="CloseEmailModal" OnEmailDeleted="RefreshData" />
}
<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">Emails</h1>
<RefreshButton OnClick="RefreshData" ButtonText="Refresh" />
</div>
<p>Below you can find all recent emails sent to one of the email addresses used in your credentials.</p>
</div>
</div>
<PageHeader
BreadcrumbItems="@BreadcrumbItems"
Title="Emails"
Description="You can view all emails received by your private email addresses below.">
<CustomActions>
<RefreshButton OnClick="RefreshData" ButtonText="Refresh" />
</CustomActions>
</PageHeader>
@if (IsLoading)
{
@@ -36,7 +34,7 @@ else if (NoEmailClaims)
{
<div class="p-4 mx-4 mt-4 bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 dark:bg-gray-800">
<div class="px-4 py-2 text-gray-400 rounded">
<p class="text-gray-500 dark:text-gray-400">You are not using any private email addresses (yet). Create a new identity and use a private email address supported by AliasVault. All emails received by these private email addresses will show up here.</p>
<p class="text-gray-500 dark:text-gray-400">You are not using any private email addresses (yet). Create a new alias and use a private email address supported by AliasVault. All emails received by these private email addresses will show up here.</p>
</div>
</div>
}

View File

@@ -9,6 +9,7 @@ namespace AliasVault.Client.Main.Pages;
using AliasVault.Client.Services;
using AliasVault.Client.Services.Auth;
using AliasVault.RazorComponents.Models;
using Blazored.LocalStorage;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Authorization;

View File

@@ -3,13 +3,11 @@
<LayoutPageTitle>General settings</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" />
<h1 class="text-xl font-semibold text-gray-900 sm:text-2xl dark:text-white">General settings</h1>
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">Configure general AliasVault settings.</p>
</div>
</div>
<PageHeader
BreadcrumbItems="@BreadcrumbItems"
Title="General settings"
Description="Configure general AliasVault settings.">
</PageHeader>
<div class="p-4 mb-4 mx-4 bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 sm:p-6 dark:bg-gray-800">
<h3 class="mb-4 text-lg font-medium text-gray-900 dark:text-white">Email Settings</h3>

View File

@@ -11,7 +11,7 @@
<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"/>
<h1 class="text-xl font-semibold text-gray-900 sm:text-2xl dark:text-white">Change password</h1>
<H1>Change password</H1>
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">Changing your master password also changes the vault encryption keys. It is advised to periodically change your master password to keep your vaults secure.</p>
</div>
</div>

View File

@@ -36,8 +36,7 @@
<td class="px-6 py-4">@session.CreatedAt.ToLocalTime().ToString("g")</td>
<td class="px-6 py-4">@session.ExpireDate.ToLocalTime().ToString("g")</td>
<td class="px-6 py-4">
<button @onclick="() => RevokeSession(session.Id)"
class="font-medium text-red-600 dark:text-red-500 hover:underline">Revoke (log out)</button>
<Button Color="danger" OnClick="() => RevokeSession(session.Id)">Revoke</Button>
</td>
</tr>
}

View File

@@ -13,7 +13,7 @@ else
<div class="grid grid-cols-1 px-4 pt-6 xl:grid-cols-3 xl:gap-4 dark:bg-gray-900">
<div class="mb-4 col-span-full xl:mb-2">
<Breadcrumb BreadcrumbItems="BreadcrumbItems"/>
<h1 class="text-xl font-semibold text-gray-900 sm:text-2xl dark:text-white">Disable two-factor authentication</h1>
<H1>Disable two-factor authentication</H1>
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">Disabling two-factor authentication means you will be able to login with only your password.</p>
</div>
</div>

View File

@@ -8,7 +8,7 @@
<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"/>
<h1 class="text-xl font-semibold text-gray-900 sm:text-2xl dark:text-white">Enable two-factor authentication</h1>
<H1>Enable two-factor authentication</H1>
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">Enable two-factor authentication to increase the security of your vaults.</p>
</div>
</div>

View File

@@ -4,16 +4,14 @@
<LayoutPageTitle>Security settings</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">Security settings</h1>
<RefreshButton OnClick="LoadData" ButtonText="Refresh" />
</div>
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">Configure security settings.</p>
</div>
</div>
<PageHeader
BreadcrumbItems="@BreadcrumbItems"
Title="Security settings"
Description="Configure security settings.">
<CustomActions>
<RefreshButton OnClick="LoadData" ButtonText="Refresh" />
</CustomActions>
</PageHeader>
<PasswordChangeSection OnStatusChanged="LoadData" />
<TwoFactorAuthenticationSection @ref="TwoFactorSection" OnStatusChanged="LoadData" />

View File

@@ -5,28 +5,20 @@
<LayoutPageTitle>Vault settings</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">Vault settings</h1>
</div>
<p>On this page you can configure your vault settings.</p>
</div>
</div>
<PageHeader
BreadcrumbItems="@BreadcrumbItems"
Title="Vault settings"
Description="On this page you can configure your vault settings.">
</PageHeader>
<div class="p-4 mx-4 mb-4 bg-white border border-gray-200 rounded-lg shadow-sm 2xl:col-span-2 dark:border-gray-700 sm:p-6 dark:bg-gray-800">
<h3 class="mb-4 text-xl font-semibold dark:text-white">Export vault</h3>
<div class="mb-4">
<div>
<button @onclick="ExportVaultSqlite" 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">
Export vault to unencrypted SQLite file
</button>
<Button OnClick="ExportVaultSqlite">Export vault to unencrypted SQLite file</Button>
</div>
<div class="mt-6">
<button @onclick="ExportVaultCsv" 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">
Export vault to unencrypted CSV file
</button>
<Button OnClick="ExportVaultCsv">Export vault to unencrypted CSV file</Button>
</div>
</div>
</div>
@@ -70,13 +62,13 @@ else if (!string.IsNullOrEmpty(ImportSuccessMessage))
{
try
{
// Decode the base64 string to a byte array
// Decode the base64 string to a byte array.
byte[] fileBytes = Convert.FromBase64String(await DbService.ExportSqliteToBase64Async());
// Create a memory stream from the byte array
// Create a memory stream from the byte array.
using (MemoryStream memoryStream = new MemoryStream(fileBytes))
{
// Invoke JavaScript to initiate the download
// Invoke JavaScript to initiate the download.
await JsInteropService.DownloadFileFromStream("aliasvault-client.sqlite", memoryStream.ToArray());
}
}
@@ -94,10 +86,10 @@ else if (!string.IsNullOrEmpty(ImportSuccessMessage))
var csvBytes = CsvImportExport.CredentialCsvService.ExportCredentialsToCsv(credentials);
// Create a memory stream from the byte array
// Create a memory stream from the byte array.
using (MemoryStream memoryStream = new MemoryStream(csvBytes))
{
// Invoke JavaScript to initiate the download
// Invoke JavaScript to initiate the download.
await JsInteropService.DownloadFileFromStream("aliasvault-client.csv", memoryStream.ToArray());
}
}
@@ -127,7 +119,7 @@ else if (!string.IsNullOrEmpty(ImportSuccessMessage))
var fileContent = System.Text.Encoding.UTF8.GetString(buffer);
var importedCredentials = CsvImportExport.CredentialCsvService.ImportCredentialsFromCsv(fileContent);
// Loop through the imported credentials and actually add them to the database
// Loop through the imported credentials and actually add them to the database.
foreach (var importedCredential in importedCredentials)
{
await CredentialService.InsertEntryAsync(importedCredential, false);

View File

@@ -4,15 +4,11 @@
<LayoutPageTitle>Test webapi call 1</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">Test webapi call 1</h1>
</div>
<p>Test webapi call 1.</p>
</div>
</div>
<PageHeader
BreadcrumbItems="@BreadcrumbItems"
Title="Test webapi call 1"
Description="Test webapi call 1">
</PageHeader>
@if (IsLoading)
{

View File

@@ -4,15 +4,11 @@
<LayoutPageTitle>Test webapi call 2</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">Test webapi call 2</h1>
</div>
<p>Test webapi call 2.</p>
</div>
</div>
<PageHeader
BreadcrumbItems="@BreadcrumbItems"
Title="Test webapi call 2"
Description="Test webapi call 2">
</PageHeader>
@if (IsLoading)
{

View File

@@ -1,7 +1,7 @@
@page "/welcome"
@inherits MainBase
<div class="container pt-10 px-4 mx-auto lg:px-0">
<div class="grid grid-cols-1 px-4 pt-6 dark:bg-gray-900">
<h1 class="mb-3 text-3xl font-bold text-gray-900 sm:text-4xl sm:leading-none sm:tracking-tight dark:text-white">Welcome to AliasVault</h1>
<p class="mb-6 text-lg font-normal text-gray-500 sm:text-xl dark:text-gray-400">It looks like you are new here. The instructions on this page will help to get you started.</p>
@@ -11,7 +11,7 @@
<div class="space-y-4">
<div>
<h3 class="text-lg font-medium text-gray-900 dark:text-white">How do I use AliasVault?</h3>
<p class="text-gray-600 dark:text-gray-400">Create a random identity with an associated email address. To get started, simply click the "+ New Identity" button in the top right corner.</p>
<p class="text-gray-600 dark:text-gray-400">Create a random identity with an associated email address. To get started, simply click the "+ New Alias" button in the top right corner.</p>
</div>
<div>
<h3 class="text-lg font-medium text-gray-900 dark:text-white">What is the purpose of AliasVault?</h3>
@@ -28,7 +28,7 @@
<h2 class="text-2xl font-semibold mb-4 text-gray-900 dark:text-white">Get Started Now</h2>
<div class="mb-4">
<p class="text-gray-600 dark:text-gray-400">
Go ahead and create a new login by clicking "+ New Identity" in the top right. Or explore these options:
Go ahead and create a new login by clicking "+ New Alias" in the top right. Or explore these options:
</p>
</div>
<div class="space-y-4">

View File

@@ -27,6 +27,8 @@
@using AliasVault.RazorComponents
@using AliasVault.RazorComponents.Alerts
@using AliasVault.RazorComponents.Buttons
@using AliasVault.RazorComponents.Headings
@using AliasVault.RazorComponents.Models
@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@using Blazored.LocalStorage

View File

@@ -3,6 +3,7 @@ module.exports = {
content: [
'./**/*.html',
'./**/*.razor',
'../Shared/AliasVault.RazorComponents/**/*.cs',
'../Shared/AliasVault.RazorComponents/**/*.razor',
],
safelist: [

View File

@@ -554,40 +554,6 @@ video {
--tw-contain-style: ;
}
.container {
width: 100%;
}
@media (min-width: 640px) {
.container {
max-width: 640px;
}
}
@media (min-width: 768px) {
.container {
max-width: 768px;
}
}
@media (min-width: 1024px) {
.container {
max-width: 1024px;
}
}
@media (min-width: 1280px) {
.container {
max-width: 1280px;
}
}
@media (min-width: 1536px) {
.container {
max-width: 1536px;
}
}
.sr-only {
position: absolute;
width: 1px;
@@ -600,6 +566,10 @@ video {
border-width: 0;
}
.visible {
visibility: visible;
}
.static {
position: static;
}
@@ -717,10 +687,6 @@ video {
margin-bottom: 1rem;
}
.mb-5 {
margin-bottom: 1.25rem;
}
.mb-6 {
margin-bottom: 1.5rem;
}
@@ -797,6 +763,10 @@ video {
margin-top: 2rem;
}
.ml-1 {
margin-left: 0.25rem;
}
.line-clamp-2 {
overflow: hidden;
display: -webkit-box;
@@ -1072,6 +1042,12 @@ video {
margin-left: calc(0.5rem * calc(1 - var(--tw-space-x-reverse)));
}
.space-x-3 > :not([hidden]) ~ :not([hidden]) {
--tw-space-x-reverse: 0;
margin-right: calc(0.75rem * var(--tw-space-x-reverse));
margin-left: calc(0.75rem * calc(1 - var(--tw-space-x-reverse)));
}
.space-x-4 > :not([hidden]) ~ :not([hidden]) {
--tw-space-x-reverse: 0;
margin-right: calc(1rem * var(--tw-space-x-reverse));
@@ -1517,10 +1493,6 @@ video {
padding-right: 0.75rem;
}
.pt-10 {
padding-top: 2.5rem;
}
.pt-16 {
padding-top: 4rem;
}
@@ -1913,11 +1885,6 @@ video {
background-color: rgb(220 38 38 / var(--tw-bg-opacity));
}
.hover\:bg-red-700:hover {
--tw-bg-opacity: 1;
background-color: rgb(185 28 28 / var(--tw-bg-opacity));
}
.hover\:bg-red-800:hover {
--tw-bg-opacity: 1;
background-color: rgb(153 27 27 / var(--tw-bg-opacity));
@@ -2119,11 +2086,6 @@ video {
background-color: rgb(17 24 39 / var(--tw-bg-opacity));
}
.dark\:bg-green-500:is(.dark *) {
--tw-bg-opacity: 1;
background-color: rgb(34 197 94 / var(--tw-bg-opacity));
}
.dark\:bg-green-600:is(.dark *) {
--tw-bg-opacity: 1;
background-color: rgb(22 163 74 / var(--tw-bg-opacity));
@@ -2139,11 +2101,6 @@ video {
background-color: rgb(214 131 56 / var(--tw-bg-opacity));
}
.dark\:bg-red-500:is(.dark *) {
--tw-bg-opacity: 1;
background-color: rgb(239 68 68 / var(--tw-bg-opacity));
}
.dark\:bg-red-600:is(.dark *) {
--tw-bg-opacity: 1;
background-color: rgb(220 38 38 / var(--tw-bg-opacity));
@@ -2267,11 +2224,6 @@ video {
background-color: rgb(55 65 81 / var(--tw-bg-opacity));
}
.dark\:hover\:bg-green-600:hover:is(.dark *) {
--tw-bg-opacity: 1;
background-color: rgb(22 163 74 / var(--tw-bg-opacity));
}
.dark\:hover\:bg-green-700:hover:is(.dark *) {
--tw-bg-opacity: 1;
background-color: rgb(21 128 61 / var(--tw-bg-opacity));
@@ -2287,11 +2239,6 @@ video {
background-color: rgb(184 112 47 / var(--tw-bg-opacity));
}
.dark\:hover\:bg-red-600:hover:is(.dark *) {
--tw-bg-opacity: 1;
background-color: rgb(220 38 38 / var(--tw-bg-opacity));
}
.dark\:hover\:bg-red-700:hover:is(.dark *) {
--tw-bg-opacity: 1;
background-color: rgb(185 28 28 / var(--tw-bg-opacity));
@@ -2569,11 +2516,6 @@ video {
.lg\:gap-4 {
gap: 1rem;
}
.lg\:px-0 {
padding-left: 0px;
padding-right: 0px;
}
}
@media (min-width: 1280px) {

View File

@@ -133,17 +133,20 @@ public class AliasServerDbContext : WorkerStatusDbContext, IDataProtectionKeyCon
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
foreach (var entity in modelBuilder.Model.GetEntityTypes())
{
foreach (var property in entity.GetProperties())
{
// NOTE: This is a workaround for SQLite. Add conditional check if SQLite is used.
// NOTE: SQL server doesn't need this override.
// SQLite does not support varchar(max) so we use TEXT.
if (property.ClrType == typeof(string) && property.GetMaxLength() == null)
// NOTE: This is a workaround for SQLite. Add conditional check if SQLite is used.
// NOTE: SQL server doesn't need this override.
if (Database.IsSqlite())
{
foreach (var entity in modelBuilder.Model.GetEntityTypes())
{
foreach (var property in entity.GetProperties())
{
property.SetColumnType("TEXT");
// SQLite does not support varchar(max) so we use TEXT.
if (property.ClrType == typeof(string) && property.GetMaxLength() == null)
{
property.SetColumnType("TEXT");
}
}
}
}

View File

@@ -54,7 +54,7 @@ public class Log
/// <summary>
/// Gets or sets the timestamp of the log entry.
/// </summary>
public DateTimeOffset TimeStamp { get; set; }
public DateTime TimeStamp { get; set; }
/// <summary>
/// Gets or sets the exception associated with the log entry.

View File

@@ -0,0 +1,831 @@
// <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("20241007153012_DatetimeOffsetToDateTime")]
partial class DatetimeOffsetToDateTime
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "8.0.8")
.HasAnnotation("Proxies:ChangeTracking", false)
.HasAnnotation("Proxies:CheckEquality", false)
.HasAnnotation("Proxies:LazyLoading", true);
modelBuilder.Entity("AliasServerDb.AdminRole", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT");
b.Property<string>("ConcurrencyStamp")
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<string>("NormalizedName")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("AdminRoles");
});
modelBuilder.Entity("AliasServerDb.AdminUser", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT");
b.Property<int>("AccessFailedCount")
.HasColumnType("INTEGER");
b.Property<string>("ConcurrencyStamp")
.HasColumnType("TEXT");
b.Property<string>("Email")
.HasColumnType("TEXT");
b.Property<bool>("EmailConfirmed")
.HasColumnType("INTEGER");
b.Property<DateTime?>("LastPasswordChanged")
.HasColumnType("TEXT");
b.Property<bool>("LockoutEnabled")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("TEXT");
b.Property<string>("NormalizedEmail")
.HasColumnType("TEXT");
b.Property<string>("NormalizedUserName")
.HasColumnType("TEXT");
b.Property<string>("PasswordHash")
.HasColumnType("TEXT");
b.Property<string>("PhoneNumber")
.HasColumnType("TEXT");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("INTEGER");
b.Property<string>("SecurityStamp")
.HasColumnType("TEXT");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("INTEGER");
b.Property<string>("UserName")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("AdminUsers");
});
modelBuilder.Entity("AliasServerDb.AliasVaultRole", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT");
b.Property<string>("ConcurrencyStamp")
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<string>("NormalizedName")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("AliasVaultRoles");
});
modelBuilder.Entity("AliasServerDb.AliasVaultUser", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT");
b.Property<int>("AccessFailedCount")
.HasColumnType("INTEGER");
b.Property<string>("ConcurrencyStamp")
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("Email")
.HasColumnType("TEXT");
b.Property<bool>("EmailConfirmed")
.HasColumnType("INTEGER");
b.Property<bool>("LockoutEnabled")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("TEXT");
b.Property<string>("NormalizedEmail")
.HasColumnType("TEXT");
b.Property<string>("NormalizedUserName")
.HasColumnType("TEXT");
b.Property<DateTime>("PasswordChangedAt")
.HasColumnType("TEXT");
b.Property<string>("PasswordHash")
.HasColumnType("TEXT");
b.Property<string>("PhoneNumber")
.HasColumnType("TEXT");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("INTEGER");
b.Property<string>("SecurityStamp")
.HasColumnType("TEXT");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("INTEGER");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("TEXT");
b.Property<string>("UserName")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("AliasVaultUsers");
});
modelBuilder.Entity("AliasServerDb.AliasVaultUserRefreshToken", 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>("IpAddress")
.HasMaxLength(45)
.HasColumnType("TEXT");
b.Property<string>("PreviousTokenValue")
.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("AliasVaultUserRefreshTokens");
});
modelBuilder.Entity("AliasServerDb.AuthLog", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("AdditionalInfo")
.HasMaxLength(255)
.HasColumnType("TEXT");
b.Property<string>("Browser")
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<string>("Country")
.HasMaxLength(50)
.HasColumnType("TEXT");
b.Property<string>("DeviceType")
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<int>("EventType")
.HasColumnType("nvarchar(50)");
b.Property<int?>("FailureReason")
.HasColumnType("INTEGER");
b.Property<string>("IpAddress")
.HasMaxLength(50)
.HasColumnType("TEXT");
b.Property<bool>("IsSuccess")
.HasColumnType("INTEGER");
b.Property<bool>("IsSuspiciousActivity")
.HasColumnType("INTEGER");
b.Property<string>("OperatingSystem")
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<string>("RequestPath")
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<DateTime>("Timestamp")
.HasColumnType("TEXT");
b.Property<string>("UserAgent")
.HasMaxLength(255)
.HasColumnType("TEXT");
b.Property<string>("Username")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex(new[] { "EventType" }, "IX_EventType");
b.HasIndex(new[] { "IpAddress" }, "IX_IpAddress");
b.HasIndex(new[] { "Timestamp" }, "IX_Timestamp");
b.HasIndex(new[] { "Username", "IsSuccess", "Timestamp" }, "IX_Username_IsSuccess_Timestamp")
.IsDescending(false, false, true);
b.HasIndex(new[] { "Username", "Timestamp" }, "IX_Username_Timestamp")
.IsDescending(false, true);
b.ToTable("AuthLogs");
});
modelBuilder.Entity("AliasServerDb.Email", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("Date")
.HasColumnType("TEXT");
b.Property<DateTime>("DateSystem")
.HasColumnType("TEXT");
b.Property<string>("EncryptedSymmetricKey")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("From")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("FromDomain")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("FromLocal")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("MessageHtml")
.HasColumnType("TEXT");
b.Property<string>("MessagePlain")
.HasColumnType("TEXT");
b.Property<string>("MessagePreview")
.HasColumnType("TEXT");
b.Property<string>("MessageSource")
.IsRequired()
.HasColumnType("TEXT");
b.Property<bool>("PushNotificationSent")
.HasColumnType("INTEGER");
b.Property<string>("Subject")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("To")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("ToDomain")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("ToLocal")
.IsRequired()
.HasColumnType("TEXT");
b.Property<Guid>("UserEncryptionKeyId")
.HasMaxLength(255)
.HasColumnType("TEXT");
b.Property<bool>("Visible")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("Date");
b.HasIndex("DateSystem");
b.HasIndex("PushNotificationSent");
b.HasIndex("ToLocal");
b.HasIndex("UserEncryptionKeyId");
b.HasIndex("Visible");
b.ToTable("Emails");
});
modelBuilder.Entity("AliasServerDb.EmailAttachment", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<byte[]>("Bytes")
.IsRequired()
.HasColumnType("BLOB");
b.Property<DateTime>("Date")
.HasColumnType("TEXT");
b.Property<int>("EmailId")
.HasColumnType("INTEGER");
b.Property<string>("Filename")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("Filesize")
.HasColumnType("INTEGER");
b.Property<string>("MimeType")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("EmailId");
b.ToTable("EmailAttachments");
});
modelBuilder.Entity("AliasServerDb.Log", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Application")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT");
b.Property<string>("Exception")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Level")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("TEXT");
b.Property<string>("LogEvent")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("LogEvent");
b.Property<string>("Message")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("MessageTemplate")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Properties")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("SourceContext")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTime>("TimeStamp")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("Application");
b.HasIndex("TimeStamp");
b.ToTable("Logs", (string)null);
});
modelBuilder.Entity("AliasServerDb.UserEmailClaim", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("Address")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("TEXT");
b.Property<string>("AddressDomain")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("TEXT");
b.Property<string>("AddressLocal")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("TEXT");
b.Property<string>("UserId")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("Address")
.IsUnique();
b.HasIndex("UserId");
b.ToTable("UserEmailClaims");
});
modelBuilder.Entity("AliasServerDb.UserEncryptionKey", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<bool>("IsPrimary")
.HasColumnType("INTEGER");
b.Property<string>("PublicKey")
.IsRequired()
.HasMaxLength(2000)
.HasColumnType("TEXT");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("TEXT");
b.Property<string>("UserId")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("UserEncryptionKeys");
});
modelBuilder.Entity("AliasServerDb.Vault", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<int>("CredentialsCount")
.HasColumnType("INTEGER");
b.Property<int>("EmailClaimsCount")
.HasColumnType("INTEGER");
b.Property<string>("EncryptionSettings")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("EncryptionType")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("FileSize")
.HasColumnType("INTEGER");
b.Property<long>("RevisionNumber")
.HasColumnType("INTEGER");
b.Property<string>("Salt")
.IsRequired()
.HasMaxLength(100)
.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>("Verifier")
.IsRequired()
.HasMaxLength(1000)
.HasColumnType("TEXT");
b.Property<string>("Version")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("Vaults");
});
modelBuilder.Entity("AliasVault.WorkerStatus.Database.WorkerServiceStatus", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("CurrentStatus")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT");
b.Property<string>("DesiredStatus")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT");
b.Property<DateTime>("Heartbeat")
.HasColumnType("TEXT");
b.Property<string>("ServiceName")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("varchar");
b.HasKey("Id");
b.ToTable("WorkerServiceStatuses");
});
modelBuilder.Entity("Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.DataProtectionKey", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("FriendlyName")
.HasColumnType("TEXT");
b.Property<string>("Xml")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("DataProtectionKeys");
});
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")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("RoleClaims", (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")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("UserClaims", (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")
.HasColumnType("TEXT");
b.HasKey("LoginProvider", "ProviderKey");
b.ToTable("UserLogins", (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.ToTable("UserRoles", (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("UserTokens", (string)null);
});
modelBuilder.Entity("AliasServerDb.AliasVaultUserRefreshToken", b =>
{
b.HasOne("AliasServerDb.AliasVaultUser", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("AliasServerDb.Email", b =>
{
b.HasOne("AliasServerDb.UserEncryptionKey", "EncryptionKey")
.WithMany("Emails")
.HasForeignKey("UserEncryptionKeyId")
.OnDelete(DeleteBehavior.NoAction)
.IsRequired();
b.Navigation("EncryptionKey");
});
modelBuilder.Entity("AliasServerDb.EmailAttachment", b =>
{
b.HasOne("AliasServerDb.Email", "Email")
.WithMany("Attachments")
.HasForeignKey("EmailId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Email");
});
modelBuilder.Entity("AliasServerDb.UserEmailClaim", b =>
{
b.HasOne("AliasServerDb.AliasVaultUser", "User")
.WithMany("EmailClaims")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("AliasServerDb.UserEncryptionKey", b =>
{
b.HasOne("AliasServerDb.AliasVaultUser", "User")
.WithMany("EncryptionKeys")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("AliasServerDb.Vault", b =>
{
b.HasOne("AliasServerDb.AliasVaultUser", "User")
.WithMany("Vaults")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("AliasServerDb.AliasVaultUser", b =>
{
b.Navigation("EmailClaims");
b.Navigation("EncryptionKeys");
b.Navigation("Vaults");
});
modelBuilder.Entity("AliasServerDb.Email", b =>
{
b.Navigation("Attachments");
});
modelBuilder.Entity("AliasServerDb.UserEncryptionKey", b =>
{
b.Navigation("Emails");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,23 @@
// <auto-generated />
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace AliasServerDb.Migrations
{
/// <inheritdoc />
public partial class DatetimeOffsetToDateTime : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
}
}
}

View File

@@ -452,7 +452,7 @@ namespace AliasServerDb.Migrations
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("TimeStamp")
b.Property<DateTime>("TimeStamp")
.HasColumnType("TEXT");
b.HasKey("Id");

View File

@@ -18,7 +18,7 @@
<ItemGroup>
<SupportedPlatform Include="browser"/>
<SupportedPlatform Include="browser" />
</ItemGroup>
<ItemGroup>

View File

@@ -1,4 +1,5 @@
<button @onclick="HandleClick"
<button type="@Type"
@onclick="HandleClick"
disabled="@IsDisabled"
class="@GetButtonClasses()">
@ChildContent
@@ -23,6 +24,12 @@
[Parameter]
public bool IsDisabled { get; set; }
/// <summary>
/// Specifies the type of the button. Default is "button".
/// </summary>
[Parameter]
public string Type { get; set; } = "button";
/// <summary>
/// The color theme of the button.
/// </summary>
@@ -35,6 +42,12 @@
[Parameter]
public string AdditionalClasses { get; set; } = "";
/// <summary>
/// The display class of the button. Defaults to inline.
/// </summary>
[Parameter]
public string Display { get; set; } = "inline";
/// <summary>
/// Handles the button click event.
/// </summary>
@@ -52,17 +65,9 @@
/// <returns>A string containing the CSS classes for the button.</returns>
private string GetButtonClasses()
{
var baseClasses = "flex center items-center px-3 py-2 text-sm font-medium text-white rounded-lg focus:outline-none focus:ring-4";
var colorClasses = Color switch
{
"primary" => "bg-primary-700 hover:bg-primary-800 focus:ring-primary-300 dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800",
"danger" => "bg-red-700 hover:bg-red-800 focus:ring-red-300 dark:bg-red-600 dark:hover:bg-red-700 dark:focus:ring-red-800",
"success" => "bg-green-700 hover:bg-green-800 focus:ring-green-300 dark:bg-green-600 dark:hover:bg-green-700 dark:focus:ring-green-800",
"secondary" => "bg-secondary-700 hover:bg-secondary-800 focus:ring-secondary-300 dark:bg-secondary-600 dark:hover:bg-secondary-700 dark:focus:ring-secondary-800",
_ => "bg-gray-700 hover:bg-gray-800 focus:ring-gray-300 dark:bg-gray-600 dark:hover:bg-gray-700 dark:focus:ring-gray-800"
};
var disabledClasses = IsDisabled ? "bg-gray-400 cursor-not-allowed" : "";
var colorClasses = ButtonStyles.GetColorClasses(Color);
var disabledClasses = IsDisabled ? ButtonStyles.DisabledClasses : "";
return $"{baseClasses} {colorClasses} {disabledClasses} {AdditionalClasses}".Trim();
return $"{Display} {ButtonStyles.BaseClasses} {colorClasses} {disabledClasses} {AdditionalClasses}".Trim();
}
}

View File

@@ -0,0 +1,38 @@
//-----------------------------------------------------------------------
// <copyright file="ButtonStyles.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.RazorComponents.Buttons;
/// <summary>
/// A static class that provides CSS classes for buttons.
/// </summary>
public static class ButtonStyles
{
/// <summary>
/// Gets the base CSS classes for buttons.
/// </summary>
public static string BaseClasses => "center items-center px-3 py-2 text-sm font-medium text-white rounded-lg focus:outline-none focus:ring-4";
/// <summary>
/// Gets the CSS classes for a disabled button.
/// </summary>
public static string DisabledClasses => "bg-gray-400 cursor-not-allowed";
/// <summary>
/// Gets the color-specific CSS classes for a button based on the provided color.
/// </summary>
/// <param name="color">The color name for the button (e.g., "primary", "danger", "success", "secondary").</param>
/// <returns>A string containing the appropriate CSS classes for the specified color.</returns>
public static string GetColorClasses(string color) => color switch
{
"primary" => "bg-primary-700 hover:bg-primary-800 focus:ring-primary-300 dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800",
"secondary" => "bg-gray-700 hover:bg-gray-800 focus:ring-gray-300 dark:bg-gray-600 dark:hover:bg-gray-700 dark:focus:ring-gray-800",
"danger" => "bg-red-700 hover:bg-red-800 focus:ring-red-300 dark:bg-red-600 dark:hover:bg-red-700 dark:focus:ring-red-800",
"success" => "bg-green-700 hover:bg-green-800 focus:ring-green-300 dark:bg-green-600 dark:hover:bg-green-700 dark:focus:ring-green-800",
_ => "bg-gray-700 hover:bg-gray-800 focus:ring-gray-300 dark:bg-gray-600 dark:hover:bg-gray-700 dark:focus:ring-gray-800",
};
}

View File

@@ -0,0 +1,26 @@
<Button OnClick="HandleClick"
Color="danger">
@ChildContent
</Button>
@code {
/// <summary>
/// The event to call in the parent when the cancel button is clicked.
/// </summary>
[Parameter]
public EventCallback OnClick { get; set; }
/// <summary>
/// The content to be displayed inside the button.
/// </summary>
[Parameter]
public RenderFragment? ChildContent { get; set; }
/// <summary>
/// Handles the button click event.
/// </summary>
private async Task HandleClick()
{
await OnClick.InvokeAsync();
}
}

View File

@@ -0,0 +1,26 @@
<Button OnClick="HandleClick"
Color="success">
@ChildContent
</Button>
@code {
/// <summary>
/// The event to call in the parent when the confirm button is clicked.
/// </summary>
[Parameter]
public EventCallback OnClick { get; set; }
/// <summary>
/// The content to be displayed inside the button.
/// </summary>
[Parameter]
public RenderFragment? ChildContent { get; set; }
/// <summary>
/// Handles the button click event.
/// </summary>
private async Task HandleClick()
{
await OnClick.InvokeAsync();
}
}

View File

@@ -1,4 +1,5 @@
<Button OnClick="HandleClick"
Display="flex"
Color="danger">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>

View File

@@ -0,0 +1,46 @@
<a href="@Href" class="space-x-3 @GetButtonClasses()">
@Text @if (AdditionalText.Length > 0) { <span class="hidden md:inline ms-1">@AdditionalText</span> }
</a>
@code {
/// <summary>
/// Gets or sets the URL that the hyperlink points to.
/// </summary>
[Parameter]
public string Href { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the main text of the button.
/// </summary>
[Parameter]
public string Text { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the additional text that appears on larger screens.
/// </summary>
[Parameter]
public string AdditionalText { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the color theme of the button.
/// </summary>
[Parameter]
public string Color { get; set; } = "primary";
/// <summary>
/// Gets or sets additional CSS classes to apply to the button.
/// </summary>
[Parameter]
public string AdditionalClasses { get; set; } = string.Empty;
/// <summary>
/// Gets the CSS classes for the link button based on the color and additional classes.
/// </summary>
/// <returns>A string containing the CSS classes for the link button.</returns>
private string GetButtonClasses()
{
var colorClasses = ButtonStyles.GetColorClasses(Color);
return $"{ButtonStyles.BaseClasses} {colorClasses} {AdditionalClasses}".Trim();
}
}

View File

@@ -1,9 +1,10 @@
@using System.Timers
<Button OnClick="HandleClick"
IsDisabled="@IsRefreshing"
Color="@Color"
AdditionalClasses="@AdditionalClasses">
IsDisabled="@IsRefreshing"
Display="flex"
Color="@Color"
AdditionalClasses="@AdditionalClasses">
<svg class="@GetIconClasses()" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
</svg>

View File

@@ -0,0 +1,13 @@
<Button Type="submit"
Color="primary"
AdditionalClasses="w-full text-center">
@ChildContent
</Button>
@code {
/// <summary>
/// The content to be displayed inside the button.
/// </summary>
[Parameter]
public RenderFragment? ChildContent { get; set; }
}

View File

@@ -1,6 +1,4 @@
@inherits ComponentBase
<nav class="flex mb-5">
<nav class="flex mb-4">
<ol class="inline-flex items-center space-x-1 text-sm font-medium md:space-x-2">
<li class="inline-flex items-center">
<a href="/" class="inline-flex items-center text-gray-700 hover:text-primary-600 dark:text-gray-300 dark:hover:text-primary-500">

View File

@@ -0,0 +1,9 @@
<h1 class="text-xl font-semibold text-gray-900 sm:text-2xl dark:text-white">@ChildContent</h1>
@code {
/// <summary>
/// The content to be displayed inside the heading.
/// </summary>
[Parameter]
public RenderFragment? ChildContent { get; set; }
}

View File

@@ -0,0 +1,41 @@
<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>@Title</H1>
@if (CustomActions != null)
{
<div class="flex items-center space-x-3">
@CustomActions
</div>
}
</div>
<p class="text-sm text-gray-600 dark:text-gray-400">@Description</p>
</div>
</div>
@code {
/// <summary>
/// Gets or sets the breadcrumb items for the header.
/// </summary>
[Parameter]
public List<BreadcrumbItem> BreadcrumbItems { get; set; } = new List<BreadcrumbItem>();
/// <summary>
/// Gets or sets the title of the header.
/// </summary>
[Parameter]
public string Title { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the description text below the title.
/// </summary>
[Parameter]
public string Description { get; set; } = string.Empty;
/// <summary>
/// The caller can provide a custom action button section to be displayed on the right side of the header.
/// </summary>
[Parameter]
public RenderFragment? CustomActions { get; set; }
}

View File

@@ -5,7 +5,7 @@
// </copyright>
//-----------------------------------------------------------------------
namespace AliasVault.Client.Main.Models;
namespace AliasVault.RazorComponents.Models;
/// <summary>
/// Represents a breadcrumb item for the breadcrumb component.

View File

@@ -1,24 +1,24 @@
//-----------------------------------------------------------------------
// <copyright file="BreadcrumbItem.cs" company="lanedirt">
// <copyright file="SortDirection.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.Admin.Main.Models;
namespace AliasVault.RazorComponents.Tables;
/// <summary>
/// Breadcrumb item model.
/// An enum that represents the direction of a sort.
/// </summary>
public class BreadcrumbItem
public enum SortDirection
{
/// <summary>
/// Gets or sets the display name.
/// The sort is ascending.
/// </summary>
public string? DisplayName { get; set; }
Ascending,
/// <summary>
/// Gets or sets the URL.
/// The sort is descending.
/// </summary>
public string? Url { get; set; }
Descending,
}

View File

@@ -0,0 +1,85 @@
<table class="w-full text-sm text-left text-gray-500 shadow rounded border">
<thead class="text-xs text-gray-700 uppercase bg-gray-50">
<tr>
@foreach (var column in Columns)
{
<th scope="col" class="px-4 py-3">
@if (column.Sortable)
{
<button class="flex items-center" @onclick="() => OnSort(column.PropertyName)">
@column.Title
<span class="ml-1">
@if (SortColumn == column.PropertyName)
{
@if (SortDirection == SortDirection.Ascending)
{
<svg class="w-3 h-3" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M5.293 7.707a1 1 0 010-1.414l4-4a1 1 0 011.414 0l4 4a1 1 0 01-1.414 1.414L11 5.414V17a1 1 0 11-2 0V5.414L6.707 7.707a1 1 0 01-1.414 0z" clip-rule="evenodd"></path>
</svg>
}
else
{
<svg class="w-3 h-3" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M14.707 12.293a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 111.414-1.414L9 14.586V3a1 1 0 012 0v11.586l2.293-2.293a1 1 0 011.414 0z" clip-rule="evenodd"></path>
</svg>
}
}
</span>
</button>
}
else
{
@column.Title
}
</th>
}
</tr>
</thead>
<tbody>
@ChildContent
</tbody>
</table>
@code {
/// <summary>
/// Gets or sets the columns to display in the table.
/// </summary>
[Parameter] public List<TableColumn> Columns { get; set; } = new();
/// <summary>
/// Gets or sets the child content of the table.
/// </summary>
[Parameter] public RenderFragment ChildContent { get; set; } = null!;
/// <summary>
/// Gets or sets the column to sort by.
/// </summary>
[Parameter] public string SortColumn { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the direction to sort by.
/// </summary>
[Parameter] public SortDirection SortDirection { get; set; }
/// <summary>
/// Gets or sets the event to invoke when the sort changes.
/// </summary>
[Parameter] public EventCallback<(string, SortDirection)> OnSortChanged { get; set; }
private async Task OnSort(string columnName)
{
if (SortColumn == columnName)
{
SortDirection = SortDirection == SortDirection.Ascending
? SortDirection.Descending
: SortDirection.Ascending;
}
else
{
SortColumn = columnName;
SortDirection = SortDirection.Ascending;
}
await OnSortChanged.InvokeAsync((SortColumn, SortDirection));
}
}

View File

@@ -0,0 +1,29 @@
//-----------------------------------------------------------------------
// <copyright file="TableColumn.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.RazorComponents.Tables;
/// <summary>
/// A class that represents a column in a table.
/// </summary>
public class TableColumn
{
/// <summary>
/// Gets or sets the title of the column.
/// </summary>
public required string Title { get; set; }
/// <summary>
/// Gets or sets the name of the property to bind to.
/// </summary>
public required string PropertyName { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the column is sortable.
/// </summary>
public bool Sortable { get; set; } = true;
}

View File

@@ -1 +1,3 @@
@using Microsoft.AspNetCore.Components.Web
@using AliasVault.RazorComponents.Models
@using AliasVault.RazorComponents.Tables
@using Microsoft.AspNetCore.Components.Web

View File

@@ -253,8 +253,8 @@ public class ClientPlaywrightTest : PlaywrightTest
await Page.ClickAsync("text=" + credentialName);
// Wait for the credential details page to load.
await WaitForUrlAsync("credentials/**", "Delete credentials entry");
await Page.ClickAsync("text=Delete credentials entry");
await WaitForUrlAsync("credentials/**", "Delete");
await Page.ClickAsync("text=Delete");
// Wait for the delete credential page to load.
await WaitForUrlAsync("credentials/**/delete", "You can delete a credentials entry below");

View File

@@ -100,7 +100,7 @@ public class CredentialTests : ClientPlaywrightTest
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 credentials entry").First;
var editButton = Page.Locator("text=Edit").First;
await editButton.ClickAsync();
await WaitForUrlAsync("edit", "Save Credentials");
@@ -113,7 +113,7 @@ public class CredentialTests : ClientPlaywrightTest
var submitButton = Page.Locator("text=Save Credentials").First;
await submitButton.ClickAsync();
await WaitForUrlAsync("credentials/**", "Delete credentials entry");
await WaitForUrlAsync("credentials/**", "Delete");
pageContent = await Page.TextContentAsync("body");
Assert.That(pageContent, Does.Contain("Credentials updated"), "Credential update confirmation message not shown.");

View File

@@ -36,7 +36,7 @@ public class JwtTokenTests : ClientPlaywrightTest
await WaitForUrlAsync("test/2", "Test 2 OK");
var pageContent = await Page.TextContentAsync("body");
Assert.That(pageContent, Does.Contain("Test webapi call 2."), "No page content after refreshing access token.");
Assert.That(pageContent, Does.Contain("Test webapi call 2"), "No page content after refreshing access token.");
}
/// <summary>
@@ -74,7 +74,7 @@ public class JwtTokenTests : ClientPlaywrightTest
await WaitForUrlAsync(startUrl, "Test 1 OK");
var pageContent = await Page.TextContentAsync("body");
Assert.That(pageContent, Does.Contain("Test webapi call 1."), "No index content after unlocking database with a expired JWT token.");
Assert.That(pageContent, Does.Contain("Test webapi call 1"), "No index content after unlocking database with a expired JWT token.");
}
/// <summary>

View File

@@ -30,10 +30,10 @@ public class UnlockTests : ClientPlaywrightTest
await RefreshPageAndUnlockVault();
// Check if we get redirected back to the page we were trying to access.
await WaitForUrlAsync(startUrl, "Test webapi call 1.");
await WaitForUrlAsync(startUrl, "Test webapi call 1");
var pageContent = await Page.TextContentAsync("body");
Assert.That(pageContent, Does.Contain("Test webapi call 1."), "No index content after unlocking database.");
Assert.That(pageContent, Does.Contain("Test 1 OK."), "No index content after unlocking database.");
}
/// <summary>
@@ -69,9 +69,9 @@ public class UnlockTests : ClientPlaywrightTest
await submitButton.ClickAsync();
// Check if we get redirected back to the page we were trying to access.
await WaitForUrlAsync(startUrl, "Test webapi call 1.");
await WaitForUrlAsync(startUrl, "Test webapi call 1");
var pageContent = await Page.TextContentAsync("body");
Assert.That(pageContent, Does.Contain("Test webapi call 1."), "No index content after unlocking database.");
Assert.That(pageContent, Does.Contain("Test 1 OK."), "No index content after unlocking database.");
}
}