diff --git a/Directory.Packages.props b/Directory.Packages.props index 806b335..29f3f19 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -7,7 +7,7 @@ - + diff --git a/src/WebApp/Pages/Account/ExternalLogin.cshtml.cs b/src/WebApp/Pages/Account/ExternalLogin.cshtml.cs index 8b33c94..77d73f5 100644 --- a/src/WebApp/Pages/Account/ExternalLogin.cshtml.cs +++ b/src/WebApp/Pages/Account/ExternalLogin.cshtml.cs @@ -8,7 +8,6 @@ using MyApp.AppServices.Staff.Dto; using MyApp.Domain.Identity; using MyApp.WebApp.Models; -using MyApp.WebApp.Platform.Logging; using MyApp.WebApp.Platform.PageModelHelpers; using MyApp.WebApp.Platform.Settings; @@ -67,7 +66,9 @@ private async Task SignInAsLocalUser() var user = await userManager.FindByIdAsync(staffId); logger.LogInformation("Local user with ID {StaffId} signed in", staffId); - await signInManager.SignInAsync(user!, false); + user!.MostRecentLogin = DateTimeOffset.Now; + await userManager.UpdateAsync(user); + await signInManager.SignInAsync(user, false); return LocalRedirectOrHome(); } @@ -92,19 +93,19 @@ public async Task OnGetCallbackAsync(string? returnUrl = null, st if (!userEmail.IsValidEmailDomain()) { - logger.LogWarning("User {UserName} with invalid email domain attempted signin", userEmail.MaskEmail()); + logger.LogWarning("User with invalid email domain attempted signin"); return RedirectToPage("./Unavailable"); } - logger.LogInformation("User {UserName} in tenant {TenantID} successfully authenticated", userEmail.MaskEmail(), - userTenant); + logger.LogInformation("User with object ID {ObjectId} in tenant {TenantID} successfully authenticated", + externalLoginInfo.Principal.GetObjectId(), userTenant); // Determine if a user account already exists with the Object ID. // If not, then determine if a user account already exists with the given username. var user = AppSettings.DevSettings.UseInMemoryData ? await userManager.FindByNameAsync(userEmail) - : await userManager.Users.SingleOrDefaultAsync(u => - u.ObjectIdentifier == externalLoginInfo.Principal.GetObjectId()) ?? + : await userManager.Users.SingleOrDefaultAsync(au => + au.ObjectIdentifier == externalLoginInfo.Principal.GetObjectId()) ?? await userManager.FindByNameAsync(userEmail); // If the user does not have a local account yet, then create one and sign in. @@ -114,7 +115,7 @@ public async Task OnGetCallbackAsync(string? returnUrl = null, st // If user has been marked as inactive, don't sign in. if (!user.Active) { - logger.LogWarning("Inactive user {Email} attempted signin", userEmail.MaskEmail()); + logger.LogWarning("Inactive user with object ID {ObjectId} attempted signin", user.ObjectIdentifier); return RedirectToPage("./Unavailable"); } @@ -163,25 +164,24 @@ private async Task CreateUserAndSignInAsync(ExternalLoginInfo inf var createUserResult = await userManager.CreateAsync(user); if (!createUserResult.Succeeded) { - logger.LogWarning("Failed to create new user {UserName}", user.Email.MaskEmail()); + logger.LogWarning("Failed to create new user with object ID {ObjectId}", user.ObjectIdentifier); return await FailedLoginAsync(createUserResult, user); } - logger.LogInformation("Created new user {Email} with object ID {ObjectId}", user.Email.MaskEmail(), - user.ObjectIdentifier); + logger.LogInformation("Created new user with object ID {ObjectId}", user.ObjectIdentifier); // Add new user to application Roles if seeded in app settings or local admin user setting is enabled. var seedAdminUsers = configuration.GetSection("SeedAdminUsers").Get(); if (AppSettings.DevSettings.LocalUserIsStaff) { - logger.LogInformation("Seeding staff role for new user {Email}", user.Email.MaskEmail()); + logger.LogInformation("Seeding staff role for new user with object ID {ObjectId}", user.ObjectIdentifier); await userManager.AddToRoleAsync(user, RoleName.Staff); } if (AppSettings.DevSettings.LocalUserIsAdmin || (seedAdminUsers != null && seedAdminUsers.Contains(user.Email, StringComparer.InvariantCultureIgnoreCase))) { - logger.LogInformation("Seeding all roles for new user {Email}", user.Email.MaskEmail()); + logger.LogInformation("Seeding all roles for new user with object ID {ObjectId}", user.ObjectIdentifier); foreach (var role in AppRole.AllRoles) await userManager.AddToRoleAsync(user, role.Key); } @@ -194,8 +194,8 @@ private async Task CreateUserAndSignInAsync(ExternalLoginInfo inf // Update local store with from external provider. private async Task RefreshUserInfoAndSignInAsync(ApplicationUser user, ExternalLoginInfo info) { - logger.LogInformation("Existing user {Email} logged in with {LoginProvider} provider", - user.Email.MaskEmail(), info.LoginProvider); + logger.LogInformation("Existing user with object ID {ObjectId} logged in with {LoginProvider} provider", + user.ObjectIdentifier, info.LoginProvider); var previousValues = new ApplicationUser { @@ -218,7 +218,6 @@ private async Task RefreshUserInfoAndSignInAsync(ApplicationUser } await userManager.UpdateAsync(user); - await signInManager.RefreshSignInAsync(user); return LocalRedirectOrHome(); } @@ -231,8 +230,8 @@ private async Task AddLoginProviderAndSignInAsync( if (!addLoginResult.Succeeded) { - logger.LogWarning("Failed to add login provider {LoginProvider} for user {Email}", - info.LoginProvider, user.Email.MaskEmail()); + logger.LogWarning("Failed to add login provider {LoginProvider} for user with object ID {ObjectId}", + info.LoginProvider, user.ObjectIdentifier); return await FailedLoginAsync(addLoginResult, user); } @@ -240,8 +239,8 @@ private async Task AddLoginProviderAndSignInAsync( user.MostRecentLogin = DateTimeOffset.Now; await userManager.UpdateAsync(user); - logger.LogInformation("Login provider {LoginProvider} added for user {Email} with object ID {ObjectId}", - info.LoginProvider, user.Email.MaskEmail(), user.ObjectIdentifier); + logger.LogInformation("Login provider {LoginProvider} added for user with object ID {ObjectId}", + info.LoginProvider, user.ObjectIdentifier); // Include the access token in the properties. var props = new AuthenticationProperties(); @@ -262,9 +261,6 @@ private async Task FailedLoginAsync(IdentityResult result, Applicati return Page(); } - private IActionResult LocalRedirectOrHome() - { - if (ReturnUrl is null) return RedirectToPage("/Staff/Index"); - return LocalRedirect(ReturnUrl); - } + private IActionResult LocalRedirectOrHome() => + ReturnUrl is null ? RedirectToPage("/Staff/Index") : LocalRedirect(ReturnUrl); } diff --git a/src/WebApp/Platform/AppConfiguration/MigratorHostedService.cs b/src/WebApp/Platform/AppConfiguration/MigratorHostedService.cs index 7b4a200..1c6f8be 100644 --- a/src/WebApp/Platform/AppConfiguration/MigratorHostedService.cs +++ b/src/WebApp/Platform/AppConfiguration/MigratorHostedService.cs @@ -17,10 +17,16 @@ public async Task StartAsync(CancellationToken cancellationToken) if (AppSettings.DevSettings.UseInMemoryData) return; var migrationConnectionString = configuration.GetConnectionString("MigrationConnection"); - var migrationOptions = new DbContextOptionsBuilder() - .UseSqlServer(migrationConnectionString, builder => builder.MigrationsAssembly("EfRepository")).Options; + var dbContextOptionsBuilder = new DbContextOptionsBuilder() + .UseSqlServer(migrationConnectionString, builder => builder.MigrationsAssembly("EfRepository")); - await using var migrationContext = new AppDbContext(migrationOptions); + if (AppSettings.DevSettings.UseDevSettings) + { + dbContextOptionsBuilder + .LogTo(Console.WriteLine, [DbLoggerCategory.Database.Command.Name], LogLevel.Information); + } + + await using var migrationContext = new AppDbContext(dbContextOptionsBuilder.Options); if (AppSettings.DevSettings.UseEfMigrations) { @@ -31,8 +37,7 @@ public async Task StartAsync(CancellationToken cancellationToken) var roleManager = scope.ServiceProvider.GetRequiredService>(); foreach (var role in AppRole.AllRoles.Keys) { - if (!await migrationContext.Roles.AnyAsync(identityRole => identityRole.Name == role, - cancellationToken)) + if (!await migrationContext.Roles.AnyAsync(idRole => idRole.Name == role, cancellationToken)) { await roleManager.CreateAsync(new IdentityRole(role)); } diff --git a/src/WebApp/Platform/Logging/Redaction.cs b/src/WebApp/Platform/Logging/Redaction.cs deleted file mode 100644 index 567e83f..0000000 --- a/src/WebApp/Platform/Logging/Redaction.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System.Text.RegularExpressions; - -namespace MyApp.WebApp.Platform.Logging; - -public static partial class Redaction -{ - public static string MaskEmail(this string? email) - { - if (string.IsNullOrEmpty(email)) return string.Empty; - - var atIndex = email.IndexOf('@'); - if (atIndex <= 1) return email; - - var maskedEmail = MyRegex().Replace(email[..atIndex], "*"); - return string.Concat(maskedEmail, email.AsSpan(atIndex)); - } - - [GeneratedRegex(".(?=.{2})")] - private static partial Regex MyRegex(); -} diff --git a/tests/EfRepositoryTests/RepositoryHelper.cs b/tests/EfRepositoryTests/RepositoryHelper.cs index 5676dd3..e5d3ada 100644 --- a/tests/EfRepositoryTests/RepositoryHelper.cs +++ b/tests/EfRepositoryTests/RepositoryHelper.cs @@ -1,5 +1,6 @@ using GaEpd.AppLibrary.Domain.Entities; using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Diagnostics; using MyApp.Domain.Entities.EntryTypes; using MyApp.Domain.Entities.Offices; using MyApp.Domain.Entities.WorkEntries; @@ -26,7 +27,7 @@ namespace EfRepositoryTests; /// public sealed class RepositoryHelper : IDisposable, IAsyncDisposable { - private AppDbContext Context { get; set; } = default!; + private AppDbContext Context { get; set; } = null!; private readonly DbContextOptions _options; private readonly AppDbContext _context; @@ -36,7 +37,8 @@ public sealed class RepositoryHelper : IDisposable, IAsyncDisposable /// private RepositoryHelper() { - _options = SqliteInMemory.CreateOptions(); + _options = SqliteInMemory.CreateOptions(builder => + builder.LogTo(Console.WriteLine, events: [RelationalEventId.CommandExecuted])); _context = new AppDbContext(_options); _context.Database.EnsureCreated(); } @@ -49,7 +51,8 @@ private RepositoryHelper() private RepositoryHelper(object callingClass, string callingMember) { _options = callingClass.CreateUniqueMethodOptions(callingMember: callingMember, - builder: opts => opts.UseSqlServer()); + builder: builder => builder.UseSqlServer() + .LogTo(Console.WriteLine, events: [RelationalEventId.CommandExecuted])); _context = new AppDbContext(_options); _context.Database.EnsureClean(); }