This commit is contained in:
David Nguyen
2025-02-04 22:25:11 +11:00
parent 381a9d3fb8
commit 540cc5bfc1
35 changed files with 529 additions and 890 deletions

View File

@ -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>
);
};

View File

@ -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();
}

View File

@ -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' },
],
},
],
};

View File

@ -8,7 +8,7 @@ import type { TGetTeamsResponse } from '@documenso/lib/server-only/team/get-team
import { getRootHref } from '@documenso/lib/utils/params';
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 { DesktopNav } from './desktop-nav';
@ -62,7 +62,7 @@ export const Header = ({ className, user, teams, ...props }: HeaderProps) => {
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"
>
<Logo className="h-6 w-auto" />
<BrandingLogo className="h-6 w-auto" />
</Link>
<DesktopNav setIsCommandMenuOpen={setIsCommandMenuOpen} />

View File

@ -1,6 +1,5 @@
import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import type { Team } from '@prisma/client';
import { useNavigate } from 'react-router';
import { formatDocumentsPath } from '@documenso/lib/utils/teams';

View File

@ -2,8 +2,8 @@ import { Trans } from '@lingui/macro';
import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
import { Logo } from '~/components/branding/logo';
import { SignInForm } from '~/components/forms/signin';
import { BrandingLogo } from '~/components/general/branding-logo';
export type EmbedAuthenticationRequiredProps = {
email?: string;
@ -17,7 +17,7 @@ export const EmbedAuthenticationRequired = ({
return (
<div className="flex min-h-[100dvh] w-full items-center justify-center">
<div className="flex w-full max-w-md flex-col">
<Logo className="h-8" />
<BrandingLogo className="h-8" />
<Alert className="mt-8" variant="warning">
<AlertDescription>

View File

@ -28,7 +28,7 @@ import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
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 { injectCss } from '~/utils/css-vars';
@ -493,7 +493,7 @@ export const EmbedDirectTemplateClientPage = ({
{!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">
<span>Powered by</span>
<Logo className="ml-2 inline-block h-[14px]" />
<BrandingLogo className="ml-2 inline-block h-[14px]" />
</div>
)}
</div>

View File

@ -20,7 +20,7 @@ import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
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 { ZSignDocumentEmbedDataSchema } from '../../types/embed-document-sign-schema';
@ -367,7 +367,7 @@ export const EmbedSignDocumentClientPage = ({
{!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">
<span>Powered by</span>
<Logo className="ml-2 inline-block h-[14px]" />
<BrandingLogo className="ml-2 inline-block h-[14px]" />
</div>
)}
</div>

View File

@ -2,7 +2,7 @@ import type { SVGAttributes } from 'react';
export type LogoProps = SVGAttributes<SVGSVGElement>;
export const Logo = ({ ...props }: LogoProps) => {
export const BrandingLogo = ({ ...props }: LogoProps) => {
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2248 320" {...props}>
<path

View File

@ -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} />}

View File

@ -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();

View File

@ -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>

View File

@ -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>

View File

@ -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';

View File

@ -1,3 +1,4 @@
// Todo: This relies on NextJS
import { ImageResponse } from 'next/og';
import { P, match } from 'ts-pattern';

View File

@ -1,10 +1,7 @@
import Link from 'next/link';
import { Trans } from '@lingui/macro';
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 { declineTeamInvitation } from '@documenso/lib/server-only/team/decline-team-invitation';
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 { Button } from '@documenso/ui/primitives/button';
type DeclineInvitationPageProps = {
params: {
token: string;
};
};
import type { Route } from './+types/team.decline.$token';
export default async function DeclineInvitationPage({
params: { token },
}: DeclineInvitationPageProps) {
await setupI18nSSR();
export async function loader({ params, context }: Route.LoaderArgs) {
const { token } = params;
const session = await getServerComponentSession();
if (!token) {
return {
state: 'InvalidLink',
} as const;
}
const teamMemberInvite = await prisma.teamMemberInvite.findUnique({
where: {
@ -32,25 +27,9 @@ export default async function DeclineInvitationPage({
});
if (!teamMemberInvite) {
return (
<div className="w-screen max-w-lg px-4">
<div className="w-full">
<h1 className="text-4xl font-semibold">
<Trans>Invalid token</Trans>
</h1>
<p className="text-muted-foreground mb-4 mt-2 text-sm">
<Trans>This token is invalid or has expired. No action is needed.</Trans>
</p>
<Button asChild>
<Link href="/">
<Trans>Return</Trans>
</Link>
</Button>
</div>
</div>
);
return {
state: 'InvalidLink',
} as const;
}
const team = await getTeamById({ teamId: teamMemberInvite.teamId });
@ -85,6 +64,49 @@ export default async function DeclineInvitationPage({
});
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">
@ -93,7 +115,7 @@ export default async function DeclineInvitationPage({
<p className="text-muted-foreground mt-2 text-sm">
<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>
</p>
@ -102,7 +124,7 @@ export default async function DeclineInvitationPage({
</p>
<Button asChild>
<Link href={`/signup?email=${encodeURIComponent(email)}`}>
<Link to={`/signup?email=${encodeURIComponent(data.email)}`}>
<Trans>Create account</Trans>
</Link>
</Button>
@ -110,8 +132,6 @@ export default async function DeclineInvitationPage({
);
}
const isSessionUserTheInvitedUser = user?.id === session.user?.id;
return (
<div className="w-screen max-w-lg px-4">
<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">
<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>
</p>
{isSessionUserTheInvitedUser ? (
{data.isSessionUserTheInvitedUser ? (
<Button asChild>
<Link href="/">
<Link to="/">
<Trans>Return to Dashboard</Trans>
</Link>
</Button>
) : (
<Button asChild>
<Link href="/">
<Link to="/">
<Trans>Return to Home</Trans>
</Link>
</Button>

View File

@ -1,10 +1,7 @@
import Link from 'next/link';
import { Trans } from '@lingui/macro';
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 { acceptTeamInvitation } from '@documenso/lib/server-only/team/accept-team-invitation';
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 { Button } from '@documenso/ui/primitives/button';
type AcceptInvitationPageProps = {
params: {
token: string;
};
};
import type { Route } from './+types/team.invite.$token';
export default async function AcceptInvitationPage({
params: { token },
}: AcceptInvitationPageProps) {
await setupI18nSSR();
export async function loader({ params, context }: Route.LoaderArgs) {
const { token } = params;
const session = await getServerComponentSession();
if (!token) {
return {
state: 'InvalidLink',
} as const;
}
const teamMemberInvite = await prisma.teamMemberInvite.findUnique({
where: {
@ -32,27 +27,9 @@ export default async function AcceptInvitationPage({
});
if (!teamMemberInvite) {
return (
<div className="w-screen max-w-lg px-4">
<div className="w-full">
<h1 className="text-4xl font-semibold">
<Trans>Invalid token</Trans>
</h1>
<p className="text-muted-foreground mb-4 mt-2 text-sm">
<Trans>
This token is invalid or has expired. Please contact your team for a new invitation.
</Trans>
</p>
<Button asChild>
<Link href="/">
<Trans>Return</Trans>
</Link>
</Button>
</div>
</div>
);
return {
state: 'InvalidLink',
} as const;
}
const team = await getTeamById({ teamId: teamMemberInvite.teamId });
@ -90,6 +67,51 @@ export default async function AcceptInvitationPage({
});
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">
@ -98,7 +120,7 @@ export default async function AcceptInvitationPage({
<p className="text-muted-foreground mt-2 text-sm">
<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>
</p>
@ -107,7 +129,7 @@ export default async function AcceptInvitationPage({
</p>
<Button asChild>
<Link href={`/signup?email=${encodeURIComponent(email)}`}>
<Link to={`/signup?email=${encodeURIComponent(data.email)}`}>
<Trans>Create account</Trans>
</Link>
</Button>
@ -115,8 +137,6 @@ export default async function AcceptInvitationPage({
);
}
const isSessionUserTheInvitedUser = user.id === session.user?.id;
return (
<div>
<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">
<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>
</p>
{isSessionUserTheInvitedUser ? (
{data.isSessionUserTheInvitedUser ? (
<Button asChild>
<Link href="/">
<Link to="/">
<Trans>Continue</Trans>
</Link>
</Button>
) : (
<Button asChild>
<Link href={`/signin?email=${encodeURIComponent(email)}`}>
<Link to={`/signin?email=${encodeURIComponent(data.email)}`}>
<Trans>Continue to login</Trans>
</Link>
</Button>

View File

@ -1,20 +1,20 @@
import Link from 'next/link';
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 { prisma } from '@documenso/prisma';
import { Button } from '@documenso/ui/primitives/button';
type VerifyTeamEmailPageProps = {
params: {
token: string;
};
};
import type { Route } from './+types/team.verify.email.$token';
export default async function VerifyTeamEmailPage({ params: { token } }: VerifyTeamEmailPageProps) {
await setupI18nSSR();
export async function loader({ params }: Route.LoaderArgs) {
const { token } = params;
if (!token) {
return {
state: 'InvalidLink',
} as const;
}
const teamEmailVerification = await prisma.teamEmailVerification.findUnique({
where: {
@ -26,51 +26,16 @@ export default async function VerifyTeamEmailPage({ params: { token } }: VerifyT
});
if (!teamEmailVerification || isTokenExpired(teamEmailVerification.expiresAt)) {
return (
<div className="w-screen max-w-lg px-4">
<div className="w-full">
<h1 className="text-4xl font-semibold">
<Trans>Invalid link</Trans>
</h1>
<p className="text-muted-foreground mb-4 mt-2 text-sm">
<Trans>
This link is invalid or has expired. Please contact your team to resend a
verification.
</Trans>
</p>
<Button asChild>
<Link href="/">
<Trans>Return</Trans>
</Link>
</Button>
</div>
</div>
);
return {
state: 'InvalidLink',
} as const;
}
if (teamEmailVerification.completed) {
return (
<div>
<h1 className="text-4xl font-semibold">
<Trans>Team email already verified!</Trans>
</h1>
<p className="text-muted-foreground mb-4 mt-2 text-sm">
<Trans>
You have already verified your email address for{' '}
<strong>{teamEmailVerification.team.name}</strong>.
</Trans>
</p>
<Button asChild>
<Link href="/">
<Trans>Continue</Trans>
</Link>
</Button>
</div>
);
return {
state: 'AlreadyCompleted',
teamName: teamEmailVerification.team.name,
} as const;
}
const { team } = teamEmailVerification;
@ -110,6 +75,69 @@ export default async function VerifyTeamEmailPage({ params: { token } }: VerifyT
}
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">
@ -119,7 +147,7 @@ export default async function VerifyTeamEmailPage({ params: { token } }: VerifyT
<p className="text-muted-foreground mt-2 text-sm">
<Trans>
Something went wrong while attempting to verify your email address for{' '}
<strong>{team.name}</strong>. Please try again later.
<strong>{data.teamName}</strong>. Please try again later.
</Trans>
</p>
</div>
@ -134,12 +162,12 @@ export default async function VerifyTeamEmailPage({ params: { token } }: VerifyT
<p className="text-muted-foreground mb-4 mt-2 text-sm">
<Trans>
You have verified your email address for <strong>{team.name}</strong>.
You have verified your email address for <strong>{data.teamName}</strong>.
</Trans>
</p>
<Button asChild>
<Link href="/">
<Link to="/">
<Trans>Continue</Trans>
</Link>
</Button>

View File

@ -1,23 +1,21 @@
import Link from 'next/link';
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 { isTokenExpired } from '@documenso/lib/utils/token-verification';
import { prisma } from '@documenso/prisma';
import { Button } from '@documenso/ui/primitives/button';
type VerifyTeamTransferPage = {
params: {
token: string;
};
};
import type { Route } from './+types/team.verify.transfer.token';
export default async function VerifyTeamTransferPage({
params: { token },
}: VerifyTeamTransferPage) {
await setupI18nSSR();
export async function loader({ params }: Route.LoaderArgs) {
const { token } = params;
if (!token) {
return {
state: 'InvalidLink',
} as const;
}
const teamTransferVerification = await prisma.teamTransferVerification.findUnique({
where: {
@ -29,6 +27,47 @@ export default async function VerifyTeamTransferPage({
});
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">
@ -44,7 +83,7 @@ export default async function VerifyTeamTransferPage({
</p>
<Button asChild>
<Link href="/">
<Link to="/">
<Trans>Return</Trans>
</Link>
</Button>
@ -53,7 +92,7 @@ export default async function VerifyTeamTransferPage({
);
}
if (teamTransferVerification.completed) {
if (data.state === 'AlreadyCompleted') {
return (
<div>
<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">
<Trans>
You have already completed the ownership transfer for{' '}
<strong>{teamTransferVerification.team.name}</strong>.
You have already completed the ownership transfer for <strong>{data.teamName}</strong>.
</Trans>
</p>
<Button asChild>
<Link href="/">
<Link to="/">
<Trans>Continue</Trans>
</Link>
</Button>
@ -76,18 +114,7 @@ export default async function VerifyTeamTransferPage({
);
}
const { team } = teamTransferVerification;
let isTransferError = false;
try {
await transferTeamOwnership({ token });
} catch (e) {
console.error(e);
isTransferError = true;
}
if (isTransferError) {
if (data.state === 'TransferError') {
return (
<div>
<h1 className="text-4xl font-semibold">
@ -97,7 +124,7 @@ export default async function VerifyTeamTransferPage({
<p className="text-muted-foreground mt-2 text-sm">
<Trans>
Something went wrong while attempting to transfer the ownership of team{' '}
<strong>{team.name}</strong> to your. Please try again later or contact support.
<strong>{data.teamName}</strong> to your. Please try again later or contact support.
</Trans>
</p>
</div>
@ -112,13 +139,13 @@ export default async function VerifyTeamTransferPage({
<p className="text-muted-foreground mb-4 mt-2 text-sm">
<Trans>
The ownership of team <strong>{team.name}</strong> has been successfully transferred to
you.
The ownership of team <strong>{data.teamName}</strong> has been successfully transferred
to you.
</Trans>
</p>
<Button asChild>
<Link href={`/t/${team.url}/settings`}>
<Link to={`/t/${data.teamUrl}/settings`}>
<Trans>Continue</Trans>
</Link>
</Button>

View 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();
}

View File

@ -1,19 +1,14 @@
import type { Metadata } from 'next';
import Link from 'next/link';
import { Trans } from '@lingui/macro';
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';
export const metadata: Metadata = {
title: 'Verify Email',
};
export default async function EmailVerificationWithoutTokenPage() {
await setupI18nSSR();
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">
@ -34,7 +29,7 @@ export default async function EmailVerificationWithoutTokenPage() {
</p>
<Button className="mt-4" asChild>
<Link href="/">
<Link to="/">
<Trans>Go back home</Trans>
</Link>
</Button>

View File

@ -22,7 +22,7 @@ app.route('/api/auth', auth);
// API servers. Todo: Configure max durations, etc?
app.route('/api/v1', tsRestHonoApp);
app.use('/api/jobs/*', jobsClient.getHonoApiHandler());
app.use('/api/jobs/*', jobsClient.getApiHandler());
app.use('/api/trpc/*', reactRouterTrpcServer);
// Unstable API server routes. Order matters for these two.

View File

@ -73,9 +73,6 @@ export async function getLoadContext(args: GetLoadContextArgs) {
* - /favicon.* (Favicon files)
* - *.webmanifest (Web manifest files)
* - 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 = {
matcher: new RegExp(

View File

@ -15,4 +15,4 @@ server.use(
const handler = handle(build, server, { getLoadContext });
serve({ fetch: handler.fetch, port: 3010 });
serve({ fetch: handler.fetch, port: 3000 });

View File

@ -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>
);
};

View File

@ -1,5 +1,3 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import type { Context as HonoContext } from 'hono';
import type { JobDefinition, SimpleTriggerJobOptions } from './_internal/job';
@ -15,11 +13,7 @@ export abstract class BaseJobProvider {
throw new Error('Not implemented');
}
public getApiHandler(): (req: NextApiRequest, res: NextApiResponse) => Promise<Response | void> {
throw new Error('Not implemented');
}
public getHonoApiHandler(): (req: HonoContext) => Promise<Response | void> {
public getApiHandler(): (req: HonoContext) => Promise<Response | void> {
throw new Error('Not implemented');
}
}

View File

@ -5,7 +5,6 @@ import type { JobDefinition, TriggerJobOptions } from './_internal/job';
import type { BaseJobProvider as JobClientProvider } from './base';
import { InngestJobProvider } from './inngest';
import { LocalJobProvider } from './local';
import { TriggerJobProvider } from './trigger';
export class JobClient<T extends ReadonlyArray<JobDefinition> = []> {
private _provider: JobClientProvider;
@ -13,7 +12,6 @@ export class JobClient<T extends ReadonlyArray<JobDefinition> = []> {
public constructor(definitions: T) {
this._provider = match(env('NEXT_PRIVATE_JOBS_PROVIDER'))
.with('inngest', () => InngestJobProvider.getInstance())
.with('trigger', () => TriggerJobProvider.getInstance())
.otherwise(() => LocalJobProvider.getInstance());
definitions.forEach((definition) => {
@ -28,8 +26,4 @@ export class JobClient<T extends ReadonlyArray<JobDefinition> = []> {
public getApiHandler() {
return this._provider.getApiHandler();
}
public getHonoApiHandler() {
return this._provider.getHonoApiHandler();
}
}

View File

@ -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, Handler, InngestFunction } from 'inngest';
import { Inngest as InngestClient } from 'inngest';
import { serve as createHonoPagesRoute } from 'inngest/hono';
import type { Logger } from 'inngest/middleware/logger';
import { serve as createPagesRoute } from 'inngest/next';
import { json } from 'micro';
import { env } from '../../utils/env';
import type { JobDefinition, JobRunIO, SimpleTriggerJobOptions } from './_internal/job';
@ -76,29 +71,29 @@ export class InngestJobProvider extends BaseJobProvider {
});
}
public getApiHandler() {
const handler = createPagesRoute({
client: this._client,
functions: this._functions,
});
// public getApiHandler() {
// const handler = createPagesRoute({
// client: this._client,
// functions: this._functions,
// });
return async (req: NextApiRequest, res: NextApiResponse) => {
// Since body-parser is disabled for this route we need to patch in the parsed body
if (req.headers['content-type'] === 'application/json') {
Object.assign(req, {
body: await json(req),
});
}
// return async (req: NextApiRequest, res: NextApiResponse) => {
// // Since body-parser is disabled for this route we need to patch in the parsed body
// if (req.headers['content-type'] === 'application/json') {
// Object.assign(req, {
// body: await json(req),
// });
// }
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const nextReq = req as unknown as NextRequest;
// // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
// 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?
public getHonoApiHandler() {
public getApiHandler() {
return async (context: HonoContext) => {
const handler = createHonoPagesRoute({
client: this._client,

View File

@ -1,9 +1,6 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { sha256 } from '@noble/hashes/sha256';
import { BackgroundJobStatus, Prisma } from '@prisma/client';
import type { Context as HonoContext } from 'hono';
import { json } from 'micro';
import { prisma } from '@documenso/prisma';
@ -71,150 +68,7 @@ export class LocalJobProvider extends BaseJobProvider {
);
}
public getApiHandler() {
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> {
public getApiHandler(): (context: HonoContext) => Promise<Response | void> {
return async (context: HonoContext) => {
const req = context.req;

View File

@ -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;
}
}

View File

@ -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,
});
};

View File

@ -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;
};

View File

@ -1,6 +1,3 @@
import type { NextApiRequest } from 'next';
import type { RequestInternal } from 'next-auth';
import { z } from 'zod';
const ZIpSchema = z.string().ip();
@ -53,35 +50,3 @@ export const extractRequestMetadata = (req: Request): RequestMetadata => {
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,
};
};

View File

@ -1,6 +1,7 @@
import type { ErrorHandlerOptions } from '@trpc/server/unstable-core-do-not-import';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { env } from '@documenso/lib/utils/env';
import { buildLogger } from '@documenso/lib/utils/logger';
const logger = buildLogger();
@ -10,8 +11,10 @@ export const handleTrpcRouterError = (
{ error, path }: Pick<ErrorHandlerOptions<undefined>, 'error' | 'path'>,
source: 'trpc' | 'apiV1' | 'apiV2',
) => {
// Always log the error for now.
console.error(error);
// Always log the error on production for now.
if (env('NODE_ENV') !== 'development') {
console.error(error);
}
const appError = AppError.parseError(error.cause || error);

View File

@ -1,5 +1,3 @@
'use client';
import { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react';
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 { motion } from 'framer-motion';
import { GripVerticalIcon, Link2Icon, Plus, Trash } from 'lucide-react';
import { useSession } from 'next-auth/react';
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 { nanoid } from '@documenso/lib/universal/id';
import { generateRecipientPlaceholder } from '@documenso/lib/utils/templates';
@ -66,9 +64,7 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
const $sensorApi = useRef<SensorAPI | null>(null);
const { _ } = useLingui();
const { data: session } = useSession();
const user = session?.user;
const { user } = useSession();
const [placeholderRecipientCount, setPlaceholderRecipientCount] = useState(() =>
recipients.length > 1 ? recipients.length + 1 : 2,
@ -169,8 +165,8 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
const onAddPlaceholderSelfRecipient = () => {
appendSigner({
formId: nanoid(12),
name: user?.name ?? '',
email: user?.email ?? '',
name: user.name ?? '',
email: user.email ?? '',
role: RecipientRole.SIGNER,
signingOrder: signers.length > 0 ? (signers[signers.length - 1]?.signingOrder ?? 0) + 1 : 1,
});