Merge branch 'main' into feat/signing-required-field-colors

Resolves conflict in document-signing-field-container.tsx (kept HEAD's
required-field-color logic) and applies biome formatting to the
auto-merged envelope-signer-page-renderer.tsx.
This commit is contained in:
ephraimduncan
2026-05-12 11:03:37 +00:00
1471 changed files with 17866 additions and 27981 deletions
@@ -1,6 +1,5 @@
import { match } from 'ts-pattern';
import { checkboxValidationSigns } from '@documenso/ui/primitives/document-flow/field-items-advanced-settings/constants';
import { match } from 'ts-pattern';
import type { TCheckboxFieldMeta } from '../types/field-meta';
@@ -45,17 +44,13 @@ export const validateCheckboxField = (
switch (validation.value) {
case '=':
lengthCondition = isSigningPage
? values.length !== validationLength
: values.length < validationLength;
lengthCondition = isSigningPage ? values.length !== validationLength : values.length < validationLength;
break;
case '>=':
lengthCondition = values.length < validationLength;
break;
case '<=':
lengthCondition = isSigningPage
? values.length > validationLength
: values.length < validationLength;
lengthCondition = isSigningPage ? values.length > validationLength : values.length < validationLength;
break;
}
+1 -6
View File
@@ -32,12 +32,7 @@ const versionToFilenameSuffix = (version: DocumentVersion): string => {
}
};
export const downloadPDF = async ({
envelopeItem,
token,
fileName,
version = 'signed',
}: DownloadPDFProps) => {
export const downloadPDF = async ({ envelopeItem, token, fileName, version = 'signed' }: DownloadPDFProps) => {
const downloadUrl = getEnvelopeItemPdfUrl({
type: 'download',
envelopeItem: envelopeItem,
@@ -1,7 +1,15 @@
import { posthog } from 'posthog-js';
import { extractPostHogConfig } from '@documenso/lib/constants/feature-flags';
let posthogPromise: Promise<typeof import('posthog-js')> | null = null;
const getPosthog = async () => {
if (!posthogPromise) {
posthogPromise = import('posthog-js');
}
return posthogPromise;
};
export function useAnalytics() {
// const featureFlags = useFeatureFlags();
const isPostHogEnabled = extractPostHogConfig();
@@ -17,7 +25,9 @@ export function useAnalytics() {
return;
}
posthog.capture(event, properties);
void getPosthog().then(({ default: posthog }) => {
posthog.capture(event, properties);
});
};
/**
@@ -31,7 +41,9 @@ export function useAnalytics() {
return;
}
posthog.captureException(error, properties);
void getPosthog().then(({ default: posthog }) => {
posthog.captureException(error, properties);
});
};
/**
@@ -5,10 +5,7 @@ type SaveRequest<T, R> = {
onResponse?: (response: R) => void;
};
export const useAutoSave = <T, R = void>(
onSave: (data: T) => Promise<R>,
options: { delay?: number } = {},
) => {
export const useAutoSave = <T, R = void>(onSave: (data: T) => Promise<R>, options: { delay?: number } = {}) => {
const { delay = 2000 } = options;
const saveTimeoutRef = useRef<NodeJS.Timeout>();
@@ -11,8 +11,7 @@ export type UseCopyShareLinkOptions = {
export function useCopyShareLink({ onSuccess, onError }: UseCopyShareLinkOptions) {
const [, copyToClipboard] = useCopyToClipboard();
const { mutateAsync: createOrGetShareLink, isPending: isCreatingShareLink } =
trpc.document.share.useMutation();
const { mutateAsync: createOrGetShareLink, isPending: isCreatingShareLink } = trpc.document.share.useMutation();
/**
* Copy a newly created, or pre-existing share link to the user's clipboard.
@@ -18,9 +18,7 @@ export function useCopyToClipboard(): [CopiedValue, CopyFn] {
// Try to save to clipboard then save it in the state if worked
try {
isClipboardApiSupported
? await handleClipboardApiCopy(text, blobType)
: await handleWriteTextCopy(text);
isClipboardApiSupported ? await handleClipboardApiCopy(text, blobType) : await handleWriteTextCopy(text);
setCopiedText(await text);
return true;
@@ -1,6 +1,5 @@
import { useCallback } from 'react';
import { getBoundingClientRect } from '@documenso/lib/client-only/get-bounding-client-rect';
import { useCallback } from 'react';
export const useDocumentElement = () => {
/**
@@ -15,9 +14,7 @@ export const useDocumentElement = () => {
const $page =
target.closest<HTMLElement>(pageSelector) ??
document
.elementsFromPoint(event.clientX, event.clientY)
.find((el) => el.matches(pageSelector));
document.elementsFromPoint(event.clientX, event.clientY).find((el) => el.matches(pageSelector));
if (!$page) {
return null;
@@ -31,19 +28,9 @@ export const useDocumentElement = () => {
* as a percentage of the page width and height.
*/
const getFieldPosition = (page: HTMLElement, field: HTMLElement) => {
const {
top: pageTop,
left: pageLeft,
height: pageHeight,
width: pageWidth,
} = getBoundingClientRect(page);
const { top: pageTop, left: pageLeft, height: pageHeight, width: pageWidth } = getBoundingClientRect(page);
const {
top: fieldTop,
left: fieldLeft,
height: fieldHeight,
width: fieldWidth,
} = getBoundingClientRect(field);
const { top: fieldTop, left: fieldLeft, height: fieldHeight, width: fieldWidth } = getBoundingClientRect(field);
return {
x: ((fieldLeft - pageLeft) / pageWidth) * 100,
@@ -60,31 +47,28 @@ export const useDocumentElement = () => {
* @param mouseWidth The artifical width of the mouse.
* @param mouseHeight The artifical height of the mouse.
*/
const isWithinPageBounds = useCallback(
(event: MouseEvent, pageSelector: string, mouseWidth = 0, mouseHeight = 0) => {
const $page = getPage(event, pageSelector);
const isWithinPageBounds = useCallback((event: MouseEvent, pageSelector: string, mouseWidth = 0, mouseHeight = 0) => {
const $page = getPage(event, pageSelector);
if (!$page) {
return false;
}
if (!$page) {
return false;
}
const { top, left, height, width } = $page.getBoundingClientRect();
const { top, left, height, width } = $page.getBoundingClientRect();
const halfMouseWidth = mouseWidth / 2;
const halfMouseHeight = mouseHeight / 2;
const halfMouseWidth = mouseWidth / 2;
const halfMouseHeight = mouseHeight / 2;
if (event.clientY > top + height - halfMouseHeight || event.clientY < top + halfMouseHeight) {
return false;
}
if (event.clientY > top + height - halfMouseHeight || event.clientY < top + halfMouseHeight) {
return false;
}
if (event.clientX > left + width - halfMouseWidth || event.clientX < left + halfMouseWidth) {
return false;
}
if (event.clientX > left + width - halfMouseWidth || event.clientX < left + halfMouseWidth) {
return false;
}
return true;
},
[],
);
return true;
}, []);
return {
getPage,
@@ -1,15 +1,13 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import type { Field } from '@prisma/client';
import { FieldType } from '@prisma/client';
import { useFieldArray, useForm } from 'react-hook-form';
import { z } from 'zod';
import { getPdfPagesCount } from '@documenso/lib/constants/pdf-viewer';
import type { TEditorEnvelope } from '@documenso/lib/types/envelope-editor';
import { ZFieldMetaSchema } from '@documenso/lib/types/field-meta';
import { nanoid } from '@documenso/lib/universal/id';
import { zodResolver } from '@hookform/resolvers/zod';
import type { Field } from '@prisma/client';
import { FieldType } from '@prisma/client';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useFieldArray, useForm } from 'react-hook-form';
import { z } from 'zod';
export const ZLocalFieldSchema = z.object({
// This is the actual ID of the field if created.
@@ -67,10 +65,7 @@ type UseEditorFieldsResponse = {
resetForm: (fields?: Field[]) => void;
};
export const useEditorFields = ({
envelope,
handleFieldsUpdate,
}: EditorFieldsProps): UseEditorFieldsResponse => {
export const useEditorFields = ({ envelope, handleFieldsUpdate }: EditorFieldsProps): UseEditorFieldsResponse => {
const [selectedFieldFormId, setSelectedFieldFormId] = useState<string | null>(null);
const [selectedRecipientId, setSelectedRecipientId] = useState<number | null>(null);
@@ -123,9 +118,7 @@ export const useEditorFields = ({
}
const foundField = localFields.find((field) => field.formId === formId);
const recipient = envelope.recipients.find(
(recipient) => recipient.id === foundField?.recipientId,
);
const recipient = envelope.recipients.find((recipient) => recipient.id === foundField?.recipientId);
if (recipient) {
setSelectedRecipient(recipient.id);
@@ -319,9 +312,7 @@ export const useEditorFields = ({
};
};
const restrictFieldPosValues = (
field: Pick<TLocalField, 'positionX' | 'positionY' | 'width' | 'height'>,
) => {
const restrictFieldPosValues = (field: Pick<TLocalField, 'positionX' | 'positionY' | 'width' | 'height'>) => {
return {
positionX: Math.max(0, Math.min(100, field.positionX)),
positionY: Math.max(0, Math.min(100, field.positionY)),
@@ -1,19 +1,14 @@
import { useId } from 'react';
import { ZRecipientActionAuthTypesSchema, ZRecipientAuthOptionsSchema } from '@documenso/lib/types/document-auth';
import type { TEditorEnvelope } from '@documenso/lib/types/envelope-editor';
import { ZRecipientEmailSchema } from '@documenso/lib/types/recipient';
import { zodResolver } from '@hookform/resolvers/zod';
import { DocumentSigningOrder, RecipientRole } from '@prisma/client';
import { useId } from 'react';
import type { UseFormReturn } from 'react-hook-form';
import { useForm } from 'react-hook-form';
import { prop, sortBy } from 'remeda';
import { z } from 'zod';
import {
ZRecipientActionAuthTypesSchema,
ZRecipientAuthOptionsSchema,
} from '@documenso/lib/types/document-auth';
import type { TEditorEnvelope } from '@documenso/lib/types/envelope-editor';
import { ZRecipientEmailSchema } from '@documenso/lib/types/recipient';
const LocalRecipientSchema = z.object({
formId: z.string().min(1),
id: z.number().optional(),
@@ -48,9 +43,7 @@ type UseEditorRecipientsResponse = {
resetForm: (options?: ResetFormOptions) => void;
};
export const useEditorRecipients = ({
envelope,
}: EditorRecipientsProps): UseEditorRecipientsResponse => {
export const useEditorRecipients = ({ envelope }: EditorRecipientsProps): UseEditorRecipientsResponse => {
const initialId = useId();
const generateDefaultValues = (options?: ResetFormOptions) => {
@@ -83,8 +76,7 @@ export const useEditorRecipients = ({
return {
signers,
signingOrder: documentMeta?.signingOrder ?? envelope.documentMeta.signingOrder,
allowDictateNextSigner:
documentMeta?.allowDictateNextSigner ?? envelope.documentMeta.allowDictateNextSigner,
allowDictateNextSigner: documentMeta?.allowDictateNextSigner ?? envelope.documentMeta.allowDictateNextSigner,
};
};
@@ -1,6 +1,5 @@
import { useCallback, useEffect, useState } from 'react';
import { getBoundingClientRect } from '@documenso/lib/client-only/get-bounding-client-rect';
import { useCallback, useEffect, useState } from 'react';
export const useElementBounds = (elementOrSelector: HTMLElement | string, withScroll = false) => {
const [bounds, setBounds] = useState({
@@ -1,5 +1,5 @@
/* eslint-disable @typescript-eslint/consistent-type-assertions */
import { RefObject, useEffect, useState } from 'react';
import { useEffect, useState } from 'react';
/**
* Calculate the width and height of a text element.
@@ -15,7 +15,9 @@ export function useEnvelopeAutosave<T>(saveFn: (data: T) => Promise<void>, delay
// A debounce or promise means something is pending
setIsPending(true);
if (timeoutRef.current) clearTimeout(timeoutRef.current);
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
// eslint-disable-next-line @typescript-eslint/no-misused-promises
timeoutRef.current = setTimeout(async () => {
@@ -1,16 +1,9 @@
import { getBoundingClientRect } from '@documenso/lib/client-only/get-bounding-client-rect';
import { PDF_VIEWER_CONTENT_SELECTOR, PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
import type { Field } from '@prisma/client';
import { useCallback, useEffect, useState } from 'react';
import type { Field } from '@prisma/client';
import { getBoundingClientRect } from '@documenso/lib/client-only/get-bounding-client-rect';
import {
PDF_VIEWER_CONTENT_SELECTOR,
PDF_VIEWER_PAGE_SELECTOR,
} from '@documenso/lib/constants/pdf-viewer';
export const useFieldPageCoords = (
field: Pick<Field, 'positionX' | 'positionY' | 'width' | 'height' | 'page'>,
) => {
export const useFieldPageCoords = (field: Pick<Field, 'positionX' | 'positionY' | 'width' | 'height' | 'page'>) => {
const [coords, setCoords] = useState({
x: 0,
y: 0,
@@ -19,9 +12,7 @@ export const useFieldPageCoords = (
});
const calculateCoords = useCallback(() => {
const $page = document.querySelector<HTMLElement>(
`${PDF_VIEWER_PAGE_SELECTOR}[data-page-number="${field.page}"]`,
);
const $page = document.querySelector<HTMLElement>(`${PDF_VIEWER_PAGE_SELECTOR}[data-page-number="${field.page}"]`);
if (!$page) {
return;
@@ -1,10 +1,6 @@
import { PDF_VIEWER_CONTENT_SELECTOR, PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
import { useEffect, useState } from 'react';
import {
PDF_VIEWER_CONTENT_SELECTOR,
PDF_VIEWER_PAGE_SELECTOR,
} from '@documenso/lib/constants/pdf-viewer';
/**
* Returns whether the PDF page element for the given page number is currently
* present in the DOM. With virtual list rendering only pages near the viewport
@@ -1,8 +1,7 @@
import Konva from 'konva';
import { useEffect, useMemo, useRef } from 'react';
import Konva from 'konva';
import { type PageRenderData } from '../providers/envelope-render-provider';
import type { PageRenderData } from '../providers/envelope-render-provider';
type RenderFunction = (props: { stage: Konva.Stage; pageLayer: Konva.Layer }) => void;
@@ -66,7 +65,7 @@ export const usePageRenderer = (renderFunction: RenderFunction, pageData: PageRe
pageLayer: pageLayer.current,
});
void document.fonts.ready.then(function () {
void document.fonts.ready.then(() => {
pageLayer.current?.batchDraw();
});
@@ -1,5 +1,5 @@
import * as React from 'react';
import type { Dispatch, SetStateAction } from 'react';
import * as React from 'react';
function dispatchStorageEvent(key: string, newValue: string | null) {
window.dispatchEvent(new StorageEvent('storage', { key, newValue }));
@@ -25,20 +25,13 @@ const useSessionStorageSubscribe = (callback: (event: StorageEvent) => void) =>
return () => window.removeEventListener('storage', callback);
};
export function useSessionStorage<T>(
key: string,
initialValue: T,
): [T, Dispatch<SetStateAction<T>>] {
export function useSessionStorage<T>(key: string, initialValue: T): [T, Dispatch<SetStateAction<T>>] {
const serializedInitialValue = JSON.stringify(initialValue);
const getSnapshot = () => getSessionStorageItem(key);
const getServerSnapshot = () => serializedInitialValue;
const store = React.useSyncExternalStore(
useSessionStorageSubscribe,
getSnapshot,
getServerSnapshot,
);
const store = React.useSyncExternalStore(useSessionStorageSubscribe, getSnapshot, getServerSnapshot);
const setState: Dispatch<SetStateAction<T>> = React.useCallback(
(v) => {
@@ -1,9 +1,3 @@
import React, { createContext, useCallback, useContext, useMemo, useRef, useState } from 'react';
import { useLingui } from '@lingui/react/macro';
import { EnvelopeType, Prisma, ReadStatus, SendStatus, SigningStatus } from '@prisma/client';
import { useSearchParams } from 'react-router';
import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc';
import {
DEFAULT_EDITOR_CONFIG,
@@ -17,11 +11,16 @@ import type { TUpdateEnvelopeRequest } from '@documenso/trpc/server/envelope-rou
import type { TRecipientColor } from '@documenso/ui/lib/recipient-colors';
import { getRecipientColor } from '@documenso/ui/lib/recipient-colors';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { useLingui } from '@lingui/react/macro';
import { EnvelopeType, Prisma, ReadStatus, SendStatus, SigningStatus } from '@prisma/client';
import type React from 'react';
import { createContext, useCallback, useContext, useMemo, useRef, useState } from 'react';
import { useSearchParams } from 'react-router';
import type { TDocumentEmailSettings } from '../../types/document-email';
import { formatDocumentsPath, formatTemplatesPath } from '../../utils/teams';
import { useEditorFields } from '../hooks/use-editor-fields';
import type { TLocalField } from '../hooks/use-editor-fields';
import { useEditorFields } from '../hooks/use-editor-fields';
import { useEditorRecipients } from '../hooks/use-editor-recipients';
import { useEnvelopeAutosave } from '../hooks/use-envelope-autosave';
@@ -176,16 +175,12 @@ export const EnvelopeEditorProvider = ({
setEnvelope((prev) => ({
...prev,
recipients,
fields: prev.fields.filter((field) =>
recipients.some((recipient) => recipient.id === field.recipientId),
),
fields: prev.fields.filter((field) => recipients.some((recipient) => recipient.id === field.recipientId)),
}));
// Reset the local fields to ensure deleted recipient fields are removed.
editorFields.resetForm(
envelope.fields.filter((field) =>
recipients.some((recipient) => recipient.id === field.recipientId),
),
envelope.fields.filter((field) => recipients.some((recipient) => recipient.id === field.recipientId)),
);
setAutosaveError(false);
@@ -203,9 +198,7 @@ export const EnvelopeEditorProvider = ({
}
}, 1000);
const setRecipientsAsync = async (
localRecipients: TSetEnvelopeRecipientsRequest['recipients'],
) => {
const setRecipientsAsync = async (localRecipients: TSetEnvelopeRecipientsRequest['recipients']) => {
setRecipientsDebounced(localRecipients);
await flushSetRecipients();
};
@@ -346,8 +339,7 @@ export const EnvelopeEditorProvider = ({
};
const getRecipientColorKey = useCallback(
(recipientId: number) =>
getRecipientColor(envelope.recipients.findIndex((r) => r.id === recipientId)),
(recipientId: number) => getRecipientColor(envelope.recipients.findIndex((r) => r.id === recipientId)),
[envelope.recipients],
);
@@ -536,10 +528,7 @@ const mapLocalRecipientsToRecipients = ({
documentDeletedAt: foundRecipient?.documentDeletedAt || null,
expired: foundRecipient?.expired || null,
signedAt: foundRecipient?.signedAt || null,
authOptions:
recipient.actionAuth.length > 0
? { actionAuth: recipient.actionAuth, accessAuth: [] }
: null,
authOptions: recipient.actionAuth.length > 0 ? { actionAuth: recipient.actionAuth, accessAuth: [] } : null,
signingOrder: recipient.signingOrder ?? null,
rejectionReason: foundRecipient?.rejectionReason || null,
role: recipient.role,
@@ -1,12 +1,10 @@
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
import React from 'react';
import { type Field, type Recipient } from '@prisma/client';
import type { DocumentDataVersion } from '@documenso/lib/types/document';
import { getDocumentDataUrl } from '@documenso/lib/utils/envelope-download';
import type { TRecipientColor } from '@documenso/ui/lib/recipient-colors';
import { getRecipientColor } from '@documenso/ui/lib/recipient-colors';
import type { Field, Recipient } from '@prisma/client';
import type React from 'react';
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
import type { TEnvelope } from '../../types/envelope';
import type { FieldRenderMode } from '../../universal/field-renderer/render-field';
@@ -215,10 +213,7 @@ export const EnvelopeRenderProvider = ({
}
}, [currentItem, envelopeItems]);
const recipientIds = useMemo(
() => recipients.map((recipient) => recipient.id).sort(),
[recipients],
);
const recipientIds = useMemo(() => recipients.map((recipient) => recipient.id).sort(), [recipients]);
const getRecipientColorKey = useCallback(
(recipientId: number) => getRecipientColor(recipientIds.findIndex((id) => id === recipientId)),
@@ -1,11 +1,7 @@
import type { I18n, Messages } from '@lingui/core';
import { setupI18n } from '@lingui/core';
import {
APP_I18N_OPTIONS,
SUPPORTED_LANGUAGE_CODES,
isValidLanguageCode,
} from '../../constants/i18n';
import { APP_I18N_OPTIONS, isValidLanguageCode, SUPPORTED_LANGUAGE_CODES } from '../../constants/i18n';
import { env } from '../../utils/env';
import { remember } from '../../utils/remember';
@@ -1,7 +1,6 @@
import { useState } from 'react';
import { type Messages, setupI18n } from '@lingui/core';
import { I18nProvider } from '@lingui/react';
import { useState } from 'react';
import type { I18nLocaleData } from '../../constants/i18n';
@@ -1,7 +1,6 @@
import { createContext, useContext } from 'react';
import React from 'react';
import type { OrganisationSession } from '@documenso/trpc/server/organisation-router/get-organisation-session.types';
import type React from 'react';
import { createContext, useContext } from 'react';
type OrganisationProviderValue = OrganisationSession;
@@ -27,7 +26,5 @@ export const useOptionalCurrentOrganisation = () => {
};
export const OrganisationProvider = ({ children, organisation }: OrganisationProviderProps) => {
return (
<OrganisationContext.Provider value={organisation}>{children}</OrganisationContext.Provider>
);
return <OrganisationContext.Provider value={organisation}>{children}</OrganisationContext.Provider>;
};
@@ -1,13 +1,11 @@
import { createContext, useCallback, useContext, useEffect, useState } from 'react';
import React from 'react';
import type { Session } from '@prisma/client';
import { useLocation } from 'react-router';
import { authClient } from '@documenso/auth/client';
import type { SessionUser } from '@documenso/auth/server/lib/session/session';
import { trpc } from '@documenso/trpc/client';
import type { TGetOrganisationSessionResponse } from '@documenso/trpc/server/organisation-router/get-organisation-session.types';
import type { Session } from '@prisma/client';
import type React from 'react';
import { createContext, useCallback, useContext, useEffect, useState } from 'react';
import { useLocation } from 'react-router';
import { SKIP_QUERY_BATCH_META } from '../../constants/trpc';
@@ -74,8 +72,7 @@ export const SessionProvider = ({ children, initialSession }: SessionProviderPro
.catch((e) => {
const errorMessage = typeof e.message === 'string' ? e.message.toLowerCase() : '';
const isNetworkError =
errorMessage.includes('networkerror') || errorMessage.includes('failed to fetch');
const isNetworkError = errorMessage.includes('networkerror') || errorMessage.includes('failed to fetch');
// If the error is a transient network/abort error (e.g. page refresh while
// fetch was in-flight), return null to signal we should skip the state update.
+2 -11
View File
@@ -1,11 +1,5 @@
import type { Recipient } from '@prisma/client';
import {
DocumentDistributionMethod,
ReadStatus,
RecipientRole,
SendStatus,
SigningStatus,
} from '@prisma/client';
import { DocumentDistributionMethod, ReadStatus, RecipientRole, SendStatus, SigningStatus } from '@prisma/client';
type RecipientForType = Pick<Recipient, 'role' | 'signingStatus' | 'readStatus' | 'sendStatus'>;
@@ -29,10 +23,7 @@ export const getRecipientType = (
return RecipientStatusType.REJECTED;
}
if (
recipient.readStatus === ReadStatus.OPENED &&
recipient.signingStatus === SigningStatus.NOT_SIGNED
) {
if (recipient.readStatus === ReadStatus.OPENED && recipient.signingStatus === SigningStatus.NOT_SIGNED) {
return RecipientStatusType.OPENED;
}
+6 -12
View File
@@ -1,10 +1,8 @@
import { env } from '@documenso/lib/utils/env';
export const APP_DOCUMENT_UPLOAD_SIZE_LIMIT =
Number(env('NEXT_PUBLIC_DOCUMENT_SIZE_UPLOAD_LIMIT')) || 50;
export const APP_DOCUMENT_UPLOAD_SIZE_LIMIT = Number(env('NEXT_PUBLIC_DOCUMENT_SIZE_UPLOAD_LIMIT')) || 50;
export const NEXT_PUBLIC_WEBAPP_URL = () =>
env('NEXT_PUBLIC_WEBAPP_URL') ?? 'http://localhost:3000';
export const NEXT_PUBLIC_WEBAPP_URL = () => env('NEXT_PUBLIC_WEBAPP_URL') ?? 'http://localhost:3000';
export const NEXT_PUBLIC_SIGNING_CONTACT_INFO = () =>
env('NEXT_PUBLIC_SIGNING_CONTACT_INFO') ?? NEXT_PUBLIC_WEBAPP_URL();
@@ -22,11 +20,9 @@ export const API_V2_URL = '/api/v2';
export const SUPPORT_EMAIL = env('NEXT_PUBLIC_SUPPORT_EMAIL') ?? 'support@documenso.com';
export const USE_INTERNAL_URL_BROWSERLESS = () =>
env('NEXT_PUBLIC_USE_INTERNAL_URL_BROWSERLESS') === 'true';
export const USE_INTERNAL_URL_BROWSERLESS = () => env('NEXT_PUBLIC_USE_INTERNAL_URL_BROWSERLESS') === 'true';
export const IS_AI_FEATURES_CONFIGURED = () =>
!!env('GOOGLE_VERTEX_PROJECT_ID') && !!env('GOOGLE_VERTEX_API_KEY');
export const IS_AI_FEATURES_CONFIGURED = () => !!env('GOOGLE_VERTEX_PROJECT_ID') && !!env('GOOGLE_VERTEX_API_KEY');
/**
* Temporary flag to toggle between Playwright-based and Konva-based PDF generation
@@ -34,8 +30,6 @@ export const IS_AI_FEATURES_CONFIGURED = () =>
*
* @deprecated This is a temporary flag and will be removed once Konva-based generation is stable.
*/
export const NEXT_PRIVATE_USE_PLAYWRIGHT_PDF = () =>
env('NEXT_PRIVATE_USE_PLAYWRIGHT_PDF') === 'true';
export const NEXT_PRIVATE_USE_PLAYWRIGHT_PDF = () => env('NEXT_PRIVATE_USE_PLAYWRIGHT_PDF') === 'true';
export const NEXT_PRIVATE_SIGNING_TIMESTAMP_AUTHORITY = () =>
env('NEXT_PRIVATE_SIGNING_TIMESTAMP_AUTHORITY');
export const NEXT_PRIVATE_SIGNING_TIMESTAMP_AUTHORITY = () => env('NEXT_PRIVATE_SIGNING_TIMESTAMP_AUTHORITY');
+20 -3
View File
@@ -34,9 +34,7 @@ export const IS_MICROSOFT_SSO_ENABLED = Boolean(
);
export const IS_OIDC_SSO_ENABLED = Boolean(
env('NEXT_PRIVATE_OIDC_WELL_KNOWN') &&
env('NEXT_PRIVATE_OIDC_CLIENT_ID') &&
env('NEXT_PRIVATE_OIDC_CLIENT_SECRET'),
env('NEXT_PRIVATE_OIDC_WELL_KNOWN') && env('NEXT_PRIVATE_OIDC_CLIENT_ID') && env('NEXT_PRIVATE_OIDC_CLIENT_SECRET'),
);
export const OIDC_PROVIDER_LABEL = env('NEXT_PRIVATE_OIDC_PROVIDER_LABEL');
@@ -121,3 +119,22 @@ export const isEmailDomainAllowedForSignup = (email: string): boolean => {
return allowedDomains.includes(emailDomain);
};
/**
* Check if signup is enabled for the given provider.
* The master switch takes precedence over the per-provider flags.
*/
export const isSignupEnabledForProvider = (provider: 'email' | 'google' | 'microsoft' | 'oidc'): boolean => {
if (env('NEXT_PUBLIC_DISABLE_SIGNUP') === 'true') {
return false;
}
const flagMap = {
email: 'NEXT_PUBLIC_DISABLE_EMAIL_PASSWORD_SIGNUP',
google: 'NEXT_PUBLIC_DISABLE_GOOGLE_SIGNUP',
microsoft: 'NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNUP',
oidc: 'NEXT_PUBLIC_DISABLE_OIDC_SIGNUP',
} as const;
return env(flagMap[provider]) !== 'true';
};
+11
View File
@@ -0,0 +1,11 @@
/**
* Maximum length (in characters) of the user-supplied custom CSS for branding.
* Bound enforced at the TRPC request boundary on both the organisation and
* team settings update routes. The sanitiser is run after this check; this
* limit is purely a request-size guard.
*
* 256 KB — generous enough for hand-written branding CSS and the occasional
* compiled-from-Tailwind-or-similar paste, while still keeping a request
* cap so a malicious or runaway payload can't exhaust PostCSS/server memory.
*/
export const BRANDING_CSS_MAX_LENGTH = 256 * 1024;
@@ -1,8 +1,7 @@
import { DocumentVisibility } from '@documenso/lib/types/document-visibility';
import type { MessageDescriptor } from '@lingui/core';
import { msg } from '@lingui/core/macro';
import { DocumentVisibility } from '@documenso/lib/types/document-visibility';
import type { TDocumentVisibility } from '../types/document-visibility';
type DocumentVisibilityTypeData = {
+3 -4
View File
@@ -1,11 +1,10 @@
import type { MessageDescriptor } from '@lingui/core';
import { msg } from '@lingui/core/macro';
import { DocumentDistributionMethod, DocumentStatus } from '@prisma/client';
/**
* Workaround for E2E tests to not import `msg`.
*/
import { DocumentSignatureType } from '@documenso/lib/utils/teams';
import type { MessageDescriptor } from '@lingui/core';
import { msg } from '@lingui/core/macro';
import { DocumentDistributionMethod, DocumentStatus } from '@prisma/client';
export { DocumentSignatureType };
@@ -19,10 +19,7 @@ export const ZEnvelopeExpirationPeriod = z.union([
export type TEnvelopeExpirationPeriod = z.infer<typeof ZEnvelopeExpirationPeriod>;
export type TEnvelopeExpirationDurationPeriod = z.infer<typeof ZEnvelopeExpirationDurationPeriod>;
const UNIT_TO_LUXON_KEY: Record<
TEnvelopeExpirationDurationPeriod['unit'],
keyof DurationLikeObject
> = {
const UNIT_TO_LUXON_KEY: Record<TEnvelopeExpirationDurationPeriod['unit'], keyof DurationLikeObject> = {
day: 'days',
week: 'weeks',
month: 'months',
@@ -34,9 +31,7 @@ export const DEFAULT_ENVELOPE_EXPIRATION_PERIOD: TEnvelopeExpirationDurationPeri
amount: 3,
};
export const getEnvelopeExpirationDuration = (
period: TEnvelopeExpirationDurationPeriod,
): Duration => {
export const getEnvelopeExpirationDuration = (period: TEnvelopeExpirationDurationPeriod): Duration => {
return Duration.fromObject({ [UNIT_TO_LUXON_KEY[period.unit]]: period.amount });
};
+7 -13
View File
@@ -11,10 +11,7 @@ export const ZEnvelopeReminderDisabledPeriod = z.object({
disabled: z.literal(true),
});
export const ZEnvelopeReminderPeriod = z.union([
ZEnvelopeReminderDurationPeriod,
ZEnvelopeReminderDisabledPeriod,
]);
export const ZEnvelopeReminderPeriod = z.union([ZEnvelopeReminderDurationPeriod, ZEnvelopeReminderDisabledPeriod]);
export type TEnvelopeReminderPeriod = z.infer<typeof ZEnvelopeReminderPeriod>;
export type TEnvelopeReminderDurationPeriod = z.infer<typeof ZEnvelopeReminderDurationPeriod>;
@@ -39,12 +36,11 @@ export const DEFAULT_ENVELOPE_REMINDER_SETTINGS: TEnvelopeReminderSettings = {
*/
export const MAX_REMINDER_WINDOW_DAYS = 30;
const UNIT_TO_LUXON_KEY: Record<TEnvelopeReminderDurationPeriod['unit'], keyof DurationLikeObject> =
{
day: 'days',
week: 'weeks',
month: 'months',
};
const UNIT_TO_LUXON_KEY: Record<TEnvelopeReminderDurationPeriod['unit'], keyof DurationLikeObject> = {
day: 'days',
week: 'weeks',
month: 'months',
};
export const getEnvelopeReminderDuration = (period: TEnvelopeReminderDurationPeriod): Duration => {
return Duration.fromObject({ [UNIT_TO_LUXON_KEY[period.unit]]: period.amount });
@@ -75,9 +71,7 @@ export const resolveNextReminderAt = (options: {
return null;
}
const maxReminderAt = new Date(
sentAt.getTime() + Duration.fromObject({ days: MAX_REMINDER_WINDOW_DAYS }).toMillis(),
);
const maxReminderAt = new Date(sentAt.getTime() + Duration.fromObject({ days: MAX_REMINDER_WINDOW_DAYS }).toMillis());
let candidate: Date;
+1 -13
View File
@@ -1,18 +1,6 @@
import { z } from 'zod';
export const SUPPORTED_LANGUAGE_CODES = [
'de',
'en',
'fr',
'es',
'it',
'nl',
'pl',
'pt-BR',
'ja',
'ko',
'zh',
] as const;
export const SUPPORTED_LANGUAGE_CODES = ['de', 'en', 'fr', 'es', 'it', 'nl', 'pl', 'pt-BR', 'ja', 'ko', 'zh'] as const;
export type SupportedLanguageCodes = (typeof SUPPORTED_LANGUAGE_CODES)[number];
@@ -6,19 +6,13 @@ import type { MessageDescriptor } from '@lingui/core';
import { msg } from '@lingui/core/macro';
import type { OrganisationMemberRole } from '@prisma/client';
export const ORGANISATION_MEMBER_ROLE_MAP: Record<
keyof typeof OrganisationMemberRole,
MessageDescriptor
> = {
export const ORGANISATION_MEMBER_ROLE_MAP: Record<keyof typeof OrganisationMemberRole, MessageDescriptor> = {
ADMIN: msg`Admin`,
MANAGER: msg`Manager`,
MEMBER: msg`Member`,
};
export const EXTENDED_ORGANISATION_MEMBER_ROLE_MAP: Record<
keyof typeof OrganisationMemberRole,
MessageDescriptor
> = {
export const EXTENDED_ORGANISATION_MEMBER_ROLE_MAP: Record<keyof typeof OrganisationMemberRole, MessageDescriptor> = {
ADMIN: msg`Organisation Admin`,
MANAGER: msg`Organisation Manager`,
MEMBER: msg`Organisation Member`,
+2 -2
View File
@@ -1,7 +1,7 @@
import { OrganisationGroupType, OrganisationMemberRole } from '@prisma/client';
export const ORGANISATION_URL_ROOT_REGEX = new RegExp('^/t/[^/]+/?$');
export const ORGANISATION_URL_REGEX = new RegExp('^/t/[^/]+');
export const ORGANISATION_URL_ROOT_REGEX = /^\/t\/[^/]+\/?$/;
export const ORGANISATION_URL_REGEX = /^\/t\/[^/]+/;
export const ORGANISATION_INTERNAL_GROUPS: {
organisationRole: OrganisationMemberRole;
+1 -3
View File
@@ -5,9 +5,7 @@ export const PDF_VIEWER_PAGE_CLASSNAME = 'react-pdf__Page z-0';
export const PDF_VIEWER_CONTENT_SELECTOR = '[data-pdf-content]';
export const getPdfPagesCount = () => {
const pageCountAttr = document
.querySelector(PDF_VIEWER_CONTENT_SELECTOR)
?.getAttribute('data-page-count');
const pageCountAttr = document.querySelector(PDF_VIEWER_CONTENT_SELECTOR)?.getAttribute('data-page-count');
const totalPages = Number(pageCountAttr);
+5 -6
View File
@@ -8,9 +8,8 @@ export const TEAM_MEMBER_ROLE_MAP: Record<keyof typeof TeamMemberRole, MessageDe
MEMBER: msg`Member`,
};
export const EXTENDED_TEAM_MEMBER_ROLE_MAP: Record<keyof typeof TeamMemberRole, MessageDescriptor> =
{
ADMIN: msg`Team Admin`,
MANAGER: msg`Team Manager`,
MEMBER: msg`Team Member`,
};
export const EXTENDED_TEAM_MEMBER_ROLE_MAP: Record<keyof typeof TeamMemberRole, MessageDescriptor> = {
ADMIN: msg`Team Admin`,
MANAGER: msg`Team Manager`,
MEMBER: msg`Team Member`,
};
+3 -7
View File
@@ -1,7 +1,7 @@
import { DocumentVisibility, OrganisationGroupType, TeamMemberRole } from '@prisma/client';
export const TEAM_URL_ROOT_REGEX = new RegExp('^/t/[^/]+/?$');
export const TEAM_URL_REGEX = new RegExp('^/t/[^/]+');
export const TEAM_URL_ROOT_REGEX = /^\/t\/[^/]+\/?$/;
export const TEAM_URL_REGEX = /^\/t\/[^/]+/;
export const LOWEST_TEAM_ROLE = TeamMemberRole.MEMBER;
@@ -34,11 +34,7 @@ export const TEAM_MEMBER_ROLE_PERMISSIONS_MAP = {
} satisfies Record<string, TeamMemberRole[]>;
export const TEAM_DOCUMENT_VISIBILITY_MAP = {
[TeamMemberRole.ADMIN]: [
DocumentVisibility.ADMIN,
DocumentVisibility.MANAGER_AND_ABOVE,
DocumentVisibility.EVERYONE,
],
[TeamMemberRole.ADMIN]: [DocumentVisibility.ADMIN, DocumentVisibility.MANAGER_AND_ABOVE, DocumentVisibility.EVERYONE],
[TeamMemberRole.MANAGER]: [DocumentVisibility.MANAGER_AND_ABOVE, DocumentVisibility.EVERYONE],
[TeamMemberRole.MEMBER]: [DocumentVisibility.EVERYONE],
} satisfies Record<TeamMemberRole, DocumentVisibility[]>;
+58
View File
@@ -0,0 +1,58 @@
import type { TCssVarsSchema } from '../types/css-vars';
/**
* !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
*
* KEEP THIS FILE IN SYNC WITH `packages/ui/styles/theme.css`.
*
* These are the light-mode default values for the CSS custom properties
* defined under `:root` in the theme stylesheet, exposed here as hex strings
* so they can be used as defaults for colour-picker UI components and other
* places that don't render through CSS variables.
*
* If you change a value in `theme.css`, update it here too. There is NO
* automated check linking the two files; they have drifted historically
* and will drift again unless you update both.
*
* Computed via `colord({ h, s, l }).toHex()` — see the inline HSL comments
* for the source-of-truth values from `theme.css`.
*
* !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
*/
export const DEFAULT_BRAND_COLORS = {
background: '#ffffff', // 0 0% 100%
foreground: '#0f172a', // 222.2 47.4% 11.2%
muted: '#f1f5f9', // 210 40% 96.1%
mutedForeground: '#64748b', // 215.4 16.3% 46.9%
popover: '#ffffff', // 0 0% 100%
popoverForeground: '#0f172a', // 222.2 47.4% 11.2%
card: '#ffffff', // 0 0% 100%
cardBorder: '#e2e8f0', // 214.3 31.8% 91.4%
cardForeground: '#0f172a', // 222.2 47.4% 11.2%
fieldCard: '#e2f8d3', // 95 74% 90%
fieldCardBorder: '#a2e771', // 95.08 71.08% 67.45%
fieldCardForeground: '#0f172a', // 222.2 47.4% 11.2%
widget: '#f7f7f7', // 0 0% 97%
widgetForeground: '#f2f2f2', // 0 0% 95%
border: '#e2e8f0', // 214.3 31.8% 91.4%
input: '#e2e8f0', // 214.3 31.8% 91.4%
primary: '#a2e771', // 95.08 71.08% 67.45%
primaryForeground: '#162c07', // 95.08 71.08% 10%
secondary: '#f1f5f9', // 210 40% 96.1%
secondaryForeground: '#0f172a', // 222.2 47.4% 11.2%
accent: '#f1f5f9', // 210 40% 96.1%
accentForeground: '#0f172a', // 222.2 47.4% 11.2%
destructive: '#ff0000', // 0 100% 50%
destructiveForeground: '#f8fafc', // 210 40% 98%
ring: '#a2e771', // 95.08 71.08% 67.45%
warning: '#e1cb05', // 54 96% 45%
envelopeEditorBackground: '#f8fafc', //210 40% 98.04%
// `cardBorderTint` is intentionally excluded from the colour-picker UI:
// unlike the rest of these tokens it is consumed via `rgb(var(--token))`
// (not `hsl(...)`) and stored as raw RGB triplets in `theme.css`. It does
// not flow through `toNativeCssVars` and is not user-customisable from the
// branding form. `radius` is a length, not a colour, so it lives in
// `DEFAULT_BRAND_RADIUS` below.
} as const satisfies Record<keyof Omit<TCssVarsSchema, 'radius' | 'cardBorderTint'>, string>;
export const DEFAULT_BRAND_RADIUS = '0.5rem';
+45 -47
View File
@@ -5,53 +5,52 @@ import { z } from 'zod';
* Generic application error codes.
*/
export enum AppErrorCode {
'ALREADY_EXISTS' = 'ALREADY_EXISTS',
'EXPIRED_CODE' = 'EXPIRED_CODE',
'INVALID_BODY' = 'INVALID_BODY',
'INVALID_REQUEST' = 'INVALID_REQUEST',
'RECIPIENT_EXPIRED' = 'RECIPIENT_EXPIRED',
'LIMIT_EXCEEDED' = 'LIMIT_EXCEEDED',
'NOT_FOUND' = 'NOT_FOUND',
'NOT_IMPLEMENTED' = 'NOT_IMPLEMENTED',
'NOT_SETUP' = 'NOT_SETUP',
'INVALID_CAPTCHA' = 'INVALID_CAPTCHA',
'UNAUTHORIZED' = 'UNAUTHORIZED',
'FORBIDDEN' = 'FORBIDDEN',
'UNKNOWN_ERROR' = 'UNKNOWN_ERROR',
'RETRY_EXCEPTION' = 'RETRY_EXCEPTION',
'SCHEMA_FAILED' = 'SCHEMA_FAILED',
'TOO_MANY_REQUESTS' = 'TOO_MANY_REQUESTS',
'TWO_FACTOR_AUTH_FAILED' = 'TWO_FACTOR_AUTH_FAILED',
'WEBHOOK_INVALID_REQUEST' = 'WEBHOOK_INVALID_REQUEST',
'ENVELOPE_DRAFT' = 'ENVELOPE_DRAFT',
'ENVELOPE_COMPLETED' = 'ENVELOPE_COMPLETED',
'ENVELOPE_REJECTED' = 'ENVELOPE_REJECTED',
'ENVELOPE_LEGACY' = 'ENVELOPE_LEGACY',
ALREADY_EXISTS = 'ALREADY_EXISTS',
EXPIRED_CODE = 'EXPIRED_CODE',
INVALID_BODY = 'INVALID_BODY',
INVALID_REQUEST = 'INVALID_REQUEST',
RECIPIENT_EXPIRED = 'RECIPIENT_EXPIRED',
LIMIT_EXCEEDED = 'LIMIT_EXCEEDED',
NOT_FOUND = 'NOT_FOUND',
NOT_IMPLEMENTED = 'NOT_IMPLEMENTED',
NOT_SETUP = 'NOT_SETUP',
INVALID_CAPTCHA = 'INVALID_CAPTCHA',
UNAUTHORIZED = 'UNAUTHORIZED',
FORBIDDEN = 'FORBIDDEN',
UNKNOWN_ERROR = 'UNKNOWN_ERROR',
RETRY_EXCEPTION = 'RETRY_EXCEPTION',
SCHEMA_FAILED = 'SCHEMA_FAILED',
TOO_MANY_REQUESTS = 'TOO_MANY_REQUESTS',
TWO_FACTOR_AUTH_FAILED = 'TWO_FACTOR_AUTH_FAILED',
WEBHOOK_INVALID_REQUEST = 'WEBHOOK_INVALID_REQUEST',
ENVELOPE_DRAFT = 'ENVELOPE_DRAFT',
ENVELOPE_COMPLETED = 'ENVELOPE_COMPLETED',
ENVELOPE_REJECTED = 'ENVELOPE_REJECTED',
ENVELOPE_LEGACY = 'ENVELOPE_LEGACY',
}
export const genericErrorCodeToTrpcErrorCodeMap: Record<string, { code: string; status: number }> =
{
[AppErrorCode.ALREADY_EXISTS]: { code: 'BAD_REQUEST', status: 400 },
[AppErrorCode.RECIPIENT_EXPIRED]: { code: 'BAD_REQUEST', status: 400 },
[AppErrorCode.EXPIRED_CODE]: { code: 'BAD_REQUEST', status: 400 },
[AppErrorCode.INVALID_BODY]: { code: 'BAD_REQUEST', status: 400 },
[AppErrorCode.INVALID_REQUEST]: { code: 'BAD_REQUEST', status: 400 },
[AppErrorCode.INVALID_CAPTCHA]: { code: 'BAD_REQUEST', status: 400 },
[AppErrorCode.NOT_FOUND]: { code: 'NOT_FOUND', status: 404 },
[AppErrorCode.NOT_IMPLEMENTED]: { code: 'INTERNAL_SERVER_ERROR', status: 501 },
[AppErrorCode.NOT_SETUP]: { code: 'BAD_REQUEST', status: 400 },
[AppErrorCode.UNAUTHORIZED]: { code: 'UNAUTHORIZED', status: 401 },
[AppErrorCode.FORBIDDEN]: { code: 'FORBIDDEN', status: 403 },
[AppErrorCode.UNKNOWN_ERROR]: { code: 'INTERNAL_SERVER_ERROR', status: 500 },
[AppErrorCode.RETRY_EXCEPTION]: { code: 'INTERNAL_SERVER_ERROR', status: 500 },
[AppErrorCode.SCHEMA_FAILED]: { code: 'INTERNAL_SERVER_ERROR', status: 500 },
[AppErrorCode.TOO_MANY_REQUESTS]: { code: 'TOO_MANY_REQUESTS', status: 429 },
[AppErrorCode.TWO_FACTOR_AUTH_FAILED]: { code: 'UNAUTHORIZED', status: 401 },
[AppErrorCode.ENVELOPE_DRAFT]: { code: 'BAD_REQUEST', status: 400 },
[AppErrorCode.ENVELOPE_COMPLETED]: { code: 'BAD_REQUEST', status: 400 },
[AppErrorCode.ENVELOPE_REJECTED]: { code: 'BAD_REQUEST', status: 400 },
[AppErrorCode.ENVELOPE_LEGACY]: { code: 'BAD_REQUEST', status: 400 },
};
export const genericErrorCodeToTrpcErrorCodeMap: Record<string, { code: string; status: number }> = {
[AppErrorCode.ALREADY_EXISTS]: { code: 'BAD_REQUEST', status: 400 },
[AppErrorCode.RECIPIENT_EXPIRED]: { code: 'BAD_REQUEST', status: 400 },
[AppErrorCode.EXPIRED_CODE]: { code: 'BAD_REQUEST', status: 400 },
[AppErrorCode.INVALID_BODY]: { code: 'BAD_REQUEST', status: 400 },
[AppErrorCode.INVALID_REQUEST]: { code: 'BAD_REQUEST', status: 400 },
[AppErrorCode.INVALID_CAPTCHA]: { code: 'BAD_REQUEST', status: 400 },
[AppErrorCode.NOT_FOUND]: { code: 'NOT_FOUND', status: 404 },
[AppErrorCode.NOT_IMPLEMENTED]: { code: 'INTERNAL_SERVER_ERROR', status: 501 },
[AppErrorCode.NOT_SETUP]: { code: 'BAD_REQUEST', status: 400 },
[AppErrorCode.UNAUTHORIZED]: { code: 'UNAUTHORIZED', status: 401 },
[AppErrorCode.FORBIDDEN]: { code: 'FORBIDDEN', status: 403 },
[AppErrorCode.UNKNOWN_ERROR]: { code: 'INTERNAL_SERVER_ERROR', status: 500 },
[AppErrorCode.RETRY_EXCEPTION]: { code: 'INTERNAL_SERVER_ERROR', status: 500 },
[AppErrorCode.SCHEMA_FAILED]: { code: 'INTERNAL_SERVER_ERROR', status: 500 },
[AppErrorCode.TOO_MANY_REQUESTS]: { code: 'TOO_MANY_REQUESTS', status: 429 },
[AppErrorCode.TWO_FACTOR_AUTH_FAILED]: { code: 'UNAUTHORIZED', status: 401 },
[AppErrorCode.ENVELOPE_DRAFT]: { code: 'BAD_REQUEST', status: 400 },
[AppErrorCode.ENVELOPE_COMPLETED]: { code: 'BAD_REQUEST', status: 400 },
[AppErrorCode.ENVELOPE_REJECTED]: { code: 'BAD_REQUEST', status: 400 },
[AppErrorCode.ENVELOPE_LEGACY]: { code: 'BAD_REQUEST', status: 400 },
};
export const ZAppErrorJsonSchema = z.object({
code: z.string(),
@@ -155,8 +154,7 @@ export class AppError extends Error {
const validCode: string | null = typeof code === 'string' ? code : AppErrorCode.UNKNOWN_ERROR;
const validMessage: string | undefined = typeof message === 'string' ? message : undefined;
const validUserMessage: string | undefined =
typeof userMessage === 'string' ? userMessage : undefined;
const validUserMessage: string | undefined = typeof userMessage === 'string' ? userMessage : undefined;
const validStatusCode = typeof statusCode === 'number' ? statusCode : undefined;
+2 -7
View File
@@ -47,10 +47,7 @@ export type JobDefinition<Name extends string = string, Schema = any> = {
export interface JobRunIO {
// stableRun<T extends Json | void>(cacheKey: string, callback: (io: JobRunIO) => T | Promise<T>): Promise<T>;
runTask<T extends Json | void | undefined>(
cacheKey: string,
callback: () => Promise<T>,
): Promise<T>;
runTask<T extends Json | void | undefined>(cacheKey: string, callback: () => Promise<T>): Promise<T>;
triggerJob(cacheKey: string, options: SimpleTriggerJobOptions): Promise<unknown>;
wait(cacheKey: string, ms: number): Promise<void>;
logger: {
@@ -62,6 +59,4 @@ export interface JobRunIO {
};
}
export const defineJob = <N extends string, T = unknown>(
job: JobDefinition<N, T>,
): JobDefinition<N, T> => job;
export const defineJob = <N extends string, T = unknown>(job: JobDefinition<N, T>): JobDefinition<N, T> => job;
+7 -12
View File
@@ -1,18 +1,17 @@
import { createRequire } from 'node:module';
import path from 'node:path';
import { createBullBoard } from '@bull-board/api';
import { BullMQAdapter } from '@bull-board/api/bullMQAdapter';
import { HonoAdapter } from '@bull-board/hono';
import { prisma } from '@documenso/prisma';
import { serveStatic } from '@hono/node-server/serve-static';
import { sha256 } from '@noble/hashes/sha2';
import { BackgroundJobStatus, Prisma } from '@prisma/client';
import { Queue, Worker } from 'bullmq';
import type { Job } from 'bullmq';
import { Hono } from 'hono';
import { Queue, Worker } from 'bullmq';
import type { Context as HonoContext } from 'hono';
import { Hono } from 'hono';
import IORedis from 'ioredis';
import { createRequire } from 'node:module';
import path from 'node:path';
import { prisma } from '@documenso/prisma';
import { env } from '../../utils/env';
import type { JobDefinition, JobRunIO, SimpleTriggerJobOptions } from './_internal/job';
@@ -42,9 +41,7 @@ export class BullMQJobProvider extends BaseJobProvider {
const redisUrl = env('NEXT_PRIVATE_REDIS_URL');
if (!redisUrl) {
throw new Error(
'[JOBS]: NEXT_PRIVATE_REDIS_URL is required when using the BullMQ jobs provider',
);
throw new Error('[JOBS]: NEXT_PRIVATE_REDIS_URL is required when using the BullMQ jobs provider');
}
const prefix = env('NEXT_PRIVATE_REDIS_PREFIX') || 'documenso';
@@ -135,9 +132,7 @@ export class BullMQJobProvider extends BaseJobProvider {
}
public async triggerJob(options: SimpleTriggerJobOptions) {
const eligibleJobs = Object.values(this._jobDefinitions).filter(
(job) => job.trigger.name === options.name,
);
const eligibleJobs = Object.values(this._jobDefinitions).filter((job) => job.trigger.name === options.name);
await Promise.all(
eligibleJobs.map(async (job) => {
+5 -7
View File
@@ -1,7 +1,6 @@
import type { Context as HonoContext } from 'hono';
import type { Context, Handler, InngestFunction } from 'inngest';
import type { Context, Handler, InngestFunction, Logger } from 'inngest';
import { Inngest as InngestClient } from 'inngest';
import type { Logger } from 'inngest';
import { serve as createHonoPagesRoute } from 'inngest/hono';
import { env } from '../../utils/env';
@@ -12,8 +11,7 @@ export class InngestJobProvider extends BaseJobProvider {
private static _instance: InngestJobProvider;
private _client: InngestClient;
private _functions: Array<InngestFunction<InngestFunction.Options, Handler.Any, Handler.Any>> =
[];
private _functions: Array<InngestFunction<InngestFunction.Options, Handler.Any, Handler.Any>> = [];
private constructor(options: { client: InngestClient }) {
super();
@@ -22,17 +20,17 @@ export class InngestJobProvider extends BaseJobProvider {
}
static getInstance() {
if (!this._instance) {
if (!InngestJobProvider._instance) {
const client = new InngestClient({
id: env('NEXT_PRIVATE_INNGEST_APP_ID') || 'documenso-app',
eventKey: env('INNGEST_EVENT_KEY') || env('NEXT_PRIVATE_INNGEST_EVENT_KEY'),
logger: console,
});
this._instance = new InngestJobProvider({ client });
InngestJobProvider._instance = new InngestJobProvider({ client });
}
return this._instance;
return InngestJobProvider._instance;
}
public defineJob<N extends string, T>(job: JobDefinition<N, T>): void {
+7 -15
View File
@@ -1,10 +1,9 @@
import { prisma } from '@documenso/prisma';
import { sha256 } from '@noble/hashes/sha2';
import { BackgroundJobStatus, Prisma } from '@prisma/client';
import { CronExpressionParser } from 'cron-parser';
import type { Context as HonoContext } from 'hono';
import { prisma } from '@documenso/prisma';
import { NEXT_PRIVATE_INTERNAL_WEBAPP_URL } from '../../constants/app';
import { sign } from '../../server-only/crypto/sign';
import { verify } from '../../server-only/crypto/verify';
@@ -50,11 +49,11 @@ export class LocalJobProvider extends BaseJobProvider {
}
static getInstance() {
if (!this._instance) {
this._instance = new LocalJobProvider();
if (!LocalJobProvider._instance) {
LocalJobProvider._instance = new LocalJobProvider();
}
return this._instance;
return LocalJobProvider._instance;
}
public defineJob<N extends string, T>(definition: JobDefinition<N, T>) {
@@ -192,9 +191,7 @@ export class LocalJobProvider extends BaseJobProvider {
}
public async triggerJob(options: SimpleTriggerJobOptions) {
const eligibleJobs = Object.values(this._jobDefinitions).filter(
(job) => job.trigger.name === options.name,
);
const eligibleJobs = Object.values(this._jobDefinitions).filter((job) => job.trigger.name === options.name);
await Promise.all(
eligibleJobs.map(async (job) => {
@@ -245,11 +242,7 @@ export class LocalJobProvider extends BaseJobProvider {
const definition = this._jobDefinitions[options.name];
if (
typeof jobId !== 'string' ||
typeof signature !== 'string' ||
typeof options !== 'object'
) {
if (typeof jobId !== 'string' || typeof signature !== 'string' || typeof options !== 'object') {
return c.text('Bad request', 400);
}
@@ -318,8 +311,7 @@ export class LocalJobProvider extends BaseJobProvider {
const taskHasExceededRetries = error instanceof BackgroundTaskExceededRetriesError;
const jobHasExceededRetries =
backgroundJob.retried >= backgroundJob.maxRetries &&
!(error instanceof BackgroundTaskFailedError);
backgroundJob.retried >= backgroundJob.maxRetries && !(error instanceof BackgroundTaskFailedError);
if (taskHasExceededRetries || jobHasExceededRetries) {
backgroundJob = await prisma.backgroundJob.update({
@@ -10,9 +10,7 @@ const SEND_CONFIRMATION_EMAIL_JOB_DEFINITION_SCHEMA = z.object({
force: z.boolean().optional(),
});
export type TSendConfirmationEmailJobDefinition = z.infer<
typeof SEND_CONFIRMATION_EMAIL_JOB_DEFINITION_SCHEMA
>;
export type TSendConfirmationEmailJobDefinition = z.infer<typeof SEND_CONFIRMATION_EMAIL_JOB_DEFINITION_SCHEMA>;
export const SEND_CONFIRMATION_EMAIL_JOB_DEFINITION = {
id: SEND_CONFIRMATION_EMAIL_JOB_DEFINITION_ID,
@@ -1,12 +1,10 @@
import { createElement } from 'react';
import { msg } from '@lingui/core/macro';
import { EnvelopeType, ReadStatus, SendStatus, SigningStatus } from '@prisma/client';
import { mailer } from '@documenso/email/mailer';
import DocumentCancelTemplate from '@documenso/email/templates/document-cancel';
import { isRecipientEmailValidForSending } from '@documenso/lib/utils/recipients';
import { prisma } from '@documenso/prisma';
import { msg } from '@lingui/core/macro';
import { EnvelopeType, ReadStatus, SendStatus, SigningStatus } from '@prisma/client';
import { createElement } from 'react';
import { getI18nInstance } from '../../../client-only/providers/i18n-server';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../../constants/app';
@@ -17,13 +15,7 @@ import { renderEmailWithI18N } from '../../../utils/render-email-with-i18n';
import type { JobRunIO } from '../../client/_internal/job';
import type { TSendDocumentCancelledEmailsJobDefinition } from './send-document-cancelled-emails';
export const run = async ({
payload,
io,
}: {
payload: TSendDocumentCancelledEmailsJobDefinition;
io: JobRunIO;
}) => {
export const run = async ({ payload, io }: { payload: TSendDocumentCancelledEmailsJobDefinition; io: JobRunIO }) => {
const { documentId, cancellationReason } = payload;
const envelope = await prisma.envelope.findFirstOrThrow({
@@ -1,6 +1,6 @@
import { z } from 'zod';
import { type JobDefinition } from '../../client/_internal/job';
import type { JobDefinition } from '../../client/_internal/job';
const SEND_DOCUMENT_CANCELLED_EMAILS_JOB_DEFINITION_ID = 'send.document.cancelled.emails';
@@ -1,10 +1,8 @@
import { createElement } from 'react';
import { msg } from '@lingui/core/macro';
import { mailer } from '@documenso/email/mailer';
import { DocumentCreatedFromDirectTemplateEmailTemplate } from '@documenso/email/templates/document-created-from-direct-template';
import { prisma } from '@documenso/prisma';
import { msg } from '@lingui/core/macro';
import { createElement } from 'react';
import { getI18nInstance } from '../../../client-only/providers/i18n-server';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../../constants/app';
@@ -13,11 +11,7 @@ import { renderEmailWithI18N } from '../../../utils/render-email-with-i18n';
import { formatDocumentsPath } from '../../../utils/teams';
import type { TSendDocumentCreatedFromDirectTemplateEmailJobDefinition } from './send-document-created-from-direct-template-email';
export const run = async ({
payload,
}: {
payload: TSendDocumentCreatedFromDirectTemplateEmailJobDefinition;
}) => {
export const run = async ({ payload }: { payload: TSendDocumentCreatedFromDirectTemplateEmailJobDefinition }) => {
const { envelopeId, recipientId } = payload;
const envelope = await prisma.envelope.findFirst({
@@ -1,6 +1,6 @@
import { z } from 'zod';
import { type JobDefinition } from '../../client/_internal/job';
import type { JobDefinition } from '../../client/_internal/job';
const SEND_DOCUMENT_CREATED_FROM_DIRECT_TEMPLATE_EMAIL_JOB_DEFINITION_ID =
'send.document.created.from.direct.template.email';
@@ -1,10 +1,8 @@
import { createElement } from 'react';
import { msg } from '@lingui/core/macro';
import { mailer } from '@documenso/email/mailer';
import OrganisationJoinEmailTemplate from '@documenso/email/templates/organisation-join';
import { prisma } from '@documenso/prisma';
import { msg } from '@lingui/core/macro';
import { createElement } from 'react';
import { getI18nInstance } from '../../../client-only/providers/i18n-server';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../../constants/app';
@@ -80,41 +78,38 @@ export const run = async ({
continue;
}
await io.runTask(
`send-organisation-member-joined-email--${invitedMember.id}_${member.id}`,
async () => {
const emailContent = createElement(OrganisationJoinEmailTemplate, {
assetBaseUrl: NEXT_PUBLIC_WEBAPP_URL(),
baseUrl: NEXT_PUBLIC_WEBAPP_URL(),
memberName: invitedMember.user.name || '',
memberEmail: invitedMember.user.email,
organisationName: organisation.name,
organisationUrl: organisation.url,
});
await io.runTask(`send-organisation-member-joined-email--${invitedMember.id}_${member.id}`, async () => {
const emailContent = createElement(OrganisationJoinEmailTemplate, {
assetBaseUrl: NEXT_PUBLIC_WEBAPP_URL(),
baseUrl: NEXT_PUBLIC_WEBAPP_URL(),
memberName: invitedMember.user.name || '',
memberEmail: invitedMember.user.email,
organisationName: organisation.name,
organisationUrl: organisation.url,
});
// !: Replace with the actual language of the recipient later
const [html, text] = await Promise.all([
renderEmailWithI18N(emailContent, {
lang: emailLanguage,
branding,
}),
renderEmailWithI18N(emailContent, {
lang: emailLanguage,
branding,
plainText: true,
}),
]);
// !: Replace with the actual language of the recipient later
const [html, text] = await Promise.all([
renderEmailWithI18N(emailContent, {
lang: emailLanguage,
branding,
}),
renderEmailWithI18N(emailContent, {
lang: emailLanguage,
branding,
plainText: true,
}),
]);
const i18n = await getI18nInstance(emailLanguage);
const i18n = await getI18nInstance(emailLanguage);
await mailer.sendMail({
to: member.user.email,
from: senderEmail,
subject: i18n._(msg`A new member has joined your organisation`),
html,
text,
});
},
);
await mailer.sendMail({
to: member.user.email,
from: senderEmail,
subject: i18n._(msg`A new member has joined your organisation`),
html,
text,
});
});
}
};
@@ -2,8 +2,7 @@ import { z } from 'zod';
import type { JobDefinition } from '../../client/_internal/job';
const SEND_ORGANISATION_MEMBER_JOINED_EMAIL_JOB_DEFINITION_ID =
'send.organisation-member-joined.email';
const SEND_ORGANISATION_MEMBER_JOINED_EMAIL_JOB_DEFINITION_ID = 'send.organisation-member-joined.email';
const SEND_ORGANISATION_MEMBER_JOINED_EMAIL_JOB_DEFINITION_SCHEMA = z.object({
organisationId: z.string(),
@@ -1,10 +1,8 @@
import { createElement } from 'react';
import { msg } from '@lingui/core/macro';
import { mailer } from '@documenso/email/mailer';
import OrganisationLeaveEmailTemplate from '@documenso/email/templates/organisation-leave';
import { prisma } from '@documenso/prisma';
import { msg } from '@lingui/core/macro';
import { createElement } from 'react';
import { getI18nInstance } from '../../../client-only/providers/i18n-server';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../../constants/app';
@@ -75,40 +73,37 @@ export const run = async ({
continue;
}
await io.runTask(
`send-organisation-member-left-email--${oldMember.id}_${member.id}`,
async () => {
const emailContent = createElement(OrganisationLeaveEmailTemplate, {
assetBaseUrl: NEXT_PUBLIC_WEBAPP_URL(),
baseUrl: NEXT_PUBLIC_WEBAPP_URL(),
memberName: oldMember.name || '',
memberEmail: oldMember.email,
organisationName: organisation.name,
organisationUrl: organisation.url,
});
await io.runTask(`send-organisation-member-left-email--${oldMember.id}_${member.id}`, async () => {
const emailContent = createElement(OrganisationLeaveEmailTemplate, {
assetBaseUrl: NEXT_PUBLIC_WEBAPP_URL(),
baseUrl: NEXT_PUBLIC_WEBAPP_URL(),
memberName: oldMember.name || '',
memberEmail: oldMember.email,
organisationName: organisation.name,
organisationUrl: organisation.url,
});
const [html, text] = await Promise.all([
renderEmailWithI18N(emailContent, {
lang: emailLanguage,
branding,
}),
renderEmailWithI18N(emailContent, {
lang: emailLanguage,
branding,
plainText: true,
}),
]);
const [html, text] = await Promise.all([
renderEmailWithI18N(emailContent, {
lang: emailLanguage,
branding,
}),
renderEmailWithI18N(emailContent, {
lang: emailLanguage,
branding,
plainText: true,
}),
]);
const i18n = await getI18nInstance(emailLanguage);
const i18n = await getI18nInstance(emailLanguage);
await mailer.sendMail({
to: member.user.email,
from: senderEmail,
subject: i18n._(msg`A member has left your organisation`),
html,
text,
});
},
);
await mailer.sendMail({
to: member.user.email,
from: senderEmail,
subject: i18n._(msg`A member has left your organisation`),
html,
text,
});
});
}
};
@@ -1,10 +1,8 @@
import { createElement } from 'react';
import { msg } from '@lingui/core/macro';
import { mailer } from '@documenso/email/mailer';
import { RecipientExpiredTemplate } from '@documenso/email/templates/recipient-expired';
import { prisma } from '@documenso/prisma';
import { msg } from '@lingui/core/macro';
import { createElement } from 'react';
import { getI18nInstance } from '../../../client-only/providers/i18n-server';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../../constants/app';
@@ -15,13 +13,7 @@ import { formatDocumentsPath } from '../../../utils/teams';
import type { JobRunIO } from '../../client/_internal/job';
import type { TSendOwnerRecipientExpiredEmailJobDefinition } from './send-owner-recipient-expired-email';
export const run = async ({
payload,
io,
}: {
payload: TSendOwnerRecipientExpiredEmailJobDefinition;
io: JobRunIO;
}) => {
export const run = async ({ payload, io }: { payload: TSendOwnerRecipientExpiredEmailJobDefinition; io: JobRunIO }) => {
const { recipientId, envelopeId } = payload;
const envelope = await prisma.envelope.findFirst({
@@ -107,9 +99,7 @@ export const run = async ({
address: documentOwner.email,
},
from: senderEmail,
subject: i18n._(
msg`Signing window expired for "${recipient.name || recipient.email}" on "${envelope.title}"`,
),
subject: i18n._(msg`Signing window expired for "${recipient.name || recipient.email}" on "${envelope.title}"`),
html,
text,
});
@@ -1,6 +1,6 @@
import { z } from 'zod';
import { type JobDefinition } from '../../client/_internal/job';
import type { JobDefinition } from '../../client/_internal/job';
const SEND_OWNER_RECIPIENT_EXPIRED_EMAIL_JOB_DEFINITION_ID = 'send.owner.recipient.expired.email';
@@ -1,11 +1,7 @@
import { sendResetPassword } from '../../../server-only/auth/send-reset-password';
import type { TSendPasswordResetSuccessEmailJobDefinition } from './send-password-reset-success-email';
export const run = async ({
payload,
}: {
payload: TSendPasswordResetSuccessEmailJobDefinition;
}) => {
export const run = async ({ payload }: { payload: TSendPasswordResetSuccessEmailJobDefinition }) => {
await sendResetPassword({
userId: payload.userId,
});
@@ -1,11 +1,9 @@
import { createElement } from 'react';
import { msg } from '@lingui/core/macro';
import { EnvelopeType } from '@prisma/client';
import { mailer } from '@documenso/email/mailer';
import { DocumentRecipientSignedEmailTemplate } from '@documenso/email/templates/document-recipient-signed';
import { prisma } from '@documenso/prisma';
import { msg } from '@lingui/core/macro';
import { EnvelopeType } from '@prisma/client';
import { createElement } from 'react';
import { getI18nInstance } from '../../../client-only/providers/i18n-server';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../../constants/app';
@@ -17,13 +15,7 @@ import { renderEmailWithI18N } from '../../../utils/render-email-with-i18n';
import type { JobRunIO } from '../../client/_internal/job';
import type { TSendRecipientSignedEmailJobDefinition } from './send-recipient-signed-email';
export const run = async ({
payload,
io,
}: {
payload: TSendRecipientSignedEmailJobDefinition;
io: JobRunIO;
}) => {
export const run = async ({ payload, io }: { payload: TSendRecipientSignedEmailJobDefinition; io: JobRunIO }) => {
const { documentId, recipientId } = payload;
const envelope = await prisma.envelope.findFirst({
@@ -66,9 +58,7 @@ export const run = async ({
throw new Error('Document has no recipients');
}
const isRecipientSignedEmailEnabled = extractDerivedDocumentEmailSettings(
envelope.documentMeta,
).recipientSigned;
const isRecipientSignedEmailEnabled = extractDerivedDocumentEmailSettings(envelope.documentMeta).recipientSigned;
if (!isRecipientSignedEmailEnabled) {
return;
@@ -1,6 +1,6 @@
import { z } from 'zod';
import { type JobDefinition } from '../../client/_internal/job';
import type { JobDefinition } from '../../client/_internal/job';
const SEND_RECIPIENT_SIGNED_EMAIL_JOB_DEFINITION_ID = 'send.recipient.signed.email';
@@ -9,9 +9,7 @@ const SEND_RECIPIENT_SIGNED_EMAIL_JOB_DEFINITION_SCHEMA = z.object({
recipientId: z.number(),
});
export type TSendRecipientSignedEmailJobDefinition = z.infer<
typeof SEND_RECIPIENT_SIGNED_EMAIL_JOB_DEFINITION_SCHEMA
>;
export type TSendRecipientSignedEmailJobDefinition = z.infer<typeof SEND_RECIPIENT_SIGNED_EMAIL_JOB_DEFINITION_SCHEMA>;
export const SEND_RECIPIENT_SIGNED_EMAIL_JOB_DEFINITION = {
id: SEND_RECIPIENT_SIGNED_EMAIL_JOB_DEFINITION_ID,
@@ -1,13 +1,11 @@
import { createElement } from 'react';
import { msg } from '@lingui/core/macro';
import { EnvelopeType, SendStatus, SigningStatus } from '@prisma/client';
import { mailer } from '@documenso/email/mailer';
import DocumentRejectedEmail from '@documenso/email/templates/document-rejected';
import DocumentRejectionConfirmedEmail from '@documenso/email/templates/document-rejection-confirmed';
import { isRecipientEmailValidForSending } from '@documenso/lib/utils/recipients';
import { prisma } from '@documenso/prisma';
import { msg } from '@lingui/core/macro';
import { EnvelopeType, SendStatus, SigningStatus } from '@prisma/client';
import { createElement } from 'react';
import { getI18nInstance } from '../../../client-only/providers/i18n-server';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../../constants/app';
@@ -20,13 +18,7 @@ import { formatDocumentsPath } from '../../../utils/teams';
import type { JobRunIO } from '../../client/_internal/job';
import type { TSendSigningRejectionEmailsJobDefinition } from './send-rejection-emails';
export const run = async ({
payload,
io,
}: {
payload: TSendSigningRejectionEmailsJobDefinition;
io: JobRunIO;
}) => {
export const run = async ({ payload, io }: { payload: TSendSigningRejectionEmailsJobDefinition; io: JobRunIO }) => {
const { documentId, recipientId } = payload;
const [envelope, recipient] = await Promise.all([
@@ -66,9 +58,7 @@ export const run = async ({
const { user: documentOwner } = envelope;
const isEmailEnabled = extractDerivedDocumentEmailSettings(
envelope.documentMeta,
).recipientSigningRequest;
const isEmailEnabled = extractDerivedDocumentEmailSettings(envelope.documentMeta).recipientSigningRequest;
if (!isEmailEnabled) {
return;
@@ -124,9 +114,7 @@ export const run = async ({
const ownerTemplate = createElement(DocumentRejectedEmail, {
recipientName: recipient.name,
documentName: envelope.title,
documentUrl: `${NEXT_PUBLIC_WEBAPP_URL()}${formatDocumentsPath(envelope.team?.url)}/${
envelope.id
}`,
documentUrl: `${NEXT_PUBLIC_WEBAPP_URL()}${formatDocumentsPath(envelope.team?.url)}/${envelope.id}`,
rejectionReason: recipient.rejectionReason || '',
assetBaseUrl: NEXT_PUBLIC_WEBAPP_URL(),
});
@@ -1,6 +1,6 @@
import { z } from 'zod';
import { type JobDefinition } from '../../client/_internal/job';
import type { JobDefinition } from '../../client/_internal/job';
const SEND_SIGNING_REJECTION_EMAILS_JOB_DEFINITION_ID = 'send.signing.rejected.emails';
@@ -1,5 +1,7 @@
import { createElement } from 'react';
import { mailer } from '@documenso/email/mailer';
import DocumentInviteEmailTemplate from '@documenso/email/templates/document-invite';
import { isRecipientEmailValidForSending } from '@documenso/lib/utils/recipients';
import { prisma } from '@documenso/prisma';
import { msg } from '@lingui/core/macro';
import {
DocumentSource,
@@ -9,18 +11,11 @@ import {
RecipientRole,
SendStatus,
} from '@prisma/client';
import { mailer } from '@documenso/email/mailer';
import DocumentInviteEmailTemplate from '@documenso/email/templates/document-invite';
import { isRecipientEmailValidForSending } from '@documenso/lib/utils/recipients';
import { prisma } from '@documenso/prisma';
import { createElement } from 'react';
import { getI18nInstance } from '../../../client-only/providers/i18n-server';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../../constants/app';
import {
RECIPIENT_ROLES_DESCRIPTION,
RECIPIENT_ROLE_TO_EMAIL_TYPE,
} from '../../../constants/recipient-roles';
import { RECIPIENT_ROLE_TO_EMAIL_TYPE, RECIPIENT_ROLES_DESCRIPTION } from '../../../constants/recipient-roles';
import { getEmailContext } from '../../../server-only/email/get-email-context';
import { updateRecipientNextReminder } from '../../../server-only/recipient/update-recipient-next-reminder';
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../../types/document-audit-logs';
@@ -32,13 +27,7 @@ import { renderEmailWithI18N } from '../../../utils/render-email-with-i18n';
import type { JobRunIO } from '../../client/_internal/job';
import type { TSendSigningEmailJobDefinition } from './send-signing-email';
export const run = async ({
payload,
io,
}: {
payload: TSendSigningEmailJobDefinition;
io: JobRunIO;
}) => {
export const run = async ({ payload, io }: { payload: TSendSigningEmailJobDefinition; io: JobRunIO }) => {
const { userId, documentId, recipientId, requestMetadata } = payload;
const [user, envelope, recipient] = await Promise.all([
@@ -94,15 +83,14 @@ export const run = async ({
return;
}
const { branding, emailLanguage, settings, organisationType, senderEmail, replyToEmail } =
await getEmailContext({
emailType: 'RECIPIENT',
source: {
type: 'team',
teamId: envelope.teamId,
},
meta: envelope.documentMeta,
});
const { branding, emailLanguage, settings, organisationType, senderEmail, replyToEmail } = await getEmailContext({
emailType: 'RECIPIENT',
source: {
type: 'team',
teamId: envelope.teamId,
},
meta: envelope.documentMeta,
});
const customEmail = envelope?.documentMeta;
const isDirectTemplate = envelope.source === DocumentSource.TEMPLATE_DIRECT_LINK;
@@ -114,9 +102,7 @@ export const run = async ({
const i18n = await getI18nInstance(emailLanguage);
const recipientActionVerb = i18n
._(RECIPIENT_ROLES_DESCRIPTION[recipient.role].actionVerb)
.toLowerCase();
const recipientActionVerb = i18n._(RECIPIENT_ROLES_DESCRIPTION[recipient.role].actionVerb).toLowerCase();
let emailMessage = customEmail?.message || '';
let emailSubject = i18n._(msg`Please ${recipientActionVerb} this document`);
@@ -132,9 +118,7 @@ export const run = async ({
emailMessage = i18n._(
msg`A document was created by your direct template that requires you to ${recipientActionVerb} it.`,
);
emailSubject = i18n._(
msg`Please ${recipientActionVerb} this document created by your direct template`,
);
emailSubject = i18n._(msg`Please ${recipientActionVerb} this document created by your direct template`);
}
if (organisationType === OrganisationType.ORGANISATION) {
@@ -165,9 +149,7 @@ export const run = async ({
documentName: envelope.title,
inviterName: user.name || undefined,
inviterEmail:
organisationType === OrganisationType.ORGANISATION
? team?.teamEmail?.email || user.email
: user.email,
organisationType === OrganisationType.ORGANISATION ? team?.teamEmail?.email || user.email : user.email,
assetBaseUrl,
signDocumentLink,
customBody: renderCustomEmailTemplate(emailMessage, customEmailTemplate),
@@ -197,10 +179,7 @@ export const run = async ({
},
from: senderEmail,
replyTo: replyToEmail,
subject: renderCustomEmailTemplate(
documentMeta?.subject || emailSubject,
customEmailTemplate,
),
subject: renderCustomEmailTemplate(documentMeta?.subject || emailSubject, customEmailTemplate),
html,
text,
});
@@ -1,7 +1,7 @@
import { z } from 'zod';
import { ZRequestMetadataSchema } from '../../../universal/extract-request-metadata';
import { type JobDefinition } from '../../client/_internal/job';
import type { JobDefinition } from '../../client/_internal/job';
const SEND_SIGNING_EMAIL_JOB_DEFINITION_ID = 'send.signing.requested.email';
@@ -12,9 +12,7 @@ const SEND_SIGNING_EMAIL_JOB_DEFINITION_SCHEMA = z.object({
requestMetadata: ZRequestMetadataSchema.optional(),
});
export type TSendSigningEmailJobDefinition = z.infer<
typeof SEND_SIGNING_EMAIL_JOB_DEFINITION_SCHEMA
>;
export type TSendSigningEmailJobDefinition = z.infer<typeof SEND_SIGNING_EMAIL_JOB_DEFINITION_SCHEMA>;
export const SEND_SIGNING_EMAIL_JOB_DEFINITION = {
id: SEND_SIGNING_EMAIL_JOB_DEFINITION_ID,
@@ -29,7 +27,4 @@ export const SEND_SIGNING_EMAIL_JOB_DEFINITION = {
await handler.run({ payload, io });
},
} as const satisfies JobDefinition<
typeof SEND_SIGNING_EMAIL_JOB_DEFINITION_ID,
TSendSigningEmailJobDefinition
>;
} as const satisfies JobDefinition<typeof SEND_SIGNING_EMAIL_JOB_DEFINITION_ID, TSendSigningEmailJobDefinition>;
@@ -2,13 +2,7 @@ import { sendTeamDeleteEmail } from '../../../server-only/team/delete-team';
import type { JobRunIO } from '../../client/_internal/job';
import type { TSendTeamDeletedEmailJobDefinition } from './send-team-deleted-email';
export const run = async ({
payload,
io,
}: {
payload: TSendTeamDeletedEmailJobDefinition;
io: JobRunIO;
}) => {
export const run = async ({ payload, io }: { payload: TSendTeamDeletedEmailJobDefinition; io: JobRunIO }) => {
const { team, members, organisationId } = payload;
for (const member of members) {
@@ -19,9 +19,7 @@ const SEND_TEAM_DELETED_EMAIL_JOB_DEFINITION_SCHEMA = z.object({
),
});
export type TSendTeamDeletedEmailJobDefinition = z.infer<
typeof SEND_TEAM_DELETED_EMAIL_JOB_DEFINITION_SCHEMA
>;
export type TSendTeamDeletedEmailJobDefinition = z.infer<typeof SEND_TEAM_DELETED_EMAIL_JOB_DEFINITION_SCHEMA>;
export const SEND_TEAM_DELETED_EMAIL_JOB_DEFINITION = {
id: SEND_TEAM_DELETED_EMAIL_JOB_DEFINITION_ID,
@@ -4,13 +4,7 @@ import { AppError, AppErrorCode } from '../../../errors/app-error';
import type { JobRunIO } from '../../client/_internal/job';
import type { TBackportSubscriptionClaimJobDefinition } from './backport-subscription-claims';
export const run = async ({
payload,
io,
}: {
payload: TBackportSubscriptionClaimJobDefinition;
io: JobRunIO;
}) => {
export const run = async ({ payload, io }: { payload: TBackportSubscriptionClaimJobDefinition; io: JobRunIO }) => {
const { subscriptionClaimId, flags } = payload;
const subscriptionClaim = await prisma.subscriptionClaim.findFirst({
@@ -1,6 +1,6 @@
import { z } from 'zod';
import { type JobDefinition } from '../../client/_internal/job';
import type { JobDefinition } from '../../client/_internal/job';
const BACKPORT_SUBSCRIPTION_CLAIM_JOB_DEFINITION_ID = 'internal.backport-subscription-claims';
@@ -25,9 +25,7 @@ const BACKPORT_SUBSCRIPTION_CLAIM_JOB_DEFINITION_SCHEMA = z.object({
}),
});
export type TBackportSubscriptionClaimJobDefinition = z.infer<
typeof BACKPORT_SUBSCRIPTION_CLAIM_JOB_DEFINITION_SCHEMA
>;
export type TBackportSubscriptionClaimJobDefinition = z.infer<typeof BACKPORT_SUBSCRIPTION_CLAIM_JOB_DEFINITION_SCHEMA>;
export const BACKPORT_SUBSCRIPTION_CLAIM_JOB_DEFINITION = {
id: BACKPORT_SUBSCRIPTION_CLAIM_JOB_DEFINITION_ID,
@@ -1,9 +1,3 @@
import { createElement } from 'react';
import { msg } from '@lingui/macro';
import { parse } from 'csv-parse/sync';
import { z } from 'zod';
import { mailer } from '@documenso/email/mailer';
import { BulkSendCompleteEmail } from '@documenso/email/templates/bulk-send-complete';
import { sendDocument } from '@documenso/lib/server-only/document/send-document';
@@ -11,6 +5,10 @@ import { createDocumentFromTemplate } from '@documenso/lib/server-only/template/
import { getTemplateById } from '@documenso/lib/server-only/template/get-template-by-id';
import { zEmail } from '@documenso/lib/utils/zod';
import { prisma } from '@documenso/prisma';
import { msg } from '@lingui/macro';
import { parse } from 'csv-parse/sync';
import { createElement } from 'react';
import { z } from 'zod';
import { getI18nInstance } from '../../../client-only/providers/i18n-server';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../../constants/app';
@@ -28,13 +26,7 @@ const ZRecipientRowSchema = z.object({
]),
});
export const run = async ({
payload,
io,
}: {
payload: TBulkSendTemplateJobDefinition;
io: JobRunIO;
}) => {
export const run = async ({ payload, io }: { payload: TBulkSendTemplateJobDefinition; io: JobRunIO }) => {
const { userId, teamId, templateId, csvContent, sendImmediately, requestMetadata } = payload;
const template = await getTemplateById({
@@ -82,7 +74,7 @@ export const run = async ({
const results = {
success: 0,
failed: 0,
errors: Array<string>(),
errors: [] as string[],
};
// Process each row
@@ -1,7 +1,7 @@
import { z } from 'zod';
import { ZRequestMetadataSchema } from '../../../universal/extract-request-metadata';
import { type JobDefinition } from '../../client/_internal/job';
import type { JobDefinition } from '../../client/_internal/job';
const BULK_SEND_TEMPLATE_JOB_DEFINITION_ID = 'internal.bulk-send-template';
@@ -14,9 +14,7 @@ const BULK_SEND_TEMPLATE_JOB_DEFINITION_SCHEMA = z.object({
requestMetadata: ZRequestMetadataSchema.optional(),
});
export type TBulkSendTemplateJobDefinition = z.infer<
typeof BULK_SEND_TEMPLATE_JOB_DEFINITION_SCHEMA
>;
export type TBulkSendTemplateJobDefinition = z.infer<typeof BULK_SEND_TEMPLATE_JOB_DEFINITION_SCHEMA>;
export const BULK_SEND_TEMPLATE_JOB_DEFINITION = {
id: BULK_SEND_TEMPLATE_JOB_DEFINITION_ID,
@@ -31,7 +29,4 @@ export const BULK_SEND_TEMPLATE_JOB_DEFINITION = {
await handler.run({ payload, io });
},
} as const satisfies JobDefinition<
typeof BULK_SEND_TEMPLATE_JOB_DEFINITION_ID,
TBulkSendTemplateJobDefinition
>;
} as const satisfies JobDefinition<typeof BULK_SEND_TEMPLATE_JOB_DEFINITION_ID, TBulkSendTemplateJobDefinition>;
@@ -1,6 +1,5 @@
import { DateTime } from 'luxon';
import { prisma } from '@documenso/prisma';
import { DateTime } from 'luxon';
import type { JobRunIO } from '../../client/_internal/job';
import type { TCleanupRateLimitsJobDefinition } from './cleanup-rate-limits';
@@ -1,14 +1,12 @@
import { z } from 'zod';
import { type JobDefinition } from '../../client/_internal/job';
import type { JobDefinition } from '../../client/_internal/job';
const CLEANUP_RATE_LIMITS_JOB_DEFINITION_ID = 'internal.cleanup-rate-limits';
const CLEANUP_RATE_LIMITS_JOB_DEFINITION_SCHEMA = z.object({});
export type TCleanupRateLimitsJobDefinition = z.infer<
typeof CLEANUP_RATE_LIMITS_JOB_DEFINITION_SCHEMA
>;
export type TCleanupRateLimitsJobDefinition = z.infer<typeof CLEANUP_RATE_LIMITS_JOB_DEFINITION_SCHEMA>;
export const CLEANUP_RATE_LIMITS_JOB_DEFINITION = {
id: CLEANUP_RATE_LIMITS_JOB_DEFINITION_ID,
@@ -24,7 +22,4 @@ export const CLEANUP_RATE_LIMITS_JOB_DEFINITION = {
await handler.run({ payload, io });
},
} as const satisfies JobDefinition<
typeof CLEANUP_RATE_LIMITS_JOB_DEFINITION_ID,
TCleanupRateLimitsJobDefinition
>;
} as const satisfies JobDefinition<typeof CLEANUP_RATE_LIMITS_JOB_DEFINITION_ID, TCleanupRateLimitsJobDefinition>;
@@ -1,19 +1,12 @@
import type { Prisma } from '@prisma/client';
import { WebhookCallStatus } from '@prisma/client';
import { executeWebhookCall } from '@documenso/lib/server-only/webhooks/execute-webhook-call';
import { prisma } from '@documenso/prisma';
import type { Prisma } from '@prisma/client';
import { WebhookCallStatus } from '@prisma/client';
import type { JobRunIO } from '../../client/_internal/job';
import type { TExecuteWebhookJobDefinition } from './execute-webhook';
export const run = async ({
payload,
io: _io,
}: {
payload: TExecuteWebhookJobDefinition;
io: JobRunIO;
}) => {
export const run = async ({ payload, io: _io }: { payload: TExecuteWebhookJobDefinition; io: JobRunIO }) => {
const { event, webhookId, data } = payload;
const webhook = await prisma.webhook.findUniqueOrThrow({
@@ -2,7 +2,7 @@ import { WebhookTriggerEvents } from '@prisma/client';
import { z } from 'zod';
import { ZRequestMetadataSchema } from '../../../universal/extract-request-metadata';
import { type JobDefinition } from '../../client/_internal/job';
import type { JobDefinition } from '../../client/_internal/job';
const EXECUTE_WEBHOOK_JOB_DEFINITION_ID = 'internal.execute-webhook';
@@ -28,7 +28,4 @@ export const EXECUTE_WEBHOOK_JOB_DEFINITION = {
await handler.run({ payload, io });
},
} as const satisfies JobDefinition<
typeof EXECUTE_WEBHOOK_JOB_DEFINITION_ID,
TExecuteWebhookJobDefinition
>;
} as const satisfies JobDefinition<typeof EXECUTE_WEBHOOK_JOB_DEFINITION_ID, TExecuteWebhookJobDefinition>;
@@ -1,17 +1,11 @@
import { DocumentStatus, SigningStatus } from '@prisma/client';
import { prisma } from '@documenso/prisma';
import { DocumentStatus, SigningStatus } from '@prisma/client';
import { jobs } from '../../client';
import type { JobRunIO } from '../../client/_internal/job';
import type { TExpireRecipientsSweepJobDefinition } from './expire-recipients-sweep';
export const run = async ({
io,
}: {
payload: TExpireRecipientsSweepJobDefinition;
io: JobRunIO;
}) => {
export const run = async ({ io }: { payload: TExpireRecipientsSweepJobDefinition; io: JobRunIO }) => {
const now = new Date();
const expiredRecipients = await prisma.recipient.findMany({
@@ -1,14 +1,12 @@
import { z } from 'zod';
import { type JobDefinition } from '../../client/_internal/job';
import type { JobDefinition } from '../../client/_internal/job';
const EXPIRE_RECIPIENTS_SWEEP_JOB_DEFINITION_ID = 'internal.expire-recipients-sweep';
const EXPIRE_RECIPIENTS_SWEEP_JOB_DEFINITION_SCHEMA = z.object({});
export type TExpireRecipientsSweepJobDefinition = z.infer<
typeof EXPIRE_RECIPIENTS_SWEEP_JOB_DEFINITION_SCHEMA
>;
export type TExpireRecipientsSweepJobDefinition = z.infer<typeof EXPIRE_RECIPIENTS_SWEEP_JOB_DEFINITION_SCHEMA>;
export const EXPIRE_RECIPIENTS_SWEEP_JOB_DEFINITION = {
id: EXPIRE_RECIPIENTS_SWEEP_JOB_DEFINITION_ID,
@@ -1,25 +1,15 @@
import { SigningStatus, WebhookTriggerEvents } from '@prisma/client';
import { prisma } from '@documenso/prisma';
import { SigningStatus, WebhookTriggerEvents } from '@prisma/client';
import { triggerWebhook } from '../../../server-only/webhooks/trigger/trigger-webhook';
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../../types/document-audit-logs';
import {
ZWebhookDocumentSchema,
mapEnvelopeToWebhookDocumentPayload,
} from '../../../types/webhook-payload';
import { mapEnvelopeToWebhookDocumentPayload, ZWebhookDocumentSchema } from '../../../types/webhook-payload';
import { createDocumentAuditLogData } from '../../../utils/document-audit-logs';
import { jobs } from '../../client';
import type { JobRunIO } from '../../client/_internal/job';
import type { TProcessRecipientExpiredJobDefinition } from './process-recipient-expired';
export const run = async ({
payload,
io,
}: {
payload: TProcessRecipientExpiredJobDefinition;
io: JobRunIO;
}) => {
export const run = async ({ payload, io }: { payload: TProcessRecipientExpiredJobDefinition; io: JobRunIO }) => {
const { recipientId } = payload;
// Atomic idempotency guard — only one concurrent worker wins.
@@ -57,9 +47,7 @@ export const run = async ({
const { envelope } = recipient;
io.logger.info(
`Recipient ${recipientId} (${recipient.email}) expired on envelope ${recipient.envelopeId}`,
);
io.logger.info(`Recipient ${recipientId} (${recipient.email}) expired on envelope ${recipient.envelopeId}`);
// Create audit log entry — wrapped so a retry skips this if it already succeeded.
await io.runTask('create-audit-log', async () => {
@@ -1,6 +1,6 @@
import { z } from 'zod';
import { type JobDefinition } from '../../client/_internal/job';
import type { JobDefinition } from '../../client/_internal/job';
const PROCESS_RECIPIENT_EXPIRED_JOB_DEFINITION_ID = 'internal.process-recipient-expired';
@@ -8,9 +8,7 @@ const PROCESS_RECIPIENT_EXPIRED_JOB_DEFINITION_SCHEMA = z.object({
recipientId: z.number(),
});
export type TProcessRecipientExpiredJobDefinition = z.infer<
typeof PROCESS_RECIPIENT_EXPIRED_JOB_DEFINITION_SCHEMA
>;
export type TProcessRecipientExpiredJobDefinition = z.infer<typeof PROCESS_RECIPIENT_EXPIRED_JOB_DEFINITION_SCHEMA>;
export const PROCESS_RECIPIENT_EXPIRED_JOB_DEFINITION = {
id: PROCESS_RECIPIENT_EXPIRED_JOB_DEFINITION_ID,
@@ -1,5 +1,6 @@
import { createElement } from 'react';
import { mailer } from '@documenso/email/mailer';
import DocumentReminderEmailTemplate from '@documenso/email/templates/document-reminder';
import { prisma } from '@documenso/prisma';
import { msg } from '@lingui/core/macro';
import {
DocumentDistributionMethod,
@@ -10,10 +11,7 @@ import {
SigningStatus,
WebhookTriggerEvents,
} from '@prisma/client';
import { mailer } from '@documenso/email/mailer';
import DocumentReminderEmailTemplate from '@documenso/email/templates/document-reminder';
import { prisma } from '@documenso/prisma';
import { createElement } from 'react';
import { getI18nInstance } from '../../../client-only/providers/i18n-server';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../../constants/app';
@@ -23,23 +21,14 @@ import { updateRecipientNextReminder } from '../../../server-only/recipient/upda
import { triggerWebhook } from '../../../server-only/webhooks/trigger/trigger-webhook';
import { DOCUMENT_AUDIT_LOG_TYPE, DOCUMENT_EMAIL_TYPE } from '../../../types/document-audit-logs';
import { extractDerivedDocumentEmailSettings } from '../../../types/document-email';
import {
ZWebhookDocumentSchema,
mapEnvelopeToWebhookDocumentPayload,
} from '../../../types/webhook-payload';
import { mapEnvelopeToWebhookDocumentPayload, ZWebhookDocumentSchema } from '../../../types/webhook-payload';
import { createDocumentAuditLogData } from '../../../utils/document-audit-logs';
import { renderCustomEmailTemplate } from '../../../utils/render-custom-email-template';
import { renderEmailWithI18N } from '../../../utils/render-email-with-i18n';
import type { JobRunIO } from '../../client/_internal/job';
import type { TProcessSigningReminderJobDefinition } from './process-signing-reminder';
export const run = async ({
payload,
io,
}: {
payload: TProcessSigningReminderJobDefinition;
io: JobRunIO;
}) => {
export const run = async ({ payload, io }: { payload: TProcessSigningReminderJobDefinition; io: JobRunIO }) => {
const { recipientId } = payload;
const now = new Date();
@@ -111,30 +100,23 @@ export const run = async ({
return;
}
const { branding, emailLanguage, organisationType, senderEmail, replyToEmail } =
await getEmailContext({
emailType: 'RECIPIENT',
source: {
type: 'team',
teamId: envelope.teamId,
},
meta: envelope.documentMeta,
});
const { branding, emailLanguage, organisationType, senderEmail, replyToEmail } = await getEmailContext({
emailType: 'RECIPIENT',
source: {
type: 'team',
teamId: envelope.teamId,
},
meta: envelope.documentMeta,
});
const i18n = await getI18nInstance(emailLanguage);
const recipientActionVerb = i18n
._(RECIPIENT_ROLES_DESCRIPTION[recipient.role].actionVerb)
.toLowerCase();
const recipientActionVerb = i18n._(RECIPIENT_ROLES_DESCRIPTION[recipient.role].actionVerb).toLowerCase();
let emailSubject = i18n._(
msg`Reminder: Please ${recipientActionVerb} the document "${envelope.title}"`,
);
let emailSubject = i18n._(msg`Reminder: Please ${recipientActionVerb} the document "${envelope.title}"`);
if (organisationType === OrganisationType.ORGANISATION) {
emailSubject = i18n._(
msg`Reminder: ${envelope.team.name} invited you to ${recipientActionVerb} a document`,
);
emailSubject = i18n._(msg`Reminder: ${envelope.team.name} invited you to ${recipientActionVerb} a document`);
}
const customEmailTemplate = {
@@ -1,6 +1,6 @@
import { z } from 'zod';
import { type JobDefinition } from '../../client/_internal/job';
import type { JobDefinition } from '../../client/_internal/job';
const PROCESS_SIGNING_REMINDER_JOB_DEFINITION_ID = 'internal.process-signing-reminder';
@@ -8,9 +8,7 @@ const PROCESS_SIGNING_REMINDER_JOB_DEFINITION_SCHEMA = z.object({
recipientId: z.number(),
});
export type TProcessSigningReminderJobDefinition = z.infer<
typeof PROCESS_SIGNING_REMINDER_JOB_DEFINITION_SCHEMA
>;
export type TProcessSigningReminderJobDefinition = z.infer<typeof PROCESS_SIGNING_REMINDER_JOB_DEFINITION_SCHEMA>;
export const PROCESS_SIGNING_REMINDER_JOB_DEFINITION = {
id: PROCESS_SIGNING_REMINDER_JOB_DEFINITION_ID,
@@ -1,8 +1,7 @@
import { kyselyPrisma, sql } from '@documenso/prisma';
import { DocumentStatus, EnvelopeType, RecipientRole, SigningStatus } from '@prisma/client';
import { DateTime } from 'luxon';
import { kyselyPrisma, sql } from '@documenso/prisma';
import { mapSecondaryIdToDocumentId } from '../../../utils/envelope';
import { jobs } from '../../client';
import type { JobRunIO } from '../../client/_internal/job';
@@ -30,9 +29,7 @@ export const run = async ({ io }: { payload: TSealDocumentSweepJobDefinition; io
.where('Envelope.type', '=', sql.lit(EnvelopeType.DOCUMENT))
.where('Envelope.deletedAt', 'is', null)
// Ensure there is at least one recipient.
.where((eb) =>
eb.exists(eb.selectFrom('Recipient').whereRef('Recipient.envelopeId', '=', 'Envelope.id')),
)
.where((eb) => eb.exists(eb.selectFrom('Recipient').whereRef('Recipient.envelopeId', '=', 'Envelope.id')))
// Document is ready to seal: all recipients are SIGNED/CC, or any recipient REJECTED.
.where((eb) =>
eb.or([
@@ -1,14 +1,12 @@
import { z } from 'zod';
import { type JobDefinition } from '../../client/_internal/job';
import type { JobDefinition } from '../../client/_internal/job';
const SEAL_DOCUMENT_SWEEP_JOB_DEFINITION_ID = 'internal.seal-document-sweep';
const SEAL_DOCUMENT_SWEEP_JOB_DEFINITION_SCHEMA = z.object({});
export type TSealDocumentSweepJobDefinition = z.infer<
typeof SEAL_DOCUMENT_SWEEP_JOB_DEFINITION_SCHEMA
>;
export type TSealDocumentSweepJobDefinition = z.infer<typeof SEAL_DOCUMENT_SWEEP_JOB_DEFINITION_SCHEMA>;
export const SEAL_DOCUMENT_SWEEP_JOB_DEFINITION = {
id: SEAL_DOCUMENT_SWEEP_JOB_DEFINITION_ID,
@@ -24,7 +22,4 @@ export const SEAL_DOCUMENT_SWEEP_JOB_DEFINITION = {
await handler.run({ payload, io });
},
} as const satisfies JobDefinition<
typeof SEAL_DOCUMENT_SWEEP_JOB_DEFINITION_ID,
TSealDocumentSweepJobDefinition
>;
} as const satisfies JobDefinition<typeof SEAL_DOCUMENT_SWEEP_JOB_DEFINITION_ID, TSealDocumentSweepJobDefinition>;
@@ -1,23 +1,16 @@
import { PDFDocument } from '@cantoo/pdf-lib';
import { PDF } from '@libpdf/core';
import type { DocumentData, Envelope, EnvelopeItem, Field } from '@prisma/client';
import {
DocumentStatus,
EnvelopeType,
RecipientRole,
SigningStatus,
WebhookTriggerEvents,
} from '@prisma/client';
import { nanoid } from 'nanoid';
import path from 'node:path';
import { groupBy } from 'remeda';
import { PDFDocument } from '@cantoo/pdf-lib';
import { addRejectionStampToPdf } from '@documenso/lib/server-only/pdf/add-rejection-stamp-to-pdf';
import { generateAuditLogPdf } from '@documenso/lib/server-only/pdf/generate-audit-log-pdf';
import { generateCertificatePdf } from '@documenso/lib/server-only/pdf/generate-certificate-pdf';
import { getLastPageDimensions } from '@documenso/lib/server-only/pdf/get-page-size';
import { prisma } from '@documenso/prisma';
import { signPdf } from '@documenso/signing';
import { PDF } from '@libpdf/core';
import type { DocumentData, Envelope, EnvelopeItem, Field } from '@prisma/client';
import { DocumentStatus, EnvelopeType, RecipientRole, SigningStatus, WebhookTriggerEvents } from '@prisma/client';
import { nanoid } from 'nanoid';
import { groupBy } from 'remeda';
import { NEXT_PRIVATE_USE_PLAYWRIGHT_PDF } from '../../../constants/app';
import { AppError, AppErrorCode } from '../../../errors/app-error';
@@ -29,14 +22,8 @@ import { insertFieldInPDFV2 } from '../../../server-only/pdf/insert-field-in-pdf
import { legacy_insertFieldInPDF } from '../../../server-only/pdf/legacy-insert-field-in-pdf';
import { getTeamSettings } from '../../../server-only/team/get-team-settings';
import { triggerWebhook } from '../../../server-only/webhooks/trigger/trigger-webhook';
import {
DOCUMENT_AUDIT_LOG_TYPE,
type TDocumentAuditLog,
} from '../../../types/document-audit-logs';
import {
ZWebhookDocumentSchema,
mapEnvelopeToWebhookDocumentPayload,
} from '../../../types/webhook-payload';
import { DOCUMENT_AUDIT_LOG_TYPE, type TDocumentAuditLog } from '../../../types/document-audit-logs';
import { mapEnvelopeToWebhookDocumentPayload, ZWebhookDocumentSchema } from '../../../types/webhook-payload';
import { prefixedId } from '../../../universal/id';
import { getFileServerSide } from '../../../universal/upload/get-file.server';
import { putPdfFileServerSide } from '../../../universal/upload/put-file.server';
@@ -47,13 +34,7 @@ import { mapDocumentIdToSecondaryId } from '../../../utils/envelope';
import type { JobRunIO } from '../../client/_internal/job';
import type { TSealDocumentJobDefinition } from './seal-document';
export const run = async ({
payload,
io,
}: {
payload: TSealDocumentJobDefinition;
io: JobRunIO;
}) => {
export const run = async ({ payload, io }: { payload: TSealDocumentJobDefinition; io: JobRunIO }) => {
const { documentId, sendEmail = true, isResealing = false, requestMetadata } = payload;
const { envelopeId, envelopeStatus, isRejected } = await io.runTask('seal-document', async () => {
@@ -112,8 +93,7 @@ export const run = async ({
const isComplete =
envelope.recipients.some((recipient) => recipient.signingStatus === SigningStatus.REJECTED) ||
envelope.recipients.every(
(recipient) =>
recipient.signingStatus === SigningStatus.SIGNED || recipient.role === RecipientRole.CC,
(recipient) => recipient.signingStatus === SigningStatus.SIGNED || recipient.role === RecipientRole.CC,
);
if (!isComplete) {
@@ -130,9 +110,7 @@ export const run = async ({
throw new Error(`Document ${envelope.id} has no envelope items`);
}
const recipientsWithoutCCers = envelope.recipients.filter(
(recipient) => recipient.role !== RecipientRole.CC,
);
const recipientsWithoutCCers = envelope.recipients.filter((recipient) => recipient.role !== RecipientRole.CC);
// Determine if the document has been rejected by checking if any recipient has rejected it
const rejectedRecipient = recipientsWithoutCCers.find(
@@ -204,9 +182,7 @@ export const run = async ({
const newDocumentData: Array<{ oldDocumentDataId: string; newDocumentDataId: string }> = [];
for (const { envelopeItem, pdfData } of prefetchedItems) {
const envelopeItemFields = envelope.envelopeItems.find(
(item) => item.id === envelopeItem.id,
)?.field;
const envelopeItemFields = envelope.envelopeItems.find((item) => item.id === envelopeItem.id)?.field;
if (!envelopeItemFields) {
throw new Error(`Envelope item fields not found for envelope item ${envelopeItem.id}`);
@@ -344,9 +320,7 @@ export const run = async ({
});
await triggerWebhook({
event: isRejected
? WebhookTriggerEvents.DOCUMENT_REJECTED
: WebhookTriggerEvents.DOCUMENT_COMPLETED,
event: isRejected ? WebhookTriggerEvents.DOCUMENT_REJECTED : WebhookTriggerEvents.DOCUMENT_COMPLETED,
data: ZWebhookDocumentSchema.parse(mapEnvelopeToWebhookDocumentPayload(updatedEnvelope)),
userId: updatedEnvelope.userId,
teamId: updatedEnvelope.teamId ?? undefined,
@@ -1,7 +1,7 @@
import { z } from 'zod';
import { ZRequestMetadataSchema } from '../../../universal/extract-request-metadata';
import { type JobDefinition } from '../../client/_internal/job';
import type { JobDefinition } from '../../client/_internal/job';
const SEAL_DOCUMENT_JOB_DEFINITION_ID = 'internal.seal-document';
@@ -28,7 +28,4 @@ export const SEAL_DOCUMENT_JOB_DEFINITION = {
await handler.run({ payload, io });
},
} as const satisfies JobDefinition<
typeof SEAL_DOCUMENT_JOB_DEFINITION_ID,
TSealDocumentJobDefinition
>;
} as const satisfies JobDefinition<typeof SEAL_DOCUMENT_JOB_DEFINITION_ID, TSealDocumentJobDefinition>;
@@ -1,17 +1,11 @@
import { DocumentStatus, RecipientRole, SendStatus, SigningStatus } from '@prisma/client';
import { prisma } from '@documenso/prisma';
import { DocumentStatus, RecipientRole, SendStatus, SigningStatus } from '@prisma/client';
import { jobs } from '../../client';
import type { JobRunIO } from '../../client/_internal/job';
import type { TSendSigningRemindersSweepJobDefinition } from './send-signing-reminders-sweep';
export const run = async ({
io,
}: {
payload: TSendSigningRemindersSweepJobDefinition;
io: JobRunIO;
}) => {
export const run = async ({ io }: { payload: TSendSigningRemindersSweepJobDefinition; io: JobRunIO }) => {
const now = new Date();
const recipients = await prisma.recipient.findMany({
@@ -1,6 +1,6 @@
import { z } from 'zod';
import { type JobDefinition } from '../../client/_internal/job';
import type { JobDefinition } from '../../client/_internal/job';
const SEND_SIGNING_REMINDERS_SWEEP_JOB_DEFINITION_ID = 'internal.send-signing-reminders-sweep';
@@ -1,8 +1,7 @@
import { DateTime } from 'luxon';
import { reregisterEmailDomain } from '@documenso/ee/server-only/lib/reregister-email-domain';
import { verifyEmailDomain } from '@documenso/ee/server-only/lib/verify-email-domain';
import { prisma } from '@documenso/prisma';
import { DateTime } from 'luxon';
import type { JobRunIO } from '../../client/_internal/job';
import type { TSyncEmailDomainsJobDefinition } from './sync-email-domains';
@@ -1,14 +1,12 @@
import { z } from 'zod';
import { type JobDefinition } from '../../client/_internal/job';
import type { JobDefinition } from '../../client/_internal/job';
const SYNC_EMAIL_DOMAINS_JOB_DEFINITION_ID = 'internal.sync-email-domains';
const SYNC_EMAIL_DOMAINS_JOB_DEFINITION_SCHEMA = z.object({});
export type TSyncEmailDomainsJobDefinition = z.infer<
typeof SYNC_EMAIL_DOMAINS_JOB_DEFINITION_SCHEMA
>;
export type TSyncEmailDomainsJobDefinition = z.infer<typeof SYNC_EMAIL_DOMAINS_JOB_DEFINITION_SCHEMA>;
export const SYNC_EMAIL_DOMAINS_JOB_DEFINITION = {
id: SYNC_EMAIL_DOMAINS_JOB_DEFINITION_ID,
@@ -24,7 +22,4 @@ export const SYNC_EMAIL_DOMAINS_JOB_DEFINITION = {
await handler.run({ payload, io });
},
} as const satisfies JobDefinition<
typeof SYNC_EMAIL_DOMAINS_JOB_DEFINITION_ID,
TSyncEmailDomainsJobDefinition
>;
} as const satisfies JobDefinition<typeof SYNC_EMAIL_DOMAINS_JOB_DEFINITION_ID, TSyncEmailDomainsJobDefinition>;
+2 -2
View File
@@ -12,8 +12,6 @@
"scripts": {
"test": "vitest run",
"test:watch": "vitest",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"clean": "rimraf node_modules"
},
"dependencies": {
@@ -59,6 +57,8 @@
"pino": "^9.14.0",
"pino-pretty": "^13.1.2",
"playwright": "1.56.1",
"postcss": "^8.5.6",
"postcss-selector-parser": "^7.1.0",
"posthog-js": "^1.297.2",
"posthog-node": "4.18.0",
"react": "^18",
+1 -2
View File
@@ -1,6 +1,5 @@
import { PlainClient } from '@team-plain/typescript-sdk';
import { env } from '@documenso/lib/utils/env';
import { PlainClient } from '@team-plain/typescript-sdk';
export const plainClient = new PlainClient({
apiKey: env('NEXT_PRIVATE_PLAIN_API_KEY') ?? '',
+2 -6
View File
@@ -1,17 +1,13 @@
import { prisma } from '@documenso/prisma';
import type { User } from '@prisma/client';
import { UserSecurityAuditLogType } from '@prisma/client';
import { prisma } from '@documenso/prisma';
import { AppError, AppErrorCode } from '../../errors/app-error';
import type { RequestMetadata } from '../../universal/extract-request-metadata';
import { validateTwoFactorAuthentication } from './validate-2fa';
type DisableTwoFactorAuthenticationOptions = {
user: Pick<
User,
'id' | 'email' | 'twoFactorEnabled' | 'twoFactorSecret' | 'twoFactorBackupCodes'
>;
user: Pick<User, 'id' | 'email' | 'twoFactorEnabled' | 'twoFactorSecret' | 'twoFactorBackupCodes'>;
totpCode?: string;
backupCode?: string;
requestMetadata?: RequestMetadata;
@@ -1,12 +1,10 @@
import { createElement } from 'react';
import { msg } from '@lingui/core/macro';
import { EnvelopeType } from '@prisma/client';
import { mailer } from '@documenso/email/mailer';
import { AccessAuth2FAEmailTemplate } from '@documenso/email/templates/access-auth-2fa';
import { isRecipientEmailValidForSending } from '@documenso/lib/utils/recipients';
import { prisma } from '@documenso/prisma';
import { msg } from '@lingui/core/macro';
import { EnvelopeType } from '@prisma/client';
import { createElement } from 'react';
import { getI18nInstance } from '../../../client-only/providers/i18n-server';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../../constants/app';
+1 -2
View File
@@ -1,6 +1,5 @@
import { type User, UserSecurityAuditLogType } from '@prisma/client';
import { prisma } from '@documenso/prisma';
import { type User, UserSecurityAuditLogType } from '@prisma/client';
import { AppError } from '../../errors/app-error';
import type { RequestMetadata } from '../../universal/extract-request-metadata';
@@ -25,9 +25,7 @@ export const getBackupCodes = ({ user }: GetBackupCodesOptions) => {
throw new Error('User has no backup codes');
}
const secret = Buffer.from(symmetricDecrypt({ key, data: user.twoFactorBackupCodes })).toString(
'utf-8',
);
const secret = Buffer.from(symmetricDecrypt({ key, data: user.twoFactorBackupCodes })).toString('utf-8');
const data = JSON.parse(secret);
@@ -6,8 +6,6 @@ type IsTwoFactorAuthenticationEnabledOptions = {
user: User;
};
export const isTwoFactorAuthenticationEnabled = ({
user,
}: IsTwoFactorAuthenticationEnabledOptions) => {
export const isTwoFactorAuthenticationEnabled = ({ user }: IsTwoFactorAuthenticationEnabledOptions) => {
return user.twoFactorEnabled && typeof DOCUMENSO_ENCRYPTION_KEY === 'string';
};
+3 -6
View File
@@ -1,10 +1,9 @@
import { type User } from '@prisma/client';
import { prisma } from '@documenso/prisma';
import type { User } from '@prisma/client';
import { base32 } from '@scure/base';
import crypto from 'crypto';
import { createTOTPKeyURI } from 'oslo/otp';
import { prisma } from '@documenso/prisma';
import { DOCUMENSO_ENCRYPTION_KEY } from '../../constants/crypto';
import { symmetricEncrypt } from '../../universal/crypto';
@@ -14,9 +13,7 @@ type SetupTwoFactorAuthenticationOptions = {
const ISSUER = 'Documenso';
export const setupTwoFactorAuthentication = async ({
user,
}: SetupTwoFactorAuthenticationOptions) => {
export const setupTwoFactorAuthentication = async ({ user }: SetupTwoFactorAuthenticationOptions) => {
const key = DOCUMENSO_ENCRYPTION_KEY;
if (!key) {
+1 -4
View File
@@ -7,10 +7,7 @@ import { verifyBackupCode } from './verify-backup-code';
type ValidateTwoFactorAuthenticationOptions = {
totpCode?: string;
backupCode?: string;
user: Pick<
User,
'id' | 'email' | 'twoFactorEnabled' | 'twoFactorSecret' | 'twoFactorBackupCodes'
>;
user: Pick<User, 'id' | 'email' | 'twoFactorEnabled' | 'twoFactorSecret' | 'twoFactorBackupCodes'>;
};
export const validateTwoFactorAuthentication = async ({
@@ -30,9 +30,7 @@ export const verifyTwoFactorAuthenticationToken = async ({
throw new Error('user missing 2fa secret');
}
const secret = Buffer.from(symmetricDecrypt({ key, data: user.twoFactorSecret })).toString(
'utf-8',
);
const secret = Buffer.from(symmetricDecrypt({ key, data: user.twoFactorSecret })).toString('utf-8');
const decodedSecret = base32.decode(secret);
@@ -1,6 +1,5 @@
import { compare } from '@node-rs/bcrypt';
import { prisma } from '@documenso/prisma';
import { compare } from '@node-rs/bcrypt';
type VerifyPasswordOptions = {
userId: number;
@@ -5,10 +5,7 @@ import { getBackupCodes } from './get-backup-code';
import { validateTwoFactorAuthentication } from './validate-2fa';
type ViewBackupCodesOptions = {
user: Pick<
User,
'id' | 'email' | 'twoFactorEnabled' | 'twoFactorSecret' | 'twoFactorBackupCodes'
>;
user: Pick<User, 'id' | 'email' | 'twoFactorEnabled' | 'twoFactorSecret' | 'twoFactorBackupCodes'>;
token: string;
};
@@ -1,6 +1,5 @@
import { EnvelopeType, type Prisma } from '@prisma/client';
import { prisma } from '@documenso/prisma';
import { EnvelopeType, type Prisma } from '@prisma/client';
import type { FindResultResponse } from '../../types/search-params';
@@ -10,11 +9,7 @@ export interface AdminFindDocumentsOptions {
perPage?: number;
}
export const adminFindDocuments = async ({
query,
page = 1,
perPage = 10,
}: AdminFindDocumentsOptions) => {
export const adminFindDocuments = async ({ query, page = 1, perPage = 10 }: AdminFindDocumentsOptions) => {
let termFilters: Prisma.EnvelopeWhereInput | undefined = !query
? undefined
: {
@@ -1,6 +1,5 @@
import { DocumentStatus, EnvelopeType, RecipientRole, SigningStatus } from '@prisma/client';
import { kyselyPrisma, sql } from '@documenso/prisma';
import { DocumentStatus, EnvelopeType, RecipientRole, SigningStatus } from '@prisma/client';
import type { FindResultResponse } from '../../types/search-params';
@@ -35,9 +34,7 @@ export const adminFindUnsealedDocuments = async ({
.where('Envelope.type', '=', sql.lit(EnvelopeType.DOCUMENT))
.where('Envelope.deletedAt', 'is', null)
// Must have at least one recipient.
.where((eb) =>
eb.exists(eb.selectFrom('Recipient').whereRef('Recipient.envelopeId', '=', 'Envelope.id')),
)
.where((eb) => eb.exists(eb.selectFrom('Recipient').whereRef('Recipient.envelopeId', '=', 'Envelope.id')))
// Document is ready to seal: all recipients are SIGNED/CC, or any recipient REJECTED.
.where((eb) =>
eb.or([
@@ -1,11 +1,9 @@
import { createElement } from 'react';
import { msg } from '@lingui/core/macro';
import { DocumentStatus, SendStatus } from '@prisma/client';
import { mailer } from '@documenso/email/mailer';
import DocumentCancelTemplate from '@documenso/email/templates/document-cancel';
import { prisma } from '@documenso/prisma';
import { msg } from '@lingui/core/macro';
import { DocumentStatus, SendStatus } from '@prisma/client';
import { createElement } from 'react';
import { getI18nInstance } from '../../client-only/providers/i18n-server';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
@@ -23,10 +21,7 @@ export type AdminSuperDeleteDocumentOptions = {
requestMetadata?: RequestMetadata;
};
export const adminSuperDeleteDocument = async ({
envelopeId,
requestMetadata,
}: AdminSuperDeleteDocumentOptions) => {
export const adminSuperDeleteDocument = async ({ envelopeId, requestMetadata }: AdminSuperDeleteDocumentOptions) => {
const envelope = await prisma.envelope.findUnique({
where: {
id: envelopeId,
@@ -61,20 +56,12 @@ export const adminSuperDeleteDocument = async ({
const { status, user } = envelope;
const isDocumentDeletedEmailEnabled = extractDerivedDocumentEmailSettings(
envelope.documentMeta,
).documentDeleted;
const isDocumentDeletedEmailEnabled = extractDerivedDocumentEmailSettings(envelope.documentMeta).documentDeleted;
const recipientsToNotify = envelope.recipients.filter((recipient) =>
isRecipientEmailValidForSending(recipient),
);
const recipientsToNotify = envelope.recipients.filter((recipient) => isRecipientEmailValidForSending(recipient));
// if the document is pending, send cancellation emails to all recipients
if (
status === DocumentStatus.PENDING &&
recipientsToNotify.length > 0 &&
isDocumentDeletedEmailEnabled
) {
if (status === DocumentStatus.PENDING && recipientsToNotify.length > 0 && isDocumentDeletedEmailEnabled) {
await Promise.all(
recipientsToNotify.map(async (recipient) => {
if (recipient.sendStatus !== SendStatus.SENT) {
@@ -1,7 +1,6 @@
import { EnvelopeType } from '@prisma/client';
import { prisma } from '@documenso/prisma';
import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status';
import { EnvelopeType } from '@prisma/client';
export const getDocumentStats = async () => {
const counts = await prisma.envelope.groupBy({
@@ -1,6 +1,5 @@
import type { EnvelopeType } from '@prisma/client';
import { prisma } from '@documenso/prisma';
import type { EnvelopeType } from '@prisma/client';
import { AppError, AppErrorCode } from '../../errors/app-error';
import type { EnvelopeIdOptions } from '../../utils/envelope';

Some files were not shown because too many files have changed in this diff Show More