fix: merge conflicts

This commit is contained in:
Ephraim Atta-Duncan
2025-02-06 11:47:44 +00:00
783 changed files with 20849 additions and 22470 deletions

View File

@ -0,0 +1,51 @@
import { type Field, FieldType } from '@documenso/prisma/client';
import { ZFieldMetaSchema } from '../types/field-meta';
// Currently it seems that the majority of fields have advanced fields for font reasons.
// This array should only contain fields that have an optional setting in the fieldMeta.
export const ADVANCED_FIELD_TYPES_WITH_OPTIONAL_SETTING: FieldType[] = [
FieldType.NUMBER,
FieldType.TEXT,
FieldType.DROPDOWN,
FieldType.RADIO,
FieldType.CHECKBOX,
];
/**
* Whether a field is required to be inserted.
*/
export const isRequiredField = (field: Field) => {
// All fields without the optional metadata are assumed to be required.
if (!ADVANCED_FIELD_TYPES_WITH_OPTIONAL_SETTING.includes(field.type)) {
return true;
}
// Not sure why fieldMeta can be optional for advanced fields, but it is.
// Therefore we must assume if there is no fieldMeta, then the field is optional.
if (!field.fieldMeta) {
return false;
}
const parsedData = ZFieldMetaSchema.safeParse(field.fieldMeta);
// If it fails, assume the field is optional.
// This needs to be logged somewhere.
if (!parsedData.success) {
return false;
}
return parsedData.data?.required === true;
};
/**
* Whether the provided field is required and not inserted.
*/
export const isFieldUnsignedAndRequired = (field: Field) =>
isRequiredField(field) && !field.inserted;
/**
* Whether the provided fields contains a field that is required to be inserted.
*/
export const fieldsContainUnsignedRequiredField = (fields: Field[]) =>
fields.some(isFieldUnsignedAndRequired);

View File

@ -1,6 +1,3 @@
import { env } from 'next-runtime-env';
import { IS_BILLING_ENABLED } from '../constants/app';
import type { Subscription } from '.prisma/client';
import { SubscriptionStatus } from '.prisma/client';
@ -10,12 +7,21 @@ import { SubscriptionStatus } from '.prisma/client';
export const subscriptionsContainsActivePlan = (
subscriptions: Subscription[],
priceIds: string[],
allowPastDue?: boolean,
) => {
const allowedSubscriptionStatuses: SubscriptionStatus[] = [SubscriptionStatus.ACTIVE];
if (allowPastDue) {
allowedSubscriptionStatuses.push(SubscriptionStatus.PAST_DUE);
}
return subscriptions.some(
(subscription) =>
subscription.status === SubscriptionStatus.ACTIVE && priceIds.includes(subscription.priceId),
allowedSubscriptionStatuses.includes(subscription.status) &&
priceIds.includes(subscription.priceId),
);
};
/**
* Returns true if there is a subscription that is active and is one of the provided product IDs.
*/
@ -28,15 +34,3 @@ export const subscriptionsContainsActiveProductId = (
subscription.status === SubscriptionStatus.ACTIVE && productId.includes(subscription.planId),
);
};
export const subscriptionsContainActiveEnterprisePlan = (
subscriptions?: Subscription[],
): boolean => {
const enterprisePlanId = env('NEXT_PUBLIC_STRIPE_ENTERPRISE_PLAN_MONTHLY_PRICE_ID');
if (!enterprisePlanId || !subscriptions || !IS_BILLING_ENABLED()) {
return false;
}
return subscriptionsContainsActivePlan(subscriptions, [enterprisePlanId]);
};

View File

