fix: merge conflicts

This commit is contained in:
Ephraim Atta-Duncan
2024-09-21 09:07:16 +00:00
449 changed files with 28724 additions and 4841 deletions

View File

@ -2,25 +2,33 @@ import { prisma } from '@documenso/prisma';
import type { User } from '@documenso/prisma/client';
import { UserSecurityAuditLogType } from '@documenso/prisma/client';
import { AppError } from '../../errors/app-error';
import { AppError, AppErrorCode } from '../../errors/app-error';
import type { RequestMetadata } from '../../universal/extract-request-metadata';
import { validateTwoFactorAuthentication } from './validate-2fa';
type DisableTwoFactorAuthenticationOptions = {
user: User;
token: string;
totpCode?: string;
backupCode?: string;
requestMetadata?: RequestMetadata;
};
export const disableTwoFactorAuthentication = async ({
token,
totpCode,
backupCode,
user,
requestMetadata,
}: DisableTwoFactorAuthenticationOptions) => {
let isValid = await validateTwoFactorAuthentication({ totpCode: token, user });
let isValid = false;
if (!isValid) {
isValid = await validateTwoFactorAuthentication({ backupCode: token, user });
if (!totpCode && !backupCode) {
throw new AppError(AppErrorCode.INVALID_REQUEST);
}
if (totpCode) {
isValid = await validateTwoFactorAuthentication({ totpCode, user });
} else if (backupCode) {
isValid = await validateTwoFactorAuthentication({ backupCode, user });
}
if (!isValid) {

View File

@ -7,6 +7,7 @@ import {
diffDocumentMetaChanges,
} from '@documenso/lib/utils/document-audit-logs';
import { prisma } from '@documenso/prisma';
import type { DocumentSigningOrder } from '@documenso/prisma/client';
export type CreateDocumentMetaOptions = {
documentId: number;
@ -16,6 +17,7 @@ export type CreateDocumentMetaOptions = {
password?: string;
dateFormat?: string;
redirectUrl?: string;
signingOrder?: DocumentSigningOrder;
userId: number;
requestMetadata: RequestMetadata;
};
@ -29,6 +31,7 @@ export const upsertDocumentMeta = async ({
password,
userId,
redirectUrl,
signingOrder,
requestMetadata,
}: CreateDocumentMetaOptions) => {
const user = await prisma.user.findFirstOrThrow({
@ -78,6 +81,7 @@ export const upsertDocumentMeta = async ({
timezone,
documentId,
redirectUrl,
signingOrder,
},
update: {
subject,
@ -86,6 +90,7 @@ export const upsertDocumentMeta = async ({
dateFormat,
timezone,
redirectUrl,
signingOrder,
},
});

View File

@ -2,11 +2,18 @@ import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-log
import type { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
import { prisma } from '@documenso/prisma';
import { DocumentStatus, SigningStatus } from '@documenso/prisma/client';
import {
DocumentSigningOrder,
DocumentStatus,
RecipientRole,
SendStatus,
SigningStatus,
} from '@documenso/prisma/client';
import { WebhookTriggerEvents } from '@documenso/prisma/client';
import { jobs } from '../../jobs/client';
import type { TRecipientActionAuth } from '../../types/document-auth';
import { getIsRecipientsTurnToSign } from '../recipient/get-is-recipient-turn';
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
import { sendPendingEmail } from './send-pending-email';
@ -29,6 +36,7 @@ const getDocument = async ({ token, documentId }: CompleteDocumentWithTokenOptio
},
},
include: {
documentMeta: true,
Recipient: {
where: {
token,
@ -59,6 +67,16 @@ export const completeDocumentWithToken = async ({
throw new Error(`Recipient ${recipient.id} has already signed`);
}
if (document.documentMeta?.signingOrder === DocumentSigningOrder.SEQUENTIAL) {
const isRecipientsTurn = await getIsRecipientsTurnToSign({ token: recipient.token });
if (!isRecipientsTurn) {
throw new Error(
`Recipient ${recipient.id} attempted to complete the document before it was their turn`,
);
}
}
const fields = await prisma.field.findMany({
where: {
documentId: document.id,
@ -120,17 +138,48 @@ export const completeDocumentWithToken = async ({
});
});
const pendingRecipients = await prisma.recipient.count({
const pendingRecipients = await prisma.recipient.findMany({
select: {
id: true,
signingOrder: true,
},
where: {
documentId: document.id,
signingStatus: {
not: SigningStatus.SIGNED,
},
role: {
not: RecipientRole.CC,
},
},
// Composite sort so our next recipient is always the one with the lowest signing order or id
// if there is a tie.
orderBy: [{ signingOrder: { sort: 'asc', nulls: 'last' } }, { id: 'asc' }],
});
if (pendingRecipients > 0) {
if (pendingRecipients.length > 0) {
await sendPendingEmail({ documentId, recipientId: recipient.id });
if (document.documentMeta?.signingOrder === DocumentSigningOrder.SEQUENTIAL) {
const [nextRecipient] = pendingRecipients;
await prisma.$transaction(async (tx) => {
await tx.recipient.update({
where: { id: nextRecipient.id },
data: { sendStatus: SendStatus.SENT },
});
await jobs.triggerJob({
name: 'send.signing.requested.email',
payload: {
userId: document.userId,
documentId: document.id,
recipientId: nextRecipient.id,
requestMetadata,
},
});
});
}
}
const haveAllRecipientsSigned = await prisma.document.findFirst({
@ -138,7 +187,7 @@ export const completeDocumentWithToken = async ({
id: document.id,
Recipient: {
every: {
signingStatus: SigningStatus.SIGNED,
OR: [{ signingStatus: SigningStatus.SIGNED }, { role: RecipientRole.CC }],
},
},
},

View File

@ -2,10 +2,11 @@ import { DateTime } from 'luxon';
import { P, match } from 'ts-pattern';
import { prisma } from '@documenso/prisma';
import { Prisma, RecipientRole, SigningStatus } from '@documenso/prisma/client';
import { Prisma, RecipientRole, SigningStatus, TeamMemberRole } from '@documenso/prisma/client';
import type { Document, Team, TeamEmail, User } from '@documenso/prisma/client';
import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status';
import { DocumentVisibility } from '../../types/document-visibility';
import type { FindResultSet } from '../../types/find-result-set';
import { maskRecipientTokensForDocument } from '../../utils/mask-recipient-tokens-for-document';
@ -48,9 +49,23 @@ export const findDocuments = async ({
team = await tx.team.findFirstOrThrow({
where: {
id: teamId,
members: { some: { userId } },
members: {
some: {
userId,
},
},
},
include: {
teamEmail: true,
members: {
where: {
userId,
},
select: {
role: true,
},
},
},
include: { teamEmail: true },
});
}
@ -59,6 +74,7 @@ export const findDocuments = async ({
const orderByColumn = orderBy?.column ?? 'createdAt';
const orderByDirection = orderBy?.direction ?? 'desc';
const teamMemberRole = team?.members[0].role ?? null;
const termFilters = match(term)
.with(P.string.minLength(1), () => ({
@ -69,7 +85,37 @@ export const findDocuments = async ({
}))
.otherwise(() => undefined);
const filters = team ? findTeamDocumentsFilter(status, team) : findDocumentsFilter(status, user);
const visibilityFilters = [
match(teamMemberRole)
.with(TeamMemberRole.ADMIN, () => ({
visibility: {
in: [
DocumentVisibility.EVERYONE,
DocumentVisibility.MANAGER_AND_ABOVE,
DocumentVisibility.ADMIN,
],
},
}))
.with(TeamMemberRole.MANAGER, () => ({
visibility: {
in: [DocumentVisibility.EVERYONE, DocumentVisibility.MANAGER_AND_ABOVE],
},
}))
.otherwise(() => ({ visibility: DocumentVisibility.EVERYONE })),
{
Recipient: {
some: {
email: user.email,
},
},
},
];
let filters: Prisma.DocumentWhereInput | null = findDocumentsFilter(status, user);
if (team) {
filters = findTeamDocumentsFilter(status, team, visibilityFilters);
}
if (filters === null) {
return {
@ -309,6 +355,7 @@ const findDocumentsFilter = (status: ExtendedDocumentStatus, user: User) => {
const findTeamDocumentsFilter = (
status: ExtendedDocumentStatus,
team: Team & { teamEmail: TeamEmail | null },
visibilityFilters: Prisma.DocumentWhereInput[],
) => {
const teamEmail = team.teamEmail?.email ?? null;
@ -319,6 +366,7 @@ const findTeamDocumentsFilter = (
{
teamId: team.id,
deletedAt: null,
OR: visibilityFilters,
},
],
};
@ -334,6 +382,7 @@ const findTeamDocumentsFilter = (
},
},
deletedAt: null,
OR: visibilityFilters,
});
filter.OR.push({
@ -341,6 +390,7 @@ const findTeamDocumentsFilter = (
email: teamEmail,
},
deletedAt: null,
OR: visibilityFilters,
});
}
@ -365,6 +415,7 @@ const findTeamDocumentsFilter = (
},
},
deletedAt: null,
OR: visibilityFilters,
};
})
.with(ExtendedDocumentStatus.DRAFT, () => {
@ -374,6 +425,7 @@ const findTeamDocumentsFilter = (
teamId: team.id,
status: ExtendedDocumentStatus.DRAFT,
deletedAt: null,
OR: visibilityFilters,
},
],
};
@ -385,6 +437,7 @@ const findTeamDocumentsFilter = (
email: teamEmail,
},
deletedAt: null,
OR: visibilityFilters,
});
}
@ -397,6 +450,7 @@ const findTeamDocumentsFilter = (
teamId: team.id,
status: ExtendedDocumentStatus.PENDING,
deletedAt: null,
OR: visibilityFilters,
},
],
};
@ -415,11 +469,13 @@ const findTeamDocumentsFilter = (
},
},
},
OR: visibilityFilters,
},
{
User: {
email: teamEmail,
},
OR: visibilityFilters,
},
],
deletedAt: null,
@ -435,6 +491,7 @@ const findTeamDocumentsFilter = (
OR: [
{
teamId: team.id,
OR: visibilityFilters,
},
],
};
@ -447,11 +504,13 @@ const findTeamDocumentsFilter = (
email: teamEmail,
},
},
OR: visibilityFilters,
},
{
User: {
email: teamEmail,
},
OR: visibilityFilters,
},
);
}

View File

@ -1,6 +1,10 @@
import { match } from 'ts-pattern';
import { prisma } from '@documenso/prisma';
import type { Prisma } from '@documenso/prisma/client';
import { TeamMemberRole } from '@documenso/prisma/client';
import { DocumentVisibility } from '../../types/document-visibility';
import { getTeamById } from '../team/get-team';
export type GetDocumentByIdOptions = {
@ -28,6 +32,11 @@ export const getDocumentById = async ({ id, userId, teamId }: GetDocumentByIdOpt
email: true,
},
},
Recipient: {
select: {
email: true,
},
},
team: {
select: {
id: true,
@ -115,5 +124,35 @@ export const getDocumentWhereInput = async ({
);
}
return documentWhereInput;
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 }]),
{
Recipient: {
some: {
email: user.email,
},
},
},
];
return {
...documentWhereInput,
OR: [...visibilityFilters],
};
};

View File

@ -147,7 +147,7 @@ export const getDocumentAndRecipientByToken = async ({
},
});
const recipient = result.Recipient[0];
const [recipient] = result.Recipient;
// Sanity check, should not be possible.
if (!recipient) {

View File

@ -1,12 +1,16 @@
import { DateTime } from 'luxon';
import { match } from 'ts-pattern';
import type { PeriodSelectorValue } from '@documenso/lib/server-only/document/find-documents';
import { prisma } from '@documenso/prisma';
import { TeamMemberRole } from '@documenso/prisma/client';
import type { Prisma, User } from '@documenso/prisma/client';
import { SigningStatus } from '@documenso/prisma/client';
import { isExtendedDocumentStatus } from '@documenso/prisma/guards/is-extended-document-status';
import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status';
import { DocumentVisibility } from '../../types/document-visibility';
export type GetStatsInput = {
user: User;
team?: Omit<GetTeamCountsOption, 'createdAt'>;
@ -27,7 +31,7 @@ export const getStats = async ({ user, period, ...options }: GetStatsInput) => {
}
const [ownerCounts, notSignedCounts, hasSignedCounts, deletedCounts] = await (options.team
? getTeamCounts({ ...options.team, createdAt })
? getTeamCounts({ ...options.team, createdAt, currentUserEmail: user.email, userId: user.id })
: getCounts({ user, createdAt }));
const stats: Record<ExtendedDocumentStatus, number> = {
@ -194,11 +198,21 @@ type GetTeamCountsOption = {
teamId: number;
teamEmail?: string;
senderIds?: number[];
currentUserEmail: string;
userId: number;
createdAt: Prisma.DocumentWhereInput['createdAt'];
currentTeamMemberRole?: TeamMemberRole;
};
const getTeamCounts = async (options: GetTeamCountsOption) => {
const { createdAt, teamId, teamEmail, senderIds = [] } = options;
const {
createdAt,
teamId,
teamEmail,
senderIds = [],
currentUserEmail,
currentTeamMemberRole,
} = options;
const userIdWhereClause: Prisma.DocumentWhereInput['userId'] =
senderIds.length > 0
@ -207,10 +221,50 @@ const getTeamCounts = async (options: GetTeamCountsOption) => {
}
: undefined;
const visibilityFilters = [
...match(currentTeamMemberRole)
.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 }]),
];
const ownerCountsWhereInput: Prisma.DocumentWhereInput = {
userId: userIdWhereClause,
createdAt,
OR: [{ teamId }, ...(teamEmail ? [{ User: { email: teamEmail } }] : [])],
OR: [
{ teamId },
...(teamEmail ? [{ User: { email: teamEmail } }] : []),
{
AND: [
{
visibility: {
in: visibilityFilters.map((filter) => filter.visibility),
},
},
{
Recipient: {
none: {
email: currentUserEmail,
},
},
},
],
},
{
Recipient: {
some: {
email: currentUserEmail,
},
},
},
],
deletedAt: null,
};
@ -246,9 +300,6 @@ const getTeamCounts = async (options: GetTeamCountsOption) => {
status: {
in: [ExtendedDocumentStatus.PENDING, ExtendedDocumentStatus.COMPLETED],
},
deletedAt: {
gte: DateTime.now().minus({ days: 30 }).startOf('day').toJSDate(),
},
},
...(teamEmail
? [
@ -257,17 +308,12 @@ const getTeamCounts = async (options: GetTeamCountsOption) => {
some: {
email: teamEmail,
signingStatus: SigningStatus.SIGNED,
documentDeletedAt: {
gte: DateTime.now().minus({ days: 30 }).startOf('day').toJSDate(),
},
documentDeletedAt: null,
},
},
status: {
in: [ExtendedDocumentStatus.PENDING, ExtendedDocumentStatus.COMPLETED],
},
deletedAt: {
gte: DateTime.now().minus({ days: 30 }).startOf('day').toJSDate(),
},
},
]
: []),

View File

@ -5,7 +5,7 @@ import { render } from '@documenso/email/render';
import { DocumentInviteEmailTemplate } from '@documenso/email/templates/document-invite';
import { FROM_ADDRESS, FROM_NAME } from '@documenso/lib/constants/email';
import {
RECIPIENT_ROLES_DESCRIPTION,
RECIPIENT_ROLES_DESCRIPTION_ENG,
RECIPIENT_ROLE_TO_EMAIL_TYPE,
} from '@documenso/lib/constants/recipient-roles';
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
@ -97,8 +97,8 @@ export const resendDocument = async ({
const { email, name } = recipient;
const selfSigner = email === user.email;
const { actionVerb } = RECIPIENT_ROLES_DESCRIPTION[recipient.role];
const recipientActionVerb = actionVerb.toLowerCase();
const recipientActionVerb =
RECIPIENT_ROLES_DESCRIPTION_ENG[recipient.role].actionVerb.toLowerCase();
let emailMessage = customEmail?.message || '';
let emailSubject = `Reminder: Please ${recipientActionVerb} this document`;

View File

@ -3,7 +3,13 @@ import type { RequestMetadata } from '@documenso/lib/universal/extract-request-m
import { putPdfFile } from '@documenso/lib/universal/upload/put-file';
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
import { prisma } from '@documenso/prisma';
import { DocumentStatus, RecipientRole, SendStatus, SigningStatus } from '@documenso/prisma/client';
import {
DocumentSigningOrder,
DocumentStatus,
RecipientRole,
SendStatus,
SigningStatus,
} from '@documenso/prisma/client';
import { WebhookTriggerEvents } from '@documenso/prisma/client';
import { jobs } from '../../jobs/client';
@ -57,7 +63,9 @@ export const sendDocument = async ({
}),
},
include: {
Recipient: true,
Recipient: {
orderBy: [{ signingOrder: { sort: 'asc', nulls: 'last' } }, { id: 'asc' }],
},
documentMeta: true,
documentData: true,
},
@ -75,6 +83,21 @@ export const sendDocument = async ({
throw new Error('Can not send completed document');
}
const signingOrder = document.documentMeta?.signingOrder || DocumentSigningOrder.PARALLEL;
let recipientsToNotify = document.Recipient;
if (signingOrder === DocumentSigningOrder.SEQUENTIAL) {
// Get the currently active recipient.
recipientsToNotify = document.Recipient.filter(
(r) => r.signingStatus === SigningStatus.NOT_SIGNED && r.role !== RecipientRole.CC,
).slice(0, 1);
// Secondary filter so we aren't resending if the current active recipient has already
// received the document.
recipientsToNotify.filter((r) => r.sendStatus !== SendStatus.SENT);
}
const { documentData } = document;
if (!documentData.data) {
@ -135,7 +158,7 @@ export const sendDocument = async ({
if (sendEmail) {
await Promise.all(
document.Recipient.map(async (recipient) => {
recipientsToNotify.map(async (recipient) => {
if (recipient.sendStatus === SendStatus.SENT || recipient.role === RecipientRole.CC) {
return;
}

View File

@ -6,6 +6,7 @@ import type { RequestMetadata } from '@documenso/lib/universal/extract-request-m
import type { CreateDocumentAuditLogDataResponse } from '@documenso/lib/utils/document-audit-logs';
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
import { prisma } from '@documenso/prisma';
import type { DocumentVisibility } from '@documenso/prisma/client';
import { DocumentStatus } from '@documenso/prisma/client';
import { AppError, AppErrorCode } from '../../errors/app-error';
@ -19,6 +20,7 @@ export type UpdateDocumentSettingsOptions = {
data: {
title?: string;
externalId?: string | null;
visibility?: string | null;
globalAccessAuth?: TDocumentAccessAuthTypes | null;
globalActionAuth?: TDocumentActionAuthTypes | null;
};
@ -91,10 +93,14 @@ export const updateDocumentSettings = async ({
}
}
const isTitleSame = data.title === document.title;
const isExternalIdSame = data.externalId === document.externalId;
const isGlobalAccessSame = documentGlobalAccessAuth === newGlobalAccessAuth;
const isGlobalActionSame = documentGlobalActionAuth === newGlobalActionAuth;
const isTitleSame = data.title === undefined || data.title === document.title;
const isExternalIdSame = data.externalId === undefined || data.externalId === document.externalId;
const isGlobalAccessSame =
documentGlobalAccessAuth === undefined || documentGlobalAccessAuth === newGlobalAccessAuth;
const isGlobalActionSame =
documentGlobalActionAuth === undefined || documentGlobalActionAuth === newGlobalActionAuth;
const isDocumentVisibilitySame =
data.visibility === undefined || data.visibility === document.visibility;
const auditLogs: CreateDocumentAuditLogDataResponse[] = [];
@ -165,6 +171,21 @@ export const updateDocumentSettings = async ({
);
}
if (!isDocumentVisibilitySame) {
auditLogs.push(
createDocumentAuditLogData({
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_VISIBILITY_UPDATED,
documentId,
user,
requestMetadata,
data: {
from: document.visibility,
to: data.visibility || '',
},
}),
);
}
// Early return if nothing is required.
if (auditLogs.length === 0) {
return document;
@ -182,7 +203,8 @@ export const updateDocumentSettings = async ({
},
data: {
title: data.title,
externalId: data.externalId || null,
externalId: data.externalId,
visibility: data.visibility as DocumentVisibility,
authOptions,
},
});

View File

@ -5,7 +5,11 @@ import { getToken } from 'next-auth/jwt';
import { LOCAL_FEATURE_FLAGS } from '@documenso/lib/constants/feature-flags';
import PostHogServerClient from '@documenso/lib/server-only/feature-flags/get-post-hog-server-client';
import { NEXT_PUBLIC_MARKETING_URL, NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
import {
NEXT_PRIVATE_INTERNAL_WEBAPP_URL,
NEXT_PUBLIC_MARKETING_URL,
NEXT_PUBLIC_WEBAPP_URL,
} from '../../constants/app';
import { extractDistinctUserId, mapJwtToFlagProperties } from './get';
/**
@ -46,6 +50,10 @@ export default async function handlerFeatureFlagAll(req: Request) {
if (origin.startsWith(NEXT_PUBLIC_MARKETING_URL() ?? 'http://localhost:3001')) {
res.headers.set('Access-Control-Allow-Origin', origin);
}
if (origin.startsWith(NEXT_PRIVATE_INTERNAL_WEBAPP_URL ?? 'http://localhost:3000')) {
res.headers.set('Access-Control-Allow-Origin', origin);
}
}
return res;

View File

@ -7,7 +7,11 @@ import { getToken } from 'next-auth/jwt';
import { LOCAL_FEATURE_FLAGS, extractPostHogConfig } from '@documenso/lib/constants/feature-flags';
import PostHogServerClient from '@documenso/lib/server-only/feature-flags/get-post-hog-server-client';
import { NEXT_PUBLIC_MARKETING_URL, NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
import {
NEXT_PRIVATE_INTERNAL_WEBAPP_URL,
NEXT_PUBLIC_MARKETING_URL,
NEXT_PUBLIC_WEBAPP_URL,
} from '../../constants/app';
/**
* Evaluate a single feature flag based on the current user if possible.
@ -67,6 +71,10 @@ export default async function handleFeatureFlagGet(req: Request) {
if (origin.startsWith(NEXT_PUBLIC_MARKETING_URL() ?? 'http://localhost:3001')) {
res.headers.set('Access-Control-Allow-Origin', origin);
}
if (origin.startsWith(NEXT_PRIVATE_INTERNAL_WEBAPP_URL ?? 'http://localhost:3000')) {
res.headers.set('Access-Control-Allow-Origin', origin);
}
}
return res;

View File

@ -1,6 +1,16 @@
import { match } from 'ts-pattern';
import { prisma } from '@documenso/prisma';
import type { FieldType, Team } from '@documenso/prisma/client';
import {
ZCheckboxFieldMeta,
ZDropdownFieldMeta,
ZNumberFieldMeta,
ZRadioFieldMeta,
ZTextFieldMeta,
} from '../../types/field-meta';
import type { TFieldMetaSchema as FieldMeta } from '../../types/field-meta';
import type { RequestMetadata } from '../../universal/extract-request-metadata';
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
@ -15,6 +25,7 @@ export type CreateFieldOptions = {
pageY: number;
pageWidth: number;
pageHeight: number;
fieldMeta?: FieldMeta;
requestMetadata?: RequestMetadata;
};
@ -29,6 +40,7 @@ export const createField = async ({
pageY,
pageWidth,
pageHeight,
fieldMeta,
requestMetadata,
}: CreateFieldOptions) => {
const document = await prisma.document.findFirst({
@ -85,6 +97,39 @@ export const createField = async ({
});
}
const advancedField = ['NUMBER', 'RADIO', 'CHECKBOX', 'DROPDOWN', 'TEXT'].includes(type);
if (advancedField && !fieldMeta) {
throw new Error(
'Field meta is required for this type of field. Please provide the appropriate field meta object.',
);
}
if (fieldMeta && fieldMeta.type.toLowerCase() !== String(type).toLowerCase()) {
throw new Error('Field meta type does not match the field type');
}
const result = match(type)
.with('RADIO', () => ZRadioFieldMeta.safeParse(fieldMeta))
.with('CHECKBOX', () => ZCheckboxFieldMeta.safeParse(fieldMeta))
.with('DROPDOWN', () => ZDropdownFieldMeta.safeParse(fieldMeta))
.with('NUMBER', () => ZNumberFieldMeta.safeParse(fieldMeta))
.with('TEXT', () => ZTextFieldMeta.safeParse(fieldMeta))
.with('SIGNATURE', 'INITIALS', 'DATE', 'EMAIL', 'NAME', () => ({
success: true,
data: {},
}))
.with('FREE_SIGNATURE', () => ({
success: false,
error: 'FREE_SIGNATURE is not supported',
data: {},
}))
.exhaustive();
if (!result.success) {
throw new Error('Field meta parsing failed');
}
const field = await prisma.field.create({
data: {
documentId,
@ -97,6 +142,7 @@ export const createField = async ({
height: pageHeight,
customText: '',
inserted: false,
fieldMeta: result.data,
},
include: {
Recipient: true,

View File

@ -1,3 +1,5 @@
import { isDeepEqual } from 'remeda';
import { validateCheckboxField } from '@documenso/lib/advanced-fields-validation/validate-checkbox';
import { validateDropdownField } from '@documenso/lib/advanced-fields-validation/validate-dropdown';
import { validateNumberField } from '@documenso/lib/advanced-fields-validation/validate-number';
@ -20,22 +22,15 @@ import {
} from '@documenso/lib/utils/document-audit-logs';
import { prisma } from '@documenso/prisma';
import type { Field } from '@documenso/prisma/client';
import { FieldType, SendStatus, SigningStatus } from '@documenso/prisma/client';
import { FieldType } from '@documenso/prisma/client';
import { AppError, AppErrorCode } from '../../errors/app-error';
import { canRecipientFieldsBeModified } from '../../utils/recipients';
export interface SetFieldsForDocumentOptions {
userId: number;
documentId: number;
fields: {
id?: number | null;
type: FieldType;
signerEmail: string;
pageNumber: number;
pageX: number;
pageY: number;
pageWidth: number;
pageHeight: number;
fieldMeta?: FieldMeta;
}[];
fields: FieldData[];
requestMetadata?: RequestMetadata;
}
@ -63,6 +58,9 @@ export const setFieldsForDocument = async ({
},
],
},
include: {
Recipient: true,
},
});
const user = await prisma.user.findFirstOrThrow({
@ -97,21 +95,36 @@ export const setFieldsForDocument = async ({
(existingField) => !fields.find((field) => field.id === existingField.id),
);
const linkedFields = fields
.map((field) => {
const existing = existingFields.find((existingField) => existingField.id === field.id);
const linkedFields = fields.map((field) => {
const existing = existingFields.find((existingField) => existingField.id === field.id);
return {
...field,
_persisted: existing,
};
})
.filter((field) => {
return (
field._persisted?.Recipient?.sendStatus !== SendStatus.SENT &&
field._persisted?.Recipient?.signingStatus !== SigningStatus.SIGNED
const recipient = document.Recipient.find(
(recipient) => recipient.email.toLowerCase() === field.signerEmail.toLowerCase(),
);
// Each field MUST have a recipient associated with it.
if (!recipient) {
throw new AppError(AppErrorCode.INVALID_REQUEST, `Recipient not found for field ${field.id}`);
}
// Check whether the existing field can be modified.
if (
existing &&
hasFieldBeenChanged(existing, field) &&
!canRecipientFieldsBeModified(recipient, existingFields)
) {
throw new AppError(
AppErrorCode.INVALID_REQUEST,
'Cannot modify a field where the recipient has already interacted with the document',
);
});
}
return {
...field,
_persisted: existing,
_recipient: recipient,
};
});
const persistedFields = await prisma.$transaction(async (tx) => {
return await Promise.all(
@ -322,3 +335,33 @@ export const setFieldsForDocument = async ({
return [...filteredFields, ...persistedFields];
};
/**
* If you change this you MUST update the `hasFieldBeenChanged` function.
*/
type FieldData = {
id?: number | null;
type: FieldType;
signerEmail: string;
pageNumber: number;
pageX: number;
pageY: number;
pageWidth: number;
pageHeight: number;
fieldMeta?: FieldMeta;
};
const hasFieldBeenChanged = (field: Field, newFieldData: FieldData) => {
const currentFieldMeta = field.fieldMeta || null;
const newFieldMeta = newFieldData.fieldMeta || null;
return (
field.type !== newFieldData.type ||
field.page !== newFieldData.pageNumber ||
field.positionX.toNumber() !== newFieldData.pageX ||
field.positionY.toNumber() !== newFieldData.pageY ||
field.width.toNumber() !== newFieldData.pageWidth ||
field.height.toNumber() !== newFieldData.pageHeight ||
!isDeepEqual(currentFieldMeta, newFieldMeta)
);
};

View File

@ -107,7 +107,10 @@ export const setFieldsForTemplate = async ({
}
}
if (field.type === FieldType.CHECKBOX && field.fieldMeta) {
if (field.type === FieldType.CHECKBOX) {
if (!field.fieldMeta) {
throw new Error('Checkbox field is missing required metadata');
}
const checkboxFieldParsedMeta = ZCheckboxFieldMeta.parse(field.fieldMeta);
const errors = validateCheckboxField(
checkboxFieldParsedMeta?.values?.map((item) => item.value) ?? [],
@ -118,7 +121,10 @@ export const setFieldsForTemplate = async ({
}
}
if (field.type === FieldType.RADIO && field.fieldMeta) {
if (field.type === FieldType.RADIO) {
if (!field.fieldMeta) {
throw new Error('Radio field is missing required metadata');
}
const radioFieldParsedMeta = ZRadioFieldMeta.parse(field.fieldMeta);
const checkedRadioFieldValue = radioFieldParsedMeta.values?.find(
(option) => option.checked,
@ -129,7 +135,10 @@ export const setFieldsForTemplate = async ({
}
}
if (field.type === FieldType.DROPDOWN && field.fieldMeta) {
if (field.type === FieldType.DROPDOWN) {
if (!field.fieldMeta) {
throw new Error('Dropdown field is missing required metadata');
}
const dropdownFieldParsedMeta = ZDropdownFieldMeta.parse(field.fieldMeta);
const errors = validateDropdownField(undefined, dropdownFieldParsedMeta);
if (errors.length > 0) {

View File

@ -37,6 +37,10 @@ export const updateField = async ({
requestMetadata,
fieldMeta,
}: UpdateFieldOptions) => {
if (type === 'FREE_SIGNATURE') {
throw new Error('Cannot update a FREE_SIGNATURE field');
}
const oldField = await prisma.field.findFirstOrThrow({
where: {
id: fieldId,
@ -61,6 +65,11 @@ export const updateField = async ({
},
});
const newFieldMeta = {
...(oldField.fieldMeta as FieldMeta),
...fieldMeta,
};
const field = prisma.$transaction(async (tx) => {
const updatedField = await tx.field.update({
where: {
@ -74,13 +83,39 @@ export const updateField = async ({
positionY: pageY,
width: pageWidth,
height: pageHeight,
fieldMeta,
fieldMeta: newFieldMeta,
},
include: {
Recipient: true,
},
});
const user = await prisma.user.findFirstOrThrow({
where: {
id: userId,
},
select: {
id: true,
name: true,
email: true,
},
});
let team: Team | null = null;
if (teamId) {
team = await prisma.team.findFirst({
where: {
id: teamId,
members: {
some: {
userId,
},
},
},
});
}
await tx.documentAuditLog.create({
data: createDocumentAuditLogData({
type: DOCUMENT_AUDIT_LOG_TYPE.FIELD_UPDATED,
@ -104,31 +139,5 @@ export const updateField = async ({
return updatedField;
});
const user = await prisma.user.findFirstOrThrow({
where: {
id: userId,
},
select: {
id: true,
name: true,
email: true,
},
});
let team: Team | null = null;
if (teamId) {
team = await prisma.team.findFirst({
where: {
id: teamId,
members: {
some: {
userId,
},
},
},
});
}
return field;
};

View File

@ -1,11 +0,0 @@
import { headers } from 'next/headers';
export const getLocale = () => {
const headerItems = headers();
const locales = headerItems.get('accept-language') ?? 'en-US';
const [locale] = locales.split(',');
return locale;
};

View File

@ -4,5 +4,5 @@ import { cookies } from 'next/headers';
// eslint-disable-next-line @typescript-eslint/require-await
export const switchI18NLanguage = async (lang: string) => {
cookies().set('i18n', lang);
cookies().set('language', lang);
};

View File

@ -0,0 +1,46 @@
import { prisma } from '@documenso/prisma';
import { DocumentSigningOrder, SigningStatus } from '@documenso/prisma/client';
export type GetIsRecipientTurnOptions = {
token: string;
};
export async function getIsRecipientsTurnToSign({ token }: GetIsRecipientTurnOptions) {
const document = await prisma.document.findFirstOrThrow({
where: {
Recipient: {
some: {
token,
},
},
},
include: {
documentMeta: true,
Recipient: {
orderBy: {
signingOrder: 'asc',
},
},
},
});
if (document.documentMeta?.signingOrder !== DocumentSigningOrder.SEQUENTIAL) {
return true;
}
const recipients = document.Recipient;
const currentRecipientIndex = recipients.findIndex((r) => r.token === token);
if (currentRecipientIndex === -1) {
return false;
}
for (let i = 0; i < currentRecipientIndex; i++) {
if (recipients[i].signingStatus !== SigningStatus.SIGNED) {
return false;
}
}
return true;
}

View File

@ -1,4 +1,9 @@
import { createElement } from 'react';
import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
import { mailer } from '@documenso/email/mailer';
import { render } from '@documenso/email/render';
import RecipientRemovedFromDocumentTemplate from '@documenso/email/templates/recipient-removed-from-document';
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
import {
type TRecipientActionAuthTypes,
@ -16,19 +21,16 @@ import type { Recipient } from '@documenso/prisma/client';
import { RecipientRole } from '@documenso/prisma/client';
import { SendStatus, SigningStatus } from '@documenso/prisma/client';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
import { FROM_ADDRESS, FROM_NAME } from '../../constants/email';
import { AppError, AppErrorCode } from '../../errors/app-error';
import { canRecipientBeModified } from '../../utils/recipients';
export interface SetRecipientsForDocumentOptions {
userId: number;
teamId?: number;
documentId: number;
recipients: {
id?: number | null;
email: string;
name: string;
role: RecipientRole;
actionAuth?: TRecipientActionAuthTypes | null;
}[];
recipients: RecipientData[];
requestMetadata?: RequestMetadata;
}
@ -58,6 +60,9 @@ export const setRecipientsForDocument = async ({
teamId: null,
}),
},
include: {
Field: true,
},
});
const user = await prisma.user.findFirstOrThrow({
@ -115,25 +120,28 @@ export const setRecipientsForDocument = async ({
),
);
const linkedRecipients = normalizedRecipients
.map((recipient) => {
const existing = existingRecipients.find(
(existingRecipient) =>
existingRecipient.id === recipient.id || existingRecipient.email === recipient.email,
);
const linkedRecipients = normalizedRecipients.map((recipient) => {
const existing = existingRecipients.find(
(existingRecipient) =>
existingRecipient.id === recipient.id || existingRecipient.email === recipient.email,
);
return {
...recipient,
_persisted: existing,
};
})
.filter((recipient) => {
return (
recipient._persisted?.role === RecipientRole.CC ||
(recipient._persisted?.sendStatus !== SendStatus.SENT &&
recipient._persisted?.signingStatus !== SigningStatus.SIGNED)
if (
existing &&
hasRecipientBeenChanged(existing, recipient) &&
!canRecipientBeModified(existing, document.Field)
) {
throw new AppError(
AppErrorCode.INVALID_REQUEST,
'Cannot modify a recipient who has already interacted with the document',
);
});
}
return {
...recipient,
_persisted: existing,
};
});
const persistedRecipients = await prisma.$transaction(async (tx) => {
return await Promise.all(
@ -156,6 +164,7 @@ export const setRecipientsForDocument = async ({
name: recipient.name,
email: recipient.email,
role: recipient.role,
signingOrder: recipient.signingOrder,
documentId,
sendStatus: recipient.role === RecipientRole.CC ? SendStatus.SENT : SendStatus.NOT_SENT,
signingStatus:
@ -166,6 +175,7 @@ export const setRecipientsForDocument = async ({
name: recipient.name,
email: recipient.email,
role: recipient.role,
signingOrder: recipient.signingOrder,
token: nanoid(),
documentId,
sendStatus: recipient.role === RecipientRole.CC ? SendStatus.SENT : SendStatus.NOT_SENT,
@ -265,6 +275,37 @@ export const setRecipientsForDocument = async ({
),
});
});
// Send emails to deleted recipients.
await Promise.all(
removedRecipients.map(async (recipient) => {
if (recipient.sendStatus !== SendStatus.SENT) {
return;
}
const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000';
const template = createElement(RecipientRemovedFromDocumentTemplate, {
documentName: document.title,
inviterName: user.name || undefined,
assetBaseUrl,
});
await mailer.sendMail({
to: {
address: recipient.email,
name: recipient.name,
},
from: {
name: FROM_NAME,
address: FROM_ADDRESS,
},
subject: 'You have been removed from a document',
html: render(template),
text: render(template, { plainText: true }),
});
}),
);
}
// Filter out recipients that have been removed or have been updated.
@ -281,3 +322,27 @@ export const setRecipientsForDocument = async ({
return [...filteredRecipients, ...persistedRecipients];
};
/**
* If you change this you MUST update the `hasRecipientBeenChanged` function.
*/
type RecipientData = {
id?: number | null;
email: string;
name: string;
role: RecipientRole;
signingOrder?: number | null;
actionAuth?: TRecipientActionAuthTypes | null;
};
const hasRecipientBeenChanged = (recipient: Recipient, newRecipientData: RecipientData) => {
const authOptions = ZRecipientAuthOptionsSchema.parse(recipient.authOptions);
return (
recipient.email !== newRecipientData.email ||
recipient.name !== newRecipientData.name ||
recipient.role !== newRecipientData.role ||
recipient.signingOrder !== newRecipientData.signingOrder ||
authOptions.actionAuth !== newRecipientData.actionAuth
);
};

View File

@ -1,12 +1,12 @@
import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
import {
DIRECT_TEMPLATE_RECIPIENT_EMAIL,
DIRECT_TEMPLATE_RECIPIENT_NAME,
} from '@documenso/lib/constants/direct-templates';
import { prisma } from '@documenso/prisma';
import type { Recipient } from '@documenso/prisma/client';
import { RecipientRole } from '@documenso/prisma/client';
import {
DIRECT_TEMPLATE_RECIPIENT_EMAIL,
DIRECT_TEMPLATE_RECIPIENT_NAME,
} from '../../constants/template';
import { AppError, AppErrorCode } from '../../errors/app-error';
import {
type TRecipientActionAuthTypes,
@ -24,6 +24,7 @@ export type SetRecipientsForTemplateOptions = {
email: string;
name: string;
role: RecipientRole;
signingOrder?: number | null;
actionAuth?: TRecipientActionAuthTypes | null;
}[];
};
@ -162,6 +163,7 @@ export const setRecipientsForTemplate = async ({
name: recipient.name,
email: recipient.email,
role: recipient.role,
signingOrder: recipient.signingOrder,
templateId,
authOptions,
},
@ -169,6 +171,7 @@ export const setRecipientsForTemplate = async ({
name: recipient.name,
email: recipient.email,
role: recipient.role,
signingOrder: recipient.signingOrder,
token: nanoid(),
templateId,
authOptions,

View File

@ -1,9 +1,16 @@
import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
import { prisma } from '@documenso/prisma';
import type { RecipientRole, Team } from '@documenso/prisma/client';
import { AppError, AppErrorCode } from '../../errors/app-error';
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs';
import {
type TRecipientActionAuthTypes,
ZRecipientAuthOptionsSchema,
} from '../../types/document-auth';
import type { RequestMetadata } from '../../universal/extract-request-metadata';
import { createDocumentAuditLogData, diffRecipientChanges } from '../../utils/document-audit-logs';
import { createRecipientAuthOptions } from '../../utils/document-auth';
export type UpdateRecipientOptions = {
documentId: number;
@ -11,6 +18,8 @@ export type UpdateRecipientOptions = {
email?: string;
name?: string;
role?: RecipientRole;
signingOrder?: number | null;
actionAuth?: TRecipientActionAuthTypes | null;
userId: number;
teamId?: number;
requestMetadata?: RequestMetadata;
@ -22,6 +31,8 @@ export const updateRecipient = async ({
email,
name,
role,
signingOrder,
actionAuth,
userId,
teamId,
requestMetadata,
@ -48,6 +59,9 @@ export const updateRecipient = async ({
}),
},
},
include: {
Document: true,
},
});
let team: Team | null = null;
@ -75,6 +89,22 @@ export const updateRecipient = async ({
throw new Error('Recipient not found');
}
if (actionAuth) {
const isDocumentEnterprise = await isUserEnterprise({
userId,
teamId,
});
if (!isDocumentEnterprise) {
throw new AppError(
AppErrorCode.UNAUTHORIZED,
'You do not have permission to set the action auth',
);
}
}
const recipientAuthOptions = ZRecipientAuthOptionsSchema.parse(recipient.authOptions);
const updatedRecipient = await prisma.$transaction(async (tx) => {
const persisted = await prisma.recipient.update({
where: {
@ -84,6 +114,11 @@ export const updateRecipient = async ({
email: email?.toLowerCase() ?? recipient.email,
name: name ?? recipient.name,
role: role ?? recipient.role,
signingOrder,
authOptions: createRecipientAuthOptions({
accessAuth: recipientAuthOptions.accessAuth,
actionAuth: actionAuth ?? null,
}),
},
});

View File

@ -0,0 +1,21 @@
'use server';
import { prisma } from '@documenso/prisma';
import { SubscriptionStatus } from '@documenso/prisma/client';
export type GetActiveSubscriptionsByUserIdOptions = {
userId: number;
};
export const getActiveSubscriptionsByUserId = async ({
userId,
}: GetActiveSubscriptionsByUserIdOptions) => {
return await prisma.subscription.findMany({
where: {
userId,
status: {
not: SubscriptionStatus.INACTIVE,
},
},
});
};

View File

@ -1,6 +1,7 @@
import { updateSubscriptionItemQuantity } from '@documenso/ee/server-only/stripe/update-subscription-item-quantity';
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
import { prisma } from '@documenso/prisma';
import { TeamMemberInviteStatus } from '@documenso/prisma/client';
import { jobs } from '../../jobs/client';
@ -22,6 +23,9 @@ export const acceptTeamInvitation = async ({ userId, teamId }: AcceptTeamInvitat
where: {
teamId,
email: user.email,
status: {
not: TeamMemberInviteStatus.DECLINED,
},
},
include: {
team: {
@ -37,6 +41,10 @@ export const acceptTeamInvitation = async ({ userId, teamId }: AcceptTeamInvitat
},
});
if (teamMemberInvite.status === TeamMemberInviteStatus.ACCEPTED) {
return;
}
const { team } = teamMemberInvite;
const teamMember = await tx.teamMember.create({
@ -47,10 +55,13 @@ export const acceptTeamInvitation = async ({ userId, teamId }: AcceptTeamInvitat
},
});
await tx.teamMemberInvite.delete({
await tx.teamMemberInvite.update({
where: {
id: teamMemberInvite.id,
},
data: {
status: TeamMemberInviteStatus.ACCEPTED,
},
});
if (IS_BILLING_ENABLED() && team.subscription) {

View File

@ -6,6 +6,8 @@ export type GetTeamByIdOptions = {
teamId: number;
};
export type GetTeamResponse = Awaited<ReturnType<typeof getTeamById>>;
/**
* Get a team given a teamId.
*

View File

@ -28,11 +28,24 @@ export const transferTeamOwnership = async ({ token }: TransferTeamOwnershipOpti
const { team, userId: newOwnerUserId } = teamTransferVerification;
await tx.teamTransferVerification.delete({
where: {
teamId: team.id,
},
});
await Promise.all([
tx.teamTransferVerification.updateMany({
where: {
teamId: team.id,
},
data: {
completed: true,
},
}),
tx.teamTransferVerification.deleteMany({
where: {
teamId: team.id,
expiresAt: {
lt: new Date(),
},
},
}),
]);
const newOwnerUser = await tx.user.findFirstOrThrow({
where: {

View File

@ -210,7 +210,7 @@ export const createDocumentFromDirectTemplate = async ({
const initialRequestTime = new Date();
const { documentId, directRecipientToken } = await prisma.$transaction(async (tx) => {
const { documentId, recipientId, token } = await prisma.$transaction(async (tx) => {
const documentData = await tx.documentData.create({
data: {
type: template.templateDocumentData.type,
@ -539,8 +539,9 @@ export const createDocumentFromDirectTemplate = async ({
});
return {
token: createdDirectRecipient.token,
documentId: document.id,
directRecipientToken: createdDirectRecipient.token,
recipientId: createdDirectRecipient.id,
};
});
@ -559,5 +560,9 @@ export const createDocumentFromDirectTemplate = async ({
// Log and reseal as required until we configure middleware.
}
return directRecipientToken;
return {
token,
documentId,
recipientId,
};
};

View File

@ -10,6 +10,7 @@ export type CreateDocumentFromTemplateLegacyOptions = {
name?: string;
email: string;
role?: RecipientRole;
signingOrder?: number | null;
}[];
};
@ -73,6 +74,7 @@ export const createDocumentFromTemplateLegacy = async ({
email: recipient.email,
name: recipient.name,
role: recipient.role,
signingOrder: recipient.signingOrder,
token: nanoid(),
})),
},
@ -129,12 +131,14 @@ export const createDocumentFromTemplateLegacy = async ({
name: recipient.name,
email: recipient.email,
role: recipient.role,
signingOrder: recipient.signingOrder,
},
create: {
documentId: document.id,
email: recipient.email,
name: recipient.name,
role: recipient.role,
signingOrder: recipient.signingOrder,
token: nanoid(),
},
});

View File

@ -1,6 +1,6 @@
import { nanoid } from '@documenso/lib/universal/id';
import { prisma } from '@documenso/prisma';
import type { Field } from '@documenso/prisma/client';
import type { DocumentSigningOrder, Field } from '@documenso/prisma/client';
import {
DocumentSource,
type Recipient,
@ -41,6 +41,7 @@ export type CreateDocumentFromTemplateOptions = {
id: number;
name?: string;
email: string;
signingOrder?: number | null;
}[];
/**
@ -54,6 +55,7 @@ export type CreateDocumentFromTemplateOptions = {
password?: string;
dateFormat?: string;
redirectUrl?: string;
signingOrder?: DocumentSigningOrder;
};
requestMetadata?: RequestMetadata;
};
@ -134,6 +136,7 @@ export const createDocumentFromTemplate = async ({
name: foundRecipient ? foundRecipient.name ?? '' : templateRecipient.name,
email: foundRecipient ? foundRecipient.email : templateRecipient.email,
role: templateRecipient.role,
signingOrder: foundRecipient?.signingOrder ?? templateRecipient.signingOrder,
authOptions: templateRecipient.authOptions,
};
});
@ -168,6 +171,8 @@ export const createDocumentFromTemplate = async ({
password: override?.password || template.templateMeta?.password,
dateFormat: override?.dateFormat || template.templateMeta?.dateFormat,
redirectUrl: override?.redirectUrl || template.templateMeta?.redirectUrl,
signingOrder:
override?.signingOrder || template.templateMeta?.signingOrder || undefined,
},
},
Recipient: {

View File

@ -2,13 +2,13 @@
import { nanoid } from 'nanoid';
import { prisma } from '@documenso/prisma';
import type { Recipient, TemplateDirectLink } from '@documenso/prisma/client';
import {
DIRECT_TEMPLATE_RECIPIENT_EMAIL,
DIRECT_TEMPLATE_RECIPIENT_NAME,
} from '../../constants/template';
} from '@documenso/lib/constants/direct-templates';
import { prisma } from '@documenso/prisma';
import type { Recipient, TemplateDirectLink } from '@documenso/prisma/client';
import { AppError, AppErrorCode } from '../../errors/app-error';
export type CreateTemplateDirectLinkOptions = {

View File

@ -33,7 +33,7 @@ export const updateTemplateSettings = async ({
meta,
data,
}: UpdateTemplateSettingsOptions) => {
if (Object.values(data).length === 0) {
if (Object.values(data).length === 0 && Object.keys(meta ?? {}).length === 0) {
throw new AppError(AppErrorCode.INVALID_BODY, 'Missing data to update');
}

View File

@ -4,6 +4,13 @@ import { prisma } from '@documenso/prisma';
import { jobsClient } from '../../jobs/client';
export const EMAIL_VERIFICATION_STATE = {
NOT_FOUND: 'NOT_FOUND',
VERIFIED: 'VERIFIED',
EXPIRED: 'EXPIRED',
ALREADY_VERIFIED: 'ALREADY_VERIFIED',
} as const;
export type VerifyEmailProps = {
token: string;
};
@ -19,7 +26,7 @@ export const verifyEmail = async ({ token }: VerifyEmailProps) => {
});
if (!verificationToken) {
return null;
return EMAIL_VERIFICATION_STATE.NOT_FOUND;
}
// check if the token is valid or expired
@ -48,10 +55,14 @@ export const verifyEmail = async ({ token }: VerifyEmailProps) => {
});
}
return valid;
return EMAIL_VERIFICATION_STATE.EXPIRED;
}
const [updatedUser, deletedToken] = await prisma.$transaction([
if (verificationToken.completed) {
return EMAIL_VERIFICATION_STATE.ALREADY_VERIFIED;
}
const [updatedUser] = await prisma.$transaction([
prisma.user.update({
where: {
id: verificationToken.userId,
@ -60,16 +71,28 @@ export const verifyEmail = async ({ token }: VerifyEmailProps) => {
emailVerified: new Date(),
},
}),
prisma.verificationToken.updateMany({
where: {
userId: verificationToken.userId,
},
data: {
completed: true,
},
}),
// Tidy up old expired tokens
prisma.verificationToken.deleteMany({
where: {
userId: verificationToken.userId,
expires: {
lt: new Date(),
},
},
}),
]);
if (!updatedUser || !deletedToken) {
if (!updatedUser) {
throw new Error('Something went wrong while verifying your email. Please try again.');
}
return !!updatedUser && !!deletedToken;
return EMAIL_VERIFICATION_STATE.VERIFIED;
};

View File

@ -1,6 +1,6 @@
import type { WebhookTriggerEvents } from '@documenso/prisma/client';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../../constants/app';
import { NEXT_PRIVATE_INTERNAL_WEBAPP_URL } from '../../../constants/app';
import { sign } from '../../crypto/sign';
import { getAllWebhooksByEventTrigger } from '../get-all-webhooks-by-event-trigger';
@ -29,7 +29,7 @@ export const triggerWebhook = async ({ event, data, userId, teamId }: TriggerWeb
const signature = sign(body);
await Promise.race([
fetch(`${NEXT_PUBLIC_WEBAPP_URL()}/api/webhook/trigger`, {
fetch(`${NEXT_PRIVATE_INTERNAL_WEBAPP_URL}/api/webhook/trigger`, {
method: 'POST',
headers: {
'content-type': 'application/json',