feat: added invite links for admins and UI for users accepting invites#19
feat: added invite links for admins and UI for users accepting invites#19BuckyMcYolo merged 4 commits intomainfrom
Conversation
|
Caution Review failedPull request was closed or merged during review 📝 WalkthroughWalkthroughAdds a complete guild invite system (DB schema, API routes, schemas, handlers), web UI for creating/managing/accepting invites, realtime typing and guild-member-joined events (types, server handlers, client hook/UI), and mounts the invites router under the v1 API surface. Changes
Sequence DiagramsequenceDiagram
participant User as User
participant Web as Web Client
participant API as API Server
participant DB as Database
participant Realtime as Realtime Socket
participant Members as Other Clients
User->>Web: Click "Accept Invite"
Web->>API: POST /v1/invites/{code}/accept
Note over API,DB: Validate invite (code, expiry, uses, bans)\nfetch invite + guild/channel
API->>DB: Read invite record
DB-->>API: Invite record
API->>DB: Begin transaction: insert member, increment uses
DB-->>API: Commit success
API-->>Web: 200 OK + guild info
Web->>Realtime: emit "guild:member:joined" (guildId,user)
Realtime->>Realtime: validate payload, join socket to guild room
Realtime->>Members: broadcast "guild:member:joined" to room
Members->>Web: Other clients receive event -> refresh members UI
Web->>Web: Invalidate queries, navigate to guild
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
📝 Coding Plan
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 7
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/api/src/routes/v1/invites/handlers.ts`:
- Around line 161-173: The current code filters expired/maxed invites in memory
after fetching all rows (see invites, rows, isInviteExpired, isInviteMaxedOut,
toInviteResponse); change the database query that populates rows to only return
active invites by adding WHERE conditions to exclude expired invites and invites
that reached max uses (e.g., WHERE (expires_at IS NULL OR expires_at > now())
AND (max_uses IS NULL OR uses < max_uses)), then keep the mapping logic to
toInviteResponse but remove the in-memory .filter; update the query used where
rows is assigned so the DB does the filtering.
In `@apps/api/src/routes/v1/invites/routes.ts`:
- Around line 12-22: In routes.ts replace the relative import of the schema
module with the app alias form so the listed symbols
(acceptInviteResponseSchema, createInviteRequestSchema,
createInviteResponseSchema, deleteInviteResponseSchema,
guildInviteCodeParamsSchema, guildSlugParamsSchema, inviteCodeParamsSchema,
invitePreviewResponseSchema, listInvitesResponseSchema) are imported via the
repo alias (e.g., change the "./schema" import to the corresponding "@/..." path
used in apps/api, keeping the same imported names) so tsc-alias can rewrite
paths during build; update only the import path in the top-level import in
routes.ts.
In `@apps/realtime/src/index.ts`:
- Around line 456-477: The handler registered in
socket.on("guild:member:joined") currently trusts the payload guildId and allows
any authenticated socket to join/broadcast any guild room and may duplicate
entries in socket.data.guildIds; add an authorization check that verifies the
current user (socket.data.user or its id) is actually a member of parsed.guildId
before calling socket.join(guildRoom(parsed.guildId)) and emitting the event,
and return ack with an authorization error if not allowed; additionally, when
updating socket.data.guildIds ensure you deduplicate (e.g., only push
parsed.guildId if not already present) to avoid duplicates; use the existing
guildMemberJoinedPayloadSchema, guildRoom, socket.data guild/user fields, and
toErrorMessage in your changes.
In `@apps/web/src/components/invite/create-invite-dialog.tsx`:
- Around line 142-153: The icon-only copy Button used in the create-invite
dialog (the Button element with onClick={handleCopy} showing Check or Copy based
on the copied state) lacks an accessible name; add an accessible label by
supplying an aria-label (e.g., aria-label={copied ? "Copied link" : "Copy
link"}) or include sr-only text inside the Button so screen readers can identify
the action while keeping the visual icon-only appearance. Ensure the label
updates with the copied state to reflect the current action.
In `@apps/web/src/components/invite/manage-invites-dialog.tsx`:
- Around line 37-48: The query currently swallows fetch failures and the UI
falls back to "No active invites"; update the useQuery in ManageInvitesDialog
(the queryKey ["guild-invites", guildSlug] / queryFn) to surface errors instead
of returning an empty list: when apiClient.v1.guilds[":guildSlug"].invites.$get
returns a non-ok response, throw an Error with the response error/details (or
JSON message) so react-query sets isError/error; then update the component
render logic (the branch that shows "No active invites") to check isError and
show an error state/message when isError is true (or propagate the error to an
ErrorBoundary), and apply the same change to the other similar useQuery instance
around the later invite list code.
- Around line 141-156: The icon-only invite action buttons lack accessible
names; update the two Button components that call handleCopy(invite.code) and
setRevokeCode(invite.code) to provide an accessible label (e.g., aria-label or
aria-labelledby) describing the action and target (like "Copy invite code
{invite.code}" and "Revoke invite {invite.code}"), or include a visually-hidden
text node inside the Button; ensure the labels reference invite.code so screen
readers convey which invite is affected and keep existing onClick handlers
(handleCopy and setRevokeCode) unchanged.
In `@apps/web/src/components/sidebar/channel-panel/guild-header.tsx`:
- Around line 52-59: The invite menu items are currently shown to everyone;
restrict them to admin-capable members by conditional rendering: compute a
boolean (e.g., canManageInvites) from the current member/permissions state used
in this component (check whatever existing property you have like
currentMember.permissions, currentMember.isAdmin, or
hasPermission('MANAGE_INVITES')), then wrap the two DropdownMenuItem elements
that call setInviteDialogOpen and setManageInvitesOpen in a conditional render
that only displays them when canManageInvites is true (keep the existing
DropdownMenuItem handlers and icons intact).
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: e1e454b2-1a08-4dd7-b871-776ab55350b0
📒 Files selected for processing (17)
ROADMAP.mdapps/api/src/app.tsapps/api/src/routes/v1/invites/handlers.tsapps/api/src/routes/v1/invites/index.tsapps/api/src/routes/v1/invites/routes.tsapps/api/src/routes/v1/invites/schema.tsapps/realtime/src/index.tsapps/web/src/components/invite/create-invite-dialog.tsxapps/web/src/components/invite/manage-invites-dialog.tsxapps/web/src/components/sidebar/channel-panel/guild-header.tsxapps/web/src/components/sidebar/right-panel/guild-members-panel.tsxapps/web/src/lib/api-types.tsapps/web/src/routes/_authenticated/invite/$code.tsxpackages/db/src/schemas/guild-invites.tspackages/db/src/schemas/guilds.tspackages/db/src/schemas/index.tspackages/realtime-types/src/events.ts
There was a problem hiding this comment.
Actionable comments posted: 7
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/api/src/routes/v1/invites/handlers.ts`:
- Line 447: Implement the missing realtime broadcast for the "member:join" event
where the TODO comment exists: after the invite acceptance logic commits the new
membership (in the invite accept handler), call the realtime/socket broadcasting
utility (e.g., RealtimeService.broadcastToGuild or io.to/guildRoom) to emit
"member:join" with payload { guildId, memberId, user: { id, name, avatar },
role, createdAt } to the guild channel, ensuring you exclude the joining user's
own socket connection so other members are notified; place this broadcast
immediately after the membership is persisted and before sending the HTTP
response.
- Around line 344-420: The code is vulnerable to races because existingMember
and isInviteMaxedOut checks run outside the db.transaction while
tx.insert(schema.guildMember) and the invite uses increment run inside; fix by
moving those validations into the same transaction and applying row-level
locking or enforce uniqueness and handle constraint errors: inside
db.transaction, SELECT the invite row FOR UPDATE (or use an atomic UPDATE WHERE
uses < maxUses RETURNING) to verify and increment uses, SELECT the guildMember
row FOR UPDATE (or attempt the insert and catch unique constraint violation) to
prevent duplicate memberships, and update the codepaths around existingMember,
isInviteMaxedOut, tx.insert(schema.guildMember) and the invite increment so all
checks/updates occur within the same transaction (or add a
UNIQUE(guildId,userId) constraint on schema.guildMember and handle the
constraint violation when inserting).
In `@apps/realtime/src/index.ts`:
- Around line 457-468: The typing:start handler currently trusts the payload and
emits to channelRoom(parsed.channelId) without checking membership; update the
socket.on("typing:start") flow to validate that socket.data.user is authorized
for parsed.channelId (use the same channel access check used by other handlers —
e.g., the existing channel membership/authorization helper or service used
elsewhere in this file) after parsing with typingStartPayloadSchema and before
calling socket.to(...).emit, and if the check fails simply return/ignore so
unauthorized users cannot spoof typing events.
In `@apps/web/src/components/invite/create-invite-dialog.tsx`:
- Around line 59-93: The create-invite mutation can complete after the dialog
has closed and then repopulate inviteCode; update the mutation callbacks
(createMutation.onSuccess and onError) to first check the dialog open state
(e.g., an isOpen / isDialogOpen boolean from component props/state) and return
early if the dialog is closed before calling setInviteCode or showing a toast;
apply the same guard to the other create-invite mutation instances noted (the
ones referenced at lines 111-120 and 216-224) so in-flight results are ignored
when the dialog is not open.
- Around line 135-140: Generate stable IDs with React's useId and assign them to
the form controls and labels: import useId in create-invite-dialog.tsx, call
const inviteInputId = useId() (and similarly roleSelectId, maxUsesSelectId), set
the Label for the invite input to htmlFor={inviteInputId} and add
id={inviteInputId} to the Input that renders getInviteUrl(inviteCode), and for
each SelectTrigger referenced by the other Label elements set the Label id
(e.g., id={roleSelectId}-label) and add aria-labelledby={roleSelectId +
'-label'} on the corresponding SelectTrigger (repeat for maxUses), ensuring you
update the Label/SelectTrigger props with the exact identifier names
(inviteInputId, roleSelectId, maxUsesSelectId) so labels are programmatically
associated with their controls.
In `@apps/web/src/components/invite/manage-invites-dialog.tsx`:
- Around line 171-193: The AlertDialog auto-closes when AlertDialogAction is
clicked, so change the onClick handler to call event.preventDefault() first (so
the dialog doesn't close immediately) and then call
revokeMutation.mutate(revokeCode); rely on the existing revokeMutation onSuccess
callback that calls setRevokeCode(null) to close the dialog; ensure the handler
still respects revokeCode truthiness and revokeMutation.isPending to disable the
button while revoking.
In `@apps/web/src/components/sidebar/channel-panel/guild-header.tsx`:
- Around line 32-40: The current queryFn masks all getActiveMember() failures by
returning null on any res.error, conflating network/auth failures with a
legitimate permission denial; update the queryFn in the useQuery for
["active-guild-member", guildSlug] so that it only returns null for an explicit
permission-denied response (e.g., res.error.status === 403 or an indicated "no
permission" code) but throws the error for other failures so the query error
surfaces (alternatively just throw res.error on non-403 errors); keep the rest
of the hook (queryKey, enabled) unchanged and ensure callers still handle
activeMember possibly being null for the real permission-denied case.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: a1093719-3baf-44c6-96d3-2a9f35cfb2f1
📒 Files selected for processing (11)
apps/api/src/routes/v1/invites/handlers.tsapps/realtime/src/index.tsapps/web/src/components/chat/composer/message-input.tsxapps/web/src/components/chat/typing-indicator.tsxapps/web/src/components/invite/create-invite-dialog.tsxapps/web/src/components/invite/manage-invites-dialog.tsxapps/web/src/components/sidebar/channel-panel/guild-header.tsxapps/web/src/hooks/use-typing-indicator.tsapps/web/src/routes/_authenticated/$guildSlug/$channelId.tsxapps/web/src/routes/_authenticated/dms/$dmId.tsxpackages/realtime-types/src/events.ts
Summary
This PR implements a complete guild invite system spanning API, database schema, realtime types/handlers, and web UI. Key additions:
Backend API:
Database:
Realtime:
Frontend:
Known Gaps / Issues (observed)
Confidence Score: 2/5
Rationale: The feature is well-designed and consistent with existing patterns (transactional acceptance, permission checks, typed API, and UI integration). However, the lack of committed DB migrations and the absence of a server-side realtime broadcast for membership joins are notable gaps that should be resolved before merging.