diff --git a/apps/remix/app/_todo/app/(unauthenticated)/verify-email/[token]/client.tsx b/apps/remix/app/_todo/app/(unauthenticated)/verify-email/[token]/client.tsx deleted file mode 100644 index d7c2a936a..000000000 --- a/apps/remix/app/_todo/app/(unauthenticated)/verify-email/[token]/client.tsx +++ /dev/null @@ -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 ( -
-
-
- -
- -
-

- Email Confirmed! -

- -

- - Your email has been successfully confirmed! You can now use all features of Documenso. - -

- - {!signInData && ( - - )} -
-
-
- ); -}; diff --git a/apps/remix/app/_todo/app/(unauthenticated)/verify-email/[token]/page.tsx b/apps/remix/app/_todo/app/(unauthenticated)/verify-email/[token]/page.tsx deleted file mode 100644 index eb88538c4..000000000 --- a/apps/remix/app/_todo/app/(unauthenticated)/verify-email/[token]/page.tsx +++ /dev/null @@ -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 ( -
-
-
- -
- -

- No token provided -

-

- - It seems that there is no token provided. Please check your email and try again. - -

-
-
- ); - } - - const verified = await verifyEmail({ token }); - - return await match(verified) - .with(EMAIL_VERIFICATION_STATE.NOT_FOUND, () => ( -
-
-
- -
- -
-

- Something went wrong -

- -

- - We were unable to verify your email. If your email is not verified already, please - try again. - -

- - -
-
-
- )) - .with(EMAIL_VERIFICATION_STATE.EXPIRED, () => ( -
-
-
- -
- -
-

- Your token has expired! -

- -

- - It seems that the provided token has expired. We've just sent you another token, - please check your email and try again. - -

- - -
-
-
- )) - .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 ; - }) - .with(EMAIL_VERIFICATION_STATE.ALREADY_VERIFIED, () => ) - .exhaustive(); -} diff --git a/apps/remix/app/_todo/middleware.ts b/apps/remix/app/_todo/middleware.ts deleted file mode 100644 index 25de9debb..000000000 --- a/apps/remix/app/_todo/middleware.ts +++ /dev/null @@ -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 { - 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/` to `/t//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' }, - ], - }, - ], -}; diff --git a/apps/remix/app/components/(dashboard)/layout/header.tsx b/apps/remix/app/components/(dashboard)/layout/header.tsx index 7b97a87a3..42fbe9c2c 100644 --- a/apps/remix/app/components/(dashboard)/layout/header.tsx +++ b/apps/remix/app/components/(dashboard)/layout/header.tsx @@ -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" > - + diff --git a/apps/remix/app/components/dialogs/document-duplicate-dialog.tsx b/apps/remix/app/components/dialogs/document-duplicate-dialog.tsx index b7c5dd013..b841fb839 100644 --- a/apps/remix/app/components/dialogs/document-duplicate-dialog.tsx +++ b/apps/remix/app/components/dialogs/document-duplicate-dialog.tsx @@ -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'; diff --git a/apps/remix/app/components/embed/embed-authentication-required.tsx b/apps/remix/app/components/embed/embed-authentication-required.tsx index 5fd8bc69e..08bd0696b 100644 --- a/apps/remix/app/components/embed/embed-authentication-required.tsx +++ b/apps/remix/app/components/embed/embed-authentication-required.tsx @@ -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 (
- + diff --git a/apps/remix/app/components/embed/embed-direct-template-client-page.tsx b/apps/remix/app/components/embed/embed-direct-template-client-page.tsx index 0c9d29f22..f30dda8ea 100644 --- a/apps/remix/app/components/embed/embed-direct-template-client-page.tsx +++ b/apps/remix/app/components/embed/embed-direct-template-client-page.tsx @@ -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 && (
Powered by - +
)}
diff --git a/apps/remix/app/components/embed/embed-document-signing-page.tsx b/apps/remix/app/components/embed/embed-document-signing-page.tsx index 2aad8b292..bc2b039f7 100644 --- a/apps/remix/app/components/embed/embed-document-signing-page.tsx +++ b/apps/remix/app/components/embed/embed-document-signing-page.tsx @@ -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 && (
Powered by - +
)}
diff --git a/apps/remix/app/components/branding/logo.tsx b/apps/remix/app/components/general/branding-logo.tsx similarity index 99% rename from apps/remix/app/components/branding/logo.tsx rename to apps/remix/app/components/general/branding-logo.tsx index 92087a149..57932129a 100644 --- a/apps/remix/app/components/branding/logo.tsx +++ b/apps/remix/app/components/general/branding-logo.tsx @@ -2,7 +2,7 @@ import type { SVGAttributes } from 'react'; export type LogoProps = SVGAttributes; -export const Logo = ({ ...props }: LogoProps) => { +export const BrandingLogo = ({ ...props }: LogoProps) => { return ( { +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 ( - + {!user.emailVerified && } {banner && } diff --git a/apps/remix/app/routes/_authenticated+/documents+/$id._index.tsx b/apps/remix/app/routes/_authenticated+/documents+/$id._index.tsx index dc653b0c9..cf674e98c 100644 --- a/apps/remix/app/routes/_authenticated+/documents+/$id._index.tsx +++ b/apps/remix/app/routes/_authenticated+/documents+/$id._index.tsx @@ -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(); + const { _ } = useLingui(); const { user } = useSession(); diff --git a/apps/remix/app/routes/_internal+/[__htmltopdf]+/audit-log.tsx b/apps/remix/app/routes/_internal+/[__htmltopdf]+/audit-log.tsx index ddeeba5fd..f1a604181 100644 --- a/apps/remix/app/routes/_internal+/[__htmltopdf]+/audit-log.tsx +++ b/apps/remix/app/routes/_internal+/[__htmltopdf]+/audit-log.tsx @@ -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) {
- +
diff --git a/apps/remix/app/routes/_internal+/[__htmltopdf]+/certificate.tsx b/apps/remix/app/routes/_internal+/[__htmltopdf]+/certificate.tsx index 28d5ce96c..60758908c 100644 --- a/apps/remix/app/routes/_internal+/[__htmltopdf]+/certificate.tsx +++ b/apps/remix/app/routes/_internal+/[__htmltopdf]+/certificate.tsx @@ -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`)}:

- + diff --git a/apps/remix/app/routes/_profile+/_layout.tsx b/apps/remix/app/routes/_profile+/_layout.tsx index a47915c73..d5a6d7737 100644 --- a/apps/remix/app/routes/_profile+/_layout.tsx +++ b/apps/remix/app/routes/_profile+/_layout.tsx @@ -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'; diff --git a/apps/remix/app/routes/_unauthenticated+/share.$slug.opengraph.tsx b/apps/remix/app/routes/_unauthenticated+/share.$slug.opengraph.tsx index 606d85fbc..2f264f4ef 100644 --- a/apps/remix/app/routes/_unauthenticated+/share.$slug.opengraph.tsx +++ b/apps/remix/app/routes/_unauthenticated+/share.$slug.opengraph.tsx @@ -1,3 +1,4 @@ +// Todo: This relies on NextJS import { ImageResponse } from 'next/og'; import { P, match } from 'ts-pattern'; diff --git a/apps/remix/app/_todo/app/(unauthenticated)/team/decline/[token]/page.tsx b/apps/remix/app/routes/_unauthenticated+/team.decline.$token.tsx similarity index 70% rename from apps/remix/app/_todo/app/(unauthenticated)/team/decline/[token]/page.tsx rename to apps/remix/app/routes/_unauthenticated+/team.decline.$token.tsx index 06c7dadc9..e6a402ac9 100644 --- a/apps/remix/app/_todo/app/(unauthenticated)/team/decline/[token]/page.tsx +++ b/apps/remix/app/routes/_unauthenticated+/team.decline.$token.tsx @@ -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 ( -
-
-

- Invalid token -

- -

- This token is invalid or has expired. No action is needed. -

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

+ Invalid token +

+ +

+ This token is invalid or has expired. No action is needed. +

+ + +
+
+ ); + } + + if (data.state === 'LoginRequired') { return (

@@ -93,7 +115,7 @@ export default async function DeclineInvitationPage({

- You have been invited by {team.name} to join their team. + You have been invited by {data.teamName} to join their team.

@@ -102,7 +124,7 @@ export default async function DeclineInvitationPage({

@@ -110,8 +132,6 @@ export default async function DeclineInvitationPage({ ); } - const isSessionUserTheInvitedUser = user?.id === session.user?.id; - return (

@@ -120,19 +140,19 @@ export default async function DeclineInvitationPage({

- You have declined the invitation from {team.name} to join their team. + You have declined the invitation from {data.teamName} to join their team.

- {isSessionUserTheInvitedUser ? ( + {data.isSessionUserTheInvitedUser ? ( ) : ( diff --git a/apps/remix/app/_todo/app/(unauthenticated)/team/invite/[token]/page.tsx b/apps/remix/app/routes/_unauthenticated+/team.invite.$token.tsx similarity index 72% rename from apps/remix/app/_todo/app/(unauthenticated)/team/invite/[token]/page.tsx rename to apps/remix/app/routes/_unauthenticated+/team.invite.$token.tsx index 3441dbed7..561f96d8e 100644 --- a/apps/remix/app/_todo/app/(unauthenticated)/team/invite/[token]/page.tsx +++ b/apps/remix/app/routes/_unauthenticated+/team.invite.$token.tsx @@ -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 ( -
-
-

- Invalid token -

- -

- - This token is invalid or has expired. Please contact your team for a new invitation. - -

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

+ Invalid token +

+ +

+ + This token is invalid or has expired. Please contact your team for a new invitation. + +

+ + +
+
+ ); + } + + if (data.state === 'LoginRequired') { return (

@@ -98,7 +120,7 @@ export default async function AcceptInvitationPage({

- You have been invited by {team.name} to join their team. + You have been invited by {data.teamName} to join their team.

@@ -107,7 +129,7 @@ export default async function AcceptInvitationPage({

@@ -115,8 +137,6 @@ export default async function AcceptInvitationPage({ ); } - const isSessionUserTheInvitedUser = user.id === session.user?.id; - return (

@@ -125,19 +145,19 @@ export default async function AcceptInvitationPage({

- You have accepted an invitation from {team.name} to join their team. + You have accepted an invitation from {data.teamName} to join their team.

- {isSessionUserTheInvitedUser ? ( + {data.isSessionUserTheInvitedUser ? ( ) : ( diff --git a/apps/remix/app/_todo/app/(unauthenticated)/team/verify/email/[token]/page.tsx b/apps/remix/app/routes/_unauthenticated+/team.verify.email.$token.tsx similarity index 71% rename from apps/remix/app/_todo/app/(unauthenticated)/team/verify/email/[token]/page.tsx rename to apps/remix/app/routes/_unauthenticated+/team.verify.email.$token.tsx index b53fb5f71..023b531f7 100644 --- a/apps/remix/app/_todo/app/(unauthenticated)/team/verify/email/[token]/page.tsx +++ b/apps/remix/app/routes/_unauthenticated+/team.verify.email.$token.tsx @@ -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 ( -
-
-

- Invalid link -

- -

- - This link is invalid or has expired. Please contact your team to resend a - verification. - -

- - -
-
- ); + return { + state: 'InvalidLink', + } as const; } if (teamEmailVerification.completed) { - return ( -
-

- Team email already verified! -

- -

- - You have already verified your email address for{' '} - {teamEmailVerification.team.name}. - -

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

+ Invalid link +

+ +

+ + This link is invalid or has expired. Please contact your team to resend a + verification. + +

+ + +
+
+ ); + } + + if (data.state === 'AlreadyCompleted') { + return ( +
+

+ Team email already verified! +

+ +

+ + You have already verified your email address for {data.teamName}. + +

+ + +
+ ); + } + + if (data.state === 'VerificationError') { return (

@@ -119,7 +147,7 @@ export default async function VerifyTeamEmailPage({ params: { token } }: VerifyT

Something went wrong while attempting to verify your email address for{' '} - {team.name}. Please try again later. + {data.teamName}. Please try again later.

@@ -134,12 +162,12 @@ export default async function VerifyTeamEmailPage({ params: { token } }: VerifyT

- You have verified your email address for {team.name}. + You have verified your email address for {data.teamName}.

diff --git a/apps/remix/app/_todo/app/(unauthenticated)/team/verify/transfer/[token]/page.tsx b/apps/remix/app/routes/_unauthenticated+/team.verify.transfer.token.tsx similarity index 67% rename from apps/remix/app/_todo/app/(unauthenticated)/team/verify/transfer/[token]/page.tsx rename to apps/remix/app/routes/_unauthenticated+/team.verify.transfer.token.tsx index 8713aeecd..726cff626 100644 --- a/apps/remix/app/_todo/app/(unauthenticated)/team/verify/transfer/[token]/page.tsx +++ b/apps/remix/app/routes/_unauthenticated+/team.verify.transfer.token.tsx @@ -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 (
@@ -44,7 +83,7 @@ export default async function VerifyTeamTransferPage({

@@ -53,7 +92,7 @@ export default async function VerifyTeamTransferPage({ ); } - if (teamTransferVerification.completed) { + if (data.state === 'AlreadyCompleted') { return (

@@ -62,13 +101,12 @@ export default async function VerifyTeamTransferPage({

- You have already completed the ownership transfer for{' '} - {teamTransferVerification.team.name}. + You have already completed the ownership transfer for {data.teamName}.

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

@@ -97,7 +124,7 @@ export default async function VerifyTeamTransferPage({

Something went wrong while attempting to transfer the ownership of team{' '} - {team.name} to your. Please try again later or contact support. + {data.teamName} to your. Please try again later or contact support.

@@ -112,13 +139,13 @@ export default async function VerifyTeamTransferPage({

- The ownership of team {team.name} has been successfully transferred to - you. + The ownership of team {data.teamName} has been successfully transferred + to you.

diff --git a/apps/remix/app/routes/_unauthenticated+/verify-email.$token.tsx b/apps/remix/app/routes/_unauthenticated+/verify-email.$token.tsx new file mode 100644 index 000000000..dd937991d --- /dev/null +++ b/apps/remix/app/routes/_unauthenticated+/verify-email.$token.tsx @@ -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(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 ( +
+ +
+ ); + } + + return match(state) + .with(EMAIL_VERIFICATION_STATE.NOT_FOUND, () => ( +
+
+
+ +
+ +
+

+ Something went wrong +

+ +

+ + We were unable to verify your email. If your email is not verified already, please + try again. + +

+ + +
+
+
+ )) + .with(EMAIL_VERIFICATION_STATE.EXPIRED, () => ( +
+
+
+ +
+ +
+

+ Your token has expired! +

+ +

+ + It seems that the provided token has expired. We've just sent you another token, + please check your email and try again. + +

+ + +
+
+
+ )) + .with(EMAIL_VERIFICATION_STATE.VERIFIED, () => ( +
+
+
+ +
+ +
+

+ Email Confirmed! +

+ +

+ + Your email has been successfully confirmed! You can now use all features of + Documenso. + +

+ + +
+
+
+ )) + .with(EMAIL_VERIFICATION_STATE.ALREADY_VERIFIED, () => ( +
+
+
+ +
+ +
+

+ Email already confirmed +

+ +

+ + Your email has already been confirmed. You can now use all features of Documenso. + +

+ + +
+
+
+ )) + .exhaustive(); +} diff --git a/apps/remix/app/_todo/app/(unauthenticated)/verify-email/page.tsx b/apps/remix/app/routes/_unauthenticated+/verify-email.tsx similarity index 74% rename from apps/remix/app/_todo/app/(unauthenticated)/verify-email/page.tsx rename to apps/remix/app/routes/_unauthenticated+/verify-email.tsx index cd518a913..e7c022b4d 100644 --- a/apps/remix/app/_todo/app/(unauthenticated)/verify-email/page.tsx +++ b/apps/remix/app/routes/_unauthenticated+/verify-email.tsx @@ -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 (
@@ -34,7 +29,7 @@ export default async function EmailVerificationWithoutTokenPage() {

diff --git a/apps/remix/server/index.ts b/apps/remix/server/index.ts index 0e3dab546..e2b9261af 100644 --- a/apps/remix/server/index.ts +++ b/apps/remix/server/index.ts @@ -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. diff --git a/apps/remix/server/load-context.ts b/apps/remix/server/load-context.ts index e0d8fabdd..62e0bb51d 100644 --- a/apps/remix/server/load-context.ts +++ b/apps/remix/server/load-context.ts @@ -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( diff --git a/apps/remix/server/node.ts b/apps/remix/server/main.ts similarity index 90% rename from apps/remix/server/node.ts rename to apps/remix/server/main.ts index 42faa28b0..227497daa 100644 --- a/apps/remix/server/node.ts +++ b/apps/remix/server/main.ts @@ -15,4 +15,4 @@ server.use( const handler = handle(build, server, { getLoadContext }); -serve({ fetch: handler.fetch, port: 3010 }); +serve({ fetch: handler.fetch, port: 3000 }); diff --git a/packages/ee/server-only/limits/provider/server.tsx b/packages/ee/server-only/limits/provider/server.tsx deleted file mode 100644 index 969361060..000000000 --- a/packages/ee/server-only/limits/provider/server.tsx +++ /dev/null @@ -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 ( - - {children} - - ); -}; diff --git a/packages/lib/jobs/client/base.ts b/packages/lib/jobs/client/base.ts index d1b6b33f6..40ff7360d 100644 --- a/packages/lib/jobs/client/base.ts +++ b/packages/lib/jobs/client/base.ts @@ -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 { - throw new Error('Not implemented'); - } - - public getHonoApiHandler(): (req: HonoContext) => Promise { + public getApiHandler(): (req: HonoContext) => Promise { throw new Error('Not implemented'); } } diff --git a/packages/lib/jobs/client/client.ts b/packages/lib/jobs/client/client.ts index c76cf20bd..8c9956e34 100644 --- a/packages/lib/jobs/client/client.ts +++ b/packages/lib/jobs/client/client.ts @@ -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 = []> { private _provider: JobClientProvider; @@ -13,7 +12,6 @@ export class JobClient = []> { 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 = []> { public getApiHandler() { return this._provider.getApiHandler(); } - - public getHonoApiHandler() { - return this._provider.getHonoApiHandler(); - } } diff --git a/packages/lib/jobs/client/inngest.ts b/packages/lib/jobs/client/inngest.ts index 1e4be2fa1..ce91cb56a 100644 --- a/packages/lib/jobs/client/inngest.ts +++ b/packages/lib/jobs/client/inngest.ts @@ -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, diff --git a/packages/lib/jobs/client/local.ts b/packages/lib/jobs/client/local.ts index f637530f4..975b9cc03 100644 --- a/packages/lib/jobs/client/local.ts +++ b/packages/lib/jobs/client/local.ts @@ -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 { + public getApiHandler(): (context: HonoContext) => Promise { return async (context: HonoContext) => { const req = context.req; diff --git a/packages/lib/jobs/client/trigger.ts b/packages/lib/jobs/client/trigger.ts deleted file mode 100644 index 73ef12176..000000000 --- a/packages/lib/jobs/client/trigger.ts +++ /dev/null @@ -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(job: JobDefinition): 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 { - 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 { - // 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; - } -} diff --git a/packages/lib/server-only/http/to-next-request.ts b/packages/lib/server-only/http/to-next-request.ts deleted file mode 100644 index 59b6b70c6..000000000 --- a/packages/lib/server-only/http/to-next-request.ts +++ /dev/null @@ -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, - }); -}; diff --git a/packages/lib/server-only/http/with-swr.ts b/packages/lib/server-only/http/with-swr.ts deleted file mode 100644 index 029f2bb6d..000000000 --- a/packages/lib/server-only/http/with-swr.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { NextApiResponse } from 'next'; -import { NextResponse } from 'next/server'; - -type NarrowedResponse = T extends NextResponse - ? NextResponse - : T extends NextApiResponse - ? NextApiResponse - : never; - -export const withStaleWhileRevalidate = ( - res: NarrowedResponse, - 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; -}; diff --git a/packages/lib/universal/extract-request-metadata.ts b/packages/lib/universal/extract-request-metadata.ts index 84ff181d6..a91f75537 100644 --- a/packages/lib/universal/extract-request-metadata.ts +++ b/packages/lib/universal/extract-request-metadata.ts @@ -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, -): RequestMetadata => { - return extractNextHeaderRequestMetadata(req.headers ?? {}); -}; - -export const extractNextHeaderRequestMetadata = ( - headers: Record, -): RequestMetadata => { - const parsedIp = ZIpSchema.safeParse(headers?.['x-forwarded-for']); - - const ipAddress = parsedIp.success ? parsedIp.data : undefined; - const userAgent = headers?.['user-agent']; - - return { - ipAddress, - userAgent, - }; -}; diff --git a/packages/trpc/utils/trpc-error-handler.ts b/packages/trpc/utils/trpc-error-handler.ts index c6da291ad..99fc1e976 100644 --- a/packages/trpc/utils/trpc-error-handler.ts +++ b/packages/trpc/utils/trpc-error-handler.ts @@ -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, '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); diff --git a/packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx b/packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx index ae909d930..9e154469f 100644 --- a/packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx +++ b/packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx @@ -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(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, });