Read-only mirrors. Development, issues and pull requests are on GitHub.

summaryrefslogtreecommitdiffhomepage
path: root/apps
diff options
context:
space:
mode:
authorLeitwolf112026-08-28 23:03:57 +0300
committerLeitwolf112026-08-28 23:03:57 +0300
commit18da54ce240b1bfefce36aa2ec3b44b58bd0dbde (patch)
treece146c2ca1d768f3f6418643b8ba3e6d8abce4df /apps
downloadprogmaweb-18da54ce240b1bfefce36aa2ec3b44b58bd0dbde.tar.gz
Initialize Progmaweb 1.0.0 corporate and account platform: add a responsive Next.js corporate site without Visual X# compiler-pipeline content, host-aware account routes, registration and dashboard UX, an ASP.NET Core authentication API with account policies, password hashing, secure sessions, rate limiting and security headers, integration tests on Microsoft Testing Platform, locked dependency graphs, reproducible local orchestration, and separate frontend, backend and security workflows.
Diffstat (limited to 'apps')
-rw-r--r--apps/api-tests/AccountApiFactory.cs29
-rw-r--r--apps/api-tests/AccountEndpointTests.cs180
-rw-r--r--apps/api-tests/GlobalUsings.cs1
-rw-r--r--apps/api-tests/Progmaweb.Api.Tests.csproj17
-rw-r--r--apps/api-tests/SecurityHeaderTests.cs24
-rw-r--r--apps/api-tests/packages.lock.json476
-rw-r--r--apps/api/Accounts/AccountContracts.cs24
-rw-r--r--apps/api/Accounts/AccountEndpoints.cs95
-rw-r--r--apps/api/Accounts/AccountNamePolicy.cs46
-rw-r--r--apps/api/Accounts/AccountRecord.cs17
-rw-r--r--apps/api/Accounts/AccountService.cs110
-rw-r--r--apps/api/Accounts/EmailAddressPolicy.cs39
-rw-r--r--apps/api/Accounts/IAccountStore.cs12
-rw-r--r--apps/api/Accounts/InMemoryAccountStore.cs86
-rw-r--r--apps/api/Accounts/PasswordPolicy.cs31
-rw-r--r--apps/api/Accounts/SessionService.cs89
-rw-r--r--apps/api/Progmaweb.Api.csproj8
-rw-r--r--apps/api/Program.cs64
-rw-r--r--apps/api/Security/SecurityHeadersMiddleware.cs18
-rw-r--r--apps/api/appsettings.Development.json10
-rw-r--r--apps/api/appsettings.json14
-rw-r--r--apps/api/packages.lock.json6
-rw-r--r--apps/web/app/[accountName]/dashboard/page.tsx16
-rw-r--r--apps/web/app/account/page.tsx85
-rw-r--r--apps/web/app/globals.css1215
-rw-r--r--apps/web/app/layout.tsx28
-rw-r--r--apps/web/app/login/page.tsx20
-rw-r--r--apps/web/app/page.tsx213
-rw-r--r--apps/web/app/recover/page.tsx43
-rw-r--r--apps/web/app/register/page.tsx20
-rw-r--r--apps/web/app/robots.ts20
-rw-r--r--apps/web/app/sitemap.ts30
-rw-r--r--apps/web/components/AccountShell.tsx38
-rw-r--r--apps/web/components/AuthForm.tsx227
-rw-r--r--apps/web/components/Brand.tsx27
-rw-r--r--apps/web/components/DashboardClient.tsx165
-rw-r--r--apps/web/components/Footer.tsx33
-rw-r--r--apps/web/components/SiteHeader.tsx51
-rw-r--r--apps/web/eslint.config.mjs9
-rw-r--r--apps/web/next-env.d.ts7
-rw-r--r--apps/web/next.config.ts30
-rw-r--r--apps/web/package.json27
-rw-r--r--apps/web/proxy.ts28
-rw-r--r--apps/web/tsconfig.json34
44 files changed, 3762 insertions, 0 deletions
diff --git a/apps/api-tests/AccountApiFactory.cs b/apps/api-tests/AccountApiFactory.cs
new file mode 100644
index 0000000..fb21f60
--- /dev/null
+++ b/apps/api-tests/AccountApiFactory.cs
@@ -0,0 +1,29 @@
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Mvc.Testing;
+using Microsoft.Extensions.Configuration;
+
+namespace Progmasoft.Progmaweb.Api.Tests;
+
+internal sealed class AccountApiFactory : WebApplicationFactory<Program>
+{
+ protected override void ConfigureWebHost(IWebHostBuilder builder)
+ {
+ builder.UseEnvironment("Testing");
+ builder.ConfigureAppConfiguration((_, configuration) =>
+ {
+ configuration.AddInMemoryCollection(new Dictionary<string, string?>
+ {
+ ["AccountSessions:LifetimeHours"] = "2",
+ ["AccountSessions:CookieName"] = "__Host-ProgmasoftSession"
+ });
+ });
+ }
+
+ public HttpClient CreateAccountClient(bool handleCookies = true) => CreateClient(
+ new WebApplicationFactoryClientOptions
+ {
+ AllowAutoRedirect = false,
+ BaseAddress = new Uri("https://localhost"),
+ HandleCookies = handleCookies
+ });
+}
diff --git a/apps/api-tests/AccountEndpointTests.cs b/apps/api-tests/AccountEndpointTests.cs
new file mode 100644
index 0000000..b868ccd
--- /dev/null
+++ b/apps/api-tests/AccountEndpointTests.cs
@@ -0,0 +1,180 @@
+using System.Net;
+using System.Net.Http.Json;
+using System.Text.Json;
+
+namespace Progmasoft.Progmaweb.Api.Tests;
+
+public sealed class AccountEndpointTests
+{
+ [Fact]
+ public async Task RegisterCreatesSessionAndMeReturnsAccount()
+ {
+ await using AccountApiFactory factory = new();
+ using HttpClient client = factory.CreateAccountClient();
+
+ HttpResponseMessage registration = await client.PostAsJsonAsync("/api/v1/accounts/register", new
+ {
+ accountName = "PrimaryUser",
+ email = "leitwolf1112@gmail.com",
+ password = "a-long-development-password"
+ }, TestContext.Current.CancellationToken);
+
+ Assert.Equal(HttpStatusCode.Created, registration.StatusCode);
+ Assert.True(registration.Headers.TryGetValues("Set-Cookie", out IEnumerable<string>? cookies));
+ string sessionCookie = Assert.Single(cookies);
+ Assert.Contains("__Host-ProgmasoftSession=", sessionCookie, StringComparison.Ordinal);
+ Assert.Contains("secure", sessionCookie, StringComparison.OrdinalIgnoreCase);
+ Assert.Contains("httponly", sessionCookie, StringComparison.OrdinalIgnoreCase);
+ Assert.Contains("samesite=lax", sessionCookie, StringComparison.OrdinalIgnoreCase);
+
+ HttpResponseMessage me =
+ await client.GetAsync("/api/v1/accounts/me", TestContext.Current.CancellationToken);
+ Assert.Equal(HttpStatusCode.OK, me.StatusCode);
+ using JsonDocument body =
+ JsonDocument.Parse(await me.Content.ReadAsStringAsync(TestContext.Current.CancellationToken));
+ Assert.Equal("PrimaryUser", body.RootElement.GetProperty("accountName").GetString());
+ Assert.Equal("leitwolf1112@gmail.com", body.RootElement.GetProperty("email").GetString());
+ }
+
+ [Fact]
+ public async Task LogoutRevokesCurrentSession()
+ {
+ await using AccountApiFactory factory = new();
+ using HttpClient client = factory.CreateAccountClient();
+
+ HttpResponseMessage registration = await client.PostAsJsonAsync("/api/v1/accounts/register", new
+ {
+ accountName = "LogoutUser",
+ email = "logout@example.com",
+ password = "a-long-development-password"
+ }, TestContext.Current.CancellationToken);
+ Assert.Equal(HttpStatusCode.Created, registration.StatusCode);
+
+ Assert.Equal(HttpStatusCode.NoContent,
+ (await client.PostAsync("/api/v1/accounts/logout", null, TestContext.Current.CancellationToken)).StatusCode);
+ Assert.Equal(HttpStatusCode.Unauthorized,
+ (await client.GetAsync("/api/v1/accounts/me", TestContext.Current.CancellationToken)).StatusCode);
+ }
+
+ [Theory]
+ [InlineData("lowercase")]
+ [InlineData("Short7")]
+ [InlineData("Contains-Dash")]
+ [InlineData("ĞecersizAd")]
+ [InlineData("Progmasoft")]
+ [InlineData("Leitwolf")]
+ public async Task RegisterRejectsInvalidOrReservedAccountName(string accountName)
+ {
+ await using AccountApiFactory factory = new();
+ using HttpClient client = factory.CreateAccountClient();
+
+ HttpResponseMessage response = await client.PostAsJsonAsync("/api/v1/accounts/register", new
+ {
+ accountName,
+ email = $"{Guid.NewGuid():N}@example.com",
+ password = "a-long-development-password"
+ }, TestContext.Current.CancellationToken);
+
+ Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
+ string body = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken);
+ Assert.Contains("accountName", body, StringComparison.Ordinal);
+ }
+
+ [Fact]
+ public async Task EmailUniquenessIsCaseInsensitive()
+ {
+ await using AccountApiFactory factory = new();
+ using HttpClient client = factory.CreateAccountClient();
+
+ HttpResponseMessage first = await RegisterAsync(client, "SampleUser", "Person@Example.com");
+ HttpResponseMessage second = await RegisterAsync(client, "DifferentUser", "person@example.COM");
+
+ Assert.Equal(HttpStatusCode.Created, first.StatusCode);
+ Assert.Equal(HttpStatusCode.BadRequest, second.StatusCode);
+ Assert.Contains("email", await second.Content.ReadAsStringAsync(TestContext.Current.CancellationToken),
+ StringComparison.Ordinal);
+ }
+
+ [Fact]
+ public async Task AccountNameUniquenessIsCaseInsensitiveWhileDisplayCaseIsPreserved()
+ {
+ await using AccountApiFactory factory = new();
+ using HttpClient client = factory.CreateAccountClient();
+
+ HttpResponseMessage first = await RegisterAsync(client, "SampleUser", "first-name@example.com");
+ HttpResponseMessage second = await RegisterAsync(client, "SampleUSER", "second-name@example.com");
+
+ Assert.Equal(HttpStatusCode.Created, first.StatusCode);
+ Assert.Equal(HttpStatusCode.BadRequest, second.StatusCode);
+ Assert.Contains("accountName", await second.Content.ReadAsStringAsync(TestContext.Current.CancellationToken),
+ StringComparison.Ordinal);
+ }
+
+ [Fact]
+ public async Task DuplicateAccountNameIsRejectedWithoutChangingCase()
+ {
+ await using AccountApiFactory factory = new();
+ using HttpClient client = factory.CreateAccountClient();
+
+ HttpResponseMessage first = await RegisterAsync(client, "UniqueName", "first@example.com");
+ HttpResponseMessage second = await RegisterAsync(client, "UniqueName", "second@example.com");
+
+ Assert.Equal(HttpStatusCode.Created, first.StatusCode);
+ Assert.Equal(HttpStatusCode.BadRequest, second.StatusCode);
+ Assert.Contains("accountName", await second.Content.ReadAsStringAsync(TestContext.Current.CancellationToken),
+ StringComparison.Ordinal);
+ }
+
+ [Fact]
+ public async Task LoginUsesGenericFailureForUnknownEmailAndWrongPassword()
+ {
+ await using AccountApiFactory factory = new();
+ using HttpClient client = factory.CreateAccountClient();
+ await RegisterAsync(client, "LoginUser", "login@example.com");
+ await client.PostAsync("/api/v1/accounts/logout", null, TestContext.Current.CancellationToken);
+
+ HttpResponseMessage missing = await client.PostAsJsonAsync("/api/v1/accounts/login", new
+ {
+ email = "missing@example.com",
+ password = "a-long-development-password"
+ }, TestContext.Current.CancellationToken);
+ HttpResponseMessage wrong = await client.PostAsJsonAsync("/api/v1/accounts/login", new
+ {
+ email = "login@example.com",
+ password = "a-long-but-incorrect-password"
+ }, TestContext.Current.CancellationToken);
+
+ Assert.Equal(HttpStatusCode.Unauthorized, missing.StatusCode);
+ Assert.Equal(HttpStatusCode.Unauthorized, wrong.StatusCode);
+ using JsonDocument missingBody =
+ JsonDocument.Parse(await missing.Content.ReadAsStringAsync(TestContext.Current.CancellationToken));
+ using JsonDocument wrongBody =
+ JsonDocument.Parse(await wrong.Content.ReadAsStringAsync(TestContext.Current.CancellationToken));
+ Assert.Equal(missingBody.RootElement.GetProperty("title").GetString(),
+ wrongBody.RootElement.GetProperty("title").GetString());
+ Assert.Equal(missingBody.RootElement.GetProperty("detail").GetString(),
+ wrongBody.RootElement.GetProperty("detail").GetString());
+ }
+
+ [Fact]
+ public async Task StatusResponseAdvertisesProgmawebVersion()
+ {
+ await using AccountApiFactory factory = new();
+ using HttpClient client = factory.CreateAccountClient(handleCookies: false);
+
+ using JsonDocument body =
+ JsonDocument.Parse(await client.GetStringAsync("/api/v1/status", TestContext.Current.CancellationToken));
+
+ Assert.Equal("progmaweb-account", body.RootElement.GetProperty("service").GetString());
+ Assert.Equal("online", body.RootElement.GetProperty("status").GetString());
+ Assert.Equal("1.0.0", body.RootElement.GetProperty("version").GetString());
+ }
+
+ private static Task<HttpResponseMessage> RegisterAsync(HttpClient client, string accountName, string email) =>
+ client.PostAsJsonAsync("/api/v1/accounts/register", new
+ {
+ accountName,
+ email,
+ password = "a-long-development-password"
+ }, TestContext.Current.CancellationToken);
+}
diff --git a/apps/api-tests/GlobalUsings.cs b/apps/api-tests/GlobalUsings.cs
new file mode 100644
index 0000000..c802f44
--- /dev/null
+++ b/apps/api-tests/GlobalUsings.cs
@@ -0,0 +1 @@
+global using Xunit;
diff --git a/apps/api-tests/Progmaweb.Api.Tests.csproj b/apps/api-tests/Progmaweb.Api.Tests.csproj
new file mode 100644
index 0000000..7c7202f
--- /dev/null
+++ b/apps/api-tests/Progmaweb.Api.Tests.csproj
@@ -0,0 +1,17 @@
+<Project Sdk="Microsoft.NET.Sdk">
+ <PropertyGroup>
+ <TargetFramework>net10.0</TargetFramework>
+ <IsPackable>false</IsPackable>
+ <IsTestProject>true</IsTestProject>
+ <RootNamespace>Progmasoft.Progmaweb.Api.Tests</RootNamespace>
+ </PropertyGroup>
+ <ItemGroup>
+ <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.11" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.9.0" />
+ <PackageReference Include="xunit.v3" Version="4.0.0" />
+ <PackageReference Include="xunit.runner.visualstudio" Version="4.0.0" />
+ </ItemGroup>
+ <ItemGroup>
+ <ProjectReference Include="..\api\Progmaweb.Api.csproj" />
+ </ItemGroup>
+</Project>
diff --git a/apps/api-tests/SecurityHeaderTests.cs b/apps/api-tests/SecurityHeaderTests.cs
new file mode 100644
index 0000000..384ee75
--- /dev/null
+++ b/apps/api-tests/SecurityHeaderTests.cs
@@ -0,0 +1,24 @@
+using System.Net;
+
+namespace Progmasoft.Progmaweb.Api.Tests;
+
+public sealed class SecurityHeaderTests
+{
+ [Fact]
+ public async Task ApiResponsesSetDefensiveHeaders()
+ {
+ await using AccountApiFactory factory = new();
+ using HttpClient client = factory.CreateAccountClient(handleCookies: false);
+
+ HttpResponseMessage response =
+ await client.GetAsync("/api/v1/status", TestContext.Current.CancellationToken);
+
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ Assert.Equal("no-store", Assert.Single(response.Headers.GetValues("Cache-Control")));
+ Assert.Equal("no-referrer", Assert.Single(response.Headers.GetValues("Referrer-Policy")));
+ Assert.Equal("nosniff", Assert.Single(response.Headers.GetValues("X-Content-Type-Options")));
+ Assert.Equal("DENY", Assert.Single(response.Headers.GetValues("X-Frame-Options")));
+ Assert.Contains("frame-ancestors 'none'",
+ Assert.Single(response.Headers.GetValues("Content-Security-Policy")), StringComparison.Ordinal);
+ }
+}
diff --git a/apps/api-tests/packages.lock.json b/apps/api-tests/packages.lock.json
new file mode 100644
index 0000000..0a5ef16
--- /dev/null
+++ b/apps/api-tests/packages.lock.json
@@ -0,0 +1,476 @@
+{
+ "version": 1,
+ "dependencies": {
+ "net10.0": {
+ "Microsoft.AspNetCore.Mvc.Testing": {
+ "type": "Direct",
+ "requested": "[10.0.11, )",
+ "resolved": "10.0.11",
+ "contentHash": "BJi3rpYETlh0wd14Lg5Is9MWvEf55TAGd7yo+xGHuEZp7ololyvftbQZ1i7ZuITA7DgNPJED6EMhFSWycq7SBg==",
+ "dependencies": {
+ "Microsoft.AspNetCore.TestHost": "10.0.11",
+ "Microsoft.Extensions.DependencyModel": "10.0.11",
+ "Microsoft.Extensions.Hosting": "10.0.11"
+ }
+ },
+ "Microsoft.NET.Test.Sdk": {
+ "type": "Direct",
+ "requested": "[18.9.0, )",
+ "resolved": "18.9.0",
+ "contentHash": "xIzVXa/VpKXqDWzyC/5Hw8JbFY5u9k3Jc53CpcjBiOXvkQGio484LD9FNwOiGUSMDcYL3Rcw9sNgGi5WrswlPQ==",
+ "dependencies": {
+ "Microsoft.CodeCoverage": "18.9.0",
+ "Microsoft.TestPlatform.TestHost": "18.9.0"
+ }
+ },
+ "xunit.runner.visualstudio": {
+ "type": "Direct",
+ "requested": "[4.0.0, )",
+ "resolved": "4.0.0",
+ "contentHash": "kzLFyBYUnoidpGOXvpNOfIo8C92gVgVR6X8ncHwhcrDYugiOut5rrhDkr0L3cWHd7J2pKXf6LgaeXoBBDlx1ag=="
+ },
+ "xunit.v3": {
+ "type": "Direct",
+ "requested": "[4.0.0, )",
+ "resolved": "4.0.0",
+ "contentHash": "czH4MaZ2k2eLjetuN5W1fUEnNVADsTOeYxDvrqIFh04XO6H3Y8Yu1FrSy3psF1q06DZV33wftjqZVv4acwIp9Q==",
+ "dependencies": {
+ "xunit.v3.mtp-v2": "[4.0.0]"
+ }
+ },
+ "Microsoft.ApplicationInsights": {
+ "type": "Transitive",
+ "resolved": "2.23.0",
+ "contentHash": "nWArUZTdU7iqZLycLKWe0TDms48KKGE6pONH2terYNa8REXiqixrMOkf1sk5DHGMaUTqONU2YkS4SAXBhLStgw=="
+ },
+ "Microsoft.AspNetCore.TestHost": {
+ "type": "Transitive",
+ "resolved": "10.0.11",
+ "contentHash": "IVGNbZVxLuL80y7cmXYfhIFoTGneHkp4D7xXarYe6V/wjo8/2qiSLUwWax1/h2eHtJ3Y5cuqmtpOyWMnXIsUMA=="
+ },
+ "Microsoft.Bcl.AsyncInterfaces": {
+ "type": "Transitive",
+ "resolved": "6.0.0",
+ "contentHash": "UcSjPsst+DfAdJGVDsu346FX0ci0ah+lw3WRtn18NUwEqRt70HaOQ7lI72vy3+1LxtqI3T5GWwV39rQSrCzAeg=="
+ },
+ "Microsoft.CodeCoverage": {
+ "type": "Transitive",
+ "resolved": "18.9.0",
+ "contentHash": "MtegCIKGuG0r/LCdIjoR1HgzCGIUBhR3aNdbtQpts5vMXQuqBDZ2Jsd2uFKoHFM2XrQV6ZrDf3F5oLi3SLTp8w=="
+ },
+ "Microsoft.Extensions.Configuration": {
+ "type": "Transitive",
+ "resolved": "10.0.11",
+ "contentHash": "wlhRqZW8LcJPa+vk2oLAc/REXDItHtkFQdf/QcXYGZbZOO13izcsKY1pCvuFQYwUiZD+hwSZwsKASjqT+BNaVg==",
+ "dependencies": {
+ "Microsoft.Extensions.Configuration.Abstractions": "10.0.11",
+ "Microsoft.Extensions.Primitives": "10.0.11"
+ }
+ },
+ "Microsoft.Extensions.Configuration.Abstractions": {
+ "type": "Transitive",
+ "resolved": "10.0.11",
+ "contentHash": "fVi053xdpda9Em7vSkmgVxO/PtgC2m78ekReKWsgcyskqY0U82Bz/MONwxpGzI0hElYKJfw+fupqMVeKW3fSaA==",
+ "dependencies": {
+ "Microsoft.Extensions.Primitives": "10.0.11"
+ }
+ },
+ "Microsoft.Extensions.Configuration.Binder": {
+ "type": "Transitive",
+ "resolved": "10.0.11",
+ "contentHash": "rFn8RuszZn3qquPVkDytMUlPc2+rXl9MCoygwc1XmAgC5vg5/oXJ8hkOosOrLoBLsqdTy4lFwP6iQdPS9uSYOA==",
+ "dependencies": {
+ "Microsoft.Extensions.Configuration": "10.0.11",
+ "Microsoft.Extensions.Configuration.Abstractions": "10.0.11"
+ }
+ },
+ "Microsoft.Extensions.Configuration.CommandLine": {
+ "type": "Transitive",
+ "resolved": "10.0.11",
+ "contentHash": "1KHr/1L56llwQ/yI0tAisEA31UpPsn8aasjASIwELOaN4JIUcbjuQBMdFOIzfNBBeULoUa0XfBe5QDtRRUY+fg==",
+ "dependencies": {
+ "Microsoft.Extensions.Configuration": "10.0.11",
+ "Microsoft.Extensions.Configuration.Abstractions": "10.0.11"
+ }
+ },
+ "Microsoft.Extensions.Configuration.EnvironmentVariables": {
+ "type": "Transitive",
+ "resolved": "10.0.11",
+ "contentHash": "KICyU3eVi5jvloKm01EXV69L97H/zkhISVtV98cIuzuFOxNx3xTUVcXqvWTz3aq7OvUuDB/MFlPFjmxRaKF7/A==",
+ "dependencies": {
+ "Microsoft.Extensions.Configuration": "10.0.11",
+ "Microsoft.Extensions.Configuration.Abstractions": "10.0.11"
+ }
+ },
+ "Microsoft.Extensions.Configuration.FileExtensions": {
+ "type": "Transitive",
+ "resolved": "10.0.11",
+ "contentHash": "mDW7KVFB05M6jiRUyaZiOMWhS31n5HlSZwoYctHAZAucD4sMDJ70IxOmkGDt6RpstchD+keWBjhdzcMpSkWvWQ==",
+ "dependencies": {
+ "Microsoft.Extensions.Configuration": "10.0.11",
+ "Microsoft.Extensions.Configuration.Abstractions": "10.0.11",
+ "Microsoft.Extensions.FileProviders.Abstractions": "10.0.11",
+ "Microsoft.Extensions.FileProviders.Physical": "10.0.11",
+ "Microsoft.Extensions.Primitives": "10.0.11"
+ }
+ },
+ "Microsoft.Extensions.Configuration.Json": {
+ "type": "Transitive",
+ "resolved": "10.0.11",
+ "contentHash": "nSPrT8U/cNoB4coqkmnanAMK9PsL7lsjG+LLUKEwHRFwS6E78b8S1wdv/y88EOxBhasWov1rLd7RTHmmsYPOLg==",
+ "dependencies": {
+ "Microsoft.Extensions.Configuration": "10.0.11",
+ "Microsoft.Extensions.Configuration.Abstractions": "10.0.11",
+ "Microsoft.Extensions.Configuration.FileExtensions": "10.0.11",
+ "Microsoft.Extensions.FileProviders.Abstractions": "10.0.11"
+ }
+ },
+ "Microsoft.Extensions.Configuration.UserSecrets": {
+ "type": "Transitive",
+ "resolved": "10.0.11",
+ "contentHash": "BRliLdUowglV8GS+J1G/QsSofCJYYFg3U8QZx0ACRn+a91az/Qnpy+h6PyHS94WgV2TSazX5D/cuuk6wnCJatw==",
+ "dependencies": {
+ "Microsoft.Extensions.Configuration.Abstractions": "10.0.11",
+ "Microsoft.Extensions.Configuration.Json": "10.0.11",
+ "Microsoft.Extensions.FileProviders.Abstractions": "10.0.11",
+ "Microsoft.Extensions.FileProviders.Physical": "10.0.11"
+ }
+ },
+ "Microsoft.Extensions.DependencyInjection": {
+ "type": "Transitive",
+ "resolved": "10.0.11",
+ "contentHash": "PSmotV19c7E3lKed++uYo1kSiXFI+uTl37CBSrhq+CfLC3FCHjG7R91+xPnNehQfHS1b0Tzo/CCLPWH3qaEheg==",
+ "dependencies": {
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.11"
+ }
+ },
+ "Microsoft.Extensions.DependencyInjection.Abstractions": {
+ "type": "Transitive",
+ "resolved": "10.0.11",
+ "contentHash": "/a1aJz4m7ylhEDf25ugQChLQoN5XwoGjWw/BoR/ZWWKsO1v4DdJElS1uyngahz4B/eOzjFk1KNTkarRLE5wsIg=="
+ },
+ "Microsoft.Extensions.DependencyModel": {
+ "type": "Transitive",
+ "resolved": "10.0.11",
+ "contentHash": "PJPtFYsZ+r+uz9qqXWUTEyKeJ1EiBGIJtqavkg9ZXijjGSFAk4Fgi5sqIxj+uAyLZwEKgexDUQXhWhvU6l3+og=="
+ },
+ "Microsoft.Extensions.Diagnostics": {
+ "type": "Transitive",
+ "resolved": "10.0.11",
+ "contentHash": "HT70uGPxMLqqnOzKMcnQtDmeV4r0KHr4qVCLhP7SXil9jMEm8sQXwcybxVVFGXZJ1V44xV0mLqQ54aZbcR2OiQ==",
+ "dependencies": {
+ "Microsoft.Extensions.Configuration": "10.0.11",
+ "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.11",
+ "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.11"
+ }
+ },
+ "Microsoft.Extensions.Diagnostics.Abstractions": {
+ "type": "Transitive",
+ "resolved": "10.0.11",
+ "contentHash": "se7Kx8QpJEt+nf26L4qIVAofGTDr1wbexxsh/Fm3Xc04xUkqUXK06KUS7FLwSQYSjqb7q9n+T7MEcXYBhI1Y5g==",
+ "dependencies": {
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.11",
+ "Microsoft.Extensions.Options": "10.0.11"
+ }
+ },
+ "Microsoft.Extensions.FileProviders.Abstractions": {
+ "type": "Transitive",
+ "resolved": "10.0.11",
+ "contentHash": "JOjac6SQQgZmdmB8WGEw61/7siqMZoWJMkmq2p1goJGxqI59lO6oB4bOl0jNsbaPBdYy5Mlkb+6U7T4+CjnD8Q==",
+ "dependencies": {
+ "Microsoft.Extensions.Primitives": "10.0.11"
+ }
+ },
+ "Microsoft.Extensions.FileProviders.Physical": {
+ "type": "Transitive",
+ "resolved": "10.0.11",
+ "contentHash": "Tq/UqMaczePv9yWwSsJZRgKtgA46djVR5xHj/lZBCueQ3ag8f9v5mu0EdhrNx7tXxNk+Y9OurG2oKuSKINjr0A==",
+ "dependencies": {
+ "Microsoft.Extensions.FileProviders.Abstractions": "10.0.11",
+ "Microsoft.Extensions.FileSystemGlobbing": "10.0.11",
+ "Microsoft.Extensions.Primitives": "10.0.11"
+ }
+ },
+ "Microsoft.Extensions.FileSystemGlobbing": {
+ "type": "Transitive",
+ "resolved": "10.0.11",
+ "contentHash": "2i6rtW/B5rCnWCnhdmWWEmaM9O0HD0zsPY9eRqa++y4tclI3Uw8zvGbBvhY/LjAdtf8gUHhUPcAWj3DRlWMXmQ=="
+ },
+ "Microsoft.Extensions.Hosting": {
+ "type": "Transitive",
+ "resolved": "10.0.11",
+ "contentHash": "eIDa/Rl+93aj17gMlFsJJx+LhBvb3CP0Mu1PeVYkDp2Y3S4Jock8UynfGQEcx7lrlq+gKW+ECQJHbro/LTPDEQ==",
+ "dependencies": {
+ "Microsoft.Extensions.Configuration": "10.0.11",
+ "Microsoft.Extensions.Configuration.Abstractions": "10.0.11",
+ "Microsoft.Extensions.Configuration.Binder": "10.0.11",
+ "Microsoft.Extensions.Configuration.CommandLine": "10.0.11",
+ "Microsoft.Extensions.Configuration.EnvironmentVariables": "10.0.11",
+ "Microsoft.Extensions.Configuration.FileExtensions": "10.0.11",
+ "Microsoft.Extensions.Configuration.Json": "10.0.11",
+ "Microsoft.Extensions.Configuration.UserSecrets": "10.0.11",
+ "Microsoft.Extensions.DependencyInjection": "10.0.11",
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.11",
+ "Microsoft.Extensions.Diagnostics": "10.0.11",
+ "Microsoft.Extensions.FileProviders.Abstractions": "10.0.11",
+ "Microsoft.Extensions.FileProviders.Physical": "10.0.11",
+ "Microsoft.Extensions.Hosting.Abstractions": "10.0.11",
+ "Microsoft.Extensions.Logging": "10.0.11",
+ "Microsoft.Extensions.Logging.Abstractions": "10.0.11",
+ "Microsoft.Extensions.Logging.Configuration": "10.0.11",
+ "Microsoft.Extensions.Logging.Console": "10.0.11",
+ "Microsoft.Extensions.Logging.Debug": "10.0.11",
+ "Microsoft.Extensions.Logging.EventLog": "10.0.11",
+ "Microsoft.Extensions.Logging.EventSource": "10.0.11",
+ "Microsoft.Extensions.Options": "10.0.11"
+ }
+ },
+ "Microsoft.Extensions.Hosting.Abstractions": {
+ "type": "Transitive",
+ "resolved": "10.0.11",
+ "contentHash": "pwtpF7iF/NNaOBcX+pvMZ7y2+JAVbH5KkNrH9uMZtuVxVJsFTDiWiCR7Tk3HVptsAijaetipUZVRVK2LLq+nvA==",
+ "dependencies": {
+ "Microsoft.Extensions.Configuration.Abstractions": "10.0.11",
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.11",
+ "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.11",
+ "Microsoft.Extensions.FileProviders.Abstractions": "10.0.11",
+ "Microsoft.Extensions.Logging.Abstractions": "10.0.11"
+ }
+ },
+ "Microsoft.Extensions.Logging": {
+ "type": "Transitive",
+ "resolved": "10.0.11",
+ "contentHash": "nUOJwgFkSiLHiVGFpU22pIJtuWYewuSYQ3JVuP/gdK8ASMT807Px+TYQiRWs6uSsOmoyFTaVCwKXTasczV6BpA==",
+ "dependencies": {
+ "Microsoft.Extensions.DependencyInjection": "10.0.11",
+ "Microsoft.Extensions.Logging.Abstractions": "10.0.11",
+ "Microsoft.Extensions.Options": "10.0.11"
+ }
+ },
+ "Microsoft.Extensions.Logging.Abstractions": {
+ "type": "Transitive",
+ "resolved": "10.0.11",
+ "contentHash": "Ljd0Uxoq5XpScD2Bg0nM/r3mwx7Ao5Uq24eo2ARxbGvqJ7Zht6rt2cJtwVRH4Cv+1ZVMdXz6TB43KbpmsxRrvQ==",
+ "dependencies": {
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.11"
+ }
+ },
+ "Microsoft.Extensions.Logging.Configuration": {
+ "type": "Transitive",
+ "resolved": "10.0.11",
+ "contentHash": "S7LvLeVHKNPaY2NMyxW7c2TBGsLgxoSUBCV5Ev5iN8kgC7EPR2UB7eW7vHsElGMcIUDwRmoxLfvGDynCn3q6EA==",
+ "dependencies": {
+ "Microsoft.Extensions.Configuration": "10.0.11",
+ "Microsoft.Extensions.Configuration.Abstractions": "10.0.11",
+ "Microsoft.Extensions.Configuration.Binder": "1