{"record":{"id":"f56657a0e3054ef3","repo":"fullstackhero/dotnet-starter-kit","slug":"channel-admin-role-required","errorCode":null,"errorMessage":"Channel admin role required.","messagePattern":"Channel admin role required\\.","errorType":"exception","errorClass":"ForbiddenException","httpStatus":403,"severity":"error","filePath":"src/Modules/Chat/Modules.Chat/Features/v1/Internal/ChannelAuthorization.cs","lineNumber":25,"sourceCode":"/// <summary>\n/// Small assertion helpers used by channel/message handlers so the rules stay in one place.\n/// Throws framework-aware exceptions so the global handler emits the right HTTP status.\n/// </summary>\ninternal static class ChannelAuthorization\n{\n    public static ChannelMember RequireMember(this ChatChannel channel, string userId)\n    {\n        var member = channel.Members.FirstOrDefault(m => string.Equals(m.UserId, userId, StringComparison.Ordinal));\n        // Use NotFoundException (404) instead of Forbidden so non-members can't probe channel existence.\n        return member ?? throw new NotFoundException(\"Channel not found.\");\n    }\n\n    public static ChannelMember RequireAdmin(this ChatChannel channel, string userId)\n    {\n        var member = channel.RequireMember(userId);\n        if (member.Role != ChannelMemberRole.Admin)\n        {\n            throw new ForbiddenException(\"Channel admin role required.\");\n        }\n        return member;\n    }\n}\n","sourceCodeStart":7,"sourceCodeEnd":30,"githubUrl":"https://github.com/fullstackhero/dotnet-starter-kit/blob/3f2959e683e9f83f13e55e1678c9119f63c7e8e5/src/Modules/Chat/Modules.Chat/Features/v1/Internal/ChannelAuthorization.cs#L7-L30","documentation":"ChannelAuthorization.RequireAdmin throws ForbiddenException(\"Channel admin role required.\") when the user is a member of the channel but their ChannelMemberRole is not Admin. It is raised after RequireMember succeeds, so the user is authenticated and a member — just not privileged enough. Maps to HTTP 403.","triggerScenarios":"UpdateChannelCommandHandler calls channel.RequireAdmin(userId); a plain Member (or Owner with non-admin role) attempts to rename a channel, change its description, or toggle IsPrivate.","commonSituations":"A regular member trying to rename a channel in the UI where the button was wrongly enabled; role demoted from Admin to Member after the client cached stale permissions; frontend not checking the member role before offering admin actions.","solutions":["Have an existing channel Admin promote the user: set their ChannelMemberRole to Admin.","Hide/disable admin-only UI actions unless the current member's role is Admin.","Confirm the membership role loaded in the handler is current (stale cache may hold an old role).","If the operation should be member-wide, relax the handler to RequireMember instead — only with product approval."],"exampleFix":"// before\nchannel.RequireAdmin(userId); // throws for plain members\n\n// after\nvar member = channel.Members.First(m => m.UserId == userId);\nif (member.Role != ChannelMemberRole.Admin)\n{\n    member.Role = ChannelMemberRole.Admin; // promoted by an existing admin beforehand\n}\nchannel.RequireAdmin(userId);","handlingStrategy":"type-guard","validationCode":"const me = channel.members.find(m => m.userId === currentUserId);\nif (!me || me.role !== \"admin\") {\n  // hide admin actions / show \"admin required\" notice\n  return;\n}","typeGuard":"function isAdminMember(m: { userId: string; role: string } | undefined, userId: string): m is { userId: string; role: \"admin\" } {\n  return !!m && m.userId === userId && m.role === \"admin\";\n}","tryCatchPattern":"try {\n  await api.updateChannel({ channelId, name });\n} catch (e) {\n  if (e.status === 403) { notify(\"Channel admin role required.\"); }\n  else throw e;\n}","preventionTips":["Gate admin UI actions on the member role, not just membership.","Re-fetch role after any demotion event.","Don't cache roles longer than the channel membership query.","Promote users via an admin before assigning them admin workflows."],"tags":["chat","authorization","forbidden","roles"],"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"}