Skip to content

Commit dd34638

Browse files
committed
feat: Ability to revoke an invite to an org
1 parent 2aabb99 commit dd34638

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
@@ -29,7 +29,12 @@ import { useUser } from "~/hooks/useUser";
2929
import { getTeamMembersAndInvites, removeTeamMember } from "~/models/member.server";
3030
import { redirectWithSuccessMessage } from "~/models/message.server";
3131
import { requireUserId } from "~/services/session.server";
32-
import { inviteTeamMemberPath, organizationTeamPath, resendInvitePath } from "~/utils/pathBuilder";
32+
import {
33+
inviteTeamMemberPath,
34+
organizationTeamPath,
35+
resendInvitePath,
36+
revokeInvitePath,
37+
} from "~/utils/pathBuilder";
3338
import { DateTime } from "~/components/primitives/DateTime";
3439
import { PageHeader, PageTitleRow, PageTitle } from "~/components/primitives/PageHeader";
3540
import { BreadcrumbLink } from "~/components/navigation/Breadcrumb";
@@ -155,8 +160,9 @@ export default function Page() {
155160
Invite sent {<DateTime date={invite.updatedAt} />}
156161
</Paragraph>
157162
</div>
158-
<div className="flex grow items-center justify-end gap-4">
163+
<div className="flex grow items-center justify-end gap-x-2">
159164
<ResendButton invite={invite} />
165+
<RevokeButton invite={invite} />
160166
</div>
161167
</li>
162168
))}
@@ -280,11 +286,28 @@ function LeaveTeamModal({
280286

281287
function ResendButton({ invite }: { invite: Invite }) {
282288
return (
283-
<Form method="post" action={resendInvitePath()}>
289+
<Form method="post" action={resendInvitePath()} className="flex">
284290
<input type="hidden" value={invite.id} name="inviteId" />
285291
<Button type="submit" variant="secondary/small">
286292
Resend invite
287293
</Button>
288294
</Form>
289295
);
290296
}
297+
298+
function RevokeButton({ invite }: { invite: Invite }) {
299+
const organization = useOrganization();
300+
301+
return (
302+
<Form method="post" action={revokeInvitePath()} className="flex">
303+
<input type="hidden" value={invite.id} name="inviteId" />
304+
<input type="hidden" value={organization.slug} name="slug" />
305+
<Button
306+
type="submit"
307+
variant="danger/small"
308+
LeadingIcon="trash-can"
309+
leadingIconClassName="text-white"
310+
/>
311+
</Form>
312+
);
313+
}
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
@@ -106,6 +106,10 @@ export function logoutPath() {
106106
return `/logout`;
107107
}
108108

109+
export function revokeInvitePath() {
110+
return `/invite-revoke`;
111+
}
112+
109113
// Org
110114
export function organizationPath(organization: OrgForPath) {
111115
return `/orgs/${organizationParam(organization)}`;

0 commit comments

Comments
 (0)