chore: merge main

This commit is contained in:
Catalin Documenso
2025-06-24 10:49:08 +03:00
787 changed files with 55308 additions and 42985 deletions

View File

@ -23,6 +23,7 @@ import {
ZWebhookDocumentSchema,
mapDocumentToWebhookDocumentPayload,
} from '../../types/webhook-payload';
import { extractDocumentAuthMethods } from '../../utils/document-auth';
import { getIsRecipientsTurnToSign } from '../recipient/get-is-recipient-turn';
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
import { sendPendingEmail } from './send-pending-email';
@ -140,6 +141,11 @@ export const completeDocumentWithToken = async ({
},
});
const authOptions = extractDocumentAuthMethods({
documentAuth: document.authOptions,
recipientAuth: recipient.authOptions,
});
await tx.documentAuditLog.create({
data: createDocumentAuditLogData({
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_COMPLETED,
@ -154,6 +160,7 @@ export const completeDocumentWithToken = async ({
recipientName: recipient.name,
recipientId: recipient.id,
recipientRole: recipient.role,
actionAuth: authOptions.derivedRecipientActionAuth,
},
}),
});

View File

@ -6,9 +6,7 @@ import {
SigningStatus,
WebhookTriggerEvents,
} from '@prisma/client';
import { TeamMemberRole } from '@prisma/client';
import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { normalizePdf as makeNormalizedPdf } from '@documenso/lib/server-only/pdf/normalize-pdf';
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
@ -28,19 +26,22 @@ import { getFileServerSide } from '../../universal/upload/get-file.server';
import { putPdfFileServerSide } from '../../universal/upload/put-file.server';
import { createDocumentAuthOptions, createRecipientAuthOptions } from '../../utils/document-auth';
import { determineDocumentVisibility } from '../../utils/document-visibility';
import { buildTeamWhereQuery } from '../../utils/teams';
import { getMemberRoles } from '../team/get-member-roles';
import { getTeamSettings } from '../team/get-team-settings';
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
export type CreateDocumentOptions = {
userId: number;
teamId?: number;
teamId: number;
documentDataId: string;
normalizePdf?: boolean;
data: {
title: string;
externalId?: string;
visibility?: DocumentVisibility;
globalAccessAuth?: TDocumentAccessAuthTypes;
globalActionAuth?: TDocumentActionAuthTypes;
globalAccessAuth?: TDocumentAccessAuthTypes[];
globalActionAuth?: TDocumentActionAuthTypes[];
formValues?: TDocumentFormValues;
recipients: TCreateDocumentV2Request['recipients'];
};
@ -59,36 +60,28 @@ export const createDocumentV2 = async ({
}: CreateDocumentOptions) => {
const { title, formValues } = data;
const team = teamId
? await prisma.team.findFirst({
where: {
id: teamId,
members: {
some: {
userId,
},
},
const team = await prisma.team.findFirst({
where: buildTeamWhereQuery({ teamId, userId }),
include: {
organisation: {
select: {
organisationClaim: true,
},
include: {
teamGlobalSettings: true,
members: {
where: {
userId: userId,
},
select: {
role: true,
},
},
},
})
: null;
},
},
});
if (teamId !== undefined && !team) {
if (!team) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Team not found',
});
}
const settings = await getTeamSettings({
userId,
teamId,
});
if (normalizePdf) {
const documentData = await prisma.documentData.findFirst({
where: {
@ -113,30 +106,33 @@ export const createDocumentV2 = async ({
}
const authOptions = createDocumentAuthOptions({
globalAccessAuth: data?.globalAccessAuth || null,
globalActionAuth: data?.globalActionAuth || null,
globalAccessAuth: data?.globalAccessAuth || [],
globalActionAuth: data?.globalActionAuth || [],
});
const recipientsHaveActionAuth = data.recipients?.some((recipient) => recipient.actionAuth);
const recipientsHaveActionAuth = data.recipients?.some(
(recipient) => recipient.actionAuth && recipient.actionAuth.length > 0,
);
// Check if user has permission to set the global action auth.
if (authOptions.globalActionAuth || recipientsHaveActionAuth) {
const isDocumentEnterprise = await isUserEnterprise({
userId,
teamId,
if (
(authOptions.globalActionAuth.length > 0 || recipientsHaveActionAuth) &&
!team.organisation.organisationClaim.flags.cfr21
) {
throw new AppError(AppErrorCode.UNAUTHORIZED, {
message: 'You do not have permission to set the action auth',
});
if (!isDocumentEnterprise) {
throw new AppError(AppErrorCode.UNAUTHORIZED, {
message: 'You do not have permission to set the action auth',
});
}
}
const visibility = determineDocumentVisibility(
team?.teamGlobalSettings?.documentVisibility,
team?.members[0].role ?? TeamMemberRole.MEMBER,
);
const { teamRole } = await getMemberRoles({
teamId,
reference: {
type: 'User',
id: userId,
},
});
const visibility = determineDocumentVisibility(settings.documentVisibility, teamRole);
return await prisma.$transaction(async (tx) => {
const document = await tx.document.create({
@ -156,13 +152,10 @@ export const createDocumentV2 = async ({
...meta,
signingOrder: meta?.signingOrder || undefined,
emailSettings: meta?.emailSettings || undefined,
language: meta?.language || team?.teamGlobalSettings?.documentLanguage,
typedSignatureEnabled:
meta?.typedSignatureEnabled ?? team?.teamGlobalSettings?.typedSignatureEnabled,
uploadSignatureEnabled:
meta?.uploadSignatureEnabled ?? team?.teamGlobalSettings?.uploadSignatureEnabled,
drawSignatureEnabled:
meta?.drawSignatureEnabled ?? team?.teamGlobalSettings?.drawSignatureEnabled,
language: meta?.language || settings.documentLanguage,
typedSignatureEnabled: meta?.typedSignatureEnabled ?? settings.typedSignatureEnabled,
uploadSignatureEnabled: meta?.uploadSignatureEnabled ?? settings.uploadSignatureEnabled,
drawSignatureEnabled: meta?.drawSignatureEnabled ?? settings.drawSignatureEnabled,
},
},
},
@ -171,8 +164,8 @@ export const createDocumentV2 = async ({
await Promise.all(
(data.recipients || []).map(async (recipient) => {
const recipientAuthOptions = createRecipientAuthOptions({
accessAuth: recipient.accessAuth || null,
actionAuth: recipient.actionAuth || null,
accessAuth: recipient.accessAuth ?? [],
actionAuth: recipient.actionAuth ?? [],
});
await tx.recipient.create({

View File

@ -1,14 +1,13 @@
import { DocumentSource, WebhookTriggerEvents } from '@prisma/client';
import type { DocumentVisibility, Team, TeamGlobalSettings } from '@prisma/client';
import { TeamMemberRole } from '@prisma/client';
import type { DocumentVisibility } from '@prisma/client';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { normalizePdf as makeNormalizedPdf } from '@documenso/lib/server-only/pdf/normalize-pdf';
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
import { prisma } from '@documenso/prisma';
import { AppError, AppErrorCode } from '../../errors/app-error';
import {
ZWebhookDocumentSchema,
mapDocumentToWebhookDocumentPayload,
@ -17,13 +16,15 @@ import { prefixedId } from '../../universal/id';
import { getFileServerSide } from '../../universal/upload/get-file.server';
import { putPdfFileServerSide } from '../../universal/upload/put-file.server';
import { determineDocumentVisibility } from '../../utils/document-visibility';
import { getTeamById } from '../team/get-team';
import { getTeamSettings } from '../team/get-team-settings';
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
export type CreateDocumentOptions = {
title: string;
externalId?: string | null;
userId: number;
teamId?: number;
teamId: number;
documentDataId: string;
formValues?: Record<string, string | number | boolean>;
normalizePdf?: boolean;
@ -44,53 +45,13 @@ export const createDocument = async ({
timezone,
folderId,
}: CreateDocumentOptions) => {
const user = await prisma.user.findFirstOrThrow({
where: {
id: userId,
},
include: {
teamMembers: {
select: {
teamId: true,
},
},
},
const team = await getTeamById({ userId, teamId });
const settings = await getTeamSettings({
userId,
teamId,
});
if (
teamId !== undefined &&
!user.teamMembers.some((teamMember) => teamMember.teamId === teamId)
) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Team not found',
});
}
let team: (Team & { teamGlobalSettings: TeamGlobalSettings | null }) | null = null;
let userTeamRole: TeamMemberRole | undefined;
if (teamId) {
const teamWithUserRole = await prisma.team.findFirstOrThrow({
where: {
id: teamId,
},
include: {
teamGlobalSettings: true,
members: {
where: {
userId: userId,
},
select: {
role: true,
},
},
},
});
team = teamWithUserRole;
userTeamRole = teamWithUserRole.members[0]?.role;
}
let folderVisibility: DocumentVisibility | undefined;
if (folderId) {
@ -149,19 +110,16 @@ export const createDocument = async ({
folderId,
visibility:
folderVisibility ??
determineDocumentVisibility(
team?.teamGlobalSettings?.documentVisibility,
userTeamRole ?? TeamMemberRole.MEMBER,
),
determineDocumentVisibility(settings.documentVisibility, team.currentTeamRole),
formValues,
source: DocumentSource.DOCUMENT,
documentMeta: {
create: {
language: team?.teamGlobalSettings?.documentLanguage,
language: settings.documentLanguage,
timezone: timezone,
typedSignatureEnabled: team?.teamGlobalSettings?.typedSignatureEnabled ?? true,
uploadSignatureEnabled: team?.teamGlobalSettings?.uploadSignatureEnabled ?? true,
drawSignatureEnabled: team?.teamGlobalSettings?.drawSignatureEnabled ?? true,
typedSignatureEnabled: settings.typedSignatureEnabled,
uploadSignatureEnabled: settings.uploadSignatureEnabled,
drawSignatureEnabled: settings.drawSignatureEnabled,
},
},
},

View File

@ -1,14 +1,7 @@
import { createElement } from 'react';
import { msg } from '@lingui/core/macro';
import type {
Document,
DocumentMeta,
Recipient,
Team,
TeamGlobalSettings,
User,
} from '@prisma/client';
import type { Document, DocumentMeta, Recipient, User } from '@prisma/client';
import { DocumentStatus, SendStatus, WebhookTriggerEvents } from '@prisma/client';
import { mailer } from '@documenso/email/mailer';
@ -29,13 +22,14 @@ import type { ApiRequestMetadata } from '../../universal/extract-request-metadat
import { isDocumentCompleted } from '../../utils/document';
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
import { teamGlobalSettingsToBranding } from '../../utils/team-global-settings-to-branding';
import { getEmailContext } from '../email/get-email-context';
import { getMemberRoles } from '../team/get-member-roles';
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
export type DeleteDocumentOptions = {
id: number;
userId: number;
teamId?: number;
teamId: number;
requestMetadata: ApiRequestMetadata;
};
@ -64,23 +58,26 @@ export const deleteDocument = async ({
include: {
recipients: true,
documentMeta: true,
team: {
include: {
members: true,
teamGlobalSettings: true,
},
},
},
});
if (!document || (teamId !== undefined && teamId !== document.teamId)) {
if (!document) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Document not found',
});
}
const isUserTeamMember = await getMemberRoles({
teamId: document.teamId,
reference: {
type: 'User',
id: userId,
},
})
.then(() => true)
.catch(() => false);
const isUserOwner = document.userId === userId;
const isUserTeamMember = document.team?.members.some((member) => member.userId === userId);
const userRecipient = document.recipients.find((recipient) => recipient.email === user.email);
if (!isUserOwner && !isUserTeamMember && !userRecipient) {
@ -94,7 +91,6 @@ export const deleteDocument = async ({
await handleDocumentOwnerDelete({
document,
user,
team: document.team,
requestMetadata,
});
}
@ -142,11 +138,6 @@ type HandleDocumentOwnerDeleteOptions = {
recipients: Recipient[];
documentMeta: DocumentMeta | null;
};
team?:
| (Team & {
teamGlobalSettings?: TeamGlobalSettings | null;
})
| null;
user: User;
requestMetadata: ApiRequestMetadata;
};
@ -154,13 +145,19 @@ type HandleDocumentOwnerDeleteOptions = {
const handleDocumentOwnerDelete = async ({
document,
user,
team,
requestMetadata,
}: HandleDocumentOwnerDeleteOptions) => {
if (document.deletedAt) {
return;
}
const { branding, settings } = await getEmailContext({
source: {
type: 'team',
teamId: document.teamId,
},
});
// Soft delete completed documents.
if (isDocumentCompleted(document.status)) {
return await prisma.$transaction(async (tx) => {
@ -235,20 +232,18 @@ const handleDocumentOwnerDelete = async ({
assetBaseUrl,
});
const branding = team?.teamGlobalSettings
? teamGlobalSettingsToBranding(team.teamGlobalSettings)
: undefined;
const lang = document.documentMeta?.language ?? settings.documentLanguage;
const [html, text] = await Promise.all([
renderEmailWithI18N(template, { lang: document.documentMeta?.language, branding }),
renderEmailWithI18N(template, { lang, branding }),
renderEmailWithI18N(template, {
lang: document.documentMeta?.language,
lang,
branding,
plainText: true,
}),
]);
const i18n = await getI18nInstance(document.documentMeta?.language);
const i18n = await getI18nInstance(lang);
await mailer.sendMail({
to: {

View File

@ -1,15 +1,22 @@
import { DocumentSource, type Prisma } from '@prisma/client';
import type { Prisma, Recipient } from '@prisma/client';
import { DocumentSource, WebhookTriggerEvents } from '@prisma/client';
import { omit } from 'remeda';
import { prisma } from '@documenso/prisma';
import { AppError, AppErrorCode } from '../../errors/app-error';
import { prefixedId } from '../../universal/id';
import {
ZWebhookDocumentSchema,
mapDocumentToWebhookDocumentPayload,
} from '../../types/webhook-payload';
import { nanoid, prefixedId } from '../../universal/id';
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
import { getDocumentWhereInput } from './get-document-by-id';
export interface DuplicateDocumentOptions {
documentId: number;
userId: number;
teamId?: number;
teamId: number;
}
export const duplicateDocument = async ({
@ -17,7 +24,7 @@ export const duplicateDocument = async ({
userId,
teamId,
}: DuplicateDocumentOptions) => {
const documentWhereInput = await getDocumentWhereInput({
const { documentWhereInput } = await getDocumentWhereInput({
documentId,
userId,
teamId,
@ -35,14 +42,16 @@ export const duplicateDocument = async ({
type: true,
},
},
documentMeta: {
authOptions: true,
visibility: true,
documentMeta: true,
recipients: {
select: {
message: true,
subject: true,
dateFormat: true,
password: true,
timezone: true,
redirectUrl: true,
email: true,
name: true,
role: true,
signingOrder: true,
fields: true,
},
},
},
@ -54,39 +63,88 @@ export const duplicateDocument = async ({
});
}
const createDocumentArguments: Prisma.DocumentCreateArgs = {
const documentData = await prisma.documentData.create({
data: {
title: document.title,
qrToken: prefixedId('qr'),
user: {
connect: {
id: document.userId,
},
},
documentData: {
create: {
...document.documentData,
data: document.documentData.initialData,
},
},
documentMeta: {
create: {
...document.documentMeta,
},
},
source: DocumentSource.DOCUMENT,
type: document.documentData.type,
data: document.documentData.initialData,
initialData: document.documentData.initialData,
},
};
});
if (teamId !== undefined) {
createDocumentArguments.data.team = {
connect: {
id: teamId,
let documentMeta: Prisma.DocumentCreateArgs['data']['documentMeta'] | undefined = undefined;
if (document.documentMeta) {
documentMeta = {
create: {
...omit(document.documentMeta, ['id', 'documentId']),
emailSettings: document.documentMeta.emailSettings || undefined,
},
};
}
const createdDocument = await prisma.document.create(createDocumentArguments);
const createdDocument = await prisma.document.create({
data: {
userId: document.userId,
teamId: teamId,
title: document.title,
documentDataId: documentData.id,
authOptions: document.authOptions || undefined,
visibility: document.visibility,
qrToken: prefixedId('qr'),
documentMeta,
source: DocumentSource.DOCUMENT,
},
include: {
recipients: true,
documentMeta: true,
},
});
const recipientsToCreate = document.recipients.map((recipient) => ({
documentId: createdDocument.id,
email: recipient.email,
name: recipient.name,
role: recipient.role,
signingOrder: recipient.signingOrder,
token: nanoid(),
fields: {
createMany: {
data: recipient.fields.map((field) => ({
documentId: createdDocument.id,
type: field.type,
page: field.page,
positionX: field.positionX,
positionY: field.positionY,
width: field.width,
height: field.height,
customText: '',
inserted: false,
fieldMeta: field.fieldMeta as PrismaJson.FieldMeta,
})),
},
},
}));
const recipients: Recipient[] = [];
for (const recipientData of recipientsToCreate) {
const newRecipient = await prisma.recipient.create({
data: recipientData,
});
recipients.push(newRecipient);
}
await triggerWebhook({
event: WebhookTriggerEvents.DOCUMENT_CREATED,
data: ZWebhookDocumentSchema.parse({
...mapDocumentToWebhookDocumentPayload(createdDocument),
recipients,
documentMeta: createdDocument.documentMeta,
}),
userId: userId,
teamId: teamId,
});
return {
documentId: createdDocument.id,

View File

@ -6,10 +6,11 @@ import { AppError, AppErrorCode } from '../../errors/app-error';
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs';
import type { FindResultResponse } from '../../types/search-params';
import { parseDocumentAuditLogData } from '../../utils/document-audit-logs';
import { getDocumentWhereInput } from './get-document-by-id';
export interface FindDocumentAuditLogsOptions {
userId: number;
teamId?: number;
teamId: number;
documentId: number;
page?: number;
perPage?: number;
@ -34,25 +35,14 @@ export const findDocumentAuditLogs = async ({
const orderByColumn = orderBy?.column ?? 'createdAt';
const orderByDirection = orderBy?.direction ?? 'desc';
const { documentWhereInput } = await getDocumentWhereInput({
documentId,
userId,
teamId,
});
const document = await prisma.document.findFirst({
where: {
id: documentId,
...(teamId
? {
team: {
id: teamId,
members: {
some: {
userId,
},
},
},
}
: {
userId,
teamId: null,
}),
},
where: documentWhereInput,
});
if (!document) {

View File

@ -9,6 +9,7 @@ import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-documen
import { DocumentVisibility } from '../../types/document-visibility';
import { type FindResultResponse } from '../../types/search-params';
import { maskRecipientTokensForDocument } from '../../utils/mask-recipient-tokens-for-document';
import { getTeamById } from '../team/get-team';
export type PeriodSelectorValue = '' | '7d' | '14d' | '30d';
@ -53,32 +54,15 @@ export const findDocuments = async ({
let team = null;
if (teamId !== undefined) {
team = await prisma.team.findFirstOrThrow({
where: {
id: teamId,
members: {
some: {
userId,
},
},
},
include: {
teamEmail: true,
members: {
where: {
userId,
},
select: {
role: true,
},
},
},
team = await getTeamById({
userId,
teamId,
});
}
const orderByColumn = orderBy?.column ?? 'createdAt';
const orderByDirection = orderBy?.direction ?? 'desc';
const teamMemberRole = team?.members[0].role ?? null;
const teamMemberRole = team?.currentTeamRole ?? null;
const searchFilter: Prisma.DocumentWhereInput = {
OR: [
@ -291,7 +275,6 @@ const findDocumentsFilter = (
OR: [
{
userId: user.id,
teamId: null,
folderId: folderId,
},
{
@ -330,14 +313,12 @@ const findDocumentsFilter = (
}))
.with(ExtendedDocumentStatus.DRAFT, () => ({
userId: user.id,
teamId: null,
status: ExtendedDocumentStatus.DRAFT,
}))
.with(ExtendedDocumentStatus.PENDING, () => ({
OR: [
{
userId: user.id,
teamId: null,
status: ExtendedDocumentStatus.PENDING,
folderId: folderId,
},
@ -360,7 +341,6 @@ const findDocumentsFilter = (
OR: [
{
userId: user.id,
teamId: null,
status: ExtendedDocumentStatus.COMPLETED,
folderId: folderId,
},
@ -379,7 +359,6 @@ const findDocumentsFilter = (
OR: [
{
userId: user.id,
teamId: null,
status: ExtendedDocumentStatus.REJECTED,
folderId: folderId,
},

View File

@ -1,5 +1,5 @@
import type { Prisma } from '@prisma/client';
import { TeamMemberRole } from '@prisma/client';
import { DocumentStatus, TeamMemberRole } from '@prisma/client';
import { match } from 'ts-pattern';
import { prisma } from '@documenso/prisma';
@ -11,7 +11,7 @@ import { getTeamById } from '../team/get-team';
export type GetDocumentByIdOptions = {
documentId: number;
userId: number;
teamId?: number;
teamId: number;
folderId?: string;
};
@ -21,7 +21,7 @@ export const getDocumentById = async ({
teamId,
folderId,
}: GetDocumentByIdOptions) => {
const documentWhereInput = await getDocumentWhereInput({
const { documentWhereInput } = await getDocumentWhereInput({
documentId,
userId,
teamId,
@ -68,18 +68,7 @@ export const getDocumentById = async ({
export type GetDocumentWhereInputOptions = {
documentId: number;
userId: number;
teamId?: number;
/**
* Whether to return a filter that allows access to both the user and team documents.
* This only applies if `teamId` is passed in.
*
* If true, and `teamId` is passed in, the filter will allow both team and user documents.
* If false, and `teamId` is passed in, the filter will only allow team documents.
*
* Defaults to false.
*/
overlapUserTeamScope?: boolean;
teamId: number;
};
/**
@ -91,42 +80,55 @@ export const getDocumentWhereInput = async ({
documentId,
userId,
teamId,
overlapUserTeamScope = false,
}: GetDocumentWhereInputOptions) => {
const documentWhereInput: Prisma.DocumentWhereUniqueInput = {
id: documentId,
OR: [
{
userId,
},
],
};
if (teamId === undefined || !documentWhereInput.OR) {
return documentWhereInput;
}
const team = await getTeamById({ teamId, userId });
// Allow access to team and user documents.
if (overlapUserTeamScope) {
documentWhereInput.OR.push({
teamId: team.id,
});
}
const user = await prisma.user.findFirstOrThrow({
where: {
id: userId,
},
});
// Allow access to only team documents.
if (!overlapUserTeamScope) {
documentWhereInput.OR = [
{
teamId: team.id,
const teamVisibilityFilters = match(team.currentTeamRole)
.with(TeamMemberRole.ADMIN, () => [
DocumentVisibility.EVERYONE,
DocumentVisibility.MANAGER_AND_ABOVE,
DocumentVisibility.ADMIN,
])
.with(TeamMemberRole.MANAGER, () => [
DocumentVisibility.EVERYONE,
DocumentVisibility.MANAGER_AND_ABOVE,
])
.otherwise(() => [DocumentVisibility.EVERYONE]);
const documentOrInput: Prisma.DocumentWhereInput[] = [
// Allow access if they own the document.
{
userId,
},
// Or, if they belong to the team that the document is associated with.
{
visibility: {
in: teamVisibilityFilters,
},
];
}
teamId,
},
// Or, if they are a recipient of the document.
{
status: {
not: DocumentStatus.DRAFT,
},
recipients: {
some: {
email: user.email,
},
},
},
];
// Allow access to documents sent to or from the team email.
if (team.teamEmail) {
documentWhereInput.OR.push(
documentOrInput.push(
{
recipients: {
some: {
@ -142,42 +144,13 @@ export const getDocumentWhereInput = async ({
);
}
const user = await prisma.user.findFirstOrThrow({
where: {
id: userId,
},
});
const visibilityFilters = [
...match(team.currentTeamMember?.role)
.with(TeamMemberRole.ADMIN, () => [
{ visibility: DocumentVisibility.EVERYONE },
{ visibility: DocumentVisibility.MANAGER_AND_ABOVE },
{ visibility: DocumentVisibility.ADMIN },
])
.with(TeamMemberRole.MANAGER, () => [
{ visibility: DocumentVisibility.EVERYONE },
{ visibility: DocumentVisibility.MANAGER_AND_ABOVE },
])
.otherwise(() => [{ visibility: DocumentVisibility.EVERYONE }]),
{
OR: [
{
recipients: {
some: {
email: user.email,
},
},
},
{
userId: user.id,
},
],
},
];
const documentWhereInput: Prisma.DocumentWhereUniqueInput = {
id: documentId,
OR: documentOrInput,
};
return {
...documentWhereInput,
OR: [...visibilityFilters],
documentWhereInput,
team,
};
};

View File

@ -86,11 +86,6 @@ export const getDocumentAndSenderByToken = async ({
select: {
name: true,
teamEmail: true,
teamGlobalSettings: {
select: {
includeSenderDetails: true,
},
},
},
},
},

View File

@ -17,6 +17,7 @@ export const getDocumentCertificateAuditLogs = async ({
in: [
DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_COMPLETED,
DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_REJECTED,
DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FIELD_INSERTED,
DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_OPENED,
DOCUMENT_AUDIT_LOG_TYPE.EMAIL_SENT,
],
@ -36,6 +37,9 @@ export const getDocumentCertificateAuditLogs = async ({
[DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_OPENED]: auditLogs.filter(
(log) => log.type === DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_OPENED,
),
[DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FIELD_INSERTED]: auditLogs.filter(
(log) => log.type === DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FIELD_INSERTED,
),
[DOCUMENT_AUDIT_LOG_TYPE.EMAIL_SENT]: auditLogs.filter(
(log) =>
log.type === DOCUMENT_AUDIT_LOG_TYPE.EMAIL_SENT &&

View File

@ -6,7 +6,7 @@ import { getDocumentWhereInput } from './get-document-by-id';
export type GetDocumentWithDetailsByIdOptions = {
documentId: number;
userId: number;
teamId?: number;
teamId: number;
folderId?: string;
};
@ -16,7 +16,7 @@ export const getDocumentWithDetailsById = async ({
teamId,
folderId,
}: GetDocumentWithDetailsByIdOptions) => {
const documentWhereInput = await getDocumentWhereInput({
const { documentWhereInput } = await getDocumentWhereInput({
documentId,
userId,
teamId,
@ -31,9 +31,33 @@ export const getDocumentWithDetailsById = async ({
documentData: true,
documentMeta: true,
recipients: true,
fields: true,
attachments: true,
folder: true,
fields: {
include: {
signature: true,
recipient: {
select: {
name: true,
email: true,
signingStatus: true,
},
},
},
},
team: {
select: {
id: true,
url: true,
},
},
user: {
select: {
id: true,
name: true,
email: true,
},
},
},
});

View File

@ -11,7 +11,7 @@ import { isExtendedDocumentStatus } from '@documenso/prisma/guards/is-extended-d
import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status';
export type GetStatsInput = {
user: User;
user: Pick<User, 'id' | 'email'>;
team?: Omit<GetTeamCountsOption, 'createdAt'>;
period?: PeriodSelectorValue;
search?: string;
@ -89,7 +89,7 @@ export const getStats = async ({
};
type GetCountsOption = {
user: User;
user: Pick<User, 'id' | 'email'>;
createdAt: Prisma.DocumentWhereInput['createdAt'];
search?: string;
folderId?: string | null;
@ -116,7 +116,6 @@ const getCounts = async ({ user, createdAt, search, folderId }: GetCountsOption)
where: {
userId: user.id,
createdAt,
teamId: null,
deletedAt: null,
AND: [searchFilter, rootPageFilter, folderId ? { folderId } : {}],
},
@ -334,7 +333,6 @@ const getTeamCounts = async (options: GetTeamCountsOption) => {
documentDeletedAt: null,
},
},
deletedAt: null,
},
],
},

View File

@ -5,6 +5,7 @@ import { match } from 'ts-pattern';
import { prisma } from '@documenso/prisma';
import { verifyTwoFactorAuthenticationToken } from '../2fa/verify-2fa-token';
import { verifyPassword } from '../2fa/verify-password';
import { AppError, AppErrorCode } from '../../errors/app-error';
import type { TDocumentAuth, TDocumentAuthMethods } from '../../types/document-auth';
import { DocumentAuth } from '../../types/document-auth';
@ -60,23 +61,26 @@ export const isRecipientAuthorized = async ({
recipientAuth: recipient.authOptions,
});
const authMethod: TDocumentAuth | null =
const authMethods: TDocumentAuth[] =
type === 'ACCESS' ? derivedRecipientAccessAuth : derivedRecipientActionAuth;
// Early true return when auth is not required.
if (!authMethod || authMethod === DocumentAuth.EXPLICIT_NONE) {
if (
authMethods.length === 0 ||
authMethods.some((method) => method === DocumentAuth.EXPLICIT_NONE)
) {
return true;
}
// Create auth options when none are passed for account.
if (!authOptions && authMethod === DocumentAuth.ACCOUNT) {
if (!authOptions && authMethods.some((method) => method === DocumentAuth.ACCOUNT)) {
authOptions = {
type: DocumentAuth.ACCOUNT,
};
}
// Authentication required does not match provided method.
if (!authOptions || authOptions.type !== authMethod || !userId) {
if (!authOptions || !authMethods.includes(authOptions.type) || !userId) {
return false;
}
@ -117,6 +121,15 @@ export const isRecipientAuthorized = async ({
window: 10, // 5 minutes worth of tokens
});
})
.with({ type: DocumentAuth.PASSWORD }, async ({ password }) => {
return await verifyPassword({
userId,
password,
});
})
.with({ type: DocumentAuth.EXPLICIT_NONE }, () => {
return true;
})
.exhaustive();
};
@ -160,7 +173,7 @@ const verifyPasskey = async ({
}: VerifyPasskeyOptions): Promise<void> => {
const passkey = await prisma.passkey.findFirst({
where: {
credentialId: Buffer.from(authenticationResponse.id, 'base64'),
credentialId: new Uint8Array(Buffer.from(authenticationResponse.id, 'base64')),
userId,
},
});

View File

@ -1,73 +0,0 @@
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import { prisma } from '@documenso/prisma';
import { AppError, AppErrorCode } from '../../errors/app-error';
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs';
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
export type MoveDocumentToTeamOptions = {
documentId: number;
teamId: number;
userId: number;
requestMetadata: ApiRequestMetadata;
};
export const moveDocumentToTeam = async ({
documentId,
teamId,
userId,
requestMetadata,
}: MoveDocumentToTeamOptions) => {
return await prisma.$transaction(async (tx) => {
const document = await tx.document.findFirst({
where: {
id: documentId,
userId,
teamId: null,
},
});
if (!document) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Document not found or already associated with a team.',
});
}
const team = await tx.team.findFirst({
where: {
id: teamId,
members: {
some: {
userId,
},
},
},
});
if (!team) {
throw new AppError(AppErrorCode.UNAUTHORIZED, {
message: 'This team does not exist, or you are not a member of this team.',
});
}
const updatedDocument = await tx.document.update({
where: { id: documentId },
data: { teamId },
});
await tx.documentAuditLog.create({
data: createDocumentAuditLogData({
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_MOVED_TO_TEAM,
documentId: updatedDocument.id,
metadata: requestMetadata,
data: {
movedByUserId: userId,
fromPersonalAccount: true,
toTeamId: teamId,
},
}),
});
return updatedDocument;
});
};

View File

@ -1,8 +1,7 @@
import { createElement } from 'react';
import { msg } from '@lingui/core/macro';
import { DocumentStatus, RecipientRole, SigningStatus } from '@prisma/client';
import type { Prisma } from '@prisma/client';
import { DocumentStatus, OrganisationType, RecipientRole, SigningStatus } from '@prisma/client';
import { mailer } from '@documenso/email/mailer';
import { DocumentInviteEmailTemplate } from '@documenso/email/templates/document-invite';
@ -22,14 +21,14 @@ import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
import { isDocumentCompleted } from '../../utils/document';
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
import { teamGlobalSettingsToBranding } from '../../utils/team-global-settings-to-branding';
import { getEmailContext } from '../email/get-email-context';
import { getDocumentWhereInput } from './get-document-by-id';
export type ResendDocumentOptions = {
documentId: number;
userId: number;
recipients: number[];
teamId?: number;
teamId: number;
requestMetadata: ApiRequestMetadata;
};
@ -46,7 +45,7 @@ export const resendDocument = async ({
},
});
const documentWhereInput: Prisma.DocumentWhereUniqueInput = await getDocumentWhereInput({
const { documentWhereInput } = await getDocumentWhereInput({
documentId,
userId,
teamId,
@ -68,14 +67,12 @@ export const resendDocument = async ({
select: {
teamEmail: true,
name: true,
teamGlobalSettings: true,
},
},
},
});
const customEmail = document?.documentMeta;
const isTeamDocument = document?.team !== null;
if (!document) {
throw new Error('Document not found');
@ -101,13 +98,21 @@ export const resendDocument = async ({
return;
}
const { branding, settings, organisationType } = await getEmailContext({
source: {
type: 'team',
teamId: document.teamId,
},
});
await Promise.all(
document.recipients.map(async (recipient) => {
if (recipient.role === RecipientRole.CC) {
return;
}
const i18n = await getI18nInstance(document.documentMeta?.language);
const lang = document.documentMeta?.language ?? settings.documentLanguage;
const i18n = await getI18nInstance(lang);
const recipientEmailType = RECIPIENT_ROLE_TO_EMAIL_TYPE[recipient.role];
@ -128,7 +133,7 @@ export const resendDocument = async ({
emailSubject = i18n._(msg`Reminder: Please ${recipientActionVerb} your document`);
}
if (isTeamDocument && document.team) {
if (organisationType === OrganisationType.ORGANISATION) {
emailSubject = i18n._(
msg`Reminder: ${document.team.name} invited you to ${recipientActionVerb} a document`,
);
@ -151,27 +156,26 @@ export const resendDocument = async ({
const template = createElement(DocumentInviteEmailTemplate, {
documentName: document.title,
inviterName: user.name || undefined,
inviterEmail: isTeamDocument ? document.team?.teamEmail?.email || user.email : user.email,
inviterEmail:
organisationType === OrganisationType.ORGANISATION
? document.team?.teamEmail?.email || user.email
: user.email,
assetBaseUrl,
signDocumentLink,
customBody: renderCustomEmailTemplate(emailMessage, customEmailTemplate),
role: recipient.role,
selfSigner,
isTeamInvite: isTeamDocument,
organisationType,
teamName: document.team?.name,
});
const branding = document.team?.teamGlobalSettings
? teamGlobalSettingsToBranding(document.team.teamGlobalSettings)
: undefined;
const [html, text] = await Promise.all([
renderEmailWithI18N(template, {
lang: document.documentMeta?.language,
lang,
branding,
}),
renderEmailWithI18N(template, {
lang: document.documentMeta?.language,
lang,
branding,
plainText: true,
}),

View File

@ -24,6 +24,7 @@ import { flattenForm } from '../pdf/flatten-form';
import { insertFieldInPDF } from '../pdf/insert-field-in-pdf';
import { legacy_insertFieldInPDF } from '../pdf/legacy-insert-field-in-pdf';
import { normalizeSignatureAppearances } from '../pdf/normalize-signature-appearances';
import { getTeamSettings } from '../team/get-team-settings';
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
import { sendCompletedEmail } from './send-completed-email';
@ -48,15 +49,6 @@ export const sealDocument = async ({
documentData: true,
documentMeta: true,
recipients: true,
team: {
select: {
teamGlobalSettings: {
select: {
includeSigningCertificate: true,
},
},
},
},
},
});
@ -66,6 +58,11 @@ export const sealDocument = async ({
throw new Error(`Document ${document.id} has no document data`);
}
const settings = await getTeamSettings({
userId: document.userId,
teamId: document.teamId,
});
const recipients = await prisma.recipient.findMany({
where: {
documentId: document.id,
@ -116,19 +113,18 @@ export const sealDocument = async ({
// !: Need to write the fields onto the document as a hard copy
const pdfData = await getFileServerSide(documentData);
const certificateData =
(document.team?.teamGlobalSettings?.includeSigningCertificate ?? true)
? await getCertificatePdf({
documentId,
language: document.documentMeta?.language,
}).catch(() => null)
: null;
const certificateData = settings.includeSigningCertificate
? await getCertificatePdf({
documentId,
language: document.documentMeta?.language,
}).catch(() => null)
: null;
const doc = await PDFDocument.load(pdfData);
// Normalize and flatten layers that could cause issues with the signature
normalizeSignatureAppearances(doc);
flattenForm(doc);
await flattenForm(doc);
flattenAnnotations(doc);
// Add rejection stamp if the document is rejected
@ -153,7 +149,7 @@ export const sealDocument = async ({
}
// Re-flatten post-insertion to handle fields that create arcoFields
flattenForm(doc);
await flattenForm(doc);
const pdfBytes = await doc.save();

View File

@ -3,7 +3,11 @@ import type { Document, Recipient, User } from '@prisma/client';
import { DocumentVisibility, TeamMemberRole } from '@prisma/client';
import { match } from 'ts-pattern';
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
import {
buildTeamWhereQuery,
formatDocumentsPath,
getHighestTeamRoleInGroup,
} from '@documenso/lib/utils/teams';
import { prisma } from '@documenso/prisma';
export type SearchDocumentsWithKeywordOptions = {
@ -84,16 +88,7 @@ export const searchDocumentsWithKeyword = async ({
contains: query,
mode: 'insensitive',
},
teamId: {
not: null,
},
team: {
members: {
some: {
userId: userId,
},
},
},
team: buildTeamWhereQuery({ teamId: undefined, userId }),
deletedAt: null,
},
{
@ -101,16 +96,7 @@ export const searchDocumentsWithKeyword = async ({
contains: query,
mode: 'insensitive',
},
teamId: {
not: null,
},
team: {
members: {
some: {
userId: userId,
},
},
},
team: buildTeamWhereQuery({ teamId: undefined, userId }),
deletedAt: null,
},
],
@ -120,12 +106,17 @@ export const searchDocumentsWithKeyword = async ({
team: {
select: {
url: true,
members: {
teamGroups: {
where: {
userId: userId,
},
select: {
role: true,
organisationGroup: {
organisationGroupMembers: {
some: {
organisationMember: {
userId,
},
},
},
},
},
},
},
@ -147,7 +138,9 @@ export const searchDocumentsWithKeyword = async ({
return true;
}
const teamMemberRole = document.team?.members[0]?.role;
const teamMemberRole = getHighestTeamRoleInGroup(
document.team.teamGroups.filter((tg) => tg.teamId === document.teamId),
);
if (!teamMemberRole) {
return false;

View File

@ -17,8 +17,8 @@ import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
import { env } from '../../utils/env';
import { renderCustomEmailTemplate } from '../../utils/render-custom-email-template';
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
import { teamGlobalSettingsToBranding } from '../../utils/team-global-settings-to-branding';
import { formatDocumentsPath } from '../../utils/teams';
import { getEmailContext } from '../email/get-email-context';
export interface SendDocumentOptions {
documentId: number;
@ -39,7 +39,6 @@ export const sendCompletedEmail = async ({ documentId, requestMetadata }: SendDo
select: {
id: true,
url: true,
teamGlobalSettings: true,
},
},
},
@ -55,6 +54,13 @@ export const sendCompletedEmail = async ({ documentId, requestMetadata }: SendDo
throw new Error('Document has no recipients');
}
const { branding, settings } = await getEmailContext({
source: {
type: 'team',
teamId: document.teamId,
},
});
const { user: owner } = document;
const completedDocument = await getFileServerSide(document.documentData);
@ -71,8 +77,6 @@ export const sendCompletedEmail = async ({ documentId, requestMetadata }: SendDo
}`;
}
const i18n = await getI18nInstance(document.documentMeta?.language);
const emailSettings = extractDerivedDocumentEmailSettings(document.documentMeta);
const isDocumentCompletedEmailEnabled = emailSettings.documentCompleted;
const isOwnerDocumentCompletedEmailEnabled = emailSettings.ownerDocumentCompleted;
@ -93,19 +97,19 @@ export const sendCompletedEmail = async ({ documentId, requestMetadata }: SendDo
downloadLink: documentOwnerDownloadLink,
});
const branding = document.team?.teamGlobalSettings
? teamGlobalSettingsToBranding(document.team.teamGlobalSettings)
: undefined;
const lang = document.documentMeta?.language ?? settings.documentLanguage;
const [html, text] = await Promise.all([
renderEmailWithI18N(template, { lang: document.documentMeta?.language, branding }),
renderEmailWithI18N(template, { lang, branding }),
renderEmailWithI18N(template, {
lang: document.documentMeta?.language,
lang,
branding,
plainText: true,
}),
]);
const i18n = await getI18nInstance(lang);
await mailer.sendMail({
to: [
{
@ -170,19 +174,19 @@ export const sendCompletedEmail = async ({ documentId, requestMetadata }: SendDo
: undefined,
});
const branding = document.team?.teamGlobalSettings
? teamGlobalSettingsToBranding(document.team.teamGlobalSettings)
: undefined;
const lang = document.documentMeta?.language ?? settings.documentLanguage;
const [html, text] = await Promise.all([
renderEmailWithI18N(template, { lang: document.documentMeta?.language, branding }),
renderEmailWithI18N(template, { lang, branding }),
renderEmailWithI18N(template, {
lang: document.documentMeta?.language,
lang,
branding,
plainText: true,
}),
]);
const i18n = await getI18nInstance(lang);
await mailer.sendMail({
to: [
{

View File

@ -12,7 +12,7 @@ import { AppError, AppErrorCode } from '../../errors/app-error';
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
import { env } from '../../utils/env';
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
import { teamGlobalSettingsToBranding } from '../../utils/team-global-settings-to-branding';
import { getEmailContext } from '../email/get-email-context';
export interface SendDeleteEmailOptions {
documentId: number;
@ -27,11 +27,6 @@ export const sendDeleteEmail = async ({ documentId, reason }: SendDeleteEmailOpt
include: {
user: true,
documentMeta: true,
team: {
include: {
teamGlobalSettings: true,
},
},
},
});
@ -49,6 +44,13 @@ export const sendDeleteEmail = async ({ documentId, reason }: SendDeleteEmailOpt
return;
}
const { branding, settings } = await getEmailContext({
source: {
type: 'team',
teamId: document.teamId,
},
});
const { email, name } = document.user;
const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000';
@ -59,20 +61,18 @@ export const sendDeleteEmail = async ({ documentId, reason }: SendDeleteEmailOpt
assetBaseUrl,
});
const branding = document.team?.teamGlobalSettings
? teamGlobalSettingsToBranding(document.team.teamGlobalSettings)
: undefined;
const lang = document.documentMeta?.language ?? settings.documentLanguage;
const [html, text] = await Promise.all([
renderEmailWithI18N(template, { lang: document.documentMeta?.language, branding }),
renderEmailWithI18N(template, { lang, branding }),
renderEmailWithI18N(template, {
lang: document.documentMeta?.language,
lang,
branding,
plainText: true,
}),
]);
const i18n = await getI18nInstance();
const i18n = await getI18nInstance(lang);
await mailer.sendMail({
to: {

View File

@ -23,11 +23,12 @@ import { putPdfFileServerSide } from '../../universal/upload/put-file.server';
import { isDocumentCompleted } from '../../utils/document';
import { insertFormValuesInPdf } from '../pdf/insert-form-values-in-pdf';
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
import { getDocumentWhereInput } from './get-document-by-id';
export type SendDocumentOptions = {
documentId: number;
userId: number;
teamId?: number;
teamId: number;
sendEmail?: boolean;
requestMetadata: ApiRequestMetadata;
};
@ -39,25 +40,14 @@ export const sendDocument = async ({
sendEmail,
requestMetadata,
}: SendDocumentOptions) => {
const document = await prisma.document.findUnique({
where: {
id: documentId,
...(teamId
? {
team: {
id: teamId,
members: {
some: {
userId,
},
},
},
}
: {
userId,
teamId: null,
}),
},
const { documentWhereInput } = await getDocumentWhereInput({
documentId,
userId,
teamId,
});
const document = await prisma.document.findFirst({
where: documentWhereInput,
include: {
recipients: {
orderBy: [{ signingOrder: { sort: 'asc', nulls: 'last' } }, { id: 'asc' }],

View File

@ -11,7 +11,7 @@ import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
import { env } from '../../utils/env';
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
import { teamGlobalSettingsToBranding } from '../../utils/team-global-settings-to-branding';
import { getEmailContext } from '../email/get-email-context';
export interface SendPendingEmailOptions {
documentId: number;
@ -35,11 +35,6 @@ export const sendPendingEmail = async ({ documentId, recipientId }: SendPendingE
},
},
documentMeta: true,
team: {
include: {
teamGlobalSettings: true,
},
},
},
});
@ -51,6 +46,13 @@ export const sendPendingEmail = async ({ documentId, recipientId }: SendPendingE
throw new Error('Document has no recipients');
}
const { branding, settings } = await getEmailContext({
source: {
type: 'team',
teamId: document.teamId,
},
});
const isDocumentPendingEmailEnabled = extractDerivedDocumentEmailSettings(
document.documentMeta,
).documentPending;
@ -70,20 +72,18 @@ export const sendPendingEmail = async ({ documentId, recipientId }: SendPendingE
assetBaseUrl,
});
const branding = document.team?.teamGlobalSettings
? teamGlobalSettingsToBranding(document.team.teamGlobalSettings)
: undefined;
const lang = document.documentMeta?.language ?? settings.documentLanguage;
const [html, text] = await Promise.all([
renderEmailWithI18N(template, { lang: document.documentMeta?.language, branding }),
renderEmailWithI18N(template, { lang, branding }),
renderEmailWithI18N(template, {
lang: document.documentMeta?.language,
lang,
branding,
plainText: true,
}),
]);
const i18n = await getI18nInstance(document.documentMeta?.language);
const i18n = await getI18nInstance(lang);
await mailer.sendMail({
to: {

View File

@ -16,7 +16,7 @@ import { extractDerivedDocumentEmailSettings } from '../../types/document-email'
import type { RequestMetadata } from '../../universal/extract-request-metadata';
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
import { teamGlobalSettingsToBranding } from '../../utils/team-global-settings-to-branding';
import { getEmailContext } from '../email/get-email-context';
export type SuperDeleteDocumentOptions = {
id: number;
@ -32,11 +32,6 @@ export const superDeleteDocument = async ({ id, requestMetadata }: SuperDeleteDo
recipients: true,
documentMeta: true,
user: true,
team: {
include: {
teamGlobalSettings: true,
},
},
},
});
@ -46,6 +41,13 @@ export const superDeleteDocument = async ({ id, requestMetadata }: SuperDeleteDo
});
}
const { branding, settings } = await getEmailContext({
source: {
type: 'team',
teamId: document.teamId,
},
});
const { status, user } = document;
const isDocumentDeletedEmailEnabled = extractDerivedDocumentEmailSettings(
@ -72,20 +74,18 @@ export const superDeleteDocument = async ({ id, requestMetadata }: SuperDeleteDo
assetBaseUrl,
});
const branding = document.team?.teamGlobalSettings
? teamGlobalSettingsToBranding(document.team.teamGlobalSettings)
: undefined;
const lang = document.documentMeta?.language ?? settings.documentLanguage;
const [html, text] = await Promise.all([
renderEmailWithI18N(template, { lang: document.documentMeta?.language, branding }),
renderEmailWithI18N(template, { lang, branding }),
renderEmailWithI18N(template, {
lang: document.documentMeta?.language,
lang,
branding,
plainText: true,
}),
]);
const i18n = await getI18nInstance(document.documentMeta?.language);
const i18n = await getI18nInstance(lang);
await mailer.sendMail({
to: {

View File

@ -2,7 +2,6 @@ import { DocumentVisibility } from '@prisma/client';
import { DocumentStatus, TeamMemberRole } from '@prisma/client';
import { match } from 'ts-pattern';
import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import type { CreateDocumentAuditLogDataResponse } from '@documenso/lib/utils/document-audit-logs';
@ -13,17 +12,18 @@ import type { Attachment } from '@documenso/prisma/generated/zod/modelSchema/Att
import { AppError, AppErrorCode } from '../../errors/app-error';
import type { TDocumentAccessAuthTypes, TDocumentActionAuthTypes } from '../../types/document-auth';
import { createDocumentAuthOptions, extractDocumentAuthMethods } from '../../utils/document-auth';
import { getDocumentWhereInput } from './get-document-by-id';
export type UpdateDocumentOptions = {
userId: number;
teamId?: number;
teamId: number;
documentId: number;
data?: {
title?: string;
externalId?: string | null;
visibility?: DocumentVisibility | null;
globalAccessAuth?: TDocumentAccessAuthTypes | null;
globalActionAuth?: TDocumentActionAuthTypes | null;
globalAccessAuth?: TDocumentAccessAuthTypes[];
globalActionAuth?: TDocumentActionAuthTypes[];
useLegacyFieldInsertion?: boolean;
attachments?: Pick<Attachment, 'id' | 'label' | 'url'>[];
};
@ -37,34 +37,20 @@ export const updateDocument = async ({
data,
requestMetadata,
}: UpdateDocumentOptions) => {
const { documentWhereInput, team } = await getDocumentWhereInput({
documentId,
userId,
teamId,
});
const document = await prisma.document.findFirst({
where: {
id: documentId,
...(teamId
? {
team: {
id: teamId,
members: {
some: {
userId,
},
},
},
}
: {
userId,
teamId: null,
}),
},
where: documentWhereInput,
include: {
team: {
select: {
members: {
where: {
userId,
},
organisation: {
select: {
role: true,
organisationClaim: true,
},
},
},
@ -79,50 +65,46 @@ export const updateDocument = async ({
});
}
if (teamId) {
const currentUserRole = document.team?.members[0]?.role;
const isDocumentOwner = document.userId === userId;
const requestedVisibility = data?.visibility;
const isDocumentOwner = document.userId === userId;
const requestedVisibility = data?.visibility;
if (!isDocumentOwner) {
match(currentUserRole)
.with(TeamMemberRole.ADMIN, () => true)
.with(TeamMemberRole.MANAGER, () => {
const allowedVisibilities: DocumentVisibility[] = [
DocumentVisibility.EVERYONE,
DocumentVisibility.MANAGER_AND_ABOVE,
];
if (!isDocumentOwner) {
match(team.currentTeamRole)
.with(TeamMemberRole.ADMIN, () => true)
.with(TeamMemberRole.MANAGER, () => {
const allowedVisibilities: DocumentVisibility[] = [
DocumentVisibility.EVERYONE,
DocumentVisibility.MANAGER_AND_ABOVE,
];
if (
!allowedVisibilities.includes(document.visibility) ||
(requestedVisibility && !allowedVisibilities.includes(requestedVisibility))
) {
throw new AppError(AppErrorCode.UNAUTHORIZED, {
message: 'You do not have permission to update the document visibility',
});
}
})
.with(TeamMemberRole.MEMBER, () => {
if (
document.visibility !== DocumentVisibility.EVERYONE ||
(requestedVisibility && requestedVisibility !== DocumentVisibility.EVERYONE)
) {
throw new AppError(AppErrorCode.UNAUTHORIZED, {
message: 'You do not have permission to update the document visibility',
});
}
})
.otherwise(() => {
if (
!allowedVisibilities.includes(document.visibility) ||
(requestedVisibility && !allowedVisibilities.includes(requestedVisibility))
) {
throw new AppError(AppErrorCode.UNAUTHORIZED, {
message: 'You do not have permission to update the document',
message: 'You do not have permission to update the document visibility',
});
}
})
.with(TeamMemberRole.MEMBER, () => {
if (
document.visibility !== DocumentVisibility.EVERYONE ||
(requestedVisibility && requestedVisibility !== DocumentVisibility.EVERYONE)
) {
throw new AppError(AppErrorCode.UNAUTHORIZED, {
message: 'You do not have permission to update the document visibility',
});
}
})
.otherwise(() => {
throw new AppError(AppErrorCode.UNAUTHORIZED, {
message: 'You do not have permission to update the document',
});
}
});
}
// If no data just return the document since this function is normally chained after a meta update.
if (!data || Object.values(data).length === 0) {
console.log('no data');
return document;
}
@ -140,17 +122,10 @@ export const updateDocument = async ({
data?.globalActionAuth === undefined ? documentGlobalActionAuth : data.globalActionAuth;
// Check if user has permission to set the global action auth.
if (newGlobalActionAuth) {
const isDocumentEnterprise = await isUserEnterprise({
userId,
teamId,
if (newGlobalActionAuth.length > 0 && !document.team.organisation.organisationClaim.flags.cfr21) {
throw new AppError(AppErrorCode.UNAUTHORIZED, {
message: 'You do not have permission to set the action auth',
});
if (!isDocumentEnterprise) {
throw new AppError(AppErrorCode.UNAUTHORIZED, {
message: 'You do not have permission to set the action auth',
});
}
}
const isTitleSame = data.title === undefined || data.title === document.title;

View File

@ -1,81 +0,0 @@
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
import type { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
import { prisma } from '@documenso/prisma';
export type UpdateTitleOptions = {
userId: number;
teamId?: number;
documentId: number;
title: string;
requestMetadata?: RequestMetadata;
};
export const updateTitle = async ({
userId,
teamId,
documentId,
title,
requestMetadata,
}: UpdateTitleOptions) => {
const user = await prisma.user.findFirstOrThrow({
where: {
id: userId,
},
});
const document = await prisma.document.findFirstOrThrow({
where: {
id: documentId,
...(teamId
? {
team: {
id: teamId,
members: {
some: {
userId,
},
},
},
}
: {
userId,
teamId: null,
}),
},
});
if (document.title === title) {
return document;
}
return await prisma.$transaction(async (tx) => {
// Instead of doing everything in a transaction we can use our knowledge
// of the current document title to ensure we aren't performing a conflicting
// update.
const updatedDocument = await tx.document.update({
where: {
id: documentId,
title: document.title,
},
data: {
title,
},
});
await tx.documentAuditLog.create({
data: createDocumentAuditLogData({
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_TITLE_UPDATED,
documentId,
user,
requestMetadata,
data: {
from: document.title,
to: updatedDocument.title,
},
}),
});
return updatedDocument;
});
};

View File

@ -3,7 +3,6 @@ import { FieldType } from '@prisma/client';
import { AppError, AppErrorCode } from '../../errors/app-error';
import type { TRecipientActionAuth } from '../../types/document-auth';
import { extractDocumentAuthMethods } from '../../utils/document-auth';
import { isRecipientAuthorized } from './is-recipient-authorized';
export type ValidateFieldAuthOptions = {
@ -26,14 +25,9 @@ export const validateFieldAuth = async ({
userId,
authOptions,
}: ValidateFieldAuthOptions) => {
const { derivedRecipientActionAuth } = extractDocumentAuthMethods({
documentAuth: documentAuthOptions,
recipientAuth: recipient.authOptions,
});
// Override all non-signature fields to not require any auth.
if (field.type !== FieldType.SIGNATURE) {
return null;
return undefined;
}
const isValid = await isRecipientAuthorized({
@ -50,5 +44,5 @@ export const validateFieldAuth = async ({
});
}
return derivedRecipientActionAuth;
return authOptions?.type;
};

View File

@ -15,7 +15,7 @@ import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
export type ViewedDocumentOptions = {
token: string;
recipientAccessAuth?: TDocumentAccessAuthTypes | null;
recipientAccessAuth?: TDocumentAccessAuthTypes[];
requestMetadata?: RequestMetadata;
};
@ -63,7 +63,7 @@ export const viewedDocument = async ({
recipientId: recipient.id,
recipientName: recipient.name,
recipientRole: recipient.role,
accessAuth: recipientAccessAuth || undefined,
accessAuth: recipientAccessAuth ?? [],
},
}),
});