mirror of
https://github.com/documenso/documenso.git
synced 2025-11-13 08:13:56 +10:00
feat: restrict reauth to EE
This commit is contained in:
@ -104,6 +104,7 @@ NEXT_PUBLIC_DOCUMENT_SIZE_UPLOAD_LIMIT=5
|
|||||||
NEXT_PRIVATE_STRIPE_API_KEY=
|
NEXT_PRIVATE_STRIPE_API_KEY=
|
||||||
NEXT_PRIVATE_STRIPE_WEBHOOK_SECRET=
|
NEXT_PRIVATE_STRIPE_WEBHOOK_SECRET=
|
||||||
NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID=
|
NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID=
|
||||||
|
NEXT_PUBLIC_STRIPE_ENTERPRISE_PLAN_MONTHLY_PRICE_ID=
|
||||||
|
|
||||||
# [[FEATURES]]
|
# [[FEATURES]]
|
||||||
# OPTIONAL: Leave blank to disable PostHog and feature flags.
|
# OPTIONAL: Leave blank to disable PostHog and feature flags.
|
||||||
|
|||||||
@ -40,6 +40,7 @@ export type EditDocumentFormProps = {
|
|||||||
fields: Field[];
|
fields: Field[];
|
||||||
documentData: DocumentData;
|
documentData: DocumentData;
|
||||||
documentRootPath: string;
|
documentRootPath: string;
|
||||||
|
isDocumentEnterprise: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type EditDocumentStep = 'settings' | 'signers' | 'fields' | 'subject';
|
type EditDocumentStep = 'settings' | 'signers' | 'fields' | 'subject';
|
||||||
@ -54,6 +55,7 @@ export const EditDocumentForm = ({
|
|||||||
user: _user,
|
user: _user,
|
||||||
documentData,
|
documentData,
|
||||||
documentRootPath,
|
documentRootPath,
|
||||||
|
isDocumentEnterprise,
|
||||||
}: EditDocumentFormProps) => {
|
}: EditDocumentFormProps) => {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
@ -261,6 +263,7 @@ export const EditDocumentForm = ({
|
|||||||
document={document}
|
document={document}
|
||||||
recipients={recipients}
|
recipients={recipients}
|
||||||
fields={fields}
|
fields={fields}
|
||||||
|
isDocumentEnterprise={isDocumentEnterprise}
|
||||||
onSubmit={onAddSettingsFormSubmit}
|
onSubmit={onAddSettingsFormSubmit}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@ -269,6 +272,7 @@ export const EditDocumentForm = ({
|
|||||||
documentFlow={documentFlow.signers}
|
documentFlow={documentFlow.signers}
|
||||||
recipients={recipients}
|
recipients={recipients}
|
||||||
fields={fields}
|
fields={fields}
|
||||||
|
isDocumentEnterprise={isDocumentEnterprise}
|
||||||
onSubmit={onAddSignersFormSubmit}
|
onSubmit={onAddSignersFormSubmit}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { redirect } from 'next/navigation';
|
|||||||
|
|
||||||
import { ChevronLeft, Users2 } from 'lucide-react';
|
import { ChevronLeft, Users2 } from 'lucide-react';
|
||||||
|
|
||||||
|
import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
|
||||||
import { DOCUMENSO_ENCRYPTION_KEY } from '@documenso/lib/constants/crypto';
|
import { DOCUMENSO_ENCRYPTION_KEY } from '@documenso/lib/constants/crypto';
|
||||||
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
|
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
|
||||||
import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id';
|
import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id';
|
||||||
@ -37,6 +38,11 @@ export const DocumentEditPageView = async ({ params, team }: DocumentEditPageVie
|
|||||||
|
|
||||||
const { user } = await getRequiredServerComponentSession();
|
const { user } = await getRequiredServerComponentSession();
|
||||||
|
|
||||||
|
const isDocumentEnterprise = await isUserEnterprise({
|
||||||
|
userId: user.id,
|
||||||
|
teamId: team?.id,
|
||||||
|
});
|
||||||
|
|
||||||
const document = await getDocumentById({
|
const document = await getDocumentById({
|
||||||
id: documentId,
|
id: documentId,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
@ -116,6 +122,7 @@ export const DocumentEditPageView = async ({ params, team }: DocumentEditPageVie
|
|||||||
fields={fields}
|
fields={fields}
|
||||||
documentData={documentData}
|
documentData={documentData}
|
||||||
documentRootPath={documentRootPath}
|
documentRootPath={documentRootPath}
|
||||||
|
isDocumentEnterprise={isDocumentEnterprise}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,16 +1,148 @@
|
|||||||
import { expect, test } from '@playwright/test';
|
import { expect, test } from '@playwright/test';
|
||||||
|
import dotenv from 'dotenv';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
seedBlankDocument,
|
seedBlankDocument,
|
||||||
seedDraftDocument,
|
seedDraftDocument,
|
||||||
seedPendingDocument,
|
seedPendingDocument,
|
||||||
} from '@documenso/prisma/seed/documents';
|
} from '@documenso/prisma/seed/documents';
|
||||||
|
import { seedUserSubscription } from '@documenso/prisma/seed/subscriptions';
|
||||||
|
import { seedTeam, unseedTeam } from '@documenso/prisma/seed/teams';
|
||||||
import { seedUser, unseedUser } from '@documenso/prisma/seed/users';
|
import { seedUser, unseedUser } from '@documenso/prisma/seed/users';
|
||||||
|
|
||||||
import { apiSignin } from '../fixtures/authentication';
|
import { apiSignin } from '../fixtures/authentication';
|
||||||
|
|
||||||
|
dotenv.config({ path: path.resolve(__dirname, '../../../../', '.env.local') });
|
||||||
|
|
||||||
test.describe.configure({ mode: 'parallel' });
|
test.describe.configure({ mode: 'parallel' });
|
||||||
|
|
||||||
|
test.describe('[EE_ONLY]', () => {
|
||||||
|
const enterprisePriceId = process.env.NEXT_PUBLIC_STRIPE_ENTERPRISE_PLAN_MONTHLY_PRICE_ID || '';
|
||||||
|
|
||||||
|
test.beforeEach(() => {
|
||||||
|
test.skip(
|
||||||
|
process.env.NEXT_PUBLIC_FEATURE_BILLING_ENABLED !== 'true' || !enterprisePriceId,
|
||||||
|
'Billing required for this test',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('[DOCUMENT_FLOW] add action auth settings', async ({ page }) => {
|
||||||
|
const user = await seedUser();
|
||||||
|
|
||||||
|
await seedUserSubscription({
|
||||||
|
userId: user.id,
|
||||||
|
priceId: enterprisePriceId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const document = await seedBlankDocument(user);
|
||||||
|
|
||||||
|
await apiSignin({
|
||||||
|
page,
|
||||||
|
email: user.email,
|
||||||
|
redirectPath: `/documents/${document.id}/edit`,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set EE action auth.
|
||||||
|
await page.getByTestId('documentActionSelectValue').click();
|
||||||
|
await page.getByLabel('Require account').getByText('Require account').click();
|
||||||
|
await expect(page.getByTestId('documentActionSelectValue')).toContainText('Require account');
|
||||||
|
|
||||||
|
// Save the settings by going to the next step.
|
||||||
|
await page.getByRole('button', { name: 'Continue' }).click();
|
||||||
|
await expect(page.getByRole('heading', { name: 'Add Signers' })).toBeVisible();
|
||||||
|
|
||||||
|
// Return to the settings step to check that the results are saved correctly.
|
||||||
|
await page.getByRole('button', { name: 'Go Back' }).click();
|
||||||
|
await expect(page.getByRole('heading', { name: 'General' })).toBeVisible();
|
||||||
|
|
||||||
|
// Todo: Verify that the values are correct once we fix the issue where going back
|
||||||
|
// does not show the updated values.
|
||||||
|
// await expect(page.getByLabel('Title')).toContainText('New Title');
|
||||||
|
// await expect(page.getByTestId('documentAccessSelectValue')).toContainText('Require account');
|
||||||
|
// await expect(page.getByTestId('documentActionSelectValue')).toContainText('Require account');
|
||||||
|
|
||||||
|
await unseedUser(user.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('[DOCUMENT_FLOW] enterprise team member can add action auth settings', async ({ page }) => {
|
||||||
|
const team = await seedTeam({
|
||||||
|
createTeamMembers: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
const owner = team.owner;
|
||||||
|
const teamMemberUser = team.members[1].user;
|
||||||
|
|
||||||
|
// Make the team enterprise by giving the owner the enterprise subscription.
|
||||||
|
await seedUserSubscription({
|
||||||
|
userId: team.ownerUserId,
|
||||||
|
priceId: enterprisePriceId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const document = await seedBlankDocument(owner, {
|
||||||
|
createDocumentOptions: {
|
||||||
|
teamId: team.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await apiSignin({
|
||||||
|
page,
|
||||||
|
email: teamMemberUser.email,
|
||||||
|
redirectPath: `/t/${team.url}/documents/${document.id}/edit`,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set EE action auth.
|
||||||
|
await page.getByTestId('documentActionSelectValue').click();
|
||||||
|
await page.getByLabel('Require account').getByText('Require account').click();
|
||||||
|
await expect(page.getByTestId('documentActionSelectValue')).toContainText('Require account');
|
||||||
|
|
||||||
|
// Save the settings by going to the next step.
|
||||||
|
await page.getByRole('button', { name: 'Continue' }).click();
|
||||||
|
await expect(page.getByRole('heading', { name: 'Add Signers' })).toBeVisible();
|
||||||
|
|
||||||
|
// Advanced settings should be visible.
|
||||||
|
await expect(page.getByLabel('Show advanced settings')).toBeVisible();
|
||||||
|
|
||||||
|
await unseedTeam(team.url);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('[DOCUMENT_FLOW] enterprise team member should not have access to enterprise on personal account', async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
const team = await seedTeam({
|
||||||
|
createTeamMembers: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
const teamMemberUser = team.members[1].user;
|
||||||
|
|
||||||
|
// Make the team enterprise by giving the owner the enterprise subscription.
|
||||||
|
await seedUserSubscription({
|
||||||
|
userId: team.ownerUserId,
|
||||||
|
priceId: enterprisePriceId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const document = await seedBlankDocument(teamMemberUser);
|
||||||
|
|
||||||
|
await apiSignin({
|
||||||
|
page,
|
||||||
|
email: teamMemberUser.email,
|
||||||
|
redirectPath: `/documents/${document.id}/edit`,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Global action auth should not be visible.
|
||||||
|
await expect(page.getByTestId('documentActionSelectValue')).not.toBeVisible();
|
||||||
|
|
||||||
|
// Next step.
|
||||||
|
await page.getByRole('button', { name: 'Continue' }).click();
|
||||||
|
await expect(page.getByRole('heading', { name: 'Add Signers' })).toBeVisible();
|
||||||
|
|
||||||
|
// Advanced settings should not be visible.
|
||||||
|
await expect(page.getByLabel('Show advanced settings')).not.toBeVisible();
|
||||||
|
|
||||||
|
await unseedTeam(team.url);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
test('[DOCUMENT_FLOW]: add settings', async ({ page }) => {
|
test('[DOCUMENT_FLOW]: add settings', async ({ page }) => {
|
||||||
const user = await seedUser();
|
const user = await seedUser();
|
||||||
const document = await seedBlankDocument(user);
|
const document = await seedBlankDocument(user);
|
||||||
@ -29,10 +161,8 @@ test('[DOCUMENT_FLOW]: add settings', async ({ page }) => {
|
|||||||
await page.getByLabel('Require account').getByText('Require account').click();
|
await page.getByLabel('Require account').getByText('Require account').click();
|
||||||
await expect(page.getByTestId('documentAccessSelectValue')).toContainText('Require account');
|
await expect(page.getByTestId('documentAccessSelectValue')).toContainText('Require account');
|
||||||
|
|
||||||
// Set action auth.
|
// Action auth should NOT be visible.
|
||||||
await page.getByTestId('documentActionSelectValue').click();
|
await expect(page.getByTestId('documentActionSelectValue')).not.toBeVisible();
|
||||||
await page.getByLabel('Require account').getByText('Require account').click();
|
|
||||||
await expect(page.getByTestId('documentActionSelectValue')).toContainText('Require account');
|
|
||||||
|
|
||||||
// Save the settings by going to the next step.
|
// Save the settings by going to the next step.
|
||||||
await page.getByRole('button', { name: 'Continue' }).click();
|
await page.getByRole('button', { name: 'Continue' }).click();
|
||||||
|
|||||||
@ -1,12 +1,71 @@
|
|||||||
import { expect, test } from '@playwright/test';
|
import { expect, test } from '@playwright/test';
|
||||||
|
import dotenv from 'dotenv';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
import { seedBlankDocument } from '@documenso/prisma/seed/documents';
|
import { seedBlankDocument } from '@documenso/prisma/seed/documents';
|
||||||
|
import { seedUserSubscription } from '@documenso/prisma/seed/subscriptions';
|
||||||
import { seedUser, unseedUser } from '@documenso/prisma/seed/users';
|
import { seedUser, unseedUser } from '@documenso/prisma/seed/users';
|
||||||
|
|
||||||
import { apiSignin } from '../fixtures/authentication';
|
import { apiSignin } from '../fixtures/authentication';
|
||||||
|
|
||||||
|
dotenv.config({ path: path.resolve(__dirname, '../../../../', '.env.local') });
|
||||||
|
|
||||||
test.describe.configure({ mode: 'parallel' });
|
test.describe.configure({ mode: 'parallel' });
|
||||||
|
|
||||||
|
test.describe('[EE_ONLY]', () => {
|
||||||
|
const enterprisePriceId = process.env.NEXT_PUBLIC_STRIPE_ENTERPRISE_PLAN_MONTHLY_PRICE_ID || '';
|
||||||
|
|
||||||
|
test.beforeEach(() => {
|
||||||
|
test.skip(
|
||||||
|
process.env.NEXT_PUBLIC_FEATURE_BILLING_ENABLED !== 'true' || !enterprisePriceId,
|
||||||
|
'Billing required for this test',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('[DOCUMENT_FLOW] add EE settings', async ({ page }) => {
|
||||||
|
const user = await seedUser();
|
||||||
|
|
||||||
|
await seedUserSubscription({
|
||||||
|
userId: user.id,
|
||||||
|
priceId: enterprisePriceId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const document = await seedBlankDocument(user);
|
||||||
|
|
||||||
|
await apiSignin({
|
||||||
|
page,
|
||||||
|
email: user.email,
|
||||||
|
redirectPath: `/documents/${document.id}/edit`,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Save the settings by going to the next step.
|
||||||
|
await page.getByRole('button', { name: 'Continue' }).click();
|
||||||
|
await expect(page.getByRole('heading', { name: 'Add Signers' })).toBeVisible();
|
||||||
|
|
||||||
|
// Add 2 signers.
|
||||||
|
await page.getByPlaceholder('Email').fill('recipient1@documenso.com');
|
||||||
|
await page.getByPlaceholder('Name').fill('Recipient 1');
|
||||||
|
await page.getByRole('button', { name: 'Add Signer' }).click();
|
||||||
|
await page
|
||||||
|
.getByRole('textbox', { name: 'Email', exact: true })
|
||||||
|
.fill('recipient2@documenso.com');
|
||||||
|
await page.getByRole('textbox', { name: 'Name', exact: true }).fill('Recipient 2');
|
||||||
|
|
||||||
|
// Display advanced settings.
|
||||||
|
await page.getByLabel('Show advanced settings').click();
|
||||||
|
|
||||||
|
// Navigate to the next step and back.
|
||||||
|
await page.getByRole('button', { name: 'Continue' }).click();
|
||||||
|
await expect(page.getByRole('heading', { name: 'Add Fields' })).toBeVisible();
|
||||||
|
await page.getByRole('button', { name: 'Go Back' }).click();
|
||||||
|
await expect(page.getByRole('heading', { name: 'Add Signers' })).toBeVisible();
|
||||||
|
|
||||||
|
// Todo: Fix stepper component back issue before finishing test.
|
||||||
|
|
||||||
|
await unseedUser(user.id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// Note: Not complete yet due to issue with back button.
|
// Note: Not complete yet due to issue with back button.
|
||||||
test('[DOCUMENT_FLOW]: add signers', async ({ page }) => {
|
test('[DOCUMENT_FLOW]: add signers', async ({ page }) => {
|
||||||
const user = await seedUser();
|
const user = await seedUser();
|
||||||
@ -29,8 +88,8 @@ test('[DOCUMENT_FLOW]: add signers', async ({ page }) => {
|
|||||||
await page.getByRole('textbox', { name: 'Email', exact: true }).fill('recipient2@documenso.com');
|
await page.getByRole('textbox', { name: 'Email', exact: true }).fill('recipient2@documenso.com');
|
||||||
await page.getByRole('textbox', { name: 'Name', exact: true }).fill('Recipient 2');
|
await page.getByRole('textbox', { name: 'Name', exact: true }).fill('Recipient 2');
|
||||||
|
|
||||||
// Display advanced settings.
|
// Advanced settings should not be visible for non EE users.
|
||||||
await page.getByLabel('Show advanced settings').click();
|
await expect(page.getByLabel('Show advanced settings')).toBeHidden();
|
||||||
|
|
||||||
// Navigate to the next step and back.
|
// Navigate to the next step and back.
|
||||||
await page.getByRole('button', { name: 'Continue' }).click();
|
await page.getByRole('button', { name: 'Continue' }).click();
|
||||||
|
|||||||
56
packages/ee/server-only/util/is-document-enterprise.ts
Normal file
56
packages/ee/server-only/util/is-document-enterprise.ts
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
|
||||||
|
import { subscriptionsContainActiveEnterprisePlan } from '@documenso/lib/utils/billing';
|
||||||
|
import { prisma } from '@documenso/prisma';
|
||||||
|
import type { Subscription } from '@documenso/prisma/client';
|
||||||
|
|
||||||
|
export type IsUserEnterpriseOptions = {
|
||||||
|
userId: number;
|
||||||
|
teamId?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the user is enterprise, or has permission to use enterprise features on
|
||||||
|
* behalf of their team.
|
||||||
|
*
|
||||||
|
* It is assumed that the provided user is part of the provided team.
|
||||||
|
*/
|
||||||
|
export const isUserEnterprise = async ({
|
||||||
|
userId,
|
||||||
|
teamId,
|
||||||
|
}: IsUserEnterpriseOptions): Promise<boolean> => {
|
||||||
|
let subscriptions: Subscription[] = [];
|
||||||
|
|
||||||
|
if (!IS_BILLING_ENABLED()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (teamId) {
|
||||||
|
subscriptions = await prisma.team
|
||||||
|
.findFirstOrThrow({
|
||||||
|
where: {
|
||||||
|
id: teamId,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
owner: {
|
||||||
|
include: {
|
||||||
|
Subscription: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then((team) => team.owner.Subscription);
|
||||||
|
} else {
|
||||||
|
subscriptions = await prisma.user
|
||||||
|
.findFirstOrThrow({
|
||||||
|
where: {
|
||||||
|
id: userId,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
Subscription: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then((user) => user.Subscription);
|
||||||
|
}
|
||||||
|
|
||||||
|
return subscriptionsContainActiveEnterprisePlan(subscriptions);
|
||||||
|
};
|
||||||
@ -1,5 +1,6 @@
|
|||||||
'use server';
|
'use server';
|
||||||
|
|
||||||
|
import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
|
||||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
||||||
import type { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
import type { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||||
import type { CreateDocumentAuditLogDataResponse } from '@documenso/lib/utils/document-audit-logs';
|
import type { CreateDocumentAuditLogDataResponse } from '@documenso/lib/utils/document-audit-logs';
|
||||||
@ -74,6 +75,21 @@ export const updateDocumentSettings = async ({
|
|||||||
const newGlobalActionAuth =
|
const newGlobalActionAuth =
|
||||||
data?.globalActionAuth === undefined ? documentGlobalActionAuth : data.globalActionAuth;
|
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 (!isDocumentEnterprise) {
|
||||||
|
throw new AppError(
|
||||||
|
AppErrorCode.UNAUTHORIZED,
|
||||||
|
'You do not have permission to set the action auth',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const isTitleSame = data.title === document.title;
|
const isTitleSame = data.title === document.title;
|
||||||
const isGlobalAccessSame = documentGlobalAccessAuth === newGlobalAccessAuth;
|
const isGlobalAccessSame = documentGlobalAccessAuth === newGlobalAccessAuth;
|
||||||
const isGlobalActionSame = documentGlobalActionAuth === newGlobalActionAuth;
|
const isGlobalActionSame = documentGlobalActionAuth === newGlobalActionAuth;
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
|
||||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
||||||
import {
|
import {
|
||||||
type TRecipientActionAuthTypes,
|
type TRecipientActionAuthTypes,
|
||||||
@ -14,6 +15,8 @@ import { prisma } from '@documenso/prisma';
|
|||||||
import { RecipientRole } from '@documenso/prisma/client';
|
import { RecipientRole } from '@documenso/prisma/client';
|
||||||
import { SendStatus, SigningStatus } from '@documenso/prisma/client';
|
import { SendStatus, SigningStatus } from '@documenso/prisma/client';
|
||||||
|
|
||||||
|
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||||
|
|
||||||
export interface SetRecipientsForDocumentOptions {
|
export interface SetRecipientsForDocumentOptions {
|
||||||
userId: number;
|
userId: number;
|
||||||
teamId?: number;
|
teamId?: number;
|
||||||
@ -75,6 +78,23 @@ export const setRecipientsForDocument = async ({
|
|||||||
throw new Error('Document already complete');
|
throw new Error('Document already complete');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const recipientsHaveActionAuth = recipients.some((recipient) => recipient.actionAuth);
|
||||||
|
|
||||||
|
// Check if user has permission to set the global action auth.
|
||||||
|
if (recipientsHaveActionAuth) {
|
||||||
|
const isDocumentEnterprise = await isUserEnterprise({
|
||||||
|
userId,
|
||||||
|
teamId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!isDocumentEnterprise) {
|
||||||
|
throw new AppError(
|
||||||
|
AppErrorCode.UNAUTHORIZED,
|
||||||
|
'You do not have permission to set the action auth',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const normalizedRecipients = recipients.map((recipient) => ({
|
const normalizedRecipients = recipients.map((recipient) => ({
|
||||||
...recipient,
|
...recipient,
|
||||||
email: recipient.email.toLowerCase(),
|
email: recipient.email.toLowerCase(),
|
||||||
|
|||||||
@ -1,3 +1,6 @@
|
|||||||
|
import { env } from 'next-runtime-env';
|
||||||
|
|
||||||
|
import { IS_BILLING_ENABLED } from '../constants/app';
|
||||||
import type { Subscription } from '.prisma/client';
|
import type { Subscription } from '.prisma/client';
|
||||||
import { SubscriptionStatus } from '.prisma/client';
|
import { SubscriptionStatus } from '.prisma/client';
|
||||||
|
|
||||||
@ -13,3 +16,15 @@ export const subscriptionsContainsActivePlan = (
|
|||||||
subscription.status === SubscriptionStatus.ACTIVE && priceIds.includes(subscription.priceId),
|
subscription.status === SubscriptionStatus.ACTIVE && priceIds.includes(subscription.priceId),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const subscriptionsContainActiveEnterprisePlan = (
|
||||||
|
subscriptions?: Subscription[],
|
||||||
|
): boolean => {
|
||||||
|
const enterprisePlanId = env('NEXT_PUBLIC_STRIPE_ENTERPRISE_PLAN_MONTHLY_PRICE_ID');
|
||||||
|
|
||||||
|
if (!enterprisePlanId || !subscriptions || !IS_BILLING_ENABLED()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return subscriptionsContainsActivePlan(subscriptions, [enterprisePlanId]);
|
||||||
|
};
|
||||||
|
|||||||
19
packages/prisma/seed/subscriptions.ts
Normal file
19
packages/prisma/seed/subscriptions.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { prisma } from '..';
|
||||||
|
|
||||||
|
export const seedTestEmail = () => `user-${Date.now()}@test.documenso.com`;
|
||||||
|
|
||||||
|
type SeedSubscriptionOptions = {
|
||||||
|
userId: number;
|
||||||
|
priceId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const seedUserSubscription = async ({ userId, priceId }: SeedSubscriptionOptions) => {
|
||||||
|
return await prisma.subscription.create({
|
||||||
|
data: {
|
||||||
|
userId,
|
||||||
|
planId: Date.now().toString(),
|
||||||
|
priceId,
|
||||||
|
status: 'ACTIVE',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
@ -49,6 +49,7 @@ export type AddSettingsFormProps = {
|
|||||||
documentFlow: DocumentFlowStep;
|
documentFlow: DocumentFlowStep;
|
||||||
recipients: Recipient[];
|
recipients: Recipient[];
|
||||||
fields: Field[];
|
fields: Field[];
|
||||||
|
isDocumentEnterprise: boolean;
|
||||||
document: DocumentWithData;
|
document: DocumentWithData;
|
||||||
onSubmit: (_data: TAddSettingsFormSchema) => void;
|
onSubmit: (_data: TAddSettingsFormSchema) => void;
|
||||||
};
|
};
|
||||||
@ -57,6 +58,7 @@ export const AddSettingsFormPartial = ({
|
|||||||
documentFlow,
|
documentFlow,
|
||||||
recipients,
|
recipients,
|
||||||
fields,
|
fields,
|
||||||
|
isDocumentEnterprise,
|
||||||
document,
|
document,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
}: AddSettingsFormProps) => {
|
}: AddSettingsFormProps) => {
|
||||||
@ -183,66 +185,67 @@ export const AddSettingsFormPartial = ({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FormField
|
{isDocumentEnterprise && (
|
||||||
control={form.control}
|
<FormField
|
||||||
name="globalActionAuth"
|
control={form.control}
|
||||||
render={({ field }) => (
|
name="globalActionAuth"
|
||||||
<FormItem>
|
render={({ field }) => (
|
||||||
<FormLabel className="flex flex-row items-center">
|
<FormItem>
|
||||||
Recipient action authentication
|
<FormLabel className="flex flex-row items-center">
|
||||||
<Tooltip>
|
Recipient action authentication
|
||||||
<TooltipTrigger>
|
<Tooltip>
|
||||||
<InfoIcon className="mx-2 h-4 w-4" />
|
<TooltipTrigger>
|
||||||
</TooltipTrigger>
|
<InfoIcon className="mx-2 h-4 w-4" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
|
||||||
<TooltipContent className="text-foreground max-w-md space-y-2 p-4">
|
<TooltipContent className="text-foreground max-w-md space-y-2 p-4">
|
||||||
<h2>
|
<h2>
|
||||||
<strong>Global recipient action authentication</strong>
|
<strong>Global recipient action authentication</strong>
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
The authentication required for recipients to sign fields and complete the
|
The authentication required for recipients to sign the signature field.
|
||||||
document.
|
</p>
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
This can be overriden by setting the authentication requirements directly
|
This can be overriden by setting the authentication requirements
|
||||||
on each recipient in the next step.
|
directly on each recipient in the next step.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<ul className="ml-3.5 list-outside list-disc space-y-0.5 py-2">
|
<ul className="ml-3.5 list-outside list-disc space-y-0.5 py-2">
|
||||||
<li>
|
<li>
|
||||||
<strong>Require account</strong> - The recipient must be signed in
|
<strong>Require account</strong> - The recipient must be signed in
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<strong>None</strong> - No authentication required
|
<strong>None</strong> - No authentication required
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
|
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Select {...field} onValueChange={field.onChange}>
|
<Select {...field} onValueChange={field.onChange}>
|
||||||
<SelectTrigger className="bg-background text-muted-foreground">
|
<SelectTrigger className="bg-background text-muted-foreground">
|
||||||
<SelectValue data-testid="documentActionSelectValue" placeholder="None" />
|
<SelectValue data-testid="documentActionSelectValue" placeholder="None" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
|
|
||||||
<SelectContent position="popper">
|
<SelectContent position="popper">
|
||||||
{Object.values(DocumentActionAuth).map((authType) => (
|
{Object.values(DocumentActionAuth).map((authType) => (
|
||||||
<SelectItem key={authType} value={authType}>
|
<SelectItem key={authType} value={authType}>
|
||||||
{DOCUMENT_AUTH_TYPES[authType].value}
|
{DOCUMENT_AUTH_TYPES[authType].value}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{/* Note: -1 is remapped in the Zod schema to the required value. */}
|
{/* Note: -1 is remapped in the Zod schema to the required value. */}
|
||||||
<SelectItem value={'-1'}>None</SelectItem>
|
<SelectItem value={'-1'}>None</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<Accordion type="multiple" className="mt-6">
|
<Accordion type="multiple" className="mt-6">
|
||||||
<AccordionItem value="advanced-options" className="border-none">
|
<AccordionItem value="advanced-options" className="border-none">
|
||||||
|
|||||||
@ -45,6 +45,7 @@ export type AddSignersFormProps = {
|
|||||||
documentFlow: DocumentFlowStep;
|
documentFlow: DocumentFlowStep;
|
||||||
recipients: Recipient[];
|
recipients: Recipient[];
|
||||||
fields: Field[];
|
fields: Field[];
|
||||||
|
isDocumentEnterprise: boolean;
|
||||||
onSubmit: (_data: TAddSignersFormSchema) => void;
|
onSubmit: (_data: TAddSignersFormSchema) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -52,6 +53,7 @@ export const AddSignersFormPartial = ({
|
|||||||
documentFlow,
|
documentFlow,
|
||||||
recipients,
|
recipients,
|
||||||
fields,
|
fields,
|
||||||
|
isDocumentEnterprise,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
}: AddSignersFormProps) => {
|
}: AddSignersFormProps) => {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
@ -243,14 +245,18 @@ export const AddSignersFormPartial = ({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{showAdvancedSettings && (
|
{showAdvancedSettings && isDocumentEnterprise && (
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name={`signers.${index}.actionAuth`}
|
name={`signers.${index}.actionAuth`}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="col-span-6">
|
<FormItem className="col-span-6">
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Select {...field} onValueChange={field.onChange}>
|
<Select
|
||||||
|
{...field}
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
disabled={isSubmitting || hasBeenSentToRecipientId(signer.nativeId)}
|
||||||
|
>
|
||||||
<SelectTrigger className="bg-background text-muted-foreground">
|
<SelectTrigger className="bg-background text-muted-foreground">
|
||||||
<SelectValue placeholder="Inherit authentication method" />
|
<SelectValue placeholder="Inherit authentication method" />
|
||||||
|
|
||||||
@ -396,7 +402,7 @@ export const AddSignersFormPartial = ({
|
|||||||
Add Signer
|
Add Signer
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{!alwaysShowAdvancedSettings && (
|
{!alwaysShowAdvancedSettings && isDocumentEnterprise && (
|
||||||
<div className="flex flex-row items-center">
|
<div className="flex flex-row items-center">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
id="showAdvancedRecipientSettings"
|
id="showAdvancedRecipientSettings"
|
||||||
|
|||||||
@ -67,6 +67,8 @@ services:
|
|||||||
sync: false
|
sync: false
|
||||||
- key: NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID
|
- key: NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID
|
||||||
sync: false
|
sync: false
|
||||||
|
- key: NEXT_PUBLIC_STRIPE_ENTERPRISE_PLAN_MONTHLY_PRICE_ID
|
||||||
|
sync: false
|
||||||
|
|
||||||
# Features
|
# Features
|
||||||
- key: NEXT_PUBLIC_POSTHOG_KEY
|
- key: NEXT_PUBLIC_POSTHOG_KEY
|
||||||
|
|||||||
@ -44,6 +44,7 @@
|
|||||||
"NEXT_PUBLIC_POSTHOG_KEY",
|
"NEXT_PUBLIC_POSTHOG_KEY",
|
||||||
"NEXT_PUBLIC_FEATURE_BILLING_ENABLED",
|
"NEXT_PUBLIC_FEATURE_BILLING_ENABLED",
|
||||||
"NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID",
|
"NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID",
|
||||||
|
"NEXT_PUBLIC_STRIPE_ENTERPRISE_PLAN_MONTHLY_PRICE_ID",
|
||||||
"NEXT_PUBLIC_DISABLE_SIGNUP",
|
"NEXT_PUBLIC_DISABLE_SIGNUP",
|
||||||
"NEXT_PUBLIC_DOCUMENT_SIZE_UPLOAD_LIMIT",
|
"NEXT_PUBLIC_DOCUMENT_SIZE_UPLOAD_LIMIT",
|
||||||
"NEXT_PRIVATE_DATABASE_URL",
|
"NEXT_PRIVATE_DATABASE_URL",
|
||||||
|
|||||||
Reference in New Issue
Block a user