mirror of
https://github.com/documenso/documenso.git
synced 2025-11-10 04:22:32 +10:00
This PR is handles the changes required to support envelopes. The new envelope editor/signing page will be hidden during release. The core changes here is to migrate the documents and templates model to a centralized envelopes model. Even though Documents and Templates are removed, from the user perspective they will still exist as we remap envelopes to documents and templates.
233 lines
7.3 KiB
TypeScript
233 lines
7.3 KiB
TypeScript
import { createContext, useContext, useEffect, useMemo, useState } from 'react';
|
|
|
|
import { type Envelope, FieldType, type Passkey, type Recipient } from '@prisma/client';
|
|
|
|
import type { SessionUser } from '@documenso/auth/server/lib/session/session';
|
|
import { MAXIMUM_PASSKEYS } from '@documenso/lib/constants/auth';
|
|
import type {
|
|
TDocumentAuthOptions,
|
|
TRecipientAccessAuthTypes,
|
|
TRecipientActionAuthTypes,
|
|
TRecipientAuthOptions,
|
|
} from '@documenso/lib/types/document-auth';
|
|
import { DocumentAuth } from '@documenso/lib/types/document-auth';
|
|
import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
|
|
import { trpc } from '@documenso/trpc/react';
|
|
|
|
import type { DocumentSigningAuthDialogProps } from './document-signing-auth-dialog';
|
|
import { DocumentSigningAuthDialog } from './document-signing-auth-dialog';
|
|
|
|
type PasskeyData = {
|
|
passkeys: Omit<Passkey, 'credentialId' | 'credentialPublicKey'>[];
|
|
isInitialLoading: boolean;
|
|
isRefetching: boolean;
|
|
isError: boolean;
|
|
};
|
|
|
|
type SigningAuthRecipient = Pick<Recipient, 'authOptions' | 'email' | 'role' | 'name' | 'token'>;
|
|
|
|
export type DocumentSigningAuthContextValue = {
|
|
executeActionAuthProcedure: (_value: ExecuteActionAuthProcedureOptions) => Promise<void>;
|
|
documentAuthOptions: Envelope['authOptions'];
|
|
documentAuthOption: TDocumentAuthOptions;
|
|
setDocumentAuthOptions: (_value: Envelope['authOptions']) => void;
|
|
recipient: SigningAuthRecipient;
|
|
recipientAuthOption: TRecipientAuthOptions;
|
|
setRecipient: (_value: SigningAuthRecipient) => void;
|
|
derivedRecipientAccessAuth: TRecipientAccessAuthTypes[];
|
|
derivedRecipientActionAuth: TRecipientActionAuthTypes[];
|
|
isAuthRedirectRequired: boolean;
|
|
isCurrentlyAuthenticating: boolean;
|
|
setIsCurrentlyAuthenticating: (_value: boolean) => void;
|
|
passkeyData: PasskeyData;
|
|
preferredPasskeyId: string | null;
|
|
setPreferredPasskeyId: (_value: string | null) => void;
|
|
user?: SessionUser | null;
|
|
refetchPasskeys: () => Promise<void>;
|
|
};
|
|
|
|
const DocumentSigningAuthContext = createContext<DocumentSigningAuthContextValue | null>(null);
|
|
|
|
export const useDocumentSigningAuthContext = () => {
|
|
return useContext(DocumentSigningAuthContext);
|
|
};
|
|
|
|
export const useRequiredDocumentSigningAuthContext = () => {
|
|
const context = useDocumentSigningAuthContext();
|
|
|
|
if (!context) {
|
|
throw new Error('Document signing auth context is required');
|
|
}
|
|
|
|
return context;
|
|
};
|
|
|
|
export interface DocumentSigningAuthProviderProps {
|
|
documentAuthOptions: Envelope['authOptions'];
|
|
recipient: SigningAuthRecipient;
|
|
user?: SessionUser | null;
|
|
children: React.ReactNode;
|
|
}
|
|
|
|
export const DocumentSigningAuthProvider = ({
|
|
documentAuthOptions: initialDocumentAuthOptions,
|
|
recipient: initialRecipient,
|
|
user,
|
|
children,
|
|
}: DocumentSigningAuthProviderProps) => {
|
|
const [documentAuthOptions, setDocumentAuthOptions] = useState(initialDocumentAuthOptions);
|
|
const [recipient, setRecipient] = useState(initialRecipient);
|
|
|
|
const [isCurrentlyAuthenticating, setIsCurrentlyAuthenticating] = useState(false);
|
|
const [preferredPasskeyId, setPreferredPasskeyId] = useState<string | null>(null);
|
|
|
|
const {
|
|
documentAuthOption,
|
|
recipientAuthOption,
|
|
derivedRecipientAccessAuth,
|
|
derivedRecipientActionAuth,
|
|
} = useMemo(
|
|
() =>
|
|
extractDocumentAuthMethods({
|
|
documentAuth: documentAuthOptions,
|
|
recipientAuth: recipient.authOptions,
|
|
}),
|
|
[documentAuthOptions, recipient],
|
|
);
|
|
|
|
const passkeyQuery = trpc.auth.passkey.find.useQuery(
|
|
{
|
|
perPage: MAXIMUM_PASSKEYS,
|
|
},
|
|
{
|
|
placeholderData: (previousData) => previousData,
|
|
enabled: derivedRecipientActionAuth?.includes(DocumentAuth.PASSKEY) ?? false,
|
|
},
|
|
);
|
|
|
|
const passkeyData: PasskeyData = {
|
|
passkeys: passkeyQuery.data?.data || [],
|
|
isInitialLoading: passkeyQuery.isInitialLoading,
|
|
isRefetching: passkeyQuery.isRefetching,
|
|
isError: passkeyQuery.isError,
|
|
};
|
|
|
|
const [documentAuthDialogPayload, setDocumentAuthDialogPayload] =
|
|
useState<ExecuteActionAuthProcedureOptions | null>(null);
|
|
|
|
/**
|
|
* The pre calculated auth payload if the current user is authenticated correctly
|
|
* for the `derivedRecipientActionAuth`.
|
|
*
|
|
* Will be `null` if the user still requires authentication, or if they don't need
|
|
* authentication.
|
|
*/
|
|
const preCalculatedActionAuthOptions = useMemo(() => {
|
|
if (
|
|
!derivedRecipientActionAuth ||
|
|
derivedRecipientActionAuth.length === 0 ||
|
|
derivedRecipientActionAuth.includes(DocumentAuth.EXPLICIT_NONE)
|
|
) {
|
|
return {
|
|
type: DocumentAuth.EXPLICIT_NONE,
|
|
};
|
|
}
|
|
|
|
if (
|
|
derivedRecipientActionAuth.includes(DocumentAuth.ACCOUNT) &&
|
|
user?.email == recipient.email
|
|
) {
|
|
return {
|
|
type: DocumentAuth.ACCOUNT,
|
|
};
|
|
}
|
|
|
|
return null;
|
|
}, [derivedRecipientActionAuth, user, recipient]);
|
|
|
|
const executeActionAuthProcedure = async (options: ExecuteActionAuthProcedureOptions) => {
|
|
// Directly run callback if no auth required.
|
|
if (!derivedRecipientActionAuth || options.actionTarget !== FieldType.SIGNATURE) {
|
|
await options.onReauthFormSubmit();
|
|
return;
|
|
}
|
|
|
|
// Run callback with precalculated auth options if available.
|
|
if (preCalculatedActionAuthOptions) {
|
|
setDocumentAuthDialogPayload(null);
|
|
await options.onReauthFormSubmit(preCalculatedActionAuthOptions);
|
|
return;
|
|
}
|
|
|
|
// Request the required auth from the user.
|
|
setDocumentAuthDialogPayload({
|
|
...options,
|
|
});
|
|
};
|
|
|
|
useEffect(() => {
|
|
const { passkeys } = passkeyData;
|
|
|
|
if (!preferredPasskeyId && passkeys.length > 0) {
|
|
setPreferredPasskeyId(passkeys[0].id);
|
|
}
|
|
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [passkeyData.passkeys]);
|
|
|
|
// Assume that a user must be logged in for any auth requirements.
|
|
const isAuthRedirectRequired = Boolean(
|
|
derivedRecipientActionAuth &&
|
|
derivedRecipientActionAuth.length > 0 &&
|
|
!derivedRecipientActionAuth.includes(DocumentAuth.EXPLICIT_NONE) &&
|
|
user?.email !== recipient.email,
|
|
);
|
|
|
|
const refetchPasskeys = async () => {
|
|
await passkeyQuery.refetch();
|
|
};
|
|
|
|
return (
|
|
<DocumentSigningAuthContext.Provider
|
|
value={{
|
|
user,
|
|
documentAuthOptions,
|
|
setDocumentAuthOptions,
|
|
executeActionAuthProcedure,
|
|
recipient,
|
|
setRecipient,
|
|
documentAuthOption,
|
|
recipientAuthOption,
|
|
derivedRecipientAccessAuth,
|
|
derivedRecipientActionAuth,
|
|
isAuthRedirectRequired,
|
|
isCurrentlyAuthenticating,
|
|
setIsCurrentlyAuthenticating,
|
|
passkeyData,
|
|
preferredPasskeyId,
|
|
setPreferredPasskeyId,
|
|
refetchPasskeys,
|
|
}}
|
|
>
|
|
{children}
|
|
|
|
{documentAuthDialogPayload && derivedRecipientActionAuth && (
|
|
<DocumentSigningAuthDialog
|
|
open={true}
|
|
onOpenChange={() => setDocumentAuthDialogPayload(null)}
|
|
onReauthFormSubmit={documentAuthDialogPayload.onReauthFormSubmit}
|
|
actionTarget={documentAuthDialogPayload.actionTarget}
|
|
availableAuthTypes={derivedRecipientActionAuth}
|
|
/>
|
|
)}
|
|
</DocumentSigningAuthContext.Provider>
|
|
);
|
|
};
|
|
|
|
type ExecuteActionAuthProcedureOptions = Omit<
|
|
DocumentSigningAuthDialogProps,
|
|
'open' | 'onOpenChange' | 'documentAuthType' | 'recipientRole' | 'availableAuthTypes'
|
|
>;
|
|
|
|
DocumentSigningAuthProvider.displayName = 'DocumentSigningAuthProvider';
|