mirror of
https://github.com/documenso/documenso.git
synced 2025-11-18 02:32:00 +10:00
chore: merge main
This commit is contained in:
75
packages/lib/client-only/hooks/use-element-bounds.ts
Normal file
75
packages/lib/client-only/hooks/use-element-bounds.ts
Normal file
@ -0,0 +1,75 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { getBoundingClientRect } from '@documenso/lib/client-only/get-bounding-client-rect';
|
||||
|
||||
export const useElementBounds = (elementOrSelector: HTMLElement | string, withScroll = false) => {
|
||||
const [bounds, setBounds] = useState({
|
||||
top: 0,
|
||||
left: 0,
|
||||
height: 0,
|
||||
width: 0,
|
||||
});
|
||||
|
||||
const calculateBounds = () => {
|
||||
const $el =
|
||||
typeof elementOrSelector === 'string'
|
||||
? document.querySelector<HTMLElement>(elementOrSelector)
|
||||
: elementOrSelector;
|
||||
|
||||
if (!$el) {
|
||||
throw new Error('Element not found');
|
||||
}
|
||||
|
||||
if (withScroll) {
|
||||
return getBoundingClientRect($el);
|
||||
}
|
||||
|
||||
const { top, left, width, height } = $el.getBoundingClientRect();
|
||||
|
||||
return {
|
||||
top,
|
||||
left,
|
||||
width,
|
||||
height,
|
||||
};
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setBounds(calculateBounds());
|
||||
}, [calculateBounds]);
|
||||
|
||||
useEffect(() => {
|
||||
const onResize = () => {
|
||||
setBounds(calculateBounds());
|
||||
};
|
||||
|
||||
window.addEventListener('resize', onResize);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', onResize);
|
||||
};
|
||||
}, [calculateBounds]);
|
||||
|
||||
useEffect(() => {
|
||||
const $el =
|
||||
typeof elementOrSelector === 'string'
|
||||
? document.querySelector<HTMLElement>(elementOrSelector)
|
||||
: elementOrSelector;
|
||||
|
||||
if (!$el) {
|
||||
return;
|
||||
}
|
||||
|
||||
const observer = new ResizeObserver(() => {
|
||||
setBounds(calculateBounds());
|
||||
});
|
||||
|
||||
observer.observe($el);
|
||||
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
};
|
||||
}, [calculateBounds]);
|
||||
|
||||
return bounds;
|
||||
};
|
||||
33
packages/lib/client-only/providers/organisation.tsx
Normal file
33
packages/lib/client-only/providers/organisation.tsx
Normal file
@ -0,0 +1,33 @@
|
||||
import { createContext, useContext } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
import type { OrganisationSession } from '@documenso/trpc/server/organisation-router/get-organisation-session.types';
|
||||
|
||||
type OrganisationProviderValue = OrganisationSession;
|
||||
|
||||
interface OrganisationProviderProps {
|
||||
children: React.ReactNode;
|
||||
organisation: OrganisationProviderValue | null;
|
||||
}
|
||||
|
||||
const OrganisationContext = createContext<OrganisationProviderValue | null>(null);
|
||||
|
||||
export const useCurrentOrganisation = () => {
|
||||
const context = useContext(OrganisationContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error('useCurrentOrganisation must be used within a OrganisationProvider');
|
||||
}
|
||||
|
||||
return context;
|
||||
};
|
||||
|
||||
export const useOptionalCurrentOrganisation = () => {
|
||||
return useContext(OrganisationContext);
|
||||
};
|
||||
|
||||
export const OrganisationProvider = ({ children, organisation }: OrganisationProviderProps) => {
|
||||
return (
|
||||
<OrganisationContext.Provider value={organisation}>{children}</OrganisationContext.Provider>
|
||||
);
|
||||
};
|
||||
@ -6,13 +6,15 @@ import { useLocation } from 'react-router';
|
||||
|
||||
import { authClient } from '@documenso/auth/client';
|
||||
import type { SessionUser } from '@documenso/auth/server/lib/session/session';
|
||||
import { type TGetTeamsResponse } from '@documenso/lib/server-only/team/get-teams';
|
||||
import { trpc } from '@documenso/trpc/client';
|
||||
import type { TGetOrganisationSessionResponse } from '@documenso/trpc/server/organisation-router/get-organisation-session.types';
|
||||
|
||||
import { SKIP_QUERY_BATCH_META } from '../../constants/trpc';
|
||||
|
||||
export type AppSession = {
|
||||
session: Session;
|
||||
user: SessionUser;
|
||||
teams: TGetTeamsResponse;
|
||||
organisations: TGetOrganisationSessionResponse;
|
||||
};
|
||||
|
||||
interface SessionProviderProps {
|
||||
@ -67,15 +69,17 @@ export const SessionProvider = ({ children, initialSession }: SessionProviderPro
|
||||
return;
|
||||
}
|
||||
|
||||
const teams = await trpc.team.getTeams.query().catch(() => {
|
||||
// Todo: (RR7) Log
|
||||
return [];
|
||||
});
|
||||
const organisations = await trpc.organisation.internal.getOrganisationSession
|
||||
.query(undefined, SKIP_QUERY_BATCH_META.trpc)
|
||||
.catch(() => {
|
||||
// Todo: (RR7) Log
|
||||
return [];
|
||||
});
|
||||
|
||||
setSession({
|
||||
session: newSession.session,
|
||||
user: newSession.user,
|
||||
teams,
|
||||
organisations,
|
||||
});
|
||||
}, []);
|
||||
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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',
|
||||
};
|
||||
|
||||
@ -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)',
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
25
packages/lib/constants/organisations-translations.ts
Normal file
25
packages/lib/constants/organisations-translations.ts
Normal 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`,
|
||||
};
|
||||
128
packages/lib/constants/organisations.ts
Normal file
128
packages/lib/constants/organisations.ts
Normal 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}`));
|
||||
};
|
||||
16
packages/lib/constants/teams-translations.ts
Normal file
16
packages/lib/constants/teams-translations.ts
Normal 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`,
|
||||
};
|
||||
@ -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],
|
||||
|
||||
@ -1,13 +1,14 @@
|
||||
import { JobClient } from './client/client';
|
||||
import { SEND_CONFIRMATION_EMAIL_JOB_DEFINITION } from './definitions/emails/send-confirmation-email';
|
||||
import { SEND_DOCUMENT_CANCELLED_EMAILS_JOB_DEFINITION } from './definitions/emails/send-document-cancelled-emails';
|
||||
import { SEND_ORGANISATION_MEMBER_JOINED_EMAIL_JOB_DEFINITION } from './definitions/emails/send-organisation-member-joined-email';
|
||||
import { SEND_ORGANISATION_MEMBER_LEFT_EMAIL_JOB_DEFINITION } from './definitions/emails/send-organisation-member-left-email';
|
||||
import { SEND_PASSWORD_RESET_SUCCESS_EMAIL_JOB_DEFINITION } from './definitions/emails/send-password-reset-success-email';
|
||||
import { SEND_RECIPIENT_SIGNED_EMAIL_JOB_DEFINITION } from './definitions/emails/send-recipient-signed-email';
|
||||
import { SEND_SIGNING_REJECTION_EMAILS_JOB_DEFINITION } from './definitions/emails/send-rejection-emails';
|
||||
import { SEND_SIGNING_EMAIL_JOB_DEFINITION } from './definitions/emails/send-signing-email';
|
||||
import { SEND_TEAM_DELETED_EMAIL_JOB_DEFINITION } from './definitions/emails/send-team-deleted-email';
|
||||
import { SEND_TEAM_MEMBER_JOINED_EMAIL_JOB_DEFINITION } from './definitions/emails/send-team-member-joined-email';
|
||||
import { SEND_TEAM_MEMBER_LEFT_EMAIL_JOB_DEFINITION } from './definitions/emails/send-team-member-left-email';
|
||||
import { BACKPORT_SUBSCRIPTION_CLAIM_JOB_DEFINITION } from './definitions/internal/backport-subscription-claims';
|
||||
import { BULK_SEND_TEMPLATE_JOB_DEFINITION } from './definitions/internal/bulk-send-template';
|
||||
import { EXECUTE_WEBHOOK_JOB_DEFINITION } from './definitions/internal/execute-webhook';
|
||||
import { SEAL_DOCUMENT_JOB_DEFINITION } from './definitions/internal/seal-document';
|
||||
@ -19,14 +20,15 @@ import { SEAL_DOCUMENT_JOB_DEFINITION } from './definitions/internal/seal-docume
|
||||
export const jobsClient = new JobClient([
|
||||
SEND_SIGNING_EMAIL_JOB_DEFINITION,
|
||||
SEND_CONFIRMATION_EMAIL_JOB_DEFINITION,
|
||||
SEND_TEAM_MEMBER_JOINED_EMAIL_JOB_DEFINITION,
|
||||
SEND_TEAM_MEMBER_LEFT_EMAIL_JOB_DEFINITION,
|
||||
SEND_ORGANISATION_MEMBER_JOINED_EMAIL_JOB_DEFINITION,
|
||||
SEND_ORGANISATION_MEMBER_LEFT_EMAIL_JOB_DEFINITION,
|
||||
SEND_TEAM_DELETED_EMAIL_JOB_DEFINITION,
|
||||
SEAL_DOCUMENT_JOB_DEFINITION,
|
||||
SEND_PASSWORD_RESET_SUCCESS_EMAIL_JOB_DEFINITION,
|
||||
SEND_SIGNING_REJECTION_EMAILS_JOB_DEFINITION,
|
||||
SEND_RECIPIENT_SIGNED_EMAIL_JOB_DEFINITION,
|
||||
SEND_DOCUMENT_CANCELLED_EMAILS_JOB_DEFINITION,
|
||||
BACKPORT_SUBSCRIPTION_CLAIM_JOB_DEFINITION,
|
||||
BULK_SEND_TEMPLATE_JOB_DEFINITION,
|
||||
EXECUTE_WEBHOOK_JOB_DEFINITION,
|
||||
] as const);
|
||||
|
||||
@ -1,39 +0,0 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { ZRequestMetadataSchema } from '../../../universal/extract-request-metadata';
|
||||
import { type JobDefinition } from '../../client/_internal/job';
|
||||
|
||||
const SEND_BULK_COMPLETE_EMAIL_JOB_DEFINITION_ID = 'send.bulk.complete.email';
|
||||
|
||||
const SEND_BULK_COMPLETE_EMAIL_JOB_DEFINITION_SCHEMA = z.object({
|
||||
userId: z.number(),
|
||||
templateId: z.number(),
|
||||
templateName: z.string(),
|
||||
totalProcessed: z.number(),
|
||||
successCount: z.number(),
|
||||
failedCount: z.number(),
|
||||
errors: z.array(z.string()),
|
||||
requestMetadata: ZRequestMetadataSchema.optional(),
|
||||
});
|
||||
|
||||
export type TSendBulkCompleteEmailJobDefinition = z.infer<
|
||||
typeof SEND_BULK_COMPLETE_EMAIL_JOB_DEFINITION_SCHEMA
|
||||
>;
|
||||
|
||||
export const SEND_BULK_COMPLETE_EMAIL_JOB_DEFINITION = {
|
||||
id: SEND_BULK_COMPLETE_EMAIL_JOB_DEFINITION_ID,
|
||||
name: 'Send Bulk Complete Email',
|
||||
version: '1.0.0',
|
||||
trigger: {
|
||||
name: SEND_BULK_COMPLETE_EMAIL_JOB_DEFINITION_ID,
|
||||
schema: SEND_BULK_COMPLETE_EMAIL_JOB_DEFINITION_SCHEMA,
|
||||
},
|
||||
handler: async ({ payload, io }) => {
|
||||
const handler = await import('./send-bulk-complete-email.handler');
|
||||
|
||||
await handler.run({ payload, io });
|
||||
},
|
||||
} as const satisfies JobDefinition<
|
||||
typeof SEND_BULK_COMPLETE_EMAIL_JOB_DEFINITION_ID,
|
||||
TSendBulkCompleteEmailJobDefinition
|
||||
>;
|
||||
@ -10,9 +10,9 @@ import { prisma } from '@documenso/prisma';
|
||||
import { getI18nInstance } from '../../../client-only/providers/i18n-server';
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../../constants/app';
|
||||
import { FROM_ADDRESS, FROM_NAME } from '../../../constants/email';
|
||||
import { getEmailContext } from '../../../server-only/email/get-email-context';
|
||||
import { extractDerivedDocumentEmailSettings } from '../../../types/document-email';
|
||||
import { renderEmailWithI18N } from '../../../utils/render-email-with-i18n';
|
||||
import { teamGlobalSettingsToBranding } from '../../../utils/team-global-settings-to-branding';
|
||||
import type { JobRunIO } from '../../client/_internal/job';
|
||||
import type { TSendDocumentCancelledEmailsJobDefinition } from './send-document-cancelled-emails';
|
||||
|
||||
@ -38,12 +38,18 @@ export const run = async ({
|
||||
teamEmail: true,
|
||||
name: true,
|
||||
url: true,
|
||||
teamGlobalSettings: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { branding, settings } = await getEmailContext({
|
||||
source: {
|
||||
type: 'team',
|
||||
teamId: document.teamId,
|
||||
},
|
||||
});
|
||||
|
||||
const { documentMeta, user: documentOwner } = document;
|
||||
|
||||
// Check if document cancellation emails are enabled
|
||||
@ -53,7 +59,9 @@ export const run = async ({
|
||||
return;
|
||||
}
|
||||
|
||||
const i18n = await getI18nInstance(documentMeta?.language);
|
||||
const lang = documentMeta?.language ?? settings.documentLanguage;
|
||||
|
||||
const i18n = await getI18nInstance(lang);
|
||||
|
||||
// Send cancellation emails to all recipients who have been sent the document or viewed it
|
||||
const recipientsToNotify = document.recipients.filter(
|
||||
@ -73,14 +81,10 @@ export const run = async ({
|
||||
cancellationReason: cancellationReason || 'The document has been cancelled.',
|
||||
});
|
||||
|
||||
const branding = document.team?.teamGlobalSettings
|
||||
? teamGlobalSettingsToBranding(document.team.teamGlobalSettings)
|
||||
: undefined;
|
||||
|
||||
const [html, text] = await Promise.all([
|
||||
renderEmailWithI18N(template, { lang: documentMeta?.language, branding }),
|
||||
renderEmailWithI18N(template, { lang, branding }),
|
||||
renderEmailWithI18N(template, {
|
||||
lang: documentMeta?.language,
|
||||
lang,
|
||||
branding,
|
||||
plainText: true,
|
||||
}),
|
||||
|
||||
@ -1,78 +1,86 @@
|
||||
import { createElement } from 'react';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { TeamMemberRole } from '@prisma/client';
|
||||
|
||||
import { mailer } from '@documenso/email/mailer';
|
||||
import TeamJoinEmailTemplate from '@documenso/email/templates/team-join';
|
||||
import OrganisationJoinEmailTemplate from '@documenso/email/templates/organisation-join';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { getI18nInstance } from '../../../client-only/providers/i18n-server';
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../../constants/app';
|
||||
import { FROM_ADDRESS, FROM_NAME } from '../../../constants/email';
|
||||
import { ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP } from '../../../constants/organisations';
|
||||
import { getEmailContext } from '../../../server-only/email/get-email-context';
|
||||
import { renderEmailWithI18N } from '../../../utils/render-email-with-i18n';
|
||||
import { teamGlobalSettingsToBranding } from '../../../utils/team-global-settings-to-branding';
|
||||
import type { JobRunIO } from '../../client/_internal/job';
|
||||
import type { TSendTeamMemberJoinedEmailJobDefinition } from './send-team-member-joined-email';
|
||||
import type { TSendOrganisationMemberJoinedEmailJobDefinition } from './send-organisation-member-joined-email';
|
||||
|
||||
export const run = async ({
|
||||
payload,
|
||||
io,
|
||||
}: {
|
||||
payload: TSendTeamMemberJoinedEmailJobDefinition;
|
||||
payload: TSendOrganisationMemberJoinedEmailJobDefinition;
|
||||
io: JobRunIO;
|
||||
}) => {
|
||||
const team = await prisma.team.findFirstOrThrow({
|
||||
const organisation = await prisma.organisation.findFirstOrThrow({
|
||||
where: {
|
||||
id: payload.teamId,
|
||||
id: payload.organisationId,
|
||||
},
|
||||
include: {
|
||||
members: {
|
||||
where: {
|
||||
role: {
|
||||
in: [TeamMemberRole.ADMIN, TeamMemberRole.MANAGER],
|
||||
organisationGroupMembers: {
|
||||
some: {
|
||||
group: {
|
||||
organisationRole: {
|
||||
in: ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_ORGANISATION'],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
include: {
|
||||
user: true,
|
||||
},
|
||||
},
|
||||
teamGlobalSettings: true,
|
||||
},
|
||||
});
|
||||
|
||||
const invitedMember = await prisma.teamMember.findFirstOrThrow({
|
||||
const invitedMember = await prisma.organisationMember.findFirstOrThrow({
|
||||
where: {
|
||||
id: payload.memberId,
|
||||
teamId: payload.teamId,
|
||||
userId: payload.memberUserId,
|
||||
organisationId: payload.organisationId,
|
||||
},
|
||||
include: {
|
||||
user: true,
|
||||
},
|
||||
});
|
||||
|
||||
for (const member of team.members) {
|
||||
const { branding, settings } = await getEmailContext({
|
||||
source: {
|
||||
type: 'organisation',
|
||||
organisationId: organisation.id,
|
||||
},
|
||||
});
|
||||
|
||||
for (const member of organisation.members) {
|
||||
if (member.id === invitedMember.id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
await io.runTask(
|
||||
`send-team-member-joined-email--${invitedMember.id}_${member.id}`,
|
||||
`send-organisation-member-joined-email--${invitedMember.id}_${member.id}`,
|
||||
async () => {
|
||||
const emailContent = createElement(TeamJoinEmailTemplate, {
|
||||
const emailContent = createElement(OrganisationJoinEmailTemplate, {
|
||||
assetBaseUrl: NEXT_PUBLIC_WEBAPP_URL(),
|
||||
baseUrl: NEXT_PUBLIC_WEBAPP_URL(),
|
||||
memberName: invitedMember.user.name || '',
|
||||
memberEmail: invitedMember.user.email,
|
||||
teamName: team.name,
|
||||
teamUrl: team.url,
|
||||
organisationName: organisation.name,
|
||||
organisationUrl: organisation.url,
|
||||
});
|
||||
|
||||
const branding = team.teamGlobalSettings
|
||||
? teamGlobalSettingsToBranding(team.teamGlobalSettings)
|
||||
: undefined;
|
||||
|
||||
const lang = team.teamGlobalSettings?.documentLanguage;
|
||||
const lang = settings.documentLanguage;
|
||||
|
||||
// !: Replace with the actual language of the recipient later
|
||||
const [html, text] = await Promise.all([
|
||||
@ -95,7 +103,7 @@ export const run = async ({
|
||||
name: FROM_NAME,
|
||||
address: FROM_ADDRESS,
|
||||
},
|
||||
subject: i18n._(msg`A new member has joined your team`),
|
||||
subject: i18n._(msg`A new member has joined your organisation`),
|
||||
html,
|
||||
text,
|
||||
});
|
||||
@ -0,0 +1,33 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import type { JobDefinition } from '../../client/_internal/job';
|
||||
|
||||
const SEND_ORGANISATION_MEMBER_JOINED_EMAIL_JOB_DEFINITION_ID =
|
||||
'send.organisation-member-joined.email';
|
||||
|
||||
const SEND_ORGANISATION_MEMBER_JOINED_EMAIL_JOB_DEFINITION_SCHEMA = z.object({
|
||||
organisationId: z.string(),
|
||||
memberUserId: z.number(),
|
||||
});
|
||||
|
||||
export type TSendOrganisationMemberJoinedEmailJobDefinition = z.infer<
|
||||
typeof SEND_ORGANISATION_MEMBER_JOINED_EMAIL_JOB_DEFINITION_SCHEMA
|
||||
>;
|
||||
|
||||
export const SEND_ORGANISATION_MEMBER_JOINED_EMAIL_JOB_DEFINITION = {
|
||||
id: SEND_ORGANISATION_MEMBER_JOINED_EMAIL_JOB_DEFINITION_ID,
|
||||
name: 'Send Organisation Member Joined Email',
|
||||
version: '1.0.0',
|
||||
trigger: {
|
||||
name: SEND_ORGANISATION_MEMBER_JOINED_EMAIL_JOB_DEFINITION_ID,
|
||||
schema: SEND_ORGANISATION_MEMBER_JOINED_EMAIL_JOB_DEFINITION_SCHEMA,
|
||||
},
|
||||
handler: async ({ payload, io }) => {
|
||||
const handler = await import('./send-organisation-member-joined-email.handler');
|
||||
|
||||
await handler.run({ payload, io });
|
||||
},
|
||||
} as const satisfies JobDefinition<
|
||||
typeof SEND_ORGANISATION_MEMBER_JOINED_EMAIL_JOB_DEFINITION_ID,
|
||||
TSendOrganisationMemberJoinedEmailJobDefinition
|
||||
>;
|
||||
@ -0,0 +1,108 @@
|
||||
import { createElement } from 'react';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
|
||||
import { mailer } from '@documenso/email/mailer';
|
||||
import OrganisationLeaveEmailTemplate from '@documenso/email/templates/organisation-leave';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { getI18nInstance } from '../../../client-only/providers/i18n-server';
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../../constants/app';
|
||||
import { FROM_ADDRESS, FROM_NAME } from '../../../constants/email';
|
||||
import { ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP } from '../../../constants/organisations';
|
||||
import { getEmailContext } from '../../../server-only/email/get-email-context';
|
||||
import { renderEmailWithI18N } from '../../../utils/render-email-with-i18n';
|
||||
import type { JobRunIO } from '../../client/_internal/job';
|
||||
import type { TSendOrganisationMemberLeftEmailJobDefinition } from './send-organisation-member-left-email';
|
||||
|
||||
export const run = async ({
|
||||
payload,
|
||||
io,
|
||||
}: {
|
||||
payload: TSendOrganisationMemberLeftEmailJobDefinition;
|
||||
io: JobRunIO;
|
||||
}) => {
|
||||
const organisation = await prisma.organisation.findFirstOrThrow({
|
||||
where: {
|
||||
id: payload.organisationId,
|
||||
},
|
||||
include: {
|
||||
members: {
|
||||
where: {
|
||||
organisationGroupMembers: {
|
||||
some: {
|
||||
group: {
|
||||
organisationRole: {
|
||||
in: ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_ORGANISATION'],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
include: {
|
||||
user: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const oldMember = await prisma.user.findFirstOrThrow({
|
||||
where: {
|
||||
id: payload.memberUserId,
|
||||
},
|
||||
});
|
||||
|
||||
const { branding, settings } = await getEmailContext({
|
||||
source: {
|
||||
type: 'organisation',
|
||||
organisationId: organisation.id,
|
||||
},
|
||||
});
|
||||
|
||||
for (const member of organisation.members) {
|
||||
if (member.userId === oldMember.id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
await io.runTask(
|
||||
`send-organisation-member-left-email--${oldMember.id}_${member.id}`,
|
||||
async () => {
|
||||
const emailContent = createElement(OrganisationLeaveEmailTemplate, {
|
||||
assetBaseUrl: NEXT_PUBLIC_WEBAPP_URL(),
|
||||
baseUrl: NEXT_PUBLIC_WEBAPP_URL(),
|
||||
memberName: oldMember.name || '',
|
||||
memberEmail: oldMember.email,
|
||||
organisationName: organisation.name,
|
||||
organisationUrl: organisation.url,
|
||||
});
|
||||
|
||||
const lang = settings.documentLanguage;
|
||||
|
||||
const [html, text] = await Promise.all([
|
||||
renderEmailWithI18N(emailContent, {
|
||||
lang,
|
||||
branding,
|
||||
}),
|
||||
renderEmailWithI18N(emailContent, {
|
||||
lang,
|
||||
branding,
|
||||
plainText: true,
|
||||
}),
|
||||
]);
|
||||
|
||||
const i18n = await getI18nInstance(lang);
|
||||
|
||||
await mailer.sendMail({
|
||||
to: member.user.email,
|
||||
from: {
|
||||
name: FROM_NAME,
|
||||
address: FROM_ADDRESS,
|
||||
},
|
||||
subject: i18n._(msg`A member has left your organisation`),
|
||||
html,
|
||||
text,
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,32 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import type { JobDefinition } from '../../client/_internal/job';
|
||||
|
||||
const SEND_ORGANISATION_MEMBER_LEFT_EMAIL_JOB_DEFINITION_ID = 'send.organisation-member-left.email';
|
||||
|
||||
const SEND_ORGANISATION_MEMBER_LEFT_EMAIL_JOB_DEFINITION_SCHEMA = z.object({
|
||||
organisationId: z.string(),
|
||||
memberUserId: z.number(),
|
||||
});
|
||||
|
||||
export type TSendOrganisationMemberLeftEmailJobDefinition = z.infer<
|
||||
typeof SEND_ORGANISATION_MEMBER_LEFT_EMAIL_JOB_DEFINITION_SCHEMA
|
||||
>;
|
||||
|
||||
export const SEND_ORGANISATION_MEMBER_LEFT_EMAIL_JOB_DEFINITION = {
|
||||
id: SEND_ORGANISATION_MEMBER_LEFT_EMAIL_JOB_DEFINITION_ID,
|
||||
name: 'Send Organisation Member Left Email',
|
||||
version: '1.0.0',
|
||||
trigger: {
|
||||
name: SEND_ORGANISATION_MEMBER_LEFT_EMAIL_JOB_DEFINITION_ID,
|
||||
schema: SEND_ORGANISATION_MEMBER_LEFT_EMAIL_JOB_DEFINITION_SCHEMA,
|
||||
},
|
||||
handler: async ({ payload, io }) => {
|
||||
const handler = await import('./send-organisation-member-left-email.handler');
|
||||
|
||||
await handler.run({ payload, io });
|
||||
},
|
||||
} as const satisfies JobDefinition<
|
||||
typeof SEND_ORGANISATION_MEMBER_LEFT_EMAIL_JOB_DEFINITION_ID,
|
||||
TSendOrganisationMemberLeftEmailJobDefinition
|
||||
>;
|
||||
@ -9,9 +9,9 @@ import { prisma } from '@documenso/prisma';
|
||||
import { getI18nInstance } from '../../../client-only/providers/i18n-server';
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../../constants/app';
|
||||
import { FROM_ADDRESS, FROM_NAME } from '../../../constants/email';
|
||||
import { getEmailContext } from '../../../server-only/email/get-email-context';
|
||||
import { extractDerivedDocumentEmailSettings } from '../../../types/document-email';
|
||||
import { renderEmailWithI18N } from '../../../utils/render-email-with-i18n';
|
||||
import { teamGlobalSettingsToBranding } from '../../../utils/team-global-settings-to-branding';
|
||||
import type { JobRunIO } from '../../client/_internal/job';
|
||||
import type { TSendRecipientSignedEmailJobDefinition } from './send-recipient-signed-email';
|
||||
|
||||
@ -41,11 +41,6 @@ export const run = async ({
|
||||
},
|
||||
user: true,
|
||||
documentMeta: true,
|
||||
team: {
|
||||
include: {
|
||||
teamGlobalSettings: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@ -76,8 +71,17 @@ export const run = async ({
|
||||
return;
|
||||
}
|
||||
|
||||
const { branding, settings } = await getEmailContext({
|
||||
source: {
|
||||
type: 'team',
|
||||
teamId: document.teamId,
|
||||
},
|
||||
});
|
||||
|
||||
const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000';
|
||||
const i18n = await getI18nInstance(document.documentMeta?.language);
|
||||
|
||||
const lang = document.documentMeta?.language ?? settings.documentLanguage;
|
||||
const i18n = await getI18nInstance(lang);
|
||||
|
||||
const template = createElement(DocumentRecipientSignedEmailTemplate, {
|
||||
documentName: document.title,
|
||||
@ -87,14 +91,10 @@ export const run = async ({
|
||||
});
|
||||
|
||||
await io.runTask('send-recipient-signed-email', async () => {
|
||||
const branding = document.team?.teamGlobalSettings
|
||||
? teamGlobalSettingsToBranding(document.team.teamGlobalSettings)
|
||||
: undefined;
|
||||
|
||||
const [html, text] = await Promise.all([
|
||||
renderEmailWithI18N(template, { lang: document.documentMeta?.language, branding }),
|
||||
renderEmailWithI18N(template, { lang, branding }),
|
||||
renderEmailWithI18N(template, {
|
||||
lang: document.documentMeta?.language,
|
||||
lang,
|
||||
branding,
|
||||
plainText: true,
|
||||
}),
|
||||
|
||||
@ -11,9 +11,9 @@ import { prisma } from '@documenso/prisma';
|
||||
import { getI18nInstance } from '../../../client-only/providers/i18n-server';
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../../constants/app';
|
||||
import { FROM_ADDRESS, FROM_NAME } from '../../../constants/email';
|
||||
import { getEmailContext } from '../../../server-only/email/get-email-context';
|
||||
import { extractDerivedDocumentEmailSettings } from '../../../types/document-email';
|
||||
import { renderEmailWithI18N } from '../../../utils/render-email-with-i18n';
|
||||
import { teamGlobalSettingsToBranding } from '../../../utils/team-global-settings-to-branding';
|
||||
import { formatDocumentsPath } from '../../../utils/teams';
|
||||
import type { JobRunIO } from '../../client/_internal/job';
|
||||
import type { TSendSigningRejectionEmailsJobDefinition } from './send-rejection-emails';
|
||||
@ -40,7 +40,6 @@ export const run = async ({
|
||||
teamEmail: true,
|
||||
name: true,
|
||||
url: true,
|
||||
teamGlobalSettings: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -63,7 +62,16 @@ export const run = async ({
|
||||
return;
|
||||
}
|
||||
|
||||
const i18n = await getI18nInstance(documentMeta?.language);
|
||||
const { branding, settings } = await getEmailContext({
|
||||
source: {
|
||||
type: 'team',
|
||||
teamId: document.teamId,
|
||||
},
|
||||
});
|
||||
|
||||
const lang = documentMeta?.language ?? settings.documentLanguage;
|
||||
|
||||
const i18n = await getI18nInstance(lang);
|
||||
|
||||
// Send confirmation email to the recipient who rejected
|
||||
await io.runTask('send-rejection-confirmation-email', async () => {
|
||||
@ -75,14 +83,10 @@ export const run = async ({
|
||||
assetBaseUrl: NEXT_PUBLIC_WEBAPP_URL(),
|
||||
});
|
||||
|
||||
const branding = document.team?.teamGlobalSettings
|
||||
? teamGlobalSettingsToBranding(document.team.teamGlobalSettings)
|
||||
: undefined;
|
||||
|
||||
const [html, text] = await Promise.all([
|
||||
renderEmailWithI18N(recipientTemplate, { lang: documentMeta?.language, branding }),
|
||||
renderEmailWithI18N(recipientTemplate, { lang, branding }),
|
||||
renderEmailWithI18N(recipientTemplate, {
|
||||
lang: documentMeta?.language,
|
||||
lang,
|
||||
branding,
|
||||
plainText: true,
|
||||
}),
|
||||
@ -115,14 +119,10 @@ export const run = async ({
|
||||
assetBaseUrl: NEXT_PUBLIC_WEBAPP_URL(),
|
||||
});
|
||||
|
||||
const branding = document.team?.teamGlobalSettings
|
||||
? teamGlobalSettingsToBranding(document.team.teamGlobalSettings)
|
||||
: undefined;
|
||||
|
||||
const [html, text] = await Promise.all([
|
||||
renderEmailWithI18N(ownerTemplate, { lang: documentMeta?.language, branding }),
|
||||
renderEmailWithI18N(ownerTemplate, { lang, branding }),
|
||||
renderEmailWithI18N(ownerTemplate, {
|
||||
lang: documentMeta?.language,
|
||||
lang,
|
||||
branding,
|
||||
plainText: true,
|
||||
}),
|
||||
|
||||
@ -1,7 +1,13 @@
|
||||
import { createElement } from 'react';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { DocumentSource, DocumentStatus, RecipientRole, SendStatus } from '@prisma/client';
|
||||
import {
|
||||
DocumentSource,
|
||||
DocumentStatus,
|
||||
OrganisationType,
|
||||
RecipientRole,
|
||||
SendStatus,
|
||||
} from '@prisma/client';
|
||||
|
||||
import { mailer } from '@documenso/email/mailer';
|
||||
import DocumentInviteEmailTemplate from '@documenso/email/templates/document-invite';
|
||||
@ -14,12 +20,12 @@ import {
|
||||
RECIPIENT_ROLES_DESCRIPTION,
|
||||
RECIPIENT_ROLE_TO_EMAIL_TYPE,
|
||||
} from '../../../constants/recipient-roles';
|
||||
import { getEmailContext } from '../../../server-only/email/get-email-context';
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../../types/document-audit-logs';
|
||||
import { extractDerivedDocumentEmailSettings } from '../../../types/document-email';
|
||||
import { createDocumentAuditLogData } from '../../../utils/document-audit-logs';
|
||||
import { renderCustomEmailTemplate } from '../../../utils/render-custom-email-template';
|
||||
import { renderEmailWithI18N } from '../../../utils/render-email-with-i18n';
|
||||
import { teamGlobalSettingsToBranding } from '../../../utils/team-global-settings-to-branding';
|
||||
import type { JobRunIO } from '../../client/_internal/job';
|
||||
import type { TSendSigningEmailJobDefinition } from './send-signing-email';
|
||||
|
||||
@ -49,7 +55,6 @@ export const run = async ({
|
||||
select: {
|
||||
teamEmail: true,
|
||||
name: true,
|
||||
teamGlobalSettings: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -75,16 +80,24 @@ export const run = async ({
|
||||
return;
|
||||
}
|
||||
|
||||
const { branding, settings, organisationType } = await getEmailContext({
|
||||
source: {
|
||||
type: 'team',
|
||||
teamId: document.teamId,
|
||||
},
|
||||
});
|
||||
|
||||
const customEmail = document?.documentMeta;
|
||||
const isDirectTemplate = document.source === DocumentSource.TEMPLATE_DIRECT_LINK;
|
||||
const isTeamDocument = document.teamId !== null;
|
||||
|
||||
const recipientEmailType = RECIPIENT_ROLE_TO_EMAIL_TYPE[recipient.role];
|
||||
|
||||
const { email, name } = recipient;
|
||||
const selfSigner = email === user.email;
|
||||
|
||||
const i18n = await getI18nInstance(documentMeta?.language);
|
||||
const lang = documentMeta?.language ?? settings.documentLanguage;
|
||||
|
||||
const i18n = await getI18nInstance(lang);
|
||||
|
||||
const recipientActionVerb = i18n
|
||||
._(RECIPIENT_ROLES_DESCRIPTION[recipient.role].actionVerb)
|
||||
@ -109,7 +122,7 @@ export const run = async ({
|
||||
);
|
||||
}
|
||||
|
||||
if (isTeamDocument && team) {
|
||||
if (organisationType === OrganisationType.ORGANISATION) {
|
||||
emailSubject = i18n._(msg`${team.name} invited you to ${recipientActionVerb} a document`);
|
||||
emailMessage = customEmail?.message ?? '';
|
||||
|
||||
@ -117,7 +130,7 @@ export const run = async ({
|
||||
const inviterName = user.name || '';
|
||||
|
||||
emailMessage = i18n._(
|
||||
team.teamGlobalSettings?.includeSenderDetails
|
||||
settings.includeSenderDetails
|
||||
? msg`${inviterName} on behalf of "${team.name}" has invited you to ${recipientActionVerb} the document "${document.title}".`
|
||||
: msg`${team.name} has invited you to ${recipientActionVerb} the document "${document.title}".`,
|
||||
);
|
||||
@ -136,27 +149,26 @@ export const run = async ({
|
||||
const template = createElement(DocumentInviteEmailTemplate, {
|
||||
documentName: document.title,
|
||||
inviterName: user.name || undefined,
|
||||
inviterEmail: isTeamDocument ? team?.teamEmail?.email || user.email : user.email,
|
||||
inviterEmail:
|
||||
organisationType === OrganisationType.ORGANISATION
|
||||
? team?.teamEmail?.email || user.email
|
||||
: user.email,
|
||||
assetBaseUrl,
|
||||
signDocumentLink,
|
||||
customBody: renderCustomEmailTemplate(emailMessage, customEmailTemplate),
|
||||
role: recipient.role,
|
||||
selfSigner,
|
||||
isTeamInvite: isTeamDocument,
|
||||
organisationType,
|
||||
teamName: team?.name,
|
||||
teamEmail: team?.teamEmail?.email,
|
||||
includeSenderDetails: team?.teamGlobalSettings?.includeSenderDetails,
|
||||
includeSenderDetails: settings.includeSenderDetails,
|
||||
});
|
||||
|
||||
await io.runTask('send-signing-email', async () => {
|
||||
const branding = document.team?.teamGlobalSettings
|
||||
? teamGlobalSettingsToBranding(document.team.teamGlobalSettings)
|
||||
: undefined;
|
||||
|
||||
const [html, text] = await Promise.all([
|
||||
renderEmailWithI18N(template, { lang: documentMeta?.language, branding }),
|
||||
renderEmailWithI18N(template, { lang, branding }),
|
||||
renderEmailWithI18N(template, {
|
||||
lang: documentMeta?.language,
|
||||
lang,
|
||||
branding,
|
||||
plainText: true,
|
||||
}),
|
||||
|
||||
@ -9,14 +9,14 @@ export const run = async ({
|
||||
payload: TSendTeamDeletedEmailJobDefinition;
|
||||
io: JobRunIO;
|
||||
}) => {
|
||||
const { team, members } = payload;
|
||||
const { team, members, organisationId } = payload;
|
||||
|
||||
for (const member of members) {
|
||||
await io.runTask(`send-team-deleted-email--${team.url}_${member.id}`, async () => {
|
||||
await sendTeamDeleteEmail({
|
||||
email: member.email,
|
||||
team,
|
||||
isOwner: member.id === team.ownerUserId,
|
||||
organisationId,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import { DocumentVisibility } from '@prisma/client';
|
||||
import { z } from 'zod';
|
||||
|
||||
import type { JobDefinition } from '../../client/_internal/job';
|
||||
@ -6,28 +5,10 @@ import type { JobDefinition } from '../../client/_internal/job';
|
||||
const SEND_TEAM_DELETED_EMAIL_JOB_DEFINITION_ID = 'send.team-deleted.email';
|
||||
|
||||
const SEND_TEAM_DELETED_EMAIL_JOB_DEFINITION_SCHEMA = z.object({
|
||||
organisationId: z.string(),
|
||||
team: z.object({
|
||||
name: z.string(),
|
||||
url: z.string(),
|
||||
ownerUserId: z.number(),
|
||||
teamGlobalSettings: z
|
||||
.object({
|
||||
documentVisibility: z.nativeEnum(DocumentVisibility),
|
||||
documentLanguage: z.string(),
|
||||
includeSenderDetails: z.boolean(),
|
||||
includeSigningCertificate: z.boolean(),
|
||||
brandingEnabled: z.boolean(),
|
||||
brandingLogo: z.string(),
|
||||
brandingUrl: z.string(),
|
||||
brandingCompanyDetails: z.string(),
|
||||
brandingHidePoweredBy: z.boolean(),
|
||||
teamId: z.number(),
|
||||
typedSignatureEnabled: z.boolean(),
|
||||
uploadSignatureEnabled: z.boolean(),
|
||||
drawSignatureEnabled: z.boolean(),
|
||||
allowEmbeddedAuthoring: z.boolean(),
|
||||
})
|
||||
.nullish(),
|
||||
}),
|
||||
members: z.array(
|
||||
z.object({
|
||||
|
||||
@ -1,32 +0,0 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import type { JobDefinition } from '../../client/_internal/job';
|
||||
|
||||
const SEND_TEAM_MEMBER_JOINED_EMAIL_JOB_DEFINITION_ID = 'send.team-member-joined.email';
|
||||
|
||||
const SEND_TEAM_MEMBER_JOINED_EMAIL_JOB_DEFINITION_SCHEMA = z.object({
|
||||
teamId: z.number(),
|
||||
memberId: z.number(),
|
||||
});
|
||||
|
||||
export type TSendTeamMemberJoinedEmailJobDefinition = z.infer<
|
||||
typeof SEND_TEAM_MEMBER_JOINED_EMAIL_JOB_DEFINITION_SCHEMA
|
||||
>;
|
||||
|
||||
export const SEND_TEAM_MEMBER_JOINED_EMAIL_JOB_DEFINITION = {
|
||||
id: SEND_TEAM_MEMBER_JOINED_EMAIL_JOB_DEFINITION_ID,
|
||||
name: 'Send Team Member Joined Email',
|
||||
version: '1.0.0',
|
||||
trigger: {
|
||||
name: SEND_TEAM_MEMBER_JOINED_EMAIL_JOB_DEFINITION_ID,
|
||||
schema: SEND_TEAM_MEMBER_JOINED_EMAIL_JOB_DEFINITION_SCHEMA,
|
||||
},
|
||||
handler: async ({ payload, io }) => {
|
||||
const handler = await import('./send-team-member-joined-email.handler');
|
||||
|
||||
await handler.run({ payload, io });
|
||||
},
|
||||
} as const satisfies JobDefinition<
|
||||
typeof SEND_TEAM_MEMBER_JOINED_EMAIL_JOB_DEFINITION_ID,
|
||||
TSendTeamMemberJoinedEmailJobDefinition
|
||||
>;
|
||||
@ -1,93 +0,0 @@
|
||||
import { createElement } from 'react';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { TeamMemberRole } from '@prisma/client';
|
||||
|
||||
import { mailer } from '@documenso/email/mailer';
|
||||
import TeamJoinEmailTemplate from '@documenso/email/templates/team-join';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { getI18nInstance } from '../../../client-only/providers/i18n-server';
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../../constants/app';
|
||||
import { FROM_ADDRESS, FROM_NAME } from '../../../constants/email';
|
||||
import { renderEmailWithI18N } from '../../../utils/render-email-with-i18n';
|
||||
import { teamGlobalSettingsToBranding } from '../../../utils/team-global-settings-to-branding';
|
||||
import type { JobRunIO } from '../../client/_internal/job';
|
||||
import type { TSendTeamMemberLeftEmailJobDefinition } from './send-team-member-left-email';
|
||||
|
||||
export const run = async ({
|
||||
payload,
|
||||
io,
|
||||
}: {
|
||||
payload: TSendTeamMemberLeftEmailJobDefinition;
|
||||
io: JobRunIO;
|
||||
}) => {
|
||||
const team = await prisma.team.findFirstOrThrow({
|
||||
where: {
|
||||
id: payload.teamId,
|
||||
},
|
||||
include: {
|
||||
members: {
|
||||
where: {
|
||||
role: {
|
||||
in: [TeamMemberRole.ADMIN, TeamMemberRole.MANAGER],
|
||||
},
|
||||
},
|
||||
include: {
|
||||
user: true,
|
||||
},
|
||||
},
|
||||
teamGlobalSettings: true,
|
||||
},
|
||||
});
|
||||
|
||||
const oldMember = await prisma.user.findFirstOrThrow({
|
||||
where: {
|
||||
id: payload.memberUserId,
|
||||
},
|
||||
});
|
||||
|
||||
for (const member of team.members) {
|
||||
await io.runTask(`send-team-member-left-email--${oldMember.id}_${member.id}`, async () => {
|
||||
const emailContent = createElement(TeamJoinEmailTemplate, {
|
||||
assetBaseUrl: NEXT_PUBLIC_WEBAPP_URL(),
|
||||
baseUrl: NEXT_PUBLIC_WEBAPP_URL(),
|
||||
memberName: oldMember.name || '',
|
||||
memberEmail: oldMember.email,
|
||||
teamName: team.name,
|
||||
teamUrl: team.url,
|
||||
});
|
||||
|
||||
const branding = team.teamGlobalSettings
|
||||
? teamGlobalSettingsToBranding(team.teamGlobalSettings)
|
||||
: undefined;
|
||||
|
||||
const lang = team.teamGlobalSettings?.documentLanguage;
|
||||
|
||||
const [html, text] = await Promise.all([
|
||||
renderEmailWithI18N(emailContent, {
|
||||
lang,
|
||||
branding,
|
||||
}),
|
||||
renderEmailWithI18N(emailContent, {
|
||||
lang,
|
||||
branding,
|
||||
plainText: true,
|
||||
}),
|
||||
]);
|
||||
|
||||
const i18n = await getI18nInstance(lang);
|
||||
|
||||
await mailer.sendMail({
|
||||
to: member.user.email,
|
||||
from: {
|
||||
name: FROM_NAME,
|
||||
address: FROM_ADDRESS,
|
||||
},
|
||||
subject: i18n._(msg`A team member has left ${team.name}`),
|
||||
html,
|
||||
text,
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -1,32 +0,0 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import type { JobDefinition } from '../../client/_internal/job';
|
||||
|
||||
const SEND_TEAM_MEMBER_LEFT_EMAIL_JOB_DEFINITION_ID = 'send.team-member-left.email';
|
||||
|
||||
const SEND_TEAM_MEMBER_LEFT_EMAIL_JOB_DEFINITION_SCHEMA = z.object({
|
||||
teamId: z.number(),
|
||||
memberUserId: z.number(),
|
||||
});
|
||||
|
||||
export type TSendTeamMemberLeftEmailJobDefinition = z.infer<
|
||||
typeof SEND_TEAM_MEMBER_LEFT_EMAIL_JOB_DEFINITION_SCHEMA
|
||||
>;
|
||||
|
||||
export const SEND_TEAM_MEMBER_LEFT_EMAIL_JOB_DEFINITION = {
|
||||
id: SEND_TEAM_MEMBER_LEFT_EMAIL_JOB_DEFINITION_ID,
|
||||
name: 'Send Team Member Left Email',
|
||||
version: '1.0.0',
|
||||
trigger: {
|
||||
name: SEND_TEAM_MEMBER_LEFT_EMAIL_JOB_DEFINITION_ID,
|
||||
schema: SEND_TEAM_MEMBER_LEFT_EMAIL_JOB_DEFINITION_SCHEMA,
|
||||
},
|
||||
handler: async ({ payload, io }) => {
|
||||
const handler = await import('./send-team-member-left-email.handler');
|
||||
|
||||
await handler.run({ payload, io });
|
||||
},
|
||||
} as const satisfies JobDefinition<
|
||||
typeof SEND_TEAM_MEMBER_LEFT_EMAIL_JOB_DEFINITION_ID,
|
||||
TSendTeamMemberLeftEmailJobDefinition
|
||||
>;
|
||||
@ -0,0 +1,35 @@
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../../errors/app-error';
|
||||
import type { JobRunIO } from '../../client/_internal/job';
|
||||
import type { TBackportSubscriptionClaimJobDefinition } from './backport-subscription-claims';
|
||||
|
||||
export const run = async ({
|
||||
payload,
|
||||
io,
|
||||
}: {
|
||||
payload: TBackportSubscriptionClaimJobDefinition;
|
||||
io: JobRunIO;
|
||||
}) => {
|
||||
const { subscriptionClaimId, flags } = payload;
|
||||
|
||||
const subscriptionClaim = await prisma.subscriptionClaim.findFirst({
|
||||
where: {
|
||||
id: subscriptionClaimId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!subscriptionClaim) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, { message: 'Subscription claim not found' });
|
||||
}
|
||||
|
||||
await io.runTask('backport-claims', async () => {
|
||||
const newFlagsJson = JSON.stringify(flags);
|
||||
|
||||
await prisma.$executeRaw`
|
||||
UPDATE "OrganisationClaim"
|
||||
SET "flags" = "flags" || ${newFlagsJson}::jsonb
|
||||
WHERE "originalSubscriptionClaimId" = ${subscriptionClaimId}
|
||||
`;
|
||||
});
|
||||
};
|
||||
@ -0,0 +1,44 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { type JobDefinition } from '../../client/_internal/job';
|
||||
|
||||
const BACKPORT_SUBSCRIPTION_CLAIM_JOB_DEFINITION_ID = 'internal.backport-subscription-claims';
|
||||
|
||||
const BACKPORT_SUBSCRIPTION_CLAIM_JOB_DEFINITION_SCHEMA = z.object({
|
||||
subscriptionClaimId: z.string(),
|
||||
// I would prefer to fetch the subscription within the runner, but
|
||||
// it seems the local job runs it asynchronously, so we can't get
|
||||
// the updated values in the job.
|
||||
flags: z.object({
|
||||
unlimitedDocuments: z.literal(true).optional(),
|
||||
allowCustomBranding: z.literal(true).optional(),
|
||||
hidePoweredBy: z.literal(true).optional(),
|
||||
embedAuthoring: z.literal(true).optional(),
|
||||
embedAuthoringWhiteLabel: z.literal(true).optional(),
|
||||
embedSigning: z.literal(true).optional(),
|
||||
embedSigningWhiteLabel: z.literal(true).optional(),
|
||||
cfr21: z.literal(true).optional(),
|
||||
}),
|
||||
});
|
||||
|
||||
export type TBackportSubscriptionClaimJobDefinition = z.infer<
|
||||
typeof BACKPORT_SUBSCRIPTION_CLAIM_JOB_DEFINITION_SCHEMA
|
||||
>;
|
||||
|
||||
export const BACKPORT_SUBSCRIPTION_CLAIM_JOB_DEFINITION = {
|
||||
id: BACKPORT_SUBSCRIPTION_CLAIM_JOB_DEFINITION_ID,
|
||||
name: 'Backport Subscription Claims',
|
||||
version: '1.0.0',
|
||||
trigger: {
|
||||
name: BACKPORT_SUBSCRIPTION_CLAIM_JOB_DEFINITION_ID,
|
||||
schema: BACKPORT_SUBSCRIPTION_CLAIM_JOB_DEFINITION_SCHEMA,
|
||||
},
|
||||
handler: async ({ payload, io }) => {
|
||||
const handler = await import('./backport-subscription-claims.handler');
|
||||
|
||||
await handler.run({ payload, io });
|
||||
},
|
||||
} as const satisfies JobDefinition<
|
||||
typeof BACKPORT_SUBSCRIPTION_CLAIM_JOB_DEFINITION_ID,
|
||||
TBackportSubscriptionClaimJobDefinition
|
||||
>;
|
||||
@ -1,7 +1,6 @@
|
||||
import { createElement } from 'react';
|
||||
|
||||
import { msg } from '@lingui/macro';
|
||||
import type { TeamGlobalSettings } from '@prisma/client';
|
||||
import { parse } from 'csv-parse/sync';
|
||||
import { z } from 'zod';
|
||||
|
||||
@ -16,8 +15,8 @@ import { getI18nInstance } from '../../../client-only/providers/i18n-server';
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../../constants/app';
|
||||
import { FROM_ADDRESS, FROM_NAME } from '../../../constants/email';
|
||||
import { AppError } from '../../../errors/app-error';
|
||||
import { getEmailContext } from '../../../server-only/email/get-email-context';
|
||||
import { renderEmailWithI18N } from '../../../utils/render-email-with-i18n';
|
||||
import { teamGlobalSettingsToBranding } from '../../../utils/team-global-settings-to-branding';
|
||||
import type { JobRunIO } from '../../client/_internal/job';
|
||||
import type { TBulkSendTemplateJobDefinition } from './bulk-send-template';
|
||||
|
||||
@ -163,29 +162,24 @@ export const run = async ({
|
||||
assetBaseUrl: NEXT_PUBLIC_WEBAPP_URL(),
|
||||
});
|
||||
|
||||
let teamGlobalSettings: TeamGlobalSettings | undefined | null;
|
||||
const { branding, settings } = await getEmailContext({
|
||||
source: {
|
||||
type: 'team',
|
||||
teamId,
|
||||
},
|
||||
});
|
||||
|
||||
if (template.teamId) {
|
||||
teamGlobalSettings = await prisma.teamGlobalSettings.findUnique({
|
||||
where: {
|
||||
teamId: template.teamId,
|
||||
},
|
||||
});
|
||||
}
|
||||
const lang = template.templateMeta?.language ?? settings.documentLanguage;
|
||||
|
||||
const branding = teamGlobalSettings
|
||||
? teamGlobalSettingsToBranding(teamGlobalSettings)
|
||||
: undefined;
|
||||
|
||||
const i18n = await getI18nInstance(teamGlobalSettings?.documentLanguage);
|
||||
const i18n = await getI18nInstance(lang);
|
||||
|
||||
const [html, text] = await Promise.all([
|
||||
renderEmailWithI18N(completionTemplate, {
|
||||
lang: teamGlobalSettings?.documentLanguage,
|
||||
lang,
|
||||
branding,
|
||||
}),
|
||||
renderEmailWithI18N(completionTemplate, {
|
||||
lang: teamGlobalSettings?.documentLanguage,
|
||||
lang,
|
||||
branding,
|
||||
plainText: true,
|
||||
}),
|
||||
|
||||
@ -7,7 +7,7 @@ const BULK_SEND_TEMPLATE_JOB_DEFINITION_ID = 'internal.bulk-send-template';
|
||||
|
||||
const BULK_SEND_TEMPLATE_JOB_DEFINITION_SCHEMA = z.object({
|
||||
userId: z.number(),
|
||||
teamId: z.number().optional(),
|
||||
teamId: z.number(),
|
||||
templateId: z.number(),
|
||||
csvContent: z.string(),
|
||||
sendImmediately: z.boolean(),
|
||||
|
||||
@ -16,6 +16,7 @@ import { flattenForm } from '../../../server-only/pdf/flatten-form';
|
||||
import { insertFieldInPDF } from '../../../server-only/pdf/insert-field-in-pdf';
|
||||
import { legacy_insertFieldInPDF } from '../../../server-only/pdf/legacy-insert-field-in-pdf';
|
||||
import { normalizeSignatureAppearances } from '../../../server-only/pdf/normalize-signature-appearances';
|
||||
import { getTeamSettings } from '../../../server-only/team/get-team-settings';
|
||||
import { triggerWebhook } from '../../../server-only/webhooks/trigger/trigger-webhook';
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../../types/document-audit-logs';
|
||||
import {
|
||||
@ -47,18 +48,14 @@ export const run = async ({
|
||||
include: {
|
||||
documentMeta: true,
|
||||
recipients: true,
|
||||
team: {
|
||||
select: {
|
||||
teamGlobalSettings: {
|
||||
select: {
|
||||
includeSigningCertificate: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const settings = await getTeamSettings({
|
||||
userId: document.userId,
|
||||
teamId: document.teamId,
|
||||
});
|
||||
|
||||
const isComplete =
|
||||
document.recipients.some((recipient) => recipient.signingStatus === SigningStatus.REJECTED) ||
|
||||
document.recipients.every((recipient) => recipient.signingStatus === SigningStatus.SIGNED);
|
||||
@ -144,20 +141,19 @@ export const run = async ({
|
||||
|
||||
const pdfData = await getFileServerSide(documentData);
|
||||
|
||||
const certificateData =
|
||||
(document.team?.teamGlobalSettings?.includeSigningCertificate ?? true)
|
||||
? await getCertificatePdf({
|
||||
documentId,
|
||||
language: document.documentMeta?.language,
|
||||
}).catch(() => null)
|
||||
: null;
|
||||
const certificateData = settings.includeSigningCertificate
|
||||
? await getCertificatePdf({
|
||||
documentId,
|
||||
language: document.documentMeta?.language,
|
||||
}).catch(() => null)
|
||||
: null;
|
||||
|
||||
const newDataId = await io.runTask('decorate-and-sign-pdf', async () => {
|
||||
const pdfDoc = await PDFDocument.load(pdfData);
|
||||
|
||||
// Normalize and flatten layers that could cause issues with the signature
|
||||
normalizeSignatureAppearances(pdfDoc);
|
||||
flattenForm(pdfDoc);
|
||||
await flattenForm(pdfDoc);
|
||||
flattenAnnotations(pdfDoc);
|
||||
|
||||
// Add rejection stamp if the document is rejected
|
||||
@ -188,7 +184,7 @@ export const run = async ({
|
||||
|
||||
// Re-flatten the form to handle our checkbox and radio fields that
|
||||
// create native arcoFields
|
||||
flattenForm(pdfDoc);
|
||||
await flattenForm(pdfDoc);
|
||||
|
||||
const pdfBytes = await pdfDoc.save();
|
||||
const pdfBuffer = await signPdf({ pdf: Buffer.from(pdfBytes) });
|
||||
|
||||
@ -15,7 +15,6 @@
|
||||
"clean": "rimraf node_modules"
|
||||
},
|
||||
"dependencies": {
|
||||
"@auth/kysely-adapter": "^0.6.0",
|
||||
"@aws-sdk/client-s3": "^3.410.0",
|
||||
"@aws-sdk/cloudfront-signer": "^3.410.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.410.0",
|
||||
@ -41,12 +40,13 @@
|
||||
"kysely": "0.26.3",
|
||||
"luxon": "^3.4.0",
|
||||
"micro": "^10.0.1",
|
||||
"nanoid": "^4.0.2",
|
||||
"nanoid": "^5.1.5",
|
||||
"oslo": "^0.17.0",
|
||||
"pdf-lib": "^1.17.1",
|
||||
"pg": "^8.11.3",
|
||||
"playwright": "1.43.0",
|
||||
"posthog-js": "^1.224.0",
|
||||
"playwright": "1.52.0",
|
||||
"posthog-js": "^1.245.0",
|
||||
"posthog-node": "^4.17.0",
|
||||
"react": "^18",
|
||||
"remeda": "^2.17.3",
|
||||
"sharp": "0.32.6",
|
||||
@ -55,7 +55,7 @@
|
||||
"zod": "3.24.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/browser-chromium": "1.43.0",
|
||||
"@playwright/browser-chromium": "1.52.0",
|
||||
"@types/luxon": "^3.3.1",
|
||||
"@types/pg": "^8.11.4"
|
||||
}
|
||||
|
||||
20
packages/lib/server-only/2fa/verify-password.ts
Normal file
20
packages/lib/server-only/2fa/verify-password.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { compare } from '@node-rs/bcrypt';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
type VerifyPasswordOptions = {
|
||||
userId: number;
|
||||
password: string;
|
||||
};
|
||||
|
||||
export const verifyPassword = async ({ userId, password }: VerifyPasswordOptions) => {
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
});
|
||||
|
||||
if (!user || !user.password) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return await compare(password, user.password);
|
||||
};
|
||||
@ -1,13 +0,0 @@
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
export const findSubscriptions = async () => {
|
||||
return prisma.subscription.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
status: true,
|
||||
createdAt: true,
|
||||
periodEnd: true,
|
||||
userId: true,
|
||||
},
|
||||
});
|
||||
};
|
||||
@ -1,7 +1,16 @@
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { DocumentStatus, SubscriptionStatus } from '@documenso/prisma/client';
|
||||
import { DocumentStatus, SubscriptionStatus } from '@prisma/client';
|
||||
|
||||
type GetSigningVolumeOptions = {
|
||||
import { kyselyPrisma, sql } from '@documenso/prisma';
|
||||
|
||||
export type SigningVolume = {
|
||||
id: number;
|
||||
name: string;
|
||||
signingVolume: number;
|
||||
createdAt: Date;
|
||||
planId: string;
|
||||
};
|
||||
|
||||
export type GetSigningVolumeOptions = {
|
||||
search?: string;
|
||||
page?: number;
|
||||
perPage?: number;
|
||||
@ -9,187 +18,68 @@ type GetSigningVolumeOptions = {
|
||||
sortOrder?: 'asc' | 'desc';
|
||||
};
|
||||
|
||||
export const getSigningVolume = async ({
|
||||
export async function getSigningVolume({
|
||||
search = '',
|
||||
page = 1,
|
||||
perPage = 10,
|
||||
sortBy = 'signingVolume',
|
||||
sortOrder = 'desc',
|
||||
}: GetSigningVolumeOptions) => {
|
||||
const validPage = Math.max(1, page);
|
||||
const validPerPage = Math.max(1, perPage);
|
||||
const skip = (validPage - 1) * validPerPage;
|
||||
}: GetSigningVolumeOptions) {
|
||||
const offset = Math.max(page - 1, 0) * perPage;
|
||||
|
||||
const activeSubscriptions = await prisma.subscription.findMany({
|
||||
where: {
|
||||
status: SubscriptionStatus.ACTIVE,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
planId: true,
|
||||
userId: true,
|
||||
teamId: true,
|
||||
createdAt: true,
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
createdAt: true,
|
||||
},
|
||||
},
|
||||
team: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
teamEmail: {
|
||||
select: {
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
createdAt: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
let findQuery = kyselyPrisma.$kysely
|
||||
.selectFrom('Subscription as s')
|
||||
.innerJoin('Organisation as o', 's.organisationId', 'o.id')
|
||||
.leftJoin('Team as t', 'o.id', 't.organisationId')
|
||||
.leftJoin('Document as d', (join) =>
|
||||
join
|
||||
.onRef('t.id', '=', 'd.teamId')
|
||||
.on('d.status', '=', sql.lit(DocumentStatus.COMPLETED))
|
||||
.on('d.deletedAt', 'is', null),
|
||||
)
|
||||
.where(sql`s.status = ${SubscriptionStatus.ACTIVE}::"SubscriptionStatus"`)
|
||||
.where((eb) =>
|
||||
eb.or([eb('o.name', 'ilike', `%${search}%`), eb('t.name', 'ilike', `%${search}%`)]),
|
||||
)
|
||||
.select([
|
||||
's.id as id',
|
||||
's.createdAt as createdAt',
|
||||
's.planId as planId',
|
||||
sql<string>`COALESCE(o.name, 'Unknown')`.as('name'),
|
||||
sql<number>`COUNT(DISTINCT d.id)`.as('signingVolume'),
|
||||
])
|
||||
.groupBy(['s.id', 'o.name']);
|
||||
|
||||
const userSubscriptionsMap = new Map();
|
||||
const teamSubscriptionsMap = new Map();
|
||||
switch (sortBy) {
|
||||
case 'name':
|
||||
findQuery = findQuery.orderBy('name', sortOrder);
|
||||
break;
|
||||
case 'createdAt':
|
||||
findQuery = findQuery.orderBy('createdAt', sortOrder);
|
||||
break;
|
||||
case 'signingVolume':
|
||||
findQuery = findQuery.orderBy('signingVolume', sortOrder);
|
||||
break;
|
||||
default:
|
||||
findQuery = findQuery.orderBy('signingVolume', 'desc');
|
||||
}
|
||||
|
||||
activeSubscriptions.forEach((subscription) => {
|
||||
const isTeam = !!subscription.teamId;
|
||||
findQuery = findQuery.limit(perPage).offset(offset);
|
||||
|
||||
if (isTeam && subscription.teamId) {
|
||||
if (!teamSubscriptionsMap.has(subscription.teamId)) {
|
||||
teamSubscriptionsMap.set(subscription.teamId, {
|
||||
id: subscription.id,
|
||||
planId: subscription.planId,
|
||||
teamId: subscription.teamId,
|
||||
name: subscription.team?.name || '',
|
||||
email: subscription.team?.teamEmail?.email || `Team ${subscription.team?.id}`,
|
||||
createdAt: subscription.team?.createdAt,
|
||||
isTeam: true,
|
||||
subscriptionIds: [subscription.id],
|
||||
});
|
||||
} else {
|
||||
const existingTeam = teamSubscriptionsMap.get(subscription.teamId);
|
||||
existingTeam.subscriptionIds.push(subscription.id);
|
||||
}
|
||||
} else if (subscription.userId) {
|
||||
if (!userSubscriptionsMap.has(subscription.userId)) {
|
||||
userSubscriptionsMap.set(subscription.userId, {
|
||||
id: subscription.id,
|
||||
planId: subscription.planId,
|
||||
userId: subscription.userId,
|
||||
name: subscription.user?.name || '',
|
||||
email: subscription.user?.email || '',
|
||||
createdAt: subscription.user?.createdAt,
|
||||
isTeam: false,
|
||||
subscriptionIds: [subscription.id],
|
||||
});
|
||||
} else {
|
||||
const existingUser = userSubscriptionsMap.get(subscription.userId);
|
||||
existingUser.subscriptionIds.push(subscription.id);
|
||||
}
|
||||
}
|
||||
});
|
||||
const countQuery = kyselyPrisma.$kysely
|
||||
.selectFrom('Subscription as s')
|
||||
.innerJoin('Organisation as o', 's.organisationId', 'o.id')
|
||||
.leftJoin('Team as t', 'o.id', 't.organisationId')
|
||||
.where(sql`s.status = ${SubscriptionStatus.ACTIVE}::"SubscriptionStatus"`)
|
||||
.where((eb) =>
|
||||
eb.or([eb('o.name', 'ilike', `%${search}%`), eb('t.name', 'ilike', `%${search}%`)]),
|
||||
)
|
||||
.select(({ fn }) => [fn.countAll().as('count')]);
|
||||
|
||||
const subscriptions = [
|
||||
...Array.from(userSubscriptionsMap.values()),
|
||||
...Array.from(teamSubscriptionsMap.values()),
|
||||
];
|
||||
|
||||
const filteredSubscriptions = search
|
||||
? subscriptions.filter((sub) => {
|
||||
const searchLower = search.toLowerCase();
|
||||
return (
|
||||
sub.name?.toLowerCase().includes(searchLower) ||
|
||||
sub.email?.toLowerCase().includes(searchLower)
|
||||
);
|
||||
})
|
||||
: subscriptions;
|
||||
|
||||
const signingVolume = await Promise.all(
|
||||
filteredSubscriptions.map(async (subscription) => {
|
||||
let signingVolume = 0;
|
||||
|
||||
if (subscription.userId && !subscription.isTeam) {
|
||||
const personalCount = await prisma.document.count({
|
||||
where: {
|
||||
userId: subscription.userId,
|
||||
status: DocumentStatus.COMPLETED,
|
||||
teamId: null,
|
||||
},
|
||||
});
|
||||
|
||||
signingVolume += personalCount;
|
||||
|
||||
const userTeams = await prisma.teamMember.findMany({
|
||||
where: {
|
||||
userId: subscription.userId,
|
||||
},
|
||||
select: {
|
||||
teamId: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (userTeams.length > 0) {
|
||||
const teamIds = userTeams.map((team) => team.teamId);
|
||||
const teamCount = await prisma.document.count({
|
||||
where: {
|
||||
teamId: {
|
||||
in: teamIds,
|
||||
},
|
||||
status: DocumentStatus.COMPLETED,
|
||||
},
|
||||
});
|
||||
|
||||
signingVolume += teamCount;
|
||||
}
|
||||
}
|
||||
|
||||
if (subscription.teamId) {
|
||||
const teamCount = await prisma.document.count({
|
||||
where: {
|
||||
teamId: subscription.teamId,
|
||||
status: DocumentStatus.COMPLETED,
|
||||
},
|
||||
});
|
||||
|
||||
signingVolume += teamCount;
|
||||
}
|
||||
|
||||
return {
|
||||
...subscription,
|
||||
signingVolume,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
const sortedResults = [...signingVolume].sort((a, b) => {
|
||||
if (sortBy === 'name') {
|
||||
return sortOrder === 'asc'
|
||||
? (a.name || '').localeCompare(b.name || '')
|
||||
: (b.name || '').localeCompare(a.name || '');
|
||||
}
|
||||
|
||||
if (sortBy === 'createdAt') {
|
||||
const dateA = a.createdAt ? new Date(a.createdAt).getTime() : 0;
|
||||
const dateB = b.createdAt ? new Date(b.createdAt).getTime() : 0;
|
||||
return sortOrder === 'asc' ? dateA - dateB : dateB - dateA;
|
||||
}
|
||||
|
||||
return sortOrder === 'asc'
|
||||
? a.signingVolume - b.signingVolume
|
||||
: b.signingVolume - a.signingVolume;
|
||||
});
|
||||
|
||||
const paginatedResults = sortedResults.slice(skip, skip + validPerPage);
|
||||
|
||||
const totalPages = Math.ceil(sortedResults.length / validPerPage);
|
||||
const [results, [{ count }]] = await Promise.all([findQuery.execute(), countQuery.execute()]);
|
||||
|
||||
return {
|
||||
leaderboard: paginatedResults,
|
||||
totalPages,
|
||||
leaderboard: results,
|
||||
totalPages: Math.ceil(Number(count) / perPage),
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@ -1,50 +1,17 @@
|
||||
import { DocumentStatus, SubscriptionStatus } from '@prisma/client';
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { kyselyPrisma, prisma, sql } from '@documenso/prisma';
|
||||
import { SubscriptionStatus, UserSecurityAuditLogType } from '@documenso/prisma/client';
|
||||
|
||||
export const getUsersCount = async () => {
|
||||
return await prisma.user.count();
|
||||
};
|
||||
|
||||
export const getUsersWithSubscriptionsCount = async () => {
|
||||
return await prisma.user.count({
|
||||
export const getOrganisationsWithSubscriptionsCount = async () => {
|
||||
return await prisma.organisation.count({
|
||||
where: {
|
||||
subscriptions: {
|
||||
some: {
|
||||
status: SubscriptionStatus.ACTIVE,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const getUserWithAtLeastOneDocumentPerMonth = async () => {
|
||||
return await prisma.user.count({
|
||||
where: {
|
||||
documents: {
|
||||
some: {
|
||||
createdAt: {
|
||||
gte: DateTime.now().minus({ months: 1 }).toJSDate(),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const getUserWithAtLeastOneDocumentSignedPerMonth = async () => {
|
||||
return await prisma.user.count({
|
||||
where: {
|
||||
documents: {
|
||||
some: {
|
||||
status: {
|
||||
equals: DocumentStatus.COMPLETED,
|
||||
},
|
||||
completedAt: {
|
||||
gte: DateTime.now().minus({ months: 1 }).toJSDate(),
|
||||
},
|
||||
},
|
||||
subscription: {
|
||||
status: SubscriptionStatus.ACTIVE,
|
||||
},
|
||||
},
|
||||
});
|
||||
@ -69,6 +36,8 @@ export const getUserWithSignedDocumentMonthlyGrowth = async () => {
|
||||
COUNT(DISTINCT "Document"."userId") as "count",
|
||||
COUNT(DISTINCT CASE WHEN "Document"."status" = 'COMPLETED' THEN "Document"."userId" END) as "signed_count"
|
||||
FROM "Document"
|
||||
INNER JOIN "Team" ON "Document"."teamId" = "Team"."id"
|
||||
INNER JOIN "Organisation" ON "Team"."organisationId" = "Organisation"."id"
|
||||
GROUP BY "month"
|
||||
ORDER BY "month" DESC
|
||||
LIMIT 12
|
||||
@ -80,3 +49,37 @@ export const getUserWithSignedDocumentMonthlyGrowth = async () => {
|
||||
signed_count: Number(row.signed_count),
|
||||
}));
|
||||
};
|
||||
|
||||
export type GetMonthlyActiveUsersResult = Array<{
|
||||
month: string;
|
||||
count: number;
|
||||
cume_count: number;
|
||||
}>;
|
||||
|
||||
export const getMonthlyActiveUsers = async () => {
|
||||
const qb = kyselyPrisma.$kysely
|
||||
.selectFrom('UserSecurityAuditLog')
|
||||
.select(({ fn }) => [
|
||||
fn<Date>('DATE_TRUNC', [sql.lit('MONTH'), 'UserSecurityAuditLog.createdAt']).as('month'),
|
||||
fn.count('userId').distinct().as('count'),
|
||||
fn
|
||||
.sum(fn.count('userId').distinct())
|
||||
.over((ob) =>
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/consistent-type-assertions
|
||||
ob.orderBy(fn('DATE_TRUNC', [sql.lit('MONTH'), 'UserSecurityAuditLog.createdAt']) as any),
|
||||
)
|
||||
.as('cume_count'),
|
||||
])
|
||||
.where(sql`type = ${UserSecurityAuditLogType.SIGN_IN}::"UserSecurityAuditLogType"`)
|
||||
.groupBy(({ fn }) => fn('DATE_TRUNC', [sql.lit('MONTH'), 'UserSecurityAuditLog.createdAt']))
|
||||
.orderBy('month', 'desc')
|
||||
.limit(12);
|
||||
|
||||
const result = await qb.execute();
|
||||
|
||||
return result.map((row) => ({
|
||||
month: DateTime.fromJSDate(row.month).toFormat('yyyy-MM'),
|
||||
count: Number(row.count),
|
||||
cume_count: Number(row.cume_count),
|
||||
}));
|
||||
};
|
||||
|
||||
@ -11,10 +11,11 @@ import { prisma } from '@documenso/prisma';
|
||||
import type { SupportedLanguageCodes } from '../../constants/i18n';
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import type { TDocumentEmailSettings } from '../../types/document-email';
|
||||
import { getDocumentWhereInput } from '../document/get-document-by-id';
|
||||
|
||||
export type CreateDocumentMetaOptions = {
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
teamId: number;
|
||||
documentId: number;
|
||||
subject?: string;
|
||||
message?: string;
|
||||
@ -53,25 +54,14 @@ export const upsertDocumentMeta = async ({
|
||||
language,
|
||||
requestMetadata,
|
||||
}: CreateDocumentMetaOptions) => {
|
||||
const { documentWhereInput } = await getDocumentWhereInput({
|
||||
documentId,
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
const document = await prisma.document.findFirst({
|
||||
where: {
|
||||
id: documentId,
|
||||
...(teamId
|
||||
? {
|
||||
team: {
|
||||
id: teamId,
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
: {
|
||||
userId,
|
||||
teamId: null,
|
||||
}),
|
||||
},
|
||||
where: documentWhereInput,
|
||||
include: {
|
||||
documentMeta: true,
|
||||
},
|
||||
|
||||
@ -23,6 +23,7 @@ import {
|
||||
ZWebhookDocumentSchema,
|
||||
mapDocumentToWebhookDocumentPayload,
|
||||
} from '../../types/webhook-payload';
|
||||
import { extractDocumentAuthMethods } from '../../utils/document-auth';
|
||||
import { getIsRecipientsTurnToSign } from '../recipient/get-is-recipient-turn';
|
||||
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
|
||||
import { sendPendingEmail } from './send-pending-email';
|
||||
@ -140,6 +141,11 @@ export const completeDocumentWithToken = async ({
|
||||
},
|
||||
});
|
||||
|
||||
const authOptions = extractDocumentAuthMethods({
|
||||
documentAuth: document.authOptions,
|
||||
recipientAuth: recipient.authOptions,
|
||||
});
|
||||
|
||||
await tx.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_COMPLETED,
|
||||
@ -154,6 +160,7 @@ export const completeDocumentWithToken = async ({
|
||||
recipientName: recipient.name,
|
||||
recipientId: recipient.id,
|
||||
recipientRole: recipient.role,
|
||||
actionAuth: authOptions.derivedRecipientActionAuth,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
@ -6,9 +6,7 @@ import {
|
||||
SigningStatus,
|
||||
WebhookTriggerEvents,
|
||||
} from '@prisma/client';
|
||||
import { TeamMemberRole } from '@prisma/client';
|
||||
|
||||
import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { normalizePdf as makeNormalizedPdf } from '@documenso/lib/server-only/pdf/normalize-pdf';
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
||||
@ -28,19 +26,22 @@ import { getFileServerSide } from '../../universal/upload/get-file.server';
|
||||
import { putPdfFileServerSide } from '../../universal/upload/put-file.server';
|
||||
import { createDocumentAuthOptions, createRecipientAuthOptions } from '../../utils/document-auth';
|
||||
import { determineDocumentVisibility } from '../../utils/document-visibility';
|
||||
import { buildTeamWhereQuery } from '../../utils/teams';
|
||||
import { getMemberRoles } from '../team/get-member-roles';
|
||||
import { getTeamSettings } from '../team/get-team-settings';
|
||||
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
|
||||
|
||||
export type CreateDocumentOptions = {
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
teamId: number;
|
||||
documentDataId: string;
|
||||
normalizePdf?: boolean;
|
||||
data: {
|
||||
title: string;
|
||||
externalId?: string;
|
||||
visibility?: DocumentVisibility;
|
||||
globalAccessAuth?: TDocumentAccessAuthTypes;
|
||||
globalActionAuth?: TDocumentActionAuthTypes;
|
||||
globalAccessAuth?: TDocumentAccessAuthTypes[];
|
||||
globalActionAuth?: TDocumentActionAuthTypes[];
|
||||
formValues?: TDocumentFormValues;
|
||||
recipients: TCreateDocumentV2Request['recipients'];
|
||||
};
|
||||
@ -59,36 +60,28 @@ export const createDocumentV2 = async ({
|
||||
}: CreateDocumentOptions) => {
|
||||
const { title, formValues } = data;
|
||||
|
||||
const team = teamId
|
||||
? await prisma.team.findFirst({
|
||||
where: {
|
||||
id: teamId,
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
const team = await prisma.team.findFirst({
|
||||
where: buildTeamWhereQuery({ teamId, userId }),
|
||||
include: {
|
||||
organisation: {
|
||||
select: {
|
||||
organisationClaim: true,
|
||||
},
|
||||
include: {
|
||||
teamGlobalSettings: true,
|
||||
members: {
|
||||
where: {
|
||||
userId: userId,
|
||||
},
|
||||
select: {
|
||||
role: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
: null;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (teamId !== undefined && !team) {
|
||||
if (!team) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Team not found',
|
||||
});
|
||||
}
|
||||
|
||||
const settings = await getTeamSettings({
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
if (normalizePdf) {
|
||||
const documentData = await prisma.documentData.findFirst({
|
||||
where: {
|
||||
@ -113,30 +106,33 @@ export const createDocumentV2 = async ({
|
||||
}
|
||||
|
||||
const authOptions = createDocumentAuthOptions({
|
||||
globalAccessAuth: data?.globalAccessAuth || null,
|
||||
globalActionAuth: data?.globalActionAuth || null,
|
||||
globalAccessAuth: data?.globalAccessAuth || [],
|
||||
globalActionAuth: data?.globalActionAuth || [],
|
||||
});
|
||||
|
||||
const recipientsHaveActionAuth = data.recipients?.some((recipient) => recipient.actionAuth);
|
||||
const recipientsHaveActionAuth = data.recipients?.some(
|
||||
(recipient) => recipient.actionAuth && recipient.actionAuth.length > 0,
|
||||
);
|
||||
|
||||
// Check if user has permission to set the global action auth.
|
||||
if (authOptions.globalActionAuth || recipientsHaveActionAuth) {
|
||||
const isDocumentEnterprise = await isUserEnterprise({
|
||||
userId,
|
||||
teamId,
|
||||
if (
|
||||
(authOptions.globalActionAuth.length > 0 || recipientsHaveActionAuth) &&
|
||||
!team.organisation.organisationClaim.flags.cfr21
|
||||
) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'You do not have permission to set the action auth',
|
||||
});
|
||||
|
||||
if (!isDocumentEnterprise) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'You do not have permission to set the action auth',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const visibility = determineDocumentVisibility(
|
||||
team?.teamGlobalSettings?.documentVisibility,
|
||||
team?.members[0].role ?? TeamMemberRole.MEMBER,
|
||||
);
|
||||
const { teamRole } = await getMemberRoles({
|
||||
teamId,
|
||||
reference: {
|
||||
type: 'User',
|
||||
id: userId,
|
||||
},
|
||||
});
|
||||
|
||||
const visibility = determineDocumentVisibility(settings.documentVisibility, teamRole);
|
||||
|
||||
return await prisma.$transaction(async (tx) => {
|
||||
const document = await tx.document.create({
|
||||
@ -156,13 +152,10 @@ export const createDocumentV2 = async ({
|
||||
...meta,
|
||||
signingOrder: meta?.signingOrder || undefined,
|
||||
emailSettings: meta?.emailSettings || undefined,
|
||||
language: meta?.language || team?.teamGlobalSettings?.documentLanguage,
|
||||
typedSignatureEnabled:
|
||||
meta?.typedSignatureEnabled ?? team?.teamGlobalSettings?.typedSignatureEnabled,
|
||||
uploadSignatureEnabled:
|
||||
meta?.uploadSignatureEnabled ?? team?.teamGlobalSettings?.uploadSignatureEnabled,
|
||||
drawSignatureEnabled:
|
||||
meta?.drawSignatureEnabled ?? team?.teamGlobalSettings?.drawSignatureEnabled,
|
||||
language: meta?.language || settings.documentLanguage,
|
||||
typedSignatureEnabled: meta?.typedSignatureEnabled ?? settings.typedSignatureEnabled,
|
||||
uploadSignatureEnabled: meta?.uploadSignatureEnabled ?? settings.uploadSignatureEnabled,
|
||||
drawSignatureEnabled: meta?.drawSignatureEnabled ?? settings.drawSignatureEnabled,
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -171,8 +164,8 @@ export const createDocumentV2 = async ({
|
||||
await Promise.all(
|
||||
(data.recipients || []).map(async (recipient) => {
|
||||
const recipientAuthOptions = createRecipientAuthOptions({
|
||||
accessAuth: recipient.accessAuth || null,
|
||||
actionAuth: recipient.actionAuth || null,
|
||||
accessAuth: recipient.accessAuth ?? [],
|
||||
actionAuth: recipient.actionAuth ?? [],
|
||||
});
|
||||
|
||||
await tx.recipient.create({
|
||||
|
||||
@ -1,14 +1,13 @@
|
||||
import { DocumentSource, WebhookTriggerEvents } from '@prisma/client';
|
||||
import type { DocumentVisibility, Team, TeamGlobalSettings } from '@prisma/client';
|
||||
import { TeamMemberRole } from '@prisma/client';
|
||||
import type { DocumentVisibility } from '@prisma/client';
|
||||
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { normalizePdf as makeNormalizedPdf } from '@documenso/lib/server-only/pdf/normalize-pdf';
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
||||
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import {
|
||||
ZWebhookDocumentSchema,
|
||||
mapDocumentToWebhookDocumentPayload,
|
||||
@ -17,13 +16,15 @@ import { prefixedId } from '../../universal/id';
|
||||
import { getFileServerSide } from '../../universal/upload/get-file.server';
|
||||
import { putPdfFileServerSide } from '../../universal/upload/put-file.server';
|
||||
import { determineDocumentVisibility } from '../../utils/document-visibility';
|
||||
import { getTeamById } from '../team/get-team';
|
||||
import { getTeamSettings } from '../team/get-team-settings';
|
||||
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
|
||||
|
||||
export type CreateDocumentOptions = {
|
||||
title: string;
|
||||
externalId?: string | null;
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
teamId: number;
|
||||
documentDataId: string;
|
||||
formValues?: Record<string, string | number | boolean>;
|
||||
normalizePdf?: boolean;
|
||||
@ -44,53 +45,13 @@ export const createDocument = async ({
|
||||
timezone,
|
||||
folderId,
|
||||
}: CreateDocumentOptions) => {
|
||||
const user = await prisma.user.findFirstOrThrow({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
include: {
|
||||
teamMembers: {
|
||||
select: {
|
||||
teamId: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
const team = await getTeamById({ userId, teamId });
|
||||
|
||||
const settings = await getTeamSettings({
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
if (
|
||||
teamId !== undefined &&
|
||||
!user.teamMembers.some((teamMember) => teamMember.teamId === teamId)
|
||||
) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Team not found',
|
||||
});
|
||||
}
|
||||
|
||||
let team: (Team & { teamGlobalSettings: TeamGlobalSettings | null }) | null = null;
|
||||
let userTeamRole: TeamMemberRole | undefined;
|
||||
|
||||
if (teamId) {
|
||||
const teamWithUserRole = await prisma.team.findFirstOrThrow({
|
||||
where: {
|
||||
id: teamId,
|
||||
},
|
||||
include: {
|
||||
teamGlobalSettings: true,
|
||||
members: {
|
||||
where: {
|
||||
userId: userId,
|
||||
},
|
||||
select: {
|
||||
role: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
team = teamWithUserRole;
|
||||
userTeamRole = teamWithUserRole.members[0]?.role;
|
||||
}
|
||||
|
||||
let folderVisibility: DocumentVisibility | undefined;
|
||||
|
||||
if (folderId) {
|
||||
@ -149,19 +110,16 @@ export const createDocument = async ({
|
||||
folderId,
|
||||
visibility:
|
||||
folderVisibility ??
|
||||
determineDocumentVisibility(
|
||||
team?.teamGlobalSettings?.documentVisibility,
|
||||
userTeamRole ?? TeamMemberRole.MEMBER,
|
||||
),
|
||||
determineDocumentVisibility(settings.documentVisibility, team.currentTeamRole),
|
||||
formValues,
|
||||
source: DocumentSource.DOCUMENT,
|
||||
documentMeta: {
|
||||
create: {
|
||||
language: team?.teamGlobalSettings?.documentLanguage,
|
||||
language: settings.documentLanguage,
|
||||
timezone: timezone,
|
||||
typedSignatureEnabled: team?.teamGlobalSettings?.typedSignatureEnabled ?? true,
|
||||
uploadSignatureEnabled: team?.teamGlobalSettings?.uploadSignatureEnabled ?? true,
|
||||
drawSignatureEnabled: team?.teamGlobalSettings?.drawSignatureEnabled ?? true,
|
||||
typedSignatureEnabled: settings.typedSignatureEnabled,
|
||||
uploadSignatureEnabled: settings.uploadSignatureEnabled,
|
||||
drawSignatureEnabled: settings.drawSignatureEnabled,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@ -1,14 +1,7 @@
|
||||
import { createElement } from 'react';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import type {
|
||||
Document,
|
||||
DocumentMeta,
|
||||
Recipient,
|
||||
Team,
|
||||
TeamGlobalSettings,
|
||||
User,
|
||||
} from '@prisma/client';
|
||||
import type { Document, DocumentMeta, Recipient, User } from '@prisma/client';
|
||||
import { DocumentStatus, SendStatus, WebhookTriggerEvents } from '@prisma/client';
|
||||
|
||||
import { mailer } from '@documenso/email/mailer';
|
||||
@ -29,13 +22,14 @@ import type { ApiRequestMetadata } from '../../universal/extract-request-metadat
|
||||
import { isDocumentCompleted } from '../../utils/document';
|
||||
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
|
||||
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
||||
import { teamGlobalSettingsToBranding } from '../../utils/team-global-settings-to-branding';
|
||||
import { getEmailContext } from '../email/get-email-context';
|
||||
import { getMemberRoles } from '../team/get-member-roles';
|
||||
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
|
||||
|
||||
export type DeleteDocumentOptions = {
|
||||
id: number;
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
teamId: number;
|
||||
requestMetadata: ApiRequestMetadata;
|
||||
};
|
||||
|
||||
@ -64,23 +58,26 @@ export const deleteDocument = async ({
|
||||
include: {
|
||||
recipients: true,
|
||||
documentMeta: true,
|
||||
team: {
|
||||
include: {
|
||||
members: true,
|
||||
teamGlobalSettings: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!document || (teamId !== undefined && teamId !== document.teamId)) {
|
||||
if (!document) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Document not found',
|
||||
});
|
||||
}
|
||||
|
||||
const isUserTeamMember = await getMemberRoles({
|
||||
teamId: document.teamId,
|
||||
reference: {
|
||||
type: 'User',
|
||||
id: userId,
|
||||
},
|
||||
})
|
||||
.then(() => true)
|
||||
.catch(() => false);
|
||||
|
||||
const isUserOwner = document.userId === userId;
|
||||
const isUserTeamMember = document.team?.members.some((member) => member.userId === userId);
|
||||
const userRecipient = document.recipients.find((recipient) => recipient.email === user.email);
|
||||
|
||||
if (!isUserOwner && !isUserTeamMember && !userRecipient) {
|
||||
@ -94,7 +91,6 @@ export const deleteDocument = async ({
|
||||
await handleDocumentOwnerDelete({
|
||||
document,
|
||||
user,
|
||||
team: document.team,
|
||||
requestMetadata,
|
||||
});
|
||||
}
|
||||
@ -142,11 +138,6 @@ type HandleDocumentOwnerDeleteOptions = {
|
||||
recipients: Recipient[];
|
||||
documentMeta: DocumentMeta | null;
|
||||
};
|
||||
team?:
|
||||
| (Team & {
|
||||
teamGlobalSettings?: TeamGlobalSettings | null;
|
||||
})
|
||||
| null;
|
||||
user: User;
|
||||
requestMetadata: ApiRequestMetadata;
|
||||
};
|
||||
@ -154,13 +145,19 @@ type HandleDocumentOwnerDeleteOptions = {
|
||||
const handleDocumentOwnerDelete = async ({
|
||||
document,
|
||||
user,
|
||||
team,
|
||||
requestMetadata,
|
||||
}: HandleDocumentOwnerDeleteOptions) => {
|
||||
if (document.deletedAt) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { branding, settings } = await getEmailContext({
|
||||
source: {
|
||||
type: 'team',
|
||||
teamId: document.teamId,
|
||||
},
|
||||
});
|
||||
|
||||
// Soft delete completed documents.
|
||||
if (isDocumentCompleted(document.status)) {
|
||||
return await prisma.$transaction(async (tx) => {
|
||||
@ -235,20 +232,18 @@ const handleDocumentOwnerDelete = async ({
|
||||
assetBaseUrl,
|
||||
});
|
||||
|
||||
const branding = team?.teamGlobalSettings
|
||||
? teamGlobalSettingsToBranding(team.teamGlobalSettings)
|
||||
: undefined;
|
||||
const lang = document.documentMeta?.language ?? settings.documentLanguage;
|
||||
|
||||
const [html, text] = await Promise.all([
|
||||
renderEmailWithI18N(template, { lang: document.documentMeta?.language, branding }),
|
||||
renderEmailWithI18N(template, { lang, branding }),
|
||||
renderEmailWithI18N(template, {
|
||||
lang: document.documentMeta?.language,
|
||||
lang,
|
||||
branding,
|
||||
plainText: true,
|
||||
}),
|
||||
]);
|
||||
|
||||
const i18n = await getI18nInstance(document.documentMeta?.language);
|
||||
const i18n = await getI18nInstance(lang);
|
||||
|
||||
await mailer.sendMail({
|
||||
to: {
|
||||
|
||||
@ -1,15 +1,22 @@
|
||||
import { DocumentSource, type Prisma } from '@prisma/client';
|
||||
import type { Prisma, Recipient } from '@prisma/client';
|
||||
import { DocumentSource, WebhookTriggerEvents } from '@prisma/client';
|
||||
import { omit } from 'remeda';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { prefixedId } from '../../universal/id';
|
||||
import {
|
||||
ZWebhookDocumentSchema,
|
||||
mapDocumentToWebhookDocumentPayload,
|
||||
} from '../../types/webhook-payload';
|
||||
import { nanoid, prefixedId } from '../../universal/id';
|
||||
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
|
||||
import { getDocumentWhereInput } from './get-document-by-id';
|
||||
|
||||
export interface DuplicateDocumentOptions {
|
||||
documentId: number;
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
teamId: number;
|
||||
}
|
||||
|
||||
export const duplicateDocument = async ({
|
||||
@ -17,7 +24,7 @@ export const duplicateDocument = async ({
|
||||
userId,
|
||||
teamId,
|
||||
}: DuplicateDocumentOptions) => {
|
||||
const documentWhereInput = await getDocumentWhereInput({
|
||||
const { documentWhereInput } = await getDocumentWhereInput({
|
||||
documentId,
|
||||
userId,
|
||||
teamId,
|
||||
@ -35,14 +42,16 @@ export const duplicateDocument = async ({
|
||||
type: true,
|
||||
},
|
||||
},
|
||||
documentMeta: {
|
||||
authOptions: true,
|
||||
visibility: true,
|
||||
documentMeta: true,
|
||||
recipients: {
|
||||
select: {
|
||||
message: true,
|
||||
subject: true,
|
||||
dateFormat: true,
|
||||
password: true,
|
||||
timezone: true,
|
||||
redirectUrl: true,
|
||||
email: true,
|
||||
name: true,
|
||||
role: true,
|
||||
signingOrder: true,
|
||||
fields: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -54,39 +63,88 @@ export const duplicateDocument = async ({
|
||||
});
|
||||
}
|
||||
|
||||
const createDocumentArguments: Prisma.DocumentCreateArgs = {
|
||||
const documentData = await prisma.documentData.create({
|
||||
data: {
|
||||
title: document.title,
|
||||
qrToken: prefixedId('qr'),
|
||||
user: {
|
||||
connect: {
|
||||
id: document.userId,
|
||||
},
|
||||
},
|
||||
documentData: {
|
||||
create: {
|
||||
...document.documentData,
|
||||
data: document.documentData.initialData,
|
||||
},
|
||||
},
|
||||
documentMeta: {
|
||||
create: {
|
||||
...document.documentMeta,
|
||||
},
|
||||
},
|
||||
source: DocumentSource.DOCUMENT,
|
||||
type: document.documentData.type,
|
||||
data: document.documentData.initialData,
|
||||
initialData: document.documentData.initialData,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
if (teamId !== undefined) {
|
||||
createDocumentArguments.data.team = {
|
||||
connect: {
|
||||
id: teamId,
|
||||
let documentMeta: Prisma.DocumentCreateArgs['data']['documentMeta'] | undefined = undefined;
|
||||
|
||||
if (document.documentMeta) {
|
||||
documentMeta = {
|
||||
create: {
|
||||
...omit(document.documentMeta, ['id', 'documentId']),
|
||||
emailSettings: document.documentMeta.emailSettings || undefined,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const createdDocument = await prisma.document.create(createDocumentArguments);
|
||||
const createdDocument = await prisma.document.create({
|
||||
data: {
|
||||
userId: document.userId,
|
||||
teamId: teamId,
|
||||
title: document.title,
|
||||
documentDataId: documentData.id,
|
||||
authOptions: document.authOptions || undefined,
|
||||
visibility: document.visibility,
|
||||
qrToken: prefixedId('qr'),
|
||||
documentMeta,
|
||||
source: DocumentSource.DOCUMENT,
|
||||
},
|
||||
include: {
|
||||
recipients: true,
|
||||
documentMeta: true,
|
||||
},
|
||||
});
|
||||
|
||||
const recipientsToCreate = document.recipients.map((recipient) => ({
|
||||
documentId: createdDocument.id,
|
||||
email: recipient.email,
|
||||
name: recipient.name,
|
||||
role: recipient.role,
|
||||
signingOrder: recipient.signingOrder,
|
||||
token: nanoid(),
|
||||
fields: {
|
||||
createMany: {
|
||||
data: recipient.fields.map((field) => ({
|
||||
documentId: createdDocument.id,
|
||||
type: field.type,
|
||||
page: field.page,
|
||||
positionX: field.positionX,
|
||||
positionY: field.positionY,
|
||||
width: field.width,
|
||||
height: field.height,
|
||||
customText: '',
|
||||
inserted: false,
|
||||
fieldMeta: field.fieldMeta as PrismaJson.FieldMeta,
|
||||
})),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
const recipients: Recipient[] = [];
|
||||
|
||||
for (const recipientData of recipientsToCreate) {
|
||||
const newRecipient = await prisma.recipient.create({
|
||||
data: recipientData,
|
||||
});
|
||||
|
||||
recipients.push(newRecipient);
|
||||
}
|
||||
|
||||
await triggerWebhook({
|
||||
event: WebhookTriggerEvents.DOCUMENT_CREATED,
|
||||
data: ZWebhookDocumentSchema.parse({
|
||||
...mapDocumentToWebhookDocumentPayload(createdDocument),
|
||||
recipients,
|
||||
documentMeta: createdDocument.documentMeta,
|
||||
}),
|
||||
userId: userId,
|
||||
teamId: teamId,
|
||||
});
|
||||
|
||||
return {
|
||||
documentId: createdDocument.id,
|
||||
|
||||
@ -6,10 +6,11 @@ import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs';
|
||||
import type { FindResultResponse } from '../../types/search-params';
|
||||
import { parseDocumentAuditLogData } from '../../utils/document-audit-logs';
|
||||
import { getDocumentWhereInput } from './get-document-by-id';
|
||||
|
||||
export interface FindDocumentAuditLogsOptions {
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
teamId: number;
|
||||
documentId: number;
|
||||
page?: number;
|
||||
perPage?: number;
|
||||
@ -34,25 +35,14 @@ export const findDocumentAuditLogs = async ({
|
||||
const orderByColumn = orderBy?.column ?? 'createdAt';
|
||||
const orderByDirection = orderBy?.direction ?? 'desc';
|
||||
|
||||
const { documentWhereInput } = await getDocumentWhereInput({
|
||||
documentId,
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
const document = await prisma.document.findFirst({
|
||||
where: {
|
||||
id: documentId,
|
||||
...(teamId
|
||||
? {
|
||||
team: {
|
||||
id: teamId,
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
: {
|
||||
userId,
|
||||
teamId: null,
|
||||
}),
|
||||
},
|
||||
where: documentWhereInput,
|
||||
});
|
||||
|
||||
if (!document) {
|
||||
|
||||
@ -9,6 +9,7 @@ import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-documen
|
||||
import { DocumentVisibility } from '../../types/document-visibility';
|
||||
import { type FindResultResponse } from '../../types/search-params';
|
||||
import { maskRecipientTokensForDocument } from '../../utils/mask-recipient-tokens-for-document';
|
||||
import { getTeamById } from '../team/get-team';
|
||||
|
||||
export type PeriodSelectorValue = '' | '7d' | '14d' | '30d';
|
||||
|
||||
@ -53,32 +54,15 @@ export const findDocuments = async ({
|
||||
let team = null;
|
||||
|
||||
if (teamId !== undefined) {
|
||||
team = await prisma.team.findFirstOrThrow({
|
||||
where: {
|
||||
id: teamId,
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
include: {
|
||||
teamEmail: true,
|
||||
members: {
|
||||
where: {
|
||||
userId,
|
||||
},
|
||||
select: {
|
||||
role: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
team = await getTeamById({
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
}
|
||||
|
||||
const orderByColumn = orderBy?.column ?? 'createdAt';
|
||||
const orderByDirection = orderBy?.direction ?? 'desc';
|
||||
const teamMemberRole = team?.members[0].role ?? null;
|
||||
const teamMemberRole = team?.currentTeamRole ?? null;
|
||||
|
||||
const searchFilter: Prisma.DocumentWhereInput = {
|
||||
OR: [
|
||||
@ -291,7 +275,6 @@ const findDocumentsFilter = (
|
||||
OR: [
|
||||
{
|
||||
userId: user.id,
|
||||
teamId: null,
|
||||
folderId: folderId,
|
||||
},
|
||||
{
|
||||
@ -330,14 +313,12 @@ const findDocumentsFilter = (
|
||||
}))
|
||||
.with(ExtendedDocumentStatus.DRAFT, () => ({
|
||||
userId: user.id,
|
||||
teamId: null,
|
||||
status: ExtendedDocumentStatus.DRAFT,
|
||||
}))
|
||||
.with(ExtendedDocumentStatus.PENDING, () => ({
|
||||
OR: [
|
||||
{
|
||||
userId: user.id,
|
||||
teamId: null,
|
||||
status: ExtendedDocumentStatus.PENDING,
|
||||
folderId: folderId,
|
||||
},
|
||||
@ -360,7 +341,6 @@ const findDocumentsFilter = (
|
||||
OR: [
|
||||
{
|
||||
userId: user.id,
|
||||
teamId: null,
|
||||
status: ExtendedDocumentStatus.COMPLETED,
|
||||
folderId: folderId,
|
||||
},
|
||||
@ -379,7 +359,6 @@ const findDocumentsFilter = (
|
||||
OR: [
|
||||
{
|
||||
userId: user.id,
|
||||
teamId: null,
|
||||
status: ExtendedDocumentStatus.REJECTED,
|
||||
folderId: folderId,
|
||||
},
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import type { Prisma } from '@prisma/client';
|
||||
import { TeamMemberRole } from '@prisma/client';
|
||||
import { DocumentStatus, TeamMemberRole } from '@prisma/client';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
@ -11,7 +11,7 @@ import { getTeamById } from '../team/get-team';
|
||||
export type GetDocumentByIdOptions = {
|
||||
documentId: number;
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
teamId: number;
|
||||
folderId?: string;
|
||||
};
|
||||
|
||||
@ -21,7 +21,7 @@ export const getDocumentById = async ({
|
||||
teamId,
|
||||
folderId,
|
||||
}: GetDocumentByIdOptions) => {
|
||||
const documentWhereInput = await getDocumentWhereInput({
|
||||
const { documentWhereInput } = await getDocumentWhereInput({
|
||||
documentId,
|
||||
userId,
|
||||
teamId,
|
||||
@ -68,18 +68,7 @@ export const getDocumentById = async ({
|
||||
export type GetDocumentWhereInputOptions = {
|
||||
documentId: number;
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
|
||||
/**
|
||||
* Whether to return a filter that allows access to both the user and team documents.
|
||||
* This only applies if `teamId` is passed in.
|
||||
*
|
||||
* If true, and `teamId` is passed in, the filter will allow both team and user documents.
|
||||
* If false, and `teamId` is passed in, the filter will only allow team documents.
|
||||
*
|
||||
* Defaults to false.
|
||||
*/
|
||||
overlapUserTeamScope?: boolean;
|
||||
teamId: number;
|
||||
};
|
||||
|
||||
/**
|
||||
@ -91,42 +80,55 @@ export const getDocumentWhereInput = async ({
|
||||
documentId,
|
||||
userId,
|
||||
teamId,
|
||||
overlapUserTeamScope = false,
|
||||
}: GetDocumentWhereInputOptions) => {
|
||||
const documentWhereInput: Prisma.DocumentWhereUniqueInput = {
|
||||
id: documentId,
|
||||
OR: [
|
||||
{
|
||||
userId,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
if (teamId === undefined || !documentWhereInput.OR) {
|
||||
return documentWhereInput;
|
||||
}
|
||||
|
||||
const team = await getTeamById({ teamId, userId });
|
||||
|
||||
// Allow access to team and user documents.
|
||||
if (overlapUserTeamScope) {
|
||||
documentWhereInput.OR.push({
|
||||
teamId: team.id,
|
||||
});
|
||||
}
|
||||
const user = await prisma.user.findFirstOrThrow({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
});
|
||||
|
||||
// Allow access to only team documents.
|
||||
if (!overlapUserTeamScope) {
|
||||
documentWhereInput.OR = [
|
||||
{
|
||||
teamId: team.id,
|
||||
const teamVisibilityFilters = match(team.currentTeamRole)
|
||||
.with(TeamMemberRole.ADMIN, () => [
|
||||
DocumentVisibility.EVERYONE,
|
||||
DocumentVisibility.MANAGER_AND_ABOVE,
|
||||
DocumentVisibility.ADMIN,
|
||||
])
|
||||
.with(TeamMemberRole.MANAGER, () => [
|
||||
DocumentVisibility.EVERYONE,
|
||||
DocumentVisibility.MANAGER_AND_ABOVE,
|
||||
])
|
||||
.otherwise(() => [DocumentVisibility.EVERYONE]);
|
||||
|
||||
const documentOrInput: Prisma.DocumentWhereInput[] = [
|
||||
// Allow access if they own the document.
|
||||
{
|
||||
userId,
|
||||
},
|
||||
// Or, if they belong to the team that the document is associated with.
|
||||
{
|
||||
visibility: {
|
||||
in: teamVisibilityFilters,
|
||||
},
|
||||
];
|
||||
}
|
||||
teamId,
|
||||
},
|
||||
// Or, if they are a recipient of the document.
|
||||
{
|
||||
status: {
|
||||
not: DocumentStatus.DRAFT,
|
||||
},
|
||||
recipients: {
|
||||
some: {
|
||||
email: user.email,
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
// Allow access to documents sent to or from the team email.
|
||||
if (team.teamEmail) {
|
||||
documentWhereInput.OR.push(
|
||||
documentOrInput.push(
|
||||
{
|
||||
recipients: {
|
||||
some: {
|
||||
@ -142,42 +144,13 @@ export const getDocumentWhereInput = async ({
|
||||
);
|
||||
}
|
||||
|
||||
const user = await prisma.user.findFirstOrThrow({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
});
|
||||
|
||||
const visibilityFilters = [
|
||||
...match(team.currentTeamMember?.role)
|
||||
.with(TeamMemberRole.ADMIN, () => [
|
||||
{ visibility: DocumentVisibility.EVERYONE },
|
||||
{ visibility: DocumentVisibility.MANAGER_AND_ABOVE },
|
||||
{ visibility: DocumentVisibility.ADMIN },
|
||||
])
|
||||
.with(TeamMemberRole.MANAGER, () => [
|
||||
{ visibility: DocumentVisibility.EVERYONE },
|
||||
{ visibility: DocumentVisibility.MANAGER_AND_ABOVE },
|
||||
])
|
||||
.otherwise(() => [{ visibility: DocumentVisibility.EVERYONE }]),
|
||||
{
|
||||
OR: [
|
||||
{
|
||||
recipients: {
|
||||
some: {
|
||||
email: user.email,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
userId: user.id,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
const documentWhereInput: Prisma.DocumentWhereUniqueInput = {
|
||||
id: documentId,
|
||||
OR: documentOrInput,
|
||||
};
|
||||
|
||||
return {
|
||||
...documentWhereInput,
|
||||
OR: [...visibilityFilters],
|
||||
documentWhereInput,
|
||||
team,
|
||||
};
|
||||
};
|
||||
|
||||
@ -86,11 +86,6 @@ export const getDocumentAndSenderByToken = async ({
|
||||
select: {
|
||||
name: true,
|
||||
teamEmail: true,
|
||||
teamGlobalSettings: {
|
||||
select: {
|
||||
includeSenderDetails: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@ -17,6 +17,7 @@ export const getDocumentCertificateAuditLogs = async ({
|
||||
in: [
|
||||
DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_COMPLETED,
|
||||
DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_REJECTED,
|
||||
DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FIELD_INSERTED,
|
||||
DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_OPENED,
|
||||
DOCUMENT_AUDIT_LOG_TYPE.EMAIL_SENT,
|
||||
],
|
||||
@ -36,6 +37,9 @@ export const getDocumentCertificateAuditLogs = async ({
|
||||
[DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_OPENED]: auditLogs.filter(
|
||||
(log) => log.type === DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_OPENED,
|
||||
),
|
||||
[DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FIELD_INSERTED]: auditLogs.filter(
|
||||
(log) => log.type === DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FIELD_INSERTED,
|
||||
),
|
||||
[DOCUMENT_AUDIT_LOG_TYPE.EMAIL_SENT]: auditLogs.filter(
|
||||
(log) =>
|
||||
log.type === DOCUMENT_AUDIT_LOG_TYPE.EMAIL_SENT &&
|
||||
|
||||
@ -6,7 +6,7 @@ import { getDocumentWhereInput } from './get-document-by-id';
|
||||
export type GetDocumentWithDetailsByIdOptions = {
|
||||
documentId: number;
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
teamId: number;
|
||||
folderId?: string;
|
||||
};
|
||||
|
||||
@ -16,7 +16,7 @@ export const getDocumentWithDetailsById = async ({
|
||||
teamId,
|
||||
folderId,
|
||||
}: GetDocumentWithDetailsByIdOptions) => {
|
||||
const documentWhereInput = await getDocumentWhereInput({
|
||||
const { documentWhereInput } = await getDocumentWhereInput({
|
||||
documentId,
|
||||
userId,
|
||||
teamId,
|
||||
@ -31,9 +31,33 @@ export const getDocumentWithDetailsById = async ({
|
||||
documentData: true,
|
||||
documentMeta: true,
|
||||
recipients: true,
|
||||
fields: true,
|
||||
attachments: true,
|
||||
folder: true,
|
||||
fields: {
|
||||
include: {
|
||||
signature: true,
|
||||
recipient: {
|
||||
select: {
|
||||
name: true,
|
||||
email: true,
|
||||
signingStatus: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
team: {
|
||||
select: {
|
||||
id: true,
|
||||
url: true,
|
||||
},
|
||||
},
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@ -11,7 +11,7 @@ import { isExtendedDocumentStatus } from '@documenso/prisma/guards/is-extended-d
|
||||
import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status';
|
||||
|
||||
export type GetStatsInput = {
|
||||
user: User;
|
||||
user: Pick<User, 'id' | 'email'>;
|
||||
team?: Omit<GetTeamCountsOption, 'createdAt'>;
|
||||
period?: PeriodSelectorValue;
|
||||
search?: string;
|
||||
@ -89,7 +89,7 @@ export const getStats = async ({
|
||||
};
|
||||
|
||||
type GetCountsOption = {
|
||||
user: User;
|
||||
user: Pick<User, 'id' | 'email'>;
|
||||
createdAt: Prisma.DocumentWhereInput['createdAt'];
|
||||
search?: string;
|
||||
folderId?: string | null;
|
||||
@ -116,7 +116,6 @@ const getCounts = async ({ user, createdAt, search, folderId }: GetCountsOption)
|
||||
where: {
|
||||
userId: user.id,
|
||||
createdAt,
|
||||
teamId: null,
|
||||
deletedAt: null,
|
||||
AND: [searchFilter, rootPageFilter, folderId ? { folderId } : {}],
|
||||
},
|
||||
@ -334,7 +333,6 @@ const getTeamCounts = async (options: GetTeamCountsOption) => {
|
||||
documentDeletedAt: null,
|
||||
},
|
||||
},
|
||||
deletedAt: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@ -5,6 +5,7 @@ import { match } from 'ts-pattern';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { verifyTwoFactorAuthenticationToken } from '../2fa/verify-2fa-token';
|
||||
import { verifyPassword } from '../2fa/verify-password';
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import type { TDocumentAuth, TDocumentAuthMethods } from '../../types/document-auth';
|
||||
import { DocumentAuth } from '../../types/document-auth';
|
||||
@ -60,23 +61,26 @@ export const isRecipientAuthorized = async ({
|
||||
recipientAuth: recipient.authOptions,
|
||||
});
|
||||
|
||||
const authMethod: TDocumentAuth | null =
|
||||
const authMethods: TDocumentAuth[] =
|
||||
type === 'ACCESS' ? derivedRecipientAccessAuth : derivedRecipientActionAuth;
|
||||
|
||||
// Early true return when auth is not required.
|
||||
if (!authMethod || authMethod === DocumentAuth.EXPLICIT_NONE) {
|
||||
if (
|
||||
authMethods.length === 0 ||
|
||||
authMethods.some((method) => method === DocumentAuth.EXPLICIT_NONE)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Create auth options when none are passed for account.
|
||||
if (!authOptions && authMethod === DocumentAuth.ACCOUNT) {
|
||||
if (!authOptions && authMethods.some((method) => method === DocumentAuth.ACCOUNT)) {
|
||||
authOptions = {
|
||||
type: DocumentAuth.ACCOUNT,
|
||||
};
|
||||
}
|
||||
|
||||
// Authentication required does not match provided method.
|
||||
if (!authOptions || authOptions.type !== authMethod || !userId) {
|
||||
if (!authOptions || !authMethods.includes(authOptions.type) || !userId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -117,6 +121,15 @@ export const isRecipientAuthorized = async ({
|
||||
window: 10, // 5 minutes worth of tokens
|
||||
});
|
||||
})
|
||||
.with({ type: DocumentAuth.PASSWORD }, async ({ password }) => {
|
||||
return await verifyPassword({
|
||||
userId,
|
||||
password,
|
||||
});
|
||||
})
|
||||
.with({ type: DocumentAuth.EXPLICIT_NONE }, () => {
|
||||
return true;
|
||||
})
|
||||
.exhaustive();
|
||||
};
|
||||
|
||||
@ -160,7 +173,7 @@ const verifyPasskey = async ({
|
||||
}: VerifyPasskeyOptions): Promise<void> => {
|
||||
const passkey = await prisma.passkey.findFirst({
|
||||
where: {
|
||||
credentialId: Buffer.from(authenticationResponse.id, 'base64'),
|
||||
credentialId: new Uint8Array(Buffer.from(authenticationResponse.id, 'base64')),
|
||||
userId,
|
||||
},
|
||||
});
|
||||
|
||||
@ -1,73 +0,0 @@
|
||||
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs';
|
||||
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
|
||||
|
||||
export type MoveDocumentToTeamOptions = {
|
||||
documentId: number;
|
||||
teamId: number;
|
||||
userId: number;
|
||||
requestMetadata: ApiRequestMetadata;
|
||||
};
|
||||
|
||||
export const moveDocumentToTeam = async ({
|
||||
documentId,
|
||||
teamId,
|
||||
userId,
|
||||
requestMetadata,
|
||||
}: MoveDocumentToTeamOptions) => {
|
||||
return await prisma.$transaction(async (tx) => {
|
||||
const document = await tx.document.findFirst({
|
||||
where: {
|
||||
id: documentId,
|
||||
userId,
|
||||
teamId: null,
|
||||
},
|
||||
});
|
||||
|
||||
if (!document) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Document not found or already associated with a team.',
|
||||
});
|
||||
}
|
||||
|
||||
const team = await tx.team.findFirst({
|
||||
where: {
|
||||
id: teamId,
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!team) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'This team does not exist, or you are not a member of this team.',
|
||||
});
|
||||
}
|
||||
|
||||
const updatedDocument = await tx.document.update({
|
||||
where: { id: documentId },
|
||||
data: { teamId },
|
||||
});
|
||||
|
||||
await tx.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_MOVED_TO_TEAM,
|
||||
documentId: updatedDocument.id,
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
movedByUserId: userId,
|
||||
fromPersonalAccount: true,
|
||||
toTeamId: teamId,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
return updatedDocument;
|
||||
});
|
||||
};
|
||||
@ -1,8 +1,7 @@
|
||||
import { createElement } from 'react';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { DocumentStatus, RecipientRole, SigningStatus } from '@prisma/client';
|
||||
import type { Prisma } from '@prisma/client';
|
||||
import { DocumentStatus, OrganisationType, RecipientRole, SigningStatus } from '@prisma/client';
|
||||
|
||||
import { mailer } from '@documenso/email/mailer';
|
||||
import { DocumentInviteEmailTemplate } from '@documenso/email/templates/document-invite';
|
||||
@ -22,14 +21,14 @@ import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
||||
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
|
||||
import { isDocumentCompleted } from '../../utils/document';
|
||||
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
||||
import { teamGlobalSettingsToBranding } from '../../utils/team-global-settings-to-branding';
|
||||
import { getEmailContext } from '../email/get-email-context';
|
||||
import { getDocumentWhereInput } from './get-document-by-id';
|
||||
|
||||
export type ResendDocumentOptions = {
|
||||
documentId: number;
|
||||
userId: number;
|
||||
recipients: number[];
|
||||
teamId?: number;
|
||||
teamId: number;
|
||||
requestMetadata: ApiRequestMetadata;
|
||||
};
|
||||
|
||||
@ -46,7 +45,7 @@ export const resendDocument = async ({
|
||||
},
|
||||
});
|
||||
|
||||
const documentWhereInput: Prisma.DocumentWhereUniqueInput = await getDocumentWhereInput({
|
||||
const { documentWhereInput } = await getDocumentWhereInput({
|
||||
documentId,
|
||||
userId,
|
||||
teamId,
|
||||
@ -68,14 +67,12 @@ export const resendDocument = async ({
|
||||
select: {
|
||||
teamEmail: true,
|
||||
name: true,
|
||||
teamGlobalSettings: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const customEmail = document?.documentMeta;
|
||||
const isTeamDocument = document?.team !== null;
|
||||
|
||||
if (!document) {
|
||||
throw new Error('Document not found');
|
||||
@ -101,13 +98,21 @@ export const resendDocument = async ({
|
||||
return;
|
||||
}
|
||||
|
||||
const { branding, settings, organisationType } = await getEmailContext({
|
||||
source: {
|
||||
type: 'team',
|
||||
teamId: document.teamId,
|
||||
},
|
||||
});
|
||||
|
||||
await Promise.all(
|
||||
document.recipients.map(async (recipient) => {
|
||||
if (recipient.role === RecipientRole.CC) {
|
||||
return;
|
||||
}
|
||||
|
||||
const i18n = await getI18nInstance(document.documentMeta?.language);
|
||||
const lang = document.documentMeta?.language ?? settings.documentLanguage;
|
||||
const i18n = await getI18nInstance(lang);
|
||||
|
||||
const recipientEmailType = RECIPIENT_ROLE_TO_EMAIL_TYPE[recipient.role];
|
||||
|
||||
@ -128,7 +133,7 @@ export const resendDocument = async ({
|
||||
emailSubject = i18n._(msg`Reminder: Please ${recipientActionVerb} your document`);
|
||||
}
|
||||
|
||||
if (isTeamDocument && document.team) {
|
||||
if (organisationType === OrganisationType.ORGANISATION) {
|
||||
emailSubject = i18n._(
|
||||
msg`Reminder: ${document.team.name} invited you to ${recipientActionVerb} a document`,
|
||||
);
|
||||
@ -151,27 +156,26 @@ export const resendDocument = async ({
|
||||
const template = createElement(DocumentInviteEmailTemplate, {
|
||||
documentName: document.title,
|
||||
inviterName: user.name || undefined,
|
||||
inviterEmail: isTeamDocument ? document.team?.teamEmail?.email || user.email : user.email,
|
||||
inviterEmail:
|
||||
organisationType === OrganisationType.ORGANISATION
|
||||
? document.team?.teamEmail?.email || user.email
|
||||
: user.email,
|
||||
assetBaseUrl,
|
||||
signDocumentLink,
|
||||
customBody: renderCustomEmailTemplate(emailMessage, customEmailTemplate),
|
||||
role: recipient.role,
|
||||
selfSigner,
|
||||
isTeamInvite: isTeamDocument,
|
||||
organisationType,
|
||||
teamName: document.team?.name,
|
||||
});
|
||||
|
||||
const branding = document.team?.teamGlobalSettings
|
||||
? teamGlobalSettingsToBranding(document.team.teamGlobalSettings)
|
||||
: undefined;
|
||||
|
||||
const [html, text] = await Promise.all([
|
||||
renderEmailWithI18N(template, {
|
||||
lang: document.documentMeta?.language,
|
||||
lang,
|
||||
branding,
|
||||
}),
|
||||
renderEmailWithI18N(template, {
|
||||
lang: document.documentMeta?.language,
|
||||
lang,
|
||||
branding,
|
||||
plainText: true,
|
||||
}),
|
||||
|
||||
@ -24,6 +24,7 @@ import { flattenForm } from '../pdf/flatten-form';
|
||||
import { insertFieldInPDF } from '../pdf/insert-field-in-pdf';
|
||||
import { legacy_insertFieldInPDF } from '../pdf/legacy-insert-field-in-pdf';
|
||||
import { normalizeSignatureAppearances } from '../pdf/normalize-signature-appearances';
|
||||
import { getTeamSettings } from '../team/get-team-settings';
|
||||
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
|
||||
import { sendCompletedEmail } from './send-completed-email';
|
||||
|
||||
@ -48,15 +49,6 @@ export const sealDocument = async ({
|
||||
documentData: true,
|
||||
documentMeta: true,
|
||||
recipients: true,
|
||||
team: {
|
||||
select: {
|
||||
teamGlobalSettings: {
|
||||
select: {
|
||||
includeSigningCertificate: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@ -66,6 +58,11 @@ export const sealDocument = async ({
|
||||
throw new Error(`Document ${document.id} has no document data`);
|
||||
}
|
||||
|
||||
const settings = await getTeamSettings({
|
||||
userId: document.userId,
|
||||
teamId: document.teamId,
|
||||
});
|
||||
|
||||
const recipients = await prisma.recipient.findMany({
|
||||
where: {
|
||||
documentId: document.id,
|
||||
@ -116,19 +113,18 @@ export const sealDocument = async ({
|
||||
// !: Need to write the fields onto the document as a hard copy
|
||||
const pdfData = await getFileServerSide(documentData);
|
||||
|
||||
const certificateData =
|
||||
(document.team?.teamGlobalSettings?.includeSigningCertificate ?? true)
|
||||
? await getCertificatePdf({
|
||||
documentId,
|
||||
language: document.documentMeta?.language,
|
||||
}).catch(() => null)
|
||||
: null;
|
||||
const certificateData = settings.includeSigningCertificate
|
||||
? await getCertificatePdf({
|
||||
documentId,
|
||||
language: document.documentMeta?.language,
|
||||
}).catch(() => null)
|
||||
: null;
|
||||
|
||||
const doc = await PDFDocument.load(pdfData);
|
||||
|
||||
// Normalize and flatten layers that could cause issues with the signature
|
||||
normalizeSignatureAppearances(doc);
|
||||
flattenForm(doc);
|
||||
await flattenForm(doc);
|
||||
flattenAnnotations(doc);
|
||||
|
||||
// Add rejection stamp if the document is rejected
|
||||
@ -153,7 +149,7 @@ export const sealDocument = async ({
|
||||
}
|
||||
|
||||
// Re-flatten post-insertion to handle fields that create arcoFields
|
||||
flattenForm(doc);
|
||||
await flattenForm(doc);
|
||||
|
||||
const pdfBytes = await doc.save();
|
||||
|
||||
|
||||
@ -3,7 +3,11 @@ import type { Document, Recipient, User } from '@prisma/client';
|
||||
import { DocumentVisibility, TeamMemberRole } from '@prisma/client';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
|
||||
import {
|
||||
buildTeamWhereQuery,
|
||||
formatDocumentsPath,
|
||||
getHighestTeamRoleInGroup,
|
||||
} from '@documenso/lib/utils/teams';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
export type SearchDocumentsWithKeywordOptions = {
|
||||
@ -84,16 +88,7 @@ export const searchDocumentsWithKeyword = async ({
|
||||
contains: query,
|
||||
mode: 'insensitive',
|
||||
},
|
||||
teamId: {
|
||||
not: null,
|
||||
},
|
||||
team: {
|
||||
members: {
|
||||
some: {
|
||||
userId: userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
team: buildTeamWhereQuery({ teamId: undefined, userId }),
|
||||
deletedAt: null,
|
||||
},
|
||||
{
|
||||
@ -101,16 +96,7 @@ export const searchDocumentsWithKeyword = async ({
|
||||
contains: query,
|
||||
mode: 'insensitive',
|
||||
},
|
||||
teamId: {
|
||||
not: null,
|
||||
},
|
||||
team: {
|
||||
members: {
|
||||
some: {
|
||||
userId: userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
team: buildTeamWhereQuery({ teamId: undefined, userId }),
|
||||
deletedAt: null,
|
||||
},
|
||||
],
|
||||
@ -120,12 +106,17 @@ export const searchDocumentsWithKeyword = async ({
|
||||
team: {
|
||||
select: {
|
||||
url: true,
|
||||
members: {
|
||||
teamGroups: {
|
||||
where: {
|
||||
userId: userId,
|
||||
},
|
||||
select: {
|
||||
role: true,
|
||||
organisationGroup: {
|
||||
organisationGroupMembers: {
|
||||
some: {
|
||||
organisationMember: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -147,7 +138,9 @@ export const searchDocumentsWithKeyword = async ({
|
||||
return true;
|
||||
}
|
||||
|
||||
const teamMemberRole = document.team?.members[0]?.role;
|
||||
const teamMemberRole = getHighestTeamRoleInGroup(
|
||||
document.team.teamGroups.filter((tg) => tg.teamId === document.teamId),
|
||||
);
|
||||
|
||||
if (!teamMemberRole) {
|
||||
return false;
|
||||
|
||||
@ -17,8 +17,8 @@ import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
|
||||
import { env } from '../../utils/env';
|
||||
import { renderCustomEmailTemplate } from '../../utils/render-custom-email-template';
|
||||
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
||||
import { teamGlobalSettingsToBranding } from '../../utils/team-global-settings-to-branding';
|
||||
import { formatDocumentsPath } from '../../utils/teams';
|
||||
import { getEmailContext } from '../email/get-email-context';
|
||||
|
||||
export interface SendDocumentOptions {
|
||||
documentId: number;
|
||||
@ -39,7 +39,6 @@ export const sendCompletedEmail = async ({ documentId, requestMetadata }: SendDo
|
||||
select: {
|
||||
id: true,
|
||||
url: true,
|
||||
teamGlobalSettings: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -55,6 +54,13 @@ export const sendCompletedEmail = async ({ documentId, requestMetadata }: SendDo
|
||||
throw new Error('Document has no recipients');
|
||||
}
|
||||
|
||||
const { branding, settings } = await getEmailContext({
|
||||
source: {
|
||||
type: 'team',
|
||||
teamId: document.teamId,
|
||||
},
|
||||
});
|
||||
|
||||
const { user: owner } = document;
|
||||
|
||||
const completedDocument = await getFileServerSide(document.documentData);
|
||||
@ -71,8 +77,6 @@ export const sendCompletedEmail = async ({ documentId, requestMetadata }: SendDo
|
||||
}`;
|
||||
}
|
||||
|
||||
const i18n = await getI18nInstance(document.documentMeta?.language);
|
||||
|
||||
const emailSettings = extractDerivedDocumentEmailSettings(document.documentMeta);
|
||||
const isDocumentCompletedEmailEnabled = emailSettings.documentCompleted;
|
||||
const isOwnerDocumentCompletedEmailEnabled = emailSettings.ownerDocumentCompleted;
|
||||
@ -93,19 +97,19 @@ export const sendCompletedEmail = async ({ documentId, requestMetadata }: SendDo
|
||||
downloadLink: documentOwnerDownloadLink,
|
||||
});
|
||||
|
||||
const branding = document.team?.teamGlobalSettings
|
||||
? teamGlobalSettingsToBranding(document.team.teamGlobalSettings)
|
||||
: undefined;
|
||||
const lang = document.documentMeta?.language ?? settings.documentLanguage;
|
||||
|
||||
const [html, text] = await Promise.all([
|
||||
renderEmailWithI18N(template, { lang: document.documentMeta?.language, branding }),
|
||||
renderEmailWithI18N(template, { lang, branding }),
|
||||
renderEmailWithI18N(template, {
|
||||
lang: document.documentMeta?.language,
|
||||
lang,
|
||||
branding,
|
||||
plainText: true,
|
||||
}),
|
||||
]);
|
||||
|
||||
const i18n = await getI18nInstance(lang);
|
||||
|
||||
await mailer.sendMail({
|
||||
to: [
|
||||
{
|
||||
@ -170,19 +174,19 @@ export const sendCompletedEmail = async ({ documentId, requestMetadata }: SendDo
|
||||
: undefined,
|
||||
});
|
||||
|
||||
const branding = document.team?.teamGlobalSettings
|
||||
? teamGlobalSettingsToBranding(document.team.teamGlobalSettings)
|
||||
: undefined;
|
||||
const lang = document.documentMeta?.language ?? settings.documentLanguage;
|
||||
|
||||
const [html, text] = await Promise.all([
|
||||
renderEmailWithI18N(template, { lang: document.documentMeta?.language, branding }),
|
||||
renderEmailWithI18N(template, { lang, branding }),
|
||||
renderEmailWithI18N(template, {
|
||||
lang: document.documentMeta?.language,
|
||||
lang,
|
||||
branding,
|
||||
plainText: true,
|
||||
}),
|
||||
]);
|
||||
|
||||
const i18n = await getI18nInstance(lang);
|
||||
|
||||
await mailer.sendMail({
|
||||
to: [
|
||||
{
|
||||
|
||||
@ -12,7 +12,7 @@ import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
|
||||
import { env } from '../../utils/env';
|
||||
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
||||
import { teamGlobalSettingsToBranding } from '../../utils/team-global-settings-to-branding';
|
||||
import { getEmailContext } from '../email/get-email-context';
|
||||
|
||||
export interface SendDeleteEmailOptions {
|
||||
documentId: number;
|
||||
@ -27,11 +27,6 @@ export const sendDeleteEmail = async ({ documentId, reason }: SendDeleteEmailOpt
|
||||
include: {
|
||||
user: true,
|
||||
documentMeta: true,
|
||||
team: {
|
||||
include: {
|
||||
teamGlobalSettings: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@ -49,6 +44,13 @@ export const sendDeleteEmail = async ({ documentId, reason }: SendDeleteEmailOpt
|
||||
return;
|
||||
}
|
||||
|
||||
const { branding, settings } = await getEmailContext({
|
||||
source: {
|
||||
type: 'team',
|
||||
teamId: document.teamId,
|
||||
},
|
||||
});
|
||||
|
||||
const { email, name } = document.user;
|
||||
|
||||
const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000';
|
||||
@ -59,20 +61,18 @@ export const sendDeleteEmail = async ({ documentId, reason }: SendDeleteEmailOpt
|
||||
assetBaseUrl,
|
||||
});
|
||||
|
||||
const branding = document.team?.teamGlobalSettings
|
||||
? teamGlobalSettingsToBranding(document.team.teamGlobalSettings)
|
||||
: undefined;
|
||||
const lang = document.documentMeta?.language ?? settings.documentLanguage;
|
||||
|
||||
const [html, text] = await Promise.all([
|
||||
renderEmailWithI18N(template, { lang: document.documentMeta?.language, branding }),
|
||||
renderEmailWithI18N(template, { lang, branding }),
|
||||
renderEmailWithI18N(template, {
|
||||
lang: document.documentMeta?.language,
|
||||
lang,
|
||||
branding,
|
||||
plainText: true,
|
||||
}),
|
||||
]);
|
||||
|
||||
const i18n = await getI18nInstance();
|
||||
const i18n = await getI18nInstance(lang);
|
||||
|
||||
await mailer.sendMail({
|
||||
to: {
|
||||
|
||||
@ -23,11 +23,12 @@ import { putPdfFileServerSide } from '../../universal/upload/put-file.server';
|
||||
import { isDocumentCompleted } from '../../utils/document';
|
||||
import { insertFormValuesInPdf } from '../pdf/insert-form-values-in-pdf';
|
||||
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
|
||||
import { getDocumentWhereInput } from './get-document-by-id';
|
||||
|
||||
export type SendDocumentOptions = {
|
||||
documentId: number;
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
teamId: number;
|
||||
sendEmail?: boolean;
|
||||
requestMetadata: ApiRequestMetadata;
|
||||
};
|
||||
@ -39,25 +40,14 @@ export const sendDocument = async ({
|
||||
sendEmail,
|
||||
requestMetadata,
|
||||
}: SendDocumentOptions) => {
|
||||
const document = await prisma.document.findUnique({
|
||||
where: {
|
||||
id: documentId,
|
||||
...(teamId
|
||||
? {
|
||||
team: {
|
||||
id: teamId,
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
: {
|
||||
userId,
|
||||
teamId: null,
|
||||
}),
|
||||
},
|
||||
const { documentWhereInput } = await getDocumentWhereInput({
|
||||
documentId,
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
const document = await prisma.document.findFirst({
|
||||
where: documentWhereInput,
|
||||
include: {
|
||||
recipients: {
|
||||
orderBy: [{ signingOrder: { sort: 'asc', nulls: 'last' } }, { id: 'asc' }],
|
||||
|
||||
@ -11,7 +11,7 @@ import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
||||
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
|
||||
import { env } from '../../utils/env';
|
||||
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
||||
import { teamGlobalSettingsToBranding } from '../../utils/team-global-settings-to-branding';
|
||||
import { getEmailContext } from '../email/get-email-context';
|
||||
|
||||
export interface SendPendingEmailOptions {
|
||||
documentId: number;
|
||||
@ -35,11 +35,6 @@ export const sendPendingEmail = async ({ documentId, recipientId }: SendPendingE
|
||||
},
|
||||
},
|
||||
documentMeta: true,
|
||||
team: {
|
||||
include: {
|
||||
teamGlobalSettings: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@ -51,6 +46,13 @@ export const sendPendingEmail = async ({ documentId, recipientId }: SendPendingE
|
||||
throw new Error('Document has no recipients');
|
||||
}
|
||||
|
||||
const { branding, settings } = await getEmailContext({
|
||||
source: {
|
||||
type: 'team',
|
||||
teamId: document.teamId,
|
||||
},
|
||||
});
|
||||
|
||||
const isDocumentPendingEmailEnabled = extractDerivedDocumentEmailSettings(
|
||||
document.documentMeta,
|
||||
).documentPending;
|
||||
@ -70,20 +72,18 @@ export const sendPendingEmail = async ({ documentId, recipientId }: SendPendingE
|
||||
assetBaseUrl,
|
||||
});
|
||||
|
||||
const branding = document.team?.teamGlobalSettings
|
||||
? teamGlobalSettingsToBranding(document.team.teamGlobalSettings)
|
||||
: undefined;
|
||||
const lang = document.documentMeta?.language ?? settings.documentLanguage;
|
||||
|
||||
const [html, text] = await Promise.all([
|
||||
renderEmailWithI18N(template, { lang: document.documentMeta?.language, branding }),
|
||||
renderEmailWithI18N(template, { lang, branding }),
|
||||
renderEmailWithI18N(template, {
|
||||
lang: document.documentMeta?.language,
|
||||
lang,
|
||||
branding,
|
||||
plainText: true,
|
||||
}),
|
||||
]);
|
||||
|
||||
const i18n = await getI18nInstance(document.documentMeta?.language);
|
||||
const i18n = await getI18nInstance(lang);
|
||||
|
||||
await mailer.sendMail({
|
||||
to: {
|
||||
|
||||
@ -16,7 +16,7 @@ import { extractDerivedDocumentEmailSettings } from '../../types/document-email'
|
||||
import type { RequestMetadata } from '../../universal/extract-request-metadata';
|
||||
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
|
||||
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
||||
import { teamGlobalSettingsToBranding } from '../../utils/team-global-settings-to-branding';
|
||||
import { getEmailContext } from '../email/get-email-context';
|
||||
|
||||
export type SuperDeleteDocumentOptions = {
|
||||
id: number;
|
||||
@ -32,11 +32,6 @@ export const superDeleteDocument = async ({ id, requestMetadata }: SuperDeleteDo
|
||||
recipients: true,
|
||||
documentMeta: true,
|
||||
user: true,
|
||||
team: {
|
||||
include: {
|
||||
teamGlobalSettings: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@ -46,6 +41,13 @@ export const superDeleteDocument = async ({ id, requestMetadata }: SuperDeleteDo
|
||||
});
|
||||
}
|
||||
|
||||
const { branding, settings } = await getEmailContext({
|
||||
source: {
|
||||
type: 'team',
|
||||
teamId: document.teamId,
|
||||
},
|
||||
});
|
||||
|
||||
const { status, user } = document;
|
||||
|
||||
const isDocumentDeletedEmailEnabled = extractDerivedDocumentEmailSettings(
|
||||
@ -72,20 +74,18 @@ export const superDeleteDocument = async ({ id, requestMetadata }: SuperDeleteDo
|
||||
assetBaseUrl,
|
||||
});
|
||||
|
||||
const branding = document.team?.teamGlobalSettings
|
||||
? teamGlobalSettingsToBranding(document.team.teamGlobalSettings)
|
||||
: undefined;
|
||||
const lang = document.documentMeta?.language ?? settings.documentLanguage;
|
||||
|
||||
const [html, text] = await Promise.all([
|
||||
renderEmailWithI18N(template, { lang: document.documentMeta?.language, branding }),
|
||||
renderEmailWithI18N(template, { lang, branding }),
|
||||
renderEmailWithI18N(template, {
|
||||
lang: document.documentMeta?.language,
|
||||
lang,
|
||||
branding,
|
||||
plainText: true,
|
||||
}),
|
||||
]);
|
||||
|
||||
const i18n = await getI18nInstance(document.documentMeta?.language);
|
||||
const i18n = await getI18nInstance(lang);
|
||||
|
||||
await mailer.sendMail({
|
||||
to: {
|
||||
|
||||
@ -2,7 +2,6 @@ import { DocumentVisibility } from '@prisma/client';
|
||||
import { DocumentStatus, TeamMemberRole } from '@prisma/client';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
||||
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||
import type { CreateDocumentAuditLogDataResponse } from '@documenso/lib/utils/document-audit-logs';
|
||||
@ -13,17 +12,18 @@ import type { Attachment } from '@documenso/prisma/generated/zod/modelSchema/Att
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import type { TDocumentAccessAuthTypes, TDocumentActionAuthTypes } from '../../types/document-auth';
|
||||
import { createDocumentAuthOptions, extractDocumentAuthMethods } from '../../utils/document-auth';
|
||||
import { getDocumentWhereInput } from './get-document-by-id';
|
||||
|
||||
export type UpdateDocumentOptions = {
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
teamId: number;
|
||||
documentId: number;
|
||||
data?: {
|
||||
title?: string;
|
||||
externalId?: string | null;
|
||||
visibility?: DocumentVisibility | null;
|
||||
globalAccessAuth?: TDocumentAccessAuthTypes | null;
|
||||
globalActionAuth?: TDocumentActionAuthTypes | null;
|
||||
globalAccessAuth?: TDocumentAccessAuthTypes[];
|
||||
globalActionAuth?: TDocumentActionAuthTypes[];
|
||||
useLegacyFieldInsertion?: boolean;
|
||||
attachments?: Pick<Attachment, 'id' | 'label' | 'url'>[];
|
||||
};
|
||||
@ -37,34 +37,20 @@ export const updateDocument = async ({
|
||||
data,
|
||||
requestMetadata,
|
||||
}: UpdateDocumentOptions) => {
|
||||
const { documentWhereInput, team } = await getDocumentWhereInput({
|
||||
documentId,
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
const document = await prisma.document.findFirst({
|
||||
where: {
|
||||
id: documentId,
|
||||
...(teamId
|
||||
? {
|
||||
team: {
|
||||
id: teamId,
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
: {
|
||||
userId,
|
||||
teamId: null,
|
||||
}),
|
||||
},
|
||||
where: documentWhereInput,
|
||||
include: {
|
||||
team: {
|
||||
select: {
|
||||
members: {
|
||||
where: {
|
||||
userId,
|
||||
},
|
||||
organisation: {
|
||||
select: {
|
||||
role: true,
|
||||
organisationClaim: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -79,50 +65,46 @@ export const updateDocument = async ({
|
||||
});
|
||||
}
|
||||
|
||||
if (teamId) {
|
||||
const currentUserRole = document.team?.members[0]?.role;
|
||||
const isDocumentOwner = document.userId === userId;
|
||||
const requestedVisibility = data?.visibility;
|
||||
const isDocumentOwner = document.userId === userId;
|
||||
const requestedVisibility = data?.visibility;
|
||||
|
||||
if (!isDocumentOwner) {
|
||||
match(currentUserRole)
|
||||
.with(TeamMemberRole.ADMIN, () => true)
|
||||
.with(TeamMemberRole.MANAGER, () => {
|
||||
const allowedVisibilities: DocumentVisibility[] = [
|
||||
DocumentVisibility.EVERYONE,
|
||||
DocumentVisibility.MANAGER_AND_ABOVE,
|
||||
];
|
||||
if (!isDocumentOwner) {
|
||||
match(team.currentTeamRole)
|
||||
.with(TeamMemberRole.ADMIN, () => true)
|
||||
.with(TeamMemberRole.MANAGER, () => {
|
||||
const allowedVisibilities: DocumentVisibility[] = [
|
||||
DocumentVisibility.EVERYONE,
|
||||
DocumentVisibility.MANAGER_AND_ABOVE,
|
||||
];
|
||||
|
||||
if (
|
||||
!allowedVisibilities.includes(document.visibility) ||
|
||||
(requestedVisibility && !allowedVisibilities.includes(requestedVisibility))
|
||||
) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'You do not have permission to update the document visibility',
|
||||
});
|
||||
}
|
||||
})
|
||||
.with(TeamMemberRole.MEMBER, () => {
|
||||
if (
|
||||
document.visibility !== DocumentVisibility.EVERYONE ||
|
||||
(requestedVisibility && requestedVisibility !== DocumentVisibility.EVERYONE)
|
||||
) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'You do not have permission to update the document visibility',
|
||||
});
|
||||
}
|
||||
})
|
||||
.otherwise(() => {
|
||||
if (
|
||||
!allowedVisibilities.includes(document.visibility) ||
|
||||
(requestedVisibility && !allowedVisibilities.includes(requestedVisibility))
|
||||
) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'You do not have permission to update the document',
|
||||
message: 'You do not have permission to update the document visibility',
|
||||
});
|
||||
}
|
||||
})
|
||||
.with(TeamMemberRole.MEMBER, () => {
|
||||
if (
|
||||
document.visibility !== DocumentVisibility.EVERYONE ||
|
||||
(requestedVisibility && requestedVisibility !== DocumentVisibility.EVERYONE)
|
||||
) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'You do not have permission to update the document visibility',
|
||||
});
|
||||
}
|
||||
})
|
||||
.otherwise(() => {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'You do not have permission to update the document',
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// If no data just return the document since this function is normally chained after a meta update.
|
||||
if (!data || Object.values(data).length === 0) {
|
||||
console.log('no data');
|
||||
return document;
|
||||
}
|
||||
|
||||
@ -140,17 +122,10 @@ export const updateDocument = async ({
|
||||
data?.globalActionAuth === undefined ? documentGlobalActionAuth : data.globalActionAuth;
|
||||
|
||||
// Check if user has permission to set the global action auth.
|
||||
if (newGlobalActionAuth) {
|
||||
const isDocumentEnterprise = await isUserEnterprise({
|
||||
userId,
|
||||
teamId,
|
||||
if (newGlobalActionAuth.length > 0 && !document.team.organisation.organisationClaim.flags.cfr21) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'You do not have permission to set the action auth',
|
||||
});
|
||||
|
||||
if (!isDocumentEnterprise) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'You do not have permission to set the action auth',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const isTitleSame = data.title === undefined || data.title === document.title;
|
||||
|
||||
@ -1,81 +0,0 @@
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
||||
import type { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
export type UpdateTitleOptions = {
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
documentId: number;
|
||||
title: string;
|
||||
requestMetadata?: RequestMetadata;
|
||||
};
|
||||
|
||||
export const updateTitle = async ({
|
||||
userId,
|
||||
teamId,
|
||||
documentId,
|
||||
title,
|
||||
requestMetadata,
|
||||
}: UpdateTitleOptions) => {
|
||||
const user = await prisma.user.findFirstOrThrow({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
});
|
||||
|
||||
const document = await prisma.document.findFirstOrThrow({
|
||||
where: {
|
||||
id: documentId,
|
||||
...(teamId
|
||||
? {
|
||||
team: {
|
||||
id: teamId,
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
: {
|
||||
userId,
|
||||
teamId: null,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
if (document.title === title) {
|
||||
return document;
|
||||
}
|
||||
|
||||
return await prisma.$transaction(async (tx) => {
|
||||
// Instead of doing everything in a transaction we can use our knowledge
|
||||
// of the current document title to ensure we aren't performing a conflicting
|
||||
// update.
|
||||
const updatedDocument = await tx.document.update({
|
||||
where: {
|
||||
id: documentId,
|
||||
title: document.title,
|
||||
},
|
||||
data: {
|
||||
title,
|
||||
},
|
||||
});
|
||||
|
||||
await tx.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_TITLE_UPDATED,
|
||||
documentId,
|
||||
user,
|
||||
requestMetadata,
|
||||
data: {
|
||||
from: document.title,
|
||||
to: updatedDocument.title,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
return updatedDocument;
|
||||
});
|
||||
};
|
||||
@ -3,7 +3,6 @@ import { FieldType } from '@prisma/client';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import type { TRecipientActionAuth } from '../../types/document-auth';
|
||||
import { extractDocumentAuthMethods } from '../../utils/document-auth';
|
||||
import { isRecipientAuthorized } from './is-recipient-authorized';
|
||||
|
||||
export type ValidateFieldAuthOptions = {
|
||||
@ -26,14 +25,9 @@ export const validateFieldAuth = async ({
|
||||
userId,
|
||||
authOptions,
|
||||
}: ValidateFieldAuthOptions) => {
|
||||
const { derivedRecipientActionAuth } = extractDocumentAuthMethods({
|
||||
documentAuth: documentAuthOptions,
|
||||
recipientAuth: recipient.authOptions,
|
||||
});
|
||||
|
||||
// Override all non-signature fields to not require any auth.
|
||||
if (field.type !== FieldType.SIGNATURE) {
|
||||
return null;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const isValid = await isRecipientAuthorized({
|
||||
@ -50,5 +44,5 @@ export const validateFieldAuth = async ({
|
||||
});
|
||||
}
|
||||
|
||||
return derivedRecipientActionAuth;
|
||||
return authOptions?.type;
|
||||
};
|
||||
|
||||
@ -15,7 +15,7 @@ import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
|
||||
|
||||
export type ViewedDocumentOptions = {
|
||||
token: string;
|
||||
recipientAccessAuth?: TDocumentAccessAuthTypes | null;
|
||||
recipientAccessAuth?: TDocumentAccessAuthTypes[];
|
||||
requestMetadata?: RequestMetadata;
|
||||
};
|
||||
|
||||
@ -63,7 +63,7 @@ export const viewedDocument = async ({
|
||||
recipientId: recipient.id,
|
||||
recipientName: recipient.name,
|
||||
recipientRole: recipient.role,
|
||||
accessAuth: recipientAccessAuth || undefined,
|
||||
accessAuth: recipientAccessAuth ?? [],
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
90
packages/lib/server-only/email/get-email-context.ts
Normal file
90
packages/lib/server-only/email/get-email-context.ts
Normal file
@ -0,0 +1,90 @@
|
||||
import type { BrandingSettings } from '@documenso/email/providers/branding';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import type { OrganisationType } from '@documenso/prisma/client';
|
||||
import { type OrganisationClaim, type OrganisationGlobalSettings } from '@documenso/prisma/client';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import {
|
||||
organisationGlobalSettingsToBranding,
|
||||
teamGlobalSettingsToBranding,
|
||||
} from '../../utils/team-global-settings-to-branding';
|
||||
import { getTeamSettings } from '../team/get-team-settings';
|
||||
|
||||
type GetEmailContextOptions = {
|
||||
source:
|
||||
| {
|
||||
type: 'team';
|
||||
teamId: number;
|
||||
}
|
||||
| {
|
||||
type: 'organisation';
|
||||
organisationId: string;
|
||||
};
|
||||
};
|
||||
|
||||
type EmailContextResponse = {
|
||||
branding: BrandingSettings;
|
||||
settings: Omit<OrganisationGlobalSettings, 'id'>;
|
||||
claims: OrganisationClaim;
|
||||
organisationType: OrganisationType;
|
||||
};
|
||||
|
||||
export const getEmailContext = async (
|
||||
options: GetEmailContextOptions,
|
||||
): Promise<EmailContextResponse> => {
|
||||
const { source } = options;
|
||||
|
||||
const organisation = await prisma.organisation.findFirst({
|
||||
where:
|
||||
source.type === 'organisation'
|
||||
? {
|
||||
id: source.organisationId,
|
||||
}
|
||||
: {
|
||||
teams: {
|
||||
some: {
|
||||
id: source.teamId,
|
||||
},
|
||||
},
|
||||
},
|
||||
include: {
|
||||
subscription: true,
|
||||
organisationClaim: true,
|
||||
organisationGlobalSettings: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!organisation) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND);
|
||||
}
|
||||
|
||||
const claims = organisation.organisationClaim;
|
||||
|
||||
if (source.type === 'organisation') {
|
||||
return {
|
||||
branding: organisationGlobalSettingsToBranding(
|
||||
organisation.organisationGlobalSettings,
|
||||
organisation.id,
|
||||
claims.flags.hidePoweredBy ?? false,
|
||||
),
|
||||
settings: organisation.organisationGlobalSettings,
|
||||
claims,
|
||||
organisationType: organisation.type,
|
||||
};
|
||||
}
|
||||
|
||||
const teamSettings = await getTeamSettings({
|
||||
teamId: source.teamId,
|
||||
});
|
||||
|
||||
return {
|
||||
branding: teamGlobalSettingsToBranding(
|
||||
teamSettings,
|
||||
source.teamId,
|
||||
claims.flags.hidePoweredBy ?? false,
|
||||
),
|
||||
settings: teamSettings,
|
||||
claims,
|
||||
organisationType: organisation.type,
|
||||
};
|
||||
};
|
||||
@ -27,7 +27,6 @@ export const createEmbeddingPresignToken = async ({
|
||||
// In development mode, allow setting expiresIn to 0 for testing
|
||||
// In production, enforce a minimum expiration time
|
||||
const isDevelopment = env('NODE_ENV') !== 'production';
|
||||
console.log('isDevelopment', isDevelopment);
|
||||
const minExpirationMinutes = isDevelopment ? 0 : 5;
|
||||
|
||||
// Ensure expiresIn is at least the minimum allowed value
|
||||
|
||||
@ -6,10 +6,11 @@ import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { canRecipientFieldsBeModified } from '../../utils/recipients';
|
||||
import { getDocumentWhereInput } from '../document/get-document-by-id';
|
||||
|
||||
export interface CreateDocumentFieldsOptions {
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
teamId: number;
|
||||
documentId: number;
|
||||
fields: (TFieldAndMeta & {
|
||||
recipientId: number;
|
||||
@ -29,25 +30,14 @@ export const createDocumentFields = async ({
|
||||
fields,
|
||||
requestMetadata,
|
||||
}: CreateDocumentFieldsOptions) => {
|
||||
const { documentWhereInput } = await getDocumentWhereInput({
|
||||
documentId,
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
const document = await prisma.document.findFirst({
|
||||
where: {
|
||||
id: documentId,
|
||||
...(teamId
|
||||
? {
|
||||
team: {
|
||||
id: teamId,
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
: {
|
||||
userId,
|
||||
teamId: null,
|
||||
}),
|
||||
},
|
||||
where: documentWhereInput,
|
||||
include: {
|
||||
recipients: true,
|
||||
fields: true,
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { FieldType, Team } from '@prisma/client';
|
||||
import type { FieldType } from '@prisma/client';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
@ -13,11 +13,12 @@ import {
|
||||
import type { TFieldMetaSchema as FieldMeta } from '../../types/field-meta';
|
||||
import type { RequestMetadata } from '../../universal/extract-request-metadata';
|
||||
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
|
||||
import { getDocumentWhereInput } from '../document/get-document-by-id';
|
||||
|
||||
export type CreateFieldOptions = {
|
||||
documentId: number;
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
teamId: number;
|
||||
recipientId: number;
|
||||
type: FieldType;
|
||||
pageNumber: number;
|
||||
@ -43,60 +44,23 @@ export const createField = async ({
|
||||
fieldMeta,
|
||||
requestMetadata,
|
||||
}: CreateFieldOptions) => {
|
||||
const { documentWhereInput, team } = await getDocumentWhereInput({
|
||||
documentId,
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
const document = await prisma.document.findFirst({
|
||||
where: documentWhereInput,
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
where: {
|
||||
id: documentId,
|
||||
...(teamId
|
||||
? {
|
||||
team: {
|
||||
id: teamId,
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
: {
|
||||
userId,
|
||||
teamId: null,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
if (!document) {
|
||||
throw new Error('Document not found');
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const advancedField = ['NUMBER', 'RADIO', 'CHECKBOX', 'DROPDOWN', 'TEXT'].includes(type);
|
||||
|
||||
if (advancedField && !fieldMeta) {
|
||||
@ -154,9 +118,9 @@ export const createField = async ({
|
||||
type: 'FIELD_CREATED',
|
||||
documentId,
|
||||
user: {
|
||||
id: team?.id ?? user.id,
|
||||
email: team?.name ?? user.email,
|
||||
name: team ? '' : user.name,
|
||||
id: team.id,
|
||||
email: team.name,
|
||||
name: '',
|
||||
},
|
||||
data: {
|
||||
fieldId: field.secondaryId,
|
||||
|
||||
@ -5,10 +5,11 @@ import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { canRecipientFieldsBeModified } from '../../utils/recipients';
|
||||
import { buildTeamWhereQuery } from '../../utils/teams';
|
||||
|
||||
export interface CreateTemplateFieldsOptions {
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
teamId: number;
|
||||
templateId: number;
|
||||
fields: {
|
||||
recipientId: number;
|
||||
@ -31,21 +32,7 @@ export const createTemplateFields = async ({
|
||||
const template = await prisma.template.findFirst({
|
||||
where: {
|
||||
id: templateId,
|
||||
...(teamId
|
||||
? {
|
||||
team: {
|
||||
id: teamId,
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
: {
|
||||
userId,
|
||||
teamId: null,
|
||||
}),
|
||||
team: buildTeamWhereQuery({ teamId, userId }),
|
||||
},
|
||||
include: {
|
||||
recipients: true,
|
||||
|
||||
@ -5,10 +5,11 @@ import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { canRecipientFieldsBeModified } from '../../utils/recipients';
|
||||
import { getDocumentWhereInput } from '../document/get-document-by-id';
|
||||
|
||||
export interface DeleteDocumentFieldOptions {
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
teamId: number;
|
||||
fieldId: number;
|
||||
requestMetadata: ApiRequestMetadata;
|
||||
}
|
||||
@ -39,25 +40,14 @@ export const deleteDocumentField = async ({
|
||||
});
|
||||
}
|
||||
|
||||
const { documentWhereInput } = await getDocumentWhereInput({
|
||||
documentId,
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
const document = await prisma.document.findFirst({
|
||||
where: {
|
||||
id: documentId,
|
||||
...(teamId
|
||||
? {
|
||||
team: {
|
||||
id: teamId,
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
: {
|
||||
userId,
|
||||
teamId: null,
|
||||
}),
|
||||
},
|
||||
where: documentWhereInput,
|
||||
include: {
|
||||
recipients: {
|
||||
where: {
|
||||
|
||||
@ -4,12 +4,13 @@ import { prisma } from '@documenso/prisma';
|
||||
|
||||
import type { RequestMetadata } from '../../universal/extract-request-metadata';
|
||||
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
|
||||
import { buildTeamWhereQuery } from '../../utils/teams';
|
||||
|
||||
export type DeleteFieldOptions = {
|
||||
fieldId: number;
|
||||
documentId: number;
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
teamId: number;
|
||||
requestMetadata?: RequestMetadata;
|
||||
};
|
||||
|
||||
@ -25,21 +26,8 @@ export const deleteField = async ({
|
||||
id: fieldId,
|
||||
document: {
|
||||
id: documentId,
|
||||
...(teamId
|
||||
? {
|
||||
team: {
|
||||
id: teamId,
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
: {
|
||||
userId,
|
||||
teamId: null,
|
||||
}),
|
||||
userId,
|
||||
team: buildTeamWhereQuery({ teamId, userId }),
|
||||
},
|
||||
},
|
||||
include: {
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { buildTeamWhereQuery } from '../../utils/teams';
|
||||
|
||||
export interface DeleteTemplateFieldOptions {
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
teamId: number;
|
||||
fieldId: number;
|
||||
}
|
||||
|
||||
@ -16,21 +17,9 @@ export const deleteTemplateField = async ({
|
||||
const field = await prisma.field.findFirst({
|
||||
where: {
|
||||
id: fieldId,
|
||||
template: teamId
|
||||
? {
|
||||
team: {
|
||||
id: teamId,
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
: {
|
||||
userId,
|
||||
teamId: null,
|
||||
},
|
||||
template: {
|
||||
team: buildTeamWhereQuery({ teamId, userId }),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@ -1,53 +1,49 @@
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { buildTeamWhereQuery } from '../../utils/teams';
|
||||
|
||||
export type GetFieldByIdOptions = {
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
teamId: number;
|
||||
fieldId: number;
|
||||
documentId?: number;
|
||||
templateId?: number;
|
||||
};
|
||||
|
||||
export const getFieldById = async ({
|
||||
userId,
|
||||
teamId,
|
||||
fieldId,
|
||||
documentId,
|
||||
templateId,
|
||||
}: GetFieldByIdOptions) => {
|
||||
export const getFieldById = async ({ userId, teamId, fieldId }: GetFieldByIdOptions) => {
|
||||
const field = await prisma.field.findFirst({
|
||||
where: {
|
||||
id: fieldId,
|
||||
documentId,
|
||||
templateId,
|
||||
},
|
||||
include: {
|
||||
document: {
|
||||
OR:
|
||||
teamId === undefined
|
||||
? [
|
||||
{
|
||||
userId,
|
||||
teamId: null,
|
||||
},
|
||||
]
|
||||
: [
|
||||
{
|
||||
teamId,
|
||||
team: {
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
select: {
|
||||
teamId: true,
|
||||
},
|
||||
},
|
||||
template: {
|
||||
select: {
|
||||
teamId: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!field) {
|
||||
const foundTeamId = field?.document?.teamId || field?.template?.teamId;
|
||||
|
||||
if (!field || !foundTeamId || foundTeamId !== teamId) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Field not found',
|
||||
});
|
||||
}
|
||||
|
||||
const team = await prisma.team.findUnique({
|
||||
where: buildTeamWhereQuery({
|
||||
teamId: foundTeamId,
|
||||
userId,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!team) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Field not found',
|
||||
});
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { buildTeamWhereQuery } from '../../utils/teams';
|
||||
|
||||
export interface GetFieldsForDocumentOptions {
|
||||
documentId: number;
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
teamId: number;
|
||||
}
|
||||
|
||||
export type DocumentField = Awaited<ReturnType<typeof getFieldsForDocument>>[number];
|
||||
@ -15,22 +17,10 @@ export const getFieldsForDocument = async ({
|
||||
}: GetFieldsForDocumentOptions) => {
|
||||
const fields = await prisma.field.findMany({
|
||||
where: {
|
||||
documentId,
|
||||
document: teamId
|
||||
? {
|
||||
team: {
|
||||
id: teamId,
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
: {
|
||||
userId,
|
||||
teamId: null,
|
||||
},
|
||||
document: {
|
||||
id: documentId,
|
||||
team: buildTeamWhereQuery({ teamId, userId }),
|
||||
},
|
||||
},
|
||||
include: {
|
||||
signature: true,
|
||||
|
||||
@ -26,10 +26,11 @@ import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { canRecipientFieldsBeModified } from '../../utils/recipients';
|
||||
import { getDocumentWhereInput } from '../document/get-document-by-id';
|
||||
|
||||
export interface SetFieldsForDocumentOptions {
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
teamId: number;
|
||||
documentId: number;
|
||||
fields: FieldData[];
|
||||
requestMetadata: ApiRequestMetadata;
|
||||
@ -42,25 +43,14 @@ export const setFieldsForDocument = async ({
|
||||
fields,
|
||||
requestMetadata,
|
||||
}: SetFieldsForDocumentOptions) => {
|
||||
const { documentWhereInput } = await getDocumentWhereInput({
|
||||
documentId,
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
const document = await prisma.document.findFirst({
|
||||
where: {
|
||||
id: documentId,
|
||||
...(teamId
|
||||
? {
|
||||
team: {
|
||||
id: teamId,
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
: {
|
||||
userId,
|
||||
teamId: null,
|
||||
}),
|
||||
},
|
||||
where: documentWhereInput,
|
||||
include: {
|
||||
recipients: true,
|
||||
},
|
||||
|
||||
@ -16,9 +16,11 @@ import {
|
||||
} from '@documenso/lib/types/field-meta';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { buildTeamWhereQuery } from '../../utils/teams';
|
||||
|
||||
export type SetFieldsForTemplateOptions = {
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
teamId: number;
|
||||
templateId: number;
|
||||
fields: {
|
||||
id?: number | null;
|
||||
@ -42,21 +44,7 @@ export const setFieldsForTemplate = async ({
|
||||
const template = await prisma.template.findFirst({
|
||||
where: {
|
||||
id: templateId,
|
||||
...(teamId
|
||||
? {
|
||||
team: {
|
||||
id: teamId,
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
: {
|
||||
userId,
|
||||
teamId: null,
|
||||
}),
|
||||
team: buildTeamWhereQuery({ teamId, userId }),
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@ -11,10 +11,11 @@ import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { canRecipientFieldsBeModified } from '../../utils/recipients';
|
||||
import { getDocumentWhereInput } from '../document/get-document-by-id';
|
||||
|
||||
export interface UpdateDocumentFieldsOptions {
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
teamId: number;
|
||||
documentId: number;
|
||||
fields: {
|
||||
id: number;
|
||||
@ -36,25 +37,14 @@ export const updateDocumentFields = async ({
|
||||
fields,
|
||||
requestMetadata,
|
||||
}: UpdateDocumentFieldsOptions) => {
|
||||
const { documentWhereInput } = await getDocumentWhereInput({
|
||||
documentId,
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
const document = await prisma.document.findFirst({
|
||||
where: {
|
||||
id: documentId,
|
||||
...(teamId
|
||||
? {
|
||||
team: {
|
||||
id: teamId,
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
: {
|
||||
userId,
|
||||
teamId: null,
|
||||
}),
|
||||
},
|
||||
where: documentWhereInput,
|
||||
include: {
|
||||
recipients: true,
|
||||
fields: true,
|
||||
|
||||
@ -6,12 +6,13 @@ import { prisma } from '@documenso/prisma';
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs';
|
||||
import type { RequestMetadata } from '../../universal/extract-request-metadata';
|
||||
import { createDocumentAuditLogData, diffFieldChanges } from '../../utils/document-audit-logs';
|
||||
import { buildTeamWhereQuery } from '../../utils/teams';
|
||||
|
||||
export type UpdateFieldOptions = {
|
||||
fieldId: number;
|
||||
documentId: number;
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
teamId: number;
|
||||
recipientId?: number;
|
||||
type?: FieldType;
|
||||
pageNumber?: number;
|
||||
@ -47,21 +48,8 @@ export const updateField = async ({
|
||||
id: fieldId,
|
||||
document: {
|
||||
id: documentId,
|
||||
...(teamId
|
||||
? {
|
||||
team: {
|
||||
id: teamId,
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
: {
|
||||
userId,
|
||||
teamId: null,
|
||||
}),
|
||||
userId,
|
||||
team: buildTeamWhereQuery({ teamId, userId }),
|
||||
},
|
||||
},
|
||||
});
|
||||
@ -101,14 +89,7 @@ export const updateField = async ({
|
||||
|
||||
if (teamId) {
|
||||
team = await prisma.team.findFirst({
|
||||
where: {
|
||||
id: teamId,
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
where: buildTeamWhereQuery({ teamId, userId }),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -5,10 +5,11 @@ import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { canRecipientFieldsBeModified } from '../../utils/recipients';
|
||||
import { buildTeamWhereQuery } from '../../utils/teams';
|
||||
|
||||
export interface UpdateTemplateFieldsOptions {
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
teamId: number;
|
||||
templateId: number;
|
||||
fields: {
|
||||
id: number;
|
||||
@ -31,21 +32,7 @@ export const updateTemplateFields = async ({
|
||||
const template = await prisma.template.findFirst({
|
||||
where: {
|
||||
id: templateId,
|
||||
...(teamId
|
||||
? {
|
||||
team: {
|
||||
id: teamId,
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
: {
|
||||
userId,
|
||||
teamId: null,
|
||||
}),
|
||||
team: buildTeamWhereQuery({ teamId, userId }),
|
||||
},
|
||||
include: {
|
||||
recipients: true,
|
||||
|
||||
@ -1,16 +1,14 @@
|
||||
import { TeamMemberRole } from '@prisma/client';
|
||||
import type { Team, TeamGlobalSettings } from '@prisma/client';
|
||||
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import type { TFolderType } from '../../types/folder-type';
|
||||
import { FolderType } from '../../types/folder-type';
|
||||
import { determineDocumentVisibility } from '../../utils/document-visibility';
|
||||
import { getTeamById } from '../team/get-team';
|
||||
import { getTeamSettings } from '../team/get-team-settings';
|
||||
|
||||
export interface CreateFolderOptions {
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
teamId: number;
|
||||
name: string;
|
||||
parentId?: string | null;
|
||||
type?: TFolderType;
|
||||
@ -23,52 +21,9 @@ export const createFolder = async ({
|
||||
parentId,
|
||||
type = FolderType.DOCUMENT,
|
||||
}: CreateFolderOptions) => {
|
||||
const user = await prisma.user.findFirstOrThrow({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
include: {
|
||||
teamMembers: {
|
||||
select: {
|
||||
teamId: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const team = await getTeamById({ userId, teamId });
|
||||
|
||||
if (
|
||||
teamId !== undefined &&
|
||||
!user.teamMembers.some((teamMember) => teamMember.teamId === teamId)
|
||||
) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Team not found',
|
||||
});
|
||||
}
|
||||
|
||||
let team: (Team & { teamGlobalSettings: TeamGlobalSettings | null }) | null = null;
|
||||
let userTeamRole: TeamMemberRole | undefined;
|
||||
|
||||
if (teamId) {
|
||||
const teamWithUserRole = await prisma.team.findFirstOrThrow({
|
||||
where: {
|
||||
id: teamId,
|
||||
},
|
||||
include: {
|
||||
teamGlobalSettings: true,
|
||||
members: {
|
||||
where: {
|
||||
userId: userId,
|
||||
},
|
||||
select: {
|
||||
role: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
team = teamWithUserRole;
|
||||
userTeamRole = teamWithUserRole.members[0]?.role;
|
||||
}
|
||||
const settings = await getTeamSettings({ userId, teamId });
|
||||
|
||||
return await prisma.folder.create({
|
||||
data: {
|
||||
@ -77,10 +32,7 @@ export const createFolder = async ({
|
||||
teamId,
|
||||
parentId,
|
||||
type,
|
||||
visibility: determineDocumentVisibility(
|
||||
team?.teamGlobalSettings?.documentVisibility,
|
||||
userTeamRole ?? TeamMemberRole.MEMBER,
|
||||
),
|
||||
visibility: determineDocumentVisibility(settings.documentVisibility, team.currentTeamRole),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@ -4,45 +4,16 @@ import { match } from 'ts-pattern';
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { getTeamById } from '../team/get-team';
|
||||
|
||||
export interface DeleteFolderOptions {
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
teamId: number;
|
||||
folderId: string;
|
||||
}
|
||||
|
||||
export const deleteFolder = async ({ userId, teamId, folderId }: DeleteFolderOptions) => {
|
||||
let teamMemberRole: TeamMemberRole | null = null;
|
||||
|
||||
if (teamId) {
|
||||
const team = await prisma.team.findFirst({
|
||||
where: {
|
||||
id: teamId,
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
include: {
|
||||
members: {
|
||||
where: {
|
||||
userId,
|
||||
},
|
||||
select: {
|
||||
role: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!team) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Team not found',
|
||||
});
|
||||
}
|
||||
|
||||
teamMemberRole = team.members[0]?.role ?? null;
|
||||
}
|
||||
const team = await getTeamById({ userId, teamId });
|
||||
|
||||
const folder = await prisma.folder.findFirst({
|
||||
where: {
|
||||
@ -63,18 +34,16 @@ export const deleteFolder = async ({ userId, teamId, folderId }: DeleteFolderOpt
|
||||
});
|
||||
}
|
||||
|
||||
if (teamId && teamMemberRole) {
|
||||
const hasPermission = match(teamMemberRole)
|
||||
.with(TeamMemberRole.ADMIN, () => true)
|
||||
.with(TeamMemberRole.MANAGER, () => folder.visibility !== DocumentVisibility.ADMIN)
|
||||
.with(TeamMemberRole.MEMBER, () => folder.visibility === DocumentVisibility.EVERYONE)
|
||||
.otherwise(() => false);
|
||||
const hasPermission = match(team.currentTeamRole)
|
||||
.with(TeamMemberRole.ADMIN, () => true)
|
||||
.with(TeamMemberRole.MANAGER, () => folder.visibility !== DocumentVisibility.ADMIN)
|
||||
.with(TeamMemberRole.MEMBER, () => folder.visibility === DocumentVisibility.EVERYONE)
|
||||
.otherwise(() => false);
|
||||
|
||||
if (!hasPermission) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'You do not have permission to delete this folder',
|
||||
});
|
||||
}
|
||||
if (!hasPermission) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'You do not have permission to delete this folder',
|
||||
});
|
||||
}
|
||||
|
||||
return await prisma.folder.delete({
|
||||
|
||||
@ -5,49 +5,19 @@ import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { DocumentVisibility } from '../../types/document-visibility';
|
||||
import type { TFolderType } from '../../types/folder-type';
|
||||
import { getTeamById } from '../team/get-team';
|
||||
|
||||
export interface FindFoldersOptions {
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
teamId: number;
|
||||
parentId?: string | null;
|
||||
type?: TFolderType;
|
||||
}
|
||||
|
||||
export const findFolders = async ({ userId, teamId, parentId, type }: FindFoldersOptions) => {
|
||||
let team = null;
|
||||
let teamMemberRole = null;
|
||||
const team = await getTeamById({ userId, teamId });
|
||||
|
||||
if (teamId !== undefined) {
|
||||
try {
|
||||
team = await prisma.team.findFirstOrThrow({
|
||||
where: {
|
||||
id: teamId,
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
include: {
|
||||
members: {
|
||||
where: {
|
||||
userId,
|
||||
},
|
||||
select: {
|
||||
role: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
teamMemberRole = team.members[0].role;
|
||||
} catch (error) {
|
||||
console.error('Error finding team:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
const visibilityFilters = match(teamMemberRole)
|
||||
const visibilityFilters = match(team.currentTeamRole)
|
||||
.with(TeamMemberRole.ADMIN, () => ({
|
||||
visibility: {
|
||||
in: [
|
||||
@ -67,14 +37,12 @@ export const findFolders = async ({ userId, teamId, parentId, type }: FindFolder
|
||||
const whereClause = {
|
||||
AND: [
|
||||
{ parentId },
|
||||
teamId
|
||||
? {
|
||||
OR: [
|
||||
{ teamId, ...visibilityFilters },
|
||||
{ userId, teamId },
|
||||
],
|
||||
}
|
||||
: { userId, teamId: null },
|
||||
{
|
||||
OR: [
|
||||
{ teamId, ...visibilityFilters },
|
||||
{ userId, teamId },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@ -94,7 +62,8 @@ export const findFolders = async ({ userId, teamId, parentId, type }: FindFolder
|
||||
prisma.folder.findMany({
|
||||
where: {
|
||||
parentId: folder.id,
|
||||
...(teamId ? { teamId, ...visibilityFilters } : { userId, teamId: null }),
|
||||
teamId,
|
||||
...visibilityFilters,
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
@ -113,7 +82,8 @@ export const findFolders = async ({ userId, teamId, parentId, type }: FindFolder
|
||||
prisma.folder.count({
|
||||
where: {
|
||||
parentId: folder.id,
|
||||
...(teamId ? { teamId, ...visibilityFilters } : { userId, teamId: null }),
|
||||
teamId,
|
||||
...visibilityFilters,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
@ -5,10 +5,11 @@ import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { DocumentVisibility } from '../../types/document-visibility';
|
||||
import type { TFolderType } from '../../types/folder-type';
|
||||
import { getTeamById } from '../team/get-team';
|
||||
|
||||
export interface GetFolderBreadcrumbsOptions {
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
teamId: number;
|
||||
folderId: string;
|
||||
type?: TFolderType;
|
||||
}
|
||||
@ -19,39 +20,9 @@ export const getFolderBreadcrumbs = async ({
|
||||
folderId,
|
||||
type,
|
||||
}: GetFolderBreadcrumbsOptions) => {
|
||||
let teamMemberRole = null;
|
||||
const team = await getTeamById({ userId, teamId });
|
||||
|
||||
if (teamId !== undefined) {
|
||||
try {
|
||||
const team = await prisma.team.findFirstOrThrow({
|
||||
where: {
|
||||
id: teamId,
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
include: {
|
||||
members: {
|
||||
where: {
|
||||
userId,
|
||||
},
|
||||
select: {
|
||||
role: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
teamMemberRole = team.members[0].role;
|
||||
} catch (error) {
|
||||
console.error('Error finding team:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
const visibilityFilters = match(teamMemberRole)
|
||||
const visibilityFilters = match(team.currentTeamRole)
|
||||
.with(TeamMemberRole.ADMIN, () => ({
|
||||
visibility: {
|
||||
in: [
|
||||
@ -71,14 +42,10 @@ export const getFolderBreadcrumbs = async ({
|
||||
const whereClause = (folderId: string) => ({
|
||||
id: folderId,
|
||||
...(type ? { type } : {}),
|
||||
...(teamId
|
||||
? {
|
||||
OR: [
|
||||
{ teamId, ...visibilityFilters },
|
||||
{ userId, teamId },
|
||||
],
|
||||
}
|
||||
: { userId, teamId: null }),
|
||||
OR: [
|
||||
{ teamId, ...visibilityFilters },
|
||||
{ userId, teamId },
|
||||
],
|
||||
});
|
||||
|
||||
const breadcrumbs = [];
|
||||
|
||||
@ -6,49 +6,19 @@ import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { DocumentVisibility } from '../../types/document-visibility';
|
||||
import type { TFolderType } from '../../types/folder-type';
|
||||
import { getTeamById } from '../team/get-team';
|
||||
|
||||
export interface GetFolderByIdOptions {
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
teamId: number;
|
||||
folderId?: string;
|
||||
type?: TFolderType;
|
||||
}
|
||||
|
||||
export const getFolderById = async ({ userId, teamId, folderId, type }: GetFolderByIdOptions) => {
|
||||
let teamMemberRole = null;
|
||||
const team = await getTeamById({ userId, teamId });
|
||||
|
||||
if (teamId !== undefined) {
|
||||
try {
|
||||
const team = await prisma.team.findFirstOrThrow({
|
||||
where: {
|
||||
id: teamId,
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
include: {
|
||||
members: {
|
||||
where: {
|
||||
userId,
|
||||
},
|
||||
select: {
|
||||
role: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
teamMemberRole = team.members[0].role;
|
||||
} catch (error) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Team not found',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const visibilityFilters = match(teamMemberRole)
|
||||
const visibilityFilters = match(team.currentTeamRole)
|
||||
.with(TeamMemberRole.ADMIN, () => ({
|
||||
visibility: {
|
||||
in: [
|
||||
@ -68,14 +38,10 @@ export const getFolderById = async ({ userId, teamId, folderId, type }: GetFolde
|
||||
const whereClause = {
|
||||
id: folderId,
|
||||
...(type ? { type } : {}),
|
||||
...(teamId
|
||||
? {
|
||||
OR: [
|
||||
{ teamId, ...visibilityFilters },
|
||||
{ userId, teamId },
|
||||
],
|
||||
}
|
||||
: { userId, teamId: null }),
|
||||
OR: [
|
||||
{ teamId, ...visibilityFilters },
|
||||
{ userId, teamId },
|
||||
],
|
||||
};
|
||||
|
||||
const folder = await prisma.folder.findFirst({
|
||||
|
||||
@ -7,9 +7,11 @@ import { FolderType } from '@documenso/lib/types/folder-type';
|
||||
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { getTeamById } from '../team/get-team';
|
||||
|
||||
export interface MoveDocumentToFolderOptions {
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
teamId: number;
|
||||
documentId: number;
|
||||
folderId?: string | null;
|
||||
requestMetadata?: ApiRequestMetadata;
|
||||
@ -21,41 +23,9 @@ export const moveDocumentToFolder = async ({
|
||||
documentId,
|
||||
folderId,
|
||||
}: MoveDocumentToFolderOptions) => {
|
||||
let teamMemberRole = null;
|
||||
const team = await getTeamById({ userId, teamId });
|
||||
|
||||
if (teamId !== undefined) {
|
||||
try {
|
||||
const team = await prisma.team.findFirstOrThrow({
|
||||
where: {
|
||||
id: teamId,
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
include: {
|
||||
members: {
|
||||
where: {
|
||||
userId,
|
||||
},
|
||||
select: {
|
||||
role: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
teamMemberRole = team.members[0].role;
|
||||
} catch (error) {
|
||||
console.error('Error finding team:', error);
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Team not found',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const visibilityFilters = match(teamMemberRole)
|
||||
const visibilityFilters = match(team.currentTeamRole)
|
||||
.with(TeamMemberRole.ADMIN, () => ({
|
||||
visibility: {
|
||||
in: [
|
||||
@ -74,14 +44,10 @@ export const moveDocumentToFolder = async ({
|
||||
|
||||
const documentWhereClause = {
|
||||
id: documentId,
|
||||
...(teamId
|
||||
? {
|
||||
OR: [
|
||||
{ teamId, ...visibilityFilters },
|
||||
{ userId, teamId },
|
||||
],
|
||||
}
|
||||
: { userId, teamId: null }),
|
||||
OR: [
|
||||
{ teamId, ...visibilityFilters },
|
||||
{ userId, teamId },
|
||||
],
|
||||
};
|
||||
|
||||
const document = await prisma.document.findFirst({
|
||||
@ -98,14 +64,10 @@ export const moveDocumentToFolder = async ({
|
||||
const folderWhereClause = {
|
||||
id: folderId,
|
||||
type: FolderType.DOCUMENT,
|
||||
...(teamId
|
||||
? {
|
||||
OR: [
|
||||
{ teamId, ...visibilityFilters },
|
||||
{ userId, teamId },
|
||||
],
|
||||
}
|
||||
: { userId, teamId: null }),
|
||||
OR: [
|
||||
{ teamId, ...visibilityFilters },
|
||||
{ userId, teamId },
|
||||
],
|
||||
};
|
||||
|
||||
const folder = await prisma.folder.findFirst({
|
||||
|
||||
@ -0,0 +1,105 @@
|
||||
import { OrganisationGroupType, OrganisationMemberInviteStatus } from '@prisma/client';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { jobs } from '../../jobs/client';
|
||||
import { generateDatabaseId } from '../../universal/id';
|
||||
|
||||
export type AcceptOrganisationInvitationOptions = {
|
||||
token: string;
|
||||
};
|
||||
|
||||
export const acceptOrganisationInvitation = async ({
|
||||
token,
|
||||
}: AcceptOrganisationInvitationOptions) => {
|
||||
const organisationMemberInvite = await prisma.organisationMemberInvite.findFirst({
|
||||
where: {
|
||||
token,
|
||||
status: {
|
||||
not: OrganisationMemberInviteStatus.DECLINED,
|
||||
},
|
||||
},
|
||||
include: {
|
||||
organisation: {
|
||||
include: {
|
||||
groups: {
|
||||
include: {
|
||||
teamGroups: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!organisationMemberInvite) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND);
|
||||
}
|
||||
|
||||
if (organisationMemberInvite.status === OrganisationMemberInviteStatus.ACCEPTED) {
|
||||
return;
|
||||
}
|
||||
|
||||
const user = await prisma.user.findFirst({
|
||||
where: {
|
||||
email: organisationMemberInvite.email,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'User must exist to accept an organisation invitation',
|
||||
});
|
||||
}
|
||||
|
||||
const { organisation } = organisationMemberInvite;
|
||||
|
||||
const organisationGroupToUse = organisation.groups.find(
|
||||
(group) =>
|
||||
group.type === OrganisationGroupType.INTERNAL_ORGANISATION &&
|
||||
group.organisationRole === organisationMemberInvite.organisationRole,
|
||||
);
|
||||
|
||||
if (!organisationGroupToUse) {
|
||||
throw new AppError(AppErrorCode.UNKNOWN_ERROR, {
|
||||
message: 'Organisation group not found',
|
||||
});
|
||||
}
|
||||
|
||||
await prisma.$transaction(
|
||||
async (tx) => {
|
||||
await tx.organisationMember.create({
|
||||
data: {
|
||||
id: generateDatabaseId('member'),
|
||||
userId: user.id,
|
||||
organisationId: organisation.id,
|
||||
organisationGroupMembers: {
|
||||
create: {
|
||||
id: generateDatabaseId('group_member'),
|
||||
groupId: organisationGroupToUse.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await tx.organisationMemberInvite.update({
|
||||
where: {
|
||||
id: organisationMemberInvite.id,
|
||||
},
|
||||
data: {
|
||||
status: OrganisationMemberInviteStatus.ACCEPTED,
|
||||
},
|
||||
});
|
||||
|
||||
await jobs.triggerJob({
|
||||
name: 'send.organisation-member-joined.email',
|
||||
payload: {
|
||||
organisationId: organisation.id,
|
||||
memberUserId: user.id,
|
||||
},
|
||||
});
|
||||
},
|
||||
{ timeout: 30_000 },
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,224 @@
|
||||
import { createElement } from 'react';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import type { Organisation, Prisma } from '@prisma/client';
|
||||
import { OrganisationMemberInviteStatus } from '@prisma/client';
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
import { syncMemberCountWithStripeSeatPlan } from '@documenso/ee/server-only/stripe/update-subscription-item-quantity';
|
||||
import { mailer } from '@documenso/email/mailer';
|
||||
import { OrganisationInviteEmailTemplate } from '@documenso/email/templates/organisation-invite';
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
||||
import { FROM_ADDRESS, FROM_NAME } from '@documenso/lib/constants/email';
|
||||
import { ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP } from '@documenso/lib/constants/organisations';
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { isOrganisationRoleWithinUserHierarchy } from '@documenso/lib/utils/organisations';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import type { TCreateOrganisationMemberInvitesRequestSchema } from '@documenso/trpc/server/organisation-router/create-organisation-member-invites.types';
|
||||
|
||||
import { getI18nInstance } from '../../client-only/providers/i18n-server';
|
||||
import { generateDatabaseId } from '../../universal/id';
|
||||
import { validateIfSubscriptionIsRequired } from '../../utils/billing';
|
||||
import { buildOrganisationWhereQuery } from '../../utils/organisations';
|
||||
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
||||
import { getEmailContext } from '../email/get-email-context';
|
||||
import { getMemberOrganisationRole } from '../team/get-member-roles';
|
||||
|
||||
export type CreateOrganisationMemberInvitesOptions = {
|
||||
userId: number;
|
||||
userName: string;
|
||||
organisationId: string;
|
||||
invitations: TCreateOrganisationMemberInvitesRequestSchema['invitations'];
|
||||
};
|
||||
|
||||
/**
|
||||
* Invite organisation members via email to join a organisation.
|
||||
*/
|
||||
export const createOrganisationMemberInvites = async ({
|
||||
userId,
|
||||
userName,
|
||||
organisationId,
|
||||
invitations,
|
||||
}: CreateOrganisationMemberInvitesOptions): Promise<void> => {
|
||||
const organisation = await prisma.organisation.findFirst({
|
||||
where: buildOrganisationWhereQuery({
|
||||
organisationId,
|
||||
userId,
|
||||
roles: ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_ORGANISATION'],
|
||||
}),
|
||||
include: {
|
||||
members: {
|
||||
select: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
invites: {
|
||||
where: {
|
||||
status: OrganisationMemberInviteStatus.PENDING,
|
||||
},
|
||||
},
|
||||
organisationGlobalSettings: true,
|
||||
organisationClaim: true,
|
||||
subscription: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!organisation) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND);
|
||||
}
|
||||
|
||||
const { organisationClaim } = organisation;
|
||||
|
||||
const subscription = validateIfSubscriptionIsRequired(organisation.subscription);
|
||||
|
||||
const currentOrganisationMemberRole = await getMemberOrganisationRole({
|
||||
organisationId: organisation.id,
|
||||
reference: {
|
||||
type: 'User',
|
||||
id: userId,
|
||||
},
|
||||
});
|
||||
|
||||
const organisationMemberEmails = organisation.members.map((member) => member.user.email);
|
||||
const organisationMemberInviteEmails = organisation.invites.map((invite) => invite.email);
|
||||
|
||||
const usersToInvite = invitations.filter((invitation) => {
|
||||
// Filter out users that are already members of the organisation.
|
||||
if (organisationMemberEmails.includes(invitation.email)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Filter out users that have already been invited to the organisation.
|
||||
if (organisationMemberInviteEmails.includes(invitation.email)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
const unauthorizedRoleAccess = usersToInvite.some(
|
||||
({ organisationRole }) =>
|
||||
!isOrganisationRoleWithinUserHierarchy(currentOrganisationMemberRole, organisationRole),
|
||||
);
|
||||
|
||||
if (unauthorizedRoleAccess) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'User does not have permission to set high level roles',
|
||||
});
|
||||
}
|
||||
|
||||
const organisationMemberInvites: Prisma.OrganisationMemberInviteCreateManyInput[] =
|
||||
usersToInvite.map(({ email, organisationRole }) => ({
|
||||
id: generateDatabaseId('member_invite'),
|
||||
email,
|
||||
organisationId,
|
||||
organisationRole,
|
||||
token: nanoid(32),
|
||||
}));
|
||||
|
||||
const numberOfCurrentMembers = organisation.members.length;
|
||||
const numberOfCurrentInvites = organisation.invites.length;
|
||||
const numberOfNewInvites = organisationMemberInvites.length;
|
||||
|
||||
const totalMemberCountWithInvites =
|
||||
numberOfCurrentMembers + numberOfCurrentInvites + numberOfNewInvites;
|
||||
|
||||
// Handle billing for seat based plans.
|
||||
if (subscription) {
|
||||
await syncMemberCountWithStripeSeatPlan(
|
||||
subscription,
|
||||
organisationClaim,
|
||||
totalMemberCountWithInvites,
|
||||
);
|
||||
}
|
||||
|
||||
await prisma.organisationMemberInvite.createMany({
|
||||
data: organisationMemberInvites,
|
||||
});
|
||||
|
||||
const sendEmailResult = await Promise.allSettled(
|
||||
organisationMemberInvites.map(async ({ email, token }) =>
|
||||
sendOrganisationMemberInviteEmail({
|
||||
email,
|
||||
token,
|
||||
organisation,
|
||||
senderName: userName,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
const sendEmailResultErrorList = sendEmailResult.filter(
|
||||
(result): result is PromiseRejectedResult => result.status === 'rejected',
|
||||
);
|
||||
|
||||
if (sendEmailResultErrorList.length > 0) {
|
||||
console.error(JSON.stringify(sendEmailResultErrorList));
|
||||
|
||||
throw new AppError('EmailDeliveryFailed', {
|
||||
message: 'Failed to send invite emails to one or more users.',
|
||||
userMessage: `Failed to send invites to ${sendEmailResultErrorList.length}/${organisationMemberInvites.length} users.`,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
type SendOrganisationMemberInviteEmailOptions = {
|
||||
email: string;
|
||||
senderName: string;
|
||||
token: string;
|
||||
organisation: Pick<Organisation, 'id' | 'name'>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Send an email to a user inviting them to join a organisation.
|
||||
*/
|
||||
export const sendOrganisationMemberInviteEmail = async ({
|
||||
email,
|
||||
senderName,
|
||||
token,
|
||||
organisation,
|
||||
}: SendOrganisationMemberInviteEmailOptions) => {
|
||||
const template = createElement(OrganisationInviteEmailTemplate, {
|
||||
assetBaseUrl: NEXT_PUBLIC_WEBAPP_URL(),
|
||||
baseUrl: NEXT_PUBLIC_WEBAPP_URL(),
|
||||
senderName,
|
||||
token,
|
||||
organisationName: organisation.name,
|
||||
});
|
||||
|
||||
const { branding, settings } = await getEmailContext({
|
||||
source: {
|
||||
type: 'organisation',
|
||||
organisationId: organisation.id,
|
||||
},
|
||||
});
|
||||
|
||||
const [html, text] = await Promise.all([
|
||||
renderEmailWithI18N(template, {
|
||||
lang: settings.documentLanguage,
|
||||
branding,
|
||||
}),
|
||||
renderEmailWithI18N(template, {
|
||||
lang: settings.documentLanguage,
|
||||
branding,
|
||||
plainText: true,
|
||||
}),
|
||||
]);
|
||||
|
||||
const i18n = await getI18nInstance(settings.documentLanguage);
|
||||
|
||||
await mailer.sendMail({
|
||||
to: email,
|
||||
from: {
|
||||
name: FROM_NAME,
|
||||
address: FROM_ADDRESS,
|
||||
},
|
||||
subject: i18n._(msg`You have been invited to join ${organisation.name} on Documenso`),
|
||||
html,
|
||||
text,
|
||||
});
|
||||
};
|
||||
175
packages/lib/server-only/organisation/create-organisation.ts
Normal file
175
packages/lib/server-only/organisation/create-organisation.ts
Normal file
@ -0,0 +1,175 @@
|
||||
import type { Prisma } from '@prisma/client';
|
||||
import { OrganisationType } from '@prisma/client';
|
||||
import { OrganisationMemberRole } from '@prisma/client';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { ORGANISATION_INTERNAL_GROUPS } from '../../constants/organisations';
|
||||
import { AppErrorCode } from '../../errors/app-error';
|
||||
import { AppError } from '../../errors/app-error';
|
||||
import type { InternalClaim } from '../../types/subscription';
|
||||
import { INTERNAL_CLAIM_ID, internalClaims } from '../../types/subscription';
|
||||
import { generateDatabaseId, prefixedId } from '../../universal/id';
|
||||
import { generateDefaultOrganisationSettings } from '../../utils/organisations';
|
||||
import { createTeam } from '../team/create-team';
|
||||
|
||||
type CreateOrganisationOptions = {
|
||||
userId: number;
|
||||
name: string;
|
||||
type: OrganisationType;
|
||||
url?: string;
|
||||
customerId?: string;
|
||||
claim: InternalClaim;
|
||||
};
|
||||
|
||||
export const createOrganisation = async ({
|
||||
name,
|
||||
url,
|
||||
type,
|
||||
userId,
|
||||
customerId,
|
||||
claim,
|
||||
}: CreateOrganisationOptions) => {
|
||||
return await prisma.$transaction(async (tx) => {
|
||||
const organisationSetting = await tx.organisationGlobalSettings.create({
|
||||
data: {
|
||||
...generateDefaultOrganisationSettings(),
|
||||
id: generateDatabaseId('org_setting'),
|
||||
},
|
||||
});
|
||||
|
||||
const organisationClaim = await tx.organisationClaim.create({
|
||||
data: {
|
||||
id: generateDatabaseId('org_claim'),
|
||||
originalSubscriptionClaimId: claim.id,
|
||||
...createOrganisationClaimUpsertData(claim),
|
||||
},
|
||||
});
|
||||
|
||||
const orgIdAndUrl = prefixedId('org');
|
||||
|
||||
const organisation = await tx.organisation
|
||||
.create({
|
||||
data: {
|
||||
id: orgIdAndUrl,
|
||||
name,
|
||||
type,
|
||||
url: url || orgIdAndUrl,
|
||||
ownerUserId: userId,
|
||||
organisationGlobalSettingsId: organisationSetting.id,
|
||||
organisationClaimId: organisationClaim.id,
|
||||
groups: {
|
||||
create: ORGANISATION_INTERNAL_GROUPS.map((group) => ({
|
||||
...group,
|
||||
id: generateDatabaseId('org_group'),
|
||||
})),
|
||||
},
|
||||
customerId,
|
||||
},
|
||||
include: {
|
||||
groups: true,
|
||||
},
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err.code === 'P2002') {
|
||||
throw new AppError(AppErrorCode.ALREADY_EXISTS, {
|
||||
message: 'Organisation URL already exists',
|
||||
});
|
||||
}
|
||||
|
||||
throw err;
|
||||
});
|
||||
|
||||
const adminGroup = organisation.groups.find(
|
||||
(group) => group.organisationRole === OrganisationMemberRole.ADMIN,
|
||||
);
|
||||
|
||||
if (!adminGroup) {
|
||||
throw new AppError(AppErrorCode.UNKNOWN_ERROR, {
|
||||
message: 'Admin group not found',
|
||||
});
|
||||
}
|
||||
|
||||
await tx.organisationMember.create({
|
||||
data: {
|
||||
id: generateDatabaseId('member'),
|
||||
userId,
|
||||
organisationId: organisation.id,
|
||||
organisationGroupMembers: {
|
||||
create: {
|
||||
id: generateDatabaseId('group_member'),
|
||||
groupId: adminGroup.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return organisation;
|
||||
});
|
||||
};
|
||||
|
||||
type CreatePersonalOrganisationOptions = {
|
||||
userId: number;
|
||||
orgUrl?: string;
|
||||
throwErrorOnOrganisationCreationFailure?: boolean;
|
||||
inheritMembers?: boolean;
|
||||
type?: OrganisationType;
|
||||
};
|
||||
|
||||
export const createPersonalOrganisation = async ({
|
||||
userId,
|
||||
orgUrl,
|
||||
throwErrorOnOrganisationCreationFailure = false,
|
||||
inheritMembers = true,
|
||||
type = OrganisationType.PERSONAL,
|
||||
}: CreatePersonalOrganisationOptions) => {
|
||||
const organisation = await createOrganisation({
|
||||
name: 'Personal Organisation',
|
||||
userId,
|
||||
url: orgUrl,
|
||||
type,
|
||||
claim: internalClaims[INTERNAL_CLAIM_ID.FREE],
|
||||
}).catch((err) => {
|
||||
console.error(err);
|
||||
|
||||
if (throwErrorOnOrganisationCreationFailure) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
// Todo: (LOGS)
|
||||
});
|
||||
|
||||
if (organisation) {
|
||||
await createTeam({
|
||||
userId,
|
||||
teamName: 'Personal Team',
|
||||
teamUrl: prefixedId('personal'),
|
||||
organisationId: organisation.id,
|
||||
inheritMembers,
|
||||
}).catch((err) => {
|
||||
console.error(err);
|
||||
|
||||
// Todo: (LOGS)
|
||||
});
|
||||
}
|
||||
|
||||
return organisation;
|
||||
};
|
||||
|
||||
export const createOrganisationClaimUpsertData = (subscriptionClaim: InternalClaim) => {
|
||||
// Done like this to ensure type errors are thrown if items are added.
|
||||
const data: Omit<
|
||||
Prisma.SubscriptionClaimCreateInput,
|
||||
'id' | 'createdAt' | 'updatedAt' | 'locked' | 'name'
|
||||
> = {
|
||||
flags: {
|
||||
...subscriptionClaim.flags,
|
||||
},
|
||||
teamCount: subscriptionClaim.teamCount,
|
||||
memberCount: subscriptionClaim.memberCount,
|
||||
};
|
||||
|
||||
return {
|
||||
...data,
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,39 @@
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
|
||||
export const getOrganisationClaim = async ({ organisationId }: { organisationId: string }) => {
|
||||
const organisationClaim = await prisma.organisationClaim.findFirst({
|
||||
where: {
|
||||
organisation: {
|
||||
id: organisationId,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!organisationClaim) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND);
|
||||
}
|
||||
|
||||
return organisationClaim;
|
||||
};
|
||||
|
||||
export const getOrganisationClaimByTeamId = async ({ teamId }: { teamId: number }) => {
|
||||
const organisationClaim = await prisma.organisationClaim.findFirst({
|
||||
where: {
|
||||
organisation: {
|
||||
teams: {
|
||||
some: {
|
||||
id: teamId,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!organisationClaim) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND);
|
||||
}
|
||||
|
||||
return organisationClaim;
|
||||
};
|
||||
@ -1,3 +1,4 @@
|
||||
import fontkit from '@pdf-lib/fontkit';
|
||||
import type { PDFField, PDFWidgetAnnotation } from 'pdf-lib';
|
||||
import {
|
||||
PDFCheckBox,
|
||||
@ -13,6 +14,8 @@ import {
|
||||
translate,
|
||||
} from 'pdf-lib';
|
||||
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
||||
|
||||
export const removeOptionalContentGroups = (document: PDFDocument) => {
|
||||
const context = document.context;
|
||||
const catalog = context.lookup(context.trailerInfo.Root);
|
||||
@ -21,12 +24,20 @@ export const removeOptionalContentGroups = (document: PDFDocument) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const flattenForm = (document: PDFDocument) => {
|
||||
export const flattenForm = async (document: PDFDocument) => {
|
||||
removeOptionalContentGroups(document);
|
||||
|
||||
const form = document.getForm();
|
||||
|
||||
form.updateFieldAppearances();
|
||||
const fontNoto = await fetch(`${NEXT_PUBLIC_WEBAPP_URL()}/fonts/noto-sans.ttf`).then(
|
||||
async (res) => res.arrayBuffer(),
|
||||
);
|
||||
|
||||
document.registerFontkit(fontkit);
|
||||
|
||||
const font = await document.embedFont(fontNoto);
|
||||
|
||||
form.updateFieldAppearances(font);
|
||||
|
||||
for (const field of form.getFields()) {
|
||||
for (const widget of field.acroField.getWidgets()) {
|
||||
|
||||
@ -1,8 +1,15 @@
|
||||
// https://github.com/Hopding/pdf-lib/issues/20#issuecomment-412852821
|
||||
import fontkit from '@pdf-lib/fontkit';
|
||||
import { FieldType } from '@prisma/client';
|
||||
import type { PDFDocument, PDFFont } from 'pdf-lib';
|
||||
import { RotationTypes, TextAlignment, degrees, radiansToDegrees, rgb } from 'pdf-lib';
|
||||
import type { PDFDocument, PDFFont, PDFTextField } from 'pdf-lib';
|
||||
import {
|
||||
RotationTypes,
|
||||
TextAlignment,
|
||||
degrees,
|
||||
radiansToDegrees,
|
||||
rgb,
|
||||
setFontAndSize,
|
||||
} from 'pdf-lib';
|
||||
import { P, match } from 'ts-pattern';
|
||||
|
||||
import {
|
||||
@ -442,6 +449,10 @@ export const insertFieldInPDF = async (pdf: PDFDocument, field: FieldWithSignatu
|
||||
adjustedFieldY = adjustedPosition.yPos;
|
||||
}
|
||||
|
||||
// Set properties for the text field
|
||||
setTextFieldFontSize(textField, font, fontSize);
|
||||
textField.setText(textToInsert);
|
||||
|
||||
// Set the position and size of the text field
|
||||
textField.addToPage(page, {
|
||||
x: adjustedFieldX,
|
||||
@ -450,6 +461,8 @@ export const insertFieldInPDF = async (pdf: PDFDocument, field: FieldWithSignatu
|
||||
height: adjustedFieldHeight,
|
||||
rotate: degrees(pageRotationInDegrees),
|
||||
|
||||
font,
|
||||
|
||||
// Hide borders.
|
||||
borderWidth: 0,
|
||||
borderColor: undefined,
|
||||
@ -457,10 +470,6 @@ export const insertFieldInPDF = async (pdf: PDFDocument, field: FieldWithSignatu
|
||||
|
||||
...(isDebugMode ? { borderWidth: 1, borderColor: rgb(0, 0, 1) } : {}),
|
||||
});
|
||||
|
||||
// Set properties for the text field
|
||||
textField.setFontSize(fontSize);
|
||||
textField.setText(textToInsert);
|
||||
});
|
||||
|
||||
return pdf;
|
||||
@ -629,3 +638,21 @@ function breakLongString(text: string, maxWidth: number, font: PDFFont, fontSize
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
const setTextFieldFontSize = (textField: PDFTextField, font: PDFFont, fontSize: number) => {
|
||||
textField.defaultUpdateAppearances(font);
|
||||
textField.updateAppearances(font);
|
||||
|
||||
try {
|
||||
textField.setFontSize(fontSize);
|
||||
} catch (err) {
|
||||
let da = textField.acroField.getDefaultAppearance() ?? '';
|
||||
|
||||
da += `\n ${setFontAndSize(font.name, fontSize)}`;
|
||||
|
||||
textField.acroField.setDefaultAppearance(da);
|
||||
}
|
||||
|
||||
textField.defaultUpdateAppearances(font);
|
||||
textField.updateAppearances(font);
|
||||
};
|
||||
|
||||
@ -11,7 +11,7 @@ export const normalizePdf = async (pdf: Buffer) => {
|
||||
}
|
||||
|
||||
removeOptionalContentGroups(pdfDoc);
|
||||
flattenForm(pdfDoc);
|
||||
await flattenForm(pdfDoc);
|
||||
flattenAnnotations(pdfDoc);
|
||||
|
||||
return Buffer.from(await pdfDoc.save());
|
||||
|
||||
@ -1,15 +1,8 @@
|
||||
import type { Template, TemplateDirectLink } from '@prisma/client';
|
||||
import {
|
||||
SubscriptionStatus,
|
||||
type TeamProfile,
|
||||
TemplateType,
|
||||
type UserProfile,
|
||||
} from '@prisma/client';
|
||||
import { type TeamProfile, TemplateType } from '@prisma/client';
|
||||
|
||||
import { getCommunityPlanPriceIds } from '@documenso/ee/server-only/stripe/get-community-plan-prices';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { IS_BILLING_ENABLED } from '../../constants/app';
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
|
||||
export type GetPublicProfileByUrlOptions = {
|
||||
@ -23,7 +16,7 @@ type PublicDirectLinkTemplate = Template & {
|
||||
};
|
||||
};
|
||||
|
||||
type BaseResponse = {
|
||||
type GetPublicProfileByUrlResponse = {
|
||||
url: string;
|
||||
name: string;
|
||||
avatarImageId?: string | null;
|
||||
@ -32,155 +25,56 @@ type BaseResponse = {
|
||||
since: Date;
|
||||
};
|
||||
templates: PublicDirectLinkTemplate[];
|
||||
profile: TeamProfile;
|
||||
};
|
||||
|
||||
type GetPublicProfileByUrlResponse = BaseResponse &
|
||||
(
|
||||
| {
|
||||
type: 'User';
|
||||
profile: UserProfile;
|
||||
}
|
||||
| {
|
||||
type: 'Team';
|
||||
profile: TeamProfile;
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Get the user or team public profile by URL.
|
||||
*/
|
||||
export const getPublicProfileByUrl = async ({
|
||||
profileUrl,
|
||||
}: GetPublicProfileByUrlOptions): Promise<GetPublicProfileByUrlResponse> => {
|
||||
const [user, team] = await Promise.all([
|
||||
prisma.user.findFirst({
|
||||
where: {
|
||||
url: profileUrl,
|
||||
profile: {
|
||||
enabled: true,
|
||||
const team = await prisma.team.findFirst({
|
||||
where: {
|
||||
url: profileUrl,
|
||||
profile: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
include: {
|
||||
profile: true,
|
||||
templates: {
|
||||
where: {
|
||||
directLink: {
|
||||
enabled: true,
|
||||
},
|
||||
type: TemplateType.PUBLIC,
|
||||
},
|
||||
include: {
|
||||
directLink: true,
|
||||
},
|
||||
},
|
||||
include: {
|
||||
profile: true,
|
||||
templates: {
|
||||
where: {
|
||||
directLink: {
|
||||
enabled: true,
|
||||
},
|
||||
type: TemplateType.PUBLIC,
|
||||
},
|
||||
include: {
|
||||
directLink: true,
|
||||
},
|
||||
},
|
||||
// Subscriptions and teamMembers are used to calculate the badges.
|
||||
subscriptions: {
|
||||
where: {
|
||||
status: SubscriptionStatus.ACTIVE,
|
||||
},
|
||||
},
|
||||
teamMembers: {
|
||||
select: {
|
||||
createdAt: true,
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'asc',
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
prisma.team.findFirst({
|
||||
where: {
|
||||
url: profileUrl,
|
||||
profile: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
include: {
|
||||
profile: true,
|
||||
templates: {
|
||||
where: {
|
||||
directLink: {
|
||||
enabled: true,
|
||||
},
|
||||
type: TemplateType.PUBLIC,
|
||||
},
|
||||
include: {
|
||||
directLink: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
]);
|
||||
},
|
||||
});
|
||||
|
||||
// Log as critical error.
|
||||
if (user?.profile && team?.profile) {
|
||||
console.error('Profile URL is ambiguous', { profileUrl, userId: user.id, teamId: team.id });
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message: 'Profile URL is ambiguous',
|
||||
if (!team?.profile?.enabled) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Profile not found',
|
||||
});
|
||||
}
|
||||
|
||||
if (user?.profile?.enabled) {
|
||||
let badge: BaseResponse['badge'] = undefined;
|
||||
|
||||
if (user.teamMembers[0]) {
|
||||
badge = {
|
||||
type: 'Premium',
|
||||
since: user.teamMembers[0]['createdAt'],
|
||||
};
|
||||
}
|
||||
|
||||
if (IS_BILLING_ENABLED()) {
|
||||
const earlyAdopterPriceIds = await getCommunityPlanPriceIds();
|
||||
|
||||
const activeEarlyAdopterSub = user.subscriptions.find(
|
||||
(subscription) =>
|
||||
subscription.status === SubscriptionStatus.ACTIVE &&
|
||||
earlyAdopterPriceIds.includes(subscription.priceId),
|
||||
);
|
||||
|
||||
if (activeEarlyAdopterSub) {
|
||||
badge = {
|
||||
type: 'EarlySupporter',
|
||||
since: activeEarlyAdopterSub.createdAt,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'User',
|
||||
badge,
|
||||
profile: user.profile,
|
||||
url: profileUrl,
|
||||
avatarImageId: user.avatarImageId,
|
||||
name: user.name || '',
|
||||
templates: user.templates.filter(
|
||||
(template): template is PublicDirectLinkTemplate =>
|
||||
template.directLink?.enabled === true && template.type === TemplateType.PUBLIC,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
if (team?.profile?.enabled) {
|
||||
return {
|
||||
type: 'Team',
|
||||
badge: {
|
||||
type: 'Premium',
|
||||
since: team.createdAt,
|
||||
},
|
||||
profile: team.profile,
|
||||
url: profileUrl,
|
||||
avatarImageId: team.avatarImageId,
|
||||
name: team.name || '',
|
||||
templates: team.templates.filter(
|
||||
(template): template is PublicDirectLinkTemplate =>
|
||||
template.directLink?.enabled === true && template.type === TemplateType.PUBLIC,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Profile not found',
|
||||
});
|
||||
return {
|
||||
badge: {
|
||||
type: 'Premium',
|
||||
since: team.createdAt,
|
||||
},
|
||||
profile: team.profile,
|
||||
url: profileUrl,
|
||||
avatarImageId: team.avatarImageId,
|
||||
name: team.name || '',
|
||||
templates: team.templates.filter(
|
||||
(template): template is PublicDirectLinkTemplate =>
|
||||
template.directLink?.enabled === true && template.type === TemplateType.PUBLIC,
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
@ -2,51 +2,49 @@ import sharp from 'sharp';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP } from '../../constants/organisations';
|
||||
import { TEAM_MEMBER_ROLE_PERMISSIONS_MAP } from '../../constants/teams';
|
||||
import { AppError } from '../../errors/app-error';
|
||||
import type { ApiRequestMetadata } from '../../universal/extract-request-metadata';
|
||||
import { buildOrganisationWhereQuery } from '../../utils/organisations';
|
||||
import { buildTeamWhereQuery } from '../../utils/teams';
|
||||
|
||||
export type SetAvatarImageOptions = {
|
||||
userId: number;
|
||||
teamId?: number | null;
|
||||
target:
|
||||
| {
|
||||
type: 'user';
|
||||
}
|
||||
| {
|
||||
type: 'team';
|
||||
teamId: number;
|
||||
}
|
||||
| {
|
||||
type: 'organisation';
|
||||
organisationId: string;
|
||||
};
|
||||
bytes?: string | null;
|
||||
requestMetadata: ApiRequestMetadata;
|
||||
};
|
||||
|
||||
/**
|
||||
* Pretty nasty but will do for now.
|
||||
*/
|
||||
export const setAvatarImage = async ({
|
||||
userId,
|
||||
teamId,
|
||||
target,
|
||||
bytes,
|
||||
requestMetadata,
|
||||
}: SetAvatarImageOptions) => {
|
||||
let oldAvatarImageId: string | null = null;
|
||||
|
||||
const user = await prisma.user.findUnique({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
include: {
|
||||
avatarImage: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'User not found',
|
||||
});
|
||||
}
|
||||
|
||||
oldAvatarImageId = user.avatarImageId;
|
||||
|
||||
if (teamId) {
|
||||
if (target.type === 'team') {
|
||||
const team = await prisma.team.findFirst({
|
||||
where: {
|
||||
id: teamId,
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
where: buildTeamWhereQuery({
|
||||
teamId: target.teamId,
|
||||
userId,
|
||||
roles: TEAM_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_TEAM'],
|
||||
}),
|
||||
});
|
||||
|
||||
if (!team) {
|
||||
@ -56,6 +54,39 @@ export const setAvatarImage = async ({
|
||||
}
|
||||
|
||||
oldAvatarImageId = team.avatarImageId;
|
||||
} else if (target.type === 'organisation') {
|
||||
const organisation = await prisma.organisation.findFirst({
|
||||
where: buildOrganisationWhereQuery({
|
||||
organisationId: target.organisationId,
|
||||
userId,
|
||||
roles: ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_ORGANISATION'],
|
||||
}),
|
||||
});
|
||||
|
||||
if (!organisation) {
|
||||
throw new AppError('ORGANISATION_NOT_FOUND', {
|
||||
statusCode: 404,
|
||||
});
|
||||
}
|
||||
|
||||
oldAvatarImageId = organisation.avatarImageId;
|
||||
} else {
|
||||
const user = await prisma.user.findUnique({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
include: {
|
||||
avatarImage: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new AppError('USER_NOT_FOUND', {
|
||||
statusCode: 404,
|
||||
});
|
||||
}
|
||||
|
||||
oldAvatarImageId = user.avatarImageId;
|
||||
}
|
||||
|
||||
if (oldAvatarImageId) {
|
||||
@ -83,17 +114,26 @@ export const setAvatarImage = async ({
|
||||
newAvatarImageId = avatarImage.id;
|
||||
}
|
||||
|
||||
if (teamId) {
|
||||
// TODO: Audit Logs
|
||||
|
||||
if (target.type === 'team') {
|
||||
await prisma.team.update({
|
||||
where: {
|
||||
id: teamId,
|
||||
id: target.teamId,
|
||||
},
|
||||
data: {
|
||||
avatarImageId: newAvatarImageId,
|
||||
},
|
||||
});
|
||||
} else if (target.type === 'organisation') {
|
||||
await prisma.organisation.update({
|
||||
where: {
|
||||
id: target.organisationId,
|
||||
},
|
||||
data: {
|
||||
avatarImageId: newAvatarImageId,
|
||||
},
|
||||
});
|
||||
|
||||
// TODO: Audit Logs
|
||||
} else {
|
||||
await prisma.user.update({
|
||||
where: {
|
||||
@ -103,8 +143,6 @@ export const setAvatarImage = async ({
|
||||
avatarImageId: newAvatarImageId,
|
||||
},
|
||||
});
|
||||
|
||||
// TODO: Audit Logs
|
||||
}
|
||||
|
||||
return newAvatarImageId;
|
||||
|
||||
@ -1,13 +1,14 @@
|
||||
import { TeamMemberRole } from '@prisma/client';
|
||||
import type { Duration } from 'luxon';
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { TEAM_MEMBER_ROLE_PERMISSIONS_MAP } from '../../constants/teams';
|
||||
// temporary choice for testing only
|
||||
import * as timeConstants from '../../constants/time';
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { alphaid } from '../../universal/id';
|
||||
import { buildTeamWhereQuery } from '../../utils/teams';
|
||||
import { hashString } from '../auth/hash';
|
||||
|
||||
type TimeConstants = typeof timeConstants & {
|
||||
@ -16,7 +17,7 @@ type TimeConstants = typeof timeConstants & {
|
||||
|
||||
type CreateApiTokenInput = {
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
teamId: number;
|
||||
tokenName: string;
|
||||
expiresIn: string | null;
|
||||
};
|
||||
@ -33,20 +34,18 @@ export const createApiToken = async ({
|
||||
|
||||
const timeConstantsRecords: TimeConstants = timeConstants;
|
||||
|
||||
if (teamId) {
|
||||
const member = await prisma.teamMember.findFirst({
|
||||
where: {
|
||||
userId,
|
||||
teamId,
|
||||
role: TeamMemberRole.ADMIN,
|
||||
},
|
||||
});
|
||||
const team = await prisma.team.findFirst({
|
||||
where: buildTeamWhereQuery({
|
||||
teamId,
|
||||
userId,
|
||||
roles: TEAM_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_TEAM'],
|
||||
}),
|
||||
});
|
||||
|
||||
if (!member) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'You do not have permission to create a token for this team',
|
||||
});
|
||||
}
|
||||
if (!team) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'You do not have permission to create a token for this team',
|
||||
});
|
||||
}
|
||||
|
||||
const storedToken = await prisma.apiToken.create({
|
||||
|
||||
@ -1,32 +1,34 @@
|
||||
import { TeamMemberRole } from '@prisma/client';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { TEAM_MEMBER_ROLE_PERMISSIONS_MAP } from '../../constants/teams';
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { buildTeamWhereQuery } from '../../utils/teams';
|
||||
|
||||
export type DeleteTokenByIdOptions = {
|
||||
id: number;
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
teamId: number;
|
||||
};
|
||||
|
||||
export const deleteTokenById = async ({ id, userId, teamId }: DeleteTokenByIdOptions) => {
|
||||
if (teamId) {
|
||||
const member = await prisma.teamMember.findFirst({
|
||||
where: {
|
||||
userId,
|
||||
teamId,
|
||||
role: TeamMemberRole.ADMIN,
|
||||
},
|
||||
});
|
||||
const team = await prisma.team.findFirst({
|
||||
where: buildTeamWhereQuery({
|
||||
teamId,
|
||||
userId,
|
||||
roles: TEAM_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_TEAM'],
|
||||
}),
|
||||
});
|
||||
|
||||
if (!member) {
|
||||
throw new Error('You do not have permission to delete this token');
|
||||
}
|
||||
if (!team) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'You do not have permission to delete this token',
|
||||
});
|
||||
}
|
||||
|
||||
return await prisma.apiToken.delete({
|
||||
where: {
|
||||
id,
|
||||
teamId: teamId ?? null,
|
||||
teamId,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@ -1,15 +0,0 @@
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
export type GetApiTokenByIdOptions = {
|
||||
id: number;
|
||||
userId: number;
|
||||
};
|
||||
|
||||
export const getApiTokenById = async ({ id, userId }: GetApiTokenByIdOptions) => {
|
||||
return await prisma.apiToken.findFirstOrThrow({
|
||||
where: {
|
||||
id,
|
||||
userId,
|
||||
},
|
||||
});
|
||||
};
|
||||
@ -10,8 +10,30 @@ export const getApiTokenByToken = async ({ token }: { token: string }) => {
|
||||
token: hashedToken,
|
||||
},
|
||||
include: {
|
||||
team: true,
|
||||
user: true,
|
||||
team: {
|
||||
include: {
|
||||
organisation: {
|
||||
include: {
|
||||
owner: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
disabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
disabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@ -25,11 +47,7 @@ export const getApiTokenByToken = async ({ token }: { token: string }) => {
|
||||
|
||||
// Handle a silly choice from many moons ago
|
||||
if (apiToken.team && !apiToken.user) {
|
||||
apiToken.user = await prisma.user.findFirst({
|
||||
where: {
|
||||
id: apiToken.team.ownerUserId,
|
||||
},
|
||||
});
|
||||
apiToken.user = apiToken.team.organisation.owner;
|
||||
}
|
||||
|
||||
const { user } = apiToken;
|
||||
|
||||
@ -1,31 +1,21 @@
|
||||
import { TeamMemberRole } from '@prisma/client';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { TEAM_MEMBER_ROLE_PERMISSIONS_MAP } from '../../constants/teams';
|
||||
import { buildTeamWhereQuery } from '../../utils/teams';
|
||||
|
||||
export type GetApiTokensOptions = {
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
teamId: number;
|
||||
};
|
||||
|
||||
export const getApiTokens = async ({ userId, teamId }: GetApiTokensOptions) => {
|
||||
return await prisma.apiToken.findMany({
|
||||
where: {
|
||||
...(teamId
|
||||
? {
|
||||
team: {
|
||||
id: teamId,
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
role: TeamMemberRole.ADMIN,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
: {
|
||||
userId,
|
||||
teamId: null,
|
||||
}),
|
||||
team: buildTeamWhereQuery({
|
||||
teamId,
|
||||
userId,
|
||||
roles: TEAM_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_TEAM'],
|
||||
}),
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user