149 lines
6.4 KiB
C#
149 lines
6.4 KiB
C#
using Microsoft.AspNetCore.Http;
|
|
using Microsoft.AspNetCore.Identity;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using PowderCoating.Core.Entities;
|
|
using PowderCoating.Core.Interfaces;
|
|
using PowderCoating.Infrastructure.Data;
|
|
using System.Security.Claims;
|
|
|
|
namespace PowderCoating.Infrastructure.Services;
|
|
|
|
/// <summary>
|
|
/// Resolves the current tenant's <c>CompanyId</c> from the active HTTP request principal.
|
|
/// Consumed by <c>ApplicationDbContext</c>'s global query filters to enforce company-level
|
|
/// data isolation: every EF query automatically appends <c>WHERE CompanyId = @currentCompanyId</c>
|
|
/// unless the caller passes <c>ignoreQueryFilters: true</c>.
|
|
/// SuperAdmins bypass isolation entirely (no company filter applied) unless they are
|
|
/// actively impersonating a company via the session-stored override.
|
|
/// </summary>
|
|
public class TenantContext : ITenantContext
|
|
{
|
|
private readonly IHttpContextAccessor _httpContextAccessor;
|
|
private readonly UserManager<ApplicationUser> _userManager;
|
|
private readonly ApplicationDbContext _context;
|
|
|
|
/// <summary>
|
|
/// Constructs the context. All three dependencies are scoped per-request, matching
|
|
/// the scoped lifetime of this service and <c>ApplicationDbContext</c>.
|
|
/// </summary>
|
|
public TenantContext(
|
|
IHttpContextAccessor httpContextAccessor,
|
|
UserManager<ApplicationUser> userManager,
|
|
ApplicationDbContext context)
|
|
{
|
|
_httpContextAccessor = httpContextAccessor;
|
|
_userManager = userManager;
|
|
_context = context;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the <c>CompanyId</c> of the currently authenticated user, or <c>null</c>
|
|
/// for unauthenticated requests. Resolution order:
|
|
/// <list type="number">
|
|
/// <item>SuperAdmin impersonation override from <c>ISession["ImpersonatingCompanyId"]</c>.</item>
|
|
/// <item>The <c>CompanyId</c> claim baked into the authentication cookie/JWT at login time
|
|
/// (fast path — no DB hit).</item>
|
|
/// <item>Synchronous database fallback via <see cref="UserManager{TUser}.Users"/> when the
|
|
/// claim is absent (e.g. user logged in before claim issuance was added). This path
|
|
/// blocks the thread and should be treated as a migration path — users re-logging in
|
|
/// will pick up the claim and avoid the DB call.</item>
|
|
/// </list>
|
|
/// </summary>
|
|
public int? GetCurrentCompanyId()
|
|
{
|
|
var user = _httpContextAccessor.HttpContext?.User;
|
|
if (user?.Identity?.IsAuthenticated != true)
|
|
return null;
|
|
|
|
// SuperAdmin impersonation override — checked before claims
|
|
if (user.IsInRole("SuperAdmin"))
|
|
{
|
|
var overrideId = _httpContextAccessor.HttpContext?.Session.GetInt32("ImpersonatingCompanyId");
|
|
if (overrideId.HasValue) return overrideId.Value;
|
|
}
|
|
|
|
// Try to get from claims first (performance optimization)
|
|
var companyIdClaim = user.FindFirst("CompanyId")?.Value;
|
|
if (companyIdClaim != null && int.TryParse(companyIdClaim, out int companyId))
|
|
return companyId;
|
|
|
|
// Fallback: Get from database if claim is missing
|
|
// This can happen if user logged in before claims were set up
|
|
var userName = user.Identity.Name;
|
|
if (!string.IsNullOrEmpty(userName))
|
|
{
|
|
// Note: This is synchronous and may impact performance
|
|
// Consider requiring users to re-login to get claims
|
|
var appUser = _userManager.Users
|
|
.FirstOrDefault(u => u.UserName == userName);
|
|
|
|
if (appUser != null && appUser.CompanyId > 0)
|
|
return appUser.CompanyId;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Asynchronously retrieves the full <see cref="PowderCoating.Core.Entities.Company"/> entity
|
|
/// for the currently authenticated user. Uses <see cref="UserManager{TUser}.GetUserAsync"/>
|
|
/// which reads from the Identity user cache when available. Returns <c>null</c> for
|
|
/// unauthenticated requests or when the user record cannot be found.
|
|
/// </summary>
|
|
public async Task<Company?> GetCurrentCompanyAsync()
|
|
{
|
|
var user = _httpContextAccessor.HttpContext?.User;
|
|
if (user?.Identity?.IsAuthenticated != true)
|
|
return null;
|
|
|
|
var appUser = await _userManager.GetUserAsync(user);
|
|
return appUser?.Company;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns <c>true</c> when the current user is in the <c>SuperAdmin</c> ASP.NET Identity role.
|
|
/// SuperAdmins see all companies' data (no company filter) and have access to Platform
|
|
/// Management pages. Used by <c>ApplicationDbContext</c> to decide whether to suppress the
|
|
/// global <c>CompanyId</c> query filter entirely.
|
|
/// </summary>
|
|
public bool IsSuperAdmin()
|
|
{
|
|
var user = _httpContextAccessor.HttpContext?.User;
|
|
return user?.IsInRole("SuperAdmin") == true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns <c>true</c> when the current user is a SuperAdmin who is not actively scoped to
|
|
/// a specific tenant company. Platform admins have unrestricted access to all companies,
|
|
/// global settings, and the seeding UI. A SuperAdmin impersonating a tenant company (via the
|
|
/// session override in <see cref="GetCurrentCompanyId"/>) is treated as a company admin for
|
|
/// that session, not a platform admin.
|
|
/// </summary>
|
|
public bool IsPlatformAdmin()
|
|
{
|
|
if (!IsSuperAdmin()) return false;
|
|
var companyId = GetCurrentCompanyId();
|
|
// No company assigned or explicitly on company 1 = full platform admin
|
|
return companyId == null || companyId == 1;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns <c>true</c> when the current company has opted into metric units (kg/m²) rather
|
|
/// than the default Imperial system (lb/ft²). Used by views and PDF generators to display
|
|
/// the correct unit labels and conversion factors. Defaults to <c>false</c> (Imperial) when
|
|
/// there is no company context or no preference row exists.
|
|
/// </summary>
|
|
public async Task<bool> UseMetricSystemAsync()
|
|
{
|
|
var companyId = GetCurrentCompanyId();
|
|
if (companyId == null)
|
|
return false; // Default to Imperial if no company context
|
|
|
|
var preferences = await _context.CompanyPreferences
|
|
.Where(p => p.CompanyId == companyId.Value)
|
|
.FirstOrDefaultAsync();
|
|
|
|
return preferences?.UseMetricSystem ?? false;
|
|
}
|
|
}
|