Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughThis PR renames the core domain model from workspace/channel to organization/team across the entire application—schemas, middleware, ORPC routers, realtime providers, and UI components—and rebrands the product from TeamFlow to TeamComms throughout copy, assets, and configuration. ChangesTeamFlow to TeamComms Rebrand
Organization/Team Domain Refactor
Estimated code review effort: 5 (Critical) | ~120 minutes Sequence Diagram(s)sequenceDiagram
participant MessageInput
participant MessageRouter as message router
participant Prisma
participant RealtimeProvider as RealtimeTeamProvider
participant Cache as React Query Cache
MessageInput->>MessageRouter: createMessage({teamId, content})
MessageRouter->>Prisma: findFirst team(teamId, organizationId)
Prisma-->>MessageRouter: team
MessageRouter->>Prisma: create message(teamId)
Prisma-->>MessageRouter: message
MessageRouter-->>MessageInput: created message
MessageInput->>RealtimeProvider: send(message:created)
RealtimeProvider->>Cache: setQueryData(["message.list", teamId])
Possibly related PRs
Suggested labels: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 8
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (4)
app/router/organization.ts (2)
555-572: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy liftMake leave cleanup reliable before the operation is considered complete.
If
auth.api.leaveOrganizationsucceeds andprisma.teamMember.deleteManyfails, the user has left the organization but stale team memberships remain. Move this cleanup into a durable hook/outbox/retry path or another atomic ownership point.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/router/organization.ts` around lines 555 - 572, The leave flow in organization cleanup is not reliable because `auth.api.leaveOrganization` can succeed while the follow-up `prisma.teamMember.deleteMany` in the same handler fails, leaving stale memberships behind. Move the cleanup out of the `leaveOrganization` request path into a durable ownership point such as a hook, outbox, or retryable job, and reference the `leaveOrganization` handler and `teamMember.deleteMany` cleanup so the user is only considered fully left once both steps are safely completed.
274-310: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick winBind privileged member actions to the authorized organization.
The admin/owner check is for
context.organization, butorgIdcan come frominput.organizationId. That lets authorization and the target organization diverge.Proposed fix pattern
- const orgId = input.organizationId ?? context.organization.id; + // The role check above is scoped to the active organization, so reject caller-supplied ids from another tenant. + if (input.organizationId && input.organizationId !== context.organization.id) { + throw errors.FORBIDDEN({ + message: "Cannot manage members outside the active organization", + }); + } + + const orgId = context.organization.id;Also applies to: 489-529
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/router/organization.ts` around lines 274 - 310, The privileged member role update flow in organization router is using a caller-supplied organizationId while authorization is checked against context.organization, which can let the target org differ from the authorized one. Update the member-role handler in the organization router so the admin/owner permission check and the member lookup/update both use the same authorized organization source, preferably context.organization.id, and reject or ignore any mismatched input.organizationId. Apply the same binding rule to the related member action handler referenced by the review comment so all privileged membership operations stay scoped to the authorized organization.app/router/ai.ts (1)
29-34: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick winRequire team membership before exporting thread content to AI.
The summary route now limits lookups to the organization, but not to teams the caller belongs to. That can expose another team’s thread content to the AI provider if an organization member knows the thread ID.
Representative fix
threadId: input.threadId, team: { organizationId: context.organization.id, + // Prevent AI export for teams the caller is not a member of. + teammembers: { some: { userId: context.user.id } }, },Apply the same predicate to the parent lookup below.
Also applies to: 49-54
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/router/ai.ts` around lines 29 - 34, The parent message lookup in the ai router only scopes by organization, which still allows cross-team thread access; update the parent lookup in the thread export flow to apply the same team-membership predicate used elsewhere, so only messages from teams the caller belongs to can be resolved. Use the existing parent lookup in the ai route and the thread summary query as the reference points, and make sure the prisma.message.findFirst filter includes the caller’s team membership constraint alongside organizationId and threadId.app/router/message.ts (1)
52-70: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick winKeep message access scoped to team members.
These checks now prove the team/message belongs to the current organization, but they do not verify
context.user.idis a member of that team. With team membership modeled separately, any organization member who knows a team/message ID can read or write across teams. Add ateammembersfilter here or centralize this in a team-member middleware.Representative fix
const team = await prisma.team.findFirst({ where: { id: input.teamId, organizationId: context.organization.id, + // Enforce the team membership boundary, not only the organization boundary. + teammembers: { some: { userId: context.user.id } }, }, select: { id: true,Apply the same membership predicate to the parent/message lookups that currently filter only by
team.organization.Also applies to: 73-86, 155-166, 260-266, 320-326, 400-405
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/router/message.ts` around lines 52 - 70, The message/thread lookup logic in message.ts only verifies organization ownership and does not enforce that context.user.id belongs to the target team, so access can leak across teams. Update the parent/message validation in the affected message routes to include a team membership check (or route all such checks through a shared team-member middleware) alongside the existing team.organization filter. Apply the same membership-scoped predicate in the message lookup paths around the parentMessage/threadId checks and the other listed message access handlers so only team members can read or write those messages.
🧹 Nitpick comments (6)
app/(organization)/organizations/[organizationId]/page.tsx (1)
5-16: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winLeftover "workspace"-era naming (
w) and no dedicated single-org lookup.
.find((w) => w.id === organizationId)usesw, a naming residue from the pre-rename "workspace" domain. Since this PR's entire purpose is eliminating workspace terminology, rename to something likeorgfor clarity. Separately, fetching the entireorganizationslist just to resolve one record by id is wasteful if a dedicated single-item fetch (e.g.,client.organization.get) exists — worth confirming.As per coding guidelines,
**/*.{ts,tsx,js,jsx}requires that new code include "comments... explaining the reason for the code or the non-obvious decision it implements" — a short comment on why the list-then-find approach is used (vs. a direct getter) would help future readers.♻️ Proposed naming fix
- const { organizations } = await client.organization.list(); - const organization = organizations.find((w) => w.id === organizationId); + const { organizations } = await client.organization.list(); + // TODO: replace with a dedicated single-organization lookup if available + const organization = organizations.find((org) => org.id === organizationId);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/`(organization)/organizations/[organizationId]/page.tsx around lines 5 - 16, Rename the leftover workspace-era iterator name in generateMetadata from w to org (or similar) to match the organization domain, and verify whether a direct single-organization lookup like client.organization.get exists instead of listing all organizations and then finding one. If the list-then-find approach stays, add a brief comment in generateMetadata explaining why that non-obvious choice is used so future readers understand it.Source: Coding guidelines
app/(organization)/organizations/[organizationId]/_components/organization-header.tsx (1)
17-28: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winSequential suspense queries create a render waterfall.
useSuspenseQuery(orpc.organization.list...)anduseSuspenseQuery(orpc.team.list...)are separate hook calls in the same component. When the first suspends, React aborts the render before reaching the second hook, soteam.listdoesn't start fetching untilorganization.listresolves — a serial waterfall instead of parallel fetches, adding latency to every render of this header.Consider
useSuspenseQueriesto fire both requests in parallel.⚡ Proposed fix using useSuspenseQueries
- const { - data: { currentOrganization }, - } = useSuspenseQuery(orpc.organization.list.queryOptions()); - - const userData = useQuery(orpc.user.get.queryOptions()); - const user = userData.data; - - const { - data: { teams }, - } = useSuspenseQuery( - orpc.team.list.queryOptions({ input: { organizationId: organizationId } }) - ); + const [ + { data: { currentOrganization } }, + { data: { teams } }, + ] = useSuspenseQueries({ + queries: [ + orpc.organization.list.queryOptions(), + orpc.team.list.queryOptions({ input: { organizationId } }), + ], + }); + + const userData = useQuery(orpc.user.get.queryOptions()); + const user = userData.data;🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/`(organization)/organizations/[organizationId]/_components/organization-header.tsx around lines 17 - 28, The header component is triggering a suspense waterfall because `useSuspenseQuery` for `orpc.organization.list.queryOptions()` and `orpc.team.list.queryOptions()` run sequentially in `organization-header.tsx`. Update the `OrganizationHeader` data fetching to use `useSuspenseQueries` so both queries start in parallel, keeping the existing `currentOrganization`, `teams`, and `userData` usage intact while removing the serialized suspend behavior.app/(organization)/organizations/[organizationId]/_components/messaeg-input.tsx (1)
101-101: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winExtract the repeated
["message.list", teamId]key into a local constant.The raw key literal is duplicated 5+ times in this file. Sibling files (
edit-message-form.tsx,delete-message-dialog.tsx) already extract this into a singlemessageListKeyconst with a "must stay in sync" comment — following the same pattern here reduces the risk of a typo causing cache/key drift (as already happened with thechanelIdfield elsewhere in this PR).♻️ Proposed refactor
+ // Raw key the infinite query is stored under; must stay in sync with MessageList. + const messageListKey = ["message.list", teamId] as const; + const createMessageMutation = useMutation( orpc.message.create.mutationOptions({ onMutate: async (variables) => { await queryclient.cancelQueries({ - queryKey: ["message.list", teamId], + queryKey: messageListKey, }); const prevData = queryclient.getQueryData< InfiniteData<MessagePage, string | undefined> - >(["message.list", teamId]); + >(messageListKey);(apply similarly to the remaining occurrences at lines 101, 125, 153, 177)
Also applies to: 125-125, 147-153, 177-177
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/`(organization)/organizations/[organizationId]/_components/messaeg-input.tsx at line 101, The query key ["message.list", teamId] is repeated multiple times in messaeg-input.tsx, so extract it into a local messageListKey constant like in edit-message-form.tsx and delete-message-dialog.tsx, with the same must stay in sync comment. Update all usages in the message input flow (including the mutation/invalidation and any related hooks in this component) to reference that constant so the key stays consistent and typo-safe.components/delete-team-dialog.tsx (1)
38-59: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winLeftover "channel" naming in
onSuccessparam; conditional redirect lacks explanatory comment.The mutation result is bound to
ch— a leftover abbreviation from the old "channel" domain that this PR is explicitly renaming away from. Also, the redirect logic (if (teamId === team.id) router.push(...), navigating away only when the deleted team is the one currently being viewed) is non-obvious and has no comment explaining why.As per coding guidelines, "Add comments to any new code you add, explaining the reason for the code or the non-obvious decision it implements."
♻️ Proposed fix
orpc.team.delete.mutationOptions({ - onSuccess: (ch) => { + onSuccess: (result) => { toast.success(`Team ${team.name} Deleted successfully!`); queryClient.invalidateQueries({ queryKey: orpc.team.list.queryKey({ - input: { organizationId: ch.organizationId }, + input: { organizationId: result.organizationId }, }), }); + // Only redirect away if the user is currently viewing the team being deleted; + // deleting from a list elsewhere should keep the user on the current page. if (teamId === team.id) - router.push(`/organizations/${ch.organizationId}`); + router.push(`/organizations/${result.organizationId}`);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@components/delete-team-dialog.tsx` around lines 38 - 59, The issue is the leftover abbreviated `ch` name in `deleteTeamMutation`’s `onSuccess` handler and the non-obvious redirect decision. Rename `ch` to a team/organization-specific name in `useMutation(orpc.team.delete.mutationOptions(...))` and add a short comment near the `if (teamId === team.id) router.push(...)` branch in `onSuccess` explaining that we only navigate away when the deleted team is the one currently being viewed. Also keep the query invalidation and dialog close behavior unchanged.Source: Coding guidelines
app/(organization)/organizations/[organizationId]/_components/MessageList.tsx (1)
36-36: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick winDocument or centralize this shared cache key.
["message.list", teamId]must stay aligned with the thread sidebar’s optimistic update key. Add a shared helper or an inline comment explaining the cross-component contract to prevent cache invalidation drift. As per coding guidelines, “Add comments to any new code you add, explaining the reason for the code or the non-obvious decision it implements.”🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/`(organization)/organizations/[organizationId]/_components/MessageList.tsx at line 36, The cache key in MessageList’s queryKey is a shared contract with the thread sidebar’s optimistic update logic, so it should not be left as an undocumented literal. Either centralize the key generation in a shared helper used by both MessageList and the sidebar optimistic update code, or add a concise inline comment near the queryKey explaining that it must remain aligned across components. Keep the shared symbol consistent wherever the message list cache is read or invalidated.Source: Coding guidelines
components/app-sidebar.tsx (1)
134-146: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winFinish the channel-to-team rename in the loop variable.
teams.map((ch) => ...)leaves channel terminology in the team navigation code. Rename it toteamto keep this PR’s domain rename consistent.Suggested cleanup
- {teams && - teams.map((ch) => ( + {teams && + teams.map((team) => ( <SidebarMenuSubItem - key={ch.id} + key={team.id} className="flex items-center justify-center flex-row" > <SidebarMenuSubButton asChild - isActive={teamId === ch.id} + isActive={teamId === team.id} className="text-muted-foreground flex-1" > <Link - href={`/organizations/${organizationId}/team/${ch.id}`} - title={ch.name} + href={`/organizations/${organizationId}/team/${team.id}`} + title={team.name} >🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@components/app-sidebar.tsx` around lines 134 - 146, Rename the loop variable in the team navigation render so the channel terminology is fully removed: in the teams.map callback inside the sidebar component, change the generic alias from ch to team and update all references in that mapping block accordingly. Keep the rest of the team-link rendering logic the same, using the existing teams map and SidebarMenuSubItem/SidebarMenuSubButton structure.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@app/`(organization)/organizations/[organizationId]/_components/leave-organization-dialog.tsx:
- Around line 68-75: The dialog title in leave-organization-dialog.tsx still
shows leftover copy from a delete-message flow, so update the DialogTitle in
LeaveOrganizationDialog to match the leave action. Keep the existing
DialogContent/DialogDescription structure, but replace the misleading title text
with a clear “Leave Organization” label so the confirmation dialog aligns with
its purpose.
In
`@app/`(organization)/organizations/[organizationId]/_components/realtime-provider-wrapper.tsx:
- Around line 10-12: The early return in realtime-provider-wrapper’s route check
is intentional and should be documented so it is not mistaken for dead code. Add
a short inline comment near the useParams teamId guard explaining that the
wrapper only provides realtime context on team routes and otherwise
intentionally renders children unchanged. Keep the note close to the teamId
conditional in RealtimeProviderWrapper so the scoping decision is obvious.
In
`@app/`(organization)/organizations/[organizationId]/team/[teamId]/members/_components/remov-member-dialog.tsx:
- Around line 71-75: Tighten the confirmation copy in the RemoveMemberDialog
component so the DialogDescription reads naturally; update the user-facing text
in remove-member-dialog.tsx from “Are you sure to remove” to “Are you sure you
want to remove” while keeping the rest of the memberName and team wording
intact.
In `@app/router/members.ts`:
- Around line 12-20: The invite flow in inviteMember is still trusting
client-supplied organizationId instead of the organization established by
requireOrganizationMiddleware. Update the handler to pass
context.organization.id into auth.api.createInvitation, and if input.teamId is
still accepted, verify that it belongs to the same organization before creating
the invitation. Keep the fix localized to inviteMember and the organization/team
lookup logic so the middleware-scoped organization is always the source of
truth.
In `@app/router/organization.ts`:
- Around line 107-108: The create-organization flow is logging raw tenant
metadata via the console statement in the organization router, which can leak
customer context. Remove the direct console.log from the slug creation path and
replace it with structured, redacted logging only if needed, using the
createSlug and organization creation logic as the place to update. Also apply
the same cleanup to the other referenced log site in this router.
- Around line 199-209: The organization members routes are using the
`:organizationId` path param but the handler is still relying on
`context.organization`, which can return members for the wrong organization.
Update `listOrganizationMembers` (and the related route noted in the comment) to
explicitly validate that the requested `organizationId` matches the loaded
organization in `requireOrganizationMiddleware`, and reject mismatches before
returning any members. Use the existing `organizationId` param and
`context.organization` comparison in the route handler so stale URLs cannot
fetch the wrong organization’s members.
In `@app/router/team.ts`:
- Around line 208-215: The team read endpoints are not scoped to the active
organization, so `getTeam` and `listTeamMembers` can return data for arbitrary
`teamId` values. Update the Prisma queries in `team.get` and `listTeamMembers`
to include `context.organization.id` in the lookup/filter, and ensure membership
checks also verify the requested team belongs to that organization before
returning results. Use the existing handler/context symbols to keep the fix
localized and consistent.
- Around line 149-153: The team listing handler is still querying with the
caller-supplied organizationId instead of the authorized active organization.
Update the query in the team router handler to bind the prisma.team.findMany
filter to context.organization.id, keeping requireOrganizationMiddleware as the
source of truth and avoiding organization mismatches.
---
Outside diff comments:
In `@app/router/ai.ts`:
- Around line 29-34: The parent message lookup in the ai router only scopes by
organization, which still allows cross-team thread access; update the parent
lookup in the thread export flow to apply the same team-membership predicate
used elsewhere, so only messages from teams the caller belongs to can be
resolved. Use the existing parent lookup in the ai route and the thread summary
query as the reference points, and make sure the prisma.message.findFirst filter
includes the caller’s team membership constraint alongside organizationId and
threadId.
In `@app/router/message.ts`:
- Around line 52-70: The message/thread lookup logic in message.ts only verifies
organization ownership and does not enforce that context.user.id belongs to the
target team, so access can leak across teams. Update the parent/message
validation in the affected message routes to include a team membership check (or
route all such checks through a shared team-member middleware) alongside the
existing team.organization filter. Apply the same membership-scoped predicate in
the message lookup paths around the parentMessage/threadId checks and the other
listed message access handlers so only team members can read or write those
messages.
In `@app/router/organization.ts`:
- Around line 555-572: The leave flow in organization cleanup is not reliable
because `auth.api.leaveOrganization` can succeed while the follow-up
`prisma.teamMember.deleteMany` in the same handler fails, leaving stale
memberships behind. Move the cleanup out of the `leaveOrganization` request path
into a durable ownership point such as a hook, outbox, or retryable job, and
reference the `leaveOrganization` handler and `teamMember.deleteMany` cleanup so
the user is only considered fully left once both steps are safely completed.
- Around line 274-310: The privileged member role update flow in organization
router is using a caller-supplied organizationId while authorization is checked
against context.organization, which can let the target org differ from the
authorized one. Update the member-role handler in the organization router so the
admin/owner permission check and the member lookup/update both use the same
authorized organization source, preferably context.organization.id, and reject
or ignore any mismatched input.organizationId. Apply the same binding rule to
the related member action handler referenced by the review comment so all
privileged membership operations stay scoped to the authorized organization.
---
Nitpick comments:
In
`@app/`(organization)/organizations/[organizationId]/_components/messaeg-input.tsx:
- Line 101: The query key ["message.list", teamId] is repeated multiple times in
messaeg-input.tsx, so extract it into a local messageListKey constant like in
edit-message-form.tsx and delete-message-dialog.tsx, with the same must stay in
sync comment. Update all usages in the message input flow (including the
mutation/invalidation and any related hooks in this component) to reference that
constant so the key stays consistent and typo-safe.
In
`@app/`(organization)/organizations/[organizationId]/_components/MessageList.tsx:
- Line 36: The cache key in MessageList’s queryKey is a shared contract with the
thread sidebar’s optimistic update logic, so it should not be left as an
undocumented literal. Either centralize the key generation in a shared helper
used by both MessageList and the sidebar optimistic update code, or add a
concise inline comment near the queryKey explaining that it must remain aligned
across components. Keep the shared symbol consistent wherever the message list
cache is read or invalidated.
In
`@app/`(organization)/organizations/[organizationId]/_components/organization-header.tsx:
- Around line 17-28: The header component is triggering a suspense waterfall
because `useSuspenseQuery` for `orpc.organization.list.queryOptions()` and
`orpc.team.list.queryOptions()` run sequentially in `organization-header.tsx`.
Update the `OrganizationHeader` data fetching to use `useSuspenseQueries` so
both queries start in parallel, keeping the existing `currentOrganization`,
`teams`, and `userData` usage intact while removing the serialized suspend
behavior.
In `@app/`(organization)/organizations/[organizationId]/page.tsx:
- Around line 5-16: Rename the leftover workspace-era iterator name in
generateMetadata from w to org (or similar) to match the organization domain,
and verify whether a direct single-organization lookup like
client.organization.get exists instead of listing all organizations and then
finding one. If the list-then-find approach stays, add a brief comment in
generateMetadata explaining why that non-obvious choice is used so future
readers understand it.
In `@components/app-sidebar.tsx`:
- Around line 134-146: Rename the loop variable in the team navigation render so
the channel terminology is fully removed: in the teams.map callback inside the
sidebar component, change the generic alias from ch to team and update all
references in that mapping block accordingly. Keep the rest of the team-link
rendering logic the same, using the existing teams map and
SidebarMenuSubItem/SidebarMenuSubButton structure.
In `@components/delete-team-dialog.tsx`:
- Around line 38-59: The issue is the leftover abbreviated `ch` name in
`deleteTeamMutation`’s `onSuccess` handler and the non-obvious redirect
decision. Rename `ch` to a team/organization-specific name in
`useMutation(orpc.team.delete.mutationOptions(...))` and add a short comment
near the `if (teamId === team.id) router.push(...)` branch in `onSuccess`
explaining that we only navigate away when the deleted team is the one currently
being viewed. Also keep the query invalidation and dialog close behavior
unchanged.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: fdf8b5d9-e5d6-48de-a72d-0b4700163902
⛔ Files ignored due to path filters (1)
public/team-comms.pngis excluded by!**/*.png
📒 Files selected for processing (77)
README.mdapp/(auth)/login/page.tsxapp/(organization)/layout.tsxapp/(organization)/organizations/[organizationId]/_components/MessageList.tsxapp/(organization)/organizations/[organizationId]/_components/attachment-chip.tsxapp/(organization)/organizations/[organizationId]/_components/delete-message-dialog.tsxapp/(organization)/organizations/[organizationId]/_components/edit-message-form.tsxapp/(organization)/organizations/[organizationId]/_components/image-dialog.tsxapp/(organization)/organizations/[organizationId]/_components/leave-organization-dialog.tsxapp/(organization)/organizations/[organizationId]/_components/messaeg-input.tsxapp/(organization)/organizations/[organizationId]/_components/message-item.tsxapp/(organization)/organizations/[organizationId]/_components/message-omposer.tsxapp/(organization)/organizations/[organizationId]/_components/organization-header.tsxapp/(organization)/organizations/[organizationId]/_components/realtime-provider-wrapper.tsxapp/(organization)/organizations/[organizationId]/_components/team-card.tsxapp/(organization)/organizations/[organizationId]/_components/team-list.tsxapp/(organization)/organizations/[organizationId]/invitations/_components/cancel-invitation-dialog.tsxapp/(organization)/organizations/[organizationId]/invitations/_components/invitation-table-actions-dropdown.tsxapp/(organization)/organizations/[organizationId]/invitations/_components/invitation-table.tsxapp/(organization)/organizations/[organizationId]/invitations/layout.tsxapp/(organization)/organizations/[organizationId]/invitations/page.tsxapp/(organization)/organizations/[organizationId]/layout.tsxapp/(organization)/organizations/[organizationId]/page.tsxapp/(organization)/organizations/[organizationId]/team/[teamId]/loading.tsxapp/(organization)/organizations/[organizationId]/team/[teamId]/members/_components/remov-member-dialog.tsxapp/(organization)/organizations/[organizationId]/team/[teamId]/members/layout.tsxapp/(organization)/organizations/[organizationId]/team/[teamId]/members/page.tsxapp/(organization)/organizations/[organizationId]/team/[teamId]/page.tsxapp/(organization)/organizations/_components/create-organization-dialog.tsxapp/(organization)/organizations/_components/header.tsxapp/(organization)/organizations/_components/invite-organization-dialog.tsxapp/(organization)/organizations/_components/organization-list.tsxapp/(organization)/organizations/_components/update-organization-dialog.tsxapp/(organization)/organizations/layout.tsxapp/(organization)/organizations/page.tsxapp/(organization)/organizations/schema.tsapp/(organization)/page.tsxapp/(workspace)/workspaces/[workspaceId]/_components/realtime-provider-wrapper.tsxapp/(workspace)/workspaces/[workspaceId]/page.tsxapp/accept-invite/[id]/page.tsxapp/middlewares/member.tsapp/middlewares/organization.tsapp/profile/_components/update-profile-form.tsxapp/profile/layout.tsxapp/router/ai.tsapp/router/index.tsapp/router/members.tsapp/router/message.tsapp/router/organization.tsapp/router/team.tsapp/router/user.tscomponents/add-member-to-team.tsxcomponents/app-sidebar.tsxcomponents/create-tem-dialog.tsxcomponents/delete-organization-dialog.tsxcomponents/delete-team-dialog.tsxcomponents/general/footer.tsxcomponents/general/hero.tsxcomponents/general/navbar.tsxcomponents/general/user-avatar-dropdown.tsxcomponents/organization-switcher.tsxcomponents/remove-member-dialog.tsxcomponents/team-realtime-provider.tsxcomponents/thread-sidebar/right-sidebar.tsxcomponents/thread-sidebar/thread-context.tsxcomponents/thread-sidebar/threads-form.tsxcomponents/update-member-role-dialog.tsxcomponents/update-team-dialog.tsxlib/app/site.tslib/auth-client.tslib/auth.tslib/schema.tslib/utils.tspackage.jsonproxy.tsrealtime/index.tsrealtime/schema.ts
💤 Files with no reviewable changes (2)
- app/(workspace)/workspaces/[workspaceId]/page.tsx
- app/(workspace)/workspaces/[workspaceId]/_components/realtime-provider-wrapper.tsx
| <DialogContent className="sm:max-w-sm"> | ||
| <DialogHeader> | ||
| <DialogTitle>Delete Message</DialogTitle> | ||
| <DialogDescription> | ||
| Are you sure you want to leave this workspace? This action cannot be | ||
| undone. | ||
| Are you sure you want to leave this organization? This action cannot | ||
| be undone. | ||
| </DialogDescription> | ||
| </DialogHeader> |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Wrong dialog title: "Delete Message" instead of "Leave Organization".
This appears to be leftover copy from a different dialog (delete-message). Users confirming to leave an organization will see a misleading title.
🐛 Proposed fix
- <DialogTitle>Delete Message</DialogTitle>
+ <DialogTitle>Leave Organization</DialogTitle>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <DialogContent className="sm:max-w-sm"> | |
| <DialogHeader> | |
| <DialogTitle>Delete Message</DialogTitle> | |
| <DialogDescription> | |
| Are you sure you want to leave this workspace? This action cannot be | |
| undone. | |
| Are you sure you want to leave this organization? This action cannot | |
| be undone. | |
| </DialogDescription> | |
| </DialogHeader> | |
| <DialogContent className="sm:max-w-sm"> | |
| <DialogHeader> | |
| <DialogTitle>Leave Organization</DialogTitle> | |
| <DialogDescription> | |
| Are you sure you want to leave this organization? This action cannot | |
| be undone. | |
| </DialogDescription> | |
| </DialogHeader> |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@app/`(organization)/organizations/[organizationId]/_components/leave-organization-dialog.tsx
around lines 68 - 75, The dialog title in leave-organization-dialog.tsx still
shows leftover copy from a delete-message flow, so update the DialogTitle in
LeaveOrganizationDialog to match the leave action. Keep the existing
DialogContent/DialogDescription structure, but replace the misleading title text
with a clear “Leave Organization” label so the confirmation dialog aligns with
its purpose.
| const { teamId } = useParams<{ teamId?: string }>(); | ||
|
|
||
| if (!teamId) return <>{children}</>; |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Document why the wrapper no-ops outside team routes.
The conditional provider is intentional route scoping; add a short comment so it is not mistaken for dead code. As per coding guidelines, **/*.{ts,tsx,js,jsx}: Add comments to any new code you add, explaining the reason for the code or the non-obvious decision it implements.
Proposed comment
const { teamId } = useParams<{ teamId?: string }>();
+ // Organization-level routes do not include a teamId, so avoid joining a realtime team room there.
if (!teamId) return <>{children}</>;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const { teamId } = useParams<{ teamId?: string }>(); | |
| if (!teamId) return <>{children}</>; | |
| const { teamId } = useParams<{ teamId?: string }>(); | |
| // Organization-level routes do not include a teamId, so avoid joining a realtime team room there. | |
| if (!teamId) return <>{children}</>; |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@app/`(organization)/organizations/[organizationId]/_components/realtime-provider-wrapper.tsx
around lines 10 - 12, The early return in realtime-provider-wrapper’s route
check is intentional and should be documented so it is not mistaken for dead
code. Add a short inline comment near the useParams teamId guard explaining that
the wrapper only provides realtime context on team routes and otherwise
intentionally renders children unchanged. Keep the note close to the teamId
conditional in RealtimeProviderWrapper so the scoping decision is obvious.
Source: Coding guidelines
| <DialogTitle>Remove Member from this team</DialogTitle> | ||
| <DialogDescription> | ||
| Are you sure to remove{" "} | ||
| <span className="font-bold text-destructive">{memberName}</span> from | ||
| this channel? | ||
| this team? |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Tighten the confirmation dialog copy.
“Are you sure to remove” reads awkwardly; use “Are you sure you want to remove” for clearer user-facing text.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@app/`(organization)/organizations/[organizationId]/team/[teamId]/members/_components/remov-member-dialog.tsx
around lines 71 - 75, Tighten the confirmation copy in the RemoveMemberDialog
component so the DialogDescription reads naturally; update the user-facing text
in remove-member-dialog.tsx from “Are you sure to remove” to “Are you sure you
want to remove” while keeping the rest of the memberName and team wording
intact.
| export const inviteMember = base | ||
| .use(requireAuthMiddleware) | ||
| .use(requireworkspaceMiddleware) | ||
| .use(requireOrganizationMiddleware) | ||
| .use(standardsecurityMiddleware) | ||
| .use(heavyWritesecurityMiddleware) | ||
| .route({ | ||
| method: "POST", | ||
| path: "/workspace/members/invite", | ||
| summary: "Invite members to the workspace", | ||
| path: "/organization/members/invite", | ||
| summary: "Invite members to the organization", |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Bind invites to the middleware-scoped organization.
Line 14 authenticates an organization in context, but the handler still sends the client-provided input.organizationId to auth.api.createInvitation. Use context.organization.id instead, and validate input.teamId belongs to that same organization if it remains accepted.
Proposed fix
- .handler(async ({ input, errors }) => {
+ .handler(async ({ context, input, errors }) => {
console.log("[inviteMember]:", { input });
try {
await auth.api.createInvitation({
body: {
email: input.email,
role: input.role,
- organizationId: input.organizationId,
+ // Trust the organization resolved by middleware instead of a client-controlled ID.
+ organizationId: context.organization.id,
resend: input.resend,
teamId: input.teamId ?? undefined,
},🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app/router/members.ts` around lines 12 - 20, The invite flow in inviteMember
is still trusting client-supplied organizationId instead of the organization
established by requireOrganizationMiddleware. Update the handler to pass
context.organization.id into auth.api.createInvitation, and if input.teamId is
still accepted, verify that it belongs to the same organization before creating
the invitation. Keep the fix localized to inviteMember and the organization/team
lookup logic so the middleware-scoped organization is always the source of
truth.
| const slug = createSlug(input.name); | ||
| console.log("Creating workspace with slug:", slug); | ||
| console.log("Creating organization with slug:", slug); |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Remove raw organization identifiers from console logs.
Organization slugs/ids are tenant metadata and can leak customer context into production logs. Prefer structured, redacted logging if this needs observability.
Proposed cleanup
- console.log("Creating organization with slug:", slug);
@@
- console.log("Left organization", organizationId);Also applies to: 574-574
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app/router/organization.ts` around lines 107 - 108, The create-organization
flow is logging raw tenant metadata via the console statement in the
organization router, which can leak customer context. Remove the direct
console.log from the slug creation path and replace it with structured, redacted
logging only if needed, using the createSlug and organization creation logic as
the place to update. Also apply the same cleanup to the other referenced log
site in this router.
| export const listOrganizationMembers = base | ||
| .use(requireAuthMiddleware) | ||
| .use(requireworkspaceMiddleware) | ||
| .use(requireOrganizationMiddleware) | ||
| .use(standardsecurityMiddleware) | ||
| .route({ | ||
| method: "GET", | ||
| path: "/workspace/:workspaceId/members", | ||
| summary: "List all members of a workspace", | ||
| tags: ["Workspace"], | ||
| path: "/organization/:organizationId/members", | ||
| summary: "List all members of a organization", | ||
| tags: ["Organization"], | ||
| }) | ||
| .input(z.void()) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Validate the route organization before returning members.
The route declares :organizationId, but the handler ignores it and returns context.organization members. A stale organization URL can render the wrong member list instead of being rejected.
Proposed fix
- .input(z.void())
+ .input(
+ z.object({
+ organizationId: z.string(),
+ })
+ )
@@
- .handler(async ({ context, errors }) => {
+ .handler(async ({ context, input, errors }) => {
+ // Reject stale route params so this endpoint cannot return members for the wrong organization page.
+ if (input.organizationId !== context.organization.id) {
+ throw errors.BAD_REQUEST({
+ message: "You recently switched organization",
+ });
+ }
+
let organizationmembers;Also applies to: 223-229
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app/router/organization.ts` around lines 199 - 209, The organization members
routes are using the `:organizationId` path param but the handler is still
relying on `context.organization`, which can return members for the wrong
organization. Update `listOrganizationMembers` (and the related route noted in
the comment) to explicitly validate that the requested `organizationId` matches
the loaded organization in `requireOrganizationMiddleware`, and reject
mismatches before returning any members. Use the existing `organizationId` param
and `context.organization` comparison in the route handler so stale URLs cannot
fetch the wrong organization’s members.
| .handler(async ({ context, input }) => { | ||
| const channels = await prisma.team.findMany({ | ||
| const teams = await prisma.team.findMany({ | ||
| where: { | ||
| organizationId: input.organizationId, | ||
| teammembers: { |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Reject organization mismatches in team listing.
requireOrganizationMiddleware authorizes the active organization, but the query uses caller-provided input.organizationId. Keep the query bound to context.organization.id.
Proposed fix
- .handler(async ({ context, input }) => {
+ .handler(async ({ context, input, errors }) => {
+ // Keep route state aligned with the authenticated organization context.
+ if (input.organizationId !== context.organization.id) {
+ throw errors.BAD_REQUEST({
+ message: "You recently switched organization",
+ });
+ }
+
const teams = await prisma.team.findMany({
where: {
- organizationId: input.organizationId,
+ organizationId: context.organization.id,📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| .handler(async ({ context, input }) => { | |
| const channels = await prisma.team.findMany({ | |
| const teams = await prisma.team.findMany({ | |
| where: { | |
| organizationId: input.organizationId, | |
| teammembers: { | |
| .handler(async ({ context, input, errors }) => { | |
| // Keep route state aligned with the authenticated organization context. | |
| if (input.organizationId !== context.organization.id) { | |
| throw errors.BAD_REQUEST({ | |
| message: "You recently switched organization", | |
| }); | |
| } | |
| const teams = await prisma.team.findMany({ | |
| where: { | |
| organizationId: context.organization.id, | |
| teammembers: { |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app/router/team.ts` around lines 149 - 153, The team listing handler is still
querying with the caller-supplied organizationId instead of the authorized
active organization. Update the query in the team router handler to bind the
prisma.team.findMany filter to context.organization.id, keeping
requireOrganizationMiddleware as the source of truth and avoiding organization
mismatches.
| .handler(async ({ input, context, errors }) => { | ||
| console.log("[channel.get]: called for", input.channelId); | ||
| const channel = await prisma.team.findUnique({ | ||
| where: { id: input.channelId }, | ||
| console.log("[team.get]: called for", input.teamId); | ||
| const team = await prisma.team.findUnique({ | ||
| where: { id: input.teamId }, | ||
| select: { | ||
| name: true, | ||
| }, | ||
| }); |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Scope direct team reads to the active organization and membership.
getTeam and listTeamMembers accept any teamId; the Prisma reads are not constrained by context.organization.id, so arbitrary team IDs can expose team/member data.
Proposed fix pattern
- const team = await prisma.team.findUnique({
- where: { id: input.teamId },
+ const team = await prisma.team.findFirst({
+ where: {
+ id: input.teamId,
+ // Scope by active organization and membership so arbitrary team IDs cannot be read.
+ organizationId: context.organization.id,
+ teammembers: { some: { userId: context.user.id } },
+ },
select: {
name: true,
},
@@
- where: { teamId: input.teamId },
+ where: {
+ teamId: input.teamId,
+ team: {
+ // Ensure team member lists cannot be read outside the active organization.
+ organizationId: context.organization.id,
+ teammembers: { some: { userId: context.user.id } },
+ },
+ },Also applies to: 382-392
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app/router/team.ts` around lines 208 - 215, The team read endpoints are not
scoped to the active organization, so `getTeam` and `listTeamMembers` can return
data for arbitrary `teamId` values. Update the Prisma queries in `team.get` and
`listTeamMembers` to include `context.organization.id` in the lookup/filter, and
ensure membership checks also verify the requested team belongs to that
organization before returning results. Use the existing handler/context symbols
to keep the fix localized and consistent.
Summary by CodeRabbit
New Features
Bug Fixes