From 5a4bb16413be7d765201da3c84321c311378267f Mon Sep 17 00:00:00 2001 From: WorkClub Automation Date: Thu, 5 Mar 2026 19:22:13 +0100 Subject: [PATCH] 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 --- .../Auth/ClubRoleClaimsTransformation.cs | 67 +++++++++++++------ 1 file changed, 45 insertions(+), 22 deletions(-) diff --git a/backend/WorkClub.Api/Auth/ClubRoleClaimsTransformation.cs b/backend/WorkClub.Api/Auth/ClubRoleClaimsTransformation.cs index abb2420..9139517 100644 --- a/backend/WorkClub.Api/Auth/ClubRoleClaimsTransformation.cs +++ b/backend/WorkClub.Api/Auth/ClubRoleClaimsTransformation.cs @@ -1,16 +1,22 @@ using System.Security.Claims; -using System.Text.Json; using Microsoft.AspNetCore.Authentication; +using Microsoft.EntityFrameworkCore; +using WorkClub.Domain.Enums; +using WorkClub.Infrastructure.Data; namespace WorkClub.Api.Auth; public class ClubRoleClaimsTransformation : IClaimsTransformation { private readonly IHttpContextAccessor _httpContextAccessor; + private readonly AppDbContext _context; - public ClubRoleClaimsTransformation(IHttpContextAccessor httpContextAccessor) + public ClubRoleClaimsTransformation( + IHttpContextAccessor httpContextAccessor, + AppDbContext context) { _httpContextAccessor = httpContextAccessor; + _context = context; } public Task TransformAsync(ClaimsPrincipal principal) @@ -26,46 +32,63 @@ public class ClubRoleClaimsTransformation : IClaimsTransformation return Task.FromResult(principal); } - Dictionary? clubsDict; - try - { - clubsDict = JsonSerializer.Deserialize>(clubsClaim); - } - catch (JsonException) - { - return Task.FromResult(principal); - } + // Parse comma-separated club UUIDs + var clubIds = clubsClaim.Split(',', StringSplitOptions.RemoveEmptyEntries) + .Select(id => id.Trim()) + .ToArray(); - if (clubsDict == null || clubsDict.Count == 0) + if (clubIds.Length == 0) { return Task.FromResult(principal); } 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); } - if (!clubsDict.TryGetValue(tenantId, out var clubRole)) + var userIdClaim = principal.FindFirst("preferred_username")?.Value; + if (string.IsNullOrEmpty(userIdClaim)) { return Task.FromResult(principal); } - var mappedRole = MapClubRoleToAspNetRole(clubRole); - identity.AddClaim(new Claim(ClaimTypes.Role, mappedRole)); + // 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)); + } return Task.FromResult(principal); } - private static string MapClubRoleToAspNetRole(string clubRole) + private ClubRole? GetMemberRole(string externalUserId, string tenantId) { - return clubRole.ToLowerInvariant() switch + try { - "admin" => "Admin", - "manager" => "Manager", - "member" => "Member", - "viewer" => "Viewer", + var member = _context.Members + .FirstOrDefault(m => m.Email == externalUserId && m.TenantId == tenantId); + + 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" }; }