feat: add global settings for teams (#1391)

## Description

This PR introduces global settings for teams. At the moment, it allows
team admins to configure the following:
* The default visibility of the documents uploaded to the team account
* Whether to include the document owner (sender) details when sending
emails to the recipients.

### Include Sender Details

If the Sender Details setting is enabled, the emails sent by the team
will include the sender's name:

> "Example User" on behalf of "Example Team" has invited you to sign
"document.pdf"

Otherwise, the email will say:

> "Example Team" has invited you to sign "document.pdf"

### Default Document Visibility

This new option allows users to set the default visibility for the
documents uploaded to the team account. It can have the following
values:
* Everyone
* Manager and above
* Admins only

If the default document visibility isn't set, the document will be set
to the role of the user who created the document:
* If a user with the "User" role creates a document, the document's
visibility is set to "Everyone".
* Manager role -> "Manager and above"
* Admin role -> "Admins only"

Otherwise, if there is a default document visibility value, it uses that
value.

#### Gotcha

To avoid issues, the `document owner` and the `recipient` can access the
document irrespective of their role. For example:
* If a team member with the role "Member" uploads a document and the
default document visibility is "Admins", only the document owner and
admins can access the document.
  * Similar to the other scenarios.

* If an admin uploads a document and the default document visibility is
"Admins", the recipient can access the document.

* The admins have access to all the documents.
* Managers have access to documents with the visibility set to
"Everyone" and "Manager and above"
* Members have access only to the documents with the visibility set to
"Everyone".

## Testing Performed

Tested it locally.
This commit is contained in:
Catalin Pit
2024-11-08 13:50:49 +02:00
committed by GitHub
parent f6bcf921d5
commit 23a0537648
99 changed files with 4372 additions and 1037 deletions

View File

@ -169,7 +169,7 @@ export class LocalJobProvider extends BaseJobProvider {
},
});
} catch (error) {
console.error(`[JOBS]: Job ${options.name} failed`, error);
console.log(`[JOBS]: Job ${options.name} failed`, error);
const taskHasExceededRetries = error instanceof BackgroundTaskExceededRetriesError;
const jobHasExceededRetries =
@ -295,7 +295,7 @@ export class LocalJobProvider extends BaseJobProvider {
});
return result;
} catch {
} catch (err) {
task = await prisma.backgroundJobTask.update({
where: {
id: task.id,
@ -309,6 +309,8 @@ export class LocalJobProvider extends BaseJobProvider {
},
});
console.log(`[JOBS:${task.id}] Task failed`, err);
throw new BackgroundTaskFailedError('Task failed');
}
},

View File

@ -26,6 +26,7 @@ import { ZRequestMetadataSchema } from '../../../universal/extract-request-metad
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 JobDefinition } from '../../client/_internal/job';
const SEND_SIGNING_EMAIL_JOB_DEFINITION_ID = 'send.signing.requested.email';
@ -65,6 +66,7 @@ export const SEND_SIGNING_EMAIL_JOB_DEFINITION = {
select: {
teamEmail: true,
name: true,
teamGlobalSettings: true,
},
},
},
@ -126,11 +128,15 @@ export const SEND_SIGNING_EMAIL_JOB_DEFINITION = {
if (isTeamDocument && team) {
emailSubject = i18n._(msg`${team.name} invited you to ${recipientActionVerb} a document`);
emailMessage =
customEmail?.message ||
i18n._(
msg`${user.name} on behalf of ${team.name} has invited you to ${recipientActionVerb} the document "${document.title}".`,
emailMessage = customEmail?.message ?? '';
if (!emailMessage) {
emailMessage = i18n._(
team.teamGlobalSettings?.includeSenderDetails
? msg`${user.name} 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}".`,
);
}
}
const customEmailTemplate = {
@ -154,13 +160,19 @@ export const SEND_SIGNING_EMAIL_JOB_DEFINITION = {
isTeamInvite: isTeamDocument,
teamName: team?.name,
teamEmail: team?.teamEmail?.email,
includeSenderDetails: team?.teamGlobalSettings?.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 }),
renderEmailWithI18N(template, { lang: documentMeta?.language, branding }),
renderEmailWithI18N(template, {
lang: documentMeta?.language,
branding,
plainText: true,
}),
]);

View File

