From a410efdbda142e8d605ea13188d866cebad3f6d8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 11 Jul 2025 05:57:44 +0000 Subject: [PATCH 1/3] Initial plan From 0d84c90ced1af770910cd1f4f383aafc0d376c63 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 11 Jul 2025 06:04:30 +0000 Subject: [PATCH 2/3] Initial analysis and build setup Co-authored-by: BenjaminMichaelis <22186029+BenjaminMichaelis@users.noreply.github.com> --- .../EssentialCSharp.Web.Tests.csproj | 2 +- EssentialCSharp.Web/EssentialCSharp.Web.csproj | 2 +- global.json | 2 +- packages-microsoft-prod.deb | Bin 0 -> 3690 bytes 4 files changed, 3 insertions(+), 3 deletions(-) create mode 100644 packages-microsoft-prod.deb diff --git a/EssentialCSharp.Web.Tests/EssentialCSharp.Web.Tests.csproj b/EssentialCSharp.Web.Tests/EssentialCSharp.Web.Tests.csproj index cde07e16..3499899b 100644 --- a/EssentialCSharp.Web.Tests/EssentialCSharp.Web.Tests.csproj +++ b/EssentialCSharp.Web.Tests/EssentialCSharp.Web.Tests.csproj @@ -1,7 +1,7 @@  - net9.0 + net8.0 false false diff --git a/EssentialCSharp.Web/EssentialCSharp.Web.csproj b/EssentialCSharp.Web/EssentialCSharp.Web.csproj index 4cb85e02..8b2ebfb0 100644 --- a/EssentialCSharp.Web/EssentialCSharp.Web.csproj +++ b/EssentialCSharp.Web/EssentialCSharp.Web.csproj @@ -1,6 +1,6 @@  - net9.0 + net8.0 diff --git a/global.json b/global.json index e7673427..65b01979 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "9.0.101", + "version": "8.0.117", "rollForward": "latestMinor" } } \ No newline at end of file diff --git a/packages-microsoft-prod.deb b/packages-microsoft-prod.deb new file mode 100644 index 0000000000000000000000000000000000000000..40874bc7face2e6f3b47dbd8eaff9cca5ab8ca6c GIT binary patch literal 3690 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 literal 0 HcmV?d00001 From b31d6ce72527d5611f24cd348930cfccc70864bb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 11 Jul 2025 06:09:09 +0000 Subject: [PATCH 3/3] Add captcha to Login, ResendEmailConfirmation, and ResetPassword pages Co-authored-by: BenjaminMichaelis <22186029+BenjaminMichaelis@users.noreply.github.com> --- .../EssentialCSharp.Web.Tests.csproj | 2 +- .../Areas/Identity/Pages/Account/Login.cshtml | 4 + .../Identity/Pages/Account/Login.cshtml.cs | 148 +++++++++++++----- .../Account/ResendEmailConfirmation.cshtml | 4 + .../Account/ResendEmailConfirmation.cshtml.cs | 113 ++++++++++--- .../Pages/Account/ResetPassword.cshtml | 4 + .../Pages/Account/ResetPassword.cshtml.cs | 115 +++++++++++--- .../EssentialCSharp.Web.csproj | 2 +- global.json | 2 +- packages-microsoft-prod.deb | Bin 3690 -> 0 bytes 10 files changed, 309 insertions(+), 85 deletions(-) delete mode 100644 packages-microsoft-prod.deb diff --git a/EssentialCSharp.Web.Tests/EssentialCSharp.Web.Tests.csproj b/EssentialCSharp.Web.Tests/EssentialCSharp.Web.Tests.csproj index 3499899b..cde07e16 100644 --- a/EssentialCSharp.Web.Tests/EssentialCSharp.Web.Tests.csproj +++ b/EssentialCSharp.Web.Tests/EssentialCSharp.Web.Tests.csproj @@ -1,7 +1,7 @@  - net8.0 + net9.0 false false diff --git a/EssentialCSharp.Web/Areas/Identity/Pages/Account/Login.cshtml b/EssentialCSharp.Web/Areas/Identity/Pages/Account/Login.cshtml index ddb1f9a2..9863af53 100644 --- a/EssentialCSharp.Web/Areas/Identity/Pages/Account/Login.cshtml +++ b/EssentialCSharp.Web/Areas/Identity/Pages/Account/Login.cshtml @@ -29,6 +29,9 @@ @Html.DisplayNameFor(m => m.Input.RememberMe) +
+
+
@@ -82,5 +85,6 @@ @section Scripts { + } diff --git a/EssentialCSharp.Web/Areas/Identity/Pages/Account/Login.cshtml.cs b/EssentialCSharp.Web/Areas/Identity/Pages/Account/Login.cshtml.cs index a3ea80a7..5bb5f29f 100644 --- a/EssentialCSharp.Web/Areas/Identity/Pages/Account/Login.cshtml.cs +++ b/EssentialCSharp.Web/Areas/Identity/Pages/Account/Login.cshtml.cs @@ -1,15 +1,19 @@ using System.ComponentModel.DataAnnotations; using EssentialCSharp.Web.Areas.Identity.Data; +using EssentialCSharp.Web.Models; +using EssentialCSharp.Web.Services; using EssentialCSharp.Web.Services.Referrals; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.Extensions.Options; namespace EssentialCSharp.Web.Areas.Identity.Pages.Account; -public class LoginModel(SignInManager signInManager, UserManager userManager, ILogger logger, IReferralService referralService) : PageModel +public class LoginModel(SignInManager signInManager, UserManager userManager, ILogger logger, IReferralService referralService, ICaptchaService captchaService, IOptions optionsAccessor) : PageModel { + public CaptchaOptions CaptchaOptions { get; } = optionsAccessor.Value; private InputModel? _Input; [BindProperty] public InputModel Input @@ -60,49 +64,119 @@ public async Task OnGetAsync(string? returnUrl = null) public async Task OnPostAsync(string? returnUrl = null) { returnUrl ??= Url.Content("~/"); + string? hCaptcha_response = Request.Form[CaptchaOptions.HttpPostResponseKeyName]; ExternalLogins = (await signInManager.GetExternalAuthenticationSchemesAsync()).ToList(); - if (ModelState.IsValid) + if (!ModelState.IsValid) { - Microsoft.AspNetCore.Identity.SignInResult result; - if (Input.Email is null) - { - return RedirectToPage(Url.Content("~/"), new { ReturnUrl = returnUrl }); - } - EssentialCSharpWebUser? foundUser = await userManager.FindByEmailAsync(Input.Email); - if (Input.Password is null) - { - return RedirectToPage(Url.Content("~/"), new { ReturnUrl = returnUrl }); - } - if (foundUser is not null) - { - result = await signInManager.PasswordSignInAsync(foundUser, Input.Password, Input.RememberMe, lockoutOnFailure: true); - // Call the referral service to get the referral ID and set it onto the user claim - _ = await referralService.EnsureReferralIdAsync(foundUser); - } - else - { - result = await signInManager.PasswordSignInAsync(Input.Email, Input.Password, Input.RememberMe, lockoutOnFailure: true); - } - if (result.Succeeded) - { - logger.LogInformation("User logged in."); - return LocalRedirect(returnUrl); - } - if (result.RequiresTwoFactor) - { - return RedirectToPage("./LoginWith2fa", new { ReturnUrl = returnUrl, RememberMe = Input.RememberMe }); - } - if (result.IsLockedOut) + return Page(); + } + + if (hCaptcha_response is null) + { + ModelState.AddModelError(CaptchaOptions.HttpPostResponseKeyName, HCaptchaErrorDetails.GetValue(HCaptchaErrorDetails.MissingInputResponse).FriendlyDescription); + return Page(); + } + + HCaptchaResult? response = await captchaService.VerifyAsync(hCaptcha_response); + if (response is null) + { + ModelState.AddModelError(CaptchaOptions.HttpPostResponseKeyName, "Error: HCaptcha API response unexpectedly null"); + return Page(); + } + + if (response.Success) + { + if (ModelState.IsValid) { - logger.LogWarning("User account locked out."); - return RedirectToPage("./Lockout"); + Microsoft.AspNetCore.Identity.SignInResult result; + if (Input.Email is null) + { + return RedirectToPage(Url.Content("~/"), new { ReturnUrl = returnUrl }); + } + EssentialCSharpWebUser? foundUser = await userManager.FindByEmailAsync(Input.Email); + if (Input.Password is null) + { + return RedirectToPage(Url.Content("~/"), new { ReturnUrl = returnUrl }); + } + if (foundUser is not null) + { + result = await signInManager.PasswordSignInAsync(foundUser, Input.Password, Input.RememberMe, lockoutOnFailure: true); + // Call the referral service to get the referral ID and set it onto the user claim + _ = await referralService.EnsureReferralIdAsync(foundUser); + } + else + { + result = await signInManager.PasswordSignInAsync(Input.Email, Input.Password, Input.RememberMe, lockoutOnFailure: true); + } + if (result.Succeeded) + { + logger.LogInformation("User logged in."); + return LocalRedirect(returnUrl); + } + if (result.RequiresTwoFactor) + { + return RedirectToPage("./LoginWith2fa", new { ReturnUrl = returnUrl, RememberMe = Input.RememberMe }); + } + if (result.IsLockedOut) + { + logger.LogWarning("User account locked out."); + return RedirectToPage("./Lockout"); + } + else + { + ModelState.AddModelError(string.Empty, "Invalid login attempt."); + return Page(); + } } - else + } + else + { + switch (response.ErrorCodes?.Length) { - ModelState.AddModelError(string.Empty, "Invalid login attempt."); - return Page(); + case 0: + throw new InvalidOperationException("The HCaptcha determined the passcode is not valid, and does not meet the security criteria"); + case > 1: + throw new InvalidOperationException("HCaptcha returned error codes: " + string.Join(", ", response.ErrorCodes)); + default: + { + if (response.ErrorCodes is null) + { + throw new InvalidOperationException("HCaptcha returned error codes unexpectedly null"); + } + if (HCaptchaErrorDetails.TryGetValue(response.ErrorCodes.Single(), out HCaptchaErrorDetails? details)) + { + switch (details.ErrorCode) + { + case HCaptchaErrorDetails.MissingInputResponse: + case HCaptchaErrorDetails.InvalidInputResponse: + case HCaptchaErrorDetails.InvalidOrAlreadySeenResponse: + ModelState.AddModelError(string.Empty, details.FriendlyDescription); + logger.LogInformation("HCaptcha returned error code: {ErrorDetails}", details.ToString()); + break; + case HCaptchaErrorDetails.BadRequest: + ModelState.AddModelError(string.Empty, details.FriendlyDescription); + logger.LogInformation("HCaptcha returned error code: {ErrorDetails}", details.ToString()); + break; + case HCaptchaErrorDetails.MissingInputSecret: + case HCaptchaErrorDetails.InvalidInputSecret: + case HCaptchaErrorDetails.NotUsingDummyPasscode: + case HCaptchaErrorDetails.SitekeySecretMismatch: + logger.LogCritical("HCaptcha returned error code: {ErrorDetails}", details.ToString()); + break; + default: + throw new InvalidOperationException("HCaptcha returned unknown error code: " + details?.ErrorCode); + } + } + else + { + throw new InvalidOperationException("HCaptcha returned unknown error code: " + response.ErrorCodes.Single()); + } + + break; + } + } } diff --git a/EssentialCSharp.Web/Areas/Identity/Pages/Account/ResendEmailConfirmation.cshtml b/EssentialCSharp.Web/Areas/Identity/Pages/Account/ResendEmailConfirmation.cshtml index d457b6b8..ce2c304f 100644 --- a/EssentialCSharp.Web/Areas/Identity/Pages/Account/ResendEmailConfirmation.cshtml +++ b/EssentialCSharp.Web/Areas/Identity/Pages/Account/ResendEmailConfirmation.cshtml @@ -16,11 +16,15 @@ +
+
+
@section Scripts { + } diff --git a/EssentialCSharp.Web/Areas/Identity/Pages/Account/ResendEmailConfirmation.cshtml.cs b/EssentialCSharp.Web/Areas/Identity/Pages/Account/ResendEmailConfirmation.cshtml.cs index 8ef99283..15c4400e 100644 --- a/EssentialCSharp.Web/Areas/Identity/Pages/Account/ResendEmailConfirmation.cshtml.cs +++ b/EssentialCSharp.Web/Areas/Identity/Pages/Account/ResendEmailConfirmation.cshtml.cs @@ -2,18 +2,22 @@ using System.Text; using System.Text.Encodings.Web; using EssentialCSharp.Web.Areas.Identity.Data; +using EssentialCSharp.Web.Models; +using EssentialCSharp.Web.Services; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity.UI.Services; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.AspNetCore.WebUtilities; +using Microsoft.Extensions.Options; namespace EssentialCSharp.Web.Areas.Identity.Pages.Account; [AllowAnonymous] -public class ResendEmailConfirmationModel(UserManager userManager, IEmailSender emailSender) : PageModel +public class ResendEmailConfirmationModel(UserManager userManager, IEmailSender emailSender, ICaptchaService captchaService, IOptions optionsAccessor) : PageModel { + public CaptchaOptions CaptchaOptions { get; } = optionsAccessor.Value; private InputModel? _Input; [BindProperty] public InputModel Input @@ -31,43 +35,108 @@ public class InputModel public async Task OnPostAsync() { + string? hCaptcha_response = Request.Form[CaptchaOptions.HttpPostResponseKeyName]; + if (!ModelState.IsValid) { return Page(); } - if (Input.Email is null) + if (hCaptcha_response is null) { - ModelState.AddModelError(string.Empty, "Error: Email is null. Please enter in an email"); + ModelState.AddModelError(CaptchaOptions.HttpPostResponseKeyName, HCaptchaErrorDetails.GetValue(HCaptchaErrorDetails.MissingInputResponse).FriendlyDescription); return Page(); } - EssentialCSharpWebUser? user = await userManager.FindByEmailAsync(Input.Email); - if (user is null) + HCaptchaResult? response = await captchaService.VerifyAsync(hCaptcha_response); + if (response is null) { - return NotFound($"Unable to load user with ID '{userManager.GetUserId(User)}'."); + ModelState.AddModelError(CaptchaOptions.HttpPostResponseKeyName, "Error: HCaptcha API response unexpectedly null"); + return Page(); } - string userId = await userManager.GetUserIdAsync(user); - string code = await userManager.GenerateEmailConfirmationTokenAsync(user); - code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code)); - string? callbackUrl = Url.Page( - "/Account/ConfirmEmail", - pageHandler: null, - values: new { userId = userId, code = code }, - protocol: Request.Scheme); - - if (callbackUrl is null) + if (response.Success) { - ModelState.AddModelError(string.Empty, "Error: callback url unexpectedly null."); + if (Input.Email is null) + { + ModelState.AddModelError(string.Empty, "Error: Email is null. Please enter in an email"); + return Page(); + } + + EssentialCSharpWebUser? user = await userManager.FindByEmailAsync(Input.Email); + if (user is null) + { + return NotFound($"Unable to load user with ID '{userManager.GetUserId(User)}'."); + } + + string userId = await userManager.GetUserIdAsync(user); + string code = await userManager.GenerateEmailConfirmationTokenAsync(user); + code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code)); + string? callbackUrl = Url.Page( + "/Account/ConfirmEmail", + pageHandler: null, + values: new { userId = userId, code = code }, + protocol: Request.Scheme); + + if (callbackUrl is null) + { + ModelState.AddModelError(string.Empty, "Error: callback url unexpectedly null."); + return Page(); + } + await emailSender.SendEmailAsync( + Input.Email, + "Confirm your email", + $"Please confirm your account by clicking here."); + + ModelState.AddModelError(string.Empty, "Verification email sent. Please check your email. If you can't find the email, please check your spam folder."); return Page(); } - await emailSender.SendEmailAsync( - Input.Email, - "Confirm your email", - $"Please confirm your account by clicking here."); + else + { + switch (response.ErrorCodes?.Length) + { + case 0: + throw new InvalidOperationException("The HCaptcha determined the passcode is not valid, and does not meet the security criteria"); + case > 1: + throw new InvalidOperationException("HCaptcha returned error codes: " + string.Join(", ", response.ErrorCodes)); + default: + { + if (response.ErrorCodes is null) + { + throw new InvalidOperationException("HCaptcha returned error codes unexpectedly null"); + } + if (HCaptchaErrorDetails.TryGetValue(response.ErrorCodes.Single(), out HCaptchaErrorDetails? details)) + { + switch (details.ErrorCode) + { + case HCaptchaErrorDetails.MissingInputResponse: + case HCaptchaErrorDetails.InvalidInputResponse: + case HCaptchaErrorDetails.InvalidOrAlreadySeenResponse: + ModelState.AddModelError(string.Empty, details.FriendlyDescription); + break; + case HCaptchaErrorDetails.BadRequest: + ModelState.AddModelError(string.Empty, details.FriendlyDescription); + break; + case HCaptchaErrorDetails.MissingInputSecret: + case HCaptchaErrorDetails.InvalidInputSecret: + case HCaptchaErrorDetails.NotUsingDummyPasscode: + case HCaptchaErrorDetails.SitekeySecretMismatch: + break; + default: + throw new InvalidOperationException("HCaptcha returned unknown error code: " + details?.ErrorCode); + } + } + else + { + throw new InvalidOperationException("HCaptcha returned unknown error code: " + response.ErrorCodes.Single()); + } + + break; + } + + } + } - ModelState.AddModelError(string.Empty, "Verification email sent. Please check your email. If you can't find the email, please check your spam folder."); return Page(); } } diff --git a/EssentialCSharp.Web/Areas/Identity/Pages/Account/ResetPassword.cshtml b/EssentialCSharp.Web/Areas/Identity/Pages/Account/ResetPassword.cshtml index e430d01e..23749f1d 100644 --- a/EssentialCSharp.Web/Areas/Identity/Pages/Account/ResetPassword.cshtml +++ b/EssentialCSharp.Web/Areas/Identity/Pages/Account/ResetPassword.cshtml @@ -27,11 +27,15 @@ +
+
+
@section Scripts { + } diff --git a/EssentialCSharp.Web/Areas/Identity/Pages/Account/ResetPassword.cshtml.cs b/EssentialCSharp.Web/Areas/Identity/Pages/Account/ResetPassword.cshtml.cs index 383de416..dcc1e30a 100644 --- a/EssentialCSharp.Web/Areas/Identity/Pages/Account/ResetPassword.cshtml.cs +++ b/EssentialCSharp.Web/Areas/Identity/Pages/Account/ResetPassword.cshtml.cs @@ -1,15 +1,19 @@ using System.ComponentModel.DataAnnotations; using System.Text; using EssentialCSharp.Web.Areas.Identity.Data; +using EssentialCSharp.Web.Models; +using EssentialCSharp.Web.Services; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.AspNetCore.WebUtilities; +using Microsoft.Extensions.Options; namespace EssentialCSharp.Web.Areas.Identity.Pages.Account; -public class ResetPasswordModel(UserManager userManager) : PageModel +public class ResetPasswordModel(UserManager userManager, ICaptchaService captchaService, IOptions optionsAccessor) : PageModel { + public CaptchaOptions CaptchaOptions { get; } = optionsAccessor.Value; private InputModel? _Input; [BindProperty] public InputModel Input @@ -57,44 +61,109 @@ public IActionResult OnGet(string? code = null) public async Task OnPostAsync() { + string? hCaptcha_response = Request.Form[CaptchaOptions.HttpPostResponseKeyName]; + if (!ModelState.IsValid) { return Page(); } - if (Input.Email is null) + if (hCaptcha_response is null) { - ModelState.AddModelError(string.Empty, "Error: Email is required."); - return RedirectToPage(); + ModelState.AddModelError(CaptchaOptions.HttpPostResponseKeyName, HCaptchaErrorDetails.GetValue(HCaptchaErrorDetails.MissingInputResponse).FriendlyDescription); + return Page(); } - EssentialCSharpWebUser? user = await userManager.FindByEmailAsync(Input.Email); - if (user is null) + + HCaptchaResult? response = await captchaService.VerifyAsync(hCaptcha_response); + if (response is null) { - // Don't reveal that the user does not exist - return RedirectToPage("./ResetPasswordConfirmation"); + ModelState.AddModelError(CaptchaOptions.HttpPostResponseKeyName, "Error: HCaptcha API response unexpectedly null"); + return Page(); } - if (Input.Password is null) + if (response.Success) { - ModelState.AddModelError(string.Empty, "Error: Password is required."); - return RedirectToPage(); + if (Input.Email is null) + { + ModelState.AddModelError(string.Empty, "Error: Email is required."); + return RedirectToPage(); + } + EssentialCSharpWebUser? user = await userManager.FindByEmailAsync(Input.Email); + if (user is null) + { + // Don't reveal that the user does not exist + return RedirectToPage("./ResetPasswordConfirmation"); + } + + if (Input.Password is null) + { + ModelState.AddModelError(string.Empty, "Error: Password is required."); + return RedirectToPage(); + } + if (Input.Code is null) + { + ModelState.AddModelError(string.Empty, "Error: Code is required."); + return RedirectToPage(); + } + + IdentityResult result = await userManager.ResetPasswordAsync(user, Input.Code, Input.Password); + if (result.Succeeded) + { + return RedirectToPage("./ResetPasswordConfirmation"); + } + + foreach (IdentityError error in result.Errors) + { + ModelState.AddModelError(string.Empty, error.Description); + } + return Page(); } - if (Input.Code is null) + else { - ModelState.AddModelError(string.Empty, "Error: Code is required."); - return RedirectToPage(); - } + switch (response.ErrorCodes?.Length) + { + case 0: + throw new InvalidOperationException("The HCaptcha determined the passcode is not valid, and does not meet the security criteria"); + case > 1: + throw new InvalidOperationException("HCaptcha returned error codes: " + string.Join(", ", response.ErrorCodes)); + default: + { + if (response.ErrorCodes is null) + { + throw new InvalidOperationException("HCaptcha returned error codes unexpectedly null"); + } + if (HCaptchaErrorDetails.TryGetValue(response.ErrorCodes.Single(), out HCaptchaErrorDetails? details)) + { + switch (details.ErrorCode) + { + case HCaptchaErrorDetails.MissingInputResponse: + case HCaptchaErrorDetails.InvalidInputResponse: + case HCaptchaErrorDetails.InvalidOrAlreadySeenResponse: + ModelState.AddModelError(string.Empty, details.FriendlyDescription); + break; + case HCaptchaErrorDetails.BadRequest: + ModelState.AddModelError(string.Empty, details.FriendlyDescription); + break; + case HCaptchaErrorDetails.MissingInputSecret: + case HCaptchaErrorDetails.InvalidInputSecret: + case HCaptchaErrorDetails.NotUsingDummyPasscode: + case HCaptchaErrorDetails.SitekeySecretMismatch: + break; + default: + throw new InvalidOperationException("HCaptcha returned unknown error code: " + details?.ErrorCode); + } + } + else + { + throw new InvalidOperationException("HCaptcha returned unknown error code: " + response.ErrorCodes.Single()); + } - IdentityResult result = await userManager.ResetPasswordAsync(user, Input.Code, Input.Password); - if (result.Succeeded) - { - return RedirectToPage("./ResetPasswordConfirmation"); - } + break; + } - foreach (IdentityError error in result.Errors) - { - ModelState.AddModelError(string.Empty, error.Description); + } } + return Page(); } } diff --git a/EssentialCSharp.Web/EssentialCSharp.Web.csproj b/EssentialCSharp.Web/EssentialCSharp.Web.csproj index 8b2ebfb0..4cb85e02 100644 --- a/EssentialCSharp.Web/EssentialCSharp.Web.csproj +++ b/EssentialCSharp.Web/EssentialCSharp.Web.csproj @@ -1,6 +1,6 @@  - net8.0 + net9.0 diff --git a/global.json b/global.json index 65b01979..e7673427 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "8.0.117", + "version": "9.0.101", "rollForward": "latestMinor" } } \ No newline at end of file diff --git a/packages-microsoft-prod.deb b/packages-microsoft-prod.deb deleted file mode 100644 index 40874bc7face2e6f3b47dbd8eaff9cca5ab8ca6c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3690 zcmai12{aUJ+g1o8yX*=HWt$m>k-}gsW1BISF(Jz^3$vIR23ewb%Th0jEa6o|D3r<; zAreW+lC`2DTe60k`Fr2a|NrNF=ljlgJ?Gr#KF@RC=XuV#&U0PoRz2lU4XE}(pOOds{B8G=f9(oq2WIVzvJJj9{{We1O!qkOgc4GkLgd>!$<4?*M1P# z$oTgSet;vXf;?hErvrF+m%PZ-aU=15JUgWl8yX_Mz54X-no`sL>xkk5WG- zm0_K5J1?i28+#3BnmyC$Y^phWR{*Gkn(a6m)oglLch_%)X7~xX$bk%VoAAcpCq@~G zZRz1rN==-77k6csJ``sFgwFFmYiPVb^=O5Q!bIq5RnD4qKO(b_b}Qa%UDzq~jmKXF z>#io<=dR%+UY1}RU{;Zd35j`~@GVvr=VdqBI?U_U6^3~Z&y4F7}vV#`VX|*=H0+&#FL;~h zVNIICI$g84Ue$oi%5|+jwLeYc2H-h3IiuulK?#i9AUVJ9r` zAocZByGcu3;lKe*guUhaYxJwJq3^l(6rU{YPOo*dxmtpD)@m0^MsM9K+TOQ$TA>N` zAy7%8Xon|z((W%_KJ{{+xusXgpfxc!L7Vc2D_})vI8jM_Q4{4@N1Xeqt1R@=9tlv$UKft0C&NmU+Xp7(}zq(YWN8I#bgx8`u4QI?9V)86V}r6efAgSpLS>?`T;0c`>$$$Ntyr5}}_RH3=0R@0Y#H z-k)?4ye(1n@~;X;f}qbwwT&oI+2-SoJBMN}G>GpkdU?0-`GSYCvu*RxMoFaO7=A(5 zXRpIQM;(5J{dg{?dIKVE_1fIc8}^IH(kn6tee^30Co8r`A2G^;oyHDBHX18ZPApAs zD1eVEDp#JP0F{GZ71o>RuRTAz7QRQ=y1H~N-q5(LN2@RlI1Z?3ke{p24gVwBQKs+p z#?phJT;7uBNR4B2qs50xJfA}3JuVuo^mBB(-c+I;K%NCYHepR~T0zN*`zW*Ekq61C zX{w0vi7sz`&hm70OF^r6W7$|pLsy)IFk1A9Ls?srhXCR3TFO}hL^=vh(oLM^#(az; zx98tK6_{JXl)W7rUFYI#R!>%_jc0C|mrR5l8DEJXX17Y&zM115;eEUkfAYeZ$-4Ra%iH}QoHELM1f)Uqg{iQ46`Ls;zyVncXlc}e1^O`6Ya+DytKw{4 zsN^8>9~~q=cG^`F(#hXVmcFVv)KXZa#{+is+ZuBeC$U0&71L7FnPAb z>B;+`fmpWuZHsvHK$SUM&h8juwO$Us<{qmkIyrJyC&%GDr>vNl9eyKIT}v(P%e=ki z5cA>J*92*dMh3II^^*N&^+rx_XMl|QVpGw3&;{@KLka+0Tf5ROsW%lkN!G=8E!)Ev zlPnbShS7ioi>q245)din`|XMM)tURP%J=9qs&)0Pk6SVVIZ;E1Z-wt6xap+cyWnhG!&(1o zJI%TuMep29aaXn7D1ts`qvl7MEg|-_&^D9hDY#;xHcl+~h~9)mEq-kBR^gj297vz` zlu}SS#yS|?qaV_~Dik#>6WKRP-s;)}6V@Sz;hL_{$4_M!{%7#sX$SwPWQRx%U+LYjhMd zZ)iH?>XX$U1s+S0GI1SqXoUv$ z!4%x|IYVUxTwhx9&e_Qte~u24da)De{U0ZPSMQS(iZ{k=_C^&z3BA z<9Ue|LcFMK;i6Yi#pN?-c^(0sd~8iBBMv_~^6}2wuE~san{-C6%qZ2%+sH&$a@t+G zDZ6HJ8;H6l9cQrpW6H^yCE1^h@tnTy=@>rRt^v)dt`V-cQidmz<5Wwv&9935oUi!Q zBAFvSG$ZmwzKA_+ap&EOsluSXpS>HAExl`<+F{QjdqbsC9ZJH@x6LW*-A7K~_4LnW zBDuD%GV#SCz!xCoESJ7#t0Az_dYtgC=&g$S*j3cPh1DaMmD0Gm7iHM>a?XK*Q(|EQ z7dBf34{(fymNqoikeQusT0{7ZUth3P4alJ?_}+YcJU+dSwnt`NNplc%UQd zz=VaoxUNf5hO%szP(k;lgi3MOAxzzByWlKDw)xbvYk_)$q~nK6?nLPe)HV3=)Hyud zJ_}zTaindnnIB9IQsL$&4)3u1>V3Q5TN^n&LB+ow?K9ZQ8ceu3#9jKr^}`$xhOw!D zw!P^OWg#3-$sK|uIwbnb`Tjlq48+*>SuMO#8&|_mbuiA;LYHw^7&sx$&)#WXE;&>g1m}Bx~|qCd+iBjh;GAnRWTfdd`T^LhOA-xBsCBF`F`Fa87#!AEiAE@ zezWS^>@48_;&Zfd_h0)Hlhl5;PL1cQZ17zkkV|05`XQw6+}Q2IQEtOjZ|Kj`V{`8b z@;lAH(mjB!Ax}&nf2eSjeG??Hq zPiar!?7aPt3uOY_aErh=4NF|NW=Sg`!%L!hF2>3&5hq( zvxZ}1p14JPAGx%L8&g}c!^PYUf6`9r1hf`Q)sG8obLB$U%h}VfHdJnIx3YJ{y12ma zt@b<%=50G-H5^`^|A46&&b||mP_jJ8Uru%wPs3B`L_Cr5Yi#%hkdYzums^2MAph%X z|1vq4fd8rcdvehI8xT0Bq2VP#XjNNO~xiYJfyWGXjY2My^a-n7+eSQHfFj03Zvp@I5LI27y!CfhhV1wu^Fp$K|dfQcQ-9vo;G9S|LeG;|RGMBwe5 zNC7Zg7qq(}&C5L?5E{nvpn$xb(O8s$i={Tj6&gu0z>=ZvCPDaUD4nh!O(enmqgi-Q zR1}%+AOZ-YvApO+k`oe(CmB(3WG3DKZHS`LEg``a5{M8+z&W@$Q=DN|BnQioASWo$ io{6<&5v{^NmOu;)c3K2*nnp!nM1Io=ceehwWd0A>I)OX@