mirror of
https://github.com/documenso/documenso.git
synced 2025-11-10 04:22:32 +10:00
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 { 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} />
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
@ -1,5 +1,6 @@
|
||||
import { Outlet, redirect } from 'react-router';
|
||||
|
||||
import { getLimits } from '@documenso/ee/server-only/limits/client';
|
||||
import { LimitsProvider } from '@documenso/ee/server-only/limits/provider/client';
|
||||
import { getSiteSettings } from '@documenso/lib/server-only/site-settings/get-site-settings';
|
||||
import { SITE_SETTINGS_BANNER_ID } from '@documenso/lib/server-only/site-settings/schemas/banner';
|
||||
@ -10,29 +11,35 @@ import { VerifyEmailBanner } from '~/components/(dashboard)/layout/verify-email-
|
||||
|
||||
import type { Route } from './+types/_layout';
|
||||
|
||||
export const loader = async ({ context }: Route.LoaderArgs) => {
|
||||
export const loader = async ({ request, context }: Route.LoaderArgs) => {
|
||||
const { session } = context;
|
||||
|
||||
const banner = await getSiteSettings().then((settings) =>
|
||||
settings.find((setting) => setting.id === SITE_SETTINGS_BANNER_ID),
|
||||
);
|
||||
|
||||
if (!session) {
|
||||
throw redirect('/signin');
|
||||
}
|
||||
|
||||
const banner = await getSiteSettings().then((settings) =>
|
||||
settings.find((setting) => setting.id === SITE_SETTINGS_BANNER_ID),
|
||||
);
|
||||
|
||||
const requestHeaders = Object.fromEntries(request.headers.entries());
|
||||
|
||||
const limits = await getLimits({ headers: requestHeaders, teamId: session.currentTeam?.id });
|
||||
|
||||
return {
|
||||
user: session.user,
|
||||
teams: session.teams,
|
||||
banner,
|
||||
limits,
|
||||
teamId: session.currentTeam?.id,
|
||||
};
|
||||
};
|
||||
|
||||
export default function Layout({ loaderData }: Route.ComponentProps) {
|
||||
const { user, teams, banner } = loaderData;
|
||||
const { user, teams, banner, limits, teamId } = loaderData;
|
||||
|
||||
return (
|
||||
<LimitsProvider>
|
||||
<LimitsProvider initialValue={limits} teamId={teamId}>
|
||||
{!user.emailVerified && <VerifyEmailBanner email={user.email} />}
|
||||
|
||||
{banner && <AppBanner banner={banner} />}
|
||||
|
||||
@ -30,6 +30,7 @@ import { DocumentPageViewDropdown } from '~/components/general/document/document
|
||||
import { DocumentPageViewInformation } from '~/components/general/document/document-page-view-information';
|
||||
import { DocumentPageViewRecentActivity } from '~/components/general/document/document-page-view-recent-activity';
|
||||
import { DocumentPageViewRecipients } from '~/components/general/document/document-page-view-recipients';
|
||||
import { superLoaderJson, useSuperLoaderData } from '~/utils/super-json-loader';
|
||||
|
||||
import type { Route } from './+types/$id._index';
|
||||
|
||||
@ -119,14 +120,16 @@ export async function loader({ params, context }: Route.LoaderArgs) {
|
||||
recipients,
|
||||
};
|
||||
|
||||
return {
|
||||
return superLoaderJson({
|
||||
document: documentWithRecipients,
|
||||
documentRootPath,
|
||||
fields,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export default function DocumentPage({ loaderData }: Route.ComponentProps) {
|
||||
export default function DocumentPage() {
|
||||
const loaderData = useSuperLoaderData<typeof loader>();
|
||||
|
||||
const { _ } = useLingui();
|
||||
const { user } = useSession();
|
||||
|
||||
|
||||
@ -12,7 +12,7 @@ import { findDocumentAuditLogs } from '@documenso/lib/server-only/document/find-
|
||||
import { dynamicActivate } from '@documenso/lib/utils/i18n';
|
||||
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
||||
|
||||
import { Logo } from '~/components/branding/logo';
|
||||
import { BrandingLogo } from '~/components/general/branding-logo';
|
||||
import { InternalAuditLogTable } from '~/components/tables/internal-audit-log-table';
|
||||
|
||||
import type { Route } from './+types/audit-log';
|
||||
@ -68,7 +68,7 @@ export default function AuditLog({ loaderData }: Route.ComponentProps) {
|
||||
const { i18n } = useLingui();
|
||||
|
||||
// Todo
|
||||
void dynamicActivate(i18n, documentLanguage);
|
||||
void dynamicActivate(documentLanguage);
|
||||
|
||||
const { _ } = useLingui();
|
||||
|
||||
@ -163,7 +163,7 @@ export default function AuditLog({ loaderData }: Route.ComponentProps) {
|
||||
|
||||
<div className="my-8 flex-row-reverse">
|
||||
<div className="flex items-end justify-end gap-x-4">
|
||||
<Logo className="max-h-6 print:max-h-4" />
|
||||
<BrandingLogo className="max-h-6 print:max-h-4" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -26,7 +26,7 @@ import {
|
||||
TableRow,
|
||||
} from '@documenso/ui/primitives/table';
|
||||
|
||||
import { Logo } from '~/components/branding/logo';
|
||||
import { BrandingLogo } from '~/components/general/branding-logo';
|
||||
|
||||
import type { Route } from './+types/certificate';
|
||||
|
||||
@ -316,7 +316,7 @@ export default function SigningCertificate({ loaderData }: Route.ComponentProps)
|
||||
{_(msg`Signing certificate provided by`)}:
|
||||
</p>
|
||||
|
||||
<Logo className="max-h-6 print:max-h-4" />
|
||||
<BrandingLogo className="max-h-6 print:max-h-4" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -10,7 +10,7 @@ import { cn } from '@documenso/ui/lib/utils';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
|
||||
import { Header as AuthenticatedHeader } from '~/components/(dashboard)/layout/header';
|
||||
import { Logo } from '~/components/branding/logo';
|
||||
import { Logo } from '~/components/general/branding-logo';
|
||||
|
||||
import type { Route } from './+types/_layout';
|
||||
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
// Todo: This relies on NextJS
|
||||
import { ImageResponse } from 'next/og';
|
||||
|
||||
import { P, match } from 'ts-pattern';
|
||||
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
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 { 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>
|
||||
@ -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.
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -15,4 +15,4 @@ server.use(
|
||||
|
||||
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 { 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');
|
||||
}
|
||||
}
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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';
|
||||
|
||||
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,
|
||||
};
|
||||
};
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user