mirror of
https://github.com/documenso/documenso.git
synced 2026-06-27 23:00:49 +10:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 08e5acd0b2 | |||
| 1b55b3f7ea |
@@ -0,0 +1,144 @@
|
||||
import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from '@documenso/ui/primitives/dialog';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { useState } from 'react';
|
||||
|
||||
export type DocumentPreferencesResetDialogProps = {
|
||||
disabled?: boolean;
|
||||
isSubmitting: boolean;
|
||||
onReset: () => Promise<void>;
|
||||
showAiFeatures?: boolean;
|
||||
showDocumentVisibility?: boolean;
|
||||
showIncludeSenderDetails?: boolean;
|
||||
trigger?: React.ReactNode;
|
||||
};
|
||||
|
||||
export const DocumentPreferencesResetDialog = ({
|
||||
disabled = false,
|
||||
isSubmitting,
|
||||
onReset,
|
||||
showAiFeatures = false,
|
||||
showDocumentVisibility = false,
|
||||
showIncludeSenderDetails = false,
|
||||
trigger,
|
||||
}: DocumentPreferencesResetDialogProps) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [isResetting, setIsResetting] = useState(false);
|
||||
|
||||
const isLoading = isSubmitting || isResetting;
|
||||
|
||||
const handleResetToDefaults = async () => {
|
||||
setIsResetting(true);
|
||||
|
||||
try {
|
||||
await onReset();
|
||||
setOpen(false);
|
||||
} finally {
|
||||
setIsResetting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={(value) => !isLoading && setOpen(value)}>
|
||||
<DialogTrigger asChild>
|
||||
{trigger ?? (
|
||||
<Button variant="destructive" type="button" disabled={disabled || isLoading}>
|
||||
<Trans>Reset</Trans>
|
||||
</Button>
|
||||
)}
|
||||
</DialogTrigger>
|
||||
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
<Trans>Reset document preferences</Trans>
|
||||
</DialogTitle>
|
||||
|
||||
<DialogDescription>
|
||||
<Trans>
|
||||
This will reset all document preferences to their default values and save the changes immediately.
|
||||
</Trans>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<Alert variant="warning">
|
||||
<AlertDescription>
|
||||
<p>
|
||||
<Trans>Once confirmed, the following will be reset:</Trans>
|
||||
</p>
|
||||
|
||||
<ul className="mt-0.5 list-inside list-disc">
|
||||
{showDocumentVisibility && (
|
||||
<li>
|
||||
<Trans>Default document visibility</Trans>
|
||||
</li>
|
||||
)}
|
||||
<li>
|
||||
<Trans>Default document language</Trans>
|
||||
</li>
|
||||
<li>
|
||||
<Trans>Default date format</Trans>
|
||||
</li>
|
||||
<li>
|
||||
<Trans>Default time zone</Trans>
|
||||
</li>
|
||||
<li>
|
||||
<Trans>Default signature settings</Trans>
|
||||
</li>
|
||||
{showIncludeSenderDetails && (
|
||||
<li>
|
||||
<Trans>Send on behalf of team</Trans>
|
||||
</li>
|
||||
)}
|
||||
<li>
|
||||
<Trans>Include the signing certificate in the document</Trans>
|
||||
</li>
|
||||
<li>
|
||||
<Trans>Include the audit logs in the document</Trans>
|
||||
</li>
|
||||
<li>
|
||||
<Trans>Default recipients</Trans>
|
||||
</li>
|
||||
<li>
|
||||
<Trans>Delegate document ownership</Trans>
|
||||
</li>
|
||||
<li>
|
||||
<Trans>Default envelope expiration</Trans>
|
||||
</li>
|
||||
<li>
|
||||
<Trans>Default signing reminders</Trans>
|
||||
</li>
|
||||
{showAiFeatures && (
|
||||
<li>
|
||||
<Trans>AI features</Trans>
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<DialogFooter>
|
||||
<DialogClose asChild>
|
||||
<Button type="button" variant="secondary" disabled={isLoading}>
|
||||
<Trans>Cancel</Trans>
|
||||
</Button>
|
||||
</DialogClose>
|
||||
|
||||
<Button type="button" variant="destructive" loading={isLoading} onClick={() => void handleResetToDefaults()}>
|
||||
<Trans>Reset to defaults</Trans>
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -11,10 +11,10 @@ import { isValidLanguageCode, SUPPORTED_LANGUAGE_CODES, SUPPORTED_LANGUAGES } fr
|
||||
import { TIME_ZONES } from '@documenso/lib/constants/time-zones';
|
||||
import type { TDefaultRecipients } from '@documenso/lib/types/default-recipients';
|
||||
import { ZDefaultRecipientsSchema } from '@documenso/lib/types/default-recipients';
|
||||
import { type TDocumentMetaDateFormat, ZDocumentMetaTimezoneSchema } from '@documenso/lib/types/document-meta';
|
||||
import { isPersonalLayout } from '@documenso/lib/utils/organisations';
|
||||
import { type TDocumentMetaDateFormat, ZDocumentMetaDateFormatSchema } from '@documenso/lib/types/document-meta';
|
||||
import { generateDefaultOrganisationSettings, isPersonalLayout } from '@documenso/lib/utils/organisations';
|
||||
import { recipientAbbreviation } from '@documenso/lib/utils/recipient-formatter';
|
||||
import { extractTeamSignatureSettings } from '@documenso/lib/utils/teams';
|
||||
import { extractTeamSignatureSettings, generateDefaultTeamSettings } from '@documenso/lib/utils/teams';
|
||||
import { DocumentSignatureSettingsTooltip } from '@documenso/ui/components/document/document-signature-settings-tooltip';
|
||||
import { ExpirationPeriodPicker } from '@documenso/ui/components/document/expiration-period-picker';
|
||||
import { ReminderSettingsPicker } from '@documenso/ui/components/document/reminder-settings-picker';
|
||||
@@ -38,11 +38,11 @@ import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { msg, t } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import type { TeamGlobalSettings } from '@prisma/client';
|
||||
import { DocumentVisibility, OrganisationType, type RecipientRole } from '@prisma/client';
|
||||
import { DocumentVisibility, OrganisationType, type RecipientRole, type TeamGlobalSettings } from '@prisma/client';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { DocumentPreferencesResetDialog } from '~/components/dialogs/document-preferences-reset-dialog';
|
||||
import { useOptionalCurrentTeam } from '~/providers/team';
|
||||
|
||||
import { DefaultRecipientsMultiSelectCombobox } from '../general/default-recipients-multiselect-combobox';
|
||||
@@ -93,6 +93,26 @@ export type DocumentPreferencesFormProps = {
|
||||
onFormSubmit: (data: TDocumentPreferencesFormSchema) => Promise<void>;
|
||||
};
|
||||
|
||||
const getDocumentPreferencesFormValues = (settings: SettingsSubset): TDocumentPreferencesFormSchema => {
|
||||
const parsedDocumentDateFormat = ZDocumentMetaDateFormatSchema.safeParse(settings.documentDateFormat);
|
||||
|
||||
return {
|
||||
documentVisibility: settings.documentVisibility,
|
||||
documentLanguage: isValidLanguageCode(settings.documentLanguage) ? settings.documentLanguage : null,
|
||||
documentTimezone: settings.documentTimezone,
|
||||
documentDateFormat: parsedDocumentDateFormat.success ? parsedDocumentDateFormat.data : null,
|
||||
includeSenderDetails: settings.includeSenderDetails,
|
||||
includeSigningCertificate: settings.includeSigningCertificate,
|
||||
includeAuditLog: settings.includeAuditLog,
|
||||
signatureTypes: extractTeamSignatureSettings({ ...settings }),
|
||||
defaultRecipients: settings.defaultRecipients ? ZDefaultRecipientsSchema.parse(settings.defaultRecipients) : null,
|
||||
delegateDocumentOwnership: settings.delegateDocumentOwnership,
|
||||
aiFeaturesEnabled: settings.aiFeaturesEnabled,
|
||||
envelopeExpirationPeriod: settings.envelopeExpirationPeriod ?? null,
|
||||
reminderSettings: settings.reminderSettings ?? null,
|
||||
};
|
||||
};
|
||||
|
||||
export const DocumentPreferencesForm = ({
|
||||
settings,
|
||||
onFormSubmit,
|
||||
@@ -113,7 +133,7 @@ export const DocumentPreferencesForm = ({
|
||||
documentVisibility: z.nativeEnum(DocumentVisibility).nullable(),
|
||||
documentLanguage: z.enum(SUPPORTED_LANGUAGE_CODES).nullable(),
|
||||
documentTimezone: z.string().nullable(),
|
||||
documentDateFormat: ZDocumentMetaTimezoneSchema.nullable(),
|
||||
documentDateFormat: ZDocumentMetaDateFormatSchema.nullable(),
|
||||
includeSenderDetails: z.boolean().nullable(),
|
||||
includeSigningCertificate: z.boolean().nullable(),
|
||||
includeAuditLog: z.boolean().nullable(),
|
||||
@@ -127,26 +147,27 @@ export const DocumentPreferencesForm = ({
|
||||
reminderSettings: ZEnvelopeReminderSettings.nullable(),
|
||||
});
|
||||
|
||||
const defaultValues = getDocumentPreferencesFormValues(settings);
|
||||
const defaultSettings = canInherit ? generateDefaultTeamSettings() : generateDefaultOrganisationSettings();
|
||||
const baseResetValues = getDocumentPreferencesFormValues(defaultSettings);
|
||||
const resetValues = {
|
||||
...baseResetValues,
|
||||
aiFeaturesEnabled: isAiFeaturesConfigured ? baseResetValues.aiFeaturesEnabled : defaultValues.aiFeaturesEnabled,
|
||||
};
|
||||
|
||||
const form = useForm<TDocumentPreferencesFormSchema>({
|
||||
defaultValues: {
|
||||
documentVisibility: settings.documentVisibility,
|
||||
documentLanguage: isValidLanguageCode(settings.documentLanguage) ? settings.documentLanguage : null,
|
||||
documentTimezone: settings.documentTimezone,
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
documentDateFormat: settings.documentDateFormat as TDocumentMetaDateFormat | null,
|
||||
includeSenderDetails: settings.includeSenderDetails,
|
||||
includeSigningCertificate: settings.includeSigningCertificate,
|
||||
includeAuditLog: settings.includeAuditLog,
|
||||
signatureTypes: extractTeamSignatureSettings({ ...settings }),
|
||||
defaultRecipients: settings.defaultRecipients ? ZDefaultRecipientsSchema.parse(settings.defaultRecipients) : null,
|
||||
delegateDocumentOwnership: settings.delegateDocumentOwnership,
|
||||
aiFeaturesEnabled: settings.aiFeaturesEnabled,
|
||||
envelopeExpirationPeriod: settings.envelopeExpirationPeriod ?? null,
|
||||
reminderSettings: settings.reminderSettings ?? null,
|
||||
},
|
||||
defaultValues,
|
||||
resolver: zodResolver(ZDocumentPreferencesFormSchema),
|
||||
});
|
||||
|
||||
const currentValues = form.watch();
|
||||
const isResetDisabled = !form.formState.isDirty && JSON.stringify(currentValues) === JSON.stringify(resetValues);
|
||||
|
||||
const handleResetToDefaults = async () => {
|
||||
await onFormSubmit(resetValues);
|
||||
form.reset(resetValues);
|
||||
};
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onFormSubmit)}>
|
||||
@@ -760,6 +781,14 @@ export const DocumentPreferencesForm = ({
|
||||
<Button type="submit" loading={form.formState.isSubmitting}>
|
||||
<Trans>Update</Trans>
|
||||
</Button>
|
||||
<DocumentPreferencesResetDialog
|
||||
disabled={isResetDisabled}
|
||||
isSubmitting={form.formState.isSubmitting}
|
||||
onReset={handleResetToDefaults}
|
||||
showAiFeatures={isAiFeaturesConfigured}
|
||||
showDocumentVisibility={!isPersonalLayoutMode}
|
||||
showIncludeSenderDetails={!isPersonalLayoutMode && !isPersonalOrganisation}
|
||||
/>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
|
||||
@@ -146,7 +146,7 @@ export const downloadRoute = new Hono<HonoEnv>()
|
||||
* Requires API key authentication via Authorization header.
|
||||
*/
|
||||
.get(
|
||||
'/envelope/:envelopeId/audit-log/download',
|
||||
'/envelope/:envelopeId/audit-log/pdf',
|
||||
sValidator('param', ZDownloadEnvelopeAuditLogPdfRequestParamsSchema),
|
||||
async (c) => {
|
||||
const logger = c.get('logger');
|
||||
@@ -220,7 +220,7 @@ export const downloadRoute = new Hono<HonoEnv>()
|
||||
* Requires API key authentication via Authorization header.
|
||||
*/
|
||||
.get(
|
||||
'/envelope/:envelopeId/certificate/download',
|
||||
'/envelope/:envelopeId/certificate/pdf',
|
||||
sValidator('param', ZDownloadEnvelopeCertificatePdfRequestParamsSchema),
|
||||
async (c) => {
|
||||
const logger = c.get('logger');
|
||||
|
||||
-284
@@ -1,284 +0,0 @@
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
||||
import { hashString } from '@documenso/lib/server-only/auth/hash';
|
||||
import { createApiToken } from '@documenso/lib/server-only/public-api/create-api-token';
|
||||
import { alphaid } from '@documenso/lib/universal/id';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { DocumentVisibility, TeamMemberRole } from '@documenso/prisma/client';
|
||||
import { seedCompletedDocument } from '@documenso/prisma/seed/documents';
|
||||
import { seedTeam, seedTeamMember } from '@documenso/prisma/seed/teams';
|
||||
import { seedUser } from '@documenso/prisma/seed/users';
|
||||
import { type APIRequestContext, expect, test } from '@playwright/test';
|
||||
import type { Team, User } from '@prisma/client';
|
||||
|
||||
const WEBAPP_BASE_URL = NEXT_PUBLIC_WEBAPP_URL();
|
||||
const API_BASE_URL = `${WEBAPP_BASE_URL}/api/v2-beta`;
|
||||
|
||||
/**
|
||||
* Create an API token directly, bypassing the role check in `createApiToken`.
|
||||
*
|
||||
* This simulates a token that was minted while the user had permission, and which
|
||||
* survives a later downgrade to a lower team role (e.g. MEMBER). Such a token must
|
||||
* still respect document visibility at request time.
|
||||
*/
|
||||
const seedApiTokenForUser = async ({
|
||||
userId,
|
||||
teamId,
|
||||
tokenName,
|
||||
}: {
|
||||
userId: number;
|
||||
teamId: number;
|
||||
tokenName: string;
|
||||
}) => {
|
||||
const token = `api_${alphaid(16)}`;
|
||||
|
||||
await prisma.apiToken.create({
|
||||
data: {
|
||||
name: tokenName,
|
||||
token: hashString(token),
|
||||
expires: null,
|
||||
userId,
|
||||
teamId,
|
||||
},
|
||||
});
|
||||
|
||||
return { token };
|
||||
};
|
||||
|
||||
test.describe.configure({
|
||||
mode: 'parallel',
|
||||
});
|
||||
|
||||
const downloadAuditLogPdf = (request: APIRequestContext, envelopeId: string, authToken?: string) => {
|
||||
return request.get(`${API_BASE_URL}/envelope/${envelopeId}/audit-log/download`, {
|
||||
headers: authToken ? { Authorization: `Bearer ${authToken}` } : {},
|
||||
});
|
||||
};
|
||||
|
||||
const downloadCertificatePdf = (request: APIRequestContext, envelopeId: string, authToken?: string) => {
|
||||
return request.get(`${API_BASE_URL}/envelope/${envelopeId}/certificate/download`, {
|
||||
headers: authToken ? { Authorization: `Bearer ${authToken}` } : {},
|
||||
});
|
||||
};
|
||||
|
||||
test.describe('Envelope certificate / audit log PDF download API V2 - access control', () => {
|
||||
let userA: User, teamA: Team, userB: User, teamB: Team, tokenA: string, tokenB: string;
|
||||
|
||||
test.beforeEach(async () => {
|
||||
({ user: userA, team: teamA } = await seedUser());
|
||||
({ token: tokenA } = await createApiToken({
|
||||
userId: userA.id,
|
||||
teamId: teamA.id,
|
||||
tokenName: 'userA',
|
||||
expiresIn: null,
|
||||
}));
|
||||
|
||||
({ user: userB, team: teamB } = await seedUser());
|
||||
({ token: tokenB } = await createApiToken({
|
||||
userId: userB.id,
|
||||
teamId: teamB.id,
|
||||
tokenName: 'userB',
|
||||
expiresIn: null,
|
||||
}));
|
||||
});
|
||||
|
||||
test('should reject audit log download without an API token', async ({ request }) => {
|
||||
const document = await seedCompletedDocument(userA, teamA.id, ['recipient@test.documenso.com']);
|
||||
|
||||
const res = await downloadAuditLogPdf(request, document.id);
|
||||
|
||||
expect(res.ok()).toBeFalsy();
|
||||
expect(res.status()).toBe(401);
|
||||
});
|
||||
|
||||
test('should reject certificate download without an API token', async ({ request }) => {
|
||||
const document = await seedCompletedDocument(userA, teamA.id, ['recipient@test.documenso.com']);
|
||||
|
||||
const res = await downloadCertificatePdf(request, document.id);
|
||||
|
||||
expect(res.ok()).toBeFalsy();
|
||||
expect(res.status()).toBe(401);
|
||||
});
|
||||
|
||||
test('should reject audit log download from a user in a different team', async ({ request }) => {
|
||||
const document = await seedCompletedDocument(userA, teamA.id, ['recipient@test.documenso.com']);
|
||||
|
||||
const res = await downloadAuditLogPdf(request, document.id, tokenB);
|
||||
|
||||
expect(res.ok()).toBeFalsy();
|
||||
expect(res.status()).toBe(404);
|
||||
});
|
||||
|
||||
test('should reject certificate download from a user in a different team', async ({ request }) => {
|
||||
const document = await seedCompletedDocument(userA, teamA.id, ['recipient@test.documenso.com']);
|
||||
|
||||
const res = await downloadCertificatePdf(request, document.id, tokenB);
|
||||
|
||||
expect(res.ok()).toBeFalsy();
|
||||
expect(res.status()).toBe(404);
|
||||
});
|
||||
|
||||
test('should reject a disabled user downloading the audit log', async ({ request }) => {
|
||||
const document = await seedCompletedDocument(userA, teamA.id, ['recipient@test.documenso.com']);
|
||||
|
||||
await prisma.user.update({
|
||||
where: { id: userA.id },
|
||||
data: { disabled: true },
|
||||
});
|
||||
|
||||
const res = await downloadAuditLogPdf(request, document.id, tokenA);
|
||||
|
||||
expect(res.ok()).toBeFalsy();
|
||||
expect(res.status()).toBe(401);
|
||||
});
|
||||
|
||||
test('should reject a disabled user downloading the certificate', async ({ request }) => {
|
||||
const document = await seedCompletedDocument(userA, teamA.id, ['recipient@test.documenso.com']);
|
||||
|
||||
await prisma.user.update({
|
||||
where: { id: userA.id },
|
||||
data: { disabled: true },
|
||||
});
|
||||
|
||||
const res = await downloadCertificatePdf(request, document.id, tokenA);
|
||||
|
||||
expect(res.ok()).toBeFalsy();
|
||||
expect(res.status()).toBe(401);
|
||||
});
|
||||
|
||||
test('should return 404 for a non-existent envelope id', async ({ request }) => {
|
||||
const auditLogRes = await downloadAuditLogPdf(request, 'envelope_doesnotexist', tokenA);
|
||||
expect(auditLogRes.status()).toBe(404);
|
||||
|
||||
const certificateRes = await downloadCertificatePdf(request, 'envelope_doesnotexist', tokenA);
|
||||
expect(certificateRes.status()).toBe(404);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Envelope certificate / audit log PDF download API V2 - document visibility', () => {
|
||||
test.describe.configure({
|
||||
mode: 'parallel',
|
||||
});
|
||||
|
||||
test('should hide an ADMIN-only document from a downgraded member (audit log)', async ({ request }) => {
|
||||
const { team, owner } = await seedTeam();
|
||||
const member = await seedTeamMember({ teamId: team.id, role: TeamMemberRole.MEMBER });
|
||||
|
||||
const { token: memberToken } = await seedApiTokenForUser({
|
||||
userId: member.id,
|
||||
teamId: team.id,
|
||||
tokenName: 'member-audit-log-token',
|
||||
});
|
||||
|
||||
// ADMIN-visibility document owned by the team owner - a member must not see it.
|
||||
const document = await seedCompletedDocument(owner, team.id, ['recipient@test.documenso.com'], {
|
||||
createDocumentOptions: { visibility: DocumentVisibility.ADMIN },
|
||||
});
|
||||
|
||||
const res = await downloadAuditLogPdf(request, document.id, memberToken);
|
||||
|
||||
// Visibility failure surfaces as not-found, matching the canonical access checks.
|
||||
expect(res.ok()).toBeFalsy();
|
||||
expect(res.status()).toBe(404);
|
||||
});
|
||||
|
||||
test('should hide an ADMIN-only document from a downgraded member (certificate)', async ({ request }) => {
|
||||
const { team, owner } = await seedTeam();
|
||||
const member = await seedTeamMember({ teamId: team.id, role: TeamMemberRole.MEMBER });
|
||||
|
||||
const { token: memberToken } = await seedApiTokenForUser({
|
||||
userId: member.id,
|
||||
teamId: team.id,
|
||||
tokenName: 'member-certificate-token',
|
||||
});
|
||||
|
||||
const document = await seedCompletedDocument(owner, team.id, ['recipient@test.documenso.com'], {
|
||||
createDocumentOptions: { visibility: DocumentVisibility.ADMIN },
|
||||
});
|
||||
|
||||
const res = await downloadCertificatePdf(request, document.id, memberToken);
|
||||
|
||||
expect(res.ok()).toBeFalsy();
|
||||
expect(res.status()).toBe(404);
|
||||
});
|
||||
|
||||
test('should hide a MANAGER_AND_ABOVE document from a downgraded member (audit log)', async ({ request }) => {
|
||||
const { team, owner } = await seedTeam();
|
||||
const member = await seedTeamMember({ teamId: team.id, role: TeamMemberRole.MEMBER });
|
||||
|
||||
const { token: memberToken } = await seedApiTokenForUser({
|
||||
userId: member.id,
|
||||
teamId: team.id,
|
||||
tokenName: 'member-manager-vis-token',
|
||||
});
|
||||
|
||||
const document = await seedCompletedDocument(owner, team.id, ['recipient@test.documenso.com'], {
|
||||
createDocumentOptions: { visibility: DocumentVisibility.MANAGER_AND_ABOVE },
|
||||
});
|
||||
|
||||
const res = await downloadAuditLogPdf(request, document.id, memberToken);
|
||||
|
||||
expect(res.ok()).toBeFalsy();
|
||||
expect(res.status()).toBe(404);
|
||||
});
|
||||
|
||||
test('should hide a MANAGER_AND_ABOVE document from a downgraded member (certificate)', async ({ request }) => {
|
||||
const { team, owner } = await seedTeam();
|
||||
const member = await seedTeamMember({ teamId: team.id, role: TeamMemberRole.MEMBER });
|
||||
|
||||
const { token: memberToken } = await seedApiTokenForUser({
|
||||
userId: member.id,
|
||||
teamId: team.id,
|
||||
tokenName: 'member-manager-vis-cert-token',
|
||||
});
|
||||
|
||||
const document = await seedCompletedDocument(owner, team.id, ['recipient@test.documenso.com'], {
|
||||
createDocumentOptions: { visibility: DocumentVisibility.MANAGER_AND_ABOVE },
|
||||
});
|
||||
|
||||
const res = await downloadCertificatePdf(request, document.id, memberToken);
|
||||
|
||||
expect(res.ok()).toBeFalsy();
|
||||
expect(res.status()).toBe(404);
|
||||
});
|
||||
|
||||
test('should hide an ADMIN-only document from a downgraded manager (certificate)', async ({ request }) => {
|
||||
const { team, owner } = await seedTeam();
|
||||
const manager = await seedTeamMember({ teamId: team.id, role: TeamMemberRole.MANAGER });
|
||||
|
||||
const { token: managerToken } = await seedApiTokenForUser({
|
||||
userId: manager.id,
|
||||
teamId: team.id,
|
||||
tokenName: 'manager-admin-vis-cert-token',
|
||||
});
|
||||
|
||||
const document = await seedCompletedDocument(owner, team.id, ['recipient@test.documenso.com'], {
|
||||
createDocumentOptions: { visibility: DocumentVisibility.ADMIN },
|
||||
});
|
||||
|
||||
const res = await downloadCertificatePdf(request, document.id, managerToken);
|
||||
|
||||
expect(res.ok()).toBeFalsy();
|
||||
expect(res.status()).toBe(404);
|
||||
});
|
||||
|
||||
test('should allow a member to download an EVERYONE-visibility document (audit log)', async ({ request }) => {
|
||||
const { team, owner } = await seedTeam();
|
||||
const member = await seedTeamMember({ teamId: team.id, role: TeamMemberRole.MEMBER });
|
||||
|
||||
const { token: memberToken } = await seedApiTokenForUser({
|
||||
userId: member.id,
|
||||
teamId: team.id,
|
||||
tokenName: 'member-everyone-vis-token',
|
||||
});
|
||||
|
||||
const document = await seedCompletedDocument(owner, team.id, ['recipient@test.documenso.com'], {
|
||||
createDocumentOptions: { visibility: DocumentVisibility.EVERYONE },
|
||||
});
|
||||
|
||||
const res = await downloadAuditLogPdf(request, document.id, memberToken);
|
||||
|
||||
expect(res.ok()).toBeTruthy();
|
||||
expect(res.status()).toBe(200);
|
||||
expect(res.headers()['content-type']).toContain('application/pdf');
|
||||
});
|
||||
});
|
||||
@@ -5,7 +5,7 @@ import type { TrpcRouteMeta } from '../trpc';
|
||||
export const downloadEnvelopeAuditLogPdfMeta: TrpcRouteMeta = {
|
||||
openapi: {
|
||||
method: 'GET',
|
||||
path: '/envelope/{envelopeId}/audit-log/download',
|
||||
path: '/envelope/{envelopeId}/audit-log/pdf',
|
||||
summary: 'Download envelope audit log PDF',
|
||||
description: 'Download the audit log for a document as a PDF.',
|
||||
tags: ['Envelope'],
|
||||
|
||||
@@ -5,7 +5,7 @@ import type { TrpcRouteMeta } from '../trpc';
|
||||
export const downloadEnvelopeCertificatePdfMeta: TrpcRouteMeta = {
|
||||
openapi: {
|
||||
method: 'GET',
|
||||
path: '/envelope/{envelopeId}/certificate/download',
|
||||
path: '/envelope/{envelopeId}/certificate/pdf',
|
||||
summary: 'Download envelope certificate PDF',
|
||||
description: 'Download the signing certificate for a completed document as a PDF.',
|
||||
tags: ['Envelope'],
|
||||
|
||||
Reference in New Issue
Block a user