Compare commits

..

6 Commits

13 changed files with 199 additions and 104 deletions

View File

@ -6,7 +6,7 @@ import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { type Recipient, SigningStatus } from '@prisma/client';
import { History } from 'lucide-react';
import { useForm, useWatch } from 'react-hook-form';
import { useForm } from 'react-hook-form';
import * as z from 'zod';
import { useSession } from '@documenso/lib/client-only/providers/session';
@ -85,11 +85,6 @@ export const DocumentResendDialog = ({ document, recipients }: DocumentResendDia
formState: { isSubmitting },
} = form;
const selectedRecipients = useWatch({
control: form.control,
name: 'recipients',
});
const onFormSubmit = async ({ recipients }: TResendDocumentFormSchema) => {
try {
await resendDocument({ documentId: document.id, recipients });
@ -156,7 +151,7 @@ export const DocumentResendDialog = ({ document, recipients }: DocumentResendDia
<FormControl>
<Checkbox
className="h-5 w-5 rounded-full border border-neutral-400"
className="h-5 w-5 rounded-full"
value={recipient.id}
checked={value.includes(recipient.id)}
onCheckedChange={(checked: boolean) =>
@ -187,13 +182,7 @@ export const DocumentResendDialog = ({ document, recipients }: DocumentResendDia
</Button>
</DialogClose>
<Button
className="flex-1"
loading={isSubmitting}
type="submit"
form={FORM_ID}
disabled={isSubmitting || selectedRecipients.length === 0}
>
<Button className="flex-1" loading={isSubmitting} type="submit" form={FORM_ID}>
<Trans>Send reminder</Trans>
</Button>
</div>

View File

@ -160,14 +160,6 @@ export const DocumentSigningPageView = ({
return (
<DocumentSigningRecipientProvider recipient={recipient} targetSigner={targetSigner}>
<div className="mx-auto w-full max-w-screen-xl sm:px-6">
{document.team.teamGlobalSettings.brandingEnabled &&
document.team.teamGlobalSettings.brandingLogo && (
<img
src={`/api/branding/logo/team/${document.teamId}`}
alt={`${document.team.name}'s Logo`}
className="mb-4 h-12 w-12 md:mb-2"
/>
)}
<h1
className="block max-w-[20rem] truncate text-2xl font-semibold sm:mt-4 md:max-w-[30rem] md:text-3xl"
title={document.title}

View File

@ -1,13 +1,16 @@
import { useState } from 'react';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { DownloadIcon } from 'lucide-react';
import { trpc } from '@documenso/trpc/react';
import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { useCurrentTeam } from '~/providers/team';
export type DocumentAuditLogDownloadButtonProps = {
className?: string;
documentId: number;
@ -19,44 +22,38 @@ export const DocumentAuditLogDownloadButton = ({
}: DocumentAuditLogDownloadButtonProps) => {
const { toast } = useToast();
const { _ } = useLingui();
const { mutateAsync: downloadAuditLogs, isPending } =
trpc.document.auditLog.download.useMutation();
const [isPending, setIsPending] = useState(false);
const team = useCurrentTeam();
const onDownloadAuditLogsClick = async () => {
setIsPending(true);
try {
const { url } = await downloadAuditLogs({ documentId });
const response = await fetch(`/api/t/${team.url}/download/audit-logs/${documentId}`);
const iframe = Object.assign(document.createElement('iframe'), {
src: url,
});
if (!response.ok) {
throw new Error('Failed to download certificate');
}
Object.assign(iframe.style, {
position: 'fixed',
top: '0',
left: '0',
width: '0',
height: '0',
});
const contentDisposition = response.headers.get('Content-Disposition');
const filename =
contentDisposition?.split('filename="')[1]?.split('"')[0] ||
`document_${documentId}_audit_logs.pdf`;
const onLoaded = () => {
if (iframe.contentDocument?.readyState === 'complete') {
iframe.contentWindow?.print();
const blob = await response.blob();
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
iframe.contentWindow?.addEventListener('afterprint', () => {
document.body.removeChild(iframe);
});
}
};
link.href = url;
link.download = filename;
// When the iframe has loaded, print the iframe and remove it from the dom
iframe.addEventListener('load', onLoaded);
document.body.appendChild(link);
link.click();
document.body.appendChild(iframe);
onLoaded();
document.body.removeChild(link);
URL.revokeObjectURL(url);
} catch (error) {
console.error(error);
console.error('Audit logs download error:', error);
toast({
title: _(msg`Something went wrong`),
@ -65,6 +62,8 @@ export const DocumentAuditLogDownloadButton = ({
),
variant: 'destructive',
});
} finally {
setIsPending(false);
}
};

View File

@ -1,3 +1,5 @@
import { useState } from 'react';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
@ -5,11 +7,12 @@ import type { DocumentStatus } from '@prisma/client';
import { DownloadIcon } from 'lucide-react';
import { isDocumentCompleted } from '@documenso/lib/utils/document';
import { trpc } from '@documenso/trpc/react';
import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { useCurrentTeam } from '~/providers/team';
export type DocumentCertificateDownloadButtonProps = {
className?: string;
documentId: number;
@ -23,44 +26,38 @@ export const DocumentCertificateDownloadButton = ({
}: DocumentCertificateDownloadButtonProps) => {
const { toast } = useToast();
const { _ } = useLingui();
const { mutateAsync: downloadCertificate, isPending } =
trpc.document.downloadCertificate.useMutation();
const [isPending, setIsPending] = useState(false);
const team = useCurrentTeam();
const onDownloadCertificatesClick = async () => {
setIsPending(true);
try {
const { url } = await downloadCertificate({ documentId });
const response = await fetch(`/api/t/${team.url}/download/certificate/${documentId}`);
const iframe = Object.assign(document.createElement('iframe'), {
src: url,
});
if (!response.ok) {
throw new Error('Failed to download certificate');
}
Object.assign(iframe.style, {
position: 'fixed',
top: '0',
left: '0',
width: '0',
height: '0',
});
const contentDisposition = response.headers.get('Content-Disposition');
const filename =
contentDisposition?.split('filename="')[1]?.split('"')[0] ||
`document_${documentId}_certificate.pdf`;
const onLoaded = () => {
if (iframe.contentDocument?.readyState === 'complete') {
iframe.contentWindow?.print();
const blob = await response.blob();
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
iframe.contentWindow?.addEventListener('afterprint', () => {
document.body.removeChild(iframe);
});
}
};
link.href = url;
link.download = filename;
// When the iframe has loaded, print the iframe and remove it from the dom
iframe.addEventListener('load', onLoaded);
document.body.appendChild(link);
link.click();
document.body.appendChild(iframe);
onLoaded();
document.body.removeChild(link);
URL.revokeObjectURL(url);
} catch (error) {
console.error(error);
console.error('Certificate download error:', error);
toast({
title: _(msg`Something went wrong`),
@ -69,6 +66,8 @@ export const DocumentCertificateDownloadButton = ({
),
variant: 'destructive',
});
} finally {
setIsPending(false);
}
};

View File

@ -23,12 +23,10 @@ export const loader = async () => {
try {
const certStatus = getCertificateStatus();
if (certStatus.isAvailable) {
checks.certificate = { status: 'ok' };
} else {
checks.certificate = { status: 'warning' };
if (overallStatus === 'ok') {
overallStatus = 'warning';
}

View File

@ -0,0 +1,61 @@
import { getSession } from '@documenso/auth/server/lib/utils/get-session';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id';
import { getAuditLogsPdf } from '@documenso/lib/server-only/htmltopdf/get-audit-logs-pdf';
import { getTeamByUrl } from '@documenso/lib/server-only/team/get-team';
import type { Route } from './+types/t.$teamUrl.download.audit-logs.$documentId';
export async function loader({ request, params }: Route.LoaderArgs) {
const documentId = Number(params.documentId);
const teamUrl = params.teamUrl;
if (!documentId || !teamUrl) {
return Response.json({ error: 'Invalid document ID or team URL' }, { status: 400 });
}
try {
const { user } = await getSession(request);
const team = await getTeamByUrl({ userId: user.id, teamUrl });
if (!team) {
return Response.json({ error: 'Team not found or access denied' }, { status: 404 });
}
const document = await getDocumentById({
documentId,
userId: user.id,
teamId: team.id,
}).catch(() => null);
if (!document || (team.id && document.teamId !== team.id)) {
return Response.json({ error: 'Document not found or access denied' }, { status: 404 });
}
const pdfBuffer = await getAuditLogsPdf({
documentId: document.id,
language: document.documentMeta?.language,
});
const filename = `${document.title.replace(/\.pdf$/, '')}_audit_logs.pdf`;
return new Response(pdfBuffer, {
headers: {
'Content-Type': 'application/pdf',
'Content-Disposition': `attachment; filename="${filename}"`,
'Content-Length': pdfBuffer.length.toString(),
'Cache-Control': 'no-cache, no-store, must-revalidate',
Expires: '0',
},
});
} catch (error) {
if (error instanceof AppError) {
const statusCode = error.code === AppErrorCode.UNAUTHORIZED ? 401 : 400;
return Response.json({ error: error.message }, { status: statusCode });
}
return Response.json({ error: 'Failed to generate audit logs PDF' }, { status: 500 });
}
}

View File

@ -0,0 +1,69 @@
import { getSession } from '@documenso/auth/server/lib/utils/get-session';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id';
import { getCertificatePdf } from '@documenso/lib/server-only/htmltopdf/get-certificate-pdf';
import { getTeamByUrl } from '@documenso/lib/server-only/team/get-team';
import { isDocumentCompleted } from '@documenso/lib/utils/document';
import type { Route } from './+types/t.$teamUrl.download.certificate.$documentId';
export async function loader({ request, params }: Route.LoaderArgs) {
const documentId = Number(params.documentId);
const teamUrl = params.teamUrl;
if (!documentId || !teamUrl) {
return Response.json({ error: 'Invalid document ID or team URL' }, { status: 400 });
}
try {
const { user } = await getSession(request);
const team = await getTeamByUrl({ userId: user.id, teamUrl }).catch(() => null);
if (!team) {
return Response.json({ error: 'Team not found or access denied' }, { status: 404 });
}
const document = await getDocumentById({
documentId,
userId: user.id,
teamId: team.id,
}).catch(() => null);
if (!document || document.teamId !== team.id) {
return Response.json({ error: 'Document not found or access denied' }, { status: 404 });
}
if (!isDocumentCompleted(document.status)) {
return Response.json(
{ error: 'Document must be completed to download the certificate' },
{ status: 400 },
);
}
const pdfBuffer = await getCertificatePdf({
documentId: document.id,
language: document.documentMeta?.language,
});
const filename = `${document.title.replace(/\.pdf$/, '')}_certificate.pdf`;
return new Response(pdfBuffer, {
headers: {
'Content-Type': 'application/pdf',
'Content-Disposition': `attachment; filename="${filename}"`,
'Content-Length': pdfBuffer.length.toString(),
'Cache-Control': 'no-cache, no-store, must-revalidate',
Expires: '0',
},
});
} catch (error) {
if (error instanceof AppError) {
const statusCode = error.code === AppErrorCode.UNAUTHORIZED ? 401 : 400;
return Response.json({ error: error.message }, { status: statusCode });
}
return Response.json({ error: 'Failed to generate certificate PDF' }, { status: 500 });
}
}

View File

@ -101,5 +101,5 @@
"vite-plugin-babel-macros": "^1.0.6",
"vite-tsconfig-paths": "^5.1.4"
},
"version": "1.12.4"
"version": "1.12.2-rc.6"
}

6
package-lock.json generated
View File

@ -1,12 +1,12 @@
{
"name": "@documenso/root",
"version": "1.12.4",
"version": "1.12.2-rc.6",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@documenso/root",
"version": "1.12.4",
"version": "1.12.2-rc.6",
"workspaces": [
"apps/*",
"packages/*"
@ -89,7 +89,7 @@
},
"apps/remix": {
"name": "@documenso/remix",
"version": "1.12.4",
"version": "1.12.2-rc.6",
"dependencies": {
"@documenso/api": "*",
"@documenso/assets": "*",

View File

@ -1,6 +1,6 @@
{
"private": true,
"version": "1.12.4",
"version": "1.12.2-rc.6",
"scripts": {
"build": "turbo run build",
"dev": "turbo run dev --filter=@documenso/remix",

View File

@ -2,25 +2,18 @@ import * as fs from 'node:fs';
import { env } from '@documenso/lib/utils/env';
export const getCertificateStatus = () => {
if (env('NEXT_PRIVATE_SIGNING_TRANSPORT') !== 'local') {
return { isAvailable: true };
}
if (env('NEXT_PRIVATE_SIGNING_LOCAL_FILE_CONTENTS')) {
return { isAvailable: true };
}
export type CertificateStatus = {
isAvailable: boolean;
};
export const getCertificateStatus = (): CertificateStatus => {
const defaultPath =
env('NODE_ENV') === 'production' ? '/opt/documenso/cert.p12' : './example/cert.p12';
const filePath = env('NEXT_PRIVATE_SIGNING_LOCAL_FILE_PATH') || defaultPath;
try {
fs.accessSync(filePath, fs.constants.F_OK | fs.constants.R_OK);
const stats = fs.statSync(filePath);
return { isAvailable: stats.size > 0 };
} catch {
return { isAvailable: false };

View File

@ -91,12 +91,6 @@ export const getDocumentAndSenderByToken = async ({
select: {
name: true,
teamEmail: true,
teamGlobalSettings: {
select: {
brandingEnabled: true,
brandingLogo: true,
},
},
},
},
},

View File

@ -714,6 +714,7 @@ export const AddSignersFormPartial = ({
handleRecipientAutoCompleteSelect(index, suggestion)
}
onSearchQueryChange={(query) => {
console.log('onSearchQueryChange', query);
field.onChange(query);
setRecipientSearchQuery(query);
}}