Skip to content

feat: added invite links for admins and UI for users accepting invites#19

Merged
BuckyMcYolo merged 4 commits intomainfrom
dev
Mar 17, 2026
Merged

feat: added invite links for admins and UI for users accepting invites#19
BuckyMcYolo merged 4 commits intomainfrom
dev

Conversation

@BuckyMcYolo
Copy link
Copy Markdown
Owner

@BuckyMcYolo BuckyMcYolo commented Mar 17, 2026

Summary

This PR implements a complete guild invite system spanning API, database schema, realtime types/handlers, and web UI. Key additions:

  • Backend API:

    • New invites routes and handlers under v1:
      • POST /guilds/{guildSlug}/invites — create invite (guild auth)
      • GET /guilds/{guildSlug}/invites — list invites (guild kick permission)
      • DELETE /guilds/{guildSlug}/invites/{code} — revoke invite (guild auth)
      • GET /invites/{code} — preview invite (session)
      • POST /invites/{code}/accept — accept invite (session)
    • Handler logic includes invite code generation with retry on unique constraint, expiry/max-uses checks, ban enforcement, transactional acceptance (membership creation + invite uses increment), and detailed errors.
  • Database:

    • New guild_invite table/schema (packages/db/src/schemas/guild-invites.ts) with:
      • id (UUID), guildId, code (unique), inviterId, channelId, maxUses, uses, expiresAt, createdAt
      • Relations to guild, inviter (user), and channel
    • Schema exported via packages/db/src/schemas/index.ts and referenced from packages/db/src/schemas/guilds.ts.
  • Realtime:

    • New realtime types/events added (packages/realtime-types): guild:member:joined and typing indicator events (typing:start, typing:update).
    • Server socket handlers: typing:start handler and guild:member:joined handler added to apps/realtime/src/index.ts.
    • Frontend listens for guild:member:joined and invalidates member list; accept flow emits guild:member:joined from client on successful accept.
  • Frontend:

    • CreateInviteDialog and ManageInvitesDialog components to create and manage shareable invite links.
    • Guild header dropdown wired to open invite dialogs when user has manage-invite permission.
    • Invite accept page at /_authenticated/invite/$code with preview and accept flows (shows expired/maxed-out states, handles errors, emits guild:member:joined on success).
    • New API types for invites, typing indicator UI and hook, and wiring of typing indicator into DMs and channel views.
    • MessageInput gains optional onTyping prop to support typing emissions.

Known Gaps / Issues (observed)

  • Missing committed DB migrations: the Drizzle schema for guild_invite exists (packages/db/src/schemas/guild-invites.ts) but there are no generated migration files checked into the repo (no packages/db/drizzle or migrations output found). Deploying this change will require adding generated Drizzle migration(s) or running db:push in the deployment pipeline.
  • No server-side realtime broadcast observed for member-join after accept: I could not find code that publishes a server-side guild:member:joined broadcast from the accept-invite flow; the current flow relies on the client to emit guild:member:joined after accepting, which means other clients may not reliably receive the join event unless clients always emit it.

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.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 17, 2026

Caution

Review failed

Pull request was closed or merged during review

📝 Walkthrough

Walkthrough

Adds 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

Cohort / File(s) Summary
Roadmap
ROADMAP.md
Marks "Shareable invite links" as in-progress; notes schema, API, and UI implemented.
API app wiring
apps/api/src/app.ts
Mounts new invites router at /v1.
Invite API (routes / handlers / schema / index)
apps/api/src/routes/v1/invites/... (handlers.ts, routes.ts, schema.ts, index.ts)
Adds create/list/delete (guild-scoped) and preview/accept (public/session) endpoints, Zod/OpenAPI schemas, permission checks, unique code generation with retries, transactional accept flow, and error handling.
Database schema & relations
packages/db/src/schemas/guild-invites.ts, packages/db/src/schemas/guilds.ts, packages/db/src/schemas/index.ts
Defines guild_invite table, unique code index, relations to guild/user/channel, and attaches invites relation to guilds; re-exports schema.
Realtime types & server
packages/realtime-types/src/events.ts, apps/realtime/src/index.ts
Adds typing and guild:member:joined event schemas/types; server handlers for typing:start (emits typing:update) and guild:member:joined (validates, joins socket to guild room, broadcasts).
Web — invite UI components
apps/web/src/components/invite/create-invite-dialog.tsx, apps/web/src/components/invite/manage-invites-dialog.tsx
Adds dialogs to create, copy, and revoke invites; integrates react-query mutations/queries, toasts, and confirm flows.
Web — guild header integration
apps/web/src/components/sidebar/channel-panel/guild-header.tsx
Adds permission-aware dropdown with "Invite People" and "Manage Invites" actions and dialog state.
Web — invite route & types
apps/web/src/routes/_authenticated/invite/$code.tsx, apps/web/src/lib/api-types.ts
Adds invite preview/accept route, response types, accept flow with socket emit and navigation.
Web — member list realtime
apps/web/src/components/sidebar/right-panel/guild-members-panel.tsx
Listens for guild:member:joined and invalidates member list query to refresh members.
Web — typing indicator infra
apps/web/src/components/chat/typing-indicator.tsx, apps/web/src/hooks/use-typing-indicator.ts, apps/web/src/components/chat/composer/message-input.tsx, apps/web/src/routes/_authenticated/$guildSlug/$channelId.tsx, apps/web/src/routes/_authenticated/dms/$dmId.tsx
Adds TypingIndicator component and useTypingIndicator hook (throttled emit, per-user expiry, cleanup), passes onTyping from MessageInput, integrates indicator UI in channel and DM routes.
Misc / manifest & imports
package.json, other small imports
Adds schema/type exports and small import adjustments (realtime-types, db imports).

