mirror of
https://github.com/documenso/documenso.git
synced 2026-07-12 14:05:20 +10:00
0b86ece1d5
Platform-plan organisations and teams can now customise non-embed signing pages with six brand colour tokens, a border-radius, and a free-text custom CSS block (up to 256 KB). - Stored on OrganisationGlobalSettings / TeamGlobalSettings; teams inherit from the org via brandingEnabled === null. - CSS is sanitised on save (PostCSS) so we can inline it at SSR with no per-render parsing. - Rendered via a nonce'd <style> scoped under .documenso-branded, using native CSS nesting so user selectors don't need scoping. - Gated on the existing embedSigningWhiteLabel claim (or self-hosted) — reuses the embed white-label decision.
203 lines
6.8 KiB
TypeScript
203 lines
6.8 KiB
TypeScript
import { ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP } from '@documenso/lib/constants/organisations';
|
|
import { TEAM_MEMBER_ROLE_PERMISSIONS_MAP } from '@documenso/lib/constants/teams';
|
|
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
|
import { normalizeBrandingColors } from '@documenso/lib/utils/normalize-branding-colors';
|
|
import { buildOrganisationWhereQuery } from '@documenso/lib/utils/organisations';
|
|
import { type SanitizeBrandingCssWarning, sanitizeBrandingCss } from '@documenso/lib/utils/sanitize-branding-css';
|
|
import { buildTeamWhereQuery } from '@documenso/lib/utils/teams';
|
|
import { prisma } from '@documenso/prisma';
|
|
import { OrganisationType, Prisma } from '@prisma/client';
|
|
|
|
import { authenticatedProcedure } from '../trpc';
|
|
import { ZUpdateTeamSettingsRequestSchema, ZUpdateTeamSettingsResponseSchema } from './update-team-settings.types';
|
|
|
|
export const updateTeamSettingsRoute = authenticatedProcedure
|
|
.input(ZUpdateTeamSettingsRequestSchema)
|
|
.output(ZUpdateTeamSettingsResponseSchema)
|
|
.mutation(async ({ ctx, input }) => {
|
|
const { user } = ctx;
|
|
const { teamId, data } = input;
|
|
|
|
ctx.logger.info({
|
|
input: {
|
|
teamId,
|
|
},
|
|
});
|
|
|
|
const {
|
|
// Document related settings.
|
|
documentVisibility,
|
|
documentLanguage,
|
|
documentTimezone,
|
|
documentDateFormat,
|
|
includeSenderDetails,
|
|
includeSigningCertificate,
|
|
includeAuditLog,
|
|
typedSignatureEnabled,
|
|
uploadSignatureEnabled,
|
|
drawSignatureEnabled,
|
|
delegateDocumentOwnership,
|
|
envelopeExpirationPeriod,
|
|
reminderSettings,
|
|
|
|
// Branding related settings.
|
|
brandingEnabled,
|
|
brandingLogo,
|
|
brandingUrl,
|
|
brandingCompanyDetails,
|
|
brandingColors,
|
|
brandingCss,
|
|
|
|
// Email related settings.
|
|
emailId,
|
|
emailReplyTo,
|
|
// emailReplyToName,
|
|
emailDocumentSettings,
|
|
|
|
// Default recipients settings.
|
|
defaultRecipients,
|
|
// AI features settings.
|
|
aiFeaturesEnabled,
|
|
} = data;
|
|
|
|
if (Object.values(data).length === 0) {
|
|
throw new AppError(AppErrorCode.INVALID_BODY, {
|
|
message: 'No settings to update',
|
|
});
|
|
}
|
|
|
|
// Signatures will only be inherited if all are NULL.
|
|
if (typedSignatureEnabled === false && uploadSignatureEnabled === false && drawSignatureEnabled === false) {
|
|
throw new AppError(AppErrorCode.INVALID_BODY, {
|
|
message: 'At least one signature type must be enabled',
|
|
});
|
|
}
|
|
|
|
const team = await prisma.team.findFirst({
|
|
where: buildTeamWhereQuery({
|
|
teamId,
|
|
userId: user.id,
|
|
roles: TEAM_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_TEAM'],
|
|
}),
|
|
});
|
|
|
|
if (!team) {
|
|
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
|
message: 'You do not have permission to update this team.',
|
|
});
|
|
}
|
|
|
|
// Validate that the email ID belongs to the organisation.
|
|
if (emailId) {
|
|
const email = await prisma.organisationEmail.findFirst({
|
|
where: {
|
|
id: emailId,
|
|
organisationId: team.organisationId,
|
|
},
|
|
});
|
|
|
|
if (!email) {
|
|
throw new AppError(AppErrorCode.NOT_FOUND, {
|
|
message: 'Email not found',
|
|
});
|
|
}
|
|
}
|
|
|
|
const organisation = await prisma.organisation.findFirst({
|
|
where: buildOrganisationWhereQuery({
|
|
organisationId: team.organisationId,
|
|
userId: user.id,
|
|
roles: ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_ORGANISATION'],
|
|
}),
|
|
select: {
|
|
type: true,
|
|
organisationGlobalSettings: {
|
|
select: {
|
|
includeSenderDetails: true,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
const isPersonalOrganisation = organisation?.type === OrganisationType.PERSONAL;
|
|
const currentIncludeSenderDetails = organisation?.organisationGlobalSettings.includeSenderDetails;
|
|
|
|
const isChangingIncludeSenderDetails =
|
|
includeSenderDetails !== undefined && includeSenderDetails !== currentIncludeSenderDetails;
|
|
|
|
if (isPersonalOrganisation && isChangingIncludeSenderDetails) {
|
|
throw new AppError(AppErrorCode.INVALID_BODY, {
|
|
message: 'Personal teams cannot update the sender details',
|
|
});
|
|
}
|
|
|
|
// Sanitize custom branding CSS at write time. `null` means inherit-from-org
|
|
// for teams, so only run the sanitiser when an explicit string is provided.
|
|
// An empty string after sanitisation is collapsed to `null` so the team
|
|
// row inherits rather than persisting an empty override.
|
|
let cssWarnings: SanitizeBrandingCssWarning[] | undefined;
|
|
let sanitizedBrandingCss: string | null | undefined;
|
|
|
|
if (brandingCss === null) {
|
|
sanitizedBrandingCss = null;
|
|
} else if (typeof brandingCss === 'string') {
|
|
const result = sanitizeBrandingCss(brandingCss);
|
|
sanitizedBrandingCss = result.css.trim() === '' ? null : result.css;
|
|
cssWarnings = result.warnings;
|
|
}
|
|
|
|
// Strip empty-string colour values; collapse to `null` when the payload
|
|
// contains no overrides. For teams this matters because brandingEnabled
|
|
// = null inherits from the org — leaving `{}` here would persist a real
|
|
// override of nothing once a team toggles brandingEnabled = true.
|
|
const normalizedBrandingColors = normalizeBrandingColors(brandingColors);
|
|
|
|
await prisma.team.update({
|
|
where: {
|
|
id: teamId,
|
|
},
|
|
data: {
|
|
teamGlobalSettings: {
|
|
update: {
|
|
// Document related settings.
|
|
documentVisibility,
|
|
documentLanguage,
|
|
documentTimezone,
|
|
documentDateFormat,
|
|
includeSenderDetails,
|
|
includeSigningCertificate,
|
|
includeAuditLog,
|
|
typedSignatureEnabled,
|
|
uploadSignatureEnabled,
|
|
drawSignatureEnabled,
|
|
delegateDocumentOwnership,
|
|
envelopeExpirationPeriod: envelopeExpirationPeriod === null ? Prisma.DbNull : envelopeExpirationPeriod,
|
|
reminderSettings: reminderSettings === null ? Prisma.DbNull : reminderSettings,
|
|
|
|
// Branding related settings.
|
|
brandingEnabled,
|
|
brandingLogo,
|
|
brandingUrl,
|
|
brandingCompanyDetails,
|
|
brandingColors: normalizedBrandingColors === null ? Prisma.DbNull : normalizedBrandingColors,
|
|
brandingCss: sanitizedBrandingCss,
|
|
|
|
// Email related settings.
|
|
emailId,
|
|
emailReplyTo,
|
|
// emailReplyToName,
|
|
emailDocumentSettings: emailDocumentSettings === null ? Prisma.DbNull : emailDocumentSettings,
|
|
defaultRecipients: defaultRecipients === null ? Prisma.DbNull : defaultRecipients,
|
|
|
|
// AI features settings.
|
|
aiFeaturesEnabled,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
return {
|
|
cssWarnings: cssWarnings && cssWarnings.length > 0 ? cssWarnings : undefined,
|
|
};
|
|
});
|