Files
multica/server/migrations/041_workspace_invitation.up.sql
Bohan Jiang ff1d348274 feat(security): invitation acceptance flow for workspace members (#1019)
* feat(security): replace instant member-add with invitation acceptance flow

Users invited to a workspace must now explicitly accept the invitation
before becoming a member. This fixes the security vulnerability where
knowing someone's email was enough to auto-register their runtime to
your workspace.

Changes:
- Add workspace_invitation table with pending/accepted/declined/expired states
- Replace CreateMember with CreateInvitation (same endpoint, new behavior)
- Add accept/decline/revoke/list invitation API endpoints
- Add invitation WS events for real-time notification
- Frontend: invitation accept/decline UI in workspace switcher
- Frontend: pending invitations section in members settings tab

* fix(invitation): address PR review nits

- Fix invitation:revoked listener to send event to invitee user (was no-op)
- Remove duplicate queryClient2 in app-sidebar.tsx, reuse existing queryClient
- Add expires_at > now() filter to ListPendingInvitationsByWorkspace query
2026-04-15 00:01:18 +08:00

21 lines
1.1 KiB
SQL

CREATE TABLE workspace_invitation (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
workspace_id UUID NOT NULL REFERENCES workspace(id) ON DELETE CASCADE,
inviter_id UUID NOT NULL REFERENCES "user"(id),
invitee_email TEXT NOT NULL,
invitee_user_id UUID REFERENCES "user"(id),
role TEXT NOT NULL CHECK (role IN ('admin', 'member')),
status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'accepted', 'declined', 'expired')),
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
expires_at TIMESTAMPTZ NOT NULL DEFAULT now() + INTERVAL '7 days'
);
-- Only one pending invitation per workspace + email at a time.
CREATE UNIQUE INDEX idx_invitation_unique_pending
ON workspace_invitation(workspace_id, invitee_email) WHERE status = 'pending';
-- Fast lookup of pending invitations for a user (by email or user_id).
CREATE INDEX idx_invitation_invitee_email ON workspace_invitation(invitee_email) WHERE status = 'pending';
CREATE INDEX idx_invitation_invitee_user ON workspace_invitation(invitee_user_id) WHERE status = 'pending';