mirror of
https://github.com/documenso/documenso.git
synced 2025-11-20 03:32:14 +10:00
fix: wip
This commit is contained in:
@ -1,5 +1,6 @@
|
||||
import { Outlet, redirect } from 'react-router';
|
||||
|
||||
import { getLimits } from '@documenso/ee/server-only/limits/client';
|
||||
import { LimitsProvider } from '@documenso/ee/server-only/limits/provider/client';
|
||||
import { getSiteSettings } from '@documenso/lib/server-only/site-settings/get-site-settings';
|
||||
import { SITE_SETTINGS_BANNER_ID } from '@documenso/lib/server-only/site-settings/schemas/banner';
|
||||
@ -10,29 +11,35 @@ import { VerifyEmailBanner } from '~/components/(dashboard)/layout/verify-email-
|
||||
|
||||
import type { Route } from './+types/_layout';
|
||||
|
||||
export const loader = async ({ context }: Route.LoaderArgs) => {
|
||||
export const loader = async ({ request, context }: Route.LoaderArgs) => {
|
||||
const { session } = context;
|
||||
|
||||
const banner = await getSiteSettings().then((settings) =>
|
||||
settings.find((setting) => setting.id === SITE_SETTINGS_BANNER_ID),
|
||||
);
|
||||
|
||||
if (!session) {
|
||||
throw redirect('/signin');
|
||||
}
|
||||
|
||||
const banner = await getSiteSettings().then((settings) =>
|
||||
settings.find((setting) => setting.id === SITE_SETTINGS_BANNER_ID),
|
||||
);
|
||||
|
||||
const requestHeaders = Object.fromEntries(request.headers.entries());
|
||||
|
||||
const limits = await getLimits({ headers: requestHeaders, teamId: session.currentTeam?.id });
|
||||
|
||||
return {
|
||||
user: session.user,
|
||||
teams: session.teams,
|
||||
banner,
|
||||
limits,
|
||||
teamId: session.currentTeam?.id,
|
||||
};
|
||||
};
|
||||
|
||||
export default function Layout({ loaderData }: Route.ComponentProps) {
|
||||
const { user, teams, banner } = loaderData;
|
||||
const { user, teams, banner, limits, teamId } = loaderData;
|
||||
|
||||
return (
|
||||
<LimitsProvider>
|
||||
<LimitsProvider initialValue={limits} teamId={teamId}>
|
||||
{!user.emailVerified && <VerifyEmailBanner email={user.email} />}
|
||||
|
||||
{banner && <AppBanner banner={banner} />}
|
||||
|
||||
@ -30,6 +30,7 @@ import { DocumentPageViewDropdown } from '~/components/general/document/document
|
||||
import { DocumentPageViewInformation } from '~/components/general/document/document-page-view-information';
|
||||
import { DocumentPageViewRecentActivity } from '~/components/general/document/document-page-view-recent-activity';
|
||||
import { DocumentPageViewRecipients } from '~/components/general/document/document-page-view-recipients';
|
||||
import { superLoaderJson, useSuperLoaderData } from '~/utils/super-json-loader';
|
||||
|
||||
import type { Route } from './+types/$id._index';
|
||||
|
||||
@ -119,14 +120,16 @@ export async function loader({ params, context }: Route.LoaderArgs) {
|
||||
recipients,
|
||||
};
|
||||
|
||||
return {
|
||||
return superLoaderJson({
|
||||
document: documentWithRecipients,
|
||||
documentRootPath,
|
||||
fields,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export default function DocumentPage({ loaderData }: Route.ComponentProps) {
|
||||
export default function DocumentPage() {
|
||||
const loaderData = useSuperLoaderData<typeof loader>();
|
||||
|
||||
const { _ } = useLingui();
|
||||
const { user } = useSession();
|
||||
|
||||
|
||||
@ -12,7 +12,7 @@ import { findDocumentAuditLogs } from '@documenso/lib/server-only/document/find-
|
||||
import { dynamicActivate } from '@documenso/lib/utils/i18n';
|
||||
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
||||
|
||||
import { Logo } from '~/components/branding/logo';
|
||||
import { BrandingLogo } from '~/components/general/branding-logo';
|
||||
import { InternalAuditLogTable } from '~/components/tables/internal-audit-log-table';
|
||||
|
||||
import type { Route } from './+types/audit-log';
|
||||
@ -68,7 +68,7 @@ export default function AuditLog({ loaderData }: Route.ComponentProps) {
|
||||
const { i18n } = useLingui();
|
||||
|
||||
// Todo
|
||||
void dynamicActivate(i18n, documentLanguage);
|
||||
void dynamicActivate(documentLanguage);
|
||||
|
||||
const { _ } = useLingui();
|
||||
|
||||
@ -163,7 +163,7 @@ export default function AuditLog({ loaderData }: Route.ComponentProps) {
|
||||
|
||||
<div className="my-8 flex-row-reverse">
|
||||
<div className="flex items-end justify-end gap-x-4">
|
||||
<Logo className="max-h-6 print:max-h-4" />
|
||||
<BrandingLogo className="max-h-6 print:max-h-4" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -26,7 +26,7 @@ import {
|
||||
TableRow,
|
||||
} from '@documenso/ui/primitives/table';
|
||||
|
||||
import { Logo } from '~/components/branding/logo';
|
||||
import { BrandingLogo } from '~/components/general/branding-logo';
|
||||
|
||||
import type { Route } from './+types/certificate';
|
||||
|
||||
@ -316,7 +316,7 @@ export default function SigningCertificate({ loaderData }: Route.ComponentProps)
|
||||
{_(msg`Signing certificate provided by`)}:
|
||||
</p>
|
||||
|
||||
<Logo className="max-h-6 print:max-h-4" />
|
||||
<BrandingLogo className="max-h-6 print:max-h-4" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -10,7 +10,7 @@ import { cn } from '@documenso/ui/lib/utils';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
|
||||
import { Header as AuthenticatedHeader } from '~/components/(dashboard)/layout/header';
|
||||
import { Logo } from '~/components/branding/logo';
|
||||
import { Logo } from '~/components/general/branding-logo';
|
||||
|
||||
import type { Route } from './+types/_layout';
|
||||
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
// Todo: This relies on NextJS
|
||||
import { ImageResponse } from 'next/og';
|
||||
|
||||
import { P, match } from 'ts-pattern';
|
||||
|
||||
162
apps/remix/app/routes/_unauthenticated+/team.decline.$token.tsx
Normal file
162
apps/remix/app/routes/_unauthenticated+/team.decline.$token.tsx
Normal file
@ -0,0 +1,162 @@
|
||||
import { Trans } from '@lingui/macro';
|
||||
import { DateTime } from 'luxon';
|
||||
import { Link } from 'react-router';
|
||||
|
||||
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';
|
||||
|
||||
import type { Route } from './+types/team.decline.$token';
|
||||
|
||||
export async function loader({ params, context }: Route.LoaderArgs) {
|
||||
const { token } = params;
|
||||
|
||||
if (!token) {
|
||||
return {
|
||||
state: 'InvalidLink',
|
||||
} as const;
|
||||
}
|
||||
|
||||
const teamMemberInvite = await prisma.teamMemberInvite.findUnique({
|
||||
where: {
|
||||
token,
|
||||
},
|
||||
});
|
||||
|
||||
if (!teamMemberInvite) {
|
||||
return {
|
||||
state: 'InvalidLink',
|
||||
} as const;
|
||||
}
|
||||
|
||||
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 {
|
||||
state: 'LoginRequired',
|
||||
email,
|
||||
teamName: team.name,
|
||||
} as const;
|
||||
}
|
||||
|
||||
const isSessionUserTheInvitedUser = user.id === context.session?.user.id;
|
||||
|
||||
return {
|
||||
state: 'Success',
|
||||
email,
|
||||
teamName: team.name,
|
||||
isSessionUserTheInvitedUser,
|
||||
} as const;
|
||||
}
|
||||
|
||||
export default function DeclineInvitationPage({ loaderData }: Route.ComponentProps) {
|
||||
const data = loaderData;
|
||||
|
||||
if (data.state === 'InvalidLink') {
|
||||
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 to="/">
|
||||
<Trans>Return</Trans>
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (data.state === 'LoginRequired') {
|
||||
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>{data.teamName}</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 to={`/signup?email=${encodeURIComponent(data.email)}`}>
|
||||
<Trans>Create account</Trans>
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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>{data.teamName}</strong> to join their team.
|
||||
</Trans>
|
||||
</p>
|
||||
|
||||
{data.isSessionUserTheInvitedUser ? (
|
||||
<Button asChild>
|
||||
<Link to="/">
|
||||
<Trans>Return to Dashboard</Trans>
|
||||
</Link>
|
||||
</Button>
|
||||
) : (
|
||||
<Button asChild>
|
||||
<Link to="/">
|
||||
<Trans>Return to Home</Trans>
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
167
apps/remix/app/routes/_unauthenticated+/team.invite.$token.tsx
Normal file
167
apps/remix/app/routes/_unauthenticated+/team.invite.$token.tsx
Normal file
@ -0,0 +1,167 @@
|
||||
import { Trans } from '@lingui/macro';
|
||||
import { DateTime } from 'luxon';
|
||||
import { Link } from 'react-router';
|
||||
|
||||
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';
|
||||
|
||||
import type { Route } from './+types/team.invite.$token';
|
||||
|
||||
export async function loader({ params, context }: Route.LoaderArgs) {
|
||||
const { token } = params;
|
||||
|
||||
if (!token) {
|
||||
return {
|
||||
state: 'InvalidLink',
|
||||
} as const;
|
||||
}
|
||||
|
||||
const teamMemberInvite = await prisma.teamMemberInvite.findUnique({
|
||||
where: {
|
||||
token,
|
||||
},
|
||||
});
|
||||
|
||||
if (!teamMemberInvite) {
|
||||
return {
|
||||
state: 'InvalidLink',
|
||||
} as const;
|
||||
}
|
||||
|
||||
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 {
|
||||
state: 'LoginRequired',
|
||||
email,
|
||||
teamName: team.name,
|
||||
} as const;
|
||||
}
|
||||
|
||||
const isSessionUserTheInvitedUser = user.id === context.session?.user.id;
|
||||
|
||||
return {
|
||||
state: 'Success',
|
||||
email,
|
||||
teamName: team.name,
|
||||
isSessionUserTheInvitedUser,
|
||||
} as const;
|
||||
}
|
||||
|
||||
export default function AcceptInvitationPage({ loaderData }: Route.ComponentProps) {
|
||||
const data = loaderData;
|
||||
|
||||
if (data.state === 'InvalidLink') {
|
||||
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 to="/">
|
||||
<Trans>Return</Trans>
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (data.state === 'LoginRequired') {
|
||||
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>{data.teamName}</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 to={`/signup?email=${encodeURIComponent(data.email)}`}>
|
||||
<Trans>Create account</Trans>
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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>{data.teamName}</strong> to join their team.
|
||||
</Trans>
|
||||
</p>
|
||||
|
||||
{data.isSessionUserTheInvitedUser ? (
|
||||
<Button asChild>
|
||||
<Link to="/">
|
||||
<Trans>Continue</Trans>
|
||||
</Link>
|
||||
</Button>
|
||||
) : (
|
||||
<Button asChild>
|
||||
<Link to={`/signin?email=${encodeURIComponent(data.email)}`}>
|
||||
<Trans>Continue to login</Trans>
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,176 @@
|
||||
import { Trans } from '@lingui/macro';
|
||||
import { Link } from 'react-router';
|
||||
|
||||
import { isTokenExpired } from '@documenso/lib/utils/token-verification';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
|
||||
import type { Route } from './+types/team.verify.email.$token';
|
||||
|
||||
export async function loader({ params }: Route.LoaderArgs) {
|
||||
const { token } = params;
|
||||
|
||||
if (!token) {
|
||||
return {
|
||||
state: 'InvalidLink',
|
||||
} as const;
|
||||
}
|
||||
|
||||
const teamEmailVerification = await prisma.teamEmailVerification.findUnique({
|
||||
where: {
|
||||
token,
|
||||
},
|
||||
include: {
|
||||
team: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!teamEmailVerification || isTokenExpired(teamEmailVerification.expiresAt)) {
|
||||
return {
|
||||
state: 'InvalidLink',
|
||||
} as const;
|
||||
}
|
||||
|
||||
if (teamEmailVerification.completed) {
|
||||
return {
|
||||
state: 'AlreadyCompleted',
|
||||
teamName: teamEmailVerification.team.name,
|
||||
} as const;
|
||||
}
|
||||
|
||||
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 {
|
||||
state: 'VerificationError',
|
||||
teamName: team.name,
|
||||
} as const;
|
||||
}
|
||||
|
||||
return {
|
||||
state: 'Success',
|
||||
teamName: team.name,
|
||||
} as const;
|
||||
}
|
||||
|
||||
export default function VerifyTeamEmailPage({ loaderData }: Route.ComponentProps) {
|
||||
const data = loaderData;
|
||||
|
||||
if (data.state === 'InvalidLink') {
|
||||
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 to="/">
|
||||
<Trans>Return</Trans>
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (data.state === 'AlreadyCompleted') {
|
||||
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>{data.teamName}</strong>.
|
||||
</Trans>
|
||||
</p>
|
||||
|
||||
<Button asChild>
|
||||
<Link to="/">
|
||||
<Trans>Continue</Trans>
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (data.state === 'VerificationError') {
|
||||
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>{data.teamName}</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>{data.teamName}</strong>.
|
||||
</Trans>
|
||||
</p>
|
||||
|
||||
<Button asChild>
|
||||
<Link to="/">
|
||||
<Trans>Continue</Trans>
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,154 @@
|
||||
import { Trans } from '@lingui/macro';
|
||||
import { Link } from 'react-router';
|
||||
|
||||
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';
|
||||
|
||||
import type { Route } from './+types/team.verify.transfer.token';
|
||||
|
||||
export async function loader({ params }: Route.LoaderArgs) {
|
||||
const { token } = params;
|
||||
|
||||
if (!token) {
|
||||
return {
|
||||
state: 'InvalidLink',
|
||||
} as const;
|
||||
}
|
||||
|
||||
const teamTransferVerification = await prisma.teamTransferVerification.findUnique({
|
||||
where: {
|
||||
token,
|
||||
},
|
||||
include: {
|
||||
team: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!teamTransferVerification || isTokenExpired(teamTransferVerification.expiresAt)) {
|
||||
return {
|
||||
state: 'InvalidLink',
|
||||
} as const;
|
||||
}
|
||||
|
||||
if (teamTransferVerification.completed) {
|
||||
return {
|
||||
state: 'AlreadyCompleted',
|
||||
teamName: teamTransferVerification.team.name,
|
||||
} as const;
|
||||
}
|
||||
|
||||
const { team } = teamTransferVerification;
|
||||
|
||||
let isTransferError = false;
|
||||
|
||||
try {
|
||||
await transferTeamOwnership({ token });
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
isTransferError = true;
|
||||
}
|
||||
|
||||
if (isTransferError) {
|
||||
return {
|
||||
state: 'TransferError',
|
||||
teamName: team.name,
|
||||
} as const;
|
||||
}
|
||||
|
||||
return {
|
||||
state: 'Success',
|
||||
teamName: team.name,
|
||||
teamUrl: team.url,
|
||||
} as const;
|
||||
}
|
||||
|
||||
export default function VerifyTeamTransferPage({ loaderData }: Route.ComponentProps) {
|
||||
const data = loaderData;
|
||||
|
||||
if (data.state === 'InvalidLink') {
|
||||
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 to="/">
|
||||
<Trans>Return</Trans>
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (data.state === 'AlreadyCompleted') {
|
||||
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>{data.teamName}</strong>.
|
||||
</Trans>
|
||||
</p>
|
||||
|
||||
<Button asChild>
|
||||
<Link to="/">
|
||||
<Trans>Continue</Trans>
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (data.state === 'TransferError') {
|
||||
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>{data.teamName}</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>{data.teamName}</strong> has been successfully transferred
|
||||
to you.
|
||||
</Trans>
|
||||
</p>
|
||||
|
||||
<Button asChild>
|
||||
<Link to={`/t/${data.teamUrl}/settings`}>
|
||||
<Trans>Continue</Trans>
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
189
apps/remix/app/routes/_unauthenticated+/verify-email.$token.tsx
Normal file
189
apps/remix/app/routes/_unauthenticated+/verify-email.$token.tsx
Normal file
@ -0,0 +1,189 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { Trans, msg } from '@lingui/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { AlertTriangle, CheckCircle2, Loader, XCircle } from 'lucide-react';
|
||||
import { Link, redirect, useNavigate } from 'react-router';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { authClient } from '@documenso/auth/client';
|
||||
import { EMAIL_VERIFICATION_STATE } from '@documenso/lib/server-only/user/verify-email';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
import type { Route } from './+types/verify-email.$token';
|
||||
|
||||
export const loader = ({ params }: Route.LoaderArgs) => {
|
||||
const { token } = params;
|
||||
|
||||
if (!token) {
|
||||
throw redirect('/verify-email');
|
||||
}
|
||||
|
||||
return {
|
||||
token,
|
||||
};
|
||||
};
|
||||
|
||||
export default function VerifyEmailPage({ loaderData }: Route.ComponentProps) {
|
||||
console.log('hello world');
|
||||
|
||||
const { token } = loaderData;
|
||||
|
||||
const { _ } = useLingui();
|
||||
const { toast } = useToast();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [state, setState] = useState<keyof typeof EMAIL_VERIFICATION_STATE | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const verifyToken = async () => {
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
// Todo: Types and check.
|
||||
const response = await authClient.emailPassword.verifyEmail({
|
||||
token,
|
||||
});
|
||||
|
||||
setState(response.state);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
|
||||
toast({
|
||||
title: _(msg`Something went wrong`),
|
||||
description: _(msg`We were unable to verify your email at this time.`),
|
||||
});
|
||||
|
||||
await navigate('/verify-email');
|
||||
}
|
||||
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
void verifyToken();
|
||||
}, []);
|
||||
|
||||
if (isLoading || state === null) {
|
||||
return (
|
||||
<div className="relative">
|
||||
<Loader className="text-documenso h-8 w-8 animate-spin" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return match(state)
|
||||
.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 to="/">
|
||||
<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 to="/">
|
||||
<Trans>Go back home</Trans>
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
.with(EMAIL_VERIFICATION_STATE.VERIFIED, () => (
|
||||
<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>
|
||||
|
||||
<Button className="mt-4" asChild>
|
||||
<Link to="/">
|
||||
<Trans>Continue</Trans>
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
.with(EMAIL_VERIFICATION_STATE.ALREADY_VERIFIED, () => (
|
||||
<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 already confirmed</Trans>
|
||||
</h2>
|
||||
|
||||
<p className="text-muted-foreground mt-4">
|
||||
<Trans>
|
||||
Your email has already been confirmed. You can now use all features of Documenso.
|
||||
</Trans>
|
||||
</p>
|
||||
|
||||
<Button className="mt-4" asChild>
|
||||
<Link to="/">
|
||||
<Trans>Go back home</Trans>
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
.exhaustive();
|
||||
}
|
||||
40
apps/remix/app/routes/_unauthenticated+/verify-email.tsx
Normal file
40
apps/remix/app/routes/_unauthenticated+/verify-email.tsx
Normal file
@ -0,0 +1,40 @@
|
||||
import { Trans } from '@lingui/macro';
|
||||
import { XCircle } from 'lucide-react';
|
||||
import { Link } from 'react-router';
|
||||
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
|
||||
export function meta() {
|
||||
return [{ title: 'Verify Email' }];
|
||||
}
|
||||
|
||||
export default function EmailVerificationWithoutTokenPage() {
|
||||
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 to="/">
|
||||
<Trans>Go back home</Trans>
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user