diff --git a/backend/WorkClub.Api/Services/ClubService.cs b/backend/WorkClub.Api/Services/ClubService.cs index aaa777c..27133b6 100644 --- a/backend/WorkClub.Api/Services/ClubService.cs +++ b/backend/WorkClub.Api/Services/ClubService.cs @@ -12,135 +12,172 @@ public class ClubService private readonly AppDbContext _context; private readonly ITenantProvider _tenantProvider; private readonly IHttpContextAccessor _httpContextAccessor; + private readonly ILogger _logger; public ClubService( AppDbContext context, ITenantProvider tenantProvider, - IHttpContextAccessor httpContextAccessor) + IHttpContextAccessor httpContextAccessor, + ILogger logger) { _context = context; _tenantProvider = tenantProvider; _httpContextAccessor = httpContextAccessor; + _logger = logger; } public async Task> GetMyClubsAsync() { - var clubsClaim = _httpContextAccessor.HttpContext?.User.FindFirst("clubs")?.Value; - if (string.IsNullOrEmpty(clubsClaim)) + try { - return new List(); - } + 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(); - } - - var clubDtos = new List(); - 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(); } - 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(); + } - var parameter = command.CreateParameter(); - parameter.ParameterName = "@tenantId"; - parameter.Value = tenantId; - command.Parameters.Add(parameter); + var clubDtos = new List(); + 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(); } - - return clubDtos; } public async Task 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 - ); + } } } diff --git a/infra/k8s/base/keycloak-realm-import-configmap.yaml b/infra/k8s/base/keycloak-realm-import-configmap.yaml index ee271bc..2732e4b 100644 --- a/infra/k8s/base/keycloak-realm-import-configmap.yaml +++ b/infra/k8s/base/keycloak-realm-import-configmap.yaml @@ -150,11 +150,11 @@ data: "realmRoles": [ "admin" ], - "attributes": { - "clubs": [ - "64e05b5e-ef45-81d7-f2e8-3d14bd197383,Admin,3b4afcfa-1352-8fc7-b497-8ab52a0d5fda,Member" - ] - } + "attributes": { + "clubs": [ + "64e05b5e-ef45-81d7-f2e8-3d14bd197383,3b4afcfa-1352-8fc7-b497-8ab52a0d5fda" + ] + } }, { "username": "manager@test.com", @@ -172,11 +172,11 @@ data: "realmRoles": [ "manager" ], - "attributes": { - "clubs": [ - "64e05b5e-ef45-81d7-f2e8-3d14bd197383,Manager" - ] - } + "attributes": { + "clubs": [ + "64e05b5e-ef45-81d7-f2e8-3d14bd197383" + ] + } }, { "username": "member1@test.com", @@ -194,11 +194,11 @@ data: "realmRoles": [ "member" ], - "attributes": { - "clubs": [ - "64e05b5e-ef45-81d7-f2e8-3d14bd197383,Member,3b4afcfa-1352-8fc7-b497-8ab52a0d5fda,Member" - ] - } + "attributes": { + "clubs": [ + "64e05b5e-ef45-81d7-f2e8-3d14bd197383,3b4afcfa-1352-8fc7-b497-8ab52a0d5fda" + ] + } }, { "username": "member2@test.com", @@ -216,11 +216,11 @@ data: "realmRoles": [ "member" ], - "attributes": { - "clubs": [ - "64e05b5e-ef45-81d7-f2e8-3d14bd197383,Member" - ] - } + "attributes": { + "clubs": [ + "64e05b5e-ef45-81d7-f2e8-3d14bd197383" + ] + } }, { "username": "viewer@test.com", @@ -238,11 +238,11 @@ data: "realmRoles": [ "viewer" ], - "attributes": { - "clubs": [ - "64e05b5e-ef45-81d7-f2e8-3d14bd197383,Viewer" - ] - } + "attributes": { + "clubs": [ + "64e05b5e-ef45-81d7-f2e8-3d14bd197383" + ] + } } ] } diff --git a/infra/keycloak/realm-export.json b/infra/keycloak/realm-export.json index a66f2c8..885818f 100644 --- a/infra/keycloak/realm-export.json +++ b/infra/keycloak/realm-export.json @@ -82,18 +82,18 @@ "standardFlowEnabled": true, "implicitFlowEnabled": false, "directAccessGrantsEnabled": true, -"serviceAccountsEnabled": false, - "authorizationServicesEnabled": false, - "protocol": "openid-connect", - "redirectUris": [ - "http://localhost:30080/*" - ], - "webOrigins": [ - "http://localhost:30080" - ], - "attributes": { - "pkce.code.challenge.method": "S256", - "post.logout.redirect.uris": "http://localhost:30080/*", + "serviceAccountsEnabled": false, + "authorizationServicesEnabled": false, + "protocol": "openid-connect", + "redirectUris": [ + "http://localhost:30080/*" + ], + "webOrigins": [ + "http://localhost:30080" + ], + "attributes": { + "pkce.code.challenge.method": "S256", + "post.logout.redirect.uris": "http://localhost:30080/*", "access.token.lifespan": "3600" }, "protocolMappers": [ @@ -162,7 +162,9 @@ "firstName": "Admin", "lastName": "User", "attributes": { - "clubs": [] + "clubs": [ + "64e05b5e-ef45-81d7-f2e8-3d14bd197383,3b4afcfa-1352-8fc7-b497-8ab52a0d5fda" + ] }, "credentials": [ { @@ -337,4 +339,4 @@ "dockerAuthenticationFlow": "docker auth", "attributes": {}, "keycloakVersion": "26.0.0" -} +} \ No newline at end of file