diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index af752cb..fdae500 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -14,7 +14,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-dotnet@v4 with: - dotnet-version: '8.0.x' + dotnet-version: '10.0.x' - name: Restore run: dotnet restore InboxIntel.sln - name: Build @@ -45,7 +45,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-dotnet@v4 with: - dotnet-version: '8.0.x' + dotnet-version: '10.0.x' - name: dotnet format (verify only) run: dotnet format InboxIntel.sln --verify-no-changes @@ -67,7 +67,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-dotnet@v4 with: - dotnet-version: '8.0.x' + dotnet-version: '10.0.x' - name: Wait for Postgres run: | for i in $(seq 1 30); do diff --git a/.gitea/workflows/security.yml b/.gitea/workflows/security.yml index fce1e4f..c9a354e 100644 --- a/.gitea/workflows/security.yml +++ b/.gitea/workflows/security.yml @@ -31,7 +31,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-dotnet@v4 with: - dotnet-version: '8.0.x' + dotnet-version: '10.0.x' - name: Restore run: dotnet restore InboxIntel.sln - name: .NET vulnerable packages (fail on any) diff --git a/Directory.Build.props b/Directory.Build.props index 626a974..bf8c979 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,6 +1,6 @@ - net8.0 + net10.0 enable enable latest diff --git a/src/InboxIntel.Api/Dockerfile b/src/InboxIntel.Api/Dockerfile index 384d9e0..a366fc2 100644 --- a/src/InboxIntel.Api/Dockerfile +++ b/src/InboxIntel.Api/Dockerfile @@ -1,5 +1,5 @@ # Multi-stage build for the ASP.NET Core API. -FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build WORKDIR /src # Copy solution + project files first for layer-cached restore. @@ -13,7 +13,7 @@ RUN dotnet restore src/InboxIntel.Api/InboxIntel.Api.csproj COPY src/ src/ RUN dotnet publish src/InboxIntel.Api/InboxIntel.Api.csproj -c Release -o /app/publish /p:UseAppHost=false -FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime +FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS runtime WORKDIR /app COPY --from=build /app/publish . diff --git a/src/InboxIntel.Api/InboxIntel.Api.csproj b/src/InboxIntel.Api/InboxIntel.Api.csproj index cf8de7e..8b126b4 100644 --- a/src/InboxIntel.Api/InboxIntel.Api.csproj +++ b/src/InboxIntel.Api/InboxIntel.Api.csproj @@ -5,10 +5,10 @@ 210c6d96-c7e4-4ee9-8982-8b91424979b8 - - + + - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/src/InboxIntel.Api/Program.cs b/src/InboxIntel.Api/Program.cs index bb50ae0..d1d58ce 100644 --- a/src/InboxIntel.Api/Program.cs +++ b/src/InboxIntel.Api/Program.cs @@ -38,8 +38,8 @@ var dp = builder.Services.AddDataProtection() var dpCertPath = builder.Configuration["DataProtection:CertificatePath"]; if (!string.IsNullOrWhiteSpace(dpCertPath)) { - dp.ProtectKeysWithCertificate(new System.Security.Cryptography.X509Certificates.X509Certificate2( - dpCertPath, builder.Configuration["DataProtection:CertificatePassword"])); + dp.ProtectKeysWithCertificate(System.Security.Cryptography.X509Certificates.X509CertificateLoader + .LoadPkcs12FromFile(dpCertPath, builder.Configuration["DataProtection:CertificatePassword"])); } builder.Services.AddApplication(); @@ -201,15 +201,14 @@ var forwardedOptions = new ForwardedHeadersOptions ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto | ForwardedHeaders.XForwardedHost, ForwardLimit = app.Configuration.GetValue("ForwardedHeaders:ForwardLimit") ?? 1 }; -forwardedOptions.KnownNetworks.Clear(); +forwardedOptions.KnownIPNetworks.Clear(); forwardedOptions.KnownProxies.Clear(); var trustedNetworks = app.Configuration.GetSection("ForwardedHeaders:KnownNetworks").Get() ?? new[] { "10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "127.0.0.0/8", "::1/128" }; foreach (var cidr in trustedNetworks) { - var parts = cidr.Split('/'); - if (parts.Length == 2 && System.Net.IPAddress.TryParse(parts[0], out var prefix) && int.TryParse(parts[1], out var len)) - forwardedOptions.KnownNetworks.Add(new Microsoft.AspNetCore.HttpOverrides.IPNetwork(prefix, len)); + if (System.Net.IPNetwork.TryParse(cidr, out var network)) + forwardedOptions.KnownIPNetworks.Add(network); } app.UseForwardedHeaders(forwardedOptions); diff --git a/src/InboxIntel.Application/InboxIntel.Application.csproj b/src/InboxIntel.Application/InboxIntel.Application.csproj index ed04e5a..0576473 100644 --- a/src/InboxIntel.Application/InboxIntel.Application.csproj +++ b/src/InboxIntel.Application/InboxIntel.Application.csproj @@ -6,13 +6,13 @@ - + - + - - + + diff --git a/src/InboxIntel.Domain/InboxIntel.Domain.csproj b/src/InboxIntel.Domain/InboxIntel.Domain.csproj index 955204b..727f931 100644 --- a/src/InboxIntel.Domain/InboxIntel.Domain.csproj +++ b/src/InboxIntel.Domain/InboxIntel.Domain.csproj @@ -6,7 +6,7 @@ - - + + diff --git a/src/InboxIntel.Infrastructure/InboxIntel.Infrastructure.csproj b/src/InboxIntel.Infrastructure/InboxIntel.Infrastructure.csproj index 402a50a..2a0a80a 100644 --- a/src/InboxIntel.Infrastructure/InboxIntel.Infrastructure.csproj +++ b/src/InboxIntel.Infrastructure/InboxIntel.Infrastructure.csproj @@ -4,18 +4,18 @@ InboxIntel.Infrastructure - - - + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - - - - + + + + @@ -24,9 +24,9 @@ - - - + + + diff --git a/tests/InboxIntel.IntegrationTests/AuditFixesTests.cs b/tests/InboxIntel.IntegrationTests/AuditFixesTests.cs index 9cc9b6e..fe608a0 100644 --- a/tests/InboxIntel.IntegrationTests/AuditFixesTests.cs +++ b/tests/InboxIntel.IntegrationTests/AuditFixesTests.cs @@ -25,7 +25,7 @@ namespace InboxIntel.IntegrationTests; /// endpoints (model validation, per-user rate limits) without a real Google login. public class TestAuthHandler : AuthenticationHandler { - public const string Scheme = "Test"; + public new const string Scheme = "Test"; // Stable across requests so per-user rate-limit partitions accumulate correctly. public static readonly string Uid = Guid.NewGuid().ToString(); @@ -52,6 +52,9 @@ public class AuditTestAppFactory : WebApplicationFactory builder.ConfigureHostConfiguration(cfg => cfg.AddInMemoryCollection(new Dictionary { ["Database:AutoMigrate"] = "false", + // Npgsql 10 eagerly validates the connection string when the DbContext is + // resolved (8.x was lazy); these tests never connect, but the string must parse. + ["ConnectionStrings:Postgres"] = "Host=localhost;Database=test;Username=test;Password=test", ["GoogleOAuth:ClientId"] = "test-client-id", ["GoogleOAuth:ClientSecret"] = "test-client-secret", // H-2: make the auth policy trip on the 3rd request within the window. diff --git a/tests/InboxIntel.IntegrationTests/AuthEndpointsTests.cs b/tests/InboxIntel.IntegrationTests/AuthEndpointsTests.cs index fcd59e2..bed6589 100644 --- a/tests/InboxIntel.IntegrationTests/AuthEndpointsTests.cs +++ b/tests/InboxIntel.IntegrationTests/AuthEndpointsTests.cs @@ -19,6 +19,9 @@ public class TestAppFactory : WebApplicationFactory builder.ConfigureHostConfiguration(cfg => cfg.AddInMemoryCollection(new Dictionary { ["Database:AutoMigrate"] = "false", + // Npgsql 10 eagerly validates the connection string when the DbContext is + // resolved (8.x was lazy); these tests never connect, but the string must parse. + ["ConnectionStrings:Postgres"] = "Host=localhost;Database=test;Username=test;Password=test", // Dummy OAuth creds so the Google challenge produces a real 302 redirect // (an empty ClientId can make the handler throw instead of redirecting). ["GoogleOAuth:ClientId"] = "test-client-id", diff --git a/tests/InboxIntel.IntegrationTests/InboxIntel.IntegrationTests.csproj b/tests/InboxIntel.IntegrationTests/InboxIntel.IntegrationTests.csproj index 28ff553..69f6157 100644 --- a/tests/InboxIntel.IntegrationTests/InboxIntel.IntegrationTests.csproj +++ b/tests/InboxIntel.IntegrationTests/InboxIntel.IntegrationTests.csproj @@ -7,8 +7,8 @@ - - + +