From 61b72193f36c74cfe8bba9b7740cc53801b32248 Mon Sep 17 00:00:00 2001 From: myftija Date: Thu, 25 Sep 2025 16:39:10 +0200 Subject: [PATCH 1/2] fix: org invite scoping Fixes some scoping issues with team invites. --- apps/webapp/app/models/member.server.ts | 60 +++++++++++++++++++----- apps/webapp/app/routes/invite-accept.tsx | 14 +++--- apps/webapp/app/routes/invite-resend.tsx | 3 +- apps/webapp/app/routes/invite-revoke.tsx | 4 +- 4 files changed, 59 insertions(+), 22 deletions(-) diff --git a/apps/webapp/app/models/member.server.ts b/apps/webapp/app/models/member.server.ts index 86ae5d371d..1cf2c70f2e 100644 --- a/apps/webapp/app/models/member.server.ts +++ b/apps/webapp/app/models/member.server.ts @@ -120,10 +120,17 @@ export async function inviteMembers({ }); } -export async function getInviteFromToken({ token }: { token: string }) { +export async function getInviteFromToken({ token, userId }: { token: string; userId: string }) { return await prisma.orgMemberInvite.findFirst({ where: { token, + organization: { + members: { + some: { + userId, + }, + }, + }, }, include: { organization: true, @@ -153,6 +160,13 @@ export async function acceptInvite({ userId, inviteId }: { userId: string; invit const invite = await tx.orgMemberInvite.delete({ where: { id: inviteId, + organization: { + members: { + some: { + userId, + }, + }, + }, }, include: { organization: { @@ -188,6 +202,13 @@ export async function acceptInvite({ userId, inviteId }: { userId: string; invit const remainingInvites = await tx.orgMemberInvite.findMany({ where: { email: invite.email, + organization: { + members: { + some: { + userId, + }, + }, + }, }, }); @@ -201,6 +222,13 @@ export async function declineInvite({ userId, inviteId }: { userId: string; invi const declinedInvite = await prisma.orgMemberInvite.delete({ where: { id: inviteId, + organization: { + members: { + some: { + userId, + }, + }, + }, }, include: { organization: true, @@ -217,6 +245,13 @@ export async function declineInvite({ userId, inviteId }: { userId: string; invi const remainingInvites = await prisma.orgMemberInvite.findMany({ where: { email: user!.email, + organization: { + members: { + some: { + userId, + }, + }, + }, }, }); @@ -224,10 +259,11 @@ export async function declineInvite({ userId, inviteId }: { userId: string; invi }); } -export async function resendInvite({ inviteId }: { inviteId: string }) { +export async function resendInvite({ inviteId, userId }: { inviteId: string; userId: string }) { return await prisma.orgMemberInvite.update({ where: { id: inviteId, + inviterId: userId, }, data: { updatedAt: new Date(), @@ -241,24 +277,24 @@ export async function resendInvite({ inviteId }: { inviteId: string }) { export async function revokeInvite({ userId, - slug, + orgSlug, inviteId, }: { userId: string; - slug: string; + orgSlug: string; inviteId: string; }) { - const org = await prisma.organization.findFirst({ - where: { slug, members: { some: { userId } } }, - }); - - if (!org) { - throw new Error("User does not have access to this organization"); - } const invite = await prisma.orgMemberInvite.delete({ where: { id: inviteId, - organizationId: org.id, + organization: { + slug: orgSlug, + members: { + some: { + userId, + }, + }, + }, }, select: { email: true, diff --git a/apps/webapp/app/routes/invite-accept.tsx b/apps/webapp/app/routes/invite-accept.tsx index 57c1ddff06..31aea24b87 100644 --- a/apps/webapp/app/routes/invite-accept.tsx +++ b/apps/webapp/app/routes/invite-accept.tsx @@ -18,7 +18,13 @@ export async function loader({ request }: LoaderFunctionArgs) { ); } - const invite = await getInviteFromToken({ token }); + if (!user) { + return redirectWithSuccessMessage("/", request, "Please log in to accept the invite.", { + ephemeral: false, + }); + } + + const invite = await getInviteFromToken({ token, userId: user.id }); if (!invite) { return redirectWithErrorMessage( "/", @@ -28,12 +34,6 @@ export async function loader({ request }: LoaderFunctionArgs) { ); } - if (!user) { - return redirectWithSuccessMessage("/", request, "Please log in to accept the invite.", { - ephemeral: false, - }); - } - if (invite.email !== user.email) { return redirectWithErrorMessage( "/", diff --git a/apps/webapp/app/routes/invite-resend.tsx b/apps/webapp/app/routes/invite-resend.tsx index 9d5ee08abe..dc66e89851 100644 --- a/apps/webapp/app/routes/invite-resend.tsx +++ b/apps/webapp/app/routes/invite-resend.tsx @@ -1,5 +1,5 @@ import { parse } from "@conform-to/zod"; -import { ActionFunction, json } from "@remix-run/server-runtime"; +import { type ActionFunction, json } from "@remix-run/server-runtime"; import { env } from "process"; import { z } from "zod"; import { resendInvite } from "~/models/member.server"; @@ -25,6 +25,7 @@ export const action: ActionFunction = async ({ request }) => { try { const invite = await resendInvite({ inviteId: submission.value.inviteId, + userId, }); try { diff --git a/apps/webapp/app/routes/invite-revoke.tsx b/apps/webapp/app/routes/invite-revoke.tsx index b066a08ba3..cd499e58dc 100644 --- a/apps/webapp/app/routes/invite-revoke.tsx +++ b/apps/webapp/app/routes/invite-revoke.tsx @@ -1,5 +1,5 @@ import { parse } from "@conform-to/zod"; -import { ActionFunction, json } from "@remix-run/server-runtime"; +import { type ActionFunction, json } from "@remix-run/server-runtime"; import { z } from "zod"; import { revokeInvite } from "~/models/member.server"; import { redirectWithSuccessMessage } from "~/models/message.server"; @@ -24,7 +24,7 @@ export const action: ActionFunction = async ({ request }) => { try { const { email, organization } = await revokeInvite({ userId, - slug: submission.value.slug, + orgSlug: submission.value.slug, inviteId: submission.value.inviteId, }); From 02d3e025d390aa7303d1f39b891f275602888eda Mon Sep 17 00:00:00 2001 From: myftija Date: Thu, 25 Sep 2025 18:26:28 +0200 Subject: [PATCH 2/2] Fix invite flow changes --- apps/webapp/app/models/member.server.ts | 78 +++++++++--------------- apps/webapp/app/routes/invite-accept.tsx | 2 +- apps/webapp/app/routes/invites.tsx | 8 +-- 3 files changed, 34 insertions(+), 54 deletions(-) diff --git a/apps/webapp/app/models/member.server.ts b/apps/webapp/app/models/member.server.ts index 1cf2c70f2e..82af3d01b9 100644 --- a/apps/webapp/app/models/member.server.ts +++ b/apps/webapp/app/models/member.server.ts @@ -120,17 +120,10 @@ export async function inviteMembers({ }); } -export async function getInviteFromToken({ token, userId }: { token: string; userId: string }) { +export async function getInviteFromToken({ token }: { token: string }) { return await prisma.orgMemberInvite.findFirst({ where: { token, - organization: { - members: { - some: { - userId, - }, - }, - }, }, include: { organization: true, @@ -154,19 +147,19 @@ export async function getUsersInvites({ email }: { email: string }) { }); } -export async function acceptInvite({ userId, inviteId }: { userId: string; inviteId: string }) { +export async function acceptInvite({ + user, + inviteId, +}: { + user: { id: string; email: string }; + inviteId: string; +}) { return await prisma.$transaction(async (tx) => { // 1. Delete the invite and get the invite details const invite = await tx.orgMemberInvite.delete({ where: { id: inviteId, - organization: { - members: { - some: { - userId, - }, - }, - }, + email: user.email, }, include: { organization: { @@ -181,7 +174,7 @@ export async function acceptInvite({ userId, inviteId }: { userId: string; invit const member = await tx.orgMember.create({ data: { organizationId: invite.organizationId, - userId, + userId: user.id, role: invite.role, }, }); @@ -201,14 +194,7 @@ export async function acceptInvite({ userId, inviteId }: { userId: string; invit // 4. Check for other invites const remainingInvites = await tx.orgMemberInvite.findMany({ where: { - email: invite.email, - organization: { - members: { - some: { - userId, - }, - }, - }, + email: user.email, }, }); @@ -216,42 +202,29 @@ export async function acceptInvite({ userId, inviteId }: { userId: string; invit }); } -export async function declineInvite({ userId, inviteId }: { userId: string; inviteId: string }) { +export async function declineInvite({ + user, + inviteId, +}: { + user: { id: string; email: string }; + inviteId: string; +}) { return await prisma.$transaction(async (tx) => { //1. delete invite const declinedInvite = await prisma.orgMemberInvite.delete({ where: { id: inviteId, - organization: { - members: { - some: { - userId, - }, - }, - }, + email: user.email, }, include: { organization: true, }, }); - //2. get email - const user = await prisma.user.findUnique({ - where: { id: userId }, - select: { email: true }, - }); - - //3. check for other invites + //2. check for other invites const remainingInvites = await prisma.orgMemberInvite.findMany({ where: { - email: user!.email, - organization: { - members: { - some: { - userId, - }, - }, - }, + email: user.email, }, }); @@ -284,7 +257,7 @@ export async function revokeInvite({ orgSlug: string; inviteId: string; }) { - const invite = await prisma.orgMemberInvite.delete({ + const invite = await prisma.orgMemberInvite.findFirst({ where: { id: inviteId, organization: { @@ -297,6 +270,7 @@ export async function revokeInvite({ }, }, select: { + id: true, email: true, organization: true, }, @@ -306,5 +280,11 @@ export async function revokeInvite({ throw new Error("Invite not found"); } + await prisma.orgMemberInvite.delete({ + where: { + id: invite.id, + }, + }); + return { email: invite.email, organization: invite.organization }; } diff --git a/apps/webapp/app/routes/invite-accept.tsx b/apps/webapp/app/routes/invite-accept.tsx index 31aea24b87..592384b951 100644 --- a/apps/webapp/app/routes/invite-accept.tsx +++ b/apps/webapp/app/routes/invite-accept.tsx @@ -24,7 +24,7 @@ export async function loader({ request }: LoaderFunctionArgs) { }); } - const invite = await getInviteFromToken({ token, userId: user.id }); + const invite = await getInviteFromToken({ token }); if (!invite) { return redirectWithErrorMessage( "/", diff --git a/apps/webapp/app/routes/invites.tsx b/apps/webapp/app/routes/invites.tsx index c4bd0057ec..11998a4676 100644 --- a/apps/webapp/app/routes/invites.tsx +++ b/apps/webapp/app/routes/invites.tsx @@ -1,6 +1,6 @@ import { conform, useForm } from "@conform-to/react"; import { parse } from "@conform-to/zod"; -import { ActionFunction, LoaderFunctionArgs, json, redirect } from "@remix-run/node"; +import { type ActionFunction, type LoaderFunctionArgs, json, redirect } from "@remix-run/node"; import { Form, useActionData } from "@remix-run/react"; import { typedjson, useTypedLoaderData } from "remix-typedjson"; import { z } from "zod"; @@ -36,7 +36,7 @@ const schema = z.object({ }); export const action: ActionFunction = async ({ request }) => { - const userId = await requireUserId(request); + const user = await requireUser(request); const formData = await request.formData(); const submission = parse(formData, { schema }); @@ -49,7 +49,7 @@ export const action: ActionFunction = async ({ request }) => { if (submission.intent === "accept") { const { remainingInvites, organization } = await acceptInvite({ inviteId: submission.value.inviteId, - userId, + user: { id: user.id, email: user.email }, }); if (remainingInvites.length === 0) { @@ -64,7 +64,7 @@ export const action: ActionFunction = async ({ request }) => { } else if (submission.intent === "decline") { const { remainingInvites, organization } = await declineInvite({ inviteId: submission.value.inviteId, - userId, + user: { id: user.id, email: user.email }, }); if (remainingInvites.length === 0) { return redirectWithSuccessMessage(