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 <matt@mattaitken.com>
This commit is contained in:
Kritik Jiyaviya
2024-03-22 20:05:49 +05:30
committed by GitHub
parent 61c9a7c7d6
commit a49b659701
4 changed files with 102 additions and 3 deletions
+33
View File
@@ -225,3 +225,36 @@ export async function resendInvite({ inviteId }: { inviteId: string }) {
},
});
}
export async function revokeInvite({
userId,
slug,
inviteId,
}: {
userId: string;
slug: 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,
},
select: {
email: true,
organization: true,
},
});
if (!invite) {
throw new Error("Invite not found");
}
return { email: invite.email, organization: invite.organization };
}
@@ -31,7 +31,12 @@ import { useUser } from "~/hooks/useUser";
import { getTeamMembersAndInvites, removeTeamMember } from "~/models/member.server";
import { redirectWithSuccessMessage } from "~/models/message.server";
import { requireUserId } from "~/services/session.server";
import { inviteTeamMemberPath, organizationTeamPath, resendInvitePath } from "~/utils/pathBuilder";
import {
inviteTeamMemberPath,
organizationTeamPath,
resendInvitePath,
revokeInvitePath,
} from "~/utils/pathBuilder";
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
const userId = await requireUserId(request);
@@ -147,8 +152,9 @@ export default function Page() {
Invite sent {<DateTime date={invite.updatedAt} />}
</Paragraph>
</div>
<div className="flex grow items-center justify-end gap-4">
<div className="flex grow items-center justify-end gap-x-2">
<ResendButton invite={invite} />
<RevokeButton invite={invite} />
</div>
</li>
))}
@@ -272,7 +278,7 @@ function LeaveTeamModal({
function ResendButton({ invite }: { invite: Invite }) {
return (
<Form method="post" action={resendInvitePath()}>
<Form method="post" action={resendInvitePath()} className="flex">
<input type="hidden" value={invite.id} name="inviteId" />
<Button type="submit" variant="tertiary/small">
Resend invite
@@ -280,3 +286,20 @@ function ResendButton({ invite }: { invite: Invite }) {
</Form>
);
}
function RevokeButton({ invite }: { invite: Invite }) {
const organization = useOrganization();
return (
<Form method="post" action={revokeInvitePath()} className="flex">
<input type="hidden" value={invite.id} name="inviteId" />
<input type="hidden" value={organization.slug} name="slug" />
<Button
type="submit"
variant="danger/small"
LeadingIcon="trash-can"
leadingIconClassName="text-white"
/>
</Form>
);
}
+39
View File
@@ -0,0 +1,39 @@
import { parse } from "@conform-to/zod";
import { ActionFunction, json } from "@remix-run/server-runtime";
import { z } from "zod";
import { revokeInvite } from "~/models/member.server";
import { redirectWithSuccessMessage } from "~/models/message.server";
import { requireUserId } from "~/services/session.server";
import { organizationTeamPath } from "~/utils/pathBuilder";
export const revokeSchema = z.object({
inviteId: z.string(),
slug: z.string(),
});
export const action: ActionFunction = async ({ request }) => {
const userId = await requireUserId(request);
const formData = await request.formData();
const submission = parse(formData, { schema: revokeSchema });
if (!submission.value || submission.intent !== "submit") {
return json(submission);
}
try {
const { email, organization } = await revokeInvite({
userId,
slug: submission.value.slug,
inviteId: submission.value.inviteId,
});
return redirectWithSuccessMessage(
organizationTeamPath(organization),
request,
`Invite revoked for ${email}`
);
} catch (error: any) {
return json({ errors: { body: error.message } }, { status: 400 });
}
};
+4
View File
@@ -132,6 +132,10 @@ export function logoutPath() {
return `/logout`;
}
export function revokeInvitePath() {
return `/invite-revoke`;
}
// Org
export function organizationPath(organization: OrgForPath) {
return `/orgs/${organizationParam(organization)}`;