import { parseWithZod } from "@conform-to/zod"; import { json } from "@remix-run/server-runtime"; import { env } from "process"; import { z } from "zod"; import { $replica } from "~/db.server"; import { resendInvite } from "~/models/member.server"; import { redirectWithSuccessMessage } from "~/models/message.server"; import { scheduleEmail } from "~/services/scheduleEmail.server"; import { ssoController } from "~/services/sso.server"; import { dashboardAction } from "~/services/routeBuilders/dashboardBuilder"; import { acceptInvitePath, organizationTeamPath } from "~/utils/pathBuilder"; export const resendSchema = z.object({ inviteId: z.string(), }); export const action = dashboardAction( { // No URL params — resolve the org for the auth scope from the invite // referenced in the form body. Read it off a clone so the handler can // still parse the original request. context: async (_params, request) => { const form = await request.clone().formData(); const inviteId = form.get("inviteId"); if (typeof inviteId !== "string") return {}; const invite = await $replica.orgMemberInvite.findFirst({ where: { id: inviteId }, select: { organizationId: true }, }); return invite ? { organizationId: invite.organizationId } : {}; }, authorization: { action: "manage", resource: { type: "members" } }, }, async ({ request, user, context }) => { const formData = await request.formData(); const submission = parseWithZod(formData, { schema: resendSchema }); if (submission.status !== "success") { return json(submission.reply()); } // Resending is an "add" — blocked when membership is directory-managed. if (context.organizationId) { const policy = await ssoController.getMembershipPolicy(context.organizationId); if (policy.isOk() && !policy.value.manualMembershipAllowed) { return json( { errors: { body: "Membership is managed by Directory Sync" } }, { status: 403 } ); } } try { const invite = await resendInvite({ inviteId: submission.value.inviteId, userId: user.id, }); try { await scheduleEmail({ email: "invite", to: invite.email, orgName: invite.organization.title, inviterName: invite.inviter.name ?? undefined, inviterEmail: invite.inviter.email, inviteLink: `${env.LOGIN_ORIGIN}${acceptInvitePath(invite.token)}`, }); } catch (error) { console.error("Failed to send invite email"); console.error(error); throw new Error("Failed to send invite email"); } return redirectWithSuccessMessage( organizationTeamPath(invite.organization), request, `Invite resent to ${invite.email}` ); } catch (error: any) { return json({ errors: { body: error.message } }, { status: 400 }); } } );