fullstackhero/dotnet-starter-kit · error · ForbiddenException

Only channel admins can add members to private channels.

Error message

Only channel admins can add members to private channels.

What it means

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.

Solutions

  1. Have a channel admin perform the invite, or promote the caller to ChannelMemberRole.Admin
  2. Hide/disable the add-members UI for non-admins on private channels
  3. Check the caller's role via channel state before calling the endpoint

Example fix

// before
await api.addChannelMembers(channelId, userIds); // caller is Member on private channel
// after
if (channel.isPrivate && myRole !== 'Admin') {
  throw new Error('Only channel admins can add members to private channels.');
}
await api.addChannelMembers(channelId, userIds);
Defensive patterns

Strategy: validation

Validate before calling

var caller = channel.RequireMember(currentUserId);
if (channel.IsPrivate && caller.Role != ChannelMemberRole.Admin) return 403; // before calling

Type guard

bool mayInvite = !channel.IsPrivate || myRoleInChannel === 'Admin';

Try / catch

try { await addMembers(channelId, userIds); }
catch (ForbiddenException) { /* show 'admin required' message */ }

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Understand the failure class

Background: Permission denied / not authorized / 403 Forbidden: access-control rejections when the caller lacks the required role, grant, or ownership — this error's family across 18 libraries.

Related errors


AI-assisted analysis of fullstackhero/dotnet-starter-kit@3f2959e683 (2026-09-15). Data as JSON: /api/errors/e1189499f8818101. Report an issue: GitHub.

Appendix: source

Thrown at src/Modules/Chat/Modules.Chat/Features/v1/Channels/AddChannelMembers/AddChannelMembersCommandHandler.cs:35

    IHubContext<AppHub> hub)
    : ICommandHandler<AddChannelMembersCommand, Unit>
{
    public async ValueTask<Unit> Handle(AddChannelMembersCommand cmd, CancellationToken cancellationToken)
    {
        ArgumentNullException.ThrowIfNull(cmd);
        var userId = currentUser.GetUserId();
        if (userId == Guid.Empty) throw new UnauthorizedException("no current user");
        var currentUserId = userId.ToString();

        var channel = await db.Channels.FirstOrDefaultAsync(c => c.Id == cmd.ChannelId, cancellationToken)
            .ConfigureAwait(false)
            ?? throw new NotFoundException("Channel not found.");

        // Members can invite to public channels they belong to; private channels require Admin.
        var caller = channel.RequireMember(currentUserId);
        if (channel.IsPrivate && caller.Role != ChannelMemberRole.Admin)
        {
            throw new ForbiddenException("Only channel admins can add members to private channels.");
        }

        var newlyAdded = new List<string>();
        foreach (var uid in cmd.UserIds.Distinct(StringComparer.Ordinal))
        {
            // Skip duplicates silently — endpoint is idempotent for already-members.
            if (channel.Members.Any(m => string.Equals(m.UserId, uid, StringComparison.Ordinal))) continue;
            channel.AddMember(uid, currentUserId);
            newlyAdded.Add(uid);
        }

        await db.SaveChangesAsync(cancellationToken).ConfigureAwait(false);

        foreach (var uid in newlyAdded)
        {
            // Notify existing members. The new member isn't in the channel:{id} group yet; they pick
            // it up on next reconnect (OnConnectedAsync pre-joins all their channels).
            await hub.Clients.Group($"channel:{channel.Id}")

View on GitHub (pinned to 3f2959e683)