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; } }