mirror of
https://github.com/documenso/documenso.git
synced 2025-11-10 04:22:32 +10:00
Compare commits
17 Commits
feat/docum
...
v1.12.2-rc
| Author | SHA1 | Date | |
|---|---|---|---|
| fe4d3ed1fd | |||
| b8d07fd1a6 | |||
| 49fabeb0ec | |||
| 5a5bfe6e34 | |||
| d7e5a9eec7 | |||
| adefac81e2 | |||
| 67501b45cf | |||
| 17b36ac8e4 | |||
| 80e452afa2 | |||
| 1cb9de8083 | |||
| 231ef9c27e | |||
| 6f35342a83 | |||
| a51110d276 | |||
| 7f81231467 | |||
| 439262fd02 | |||
| 93a184355b | |||
| 1dea0b8fab |
@ -136,3 +136,5 @@ 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,8 +214,6 @@ 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:
|
||||
@ -258,7 +256,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/web` folder.
|
||||
> 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.
|
||||
|
||||
### Run as a service
|
||||
|
||||
@ -308,7 +306,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 Next.js 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 Remix start command
|
||||
|
||||
For local docker run
|
||||
|
||||
|
||||
@ -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/web/resources/certificate.p12` (If the path does not exist, it needs to be created)
|
||||
5. Place the certificate `/apps/remix/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/web/src/app/(signing)/sign/[token]/no-longer-available.tsx:61
|
||||
#: apps/remix/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:web
|
||||
npm run build
|
||||
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/web` folder.
|
||||
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.
|
||||
</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/web
|
||||
WorkingDirectory=/var/www/documenso/apps/remix
|
||||
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.deleteDocument.useMutation();
|
||||
trpc.admin.document.delete.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.deleteUser.useMutation();
|
||||
trpc.admin.user.delete.useMutation();
|
||||
|
||||
const onDeleteAccount = async () => {
|
||||
try {
|
||||
|
||||
@ -37,7 +37,7 @@ export const AdminUserDisableDialog = ({
|
||||
const [email, setEmail] = useState('');
|
||||
|
||||
const { mutateAsync: disableUser, isPending: isDisablingUser } =
|
||||
trpc.admin.disableUser.useMutation();
|
||||
trpc.admin.user.disable.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.enableUser.useMutation();
|
||||
trpc.admin.user.enable.useMutation();
|
||||
|
||||
const onEnableAccount = async () => {
|
||||
try {
|
||||
|
||||
@ -0,0 +1,159 @@
|
||||
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.deleteDocument.useMutation({
|
||||
const { mutateAsync: deleteDocument, isPending } = trpcReact.document.delete.useMutation({
|
||||
onSuccess: async () => {
|
||||
void refreshLimits();
|
||||
|
||||
|
||||
@ -36,11 +36,12 @@ export const DocumentDuplicateDialog = ({
|
||||
|
||||
const team = useCurrentTeam();
|
||||
|
||||
const { data: document, isLoading } = trpcReact.document.getDocumentById.useQuery(
|
||||
const { data: document, isLoading } = trpcReact.document.get.useQuery(
|
||||
{
|
||||
documentId: id,
|
||||
},
|
||||
{
|
||||
queryHash: `document-duplicate-dialog-${id}`,
|
||||
enabled: open === true,
|
||||
},
|
||||
);
|
||||
@ -55,7 +56,7 @@ export const DocumentDuplicateDialog = ({
|
||||
const documentsPath = formatDocumentsPath(team.url);
|
||||
|
||||
const { mutateAsync: duplicateDocument, isPending: isDuplicateLoading } =
|
||||
trpcReact.document.duplicateDocument.useMutation({
|
||||
trpcReact.document.duplicate.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.resendDocument.useMutation();
|
||||
const { mutateAsync: resendDocument } = trpcReact.document.redistribute.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.createPasskeyRegistrationOptions.useMutation();
|
||||
trpc.auth.passkey.createRegistrationOptions.useMutation();
|
||||
|
||||
const { mutateAsync: createPasskey } = trpc.auth.createPasskey.useMutation();
|
||||
const { mutateAsync: createPasskey } = trpc.auth.passkey.create.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.deleteTokenById.useMutation({
|
||||
const { mutateAsync: deleteTokenMutation } = trpc.apiToken.delete.useMutation({
|
||||
onSuccess() {
|
||||
onDelete?.();
|
||||
},
|
||||
|
||||
@ -114,7 +114,7 @@ export const SignInForm = ({
|
||||
}, [returnTo]);
|
||||
|
||||
const { mutateAsync: createPasskeySigninOptions } =
|
||||
trpc.auth.createPasskeySigninOptions.useMutation();
|
||||
trpc.auth.passkey.createSigninOptions.useMutation();
|
||||
|
||||
const form = useForm<TSignInFormSchema>({
|
||||
values: {
|
||||
|
||||
138
apps/remix/app/components/forms/support-ticket-form.tsx
Normal file
138
apps/remix/app/components/forms/support-ticket-form.tsx
Normal file
@ -0,0 +1,138 @@
|
||||
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 { ZCreateTokenMutationSchema } from '@documenso/trpc/server/api-token-router/schema';
|
||||
import { ZCreateApiTokenRequestSchema } from '@documenso/trpc/server/api-token-router/create-api-token.types';
|
||||
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 = ZCreateTokenMutationSchema.pick({
|
||||
const ZCreateTokenFormSchema = ZCreateApiTokenRequestSchema.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.createToken.useMutation({
|
||||
const { mutateAsync: createTokenMutation } = trpc.apiToken.create.useMutation({
|
||||
onSuccess(data) {
|
||||
setNewlyCreatedToken(data);
|
||||
},
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
'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.searchDocuments.useQuery(
|
||||
trpcReact.document.search.useQuery(
|
||||
{
|
||||
query: search,
|
||||
},
|
||||
|
||||
@ -77,7 +77,7 @@ export const DocumentSigningAuthPasskey = ({
|
||||
});
|
||||
|
||||
const { mutateAsync: createPasskeyAuthenticationOptions } =
|
||||
trpc.auth.createPasskeyAuthenticationOptions.useMutation();
|
||||
trpc.auth.passkey.createAuthenticationOptions.useMutation();
|
||||
|
||||
const [formErrorCode, setFormErrorCode] = useState<string | null>(null);
|
||||
|
||||
|
||||
@ -93,7 +93,7 @@ export const DocumentSigningAuthProvider = ({
|
||||
[documentAuthOptions, recipient],
|
||||
);
|
||||
|
||||
const passkeyQuery = trpc.auth.findPasskeys.useQuery(
|
||||
const passkeyQuery = trpc.auth.passkey.find.useQuery(
|
||||
{
|
||||
perPage: MAXIMUM_PASSKEYS,
|
||||
},
|
||||
|
||||
@ -21,7 +21,7 @@ export const DocumentAuditLogDownloadButton = ({
|
||||
const { _ } = useLingui();
|
||||
|
||||
const { mutateAsync: downloadAuditLogs, isPending } =
|
||||
trpc.document.downloadAuditLogs.useMutation();
|
||||
trpc.document.auditLog.download.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.createDocument.useMutation();
|
||||
const { mutateAsync: createDocument } = trpc.document.create.useMutation();
|
||||
|
||||
const isUploadDisabled = remaining.documents === 0 || !user.emailVerified;
|
||||
|
||||
|
||||
@ -59,23 +59,22 @@ export const DocumentEditForm = ({
|
||||
|
||||
const utils = trpc.useUtils();
|
||||
|
||||
const { data: document, refetch: refetchDocument } =
|
||||
trpc.document.getDocumentWithDetailsById.useQuery(
|
||||
{
|
||||
documentId: initialDocument.id,
|
||||
},
|
||||
{
|
||||
initialData: initialDocument,
|
||||
...SKIP_QUERY_BATCH_META,
|
||||
},
|
||||
);
|
||||
const { data: document, refetch: refetchDocument } = trpc.document.get.useQuery(
|
||||
{
|
||||
documentId: initialDocument.id,
|
||||
},
|
||||
{
|
||||
initialData: initialDocument,
|
||||
...SKIP_QUERY_BATCH_META,
|
||||
},
|
||||
);
|
||||
|
||||
const { recipients, fields } = document;
|
||||
|
||||
const { mutateAsync: updateDocument } = trpc.document.updateDocument.useMutation({
|
||||
const { mutateAsync: updateDocument } = trpc.document.update.useMutation({
|
||||
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
|
||||
onSuccess: (newData) => {
|
||||
utils.document.getDocumentWithDetailsById.setData(
|
||||
utils.document.get.setData(
|
||||
{
|
||||
documentId: initialDocument.id,
|
||||
},
|
||||
@ -84,23 +83,10 @@ 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.getDocumentWithDetailsById.setData(
|
||||
utils.document.get.setData(
|
||||
{
|
||||
documentId: initialDocument.id,
|
||||
},
|
||||
@ -112,7 +98,7 @@ export const DocumentEditForm = ({
|
||||
const { mutateAsync: setRecipients } = trpc.recipient.setDocumentRecipients.useMutation({
|
||||
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
|
||||
onSuccess: ({ recipients: newRecipients }) => {
|
||||
utils.document.getDocumentWithDetailsById.setData(
|
||||
utils.document.get.setData(
|
||||
{
|
||||
documentId: initialDocument.id,
|
||||
},
|
||||
@ -121,10 +107,10 @@ export const DocumentEditForm = ({
|
||||
},
|
||||
});
|
||||
|
||||
const { mutateAsync: sendDocument } = trpc.document.sendDocument.useMutation({
|
||||
const { mutateAsync: sendDocument } = trpc.document.distribute.useMutation({
|
||||
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
|
||||
onSuccess: (newData) => {
|
||||
utils.document.getDocumentWithDetailsById.setData(
|
||||
utils.document.get.setData(
|
||||
{
|
||||
documentId: initialDocument.id,
|
||||
},
|
||||
@ -216,15 +202,11 @@ 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.getDocumentById.query(
|
||||
const documentWithData = await trpcClient.document.get.query(
|
||||
{
|
||||
documentId: document.id,
|
||||
},
|
||||
|
||||
@ -71,7 +71,7 @@ export const DocumentPageViewDropdown = ({ document }: DocumentPageViewDropdownP
|
||||
|
||||
const onDownloadClick = async () => {
|
||||
try {
|
||||
const documentWithData = await trpcClient.document.getDocumentById.query(
|
||||
const documentWithData = await trpcClient.document.get.query(
|
||||
{
|
||||
documentId: document.id,
|
||||
},
|
||||
@ -100,7 +100,7 @@ export const DocumentPageViewDropdown = ({ document }: DocumentPageViewDropdownP
|
||||
|
||||
const onDownloadOriginalClick = async () => {
|
||||
try {
|
||||
const documentWithData = await trpcClient.document.getDocumentById.query(
|
||||
const documentWithData = await trpcClient.document.get.query(
|
||||
{
|
||||
documentId: document.id,
|
||||
},
|
||||
|
||||
@ -32,7 +32,7 @@ export const DocumentPageViewRecentActivity = ({
|
||||
hasNextPage,
|
||||
fetchNextPage,
|
||||
isFetchingNextPage,
|
||||
} = trpc.document.findDocumentAuditLogs.useInfiniteQuery(
|
||||
} = trpc.document.auditLog.find.useInfiniteQuery(
|
||||
{
|
||||
documentId,
|
||||
filterForRecentActivity: true,
|
||||
|
||||
@ -52,7 +52,7 @@ export const DocumentUploadDropzone = ({ className }: DocumentUploadDropzoneProp
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const { mutateAsync: createDocument } = trpc.document.createDocument.useMutation();
|
||||
const { mutateAsync: createDocument } = trpc.document.create.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.updateDocument.useMutation();
|
||||
trpc.document.update.useMutation();
|
||||
|
||||
const onUpdateFieldsClick = async () => {
|
||||
if (type === 'document') {
|
||||
|
||||
@ -321,6 +321,19 @@ 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()}
|
||||
|
||||
@ -67,7 +67,7 @@ export const TemplatePageViewDocumentsTable = ({
|
||||
Object.fromEntries(searchParams ?? []),
|
||||
);
|
||||
|
||||
const { data, isLoading, isLoadingError } = trpc.document.findDocuments.useQuery(
|
||||
const { data, isLoading, isLoadingError } = trpc.document.find.useQuery(
|
||||
{
|
||||
templateId,
|
||||
page: parsedSearchParams.page,
|
||||
|
||||
@ -18,7 +18,7 @@ export const TemplatePageViewRecentActivity = ({
|
||||
templateId,
|
||||
documentRootPath,
|
||||
}: TemplatePageViewRecentActivityProps) => {
|
||||
const { data, isLoading, isLoadingError, refetch } = trpc.document.findDocuments.useQuery({
|
||||
const { data, isLoading, isLoadingError, refetch } = trpc.document.find.useQuery({
|
||||
templateId,
|
||||
orderByColumn: 'createdAt',
|
||||
orderByDirection: 'asc',
|
||||
|
||||
@ -52,7 +52,7 @@ export const AdminDocumentRecipientItemTable = ({ recipient }: RecipientItemProp
|
||||
},
|
||||
});
|
||||
|
||||
const { mutateAsync: updateRecipient } = trpc.admin.updateRecipient.useMutation();
|
||||
const { mutateAsync: updateRecipient } = trpc.admin.recipient.update.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.findDocumentAuditLogs.useQuery(
|
||||
const { data, isLoading, isLoadingError } = trpc.document.auditLog.find.useQuery(
|
||||
{
|
||||
documentId,
|
||||
page: parsedSearchParams.page,
|
||||
|
||||
@ -45,7 +45,7 @@ export const DocumentsTableActionButton = ({ row }: DocumentsTableActionButtonPr
|
||||
const onDownloadClick = async () => {
|
||||
try {
|
||||
const document = !recipient
|
||||
? await trpcClient.document.getDocumentById.query(
|
||||
? await trpcClient.document.get.query(
|
||||
{
|
||||
documentId: row.id,
|
||||
},
|
||||
|
||||
@ -77,7 +77,7 @@ export const DocumentsTableActionDropdown = ({
|
||||
const onDownloadClick = async () => {
|
||||
try {
|
||||
const document = !recipient
|
||||
? await trpcClient.document.getDocumentById.query({
|
||||
? await trpcClient.document.get.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.getDocumentById.query({
|
||||
? await trpcClient.document.get.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/schema';
|
||||
import type { TFindDocumentsResponse } from '@documenso/trpc/server/document-router/find-documents.types';
|
||||
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,7 +17,6 @@ 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';
|
||||
@ -32,12 +31,12 @@ import { useOptionalCurrentTeam } from '~/providers/team';
|
||||
import { StackAvatarsWithTooltip } from '../general/stack-avatars-with-tooltip';
|
||||
|
||||
export type DocumentsTableProps = {
|
||||
data?: TFindDocumentsResponse;
|
||||
data?: TFindInboxResponse;
|
||||
isLoading?: boolean;
|
||||
isLoadingError?: boolean;
|
||||
};
|
||||
|
||||
type DocumentsTableRow = TFindDocumentsResponse['data'][number];
|
||||
type DocumentsTableRow = TFindInboxResponse['data'][number];
|
||||
|
||||
export const InboxTable = () => {
|
||||
const { _, i18n } = useLingui();
|
||||
|
||||
@ -62,7 +62,7 @@ export const SettingsSecurityPasskeyTableActions = ({
|
||||
});
|
||||
|
||||
const { mutateAsync: updatePasskey, isPending: isUpdatingPasskey } =
|
||||
trpc.auth.updatePasskey.useMutation({
|
||||
trpc.auth.passkey.update.useMutation({
|
||||
onSuccess: () => {
|
||||
toast({
|
||||
title: _(msg`Success`),
|
||||
@ -84,7 +84,7 @@ export const SettingsSecurityPasskeyTableActions = ({
|
||||
});
|
||||
|
||||
const { mutateAsync: deletePasskey, isPending: isDeletingPasskey } =
|
||||
trpc.auth.deletePasskey.useMutation({
|
||||
trpc.auth.passkey.delete.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.findPasskeys.useQuery(
|
||||
const { data, isLoading, isLoadingError } = trpc.auth.passkey.find.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.resealDocument.useMutation({
|
||||
trpc.admin.document.reseal.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.findDocuments.useQuery(
|
||||
trpc.admin.document.find.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 { ZAdminUpdateProfileMutationSchema } from '@documenso/trpc/server/admin-router/schema';
|
||||
import { ZUpdateUserRequestSchema } from '@documenso/trpc/server/admin-router/update-user.types';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import {
|
||||
Form,
|
||||
@ -27,12 +27,13 @@ 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 = ZAdminUpdateProfileMutationSchema.omit({ id: true });
|
||||
const ZUserFormSchema = ZUpdateUserRequestSchema.omit({ id: true });
|
||||
|
||||
type TUserFormSchema = z.infer<typeof ZUserFormSchema>;
|
||||
|
||||
@ -84,7 +85,7 @@ const AdminUserPage = ({ user }: { user: User }) => {
|
||||
|
||||
const roles = user.roles ?? [];
|
||||
|
||||
const { mutateAsync: updateUserMutation } = trpc.admin.updateUser.useMutation();
|
||||
const { mutateAsync: updateUserMutation } = trpc.admin.user.update.useMutation();
|
||||
|
||||
const form = useForm<TUserFormSchema>({
|
||||
resolver: zodResolver(ZUserFormSchema),
|
||||
@ -219,10 +220,11 @@ const AdminUserPage = ({ user }: { user: User }) => {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-16 flex flex-col items-center gap-4">
|
||||
{user && <AdminUserDeleteDialog user={user} />}
|
||||
<div className="mt-16 flex flex-col gap-4">
|
||||
{user && user.twoFactorEnabled && <AdminUserResetTwoFactorDialog user={user} />}
|
||||
{user && user.disabled && <AdminUserEnableDialog userToEnable={user} />}
|
||||
{user && !user.disabled && <AdminUserDisableDialog userToDisable={user} />}
|
||||
{user && <AdminUserDeleteDialog user={user} />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
125
apps/remix/app/routes/_authenticated+/o.$orgUrl.support.tsx
Normal file
125
apps/remix/app/routes/_authenticated+/o.$orgUrl.support.tsx
Normal file
@ -0,0 +1,125 @@
|
||||
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,10 +12,8 @@ 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,
|
||||
ZFindDocumentsInternalRequestSchema,
|
||||
} from '@documenso/trpc/server/document-router/schema';
|
||||
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 { 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.getTokens.useQuery();
|
||||
const { data: tokens } = trpc.apiToken.getMany.useQuery();
|
||||
|
||||
const team = useOptionalCurrentTeam();
|
||||
|
||||
|
||||
@ -101,5 +101,5 @@
|
||||
"vite-plugin-babel-macros": "^1.0.6",
|
||||
"vite-tsconfig-paths": "^5.1.4"
|
||||
},
|
||||
"version": "1.12.2-rc.3"
|
||||
"version": "1.12.2-rc.5"
|
||||
}
|
||||
|
||||
@ -51,4 +51,4 @@ services:
|
||||
ports:
|
||||
- 3000:3000
|
||||
volumes:
|
||||
- ../../apps/web/example/cert.p12:/opt/documenso/cert.p12
|
||||
- ../../apps/remix/example/cert.p12:/opt/documenso/cert.p12
|
||||
|
||||
60
package-lock.json
generated
60
package-lock.json
generated
@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@documenso/root",
|
||||
"version": "1.12.2-rc.3",
|
||||
"version": "1.12.2-rc.5",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@documenso/root",
|
||||
"version": "1.12.2-rc.3",
|
||||
"version": "1.12.2-rc.5",
|
||||
"workspaces": [
|
||||
"apps/*",
|
||||
"packages/*"
|
||||
@ -89,7 +89,7 @@
|
||||
},
|
||||
"apps/remix": {
|
||||
"name": "@documenso/remix",
|
||||
"version": "1.12.2-rc.3",
|
||||
"version": "1.12.2-rc.5",
|
||||
"dependencies": {
|
||||
"@documenso/api": "*",
|
||||
"@documenso/assets": "*",
|
||||
@ -3522,6 +3522,15 @@
|
||||
"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",
|
||||
@ -11826,6 +11835,20 @@
|
||||
"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",
|
||||
@ -13235,7 +13258,6 @@
|
||||
"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",
|
||||
@ -13248,6 +13270,23 @@
|
||||
"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",
|
||||
@ -18771,7 +18810,6 @@
|
||||
"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",
|
||||
@ -19847,6 +19885,15 @@
|
||||
"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",
|
||||
@ -22329,7 +22376,6 @@
|
||||
"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": {
|
||||
@ -30570,7 +30616,6 @@
|
||||
"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"
|
||||
@ -36583,6 +36628,7 @@
|
||||
"@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,6 +1,6 @@
|
||||
{
|
||||
"private": true,
|
||||
"version": "1.12.2-rc.3",
|
||||
"version": "1.12.2-rc.5",
|
||||
"scripts": {
|
||||
"build": "turbo run build",
|
||||
"dev": "turbo run dev --filter=@documenso/remix",
|
||||
|
||||
@ -330,6 +330,7 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, {
|
||||
userId: user.id,
|
||||
teamId: team?.id,
|
||||
formValues: body.formValues,
|
||||
folderId: body.folderId,
|
||||
documentDataId: documentData.id,
|
||||
requestMetadata: metadata,
|
||||
});
|
||||
@ -736,6 +737,7 @@ 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,6 +136,12 @@ 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),
|
||||
@ -287,6 +293,12 @@ 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({
|
||||
|
||||
@ -144,10 +144,11 @@ test('[TEMPLATES]: use template', async ({ page }) => {
|
||||
await page.getByRole('button', { name: 'Use Template' }).click();
|
||||
|
||||
// Enter template values.
|
||||
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');
|
||||
// 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.getByRole('button', { name: 'Create as draft' }).click();
|
||||
await page.waitForURL(/\/t\/.+\/documents/);
|
||||
|
||||
@ -92,7 +92,11 @@ export const handleOAuthCallbackUrl = async (options: HandleOAuthCallbackUrlOpti
|
||||
providerAccountId: sub,
|
||||
},
|
||||
include: {
|
||||
user: true,
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@ -29,7 +29,13 @@ export const run = async ({
|
||||
id: documentId,
|
||||
},
|
||||
include: {
|
||||
user: true,
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
documentMeta: true,
|
||||
recipients: true,
|
||||
team: {
|
||||
|
||||
@ -39,7 +39,13 @@ export const run = async ({
|
||||
},
|
||||
},
|
||||
include: {
|
||||
user: true,
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -51,7 +57,13 @@ export const run = async ({
|
||||
organisationId: payload.organisationId,
|
||||
},
|
||||
include: {
|
||||
user: true,
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@ -39,7 +39,13 @@ export const run = async ({
|
||||
},
|
||||
},
|
||||
include: {
|
||||
user: true,
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -49,6 +55,11 @@ export const run = async ({
|
||||
where: {
|
||||
id: payload.memberUserId,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
},
|
||||
});
|
||||
|
||||
const { branding, emailLanguage, senderEmail } = await getEmailContext({
|
||||
|
||||
@ -38,7 +38,13 @@ export const run = async ({
|
||||
id: recipientId,
|
||||
},
|
||||
},
|
||||
user: true,
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
documentMeta: true,
|
||||
},
|
||||
});
|
||||
|
||||
@ -33,7 +33,13 @@ export const run = async ({
|
||||
id: documentId,
|
||||
},
|
||||
include: {
|
||||
user: true,
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
documentMeta: true,
|
||||
team: {
|
||||
select: {
|
||||
|
||||
@ -33,6 +33,7 @@
|
||||
"@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",
|
||||
|
||||
7
packages/lib/plain/client.ts
Normal file
7
packages/lib/plain/client.ts
Normal file
@ -0,0 +1,7 @@
|
||||
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') ?? '',
|
||||
});
|
||||
@ -16,7 +16,7 @@ export const updateUser = async ({ id, name, email, roles }: UpdateUserOptions)
|
||||
},
|
||||
});
|
||||
|
||||
return await prisma.user.update({
|
||||
await prisma.user.update({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import type { DocumentVisibility, TemplateMeta } from '@prisma/client';
|
||||
import {
|
||||
DocumentSource,
|
||||
FolderType,
|
||||
RecipientRole,
|
||||
SendStatus,
|
||||
SigningStatus,
|
||||
@ -14,7 +15,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 { TCreateDocumentV2Request } from '@documenso/trpc/server/document-router/schema';
|
||||
import type { TCreateDocumentTemporaryRequest } from '@documenso/trpc/server/document-router/create-document-temporary.types';
|
||||
|
||||
import type { TDocumentAccessAuthTypes, TDocumentActionAuthTypes } from '../../types/document-auth';
|
||||
import type { TDocumentFormValues } from '../../types/document-form-values';
|
||||
@ -44,7 +45,8 @@ export type CreateDocumentOptions = {
|
||||
globalAccessAuth?: TDocumentAccessAuthTypes[];
|
||||
globalActionAuth?: TDocumentActionAuthTypes[];
|
||||
formValues?: TDocumentFormValues;
|
||||
recipients: TCreateDocumentV2Request['recipients'];
|
||||
recipients: TCreateDocumentTemporaryRequest['recipients'];
|
||||
folderId?: string;
|
||||
};
|
||||
meta?: Partial<Omit<TemplateMeta, 'id' | 'templateId'>>;
|
||||
requestMetadata: ApiRequestMetadata;
|
||||
@ -59,7 +61,7 @@ export const createDocumentV2 = async ({
|
||||
meta,
|
||||
requestMetadata,
|
||||
}: CreateDocumentOptions) => {
|
||||
const { title, formValues } = data;
|
||||
const { title, formValues, folderId } = data;
|
||||
|
||||
const team = await prisma.team.findFirst({
|
||||
where: buildTeamWhereQuery({ teamId, userId }),
|
||||
@ -78,6 +80,22 @@ 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,
|
||||
@ -164,6 +182,7 @@ export const createDocumentV2 = async ({
|
||||
teamId,
|
||||
authOptions,
|
||||
visibility,
|
||||
folderId,
|
||||
formValues,
|
||||
source: DocumentSource.DOCUMENT,
|
||||
documentMeta: {
|
||||
|
||||
@ -49,6 +49,11 @@ export const findDocuments = async ({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
},
|
||||
});
|
||||
|
||||
let team = null;
|
||||
@ -267,7 +272,7 @@ export const findDocuments = async ({
|
||||
|
||||
const findDocumentsFilter = (
|
||||
status: ExtendedDocumentStatus,
|
||||
user: User,
|
||||
user: Pick<User, 'id' | 'email' | 'name'>,
|
||||
folderId?: string | null,
|
||||
) => {
|
||||
return match<ExtendedDocumentStatus, Prisma.DocumentWhereInput>(status)
|
||||
|
||||
@ -111,7 +111,7 @@ export const getDocumentWhereInput = async ({
|
||||
visibility: {
|
||||
in: teamVisibilityFilters,
|
||||
},
|
||||
teamId,
|
||||
teamId: team.id,
|
||||
},
|
||||
// Or, if they are a recipient of the document.
|
||||
{
|
||||
|
||||
@ -73,7 +73,13 @@ export const getDocumentAndSenderByToken = async ({
|
||||
},
|
||||
},
|
||||
include: {
|
||||
user: true,
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
documentData: true,
|
||||
documentMeta: true,
|
||||
recipients: {
|
||||
@ -90,9 +96,6 @@ 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.
|
||||
@ -120,7 +123,11 @@ export const getDocumentAndSenderByToken = async ({
|
||||
|
||||
return {
|
||||
...result,
|
||||
user,
|
||||
user: {
|
||||
id: result.user.id,
|
||||
email: result.user.email,
|
||||
name: result.user.name,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@ -7,14 +7,12 @@ 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,
|
||||
@ -25,7 +23,6 @@ export const getDocumentWithDetailsById = async ({
|
||||
const document = await prisma.document.findFirst({
|
||||
where: {
|
||||
...documentWhereInput,
|
||||
folderId,
|
||||
},
|
||||
include: {
|
||||
documentData: true,
|
||||
|
||||
@ -28,13 +28,7 @@ export async function rejectDocumentWithToken({
|
||||
documentId,
|
||||
},
|
||||
include: {
|
||||
document: {
|
||||
include: {
|
||||
user: true,
|
||||
recipients: true,
|
||||
documentMeta: true,
|
||||
},
|
||||
},
|
||||
document: true,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@ -33,7 +33,13 @@ export const sendCompletedEmail = async ({ documentId, requestMetadata }: SendDo
|
||||
documentData: true,
|
||||
documentMeta: true,
|
||||
recipients: true,
|
||||
user: true,
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
team: {
|
||||
select: {
|
||||
id: true,
|
||||
|
||||
@ -24,7 +24,13 @@ export const sendDeleteEmail = async ({ documentId, reason }: SendDeleteEmailOpt
|
||||
id: documentId,
|
||||
},
|
||||
include: {
|
||||
user: true,
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
documentMeta: true,
|
||||
},
|
||||
});
|
||||
|
||||
@ -30,7 +30,13 @@ export const superDeleteDocument = async ({ id, requestMetadata }: SuperDeleteDo
|
||||
include: {
|
||||
recipients: true,
|
||||
documentMeta: true,
|
||||
user: true,
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@ -3,6 +3,7 @@ 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.
|
||||
@ -27,7 +28,7 @@ export async function addRejectionStampToPdf(
|
||||
|
||||
for (let i = 0; i < pages.length; i++) {
|
||||
const page = pages[i];
|
||||
const { width, height } = page.getSize();
|
||||
const { width, height } = getPageSize(page);
|
||||
|
||||
// Draw the "REJECTED" text
|
||||
const rejectedTitleText = 'DOCUMENT REJECTED';
|
||||
|
||||
18
packages/lib/server-only/pdf/get-page-size.ts
Normal file
18
packages/lib/server-only/pdf/get-page-size.ts
Normal file
@ -0,0 +1,18 @@
|
||||
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,6 +33,7 @@ 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([
|
||||
@ -77,7 +78,7 @@ export const insertFieldInPDF = async (pdf: PDFDocument, field: FieldWithSignatu
|
||||
|
||||
const isPageRotatedToLandscape = pageRotationInDegrees === 90 || pageRotationInDegrees === 270;
|
||||
|
||||
let { width: pageWidth, height: pageHeight } = page.getSize();
|
||||
let { width: pageWidth, height: pageHeight } = getPageSize(page);
|
||||
|
||||
// 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,6 +26,7 @@ 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([
|
||||
@ -63,7 +64,7 @@ export const legacy_insertFieldInPDF = async (pdf: PDFDocument, field: FieldWith
|
||||
|
||||
const isPageRotatedToLandscape = pageRotationInDegrees === 90 || pageRotationInDegrees === 270;
|
||||
|
||||
let { width: pageWidth, height: pageHeight } = page.getSize();
|
||||
let { width: pageWidth, height: pageHeight } = getPageSize(page);
|
||||
|
||||
// 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
|
||||
});
|
||||
}
|
||||
|
||||
return await prisma.apiToken.delete({
|
||||
await prisma.apiToken.delete({
|
||||
where: {
|
||||
id,
|
||||
teamId,
|
||||
|
||||
@ -134,6 +134,9 @@ export const setDocumentRecipients = async ({
|
||||
existingRecipient.id === recipient.id || existingRecipient.email === recipient.email,
|
||||
);
|
||||
|
||||
const canPersistedRecipientBeModified =
|
||||
existing && canRecipientBeModified(existing, document.fields);
|
||||
|
||||
if (
|
||||
existing &&
|
||||
hasRecipientBeenChanged(existing, recipient) &&
|
||||
@ -147,6 +150,7 @@ export const setDocumentRecipients = async ({
|
||||
return {
|
||||
...recipient,
|
||||
_persisted: existing,
|
||||
canPersistedRecipientBeModified,
|
||||
};
|
||||
});
|
||||
|
||||
@ -162,6 +166,13 @@ 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,7 +1,5 @@
|
||||
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';
|
||||
@ -104,10 +102,7 @@ export const updateDocumentRecipients = async ({
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
hasRecipientBeenChanged(originalRecipient, recipient) &&
|
||||
!canRecipientBeModified(originalRecipient, document.fields)
|
||||
) {
|
||||
if (!canRecipientBeModified(originalRecipient, document.fields)) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message: 'Cannot modify a recipient who has already interacted with the document',
|
||||
});
|
||||
@ -203,9 +198,6 @@ export const updateDocumentRecipients = async ({
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* If you change this you MUST update the `hasRecipientBeenChanged` function.
|
||||
*/
|
||||
type RecipientData = {
|
||||
id: number;
|
||||
email?: string;
|
||||
@ -215,19 +207,3 @@ 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,7 +105,13 @@ export const createDocumentFromDirectTemplate = async ({
|
||||
directLink: true,
|
||||
templateDocumentData: true,
|
||||
templateMeta: true,
|
||||
user: true,
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@ -2,6 +2,7 @@ import type { DocumentDistributionMethod, DocumentSigningOrder } from '@prisma/c
|
||||
import {
|
||||
DocumentSource,
|
||||
type Field,
|
||||
FolderType,
|
||||
type Recipient,
|
||||
RecipientRole,
|
||||
SendStatus,
|
||||
@ -69,6 +70,7 @@ export type CreateDocumentFromTemplateOptions = {
|
||||
email: string;
|
||||
signingOrder?: number | null;
|
||||
}[];
|
||||
folderId?: string;
|
||||
prefillFields?: TFieldMetaPrefillFieldsSchema[];
|
||||
customDocumentDataId?: string;
|
||||
|
||||
@ -274,6 +276,7 @@ export const createDocumentFromTemplate = async ({
|
||||
customDocumentDataId,
|
||||
override,
|
||||
requestMetadata,
|
||||
folderId,
|
||||
prefillFields,
|
||||
}: CreateDocumentFromTemplateOptions) => {
|
||||
const template = await prisma.template.findUnique({
|
||||
@ -298,6 +301,22 @@ 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,
|
||||
@ -368,6 +387,7 @@ export const createDocumentFromTemplate = async ({
|
||||
externalId: externalId || template.externalId,
|
||||
templateId: template.id,
|
||||
userId,
|
||||
folderId,
|
||||
teamId: template.teamId,
|
||||
title: override?.title || template.title,
|
||||
documentDataId: documentData.id,
|
||||
|
||||
@ -24,7 +24,14 @@ export const resetPassword = async ({ token, password, requestMetadata }: ResetP
|
||||
token,
|
||||
},
|
||||
include: {
|
||||
user: true,
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
password: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
72
packages/lib/server-only/user/submit-support-ticket.ts
Normal file
72
packages/lib/server-only/user/submit-support-ticket.ts
Normal file
@ -0,0 +1,72 @@
|
||||
import { plainClient } from '@documenso/lib/plain/client';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { buildOrganisationWhereQuery } from '../../utils/organisations';
|
||||
import { getTeamById } from '../team/get-team';
|
||||
|
||||
type SubmitSupportTicketOptions = {
|
||||
subject: string;
|
||||
message: string;
|
||||
userId: number;
|
||||
organisationId: string;
|
||||
teamId?: number | null;
|
||||
};
|
||||
|
||||
export const submitSupportTicket = async ({
|
||||
subject,
|
||||
message,
|
||||
userId,
|
||||
organisationId,
|
||||
teamId,
|
||||
}: SubmitSupportTicketOptions) => {
|
||||
const user = await prisma.user.findFirst({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'User not found',
|
||||
});
|
||||
}
|
||||
|
||||
const organisation = await prisma.organisation.findFirst({
|
||||
where: buildOrganisationWhereQuery({
|
||||
organisationId,
|
||||
userId,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!organisation) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Organisation not found',
|
||||
});
|
||||
}
|
||||
|
||||
const team = teamId
|
||||
? await getTeamById({
|
||||
userId,
|
||||
teamId,
|
||||
})
|
||||
: null;
|
||||
|
||||
const customMessage = `
|
||||
Organisation: ${organisation.name} (${organisation.id})
|
||||
Team: ${team ? `${team.name} (${team.id})` : 'No team provided'}
|
||||
|
||||
${message}`;
|
||||
|
||||
const res = await plainClient.createThread({
|
||||
title: subject,
|
||||
customerIdentifier: { emailAddress: user.email },
|
||||
components: [{ componentText: { text: customMessage } }],
|
||||
});
|
||||
|
||||
if (res.error) {
|
||||
throw new Error(res.error.message);
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
@ -12,7 +12,13 @@ export type VerifyEmailProps = {
|
||||
export const verifyEmail = async ({ token }: VerifyEmailProps) => {
|
||||
const verificationToken = await prisma.verificationToken.findFirst({
|
||||
include: {
|
||||
user: true,
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
where: {
|
||||
token,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -4,7 +4,7 @@ import type { DocumentWithRecipients } from '@documenso/prisma/types/document-wi
|
||||
|
||||
export type MaskRecipientTokensForDocumentOptions<T extends DocumentWithRecipients> = {
|
||||
document: T;
|
||||
user?: User;
|
||||
user?: Pick<User, 'id' | 'email' | 'name'>;
|
||||
token?: string;
|
||||
};
|
||||
|
||||
|
||||
28
packages/trpc/server/admin-router/delete-document.ts
Normal file
28
packages/trpc/server/admin-router/delete-document.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import { sendDeleteEmail } from '@documenso/lib/server-only/document/send-delete-email';
|
||||
import { superDeleteDocument } from '@documenso/lib/server-only/document/super-delete-document';
|
||||
|
||||
import { adminProcedure } from '../trpc';
|
||||
import {
|
||||
ZDeleteDocumentRequestSchema,
|
||||
ZDeleteDocumentResponseSchema,
|
||||
} from './delete-document.types';
|
||||
|
||||
export const deleteDocumentRoute = adminProcedure
|
||||
.input(ZDeleteDocumentRequestSchema)
|
||||
.output(ZDeleteDocumentResponseSchema)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const { id, reason } = input;
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
|
||||
await sendDeleteEmail({ documentId: id, reason });
|
||||
|
||||
await superDeleteDocument({
|
||||
id,
|
||||
requestMetadata: ctx.metadata.requestMetadata,
|
||||
});
|
||||
});
|
||||
11
packages/trpc/server/admin-router/delete-document.types.ts
Normal file
11
packages/trpc/server/admin-router/delete-document.types.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const ZDeleteDocumentRequestSchema = z.object({
|
||||
id: z.number().min(1),
|
||||
reason: z.string(),
|
||||
});
|
||||
|
||||
export const ZDeleteDocumentResponseSchema = z.void();
|
||||
|
||||
export type TDeleteDocumentRequest = z.infer<typeof ZDeleteDocumentRequestSchema>;
|
||||
export type TDeleteDocumentResponse = z.infer<typeof ZDeleteDocumentResponseSchema>;
|
||||
19
packages/trpc/server/admin-router/delete-user.ts
Normal file
19
packages/trpc/server/admin-router/delete-user.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { deleteUser } from '@documenso/lib/server-only/user/delete-user';
|
||||
|
||||
import { adminProcedure } from '../trpc';
|
||||
import { ZDeleteUserRequestSchema, ZDeleteUserResponseSchema } from './delete-user.types';
|
||||
|
||||
export const deleteUserRoute = adminProcedure
|
||||
.input(ZDeleteUserRequestSchema)
|
||||
.output(ZDeleteUserResponseSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { id } = input;
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
|
||||
await deleteUser({ id });
|
||||
});
|
||||
10
packages/trpc/server/admin-router/delete-user.types.ts
Normal file
10
packages/trpc/server/admin-router/delete-user.types.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const ZDeleteUserRequestSchema = z.object({
|
||||
id: z.number().min(1),
|
||||
});
|
||||
|
||||
export const ZDeleteUserResponseSchema = z.void();
|
||||
|
||||
export type TDeleteUserRequest = z.infer<typeof ZDeleteUserRequestSchema>;
|
||||
export type TDeleteUserResponse = z.infer<typeof ZDeleteUserResponseSchema>;
|
||||
29
packages/trpc/server/admin-router/disable-user.ts
Normal file
29
packages/trpc/server/admin-router/disable-user.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { disableUser } from '@documenso/lib/server-only/user/disable-user';
|
||||
import { getUserById } from '@documenso/lib/server-only/user/get-user-by-id';
|
||||
|
||||
import { adminProcedure } from '../trpc';
|
||||
import { ZDisableUserRequestSchema, ZDisableUserResponseSchema } from './disable-user.types';
|
||||
|
||||
export const disableUserRoute = adminProcedure
|
||||
.input(ZDisableUserRequestSchema)
|
||||
.output(ZDisableUserResponseSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { id } = input;
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
|
||||
const user = await getUserById({ id }).catch(() => null);
|
||||
|
||||
if (!user) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'User not found',
|
||||
});
|
||||
}
|
||||
|
||||
await disableUser({ id });
|
||||
});
|
||||
10
packages/trpc/server/admin-router/disable-user.types.ts
Normal file
10
packages/trpc/server/admin-router/disable-user.types.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const ZDisableUserRequestSchema = z.object({
|
||||
id: z.number().min(1),
|
||||
});
|
||||
|
||||
export const ZDisableUserResponseSchema = z.void();
|
||||
|
||||
export type TDisableUserRequest = z.infer<typeof ZDisableUserRequestSchema>;
|
||||
export type TDisableUserResponse = z.infer<typeof ZDisableUserResponseSchema>;
|
||||
29
packages/trpc/server/admin-router/enable-user.ts
Normal file
29
packages/trpc/server/admin-router/enable-user.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { enableUser } from '@documenso/lib/server-only/user/enable-user';
|
||||
import { getUserById } from '@documenso/lib/server-only/user/get-user-by-id';
|
||||
|
||||
import { adminProcedure } from '../trpc';
|
||||
import { ZEnableUserRequestSchema, ZEnableUserResponseSchema } from './enable-user.types';
|
||||
|
||||
export const enableUserRoute = adminProcedure
|
||||
.input(ZEnableUserRequestSchema)
|
||||
.output(ZEnableUserResponseSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { id } = input;
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
|
||||
const user = await getUserById({ id }).catch(() => null);
|
||||
|
||||
if (!user) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'User not found',
|
||||
});
|
||||
}
|
||||
|
||||
await enableUser({ id });
|
||||
});
|
||||
10
packages/trpc/server/admin-router/enable-user.types.ts
Normal file
10
packages/trpc/server/admin-router/enable-user.types.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const ZEnableUserRequestSchema = z.object({
|
||||
id: z.number().min(1),
|
||||
});
|
||||
|
||||
export const ZEnableUserResponseSchema = z.void();
|
||||
|
||||
export type TEnableUserRequest = z.infer<typeof ZEnableUserRequestSchema>;
|
||||
export type TEnableUserResponse = z.infer<typeof ZEnableUserResponseSchema>;
|
||||
13
packages/trpc/server/admin-router/find-documents.ts
Normal file
13
packages/trpc/server/admin-router/find-documents.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { findDocuments } from '@documenso/lib/server-only/admin/get-all-documents';
|
||||
|
||||
import { adminProcedure } from '../trpc';
|
||||
import { ZFindDocumentsRequestSchema, ZFindDocumentsResponseSchema } from './find-documents.types';
|
||||
|
||||
export const findDocumentsRoute = adminProcedure
|
||||
.input(ZFindDocumentsRequestSchema)
|
||||
.output(ZFindDocumentsResponseSchema)
|
||||
.query(async ({ input }) => {
|
||||
const { query, page, perPage } = input;
|
||||
|
||||
return await findDocuments({ query, page, perPage });
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user