Skip to content

Team#952

Open
goldflag wants to merge 2 commits intomasterfrom
team
Open

Team#952
goldflag wants to merge 2 commits intomasterfrom
team

Conversation

@goldflag
Copy link
Collaborator

@goldflag goldflag commented Mar 23, 2026

Summary by CodeRabbit

New Features

  • Added team management system with ability to create, edit, and delete teams.
  • Introduced team-based site access control, allowing organizations to assign teams to specific sites.
  • Added team filtering on the dashboard to view sites by team assignment.
  • Added "Teams" section in organization settings for team administration.
  • Extended member invitations to support team assignments.

Localization

  • Updated translations across all supported languages with new team-related UI strings and message placeholders.

- Added new API endpoints for managing teams, including fetching, creating, updating, and deleting teams.
- Updated the site retrieval logic to incorporate team-based access control, allowing users to filter sites based on team membership.
- Enhanced the frontend to support team selection in site filtering, improving user experience in managing site access.
- Introduced new database schema for teams and their relationships with members and sites, ensuring robust data handling.
- Added functionality to allow team assignments when inviting new members to an organization.
- Updated the invitation API to handle team IDs, ensuring that team memberships are validated and stored correctly.
- Enhanced the frontend to support team selection during the invitation process, improving user experience.
- Modified database schema to include team IDs in the invitation records, facilitating better access control and organization management.
@vercel
Copy link

vercel bot commented Mar 23, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
rybbit Ready Ready Preview, Comment Mar 23, 2026 5:43am

Request Review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 23, 2026

📝 Walkthrough

Walkthrough

Introduces team management functionality across the application, including new database tables for teams and team-site access, REST API endpoints for CRUD operations, client-side hooks and components for managing teams, team-based site access restrictions for members, and team selection during member invitations.

Changes

Cohort / File(s) Summary
Localization Updates
client/messages/cs.json, client/messages/de.json, client/messages/en.json, client/messages/es.json, client/messages/fr.json, client/messages/it.json, client/messages/ja.json, client/messages/ko.json, client/messages/pl.json, client/messages/pt.json, client/messages/zh.json
Reordered existing message keys and added approximately 35 new empty-string placeholder keys across all supported locales (Czech, German, English, Spanish, French, Italian, Japanese, Korean, Polish, Portuguese, Chinese). English includes full team-feature translations; other locales are placeholders.
Database Schema
server/drizzle/0002_short_bishop.sql, server/drizzle/meta/0002_snapshot.json, server/drizzle/meta/_journal.json
Added three new database tables (team, teamMember, teamSiteAccess) with foreign key constraints and cascading deletes. Extended invitation table with team_ids JSONB column. Includes comprehensive schema snapshot metadata.
Database Schema Definition
server/src/db/postgres/schema.ts
Added Drizzle ORM schema definitions for new team tables (team, teamMember, teamSiteAccess) with relationships, uniqueness constraints, and indexes. Added teamIds field to invitation table schema.
Server-Side Team API Endpoints
server/src/api/teams/listTeams.ts, server/src/api/teams/createTeam.ts, server/src/api/teams/updateTeam.ts, server/src/api/teams/deleteTeam.ts, server/src/api/teams/index.ts
Implemented four REST handlers for team CRUD operations with validation (user/site verification), transaction-based writes, cache invalidation on member changes, and permission checks (admin/owner roles for list filtering).
Server-Side Access Control Logic
server/src/api/sites/getSitesFromOrg.ts, server/src/lib/auth-utils.ts, server/src/lib/auth.ts
Extended site-access filtering to include team-based restrictions for member-role users. Updated invitation acceptance to copy team assignments. Modified getSitesUserHasAccessTo to exclude team-gated sites unless user's teams grant access.
Server-Side Member/Invitation Updates
server/src/api/memberAccess/updateInvitationSiteAccess.ts
Extended endpoint to accept optional teamIds array, validate team membership, and persist team assignments on invitation records.
Server Route Registration
server/src/index.ts
Registered new team API routes under /api/organizations/:organizationId/teams with appropriate auth middleware.
Client-Side API Types and Hooks
client/src/api/admin/endpoints/teams.ts, client/src/api/admin/endpoints/index.ts, client/src/api/admin/hooks/useTeams.ts
Created new TypeScript types for Team, TeamMember, TeamSite and request/response shapes. Implemented four async endpoint functions. Exported React Query hooks for team CRUD with automatic cache invalidation on mutations.
Client-Side Sites Endpoint
client/src/api/admin/endpoints/sites.ts
Added optional teams array field to site objects in GetSitesFromOrgResponse.
Client Teams Management Page
client/src/app/settings/organization/teams/page.tsx, client/src/app/settings/organization/teams/components/CreateEditTeamDialog.tsx, client/src/app/settings/organization/teams/components/DeleteTeamDialog.tsx
Implemented full team management UI: teams listing page with member/site counts and edit/delete actions, create/edit dialog with member/site selection, and delete confirmation dialog with cache invalidation.
Client Navigation and Layout Updates
client/src/app/settings/organization/layout.tsx, client/src/app/settings/SettingsSidebar.tsx
Added "Teams" tab/navigation entry to settings organization layout and sidebar with conditional routing and icon display.
Client Main Application Logic
client/src/app/page.tsx, client/src/app/settings/organization/members/components/InviteMemberDialog.tsx
Integrated team filtering into site list (fetch teams, add filter dropdown, apply team-based site matching). Extended member invitation to include optional team selection with form updates during submission.

Sequence Diagram(s)

