Skip to content

Commit a49b659

Browse files
feat: Ability to revoke an invite to an org (#902)
* feat: Ability to revoke an invite to an org * Resolve a conflict with main --------- Co-authored-by: Matt Aitken <[email protected]>
1 parent 61c9a7c commit a49b659

File tree

4 files changed

+102
-3
lines changed

4 files changed

+102
-3
lines changed

apps/webapp/app/models/member.server.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,3 +225,36 @@ export async function resendInvite({ inviteId }: { inviteId: string }) {
225225
},
226226
});
227227
}
228+
229+
export async function revokeInvite({
230+
userId,
231+
slug,
232+
inviteId,
233+
}: {
234+
userId: string;
235+
slug: string;
236+
inviteId: string;
237+
}) {
238+
const org = await prisma.organization.findFirst({
239+
where: { slug, members: { some: { userId } } },
240+
});
241+
242+
if (!org) {
243+
throw new Error("User does not have access to this organization");
244+
}
245+
const invite = await prisma.orgMemberInvite.delete({
246+
where: {
247+
id: inviteId,
248+
},
249+
select: {
250+
email: true,
251+
organization: true,
252+
},
253+
});
254+
255+
if (!invite) {
256+
throw new Error("Invite not found");
257+
}
258+
259+
return { email: invite.email, organization: invite.organization };
260+
}

apps/webapp/app/routes/_app.orgs.$organizationSlug.team/route.tsx

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,12 @@ import { useUser } from "~/hooks/useUser";
3131
import { getTeamMembersAndInvites, removeTeamMember } from "~/models/member.server";
3232
import { redirectWithSuccessMessage } from "~/models/message.server";
3333
import { requireUserId } from "~/services/session.server";
34-
import { inviteTeamMemberPath, organizationTeamPath, resendInvitePath } from "~/utils/pathBuilder";
34+
import {
35+
inviteTeamMemberPath,
36+
organizationTeamPath,
37+
resendInvitePath,
38+
revokeInvitePath,
39+
} from "~/utils/pathBuilder";
3540

3641
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
3742
const userId = await requireUserId(request);
@@ -147,8 +152,9 @@ export default function Page() {
147152
Invite sent {<DateTime date={invite.updatedAt} />}
148153
</Paragraph>
149154
</div>
150-
<div className="flex grow items-center justify-end gap-4">
155+
<div className="flex grow items-center justify-end gap-x-2">
151156
<ResendButton invite={invite} />
157+
<RevokeButton invite={invite} />
152158
</div>
153159
</li>
154160
))}
@@ -272,11 +278,28 @@ function LeaveTeamModal({
272278

273279
function ResendButton({ invite }: { invite: Invite }) {
274280
return (
275-
<Form method="post" action={resendInvitePath()}>
281+
<Form method="post" action={resendInvitePath()} className="flex">
276282
<input type="hidden" value={invite.id} name="inviteId" />
277283
<Button type="submit" variant="tertiary/small">
278284
Resend invite
279285
</Button>
280286
</Form>
281287
);
282288
}
289+
290+
function RevokeButton({ invite }: { invite: Invite }) {
291+
const organization = useOrganization();
292+
293+
return (
294+
<Form method="post" action={revokeInvitePath()} className="flex">
295+
<input type="hidden" value={invite.id} name="inviteId" />
296+
<input type="hidden" value={organization.slug} name="slug" />
297+
<Button
298+
type="submit"
299+
variant="danger/small"
300+
LeadingIcon="trash-can"
301+
leadingIconClassName="text-white"
302+
/>
303+
</Form>
304+
);
305+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { parse } from "@conform-to/zod";
2+
import { ActionFunction, json } from "@remix-run/server-runtime";
3+
import { z } from "zod";
4+
import { revokeInvite } from "~/models/member.server";
5+
import { redirectWithSuccessMessage } from "~/models/message.server";
6+
import { requireUserId } from "~/services/session.server";
7+
import { organizationTeamPath } from "~/utils/pathBuilder";
8+
9+
export const revokeSchema = z.object({
10+
inviteId: z.string(),
11+
slug: z.string(),
12+
});
13+
14+
export const action: ActionFunction = async ({ request }) => {
15+
const userId = await requireUserId(request);
16+
17+
const formData = await request.formData();
18+
const submission = parse(formData, { schema: revokeSchema });
19+
20+
if (!submission.value || submission.intent !== "submit") {
21+
return json(submission);
22+
}
23+
24+
try {
25+
const { email, organization } = await revokeInvite({
26+
userId,
27+
slug: submission.value.slug,
28+
inviteId: submission.value.inviteId,
29+
});
30+
31+
return redirectWithSuccessMessage(
32+
organizationTeamPath(organization),
33+
request,
34+
`Invite revoked for ${email}`
35+
);
36+
} catch (error: any) {
37+
return json({ errors: { body: error.message } }, { status: 400 });
38+
}
39+
};

apps/webapp/app/utils/pathBuilder.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,10 @@ export function logoutPath() {
132132
return `/logout`;
133133
}
134134

135+
export function revokeInvitePath() {
136+
return `/invite-revoke`;
137+
}
138+
135139
// Org
136140
export function organizationPath(organization: OrgForPath) {
137141
return `/orgs/${organizationParam(organization)}`;

0 commit comments

Comments
 (0)