mirror of
https://github.com/documenso/documenso.git
synced 2026-07-10 13:06:00 +10:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 35410e100e | |||
| e52af336b4 | |||
| 04e3e1eeb9 | |||
| 43810c4357 |
@@ -136,5 +136,3 @@ E2E_TEST_AUTHENTICATE_USER_PASSWORD="test_Password123"
|
||||
# OPTIONAL: The file to save the logger output to. Will disable stdout if provided.
|
||||
NEXT_PRIVATE_LOGGER_FILE_PATH=
|
||||
|
||||
# [[PLAIN SUPPORT]]
|
||||
NEXT_PRIVATE_PLAIN_API_KEY=
|
||||
|
||||
@@ -214,6 +214,8 @@ For detailed instructions on how to configure and run the Docker container, plea
|
||||
|
||||
We support a variety of deployment methods, and are actively working on adding more. Stay tuned for updates!
|
||||
|
||||
> Please note that the below deployment methods are for v0.9, we will update these to v1.0 once it has been released.
|
||||
|
||||
### Fetch, configure, and build
|
||||
|
||||
First, clone the code from Github:
|
||||
@@ -256,7 +258,7 @@ npm run start
|
||||
|
||||
This will start the server on `localhost:3000`. For now, any reverse proxy can then do the frontend and SSL termination.
|
||||
|
||||
> If you want to run with another port than 3000, you can start the application with `next -p <ANY PORT>` from the `apps/remix` folder.
|
||||
> If you want to run with another port than 3000, you can start the application with `next -p <ANY PORT>` from the `apps/web` folder.
|
||||
|
||||
### Run as a service
|
||||
|
||||
@@ -306,7 +308,7 @@ The Web UI can be found at http://localhost:9000, while the SMTP port will be on
|
||||
|
||||
### Support IPv6
|
||||
|
||||
If you are deploying to a cluster that uses only IPv6, You can use a custom command to pass a parameter to the Remix start command
|
||||
If you are deploying to a cluster that uses only IPv6, You can use a custom command to pass a parameter to the Next.js start command
|
||||
|
||||
For local docker run
|
||||
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ For the digital signature of your documents you need a signing certificate in .p
|
||||
|
||||
4. You will be prompted to enter a password for the p12 file. Choose a strong password and remember it, as you will need it to use the certificate (**can be empty for dev certificates**)
|
||||
|
||||
5. Place the certificate `/apps/remix/resources/certificate.p12` (If the path does not exist, it needs to be created)
|
||||
5. Place the certificate `/apps/web/resources/certificate.p12` (If the path does not exist, it needs to be created)
|
||||
|
||||
## Docker
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ The translation files are organized into folders represented by their respective
|
||||
Each PO file contains translations which look like this:
|
||||
|
||||
```po
|
||||
#: apps/remix/app/(signing)/sign/[token]/no-longer-available.tsx:61
|
||||
#: apps/web/src/app/(signing)/sign/[token]/no-longer-available.tsx:61
|
||||
msgid "Want to send slick signing links like this one? <0>Check out Documenso.</0>"
|
||||
msgstr "Möchten Sie auffällige Signatur-Links wie diesen senden? <0>Überprüfen Sie Documenso.</0>"
|
||||
```
|
||||
|
||||
@@ -54,7 +54,7 @@ Install the project dependencies as follows:
|
||||
|
||||
```bash
|
||||
npm i
|
||||
npm run build
|
||||
npm run build:web
|
||||
npm run prisma:migrate-deploy
|
||||
```
|
||||
|
||||
@@ -69,7 +69,7 @@ npm run start
|
||||
This will start the server on `localhost:3000`. Any reverse proxy can handle the front end and SSL termination.
|
||||
|
||||
<Callout type="info">
|
||||
If you want to run with another port than `3000`, you can start the application with `next -p <ANY PORT>` from the `apps/remix` folder.
|
||||
If you want to run with another port than `3000`, you can start the application with `next -p <ANY PORT>` from the `apps/web` folder.
|
||||
</Callout>
|
||||
|
||||
</Steps>
|
||||
@@ -249,7 +249,7 @@ After=network.target
|
||||
Environment=PATH=/path/to/your/node/binaries
|
||||
Type=simple
|
||||
User=www-data
|
||||
WorkingDirectory=/var/www/documenso/apps/remix
|
||||
WorkingDirectory=/var/www/documenso/apps/web
|
||||
ExecStart=/usr/bin/next start -p 3500
|
||||
TimeoutSec=15
|
||||
Restart=always
|
||||
|
||||
@@ -34,7 +34,7 @@ export const AdminDocumentDeleteDialog = ({ document }: AdminDocumentDeleteDialo
|
||||
const [reason, setReason] = useState('');
|
||||
|
||||
const { mutateAsync: deleteDocument, isPending: isDeletingDocument } =
|
||||
trpc.admin.document.delete.useMutation();
|
||||
trpc.admin.deleteDocument.useMutation();
|
||||
|
||||
const handleDeleteDocument = async () => {
|
||||
try {
|
||||
|
||||
@@ -35,7 +35,7 @@ export const AdminUserDeleteDialog = ({ className, user }: AdminUserDeleteDialog
|
||||
const [email, setEmail] = useState('');
|
||||
|
||||
const { mutateAsync: deleteUser, isPending: isDeletingUser } =
|
||||
trpc.admin.user.delete.useMutation();
|
||||
trpc.admin.deleteUser.useMutation();
|
||||
|
||||
const onDeleteAccount = async () => {
|
||||
try {
|
||||
|
||||
@@ -37,7 +37,7 @@ export const AdminUserDisableDialog = ({
|
||||
const [email, setEmail] = useState('');
|
||||
|
||||
const { mutateAsync: disableUser, isPending: isDisablingUser } =
|
||||
trpc.admin.user.disable.useMutation();
|
||||
trpc.admin.disableUser.useMutation();
|
||||
|
||||
const onDisableAccount = async () => {
|
||||
try {
|
||||
|
||||
@@ -34,7 +34,7 @@ export const AdminUserEnableDialog = ({ className, userToEnable }: AdminUserEnab
|
||||
const [email, setEmail] = useState('');
|
||||
|
||||
const { mutateAsync: enableUser, isPending: isEnablingUser } =
|
||||
trpc.admin.user.enable.useMutation();
|
||||
trpc.admin.enableUser.useMutation();
|
||||
|
||||
const onEnableAccount = async () => {
|
||||
try {
|
||||
|
||||
@@ -1,159 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import type { User } from '@prisma/client';
|
||||
import { useRevalidator } from 'react-router';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from '@documenso/ui/primitives/dialog';
|
||||
import { Input } from '@documenso/ui/primitives/input';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
export type AdminUserResetTwoFactorDialogProps = {
|
||||
className?: string;
|
||||
user: User;
|
||||
};
|
||||
|
||||
export const AdminUserResetTwoFactorDialog = ({
|
||||
className,
|
||||
user,
|
||||
}: AdminUserResetTwoFactorDialogProps) => {
|
||||
const { _ } = useLingui();
|
||||
const { toast } = useToast();
|
||||
const { revalidate } = useRevalidator();
|
||||
const [email, setEmail] = useState('');
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const { mutateAsync: resetTwoFactor, isPending: isResettingTwoFactor } =
|
||||
trpc.admin.user.resetTwoFactor.useMutation();
|
||||
|
||||
const onResetTwoFactor = async () => {
|
||||
try {
|
||||
await resetTwoFactor({
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
toast({
|
||||
title: _(msg`2FA Reset`),
|
||||
description: _(msg`The user's two factor authentication has been reset successfully.`),
|
||||
duration: 5000,
|
||||
});
|
||||
|
||||
await revalidate();
|
||||
setOpen(false);
|
||||
} catch (err) {
|
||||
const error = AppError.parseError(err);
|
||||
|
||||
const errorMessage = match(error.code)
|
||||
.with(AppErrorCode.NOT_FOUND, () => msg`User not found.`)
|
||||
.with(
|
||||
AppErrorCode.UNAUTHORIZED,
|
||||
() => msg`You are not authorized to reset two factor authentcation for this user.`,
|
||||
)
|
||||
.otherwise(
|
||||
() => msg`An error occurred while resetting two factor authentication for the user.`,
|
||||
);
|
||||
|
||||
toast({
|
||||
title: _(msg`Error`),
|
||||
description: _(errorMessage),
|
||||
variant: 'destructive',
|
||||
duration: 7500,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpenChange = (newOpen: boolean) => {
|
||||
setOpen(newOpen);
|
||||
|
||||
if (!newOpen) {
|
||||
setEmail('');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<Alert
|
||||
className="flex flex-col items-center justify-between gap-4 p-6 md:flex-row"
|
||||
variant="neutral"
|
||||
>
|
||||
<div>
|
||||
<AlertTitle>Reset Two Factor Authentication</AlertTitle>
|
||||
<AlertDescription className="mr-2">
|
||||
<Trans>
|
||||
Reset the users two factor authentication. This action is irreversible and will
|
||||
disable two factor authentication for the user.
|
||||
</Trans>
|
||||
</AlertDescription>
|
||||
</div>
|
||||
|
||||
<div className="flex-shrink-0">
|
||||
<Dialog open={open} onOpenChange={handleOpenChange}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="destructive">
|
||||
<Trans>Reset 2FA</Trans>
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
|
||||
<DialogContent>
|
||||
<DialogHeader className="space-y-4">
|
||||
<DialogTitle>
|
||||
<Trans>Reset Two Factor Authentication</Trans>
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<Alert variant="destructive">
|
||||
<AlertDescription className="selection:bg-red-100">
|
||||
<Trans>
|
||||
This action is irreversible. Please ensure you have informed the user before
|
||||
proceeding.
|
||||
</Trans>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<div>
|
||||
<DialogDescription>
|
||||
<Trans>
|
||||
To confirm, please enter the accounts email address <br />({user.email}).
|
||||
</Trans>
|
||||
</DialogDescription>
|
||||
|
||||
<Input
|
||||
className="mt-2"
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="destructive"
|
||||
disabled={email !== user.email}
|
||||
onClick={onResetTwoFactor}
|
||||
loading={isResettingTwoFactor}
|
||||
>
|
||||
<Trans>Reset 2FA</Trans>
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</Alert>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -49,7 +49,7 @@ export const DocumentDeleteDialog = ({
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
const [isDeleteEnabled, setIsDeleteEnabled] = useState(status === DocumentStatus.DRAFT);
|
||||
|
||||
const { mutateAsync: deleteDocument, isPending } = trpcReact.document.delete.useMutation({
|
||||
const { mutateAsync: deleteDocument, isPending } = trpcReact.document.deleteDocument.useMutation({
|
||||
onSuccess: async () => {
|
||||
void refreshLimits();
|
||||
|
||||
|
||||
@@ -36,12 +36,11 @@ export const DocumentDuplicateDialog = ({
|
||||
|
||||
const team = useCurrentTeam();
|
||||
|
||||
const { data: document, isLoading } = trpcReact.document.get.useQuery(
|
||||
const { data: document, isLoading } = trpcReact.document.getDocumentById.useQuery(
|
||||
{
|
||||
documentId: id,
|
||||
},
|
||||
{
|
||||
queryHash: `document-duplicate-dialog-${id}`,
|
||||
enabled: open === true,
|
||||
},
|
||||
);
|
||||
@@ -56,7 +55,7 @@ export const DocumentDuplicateDialog = ({
|
||||
const documentsPath = formatDocumentsPath(team.url);
|
||||
|
||||
const { mutateAsync: duplicateDocument, isPending: isDuplicateLoading } =
|
||||
trpcReact.document.duplicate.useMutation({
|
||||
trpcReact.document.duplicateDocument.useMutation({
|
||||
onSuccess: async ({ documentId }) => {
|
||||
toast({
|
||||
title: _(msg`Document Duplicated`),
|
||||
|
||||
@@ -71,7 +71,7 @@ export const DocumentResendDialog = ({ document, recipients }: DocumentResendDia
|
||||
document.status !== 'PENDING' ||
|
||||
!recipients.some((r) => r.signingStatus === SigningStatus.NOT_SIGNED);
|
||||
|
||||
const { mutateAsync: resendDocument } = trpcReact.document.redistribute.useMutation();
|
||||
const { mutateAsync: resendDocument } = trpcReact.document.resendDocument.useMutation();
|
||||
|
||||
const form = useForm<TResendDocumentFormSchema>({
|
||||
resolver: zodResolver(ZResendDocumentFormSchema),
|
||||
|
||||
@@ -65,9 +65,9 @@ export const PasskeyCreateDialog = ({ trigger, onSuccess, ...props }: PasskeyCre
|
||||
});
|
||||
|
||||
const { mutateAsync: createPasskeyRegistrationOptions, isPending } =
|
||||
trpc.auth.passkey.createRegistrationOptions.useMutation();
|
||||
trpc.auth.createPasskeyRegistrationOptions.useMutation();
|
||||
|
||||
const { mutateAsync: createPasskey } = trpc.auth.passkey.create.useMutation();
|
||||
const { mutateAsync: createPasskey } = trpc.auth.createPasskey.useMutation();
|
||||
|
||||
const onFormSubmit = async ({ passkeyName }: TCreatePasskeyFormSchema) => {
|
||||
setFormError(null);
|
||||
|
||||
@@ -56,7 +56,7 @@ export default function TokenDeleteDialog({ token, onDelete, children }: TokenDe
|
||||
|
||||
type TDeleteTokenByIdMutationSchema = z.infer<typeof ZTokenDeleteDialogSchema>;
|
||||
|
||||
const { mutateAsync: deleteTokenMutation } = trpc.apiToken.delete.useMutation({
|
||||
const { mutateAsync: deleteTokenMutation } = trpc.apiToken.deleteTokenById.useMutation({
|
||||
onSuccess() {
|
||||
onDelete?.();
|
||||
},
|
||||
|
||||
@@ -114,7 +114,7 @@ export const SignInForm = ({
|
||||
}, [returnTo]);
|
||||
|
||||
const { mutateAsync: createPasskeySigninOptions } =
|
||||
trpc.auth.passkey.createSigninOptions.useMutation();
|
||||
trpc.auth.createPasskeySigninOptions.useMutation();
|
||||
|
||||
const form = useForm<TSignInFormSchema>({
|
||||
values: {
|
||||
|
||||
@@ -1,138 +0,0 @@
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { Trans, useLingui } from '@lingui/react/macro';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@documenso/ui/primitives/form/form';
|
||||
import { Input } from '@documenso/ui/primitives/input';
|
||||
import { Textarea } from '@documenso/ui/primitives/textarea';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
const ZSupportTicketSchema = z.object({
|
||||
subject: z.string().min(3, 'Subject is required'),
|
||||
message: z.string().min(10, 'Message must be at least 10 characters'),
|
||||
});
|
||||
|
||||
type TSupportTicket = z.infer<typeof ZSupportTicketSchema>;
|
||||
|
||||
export type SupportTicketFormProps = {
|
||||
organisationId: string;
|
||||
teamId?: string | null;
|
||||
onSuccess?: () => void;
|
||||
onClose?: () => void;
|
||||
};
|
||||
|
||||
export const SupportTicketForm = ({
|
||||
organisationId,
|
||||
teamId,
|
||||
onSuccess,
|
||||
onClose,
|
||||
}: SupportTicketFormProps) => {
|
||||
const { t } = useLingui();
|
||||
const { toast } = useToast();
|
||||
|
||||
const { mutateAsync: submitSupportTicket, isPending } =
|
||||
trpc.profile.submitSupportTicket.useMutation();
|
||||
|
||||
const form = useForm<TSupportTicket>({
|
||||
resolver: zodResolver(ZSupportTicketSchema),
|
||||
defaultValues: {
|
||||
subject: '',
|
||||
message: '',
|
||||
},
|
||||
});
|
||||
|
||||
const isLoading = form.formState.isLoading || isPending;
|
||||
|
||||
const onSubmit = async (data: TSupportTicket) => {
|
||||
const { subject, message } = data;
|
||||
|
||||
try {
|
||||
await submitSupportTicket({
|
||||
subject,
|
||||
message,
|
||||
organisationId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
toast({
|
||||
title: t`Support ticket created`,
|
||||
description: t`Your support request has been submitted. We'll get back to you soon!`,
|
||||
});
|
||||
|
||||
if (onSuccess) {
|
||||
onSuccess();
|
||||
}
|
||||
|
||||
form.reset();
|
||||
} catch (err) {
|
||||
toast({
|
||||
title: t`Failed to create support ticket`,
|
||||
description: t`An error occurred. Please try again later.`,
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)}>
|
||||
<fieldset disabled={isLoading} className="flex flex-col gap-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="subject"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel required>
|
||||
<Trans>Subject</Trans>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="message"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel required>
|
||||
<Trans>Message</Trans>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea rows={5} {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="mt-2 flex flex-row gap-2">
|
||||
<Button type="submit" size="sm" loading={isLoading}>
|
||||
<Trans>Submit</Trans>
|
||||
</Button>
|
||||
{onClose && (
|
||||
<Button variant="outline" size="sm" type="button" onClick={onClose}>
|
||||
<Trans>Close</Trans>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
</Form>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -13,7 +13,7 @@ import type { z } from 'zod';
|
||||
import { useCopyToClipboard } from '@documenso/lib/client-only/hooks/use-copy-to-clipboard';
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { ZCreateApiTokenRequestSchema } from '@documenso/trpc/server/api-token-router/create-api-token.types';
|
||||
import { ZCreateTokenMutationSchema } from '@documenso/trpc/server/api-token-router/schema';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
||||
@@ -47,7 +47,7 @@ export const EXPIRATION_DATES = {
|
||||
ONE_YEAR: msg`12 months`,
|
||||
} as const;
|
||||
|
||||
const ZCreateTokenFormSchema = ZCreateApiTokenRequestSchema.pick({
|
||||
const ZCreateTokenFormSchema = ZCreateTokenMutationSchema.pick({
|
||||
tokenName: true,
|
||||
expirationDate: true,
|
||||
});
|
||||
@@ -75,7 +75,7 @@ export const ApiTokenForm = ({ className, tokens }: ApiTokenFormProps) => {
|
||||
const [newlyCreatedToken, setNewlyCreatedToken] = useState<NewlyCreatedToken | null>();
|
||||
const [noExpirationDate, setNoExpirationDate] = useState(false);
|
||||
|
||||
const { mutateAsync: createTokenMutation } = trpc.apiToken.create.useMutation({
|
||||
const { mutateAsync: createTokenMutation } = trpc.apiToken.createToken.useMutation({
|
||||
onSuccess(data) {
|
||||
setNewlyCreatedToken(data);
|
||||
},
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
'use client';
|
||||
|
||||
import { DateTime } from 'luxon';
|
||||
import type { TooltipProps } from 'recharts';
|
||||
import { Bar, BarChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts';
|
||||
|
||||
@@ -64,7 +64,7 @@ export function AppCommandMenu({ open, onOpenChange }: AppCommandMenuProps) {
|
||||
const [pages, setPages] = useState<string[]>([]);
|
||||
|
||||
const { data: searchDocumentsData, isPending: isSearchingDocuments } =
|
||||
trpcReact.document.search.useQuery(
|
||||
trpcReact.document.searchDocuments.useQuery(
|
||||
{
|
||||
query: search,
|
||||
},
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useEffect, useRef, 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 { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import { DialogFooter } from '@documenso/ui/primitives/dialog';
|
||||
import {
|
||||
@@ -20,6 +20,8 @@ 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';
|
||||
|
||||
@@ -51,6 +53,7 @@ export const DocumentSigningAuth2FA = ({
|
||||
}: DocumentSigningAuth2FAProps) => {
|
||||
const { recipient, user, isCurrentlyAuthenticating, setIsCurrentlyAuthenticating } =
|
||||
useRequiredDocumentSigningAuthContext();
|
||||
const { toast } = useToast();
|
||||
|
||||
const form = useForm<T2FAAuthFormSchema>({
|
||||
resolver: zodResolver(Z2FAAuthFormSchema),
|
||||
@@ -60,27 +63,104 @@ export const DocumentSigningAuth2FA = ({
|
||||
});
|
||||
|
||||
const [is2FASetupSuccessful, setIs2FASetupSuccessful] = useState(false);
|
||||
const [formErrorCode, setFormErrorCode] = useState<string | null>(null);
|
||||
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 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);
|
||||
|
||||
const error = AppError.parseError(err);
|
||||
setFormErrorCode(error.code);
|
||||
|
||||
// Todo: Alert.
|
||||
toast({
|
||||
title: 'Unauthorized',
|
||||
description: 'We were unable to verify your details.',
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -90,21 +170,46 @@ export const DocumentSigningAuth2FA = ({
|
||||
});
|
||||
|
||||
setIs2FASetupSuccessful(false);
|
||||
setFormErrorCode(null);
|
||||
setIsEmailCodeSent(false);
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [open]);
|
||||
if (open && !user?.twoFactorEnabled) {
|
||||
setVerificationMethod('email');
|
||||
}
|
||||
}, [open, user?.twoFactorEnabled, form]);
|
||||
|
||||
if (!user?.twoFactorEnabled && !is2FASetupSuccessful) {
|
||||
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) {
|
||||
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>
|
||||
@@ -129,59 +234,106 @@ export const DocumentSigningAuth2FA = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<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>
|
||||
<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>
|
||||
)}
|
||||
|
||||
<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>
|
||||
)}
|
||||
/>
|
||||
|
||||
{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>
|
||||
{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>
|
||||
)}
|
||||
|
||||
<DialogFooter>
|
||||
<Button type="button" variant="secondary" onClick={() => onOpenChange(false)}>
|
||||
<Trans>Cancel</Trans>
|
||||
</Button>
|
||||
<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>
|
||||
|
||||
<Button type="submit" loading={isCurrentlyAuthenticating}>
|
||||
<Trans>Sign</Trans>
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
</Form>
|
||||
<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>
|
||||
)}
|
||||
|
||||
<DialogFooter>
|
||||
<Button type="button" variant="secondary" onClick={() => onOpenChange(false)}>
|
||||
<Trans>Cancel</Trans>
|
||||
</Button>
|
||||
|
||||
<Button type="submit" loading={isCurrentlyAuthenticating}>
|
||||
<Trans>{actionTarget === 'DOCUMENT' ? 'Sign Document' : 'Sign Field'}</Trans>
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
+22
-3
@@ -43,6 +43,7 @@ export const DocumentSigningAuthDialog = ({
|
||||
title,
|
||||
description,
|
||||
availableAuthTypes,
|
||||
actionTarget,
|
||||
open,
|
||||
onOpenChange,
|
||||
onReauthFormSubmit,
|
||||
@@ -107,15 +108,32 @@ export const DocumentSigningAuthDialog = ({
|
||||
>
|
||||
<ChevronLeftIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
<span>{title || <Trans>Sign field</Trans>}</span>
|
||||
<span>
|
||||
{title ||
|
||||
(actionTarget === 'DOCUMENT' ? (
|
||||
<Trans>Sign document</Trans>
|
||||
) : (
|
||||
<Trans>Sign field</Trans>
|
||||
))}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{(!selectedAuthType || validAuthTypes.length === 1) &&
|
||||
(title || <Trans>Sign field</Trans>)}
|
||||
(title ||
|
||||
(actionTarget === 'DOCUMENT' ? (
|
||||
<Trans>Sign document</Trans>
|
||||
) : (
|
||||
<Trans>Sign field</Trans>
|
||||
)))}
|
||||
</DialogTitle>
|
||||
|
||||
<DialogDescription>
|
||||
{description || <Trans>Reauthentication is required to sign this field</Trans>}
|
||||
{description || (
|
||||
<Trans>
|
||||
Reauthentication is required to sign this{' '}
|
||||
{actionTarget === 'DOCUMENT' ? 'document' : 'field'}
|
||||
</Trans>
|
||||
)}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
@@ -180,6 +198,7 @@ export const DocumentSigningAuthDialog = ({
|
||||
))
|
||||
.with({ documentAuthType: DocumentAuth.TWO_FACTOR_AUTH }, () => (
|
||||
<DocumentSigningAuth2FA
|
||||
actionTarget={actionTarget === 'DOCUMENT' ? 'DOCUMENT' : 'FIELD'}
|
||||
open={open}
|
||||
onOpenChange={onOpenChange}
|
||||
onReauthFormSubmit={onReauthFormSubmit}
|
||||
|
||||
+1
-1
@@ -77,7 +77,7 @@ export const DocumentSigningAuthPasskey = ({
|
||||
});
|
||||
|
||||
const { mutateAsync: createPasskeyAuthenticationOptions } =
|
||||
trpc.auth.passkey.createAuthenticationOptions.useMutation();
|
||||
trpc.auth.createPasskeyAuthenticationOptions.useMutation();
|
||||
|
||||
const [formErrorCode, setFormErrorCode] = useState<string | null>(null);
|
||||
|
||||
|
||||
+15
-4
@@ -42,6 +42,7 @@ export type DocumentSigningAuthContextValue = {
|
||||
setPreferredPasskeyId: (_value: string | null) => void;
|
||||
user?: SessionUser | null;
|
||||
refetchPasskeys: () => Promise<void>;
|
||||
isEnterprise: boolean;
|
||||
};
|
||||
|
||||
const DocumentSigningAuthContext = createContext<DocumentSigningAuthContextValue | null>(null);
|
||||
@@ -65,6 +66,7 @@ export interface DocumentSigningAuthProviderProps {
|
||||
recipient: Recipient;
|
||||
user?: SessionUser | null;
|
||||
children: React.ReactNode;
|
||||
isEnterprise: boolean;
|
||||
}
|
||||
|
||||
export const DocumentSigningAuthProvider = ({
|
||||
@@ -72,6 +74,7 @@ export const DocumentSigningAuthProvider = ({
|
||||
recipient: initialRecipient,
|
||||
user,
|
||||
children,
|
||||
isEnterprise,
|
||||
}: DocumentSigningAuthProviderProps) => {
|
||||
const [documentAuthOptions, setDocumentAuthOptions] = useState(initialDocumentAuthOptions);
|
||||
const [recipient, setRecipient] = useState(initialRecipient);
|
||||
@@ -93,7 +96,7 @@ export const DocumentSigningAuthProvider = ({
|
||||
[documentAuthOptions, recipient],
|
||||
);
|
||||
|
||||
const passkeyQuery = trpc.auth.passkey.find.useQuery(
|
||||
const passkeyQuery = trpc.auth.findPasskeys.useQuery(
|
||||
{
|
||||
perPage: MAXIMUM_PASSKEYS,
|
||||
},
|
||||
@@ -144,8 +147,13 @@ export const DocumentSigningAuthProvider = ({
|
||||
}, [derivedRecipientActionAuth, user, recipient]);
|
||||
|
||||
const executeActionAuthProcedure = async (options: ExecuteActionAuthProcedureOptions) => {
|
||||
// Directly run callback if no auth required.
|
||||
if (!derivedRecipientActionAuth || options.actionTarget !== FieldType.SIGNATURE) {
|
||||
// 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) {
|
||||
await options.onReauthFormSubmit();
|
||||
return;
|
||||
}
|
||||
@@ -205,6 +213,7 @@ export const DocumentSigningAuthProvider = ({
|
||||
preferredPasskeyId,
|
||||
setPreferredPasskeyId,
|
||||
refetchPasskeys,
|
||||
isEnterprise,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
@@ -225,6 +234,8 @@ export const DocumentSigningAuthProvider = ({
|
||||
type ExecuteActionAuthProcedureOptions = Omit<
|
||||
DocumentSigningAuthDialogProps,
|
||||
'open' | 'onOpenChange' | 'documentAuthType' | 'recipientRole' | 'availableAuthTypes'
|
||||
>;
|
||||
> & {
|
||||
actionTarget: FieldType | 'DOCUMENT';
|
||||
};
|
||||
|
||||
DocumentSigningAuthProvider.displayName = 'DocumentSigningAuthProvider';
|
||||
|
||||
@@ -27,6 +27,7 @@ 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';
|
||||
|
||||
@@ -38,6 +39,7 @@ export type DocumentSigningFormProps = {
|
||||
isRecipientsTurn: boolean;
|
||||
allRecipients?: RecipientWithFields[];
|
||||
setSelectedSignerId?: (id: number | null) => void;
|
||||
isEnterprise: boolean;
|
||||
};
|
||||
|
||||
export const DocumentSigningForm = ({
|
||||
@@ -48,6 +50,7 @@ export const DocumentSigningForm = ({
|
||||
isRecipientsTurn,
|
||||
allRecipients = [],
|
||||
setSelectedSignerId,
|
||||
isEnterprise,
|
||||
}: DocumentSigningFormProps) => {
|
||||
const { sessionData } = useOptionalSession();
|
||||
const user = sessionData?.user;
|
||||
@@ -61,6 +64,7 @@ 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);
|
||||
@@ -113,11 +117,16 @@ export const DocumentSigningForm = ({
|
||||
setIsAssistantSubmitting(true);
|
||||
|
||||
try {
|
||||
await completeDocument(undefined, nextSigner);
|
||||
await executeActionAuthProcedure({
|
||||
actionTarget: 'DOCUMENT',
|
||||
onReauthFormSubmit: async (authOptions) => {
|
||||
await completeDocument(authOptions, nextSigner);
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: 'An error occurred while completing the document. Please try again.',
|
||||
title: _(msg`Error`),
|
||||
description: _(msg`An error occurred while completing the document. Please try again.`),
|
||||
variant: 'destructive',
|
||||
});
|
||||
|
||||
@@ -207,7 +216,12 @@ export const DocumentSigningForm = ({
|
||||
fields={fields}
|
||||
fieldsValidated={fieldsValidated}
|
||||
onSignatureComplete={async (nextSigner) => {
|
||||
await completeDocument(undefined, nextSigner);
|
||||
await executeActionAuthProcedure({
|
||||
actionTarget: 'DOCUMENT',
|
||||
onReauthFormSubmit: async (authOptions) => {
|
||||
await completeDocument(authOptions, nextSigner);
|
||||
},
|
||||
});
|
||||
}}
|
||||
role={recipient.role}
|
||||
allowDictateNextSigner={document.documentMeta?.allowDictateNextSigner}
|
||||
@@ -367,7 +381,12 @@ export const DocumentSigningForm = ({
|
||||
fieldsValidated={fieldsValidated}
|
||||
disabled={!isRecipientsTurn}
|
||||
onSignatureComplete={async (nextSigner) => {
|
||||
await completeDocument(undefined, nextSigner);
|
||||
await executeActionAuthProcedure({
|
||||
actionTarget: 'DOCUMENT',
|
||||
onReauthFormSubmit: async (authOptions) => {
|
||||
await completeDocument(authOptions, nextSigner);
|
||||
},
|
||||
});
|
||||
}}
|
||||
role={recipient.role}
|
||||
allowDictateNextSigner={
|
||||
|
||||
@@ -50,6 +50,7 @@ export type DocumentSigningPageViewProps = {
|
||||
isRecipientsTurn: boolean;
|
||||
allRecipients?: RecipientWithFields[];
|
||||
includeSenderDetails: boolean;
|
||||
isEnterprise: boolean;
|
||||
};
|
||||
|
||||
export const DocumentSigningPageView = ({
|
||||
@@ -60,6 +61,7 @@ export const DocumentSigningPageView = ({
|
||||
isRecipientsTurn,
|
||||
allRecipients = [],
|
||||
includeSenderDetails,
|
||||
isEnterprise,
|
||||
}: DocumentSigningPageViewProps) => {
|
||||
const { documentData, documentMeta } = document;
|
||||
|
||||
@@ -208,6 +210,7 @@ export const DocumentSigningPageView = ({
|
||||
isRecipientsTurn={isRecipientsTurn}
|
||||
allRecipients={allRecipients}
|
||||
setSelectedSignerId={setSelectedSignerId}
|
||||
isEnterprise={isEnterprise}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -21,7 +21,7 @@ export const DocumentAuditLogDownloadButton = ({
|
||||
const { _ } = useLingui();
|
||||
|
||||
const { mutateAsync: downloadAuditLogs, isPending } =
|
||||
trpc.document.auditLog.download.useMutation();
|
||||
trpc.document.downloadAuditLogs.useMutation();
|
||||
|
||||
const onDownloadAuditLogsClick = async () => {
|
||||
try {
|
||||
|
||||
@@ -49,7 +49,7 @@ export const DocumentDropZoneWrapper = ({ children, className }: DocumentDropZon
|
||||
|
||||
const { quota, remaining, refreshLimits } = useLimits();
|
||||
|
||||
const { mutateAsync: createDocument } = trpc.document.create.useMutation();
|
||||
const { mutateAsync: createDocument } = trpc.document.createDocument.useMutation();
|
||||
|
||||
const isUploadDisabled = remaining.documents === 0 || !user.emailVerified;
|
||||
|
||||
|
||||
@@ -59,22 +59,23 @@ export const DocumentEditForm = ({
|
||||
|
||||
const utils = trpc.useUtils();
|
||||
|
||||
const { data: document, refetch: refetchDocument } = trpc.document.get.useQuery(
|
||||
{
|
||||
documentId: initialDocument.id,
|
||||
},
|
||||
{
|
||||
initialData: initialDocument,
|
||||
...SKIP_QUERY_BATCH_META,
|
||||
},
|
||||
);
|
||||
const { data: document, refetch: refetchDocument } =
|
||||
trpc.document.getDocumentWithDetailsById.useQuery(
|
||||
{
|
||||
documentId: initialDocument.id,
|
||||
},
|
||||
{
|
||||
initialData: initialDocument,
|
||||
...SKIP_QUERY_BATCH_META,
|
||||
},
|
||||
);
|
||||
|
||||
const { recipients, fields } = document;
|
||||
|
||||
const { mutateAsync: updateDocument } = trpc.document.update.useMutation({
|
||||
const { mutateAsync: updateDocument } = trpc.document.updateDocument.useMutation({
|
||||
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
|
||||
onSuccess: (newData) => {
|
||||
utils.document.get.setData(
|
||||
utils.document.getDocumentWithDetailsById.setData(
|
||||
{
|
||||
documentId: initialDocument.id,
|
||||
},
|
||||
@@ -83,10 +84,23 @@ export const DocumentEditForm = ({
|
||||
},
|
||||
});
|
||||
|
||||
const { mutateAsync: setSigningOrderForDocument } =
|
||||
trpc.document.setSigningOrderForDocument.useMutation({
|
||||
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
|
||||
onSuccess: (newData) => {
|
||||
utils.document.getDocumentWithDetailsById.setData(
|
||||
{
|
||||
documentId: initialDocument.id,
|
||||
},
|
||||
(oldData) => ({ ...(oldData || initialDocument), ...newData, id: Number(newData.id) }),
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
const { mutateAsync: addFields } = trpc.field.addFields.useMutation({
|
||||
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
|
||||
onSuccess: ({ fields: newFields }) => {
|
||||
utils.document.get.setData(
|
||||
utils.document.getDocumentWithDetailsById.setData(
|
||||
{
|
||||
documentId: initialDocument.id,
|
||||
},
|
||||
@@ -98,7 +112,7 @@ export const DocumentEditForm = ({
|
||||
const { mutateAsync: setRecipients } = trpc.recipient.setDocumentRecipients.useMutation({
|
||||
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
|
||||
onSuccess: ({ recipients: newRecipients }) => {
|
||||
utils.document.get.setData(
|
||||
utils.document.getDocumentWithDetailsById.setData(
|
||||
{
|
||||
documentId: initialDocument.id,
|
||||
},
|
||||
@@ -107,10 +121,10 @@ export const DocumentEditForm = ({
|
||||
},
|
||||
});
|
||||
|
||||
const { mutateAsync: sendDocument } = trpc.document.distribute.useMutation({
|
||||
const { mutateAsync: sendDocument } = trpc.document.sendDocument.useMutation({
|
||||
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
|
||||
onSuccess: (newData) => {
|
||||
utils.document.get.setData(
|
||||
utils.document.getDocumentWithDetailsById.setData(
|
||||
{
|
||||
documentId: initialDocument.id,
|
||||
},
|
||||
@@ -202,11 +216,15 @@ export const DocumentEditForm = ({
|
||||
const onAddSignersFormSubmit = async (data: TAddSignersFormSchema) => {
|
||||
try {
|
||||
await Promise.all([
|
||||
setSigningOrderForDocument({
|
||||
documentId: document.id,
|
||||
signingOrder: data.signingOrder,
|
||||
}),
|
||||
|
||||
updateDocument({
|
||||
documentId: document.id,
|
||||
meta: {
|
||||
allowDictateNextSigner: data.allowDictateNextSigner,
|
||||
signingOrder: data.signingOrder,
|
||||
},
|
||||
}),
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ export const DocumentPageViewButton = ({ document }: DocumentPageViewButtonProps
|
||||
|
||||
const onDownloadClick = async () => {
|
||||
try {
|
||||
const documentWithData = await trpcClient.document.get.query(
|
||||
const documentWithData = await trpcClient.document.getDocumentById.query(
|
||||
{
|
||||
documentId: document.id,
|
||||
},
|
||||
|
||||
@@ -71,7 +71,7 @@ export const DocumentPageViewDropdown = ({ document }: DocumentPageViewDropdownP
|
||||
|
||||
const onDownloadClick = async () => {
|
||||
try {
|
||||
const documentWithData = await trpcClient.document.get.query(
|
||||
const documentWithData = await trpcClient.document.getDocumentById.query(
|
||||
{
|
||||
documentId: document.id,
|
||||
},
|
||||
@@ -100,7 +100,7 @@ export const DocumentPageViewDropdown = ({ document }: DocumentPageViewDropdownP
|
||||
|
||||
const onDownloadOriginalClick = async () => {
|
||||
try {
|
||||
const documentWithData = await trpcClient.document.get.query(
|
||||
const documentWithData = await trpcClient.document.getDocumentById.query(
|
||||
{
|
||||
documentId: document.id,
|
||||
},
|
||||
|
||||
@@ -32,7 +32,7 @@ export const DocumentPageViewRecentActivity = ({
|
||||
hasNextPage,
|
||||
fetchNextPage,
|
||||
isFetchingNextPage,
|
||||
} = trpc.document.auditLog.find.useInfiniteQuery(
|
||||
} = trpc.document.findDocumentAuditLogs.useInfiniteQuery(
|
||||
{
|
||||
documentId,
|
||||
filterForRecentActivity: true,
|
||||
|
||||
@@ -52,7 +52,7 @@ export const DocumentUploadDropzone = ({ className }: DocumentUploadDropzoneProp
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const { mutateAsync: createDocument } = trpc.document.create.useMutation();
|
||||
const { mutateAsync: createDocument } = trpc.document.createDocument.useMutation();
|
||||
|
||||
const disabledMessage = useMemo(() => {
|
||||
if (organisation.subscription && remaining.documents === 0) {
|
||||
|
||||
@@ -28,7 +28,7 @@ export const LegacyFieldWarningPopover = ({
|
||||
const { mutateAsync: updateTemplate, isPending: isUpdatingTemplate } =
|
||||
trpc.template.updateTemplate.useMutation();
|
||||
const { mutateAsync: updateDocument, isPending: isUpdatingDocument } =
|
||||
trpc.document.update.useMutation();
|
||||
trpc.document.updateDocument.useMutation();
|
||||
|
||||
const onUpdateFieldsClick = async () => {
|
||||
if (type === 'document') {
|
||||
|
||||
@@ -321,19 +321,6 @@ export const OrgMenuSwitcher = () => {
|
||||
<Trans>Language</Trans>
|
||||
</DropdownMenuItem>
|
||||
|
||||
{currentOrganisation && (
|
||||
<DropdownMenuItem className="text-muted-foreground px-4 py-2" asChild>
|
||||
<Link
|
||||
to={{
|
||||
pathname: `/o/${currentOrganisation.url}/support`,
|
||||
search: currentTeam ? `?team=${currentTeam.id}` : '',
|
||||
}}
|
||||
>
|
||||
<Trans>Support</Trans>
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
|
||||
<DropdownMenuItem
|
||||
className="text-muted-foreground hover:!text-muted-foreground px-4 py-2"
|
||||
onSelect={async () => authClient.signOut()}
|
||||
|
||||
@@ -157,7 +157,7 @@ export const TemplateEditForm = ({
|
||||
|
||||
toast({
|
||||
title: _(msg`Error`),
|
||||
description: _(msg`An error occurred while updating the document settings.`),
|
||||
description: _(msg`An error occurred while updating the template settings.`),
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ export const TemplatePageViewDocumentsTable = ({
|
||||
Object.fromEntries(searchParams ?? []),
|
||||
);
|
||||
|
||||
const { data, isLoading, isLoadingError } = trpc.document.find.useQuery(
|
||||
const { data, isLoading, isLoadingError } = trpc.document.findDocuments.useQuery(
|
||||
{
|
||||
templateId,
|
||||
page: parsedSearchParams.page,
|
||||
|
||||
@@ -18,7 +18,7 @@ export const TemplatePageViewRecentActivity = ({
|
||||
templateId,
|
||||
documentRootPath,
|
||||
}: TemplatePageViewRecentActivityProps) => {
|
||||
const { data, isLoading, isLoadingError, refetch } = trpc.document.find.useQuery({
|
||||
const { data, isLoading, isLoadingError, refetch } = trpc.document.findDocuments.useQuery({
|
||||
templateId,
|
||||
orderByColumn: 'createdAt',
|
||||
orderByDirection: 'asc',
|
||||
|
||||
@@ -52,7 +52,7 @@ export const AdminDocumentRecipientItemTable = ({ recipient }: RecipientItemProp
|
||||
},
|
||||
});
|
||||
|
||||
const { mutateAsync: updateRecipient } = trpc.admin.recipient.update.useMutation();
|
||||
const { mutateAsync: updateRecipient } = trpc.admin.updateRecipient.useMutation();
|
||||
|
||||
const columns = useMemo(() => {
|
||||
return [
|
||||
|
||||
@@ -34,7 +34,7 @@ export const DocumentLogsTable = ({ documentId }: DocumentLogsTableProps) => {
|
||||
|
||||
const parsedSearchParams = ZUrlSearchParamsSchema.parse(Object.fromEntries(searchParams ?? []));
|
||||
|
||||
const { data, isLoading, isLoadingError } = trpc.document.auditLog.find.useQuery(
|
||||
const { data, isLoading, isLoadingError } = trpc.document.findDocumentAuditLogs.useQuery(
|
||||
{
|
||||
documentId,
|
||||
page: parsedSearchParams.page,
|
||||
|
||||
@@ -45,7 +45,7 @@ export const DocumentsTableActionButton = ({ row }: DocumentsTableActionButtonPr
|
||||
const onDownloadClick = async () => {
|
||||
try {
|
||||
const document = !recipient
|
||||
? await trpcClient.document.get.query(
|
||||
? await trpcClient.document.getDocumentById.query(
|
||||
{
|
||||
documentId: row.id,
|
||||
},
|
||||
|
||||
@@ -77,7 +77,7 @@ export const DocumentsTableActionDropdown = ({
|
||||
const onDownloadClick = async () => {
|
||||
try {
|
||||
const document = !recipient
|
||||
? await trpcClient.document.get.query({
|
||||
? await trpcClient.document.getDocumentById.query({
|
||||
documentId: row.id,
|
||||
})
|
||||
: await trpcClient.document.getDocumentByToken.query({
|
||||
@@ -103,7 +103,7 @@ export const DocumentsTableActionDropdown = ({
|
||||
const onDownloadOriginalClick = async () => {
|
||||
try {
|
||||
const document = !recipient
|
||||
? await trpcClient.document.get.query({
|
||||
? await trpcClient.document.getDocumentById.query({
|
||||
documentId: row.id,
|
||||
})
|
||||
: await trpcClient.document.getDocumentByToken.query({
|
||||
|
||||
@@ -11,7 +11,7 @@ import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-upda
|
||||
import { useSession } from '@documenso/lib/client-only/providers/session';
|
||||
import { isDocumentCompleted } from '@documenso/lib/utils/document';
|
||||
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
|
||||
import type { TFindDocumentsResponse } from '@documenso/trpc/server/document-router/find-documents.types';
|
||||
import type { TFindDocumentsResponse } from '@documenso/trpc/server/document-router/schema';
|
||||
import type { DataTableColumnDef } from '@documenso/ui/primitives/data-table';
|
||||
import { DataTable } from '@documenso/ui/primitives/data-table';
|
||||
import { DataTablePagination } from '@documenso/ui/primitives/data-table-pagination';
|
||||
|
||||
@@ -17,6 +17,7 @@ import { isDocumentCompleted } from '@documenso/lib/utils/document';
|
||||
import { trpc as trpcClient } from '@documenso/trpc/client';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import type { TFindInboxResponse } from '@documenso/trpc/server/document-router/find-inbox.types';
|
||||
import type { TFindDocumentsResponse } from '@documenso/trpc/server/document-router/schema';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import type { DataTableColumnDef } from '@documenso/ui/primitives/data-table';
|
||||
import { DataTable } from '@documenso/ui/primitives/data-table';
|
||||
@@ -31,12 +32,12 @@ import { useOptionalCurrentTeam } from '~/providers/team';
|
||||
import { StackAvatarsWithTooltip } from '../general/stack-avatars-with-tooltip';
|
||||
|
||||
export type DocumentsTableProps = {
|
||||
data?: TFindInboxResponse;
|
||||
data?: TFindDocumentsResponse;
|
||||
isLoading?: boolean;
|
||||
isLoadingError?: boolean;
|
||||
};
|
||||
|
||||
type DocumentsTableRow = TFindInboxResponse['data'][number];
|
||||
type DocumentsTableRow = TFindDocumentsResponse['data'][number];
|
||||
|
||||
export const InboxTable = () => {
|
||||
const { _, i18n } = useLingui();
|
||||
|
||||
@@ -62,7 +62,7 @@ export const SettingsSecurityPasskeyTableActions = ({
|
||||
});
|
||||
|
||||
const { mutateAsync: updatePasskey, isPending: isUpdatingPasskey } =
|
||||
trpc.auth.passkey.update.useMutation({
|
||||
trpc.auth.updatePasskey.useMutation({
|
||||
onSuccess: () => {
|
||||
toast({
|
||||
title: _(msg`Success`),
|
||||
@@ -84,7 +84,7 @@ export const SettingsSecurityPasskeyTableActions = ({
|
||||
});
|
||||
|
||||
const { mutateAsync: deletePasskey, isPending: isDeletingPasskey } =
|
||||
trpc.auth.passkey.delete.useMutation({
|
||||
trpc.auth.deletePasskey.useMutation({
|
||||
onSuccess: () => {
|
||||
toast({
|
||||
title: _(msg`Success`),
|
||||
|
||||
@@ -26,7 +26,7 @@ export const SettingsSecurityPasskeyTable = () => {
|
||||
|
||||
const parsedSearchParams = ZUrlSearchParamsSchema.parse(Object.fromEntries(searchParams ?? []));
|
||||
|
||||
const { data, isLoading, isLoadingError } = trpc.auth.passkey.find.useQuery(
|
||||
const { data, isLoading, isLoadingError } = trpc.auth.findPasskeys.useQuery(
|
||||
{
|
||||
page: parsedSearchParams.page,
|
||||
perPage: parsedSearchParams.perPage,
|
||||
|
||||
@@ -48,7 +48,7 @@ export default function AdminDocumentDetailsPage({ loaderData }: Route.Component
|
||||
const { toast } = useToast();
|
||||
|
||||
const { mutate: resealDocument, isPending: isResealDocumentLoading } =
|
||||
trpc.admin.document.reseal.useMutation({
|
||||
trpc.admin.resealDocument.useMutation({
|
||||
onSuccess: () => {
|
||||
toast({
|
||||
title: _(msg`Success`),
|
||||
|
||||
@@ -33,7 +33,7 @@ export default function AdminDocumentsPage() {
|
||||
const perPage = searchParams?.get?.('perPage') ? Number(searchParams.get('perPage')) : undefined;
|
||||
|
||||
const { data: findDocumentsData, isPending: isFindDocumentsLoading } =
|
||||
trpc.admin.document.find.useQuery(
|
||||
trpc.admin.findDocuments.useQuery(
|
||||
{
|
||||
query: debouncedTerm,
|
||||
page: page || 1,
|
||||
|
||||
@@ -9,7 +9,7 @@ import { Link } from 'react-router';
|
||||
import type { z } from 'zod';
|
||||
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { ZUpdateUserRequestSchema } from '@documenso/trpc/server/admin-router/update-user.types';
|
||||
import { ZAdminUpdateProfileMutationSchema } from '@documenso/trpc/server/admin-router/schema';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import {
|
||||
Form,
|
||||
@@ -27,13 +27,12 @@ import { AdminOrganisationCreateDialog } from '~/components/dialogs/admin-organi
|
||||
import { AdminUserDeleteDialog } from '~/components/dialogs/admin-user-delete-dialog';
|
||||
import { AdminUserDisableDialog } from '~/components/dialogs/admin-user-disable-dialog';
|
||||
import { AdminUserEnableDialog } from '~/components/dialogs/admin-user-enable-dialog';
|
||||
import { AdminUserResetTwoFactorDialog } from '~/components/dialogs/admin-user-reset-two-factor-dialog';
|
||||
import { GenericErrorLayout } from '~/components/general/generic-error-layout';
|
||||
import { AdminOrganisationsTable } from '~/components/tables/admin-organisations-table';
|
||||
|
||||
import { MultiSelectRoleCombobox } from '../../../components/general/multiselect-role-combobox';
|
||||
|
||||
const ZUserFormSchema = ZUpdateUserRequestSchema.omit({ id: true });
|
||||
const ZUserFormSchema = ZAdminUpdateProfileMutationSchema.omit({ id: true });
|
||||
|
||||
type TUserFormSchema = z.infer<typeof ZUserFormSchema>;
|
||||
|
||||
@@ -85,7 +84,7 @@ const AdminUserPage = ({ user }: { user: User }) => {
|
||||
|
||||
const roles = user.roles ?? [];
|
||||
|
||||
const { mutateAsync: updateUserMutation } = trpc.admin.user.update.useMutation();
|
||||
const { mutateAsync: updateUserMutation } = trpc.admin.updateUser.useMutation();
|
||||
|
||||
const form = useForm<TUserFormSchema>({
|
||||
resolver: zodResolver(ZUserFormSchema),
|
||||
@@ -220,11 +219,10 @@ const AdminUserPage = ({ user }: { user: User }) => {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-16 flex flex-col gap-4">
|
||||
{user && user.twoFactorEnabled && <AdminUserResetTwoFactorDialog user={user} />}
|
||||
<div className="mt-16 flex flex-col items-center gap-4">
|
||||
{user && <AdminUserDeleteDialog user={user} />}
|
||||
{user && user.disabled && <AdminUserEnableDialog userToEnable={user} />}
|
||||
{user && !user.disabled && <AdminUserDisableDialog userToDisable={user} />}
|
||||
{user && <AdminUserDeleteDialog user={user} />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,125 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { BookIcon, HelpCircleIcon, Link2Icon } from 'lucide-react';
|
||||
import { Link, useSearchParams } from 'react-router';
|
||||
|
||||
import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation';
|
||||
import { useSession } from '@documenso/lib/client-only/providers/session';
|
||||
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
|
||||
import { SupportTicketForm } from '~/components/forms/support-ticket-form';
|
||||
import { appMetaTags } from '~/utils/meta';
|
||||
|
||||
export function meta() {
|
||||
return appMetaTags('Support');
|
||||
}
|
||||
|
||||
export default function SupportPage() {
|
||||
const [showForm, setShowForm] = useState(false);
|
||||
const { user } = useSession();
|
||||
const organisation = useCurrentOrganisation();
|
||||
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
const teamId = searchParams.get('team');
|
||||
|
||||
const subscriptionStatus = organisation.subscription?.status;
|
||||
|
||||
const handleSuccess = () => {
|
||||
setShowForm(false);
|
||||
};
|
||||
|
||||
const handleCloseForm = () => {
|
||||
setShowForm(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mx-auto w-full max-w-screen-xl px-4 md:px-8">
|
||||
<div className="mb-8">
|
||||
<h1 className="flex flex-row items-center gap-2 text-3xl font-bold">
|
||||
<HelpCircleIcon className="text-muted-foreground h-8 w-8" />
|
||||
<Trans>Support</Trans>
|
||||
</h1>
|
||||
|
||||
<p className="text-muted-foreground mt-2">
|
||||
<Trans>Your current plan includes the following support channels:</Trans>
|
||||
</p>
|
||||
|
||||
<div className="mt-6 flex flex-col gap-4">
|
||||
<div className="rounded-lg border p-4">
|
||||
<h2 className="flex items-center gap-2 text-lg font-bold">
|
||||
<BookIcon className="text-muted-foreground h-5 w-5" />
|
||||
<Link
|
||||
to="https://docs.documenso.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="hover:underline"
|
||||
>
|
||||
<Trans>Documentation</Trans>
|
||||
</Link>
|
||||
</h2>
|
||||
<p className="text-muted-foreground mt-1">
|
||||
<Trans>Read our documentation to get started with Documenso.</Trans>
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-lg border p-4">
|
||||
<h2 className="flex items-center gap-2 text-lg font-bold">
|
||||
<Link2Icon className="text-muted-foreground h-5 w-5" />
|
||||
<Link
|
||||
to="https://documen.so/discord"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="hover:underline"
|
||||
>
|
||||
<Trans>Discord</Trans>
|
||||
</Link>
|
||||
</h2>
|
||||
<p className="text-muted-foreground mt-1">
|
||||
<Trans>
|
||||
Join our community on{' '}
|
||||
<Link
|
||||
to="https://documen.so/discord"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="hover:underline"
|
||||
>
|
||||
Discord
|
||||
</Link>{' '}
|
||||
for community support and discussion.
|
||||
</Trans>
|
||||
</p>
|
||||
</div>
|
||||
{organisation && IS_BILLING_ENABLED() && subscriptionStatus && (
|
||||
<>
|
||||
<div className="rounded-lg border p-4">
|
||||
<h2 className="flex items-center gap-2 text-lg font-bold">
|
||||
<Link2Icon className="text-muted-foreground h-5 w-5" />
|
||||
<Trans>Contact us</Trans>
|
||||
</h2>
|
||||
<p className="text-muted-foreground mt-1">
|
||||
<Trans>We'll get back to you as soon as possible via email.</Trans>
|
||||
</p>
|
||||
<div className="mt-4">
|
||||
{!showForm ? (
|
||||
<Button variant="outline" size="sm" onClick={() => setShowForm(true)}>
|
||||
<Trans>Create a support ticket</Trans>
|
||||
</Button>
|
||||
) : (
|
||||
<SupportTicketForm
|
||||
organisationId={organisation.id}
|
||||
teamId={teamId}
|
||||
onSuccess={handleSuccess}
|
||||
onClose={handleCloseForm}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -12,8 +12,10 @@ import { parseToIntegerArray } from '@documenso/lib/utils/params';
|
||||
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
|
||||
import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import type { TFindDocumentsInternalResponse } from '@documenso/trpc/server/document-router/find-documents-internal.types';
|
||||
import { ZFindDocumentsInternalRequestSchema } from '@documenso/trpc/server/document-router/find-documents-internal.types';
|
||||
import {
|
||||
type TFindDocumentsInternalResponse,
|
||||
ZFindDocumentsInternalRequestSchema,
|
||||
} from '@documenso/trpc/server/document-router/schema';
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@documenso/ui/primitives/avatar';
|
||||
import { Tabs, TabsList, TabsTrigger } from '@documenso/ui/primitives/tabs';
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ export function meta() {
|
||||
export default function ApiTokensPage() {
|
||||
const { i18n } = useLingui();
|
||||
|
||||
const { data: tokens } = trpc.apiToken.getMany.useQuery();
|
||||
const { data: tokens } = trpc.apiToken.getTokens.useQuery();
|
||||
|
||||
const team = useOptionalCurrentTeam();
|
||||
|
||||
|
||||
@@ -9,10 +9,10 @@ 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';
|
||||
import { TemplateDropZoneWrapper } from '~/components/general/template/template-drop-zone-wrapper';
|
||||
|
||||
export function meta() {
|
||||
return appMetaTags('Templates');
|
||||
|
||||
@@ -94,6 +94,7 @@ 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,6 +19,7 @@ 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';
|
||||
|
||||
@@ -41,6 +42,10 @@ 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,
|
||||
@@ -116,6 +121,7 @@ export async function loader({ params, request }: Route.LoaderArgs) {
|
||||
isDocumentAccessValid: false,
|
||||
recipientEmail: recipient.email,
|
||||
recipientHasAccount,
|
||||
isEnterprise,
|
||||
} as const);
|
||||
}
|
||||
|
||||
@@ -153,6 +159,7 @@ export async function loader({ params, request }: Route.LoaderArgs) {
|
||||
recipientSignature,
|
||||
isRecipientsTurn,
|
||||
includeSenderDetails: settings.includeSenderDetails,
|
||||
isEnterprise,
|
||||
} as const);
|
||||
}
|
||||
|
||||
@@ -181,6 +188,7 @@ export default function SigningPage() {
|
||||
allRecipients,
|
||||
includeSenderDetails,
|
||||
recipientWithFields,
|
||||
isEnterprise,
|
||||
} = data;
|
||||
|
||||
if (document.deletedAt || document.status === DocumentStatus.REJECTED) {
|
||||
@@ -246,6 +254,7 @@ export default function SigningPage() {
|
||||
documentAuthOptions={document.authOptions}
|
||||
recipient={recipient}
|
||||
user={user}
|
||||
isEnterprise={isEnterprise}
|
||||
>
|
||||
<DocumentSigningPageView
|
||||
recipient={recipientWithFields}
|
||||
@@ -255,6 +264,7 @@ export default function SigningPage() {
|
||||
isRecipientsTurn={isRecipientsTurn}
|
||||
allRecipients={allRecipients}
|
||||
includeSenderDetails={includeSenderDetails}
|
||||
isEnterprise={isEnterprise}
|
||||
/>
|
||||
</DocumentSigningAuthProvider>
|
||||
</DocumentSigningProvider>
|
||||
|
||||
@@ -88,6 +88,8 @@ 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,
|
||||
@@ -96,12 +98,21 @@ export async function loader({ params, request }: Route.LoaderArgs) {
|
||||
fields,
|
||||
hidePoweredBy,
|
||||
allowEmbedSigningWhitelabel,
|
||||
isEnterpriseDocument,
|
||||
});
|
||||
}
|
||||
|
||||
export default function EmbedDirectTemplatePage() {
|
||||
const { token, user, template, recipient, fields, hidePoweredBy, allowEmbedSigningWhitelabel } =
|
||||
useSuperLoaderData<typeof loader>();
|
||||
const {
|
||||
token,
|
||||
user,
|
||||
template,
|
||||
recipient,
|
||||
fields,
|
||||
hidePoweredBy,
|
||||
allowEmbedSigningWhitelabel,
|
||||
isEnterpriseDocument,
|
||||
} = useSuperLoaderData<typeof loader>();
|
||||
|
||||
return (
|
||||
<DocumentSigningProvider
|
||||
@@ -116,6 +127,7 @@ export default function EmbedDirectTemplatePage() {
|
||||
documentAuthOptions={template.authOptions}
|
||||
recipient={recipient}
|
||||
user={user}
|
||||
isEnterprise={isEnterpriseDocument}
|
||||
>
|
||||
<DocumentSigningRecipientProvider recipient={recipient}>
|
||||
<EmbedDirectTemplateClientPage
|
||||
|
||||
@@ -109,6 +109,8 @@ export async function loader({ params, request }: Route.LoaderArgs) {
|
||||
})
|
||||
: [];
|
||||
|
||||
const isEnterpriseDocument = Boolean(organisationClaim);
|
||||
|
||||
return superLoaderJson({
|
||||
token,
|
||||
user,
|
||||
@@ -119,6 +121,7 @@ export async function loader({ params, request }: Route.LoaderArgs) {
|
||||
completedFields,
|
||||
hidePoweredBy,
|
||||
allowEmbedSigningWhitelabel,
|
||||
isEnterpriseDocument,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -133,6 +136,7 @@ export default function EmbedSignDocumentPage() {
|
||||
completedFields,
|
||||
hidePoweredBy,
|
||||
allowEmbedSigningWhitelabel,
|
||||
isEnterpriseDocument,
|
||||
} = useSuperLoaderData<typeof loader>();
|
||||
|
||||
return (
|
||||
@@ -148,6 +152,7 @@ export default function EmbedSignDocumentPage() {
|
||||
documentAuthOptions={document.authOptions}
|
||||
recipient={recipient}
|
||||
user={user}
|
||||
isEnterprise={isEnterpriseDocument}
|
||||
>
|
||||
<EmbedSignDocumentClientPage
|
||||
token={token}
|
||||
|
||||
@@ -48,6 +48,7 @@ export async function loader({ request }: Route.LoaderArgs) {
|
||||
user,
|
||||
hidePoweredBy: false,
|
||||
allowWhitelabelling: false,
|
||||
isEnterprise: false,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -55,17 +56,19 @@ 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 } =
|
||||
const { envelopes, user, hidePoweredBy, allowWhitelabelling, isEnterprise } =
|
||||
useSuperLoaderData<typeof loader>();
|
||||
const revalidator = useRevalidator();
|
||||
|
||||
@@ -264,6 +267,7 @@ 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.5"
|
||||
"version": "1.12.2-rc.3"
|
||||
}
|
||||
|
||||
@@ -51,4 +51,4 @@ services:
|
||||
ports:
|
||||
- 3000:3000
|
||||
volumes:
|
||||
- ../../apps/remix/example/cert.p12:/opt/documenso/cert.p12
|
||||
- ../../apps/web/example/cert.p12:/opt/documenso/cert.p12
|
||||
|
||||
Generated
+7
-53
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@documenso/root",
|
||||
"version": "1.12.2-rc.5",
|
||||
"version": "1.12.2-rc.3",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@documenso/root",
|
||||
"version": "1.12.2-rc.5",
|
||||
"version": "1.12.2-rc.3",
|
||||
"workspaces": [
|
||||
"apps/*",
|
||||
"packages/*"
|
||||
@@ -89,7 +89,7 @@
|
||||
},
|
||||
"apps/remix": {
|
||||
"name": "@documenso/remix",
|
||||
"version": "1.12.2-rc.5",
|
||||
"version": "1.12.2-rc.3",
|
||||
"dependencies": {
|
||||
"@documenso/api": "*",
|
||||
"@documenso/assets": "*",
|
||||
@@ -3522,15 +3522,6 @@
|
||||
"integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@graphql-typed-document-node/core": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.2.0.tgz",
|
||||
"integrity": "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@grpc/grpc-js": {
|
||||
"version": "1.13.3",
|
||||
"resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.13.3.tgz",
|
||||
@@ -11835,20 +11826,6 @@
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
}
|
||||
},
|
||||
"node_modules/@team-plain/typescript-sdk": {
|
||||
"version": "5.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@team-plain/typescript-sdk/-/typescript-sdk-5.9.0.tgz",
|
||||
"integrity": "sha512-AHSXyt1kDt74m9YKZBCRCd6cQjB8QjUNr9cehtR2QHzZ/8yXJPzawPJDqOQ3ms5KvwuYrBx2qT3e6C/zrQ5UtA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@graphql-typed-document-node/core": "^3.2.0",
|
||||
"ajv": "^8.12.0",
|
||||
"ajv-formats": "^2.1.1",
|
||||
"graphql": "^16.6.0",
|
||||
"lodash.get": "^4.4.2",
|
||||
"zod": "3.22.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@theguild/remark-mermaid": {
|
||||
"version": "0.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@theguild/remark-mermaid/-/remark-mermaid-0.0.5.tgz",
|
||||
@@ -13258,6 +13235,7 @@
|
||||
"version": "8.17.1",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
|
||||
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
@@ -13270,23 +13248,6 @@
|
||||
"url": "https://github.com/sponsors/epoberezkin"
|
||||
}
|
||||
},
|
||||
"node_modules/ajv-formats": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz",
|
||||
"integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ajv": "^8.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"ajv": "^8.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"ajv": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-escapes": {
|
||||
"version": "4.3.2",
|
||||
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
|
||||
@@ -18810,6 +18771,7 @@
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz",
|
||||
"integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -19885,15 +19847,6 @@
|
||||
"integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/graphql": {
|
||||
"version": "16.11.0",
|
||||
"resolved": "https://registry.npmjs.org/graphql/-/graphql-16.11.0.tgz",
|
||||
"integrity": "sha512-mS1lbMsxgQj6hge1XZ6p7GPhbrtFwUFYi3wRzXAC/FmYnyXMTvvI3td3rjmQ2u8ewXueaSvRPWaEcgVVOT9Jnw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/gray-matter": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz",
|
||||
@@ -22376,6 +22329,7 @@
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
||||
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/json-stable-stringify-without-jsonify": {
|
||||
@@ -30616,6 +30570,7 @@
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
|
||||
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
@@ -36628,7 +36583,6 @@
|
||||
"@pdf-lib/fontkit": "^1.1.1",
|
||||
"@scure/base": "^1.1.3",
|
||||
"@sindresorhus/slugify": "^2.2.1",
|
||||
"@team-plain/typescript-sdk": "^5.9.0",
|
||||
"@vvo/tzdb": "^6.117.0",
|
||||
"csv-parse": "^5.6.0",
|
||||
"inngest": "^3.19.13",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"private": true,
|
||||
"version": "1.12.2-rc.5",
|
||||
"version": "1.12.2-rc.3",
|
||||
"scripts": {
|
||||
"build": "turbo run build",
|
||||
"dev": "turbo run dev --filter=@documenso/remix",
|
||||
|
||||
@@ -330,7 +330,6 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, {
|
||||
userId: user.id,
|
||||
teamId: team?.id,
|
||||
formValues: body.formValues,
|
||||
folderId: body.folderId,
|
||||
documentDataId: documentData.id,
|
||||
requestMetadata: metadata,
|
||||
});
|
||||
@@ -737,7 +736,6 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, {
|
||||
teamId: team?.id,
|
||||
recipients: body.recipients,
|
||||
prefillFields: body.prefillFields,
|
||||
folderId: body.folderId,
|
||||
override: {
|
||||
title: body.title,
|
||||
...body.meta,
|
||||
|
||||
@@ -136,12 +136,6 @@ export type TUploadDocumentSuccessfulSchema = z.infer<typeof ZUploadDocumentSucc
|
||||
export const ZCreateDocumentMutationSchema = z.object({
|
||||
title: z.string().min(1),
|
||||
externalId: z.string().nullish(),
|
||||
folderId: z
|
||||
.string()
|
||||
.describe(
|
||||
'The ID of the folder to create the document in. If not provided, the document will be created in the root folder.',
|
||||
)
|
||||
.optional(),
|
||||
recipients: z.array(
|
||||
z.object({
|
||||
name: z.string().min(1),
|
||||
@@ -293,12 +287,6 @@ export type TCreateDocumentFromTemplateMutationResponseSchema = z.infer<
|
||||
export const ZGenerateDocumentFromTemplateMutationSchema = z.object({
|
||||
title: z.string().optional(),
|
||||
externalId: z.string().optional(),
|
||||
folderId: z
|
||||
.string()
|
||||
.describe(
|
||||
'The ID of the folder to create the document in. If not provided, the document will be created in the root folder.',
|
||||
)
|
||||
.optional(),
|
||||
recipients: z
|
||||
.array(
|
||||
z.object({
|
||||
|
||||
@@ -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 NOT be visible.
|
||||
await expect(page.getByTestId('documentActionSelectValue')).not.toBeVisible();
|
||||
// Action auth should now be visible for all users
|
||||
await expect(page.getByTestId('documentActionSelectValue')).toBeVisible();
|
||||
|
||||
// Save the settings by going to the next step.
|
||||
|
||||
|
||||
@@ -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 NOT be visible.
|
||||
await expect(page.getByTestId('documentActionSelectValue')).not.toBeVisible();
|
||||
// Action auth should now be visible for all users
|
||||
await expect(page.getByTestId('documentActionSelectValue')).toBeVisible();
|
||||
|
||||
// Save the settings by going to the next step.
|
||||
await page.getByRole('button', { name: 'Continue' }).click();
|
||||
|
||||
@@ -144,11 +144,10 @@ test('[TEMPLATES]: use template', async ({ page }) => {
|
||||
await page.getByRole('button', { name: 'Use Template' }).click();
|
||||
|
||||
// Enter template values.
|
||||
// Get input with Email label placeholder.
|
||||
await page.getByLabel('Email').click();
|
||||
await page.getByLabel('Email').fill(teamMemberUser.email);
|
||||
await page.getByLabel('Name').click();
|
||||
await page.getByLabel('Name').fill('name');
|
||||
await page.getByPlaceholder('recipient.1@documenso.com').click();
|
||||
await page.getByPlaceholder('recipient.1@documenso.com').fill(teamMemberUser.email);
|
||||
await page.getByPlaceholder('Recipient 1').click();
|
||||
await page.getByPlaceholder('Recipient 1').fill('name');
|
||||
|
||||
await page.getByRole('button', { name: 'Create as draft' }).click();
|
||||
await page.waitForURL(/\/t\/.+\/documents/);
|
||||
|
||||
@@ -92,11 +92,7 @@ export const handleOAuthCallbackUrl = async (options: HandleOAuthCallbackUrlOpti
|
||||
providerAccountId: sub,
|
||||
},
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
},
|
||||
user: true,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
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;
|
||||
@@ -0,0 +1,62 @@
|
||||
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;
|
||||
@@ -29,13 +29,7 @@ export const run = async ({
|
||||
id: documentId,
|
||||
},
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
user: true,
|
||||
documentMeta: true,
|
||||
recipients: true,
|
||||
team: {
|
||||
|
||||
+2
-14
@@ -39,13 +39,7 @@ export const run = async ({
|
||||
},
|
||||
},
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
user: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -57,13 +51,7 @@ export const run = async ({
|
||||
organisationId: payload.organisationId,
|
||||
},
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
user: true,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
+1
-12
@@ -39,13 +39,7 @@ export const run = async ({
|
||||
},
|
||||
},
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
user: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -55,11 +49,6 @@ export const run = async ({
|
||||
where: {
|
||||
id: payload.memberUserId,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
},
|
||||
});
|
||||
|
||||
const { branding, emailLanguage, senderEmail } = await getEmailContext({
|
||||
|
||||
@@ -38,13 +38,7 @@ export const run = async ({
|
||||
id: recipientId,
|
||||
},
|
||||
},
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
user: true,
|
||||
documentMeta: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -33,13 +33,7 @@ export const run = async ({
|
||||
id: documentId,
|
||||
},
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
user: true,
|
||||
documentMeta: true,
|
||||
team: {
|
||||
select: {
|
||||
|
||||
@@ -33,7 +33,6 @@
|
||||
"@pdf-lib/fontkit": "^1.1.1",
|
||||
"@scure/base": "^1.1.3",
|
||||
"@sindresorhus/slugify": "^2.2.1",
|
||||
"@team-plain/typescript-sdk": "^5.9.0",
|
||||
"@vvo/tzdb": "^6.117.0",
|
||||
"csv-parse": "^5.6.0",
|
||||
"inngest": "^3.19.13",
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import { PlainClient } from '@team-plain/typescript-sdk';
|
||||
|
||||
import { env } from '@documenso/lib/utils/env';
|
||||
|
||||
export const plainClient = new PlainClient({
|
||||
apiKey: env('NEXT_PRIVATE_PLAIN_API_KEY') ?? '',
|
||||
});
|
||||
@@ -0,0 +1,120 @@
|
||||
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);
|
||||
}
|
||||
};
|
||||
@@ -16,7 +16,7 @@ export const updateUser = async ({ id, name, email, roles }: UpdateUserOptions)
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.user.update({
|
||||
return await prisma.user.update({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { DocumentVisibility, TemplateMeta } from '@prisma/client';
|
||||
import {
|
||||
DocumentSource,
|
||||
FolderType,
|
||||
RecipientRole,
|
||||
SendStatus,
|
||||
SigningStatus,
|
||||
@@ -15,7 +14,7 @@ import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-reques
|
||||
import { nanoid, prefixedId } from '@documenso/lib/universal/id';
|
||||
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import type { TCreateDocumentTemporaryRequest } from '@documenso/trpc/server/document-router/create-document-temporary.types';
|
||||
import type { TCreateDocumentV2Request } from '@documenso/trpc/server/document-router/schema';
|
||||
|
||||
import type { TDocumentAccessAuthTypes, TDocumentActionAuthTypes } from '../../types/document-auth';
|
||||
import type { TDocumentFormValues } from '../../types/document-form-values';
|
||||
@@ -45,8 +44,7 @@ export type CreateDocumentOptions = {
|
||||
globalAccessAuth?: TDocumentAccessAuthTypes[];
|
||||
globalActionAuth?: TDocumentActionAuthTypes[];
|
||||
formValues?: TDocumentFormValues;
|
||||
recipients: TCreateDocumentTemporaryRequest['recipients'];
|
||||
folderId?: string;
|
||||
recipients: TCreateDocumentV2Request['recipients'];
|
||||
};
|
||||
meta?: Partial<Omit<TemplateMeta, 'id' | 'templateId'>>;
|
||||
requestMetadata: ApiRequestMetadata;
|
||||
@@ -61,7 +59,7 @@ export const createDocumentV2 = async ({
|
||||
meta,
|
||||
requestMetadata,
|
||||
}: CreateDocumentOptions) => {
|
||||
const { title, formValues, folderId } = data;
|
||||
const { title, formValues } = data;
|
||||
|
||||
const team = await prisma.team.findFirst({
|
||||
where: buildTeamWhereQuery({ teamId, userId }),
|
||||
@@ -80,22 +78,6 @@ export const createDocumentV2 = async ({
|
||||
});
|
||||
}
|
||||
|
||||
if (folderId) {
|
||||
const folder = await prisma.folder.findUnique({
|
||||
where: {
|
||||
id: folderId,
|
||||
type: FolderType.DOCUMENT,
|
||||
team: buildTeamWhereQuery({ teamId, userId }),
|
||||
},
|
||||
});
|
||||
|
||||
if (!folder) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Folder not found',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const settings = await getTeamSettings({
|
||||
userId,
|
||||
teamId,
|
||||
@@ -182,7 +164,6 @@ export const createDocumentV2 = async ({
|
||||
teamId,
|
||||
authOptions,
|
||||
visibility,
|
||||
folderId,
|
||||
formValues,
|
||||
source: DocumentSource.DOCUMENT,
|
||||
documentMeta: {
|
||||
|
||||
@@ -49,11 +49,6 @@ export const findDocuments = async ({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
},
|
||||
});
|
||||
|
||||
let team = null;
|
||||
@@ -272,7 +267,7 @@ export const findDocuments = async ({
|
||||
|
||||
const findDocumentsFilter = (
|
||||
status: ExtendedDocumentStatus,
|
||||
user: Pick<User, 'id' | 'email' | 'name'>,
|
||||
user: User,
|
||||
folderId?: string | null,
|
||||
) => {
|
||||
return match<ExtendedDocumentStatus, Prisma.DocumentWhereInput>(status)
|
||||
|
||||
@@ -111,7 +111,7 @@ export const getDocumentWhereInput = async ({
|
||||
visibility: {
|
||||
in: teamVisibilityFilters,
|
||||
},
|
||||
teamId: team.id,
|
||||
teamId,
|
||||
},
|
||||
// Or, if they are a recipient of the document.
|
||||
{
|
||||
|
||||
@@ -73,13 +73,7 @@ export const getDocumentAndSenderByToken = async ({
|
||||
},
|
||||
},
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
user: true,
|
||||
documentData: true,
|
||||
documentMeta: true,
|
||||
recipients: {
|
||||
@@ -96,6 +90,9 @@ export const getDocumentAndSenderByToken = async ({
|
||||
},
|
||||
});
|
||||
|
||||
// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
|
||||
const { password: _password, ...user } = result.user;
|
||||
|
||||
const recipient = result.recipients[0];
|
||||
|
||||
// Sanity check, should not be possible.
|
||||
@@ -123,11 +120,7 @@ export const getDocumentAndSenderByToken = async ({
|
||||
|
||||
return {
|
||||
...result,
|
||||
user: {
|
||||
id: result.user.id,
|
||||
email: result.user.email,
|
||||
name: result.user.name,
|
||||
},
|
||||
user,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -7,12 +7,14 @@ export type GetDocumentWithDetailsByIdOptions = {
|
||||
documentId: number;
|
||||
userId: number;
|
||||
teamId: number;
|
||||
folderId?: string;
|
||||
};
|
||||
|
||||
export const getDocumentWithDetailsById = async ({
|
||||
documentId,
|
||||
userId,
|
||||
teamId,
|
||||
folderId,
|
||||
}: GetDocumentWithDetailsByIdOptions) => {
|
||||
const { documentWhereInput } = await getDocumentWhereInput({
|
||||
documentId,
|
||||
@@ -23,6 +25,7 @@ export const getDocumentWithDetailsById = async ({
|
||||
const document = await prisma.document.findFirst({
|
||||
where: {
|
||||
...documentWhereInput,
|
||||
folderId,
|
||||
},
|
||||
include: {
|
||||
documentData: true,
|
||||
|
||||
@@ -28,7 +28,13 @@ export async function rejectDocumentWithToken({
|
||||
documentId,
|
||||
},
|
||||
include: {
|
||||
document: true,
|
||||
document: {
|
||||
include: {
|
||||
user: true,
|
||||
recipients: true,
|
||||
documentMeta: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -33,13 +33,7 @@ export const sendCompletedEmail = async ({ documentId, requestMetadata }: SendDo
|
||||
documentData: true,
|
||||
documentMeta: true,
|
||||
recipients: true,
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
user: true,
|
||||
team: {
|
||||
select: {
|
||||
id: true,
|
||||
|
||||
@@ -24,13 +24,7 @@ export const sendDeleteEmail = async ({ documentId, reason }: SendDeleteEmailOpt
|
||||
id: documentId,
|
||||
},
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
user: true,
|
||||
documentMeta: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -30,13 +30,7 @@ export const superDeleteDocument = async ({ id, requestMetadata }: SuperDeleteDo
|
||||
include: {
|
||||
recipients: true,
|
||||
documentMeta: true,
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
user: true,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { DocumentVisibility } from '@prisma/client';
|
||||
import { DocumentStatus, TeamMemberRole } from '@prisma/client';
|
||||
import { DocumentStatus, DocumentVisibility, TeamMemberRole } from '@prisma/client';
|
||||
import { isDeepEqual } from 'remeda';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
@@ -119,13 +118,6 @@ 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 =
|
||||
|
||||
@@ -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 } from '../../types/document-auth';
|
||||
import type { TRecipientActionAuth, TRecipientActionAuthTypes } from '../../types/document-auth';
|
||||
import {
|
||||
ZCheckboxFieldMeta,
|
||||
ZDropdownFieldMeta,
|
||||
@@ -25,7 +25,9 @@ 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;
|
||||
@@ -171,13 +173,25 @@ export const signFieldWithToken = async ({
|
||||
}
|
||||
}
|
||||
|
||||
const derivedRecipientActionAuth = await validateFieldAuth({
|
||||
documentAuthOptions: document.authOptions,
|
||||
recipient,
|
||||
field,
|
||||
userId,
|
||||
authOptions,
|
||||
});
|
||||
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 documentMeta = await prisma.documentMeta.findFirst({
|
||||
where: {
|
||||
@@ -311,9 +325,9 @@ export const signFieldWithToken = async ({
|
||||
}),
|
||||
)
|
||||
.exhaustive(),
|
||||
fieldSecurity: derivedRecipientActionAuth
|
||||
fieldSecurity: requiredAuthType
|
||||
? {
|
||||
type: derivedRecipientActionAuth,
|
||||
type: requiredAuthType,
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
|
||||
@@ -37,3 +37,23 @@ 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;
|
||||
};
|
||||
|
||||
@@ -3,7 +3,6 @@ import type { PDFDocument } from 'pdf-lib';
|
||||
import { TextAlignment, rgb, setFontAndSize } from 'pdf-lib';
|
||||
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
||||
import { getPageSize } from './get-page-size';
|
||||
|
||||
/**
|
||||
* Adds a rejection stamp to each page of a PDF document.
|
||||
@@ -28,7 +27,7 @@ export async function addRejectionStampToPdf(
|
||||
|
||||
for (let i = 0; i < pages.length; i++) {
|
||||
const page = pages[i];
|
||||
const { width, height } = getPageSize(page);
|
||||
const { width, height } = page.getSize();
|
||||
|
||||
// Draw the "REJECTED" text
|
||||
const rejectedTitleText = 'DOCUMENT REJECTED';
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
import type { PDFPage } from 'pdf-lib';
|
||||
|
||||
/**
|
||||
* Gets the effective page size for PDF operations.
|
||||
*
|
||||
* Uses CropBox by default to handle rare cases where MediaBox is larger than CropBox.
|
||||
* Falls back to MediaBox when it's smaller than CropBox, following typical PDF reader behavior.
|
||||
*/
|
||||
export const getPageSize = (page: PDFPage) => {
|
||||
const cropBox = page.getCropBox();
|
||||
const mediaBox = page.getMediaBox();
|
||||
|
||||
if (mediaBox.width < cropBox.width || mediaBox.height < cropBox.height) {
|
||||
return mediaBox;
|
||||
}
|
||||
|
||||
return cropBox;
|
||||
};
|
||||
@@ -33,7 +33,6 @@ import {
|
||||
ZRadioFieldMeta,
|
||||
ZTextFieldMeta,
|
||||
} from '../../types/field-meta';
|
||||
import { getPageSize } from './get-page-size';
|
||||
|
||||
export const insertFieldInPDF = async (pdf: PDFDocument, field: FieldWithSignature) => {
|
||||
const [fontCaveat, fontNoto] = await Promise.all([
|
||||
@@ -78,7 +77,7 @@ export const insertFieldInPDF = async (pdf: PDFDocument, field: FieldWithSignatu
|
||||
|
||||
const isPageRotatedToLandscape = pageRotationInDegrees === 90 || pageRotationInDegrees === 270;
|
||||
|
||||
let { width: pageWidth, height: pageHeight } = getPageSize(page);
|
||||
let { width: pageWidth, height: pageHeight } = page.getSize();
|
||||
|
||||
// PDFs can have pages that are rotated, which are correctly rendered in the frontend.
|
||||
// However when we load the PDF in the backend, the rotation is applied.
|
||||
|
||||
@@ -26,7 +26,6 @@ import {
|
||||
ZRadioFieldMeta,
|
||||
ZTextFieldMeta,
|
||||
} from '../../types/field-meta';
|
||||
import { getPageSize } from './get-page-size';
|
||||
|
||||
export const legacy_insertFieldInPDF = async (pdf: PDFDocument, field: FieldWithSignature) => {
|
||||
const [fontCaveat, fontNoto] = await Promise.all([
|
||||
@@ -64,7 +63,7 @@ export const legacy_insertFieldInPDF = async (pdf: PDFDocument, field: FieldWith
|
||||
|
||||
const isPageRotatedToLandscape = pageRotationInDegrees === 90 || pageRotationInDegrees === 270;
|
||||
|
||||
let { width: pageWidth, height: pageHeight } = getPageSize(page);
|
||||
let { width: pageWidth, height: pageHeight } = page.getSize();
|
||||
|
||||
// PDFs can have pages that are rotated, which are correctly rendered in the frontend.
|
||||
// However when we load the PDF in the backend, the rotation is applied.
|
||||
|
||||
@@ -25,7 +25,7 @@ export const deleteTokenById = async ({ id, userId, teamId }: DeleteTokenByIdOpt
|
||||
});
|
||||
}
|
||||
|
||||
await prisma.apiToken.delete({
|
||||
return await prisma.apiToken.delete({
|
||||
where: {
|
||||
id,
|
||||
teamId,
|
||||
|
||||
@@ -134,9 +134,6 @@ export const setDocumentRecipients = async ({
|
||||
existingRecipient.id === recipient.id || existingRecipient.email === recipient.email,
|
||||
);
|
||||
|
||||
const canPersistedRecipientBeModified =
|
||||
existing && canRecipientBeModified(existing, document.fields);
|
||||
|
||||
if (
|
||||
existing &&
|
||||
hasRecipientBeenChanged(existing, recipient) &&
|
||||
@@ -150,7 +147,6 @@ export const setDocumentRecipients = async ({
|
||||
return {
|
||||
...recipient,
|
||||
_persisted: existing,
|
||||
canPersistedRecipientBeModified,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -166,13 +162,6 @@ export const setDocumentRecipients = async ({
|
||||
});
|
||||
}
|
||||
|
||||
if (recipient._persisted && !recipient.canPersistedRecipientBeModified) {
|
||||
return {
|
||||
...recipient._persisted,
|
||||
clientId: recipient.clientId,
|
||||
};
|
||||
}
|
||||
|
||||
const upsertedRecipient = await tx.recipient.upsert({
|
||||
where: {
|
||||
id: recipient._persisted?.id ?? -1,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import type { Recipient } from '@prisma/client';
|
||||
import { RecipientRole } from '@prisma/client';
|
||||
import { SendStatus, SigningStatus } from '@prisma/client';
|
||||
import { isDeepEqual } from 'remeda';
|
||||
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
||||
import type { TRecipientAccessAuthTypes } from '@documenso/lib/types/document-auth';
|
||||
@@ -102,7 +104,10 @@ export const updateDocumentRecipients = async ({
|
||||
});
|
||||
}
|
||||
|
||||
if (!canRecipientBeModified(originalRecipient, document.fields)) {
|
||||
if (
|
||||
hasRecipientBeenChanged(originalRecipient, recipient) &&
|
||||
!canRecipientBeModified(originalRecipient, document.fields)
|
||||
) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message: 'Cannot modify a recipient who has already interacted with the document',
|
||||
});
|
||||
@@ -198,6 +203,9 @@ export const updateDocumentRecipients = async ({
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* If you change this you MUST update the `hasRecipientBeenChanged` function.
|
||||
*/
|
||||
type RecipientData = {
|
||||
id: number;
|
||||
email?: string;
|
||||
@@ -207,3 +215,19 @@ type RecipientData = {
|
||||
accessAuth?: TRecipientAccessAuthTypes[];
|
||||
actionAuth?: TRecipientActionAuthTypes[];
|
||||
};
|
||||
|
||||
const hasRecipientBeenChanged = (recipient: Recipient, newRecipientData: RecipientData) => {
|
||||
const authOptions = ZRecipientAuthOptionsSchema.parse(recipient.authOptions);
|
||||
|
||||
const newRecipientAccessAuth = newRecipientData.accessAuth || null;
|
||||
const newRecipientActionAuth = newRecipientData.actionAuth || null;
|
||||
|
||||
return (
|
||||
recipient.email !== newRecipientData.email ||
|
||||
recipient.name !== newRecipientData.name ||
|
||||
recipient.role !== newRecipientData.role ||
|
||||
recipient.signingOrder !== newRecipientData.signingOrder ||
|
||||
!isDeepEqual(authOptions.accessAuth, newRecipientAccessAuth) ||
|
||||
!isDeepEqual(authOptions.actionAuth, newRecipientActionAuth)
|
||||
);
|
||||
};
|
||||
|
||||
@@ -105,13 +105,7 @@ export const createDocumentFromDirectTemplate = async ({
|
||||
directLink: true,
|
||||
templateDocumentData: true,
|
||||
templateMeta: true,
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
user: true,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ import type { DocumentDistributionMethod, DocumentSigningOrder } from '@prisma/c
|
||||
import {
|
||||
DocumentSource,
|
||||
type Field,
|
||||
FolderType,
|
||||
type Recipient,
|
||||
RecipientRole,
|
||||
SendStatus,
|
||||
@@ -70,7 +69,6 @@ export type CreateDocumentFromTemplateOptions = {
|
||||
email: string;
|
||||
signingOrder?: number | null;
|
||||
}[];
|
||||
folderId?: string;
|
||||
prefillFields?: TFieldMetaPrefillFieldsSchema[];
|
||||
customDocumentDataId?: string;
|
||||
|
||||
@@ -276,7 +274,6 @@ export const createDocumentFromTemplate = async ({
|
||||
customDocumentDataId,
|
||||
override,
|
||||
requestMetadata,
|
||||
folderId,
|
||||
prefillFields,
|
||||
}: CreateDocumentFromTemplateOptions) => {
|
||||
const template = await prisma.template.findUnique({
|
||||
@@ -301,22 +298,6 @@ export const createDocumentFromTemplate = async ({
|
||||
});
|
||||
}
|
||||
|
||||
if (folderId) {
|
||||
const folder = await prisma.folder.findUnique({
|
||||
where: {
|
||||
id: folderId,
|
||||
type: FolderType.DOCUMENT,
|
||||
team: buildTeamWhereQuery({ teamId, userId }),
|
||||
},
|
||||
});
|
||||
|
||||
if (!folder) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Folder not found',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const settings = await getTeamSettings({
|
||||
userId,
|
||||
teamId,
|
||||
@@ -387,7 +368,6 @@ export const createDocumentFromTemplate = async ({
|
||||
externalId: externalId || template.externalId,
|
||||
templateId: template.id,
|
||||
userId,
|
||||
folderId,
|
||||
teamId: template.teamId,
|
||||
title: override?.title || template.title,
|
||||
documentDataId: documentData.id,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user