mirror of
https://github.com/documenso/documenso.git
synced 2025-11-13 08:13:56 +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}
|
||||
disabled={
|
||||
field.disabled ||
|
||||
derivedRecipientAccessAuth !== null ||
|
||||
derivedRecipientAccessAuth.length > 0 ||
|
||||
user?.email !== undefined
|
||||
}
|
||||
placeholder="recipient@documenso.com"
|
||||
|
||||
@ -1,5 +1,8 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import type { FieldType } from '@prisma/client';
|
||||
import { ChevronLeftIcon } from 'lucide-react';
|
||||
import { P, match } from 'ts-pattern';
|
||||
|
||||
import {
|
||||
@ -7,6 +10,7 @@ import {
|
||||
type TRecipientActionAuth,
|
||||
type TRecipientActionAuthTypes,
|
||||
} from '@documenso/lib/types/document-auth';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@ -18,11 +22,12 @@ import {
|
||||
import { DocumentSigningAuth2FA } from './document-signing-auth-2fa';
|
||||
import { DocumentSigningAuthAccount } from './document-signing-auth-account';
|
||||
import { DocumentSigningAuthPasskey } from './document-signing-auth-passkey';
|
||||
import { DocumentSigningAuthPassword } from './document-signing-auth-password';
|
||||
import { useRequiredDocumentSigningAuthContext } from './document-signing-auth-provider';
|
||||
|
||||
export type DocumentSigningAuthDialogProps = {
|
||||
title?: string;
|
||||
documentAuthType: TRecipientActionAuthTypes;
|
||||
availableAuthTypes: TRecipientActionAuthTypes[];
|
||||
description?: string;
|
||||
actionTarget: FieldType | 'DOCUMENT';
|
||||
open: boolean;
|
||||
@ -37,33 +42,130 @@ export type DocumentSigningAuthDialogProps = {
|
||||
export const DocumentSigningAuthDialog = ({
|
||||
title,
|
||||
description,
|
||||
documentAuthType,
|
||||
availableAuthTypes,
|
||||
open,
|
||||
onOpenChange,
|
||||
onReauthFormSubmit,
|
||||
}: DocumentSigningAuthDialogProps) => {
|
||||
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) => {
|
||||
if (isCurrentlyAuthenticating) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Reset selected auth type when dialog closes
|
||||
if (!value) {
|
||||
setSelectedAuthType(() => {
|
||||
if (validAuthTypes.length === 1) {
|
||||
return validAuthTypes[0];
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
onOpenChange(value);
|
||||
};
|
||||
|
||||
const handleBackToChooser = () => {
|
||||
setSelectedAuthType(null);
|
||||
};
|
||||
|
||||
// If no valid auth types available, don't render anything
|
||||
if (validAuthTypes.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={handleOnOpenChange}>
|
||||
<DialogContent>
|
||||
<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>
|
||||
{description || <Trans>Reauthentication is required to sign this field</Trans>}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
{match({ documentAuthType, user })
|
||||
{/* Show chooser if no auth type is selected and there are multiple options */}
|
||||
{!selectedAuthType && validAuthTypes.length > 1 && (
|
||||
<div className="space-y-4">
|
||||
<p className="text-muted-foreground text-sm">
|
||||
<Trans>Choose your preferred authentication method:</Trans>
|
||||
</p>
|
||||
<div className="grid gap-2">
|
||||
{validAuthTypes.map((authType) => (
|
||||
<Button
|
||||
key={authType}
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="h-auto justify-start p-4"
|
||||
onClick={() => setSelectedAuthType(authType)}
|
||||
>
|
||||
<div className="text-left">
|
||||
<div className="font-medium">
|
||||
{match(authType)
|
||||
.with(DocumentAuth.ACCOUNT, () => <Trans>Account</Trans>)
|
||||
.with(DocumentAuth.PASSKEY, () => <Trans>Passkey</Trans>)
|
||||
.with(DocumentAuth.TWO_FACTOR_AUTH, () => <Trans>2FA</Trans>)
|
||||
.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.
|
||||
@ -83,6 +185,13 @@ export const DocumentSigningAuthDialog = ({
|
||||
onReauthFormSubmit={onReauthFormSubmit}
|
||||
/>
|
||||
))
|
||||
.with({ documentAuthType: DocumentAuth.PASSWORD }, () => (
|
||||
<DocumentSigningAuthPassword
|
||||
open={open}
|
||||
onOpenChange={onOpenChange}
|
||||
onReauthFormSubmit={onReauthFormSubmit}
|
||||
/>
|
||||
))
|
||||
.with({ documentAuthType: DocumentAuth.EXPLICIT_NONE }, () => null)
|
||||
.exhaustive()}
|
||||
</DialogContent>
|
||||
|
||||
@ -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 { 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 { MAXIMUM_PASSKEYS } from '@documenso/lib/constants/auth';
|
||||
@ -33,8 +32,8 @@ export type DocumentSigningAuthContextValue = {
|
||||
recipient: Recipient;
|
||||
recipientAuthOption: TRecipientAuthOptions;
|
||||
setRecipient: (_value: Recipient) => void;
|
||||
derivedRecipientAccessAuth: TRecipientAccessAuthTypes | null;
|
||||
derivedRecipientActionAuth: TRecipientActionAuthTypes | null;
|
||||
derivedRecipientAccessAuth: TRecipientAccessAuthTypes[];
|
||||
derivedRecipientActionAuth: TRecipientActionAuthTypes[];
|
||||
isAuthRedirectRequired: boolean;
|
||||
isCurrentlyAuthenticating: boolean;
|
||||
setIsCurrentlyAuthenticating: (_value: boolean) => void;
|
||||
@ -100,7 +99,7 @@ export const DocumentSigningAuthProvider = ({
|
||||
},
|
||||
{
|
||||
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
|
||||
* authentication.
|
||||
*/
|
||||
const preCalculatedActionAuthOptions = match(derivedRecipientActionAuth)
|
||||
.with(DocumentAuth.ACCOUNT, () => {
|
||||
if (recipient.email !== user?.email) {
|
||||
return null;
|
||||
const preCalculatedActionAuthOptions = useMemo(() => {
|
||||
if (
|
||||
!derivedRecipientActionAuth ||
|
||||
derivedRecipientActionAuth.length === 0 ||
|
||||
derivedRecipientActionAuth.includes(DocumentAuth.EXPLICIT_NONE)
|
||||
) {
|
||||
return {
|
||||
type: DocumentAuth.EXPLICIT_NONE,
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
derivedRecipientActionAuth.includes(DocumentAuth.ACCOUNT) &&
|
||||
user?.email == recipient.email
|
||||
) {
|
||||
return {
|
||||
type: DocumentAuth.ACCOUNT,
|
||||
};
|
||||
})
|
||||
.with(DocumentAuth.EXPLICIT_NONE, () => ({
|
||||
type: DocumentAuth.EXPLICIT_NONE,
|
||||
}))
|
||||
.with(DocumentAuth.PASSKEY, DocumentAuth.TWO_FACTOR_AUTH, null, () => null)
|
||||
.exhaustive();
|
||||
}
|
||||
|
||||
return null;
|
||||
}, [derivedRecipientActionAuth, user, recipient]);
|
||||
|
||||
const executeActionAuthProcedure = async (options: ExecuteActionAuthProcedureOptions) => {
|
||||
// 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.
|
||||
const isAuthRedirectRequired = Boolean(
|
||||
derivedRecipientActionAuth &&
|
||||
derivedRecipientActionAuth !== DocumentAuth.EXPLICIT_NONE &&
|
||||
derivedRecipientActionAuth.length > 0 &&
|
||||
!derivedRecipientActionAuth.includes(DocumentAuth.EXPLICIT_NONE) &&
|
||||
user?.email !== recipient.email,
|
||||
);
|
||||
|
||||
@ -208,7 +215,7 @@ export const DocumentSigningAuthProvider = ({
|
||||
onOpenChange={() => setDocumentAuthDialogPayload(null)}
|
||||
onReauthFormSubmit={documentAuthDialogPayload.onReauthFormSubmit}
|
||||
actionTarget={documentAuthDialogPayload.actionTarget}
|
||||
documentAuthType={derivedRecipientActionAuth}
|
||||
availableAuthTypes={derivedRecipientActionAuth}
|
||||
/>
|
||||
)}
|
||||
</DocumentSigningAuthContext.Provider>
|
||||
@ -217,7 +224,7 @@ export const DocumentSigningAuthProvider = ({
|
||||
|
||||
type ExecuteActionAuthProcedureOptions = Omit<
|
||||
DocumentSigningAuthDialogProps,
|
||||
'open' | 'onOpenChange' | 'documentAuthType' | 'recipientRole'
|
||||
'open' | 'onOpenChange' | 'documentAuthType' | 'recipientRole' | 'availableAuthTypes'
|
||||
>;
|
||||
|
||||
DocumentSigningAuthProvider.displayName = 'DocumentSigningAuthProvider';
|
||||
|
||||
@ -44,6 +44,7 @@ const AUTO_SIGNABLE_FIELD_TYPES: string[] = [
|
||||
// other field types.
|
||||
const NON_AUTO_SIGNABLE_ACTION_AUTH_TYPES: string[] = [
|
||||
DocumentAuth.PASSKEY,
|
||||
DocumentAuth.PASSWORD,
|
||||
DocumentAuth.TWO_FACTOR_AUTH,
|
||||
];
|
||||
|
||||
@ -96,8 +97,8 @@ export const DocumentSigningAutoSign = ({ recipient, fields }: DocumentSigningAu
|
||||
return true;
|
||||
});
|
||||
|
||||
const actionAuthAllowsAutoSign = !NON_AUTO_SIGNABLE_ACTION_AUTH_TYPES.includes(
|
||||
derivedRecipientActionAuth ?? '',
|
||||
const actionAuthAllowsAutoSign = derivedRecipientActionAuth.every(
|
||||
(actionAuth) => !NON_AUTO_SIGNABLE_ACTION_AUTH_TYPES.includes(actionAuth),
|
||||
);
|
||||
|
||||
const onSubmit = async () => {
|
||||
@ -110,16 +111,16 @@ export const DocumentSigningAutoSign = ({ recipient, fields }: DocumentSigningAu
|
||||
.with(FieldType.DATE, () => new Date().toISOString())
|
||||
.otherwise(() => '');
|
||||
|
||||
const authOptions = match(derivedRecipientActionAuth)
|
||||
const authOptions = match(derivedRecipientActionAuth.at(0))
|
||||
.with(DocumentAuth.ACCOUNT, () => ({
|
||||
type: DocumentAuth.ACCOUNT,
|
||||
}))
|
||||
.with(DocumentAuth.EXPLICIT_NONE, () => ({
|
||||
type: DocumentAuth.EXPLICIT_NONE,
|
||||
}))
|
||||
.with(null, () => undefined)
|
||||
.with(undefined, () => undefined)
|
||||
.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.
|
||||
() => 'NOT_SUPPORTED' as const,
|
||||
)
|
||||
|
||||
@ -92,8 +92,6 @@ export const DocumentSigningCompleteDialog = ({
|
||||
};
|
||||
|
||||
const onFormSubmit = async (data: TNextSignerFormSchema) => {
|
||||
console.log('data', data);
|
||||
console.log('form.formState.errors', form.formState.errors);
|
||||
try {
|
||||
if (allowDictateNextSigner && data.name && data.email) {
|
||||
await onSignatureComplete({ name: data.name, email: data.email });
|
||||
|
||||
@ -183,8 +183,8 @@ export const DocumentEditForm = ({
|
||||
title: data.title,
|
||||
externalId: data.externalId || null,
|
||||
visibility: data.visibility,
|
||||
globalAccessAuth: data.globalAccessAuth ?? null,
|
||||
globalActionAuth: data.globalActionAuth ?? null,
|
||||
globalAccessAuth: data.globalAccessAuth ?? [],
|
||||
globalActionAuth: data.globalActionAuth ?? [],
|
||||
},
|
||||
meta: {
|
||||
timezone,
|
||||
@ -229,7 +229,7 @@ export const DocumentEditForm = ({
|
||||
recipients: data.signers.map((signer) => ({
|
||||
...signer,
|
||||
// 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
|
||||
* @returns The formatted text
|
||||
*/
|
||||
const formatGenericText = (text?: string | null) => {
|
||||
const formatGenericText = (text?: string | string[] | null): string => {
|
||||
if (!text) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (Array.isArray(text)) {
|
||||
return text.map((t) => formatGenericText(t)).join(', ');
|
||||
}
|
||||
|
||||
return (text.charAt(0).toUpperCase() + text.slice(1).toLowerCase()).replaceAll('_', ' ');
|
||||
};
|
||||
|
||||
@ -245,11 +249,19 @@ export const DocumentHistorySheet = ({
|
||||
values={[
|
||||
{
|
||||
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',
|
||||
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,
|
||||
externalId: data.externalId || null,
|
||||
visibility: data.visibility,
|
||||
globalAccessAuth: data.globalAccessAuth ?? null,
|
||||
globalActionAuth: data.globalActionAuth ?? null,
|
||||
globalAccessAuth: data.globalAccessAuth ?? [],
|
||||
globalActionAuth: data.globalActionAuth ?? [],
|
||||
},
|
||||
meta: {
|
||||
...data.meta,
|
||||
|
||||
@ -3,6 +3,7 @@ import { useLingui } from '@lingui/react';
|
||||
import { FieldType, SigningStatus } from '@prisma/client';
|
||||
import { DateTime } from 'luxon';
|
||||
import { redirect } from 'react-router';
|
||||
import { prop, sortBy } from 'remeda';
|
||||
import { match } from 'ts-pattern';
|
||||
import { UAParser } from 'ua-parser-js';
|
||||
import { renderSVG } from 'uqr';
|
||||
@ -133,18 +134,30 @@ export default function SigningCertificate({ loaderData }: Route.ComponentProps)
|
||||
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('TWO_FACTOR_AUTH', () => _(msg`Two-Factor Re-Authentication`))
|
||||
.with('PASSWORD', () => _(msg`Password Re-Authentication`))
|
||||
.with('PASSKEY', () => _(msg`Passkey Re-Authentication`))
|
||||
.with('EXPLICIT_NONE', () => _(msg`Email`))
|
||||
.with(null, () => null)
|
||||
.with(undefined, () => null)
|
||||
.exhaustive();
|
||||
|
||||
if (!authLevel) {
|
||||
authLevel = match(extractedAuthMethods.derivedRecipientAccessAuth)
|
||||
const accessAuthMethod = extractedAuthMethods.derivedRecipientAccessAuth.at(0);
|
||||
|
||||
authLevel = match(accessAuthMethod)
|
||||
.with('ACCOUNT', () => _(msg`Account Authentication`))
|
||||
.with(null, () => _(msg`Email`))
|
||||
.with(undefined, () => _(msg`Email`))
|
||||
.exhaustive();
|
||||
}
|
||||
|
||||
|
||||
@ -47,9 +47,9 @@ export async function loader({ params, request }: Route.LoaderArgs) {
|
||||
});
|
||||
|
||||
// Ensure typesafety when we add more options.
|
||||
const isAccessAuthValid = match(derivedRecipientAccessAuth)
|
||||
const isAccessAuthValid = match(derivedRecipientAccessAuth.at(0))
|
||||
.with(DocumentAccessAuth.ACCOUNT, () => Boolean(session.user))
|
||||
.with(null, () => true)
|
||||
.with(undefined, () => true)
|
||||
.exhaustive();
|
||||
|
||||
if (!isAccessAuthValid) {
|
||||
|
||||
@ -68,9 +68,9 @@ export async function loader({ params, request }: Route.LoaderArgs) {
|
||||
}),
|
||||
]);
|
||||
|
||||
const isAccessAuthValid = match(derivedRecipientAccessAuth)
|
||||
.with(DocumentAccessAuth.ACCOUNT, () => user !== null)
|
||||
.with(null, () => true)
|
||||
const isAccessAuthValid = match(derivedRecipientAccessAuth.at(0))
|
||||
.with(DocumentAccessAuth.ACCOUNT, () => !!user)
|
||||
.with(undefined, () => true)
|
||||
.exhaustive();
|
||||
|
||||
if (!isAccessAuthValid) {
|
||||
|
||||
@ -81,9 +81,9 @@ export async function loader({ params, request }: Route.LoaderArgs) {
|
||||
documentAuth: document.authOptions,
|
||||
});
|
||||
|
||||
const isAccessAuthValid = match(derivedRecipientAccessAuth)
|
||||
.with(DocumentAccessAuth.ACCOUNT, () => user !== null)
|
||||
.with(null, () => true)
|
||||
const isAccessAuthValid = match(derivedRecipientAccessAuth.at(0))
|
||||
.with(DocumentAccessAuth.ACCOUNT, () => user && user.email === recipient.email)
|
||||
.with(undefined, () => true)
|
||||
.exhaustive();
|
||||
|
||||
if (!isAccessAuthValid) {
|
||||
|
||||
@ -38,8 +38,6 @@ export async function loader({ request }: Route.LoaderArgs) {
|
||||
|
||||
const recipient = await getRecipientByToken({ token });
|
||||
|
||||
console.log('document', document.id);
|
||||
|
||||
return { document, recipient };
|
||||
}),
|
||||
);
|
||||
|
||||
@ -821,7 +821,7 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, {
|
||||
name,
|
||||
role,
|
||||
signingOrder,
|
||||
actionAuth: authOptions?.actionAuth ?? null,
|
||||
actionAuth: authOptions?.actionAuth ?? [],
|
||||
},
|
||||
],
|
||||
requestMetadata: metadata,
|
||||
@ -888,7 +888,7 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, {
|
||||
name,
|
||||
role,
|
||||
signingOrder,
|
||||
actionAuth: authOptions?.actionAuth,
|
||||
actionAuth: authOptions?.actionAuth ?? [],
|
||||
requestMetadata: metadata.requestMetadata,
|
||||
}).catch(() => null);
|
||||
|
||||
|
||||
@ -177,8 +177,8 @@ export const ZCreateDocumentMutationSchema = z.object({
|
||||
.default({}),
|
||||
authOptions: z
|
||||
.object({
|
||||
globalAccessAuth: ZDocumentAccessAuthTypesSchema.optional(),
|
||||
globalActionAuth: ZDocumentActionAuthTypesSchema.optional(),
|
||||
globalAccessAuth: z.array(ZDocumentAccessAuthTypesSchema).optional().default([]),
|
||||
globalActionAuth: z.array(ZDocumentActionAuthTypesSchema).optional().default([]),
|
||||
})
|
||||
.optional()
|
||||
.openapi({
|
||||
@ -237,8 +237,8 @@ export const ZCreateDocumentFromTemplateMutationSchema = z.object({
|
||||
.optional(),
|
||||
authOptions: z
|
||||
.object({
|
||||
globalAccessAuth: ZDocumentAccessAuthTypesSchema.optional(),
|
||||
globalActionAuth: ZDocumentActionAuthTypesSchema.optional(),
|
||||
globalAccessAuth: z.array(ZDocumentAccessAuthTypesSchema).optional().default([]),
|
||||
globalActionAuth: z.array(ZDocumentActionAuthTypesSchema).optional().default([]),
|
||||
})
|
||||
.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(),
|
||||
authOptions: z
|
||||
.object({
|
||||
globalAccessAuth: ZDocumentAccessAuthTypesSchema.optional(),
|
||||
globalActionAuth: ZDocumentActionAuthTypesSchema.optional(),
|
||||
globalAccessAuth: z.array(ZDocumentAccessAuthTypesSchema).optional().default([]),
|
||||
globalActionAuth: z.array(ZDocumentActionAuthTypesSchema).optional().default([]),
|
||||
})
|
||||
.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(),
|
||||
authOptions: z
|
||||
.object({
|
||||
actionAuth: ZRecipientActionAuthTypesSchema.optional(),
|
||||
actionAuth: z.array(ZRecipientActionAuthTypesSchema).optional().default([]),
|
||||
})
|
||||
.optional()
|
||||
.openapi({
|
||||
|
||||
@ -42,8 +42,8 @@ test('[DOCUMENT_AUTH]: should allow or deny access when required', async ({ page
|
||||
{
|
||||
createDocumentOptions: {
|
||||
authOptions: createDocumentAuthOptions({
|
||||
globalAccessAuth: 'ACCOUNT',
|
||||
globalActionAuth: null,
|
||||
globalAccessAuth: ['ACCOUNT'],
|
||||
globalActionAuth: [],
|
||||
}),
|
||||
},
|
||||
},
|
||||
|
||||
@ -65,8 +65,8 @@ test('[DOCUMENT_AUTH]: should allow signing with valid global auth', async ({ pa
|
||||
recipients: [recipientWithAccount],
|
||||
updateDocumentOptions: {
|
||||
authOptions: createDocumentAuthOptions({
|
||||
globalAccessAuth: null,
|
||||
globalActionAuth: 'ACCOUNT',
|
||||
globalAccessAuth: [],
|
||||
globalActionAuth: ['ACCOUNT'],
|
||||
}),
|
||||
},
|
||||
});
|
||||
@ -116,8 +116,8 @@ test.skip('[DOCUMENT_AUTH]: should deny signing document when required for globa
|
||||
recipients: [recipientWithAccount],
|
||||
updateDocumentOptions: {
|
||||
authOptions: createDocumentAuthOptions({
|
||||
globalAccessAuth: null,
|
||||
globalActionAuth: 'ACCOUNT',
|
||||
globalAccessAuth: [],
|
||||
globalActionAuth: ['ACCOUNT'],
|
||||
}),
|
||||
},
|
||||
});
|
||||
@ -147,8 +147,8 @@ test('[DOCUMENT_AUTH]: should deny signing fields when required for global auth'
|
||||
recipients: [recipientWithAccount, seedTestEmail()],
|
||||
updateDocumentOptions: {
|
||||
authOptions: createDocumentAuthOptions({
|
||||
globalAccessAuth: null,
|
||||
globalActionAuth: 'ACCOUNT',
|
||||
globalAccessAuth: [],
|
||||
globalActionAuth: ['ACCOUNT'],
|
||||
}),
|
||||
},
|
||||
});
|
||||
@ -193,20 +193,20 @@ test('[DOCUMENT_AUTH]: should allow field signing when required for recipient au
|
||||
recipientsCreateOptions: [
|
||||
{
|
||||
authOptions: createRecipientAuthOptions({
|
||||
accessAuth: null,
|
||||
actionAuth: null,
|
||||
accessAuth: [],
|
||||
actionAuth: [],
|
||||
}),
|
||||
},
|
||||
{
|
||||
authOptions: createRecipientAuthOptions({
|
||||
accessAuth: null,
|
||||
actionAuth: 'EXPLICIT_NONE',
|
||||
accessAuth: [],
|
||||
actionAuth: ['EXPLICIT_NONE'],
|
||||
}),
|
||||
},
|
||||
{
|
||||
authOptions: createRecipientAuthOptions({
|
||||
accessAuth: null,
|
||||
actionAuth: 'ACCOUNT',
|
||||
accessAuth: [],
|
||||
actionAuth: ['ACCOUNT'],
|
||||
}),
|
||||
},
|
||||
],
|
||||
@ -218,7 +218,7 @@ test('[DOCUMENT_AUTH]: should allow field signing when required for recipient au
|
||||
const { actionAuth } = ZRecipientAuthOptionsSchema.parse(recipient.authOptions);
|
||||
|
||||
// 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}`;
|
||||
|
||||
@ -292,28 +292,28 @@ test('[DOCUMENT_AUTH]: should allow field signing when required for recipient an
|
||||
recipientsCreateOptions: [
|
||||
{
|
||||
authOptions: createRecipientAuthOptions({
|
||||
accessAuth: null,
|
||||
actionAuth: null,
|
||||
accessAuth: [],
|
||||
actionAuth: [],
|
||||
}),
|
||||
},
|
||||
{
|
||||
authOptions: createRecipientAuthOptions({
|
||||
accessAuth: null,
|
||||
actionAuth: 'EXPLICIT_NONE',
|
||||
accessAuth: [],
|
||||
actionAuth: ['EXPLICIT_NONE'],
|
||||
}),
|
||||
},
|
||||
{
|
||||
authOptions: createRecipientAuthOptions({
|
||||
accessAuth: null,
|
||||
actionAuth: 'ACCOUNT',
|
||||
accessAuth: [],
|
||||
actionAuth: ['ACCOUNT'],
|
||||
}),
|
||||
},
|
||||
],
|
||||
fields: [FieldType.DATE, FieldType.SIGNATURE],
|
||||
updateDocumentOptions: {
|
||||
authOptions: createDocumentAuthOptions({
|
||||
globalAccessAuth: null,
|
||||
globalActionAuth: 'ACCOUNT',
|
||||
globalAccessAuth: [],
|
||||
globalActionAuth: ['ACCOUNT'],
|
||||
}),
|
||||
},
|
||||
});
|
||||
@ -323,7 +323,7 @@ test('[DOCUMENT_AUTH]: should allow field signing when required for recipient an
|
||||
const { actionAuth } = ZRecipientAuthOptionsSchema.parse(recipient.authOptions);
|
||||
|
||||
// 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}`;
|
||||
|
||||
|
||||
@ -40,7 +40,7 @@ test.describe('[EE_ONLY]', () => {
|
||||
|
||||
// Set EE action auth.
|
||||
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');
|
||||
|
||||
// Save the settings by going to the next step.
|
||||
@ -82,7 +82,7 @@ test.describe('[EE_ONLY]', () => {
|
||||
|
||||
// Set EE action auth.
|
||||
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');
|
||||
|
||||
// Save the settings by going to the next step.
|
||||
@ -143,7 +143,7 @@ test('[DOCUMENT_FLOW]: add settings', async ({ page }) => {
|
||||
|
||||
// Set access auth.
|
||||
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');
|
||||
|
||||
// Action auth should NOT be visible.
|
||||
|
||||
@ -37,7 +37,7 @@ test.describe('[EE_ONLY]', () => {
|
||||
|
||||
// Set EE action auth.
|
||||
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');
|
||||
|
||||
// Save the settings by going to the next step.
|
||||
@ -79,7 +79,7 @@ test.describe('[EE_ONLY]', () => {
|
||||
|
||||
// Set EE action auth.
|
||||
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');
|
||||
|
||||
// Save the settings by going to the next step.
|
||||
@ -140,7 +140,7 @@ test('[TEMPLATE_FLOW]: add settings', async ({ page }) => {
|
||||
|
||||
// Set access auth.
|
||||
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');
|
||||
|
||||
// Action auth should NOT be visible.
|
||||
|
||||
@ -58,8 +58,8 @@ test.describe('[EE_ONLY]', () => {
|
||||
|
||||
// Add advanced settings for a single recipient.
|
||||
await page.getByLabel('Show advanced settings').check();
|
||||
await page.getByRole('combobox').first().click();
|
||||
await page.getByLabel('Require passkey').click();
|
||||
await page.getByTestId('documentActionSelectValue').click();
|
||||
await page.getByRole('option').filter({ hasText: 'Require passkey' }).click();
|
||||
|
||||
// Navigate to the next step and back.
|
||||
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.
|
||||
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');
|
||||
|
||||
// Set EE action auth.
|
||||
if (isBillingEnabled) {
|
||||
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');
|
||||
}
|
||||
|
||||
@ -85,8 +85,8 @@ test('[TEMPLATE]: should create a document from a template', async ({ page }) =>
|
||||
// Apply require passkey for Recipient 1.
|
||||
if (isBillingEnabled) {
|
||||
await page.getByLabel('Show advanced settings').check();
|
||||
await page.getByRole('combobox').first().click();
|
||||
await page.getByLabel('Require passkey').click();
|
||||
await page.getByTestId('documentActionSelectValue').click();
|
||||
await page.getByRole('option').filter({ hasText: 'Require passkey' }).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(documentAuth.documentAuthOption.globalAccessAuth).toEqual('ACCOUNT');
|
||||
expect(documentAuth.documentAuthOption.globalActionAuth).toEqual(
|
||||
isBillingEnabled ? 'PASSKEY' : null,
|
||||
);
|
||||
expect(documentAuth.documentAuthOption.globalAccessAuth).toContain('ACCOUNT');
|
||||
|
||||
if (isBillingEnabled) {
|
||||
expect(documentAuth.documentAuthOption.globalActionAuth).toContain('PASSKEY');
|
||||
}
|
||||
|
||||
expect(document.documentMeta?.dateFormat).toEqual('dd/MM/yyyy hh:mm a');
|
||||
expect(document.documentMeta?.message).toEqual('MESSAGE');
|
||||
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) {
|
||||
expect(recipientOneAuth.derivedRecipientActionAuth).toEqual('PASSKEY');
|
||||
expect(recipientOneAuth.derivedRecipientActionAuth).toContain('PASSKEY');
|
||||
}
|
||||
|
||||
expect(recipientOneAuth.derivedRecipientAccessAuth).toEqual('ACCOUNT');
|
||||
expect(recipientTwoAuth.derivedRecipientAccessAuth).toEqual('ACCOUNT');
|
||||
expect(recipientOneAuth.derivedRecipientAccessAuth).toContain('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.
|
||||
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');
|
||||
|
||||
// Set EE action auth.
|
||||
if (isBillingEnabled) {
|
||||
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');
|
||||
}
|
||||
|
||||
@ -220,8 +222,8 @@ test('[TEMPLATE]: should create a team document from a team template', async ({
|
||||
// Apply require passkey for Recipient 1.
|
||||
if (isBillingEnabled) {
|
||||
await page.getByLabel('Show advanced settings').check();
|
||||
await page.getByRole('combobox').first().click();
|
||||
await page.getByLabel('Require passkey').click();
|
||||
await page.getByTestId('documentActionSelectValue').click();
|
||||
await page.getByRole('option').filter({ hasText: 'Require passkey' }).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(documentAuth.documentAuthOption.globalAccessAuth).toEqual('ACCOUNT');
|
||||
expect(documentAuth.documentAuthOption.globalActionAuth).toEqual(
|
||||
isBillingEnabled ? 'PASSKEY' : null,
|
||||
);
|
||||
expect(documentAuth.documentAuthOption.globalAccessAuth).toContain('ACCOUNT');
|
||||
|
||||
if (isBillingEnabled) {
|
||||
expect(documentAuth.documentAuthOption.globalActionAuth).toContain('PASSKEY');
|
||||
}
|
||||
|
||||
expect(document.documentMeta?.dateFormat).toEqual('dd/MM/yyyy hh:mm a');
|
||||
expect(document.documentMeta?.message).toEqual('MESSAGE');
|
||||
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) {
|
||||
expect(recipientOneAuth.derivedRecipientActionAuth).toEqual('PASSKEY');
|
||||
expect(recipientOneAuth.derivedRecipientActionAuth).toContain('PASSKEY');
|
||||
}
|
||||
|
||||
expect(recipientOneAuth.derivedRecipientAccessAuth).toEqual('ACCOUNT');
|
||||
expect(recipientTwoAuth.derivedRecipientAccessAuth).toEqual('ACCOUNT');
|
||||
expect(recipientOneAuth.derivedRecipientAccessAuth).toContain('ACCOUNT');
|
||||
expect(recipientTwoAuth.derivedRecipientAccessAuth).toContain('ACCOUNT');
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
@ -172,8 +172,8 @@ test('[DIRECT_TEMPLATES]: direct template link auth access', async ({ page }) =>
|
||||
userId: user.id,
|
||||
createTemplateOptions: {
|
||||
authOptions: createDocumentAuthOptions({
|
||||
globalAccessAuth: 'ACCOUNT',
|
||||
globalActionAuth: null,
|
||||
globalAccessAuth: ['ACCOUNT'],
|
||||
globalActionAuth: [],
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
@ -19,6 +19,10 @@ export const DOCUMENT_AUTH_TYPES: Record<string, DocumentAuthTypeData> = {
|
||||
key: DocumentAuth.TWO_FACTOR_AUTH,
|
||||
value: 'Require 2FA',
|
||||
},
|
||||
[DocumentAuth.PASSWORD]: {
|
||||
key: DocumentAuth.PASSWORD,
|
||||
value: 'Require password',
|
||||
},
|
||||
[DocumentAuth.EXPLICIT_NONE]: {
|
||||
key: DocumentAuth.EXPLICIT_NONE,
|
||||
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,
|
||||
mapDocumentToWebhookDocumentPayload,
|
||||
} from '../../types/webhook-payload';
|
||||
import { extractDocumentAuthMethods } from '../../utils/document-auth';
|
||||
import { getIsRecipientsTurnToSign } from '../recipient/get-is-recipient-turn';
|
||||
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
|
||||
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({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_COMPLETED,
|
||||
@ -154,6 +160,7 @@ export const completeDocumentWithToken = async ({
|
||||
recipientName: recipient.name,
|
||||
recipientId: recipient.id,
|
||||
recipientRole: recipient.role,
|
||||
actionAuth: authOptions.derivedRecipientActionAuth,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
@ -39,8 +39,8 @@ export type CreateDocumentOptions = {
|
||||
title: string;
|
||||
externalId?: string;
|
||||
visibility?: DocumentVisibility;
|
||||
globalAccessAuth?: TDocumentAccessAuthTypes;
|
||||
globalActionAuth?: TDocumentActionAuthTypes;
|
||||
globalAccessAuth?: TDocumentAccessAuthTypes[];
|
||||
globalActionAuth?: TDocumentActionAuthTypes[];
|
||||
formValues?: TDocumentFormValues;
|
||||
recipients: TCreateDocumentV2Request['recipients'];
|
||||
};
|
||||
@ -113,14 +113,16 @@ export const createDocumentV2 = async ({
|
||||
}
|
||||
|
||||
const authOptions = createDocumentAuthOptions({
|
||||
globalAccessAuth: data?.globalAccessAuth || null,
|
||||
globalActionAuth: data?.globalActionAuth || null,
|
||||
globalAccessAuth: data?.globalAccessAuth || [],
|
||||
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.
|
||||
if (authOptions.globalActionAuth || recipientsHaveActionAuth) {
|
||||
if (authOptions.globalActionAuth.length > 0 || recipientsHaveActionAuth) {
|
||||
const isDocumentEnterprise = await isUserEnterprise({
|
||||
userId,
|
||||
teamId,
|
||||
@ -171,8 +173,8 @@ export const createDocumentV2 = async ({
|
||||
await Promise.all(
|
||||
(data.recipients || []).map(async (recipient) => {
|
||||
const recipientAuthOptions = createRecipientAuthOptions({
|
||||
accessAuth: recipient.accessAuth || null,
|
||||
actionAuth: recipient.actionAuth || null,
|
||||
accessAuth: recipient.accessAuth ?? [],
|
||||
actionAuth: recipient.actionAuth ?? [],
|
||||
});
|
||||
|
||||
await tx.recipient.create({
|
||||
|
||||
@ -17,6 +17,7 @@ export const getDocumentCertificateAuditLogs = async ({
|
||||
in: [
|
||||
DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_COMPLETED,
|
||||
DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_REJECTED,
|
||||
DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FIELD_INSERTED,
|
||||
DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_OPENED,
|
||||
DOCUMENT_AUDIT_LOG_TYPE.EMAIL_SENT,
|
||||
],
|
||||
@ -36,6 +37,9 @@ export const getDocumentCertificateAuditLogs = async ({
|
||||
[DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_OPENED]: auditLogs.filter(
|
||||
(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(
|
||||
(log) =>
|
||||
log.type === DOCUMENT_AUDIT_LOG_TYPE.EMAIL_SENT &&
|
||||
|
||||
@ -5,6 +5,7 @@ import { match } from 'ts-pattern';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { verifyTwoFactorAuthenticationToken } from '../2fa/verify-2fa-token';
|
||||
import { verifyPassword } from '../2fa/verify-password';
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import type { TDocumentAuth, TDocumentAuthMethods } from '../../types/document-auth';
|
||||
import { DocumentAuth } from '../../types/document-auth';
|
||||
@ -60,23 +61,26 @@ export const isRecipientAuthorized = async ({
|
||||
recipientAuth: recipient.authOptions,
|
||||
});
|
||||
|
||||
const authMethod: TDocumentAuth | null =
|
||||
const authMethods: TDocumentAuth[] =
|
||||
type === 'ACCESS' ? derivedRecipientAccessAuth : derivedRecipientActionAuth;
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
// Create auth options when none are passed for account.
|
||||
if (!authOptions && authMethod === DocumentAuth.ACCOUNT) {
|
||||
if (!authOptions && authMethods.some((method) => method === DocumentAuth.ACCOUNT)) {
|
||||
authOptions = {
|
||||
type: DocumentAuth.ACCOUNT,
|
||||
};
|
||||
}
|
||||
|
||||
// Authentication required does not match provided method.
|
||||
if (!authOptions || authOptions.type !== authMethod || !userId) {
|
||||
if (!authOptions || !authMethods.includes(authOptions.type) || !userId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -117,6 +121,15 @@ export const isRecipientAuthorized = async ({
|
||||
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();
|
||||
};
|
||||
|
||||
@ -160,7 +173,7 @@ const verifyPasskey = async ({
|
||||
}: VerifyPasskeyOptions): Promise<void> => {
|
||||
const passkey = await prisma.passkey.findFirst({
|
||||
where: {
|
||||
credentialId: Buffer.from(authenticationResponse.id, 'base64'),
|
||||
credentialId: new Uint8Array(Buffer.from(authenticationResponse.id, 'base64')),
|
||||
userId,
|
||||
},
|
||||
});
|
||||
|
||||
@ -21,8 +21,8 @@ export type UpdateDocumentOptions = {
|
||||
title?: string;
|
||||
externalId?: string | null;
|
||||
visibility?: DocumentVisibility | null;
|
||||
globalAccessAuth?: TDocumentAccessAuthTypes | null;
|
||||
globalActionAuth?: TDocumentActionAuthTypes | null;
|
||||
globalAccessAuth?: TDocumentAccessAuthTypes[];
|
||||
globalActionAuth?: TDocumentActionAuthTypes[];
|
||||
useLegacyFieldInsertion?: boolean;
|
||||
};
|
||||
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 (!data || Object.values(data).length === 0) {
|
||||
console.log('no data');
|
||||
return document;
|
||||
}
|
||||
|
||||
@ -137,7 +136,7 @@ export const updateDocument = async ({
|
||||
data?.globalActionAuth === undefined ? documentGlobalActionAuth : data.globalActionAuth;
|
||||
|
||||
// Check if user has permission to set the global action auth.
|
||||
if (newGlobalActionAuth) {
|
||||
if (newGlobalActionAuth && newGlobalActionAuth.length > 0) {
|
||||
const isDocumentEnterprise = await isUserEnterprise({
|
||||
userId,
|
||||
teamId,
|
||||
|
||||
@ -3,7 +3,6 @@ import { FieldType } from '@prisma/client';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import type { TRecipientActionAuth } from '../../types/document-auth';
|
||||
import { extractDocumentAuthMethods } from '../../utils/document-auth';
|
||||
import { isRecipientAuthorized } from './is-recipient-authorized';
|
||||
|
||||
export type ValidateFieldAuthOptions = {
|
||||
@ -26,14 +25,9 @@ export const validateFieldAuth = async ({
|
||||
userId,
|
||||
authOptions,
|
||||
}: ValidateFieldAuthOptions) => {
|
||||
const { derivedRecipientActionAuth } = extractDocumentAuthMethods({
|
||||
documentAuth: documentAuthOptions,
|
||||
recipientAuth: recipient.authOptions,
|
||||
});
|
||||
|
||||
// Override all non-signature fields to not require any auth.
|
||||
if (field.type !== FieldType.SIGNATURE) {
|
||||
return null;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
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 = {
|
||||
token: string;
|
||||
recipientAccessAuth?: TDocumentAccessAuthTypes | null;
|
||||
recipientAccessAuth?: TDocumentAccessAuthTypes[];
|
||||
requestMetadata?: RequestMetadata;
|
||||
};
|
||||
|
||||
@ -63,7 +63,7 @@ export const viewedDocument = async ({
|
||||
recipientId: recipient.id,
|
||||
recipientName: recipient.name,
|
||||
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 production, enforce a minimum expiration time
|
||||
const isDevelopment = env('NODE_ENV') !== 'production';
|
||||
console.log('isDevelopment', isDevelopment);
|
||||
const minExpirationMinutes = isDevelopment ? 0 : 5;
|
||||
|
||||
// Ensure expiresIn is at least the minimum allowed value
|
||||
|
||||
@ -22,8 +22,8 @@ export interface CreateDocumentRecipientsOptions {
|
||||
name: string;
|
||||
role: RecipientRole;
|
||||
signingOrder?: number | null;
|
||||
accessAuth?: TRecipientAccessAuthTypes | null;
|
||||
actionAuth?: TRecipientActionAuthTypes | null;
|
||||
accessAuth?: TRecipientAccessAuthTypes[];
|
||||
actionAuth?: TRecipientActionAuthTypes[];
|
||||
}[];
|
||||
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.
|
||||
if (recipientsHaveActionAuth) {
|
||||
@ -110,8 +112,8 @@ export const createDocumentRecipients = async ({
|
||||
return await Promise.all(
|
||||
normalizedRecipients.map(async (recipient) => {
|
||||
const authOptions = createRecipientAuthOptions({
|
||||
accessAuth: recipient.accessAuth || null,
|
||||
actionAuth: recipient.actionAuth || null,
|
||||
accessAuth: recipient.accessAuth ?? [],
|
||||
actionAuth: recipient.actionAuth ?? [],
|
||||
});
|
||||
|
||||
const createdRecipient = await tx.recipient.create({
|
||||
@ -140,8 +142,8 @@ export const createDocumentRecipients = async ({
|
||||
recipientName: createdRecipient.name,
|
||||
recipientId: createdRecipient.id,
|
||||
recipientRole: createdRecipient.role,
|
||||
accessAuth: recipient.accessAuth || undefined,
|
||||
actionAuth: recipient.actionAuth || undefined,
|
||||
accessAuth: recipient.accessAuth ?? [],
|
||||
actionAuth: recipient.actionAuth ?? [],
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
@ -19,8 +19,8 @@ export interface CreateTemplateRecipientsOptions {
|
||||
name: string;
|
||||
role: RecipientRole;
|
||||
signingOrder?: number | null;
|
||||
accessAuth?: TRecipientAccessAuthTypes | null;
|
||||
actionAuth?: TRecipientActionAuthTypes | null;
|
||||
accessAuth?: TRecipientAccessAuthTypes[];
|
||||
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.
|
||||
if (recipientsHaveActionAuth) {
|
||||
@ -99,8 +101,8 @@ export const createTemplateRecipients = async ({
|
||||
return await Promise.all(
|
||||
normalizedRecipients.map(async (recipient) => {
|
||||
const authOptions = createRecipientAuthOptions({
|
||||
accessAuth: recipient.accessAuth || null,
|
||||
actionAuth: recipient.actionAuth || null,
|
||||
accessAuth: recipient.accessAuth ?? [],
|
||||
actionAuth: recipient.actionAuth ?? [],
|
||||
});
|
||||
|
||||
const createdRecipient = await tx.recipient.create({
|
||||
|
||||
@ -4,6 +4,7 @@ import { msg } from '@lingui/core/macro';
|
||||
import type { Recipient } from '@prisma/client';
|
||||
import { RecipientRole } 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 { mailer } from '@documenso/email/mailer';
|
||||
@ -96,7 +97,9 @@ export const setDocumentRecipients = async ({
|
||||
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.
|
||||
if (recipientsHaveActionAuth) {
|
||||
@ -245,8 +248,8 @@ export const setDocumentRecipients = async ({
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
...baseAuditLog,
|
||||
accessAuth: recipient.accessAuth || undefined,
|
||||
actionAuth: recipient.actionAuth || undefined,
|
||||
accessAuth: recipient.accessAuth || [],
|
||||
actionAuth: recipient.actionAuth || [],
|
||||
},
|
||||
}),
|
||||
});
|
||||
@ -361,8 +364,8 @@ type RecipientData = {
|
||||
name: string;
|
||||
role: RecipientRole;
|
||||
signingOrder?: number | null;
|
||||
accessAuth?: TRecipientAccessAuthTypes | null;
|
||||
actionAuth?: TRecipientActionAuthTypes | null;
|
||||
accessAuth?: TRecipientAccessAuthTypes[];
|
||||
actionAuth?: TRecipientActionAuthTypes[];
|
||||
};
|
||||
|
||||
type RecipientDataWithClientId = Recipient & {
|
||||
@ -372,15 +375,15 @@ type RecipientDataWithClientId = Recipient & {
|
||||
const hasRecipientBeenChanged = (recipient: Recipient, newRecipientData: RecipientData) => {
|
||||
const authOptions = ZRecipientAuthOptionsSchema.parse(recipient.authOptions);
|
||||
|
||||
const newRecipientAccessAuth = newRecipientData.accessAuth || null;
|
||||
const newRecipientActionAuth = newRecipientData.actionAuth || null;
|
||||
const newRecipientAccessAuth = newRecipientData.accessAuth || [];
|
||||
const newRecipientActionAuth = newRecipientData.actionAuth || [];
|
||||
|
||||
return (
|
||||
recipient.email !== newRecipientData.email ||
|
||||
recipient.name !== newRecipientData.name ||
|
||||
recipient.role !== newRecipientData.role ||
|
||||
recipient.signingOrder !== newRecipientData.signingOrder ||
|
||||
authOptions.accessAuth !== newRecipientAccessAuth ||
|
||||
authOptions.actionAuth !== newRecipientActionAuth
|
||||
!isDeepEqual(authOptions.accessAuth, newRecipientAccessAuth) ||
|
||||
!isDeepEqual(authOptions.actionAuth, newRecipientActionAuth)
|
||||
);
|
||||
};
|
||||
|
||||
@ -26,7 +26,7 @@ export type SetTemplateRecipientsOptions = {
|
||||
name: string;
|
||||
role: RecipientRole;
|
||||
signingOrder?: number | null;
|
||||
actionAuth?: TRecipientActionAuthTypes | null;
|
||||
actionAuth?: TRecipientActionAuthTypes[];
|
||||
}[];
|
||||
};
|
||||
|
||||
@ -64,7 +64,9 @@ export const setTemplateRecipients = async ({
|
||||
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.
|
||||
if (recipientsHaveActionAuth) {
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import type { Recipient } from '@prisma/client';
|
||||
import { RecipientRole } 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 { 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.
|
||||
if (recipientsHaveActionAuth) {
|
||||
@ -218,8 +221,8 @@ type RecipientData = {
|
||||
name?: string;
|
||||
role?: RecipientRole;
|
||||
signingOrder?: number | null;
|
||||
accessAuth?: TRecipientAccessAuthTypes | null;
|
||||
actionAuth?: TRecipientActionAuthTypes | null;
|
||||
accessAuth?: TRecipientAccessAuthTypes[];
|
||||
actionAuth?: TRecipientActionAuthTypes[];
|
||||
};
|
||||
|
||||
const hasRecipientBeenChanged = (recipient: Recipient, newRecipientData: RecipientData) => {
|
||||
@ -233,7 +236,7 @@ const hasRecipientBeenChanged = (recipient: Recipient, newRecipientData: Recipie
|
||||
recipient.name !== newRecipientData.name ||
|
||||
recipient.role !== newRecipientData.role ||
|
||||
recipient.signingOrder !== newRecipientData.signingOrder ||
|
||||
authOptions.accessAuth !== newRecipientAccessAuth ||
|
||||
authOptions.actionAuth !== newRecipientActionAuth
|
||||
!isDeepEqual(authOptions.accessAuth, newRecipientAccessAuth) ||
|
||||
!isDeepEqual(authOptions.actionAuth, newRecipientActionAuth)
|
||||
);
|
||||
};
|
||||
|
||||
@ -20,7 +20,7 @@ export type UpdateRecipientOptions = {
|
||||
name?: string;
|
||||
role?: RecipientRole;
|
||||
signingOrder?: number | null;
|
||||
actionAuth?: TRecipientActionAuthTypes | null;
|
||||
actionAuth?: TRecipientActionAuthTypes[];
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
requestMetadata?: RequestMetadata;
|
||||
@ -90,7 +90,7 @@ export const updateRecipient = async ({
|
||||
throw new Error('Recipient not found');
|
||||
}
|
||||
|
||||
if (actionAuth) {
|
||||
if (actionAuth && actionAuth.length > 0) {
|
||||
const isDocumentEnterprise = await isUserEnterprise({
|
||||
userId,
|
||||
teamId,
|
||||
@ -117,7 +117,7 @@ export const updateRecipient = async ({
|
||||
signingOrder,
|
||||
authOptions: createRecipientAuthOptions({
|
||||
accessAuth: recipientAuthOptions.accessAuth,
|
||||
actionAuth: actionAuth ?? null,
|
||||
actionAuth: actionAuth ?? [],
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
@ -22,8 +22,8 @@ export interface UpdateTemplateRecipientsOptions {
|
||||
name?: string;
|
||||
role?: RecipientRole;
|
||||
signingOrder?: number | null;
|
||||
accessAuth?: TRecipientAccessAuthTypes | null;
|
||||
actionAuth?: TRecipientActionAuthTypes | null;
|
||||
accessAuth?: TRecipientAccessAuthTypes[];
|
||||
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.
|
||||
if (recipientsHaveActionAuth) {
|
||||
|
||||
@ -70,7 +70,7 @@ export type CreateDocumentFromDirectTemplateOptions = {
|
||||
|
||||
type CreatedDirectRecipientField = {
|
||||
field: Field & { signature?: Signature | null };
|
||||
derivedRecipientActionAuth: TRecipientActionAuthTypes | null;
|
||||
derivedRecipientActionAuth?: TRecipientActionAuthTypes;
|
||||
};
|
||||
|
||||
export const ZCreateDocumentFromDirectTemplateResponseSchema = z.object({
|
||||
@ -151,9 +151,9 @@ export const createDocumentFromDirectTemplate = async ({
|
||||
const directRecipientName = user?.name || initialDirectRecipientName;
|
||||
|
||||
// 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(null, () => true)
|
||||
.with(undefined, () => true)
|
||||
.exhaustive();
|
||||
|
||||
if (!isAccessAuthValid) {
|
||||
@ -460,7 +460,7 @@ export const createDocumentFromDirectTemplate = async ({
|
||||
const createdDirectRecipientFields: CreatedDirectRecipientField[] = [
|
||||
...createdDirectRecipient.fields.map((field) => ({
|
||||
field,
|
||||
derivedRecipientActionAuth: null,
|
||||
derivedRecipientActionAuth: undefined,
|
||||
})),
|
||||
...createdDirectRecipientSignatureFields,
|
||||
];
|
||||
@ -567,6 +567,7 @@ export const createDocumentFromDirectTemplate = async ({
|
||||
recipientId: createdDirectRecipient.id,
|
||||
recipientName: createdDirectRecipient.name,
|
||||
recipientRole: createdDirectRecipient.role,
|
||||
actionAuth: createdDirectRecipient.authOptions?.actionAuth ?? [],
|
||||
},
|
||||
}),
|
||||
];
|
||||
|
||||
@ -15,8 +15,8 @@ export type UpdateTemplateOptions = {
|
||||
title?: string;
|
||||
externalId?: string | null;
|
||||
visibility?: DocumentVisibility;
|
||||
globalAccessAuth?: TDocumentAccessAuthTypes | null;
|
||||
globalActionAuth?: TDocumentActionAuthTypes | null;
|
||||
globalAccessAuth?: TDocumentAccessAuthTypes[];
|
||||
globalActionAuth?: TDocumentActionAuthTypes[];
|
||||
publicTitle?: string;
|
||||
publicDescription?: string;
|
||||
type?: Template['type'];
|
||||
@ -74,7 +74,7 @@ export const updateTemplate = async ({
|
||||
data?.globalActionAuth === undefined ? documentGlobalActionAuth : data.globalActionAuth;
|
||||
|
||||
// Check if user has permission to set the global action auth.
|
||||
if (newGlobalActionAuth) {
|
||||
if (newGlobalActionAuth && newGlobalActionAuth.length > 0) {
|
||||
const isDocumentEnterprise = await isUserEnterprise({
|
||||
userId,
|
||||
teamId,
|
||||
|
||||
@ -123,8 +123,8 @@ export const ZDocumentAuditLogFieldDiffSchema = z.union([
|
||||
]);
|
||||
|
||||
export const ZGenericFromToSchema = z.object({
|
||||
from: z.string().nullable(),
|
||||
to: z.string().nullable(),
|
||||
from: z.union([z.string(), z.array(z.string())]).nullable(),
|
||||
to: z.union([z.string(), z.array(z.string())]).nullable(),
|
||||
});
|
||||
|
||||
export const ZRecipientDiffActionAuthSchema = ZGenericFromToSchema.extend({
|
||||
@ -296,7 +296,7 @@ export const ZDocumentAuditLogEventDocumentFieldInsertedSchema = z.object({
|
||||
},
|
||||
z
|
||||
.object({
|
||||
type: ZRecipientActionAuthTypesSchema,
|
||||
type: ZRecipientActionAuthTypesSchema.optional(),
|
||||
})
|
||||
.optional(),
|
||||
),
|
||||
@ -384,7 +384,7 @@ export const ZDocumentAuditLogEventDocumentFieldPrefilledSchema = z.object({
|
||||
},
|
||||
z
|
||||
.object({
|
||||
type: ZRecipientActionAuthTypesSchema,
|
||||
type: ZRecipientActionAuthTypesSchema.optional(),
|
||||
})
|
||||
.optional(),
|
||||
),
|
||||
@ -428,7 +428,13 @@ export const ZDocumentAuditLogEventDocumentMetaUpdatedSchema = z.object({
|
||||
export const ZDocumentAuditLogEventDocumentOpenedSchema = z.object({
|
||||
type: z.literal(DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_OPENED),
|
||||
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({
|
||||
type: z.literal(DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_COMPLETED),
|
||||
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({
|
||||
type: z.literal(DOCUMENT_AUDIT_LOG_TYPE.RECIPIENT_CREATED),
|
||||
data: ZBaseRecipientDataSchema.extend({
|
||||
accessAuth: ZRecipientAccessAuthTypesSchema.optional(),
|
||||
actionAuth: ZRecipientActionAuthTypesSchema.optional(),
|
||||
accessAuth: z.preprocess((unknownValue) => {
|
||||
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',
|
||||
'PASSKEY',
|
||||
'TWO_FACTOR_AUTH',
|
||||
'PASSWORD',
|
||||
'EXPLICIT_NONE',
|
||||
]);
|
||||
|
||||
export const DocumentAuth = ZDocumentAuthTypesSchema.Enum;
|
||||
|
||||
const ZDocumentAuthAccountSchema = z.object({
|
||||
@ -27,6 +29,11 @@ const ZDocumentAuthPasskeySchema = z.object({
|
||||
tokenReference: z.string().min(1),
|
||||
});
|
||||
|
||||
const ZDocumentAuthPasswordSchema = z.object({
|
||||
type: z.literal(DocumentAuth.PASSWORD),
|
||||
password: z.string().min(1),
|
||||
});
|
||||
|
||||
const ZDocumentAuth2FASchema = z.object({
|
||||
type: z.literal(DocumentAuth.TWO_FACTOR_AUTH),
|
||||
token: z.string().min(4).max(10),
|
||||
@ -40,6 +47,7 @@ export const ZDocumentAuthMethodsSchema = z.discriminatedUnion('type', [
|
||||
ZDocumentAuthExplicitNoneSchema,
|
||||
ZDocumentAuthPasskeySchema,
|
||||
ZDocumentAuth2FASchema,
|
||||
ZDocumentAuthPasswordSchema,
|
||||
]);
|
||||
|
||||
/**
|
||||
@ -61,9 +69,15 @@ export const ZDocumentActionAuthSchema = z.discriminatedUnion('type', [
|
||||
ZDocumentAuthAccountSchema,
|
||||
ZDocumentAuthPasskeySchema,
|
||||
ZDocumentAuth2FASchema,
|
||||
ZDocumentAuthPasswordSchema,
|
||||
]);
|
||||
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(
|
||||
'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,
|
||||
ZDocumentAuthPasskeySchema,
|
||||
ZDocumentAuth2FASchema,
|
||||
ZDocumentAuthPasswordSchema,
|
||||
ZDocumentAuthExplicitNoneSchema,
|
||||
]);
|
||||
export const ZRecipientActionAuthTypesSchema = z
|
||||
@ -96,6 +111,7 @@ export const ZRecipientActionAuthTypesSchema = z
|
||||
DocumentAuth.ACCOUNT,
|
||||
DocumentAuth.PASSKEY,
|
||||
DocumentAuth.TWO_FACTOR_AUTH,
|
||||
DocumentAuth.PASSWORD,
|
||||
DocumentAuth.EXPLICIT_NONE,
|
||||
])
|
||||
.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(
|
||||
(unknownValue) => {
|
||||
if (unknownValue) {
|
||||
return unknownValue;
|
||||
if (!unknownValue || typeof unknownValue !== 'object') {
|
||||
return {
|
||||
globalAccessAuth: [],
|
||||
globalActionAuth: [],
|
||||
};
|
||||
}
|
||||
|
||||
const globalAccessAuth =
|
||||
'globalAccessAuth' in unknownValue ? processAuthValue(unknownValue.globalAccessAuth) : [];
|
||||
const globalActionAuth =
|
||||
'globalActionAuth' in unknownValue ? processAuthValue(unknownValue.globalActionAuth) : [];
|
||||
|
||||
return {
|
||||
globalAccessAuth: null,
|
||||
globalActionAuth: null,
|
||||
globalAccessAuth,
|
||||
globalActionAuth,
|
||||
};
|
||||
},
|
||||
z.object({
|
||||
globalAccessAuth: ZDocumentAccessAuthTypesSchema.nullable(),
|
||||
globalActionAuth: ZDocumentActionAuthTypesSchema.nullable(),
|
||||
globalAccessAuth: z.array(ZDocumentAccessAuthTypesSchema),
|
||||
globalActionAuth: z.array(ZDocumentActionAuthTypesSchema),
|
||||
}),
|
||||
);
|
||||
|
||||
@ -130,21 +154,46 @@ export const ZDocumentAuthOptionsSchema = z.preprocess(
|
||||
*/
|
||||
export const ZRecipientAuthOptionsSchema = z.preprocess(
|
||||
(unknownValue) => {
|
||||
if (unknownValue) {
|
||||
return unknownValue;
|
||||
if (!unknownValue || typeof unknownValue !== 'object') {
|
||||
return {
|
||||
accessAuth: [],
|
||||
actionAuth: [],
|
||||
};
|
||||
}
|
||||
|
||||
const accessAuth =
|
||||
'accessAuth' in unknownValue ? processAuthValue(unknownValue.accessAuth) : [];
|
||||
const actionAuth =
|
||||
'actionAuth' in unknownValue ? processAuthValue(unknownValue.actionAuth) : [];
|
||||
|
||||
return {
|
||||
accessAuth: null,
|
||||
actionAuth: null,
|
||||
accessAuth,
|
||||
actionAuth,
|
||||
};
|
||||
},
|
||||
z.object({
|
||||
accessAuth: ZRecipientAccessAuthTypesSchema.nullable(),
|
||||
actionAuth: ZRecipientActionAuthTypesSchema.nullable(),
|
||||
accessAuth: z.array(ZRecipientAccessAuthTypesSchema),
|
||||
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 TDocumentAuthMethods = z.infer<typeof ZDocumentAuthMethodsSchema>;
|
||||
export type TDocumentAuthOptions = z.infer<typeof ZDocumentAuthOptionsSchema>;
|
||||
|
||||
@ -2,6 +2,7 @@ import type { I18n } from '@lingui/core';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import type { DocumentAuditLog, DocumentMeta, Field, Recipient } from '@prisma/client';
|
||||
import { RecipientRole } from '@prisma/client';
|
||||
import { isDeepEqual } from 'remeda';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import type {
|
||||
@ -106,7 +107,7 @@ export const diffRecipientChanges = (
|
||||
const newActionAuth =
|
||||
newAuthOptions?.actionAuth === undefined ? oldActionAuth : newAuthOptions.actionAuth;
|
||||
|
||||
if (oldAccessAuth !== newAccessAuth) {
|
||||
if (!isDeepEqual(oldAccessAuth, newAccessAuth)) {
|
||||
diffs.push({
|
||||
type: RECIPIENT_DIFF_TYPE.ACCESS_AUTH,
|
||||
from: oldAccessAuth ?? '',
|
||||
@ -114,7 +115,7 @@ export const diffRecipientChanges = (
|
||||
});
|
||||
}
|
||||
|
||||
if (oldActionAuth !== newActionAuth) {
|
||||
if (!isDeepEqual(oldActionAuth, newActionAuth)) {
|
||||
diffs.push({
|
||||
type: RECIPIENT_DIFF_TYPE.ACTION_AUTH,
|
||||
from: oldActionAuth ?? '',
|
||||
|
||||
@ -27,17 +27,21 @@ export const extractDocumentAuthMethods = ({
|
||||
const documentAuthOption = ZDocumentAuthOptionsSchema.parse(documentAuth);
|
||||
const recipientAuthOption = ZRecipientAuthOptionsSchema.parse(recipientAuth);
|
||||
|
||||
const derivedRecipientAccessAuth: TRecipientAccessAuthTypes | null =
|
||||
recipientAuthOption.accessAuth || documentAuthOption.globalAccessAuth;
|
||||
const derivedRecipientAccessAuth: TRecipientAccessAuthTypes[] =
|
||||
recipientAuthOption.accessAuth.length > 0
|
||||
? recipientAuthOption.accessAuth
|
||||
: documentAuthOption.globalAccessAuth;
|
||||
|
||||
const derivedRecipientActionAuth: TRecipientActionAuthTypes | null =
|
||||
recipientAuthOption.actionAuth || documentAuthOption.globalActionAuth;
|
||||
const derivedRecipientActionAuth: TRecipientActionAuthTypes[] =
|
||||
recipientAuthOption.actionAuth.length > 0
|
||||
? recipientAuthOption.actionAuth
|
||||
: documentAuthOption.globalActionAuth;
|
||||
|
||||
const recipientAccessAuthRequired = derivedRecipientAccessAuth !== null;
|
||||
const recipientAccessAuthRequired = derivedRecipientAccessAuth.length > 0;
|
||||
|
||||
const recipientActionAuthRequired =
|
||||
derivedRecipientActionAuth !== DocumentAuth.EXPLICIT_NONE &&
|
||||
derivedRecipientActionAuth !== null;
|
||||
derivedRecipientActionAuth.length > 0 &&
|
||||
!derivedRecipientActionAuth.includes(DocumentAuth.EXPLICIT_NONE);
|
||||
|
||||
return {
|
||||
derivedRecipientAccessAuth,
|
||||
@ -54,8 +58,8 @@ export const extractDocumentAuthMethods = ({
|
||||
*/
|
||||
export const createDocumentAuthOptions = (options: TDocumentAuthOptions): TDocumentAuthOptions => {
|
||||
return {
|
||||
globalAccessAuth: options?.globalAccessAuth ?? null,
|
||||
globalActionAuth: options?.globalActionAuth ?? null,
|
||||
globalAccessAuth: options?.globalAccessAuth ?? [],
|
||||
globalActionAuth: options?.globalActionAuth ?? [],
|
||||
};
|
||||
};
|
||||
|
||||
@ -66,7 +70,7 @@ export const createRecipientAuthOptions = (
|
||||
options: TRecipientAuthOptions,
|
||||
): TRecipientAuthOptions => {
|
||||
return {
|
||||
accessAuth: options?.accessAuth ?? null,
|
||||
actionAuth: options?.actionAuth ?? null,
|
||||
accessAuth: options?.accessAuth ?? [],
|
||||
actionAuth: options?.actionAuth ?? [],
|
||||
};
|
||||
};
|
||||
|
||||
@ -206,8 +206,8 @@ export const ZCreateDocumentV2RequestSchema = z.object({
|
||||
title: ZDocumentTitleSchema,
|
||||
externalId: ZDocumentExternalIdSchema.optional(),
|
||||
visibility: ZDocumentVisibilitySchema.optional(),
|
||||
globalAccessAuth: ZDocumentAccessAuthTypesSchema.optional(),
|
||||
globalActionAuth: ZDocumentActionAuthTypesSchema.optional(),
|
||||
globalAccessAuth: z.array(ZDocumentAccessAuthTypesSchema).optional(),
|
||||
globalActionAuth: z.array(ZDocumentActionAuthTypesSchema).optional(),
|
||||
formValues: ZDocumentFormValuesSchema.optional(),
|
||||
recipients: z
|
||||
.array(
|
||||
|
||||
@ -42,8 +42,8 @@ export const ZUpdateDocumentRequestSchema = z.object({
|
||||
title: ZDocumentTitleSchema.optional(),
|
||||
externalId: ZDocumentExternalIdSchema.nullish(),
|
||||
visibility: ZDocumentVisibilitySchema.optional(),
|
||||
globalAccessAuth: ZDocumentAccessAuthTypesSchema.nullish(),
|
||||
globalActionAuth: ZDocumentActionAuthTypesSchema.nullish(),
|
||||
globalAccessAuth: z.array(ZDocumentAccessAuthTypesSchema).optional(),
|
||||
globalActionAuth: z.array(ZDocumentActionAuthTypesSchema).optional(),
|
||||
useLegacyFieldInsertion: z.boolean().optional(),
|
||||
})
|
||||
.optional(),
|
||||
|
||||
@ -26,8 +26,8 @@ export const ZCreateRecipientSchema = z.object({
|
||||
name: z.string(),
|
||||
role: z.nativeEnum(RecipientRole),
|
||||
signingOrder: z.number().optional(),
|
||||
accessAuth: ZRecipientAccessAuthTypesSchema.optional().nullable(),
|
||||
actionAuth: ZRecipientActionAuthTypesSchema.optional().nullable(),
|
||||
accessAuth: z.array(ZRecipientAccessAuthTypesSchema).optional().default([]),
|
||||
actionAuth: z.array(ZRecipientActionAuthTypesSchema).optional().default([]),
|
||||
});
|
||||
|
||||
export const ZUpdateRecipientSchema = z.object({
|
||||
@ -36,8 +36,8 @@ export const ZUpdateRecipientSchema = z.object({
|
||||
name: z.string().optional(),
|
||||
role: z.nativeEnum(RecipientRole).optional(),
|
||||
signingOrder: z.number().optional(),
|
||||
accessAuth: ZRecipientAccessAuthTypesSchema.optional().nullable(),
|
||||
actionAuth: ZRecipientActionAuthTypesSchema.optional().nullable(),
|
||||
accessAuth: z.array(ZRecipientAccessAuthTypesSchema).optional().default([]),
|
||||
actionAuth: z.array(ZRecipientActionAuthTypesSchema).optional().default([]),
|
||||
});
|
||||
|
||||
export const ZCreateDocumentRecipientRequestSchema = z.object({
|
||||
@ -106,7 +106,7 @@ export const ZSetDocumentRecipientsRequestSchema = z
|
||||
name: z.string(),
|
||||
role: z.nativeEnum(RecipientRole),
|
||||
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(),
|
||||
role: z.nativeEnum(RecipientRole),
|
||||
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(),
|
||||
externalId: z.string().nullish(),
|
||||
visibility: z.nativeEnum(DocumentVisibility).optional(),
|
||||
globalAccessAuth: ZDocumentAccessAuthTypesSchema.nullable().optional(),
|
||||
globalActionAuth: ZDocumentActionAuthTypesSchema.nullable().optional(),
|
||||
globalAccessAuth: z.array(ZDocumentAccessAuthTypesSchema).optional().default([]),
|
||||
globalActionAuth: z.array(ZDocumentActionAuthTypesSchema).optional().default([]),
|
||||
publicTitle: z
|
||||
.string()
|
||||
.trim()
|
||||
|
||||
@ -1,51 +1,75 @@
|
||||
import { forwardRef } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import type { SelectProps } from '@radix-ui/react-select';
|
||||
import { InfoIcon } from 'lucide-react';
|
||||
|
||||
import { DOCUMENT_AUTH_TYPES } from '@documenso/lib/constants/document-auth';
|
||||
import { DocumentAccessAuth } from '@documenso/lib/types/document-auth';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@documenso/ui/primitives/select';
|
||||
import { MultiSelect, type Option } from '@documenso/ui/primitives/multiselect';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip';
|
||||
|
||||
export const DocumentGlobalAuthAccessSelect = forwardRef<HTMLButtonElement, SelectProps>(
|
||||
(props, ref) => {
|
||||
export interface DocumentGlobalAuthAccessSelectProps {
|
||||
value?: string[];
|
||||
defaultValue?: string[];
|
||||
onValueChange?: (value: string[]) => void;
|
||||
disabled?: boolean;
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
export const DocumentGlobalAuthAccessSelect = ({
|
||||
value,
|
||||
defaultValue,
|
||||
onValueChange,
|
||||
disabled,
|
||||
placeholder,
|
||||
}: DocumentGlobalAuthAccessSelectProps) => {
|
||||
const { _ } = useLingui();
|
||||
|
||||
return (
|
||||
<Select {...props}>
|
||||
<SelectTrigger ref={ref} className="bg-background text-muted-foreground">
|
||||
<SelectValue
|
||||
data-testid="documentAccessSelectValue"
|
||||
placeholder={_(msg`No restrictions`)}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
|
||||
<SelectContent position="popper">
|
||||
{/* Note: -1 is remapped in the Zod schema to the required value. */}
|
||||
<SelectItem value={'-1'}>
|
||||
<Trans>No restrictions</Trans>
|
||||
</SelectItem>
|
||||
|
||||
{Object.values(DocumentAccessAuth).map((authType) => (
|
||||
<SelectItem key={authType} value={authType}>
|
||||
{DOCUMENT_AUTH_TYPES[authType].value}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
// Convert auth types to MultiSelect options
|
||||
const authOptions: Option[] = [
|
||||
{
|
||||
value: '-1',
|
||||
label: _(msg`No restrictions`),
|
||||
},
|
||||
);
|
||||
...Object.values(DocumentAccessAuth).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 (
|
||||
<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';
|
||||
|
||||
@ -63,7 +87,11 @@ export const DocumentGlobalAuthAccessTooltip = () => (
|
||||
</h2>
|
||||
|
||||
<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>
|
||||
|
||||
<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 { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import type { SelectProps } from '@radix-ui/react-select';
|
||||
import { InfoIcon } from 'lucide-react';
|
||||
|
||||
import { DOCUMENT_AUTH_TYPES } from '@documenso/lib/constants/document-auth';
|
||||
import { DocumentActionAuth, DocumentAuth } from '@documenso/lib/types/document-auth';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@documenso/ui/primitives/select';
|
||||
import { MultiSelect, type Option } from '@documenso/ui/primitives/multiselect';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip';
|
||||
|
||||
export const DocumentGlobalAuthActionSelect = forwardRef<HTMLButtonElement, SelectProps>(
|
||||
(props, ref) => {
|
||||
export interface DocumentGlobalAuthActionSelectProps {
|
||||
value?: string[];
|
||||
defaultValue?: string[];
|
||||
onValueChange?: (value: string[]) => void;
|
||||
disabled?: boolean;
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
export const DocumentGlobalAuthActionSelect = ({
|
||||
value,
|
||||
defaultValue,
|
||||
onValueChange,
|
||||
disabled,
|
||||
placeholder,
|
||||
}: DocumentGlobalAuthActionSelectProps) => {
|
||||
const { _ } = useLingui();
|
||||
|
||||
return (
|
||||
<Select {...props}>
|
||||
<SelectTrigger className="bg-background text-muted-foreground">
|
||||
<SelectValue
|
||||
ref={ref}
|
||||
data-testid="documentActionSelectValue"
|
||||
placeholder={_(msg`No restrictions`)}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
|
||||
<SelectContent position="popper">
|
||||
{/* Note: -1 is remapped in the Zod schema to the required value. */}
|
||||
<SelectItem value={'-1'}>
|
||||
<Trans>No restrictions</Trans>
|
||||
</SelectItem>
|
||||
|
||||
{Object.values(DocumentActionAuth)
|
||||
.filter((auth) => auth !== DocumentAuth.ACCOUNT)
|
||||
.map((authType) => (
|
||||
<SelectItem key={authType} value={authType}>
|
||||
{DOCUMENT_AUTH_TYPES[authType].value}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
// Convert auth types to MultiSelect options
|
||||
const authOptions: Option[] = [
|
||||
{
|
||||
value: '-1',
|
||||
label: _(msg`No restrictions`),
|
||||
},
|
||||
);
|
||||
...Object.values(DocumentActionAuth)
|
||||
.filter((auth) => auth !== DocumentAuth.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 (
|
||||
<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';
|
||||
|
||||
@ -64,20 +85,19 @@ export const DocumentGlobalAuthActionTooltip = () => (
|
||||
</h2>
|
||||
|
||||
<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>
|
||||
<Trans>
|
||||
This can be overriden by setting the authentication requirements directly on each
|
||||
recipient in the next step.
|
||||
These can be overriden by setting the authentication requirements directly on each
|
||||
recipient in the next step. Multiple methods can be selected.
|
||||
</Trans>
|
||||
</p>
|
||||
|
||||
<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>
|
||||
<Trans>
|
||||
<strong>Require passkey</strong> - The recipient must have an account and passkey
|
||||
@ -90,6 +110,14 @@ export const DocumentGlobalAuthActionTooltip = () => (
|
||||
their settings
|
||||
</Trans>
|
||||
</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>
|
||||
<Trans>
|
||||
<strong>No restrictions</strong> - No authentication required
|
||||
|
||||
@ -3,33 +3,78 @@ import React from 'react';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import type { SelectProps } from '@radix-ui/react-select';
|
||||
import { InfoIcon } from 'lucide-react';
|
||||
|
||||
import { DOCUMENT_AUTH_TYPES } from '@documenso/lib/constants/document-auth';
|
||||
import { RecipientActionAuth } from '@documenso/lib/types/document-auth';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@documenso/ui/primitives/select';
|
||||
import { MultiSelect, type Option } from '@documenso/ui/primitives/multiselect';
|
||||
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();
|
||||
|
||||
// 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 (
|
||||
<Select {...props}>
|
||||
<SelectTrigger className="bg-background text-muted-foreground">
|
||||
<SelectValue placeholder={_(msg`Inherit authentication method`)} />
|
||||
<div className="relative">
|
||||
<MultiSelect
|
||||
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>
|
||||
<TooltipTrigger className="-mr-1 ml-auto">
|
||||
<InfoIcon className="mx-2 h-4 w-4" />
|
||||
<TooltipTrigger className="absolute right-2 top-1/2 -translate-y-1/2">
|
||||
<InfoIcon className="h-4 w-4" />
|
||||
</TooltipTrigger>
|
||||
|
||||
<TooltipContent className="text-foreground max-w-md p-4">
|
||||
@ -40,11 +85,13 @@ export const RecipientActionAuthSelect = (props: RecipientActionAuthSelectProps)
|
||||
</h2>
|
||||
|
||||
<p>
|
||||
<Trans>The authentication required for recipients to sign fields</Trans>
|
||||
<Trans>The authentication methods required for recipients to sign fields</Trans>
|
||||
</p>
|
||||
|
||||
<p className="mt-2">
|
||||
<Trans>This will override any global settings.</Trans>
|
||||
<Trans>
|
||||
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">
|
||||
@ -54,10 +101,6 @@ export const RecipientActionAuthSelect = (props: RecipientActionAuthSelectProps)
|
||||
authentication method configured in the "General Settings" step
|
||||
</Trans>
|
||||
</li>
|
||||
{/* <li>
|
||||
<strong>Require account</strong> - The recipient must be
|
||||
signed in
|
||||
</li> */}
|
||||
<li>
|
||||
<Trans>
|
||||
<strong>Require passkey</strong> - The recipient must have an account and passkey
|
||||
@ -78,22 +121,6 @@ export const RecipientActionAuthSelect = (props: RecipientActionAuthSelectProps)
|
||||
</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>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -98,8 +98,8 @@ export const AddSettingsFormPartial = ({
|
||||
title: document.title,
|
||||
externalId: document.externalId || '',
|
||||
visibility: document.visibility || '',
|
||||
globalAccessAuth: documentAuthOption?.globalAccessAuth || undefined,
|
||||
globalActionAuth: documentAuthOption?.globalActionAuth || undefined,
|
||||
globalAccessAuth: documentAuthOption?.globalAccessAuth || [],
|
||||
globalActionAuth: documentAuthOption?.globalActionAuth || [],
|
||||
|
||||
meta: {
|
||||
timezone:
|
||||
@ -131,6 +131,12 @@ export const AddSettingsFormPartial = ({
|
||||
)
|
||||
.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
|
||||
// when the document is signed.
|
||||
useEffect(() => {
|
||||
@ -214,7 +220,11 @@ export const AddSettingsFormPartial = ({
|
||||
</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
<Select {...field} onValueChange={field.onChange}>
|
||||
<Select
|
||||
value={field.value}
|
||||
disabled={field.disabled}
|
||||
onValueChange={field.onChange}
|
||||
>
|
||||
<SelectTrigger className="bg-background">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
@ -244,7 +254,11 @@ export const AddSettingsFormPartial = ({
|
||||
</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
<DocumentGlobalAuthAccessSelect {...field} onValueChange={field.onChange} />
|
||||
<DocumentGlobalAuthAccessSelect
|
||||
value={field.value}
|
||||
disabled={field.disabled}
|
||||
onValueChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
@ -286,7 +300,11 @@ export const AddSettingsFormPartial = ({
|
||||
</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
<DocumentGlobalAuthActionSelect {...field} onValueChange={field.onChange} />
|
||||
<DocumentGlobalAuthActionSelect
|
||||
value={field.value}
|
||||
disabled={field.disabled}
|
||||
onValueChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
@ -370,7 +388,7 @@ export const AddSettingsFormPartial = ({
|
||||
|
||||
<FormControl>
|
||||
<Select
|
||||
{...field}
|
||||
value={field.value}
|
||||
onValueChange={field.onChange}
|
||||
disabled={documentHasBeenSent}
|
||||
>
|
||||
@ -406,7 +424,7 @@ export const AddSettingsFormPartial = ({
|
||||
<Combobox
|
||||
className="bg-background"
|
||||
options={TIME_ZONES}
|
||||
{...field}
|
||||
value={field.value}
|
||||
onChange={(value) => value && field.onChange(value)}
|
||||
disabled={documentHasBeenSent}
|
||||
/>
|
||||
@ -461,7 +479,7 @@ export const AddSettingsFormPartial = ({
|
||||
disabled={form.formState.isSubmitting}
|
||||
canGoBack={stepIndex !== 0}
|
||||
onGoBackClick={previousStep}
|
||||
onGoNextClick={form.handleSubmit(onSubmit)}
|
||||
onGoNextClick={onGoNextClick}
|
||||
/>
|
||||
</DocumentFlowFormContainerFooter>
|
||||
</>
|
||||
|
||||
@ -16,17 +16,6 @@ import {
|
||||
ZDocumentMetaTimezoneSchema,
|
||||
} 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({
|
||||
title: z
|
||||
.string()
|
||||
@ -34,12 +23,8 @@ export const ZAddSettingsFormSchema = z.object({
|
||||
.min(1, { message: msg`Title cannot be empty`.id }),
|
||||
externalId: z.string().optional(),
|
||||
visibility: z.nativeEnum(DocumentVisibility).optional(),
|
||||
globalAccessAuth: ZMapNegativeOneToUndefinedSchema.pipe(
|
||||
ZDocumentAccessAuthTypesSchema.optional(),
|
||||
),
|
||||
globalActionAuth: ZMapNegativeOneToUndefinedSchema.pipe(
|
||||
ZDocumentActionAuthTypesSchema.optional(),
|
||||
),
|
||||
globalAccessAuth: z.array(ZDocumentAccessAuthTypesSchema),
|
||||
globalActionAuth: z.array(ZDocumentActionAuthTypesSchema),
|
||||
meta: z.object({
|
||||
timezone: ZDocumentMetaTimezoneSchema.optional().default(DEFAULT_DOCUMENT_TIME_ZONE),
|
||||
dateFormat: ZDocumentMetaDateFormatSchema.optional().default(DEFAULT_DOCUMENT_DATE_FORMAT),
|
||||
|
||||
@ -85,7 +85,7 @@ export const AddSignersFormPartial = ({
|
||||
email: '',
|
||||
role: RecipientRole.SIGNER,
|
||||
signingOrder: 1,
|
||||
actionAuth: undefined,
|
||||
actionAuth: [],
|
||||
},
|
||||
];
|
||||
|
||||
@ -119,10 +119,14 @@ export const AddSignersFormPartial = ({
|
||||
const recipientHasAuthOptions = recipients.find((recipient) => {
|
||||
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;
|
||||
}, [recipients, form]);
|
||||
@ -190,7 +194,7 @@ export const AddSignersFormPartial = ({
|
||||
name: '',
|
||||
email: '',
|
||||
role: RecipientRole.SIGNER,
|
||||
actionAuth: undefined,
|
||||
actionAuth: [],
|
||||
signingOrder: signers.length > 0 ? (signers[signers.length - 1]?.signingOrder ?? 0) + 1 : 1,
|
||||
});
|
||||
};
|
||||
@ -226,7 +230,7 @@ export const AddSignersFormPartial = ({
|
||||
name: user?.name ?? '',
|
||||
email: user?.email ?? '',
|
||||
role: RecipientRole.SIGNER,
|
||||
actionAuth: undefined,
|
||||
actionAuth: [],
|
||||
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 { ZMapNegativeOneToUndefinedSchema } from './add-settings.types';
|
||||
|
||||
export const ZAddSignersFormSchema = z
|
||||
.object({
|
||||
signers: z.array(
|
||||
@ -19,9 +17,7 @@ export const ZAddSignersFormSchema = z
|
||||
name: z.string(),
|
||||
role: z.nativeEnum(RecipientRole),
|
||||
signingOrder: z.number().optional(),
|
||||
actionAuth: ZMapNegativeOneToUndefinedSchema.pipe(
|
||||
ZRecipientActionAuthTypesSchema.optional(),
|
||||
),
|
||||
actionAuth: z.array(ZRecipientActionAuthTypesSchema).optional().default([]),
|
||||
}),
|
||||
),
|
||||
signingOrder: z.nativeEnum(DocumentSigningOrder),
|
||||
|
||||
@ -78,6 +78,8 @@ interface MultiSelectProps {
|
||||
>;
|
||||
/** hide the clear all button. */
|
||||
hideClearAllButton?: boolean;
|
||||
/** test id for the select value. */
|
||||
'data-testid'?: string;
|
||||
}
|
||||
|
||||
export interface MultiSelectRef {
|
||||
@ -170,6 +172,7 @@ const MultiSelect = ({
|
||||
commandProps,
|
||||
inputProps,
|
||||
hideClearAllButton = false,
|
||||
'data-testid': dataTestId,
|
||||
}: MultiSelectProps) => {
|
||||
const inputRef = React.useRef<HTMLInputElement>(null);
|
||||
const [open, setOpen] = React.useState(false);
|
||||
@ -403,6 +406,7 @@ const MultiSelect = ({
|
||||
commandProps?.shouldFilter !== undefined ? commandProps.shouldFilter : !onSearch
|
||||
} // When onSearch is provided, we don‘t want to filter the options. You can still override it.
|
||||
filter={commandFilter()}
|
||||
data-testid={dataTestId}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
|
||||
@ -86,7 +86,7 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
|
||||
{
|
||||
formId: initialId,
|
||||
role: RecipientRole.SIGNER,
|
||||
actionAuth: undefined,
|
||||
actionAuth: [],
|
||||
...generateRecipientPlaceholder(1),
|
||||
signingOrder: 1,
|
||||
},
|
||||
@ -136,10 +136,14 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
|
||||
const recipientHasAuthOptions = recipients.find((recipient) => {
|
||||
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;
|
||||
}, [recipients, form]);
|
||||
@ -179,6 +183,7 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
|
||||
email: user.email ?? '',
|
||||
role: RecipientRole.SIGNER,
|
||||
signingOrder: signers.length > 0 ? (signers[signers.length - 1]?.signingOrder ?? 0) + 1 : 1,
|
||||
actionAuth: [],
|
||||
});
|
||||
};
|
||||
|
||||
@ -188,6 +193,7 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
|
||||
role: RecipientRole.SIGNER,
|
||||
...generateRecipientPlaceholder(placeholderRecipientCount),
|
||||
signingOrder: signers.length > 0 ? (signers[signers.length - 1]?.signingOrder ?? 0) + 1 : 1,
|
||||
actionAuth: [],
|
||||
});
|
||||
|
||||
setPlaceholderRecipientCount((count) => count + 1);
|
||||
|
||||
@ -3,8 +3,6 @@ import { z } from 'zod';
|
||||
|
||||
import { ZRecipientActionAuthTypesSchema } from '@documenso/lib/types/document-auth';
|
||||
|
||||
import { ZMapNegativeOneToUndefinedSchema } from '../document-flow/add-settings.types';
|
||||
|
||||
export const ZAddTemplatePlacholderRecipientsFormSchema = z
|
||||
.object({
|
||||
signers: z.array(
|
||||
@ -15,9 +13,7 @@ export const ZAddTemplatePlacholderRecipientsFormSchema = z
|
||||
name: z.string(),
|
||||
role: z.nativeEnum(RecipientRole),
|
||||
signingOrder: z.number().optional(),
|
||||
actionAuth: ZMapNegativeOneToUndefinedSchema.pipe(
|
||||
ZRecipientActionAuthTypesSchema.optional(),
|
||||
),
|
||||
actionAuth: z.array(ZRecipientActionAuthTypesSchema).optional().default([]),
|
||||
}),
|
||||
),
|
||||
signingOrder: z.nativeEnum(DocumentSigningOrder),
|
||||
|
||||
@ -106,8 +106,8 @@ export const AddTemplateSettingsFormPartial = ({
|
||||
title: template.title,
|
||||
externalId: template.externalId || undefined,
|
||||
visibility: template.visibility || '',
|
||||
globalAccessAuth: documentAuthOption?.globalAccessAuth || undefined,
|
||||
globalActionAuth: documentAuthOption?.globalActionAuth || undefined,
|
||||
globalAccessAuth: documentAuthOption?.globalAccessAuth || [],
|
||||
globalActionAuth: documentAuthOption?.globalActionAuth || [],
|
||||
meta: {
|
||||
subject: template.templateMeta?.subject ?? '',
|
||||
message: template.templateMeta?.message ?? '',
|
||||
@ -237,7 +237,11 @@ export const AddTemplateSettingsFormPartial = ({
|
||||
</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
<DocumentGlobalAuthAccessSelect {...field} onValueChange={field.onChange} />
|
||||
<DocumentGlobalAuthAccessSelect
|
||||
value={field.value}
|
||||
disabled={field.disabled}
|
||||
onValueChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
@ -378,7 +382,11 @@ export const AddTemplateSettingsFormPartial = ({
|
||||
</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
<DocumentGlobalAuthActionSelect {...field} onValueChange={field.onChange} />
|
||||
<DocumentGlobalAuthActionSelect
|
||||
value={field.value}
|
||||
disabled={field.disabled}
|
||||
onValueChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
|
||||
@ -18,18 +18,12 @@ import {
|
||||
ZDocumentMetaTimezoneSchema,
|
||||
} from '@documenso/trpc/server/document-router/schema';
|
||||
|
||||
import { ZMapNegativeOneToUndefinedSchema } from '../document-flow/add-settings.types';
|
||||
|
||||
export const ZAddTemplateSettingsFormSchema = z.object({
|
||||
title: z.string().trim().min(1, { message: "Title can't be empty" }),
|
||||
externalId: z.string().optional(),
|
||||
visibility: z.nativeEnum(DocumentVisibility).optional(),
|
||||
globalAccessAuth: ZMapNegativeOneToUndefinedSchema.pipe(
|
||||
ZDocumentAccessAuthTypesSchema.optional(),
|
||||
),
|
||||
globalActionAuth: ZMapNegativeOneToUndefinedSchema.pipe(
|
||||
ZDocumentActionAuthTypesSchema.optional(),
|
||||
),
|
||||
globalAccessAuth: z.array(ZDocumentAccessAuthTypesSchema).optional().default([]),
|
||||
globalActionAuth: z.array(ZDocumentActionAuthTypesSchema).optional().default([]),
|
||||
meta: z.object({
|
||||
subject: z.string(),
|
||||
message: z.string(),
|
||||
|
||||
Reference in New Issue
Block a user