@ -1,5 +1,7 @@
import { z } from 'zod';
import { DocumentVisibility } from '@documenso/prisma/client';
import { sendTeamDeleteEmail } from '../../../server-only/team/delete-team';
import type { JobDefinition } from '../../client/_internal/job';
@ -10,6 +12,19 @@ const SEND_TEAM_DELETED_EMAIL_JOB_DEFINITION_SCHEMA = z.object({
name: z.string(),
url: z.string(),
ownerUserId: z.number(),
teamGlobalSettings: z
.object({
documentVisibility: z.nativeEnum(DocumentVisibility),
documentLanguage: z.string(),
includeSenderDetails: z.boolean(),
brandingEnabled: z.boolean(),
brandingLogo: z.string(),
brandingUrl: z.string(),
brandingCompanyDetails: z.string(),
brandingHidePoweredBy: z.boolean(),
teamId: z.number(),
})
.nullish(),
}),
members: z.array(
z.object({
@ -35,8 +50,7 @@ export const SEND_TEAM_DELETED_EMAIL_JOB_DEFINITION = {
await io.runTask(`send-team-deleted-email--${team.url}_${member.id}`, async () => {
await sendTeamDeleteEmail({
email: member.email,
teamName: team.name,
teamUrl: team.url,
team,
isOwner: member.id === team.ownerUserId,
});
});

View File

@ -1,3 +1,5 @@
import { createElement } from 'react';
import { msg } from '@lingui/macro';
import { z } from 'zod';
@ -10,6 +12,7 @@ import { getI18nInstance } from '../../../client-only/providers/i18n.server';
import { WEBAPP_BASE_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 { JobDefinition } from '../../client/_internal/job';
const SEND_TEAM_MEMBER_JOINED_EMAIL_JOB_DEFINITION_ID = 'send.team-member-joined.email';
@ -43,6 +46,7 @@ export const SEND_TEAM_MEMBER_JOINED_EMAIL_JOB_DEFINITION = {
user: true,
},
},
teamGlobalSettings: true,
},
});
@ -64,7 +68,7 @@ export const SEND_TEAM_MEMBER_JOINED_EMAIL_JOB_DEFINITION = {
await io.runTask(
`send-team-member-joined-email--${invitedMember.id}_${member.id}`,
async () => {
const emailContent = TeamJoinEmailTemplate({
const emailContent = createElement(TeamJoinEmailTemplate, {
assetBaseUrl: WEBAPP_BASE_URL,
baseUrl: WEBAPP_BASE_URL,
memberName: invitedMember.user.name || '',
@ -73,13 +77,26 @@ export const SEND_TEAM_MEMBER_JOINED_EMAIL_JOB_DEFINITION = {
teamUrl: team.url,
});
const branding = team.teamGlobalSettings
? teamGlobalSettingsToBranding(team.teamGlobalSettings)
: undefined;
const lang = team.teamGlobalSettings?.documentLanguage;
// !: Replace with the actual language of the recipient later
const [html, text] = await Promise.all([
renderEmailWithI18N(emailContent),
renderEmailWithI18N(emailContent, { plainText: true }),
renderEmailWithI18N(emailContent, {
lang,
branding,
}),
renderEmailWithI18N(emailContent, {
lang,
branding,
plainText: true,
}),
]);
const i18n = await getI18nInstance();
const i18n = await getI18nInstance(lang);
await mailer.sendMail({
to: member.user.email,

View File

@ -1,3 +1,5 @@
import { createElement } from 'react';
import { msg } from '@lingui/macro';
import { z } from 'zod';
@ -10,6 +12,7 @@ import { getI18nInstance } from '../../../client-only/providers/i18n.server';
import { WEBAPP_BASE_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 { JobDefinition } from '../../client/_internal/job';
const SEND_TEAM_MEMBER_LEFT_EMAIL_JOB_DEFINITION_ID = 'send.team-member-left.email';
@ -43,6 +46,7 @@ export const SEND_TEAM_MEMBER_LEFT_EMAIL_JOB_DEFINITION = {
user: true,
},
},
teamGlobalSettings: true,
},
});
@ -54,7 +58,7 @@ export const SEND_TEAM_MEMBER_LEFT_EMAIL_JOB_DEFINITION = {
for (const member of team.members) {
await io.runTask(`send-team-member-left-email--${oldMember.id}_${member.id}`, async () => {
const emailContent = TeamJoinEmailTemplate({
const emailContent = createElement(TeamJoinEmailTemplate, {
assetBaseUrl: WEBAPP_BASE_URL,
baseUrl: WEBAPP_BASE_URL,
memberName: oldMember.name || '',
@ -63,12 +67,25 @@ export const SEND_TEAM_MEMBER_LEFT_EMAIL_JOB_DEFINITION = {
teamUrl: team.url,
});
const branding = team.teamGlobalSettings
? teamGlobalSettingsToBranding(team.teamGlobalSettings)
: undefined;
const lang = team.teamGlobalSettings?.documentLanguage;
const [html, text] = await Promise.all([
renderEmailWithI18N(emailContent),
renderEmailWithI18N(emailContent, { plainText: true }),
renderEmailWithI18N(emailContent, {
lang,
branding,
}),
renderEmailWithI18N(emailContent, {
lang,
branding,
plainText: true,
}),
]);
const i18n = await getI18nInstance();
const i18n = await getI18nInstance(lang);
await mailer.sendMail({
to: member.user.email,