mirror of
https://github.com/documenso/documenso.git
synced 2025-11-19 11:12:06 +10:00
fix: wip
This commit is contained in:
@ -0,0 +1,142 @@
|
||||
import Link from 'next/link';
|
||||
|
||||
import { Trans } from '@lingui/macro';
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
|
||||
import { getServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
|
||||
import { encryptSecondaryData } from '@documenso/lib/server-only/crypto/encrypt';
|
||||
import { declineTeamInvitation } from '@documenso/lib/server-only/team/decline-team-invitation';
|
||||
import { getTeamById } from '@documenso/lib/server-only/team/get-team';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { TeamMemberInviteStatus } from '@documenso/prisma/client';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
|
||||
type DeclineInvitationPageProps = {
|
||||
params: {
|
||||
token: string;
|
||||
};
|
||||
};
|
||||
|
||||
export default async function DeclineInvitationPage({
|
||||
params: { token },
|
||||
}: DeclineInvitationPageProps) {
|
||||
await setupI18nSSR();
|
||||
|
||||
const session = await getServerComponentSession();
|
||||
|
||||
const teamMemberInvite = await prisma.teamMemberInvite.findUnique({
|
||||
where: {
|
||||
token,
|
||||
},
|
||||
});
|
||||
|
||||
if (!teamMemberInvite) {
|
||||
return (
|
||||
<div className="w-screen max-w-lg px-4">
|
||||
<div className="w-full">
|
||||
<h1 className="text-4xl font-semibold">
|
||||
<Trans>Invalid token</Trans>
|
||||
</h1>
|
||||
|
||||
<p className="text-muted-foreground mb-4 mt-2 text-sm">
|
||||
<Trans>This token is invalid or has expired. No action is needed.</Trans>
|
||||
</p>
|
||||
|
||||
<Button asChild>
|
||||
<Link href="/">
|
||||
<Trans>Return</Trans>
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const team = await getTeamById({ teamId: teamMemberInvite.teamId });
|
||||
|
||||
const user = await prisma.user.findFirst({
|
||||
where: {
|
||||
email: {
|
||||
equals: teamMemberInvite.email,
|
||||
mode: 'insensitive',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (user) {
|
||||
await declineTeamInvitation({ userId: user.id, teamId: team.id });
|
||||
}
|
||||
|
||||
if (!user && teamMemberInvite.status !== TeamMemberInviteStatus.DECLINED) {
|
||||
await prisma.teamMemberInvite.update({
|
||||
where: {
|
||||
id: teamMemberInvite.id,
|
||||
},
|
||||
data: {
|
||||
status: TeamMemberInviteStatus.DECLINED,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const email = encryptSecondaryData({
|
||||
data: teamMemberInvite.email,
|
||||
expiresAt: DateTime.now().plus({ days: 1 }).toMillis(),
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return (
|
||||
<div>
|
||||
<h1 className="text-4xl font-semibold">
|
||||
<Trans>Team invitation</Trans>
|
||||
</h1>
|
||||
|
||||
<p className="text-muted-foreground mt-2 text-sm">
|
||||
<Trans>
|
||||
You have been invited by <strong>{team.name}</strong> to join their team.
|
||||
</Trans>
|
||||
</p>
|
||||
|
||||
<p className="text-muted-foreground mb-4 mt-1 text-sm">
|
||||
<Trans>To decline this invitation you must create an account.</Trans>
|
||||
</p>
|
||||
|
||||
<Button asChild>
|
||||
<Link href={`/signup?email=${encodeURIComponent(email)}`}>
|
||||
<Trans>Create account</Trans>
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const isSessionUserTheInvitedUser = user?.id === session.user?.id;
|
||||
|
||||
return (
|
||||
<div className="w-screen max-w-lg px-4">
|
||||
<h1 className="text-4xl font-semibold">
|
||||
<Trans>Invitation declined</Trans>
|
||||
</h1>
|
||||
|
||||
<p className="text-muted-foreground mb-4 mt-2 text-sm">
|
||||
<Trans>
|
||||
You have declined the invitation from <strong>{team.name}</strong> to join their team.
|
||||
</Trans>
|
||||
</p>
|
||||
|
||||
{isSessionUserTheInvitedUser ? (
|
||||
<Button asChild>
|
||||
<Link href="/">
|
||||
<Trans>Return to Dashboard</Trans>
|
||||
</Link>
|
||||
</Button>
|
||||
) : (
|
||||
<Button asChild>
|
||||
<Link href="/">
|
||||
<Trans>Return to Home</Trans>
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,147 @@
|
||||
import Link from 'next/link';
|
||||
|
||||
import { Trans } from '@lingui/macro';
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
|
||||
import { getServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
|
||||
import { encryptSecondaryData } from '@documenso/lib/server-only/crypto/encrypt';
|
||||
import { acceptTeamInvitation } from '@documenso/lib/server-only/team/accept-team-invitation';
|
||||
import { getTeamById } from '@documenso/lib/server-only/team/get-team';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { TeamMemberInviteStatus } from '@documenso/prisma/client';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
|
||||
type AcceptInvitationPageProps = {
|
||||
params: {
|
||||
token: string;
|
||||
};
|
||||
};
|
||||
|
||||
export default async function AcceptInvitationPage({
|
||||
params: { token },
|
||||
}: AcceptInvitationPageProps) {
|
||||
await setupI18nSSR();
|
||||
|
||||
const session = await getServerComponentSession();
|
||||
|
||||
const teamMemberInvite = await prisma.teamMemberInvite.findUnique({
|
||||
where: {
|
||||
token,
|
||||
},
|
||||
});
|
||||
|
||||
if (!teamMemberInvite) {
|
||||
return (
|
||||
<div className="w-screen max-w-lg px-4">
|
||||
<div className="w-full">
|
||||
<h1 className="text-4xl font-semibold">
|
||||
<Trans>Invalid token</Trans>
|
||||
</h1>
|
||||
|
||||
<p className="text-muted-foreground mb-4 mt-2 text-sm">
|
||||
<Trans>
|
||||
This token is invalid or has expired. Please contact your team for a new invitation.
|
||||
</Trans>
|
||||
</p>
|
||||
|
||||
<Button asChild>
|
||||
<Link href="/">
|
||||
<Trans>Return</Trans>
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const team = await getTeamById({ teamId: teamMemberInvite.teamId });
|
||||
|
||||
const user = await prisma.user.findFirst({
|
||||
where: {
|
||||
email: {
|
||||
equals: teamMemberInvite.email,
|
||||
mode: 'insensitive',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Directly convert the team member invite to a team member if they already have an account.
|
||||
if (user) {
|
||||
await acceptTeamInvitation({ userId: user.id, teamId: team.id });
|
||||
}
|
||||
|
||||
// For users who do not exist yet, set the team invite status to accepted, which is checked during
|
||||
// user creation to determine if we should add the user to the team at that time.
|
||||
if (!user && teamMemberInvite.status !== TeamMemberInviteStatus.ACCEPTED) {
|
||||
await prisma.teamMemberInvite.update({
|
||||
where: {
|
||||
id: teamMemberInvite.id,
|
||||
},
|
||||
data: {
|
||||
status: TeamMemberInviteStatus.ACCEPTED,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const email = encryptSecondaryData({
|
||||
data: teamMemberInvite.email,
|
||||
expiresAt: DateTime.now().plus({ days: 1 }).toMillis(),
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return (
|
||||
<div>
|
||||
<h1 className="text-4xl font-semibold">
|
||||
<Trans>Team invitation</Trans>
|
||||
</h1>
|
||||
|
||||
<p className="text-muted-foreground mt-2 text-sm">
|
||||
<Trans>
|
||||
You have been invited by <strong>{team.name}</strong> to join their team.
|
||||
</Trans>
|
||||
</p>
|
||||
|
||||
<p className="text-muted-foreground mb-4 mt-1 text-sm">
|
||||
<Trans>To accept this invitation you must create an account.</Trans>
|
||||
</p>
|
||||
|
||||
<Button asChild>
|
||||
<Link href={`/signup?email=${encodeURIComponent(email)}`}>
|
||||
<Trans>Create account</Trans>
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const isSessionUserTheInvitedUser = user.id === session.user?.id;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1 className="text-4xl font-semibold">
|
||||
<Trans>Invitation accepted!</Trans>
|
||||
</h1>
|
||||
|
||||
<p className="text-muted-foreground mb-4 mt-2 text-sm">
|
||||
<Trans>
|
||||
You have accepted an invitation from <strong>{team.name}</strong> to join their team.
|
||||
</Trans>
|
||||
</p>
|
||||
|
||||
{isSessionUserTheInvitedUser ? (
|
||||
<Button asChild>
|
||||
<Link href="/">
|
||||
<Trans>Continue</Trans>
|
||||
</Link>
|
||||
</Button>
|
||||
) : (
|
||||
<Button asChild>
|
||||
<Link href={`/signin?email=${encodeURIComponent(email)}`}>
|
||||
<Trans>Continue to login</Trans>
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,148 @@
|
||||
import Link from 'next/link';
|
||||
|
||||
import { Trans } from '@lingui/macro';
|
||||
|
||||
import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
|
||||
import { isTokenExpired } from '@documenso/lib/utils/token-verification';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
|
||||
type VerifyTeamEmailPageProps = {
|
||||
params: {
|
||||
token: string;
|
||||
};
|
||||
};
|
||||
|
||||
export default async function VerifyTeamEmailPage({ params: { token } }: VerifyTeamEmailPageProps) {
|
||||
await setupI18nSSR();
|
||||
|
||||
const teamEmailVerification = await prisma.teamEmailVerification.findUnique({
|
||||
where: {
|
||||
token,
|
||||
},
|
||||
include: {
|
||||
team: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!teamEmailVerification || isTokenExpired(teamEmailVerification.expiresAt)) {
|
||||
return (
|
||||
<div className="w-screen max-w-lg px-4">
|
||||
<div className="w-full">
|
||||
<h1 className="text-4xl font-semibold">
|
||||
<Trans>Invalid link</Trans>
|
||||
</h1>
|
||||
|
||||
<p className="text-muted-foreground mb-4 mt-2 text-sm">
|
||||
<Trans>
|
||||
This link is invalid or has expired. Please contact your team to resend a
|
||||
verification.
|
||||
</Trans>
|
||||
</p>
|
||||
|
||||
<Button asChild>
|
||||
<Link href="/">
|
||||
<Trans>Return</Trans>
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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({
|
||||
data: {
|
||||
teamId: team.id,
|
||||
email: teamEmailVerification.email,
|
||||
name: teamEmailVerification.name,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
isTeamEmailVerificationError = true;
|
||||
}
|
||||
|
||||
if (isTeamEmailVerificationError) {
|
||||
return (
|
||||
<div>
|
||||
<h1 className="text-4xl font-semibold">
|
||||
<Trans>Team email verification</Trans>
|
||||
</h1>
|
||||
|
||||
<p className="text-muted-foreground mt-2 text-sm">
|
||||
<Trans>
|
||||
Something went wrong while attempting to verify your email address for{' '}
|
||||
<strong>{team.name}</strong>. Please try again later.
|
||||
</Trans>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1 className="text-4xl font-semibold">
|
||||
<Trans>Team email verified!</Trans>
|
||||
</h1>
|
||||
|
||||
<p className="text-muted-foreground mb-4 mt-2 text-sm">
|
||||
<Trans>
|
||||
You have verified your email address for <strong>{team.name}</strong>.
|
||||
</Trans>
|
||||
</p>
|
||||
|
||||
<Button asChild>
|
||||
<Link href="/">
|
||||
<Trans>Continue</Trans>
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,127 @@
|
||||
import Link from 'next/link';
|
||||
|
||||
import { Trans } from '@lingui/macro';
|
||||
|
||||
import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
|
||||
import { transferTeamOwnership } from '@documenso/lib/server-only/team/transfer-team-ownership';
|
||||
import { isTokenExpired } from '@documenso/lib/utils/token-verification';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
|
||||
type VerifyTeamTransferPage = {
|
||||
params: {
|
||||
token: string;
|
||||
};
|
||||
};
|
||||
|
||||
export default async function VerifyTeamTransferPage({
|
||||
params: { token },
|
||||
}: VerifyTeamTransferPage) {
|
||||
await setupI18nSSR();
|
||||
|
||||
const teamTransferVerification = await prisma.teamTransferVerification.findUnique({
|
||||
where: {
|
||||
token,
|
||||
},
|
||||
include: {
|
||||
team: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!teamTransferVerification || isTokenExpired(teamTransferVerification.expiresAt)) {
|
||||
return (
|
||||
<div className="w-screen max-w-lg px-4">
|
||||
<div className="w-full">
|
||||
<h1 className="text-4xl font-semibold">
|
||||
<Trans>Invalid link</Trans>
|
||||
</h1>
|
||||
|
||||
<p className="text-muted-foreground mb-4 mt-2 text-sm">
|
||||
<Trans>
|
||||
This link is invalid or has expired. Please contact your team to resend a transfer
|
||||
request.
|
||||
</Trans>
|
||||
</p>
|
||||
|
||||
<Button asChild>
|
||||
<Link href="/">
|
||||
<Trans>Return</Trans>
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
try {
|
||||
await transferTeamOwnership({ token });
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
isTransferError = true;
|
||||
}
|
||||
|
||||
if (isTransferError) {
|
||||
return (
|
||||
<div>
|
||||
<h1 className="text-4xl font-semibold">
|
||||
<Trans>Team ownership transfer</Trans>
|
||||
</h1>
|
||||
|
||||
<p className="text-muted-foreground mt-2 text-sm">
|
||||
<Trans>
|
||||
Something went wrong while attempting to transfer the ownership of team{' '}
|
||||
<strong>{team.name}</strong> to your. Please try again later or contact support.
|
||||
</Trans>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1 className="text-4xl font-semibold">
|
||||
<Trans>Team ownership transferred!</Trans>
|
||||
</h1>
|
||||
|
||||
<p className="text-muted-foreground mb-4 mt-2 text-sm">
|
||||
<Trans>
|
||||
The ownership of team <strong>{team.name}</strong> has been successfully transferred to
|
||||
you.
|
||||
</Trans>
|
||||
</p>
|
||||
|
||||
<Button asChild>
|
||||
<Link href={`/t/${team.url}/settings`}>
|
||||
<Trans>Continue</Trans>
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,130 @@
|
||||
import Link from 'next/link';
|
||||
|
||||
import { Trans } from '@lingui/macro';
|
||||
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 { 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;
|
||||
};
|
||||
};
|
||||
|
||||
export default async function VerifyEmailPage({ params: { token } }: PageProps) {
|
||||
await setupI18nSSR();
|
||||
|
||||
if (!token) {
|
||||
return (
|
||||
<div className="w-screen max-w-lg px-4">
|
||||
<div className="w-full">
|
||||
<div className="mb-4 text-red-300">
|
||||
<XOctagon />
|
||||
</div>
|
||||
|
||||
<h2 className="text-4xl font-semibold">
|
||||
<Trans>No token provided</Trans>
|
||||
</h2>
|
||||
<p className="text-muted-foreground mt-2 text-base">
|
||||
<Trans>
|
||||
It seems that there is no token provided. Please check your email and try again.
|
||||
</Trans>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const verified = await verifyEmail({ token });
|
||||
|
||||
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">
|
||||
<AlertTriangle className="h-10 w-10 text-yellow-500" strokeWidth={2} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold md:text-4xl">
|
||||
<Trans>Something went wrong</Trans>
|
||||
</h2>
|
||||
|
||||
<p className="text-muted-foreground mt-4">
|
||||
<Trans>
|
||||
We were unable to verify your email. If your email is not verified already, please
|
||||
try again.
|
||||
</Trans>
|
||||
</p>
|
||||
|
||||
<Button className="mt-4" asChild>
|
||||
<Link href="/">
|
||||
<Trans>Go back home</Trans>
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
.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">
|
||||
<XCircle className="text-destructive h-10 w-10" strokeWidth={2} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold md:text-4xl">
|
||||
<Trans>Your token has expired!</Trans>
|
||||
</h2>
|
||||
|
||||
<p className="text-muted-foreground mt-4">
|
||||
<Trans>
|
||||
It seems that the provided token has expired. We've just sent you another token,
|
||||
please check your email and try again.
|
||||
</Trans>
|
||||
</p>
|
||||
|
||||
<Button className="mt-4" asChild>
|
||||
<Link href="/">
|
||||
<Trans>Go back home</Trans>
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
.with(EMAIL_VERIFICATION_STATE.VERIFIED, async () => {
|
||||
const { user } = await prisma.verificationToken.findFirstOrThrow({
|
||||
where: {
|
||||
token,
|
||||
},
|
||||
include: {
|
||||
user: true,
|
||||
},
|
||||
});
|
||||
|
||||
const data = encryptSecondaryData({
|
||||
data: JSON.stringify({
|
||||
userId: user.id,
|
||||
email: user.email,
|
||||
}),
|
||||
expiresAt: DateTime.now().plus({ minutes: 5 }).toMillis(),
|
||||
});
|
||||
|
||||
return <VerifyEmailPageClient signInData={data} />;
|
||||
})
|
||||
.with(EMAIL_VERIFICATION_STATE.ALREADY_VERIFIED, () => <VerifyEmailPageClient />)
|
||||
.exhaustive();
|
||||
}
|
||||
@ -0,0 +1,45 @@
|
||||
import type { Metadata } from 'next';
|
||||
import Link from 'next/link';
|
||||
|
||||
import { Trans } from '@lingui/macro';
|
||||
import { XCircle } from 'lucide-react';
|
||||
|
||||
import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Verify Email',
|
||||
};
|
||||
|
||||
export default async function EmailVerificationWithoutTokenPage() {
|
||||
await setupI18nSSR();
|
||||
|
||||
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">
|
||||
<XCircle className="text-destructive h-10 w-10" strokeWidth={2} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold md:text-4xl">
|
||||
<Trans>Uh oh! Looks like you're missing a token</Trans>
|
||||
</h2>
|
||||
|
||||
<p className="text-muted-foreground mt-4">
|
||||
<Trans>
|
||||
It seems that there is no token provided, if you are trying to verify your email
|
||||
please follow the link in your email.
|
||||
</Trans>
|
||||
</p>
|
||||
|
||||
<Button className="mt-4" asChild>
|
||||
<Link href="/">
|
||||
<Trans>Go back home</Trans>
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
123
apps/remix/app/_todo/middleware.ts
Normal file
123
apps/remix/app/_todo/middleware.ts
Normal file
@ -0,0 +1,123 @@
|
||||
import { cookies } from 'next/headers';
|
||||
import type { NextRequest } from 'next/server';
|
||||
import { NextResponse } from 'next/server';
|
||||
|
||||
import { getToken } from 'next-auth/jwt';
|
||||
|
||||
import { TEAM_URL_ROOT_REGEX } from '@documenso/lib/constants/teams';
|
||||
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
|
||||
|
||||
async function middleware(req: NextRequest): Promise<NextResponse> {
|
||||
const preferredTeamUrl = cookies().get('preferred-team-url');
|
||||
|
||||
const referrer = req.headers.get('referer');
|
||||
const referrerUrl = referrer ? new URL(referrer) : null;
|
||||
const referrerPathname = referrerUrl ? referrerUrl.pathname : null;
|
||||
|
||||
// Whether to reset the preferred team url cookie if the user accesses a non team page from a team page.
|
||||
const resetPreferredTeamUrl =
|
||||
referrerPathname &&
|
||||
referrerPathname.startsWith('/t/') &&
|
||||
(!req.nextUrl.pathname.startsWith('/t/') || req.nextUrl.pathname === '/');
|
||||
|
||||
// Redirect root page to `/documents` or `/t/{preferredTeamUrl}/documents`.
|
||||
if (req.nextUrl.pathname === '/') {
|
||||
const redirectUrlPath = formatDocumentsPath(
|
||||
resetPreferredTeamUrl ? undefined : preferredTeamUrl?.value,
|
||||
);
|
||||
|
||||
const redirectUrl = new URL(redirectUrlPath, req.url);
|
||||
const response = NextResponse.redirect(redirectUrl);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
// Redirect `/t` to `/settings/teams`.
|
||||
if (req.nextUrl.pathname === '/t') {
|
||||
const redirectUrl = new URL('/settings/teams', req.url);
|
||||
|
||||
return NextResponse.redirect(redirectUrl);
|
||||
}
|
||||
|
||||
// Redirect `/t/<team_url>` to `/t/<team_url>/documents`.
|
||||
if (TEAM_URL_ROOT_REGEX.test(req.nextUrl.pathname)) {
|
||||
const redirectUrl = new URL(`${req.nextUrl.pathname}/documents`, req.url);
|
||||
|
||||
const response = NextResponse.redirect(redirectUrl);
|
||||
response.cookies.set('preferred-team-url', req.nextUrl.pathname.replace('/t/', ''));
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
// Set the preferred team url cookie if user accesses a team page.
|
||||
if (req.nextUrl.pathname.startsWith('/t/')) {
|
||||
const response = NextResponse.next();
|
||||
response.cookies.set('preferred-team-url', req.nextUrl.pathname.split('/')[2]);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
if (req.nextUrl.pathname.startsWith('/signin')) {
|
||||
const token = await getToken({ req });
|
||||
|
||||
if (token) {
|
||||
const redirectUrl = new URL('/documents', req.url);
|
||||
|
||||
return NextResponse.redirect(redirectUrl);
|
||||
}
|
||||
}
|
||||
|
||||
// Clear preferred team url cookie if user accesses a non team page from a team page.
|
||||
if (resetPreferredTeamUrl || req.nextUrl.pathname === '/documents') {
|
||||
const response = NextResponse.next();
|
||||
response.cookies.set('preferred-team-url', '');
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
if (req.nextUrl.pathname.startsWith('/embed')) {
|
||||
const res = NextResponse.next();
|
||||
|
||||
const origin = req.headers.get('Origin') ?? '*';
|
||||
|
||||
// Allow third parties to iframe the document.
|
||||
res.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
||||
res.headers.set('Access-Control-Allow-Origin', origin);
|
||||
res.headers.set('Content-Security-Policy', `frame-ancestors ${origin}`);
|
||||
res.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
|
||||
res.headers.set('X-Content-Type-Options', 'nosniff');
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
return NextResponse.next();
|
||||
}
|
||||
|
||||
export default async function middlewareWrapper(req: NextRequest) {
|
||||
const response = await middleware(req);
|
||||
|
||||
// Can place anything that needs to be set on the response here.
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
export const config = {
|
||||
matcher: [
|
||||
/*
|
||||
* Match all request paths except for the ones starting with:
|
||||
* - api (API routes)
|
||||
* - _next/static (static files)
|
||||
* - _next/image (image optimization files)
|
||||
* - favicon.ico (favicon file)
|
||||
* - ingest (analytics)
|
||||
* - site.webmanifest
|
||||
*/
|
||||
{
|
||||
source: '/((?!api|_next/static|_next/image|ingest|favicon|site.webmanifest).*)',
|
||||
missing: [
|
||||
{ type: 'header', key: 'next-router-prefetch' },
|
||||
{ type: 'header', key: 'purpose', value: 'prefetch' },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
Reference in New Issue
Block a user