fix(channel): allow any workspace member to create DM/group_dm channels #32
No reviewers
Labels
No labels
prio_critical
prio_low
type_bug
type_contact
type_issue
type_lead
type_question
type_story
type_task
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
lhumina_code/hero_collab!32
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "fix/dm-create-permission-gate"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Closes #30.
Two changes that together restore Slack-parity DM creation for ordinary workspace members:
Server-side:
channel.createadmin-gated every kind including DMs, blockingstartDmfor non-admin accounts. Fix splits the gate bykind— public/private remain admin-gated, DM/group_dm only require workspace membership.Client-side:
startDmfollowedchannel.createwith twochannel.member.addcalls for creator + peer. The creator self-add tripped channel.member.add's self-add guard (Cannot self-add to dm channel <N>: user_id matches caller_id) and surfaced as a console error. Replaced with passingpeer_ids: [userId]tochannel.createitself — the server's kind=='dm' branch (channel.rs:117-133) inserts the peer atomically under the same db.lock as the creator, and the existing dispatch at channel.rs:213-217 firesChannelAddedtoAudience::Users([creator, peer])covering both participants in one fanout.The two changes together fix the symptom end-to-end: opening the server-side gate alone produces a working DM but with the self-add console error; client-side adoption of the inline-peers contract eliminates the error and aligns with the contract intent at channel.rs:100-114.
What changed
Server (
crates/hero_collab_server/src/handlers/channel.rs::create)Permission gate splits by
kind:kind == "dm" || kind == "group_dm": bypasscheck_permission. Require an authenticated caller (Unauthenticatedif missing) and verify caller is a member ofworkspace_id. Membership probe is skipped underis_dev_mode()for parity withcheck_permission's blanket dev bypass — keeps--auth-mode=devdeveloper flows from needing pre-seededworkspace_membersrows for picker-selected identities.kind == "channel" || kind == "private": unchanged — still callscheck_permission("channel.create")exactly as before.Client (
crates/hero_collab_ui/static/js/chat-app.js::startDm)peer_ids: [userId]tochannel.create.channel.member.addcalls.Auth-mode regression check
currentUser.id===0channel.member.addwould also fail with caller_id=NoneAudience::Users([creator,peer])ChannelAddeddispatchTest plan
channel_members✓Cannot self-add to dm channel <N>: user_id matches caller_id) ✓Out of scope
🤖 Generated with Claude Code
`channel.create` routed every kind through the same admin gate (`channel_manage` / `workspace_admin`), which blocked ordinary users from starting DMs. Symptom from dogfooding: clicking a user in the DM picker surfaced `caller '<id>' lacks permission for 'channel.create'`, making 1:1 conversations unreachable for non-admin accounts. Slack-parity contract: any workspace member can DM any other workspace member without admin involvement. Public/private channels remain admin-gated. Fix splits the gate by kind. DM and group_dm bypass `check_permission` and instead require: - an authenticated caller (`Unauthenticated` if missing), - caller is a member of the target workspace (skipped under `is_dev_mode()` for parity with the blanket dev bypass in `check_permission`, so `--auth-mode=dev` developer flows don't need pre-seeded `workspace_members` rows for picker-selected identities). Public/private kinds continue to call `check_permission("channel.create")` exactly as before — no change in behaviour for the admin-gated path. Verified empirically: - viewer_test (non-admin, workspace member): DM create → 200 OK. - viewer_test: public channel create → still 403 PermissionDenied. - viewer_test: private channel create → still 403 PermissionDenied. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>`channel.create` for `kind=='dm'` already inserts both the creator (as admin) and the peer (as member) into channel_members atomically on the server (channel.rs::create, lines 85-133). The two `channel.member.add` calls in startDm were leftover from before that server-side change and are now actively harmful — the creator self-add hits `channel.member.add`'s "user_id matches caller_id" guard and surfaces as a console error: Error: Cannot self-add to dm channel <N>: user_id matches caller_id Symptom: every DM creation logs an error to the browser console even though the DM works (server-side inline-add already succeeded). Removing the now-redundant client-side adds is purely cosmetic — DM behaviour is unchanged. The peer-side `channel.member.add` was also redundant since the same `channel.create` for kind=='dm' inserts the peer too. Folded into the DM permission gate PR because it lives on the same flow (`startDm` → `channel.create('dm')` → DM ready) and only makes sense after the server-side permission gate is opened up — the two together produce the working "any workspace member can DM any other" behaviour. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>a4c6701b2dtoe3b066fb62