Merge branch 'main' into feat/signing-reminders

This commit is contained in:
Ephraim Atta-Duncan
2025-08-22 05:05:00 +00:00
977 changed files with 92471 additions and 41466 deletions

View File

@ -12,3 +12,5 @@ export const NEXT_PRIVATE_INTERNAL_WEBAPP_URL =
export const IS_BILLING_ENABLED = () => env('NEXT_PUBLIC_FEATURE_BILLING_ENABLED') === 'true';
export const API_V2_BETA_URL = '/api/v2-beta';
export const SUPPORT_EMAIL = env('NEXT_PUBLIC_SUPPORT_EMAIL') ?? 'support@documenso.com';

View File

@ -31,6 +31,7 @@ export const USER_SECURITY_AUDIT_LOG_MAP: Record<string, string> = {
PASSKEY_UPDATED: 'Passkey updated',
PASSWORD_RESET: 'Password reset',
PASSWORD_UPDATE: 'Password updated',
SESSION_REVOKED: 'Session revoked',
SIGN_OUT: 'Signed Out',
SIGN_IN: 'Signed In',
SIGN_IN_FAIL: 'Sign in attempt failed',

View File

@ -0,0 +1,8 @@
import { FieldType } from '@prisma/client';
export const AUTO_SIGNABLE_FIELD_TYPES: FieldType[] = [
FieldType.NAME,
FieldType.INITIALS,
FieldType.EMAIL,
FieldType.DATE,
];

View File

@ -1,12 +1,18 @@
export enum STRIPE_CUSTOMER_TYPE {
INDIVIDUAL = 'individual',
TEAM = 'team',
}
import { SubscriptionStatus } from '@prisma/client';
export enum STRIPE_PLAN_TYPE {
REGULAR = 'regular',
TEAM = 'team',
COMMUNITY = 'community',
FREE = 'free',
INDIVIDUAL = 'individual',
PRO = 'pro',
EARLY_ADOPTER = 'earlyAdopter',
PLATFORM = 'platform',
ENTERPRISE = 'enterprise',
}
export const FREE_TIER_DOCUMENT_QUOTA = 5;
export const SUBSCRIPTION_STATUS_MAP = {
[SubscriptionStatus.ACTIVE]: 'Active',
[SubscriptionStatus.INACTIVE]: 'Inactive',
[SubscriptionStatus.PAST_DUE]: 'Past Due',
};

View File

@ -9,6 +9,7 @@ export const VALID_DATE_FORMAT_VALUES = [
'yyyy-MM-dd',
'dd/MM/yyyy hh:mm a',
'MM/dd/yyyy hh:mm a',
'dd.MM.yyyy HH:mm',
'yyyy-MM-dd HH:mm',
'yy-MM-dd hh:mm a',
'yyyy-MM-dd HH:mm:ss',
@ -17,6 +18,8 @@ export const VALID_DATE_FORMAT_VALUES = [
"yyyy-MM-dd'T'HH:mm:ss.SSSXXX",
] as const;
export type ValidDateFormat = (typeof VALID_DATE_FORMAT_VALUES)[number];
export const DATE_FORMATS = [
{
key: 'yyyy-MM-dd_hh:mm_a',
@ -38,6 +41,11 @@ export const DATE_FORMATS = [
label: 'MM/DD/YYYY',
value: 'MM/dd/yyyy hh:mm a',
},
{
key: 'DDMMYYYYHHMM',
label: 'DD.MM.YYYY HH:mm',
value: 'dd.MM.yyyy HH:mm',
},
{
key: 'YYYYMMDDHHmm',
label: 'YYYY-MM-DD HH:mm',
@ -94,3 +102,7 @@ export const convertToLocalSystemFormat = (
return formattedDate;
};
export const isValidDateFormat = (dateFormat: unknown): dateFormat is ValidDateFormat => {
return VALID_DATE_FORMAT_VALUES.includes(dateFormat as ValidDateFormat);
};

View File

@ -19,6 +19,10 @@ export const DOCUMENT_AUTH_TYPES: Record<string, DocumentAuthTypeData> = {
key: DocumentAuth.TWO_FACTOR_AUTH,
value: 'Require 2FA',
},
[DocumentAuth.PASSWORD]: {
key: DocumentAuth.PASSWORD,
value: 'Require password',
},
[DocumentAuth.EXPLICIT_NONE]: {
key: DocumentAuth.EXPLICIT_NONE,
value: 'None (Overrides global settings)',

View File

@ -2,6 +2,13 @@ import type { MessageDescriptor } from '@lingui/core';
import { msg } from '@lingui/core/macro';
import { DocumentDistributionMethod, DocumentStatus } from '@prisma/client';
/**
* Workaround for E2E tests to not import `msg`.
*/
import { DocumentSignatureType } from '@documenso/lib/utils/teams';
export { DocumentSignatureType };
export const DOCUMENT_STATUS: {
[status in DocumentStatus]: { description: MessageDescriptor };
} = {
@ -35,12 +42,6 @@ export const DOCUMENT_DISTRIBUTION_METHODS: Record<string, DocumentDistributionM
},
} satisfies Record<DocumentDistributionMethod, DocumentDistributionMethodTypeData>;
export enum DocumentSignatureType {
DRAW = 'draw',
TYPE = 'type',
UPLOAD = 'upload',
}
type DocumentSignatureTypeData = {
label: MessageDescriptor;
value: DocumentSignatureType;
@ -48,15 +49,24 @@ type DocumentSignatureTypeData = {
export const DOCUMENT_SIGNATURE_TYPES = {
[DocumentSignatureType.DRAW]: {
label: msg`Draw`,
label: msg({
message: `Draw`,
context: `Draw signatute type`,
}),
value: DocumentSignatureType.DRAW,
},
[DocumentSignatureType.TYPE]: {
label: msg`Type`,
label: msg({
message: `Type`,
context: `Type signatute type`,
}),
value: DocumentSignatureType.TYPE,
},
[DocumentSignatureType.UPLOAD]: {
label: msg`Upload`,
label: msg({
message: `Upload`,
context: `Upload signatute type`,
}),
value: DocumentSignatureType.UPLOAD,
},
} satisfies Record<DocumentSignatureType, DocumentSignatureTypeData>;

View File

@ -3,6 +3,11 @@ import { env } from '../utils/env';
export const FROM_ADDRESS = env('NEXT_PRIVATE_SMTP_FROM_ADDRESS') || 'noreply@documenso.com';
export const FROM_NAME = env('NEXT_PRIVATE_SMTP_FROM_NAME') || 'Documenso';
export const DOCUMENSO_INTERNAL_EMAIL = {
name: FROM_NAME,
address: FROM_ADDRESS,
};
export const SERVICE_USER_EMAIL = 'serviceaccount@documenso.com';
export const EMAIL_VERIFICATION_STATE = {

View File

@ -2,7 +2,6 @@ import { env } from '@documenso/lib/utils/env';
import { NEXT_PUBLIC_WEBAPP_URL } from './app';
const NEXT_PUBLIC_FEATURE_BILLING_ENABLED = () => env('NEXT_PUBLIC_FEATURE_BILLING_ENABLED');
const NEXT_PUBLIC_POSTHOG_KEY = () => env('NEXT_PUBLIC_POSTHOG_KEY');
/**
@ -10,26 +9,6 @@ const NEXT_PUBLIC_POSTHOG_KEY = () => env('NEXT_PUBLIC_POSTHOG_KEY');
*/
export const FEATURE_FLAG_GLOBAL_SESSION_RECORDING = 'global_session_recording';
/**
* How frequent to poll for new feature flags in milliseconds.
*/
export const FEATURE_FLAG_POLL_INTERVAL = 30000;
/**
* Feature flags that will be used when PostHog is disabled.
*
* Does not take any person or group properties into account.
*/
export const LOCAL_FEATURE_FLAGS: Record<string, boolean> = {
app_allow_encrypted_documents: false,
app_billing: NEXT_PUBLIC_FEATURE_BILLING_ENABLED() === 'true',
app_document_page_view_history_sheet: false,
app_passkey: true,
app_public_profile: true,
marketing_header_single_player_mode: false,
marketing_profiles_announcement_bar: true,
} as const;
/**
* Extract the PostHog configuration from the environment.
*/
@ -46,10 +25,3 @@ export function extractPostHogConfig(): { key: string; host: string } | null {
host: postHogHost,
};
}
/**
* Whether feature flags are enabled for the current instance.
*/
export function isFeatureFlagEnabled(): boolean {
return extractPostHogConfig() !== null;
}

View File

@ -0,0 +1,25 @@
/**
* These constants are in a different file to avoid E2E tests from importing `msg`
* which will break it.
*/
import type { MessageDescriptor } from '@lingui/core';
import { msg } from '@lingui/core/macro';
import type { OrganisationMemberRole } from '@prisma/client';
export const ORGANISATION_MEMBER_ROLE_MAP: Record<
keyof typeof OrganisationMemberRole,
MessageDescriptor
> = {
ADMIN: msg`Admin`,
MANAGER: msg`Manager`,
MEMBER: msg`Member`,
};
export const EXTENDED_ORGANISATION_MEMBER_ROLE_MAP: Record<
keyof typeof OrganisationMemberRole,
MessageDescriptor
> = {
ADMIN: msg`Organisation Admin`,
MANAGER: msg`Organisation Manager`,
MEMBER: msg`Organisation Member`,
};

View File

@ -0,0 +1,128 @@
import { OrganisationGroupType, OrganisationMemberRole } from '@prisma/client';
export const ORGANISATION_URL_ROOT_REGEX = new RegExp('^/t/[^/]+/?$');
export const ORGANISATION_URL_REGEX = new RegExp('^/t/[^/]+');
export const ORGANISATION_INTERNAL_GROUPS: {
organisationRole: OrganisationMemberRole;
type: OrganisationGroupType;
}[] = [
{
organisationRole: OrganisationMemberRole.ADMIN,
type: OrganisationGroupType.INTERNAL_ORGANISATION,
},
{
organisationRole: OrganisationMemberRole.MANAGER,
type: OrganisationGroupType.INTERNAL_ORGANISATION,
},
{
organisationRole: OrganisationMemberRole.MEMBER,
type: OrganisationGroupType.INTERNAL_ORGANISATION,
},
] as const;
export const ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP = {
/**
* Includes permissions to:
* - Manage organisation members
* - Manage organisation settings, changing name, url, etc.
*/
DELETE_ORGANISATION: [OrganisationMemberRole.ADMIN],
MANAGE_BILLING: [OrganisationMemberRole.ADMIN],
DELETE_ORGANISATION_TRANSFER_REQUEST: [OrganisationMemberRole.ADMIN],
MANAGE_ORGANISATION: [OrganisationMemberRole.ADMIN, OrganisationMemberRole.MANAGER],
} satisfies Record<string, OrganisationMemberRole[]>;
/**
* A hierarchy of organisation member roles to determine which role has higher permission than another.
*
* Warning: The length of the array is used to determine the priority of the role.
* See `getHighestOrganisationRoleInGroup`
*/
export const ORGANISATION_MEMBER_ROLE_HIERARCHY = {
[OrganisationMemberRole.ADMIN]: [
OrganisationMemberRole.ADMIN,
OrganisationMemberRole.MANAGER,
OrganisationMemberRole.MEMBER,
],
[OrganisationMemberRole.MANAGER]: [OrganisationMemberRole.MANAGER, OrganisationMemberRole.MEMBER],
[OrganisationMemberRole.MEMBER]: [OrganisationMemberRole.MEMBER],
} satisfies Record<OrganisationMemberRole, OrganisationMemberRole[]>;
export const LOWEST_ORGANISATION_ROLE = OrganisationMemberRole.MEMBER;
export const PROTECTED_ORGANISATION_URLS = [
'403',
'404',
'500',
'502',
'503',
'504',
'about',
'account',
'admin',
'administrator',
'api',
'app',
'archive',
'auth',
'backup',
'config',
'configure',
'contact',
'contact-us',
'copyright',
'crime',
'criminal',
'dashboard',
'docs',
'documentation',
'document',
'documents',
'error',
'exploit',
'exploitation',
'exploiter',
'feedback',
'finance',
'forgot-password',
'fraud',
'fraudulent',
'hack',
'hacker',
'harassment',
'help',
'helpdesk',
'illegal',
'internal',
'legal',
'login',
'logout',
'maintenance',
'malware',
'newsletter',
'policy',
'privacy',
'profile',
'public',
'reset-password',
'scam',
'scammer',
'settings',
'setup',
'sign',
'signin',
'signout',
'signup',
'spam',
'support',
'system',
'organisation',
'terms',
'virus',
'webhook',
];
export const isOrganisationUrlProtected = (url: string) => {
return PROTECTED_ORGANISATION_URLS.some((protectedUrl) => url.startsWith(`/${protectedUrl}`));
};

View File

@ -4,39 +4,114 @@ import { RecipientRole } from '@prisma/client';
export const RECIPIENT_ROLES_DESCRIPTION = {
[RecipientRole.APPROVER]: {
actionVerb: msg`Approve`,
actioned: msg`Approved`,
progressiveVerb: msg`Approving`,
roleName: msg`Approver`,
roleNamePlural: msg`Approvers`,
actionVerb: msg({
message: `Approve`,
context: `Recipient role action verb`,
}),
actioned: msg({
message: `Approved`,
context: `Recipient role actioned`,
}),
progressiveVerb: msg({
message: `Approving`,
context: `Recipient role progressive verb`,
}),
roleName: msg({
message: `Approver`,
context: `Recipient role name`,
}),
roleNamePlural: msg({
message: `Approvers`,
context: `Recipient role plural name`,
}),
},
[RecipientRole.CC]: {
actionVerb: msg`CC`,
actioned: msg`CC'd`,
progressiveVerb: msg`CC`,
roleName: msg`Cc`,
roleNamePlural: msg`Ccers`,
actionVerb: msg({
message: `CC`,
context: `Recipient role action verb`,
}),
actioned: msg({
message: `CC'd`,
context: `Recipient role actioned`,
}),
progressiveVerb: msg({
message: `CC`,
context: `Recipient role progressive verb`,
}),
roleName: msg({
message: `Cc`,
context: `Recipient role name`,
}),
roleNamePlural: msg({
message: `Ccers`,
context: `Recipient role plural name`,
}),
},
[RecipientRole.SIGNER]: {
actionVerb: msg`Sign`,
actioned: msg`Signed`,
progressiveVerb: msg`Signing`,
roleName: msg`Signer`,
roleNamePlural: msg`Signers`,
actionVerb: msg({
message: `Sign`,
context: `Recipient role action verb`,
}),
actioned: msg({
message: `Signed`,
context: `Recipient role actioned`,
}),
progressiveVerb: msg({
message: `Signing`,
context: `Recipient role progressive verb`,
}),
roleName: msg({
message: `Signer`,
context: `Recipient role name`,
}),
roleNamePlural: msg({
message: `Signers`,
context: `Recipient role plural name`,
}),
},
[RecipientRole.VIEWER]: {
actionVerb: msg`View`,
actioned: msg`Viewed`,
progressiveVerb: msg`Viewing`,
roleName: msg`Viewer`,
roleNamePlural: msg`Viewers`,
actionVerb: msg({
message: `View`,
context: `Recipient role action verb`,
}),
actioned: msg({
message: `Viewed`,
context: `Recipient role actioned`,
}),
progressiveVerb: msg({
message: `Viewing`,
context: `Recipient role progressive verb`,
}),
roleName: msg({
message: `Viewer`,
context: `Recipient role name`,
}),
roleNamePlural: msg({
message: `Viewers`,
context: `Recipient role plural name`,
}),
},
[RecipientRole.ASSISTANT]: {
actionVerb: msg`Assist`,
actioned: msg`Assisted`,
progressiveVerb: msg`Assisting`,
roleName: msg`Assistant`,
roleNamePlural: msg`Assistants`,
actionVerb: msg({
message: `Assist`,
context: `Recipient role action verb`,
}),
actioned: msg({
message: `Assisted`,
context: `Recipient role actioned`,
}),
progressiveVerb: msg({
message: `Assisting`,
context: `Recipient role progressive verb`,
}),
roleName: msg({
message: `Assistant`,
context: `Recipient role name`,
}),
roleNamePlural: msg({
message: `Assistants`,
context: `Recipient role plural name`,
}),
},
} satisfies Record<keyof typeof RecipientRole, unknown>;

View File

@ -0,0 +1,16 @@
import type { MessageDescriptor } from '@lingui/core';
import { msg } from '@lingui/core/macro';
import type { TeamMemberRole } from '@prisma/client';
export const TEAM_MEMBER_ROLE_MAP: Record<keyof typeof TeamMemberRole, MessageDescriptor> = {
ADMIN: msg`Admin`,
MANAGER: msg`Manager`,
MEMBER: msg`Member`,
};
export const EXTENDED_TEAM_MEMBER_ROLE_MAP: Record<keyof typeof TeamMemberRole, MessageDescriptor> =
{
ADMIN: msg`Team Admin`,
MANAGER: msg`Team Manager`,
MEMBER: msg`Team Member`,
};

View File

@ -1,29 +1,43 @@
import type { MessageDescriptor } from '@lingui/core';
import { msg } from '@lingui/core/macro';
import { TeamMemberRole } from '@prisma/client';
import { OrganisationGroupType, TeamMemberRole } from '@prisma/client';
export const TEAM_URL_ROOT_REGEX = new RegExp('^/t/[^/]+/?$');
export const TEAM_URL_REGEX = new RegExp('^/t/[^/]+');
export const TEAM_MEMBER_ROLE_MAP: Record<keyof typeof TeamMemberRole, MessageDescriptor> = {
ADMIN: msg`Admin`,
MANAGER: msg`Manager`,
MEMBER: msg`Member`,
};
export const LOWEST_TEAM_ROLE = TeamMemberRole.MEMBER;
export const ALLOWED_TEAM_GROUP_TYPES: OrganisationGroupType[] = [
OrganisationGroupType.CUSTOM,
OrganisationGroupType.INTERNAL_ORGANISATION,
];
export const TEAM_INTERNAL_GROUPS: {
teamRole: TeamMemberRole;
type: OrganisationGroupType;
}[] = [
{
teamRole: TeamMemberRole.ADMIN,
type: OrganisationGroupType.INTERNAL_TEAM,
},
{
teamRole: TeamMemberRole.MANAGER,
type: OrganisationGroupType.INTERNAL_TEAM,
},
{
teamRole: TeamMemberRole.MEMBER,
type: OrganisationGroupType.INTERNAL_TEAM,
},
] as const;
export const TEAM_MEMBER_ROLE_PERMISSIONS_MAP = {
/**
* Includes permissions to:
* - Manage team members
* - Manage team settings, changing name, url, etc.
*/
DELETE_TEAM: [TeamMemberRole.ADMIN],
MANAGE_TEAM: [TeamMemberRole.ADMIN, TeamMemberRole.MANAGER],
MANAGE_BILLING: [TeamMemberRole.ADMIN],
DELETE_TEAM_TRANSFER_REQUEST: [TeamMemberRole.ADMIN],
} satisfies Record<string, TeamMemberRole[]>;
/**
* A hierarchy of team member roles to determine which role has higher permission than another.
*
* Warning: The length of the array is used to determine the priority of the role.
* See `getHighestTeamRoleInGroup`
*/
export const TEAM_MEMBER_ROLE_HIERARCHY = {
[TeamMemberRole.ADMIN]: [TeamMemberRole.ADMIN, TeamMemberRole.MANAGER, TeamMemberRole.MEMBER],

View File

@ -3,6 +3,10 @@ import { msg } from '@lingui/core/macro';
export const TEMPLATE_RECIPIENT_EMAIL_PLACEHOLDER_REGEX = /recipient\.\d+@documenso\.com/i;
export const TEMPLATE_RECIPIENT_NAME_PLACEHOLDER_REGEX = /Recipient \d+/i;
export const isTemplateRecipientEmailPlaceholder = (email: string) => {
return TEMPLATE_RECIPIENT_EMAIL_PLACEHOLDER_REGEX.test(email);
};
export const DIRECT_TEMPLATE_DOCUMENTATION = [
{
title: msg`Enable Direct Link Signing`,