feat: add channel membership with access control and member list panel#161
Merged
feat: add channel membership with access control and member list panel#161
Conversation
- Add GET /api/channels/memberships endpoint returning joined vs browsable channels - Add GET /api/channels/:id/members endpoint with membership gate - Enforce membership check on GET /api/messages (403 for non-members) - Enforce membership check on WebSocket message send in ChatRoom - Broadcast memberJoin/memberLeave events via WebSocket for real-time updates - Add collapsible members panel in chat UI showing channel members - Update client to use memberships endpoint as source of truth on page load - Add unit tests for all new endpoints and access control
Contributor
There was a problem hiding this comment.
Pull request overview
Adds first-class channel membership as a server-backed source of truth, enforcing access control for both HTTP message history and WebSocket message sending, and introduces a channel members panel in the chat UI.
Changes:
- Added channel membership APIs (
/api/channels/memberships,/api/channels/:id/members) and enforced membership checks for message reads. - Enforced membership checks in the ChatRoom Durable Object before persisting/broadcasting messages.
- Added a right-side members panel UI, toggle button, and client logic to fetch/render members and react to membership events.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| hono/static/ChatPage.ts | Client: switch to memberships endpoint, fetch/render member list, members panel toggle, send/receive membership WS events |
| hono/routes/messages.ts | Server: enforce channel membership for GET /api/messages |
| hono/routes/messages.test.ts | Tests: add coverage for membership enforcement on messages endpoint |
| hono/routes/channels.ts | Server: add memberships endpoint and channel members endpoint |
| hono/routes/channels.test.ts | Tests: add coverage for new channel endpoints |
| hono/durableObjects/ChatRoom.ts | DO: enforce membership for sending; add membership event broadcast handling |
| hono/components/ChatPage.tsx | UI: add members panel markup and toggle button |
| hono/components/ChatPage.test.ts | Tests: assert members panel + toggle exist in rendered HTML |
| hono/components/ChatPage.css | Styles: layout for members panel and toggle button |
Comments suppressed due to low confidence (2)
hono/durableObjects/ChatRoom.ts:148
handleMembershipEventcurrently addsevent.userEmailto the recipient set formemberJoin, which can cause the event to be delivered to a non-member if a client spoofs the payload. Prefer computing recipients strictly from authoritative membership data (DB) and only broadcasting to those members; if the joiner should receive the event, rely on the DB membership already being committed rather than trusting the payload.
try {
const db = getDb(this.env.DB);
const members = await db.select().from(channelMembers).where(eq(channelMembers.channelId, event.channelId));
const memberEmails = new Set(members.map((m) => m.userEmail));
if (event.type === "memberJoin") {
memberEmails.add(event.userEmail);
}
hono/static/ChatPage.ts:254
- The membership UI is driven by client-sent
memberJoin/memberLeaveevents. As written, a join request that returns "Already a member" will still broadcast amemberJoinevent, and a failed join/leave could leave other clients out of sync. Consider emitting these events from the server after a successful join/leave (or at least keying off the join/leave response so events are only sent when membership actually changes).
async function joinChannel(channelId: number): Promise<void> {
try {
const response = await fetch(`/api/channels/${channelId}/join`, { method: "POST" });
if (response.ok) {
myChannelIds.add(channelId);
notifyMembershipChange("memberJoin", channelId);
const ch = allChannels.find((c) => c.id === channelId);
renderChannelLists();
if (ch) {
await switchChannel(channelId, ch.name);
}
}
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements proper channel membership so users see only channels they belong to, are blocked from accessing channels they haven't joined, and can view the member list of any channel they're in.
Changes
New API Endpoints
GET /api/channels/memberships— Returns{ myChannels, otherChannels }for the authenticated user, replacing client-side guessing with server as source of truthGET /api/channels/:id/members— Returns member list (email + name) for a channel; 403 if the requester is not a memberAccess Control Enforcement
GET /api/messagesnow returns 403 if the requesting user is not a member of the specified channelReal-time Membership Events
memberJoin/memberLeaveWebSocket event types broadcast to channel members when someone joins or leaves, so member lists update in real-timeUI — Members Panel
Client-side
fetchChannels()withfetchMemberships()callingGET /api/channels/memberships— server is now the source of truth for channel membership on page loadmemberJoin/memberLeaveWebSocket eventsTests