diff --git a/.github/workflows/tls-lifecycle.yml b/.github/workflows/tls-lifecycle.yml index 714162a..3f1191b 100644 --- a/.github/workflows/tls-lifecycle.yml +++ b/.github/workflows/tls-lifecycle.yml @@ -3,17 +3,13 @@ name: TLS lifecycle tests on: pull_request: paths: - - "TruthGate-Web/TruthGate-Web/Configuration/**" - - "TruthGate-Web/TruthGate-Web/Services/ConfigWatchAndIssueService.cs" - - "TruthGate-Web/TruthGate-Web.Tests/**" + - "TruthGate-Web/**" - ".github/workflows/tls-lifecycle.yml" push: branches: - master paths: - - "TruthGate-Web/TruthGate-Web/Configuration/**" - - "TruthGate-Web/TruthGate-Web/Services/ConfigWatchAndIssueService.cs" - - "TruthGate-Web/TruthGate-Web.Tests/**" + - "TruthGate-Web/**" - ".github/workflows/tls-lifecycle.yml" jobs: @@ -25,37 +21,22 @@ jobs: - uses: actions/setup-dotnet@v4 with: - dotnet-version: "9.0.x" + dotnet-version: "10.0.x" - - name: Restore + - name: Restore solution + run: dotnet restore TruthGate-IPFS.sln + + # This test project is intentionally outside TruthGate-IPFS.sln, so restore it explicitly. + - name: Restore TLS tests run: dotnet restore TruthGate-Web/TruthGate-Web.Tests/TruthGate-Web.Tests.csproj - - name: Test - id: test - shell: bash - run: | - set +e - dotnet test \ - TruthGate-Web/TruthGate-Web.Tests/TruthGate-Web.Tests.csproj \ - --no-restore \ - --configuration Release \ - --logger "console;verbosity=normal" \ - > tls-test-output.log 2>&1 - status=$? - echo "status=$status" >> "$GITHUB_OUTPUT" - exit 0 - - - name: Upload test diagnostics - if: always() - uses: actions/upload-artifact@v4 - with: - name: tls-test-output - path: tls-test-output.log - if-no-files-found: error - - - name: Report failure - if: steps.test.outputs.status != '0' - shell: bash - run: | - echo "TLS lifecycle tests failed. Download the tls-test-output artifact for the complete compiler/test diagnostics." - exit 1 + - name: Build solution + run: dotnet build TruthGate-IPFS.sln --no-restore --configuration Release + + - name: Test TLS lifecycle + run: >- + dotnet test + TruthGate-Web/TruthGate-Web.Tests/TruthGate-Web.Tests.csproj + --no-restore + --configuration Release + --logger "console;verbosity=normal" diff --git a/Publish.CLI/Publish.CLI.csproj b/Publish.CLI/Publish.CLI.csproj index fd4bd08..ed9781c 100644 --- a/Publish.CLI/Publish.CLI.csproj +++ b/Publish.CLI/Publish.CLI.csproj @@ -2,7 +2,7 @@ Exe - net9.0 + net10.0 enable enable diff --git a/Test.TruthGate/Test.TruthGate.csproj b/Test.TruthGate/Test.TruthGate.csproj index 63bf931..a396ab3 100644 --- a/Test.TruthGate/Test.TruthGate.csproj +++ b/Test.TruthGate/Test.TruthGate.csproj @@ -1,23 +1,29 @@  - net9.0 + net10.0 enable enable false - - - - + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + diff --git a/TruthGate-Web/TruthGate-Web.Client/Cache.cs b/TruthGate-Web/TruthGate-Web.Client/Cache.cs index 0cedb0f..9ab4425 100644 --- a/TruthGate-Web/TruthGate-Web.Client/Cache.cs +++ b/TruthGate-Web/TruthGate-Web.Client/Cache.cs @@ -6,10 +6,10 @@ public static class Cache { public static MudTheme CustomTheme = new() { - PaletteLight = new() + PaletteLight = new PaletteLight { - Primary = "#2D2A5A", // Deep Indigo - Secondary = "#00E3C0", // Electric Mint + Primary = "#2D2A5A", + Secondary = "#00E3C0", Black = "#110e2d", AppbarText = "#424242", AppbarBackground = "rgba(255,255,255,0.8)", @@ -17,22 +17,24 @@ public static class Cache GrayLight = "#e8e8e8", GrayLighter = "#f9f9f9", }, - PaletteDark = new() + + PaletteDark = new PaletteDark { - Primary = "#00E3C0", // Electric Mint now the star + Primary = "#00E3C0", PrimaryContrastText = "#000000", - Secondary = "#3C3970", // Softer indigo accent (less oppressive than deep indigo) - Background = "#1E1F22", // Discord-ish near-black with a whisper of warmth - Surface = "#2B2D31", // Elevated panels (matches Discord card tone) - DrawerBackground = "#232428", // Sidebar tone - AppbarBackground = "#1B1C1F", // Slightly darker than background for separation - AppbarText = "#E0E0E0", // Soft but high-contrast - TextPrimary = "#FFFFFF", // True white for legibility - TextSecondary = "#A3A6AA", // Muted gray - ActionDefault = "#00E3C0", // Same as primary for cohesion - Divider = "#383A40" // Subtle panel separators + Secondary = "#3C3970", + Background = "#1E1F22", + Surface = "#2B2D31", + DrawerBackground = "#232428", + AppbarBackground = "#1B1C1F", + AppbarText = "#E0E0E0", + TextPrimary = "#FFFFFF", + TextSecondary = "#A3A6AA", + ActionDefault = "#00E3C0", + Divider = "#383A40" }, + LayoutProperties = new LayoutProperties() }; } -} +} \ No newline at end of file diff --git a/TruthGate-Web/TruthGate-Web.Client/TruthGate-Web.Client.csproj b/TruthGate-Web/TruthGate-Web.Client/TruthGate-Web.Client.csproj index 32d92b4..01abcaf 100644 --- a/TruthGate-Web/TruthGate-Web.Client/TruthGate-Web.Client.csproj +++ b/TruthGate-Web/TruthGate-Web.Client/TruthGate-Web.Client.csproj @@ -1,7 +1,7 @@  - net9.0 + net10.0 enable enable true @@ -12,8 +12,8 @@ - - + + diff --git a/TruthGate-Web/TruthGate-Web.Tests/TruthGate-Web.Tests.csproj b/TruthGate-Web/TruthGate-Web.Tests/TruthGate-Web.Tests.csproj index 69e4f2d..4b520df 100644 --- a/TruthGate-Web/TruthGate-Web.Tests/TruthGate-Web.Tests.csproj +++ b/TruthGate-Web/TruthGate-Web.Tests/TruthGate-Web.Tests.csproj @@ -1,7 +1,7 @@ - net9.0 + net10.0 enable enable false @@ -9,9 +9,9 @@ - - - + + + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/TruthGate-Web/TruthGate-Web/Components/Pages/Pinned.razor b/TruthGate-Web/TruthGate-Web/Components/Pages/Pinned.razor index 2e8862b..c57db2a 100644 --- a/TruthGate-Web/TruthGate-Web/Components/Pages/Pinned.razor +++ b/TruthGate-Web/TruthGate-Web/Components/Pages/Pinned.razor @@ -282,7 +282,7 @@ MaxWidth = MaxWidth.Medium }; var parameters = new DialogParameters(); - var dialog = Dialogs.Show("Add Pinned Item", parameters, options); + var dialog = await Dialogs.ShowAsync("Add Pinned Item", parameters, options); var result = await dialog.Result; if (result.Canceled || result.Data is not AddPinnedItemDialog.AddPinnedResult r) return; @@ -331,7 +331,7 @@ { { x => x.Message, $"Unpin and delete '{(row.Name ?? row.Cid)}'?" } }; - var dialog = Dialogs.Show("Confirm Delete", parameters); + var dialog = await Dialogs.ShowAsync("Confirm Delete", parameters); var result = await dialog.Result; if (result.Canceled || result.Data is not bool ok || !ok) return; diff --git a/TruthGate-Web/TruthGate-Web/Components/Pages/PinnedIpns.razor b/TruthGate-Web/TruthGate-Web/Components/Pages/PinnedIpns.razor index 1e594cd..f7f1034 100644 --- a/TruthGate-Web/TruthGate-Web/Components/Pages/PinnedIpns.razor +++ b/TruthGate-Web/TruthGate-Web/Components/Pages/PinnedIpns.razor @@ -283,7 +283,7 @@ { x => x.ManagedRoot, ManagedRoot }, { x => x.IsEdit, false } }; - var dlg = Dialogs.Show("Add IPNS", parameters, + var dlg = await Dialogs.ShowAsync("Add IPNS", parameters, new DialogOptions { FullWidth = true, MaxWidth = MaxWidth.Medium, CloseButton = true }); var res = await dlg.Result; if (res.Canceled || res.Data is not AddOrEditIpnsDialog.IpnsResult r) return; @@ -317,7 +317,7 @@ { x => x.InitialKeepOldCidPinned, row.KeepOld } }; - var dlg = Dialogs.Show($"Edit IPNS - {row.Name}", parameters, + var dlg = await Dialogs.ShowAsync($"Edit IPNS - {row.Name}", parameters, new DialogOptions { FullWidth = true, MaxWidth = MaxWidth.Medium, CloseButton = true }); var res = await dlg.Result; if (res.Canceled || res.Data is not AddOrEditIpnsDialog.IpnsResult r) return; @@ -348,7 +348,7 @@ { { x => x.Message, $"Remove IPNS '{row.Name}' from tracking? (Pinned versions stay.)" } }; - var dlg = Dialogs.Show("Confirm Remove", confirmParams); + var dlg = await Dialogs.ShowAsync("Confirm Remove", confirmParams); var res = await dlg.Result; if (res.Canceled || res.Data is not bool ok || !ok) return; diff --git a/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/ApiKeys.razor b/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/ApiKeys.razor index 2aa622a..5d7aaf9 100644 --- a/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/ApiKeys.razor +++ b/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/ApiKeys.razor @@ -1,4 +1,4 @@ -@page "/keys" +@page "/keys" @using TruthGate_Web.Components.Pages.Settings.Shared @using TruthGate_Web.Models @using TruthGate_Web.Utils @@ -71,10 +71,10 @@ { x => x.ExistingNames, _keys.Select(k => k.Name).ToList() } }; - var dialog = Dialogs.Show("Add API Key", p, new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.Small, FullWidth = true }); + var dialog = await Dialogs.ShowAsync("Add API Key", p, new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.Small, FullWidth = true }); var result = await dialog.Result; - if (result.Canceled || result.Data is not AddApiKeyDialog.AddApiKeyResult r) return; + if (result is null || result.Canceled || result.Data is not AddApiKeyDialog.AddApiKeyResult r) return; // Final uniqueness check (server-side) if (_keys.Any(k => string.Equals(k.Name, r.Name, StringComparison.OrdinalIgnoreCase))) @@ -103,10 +103,10 @@ { x => x.Message, $"Delete API key '{key.Name}'?" } }; - var dialog = Dialogs.Show("Confirm Delete", p); + var dialog = await Dialogs.ShowAsync("Confirm Delete", p); var result = await dialog.Result; - if (result.Canceled || result.Data is not bool yes || !yes) return; + if (result is null || result.Canceled || result.Data is not bool yes || !yes) return; await ConfigSvc.UpdateAsync(cfg => { diff --git a/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/Domains.razor b/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/Domains.razor index 8e0e32a..ffe2c93 100644 --- a/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/Domains.razor +++ b/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/Domains.razor @@ -1,4 +1,4 @@ -@page "/domains" +@page "/domains" @using System.Globalization @using System.Text.RegularExpressions @using MudBlazor @@ -47,7 +47,7 @@ @context.Domain @if (!string.IsNullOrWhiteSpace(context.RedirectUrl)) { - + @{ var u = context.RedirectUrl ?? string.Empty; } @@ -60,7 +60,7 @@ - + @($"{GetIpnsHost(context, useShort: true)}") @@ -88,7 +88,7 @@ @context.IpnsKeyName *@ - + IPNS: @Short(context.IpnsPeerId) @@ -168,7 +168,7 @@ private async Task PublishDomain(EdgeDomain d) { var parms = new DialogParameters { { x => x.Domain, d.Domain } }; - var dlg = Dialogs.Show($"Publish: {d.Domain}", parms, + var dlg = await Dialogs.ShowAsync($"Publish: {d.Domain}", parms, new DialogOptions { FullWidth = true, MaxWidth = MaxWidth.Large, CloseButton = true }); await dlg.Result; @@ -225,7 +225,7 @@ private async Task ImportDomain() { var parms = new DialogParameters(); - var dlg = Dialogs.Show("Import Domain Backup", parms, + var dlg = await Dialogs.ShowAsync("Import Domain Backup", parms, new DialogOptions { FullWidth = true, MaxWidth = MaxWidth.Medium, CloseButton = true }); await dlg.Result; } @@ -233,7 +233,7 @@ private async Task PromptAsync(string title, string message) { var p = new DialogParameters { { x => x.Message, message } }; - var dlg = Dialogs.Show(title, p); + var dlg = await Dialogs.ShowAsync(title, p); var res = await dlg.Result; return res.Canceled ? null : (res.Data as string); } @@ -271,7 +271,7 @@ => string.IsNullOrWhiteSpace(s) || s.Length <= head + tail + 1 ? s : $"{s[..head]}…{s[^tail..]}"; private async Task AddDomain() { - var dialog = Dialogs.Show("Add Domain"); + var dialog = await Dialogs.ShowAsync("Add Domain"); var result = await dialog.Result; if (result.Canceled || result.Data is not AddOrEditDomainDialog.DomainResult r) return; @@ -316,7 +316,7 @@ { x => x.IsEdit, true } }; - var dialog = Dialogs.Show($"Edit Domain: {domain.Domain}", parameters); + var dialog = await Dialogs.ShowAsync($"Edit Domain: {domain.Domain}", parameters); var result = await dialog.Result; if (result.Canceled || result.Data is not AddOrEditDomainDialog.DomainResult r) return; @@ -359,7 +359,7 @@ { x => x.Message, $"Delete domain '{domain.Domain}'?\n\nThis will remove its config entry, IPNS key, pins, and MFS folders." } }; - var dialog = Dialogs.Show("Confirm Delete", parameters); + var dialog = await Dialogs.ShowAsync("Confirm Delete", parameters); var result = await dialog.Result; if (result.Canceled || result.Data is not bool confirmed || !confirmed) return; diff --git a/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/IpBansPage.razor b/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/IpBansPage.razor index 8e1235a..acb3adc 100644 --- a/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/IpBansPage.razor +++ b/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/IpBansPage.razor @@ -1,4 +1,4 @@ -@page "/ip-bans" +@page "/ip-bans" @using Microsoft.EntityFrameworkCore @using System.Net @using TruthGate_Web.Components.Pages.Settings.Shared @@ -52,7 +52,7 @@ Hover="true" Dense="true" Bordered="true" - Sortable="false" + SortMode="SortMode.None" Filterable="false" RowsPerPage="20"> @@ -118,7 +118,7 @@ Hover="true" Dense="true" Bordered="true" - Sortable="false" + SortMode="SortMode.None" Filterable="false" RowsPerPage="20"> @@ -164,7 +164,7 @@ Hover="true" Dense="true" Bordered="true" - Sortable="false" + SortMode="SortMode.None" Filterable="false" RowsPerPage="20"> @@ -212,7 +212,7 @@ } private BanFilters _banFilters = new(); - private async Task> LoadBansGridAsync(GridState state) + private async Task> LoadBansGridAsync(GridState state, CancellationToken cancellationToken) { var page = state.Page + 1; // Mud uses 0-based; your API is 1-based var pageSize = state.PageSize; @@ -237,7 +237,7 @@ private async Task ConfirmUnbanById(Guid id) { - var res = await Dialogs.ShowMessageBox("Confirm", "Unban this entry?", yesText: "Unban", cancelText: "Cancel"); + var res = await Dialogs.ShowMessageBoxAsync("Confirm", "Unban this entry?", yesText: "Unban", cancelText: "Cancel"); if (res == true) { var ok = await RateSvc.UnbanByIdAsync(id); @@ -248,7 +248,7 @@ private async Task ConfirmUnbanIp(string ip) { - var res = await Dialogs.ShowMessageBox("Confirm", $"Unban IP {ip} ?", yesText: "Unban", cancelText: "Cancel"); + var res = await Dialogs.ShowMessageBoxAsync("Confirm", $"Unban IP {ip} ?", yesText: "Unban", cancelText: "Cancel"); if (res == true) { var ok = await RateSvc.UnbanIpAsync(ip); @@ -259,7 +259,7 @@ private async Task ConfirmUnbanPrefix(string pfx) { - var res = await Dialogs.ShowMessageBox("Confirm", $"Unban prefix {pfx} ?", yesText: "Unban", cancelText: "Cancel"); + var res = await Dialogs.ShowMessageBoxAsync("Confirm", $"Unban prefix {pfx} ?", yesText: "Unban", cancelText: "Cancel"); if (res == true) { var ok = await RateSvc.UnbanIpv6PrefixAsync(pfx); @@ -272,13 +272,15 @@ private async Task OpenSearchDialog() { var parameters = new DialogParameters { { x => x.InitialIp, _banFilters.IpFilter } }; - await Dialogs.Show("Search IP", parameters, new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.Small }).Result; + var dialog = await Dialogs.ShowAsync("Search IP", parameters, new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.Small }); + await dialog.Result; } // -------------------- ADD BAN DIALOG -------------------- private async Task OpenAddBanDialog() { - await Dialogs.Show("Add Ban", new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.Small }).Result; + var dialog = await Dialogs.ShowAsync("Add Ban", new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.Small }); + await dialog.Result; await ReloadBans(); } @@ -289,9 +291,9 @@ private string? _newWhitelistReason; private int? _newWhitelistExpireHours; - private async Task> LoadWhitelistIpGridAsync(GridState state) + private async Task> LoadWhitelistIpGridAsync(GridState state, CancellationToken cancellationToken) { - await using var db = await DbFactory.CreateDbContextAsync(); + await using var db = await DbFactory.CreateDbContextAsync(cancellationToken); var q = db.Whitelists.AsNoTracking().Where(x => x.Ip != null); if (!string.IsNullOrWhiteSpace(_wlIpSearch)) @@ -300,12 +302,12 @@ q = q.Where(x => x.Ip!.Contains(s)); } - var total = await q.CountAsync(); + var total = await q.CountAsync(cancellationToken); var items = await q .OrderByDescending(x => x.CreatedUtc) .Skip(state.Page * state.PageSize) .Take(state.PageSize) - .ToListAsync(); + .ToListAsync(cancellationToken); return new GridData { Items = items, TotalItems = total }; } @@ -331,7 +333,7 @@ private async Task RemoveWhitelistIp(string ip) { - var res = await Dialogs.ShowMessageBox("Confirm", $"Remove {ip} from whitelist?", yesText: "Remove", cancelText: "Cancel"); + var res = await Dialogs.ShowMessageBoxAsync("Confirm", $"Remove {ip} from whitelist?", yesText: "Remove", cancelText: "Cancel"); if (res == true) { var ok = await RateSvc.RemoveWhitelistIpAsync(ip); @@ -347,9 +349,9 @@ private string? _newWhitelistReasonPfx; private int? _newWhitelistExpireHoursPfx; - private async Task> LoadWhitelistPrefixGridAsync(GridState state) + private async Task> LoadWhitelistPrefixGridAsync(GridState state, CancellationToken cancellationToken) { - await using var db = await DbFactory.CreateDbContextAsync(); + await using var db = await DbFactory.CreateDbContextAsync(cancellationToken); var q = db.Whitelists.AsNoTracking().Where(x => x.Ipv6Prefix != null); if (!string.IsNullOrWhiteSpace(_wlPfxSearch)) @@ -358,12 +360,12 @@ q = q.Where(x => x.Ipv6Prefix!.Contains(s)); } - var total = await q.CountAsync(); + var total = await q.CountAsync(cancellationToken); var items = await q .OrderByDescending(x => x.CreatedUtc) .Skip(state.Page * state.PageSize) .Take(state.PageSize) - .ToListAsync(); + .ToListAsync(cancellationToken); return new GridData { Items = items, TotalItems = total }; } @@ -389,7 +391,7 @@ private async Task RemoveWhitelistPrefix(string pfx) { - var res = await Dialogs.ShowMessageBox("Confirm", $"Remove {pfx} from whitelist?", yesText: "Remove", cancelText: "Cancel"); + var res = await Dialogs.ShowMessageBoxAsync("Confirm", $"Remove {pfx} from whitelist?", yesText: "Remove", cancelText: "Cancel"); if (res == true) { var ok = await RateSvc.RemoveWhitelistIpv6PrefixAsync(pfx); diff --git a/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/Shared/AddBanDialog.razor b/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/Shared/AddBanDialog.razor index 45e4ca4..55d30b2 100644 --- a/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/Shared/AddBanDialog.razor +++ b/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/Shared/AddBanDialog.razor @@ -10,7 +10,7 @@ @Message } - + @@ -19,6 +19,7 @@ Placeholder="@(_usePrefix ? "2001:db8::/64" : "203.0.113.42 or ::ffff:203.0.113.42")" Immediate="true" Required="true" + Validation="@(new Func(ValidateTarget))" @onkeydown="OnKeyDown" /> @@ -30,7 +31,13 @@ - + Validate() + private string? ValidateTarget(string? value) { _error = null; - if (string.IsNullOrWhiteSpace(_target)) - { - _error = "Please provide a target."; - yield return _error!; - yield break; - } - - var t = _target.Trim(); + if (string.IsNullOrWhiteSpace(value)) + return _error = "Please provide a target."; + var target = value.Trim(); if (_usePrefix) { - // Accept "2001:db8::" or "2001:db8::/64" — the service canonicalizes either - var bare = t.Contains('/') ? t[..t.IndexOf('/')] : t; - if (!IPAddress.TryParse(bare, out var parsed) || parsed.AddressFamily != System.Net.Sockets.AddressFamily.InterNetworkV6) + // Accept "2001:db8::" or "2001:db8::/64" — the service canonicalizes either. + var slash = target.IndexOf('/'); + var bare = slash >= 0 ? target[..slash] : target; + if (!IPAddress.TryParse(bare, out var parsed) || + parsed.AddressFamily != System.Net.Sockets.AddressFamily.InterNetworkV6) { - _error = "Invalid IPv6 prefix format."; - yield return _error!; + return _error = "Invalid IPv6 prefix format."; } } - else + else if (!IPAddress.TryParse(target, out _)) { - if (!IPAddress.TryParse(t, out _)) - { - _error = "Invalid IP address."; - yield return _error!; - } + return _error = "Invalid IP address."; } - if (_durationMinutes <= 0) - { - _error = "Duration must be at least 1 minute."; - yield return _error!; - } + return null; + } + + private string? ValidateDuration(int value) + { + if (value <= 0) + return _error = "Duration must be at least 1 minute."; + + return null; } private async Task Submit() { - await _form.Validate(); + await _form.ValidateAsync(); if (!_form.IsValid) return; try { - var t = _target!.Trim(); - var dur = TimeSpan.FromMinutes(_durationMinutes); - var reason = string.IsNullOrWhiteSpace(_reason) ? (_usePrefix ? "MANUAL_PREFIX_BAN" : "MANUAL_BAN") : _reason; - - bool ok = _usePrefix - ? await RateSvc.BanIpv6PrefixAsync(t, dur, _scope, _trueBan, reason) - : await RateSvc.BanIpAsync(t, dur, _scope, _trueBan, reason); + var target = _target!.Trim(); + var duration = TimeSpan.FromMinutes(_durationMinutes); + var reason = string.IsNullOrWhiteSpace(_reason) + ? (_usePrefix ? "MANUAL_PREFIX_BAN" : "MANUAL_BAN") + : _reason; + + var ok = _usePrefix + ? await RateSvc.BanIpv6PrefixAsync(target, duration, _scope, _trueBan, reason) + : await RateSvc.BanIpAsync(target, duration, _scope, _trueBan, reason); if (ok) { diff --git a/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/Shared/AddOrEditDomainDialog.razor b/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/Shared/AddOrEditDomainDialog.razor index ceeb101..d57b477 100644 --- a/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/Shared/AddOrEditDomainDialog.razor +++ b/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/Shared/AddOrEditDomainDialog.razor @@ -3,20 +3,20 @@ - + + @bind-Value="_model.Domain" + Immediate="true" + Required="true" + RequiredError="Domain is required." + For="@(() => _model.Domain)" />
+ @bind-Value="_model.UseSSL" + Color="Color.Primary" + Label="@(_model.UseSSL ? "Enabled" : "Disabled")" />
When enabled, TruthGate provisions a valid Let’s Encrypt certificate for this domain. @@ -25,16 +25,15 @@
+ @bind-Value="_model.RedirectUrl" + Immediate="true" + Required="false" + For="@(() => _model.RedirectUrl)" /> Leave empty to serve this domain normally. If a Redirect URL is set, all visitors will be instantly redirected there instead. - @if (!string.IsNullOrEmpty(_error)) { @_error @@ -85,14 +84,6 @@ public string? RedirectUrl { get; set; } } - private IEnumerable Validate() - { - _error = null; - - if (string.IsNullOrWhiteSpace(_model.Domain)) - yield return "Domain is required."; - } - private void Cancel() => MudDialog.Cancel(); [Inject] IHttpClientFactory HttpFactory { get; set; } = default!; @@ -100,38 +91,46 @@ private static string ToSafeFolderLeaf(string input) { - var s = (input ?? "").Trim().ToLowerInvariant(); - // kill pathy stuff — you only want a single folder leaf here - s = s.Replace('\\', '/').Trim('/'); - if (s.Contains("..")) throw new InvalidOperationException("Invalid domain."); - if (string.IsNullOrWhiteSpace(s)) throw new InvalidOperationException("Empty domain."); - return s; + var value = (input ?? "").Trim().ToLowerInvariant(); + // Kill path-like input — this must remain a single folder leaf. + value = value.Replace('\\', '/').Trim('/'); + if (value.Contains("..")) throw new InvalidOperationException("Invalid domain."); + if (string.IsNullOrWhiteSpace(value)) throw new InvalidOperationException("Empty domain."); + return value; } private async Task Submit() { - await _form.Validate(); + _error = null; + await _form.ValidateAsync(); if (!_form.IsValid) return; - var domainLeaf = ToSafeFolderLeaf(_model.Domain); - var basePath = "/production/sites"; - var fullPath = $"{basePath}/{domainLeaf}"; + try + { + var domainLeaf = ToSafeFolderLeaf(_model.Domain); + const string basePath = "/production/sites"; + var fullPath = $"{basePath}/{domainLeaf}"; - // Ensure the folder path exists (creates parents as needed), returns final CID - var cid = await IpfsGateway.EnsureMfsFolderExistsAsync(fullPath, HttpFactory); + // Ensure the folder path exists (creates parents as needed), returning the final CID. + _ = await IpfsGateway.EnsureMfsFolderExistsAsync(fullPath, HttpFactory); - // (Optional) warm/refresh your cached CID for this MFS path, so the rest of your app sees it: - var ttl = TimeSpan.FromHours(2); - _ = await IpfsGateway.GetCidForMfsPathAsync( + // Warm/refresh the cached CID so the rest of the app sees the newly created path. + var ttl = TimeSpan.FromHours(2); + _ = await IpfsGateway.GetCidForMfsPathAsync( fullPath, HttpFactory, Cache, ttl, IpfsGateway.CacheMode.Refresh); - var result = new DomainResult + var result = new DomainResult + { + Domain = _model.Domain, // Normalization happens on the page. + UseSSL = _model.UseSSL, + RedirectUrl = _model.RedirectUrl + }; + + MudDialog.Close(DialogResult.Ok(result)); + } + catch (Exception ex) { - Domain = _model.Domain, // normalization happens on the page - UseSSL = _model.UseSSL, - RedirectUrl = _model.RedirectUrl - }; - - MudDialog.Close(DialogResult.Ok(result)); + _error = ex.Message; + } } } diff --git a/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/Shared/AddUserDialog.razor b/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/Shared/AddUserDialog.razor index 53e1c30..8e2691c 100644 --- a/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/Shared/AddUserDialog.razor +++ b/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/Shared/AddUserDialog.razor @@ -2,10 +2,24 @@ - - - - + + + + @if (!string.IsNullOrEmpty(_error)) { @_error @@ -29,7 +43,7 @@ } private MudForm _form = default!; - private Model _model = new(); + private readonly Model _model = new(); private string? _error; private class Model @@ -39,34 +53,33 @@ public string Confirm { get; set; } = ""; } - private IEnumerable Validate() + private string? ValidateConfirmation(string? value) { _error = null; - if (string.IsNullOrWhiteSpace(_model.UserName)) - yield return "Username is required."; + if (string.IsNullOrEmpty(value)) + return _error = "Please confirm the password."; - if (string.IsNullOrEmpty(_model.Password)) - yield return "Password is required."; + if (_model.Password != value) + return _error = "Passwords must match."; - if (_model.Password != _model.Confirm) - yield return "Passwords must match."; + return null; } private void Cancel() => MudDialog.Cancel(); private async Task Submit() { - await _form.Validate(); + await _form.ValidateAsync(); if (!_form.IsValid) return; - // normalize username to lowercase (your Config also normalizes) + // Normalize username to lowercase (Config also normalizes it). var result = new AddUserResult - { - UserName = _model.UserName.Trim().ToLowerInvariant(), - Password = _model.Password - }; + { + UserName = _model.UserName.Trim().ToLowerInvariant(), + Password = _model.Password + }; MudDialog.Close(DialogResult.Ok(result)); } -} \ No newline at end of file +} diff --git a/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/Shared/ChangePasswordDialog.razor b/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/Shared/ChangePasswordDialog.razor index 07ba96d..06383f3 100644 --- a/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/Shared/ChangePasswordDialog.razor +++ b/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/Shared/ChangePasswordDialog.razor @@ -2,10 +2,20 @@ - + User: @TargetUserName - - + + @if (!string.IsNullOrEmpty(_error)) { @_error @@ -29,7 +39,7 @@ } private MudForm _form = default!; - private Model _model = new(); + private readonly Model _model = new(); private string? _error; private class Model @@ -38,25 +48,27 @@ public string Confirm { get; set; } = ""; } - private IEnumerable Validate() + private string? ValidateConfirmation(string? value) { _error = null; - if (string.IsNullOrEmpty(_model.Password)) - yield return "Password is required."; + if (string.IsNullOrEmpty(value)) + return _error = "Please confirm the password."; - if (_model.Password != _model.Confirm) - yield return "Passwords must match."; + if (_model.Password != value) + return _error = "Passwords must match."; + + return null; } private void Cancel() => MudDialog.Cancel(); private async Task Submit() { - await _form.Validate(); + await _form.ValidateAsync(); if (!_form.IsValid) return; var result = new ChangePasswordResult { NewPassword = _model.Password }; MudDialog.Close(DialogResult.Ok(result)); } -} \ No newline at end of file +} diff --git a/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/Shared/ImportDomainDialog.razor b/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/Shared/ImportDomainDialog.razor index dfd94d7..3a6e7da 100644 --- a/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/Shared/ImportDomainDialog.razor +++ b/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/Shared/ImportDomainDialog.razor @@ -14,15 +14,17 @@ Immediate="true" Required="true" /> -
- - +
+ + + StartIcon="@Icons.Material.Filled.VpnKey" + OnClick="@context.OpenFilePickerAsync"> Select Backup (.tgbackup) -
+ +
@@ -43,8 +45,8 @@ private IBrowserFile? _file; private string? _pass; - private void OnFileChanged(IReadOnlyList picked) - => _file = picked.FirstOrDefault(); + private void OnFileChanged(IBrowserFile picked) + => _file = picked; private async Task Submit() { diff --git a/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/Shared/SearchIpDialog.razor b/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/Shared/SearchIpDialog.razor index 997c8b8..142514b 100644 --- a/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/Shared/SearchIpDialog.razor +++ b/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/Shared/SearchIpDialog.razor @@ -1,4 +1,4 @@ -@using System.Net +@using System.Net @using Microsoft.EntityFrameworkCore @using MudBlazor @using TruthGate_Web.Security @@ -11,7 +11,7 @@ @Message } - + Recent ban records (DB, latest 10)
- + @context.Item.CreatedUtc.ToString("u") @@ -106,25 +106,21 @@ _ip = InitialIp?.Trim() ?? ""; } - private IEnumerable Validate() + private string? ValidateIp(string? value) { _error = null; - if (string.IsNullOrWhiteSpace(_ip)) - { - _error = "Please enter an IP address."; - yield return _error; - yield break; - } - if (!IPAddress.TryParse(_ip, out _)) - { - _error = "Invalid IP format (v4 or v6 required)."; - yield return _error; - } + if (string.IsNullOrWhiteSpace(value)) + return _error = "Please enter an IP address."; + + if (!IPAddress.TryParse(value, out _)) + return _error = "Invalid IP format (v4 or v6 required)."; + + return null; } private async Task RunSearch() { - await _form.Validate(); + await _form.ValidateAsync(); if (!_form.IsValid) return; try diff --git a/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/UserSettings.razor b/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/UserSettings.razor index 053b3a4..7dd9d7e 100644 --- a/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/UserSettings.razor +++ b/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/UserSettings.razor @@ -83,7 +83,7 @@ private async Task AddUser() { - var dialog = Dialogs.Show("Add User"); + var dialog = await Dialogs.ShowAsync("Add User"); var result = await dialog.Result; if (result.Canceled || result.Data is not AddUserDialog.AddUserResult r) return; @@ -115,7 +115,7 @@ { x => x.TargetUserName, user.UserName } }; - var dialog = Dialogs.Show($"Change Password: {user.UserName}", parameters); + var dialog = await Dialogs.ShowAsync($"Change Password: {user.UserName}", parameters); var result = await dialog.Result; if (result.Canceled || result.Data is not ChangePasswordDialog.ChangePasswordResult r) return; @@ -147,7 +147,7 @@ { x => x.Message, $"Delete user '{user.UserName}'?" } }; - var dialog = Dialogs.Show("Confirm Delete", parameters); + var dialog = await Dialogs.ShowAsync("Confirm Delete", parameters); var result = await dialog.Result; if (result.Canceled || result.Data is not bool confirmed || !confirmed) return; diff --git a/TruthGate-Web/TruthGate-Web/Components/Shared/AddOrEditIpnsDialog.razor b/TruthGate-Web/TruthGate-Web/Components/Shared/AddOrEditIpnsDialog.razor index 11bd822..6f20b04 100644 --- a/TruthGate-Web/TruthGate-Web/Components/Shared/AddOrEditIpnsDialog.razor +++ b/TruthGate-Web/TruthGate-Web/Components/Shared/AddOrEditIpnsDialog.razor @@ -1,4 +1,4 @@ -@using MudBlazor +@using MudBlazor @using System.Text.Json @using TruthGate_Web.Utils @using TruthGate_Web.Endpoints @@ -6,7 +6,7 @@ - + - +
@@ -60,7 +60,7 @@ - +
@@ -193,20 +193,23 @@ _model.KeepOldCidPinned = InitialKeepOldCidPinned; } - private IEnumerable Validate() + private string? ValidateName(string? value) { - _error = null; + if (string.IsNullOrWhiteSpace(value)) + return "Name is required."; - if (string.IsNullOrWhiteSpace(_model.Name)) - yield return "Name is required."; - if (string.IsNullOrWhiteSpace(_model.Key)) - yield return "IPNS key is required."; + return IpfsGateway.ToSafeLeaf(value) is null ? "Invalid name." : null; + } - var leaf = IpfsGateway.ToSafeLeaf(_model.Name); - if (leaf is null) - yield return "Invalid name."; - if (!_model.Key.StartsWith("k51") && !_model.Key.StartsWith("/ipns/")) - yield return "IPNS Key should look like k51… or /ipns/k51…"; + private static string? ValidateKey(string? value) + { + if (string.IsNullOrWhiteSpace(value)) + return "IPNS key is required."; + + return value.StartsWith("k51", StringComparison.OrdinalIgnoreCase) || + value.StartsWith("/ipns/k51", StringComparison.OrdinalIgnoreCase) + ? null + : "IPNS Key should look like k51… or /ipns/k51…"; } private void Cancel() => MudDialog.Cancel(); @@ -293,9 +296,9 @@ return null; } - private async void Submit() + private async Task Submit() { - await _form.Validate(); + await _form.ValidateAsync(); if (!_form.IsValid) return; // Name leaf (immutable on edit) diff --git a/TruthGate-Web/TruthGate-Web/Components/Shared/AddPinnedItemDialog.razor b/TruthGate-Web/TruthGate-Web/Components/Shared/AddPinnedItemDialog.razor index b473c89..76207eb 100644 --- a/TruthGate-Web/TruthGate-Web/Components/Shared/AddPinnedItemDialog.razor +++ b/TruthGate-Web/TruthGate-Web/Components/Shared/AddPinnedItemDialog.razor @@ -1,9 +1,9 @@ -@using MudBlazor +@using MudBlazor @using TruthGate_Web.Utils - + Add Pinned Item CPU % (System vs Process) @@ -91,10 +90,9 @@ System Memory Used (GB) @@ -105,10 +103,9 @@ Process Memory (MB) · Working Set & GC Heap @@ -119,11 +116,10 @@ ThreadPool · Threads & Queue + ChartOptions="@_chartOptions" /> @@ -156,38 +152,34 @@ private int _detailedWindowSeconds = 30; // Detailed: last 30s private bool _rotateXLabels = true; - // ===== Axis cosmetics ===== - private readonly AxisChartOptions _axisOptions = new() - { - MatchBoundsToSize = true, - XAxisLabelRotation = 45 - }; - private readonly ChartOptions _chartOptions = new() - { - YAxisTicks = 8 - }; + // ===== Chart cosmetics ===== + private readonly LineChartOptions _chartOptions = new() + { + XAxisLabelRotation = 45, + YAxisTicks = 8 + }; // ===== Series & labels ===== private string[] _xLabels = Array.Empty(); - private readonly List _cpuSeries = new() + private readonly List> _cpuSeries = new() { - new ChartSeries { Name = "System CPU %", Data = Array.Empty() }, - new ChartSeries { Name = "Process CPU %", Data = Array.Empty() } + new ChartSeries { Name = "System CPU %", Data = Array.Empty() }, + new ChartSeries { Name = "Process CPU %", Data = Array.Empty() } }; - private readonly List _sysMemSeries = new() + private readonly List> _sysMemSeries = new() { - new ChartSeries { Name = "System Used (GB)", Data = Array.Empty() } + new ChartSeries { Name = "System Used (GB)", Data = Array.Empty() } }; - private readonly List _procMemSeries = new() + private readonly List> _procMemSeries = new() { - new ChartSeries { Name = "Working Set (MB)", Data = Array.Empty() }, - new ChartSeries { Name = "GC Heap (MB)", Data = Array.Empty() } + new ChartSeries { Name = "Working Set (MB)", Data = Array.Empty() }, + new ChartSeries { Name = "GC Heap (MB)", Data = Array.Empty() } }; - private readonly List _tpSeries = new() + private readonly List> _tpSeries = new() { - new ChartSeries { Name = "Threads", Data = Array.Empty() }, - new ChartSeries { Name = "Queue Length", Data = Array.Empty() } + new ChartSeries { Name = "Threads", Data = Array.Empty() }, + new ChartSeries { Name = "Queue Length", Data = Array.Empty() } }; // ===== Now-cards ===== @@ -214,7 +206,7 @@ { while (await _timer!.WaitForNextTickAsync(_cts.Token)) { - _axisOptions.XAxisLabelRotation = _rotateXLabels ? 45 : 0; + _chartOptions.XAxisLabelRotation = _rotateXLabels ? 45 : 0; RebuildSeries(); await InvokeAsync(StateHasChanged); } diff --git a/TruthGate-Web/TruthGate-Web/Components/Shared/SimplePromptDialog.razor b/TruthGate-Web/TruthGate-Web/Components/Shared/SimplePromptDialog.razor index 8416526..5052f11 100644 --- a/TruthGate-Web/TruthGate-Web/Components/Shared/SimplePromptDialog.razor +++ b/TruthGate-Web/TruthGate-Web/Components/Shared/SimplePromptDialog.razor @@ -1,4 +1,4 @@ -@using MudBlazor +@using MudBlazor @@ -7,7 +7,7 @@ @Message } - + Validate() + private string? ValidateValue(string? value) { _error = null; - if (Required && string.IsNullOrWhiteSpace(_value)) - { - _error = "Please enter a value."; - yield return _error; - } + if (Required && string.IsNullOrWhiteSpace(value)) + return _error = "Please enter a value."; + + return null; } private async Task Submit() { - await _form.Validate(); + await _form.ValidateAsync(); if (!_form.IsValid) return; MudDialog.Close(DialogResult.Ok(_value)); } diff --git a/TruthGate-Web/TruthGate-Web/TruthGate-Web.csproj b/TruthGate-Web/TruthGate-Web/TruthGate-Web.csproj index 624c429..d18a0fb 100644 --- a/TruthGate-Web/TruthGate-Web/TruthGate-Web.csproj +++ b/TruthGate-Web/TruthGate-Web/TruthGate-Web.csproj @@ -1,7 +1,7 @@  - net9.0 + net10.0 enable enable TruthGate_Web @@ -9,20 +9,21 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all - - - - - - - + + + + + + + +