mirror of
https://github.com/documenso/documenso.git
synced 2026-07-24 17:04:12 +10:00
Merge branch 'main' into feat/prefetch-intent-navigation-links
This commit is contained in:
@@ -571,6 +571,22 @@ export const formatDocumentAuditLogAction = (
|
||||
you: msg`You deleted an envelope item with title ${data.envelopeItemTitle}`,
|
||||
user: msg`${user} deleted an envelope item with title ${data.envelopeItemTitle}`,
|
||||
}))
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.ENVELOPE_ITEM_UPDATED }, () => ({
|
||||
anonymous: msg({
|
||||
message: `Envelope item updated`,
|
||||
context: `Audit log format`,
|
||||
}),
|
||||
you: msg`You updated an envelope item`,
|
||||
user: msg`${user} updated an envelope item`,
|
||||
}))
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.ENVELOPE_ITEM_PDF_REPLACED }, ({ data }) => ({
|
||||
anonymous: msg({
|
||||
message: `Envelope item PDF replaced`,
|
||||
context: `Audit log format`,
|
||||
}),
|
||||
you: msg`You replaced the PDF for envelope item ${data.envelopeItemTitle}`,
|
||||
user: msg`${user} replaced the PDF for envelope item ${data.envelopeItemTitle}`,
|
||||
}))
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_EXPIRED }, ({ data }) => ({
|
||||
anonymous: msg({
|
||||
message: `Recipient signing window expired`,
|
||||
|
||||
@@ -66,6 +66,9 @@ export const extractDerivedDocumentMeta = (
|
||||
// Envelope expiration.
|
||||
envelopeExpirationPeriod:
|
||||
meta.envelopeExpirationPeriod ?? settings.envelopeExpirationPeriod ?? null,
|
||||
|
||||
// Reminder settings.
|
||||
reminderSettings: meta.reminderSettings ?? settings.reminderSettings ?? null,
|
||||
} satisfies Omit<DocumentMeta, 'id'>;
|
||||
};
|
||||
|
||||
|
||||
@@ -67,6 +67,9 @@ export const buildEmbeddedFeatures = (
|
||||
allowConfigureExpirationPeriod:
|
||||
features.settings?.allowConfigureExpirationPeriod ??
|
||||
DEFAULT_EMBEDDED_EDITOR_CONFIG.settings.allowConfigureExpirationPeriod,
|
||||
allowConfigureReminders:
|
||||
features.settings?.allowConfigureReminders ??
|
||||
DEFAULT_EMBEDDED_EDITOR_CONFIG.settings.allowConfigureReminders,
|
||||
allowConfigureEmailSender:
|
||||
features.settings?.allowConfigureEmailSender ??
|
||||
DEFAULT_EMBEDDED_EDITOR_CONFIG.settings.allowConfigureEmailSender,
|
||||
@@ -88,6 +91,9 @@ export const buildEmbeddedFeatures = (
|
||||
allowDuplication:
|
||||
features.actions?.allowDuplication ??
|
||||
DEFAULT_EMBEDDED_EDITOR_CONFIG.actions.allowDuplication,
|
||||
allowSaveAsTemplate:
|
||||
features.actions?.allowSaveAsTemplate ??
|
||||
DEFAULT_EMBEDDED_EDITOR_CONFIG.actions.allowSaveAsTemplate,
|
||||
allowDownloadPDF:
|
||||
features.actions?.allowDownloadPDF ??
|
||||
DEFAULT_EMBEDDED_EDITOR_CONFIG.actions.allowDownloadPDF,
|
||||
@@ -110,6 +116,9 @@ export const buildEmbeddedFeatures = (
|
||||
allowDelete:
|
||||
features.envelopeItems?.allowDelete ??
|
||||
DEFAULT_EMBEDDED_EDITOR_CONFIG.envelopeItems.allowDelete,
|
||||
allowReplace:
|
||||
features.envelopeItems?.allowReplace ??
|
||||
DEFAULT_EMBEDDED_EDITOR_CONFIG.envelopeItems.allowReplace,
|
||||
}
|
||||
: null,
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
ZTextFieldMeta,
|
||||
} from '@documenso/lib/types/field-meta';
|
||||
import { toCheckboxCustomText, toRadioCustomText } from '@documenso/lib/utils/fields';
|
||||
import { zEmail } from '@documenso/lib/utils/zod';
|
||||
import type { TSignEnvelopeFieldValue } from '@documenso/trpc/server/envelope-router/sign-envelope-field.types';
|
||||
import { checkboxValidationSigns } from '@documenso/ui/primitives/document-flow/field-items-advanced-settings/constants';
|
||||
|
||||
@@ -37,7 +38,7 @@ export const extractFieldInsertionValues = ({
|
||||
}: ExtractFieldInsertionValuesOptions): { customText: string; inserted: boolean } => {
|
||||
return match(fieldValue)
|
||||
.with({ type: FieldType.EMAIL }, (fieldValue) => {
|
||||
const parsedEmailValue = z.string().email().nullable().safeParse(fieldValue.value);
|
||||
const parsedEmailValue = zEmail().nullable().safeParse(fieldValue.value);
|
||||
|
||||
if (!parsedEmailValue.success) {
|
||||
throw new AppError(AppErrorCode.INVALID_BODY, {
|
||||
|
||||
@@ -244,28 +244,57 @@ export const mapSecondaryIdToTemplateId = (secondaryId: string) => {
|
||||
return parseInt(parsed.data.split('_')[1]);
|
||||
};
|
||||
|
||||
export const canEnvelopeItemsBeModified = (
|
||||
envelope: Pick<Envelope, 'completedAt' | 'deletedAt' | 'type' | 'status'>,
|
||||
recipients: Recipient[],
|
||||
) => {
|
||||
if (envelope.completedAt || envelope.deletedAt || envelope.status !== DocumentStatus.DRAFT) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (envelope.type === EnvelopeType.TEMPLATE) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
recipients.some(
|
||||
(recipient) =>
|
||||
recipient.role !== RecipientRole.CC &&
|
||||
(recipient.signingStatus === SigningStatus.SIGNED ||
|
||||
recipient.sendStatus === SendStatus.SENT),
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
export type EnvelopeItemPermissions = {
|
||||
canTitleBeChanged: boolean;
|
||||
canFileBeChanged: boolean;
|
||||
canOrderBeChanged: boolean;
|
||||
};
|
||||
|
||||
export const getEnvelopeItemPermissions = (
|
||||
envelope: Pick<Envelope, 'completedAt' | 'deletedAt' | 'type' | 'status'>,
|
||||
recipients: Pick<Recipient, 'role' | 'signingStatus' | 'sendStatus'>[],
|
||||
): EnvelopeItemPermissions => {
|
||||
// Always reject completed/rejected/deleted envelopes.
|
||||
if (
|
||||
envelope.completedAt ||
|
||||
envelope.deletedAt ||
|
||||
envelope.status === DocumentStatus.REJECTED ||
|
||||
envelope.status === DocumentStatus.COMPLETED
|
||||
) {
|
||||
return {
|
||||
canTitleBeChanged: false,
|
||||
canFileBeChanged: false,
|
||||
canOrderBeChanged: false,
|
||||
};
|
||||
}
|
||||
|
||||
// Templates can always be modified.
|
||||
if (envelope.type === EnvelopeType.TEMPLATE) {
|
||||
return {
|
||||
canTitleBeChanged: true,
|
||||
canFileBeChanged: true,
|
||||
canOrderBeChanged: true,
|
||||
};
|
||||
}
|
||||
|
||||
const hasActiveRecipients = recipients.some(
|
||||
(recipient) =>
|
||||
recipient.role !== RecipientRole.CC &&
|
||||
(recipient.signingStatus === SigningStatus.SIGNED ||
|
||||
recipient.signingStatus === SigningStatus.REJECTED ||
|
||||
recipient.sendStatus === SendStatus.SENT),
|
||||
);
|
||||
|
||||
return match(envelope.status)
|
||||
.with(DocumentStatus.DRAFT, () => ({
|
||||
canTitleBeChanged: true,
|
||||
canFileBeChanged: true,
|
||||
canOrderBeChanged: true,
|
||||
}))
|
||||
.with(DocumentStatus.PENDING, () => ({
|
||||
canTitleBeChanged: true,
|
||||
canFileBeChanged: false,
|
||||
canOrderBeChanged: !hasActiveRecipients, // Only allow order changes if no active recipients.
|
||||
}))
|
||||
.exhaustive();
|
||||
};
|
||||
|
||||
@@ -9,6 +9,7 @@ import type { ORGANISATION_MEMBER_ROLE_MAP } from '@documenso/lib/constants/orga
|
||||
|
||||
import { DEFAULT_DOCUMENT_DATE_FORMAT } from '../constants/date-formats';
|
||||
import { DEFAULT_ENVELOPE_EXPIRATION_PERIOD } from '../constants/envelope-expiration';
|
||||
import { DEFAULT_ENVELOPE_REMINDER_SETTINGS } from '../constants/envelope-reminder';
|
||||
import {
|
||||
LOWEST_ORGANISATION_ROLE,
|
||||
ORGANISATION_MEMBER_ROLE_HIERARCHY,
|
||||
@@ -142,6 +143,8 @@ export const generateDefaultOrganisationSettings = (): Omit<
|
||||
|
||||
envelopeExpirationPeriod: DEFAULT_ENVELOPE_EXPIRATION_PERIOD,
|
||||
|
||||
reminderSettings: DEFAULT_ENVELOPE_REMINDER_SETTINGS,
|
||||
|
||||
aiFeaturesEnabled: false,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import type { Envelope } from '@prisma/client';
|
||||
import { type Field, type Recipient, RecipientRole, SigningStatus } from '@prisma/client';
|
||||
import { z } from 'zod';
|
||||
import { type Field, RecipientRole, SigningStatus } from '@prisma/client';
|
||||
|
||||
import { isSignatureFieldType } from '@documenso/prisma/guards/is-signature-field';
|
||||
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../constants/app';
|
||||
import { AppError, AppErrorCode } from '../errors/app-error';
|
||||
import type { TRecipientLite } from '../types/recipient';
|
||||
import { extractLegacyIds } from '../universal/id';
|
||||
import { zEmail } from './zod';
|
||||
|
||||
/**
|
||||
* Roles that require fields to be assigned before a document can be distributed.
|
||||
@@ -20,7 +21,7 @@ export const RECIPIENT_ROLES_THAT_REQUIRE_FIELDS = [RecipientRole.SIGNER] as con
|
||||
*
|
||||
* Currently only SIGNERs are validated - they must have at least one signature field.
|
||||
*/
|
||||
export const getRecipientsWithMissingFields = <T extends Pick<Recipient, 'id' | 'role'>>(
|
||||
export const getRecipientsWithMissingFields = <T extends Pick<TRecipientLite, 'id' | 'role'>>(
|
||||
recipients: T[],
|
||||
fields: Pick<Field, 'type' | 'recipientId'>[],
|
||||
): T[] => {
|
||||
@@ -42,7 +43,10 @@ export const formatSigningLink = (token: string) => `${NEXT_PUBLIC_WEBAPP_URL()}
|
||||
/**
|
||||
* Whether a recipient can be modified by the document owner.
|
||||
*/
|
||||
export const canRecipientBeModified = (recipient: Recipient, fields: Field[]) => {
|
||||
export const canRecipientBeModified = (
|
||||
recipient: TRecipientLite,
|
||||
fields: Pick<Field, 'recipientId' | 'inserted'>[],
|
||||
) => {
|
||||
if (!recipient) {
|
||||
return false;
|
||||
}
|
||||
@@ -72,7 +76,10 @@ export const canRecipientBeModified = (recipient: Recipient, fields: Field[]) =>
|
||||
* - They are not a Viewer or CCer
|
||||
* - They can be modified (canRecipientBeModified)
|
||||
*/
|
||||
export const canRecipientFieldsBeModified = (recipient: Recipient, fields: Field[]) => {
|
||||
export const canRecipientFieldsBeModified = (
|
||||
recipient: TRecipientLite,
|
||||
fields: Pick<Field, 'recipientId' | 'inserted'>[],
|
||||
) => {
|
||||
if (!canRecipientBeModified(recipient, fields)) {
|
||||
return false;
|
||||
}
|
||||
@@ -81,7 +88,7 @@ export const canRecipientFieldsBeModified = (recipient: Recipient, fields: Field
|
||||
};
|
||||
|
||||
export const mapRecipientToLegacyRecipient = (
|
||||
recipient: Recipient,
|
||||
recipient: TRecipientLite,
|
||||
envelope: Pick<Envelope, 'type' | 'secondaryId'>,
|
||||
) => {
|
||||
const legacyId = extractLegacyIds(envelope);
|
||||
@@ -92,8 +99,18 @@ export const mapRecipientToLegacyRecipient = (
|
||||
};
|
||||
};
|
||||
|
||||
export const isRecipientEmailValidForSending = (recipient: Pick<Recipient, 'email'>) => {
|
||||
return z.string().email().safeParse(recipient.email).success;
|
||||
export const findRecipientByEmail = <T extends { email: string }>({
|
||||
recipients,
|
||||
userEmail,
|
||||
teamEmail,
|
||||
}: {
|
||||
recipients: T[];
|
||||
userEmail: string;
|
||||
teamEmail?: string | null;
|
||||
}) => recipients.find((r) => r.email === userEmail || (teamEmail && r.email === teamEmail));
|
||||
|
||||
export const isRecipientEmailValidForSending = (recipient: Pick<TRecipientLite, 'email'>) => {
|
||||
return zEmail().safeParse(recipient.email).success;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -208,6 +208,8 @@ export const generateDefaultTeamSettings = (): Omit<TeamGlobalSettings, 'id' | '
|
||||
|
||||
envelopeExpirationPeriod: null,
|
||||
|
||||
reminderSettings: null,
|
||||
|
||||
aiFeaturesEnabled: null,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
/**
|
||||
* RFC 5322 compliant email regex.
|
||||
*
|
||||
* This is more permissive than Zod's built-in `.email()` validator which rejects
|
||||
* valid international characters (e.g. "Søren@gmail.com").
|
||||
*
|
||||
* Compiled once at module level to avoid re-compilation on every validation call.
|
||||
*/
|
||||
const EMAIL_REGEX =
|
||||
/^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~\u{0080}-\u{FFFF}-]+@[a-zA-Z0-9\u{0080}-\u{FFFF}](?:[a-zA-Z0-9\u{0080}-\u{FFFF}-]{0,61}[a-zA-Z0-9\u{0080}-\u{FFFF}])?(?:\.[a-zA-Z0-9\u{0080}-\u{FFFF}](?:[a-zA-Z0-9\u{0080}-\u{FFFF}-]{0,61}[a-zA-Z0-9\u{0080}-\u{FFFF}])?)*$/u;
|
||||
|
||||
const DEFAULT_EMAIL_MESSAGE = 'Invalid email address';
|
||||
|
||||
/**
|
||||
* Creates a Zod email schema using an RFC 5322 compliant regex.
|
||||
*
|
||||
* Supports international characters in the local part and domain
|
||||
* (e.g. "Søren@gmail.com", "user@dömain.com").
|
||||
*
|
||||
* Returns a standard `ZodString` so all string methods are chainable:
|
||||
* `.min()`, `.max()`, `.trim()`, `.toLowerCase()`, `.optional()`, `.nullish()`, etc.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* zEmail()
|
||||
* zEmail().min(1).max(254)
|
||||
* zEmail().trim().toLowerCase()
|
||||
* zEmail('Email is invalid')
|
||||
* zEmail({ message: 'Email is invalid' })
|
||||
* ```
|
||||
*/
|
||||
export const zEmail = (options?: string | { message?: string }) => {
|
||||
const message =
|
||||
typeof options === 'string' ? options : (options?.message ?? DEFAULT_EMAIL_MESSAGE);
|
||||
|
||||
return z.string().regex(EMAIL_REGEX, { message });
|
||||
};
|
||||
Reference in New Issue
Block a user