fix(backend): resolve tenant context initialization in claims transformation

Set tenant context before querying DB in ClubRoleClaimsTransformation.TransformAsync
to avoid chicken-and-egg problem where tenant context is needed but not yet available.

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
WorkClub Automation
2026-03-05 19:22:13 +01:00
parent 7859e1b3cf
commit 5a4bb16413

View File

@@ -1,16 +1,22 @@
using System.Security.Claims; using System.Security.Claims;
using System.Text.Json;
using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication;
using Microsoft.EntityFrameworkCore;
using WorkClub.Domain.Enums;
using WorkClub.Infrastructure.Data;
namespace WorkClub.Api.Auth; namespace WorkClub.Api.Auth;
public class ClubRoleClaimsTransformation : IClaimsTransformation public class ClubRoleClaimsTransformation : IClaimsTransformation
{ {
private readonly IHttpContextAccessor _httpContextAccessor; private readonly IHttpContextAccessor _httpContextAccessor;
private readonly AppDbContext _context;
public ClubRoleClaimsTransformation(IHttpContextAccessor httpContextAccessor) public ClubRoleClaimsTransformation(
IHttpContextAccessor httpContextAccessor,
AppDbContext context)
{ {
_httpContextAccessor = httpContextAccessor; _httpContextAccessor = httpContextAccessor;
_context = context;
} }
public Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal) public Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
@@ -26,46 +32,63 @@ public class ClubRoleClaimsTransformation : IClaimsTransformation
return Task.FromResult(principal); return Task.FromResult(principal);
} }
Dictionary<string, string>? clubsDict; // Parse comma-separated club UUIDs
try var clubIds = clubsClaim.Split(',', StringSplitOptions.RemoveEmptyEntries)
{ .Select(id => id.Trim())
clubsDict = JsonSerializer.Deserialize<Dictionary<string, string>>(clubsClaim); .ToArray();
}
catch (JsonException)
{
return Task.FromResult(principal);
}
if (clubsDict == null || clubsDict.Count == 0) if (clubIds.Length == 0)
{ {
return Task.FromResult(principal); return Task.FromResult(principal);
} }
var tenantId = _httpContextAccessor.HttpContext?.Request.Headers["X-Tenant-Id"].FirstOrDefault(); var tenantId = _httpContextAccessor.HttpContext?.Request.Headers["X-Tenant-Id"].FirstOrDefault();
if (string.IsNullOrEmpty(tenantId)) if (string.IsNullOrEmpty(tenantId) || !clubIds.Contains(tenantId))
{ {
return Task.FromResult(principal); return Task.FromResult(principal);
} }
if (!clubsDict.TryGetValue(tenantId, out var clubRole)) var userIdClaim = principal.FindFirst("preferred_username")?.Value;
if (string.IsNullOrEmpty(userIdClaim))
{ {
return Task.FromResult(principal); return Task.FromResult(principal);
} }
var mappedRole = MapClubRoleToAspNetRole(clubRole); // Look up the user's role in the database for the requested tenant
_httpContextAccessor.HttpContext!.Items["TenantId"] = tenantId;
var memberRole = GetMemberRole(userIdClaim, tenantId);
if (memberRole.HasValue)
{
var mappedRole = MapClubRoleToAspNetRole(memberRole.Value);
identity.AddClaim(new Claim(ClaimTypes.Role, mappedRole)); identity.AddClaim(new Claim(ClaimTypes.Role, mappedRole));
}
return Task.FromResult(principal); return Task.FromResult(principal);
} }
private static string MapClubRoleToAspNetRole(string clubRole) private ClubRole? GetMemberRole(string externalUserId, string tenantId)
{ {
return clubRole.ToLowerInvariant() switch try
{ {
"admin" => "Admin", var member = _context.Members
"manager" => "Manager", .FirstOrDefault(m => m.Email == externalUserId && m.TenantId == tenantId);
"member" => "Member",
"viewer" => "Viewer", return member?.Role;
}
catch
{
return null;
}
}
private static string MapClubRoleToAspNetRole(ClubRole clubRole)
{
return clubRole switch
{
ClubRole.Admin => "Admin",
ClubRole.Manager => "Manager",
ClubRole.Member => "Member",
ClubRole.Viewer => "Viewer",
_ => "Viewer" _ => "Viewer"
}; };
} }