Fix: Admin club management 500 error - JWT clubs claim format
- Fix clubs attribute in Keycloak to contain only UUIDs (removed role names) - Add defensive error handling in ClubService.GetMyClubsAsync() - Add logging for debugging club retrieval issues - Return empty list instead of 500 error on failures Fixes: Admin users can now manage clubs without contact admin error
This commit is contained in:
@@ -12,135 +12,172 @@ public class ClubService
|
||||
private readonly AppDbContext _context;
|
||||
private readonly ITenantProvider _tenantProvider;
|
||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||
private readonly ILogger<ClubService> _logger;
|
||||
|
||||
public ClubService(
|
||||
AppDbContext context,
|
||||
ITenantProvider tenantProvider,
|
||||
IHttpContextAccessor httpContextAccessor)
|
||||
IHttpContextAccessor httpContextAccessor,
|
||||
ILogger<ClubService> logger)
|
||||
{
|
||||
_context = context;
|
||||
_tenantProvider = tenantProvider;
|
||||
_httpContextAccessor = httpContextAccessor;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<List<ClubListDto>> GetMyClubsAsync()
|
||||
{
|
||||
var clubsClaim = _httpContextAccessor.HttpContext?.User.FindFirst("clubs")?.Value;
|
||||
if (string.IsNullOrEmpty(clubsClaim))
|
||||
try
|
||||
{
|
||||
return new List<ClubListDto>();
|
||||
}
|
||||
var clubsClaim = _httpContextAccessor.HttpContext?.User.FindFirst("clubs")?.Value;
|
||||
_logger.LogInformation("GetMyClubsAsync: Clubs claim value: {ClubsClaim}", clubsClaim);
|
||||
|
||||
var tenantIds = clubsClaim.Split(',', StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(t => t.Trim())
|
||||
.Where(t => !string.IsNullOrEmpty(t) && Guid.TryParse(t, out _))
|
||||
.ToList();
|
||||
|
||||
if (tenantIds.Count == 0)
|
||||
{
|
||||
return new List<ClubListDto>();
|
||||
}
|
||||
|
||||
var clubDtos = new List<ClubListDto>();
|
||||
var connectionString = _context.Database.GetConnectionString();
|
||||
|
||||
foreach (var tenantId in tenantIds)
|
||||
{
|
||||
await using var connection = new NpgsqlConnection(connectionString);
|
||||
await connection.OpenAsync();
|
||||
|
||||
await using var transaction = await connection.BeginTransactionAsync();
|
||||
|
||||
// Set RLS context
|
||||
using (var command = connection.CreateCommand())
|
||||
if (string.IsNullOrEmpty(clubsClaim))
|
||||
{
|
||||
command.Transaction = transaction;
|
||||
command.CommandText = $"SET LOCAL app.current_tenant_id = '{tenantId}'";
|
||||
await command.ExecuteNonQueryAsync();
|
||||
_logger.LogWarning("GetMyClubsAsync: No clubs claim found for user");
|
||||
return new List<ClubListDto>();
|
||||
}
|
||||
|
||||
Guid? clubId = null;
|
||||
string? clubName = null;
|
||||
int? sportTypeInt = null;
|
||||
// Parse UUIDs from comma-separated claim, filtering out non-UUID values (like role names)
|
||||
var tenantIds = clubsClaim.Split(',', StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(t => t.Trim())
|
||||
.Where(t => !string.IsNullOrEmpty(t) && Guid.TryParse(t, out _))
|
||||
.ToList();
|
||||
|
||||
// Fetch club details
|
||||
using (var command = connection.CreateCommand())
|
||||
_logger.LogInformation("GetMyClubsAsync: Parsed {Count} valid tenant IDs from claim", tenantIds.Count);
|
||||
|
||||
if (tenantIds.Count == 0)
|
||||
{
|
||||
command.Transaction = transaction;
|
||||
command.CommandText = @"
|
||||
SELECT c.""Id"", c.""Name"", c.""SportType""
|
||||
FROM clubs AS c
|
||||
WHERE c.""TenantId"" = @tenantId";
|
||||
_logger.LogWarning("GetMyClubsAsync: No valid tenant IDs found in clubs claim: {ClubsClaim}", clubsClaim);
|
||||
return new List<ClubListDto>();
|
||||
}
|
||||
|
||||
var parameter = command.CreateParameter();
|
||||
parameter.ParameterName = "@tenantId";
|
||||
parameter.Value = tenantId;
|
||||
command.Parameters.Add(parameter);
|
||||
var clubDtos = new List<ClubListDto>();
|
||||
var connectionString = _context.Database.GetConnectionString();
|
||||
|
||||
using (var reader = await command.ExecuteReaderAsync())
|
||||
foreach (var tenantId in tenantIds)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (await reader.ReadAsync())
|
||||
await using var connection = new NpgsqlConnection(connectionString);
|
||||
await connection.OpenAsync();
|
||||
|
||||
await using var transaction = await connection.BeginTransactionAsync();
|
||||
|
||||
// Set RLS context - tenantId is already validated as a valid GUID
|
||||
// Use direct string since SET LOCAL doesn't support parameters
|
||||
using (var command = connection.CreateCommand())
|
||||
{
|
||||
clubId = reader.GetGuid(0);
|
||||
clubName = reader.GetString(1);
|
||||
sportTypeInt = reader.GetInt32(2);
|
||||
command.Transaction = transaction;
|
||||
command.CommandText = $"SET LOCAL app.current_tenant_id = '{tenantId}'";
|
||||
await command.ExecuteNonQueryAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch member count if club exists
|
||||
if (clubId.HasValue && clubName != null && sportTypeInt.HasValue)
|
||||
{
|
||||
using (var memberCommand = connection.CreateCommand())
|
||||
Guid? clubId = null;
|
||||
string? clubName = null;
|
||||
int? sportTypeInt = null;
|
||||
|
||||
// Fetch club details
|
||||
using (var command = connection.CreateCommand())
|
||||
{
|
||||
command.Transaction = transaction;
|
||||
command.CommandText = @"
|
||||
SELECT c.""Id"", c.""Name"", c.""SportType""
|
||||
FROM clubs AS c
|
||||
WHERE c.""TenantId"" = @tenantId";
|
||||
|
||||
var parameter = command.CreateParameter();
|
||||
parameter.ParameterName = "@tenantId";
|
||||
parameter.Value = tenantId;
|
||||
command.Parameters.Add(parameter);
|
||||
|
||||
using (var reader = await command.ExecuteReaderAsync())
|
||||
{
|
||||
if (await reader.ReadAsync())
|
||||
{
|
||||
clubId = reader.GetGuid(0);
|
||||
clubName = reader.GetString(1);
|
||||
sportTypeInt = reader.GetInt32(2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch member count if club exists
|
||||
if (clubId.HasValue && clubName != null && sportTypeInt.HasValue)
|
||||
{
|
||||
using (var memberCommand = connection.CreateCommand())
|
||||
{
|
||||
memberCommand.Transaction = transaction;
|
||||
memberCommand.CommandText = @"
|
||||
SELECT COUNT(*)
|
||||
FROM members AS m
|
||||
WHERE m.""ClubId"" = @clubId";
|
||||
|
||||
var param = memberCommand.CreateParameter();
|
||||
param.ParameterName = "@clubId";
|
||||
param.Value = clubId;
|
||||
memberCommand.Parameters.Add(param);
|
||||
|
||||
var memberCountResult = await memberCommand.ExecuteScalarAsync();
|
||||
var memberCount = memberCountResult != null ? Convert.ToInt32(memberCountResult) : 0;
|
||||
var sportTypeEnum = ((SportType)sportTypeInt.Value).ToString();
|
||||
|
||||
clubDtos.Add(new ClubListDto(
|
||||
clubId.Value,
|
||||
clubName,
|
||||
sportTypeEnum,
|
||||
memberCount,
|
||||
Guid.Parse(tenantId)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
await transaction.CommitAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
memberCommand.Transaction = transaction;
|
||||
memberCommand.CommandText = @"
|
||||
SELECT COUNT(*)
|
||||
FROM members AS m
|
||||
WHERE m.""ClubId"" = @clubId";
|
||||
|
||||
var param = memberCommand.CreateParameter();
|
||||
param.ParameterName = "@clubId";
|
||||
param.Value = clubId;
|
||||
memberCommand.Parameters.Add(param);
|
||||
|
||||
var memberCountResult = await memberCommand.ExecuteScalarAsync();
|
||||
var memberCount = memberCountResult != null ? Convert.ToInt32(memberCountResult) : 0;
|
||||
var sportTypeEnum = ((SportType)sportTypeInt.Value).ToString();
|
||||
|
||||
clubDtos.Add(new ClubListDto(
|
||||
clubId.Value,
|
||||
clubName,
|
||||
sportTypeEnum,
|
||||
memberCount,
|
||||
Guid.Parse(tenantId)
|
||||
));
|
||||
_logger.LogError(ex, "GetMyClubsAsync: Error processing tenant {TenantId}", tenantId);
|
||||
// Continue with next tenant instead of failing entirely
|
||||
}
|
||||
}
|
||||
|
||||
await transaction.CommitAsync();
|
||||
_logger.LogInformation("GetMyClubsAsync: Returning {Count} clubs", clubDtos.Count);
|
||||
return clubDtos;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "GetMyClubsAsync: Unexpected error getting user clubs");
|
||||
// Return empty list instead of throwing to prevent 500 error
|
||||
return new List<ClubListDto>();
|
||||
}
|
||||
|
||||
return clubDtos;
|
||||
}
|
||||
|
||||
public async Task<ClubDetailDto?> GetCurrentClubAsync()
|
||||
{
|
||||
var tenantId = _tenantProvider.GetTenantId();
|
||||
try
|
||||
{
|
||||
var tenantId = _tenantProvider.GetTenantId();
|
||||
|
||||
var club = await _context.Clubs
|
||||
.FirstOrDefaultAsync(c => c.TenantId == tenantId);
|
||||
var club = await _context.Clubs
|
||||
.FirstOrDefaultAsync(c => c.TenantId == tenantId);
|
||||
|
||||
if (club == null)
|
||||
if (club == null)
|
||||
return null;
|
||||
|
||||
return new ClubDetailDto(
|
||||
club.Id,
|
||||
club.Name,
|
||||
club.SportType.ToString(),
|
||||
club.Description,
|
||||
club.CreatedAt,
|
||||
club.UpdatedAt
|
||||
);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "GetCurrentClubAsync: Error getting current club");
|
||||
return null;
|
||||
|
||||
return new ClubDetailDto(
|
||||
club.Id,
|
||||
club.Name,
|
||||
club.SportType.ToString(),
|
||||
club.Description,
|
||||
club.CreatedAt,
|
||||
club.UpdatedAt
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user