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:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
};
|
||||
@@ -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)}`;
|
||||
|
||||
Reference in New Issue
Block a user