Sequence Diagram

sequenceDiagram
    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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • Dev #10 — Adds/aligns realtime typing and guild-member-joined events and related server/client wiring; strong overlap with event handlers and types.
  • Dev #12 — Modifies MessageInputProps and typing-related integration; directly related to the onTyping prop and upstream typing flow.
  • feat: added 'v1' prefix to add external facing routes #1 — Introduced or adjusted v1 app routing scaffold; this PR mounts the new invites router into that v1 chain.

Poem

🐰 A hop, a link, a gentle codey cheer,
I plant a door so new friends may appear,
Copy, revoke, or let the countdown run,
Typing dots shimmer—another join begun,
Nibble the carrot, welcome’s served with cheer!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: new invite link creation functionality for admins and UI for users to accept invites, matching the substantial additions across API routes, handlers, database schema, and frontend components.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch dev
📝 Coding Plan
  • Generate coding plan for human review comments

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

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

📥 Commits

Reviewing files that changed from the base of the PR and between d19a34b and ffe8f63.

📒 Files selected for processing (17)
  • ROADMAP.md
  • apps/api/src/app.ts
  • apps/api/src/routes/v1/invites/handlers.ts
  • apps/api/src/routes/v1/invites/index.ts
  • apps/api/src/routes/v1/invites/routes.ts
  • apps/api/src/routes/v1/invites/schema.ts
  • apps/realtime/src/index.ts
  • apps/web/src/components/invite/create-invite-dialog.tsx
  • apps/web/src/components/invite/manage-invites-dialog.tsx
  • apps/web/src/components/sidebar/channel-panel/guild-header.tsx
  • apps/web/src/components/sidebar/right-panel/guild-members-panel.tsx
  • apps/web/src/lib/api-types.ts
  • apps/web/src/routes/_authenticated/invite/$code.tsx
  • packages/db/src/schemas/guild-invites.ts
  • packages/db/src/schemas/guilds.ts
  • packages/db/src/schemas/index.ts
  • packages/realtime-types/src/events.ts

Comment thread apps/api/src/routes/v1/invites/handlers.ts Outdated
Comment thread apps/api/src/routes/v1/invites/routes.ts
Comment thread apps/realtime/src/index.ts
Comment thread apps/web/src/components/invite/create-invite-dialog.tsx
Comment thread apps/web/src/components/invite/manage-invites-dialog.tsx Outdated
Comment thread apps/web/src/components/invite/manage-invites-dialog.tsx
Comment thread apps/web/src/components/sidebar/channel-panel/guild-header.tsx
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

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

📥 Commits

Reviewing files that changed from the base of the PR and between ffe8f63 and bb5ad15.

📒 Files selected for processing (11)
  • apps/api/src/routes/v1/invites/handlers.ts
  • apps/realtime/src/index.ts
  • apps/web/src/components/chat/composer/message-input.tsx
  • apps/web/src/components/chat/typing-indicator.tsx
  • apps/web/src/components/invite/create-invite-dialog.tsx
  • apps/web/src/components/invite/manage-invites-dialog.tsx
  • apps/web/src/components/sidebar/channel-panel/guild-header.tsx
  • apps/web/src/hooks/use-typing-indicator.ts
  • apps/web/src/routes/_authenticated/$guildSlug/$channelId.tsx
  • apps/web/src/routes/_authenticated/dms/$dmId.tsx
  • packages/realtime-types/src/events.ts

Comment thread apps/api/src/routes/v1/invites/handlers.ts Outdated
Comment thread apps/api/src/routes/v1/invites/handlers.ts Outdated
Comment thread apps/realtime/src/index.ts Outdated
Comment thread apps/web/src/components/invite/create-invite-dialog.tsx
Comment thread apps/web/src/components/invite/create-invite-dialog.tsx
Comment thread apps/web/src/components/invite/manage-invites-dialog.tsx
Comment thread apps/web/src/components/sidebar/channel-panel/guild-header.tsx
@BuckyMcYolo BuckyMcYolo merged commit a73b4dd into main Mar 17, 2026
1 check was pending
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.

1 participant