fix: embedded direct template recipient auth

This commit is contained in:
Lucas Smith
2025-10-28 17:02:26 +11:00
parent d6a2f5a4c9
commit 7f19ec1265
13 changed files with 191 additions and 68 deletions

View File

@ -9,6 +9,7 @@ export type EmbedAuthenticationRequiredProps = {
email?: string; email?: string;
returnTo: string; returnTo: string;
isGoogleSSOEnabled?: boolean; isGoogleSSOEnabled?: boolean;
isMicrosoftSSOEnabled?: boolean;
isOIDCSSOEnabled?: boolean; isOIDCSSOEnabled?: boolean;
oidcProviderLabel?: string; oidcProviderLabel?: string;
}; };
@ -17,6 +18,7 @@ export const EmbedAuthenticationRequired = ({
email, email,
returnTo, returnTo,
// isGoogleSSOEnabled, // isGoogleSSOEnabled,
// isMicrosoftSSOEnabled,
// isOIDCSSOEnabled, // isOIDCSSOEnabled,
// oidcProviderLabel, // oidcProviderLabel,
}: EmbedAuthenticationRequiredProps) => { }: EmbedAuthenticationRequiredProps) => {
@ -37,6 +39,7 @@ export const EmbedAuthenticationRequired = ({
<SignInForm <SignInForm
// Embed currently not supported. // Embed currently not supported.
// isGoogleSSOEnabled={isGoogleSSOEnabled} // isGoogleSSOEnabled={isGoogleSSOEnabled}
// isMicrosoftSSOEnabled={isMicrosoftSSOEnabled}
// isOIDCSSOEnabled={isOIDCSSOEnabled} // isOIDCSSOEnabled={isOIDCSSOEnabled}
// oidcProviderLabel={oidcProviderLabel} // oidcProviderLabel={oidcProviderLabel}
className="mt-4" className="mt-4"

View File

@ -92,6 +92,7 @@ export const SignInForm = ({
const [isTwoFactorAuthenticationDialogOpen, setIsTwoFactorAuthenticationDialogOpen] = const [isTwoFactorAuthenticationDialogOpen, setIsTwoFactorAuthenticationDialogOpen] =
useState(false); useState(false);
const [isEmbeddedRedirect, setIsEmbeddedRedirect] = useState(false);
const [twoFactorAuthenticationMethod, setTwoFactorAuthenticationMethod] = useState< const [twoFactorAuthenticationMethod, setTwoFactorAuthenticationMethod] = useState<
'totp' | 'backup' 'totp' | 'backup'
@ -317,6 +318,8 @@ export const SignInForm = ({
if (email) { if (email) {
form.setValue('email', email); form.setValue('email', email);
} }
setIsEmbeddedRedirect(params.get('embedded') === 'true');
}, [form]); }, [form]);
return ( return (
@ -383,56 +386,64 @@ export const SignInForm = ({
{isSubmitting ? <Trans>Signing in...</Trans> : <Trans>Sign In</Trans>} {isSubmitting ? <Trans>Signing in...</Trans> : <Trans>Sign In</Trans>}
</Button> </Button>
{hasSocialAuthEnabled && ( {!isEmbeddedRedirect && (
<div className="relative flex items-center justify-center gap-x-4 py-2 text-xs uppercase"> <>
<div className="bg-border h-px flex-1" /> {hasSocialAuthEnabled && (
<span className="text-muted-foreground bg-transparent"> <div className="relative flex items-center justify-center gap-x-4 py-2 text-xs uppercase">
<Trans>Or continue with</Trans> <div className="bg-border h-px flex-1" />
</span> <span className="text-muted-foreground bg-transparent">
<div className="bg-border h-px flex-1" /> <Trans>Or continue with</Trans>
</div> </span>
)} <div className="bg-border h-px flex-1" />
</div>
)}
{isGoogleSSOEnabled && ( {isGoogleSSOEnabled && (
<Button <Button
type="button" type="button"
size="lg" size="lg"
variant="outline" variant="outline"
className="bg-background text-muted-foreground border" className="bg-background text-muted-foreground border"
disabled={isSubmitting} disabled={isSubmitting}
onClick={onSignInWithGoogleClick} onClick={onSignInWithGoogleClick}
> >
<FcGoogle className="mr-2 h-5 w-5" /> <FcGoogle className="mr-2 h-5 w-5" />
Google Google
</Button> </Button>
)} )}
{isMicrosoftSSOEnabled && ( {isMicrosoftSSOEnabled && (
<Button <Button
type="button" type="button"
size="lg" size="lg"
variant="outline" variant="outline"
className="bg-background text-muted-foreground border" className="bg-background text-muted-foreground border"
disabled={isSubmitting} disabled={isSubmitting}
onClick={onSignInWithMicrosoftClick} onClick={onSignInWithMicrosoftClick}
> >
<img className="mr-2 h-4 w-4" alt="Microsoft Logo" src={'/static/microsoft.svg'} /> <img
Microsoft className="mr-2 h-4 w-4"
</Button> alt="Microsoft Logo"
)} src={'/static/microsoft.svg'}
/>
Microsoft
</Button>
)}
{isOIDCSSOEnabled && ( {isOIDCSSOEnabled && (
<Button <Button
type="button" type="button"
size="lg" size="lg"
variant="outline" variant="outline"
className="bg-background text-muted-foreground border" className="bg-background text-muted-foreground border"
disabled={isSubmitting} disabled={isSubmitting}
onClick={onSignInWithOIDCClick} onClick={onSignInWithOIDCClick}
> >
<FaIdCardClip className="mr-2 h-5 w-5" /> <FaIdCardClip className="mr-2 h-5 w-5" />
{oidcProviderLabel || 'OIDC'} {oidcProviderLabel || 'OIDC'}
</Button> </Button>
)}
</>
)} )}
<Button <Button

View File

@ -68,6 +68,7 @@ export type SignUpFormProps = {
isGoogleSSOEnabled?: boolean; isGoogleSSOEnabled?: boolean;
isMicrosoftSSOEnabled?: boolean; isMicrosoftSSOEnabled?: boolean;
isOIDCSSOEnabled?: boolean; isOIDCSSOEnabled?: boolean;
returnTo?: string;
}; };
export const SignUpForm = ({ export const SignUpForm = ({
@ -76,6 +77,7 @@ export const SignUpForm = ({
isGoogleSSOEnabled, isGoogleSSOEnabled,
isMicrosoftSSOEnabled, isMicrosoftSSOEnabled,
isOIDCSSOEnabled, isOIDCSSOEnabled,
returnTo,
}: SignUpFormProps) => { }: SignUpFormProps) => {
const { _ } = useLingui(); const { _ } = useLingui();
const { toast } = useToast(); const { toast } = useToast();
@ -110,7 +112,7 @@ export const SignUpForm = ({
signature, signature,
}); });
await navigate(`/unverified-account`); await navigate(returnTo ? returnTo : '/unverified-account');
toast({ toast({
title: _(msg`Registration Successful`), title: _(msg`Registration Successful`),

View File

@ -22,7 +22,7 @@ export const DocumentSigningAuthAccount = ({
actionVerb = 'sign', actionVerb = 'sign',
onOpenChange, onOpenChange,
}: DocumentSigningAuthAccountProps) => { }: DocumentSigningAuthAccountProps) => {
const { recipient } = useRequiredDocumentSigningAuthContext(); const { recipient, isDirectTemplate } = useRequiredDocumentSigningAuthContext();
const { t } = useLingui(); const { t } = useLingui();
@ -34,8 +34,10 @@ export const DocumentSigningAuthAccount = ({
try { try {
setIsSigningOut(true); setIsSigningOut(true);
const currentPath = `${window.location.pathname}${window.location.search}${window.location.hash}`;
await authClient.signOut({ await authClient.signOut({
redirectPath: `/signin#email=${email}`, redirectPath: `/signin?returnTo=${encodeURIComponent(currentPath)}#embedded=true&email=${isDirectTemplate ? '' : email}`,
}); });
} catch { } catch {
setIsSigningOut(false); setIsSigningOut(false);
@ -55,16 +57,28 @@ export const DocumentSigningAuthAccount = ({
<AlertDescription> <AlertDescription>
{actionTarget === 'DOCUMENT' && recipient.role === RecipientRole.VIEWER ? ( {actionTarget === 'DOCUMENT' && recipient.role === RecipientRole.VIEWER ? (
<span> <span>
<Trans> {isDirectTemplate ? (
To mark this document as viewed, you need to be logged in as{' '} <Trans>To mark this document as viewed, you need to be logged in.</Trans>
<strong>{recipient.email}</strong> ) : (
</Trans> <Trans>
To mark this document as viewed, you need to be logged in as{' '}
<strong>{recipient.email}</strong>
</Trans>
)}
</span> </span>
) : ( ) : (
<span> <span>
{/* Todo: Translate */} {isDirectTemplate ? (
To {actionVerb.toLowerCase()} this {actionTarget.toLowerCase()}, you need to be logged <Trans>
in as <strong>{recipient.email}</strong> To {actionVerb.toLowerCase()} this {actionTarget.toLowerCase()}, you need to be
logged in.
</Trans>
) : (
<Trans>
To {actionVerb.toLowerCase()} this {actionTarget.toLowerCase()}, you need to be
logged in as <strong>{recipient.email}</strong>
</Trans>
)}
</span> </span>
)} )}
</AlertDescription> </AlertDescription>

View File

@ -47,7 +47,8 @@ export const DocumentSigningAuthDialog = ({
onOpenChange, onOpenChange,
onReauthFormSubmit, onReauthFormSubmit,
}: DocumentSigningAuthDialogProps) => { }: DocumentSigningAuthDialogProps) => {
const { recipient, user, isCurrentlyAuthenticating } = useRequiredDocumentSigningAuthContext(); const { recipient, user, isCurrentlyAuthenticating, isDirectTemplate } =
useRequiredDocumentSigningAuthContext();
// Filter out EXPLICIT_NONE from available auth types for the chooser // Filter out EXPLICIT_NONE from available auth types for the chooser
const validAuthTypes = availableAuthTypes.filter( const validAuthTypes = availableAuthTypes.filter(
@ -168,7 +169,11 @@ export const DocumentSigningAuthDialog = ({
match({ documentAuthType: selectedAuthType, user }) match({ documentAuthType: selectedAuthType, user })
.with( .with(
{ documentAuthType: DocumentAuth.ACCOUNT }, { documentAuthType: DocumentAuth.ACCOUNT },
{ user: P.when((user) => !user || user.email !== recipient.email) }, // Assume all current auth methods requires them to be logged in. {
user: P.when(
(user) => !user || (user.email !== recipient.email && !isDirectTemplate),
),
}, // Assume all current auth methods requires them to be logged in.
() => <DocumentSigningAuthAccount onOpenChange={onOpenChange} />, () => <DocumentSigningAuthAccount onOpenChange={onOpenChange} />,
) )
.with({ documentAuthType: DocumentAuth.PASSKEY }, () => ( .with({ documentAuthType: DocumentAuth.PASSKEY }, () => (

View File

@ -40,6 +40,7 @@ export type DocumentSigningAuthContextValue = {
derivedRecipientAccessAuth: TRecipientAccessAuthTypes[]; derivedRecipientAccessAuth: TRecipientAccessAuthTypes[];
derivedRecipientActionAuth: TRecipientActionAuthTypes[]; derivedRecipientActionAuth: TRecipientActionAuthTypes[];
isAuthRedirectRequired: boolean; isAuthRedirectRequired: boolean;
isDirectTemplate?: boolean;
isCurrentlyAuthenticating: boolean; isCurrentlyAuthenticating: boolean;
setIsCurrentlyAuthenticating: (_value: boolean) => void; setIsCurrentlyAuthenticating: (_value: boolean) => void;
passkeyData: PasskeyData; passkeyData: PasskeyData;
@ -68,6 +69,7 @@ export const useRequiredDocumentSigningAuthContext = () => {
export interface DocumentSigningAuthProviderProps { export interface DocumentSigningAuthProviderProps {
documentAuthOptions: Envelope['authOptions']; documentAuthOptions: Envelope['authOptions'];
recipient: SigningAuthRecipient; recipient: SigningAuthRecipient;
isDirectTemplate?: boolean;
user?: SessionUser | null; user?: SessionUser | null;
children: React.ReactNode; children: React.ReactNode;
} }
@ -75,6 +77,7 @@ export interface DocumentSigningAuthProviderProps {
export const DocumentSigningAuthProvider = ({ export const DocumentSigningAuthProvider = ({
documentAuthOptions: initialDocumentAuthOptions, documentAuthOptions: initialDocumentAuthOptions,
recipient: initialRecipient, recipient: initialRecipient,
isDirectTemplate = false,
user, user,
children, children,
}: DocumentSigningAuthProviderProps) => { }: DocumentSigningAuthProviderProps) => {
@ -204,6 +207,7 @@ export const DocumentSigningAuthProvider = ({
derivedRecipientAccessAuth, derivedRecipientAccessAuth,
derivedRecipientActionAuth, derivedRecipientActionAuth,
isAuthRedirectRequired, isAuthRedirectRequired,
isDirectTemplate,
isCurrentlyAuthenticating, isCurrentlyAuthenticating,
setIsCurrentlyAuthenticating, setIsCurrentlyAuthenticating,
passkeyData, passkeyData,

View File

@ -184,6 +184,7 @@ const DirectSigningPageV1 = ({ data }: { data: Awaited<ReturnType<typeof handleV
<DocumentSigningAuthProvider <DocumentSigningAuthProvider
documentAuthOptions={template.authOptions} documentAuthOptions={template.authOptions}
recipient={directTemplateRecipient} recipient={directTemplateRecipient}
isDirectTemplate={true}
user={user} user={user}
> >
<> <>

View File

@ -1,3 +1,5 @@
import { useEffect, useState } from 'react';
import { Trans } from '@lingui/react/macro'; import { Trans } from '@lingui/react/macro';
import { Link, redirect } from 'react-router'; import { Link, redirect } from 'react-router';
@ -9,6 +11,7 @@ import {
OIDC_PROVIDER_LABEL, OIDC_PROVIDER_LABEL,
} from '@documenso/lib/constants/auth'; } from '@documenso/lib/constants/auth';
import { env } from '@documenso/lib/utils/env'; import { env } from '@documenso/lib/utils/env';
import { isValidReturnTo, normalizeReturnTo } from '@documenso/lib/utils/is-valid-return-to';
import { SignInForm } from '~/components/forms/signin'; import { SignInForm } from '~/components/forms/signin';
import { appMetaTags } from '~/utils/meta'; import { appMetaTags } from '~/utils/meta';
@ -28,8 +31,12 @@ export async function loader({ request }: Route.LoaderArgs) {
const isOIDCSSOEnabled = IS_OIDC_SSO_ENABLED; const isOIDCSSOEnabled = IS_OIDC_SSO_ENABLED;
const oidcProviderLabel = OIDC_PROVIDER_LABEL; const oidcProviderLabel = OIDC_PROVIDER_LABEL;
let returnTo = new URL(request.url).searchParams.get('returnTo') ?? undefined;
returnTo = isValidReturnTo(returnTo) ? normalizeReturnTo(returnTo) : undefined;
if (isAuthenticated) { if (isAuthenticated) {
throw redirect('/'); throw redirect(returnTo || '/');
} }
return { return {
@ -37,12 +44,28 @@ export async function loader({ request }: Route.LoaderArgs) {
isMicrosoftSSOEnabled, isMicrosoftSSOEnabled,
isOIDCSSOEnabled, isOIDCSSOEnabled,
oidcProviderLabel, oidcProviderLabel,
returnTo,
}; };
} }
export default function SignIn({ loaderData }: Route.ComponentProps) { export default function SignIn({ loaderData }: Route.ComponentProps) {
const { isGoogleSSOEnabled, isMicrosoftSSOEnabled, isOIDCSSOEnabled, oidcProviderLabel } = const {
loaderData; isGoogleSSOEnabled,
isMicrosoftSSOEnabled,
isOIDCSSOEnabled,
oidcProviderLabel,
returnTo,
} = loaderData;
const [isEmbeddedRedirect, setIsEmbeddedRedirect] = useState(false);
useEffect(() => {
const hash = window.location.hash.slice(1);
const params = new URLSearchParams(hash);
setIsEmbeddedRedirect(params.get('embedded') === 'true');
}, []);
return ( return (
<div className="w-screen max-w-lg px-4"> <div className="w-screen max-w-lg px-4">
@ -61,13 +84,17 @@ export default function SignIn({ loaderData }: Route.ComponentProps) {
isMicrosoftSSOEnabled={isMicrosoftSSOEnabled} isMicrosoftSSOEnabled={isMicrosoftSSOEnabled}
isOIDCSSOEnabled={isOIDCSSOEnabled} isOIDCSSOEnabled={isOIDCSSOEnabled}
oidcProviderLabel={oidcProviderLabel} oidcProviderLabel={oidcProviderLabel}
returnTo={returnTo}
/> />
{env('NEXT_PUBLIC_DISABLE_SIGNUP') !== 'true' && ( {!isEmbeddedRedirect && env('NEXT_PUBLIC_DISABLE_SIGNUP') !== 'true' && (
<p className="text-muted-foreground mt-6 text-center text-sm"> <p className="text-muted-foreground mt-6 text-center text-sm">
<Trans> <Trans>
Don't have an account?{' '} Don't have an account?{' '}
<Link to="/signup" className="text-documenso-700 duration-200 hover:opacity-70"> <Link
to={returnTo ? `/signup?returnTo=${encodeURIComponent(returnTo)}` : '/signup'}
className="text-documenso-700 duration-200 hover:opacity-70"
>
Sign up Sign up
</Link> </Link>
</Trans> </Trans>

View File

@ -6,6 +6,7 @@ import {
IS_OIDC_SSO_ENABLED, IS_OIDC_SSO_ENABLED,
} from '@documenso/lib/constants/auth'; } from '@documenso/lib/constants/auth';
import { env } from '@documenso/lib/utils/env'; import { env } from '@documenso/lib/utils/env';
import { isValidReturnTo, normalizeReturnTo } from '@documenso/lib/utils/is-valid-return-to';
import { SignUpForm } from '~/components/forms/signup'; import { SignUpForm } from '~/components/forms/signup';
import { appMetaTags } from '~/utils/meta'; import { appMetaTags } from '~/utils/meta';
@ -16,7 +17,7 @@ export function meta() {
return appMetaTags('Sign Up'); return appMetaTags('Sign Up');
} }
export function loader() { export function loader({ request }: Route.LoaderArgs) {
const NEXT_PUBLIC_DISABLE_SIGNUP = env('NEXT_PUBLIC_DISABLE_SIGNUP'); const NEXT_PUBLIC_DISABLE_SIGNUP = env('NEXT_PUBLIC_DISABLE_SIGNUP');
// SSR env variables. // SSR env variables.
@ -28,15 +29,20 @@ export function loader() {
throw redirect('/signin'); throw redirect('/signin');
} }
let returnTo = new URL(request.url).searchParams.get('returnTo') ?? undefined;
returnTo = isValidReturnTo(returnTo) ? normalizeReturnTo(returnTo) : undefined;
return { return {
isGoogleSSOEnabled, isGoogleSSOEnabled,
isMicrosoftSSOEnabled, isMicrosoftSSOEnabled,
isOIDCSSOEnabled, isOIDCSSOEnabled,
returnTo,
}; };
} }
export default function SignUp({ loaderData }: Route.ComponentProps) { export default function SignUp({ loaderData }: Route.ComponentProps) {
const { isGoogleSSOEnabled, isMicrosoftSSOEnabled, isOIDCSSOEnabled } = loaderData; const { isGoogleSSOEnabled, isMicrosoftSSOEnabled, isOIDCSSOEnabled, returnTo } = loaderData;
return ( return (
<SignUpForm <SignUpForm
@ -44,6 +50,7 @@ export default function SignUp({ loaderData }: Route.ComponentProps) {
isGoogleSSOEnabled={isGoogleSSOEnabled} isGoogleSSOEnabled={isGoogleSSOEnabled}
isMicrosoftSSOEnabled={isMicrosoftSSOEnabled} isMicrosoftSSOEnabled={isMicrosoftSSOEnabled}
isOIDCSSOEnabled={isOIDCSSOEnabled} isOIDCSSOEnabled={isOIDCSSOEnabled}
returnTo={returnTo}
/> />
); );
} }

View File

@ -2,6 +2,7 @@ import { Outlet, isRouteErrorResponse, useRouteError } from 'react-router';
import { import {
IS_GOOGLE_SSO_ENABLED, IS_GOOGLE_SSO_ENABLED,
IS_MICROSOFT_SSO_ENABLED,
IS_OIDC_SSO_ENABLED, IS_OIDC_SSO_ENABLED,
OIDC_PROVIDER_LABEL, OIDC_PROVIDER_LABEL,
} from '@documenso/lib/constants/auth'; } from '@documenso/lib/constants/auth';
@ -31,11 +32,13 @@ export function headers({ loaderHeaders }: Route.HeadersArgs) {
export function loader() { export function loader() {
// SSR env variables. // SSR env variables.
const isGoogleSSOEnabled = IS_GOOGLE_SSO_ENABLED; const isGoogleSSOEnabled = IS_GOOGLE_SSO_ENABLED;
const isMicrosoftSSOEnabled = IS_MICROSOFT_SSO_ENABLED;
const isOIDCSSOEnabled = IS_OIDC_SSO_ENABLED; const isOIDCSSOEnabled = IS_OIDC_SSO_ENABLED;
const oidcProviderLabel = OIDC_PROVIDER_LABEL; const oidcProviderLabel = OIDC_PROVIDER_LABEL;
return { return {
isGoogleSSOEnabled, isGoogleSSOEnabled,
isMicrosoftSSOEnabled,
isOIDCSSOEnabled, isOIDCSSOEnabled,
oidcProviderLabel, oidcProviderLabel,
}; };
@ -46,7 +49,8 @@ export default function Layout() {
} }
export function ErrorBoundary({ loaderData }: Route.ErrorBoundaryProps) { export function ErrorBoundary({ loaderData }: Route.ErrorBoundaryProps) {
const { isGoogleSSOEnabled, isOIDCSSOEnabled, oidcProviderLabel } = loaderData || {}; const { isGoogleSSOEnabled, isMicrosoftSSOEnabled, isOIDCSSOEnabled, oidcProviderLabel } =
loaderData || {};
const error = useRouteError(); const error = useRouteError();
@ -57,6 +61,7 @@ export function ErrorBoundary({ loaderData }: Route.ErrorBoundaryProps) {
return ( return (
<EmbedAuthenticationRequired <EmbedAuthenticationRequired
isGoogleSSOEnabled={isGoogleSSOEnabled} isGoogleSSOEnabled={isGoogleSSOEnabled}
isMicrosoftSSOEnabled={isMicrosoftSSOEnabled}
isOIDCSSOEnabled={isOIDCSSOEnabled} isOIDCSSOEnabled={isOIDCSSOEnabled}
oidcProviderLabel={oidcProviderLabel} oidcProviderLabel={oidcProviderLabel}
email={error.data.email} email={error.data.email}

View File

@ -76,7 +76,6 @@ async function handleV1Loader({ params, request }: Route.LoaderArgs) {
throw data( throw data(
{ {
type: 'embed-authentication-required', type: 'embed-authentication-required',
email: user?.email,
returnTo: `/embed/direct/${token}`, returnTo: `/embed/direct/${token}`,
}, },
{ {
@ -319,6 +318,7 @@ const EmbedDirectTemplatePageV2 = ({
documentAuthOptions={envelope.authOptions} documentAuthOptions={envelope.authOptions}
recipient={recipient} recipient={recipient}
user={user} user={user}
isDirectTemplate={true}
> >
<EnvelopeRenderProvider envelope={envelope} token={recipient.token}> <EnvelopeRenderProvider envelope={envelope} token={recipient.token}>
<EmbedSignDocumentV2ClientPage <EmbedSignDocumentV2ClientPage

View File

@ -5,6 +5,7 @@ import { deleteCookie } from 'hono/cookie';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { onCreateUserHook } from '@documenso/lib/server-only/user/create-user'; import { onCreateUserHook } from '@documenso/lib/server-only/user/create-user';
import { isValidReturnTo, normalizeReturnTo } from '@documenso/lib/utils/is-valid-return-to';
import { prisma } from '@documenso/prisma'; import { prisma } from '@documenso/prisma';
import type { OAuthClientOptions } from '../../config'; import type { OAuthClientOptions } from '../../config';
@ -177,6 +178,12 @@ export const validateOauth = async (options: HandleOAuthCallbackUrlOptions) => {
redirectPath = '/'; redirectPath = '/';
} }
if (!isValidReturnTo(redirectPath)) {
redirectPath = '/';
}
redirectPath = normalizeReturnTo(redirectPath) || '/';
const tokens = await oAuthClient.validateAuthorizationCode( const tokens = await oAuthClient.validateAuthorizationCode(
token_endpoint, token_endpoint,
code, code,

View File

@ -0,0 +1,37 @@
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
export const isValidReturnTo = (returnTo?: string) => {
if (!returnTo) {
return false;
}
try {
// Decode if it's URL encoded
const decodedReturnTo = decodeURIComponent(returnTo);
const returnToUrl = new URL(decodedReturnTo, NEXT_PUBLIC_WEBAPP_URL());
if (returnToUrl.origin !== NEXT_PUBLIC_WEBAPP_URL()) {
return false;
}
return true;
} catch {
return false;
}
};
export const normalizeReturnTo = (returnTo?: string) => {
if (!returnTo) {
return undefined;
}
try {
// Decode if it's URL encoded
const decodedReturnTo = decodeURIComponent(returnTo);
const returnToUrl = new URL(decodedReturnTo, NEXT_PUBLIC_WEBAPP_URL());
return `${returnToUrl.pathname}${returnToUrl.search}${returnToUrl.hash}`;
} catch {
return undefined;
}
};