Merge branch 'main' into feat/customize-doc-audit-log-certificate

This commit is contained in:
Catalin Pit
2025-02-24 09:36:31 +02:00
committed by GitHub
10 changed files with 140 additions and 29 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "@documenso/web", "name": "@documenso/web",
"version": "1.9.1-rc.1", "version": "1.9.1-rc.2",
"private": true, "private": true,
"license": "AGPL-3.0", "license": "AGPL-3.0",
"scripts": { "scripts": {

View File

@ -43,9 +43,10 @@ type TRejectDocumentFormSchema = z.infer<typeof ZRejectDocumentFormSchema>;
export interface RejectDocumentDialogProps { export interface RejectDocumentDialogProps {
document: Pick<Document, 'id'>; document: Pick<Document, 'id'>;
token: string; token: string;
onRejected?: (reason: string) => void | Promise<void>;
} }
export function RejectDocumentDialog({ document, token }: RejectDocumentDialogProps) { export function RejectDocumentDialog({ document, token, onRejected }: RejectDocumentDialogProps) {
const { toast } = useToast(); const { toast } = useToast();
const router = useRouter(); const router = useRouter();
const searchParams = useSearchParams(); const searchParams = useSearchParams();
@ -79,7 +80,11 @@ export function RejectDocumentDialog({ document, token }: RejectDocumentDialogPr
setIsOpen(false); setIsOpen(false);
router.push(`/sign/${token}/rejected`); if (onRejected) {
await onRejected(reason);
} else {
router.push(`/sign/${token}/rejected`);
}
} catch (err) { } catch (err) {
toast({ toast({
title: 'Error', title: 'Error',

View File

@ -0,0 +1,40 @@
import { Trans } from '@lingui/macro';
import { XCircle } from 'lucide-react';
import type { Signature } from '@documenso/prisma/client';
export type EmbedDocumentRejectedPageProps = {
name?: string;
signature?: Signature;
};
export const EmbedDocumentRejected = ({ name }: EmbedDocumentRejectedPageProps) => {
return (
<div className="embed--DocumentRejected relative mx-auto flex min-h-[100dvh] max-w-screen-lg flex-col items-center justify-center p-6">
<div className="flex flex-col items-center">
<div className="flex items-center gap-x-4">
<XCircle className="text-destructive h-10 w-10" />
<h2 className="max-w-[35ch] text-center text-2xl font-semibold leading-normal md:text-3xl lg:text-4xl">
<Trans>Document Rejected</Trans>
</h2>
</div>
<div className="text-destructive mt-4 flex items-center text-center text-sm">
<Trans>You have rejected this document</Trans>
</div>
<p className="text-muted-foreground mt-6 max-w-[60ch] text-center text-sm">
<Trans>
The document owner has been notified of your decision. They may contact you with further
instructions if necessary.
</Trans>
</p>
<p className="text-muted-foreground mt-2 max-w-[60ch] text-center text-sm">
<Trans>No further action is required from you at this time.</Trans>
</p>
</div>
</div>
);
};

View File

@ -10,7 +10,13 @@ import { useThrottleFn } from '@documenso/lib/client-only/hooks/use-throttle-fn'
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer'; import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
import { validateFieldsInserted } from '@documenso/lib/utils/fields'; import { validateFieldsInserted } from '@documenso/lib/utils/fields';
import type { DocumentMeta, TemplateMeta } from '@documenso/prisma/client'; import type { DocumentMeta, TemplateMeta } from '@documenso/prisma/client';
import { type DocumentData, type Field, FieldType, RecipientRole } from '@documenso/prisma/client'; import {
type DocumentData,
type Field,
FieldType,
RecipientRole,
SigningStatus,
} from '@documenso/prisma/client';
import type { RecipientWithFields } from '@documenso/prisma/types/recipient-with-fields'; import type { RecipientWithFields } from '@documenso/prisma/types/recipient-with-fields';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import { FieldToolTip } from '@documenso/ui/components/field/field-tooltip'; import { FieldToolTip } from '@documenso/ui/components/field/field-tooltip';
@ -26,11 +32,13 @@ import { useToast } from '@documenso/ui/primitives/use-toast';
import { useRequiredSigningContext } from '~/app/(signing)/sign/[token]/provider'; import { useRequiredSigningContext } from '~/app/(signing)/sign/[token]/provider';
import { RecipientProvider } from '~/app/(signing)/sign/[token]/recipient-context'; import { RecipientProvider } from '~/app/(signing)/sign/[token]/recipient-context';
import { RejectDocumentDialog } from '~/app/(signing)/sign/[token]/reject-document-dialog';
import { Logo } from '~/components/branding/logo'; import { Logo } from '~/components/branding/logo';
import { EmbedClientLoading } from '../../client-loading'; import { EmbedClientLoading } from '../../client-loading';
import { EmbedDocumentCompleted } from '../../completed'; import { EmbedDocumentCompleted } from '../../completed';
import { EmbedDocumentFields } from '../../document-fields'; import { EmbedDocumentFields } from '../../document-fields';
import { EmbedDocumentRejected } from '../../rejected';
import { injectCss } from '../../util'; import { injectCss } from '../../util';
import { ZSignDocumentEmbedDataSchema } from './schema'; import { ZSignDocumentEmbedDataSchema } from './schema';
@ -75,6 +83,9 @@ export const EmbedSignDocumentClientPage = ({
const [hasFinishedInit, setHasFinishedInit] = useState(false); const [hasFinishedInit, setHasFinishedInit] = useState(false);
const [hasDocumentLoaded, setHasDocumentLoaded] = useState(false); const [hasDocumentLoaded, setHasDocumentLoaded] = useState(false);
const [hasCompletedDocument, setHasCompletedDocument] = useState(isCompleted); const [hasCompletedDocument, setHasCompletedDocument] = useState(isCompleted);
const [hasRejectedDocument, setHasRejectedDocument] = useState(
recipient.signingStatus === SigningStatus.REJECTED,
);
const [selectedSignerId, setSelectedSignerId] = useState<number | null>( const [selectedSignerId, setSelectedSignerId] = useState<number | null>(
allRecipients.length > 0 ? allRecipients[0].id : null, allRecipients.length > 0 ? allRecipients[0].id : null,
); );
@ -83,6 +94,8 @@ export const EmbedSignDocumentClientPage = ({
const [isNameLocked, setIsNameLocked] = useState(false); const [isNameLocked, setIsNameLocked] = useState(false);
const [showPendingFieldTooltip, setShowPendingFieldTooltip] = useState(false); const [showPendingFieldTooltip, setShowPendingFieldTooltip] = useState(false);
const [allowDocumentRejection, setAllowDocumentRejection] = useState(false);
const selectedSigner = allRecipients.find((r) => r.id === selectedSignerId); const selectedSigner = allRecipients.find((r) => r.id === selectedSignerId);
const isAssistantMode = recipient.role === RecipientRole.ASSISTANT; const isAssistantMode = recipient.role === RecipientRole.ASSISTANT;
@ -161,6 +174,25 @@ export const EmbedSignDocumentClientPage = ({
} }
}; };
const onDocumentRejected = (reason: string) => {
if (window.parent) {
window.parent.postMessage(
{
action: 'document-rejected',
data: {
token,
documentId,
recipientId: recipient.id,
reason,
},
},
'*',
);
}
setHasRejectedDocument(true);
};
useLayoutEffect(() => { useLayoutEffect(() => {
const hash = window.location.hash.slice(1); const hash = window.location.hash.slice(1);
@ -174,6 +206,7 @@ export const EmbedSignDocumentClientPage = ({
// Since a recipient can be provided a name we can lock it without requiring // Since a recipient can be provided a name we can lock it without requiring
// a to be provided by the parent application, unlike direct templates. // a to be provided by the parent application, unlike direct templates.
setIsNameLocked(!!data.lockName); setIsNameLocked(!!data.lockName);
setAllowDocumentRejection(!!data.allowDocumentRejection);
if (data.darkModeDisabled) { if (data.darkModeDisabled) {
document.documentElement.classList.add('dark-mode-disabled'); document.documentElement.classList.add('dark-mode-disabled');
@ -208,6 +241,10 @@ export const EmbedSignDocumentClientPage = ({
} }
}, [hasFinishedInit, hasDocumentLoaded]); }, [hasFinishedInit, hasDocumentLoaded]);
if (hasRejectedDocument) {
return <EmbedDocumentRejected name={fullName} />;
}
if (hasCompletedDocument) { if (hasCompletedDocument) {
return ( return (
<EmbedDocumentCompleted <EmbedDocumentCompleted
@ -229,6 +266,16 @@ export const EmbedSignDocumentClientPage = ({
<div className="embed--Root relative mx-auto flex min-h-[100dvh] max-w-screen-lg flex-col items-center justify-center p-6"> <div className="embed--Root relative mx-auto flex min-h-[100dvh] max-w-screen-lg flex-col items-center justify-center p-6">
{(!hasFinishedInit || !hasDocumentLoaded) && <EmbedClientLoading />} {(!hasFinishedInit || !hasDocumentLoaded) && <EmbedClientLoading />}
{allowDocumentRejection && (
<div className="embed--Actions mb-4 flex w-full flex-row-reverse items-baseline justify-between">
<RejectDocumentDialog
document={{ id: documentId }}
token={token}
onRejected={onDocumentRejected}
/>
</div>
)}
<div className="embed--DocumentContainer relative flex w-full flex-col gap-x-6 gap-y-12 md:flex-row"> <div className="embed--DocumentContainer relative flex w-full flex-col gap-x-6 gap-y-12 md:flex-row">
{/* Viewer */} {/* Viewer */}
<div className="embed--DocumentViewer flex-1"> <div className="embed--DocumentViewer flex-1">
@ -420,7 +467,7 @@ export const EmbedSignDocumentClientPage = ({
</Button> </Button>
) : ( ) : (
<Button <Button
className="col-start-2" className={allowDocumentRejection ? 'col-start-2' : 'col-span-2'}
disabled={ disabled={
isThrottled || (!isAssistantMode && hasSignatureField && !signatureValid) isThrottled || (!isAssistantMode && hasSignatureField && !signatureValid)
} }

View File

@ -13,4 +13,5 @@ export const ZSignDocumentEmbedDataSchema = ZBaseEmbedDataSchema.extend({
.optional() .optional()
.transform((value) => value || undefined), .transform((value) => value || undefined),
lockName: z.boolean().optional().default(false), lockName: z.boolean().optional().default(false),
allowDocumentRejection: z.boolean().optional(),
}); });

21
package-lock.json generated
View File

@ -1,12 +1,12 @@
{ {
"name": "@documenso/root", "name": "@documenso/root",
"version": "1.9.1-rc.1", "version": "1.9.1-rc.2",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@documenso/root", "name": "@documenso/root",
"version": "1.9.1-rc.1", "version": "1.9.1-rc.2",
"workspaces": [ "workspaces": [
"apps/*", "apps/*",
"packages/*" "packages/*"
@ -106,7 +106,7 @@
}, },
"apps/web": { "apps/web": {
"name": "@documenso/web", "name": "@documenso/web",
"version": "1.9.1-rc.1", "version": "1.9.1-rc.2",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"dependencies": { "dependencies": {
"@documenso/api": "*", "@documenso/api": "*",
@ -35722,21 +35722,6 @@
"engines": { "engines": {
"node": ">=6" "node": ">=6"
} }
},
"packages/trpc/node_modules/@next/swc-win32-ia32-msvc": {
"version": "14.2.6",
"resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.6.tgz",
"integrity": "sha512-hNukAxq7hu4o5/UjPp5jqoBEtrpCbOmnUqZSKNJG8GrUVzfq0ucdhQFVrHcLRMvQcwqqDh1a5AJN9ORnNDpgBQ==",
"cpu": [
"ia32"
],
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
} }
} }
} }

View File

@ -1,6 +1,6 @@
{ {
"private": true, "private": true,
"version": "1.9.1-rc.1", "version": "1.9.1-rc.2",
"scripts": { "scripts": {
"build": "turbo run build", "build": "turbo run build",
"build:web": "turbo run build --filter=@documenso/web", "build:web": "turbo run build --filter=@documenso/web",

View File

@ -12,6 +12,7 @@ import {
WebhookTriggerEvents, WebhookTriggerEvents,
} from '@documenso/prisma/client'; } from '@documenso/prisma/client';
import { AppError, AppErrorCode } from '../../errors/app-error';
import { jobs } from '../../jobs/client'; import { jobs } from '../../jobs/client';
import type { TRecipientActionAuth } from '../../types/document-auth'; import type { TRecipientActionAuth } from '../../types/document-auth';
import { import {
@ -72,6 +73,13 @@ export const completeDocumentWithToken = async ({
throw new Error(`Recipient ${recipient.id} has already signed`); throw new Error(`Recipient ${recipient.id} has already signed`);
} }
if (recipient.signingStatus === SigningStatus.REJECTED) {
throw new AppError(AppErrorCode.UNKNOWN_ERROR, {
message: 'Recipient has already rejected the document',
statusCode: 400,
});
}
if (document.documentMeta?.signingOrder === DocumentSigningOrder.SEQUENTIAL) { if (document.documentMeta?.signingOrder === DocumentSigningOrder.SEQUENTIAL) {
const isRecipientsTurn = await getIsRecipientsTurnToSign({ token: recipient.token }); const isRecipientsTurn = await getIsRecipientsTurnToSign({ token: recipient.token });

View File

@ -0,0 +1,18 @@
/*
Warnings:
- You are about to drop the column `expires` on the `Session` table. All the data in the column will be lost.
- Added the required column `expiresAt` to the `Session` table without a default value. This is not possible if the table is not empty.
- Added the required column `updatedAt` to the `Session` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE "Account" ADD COLUMN "password" TEXT;
-- AlterTable
ALTER TABLE "Session" DROP COLUMN "expires",
ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN "expiresAt" TIMESTAMP(3) NOT NULL,
ADD COLUMN "ipAddress" TEXT,
ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL,
ADD COLUMN "userAgent" TEXT;

View File

@ -270,18 +270,25 @@ model Account {
scope String? scope String?
id_token String? @db.Text id_token String? @db.Text
session_state String? session_state String?
password String?
user User? @relation(fields: [userId], references: [id], onDelete: Cascade) user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([provider, providerAccountId]) @@unique([provider, providerAccountId])
} }
model Session { model Session {
id String @id @default(cuid()) id String @id @default(cuid())
sessionToken String @unique sessionToken String @unique
userId Int userId Int
expires DateTime
user User? @relation(fields: [userId], references: [id], onDelete: Cascade) ipAddress String?
userAgent String?
expiresAt DateTime
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
} }
enum DocumentStatus { enum DocumentStatus {