mirror of
https://github.com/documenso/documenso.git
synced 2025-11-10 04:22:32 +10:00
fix: document visibility logic (#1521)
Update the logic of document visibility logic and added some tests & updated some existing ones.
This commit is contained in:
@ -17,23 +17,25 @@ The default document visibility option allows you to control who can view and ac
|
||||
|
||||
The default document visibility is set to "_EVERYONE_" by default. You can change this setting by going to the [team's general preferences page](/users/teams/preferences) and selecting a different visibility option.
|
||||
|
||||
<Callout type="warning">
|
||||
If the team member uploading the document has a role lower than the default document visibility,
|
||||
the document visibility will be set to a lower visibility level matching the team member's role.
|
||||
</Callout>
|
||||
|
||||
Here's how it works:
|
||||
|
||||
- If a user with the "_Member_" role creates a document and the default document visibility is set to "_Admin_" or "_Managers and above_", the document's visibility is set to "_Everyone_".
|
||||
- If a user with the "_Manager_" role creates a document and the default document visibility is set to "_Admin_", the document's visibility is set to "_Managers and above_".
|
||||
- Otherwise, the document's visibility is set to the default document visibility.
|
||||
- If a user with the "_Member_" role creates a document and the default document visibility is set to "_Everyone_", the document's visibility is set to "_EVERYONE_".
|
||||
- The user can't change the visibility of the document in the document editor.
|
||||
- If a user with the "_Member_" role creates a document and the default document visibility is set to "_Admin_" or "_Managers and above_", the document's visibility is set to the default document visibility ("_Admin_" or "_Managers and above_" in this case).
|
||||
- The user can't change the visibility of the document in the document editor.
|
||||
- If a user with the "_Manager_" role creates a document and the default document visibility is set to "_Everyone_" or "_Managers and above_", the document's visibility is set to the default document visibility ("_Everyone_" or "_Managers and above_" in this case).
|
||||
- The user can change the visibility of the document to any of these options, except "_Admin_", in the document editor.
|
||||
- If a user with the "_Manager_" role creates a document and the default document visibility is set to "_Admin_", the document's visibility is set to "_Admin_".
|
||||
- The user can't change the visibility of the document in the document editor.
|
||||
- If a user with the "_Admin_" role creates a document, and the default document visibility is set to "_Everyone_", "_Managers and above_", or "_Admin_", the document's visibility is set to the default document visibility.
|
||||
- The user can change the visibility of the document to any of these options in the document editor.
|
||||
|
||||
You can change the visibility of a document at any time by editing the document and selecting a different visibility option.
|
||||
|
||||

|
||||
|
||||
<Callout type="warning">
|
||||
Updating the default document visibility in the team's general settings will not affect the
|
||||
Updating the default document visibility in the team's general preferences will not affect the
|
||||
visibility of existing documents. You will need to update the visibility of each document
|
||||
individually.
|
||||
</Callout>
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
import { DocumentStatus, TeamMemberRole } from '@documenso/prisma/client';
|
||||
import { DocumentStatus, DocumentVisibility, TeamMemberRole } from '@documenso/prisma/client';
|
||||
import { seedBlankDocument } from '@documenso/prisma/seed/documents';
|
||||
import { seedDocuments, seedTeamDocuments } from '@documenso/prisma/seed/documents';
|
||||
import { seedTeam, seedTeamEmail, seedTeamMember } from '@documenso/prisma/seed/teams';
|
||||
import { seedUser } from '@documenso/prisma/seed/users';
|
||||
@ -538,7 +539,7 @@ test('[TEAMS]: ensure recipient can see document regardless of visibility', asyn
|
||||
await apiSignout({ page });
|
||||
});
|
||||
|
||||
test('[TEAMS]: check that members cannot see ADMIN-only documents', async ({ page }) => {
|
||||
test('[TEAMS]: check that MEMBER role cannot see ADMIN-only documents', async ({ page }) => {
|
||||
const team = await seedTeam();
|
||||
|
||||
// Seed a member user
|
||||
@ -575,7 +576,46 @@ test('[TEAMS]: check that members cannot see ADMIN-only documents', async ({ pag
|
||||
await apiSignout({ page });
|
||||
});
|
||||
|
||||
test('[TEAMS]: check that managers cannot see ADMIN-only documents', async ({ page }) => {
|
||||
test('[TEAMS]: check that MEMBER role cannot see MANAGER_AND_ABOVE-only documents', async ({
|
||||
page,
|
||||
}) => {
|
||||
const team = await seedTeam();
|
||||
|
||||
// Seed a member user
|
||||
const memberUser = await seedTeamMember({
|
||||
teamId: team.id,
|
||||
role: TeamMemberRole.MEMBER,
|
||||
});
|
||||
|
||||
// Seed an ADMIN-only document
|
||||
await seedDocuments([
|
||||
{
|
||||
sender: team.owner,
|
||||
recipients: [],
|
||||
type: DocumentStatus.COMPLETED,
|
||||
documentOptions: {
|
||||
teamId: team.id,
|
||||
visibility: 'MANAGER_AND_ABOVE',
|
||||
title: 'Manager and Above Only Document',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
await apiSignin({
|
||||
page,
|
||||
email: memberUser.email,
|
||||
redirectPath: `/t/${team.url}/documents?status=COMPLETED`,
|
||||
});
|
||||
|
||||
// Check that the member user cannot see the ADMIN-only document
|
||||
await expect(
|
||||
page.getByRole('link', { name: 'Admin Only Document', exact: true }),
|
||||
).not.toBeVisible();
|
||||
|
||||
await apiSignout({ page });
|
||||
});
|
||||
|
||||
test('[TEAMS]: check that MANAGER role cannot see ADMIN-only documents', async ({ page }) => {
|
||||
const team = await seedTeam();
|
||||
|
||||
// Seed a manager user
|
||||
@ -612,7 +652,7 @@ test('[TEAMS]: check that managers cannot see ADMIN-only documents', async ({ pa
|
||||
await apiSignout({ page });
|
||||
});
|
||||
|
||||
test('[TEAMS]: check that admin can see MANAGER_AND_ABOVE documents', async ({ page }) => {
|
||||
test('[TEAMS]: check that ADMIN role can see MANAGER_AND_ABOVE documents', async ({ page }) => {
|
||||
const team = await seedTeam();
|
||||
|
||||
// Seed an admin user
|
||||
@ -649,6 +689,187 @@ test('[TEAMS]: check that admin can see MANAGER_AND_ABOVE documents', async ({ p
|
||||
await apiSignout({ page });
|
||||
});
|
||||
|
||||
test('[TEAMS]: check that ADMIN role can change document visibility', async ({ page }) => {
|
||||
const team = await seedTeam({
|
||||
createTeamOptions: {
|
||||
teamGlobalSettings: {
|
||||
create: {
|
||||
documentVisibility: DocumentVisibility.MANAGER_AND_ABOVE,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const adminUser = await seedTeamMember({
|
||||
teamId: team.id,
|
||||
role: TeamMemberRole.ADMIN,
|
||||
});
|
||||
|
||||
const document = await seedBlankDocument(adminUser, {
|
||||
createDocumentOptions: {
|
||||
teamId: team.id,
|
||||
visibility: team.teamGlobalSettings?.documentVisibility,
|
||||
},
|
||||
});
|
||||
|
||||
await apiSignin({
|
||||
page,
|
||||
email: adminUser.email,
|
||||
redirectPath: `/t/${team.url}/documents/${document.id}/edit`,
|
||||
});
|
||||
|
||||
await page.getByTestId('documentVisibilitySelectValue').click();
|
||||
await page.getByLabel('Admins only').click();
|
||||
|
||||
await page.getByRole('button', { name: 'Continue' }).click();
|
||||
await expect(page.getByRole('heading', { name: 'Add Signers' })).toBeVisible();
|
||||
|
||||
await page.getByRole('button', { name: 'Go Back' }).click();
|
||||
await expect(page.getByRole('heading', { name: 'General' })).toBeVisible();
|
||||
|
||||
await expect(page.getByTestId('documentVisibilitySelectValue')).toContainText('Admins only');
|
||||
});
|
||||
|
||||
test('[TEAMS]: check that MEMBER role cannot change visibility of EVERYONE documents', async ({
|
||||
page,
|
||||
}) => {
|
||||
const team = await seedTeam({
|
||||
createTeamOptions: {
|
||||
teamGlobalSettings: {
|
||||
create: {
|
||||
documentVisibility: DocumentVisibility.EVERYONE,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const teamMember = await seedTeamMember({
|
||||
teamId: team.id,
|
||||
role: TeamMemberRole.MEMBER,
|
||||
});
|
||||
|
||||
const document = await seedBlankDocument(teamMember, {
|
||||
createDocumentOptions: {
|
||||
teamId: team.id,
|
||||
visibility: team.teamGlobalSettings?.documentVisibility,
|
||||
},
|
||||
});
|
||||
|
||||
await apiSignin({
|
||||
page,
|
||||
email: teamMember.email,
|
||||
redirectPath: `/t/${team.url}/documents/${document.id}/edit`,
|
||||
});
|
||||
|
||||
await expect(page.getByTestId('documentVisibilitySelectValue')).toHaveText('Everyone');
|
||||
await expect(page.getByTestId('documentVisibilitySelectValue')).toBeDisabled();
|
||||
});
|
||||
|
||||
test('[TEAMS]: check that MEMBER role cannot change visibility of MANAGER_AND_ABOVE documents', async ({
|
||||
page,
|
||||
}) => {
|
||||
const team = await seedTeam({
|
||||
createTeamOptions: {
|
||||
teamGlobalSettings: {
|
||||
create: {
|
||||
documentVisibility: DocumentVisibility.MANAGER_AND_ABOVE,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const teamMember = await seedTeamMember({
|
||||
teamId: team.id,
|
||||
role: TeamMemberRole.MEMBER,
|
||||
});
|
||||
|
||||
const document = await seedBlankDocument(teamMember, {
|
||||
createDocumentOptions: {
|
||||
teamId: team.id,
|
||||
visibility: team.teamGlobalSettings?.documentVisibility,
|
||||
},
|
||||
});
|
||||
|
||||
await apiSignin({
|
||||
page,
|
||||
email: teamMember.email,
|
||||
redirectPath: `/t/${team.url}/documents/${document.id}/edit`,
|
||||
});
|
||||
|
||||
await expect(page.getByTestId('documentVisibilitySelectValue')).toHaveText('Managers and above');
|
||||
await expect(page.getByTestId('documentVisibilitySelectValue')).toBeDisabled();
|
||||
});
|
||||
|
||||
test('[TEAMS]: check that MEMBER role cannot change visibility of ADMIN documents', async ({
|
||||
page,
|
||||
}) => {
|
||||
const team = await seedTeam({
|
||||
createTeamOptions: {
|
||||
teamGlobalSettings: {
|
||||
create: {
|
||||
documentVisibility: DocumentVisibility.ADMIN,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const teamMember = await seedTeamMember({
|
||||
teamId: team.id,
|
||||
role: TeamMemberRole.MEMBER,
|
||||
});
|
||||
|
||||
const document = await seedBlankDocument(teamMember, {
|
||||
createDocumentOptions: {
|
||||
teamId: team.id,
|
||||
visibility: team.teamGlobalSettings?.documentVisibility,
|
||||
},
|
||||
});
|
||||
|
||||
await apiSignin({
|
||||
page,
|
||||
email: teamMember.email,
|
||||
redirectPath: `/t/${team.url}/documents/${document.id}/edit`,
|
||||
});
|
||||
|
||||
await expect(page.getByTestId('documentVisibilitySelectValue')).toHaveText('Admins only');
|
||||
await expect(page.getByTestId('documentVisibilitySelectValue')).toBeDisabled();
|
||||
});
|
||||
|
||||
test('[TEAMS]: check that MANAGER role cannot change visibility of ADMIN documents', async ({
|
||||
page,
|
||||
}) => {
|
||||
const team = await seedTeam({
|
||||
createTeamOptions: {
|
||||
teamGlobalSettings: {
|
||||
create: {
|
||||
documentVisibility: DocumentVisibility.ADMIN,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const teamManager = await seedTeamMember({
|
||||
teamId: team.id,
|
||||
role: TeamMemberRole.MANAGER,
|
||||
});
|
||||
|
||||
const document = await seedBlankDocument(teamManager, {
|
||||
createDocumentOptions: {
|
||||
teamId: team.id,
|
||||
visibility: team.teamGlobalSettings?.documentVisibility,
|
||||
},
|
||||
});
|
||||
|
||||
await apiSignin({
|
||||
page,
|
||||
email: teamManager.email,
|
||||
redirectPath: `/t/${team.url}/documents/${document.id}/edit`,
|
||||
});
|
||||
|
||||
await expect(page.getByTestId('documentVisibilitySelectValue')).toHaveText('Admins only');
|
||||
await expect(page.getByTestId('documentVisibilitySelectValue')).toBeDisabled();
|
||||
});
|
||||
|
||||
test('[TEAMS]: users cannot see documents from other teams', async ({ page }) => {
|
||||
// Seed two teams with documents
|
||||
const { team: teamA, teamMember2: teamAMember } = await seedTeamDocuments();
|
||||
|
||||
@ -89,17 +89,16 @@ export const createDocument = async ({
|
||||
globalVisibility: DocumentVisibility | null | undefined,
|
||||
userRole: TeamMemberRole,
|
||||
): DocumentVisibility => {
|
||||
const defaultVisibility = globalVisibility ?? DocumentVisibility.EVERYONE;
|
||||
if (globalVisibility) {
|
||||
return globalVisibility;
|
||||
}
|
||||
|
||||
if (userRole === TeamMemberRole.ADMIN) {
|
||||
return defaultVisibility;
|
||||
return DocumentVisibility.ADMIN;
|
||||
}
|
||||
|
||||
if (userRole === TeamMemberRole.MANAGER) {
|
||||
if (defaultVisibility === DocumentVisibility.ADMIN) {
|
||||
return DocumentVisibility.MANAGER_AND_ABOVE;
|
||||
}
|
||||
return defaultVisibility;
|
||||
return DocumentVisibility.MANAGER_AND_ABOVE;
|
||||
}
|
||||
|
||||
return DocumentVisibility.EVERYONE;
|
||||
|
||||
@ -91,39 +91,43 @@ export const updateDocumentSettings = async ({
|
||||
|
||||
if (teamId) {
|
||||
const currentUserRole = document.team?.members[0]?.role;
|
||||
const isDocumentOwner = document.userId === userId;
|
||||
const requestedVisibility = data.visibility;
|
||||
|
||||
match(currentUserRole)
|
||||
.with(TeamMemberRole.ADMIN, () => true)
|
||||
.with(TeamMemberRole.MANAGER, () => {
|
||||
const allowedVisibilities: DocumentVisibility[] = [
|
||||
DocumentVisibility.EVERYONE,
|
||||
DocumentVisibility.MANAGER_AND_ABOVE,
|
||||
];
|
||||
if (!isDocumentOwner) {
|
||||
match(currentUserRole)
|
||||
.with(TeamMemberRole.ADMIN, () => true)
|
||||
.with(TeamMemberRole.MANAGER, () => {
|
||||
const allowedVisibilities: DocumentVisibility[] = [
|
||||
DocumentVisibility.EVERYONE,
|
||||
DocumentVisibility.MANAGER_AND_ABOVE,
|
||||
];
|
||||
|
||||
if (
|
||||
!allowedVisibilities.includes(document.visibility) ||
|
||||
(data.visibility && !allowedVisibilities.includes(data.visibility))
|
||||
) {
|
||||
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(() => {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'You do not have permission to update the document visibility',
|
||||
message: 'You do not have permission to update the document',
|
||||
});
|
||||
}
|
||||
})
|
||||
.with(TeamMemberRole.MEMBER, () => {
|
||||
if (
|
||||
document.visibility !== DocumentVisibility.EVERYONE ||
|
||||
(data.visibility && data.visibility !== 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',
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const { documentAuthOption } = extractDocumentAuthMethods({
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { customAlphabet } from 'nanoid';
|
||||
|
||||
import { prisma } from '..';
|
||||
import type { Prisma } from '../client';
|
||||
import { TeamMemberInviteStatus, TeamMemberRole } from '../client';
|
||||
import { seedUser } from './users';
|
||||
|
||||
@ -10,11 +11,13 @@ const nanoid = customAlphabet('1234567890abcdef', 10);
|
||||
type SeedTeamOptions = {
|
||||
createTeamMembers?: number;
|
||||
createTeamEmail?: true | string;
|
||||
createTeamOptions?: Partial<Prisma.TeamUncheckedCreateInput>;
|
||||
};
|
||||
|
||||
export const seedTeam = async ({
|
||||
createTeamMembers = 0,
|
||||
createTeamEmail,
|
||||
createTeamOptions = {},
|
||||
}: SeedTeamOptions = {}) => {
|
||||
const teamUrl = `team-${nanoid()}`;
|
||||
const teamEmail = createTeamEmail === true ? `${teamUrl}@${EMAIL_DOMAIN}` : createTeamEmail;
|
||||
@ -54,6 +57,7 @@ export const seedTeam = async ({
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
...createTeamOptions,
|
||||
},
|
||||
});
|
||||
|
||||
@ -69,6 +73,7 @@ export const seedTeam = async ({
|
||||
},
|
||||
},
|
||||
teamEmail: true,
|
||||
teamGlobalSettings: true,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
import React, { forwardRef } from 'react';
|
||||
|
||||
import { TeamMemberRole } from '@prisma/client';
|
||||
import type { SelectProps } from '@radix-ui/react-select';
|
||||
import { InfoIcon } from 'lucide-react';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { DOCUMENT_VISIBILITY } from '@documenso/lib/constants/document-visibility';
|
||||
import { DocumentVisibility } from '@documenso/lib/types/document-visibility';
|
||||
@ -18,15 +20,27 @@ export type DocumentVisibilitySelectType = SelectProps & {
|
||||
currentMemberRole?: string;
|
||||
isTeamSettings?: boolean;
|
||||
disabled?: boolean;
|
||||
visibility?: string;
|
||||
};
|
||||
|
||||
export const DocumentVisibilitySelect = forwardRef<HTMLButtonElement, DocumentVisibilitySelectType>(
|
||||
({ currentMemberRole, isTeamSettings = false, disabled, ...props }, ref) => {
|
||||
const canUpdateVisibility =
|
||||
currentMemberRole === 'ADMIN' || currentMemberRole === 'MANAGER' || isTeamSettings;
|
||||
({ currentMemberRole, isTeamSettings = false, disabled, visibility, ...props }, ref) => {
|
||||
const canUpdateVisibility = match(currentMemberRole)
|
||||
.with(TeamMemberRole.ADMIN, () => true)
|
||||
.with(
|
||||
TeamMemberRole.MANAGER,
|
||||
() =>
|
||||
visibility === DocumentVisibility.EVERYONE ||
|
||||
visibility === DocumentVisibility.MANAGER_AND_ABOVE,
|
||||
)
|
||||
.otherwise(() => false);
|
||||
|
||||
const isAdmin = currentMemberRole === TeamMemberRole.ADMIN;
|
||||
const isManager = currentMemberRole === TeamMemberRole.MANAGER;
|
||||
const canEdit = isTeamSettings || canUpdateVisibility;
|
||||
|
||||
return (
|
||||
<Select {...props} disabled={(!canUpdateVisibility && !isTeamSettings) || disabled}>
|
||||
<Select {...props} disabled={!canEdit || disabled}>
|
||||
<SelectTrigger ref={ref} className="bg-background text-muted-foreground">
|
||||
<SelectValue data-testid="documentVisibilitySelectValue" placeholder="Everyone" />
|
||||
</SelectTrigger>
|
||||
@ -35,13 +49,13 @@ export const DocumentVisibilitySelect = forwardRef<HTMLButtonElement, DocumentVi
|
||||
<SelectItem value={DocumentVisibility.EVERYONE}>
|
||||
{DOCUMENT_VISIBILITY.EVERYONE.value}
|
||||
</SelectItem>
|
||||
<SelectItem value={DocumentVisibility.MANAGER_AND_ABOVE} disabled={!canUpdateVisibility}>
|
||||
<SelectItem
|
||||
value={DocumentVisibility.MANAGER_AND_ABOVE}
|
||||
disabled={!isAdmin && (!isManager || visibility === DocumentVisibility.ADMIN)}
|
||||
>
|
||||
{DOCUMENT_VISIBILITY.MANAGER_AND_ABOVE.value}
|
||||
</SelectItem>
|
||||
<SelectItem
|
||||
value={DocumentVisibility.ADMIN}
|
||||
disabled={currentMemberRole !== 'ADMIN' && !isTeamSettings}
|
||||
>
|
||||
<SelectItem value={DocumentVisibility.ADMIN} disabled={!isAdmin}>
|
||||
{DOCUMENT_VISIBILITY.ADMIN.value}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
|
||||
@ -238,6 +238,7 @@ export const AddSettingsFormPartial = ({
|
||||
<FormControl>
|
||||
<DocumentVisibilitySelect
|
||||
currentMemberRole={currentTeamMemberRole}
|
||||
visibility={document.visibility}
|
||||
{...field}
|
||||
onValueChange={field.onChange}
|
||||
/>
|
||||
@ -273,7 +274,7 @@ export const AddSettingsFormPartial = ({
|
||||
</AccordionTrigger>
|
||||
|
||||
<AccordionContent className="text-muted-foreground -mx-1 px-1 pt-2 text-sm leading-relaxed">
|
||||
<div className="flex flex-col space-y-6 ">
|
||||
<div className="flex flex-col space-y-6">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="externalId"
|
||||
|
||||
Reference in New Issue
Block a user