Compare commits

...

36 Commits
0.9.0 ... 0.9.4

Author SHA1 Message Date
Leendert de Borst
a759091755 Update AppInfo.cs (#479) 2024-12-16 16:55:58 +01:00
dependabot[bot]
8dc99c09a8 Bump Swashbuckle.AspNetCore from 7.1.0 to 7.2.0
Bumps [Swashbuckle.AspNetCore](https://github.com/domaindrivendev/Swashbuckle.AspNetCore) from 7.1.0 to 7.2.0.
- [Release notes](https://github.com/domaindrivendev/Swashbuckle.AspNetCore/releases)
- [Commits](https://github.com/domaindrivendev/Swashbuckle.AspNetCore/compare/v7.1.0...v7.2.0)

---
updated-dependencies:
- dependency-name: Swashbuckle.AspNetCore
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-16 12:19:17 +01:00
dependabot[bot]
b9ec4baf66 Bump NUglify from 1.21.10 to 1.21.11
Bumps [NUglify](https://github.com/trullock/NUglify) from 1.21.10 to 1.21.11.
- [Release notes](https://github.com/trullock/NUglify/releases)
- [Changelog](https://github.com/trullock/NUglify/blob/master/changelog.md)
- [Commits](https://github.com/trullock/NUglify/compare/v1.21.10...v1.21.11)

---
updated-dependencies:
- dependency-name: NUglify
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-16 12:19:10 +01:00
Leendert de Borst
71ed62cdcb Merge pull request #478 from lanedirt/469-webassembly-required-error-not-visible-in-client-app
Add E2E test for browser with WASM disabled
2024-12-16 12:19:00 +01:00
dependabot[bot]
2bbad8c75c Bump NUnit from 4.2.2 to 4.3.0
Bumps [NUnit](https://github.com/nunit/nunit) from 4.2.2 to 4.3.0.
- [Release notes](https://github.com/nunit/nunit/releases)
- [Changelog](https://github.com/nunit/nunit/blob/main/CHANGES.md)
- [Commits](https://github.com/nunit/nunit/compare/4.2.2...4.3.0)

---
updated-dependencies:
- dependency-name: NUnit
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-16 11:24:59 +01:00
dependabot[bot]
f02b841eea Bump Serilog and Serilog.Settings.Configuration
Bumps [Serilog](https://github.com/serilog/serilog) and [Serilog.Settings.Configuration](https://github.com/serilog/serilog-settings-configuration). These dependencies needed to be updated together.

Updates `Serilog` from 4.2.0 to 4.2.0
- [Release notes](https://github.com/serilog/serilog/releases)
- [Commits](https://github.com/serilog/serilog/compare/v4.2.0...v4.2.0)

Updates `Serilog.Settings.Configuration` from 8.0.4 to 9.0.0
- [Release notes](https://github.com/serilog/serilog-settings-configuration/releases)
- [Changelog](https://github.com/serilog/serilog-settings-configuration/blob/dev/CHANGES.md)
- [Commits](https://github.com/serilog/serilog-settings-configuration/compare/v8.0.4...v9.0.0)

---
updated-dependencies:
- dependency-name: Serilog
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: Serilog.Settings.Configuration
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-16 11:24:36 +01:00
dependabot[bot]
f6fc5af8ac Bump MailKit from 4.8.0 to 4.9.0
Bumps [MailKit](https://github.com/jstedfast/MailKit) from 4.8.0 to 4.9.0.
- [Changelog](https://github.com/jstedfast/MailKit/blob/master/ReleaseNotes.md)
- [Commits](https://github.com/jstedfast/MailKit/compare/4.8.0...4.9.0)

---
updated-dependencies:
- dependency-name: MailKit
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-16 11:24:30 +01:00
dependabot[bot]
1d1155bf0e Bump MimeKit from 4.8.0 to 4.9.0
Bumps [MimeKit](https://github.com/jstedfast/MimeKit) from 4.8.0 to 4.9.0.
- [Changelog](https://github.com/jstedfast/MimeKit/blob/master/ReleaseNotes.md)
- [Commits](https://github.com/jstedfast/MimeKit/compare/4.8.0...4.9.0)

---
updated-dependencies:
- dependency-name: MimeKit
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-16 11:24:24 +01:00
Leendert de Borst
2632211af6 Merge pull request #470 from lanedirt/469-webassembly-required-error-not-visible-in-client-app
Show error if client does not support WebAssembly
2024-12-16 10:34:20 +01:00
Leendert de Borst
05cca6998e Merge pull request #468 from lanedirt/467-task-runner-jobs-do-not-always-run-at-configured-time
Add task runner job table for tracking task runner historic runs
2024-12-16 10:18:13 +01:00
Leendert de Borst
c4a8a20a62 Add E2E test for browser with WASM disabled (#469) 2024-12-15 17:05:31 +01:00
Leendert de Borst
f2c6af9ccb Update install.sh URL comment (#469) 2024-12-15 16:43:48 +01:00
Leendert de Borst
e94201acda Tweak logo on mobile view auth area (#469) 2024-12-15 16:28:57 +01:00
Leendert de Borst
9e03473208 Show error message when client does not support WebAssembly (#469) 2024-12-15 16:28:41 +01:00
Leendert de Borst
0c5b2fb1da Add task runner job table and manual start button (#467) 2024-12-15 15:59:51 +01:00
Leendert de Borst
a5c4a7618d Update AliasServerDbContext.cs so pragma settings are applied correctly (#467) 2024-12-15 14:53:33 +01:00
Leendert de Borst
70220cecbb Merge pull request #466 from lanedirt/465-prepare-093-release
Update version to 0.9.3
2024-12-13 13:09:33 +01:00
Leendert de Borst
c63faa352f Update version to 0.9.3 (#465) 2024-12-13 13:09:14 +01:00
Leendert de Borst
7e261a05c9 Merge pull request #464 from lanedirt/463-bump-spamokpasswordgenerator-library-to-110
Bump SpamOK.PasswordGenerator version to 1.1.0
2024-12-13 13:01:45 +01:00
Leendert de Borst
545ec5576e Bump SpamOK.PasswordGenerator version to 1.1.0 (#463) 2024-12-13 12:45:05 +01:00
dependabot[bot]
73dcbe5860 Bump the npm_and_yarn group across 2 directories with 1 update
Bumps the npm_and_yarn group with 1 update in the /src/AliasVault.Admin directory: [nanoid](https://github.com/ai/nanoid).
Bumps the npm_and_yarn group with 1 update in the /src/AliasVault.Client directory: [nanoid](https://github.com/ai/nanoid).


Updates `nanoid` from 3.3.7 to 3.3.8
- [Release notes](https://github.com/ai/nanoid/releases)
- [Changelog](https://github.com/ai/nanoid/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ai/nanoid/compare/3.3.7...3.3.8)

Updates `nanoid` from 3.3.7 to 3.3.8
- [Release notes](https://github.com/ai/nanoid/releases)
- [Changelog](https://github.com/ai/nanoid/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ai/nanoid/compare/3.3.7...3.3.8)

---
updated-dependencies:
- dependency-name: nanoid
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: nanoid
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-13 12:15:20 +01:00
Leendert de Borst
13917444b9 Merge pull request #461 from lanedirt/460-password-not-correct
Fix account registration username capitalization login bug
2024-12-13 12:15:08 +01:00
dependabot[bot]
119e13a9dd Bump Serilog from 4.1.0 to 4.2.0
Bumps [Serilog](https://github.com/serilog/serilog) from 4.1.0 to 4.2.0.
- [Release notes](https://github.com/serilog/serilog/releases)
- [Commits](https://github.com/serilog/serilog/compare/v4.1.0...v4.2.0)

---
updated-dependencies:
- dependency-name: Serilog
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-13 11:50:58 +01:00
dependabot[bot]
7d656e9a9a Bump Microsoft.IdentityModel.JsonWebTokens and Microsoft.IdentityModel.Tokens
Bumps [Microsoft.IdentityModel.JsonWebTokens](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet) and [Microsoft.IdentityModel.Tokens](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet). These dependencies needed to be updated together.

Updates `Microsoft.IdentityModel.JsonWebTokens` from 8.2.1 to 8.3.0
- [Release notes](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/releases)
- [Changelog](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/blob/dev/CHANGELOG.md)
- [Commits](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/compare/8.2.1...8.3.0)

Updates `Microsoft.IdentityModel.Tokens` from 8.2.1 to 8.3.0
- [Release notes](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/releases)
- [Changelog](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/blob/dev/CHANGELOG.md)
- [Commits](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/compare/8.2.1...8.3.0)

---
updated-dependencies:
- dependency-name: Microsoft.IdentityModel.JsonWebTokens
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: Microsoft.IdentityModel.Tokens
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-13 11:50:50 +01:00
Leendert de Borst
8bd05b5c2e Fix account registration username capitalization login bug (#460) 2024-12-13 11:50:18 +01:00
Leendert de Borst
1e65f14323 Update README.md 2024-12-11 23:30:41 +01:00
Leendert de Borst
2c7543889d Update README.md 2024-12-11 18:24:10 +01:00
Leendert de Borst
63c5483208 Merge pull request #455 from lanedirt/454-update-default-server-settings
Update default server settings
2024-12-05 10:07:07 +01:00
Leendert de Borst
2586d61651 Merge pull request #457 from lanedirt/456-add-task-runner-to-installsh-pull-list
Update install.sh to include task runner to image pull list
2024-12-05 10:07:00 +01:00
Leendert de Borst
c7a32cf0e9 Update install.sh (#456) 2024-12-04 21:32:13 +01:00
Leendert de Borst
46cc6527aa Update default server settings (#454) 2024-12-04 19:17:56 +01:00
Leendert de Borst
ef291bffc1 Merge pull request #452 from lanedirt/451-task-runner-cannot-access-database
Task runner cannot access database
2024-12-04 18:55:42 +01:00
Leendert de Borst
94f6199e27 Update AppInfo.cs (#451) 2024-12-04 18:55:05 +01:00
Leendert de Borst
5ababf3bf3 Update docker-compose.yml (#451) 2024-12-04 18:54:23 +01:00
Leendert de Borst
b47e735e8f Merge pull request #450 from lanedirt/449-publish-new-task-runner-image-to-ghcrio
Publish task runner docker container image
2024-12-04 18:39:34 +01:00
Leendert de Borst
de17303085 Publish task runner docker container image (#449) 2024-12-04 18:39:15 +01:00
35 changed files with 1634 additions and 89 deletions

View File

@@ -70,6 +70,14 @@ jobs:
push: true
tags: ${{ env.REGISTRY }}/${{ env.REPO_LOWER }}-smtp:latest,${{ env.REGISTRY }}/${{ env.REPO_LOWER }}-smtp:${{ github.ref_name }}
- name: Build and push TaskRunner image
uses: docker/build-push-action@v5
with:
context: .
file: src/Services/AliasVault.TaskRunner/Dockerfile
push: true
tags: ${{ env.REGISTRY }}/${{ env.REPO_LOWER }}-task-runner:latest,${{ env.REGISTRY }}/${{ env.REPO_LOWER }}-task-runner:${{ github.ref_name }}
- name: Build and push Reverse Proxy image
uses: docker/build-push-action@v5
with:

View File

@@ -6,9 +6,9 @@
<a href="https://app.aliasvault.net">Live demo 🔥</a> • <a href="https://aliasvault.net?utm_source=gh-readme">Website 🌐</a> • <a href="https://docs.aliasvault.net?utm_source=gh-readme">Documentation 📚</a> • <a href="#installation">Installation ⚙️</a>
</p>
<h3 align="center">
Open-source password and alias manager
</h3>
<p align="center">
<strong>Open-source password and alias manager</strong>
</p>
[<img src="https://img.shields.io/github/v/release/lanedirt/AliasVault?include_prereleases&logo=github">](https://github.com/lanedirt/AliasVault/releases)
[<img src="https://img.shields.io/github/actions/workflow/status/lanedirt/AliasVault/docker-compose-build.yml?label=docker-compose%20build">](https://github.com/lanedirt/AliasVault/actions/workflows/docker-compose-build.yml)
@@ -25,7 +25,7 @@ Open-source password and alias manager
</div>
AliasVault is an open-source password and alias manager built with C# ASP.NET technology. AliasVault can be self-hosted on your own server with Docker, providing a secure and private solution for managing your online identities and passwords.
AliasVault is an end-to-end encrypted password and alias manager that protects your privacy by creating alternative identities, passwords and email addresses for every website you use. The core of AliasVault is built with C# ASP.NET Blazor WASM technology. AliasVault can be self-hosted on your own server with Docker.
### What makes AliasVault unique:
- **Zero-knowledge architecture**: All data is end-to-end encrypted on the client and stored in encrypted state on the server. Your master password never leaves your device and the server never has access to your data.

View File

@@ -65,6 +65,9 @@ services:
task-runner:
image: ghcr.io/lanedirt/aliasvault-task-runner:latest
volumes:
- ./database:/database:rw
- ./logs:/logs:rw
restart: always
env_file:
- .env

View File

@@ -1,5 +1,5 @@
#!/bin/bash
# @version 0.9.0
# @version 0.9.4
# Repository information used for downloading files and images from GitHub
REPO_OWNER="lanedirt"
@@ -253,7 +253,7 @@ handle_docker_compose() {
fi
printf "\n ${CYAN}> docker-compose.yml downloaded successfully.${NC}\n"
else
printf "\n ${YELLOW}> Failed to download docker-compose.yml, please check your internet connection and try again. Alternatively, you can download it manually from ${GITHUB_RAW_URL_REPO}/blob/${version_tag}/docker-compose.yml and place it in the root directory of AliasVault.${NC}\n"
printf "\n ${YELLOW}> Failed to download docker-compose.yml, please check your internet connection and try again. Alternatively, you can download it manually from ${GITHUB_RAW_URL_REPO}/${version_tag}/docker-compose.yml and place it in the root directory of AliasVault.${NC}\n"
exit 1
fi
@@ -262,7 +262,7 @@ handle_docker_compose() {
if curl -sSf "${GITHUB_RAW_URL_REPO}/${version_tag}/docker-compose.letsencrypt.yml" -o "docker-compose.letsencrypt.yml" > /dev/null 2>&1; then
printf "\n ${CYAN}> docker-compose.letsencrypt.yml downloaded successfully.${NC}\n"
else
printf "\n ${YELLOW}> Failed to download docker-compose.letsencrypt.yml, please check your internet connection and try again. Alternatively, you can download it manually from ${GITHUB_RAW_URL_REPO}/blob/${version_tag}/docker-compose.letsencrypt.yml and place it in the root directory of AliasVault.${NC}\n"
printf "\n ${YELLOW}> Failed to download docker-compose.letsencrypt.yml, please check your internet connection and try again. Alternatively, you can download it manually from ${GITHUB_RAW_URL_REPO}/${version_tag}/docker-compose.letsencrypt.yml and place it in the root directory of AliasVault.${NC}\n"
exit 1
fi
@@ -1295,6 +1295,7 @@ handle_install_version() {
"${GITHUB_CONTAINER_REGISTRY}-client:${target_version}"
"${GITHUB_CONTAINER_REGISTRY}-admin:${target_version}"
"${GITHUB_CONTAINER_REGISTRY}-smtp:${target_version}"
"${GITHUB_CONTAINER_REGISTRY}-task-runner:${target_version}"
)
for image in "${images[@]}"; do

View File

@@ -0,0 +1,102 @@
@using AliasVault.RazorComponents.Tables
@using AliasVault.Shared.Models.Enums
@inherits MainBase
<div class="mb-4">
<Paginator CurrentPage="CurrentPage" PageSize="PageSize" TotalRecords="TotalRecords" OnPageChanged="HandlePageChanged" />
<SortableTable Columns="@_tableColumns" SortColumn="@SortColumn" SortDirection="@SortDirection" OnSortChanged="HandleSortChanged">
@foreach (var job in JobList)
{
<SortableTableRow>
<SortableTableColumn IsPrimary="true">@job.Id</SortableTableColumn>
<SortableTableColumn>@job.RunDate.ToString("yyyy-MM-dd")</SortableTableColumn>
<SortableTableColumn>@job.StartTime.ToString("HH:mm")</SortableTableColumn>
<SortableTableColumn>@(job.EndTime?.ToString("HH:mm") ?? "-")</SortableTableColumn>
<SortableTableColumn>
@{
string bgColor = job.Status switch
{
TaskRunnerJobStatus.Pending => "bg-yellow-500",
TaskRunnerJobStatus.Running => "bg-blue-500",
TaskRunnerJobStatus.Finished => "bg-green-500",
TaskRunnerJobStatus.Error => "bg-red-500",
_ => "bg-gray-500"
};
}
<span class="px-2 py-1 rounded-full text-white @bgColor">
@job.Status
</span>
</SortableTableColumn>
<SortableTableColumn>@(job.IsOnDemand ? "Yes" : "No")</SortableTableColumn>
<SortableTableColumn Title="@job.ErrorMessage">
@if (!string.IsNullOrEmpty(job.ErrorMessage))
{
<span class="text-red-600 dark:text-red-400">@(job.ErrorMessage.Length > 50 ? job.ErrorMessage[..50] + "..." : job.ErrorMessage)</span>
}
</SortableTableColumn>
</SortableTableRow>
}
</SortableTable>
</div>
@code {
private readonly List<TableColumn> _tableColumns =
[
new TableColumn { Title = "ID", PropertyName = "Id" },
new TableColumn { Title = "Date", PropertyName = "RunDate" },
new TableColumn { Title = "Start", PropertyName = "StartTime" },
new TableColumn { Title = "End", PropertyName = "EndTime" },
new TableColumn { Title = "Status", PropertyName = "Status" },
new TableColumn { Title = "On-Demand", PropertyName = "IsOnDemand" },
new TableColumn { Title = "Error", PropertyName = "ErrorMessage" },
];
private List<TaskRunnerJob> JobList { get; set; } = [];
private int CurrentPage { get; set; } = 1;
private int PageSize { get; set; } = 5;
private int TotalRecords { get; set; }
private string SortColumn { get; set; } = "Id";
private SortDirection SortDirection { get; set; } = SortDirection.Descending;
/// <summary>
/// Refreshes the data displayed in the table.
/// </summary>
public async Task RefreshData()
{
var dbContext = await DbContextFactory.CreateDbContextAsync();
var query = dbContext.TaskRunnerJobs.AsQueryable();
// Apply sorting
query = SortDirection == SortDirection.Ascending
? query.OrderBy(x => EF.Property<object>(x, SortColumn))
: query.OrderByDescending(x => EF.Property<object>(x, SortColumn));
TotalRecords = await query.CountAsync();
JobList = await query
.Skip((CurrentPage - 1) * PageSize)
.Take(PageSize)
.ToListAsync();
StateHasChanged();
}
/// <inheritdoc/>
protected override async Task OnInitializedAsync()
{
await RefreshData();
}
private async Task HandlePageChanged(int newPage)
{
CurrentPage = newPage;
await RefreshData();
}
private async Task HandleSortChanged((string column, SortDirection direction) sort)
{
SortColumn = sort.column;
SortDirection = sort.direction;
await RefreshData();
}
}

View File

@@ -1,7 +1,10 @@
@page "/settings/server"
@inject ServerSettingsService SettingsService
@inject ILogger<ServerSettingsService> Logger
@using AliasVault.Shared.Models.Enums
@using AliasVault.Shared.Server.Models
@using AliasVault.Shared.Server.Services
@using AliasVault.Admin.Main.Pages.Settings.Components
@inherits MainBase
<LayoutPageTitle>Server settings</LayoutPageTitle>
@@ -11,6 +14,7 @@
Title="Server settings"
Description="Configure AliasVault server settings.">
<CustomActions>
<RefreshButton OnClick="RefreshData" ButtonText="Refresh" />
<ConfirmButton OnClick="SaveSettings">Save changes</ConfirmButton>
</CustomActions>
</PageHeader>
@@ -40,7 +44,9 @@
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Set to 0 for unlimited emails</p>
</div>
</div>
</div>
<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">Maintenance Schedule</h3>
<div class="mb-4">
<label for="schedule" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Time (24h format)</label>
@@ -60,12 +66,23 @@
}
</div>
</div>
<div class="mb-4">
<h4 class="mb-2 text-md font-medium text-gray-900 dark:text-white">Manual Execution</h4>
<ConfirmButton OnClick="RunMaintenanceTasksNow">Run Maintenance Tasks Now</ConfirmButton>
</div>
</div>
<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">Maintenance History</h3>
<TaskRunnerHistory @ref="_taskRunnerHistoryComponent" />
</div>
</div>
@code {
private ServerSettingsModel Settings { get; set; } = new();
private TaskRunnerHistory? _taskRunnerHistoryComponent;
private ServerSettingsModel Settings { get; set; } = new();
private readonly Dictionary<int, string> DaysOfWeek = new()
{
{ 1, "Monday" },
@@ -86,9 +103,13 @@
private void ToggleDay(int day)
{
if (Settings.TaskRunnerDays.Contains(day))
{
Settings.TaskRunnerDays.Remove(day);
}
else
{
Settings.TaskRunnerDays.Add(day);
}
}
private async Task SaveSettings()
@@ -96,4 +117,50 @@
await SettingsService.SaveSettingsAsync(Settings);
GlobalNotificationService.AddSuccessMessage("Settings saved successfully", true);
}
private async Task RunMaintenanceTasksNow()
{
try
{
var dbContext = await DbContextFactory.CreateDbContextAsync();
var job = new TaskRunnerJob
{
Name = nameof(TaskRunnerJobType.Maintenance),
RunDate = DateTime.Now.Date,
StartTime = TimeOnly.FromDateTime(DateTime.Now),
Status = TaskRunnerJobStatus.Pending,
IsOnDemand = true
};
dbContext.TaskRunnerJobs.Add(job);
await dbContext.SaveChangesAsync();
// Refresh the history component to show the new job
if (_taskRunnerHistoryComponent != null)
{
await _taskRunnerHistoryComponent.RefreshData();
}
Logger.LogWarning("Maintenance tasks manually queued.");
GlobalNotificationService.AddSuccessMessage("Maintenance tasks queued. They will be executed on the next polling cycle (default every minute). Check the logs for details.", true);
}
catch (Exception ex)
{
GlobalNotificationService.AddErrorMessage($"Failed to start maintenance tasks: {ex.Message}", true);
}
}
/// <summary>
/// Refreshes the data displayed on the page.
/// </summary>
private async Task RefreshData()
{
Settings = await SettingsService.GetAllSettingsAsync();
// Refresh the history component to show the new job
if (_taskRunnerHistoryComponent != null)
{
await _taskRunnerHistoryComponent.RefreshData();
}
}
}

View File

@@ -710,9 +710,9 @@
}
},
"node_modules/nanoid": {
"version": "3.3.7",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
"integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
"version": "3.3.8",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
"integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
"funding": [
{
"type": "github",

View File

@@ -23,13 +23,13 @@
<PackageReference Include="Asp.Versioning.Mvc" Version="8.1.0" />
<PackageReference Include="Asp.Versioning.Mvc.ApiExplorer" Version="8.1.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.0" />
<PackageReference Include="Microsoft.IdentityModel.JsonWebTokens" Version="8.2.1" />
<PackageReference Include="Microsoft.IdentityModel.Tokens" Version="8.2.1" />
<PackageReference Include="Microsoft.IdentityModel.JsonWebTokens" Version="8.3.0" />
<PackageReference Include="Microsoft.IdentityModel.Tokens" Version="8.3.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Swashbuckle.AspNetCore" Version="7.1.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="7.2.0" />
</ItemGroup>
<ItemGroup>

View File

@@ -1,9 +1,9 @@
<a href="/">
<div class="text-5xl font-bold text-gray-900 dark:text-white mb-4 flex items-center">
<img src="img/logo.svg" alt="AliasVault" class="w-20 h-20 mr-2" />
<span class="relative">
<span class="relative inline-flex flex-wrap items-center">
AliasVault
<span class="absolute -top-2 bg-primary-500 text-white text-xs px-2 py-0.5 rounded-full font-normal">BETA</span>
<span class="ml-2 bg-primary-500 text-white text-xs px-2 py-0.5 rounded-full font-normal sm:absolute sm:-top-2 sm:ml-1">BETA</span>
</span>
</div>
</a>

View File

@@ -8,7 +8,7 @@
<input
id="searchWidget"
type="text"
placeholder="Type here to search"
placeholder="Search for a service..."
autocomplete="off"
class="w-full px-4 py-2 text-gray-700 bg-white border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 dark:bg-gray-800 dark:text-gray-300 dark:border-gray-600 dark:focus:ring-primary-500"
@bind-value="SearchTerm"

View File

@@ -710,9 +710,9 @@
}
},
"node_modules/nanoid": {
"version": "3.3.7",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
"integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
"version": "3.3.8",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
"integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
"funding": [
{
"type": "github",

View File

@@ -53,6 +53,7 @@
<div class="mt-4 text-center">
<p id="security-quote" class="text-sm text-primary-600 italic"></p>
</div>
<div id="error-message" class="hidden text-red-600 dark:text-red-400 mt-4"></div>
</div>
</div>
</div>
@@ -144,7 +145,7 @@
clearInterval(intervalId);
} else if (elapsedTime % 1000 < checkInterval) {
if (!('WebAssembly' in window)) {
showError("AliasVault requires WebAssembly, which this browser does not support. Please use a modern browser that supports WebAssembly.");
showError("AliasVault requires WebAssembly, which this browser does not support. Try using a more modern browser that supports WebAssembly.");
clearInterval(intervalId);
}
}
@@ -157,7 +158,6 @@
const errorMessageElement = document.getElementById('error-message');
const showError = (message) => {
loadingScreen.querySelector('.inner').classList.add('hidden');
errorMessageElement.textContent = message;
errorMessageElement.classList.remove('hidden');
document.querySelector('.loading-progress-text').classList.add('hidden');
@@ -167,14 +167,14 @@
// Listen for unhandled errors
window.addEventListener('error', function(event) {
if (event.error && event.error.message && event.error.message.includes('WebAssembly')) {
showError("AliasVault requires WebAssembly, which this browser does not support. Please use a modern browser that supports WebAssembly.");
showError("AliasVault requires WebAssembly, which this browser does not support. Try using a more modern browser that supports WebAssembly.");
}
});
// Listen for unhandled promise rejections
window.addEventListener('unhandledrejection', function(event) {
if (event.reason && event.reason.message && event.reason.message.includes('WebAssembly')) {
showError("AliasVault requires WebAssembly, which this browser does not support. Please use a modern browser that supports WebAssembly.");
showError("AliasVault requires WebAssembly, which this browser does not support. Try using a more modern browser that supports WebAssembly.");
}
});

View File

@@ -25,6 +25,7 @@ public class AliasServerDbContext : WorkerStatusDbContext, IDataProtectionKeyCon
/// </summary>
public AliasServerDbContext()
{
SetPragmaSettings();
}
/// <summary>
@@ -34,6 +35,7 @@ public class AliasServerDbContext : WorkerStatusDbContext, IDataProtectionKeyCon
public AliasServerDbContext(DbContextOptions<AliasServerDbContext> options)
: base(options)
{
SetPragmaSettings();
}
/// <summary>
@@ -131,6 +133,11 @@ public class AliasServerDbContext : WorkerStatusDbContext, IDataProtectionKeyCon
/// </summary>
public DbSet<ServerSetting> ServerSettings { get; set; } = null!;
/// <summary>
/// Gets or sets the TaskRunnerJobs DbSet.
/// </summary>
public DbSet<TaskRunnerJob> TaskRunnerJobs { get; set; }
/// <summary>
/// The OnModelCreating method.
/// </summary>
@@ -254,13 +261,34 @@ public class AliasServerDbContext : WorkerStatusDbContext, IDataProtectionKeyCon
// Add SQLite connection with enhanced settings
var connectionString = configuration.GetConnectionString("AliasServerDbContext") +
";Mode=ReadWriteCreate;Cache=Shared" +
";Journal Mode=WAL" +
";Synchronous=Normal" +
";Busy Timeout=30000";
";Mode=ReadWriteCreate;Cache=Shared";
optionsBuilder
.UseSqlite(connectionString, options => options.CommandTimeout(60))
.UseLazyLoadingProxies();
}
/// <summary>
/// Sets up the PRAGMA settings for SQLite.
/// </summary>
private void SetPragmaSettings()
{
var connection = Database.GetDbConnection();
if (connection.State != System.Data.ConnectionState.Open)
{
connection.Open();
}
using (var command = connection.CreateCommand())
{
// Increase busy timeout
command.CommandText = @"
PRAGMA busy_timeout = 30000;
PRAGMA journal_mode = WAL;
PRAGMA synchronous = NORMAL;
PRAGMA temp_store = MEMORY;
PRAGMA mmap_size = 1073741824;";
command.ExecuteNonQuery();
}
}
}

View File

@@ -1,4 +1,5 @@
using System;
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable

View File

@@ -0,0 +1,881 @@
// <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("20241215131807_AddTaskRunnerJobTable")]
partial class AddTaskRunnerJobTable
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.0")
.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.ServerSetting", b =>
{
b.Property<string>("Key")
.HasMaxLength(255)
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("TEXT");
b.Property<string>("Value")
.HasColumnType("TEXT");
b.HasKey("Key");
b.ToTable("ServerSettings");
});
modelBuilder.Entity("AliasServerDb.TaskRunnerJob", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<TimeOnly?>("EndTime")
.HasColumnType("TEXT");
b.Property<string>("ErrorMessage")
.HasColumnType("TEXT");
b.Property<bool>("IsOnDemand")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTime>("RunDate")
.HasColumnType("TEXT");
b.Property<TimeOnly>("StartTime")
.HasColumnType("TEXT");
b.Property<int>("Status")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.ToTable("TaskRunnerJobs");
});
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")
.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.Cascade)
.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");
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,42 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace AliasServerDb.Migrations
{
/// <inheritdoc />
public partial class AddTaskRunnerJobTable : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "TaskRunnerJobs",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Name = table.Column<string>(type: "TEXT", nullable: false),
RunDate = table.Column<DateTime>(type: "TEXT", nullable: false),
StartTime = table.Column<TimeOnly>(type: "TEXT", nullable: false),
EndTime = table.Column<TimeOnly>(type: "TEXT", nullable: true),
Status = table.Column<int>(type: "INTEGER", nullable: false),
ErrorMessage = table.Column<string>(type: "TEXT", nullable: true),
IsOnDemand = table.Column<bool>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_TaskRunnerJobs", x => x.Id);
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "TaskRunnerJobs");
}
}
}

View File

@@ -484,6 +484,39 @@ namespace AliasServerDb.Migrations
b.ToTable("ServerSettings");
});
modelBuilder.Entity("AliasServerDb.TaskRunnerJob", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<TimeOnly?>("EndTime")
.HasColumnType("TEXT");
b.Property<string>("ErrorMessage")
.HasColumnType("TEXT");
b.Property<bool>("IsOnDemand")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTime>("RunDate")
.HasColumnType("TEXT");
b.Property<TimeOnly>("StartTime")
.HasColumnType("TEXT");
b.Property<int>("Status")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.ToTable("TaskRunnerJobs");
});
modelBuilder.Entity("AliasServerDb.UserEmailClaim", b =>
{
b.Property<Guid>("Id")

View File

@@ -0,0 +1,62 @@
//-----------------------------------------------------------------------
// <copyright file="TaskRunnerJob.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 AliasServerDb;
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using AliasVault.Shared.Models.Enums;
/// <summary>
/// Represents a task runner job entry in the AliasServerDb.
/// </summary>
public class TaskRunnerJob
{
/// <summary>
/// Gets or sets the ID of the task runner job.
/// </summary>
[Key]
public int Id { get; set; }
/// <summary>
/// Gets or sets the name of the task runner job.
/// </summary>
[Required]
[Column(TypeName = "nvarchar(50)")]
public string Name { get; set; } = null!;
/// <summary>
/// Gets or sets the date the job was run.
/// </summary>
public DateTime RunDate { get; set; }
/// <summary>
/// Gets or sets the start time of the job.
/// </summary>
public TimeOnly StartTime { get; set; }
/// <summary>
/// Gets or sets the end time of the job.
/// </summary>
public TimeOnly? EndTime { get; set; }
/// <summary>
/// Gets or sets the status of the job.
/// </summary>
public TaskRunnerJobStatus Status { get; set; }
/// <summary>
/// Gets or sets the error message of the job.
/// </summary>
public string? ErrorMessage { get; set; }
/// <summary>
/// Gets or sets a value indicating whether this is an on-demand run.
/// </summary>
public bool IsOnDemand { get; set; }
}

View File

@@ -24,7 +24,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="SpamOK.PasswordGenerator" Version="1.0.1" />
<PackageReference Include="SpamOK.PasswordGenerator" Version="1.1.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

View File

@@ -27,8 +27,8 @@
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.0" />
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.21.0" />
<PackageReference Include="MimeKit" Version="4.8.0" />
<PackageReference Include="NUglify" Version="1.21.10" />
<PackageReference Include="MimeKit" Version="4.9.0" />
<PackageReference Include="NUglify" Version="1.21.11" />
<PackageReference Include="SmtpServer" Version="10.0.1" />
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556">
<PrivateAssets>all</PrivateAssets>

View File

@@ -52,6 +52,13 @@ public class LogCleanupTask : IMaintenanceTask
.Where(x => x.TimeStamp < cutoffDate)
.ExecuteDeleteAsync(cancellationToken);
_logger.LogWarning("Deleted {Count} general log entries older than {Days} days", deletedCount, settings.GeneralLogRetentionDays);
// Delete old task runner jobs
var jobCutoffDate = DateTime.UtcNow.AddDays(-settings.GeneralLogRetentionDays);
var deletedJobCount = await dbContext.TaskRunnerJobs
.Where(x => x.RunDate < jobCutoffDate)
.ExecuteDeleteAsync(cancellationToken);
_logger.LogWarning("Deleted {Count} task runner job entries older than {Days} days", deletedJobCount, settings.GeneralLogRetentionDays);
}
if (settings.AuthLogRetentionDays > 0)

View File

@@ -7,8 +7,11 @@
namespace AliasVault.TaskRunner.Workers;
using AliasServerDb;
using AliasVault.Shared.Models.Enums;
using AliasVault.Shared.Server.Services;
using AliasVault.TaskRunner.Tasks;
using Microsoft.EntityFrameworkCore;
/// <summary>
/// A worker for the TaskRunner.
@@ -16,54 +19,115 @@ using AliasVault.TaskRunner.Tasks;
/// <param name="logger">ILogger instance.</param>
/// <param name="tasks">List of maintenance tasks.</param>
/// <param name="settingsService">Server settings service.</param>
public class TaskRunnerWorker(ILogger<TaskRunnerWorker> logger, IEnumerable<IMaintenanceTask> tasks, ServerSettingsService settingsService) : BackgroundService
/// <param name="dbContextFactory">Database context factory.</param>
public class TaskRunnerWorker(
ILogger<TaskRunnerWorker> logger,
IEnumerable<IMaintenanceTask> tasks,
ServerSettingsService settingsService,
IDbContextFactory<AliasServerDbContext> dbContextFactory) : BackgroundService
{
private DateTime _nextRun = DateTime.MinValue;
/// <inheritdoc />
/// <inheritdoc/>
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
logger.LogWarning("TaskRunnerWorker started at: {Time}", DateTimeOffset.Now);
while (!stoppingToken.IsCancellationRequested)
{
await using var dbContext = await dbContextFactory.CreateDbContextAsync(stoppingToken);
var settings = await settingsService.GetAllSettingsAsync();
var now = DateTime.Now;
var today = now.Date;
// Calculate if we should run now or wait
var scheduledTime = settings.MaintenanceTime;
var currentTime = TimeOnly.FromDateTime(now);
var shouldRunToday = settings.TaskRunnerDays.Contains((int)now.DayOfWeek);
var hasPassedScheduledTime = currentTime >= scheduledTime;
// Check for on-demand run request
var onDemandJob = await dbContext.TaskRunnerJobs
.Where(j => j.IsOnDemand && j.Status == TaskRunnerJobStatus.Pending)
.OrderByDescending(j => j.StartTime)
.FirstOrDefaultAsync(stoppingToken);
// Run if:
// 1. We haven't run yet today (nextRun is from previous day)
// 2. It's a scheduled day
// 3. The scheduled time has passed
if (shouldRunToday && hasPassedScheduledTime && now.Date >= _nextRun.Date)
if (onDemandJob != null)
{
logger.LogWarning("Starting maintenance tasks at {Time}", now);
await ExecuteMaintenanceTasks(onDemandJob, dbContext, stoppingToken);
}
else
{
// Regular scheduled run logic
var scheduledTime = settings.MaintenanceTime;
var currentTime = TimeOnly.FromDateTime(now);
var shouldRunToday = settings.TaskRunnerDays.Contains((int)now.DayOfWeek + 1);
var hasPassedScheduledTime = currentTime >= scheduledTime;
foreach (var task in tasks)
if (shouldRunToday && hasPassedScheduledTime)
{
try
var existingJob = await dbContext.TaskRunnerJobs
.Where(j => j.Name == nameof(TaskRunnerJobType.Maintenance) && !j.IsOnDemand && j.RunDate.Date == today)
.OrderByDescending(j => j.StartTime)
.FirstOrDefaultAsync(stoppingToken);
if (existingJob == null)
{
await task.ExecuteAsync(stoppingToken);
}
catch (Exception ex)
{
logger.LogError(ex, "Error executing task {TaskName}", task.Name);
var job = new TaskRunnerJob
{
Name = nameof(TaskRunnerJobType.Maintenance),
RunDate = today,
StartTime = TimeOnly.FromDateTime(now),
Status = TaskRunnerJobStatus.Running,
IsOnDemand = false,
};
dbContext.TaskRunnerJobs.Add(job);
await dbContext.SaveChangesAsync(stoppingToken);
await ExecuteMaintenanceTasks(job, dbContext, stoppingToken);
}
}
// Set next run to tomorrow at the scheduled time
_nextRun = now.Date.AddDays(1);
logger.LogInformation("Tasks completed. Next run scheduled for date: {NextRun}", _nextRun);
}
// Calculate delay until next check
// Check every minute for schedule changes, but not more often than that
// Check every minute for schedule changes or on-demand requests
await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken);
}
}
/// <summary>
/// Executes the maintenance tasks.
/// </summary>
/// <param name="job">The job to execute.</param>
/// <param name="dbContext">The database context.</param>
/// <param name="stoppingToken">The cancellation token.</param>
private async Task ExecuteMaintenanceTasks(TaskRunnerJob job, AliasServerDbContext dbContext, CancellationToken stoppingToken)
{
logger.LogWarning("Starting maintenance tasks at {Time} (On-demand: {IsOnDemand})", DateTime.Now, job.IsOnDemand);
try
{
foreach (var task in tasks)
{
try
{
job.Status = TaskRunnerJobStatus.Running;
await dbContext.SaveChangesAsync(stoppingToken);
await task.ExecuteAsync(stoppingToken);
}
catch (Exception ex)
{
logger.LogError(ex, "Error executing task {TaskName}", task.Name);
job.ErrorMessage = $"Task {task.Name} failed: {ex.Message}";
job.Status = TaskRunnerJobStatus.Error;
await dbContext.SaveChangesAsync(stoppingToken);
break;
}
}
if (job.Status != TaskRunnerJobStatus.Error)
{
job.Status = TaskRunnerJobStatus.Finished;
}
}
finally
{
job.EndTime = TimeOnly.FromDateTime(DateTime.Now);
await dbContext.SaveChangesAsync(stoppingToken);
}
logger.LogInformation("Tasks completed with status: {Status}", job.Status);
}
}

View File

@@ -30,7 +30,7 @@ public static class AppInfo
/// <summary>
/// Gets the patch version number.
/// </summary>
public const int VersionPatch = 0;
public const int VersionPatch = 4;
/// <summary>
/// Gets the build number, typically used in CI/CD pipelines.

View File

@@ -23,7 +23,7 @@ public class ServerSettingsModel
public int AuthLogRetentionDays { get; set; } = 30;
/// <summary>
/// Gets or sets the email retention days. Defaults to 0 (disabled).
/// Gets or sets the email retention days. Defaults to 0 (unlimited).
/// </summary>
public int EmailRetentionDays { get; set; }

View File

@@ -99,19 +99,53 @@ public class ServerSettingsService(IDbContextFactory<AliasServerDbContext> dbCon
await using var dbContext = await dbContextFactory.CreateDbContextAsync(CancellationToken.None);
var settings = await dbContext.ServerSettings.ToDictionaryAsync(x => x.Key, x => x.Value);
return new ServerSettingsModel
// Create model with defaults
var model = new ServerSettingsModel();
// Only override if parsing succeeds
if (int.TryParse(settings.GetValueOrDefault("GeneralLogRetentionDays"), out var generalDays))
{
GeneralLogRetentionDays = int.TryParse(settings.GetValueOrDefault("GeneralLogRetentionDays"), out var generalDays) ? generalDays : 30,
AuthLogRetentionDays = int.TryParse(settings.GetValueOrDefault("AuthLogRetentionDays"), out var authDays) ? authDays : 90,
EmailRetentionDays = int.TryParse(settings.GetValueOrDefault("EmailRetentionDays"), out var emailDays) ? emailDays : 30,
MaxEmailsPerUser = int.TryParse(settings.GetValueOrDefault("MaxEmailsPerUser"), out var maxEmails) ? maxEmails : 100,
MaintenanceTime = TimeOnly.TryParse(
settings.GetValueOrDefault("MaintenanceTime") ?? "00:00",
CultureInfo.InvariantCulture,
DateTimeStyles.None,
out var time) ? time : new TimeOnly(0, 0),
TaskRunnerDays = settings.GetValueOrDefault("TaskRunnerDays")?.Split(',').Select(int.Parse).ToList() ?? new List<int> { 1, 2, 3, 4, 5, 6, 7 },
};
model.GeneralLogRetentionDays = generalDays;
}
if (int.TryParse(settings.GetValueOrDefault("AuthLogRetentionDays"), out var authDays))
{
model.AuthLogRetentionDays = authDays;
}
if (int.TryParse(settings.GetValueOrDefault("EmailRetentionDays"), out var emailDays))
{
model.EmailRetentionDays = emailDays;
}
if (int.TryParse(settings.GetValueOrDefault("MaxEmailsPerUser"), out var maxEmails))
{
model.MaxEmailsPerUser = maxEmails;
}
if (TimeOnly.TryParse(
settings.GetValueOrDefault("MaintenanceTime") ?? "00:00",
CultureInfo.InvariantCulture,
DateTimeStyles.None,
out var time))
{
model.MaintenanceTime = time;
}
var taskRunnerDaysStr = settings.GetValueOrDefault("TaskRunnerDays");
if (!string.IsNullOrEmpty(taskRunnerDaysStr))
{
try
{
model.TaskRunnerDays = taskRunnerDaysStr.Split(',').Select(int.Parse).ToList();
}
catch (FormatException)
{
// Keep default if parsing fails
}
}
return model;
}
/// <summary>

View File

@@ -0,0 +1,34 @@
//-----------------------------------------------------------------------
// <copyright file="TaskRunnerJobStatus.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.Shared.Models.Enums;
/// <summary>
/// The status of a task runner job.
/// </summary>
public enum TaskRunnerJobStatus
{
/// <summary>
/// The job is pending.
/// </summary>
Pending = 0,
/// <summary>
/// The job is running.
/// </summary>
Running = 1,
/// <summary>
/// The job has finished.
/// </summary>
Finished = 2,
/// <summary>
/// The job has failed.
/// </summary>
Error = 9,
}

View File

@@ -0,0 +1,19 @@
//-----------------------------------------------------------------------
// <copyright file="TaskRunnerJobType.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.Shared.Models.Enums;
/// <summary>
/// The type of a task runner job.
/// </summary>
public enum TaskRunnerJobType
{
/// <summary>
/// The job is pending.
/// </summary>
Maintenance,
}

View File

@@ -30,7 +30,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="NUnit" Version="4.2.2" />
<PackageReference Include="NUnit" Version="4.3.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
<PackageReference Include="NUnit.Analyzers" Version="4.4.0">
<PrivateAssets>all</PrivateAssets>

View File

@@ -0,0 +1,77 @@
//-----------------------------------------------------------------------
// <copyright file="BrowserWasmTests.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.E2ETests.Tests.Client.Shard3;
using Microsoft.Extensions.Configuration;
using Microsoft.Playwright;
/// <summary>
/// End-to-end tests for user two-factor authentication.
/// </summary>
[Parallelizable(ParallelScope.Self)]
[Category("ClientTests")]
[TestFixture]
public class BrowserWasmTests : ClientPlaywrightTest
{
/// <summary>
/// Test if setting up two-factor authentication and then logging in works.
/// </summary>
/// <returns>Async task.</returns>
[Test]
public async Task ShowsWarningWhenWebAssemblyNotSupported()
{
// Store current browser context and page.
var originalContext = Context;
var originalPage = Page;
try
{
// Create a new browser context and page with WebAssembly disabled to test the error message.
var configuration = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.AddJsonFile($"appsettings.Development.json", optional: true, reloadOnChange: true)
.AddEnvironmentVariables()
.Build();
bool headless = configuration.GetValue("PlaywrightSettings:Headless", true);
var playwright = await Playwright.CreateAsync();
Browser = await playwright.Chromium.LaunchAsync(new()
{
Args = ["--js-flags=--noexpose-wasm"],
Headless = headless,
});
Context = await Browser.NewContextAsync();
Page = await Context.NewPageAsync();
// Navigate to the app.
await Page.GotoAsync(AppBaseUrl);
// Wait for error message to appear.
var errorMessage = Page.Locator("#error-message");
await errorMessage.WaitForAsync(new LocatorWaitForOptions
{
State = WaitForSelectorState.Visible,
Timeout = 5000,
});
// Verify the error message.
var message = await errorMessage.TextContentAsync();
Assert.That(message, Does.Contain("AliasVault requires WebAssembly"));
}
finally
{
// Clean up the test context and page.
await Page.CloseAsync();
await Context.CloseAsync();
// Restore original context and page for further tests.
Context = originalContext;
Page = originalPage;
}
}
}

View File

@@ -68,11 +68,49 @@ public class AuthTests : ClientPlaywrightTest
}
/// <summary>
/// Test if logging out and logging in works.
/// Test if logging in with different case variations of username works.
/// </summary>
/// <returns>Async task.</returns>
[Test]
[Order(3)]
public async Task CapitalizedUsernameTest()
{
// Logout current user
await Logout();
// Create a new user with capital letters in username
var capitalUsername = "TestUser@Example.com";
await Register(checkForSuccess: true, username: capitalUsername);
await Logout();
// Test Case 1: Try to login with lowercase version of the username
var lowercaseUsername = capitalUsername.ToLower();
await LoginWithUsername(lowercaseUsername);
await VerifySuccessfulLogin();
// Test Case 2: Try to login with exact capitalized username
await Logout();
await LoginWithUsername(capitalUsername);
await VerifySuccessfulLogin();
// Test Case 3: Create new user with lowercase
await Logout();
var lowercaseUser = "testuser2@example.com";
await Register(checkForSuccess: true, username: lowercaseUser);
await Logout();
// Try logging in with uppercase version
var uppercaseVersion = lowercaseUser.ToUpper();
await LoginWithUsername(uppercaseVersion);
await VerifySuccessfulLogin();
}
/// <summary>
/// Test if logging out and logging in works.
/// </summary>
/// <returns>Async task.</returns>
[Test]
[Order(4)]
public async Task LogoutAndLoginRememberMeTest()
{
await Logout();
@@ -101,7 +139,7 @@ public class AuthTests : ClientPlaywrightTest
/// </summary>
/// <returns>Async task.</returns>
[Test]
[Order(4)]
[Order(5)]
public async Task RegisterFormWarningTest()
{
await Logout();
@@ -116,7 +154,7 @@ public class AuthTests : ClientPlaywrightTest
/// </summary>
/// <returns>Async task.</returns>
[Test]
[Order(5)]
[Order(6)]
public async Task PasswordAuthLockoutTest()
{
await Logout();
@@ -152,4 +190,41 @@ public class AuthTests : ClientPlaywrightTest
var pageContent = await Page.TextContentAsync("body");
Assert.That(pageContent, Does.Contain("locked out"), "No account lockout message.");
}
/// <summary>
/// Login with a given username.
/// </summary>
/// <param name="username">The username to login with.</param>
/// <returns>Async task.</returns>
private async Task LoginWithUsername(string username)
{
await NavigateToLogin();
var emailField = await WaitForAndGetElement("input[id='email']");
var passwordField = await WaitForAndGetElement("input[id='password']");
await emailField.FillAsync(username);
await passwordField.FillAsync(TestUserPassword);
var loginButton = await WaitForAndGetElement("button[type='submit']");
await loginButton.ClickAsync();
}
/// <summary>
/// Verify that a login was successful.
/// </summary>
/// <returns>Async task.</returns>
private async Task VerifySuccessfulLogin()
{
// Wait for the index page to load which should show "Credentials" in the top menu.
await WaitForUrlAsync("**", "Credentials");
// Check if the login was successful by verifying content.
var pageContent = await Page.TextContentAsync("body");
Assert.That(pageContent, Does.Contain(WelcomeMessage), "No index content after logging in.");
// Check if login has created an auth log entry.
var authLogEntry = await ApiDbContext.AuthLogs.FirstOrDefaultAsync(x =>
x.EventType == AuthEventType.Login);
Assert.That(authLogEntry, Is.Not.Null, "Auth log entry not found in database after login.");
}
}

View File

@@ -21,9 +21,9 @@
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.2"/>
<PackageReference Include="MailKit" Version="4.8.0" />
<PackageReference Include="MailKit" Version="4.9.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0"/>
<PackageReference Include="NUnit" Version="4.2.2"/>
<PackageReference Include="NUnit" Version="4.3.0"/>
<PackageReference Include="NUnit.Analyzers" Version="4.4.0"/>
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556">

View File

@@ -148,11 +148,7 @@ public class TaskRunnerTests
await SeedData.SeedDatabase(_testHostBuilder.GetDbContext());
// Get current day of week (1-7, Monday = 1, Sunday = 7)
var currentDay = (int)DateTime.Now.DayOfWeek;
if (currentDay == 0)
{
currentDay = 7; // Convert Sunday from 0 to 7
}
var currentDay = (int)DateTime.Now.DayOfWeek + 1;
// Update maintenance settings in database to exclude current day
var dbContext = _testHostBuilder.GetDbContext();

View File

@@ -32,7 +32,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="NUnit" Version="4.2.2" />
<PackageReference Include="NUnit" Version="4.3.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
<PackageReference Include="NUnit.Analyzers" Version="4.4.0">
<PrivateAssets>all</PrivateAssets>

View File

@@ -23,9 +23,9 @@
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Serilog" Version="4.1.0" />
<PackageReference Include="Serilog" Version="4.2.0" />
<PackageReference Include="Serilog.Extensions.Hosting" Version="8.0.0" />
<PackageReference Include="Serilog.Settings.Configuration" Version="8.0.4" />
<PackageReference Include="Serilog.Settings.Configuration" Version="9.0.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
<PackageReference Include="Serilog.Sinks.SQLite" Version="6.0.0" />

View File

@@ -26,7 +26,9 @@ public static class Srp
/// <returns>SrpSignup model.</returns>
public static SrpPasswordChange PasswordChangeAsync(SrpClient client, string salt, string username, string passwordHashString)
{
// Derive a key from the password using Argon2id
// Derive a key from the password using Argon2id.
// Make sure the username is lowercase as the SRP protocol is case sensitive.
username = username.ToLowerInvariant();
// Signup or password change: client generates a salt and verifier.
var privateKey = DerivePrivateKey(salt, username, passwordHashString);
@@ -44,6 +46,9 @@ public static class Srp
/// <returns>Private key as string.</returns>
public static string DerivePrivateKey(string salt, string username, string passwordHashString)
{
// Make sure the username is lowercase as the SRP protocol is case sensitive.
username = username.ToLowerInvariant();
var client = new SrpClient();
return client.DerivePrivateKey(salt, username, passwordHashString);
}
@@ -80,6 +85,9 @@ public static class Srp
/// <returns>session.</returns>
public static SrpSession DeriveSessionClient(string privateKey, string clientSecretEphemeral, string serverEphemeralPublic, string salt, string username)
{
// Make sure the username is lowercase as the SRP protocol is case sensitive.
username = username.ToLowerInvariant();
var client = new SrpClient();
return client.DeriveSession(
clientSecretEphemeral,
@@ -101,6 +109,9 @@ public static class Srp
/// <returns>SrpSession.</returns>
public static SrpSession? DeriveSessionServer(string serverEphemeralSecret, string clientEphemeralPublic, string salt, string username, string verifier, string clientSessionProof)
{
// Make sure the username is lowercase as the SRP protocol is case sensitive.
username = username.ToLowerInvariant();
try
{
var server = new SrpServer();