@ -19,14 +19,15 @@ import {
ZDocumentAuditLogSchema,
} from '../types/document-audit-logs';
import { ZRecipientAuthOptionsSchema } from '../types/document-auth';
import type { RequestMetadata } from '../universal/extract-request-metadata';
import type { ApiRequestMetadata, RequestMetadata } from '../universal/extract-request-metadata';
type CreateDocumentAuditLogDataOptions<T = TDocumentAuditLog['type']> = {
documentId: number;
type: T;
data: Extract<TDocumentAuditLog, { type: T }>['data'];
user: { email?: string; id?: number | null; name?: string | null } | null;
user?: { email?: string | null; id?: number | null; name?: string | null } | null;
requestMetadata?: RequestMetadata;
metadata?: ApiRequestMetadata;
};
export type CreateDocumentAuditLogDataResponse = Pick<
@ -42,16 +43,31 @@ export const createDocumentAuditLogData = <T extends TDocumentAuditLog['type']>(
data,
user,
requestMetadata,
metadata,
}: CreateDocumentAuditLogDataOptions<T>): CreateDocumentAuditLogDataResponse => {
let userId: number | null = metadata?.auditUser?.id || null;
let email: string | null = metadata?.auditUser?.email || null;
let name: string | null = metadata?.auditUser?.name || null;
// Prioritize explicit user parameter over metadata audit user.
if (user) {
userId = user.id || null;
email = user.email || null;
name = user.name || null;
}
const ipAddress = metadata?.requestMetadata.ipAddress ?? requestMetadata?.ipAddress ?? null;
const userAgent = metadata?.requestMetadata.userAgent ?? requestMetadata?.userAgent ?? null;
return {
type,
data,
documentId,
userId: user?.id ?? null,
email: user?.email ?? null,
name: user?.name ?? null,
userAgent: requestMetadata?.userAgent ?? null,
ipAddress: requestMetadata?.ipAddress ?? null,
userId,
email,
name,
userAgent,
ipAddress,
};
};
@ -298,6 +314,10 @@ export const formatDocumentAuditLogAction = (
anonymous: msg`Field unsigned`,
identified: msg`${prefix} unsigned a field`,
}))
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FIELD_PREFILLED }, () => ({
anonymous: msg`Field prefilled by assistant`,
identified: msg`${prefix} prefilled a field`,
}))
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_VISIBILITY_UPDATED }, () => ({
anonymous: msg`Document visibility updated`,
identified: msg`${prefix} updated the document visibility`,

View File

@ -0,0 +1,20 @@
import { DocumentVisibility, TeamMemberRole } from '@documenso/prisma/client';
export const determineDocumentVisibility = (
globalVisibility: DocumentVisibility | null | undefined,
userRole: TeamMemberRole,
): DocumentVisibility => {
if (globalVisibility) {
return globalVisibility;
}
if (userRole === TeamMemberRole.ADMIN) {
return DocumentVisibility.ADMIN;
}
if (userRole === TeamMemberRole.MANAGER) {
return DocumentVisibility.MANAGER_AND_ABOVE;
}
return DocumentVisibility.EVERYONE;
};

View File

@ -8,19 +8,8 @@ import { APP_I18N_OPTIONS } from '../constants/i18n';
export async function dynamicActivate(i18nInstance: I18n, locale: string) {
const extension = process.env.NODE_ENV === 'development' ? 'po' : 'js';
const context = IS_APP_WEB ? 'web' : 'marketing';
let { messages } = await import(`../translations/${locale}/${context}.${extension}`);
// Dirty way to load common messages for development since it's not compiled.
if (process.env.NODE_ENV === 'development') {
const commonMessages = await import(`../translations/${locale}/common.${extension}`);
messages = {
...messages,
...commonMessages.messages,
};
}
const { messages } = await import(`../translations/${locale}/web.${extension}`);
i18nInstance.loadAndActivate({ locale, messages });
}

View File

@ -0,0 +1,110 @@
import Honeybadger from '@honeybadger-io/js';
export const buildLogger = () => {
if (process.env.NEXT_PRIVATE_LOGGER_HONEY_BADGER_API_KEY) {
return new HoneybadgerLogger();
}
return new DefaultLogger();
};
interface LoggerDescriptionOptions {
method?: string;
path?: string;
context?: Record<string, unknown>;
/**
* The type of log to be captured.
*
* Defaults to `info`.
*/
level?: 'info' | 'error' | 'critical';
}
/**
* Basic logger implementation intended to be used in the server side for capturing
* explicit errors and other logs.
*
* Not intended to capture the request and responses.
*/
interface Logger {
log(message: string, options?: LoggerDescriptionOptions): void;
error(error: Error, options?: LoggerDescriptionOptions): void;
}
class DefaultLogger implements Logger {
log(_message: string, _options?: LoggerDescriptionOptions) {
// Do nothing.
}
error(_error: Error, _options?: LoggerDescriptionOptions): void {
// Do nothing.
}
}
class HoneybadgerLogger implements Logger {
constructor() {
if (!process.env.NEXT_PRIVATE_LOGGER_HONEY_BADGER_API_KEY) {
throw new Error('NEXT_PRIVATE_LOGGER_HONEY_BADGER_API_KEY is not set');
}
Honeybadger.configure({
apiKey: process.env.NEXT_PRIVATE_LOGGER_HONEY_BADGER_API_KEY,
});
}
/**
* Honeybadger doesn't really have a non-error logging system.
*/
log(message: string, options?: LoggerDescriptionOptions) {
const { context = {}, level = 'info' } = options || {};
try {
Honeybadger.event({
message,
context: {
level,
...context,
},
});
} catch (err) {
console.error(err);
// Do nothing.
}
}
error(error: Error, options?: LoggerDescriptionOptions): void {
const { context = {}, level = 'error', method, path } = options || {};
// const tags = [`level:${level}`];
const tags = [];
let errorMessage = error.message;
if (method) {
tags.push(`method:${method}`);
errorMessage = `[${method}]: ${error.message}`;
}
if (path) {
tags.push(`path:${path}`);
}
try {
Honeybadger.notify(errorMessage, {
context: {
level,
...context,
},
tags,
});
} catch (err) {
console.error(err);
// Do nothing.
}
}
}

View File

@ -12,7 +12,7 @@ export const maskRecipientTokensForDocument = <T extends DocumentWithRecipients>
user,
token,
}: MaskRecipientTokensForDocumentOptions<T>) => {
const maskedRecipients = document.Recipient.map((recipient) => {
const maskedRecipients = document.recipients.map((recipient) => {
if (document.userId === user?.id) {
return recipient;
}

View File

@ -8,8 +8,17 @@ export const formatSigningLink = (token: string) => `${NEXT_PUBLIC_WEBAPP_URL()}
* Whether a recipient can be modified by the document owner.
*/
export const canRecipientBeModified = (recipient: Recipient, fields: Field[]) => {
if (!recipient) {
return false;
}
// CCers can always be modified (unless document is completed).
if (recipient.role === RecipientRole.CC) {
return true;
}
// Deny if the recipient has already signed the document.
if (!recipient || recipient.signingStatus === SigningStatus.SIGNED) {
if (recipient.signingStatus === SigningStatus.SIGNED) {
return false;
}