mirror of
https://github.com/documenso/documenso.git
synced 2025-11-13 16:23:06 +10:00
feat: password reauthentication for documents and recipients (#1827)
Adds password reauthentication to our existing reauth providers, additionally swaps from an exclusive provider to an inclusive type where multiple methods can be selected to offer a this or that experience.
This commit is contained in:
@ -130,7 +130,7 @@ export const DirectTemplateConfigureForm = ({
|
|||||||
{...field}
|
{...field}
|
||||||
disabled={
|
disabled={
|
||||||
field.disabled ||
|
field.disabled ||
|
||||||
derivedRecipientAccessAuth !== null ||
|
derivedRecipientAccessAuth.length > 0 ||
|
||||||
user?.email !== undefined
|
user?.email !== undefined
|
||||||
}
|
}
|
||||||
placeholder="recipient@documenso.com"
|
placeholder="recipient@documenso.com"
|
||||||
|
|||||||
@ -1,5 +1,8 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
import { Trans } from '@lingui/react/macro';
|
import { Trans } from '@lingui/react/macro';
|
||||||
import type { FieldType } from '@prisma/client';
|
import type { FieldType } from '@prisma/client';
|
||||||
|
import { ChevronLeftIcon } from 'lucide-react';
|
||||||
import { P, match } from 'ts-pattern';
|
import { P, match } from 'ts-pattern';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -7,6 +10,7 @@ import {
|
|||||||
type TRecipientActionAuth,
|
type TRecipientActionAuth,
|
||||||
type TRecipientActionAuthTypes,
|
type TRecipientActionAuthTypes,
|
||||||
} from '@documenso/lib/types/document-auth';
|
} from '@documenso/lib/types/document-auth';
|
||||||
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@ -18,11 +22,12 @@ import {
|
|||||||
import { DocumentSigningAuth2FA } from './document-signing-auth-2fa';
|
import { DocumentSigningAuth2FA } from './document-signing-auth-2fa';
|
||||||
import { DocumentSigningAuthAccount } from './document-signing-auth-account';
|
import { DocumentSigningAuthAccount } from './document-signing-auth-account';
|
||||||
import { DocumentSigningAuthPasskey } from './document-signing-auth-passkey';
|
import { DocumentSigningAuthPasskey } from './document-signing-auth-passkey';
|
||||||
|
import { DocumentSigningAuthPassword } from './document-signing-auth-password';
|
||||||
import { useRequiredDocumentSigningAuthContext } from './document-signing-auth-provider';
|
import { useRequiredDocumentSigningAuthContext } from './document-signing-auth-provider';
|
||||||
|
|
||||||
export type DocumentSigningAuthDialogProps = {
|
export type DocumentSigningAuthDialogProps = {
|
||||||
title?: string;
|
title?: string;
|
||||||
documentAuthType: TRecipientActionAuthTypes;
|
availableAuthTypes: TRecipientActionAuthTypes[];
|
||||||
description?: string;
|
description?: string;
|
||||||
actionTarget: FieldType | 'DOCUMENT';
|
actionTarget: FieldType | 'DOCUMENT';
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@ -37,54 +42,158 @@ export type DocumentSigningAuthDialogProps = {
|
|||||||
export const DocumentSigningAuthDialog = ({
|
export const DocumentSigningAuthDialog = ({
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
documentAuthType,
|
availableAuthTypes,
|
||||||
open,
|
open,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
onReauthFormSubmit,
|
onReauthFormSubmit,
|
||||||
}: DocumentSigningAuthDialogProps) => {
|
}: DocumentSigningAuthDialogProps) => {
|
||||||
const { recipient, user, isCurrentlyAuthenticating } = useRequiredDocumentSigningAuthContext();
|
const { recipient, user, isCurrentlyAuthenticating } = useRequiredDocumentSigningAuthContext();
|
||||||
|
|
||||||
|
// Filter out EXPLICIT_NONE from available auth types for the chooser
|
||||||
|
const validAuthTypes = availableAuthTypes.filter(
|
||||||
|
(authType) => authType !== DocumentAuth.EXPLICIT_NONE,
|
||||||
|
);
|
||||||
|
|
||||||
|
const [selectedAuthType, setSelectedAuthType] = useState<TRecipientActionAuthTypes | null>(() => {
|
||||||
|
// Auto-select if there's only one valid option
|
||||||
|
if (validAuthTypes.length === 1) {
|
||||||
|
return validAuthTypes[0];
|
||||||
|
}
|
||||||
|
// Return null if multiple options - show chooser
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
const handleOnOpenChange = (value: boolean) => {
|
const handleOnOpenChange = (value: boolean) => {
|
||||||
if (isCurrentlyAuthenticating) {
|
if (isCurrentlyAuthenticating) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reset selected auth type when dialog closes
|
||||||
|
if (!value) {
|
||||||
|
setSelectedAuthType(() => {
|
||||||
|
if (validAuthTypes.length === 1) {
|
||||||
|
return validAuthTypes[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
onOpenChange(value);
|
onOpenChange(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleBackToChooser = () => {
|
||||||
|
setSelectedAuthType(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
// If no valid auth types available, don't render anything
|
||||||
|
if (validAuthTypes.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={handleOnOpenChange}>
|
<Dialog open={open} onOpenChange={handleOnOpenChange}>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>{title || <Trans>Sign field</Trans>}</DialogTitle>
|
<DialogTitle>
|
||||||
|
{selectedAuthType && validAuthTypes.length > 1 && (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleBackToChooser}
|
||||||
|
className="h-6 w-6 p-0"
|
||||||
|
>
|
||||||
|
<ChevronLeftIcon className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<span>{title || <Trans>Sign field</Trans>}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{(!selectedAuthType || validAuthTypes.length === 1) &&
|
||||||
|
(title || <Trans>Sign field</Trans>)}
|
||||||
|
</DialogTitle>
|
||||||
|
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
{description || <Trans>Reauthentication is required to sign this field</Trans>}
|
{description || <Trans>Reauthentication is required to sign this field</Trans>}
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
{match({ documentAuthType, user })
|
{/* Show chooser if no auth type is selected and there are multiple options */}
|
||||||
.with(
|
{!selectedAuthType && validAuthTypes.length > 1 && (
|
||||||
{ documentAuthType: DocumentAuth.ACCOUNT },
|
<div className="space-y-4">
|
||||||
{ user: P.when((user) => !user || user.email !== recipient.email) }, // Assume all current auth methods requires them to be logged in.
|
<p className="text-muted-foreground text-sm">
|
||||||
() => <DocumentSigningAuthAccount onOpenChange={onOpenChange} />,
|
<Trans>Choose your preferred authentication method:</Trans>
|
||||||
)
|
</p>
|
||||||
.with({ documentAuthType: DocumentAuth.PASSKEY }, () => (
|
<div className="grid gap-2">
|
||||||
<DocumentSigningAuthPasskey
|
{validAuthTypes.map((authType) => (
|
||||||
open={open}
|
<Button
|
||||||
onOpenChange={onOpenChange}
|
key={authType}
|
||||||
onReauthFormSubmit={onReauthFormSubmit}
|
type="button"
|
||||||
/>
|
variant="outline"
|
||||||
))
|
className="h-auto justify-start p-4"
|
||||||
.with({ documentAuthType: DocumentAuth.TWO_FACTOR_AUTH }, () => (
|
onClick={() => setSelectedAuthType(authType)}
|
||||||
<DocumentSigningAuth2FA
|
>
|
||||||
open={open}
|
<div className="text-left">
|
||||||
onOpenChange={onOpenChange}
|
<div className="font-medium">
|
||||||
onReauthFormSubmit={onReauthFormSubmit}
|
{match(authType)
|
||||||
/>
|
.with(DocumentAuth.ACCOUNT, () => <Trans>Account</Trans>)
|
||||||
))
|
.with(DocumentAuth.PASSKEY, () => <Trans>Passkey</Trans>)
|
||||||
.with({ documentAuthType: DocumentAuth.EXPLICIT_NONE }, () => null)
|
.with(DocumentAuth.TWO_FACTOR_AUTH, () => <Trans>2FA</Trans>)
|
||||||
.exhaustive()}
|
.with(DocumentAuth.PASSWORD, () => <Trans>Password</Trans>)
|
||||||
|
.exhaustive()}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-muted-foreground text-sm">
|
||||||
|
{match(authType)
|
||||||
|
.with(DocumentAuth.ACCOUNT, () => <Trans>Sign in to your account</Trans>)
|
||||||
|
.with(DocumentAuth.PASSKEY, () => (
|
||||||
|
<Trans>Use your passkey for authentication</Trans>
|
||||||
|
))
|
||||||
|
.with(DocumentAuth.TWO_FACTOR_AUTH, () => (
|
||||||
|
<Trans>Enter your 2FA code</Trans>
|
||||||
|
))
|
||||||
|
.with(DocumentAuth.PASSWORD, () => <Trans>Enter your password</Trans>)
|
||||||
|
.exhaustive()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Show the selected auth component */}
|
||||||
|
{selectedAuthType &&
|
||||||
|
match({ documentAuthType: selectedAuthType, user })
|
||||||
|
.with(
|
||||||
|
{ documentAuthType: DocumentAuth.ACCOUNT },
|
||||||
|
{ user: P.when((user) => !user || user.email !== recipient.email) }, // Assume all current auth methods requires them to be logged in.
|
||||||
|
() => <DocumentSigningAuthAccount onOpenChange={onOpenChange} />,
|
||||||
|
)
|
||||||
|
.with({ documentAuthType: DocumentAuth.PASSKEY }, () => (
|
||||||
|
<DocumentSigningAuthPasskey
|
||||||
|
open={open}
|
||||||
|
onOpenChange={onOpenChange}
|
||||||
|
onReauthFormSubmit={onReauthFormSubmit}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
.with({ documentAuthType: DocumentAuth.TWO_FACTOR_AUTH }, () => (
|
||||||
|
<DocumentSigningAuth2FA
|
||||||
|
open={open}
|
||||||
|
onOpenChange={onOpenChange}
|
||||||
|
onReauthFormSubmit={onReauthFormSubmit}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
.with({ documentAuthType: DocumentAuth.PASSWORD }, () => (
|
||||||
|
<DocumentSigningAuthPassword
|
||||||
|
open={open}
|
||||||
|
onOpenChange={onOpenChange}
|
||||||
|
onReauthFormSubmit={onReauthFormSubmit}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
.with({ documentAuthType: DocumentAuth.EXPLICIT_NONE }, () => null)
|
||||||
|
.exhaustive()}
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -0,0 +1,148 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { AppError } from '@documenso/lib/errors/app-error';
|
||||||
|
import { DocumentAuth, type TRecipientActionAuth } from '@documenso/lib/types/document-auth';
|
||||||
|
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
|
||||||
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
|
import { DialogFooter } from '@documenso/ui/primitives/dialog';
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from '@documenso/ui/primitives/form/form';
|
||||||
|
import { Input } from '@documenso/ui/primitives/input';
|
||||||
|
|
||||||
|
import { useRequiredDocumentSigningAuthContext } from './document-signing-auth-provider';
|
||||||
|
|
||||||
|
export type DocumentSigningAuthPasswordProps = {
|
||||||
|
actionTarget?: 'FIELD' | 'DOCUMENT';
|
||||||
|
actionVerb?: string;
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (value: boolean) => void;
|
||||||
|
onReauthFormSubmit: (values?: TRecipientActionAuth) => Promise<void> | void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ZPasswordAuthFormSchema = z.object({
|
||||||
|
password: z
|
||||||
|
.string()
|
||||||
|
.min(1, { message: 'Password is required' })
|
||||||
|
.max(72, { message: 'Password must be at most 72 characters long' }),
|
||||||
|
});
|
||||||
|
|
||||||
|
type TPasswordAuthFormSchema = z.infer<typeof ZPasswordAuthFormSchema>;
|
||||||
|
|
||||||
|
export const DocumentSigningAuthPassword = ({
|
||||||
|
actionTarget = 'FIELD',
|
||||||
|
actionVerb = 'sign',
|
||||||
|
onReauthFormSubmit,
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
}: DocumentSigningAuthPasswordProps) => {
|
||||||
|
const { recipient, isCurrentlyAuthenticating, setIsCurrentlyAuthenticating } =
|
||||||
|
useRequiredDocumentSigningAuthContext();
|
||||||
|
|
||||||
|
const form = useForm<TPasswordAuthFormSchema>({
|
||||||
|
resolver: zodResolver(ZPasswordAuthFormSchema),
|
||||||
|
defaultValues: {
|
||||||
|
password: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const [formErrorCode, setFormErrorCode] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const onFormSubmit = async ({ password }: TPasswordAuthFormSchema) => {
|
||||||
|
try {
|
||||||
|
setIsCurrentlyAuthenticating(true);
|
||||||
|
|
||||||
|
await onReauthFormSubmit({
|
||||||
|
type: DocumentAuth.PASSWORD,
|
||||||
|
password,
|
||||||
|
});
|
||||||
|
|
||||||
|
setIsCurrentlyAuthenticating(false);
|
||||||
|
|
||||||
|
onOpenChange(false);
|
||||||
|
} catch (err) {
|
||||||
|
setIsCurrentlyAuthenticating(false);
|
||||||
|
|
||||||
|
const error = AppError.parseError(err);
|
||||||
|
setFormErrorCode(error.code);
|
||||||
|
|
||||||
|
// Todo: Alert.
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
form.reset({
|
||||||
|
password: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
setFormErrorCode(null);
|
||||||
|
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [open]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form {...form}>
|
||||||
|
<form onSubmit={form.handleSubmit(onFormSubmit)}>
|
||||||
|
<fieldset disabled={isCurrentlyAuthenticating}>
|
||||||
|
<div className="space-y-4">
|
||||||
|
{formErrorCode && (
|
||||||
|
<Alert variant="destructive">
|
||||||
|
<AlertTitle>
|
||||||
|
<Trans>Unauthorized</Trans>
|
||||||
|
</AlertTitle>
|
||||||
|
<AlertDescription>
|
||||||
|
<Trans>
|
||||||
|
We were unable to verify your details. Please try again or contact support
|
||||||
|
</Trans>
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="password"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel required>
|
||||||
|
<Trans>Password</Trans>
|
||||||
|
</FormLabel>
|
||||||
|
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
placeholder="Enter your password"
|
||||||
|
{...field}
|
||||||
|
autoComplete="current-password"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button type="button" variant="secondary" onClick={() => onOpenChange(false)}>
|
||||||
|
<Trans>Cancel</Trans>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button type="submit" loading={isCurrentlyAuthenticating}>
|
||||||
|
<Trans>Sign</Trans>
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -1,7 +1,6 @@
|
|||||||
import { createContext, useContext, useEffect, useMemo, useState } from 'react';
|
import { createContext, useContext, useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import { type Document, FieldType, type Passkey, type Recipient } from '@prisma/client';
|
import { type Document, FieldType, type Passkey, type Recipient } from '@prisma/client';
|
||||||
import { match } from 'ts-pattern';
|
|
||||||
|
|
||||||
import type { SessionUser } from '@documenso/auth/server/lib/session/session';
|
import type { SessionUser } from '@documenso/auth/server/lib/session/session';
|
||||||
import { MAXIMUM_PASSKEYS } from '@documenso/lib/constants/auth';
|
import { MAXIMUM_PASSKEYS } from '@documenso/lib/constants/auth';
|
||||||
@ -33,8 +32,8 @@ export type DocumentSigningAuthContextValue = {
|
|||||||
recipient: Recipient;
|
recipient: Recipient;
|
||||||
recipientAuthOption: TRecipientAuthOptions;
|
recipientAuthOption: TRecipientAuthOptions;
|
||||||
setRecipient: (_value: Recipient) => void;
|
setRecipient: (_value: Recipient) => void;
|
||||||
derivedRecipientAccessAuth: TRecipientAccessAuthTypes | null;
|
derivedRecipientAccessAuth: TRecipientAccessAuthTypes[];
|
||||||
derivedRecipientActionAuth: TRecipientActionAuthTypes | null;
|
derivedRecipientActionAuth: TRecipientActionAuthTypes[];
|
||||||
isAuthRedirectRequired: boolean;
|
isAuthRedirectRequired: boolean;
|
||||||
isCurrentlyAuthenticating: boolean;
|
isCurrentlyAuthenticating: boolean;
|
||||||
setIsCurrentlyAuthenticating: (_value: boolean) => void;
|
setIsCurrentlyAuthenticating: (_value: boolean) => void;
|
||||||
@ -100,7 +99,7 @@ export const DocumentSigningAuthProvider = ({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
placeholderData: (previousData) => previousData,
|
placeholderData: (previousData) => previousData,
|
||||||
enabled: derivedRecipientActionAuth === DocumentAuth.PASSKEY,
|
enabled: derivedRecipientActionAuth?.includes(DocumentAuth.PASSKEY) ?? false,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -121,21 +120,28 @@ export const DocumentSigningAuthProvider = ({
|
|||||||
* Will be `null` if the user still requires authentication, or if they don't need
|
* Will be `null` if the user still requires authentication, or if they don't need
|
||||||
* authentication.
|
* authentication.
|
||||||
*/
|
*/
|
||||||
const preCalculatedActionAuthOptions = match(derivedRecipientActionAuth)
|
const preCalculatedActionAuthOptions = useMemo(() => {
|
||||||
.with(DocumentAuth.ACCOUNT, () => {
|
if (
|
||||||
if (recipient.email !== user?.email) {
|
!derivedRecipientActionAuth ||
|
||||||
return null;
|
derivedRecipientActionAuth.length === 0 ||
|
||||||
}
|
derivedRecipientActionAuth.includes(DocumentAuth.EXPLICIT_NONE)
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
type: DocumentAuth.EXPLICIT_NONE,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
derivedRecipientActionAuth.includes(DocumentAuth.ACCOUNT) &&
|
||||||
|
user?.email == recipient.email
|
||||||
|
) {
|
||||||
return {
|
return {
|
||||||
type: DocumentAuth.ACCOUNT,
|
type: DocumentAuth.ACCOUNT,
|
||||||
};
|
};
|
||||||
})
|
}
|
||||||
.with(DocumentAuth.EXPLICIT_NONE, () => ({
|
|
||||||
type: DocumentAuth.EXPLICIT_NONE,
|
return null;
|
||||||
}))
|
}, [derivedRecipientActionAuth, user, recipient]);
|
||||||
.with(DocumentAuth.PASSKEY, DocumentAuth.TWO_FACTOR_AUTH, null, () => null)
|
|
||||||
.exhaustive();
|
|
||||||
|
|
||||||
const executeActionAuthProcedure = async (options: ExecuteActionAuthProcedureOptions) => {
|
const executeActionAuthProcedure = async (options: ExecuteActionAuthProcedureOptions) => {
|
||||||
// Directly run callback if no auth required.
|
// Directly run callback if no auth required.
|
||||||
@ -170,7 +176,8 @@ export const DocumentSigningAuthProvider = ({
|
|||||||
// Assume that a user must be logged in for any auth requirements.
|
// Assume that a user must be logged in for any auth requirements.
|
||||||
const isAuthRedirectRequired = Boolean(
|
const isAuthRedirectRequired = Boolean(
|
||||||
derivedRecipientActionAuth &&
|
derivedRecipientActionAuth &&
|
||||||
derivedRecipientActionAuth !== DocumentAuth.EXPLICIT_NONE &&
|
derivedRecipientActionAuth.length > 0 &&
|
||||||
|
!derivedRecipientActionAuth.includes(DocumentAuth.EXPLICIT_NONE) &&
|
||||||
user?.email !== recipient.email,
|
user?.email !== recipient.email,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -208,7 +215,7 @@ export const DocumentSigningAuthProvider = ({
|
|||||||
onOpenChange={() => setDocumentAuthDialogPayload(null)}
|
onOpenChange={() => setDocumentAuthDialogPayload(null)}
|
||||||
onReauthFormSubmit={documentAuthDialogPayload.onReauthFormSubmit}
|
onReauthFormSubmit={documentAuthDialogPayload.onReauthFormSubmit}
|
||||||
actionTarget={documentAuthDialogPayload.actionTarget}
|
actionTarget={documentAuthDialogPayload.actionTarget}
|
||||||
documentAuthType={derivedRecipientActionAuth}
|
availableAuthTypes={derivedRecipientActionAuth}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</DocumentSigningAuthContext.Provider>
|
</DocumentSigningAuthContext.Provider>
|
||||||
@ -217,7 +224,7 @@ export const DocumentSigningAuthProvider = ({
|
|||||||
|
|
||||||
type ExecuteActionAuthProcedureOptions = Omit<
|
type ExecuteActionAuthProcedureOptions = Omit<
|
||||||
DocumentSigningAuthDialogProps,
|
DocumentSigningAuthDialogProps,
|
||||||
'open' | 'onOpenChange' | 'documentAuthType' | 'recipientRole'
|
'open' | 'onOpenChange' | 'documentAuthType' | 'recipientRole' | 'availableAuthTypes'
|
||||||
>;
|
>;
|
||||||
|
|
||||||
DocumentSigningAuthProvider.displayName = 'DocumentSigningAuthProvider';
|
DocumentSigningAuthProvider.displayName = 'DocumentSigningAuthProvider';
|
||||||
|
|||||||
@ -44,6 +44,7 @@ const AUTO_SIGNABLE_FIELD_TYPES: string[] = [
|
|||||||
// other field types.
|
// other field types.
|
||||||
const NON_AUTO_SIGNABLE_ACTION_AUTH_TYPES: string[] = [
|
const NON_AUTO_SIGNABLE_ACTION_AUTH_TYPES: string[] = [
|
||||||
DocumentAuth.PASSKEY,
|
DocumentAuth.PASSKEY,
|
||||||
|
DocumentAuth.PASSWORD,
|
||||||
DocumentAuth.TWO_FACTOR_AUTH,
|
DocumentAuth.TWO_FACTOR_AUTH,
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -96,8 +97,8 @@ export const DocumentSigningAutoSign = ({ recipient, fields }: DocumentSigningAu
|
|||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
const actionAuthAllowsAutoSign = !NON_AUTO_SIGNABLE_ACTION_AUTH_TYPES.includes(
|
const actionAuthAllowsAutoSign = derivedRecipientActionAuth.every(
|
||||||
derivedRecipientActionAuth ?? '',
|
(actionAuth) => !NON_AUTO_SIGNABLE_ACTION_AUTH_TYPES.includes(actionAuth),
|
||||||
);
|
);
|
||||||
|
|
||||||
const onSubmit = async () => {
|
const onSubmit = async () => {
|
||||||
@ -110,16 +111,16 @@ export const DocumentSigningAutoSign = ({ recipient, fields }: DocumentSigningAu
|
|||||||
.with(FieldType.DATE, () => new Date().toISOString())
|
.with(FieldType.DATE, () => new Date().toISOString())
|
||||||
.otherwise(() => '');
|
.otherwise(() => '');
|
||||||
|
|
||||||
const authOptions = match(derivedRecipientActionAuth)
|
const authOptions = match(derivedRecipientActionAuth.at(0))
|
||||||
.with(DocumentAuth.ACCOUNT, () => ({
|
.with(DocumentAuth.ACCOUNT, () => ({
|
||||||
type: DocumentAuth.ACCOUNT,
|
type: DocumentAuth.ACCOUNT,
|
||||||
}))
|
}))
|
||||||
.with(DocumentAuth.EXPLICIT_NONE, () => ({
|
.with(DocumentAuth.EXPLICIT_NONE, () => ({
|
||||||
type: DocumentAuth.EXPLICIT_NONE,
|
type: DocumentAuth.EXPLICIT_NONE,
|
||||||
}))
|
}))
|
||||||
.with(null, () => undefined)
|
.with(undefined, () => undefined)
|
||||||
.with(
|
.with(
|
||||||
P.union(DocumentAuth.PASSKEY, DocumentAuth.TWO_FACTOR_AUTH),
|
P.union(DocumentAuth.PASSKEY, DocumentAuth.TWO_FACTOR_AUTH, DocumentAuth.PASSWORD),
|
||||||
// This is a bit dirty, but the sentinel value used here is incredibly short-lived.
|
// This is a bit dirty, but the sentinel value used here is incredibly short-lived.
|
||||||
() => 'NOT_SUPPORTED' as const,
|
() => 'NOT_SUPPORTED' as const,
|
||||||
)
|
)
|
||||||
|
|||||||
@ -92,8 +92,6 @@ export const DocumentSigningCompleteDialog = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onFormSubmit = async (data: TNextSignerFormSchema) => {
|
const onFormSubmit = async (data: TNextSignerFormSchema) => {
|
||||||
console.log('data', data);
|
|
||||||
console.log('form.formState.errors', form.formState.errors);
|
|
||||||
try {
|
try {
|
||||||
if (allowDictateNextSigner && data.name && data.email) {
|
if (allowDictateNextSigner && data.name && data.email) {
|
||||||
await onSignatureComplete({ name: data.name, email: data.email });
|
await onSignatureComplete({ name: data.name, email: data.email });
|
||||||
|
|||||||
@ -183,8 +183,8 @@ export const DocumentEditForm = ({
|
|||||||
title: data.title,
|
title: data.title,
|
||||||
externalId: data.externalId || null,
|
externalId: data.externalId || null,
|
||||||
visibility: data.visibility,
|
visibility: data.visibility,
|
||||||
globalAccessAuth: data.globalAccessAuth ?? null,
|
globalAccessAuth: data.globalAccessAuth ?? [],
|
||||||
globalActionAuth: data.globalActionAuth ?? null,
|
globalActionAuth: data.globalActionAuth ?? [],
|
||||||
},
|
},
|
||||||
meta: {
|
meta: {
|
||||||
timezone,
|
timezone,
|
||||||
@ -229,7 +229,7 @@ export const DocumentEditForm = ({
|
|||||||
recipients: data.signers.map((signer) => ({
|
recipients: data.signers.map((signer) => ({
|
||||||
...signer,
|
...signer,
|
||||||
// Explicitly set to null to indicate we want to remove auth if required.
|
// Explicitly set to null to indicate we want to remove auth if required.
|
||||||
actionAuth: signer.actionAuth || null,
|
actionAuth: signer.actionAuth ?? [],
|
||||||
})),
|
})),
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|||||||
@ -81,11 +81,15 @@ export const DocumentHistorySheet = ({
|
|||||||
* @param text The text to format
|
* @param text The text to format
|
||||||
* @returns The formatted text
|
* @returns The formatted text
|
||||||
*/
|
*/
|
||||||
const formatGenericText = (text?: string | null) => {
|
const formatGenericText = (text?: string | string[] | null): string => {
|
||||||
if (!text) {
|
if (!text) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(text)) {
|
||||||
|
return text.map((t) => formatGenericText(t)).join(', ');
|
||||||
|
}
|
||||||
|
|
||||||
return (text.charAt(0).toUpperCase() + text.slice(1).toLowerCase()).replaceAll('_', ' ');
|
return (text.charAt(0).toUpperCase() + text.slice(1).toLowerCase()).replaceAll('_', ' ');
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -245,11 +249,19 @@ export const DocumentHistorySheet = ({
|
|||||||
values={[
|
values={[
|
||||||
{
|
{
|
||||||
key: 'Old',
|
key: 'Old',
|
||||||
value: DOCUMENT_AUTH_TYPES[data.from || '']?.value || 'None',
|
value: Array.isArray(data.from)
|
||||||
|
? data.from
|
||||||
|
.map((f) => DOCUMENT_AUTH_TYPES[f]?.value || 'None')
|
||||||
|
.join(', ')
|
||||||
|
: DOCUMENT_AUTH_TYPES[data.from || '']?.value || 'None',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'New',
|
key: 'New',
|
||||||
value: DOCUMENT_AUTH_TYPES[data.to || '']?.value || 'None',
|
value: Array.isArray(data.to)
|
||||||
|
? data.to
|
||||||
|
.map((f) => DOCUMENT_AUTH_TYPES[f]?.value || 'None')
|
||||||
|
.join(', ')
|
||||||
|
: DOCUMENT_AUTH_TYPES[data.to || '']?.value || 'None',
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -134,8 +134,8 @@ export const TemplateEditForm = ({
|
|||||||
title: data.title,
|
title: data.title,
|
||||||
externalId: data.externalId || null,
|
externalId: data.externalId || null,
|
||||||
visibility: data.visibility,
|
visibility: data.visibility,
|
||||||
globalAccessAuth: data.globalAccessAuth ?? null,
|
globalAccessAuth: data.globalAccessAuth ?? [],
|
||||||
globalActionAuth: data.globalActionAuth ?? null,
|
globalActionAuth: data.globalActionAuth ?? [],
|
||||||
},
|
},
|
||||||
meta: {
|
meta: {
|
||||||
...data.meta,
|
...data.meta,
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { useLingui } from '@lingui/react';
|
|||||||
import { FieldType, SigningStatus } from '@prisma/client';
|
import { FieldType, SigningStatus } from '@prisma/client';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import { redirect } from 'react-router';
|
import { redirect } from 'react-router';
|
||||||
|
import { prop, sortBy } from 'remeda';
|
||||||
import { match } from 'ts-pattern';
|
import { match } from 'ts-pattern';
|
||||||
import { UAParser } from 'ua-parser-js';
|
import { UAParser } from 'ua-parser-js';
|
||||||
import { renderSVG } from 'uqr';
|
import { renderSVG } from 'uqr';
|
||||||
@ -133,18 +134,30 @@ export default function SigningCertificate({ loaderData }: Route.ComponentProps)
|
|||||||
recipientAuth: recipient.authOptions,
|
recipientAuth: recipient.authOptions,
|
||||||
});
|
});
|
||||||
|
|
||||||
let authLevel = match(extractedAuthMethods.derivedRecipientActionAuth)
|
const insertedAuditLogsWithFieldAuth = sortBy(
|
||||||
|
auditLogs.DOCUMENT_FIELD_INSERTED.filter(
|
||||||
|
(log) => log.data.recipientId === recipient.id && log.data.fieldSecurity,
|
||||||
|
),
|
||||||
|
[prop('createdAt'), 'desc'],
|
||||||
|
);
|
||||||
|
|
||||||
|
const actionAuthMethod = insertedAuditLogsWithFieldAuth.at(0)?.data?.fieldSecurity?.type;
|
||||||
|
|
||||||
|
let authLevel = match(actionAuthMethod)
|
||||||
.with('ACCOUNT', () => _(msg`Account Re-Authentication`))
|
.with('ACCOUNT', () => _(msg`Account Re-Authentication`))
|
||||||
.with('TWO_FACTOR_AUTH', () => _(msg`Two-Factor Re-Authentication`))
|
.with('TWO_FACTOR_AUTH', () => _(msg`Two-Factor Re-Authentication`))
|
||||||
|
.with('PASSWORD', () => _(msg`Password Re-Authentication`))
|
||||||
.with('PASSKEY', () => _(msg`Passkey Re-Authentication`))
|
.with('PASSKEY', () => _(msg`Passkey Re-Authentication`))
|
||||||
.with('EXPLICIT_NONE', () => _(msg`Email`))
|
.with('EXPLICIT_NONE', () => _(msg`Email`))
|
||||||
.with(null, () => null)
|
.with(undefined, () => null)
|
||||||
.exhaustive();
|
.exhaustive();
|
||||||
|
|
||||||
if (!authLevel) {
|
if (!authLevel) {
|
||||||
authLevel = match(extractedAuthMethods.derivedRecipientAccessAuth)
|
const accessAuthMethod = extractedAuthMethods.derivedRecipientAccessAuth.at(0);
|
||||||
|
|
||||||
|
authLevel = match(accessAuthMethod)
|
||||||
.with('ACCOUNT', () => _(msg`Account Authentication`))
|
.with('ACCOUNT', () => _(msg`Account Authentication`))
|
||||||
.with(null, () => _(msg`Email`))
|
.with(undefined, () => _(msg`Email`))
|
||||||
.exhaustive();
|
.exhaustive();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -47,9 +47,9 @@ export async function loader({ params, request }: Route.LoaderArgs) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Ensure typesafety when we add more options.
|
// Ensure typesafety when we add more options.
|
||||||
const isAccessAuthValid = match(derivedRecipientAccessAuth)
|
const isAccessAuthValid = match(derivedRecipientAccessAuth.at(0))
|
||||||
.with(DocumentAccessAuth.ACCOUNT, () => Boolean(session.user))
|
.with(DocumentAccessAuth.ACCOUNT, () => Boolean(session.user))
|
||||||
.with(null, () => true)
|
.with(undefined, () => true)
|
||||||
.exhaustive();
|
.exhaustive();
|
||||||
|
|
||||||
if (!isAccessAuthValid) {
|
if (!isAccessAuthValid) {
|
||||||
|
|||||||
@ -68,9 +68,9 @@ export async function loader({ params, request }: Route.LoaderArgs) {
|
|||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const isAccessAuthValid = match(derivedRecipientAccessAuth)
|
const isAccessAuthValid = match(derivedRecipientAccessAuth.at(0))
|
||||||
.with(DocumentAccessAuth.ACCOUNT, () => user !== null)
|
.with(DocumentAccessAuth.ACCOUNT, () => !!user)
|
||||||
.with(null, () => true)
|
.with(undefined, () => true)
|
||||||
.exhaustive();
|
.exhaustive();
|
||||||
|
|
||||||
if (!isAccessAuthValid) {
|
if (!isAccessAuthValid) {
|
||||||
|
|||||||
@ -81,9 +81,9 @@ export async function loader({ params, request }: Route.LoaderArgs) {
|
|||||||
documentAuth: document.authOptions,
|
documentAuth: document.authOptions,
|
||||||
});
|
});
|
||||||
|
|
||||||
const isAccessAuthValid = match(derivedRecipientAccessAuth)
|
const isAccessAuthValid = match(derivedRecipientAccessAuth.at(0))
|
||||||
.with(DocumentAccessAuth.ACCOUNT, () => user !== null)
|
.with(DocumentAccessAuth.ACCOUNT, () => user && user.email === recipient.email)
|
||||||
.with(null, () => true)
|
.with(undefined, () => true)
|
||||||
.exhaustive();
|
.exhaustive();
|
||||||
|
|
||||||
if (!isAccessAuthValid) {
|
if (!isAccessAuthValid) {
|
||||||
|
|||||||
@ -38,8 +38,6 @@ export async function loader({ request }: Route.LoaderArgs) {
|
|||||||
|
|
||||||
const recipient = await getRecipientByToken({ token });
|
const recipient = await getRecipientByToken({ token });
|
||||||
|
|
||||||
console.log('document', document.id);
|
|
||||||
|
|
||||||
return { document, recipient };
|
return { document, recipient };
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|||||||
@ -821,7 +821,7 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, {
|
|||||||
name,
|
name,
|
||||||
role,
|
role,
|
||||||
signingOrder,
|
signingOrder,
|
||||||
actionAuth: authOptions?.actionAuth ?? null,
|
actionAuth: authOptions?.actionAuth ?? [],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
requestMetadata: metadata,
|
requestMetadata: metadata,
|
||||||
@ -888,7 +888,7 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, {
|
|||||||
name,
|
name,
|
||||||
role,
|
role,
|
||||||
signingOrder,
|
signingOrder,
|
||||||
actionAuth: authOptions?.actionAuth,
|
actionAuth: authOptions?.actionAuth ?? [],
|
||||||
requestMetadata: metadata.requestMetadata,
|
requestMetadata: metadata.requestMetadata,
|
||||||
}).catch(() => null);
|
}).catch(() => null);
|
||||||
|
|
||||||
|
|||||||
@ -177,8 +177,8 @@ export const ZCreateDocumentMutationSchema = z.object({
|
|||||||
.default({}),
|
.default({}),
|
||||||
authOptions: z
|
authOptions: z
|
||||||
.object({
|
.object({
|
||||||
globalAccessAuth: ZDocumentAccessAuthTypesSchema.optional(),
|
globalAccessAuth: z.array(ZDocumentAccessAuthTypesSchema).optional().default([]),
|
||||||
globalActionAuth: ZDocumentActionAuthTypesSchema.optional(),
|
globalActionAuth: z.array(ZDocumentActionAuthTypesSchema).optional().default([]),
|
||||||
})
|
})
|
||||||
.optional()
|
.optional()
|
||||||
.openapi({
|
.openapi({
|
||||||
@ -237,8 +237,8 @@ export const ZCreateDocumentFromTemplateMutationSchema = z.object({
|
|||||||
.optional(),
|
.optional(),
|
||||||
authOptions: z
|
authOptions: z
|
||||||
.object({
|
.object({
|
||||||
globalAccessAuth: ZDocumentAccessAuthTypesSchema.optional(),
|
globalAccessAuth: z.array(ZDocumentAccessAuthTypesSchema).optional().default([]),
|
||||||
globalActionAuth: ZDocumentActionAuthTypesSchema.optional(),
|
globalActionAuth: z.array(ZDocumentActionAuthTypesSchema).optional().default([]),
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
formValues: z.record(z.string(), z.union([z.string(), z.boolean(), z.number()])).optional(),
|
formValues: z.record(z.string(), z.union([z.string(), z.boolean(), z.number()])).optional(),
|
||||||
@ -310,8 +310,8 @@ export const ZGenerateDocumentFromTemplateMutationSchema = z.object({
|
|||||||
.optional(),
|
.optional(),
|
||||||
authOptions: z
|
authOptions: z
|
||||||
.object({
|
.object({
|
||||||
globalAccessAuth: ZDocumentAccessAuthTypesSchema.optional(),
|
globalAccessAuth: z.array(ZDocumentAccessAuthTypesSchema).optional().default([]),
|
||||||
globalActionAuth: ZDocumentActionAuthTypesSchema.optional(),
|
globalActionAuth: z.array(ZDocumentActionAuthTypesSchema).optional().default([]),
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
formValues: z.record(z.string(), z.union([z.string(), z.boolean(), z.number()])).optional(),
|
formValues: z.record(z.string(), z.union([z.string(), z.boolean(), z.number()])).optional(),
|
||||||
@ -350,7 +350,7 @@ export const ZCreateRecipientMutationSchema = z.object({
|
|||||||
signingOrder: z.number().nullish(),
|
signingOrder: z.number().nullish(),
|
||||||
authOptions: z
|
authOptions: z
|
||||||
.object({
|
.object({
|
||||||
actionAuth: ZRecipientActionAuthTypesSchema.optional(),
|
actionAuth: z.array(ZRecipientActionAuthTypesSchema).optional().default([]),
|
||||||
})
|
})
|
||||||
.optional()
|
.optional()
|
||||||
.openapi({
|
.openapi({
|
||||||
|
|||||||
@ -42,8 +42,8 @@ test('[DOCUMENT_AUTH]: should allow or deny access when required', async ({ page
|
|||||||
{
|
{
|
||||||
createDocumentOptions: {
|
createDocumentOptions: {
|
||||||
authOptions: createDocumentAuthOptions({
|
authOptions: createDocumentAuthOptions({
|
||||||
globalAccessAuth: 'ACCOUNT',
|
globalAccessAuth: ['ACCOUNT'],
|
||||||
globalActionAuth: null,
|
globalActionAuth: [],
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@ -65,8 +65,8 @@ test('[DOCUMENT_AUTH]: should allow signing with valid global auth', async ({ pa
|
|||||||
recipients: [recipientWithAccount],
|
recipients: [recipientWithAccount],
|
||||||
updateDocumentOptions: {
|
updateDocumentOptions: {
|
||||||
authOptions: createDocumentAuthOptions({
|
authOptions: createDocumentAuthOptions({
|
||||||
globalAccessAuth: null,
|
globalAccessAuth: [],
|
||||||
globalActionAuth: 'ACCOUNT',
|
globalActionAuth: ['ACCOUNT'],
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -116,8 +116,8 @@ test.skip('[DOCUMENT_AUTH]: should deny signing document when required for globa
|
|||||||
recipients: [recipientWithAccount],
|
recipients: [recipientWithAccount],
|
||||||
updateDocumentOptions: {
|
updateDocumentOptions: {
|
||||||
authOptions: createDocumentAuthOptions({
|
authOptions: createDocumentAuthOptions({
|
||||||
globalAccessAuth: null,
|
globalAccessAuth: [],
|
||||||
globalActionAuth: 'ACCOUNT',
|
globalActionAuth: ['ACCOUNT'],
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -147,8 +147,8 @@ test('[DOCUMENT_AUTH]: should deny signing fields when required for global auth'
|
|||||||
recipients: [recipientWithAccount, seedTestEmail()],
|
recipients: [recipientWithAccount, seedTestEmail()],
|
||||||
updateDocumentOptions: {
|
updateDocumentOptions: {
|
||||||
authOptions: createDocumentAuthOptions({
|
authOptions: createDocumentAuthOptions({
|
||||||
globalAccessAuth: null,
|
globalAccessAuth: [],
|
||||||
globalActionAuth: 'ACCOUNT',
|
globalActionAuth: ['ACCOUNT'],
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -193,20 +193,20 @@ test('[DOCUMENT_AUTH]: should allow field signing when required for recipient au
|
|||||||
recipientsCreateOptions: [
|
recipientsCreateOptions: [
|
||||||
{
|
{
|
||||||
authOptions: createRecipientAuthOptions({
|
authOptions: createRecipientAuthOptions({
|
||||||
accessAuth: null,
|
accessAuth: [],
|
||||||
actionAuth: null,
|
actionAuth: [],
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
authOptions: createRecipientAuthOptions({
|
authOptions: createRecipientAuthOptions({
|
||||||
accessAuth: null,
|
accessAuth: [],
|
||||||
actionAuth: 'EXPLICIT_NONE',
|
actionAuth: ['EXPLICIT_NONE'],
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
authOptions: createRecipientAuthOptions({
|
authOptions: createRecipientAuthOptions({
|
||||||
accessAuth: null,
|
accessAuth: [],
|
||||||
actionAuth: 'ACCOUNT',
|
actionAuth: ['ACCOUNT'],
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@ -218,7 +218,7 @@ test('[DOCUMENT_AUTH]: should allow field signing when required for recipient au
|
|||||||
const { actionAuth } = ZRecipientAuthOptionsSchema.parse(recipient.authOptions);
|
const { actionAuth } = ZRecipientAuthOptionsSchema.parse(recipient.authOptions);
|
||||||
|
|
||||||
// This document has no global action auth, so only account should require auth.
|
// This document has no global action auth, so only account should require auth.
|
||||||
const isAuthRequired = actionAuth === 'ACCOUNT';
|
const isAuthRequired = actionAuth.includes('ACCOUNT');
|
||||||
|
|
||||||
const signUrl = `/sign/${token}`;
|
const signUrl = `/sign/${token}`;
|
||||||
|
|
||||||
@ -292,28 +292,28 @@ test('[DOCUMENT_AUTH]: should allow field signing when required for recipient an
|
|||||||
recipientsCreateOptions: [
|
recipientsCreateOptions: [
|
||||||
{
|
{
|
||||||
authOptions: createRecipientAuthOptions({
|
authOptions: createRecipientAuthOptions({
|
||||||
accessAuth: null,
|
accessAuth: [],
|
||||||
actionAuth: null,
|
actionAuth: [],
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
authOptions: createRecipientAuthOptions({
|
authOptions: createRecipientAuthOptions({
|
||||||
accessAuth: null,
|
accessAuth: [],
|
||||||
actionAuth: 'EXPLICIT_NONE',
|
actionAuth: ['EXPLICIT_NONE'],
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
authOptions: createRecipientAuthOptions({
|
authOptions: createRecipientAuthOptions({
|
||||||
accessAuth: null,
|
accessAuth: [],
|
||||||
actionAuth: 'ACCOUNT',
|
actionAuth: ['ACCOUNT'],
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
fields: [FieldType.DATE, FieldType.SIGNATURE],
|
fields: [FieldType.DATE, FieldType.SIGNATURE],
|
||||||
updateDocumentOptions: {
|
updateDocumentOptions: {
|
||||||
authOptions: createDocumentAuthOptions({
|
authOptions: createDocumentAuthOptions({
|
||||||
globalAccessAuth: null,
|
globalAccessAuth: [],
|
||||||
globalActionAuth: 'ACCOUNT',
|
globalActionAuth: ['ACCOUNT'],
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -323,7 +323,7 @@ test('[DOCUMENT_AUTH]: should allow field signing when required for recipient an
|
|||||||
const { actionAuth } = ZRecipientAuthOptionsSchema.parse(recipient.authOptions);
|
const { actionAuth } = ZRecipientAuthOptionsSchema.parse(recipient.authOptions);
|
||||||
|
|
||||||
// This document HAS global action auth, so account and inherit should require auth.
|
// This document HAS global action auth, so account and inherit should require auth.
|
||||||
const isAuthRequired = actionAuth === 'ACCOUNT' || actionAuth === null;
|
const isAuthRequired = actionAuth.includes('ACCOUNT') || actionAuth.length === 0;
|
||||||
|
|
||||||
const signUrl = `/sign/${token}`;
|
const signUrl = `/sign/${token}`;
|
||||||
|
|
||||||
|
|||||||
@ -40,7 +40,7 @@ test.describe('[EE_ONLY]', () => {
|
|||||||
|
|
||||||
// Set EE action auth.
|
// Set EE action auth.
|
||||||
await page.getByTestId('documentActionSelectValue').click();
|
await page.getByTestId('documentActionSelectValue').click();
|
||||||
await page.getByLabel('Require passkey').getByText('Require passkey').click();
|
await page.getByRole('option').filter({ hasText: 'Require passkey' }).click();
|
||||||
await expect(page.getByTestId('documentActionSelectValue')).toContainText('Require passkey');
|
await expect(page.getByTestId('documentActionSelectValue')).toContainText('Require passkey');
|
||||||
|
|
||||||
// Save the settings by going to the next step.
|
// Save the settings by going to the next step.
|
||||||
@ -82,7 +82,7 @@ test.describe('[EE_ONLY]', () => {
|
|||||||
|
|
||||||
// Set EE action auth.
|
// Set EE action auth.
|
||||||
await page.getByTestId('documentActionSelectValue').click();
|
await page.getByTestId('documentActionSelectValue').click();
|
||||||
await page.getByLabel('Require passkey').getByText('Require passkey').click();
|
await page.getByRole('option').filter({ hasText: 'Require passkey' }).click();
|
||||||
await expect(page.getByTestId('documentActionSelectValue')).toContainText('Require passkey');
|
await expect(page.getByTestId('documentActionSelectValue')).toContainText('Require passkey');
|
||||||
|
|
||||||
// Save the settings by going to the next step.
|
// Save the settings by going to the next step.
|
||||||
@ -143,7 +143,7 @@ test('[DOCUMENT_FLOW]: add settings', async ({ page }) => {
|
|||||||
|
|
||||||
// Set access auth.
|
// Set access auth.
|
||||||
await page.getByTestId('documentAccessSelectValue').click();
|
await page.getByTestId('documentAccessSelectValue').click();
|
||||||
await page.getByLabel('Require account').getByText('Require account').click();
|
await page.getByRole('option').filter({ hasText: 'Require account' }).click();
|
||||||
await expect(page.getByTestId('documentAccessSelectValue')).toContainText('Require account');
|
await expect(page.getByTestId('documentAccessSelectValue')).toContainText('Require account');
|
||||||
|
|
||||||
// Action auth should NOT be visible.
|
// Action auth should NOT be visible.
|
||||||
|
|||||||
@ -37,7 +37,7 @@ test.describe('[EE_ONLY]', () => {
|
|||||||
|
|
||||||
// Set EE action auth.
|
// Set EE action auth.
|
||||||
await page.getByTestId('documentActionSelectValue').click();
|
await page.getByTestId('documentActionSelectValue').click();
|
||||||
await page.getByLabel('Require passkey').getByText('Require passkey').click();
|
await page.getByRole('option').filter({ hasText: 'Require passkey' }).click();
|
||||||
await expect(page.getByTestId('documentActionSelectValue')).toContainText('Require passkey');
|
await expect(page.getByTestId('documentActionSelectValue')).toContainText('Require passkey');
|
||||||
|
|
||||||
// Save the settings by going to the next step.
|
// Save the settings by going to the next step.
|
||||||
@ -79,7 +79,7 @@ test.describe('[EE_ONLY]', () => {
|
|||||||
|
|
||||||
// Set EE action auth.
|
// Set EE action auth.
|
||||||
await page.getByTestId('documentActionSelectValue').click();
|
await page.getByTestId('documentActionSelectValue').click();
|
||||||
await page.getByLabel('Require passkey').getByText('Require passkey').click();
|
await page.getByRole('option').filter({ hasText: 'Require passkey' }).click();
|
||||||
await expect(page.getByTestId('documentActionSelectValue')).toContainText('Require passkey');
|
await expect(page.getByTestId('documentActionSelectValue')).toContainText('Require passkey');
|
||||||
|
|
||||||
// Save the settings by going to the next step.
|
// Save the settings by going to the next step.
|
||||||
@ -140,7 +140,7 @@ test('[TEMPLATE_FLOW]: add settings', async ({ page }) => {
|
|||||||
|
|
||||||
// Set access auth.
|
// Set access auth.
|
||||||
await page.getByTestId('documentAccessSelectValue').click();
|
await page.getByTestId('documentAccessSelectValue').click();
|
||||||
await page.getByLabel('Require account').getByText('Require account').click();
|
await page.getByRole('option').filter({ hasText: 'Require account' }).click();
|
||||||
await expect(page.getByTestId('documentAccessSelectValue')).toContainText('Require account');
|
await expect(page.getByTestId('documentAccessSelectValue')).toContainText('Require account');
|
||||||
|
|
||||||
// Action auth should NOT be visible.
|
// Action auth should NOT be visible.
|
||||||
|
|||||||
@ -58,8 +58,8 @@ test.describe('[EE_ONLY]', () => {
|
|||||||
|
|
||||||
// Add advanced settings for a single recipient.
|
// Add advanced settings for a single recipient.
|
||||||
await page.getByLabel('Show advanced settings').check();
|
await page.getByLabel('Show advanced settings').check();
|
||||||
await page.getByRole('combobox').first().click();
|
await page.getByTestId('documentActionSelectValue').click();
|
||||||
await page.getByLabel('Require passkey').click();
|
await page.getByRole('option').filter({ hasText: 'Require passkey' }).click();
|
||||||
|
|
||||||
// Navigate to the next step and back.
|
// Navigate to the next step and back.
|
||||||
await page.getByRole('button', { name: 'Continue' }).click();
|
await page.getByRole('button', { name: 'Continue' }).click();
|
||||||
|
|||||||
@ -48,13 +48,13 @@ test('[TEMPLATE]: should create a document from a template', async ({ page }) =>
|
|||||||
|
|
||||||
// Set template document access.
|
// Set template document access.
|
||||||
await page.getByTestId('documentAccessSelectValue').click();
|
await page.getByTestId('documentAccessSelectValue').click();
|
||||||
await page.getByLabel('Require account').getByText('Require account').click();
|
await page.getByRole('option').filter({ hasText: 'Require account' }).click();
|
||||||
await expect(page.getByTestId('documentAccessSelectValue')).toContainText('Require account');
|
await expect(page.getByTestId('documentAccessSelectValue')).toContainText('Require account');
|
||||||
|
|
||||||
// Set EE action auth.
|
// Set EE action auth.
|
||||||
if (isBillingEnabled) {
|
if (isBillingEnabled) {
|
||||||
await page.getByTestId('documentActionSelectValue').click();
|
await page.getByTestId('documentActionSelectValue').click();
|
||||||
await page.getByLabel('Require passkey').getByText('Require passkey').click();
|
await page.getByRole('option').filter({ hasText: 'Require passkey' }).click();
|
||||||
await expect(page.getByTestId('documentActionSelectValue')).toContainText('Require passkey');
|
await expect(page.getByTestId('documentActionSelectValue')).toContainText('Require passkey');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -85,8 +85,8 @@ test('[TEMPLATE]: should create a document from a template', async ({ page }) =>
|
|||||||
// Apply require passkey for Recipient 1.
|
// Apply require passkey for Recipient 1.
|
||||||
if (isBillingEnabled) {
|
if (isBillingEnabled) {
|
||||||
await page.getByLabel('Show advanced settings').check();
|
await page.getByLabel('Show advanced settings').check();
|
||||||
await page.getByRole('combobox').first().click();
|
await page.getByTestId('documentActionSelectValue').click();
|
||||||
await page.getByLabel('Require passkey').click();
|
await page.getByRole('option').filter({ hasText: 'Require passkey' }).click();
|
||||||
}
|
}
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Continue' }).click();
|
await page.getByRole('button', { name: 'Continue' }).click();
|
||||||
@ -119,10 +119,12 @@ test('[TEMPLATE]: should create a document from a template', async ({ page }) =>
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(document.title).toEqual('TEMPLATE_TITLE');
|
expect(document.title).toEqual('TEMPLATE_TITLE');
|
||||||
expect(documentAuth.documentAuthOption.globalAccessAuth).toEqual('ACCOUNT');
|
expect(documentAuth.documentAuthOption.globalAccessAuth).toContain('ACCOUNT');
|
||||||
expect(documentAuth.documentAuthOption.globalActionAuth).toEqual(
|
|
||||||
isBillingEnabled ? 'PASSKEY' : null,
|
if (isBillingEnabled) {
|
||||||
);
|
expect(documentAuth.documentAuthOption.globalActionAuth).toContain('PASSKEY');
|
||||||
|
}
|
||||||
|
|
||||||
expect(document.documentMeta?.dateFormat).toEqual('dd/MM/yyyy hh:mm a');
|
expect(document.documentMeta?.dateFormat).toEqual('dd/MM/yyyy hh:mm a');
|
||||||
expect(document.documentMeta?.message).toEqual('MESSAGE');
|
expect(document.documentMeta?.message).toEqual('MESSAGE');
|
||||||
expect(document.documentMeta?.redirectUrl).toEqual('https://documenso.com');
|
expect(document.documentMeta?.redirectUrl).toEqual('https://documenso.com');
|
||||||
@ -143,11 +145,11 @@ test('[TEMPLATE]: should create a document from a template', async ({ page }) =>
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (isBillingEnabled) {
|
if (isBillingEnabled) {
|
||||||
expect(recipientOneAuth.derivedRecipientActionAuth).toEqual('PASSKEY');
|
expect(recipientOneAuth.derivedRecipientActionAuth).toContain('PASSKEY');
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(recipientOneAuth.derivedRecipientAccessAuth).toEqual('ACCOUNT');
|
expect(recipientOneAuth.derivedRecipientAccessAuth).toContain('ACCOUNT');
|
||||||
expect(recipientTwoAuth.derivedRecipientAccessAuth).toEqual('ACCOUNT');
|
expect(recipientTwoAuth.derivedRecipientAccessAuth).toContain('ACCOUNT');
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -183,13 +185,13 @@ test('[TEMPLATE]: should create a team document from a team template', async ({
|
|||||||
|
|
||||||
// Set template document access.
|
// Set template document access.
|
||||||
await page.getByTestId('documentAccessSelectValue').click();
|
await page.getByTestId('documentAccessSelectValue').click();
|
||||||
await page.getByLabel('Require account').getByText('Require account').click();
|
await page.getByRole('option').filter({ hasText: 'Require account' }).click();
|
||||||
await expect(page.getByTestId('documentAccessSelectValue')).toContainText('Require account');
|
await expect(page.getByTestId('documentAccessSelectValue')).toContainText('Require account');
|
||||||
|
|
||||||
// Set EE action auth.
|
// Set EE action auth.
|
||||||
if (isBillingEnabled) {
|
if (isBillingEnabled) {
|
||||||
await page.getByTestId('documentActionSelectValue').click();
|
await page.getByTestId('documentActionSelectValue').click();
|
||||||
await page.getByLabel('Require passkey').getByText('Require passkey').click();
|
await page.getByRole('option').filter({ hasText: 'Require passkey' }).click();
|
||||||
await expect(page.getByTestId('documentActionSelectValue')).toContainText('Require passkey');
|
await expect(page.getByTestId('documentActionSelectValue')).toContainText('Require passkey');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -220,8 +222,8 @@ test('[TEMPLATE]: should create a team document from a team template', async ({
|
|||||||
// Apply require passkey for Recipient 1.
|
// Apply require passkey for Recipient 1.
|
||||||
if (isBillingEnabled) {
|
if (isBillingEnabled) {
|
||||||
await page.getByLabel('Show advanced settings').check();
|
await page.getByLabel('Show advanced settings').check();
|
||||||
await page.getByRole('combobox').first().click();
|
await page.getByTestId('documentActionSelectValue').click();
|
||||||
await page.getByLabel('Require passkey').click();
|
await page.getByRole('option').filter({ hasText: 'Require passkey' }).click();
|
||||||
}
|
}
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Continue' }).click();
|
await page.getByRole('button', { name: 'Continue' }).click();
|
||||||
@ -256,10 +258,12 @@ test('[TEMPLATE]: should create a team document from a team template', async ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(document.title).toEqual('TEMPLATE_TITLE');
|
expect(document.title).toEqual('TEMPLATE_TITLE');
|
||||||
expect(documentAuth.documentAuthOption.globalAccessAuth).toEqual('ACCOUNT');
|
expect(documentAuth.documentAuthOption.globalAccessAuth).toContain('ACCOUNT');
|
||||||
expect(documentAuth.documentAuthOption.globalActionAuth).toEqual(
|
|
||||||
isBillingEnabled ? 'PASSKEY' : null,
|
if (isBillingEnabled) {
|
||||||
);
|
expect(documentAuth.documentAuthOption.globalActionAuth).toContain('PASSKEY');
|
||||||
|
}
|
||||||
|
|
||||||
expect(document.documentMeta?.dateFormat).toEqual('dd/MM/yyyy hh:mm a');
|
expect(document.documentMeta?.dateFormat).toEqual('dd/MM/yyyy hh:mm a');
|
||||||
expect(document.documentMeta?.message).toEqual('MESSAGE');
|
expect(document.documentMeta?.message).toEqual('MESSAGE');
|
||||||
expect(document.documentMeta?.redirectUrl).toEqual('https://documenso.com');
|
expect(document.documentMeta?.redirectUrl).toEqual('https://documenso.com');
|
||||||
@ -280,11 +284,11 @@ test('[TEMPLATE]: should create a team document from a team template', async ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (isBillingEnabled) {
|
if (isBillingEnabled) {
|
||||||
expect(recipientOneAuth.derivedRecipientActionAuth).toEqual('PASSKEY');
|
expect(recipientOneAuth.derivedRecipientActionAuth).toContain('PASSKEY');
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(recipientOneAuth.derivedRecipientAccessAuth).toEqual('ACCOUNT');
|
expect(recipientOneAuth.derivedRecipientAccessAuth).toContain('ACCOUNT');
|
||||||
expect(recipientTwoAuth.derivedRecipientAccessAuth).toEqual('ACCOUNT');
|
expect(recipientTwoAuth.derivedRecipientAccessAuth).toContain('ACCOUNT');
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -172,8 +172,8 @@ test('[DIRECT_TEMPLATES]: direct template link auth access', async ({ page }) =>
|
|||||||
userId: user.id,
|
userId: user.id,
|
||||||
createTemplateOptions: {
|
createTemplateOptions: {
|
||||||
authOptions: createDocumentAuthOptions({
|
authOptions: createDocumentAuthOptions({
|
||||||
globalAccessAuth: 'ACCOUNT',
|
globalAccessAuth: ['ACCOUNT'],
|
||||||
globalActionAuth: null,
|
globalActionAuth: [],
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -19,6 +19,10 @@ export const DOCUMENT_AUTH_TYPES: Record<string, DocumentAuthTypeData> = {
|
|||||||
key: DocumentAuth.TWO_FACTOR_AUTH,
|
key: DocumentAuth.TWO_FACTOR_AUTH,
|
||||||
value: 'Require 2FA',
|
value: 'Require 2FA',
|
||||||
},
|
},
|
||||||
|
[DocumentAuth.PASSWORD]: {
|
||||||
|
key: DocumentAuth.PASSWORD,
|
||||||
|
value: 'Require password',
|
||||||
|
},
|
||||||
[DocumentAuth.EXPLICIT_NONE]: {
|
[DocumentAuth.EXPLICIT_NONE]: {
|
||||||
key: DocumentAuth.EXPLICIT_NONE,
|
key: DocumentAuth.EXPLICIT_NONE,
|
||||||
value: 'None (Overrides global settings)',
|
value: 'None (Overrides global settings)',
|
||||||
|
|||||||
20
packages/lib/server-only/2fa/verify-password.ts
Normal file
20
packages/lib/server-only/2fa/verify-password.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { compare } from '@node-rs/bcrypt';
|
||||||
|
|
||||||
|
import { prisma } from '@documenso/prisma';
|
||||||
|
|
||||||
|
type VerifyPasswordOptions = {
|
||||||
|
userId: number;
|
||||||
|
password: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const verifyPassword = async ({ userId, password }: VerifyPasswordOptions) => {
|
||||||
|
const user = await prisma.user.findUnique({
|
||||||
|
where: { id: userId },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user || !user.password) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await compare(password, user.password);
|
||||||
|
};
|
||||||
@ -23,6 +23,7 @@ import {
|
|||||||
ZWebhookDocumentSchema,
|
ZWebhookDocumentSchema,
|
||||||
mapDocumentToWebhookDocumentPayload,
|
mapDocumentToWebhookDocumentPayload,
|
||||||
} from '../../types/webhook-payload';
|
} from '../../types/webhook-payload';
|
||||||
|
import { extractDocumentAuthMethods } from '../../utils/document-auth';
|
||||||
import { getIsRecipientsTurnToSign } from '../recipient/get-is-recipient-turn';
|
import { getIsRecipientsTurnToSign } from '../recipient/get-is-recipient-turn';
|
||||||
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
|
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
|
||||||
import { sendPendingEmail } from './send-pending-email';
|
import { sendPendingEmail } from './send-pending-email';
|
||||||
@ -140,6 +141,11 @@ export const completeDocumentWithToken = async ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const authOptions = extractDocumentAuthMethods({
|
||||||
|
documentAuth: document.authOptions,
|
||||||
|
recipientAuth: recipient.authOptions,
|
||||||
|
});
|
||||||
|
|
||||||
await tx.documentAuditLog.create({
|
await tx.documentAuditLog.create({
|
||||||
data: createDocumentAuditLogData({
|
data: createDocumentAuditLogData({
|
||||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_COMPLETED,
|
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_COMPLETED,
|
||||||
@ -154,6 +160,7 @@ export const completeDocumentWithToken = async ({
|
|||||||
recipientName: recipient.name,
|
recipientName: recipient.name,
|
||||||
recipientId: recipient.id,
|
recipientId: recipient.id,
|
||||||
recipientRole: recipient.role,
|
recipientRole: recipient.role,
|
||||||
|
actionAuth: authOptions.derivedRecipientActionAuth,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@ -39,8 +39,8 @@ export type CreateDocumentOptions = {
|
|||||||
title: string;
|
title: string;
|
||||||
externalId?: string;
|
externalId?: string;
|
||||||
visibility?: DocumentVisibility;
|
visibility?: DocumentVisibility;
|
||||||
globalAccessAuth?: TDocumentAccessAuthTypes;
|
globalAccessAuth?: TDocumentAccessAuthTypes[];
|
||||||
globalActionAuth?: TDocumentActionAuthTypes;
|
globalActionAuth?: TDocumentActionAuthTypes[];
|
||||||
formValues?: TDocumentFormValues;
|
formValues?: TDocumentFormValues;
|
||||||
recipients: TCreateDocumentV2Request['recipients'];
|
recipients: TCreateDocumentV2Request['recipients'];
|
||||||
};
|
};
|
||||||
@ -113,14 +113,16 @@ export const createDocumentV2 = async ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const authOptions = createDocumentAuthOptions({
|
const authOptions = createDocumentAuthOptions({
|
||||||
globalAccessAuth: data?.globalAccessAuth || null,
|
globalAccessAuth: data?.globalAccessAuth || [],
|
||||||
globalActionAuth: data?.globalActionAuth || null,
|
globalActionAuth: data?.globalActionAuth || [],
|
||||||
});
|
});
|
||||||
|
|
||||||
const recipientsHaveActionAuth = data.recipients?.some((recipient) => recipient.actionAuth);
|
const recipientsHaveActionAuth = data.recipients?.some(
|
||||||
|
(recipient) => recipient.actionAuth && recipient.actionAuth.length > 0,
|
||||||
|
);
|
||||||
|
|
||||||
// Check if user has permission to set the global action auth.
|
// Check if user has permission to set the global action auth.
|
||||||
if (authOptions.globalActionAuth || recipientsHaveActionAuth) {
|
if (authOptions.globalActionAuth.length > 0 || recipientsHaveActionAuth) {
|
||||||
const isDocumentEnterprise = await isUserEnterprise({
|
const isDocumentEnterprise = await isUserEnterprise({
|
||||||
userId,
|
userId,
|
||||||
teamId,
|
teamId,
|
||||||
@ -171,8 +173,8 @@ export const createDocumentV2 = async ({
|
|||||||
await Promise.all(
|
await Promise.all(
|
||||||
(data.recipients || []).map(async (recipient) => {
|
(data.recipients || []).map(async (recipient) => {
|
||||||
const recipientAuthOptions = createRecipientAuthOptions({
|
const recipientAuthOptions = createRecipientAuthOptions({
|
||||||
accessAuth: recipient.accessAuth || null,
|
accessAuth: recipient.accessAuth ?? [],
|
||||||
actionAuth: recipient.actionAuth || null,
|
actionAuth: recipient.actionAuth ?? [],
|
||||||
});
|
});
|
||||||
|
|
||||||
await tx.recipient.create({
|
await tx.recipient.create({
|
||||||
|
|||||||
@ -17,6 +17,7 @@ export const getDocumentCertificateAuditLogs = async ({
|
|||||||
in: [
|
in: [
|
||||||
DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_COMPLETED,
|
DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_COMPLETED,
|
||||||
DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_REJECTED,
|
DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_REJECTED,
|
||||||
|
DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FIELD_INSERTED,
|
||||||
DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_OPENED,
|
DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_OPENED,
|
||||||
DOCUMENT_AUDIT_LOG_TYPE.EMAIL_SENT,
|
DOCUMENT_AUDIT_LOG_TYPE.EMAIL_SENT,
|
||||||
],
|
],
|
||||||
@ -36,6 +37,9 @@ export const getDocumentCertificateAuditLogs = async ({
|
|||||||
[DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_OPENED]: auditLogs.filter(
|
[DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_OPENED]: auditLogs.filter(
|
||||||
(log) => log.type === DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_OPENED,
|
(log) => log.type === DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_OPENED,
|
||||||
),
|
),
|
||||||
|
[DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FIELD_INSERTED]: auditLogs.filter(
|
||||||
|
(log) => log.type === DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FIELD_INSERTED,
|
||||||
|
),
|
||||||
[DOCUMENT_AUDIT_LOG_TYPE.EMAIL_SENT]: auditLogs.filter(
|
[DOCUMENT_AUDIT_LOG_TYPE.EMAIL_SENT]: auditLogs.filter(
|
||||||
(log) =>
|
(log) =>
|
||||||
log.type === DOCUMENT_AUDIT_LOG_TYPE.EMAIL_SENT &&
|
log.type === DOCUMENT_AUDIT_LOG_TYPE.EMAIL_SENT &&
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import { match } from 'ts-pattern';
|
|||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
|
|
||||||
import { verifyTwoFactorAuthenticationToken } from '../2fa/verify-2fa-token';
|
import { verifyTwoFactorAuthenticationToken } from '../2fa/verify-2fa-token';
|
||||||
|
import { verifyPassword } from '../2fa/verify-password';
|
||||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||||
import type { TDocumentAuth, TDocumentAuthMethods } from '../../types/document-auth';
|
import type { TDocumentAuth, TDocumentAuthMethods } from '../../types/document-auth';
|
||||||
import { DocumentAuth } from '../../types/document-auth';
|
import { DocumentAuth } from '../../types/document-auth';
|
||||||
@ -60,23 +61,26 @@ export const isRecipientAuthorized = async ({
|
|||||||
recipientAuth: recipient.authOptions,
|
recipientAuth: recipient.authOptions,
|
||||||
});
|
});
|
||||||
|
|
||||||
const authMethod: TDocumentAuth | null =
|
const authMethods: TDocumentAuth[] =
|
||||||
type === 'ACCESS' ? derivedRecipientAccessAuth : derivedRecipientActionAuth;
|
type === 'ACCESS' ? derivedRecipientAccessAuth : derivedRecipientActionAuth;
|
||||||
|
|
||||||
// Early true return when auth is not required.
|
// Early true return when auth is not required.
|
||||||
if (!authMethod || authMethod === DocumentAuth.EXPLICIT_NONE) {
|
if (
|
||||||
|
authMethods.length === 0 ||
|
||||||
|
authMethods.some((method) => method === DocumentAuth.EXPLICIT_NONE)
|
||||||
|
) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create auth options when none are passed for account.
|
// Create auth options when none are passed for account.
|
||||||
if (!authOptions && authMethod === DocumentAuth.ACCOUNT) {
|
if (!authOptions && authMethods.some((method) => method === DocumentAuth.ACCOUNT)) {
|
||||||
authOptions = {
|
authOptions = {
|
||||||
type: DocumentAuth.ACCOUNT,
|
type: DocumentAuth.ACCOUNT,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Authentication required does not match provided method.
|
// Authentication required does not match provided method.
|
||||||
if (!authOptions || authOptions.type !== authMethod || !userId) {
|
if (!authOptions || !authMethods.includes(authOptions.type) || !userId) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -117,6 +121,15 @@ export const isRecipientAuthorized = async ({
|
|||||||
window: 10, // 5 minutes worth of tokens
|
window: 10, // 5 minutes worth of tokens
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
|
.with({ type: DocumentAuth.PASSWORD }, async ({ password }) => {
|
||||||
|
return await verifyPassword({
|
||||||
|
userId,
|
||||||
|
password,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.with({ type: DocumentAuth.EXPLICIT_NONE }, () => {
|
||||||
|
return true;
|
||||||
|
})
|
||||||
.exhaustive();
|
.exhaustive();
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -160,7 +173,7 @@ const verifyPasskey = async ({
|
|||||||
}: VerifyPasskeyOptions): Promise<void> => {
|
}: VerifyPasskeyOptions): Promise<void> => {
|
||||||
const passkey = await prisma.passkey.findFirst({
|
const passkey = await prisma.passkey.findFirst({
|
||||||
where: {
|
where: {
|
||||||
credentialId: Buffer.from(authenticationResponse.id, 'base64'),
|
credentialId: new Uint8Array(Buffer.from(authenticationResponse.id, 'base64')),
|
||||||
userId,
|
userId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -21,8 +21,8 @@ export type UpdateDocumentOptions = {
|
|||||||
title?: string;
|
title?: string;
|
||||||
externalId?: string | null;
|
externalId?: string | null;
|
||||||
visibility?: DocumentVisibility | null;
|
visibility?: DocumentVisibility | null;
|
||||||
globalAccessAuth?: TDocumentAccessAuthTypes | null;
|
globalAccessAuth?: TDocumentAccessAuthTypes[];
|
||||||
globalActionAuth?: TDocumentActionAuthTypes | null;
|
globalActionAuth?: TDocumentActionAuthTypes[];
|
||||||
useLegacyFieldInsertion?: boolean;
|
useLegacyFieldInsertion?: boolean;
|
||||||
};
|
};
|
||||||
requestMetadata: ApiRequestMetadata;
|
requestMetadata: ApiRequestMetadata;
|
||||||
@ -119,7 +119,6 @@ export const updateDocument = async ({
|
|||||||
|
|
||||||
// If no data just return the document since this function is normally chained after a meta update.
|
// If no data just return the document since this function is normally chained after a meta update.
|
||||||
if (!data || Object.values(data).length === 0) {
|
if (!data || Object.values(data).length === 0) {
|
||||||
console.log('no data');
|
|
||||||
return document;
|
return document;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -137,7 +136,7 @@ export const updateDocument = async ({
|
|||||||
data?.globalActionAuth === undefined ? documentGlobalActionAuth : data.globalActionAuth;
|
data?.globalActionAuth === undefined ? documentGlobalActionAuth : data.globalActionAuth;
|
||||||
|
|
||||||
// Check if user has permission to set the global action auth.
|
// Check if user has permission to set the global action auth.
|
||||||
if (newGlobalActionAuth) {
|
if (newGlobalActionAuth && newGlobalActionAuth.length > 0) {
|
||||||
const isDocumentEnterprise = await isUserEnterprise({
|
const isDocumentEnterprise = await isUserEnterprise({
|
||||||
userId,
|
userId,
|
||||||
teamId,
|
teamId,
|
||||||
|
|||||||
@ -3,7 +3,6 @@ import { FieldType } from '@prisma/client';
|
|||||||
|
|
||||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||||
import type { TRecipientActionAuth } from '../../types/document-auth';
|
import type { TRecipientActionAuth } from '../../types/document-auth';
|
||||||
import { extractDocumentAuthMethods } from '../../utils/document-auth';
|
|
||||||
import { isRecipientAuthorized } from './is-recipient-authorized';
|
import { isRecipientAuthorized } from './is-recipient-authorized';
|
||||||
|
|
||||||
export type ValidateFieldAuthOptions = {
|
export type ValidateFieldAuthOptions = {
|
||||||
@ -26,14 +25,9 @@ export const validateFieldAuth = async ({
|
|||||||
userId,
|
userId,
|
||||||
authOptions,
|
authOptions,
|
||||||
}: ValidateFieldAuthOptions) => {
|
}: ValidateFieldAuthOptions) => {
|
||||||
const { derivedRecipientActionAuth } = extractDocumentAuthMethods({
|
|
||||||
documentAuth: documentAuthOptions,
|
|
||||||
recipientAuth: recipient.authOptions,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Override all non-signature fields to not require any auth.
|
// Override all non-signature fields to not require any auth.
|
||||||
if (field.type !== FieldType.SIGNATURE) {
|
if (field.type !== FieldType.SIGNATURE) {
|
||||||
return null;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isValid = await isRecipientAuthorized({
|
const isValid = await isRecipientAuthorized({
|
||||||
@ -50,5 +44,5 @@ export const validateFieldAuth = async ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return derivedRecipientActionAuth;
|
return authOptions?.type;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -15,7 +15,7 @@ import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
|
|||||||
|
|
||||||
export type ViewedDocumentOptions = {
|
export type ViewedDocumentOptions = {
|
||||||
token: string;
|
token: string;
|
||||||
recipientAccessAuth?: TDocumentAccessAuthTypes | null;
|
recipientAccessAuth?: TDocumentAccessAuthTypes[];
|
||||||
requestMetadata?: RequestMetadata;
|
requestMetadata?: RequestMetadata;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -63,7 +63,7 @@ export const viewedDocument = async ({
|
|||||||
recipientId: recipient.id,
|
recipientId: recipient.id,
|
||||||
recipientName: recipient.name,
|
recipientName: recipient.name,
|
||||||
recipientRole: recipient.role,
|
recipientRole: recipient.role,
|
||||||
accessAuth: recipientAccessAuth || undefined,
|
accessAuth: recipientAccessAuth ?? [],
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@ -27,7 +27,6 @@ export const createEmbeddingPresignToken = async ({
|
|||||||
// In development mode, allow setting expiresIn to 0 for testing
|
// In development mode, allow setting expiresIn to 0 for testing
|
||||||
// In production, enforce a minimum expiration time
|
// In production, enforce a minimum expiration time
|
||||||
const isDevelopment = env('NODE_ENV') !== 'production';
|
const isDevelopment = env('NODE_ENV') !== 'production';
|
||||||
console.log('isDevelopment', isDevelopment);
|
|
||||||
const minExpirationMinutes = isDevelopment ? 0 : 5;
|
const minExpirationMinutes = isDevelopment ? 0 : 5;
|
||||||
|
|
||||||
// Ensure expiresIn is at least the minimum allowed value
|
// Ensure expiresIn is at least the minimum allowed value
|
||||||
|
|||||||
@ -22,8 +22,8 @@ export interface CreateDocumentRecipientsOptions {
|
|||||||
name: string;
|
name: string;
|
||||||
role: RecipientRole;
|
role: RecipientRole;
|
||||||
signingOrder?: number | null;
|
signingOrder?: number | null;
|
||||||
accessAuth?: TRecipientAccessAuthTypes | null;
|
accessAuth?: TRecipientAccessAuthTypes[];
|
||||||
actionAuth?: TRecipientActionAuthTypes | null;
|
actionAuth?: TRecipientActionAuthTypes[];
|
||||||
}[];
|
}[];
|
||||||
requestMetadata: ApiRequestMetadata;
|
requestMetadata: ApiRequestMetadata;
|
||||||
}
|
}
|
||||||
@ -71,7 +71,9 @@ export const createDocumentRecipients = async ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const recipientsHaveActionAuth = recipientsToCreate.some((recipient) => recipient.actionAuth);
|
const recipientsHaveActionAuth = recipientsToCreate.some(
|
||||||
|
(recipient) => recipient.actionAuth && recipient.actionAuth.length > 0,
|
||||||
|
);
|
||||||
|
|
||||||
// Check if user has permission to set the global action auth.
|
// Check if user has permission to set the global action auth.
|
||||||
if (recipientsHaveActionAuth) {
|
if (recipientsHaveActionAuth) {
|
||||||
@ -110,8 +112,8 @@ export const createDocumentRecipients = async ({
|
|||||||
return await Promise.all(
|
return await Promise.all(
|
||||||
normalizedRecipients.map(async (recipient) => {
|
normalizedRecipients.map(async (recipient) => {
|
||||||
const authOptions = createRecipientAuthOptions({
|
const authOptions = createRecipientAuthOptions({
|
||||||
accessAuth: recipient.accessAuth || null,
|
accessAuth: recipient.accessAuth ?? [],
|
||||||
actionAuth: recipient.actionAuth || null,
|
actionAuth: recipient.actionAuth ?? [],
|
||||||
});
|
});
|
||||||
|
|
||||||
const createdRecipient = await tx.recipient.create({
|
const createdRecipient = await tx.recipient.create({
|
||||||
@ -140,8 +142,8 @@ export const createDocumentRecipients = async ({
|
|||||||
recipientName: createdRecipient.name,
|
recipientName: createdRecipient.name,
|
||||||
recipientId: createdRecipient.id,
|
recipientId: createdRecipient.id,
|
||||||
recipientRole: createdRecipient.role,
|
recipientRole: createdRecipient.role,
|
||||||
accessAuth: recipient.accessAuth || undefined,
|
accessAuth: recipient.accessAuth ?? [],
|
||||||
actionAuth: recipient.actionAuth || undefined,
|
actionAuth: recipient.actionAuth ?? [],
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@ -19,8 +19,8 @@ export interface CreateTemplateRecipientsOptions {
|
|||||||
name: string;
|
name: string;
|
||||||
role: RecipientRole;
|
role: RecipientRole;
|
||||||
signingOrder?: number | null;
|
signingOrder?: number | null;
|
||||||
accessAuth?: TRecipientAccessAuthTypes | null;
|
accessAuth?: TRecipientAccessAuthTypes[];
|
||||||
actionAuth?: TRecipientActionAuthTypes | null;
|
actionAuth?: TRecipientActionAuthTypes[];
|
||||||
}[];
|
}[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -60,7 +60,9 @@ export const createTemplateRecipients = async ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const recipientsHaveActionAuth = recipientsToCreate.some((recipient) => recipient.actionAuth);
|
const recipientsHaveActionAuth = recipientsToCreate.some(
|
||||||
|
(recipient) => recipient.actionAuth && recipient.actionAuth.length > 0,
|
||||||
|
);
|
||||||
|
|
||||||
// Check if user has permission to set the global action auth.
|
// Check if user has permission to set the global action auth.
|
||||||
if (recipientsHaveActionAuth) {
|
if (recipientsHaveActionAuth) {
|
||||||
@ -99,8 +101,8 @@ export const createTemplateRecipients = async ({
|
|||||||
return await Promise.all(
|
return await Promise.all(
|
||||||
normalizedRecipients.map(async (recipient) => {
|
normalizedRecipients.map(async (recipient) => {
|
||||||
const authOptions = createRecipientAuthOptions({
|
const authOptions = createRecipientAuthOptions({
|
||||||
accessAuth: recipient.accessAuth || null,
|
accessAuth: recipient.accessAuth ?? [],
|
||||||
actionAuth: recipient.actionAuth || null,
|
actionAuth: recipient.actionAuth ?? [],
|
||||||
});
|
});
|
||||||
|
|
||||||
const createdRecipient = await tx.recipient.create({
|
const createdRecipient = await tx.recipient.create({
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { msg } from '@lingui/core/macro';
|
|||||||
import type { Recipient } from '@prisma/client';
|
import type { Recipient } from '@prisma/client';
|
||||||
import { RecipientRole } from '@prisma/client';
|
import { RecipientRole } from '@prisma/client';
|
||||||
import { SendStatus, SigningStatus } from '@prisma/client';
|
import { SendStatus, SigningStatus } from '@prisma/client';
|
||||||
|
import { isDeepEqual } from 'remeda';
|
||||||
|
|
||||||
import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
|
import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
|
||||||
import { mailer } from '@documenso/email/mailer';
|
import { mailer } from '@documenso/email/mailer';
|
||||||
@ -96,7 +97,9 @@ export const setDocumentRecipients = async ({
|
|||||||
throw new Error('Document already complete');
|
throw new Error('Document already complete');
|
||||||
}
|
}
|
||||||
|
|
||||||
const recipientsHaveActionAuth = recipients.some((recipient) => recipient.actionAuth);
|
const recipientsHaveActionAuth = recipients.some(
|
||||||
|
(recipient) => recipient.actionAuth && recipient.actionAuth.length > 0,
|
||||||
|
);
|
||||||
|
|
||||||
// Check if user has permission to set the global action auth.
|
// Check if user has permission to set the global action auth.
|
||||||
if (recipientsHaveActionAuth) {
|
if (recipientsHaveActionAuth) {
|
||||||
@ -245,8 +248,8 @@ export const setDocumentRecipients = async ({
|
|||||||
metadata: requestMetadata,
|
metadata: requestMetadata,
|
||||||
data: {
|
data: {
|
||||||
...baseAuditLog,
|
...baseAuditLog,
|
||||||
accessAuth: recipient.accessAuth || undefined,
|
accessAuth: recipient.accessAuth || [],
|
||||||
actionAuth: recipient.actionAuth || undefined,
|
actionAuth: recipient.actionAuth || [],
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
@ -361,8 +364,8 @@ type RecipientData = {
|
|||||||
name: string;
|
name: string;
|
||||||
role: RecipientRole;
|
role: RecipientRole;
|
||||||
signingOrder?: number | null;
|
signingOrder?: number | null;
|
||||||
accessAuth?: TRecipientAccessAuthTypes | null;
|
accessAuth?: TRecipientAccessAuthTypes[];
|
||||||
actionAuth?: TRecipientActionAuthTypes | null;
|
actionAuth?: TRecipientActionAuthTypes[];
|
||||||
};
|
};
|
||||||
|
|
||||||
type RecipientDataWithClientId = Recipient & {
|
type RecipientDataWithClientId = Recipient & {
|
||||||
@ -372,15 +375,15 @@ type RecipientDataWithClientId = Recipient & {
|
|||||||
const hasRecipientBeenChanged = (recipient: Recipient, newRecipientData: RecipientData) => {
|
const hasRecipientBeenChanged = (recipient: Recipient, newRecipientData: RecipientData) => {
|
||||||
const authOptions = ZRecipientAuthOptionsSchema.parse(recipient.authOptions);
|
const authOptions = ZRecipientAuthOptionsSchema.parse(recipient.authOptions);
|
||||||
|
|
||||||
const newRecipientAccessAuth = newRecipientData.accessAuth || null;
|
const newRecipientAccessAuth = newRecipientData.accessAuth || [];
|
||||||
const newRecipientActionAuth = newRecipientData.actionAuth || null;
|
const newRecipientActionAuth = newRecipientData.actionAuth || [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
recipient.email !== newRecipientData.email ||
|
recipient.email !== newRecipientData.email ||
|
||||||
recipient.name !== newRecipientData.name ||
|
recipient.name !== newRecipientData.name ||
|
||||||
recipient.role !== newRecipientData.role ||
|
recipient.role !== newRecipientData.role ||
|
||||||
recipient.signingOrder !== newRecipientData.signingOrder ||
|
recipient.signingOrder !== newRecipientData.signingOrder ||
|
||||||
authOptions.accessAuth !== newRecipientAccessAuth ||
|
!isDeepEqual(authOptions.accessAuth, newRecipientAccessAuth) ||
|
||||||
authOptions.actionAuth !== newRecipientActionAuth
|
!isDeepEqual(authOptions.actionAuth, newRecipientActionAuth)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -26,7 +26,7 @@ export type SetTemplateRecipientsOptions = {
|
|||||||
name: string;
|
name: string;
|
||||||
role: RecipientRole;
|
role: RecipientRole;
|
||||||
signingOrder?: number | null;
|
signingOrder?: number | null;
|
||||||
actionAuth?: TRecipientActionAuthTypes | null;
|
actionAuth?: TRecipientActionAuthTypes[];
|
||||||
}[];
|
}[];
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -64,7 +64,9 @@ export const setTemplateRecipients = async ({
|
|||||||
throw new Error('Template not found');
|
throw new Error('Template not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
const recipientsHaveActionAuth = recipients.some((recipient) => recipient.actionAuth);
|
const recipientsHaveActionAuth = recipients.some(
|
||||||
|
(recipient) => recipient.actionAuth && recipient.actionAuth.length > 0,
|
||||||
|
);
|
||||||
|
|
||||||
// Check if user has permission to set the global action auth.
|
// Check if user has permission to set the global action auth.
|
||||||
if (recipientsHaveActionAuth) {
|
if (recipientsHaveActionAuth) {
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import type { Recipient } from '@prisma/client';
|
import type { Recipient } from '@prisma/client';
|
||||||
import { RecipientRole } from '@prisma/client';
|
import { RecipientRole } from '@prisma/client';
|
||||||
import { SendStatus, SigningStatus } from '@prisma/client';
|
import { SendStatus, SigningStatus } from '@prisma/client';
|
||||||
|
import { isDeepEqual } from 'remeda';
|
||||||
|
|
||||||
import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
|
import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
|
||||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
||||||
@ -72,7 +73,9 @@ export const updateDocumentRecipients = async ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const recipientsHaveActionAuth = recipients.some((recipient) => recipient.actionAuth);
|
const recipientsHaveActionAuth = recipients.some(
|
||||||
|
(recipient) => recipient.actionAuth && recipient.actionAuth.length > 0,
|
||||||
|
);
|
||||||
|
|
||||||
// Check if user has permission to set the global action auth.
|
// Check if user has permission to set the global action auth.
|
||||||
if (recipientsHaveActionAuth) {
|
if (recipientsHaveActionAuth) {
|
||||||
@ -218,8 +221,8 @@ type RecipientData = {
|
|||||||
name?: string;
|
name?: string;
|
||||||
role?: RecipientRole;
|
role?: RecipientRole;
|
||||||
signingOrder?: number | null;
|
signingOrder?: number | null;
|
||||||
accessAuth?: TRecipientAccessAuthTypes | null;
|
accessAuth?: TRecipientAccessAuthTypes[];
|
||||||
actionAuth?: TRecipientActionAuthTypes | null;
|
actionAuth?: TRecipientActionAuthTypes[];
|
||||||
};
|
};
|
||||||
|
|
||||||
const hasRecipientBeenChanged = (recipient: Recipient, newRecipientData: RecipientData) => {
|
const hasRecipientBeenChanged = (recipient: Recipient, newRecipientData: RecipientData) => {
|
||||||
@ -233,7 +236,7 @@ const hasRecipientBeenChanged = (recipient: Recipient, newRecipientData: Recipie
|
|||||||
recipient.name !== newRecipientData.name ||
|
recipient.name !== newRecipientData.name ||
|
||||||
recipient.role !== newRecipientData.role ||
|
recipient.role !== newRecipientData.role ||
|
||||||
recipient.signingOrder !== newRecipientData.signingOrder ||
|
recipient.signingOrder !== newRecipientData.signingOrder ||
|
||||||
authOptions.accessAuth !== newRecipientAccessAuth ||
|
!isDeepEqual(authOptions.accessAuth, newRecipientAccessAuth) ||
|
||||||
authOptions.actionAuth !== newRecipientActionAuth
|
!isDeepEqual(authOptions.actionAuth, newRecipientActionAuth)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -20,7 +20,7 @@ export type UpdateRecipientOptions = {
|
|||||||
name?: string;
|
name?: string;
|
||||||
role?: RecipientRole;
|
role?: RecipientRole;
|
||||||
signingOrder?: number | null;
|
signingOrder?: number | null;
|
||||||
actionAuth?: TRecipientActionAuthTypes | null;
|
actionAuth?: TRecipientActionAuthTypes[];
|
||||||
userId: number;
|
userId: number;
|
||||||
teamId?: number;
|
teamId?: number;
|
||||||
requestMetadata?: RequestMetadata;
|
requestMetadata?: RequestMetadata;
|
||||||
@ -90,7 +90,7 @@ export const updateRecipient = async ({
|
|||||||
throw new Error('Recipient not found');
|
throw new Error('Recipient not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (actionAuth) {
|
if (actionAuth && actionAuth.length > 0) {
|
||||||
const isDocumentEnterprise = await isUserEnterprise({
|
const isDocumentEnterprise = await isUserEnterprise({
|
||||||
userId,
|
userId,
|
||||||
teamId,
|
teamId,
|
||||||
@ -117,7 +117,7 @@ export const updateRecipient = async ({
|
|||||||
signingOrder,
|
signingOrder,
|
||||||
authOptions: createRecipientAuthOptions({
|
authOptions: createRecipientAuthOptions({
|
||||||
accessAuth: recipientAuthOptions.accessAuth,
|
accessAuth: recipientAuthOptions.accessAuth,
|
||||||
actionAuth: actionAuth ?? null,
|
actionAuth: actionAuth ?? [],
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -22,8 +22,8 @@ export interface UpdateTemplateRecipientsOptions {
|
|||||||
name?: string;
|
name?: string;
|
||||||
role?: RecipientRole;
|
role?: RecipientRole;
|
||||||
signingOrder?: number | null;
|
signingOrder?: number | null;
|
||||||
accessAuth?: TRecipientAccessAuthTypes | null;
|
accessAuth?: TRecipientAccessAuthTypes[];
|
||||||
actionAuth?: TRecipientActionAuthTypes | null;
|
actionAuth?: TRecipientActionAuthTypes[];
|
||||||
}[];
|
}[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -63,7 +63,9 @@ export const updateTemplateRecipients = async ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const recipientsHaveActionAuth = recipients.some((recipient) => recipient.actionAuth);
|
const recipientsHaveActionAuth = recipients.some(
|
||||||
|
(recipient) => recipient.actionAuth && recipient.actionAuth.length > 0,
|
||||||
|
);
|
||||||
|
|
||||||
// Check if user has permission to set the global action auth.
|
// Check if user has permission to set the global action auth.
|
||||||
if (recipientsHaveActionAuth) {
|
if (recipientsHaveActionAuth) {
|
||||||
|
|||||||
@ -70,7 +70,7 @@ export type CreateDocumentFromDirectTemplateOptions = {
|
|||||||
|
|
||||||
type CreatedDirectRecipientField = {
|
type CreatedDirectRecipientField = {
|
||||||
field: Field & { signature?: Signature | null };
|
field: Field & { signature?: Signature | null };
|
||||||
derivedRecipientActionAuth: TRecipientActionAuthTypes | null;
|
derivedRecipientActionAuth?: TRecipientActionAuthTypes;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ZCreateDocumentFromDirectTemplateResponseSchema = z.object({
|
export const ZCreateDocumentFromDirectTemplateResponseSchema = z.object({
|
||||||
@ -151,9 +151,9 @@ export const createDocumentFromDirectTemplate = async ({
|
|||||||
const directRecipientName = user?.name || initialDirectRecipientName;
|
const directRecipientName = user?.name || initialDirectRecipientName;
|
||||||
|
|
||||||
// Ensure typesafety when we add more options.
|
// Ensure typesafety when we add more options.
|
||||||
const isAccessAuthValid = match(derivedRecipientAccessAuth)
|
const isAccessAuthValid = match(derivedRecipientAccessAuth.at(0))
|
||||||
.with(DocumentAccessAuth.ACCOUNT, () => user && user?.email === directRecipientEmail)
|
.with(DocumentAccessAuth.ACCOUNT, () => user && user?.email === directRecipientEmail)
|
||||||
.with(null, () => true)
|
.with(undefined, () => true)
|
||||||
.exhaustive();
|
.exhaustive();
|
||||||
|
|
||||||
if (!isAccessAuthValid) {
|
if (!isAccessAuthValid) {
|
||||||
@ -460,7 +460,7 @@ export const createDocumentFromDirectTemplate = async ({
|
|||||||
const createdDirectRecipientFields: CreatedDirectRecipientField[] = [
|
const createdDirectRecipientFields: CreatedDirectRecipientField[] = [
|
||||||
...createdDirectRecipient.fields.map((field) => ({
|
...createdDirectRecipient.fields.map((field) => ({
|
||||||
field,
|
field,
|
||||||
derivedRecipientActionAuth: null,
|
derivedRecipientActionAuth: undefined,
|
||||||
})),
|
})),
|
||||||
...createdDirectRecipientSignatureFields,
|
...createdDirectRecipientSignatureFields,
|
||||||
];
|
];
|
||||||
@ -567,6 +567,7 @@ export const createDocumentFromDirectTemplate = async ({
|
|||||||
recipientId: createdDirectRecipient.id,
|
recipientId: createdDirectRecipient.id,
|
||||||
recipientName: createdDirectRecipient.name,
|
recipientName: createdDirectRecipient.name,
|
||||||
recipientRole: createdDirectRecipient.role,
|
recipientRole: createdDirectRecipient.role,
|
||||||
|
actionAuth: createdDirectRecipient.authOptions?.actionAuth ?? [],
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
|
|||||||
@ -15,8 +15,8 @@ export type UpdateTemplateOptions = {
|
|||||||
title?: string;
|
title?: string;
|
||||||
externalId?: string | null;
|
externalId?: string | null;
|
||||||
visibility?: DocumentVisibility;
|
visibility?: DocumentVisibility;
|
||||||
globalAccessAuth?: TDocumentAccessAuthTypes | null;
|
globalAccessAuth?: TDocumentAccessAuthTypes[];
|
||||||
globalActionAuth?: TDocumentActionAuthTypes | null;
|
globalActionAuth?: TDocumentActionAuthTypes[];
|
||||||
publicTitle?: string;
|
publicTitle?: string;
|
||||||
publicDescription?: string;
|
publicDescription?: string;
|
||||||
type?: Template['type'];
|
type?: Template['type'];
|
||||||
@ -74,7 +74,7 @@ export const updateTemplate = async ({
|
|||||||
data?.globalActionAuth === undefined ? documentGlobalActionAuth : data.globalActionAuth;
|
data?.globalActionAuth === undefined ? documentGlobalActionAuth : data.globalActionAuth;
|
||||||
|
|
||||||
// Check if user has permission to set the global action auth.
|
// Check if user has permission to set the global action auth.
|
||||||
if (newGlobalActionAuth) {
|
if (newGlobalActionAuth && newGlobalActionAuth.length > 0) {
|
||||||
const isDocumentEnterprise = await isUserEnterprise({
|
const isDocumentEnterprise = await isUserEnterprise({
|
||||||
userId,
|
userId,
|
||||||
teamId,
|
teamId,
|
||||||
|
|||||||
@ -123,8 +123,8 @@ export const ZDocumentAuditLogFieldDiffSchema = z.union([
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
export const ZGenericFromToSchema = z.object({
|
export const ZGenericFromToSchema = z.object({
|
||||||
from: z.string().nullable(),
|
from: z.union([z.string(), z.array(z.string())]).nullable(),
|
||||||
to: z.string().nullable(),
|
to: z.union([z.string(), z.array(z.string())]).nullable(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const ZRecipientDiffActionAuthSchema = ZGenericFromToSchema.extend({
|
export const ZRecipientDiffActionAuthSchema = ZGenericFromToSchema.extend({
|
||||||
@ -296,7 +296,7 @@ export const ZDocumentAuditLogEventDocumentFieldInsertedSchema = z.object({
|
|||||||
},
|
},
|
||||||
z
|
z
|
||||||
.object({
|
.object({
|
||||||
type: ZRecipientActionAuthTypesSchema,
|
type: ZRecipientActionAuthTypesSchema.optional(),
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
),
|
),
|
||||||
@ -384,7 +384,7 @@ export const ZDocumentAuditLogEventDocumentFieldPrefilledSchema = z.object({
|
|||||||
},
|
},
|
||||||
z
|
z
|
||||||
.object({
|
.object({
|
||||||
type: ZRecipientActionAuthTypesSchema,
|
type: ZRecipientActionAuthTypesSchema.optional(),
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
),
|
),
|
||||||
@ -428,7 +428,13 @@ export const ZDocumentAuditLogEventDocumentMetaUpdatedSchema = z.object({
|
|||||||
export const ZDocumentAuditLogEventDocumentOpenedSchema = z.object({
|
export const ZDocumentAuditLogEventDocumentOpenedSchema = z.object({
|
||||||
type: z.literal(DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_OPENED),
|
type: z.literal(DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_OPENED),
|
||||||
data: ZBaseRecipientDataSchema.extend({
|
data: ZBaseRecipientDataSchema.extend({
|
||||||
accessAuth: z.string().optional(),
|
accessAuth: z.preprocess((unknownValue) => {
|
||||||
|
if (!unknownValue) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.isArray(unknownValue) ? unknownValue : [unknownValue];
|
||||||
|
}, z.array(ZRecipientAccessAuthTypesSchema)),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -438,7 +444,13 @@ export const ZDocumentAuditLogEventDocumentOpenedSchema = z.object({
|
|||||||
export const ZDocumentAuditLogEventDocumentRecipientCompleteSchema = z.object({
|
export const ZDocumentAuditLogEventDocumentRecipientCompleteSchema = z.object({
|
||||||
type: z.literal(DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_COMPLETED),
|
type: z.literal(DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_COMPLETED),
|
||||||
data: ZBaseRecipientDataSchema.extend({
|
data: ZBaseRecipientDataSchema.extend({
|
||||||
actionAuth: z.string().optional(),
|
actionAuth: z.preprocess((unknownValue) => {
|
||||||
|
if (!unknownValue) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.isArray(unknownValue) ? unknownValue : [unknownValue];
|
||||||
|
}, z.array(ZRecipientActionAuthTypesSchema)),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -516,8 +528,20 @@ export const ZDocumentAuditLogEventFieldUpdatedSchema = z.object({
|
|||||||
export const ZDocumentAuditLogEventRecipientAddedSchema = z.object({
|
export const ZDocumentAuditLogEventRecipientAddedSchema = z.object({
|
||||||
type: z.literal(DOCUMENT_AUDIT_LOG_TYPE.RECIPIENT_CREATED),
|
type: z.literal(DOCUMENT_AUDIT_LOG_TYPE.RECIPIENT_CREATED),
|
||||||
data: ZBaseRecipientDataSchema.extend({
|
data: ZBaseRecipientDataSchema.extend({
|
||||||
accessAuth: ZRecipientAccessAuthTypesSchema.optional(),
|
accessAuth: z.preprocess((unknownValue) => {
|
||||||
actionAuth: ZRecipientActionAuthTypesSchema.optional(),
|
if (!unknownValue) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.isArray(unknownValue) ? unknownValue : [unknownValue];
|
||||||
|
}, z.array(ZRecipientAccessAuthTypesSchema)),
|
||||||
|
actionAuth: z.preprocess((unknownValue) => {
|
||||||
|
if (!unknownValue) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.isArray(unknownValue) ? unknownValue : [unknownValue];
|
||||||
|
}, z.array(ZRecipientActionAuthTypesSchema)),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -9,8 +9,10 @@ export const ZDocumentAuthTypesSchema = z.enum([
|
|||||||
'ACCOUNT',
|
'ACCOUNT',
|
||||||
'PASSKEY',
|
'PASSKEY',
|
||||||
'TWO_FACTOR_AUTH',
|
'TWO_FACTOR_AUTH',
|
||||||
|
'PASSWORD',
|
||||||
'EXPLICIT_NONE',
|
'EXPLICIT_NONE',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const DocumentAuth = ZDocumentAuthTypesSchema.Enum;
|
export const DocumentAuth = ZDocumentAuthTypesSchema.Enum;
|
||||||
|
|
||||||
const ZDocumentAuthAccountSchema = z.object({
|
const ZDocumentAuthAccountSchema = z.object({
|
||||||
@ -27,6 +29,11 @@ const ZDocumentAuthPasskeySchema = z.object({
|
|||||||
tokenReference: z.string().min(1),
|
tokenReference: z.string().min(1),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const ZDocumentAuthPasswordSchema = z.object({
|
||||||
|
type: z.literal(DocumentAuth.PASSWORD),
|
||||||
|
password: z.string().min(1),
|
||||||
|
});
|
||||||
|
|
||||||
const ZDocumentAuth2FASchema = z.object({
|
const ZDocumentAuth2FASchema = z.object({
|
||||||
type: z.literal(DocumentAuth.TWO_FACTOR_AUTH),
|
type: z.literal(DocumentAuth.TWO_FACTOR_AUTH),
|
||||||
token: z.string().min(4).max(10),
|
token: z.string().min(4).max(10),
|
||||||
@ -40,6 +47,7 @@ export const ZDocumentAuthMethodsSchema = z.discriminatedUnion('type', [
|
|||||||
ZDocumentAuthExplicitNoneSchema,
|
ZDocumentAuthExplicitNoneSchema,
|
||||||
ZDocumentAuthPasskeySchema,
|
ZDocumentAuthPasskeySchema,
|
||||||
ZDocumentAuth2FASchema,
|
ZDocumentAuth2FASchema,
|
||||||
|
ZDocumentAuthPasswordSchema,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -61,9 +69,15 @@ export const ZDocumentActionAuthSchema = z.discriminatedUnion('type', [
|
|||||||
ZDocumentAuthAccountSchema,
|
ZDocumentAuthAccountSchema,
|
||||||
ZDocumentAuthPasskeySchema,
|
ZDocumentAuthPasskeySchema,
|
||||||
ZDocumentAuth2FASchema,
|
ZDocumentAuth2FASchema,
|
||||||
|
ZDocumentAuthPasswordSchema,
|
||||||
]);
|
]);
|
||||||
export const ZDocumentActionAuthTypesSchema = z
|
export const ZDocumentActionAuthTypesSchema = z
|
||||||
.enum([DocumentAuth.ACCOUNT, DocumentAuth.PASSKEY, DocumentAuth.TWO_FACTOR_AUTH])
|
.enum([
|
||||||
|
DocumentAuth.ACCOUNT,
|
||||||
|
DocumentAuth.PASSKEY,
|
||||||
|
DocumentAuth.TWO_FACTOR_AUTH,
|
||||||
|
DocumentAuth.PASSWORD,
|
||||||
|
])
|
||||||
.describe(
|
.describe(
|
||||||
'The type of authentication required for the recipient to sign the document. This field is restricted to Enterprise plan users only.',
|
'The type of authentication required for the recipient to sign the document. This field is restricted to Enterprise plan users only.',
|
||||||
);
|
);
|
||||||
@ -89,6 +103,7 @@ export const ZRecipientActionAuthSchema = z.discriminatedUnion('type', [
|
|||||||
ZDocumentAuthAccountSchema,
|
ZDocumentAuthAccountSchema,
|
||||||
ZDocumentAuthPasskeySchema,
|
ZDocumentAuthPasskeySchema,
|
||||||
ZDocumentAuth2FASchema,
|
ZDocumentAuth2FASchema,
|
||||||
|
ZDocumentAuthPasswordSchema,
|
||||||
ZDocumentAuthExplicitNoneSchema,
|
ZDocumentAuthExplicitNoneSchema,
|
||||||
]);
|
]);
|
||||||
export const ZRecipientActionAuthTypesSchema = z
|
export const ZRecipientActionAuthTypesSchema = z
|
||||||
@ -96,6 +111,7 @@ export const ZRecipientActionAuthTypesSchema = z
|
|||||||
DocumentAuth.ACCOUNT,
|
DocumentAuth.ACCOUNT,
|
||||||
DocumentAuth.PASSKEY,
|
DocumentAuth.PASSKEY,
|
||||||
DocumentAuth.TWO_FACTOR_AUTH,
|
DocumentAuth.TWO_FACTOR_AUTH,
|
||||||
|
DocumentAuth.PASSWORD,
|
||||||
DocumentAuth.EXPLICIT_NONE,
|
DocumentAuth.EXPLICIT_NONE,
|
||||||
])
|
])
|
||||||
.describe('The type of authentication required for the recipient to sign the document.');
|
.describe('The type of authentication required for the recipient to sign the document.');
|
||||||
@ -110,18 +126,26 @@ export const RecipientActionAuth = ZRecipientActionAuthTypesSchema.Enum;
|
|||||||
*/
|
*/
|
||||||
export const ZDocumentAuthOptionsSchema = z.preprocess(
|
export const ZDocumentAuthOptionsSchema = z.preprocess(
|
||||||
(unknownValue) => {
|
(unknownValue) => {
|
||||||
if (unknownValue) {
|
if (!unknownValue || typeof unknownValue !== 'object') {
|
||||||
return unknownValue;
|
return {
|
||||||
|
globalAccessAuth: [],
|
||||||
|
globalActionAuth: [],
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const globalAccessAuth =
|
||||||
|
'globalAccessAuth' in unknownValue ? processAuthValue(unknownValue.globalAccessAuth) : [];
|
||||||
|
const globalActionAuth =
|
||||||
|
'globalActionAuth' in unknownValue ? processAuthValue(unknownValue.globalActionAuth) : [];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
globalAccessAuth: null,
|
globalAccessAuth,
|
||||||
globalActionAuth: null,
|
globalActionAuth,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
z.object({
|
z.object({
|
||||||
globalAccessAuth: ZDocumentAccessAuthTypesSchema.nullable(),
|
globalAccessAuth: z.array(ZDocumentAccessAuthTypesSchema),
|
||||||
globalActionAuth: ZDocumentActionAuthTypesSchema.nullable(),
|
globalActionAuth: z.array(ZDocumentActionAuthTypesSchema),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -130,21 +154,46 @@ export const ZDocumentAuthOptionsSchema = z.preprocess(
|
|||||||
*/
|
*/
|
||||||
export const ZRecipientAuthOptionsSchema = z.preprocess(
|
export const ZRecipientAuthOptionsSchema = z.preprocess(
|
||||||
(unknownValue) => {
|
(unknownValue) => {
|
||||||
if (unknownValue) {
|
if (!unknownValue || typeof unknownValue !== 'object') {
|
||||||
return unknownValue;
|
return {
|
||||||
|
accessAuth: [],
|
||||||
|
actionAuth: [],
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const accessAuth =
|
||||||
|
'accessAuth' in unknownValue ? processAuthValue(unknownValue.accessAuth) : [];
|
||||||
|
const actionAuth =
|
||||||
|
'actionAuth' in unknownValue ? processAuthValue(unknownValue.actionAuth) : [];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
accessAuth: null,
|
accessAuth,
|
||||||
actionAuth: null,
|
actionAuth,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
z.object({
|
z.object({
|
||||||
accessAuth: ZRecipientAccessAuthTypesSchema.nullable(),
|
accessAuth: z.array(ZRecipientAccessAuthTypesSchema),
|
||||||
actionAuth: ZRecipientActionAuthTypesSchema.nullable(),
|
actionAuth: z.array(ZRecipientActionAuthTypesSchema),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility function to process the auth value.
|
||||||
|
*
|
||||||
|
* Converts the old singular auth value to an array of auth values.
|
||||||
|
*/
|
||||||
|
const processAuthValue = (value: unknown) => {
|
||||||
|
if (value === null || value === undefined) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [value];
|
||||||
|
};
|
||||||
|
|
||||||
export type TDocumentAuth = z.infer<typeof ZDocumentAuthTypesSchema>;
|
export type TDocumentAuth = z.infer<typeof ZDocumentAuthTypesSchema>;
|
||||||
export type TDocumentAuthMethods = z.infer<typeof ZDocumentAuthMethodsSchema>;
|
export type TDocumentAuthMethods = z.infer<typeof ZDocumentAuthMethodsSchema>;
|
||||||
export type TDocumentAuthOptions = z.infer<typeof ZDocumentAuthOptionsSchema>;
|
export type TDocumentAuthOptions = z.infer<typeof ZDocumentAuthOptionsSchema>;
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import type { I18n } from '@lingui/core';
|
|||||||
import { msg } from '@lingui/core/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import type { DocumentAuditLog, DocumentMeta, Field, Recipient } from '@prisma/client';
|
import type { DocumentAuditLog, DocumentMeta, Field, Recipient } from '@prisma/client';
|
||||||
import { RecipientRole } from '@prisma/client';
|
import { RecipientRole } from '@prisma/client';
|
||||||
|
import { isDeepEqual } from 'remeda';
|
||||||
import { match } from 'ts-pattern';
|
import { match } from 'ts-pattern';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
@ -106,7 +107,7 @@ export const diffRecipientChanges = (
|
|||||||
const newActionAuth =
|
const newActionAuth =
|
||||||
newAuthOptions?.actionAuth === undefined ? oldActionAuth : newAuthOptions.actionAuth;
|
newAuthOptions?.actionAuth === undefined ? oldActionAuth : newAuthOptions.actionAuth;
|
||||||
|
|
||||||
if (oldAccessAuth !== newAccessAuth) {
|
if (!isDeepEqual(oldAccessAuth, newAccessAuth)) {
|
||||||
diffs.push({
|
diffs.push({
|
||||||
type: RECIPIENT_DIFF_TYPE.ACCESS_AUTH,
|
type: RECIPIENT_DIFF_TYPE.ACCESS_AUTH,
|
||||||
from: oldAccessAuth ?? '',
|
from: oldAccessAuth ?? '',
|
||||||
@ -114,7 +115,7 @@ export const diffRecipientChanges = (
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (oldActionAuth !== newActionAuth) {
|
if (!isDeepEqual(oldActionAuth, newActionAuth)) {
|
||||||
diffs.push({
|
diffs.push({
|
||||||
type: RECIPIENT_DIFF_TYPE.ACTION_AUTH,
|
type: RECIPIENT_DIFF_TYPE.ACTION_AUTH,
|
||||||
from: oldActionAuth ?? '',
|
from: oldActionAuth ?? '',
|
||||||
|
|||||||
@ -27,17 +27,21 @@ export const extractDocumentAuthMethods = ({
|
|||||||
const documentAuthOption = ZDocumentAuthOptionsSchema.parse(documentAuth);
|
const documentAuthOption = ZDocumentAuthOptionsSchema.parse(documentAuth);
|
||||||
const recipientAuthOption = ZRecipientAuthOptionsSchema.parse(recipientAuth);
|
const recipientAuthOption = ZRecipientAuthOptionsSchema.parse(recipientAuth);
|
||||||
|
|
||||||
const derivedRecipientAccessAuth: TRecipientAccessAuthTypes | null =
|
const derivedRecipientAccessAuth: TRecipientAccessAuthTypes[] =
|
||||||
recipientAuthOption.accessAuth || documentAuthOption.globalAccessAuth;
|
recipientAuthOption.accessAuth.length > 0
|
||||||
|
? recipientAuthOption.accessAuth
|
||||||
|
: documentAuthOption.globalAccessAuth;
|
||||||
|
|
||||||
const derivedRecipientActionAuth: TRecipientActionAuthTypes | null =
|
const derivedRecipientActionAuth: TRecipientActionAuthTypes[] =
|
||||||
recipientAuthOption.actionAuth || documentAuthOption.globalActionAuth;
|
recipientAuthOption.actionAuth.length > 0
|
||||||
|
? recipientAuthOption.actionAuth
|
||||||
|
: documentAuthOption.globalActionAuth;
|
||||||
|
|
||||||
const recipientAccessAuthRequired = derivedRecipientAccessAuth !== null;
|
const recipientAccessAuthRequired = derivedRecipientAccessAuth.length > 0;
|
||||||
|
|
||||||
const recipientActionAuthRequired =
|
const recipientActionAuthRequired =
|
||||||
derivedRecipientActionAuth !== DocumentAuth.EXPLICIT_NONE &&
|
derivedRecipientActionAuth.length > 0 &&
|
||||||
derivedRecipientActionAuth !== null;
|
!derivedRecipientActionAuth.includes(DocumentAuth.EXPLICIT_NONE);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
derivedRecipientAccessAuth,
|
derivedRecipientAccessAuth,
|
||||||
@ -54,8 +58,8 @@ export const extractDocumentAuthMethods = ({
|
|||||||
*/
|
*/
|
||||||
export const createDocumentAuthOptions = (options: TDocumentAuthOptions): TDocumentAuthOptions => {
|
export const createDocumentAuthOptions = (options: TDocumentAuthOptions): TDocumentAuthOptions => {
|
||||||
return {
|
return {
|
||||||
globalAccessAuth: options?.globalAccessAuth ?? null,
|
globalAccessAuth: options?.globalAccessAuth ?? [],
|
||||||
globalActionAuth: options?.globalActionAuth ?? null,
|
globalActionAuth: options?.globalActionAuth ?? [],
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -66,7 +70,7 @@ export const createRecipientAuthOptions = (
|
|||||||
options: TRecipientAuthOptions,
|
options: TRecipientAuthOptions,
|
||||||
): TRecipientAuthOptions => {
|
): TRecipientAuthOptions => {
|
||||||
return {
|
return {
|
||||||
accessAuth: options?.accessAuth ?? null,
|
accessAuth: options?.accessAuth ?? [],
|
||||||
actionAuth: options?.actionAuth ?? null,
|
actionAuth: options?.actionAuth ?? [],
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@ -206,8 +206,8 @@ export const ZCreateDocumentV2RequestSchema = z.object({
|
|||||||
title: ZDocumentTitleSchema,
|
title: ZDocumentTitleSchema,
|
||||||
externalId: ZDocumentExternalIdSchema.optional(),
|
externalId: ZDocumentExternalIdSchema.optional(),
|
||||||
visibility: ZDocumentVisibilitySchema.optional(),
|
visibility: ZDocumentVisibilitySchema.optional(),
|
||||||
globalAccessAuth: ZDocumentAccessAuthTypesSchema.optional(),
|
globalAccessAuth: z.array(ZDocumentAccessAuthTypesSchema).optional(),
|
||||||
globalActionAuth: ZDocumentActionAuthTypesSchema.optional(),
|
globalActionAuth: z.array(ZDocumentActionAuthTypesSchema).optional(),
|
||||||
formValues: ZDocumentFormValuesSchema.optional(),
|
formValues: ZDocumentFormValuesSchema.optional(),
|
||||||
recipients: z
|
recipients: z
|
||||||
.array(
|
.array(
|
||||||
|
|||||||
@ -42,8 +42,8 @@ export const ZUpdateDocumentRequestSchema = z.object({
|
|||||||
title: ZDocumentTitleSchema.optional(),
|
title: ZDocumentTitleSchema.optional(),
|
||||||
externalId: ZDocumentExternalIdSchema.nullish(),
|
externalId: ZDocumentExternalIdSchema.nullish(),
|
||||||
visibility: ZDocumentVisibilitySchema.optional(),
|
visibility: ZDocumentVisibilitySchema.optional(),
|
||||||
globalAccessAuth: ZDocumentAccessAuthTypesSchema.nullish(),
|
globalAccessAuth: z.array(ZDocumentAccessAuthTypesSchema).optional(),
|
||||||
globalActionAuth: ZDocumentActionAuthTypesSchema.nullish(),
|
globalActionAuth: z.array(ZDocumentActionAuthTypesSchema).optional(),
|
||||||
useLegacyFieldInsertion: z.boolean().optional(),
|
useLegacyFieldInsertion: z.boolean().optional(),
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
|
|||||||
@ -26,8 +26,8 @@ export const ZCreateRecipientSchema = z.object({
|
|||||||
name: z.string(),
|
name: z.string(),
|
||||||
role: z.nativeEnum(RecipientRole),
|
role: z.nativeEnum(RecipientRole),
|
||||||
signingOrder: z.number().optional(),
|
signingOrder: z.number().optional(),
|
||||||
accessAuth: ZRecipientAccessAuthTypesSchema.optional().nullable(),
|
accessAuth: z.array(ZRecipientAccessAuthTypesSchema).optional().default([]),
|
||||||
actionAuth: ZRecipientActionAuthTypesSchema.optional().nullable(),
|
actionAuth: z.array(ZRecipientActionAuthTypesSchema).optional().default([]),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const ZUpdateRecipientSchema = z.object({
|
export const ZUpdateRecipientSchema = z.object({
|
||||||
@ -36,8 +36,8 @@ export const ZUpdateRecipientSchema = z.object({
|
|||||||
name: z.string().optional(),
|
name: z.string().optional(),
|
||||||
role: z.nativeEnum(RecipientRole).optional(),
|
role: z.nativeEnum(RecipientRole).optional(),
|
||||||
signingOrder: z.number().optional(),
|
signingOrder: z.number().optional(),
|
||||||
accessAuth: ZRecipientAccessAuthTypesSchema.optional().nullable(),
|
accessAuth: z.array(ZRecipientAccessAuthTypesSchema).optional().default([]),
|
||||||
actionAuth: ZRecipientActionAuthTypesSchema.optional().nullable(),
|
actionAuth: z.array(ZRecipientActionAuthTypesSchema).optional().default([]),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const ZCreateDocumentRecipientRequestSchema = z.object({
|
export const ZCreateDocumentRecipientRequestSchema = z.object({
|
||||||
@ -106,7 +106,7 @@ export const ZSetDocumentRecipientsRequestSchema = z
|
|||||||
name: z.string(),
|
name: z.string(),
|
||||||
role: z.nativeEnum(RecipientRole),
|
role: z.nativeEnum(RecipientRole),
|
||||||
signingOrder: z.number().optional(),
|
signingOrder: z.number().optional(),
|
||||||
actionAuth: ZRecipientActionAuthTypesSchema.optional().nullable(),
|
actionAuth: z.array(ZRecipientActionAuthTypesSchema).optional().default([]),
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
@ -190,7 +190,7 @@ export const ZSetTemplateRecipientsRequestSchema = z
|
|||||||
name: z.string(),
|
name: z.string(),
|
||||||
role: z.nativeEnum(RecipientRole),
|
role: z.nativeEnum(RecipientRole),
|
||||||
signingOrder: z.number().optional(),
|
signingOrder: z.number().optional(),
|
||||||
actionAuth: ZRecipientActionAuthTypesSchema.optional().nullable(),
|
actionAuth: z.array(ZRecipientActionAuthTypesSchema).optional().default([]),
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
|
|||||||
@ -133,8 +133,8 @@ export const ZUpdateTemplateRequestSchema = z.object({
|
|||||||
title: z.string().min(1).optional(),
|
title: z.string().min(1).optional(),
|
||||||
externalId: z.string().nullish(),
|
externalId: z.string().nullish(),
|
||||||
visibility: z.nativeEnum(DocumentVisibility).optional(),
|
visibility: z.nativeEnum(DocumentVisibility).optional(),
|
||||||
globalAccessAuth: ZDocumentAccessAuthTypesSchema.nullable().optional(),
|
globalAccessAuth: z.array(ZDocumentAccessAuthTypesSchema).optional().default([]),
|
||||||
globalActionAuth: ZDocumentActionAuthTypesSchema.nullable().optional(),
|
globalActionAuth: z.array(ZDocumentActionAuthTypesSchema).optional().default([]),
|
||||||
publicTitle: z
|
publicTitle: z
|
||||||
.string()
|
.string()
|
||||||
.trim()
|
.trim()
|
||||||
|
|||||||
@ -1,51 +1,75 @@
|
|||||||
import { forwardRef } from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { msg } from '@lingui/core/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
import { Trans } from '@lingui/react/macro';
|
import { Trans } from '@lingui/react/macro';
|
||||||
import type { SelectProps } from '@radix-ui/react-select';
|
|
||||||
import { InfoIcon } from 'lucide-react';
|
import { InfoIcon } from 'lucide-react';
|
||||||
|
|
||||||
import { DOCUMENT_AUTH_TYPES } from '@documenso/lib/constants/document-auth';
|
import { DOCUMENT_AUTH_TYPES } from '@documenso/lib/constants/document-auth';
|
||||||
import { DocumentAccessAuth } from '@documenso/lib/types/document-auth';
|
import { DocumentAccessAuth } from '@documenso/lib/types/document-auth';
|
||||||
import {
|
import { MultiSelect, type Option } from '@documenso/ui/primitives/multiselect';
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from '@documenso/ui/primitives/select';
|
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip';
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip';
|
||||||
|
|
||||||
export const DocumentGlobalAuthAccessSelect = forwardRef<HTMLButtonElement, SelectProps>(
|
export interface DocumentGlobalAuthAccessSelectProps {
|
||||||
(props, ref) => {
|
value?: string[];
|
||||||
const { _ } = useLingui();
|
defaultValue?: string[];
|
||||||
|
onValueChange?: (value: string[]) => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
placeholder?: string;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
export const DocumentGlobalAuthAccessSelect = ({
|
||||||
<Select {...props}>
|
value,
|
||||||
<SelectTrigger ref={ref} className="bg-background text-muted-foreground">
|
defaultValue,
|
||||||
<SelectValue
|
onValueChange,
|
||||||
data-testid="documentAccessSelectValue"
|
disabled,
|
||||||
placeholder={_(msg`No restrictions`)}
|
placeholder,
|
||||||
/>
|
}: DocumentGlobalAuthAccessSelectProps) => {
|
||||||
</SelectTrigger>
|
const { _ } = useLingui();
|
||||||
|
|
||||||
<SelectContent position="popper">
|
// Convert auth types to MultiSelect options
|
||||||
{/* Note: -1 is remapped in the Zod schema to the required value. */}
|
const authOptions: Option[] = [
|
||||||
<SelectItem value={'-1'}>
|
{
|
||||||
<Trans>No restrictions</Trans>
|
value: '-1',
|
||||||
</SelectItem>
|
label: _(msg`No restrictions`),
|
||||||
|
},
|
||||||
|
...Object.values(DocumentAccessAuth).map((authType) => ({
|
||||||
|
value: authType,
|
||||||
|
label: DOCUMENT_AUTH_TYPES[authType].value,
|
||||||
|
})),
|
||||||
|
];
|
||||||
|
|
||||||
{Object.values(DocumentAccessAuth).map((authType) => (
|
// Convert string array to Option array for MultiSelect
|
||||||
<SelectItem key={authType} value={authType}>
|
const selectedOptions =
|
||||||
{DOCUMENT_AUTH_TYPES[authType].value}
|
(value
|
||||||
</SelectItem>
|
?.map((val) => authOptions.find((option) => option.value === val))
|
||||||
))}
|
.filter(Boolean) as Option[]) || [];
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
// Convert default value to Option array
|
||||||
);
|
const defaultOptions =
|
||||||
},
|
(defaultValue
|
||||||
);
|
?.map((val) => authOptions.find((option) => option.value === val))
|
||||||
|
.filter(Boolean) as Option[]) || [];
|
||||||
|
|
||||||
|
const handleChange = (options: Option[]) => {
|
||||||
|
const values = options.map((option) => option.value);
|
||||||
|
onValueChange?.(values);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MultiSelect
|
||||||
|
value={selectedOptions}
|
||||||
|
defaultOptions={defaultOptions}
|
||||||
|
options={authOptions}
|
||||||
|
onChange={handleChange}
|
||||||
|
disabled={disabled}
|
||||||
|
placeholder={placeholder || _(msg`Select access methods`)}
|
||||||
|
className="bg-background text-muted-foreground"
|
||||||
|
hideClearAllButton={false}
|
||||||
|
data-testid="documentAccessSelectValue"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
DocumentGlobalAuthAccessSelect.displayName = 'DocumentGlobalAuthAccessSelect';
|
DocumentGlobalAuthAccessSelect.displayName = 'DocumentGlobalAuthAccessSelect';
|
||||||
|
|
||||||
@ -63,7 +87,11 @@ export const DocumentGlobalAuthAccessTooltip = () => (
|
|||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
<Trans>The authentication required for recipients to view the document.</Trans>
|
<Trans>The authentication methods required for recipients to view the document.</Trans>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p className="mt-2">
|
||||||
|
<Trans>Multiple access methods can be selected.</Trans>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<ul className="ml-3.5 list-outside list-disc space-y-0.5 py-2">
|
<ul className="ml-3.5 list-outside list-disc space-y-0.5 py-2">
|
||||||
|
|||||||
@ -1,54 +1,75 @@
|
|||||||
import { forwardRef } from 'react';
|
|
||||||
|
|
||||||
import { msg } from '@lingui/core/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
import { Trans } from '@lingui/react/macro';
|
import { Trans } from '@lingui/react/macro';
|
||||||
import type { SelectProps } from '@radix-ui/react-select';
|
|
||||||
import { InfoIcon } from 'lucide-react';
|
import { InfoIcon } from 'lucide-react';
|
||||||
|
|
||||||
import { DOCUMENT_AUTH_TYPES } from '@documenso/lib/constants/document-auth';
|
import { DOCUMENT_AUTH_TYPES } from '@documenso/lib/constants/document-auth';
|
||||||
import { DocumentActionAuth, DocumentAuth } from '@documenso/lib/types/document-auth';
|
import { DocumentActionAuth, DocumentAuth } from '@documenso/lib/types/document-auth';
|
||||||
import {
|
import { MultiSelect, type Option } from '@documenso/ui/primitives/multiselect';
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from '@documenso/ui/primitives/select';
|
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip';
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip';
|
||||||
|
|
||||||
export const DocumentGlobalAuthActionSelect = forwardRef<HTMLButtonElement, SelectProps>(
|
export interface DocumentGlobalAuthActionSelectProps {
|
||||||
(props, ref) => {
|
value?: string[];
|
||||||
const { _ } = useLingui();
|
defaultValue?: string[];
|
||||||
|
onValueChange?: (value: string[]) => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
placeholder?: string;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
export const DocumentGlobalAuthActionSelect = ({
|
||||||
<Select {...props}>
|
value,
|
||||||
<SelectTrigger className="bg-background text-muted-foreground">
|
defaultValue,
|
||||||
<SelectValue
|
onValueChange,
|
||||||
ref={ref}
|
disabled,
|
||||||
data-testid="documentActionSelectValue"
|
placeholder,
|
||||||
placeholder={_(msg`No restrictions`)}
|
}: DocumentGlobalAuthActionSelectProps) => {
|
||||||
/>
|
const { _ } = useLingui();
|
||||||
</SelectTrigger>
|
|
||||||
|
|
||||||
<SelectContent position="popper">
|
// Convert auth types to MultiSelect options
|
||||||
{/* Note: -1 is remapped in the Zod schema to the required value. */}
|
const authOptions: Option[] = [
|
||||||
<SelectItem value={'-1'}>
|
{
|
||||||
<Trans>No restrictions</Trans>
|
value: '-1',
|
||||||
</SelectItem>
|
label: _(msg`No restrictions`),
|
||||||
|
},
|
||||||
|
...Object.values(DocumentActionAuth)
|
||||||
|
.filter((auth) => auth !== DocumentAuth.ACCOUNT)
|
||||||
|
.map((authType) => ({
|
||||||
|
value: authType,
|
||||||
|
label: DOCUMENT_AUTH_TYPES[authType].value,
|
||||||
|
})),
|
||||||
|
];
|
||||||
|
|
||||||
{Object.values(DocumentActionAuth)
|
// Convert string array to Option array for MultiSelect
|
||||||
.filter((auth) => auth !== DocumentAuth.ACCOUNT)
|
const selectedOptions =
|
||||||
.map((authType) => (
|
(value
|
||||||
<SelectItem key={authType} value={authType}>
|
?.map((val) => authOptions.find((option) => option.value === val))
|
||||||
{DOCUMENT_AUTH_TYPES[authType].value}
|
.filter(Boolean) as Option[]) || [];
|
||||||
</SelectItem>
|
|
||||||
))}
|
// Convert default value to Option array
|
||||||
</SelectContent>
|
const defaultOptions =
|
||||||
</Select>
|
(defaultValue
|
||||||
);
|
?.map((val) => authOptions.find((option) => option.value === val))
|
||||||
},
|
.filter(Boolean) as Option[]) || [];
|
||||||
);
|
|
||||||
|
const handleChange = (options: Option[]) => {
|
||||||
|
const values = options.map((option) => option.value);
|
||||||
|
onValueChange?.(values);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MultiSelect
|
||||||
|
value={selectedOptions}
|
||||||
|
defaultOptions={defaultOptions}
|
||||||
|
options={authOptions}
|
||||||
|
onChange={handleChange}
|
||||||
|
disabled={disabled}
|
||||||
|
placeholder={placeholder || _(msg`Select authentication methods`)}
|
||||||
|
className="bg-background text-muted-foreground"
|
||||||
|
hideClearAllButton={false}
|
||||||
|
data-testid="documentActionSelectValue"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
DocumentGlobalAuthActionSelect.displayName = 'DocumentGlobalAuthActionSelect';
|
DocumentGlobalAuthActionSelect.displayName = 'DocumentGlobalAuthActionSelect';
|
||||||
|
|
||||||
@ -64,20 +85,19 @@ export const DocumentGlobalAuthActionTooltip = () => (
|
|||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
<Trans>The authentication required for recipients to sign the signature field.</Trans>
|
<Trans>
|
||||||
|
The authentication methods required for recipients to sign the signature field.
|
||||||
|
</Trans>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
<Trans>
|
<Trans>
|
||||||
This can be overriden by setting the authentication requirements directly on each
|
These can be overriden by setting the authentication requirements directly on each
|
||||||
recipient in the next step.
|
recipient in the next step. Multiple methods can be selected.
|
||||||
</Trans>
|
</Trans>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<ul className="ml-3.5 list-outside list-disc space-y-0.5 py-2">
|
<ul className="ml-3.5 list-outside list-disc space-y-0.5 py-2">
|
||||||
{/* <li>
|
|
||||||
<strong>Require account</strong> - The recipient must be signed in
|
|
||||||
</li> */}
|
|
||||||
<li>
|
<li>
|
||||||
<Trans>
|
<Trans>
|
||||||
<strong>Require passkey</strong> - The recipient must have an account and passkey
|
<strong>Require passkey</strong> - The recipient must have an account and passkey
|
||||||
@ -90,6 +110,14 @@ export const DocumentGlobalAuthActionTooltip = () => (
|
|||||||
their settings
|
their settings
|
||||||
</Trans>
|
</Trans>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
<li>
|
||||||
|
<Trans>
|
||||||
|
<strong>Require password</strong> - The recipient must have an account and password
|
||||||
|
configured via their settings, the password will be verified during signing
|
||||||
|
</Trans>
|
||||||
|
</li>
|
||||||
|
|
||||||
<li>
|
<li>
|
||||||
<Trans>
|
<Trans>
|
||||||
<strong>No restrictions</strong> - No authentication required
|
<strong>No restrictions</strong> - No authentication required
|
||||||
|
|||||||
@ -3,97 +3,124 @@ import React from 'react';
|
|||||||
import { msg } from '@lingui/core/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
import { Trans } from '@lingui/react/macro';
|
import { Trans } from '@lingui/react/macro';
|
||||||
import type { SelectProps } from '@radix-ui/react-select';
|
|
||||||
import { InfoIcon } from 'lucide-react';
|
import { InfoIcon } from 'lucide-react';
|
||||||
|
|
||||||
import { DOCUMENT_AUTH_TYPES } from '@documenso/lib/constants/document-auth';
|
import { DOCUMENT_AUTH_TYPES } from '@documenso/lib/constants/document-auth';
|
||||||
import { RecipientActionAuth } from '@documenso/lib/types/document-auth';
|
import { RecipientActionAuth } from '@documenso/lib/types/document-auth';
|
||||||
import {
|
import { MultiSelect, type Option } from '@documenso/ui/primitives/multiselect';
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from '@documenso/ui/primitives/select';
|
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip';
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip';
|
||||||
|
|
||||||
export type RecipientActionAuthSelectProps = SelectProps;
|
export interface RecipientActionAuthSelectProps {
|
||||||
|
value?: string[];
|
||||||
|
defaultValue?: string[];
|
||||||
|
onValueChange?: (value: string[]) => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
placeholder?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export const RecipientActionAuthSelect = (props: RecipientActionAuthSelectProps) => {
|
export const RecipientActionAuthSelect = ({
|
||||||
|
value,
|
||||||
|
defaultValue,
|
||||||
|
onValueChange,
|
||||||
|
disabled,
|
||||||
|
placeholder,
|
||||||
|
}: RecipientActionAuthSelectProps) => {
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
|
|
||||||
|
// Convert auth types to MultiSelect options
|
||||||
|
const authOptions: Option[] = [
|
||||||
|
{
|
||||||
|
value: '-1',
|
||||||
|
label: _(msg`Inherit authentication method`),
|
||||||
|
},
|
||||||
|
...Object.values(RecipientActionAuth)
|
||||||
|
.filter((auth) => auth !== RecipientActionAuth.ACCOUNT)
|
||||||
|
.map((authType) => ({
|
||||||
|
value: authType,
|
||||||
|
label: DOCUMENT_AUTH_TYPES[authType].value,
|
||||||
|
})),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Convert string array to Option array for MultiSelect
|
||||||
|
const selectedOptions =
|
||||||
|
(value
|
||||||
|
?.map((val) => authOptions.find((option) => option.value === val))
|
||||||
|
.filter(Boolean) as Option[]) || [];
|
||||||
|
|
||||||
|
// Convert default value to Option array
|
||||||
|
const defaultOptions =
|
||||||
|
(defaultValue
|
||||||
|
?.map((val) => authOptions.find((option) => option.value === val))
|
||||||
|
.filter(Boolean) as Option[]) || [];
|
||||||
|
|
||||||
|
const handleChange = (options: Option[]) => {
|
||||||
|
const values = options.map((option) => option.value);
|
||||||
|
onValueChange?.(values);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Select {...props}>
|
<div className="relative">
|
||||||
<SelectTrigger className="bg-background text-muted-foreground">
|
<MultiSelect
|
||||||
<SelectValue placeholder={_(msg`Inherit authentication method`)} />
|
value={selectedOptions}
|
||||||
|
defaultOptions={defaultOptions}
|
||||||
|
options={authOptions}
|
||||||
|
onChange={handleChange}
|
||||||
|
disabled={disabled}
|
||||||
|
placeholder={placeholder || _(msg`Select authentication methods`)}
|
||||||
|
className="bg-background text-muted-foreground"
|
||||||
|
maxSelected={4} // Allow selecting up to 4 auth methods
|
||||||
|
hideClearAllButton={false}
|
||||||
|
/>
|
||||||
|
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger className="-mr-1 ml-auto">
|
<TooltipTrigger className="absolute right-2 top-1/2 -translate-y-1/2">
|
||||||
<InfoIcon className="mx-2 h-4 w-4" />
|
<InfoIcon className="h-4 w-4" />
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
|
|
||||||
<TooltipContent className="text-foreground max-w-md p-4">
|
<TooltipContent className="text-foreground max-w-md p-4">
|
||||||
<h2>
|
<h2>
|
||||||
<strong>
|
<strong>
|
||||||
<Trans>Recipient action authentication</Trans>
|
<Trans>Recipient action authentication</Trans>
|
||||||
</strong>
|
</strong>
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
<Trans>The authentication required for recipients to sign fields</Trans>
|
<Trans>The authentication methods required for recipients to sign fields</Trans>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p className="mt-2">
|
<p className="mt-2">
|
||||||
<Trans>This will override any global settings.</Trans>
|
<Trans>
|
||||||
</p>
|
These will override any global settings. Multiple methods can be selected.
|
||||||
|
</Trans>
|
||||||
|
</p>
|
||||||
|
|
||||||
<ul className="ml-3.5 list-outside list-disc space-y-0.5 py-2">
|
<ul className="ml-3.5 list-outside list-disc space-y-0.5 py-2">
|
||||||
<li>
|
<li>
|
||||||
<Trans>
|
<Trans>
|
||||||
<strong>Inherit authentication method</strong> - Use the global action signing
|
<strong>Inherit authentication method</strong> - Use the global action signing
|
||||||
authentication method configured in the "General Settings" step
|
authentication method configured in the "General Settings" step
|
||||||
</Trans>
|
</Trans>
|
||||||
</li>
|
</li>
|
||||||
{/* <li>
|
<li>
|
||||||
<strong>Require account</strong> - The recipient must be
|
<Trans>
|
||||||
signed in
|
<strong>Require passkey</strong> - The recipient must have an account and passkey
|
||||||
</li> */}
|
configured via their settings
|
||||||
<li>
|
</Trans>
|
||||||
<Trans>
|
</li>
|
||||||
<strong>Require passkey</strong> - The recipient must have an account and passkey
|
<li>
|
||||||
configured via their settings
|
<Trans>
|
||||||
</Trans>
|
<strong>Require 2FA</strong> - The recipient must have an account and 2FA enabled
|
||||||
</li>
|
via their settings
|
||||||
<li>
|
</Trans>
|
||||||
<Trans>
|
</li>
|
||||||
<strong>Require 2FA</strong> - The recipient must have an account and 2FA enabled
|
<li>
|
||||||
via their settings
|
<Trans>
|
||||||
</Trans>
|
<strong>None</strong> - No authentication required
|
||||||
</li>
|
</Trans>
|
||||||
<li>
|
</li>
|
||||||
<Trans>
|
</ul>
|
||||||
<strong>None</strong> - No authentication required
|
</TooltipContent>
|
||||||
</Trans>
|
</Tooltip>
|
||||||
</li>
|
</div>
|
||||||
</ul>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</SelectTrigger>
|
|
||||||
|
|
||||||
<SelectContent position="popper">
|
|
||||||
{/* Note: -1 is remapped in the Zod schema to the required value. */}
|
|
||||||
<SelectItem value="-1">
|
|
||||||
<Trans>Inherit authentication method</Trans>
|
|
||||||
</SelectItem>
|
|
||||||
|
|
||||||
{Object.values(RecipientActionAuth)
|
|
||||||
.filter((auth) => auth !== RecipientActionAuth.ACCOUNT)
|
|
||||||
.map((authType) => (
|
|
||||||
<SelectItem key={authType} value={authType}>
|
|
||||||
{DOCUMENT_AUTH_TYPES[authType].value}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -98,8 +98,8 @@ export const AddSettingsFormPartial = ({
|
|||||||
title: document.title,
|
title: document.title,
|
||||||
externalId: document.externalId || '',
|
externalId: document.externalId || '',
|
||||||
visibility: document.visibility || '',
|
visibility: document.visibility || '',
|
||||||
globalAccessAuth: documentAuthOption?.globalAccessAuth || undefined,
|
globalAccessAuth: documentAuthOption?.globalAccessAuth || [],
|
||||||
globalActionAuth: documentAuthOption?.globalActionAuth || undefined,
|
globalActionAuth: documentAuthOption?.globalActionAuth || [],
|
||||||
|
|
||||||
meta: {
|
meta: {
|
||||||
timezone:
|
timezone:
|
||||||
@ -131,6 +131,12 @@ export const AddSettingsFormPartial = ({
|
|||||||
)
|
)
|
||||||
.otherwise(() => false);
|
.otherwise(() => false);
|
||||||
|
|
||||||
|
const onFormSubmit = form.handleSubmit(onSubmit);
|
||||||
|
|
||||||
|
const onGoNextClick = () => {
|
||||||
|
void onFormSubmit().catch(console.error);
|
||||||
|
};
|
||||||
|
|
||||||
// We almost always want to set the timezone to the user's local timezone to avoid confusion
|
// We almost always want to set the timezone to the user's local timezone to avoid confusion
|
||||||
// when the document is signed.
|
// when the document is signed.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -214,7 +220,11 @@ export const AddSettingsFormPartial = ({
|
|||||||
</FormLabel>
|
</FormLabel>
|
||||||
|
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Select {...field} onValueChange={field.onChange}>
|
<Select
|
||||||
|
value={field.value}
|
||||||
|
disabled={field.disabled}
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
>
|
||||||
<SelectTrigger className="bg-background">
|
<SelectTrigger className="bg-background">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
@ -244,7 +254,11 @@ export const AddSettingsFormPartial = ({
|
|||||||
</FormLabel>
|
</FormLabel>
|
||||||
|
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<DocumentGlobalAuthAccessSelect {...field} onValueChange={field.onChange} />
|
<DocumentGlobalAuthAccessSelect
|
||||||
|
value={field.value}
|
||||||
|
disabled={field.disabled}
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
@ -286,7 +300,11 @@ export const AddSettingsFormPartial = ({
|
|||||||
</FormLabel>
|
</FormLabel>
|
||||||
|
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<DocumentGlobalAuthActionSelect {...field} onValueChange={field.onChange} />
|
<DocumentGlobalAuthActionSelect
|
||||||
|
value={field.value}
|
||||||
|
disabled={field.disabled}
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
@ -370,7 +388,7 @@ export const AddSettingsFormPartial = ({
|
|||||||
|
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Select
|
<Select
|
||||||
{...field}
|
value={field.value}
|
||||||
onValueChange={field.onChange}
|
onValueChange={field.onChange}
|
||||||
disabled={documentHasBeenSent}
|
disabled={documentHasBeenSent}
|
||||||
>
|
>
|
||||||
@ -406,7 +424,7 @@ export const AddSettingsFormPartial = ({
|
|||||||
<Combobox
|
<Combobox
|
||||||
className="bg-background"
|
className="bg-background"
|
||||||
options={TIME_ZONES}
|
options={TIME_ZONES}
|
||||||
{...field}
|
value={field.value}
|
||||||
onChange={(value) => value && field.onChange(value)}
|
onChange={(value) => value && field.onChange(value)}
|
||||||
disabled={documentHasBeenSent}
|
disabled={documentHasBeenSent}
|
||||||
/>
|
/>
|
||||||
@ -461,7 +479,7 @@ export const AddSettingsFormPartial = ({
|
|||||||
disabled={form.formState.isSubmitting}
|
disabled={form.formState.isSubmitting}
|
||||||
canGoBack={stepIndex !== 0}
|
canGoBack={stepIndex !== 0}
|
||||||
onGoBackClick={previousStep}
|
onGoBackClick={previousStep}
|
||||||
onGoNextClick={form.handleSubmit(onSubmit)}
|
onGoNextClick={onGoNextClick}
|
||||||
/>
|
/>
|
||||||
</DocumentFlowFormContainerFooter>
|
</DocumentFlowFormContainerFooter>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -16,17 +16,6 @@ import {
|
|||||||
ZDocumentMetaTimezoneSchema,
|
ZDocumentMetaTimezoneSchema,
|
||||||
} from '@documenso/trpc/server/document-router/schema';
|
} from '@documenso/trpc/server/document-router/schema';
|
||||||
|
|
||||||
export const ZMapNegativeOneToUndefinedSchema = z
|
|
||||||
.string()
|
|
||||||
.optional()
|
|
||||||
.transform((val) => {
|
|
||||||
if (val === '-1') {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
return val;
|
|
||||||
});
|
|
||||||
|
|
||||||
export const ZAddSettingsFormSchema = z.object({
|
export const ZAddSettingsFormSchema = z.object({
|
||||||
title: z
|
title: z
|
||||||
.string()
|
.string()
|
||||||
@ -34,12 +23,8 @@ export const ZAddSettingsFormSchema = z.object({
|
|||||||
.min(1, { message: msg`Title cannot be empty`.id }),
|
.min(1, { message: msg`Title cannot be empty`.id }),
|
||||||
externalId: z.string().optional(),
|
externalId: z.string().optional(),
|
||||||
visibility: z.nativeEnum(DocumentVisibility).optional(),
|
visibility: z.nativeEnum(DocumentVisibility).optional(),
|
||||||
globalAccessAuth: ZMapNegativeOneToUndefinedSchema.pipe(
|
globalAccessAuth: z.array(ZDocumentAccessAuthTypesSchema),
|
||||||
ZDocumentAccessAuthTypesSchema.optional(),
|
globalActionAuth: z.array(ZDocumentActionAuthTypesSchema),
|
||||||
),
|
|
||||||
globalActionAuth: ZMapNegativeOneToUndefinedSchema.pipe(
|
|
||||||
ZDocumentActionAuthTypesSchema.optional(),
|
|
||||||
),
|
|
||||||
meta: z.object({
|
meta: z.object({
|
||||||
timezone: ZDocumentMetaTimezoneSchema.optional().default(DEFAULT_DOCUMENT_TIME_ZONE),
|
timezone: ZDocumentMetaTimezoneSchema.optional().default(DEFAULT_DOCUMENT_TIME_ZONE),
|
||||||
dateFormat: ZDocumentMetaDateFormatSchema.optional().default(DEFAULT_DOCUMENT_DATE_FORMAT),
|
dateFormat: ZDocumentMetaDateFormatSchema.optional().default(DEFAULT_DOCUMENT_DATE_FORMAT),
|
||||||
|
|||||||
@ -85,7 +85,7 @@ export const AddSignersFormPartial = ({
|
|||||||
email: '',
|
email: '',
|
||||||
role: RecipientRole.SIGNER,
|
role: RecipientRole.SIGNER,
|
||||||
signingOrder: 1,
|
signingOrder: 1,
|
||||||
actionAuth: undefined,
|
actionAuth: [],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -119,10 +119,14 @@ export const AddSignersFormPartial = ({
|
|||||||
const recipientHasAuthOptions = recipients.find((recipient) => {
|
const recipientHasAuthOptions = recipients.find((recipient) => {
|
||||||
const recipientAuthOptions = ZRecipientAuthOptionsSchema.parse(recipient.authOptions);
|
const recipientAuthOptions = ZRecipientAuthOptionsSchema.parse(recipient.authOptions);
|
||||||
|
|
||||||
return recipientAuthOptions?.accessAuth || recipientAuthOptions?.actionAuth;
|
return (
|
||||||
|
recipientAuthOptions.accessAuth.length > 0 || recipientAuthOptions.actionAuth.length > 0
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const formHasActionAuth = form.getValues('signers').find((signer) => signer.actionAuth);
|
const formHasActionAuth = form
|
||||||
|
.getValues('signers')
|
||||||
|
.find((signer) => signer.actionAuth.length > 0);
|
||||||
|
|
||||||
return recipientHasAuthOptions !== undefined || formHasActionAuth !== undefined;
|
return recipientHasAuthOptions !== undefined || formHasActionAuth !== undefined;
|
||||||
}, [recipients, form]);
|
}, [recipients, form]);
|
||||||
@ -190,7 +194,7 @@ export const AddSignersFormPartial = ({
|
|||||||
name: '',
|
name: '',
|
||||||
email: '',
|
email: '',
|
||||||
role: RecipientRole.SIGNER,
|
role: RecipientRole.SIGNER,
|
||||||
actionAuth: undefined,
|
actionAuth: [],
|
||||||
signingOrder: signers.length > 0 ? (signers[signers.length - 1]?.signingOrder ?? 0) + 1 : 1,
|
signingOrder: signers.length > 0 ? (signers[signers.length - 1]?.signingOrder ?? 0) + 1 : 1,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@ -226,7 +230,7 @@ export const AddSignersFormPartial = ({
|
|||||||
name: user?.name ?? '',
|
name: user?.name ?? '',
|
||||||
email: user?.email ?? '',
|
email: user?.email ?? '',
|
||||||
role: RecipientRole.SIGNER,
|
role: RecipientRole.SIGNER,
|
||||||
actionAuth: undefined,
|
actionAuth: [],
|
||||||
signingOrder: signers.length > 0 ? (signers[signers.length - 1]?.signingOrder ?? 0) + 1 : 1,
|
signingOrder: signers.length > 0 ? (signers[signers.length - 1]?.signingOrder ?? 0) + 1 : 1,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,8 +4,6 @@ import { z } from 'zod';
|
|||||||
|
|
||||||
import { ZRecipientActionAuthTypesSchema } from '@documenso/lib/types/document-auth';
|
import { ZRecipientActionAuthTypesSchema } from '@documenso/lib/types/document-auth';
|
||||||
|
|
||||||
import { ZMapNegativeOneToUndefinedSchema } from './add-settings.types';
|
|
||||||
|
|
||||||
export const ZAddSignersFormSchema = z
|
export const ZAddSignersFormSchema = z
|
||||||
.object({
|
.object({
|
||||||
signers: z.array(
|
signers: z.array(
|
||||||
@ -19,9 +17,7 @@ export const ZAddSignersFormSchema = z
|
|||||||
name: z.string(),
|
name: z.string(),
|
||||||
role: z.nativeEnum(RecipientRole),
|
role: z.nativeEnum(RecipientRole),
|
||||||
signingOrder: z.number().optional(),
|
signingOrder: z.number().optional(),
|
||||||
actionAuth: ZMapNegativeOneToUndefinedSchema.pipe(
|
actionAuth: z.array(ZRecipientActionAuthTypesSchema).optional().default([]),
|
||||||
ZRecipientActionAuthTypesSchema.optional(),
|
|
||||||
),
|
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
signingOrder: z.nativeEnum(DocumentSigningOrder),
|
signingOrder: z.nativeEnum(DocumentSigningOrder),
|
||||||
|
|||||||
@ -78,6 +78,8 @@ interface MultiSelectProps {
|
|||||||
>;
|
>;
|
||||||
/** hide the clear all button. */
|
/** hide the clear all button. */
|
||||||
hideClearAllButton?: boolean;
|
hideClearAllButton?: boolean;
|
||||||
|
/** test id for the select value. */
|
||||||
|
'data-testid'?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MultiSelectRef {
|
export interface MultiSelectRef {
|
||||||
@ -170,6 +172,7 @@ const MultiSelect = ({
|
|||||||
commandProps,
|
commandProps,
|
||||||
inputProps,
|
inputProps,
|
||||||
hideClearAllButton = false,
|
hideClearAllButton = false,
|
||||||
|
'data-testid': dataTestId,
|
||||||
}: MultiSelectProps) => {
|
}: MultiSelectProps) => {
|
||||||
const inputRef = React.useRef<HTMLInputElement>(null);
|
const inputRef = React.useRef<HTMLInputElement>(null);
|
||||||
const [open, setOpen] = React.useState(false);
|
const [open, setOpen] = React.useState(false);
|
||||||
@ -403,6 +406,7 @@ const MultiSelect = ({
|
|||||||
commandProps?.shouldFilter !== undefined ? commandProps.shouldFilter : !onSearch
|
commandProps?.shouldFilter !== undefined ? commandProps.shouldFilter : !onSearch
|
||||||
} // When onSearch is provided, we don‘t want to filter the options. You can still override it.
|
} // When onSearch is provided, we don‘t want to filter the options. You can still override it.
|
||||||
filter={commandFilter()}
|
filter={commandFilter()}
|
||||||
|
data-testid={dataTestId}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
|
|||||||
@ -86,7 +86,7 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
|
|||||||
{
|
{
|
||||||
formId: initialId,
|
formId: initialId,
|
||||||
role: RecipientRole.SIGNER,
|
role: RecipientRole.SIGNER,
|
||||||
actionAuth: undefined,
|
actionAuth: [],
|
||||||
...generateRecipientPlaceholder(1),
|
...generateRecipientPlaceholder(1),
|
||||||
signingOrder: 1,
|
signingOrder: 1,
|
||||||
},
|
},
|
||||||
@ -136,10 +136,14 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
|
|||||||
const recipientHasAuthOptions = recipients.find((recipient) => {
|
const recipientHasAuthOptions = recipients.find((recipient) => {
|
||||||
const recipientAuthOptions = ZRecipientAuthOptionsSchema.parse(recipient.authOptions);
|
const recipientAuthOptions = ZRecipientAuthOptionsSchema.parse(recipient.authOptions);
|
||||||
|
|
||||||
return recipientAuthOptions?.accessAuth || recipientAuthOptions?.actionAuth;
|
return (
|
||||||
|
recipientAuthOptions.accessAuth.length > 0 || recipientAuthOptions.actionAuth.length > 0
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const formHasActionAuth = form.getValues('signers').find((signer) => signer.actionAuth);
|
const formHasActionAuth = form
|
||||||
|
.getValues('signers')
|
||||||
|
.find((signer) => signer.actionAuth.length > 0);
|
||||||
|
|
||||||
return recipientHasAuthOptions !== undefined || formHasActionAuth !== undefined;
|
return recipientHasAuthOptions !== undefined || formHasActionAuth !== undefined;
|
||||||
}, [recipients, form]);
|
}, [recipients, form]);
|
||||||
@ -179,6 +183,7 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
|
|||||||
email: user.email ?? '',
|
email: user.email ?? '',
|
||||||
role: RecipientRole.SIGNER,
|
role: RecipientRole.SIGNER,
|
||||||
signingOrder: signers.length > 0 ? (signers[signers.length - 1]?.signingOrder ?? 0) + 1 : 1,
|
signingOrder: signers.length > 0 ? (signers[signers.length - 1]?.signingOrder ?? 0) + 1 : 1,
|
||||||
|
actionAuth: [],
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -188,6 +193,7 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
|
|||||||
role: RecipientRole.SIGNER,
|
role: RecipientRole.SIGNER,
|
||||||
...generateRecipientPlaceholder(placeholderRecipientCount),
|
...generateRecipientPlaceholder(placeholderRecipientCount),
|
||||||
signingOrder: signers.length > 0 ? (signers[signers.length - 1]?.signingOrder ?? 0) + 1 : 1,
|
signingOrder: signers.length > 0 ? (signers[signers.length - 1]?.signingOrder ?? 0) + 1 : 1,
|
||||||
|
actionAuth: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
setPlaceholderRecipientCount((count) => count + 1);
|
setPlaceholderRecipientCount((count) => count + 1);
|
||||||
|
|||||||
@ -3,8 +3,6 @@ import { z } from 'zod';
|
|||||||
|
|
||||||
import { ZRecipientActionAuthTypesSchema } from '@documenso/lib/types/document-auth';
|
import { ZRecipientActionAuthTypesSchema } from '@documenso/lib/types/document-auth';
|
||||||
|
|
||||||
import { ZMapNegativeOneToUndefinedSchema } from '../document-flow/add-settings.types';
|
|
||||||
|
|
||||||
export const ZAddTemplatePlacholderRecipientsFormSchema = z
|
export const ZAddTemplatePlacholderRecipientsFormSchema = z
|
||||||
.object({
|
.object({
|
||||||
signers: z.array(
|
signers: z.array(
|
||||||
@ -15,9 +13,7 @@ export const ZAddTemplatePlacholderRecipientsFormSchema = z
|
|||||||
name: z.string(),
|
name: z.string(),
|
||||||
role: z.nativeEnum(RecipientRole),
|
role: z.nativeEnum(RecipientRole),
|
||||||
signingOrder: z.number().optional(),
|
signingOrder: z.number().optional(),
|
||||||
actionAuth: ZMapNegativeOneToUndefinedSchema.pipe(
|
actionAuth: z.array(ZRecipientActionAuthTypesSchema).optional().default([]),
|
||||||
ZRecipientActionAuthTypesSchema.optional(),
|
|
||||||
),
|
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
signingOrder: z.nativeEnum(DocumentSigningOrder),
|
signingOrder: z.nativeEnum(DocumentSigningOrder),
|
||||||
|
|||||||
@ -106,8 +106,8 @@ export const AddTemplateSettingsFormPartial = ({
|
|||||||
title: template.title,
|
title: template.title,
|
||||||
externalId: template.externalId || undefined,
|
externalId: template.externalId || undefined,
|
||||||
visibility: template.visibility || '',
|
visibility: template.visibility || '',
|
||||||
globalAccessAuth: documentAuthOption?.globalAccessAuth || undefined,
|
globalAccessAuth: documentAuthOption?.globalAccessAuth || [],
|
||||||
globalActionAuth: documentAuthOption?.globalActionAuth || undefined,
|
globalActionAuth: documentAuthOption?.globalActionAuth || [],
|
||||||
meta: {
|
meta: {
|
||||||
subject: template.templateMeta?.subject ?? '',
|
subject: template.templateMeta?.subject ?? '',
|
||||||
message: template.templateMeta?.message ?? '',
|
message: template.templateMeta?.message ?? '',
|
||||||
@ -237,7 +237,11 @@ export const AddTemplateSettingsFormPartial = ({
|
|||||||
</FormLabel>
|
</FormLabel>
|
||||||
|
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<DocumentGlobalAuthAccessSelect {...field} onValueChange={field.onChange} />
|
<DocumentGlobalAuthAccessSelect
|
||||||
|
value={field.value}
|
||||||
|
disabled={field.disabled}
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
@ -378,7 +382,11 @@ export const AddTemplateSettingsFormPartial = ({
|
|||||||
</FormLabel>
|
</FormLabel>
|
||||||
|
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<DocumentGlobalAuthActionSelect {...field} onValueChange={field.onChange} />
|
<DocumentGlobalAuthActionSelect
|
||||||
|
value={field.value}
|
||||||
|
disabled={field.disabled}
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -18,18 +18,12 @@ import {
|
|||||||
ZDocumentMetaTimezoneSchema,
|
ZDocumentMetaTimezoneSchema,
|
||||||
} from '@documenso/trpc/server/document-router/schema';
|
} from '@documenso/trpc/server/document-router/schema';
|
||||||
|
|
||||||
import { ZMapNegativeOneToUndefinedSchema } from '../document-flow/add-settings.types';
|
|
||||||
|
|
||||||
export const ZAddTemplateSettingsFormSchema = z.object({
|
export const ZAddTemplateSettingsFormSchema = z.object({
|
||||||
title: z.string().trim().min(1, { message: "Title can't be empty" }),
|
title: z.string().trim().min(1, { message: "Title can't be empty" }),
|
||||||
externalId: z.string().optional(),
|
externalId: z.string().optional(),
|
||||||
visibility: z.nativeEnum(DocumentVisibility).optional(),
|
visibility: z.nativeEnum(DocumentVisibility).optional(),
|
||||||
globalAccessAuth: ZMapNegativeOneToUndefinedSchema.pipe(
|
globalAccessAuth: z.array(ZDocumentAccessAuthTypesSchema).optional().default([]),
|
||||||
ZDocumentAccessAuthTypesSchema.optional(),
|
globalActionAuth: z.array(ZDocumentActionAuthTypesSchema).optional().default([]),
|
||||||
),
|
|
||||||
globalActionAuth: ZMapNegativeOneToUndefinedSchema.pipe(
|
|
||||||
ZDocumentActionAuthTypesSchema.optional(),
|
|
||||||
),
|
|
||||||
meta: z.object({
|
meta: z.object({
|
||||||
subject: z.string(),
|
subject: z.string(),
|
||||||
message: z.string(),
|
message: z.string(),
|
||||||
|
|||||||
Reference in New Issue
Block a user