Files
protocol/sites/dev0x-portal/app/routes/create-account.verify-email.tsx

206 lines
7.2 KiB
TypeScript

import { json, redirect } from '@remix-run/node';
import { getSignedInUser, sessionStorage } from '../auth.server';
import { getUserByEmail, sendVerificationEmail, verifyEmailVerificationToken } from '../data/zippo.server';
import { z } from 'zod';
import { Form, Link, useLoaderData } from '@remix-run/react';
import { Button, LinkButton } from '../components/Button';
import { useEffect, useState } from 'react';
import { addSeconds, differenceInSeconds } from 'date-fns';
import type { ActionArgs, LoaderArgs, MetaFunction } from '@remix-run/node';
export const meta: MetaFunction = () => {
return {
title: 'Verify Email | 0x',
description: 'Verify your email',
};
};
type VerifyEmailType = {
retryAt: string;
};
const zodEmailTokenModel = z.object({
email: z.string().email('Please enter a valid email'),
token: z.string().min(1, 'Please enter a valid token'),
});
const zodResendEmailModel = z.object({
email: z.string().email('Please enter a valid email'),
});
export async function action({ request }: ActionArgs) {
const [user, headers] = await getSignedInUser(request);
if (user) {
throw redirect('/apps', { headers });
}
const data = Object.fromEntries(await request.formData());
const zodResult = zodResendEmailModel.safeParse(data);
if (!zodResult.success) {
return json({ error: 'Invalid email' }, 400);
}
const session = await sessionStorage.getSession(request.headers.get('Cookie'));
const verifyEmail: VerifyEmailType | undefined = session.get('verifyEmail');
if (!verifyEmail) {
return json({ error: 'Error while verifying retry session' }, 400);
}
const userResult = await getUserByEmail({ email: zodResult.data.email });
if (userResult.result === 'ERROR') {
return json({ error: 'Error while verifying email' }, 400);
}
const retrievedUser = userResult.data;
const now = new Date();
const retryIn = new Date(verifyEmail.retryAt) > now ? differenceInSeconds(new Date(verifyEmail.retryAt), now) : 0;
if (retryIn > 0) {
return json({ error: 'Please wait before retrying' }, 400);
}
const result = await sendVerificationEmail({ email: zodResult.data.email, userId: retrievedUser.id });
if (result.result === 'ERROR') {
return json({ error: 'Error while sending verification email' }, 400);
}
verifyEmail.retryAt = addSeconds(new Date(), 45).toISOString();
session.set('verifyEmail', verifyEmail);
headers.append('Set-Cookie', await sessionStorage.commitSession(session));
return json({ error: null }, { headers });
}
export async function loader({ request }: LoaderArgs) {
const [user, headers] = await getSignedInUser(request);
if (user) {
throw redirect('/apps', { headers });
}
// if the user doesn't have the email and token query parameters, redirect them to the create account page
const url = new URL(request.url);
const email = url.searchParams.get('email');
const token = url.searchParams.get('token');
const zodResult = zodEmailTokenModel.safeParse({ email, token });
if (!zodResult.success) {
// if we receive invalid query params, we just redirect to login
throw redirect('/login', { headers });
}
const session = await sessionStorage.getSession(request.headers.get('Cookie'));
let verifyEmailSession: VerifyEmailType | undefined = session.get('verifyEmail');
if (!verifyEmailSession) {
verifyEmailSession = {
retryAt: new Date().toISOString(),
};
session.set('verifyEmail', verifyEmailSession);
headers.append('Set-Cookie', await sessionStorage.commitSession(session));
}
const verifyEmailRetryIn = verifyEmailSession.retryAt;
const now = new Date();
const retryIn =
verifyEmailRetryIn && new Date(verifyEmailRetryIn) > now
? differenceInSeconds(new Date(verifyEmailRetryIn), now)
: 0;
const isValid = await verifyEmailVerificationToken({ email: zodResult.data.email, token: zodResult.data.token });
if (isValid) {
session.unset('verifyEmail');
headers.set('Set-Cookie', await sessionStorage.commitSession(session));
return json({ error: null, values: { email, retryIn } }, { headers });
}
return json({ error: 'Invalid token or email address', values: { email, retryIn } }, { headers });
}
function SuccessMessage() {
return (
<div className="mt-40 w-full max-w-[456px]">
<h1 className="text-2.5xl mb-4 text-black">Your email has been verified</h1>
<p className="text-grey-500 mb-10 font-sans leading-[22px]">
Your email has been successfully verified. You can now login to your account.
</p>
<LinkButton
roundness="default"
color="grey"
className="flex w-full items-center justify-center text-lg"
to="/login"
>
Back to login
</LinkButton>
</div>
);
}
function ErrorMessage({ email, retryIn }: { email: string; retryIn: number }) {
const [retryCountdown, setRetryCountdown] = useState(retryIn);
useEffect(() => {
setRetryCountdown(retryIn);
}, [retryIn]);
useEffect(() => {
if (retryCountdown === 0) return;
const timeout = setTimeout(() => {
const newRetryCountdown = retryCountdown - 1;
setRetryCountdown(newRetryCountdown);
}, 1000);
return () => {
clearTimeout(timeout);
};
}, [retryCountdown]);
return (
<div className="mt-40 w-full max-w-[456px]">
<Link className="text-grey-500 mb-8 inline-block text-base" to="/login">
Back to login
</Link>
<h1 className="text-2.5xl mb-4 text-black">Verification link expired</h1>
<p className="text-grey-500 mb-10 font-sans leading-[22px]">
Request a new verification email to <strong className="text-grey-800 font-medium">{email}</strong> to
verify your account and get started.
</p>
<Form method="post">
<input type="hidden" name="email" value={email} />
<Button
roundness="default"
color="grey"
className="flex w-full items-center justify-center text-lg "
disabled={retryCountdown > 0}
>
Resend verification email
</Button>
</Form>
{retryCountdown > 0 && (
<span className="text-grey-500 mt-2 inline-block text-base">Resend email ({retryCountdown}s)</span>
)}
</div>
);
}
export default function VerifyEmail() {
const { error, values } = useLoaderData<typeof loader>();
return (
<main className="min-w-screen flex h-full min-h-full w-full flex-col bg-white">
<div className=" flex h-full w-full justify-center">
{error ? (
<ErrorMessage email={values.email || 'unknown email'} retryIn={values.retryIn} />
) : (
<SuccessMessage />
)}
</div>
</main>
);
}