mirror of
https://github.com/aliasvault/aliasvault.git
synced 2025-12-24 06:39:12 -05:00
Compare commits
36 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a759091755 | ||
|
|
8dc99c09a8 | ||
|
|
b9ec4baf66 | ||
|
|
71ed62cdcb | ||
|
|
2bbad8c75c | ||
|
|
f02b841eea | ||
|
|
f6fc5af8ac | ||
|
|
1d1155bf0e | ||
|
|
2632211af6 | ||
|
|
05cca6998e | ||
|
|
c4a8a20a62 | ||
|
|
f2c6af9ccb | ||
|
|
e94201acda | ||
|
|
9e03473208 | ||
|
|
0c5b2fb1da | ||
|
|
a5c4a7618d | ||
|
|
70220cecbb | ||
|
|
c63faa352f | ||
|
|
7e261a05c9 | ||
|
|
545ec5576e | ||
|
|
73dcbe5860 | ||
|
|
13917444b9 | ||
|
|
119e13a9dd | ||
|
|
7d656e9a9a | ||
|
|
8bd05b5c2e | ||
|
|
1e65f14323 | ||
|
|
2c7543889d | ||
|
|
63c5483208 | ||
|
|
2586d61651 | ||
|
|
c7a32cf0e9 | ||
|
|
46cc6527aa | ||
|
|
ef291bffc1 | ||
|
|
94f6199e27 | ||
|
|
5ababf3bf3 | ||
|
|
b47e735e8f | ||
|
|
de17303085 |
8
.github/workflows/publish-docker-images.yml
vendored
8
.github/workflows/publish-docker-images.yml
vendored
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
6
src/AliasVault.Admin/package-lock.json
generated
6
src/AliasVault.Admin/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
6
src/AliasVault.Client/package-lock.json
generated
6
src/AliasVault.Client/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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.");
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
881
src/Databases/AliasServerDb/Migrations/20241215131807_AddTaskRunnerJobTable.Designer.cs
generated
Normal file
881
src/Databases/AliasServerDb/Migrations/20241215131807_AddTaskRunnerJobTable.Designer.cs
generated
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
|
||||
62
src/Databases/AliasServerDb/TaskRunnerJob.cs
Normal file
62
src/Databases/AliasServerDb/TaskRunnerJob.cs
Normal 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; }
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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; }
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user