{"record":{"id":"e1189499f8818101","repo":"fullstackhero/dotnet-starter-kit","slug":"only-channel-admins-can-add-members-to-private-channels","errorCode":null,"errorMessage":"Only channel admins can add members to private channels.","messagePattern":"Only channel admins can add members to private channels\\.","errorType":"exception","errorClass":"ForbiddenException","httpStatus":403,"severity":"error","filePath":"src/Modules/Chat/Modules.Chat/Features/v1/Channels/AddChannelMembers/AddChannelMembersCommandHandler.cs","lineNumber":35,"sourceCode":"    IHubContext<AppHub> hub)\n    : ICommandHandler<AddChannelMembersCommand, Unit>\n{\n    public async ValueTask<Unit> Handle(AddChannelMembersCommand cmd, CancellationToken cancellationToken)\n    {\n        ArgumentNullException.ThrowIfNull(cmd);\n        var userId = currentUser.GetUserId();\n        if (userId == Guid.Empty) throw new UnauthorizedException(\"no current user\");\n        var currentUserId = userId.ToString();\n\n        var channel = await db.Channels.FirstOrDefaultAsync(c => c.Id == cmd.ChannelId, cancellationToken)\n            .ConfigureAwait(false)\n            ?? throw new NotFoundException(\"Channel not found.\");\n\n        // Members can invite to public channels they belong to; private channels require Admin.\n        var caller = channel.RequireMember(currentUserId);\n        if (channel.IsPrivate && caller.Role != ChannelMemberRole.Admin)\n        {\n            throw new ForbiddenException(\"Only channel admins can add members to private channels.\");\n        }\n\n        var newlyAdded = new List<string>();\n        foreach (var uid in cmd.UserIds.Distinct(StringComparer.Ordinal))\n        {\n            // Skip duplicates silently — endpoint is idempotent for already-members.\n            if (channel.Members.Any(m => string.Equals(m.UserId, uid, StringComparison.Ordinal))) continue;\n            channel.AddMember(uid, currentUserId);\n            newlyAdded.Add(uid);\n        }\n\n        await db.SaveChangesAsync(cancellationToken).ConfigureAwait(false);\n\n        foreach (var uid in newlyAdded)\n        {\n            // Notify existing members. The new member isn't in the channel:{id} group yet; they pick\n            // it up on next reconnect (OnConnectedAsync pre-joins all their channels).\n            await hub.Clients.Group($\"channel:{channel.Id}\")","sourceCodeStart":17,"sourceCodeEnd":53,"githubUrl":"https://github.com/fullstackhero/dotnet-starter-kit/blob/3f2959e683e9f83f13e55e1678c9119f63c7e8e5/src/Modules/Chat/Modules.Chat/Features/v1/Channels/AddChannelMembers/AddChannelMembersCommandHandler.cs#L17-L53","documentation":"AddChannelMembersCommandHandler throws ForbiddenException when the target channel is private and the calling user's ChannelMemberRole is not Admin. Members can invite into public channels, but private channels restrict membership changes to admins.","triggerScenarios":"A non-admin member of a private channel invokes AddChannelMembers; a member of a public channel whose role in THAT channel is Member tries to add users to a private channel they belong to.","commonSituations":"Frontend showing 'invite' button without checking caller role; role downgraded to Member after an admin removal; user assuming channel-wide admin rights apply per-channel.","solutions":["Have a channel admin perform the invite, or promote the caller to ChannelMemberRole.Admin","Hide/disable the add-members UI for non-admins on private channels","Check the caller's role via channel state before calling the endpoint"],"exampleFix":"// before\nawait api.addChannelMembers(channelId, userIds); // caller is Member on private channel\n// after\nif (channel.isPrivate && myRole !== 'Admin') {\n  throw new Error('Only channel admins can add members to private channels.');\n}\nawait api.addChannelMembers(channelId, userIds);","handlingStrategy":"validation","validationCode":"var caller = channel.RequireMember(currentUserId);\nif (channel.IsPrivate && caller.Role != ChannelMemberRole.Admin) return 403; // before calling","typeGuard":"bool mayInvite = !channel.IsPrivate || myRoleInChannel === 'Admin';","tryCatchPattern":"try { await addMembers(channelId, userIds); }\ncatch (ForbiddenException) { /* show 'admin required' message */ }","preventionTips":["Gate invite UI on the caller's per-channel role","Re-fetch role before sensitive actions","Document that private channels require Admin"],"tags":["chat","forbidden","permissions","private-channel"],"backgroundTag":"permission-denied","analyzedSha":"3f2959e683e9f83f13e55e1678c9119f63c7e8e5","analyzedAt":"2026-09-15T22:20:53.684Z","contentChangedAt":"2026-09-15T22:20:53.684Z","schemaVersion":2},"datasetVersion":"2026-09-16T04:17:20.429Z"}