Skip to content

Commit

Permalink
change how accepting invite works
Browse files Browse the repository at this point in the history
  • Loading branch information
DonKoko committed Nov 28, 2024
1 parent 2a2d282 commit 5374c17
Show file tree
Hide file tree
Showing 2 changed files with 109 additions and 21 deletions.
15 changes: 3 additions & 12 deletions app/modules/invite/service.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -375,10 +375,10 @@ export async function updateInviteStatus({
*/
export async function checkUserAndInviteMatch({
context,
params,
invite,
}: {
context: AppLoadContext;
params: Params<string>;
invite: Invite;
}) {
const authSession = context.getSession();
const { userId } = authSession;
Expand All @@ -395,16 +395,7 @@ export async function checkUserAndInviteMatch({
})
.catch(() => null);

/** We get the invite based on the id of the params */
const inv = await db.invite
.findFirst({
where: {
id: params.inviteId,
},
})
.catch(() => null);

if (user?.email !== inv?.inviteeEmail) {
if (user?.email !== invite?.inviteeEmail) {
throw new ShelfError({
cause: null,
title: "Wrong user",
Expand Down
115 changes: 106 additions & 9 deletions app/routes/_auth+/accept-invite.$inviteId.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import { InviteStatuses } from "@prisma/client";
import type { LoaderFunctionArgs } from "@remix-run/node";
import { json, redirect } from "@remix-run/node";
import { Form, useActionData, useLoaderData } from "@remix-run/react";
import { z } from "zod";
import { Button } from "~/components/shared/button";
import { Spinner } from "~/components/shared/spinner";

Check warning on line 7 in app/routes/_auth+/accept-invite.$inviteId.tsx

View workflow job for this annotation

GitHub Actions / ⬣ ESLint

'Spinner' is defined but never used. Allowed unused vars must match /^_/u
import { db } from "~/database/db.server";
import { useSearchParams } from "~/hooks/search-params";
import { useDisabled } from "~/hooks/use-disabled";
import { signInWithEmail } from "~/modules/auth/service.server";
import { generateRandomCode } from "~/modules/invite/helpers";
import {
Expand All @@ -13,20 +18,81 @@ import { setSelectedOrganizationIdCookie } from "~/modules/organization/context.
import { setCookie } from "~/utils/cookies.server";
import { INVITE_TOKEN_SECRET } from "~/utils/env";
import { ShelfError, makeShelfError } from "~/utils/error";
import { error, parseData, safeRedirect } from "~/utils/http.server";
import {
data,
error,
getParams,
parseData,
safeRedirect,
} from "~/utils/http.server";
import jwt from "~/utils/jsonwebtoken.server";

export async function loader({ context, request, params }: LoaderFunctionArgs) {
export async function loader({ context, params }: LoaderFunctionArgs) {
const { inviteId } = getParams(params, z.object({ inviteId: z.string() }), {
additionalData: { inviteId: params.inviteId },
});
try {
/** We get the invite based on the id of the params */
const invite = await db.invite
.findFirstOrThrow({
where: {
id: inviteId,
},
include: {
organization: {
select: {
name: true,
},
},
inviter: {
select: {
firstName: true,
lastName: true,
},
},
},
})
.catch((cause) => {
throw new ShelfError({
cause,
title: "Invite not found",
message:
"The invitation you are trying to accept is either not found or expired",
label: "Invite",
});
});

/** Here we have to do a check based on the session of the current user
* If the user is already signed in, we have to make sure the invite sent, is for the same user
*/
if (context.isAuthenticated) {
await checkUserAndInviteMatch({ context, params });
await checkUserAndInviteMatch({
context,
invite,
});
}

return json(
data({
inviter: `${invite.inviter.firstName} ${invite.inviter.lastName}`,
workspace: `${invite.organization.name}`,
})
);
} catch (cause) {
const reason = makeShelfError(cause);
throw json(
error({ ...reason, title: reason.title || "Accept team invite" }),
{
status: reason.status,
}
);
}
}

export async function action({ context, request }: LoaderFunctionArgs) {
try {
const { token } = parseData(
new URL(decodeURIComponent(request.url)).searchParams,
await request.formData(),
z.object({ token: z.string() }),
{
message:
Expand Down Expand Up @@ -103,7 +169,7 @@ export async function loader({ context, request, params }: LoaderFunctionArgs) {
"The invitation link is invalid. Please try clicking the link in your email again or request a new invite. If the issue persists, feel free to contact support";
}

throw json(
return json(
error({ ...reason, title: reason.title || "Accept team invite" }),
{
status: reason.status,
Expand All @@ -113,10 +179,41 @@ export async function loader({ context, request, params }: LoaderFunctionArgs) {
}

export default function AcceptInvite() {
const { inviter, workspace } = useLoaderData<typeof loader>();
const [searchParams] = useSearchParams();
const disabled = useDisabled();
const actionData = useActionData<typeof action>();
const error = actionData?.error;
return (
<div className=" flex max-w-[400px] flex-col items-center text-center">
<Spinner />
<p className="mt-2">Validating token...</p>
</div>
<>
<div className=" flex flex-col items-center text-center">
<h2>Accept invite</h2>
<p>
<strong>{inviter}</strong> invites you to join Shelf as a member of{" "}
<strong>{workspace}’s</strong> workspace.
</p>
<Form method="post" className="my-3">
<input
type="hidden"
name="token"
value={searchParams.get("token") || ""}
/>
{error && (
<p className="mx-[-200px] mb-3 text-sm text-error-500">
{error.message}
</p>
)}
<Button type="submit" disabled={disabled || error}>
{disabled ? "Validating token..." : "Accept invite"}
</Button>
</Form>
</div>
<div className=" mx-[-200px] mt-20 flex flex-col items-center text-center text-gray-600">
<p>
If you have any questions or need assistance, please don't hesitate to
contact our support team at [email protected].
</p>
</div>
</>
);
}

0 comments on commit 5374c17

Please sign in to comment.