sequenceDiagram
    participant User as User (Member)
    participant Client as Client App
    participant Server as Server
    participant DB as Database
    
    User->>Client: Request sites list
    Client->>Server: GET /organizations/{orgId}/sites
    Server->>DB: Query user's member roles for orgs
    DB-->>Server: Member role records
    Server->>DB: Query all team-gated site IDs in user's orgs
    DB-->>Server: Team-gated site IDs
    Server->>DB: Query teams user belongs to (teamMember)
    DB-->>Server: User's team IDs
    Server->>DB: Query sites team access grants (teamSiteAccess)
    DB-->>Server: Team-accessible site IDs
    Server->>Server: Filter sites: exclude team-gated unless user's teams grant access
    Server->>DB: Fetch team metadata for returned sites
    DB-->>Server: Team associations (id, name)
    Server-->>Client: Sites with teams field (filtered by access)
    Client->>Client: Render sites with team filter dropdown
    User->>Client: Select team filter
    Client->>Client: Update filteredSites (match selected team or unassigned)
    Client-->>User: Display filtered sites
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 Teams are sprouting, row by row,
Database gardens start to grow,
Access filters, access gates,
Members hop between new states—
Collaboration's finest hour!

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 3.85% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Team' is vague and does not clearly convey the scope or primary objective of the changeset. While the PR adds comprehensive team management features across client and server, the single-word title lacks specificity about what aspects of teams are being added or modified. Consider using a more descriptive title such as 'Add team management features' or 'Implement teams support for organizations' to better communicate the changeset's purpose to reviewers.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch team

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
Contributor

@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: 16

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
client/src/app/page.tsx (1)

63-81: ⚠️ Potential issue | 🟠 Major

Clear invalid team filters after switching organizations.

selectedTeamFilter persists across org switches. If it still points at a team from the previous org, filteredSites becomes empty; and when the new org has no teams the select is hidden, so there's no visible way to recover from the stale filter.

Also applies to: 83-94, 151-166

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/src/app/page.tsx` around lines 63 - 81, selectedTeamFilter can remain
pointing at a team from the previous organization and produce empty
filteredSites after org switch; add an effect that resets selectedTeamFilter to
"all" when the organization or its teams change (watch activeOrganization?.id
and/or teams from useTeams) so stale team IDs are cleared; use
setSelectedTeamFilter("all") in that effect and ensure the UI still treats "all"
as the default when no teams exist.
server/src/lib/auth.ts (1)

220-285: ⚠️ Potential issue | 🔴 Critical

Don't swallow access-copy failures after acceptance.

This runs after the invitation has already been accepted. If either the site-access copy or the new team-membership insert fails (for example because a selected team was deleted or an insert hits a constraint), the catch only logs it and the user can remain in the organization without the intended restrictions/team assignments. This needs to be part of the same acceptance transaction, or followed by a compensating rollback of the newly created membership.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/src/lib/auth.ts` around lines 220 - 285, The invitation-to-member
access-copy currently runs in its own try/catch and only logs errors, which can
leave a newly created membership without intended site restrictions or team
assignments; move the access-copy logic into the same database transaction that
creates the membership (use the same db.transaction scope and tx used to persist
the member) so failures abort the whole acceptance, or if you must keep it
separate, perform a compensating rollback by deleting the newly created member
and any partial inserts on error; update the code paths around
invitationRecord/userRecord and replace the separate db.transaction block (and
its tx usage that inserts into memberSiteAccess and teamMember and updates
member) so they execute inside the original acceptance transaction that creates
member (referencing invitation, invitationRecord, userRecord, memberRecord,
memberSiteAccess, teamMember, and tx) to ensure atomicity.
🧹 Nitpick comments (4)
server/src/db/postgres/schema.ts (1)

216-221: Inconsistent table naming convention.

The teamMember table uses camelCase ("teamMember") while other tables in this file use snake_case (e.g., "team_site_access", "member_site_access"). This inconsistency could cause confusion when writing raw SQL queries or debugging.

