fix: wip
This commit is contained in:
@@ -1,56 +0,0 @@
|
|||||||
'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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,130 +0,0 @@
|
|||||||
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();
|
|
||||||
}
|
|
||||||
@@ -1,123 +0,0 @@
|
|||||||
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' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
@@ -8,7 +8,7 @@ import type { TGetTeamsResponse } from '@documenso/lib/server-only/team/get-team
|
|||||||
import { getRootHref } from '@documenso/lib/utils/params';
|
import { getRootHref } from '@documenso/lib/utils/params';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
|
|
||||||
import { Logo } from '~/components/branding/logo';
|
import { BrandingLogo } from '~/components/general/branding-logo';
|
||||||
|
|
||||||
import { CommandMenu } from '../common/command-menu';
|
import { CommandMenu } from '../common/command-menu';
|
||||||
import { DesktopNav } from './desktop-nav';
|
import { DesktopNav } from './desktop-nav';
|
||||||
@@ -62,7 +62,7 @@ export const Header = ({ className, user, teams, ...props }: HeaderProps) => {
|
|||||||
to={`${getRootHref(params, { returnEmptyRootString: true })}/documents`}
|
to={`${getRootHref(params, { returnEmptyRootString: true })}/documents`}
|
||||||
className="focus-visible:ring-ring ring-offset-background hidden rounded-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 md:inline"
|
className="focus-visible:ring-ring ring-offset-background hidden rounded-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 md:inline"
|
||||||
>
|
>
|
||||||
<Logo className="h-6 w-auto" />
|
<BrandingLogo className="h-6 w-auto" />
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<DesktopNav setIsCommandMenuOpen={setIsCommandMenuOpen} />
|
<DesktopNav setIsCommandMenuOpen={setIsCommandMenuOpen} />
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { Trans, msg } from '@lingui/macro';
|
import { Trans, msg } from '@lingui/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
import type { Team } from '@prisma/client';
|
|
||||||
import { useNavigate } from 'react-router';
|
import { useNavigate } from 'react-router';
|
||||||
|
|
||||||
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
|
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import { Trans } from '@lingui/macro';
|
|||||||
|
|
||||||
import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
|
import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
|
||||||
|
|
||||||
import { Logo } from '~/components/branding/logo';
|
|
||||||
import { SignInForm } from '~/components/forms/signin';
|
import { SignInForm } from '~/components/forms/signin';
|
||||||
|
import { BrandingLogo } from '~/components/general/branding-logo';
|
||||||
|
|
||||||
export type EmbedAuthenticationRequiredProps = {
|
export type EmbedAuthenticationRequiredProps = {
|
||||||
email?: string;
|
email?: string;
|
||||||
@@ -17,7 +17,7 @@ export const EmbedAuthenticationRequired = ({
|
|||||||
return (
|
return (
|
||||||
<div className="flex min-h-[100dvh] w-full items-center justify-center">
|
<div className="flex min-h-[100dvh] w-full items-center justify-center">
|
||||||
<div className="flex w-full max-w-md flex-col">
|
<div className="flex w-full max-w-md flex-col">
|
||||||
<Logo className="h-8" />
|
<BrandingLogo className="h-8" />
|
||||||
|
|
||||||
<Alert className="mt-8" variant="warning">
|
<Alert className="mt-8" variant="warning">
|
||||||
<AlertDescription>
|
<AlertDescription>
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
|
|||||||
import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
|
import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
import { Logo } from '~/components/branding/logo';
|
import { BrandingLogo } from '~/components/general/branding-logo';
|
||||||
import { ZDirectTemplateEmbedDataSchema } from '~/types/embed-direct-template-schema';
|
import { ZDirectTemplateEmbedDataSchema } from '~/types/embed-direct-template-schema';
|
||||||
import { injectCss } from '~/utils/css-vars';
|
import { injectCss } from '~/utils/css-vars';
|
||||||
|
|
||||||
@@ -493,7 +493,7 @@ export const EmbedDirectTemplateClientPage = ({
|
|||||||
{!hidePoweredBy && (
|
{!hidePoweredBy && (
|
||||||
<div className="bg-primary text-primary-foreground fixed bottom-0 left-0 z-40 rounded-tr px-2 py-1 text-xs font-medium opacity-60 hover:opacity-100">
|
<div className="bg-primary text-primary-foreground fixed bottom-0 left-0 z-40 rounded-tr px-2 py-1 text-xs font-medium opacity-60 hover:opacity-100">
|
||||||
<span>Powered by</span>
|
<span>Powered by</span>
|
||||||
<Logo className="ml-2 inline-block h-[14px]" />
|
<BrandingLogo className="ml-2 inline-block h-[14px]" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
|
|||||||
import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
|
import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
import { Logo } from '~/components/branding/logo';
|
import { BrandingLogo } from '~/components/general/branding-logo';
|
||||||
import { injectCss } from '~/utils/css-vars';
|
import { injectCss } from '~/utils/css-vars';
|
||||||
|
|
||||||
import { ZSignDocumentEmbedDataSchema } from '../../types/embed-document-sign-schema';
|
import { ZSignDocumentEmbedDataSchema } from '../../types/embed-document-sign-schema';
|
||||||
@@ -367,7 +367,7 @@ export const EmbedSignDocumentClientPage = ({
|
|||||||
{!hidePoweredBy && (
|
{!hidePoweredBy && (
|
||||||
<div className="bg-primary text-primary-foreground fixed bottom-0 left-0 z-40 rounded-tr px-2 py-1 text-xs font-medium opacity-60 hover:opacity-100">
|
<div className="bg-primary text-primary-foreground fixed bottom-0 left-0 z-40 rounded-tr px-2 py-1 text-xs font-medium opacity-60 hover:opacity-100">
|
||||||
<span>Powered by</span>
|
<span>Powered by</span>
|
||||||
<Logo className="ml-2 inline-block h-[14px]" />
|
<BrandingLogo className="ml-2 inline-block h-[14px]" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import type { SVGAttributes } from 'react';
|
|||||||
|
|
||||||
export type LogoProps = SVGAttributes<SVGSVGElement>;
|
export type LogoProps = SVGAttributes<SVGSVGElement>;
|
||||||
|
|
||||||
export const Logo = ({ ...props }: LogoProps) => {
|
export const BrandingLogo = ({ ...props }: LogoProps) => {
|
||||||
return (
|
return (
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2248 320" {...props}>
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2248 320" {...props}>
|
||||||
<path
|
<path
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Outlet, redirect } from 'react-router';
|
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 { LimitsProvider } from '@documenso/ee/server-only/limits/provider/client';
|
||||||
import { getSiteSettings } from '@documenso/lib/server-only/site-settings/get-site-settings';
|
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';
|
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';
|
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 { session } = context;
|
||||||
|
|
||||||
const banner = await getSiteSettings().then((settings) =>
|
|
||||||
settings.find((setting) => setting.id === SITE_SETTINGS_BANNER_ID),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!session) {
|
if (!session) {
|
||||||
throw redirect('/signin');
|
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 {
|
return {
|
||||||
user: session.user,
|
user: session.user,
|
||||||
teams: session.teams,
|
teams: session.teams,
|
||||||
banner,
|
banner,
|
||||||
|
limits,
|
||||||
|
teamId: session.currentTeam?.id,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function Layout({ loaderData }: Route.ComponentProps) {
|
export default function Layout({ loaderData }: Route.ComponentProps) {
|
||||||
const { user, teams, banner } = loaderData;
|
const { user, teams, banner, limits, teamId } = loaderData;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LimitsProvider>
|
<LimitsProvider initialValue={limits} teamId={teamId}>
|
||||||
{!user.emailVerified && <VerifyEmailBanner email={user.email} />}
|
{!user.emailVerified && <VerifyEmailBanner email={user.email} />}
|
||||||
|
|
||||||
{banner && <AppBanner banner={banner} />}
|
{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 { DocumentPageViewInformation } from '~/components/general/document/document-page-view-information';
|
||||||
import { DocumentPageViewRecentActivity } from '~/components/general/document/document-page-view-recent-activity';
|
import { DocumentPageViewRecentActivity } from '~/components/general/document/document-page-view-recent-activity';
|
||||||
import { DocumentPageViewRecipients } from '~/components/general/document/document-page-view-recipients';
|
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';
|
import type { Route } from './+types/$id._index';
|
||||||
|
|
||||||
@@ -119,14 +120,16 @@ export async function loader({ params, context }: Route.LoaderArgs) {
|
|||||||
recipients,
|
recipients,
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return superLoaderJson({
|
||||||
document: documentWithRecipients,
|
document: documentWithRecipients,
|
||||||
documentRootPath,
|
documentRootPath,
|
||||||
fields,
|
fields,
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function DocumentPage({ loaderData }: Route.ComponentProps) {
|
export default function DocumentPage() {
|
||||||
|
const loaderData = useSuperLoaderData<typeof loader>();
|
||||||
|
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
const { user } = useSession();
|
const { user } = useSession();
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { findDocumentAuditLogs } from '@documenso/lib/server-only/document/find-
|
|||||||
import { dynamicActivate } from '@documenso/lib/utils/i18n';
|
import { dynamicActivate } from '@documenso/lib/utils/i18n';
|
||||||
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
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 { InternalAuditLogTable } from '~/components/tables/internal-audit-log-table';
|
||||||
|
|
||||||
import type { Route } from './+types/audit-log';
|
import type { Route } from './+types/audit-log';
|
||||||
@@ -68,7 +68,7 @@ export default function AuditLog({ loaderData }: Route.ComponentProps) {
|
|||||||
const { i18n } = useLingui();
|
const { i18n } = useLingui();
|
||||||
|
|
||||||
// Todo
|
// Todo
|
||||||
void dynamicActivate(i18n, documentLanguage);
|
void dynamicActivate(documentLanguage);
|
||||||
|
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
|
|
||||||
@@ -163,7 +163,7 @@ export default function AuditLog({ loaderData }: Route.ComponentProps) {
|
|||||||
|
|
||||||
<div className="my-8 flex-row-reverse">
|
<div className="my-8 flex-row-reverse">
|
||||||
<div className="flex items-end justify-end gap-x-4">
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import {
|
|||||||
TableRow,
|
TableRow,
|
||||||
} from '@documenso/ui/primitives/table';
|
} from '@documenso/ui/primitives/table';
|
||||||
|
|
||||||
import { Logo } from '~/components/branding/logo';
|
import { BrandingLogo } from '~/components/general/branding-logo';
|
||||||
|
|
||||||
import type { Route } from './+types/certificate';
|
import type { Route } from './+types/certificate';
|
||||||
|
|
||||||
@@ -316,7 +316,7 @@ export default function SigningCertificate({ loaderData }: Route.ComponentProps)
|
|||||||
{_(msg`Signing certificate provided by`)}:
|
{_(msg`Signing certificate provided by`)}:
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<Logo className="max-h-6 print:max-h-4" />
|
<BrandingLogo className="max-h-6 print:max-h-4" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { cn } from '@documenso/ui/lib/utils';
|
|||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
|
|
||||||
import { Header as AuthenticatedHeader } from '~/components/(dashboard)/layout/header';
|
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';
|
import type { Route } from './+types/_layout';
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// Todo: This relies on NextJS
|
||||||
import { ImageResponse } from 'next/og';
|
import { ImageResponse } from 'next/og';
|
||||||
|
|
||||||
import { P, match } from 'ts-pattern';
|
import { P, match } from 'ts-pattern';
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
import Link from 'next/link';
|
|
||||||
|
|
||||||
import { Trans } from '@lingui/macro';
|
import { Trans } from '@lingui/macro';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
|
import { Link } from 'react-router';
|
||||||
|
|
||||||
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 { encryptSecondaryData } from '@documenso/lib/server-only/crypto/encrypt';
|
||||||
import { declineTeamInvitation } from '@documenso/lib/server-only/team/decline-team-invitation';
|
import { declineTeamInvitation } from '@documenso/lib/server-only/team/decline-team-invitation';
|
||||||
import { getTeamById } from '@documenso/lib/server-only/team/get-team';
|
import { getTeamById } from '@documenso/lib/server-only/team/get-team';
|
||||||
@@ -12,18 +9,16 @@ import { prisma } from '@documenso/prisma';
|
|||||||
import { TeamMemberInviteStatus } from '@documenso/prisma/client';
|
import { TeamMemberInviteStatus } from '@documenso/prisma/client';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
|
|
||||||
type DeclineInvitationPageProps = {
|
import type { Route } from './+types/team.decline.$token';
|
||||||
params: {
|
|
||||||
token: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default async function DeclineInvitationPage({
|
export async function loader({ params, context }: Route.LoaderArgs) {
|
||||||
params: { token },
|
const { token } = params;
|
||||||
}: DeclineInvitationPageProps) {
|
|
||||||
await setupI18nSSR();
|
|
||||||
|
|
||||||
const session = await getServerComponentSession();
|
if (!token) {
|
||||||
|
return {
|
||||||
|
state: 'InvalidLink',
|
||||||
|
} as const;
|
||||||
|
}
|
||||||
|
|
||||||
const teamMemberInvite = await prisma.teamMemberInvite.findUnique({
|
const teamMemberInvite = await prisma.teamMemberInvite.findUnique({
|
||||||
where: {
|
where: {
|
||||||
@@ -32,25 +27,9 @@ export default async function DeclineInvitationPage({
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!teamMemberInvite) {
|
if (!teamMemberInvite) {
|
||||||
return (
|
return {
|
||||||
<div className="w-screen max-w-lg px-4">
|
state: 'InvalidLink',
|
||||||
<div className="w-full">
|
} as const;
|
||||||
<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 team = await getTeamById({ teamId: teamMemberInvite.teamId });
|
||||||
@@ -85,6 +64,49 @@ export default async function DeclineInvitationPage({
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!user) {
|
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 (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-4xl font-semibold">
|
<h1 className="text-4xl font-semibold">
|
||||||
@@ -93,7 +115,7 @@ export default async function DeclineInvitationPage({
|
|||||||
|
|
||||||
<p className="text-muted-foreground mt-2 text-sm">
|
<p className="text-muted-foreground mt-2 text-sm">
|
||||||
<Trans>
|
<Trans>
|
||||||
You have been invited by <strong>{team.name}</strong> to join their team.
|
You have been invited by <strong>{data.teamName}</strong> to join their team.
|
||||||
</Trans>
|
</Trans>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@@ -102,7 +124,7 @@ export default async function DeclineInvitationPage({
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<Button asChild>
|
<Button asChild>
|
||||||
<Link href={`/signup?email=${encodeURIComponent(email)}`}>
|
<Link to={`/signup?email=${encodeURIComponent(data.email)}`}>
|
||||||
<Trans>Create account</Trans>
|
<Trans>Create account</Trans>
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
@@ -110,8 +132,6 @@ export default async function DeclineInvitationPage({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const isSessionUserTheInvitedUser = user?.id === session.user?.id;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-screen max-w-lg px-4">
|
<div className="w-screen max-w-lg px-4">
|
||||||
<h1 className="text-4xl font-semibold">
|
<h1 className="text-4xl font-semibold">
|
||||||
@@ -120,19 +140,19 @@ export default async function DeclineInvitationPage({
|
|||||||
|
|
||||||
<p className="text-muted-foreground mb-4 mt-2 text-sm">
|
<p className="text-muted-foreground mb-4 mt-2 text-sm">
|
||||||
<Trans>
|
<Trans>
|
||||||
You have declined the invitation from <strong>{team.name}</strong> to join their team.
|
You have declined the invitation from <strong>{data.teamName}</strong> to join their team.
|
||||||
</Trans>
|
</Trans>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{isSessionUserTheInvitedUser ? (
|
{data.isSessionUserTheInvitedUser ? (
|
||||||
<Button asChild>
|
<Button asChild>
|
||||||
<Link href="/">
|
<Link to="/">
|
||||||
<Trans>Return to Dashboard</Trans>
|
<Trans>Return to Dashboard</Trans>
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<Button asChild>
|
<Button asChild>
|
||||||
<Link href="/">
|
<Link to="/">
|
||||||
<Trans>Return to Home</Trans>
|
<Trans>Return to Home</Trans>
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
@@ -1,10 +1,7 @@
|
|||||||
import Link from 'next/link';
|
|
||||||
|
|
||||||
import { Trans } from '@lingui/macro';
|
import { Trans } from '@lingui/macro';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
|
import { Link } from 'react-router';
|
||||||
|
|
||||||
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 { encryptSecondaryData } from '@documenso/lib/server-only/crypto/encrypt';
|
||||||
import { acceptTeamInvitation } from '@documenso/lib/server-only/team/accept-team-invitation';
|
import { acceptTeamInvitation } from '@documenso/lib/server-only/team/accept-team-invitation';
|
||||||
import { getTeamById } from '@documenso/lib/server-only/team/get-team';
|
import { getTeamById } from '@documenso/lib/server-only/team/get-team';
|
||||||
@@ -12,18 +9,16 @@ import { prisma } from '@documenso/prisma';
|
|||||||
import { TeamMemberInviteStatus } from '@documenso/prisma/client';
|
import { TeamMemberInviteStatus } from '@documenso/prisma/client';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
|
|
||||||
type AcceptInvitationPageProps = {
|
import type { Route } from './+types/team.invite.$token';
|
||||||
params: {
|
|
||||||
token: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default async function AcceptInvitationPage({
|
export async function loader({ params, context }: Route.LoaderArgs) {
|
||||||
params: { token },
|
const { token } = params;
|
||||||
}: AcceptInvitationPageProps) {
|
|
||||||
await setupI18nSSR();
|
|
||||||
|
|
||||||
const session = await getServerComponentSession();
|
if (!token) {
|
||||||
|
return {
|
||||||
|
state: 'InvalidLink',
|
||||||
|
} as const;
|
||||||
|
}
|
||||||
|
|
||||||
const teamMemberInvite = await prisma.teamMemberInvite.findUnique({
|
const teamMemberInvite = await prisma.teamMemberInvite.findUnique({
|
||||||
where: {
|
where: {
|
||||||
@@ -32,27 +27,9 @@ export default async function AcceptInvitationPage({
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!teamMemberInvite) {
|
if (!teamMemberInvite) {
|
||||||
return (
|
return {
|
||||||
<div className="w-screen max-w-lg px-4">
|
state: 'InvalidLink',
|
||||||
<div className="w-full">
|
} as const;
|
||||||
<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 team = await getTeamById({ teamId: teamMemberInvite.teamId });
|
||||||
@@ -90,6 +67,51 @@ export default async function AcceptInvitationPage({
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!user) {
|
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 (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-4xl font-semibold">
|
<h1 className="text-4xl font-semibold">
|
||||||
@@ -98,7 +120,7 @@ export default async function AcceptInvitationPage({
|
|||||||
|
|
||||||
<p className="text-muted-foreground mt-2 text-sm">
|
<p className="text-muted-foreground mt-2 text-sm">
|
||||||
<Trans>
|
<Trans>
|
||||||
You have been invited by <strong>{team.name}</strong> to join their team.
|
You have been invited by <strong>{data.teamName}</strong> to join their team.
|
||||||
</Trans>
|
</Trans>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@@ -107,7 +129,7 @@ export default async function AcceptInvitationPage({
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<Button asChild>
|
<Button asChild>
|
||||||
<Link href={`/signup?email=${encodeURIComponent(email)}`}>
|
<Link to={`/signup?email=${encodeURIComponent(data.email)}`}>
|
||||||
<Trans>Create account</Trans>
|
<Trans>Create account</Trans>
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
@@ -115,8 +137,6 @@ export default async function AcceptInvitationPage({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const isSessionUserTheInvitedUser = user.id === session.user?.id;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-4xl font-semibold">
|
<h1 className="text-4xl font-semibold">
|
||||||
@@ -125,19 +145,19 @@ export default async function AcceptInvitationPage({
|
|||||||
|
|
||||||
<p className="text-muted-foreground mb-4 mt-2 text-sm">
|
<p className="text-muted-foreground mb-4 mt-2 text-sm">
|
||||||
<Trans>
|
<Trans>
|
||||||
You have accepted an invitation from <strong>{team.name}</strong> to join their team.
|
You have accepted an invitation from <strong>{data.teamName}</strong> to join their team.
|
||||||
</Trans>
|
</Trans>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{isSessionUserTheInvitedUser ? (
|
{data.isSessionUserTheInvitedUser ? (
|
||||||
<Button asChild>
|
<Button asChild>
|
||||||
<Link href="/">
|
<Link to="/">
|
||||||
<Trans>Continue</Trans>
|
<Trans>Continue</Trans>
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<Button asChild>
|
<Button asChild>
|
||||||
<Link href={`/signin?email=${encodeURIComponent(email)}`}>
|
<Link to={`/signin?email=${encodeURIComponent(data.email)}`}>
|
||||||
<Trans>Continue to login</Trans>
|
<Trans>Continue to login</Trans>
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
@@ -1,20 +1,20 @@
|
|||||||
import Link from 'next/link';
|
|
||||||
|
|
||||||
import { Trans } from '@lingui/macro';
|
import { Trans } from '@lingui/macro';
|
||||||
|
import { Link } from 'react-router';
|
||||||
|
|
||||||
import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
|
|
||||||
import { isTokenExpired } from '@documenso/lib/utils/token-verification';
|
import { isTokenExpired } from '@documenso/lib/utils/token-verification';
|
||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
|
|
||||||
type VerifyTeamEmailPageProps = {
|
import type { Route } from './+types/team.verify.email.$token';
|
||||||
params: {
|
|
||||||
token: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default async function VerifyTeamEmailPage({ params: { token } }: VerifyTeamEmailPageProps) {
|
export async function loader({ params }: Route.LoaderArgs) {
|
||||||
await setupI18nSSR();
|
const { token } = params;
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
return {
|
||||||
|
state: 'InvalidLink',
|
||||||
|
} as const;
|
||||||
|
}
|
||||||
|
|
||||||
const teamEmailVerification = await prisma.teamEmailVerification.findUnique({
|
const teamEmailVerification = await prisma.teamEmailVerification.findUnique({
|
||||||
where: {
|
where: {
|
||||||
@@ -26,51 +26,16 @@ export default async function VerifyTeamEmailPage({ params: { token } }: VerifyT
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!teamEmailVerification || isTokenExpired(teamEmailVerification.expiresAt)) {
|
if (!teamEmailVerification || isTokenExpired(teamEmailVerification.expiresAt)) {
|
||||||
return (
|
return {
|
||||||
<div className="w-screen max-w-lg px-4">
|
state: 'InvalidLink',
|
||||||
<div className="w-full">
|
} as const;
|
||||||
<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) {
|
if (teamEmailVerification.completed) {
|
||||||
return (
|
return {
|
||||||
<div>
|
state: 'AlreadyCompleted',
|
||||||
<h1 className="text-4xl font-semibold">
|
teamName: teamEmailVerification.team.name,
|
||||||
<Trans>Team email already verified!</Trans>
|
} as const;
|
||||||
</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;
|
const { team } = teamEmailVerification;
|
||||||
@@ -110,6 +75,69 @@ export default async function VerifyTeamEmailPage({ params: { token } }: VerifyT
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isTeamEmailVerificationError) {
|
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 (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-4xl font-semibold">
|
<h1 className="text-4xl font-semibold">
|
||||||
@@ -119,7 +147,7 @@ export default async function VerifyTeamEmailPage({ params: { token } }: VerifyT
|
|||||||
<p className="text-muted-foreground mt-2 text-sm">
|
<p className="text-muted-foreground mt-2 text-sm">
|
||||||
<Trans>
|
<Trans>
|
||||||
Something went wrong while attempting to verify your email address for{' '}
|
Something went wrong while attempting to verify your email address for{' '}
|
||||||
<strong>{team.name}</strong>. Please try again later.
|
<strong>{data.teamName}</strong>. Please try again later.
|
||||||
</Trans>
|
</Trans>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -134,12 +162,12 @@ export default async function VerifyTeamEmailPage({ params: { token } }: VerifyT
|
|||||||
|
|
||||||
<p className="text-muted-foreground mb-4 mt-2 text-sm">
|
<p className="text-muted-foreground mb-4 mt-2 text-sm">
|
||||||
<Trans>
|
<Trans>
|
||||||
You have verified your email address for <strong>{team.name}</strong>.
|
You have verified your email address for <strong>{data.teamName}</strong>.
|
||||||
</Trans>
|
</Trans>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<Button asChild>
|
<Button asChild>
|
||||||
<Link href="/">
|
<Link to="/">
|
||||||
<Trans>Continue</Trans>
|
<Trans>Continue</Trans>
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
@@ -1,23 +1,21 @@
|
|||||||
import Link from 'next/link';
|
|
||||||
|
|
||||||
import { Trans } from '@lingui/macro';
|
import { Trans } from '@lingui/macro';
|
||||||
|
import { Link } from 'react-router';
|
||||||
|
|
||||||
import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
|
|
||||||
import { transferTeamOwnership } from '@documenso/lib/server-only/team/transfer-team-ownership';
|
import { transferTeamOwnership } from '@documenso/lib/server-only/team/transfer-team-ownership';
|
||||||
import { isTokenExpired } from '@documenso/lib/utils/token-verification';
|
import { isTokenExpired } from '@documenso/lib/utils/token-verification';
|
||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
|
|
||||||
type VerifyTeamTransferPage = {
|
import type { Route } from './+types/team.verify.transfer.token';
|
||||||
params: {
|
|
||||||
token: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default async function VerifyTeamTransferPage({
|
export async function loader({ params }: Route.LoaderArgs) {
|
||||||
params: { token },
|
const { token } = params;
|
||||||
}: VerifyTeamTransferPage) {
|
|
||||||
await setupI18nSSR();
|
if (!token) {
|
||||||
|
return {
|
||||||
|
state: 'InvalidLink',
|
||||||
|
} as const;
|
||||||
|
}
|
||||||
|
|
||||||
const teamTransferVerification = await prisma.teamTransferVerification.findUnique({
|
const teamTransferVerification = await prisma.teamTransferVerification.findUnique({
|
||||||
where: {
|
where: {
|
||||||
@@ -29,6 +27,47 @@ export default async function VerifyTeamTransferPage({
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!teamTransferVerification || isTokenExpired(teamTransferVerification.expiresAt)) {
|
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 (
|
return (
|
||||||
<div className="w-screen max-w-lg px-4">
|
<div className="w-screen max-w-lg px-4">
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
@@ -44,7 +83,7 @@ export default async function VerifyTeamTransferPage({
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<Button asChild>
|
<Button asChild>
|
||||||
<Link href="/">
|
<Link to="/">
|
||||||
<Trans>Return</Trans>
|
<Trans>Return</Trans>
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
@@ -53,7 +92,7 @@ export default async function VerifyTeamTransferPage({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (teamTransferVerification.completed) {
|
if (data.state === 'AlreadyCompleted') {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-4xl font-semibold">
|
<h1 className="text-4xl font-semibold">
|
||||||
@@ -62,13 +101,12 @@ export default async function VerifyTeamTransferPage({
|
|||||||
|
|
||||||
<p className="text-muted-foreground mb-4 mt-2 text-sm">
|
<p className="text-muted-foreground mb-4 mt-2 text-sm">
|
||||||
<Trans>
|
<Trans>
|
||||||
You have already completed the ownership transfer for{' '}
|
You have already completed the ownership transfer for <strong>{data.teamName}</strong>.
|
||||||
<strong>{teamTransferVerification.team.name}</strong>.
|
|
||||||
</Trans>
|
</Trans>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<Button asChild>
|
<Button asChild>
|
||||||
<Link href="/">
|
<Link to="/">
|
||||||
<Trans>Continue</Trans>
|
<Trans>Continue</Trans>
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
@@ -76,18 +114,7 @@ export default async function VerifyTeamTransferPage({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { team } = teamTransferVerification;
|
if (data.state === 'TransferError') {
|
||||||
|
|
||||||
let isTransferError = false;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await transferTeamOwnership({ token });
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
isTransferError = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isTransferError) {
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-4xl font-semibold">
|
<h1 className="text-4xl font-semibold">
|
||||||
@@ -97,7 +124,7 @@ export default async function VerifyTeamTransferPage({
|
|||||||
<p className="text-muted-foreground mt-2 text-sm">
|
<p className="text-muted-foreground mt-2 text-sm">
|
||||||
<Trans>
|
<Trans>
|
||||||
Something went wrong while attempting to transfer the ownership of team{' '}
|
Something went wrong while attempting to transfer the ownership of team{' '}
|
||||||
<strong>{team.name}</strong> to your. Please try again later or contact support.
|
<strong>{data.teamName}</strong> to your. Please try again later or contact support.
|
||||||
</Trans>
|
</Trans>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -112,13 +139,13 @@ export default async function VerifyTeamTransferPage({
|
|||||||
|
|
||||||
<p className="text-muted-foreground mb-4 mt-2 text-sm">
|
<p className="text-muted-foreground mb-4 mt-2 text-sm">
|
||||||
<Trans>
|
<Trans>
|
||||||
The ownership of team <strong>{team.name}</strong> has been successfully transferred to
|
The ownership of team <strong>{data.teamName}</strong> has been successfully transferred
|
||||||
you.
|
to you.
|
||||||
</Trans>
|
</Trans>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<Button asChild>
|
<Button asChild>
|
||||||
<Link href={`/t/${team.url}/settings`}>
|
<Link to={`/t/${data.teamUrl}/settings`}>
|
||||||
<Trans>Continue</Trans>
|
<Trans>Continue</Trans>
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
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();
|
||||||
|
}
|
||||||
@@ -1,19 +1,14 @@
|
|||||||
import type { Metadata } from 'next';
|
|
||||||
import Link from 'next/link';
|
|
||||||
|
|
||||||
import { Trans } from '@lingui/macro';
|
import { Trans } from '@lingui/macro';
|
||||||
import { XCircle } from 'lucide-react';
|
import { XCircle } from 'lucide-react';
|
||||||
|
import { Link } from 'react-router';
|
||||||
|
|
||||||
import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
|
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export function meta() {
|
||||||
title: 'Verify Email',
|
return [{ title: 'Verify Email' }];
|
||||||
};
|
}
|
||||||
|
|
||||||
export default async function EmailVerificationWithoutTokenPage() {
|
|
||||||
await setupI18nSSR();
|
|
||||||
|
|
||||||
|
export default function EmailVerificationWithoutTokenPage() {
|
||||||
return (
|
return (
|
||||||
<div className="w-screen max-w-lg px-4">
|
<div className="w-screen max-w-lg px-4">
|
||||||
<div className="flex w-full items-start">
|
<div className="flex w-full items-start">
|
||||||
@@ -34,7 +29,7 @@ export default async function EmailVerificationWithoutTokenPage() {
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<Button className="mt-4" asChild>
|
<Button className="mt-4" asChild>
|
||||||
<Link href="/">
|
<Link to="/">
|
||||||
<Trans>Go back home</Trans>
|
<Trans>Go back home</Trans>
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
@@ -22,7 +22,7 @@ app.route('/api/auth', auth);
|
|||||||
|
|
||||||
// API servers. Todo: Configure max durations, etc?
|
// API servers. Todo: Configure max durations, etc?
|
||||||
app.route('/api/v1', tsRestHonoApp);
|
app.route('/api/v1', tsRestHonoApp);
|
||||||
app.use('/api/jobs/*', jobsClient.getHonoApiHandler());
|
app.use('/api/jobs/*', jobsClient.getApiHandler());
|
||||||
app.use('/api/trpc/*', reactRouterTrpcServer);
|
app.use('/api/trpc/*', reactRouterTrpcServer);
|
||||||
|
|
||||||
// Unstable API server routes. Order matters for these two.
|
// Unstable API server routes. Order matters for these two.
|
||||||
|
|||||||
@@ -73,9 +73,6 @@ export async function getLoadContext(args: GetLoadContextArgs) {
|
|||||||
* - /favicon.* (Favicon files)
|
* - /favicon.* (Favicon files)
|
||||||
* - *.webmanifest (Web manifest files)
|
* - *.webmanifest (Web manifest files)
|
||||||
* - Paths starting with . (e.g. .well-known)
|
* - Paths starting with . (e.g. .well-known)
|
||||||
*
|
|
||||||
* The regex pattern (?!pattern) is a negative lookahead that ensures the path does NOT match any of these patterns.
|
|
||||||
* The .* at the end matches any remaining characters in the path.
|
|
||||||
*/
|
*/
|
||||||
const config = {
|
const config = {
|
||||||
matcher: new RegExp(
|
matcher: new RegExp(
|
||||||
|
|||||||
@@ -15,4 +15,4 @@ server.use(
|
|||||||
|
|
||||||
const handler = handle(build, server, { getLoadContext });
|
const handler = handle(build, server, { getLoadContext });
|
||||||
|
|
||||||
serve({ fetch: handler.fetch, port: 3010 });
|
serve({ fetch: handler.fetch, port: 3000 });
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
'use server';
|
|
||||||
|
|
||||||
import { headers } from 'next/headers';
|
|
||||||
|
|
||||||
import { getLimits } from '../client';
|
|
||||||
import { LimitsProvider as ClientLimitsProvider } from './client';
|
|
||||||
|
|
||||||
export type LimitsProviderProps = {
|
|
||||||
children?: React.ReactNode;
|
|
||||||
teamId?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const LimitsProvider = async ({ children, teamId }: LimitsProviderProps) => {
|
|
||||||
const requestHeaders = Object.fromEntries(headers().entries());
|
|
||||||
|
|
||||||
const limits = await getLimits({ headers: requestHeaders, teamId });
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ClientLimitsProvider initialValue={limits} teamId={teamId}>
|
|
||||||
{children}
|
|
||||||
</ClientLimitsProvider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
|
||||||
|
|
||||||
import type { Context as HonoContext } from 'hono';
|
import type { Context as HonoContext } from 'hono';
|
||||||
|
|
||||||
import type { JobDefinition, SimpleTriggerJobOptions } from './_internal/job';
|
import type { JobDefinition, SimpleTriggerJobOptions } from './_internal/job';
|
||||||
@@ -15,11 +13,7 @@ export abstract class BaseJobProvider {
|
|||||||
throw new Error('Not implemented');
|
throw new Error('Not implemented');
|
||||||
}
|
}
|
||||||
|
|
||||||
public getApiHandler(): (req: NextApiRequest, res: NextApiResponse) => Promise<Response | void> {
|
public getApiHandler(): (req: HonoContext) => Promise<Response | void> {
|
||||||
throw new Error('Not implemented');
|
|
||||||
}
|
|
||||||
|
|
||||||
public getHonoApiHandler(): (req: HonoContext) => Promise<Response | void> {
|
|
||||||
throw new Error('Not implemented');
|
throw new Error('Not implemented');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import type { JobDefinition, TriggerJobOptions } from './_internal/job';
|
|||||||
import type { BaseJobProvider as JobClientProvider } from './base';
|
import type { BaseJobProvider as JobClientProvider } from './base';
|
||||||
import { InngestJobProvider } from './inngest';
|
import { InngestJobProvider } from './inngest';
|
||||||
import { LocalJobProvider } from './local';
|
import { LocalJobProvider } from './local';
|
||||||
import { TriggerJobProvider } from './trigger';
|
|
||||||
|
|
||||||
export class JobClient<T extends ReadonlyArray<JobDefinition> = []> {
|
export class JobClient<T extends ReadonlyArray<JobDefinition> = []> {
|
||||||
private _provider: JobClientProvider;
|
private _provider: JobClientProvider;
|
||||||
@@ -13,7 +12,6 @@ export class JobClient<T extends ReadonlyArray<JobDefinition> = []> {
|
|||||||
public constructor(definitions: T) {
|
public constructor(definitions: T) {
|
||||||
this._provider = match(env('NEXT_PRIVATE_JOBS_PROVIDER'))
|
this._provider = match(env('NEXT_PRIVATE_JOBS_PROVIDER'))
|
||||||
.with('inngest', () => InngestJobProvider.getInstance())
|
.with('inngest', () => InngestJobProvider.getInstance())
|
||||||
.with('trigger', () => TriggerJobProvider.getInstance())
|
|
||||||
.otherwise(() => LocalJobProvider.getInstance());
|
.otherwise(() => LocalJobProvider.getInstance());
|
||||||
|
|
||||||
definitions.forEach((definition) => {
|
definitions.forEach((definition) => {
|
||||||
@@ -28,8 +26,4 @@ export class JobClient<T extends ReadonlyArray<JobDefinition> = []> {
|
|||||||
public getApiHandler() {
|
public getApiHandler() {
|
||||||
return this._provider.getApiHandler();
|
return this._provider.getApiHandler();
|
||||||
}
|
}
|
||||||
|
|
||||||
public getHonoApiHandler() {
|
|
||||||
return this._provider.getHonoApiHandler();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,8 @@
|
|||||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
|
||||||
import type { NextRequest } from 'next/server';
|
|
||||||
|
|
||||||
import type { Context as HonoContext } from 'hono';
|
import type { Context as HonoContext } from 'hono';
|
||||||
import type { Context, Handler, InngestFunction } from 'inngest';
|
import type { Context, Handler, InngestFunction } from 'inngest';
|
||||||
import { Inngest as InngestClient } from 'inngest';
|
import { Inngest as InngestClient } from 'inngest';
|
||||||
import { serve as createHonoPagesRoute } from 'inngest/hono';
|
import { serve as createHonoPagesRoute } from 'inngest/hono';
|
||||||
import type { Logger } from 'inngest/middleware/logger';
|
import type { Logger } from 'inngest/middleware/logger';
|
||||||
import { serve as createPagesRoute } from 'inngest/next';
|
|
||||||
import { json } from 'micro';
|
|
||||||
|
|
||||||
import { env } from '../../utils/env';
|
import { env } from '../../utils/env';
|
||||||
import type { JobDefinition, JobRunIO, SimpleTriggerJobOptions } from './_internal/job';
|
import type { JobDefinition, JobRunIO, SimpleTriggerJobOptions } from './_internal/job';
|
||||||
@@ -76,29 +71,29 @@ export class InngestJobProvider extends BaseJobProvider {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public getApiHandler() {
|
// public getApiHandler() {
|
||||||
const handler = createPagesRoute({
|
// const handler = createPagesRoute({
|
||||||
client: this._client,
|
// client: this._client,
|
||||||
functions: this._functions,
|
// functions: this._functions,
|
||||||
});
|
// });
|
||||||
|
|
||||||
return async (req: NextApiRequest, res: NextApiResponse) => {
|
// return async (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
// Since body-parser is disabled for this route we need to patch in the parsed body
|
// // Since body-parser is disabled for this route we need to patch in the parsed body
|
||||||
if (req.headers['content-type'] === 'application/json') {
|
// if (req.headers['content-type'] === 'application/json') {
|
||||||
Object.assign(req, {
|
// Object.assign(req, {
|
||||||
body: await json(req),
|
// body: await json(req),
|
||||||
});
|
// });
|
||||||
}
|
// }
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
// // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||||
const nextReq = req as unknown as NextRequest;
|
// const nextReq = req as unknown as NextRequest;
|
||||||
|
|
||||||
return await handler(nextReq, res);
|
// return await handler(nextReq, res);
|
||||||
};
|
// };
|
||||||
}
|
// }
|
||||||
|
|
||||||
// Todo: Do we need to handle the above?
|
// Todo: Do we need to handle the above?
|
||||||
public getHonoApiHandler() {
|
public getApiHandler() {
|
||||||
return async (context: HonoContext) => {
|
return async (context: HonoContext) => {
|
||||||
const handler = createHonoPagesRoute({
|
const handler = createHonoPagesRoute({
|
||||||
client: this._client,
|
client: this._client,
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
|
||||||
|
|
||||||
import { sha256 } from '@noble/hashes/sha256';
|
import { sha256 } from '@noble/hashes/sha256';
|
||||||
import { BackgroundJobStatus, Prisma } from '@prisma/client';
|
import { BackgroundJobStatus, Prisma } from '@prisma/client';
|
||||||
import type { Context as HonoContext } from 'hono';
|
import type { Context as HonoContext } from 'hono';
|
||||||
import { json } from 'micro';
|
|
||||||
|
|
||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
|
|
||||||
@@ -71,150 +68,7 @@ export class LocalJobProvider extends BaseJobProvider {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public getApiHandler() {
|
public getApiHandler(): (context: HonoContext) => Promise<Response | void> {
|
||||||
return async (req: NextApiRequest, res: NextApiResponse) => {
|
|
||||||
if (req.method !== 'POST') {
|
|
||||||
res.status(405).send('Method not allowed');
|
|
||||||
}
|
|
||||||
|
|
||||||
const jobId = req.headers['x-job-id'];
|
|
||||||
const signature = req.headers['x-job-signature'];
|
|
||||||
const isRetry = req.headers['x-job-retry'] !== undefined;
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
||||||
const options = await json(req)
|
|
||||||
.then(async (data) => ZSimpleTriggerJobOptionsSchema.parseAsync(data))
|
|
||||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
||||||
.then((data) => data as SimpleTriggerJobOptions)
|
|
||||||
.catch(() => null);
|
|
||||||
|
|
||||||
if (!options) {
|
|
||||||
res.status(400).send('Bad request');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const definition = this._jobDefinitions[options.name];
|
|
||||||
|
|
||||||
if (
|
|
||||||
typeof jobId !== 'string' ||
|
|
||||||
typeof signature !== 'string' ||
|
|
||||||
typeof options !== 'object'
|
|
||||||
) {
|
|
||||||
res.status(400).send('Bad request');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!definition) {
|
|
||||||
res.status(404).send('Job not found');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (definition && !definition.enabled) {
|
|
||||||
console.log('Attempted to trigger a disabled job', options.name);
|
|
||||||
|
|
||||||
res.status(404).send('Job not found');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!signature || !verify(options, signature)) {
|
|
||||||
res.status(401).send('Unauthorized');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (definition.trigger.schema) {
|
|
||||||
const result = definition.trigger.schema.safeParse(options.payload);
|
|
||||||
|
|
||||||
if (!result.success) {
|
|
||||||
res.status(400).send('Bad request');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`[JOBS]: Triggering job ${options.name} with payload`, options.payload);
|
|
||||||
|
|
||||||
let backgroundJob = await prisma.backgroundJob
|
|
||||||
.update({
|
|
||||||
where: {
|
|
||||||
id: jobId,
|
|
||||||
status: BackgroundJobStatus.PENDING,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
status: BackgroundJobStatus.PROCESSING,
|
|
||||||
retried: {
|
|
||||||
increment: isRetry ? 1 : 0,
|
|
||||||
},
|
|
||||||
lastRetriedAt: isRetry ? new Date() : undefined,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.catch(() => null);
|
|
||||||
|
|
||||||
if (!backgroundJob) {
|
|
||||||
res.status(404).send('Job not found');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await definition.handler({
|
|
||||||
payload: options.payload,
|
|
||||||
io: this.createJobRunIO(jobId),
|
|
||||||
});
|
|
||||||
|
|
||||||
backgroundJob = await prisma.backgroundJob.update({
|
|
||||||
where: {
|
|
||||||
id: jobId,
|
|
||||||
status: BackgroundJobStatus.PROCESSING,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
status: BackgroundJobStatus.COMPLETED,
|
|
||||||
completedAt: new Date(),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.log(`[JOBS]: Job ${options.name} failed`, error);
|
|
||||||
|
|
||||||
const taskHasExceededRetries = error instanceof BackgroundTaskExceededRetriesError;
|
|
||||||
const jobHasExceededRetries =
|
|
||||||
backgroundJob.retried >= backgroundJob.maxRetries &&
|
|
||||||
!(error instanceof BackgroundTaskFailedError);
|
|
||||||
|
|
||||||
if (taskHasExceededRetries || jobHasExceededRetries) {
|
|
||||||
backgroundJob = await prisma.backgroundJob.update({
|
|
||||||
where: {
|
|
||||||
id: jobId,
|
|
||||||
status: BackgroundJobStatus.PROCESSING,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
status: BackgroundJobStatus.FAILED,
|
|
||||||
completedAt: new Date(),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
res.status(500).send('Task exceeded retries');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
backgroundJob = await prisma.backgroundJob.update({
|
|
||||||
where: {
|
|
||||||
id: jobId,
|
|
||||||
status: BackgroundJobStatus.PROCESSING,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
status: BackgroundJobStatus.PENDING,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await this.submitJobToEndpoint({
|
|
||||||
jobId,
|
|
||||||
jobDefinitionId: backgroundJob.jobId,
|
|
||||||
data: options,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
res.status(200).send('OK');
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public getHonoApiHandler(): (context: HonoContext) => Promise<Response | void> {
|
|
||||||
return async (context: HonoContext) => {
|
return async (context: HonoContext) => {
|
||||||
const req = context.req;
|
const req = context.req;
|
||||||
|
|
||||||
|
|||||||
@@ -1,79 +0,0 @@
|
|||||||
import { createPagesRoute } from '@trigger.dev/nextjs';
|
|
||||||
import type { IO } from '@trigger.dev/sdk';
|
|
||||||
import { TriggerClient, eventTrigger } from '@trigger.dev/sdk';
|
|
||||||
|
|
||||||
import { env } from '../../utils/env';
|
|
||||||
import type { JobDefinition, JobRunIO, SimpleTriggerJobOptions } from './_internal/job';
|
|
||||||
import { BaseJobProvider } from './base';
|
|
||||||
|
|
||||||
export class TriggerJobProvider extends BaseJobProvider {
|
|
||||||
private static _instance: TriggerJobProvider;
|
|
||||||
|
|
||||||
private _client: TriggerClient;
|
|
||||||
|
|
||||||
private constructor(options: { client: TriggerClient }) {
|
|
||||||
super();
|
|
||||||
|
|
||||||
this._client = options.client;
|
|
||||||
}
|
|
||||||
|
|
||||||
static getInstance() {
|
|
||||||
if (!this._instance) {
|
|
||||||
const client = new TriggerClient({
|
|
||||||
id: 'documenso-app',
|
|
||||||
apiKey: env('NEXT_PRIVATE_TRIGGER_API_KEY'),
|
|
||||||
apiUrl: env('NEXT_PRIVATE_TRIGGER_API_URL'),
|
|
||||||
});
|
|
||||||
|
|
||||||
this._instance = new TriggerJobProvider({ client });
|
|
||||||
}
|
|
||||||
|
|
||||||
return this._instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
public defineJob<N extends string, T>(job: JobDefinition<N, T>): void {
|
|
||||||
this._client.defineJob({
|
|
||||||
id: job.id,
|
|
||||||
name: job.name,
|
|
||||||
version: job.version,
|
|
||||||
trigger: eventTrigger({
|
|
||||||
name: job.trigger.name,
|
|
||||||
schema: job.trigger.schema,
|
|
||||||
}),
|
|
||||||
run: async (payload, io) => job.handler({ payload, io: this.convertTriggerIoToJobRunIo(io) }),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public async triggerJob(options: SimpleTriggerJobOptions): Promise<void> {
|
|
||||||
await this._client.sendEvent({
|
|
||||||
id: options.id,
|
|
||||||
name: options.name,
|
|
||||||
payload: options.payload,
|
|
||||||
timestamp: options.timestamp ? new Date(options.timestamp) : undefined,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public getApiHandler() {
|
|
||||||
const { handler } = createPagesRoute(this._client);
|
|
||||||
|
|
||||||
return handler;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hono v2 is being deprecated so not sure if we will be required.
|
|
||||||
// public getHonoApiHandler(): (req: HonoContext) => Promise<Response | void> {
|
|
||||||
// throw new Error('Not implemented');
|
|
||||||
// }
|
|
||||||
|
|
||||||
private convertTriggerIoToJobRunIo(io: IO) {
|
|
||||||
return {
|
|
||||||
wait: io.wait,
|
|
||||||
logger: io.logger,
|
|
||||||
runTask: async (cacheKey, callback) => io.runTask(cacheKey, callback),
|
|
||||||
triggerJob: async (cacheKey, payload) =>
|
|
||||||
io.sendEvent(cacheKey, {
|
|
||||||
...payload,
|
|
||||||
timestamp: payload.timestamp ? new Date(payload.timestamp) : undefined,
|
|
||||||
}),
|
|
||||||
} satisfies JobRunIO;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import { NextRequest } from 'next/server';
|
|
||||||
|
|
||||||
export const toNextRequest = (req: Request) => {
|
|
||||||
const headers = Object.fromEntries(req.headers.entries());
|
|
||||||
|
|
||||||
return new NextRequest(req, {
|
|
||||||
headers: headers,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
import { NextApiResponse } from 'next';
|
|
||||||
import { NextResponse } from 'next/server';
|
|
||||||
|
|
||||||
type NarrowedResponse<T> = T extends NextResponse
|
|
||||||
? NextResponse
|
|
||||||
: T extends NextApiResponse<infer U>
|
|
||||||
? NextApiResponse<U>
|
|
||||||
: never;
|
|
||||||
|
|
||||||
export const withStaleWhileRevalidate = <T>(
|
|
||||||
res: NarrowedResponse<T>,
|
|
||||||
cacheInSeconds = 60,
|
|
||||||
staleCacheInSeconds = 300,
|
|
||||||
) => {
|
|
||||||
if ('headers' in res) {
|
|
||||||
res.headers.set(
|
|
||||||
'Cache-Control',
|
|
||||||
`public, s-maxage=${cacheInSeconds}, stale-while-revalidate=${staleCacheInSeconds}`,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
res.setHeader(
|
|
||||||
'Cache-Control',
|
|
||||||
`public, s-maxage=${cacheInSeconds}, stale-while-revalidate=${staleCacheInSeconds}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return res;
|
|
||||||
};
|
|
||||||
@@ -1,6 +1,3 @@
|
|||||||
import type { NextApiRequest } from 'next';
|
|
||||||
|
|
||||||
import type { RequestInternal } from 'next-auth';
|
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
const ZIpSchema = z.string().ip();
|
const ZIpSchema = z.string().ip();
|
||||||
@@ -53,35 +50,3 @@ export const extractRequestMetadata = (req: Request): RequestMetadata => {
|
|||||||
userAgent: userAgent ?? undefined,
|
userAgent: userAgent ?? undefined,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const extractNextApiRequestMetadata = (req: NextApiRequest): RequestMetadata => {
|
|
||||||
const parsedIp = ZIpSchema.safeParse(req.headers['x-forwarded-for'] || req.socket.remoteAddress);
|
|
||||||
|
|
||||||
const ipAddress = parsedIp.success ? parsedIp.data : undefined;
|
|
||||||
const userAgent = req.headers['user-agent'];
|
|
||||||
|
|
||||||
return {
|
|
||||||
ipAddress,
|
|
||||||
userAgent,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const extractNextAuthRequestMetadata = (
|
|
||||||
req: Pick<RequestInternal, 'body' | 'query' | 'headers' | 'method'>,
|
|
||||||
): RequestMetadata => {
|
|
||||||
return extractNextHeaderRequestMetadata(req.headers ?? {});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const extractNextHeaderRequestMetadata = (
|
|
||||||
headers: Record<string, string>,
|
|
||||||
): RequestMetadata => {
|
|
||||||
const parsedIp = ZIpSchema.safeParse(headers?.['x-forwarded-for']);
|
|
||||||
|
|
||||||
const ipAddress = parsedIp.success ? parsedIp.data : undefined;
|
|
||||||
const userAgent = headers?.['user-agent'];
|
|
||||||
|
|
||||||
return {
|
|
||||||
ipAddress,
|
|
||||||
userAgent,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { ErrorHandlerOptions } from '@trpc/server/unstable-core-do-not-import';
|
import type { ErrorHandlerOptions } from '@trpc/server/unstable-core-do-not-import';
|
||||||
|
|
||||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||||
|
import { env } from '@documenso/lib/utils/env';
|
||||||
import { buildLogger } from '@documenso/lib/utils/logger';
|
import { buildLogger } from '@documenso/lib/utils/logger';
|
||||||
|
|
||||||
const logger = buildLogger();
|
const logger = buildLogger();
|
||||||
@@ -10,8 +11,10 @@ export const handleTrpcRouterError = (
|
|||||||
{ error, path }: Pick<ErrorHandlerOptions<undefined>, 'error' | 'path'>,
|
{ error, path }: Pick<ErrorHandlerOptions<undefined>, 'error' | 'path'>,
|
||||||
source: 'trpc' | 'apiV1' | 'apiV2',
|
source: 'trpc' | 'apiV1' | 'apiV2',
|
||||||
) => {
|
) => {
|
||||||
// Always log the error for now.
|
// Always log the error on production for now.
|
||||||
console.error(error);
|
if (env('NODE_ENV') !== 'development') {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
|
||||||
const appError = AppError.parseError(error.cause || error);
|
const appError = AppError.parseError(error.cause || error);
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react';
|
import { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react';
|
||||||
|
|
||||||
import type { DropResult, SensorAPI } from '@hello-pangea/dnd';
|
import type { DropResult, SensorAPI } from '@hello-pangea/dnd';
|
||||||
@@ -11,9 +9,9 @@ import type { TemplateDirectLink } from '@prisma/client';
|
|||||||
import { DocumentSigningOrder, type Field, type Recipient, RecipientRole } from '@prisma/client';
|
import { DocumentSigningOrder, type Field, type Recipient, RecipientRole } from '@prisma/client';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import { GripVerticalIcon, Link2Icon, Plus, Trash } from 'lucide-react';
|
import { GripVerticalIcon, Link2Icon, Plus, Trash } from 'lucide-react';
|
||||||
import { useSession } from 'next-auth/react';
|
|
||||||
import { useFieldArray, useForm } from 'react-hook-form';
|
import { useFieldArray, useForm } from 'react-hook-form';
|
||||||
|
|
||||||
|
import { useSession } from '@documenso/lib/client-only/providers/session';
|
||||||
import { ZRecipientAuthOptionsSchema } from '@documenso/lib/types/document-auth';
|
import { ZRecipientAuthOptionsSchema } from '@documenso/lib/types/document-auth';
|
||||||
import { nanoid } from '@documenso/lib/universal/id';
|
import { nanoid } from '@documenso/lib/universal/id';
|
||||||
import { generateRecipientPlaceholder } from '@documenso/lib/utils/templates';
|
import { generateRecipientPlaceholder } from '@documenso/lib/utils/templates';
|
||||||
@@ -66,9 +64,7 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
|
|||||||
const $sensorApi = useRef<SensorAPI | null>(null);
|
const $sensorApi = useRef<SensorAPI | null>(null);
|
||||||
|
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
const { data: session } = useSession();
|
const { user } = useSession();
|
||||||
|
|
||||||
const user = session?.user;
|
|
||||||
|
|
||||||
const [placeholderRecipientCount, setPlaceholderRecipientCount] = useState(() =>
|
const [placeholderRecipientCount, setPlaceholderRecipientCount] = useState(() =>
|
||||||
recipients.length > 1 ? recipients.length + 1 : 2,
|
recipients.length > 1 ? recipients.length + 1 : 2,
|
||||||
@@ -169,8 +165,8 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
|
|||||||
const onAddPlaceholderSelfRecipient = () => {
|
const onAddPlaceholderSelfRecipient = () => {
|
||||||
appendSigner({
|
appendSigner({
|
||||||
formId: nanoid(12),
|
formId: nanoid(12),
|
||||||
name: user?.name ?? '',
|
name: user.name ?? '',
|
||||||
email: user?.email ?? '',
|
email: user.email ?? '',
|
||||||
role: RecipientRole.SIGNER,
|
role: RecipientRole.SIGNER,
|
||||||
signingOrder: signers.length > 0 ? (signers[signers.length - 1]?.signingOrder ?? 0) + 1 : 1,
|
signingOrder: signers.length > 0 ? (signers[signers.length - 1]?.signingOrder ?? 0) + 1 : 1,
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user