feat: document visibility (#1262)

Adds the ability to set a visibility scope for documents within teams.
This commit is contained in:
Catalin Pit
2024-09-16 17:14:16 +03:00
committed by GitHub
parent f7a20113e5
commit fa6453e811
32 changed files with 1995 additions and 1233 deletions

View File

@@ -2,10 +2,11 @@ import { DateTime } from 'luxon';
import { P, match } from 'ts-pattern';
import { prisma } from '@documenso/prisma';
import { RecipientRole, SigningStatus } from '@documenso/prisma/client';
import { RecipientRole, SigningStatus, TeamMemberRole } from '@documenso/prisma/client';
import type { Document, Prisma, Team, TeamEmail, User } from '@documenso/prisma/client';
import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status';
import { DocumentVisibility } from '../../types/document-visibility';
import type { FindResultSet } from '../../types/find-result-set';
import { maskRecipientTokensForDocument } from '../../utils/mask-recipient-tokens-for-document';
@@ -58,6 +59,14 @@ export const findDocuments = async ({
},
include: {
teamEmail: true,
members: {
where: {
userId,
},
select: {
role: true,
},
},
},
});
}
@@ -70,6 +79,7 @@ export const findDocuments = async ({
const orderByColumn = orderBy?.column ?? 'createdAt';
const orderByDirection = orderBy?.direction ?? 'desc';
const teamMemberRole = team?.members[0].role ?? null;
const termFilters = match(term)
.with(P.string.minLength(1), () => {
@@ -82,7 +92,37 @@ export const findDocuments = async ({
})
.otherwise(() => undefined);
const filters = team ? findTeamDocumentsFilter(status, team) : findDocumentsFilter(status, user);
const visibilityFilters = [
match(teamMemberRole)
.with(TeamMemberRole.ADMIN, () => ({
visibility: {
in: [
DocumentVisibility.EVERYONE,
DocumentVisibility.MANAGER_AND_ABOVE,
DocumentVisibility.ADMIN,
],
},
}))
.with(TeamMemberRole.MANAGER, () => ({
visibility: {
in: [DocumentVisibility.EVERYONE, DocumentVisibility.MANAGER_AND_ABOVE],
},
}))
.otherwise(() => ({ visibility: DocumentVisibility.EVERYONE })),
{
Recipient: {
some: {
email: user.email,
},
},
},
];
let filters: Prisma.DocumentWhereInput | null = findDocumentsFilter(status, user);
if (team) {
filters = findTeamDocumentsFilter(status, team, visibilityFilters);
}
if (filters === null) {
return {
@@ -148,9 +188,7 @@ export const findDocuments = async ({
}
const whereClause: Prisma.DocumentWhereInput = {
...termFilters,
...filters,
...deletedFilter,
AND: [{ ...termFilters }, { ...filters }, { ...deletedFilter }],
};
if (period) {
@@ -333,6 +371,7 @@ const findDocumentsFilter = (status: ExtendedDocumentStatus, user: User) => {
const findTeamDocumentsFilter = (
status: ExtendedDocumentStatus,
team: Team & { teamEmail: TeamEmail | null },
visibilityFilters: Prisma.DocumentWhereInput[],
) => {
const teamEmail = team.teamEmail?.email ?? null;
@@ -343,6 +382,7 @@ const findTeamDocumentsFilter = (
OR: [
{
teamId: team.id,
OR: visibilityFilters,
},
],
};
@@ -358,6 +398,7 @@ const findTeamDocumentsFilter = (
email: teamEmail,
},
},
OR: visibilityFilters,
});
// Filter to display all documents that have been sent by the team email.
@@ -365,6 +406,7 @@ const findTeamDocumentsFilter = (
User: {
email: teamEmail,
},
OR: visibilityFilters,
});
}
@@ -389,6 +431,7 @@ const findTeamDocumentsFilter = (
},
},
},
OR: visibilityFilters,
};
})
.with(ExtendedDocumentStatus.DRAFT, () => {
@@ -397,6 +440,7 @@ const findTeamDocumentsFilter = (
{
teamId: team.id,
status: ExtendedDocumentStatus.DRAFT,
OR: visibilityFilters,
},
],
};
@@ -407,6 +451,7 @@ const findTeamDocumentsFilter = (
User: {
email: teamEmail,
},
OR: visibilityFilters,
});
}
@@ -418,6 +463,7 @@ const findTeamDocumentsFilter = (
{
teamId: team.id,
status: ExtendedDocumentStatus.PENDING,
OR: visibilityFilters,
},
],
};
@@ -436,11 +482,13 @@ const findTeamDocumentsFilter = (
},
},
},
OR: visibilityFilters,
},
{
User: {
email: teamEmail,
},
OR: visibilityFilters,
},
],
});
@@ -454,6 +502,7 @@ const findTeamDocumentsFilter = (
OR: [
{
teamId: team.id,
OR: visibilityFilters,
},
],
};
@@ -466,11 +515,13 @@ const findTeamDocumentsFilter = (
email: teamEmail,
},
},
OR: visibilityFilters,
},
{
User: {
email: teamEmail,
},
OR: visibilityFilters,
},
);
}

