Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -155,8 +155,6 @@ csharp_style_prefer_top_level_statements = true:silent
csharp_style_expression_bodied_lambdas = true:silent
csharp_style_expression_bodied_local_functions = false:silent

# CA1848: Use the LoggerMessage delegates
dotnet_diagnostic.CA1848.severity = suggestion
# Test files - allow underscore-separated test method names (CA1707)
[{EssentialCSharp.Web.Tests,EssentialCSharp.Chat.Tests}/**]
dotnet_diagnostic.CA1707.severity = none
7 changes: 5 additions & 2 deletions EssentialCSharp.Chat.Shared/Services/AISearchService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

namespace EssentialCSharp.Chat.Common.Services;

public class AISearchService(
public partial class AISearchService(
VectorStore vectorStore,
EmbeddingService embeddingService,
ILogger<AISearchService> logger)
Expand Down Expand Up @@ -49,10 +49,13 @@ public async Task<IReadOnlyList<VectorSearchResult<BookContentChunk>>> ExecuteVe
// needed (clearing would evict all healthy connections, hurting concurrent users).
// The retry opens a fresh physical connection, which calls UsePasswordProvider
// and gets a new token from DefaultAzureCredential.
logger.LogWarning(ex, "Entra ID token expired on pooled connection (SqlState 28000); retrying once.");
LogEntraIdTokenExpired(logger, ex);
}
}

throw new UnreachableException("Retry loop exited without returning or throwing.");
}

[LoggerMessage(Level = LogLevel.Warning, Message = "Entra ID token expired on pooled connection (SqlState 28000); retrying once.")]
private static partial void LogEntraIdTokenExpired(ILogger<AISearchService> logger, Exception exception);
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public async Task<List<FileChunkingResult>> ProcessMarkdownFilesAsync(
// Validate input parameters
if (!directory.Exists)
{
logger.LogError("Error: Directory {DirectoryName} does not exist.", directory.FullName);
LogDirectoryDoesNotExist(logger, directory.FullName);
throw new InvalidOperationException($"Error: Directory '{directory.FullName}' does not exist.");
}

Expand Down Expand Up @@ -177,4 +177,7 @@ public FileChunkingResult ProcessSingleMarkdownFile(

[GeneratedRegex(@"^(#{1,6}) +(.+)$")]
private static partial Regex HeadingRegex();

[LoggerMessage(Level = LogLevel.Error, Message = "Directory {DirectoryName} does not exist.")]
private static partial void LogDirectoryDoesNotExist(ILogger<MarkdownChunkingService> logger, string directoryName);
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
namespace EssentialCSharp.Web.Areas.Identity.Pages.Account;

[AllowAnonymous]
public class ExternalLoginModel(
public partial class ExternalLoginModel(
SignInManager<EssentialCSharpWebUser> signInManager,
UserManager<EssentialCSharpWebUser> userManager,
IUserStore<EssentialCSharpWebUser> userStore,
Expand Down Expand Up @@ -78,7 +78,7 @@ public async Task<IActionResult> OnGetCallbackAsync(string? returnUrl = null, st
Microsoft.AspNetCore.Identity.SignInResult result = await signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, isPersistent: false, bypassTwoFactor: true);
if (result.Succeeded)
{
logger.LogInformation("{Name} logged in with {LoginProvider} provider.", info.Principal.Identity?.Name, info.LoginProvider);
LogUserLoggedInWithProvider(logger, info.Principal.Identity?.Name, info.LoginProvider);
// Ensure referral ID is set for the user
var user = await userManager.FindByLoginAsync(info.LoginProvider, info.ProviderKey);
if (user != null)
Expand Down Expand Up @@ -140,7 +140,7 @@ public async Task<IActionResult> OnPostConfirmationAsync(string? returnUrl = nul
result = await userManager.AddLoginAsync(user, info);
if (result.Succeeded)
{
logger.LogInformation("User created an account using {Name} provider.", info.LoginProvider);
LogUserCreatedWithProvider(logger, info.LoginProvider);
return await SendConfirmationEmail(returnUrl, info, user);
}
}
Expand Down Expand Up @@ -214,4 +214,10 @@ private EssentialCSharpWebUser CreateUser()
$"override the external login page in /Areas/Identity/Pages/Account/ExternalLogin.cshtml", innerException);
}
}

[LoggerMessage(Level = LogLevel.Information, Message = "{Name} logged in with {LoginProvider} provider.")]
private static partial void LogUserLoggedInWithProvider(ILogger<ExternalLoginModel> logger, string? name, string loginProvider);

[LoggerMessage(Level = LogLevel.Information, Message = "User created an account using {Name} provider.")]
private static partial void LogUserCreatedWithProvider(ILogger<ExternalLoginModel> logger, string name);
}
12 changes: 9 additions & 3 deletions EssentialCSharp.Web/Areas/Identity/Pages/Account/Login.cshtml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

namespace EssentialCSharp.Web.Areas.Identity.Pages.Account;

public class LoginModel(SignInManager<EssentialCSharpWebUser> signInManager, UserManager<EssentialCSharpWebUser> userManager, ILogger<LoginModel> logger, IReferralService referralService, ICaptchaService captchaService, IOptions<CaptchaOptions> optionsAccessor) : PageModel
public partial class LoginModel(SignInManager<EssentialCSharpWebUser> signInManager, UserManager<EssentialCSharpWebUser> userManager, ILogger<LoginModel> logger, IReferralService referralService, ICaptchaService captchaService, IOptions<CaptchaOptions> optionsAccessor) : PageModel
{
private InputModel? _Input;
[BindProperty]
Expand Down Expand Up @@ -102,7 +102,7 @@ public async Task<IActionResult> OnPostAsync(string? returnUrl = null)
}
if (result.Succeeded)
{
logger.LogInformation("User logged in.");
LogUserLoggedIn(logger);
return LocalRedirect(returnUrl);
}
if (result.RequiresTwoFactor)
Expand All @@ -111,7 +111,7 @@ public async Task<IActionResult> OnPostAsync(string? returnUrl = null)
}
if (result.IsLockedOut)
{
logger.LogWarning("User account locked out.");
LogUserAccountLockedOut(logger);
return RedirectToPage("./Lockout");
}
else
Expand All @@ -124,4 +124,10 @@ public async Task<IActionResult> OnPostAsync(string? returnUrl = null)
// If we got this far, something failed, redisplay form
return Page();
}

[LoggerMessage(Level = LogLevel.Information, Message = "User logged in.")]
private static partial void LogUserLoggedIn(ILogger<LoginModel> logger);

[LoggerMessage(Level = LogLevel.Warning, Message = "User account locked out.")]
private static partial void LogUserAccountLockedOut(ILogger<LoginModel> logger);
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

namespace EssentialCSharp.Web.Areas.Identity.Pages.Account;

public class LoginWith2faModel(
public partial class LoginWith2faModel(
SignInManager<EssentialCSharpWebUser> signInManager,
UserManager<EssentialCSharpWebUser> userManager,
ILogger<LoginWith2faModel> logger) : PageModel
Expand Down Expand Up @@ -68,19 +68,28 @@ public async Task<IActionResult> OnPostAsync(bool rememberMe, string? returnUrl

if (result.Succeeded)
{
logger.LogInformation("User with ID '{UserId}' logged in with 2fa.", user.Id);
LogUserLoggedInWith2fa(logger, user.Id);
return LocalRedirect(returnUrl);
}
else if (result.IsLockedOut)
{
logger.LogWarning("User with ID '{UserId}' account locked out.", user.Id);
LogUserAccountLockedOut2fa(logger, user.Id);
return RedirectToPage("./Lockout");
}
else
{
logger.LogWarning("Invalid authenticator code entered for user with ID '{UserId}'.", user.Id);
LogInvalidAuthenticatorCode(logger, user.Id);
ModelState.AddModelError(string.Empty, "Invalid authenticator code.");
return Page();
}
}

[LoggerMessage(Level = LogLevel.Information, Message = "User with ID '{UserId}' logged in with 2fa.")]
private static partial void LogUserLoggedInWith2fa(ILogger<LoginWith2faModel> logger, string userId);

[LoggerMessage(Level = LogLevel.Warning, Message = "User with ID '{UserId}' account locked out.")]
private static partial void LogUserAccountLockedOut2fa(ILogger<LoginWith2faModel> logger, string userId);

[LoggerMessage(Level = LogLevel.Warning, Message = "Invalid authenticator code entered for user with ID '{UserId}'.")]
private static partial void LogInvalidAuthenticatorCode(ILogger<LoginWith2faModel> logger, string userId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,8 @@
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace EssentialCSharp.Web.Areas.Identity.Pages.Account;

public class LoginWithRecoveryCodeModel(
public partial class LoginWithRecoveryCodeModel(
SignInManager<EssentialCSharpWebUser> signInManager,
UserManager<EssentialCSharpWebUser> userManager,
ILogger<LoginWithRecoveryCodeModel> logger) : PageModel
{
private InputModel? _Input;
Expand Down Expand Up @@ -55,23 +54,30 @@ public async Task<IActionResult> OnPostAsync(string? returnUrl = null)

Microsoft.AspNetCore.Identity.SignInResult result = await signInManager.TwoFactorRecoveryCodeSignInAsync(recoveryCode);

string userId = await userManager.GetUserIdAsync(user);

if (result.Succeeded)
{
logger.LogInformation("User with ID '{UserId}' logged in with a recovery code.", user.Id);
LogUserLoggedInWithRecoveryCode(logger, user.Id);
return LocalRedirect(returnUrl ?? Url.Content("~/"));
}
if (result.IsLockedOut)
{
logger.LogWarning("User account locked out.");
LogUserAccountLockedOutRecovery(logger);
return RedirectToPage("./Lockout");
}
else
{
logger.LogWarning("Invalid recovery code entered for user with ID '{UserId}' ", user.Id);
LogInvalidRecoveryCode(logger, user.Id);
ModelState.AddModelError(string.Empty, "Invalid recovery code entered.");
return Page();
}
}

[LoggerMessage(Level = LogLevel.Information, Message = "User with ID '{UserId}' logged in with a recovery code.")]
private static partial void LogUserLoggedInWithRecoveryCode(ILogger<LoginWithRecoveryCodeModel> logger, string userId);

[LoggerMessage(Level = LogLevel.Warning, Message = "User account locked out.")]
private static partial void LogUserAccountLockedOutRecovery(ILogger<LoginWithRecoveryCodeModel> logger);

[LoggerMessage(Level = LogLevel.Warning, Message = "Invalid recovery code entered for user with ID '{UserId}'.")]
private static partial void LogInvalidRecoveryCode(ILogger<LoginWithRecoveryCodeModel> logger, string userId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,17 @@

namespace EssentialCSharp.Web.Areas.Identity.Pages.Account;

public class LogoutModel(SignInManager<EssentialCSharpWebUser> signInManager, ILogger<LogoutModel> logger) : PageModel
public partial class LogoutModel(SignInManager<EssentialCSharpWebUser> signInManager, ILogger<LogoutModel> logger) : PageModel
{
public async Task<IActionResult> OnPost(string? returnUrl = null)
{
await signInManager.SignOutAsync();
logger.LogInformation("User logged out.");
// This needs to be a redirect so that the browser performs a new
// request and the identity for the user gets updated.
return returnUrl is not null ? LocalRedirect(returnUrl) : RedirectToPage();
LogUserLoggedOut(logger);
// This needs to be a redirect so that the browser performs a new
// request and the identity for the user gets updated.
return returnUrl is not null ? LocalRedirect(returnUrl) : RedirectToPage();
}

[LoggerMessage(Level = LogLevel.Information, Message = "User logged out.")]
private static partial void LogUserLoggedOut(ILogger<LogoutModel> logger);
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

namespace EssentialCSharp.Web.Areas.Identity.Pages.Account.Manage;

public class ChangePasswordModel(
public partial class ChangePasswordModel(
UserManager<EssentialCSharpWebUser> userManager,
SignInManager<EssentialCSharpWebUser> signInManager,
ILogger<ChangePasswordModel> logger) : PageModel
Expand Down Expand Up @@ -106,9 +106,12 @@ public async Task<IActionResult> OnPostAsync()
}

await signInManager.RefreshSignInAsync(user);
logger.LogInformation("User changed their password successfully.");
LogUserChangedPassword(logger);
StatusMessage = "Your password has been changed.";

return RedirectToPage();
}

[LoggerMessage(Level = LogLevel.Information, Message = "User changed their password successfully.")]
private static partial void LogUserChangedPassword(ILogger<ChangePasswordModel> logger);
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

namespace EssentialCSharp.Web.Areas.Identity.Pages.Account.Manage;

public class DeletePersonalDataModel(
public partial class DeletePersonalDataModel(
UserManager<EssentialCSharpWebUser> userManager,
SignInManager<EssentialCSharpWebUser> signInManager,
ILogger<DeletePersonalDataModel> logger) : PageModel
Expand Down Expand Up @@ -79,8 +79,11 @@ public async Task<IActionResult> OnPostAsync()

await signInManager.SignOutAsync();

logger.LogInformation("User with ID '{UserId}' deleted themselves.", userId);
LogUserDeletedThemselves(logger, userId);

return Redirect("~/");
}

[LoggerMessage(Level = LogLevel.Information, Message = "User with ID '{UserId}' deleted themselves.")]
private static partial void LogUserDeletedThemselves(ILogger<DeletePersonalDataModel> logger, string userId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

namespace EssentialCSharp.Web.Areas.Identity.Pages.Account.Manage;

public class Disable2faModel(
public partial class Disable2faModel(
UserManager<EssentialCSharpWebUser> userManager,
ILogger<Disable2faModel> logger) : PageModel
{
Expand Down Expand Up @@ -42,8 +42,11 @@ public async Task<IActionResult> OnPostAsync()
throw new InvalidOperationException($"Unexpected error occurred disabling 2FA.");
}

logger.LogInformation("User with ID '{UserId}' has disabled 2fa.", userManager.GetUserId(User));
LogUserDisabled2fa(logger, userManager.GetUserId(User));
StatusMessage = "2fa has been disabled. You can reenable 2fa when you setup an authenticator app";
return RedirectToPage("./TwoFactorAuthentication");
}

[LoggerMessage(Level = LogLevel.Information, Message = "User with ID '{UserId}' has disabled 2fa.")]
private static partial void LogUserDisabled2fa(ILogger<Disable2faModel> logger, string? userId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

namespace EssentialCSharp.Web.Areas.Identity.Pages.Account.Manage;

public class DownloadPersonalDataModel(
public partial class DownloadPersonalDataModel(
UserManager<EssentialCSharpWebUser> userManager,
ILogger<DownloadPersonalDataModel> logger) : PageModel
{
Expand All @@ -23,7 +23,7 @@ public async Task<IActionResult> OnPostAsync()
return NotFound($"Unable to load user with ID '{userManager.GetUserId(User)}'.");
}

logger.LogInformation("User with ID '{UserId}' asked for their personal data.", userManager.GetUserId(User));
LogUserAskedForPersonalData(logger, userManager.GetUserId(User));

// Only include personal data for download
var personalData = new Dictionary<string, string>();
Expand All @@ -48,4 +48,7 @@ public async Task<IActionResult> OnPostAsync()
Response.Headers.Append("Content-Disposition", "attachment; filename=PersonalData.json");
return new FileContentResult(JsonSerializer.SerializeToUtf8Bytes(personalData), "application/json");
}

[LoggerMessage(Level = LogLevel.Information, Message = "User with ID '{UserId}' asked for their personal data.")]
private static partial void LogUserAskedForPersonalData(ILogger<DownloadPersonalDataModel> logger, string? userId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

namespace EssentialCSharp.Web.Areas.Identity.Pages.Account.Manage;

public class EnableAuthenticatorModel(
public partial class EnableAuthenticatorModel(
UserManager<EssentialCSharpWebUser> userManager,
ILogger<EnableAuthenticatorModel> logger,
UrlEncoder urlEncoder) : PageModel
Expand Down Expand Up @@ -90,7 +90,7 @@ public async Task<IActionResult> OnPostAsync()

await userManager.SetTwoFactorEnabledAsync(user, true);
string userId = await userManager.GetUserIdAsync(user);
logger.LogInformation("User with ID '{UserId}' has enabled 2FA with an authenticator app.", userId);
LogUserEnabled2fa(logger, userId);

StatusMessage = "Your authenticator app has been verified.";

Expand Down Expand Up @@ -159,4 +159,7 @@ private string GenerateQrCodeUri(string email, string unformattedKey)
urlEncoder.Encode(email),
unformattedKey);
}

[LoggerMessage(Level = LogLevel.Information, Message = "User with ID '{UserId}' has enabled 2FA with an authenticator app.")]
private static partial void LogUserEnabled2fa(ILogger<EnableAuthenticatorModel> logger, string userId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

namespace EssentialCSharp.Web.Areas.Identity.Pages.Account.Manage;

public class GenerateRecoveryCodesModel(
public partial class GenerateRecoveryCodesModel(
UserManager<EssentialCSharpWebUser> userManager,
ILogger<GenerateRecoveryCodesModel> logger) : PageModel
{
Expand Down Expand Up @@ -50,8 +50,11 @@ public async Task<IActionResult> OnPostAsync()
IEnumerable<string>? recoveryCodes = await userManager.GenerateNewTwoFactorRecoveryCodesAsync(user, 10);
RecoveryCodes = recoveryCodes?.ToArray();

logger.LogInformation("User with ID '{UserId}' has generated new 2FA recovery codes.", userId);
LogUserGeneratedRecoveryCodes(logger, userId);
StatusMessage = "You have generated new recovery codes.";
return RedirectToPage("./ShowRecoveryCodes");
}

[LoggerMessage(Level = LogLevel.Information, Message = "User with ID '{UserId}' has generated new 2FA recovery codes.")]
private static partial void LogUserGeneratedRecoveryCodes(ILogger<GenerateRecoveryCodesModel> logger, string userId);
}
Loading
Loading