diff --git a/apps/web/__tests__/unit/sso-auth.test.ts b/apps/web/__tests__/unit/sso-auth.test.ts index b65ea702e89..f6f659e5f93 100644 --- a/apps/web/__tests__/unit/sso-auth.test.ts +++ b/apps/web/__tests__/unit/sso-auth.test.ts @@ -81,6 +81,7 @@ function matchesRow(condition: SQL | undefined, scope: RowScope) { function createDatabase(seed: Map) { const tables = new Map(seed); const hooks: { beforeUpdate?: (table: MySqlTable) => void } = {}; + const lockedTables: MySqlTable[] = []; const rows = (table: MySqlTable) => { const result = tables.get(table); if (!result) throw new Error("Unexpected table"); @@ -118,6 +119,7 @@ function createDatabase(seed: Map) { } for() { + if (this.table) lockedTables.push(this.table); return Promise.resolve().then(() => this.execute()); } @@ -223,7 +225,7 @@ function createDatabase(seed: Map) { }, ), }; - return { client, rows, hooks }; + return { client, rows, hooks, lockedTables }; } const USER_ID = User.UserId.make("sso-user"); @@ -322,18 +324,21 @@ function makeFixture() { id: "same-org-invite", organizationId: ORGANIZATION_ID, invitedEmail: PROFILE.email, + role: "member", status: "pending", }, { id: "other-org-invite", organizationId: OTHER_ORGANIZATION_ID, invitedEmail: PROFILE.email, + role: "admin", status: "pending", }, { id: "other-email-invite", organizationId: ORGANIZATION_ID, invitedEmail: "someone@company.example", + role: "admin", status: "pending", }, ], @@ -680,6 +685,28 @@ describe("SSO membership provisioning", () => { expect(mocks.stripe).not.toHaveBeenCalled(); }); + it("joins with the admin role from the accepted invitation", async () => { + const fixture = makeFixture(); + fixture.link(); + const invite = fixture + .rows(Db.organizationInvites) + .find((row) => row.id === "same-org-invite"); + if (!invite) throw new Error("Fixture invite was not found"); + invite.role = "admin"; + + await provisionSsoMembership(USER_ID, IDENTITY); + + expect(fixture.rows(Db.organizationMembers)).toEqual([ + expect.objectContaining({ + userId: USER_ID, + organizationId: ORGANIZATION_ID, + role: "admin", + }), + ]); + expect(invite.status).toBe("accepted"); + expect(fixture.lockedTables).toContain(Db.organizationInvites); + }); + it.each(["owner", "admin", "member"])( "preserves the existing %s role and Pro seat", async (role) => { diff --git a/packages/database/auth/sso.ts b/packages/database/auth/sso.ts index 97b5047b090..0ff2816c067 100644 --- a/packages/database/auth/sso.ts +++ b/packages/database/auth/sso.ts @@ -237,25 +237,34 @@ export async function provisionSsoMembership( ), ) .limit(1); + const pendingInvites = and( + eq(organizationInvites.organizationId, identity.organizationId), + eq(organizationInvites.invitedEmail, identity.email), + eq(organizationInvites.status, "pending"), + ); if (!member) { + const [adminInvite] = await tx + .select({ id: organizationInvites.id }) + .from(organizationInvites) + .where(and(pendingInvites, eq(organizationInvites.role, "admin"))) + .for("update"); await tx.insert(organizationMembers).values({ id: nanoId(), userId, organizationId: identity.organizationId, - role: organization.ownerId === userId ? "owner" : "member", + role: + organization.ownerId === userId + ? "owner" + : adminInvite + ? "admin" + : "member", hasProSeat: false, }); } await tx .update(organizationInvites) .set({ status: "accepted" }) - .where( - and( - eq(organizationInvites.organizationId, identity.organizationId), - eq(organizationInvites.invitedEmail, identity.email), - eq(organizationInvites.status, "pending"), - ), - ); + .where(pendingInvites); await tx .update(users) .set({