View File

@@ -1,6 +1,10 @@
import { match } from 'ts-pattern';
import { prisma } from '@documenso/prisma';
import type { Prisma } from '@documenso/prisma/client';
import { TeamMemberRole } from '@documenso/prisma/client';
import { DocumentVisibility } from '../../types/document-visibility';
import { getTeamById } from '../team/get-team';
export type GetDocumentByIdOptions = {
@@ -28,6 +32,11 @@ export const getDocumentById = async ({ id, userId, teamId }: GetDocumentByIdOpt
email: true,
},
},
Recipient: {
select: {
email: true,
},
},
team: {
select: {
id: true,
@@ -115,5 +124,35 @@ export const getDocumentWhereInput = async ({
);
}
return documentWhereInput;
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 }]),
{
Recipient: {
some: {
email: user.email,
},
},
},
];
return {
...documentWhereInput,
OR: [...visibilityFilters],
};
};

View File

@@ -1,12 +1,16 @@
import { DateTime } from 'luxon';
import { match } from 'ts-pattern';
import type { PeriodSelectorValue } from '@documenso/lib/server-only/document/find-documents';
import { prisma } from '@documenso/prisma';
import { TeamMemberRole } from '@documenso/prisma/client';
import type { Prisma, User } from '@documenso/prisma/client';
import { SigningStatus } from '@documenso/prisma/client';
import { isExtendedDocumentStatus } from '@documenso/prisma/guards/is-extended-document-status';
import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status';
import { DocumentVisibility } from '../../types/document-visibility';
export type GetStatsInput = {
user: User;
team?: Omit<GetTeamCountsOption, 'createdAt'>;
@@ -27,7 +31,7 @@ export const getStats = async ({ user, period, ...options }: GetStatsInput) => {
}
const [ownerCounts, notSignedCounts, hasSignedCounts] = await (options.team
? getTeamCounts({ ...options.team, createdAt })
? getTeamCounts({ ...options.team, createdAt, currentUserEmail: user.email, userId: user.id })
: getCounts({ user, createdAt }));
const stats: Record<ExtendedDocumentStatus, number> = {
@@ -147,7 +151,10 @@ type GetTeamCountsOption = {
teamId: number;
teamEmail?: string;
senderIds?: number[];
currentUserEmail: string;
userId: number;
createdAt: Prisma.DocumentWhereInput['createdAt'];
currentTeamMemberRole?: TeamMemberRole;
};
const getTeamCounts = async (options: GetTeamCountsOption) => {
@@ -172,6 +179,49 @@ const getTeamCounts = async (options: GetTeamCountsOption) => {
let notSignedCountsGroupByArgs = null;
let hasSignedCountsGroupByArgs = null;
const visibilityFilters = [
...match(options.currentTeamMemberRole)
.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 }]),
];
ownerCountsWhereInput = {
...ownerCountsWhereInput,
OR: [
{
AND: [
{
visibility: {
in: visibilityFilters.map((filter) => filter.visibility),
},
},
{
Recipient: {
none: {
email: options.currentUserEmail,
},
},
},
],
},
{
Recipient: {
some: {
email: options.currentUserEmail,
},
},
},
],
};
if (teamEmail) {
ownerCountsWhereInput = {
userId: userIdWhereClause,

View File

@@ -6,6 +6,7 @@ import type { RequestMetadata } from '@documenso/lib/universal/extract-request-m
import type { CreateDocumentAuditLogDataResponse } from '@documenso/lib/utils/document-audit-logs';
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
import { prisma } from '@documenso/prisma';
import type { DocumentVisibility } from '@documenso/prisma/client';
import { DocumentStatus } from '@documenso/prisma/client';
import { AppError, AppErrorCode } from '../../errors/app-error';
@@ -19,6 +20,7 @@ export type UpdateDocumentSettingsOptions = {
data: {
title?: string;
externalId?: string | null;
visibility?: string | null;
globalAccessAuth?: TDocumentAccessAuthTypes | null;
globalActionAuth?: TDocumentActionAuthTypes | null;
};
@@ -95,6 +97,7 @@ export const updateDocumentSettings = async ({
const isExternalIdSame = data.externalId === document.externalId;
const isGlobalAccessSame = documentGlobalAccessAuth === newGlobalAccessAuth;
const isGlobalActionSame = documentGlobalActionAuth === newGlobalActionAuth;
const isDocumentVisibilitySame = data.visibility === document.visibility;
const auditLogs: CreateDocumentAuditLogDataResponse[] = [];
@@ -165,6 +168,21 @@ export const updateDocumentSettings = async ({
);
}
if (!isDocumentVisibilitySame) {
auditLogs.push(
createDocumentAuditLogData({
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_VISIBILITY_UPDATED,
documentId,
user,
requestMetadata,
data: {
from: document.visibility,
to: data.visibility || '',
},
}),
);
}
// Early return if nothing is required.
if (auditLogs.length === 0) {
return document;
@@ -183,6 +201,7 @@ export const updateDocumentSettings = async ({
data: {
title: data.title,
externalId: data.externalId || null,
visibility: data.visibility as DocumentVisibility,
authOptions,
},
});