Skip to content

feat: add channel membership with access control and member list panel#161

Merged
mahata merged 2 commits intomainfrom
opencode/kind-falcon
Mar 30, 2026
Merged

feat: add channel membership with access control and member list panel#161
mahata merged 2 commits intomainfrom
opencode/kind-falcon

Conversation

@mahata
Copy link
Copy Markdown
Owner

@mahata mahata commented Mar 30, 2026

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 truth
  • GET /api/channels/:id/members — Returns member list (email + name) for a channel; 403 if the requester is not a member

Access Control Enforcement

  • GET /api/messages now returns 403 if the requesting user is not a member of the specified channel
  • WebSocket message send (ChatRoom Durable Object) now verifies sender membership before saving/broadcasting; non-members receive an error response

Real-time Membership Events

  • New memberJoin / memberLeave WebSocket event types broadcast to channel members when someone joins or leaves, so member lists update in real-time

UI — Members Panel

  • Collapsible members panel on the right side of the chat area showing all members of the active channel
  • "Members" toggle button in the channel header to show/hide the panel
  • Current user highlighted and sorted to the top

Client-side

  • Replaced fetchChannels() with fetchMemberships() calling GET /api/channels/memberships — server is now the source of truth for channel membership on page load
  • On channel switch, fetches and renders the member list
  • Handles incoming memberJoin/memberLeave WebSocket events

Tests

  • 21 channel route tests (added 6 for new endpoints)
  • 5 message route tests (added 1 for membership enforcement)
  • 8 ChatPage component tests (added 2 for members panel)
  • All 108 tests pass, lint clean, build succeeds

- 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
Copilot AI review requested due to automatic review settings March 30, 2026 05:51
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

  • handleMembershipEvent currently adds event.userEmail to the recipient set for memberJoin, 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/memberLeave events. As written, a join request that returns "Already a member" will still broadcast a memberJoin event, 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);
      }
    }

Comment thread hono/routes/channels.ts
Comment thread hono/durableObjects/ChatRoom.ts
Comment thread hono/static/ChatPage.ts
Comment thread hono/components/ChatPage.tsx
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
@mahata mahata merged commit 5623790 into main Mar 30, 2026
3 checks passed
@mahata mahata deleted the opencode/kind-falcon branch March 30, 2026 06:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants