Initial commit

This commit is contained in:
2026-04-23 21:38:24 -04:00
commit 63e12a9636
1762 changed files with 1672620 additions and 0 deletions
@@ -0,0 +1,95 @@
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options;
using PowderCoating.Core.Entities;
using System.Security.Claims;
namespace PowderCoating.Infrastructure.Identity;
/// <summary>
/// Custom ASP.NET Core Identity claims factory that embeds application-specific claims into the
/// user's <see cref="ClaimsPrincipal"/> at login time.
/// <para>
/// Registered in <c>Program.cs</c> via
/// <c>builder.Services.AddScoped&lt;IUserClaimsPrincipalFactory&lt;ApplicationUser&gt;, CustomUserClaimsPrincipalFactory&gt;()</c>
/// so that ASP.NET Core Identity calls it instead of the default factory whenever a cookie is issued.
/// </para>
/// <para>
/// <b>Why embed claims at login?</b> Reading <c>CompanyId</c> or <c>CompanyRole</c> from the database
/// on every request would add a synchronous DB round-trip to the authentication middleware, which runs
/// on every single HTTP request. Embedding the values as signed cookie claims at login time means
/// subsequent requests are verified cryptographically with zero extra DB queries.
/// </para>
/// <para>
/// Claims added:
/// <list type="bullet">
/// <item><description><c>"CompanyId"</c> — the integer ID of the company the user belongs to. Read by <see cref="Data.ApplicationDbContext.CurrentCompanyId"/> to enforce multi-tenancy query filters.</description></item>
/// <item><description><c>"CompanyRole"</c> — the user's role within their company (e.g., CompanyAdmin, Manager, Worker). Distinct from the ASP.NET Identity system role (SuperAdmin).</description></item>
/// </list>
/// </para>
/// </summary>
public class CustomUserClaimsPrincipalFactory : UserClaimsPrincipalFactory<ApplicationUser, IdentityRole>
{
/// <summary>
/// Initialises the factory with the Identity infrastructure services required by the base class.
/// </summary>
/// <param name="userManager">Used by the base class to load user-level claims.</param>
/// <param name="roleManager">Used by the base class to load role-level claims.</param>
/// <param name="options">Identity options, including the claim-type mappings used by the base class.</param>
public CustomUserClaimsPrincipalFactory(
UserManager<ApplicationUser> userManager,
RoleManager<IdentityRole> roleManager,
IOptions<IdentityOptions> options)
: base(userManager, roleManager, options)
{
}
/// <summary>
/// Generates the <see cref="ClaimsIdentity"/> for <paramref name="user"/> by first calling the base
/// implementation (which adds standard Identity claims such as NameIdentifier, Email, and Role) and
/// then appending the application-specific <c>CompanyId</c> and <c>CompanyRole</c> claims.
/// <para>
/// Each claim addition is wrapped in an individual try/catch to ensure that a missing or malformed
/// value on one field does not fail the entire authentication flow — the user is logged in with
/// whatever valid claims could be assembled. Errors are written to the debug output rather than
/// thrown so that login remains functional even in degraded configurations.
/// </para>
/// </summary>
/// <param name="user">The authenticated <see cref="ApplicationUser"/> whose claims are being constructed.</param>
/// <returns>A <see cref="ClaimsIdentity"/> containing all standard and application-specific claims.</returns>
protected override async Task<ClaimsIdentity> GenerateClaimsAsync(ApplicationUser user)
{
var identity = await base.GenerateClaimsAsync(user);
// Add CompanyId claim - only if valid
if (user.CompanyId > 0)
{
try
{
identity.AddClaim(new Claim("CompanyId", user.CompanyId.ToString()));
}
catch (Exception ex)
{
// Log but don't fail authentication
System.Diagnostics.Debug.WriteLine($"Error adding CompanyId claim: {ex.Message}");
}
}
// Add CompanyRole claim - only if valid and not empty
if (!string.IsNullOrWhiteSpace(user.CompanyRole))
{
try
{
// Sanitize the role value to ensure it's safe
var sanitizedRole = user.CompanyRole.Trim();
identity.AddClaim(new Claim("CompanyRole", sanitizedRole));
}
catch (Exception ex)
{
// Log but don't fail authentication
System.Diagnostics.Debug.WriteLine($"Error adding CompanyRole claim: {ex.Message}");
}
}
return identity;
}
}