mirror of
https://github.com/documenso/documenso.git
synced 2025-11-20 11:41:44 +10:00
Merge branch 'main' into feat/signing-reminders
This commit is contained in:
@ -1,36 +1,39 @@
|
||||
import type { Subscription } from '@prisma/client';
|
||||
import { SubscriptionStatus } from '@prisma/client';
|
||||
import type { Subscription } from '@documenso/prisma/generated/zod/modelSchema/SubscriptionSchema';
|
||||
|
||||
import { IS_BILLING_ENABLED } from '../constants/app';
|
||||
import { AppErrorCode } from '../errors/app-error';
|
||||
import { AppError } from '../errors/app-error';
|
||||
import type { StripeOrganisationCreateMetadata } from '../types/subscription';
|
||||
|
||||
export const generateStripeOrganisationCreateMetadata = (
|
||||
organisationName: string,
|
||||
userId: number,
|
||||
) => {
|
||||
const metadata: StripeOrganisationCreateMetadata = {
|
||||
organisationName,
|
||||
userId,
|
||||
};
|
||||
|
||||
return {
|
||||
organisationCreateData: JSON.stringify(metadata),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns true if there is a subscription that is active and is one of the provided price IDs.
|
||||
* Throws an error if billing is enabled and no subscription is found.
|
||||
*/
|
||||
export const subscriptionsContainsActivePlan = (
|
||||
subscriptions: Subscription[],
|
||||
priceIds: string[],
|
||||
allowPastDue?: boolean,
|
||||
) => {
|
||||
const allowedSubscriptionStatuses: SubscriptionStatus[] = [SubscriptionStatus.ACTIVE];
|
||||
export const validateIfSubscriptionIsRequired = (subscription?: Subscription | null) => {
|
||||
const isBillingEnabled = IS_BILLING_ENABLED();
|
||||
|
||||
if (allowPastDue) {
|
||||
allowedSubscriptionStatuses.push(SubscriptionStatus.PAST_DUE);
|
||||
if (!isBillingEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
return subscriptions.some(
|
||||
(subscription) =>
|
||||
allowedSubscriptionStatuses.includes(subscription.status) &&
|
||||
priceIds.includes(subscription.priceId),
|
||||
);
|
||||
};
|
||||
if (isBillingEnabled && !subscription) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Subscription not found',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if there is a subscription that is active and is one of the provided product IDs.
|
||||
*/
|
||||
export const subscriptionsContainsActiveProductId = (
|
||||
subscriptions: Subscription[],
|
||||
productId: string[],
|
||||
) => {
|
||||
return subscriptions.some(
|
||||
(subscription) =>
|
||||
subscription.status === SubscriptionStatus.ACTIVE && productId.includes(subscription.planId),
|
||||
);
|
||||
return subscription;
|
||||
};
|
||||
|
||||
@ -2,6 +2,7 @@ import type { I18n } from '@lingui/core';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import type { DocumentAuditLog, DocumentMeta, Field, Recipient } from '@prisma/client';
|
||||
import { RecipientRole } from '@prisma/client';
|
||||
import { isDeepEqual } from 'remeda';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import type {
|
||||
@ -106,7 +107,7 @@ export const diffRecipientChanges = (
|
||||
const newActionAuth =
|
||||
newAuthOptions?.actionAuth === undefined ? oldActionAuth : newAuthOptions.actionAuth;
|
||||
|
||||
if (oldAccessAuth !== newAccessAuth) {
|
||||
if (!isDeepEqual(oldAccessAuth, newAccessAuth)) {
|
||||
diffs.push({
|
||||
type: RECIPIENT_DIFF_TYPE.ACCESS_AUTH,
|
||||
from: oldAccessAuth ?? '',
|
||||
@ -114,7 +115,7 @@ export const diffRecipientChanges = (
|
||||
});
|
||||
}
|
||||
|
||||
if (oldActionAuth !== newActionAuth) {
|
||||
if (!isDeepEqual(oldActionAuth, newActionAuth)) {
|
||||
diffs.push({
|
||||
type: RECIPIENT_DIFF_TYPE.ACTION_AUTH,
|
||||
from: oldActionAuth ?? '',
|
||||
@ -204,12 +205,18 @@ export const diffDocumentMetaChanges = (
|
||||
const oldTimezone = oldData?.timezone ?? '';
|
||||
const oldPassword = oldData?.password ?? null;
|
||||
const oldRedirectUrl = oldData?.redirectUrl ?? '';
|
||||
const oldEmailId = oldData?.emailId || null;
|
||||
const oldEmailReplyTo = oldData?.emailReplyTo || null;
|
||||
const oldEmailSettings = oldData?.emailSettings || null;
|
||||
|
||||
const newDateFormat = newData?.dateFormat ?? '';
|
||||
const newMessage = newData?.message ?? '';
|
||||
const newSubject = newData?.subject ?? '';
|
||||
const newTimezone = newData?.timezone ?? '';
|
||||
const newRedirectUrl = newData?.redirectUrl ?? '';
|
||||
const newEmailId = newData?.emailId || null;
|
||||
const newEmailReplyTo = newData?.emailReplyTo || null;
|
||||
const newEmailSettings = newData?.emailSettings || null;
|
||||
|
||||
if (oldDateFormat !== newDateFormat) {
|
||||
diffs.push({
|
||||
@ -257,6 +264,30 @@ export const diffDocumentMetaChanges = (
|
||||
});
|
||||
}
|
||||
|
||||
if (oldEmailId !== newEmailId) {
|
||||
diffs.push({
|
||||
type: DOCUMENT_META_DIFF_TYPE.EMAIL_ID,
|
||||
from: oldEmailId,
|
||||
to: newEmailId,
|
||||
});
|
||||
}
|
||||
|
||||
if (oldEmailReplyTo !== newEmailReplyTo) {
|
||||
diffs.push({
|
||||
type: DOCUMENT_META_DIFF_TYPE.EMAIL_REPLY_TO,
|
||||
from: oldEmailReplyTo,
|
||||
to: newEmailReplyTo,
|
||||
});
|
||||
}
|
||||
|
||||
if (!isDeepEqual(oldEmailSettings, newEmailSettings)) {
|
||||
diffs.push({
|
||||
type: DOCUMENT_META_DIFF_TYPE.EMAIL_SETTINGS,
|
||||
from: JSON.stringify(oldEmailSettings),
|
||||
to: JSON.stringify(newEmailSettings),
|
||||
});
|
||||
}
|
||||
|
||||
return diffs;
|
||||
};
|
||||
|
||||
@ -274,83 +305,150 @@ export const formatDocumentAuditLogAction = (
|
||||
|
||||
const description = match(auditLog)
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.FIELD_CREATED }, () => ({
|
||||
anonymous: msg`A field was added`,
|
||||
anonymous: msg({
|
||||
message: `A field was added`,
|
||||
context: `Audit log format`,
|
||||
}),
|
||||
identified: msg`${prefix} added a field`,
|
||||
}))
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.FIELD_DELETED }, () => ({
|
||||
anonymous: msg`A field was removed`,
|
||||
anonymous: msg({
|
||||
message: `A field was removed`,
|
||||
context: `Audit log format`,
|
||||
}),
|
||||
identified: msg`${prefix} removed a field`,
|
||||
}))
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.FIELD_UPDATED }, () => ({
|
||||
anonymous: msg`A field was updated`,
|
||||
anonymous: msg({
|
||||
message: `A field was updated`,
|
||||
context: `Audit log format`,
|
||||
}),
|
||||
identified: msg`${prefix} updated a field`,
|
||||
}))
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.RECIPIENT_CREATED }, () => ({
|
||||
anonymous: msg`A recipient was added`,
|
||||
anonymous: msg({
|
||||
message: `A recipient was added`,
|
||||
context: `Audit log format`,
|
||||
}),
|
||||
identified: msg`${prefix} added a recipient`,
|
||||
}))
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.RECIPIENT_DELETED }, () => ({
|
||||
anonymous: msg`A recipient was removed`,
|
||||
anonymous: msg({
|
||||
message: `A recipient was removed`,
|
||||
context: `Audit log format`,
|
||||
}),
|
||||
identified: msg`${prefix} removed a recipient`,
|
||||
}))
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.RECIPIENT_UPDATED }, () => ({
|
||||
anonymous: msg`A recipient was updated`,
|
||||
anonymous: msg({
|
||||
message: `A recipient was updated`,
|
||||
context: `Audit log format`,
|
||||
}),
|
||||
identified: msg`${prefix} updated a recipient`,
|
||||
}))
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_CREATED }, () => ({
|
||||
anonymous: msg`Document created`,
|
||||
anonymous: msg({
|
||||
message: `Document created`,
|
||||
context: `Audit log format`,
|
||||
}),
|
||||
identified: msg`${prefix} created the document`,
|
||||
}))
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_DELETED }, () => ({
|
||||
anonymous: msg`Document deleted`,
|
||||
anonymous: msg({
|
||||
message: `Document deleted`,
|
||||
context: `Audit log format`,
|
||||
}),
|
||||
identified: msg`${prefix} deleted the document`,
|
||||
}))
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FIELD_INSERTED }, () => ({
|
||||
anonymous: msg`Field signed`,
|
||||
anonymous: msg({
|
||||
message: `Field signed`,
|
||||
context: `Audit log format`,
|
||||
}),
|
||||
identified: msg`${prefix} signed a field`,
|
||||
}))
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FIELD_UNINSERTED }, () => ({
|
||||
anonymous: msg`Field unsigned`,
|
||||
anonymous: msg({
|
||||
message: `Field unsigned`,
|
||||
context: `Audit log format`,
|
||||
}),
|
||||
identified: msg`${prefix} unsigned a field`,
|
||||
}))
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FIELD_PREFILLED }, () => ({
|
||||
anonymous: msg`Field prefilled by assistant`,
|
||||
anonymous: msg({
|
||||
message: `Field prefilled by assistant`,
|
||||
context: `Audit log format`,
|
||||
}),
|
||||
identified: msg`${prefix} prefilled a field`,
|
||||
}))
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_VISIBILITY_UPDATED }, () => ({
|
||||
anonymous: msg`Document visibility updated`,
|
||||
anonymous: msg({
|
||||
message: `Document visibility updated`,
|
||||
context: `Audit log format`,
|
||||
}),
|
||||
identified: msg`${prefix} updated the document visibility`,
|
||||
}))
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_GLOBAL_AUTH_ACCESS_UPDATED }, () => ({
|
||||
anonymous: msg`Document access auth updated`,
|
||||
anonymous: msg({
|
||||
message: `Document access auth updated`,
|
||||
context: `Audit log format`,
|
||||
}),
|
||||
identified: msg`${prefix} updated the document access auth requirements`,
|
||||
}))
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_GLOBAL_AUTH_ACTION_UPDATED }, () => ({
|
||||
anonymous: msg`Document signing auth updated`,
|
||||
anonymous: msg({
|
||||
message: `Document signing auth updated`,
|
||||
context: `Audit log format`,
|
||||
}),
|
||||
identified: msg`${prefix} updated the document signing auth requirements`,
|
||||
}))
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_META_UPDATED }, () => ({
|
||||
anonymous: msg`Document updated`,
|
||||
anonymous: msg({
|
||||
message: `Document updated`,
|
||||
context: `Audit log format`,
|
||||
}),
|
||||
identified: msg`${prefix} updated the document`,
|
||||
}))
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_OPENED }, () => ({
|
||||
anonymous: msg`Document opened`,
|
||||
anonymous: msg({
|
||||
message: `Document opened`,
|
||||
context: `Audit log format`,
|
||||
}),
|
||||
identified: msg`${prefix} opened the document`,
|
||||
}))
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_VIEWED }, () => ({
|
||||
anonymous: msg({
|
||||
message: `Document viewed`,
|
||||
context: `Audit log format`,
|
||||
}),
|
||||
identified: msg`${prefix} viewed the document`,
|
||||
}))
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_TITLE_UPDATED }, () => ({
|
||||
anonymous: msg`Document title updated`,
|
||||
anonymous: msg({
|
||||
message: `Document title updated`,
|
||||
context: `Audit log format`,
|
||||
}),
|
||||
identified: msg`${prefix} updated the document title`,
|
||||
}))
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_EXTERNAL_ID_UPDATED }, () => ({
|
||||
anonymous: msg`Document external ID updated`,
|
||||
anonymous: msg({
|
||||
message: `Document external ID updated`,
|
||||
context: `Audit log format`,
|
||||
}),
|
||||
identified: msg`${prefix} updated the document external ID`,
|
||||
}))
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_SENT }, () => ({
|
||||
anonymous: msg`Document sent`,
|
||||
anonymous: msg({
|
||||
message: `Document sent`,
|
||||
context: `Audit log format`,
|
||||
}),
|
||||
identified: msg`${prefix} sent the document`,
|
||||
}))
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_MOVED_TO_TEAM }, () => ({
|
||||
anonymous: msg`Document moved to team`,
|
||||
anonymous: msg({
|
||||
message: `Document moved to team`,
|
||||
context: `Audit log format`,
|
||||
}),
|
||||
identified: msg`${prefix} moved the document to team`,
|
||||
}))
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_COMPLETED }, ({ data }) => {
|
||||
@ -385,8 +483,14 @@ export const formatDocumentAuditLogAction = (
|
||||
: msg`${prefix} sent an email to ${data.recipientEmail}`,
|
||||
}))
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_COMPLETED }, () => ({
|
||||
anonymous: msg`Document completed`,
|
||||
identified: msg`Document completed`,
|
||||
anonymous: msg({
|
||||
message: `Document completed`,
|
||||
context: `Audit log format`,
|
||||
}),
|
||||
identified: msg({
|
||||
message: `Document completed`,
|
||||
context: `Audit log format`,
|
||||
}),
|
||||
}))
|
||||
.exhaustive();
|
||||
|
||||
|
||||
@ -27,17 +27,21 @@ export const extractDocumentAuthMethods = ({
|
||||
const documentAuthOption = ZDocumentAuthOptionsSchema.parse(documentAuth);
|
||||
const recipientAuthOption = ZRecipientAuthOptionsSchema.parse(recipientAuth);
|
||||
|
||||
const derivedRecipientAccessAuth: TRecipientAccessAuthTypes | null =
|
||||
recipientAuthOption.accessAuth || documentAuthOption.globalAccessAuth;
|
||||
const derivedRecipientAccessAuth: TRecipientAccessAuthTypes[] =
|
||||
recipientAuthOption.accessAuth.length > 0
|
||||
? recipientAuthOption.accessAuth
|
||||
: documentAuthOption.globalAccessAuth;
|
||||
|
||||
const derivedRecipientActionAuth: TRecipientActionAuthTypes | null =
|
||||
recipientAuthOption.actionAuth || documentAuthOption.globalActionAuth;
|
||||
const derivedRecipientActionAuth: TRecipientActionAuthTypes[] =
|
||||
recipientAuthOption.actionAuth.length > 0
|
||||
? recipientAuthOption.actionAuth
|
||||
: documentAuthOption.globalActionAuth;
|
||||
|
||||
const recipientAccessAuthRequired = derivedRecipientAccessAuth !== null;
|
||||
const recipientAccessAuthRequired = derivedRecipientAccessAuth.length > 0;
|
||||
|
||||
const recipientActionAuthRequired =
|
||||
derivedRecipientActionAuth !== DocumentAuth.EXPLICIT_NONE &&
|
||||
derivedRecipientActionAuth !== null;
|
||||
derivedRecipientActionAuth.length > 0 &&
|
||||
!derivedRecipientActionAuth.includes(DocumentAuth.EXPLICIT_NONE);
|
||||
|
||||
return {
|
||||
derivedRecipientAccessAuth,
|
||||
@ -54,8 +58,8 @@ export const extractDocumentAuthMethods = ({
|
||||
*/
|
||||
export const createDocumentAuthOptions = (options: TDocumentAuthOptions): TDocumentAuthOptions => {
|
||||
return {
|
||||
globalAccessAuth: options?.globalAccessAuth ?? null,
|
||||
globalActionAuth: options?.globalActionAuth ?? null,
|
||||
globalAccessAuth: options?.globalAccessAuth ?? [],
|
||||
globalActionAuth: options?.globalActionAuth ?? [],
|
||||
};
|
||||
};
|
||||
|
||||
@ -66,7 +70,7 @@ export const createRecipientAuthOptions = (
|
||||
options: TRecipientAuthOptions,
|
||||
): TRecipientAuthOptions => {
|
||||
return {
|
||||
accessAuth: options?.accessAuth ?? null,
|
||||
actionAuth: options?.actionAuth ?? null,
|
||||
accessAuth: options?.accessAuth ?? [],
|
||||
actionAuth: options?.actionAuth ?? [],
|
||||
};
|
||||
};
|
||||
|
||||
@ -1,8 +1,62 @@
|
||||
import type { Document } from '@prisma/client';
|
||||
import { DocumentStatus } from '@prisma/client';
|
||||
import type {
|
||||
Document,
|
||||
DocumentMeta,
|
||||
OrganisationGlobalSettings,
|
||||
TemplateMeta,
|
||||
} from '@prisma/client';
|
||||
import { DocumentDistributionMethod, DocumentSigningOrder, DocumentStatus } from '@prisma/client';
|
||||
|
||||
import { DEFAULT_DOCUMENT_TIME_ZONE } from '../constants/time-zones';
|
||||
import { DEFAULT_DOCUMENT_EMAIL_SETTINGS } from '../types/document-email';
|
||||
|
||||
export const isDocumentCompleted = (document: Pick<Document, 'status'> | DocumentStatus) => {
|
||||
const status = typeof document === 'string' ? document : document.status;
|
||||
|
||||
return status === DocumentStatus.COMPLETED || status === DocumentStatus.REJECTED;
|
||||
};
|
||||
|
||||
/**
|
||||
* Extracts the derived document meta which should be used when creating a document
|
||||
* from scratch, or from a template.
|
||||
*
|
||||
* Uses the following, the lower number overrides the higher number:
|
||||
* 1. Merged organisation/team settings
|
||||
* 2. Meta overrides
|
||||
*
|
||||
* @param settings - The merged organisation/team settings.
|
||||
* @param overrideMeta - The meta to override the settings with.
|
||||
* @returns The derived document meta.
|
||||
*/
|
||||
export const extractDerivedDocumentMeta = (
|
||||
settings: Omit<OrganisationGlobalSettings, 'id'>,
|
||||
overrideMeta: Partial<DocumentMeta | TemplateMeta> | undefined | null,
|
||||
) => {
|
||||
const meta = overrideMeta ?? {};
|
||||
|
||||
// Note: If you update this you will also need to update `create-document-from-template.ts`
|
||||
// since there is custom work there which allows 3 overrides.
|
||||
return {
|
||||
language: meta.language || settings.documentLanguage,
|
||||
timezone: meta.timezone || settings.documentTimezone || DEFAULT_DOCUMENT_TIME_ZONE,
|
||||
dateFormat: meta.dateFormat || settings.documentDateFormat,
|
||||
message: meta.message || null,
|
||||
subject: meta.subject || null,
|
||||
password: meta.password || null,
|
||||
redirectUrl: meta.redirectUrl || null,
|
||||
|
||||
signingOrder: meta.signingOrder || DocumentSigningOrder.PARALLEL,
|
||||
allowDictateNextSigner: meta.allowDictateNextSigner ?? false,
|
||||
distributionMethod: meta.distributionMethod || DocumentDistributionMethod.EMAIL, // Todo: Make this a setting.
|
||||
|
||||
// Signature settings.
|
||||
typedSignatureEnabled: meta.typedSignatureEnabled ?? settings.typedSignatureEnabled,
|
||||
uploadSignatureEnabled: meta.uploadSignatureEnabled ?? settings.uploadSignatureEnabled,
|
||||
drawSignatureEnabled: meta.drawSignatureEnabled ?? settings.drawSignatureEnabled,
|
||||
|
||||
// Email settings.
|
||||
emailId: meta.emailId ?? settings.emailId,
|
||||
emailReplyTo: meta.emailReplyTo ?? settings.emailReplyTo,
|
||||
emailSettings:
|
||||
meta.emailSettings || settings.emailDocumentSettings || DEFAULT_DOCUMENT_EMAIL_SETTINGS,
|
||||
} satisfies Omit<DocumentMeta, 'id' | 'documentId'>;
|
||||
};
|
||||
|
||||
17
packages/lib/utils/email-domains.ts
Normal file
17
packages/lib/utils/email-domains.ts
Normal file
@ -0,0 +1,17 @@
|
||||
export const generateDkimRecord = (recordName: string, publicKeyFlattened: string) => {
|
||||
return {
|
||||
name: recordName,
|
||||
value: `v=DKIM1; k=rsa; p=${publicKeyFlattened}`,
|
||||
type: 'TXT',
|
||||
};
|
||||
};
|
||||
|
||||
export const AWS_SES_SPF_RECORD = {
|
||||
name: `@`,
|
||||
value: 'v=spf1 include:amazonses.com -all',
|
||||
type: 'TXT',
|
||||
};
|
||||
|
||||
export const generateEmailDomainRecords = (recordName: string, publicKeyFlattened: string) => {
|
||||
return [generateDkimRecord(recordName, publicKeyFlattened), AWS_SES_SPF_RECORD];
|
||||
};
|
||||
5
packages/lib/utils/format-folder-count.ts
Normal file
5
packages/lib/utils/format-folder-count.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export function formatFolderCount(count: number, singular: string, plural?: string): string {
|
||||
const itemLabel = count === 1 ? singular : plural || `${singular}s`;
|
||||
|
||||
return `${count} ${itemLabel}`;
|
||||
}
|
||||
@ -1,5 +1,6 @@
|
||||
import type { I18n, MessageDescriptor } from '@lingui/core';
|
||||
import { i18n } from '@lingui/core';
|
||||
import type { MacroMessageDescriptor } from '@lingui/core/macro';
|
||||
|
||||
import type { I18nLocaleData, SupportedLanguageCodes } from '../constants/i18n';
|
||||
import { APP_I18N_OPTIONS } from '../constants/i18n';
|
||||
@ -84,3 +85,10 @@ export const extractLocaleData = ({ headers }: ExtractLocaleDataOptions): I18nLo
|
||||
export const parseMessageDescriptor = (_: I18n['_'], value: string | MessageDescriptor) => {
|
||||
return typeof value === 'string' ? value : _(value);
|
||||
};
|
||||
|
||||
export const parseMessageDescriptorMacro = (
|
||||
t: (descriptor: MacroMessageDescriptor) => string,
|
||||
value: string | MessageDescriptor,
|
||||
) => {
|
||||
return typeof value === 'string' ? value : t(value);
|
||||
};
|
||||
|
||||
@ -1,112 +1,65 @@
|
||||
import Honeybadger from '@honeybadger-io/js';
|
||||
import { type TransportTargetOptions, pino } from 'pino';
|
||||
|
||||
import type { BaseApiLog } from '../types/api-logs';
|
||||
import { extractRequestMetadata } from '../universal/extract-request-metadata';
|
||||
import { env } from './env';
|
||||
|
||||
export const buildLogger = () => {
|
||||
if (env('NEXT_PRIVATE_LOGGER_HONEY_BADGER_API_KEY')) {
|
||||
return new HoneybadgerLogger();
|
||||
}
|
||||
const transports: TransportTargetOptions[] = [];
|
||||
|
||||
return new DefaultLogger();
|
||||
if (env('NODE_ENV') !== 'production' && !env('INTERNAL_FORCE_JSON_LOGGER')) {
|
||||
transports.push({
|
||||
target: 'pino-pretty',
|
||||
level: 'info',
|
||||
});
|
||||
}
|
||||
|
||||
const loggingFilePath = env('NEXT_PRIVATE_LOGGER_FILE_PATH');
|
||||
|
||||
if (loggingFilePath) {
|
||||
transports.push({
|
||||
target: 'pino/file',
|
||||
level: 'info',
|
||||
options: {
|
||||
destination: loggingFilePath,
|
||||
mkdir: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export const logger = pino({
|
||||
level: 'info',
|
||||
transport:
|
||||
transports.length > 0
|
||||
? {
|
||||
targets: transports,
|
||||
}
|
||||
: undefined,
|
||||
});
|
||||
|
||||
export const logDocumentAccess = ({
|
||||
request,
|
||||
documentId,
|
||||
userId,
|
||||
}: {
|
||||
request: Request;
|
||||
documentId: number;
|
||||
userId: number;
|
||||
}) => {
|
||||
const metadata = extractRequestMetadata(request);
|
||||
|
||||
const data: BaseApiLog = {
|
||||
ipAddress: metadata.ipAddress,
|
||||
userAgent: metadata.userAgent,
|
||||
path: new URL(request.url).pathname,
|
||||
auth: 'session',
|
||||
source: 'app',
|
||||
userId,
|
||||
};
|
||||
|
||||
logger.info({
|
||||
...data,
|
||||
input: {
|
||||
documentId,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
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 (!env('NEXT_PRIVATE_LOGGER_HONEY_BADGER_API_KEY')) {
|
||||
throw new Error('NEXT_PRIVATE_LOGGER_HONEY_BADGER_API_KEY is not set');
|
||||
}
|
||||
|
||||
Honeybadger.configure({
|
||||
apiKey: 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.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
14
packages/lib/utils/organisations-claims.ts
Normal file
14
packages/lib/utils/organisations-claims.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import type { SubscriptionClaim } from '@prisma/client';
|
||||
|
||||
export const generateDefaultSubscriptionClaim = (): Omit<
|
||||
SubscriptionClaim,
|
||||
'id' | 'organisation' | 'createdAt' | 'updatedAt' | 'originalSubscriptionClaimId'
|
||||
> => {
|
||||
return {
|
||||
name: '',
|
||||
teamCount: 1,
|
||||
memberCount: 1,
|
||||
locked: false,
|
||||
flags: {},
|
||||
};
|
||||
};
|
||||
139
packages/lib/utils/organisations.ts
Normal file
139
packages/lib/utils/organisations.ts
Normal file
@ -0,0 +1,139 @@
|
||||
import type { Organisation, OrganisationGlobalSettings, Prisma } from '@prisma/client';
|
||||
import {
|
||||
DocumentVisibility,
|
||||
type OrganisationGroup,
|
||||
type OrganisationMemberRole,
|
||||
} from '@prisma/client';
|
||||
|
||||
import type { ORGANISATION_MEMBER_ROLE_MAP } from '@documenso/lib/constants/organisations-translations';
|
||||
|
||||
import { DEFAULT_DOCUMENT_DATE_FORMAT } from '../constants/date-formats';
|
||||
import {
|
||||
LOWEST_ORGANISATION_ROLE,
|
||||
ORGANISATION_MEMBER_ROLE_HIERARCHY,
|
||||
ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP,
|
||||
} from '../constants/organisations';
|
||||
import { DEFAULT_DOCUMENT_EMAIL_SETTINGS } from '../types/document-email';
|
||||
|
||||
export const isPersonalLayout = (organisations: Pick<Organisation, 'type'>[]) => {
|
||||
return organisations.length === 1 && organisations[0].type === 'PERSONAL';
|
||||
};
|
||||
|
||||
/**
|
||||
* Determines whether a team member can execute a given action.
|
||||
*
|
||||
* @param action The action the user is trying to execute.
|
||||
* @param role The current role of the user.
|
||||
* @returns Whether the user can execute the action.
|
||||
*/
|
||||
export const canExecuteOrganisationAction = (
|
||||
action: keyof typeof ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP,
|
||||
role: keyof typeof ORGANISATION_MEMBER_ROLE_MAP,
|
||||
) => {
|
||||
return ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP[action].some((i) => i === role);
|
||||
};
|
||||
|
||||
/**
|
||||
* Compares the provided `currentUserRole` with the provided `roleToCheck` to determine
|
||||
* whether the `currentUserRole` has permission to modify the `roleToCheck`.
|
||||
*
|
||||
* @param currentUserRole Role of the current user
|
||||
* @param roleToCheck Role of another user to see if the current user can modify
|
||||
* @returns True if the current user can modify the other user, false otherwise
|
||||
*/
|
||||
export const isOrganisationRoleWithinUserHierarchy = (
|
||||
currentUserRole: keyof typeof ORGANISATION_MEMBER_ROLE_MAP,
|
||||
roleToCheck: keyof typeof ORGANISATION_MEMBER_ROLE_MAP,
|
||||
) => {
|
||||
return ORGANISATION_MEMBER_ROLE_HIERARCHY[currentUserRole].some((i) => i === roleToCheck);
|
||||
};
|
||||
|
||||
export const getHighestOrganisationRoleInGroup = (
|
||||
groups: Pick<OrganisationGroup, 'type' | 'organisationRole'>[],
|
||||
): OrganisationMemberRole => {
|
||||
let highestOrganisationRole: OrganisationMemberRole = LOWEST_ORGANISATION_ROLE;
|
||||
|
||||
groups.forEach((group) => {
|
||||
const currentRolePriority = ORGANISATION_MEMBER_ROLE_HIERARCHY[group.organisationRole].length;
|
||||
const highestOrganisationRolePriority =
|
||||
ORGANISATION_MEMBER_ROLE_HIERARCHY[highestOrganisationRole].length;
|
||||
|
||||
if (currentRolePriority > highestOrganisationRolePriority) {
|
||||
highestOrganisationRole = group.organisationRole;
|
||||
}
|
||||
});
|
||||
|
||||
return highestOrganisationRole;
|
||||
};
|
||||
|
||||
type BuildOrganisationWhereQueryOptions = {
|
||||
organisationId: string | undefined;
|
||||
userId: number;
|
||||
roles?: OrganisationMemberRole[];
|
||||
};
|
||||
|
||||
export const buildOrganisationWhereQuery = ({
|
||||
organisationId,
|
||||
userId,
|
||||
roles,
|
||||
}: BuildOrganisationWhereQueryOptions): Prisma.OrganisationWhereInput => {
|
||||
// Note: Not using inline ternary since typesafety breaks for some reason.
|
||||
if (!roles) {
|
||||
return {
|
||||
id: organisationId,
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
id: organisationId,
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
organisationGroupMembers: {
|
||||
some: {
|
||||
group: {
|
||||
organisationRole: {
|
||||
in: roles,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const generateDefaultOrganisationSettings = (): Omit<
|
||||
OrganisationGlobalSettings,
|
||||
'id' | 'organisation'
|
||||
> => {
|
||||
return {
|
||||
documentVisibility: DocumentVisibility.EVERYONE,
|
||||
documentLanguage: 'en',
|
||||
documentTimezone: null, // Null means local timezone.
|
||||
documentDateFormat: DEFAULT_DOCUMENT_DATE_FORMAT,
|
||||
|
||||
includeSenderDetails: true,
|
||||
includeSigningCertificate: true,
|
||||
includeAuditLog: false,
|
||||
|
||||
typedSignatureEnabled: true,
|
||||
uploadSignatureEnabled: true,
|
||||
drawSignatureEnabled: true,
|
||||
|
||||
brandingEnabled: false,
|
||||
brandingLogo: '',
|
||||
brandingUrl: '',
|
||||
brandingCompanyDetails: '',
|
||||
|
||||
emailId: null,
|
||||
emailReplyTo: null,
|
||||
// emailReplyToName: null,
|
||||
emailDocumentSettings: DEFAULT_DOCUMENT_EMAIL_SETTINGS,
|
||||
};
|
||||
};
|
||||
@ -1,13 +1,33 @@
|
||||
import type { TeamGlobalSettings } from '@prisma/client';
|
||||
import type { OrganisationGlobalSettings } from '@prisma/client';
|
||||
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../constants/app';
|
||||
|
||||
export const teamGlobalSettingsToBranding = (teamGlobalSettings: TeamGlobalSettings) => {
|
||||
export const teamGlobalSettingsToBranding = (
|
||||
settings: Omit<OrganisationGlobalSettings, 'id'>,
|
||||
teamId: number,
|
||||
hidePoweredBy: boolean,
|
||||
) => {
|
||||
return {
|
||||
...teamGlobalSettings,
|
||||
...settings,
|
||||
brandingLogo:
|
||||
teamGlobalSettings.brandingEnabled && teamGlobalSettings.brandingLogo
|
||||
? `${NEXT_PUBLIC_WEBAPP_URL()}/api/branding/logo/team/${teamGlobalSettings.teamId}`
|
||||
settings.brandingEnabled && settings.brandingLogo
|
||||
? `${NEXT_PUBLIC_WEBAPP_URL()}/api/branding/logo/team/${teamId}`
|
||||
: '',
|
||||
brandingHidePoweredBy: hidePoweredBy,
|
||||
};
|
||||
};
|
||||
|
||||
export const organisationGlobalSettingsToBranding = (
|
||||
settings: Omit<OrganisationGlobalSettings, 'id'>,
|
||||
organisationId: string,
|
||||
hidePoweredBy: boolean,
|
||||
) => {
|
||||
return {
|
||||
...settings,
|
||||
brandingLogo:
|
||||
settings.brandingEnabled && settings.brandingLogo
|
||||
? `${NEXT_PUBLIC_WEBAPP_URL()}/api/branding/logo/organisation/${organisationId}`
|
||||
: '',
|
||||
brandingHidePoweredBy: hidePoweredBy,
|
||||
};
|
||||
};
|
||||
|
||||
@ -1,7 +1,24 @@
|
||||
import type { OrganisationGlobalSettings, Prisma, TeamGlobalSettings } from '@prisma/client';
|
||||
|
||||
import type { TeamGroup } from '@documenso/prisma/generated/types';
|
||||
import type { TeamMemberRole } from '@documenso/prisma/generated/types';
|
||||
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../constants/app';
|
||||
import { DocumentSignatureType } from '../constants/document';
|
||||
import type { TEAM_MEMBER_ROLE_MAP } from '../constants/teams';
|
||||
import { TEAM_MEMBER_ROLE_HIERARCHY, TEAM_MEMBER_ROLE_PERMISSIONS_MAP } from '../constants/teams';
|
||||
import {
|
||||
LOWEST_TEAM_ROLE,
|
||||
TEAM_MEMBER_ROLE_HIERARCHY,
|
||||
TEAM_MEMBER_ROLE_PERMISSIONS_MAP,
|
||||
} from '../constants/teams';
|
||||
import type { TEAM_MEMBER_ROLE_MAP } from '../constants/teams-translations';
|
||||
|
||||
/**
|
||||
* Workaround for E2E tests to not import `msg`.
|
||||
*/
|
||||
export enum DocumentSignatureType {
|
||||
DRAW = 'draw',
|
||||
TYPE = 'type',
|
||||
UPLOAD = 'upload',
|
||||
}
|
||||
|
||||
export const formatTeamUrl = (teamUrl: string, baseUrl?: string) => {
|
||||
const formattedBaseUrl = (baseUrl ?? NEXT_PUBLIC_WEBAPP_URL()).replace(/https?:\/\//, '');
|
||||
@ -9,12 +26,12 @@ export const formatTeamUrl = (teamUrl: string, baseUrl?: string) => {
|
||||
return `${formattedBaseUrl}/t/${teamUrl}`;
|
||||
};
|
||||
|
||||
export const formatDocumentsPath = (teamUrl?: string) => {
|
||||
return teamUrl ? `/t/${teamUrl}/documents` : '/documents';
|
||||
export const formatDocumentsPath = (teamUrl: string) => {
|
||||
return `/t/${teamUrl}/documents`;
|
||||
};
|
||||
|
||||
export const formatTemplatesPath = (teamUrl?: string) => {
|
||||
return teamUrl ? `/t/${teamUrl}/templates` : '/templates';
|
||||
export const formatTemplatesPath = (teamUrl: string) => {
|
||||
return `/t/${teamUrl}/templates`;
|
||||
};
|
||||
|
||||
/**
|
||||
@ -46,11 +63,26 @@ export const isTeamRoleWithinUserHierarchy = (
|
||||
return TEAM_MEMBER_ROLE_HIERARCHY[currentUserRole].some((i) => i === roleToCheck);
|
||||
};
|
||||
|
||||
export const getHighestTeamRoleInGroup = (groups: TeamGroup[]): TeamMemberRole => {
|
||||
let highestTeamRole: TeamMemberRole = LOWEST_TEAM_ROLE;
|
||||
|
||||
groups.forEach((group) => {
|
||||
const currentRolePriority = TEAM_MEMBER_ROLE_HIERARCHY[group.teamRole].length;
|
||||
const highestTeamRolePriority = TEAM_MEMBER_ROLE_HIERARCHY[highestTeamRole].length;
|
||||
|
||||
if (currentRolePriority > highestTeamRolePriority) {
|
||||
highestTeamRole = group.teamRole;
|
||||
}
|
||||
});
|
||||
|
||||
return highestTeamRole;
|
||||
};
|
||||
|
||||
export const extractTeamSignatureSettings = (
|
||||
settings?: {
|
||||
typedSignatureEnabled: boolean;
|
||||
drawSignatureEnabled: boolean;
|
||||
uploadSignatureEnabled: boolean;
|
||||
typedSignatureEnabled: boolean | null;
|
||||
drawSignatureEnabled: boolean | null;
|
||||
uploadSignatureEnabled: boolean | null;
|
||||
} | null,
|
||||
) => {
|
||||
if (!settings) {
|
||||
@ -73,3 +105,112 @@ export const extractTeamSignatureSettings = (
|
||||
|
||||
return signatureTypes;
|
||||
};
|
||||
|
||||
type BuildTeamWhereQueryOptions = {
|
||||
teamId: number | undefined;
|
||||
userId: number;
|
||||
roles?: TeamMemberRole[];
|
||||
};
|
||||
|
||||
export const buildTeamWhereQuery = ({
|
||||
teamId,
|
||||
userId,
|
||||
roles,
|
||||
}: BuildTeamWhereQueryOptions): Prisma.TeamWhereUniqueInput => {
|
||||
// Note: Not using inline ternary since typesafety breaks for some reason.
|
||||
if (!roles) {
|
||||
return {
|
||||
id: teamId,
|
||||
teamGroups: {
|
||||
some: {
|
||||
organisationGroup: {
|
||||
organisationGroupMembers: {
|
||||
some: {
|
||||
organisationMember: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
id: teamId,
|
||||
teamGroups: {
|
||||
some: {
|
||||
organisationGroup: {
|
||||
organisationGroupMembers: {
|
||||
some: {
|
||||
organisationMember: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
teamRole: {
|
||||
in: roles,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Majority of these are null which lets us inherit from the organisation settings.
|
||||
*/
|
||||
export const generateDefaultTeamSettings = (): Omit<TeamGlobalSettings, 'id' | 'team'> => {
|
||||
return {
|
||||
documentVisibility: null,
|
||||
documentLanguage: null,
|
||||
documentTimezone: null,
|
||||
documentDateFormat: null,
|
||||
|
||||
includeSenderDetails: null,
|
||||
includeSigningCertificate: null,
|
||||
includeAuditLog: null,
|
||||
|
||||
typedSignatureEnabled: null,
|
||||
uploadSignatureEnabled: null,
|
||||
drawSignatureEnabled: null,
|
||||
|
||||
brandingEnabled: null,
|
||||
brandingLogo: null,
|
||||
brandingUrl: null,
|
||||
brandingCompanyDetails: null,
|
||||
|
||||
emailDocumentSettings: null,
|
||||
emailId: null,
|
||||
emailReplyTo: null,
|
||||
// emailReplyToName: null,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Derive the final settings for a team.
|
||||
*
|
||||
* @param organisationSettings The organisation settings to inherit values from
|
||||
* @param teamSettings The team settings which can override the organisation settings
|
||||
*/
|
||||
export const extractDerivedTeamSettings = (
|
||||
organisationSettings: Omit<OrganisationGlobalSettings, 'id'>,
|
||||
teamSettings: Omit<TeamGlobalSettings, 'id'>,
|
||||
): Omit<OrganisationGlobalSettings, 'id'> => {
|
||||
const derivedSettings: Omit<OrganisationGlobalSettings, 'id'> = {
|
||||
...organisationSettings,
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
for (const key of Object.keys(derivedSettings) as (keyof typeof derivedSettings)[]) {
|
||||
const teamValue = teamSettings[key];
|
||||
|
||||
if (teamValue !== null) {
|
||||
// @ts-expect-error Should work
|
||||
derivedSettings[key] = teamValue;
|
||||
}
|
||||
}
|
||||
|
||||
return derivedSettings;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user