Skip to content

Commit e61ca4f

Browse files
regisstedileclaude
andcommitted
fix(organizations): fix data consistency bugs found in code review
- acceptInvite: create Profile + guard against user already in another org - removeMember: delete Profile + clear user.organizationId in transaction - inviteMember: reject invite if invitee already belongs to an org - profile page: fix members link pointing to /teams (now /settings/organizations/members) - E2E acceptInvite: assert user.organizationId and Profile after accept - E2E members list: scope name selector to ul li to avoid strict mode violation Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 31b5e2c commit e61ca4f

5 files changed

Lines changed: 58 additions & 10 deletions

File tree

apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/profile/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export default function OrganizationProfilePage() {
4444
</p>
4545

4646
<div className="flex flex-wrap gap-2 pt-2">
47-
<Button href="/teams">{t("members")}</Button>
47+
<Button href="/settings/organizations/members">{t("members")}</Button>
4848
<Button href="/event-types" color="secondary">
4949
{t("event_types_page_title")}
5050
</Button>

apps/web/playwright/settings/organizations.e2e.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ test.describe("Organization Settings", () => {
5454
await page.waitForLoadState("networkidle");
5555

5656
await expect(page.getByTestId("invite-member-btn")).toBeVisible();
57-
await expect(page.getByText(owner.name ?? owner.username ?? "")).toBeVisible();
57+
await expect(page.locator("ul li").filter({ hasText: owner.name ?? owner.username ?? "" })).toBeVisible();
5858
});
5959

6060
test("owner can invite existing member and member sees pending invite", async ({
@@ -135,6 +135,17 @@ test.describe("Organization Settings", () => {
135135
where: { userId_teamId: { userId: invitee.id, teamId: org.id } },
136136
});
137137
expect(membership?.accepted).toBe(true);
138+
139+
const updatedUser = await prisma.user.findUnique({
140+
where: { id: invitee.id },
141+
select: { organizationId: true },
142+
});
143+
expect(updatedUser?.organizationId).toBe(org.id);
144+
145+
const profile = await prisma.profile.findFirst({
146+
where: { userId: invitee.id, organizationId: org.id },
147+
});
148+
expect(profile).not.toBeNull();
138149
});
139150

140151
test("invitee can decline an org invite", async ({ page, users, orgs }) => {

packages/trpc/server/routers/viewer/organizations/acceptInvite.handler.ts

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1+
import { v4 as uuidv4 } from "uuid";
2+
13
import prisma from "@calcom/prisma";
24
import { TRPCError } from "@trpc/server";
35

46
import type { TrpcSessionUser } from "../../../types";
57
import type { TAcceptDeclineInviteInputSchema } from "./schema";
8+
import { getProfileUsername } from "./organizationUtils";
69

710
type AcceptInviteHandlerOptions = {
811
ctx: {
@@ -12,10 +15,16 @@ type AcceptInviteHandlerOptions = {
1215
};
1316

1417
export const acceptInviteHandler = async ({ ctx, input }: AcceptInviteHandlerOptions) => {
15-
const membership = await prisma.membership.findUnique({
16-
where: { userId_teamId: { userId: ctx.user.id, teamId: input.teamId } },
17-
select: { accepted: true, teamId: true },
18-
});
18+
const [membership, user] = await Promise.all([
19+
prisma.membership.findUnique({
20+
where: { userId_teamId: { userId: ctx.user.id, teamId: input.teamId } },
21+
select: { accepted: true, teamId: true },
22+
}),
23+
prisma.user.findUnique({
24+
where: { id: ctx.user.id },
25+
select: { id: true, username: true, email: true, organizationId: true },
26+
}),
27+
]);
1928

2029
if (!membership) {
2130
throw new TRPCError({ code: "NOT_FOUND", message: "Invite not found." });
@@ -25,11 +34,23 @@ export const acceptInviteHandler = async ({ ctx, input }: AcceptInviteHandlerOpt
2534
throw new TRPCError({ code: "BAD_REQUEST", message: "Invite already accepted." });
2635
}
2736

37+
if (user?.organizationId) {
38+
throw new TRPCError({ code: "CONFLICT", message: "You already belong to an organization." });
39+
}
40+
2841
await prisma.$transaction([
2942
prisma.membership.update({
3043
where: { userId_teamId: { userId: ctx.user.id, teamId: input.teamId } },
3144
data: { accepted: true },
3245
}),
46+
prisma.profile.create({
47+
data: {
48+
uid: uuidv4(),
49+
userId: ctx.user.id,
50+
organizationId: input.teamId,
51+
username: getProfileUsername(user ?? { email: "" }),
52+
},
53+
}),
3354
prisma.user.update({
3455
where: { id: ctx.user.id },
3556
data: { organizationId: input.teamId },

packages/trpc/server/routers/viewer/organizations/inviteMember.handler.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export const inviteMemberHandler = async ({ ctx, input }: InviteMemberHandlerOpt
2121

2222
const invitee = await prisma.user.findUnique({
2323
where: { email: input.email.toLowerCase() },
24-
select: { id: true, name: true, email: true, locale: true },
24+
select: { id: true, name: true, email: true, locale: true, organizationId: true },
2525
});
2626

2727
if (!invitee) {
@@ -31,6 +31,13 @@ export const inviteMemberHandler = async ({ ctx, input }: InviteMemberHandlerOpt
3131
});
3232
}
3333

34+
if (invitee.organizationId) {
35+
throw new TRPCError({
36+
code: "CONFLICT",
37+
message: "This user already belongs to an organization.",
38+
});
39+
}
40+
3441
const existing = await prisma.membership.findUnique({
3542
where: { userId_teamId: { userId: invitee.id, teamId: membership.team.id } },
3643
select: { accepted: true },

packages/trpc/server/routers/viewer/organizations/removeMember.handler.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,18 @@ export const removeMemberHandler = async ({ ctx, input }: RemoveMemberHandlerOpt
3333
throw new TRPCError({ code: "FORBIDDEN", message: "Cannot remove the organization owner." });
3434
}
3535

36-
await prisma.membership.delete({
37-
where: { userId_teamId: { userId: input.userId, teamId: membership.team.id } },
38-
});
36+
await prisma.$transaction([
37+
prisma.membership.delete({
38+
where: { userId_teamId: { userId: input.userId, teamId: membership.team.id } },
39+
}),
40+
prisma.profile.deleteMany({
41+
where: { userId: input.userId, organizationId: membership.team.id },
42+
}),
43+
prisma.user.updateMany({
44+
where: { id: input.userId, organizationId: membership.team.id },
45+
data: { organizationId: null },
46+
}),
47+
]);
3948

4049
return { success: true };
4150
};

0 commit comments

Comments
 (0)