mirror of
https://github.com/documenso/documenso.git
synced 2026-07-04 18:14:56 +10:00
Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 191b333e34 | |||
| 2c579c6455 | |||
| 6bd688bde4 | |||
| c0a72123bd | |||
| d710f53fb5 | |||
| 34caad2641 | |||
| 1511d2288c | |||
| e19da93ce2 | |||
| 30b240cba2 | |||
| eb78706f35 | |||
| 52b474d12b | |||
| 0b03bd3fce | |||
| 15d0be17d7 | |||
| 338965325d | |||
| 3b476e9e1f | |||
| 6da56887ee | |||
| cec25ac719 | |||
| d10ec437cf | |||
| dbacfaa841 | |||
| 6980db57d3 | |||
| e3f8e76e6a | |||
| 396a7db587 | |||
| 7ac48cb3f5 | |||
| f7ee4d0ba2 | |||
| 1b67be9099 |
@@ -20,6 +20,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.GH_PAT }}
|
||||
|
||||
- uses: ./.github/actions/node-install
|
||||
|
||||
|
||||
@@ -5,14 +5,15 @@ import { Callout, Steps } from 'nextra/components';
|
||||
Email Domains allow you to send emails to recipients from your own domain instead of the default Documenso email address.
|
||||
|
||||
<Callout type="info">
|
||||
**Enterprise Only**: Email Domains is only available to Enterprise customers and custom plans
|
||||
**Platform and Enterprise Only**: Email Domains is only available to Platform and Enterprise
|
||||
customers.
|
||||
</Callout>
|
||||
|
||||
## Creating Email Domains
|
||||
|
||||
Before setting up email domains, ensure you have:
|
||||
|
||||
- An Enterprise subscription
|
||||
- A Platform or Enterprise subscription
|
||||
- Access to your domain's DNS settings
|
||||
- Access to your Documenso organisation as an admin or manager
|
||||
|
||||
|
||||
@@ -4,9 +4,7 @@ import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { Trans, useLingui } from '@lingui/react/macro';
|
||||
import { TeamMemberRole } from '@prisma/client';
|
||||
import type * as DialogPrimitive from '@radix-ui/react-dialog';
|
||||
import { InfoIcon } from 'lucide-react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { Link } from 'react-router';
|
||||
import { match } from 'ts-pattern';
|
||||
import { z } from 'zod';
|
||||
|
||||
@@ -41,7 +39,6 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@documenso/ui/primitives/select';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
import { useCurrentTeam } from '~/providers/team';
|
||||
@@ -143,28 +140,8 @@ export const TeamMemberCreateDialog = ({ trigger, ...props }: TeamMemberCreateDi
|
||||
{match(step)
|
||||
.with('SELECT', () => (
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex flex-row items-center">
|
||||
<DialogTitle>
|
||||
<Trans>Add members</Trans>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<InfoIcon className="mx-2 h-4 w-4" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="text-muted-foreground z-[99999] max-w-xs">
|
||||
<Trans>
|
||||
To be able to add members to a team, you must first add them to the
|
||||
organisation. For more information, please see the{' '}
|
||||
<Link
|
||||
to="https://docs.documenso.com/users/organisations/members"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-documenso-700 hover:text-documenso-600 hover:underline"
|
||||
>
|
||||
documentation
|
||||
</Link>
|
||||
.
|
||||
</Trans>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</DialogTitle>
|
||||
|
||||
<DialogDescription>
|
||||
|
||||
@@ -15,7 +15,6 @@ import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT } from '@documenso/lib/constants/app';
|
||||
import {
|
||||
TEMPLATE_RECIPIENT_EMAIL_PLACEHOLDER_REGEX,
|
||||
TEMPLATE_RECIPIENT_NAME_PLACEHOLDER_REGEX,
|
||||
isTemplateRecipientEmailPlaceholder,
|
||||
} from '@documenso/lib/constants/template';
|
||||
import { AppError } from '@documenso/lib/errors/app-error';
|
||||
import { putPdfFile } from '@documenso/lib/universal/upload/put-file';
|
||||
@@ -280,11 +279,7 @@ export function TemplateUseDialog({
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
placeholder={
|
||||
isTemplateRecipientEmailPlaceholder(field.value)
|
||||
? ''
|
||||
: _(msg`Email`)
|
||||
}
|
||||
placeholder={recipients[index].email || _(msg`Email`)}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
@@ -489,7 +484,6 @@ export function TemplateUseDialog({
|
||||
|
||||
<input
|
||||
type="file"
|
||||
data-testid="template-use-dialog-file-input"
|
||||
className="absolute h-full w-full opacity-0"
|
||||
accept=".pdf,application/pdf"
|
||||
onChange={(e) => {
|
||||
|
||||
@@ -55,7 +55,6 @@ export type TDocumentPreferencesFormSchema = {
|
||||
documentDateFormat: TDocumentMetaDateFormat | null;
|
||||
includeSenderDetails: boolean | null;
|
||||
includeSigningCertificate: boolean | null;
|
||||
includeAuditLog: boolean | null;
|
||||
signatureTypes: DocumentSignatureType[];
|
||||
};
|
||||
|
||||
@@ -67,7 +66,6 @@ type SettingsSubset = Pick<
|
||||
| 'documentDateFormat'
|
||||
| 'includeSenderDetails'
|
||||
| 'includeSigningCertificate'
|
||||
| 'includeAuditLog'
|
||||
| 'typedSignatureEnabled'
|
||||
| 'uploadSignatureEnabled'
|
||||
| 'drawSignatureEnabled'
|
||||
@@ -98,7 +96,6 @@ export const DocumentPreferencesForm = ({
|
||||
documentDateFormat: ZDocumentMetaTimezoneSchema.nullable(),
|
||||
includeSenderDetails: z.boolean().nullable(),
|
||||
includeSigningCertificate: z.boolean().nullable(),
|
||||
includeAuditLog: z.boolean().nullable(),
|
||||
signatureTypes: z.array(z.nativeEnum(DocumentSignatureType)).min(canInherit ? 0 : 1, {
|
||||
message: msg`At least one signature type must be enabled`.id,
|
||||
}),
|
||||
@@ -115,7 +112,6 @@ export const DocumentPreferencesForm = ({
|
||||
documentDateFormat: settings.documentDateFormat as TDocumentMetaDateFormat | null,
|
||||
includeSenderDetails: settings.includeSenderDetails,
|
||||
includeSigningCertificate: settings.includeSigningCertificate,
|
||||
includeAuditLog: settings.includeAuditLog,
|
||||
signatureTypes: extractTeamSignatureSettings({ ...settings }),
|
||||
},
|
||||
resolver: zodResolver(ZDocumentPreferencesFormSchema),
|
||||
@@ -456,56 +452,6 @@ export const DocumentPreferencesForm = ({
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="includeAuditLog"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex-1">
|
||||
<FormLabel>
|
||||
<Trans>Include the Audit Logs in the Document</Trans>
|
||||
</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
<Select
|
||||
{...field}
|
||||
value={field.value === null ? '-1' : field.value.toString()}
|
||||
onValueChange={(value) =>
|
||||
field.onChange(value === 'true' ? true : value === 'false' ? false : null)
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="bg-background text-muted-foreground">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
|
||||
<SelectContent>
|
||||
<SelectItem value="true">
|
||||
<Trans>Yes</Trans>
|
||||
</SelectItem>
|
||||
|
||||
<SelectItem value="false">
|
||||
<Trans>No</Trans>
|
||||
</SelectItem>
|
||||
|
||||
{canInherit && (
|
||||
<SelectItem value={'-1'}>
|
||||
<Trans>Inherit from organisation</Trans>
|
||||
</SelectItem>
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<FormDescription>
|
||||
<Trans>
|
||||
Controls whether the audit logs will be included in the document when it is
|
||||
downloaded. The audit logs can still be downloaded from the logs page
|
||||
separately.
|
||||
</Trans>
|
||||
</FormDescription>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="flex flex-row justify-end space-x-4">
|
||||
<Button type="submit" loading={form.formState.isSubmitting}>
|
||||
<Trans>Update</Trans>
|
||||
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { LinkIcon } from 'lucide-react';
|
||||
|
||||
import type { DocumentAndSender } from '@documenso/lib/server-only/document/get-document-by-token';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from '@documenso/ui/primitives/dialog';
|
||||
|
||||
export type DocumentSigningAttachmentsDialogProps = {
|
||||
document: DocumentAndSender;
|
||||
};
|
||||
|
||||
export const DocumentSigningAttachmentsDialog = ({
|
||||
document,
|
||||
}: DocumentSigningAttachmentsDialogProps) => {
|
||||
const attachments = document.attachments ?? [];
|
||||
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline">
|
||||
<Trans>Attachments</Trans>
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent position="center">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
<Trans>Attachments</Trans>
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
<Trans>View all attachments for this document.</Trans>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="mt-2 flex flex-col gap-2">
|
||||
{attachments.length === 0 && (
|
||||
<span className="text-muted-foreground text-sm">
|
||||
<Trans>No attachments available.</Trans>
|
||||
</span>
|
||||
)}
|
||||
|
||||
{attachments.map((attachment, idx) => (
|
||||
<a
|
||||
key={attachment.id || idx}
|
||||
href={attachment.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="hover:bg-muted/50 flex items-center gap-2 rounded px-2 py-1"
|
||||
>
|
||||
<LinkIcon className="h-4 w-4" />
|
||||
<span className="truncate">{attachment.label}</span>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
@@ -6,9 +6,9 @@ import { RecipientRole } from '@prisma/client';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { AppError } from '@documenso/lib/errors/app-error';
|
||||
import { DocumentAuth, type TRecipientActionAuth } from '@documenso/lib/types/document-auth';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
|
||||
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import { DialogFooter } from '@documenso/ui/primitives/dialog';
|
||||
import {
|
||||
@@ -20,8 +20,6 @@ import {
|
||||
FormMessage,
|
||||
} from '@documenso/ui/primitives/form/form';
|
||||
import { PinInput, PinInputGroup, PinInputSlot } from '@documenso/ui/primitives/pin-input';
|
||||
import { Tabs, TabsList, TabsTrigger } from '@documenso/ui/primitives/tabs';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
import { EnableAuthenticatorAppDialog } from '~/components/forms/2fa/enable-authenticator-app-dialog';
|
||||
|
||||
@@ -53,7 +51,6 @@ export const DocumentSigningAuth2FA = ({
|
||||
}: DocumentSigningAuth2FAProps) => {
|
||||
const { recipient, user, isCurrentlyAuthenticating, setIsCurrentlyAuthenticating } =
|
||||
useRequiredDocumentSigningAuthContext();
|
||||
const { toast } = useToast();
|
||||
|
||||
const form = useForm<T2FAAuthFormSchema>({
|
||||
resolver: zodResolver(Z2FAAuthFormSchema),
|
||||
@@ -63,104 +60,27 @@ export const DocumentSigningAuth2FA = ({
|
||||
});
|
||||
|
||||
const [is2FASetupSuccessful, setIs2FASetupSuccessful] = useState(false);
|
||||
const [isEmailCodeSent, setIsEmailCodeSent] = useState(false);
|
||||
const [isEmailCodeSending, setIsEmailCodeSending] = useState(false);
|
||||
const [canResendEmail, setCanResendEmail] = useState(true);
|
||||
const [resendCountdown, setResendCountdown] = useState(0);
|
||||
const countdownTimerRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const [verificationMethod, setVerificationMethod] = useState<'app' | 'email'>(
|
||||
user?.twoFactorEnabled ? 'app' : 'email',
|
||||
);
|
||||
const emailSendInitiatedRef = useRef(false);
|
||||
|
||||
const sendVerificationMutation = trpc.auth.sendEmailVerification.useMutation({
|
||||
onSuccess: () => {
|
||||
setIsEmailCodeSent(true);
|
||||
setCanResendEmail(false);
|
||||
setResendCountdown(60);
|
||||
|
||||
countdownTimerRef.current = setInterval(() => {
|
||||
setResendCountdown((prev) => {
|
||||
if (prev <= 1) {
|
||||
if (countdownTimerRef.current) {
|
||||
clearInterval(countdownTimerRef.current);
|
||||
}
|
||||
setCanResendEmail(true);
|
||||
return 0;
|
||||
}
|
||||
return prev - 1;
|
||||
});
|
||||
}, 1000);
|
||||
|
||||
toast({
|
||||
title: 'Verification code sent',
|
||||
description: `A verification code has been sent to ${recipient.email}`,
|
||||
});
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error('Failed to send verification code', error);
|
||||
toast({
|
||||
title: 'Failed to send verification code',
|
||||
description: 'Please try again or contact support',
|
||||
variant: 'destructive',
|
||||
});
|
||||
},
|
||||
onSettled: () => {
|
||||
setIsEmailCodeSending(false);
|
||||
},
|
||||
});
|
||||
|
||||
const verifyCodeMutation = trpc.auth.verifyEmailCode.useMutation();
|
||||
|
||||
const sendEmailVerificationCode = async () => {
|
||||
try {
|
||||
setIsEmailCodeSending(true);
|
||||
await sendVerificationMutation.mutateAsync({
|
||||
recipientId: recipient.id,
|
||||
});
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: 'Failed to send verification code',
|
||||
description: 'Please try again.',
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (countdownTimerRef.current) {
|
||||
clearInterval(countdownTimerRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
const [formErrorCode, setFormErrorCode] = useState<string | null>(null);
|
||||
|
||||
const onFormSubmit = async ({ token }: T2FAAuthFormSchema) => {
|
||||
try {
|
||||
setIsCurrentlyAuthenticating(true);
|
||||
|
||||
if (verificationMethod === 'email') {
|
||||
await verifyCodeMutation.mutateAsync({
|
||||
code: token,
|
||||
recipientId: recipient.id,
|
||||
});
|
||||
}
|
||||
|
||||
await onReauthFormSubmit({
|
||||
type: DocumentAuth.TWO_FACTOR_AUTH,
|
||||
token,
|
||||
});
|
||||
|
||||
setIsCurrentlyAuthenticating(false);
|
||||
|
||||
onOpenChange(false);
|
||||
} catch (err) {
|
||||
setIsCurrentlyAuthenticating(false);
|
||||
|
||||
toast({
|
||||
title: 'Unauthorized',
|
||||
description: 'We were unable to verify your details.',
|
||||
variant: 'destructive',
|
||||
});
|
||||
const error = AppError.parseError(err);
|
||||
setFormErrorCode(error.code);
|
||||
|
||||
// Todo: Alert.
|
||||
}
|
||||
};
|
||||
|
||||
@@ -170,46 +90,21 @@ export const DocumentSigningAuth2FA = ({
|
||||
});
|
||||
|
||||
setIs2FASetupSuccessful(false);
|
||||
setIsEmailCodeSent(false);
|
||||
setFormErrorCode(null);
|
||||
|
||||
if (open && !user?.twoFactorEnabled) {
|
||||
setVerificationMethod('email');
|
||||
}
|
||||
}, [open, user?.twoFactorEnabled, form]);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [open]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open || verificationMethod !== 'email') {
|
||||
emailSendInitiatedRef.current = false;
|
||||
}
|
||||
}, [open, verificationMethod]);
|
||||
|
||||
useEffect(() => {
|
||||
if (open && verificationMethod === 'email' && !isEmailCodeSent && !isEmailCodeSending) {
|
||||
if (!emailSendInitiatedRef.current) {
|
||||
emailSendInitiatedRef.current = true;
|
||||
void sendEmailVerificationCode();
|
||||
}
|
||||
}
|
||||
}, [open, verificationMethod, isEmailCodeSent, isEmailCodeSending]);
|
||||
|
||||
if (verificationMethod === 'app' && !user?.twoFactorEnabled && !is2FASetupSuccessful) {
|
||||
if (!user?.twoFactorEnabled && !is2FASetupSuccessful) {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<Tabs
|
||||
value={verificationMethod}
|
||||
onValueChange={(val) => setVerificationMethod(val as 'app' | 'email')}
|
||||
>
|
||||
<TabsList className="grid w-full grid-cols-2">
|
||||
<TabsTrigger value="app">Authenticator App</TabsTrigger>
|
||||
<TabsTrigger value="email">Email Verification</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
<Alert variant="warning">
|
||||
<AlertDescription>
|
||||
<p>
|
||||
{recipient.role === RecipientRole.VIEWER && actionTarget === 'DOCUMENT' ? (
|
||||
<Trans>You need to setup 2FA to mark this document as viewed.</Trans>
|
||||
) : (
|
||||
// Todo: Translate
|
||||
`You need to setup 2FA to ${actionVerb.toLowerCase()} this ${actionTarget.toLowerCase()}.`
|
||||
)}
|
||||
</p>
|
||||
@@ -234,106 +129,59 @@ export const DocumentSigningAuth2FA = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{user?.twoFactorEnabled && (
|
||||
<Tabs
|
||||
value={verificationMethod}
|
||||
onValueChange={(val) => setVerificationMethod(val as 'app' | 'email')}
|
||||
>
|
||||
<TabsList className="grid w-full grid-cols-2">
|
||||
<TabsTrigger value="app">Authenticator App</TabsTrigger>
|
||||
<TabsTrigger value="email">Email Verification</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
)}
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onFormSubmit)}>
|
||||
<fieldset disabled={isCurrentlyAuthenticating}>
|
||||
<div className="space-y-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="token"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel required>2FA token</FormLabel>
|
||||
|
||||
{verificationMethod === 'email' && (
|
||||
<Alert variant="secondary">
|
||||
<AlertDescription>
|
||||
{isEmailCodeSent ? (
|
||||
<p>
|
||||
<Trans>
|
||||
A verification code has been sent to {recipient.email}. Please enter it below to
|
||||
continue.
|
||||
</Trans>
|
||||
</p>
|
||||
) : (
|
||||
<p>
|
||||
<Trans>
|
||||
We'll send a verification code to {recipient.email} to verify your identity.
|
||||
</Trans>
|
||||
</p>
|
||||
)}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
<FormControl>
|
||||
<PinInput {...field} value={field.value ?? ''} maxLength={6}>
|
||||
{Array(6)
|
||||
.fill(null)
|
||||
.map((_, i) => (
|
||||
<PinInputGroup key={i}>
|
||||
<PinInputSlot index={i} />
|
||||
</PinInputGroup>
|
||||
))}
|
||||
</PinInput>
|
||||
</FormControl>
|
||||
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onFormSubmit)}>
|
||||
<fieldset disabled={isCurrentlyAuthenticating}>
|
||||
<div className="space-y-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="token"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel required>
|
||||
{verificationMethod === 'app' ? (
|
||||
<Trans>2FA token</Trans>
|
||||
) : (
|
||||
<Trans>Verification code</Trans>
|
||||
)}
|
||||
</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
<PinInput {...field} value={field.value ?? ''} maxLength={6}>
|
||||
{Array(6)
|
||||
.fill(null)
|
||||
.map((_, i) => (
|
||||
<PinInputGroup key={i}>
|
||||
<PinInputSlot index={i} />
|
||||
</PinInputGroup>
|
||||
))}
|
||||
</PinInput>
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{verificationMethod === 'email' && (
|
||||
<div className="flex justify-center">
|
||||
<Button
|
||||
type="button"
|
||||
variant="link"
|
||||
disabled={isEmailCodeSending || !canResendEmail}
|
||||
onClick={() => void sendEmailVerificationCode()}
|
||||
>
|
||||
{isEmailCodeSending ? (
|
||||
<Trans>Sending...</Trans>
|
||||
) : !canResendEmail ? (
|
||||
<Trans>Resend code ({resendCountdown}s)</Trans>
|
||||
) : (
|
||||
<Trans>Resend code</Trans>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<DialogFooter>
|
||||
<Button type="button" variant="secondary" onClick={() => onOpenChange(false)}>
|
||||
<Trans>Cancel</Trans>
|
||||
</Button>
|
||||
{formErrorCode && (
|
||||
<Alert variant="destructive">
|
||||
<AlertTitle>
|
||||
<Trans>Unauthorized</Trans>
|
||||
</AlertTitle>
|
||||
<AlertDescription>
|
||||
<Trans>
|
||||
We were unable to verify your details. Please try again or contact support
|
||||
</Trans>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Button type="submit" loading={isCurrentlyAuthenticating}>
|
||||
<Trans>{actionTarget === 'DOCUMENT' ? 'Sign Document' : 'Sign Field'}</Trans>
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button type="button" variant="secondary" onClick={() => onOpenChange(false)}>
|
||||
<Trans>Cancel</Trans>
|
||||
</Button>
|
||||
|
||||
<Button type="submit" loading={isCurrentlyAuthenticating}>
|
||||
<Trans>Sign</Trans>
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
+3
-22
@@ -43,7 +43,6 @@ export const DocumentSigningAuthDialog = ({
|
||||
title,
|
||||
description,
|
||||
availableAuthTypes,
|
||||
actionTarget,
|
||||
open,
|
||||
onOpenChange,
|
||||
onReauthFormSubmit,
|
||||
@@ -108,32 +107,15 @@ export const DocumentSigningAuthDialog = ({
|
||||
>
|
||||
<ChevronLeftIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
<span>
|
||||
{title ||
|
||||
(actionTarget === 'DOCUMENT' ? (
|
||||
<Trans>Sign document</Trans>
|
||||
) : (
|
||||
<Trans>Sign field</Trans>
|
||||
))}
|
||||
</span>
|
||||
<span>{title || <Trans>Sign field</Trans>}</span>
|
||||
</div>
|
||||
)}
|
||||
{(!selectedAuthType || validAuthTypes.length === 1) &&
|
||||
(title ||
|
||||
(actionTarget === 'DOCUMENT' ? (
|
||||
<Trans>Sign document</Trans>
|
||||
) : (
|
||||
<Trans>Sign field</Trans>
|
||||
)))}
|
||||
(title || <Trans>Sign field</Trans>)}
|
||||
</DialogTitle>
|
||||
|
||||
<DialogDescription>
|
||||
{description || (
|
||||
<Trans>
|
||||
Reauthentication is required to sign this{' '}
|
||||
{actionTarget === 'DOCUMENT' ? 'document' : 'field'}
|
||||
</Trans>
|
||||
)}
|
||||
{description || <Trans>Reauthentication is required to sign this field</Trans>}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
@@ -198,7 +180,6 @@ export const DocumentSigningAuthDialog = ({
|
||||
))
|
||||
.with({ documentAuthType: DocumentAuth.TWO_FACTOR_AUTH }, () => (
|
||||
<DocumentSigningAuth2FA
|
||||
actionTarget={actionTarget === 'DOCUMENT' ? 'DOCUMENT' : 'FIELD'}
|
||||
open={open}
|
||||
onOpenChange={onOpenChange}
|
||||
onReauthFormSubmit={onReauthFormSubmit}
|
||||
|
||||
+3
-14
@@ -42,7 +42,6 @@ export type DocumentSigningAuthContextValue = {
|
||||
setPreferredPasskeyId: (_value: string | null) => void;
|
||||
user?: SessionUser | null;
|
||||
refetchPasskeys: () => Promise<void>;
|
||||
isEnterprise: boolean;
|
||||
};
|
||||
|
||||
const DocumentSigningAuthContext = createContext<DocumentSigningAuthContextValue | null>(null);
|
||||
@@ -66,7 +65,6 @@ export interface DocumentSigningAuthProviderProps {
|
||||
recipient: Recipient;
|
||||
user?: SessionUser | null;
|
||||
children: React.ReactNode;
|
||||
isEnterprise: boolean;
|
||||
}
|
||||
|
||||
export const DocumentSigningAuthProvider = ({
|
||||
@@ -74,7 +72,6 @@ export const DocumentSigningAuthProvider = ({
|
||||
recipient: initialRecipient,
|
||||
user,
|
||||
children,
|
||||
isEnterprise,
|
||||
}: DocumentSigningAuthProviderProps) => {
|
||||
const [documentAuthOptions, setDocumentAuthOptions] = useState(initialDocumentAuthOptions);
|
||||
const [recipient, setRecipient] = useState(initialRecipient);
|
||||
@@ -147,13 +144,8 @@ export const DocumentSigningAuthProvider = ({
|
||||
}, [derivedRecipientActionAuth, user, recipient]);
|
||||
|
||||
const executeActionAuthProcedure = async (options: ExecuteActionAuthProcedureOptions) => {
|
||||
// Determine if authentication is required based on enterprise status and action target.
|
||||
const requiresAuthTrigger = isEnterprise
|
||||
? derivedRecipientActionAuth && options.actionTarget === FieldType.SIGNATURE
|
||||
: derivedRecipientActionAuth && options.actionTarget === 'DOCUMENT';
|
||||
|
||||
// Directly run callback if no auth trigger is needed.
|
||||
if (!requiresAuthTrigger) {
|
||||
// Directly run callback if no auth required.
|
||||
if (!derivedRecipientActionAuth || options.actionTarget !== FieldType.SIGNATURE) {
|
||||
await options.onReauthFormSubmit();
|
||||
return;
|
||||
}
|
||||
@@ -213,7 +205,6 @@ export const DocumentSigningAuthProvider = ({
|
||||
preferredPasskeyId,
|
||||
setPreferredPasskeyId,
|
||||
refetchPasskeys,
|
||||
isEnterprise,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
@@ -234,8 +225,6 @@ export const DocumentSigningAuthProvider = ({
|
||||
type ExecuteActionAuthProcedureOptions = Omit<
|
||||
DocumentSigningAuthDialogProps,
|
||||
'open' | 'onOpenChange' | 'documentAuthType' | 'recipientRole' | 'availableAuthTypes'
|
||||
> & {
|
||||
actionTarget: FieldType | 'DOCUMENT';
|
||||
};
|
||||
>;
|
||||
|
||||
DocumentSigningAuthProvider.displayName = 'DocumentSigningAuthProvider';
|
||||
|
||||
@@ -16,6 +16,7 @@ import { sortFieldsByPosition, validateFieldsInserted } from '@documenso/lib/uti
|
||||
import type { RecipientWithFields } from '@documenso/prisma/types/recipient-with-fields';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { FieldToolTip } from '@documenso/ui/components/field/field-tooltip';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import { Input } from '@documenso/ui/primitives/input';
|
||||
import { Label } from '@documenso/ui/primitives/label';
|
||||
@@ -27,7 +28,6 @@ import {
|
||||
AssistantConfirmationDialog,
|
||||
type NextSigner,
|
||||
} from '../../dialogs/assistant-confirmation-dialog';
|
||||
import { useRequiredDocumentSigningAuthContext } from './document-signing-auth-provider';
|
||||
import { DocumentSigningCompleteDialog } from './document-signing-complete-dialog';
|
||||
import { useRequiredDocumentSigningContext } from './document-signing-provider';
|
||||
|
||||
@@ -39,7 +39,6 @@ export type DocumentSigningFormProps = {
|
||||
isRecipientsTurn: boolean;
|
||||
allRecipients?: RecipientWithFields[];
|
||||
setSelectedSignerId?: (id: number | null) => void;
|
||||
isEnterprise: boolean;
|
||||
};
|
||||
|
||||
export const DocumentSigningForm = ({
|
||||
@@ -50,7 +49,6 @@ export const DocumentSigningForm = ({
|
||||
isRecipientsTurn,
|
||||
allRecipients = [],
|
||||
setSelectedSignerId,
|
||||
isEnterprise,
|
||||
}: DocumentSigningFormProps) => {
|
||||
const { sessionData } = useOptionalSession();
|
||||
const user = sessionData?.user;
|
||||
@@ -64,7 +62,6 @@ export const DocumentSigningForm = ({
|
||||
const assistantSignersId = useId();
|
||||
|
||||
const { fullName, signature, setFullName, setSignature } = useRequiredDocumentSigningContext();
|
||||
const { executeActionAuthProcedure } = useRequiredDocumentSigningAuthContext();
|
||||
|
||||
const [validateUninsertedFields, setValidateUninsertedFields] = useState(false);
|
||||
const [isConfirmationDialogOpen, setIsConfirmationDialogOpen] = useState(false);
|
||||
@@ -117,16 +114,11 @@ export const DocumentSigningForm = ({
|
||||
setIsAssistantSubmitting(true);
|
||||
|
||||
try {
|
||||
await executeActionAuthProcedure({
|
||||
actionTarget: 'DOCUMENT',
|
||||
onReauthFormSubmit: async (authOptions) => {
|
||||
await completeDocument(authOptions, nextSigner);
|
||||
},
|
||||
});
|
||||
await completeDocument(undefined, nextSigner);
|
||||
} catch (err) {
|
||||
toast({
|
||||
title: _(msg`Error`),
|
||||
description: _(msg`An error occurred while completing the document. Please try again.`),
|
||||
title: 'Error',
|
||||
description: 'An error occurred while completing the document. Please try again.',
|
||||
variant: 'destructive',
|
||||
});
|
||||
|
||||
@@ -185,7 +177,15 @@ export const DocumentSigningForm = ({
|
||||
}, [document.documentMeta?.signingOrder, allRecipients, recipient.id]);
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
<div
|
||||
className={cn(
|
||||
'dark:bg-background border-border bg-widget sticky flex h-full flex-col rounded-xl border px-4 py-6',
|
||||
{
|
||||
'top-20 max-h-[min(68rem,calc(100vh-6rem))]': user,
|
||||
'top-4 max-h-[min(68rem,calc(100vh-2rem))]': !user,
|
||||
},
|
||||
)}
|
||||
>
|
||||
{validateUninsertedFields && uninsertedFields[0] && (
|
||||
<FieldToolTip key={uninsertedFields[0].id} field={uninsertedFields[0]} color="warning">
|
||||
<Trans>Click to insert field</Trans>
|
||||
@@ -194,8 +194,21 @@ export const DocumentSigningForm = ({
|
||||
|
||||
<div className="custom-scrollbar -mx-2 flex flex-1 flex-col overflow-y-auto overflow-x-hidden px-2">
|
||||
<div className="flex flex-1 flex-col">
|
||||
<h3 className="text-foreground text-2xl font-semibold">
|
||||
{recipient.role === RecipientRole.VIEWER && <Trans>View Document</Trans>}
|
||||
{recipient.role === RecipientRole.SIGNER && <Trans>Sign Document</Trans>}
|
||||
{recipient.role === RecipientRole.APPROVER && <Trans>Approve Document</Trans>}
|
||||
{recipient.role === RecipientRole.ASSISTANT && <Trans>Assist Document</Trans>}
|
||||
</h3>
|
||||
|
||||
{recipient.role === RecipientRole.VIEWER ? (
|
||||
<>
|
||||
<p className="text-muted-foreground mt-2 text-sm">
|
||||
<Trans>Please mark as viewed to complete</Trans>
|
||||
</p>
|
||||
|
||||
<hr className="border-border mb-8 mt-4" />
|
||||
|
||||
<div className="-mx-2 flex flex-1 flex-col gap-4 overflow-y-auto px-2">
|
||||
<div className="flex flex-1 flex-col gap-y-4" />
|
||||
<div className="flex flex-col gap-4 md:flex-row">
|
||||
@@ -216,12 +229,7 @@ export const DocumentSigningForm = ({
|
||||
fields={fields}
|
||||
fieldsValidated={fieldsValidated}
|
||||
onSignatureComplete={async (nextSigner) => {
|
||||
await executeActionAuthProcedure({
|
||||
actionTarget: 'DOCUMENT',
|
||||
onReauthFormSubmit: async (authOptions) => {
|
||||
await completeDocument(authOptions, nextSigner);
|
||||
},
|
||||
});
|
||||
await completeDocument(undefined, nextSigner);
|
||||
}}
|
||||
role={recipient.role}
|
||||
allowDictateNextSigner={document.documentMeta?.allowDictateNextSigner}
|
||||
@@ -237,6 +245,15 @@ export const DocumentSigningForm = ({
|
||||
) : recipient.role === RecipientRole.ASSISTANT ? (
|
||||
<>
|
||||
<form onSubmit={assistantForm.handleSubmit(onAssistantFormSubmit)}>
|
||||
<p className="text-muted-foreground mt-2 text-sm">
|
||||
<Trans>
|
||||
Complete the fields for the following signers. Once reviewed, they will inform
|
||||
you if any modifications are needed.
|
||||
</Trans>
|
||||
</p>
|
||||
|
||||
<hr className="border-border my-4" />
|
||||
|
||||
<fieldset className="dark:bg-background border-border rounded-2xl border bg-white p-3">
|
||||
<Controller
|
||||
name="selectedSignerId"
|
||||
@@ -323,81 +340,88 @@ export const DocumentSigningForm = ({
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<fieldset
|
||||
disabled={isSubmitting}
|
||||
className="-mx-2 flex flex-1 flex-col gap-4 overflow-y-auto px-2"
|
||||
>
|
||||
<div className="flex flex-1 flex-col gap-y-4">
|
||||
<div>
|
||||
<Label htmlFor="full-name">
|
||||
<Trans>Full Name</Trans>
|
||||
</Label>
|
||||
<div>
|
||||
<p className="text-muted-foreground mt-2 text-sm">
|
||||
{recipient.role === RecipientRole.APPROVER && !hasSignatureField ? (
|
||||
<Trans>Please review the document before approving.</Trans>
|
||||
) : (
|
||||
<Trans>Please review the document before signing.</Trans>
|
||||
)}
|
||||
</p>
|
||||
|
||||
<Input
|
||||
type="text"
|
||||
id="full-name"
|
||||
className="bg-background mt-2"
|
||||
value={fullName}
|
||||
onChange={(e) => setFullName(e.target.value.trimStart())}
|
||||
/>
|
||||
</div>
|
||||
<hr className="border-border mb-8 mt-4" />
|
||||
|
||||
{hasSignatureField && (
|
||||
<fieldset
|
||||
disabled={isSubmitting}
|
||||
className="-mx-2 flex flex-1 flex-col gap-4 overflow-y-auto px-2"
|
||||
>
|
||||
<div className="flex flex-1 flex-col gap-y-4">
|
||||
<div>
|
||||
<Label htmlFor="Signature">
|
||||
<Trans>Signature</Trans>
|
||||
<Label htmlFor="full-name">
|
||||
<Trans>Full Name</Trans>
|
||||
</Label>
|
||||
|
||||
<SignaturePadDialog
|
||||
className="mt-2"
|
||||
disabled={isSubmitting}
|
||||
value={signature ?? ''}
|
||||
onChange={(v) => setSignature(v ?? '')}
|
||||
typedSignatureEnabled={document.documentMeta?.typedSignatureEnabled}
|
||||
uploadSignatureEnabled={document.documentMeta?.uploadSignatureEnabled}
|
||||
drawSignatureEnabled={document.documentMeta?.drawSignatureEnabled}
|
||||
<Input
|
||||
type="text"
|
||||
id="full-name"
|
||||
className="bg-background mt-2"
|
||||
value={fullName}
|
||||
onChange={(e) => setFullName(e.target.value.trimStart())}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{hasSignatureField && (
|
||||
<div>
|
||||
<Label htmlFor="Signature">
|
||||
<Trans>Signature</Trans>
|
||||
</Label>
|
||||
|
||||
<SignaturePadDialog
|
||||
className="mt-2"
|
||||
disabled={isSubmitting}
|
||||
value={signature ?? ''}
|
||||
onChange={(v) => setSignature(v ?? '')}
|
||||
typedSignatureEnabled={document.documentMeta?.typedSignatureEnabled}
|
||||
uploadSignatureEnabled={document.documentMeta?.uploadSignatureEnabled}
|
||||
drawSignatureEnabled={document.documentMeta?.drawSignatureEnabled}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<div className="mt-6 flex flex-col gap-4 md:flex-row">
|
||||
<Button
|
||||
type="button"
|
||||
className="dark:bg-muted dark:hover:bg-muted/80 w-full bg-black/5 hover:bg-black/10"
|
||||
variant="secondary"
|
||||
size="lg"
|
||||
disabled={typeof window !== 'undefined' && window.history.length <= 1}
|
||||
onClick={async () => navigate(-1)}
|
||||
>
|
||||
<Trans>Cancel</Trans>
|
||||
</Button>
|
||||
|
||||
<DocumentSigningCompleteDialog
|
||||
isSubmitting={isSubmitting || isAssistantSubmitting}
|
||||
documentTitle={document.title}
|
||||
fields={fields}
|
||||
fieldsValidated={fieldsValidated}
|
||||
disabled={!isRecipientsTurn}
|
||||
onSignatureComplete={async (nextSigner) => {
|
||||
await completeDocument(undefined, nextSigner);
|
||||
}}
|
||||
role={recipient.role}
|
||||
allowDictateNextSigner={
|
||||
nextRecipient && document.documentMeta?.allowDictateNextSigner
|
||||
}
|
||||
defaultNextSigner={
|
||||
nextRecipient
|
||||
? { name: nextRecipient.name, email: nextRecipient.email }
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<div className="mt-6 flex flex-col gap-4 md:flex-row">
|
||||
<Button
|
||||
type="button"
|
||||
className="dark:bg-muted dark:hover:bg-muted/80 w-full bg-black/5 hover:bg-black/10"
|
||||
variant="secondary"
|
||||
size="lg"
|
||||
disabled={typeof window !== 'undefined' && window.history.length <= 1}
|
||||
onClick={async () => navigate(-1)}
|
||||
>
|
||||
<Trans>Cancel</Trans>
|
||||
</Button>
|
||||
|
||||
<DocumentSigningCompleteDialog
|
||||
isSubmitting={isSubmitting || isAssistantSubmitting}
|
||||
documentTitle={document.title}
|
||||
fields={fields}
|
||||
fieldsValidated={fieldsValidated}
|
||||
disabled={!isRecipientsTurn}
|
||||
onSignatureComplete={async (nextSigner) => {
|
||||
await executeActionAuthProcedure({
|
||||
actionTarget: 'DOCUMENT',
|
||||
onReauthFormSubmit: async (authOptions) => {
|
||||
await completeDocument(authOptions, nextSigner);
|
||||
},
|
||||
});
|
||||
}}
|
||||
role={recipient.role}
|
||||
allowDictateNextSigner={
|
||||
nextRecipient && document.documentMeta?.allowDictateNextSigner
|
||||
}
|
||||
defaultNextSigner={
|
||||
nextRecipient
|
||||
? { name: nextRecipient.name, email: nextRecipient.email }
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -3,7 +3,6 @@ import { useState } from 'react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import type { Field } from '@prisma/client';
|
||||
import { FieldType, RecipientRole } from '@prisma/client';
|
||||
import { LucideChevronDown, LucideChevronUp } from 'lucide-react';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats';
|
||||
@@ -21,11 +20,11 @@ import type { CompletedField } from '@documenso/lib/types/fields';
|
||||
import type { FieldWithSignatureAndFieldMeta } from '@documenso/prisma/types/field-with-signature-and-fieldmeta';
|
||||
import type { RecipientWithFields } from '@documenso/prisma/types/recipient-with-fields';
|
||||
import { DocumentReadOnlyFields } from '@documenso/ui/components/document/document-read-only-fields';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
||||
import { ElementVisible } from '@documenso/ui/primitives/element-visible';
|
||||
import { PDFViewer } from '@documenso/ui/primitives/pdf-viewer';
|
||||
|
||||
import { DocumentSigningAttachmentsDialog } from '~/components/general/document-signing/document-signing-attachments-dialog';
|
||||
import { DocumentSigningAutoSign } from '~/components/general/document-signing/document-signing-auto-sign';
|
||||
import { DocumentSigningCheckboxField } from '~/components/general/document-signing/document-signing-checkbox-field';
|
||||
import { DocumentSigningDateField } from '~/components/general/document-signing/document-signing-date-field';
|
||||
@@ -50,7 +49,6 @@ export type DocumentSigningPageViewProps = {
|
||||
isRecipientsTurn: boolean;
|
||||
allRecipients?: RecipientWithFields[];
|
||||
includeSenderDetails: boolean;
|
||||
isEnterprise: boolean;
|
||||
};
|
||||
|
||||
export const DocumentSigningPageView = ({
|
||||
@@ -61,12 +59,10 @@ export const DocumentSigningPageView = ({
|
||||
isRecipientsTurn,
|
||||
allRecipients = [],
|
||||
includeSenderDetails,
|
||||
isEnterprise,
|
||||
}: DocumentSigningPageViewProps) => {
|
||||
const { documentData, documentMeta } = document;
|
||||
|
||||
const [selectedSignerId, setSelectedSignerId] = useState<number | null>(allRecipients?.[0]?.id);
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
let senderName = document.user.name ?? '';
|
||||
let senderEmail = `(${document.user.email})`;
|
||||
@@ -82,15 +78,15 @@ export const DocumentSigningPageView = ({
|
||||
|
||||
return (
|
||||
<DocumentSigningRecipientProvider recipient={recipient} targetSigner={targetSigner}>
|
||||
<div className="mx-auto w-full max-w-screen-xl sm:px-6">
|
||||
<div className="mx-auto w-full max-w-screen-xl">
|
||||
<h1
|
||||
className="block max-w-[20rem] truncate text-2xl font-semibold sm:mt-4 md:max-w-[30rem] md:text-3xl"
|
||||
className="mt-4 block max-w-[20rem] truncate text-2xl font-semibold md:max-w-[30rem] md:text-3xl"
|
||||
title={document.title}
|
||||
>
|
||||
{document.title}
|
||||
</h1>
|
||||
|
||||
<div className="mt-1.5 flex flex-wrap items-center justify-between gap-y-2 sm:mt-2.5 sm:gap-y-0">
|
||||
<div className="mt-2.5 flex flex-wrap items-center justify-between gap-x-6 gap-y-4">
|
||||
<div className="max-w-[50ch]">
|
||||
<span className="text-muted-foreground truncate" title={senderName}>
|
||||
{senderName} {senderEmail}
|
||||
@@ -137,83 +133,32 @@ export const DocumentSigningPageView = ({
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<DocumentSigningRejectDialog document={document} token={recipient.token} />
|
||||
<div className="flex gap-2">
|
||||
<DocumentSigningAttachmentsDialog document={document} />
|
||||
<DocumentSigningRejectDialog document={document} token={recipient.token} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative mt-4 flex w-full flex-col gap-x-6 gap-y-8 sm:mt-8 md:flex-row lg:gap-x-8 lg:gap-y-0">
|
||||
<div className="flex-1">
|
||||
<Card className="rounded-xl before:rounded-xl" gradient>
|
||||
<CardContent className="p-2">
|
||||
<PDFViewer key={documentData.id} documentData={documentData} document={document} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div
|
||||
key={isExpanded ? 'expanded' : 'collapsed'}
|
||||
className="group/document-widget fixed bottom-6 left-0 z-50 h-fit max-h-[calc(100dvh-2rem)] w-full flex-shrink-0 px-4 md:sticky md:bottom-[unset] md:top-4 md:z-auto md:w-[350px] md:px-0"
|
||||
data-expanded={isExpanded || undefined}
|
||||
<div className="mt-8 grid grid-cols-12 gap-y-8 lg:gap-x-8 lg:gap-y-0">
|
||||
<Card
|
||||
className="col-span-12 rounded-xl before:rounded-xl lg:col-span-7 xl:col-span-8"
|
||||
gradient
|
||||
>
|
||||
<div className="border-border bg-widget flex w-full flex-col rounded-xl border px-4 py-4 md:py-6">
|
||||
<div className="flex items-center justify-between gap-x-2">
|
||||
<h3 className="text-foreground text-xl font-semibold md:text-2xl">
|
||||
{match(recipient.role)
|
||||
.with(RecipientRole.VIEWER, () => <Trans>View Document</Trans>)
|
||||
.with(RecipientRole.SIGNER, () => <Trans>Sign Document</Trans>)
|
||||
.with(RecipientRole.APPROVER, () => <Trans>Approve Document</Trans>)
|
||||
.with(RecipientRole.ASSISTANT, () => <Trans>Assist Document</Trans>)
|
||||
.otherwise(() => null)}
|
||||
</h3>
|
||||
<CardContent className="p-2">
|
||||
<PDFViewer key={documentData.id} documentData={documentData} document={document} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Button variant="outline" className="h-8 w-8 p-0 md:hidden">
|
||||
{isExpanded ? (
|
||||
<LucideChevronDown
|
||||
className="text-muted-foreground h-5 w-5"
|
||||
onClick={() => setIsExpanded(false)}
|
||||
/>
|
||||
) : (
|
||||
<LucideChevronUp
|
||||
className="text-muted-foreground h-5 w-5"
|
||||
onClick={() => setIsExpanded(true)}
|
||||
/>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="hidden group-data-[expanded]/document-widget:block md:block">
|
||||
<p className="text-muted-foreground mt-2 text-sm">
|
||||
{match(recipient.role)
|
||||
.with(RecipientRole.VIEWER, () => (
|
||||
<Trans>Please mark as viewed to complete.</Trans>
|
||||
))
|
||||
.with(RecipientRole.SIGNER, () => (
|
||||
<Trans>Please review the document before signing.</Trans>
|
||||
))
|
||||
.with(RecipientRole.APPROVER, () => (
|
||||
<Trans>Please review the document before approving.</Trans>
|
||||
))
|
||||
.with(RecipientRole.ASSISTANT, () => (
|
||||
<Trans>Complete the fields for the following signers.</Trans>
|
||||
))
|
||||
.otherwise(() => null)}
|
||||
</p>
|
||||
|
||||
<hr className="border-border mb-8 mt-4" />
|
||||
</div>
|
||||
|
||||
<div className="-mx-2 hidden px-2 group-data-[expanded]/document-widget:block md:block">
|
||||
<DocumentSigningForm
|
||||
document={document}
|
||||
recipient={recipient}
|
||||
fields={fields}
|
||||
redirectUrl={documentMeta?.redirectUrl}
|
||||
isRecipientsTurn={isRecipientsTurn}
|
||||
allRecipients={allRecipients}
|
||||
setSelectedSignerId={setSelectedSignerId}
|
||||
isEnterprise={isEnterprise}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-12 lg:col-span-5 xl:col-span-4">
|
||||
<DocumentSigningForm
|
||||
document={document}
|
||||
recipient={recipient}
|
||||
fields={fields}
|
||||
redirectUrl={documentMeta?.redirectUrl}
|
||||
isRecipientsTurn={isRecipientsTurn}
|
||||
allRecipients={allRecipients}
|
||||
setSelectedSignerId={setSelectedSignerId}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -227,8 +227,19 @@ export const DocumentSigningTextField = ({
|
||||
|
||||
const parsedField = field.fieldMeta ? ZTextFieldMeta.parse(field.fieldMeta) : undefined;
|
||||
|
||||
const labelDisplay = parsedField?.label;
|
||||
const textDisplay = parsedField?.text;
|
||||
const labelDisplay =
|
||||
parsedField?.label && parsedField.label.length < 20
|
||||
? parsedField.label
|
||||
: parsedField?.label
|
||||
? parsedField?.label.substring(0, 20) + '...'
|
||||
: undefined;
|
||||
|
||||
const textDisplay =
|
||||
parsedField?.text && parsedField.text.length < 20
|
||||
? parsedField.text
|
||||
: parsedField?.text
|
||||
? parsedField?.text.substring(0, 20) + '...'
|
||||
: undefined;
|
||||
|
||||
const fieldDisplayName = labelDisplay ? labelDisplay : textDisplay;
|
||||
const charactersRemaining = (parsedFieldMeta?.characterLimit ?? 0) - (localText.length ?? 0);
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { Trans, useLingui } from '@lingui/react/macro';
|
||||
import { Trash } from 'lucide-react';
|
||||
import { useFieldArray, useForm } from 'react-hook-form';
|
||||
|
||||
import { nanoid } from '@documenso/lib/universal/id';
|
||||
import { AttachmentType } from '@documenso/prisma/generated/types';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import type { TSetDocumentAttachmentsSchema } from '@documenso/trpc/server/document-router/set-document-attachments.types';
|
||||
import { ZSetDocumentAttachmentsSchema } from '@documenso/trpc/server/document-router/set-document-attachments.types';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from '@documenso/ui/primitives/dialog';
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@documenso/ui/primitives/form/form';
|
||||
import { Input } from '@documenso/ui/primitives/input';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
export type AttachmentFormProps = {
|
||||
documentId: number;
|
||||
};
|
||||
|
||||
export const AttachmentForm = ({ documentId }: AttachmentFormProps) => {
|
||||
const { t } = useLingui();
|
||||
const { toast } = useToast();
|
||||
|
||||
const { data: attachmentsData, refetch: refetchAttachments } =
|
||||
trpc.document.attachments.find.useQuery({
|
||||
documentId,
|
||||
});
|
||||
|
||||
const { mutateAsync: setDocumentAttachments } = trpc.document.attachments.set.useMutation();
|
||||
|
||||
const defaultAttachments = [
|
||||
{
|
||||
id: nanoid(12),
|
||||
label: '',
|
||||
url: '',
|
||||
type: AttachmentType.LINK,
|
||||
},
|
||||
];
|
||||
|
||||
const form = useForm<TSetDocumentAttachmentsSchema>({
|
||||
resolver: zodResolver(ZSetDocumentAttachmentsSchema),
|
||||
defaultValues: {
|
||||
documentId,
|
||||
attachments: attachmentsData ?? defaultAttachments,
|
||||
},
|
||||
});
|
||||
|
||||
const {
|
||||
fields: attachments,
|
||||
append: appendAttachment,
|
||||
remove: removeAttachment,
|
||||
} = useFieldArray({
|
||||
control: form.control,
|
||||
name: 'attachments',
|
||||
});
|
||||
|
||||
const onAddAttachment = () => {
|
||||
appendAttachment({
|
||||
id: nanoid(12),
|
||||
label: '',
|
||||
url: '',
|
||||
type: AttachmentType.LINK,
|
||||
});
|
||||
};
|
||||
|
||||
const onRemoveAttachment = (index: number) => {
|
||||
removeAttachment(index);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (attachmentsData && attachmentsData.length > 0) {
|
||||
form.setValue('attachments', attachmentsData);
|
||||
}
|
||||
}, [attachmentsData]);
|
||||
|
||||
const onSubmit = async (data: TSetDocumentAttachmentsSchema) => {
|
||||
try {
|
||||
await setDocumentAttachments({
|
||||
documentId,
|
||||
attachments: data.attachments,
|
||||
});
|
||||
|
||||
toast({
|
||||
title: t`Attachment(s) updated`,
|
||||
description: t`The attachment(s) have been updated successfully`,
|
||||
});
|
||||
|
||||
await refetchAttachments();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
|
||||
toast({
|
||||
title: t`Something went wrong`,
|
||||
description: t`We encountered an unknown error while attempting to create the attachments.`,
|
||||
variant: 'destructive',
|
||||
duration: 5000,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline">
|
||||
<Trans>Attachments</Trans>
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent position="center">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
<Trans>Attachments</Trans>
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)}>
|
||||
<fieldset disabled={form.formState.isSubmitting} className="space-y-4">
|
||||
{attachments.map((attachment, index) => (
|
||||
<div key={attachment.id} className="flex items-end gap-2">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={`attachments.${index}.label`}
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex-1">
|
||||
<FormLabel required>
|
||||
<Trans>Label</Trans>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder={t`Attachment label`} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={`attachments.${index}.url`}
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex-1">
|
||||
<FormLabel required>
|
||||
<Trans>URL</Trans>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="https://..." />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => onRemoveAttachment(index)}
|
||||
>
|
||||
<Trash className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</fieldset>
|
||||
<DialogFooter className="mt-4">
|
||||
<Button type="button" variant="outline" onClick={onAddAttachment}>
|
||||
<Trans>Add</Trans>
|
||||
</Button>
|
||||
<Button type="submit">
|
||||
<Trans>Save</Trans>
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -289,7 +289,7 @@ export const DocumentEditForm = ({
|
||||
message,
|
||||
distributionMethod,
|
||||
emailId,
|
||||
emailReplyTo: emailReplyTo || null,
|
||||
emailReplyTo,
|
||||
emailSettings: emailSettings,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -164,7 +164,7 @@ export const DocumentPageViewDropdown = ({ document }: DocumentPageViewDropdownP
|
||||
<DropdownMenuItem asChild>
|
||||
<Link to={`${documentsPath}/${document.id}/logs`}>
|
||||
<ScrollTextIcon className="mr-2 h-4 w-4" />
|
||||
<Trans>Audit Logs</Trans>
|
||||
<Trans>Audit Log</Trans>
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { Trans, useLingui } from '@lingui/react/macro';
|
||||
import { Trash } from 'lucide-react';
|
||||
import { useFieldArray, useForm } from 'react-hook-form';
|
||||
|
||||
import { nanoid } from '@documenso/lib/universal/id';
|
||||
import { AttachmentType } from '@documenso/prisma/generated/types';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import type { TSetTemplateAttachmentsSchema } from '@documenso/trpc/server/template-router/set-template-attachments.types';
|
||||
import { ZSetTemplateAttachmentsSchema } from '@documenso/trpc/server/template-router/set-template-attachments.types';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from '@documenso/ui/primitives/dialog';
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@documenso/ui/primitives/form/form';
|
||||
import { Input } from '@documenso/ui/primitives/input';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
export type AttachmentFormProps = {
|
||||
templateId: number;
|
||||
};
|
||||
|
||||
export const AttachmentForm = ({ templateId }: AttachmentFormProps) => {
|
||||
const { toast } = useToast();
|
||||
const { t } = useLingui();
|
||||
|
||||
const { data: attachmentsData, refetch: refetchAttachments } =
|
||||
trpc.template.attachments.find.useQuery({
|
||||
templateId,
|
||||
});
|
||||
|
||||
const { mutateAsync: setTemplateAttachments } = trpc.template.attachments.set.useMutation();
|
||||
|
||||
const defaultAttachments = [
|
||||
{
|
||||
id: nanoid(12),
|
||||
label: '',
|
||||
url: '',
|
||||
type: AttachmentType.LINK,
|
||||
},
|
||||
];
|
||||
|
||||
const form = useForm<TSetTemplateAttachmentsSchema>({
|
||||
resolver: zodResolver(ZSetTemplateAttachmentsSchema),
|
||||
defaultValues: {
|
||||
templateId,
|
||||
attachments: attachmentsData ?? defaultAttachments,
|
||||
},
|
||||
});
|
||||
|
||||
const {
|
||||
fields: attachments,
|
||||
append: appendAttachment,
|
||||
remove: removeAttachment,
|
||||
} = useFieldArray({
|
||||
control: form.control,
|
||||
name: 'attachments',
|
||||
});
|
||||
|
||||
const onAddAttachment = () => {
|
||||
appendAttachment({
|
||||
id: nanoid(12),
|
||||
label: '',
|
||||
url: '',
|
||||
type: AttachmentType.LINK,
|
||||
});
|
||||
};
|
||||
|
||||
const onRemoveAttachment = (index: number) => {
|
||||
removeAttachment(index);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (attachmentsData && attachmentsData.length > 0) {
|
||||
form.setValue('attachments', attachmentsData);
|
||||
}
|
||||
}, [attachmentsData]);
|
||||
|
||||
const onSubmit = async (data: TSetTemplateAttachmentsSchema) => {
|
||||
try {
|
||||
await setTemplateAttachments({
|
||||
templateId,
|
||||
attachments: data.attachments,
|
||||
});
|
||||
|
||||
toast({
|
||||
title: t`Attachment(s) updated`,
|
||||
description: t`The attachment(s) have been updated successfully`,
|
||||
});
|
||||
|
||||
await refetchAttachments();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
|
||||
toast({
|
||||
title: t`Something went wrong`,
|
||||
description: t`We encountered an unknown error while attempting to create the attachments.`,
|
||||
variant: 'destructive',
|
||||
duration: 5000,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline">
|
||||
<Trans>Attachments</Trans>
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent position="center">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
<Trans>Attachments</Trans>
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)}>
|
||||
<fieldset disabled={form.formState.isSubmitting} className="space-y-4">
|
||||
{attachments.map((attachment, index) => (
|
||||
<div key={attachment.id} className="flex items-end gap-2">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={`attachments.${index}.label`}
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex-1">
|
||||
<FormLabel required>
|
||||
<Trans>Label</Trans>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder={t`Attachment label`} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={`attachments.${index}.url`}
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex-1">
|
||||
<FormLabel required>
|
||||
<Trans>URL</Trans>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder={t`https://...`} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => onRemoveAttachment(index)}
|
||||
>
|
||||
<Trash className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</fieldset>
|
||||
<DialogFooter className="mt-4">
|
||||
<Button type="button" variant="outline" onClick={onAddAttachment}>
|
||||
<Trans>Add</Trans>
|
||||
</Button>
|
||||
<Button type="submit">
|
||||
<Trans>Save</Trans>
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -1,129 +0,0 @@
|
||||
import { type ReactNode, useState } from 'react';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { Loader } from 'lucide-react';
|
||||
import { useDropzone } from 'react-dropzone';
|
||||
import { useNavigate, useParams } from 'react-router';
|
||||
|
||||
import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT } from '@documenso/lib/constants/app';
|
||||
import { megabytesToBytes } from '@documenso/lib/universal/unit-convertions';
|
||||
import { putPdfFile } from '@documenso/lib/universal/upload/put-file';
|
||||
import { formatTemplatesPath } from '@documenso/lib/utils/teams';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
import { useCurrentTeam } from '~/providers/team';
|
||||
|
||||
export interface TemplateDropZoneWrapperProps {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const TemplateDropZoneWrapper = ({ children, className }: TemplateDropZoneWrapperProps) => {
|
||||
const { _ } = useLingui();
|
||||
const { toast } = useToast();
|
||||
const { folderId } = useParams();
|
||||
|
||||
const team = useCurrentTeam();
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const { mutateAsync: createTemplate } = trpc.template.createTemplate.useMutation();
|
||||
|
||||
const onFileDrop = async (file: File) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
const documentData = await putPdfFile(file);
|
||||
|
||||
const { id } = await createTemplate({
|
||||
title: file.name,
|
||||
templateDocumentDataId: documentData.id,
|
||||
folderId: folderId ?? undefined,
|
||||
});
|
||||
|
||||
toast({
|
||||
title: _(msg`Template uploaded`),
|
||||
description: _(
|
||||
msg`Your template has been uploaded successfully. You will be redirected to the template page.`,
|
||||
),
|
||||
duration: 5000,
|
||||
});
|
||||
|
||||
await navigate(`${formatTemplatesPath(team.url)}/${id}/edit`);
|
||||
} catch {
|
||||
toast({
|
||||
title: _(msg`Something went wrong`),
|
||||
description: _(msg`Please try again later.`),
|
||||
variant: 'destructive',
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const onFileDropRejected = () => {
|
||||
toast({
|
||||
title: _(msg`Your template failed to upload.`),
|
||||
description: _(msg`File cannot be larger than ${APP_DOCUMENT_UPLOAD_SIZE_LIMIT}MB`),
|
||||
duration: 5000,
|
||||
variant: 'destructive',
|
||||
});
|
||||
};
|
||||
|
||||
const { getRootProps, getInputProps, isDragActive } = useDropzone({
|
||||
accept: {
|
||||
'application/pdf': ['.pdf'],
|
||||
},
|
||||
//disabled: isUploadDisabled,
|
||||
multiple: false,
|
||||
maxSize: megabytesToBytes(APP_DOCUMENT_UPLOAD_SIZE_LIMIT),
|
||||
onDrop: ([acceptedFile]) => {
|
||||
if (acceptedFile) {
|
||||
void onFileDrop(acceptedFile);
|
||||
}
|
||||
},
|
||||
onDropRejected: () => {
|
||||
void onFileDropRejected();
|
||||
},
|
||||
noClick: true,
|
||||
noDragEventsBubbling: true,
|
||||
});
|
||||
|
||||
return (
|
||||
<div {...getRootProps()} className={cn('relative min-h-screen', className)}>
|
||||
<input {...getInputProps()} />
|
||||
{children}
|
||||
|
||||
{isDragActive && (
|
||||
<div className="bg-muted/60 fixed left-0 top-0 z-[9999] h-full w-full backdrop-blur-[4px]">
|
||||
<div className="pointer-events-none flex h-full w-full flex-col items-center justify-center">
|
||||
<h2 className="text-foreground text-2xl font-semibold">
|
||||
<Trans>Upload Template</Trans>
|
||||
</h2>
|
||||
|
||||
<p className="text-muted-foreground text-md mt-4">
|
||||
<Trans>Drag and drop your PDF file here</Trans>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isLoading && (
|
||||
<div className="bg-muted/30 absolute inset-0 z-50 backdrop-blur-[2px]">
|
||||
<div className="pointer-events-none flex h-1/2 w-full flex-col items-center justify-center">
|
||||
<Loader className="text-primary h-12 w-12 animate-spin" />
|
||||
<p className="text-foreground mt-8 font-medium">
|
||||
<Trans>Uploading template...</Trans>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -143,7 +143,6 @@ export const TemplateEditForm = ({
|
||||
},
|
||||
meta: {
|
||||
...data.meta,
|
||||
emailReplyTo: data.meta.emailReplyTo || null,
|
||||
typedSignatureEnabled: signatureTypes.includes(DocumentSignatureType.TYPE),
|
||||
uploadSignatureEnabled: signatureTypes.includes(DocumentSignatureType.UPLOAD),
|
||||
drawSignatureEnabled: signatureTypes.includes(DocumentSignatureType.DRAW),
|
||||
@@ -157,7 +156,7 @@ export const TemplateEditForm = ({
|
||||
|
||||
toast({
|
||||
title: _(msg`Error`),
|
||||
description: _(msg`An error occurred while updating the template settings.`),
|
||||
description: _(msg`An error occurred while updating the document settings.`),
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -6,8 +6,6 @@ import { PenIcon, PlusIcon } from 'lucide-react';
|
||||
import { Link } from 'react-router';
|
||||
|
||||
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
|
||||
import { isTemplateRecipientEmailPlaceholder } from '@documenso/lib/constants/template';
|
||||
import { extractInitials } from '@documenso/lib/utils/recipient-formatter';
|
||||
import { AvatarWithText } from '@documenso/ui/primitives/avatar';
|
||||
|
||||
export type TemplatePageViewRecipientsProps = {
|
||||
@@ -55,18 +53,8 @@ export const TemplatePageViewRecipients = ({
|
||||
{recipients.map((recipient) => (
|
||||
<li key={recipient.id} className="flex items-center justify-between px-4 py-2.5 text-sm">
|
||||
<AvatarWithText
|
||||
avatarFallback={
|
||||
isTemplateRecipientEmailPlaceholder(recipient.email)
|
||||
? extractInitials(recipient.name)
|
||||
: recipient.email.slice(0, 1).toUpperCase()
|
||||
}
|
||||
primaryText={
|
||||
isTemplateRecipientEmailPlaceholder(recipient.email) ? (
|
||||
<p className="text-muted-foreground text-sm">{recipient.name}</p>
|
||||
) : (
|
||||
<p className="text-muted-foreground text-sm">{recipient.email}</p>
|
||||
)
|
||||
}
|
||||
avatarFallback={recipient.email.slice(0, 1).toUpperCase()}
|
||||
primaryText={<p className="text-muted-foreground text-sm">{recipient.email}</p>}
|
||||
secondaryText={
|
||||
<p className="text-muted-foreground/70 text-xs">
|
||||
{_(RECIPIENT_ROLES_DESCRIPTION[recipient.role].roleName)}
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import type { DateTimeFormatOptions } from 'luxon';
|
||||
import { DateTime } from 'luxon';
|
||||
import { P, match } from 'ts-pattern';
|
||||
import type { DateTimeFormatOptions } from 'luxon';
|
||||
import { UAParser } from 'ua-parser-js';
|
||||
|
||||
import { APP_I18N_OPTIONS } from '@documenso/lib/constants/i18n';
|
||||
import {
|
||||
DOCUMENT_AUDIT_LOG_TYPE,
|
||||
type TDocumentAuditLog,
|
||||
} from '@documenso/lib/types/document-audit-logs';
|
||||
import type { TDocumentAuditLog } from '@documenso/lib/types/document-audit-logs';
|
||||
import { formatDocumentAuditLogAction } from '@documenso/lib/utils/document-audit-logs';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@documenso/ui/primitives/table';
|
||||
|
||||
export type AuditLogDataTableProps = {
|
||||
logs: TDocumentAuditLog[];
|
||||
@@ -23,129 +25,71 @@ const dateFormat: DateTimeFormatOptions = {
|
||||
hourCycle: 'h12',
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the color indicator for the audit log type
|
||||
*/
|
||||
|
||||
const getAuditLogIndicatorColor = (type: string) =>
|
||||
match(type)
|
||||
.with(DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_COMPLETED, () => 'bg-green-500')
|
||||
.with(DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_REJECTED, () => 'bg-red-500')
|
||||
.with(DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_SENT, () => 'bg-orange-500')
|
||||
.with(
|
||||
P.union(
|
||||
DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FIELD_INSERTED,
|
||||
DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FIELD_UNINSERTED,
|
||||
),
|
||||
() => 'bg-blue-500',
|
||||
)
|
||||
.otherwise(() => 'bg-muted');
|
||||
|
||||
/**
|
||||
* DO NOT USE TRANS. YOU MUST USE _ FOR THIS FILE AND ALL CHILDREN COMPONENTS.
|
||||
*/
|
||||
|
||||
const formatUserAgent = (userAgent: string | null | undefined, userAgentInfo: UAParser.IResult) => {
|
||||
if (!userAgent) {
|
||||
return msg`N/A`;
|
||||
}
|
||||
|
||||
const browser = userAgentInfo.browser.name;
|
||||
const version = userAgentInfo.browser.version;
|
||||
const os = userAgentInfo.os.name;
|
||||
|
||||
// If we can parse meaningful browser info, format it nicely
|
||||
if (browser && os) {
|
||||
const browserInfo = version ? `${browser} ${version}` : browser;
|
||||
|
||||
return msg`${browserInfo} on ${os}`;
|
||||
}
|
||||
|
||||
return msg`${userAgent}`;
|
||||
};
|
||||
|
||||
export const InternalAuditLogTable = ({ logs }: AuditLogDataTableProps) => {
|
||||
const { _ } = useLingui();
|
||||
|
||||
const parser = new UAParser();
|
||||
|
||||
const uppercaseFistLetter = (text: string) => {
|
||||
return text.charAt(0).toUpperCase() + text.slice(1);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{logs.map((log, index) => {
|
||||
parser.setUA(log.userAgent || '');
|
||||
const formattedAction = formatDocumentAuditLogAction(_, log);
|
||||
const userAgentInfo = parser.getResult();
|
||||
<Table overflowHidden>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>{_(msg`Time`)}</TableHead>
|
||||
<TableHead>{_(msg`User`)}</TableHead>
|
||||
<TableHead>{_(msg`Action`)}</TableHead>
|
||||
<TableHead>{_(msg`IP Address`)}</TableHead>
|
||||
<TableHead>{_(msg`Browser`)}</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
|
||||
return (
|
||||
<Card
|
||||
key={index}
|
||||
// Add top margin for the first card to ensure it's not cut off from the 2nd page onwards
|
||||
className={`border shadow-sm ${index > 0 ? 'print:mt-8' : ''}`}
|
||||
style={{
|
||||
pageBreakInside: 'avoid',
|
||||
breakInside: 'avoid',
|
||||
}}
|
||||
>
|
||||
<CardContent className="p-4">
|
||||
{/* Header Section with indicator, event type, and timestamp */}
|
||||
<div className="mb-3 flex items-start justify-between">
|
||||
<div className="flex items-baseline gap-3">
|
||||
<div
|
||||
className={cn(`h-2 w-2 rounded-full`, getAuditLogIndicatorColor(log.type))}
|
||||
/>
|
||||
<TableBody className="print:text-xs">
|
||||
{logs.map((log, i) => (
|
||||
<TableRow className="break-inside-avoid" key={i}>
|
||||
<TableCell>
|
||||
{DateTime.fromJSDate(log.createdAt)
|
||||
.setLocale(APP_I18N_OPTIONS.defaultLocale)
|
||||
.toLocaleString(dateFormat)}
|
||||
</TableCell>
|
||||
|
||||
<div>
|
||||
<div className="text-muted-foreground text-sm font-medium uppercase tracking-wide print:text-[8pt]">
|
||||
{log.type.replace(/_/g, ' ')}
|
||||
</div>
|
||||
|
||||
<div className="text-foreground text-sm font-medium print:text-[8pt]">
|
||||
{formattedAction.description}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-muted-foreground text-sm print:text-[8pt]">
|
||||
{DateTime.fromJSDate(log.createdAt)
|
||||
.setLocale(APP_I18N_OPTIONS.defaultLocale)
|
||||
.toLocaleString(dateFormat)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr className="my-4" />
|
||||
|
||||
{/* Details Section - Two column layout */}
|
||||
<div className="grid grid-cols-2 gap-x-8 gap-y-2 text-xs print:text-[6pt]">
|
||||
<TableCell>
|
||||
{log.name || log.email ? (
|
||||
<div>
|
||||
<div className="text-muted-foreground/70 font-medium uppercase tracking-wide">
|
||||
{_(msg`User`)}
|
||||
</div>
|
||||
{log.name && (
|
||||
<p className="break-all" title={log.name}>
|
||||
{log.name}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="text-foreground mt-1 font-mono">{log.email || 'N/A'}</div>
|
||||
{log.email && (
|
||||
<p className="text-muted-foreground break-all" title={log.email}>
|
||||
{log.email}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<p>N/A</p>
|
||||
)}
|
||||
</TableCell>
|
||||
|
||||
<div className="text-right">
|
||||
<div className="text-muted-foreground/70 font-medium uppercase tracking-wide">
|
||||
{_(msg`IP Address`)}
|
||||
</div>
|
||||
<TableCell>
|
||||
{uppercaseFistLetter(formatDocumentAuditLogAction(_, log).description)}
|
||||
</TableCell>
|
||||
|
||||
<div className="text-foreground mt-1 font-mono">{log.ipAddress || 'N/A'}</div>
|
||||
</div>
|
||||
<TableCell>{log.ipAddress}</TableCell>
|
||||
|
||||
<div className="col-span-2">
|
||||
<div className="text-muted-foreground/70 font-medium uppercase tracking-wide">
|
||||
{_(msg`User Agent`)}
|
||||
</div>
|
||||
|
||||
<div className="text-foreground mt-1">
|
||||
{_(formatUserAgent(log.userAgent, userAgentInfo))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<TableCell>
|
||||
{log.userAgent ? parser.setUA(log.userAgent).getBrowser().name : 'N/A'}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -46,7 +46,9 @@ export const TemplatesTableActionDropdown = ({
|
||||
const isOwner = row.userId === user.id;
|
||||
const isTeamTemplate = row.teamId === teamId;
|
||||
|
||||
const formatPath = `${templateRootPath}/${row.id}/edit`;
|
||||
const formatPath = row.folderId
|
||||
? `${templateRootPath}/f/${row.folderId}/${row.id}/edit`
|
||||
: `${templateRootPath}/${row.id}/edit`;
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
|
||||
@@ -46,7 +46,6 @@ export default function OrganisationSettingsDocumentPage() {
|
||||
documentDateFormat,
|
||||
includeSenderDetails,
|
||||
includeSigningCertificate,
|
||||
includeAuditLog,
|
||||
signatureTypes,
|
||||
} = data;
|
||||
|
||||
@@ -55,8 +54,7 @@ export default function OrganisationSettingsDocumentPage() {
|
||||
documentLanguage === null ||
|
||||
documentDateFormat === null ||
|
||||
includeSenderDetails === null ||
|
||||
includeSigningCertificate === null ||
|
||||
includeAuditLog === null
|
||||
includeSigningCertificate === null
|
||||
) {
|
||||
throw new Error('Should not be possible.');
|
||||
}
|
||||
@@ -70,7 +68,6 @@ export default function OrganisationSettingsDocumentPage() {
|
||||
documentDateFormat,
|
||||
includeSenderDetails,
|
||||
includeSigningCertificate,
|
||||
includeAuditLog,
|
||||
typedSignatureEnabled: signatureTypes.includes(DocumentSignatureType.TYPE),
|
||||
uploadSignatureEnabled: signatureTypes.includes(DocumentSignatureType.UPLOAD),
|
||||
drawSignatureEnabled: signatureTypes.includes(DocumentSignatureType.DRAW),
|
||||
|
||||
@@ -171,7 +171,7 @@ export default function OrganisationEmailDomainSettingsPage({ params }: Route.Co
|
||||
<OrganisationEmailDomainRecordsDialog
|
||||
records={records}
|
||||
trigger={
|
||||
<Button variant="outline">
|
||||
<Button variant="secondary">
|
||||
<Trans>View DNS Records</Trans>
|
||||
</Button>
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ import { useSession } from '@documenso/lib/client-only/providers/session';
|
||||
import { getDocumentWithDetailsById } from '@documenso/lib/server-only/document/get-document-with-details-by-id';
|
||||
import { getTeamByUrl } from '@documenso/lib/server-only/team/get-team';
|
||||
import { DocumentVisibility } from '@documenso/lib/types/document-visibility';
|
||||
import { logDocumentAccess } from '@documenso/lib/utils/logger';
|
||||
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
|
||||
import { DocumentReadOnlyFields } from '@documenso/ui/components/document/document-read-only-fields';
|
||||
import { Badge } from '@documenso/ui/primitives/badge';
|
||||
@@ -84,12 +83,6 @@ export async function loader({ params, request }: Route.LoaderArgs) {
|
||||
throw redirect(documentRootPath);
|
||||
}
|
||||
|
||||
logDocumentAccess({
|
||||
request,
|
||||
documentId,
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
return superLoaderJson({
|
||||
document,
|
||||
documentRootPath,
|
||||
|
||||
@@ -9,9 +9,9 @@ import { getDocumentWithDetailsById } from '@documenso/lib/server-only/document/
|
||||
import { getTeamByUrl } from '@documenso/lib/server-only/team/get-team';
|
||||
import { DocumentVisibility } from '@documenso/lib/types/document-visibility';
|
||||
import { isDocumentCompleted } from '@documenso/lib/utils/document';
|
||||
import { logDocumentAccess } from '@documenso/lib/utils/logger';
|
||||
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
|
||||
|
||||
import { AttachmentForm } from '~/components/general/document/document-attachment-form';
|
||||
import { DocumentEditForm } from '~/components/general/document/document-edit-form';
|
||||
import { DocumentStatus } from '~/components/general/document/document-status';
|
||||
import { LegacyFieldWarningPopover } from '~/components/general/legacy-field-warning-popover';
|
||||
@@ -79,12 +79,6 @@ export async function loader({ params, request }: Route.LoaderArgs) {
|
||||
throw redirect(`${documentRootPath}/${documentId}`);
|
||||
}
|
||||
|
||||
logDocumentAccess({
|
||||
request,
|
||||
documentId,
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
return superLoaderJson({
|
||||
document: {
|
||||
...document,
|
||||
@@ -106,7 +100,7 @@ export default function DocumentEditPage() {
|
||||
<Trans>Documents</Trans>
|
||||
</Link>
|
||||
|
||||
<div className="mt-4 flex w-full items-end justify-between">
|
||||
<div className="mt-4 flex w-full flex-col items-start justify-between gap-4 sm:flex-row sm:items-center">
|
||||
<div className="flex-1">
|
||||
<h1
|
||||
className="block max-w-[20rem] truncate text-2xl font-semibold md:max-w-[30rem] md:text-3xl"
|
||||
@@ -140,11 +134,12 @@ export default function DocumentEditPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{document.useLegacyFieldInsertion && (
|
||||
<div>
|
||||
<div className={document.useLegacyFieldInsertion ? 'flex items-center gap-2' : undefined}>
|
||||
{document.useLegacyFieldInsertion && (
|
||||
<LegacyFieldWarningPopover type="document" documentId={document.id} />
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
<AttachmentForm documentId={document.id} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DocumentEditForm
|
||||
|
||||
@@ -11,7 +11,6 @@ import { getSession } from '@documenso/auth/server/lib/utils/get-session';
|
||||
import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id';
|
||||
import { getRecipientsForDocument } from '@documenso/lib/server-only/recipient/get-recipients-for-document';
|
||||
import { getTeamByUrl } from '@documenso/lib/server-only/team/get-team';
|
||||
import { logDocumentAccess } from '@documenso/lib/utils/logger';
|
||||
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
|
||||
import { Card } from '@documenso/ui/primitives/card';
|
||||
|
||||
@@ -60,12 +59,6 @@ export async function loader({ params, request }: Route.LoaderArgs) {
|
||||
teamId: team?.id,
|
||||
});
|
||||
|
||||
logDocumentAccess({
|
||||
request,
|
||||
documentId,
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
return {
|
||||
document,
|
||||
documentRootPath,
|
||||
@@ -177,7 +170,7 @@ export default function DocumentsLogsPage({ loaderData }: Route.ComponentProps)
|
||||
<ul className="text-muted-foreground list-inside list-disc">
|
||||
{recipients.map((recipient) => (
|
||||
<li key={`recipient-${recipient.id}`}>
|
||||
<span>{formatRecipientText(recipient)}</span>
|
||||
<span className="-ml-2">{formatRecipientText(recipient)}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
@@ -38,7 +38,6 @@ export default function TeamsSettingsPage() {
|
||||
documentDateFormat,
|
||||
includeSenderDetails,
|
||||
includeSigningCertificate,
|
||||
includeAuditLog,
|
||||
signatureTypes,
|
||||
} = data;
|
||||
|
||||
@@ -51,7 +50,6 @@ export default function TeamsSettingsPage() {
|
||||
documentDateFormat,
|
||||
includeSenderDetails,
|
||||
includeSigningCertificate,
|
||||
includeAuditLog,
|
||||
...(signatureTypes.length === 0
|
||||
? {
|
||||
typedSignatureEnabled: null,
|
||||
|
||||
@@ -9,6 +9,7 @@ import { formatTemplatesPath } from '@documenso/lib/utils/teams';
|
||||
|
||||
import { TemplateDirectLinkDialogWrapper } from '~/components/dialogs/template-direct-link-dialog-wrapper';
|
||||
import { LegacyFieldWarningPopover } from '~/components/general/legacy-field-warning-popover';
|
||||
import { AttachmentForm } from '~/components/general/template/template-attachment-form';
|
||||
import { TemplateDirectLinkBadge } from '~/components/general/template/template-direct-link-badge';
|
||||
import { TemplateEditForm } from '~/components/general/template/template-edit-form';
|
||||
import { TemplateType } from '~/components/general/template/template-type';
|
||||
@@ -89,6 +90,7 @@ export default function TemplateEditPage() {
|
||||
|
||||
<div className="mt-2 flex items-center gap-2 sm:mt-0 sm:self-end">
|
||||
<TemplateDirectLinkDialogWrapper template={template} />
|
||||
<AttachmentForm templateId={template.id} />
|
||||
|
||||
{template.useLegacyFieldInsertion && (
|
||||
<div>
|
||||
|
||||
@@ -9,7 +9,6 @@ import { trpc } from '@documenso/trpc/react';
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@documenso/ui/primitives/avatar';
|
||||
|
||||
import { FolderGrid } from '~/components/general/folder/folder-grid';
|
||||
import { TemplateDropZoneWrapper } from '~/components/general/template/template-drop-zone-wrapper';
|
||||
import { TemplatesTable } from '~/components/tables/templates-table';
|
||||
import { useCurrentTeam } from '~/providers/team';
|
||||
import { appMetaTags } from '~/utils/meta';
|
||||
@@ -37,54 +36,51 @@ export default function TemplatesPage() {
|
||||
});
|
||||
|
||||
return (
|
||||
<TemplateDropZoneWrapper>
|
||||
<div className="mx-auto max-w-screen-xl px-4 md:px-8">
|
||||
<FolderGrid type={FolderType.TEMPLATE} parentId={folderId ?? null} />
|
||||
<div className="mx-auto max-w-screen-xl px-4 md:px-8">
|
||||
<FolderGrid type={FolderType.TEMPLATE} parentId={folderId ?? null} />
|
||||
|
||||
<div className="mt-8">
|
||||
<div className="flex flex-row items-center">
|
||||
<Avatar className="dark:border-border mr-3 h-12 w-12 border-2 border-solid border-white">
|
||||
{team.avatarImageId && <AvatarImage src={formatAvatarUrl(team.avatarImageId)} />}
|
||||
<AvatarFallback className="text-muted-foreground text-xs">
|
||||
{team.name.slice(0, 1)}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
|
||||
<h1 className="truncate text-2xl font-semibold md:text-3xl">
|
||||
<Trans>Templates</Trans>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div className="mt-8">
|
||||
<div className="flex flex-row items-center">
|
||||
<Avatar className="dark:border-border mr-3 h-12 w-12 border-2 border-solid border-white">
|
||||
{team.avatarImageId && <AvatarImage src={formatAvatarUrl(team.avatarImageId)} />}
|
||||
<AvatarFallback className="text-muted-foreground text-xs">
|
||||
{team.name.slice(0, 1)}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
{data && data.count === 0 ? (
|
||||
<div className="text-muted-foreground/60 flex h-96 flex-col items-center justify-center gap-y-4">
|
||||
<Bird className="h-12 w-12" strokeWidth={1.5} />
|
||||
|
||||
<h1 className="truncate text-2xl font-semibold md:text-3xl">
|
||||
<Trans>Templates</Trans>
|
||||
</h1>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<h3 className="text-lg font-semibold">
|
||||
<Trans>We're all empty</Trans>
|
||||
</h3>
|
||||
|
||||
<div className="mt-8">
|
||||
{data && data.count === 0 ? (
|
||||
<div className="text-muted-foreground/60 flex h-96 flex-col items-center justify-center gap-y-4">
|
||||
<Bird className="h-12 w-12" strokeWidth={1.5} />
|
||||
|
||||
<div className="text-center">
|
||||
<h3 className="text-lg font-semibold">
|
||||
<Trans>We're all empty</Trans>
|
||||
</h3>
|
||||
|
||||
<p className="mt-2 max-w-[50ch]">
|
||||
<Trans>
|
||||
You have not yet created any templates. To create a template please upload
|
||||
one.
|
||||
</Trans>
|
||||
</p>
|
||||
</div>
|
||||
<p className="mt-2 max-w-[50ch]">
|
||||
<Trans>
|
||||
You have not yet created any templates. To create a template please upload one.
|
||||
</Trans>
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<TemplatesTable
|
||||
data={data}
|
||||
isLoading={isLoading}
|
||||
isLoadingError={isLoadingError}
|
||||
documentRootPath={documentRootPath}
|
||||
templateRootPath={templateRootPath}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<TemplatesTable
|
||||
data={data}
|
||||
isLoading={isLoading}
|
||||
isLoadingError={isLoadingError}
|
||||
documentRootPath={documentRootPath}
|
||||
templateRootPath={templateRootPath}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</TemplateDropZoneWrapper>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
@media print {
|
||||
html {
|
||||
font-size: 10pt;
|
||||
}
|
||||
}
|
||||
@@ -12,17 +12,10 @@ import { findDocumentAuditLogs } from '@documenso/lib/server-only/document/find-
|
||||
import { getTranslations } from '@documenso/lib/utils/i18n';
|
||||
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
||||
|
||||
import appStylesheet from '~/app.css?url';
|
||||
import { BrandingLogo } from '~/components/general/branding-logo';
|
||||
import { InternalAuditLogTable } from '~/components/tables/internal-audit-log-table';
|
||||
|
||||
import type { Route } from './+types/audit-log';
|
||||
import auditLogStylesheet from './audit-log.print.css?url';
|
||||
|
||||
export const links: Route.LinksFunction = () => [
|
||||
{ rel: 'stylesheet', href: appStylesheet },
|
||||
{ rel: 'stylesheet', href: auditLogStylesheet },
|
||||
];
|
||||
|
||||
export async function loader({ request }: Route.LoaderArgs) {
|
||||
const d = new URL(request.url).searchParams.get('d');
|
||||
@@ -83,8 +76,8 @@ export default function AuditLog({ loaderData }: Route.ComponentProps) {
|
||||
|
||||
return (
|
||||
<div className="print-provider pointer-events-none mx-auto max-w-screen-md">
|
||||
<div className="mb-6 border-b pb-4">
|
||||
<h1 className="text-xl font-semibold">{_(msg`Audit Log`)}</h1>
|
||||
<div className="flex items-center">
|
||||
<h1 className="my-8 text-2xl font-bold">{_(msg`Version History`)}</h1>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
@@ -164,9 +157,11 @@ export default function AuditLog({ loaderData }: Route.ComponentProps) {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="mt-8">
|
||||
<InternalAuditLogTable logs={auditLogs} />
|
||||
</div>
|
||||
<Card className="mt-8">
|
||||
<CardContent className="p-0">
|
||||
<InternalAuditLogTable logs={auditLogs} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="my-8 flex-row-reverse">
|
||||
<div className="flex items-end justify-end gap-x-4">
|
||||
|
||||
@@ -94,7 +94,6 @@ export default function DirectTemplatePage() {
|
||||
documentAuthOptions={template.authOptions}
|
||||
recipient={directTemplateRecipient}
|
||||
user={user}
|
||||
isEnterprise={false}
|
||||
>
|
||||
<div className="mx-auto -mt-4 w-full max-w-screen-xl px-4 md:px-8">
|
||||
<h1
|
||||
|
||||
@@ -19,7 +19,6 @@ import { getRecipientSignatures } from '@documenso/lib/server-only/recipient/get
|
||||
import { getRecipientsForAssistant } from '@documenso/lib/server-only/recipient/get-recipients-for-assistant';
|
||||
import { getTeamSettings } from '@documenso/lib/server-only/team/get-team-settings';
|
||||
import { getUserByEmail } from '@documenso/lib/server-only/user/get-user-by-email';
|
||||
import { isUserEnterprise } from '@documenso/lib/server-only/user/is-user-enterprise';
|
||||
import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
|
||||
import { SigningCard3D } from '@documenso/ui/components/signing-card';
|
||||
|
||||
@@ -42,10 +41,6 @@ export async function loader({ params, request }: Route.LoaderArgs) {
|
||||
throw new Response('Not Found', { status: 404 });
|
||||
}
|
||||
|
||||
const isEnterprise = user?.id
|
||||
? await isUserEnterprise({ userId: user.id }).catch(() => false)
|
||||
: false;
|
||||
|
||||
const [document, recipient, fields, completedFields] = await Promise.all([
|
||||
getDocumentAndSenderByToken({
|
||||
token,
|
||||
@@ -121,7 +116,6 @@ export async function loader({ params, request }: Route.LoaderArgs) {
|
||||
isDocumentAccessValid: false,
|
||||
recipientEmail: recipient.email,
|
||||
recipientHasAccount,
|
||||
isEnterprise,
|
||||
} as const);
|
||||
}
|
||||
|
||||
@@ -159,7 +153,6 @@ export async function loader({ params, request }: Route.LoaderArgs) {
|
||||
recipientSignature,
|
||||
isRecipientsTurn,
|
||||
includeSenderDetails: settings.includeSenderDetails,
|
||||
isEnterprise,
|
||||
} as const);
|
||||
}
|
||||
|
||||
@@ -188,7 +181,6 @@ export default function SigningPage() {
|
||||
allRecipients,
|
||||
includeSenderDetails,
|
||||
recipientWithFields,
|
||||
isEnterprise,
|
||||
} = data;
|
||||
|
||||
if (document.deletedAt || document.status === DocumentStatus.REJECTED) {
|
||||
@@ -254,7 +246,6 @@ export default function SigningPage() {
|
||||
documentAuthOptions={document.authOptions}
|
||||
recipient={recipient}
|
||||
user={user}
|
||||
isEnterprise={isEnterprise}
|
||||
>
|
||||
<DocumentSigningPageView
|
||||
recipient={recipientWithFields}
|
||||
@@ -264,7 +255,6 @@ export default function SigningPage() {
|
||||
isRecipientsTurn={isRecipientsTurn}
|
||||
allRecipients={allRecipients}
|
||||
includeSenderDetails={includeSenderDetails}
|
||||
isEnterprise={isEnterprise}
|
||||
/>
|
||||
</DocumentSigningAuthProvider>
|
||||
</DocumentSigningProvider>
|
||||
|
||||
@@ -88,8 +88,6 @@ export async function loader({ params, request }: Route.LoaderArgs) {
|
||||
|
||||
const fields = template.fields.filter((field) => field.recipientId === directTemplateRecipientId);
|
||||
|
||||
const isEnterpriseDocument = Boolean(organisationClaim);
|
||||
|
||||
return superLoaderJson({
|
||||
token,
|
||||
user,
|
||||
@@ -98,21 +96,12 @@ export async function loader({ params, request }: Route.LoaderArgs) {
|
||||
fields,
|
||||
hidePoweredBy,
|
||||
allowEmbedSigningWhitelabel,
|
||||
isEnterpriseDocument,
|
||||
});
|
||||
}
|
||||
|
||||
export default function EmbedDirectTemplatePage() {
|
||||
const {
|
||||
token,
|
||||
user,
|
||||
template,
|
||||
recipient,
|
||||
fields,
|
||||
hidePoweredBy,
|
||||
allowEmbedSigningWhitelabel,
|
||||
isEnterpriseDocument,
|
||||
} = useSuperLoaderData<typeof loader>();
|
||||
const { token, user, template, recipient, fields, hidePoweredBy, allowEmbedSigningWhitelabel } =
|
||||
useSuperLoaderData<typeof loader>();
|
||||
|
||||
return (
|
||||
<DocumentSigningProvider
|
||||
@@ -127,7 +116,6 @@ export default function EmbedDirectTemplatePage() {
|
||||
documentAuthOptions={template.authOptions}
|
||||
recipient={recipient}
|
||||
user={user}
|
||||
isEnterprise={isEnterpriseDocument}
|
||||
>
|
||||
<DocumentSigningRecipientProvider recipient={recipient}>
|
||||
<EmbedDirectTemplateClientPage
|
||||
|
||||
@@ -109,8 +109,6 @@ export async function loader({ params, request }: Route.LoaderArgs) {
|
||||
})
|
||||
: [];
|
||||
|
||||
const isEnterpriseDocument = Boolean(organisationClaim);
|
||||
|
||||
return superLoaderJson({
|
||||
token,
|
||||
user,
|
||||
@@ -121,7 +119,6 @@ export async function loader({ params, request }: Route.LoaderArgs) {
|
||||
completedFields,
|
||||
hidePoweredBy,
|
||||
allowEmbedSigningWhitelabel,
|
||||
isEnterpriseDocument,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -136,7 +133,6 @@ export default function EmbedSignDocumentPage() {
|
||||
completedFields,
|
||||
hidePoweredBy,
|
||||
allowEmbedSigningWhitelabel,
|
||||
isEnterpriseDocument,
|
||||
} = useSuperLoaderData<typeof loader>();
|
||||
|
||||
return (
|
||||
@@ -152,7 +148,6 @@ export default function EmbedSignDocumentPage() {
|
||||
documentAuthOptions={document.authOptions}
|
||||
recipient={recipient}
|
||||
user={user}
|
||||
isEnterprise={isEnterpriseDocument}
|
||||
>
|
||||
<EmbedSignDocumentClientPage
|
||||
token={token}
|
||||
|
||||
@@ -48,7 +48,6 @@ export async function loader({ request }: Route.LoaderArgs) {
|
||||
user,
|
||||
hidePoweredBy: false,
|
||||
allowWhitelabelling: false,
|
||||
isEnterprise: false,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -56,19 +55,17 @@ export async function loader({ request }: Route.LoaderArgs) {
|
||||
|
||||
const allowWhitelabelling = organisationClaim.flags.embedSigningWhiteLabel;
|
||||
const hidePoweredBy = organisationClaim.flags.hidePoweredBy;
|
||||
const isEnterprise = Boolean(organisationClaim.flags.cfr21);
|
||||
|
||||
return superLoaderJson({
|
||||
envelopes,
|
||||
user,
|
||||
hidePoweredBy,
|
||||
allowWhitelabelling,
|
||||
isEnterprise,
|
||||
});
|
||||
}
|
||||
|
||||
export default function MultisignPage() {
|
||||
const { envelopes, user, hidePoweredBy, allowWhitelabelling, isEnterprise } =
|
||||
const { envelopes, user, hidePoweredBy, allowWhitelabelling } =
|
||||
useSuperLoaderData<typeof loader>();
|
||||
const revalidator = useRevalidator();
|
||||
|
||||
@@ -267,7 +264,6 @@ export default function MultisignPage() {
|
||||
documentAuthOptions={selectedDocument.authOptions}
|
||||
recipient={selectedRecipient}
|
||||
user={user}
|
||||
isEnterprise={isEnterprise}
|
||||
>
|
||||
<DocumentSigningRecipientProvider recipient={selectedRecipient} targetSigner={null}>
|
||||
<MultiSignDocumentSigningView
|
||||
|
||||
@@ -101,5 +101,5 @@
|
||||
"vite-plugin-babel-macros": "^1.0.6",
|
||||
"vite-tsconfig-paths": "^5.1.4"
|
||||
},
|
||||
"version": "1.12.2-rc.3"
|
||||
"version": "1.12.2-rc.2"
|
||||
}
|
||||
|
||||
Generated
+3
-3
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@documenso/root",
|
||||
"version": "1.12.2-rc.3",
|
||||
"version": "1.12.2-rc.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@documenso/root",
|
||||
"version": "1.12.2-rc.3",
|
||||
"version": "1.12.2-rc.2",
|
||||
"workspaces": [
|
||||
"apps/*",
|
||||
"packages/*"
|
||||
@@ -89,7 +89,7 @@
|
||||
},
|
||||
"apps/remix": {
|
||||
"name": "@documenso/remix",
|
||||
"version": "1.12.2-rc.3",
|
||||
"version": "1.12.2-rc.2",
|
||||
"dependencies": {
|
||||
"@documenso/api": "*",
|
||||
"@documenso/assets": "*",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"private": true,
|
||||
"version": "1.12.2-rc.3",
|
||||
"version": "1.12.2-rc.2",
|
||||
"scripts": {
|
||||
"build": "turbo run build",
|
||||
"dev": "turbo run dev --filter=@documenso/remix",
|
||||
|
||||
@@ -1178,12 +1178,13 @@ test.describe('Unauthorized Access - Document API V2', () => {
|
||||
const { user: firstRecipientUser } = await seedUser();
|
||||
const { user: secondRecipientUser } = await seedUser();
|
||||
|
||||
const updatedTemplate = await prisma.template.update({
|
||||
await prisma.template.update({
|
||||
where: { id: template.id },
|
||||
data: {
|
||||
recipients: {
|
||||
create: [
|
||||
{
|
||||
id: firstRecipientUser.id,
|
||||
name: firstRecipientUser.name || '',
|
||||
email: firstRecipientUser.email,
|
||||
token: nanoid(12),
|
||||
@@ -1192,6 +1193,7 @@ test.describe('Unauthorized Access - Document API V2', () => {
|
||||
signingStatus: SigningStatus.NOT_SIGNED,
|
||||
},
|
||||
{
|
||||
id: secondRecipientUser.id,
|
||||
name: secondRecipientUser.name || '',
|
||||
email: secondRecipientUser.email,
|
||||
token: nanoid(12),
|
||||
@@ -1202,35 +1204,21 @@ test.describe('Unauthorized Access - Document API V2', () => {
|
||||
],
|
||||
},
|
||||
},
|
||||
include: {
|
||||
recipients: true,
|
||||
},
|
||||
});
|
||||
|
||||
const recipientAId = updatedTemplate.recipients.find(
|
||||
(recipient) => recipient.email === firstRecipientUser.email,
|
||||
)?.id;
|
||||
const recipientBId = updatedTemplate.recipients.find(
|
||||
(recipient) => recipient.email === secondRecipientUser.email,
|
||||
)?.id;
|
||||
|
||||
if (!recipientAId || !recipientBId) {
|
||||
throw new Error('Recipient IDs not found');
|
||||
}
|
||||
|
||||
const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/template/use`, {
|
||||
headers: { Authorization: `Bearer ${tokenB}` },
|
||||
data: {
|
||||
templateId: template.id,
|
||||
recipients: [
|
||||
{
|
||||
id: recipientAId,
|
||||
id: firstRecipientUser.id,
|
||||
name: firstRecipientUser.name,
|
||||
email: firstRecipientUser.email,
|
||||
role: RecipientRole.SIGNER,
|
||||
},
|
||||
{
|
||||
id: recipientBId,
|
||||
id: secondRecipientUser.id,
|
||||
name: secondRecipientUser.name,
|
||||
email: secondRecipientUser.email,
|
||||
role: RecipientRole.SIGNER,
|
||||
|
||||
@@ -27,8 +27,8 @@ test('[DOCUMENT_FLOW]: add settings', async ({ page }) => {
|
||||
await page.getByRole('option').filter({ hasText: 'Require account' }).click();
|
||||
await expect(page.getByTestId('documentAccessSelectValue')).toContainText('Require account');
|
||||
|
||||
// Action auth should now be visible for all users
|
||||
await expect(page.getByTestId('documentActionSelectValue')).toBeVisible();
|
||||
// Action auth should NOT be visible.
|
||||
await expect(page.getByTestId('documentActionSelectValue')).not.toBeVisible();
|
||||
|
||||
// Save the settings by going to the next step.
|
||||
|
||||
|
||||
@@ -379,11 +379,10 @@ test('[TEAMS]: can create a template inside a template folder', async ({ page })
|
||||
.filter({ hasText: /^Upload Template DocumentDrag & drop your PDF here\.$/ })
|
||||
.nth(2)
|
||||
.click();
|
||||
await page.locator('input[type="file"]').nth(0).waitFor({ state: 'attached' });
|
||||
await page.locator('input[type="file"]').waitFor({ state: 'attached' });
|
||||
|
||||
await page
|
||||
.locator('input[type="file"]')
|
||||
.nth(0)
|
||||
.setInputFiles(path.join(__dirname, '../../../assets/documenso-supporter-pledge.pdf'));
|
||||
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
@@ -25,8 +25,8 @@ test('[TEMPLATE_FLOW]: add settings', async ({ page }) => {
|
||||
await page.getByRole('option').filter({ hasText: 'Require account' }).click();
|
||||
await expect(page.getByTestId('documentAccessSelectValue')).toContainText('Require account');
|
||||
|
||||
// Action auth should now be visible for all users
|
||||
await expect(page.getByTestId('documentActionSelectValue')).toBeVisible();
|
||||
// Action auth should NOT be visible.
|
||||
await expect(page.getByTestId('documentActionSelectValue')).not.toBeVisible();
|
||||
|
||||
// Save the settings by going to the next step.
|
||||
await page.getByRole('button', { name: 'Continue' }).click();
|
||||
|
||||
@@ -268,7 +268,7 @@ test('[TEMPLATE]: should create a document from a template with custom document'
|
||||
// Upload document.
|
||||
const [fileChooser] = await Promise.all([
|
||||
page.waitForEvent('filechooser'),
|
||||
page.getByTestId('template-use-dialog-file-input').evaluate((e) => {
|
||||
page.locator('input[type=file]').evaluate((e) => {
|
||||
if (e instanceof HTMLInputElement) {
|
||||
e.click();
|
||||
}
|
||||
@@ -361,7 +361,7 @@ test('[TEMPLATE]: should create a team document from a template with custom docu
|
||||
// Upload document.
|
||||
const [fileChooser] = await Promise.all([
|
||||
page.waitForEvent('filechooser'),
|
||||
page.getByTestId('template-use-dialog-file-input').evaluate((e) => {
|
||||
page.locator('input[type=file]').evaluate((e) => {
|
||||
if (e instanceof HTMLInputElement) {
|
||||
e.click();
|
||||
}
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
|
||||
import { Section, Text } from '../components';
|
||||
import { TemplateDocumentImage } from './template-document-image';
|
||||
|
||||
export type TemplateVerificationCodeProps = {
|
||||
verificationCode: string;
|
||||
assetBaseUrl: string;
|
||||
};
|
||||
|
||||
export const TemplateVerificationCode = ({
|
||||
verificationCode,
|
||||
assetBaseUrl,
|
||||
}: TemplateVerificationCodeProps) => {
|
||||
return (
|
||||
<>
|
||||
<TemplateDocumentImage className="mt-6" assetBaseUrl={assetBaseUrl} />
|
||||
|
||||
<Section className="flex-row items-center justify-center">
|
||||
<Text className="text-primary mx-auto mb-0 max-w-[80%] text-center text-lg font-semibold">
|
||||
<Trans>Your verification code</Trans>
|
||||
</Text>
|
||||
|
||||
<Text className="my-1 text-center text-base text-slate-400">
|
||||
<Trans>Please use the code below to verify your identity for document signing.</Trans>
|
||||
</Text>
|
||||
|
||||
<Text className="my-6 text-center text-3xl font-bold tracking-widest">
|
||||
{verificationCode}
|
||||
</Text>
|
||||
|
||||
<Text className="my-1 text-center text-sm text-slate-400">
|
||||
<Trans>
|
||||
If you did not request this code, you can ignore this email. The code will expire after
|
||||
10 minutes.
|
||||
</Trans>
|
||||
</Text>
|
||||
</Section>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default TemplateVerificationCode;
|
||||
@@ -1,62 +0,0 @@
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
|
||||
import { Body, Container, Head, Hr, Html, Img, Preview, Section } from '../components';
|
||||
import { useBranding } from '../providers/branding';
|
||||
import { TemplateFooter } from '../template-components/template-footer';
|
||||
import type { TemplateVerificationCodeProps } from '../template-components/template-verification-code';
|
||||
import { TemplateVerificationCode } from '../template-components/template-verification-code';
|
||||
|
||||
export type VerificationCodeTemplateProps = Partial<TemplateVerificationCodeProps>;
|
||||
|
||||
export const VerificationCodeTemplate = ({
|
||||
verificationCode = '000000',
|
||||
assetBaseUrl = 'http://localhost:3002',
|
||||
}: VerificationCodeTemplateProps) => {
|
||||
const { _ } = useLingui();
|
||||
const branding = useBranding();
|
||||
|
||||
const previewText = msg`Your verification code for document signing`;
|
||||
|
||||
const getAssetUrl = (path: string) => {
|
||||
return new URL(path, assetBaseUrl).toString();
|
||||
};
|
||||
|
||||
return (
|
||||
<Html>
|
||||
<Head />
|
||||
<Preview>{_(previewText)}</Preview>
|
||||
|
||||
<Body className="mx-auto my-auto font-sans">
|
||||
<Section className="bg-white">
|
||||
<Container className="mx-auto mb-2 mt-8 max-w-xl rounded-lg border border-solid border-slate-200 p-2 backdrop-blur-sm">
|
||||
<Section className="p-2">
|
||||
{branding.brandingEnabled && branding.brandingLogo ? (
|
||||
<Img src={branding.brandingLogo} alt="Branding Logo" className="mb-4 h-6" />
|
||||
) : (
|
||||
<Img
|
||||
src={getAssetUrl('/static/logo.png')}
|
||||
alt="Documenso Logo"
|
||||
className="mb-4 h-6"
|
||||
/>
|
||||
)}
|
||||
|
||||
<TemplateVerificationCode
|
||||
verificationCode={verificationCode}
|
||||
assetBaseUrl={assetBaseUrl}
|
||||
/>
|
||||
</Section>
|
||||
</Container>
|
||||
|
||||
<Hr className="mx-auto mt-12 max-w-xl" />
|
||||
|
||||
<Container className="mx-auto max-w-xl">
|
||||
<TemplateFooter isDocument={false} />
|
||||
</Container>
|
||||
</Section>
|
||||
</Body>
|
||||
</Html>
|
||||
);
|
||||
};
|
||||
|
||||
export default VerificationCodeTemplate;
|
||||
@@ -13,4 +13,4 @@ export const IS_BILLING_ENABLED = () => env('NEXT_PUBLIC_FEATURE_BILLING_ENABLED
|
||||
|
||||
export const API_V2_BETA_URL = '/api/v2-beta';
|
||||
|
||||
export const SUPPORT_EMAIL = env('NEXT_PUBLIC_SUPPORT_EMAIL') ?? 'support@documenso.com';
|
||||
export const SUPPORT_EMAIL = 'support@documenso.com';
|
||||
|
||||
@@ -9,7 +9,6 @@ export const VALID_DATE_FORMAT_VALUES = [
|
||||
'yyyy-MM-dd',
|
||||
'dd/MM/yyyy hh:mm a',
|
||||
'MM/dd/yyyy hh:mm a',
|
||||
'dd.MM.yyyy HH:mm',
|
||||
'yyyy-MM-dd HH:mm',
|
||||
'yy-MM-dd hh:mm a',
|
||||
'yyyy-MM-dd HH:mm:ss',
|
||||
@@ -41,11 +40,6 @@ export const DATE_FORMATS = [
|
||||
label: 'MM/DD/YYYY',
|
||||
value: 'MM/dd/yyyy hh:mm a',
|
||||
},
|
||||
{
|
||||
key: 'DDMMYYYYHHMM',
|
||||
label: 'DD.MM.YYYY HH:mm',
|
||||
value: 'dd.MM.yyyy HH:mm',
|
||||
},
|
||||
{
|
||||
key: 'YYYYMMDDHHmm',
|
||||
label: 'YYYY-MM-DD HH:mm',
|
||||
|
||||
@@ -49,24 +49,15 @@ type DocumentSignatureTypeData = {
|
||||
|
||||
export const DOCUMENT_SIGNATURE_TYPES = {
|
||||
[DocumentSignatureType.DRAW]: {
|
||||
label: msg({
|
||||
message: `Draw`,
|
||||
context: `Draw signatute type`,
|
||||
}),
|
||||
label: msg`Draw`,
|
||||
value: DocumentSignatureType.DRAW,
|
||||
},
|
||||
[DocumentSignatureType.TYPE]: {
|
||||
label: msg({
|
||||
message: `Type`,
|
||||
context: `Type signatute type`,
|
||||
}),
|
||||
label: msg`Type`,
|
||||
value: DocumentSignatureType.TYPE,
|
||||
},
|
||||
[DocumentSignatureType.UPLOAD]: {
|
||||
label: msg({
|
||||
message: `Upload`,
|
||||
context: `Upload signatute type`,
|
||||
}),
|
||||
label: msg`Upload`,
|
||||
value: DocumentSignatureType.UPLOAD,
|
||||
},
|
||||
} satisfies Record<DocumentSignatureType, DocumentSignatureTypeData>;
|
||||
|
||||
@@ -4,114 +4,39 @@ import { RecipientRole } from '@prisma/client';
|
||||
|
||||
export const RECIPIENT_ROLES_DESCRIPTION = {
|
||||
[RecipientRole.APPROVER]: {
|
||||
actionVerb: msg({
|
||||
message: `Approve`,
|
||||
context: `Recipient role action verb`,
|
||||
}),
|
||||
actioned: msg({
|
||||
message: `Approved`,
|
||||
context: `Recipient role actioned`,
|
||||
}),
|
||||
progressiveVerb: msg({
|
||||
message: `Approving`,
|
||||
context: `Recipient role progressive verb`,
|
||||
}),
|
||||
roleName: msg({
|
||||
message: `Approver`,
|
||||
context: `Recipient role name`,
|
||||
}),
|
||||
roleNamePlural: msg({
|
||||
message: `Approvers`,
|
||||
context: `Recipient role plural name`,
|
||||
}),
|
||||
actionVerb: msg`Approve`,
|
||||
actioned: msg`Approved`,
|
||||
progressiveVerb: msg`Approving`,
|
||||
roleName: msg`Approver`,
|
||||
roleNamePlural: msg`Approvers`,
|
||||
},
|
||||
[RecipientRole.CC]: {
|
||||
actionVerb: msg({
|
||||
message: `CC`,
|
||||
context: `Recipient role action verb`,
|
||||
}),
|
||||
actioned: msg({
|
||||
message: `CC'd`,
|
||||
context: `Recipient role actioned`,
|
||||
}),
|
||||
progressiveVerb: msg({
|
||||
message: `CC`,
|
||||
context: `Recipient role progressive verb`,
|
||||
}),
|
||||
roleName: msg({
|
||||
message: `Cc`,
|
||||
context: `Recipient role name`,
|
||||
}),
|
||||
roleNamePlural: msg({
|
||||
message: `Ccers`,
|
||||
context: `Recipient role plural name`,
|
||||
}),
|
||||
actionVerb: msg`CC`,
|
||||
actioned: msg`CC'd`,
|
||||
progressiveVerb: msg`CC`,
|
||||
roleName: msg`Cc`,
|
||||
roleNamePlural: msg`Ccers`,
|
||||
},
|
||||
[RecipientRole.SIGNER]: {
|
||||
actionVerb: msg({
|
||||
message: `Sign`,
|
||||
context: `Recipient role action verb`,
|
||||
}),
|
||||
actioned: msg({
|
||||
message: `Signed`,
|
||||
context: `Recipient role actioned`,
|
||||
}),
|
||||
progressiveVerb: msg({
|
||||
message: `Signing`,
|
||||
context: `Recipient role progressive verb`,
|
||||
}),
|
||||
roleName: msg({
|
||||
message: `Signer`,
|
||||
context: `Recipient role name`,
|
||||
}),
|
||||
roleNamePlural: msg({
|
||||
message: `Signers`,
|
||||
context: `Recipient role plural name`,
|
||||
}),
|
||||
actionVerb: msg`Sign`,
|
||||
actioned: msg`Signed`,
|
||||
progressiveVerb: msg`Signing`,
|
||||
roleName: msg`Signer`,
|
||||
roleNamePlural: msg`Signers`,
|
||||
},
|
||||
[RecipientRole.VIEWER]: {
|
||||
actionVerb: msg({
|
||||
message: `View`,
|
||||
context: `Recipient role action verb`,
|
||||
}),
|
||||
actioned: msg({
|
||||
message: `Viewed`,
|
||||
context: `Recipient role actioned`,
|
||||
}),
|
||||
progressiveVerb: msg({
|
||||
message: `Viewing`,
|
||||
context: `Recipient role progressive verb`,
|
||||
}),
|
||||
roleName: msg({
|
||||
message: `Viewer`,
|
||||
context: `Recipient role name`,
|
||||
}),
|
||||
roleNamePlural: msg({
|
||||
message: `Viewers`,
|
||||
context: `Recipient role plural name`,
|
||||
}),
|
||||
actionVerb: msg`View`,
|
||||
actioned: msg`Viewed`,
|
||||
progressiveVerb: msg`Viewing`,
|
||||
roleName: msg`Viewer`,
|
||||
roleNamePlural: msg`Viewers`,
|
||||
},
|
||||
[RecipientRole.ASSISTANT]: {
|
||||
actionVerb: msg({
|
||||
message: `Assist`,
|
||||
context: `Recipient role action verb`,
|
||||
}),
|
||||
actioned: msg({
|
||||
message: `Assisted`,
|
||||
context: `Recipient role actioned`,
|
||||
}),
|
||||
progressiveVerb: msg({
|
||||
message: `Assisting`,
|
||||
context: `Recipient role progressive verb`,
|
||||
}),
|
||||
roleName: msg({
|
||||
message: `Assistant`,
|
||||
context: `Recipient role name`,
|
||||
}),
|
||||
roleNamePlural: msg({
|
||||
message: `Assistants`,
|
||||
context: `Recipient role plural name`,
|
||||
}),
|
||||
actionVerb: msg`Assist`,
|
||||
actioned: msg`Assisted`,
|
||||
progressiveVerb: msg`Assisting`,
|
||||
roleName: msg`Assistant`,
|
||||
roleNamePlural: msg`Assistants`,
|
||||
},
|
||||
} satisfies Record<keyof typeof RecipientRole, unknown>;
|
||||
|
||||
|
||||
@@ -3,10 +3,6 @@ import { msg } from '@lingui/core/macro';
|
||||
export const TEMPLATE_RECIPIENT_EMAIL_PLACEHOLDER_REGEX = /recipient\.\d+@documenso\.com/i;
|
||||
export const TEMPLATE_RECIPIENT_NAME_PLACEHOLDER_REGEX = /Recipient \d+/i;
|
||||
|
||||
export const isTemplateRecipientEmailPlaceholder = (email: string) => {
|
||||
return TEMPLATE_RECIPIENT_EMAIL_PLACEHOLDER_REGEX.test(email);
|
||||
};
|
||||
|
||||
export const DIRECT_TEMPLATE_DOCUMENTATION = [
|
||||
{
|
||||
title: msg`Enable Direct Link Signing`,
|
||||
|
||||
@@ -48,7 +48,7 @@ export const run = async ({
|
||||
type: 'team',
|
||||
teamId: document.teamId,
|
||||
},
|
||||
meta: document.documentMeta,
|
||||
meta: document.documentMeta || null,
|
||||
});
|
||||
|
||||
const { documentMeta, user: documentOwner } = document;
|
||||
|
||||
@@ -76,7 +76,7 @@ export const run = async ({
|
||||
type: 'team',
|
||||
teamId: document.teamId,
|
||||
},
|
||||
meta: document.documentMeta,
|
||||
meta: document.documentMeta || null,
|
||||
});
|
||||
|
||||
const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000';
|
||||
|
||||
@@ -68,7 +68,7 @@ export const run = async ({
|
||||
type: 'team',
|
||||
teamId: document.teamId,
|
||||
},
|
||||
meta: document.documentMeta,
|
||||
meta: document.documentMeta || null,
|
||||
});
|
||||
|
||||
const i18n = await getI18nInstance(emailLanguage);
|
||||
|
||||
@@ -86,7 +86,7 @@ export const run = async ({
|
||||
type: 'team',
|
||||
teamId: document.teamId,
|
||||
},
|
||||
meta: document.documentMeta,
|
||||
meta: document.documentMeta || null,
|
||||
});
|
||||
|
||||
const customEmail = document?.documentMeta;
|
||||
|
||||
@@ -9,7 +9,6 @@ import { signPdf } from '@documenso/signing';
|
||||
import { AppError, AppErrorCode } from '../../../errors/app-error';
|
||||
import { sendCompletedEmail } from '../../../server-only/document/send-completed-email';
|
||||
import PostHogServerClient from '../../../server-only/feature-flags/get-post-hog-server-client';
|
||||
import { getAuditLogsPdf } from '../../../server-only/htmltopdf/get-audit-logs-pdf';
|
||||
import { getCertificatePdf } from '../../../server-only/htmltopdf/get-certificate-pdf';
|
||||
import { addRejectionStampToPdf } from '../../../server-only/pdf/add-rejection-stamp-to-pdf';
|
||||
import { flattenAnnotations } from '../../../server-only/pdf/flatten-annotations';
|
||||
@@ -146,24 +145,7 @@ export const run = async ({
|
||||
? await getCertificatePdf({
|
||||
documentId,
|
||||
language: document.documentMeta?.language,
|
||||
}).catch((e) => {
|
||||
console.log('Failed to get certificate PDF');
|
||||
console.error(e);
|
||||
|
||||
return null;
|
||||
})
|
||||
: null;
|
||||
|
||||
const auditLogData = settings.includeAuditLog
|
||||
? await getAuditLogsPdf({
|
||||
documentId,
|
||||
language: document.documentMeta?.language,
|
||||
}).catch((e) => {
|
||||
console.log('Failed to get audit logs PDF');
|
||||
console.error(e);
|
||||
|
||||
return null;
|
||||
})
|
||||
}).catch(() => null)
|
||||
: null;
|
||||
|
||||
const newDataId = await io.runTask('decorate-and-sign-pdf', async () => {
|
||||
@@ -192,16 +174,6 @@ export const run = async ({
|
||||
});
|
||||
}
|
||||
|
||||
if (auditLogData) {
|
||||
const auditLogDoc = await PDFDocument.load(auditLogData);
|
||||
|
||||
const auditLogPages = await pdfDoc.copyPages(auditLogDoc, auditLogDoc.getPageIndices());
|
||||
|
||||
auditLogPages.forEach((page) => {
|
||||
pdfDoc.addPage(page);
|
||||
});
|
||||
}
|
||||
|
||||
for (const field of fields) {
|
||||
if (field.inserted) {
|
||||
document.useLegacyFieldInsertion
|
||||
|
||||
@@ -1,120 +0,0 @@
|
||||
import { createElement } from 'react';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { randomInt } from 'crypto';
|
||||
|
||||
import { AuthenticationErrorCode } from '@documenso/auth/server/lib/errors/error-codes';
|
||||
import { mailer } from '@documenso/email/mailer';
|
||||
import { VerificationCodeTemplate } from '@documenso/email/templates/verification-code';
|
||||
import { AppError } from '@documenso/lib/errors/app-error';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { getI18nInstance } from '../../client-only/providers/i18n-server';
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
||||
import { FROM_ADDRESS, FROM_NAME } from '../../constants/email';
|
||||
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
||||
|
||||
const ExtendedAuthErrorCode = {
|
||||
...AuthenticationErrorCode,
|
||||
InternalError: 'INTERNAL_ERROR',
|
||||
VerificationNotFound: 'VERIFICATION_NOT_FOUND',
|
||||
VerificationExpired: 'VERIFICATION_EXPIRED',
|
||||
};
|
||||
|
||||
const VERIFICATION_CODE_EXPIRY = 10 * 60 * 1000;
|
||||
|
||||
export type SendEmailVerificationOptions = {
|
||||
userId: number;
|
||||
email: string;
|
||||
};
|
||||
|
||||
export const sendEmailVerification = async ({ userId, email }: SendEmailVerificationOptions) => {
|
||||
try {
|
||||
const verificationCode = randomInt(100000, 1000000).toString();
|
||||
const i18n = await getI18nInstance();
|
||||
|
||||
await prisma.userTwoFactorEmailVerification.upsert({
|
||||
where: {
|
||||
userId,
|
||||
},
|
||||
create: {
|
||||
userId,
|
||||
verificationCode,
|
||||
expiresAt: new Date(Date.now() + VERIFICATION_CODE_EXPIRY),
|
||||
},
|
||||
update: {
|
||||
verificationCode,
|
||||
expiresAt: new Date(Date.now() + VERIFICATION_CODE_EXPIRY),
|
||||
},
|
||||
});
|
||||
|
||||
const template = createElement(VerificationCodeTemplate, {
|
||||
verificationCode,
|
||||
assetBaseUrl: NEXT_PUBLIC_WEBAPP_URL(),
|
||||
});
|
||||
|
||||
const [html, text] = await Promise.all([
|
||||
renderEmailWithI18N(template, { lang: 'en' }),
|
||||
renderEmailWithI18N(template, { lang: 'en', plainText: true }),
|
||||
]);
|
||||
|
||||
await mailer.sendMail({
|
||||
to: email,
|
||||
from: {
|
||||
name: FROM_NAME,
|
||||
address: FROM_ADDRESS,
|
||||
},
|
||||
subject: i18n._(msg`Your verification code for document signing`),
|
||||
html,
|
||||
text,
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('Error sending email verification', error);
|
||||
throw new AppError(ExtendedAuthErrorCode.InternalError);
|
||||
}
|
||||
};
|
||||
|
||||
export type VerifyEmailCodeOptions = {
|
||||
userId: number;
|
||||
code: string;
|
||||
};
|
||||
|
||||
export const verifyEmailCode = async ({ userId, code }: VerifyEmailCodeOptions) => {
|
||||
try {
|
||||
const verification = await prisma.userTwoFactorEmailVerification.findUnique({
|
||||
where: {
|
||||
userId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!verification) {
|
||||
throw new AppError(ExtendedAuthErrorCode.VerificationNotFound);
|
||||
}
|
||||
|
||||
if (verification.expiresAt < new Date()) {
|
||||
throw new AppError(ExtendedAuthErrorCode.VerificationExpired);
|
||||
}
|
||||
|
||||
if (verification.verificationCode !== code) {
|
||||
throw new AppError(AuthenticationErrorCode.InvalidTwoFactorCode);
|
||||
}
|
||||
|
||||
await prisma.userTwoFactorEmailVerification.delete({
|
||||
where: {
|
||||
userId,
|
||||
},
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('Error verifying email code', error);
|
||||
|
||||
if (error instanceof AppError) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
throw new AppError(ExtendedAuthErrorCode.InternalError);
|
||||
}
|
||||
};
|
||||
@@ -212,7 +212,7 @@ export const createDocumentV2 = async ({
|
||||
}),
|
||||
);
|
||||
|
||||
// Todo: Is it necessary to create a full audit logs with all fields and recipients audit logs?
|
||||
// Todo: Is it necessary to create a full audit log with all fields and recipients audit logs?
|
||||
|
||||
await tx.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
|
||||
@@ -156,7 +156,7 @@ const handleDocumentOwnerDelete = async ({
|
||||
type: 'team',
|
||||
teamId: document.teamId,
|
||||
},
|
||||
meta: document.documentMeta,
|
||||
meta: document.documentMeta || null,
|
||||
});
|
||||
|
||||
// Soft delete completed documents.
|
||||
|
||||
@@ -81,6 +81,7 @@ export const getDocumentAndSenderByToken = async ({
|
||||
token,
|
||||
},
|
||||
},
|
||||
attachments: true,
|
||||
team: {
|
||||
select: {
|
||||
name: true,
|
||||
|
||||
@@ -102,7 +102,7 @@ export const resendDocument = async ({
|
||||
type: 'team',
|
||||
teamId: document.teamId,
|
||||
},
|
||||
meta: document.documentMeta,
|
||||
meta: document.documentMeta || null,
|
||||
});
|
||||
|
||||
await Promise.all(
|
||||
|
||||
@@ -17,7 +17,6 @@ import type { RequestMetadata } from '../../universal/extract-request-metadata';
|
||||
import { getFileServerSide } from '../../universal/upload/get-file.server';
|
||||
import { putPdfFileServerSide } from '../../universal/upload/put-file.server';
|
||||
import { fieldsContainUnsignedRequiredField } from '../../utils/advanced-fields-helpers';
|
||||
import { getAuditLogsPdf } from '../htmltopdf/get-audit-logs-pdf';
|
||||
import { getCertificatePdf } from '../htmltopdf/get-certificate-pdf';
|
||||
import { addRejectionStampToPdf } from '../pdf/add-rejection-stamp-to-pdf';
|
||||
import { flattenAnnotations } from '../pdf/flatten-annotations';
|
||||
@@ -126,18 +125,6 @@ export const sealDocument = async ({
|
||||
})
|
||||
: null;
|
||||
|
||||
const auditLogData = settings.includeAuditLog
|
||||
? await getAuditLogsPdf({
|
||||
documentId,
|
||||
language: document.documentMeta?.language,
|
||||
}).catch((e) => {
|
||||
console.log('Failed to get audit logs PDF');
|
||||
console.error(e);
|
||||
|
||||
return null;
|
||||
})
|
||||
: null;
|
||||
|
||||
const doc = await PDFDocument.load(pdfData);
|
||||
|
||||
// Normalize and flatten layers that could cause issues with the signature
|
||||
@@ -160,16 +147,6 @@ export const sealDocument = async ({
|
||||
});
|
||||
}
|
||||
|
||||
if (auditLogData) {
|
||||
const auditLog = await PDFDocument.load(auditLogData);
|
||||
|
||||
const auditLogPages = await doc.copyPages(auditLog, auditLog.getPageIndices());
|
||||
|
||||
auditLogPages.forEach((page) => {
|
||||
doc.addPage(page);
|
||||
});
|
||||
}
|
||||
|
||||
for (const field of fields) {
|
||||
document.useLegacyFieldInsertion
|
||||
? await legacy_insertFieldInPDF(doc, field)
|
||||
|
||||
@@ -59,7 +59,7 @@ export const sendCompletedEmail = async ({ documentId, requestMetadata }: SendDo
|
||||
type: 'team',
|
||||
teamId: document.teamId,
|
||||
},
|
||||
meta: document.documentMeta,
|
||||
meta: document.documentMeta || null,
|
||||
});
|
||||
|
||||
const { user: owner } = document;
|
||||
|
||||
@@ -49,7 +49,7 @@ export const sendDeleteEmail = async ({ documentId, reason }: SendDeleteEmailOpt
|
||||
type: 'team',
|
||||
teamId: document.teamId,
|
||||
},
|
||||
meta: document.documentMeta,
|
||||
meta: document.documentMeta || null,
|
||||
});
|
||||
|
||||
const { email, name } = document.user;
|
||||
|
||||
@@ -51,7 +51,7 @@ export const sendPendingEmail = async ({ documentId, recipientId }: SendPendingE
|
||||
type: 'team',
|
||||
teamId: document.teamId,
|
||||
},
|
||||
meta: document.documentMeta,
|
||||
meta: document.documentMeta || null,
|
||||
});
|
||||
|
||||
const isDocumentPendingEmailEnabled = extractDerivedDocumentEmailSettings(
|
||||
|
||||
@@ -46,7 +46,7 @@ export const superDeleteDocument = async ({ id, requestMetadata }: SuperDeleteDo
|
||||
type: 'team',
|
||||
teamId: document.teamId,
|
||||
},
|
||||
meta: document.documentMeta,
|
||||
meta: document.documentMeta || null,
|
||||
});
|
||||
|
||||
const { status, user } = document;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { DocumentStatus, DocumentVisibility, TeamMemberRole } from '@prisma/client';
|
||||
import { DocumentVisibility } from '@prisma/client';
|
||||
import { DocumentStatus, TeamMemberRole } from '@prisma/client';
|
||||
import { isDeepEqual } from 'remeda';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
@@ -118,6 +119,13 @@ export const updateDocument = async ({
|
||||
const newGlobalActionAuth =
|
||||
data?.globalActionAuth === undefined ? documentGlobalActionAuth : data.globalActionAuth;
|
||||
|
||||
// Check if user has permission to set the global action auth.
|
||||
if (newGlobalActionAuth.length > 0 && !document.team.organisation.organisationClaim.flags.cfr21) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'You do not have permission to set the action auth',
|
||||
});
|
||||
}
|
||||
|
||||
const isTitleSame = data.title === undefined || data.title === document.title;
|
||||
const isExternalIdSame = data.externalId === undefined || data.externalId === document.externalId;
|
||||
const isGlobalAccessSame =
|
||||
|
||||
@@ -59,7 +59,7 @@ type RecipientGetEmailContextOptions = BaseGetEmailContextOptions & {
|
||||
* Force meta options as a typesafe way to ensure developers don't forget to
|
||||
* pass it in if it is available.
|
||||
*/
|
||||
meta: EmailMetaOption | null | undefined;
|
||||
meta: EmailMetaOption | null;
|
||||
};
|
||||
|
||||
type GetEmailContextOptions = InternalGetEmailContextOptions | RecipientGetEmailContextOptions;
|
||||
@@ -104,7 +104,7 @@ export const getEmailContext = async (
|
||||
}
|
||||
|
||||
const replyToEmail = meta?.emailReplyTo || emailContext.settings.emailReplyTo || undefined;
|
||||
const senderEmailId = meta?.emailId === null ? null : emailContext.settings.emailId;
|
||||
const senderEmailId = meta?.emailId || emailContext.settings.emailId;
|
||||
|
||||
const foundSenderEmail = emailContext.allowedEmails.find((email) => email.id === senderEmailId);
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ import { AUTO_SIGNABLE_FIELD_TYPES } from '../../constants/autosign';
|
||||
import { DEFAULT_DOCUMENT_DATE_FORMAT } from '../../constants/date-formats';
|
||||
import { DEFAULT_DOCUMENT_TIME_ZONE } from '../../constants/time-zones';
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs';
|
||||
import type { TRecipientActionAuth, TRecipientActionAuthTypes } from '../../types/document-auth';
|
||||
import type { TRecipientActionAuth } from '../../types/document-auth';
|
||||
import {
|
||||
ZCheckboxFieldMeta,
|
||||
ZDropdownFieldMeta,
|
||||
@@ -25,9 +25,7 @@ import {
|
||||
} from '../../types/field-meta';
|
||||
import type { RequestMetadata } from '../../universal/extract-request-metadata';
|
||||
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
|
||||
import { extractDocumentAuthMethods } from '../../utils/document-auth';
|
||||
import { validateFieldAuth } from '../document/validate-field-auth';
|
||||
import { isUserEnterprise } from '../user/is-user-enterprise';
|
||||
|
||||
export type SignFieldWithTokenOptions = {
|
||||
token: string;
|
||||
@@ -173,25 +171,13 @@ export const signFieldWithToken = async ({
|
||||
}
|
||||
}
|
||||
|
||||
const isEnterprise = userId ? await isUserEnterprise({ userId }) : false;
|
||||
let requiredAuthType: TRecipientActionAuthTypes | null = null;
|
||||
|
||||
if (isEnterprise) {
|
||||
const authType = await validateFieldAuth({
|
||||
documentAuthOptions: document.authOptions,
|
||||
recipient,
|
||||
field,
|
||||
userId,
|
||||
authOptions,
|
||||
});
|
||||
requiredAuthType = authType ?? null;
|
||||
} else {
|
||||
const { derivedRecipientActionAuth } = extractDocumentAuthMethods({
|
||||
documentAuth: document.authOptions,
|
||||
recipientAuth: recipient.authOptions,
|
||||
});
|
||||
requiredAuthType = derivedRecipientActionAuth.length > 0 ? derivedRecipientActionAuth[0] : null;
|
||||
}
|
||||
const derivedRecipientActionAuth = await validateFieldAuth({
|
||||
documentAuthOptions: document.authOptions,
|
||||
recipient,
|
||||
field,
|
||||
userId,
|
||||
authOptions,
|
||||
});
|
||||
|
||||
const documentMeta = await prisma.documentMeta.findFirst({
|
||||
where: {
|
||||
@@ -325,9 +311,9 @@ export const signFieldWithToken = async ({
|
||||
}),
|
||||
)
|
||||
.exhaustive(),
|
||||
fieldSecurity: requiredAuthType
|
||||
fieldSecurity: derivedRecipientActionAuth
|
||||
? {
|
||||
type: requiredAuthType,
|
||||
type: derivedRecipientActionAuth,
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
import { DateTime } from 'luxon';
|
||||
import type { Browser } from 'playwright';
|
||||
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
||||
import { type SupportedLanguageCodes, isValidLanguageCode } from '../../constants/i18n';
|
||||
import { env } from '../../utils/env';
|
||||
import { encryptSecondaryData } from '../crypto/encrypt';
|
||||
|
||||
export type GetAuditLogsPdfOptions = {
|
||||
documentId: number;
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
language?: SupportedLanguageCodes | (string & {});
|
||||
};
|
||||
|
||||
export const getAuditLogsPdf = async ({ documentId, language }: GetAuditLogsPdfOptions) => {
|
||||
const { chromium } = await import('playwright');
|
||||
|
||||
const encryptedId = encryptSecondaryData({
|
||||
data: documentId.toString(),
|
||||
expiresAt: DateTime.now().plus({ minutes: 5 }).toJSDate().valueOf(),
|
||||
});
|
||||
|
||||
let browser: Browser;
|
||||
|
||||
const browserlessUrl = env('NEXT_PRIVATE_BROWSERLESS_URL');
|
||||
|
||||
if (browserlessUrl) {
|
||||
// !: Use CDP rather than the default `connect` method to avoid coupling to the playwright version.
|
||||
// !: Previously we would have to keep the playwright version in sync with the browserless version to avoid errors.
|
||||
browser = await chromium.connectOverCDP(browserlessUrl);
|
||||
} else {
|
||||
browser = await chromium.launch();
|
||||
}
|
||||
|
||||
if (!browser) {
|
||||
throw new Error(
|
||||
'Failed to establish a browser, please ensure you have either a Browserless.io url or chromium browser installed',
|
||||
);
|
||||
}
|
||||
|
||||
const browserContext = await browser.newContext();
|
||||
|
||||
const page = await browserContext.newPage();
|
||||
|
||||
const lang = isValidLanguageCode(language) ? language : 'en';
|
||||
|
||||
await page.context().addCookies([
|
||||
{
|
||||
name: 'language',
|
||||
value: lang,
|
||||
url: NEXT_PUBLIC_WEBAPP_URL(),
|
||||
},
|
||||
]);
|
||||
|
||||
await page.goto(`${NEXT_PUBLIC_WEBAPP_URL()}/__htmltopdf/audit-log?d=${encryptedId}`, {
|
||||
waitUntil: 'networkidle',
|
||||
timeout: 10_000,
|
||||
});
|
||||
|
||||
// !: This is a workaround to ensure the page is loaded correctly.
|
||||
// !: It's not clear why but suddenly browserless cdp connections would
|
||||
// !: cause the page to render blank until a reload is performed.
|
||||
await page.reload({
|
||||
waitUntil: 'networkidle',
|
||||
timeout: 10_000,
|
||||
});
|
||||
|
||||
await page.waitForSelector('h1', {
|
||||
state: 'visible',
|
||||
timeout: 10_000,
|
||||
});
|
||||
|
||||
const result = await page.pdf({
|
||||
format: 'A4',
|
||||
printBackground: true,
|
||||
});
|
||||
|
||||
await browserContext.close();
|
||||
|
||||
void browser.close();
|
||||
|
||||
return result;
|
||||
};
|
||||
@@ -46,7 +46,7 @@ export const getCertificatePdf = async ({ documentId, language }: GetCertificate
|
||||
|
||||
await page.context().addCookies([
|
||||
{
|
||||
name: 'lang',
|
||||
name: 'language',
|
||||
value: lang,
|
||||
url: NEXT_PUBLIC_WEBAPP_URL(),
|
||||
},
|
||||
@@ -57,22 +57,8 @@ export const getCertificatePdf = async ({ documentId, language }: GetCertificate
|
||||
timeout: 10_000,
|
||||
});
|
||||
|
||||
// !: This is a workaround to ensure the page is loaded correctly.
|
||||
// !: It's not clear why but suddenly browserless cdp connections would
|
||||
// !: cause the page to render blank until a reload is performed.
|
||||
await page.reload({
|
||||
waitUntil: 'networkidle',
|
||||
timeout: 10_000,
|
||||
});
|
||||
|
||||
await page.waitForSelector('h1', {
|
||||
state: 'visible',
|
||||
timeout: 10_000,
|
||||
});
|
||||
|
||||
const result = await page.pdf({
|
||||
format: 'A4',
|
||||
printBackground: true,
|
||||
});
|
||||
|
||||
await browserContext.close();
|
||||
|
||||
@@ -2,10 +2,8 @@ import type { Prisma } from '@prisma/client';
|
||||
import { OrganisationType } from '@prisma/client';
|
||||
import { OrganisationMemberRole } from '@prisma/client';
|
||||
|
||||
import { createCustomer } from '@documenso/ee/server-only/stripe/create-customer';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { IS_BILLING_ENABLED } from '../../constants/app';
|
||||
import { ORGANISATION_INTERNAL_GROUPS } from '../../constants/organisations';
|
||||
import { AppErrorCode } from '../../errors/app-error';
|
||||
import { AppError } from '../../errors/app-error';
|
||||
@@ -32,33 +30,6 @@ export const createOrganisation = async ({
|
||||
customerId,
|
||||
claim,
|
||||
}: CreateOrganisationOptions) => {
|
||||
let customerIdToUse = customerId;
|
||||
|
||||
if (!customerId && IS_BILLING_ENABLED()) {
|
||||
const user = await prisma.user.findUnique({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'User not found',
|
||||
});
|
||||
}
|
||||
|
||||
customerIdToUse = await createCustomer({
|
||||
name: user.name || user.email,
|
||||
email: user.email,
|
||||
})
|
||||
.then((customer) => customer.id)
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
|
||||
return undefined;
|
||||
});
|
||||
}
|
||||
|
||||
return await prisma.$transaction(async (tx) => {
|
||||
const organisationSetting = await tx.organisationGlobalSettings.create({
|
||||
data: {
|
||||
@@ -93,7 +64,7 @@ export const createOrganisation = async ({
|
||||
id: generateDatabaseId('org_group'),
|
||||
})),
|
||||
},
|
||||
customerId: customerIdToUse,
|
||||
customerId,
|
||||
},
|
||||
include: {
|
||||
groups: true,
|
||||
|
||||
@@ -37,23 +37,3 @@ export const getOrganisationClaimByTeamId = async ({ teamId }: { teamId: number
|
||||
|
||||
return organisationClaim;
|
||||
};
|
||||
|
||||
export const getOrganisationClaimByUserId = async ({ userId }: { userId: number }) => {
|
||||
const organisationClaim = await prisma.organisationClaim.findFirst({
|
||||
where: {
|
||||
organisation: {
|
||||
members: {
|
||||
some: {
|
||||
userId: userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!organisationClaim) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND);
|
||||
}
|
||||
|
||||
return organisationClaim;
|
||||
};
|
||||
|
||||
@@ -130,7 +130,7 @@ export const deleteDocumentRecipient = async ({
|
||||
type: 'team',
|
||||
teamId: document.teamId,
|
||||
},
|
||||
meta: document.documentMeta,
|
||||
meta: document.documentMeta || null,
|
||||
});
|
||||
|
||||
const [html, text] = await Promise.all([
|
||||
|
||||
@@ -95,7 +95,7 @@ export const setDocumentRecipients = async ({
|
||||
type: 'team',
|
||||
teamId,
|
||||
},
|
||||
meta: document.documentMeta,
|
||||
meta: document.documentMeta || null,
|
||||
});
|
||||
|
||||
const recipientsHaveActionAuth = recipients.some(
|
||||
|
||||
@@ -287,6 +287,7 @@ export const createDocumentFromTemplate = async ({
|
||||
fields: true,
|
||||
},
|
||||
},
|
||||
attachments: true,
|
||||
templateDocumentData: true,
|
||||
templateMeta: true,
|
||||
},
|
||||
@@ -377,6 +378,15 @@ export const createDocumentFromTemplate = async ({
|
||||
}),
|
||||
visibility: template.visibility || settings.documentVisibility,
|
||||
useLegacyFieldInsertion: template.useLegacyFieldInsertion ?? false,
|
||||
attachments: {
|
||||
create: template.attachments.map((attachment) => ({
|
||||
type: attachment.type,
|
||||
label: attachment.label,
|
||||
url: attachment.url,
|
||||
createdAt: attachment.createdAt,
|
||||
updatedAt: attachment.updatedAt,
|
||||
})),
|
||||
},
|
||||
documentMeta: {
|
||||
create: extractDerivedDocumentMeta(settings, {
|
||||
subject: override?.subject || template.templateMeta?.subject,
|
||||
|
||||
@@ -4,10 +4,8 @@ import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import type { TDocumentAccessAuthTypes, TDocumentActionAuthTypes } from '../../types/document-auth';
|
||||
import { DocumentAuth } from '../../types/document-auth';
|
||||
import { createDocumentAuthOptions, extractDocumentAuthMethods } from '../../utils/document-auth';
|
||||
import { buildTeamWhereQuery } from '../../utils/teams';
|
||||
import { isUserEnterprise } from '../user/is-user-enterprise';
|
||||
|
||||
export type UpdateTemplateOptions = {
|
||||
userId: number;
|
||||
@@ -78,21 +76,10 @@ export const updateTemplate = async ({
|
||||
data?.globalActionAuth === undefined ? documentGlobalActionAuth : data.globalActionAuth;
|
||||
|
||||
// Check if user has permission to set the global action auth.
|
||||
// Only ACCOUNT and PASSKEY require enterprise permissions
|
||||
if (
|
||||
newGlobalActionAuth &&
|
||||
(newGlobalActionAuth.includes(DocumentAuth.ACCOUNT) ||
|
||||
newGlobalActionAuth.includes(DocumentAuth.PASSKEY))
|
||||
) {
|
||||
const isDocumentEnterprise = await isUserEnterprise({
|
||||
userId,
|
||||
if (newGlobalActionAuth.length > 0 && !template.team.organisation.organisationClaim.flags.cfr21) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'You do not have permission to set the action auth',
|
||||
});
|
||||
|
||||
if (!isDocumentEnterprise) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'You do not have permission to set this action auth type',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const authOptions = createDocumentAuthOptions({
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
import { getOrganisationClaimByUserId } from '../organisation/get-organisation-claims';
|
||||
|
||||
/**
|
||||
* Check if a user has enterprise features enabled (cfr21 flag).
|
||||
*/
|
||||
export const isUserEnterprise = async ({ userId }: { userId: number }): Promise<boolean> => {
|
||||
try {
|
||||
const organisationClaim = await getOrganisationClaimByUserId({ userId });
|
||||
return Boolean(organisationClaim.flags.cfr21);
|
||||
} catch {
|
||||
// If we can't find the organisation claim, assume non-enterprise
|
||||
return false;
|
||||
}
|
||||
};
|
||||
@@ -1,5 +0,0 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# Compiled translations.
|
||||
*.js
|
||||
*.mjs
|
||||
File diff suppressed because it is too large
Load Diff
@@ -167,10 +167,6 @@ msgstr "{0} Recipient(s)"
|
||||
msgid "{0} Teams"
|
||||
msgstr "{0} Teams"
|
||||
|
||||
#: apps/remix/app/components/tables/internal-audit-log-table.tsx
|
||||
msgid "{browserInfo} on {os}"
|
||||
msgstr "{browserInfo} on {os}"
|
||||
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-text-field.tsx
|
||||
msgid "{charactersRemaining, plural, one {1 character remaining} other {{charactersRemaining} characters remaining}}"
|
||||
msgstr "{charactersRemaining, plural, one {1 character remaining} other {{charactersRemaining} characters remaining}}"
|
||||
@@ -372,10 +368,6 @@ msgstr "{teamName} has invited you to {0}<0/>\"{documentName}\""
|
||||
msgid "{teamName} has invited you to {action} {documentName}"
|
||||
msgstr "{teamName} has invited you to {action} {documentName}"
|
||||
|
||||
#: apps/remix/app/components/tables/internal-audit-log-table.tsx
|
||||
msgid "{userAgent}"
|
||||
msgstr "{userAgent}"
|
||||
|
||||
#: packages/lib/utils/document-audit-logs.ts
|
||||
msgid "{userName} approved the document"
|
||||
msgstr "{userName} approved the document"
|
||||
@@ -753,6 +745,7 @@ msgstr "Acknowledgment"
|
||||
|
||||
#: apps/remix/app/components/tables/settings-security-activity-table.tsx
|
||||
#: apps/remix/app/components/tables/settings-public-profile-templates-table.tsx
|
||||
#: apps/remix/app/components/tables/internal-audit-log-table.tsx
|
||||
#: apps/remix/app/components/tables/documents-table-action-dropdown.tsx
|
||||
#: apps/remix/app/components/tables/document-logs-table.tsx
|
||||
#: apps/remix/app/components/general/document/document-page-view-dropdown.tsx
|
||||
@@ -1493,14 +1486,10 @@ msgstr "At least one signature type must be enabled"
|
||||
msgid "Attempts sealing the document again, useful for after a code change has occurred to resolve an erroneous document."
|
||||
msgstr "Attempts sealing the document again, useful for after a code change has occurred to resolve an erroneous document."
|
||||
|
||||
#: apps/remix/app/routes/_internal+/[__htmltopdf]+/audit-log.tsx
|
||||
#: apps/remix/app/components/general/document/document-page-view-dropdown.tsx
|
||||
msgid "Audit Log"
|
||||
msgstr "Audit Log"
|
||||
|
||||
#: apps/remix/app/components/general/document/document-page-view-dropdown.tsx
|
||||
msgid "Audit Logs"
|
||||
msgstr "Audit Logs"
|
||||
|
||||
#: apps/remix/app/routes/_internal+/[__htmltopdf]+/certificate.tsx
|
||||
msgid "Authentication Level"
|
||||
msgstr "Authentication Level"
|
||||
@@ -1602,6 +1591,7 @@ msgid "Branding preferences updated"
|
||||
msgstr "Branding preferences updated"
|
||||
|
||||
#: apps/remix/app/components/tables/settings-security-activity-table.tsx
|
||||
#: apps/remix/app/components/tables/internal-audit-log-table.tsx
|
||||
msgid "Browser"
|
||||
msgstr "Browser"
|
||||
|
||||
@@ -2119,10 +2109,6 @@ msgstr "Controls the formatting of the message that will be sent when inviting a
|
||||
msgid "Controls the language for the document, including the language to be used for email notifications, and the final certificate that is generated and attached to the document."
|
||||
msgstr "Controls the language for the document, including the language to be used for email notifications, and the final certificate that is generated and attached to the document."
|
||||
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
msgid "Controls whether the audit logs will be included in the document when it is downloaded. The audit logs can still be downloaded from the logs page separately."
|
||||
msgstr "Controls whether the audit logs will be included in the document when it is downloaded. The audit logs can still be downloaded from the logs page separately."
|
||||
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
msgid "Controls whether the signing certificate will be included in the document when it is downloaded. The signing certificate can still be downloaded from the logs page separately."
|
||||
msgstr "Controls whether the signing certificate will be included in the document when it is downloaded. The signing certificate can still be downloaded from the logs page separately."
|
||||
@@ -3154,7 +3140,6 @@ msgstr "Drag & drop your PDF here."
|
||||
msgid "Drag and drop or click to upload"
|
||||
msgstr "Drag and drop or click to upload"
|
||||
|
||||
#: apps/remix/app/components/general/template/template-drop-zone-wrapper.tsx
|
||||
#: apps/remix/app/components/general/document/document-drop-zone-wrapper.tsx
|
||||
msgid "Drag and drop your PDF file here"
|
||||
msgstr "Drag and drop your PDF file here"
|
||||
@@ -3683,7 +3668,6 @@ msgstr "Fields"
|
||||
msgid "Fields updated"
|
||||
msgstr "Fields updated"
|
||||
|
||||
#: apps/remix/app/components/general/template/template-drop-zone-wrapper.tsx
|
||||
#: apps/remix/app/components/general/document/document-upload.tsx
|
||||
#: apps/remix/app/components/general/document/document-drop-zone-wrapper.tsx
|
||||
#: apps/remix/app/components/embed/authoring/configure-document-upload.tsx
|
||||
@@ -4058,10 +4042,6 @@ msgstr "Inbox"
|
||||
msgid "Inbox documents"
|
||||
msgstr "Inbox documents"
|
||||
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
msgid "Include the Audit Logs in the Document"
|
||||
msgstr "Include the Audit Logs in the Document"
|
||||
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
msgid "Include the Signing Certificate in the Document"
|
||||
msgstr "Include the Signing Certificate in the Document"
|
||||
@@ -4084,7 +4064,6 @@ msgstr "Inherit authentication method"
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/branding-preferences-form.tsx
|
||||
msgid "Inherit from organisation"
|
||||
msgstr "Inherit from organisation"
|
||||
@@ -4620,10 +4599,6 @@ msgstr "Multiple access methods can be selected."
|
||||
msgid "My Folder"
|
||||
msgstr "My Folder"
|
||||
|
||||
#: apps/remix/app/components/tables/internal-audit-log-table.tsx
|
||||
msgid "N/A"
|
||||
msgstr "N/A"
|
||||
|
||||
#: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.email-domains.$id.tsx
|
||||
#: apps/remix/app/routes/_authenticated+/admin+/users.$id.tsx
|
||||
#: apps/remix/app/components/tables/settings-security-passkey-table.tsx
|
||||
@@ -4704,7 +4679,6 @@ msgstr "Next"
|
||||
msgid "Next field"
|
||||
msgstr "Next field"
|
||||
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/branding-preferences-form.tsx
|
||||
@@ -5364,7 +5338,6 @@ msgstr "Please try a different domain."
|
||||
msgid "Please try again and make sure you enter the correct email address."
|
||||
msgstr "Please try again and make sure you enter the correct email address."
|
||||
|
||||
#: apps/remix/app/components/general/template/template-drop-zone-wrapper.tsx
|
||||
#: apps/remix/app/components/dialogs/template-create-dialog.tsx
|
||||
msgid "Please try again later."
|
||||
msgstr "Please try again later."
|
||||
@@ -6417,7 +6390,6 @@ msgstr "Some signers have not been assigned a signature field. Please assign at
|
||||
#: apps/remix/app/components/general/share-document-download-button.tsx
|
||||
#: apps/remix/app/components/general/billing-plans.tsx
|
||||
#: apps/remix/app/components/general/billing-plans.tsx
|
||||
#: apps/remix/app/components/general/template/template-drop-zone-wrapper.tsx
|
||||
#: apps/remix/app/components/general/teams/team-email-usage.tsx
|
||||
#: apps/remix/app/components/general/teams/team-email-dropdown.tsx
|
||||
#: apps/remix/app/components/general/organisations/organisation-invitations.tsx
|
||||
@@ -6855,10 +6827,6 @@ msgstr "Template title"
|
||||
msgid "Template updated successfully"
|
||||
msgstr "Template updated successfully"
|
||||
|
||||
#: apps/remix/app/components/general/template/template-drop-zone-wrapper.tsx
|
||||
msgid "Template uploaded"
|
||||
msgstr "Template uploaded"
|
||||
|
||||
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates._index.tsx
|
||||
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates.$id._index.tsx
|
||||
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.public-profile.tsx
|
||||
@@ -7477,6 +7445,7 @@ msgstr "This will remove all emails associated with this email domain"
|
||||
msgid "This will sign you out of all other devices. You will need to sign in again on those devices to continue using your account."
|
||||
msgstr "This will sign you out of all other devices. You will need to sign in again on those devices to continue using your account."
|
||||
|
||||
#: apps/remix/app/components/tables/internal-audit-log-table.tsx
|
||||
#: apps/remix/app/components/tables/document-logs-table.tsx
|
||||
msgid "Time"
|
||||
msgstr "Time"
|
||||
@@ -7510,10 +7479,6 @@ msgstr "Title cannot be empty"
|
||||
msgid "To accept this invitation you must create an account."
|
||||
msgstr "To accept this invitation you must create an account."
|
||||
|
||||
#: apps/remix/app/components/dialogs/team-member-create-dialog.tsx
|
||||
msgid "To be able to add members to a team, you must first add them to the organisation. For more information, please see the <0>documentation</0>."
|
||||
msgstr "To be able to add members to a team, you must first add them to the organisation. For more information, please see the <0>documentation</0>."
|
||||
|
||||
#: apps/remix/app/components/dialogs/team-email-update-dialog.tsx
|
||||
msgid "To change the email you must remove and add a new email address."
|
||||
msgstr "To change the email you must remove and add a new email address."
|
||||
@@ -7963,10 +7928,6 @@ msgstr "Upload Document"
|
||||
msgid "Upload Signature"
|
||||
msgstr "Upload Signature"
|
||||
|
||||
#: apps/remix/app/components/general/template/template-drop-zone-wrapper.tsx
|
||||
msgid "Upload Template"
|
||||
msgstr "Upload Template"
|
||||
|
||||
#: packages/ui/primitives/document-upload.tsx
|
||||
#: packages/ui/primitives/document-dropzone.tsx
|
||||
msgid "Upload Template Document"
|
||||
@@ -7997,10 +7958,6 @@ msgstr "Uploaded file not an allowed file type"
|
||||
msgid "Uploading document..."
|
||||
msgstr "Uploading document..."
|
||||
|
||||
#: apps/remix/app/components/general/template/template-drop-zone-wrapper.tsx
|
||||
msgid "Uploading template..."
|
||||
msgstr "Uploading template..."
|
||||
|
||||
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates.$id._index.tsx
|
||||
msgid "Use"
|
||||
msgstr "Use"
|
||||
@@ -8028,10 +7985,6 @@ msgstr "Use your passkey for authentication"
|
||||
msgid "User"
|
||||
msgstr "User"
|
||||
|
||||
#: apps/remix/app/components/tables/internal-audit-log-table.tsx
|
||||
msgid "User Agent"
|
||||
msgstr "User Agent"
|
||||
|
||||
#: apps/remix/app/components/forms/password.tsx
|
||||
msgid "User has no password."
|
||||
msgstr "User has no password."
|
||||
@@ -8109,6 +8062,10 @@ msgstr "Verify your email to upload documents."
|
||||
msgid "Verify your team email address"
|
||||
msgstr "Verify your team email address"
|
||||
|
||||
#: apps/remix/app/routes/_internal+/[__htmltopdf]+/audit-log.tsx
|
||||
msgid "Version History"
|
||||
msgstr "Version History"
|
||||
|
||||
#: packages/ui/primitives/document-flow/field-items-advanced-settings/checkbox-field.tsx
|
||||
msgid "Vertical"
|
||||
msgstr "Vertical"
|
||||
@@ -8659,7 +8616,6 @@ msgstr "Write a description to display on your public profile"
|
||||
msgid "Yearly"
|
||||
msgstr "Yearly"
|
||||
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/document-preferences-form.tsx
|
||||
#: apps/remix/app/components/forms/branding-preferences-form.tsx
|
||||
@@ -9268,10 +9224,6 @@ msgstr "Your team has been successfully deleted."
|
||||
msgid "Your team has been successfully updated."
|
||||
msgstr "Your team has been successfully updated."
|
||||
|
||||
#: apps/remix/app/components/general/template/template-drop-zone-wrapper.tsx
|
||||
msgid "Your template failed to upload."
|
||||
msgstr "Your template failed to upload."
|
||||
|
||||
#: apps/remix/app/routes/embed+/v1+/authoring_.completed.create.tsx
|
||||
msgid "Your template has been created successfully"
|
||||
msgstr "Your template has been created successfully"
|
||||
@@ -9284,10 +9236,6 @@ msgstr "Your template has been duplicated successfully."
|
||||
msgid "Your template has been successfully deleted."
|
||||
msgstr "Your template has been successfully deleted."
|
||||
|
||||
#: apps/remix/app/components/general/template/template-drop-zone-wrapper.tsx
|
||||
msgid "Your template has been uploaded successfully. You will be redirected to the template page."
|
||||
msgstr "Your template has been uploaded successfully. You will be redirected to the template page."
|
||||
|
||||
#: apps/remix/app/components/dialogs/template-duplicate-dialog.tsx
|
||||
msgid "Your template will be duplicated."
|
||||
msgstr "Your template will be duplicated."
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+114
-681
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+114
-681
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -40,6 +40,7 @@ export const ZDocumentAuditLogTypeSchema = z.enum([
|
||||
'DOCUMENT_TITLE_UPDATED', // When the document title is updated.
|
||||
'DOCUMENT_EXTERNAL_ID_UPDATED', // When the document external ID is updated.
|
||||
'DOCUMENT_MOVED_TO_TEAM', // When the document is moved to a team.
|
||||
'DOCUMENT_ATTACHMENTS_UPDATED', // When the document attachments are updated.
|
||||
]);
|
||||
|
||||
export const ZDocumentAuditLogEmailTypeSchema = z.enum([
|
||||
@@ -598,6 +599,29 @@ export const ZDocumentAuditLogEventDocumentMovedToTeamSchema = z.object({
|
||||
}),
|
||||
});
|
||||
|
||||
/**
|
||||
* Event: Document attachments updated.
|
||||
*/
|
||||
export const ZDocumentAuditLogEventDocumentAttachmentsUpdatedSchema = z.object({
|
||||
type: z.literal(DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_ATTACHMENTS_UPDATED),
|
||||
data: z.object({
|
||||
from: z.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
label: z.string(),
|
||||
url: z.string(),
|
||||
}),
|
||||
),
|
||||
to: z.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
label: z.string(),
|
||||
url: z.string(),
|
||||
}),
|
||||
),
|
||||
}),
|
||||
});
|
||||
|
||||
export const ZDocumentAuditLogBaseSchema = z.object({
|
||||
id: z.string(),
|
||||
createdAt: z.date(),
|
||||
@@ -630,6 +654,7 @@ export const ZDocumentAuditLogSchema = ZDocumentAuditLogBaseSchema.and(
|
||||
ZDocumentAuditLogEventDocumentSentSchema,
|
||||
ZDocumentAuditLogEventDocumentTitleUpdatedSchema,
|
||||
ZDocumentAuditLogEventDocumentExternalIdUpdatedSchema,
|
||||
ZDocumentAuditLogEventDocumentAttachmentsUpdatedSchema,
|
||||
ZDocumentAuditLogEventFieldCreatedSchema,
|
||||
ZDocumentAuditLogEventFieldRemovedSchema,
|
||||
ZDocumentAuditLogEventFieldUpdatedSchema,
|
||||
|
||||
@@ -82,16 +82,6 @@ export const ZDocumentActionAuthTypesSchema = z
|
||||
'The type of authentication required for the recipient to sign the document. This field is restricted to Enterprise plan users only.',
|
||||
);
|
||||
|
||||
/**
|
||||
* The non-enterprise document action auth methods.
|
||||
*
|
||||
* Only includes options available to non-enterprise users.
|
||||
*/
|
||||
export const ZNonEnterpriseDocumentActionAuthTypesSchema = z.enum([
|
||||
DocumentAuth.TWO_FACTOR_AUTH,
|
||||
DocumentAuth.EXPLICIT_NONE,
|
||||
]);
|
||||
|
||||
/**
|
||||
* The recipient access auth methods.
|
||||
*
|
||||
@@ -128,7 +118,6 @@ export const ZRecipientActionAuthTypesSchema = z
|
||||
|
||||
export const DocumentAccessAuth = ZDocumentAccessAuthTypesSchema.Enum;
|
||||
export const DocumentActionAuth = ZDocumentActionAuthTypesSchema.Enum;
|
||||
export const NonEnterpriseDocumentActionAuth = ZNonEnterpriseDocumentActionAuthTypesSchema.Enum;
|
||||
export const RecipientAccessAuth = ZRecipientAccessAuthTypesSchema.Enum;
|
||||
export const RecipientActionAuth = ZRecipientActionAuthTypesSchema.Enum;
|
||||
|
||||
@@ -212,9 +201,6 @@ export type TDocumentAccessAuth = z.infer<typeof ZDocumentAccessAuthSchema>;
|
||||
export type TDocumentAccessAuthTypes = z.infer<typeof ZDocumentAccessAuthTypesSchema>;
|
||||
export type TDocumentActionAuth = z.infer<typeof ZDocumentActionAuthSchema>;
|
||||
export type TDocumentActionAuthTypes = z.infer<typeof ZDocumentActionAuthTypesSchema>;
|
||||
export type TNonEnterpriseDocumentActionAuthTypes = z.infer<
|
||||
typeof ZNonEnterpriseDocumentActionAuthTypesSchema
|
||||
>;
|
||||
export type TRecipientAccessAuth = z.infer<typeof ZRecipientAccessAuthSchema>;
|
||||
export type TRecipientAccessAuthTypes = z.infer<typeof ZRecipientAccessAuthTypesSchema>;
|
||||
export type TRecipientActionAuth = z.infer<typeof ZRecipientActionAuthSchema>;
|
||||
|
||||
@@ -134,7 +134,7 @@ export const internalClaims: InternalClaims = {
|
||||
unlimitedDocuments: true,
|
||||
allowCustomBranding: true,
|
||||
hidePoweredBy: true,
|
||||
emailDomains: false,
|
||||
emailDomains: true,
|
||||
embedAuthoring: false,
|
||||
embedAuthoringWhiteLabel: true,
|
||||
embedSigning: false,
|
||||
|
||||
@@ -305,150 +305,87 @@ export const formatDocumentAuditLogAction = (
|
||||
|
||||
const description = match(auditLog)
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.FIELD_CREATED }, () => ({
|
||||
anonymous: msg({
|
||||
message: `A field was added`,
|
||||
context: `Audit log format`,
|
||||
}),
|
||||
anonymous: msg`A field was added`,
|
||||
identified: msg`${prefix} added a field`,
|
||||
}))
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.FIELD_DELETED }, () => ({
|
||||
anonymous: msg({
|
||||
message: `A field was removed`,
|
||||
context: `Audit log format`,
|
||||
}),
|
||||
anonymous: msg`A field was removed`,
|
||||
identified: msg`${prefix} removed a field`,
|
||||
}))
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.FIELD_UPDATED }, () => ({
|
||||
anonymous: msg({
|
||||
message: `A field was updated`,
|
||||
context: `Audit log format`,
|
||||
}),
|
||||
anonymous: msg`A field was updated`,
|
||||
identified: msg`${prefix} updated a field`,
|
||||
}))
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.RECIPIENT_CREATED }, () => ({
|
||||
anonymous: msg({
|
||||
message: `A recipient was added`,
|
||||
context: `Audit log format`,
|
||||
}),
|
||||
anonymous: msg`A recipient was added`,
|
||||
identified: msg`${prefix} added a recipient`,
|
||||
}))
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.RECIPIENT_DELETED }, () => ({
|
||||
anonymous: msg({
|
||||
message: `A recipient was removed`,
|
||||
context: `Audit log format`,
|
||||
}),
|
||||
anonymous: msg`A recipient was removed`,
|
||||
identified: msg`${prefix} removed a recipient`,
|
||||
}))
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.RECIPIENT_UPDATED }, () => ({
|
||||
anonymous: msg({
|
||||
message: `A recipient was updated`,
|
||||
context: `Audit log format`,
|
||||
}),
|
||||
anonymous: msg`A recipient was updated`,
|
||||
identified: msg`${prefix} updated a recipient`,
|
||||
}))
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_CREATED }, () => ({
|
||||
anonymous: msg({
|
||||
message: `Document created`,
|
||||
context: `Audit log format`,
|
||||
}),
|
||||
anonymous: msg`Document created`,
|
||||
identified: msg`${prefix} created the document`,
|
||||
}))
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_DELETED }, () => ({
|
||||
anonymous: msg({
|
||||
message: `Document deleted`,
|
||||
context: `Audit log format`,
|
||||
}),
|
||||
anonymous: msg`Document deleted`,
|
||||
identified: msg`${prefix} deleted the document`,
|
||||
}))
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FIELD_INSERTED }, () => ({
|
||||
anonymous: msg({
|
||||
message: `Field signed`,
|
||||
context: `Audit log format`,
|
||||
}),
|
||||
anonymous: msg`Field signed`,
|
||||
identified: msg`${prefix} signed a field`,
|
||||
}))
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FIELD_UNINSERTED }, () => ({
|
||||
anonymous: msg({
|
||||
message: `Field unsigned`,
|
||||
context: `Audit log format`,
|
||||
}),
|
||||
anonymous: msg`Field unsigned`,
|
||||
identified: msg`${prefix} unsigned a field`,
|
||||
}))
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FIELD_PREFILLED }, () => ({
|
||||
anonymous: msg({
|
||||
message: `Field prefilled by assistant`,
|
||||
context: `Audit log format`,
|
||||
}),
|
||||
anonymous: msg`Field prefilled by assistant`,
|
||||
identified: msg`${prefix} prefilled a field`,
|
||||
}))
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_VISIBILITY_UPDATED }, () => ({
|
||||
anonymous: msg({
|
||||
message: `Document visibility updated`,
|
||||
context: `Audit log format`,
|
||||
}),
|
||||
anonymous: msg`Document visibility updated`,
|
||||
identified: msg`${prefix} updated the document visibility`,
|
||||
}))
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_GLOBAL_AUTH_ACCESS_UPDATED }, () => ({
|
||||
anonymous: msg({
|
||||
message: `Document access auth updated`,
|
||||
context: `Audit log format`,
|
||||
}),
|
||||
anonymous: msg`Document access auth updated`,
|
||||
identified: msg`${prefix} updated the document access auth requirements`,
|
||||
}))
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_GLOBAL_AUTH_ACTION_UPDATED }, () => ({
|
||||
anonymous: msg({
|
||||
message: `Document signing auth updated`,
|
||||
context: `Audit log format`,
|
||||
}),
|
||||
anonymous: msg`Document signing auth updated`,
|
||||
identified: msg`${prefix} updated the document signing auth requirements`,
|
||||
}))
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_META_UPDATED }, () => ({
|
||||
anonymous: msg({
|
||||
message: `Document updated`,
|
||||
context: `Audit log format`,
|
||||
}),
|
||||
anonymous: msg`Document updated`,
|
||||
identified: msg`${prefix} updated the document`,
|
||||
}))
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_OPENED }, () => ({
|
||||
anonymous: msg({
|
||||
message: `Document opened`,
|
||||
context: `Audit log format`,
|
||||
}),
|
||||
anonymous: msg`Document opened`,
|
||||
identified: msg`${prefix} opened the document`,
|
||||
}))
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_VIEWED }, () => ({
|
||||
anonymous: msg({
|
||||
message: `Document viewed`,
|
||||
context: `Audit log format`,
|
||||
}),
|
||||
anonymous: msg`Document viewed`,
|
||||
identified: msg`${prefix} viewed the document`,
|
||||
}))
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_TITLE_UPDATED }, () => ({
|
||||
anonymous: msg({
|
||||
message: `Document title updated`,
|
||||
context: `Audit log format`,
|
||||
}),
|
||||
anonymous: msg`Document title updated`,
|
||||
identified: msg`${prefix} updated the document title`,
|
||||
}))
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_EXTERNAL_ID_UPDATED }, () => ({
|
||||
anonymous: msg({
|
||||
message: `Document external ID updated`,
|
||||
context: `Audit log format`,
|
||||
}),
|
||||
anonymous: msg`Document external ID updated`,
|
||||
identified: msg`${prefix} updated the document external ID`,
|
||||
}))
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_SENT }, () => ({
|
||||
anonymous: msg({
|
||||
message: `Document sent`,
|
||||
context: `Audit log format`,
|
||||
}),
|
||||
anonymous: msg`Document sent`,
|
||||
identified: msg`${prefix} sent the document`,
|
||||
}))
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_MOVED_TO_TEAM }, () => ({
|
||||
anonymous: msg({
|
||||
message: `Document moved to team`,
|
||||
context: `Audit log format`,
|
||||
}),
|
||||
anonymous: msg`Document moved to team`,
|
||||
identified: msg`${prefix} moved the document to team`,
|
||||
}))
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_COMPLETED }, ({ data }) => {
|
||||
@@ -483,14 +420,12 @@ export const formatDocumentAuditLogAction = (
|
||||
: msg`${prefix} sent an email to ${data.recipientEmail}`,
|
||||
}))
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_COMPLETED }, () => ({
|
||||
anonymous: msg({
|
||||
message: `Document completed`,
|
||||
context: `Audit log format`,
|
||||
}),
|
||||
identified: msg({
|
||||
message: `Document completed`,
|
||||
context: `Audit log format`,
|
||||
}),
|
||||
anonymous: msg`Document completed`,
|
||||
identified: msg`Document completed`,
|
||||
}))
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_ATTACHMENTS_UPDATED }, () => ({
|
||||
anonymous: msg`Document attachments updated`,
|
||||
identified: msg`${prefix} updated the document attachments`,
|
||||
}))
|
||||
.exhaustive();
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { type TransportTargetOptions, pino } from 'pino';
|
||||
|
||||
import type { BaseApiLog } from '../types/api-logs';
|
||||
import { extractRequestMetadata } from '../universal/extract-request-metadata';
|
||||
import { env } from './env';
|
||||
|
||||
const transports: TransportTargetOptions[] = [];
|
||||
@@ -35,31 +33,3 @@ export const logger = pino({
|
||||
}
|
||||
: undefined,
|
||||
});
|
||||
|
||||
export const logDocumentAccess = ({
|
||||
request,
|
||||
documentId,
|
||||
userId,
|
||||
}: {
|
||||
request: Request;
|
||||
documentId: number;
|
||||
userId: number;
|
||||
}) => {
|
||||
const metadata = extractRequestMetadata(request);
|
||||
|
||||
const data: BaseApiLog = {
|
||||
ipAddress: metadata.ipAddress,
|
||||
userAgent: metadata.userAgent,
|
||||
path: new URL(request.url).pathname,
|
||||
auth: 'session',
|
||||
source: 'app',
|
||||
userId,
|
||||
};
|
||||
|
||||
logger.info({
|
||||
...data,
|
||||
input: {
|
||||
documentId,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -120,7 +120,6 @@ export const generateDefaultOrganisationSettings = (): Omit<
|
||||
|
||||
includeSenderDetails: true,
|
||||
includeSigningCertificate: true,
|
||||
includeAuditLog: false,
|
||||
|
||||
typedSignatureEnabled: true,
|
||||
uploadSignatureEnabled: true,
|
||||
|
||||
@@ -170,7 +170,6 @@ export const generateDefaultTeamSettings = (): Omit<TeamGlobalSettings, 'id' | '
|
||||
|
||||
includeSenderDetails: null,
|
||||
includeSigningCertificate: null,
|
||||
includeAuditLog: null,
|
||||
|
||||
typedSignatureEnabled: null,
|
||||
uploadSignatureEnabled: null,
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "AttachmentType" AS ENUM ('FILE', 'VIDEO', 'AUDIO', 'IMAGE', 'LINK', 'OTHER');
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Attachment" (
|
||||
"id" TEXT NOT NULL,
|
||||
"type" "AttachmentType" NOT NULL,
|
||||
"label" TEXT NOT NULL,
|
||||
"url" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"documentId" INTEGER,
|
||||
"templateId" INTEGER,
|
||||
|
||||
CONSTRAINT "Attachment_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Attachment" ADD CONSTRAINT "Attachment_documentId_fkey" FOREIGN KEY ("documentId") REFERENCES "Document"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Attachment" ADD CONSTRAINT "Attachment_templateId_fkey" FOREIGN KEY ("templateId") REFERENCES "Template"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Attachment" ALTER COLUMN "type" SET DEFAULT 'LINK';
|
||||
@@ -1,5 +0,0 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "OrganisationGlobalSettings" ADD COLUMN "includeAuditLog" BOOLEAN NOT NULL DEFAULT false;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "TeamGlobalSettings" ADD COLUMN "includeAuditLog" BOOLEAN;
|
||||
@@ -1,12 +0,0 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "UserTwoFactorEmailVerification" (
|
||||
"userId" INTEGER NOT NULL,
|
||||
"verificationCode" TEXT NOT NULL,
|
||||
"expiresAt" TIMESTAMP(3) NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "UserTwoFactorEmailVerification_pkey" PRIMARY KEY ("userId")
|
||||
);
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "UserTwoFactorEmailVerification" ADD CONSTRAINT "UserTwoFactorEmailVerification_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -59,10 +59,9 @@ model User {
|
||||
ownedOrganisations Organisation[]
|
||||
organisationMember OrganisationMember[]
|
||||
|
||||
twoFactorSecret String?
|
||||
twoFactorEnabled Boolean @default(false)
|
||||
twoFactorBackupCodes String?
|
||||
twoFactorEmailVerification UserTwoFactorEmailVerification?
|
||||
twoFactorSecret String?
|
||||
twoFactorEnabled Boolean @default(false)
|
||||
twoFactorBackupCodes String?
|
||||
|
||||
folders Folder[]
|
||||
documents Document[]
|
||||
@@ -334,6 +333,32 @@ enum DocumentVisibility {
|
||||
ADMIN
|
||||
}
|
||||
|
||||
// Only "LINK" is supported for now.
|
||||
// All other attachment types are not yet supported.
|
||||
enum AttachmentType {
|
||||
FILE
|
||||
VIDEO
|
||||
AUDIO
|
||||
IMAGE
|
||||
LINK
|
||||
OTHER
|
||||
}
|
||||
|
||||
model Attachment {
|
||||
id String @id @default(uuid())
|
||||
type AttachmentType @default(LINK)
|
||||
label String
|
||||
url String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @default(now()) @updatedAt
|
||||
|
||||
documentId Int?
|
||||
document Document? @relation(fields: [documentId], references: [id], onDelete: Cascade)
|
||||
|
||||
templateId Int?
|
||||
template Template? @relation(fields: [templateId], references: [id], onDelete: Cascade)
|
||||
}
|
||||
|
||||
enum FolderType {
|
||||
DOCUMENT
|
||||
TEMPLATE
|
||||
@@ -392,14 +417,15 @@ model Document {
|
||||
templateId Int?
|
||||
source DocumentSource
|
||||
|
||||
useLegacyFieldInsertion Boolean @default(false)
|
||||
auditLogs DocumentAuditLog[]
|
||||
attachments Attachment[]
|
||||
useLegacyFieldInsertion Boolean @default(false)
|
||||
|
||||
documentData DocumentData @relation(fields: [documentDataId], references: [id], onDelete: Cascade)
|
||||
template Template? @relation(fields: [templateId], references: [id], onDelete: SetNull)
|
||||
|
||||
auditLogs DocumentAuditLog[]
|
||||
folder Folder? @relation(fields: [folderId], references: [id], onDelete: SetNull)
|
||||
folderId String?
|
||||
folder Folder? @relation(fields: [folderId], references: [id], onDelete: Cascade)
|
||||
folderId String?
|
||||
|
||||
@@unique([documentDataId])
|
||||
@@index([userId])
|
||||
@@ -735,13 +761,13 @@ model OrganisationGlobalSettings {
|
||||
id String @id
|
||||
organisation Organisation?
|
||||
|
||||
documentVisibility DocumentVisibility @default(EVERYONE)
|
||||
documentLanguage String @default("en")
|
||||
includeSenderDetails Boolean @default(true)
|
||||
includeSigningCertificate Boolean @default(true)
|
||||
includeAuditLog Boolean @default(false)
|
||||
documentTimezone String? // Nullable to allow using local timezones if not set.
|
||||
documentDateFormat String @default("yyyy-MM-dd hh:mm a")
|
||||
documentVisibility DocumentVisibility @default(EVERYONE)
|
||||
documentLanguage String @default("en")
|
||||
documentTimezone String? // Nullable to allow using local timezones if not set.
|
||||
documentDateFormat String @default("yyyy-MM-dd hh:mm a")
|
||||
|
||||
includeSenderDetails Boolean @default(true)
|
||||
includeSigningCertificate Boolean @default(true)
|
||||
|
||||
typedSignatureEnabled Boolean @default(true)
|
||||
uploadSignatureEnabled Boolean @default(true)
|
||||
@@ -772,7 +798,6 @@ model TeamGlobalSettings {
|
||||
|
||||
includeSenderDetails Boolean?
|
||||
includeSigningCertificate Boolean?
|
||||
includeAuditLog Boolean?
|
||||
|
||||
typedSignatureEnabled Boolean?
|
||||
uploadSignatureEnabled Boolean?
|
||||
@@ -894,10 +919,11 @@ model Template {
|
||||
|
||||
templateDocumentData DocumentData @relation(fields: [templateDocumentDataId], references: [id], onDelete: Cascade)
|
||||
|
||||
recipients Recipient[]
|
||||
fields Field[]
|
||||
directLink TemplateDirectLink?
|
||||
documents Document[]
|
||||
recipients Recipient[]
|
||||
fields Field[]
|
||||
directLink TemplateDirectLink?
|
||||
documents Document[]
|
||||
attachments Attachment[]
|
||||
|
||||
folder Folder? @relation(fields: [folderId], references: [id], onDelete: SetNull)
|
||||
folderId String?
|
||||
@@ -986,15 +1012,6 @@ model AvatarImage {
|
||||
organisation Organisation[]
|
||||
}
|
||||
|
||||
model UserTwoFactorEmailVerification {
|
||||
userId Int @id
|
||||
verificationCode String
|
||||
expiresAt DateTime
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
}
|
||||
|
||||
enum EmailDomainStatus {
|
||||
PENDING
|
||||
ACTIVE
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user