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;
///
/// Resolves the current tenant's CompanyId from the active HTTP request principal.
/// Consumed by ApplicationDbContext's global query filters to enforce company-level
/// data isolation: every EF query automatically appends WHERE CompanyId = @currentCompanyId
/// unless the caller passes ignoreQueryFilters: true.
/// SuperAdmins bypass isolation entirely (no company filter applied) unless they are
/// actively impersonating a company via the session-stored override.
///
public class TenantContext : ITenantContext
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly UserManager _userManager;
private readonly ApplicationDbContext _context;
///
/// Constructs the context. All three dependencies are scoped per-request, matching
/// the scoped lifetime of this service and ApplicationDbContext.
///
public TenantContext(
IHttpContextAccessor httpContextAccessor,
UserManager userManager,
ApplicationDbContext context)
{
_httpContextAccessor = httpContextAccessor;
_userManager = userManager;
_context = context;
}
///
/// Returns the CompanyId of the currently authenticated user, or null
/// for unauthenticated requests. Resolution order:
///
/// - SuperAdmin impersonation override from ISession["ImpersonatingCompanyId"].
/// - The CompanyId claim baked into the authentication cookie/JWT at login time
/// (fast path — no DB hit).
/// - Synchronous database fallback via 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.
///
///
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;
}
///
/// Asynchronously retrieves the full entity
/// for the currently authenticated user. Uses
/// which reads from the Identity user cache when available. Returns null for
/// unauthenticated requests or when the user record cannot be found.
///
public async Task GetCurrentCompanyAsync()
{
var user = _httpContextAccessor.HttpContext?.User;
if (user?.Identity?.IsAuthenticated != true)
return null;
var appUser = await _userManager.GetUserAsync(user);
return appUser?.Company;
}
///
/// Returns true when the current user is in the SuperAdmin ASP.NET Identity role.
/// SuperAdmins see all companies' data (no company filter) and have access to Platform
/// Management pages. Used by ApplicationDbContext to decide whether to suppress the
/// global CompanyId query filter entirely.
///
public bool IsSuperAdmin()
{
var user = _httpContextAccessor.HttpContext?.User;
return user?.IsInRole("SuperAdmin") == true;
}
///
/// Returns true 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 ) is treated as a company admin for
/// that session, not a platform admin.
///
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;
}
///
/// Returns true 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 false (Imperial) when
/// there is no company context or no preference row exists.
///
public async Task 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;
}
}