diff --git a/src/Koogle.Application/Services/UserService.cs b/src/Koogle.Application/Services/UserService.cs index 7fe97ec..20b588c 100644 --- a/src/Koogle.Application/Services/UserService.cs +++ b/src/Koogle.Application/Services/UserService.cs @@ -136,9 +136,13 @@ public class UserService : IUserService /// public async Task GetByIdentityUserIdAsync(Guid identityUserId, CancellationToken ct = default) { - // Domain-Profil laden (SoftDelete Filter wirkt) + // Domain-Profil laden mit Club-Memberships (SoftDelete Filter wirkt) var profile = await _appDb.UserProfiles .AsNoTracking() + .Include(p => p.Clubs) + .ThenInclude(c => c.Club) + .Include(p => p.Clubs) + .ThenInclude(c => c.RoleAssignments) .SingleOrDefaultAsync(p => p.IdentityUserId == identityUserId, ct); if (profile == null) @@ -156,6 +160,16 @@ public class UserService : IUserService var roles = await _userManager.GetRolesAsync(identity); dto.Identity.Roles = roles.ToList(); + // Map club memberships + dto.ClubMemberships = profile.Clubs.Select(c => new UserClubMembershipDto + { + ClubId = c.ClubId, + ClubName = c.Club?.Name ?? "", + IsDefault = c.IsDefault, + AssignedAt = c.AssignedAt, + Roles = c.RoleAssignments.Select(ra => ra.RoleName).ToList() + }).ToList(); + return dto; } diff --git a/src/Koogle.Infrastructure/DependencyInjection.cs b/src/Koogle.Infrastructure/DependencyInjection.cs index bdba4fa..3fd9522 100644 --- a/src/Koogle.Infrastructure/DependencyInjection.cs +++ b/src/Koogle.Infrastructure/DependencyInjection.cs @@ -69,7 +69,7 @@ public static class DependencyInjection options.AddPolicy("ClubAdmin", p => p.Requirements.Add(new ClubRoleRequirement("Admin"))); }); services.AddSingleton(); - + services.AddSingleton(); // HTTP Context Accessor (für CurrentUserService) services.AddHttpContextAccessor(); diff --git a/src/Koogle.Infrastructure/Security/ClubRoleRequirement.cs b/src/Koogle.Infrastructure/Security/ClubRoleRequirement.cs index 7a8da1b..87819eb 100644 --- a/src/Koogle.Infrastructure/Security/ClubRoleRequirement.cs +++ b/src/Koogle.Infrastructure/Security/ClubRoleRequirement.cs @@ -14,15 +14,31 @@ public sealed class ClubRoleRequirement : IAuthorizationRequirement public string RequiredRole { get; } } -public sealed class ClubRoleHandler : AuthorizationHandler +/// +/// Handles ClubRoleRequirement when no resource is passed (e.g., [Authorize(Policy = "ClubViewer")]). +/// Uses current_club_id claim to determine the club context. +/// +public sealed class ClubRoleHandler : AuthorizationHandler { protected override Task HandleRequirementAsync( + AuthorizationHandlerContext context, + ClubRoleRequirement requirement) + { + // Get current club from claims + var currentClubClaim = context.User.FindFirst("current_club_id"); + if (currentClubClaim == null || !Guid.TryParse(currentClubClaim.Value, out var clubId)) + { + return Task.CompletedTask; // No club context, fail + } + + return CheckClubRole(context, requirement, clubId); + } + + private static Task CheckClubRole( AuthorizationHandlerContext context, ClubRoleRequirement requirement, Guid clubId) { - // Hierarchie (optional): Admin >= Editor >= Viewer - // Du kannst das anpassen/erweitern. static int Rank(string role) => role switch { "Admin" => 3, @@ -33,7 +49,7 @@ public sealed class ClubRoleHandler : AuthorizationHandler +/// Handles ClubRoleRequirement when a specific clubId resource is passed. +/// +public sealed class ClubRoleResourceHandler : AuthorizationHandler +{ + protected override Task HandleRequirementAsync( + AuthorizationHandlerContext context, + ClubRoleRequirement requirement, + Guid clubId) + { + static int Rank(string role) => role switch + { + "Admin" => 3, + "Editor" => 2, + "Viewer" => 1, + _ => 0 + }; + + var requiredRank = Rank(requirement.RequiredRole); + + var claims = context.User.FindAll("club_role"); + foreach (var c in claims) + { + var parts = c.Value.Split(':', 2); + if (parts.Length != 2) continue; + + if (Guid.TryParse(parts[0], out var claimClubId) && claimClubId == clubId) + { + var userRole = parts[1]; + if (Rank(userRole) >= requiredRank) + { + context.Succeed(requirement); + break; + } + } + } + + return Task.CompletedTask; + } +} diff --git a/src/Koogle.Web/Store/AuthState/AuthEffects.cs b/src/Koogle.Web/Store/AuthState/AuthEffects.cs index 99f2e38..da767c0 100644 --- a/src/Koogle.Web/Store/AuthState/AuthEffects.cs +++ b/src/Koogle.Web/Store/AuthState/AuthEffects.cs @@ -12,21 +12,21 @@ namespace Koogle.Web.Store.AuthState { private readonly IUserService _userService; private readonly IAuthorizationService _authorizationService; + private readonly ICurrentClubContext _currentClubContext; private readonly ILogger _logger; /// /// Initializes a new instance of the AuthEffects class. /// - /// The user service. - /// The authorization service. - /// The logger. public AuthEffects( IUserService userService, IAuthorizationService authorizationService, + ICurrentClubContext currentClubContext, ILogger logger) { _userService = userService; _authorizationService = authorizationService; + _currentClubContext = currentClubContext; _logger = logger; } @@ -50,13 +50,32 @@ namespace Koogle.Web.Store.AuthState return; } - //var roles = await _authorizationService.GetCurrentUserRolesAsync(); - //var companyIds = await _authorizationService.GetAccessibleCompanyIdsAsync(); + // Merge Identity roles with club-specific roles for current club + var roles = currentUser.Identity.Roles.ToList(); + var currentClubId = _currentClubContext.ClubId; - dispatcher.Dispatch(new AuthState.InitializeAuthSuccessAction(currentUser, currentUser.Identity.Roles)); + if (currentClubId != Guid.Empty) + { + var clubMembership = currentUser.ClubMemberships + .FirstOrDefault(m => m.ClubId == currentClubId); + + if (clubMembership != null) + { + // Add club roles that aren't already in the list + foreach (var clubRole in clubMembership.Roles) + { + if (!roles.Contains(clubRole)) + { + roles.Add(clubRole); + } + } + } + } + + dispatcher.Dispatch(new AuthState.InitializeAuthSuccessAction(currentUser, roles)); _logger.LogInformation("Auth initialized for user {DisplayName} with roles {Roles}", - currentUser.DisplayName, string.Join(", ", currentUser.Identity.Roles)); + currentUser.DisplayName, string.Join(", ", roles)); } catch (Exception ex) {