🔧 Suggested fix for consistency
-export const teamMember = pgTable("teamMember", {
+export const teamMember = pgTable("team_member", {
   id: text("id").primaryKey(),
   teamId: text("team_id").notNull().references(() => team.id, { onDelete: "cascade" }),
   userId: text("user_id").notNull().references(() => user.id, { onDelete: "cascade" }),
   createdAt: timestamp("created_at", { mode: "string" }),
 });

Note: If you proceed with this change, ensure the migration SQL is updated accordingly.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/src/db/postgres/schema.ts` around lines 216 - 221, The table name
string for the teamMember pgTable is inconsistent (uses "teamMember"); change
the pgTable invocation so the database table name is snake_case (e.g.,
"team_member") to match the rest of the schema, update any raw SQL/migration SQL
that refers to the old name, and verify references to the exported constant
teamMember (and any foreign-key usages) still work after renaming; locate the
instantiation via the teamMember variable and the pgTable("teamMember", ...)
call to make the change.
server/src/api/sites/getSitesFromOrg.ts (1)

53-91: Duplicate team-based filtering logic with auth-utils.ts.

This team filtering duplicates logic already present in getSitesUserHasAccessTo (auth-utils.ts:146-207), which runs before this handler is called. Both perform the same filtering: finding team-gated sites, checking user team memberships, and removing inaccessible sites.

While the duplication produces correct results, it introduces:

  • Extra database queries on every request
  • Maintenance burden if filtering rules need to change (must update both locations)

Consider consolidating the team filtering in one location—either fully in auth-utils.ts (for consistency with other access checks) or fully here (if org-specific context is needed).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/src/api/sites/getSitesFromOrg.ts` around lines 53 - 91, This handler
duplicates team-based filtering already implemented in getSitesUserHasAccessTo
(auth-utils.ts); remove the entire memberRecord role==="member" block that
queries teamSiteAccess/team and teamMember and instead rely on (or call)
getSitesUserHasAccessTo to filter sites for the current user/organization, or
extract the shared logic into a single helper used by both; ensure references to
memberRecord, teamSiteAccess, teamMember, team, and sitesData are cleaned up so
no redundant DB queries remain and the handler uses the centralized access
check.
server/src/api/teams/listTeams.ts (1)

20-40: Consider returning 403 for unauthorized org access instead of empty list.

If a user is not a member of the organization, the handler returns an empty teams list rather than a proper authorization error. While this doesn't leak data, it provides inconsistent UX—users might think there are simply no teams rather than understanding they lack access.

💡 Suggested improvement
    const memberRecord = userId
      ? await db
          .select({ id: member.id, role: member.role })
          .from(member)
          .where(
            and(
              eq(member.organizationId, organizationId),
              eq(member.userId, userId)
            )
          )
          .limit(1)
      : [];

+   if (memberRecord.length === 0) {
+     return reply.status(403).send({ error: "Not authorized to access this organization" });
+   }
+
    const isAdminOrOwner =
      memberRecord.length > 0 &&
      (memberRecord[0].role === "admin" || memberRecord[0].role === "owner");
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/src/api/teams/listTeams.ts` around lines 20 - 40, The handler
currently treats a missing membership (memberRecord) as an empty teams response;
update the logic in listTeams.ts to return a 403 Unauthorized when the
requesting user is not a member of the organization. Specifically, after
computing memberRecord and isAdminOrOwner (using request.params.organizationId,
request.user?.id / getUserIdFromRequest, and the db query against member), if
memberRecord.length === 0 (i.e., user has no membership) respond with a 403
error (or reply.code(403).send) instead of continuing to fetch/return an empty
teams list; preserve existing behavior for admins/owners (isAdminOrOwner ===
true) and for any other allowed flows.
server/src/api/teams/updateTeam.ts (1)

91-96: Consider validating non-empty name on server side.

If name is provided but trims to an empty string, this will update the team with an empty name. The client validates this, but server-side validation provides defense in depth.

🛡️ Proposed fix
     const updates: Record<string, string> = { updatedAt: now };
     if (name !== undefined) {
+      if (!name.trim()) {
+        return reply.status(400).send({ error: "Team name cannot be empty" });
+      }
       updates.name = name.trim();
     }

Note: This validation should occur before the transaction starts, around lines 88-89.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/src/api/teams/updateTeam.ts` around lines 91 - 96, The code allows a
provided name to be trimmed to an empty string and saved; before starting the
transaction (i.e. before building updates/await tx.update(team).set(...)),
validate the incoming name: if name !== undefined then compute const trimmed =
name.trim(); if trimmed === "" return/throw a 400-style error (or BadRequest)
and do not proceed; otherwise assign updates.name = trimmed. Apply this check
near where now and updates are prepared so teamId, updates, and
tx.update(team).set(...) never run with an empty name.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@client/messages/cs.json`:
- Around line 667-669: Several Czech localization keys are empty (e.g.,
"WyooVh", "DoskyC", "t6nrnE" and other keys referenced around the same change
set), causing missing UI text for Czech users; update the JSON entries by
providing appropriate Czech translations for those keys (team filter labels,
team management dialogs, member/site selection strings) or, if you cannot
translate now, add clear TODO comments and fallback English values for each key
so the UI remains readable; ensure you modify the same keys (WyooVh, DoskyC,
t6nrnE, plus the other empty keys noted near lines ~769, ~811-812, ~844-869) in
the locale JSON to include completed translations or temporary fallbacks.

In `@client/messages/es.json`:
- Around line 667-669: The JSON file contains new translation keys ("WyooVh",
"DoskyC", "t6nrnE" and other empty keys in the ranges noted) with empty string
values causing Spanish users to see blanks; update client/messages/es.json by
providing Spanish translations for each empty key (replace the empty values with
the correct Spanish strings for the corresponding team management UI labels),
ensure keys match exactly and keep JSON valid, and if you cannot supply final
translations now, add a clear placeholder comment or track these keys in a
follow-up task so they're not released untranslated.

In `@client/messages/fr.json`:
- Around line 667-669: Several newly added French translation keys (e.g.,
"WyooVh", "DoskyC", "t6nrnE" and other empty keys around the same hunk) are
blank; please provide proper French strings for each missing Teams/invitation
key so the locale isn't shipped partially untranslated—open the same JSON file
and replace the empty values for the listed keys (and the other empty entries
noted near lines 769, 811, and 844–869) with the correct French translations
matching the English source keys.

In `@client/messages/ja.json`:
- Around line 667-669: The ja.json contains empty translation values for new
Teams keys (e.g., "WyooVh", "DoskyC", "t6nrnE") which will produce blank UI; fix
by populating these keys with either proper Japanese translations or English
placeholder strings, and implement a fallback merge in the i18n loader: update
getRequestConfig to merge locale messages with the English messages (default
locale) so missing keys fall back to English (use next-intl recommended merge
pattern). Ensure all other affected locale files (de.json, es.json, fr.json,
it.json, ko.json, pl.json, pt.json, zh.json, cs.json) are either filled or rely
on the new fallback behavior.

In `@client/messages/ko.json`:
- Around line 667-669: Several new localization keys (e.g., "WyooVh", "DoskyC",
"t6nrnE" and other empty entries around the same change set) were committed with
empty strings; fill each empty value with the proper Korean translation for the
label/dialog/filter they represent, following the project's translation
guidelines for files in messages/; update the keys listed in this diff and the
other empty keys noted (other empty entries near the same commit ranges) with
accurate Korean copy, run the repository's i18n/translation validation or
linting to ensure no empty values remain, and submit the updated ko.json entries
in the same format as other locales.

In `@client/messages/pl.json`:
- Around line 667-669: Backfill the missing Polish translations for the new
Teams/invitation keys by replacing the empty values for the keys "WyooVh",
"DoskyC", and "t6nrnE" (and any other empty keys in the same locale block noted
around the ranges mentioned) with appropriate Polish strings; ensure the
translations match the intent of the English originals and preserve any
interpolation/placeholders, then run a quick locale validation to ensure no
empty values remain in the Teams/invitation section.

In `@client/messages/pt.json`:
- Around line 667-669: Backfill the missing Portuguese translations for the new
Teams strings by supplying Portuguese text for the empty keys (e.g., "WyooVh",
"DoskyC", "t6nrnE") and replace the English site-count label "{count} sites"
with a proper Portuguese pluralized string (e.g., "{count} sites" -> "{count}
sites" or locale-appropriate variant such as "{count} sites" translated to
Portuguese) so the Teams/invitation flow is fully localized; update all other
newly added empty keys in the same file ranges referenced in the comment (the
additional empty entries around the noted ranges) to ensure no untranslated
placeholders remain.

In `@client/messages/zh.json`:
- Around line 667-669: The new locale keys "WyooVh", "DoskyC", and "t6nrnE" in
client/messages/zh.json are empty, causing blank UI text; open the corresponding
English keys in client/messages/en.json (and the other empty ranges around lines
noted: 769, 811, 844-869) and provide proper Chinese translations for each key
(copy the EN string and translate it into Chinese, preserving punctuation and
placeholders), update the zh.json entries with those translated values, and run
a quick linter/JSON validator to ensure no syntax errors after inserting the
strings.

In `@client/src/api/admin/endpoints/teams.ts`:
- Around line 47-53: The createTeam client currently returns
authedFetch<Team>(...) but the server returns members: string[] and siteIds:
number[] (and uses the field name siteIds), which doesn't match the Team shape
(members: TeamMember[], sites: TeamSite[]), causing runtime errors; update
createTeam (or the useCreateTeam hook) to either (A) request the actual server
response type and transform it into the client Team shape by mapping members
string[] -> TeamMember[] (e.g., { userId: id, userName: "", userEmail: "" } or
fill from user lookup) and map siteIds number[] -> TeamSite[] with { siteId: id,
domain: "", name: "" } and rename siteIds -> sites, or (B) change the TypeScript
return type to match the server response (e.g., use an explicit
ServerCreateTeamResponse type) so callers handle the flat arrays; implement the
chosen approach inside createTeam (the authedFetch call) or immediately after
its result so downstream code expecting Team (e.g., useCreateTeam consumers)
receives the correct shape.

In
`@client/src/app/settings/organization/members/components/InviteMemberDialog.tsx`:
- Around line 51-58: When organizationId (or the teams data returned by
useTeams) changes you must clear stale selections so invites don’t submit
invalid team IDs: add a useEffect watching organizationId or teams (from
teamsData) and call setSelectedTeamIds([]) (and also clear selectedSiteIds via
setSelectedSiteIds([]) if site lists change) to reset selections; update the
logic around selectedTeamIds/selectedSiteIds (used by the invite mutation in
InviteMemberDialog) so the arrays cannot persist across org switches and cause
server validation errors.

In
`@client/src/app/settings/organization/teams/components/CreateEditTeamDialog.tsx`:
- Line 3: Replace the incorrect next-intl import and usages: change the import
named useExtracted to useTranslations (i.e., import { useTranslations } from
"next-intl") and update every call site in CreateEditTeamDialog (e.g., calls
like useExtracted("...") on line 41) to useTranslations("...") instead; ensure
any destructured values or variable names derived from the hook are updated
accordingly so the component uses useTranslations consistently.

In `@client/src/app/settings/organization/teams/components/DeleteTeamDialog.tsx`:
- Line 3: The DeleteTeamDialog component imports the wrong i18n hook; replace
the invalid useExtracted import from next-intl with useTranslations and update
any calls to useExtracted(...) (e.g., at the useExtracted(...) call around line
27 inside DeleteTeamDialog) to use useTranslations(...), keeping the same
translation namespace/key string and variable name where used so existing t(...)
calls continue to work.

In `@client/src/app/settings/organization/teams/page.tsx`:
- Around line 23-25: The page title is hard-coded in TeamsPage; replace the
literal "Organization Teams" by using next-intl's useTranslations and pass the
localized string into useSetPageTitle. In TeamsPage, import useTranslations from
'next-intl', call const t = useTranslations('<namespace>') (or the existing
translation namespace used by other strings on this page) and then call
useSetPageTitle(t('organizationTeams') or the chosen key); also add the
organizationTeams key to your messages/*.json translation files (en.json,
de.json, etc.) so the title is localized.
- Around line 28-30: The UI currently conflates a failed fetch with an empty
team list because it only checks teamsData and teamsLoading; update the render
logic that uses useTeams (symbols: useTeams, teamsData, teamsLoading) to also
check React Query's isError and error from the hook and handle errors explicitly
(e.g., render an error message or rethrow so the error boundary surfaces the
failure) before falling back to the "No teams yet" empty state; ensure the flow
is: if (teamsLoading) show spinner, else if (isError) show error UI or surface
error, else if (teamsData?.length === 0) show empty state. This change should be
applied wherever the empty-state rendering occurs (the blocks currently using
teamsData/teamsLoading).

In `@server/drizzle/0002_short_bishop.sql`:
- Around line 9-14: The teamMember table allows duplicate memberships for the
same team/user pair; add a uniqueness constraint on the (team_id, user_id) pair
to enforce one row per membership. Modify the CREATE TABLE for "teamMember" (or
add an ALTER TABLE) to include a UNIQUE(team_id, user_id) constraint (or replace
the current "id" primary key with a composite primary key on team_id and user_id
if you prefer) so inserts for the same team_id and user_id will be rejected at
the DB level.

In `@server/src/api/teams/createTeam.ts`:
- Around line 13-17: Validate and normalize the incoming CreateTeamBody
(currently represented by the CreateTeamBody interface and accessed via
request.body) using Zod before any DB work: define a Zod schema for name
(string), memberUserIds (optional array of strings) and siteIds (optional array
of numbers), parse request.body and return a 400 on failure, then normalize by
filtering out falsy values and deduplicating memberUserIds and siteIds (so
duplicates won't create duplicate teamMember rows or trigger the
team_site_access_unique DB constraint). Replace direct usage of request.body in
the create-team handler with the validated result and use the deduped arrays
when inserting into teamMember and team_site_access tables to prevent DB errors
and membership corruption.

---

Outside diff comments:
In `@client/src/app/page.tsx`:
- Around line 63-81: selectedTeamFilter can remain pointing at a team from the
previous organization and produce empty filteredSites after org switch; add an
effect that resets selectedTeamFilter to "all" when the organization or its
teams change (watch activeOrganization?.id and/or teams from useTeams) so stale
team IDs are cleared; use setSelectedTeamFilter("all") in that effect and ensure
the UI still treats "all" as the default when no teams exist.

In `@server/src/lib/auth.ts`:
- Around line 220-285: The invitation-to-member access-copy currently runs in
its own try/catch and only logs errors, which can leave a newly created
membership without intended site restrictions or team assignments; move the
access-copy logic into the same database transaction that creates the membership
(use the same db.transaction scope and tx used to persist the member) so
failures abort the whole acceptance, or if you must keep it separate, perform a
compensating rollback by deleting the newly created member and any partial
inserts on error; update the code paths around invitationRecord/userRecord and
replace the separate db.transaction block (and its tx usage that inserts into
memberSiteAccess and teamMember and updates member) so they execute inside the
original acceptance transaction that creates member (referencing invitation,
invitationRecord, userRecord, memberRecord, memberSiteAccess, teamMember, and
tx) to ensure atomicity.

---

Nitpick comments:
In `@server/src/api/sites/getSitesFromOrg.ts`:
- Around line 53-91: This handler duplicates team-based filtering already
implemented in getSitesUserHasAccessTo (auth-utils.ts); remove the entire
memberRecord role==="member" block that queries teamSiteAccess/team and
teamMember and instead rely on (or call) getSitesUserHasAccessTo to filter sites
for the current user/organization, or extract the shared logic into a single
helper used by both; ensure references to memberRecord, teamSiteAccess,
teamMember, team, and sitesData are cleaned up so no redundant DB queries remain
and the handler uses the centralized access check.

In `@server/src/api/teams/listTeams.ts`:
- Around line 20-40: The handler currently treats a missing membership
(memberRecord) as an empty teams response; update the logic in listTeams.ts to
return a 403 Unauthorized when the requesting user is not a member of the
organization. Specifically, after computing memberRecord and isAdminOrOwner
(using request.params.organizationId, request.user?.id / getUserIdFromRequest,
and the db query against member), if memberRecord.length === 0 (i.e., user has
no membership) respond with a 403 error (or reply.code(403).send) instead of
continuing to fetch/return an empty teams list; preserve existing behavior for
admins/owners (isAdminOrOwner === true) and for any other allowed flows.

In `@server/src/api/teams/updateTeam.ts`:
- Around line 91-96: The code allows a provided name to be trimmed to an empty
string and saved; before starting the transaction (i.e. before building
updates/await tx.update(team).set(...)), validate the incoming name: if name !==
undefined then compute const trimmed = name.trim(); if trimmed === ""
return/throw a 400-style error (or BadRequest) and do not proceed; otherwise
assign updates.name = trimmed. Apply this check near where now and updates are
prepared so teamId, updates, and tx.update(team).set(...) never run with an
empty name.

In `@server/src/db/postgres/schema.ts`:
- Around line 216-221: The table name string for the teamMember pgTable is
inconsistent (uses "teamMember"); change the pgTable invocation so the database
table name is snake_case (e.g., "team_member") to match the rest of the schema,
update any raw SQL/migration SQL that refers to the old name, and verify
references to the exported constant teamMember (and any foreign-key usages)
still work after renaming; locate the instantiation via the teamMember variable
and the pgTable("teamMember", ...) call to make the change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: db53c9cb-6849-4503-9aea-83da4fae1d9d

📥 Commits

Reviewing files that changed from the base of the PR and between e0893f0 and 41e6bba.

📒 Files selected for processing (36)
  • client/messages/cs.json
  • client/messages/de.json
  • client/messages/en.json
  • client/messages/es.json
  • client/messages/fr.json
  • client/messages/it.json
  • client/messages/ja.json
  • client/messages/ko.json
  • client/messages/pl.json
  • client/messages/pt.json
  • client/messages/zh.json
  • client/src/api/admin/endpoints/index.ts
  • client/src/api/admin/endpoints/sites.ts
  • client/src/api/admin/endpoints/teams.ts
  • client/src/api/admin/hooks/useTeams.ts
  • client/src/app/page.tsx
  • client/src/app/settings/components/SettingsSidebar.tsx
  • client/src/app/settings/organization/layout.tsx
  • client/src/app/settings/organization/members/components/InviteMemberDialog.tsx
  • client/src/app/settings/organization/teams/components/CreateEditTeamDialog.tsx
  • client/src/app/settings/organization/teams/components/DeleteTeamDialog.tsx
  • client/src/app/settings/organization/teams/page.tsx
  • server/drizzle/0002_short_bishop.sql
  • server/drizzle/meta/0002_snapshot.json
  • server/drizzle/meta/_journal.json
  • server/src/api/memberAccess/updateInvitationSiteAccess.ts
  • server/src/api/sites/getSitesFromOrg.ts
  • server/src/api/teams/createTeam.ts
  • server/src/api/teams/deleteTeam.ts
  • server/src/api/teams/index.ts
  • server/src/api/teams/listTeams.ts
  • server/src/api/teams/updateTeam.ts
  • server/src/db/postgres/schema.ts
  • server/src/index.ts
  • server/src/lib/auth-utils.ts
  • server/src/lib/auth.ts

Comment on lines +667 to +669
"WyooVh": "",
"DoskyC": "",
"t6nrnE": "",
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Missing Czech translations for team-related strings.

Multiple new localization keys have empty values (""), which will result in blank or missing text for Czech-speaking users. These include team filter labels, team management dialogs, and member/site selection strings.

Consider adding the Czech translations before merging, or track this as a follow-up task to ensure complete localization.

Also applies to: 769-769, 811-812, 844-869

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/messages/cs.json` around lines 667 - 669, Several Czech localization
keys are empty (e.g., "WyooVh", "DoskyC", "t6nrnE" and other keys referenced
around the same change set), causing missing UI text for Czech users; update the
JSON entries by providing appropriate Czech translations for those keys (team
filter labels, team management dialogs, member/site selection strings) or, if
you cannot translate now, add clear TODO comments and fallback English values
for each key so the UI remains readable; ensure you modify the same keys
(WyooVh, DoskyC, t6nrnE, plus the other empty keys noted near lines ~769,
~811-812, ~844-869) in the locale JSON to include completed translations or
temporary fallbacks.

Comment on lines +667 to +669
"WyooVh": "",
"DoskyC": "",
"t6nrnE": "",
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Missing Spanish translations for team management features.

Multiple new translation keys have been added with empty string values. Spanish-speaking users will see either blank text or English fallbacks for these team-related strings until translations are provided.

Consider adding the Spanish translations before release, or tracking this as a follow-up task.

Also applies to: 769-769, 811-812, 844-869

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/messages/es.json` around lines 667 - 669, The JSON file contains new
translation keys ("WyooVh", "DoskyC", "t6nrnE" and other empty keys in the
ranges noted) with empty string values causing Spanish users to see blanks;
update client/messages/es.json by providing Spanish translations for each empty
key (replace the empty values with the correct Spanish strings for the
corresponding team management UI labels), ensure keys match exactly and keep
JSON valid, and if you cannot supply final translations now, add a clear
placeholder comment or track these keys in a follow-up task so they're not
released untranslated.

Comment on lines +667 to +669
"WyooVh": "",
"DoskyC": "",
"t6nrnE": "",
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Backfill the new French team strings before release.

Several of the new Teams/invitation keys added in this hunk are still empty, so this locale will ship partially untranslated.

Also applies to: 769-769, 811-811, 844-869

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/messages/fr.json` around lines 667 - 669, Several newly added French
translation keys (e.g., "WyooVh", "DoskyC", "t6nrnE" and other empty keys around
the same hunk) are blank; please provide proper French strings for each missing
Teams/invitation key so the locale isn't shipped partially untranslated—open the
same JSON file and replace the empty values for the listed keys (and the other
empty entries noted near lines 769, 811, and 844–869) with the correct French
translations matching the English source keys.

Comment on lines +667 to +669
"WyooVh": "",
"DoskyC": "",
"t6nrnE": "",
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

next-intl fallback locale configuration

💡 Result:

In next-intl, there is no direct "fallbackLocale" configuration option. Instead, fallback behavior for locales and messages is handled in two primary ways: 1. Routing/Middleware Fallback (defaultLocale): Configure defaultLocale in your routing setup (e.g., defineRouting({ locales: ['en', 'de'], defaultLocale: 'en' })). The middleware falls back to this locale if no matching locale is detected from path, cookie, or Accept-Language header. See official docs on routing configuration and middleware. 2. Messages Fallback (Merge in getRequestConfig): For missing translations in a locale, merge messages from a fallback locale (typically the default) in i18n/request.ts before returning them: import {getRequestConfig} from 'next-intl/server'; export default getRequestConfig(async ({locale}) => { // Load primary locale messages let messages = (await import(./messages/${locale}.json)).default; // Merge with default locale as fallback (deep merge required) if (locale !== 'en') { const defaultMessages = (await import('./messages/en.json')).default; messages = merge(defaultMessages, messages); // Use deepmerge or similar } return { messages }; }); This ensures missing keys fall back to the default locale's values at runtime. Official docs explicitly recommend this approach for expected missing messages, as getMessageFallback is for error handling, not routine fallbacks. For client components, NextIntlClientProvider does not automatically inherit/apply server-side merges; pass merged messages explicitly. Use libraries like deepmerge for merging nested message objects.

Citations:


🏁 Script executed:

# First, let's check the size and content of ja.json around the specified lines
wc -l client/messages/ja.json

Repository: rybbit-io/rybbit

Length of output: 88


🏁 Script executed:

# Read ja.json around the lines mentioned (667-669, 769, 811-812, 844-869)
cat -n client/messages/ja.json | sed -n '665,671p'
cat -n client/messages/ja.json | sed -n '767,771p'
cat -n client/messages/ja.json | sed -n '809,814p'
cat -n client/messages/ja.json | sed -n '842,871p'

Repository: rybbit-io/rybbit

Length of output: 1348


🏁 Script executed:

# Check the i18n configuration setup
fd -e ts -e tsx -e js . client/src -x grep -l "getRequestConfig\|NextIntlClientProvider" {}

Repository: rybbit-io/rybbit

Length of output: 112


🏁 Script executed:

# Look for i18n related config files
fd -e ts -e js -path "*/i18n/*" .
ls -la client/src/ | grep -i i18n

Repository: rybbit-io/rybbit

Length of output: 300


🏁 Script executed:

# Check next.config.ts for next-intl configuration
cat -n next.config.ts 2>/dev/null || echo "next.config.ts not found"

Repository: rybbit-io/rybbit

Length of output: 84


🏁 Script executed:

# Count total empty string values in ja.json
rg ':\s*"",$' client/messages/ja.json | wc -l

Repository: rybbit-io/rybbit

Length of output: 62


🏁 Script executed:

# Compare with en.json to see if those keys have values
rg '"WyooVh"|"DoskyC"|"t6nrnE"' client/messages/en.json

Repository: rybbit-io/rybbit

Length of output: 140


🏁 Script executed:

# Check the i18n/request.ts configuration
cat -n client/src/i18n/request.ts

Repository: rybbit-io/rybbit

Length of output: 1990


🏁 Script executed:

# Check it.json mentioned in the review for same empty strings
rg '"WyooVh"|"DoskyC"|"t6nrnE"' client/messages/it.json

Repository: rybbit-io/rybbit

Length of output: 107


🏁 Script executed:

# List all locale files and count empty strings in each
fd -e json client/messages/ -x sh -c 'echo "File: {}"; rg ":\s*\"\",$" {} | wc -l'

Repository: rybbit-io/rybbit

Length of output: 429


🏁 Script executed:

# Check if there's a package.json or next.config.* in root
fd 'next\.config|tsconfig\.json|package\.json' -x cat -n {} \; | head -100

Repository: rybbit-io/rybbit

Length of output: 3822


🏁 Script executed:

# Check all locale files for the same empty string keys
for locale_file in client/messages/*.json; do
  locale=$(basename "$locale_file" .json)
  count=$(rg ':\s*"",$' "$locale_file" | wc -l)
  # Check if the specific keys exist
  has_empty=$(rg '"WyooVh"|"DoskyC"|"t6nrnE"' "$locale_file" | grep '""' | wc -l)
  echo "$locale: $count empty strings (sample keys empty: $has_empty)"
done

Repository: rybbit-io/rybbit

Length of output: 542


Empty translation strings will cause blank UI text across 10+ languages without fallback configuration.

All new Teams-related translation keys have empty values (""). The i18n configuration in client/src/i18n/request.ts loads only locale-specific messages without implementing a fallback mechanism, so users will see blank text instead of English defaults.

This affects:

  • ja.json, de.json, es.json, fr.json, it.json, ko.json, pl.json, pt.json, zh.json: 31 empty strings each
  • cs.json: 60 empty strings
  • Critical UI elements like team names, actions, and labels

Resolve by:

  1. Adding proper translations for all locales before merge
  2. Implementing message merging with English fallback in getRequestConfig (as documented in next-intl)
  3. Using English strings as temporary placeholders until translations are complete
Lines with empty values in ja.json
  "WyooVh": "",
  "DoskyC": "",
  "t6nrnE": "",

Also affects: 769, 811-812, 844-869

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/messages/ja.json` around lines 667 - 669, The ja.json contains empty
translation values for new Teams keys (e.g., "WyooVh", "DoskyC", "t6nrnE") which
will produce blank UI; fix by populating these keys with either proper Japanese
translations or English placeholder strings, and implement a fallback merge in
the i18n loader: update getRequestConfig to merge locale messages with the
English messages (default locale) so missing keys fall back to English (use
next-intl recommended merge pattern). Ensure all other affected locale files
(de.json, es.json, fr.json, it.json, ko.json, pl.json, pt.json, zh.json,
cs.json) are either filled or rely on the new fallback behavior.

Comment on lines +667 to +669
"WyooVh": "",
"DoskyC": "",
"t6nrnE": "",
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Complete these new Korean translations before release.

The added keys are all empty strings, so the localized teams/invitations flows will have missing copy for the affected labels, dialogs, and filters.

As per coding guidelines, "maintain translation files in messages/ directory (en.json, de.json, etc.)".

Also applies to: 769-769, 811-811, 844-869

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/messages/ko.json` around lines 667 - 669, Several new localization
keys (e.g., "WyooVh", "DoskyC", "t6nrnE" and other empty entries around the same
change set) were committed with empty strings; fill each empty value with the
proper Korean translation for the label/dialog/filter they represent, following
the project's translation guidelines for files in messages/; update the keys
listed in this diff and the other empty keys noted (other empty entries near the
same commit ranges) with accurate Korean copy, run the repository's
i18n/translation validation or linting to ensure no empty values remain, and
submit the updated ko.json entries in the same format as other locales.

@@ -0,0 +1,82 @@
"use client";

import { useExtracted } from "next-intl";
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Incorrect i18n hook import - useExtracted is not a valid next-intl export.

The import uses useExtracted from next-intl, but the correct hook is useTranslations. This will cause a runtime error.

As per coding guidelines: "Use next-intl's 'useTranslations()' hook for i18n".

🐛 Proposed fix
-import { useExtracted } from "next-intl";
+import { useTranslations } from "next-intl";

And on line 27:

-  const t = useExtracted();
+  const t = useTranslations();

Also applies to: 27-27

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/src/app/settings/organization/teams/components/DeleteTeamDialog.tsx`
at line 3, The DeleteTeamDialog component imports the wrong i18n hook; replace
the invalid useExtracted import from next-intl with useTranslations and update
any calls to useExtracted(...) (e.g., at the useExtracted(...) call around line
27 inside DeleteTeamDialog) to use useTranslations(...), keeping the same
translation namespace/key string and variable name where used so existing t(...)
calls continue to work.

Comment on lines +23 to +25
export default function TeamsPage() {
useSetPageTitle("Organization Teams");
const t = useExtracted();
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Localize the page title too.

The page/tab title is still hard-coded in English even though the rest of the screen is translated, so non-English locales will keep showing an English title here.

💡 Suggested fix
 export default function TeamsPage() {
-  useSetPageTitle("Organization Teams");
   const t = useExtracted();
+  useSetPageTitle(t("Organization Teams"));

As per coding guidelines, "client/src/**/*.{ts,tsx}: Use next-intl's 'useTranslations()' hook for i18n; maintain translation files in messages/ directory (en.json, de.json, etc.)".

📝 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.

Suggested change
export default function TeamsPage() {
useSetPageTitle("Organization Teams");
const t = useExtracted();
export default function TeamsPage() {
const t = useExtracted();
useSetPageTitle(t("Organization Teams"));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/src/app/settings/organization/teams/page.tsx` around lines 23 - 25,
The page title is hard-coded in TeamsPage; replace the literal "Organization
Teams" by using next-intl's useTranslations and pass the localized string into
useSetPageTitle. In TeamsPage, import useTranslations from 'next-intl', call
const t = useTranslations('<namespace>') (or the existing translation namespace
used by other strings on this page) and then call
useSetPageTitle(t('organizationTeams') or the chosen key); also add the
organizationTeams key to your messages/*.json translation files (en.json,
de.json, etc.) so the title is localized.

Comment on lines +28 to +30
const { data: teamsData, isLoading: teamsLoading } = useTeams(
activeOrganization?.id
);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don't collapse failed team fetches into the empty state.

If useTeams fails, teamsData stays undefined, teamsLoading flips to false, and this path renders “No teams yet”. That hides auth/server failures as an empty org and will mislead admins into recreating teams that still exist.

💡 Suggested fix
-  const { data: teamsData, isLoading: teamsLoading } = useTeams(
+  const {
+    data: teamsData,
+    isLoading: teamsLoading,
+    isError: isTeamsError,
+  } = useTeams(
     activeOrganization?.id
   );
@@
-  const teams = teamsData?.teams || [];
+  if (isTeamsError) {
+    return (
+      <Card>
+        <CardContent className="py-8 text-sm text-muted-foreground">
+          {t("Failed to load teams. Please refresh and try again.")}
+        </CardContent>
+      </Card>
+    );
+  }
+
+  const teams = teamsData?.teams ?? [];

As per coding guidelines, "Use try/catch for error handling with specific error types; let React Query surface API errors".

Also applies to: 52-52, 78-97

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/src/app/settings/organization/teams/page.tsx` around lines 28 - 30,
The UI currently conflates a failed fetch with an empty team list because it
only checks teamsData and teamsLoading; update the render logic that uses
useTeams (symbols: useTeams, teamsData, teamsLoading) to also check React
Query's isError and error from the hook and handle errors explicitly (e.g.,
render an error message or rethrow so the error boundary surfaces the failure)
before falling back to the "No teams yet" empty state; ensure the flow is: if
(teamsLoading) show spinner, else if (isError) show error UI or surface error,
else if (teamsData?.length === 0) show empty state. This change should be
applied wherever the empty-state rendering occurs (the blocks currently using
teamsData/teamsLoading).

Comment on lines +9 to +14
CREATE TABLE "teamMember" (
"id" text PRIMARY KEY NOT NULL,
"team_id" text NOT NULL,
"user_id" text NOT NULL,
"created_at" timestamp
);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Enforce one membership row per team/user pair.

teamMember currently allows the same user to be inserted into the same team multiple times. That will double-count members and makes membership/access checks depend on application-side deduping.

💡 Suggested fix
 CREATE TABLE "teamMember" (
 	"id" text PRIMARY KEY NOT NULL,
 	"team_id" text NOT NULL,
 	"user_id" text NOT NULL,
-	"created_at" timestamp
+	"created_at" timestamp,
+	CONSTRAINT "teamMember_team_user_unique" UNIQUE("team_id","user_id")
 );
 --> statement-breakpoint
+CREATE INDEX "teamMember_team_idx" ON "teamMember" USING btree ("team_id");--> statement-breakpoint
+CREATE INDEX "teamMember_user_idx" ON "teamMember" USING btree ("user_id");--> statement-breakpoint
📝 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.

Suggested change
CREATE TABLE "teamMember" (
"id" text PRIMARY KEY NOT NULL,
"team_id" text NOT NULL,
"user_id" text NOT NULL,
"created_at" timestamp
);
CREATE TABLE "teamMember" (
"id" text PRIMARY KEY NOT NULL,
"team_id" text NOT NULL,
"user_id" text NOT NULL,
"created_at" timestamp,
CONSTRAINT "teamMember_team_user_unique" UNIQUE("team_id","user_id")
);
--> statement-breakpoint
CREATE INDEX "teamMember_team_idx" ON "teamMember" USING btree ("team_id");
--> statement-breakpoint
CREATE INDEX "teamMember_user_idx" ON "teamMember" USING btree ("user_id");
--> statement-breakpoint
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/drizzle/0002_short_bishop.sql` around lines 9 - 14, The teamMember
table allows duplicate memberships for the same team/user pair; add a uniqueness
constraint on the (team_id, user_id) pair to enforce one row per membership.
Modify the CREATE TABLE for "teamMember" (or add an ALTER TABLE) to include a
UNIQUE(team_id, user_id) constraint (or replace the current "id" primary key
with a composite primary key on team_id and user_id if you prefer) so inserts
for the same team_id and user_id will be rejected at the DB level.

Comment on lines +13 to +17
interface CreateTeamBody {
name: string;
memberUserIds?: string[];
siteIds?: number[];
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Validate and dedupe the request body before the DB work.

request.body is untrusted here. A payload like {siteIds: [1, 1]} will hit team_site_access_unique and come back as a 500, while duplicate memberUserIds will currently create duplicate teamMember rows. Parse with Zod and normalize the arrays up front so bad input returns a 400 instead of tripping DB errors or corrupting membership data.

💡 Suggested fix
 import { eq, and, inArray } from "drizzle-orm";
 import { FastifyReply, FastifyRequest } from "fastify";
+import { z } from "zod";
 import { db } from "../../db/postgres/postgres.js";
@@
-interface CreateTeamBody {
-  name: string;
-  memberUserIds?: string[];
-  siteIds?: number[];
-}
+const createTeamBodySchema = z.object({
+  name: z.string().trim().min(1, "Team name is required"),
+  memberUserIds: z
+    .array(z.string().min(1))
+    .default([])
+    .transform((ids) => [...new Set(ids)]),
+  siteIds: z
+    .array(z.number().int().positive())
+    .default([])
+    .transform((ids) => [...new Set(ids)]),
+});
@@
 export async function createTeam(
   request: FastifyRequest<{
     Params: { organizationId: string };
-    Body: CreateTeamBody;
+    Body: unknown;
   }>,
   reply: FastifyReply
 ) {
   const { organizationId } = request.params;
-  const { name, memberUserIds, siteIds } = request.body;
-
-  if (!name || !name.trim()) {
-    return reply.status(400).send({ error: "Team name is required" });
+  const parsedBody = createTeamBodySchema.safeParse(request.body);
+  if (!parsedBody.success) {
+    return reply.status(400).send({ error: parsedBody.error.flatten() });
   }
+  const { name, memberUserIds, siteIds } = parsedBody.data;

Based on learnings, "Backend: Use Fastify, Drizzle ORM with Postgres, ClickHouse, and Zod".

Also applies to: 27-30, 35-72, 87-107

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/src/api/teams/createTeam.ts` around lines 13 - 17, Validate and
normalize the incoming CreateTeamBody (currently represented by the
CreateTeamBody interface and accessed via request.body) using Zod before any DB
work: define a Zod schema for name (string), memberUserIds (optional array of
strings) and siteIds (optional array of numbers), parse request.body and return
a 400 on failure, then normalize by filtering out falsy values and deduplicating
memberUserIds and siteIds (so duplicates won't create duplicate teamMember rows
or trigger the team_site_access_unique DB constraint). Replace direct usage of
request.body in the create-team handler with the validated result and use the
deduped arrays when inserting into teamMember and team_site_access tables to
prevent DB errors and membership corruption.

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