fix: make invite and confirmations long lived (#1309)

Previously we would delete all invites and confirmation tokens upon
completing the action that they represent.

This change instead adds a flag on each token indicating whether it has
been completed so we can action a
completed token differently in the UI to reduce confusion for users.

This had been brought up a number of times where confirmation emails,
team member invites and other items
may have been actioned and forgotten about causing an error toast/page
upon subsequent revisit.
This commit is contained in:
Lucas Smith
2024-08-28 14:08:35 +10:00
committed by GitHub
parent 7943ed5353
commit dfa89ffe44
18 changed files with 352 additions and 97 deletions

View File

@ -50,15 +50,50 @@ export default async function VerifyTeamEmailPage({ params: { token } }: VerifyT
);
}
if (teamEmailVerification.completed) {
return (
<div>
<h1 className="text-4xl font-semibold">
<Trans>Team email already verified!</Trans>
</h1>
<p className="text-muted-foreground mb-4 mt-2 text-sm">
<Trans>
You have already verified your email address for{' '}
<strong>{teamEmailVerification.team.name}</strong>.
</Trans>
</p>
<Button asChild>
<Link href="/">
<Trans>Continue</Trans>
</Link>
</Button>
</div>
);
}
const { team } = teamEmailVerification;
let isTeamEmailVerificationError = false;
try {
await prisma.$transaction([
prisma.teamEmailVerification.updateMany({
where: {
teamId: team.id,
email: teamEmailVerification.email,
},
data: {
completed: true,
},
}),
prisma.teamEmailVerification.deleteMany({
where: {
teamId: team.id,
expiresAt: {
lt: new Date(),
},
},
}),
prisma.teamEmail.create({

View File

@ -53,6 +53,29 @@ export default async function VerifyTeamTransferPage({
);
}
if (teamTransferVerification.completed) {
return (
<div>
<h1 className="text-4xl font-semibold">
<Trans>Team ownership transfer already completed!</Trans>
</h1>
<p className="text-muted-foreground mb-4 mt-2 text-sm">
<Trans>
You have already completed the ownership transfer for{' '}
<strong>{teamTransferVerification.team.name}</strong>.
</Trans>
</p>
<Button asChild>
<Link href="/">
<Trans>Continue</Trans>
</Link>
</Button>
</div>
);
}
const { team } = teamTransferVerification;
let isTransferError = false;

View File

@ -0,0 +1,56 @@
'use client';
import { useEffect } from 'react';
import Link from 'next/link';
import { Trans } from '@lingui/macro';
import { CheckCircle2 } from 'lucide-react';
import { signIn } from 'next-auth/react';
import { Button } from '@documenso/ui/primitives/button';
export type VerifyEmailPageClientProps = {
signInData?: string;
};
export const VerifyEmailPageClient = ({ signInData }: VerifyEmailPageClientProps) => {
useEffect(() => {
if (signInData) {
void signIn('manual', {
credential: signInData,
callbackUrl: '/documents',
});
}
}, [signInData]);
return (
<div className="w-screen max-w-lg px-4">
<div className="flex w-full items-start">
<div className="mr-4 mt-1 hidden md:block">
<CheckCircle2 className="h-10 w-10 text-green-500" strokeWidth={2} />
</div>
<div>
<h2 className="text-2xl font-bold md:text-4xl">
<Trans>Email Confirmed!</Trans>
</h2>
<p className="text-muted-foreground mt-4">
<Trans>
Your email has been successfully confirmed! You can now use all features of Documenso.
</Trans>
</p>
{!signInData && (
<Button className="mt-4" asChild>
<Link href="/">
<Trans>Go back home</Trans>
</Link>
</Button>
)}
</div>
</div>
</div>
);
};

View File

@ -1,12 +1,21 @@
import Link from 'next/link';
import { Trans } from '@lingui/macro';
import { AlertTriangle, CheckCircle2, XCircle, XOctagon } from 'lucide-react';
import { AlertTriangle, XCircle, XOctagon } from 'lucide-react';
import { DateTime } from 'luxon';
import { match } from 'ts-pattern';
import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
import { verifyEmail } from '@documenso/lib/server-only/user/verify-email';
import { encryptSecondaryData } from '@documenso/lib/server-only/crypto/encrypt';
import {
EMAIL_VERIFICATION_STATE,
verifyEmail,
} from '@documenso/lib/server-only/user/verify-email';
import { prisma } from '@documenso/prisma';
import { Button } from '@documenso/ui/primitives/button';
import { VerifyEmailPageClient } from './client';
export type PageProps = {
params: {
token: string;
@ -39,8 +48,8 @@ export default async function VerifyEmailPage({ params: { token } }: PageProps)
const verified = await verifyEmail({ token });
if (verified === null) {
return (
return await match(verified)
.with(EMAIL_VERIFICATION_STATE.NOT_FOUND, () => (
<div className="w-screen max-w-lg px-4">
<div className="flex w-full items-start">
<div className="mr-4 mt-1 hidden md:block">
@ -67,11 +76,8 @@ export default async function VerifyEmailPage({ params: { token } }: PageProps)
</div>
</div>
</div>
);
}
if (!verified) {
return (
))
.with(EMAIL_VERIFICATION_STATE.EXPIRED, () => (
<div className="w-screen max-w-lg px-4">
<div className="flex w-full items-start">
<div className="mr-4 mt-1 hidden md:block">
@ -98,34 +104,27 @@ export default async function VerifyEmailPage({ params: { token } }: PageProps)
</div>
</div>
</div>
);
}
))
.with(EMAIL_VERIFICATION_STATE.VERIFIED, async () => {
const { user } = await prisma.verificationToken.findFirstOrThrow({
where: {
token,
},
include: {
user: true,
},
});
return (
<div className="w-screen max-w-lg px-4">
<div className="flex w-full items-start">
<div className="mr-4 mt-1 hidden md:block">
<CheckCircle2 className="h-10 w-10 text-green-500" strokeWidth={2} />
</div>
const data = encryptSecondaryData({
data: JSON.stringify({
userId: user.id,
email: user.email,
}),
expiresAt: DateTime.now().plus({ minutes: 5 }).toMillis(),
});
<div>
<h2 className="text-2xl font-bold md:text-4xl">
<Trans>Email Confirmed!</Trans>
</h2>
<p className="text-muted-foreground mt-4">
<Trans>
Your email has been successfully confirmed! You can now use all features of Documenso.
</Trans>
</p>
<Button className="mt-4" asChild>
<Link href="/">
<Trans>Go back home</Trans>
</Link>
</Button>
</div>
</div>
</div>
);
return <VerifyEmailPageClient signInData={data} />;
})
.with(EMAIL_VERIFICATION_STATE.ALREADY_VERIFIED, () => <VerifyEmailPageClient />)
.exhaustive();
}