diff --git a/.env.example b/.env.example
index 87ad09a63..7b8872b69 100644
--- a/.env.example
+++ b/.env.example
@@ -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=
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 000000000..b6fba867d
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,57 @@
+# Agent Guidelines for Documenso
+
+## Build/Test/Lint Commands
+
+- `npm run build` - Build all packages
+- `npm run lint` - Lint all packages
+- `npm run lint:fix` - Auto-fix linting issues
+- `npm run test:e2e` - Run E2E tests with Playwright
+- `npm run test:dev -w @documenso/app-tests` - Run single E2E test in dev mode
+- `npm run test-ui:dev -w @documenso/app-tests` - Run E2E tests with UI
+- `npm run format` - Format code with Prettier
+- `npm run dev` - Start development server for Remix app
+
+## Code Style Guidelines
+
+- Use TypeScript for all code; prefer `type` over `interface`
+- Use functional components with `const Component = () => {}`
+- Never use classes; prefer functional/declarative patterns
+- Use descriptive variable names with auxiliary verbs (isLoading, hasError)
+- Directory names: lowercase with dashes (auth-wizard)
+- Use named exports for components
+- Never use 'use client' directive
+- Never use 1-line if statements
+- Structure files: exported component, subcomponents, helpers, static content, types
+
+## Error Handling & Validation
+
+- Use custom AppError class when throwing errors
+- When catching errors on the frontend use `const error = AppError.parse(error)` to get the error code
+- Use early returns and guard clauses
+- Use Zod for form validation and react-hook-form for forms
+- Use error boundaries for unexpected errors
+
+## UI & Styling
+
+- Use Shadcn UI, Radix, and Tailwind CSS with mobile-first approach
+- Use `
+ >
+ );
+};
diff --git a/apps/remix/app/components/forms/token.tsx b/apps/remix/app/components/forms/token.tsx
index 13b7da0b1..1b8e4f3e3 100644
--- a/apps/remix/app/components/forms/token.tsx
+++ b/apps/remix/app/components/forms/token.tsx
@@ -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();
const [noExpirationDate, setNoExpirationDate] = useState(false);
- const { mutateAsync: createTokenMutation } = trpc.apiToken.createToken.useMutation({
+ const { mutateAsync: createTokenMutation } = trpc.apiToken.create.useMutation({
onSuccess(data) {
setNewlyCreatedToken(data);
},
diff --git a/apps/remix/app/components/general/admin-monthly-active-user-charts.tsx b/apps/remix/app/components/general/admin-monthly-active-user-charts.tsx
index 048d9599c..c6f137c3d 100644
--- a/apps/remix/app/components/general/admin-monthly-active-user-charts.tsx
+++ b/apps/remix/app/components/general/admin-monthly-active-user-charts.tsx
@@ -1,5 +1,3 @@
-'use client';
-
import { DateTime } from 'luxon';
import type { TooltipProps } from 'recharts';
import { Bar, BarChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts';
diff --git a/apps/remix/app/components/general/app-command-menu.tsx b/apps/remix/app/components/general/app-command-menu.tsx
index 9e9724d2e..2305bc6af 100644
--- a/apps/remix/app/components/general/app-command-menu.tsx
+++ b/apps/remix/app/components/general/app-command-menu.tsx
@@ -64,7 +64,7 @@ export function AppCommandMenu({ open, onOpenChange }: AppCommandMenuProps) {
const [pages, setPages] = useState([]);
const { data: searchDocumentsData, isPending: isSearchingDocuments } =
- trpcReact.document.searchDocuments.useQuery(
+ trpcReact.document.search.useQuery(
{
query: search,
},
diff --git a/apps/remix/app/components/general/direct-template/direct-template-signing-form.tsx b/apps/remix/app/components/general/direct-template/direct-template-signing-form.tsx
index da38f51c4..8f78ae754 100644
--- a/apps/remix/app/components/general/direct-template/direct-template-signing-form.tsx
+++ b/apps/remix/app/components/general/direct-template/direct-template-signing-form.tsx
@@ -79,6 +79,8 @@ export const DirectTemplateSigningForm = ({
const [validateUninsertedFields, setValidateUninsertedFields] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
+ const highestPageNumber = Math.max(...localFields.map((field) => field.page));
+
const fieldsRequiringValidation = useMemo(() => {
return localFields.filter((field) => isFieldUnsignedAndRequired(field));
}, [localFields]);
@@ -221,7 +223,9 @@ export const DirectTemplateSigningForm = ({
-
+
{validateUninsertedFields && uninsertedFields[0] && (
Click to insert field
diff --git a/apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx b/apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
index 22e641713..930738c74 100644
--- a/apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
+++ b/apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
@@ -77,7 +77,7 @@ export const DocumentSigningAuthPasskey = ({
});
const { mutateAsync: createPasskeyAuthenticationOptions } =
- trpc.auth.createPasskeyAuthenticationOptions.useMutation();
+ trpc.auth.passkey.createAuthenticationOptions.useMutation();
const [formErrorCode, setFormErrorCode] = useState(null);
diff --git a/apps/remix/app/components/general/document-signing/document-signing-auth-provider.tsx b/apps/remix/app/components/general/document-signing/document-signing-auth-provider.tsx
index 42e5ffd5b..c3b1be53e 100644
--- a/apps/remix/app/components/general/document-signing/document-signing-auth-provider.tsx
+++ b/apps/remix/app/components/general/document-signing/document-signing-auth-provider.tsx
@@ -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,
},
diff --git a/apps/remix/app/components/general/document-signing/document-signing-page-view.tsx b/apps/remix/app/components/general/document-signing/document-signing-page-view.tsx
index a1bf3d24e..cbbdc7926 100644
--- a/apps/remix/app/components/general/document-signing/document-signing-page-view.tsx
+++ b/apps/remix/app/components/general/document-signing/document-signing-page-view.tsx
@@ -78,6 +78,8 @@ export const DocumentSigningPageView = ({
const targetSigner =
recipient.role === RecipientRole.ASSISTANT && selectedSigner ? selectedSigner : null;
+ const highestPageNumber = Math.max(...fields.map((field) => field.page));
+
return (
@@ -224,7 +226,9 @@ export const DocumentSigningPageView = ({
)}
-
+
{fields
.filter(
(field) =>
diff --git a/apps/remix/app/components/general/document/document-audit-log-download-button.tsx b/apps/remix/app/components/general/document/document-audit-log-download-button.tsx
index fb531eb37..77e90eff8 100644
--- a/apps/remix/app/components/general/document/document-audit-log-download-button.tsx
+++ b/apps/remix/app/components/general/document/document-audit-log-download-button.tsx
@@ -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 {
diff --git a/apps/remix/app/components/general/document/document-drop-zone-wrapper.tsx b/apps/remix/app/components/general/document/document-drop-zone-wrapper.tsx
index 16a52194e..e5fe18636 100644
--- a/apps/remix/app/components/general/document/document-drop-zone-wrapper.tsx
+++ b/apps/remix/app/components/general/document/document-drop-zone-wrapper.tsx
@@ -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;
diff --git a/apps/remix/app/components/general/document/document-edit-form.tsx b/apps/remix/app/components/general/document/document-edit-form.tsx
index 7351fad4b..9cb7debf5 100644
--- a/apps/remix/app/components/general/document/document-edit-form.tsx
+++ b/apps/remix/app/components/general/document/document-edit-form.tsx
@@ -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,
},
@@ -218,15 +204,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,
},
}),
diff --git a/apps/remix/app/components/general/document/document-page-view-button.tsx b/apps/remix/app/components/general/document/document-page-view-button.tsx
index 55f6d85c2..e5fea4d2b 100644
--- a/apps/remix/app/components/general/document/document-page-view-button.tsx
+++ b/apps/remix/app/components/general/document/document-page-view-button.tsx
@@ -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,
},
diff --git a/apps/remix/app/components/general/document/document-page-view-dropdown.tsx b/apps/remix/app/components/general/document/document-page-view-dropdown.tsx
index c4043de3a..326c7553c 100644
--- a/apps/remix/app/components/general/document/document-page-view-dropdown.tsx
+++ b/apps/remix/app/components/general/document/document-page-view-dropdown.tsx
@@ -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,
},
diff --git a/apps/remix/app/components/general/document/document-page-view-recent-activity.tsx b/apps/remix/app/components/general/document/document-page-view-recent-activity.tsx
index 10beae93b..abeeacbc4 100644
--- a/apps/remix/app/components/general/document/document-page-view-recent-activity.tsx
+++ b/apps/remix/app/components/general/document/document-page-view-recent-activity.tsx
@@ -32,7 +32,7 @@ export const DocumentPageViewRecentActivity = ({
hasNextPage,
fetchNextPage,
isFetchingNextPage,
- } = trpc.document.findDocumentAuditLogs.useInfiniteQuery(
+ } = trpc.document.auditLog.find.useInfiniteQuery(
{
documentId,
filterForRecentActivity: true,
diff --git a/apps/remix/app/components/general/document/document-upload.tsx b/apps/remix/app/components/general/document/document-upload.tsx
index c21fcc5f0..b86a12ecc 100644
--- a/apps/remix/app/components/general/document/document-upload.tsx
+++ b/apps/remix/app/components/general/document/document-upload.tsx
@@ -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) {
diff --git a/apps/remix/app/components/general/legacy-field-warning-popover.tsx b/apps/remix/app/components/general/legacy-field-warning-popover.tsx
index 6bd489c27..3165b1be7 100644
--- a/apps/remix/app/components/general/legacy-field-warning-popover.tsx
+++ b/apps/remix/app/components/general/legacy-field-warning-popover.tsx
@@ -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') {
diff --git a/apps/remix/app/components/general/org-menu-switcher.tsx b/apps/remix/app/components/general/org-menu-switcher.tsx
index 5ba8dcea4..c6c7c0040 100644
--- a/apps/remix/app/components/general/org-menu-switcher.tsx
+++ b/apps/remix/app/components/general/org-menu-switcher.tsx
@@ -321,6 +321,19 @@ export const OrgMenuSwitcher = () => {
Language
+ {currentOrganisation && (
+
+
+ Support
+
+
+ )}
+
authClient.signOut()}
diff --git a/apps/remix/app/components/general/template/template-page-view-documents-table.tsx b/apps/remix/app/components/general/template/template-page-view-documents-table.tsx
index bef9189d5..6072a8846 100644
--- a/apps/remix/app/components/general/template/template-page-view-documents-table.tsx
+++ b/apps/remix/app/components/general/template/template-page-view-documents-table.tsx
@@ -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,
diff --git a/apps/remix/app/components/general/template/template-page-view-recent-activity.tsx b/apps/remix/app/components/general/template/template-page-view-recent-activity.tsx
index e0f3f67c9..9b39a27a8 100644
--- a/apps/remix/app/components/general/template/template-page-view-recent-activity.tsx
+++ b/apps/remix/app/components/general/template/template-page-view-recent-activity.tsx
@@ -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',
diff --git a/apps/remix/app/components/tables/admin-document-recipient-item-table.tsx b/apps/remix/app/components/tables/admin-document-recipient-item-table.tsx
index 58e25b179..89a9366b1 100644
--- a/apps/remix/app/components/tables/admin-document-recipient-item-table.tsx
+++ b/apps/remix/app/components/tables/admin-document-recipient-item-table.tsx
@@ -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 [
diff --git a/apps/remix/app/components/tables/document-logs-table.tsx b/apps/remix/app/components/tables/document-logs-table.tsx
index 8cdae26d5..a042c6a44 100644
--- a/apps/remix/app/components/tables/document-logs-table.tsx
+++ b/apps/remix/app/components/tables/document-logs-table.tsx
@@ -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,
diff --git a/apps/remix/app/components/tables/documents-table-action-button.tsx b/apps/remix/app/components/tables/documents-table-action-button.tsx
index ef1323479..8a73c8bf8 100644
--- a/apps/remix/app/components/tables/documents-table-action-button.tsx
+++ b/apps/remix/app/components/tables/documents-table-action-button.tsx
@@ -46,7 +46,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,
},
diff --git a/apps/remix/app/components/tables/documents-table-action-dropdown.tsx b/apps/remix/app/components/tables/documents-table-action-dropdown.tsx
index 1186afb18..8114c6cc1 100644
--- a/apps/remix/app/components/tables/documents-table-action-dropdown.tsx
+++ b/apps/remix/app/components/tables/documents-table-action-dropdown.tsx
@@ -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({
diff --git a/apps/remix/app/components/tables/documents-table.tsx b/apps/remix/app/components/tables/documents-table.tsx
index fa5be7d2d..a003f4d0d 100644
--- a/apps/remix/app/components/tables/documents-table.tsx
+++ b/apps/remix/app/components/tables/documents-table.tsx
@@ -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';
diff --git a/apps/remix/app/components/tables/inbox-table.tsx b/apps/remix/app/components/tables/inbox-table.tsx
index d63067e70..98f45dd3b 100644
--- a/apps/remix/app/components/tables/inbox-table.tsx
+++ b/apps/remix/app/components/tables/inbox-table.tsx
@@ -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();
diff --git a/apps/remix/app/components/tables/settings-security-passkey-table-actions.tsx b/apps/remix/app/components/tables/settings-security-passkey-table-actions.tsx
index e86800149..835bebf55 100644
--- a/apps/remix/app/components/tables/settings-security-passkey-table-actions.tsx
+++ b/apps/remix/app/components/tables/settings-security-passkey-table-actions.tsx
@@ -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`),
diff --git a/apps/remix/app/components/tables/settings-security-passkey-table.tsx b/apps/remix/app/components/tables/settings-security-passkey-table.tsx
index 3d202900a..b2fe09621 100644
--- a/apps/remix/app/components/tables/settings-security-passkey-table.tsx
+++ b/apps/remix/app/components/tables/settings-security-passkey-table.tsx
@@ -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,
diff --git a/apps/remix/app/routes/_authenticated+/admin+/documents.$id.tsx b/apps/remix/app/routes/_authenticated+/admin+/documents.$id.tsx
index db1b4d0e8..623ac0938 100644
--- a/apps/remix/app/routes/_authenticated+/admin+/documents.$id.tsx
+++ b/apps/remix/app/routes/_authenticated+/admin+/documents.$id.tsx
@@ -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`),
diff --git a/apps/remix/app/routes/_authenticated+/admin+/documents._index.tsx b/apps/remix/app/routes/_authenticated+/admin+/documents._index.tsx
index 35640b28e..27b7509f2 100644
--- a/apps/remix/app/routes/_authenticated+/admin+/documents._index.tsx
+++ b/apps/remix/app/routes/_authenticated+/admin+/documents._index.tsx
@@ -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,
diff --git a/apps/remix/app/routes/_authenticated+/admin+/users.$id.tsx b/apps/remix/app/routes/_authenticated+/admin+/users.$id.tsx
index fb05128a6..3cd0f5853 100644
--- a/apps/remix/app/routes/_authenticated+/admin+/users.$id.tsx
+++ b/apps/remix/app/routes/_authenticated+/admin+/users.$id.tsx
@@ -2,14 +2,14 @@ import { zodResolver } from '@hookform/resolvers/zod';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
-import type { User } from '@prisma/client';
import { useForm } from 'react-hook-form';
import { useRevalidator } from 'react-router';
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 type { TGetUserResponse } from '@documenso/trpc/server/admin-router/get-user.types';
+import { ZUpdateUserRequestSchema } from '@documenso/trpc/server/admin-router/update-user.types';
import { Button } from '@documenso/ui/primitives/button';
import {
Form,
@@ -27,17 +27,18 @@ 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;
export default function UserPage({ params }: { params: { id: number } }) {
- const { data: user, isLoading: isLoadingUser } = trpc.profile.getUser.useQuery(
+ const { data: user, isLoading: isLoadingUser } = trpc.admin.user.get.useQuery(
{
id: Number(params.id),
},
@@ -77,14 +78,14 @@ export default function UserPage({ params }: { params: { id: number } }) {
return ;
}
-const AdminUserPage = ({ user }: { user: User }) => {
+const AdminUserPage = ({ user }: { user: TGetUserResponse }) => {
const { _ } = useLingui();
const { toast } = useToast();
const { revalidate } = useRevalidator();
const roles = user.roles ?? [];
- const { mutateAsync: updateUserMutation } = trpc.admin.updateUser.useMutation();
+ const { mutateAsync: updateUserMutation } = trpc.admin.user.update.useMutation();
const form = useForm({
resolver: zodResolver(ZUserFormSchema),
@@ -219,10 +220,11 @@ const AdminUserPage = ({ user }: { user: User }) => {
/>
-
- {user &&
}
+
+ {user && user.twoFactorEnabled &&
}
{user && user.disabled &&
}
{user && !user.disabled &&
}
+ {user &&
}
);
diff --git a/apps/remix/app/routes/_authenticated+/o.$orgUrl.support.tsx b/apps/remix/app/routes/_authenticated+/o.$orgUrl.support.tsx
new file mode 100644
index 000000000..dc27400d3
--- /dev/null
+++ b/apps/remix/app/routes/_authenticated+/o.$orgUrl.support.tsx
@@ -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 (
+
+
+
+
+ Support
+
+
+
+ Your current plan includes the following support channels:
+
+
+
+
+
+
+
+ Documentation
+
+
+
+ Read our documentation to get started with Documenso.
+
+
+
+
+
+
+ Discord
+
+
+
+
+ Join our community on{' '}
+
+ Discord
+ {' '}
+ for community support and discussion.
+
+
+
+ {organisation && IS_BILLING_ENABLED() && subscriptionStatus && (
+ <>
+
+
+
+ Contact us
+
+
+ We'll get back to you as soon as possible via email.
+
+
+ {!showForm ? (
+ setShowForm(true)}>
+ Create a support ticket
+
+ ) : (
+
+ )}
+
+
+ >
+ )}
+
+
+
+ );
+}
diff --git a/apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.$id._index.tsx b/apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.$id._index.tsx
index 75592696c..84c9c6883 100644
--- a/apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.$id._index.tsx
+++ b/apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.$id._index.tsx
@@ -67,6 +67,7 @@ export async function loader({ params, request }: Route.LoaderArgs) {
const documentVisibility = document?.visibility;
const currentTeamMemberRole = team.currentTeamRole;
const isRecipient = document?.recipients.find((recipient) => recipient.email === user.email);
+
let canAccessDocument = true;
if (!isRecipient && document?.userId !== user.id) {
diff --git a/apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents._index.tsx b/apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents._index.tsx
index 02ae85896..eac87bae7 100644
--- a/apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents._index.tsx
+++ b/apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents._index.tsx
@@ -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';
diff --git a/apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.tokens.tsx b/apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.tokens.tsx
index 1d5f86bd5..584a71197 100644
--- a/apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.tokens.tsx
+++ b/apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.tokens.tsx
@@ -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();
diff --git a/apps/remix/app/routes/_unauthenticated+/organisation.invite.$token.tsx b/apps/remix/app/routes/_unauthenticated+/organisation.invite.$token.tsx
index 5fa253ca3..39acc2194 100644
--- a/apps/remix/app/routes/_unauthenticated+/organisation.invite.$token.tsx
+++ b/apps/remix/app/routes/_unauthenticated+/organisation.invite.$token.tsx
@@ -45,6 +45,9 @@ export async function loader({ params, request }: Route.LoaderArgs) {
mode: 'insensitive',
},
},
+ select: {
+ id: true,
+ },
});
// Directly convert the team member invite to a team member if they already have an account.
diff --git a/apps/remix/package.json b/apps/remix/package.json
index a0c20d515..a97fff5f3 100644
--- a/apps/remix/package.json
+++ b/apps/remix/package.json
@@ -101,5 +101,5 @@
"vite-plugin-babel-macros": "^1.0.6",
"vite-tsconfig-paths": "^5.1.4"
},
- "version": "1.12.2-rc.4"
+ "version": "1.12.2-rc.6"
}
diff --git a/docker/testing/compose.yml b/docker/testing/compose.yml
index 28ec055c1..110e9da6b 100644
--- a/docker/testing/compose.yml
+++ b/docker/testing/compose.yml
@@ -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
diff --git a/package-lock.json b/package-lock.json
index e8455705d..647e4c43c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "@documenso/root",
- "version": "1.12.2-rc.4",
+ "version": "1.12.2-rc.6",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@documenso/root",
- "version": "1.12.2-rc.4",
+ "version": "1.12.2-rc.6",
"workspaces": [
"apps/*",
"packages/*"
@@ -89,7 +89,7 @@
},
"apps/remix": {
"name": "@documenso/remix",
- "version": "1.12.2-rc.4",
+ "version": "1.12.2-rc.6",
"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",
diff --git a/package.json b/package.json
index 1f1a39496..9e6654c0e 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"private": true,
- "version": "1.12.2-rc.4",
+ "version": "1.12.2-rc.6",
"scripts": {
"build": "turbo run build",
"dev": "turbo run dev --filter=@documenso/remix",
diff --git a/packages/api/v1/implementation.ts b/packages/api/v1/implementation.ts
index e8afd9af4..e9fcbf4d8 100644
--- a/packages/api/v1/implementation.ts
+++ b/packages/api/v1/implementation.ts
@@ -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,
diff --git a/packages/api/v1/schema.ts b/packages/api/v1/schema.ts
index 81f1b7a79..c33120ae3 100644
--- a/packages/api/v1/schema.ts
+++ b/packages/api/v1/schema.ts
@@ -136,6 +136,12 @@ export type TUploadDocumentSuccessfulSchema = z.infer {
- await prisma.user.findFirstOrThrow({
- where: {
- id,
- },
- });
-
- return await prisma.user.update({
+ await prisma.user.update({
where: {
id,
},
diff --git a/packages/lib/server-only/document/create-document-v2.ts b/packages/lib/server-only/document/create-document-v2.ts
index 8e9cebbe0..a35d9e8d2 100644
--- a/packages/lib/server-only/document/create-document-v2.ts
+++ b/packages/lib/server-only/document/create-document-v2.ts
@@ -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';
@@ -45,7 +46,8 @@ export type CreateDocumentOptions = {
globalAccessAuth?: TDocumentAccessAuthTypes[];
globalActionAuth?: TDocumentActionAuthTypes[];
formValues?: TDocumentFormValues;
- recipients: TCreateDocumentV2Request['recipients'];
+ recipients: TCreateDocumentTemporaryRequest['recipients'];
+ folderId?: string;
expiryAmount?: number;
expiryUnit?: string;
};
@@ -62,7 +64,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 }),
@@ -81,6 +83,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,
@@ -167,6 +185,7 @@ export const createDocumentV2 = async ({
teamId,
authOptions,
visibility,
+ folderId,
formValues,
source: DocumentSource.DOCUMENT,
documentMeta: {
diff --git a/packages/lib/server-only/document/find-documents.ts b/packages/lib/server-only/document/find-documents.ts
index e6b2bde42..97dd07582 100644
--- a/packages/lib/server-only/document/find-documents.ts
+++ b/packages/lib/server-only/document/find-documents.ts
@@ -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,
folderId?: string | null,
) => {
return match(status)
diff --git a/packages/lib/server-only/document/get-document-by-token.ts b/packages/lib/server-only/document/get-document-by-token.ts
index b91427328..e062a7824 100644
--- a/packages/lib/server-only/document/get-document-by-token.ts
+++ b/packages/lib/server-only/document/get-document-by-token.ts
@@ -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,
+ },
};
};
diff --git a/packages/lib/server-only/document/get-document-with-details-by-id.ts b/packages/lib/server-only/document/get-document-with-details-by-id.ts
index a889c410b..2a93156a7 100644
--- a/packages/lib/server-only/document/get-document-with-details-by-id.ts
+++ b/packages/lib/server-only/document/get-document-with-details-by-id.ts
@@ -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,
diff --git a/packages/lib/server-only/document/reject-document-with-token.ts b/packages/lib/server-only/document/reject-document-with-token.ts
index f0c5764ef..2de1d0e81 100644
--- a/packages/lib/server-only/document/reject-document-with-token.ts
+++ b/packages/lib/server-only/document/reject-document-with-token.ts
@@ -28,13 +28,7 @@ export async function rejectDocumentWithToken({
documentId,
},
include: {
- document: {
- include: {
- user: true,
- recipients: true,
- documentMeta: true,
- },
- },
+ document: true,
},
});
diff --git a/packages/lib/server-only/document/send-completed-email.ts b/packages/lib/server-only/document/send-completed-email.ts
index b4392f891..b5e5ba256 100644
--- a/packages/lib/server-only/document/send-completed-email.ts
+++ b/packages/lib/server-only/document/send-completed-email.ts
@@ -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,
diff --git a/packages/lib/server-only/document/send-delete-email.ts b/packages/lib/server-only/document/send-delete-email.ts
index 8d786ff0b..e76ac636b 100644
--- a/packages/lib/server-only/document/send-delete-email.ts
+++ b/packages/lib/server-only/document/send-delete-email.ts
@@ -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,
},
});
diff --git a/packages/lib/server-only/document/super-delete-document.ts b/packages/lib/server-only/document/super-delete-document.ts
index 87ab1a98f..ab6321c5e 100644
--- a/packages/lib/server-only/document/super-delete-document.ts
+++ b/packages/lib/server-only/document/super-delete-document.ts
@@ -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,
+ },
+ },
},
});
diff --git a/packages/lib/server-only/email/get-email-context.ts b/packages/lib/server-only/email/get-email-context.ts
index 2dcf909fe..a9f924980 100644
--- a/packages/lib/server-only/email/get-email-context.ts
+++ b/packages/lib/server-only/email/get-email-context.ts
@@ -1,3 +1,5 @@
+import { P, match } from 'ts-pattern';
+
import type { BrandingSettings } from '@documenso/email/providers/branding';
import { prisma } from '@documenso/prisma';
import type {
@@ -104,7 +106,12 @@ export const getEmailContext = async (
}
const replyToEmail = meta?.emailReplyTo || emailContext.settings.emailReplyTo || undefined;
- const senderEmailId = meta?.emailId === null ? null : emailContext.settings.emailId;
+
+ const senderEmailId = match(meta?.emailId)
+ .with(P.string, (emailId) => emailId) // Explicit string means to use the provided email ID.
+ .with(undefined, () => emailContext.settings.emailId) // Undefined means to use the inherited email ID.
+ .with(null, () => null) // Explicit null means to use the Documenso email.
+ .exhaustive();
const foundSenderEmail = emailContext.allowedEmails.find((email) => email.id === senderEmailId);
diff --git a/packages/lib/server-only/pdf/add-rejection-stamp-to-pdf.ts b/packages/lib/server-only/pdf/add-rejection-stamp-to-pdf.ts
index fe4853a5b..c0f2b123a 100644
--- a/packages/lib/server-only/pdf/add-rejection-stamp-to-pdf.ts
+++ b/packages/lib/server-only/pdf/add-rejection-stamp-to-pdf.ts
@@ -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';
diff --git a/packages/lib/server-only/pdf/get-page-size.ts b/packages/lib/server-only/pdf/get-page-size.ts
new file mode 100644
index 000000000..76c5babf7
--- /dev/null
+++ b/packages/lib/server-only/pdf/get-page-size.ts
@@ -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;
+};
diff --git a/packages/lib/server-only/pdf/insert-field-in-pdf.ts b/packages/lib/server-only/pdf/insert-field-in-pdf.ts
index fe0a5749a..9b2b8183b 100644
--- a/packages/lib/server-only/pdf/insert-field-in-pdf.ts
+++ b/packages/lib/server-only/pdf/insert-field-in-pdf.ts
@@ -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.
diff --git a/packages/lib/server-only/pdf/legacy-insert-field-in-pdf.ts b/packages/lib/server-only/pdf/legacy-insert-field-in-pdf.ts
index ba351092c..86ceea65f 100644
--- a/packages/lib/server-only/pdf/legacy-insert-field-in-pdf.ts
+++ b/packages/lib/server-only/pdf/legacy-insert-field-in-pdf.ts
@@ -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.
diff --git a/packages/lib/server-only/public-api/delete-api-token-by-id.ts b/packages/lib/server-only/public-api/delete-api-token-by-id.ts
index 751b08c4f..b7723dfd7 100644
--- a/packages/lib/server-only/public-api/delete-api-token-by-id.ts
+++ b/packages/lib/server-only/public-api/delete-api-token-by-id.ts
@@ -25,7 +25,7 @@ export const deleteTokenById = async ({ id, userId, teamId }: DeleteTokenByIdOpt
});
}
- return await prisma.apiToken.delete({
+ await prisma.apiToken.delete({
where: {
id,
teamId,
diff --git a/packages/lib/server-only/template/create-document-from-direct-template.ts b/packages/lib/server-only/template/create-document-from-direct-template.ts
index bed997aab..a7afd8a1a 100644
--- a/packages/lib/server-only/template/create-document-from-direct-template.ts
+++ b/packages/lib/server-only/template/create-document-from-direct-template.ts
@@ -105,7 +105,13 @@ export const createDocumentFromDirectTemplate = async ({
directLink: true,
templateDocumentData: true,
templateMeta: true,
- user: true,
+ user: {
+ select: {
+ id: true,
+ email: true,
+ name: true,
+ },
+ },
},
});
diff --git a/packages/lib/server-only/template/create-document-from-template.ts b/packages/lib/server-only/template/create-document-from-template.ts
index 05aa97512..31c69b7f4 100644
--- a/packages/lib/server-only/template/create-document-from-template.ts
+++ b/packages/lib/server-only/template/create-document-from-template.ts
@@ -2,6 +2,7 @@ import type { DocumentDistributionMethod, DocumentSigningOrder } from '@prisma/c
import {
DocumentSource,
type Field,
+ FolderType,
type Recipient,
RecipientRole,
SendStatus,
@@ -70,6 +71,7 @@ export type CreateDocumentFromTemplateOptions = {
email: string;
signingOrder?: number | null;
}[];
+ folderId?: string;
prefillFields?: TFieldMetaPrefillFieldsSchema[];
customDocumentDataId?: string;
@@ -277,6 +279,7 @@ export const createDocumentFromTemplate = async ({
customDocumentDataId,
override,
requestMetadata,
+ folderId,
prefillFields,
}: CreateDocumentFromTemplateOptions) => {
const template = await prisma.template.findUnique({
@@ -301,6 +304,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,
@@ -371,6 +390,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,
diff --git a/packages/lib/server-only/user/get-user-by-id.ts b/packages/lib/server-only/user/get-user-by-id.ts
index a01447206..26e0fcc7b 100644
--- a/packages/lib/server-only/user/get-user-by-id.ts
+++ b/packages/lib/server-only/user/get-user-by-id.ts
@@ -1,13 +1,31 @@
import { prisma } from '@documenso/prisma';
+import { AppError, AppErrorCode } from '../../errors/app-error';
+
export interface GetUserByIdOptions {
id: number;
}
export const getUserById = async ({ id }: GetUserByIdOptions) => {
- return await prisma.user.findFirstOrThrow({
+ const user = await prisma.user.findFirst({
where: {
id,
},
+ select: {
+ id: true,
+ name: true,
+ email: true,
+ emailVerified: true,
+ roles: true,
+ disabled: true,
+ twoFactorEnabled: true,
+ signature: true,
+ },
});
+
+ if (!user) {
+ throw new AppError(AppErrorCode.NOT_FOUND);
+ }
+
+ return user;
};
diff --git a/packages/lib/server-only/user/reset-password.ts b/packages/lib/server-only/user/reset-password.ts
index e01555cce..99d796e7b 100644
--- a/packages/lib/server-only/user/reset-password.ts
+++ b/packages/lib/server-only/user/reset-password.ts
@@ -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,
+ },
+ },
},
});
diff --git a/packages/lib/server-only/user/submit-support-ticket.ts b/packages/lib/server-only/user/submit-support-ticket.ts
new file mode 100644
index 000000000..91c55800d
--- /dev/null
+++ b/packages/lib/server-only/user/submit-support-ticket.ts
@@ -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;
+};
diff --git a/packages/lib/server-only/user/update-profile.ts b/packages/lib/server-only/user/update-profile.ts
index b156a06af..766a09e3e 100644
--- a/packages/lib/server-only/user/update-profile.ts
+++ b/packages/lib/server-only/user/update-profile.ts
@@ -24,7 +24,7 @@ export const updateProfile = async ({
},
});
- return await prisma.$transaction(async (tx) => {
+ await prisma.$transaction(async (tx) => {
await tx.userSecurityAuditLog.create({
data: {
userId,
@@ -34,7 +34,7 @@ export const updateProfile = async ({
},
});
- return await tx.user.update({
+ await tx.user.update({
where: {
id: userId,
},
diff --git a/packages/lib/server-only/user/verify-email.ts b/packages/lib/server-only/user/verify-email.ts
index 5285b9476..1b3a44e71 100644
--- a/packages/lib/server-only/user/verify-email.ts
+++ b/packages/lib/server-only/user/verify-email.ts
@@ -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,
diff --git a/packages/lib/utils/mask-recipient-tokens-for-document.ts b/packages/lib/utils/mask-recipient-tokens-for-document.ts
index c0eff4588..ddadb4f3f 100644
--- a/packages/lib/utils/mask-recipient-tokens-for-document.ts
+++ b/packages/lib/utils/mask-recipient-tokens-for-document.ts
@@ -4,7 +4,7 @@ import type { DocumentWithRecipients } from '@documenso/prisma/types/document-wi
export type MaskRecipientTokensForDocumentOptions = {
document: T;
- user?: User;
+ user?: Pick;
token?: string;
};
diff --git a/packages/trpc/server/admin-router/delete-document.ts b/packages/trpc/server/admin-router/delete-document.ts
new file mode 100644
index 000000000..70fc96591
--- /dev/null
+++ b/packages/trpc/server/admin-router/delete-document.ts
@@ -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,
+ });
+ });
diff --git a/packages/trpc/server/admin-router/delete-document.types.ts b/packages/trpc/server/admin-router/delete-document.types.ts
new file mode 100644
index 000000000..58ff6ff35
--- /dev/null
+++ b/packages/trpc/server/admin-router/delete-document.types.ts
@@ -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;
+export type TDeleteDocumentResponse = z.infer;
diff --git a/packages/trpc/server/admin-router/delete-user.ts b/packages/trpc/server/admin-router/delete-user.ts
new file mode 100644
index 000000000..c78fdd651
--- /dev/null
+++ b/packages/trpc/server/admin-router/delete-user.ts
@@ -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 });
+ });
diff --git a/packages/trpc/server/admin-router/delete-user.types.ts b/packages/trpc/server/admin-router/delete-user.types.ts
new file mode 100644
index 000000000..b2d01f91b
--- /dev/null
+++ b/packages/trpc/server/admin-router/delete-user.types.ts
@@ -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;
+export type TDeleteUserResponse = z.infer;
diff --git a/packages/trpc/server/admin-router/disable-user.ts b/packages/trpc/server/admin-router/disable-user.ts
new file mode 100644
index 000000000..9a2a1a854
--- /dev/null
+++ b/packages/trpc/server/admin-router/disable-user.ts
@@ -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 });
+ });
diff --git a/packages/trpc/server/admin-router/disable-user.types.ts b/packages/trpc/server/admin-router/disable-user.types.ts
new file mode 100644
index 000000000..51d26a3d8
--- /dev/null
+++ b/packages/trpc/server/admin-router/disable-user.types.ts
@@ -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;
+export type TDisableUserResponse = z.infer;
diff --git a/packages/trpc/server/admin-router/enable-user.ts b/packages/trpc/server/admin-router/enable-user.ts
new file mode 100644
index 000000000..171e3bf8a
--- /dev/null
+++ b/packages/trpc/server/admin-router/enable-user.ts
@@ -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 });
+ });
diff --git a/packages/trpc/server/admin-router/enable-user.types.ts b/packages/trpc/server/admin-router/enable-user.types.ts
new file mode 100644
index 000000000..5e44cb18e
--- /dev/null
+++ b/packages/trpc/server/admin-router/enable-user.types.ts
@@ -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;
+export type TEnableUserResponse = z.infer;
diff --git a/packages/trpc/server/admin-router/find-documents.ts b/packages/trpc/server/admin-router/find-documents.ts
new file mode 100644
index 000000000..7ce96c1f5
--- /dev/null
+++ b/packages/trpc/server/admin-router/find-documents.ts
@@ -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 });
+ });
diff --git a/packages/trpc/server/admin-router/find-documents.types.ts b/packages/trpc/server/admin-router/find-documents.types.ts
new file mode 100644
index 000000000..b6fc4e864
--- /dev/null
+++ b/packages/trpc/server/admin-router/find-documents.types.ts
@@ -0,0 +1,17 @@
+import { z } from 'zod';
+
+import { ZDocumentManySchema } from '@documenso/lib/types/document';
+import { ZFindResultResponse, ZFindSearchParamsSchema } from '@documenso/lib/types/search-params';
+
+export const ZFindDocumentsRequestSchema = ZFindSearchParamsSchema.extend({
+ perPage: z.number().optional().default(20),
+});
+
+export const ZFindDocumentsResponseSchema = ZFindResultResponse.extend({
+ data: ZDocumentManySchema.omit({
+ team: true,
+ }).array(),
+});
+
+export type TFindDocumentsRequest = z.infer;
+export type TFindDocumentsResponse = z.infer;
diff --git a/packages/trpc/server/admin-router/get-user.ts b/packages/trpc/server/admin-router/get-user.ts
new file mode 100644
index 000000000..c60fd6b21
--- /dev/null
+++ b/packages/trpc/server/admin-router/get-user.ts
@@ -0,0 +1,19 @@
+import { getUserById } from '@documenso/lib/server-only/user/get-user-by-id';
+
+import { adminProcedure } from '../trpc';
+import { ZGetUserRequestSchema, ZGetUserResponseSchema } from './get-user.types';
+
+export const getUserRoute = adminProcedure
+ .input(ZGetUserRequestSchema)
+ .output(ZGetUserResponseSchema)
+ .query(async ({ input, ctx }) => {
+ const { id } = input;
+
+ ctx.logger.info({
+ input: {
+ id,
+ },
+ });
+
+ return await getUserById({ id });
+ });
diff --git a/packages/trpc/server/admin-router/get-user.types.ts b/packages/trpc/server/admin-router/get-user.types.ts
new file mode 100644
index 000000000..fe4ebac5e
--- /dev/null
+++ b/packages/trpc/server/admin-router/get-user.types.ts
@@ -0,0 +1,21 @@
+import { z } from 'zod';
+
+import UserSchema from '@documenso/prisma/generated/zod/modelSchema/UserSchema';
+
+export const ZGetUserRequestSchema = z.object({
+ id: z.number().min(1),
+});
+
+export const ZGetUserResponseSchema = UserSchema.pick({
+ id: true,
+ name: true,
+ email: true,
+ emailVerified: true,
+ roles: true,
+ disabled: true,
+ twoFactorEnabled: true,
+ signature: true,
+});
+
+export type TGetUserRequest = z.infer;
+export type TGetUserResponse = z.infer;
diff --git a/packages/trpc/server/admin-router/reseal-document.ts b/packages/trpc/server/admin-router/reseal-document.ts
new file mode 100644
index 000000000..7436d29c7
--- /dev/null
+++ b/packages/trpc/server/admin-router/reseal-document.ts
@@ -0,0 +1,28 @@
+import { getEntireDocument } from '@documenso/lib/server-only/admin/get-entire-document';
+import { sealDocument } from '@documenso/lib/server-only/document/seal-document';
+import { isDocumentCompleted } from '@documenso/lib/utils/document';
+
+import { adminProcedure } from '../trpc';
+import {
+ ZResealDocumentRequestSchema,
+ ZResealDocumentResponseSchema,
+} from './reseal-document.types';
+
+export const resealDocumentRoute = adminProcedure
+ .input(ZResealDocumentRequestSchema)
+ .output(ZResealDocumentResponseSchema)
+ .mutation(async ({ input, ctx }) => {
+ const { id } = input;
+
+ ctx.logger.info({
+ input: {
+ id,
+ },
+ });
+
+ const document = await getEntireDocument({ id });
+
+ const isResealing = isDocumentCompleted(document.status);
+
+ await sealDocument({ documentId: id, isResealing });
+ });
diff --git a/packages/trpc/server/admin-router/reseal-document.types.ts b/packages/trpc/server/admin-router/reseal-document.types.ts
new file mode 100644
index 000000000..e33c2dc5c
--- /dev/null
+++ b/packages/trpc/server/admin-router/reseal-document.types.ts
@@ -0,0 +1,10 @@
+import { z } from 'zod';
+
+export const ZResealDocumentRequestSchema = z.object({
+ id: z.number().min(1),
+});
+
+export const ZResealDocumentResponseSchema = z.void();
+
+export type TResealDocumentRequest = z.infer;
+export type TResealDocumentResponse = z.infer;
diff --git a/packages/trpc/server/admin-router/reset-two-factor-authentication.ts b/packages/trpc/server/admin-router/reset-two-factor-authentication.ts
new file mode 100644
index 000000000..ffacebe22
--- /dev/null
+++ b/packages/trpc/server/admin-router/reset-two-factor-authentication.ts
@@ -0,0 +1,50 @@
+import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
+import { prisma } from '@documenso/prisma';
+
+import { adminProcedure } from '../trpc';
+import {
+ ZResetTwoFactorRequestSchema,
+ ZResetTwoFactorResponseSchema,
+} from './reset-two-factor-authentication.types';
+
+export const resetTwoFactorRoute = adminProcedure
+ .input(ZResetTwoFactorRequestSchema)
+ .output(ZResetTwoFactorResponseSchema)
+ .mutation(async ({ input, ctx }) => {
+ const { userId } = input;
+
+ ctx.logger.info({
+ input: {
+ userId,
+ },
+ });
+
+ return await resetTwoFactor({ userId });
+ });
+
+export type ResetTwoFactorOptions = {
+ userId: number;
+};
+
+export const resetTwoFactor = async ({ userId }: ResetTwoFactorOptions) => {
+ const user = await prisma.user.findFirst({
+ where: {
+ id: userId,
+ },
+ });
+
+ if (!user) {
+ throw new AppError(AppErrorCode.NOT_FOUND, { message: 'User not found' });
+ }
+
+ await prisma.user.update({
+ where: {
+ id: user.id,
+ },
+ data: {
+ twoFactorEnabled: false,
+ twoFactorBackupCodes: null,
+ twoFactorSecret: null,
+ },
+ });
+};
diff --git a/packages/trpc/server/admin-router/reset-two-factor-authentication.types.ts b/packages/trpc/server/admin-router/reset-two-factor-authentication.types.ts
new file mode 100644
index 000000000..497d36ef7
--- /dev/null
+++ b/packages/trpc/server/admin-router/reset-two-factor-authentication.types.ts
@@ -0,0 +1,10 @@
+import { z } from 'zod';
+
+export const ZResetTwoFactorRequestSchema = z.object({
+ userId: z.number(),
+});
+
+export const ZResetTwoFactorResponseSchema = z.void();
+
+export type TResetTwoFactorRequest = z.infer;
+export type TResetTwoFactorResponse = z.infer;
diff --git a/packages/trpc/server/admin-router/router.ts b/packages/trpc/server/admin-router/router.ts
index aca1a03d6..f8d472a79 100644
--- a/packages/trpc/server/admin-router/router.ts
+++ b/packages/trpc/server/admin-router/router.ts
@@ -1,39 +1,24 @@
-import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
-import { findDocuments } from '@documenso/lib/server-only/admin/get-all-documents';
-import { getEntireDocument } from '@documenso/lib/server-only/admin/get-entire-document';
-import { updateRecipient } from '@documenso/lib/server-only/admin/update-recipient';
-import { updateUser } from '@documenso/lib/server-only/admin/update-user';
-import { sealDocument } from '@documenso/lib/server-only/document/seal-document';
-import { sendDeleteEmail } from '@documenso/lib/server-only/document/send-delete-email';
-import { superDeleteDocument } from '@documenso/lib/server-only/document/super-delete-document';
-import { upsertSiteSetting } from '@documenso/lib/server-only/site-settings/upsert-site-setting';
-import { deleteUser } from '@documenso/lib/server-only/user/delete-user';
-import { disableUser } from '@documenso/lib/server-only/user/disable-user';
-import { enableUser } from '@documenso/lib/server-only/user/enable-user';
-import { getUserById } from '@documenso/lib/server-only/user/get-user-by-id';
-import { isDocumentCompleted } from '@documenso/lib/utils/document';
-
-import { adminProcedure, router } from '../trpc';
+import { router } from '../trpc';
import { createAdminOrganisationRoute } from './create-admin-organisation';
import { createStripeCustomerRoute } from './create-stripe-customer';
import { createSubscriptionClaimRoute } from './create-subscription-claim';
+import { deleteDocumentRoute } from './delete-document';
import { deleteSubscriptionClaimRoute } from './delete-subscription-claim';
+import { deleteUserRoute } from './delete-user';
+import { disableUserRoute } from './disable-user';
+import { enableUserRoute } from './enable-user';
import { findAdminOrganisationsRoute } from './find-admin-organisations';
+import { findDocumentsRoute } from './find-documents';
import { findSubscriptionClaimsRoute } from './find-subscription-claims';
import { getAdminOrganisationRoute } from './get-admin-organisation';
-import {
- ZAdminDeleteDocumentMutationSchema,
- ZAdminDeleteUserMutationSchema,
- ZAdminDisableUserMutationSchema,
- ZAdminEnableUserMutationSchema,
- ZAdminFindDocumentsQuerySchema,
- ZAdminResealDocumentMutationSchema,
- ZAdminUpdateProfileMutationSchema,
- ZAdminUpdateRecipientMutationSchema,
- ZAdminUpdateSiteSettingMutationSchema,
-} from './schema';
+import { getUserRoute } from './get-user';
+import { resealDocumentRoute } from './reseal-document';
+import { resetTwoFactorRoute } from './reset-two-factor-authentication';
import { updateAdminOrganisationRoute } from './update-admin-organisation';
+import { updateRecipientRoute } from './update-recipient';
+import { updateSiteSettingRoute } from './update-site-setting';
import { updateSubscriptionClaimRoute } from './update-subscription-claim';
+import { updateUserRoute } from './update-user';
export const adminRouter = router({
organisation: {
@@ -51,154 +36,21 @@ export const adminRouter = router({
stripe: {
createCustomer: createStripeCustomerRoute,
},
-
- // Todo: migrate old routes
- findDocuments: adminProcedure.input(ZAdminFindDocumentsQuerySchema).query(async ({ input }) => {
- const { query, page, perPage } = input;
-
- return await findDocuments({ query, page, perPage });
- }),
-
- updateUser: adminProcedure
- .input(ZAdminUpdateProfileMutationSchema)
- .mutation(async ({ input, ctx }) => {
- const { id, name, email, roles } = input;
-
- ctx.logger.info({
- input: {
- id,
- roles,
- },
- });
-
- return await updateUser({ id, name, email, roles });
- }),
-
- updateRecipient: adminProcedure
- .input(ZAdminUpdateRecipientMutationSchema)
- .mutation(async ({ input, ctx }) => {
- const { id, name, email } = input;
-
- ctx.logger.info({
- input: {
- id,
- },
- });
-
- return await updateRecipient({ id, name, email });
- }),
-
- updateSiteSetting: adminProcedure
- .input(ZAdminUpdateSiteSettingMutationSchema)
- .mutation(async ({ ctx, input }) => {
- const { id, enabled, data } = input;
-
- ctx.logger.info({
- input: {
- id,
- },
- });
-
- return await upsertSiteSetting({
- id,
- enabled,
- data,
- userId: ctx.user.id,
- });
- }),
-
- resealDocument: adminProcedure
- .input(ZAdminResealDocumentMutationSchema)
- .mutation(async ({ input, ctx }) => {
- const { id } = input;
-
- ctx.logger.info({
- input: {
- id,
- },
- });
-
- const document = await getEntireDocument({ id });
-
- const isResealing = isDocumentCompleted(document.status);
-
- return await sealDocument({ documentId: id, isResealing });
- }),
-
- enableUser: adminProcedure
- .input(ZAdminEnableUserMutationSchema)
- .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',
- });
- }
-
- return await enableUser({ id });
- }),
-
- disableUser: adminProcedure
- .input(ZAdminDisableUserMutationSchema)
- .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',
- });
- }
-
- return await disableUser({ id });
- }),
-
- deleteUser: adminProcedure
- .input(ZAdminDeleteUserMutationSchema)
- .mutation(async ({ input, ctx }) => {
- const { id } = input;
-
- ctx.logger.info({
- input: {
- id,
- },
- });
-
- return await deleteUser({ id });
- }),
-
- deleteDocument: adminProcedure
- .input(ZAdminDeleteDocumentMutationSchema)
- .mutation(async ({ ctx, input }) => {
- const { id, reason } = input;
-
- ctx.logger.info({
- input: {
- id,
- },
- });
-
- await sendDeleteEmail({ documentId: id, reason });
-
- return await superDeleteDocument({
- id,
- requestMetadata: ctx.metadata.requestMetadata,
- });
- }),
+ user: {
+ get: getUserRoute,
+ update: updateUserRoute,
+ delete: deleteUserRoute,
+ enable: enableUserRoute,
+ disable: disableUserRoute,
+ resetTwoFactor: resetTwoFactorRoute,
+ },
+ document: {
+ find: findDocumentsRoute,
+ delete: deleteDocumentRoute,
+ reseal: resealDocumentRoute,
+ },
+ recipient: {
+ update: updateRecipientRoute,
+ },
+ updateSiteSetting: updateSiteSettingRoute,
});
diff --git a/packages/trpc/server/admin-router/schema.ts b/packages/trpc/server/admin-router/schema.ts
deleted file mode 100644
index 6fc7f5df5..000000000
--- a/packages/trpc/server/admin-router/schema.ts
+++ /dev/null
@@ -1,67 +0,0 @@
-import { Role } from '@prisma/client';
-import z from 'zod';
-
-import { ZSiteSettingSchema } from '@documenso/lib/server-only/site-settings/schema';
-import { ZFindSearchParamsSchema } from '@documenso/lib/types/search-params';
-
-export const ZAdminFindDocumentsQuerySchema = ZFindSearchParamsSchema.extend({
- perPage: z.number().optional().default(20),
-});
-
-export type TAdminFindDocumentsQuerySchema = z.infer;
-
-export const ZAdminUpdateProfileMutationSchema = z.object({
- id: z.number().min(1),
- name: z.string().nullish(),
- email: z.string().email().optional(),
- roles: z.array(z.nativeEnum(Role)).optional(),
-});
-
-export type TAdminUpdateProfileMutationSchema = z.infer;
-
-export const ZAdminUpdateRecipientMutationSchema = z.object({
- id: z.number().min(1),
- name: z.string().optional(),
- email: z.string().email().optional(),
-});
-
-export type TAdminUpdateRecipientMutationSchema = z.infer<
- typeof ZAdminUpdateRecipientMutationSchema
->;
-
-export const ZAdminUpdateSiteSettingMutationSchema = ZSiteSettingSchema;
-
-export type TAdminUpdateSiteSettingMutationSchema = z.infer<
- typeof ZAdminUpdateSiteSettingMutationSchema
->;
-
-export const ZAdminResealDocumentMutationSchema = z.object({
- id: z.number().min(1),
-});
-
-export type TAdminResealDocumentMutationSchema = z.infer;
-
-export const ZAdminDeleteUserMutationSchema = z.object({
- id: z.number().min(1),
-});
-
-export type TAdminDeleteUserMutationSchema = z.infer;
-
-export const ZAdminEnableUserMutationSchema = z.object({
- id: z.number().min(1),
-});
-
-export type TAdminEnableUserMutationSchema = z.infer;
-
-export const ZAdminDisableUserMutationSchema = z.object({
- id: z.number().min(1),
-});
-
-export type TAdminDisableUserMutationSchema = z.infer;
-
-export const ZAdminDeleteDocumentMutationSchema = z.object({
- id: z.number().min(1),
- reason: z.string(),
-});
-
-export type TAdminDeleteDocomentMutationSchema = z.infer;
diff --git a/packages/trpc/server/admin-router/update-recipient.ts b/packages/trpc/server/admin-router/update-recipient.ts
new file mode 100644
index 000000000..1fc286af2
--- /dev/null
+++ b/packages/trpc/server/admin-router/update-recipient.ts
@@ -0,0 +1,22 @@
+import { updateRecipient } from '@documenso/lib/server-only/admin/update-recipient';
+
+import { adminProcedure } from '../trpc';
+import {
+ ZUpdateRecipientRequestSchema,
+ ZUpdateRecipientResponseSchema,
+} from './update-recipient.types';
+
+export const updateRecipientRoute = adminProcedure
+ .input(ZUpdateRecipientRequestSchema)
+ .output(ZUpdateRecipientResponseSchema)
+ .mutation(async ({ input, ctx }) => {
+ const { id, name, email } = input;
+
+ ctx.logger.info({
+ input: {
+ id,
+ },
+ });
+
+ await updateRecipient({ id, name, email });
+ });
diff --git a/packages/trpc/server/admin-router/update-recipient.types.ts b/packages/trpc/server/admin-router/update-recipient.types.ts
new file mode 100644
index 000000000..7b9bc4008
--- /dev/null
+++ b/packages/trpc/server/admin-router/update-recipient.types.ts
@@ -0,0 +1,12 @@
+import { z } from 'zod';
+
+export const ZUpdateRecipientRequestSchema = z.object({
+ id: z.number().min(1),
+ name: z.string().optional(),
+ email: z.string().email().optional(),
+});
+
+export const ZUpdateRecipientResponseSchema = z.void();
+
+export type TUpdateRecipientRequest = z.infer;
+export type TUpdateRecipientResponse = z.infer;
diff --git a/packages/trpc/server/admin-router/update-site-setting.ts b/packages/trpc/server/admin-router/update-site-setting.ts
new file mode 100644
index 000000000..1b0ff971a
--- /dev/null
+++ b/packages/trpc/server/admin-router/update-site-setting.ts
@@ -0,0 +1,27 @@
+import { upsertSiteSetting } from '@documenso/lib/server-only/site-settings/upsert-site-setting';
+
+import { adminProcedure } from '../trpc';
+import {
+ ZUpdateSiteSettingRequestSchema,
+ ZUpdateSiteSettingResponseSchema,
+} from './update-site-setting.types';
+
+export const updateSiteSettingRoute = adminProcedure
+ .input(ZUpdateSiteSettingRequestSchema)
+ .output(ZUpdateSiteSettingResponseSchema)
+ .mutation(async ({ ctx, input }) => {
+ const { id, enabled, data } = input;
+
+ ctx.logger.info({
+ input: {
+ id,
+ },
+ });
+
+ await upsertSiteSetting({
+ id,
+ enabled,
+ data,
+ userId: ctx.user.id,
+ });
+ });
diff --git a/packages/trpc/server/admin-router/update-site-setting.types.ts b/packages/trpc/server/admin-router/update-site-setting.types.ts
new file mode 100644
index 000000000..bd8638b3e
--- /dev/null
+++ b/packages/trpc/server/admin-router/update-site-setting.types.ts
@@ -0,0 +1,10 @@
+import { z } from 'zod';
+
+import { ZSiteSettingSchema } from '@documenso/lib/server-only/site-settings/schema';
+
+export const ZUpdateSiteSettingRequestSchema = ZSiteSettingSchema;
+
+export const ZUpdateSiteSettingResponseSchema = z.void();
+
+export type TUpdateSiteSettingRequest = z.infer;
+export type TUpdateSiteSettingResponse = z.infer;
diff --git a/packages/trpc/server/admin-router/update-user.ts b/packages/trpc/server/admin-router/update-user.ts
new file mode 100644
index 000000000..d04a7f80e
--- /dev/null
+++ b/packages/trpc/server/admin-router/update-user.ts
@@ -0,0 +1,20 @@
+import { updateUser } from '@documenso/lib/server-only/admin/update-user';
+
+import { adminProcedure } from '../trpc';
+import { ZUpdateUserRequestSchema, ZUpdateUserResponseSchema } from './update-user.types';
+
+export const updateUserRoute = adminProcedure
+ .input(ZUpdateUserRequestSchema)
+ .output(ZUpdateUserResponseSchema)
+ .mutation(async ({ input, ctx }) => {
+ const { id, name, email, roles } = input;
+
+ ctx.logger.info({
+ input: {
+ id,
+ roles,
+ },
+ });
+
+ await updateUser({ id, name, email, roles });
+ });
diff --git a/packages/trpc/server/admin-router/update-user.types.ts b/packages/trpc/server/admin-router/update-user.types.ts
new file mode 100644
index 000000000..f4650fca4
--- /dev/null
+++ b/packages/trpc/server/admin-router/update-user.types.ts
@@ -0,0 +1,14 @@
+import { Role } from '@prisma/client';
+import { z } from 'zod';
+
+export const ZUpdateUserRequestSchema = z.object({
+ id: z.number().min(1),
+ name: z.string().nullish(),
+ email: z.string().email().optional(),
+ roles: z.array(z.nativeEnum(Role)).optional(),
+});
+
+export const ZUpdateUserResponseSchema = z.void();
+
+export type TUpdateUserRequest = z.infer;
+export type TUpdateUserResponse = z.infer;
diff --git a/packages/trpc/server/api-token-router/create-api-token.ts b/packages/trpc/server/api-token-router/create-api-token.ts
new file mode 100644
index 000000000..6b8e665f8
--- /dev/null
+++ b/packages/trpc/server/api-token-router/create-api-token.ts
@@ -0,0 +1,27 @@
+import { createApiToken } from '@documenso/lib/server-only/public-api/create-api-token';
+
+import { authenticatedProcedure } from '../trpc';
+import {
+ ZCreateApiTokenRequestSchema,
+ ZCreateApiTokenResponseSchema,
+} from './create-api-token.types';
+
+export const createApiTokenRoute = authenticatedProcedure
+ .input(ZCreateApiTokenRequestSchema)
+ .output(ZCreateApiTokenResponseSchema)
+ .mutation(async ({ input, ctx }) => {
+ const { tokenName, teamId, expirationDate } = input;
+
+ ctx.logger.info({
+ input: {
+ teamId,
+ },
+ });
+
+ return await createApiToken({
+ userId: ctx.user.id,
+ teamId,
+ tokenName,
+ expiresIn: expirationDate,
+ });
+ });
diff --git a/packages/trpc/server/api-token-router/create-api-token.types.ts b/packages/trpc/server/api-token-router/create-api-token.types.ts
new file mode 100644
index 000000000..c73c65833
--- /dev/null
+++ b/packages/trpc/server/api-token-router/create-api-token.types.ts
@@ -0,0 +1,12 @@
+import { z } from 'zod';
+
+export const ZCreateApiTokenRequestSchema = z.object({
+ teamId: z.number(),
+ tokenName: z.string().min(3, { message: 'The token name should be 3 characters or longer' }),
+ expirationDate: z.string().nullable(),
+});
+
+export const ZCreateApiTokenResponseSchema = z.object({
+ id: z.number(),
+ token: z.string(),
+});
diff --git a/packages/trpc/server/api-token-router/delete-api-token.ts b/packages/trpc/server/api-token-router/delete-api-token.ts
new file mode 100644
index 000000000..45fcc2dee
--- /dev/null
+++ b/packages/trpc/server/api-token-router/delete-api-token.ts
@@ -0,0 +1,27 @@
+import { deleteTokenById } from '@documenso/lib/server-only/public-api/delete-api-token-by-id';
+
+import { authenticatedProcedure } from '../trpc';
+import {
+ ZDeleteApiTokenRequestSchema,
+ ZDeleteApiTokenResponseSchema,
+} from './delete-api-token.types';
+
+export const deleteApiTokenRoute = authenticatedProcedure
+ .input(ZDeleteApiTokenRequestSchema)
+ .output(ZDeleteApiTokenResponseSchema)
+ .mutation(async ({ input, ctx }) => {
+ const { id, teamId } = input;
+
+ ctx.logger.info({
+ input: {
+ id,
+ teamId,
+ },
+ });
+
+ await deleteTokenById({
+ id,
+ teamId,
+ userId: ctx.user.id,
+ });
+ });
diff --git a/packages/trpc/server/api-token-router/delete-api-token.types.ts b/packages/trpc/server/api-token-router/delete-api-token.types.ts
new file mode 100644
index 000000000..7cf235fae
--- /dev/null
+++ b/packages/trpc/server/api-token-router/delete-api-token.types.ts
@@ -0,0 +1,8 @@
+import { z } from 'zod';
+
+export const ZDeleteApiTokenRequestSchema = z.object({
+ id: z.number().min(1),
+ teamId: z.number(),
+});
+
+export const ZDeleteApiTokenResponseSchema = z.void();
diff --git a/packages/trpc/server/api-token-router/get-api-tokens.ts b/packages/trpc/server/api-token-router/get-api-tokens.ts
new file mode 100644
index 000000000..4473c8d42
--- /dev/null
+++ b/packages/trpc/server/api-token-router/get-api-tokens.ts
@@ -0,0 +1,19 @@
+import { getApiTokens } from '@documenso/lib/server-only/public-api/get-api-tokens';
+
+import { authenticatedProcedure } from '../trpc';
+import { ZGetApiTokensRequestSchema, ZGetApiTokensResponseSchema } from './get-api-tokens.types';
+
+export const getApiTokensRoute = authenticatedProcedure
+ .input(ZGetApiTokensRequestSchema)
+ .output(ZGetApiTokensResponseSchema)
+ .query(async ({ ctx }) => {
+ const { teamId } = ctx;
+
+ ctx.logger.info({
+ input: {
+ teamId,
+ },
+ });
+
+ return await getApiTokens({ userId: ctx.user.id, teamId });
+ });
diff --git a/packages/trpc/server/api-token-router/get-api-tokens.types.ts b/packages/trpc/server/api-token-router/get-api-tokens.types.ts
new file mode 100644
index 000000000..9e380bf9d
--- /dev/null
+++ b/packages/trpc/server/api-token-router/get-api-tokens.types.ts
@@ -0,0 +1,16 @@
+import { z } from 'zod';
+
+import ApiTokenSchema from '@documenso/prisma/generated/zod/modelSchema/ApiTokenSchema';
+
+export const ZGetApiTokensRequestSchema = z.void();
+
+export const ZGetApiTokensResponseSchema = z.array(
+ ApiTokenSchema.pick({
+ id: true,
+ name: true,
+ createdAt: true,
+ expires: true,
+ }),
+);
+
+export type TGetApiTokensResponse = z.infer;
diff --git a/packages/trpc/server/api-token-router/router.ts b/packages/trpc/server/api-token-router/router.ts
index f1439060c..8a17d5b66 100644
--- a/packages/trpc/server/api-token-router/router.ts
+++ b/packages/trpc/server/api-token-router/router.ts
@@ -1,50 +1,10 @@
-import { createApiToken } from '@documenso/lib/server-only/public-api/create-api-token';
-import { deleteTokenById } from '@documenso/lib/server-only/public-api/delete-api-token-by-id';
-import { getApiTokens } from '@documenso/lib/server-only/public-api/get-api-tokens';
-
-import { authenticatedProcedure, router } from '../trpc';
-import { ZCreateTokenMutationSchema, ZDeleteTokenByIdMutationSchema } from './schema';
+import { router } from '../trpc';
+import { createApiTokenRoute } from './create-api-token';
+import { deleteApiTokenRoute } from './delete-api-token';
+import { getApiTokensRoute } from './get-api-tokens';
export const apiTokenRouter = router({
- getTokens: authenticatedProcedure.query(async ({ ctx }) => {
- return await getApiTokens({ userId: ctx.user.id, teamId: ctx.teamId });
- }),
-
- createToken: authenticatedProcedure
- .input(ZCreateTokenMutationSchema)
- .mutation(async ({ input, ctx }) => {
- const { tokenName, teamId, expirationDate } = input;
-
- ctx.logger.info({
- input: {
- teamId,
- },
- });
-
- return await createApiToken({
- userId: ctx.user.id,
- teamId,
- tokenName,
- expiresIn: expirationDate,
- });
- }),
-
- deleteTokenById: authenticatedProcedure
- .input(ZDeleteTokenByIdMutationSchema)
- .mutation(async ({ input, ctx }) => {
- const { id, teamId } = input;
-
- ctx.logger.info({
- input: {
- id,
- teamId,
- },
- });
-
- return await deleteTokenById({
- id,
- teamId,
- userId: ctx.user.id,
- });
- }),
+ create: createApiTokenRoute,
+ getMany: getApiTokensRoute,
+ delete: deleteApiTokenRoute,
});
diff --git a/packages/trpc/server/api-token-router/schema.ts b/packages/trpc/server/api-token-router/schema.ts
deleted file mode 100644
index 85c41d956..000000000
--- a/packages/trpc/server/api-token-router/schema.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import { z } from 'zod';
-
-export const ZCreateTokenMutationSchema = z.object({
- teamId: z.number(),
- tokenName: z.string().min(3, { message: 'The token name should be 3 characters or longer' }),
- expirationDate: z.string().nullable(),
-});
-
-export type TCreateTokenMutationSchema = z.infer;
-
-export const ZDeleteTokenByIdMutationSchema = z.object({
- id: z.number().min(1),
- teamId: z.number(),
-});
-
-export type TDeleteTokenByIdMutationSchema = z.infer;
diff --git a/packages/trpc/server/auth-router/create-passkey-authentication-options.ts b/packages/trpc/server/auth-router/create-passkey-authentication-options.ts
new file mode 100644
index 000000000..6b507f7ca
--- /dev/null
+++ b/packages/trpc/server/auth-router/create-passkey-authentication-options.ts
@@ -0,0 +1,17 @@
+import { createPasskeyAuthenticationOptions } from '@documenso/lib/server-only/auth/create-passkey-authentication-options';
+
+import { authenticatedProcedure } from '../trpc';
+import {
+ ZCreatePasskeyAuthenticationOptionsRequestSchema,
+ ZCreatePasskeyAuthenticationOptionsResponseSchema,
+} from './create-passkey-authentication-options.types';
+
+export const createPasskeyAuthenticationOptionsRoute = authenticatedProcedure
+ .input(ZCreatePasskeyAuthenticationOptionsRequestSchema)
+ .output(ZCreatePasskeyAuthenticationOptionsResponseSchema)
+ .mutation(async ({ ctx, input }) => {
+ return await createPasskeyAuthenticationOptions({
+ userId: ctx.user.id,
+ preferredPasskeyId: input?.preferredPasskeyId,
+ });
+ });
diff --git a/packages/trpc/server/auth-router/create-passkey-authentication-options.types.ts b/packages/trpc/server/auth-router/create-passkey-authentication-options.types.ts
new file mode 100644
index 000000000..a9ae6ad12
--- /dev/null
+++ b/packages/trpc/server/auth-router/create-passkey-authentication-options.types.ts
@@ -0,0 +1,19 @@
+import { z } from 'zod';
+
+export const ZCreatePasskeyAuthenticationOptionsRequestSchema = z
+ .object({
+ preferredPasskeyId: z.string().optional(),
+ })
+ .optional();
+
+export const ZCreatePasskeyAuthenticationOptionsResponseSchema = z.object({
+ tokenReference: z.string(),
+ options: z.any(), // PublicKeyCredentialRequestOptions type
+});
+
+export type TCreatePasskeyAuthenticationOptionsRequest = z.infer<
+ typeof ZCreatePasskeyAuthenticationOptionsRequestSchema
+>;
+export type TCreatePasskeyAuthenticationOptionsResponse = z.infer<
+ typeof ZCreatePasskeyAuthenticationOptionsResponseSchema
+>;
diff --git a/packages/trpc/server/auth-router/create-passkey-registration-options.ts b/packages/trpc/server/auth-router/create-passkey-registration-options.ts
new file mode 100644
index 000000000..969da6d98
--- /dev/null
+++ b/packages/trpc/server/auth-router/create-passkey-registration-options.ts
@@ -0,0 +1,16 @@
+import { createPasskeyRegistrationOptions } from '@documenso/lib/server-only/auth/create-passkey-registration-options';
+
+import { authenticatedProcedure } from '../trpc';
+import {
+ ZCreatePasskeyRegistrationOptionsRequestSchema,
+ ZCreatePasskeyRegistrationOptionsResponseSchema,
+} from './create-passkey-registration-options.types';
+
+export const createPasskeyRegistrationOptionsRoute = authenticatedProcedure
+ .input(ZCreatePasskeyRegistrationOptionsRequestSchema)
+ .output(ZCreatePasskeyRegistrationOptionsResponseSchema)
+ .mutation(async ({ ctx }) => {
+ return await createPasskeyRegistrationOptions({
+ userId: ctx.user.id,
+ });
+ });
diff --git a/packages/trpc/server/auth-router/create-passkey-registration-options.types.ts b/packages/trpc/server/auth-router/create-passkey-registration-options.types.ts
new file mode 100644
index 000000000..317201d3c
--- /dev/null
+++ b/packages/trpc/server/auth-router/create-passkey-registration-options.types.ts
@@ -0,0 +1,12 @@
+import { z } from 'zod';
+
+export const ZCreatePasskeyRegistrationOptionsRequestSchema = z.void();
+
+export const ZCreatePasskeyRegistrationOptionsResponseSchema = z.any(); // PublicKeyCredentialCreationOptions type
+
+export type TCreatePasskeyRegistrationOptionsRequest = z.infer<
+ typeof ZCreatePasskeyRegistrationOptionsRequestSchema
+>;
+export type TCreatePasskeyRegistrationOptionsResponse = z.infer<
+ typeof ZCreatePasskeyRegistrationOptionsResponseSchema
+>;
diff --git a/packages/trpc/server/auth-router/create-passkey-signin-options.ts b/packages/trpc/server/auth-router/create-passkey-signin-options.ts
new file mode 100644
index 000000000..924db1821
--- /dev/null
+++ b/packages/trpc/server/auth-router/create-passkey-signin-options.ts
@@ -0,0 +1,24 @@
+import { createPasskeySigninOptions } from '@documenso/lib/server-only/auth/create-passkey-signin-options';
+import { nanoid } from '@documenso/lib/universal/id';
+
+import { procedure } from '../trpc';
+import {
+ ZCreatePasskeySigninOptionsRequestSchema,
+ ZCreatePasskeySigninOptionsResponseSchema,
+} from './create-passkey-signin-options.types';
+
+export const createPasskeySigninOptionsRoute = procedure
+ .input(ZCreatePasskeySigninOptionsRequestSchema)
+ .output(ZCreatePasskeySigninOptionsResponseSchema)
+ .mutation(async () => {
+ const sessionIdToken = nanoid(16);
+
+ const [sessionId] = decodeURI(sessionIdToken).split('|');
+
+ const options = await createPasskeySigninOptions({ sessionId });
+
+ return {
+ options,
+ sessionId,
+ };
+ });
diff --git a/packages/trpc/server/auth-router/create-passkey-signin-options.types.ts b/packages/trpc/server/auth-router/create-passkey-signin-options.types.ts
new file mode 100644
index 000000000..38585372a
--- /dev/null
+++ b/packages/trpc/server/auth-router/create-passkey-signin-options.types.ts
@@ -0,0 +1,15 @@
+import { z } from 'zod';
+
+export const ZCreatePasskeySigninOptionsRequestSchema = z.void();
+
+export const ZCreatePasskeySigninOptionsResponseSchema = z.object({
+ options: z.any(), // PublicKeyCredentialRequestOptions type
+ sessionId: z.string(),
+});
+
+export type TCreatePasskeySigninOptionsRequest = z.infer<
+ typeof ZCreatePasskeySigninOptionsRequestSchema
+>;
+export type TCreatePasskeySigninOptionsResponse = z.infer<
+ typeof ZCreatePasskeySigninOptionsResponseSchema
+>;
diff --git a/packages/trpc/server/auth-router/create-passkey.ts b/packages/trpc/server/auth-router/create-passkey.ts
new file mode 100644
index 000000000..42b97b45b
--- /dev/null
+++ b/packages/trpc/server/auth-router/create-passkey.ts
@@ -0,0 +1,21 @@
+import type { RegistrationResponseJSON } from '@simplewebauthn/types';
+
+import { createPasskey } from '@documenso/lib/server-only/auth/create-passkey';
+
+import { authenticatedProcedure } from '../trpc';
+import { ZCreatePasskeyRequestSchema, ZCreatePasskeyResponseSchema } from './create-passkey.types';
+
+export const createPasskeyRoute = authenticatedProcedure
+ .input(ZCreatePasskeyRequestSchema)
+ .output(ZCreatePasskeyResponseSchema)
+ .mutation(async ({ ctx, input }) => {
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
+ const verificationResponse = input.verificationResponse as RegistrationResponseJSON;
+
+ return await createPasskey({
+ userId: ctx.user.id,
+ verificationResponse,
+ passkeyName: input.passkeyName,
+ requestMetadata: ctx.metadata.requestMetadata,
+ });
+ });
diff --git a/packages/trpc/server/auth-router/create-passkey.types.ts b/packages/trpc/server/auth-router/create-passkey.types.ts
new file mode 100644
index 000000000..06abf1793
--- /dev/null
+++ b/packages/trpc/server/auth-router/create-passkey.types.ts
@@ -0,0 +1,13 @@
+import { z } from 'zod';
+
+import { ZRegistrationResponseJSONSchema } from '@documenso/lib/types/webauthn';
+
+export const ZCreatePasskeyRequestSchema = z.object({
+ passkeyName: z.string().trim().min(1),
+ verificationResponse: ZRegistrationResponseJSONSchema,
+});
+
+export const ZCreatePasskeyResponseSchema = z.void();
+
+export type TCreatePasskeyRequest = z.infer;
+export type TCreatePasskeyResponse = z.infer;
diff --git a/packages/trpc/server/auth-router/delete-passkey.ts b/packages/trpc/server/auth-router/delete-passkey.ts
new file mode 100644
index 000000000..af1cdc6b7
--- /dev/null
+++ b/packages/trpc/server/auth-router/delete-passkey.ts
@@ -0,0 +1,23 @@
+import { deletePasskey } from '@documenso/lib/server-only/auth/delete-passkey';
+
+import { authenticatedProcedure } from '../trpc';
+import { ZDeletePasskeyRequestSchema, ZDeletePasskeyResponseSchema } from './delete-passkey.types';
+
+export const deletePasskeyRoute = authenticatedProcedure
+ .input(ZDeletePasskeyRequestSchema)
+ .output(ZDeletePasskeyResponseSchema)
+ .mutation(async ({ ctx, input }) => {
+ const { passkeyId } = input;
+
+ ctx.logger.info({
+ input: {
+ passkeyId,
+ },
+ });
+
+ await deletePasskey({
+ userId: ctx.user.id,
+ passkeyId,
+ requestMetadata: ctx.metadata.requestMetadata,
+ });
+ });
diff --git a/packages/trpc/server/auth-router/delete-passkey.types.ts b/packages/trpc/server/auth-router/delete-passkey.types.ts
new file mode 100644
index 000000000..7cfc4eacf
--- /dev/null
+++ b/packages/trpc/server/auth-router/delete-passkey.types.ts
@@ -0,0 +1,10 @@
+import { z } from 'zod';
+
+export const ZDeletePasskeyRequestSchema = z.object({
+ passkeyId: z.string().trim().min(1),
+});
+
+export const ZDeletePasskeyResponseSchema = z.void();
+
+export type TDeletePasskeyRequest = z.infer;
+export type TDeletePasskeyResponse = z.infer;
diff --git a/packages/trpc/server/auth-router/find-passkeys.ts b/packages/trpc/server/auth-router/find-passkeys.ts
new file mode 100644
index 000000000..ff7e5be79
--- /dev/null
+++ b/packages/trpc/server/auth-router/find-passkeys.ts
@@ -0,0 +1,18 @@
+import { findPasskeys } from '@documenso/lib/server-only/auth/find-passkeys';
+
+import { authenticatedProcedure } from '../trpc';
+import { ZFindPasskeysRequestSchema, ZFindPasskeysResponseSchema } from './find-passkeys.types';
+
+export const findPasskeysRoute = authenticatedProcedure
+ .input(ZFindPasskeysRequestSchema)
+ .output(ZFindPasskeysResponseSchema)
+ .query(async ({ input, ctx }) => {
+ const { page, perPage, orderBy } = input;
+
+ return await findPasskeys({
+ page,
+ perPage,
+ orderBy,
+ userId: ctx.user.id,
+ });
+ });
diff --git a/packages/trpc/server/auth-router/find-passkeys.types.ts b/packages/trpc/server/auth-router/find-passkeys.types.ts
new file mode 100644
index 000000000..1982f430e
--- /dev/null
+++ b/packages/trpc/server/auth-router/find-passkeys.types.ts
@@ -0,0 +1,33 @@
+import { z } from 'zod';
+
+import { ZFindResultResponse, ZFindSearchParamsSchema } from '@documenso/lib/types/search-params';
+import PasskeySchema from '@documenso/prisma/generated/zod/modelSchema/PasskeySchema';
+
+export const ZFindPasskeysRequestSchema = ZFindSearchParamsSchema.extend({
+ orderBy: z
+ .object({
+ column: z.enum(['createdAt', 'updatedAt', 'name']),
+ direction: z.enum(['asc', 'desc']),
+ })
+ .optional(),
+});
+
+export const ZFindPasskeysResponseSchema = ZFindResultResponse.extend({
+ data: z.array(
+ PasskeySchema.pick({
+ id: true,
+ userId: true,
+ name: true,
+ createdAt: true,
+ updatedAt: true,
+ lastUsedAt: true,
+ counter: true,
+ credentialDeviceType: true,
+ credentialBackedUp: true,
+ transports: true,
+ }),
+ ),
+});
+
+export type TFindPasskeysRequest = z.infer;
+export type TFindPasskeysResponse = z.infer;
diff --git a/packages/trpc/server/auth-router/router.ts b/packages/trpc/server/auth-router/router.ts
index 2eb54d1e6..5fc4b2c99 100644
--- a/packages/trpc/server/auth-router/router.ts
+++ b/packages/trpc/server/auth-router/router.ts
@@ -1,113 +1,20 @@
-import type { RegistrationResponseJSON } from '@simplewebauthn/types';
-
-import { createPasskey } from '@documenso/lib/server-only/auth/create-passkey';
-import { createPasskeyAuthenticationOptions } from '@documenso/lib/server-only/auth/create-passkey-authentication-options';
-import { createPasskeyRegistrationOptions } from '@documenso/lib/server-only/auth/create-passkey-registration-options';
-import { createPasskeySigninOptions } from '@documenso/lib/server-only/auth/create-passkey-signin-options';
-import { deletePasskey } from '@documenso/lib/server-only/auth/delete-passkey';
-import { findPasskeys } from '@documenso/lib/server-only/auth/find-passkeys';
-import { updatePasskey } from '@documenso/lib/server-only/auth/update-passkey';
-import { nanoid } from '@documenso/lib/universal/id';
-
-import { authenticatedProcedure, procedure, router } from '../trpc';
-import {
- ZCreatePasskeyAuthenticationOptionsMutationSchema,
- ZCreatePasskeyMutationSchema,
- ZDeletePasskeyMutationSchema,
- ZFindPasskeysQuerySchema,
- ZUpdatePasskeyMutationSchema,
-} from './schema';
+import { router } from '../trpc';
+import { createPasskeyRoute } from './create-passkey';
+import { createPasskeyAuthenticationOptionsRoute } from './create-passkey-authentication-options';
+import { createPasskeyRegistrationOptionsRoute } from './create-passkey-registration-options';
+import { createPasskeySigninOptionsRoute } from './create-passkey-signin-options';
+import { deletePasskeyRoute } from './delete-passkey';
+import { findPasskeysRoute } from './find-passkeys';
+import { updatePasskeyRoute } from './update-passkey';
export const authRouter = router({
- createPasskey: authenticatedProcedure
- .input(ZCreatePasskeyMutationSchema)
- .mutation(async ({ ctx, input }) => {
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
- const verificationResponse = input.verificationResponse as RegistrationResponseJSON;
-
- return await createPasskey({
- userId: ctx.user.id,
- verificationResponse,
- passkeyName: input.passkeyName,
- requestMetadata: ctx.metadata.requestMetadata,
- });
- }),
-
- createPasskeyAuthenticationOptions: authenticatedProcedure
- .input(ZCreatePasskeyAuthenticationOptionsMutationSchema)
- .mutation(async ({ ctx, input }) => {
- return await createPasskeyAuthenticationOptions({
- userId: ctx.user.id,
- preferredPasskeyId: input?.preferredPasskeyId,
- });
- }),
-
- createPasskeyRegistrationOptions: authenticatedProcedure.mutation(async ({ ctx }) => {
- return await createPasskeyRegistrationOptions({
- userId: ctx.user.id,
- });
+ passkey: router({
+ create: createPasskeyRoute,
+ createAuthenticationOptions: createPasskeyAuthenticationOptionsRoute,
+ createRegistrationOptions: createPasskeyRegistrationOptionsRoute,
+ createSigninOptions: createPasskeySigninOptionsRoute,
+ delete: deletePasskeyRoute,
+ find: findPasskeysRoute,
+ update: updatePasskeyRoute,
}),
-
- createPasskeySigninOptions: procedure.mutation(async () => {
- const sessionIdToken = nanoid(16);
-
- const [sessionId] = decodeURI(sessionIdToken).split('|');
-
- const options = await createPasskeySigninOptions({ sessionId });
-
- return {
- options,
- sessionId,
- };
- }),
-
- deletePasskey: authenticatedProcedure
- .input(ZDeletePasskeyMutationSchema)
- .mutation(async ({ ctx, input }) => {
- const { passkeyId } = input;
-
- ctx.logger.info({
- input: {
- passkeyId,
- },
- });
-
- await deletePasskey({
- userId: ctx.user.id,
- passkeyId,
- requestMetadata: ctx.metadata.requestMetadata,
- });
- }),
-
- findPasskeys: authenticatedProcedure
- .input(ZFindPasskeysQuerySchema)
- .query(async ({ input, ctx }) => {
- const { page, perPage, orderBy } = input;
-
- return await findPasskeys({
- page,
- perPage,
- orderBy,
- userId: ctx.user.id,
- });
- }),
-
- updatePasskey: authenticatedProcedure
- .input(ZUpdatePasskeyMutationSchema)
- .mutation(async ({ ctx, input }) => {
- const { passkeyId, name } = input;
-
- ctx.logger.info({
- input: {
- passkeyId,
- },
- });
-
- await updatePasskey({
- userId: ctx.user.id,
- passkeyId,
- name,
- requestMetadata: ctx.metadata.requestMetadata,
- });
- }),
});
diff --git a/packages/trpc/server/auth-router/schema.ts b/packages/trpc/server/auth-router/schema.ts
index 55ea2167d..91c8d5d4e 100644
--- a/packages/trpc/server/auth-router/schema.ts
+++ b/packages/trpc/server/auth-router/schema.ts
@@ -1,8 +1,5 @@
import { z } from 'zod';
-import { ZFindSearchParamsSchema } from '@documenso/lib/types/search-params';
-import { ZRegistrationResponseJSONSchema } from '@documenso/lib/types/webauthn';
-
export const ZCurrentPasswordSchema = z
.string()
.min(6, { message: 'Must be at least 6 characters in length' })
@@ -24,50 +21,3 @@ export const ZPasswordSchema = z
.refine((value) => value.length > 25 || /[`~<>?,./!@#$%^&*()\-_"'+=|{}[\];:\\]/.test(value), {
message: 'One special character is required',
});
-
-export const ZSignUpMutationSchema = z.object({
- name: z.string().min(1),
- email: z.string().email(),
- password: ZPasswordSchema,
- signature: z.string().nullish(),
- url: z
- .string()
- .trim()
- .toLowerCase()
- .min(1)
- .regex(/^[a-z0-9-]+$/, {
- message: 'Username can only container alphanumeric characters and dashes.',
- })
- .optional(),
-});
-
-export const ZCreatePasskeyMutationSchema = z.object({
- passkeyName: z.string().trim().min(1),
- verificationResponse: ZRegistrationResponseJSONSchema,
-});
-
-export const ZCreatePasskeyAuthenticationOptionsMutationSchema = z
- .object({
- preferredPasskeyId: z.string().optional(),
- })
- .optional();
-
-export const ZDeletePasskeyMutationSchema = z.object({
- passkeyId: z.string().trim().min(1),
-});
-
-export const ZUpdatePasskeyMutationSchema = z.object({
- passkeyId: z.string().trim().min(1),
- name: z.string().trim().min(1),
-});
-
-export const ZFindPasskeysQuerySchema = ZFindSearchParamsSchema.extend({
- orderBy: z
- .object({
- column: z.enum(['createdAt', 'updatedAt', 'name']),
- direction: z.enum(['asc', 'desc']),
- })
- .optional(),
-});
-
-export type TSignUpMutationSchema = z.infer;
diff --git a/packages/trpc/server/auth-router/update-passkey.ts b/packages/trpc/server/auth-router/update-passkey.ts
new file mode 100644
index 000000000..eeec06653
--- /dev/null
+++ b/packages/trpc/server/auth-router/update-passkey.ts
@@ -0,0 +1,24 @@
+import { updatePasskey } from '@documenso/lib/server-only/auth/update-passkey';
+
+import { authenticatedProcedure } from '../trpc';
+import { ZUpdatePasskeyRequestSchema, ZUpdatePasskeyResponseSchema } from './update-passkey.types';
+
+export const updatePasskeyRoute = authenticatedProcedure
+ .input(ZUpdatePasskeyRequestSchema)
+ .output(ZUpdatePasskeyResponseSchema)
+ .mutation(async ({ ctx, input }) => {
+ const { passkeyId, name } = input;
+
+ ctx.logger.info({
+ input: {
+ passkeyId,
+ },
+ });
+
+ await updatePasskey({
+ userId: ctx.user.id,
+ passkeyId,
+ name,
+ requestMetadata: ctx.metadata.requestMetadata,
+ });
+ });
diff --git a/packages/trpc/server/auth-router/update-passkey.types.ts b/packages/trpc/server/auth-router/update-passkey.types.ts
new file mode 100644
index 000000000..e898234da
--- /dev/null
+++ b/packages/trpc/server/auth-router/update-passkey.types.ts
@@ -0,0 +1,11 @@
+import { z } from 'zod';
+
+export const ZUpdatePasskeyRequestSchema = z.object({
+ passkeyId: z.string().trim().min(1),
+ name: z.string().trim().min(1),
+});
+
+export const ZUpdatePasskeyResponseSchema = z.void();
+
+export type TUpdatePasskeyRequest = z.infer;
+export type TUpdatePasskeyResponse = z.infer;
diff --git a/packages/trpc/server/document-router/create-document-temporary.ts b/packages/trpc/server/document-router/create-document-temporary.ts
new file mode 100644
index 000000000..2585aec7a
--- /dev/null
+++ b/packages/trpc/server/document-router/create-document-temporary.ts
@@ -0,0 +1,92 @@
+import { DocumentDataType } from '@prisma/client';
+
+import { getServerLimits } from '@documenso/ee/server-only/limits/server';
+import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
+import { createDocumentData } from '@documenso/lib/server-only/document-data/create-document-data';
+import { createDocumentV2 } from '@documenso/lib/server-only/document/create-document-v2';
+import { getPresignPostUrl } from '@documenso/lib/universal/upload/server-actions';
+import { isValidExpirySettings } from '@documenso/lib/utils/expiry';
+
+import { authenticatedProcedure } from '../trpc';
+import {
+ ZCreateDocumentTemporaryRequestSchema,
+ ZCreateDocumentTemporaryResponseSchema,
+ createDocumentTemporaryMeta,
+} from './create-document-temporary.types';
+
+/**
+ * Temporariy endpoint for V2 Beta until we allow passthrough documents on create.
+ *
+ * @public
+ * @deprecated
+ */
+export const createDocumentTemporaryRoute = authenticatedProcedure
+ .meta(createDocumentTemporaryMeta)
+ .input(ZCreateDocumentTemporaryRequestSchema)
+ .output(ZCreateDocumentTemporaryResponseSchema)
+ .mutation(async ({ input, ctx }) => {
+ const { teamId, user } = ctx;
+
+ const {
+ title,
+ externalId,
+ visibility,
+ globalAccessAuth,
+ globalActionAuth,
+ recipients,
+ meta,
+ folderId,
+ expiryAmount,
+ expiryUnit,
+ } = input;
+
+ // Validate expiry settings
+ if ((expiryAmount || expiryUnit) && !isValidExpirySettings(expiryAmount, expiryUnit)) {
+ throw new AppError(AppErrorCode.INVALID_REQUEST, {
+ message: 'Invalid expiry settings. Please check your expiry configuration.',
+ });
+ }
+
+ const { remaining } = await getServerLimits({ userId: user.id, teamId });
+
+ if (remaining.documents <= 0) {
+ throw new AppError(AppErrorCode.LIMIT_EXCEEDED, {
+ message: 'You have reached your document limit for this month. Please upgrade your plan.',
+ statusCode: 400,
+ });
+ }
+
+ const fileName = title.endsWith('.pdf') ? title : `${title}.pdf`;
+
+ const { url, key } = await getPresignPostUrl(fileName, 'application/pdf');
+
+ const documentData = await createDocumentData({
+ data: key,
+ type: DocumentDataType.S3_PATH,
+ });
+
+ const createdDocument = await createDocumentV2({
+ userId: ctx.user.id,
+ teamId,
+ documentDataId: documentData.id,
+ normalizePdf: false, // Not normalizing because of presigned URL.
+ data: {
+ title,
+ externalId,
+ visibility,
+ globalAccessAuth,
+ globalActionAuth,
+ recipients,
+ folderId,
+ expiryAmount,
+ expiryUnit,
+ },
+ meta,
+ requestMetadata: ctx.metadata,
+ });
+
+ return {
+ document: createdDocument,
+ uploadUrl: url,
+ };
+ });
diff --git a/packages/trpc/server/document-router/create-document-temporary.types.ts b/packages/trpc/server/document-router/create-document-temporary.types.ts
new file mode 100644
index 000000000..e14be757a
--- /dev/null
+++ b/packages/trpc/server/document-router/create-document-temporary.types.ts
@@ -0,0 +1,124 @@
+import { DocumentSigningOrder } from '@prisma/client';
+import { z } from 'zod';
+
+import { ZDocumentSchema } from '@documenso/lib/types/document';
+import {
+ ZDocumentAccessAuthTypesSchema,
+ ZDocumentActionAuthTypesSchema,
+} from '@documenso/lib/types/document-auth';
+import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
+import { ZDocumentFormValuesSchema } from '@documenso/lib/types/document-form-values';
+import {
+ ZFieldHeightSchema,
+ ZFieldPageNumberSchema,
+ ZFieldPageXSchema,
+ ZFieldPageYSchema,
+ ZFieldWidthSchema,
+} from '@documenso/lib/types/field';
+import { ZFieldAndMetaSchema } from '@documenso/lib/types/field-meta';
+
+import { ZCreateRecipientSchema } from '../recipient-router/schema';
+import type { TrpcRouteMeta } from '../trpc';
+import {
+ ZDocumentExpiryAmountSchema,
+ ZDocumentExpiryUnitSchema,
+ ZDocumentExternalIdSchema,
+ ZDocumentMetaDateFormatSchema,
+ ZDocumentMetaDistributionMethodSchema,
+ ZDocumentMetaDrawSignatureEnabledSchema,
+ ZDocumentMetaLanguageSchema,
+ ZDocumentMetaMessageSchema,
+ ZDocumentMetaRedirectUrlSchema,
+ ZDocumentMetaSubjectSchema,
+ ZDocumentMetaTimezoneSchema,
+ ZDocumentMetaTypedSignatureEnabledSchema,
+ ZDocumentMetaUploadSignatureEnabledSchema,
+ ZDocumentTitleSchema,
+ ZDocumentVisibilitySchema,
+} from './schema';
+
+/**
+ * Temporariy endpoint for V2 Beta until we allow passthrough documents on create.
+ */
+export const createDocumentTemporaryMeta: TrpcRouteMeta = {
+ openapi: {
+ method: 'POST',
+ path: '/document/create/beta',
+ summary: 'Create document',
+ description:
+ 'You will need to upload the PDF to the provided URL returned. Note: Once V2 API is released, this will be removed since we will allow direct uploads, instead of using an upload URL.',
+ tags: ['Document'],
+ },
+};
+
+export const ZCreateDocumentTemporaryRequestSchema = z.object({
+ title: ZDocumentTitleSchema,
+ externalId: ZDocumentExternalIdSchema.optional(),
+ visibility: ZDocumentVisibilitySchema.optional(),
+ globalAccessAuth: z.array(ZDocumentAccessAuthTypesSchema).optional(),
+ globalActionAuth: z.array(ZDocumentActionAuthTypesSchema).optional(),
+ formValues: ZDocumentFormValuesSchema.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(),
+ expiryAmount: ZDocumentExpiryAmountSchema.optional(),
+ expiryUnit: ZDocumentExpiryUnitSchema.optional(),
+ recipients: z
+ .array(
+ ZCreateRecipientSchema.extend({
+ fields: ZFieldAndMetaSchema.and(
+ z.object({
+ pageNumber: ZFieldPageNumberSchema,
+ pageX: ZFieldPageXSchema,
+ pageY: ZFieldPageYSchema,
+ width: ZFieldWidthSchema,
+ height: ZFieldHeightSchema,
+ }),
+ )
+ .array()
+ .optional(),
+ }),
+ )
+ .refine(
+ (recipients) => {
+ const emails = recipients.map((recipient) => recipient.email);
+
+ return new Set(emails).size === emails.length;
+ },
+ { message: 'Recipients must have unique emails' },
+ )
+ .optional(),
+ meta: z
+ .object({
+ subject: ZDocumentMetaSubjectSchema.optional(),
+ message: ZDocumentMetaMessageSchema.optional(),
+ timezone: ZDocumentMetaTimezoneSchema.optional(),
+ dateFormat: ZDocumentMetaDateFormatSchema.optional(),
+ distributionMethod: ZDocumentMetaDistributionMethodSchema.optional(),
+ signingOrder: z.nativeEnum(DocumentSigningOrder).optional(),
+ redirectUrl: ZDocumentMetaRedirectUrlSchema.optional(),
+ language: ZDocumentMetaLanguageSchema.optional(),
+ typedSignatureEnabled: ZDocumentMetaTypedSignatureEnabledSchema.optional(),
+ drawSignatureEnabled: ZDocumentMetaDrawSignatureEnabledSchema.optional(),
+ uploadSignatureEnabled: ZDocumentMetaUploadSignatureEnabledSchema.optional(),
+ emailSettings: ZDocumentEmailSettingsSchema.optional(),
+ })
+ .optional(),
+});
+
+export const ZCreateDocumentTemporaryResponseSchema = z.object({
+ document: ZDocumentSchema,
+ uploadUrl: z
+ .string()
+ .describe(
+ 'The URL to upload the document PDF to. Use a PUT request with the file via form-data',
+ ),
+});
+
+export type TCreateDocumentTemporaryRequest = z.infer;
+export type TCreateDocumentTemporaryResponse = z.infer<
+ typeof ZCreateDocumentTemporaryResponseSchema
+>;
diff --git a/packages/trpc/server/document-router/create-document.ts b/packages/trpc/server/document-router/create-document.ts
new file mode 100644
index 000000000..747450a43
--- /dev/null
+++ b/packages/trpc/server/document-router/create-document.ts
@@ -0,0 +1,57 @@
+import { getServerLimits } from '@documenso/ee/server-only/limits/server';
+import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
+import { createDocument } from '@documenso/lib/server-only/document/create-document';
+import { isValidExpirySettings } from '@documenso/lib/utils/expiry';
+
+import { authenticatedProcedure } from '../trpc';
+import {
+ ZCreateDocumentRequestSchema,
+ ZCreateDocumentResponseSchema,
+} from './create-document.types';
+
+export const createDocumentRoute = authenticatedProcedure
+ .input(ZCreateDocumentRequestSchema)
+ .output(ZCreateDocumentResponseSchema)
+ .mutation(async ({ input, ctx }) => {
+ const { user, teamId } = ctx;
+ const { title, documentDataId, timezone, folderId, expiryAmount, expiryUnit } = input;
+
+ // Validate expiry settings
+ if ((expiryAmount || expiryUnit) && !isValidExpirySettings(expiryAmount, expiryUnit)) {
+ throw new AppError(AppErrorCode.INVALID_REQUEST, {
+ message: 'Invalid expiry settings. Please check your expiry configuration.',
+ });
+ }
+
+ ctx.logger.info({
+ input: {
+ folderId,
+ },
+ });
+
+ const { remaining } = await getServerLimits({ userId: user.id, teamId });
+
+ if (remaining.documents <= 0) {
+ throw new AppError(AppErrorCode.LIMIT_EXCEEDED, {
+ message: 'You have reached your document limit for this month. Please upgrade your plan.',
+ statusCode: 400,
+ });
+ }
+
+ const document = await createDocument({
+ userId: user.id,
+ teamId,
+ title,
+ documentDataId,
+ normalizePdf: true,
+ userTimezone: timezone,
+ requestMetadata: ctx.metadata,
+ folderId,
+ expiryAmount,
+ expiryUnit,
+ });
+
+ return {
+ id: document.id,
+ };
+ });
diff --git a/packages/trpc/server/document-router/create-document.types.ts b/packages/trpc/server/document-router/create-document.types.ts
new file mode 100644
index 000000000..e9ffb1050
--- /dev/null
+++ b/packages/trpc/server/document-router/create-document.types.ts
@@ -0,0 +1,34 @@
+import { z } from 'zod';
+
+import {
+ ZDocumentExpiryAmountSchema,
+ ZDocumentExpiryUnitSchema,
+ ZDocumentMetaTimezoneSchema,
+ ZDocumentTitleSchema,
+} from './schema';
+
+// Currently not in use until we allow passthrough documents on create.
+// export const createDocumentMeta: TrpcRouteMeta = {
+// openapi: {
+// method: 'POST',
+// path: '/document/create',
+// summary: 'Create document',
+// tags: ['Document'],
+// },
+// };
+
+export const ZCreateDocumentRequestSchema = z.object({
+ title: ZDocumentTitleSchema,
+ documentDataId: z.string().min(1),
+ timezone: ZDocumentMetaTimezoneSchema.optional(),
+ folderId: z.string().describe('The ID of the folder to create the document in').optional(),
+ expiryAmount: ZDocumentExpiryAmountSchema.optional(),
+ expiryUnit: ZDocumentExpiryUnitSchema.optional(),
+});
+
+export const ZCreateDocumentResponseSchema = z.object({
+ id: z.number(),
+});
+
+export type TCreateDocumentRequest = z.infer;
+export type TCreateDocumentResponse = z.infer;
diff --git a/packages/trpc/server/document-router/delete-document.ts b/packages/trpc/server/document-router/delete-document.ts
new file mode 100644
index 000000000..659ff0394
--- /dev/null
+++ b/packages/trpc/server/document-router/delete-document.ts
@@ -0,0 +1,35 @@
+import { deleteDocument } from '@documenso/lib/server-only/document/delete-document';
+
+import { authenticatedProcedure } from '../trpc';
+import {
+ ZDeleteDocumentRequestSchema,
+ ZDeleteDocumentResponseSchema,
+ deleteDocumentMeta,
+} from './delete-document.types';
+import { ZGenericSuccessResponse } from './schema';
+
+export const deleteDocumentRoute = authenticatedProcedure
+ .meta(deleteDocumentMeta)
+ .input(ZDeleteDocumentRequestSchema)
+ .output(ZDeleteDocumentResponseSchema)
+ .mutation(async ({ input, ctx }) => {
+ const { teamId } = ctx;
+ const { documentId } = input;
+
+ ctx.logger.info({
+ input: {
+ documentId,
+ },
+ });
+
+ const userId = ctx.user.id;
+
+ await deleteDocument({
+ id: documentId,
+ userId,
+ teamId,
+ requestMetadata: ctx.metadata,
+ });
+
+ return ZGenericSuccessResponse;
+ });
diff --git a/packages/trpc/server/document-router/delete-document.types.ts b/packages/trpc/server/document-router/delete-document.types.ts
new file mode 100644
index 000000000..72c7a711d
--- /dev/null
+++ b/packages/trpc/server/document-router/delete-document.types.ts
@@ -0,0 +1,22 @@
+import { z } from 'zod';
+
+import type { TrpcRouteMeta } from '../trpc';
+import { ZSuccessResponseSchema } from './schema';
+
+export const deleteDocumentMeta: TrpcRouteMeta = {
+ openapi: {
+ method: 'POST',
+ path: '/document/delete',
+ summary: 'Delete document',
+ tags: ['Document'],
+ },
+};
+
+export const ZDeleteDocumentRequestSchema = z.object({
+ documentId: z.number(),
+});
+
+export const ZDeleteDocumentResponseSchema = ZSuccessResponseSchema;
+
+export type TDeleteDocumentRequest = z.infer;
+export type TDeleteDocumentResponse = z.infer;
diff --git a/packages/trpc/server/document-router/distribute-document.ts b/packages/trpc/server/document-router/distribute-document.ts
new file mode 100644
index 000000000..00fe8ef92
--- /dev/null
+++ b/packages/trpc/server/document-router/distribute-document.ts
@@ -0,0 +1,50 @@
+import { upsertDocumentMeta } from '@documenso/lib/server-only/document-meta/upsert-document-meta';
+import { sendDocument } from '@documenso/lib/server-only/document/send-document';
+
+import { authenticatedProcedure } from '../trpc';
+import {
+ ZDistributeDocumentRequestSchema,
+ ZDistributeDocumentResponseSchema,
+ distributeDocumentMeta,
+} from './distribute-document.types';
+
+export const distributeDocumentRoute = authenticatedProcedure
+ .meta(distributeDocumentMeta)
+ .input(ZDistributeDocumentRequestSchema)
+ .output(ZDistributeDocumentResponseSchema)
+ .mutation(async ({ input, ctx }) => {
+ const { teamId } = ctx;
+ const { documentId, meta = {} } = input;
+
+ ctx.logger.info({
+ input: {
+ documentId,
+ },
+ });
+
+ if (Object.values(meta).length > 0) {
+ await upsertDocumentMeta({
+ userId: ctx.user.id,
+ teamId,
+ documentId,
+ subject: meta.subject,
+ message: meta.message,
+ dateFormat: meta.dateFormat,
+ timezone: meta.timezone,
+ redirectUrl: meta.redirectUrl,
+ distributionMethod: meta.distributionMethod,
+ emailSettings: meta.emailSettings,
+ language: meta.language,
+ emailId: meta.emailId,
+ emailReplyTo: meta.emailReplyTo,
+ requestMetadata: ctx.metadata,
+ });
+ }
+
+ return await sendDocument({
+ userId: ctx.user.id,
+ documentId,
+ teamId,
+ requestMetadata: ctx.metadata,
+ });
+ });
diff --git a/packages/trpc/server/document-router/distribute-document.types.ts b/packages/trpc/server/document-router/distribute-document.types.ts
new file mode 100644
index 000000000..41bf23eb2
--- /dev/null
+++ b/packages/trpc/server/document-router/distribute-document.types.ts
@@ -0,0 +1,48 @@
+import { z } from 'zod';
+
+import { ZDocumentLiteSchema } from '@documenso/lib/types/document';
+import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
+
+import type { TrpcRouteMeta } from '../trpc';
+import {
+ ZDocumentMetaDateFormatSchema,
+ ZDocumentMetaDistributionMethodSchema,
+ ZDocumentMetaLanguageSchema,
+ ZDocumentMetaMessageSchema,
+ ZDocumentMetaRedirectUrlSchema,
+ ZDocumentMetaSubjectSchema,
+ ZDocumentMetaTimezoneSchema,
+} from './schema';
+
+export const distributeDocumentMeta: TrpcRouteMeta = {
+ openapi: {
+ method: 'POST',
+ path: '/document/distribute',
+ summary: 'Distribute document',
+ description: 'Send the document out to recipients based on your distribution method',
+ tags: ['Document'],
+ },
+};
+
+export const ZDistributeDocumentRequestSchema = z.object({
+ documentId: z.number().describe('The ID of the document to send.'),
+ meta: z
+ .object({
+ subject: ZDocumentMetaSubjectSchema.optional(),
+ message: ZDocumentMetaMessageSchema.optional(),
+ timezone: ZDocumentMetaTimezoneSchema.optional(),
+ dateFormat: ZDocumentMetaDateFormatSchema.optional(),
+ distributionMethod: ZDocumentMetaDistributionMethodSchema.optional(),
+ redirectUrl: ZDocumentMetaRedirectUrlSchema.optional(),
+ language: ZDocumentMetaLanguageSchema.optional(),
+ emailId: z.string().nullish(),
+ emailReplyTo: z.string().email().nullish(),
+ emailSettings: ZDocumentEmailSettingsSchema.optional(),
+ })
+ .optional(),
+});
+
+export const ZDistributeDocumentResponseSchema = ZDocumentLiteSchema;
+
+export type TDistributeDocumentRequest = z.infer;
+export type TDistributeDocumentResponse = z.infer;
diff --git a/packages/trpc/server/document-router/download-document-audit-logs.ts b/packages/trpc/server/document-router/download-document-audit-logs.ts
new file mode 100644
index 000000000..af84c43e0
--- /dev/null
+++ b/packages/trpc/server/document-router/download-document-audit-logs.ts
@@ -0,0 +1,47 @@
+import { DateTime } from 'luxon';
+
+import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
+import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
+import { encryptSecondaryData } from '@documenso/lib/server-only/crypto/encrypt';
+import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id';
+
+import { authenticatedProcedure } from '../trpc';
+import {
+ ZDownloadDocumentAuditLogsRequestSchema,
+ ZDownloadDocumentAuditLogsResponseSchema,
+} from './download-document-audit-logs.types';
+
+export const downloadDocumentAuditLogsRoute = authenticatedProcedure
+ .input(ZDownloadDocumentAuditLogsRequestSchema)
+ .output(ZDownloadDocumentAuditLogsResponseSchema)
+ .mutation(async ({ input, ctx }) => {
+ const { teamId } = ctx;
+ const { documentId } = input;
+
+ ctx.logger.info({
+ input: {
+ documentId,
+ },
+ });
+
+ const document = await getDocumentById({
+ documentId,
+ userId: ctx.user.id,
+ teamId,
+ }).catch(() => null);
+
+ if (!document || (teamId && document.teamId !== teamId)) {
+ throw new AppError(AppErrorCode.UNAUTHORIZED, {
+ message: 'You do not have access to this document.',
+ });
+ }
+
+ const encrypted = encryptSecondaryData({
+ data: document.id.toString(),
+ expiresAt: DateTime.now().plus({ minutes: 5 }).toJSDate().valueOf(),
+ });
+
+ return {
+ url: `${NEXT_PUBLIC_WEBAPP_URL()}/__htmltopdf/audit-log?d=${encrypted}`,
+ };
+ });
diff --git a/packages/trpc/server/document-router/download-document-audit-logs.types.ts b/packages/trpc/server/document-router/download-document-audit-logs.types.ts
new file mode 100644
index 000000000..b4cc209c2
--- /dev/null
+++ b/packages/trpc/server/document-router/download-document-audit-logs.types.ts
@@ -0,0 +1,16 @@
+import { z } from 'zod';
+
+export const ZDownloadDocumentAuditLogsRequestSchema = z.object({
+ documentId: z.number(),
+});
+
+export const ZDownloadDocumentAuditLogsResponseSchema = z.object({
+ url: z.string(),
+});
+
+export type TDownloadDocumentAuditLogsRequest = z.infer<
+ typeof ZDownloadDocumentAuditLogsRequestSchema
+>;
+export type TDownloadDocumentAuditLogsResponse = z.infer<
+ typeof ZDownloadDocumentAuditLogsResponseSchema
+>;
diff --git a/packages/trpc/server/document-router/download-document-certificate.ts b/packages/trpc/server/document-router/download-document-certificate.ts
new file mode 100644
index 000000000..b59eafbf0
--- /dev/null
+++ b/packages/trpc/server/document-router/download-document-certificate.ts
@@ -0,0 +1,46 @@
+import { DateTime } from 'luxon';
+
+import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
+import { AppError } from '@documenso/lib/errors/app-error';
+import { encryptSecondaryData } from '@documenso/lib/server-only/crypto/encrypt';
+import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id';
+import { isDocumentCompleted } from '@documenso/lib/utils/document';
+
+import { authenticatedProcedure } from '../trpc';
+import {
+ ZDownloadDocumentCertificateRequestSchema,
+ ZDownloadDocumentCertificateResponseSchema,
+} from './download-document-certificate.types';
+
+export const downloadDocumentCertificateRoute = authenticatedProcedure
+ .input(ZDownloadDocumentCertificateRequestSchema)
+ .output(ZDownloadDocumentCertificateResponseSchema)
+ .mutation(async ({ input, ctx }) => {
+ const { teamId } = ctx;
+ const { documentId } = input;
+
+ ctx.logger.info({
+ input: {
+ documentId,
+ },
+ });
+
+ const document = await getDocumentById({
+ documentId,
+ userId: ctx.user.id,
+ teamId,
+ });
+
+ if (!isDocumentCompleted(document.status)) {
+ throw new AppError('DOCUMENT_NOT_COMPLETE');
+ }
+
+ const encrypted = encryptSecondaryData({
+ data: document.id.toString(),
+ expiresAt: DateTime.now().plus({ minutes: 5 }).toJSDate().valueOf(),
+ });
+
+ return {
+ url: `${NEXT_PUBLIC_WEBAPP_URL()}/__htmltopdf/certificate?d=${encrypted}`,
+ };
+ });
diff --git a/packages/trpc/server/document-router/download-document-certificate.types.ts b/packages/trpc/server/document-router/download-document-certificate.types.ts
new file mode 100644
index 000000000..df81f1cad
--- /dev/null
+++ b/packages/trpc/server/document-router/download-document-certificate.types.ts
@@ -0,0 +1,16 @@
+import { z } from 'zod';
+
+export const ZDownloadDocumentCertificateRequestSchema = z.object({
+ documentId: z.number(),
+});
+
+export const ZDownloadDocumentCertificateResponseSchema = z.object({
+ url: z.string(),
+});
+
+export type TDownloadDocumentCertificateRequest = z.infer<
+ typeof ZDownloadDocumentCertificateRequestSchema
+>;
+export type TDownloadDocumentCertificateResponse = z.infer<
+ typeof ZDownloadDocumentCertificateResponseSchema
+>;
diff --git a/packages/trpc/server/document-router/download-document.ts b/packages/trpc/server/document-router/download-document.ts
index 84b75265c..a0cbbf104 100644
--- a/packages/trpc/server/document-router/download-document.ts
+++ b/packages/trpc/server/document-router/download-document.ts
@@ -6,18 +6,14 @@ import { getPresignGetUrl } from '@documenso/lib/universal/upload/server-actions
import { isDocumentCompleted } from '@documenso/lib/utils/document';
import { authenticatedProcedure } from '../trpc';
-import { ZDownloadDocumentRequestSchema, ZDownloadDocumentResponseSchema } from './schema';
+import {
+ ZDownloadDocumentRequestSchema,
+ ZDownloadDocumentResponseSchema,
+ downloadDocumentMeta,
+} from './download-document.types';
export const downloadDocumentRoute = authenticatedProcedure
- .meta({
- openapi: {
- method: 'GET',
- path: '/document/{documentId}/download-beta',
- summary: 'Download document (beta)',
- description: 'Get a pre-signed download URL for the original or signed version of a document',
- tags: ['Document'],
- },
- })
+ .meta(downloadDocumentMeta)
.input(ZDownloadDocumentRequestSchema)
.output(ZDownloadDocumentResponseSchema)
.query(async ({ input, ctx }) => {
diff --git a/packages/trpc/server/document-router/download-document.types.ts b/packages/trpc/server/document-router/download-document.types.ts
new file mode 100644
index 000000000..be4f454f8
--- /dev/null
+++ b/packages/trpc/server/document-router/download-document.types.ts
@@ -0,0 +1,32 @@
+import { z } from 'zod';
+
+import type { TrpcRouteMeta } from '../trpc';
+
+export const downloadDocumentMeta: TrpcRouteMeta = {
+ openapi: {
+ method: 'GET',
+ path: '/document/{documentId}/download-beta',
+ summary: 'Download document (beta)',
+ description: 'Get a pre-signed download URL for the original or signed version of a document',
+ tags: ['Document'],
+ },
+};
+
+export const ZDownloadDocumentRequestSchema = z.object({
+ documentId: z.number().describe('The ID of the document to download.'),
+ version: z
+ .enum(['original', 'signed'])
+ .describe(
+ 'The version of the document to download. "signed" returns the completed document with signatures, "original" returns the original uploaded document.',
+ )
+ .default('signed'),
+});
+
+export const ZDownloadDocumentResponseSchema = z.object({
+ downloadUrl: z.string().describe('Pre-signed URL for downloading the PDF file'),
+ filename: z.string().describe('The filename of the PDF file'),
+ contentType: z.string().describe('MIME type of the file'),
+});
+
+export type TDownloadDocumentRequest = z.infer;
+export type TDownloadDocumentResponse = z.infer;
diff --git a/packages/trpc/server/document-router/duplicate-document.ts b/packages/trpc/server/document-router/duplicate-document.ts
new file mode 100644
index 000000000..be8f29d03
--- /dev/null
+++ b/packages/trpc/server/document-router/duplicate-document.ts
@@ -0,0 +1,29 @@
+import { duplicateDocument } from '@documenso/lib/server-only/document/duplicate-document-by-id';
+
+import { authenticatedProcedure } from '../trpc';
+import {
+ ZDuplicateDocumentRequestSchema,
+ ZDuplicateDocumentResponseSchema,
+ duplicateDocumentMeta,
+} from './duplicate-document.types';
+
+export const duplicateDocumentRoute = authenticatedProcedure
+ .meta(duplicateDocumentMeta)
+ .input(ZDuplicateDocumentRequestSchema)
+ .output(ZDuplicateDocumentResponseSchema)
+ .mutation(async ({ input, ctx }) => {
+ const { teamId, user } = ctx;
+ const { documentId } = input;
+
+ ctx.logger.info({
+ input: {
+ documentId,
+ },
+ });
+
+ return await duplicateDocument({
+ userId: user.id,
+ teamId,
+ documentId,
+ });
+ });
diff --git a/packages/trpc/server/document-router/duplicate-document.types.ts b/packages/trpc/server/document-router/duplicate-document.types.ts
new file mode 100644
index 000000000..e77bccc73
--- /dev/null
+++ b/packages/trpc/server/document-router/duplicate-document.types.ts
@@ -0,0 +1,23 @@
+import { z } from 'zod';
+
+import type { TrpcRouteMeta } from '../trpc';
+
+export const duplicateDocumentMeta: TrpcRouteMeta = {
+ openapi: {
+ method: 'POST',
+ path: '/document/duplicate',
+ summary: 'Duplicate document',
+ tags: ['Document'],
+ },
+};
+
+export const ZDuplicateDocumentRequestSchema = z.object({
+ documentId: z.number(),
+});
+
+export const ZDuplicateDocumentResponseSchema = z.object({
+ documentId: z.number(),
+});
+
+export type TDuplicateDocumentRequest = z.infer;
+export type TDuplicateDocumentResponse = z.infer;
diff --git a/packages/trpc/server/document-router/find-document-audit-logs.ts b/packages/trpc/server/document-router/find-document-audit-logs.ts
new file mode 100644
index 000000000..17de388a3
--- /dev/null
+++ b/packages/trpc/server/document-router/find-document-audit-logs.ts
@@ -0,0 +1,41 @@
+import { findDocumentAuditLogs } from '@documenso/lib/server-only/document/find-document-audit-logs';
+
+import { authenticatedProcedure } from '../trpc';
+import {
+ ZFindDocumentAuditLogsRequestSchema,
+ ZFindDocumentAuditLogsResponseSchema,
+} from './find-document-audit-logs.types';
+
+export const findDocumentAuditLogsRoute = authenticatedProcedure
+ .input(ZFindDocumentAuditLogsRequestSchema)
+ .output(ZFindDocumentAuditLogsResponseSchema)
+ .query(async ({ input, ctx }) => {
+ const { teamId } = ctx;
+
+ const {
+ page,
+ perPage,
+ documentId,
+ cursor,
+ filterForRecentActivity,
+ orderByColumn,
+ orderByDirection,
+ } = input;
+
+ ctx.logger.info({
+ input: {
+ documentId,
+ },
+ });
+
+ return await findDocumentAuditLogs({
+ userId: ctx.user.id,
+ teamId,
+ page,
+ perPage,
+ documentId,
+ cursor,
+ filterForRecentActivity,
+ orderBy: orderByColumn ? { column: orderByColumn, direction: orderByDirection } : undefined,
+ });
+ });
diff --git a/packages/trpc/server/document-router/find-document-audit-logs.types.ts b/packages/trpc/server/document-router/find-document-audit-logs.types.ts
new file mode 100644
index 000000000..6e8991667
--- /dev/null
+++ b/packages/trpc/server/document-router/find-document-audit-logs.types.ts
@@ -0,0 +1,20 @@
+import { z } from 'zod';
+
+import { ZDocumentAuditLogSchema } from '@documenso/lib/types/document-audit-logs';
+import { ZFindResultResponse, ZFindSearchParamsSchema } from '@documenso/lib/types/search-params';
+
+export const ZFindDocumentAuditLogsRequestSchema = ZFindSearchParamsSchema.extend({
+ documentId: z.number().min(1),
+ cursor: z.string().optional(),
+ filterForRecentActivity: z.boolean().optional(),
+ orderByColumn: z.enum(['createdAt', 'type']).optional(),
+ orderByDirection: z.enum(['asc', 'desc']).default('desc'),
+});
+
+export const ZFindDocumentAuditLogsResponseSchema = ZFindResultResponse.extend({
+ data: ZDocumentAuditLogSchema.array(),
+ nextCursor: z.string().optional(),
+});
+
+export type TFindDocumentAuditLogsRequest = z.infer;
+export type TFindDocumentAuditLogsResponse = z.infer;
diff --git a/packages/trpc/server/document-router/find-documents-internal.ts b/packages/trpc/server/document-router/find-documents-internal.ts
new file mode 100644
index 000000000..47748771b
--- /dev/null
+++ b/packages/trpc/server/document-router/find-documents-internal.ts
@@ -0,0 +1,74 @@
+import { findDocuments } from '@documenso/lib/server-only/document/find-documents';
+import type { GetStatsInput } from '@documenso/lib/server-only/document/get-stats';
+import { getStats } from '@documenso/lib/server-only/document/get-stats';
+import { getTeamById } from '@documenso/lib/server-only/team/get-team';
+
+import { authenticatedProcedure } from '../trpc';
+import {
+ ZFindDocumentsInternalRequestSchema,
+ ZFindDocumentsInternalResponseSchema,
+} from './find-documents-internal.types';
+
+export const findDocumentsInternalRoute = authenticatedProcedure
+ .input(ZFindDocumentsInternalRequestSchema)
+ .output(ZFindDocumentsInternalResponseSchema)
+ .query(async ({ input, ctx }) => {
+ const { user, teamId } = ctx;
+
+ const {
+ query,
+ templateId,
+ page,
+ perPage,
+ orderByDirection,
+ orderByColumn,
+ source,
+ status,
+ period,
+ senderIds,
+ folderId,
+ } = input;
+
+ const getStatOptions: GetStatsInput = {
+ user,
+ period,
+ search: query,
+ folderId,
+ };
+
+ if (teamId) {
+ const team = await getTeamById({ userId: user.id, teamId });
+
+ getStatOptions.team = {
+ teamId: team.id,
+ teamEmail: team.teamEmail?.email,
+ senderIds,
+ currentTeamMemberRole: team.currentTeamRole,
+ currentUserEmail: user.email,
+ userId: user.id,
+ };
+ }
+
+ const [stats, documents] = await Promise.all([
+ getStats(getStatOptions),
+ findDocuments({
+ userId: user.id,
+ teamId,
+ query,
+ templateId,
+ page,
+ perPage,
+ source,
+ status,
+ period,
+ senderIds,
+ folderId,
+ orderBy: orderByColumn ? { column: orderByColumn, direction: orderByDirection } : undefined,
+ }),
+ ]);
+
+ return {
+ ...documents,
+ stats,
+ };
+ });
diff --git a/packages/trpc/server/document-router/find-documents-internal.types.ts b/packages/trpc/server/document-router/find-documents-internal.types.ts
new file mode 100644
index 000000000..16e8edb66
--- /dev/null
+++ b/packages/trpc/server/document-router/find-documents-internal.types.ts
@@ -0,0 +1,29 @@
+import { z } from 'zod';
+
+import { ZDocumentManySchema } from '@documenso/lib/types/document';
+import { ZFindResultResponse } from '@documenso/lib/types/search-params';
+import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status';
+
+import { ZFindDocumentsRequestSchema } from './find-documents.types';
+
+export const ZFindDocumentsInternalRequestSchema = ZFindDocumentsRequestSchema.extend({
+ period: z.enum(['7d', '14d', '30d']).optional(),
+ senderIds: z.array(z.number()).optional(),
+ status: z.nativeEnum(ExtendedDocumentStatus).optional(),
+ folderId: z.string().optional(),
+});
+
+export const ZFindDocumentsInternalResponseSchema = ZFindResultResponse.extend({
+ data: ZDocumentManySchema.array(),
+ stats: z.object({
+ [ExtendedDocumentStatus.DRAFT]: z.number(),
+ [ExtendedDocumentStatus.PENDING]: z.number(),
+ [ExtendedDocumentStatus.COMPLETED]: z.number(),
+ [ExtendedDocumentStatus.REJECTED]: z.number(),
+ [ExtendedDocumentStatus.INBOX]: z.number(),
+ [ExtendedDocumentStatus.ALL]: z.number(),
+ }),
+});
+
+export type TFindDocumentsInternalRequest = z.infer;
+export type TFindDocumentsInternalResponse = z.infer;
diff --git a/packages/trpc/server/document-router/find-documents.ts b/packages/trpc/server/document-router/find-documents.ts
new file mode 100644
index 000000000..71684b326
--- /dev/null
+++ b/packages/trpc/server/document-router/find-documents.ts
@@ -0,0 +1,43 @@
+import { findDocuments } from '@documenso/lib/server-only/document/find-documents';
+
+import { authenticatedProcedure } from '../trpc';
+import {
+ ZFindDocumentsMeta,
+ ZFindDocumentsRequestSchema,
+ ZFindDocumentsResponseSchema,
+} from './find-documents.types';
+
+export const findDocumentsRoute = authenticatedProcedure
+ .meta(ZFindDocumentsMeta)
+ .input(ZFindDocumentsRequestSchema)
+ .output(ZFindDocumentsResponseSchema)
+ .query(async ({ input, ctx }) => {
+ const { user, teamId } = ctx;
+
+ const {
+ query,
+ templateId,
+ page,
+ perPage,
+ orderByDirection,
+ orderByColumn,
+ source,
+ status,
+ folderId,
+ } = input;
+
+ const documents = await findDocuments({
+ userId: user.id,
+ teamId,
+ templateId,
+ query,
+ source,
+ status,
+ page,
+ perPage,
+ folderId,
+ orderBy: orderByColumn ? { column: orderByColumn, direction: orderByDirection } : undefined,
+ });
+
+ return documents;
+ });
diff --git a/packages/trpc/server/document-router/find-documents.types.ts b/packages/trpc/server/document-router/find-documents.types.ts
new file mode 100644
index 000000000..dafb047b6
--- /dev/null
+++ b/packages/trpc/server/document-router/find-documents.types.ts
@@ -0,0 +1,42 @@
+import { DocumentSource, DocumentStatus } from '@prisma/client';
+import { z } from 'zod';
+
+import { ZDocumentManySchema } from '@documenso/lib/types/document';
+import { ZFindResultResponse, ZFindSearchParamsSchema } from '@documenso/lib/types/search-params';
+
+import type { TrpcRouteMeta } from '../trpc';
+
+export const ZFindDocumentsMeta: TrpcRouteMeta = {
+ openapi: {
+ method: 'GET',
+ path: '/document',
+ summary: 'Find documents',
+ description: 'Find documents based on a search criteria',
+ tags: ['Document'],
+ },
+};
+
+export const ZFindDocumentsRequestSchema = ZFindSearchParamsSchema.extend({
+ templateId: z
+ .number()
+ .describe('Filter documents by the template ID used to create it.')
+ .optional(),
+ source: z
+ .nativeEnum(DocumentSource)
+ .describe('Filter documents by how it was created.')
+ .optional(),
+ status: z
+ .nativeEnum(DocumentStatus)
+ .describe('Filter documents by the current status')
+ .optional(),
+ folderId: z.string().describe('Filter documents by folder ID').optional(),
+ orderByColumn: z.enum(['createdAt']).optional(),
+ orderByDirection: z.enum(['asc', 'desc']).describe('').default('desc'),
+});
+
+export const ZFindDocumentsResponseSchema = ZFindResultResponse.extend({
+ data: ZDocumentManySchema.array(),
+});
+
+export type TFindDocumentsRequest = z.infer;
+export type TFindDocumentsResponse = z.infer;
diff --git a/packages/trpc/server/document-router/get-document-by-token.ts b/packages/trpc/server/document-router/get-document-by-token.ts
new file mode 100644
index 000000000..b640f6946
--- /dev/null
+++ b/packages/trpc/server/document-router/get-document-by-token.ts
@@ -0,0 +1,43 @@
+import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
+import { prisma } from '@documenso/prisma';
+
+import { authenticatedProcedure } from '../trpc';
+import {
+ ZGetDocumentByTokenRequestSchema,
+ ZGetDocumentByTokenResponseSchema,
+} from './get-document-by-token.types';
+
+export const getDocumentByTokenRoute = authenticatedProcedure
+ .input(ZGetDocumentByTokenRequestSchema)
+ .output(ZGetDocumentByTokenResponseSchema)
+ .query(async ({ input, ctx }) => {
+ const { token } = input;
+
+ const document = await prisma.document.findFirst({
+ where: {
+ recipients: {
+ some: {
+ token,
+ email: ctx.user.email,
+ },
+ },
+ },
+ include: {
+ documentData: true,
+ },
+ });
+
+ if (!document) {
+ throw new AppError(AppErrorCode.NOT_FOUND, {
+ message: 'Document not found',
+ });
+ }
+
+ ctx.logger.info({
+ documentId: document.id,
+ });
+
+ return {
+ documentData: document.documentData,
+ };
+ });
diff --git a/packages/trpc/server/document-router/get-document-by-token.types.ts b/packages/trpc/server/document-router/get-document-by-token.types.ts
new file mode 100644
index 000000000..34f79c620
--- /dev/null
+++ b/packages/trpc/server/document-router/get-document-by-token.types.ts
@@ -0,0 +1,14 @@
+import { z } from 'zod';
+
+import DocumentDataSchema from '@documenso/prisma/generated/zod/modelSchema/DocumentDataSchema';
+
+export const ZGetDocumentByTokenRequestSchema = z.object({
+ token: z.string().min(1),
+});
+
+export const ZGetDocumentByTokenResponseSchema = z.object({
+ documentData: DocumentDataSchema,
+});
+
+export type TGetDocumentByTokenRequest = z.infer;
+export type TGetDocumentByTokenResponse = z.infer;
diff --git a/packages/trpc/server/document-router/get-document.ts b/packages/trpc/server/document-router/get-document.ts
new file mode 100644
index 000000000..4dad5291f
--- /dev/null
+++ b/packages/trpc/server/document-router/get-document.ts
@@ -0,0 +1,29 @@
+import { getDocumentWithDetailsById } from '@documenso/lib/server-only/document/get-document-with-details-by-id';
+
+import { authenticatedProcedure } from '../trpc';
+import {
+ ZGetDocumentRequestSchema,
+ ZGetDocumentResponseSchema,
+ getDocumentMeta,
+} from './get-document.types';
+
+export const getDocumentRoute = authenticatedProcedure
+ .meta(getDocumentMeta)
+ .input(ZGetDocumentRequestSchema)
+ .output(ZGetDocumentResponseSchema)
+ .query(async ({ input, ctx }) => {
+ const { teamId, user } = ctx;
+ const { documentId } = input;
+
+ ctx.logger.info({
+ input: {
+ documentId,
+ },
+ });
+
+ return await getDocumentWithDetailsById({
+ userId: user.id,
+ teamId,
+ documentId,
+ });
+ });
diff --git a/packages/trpc/server/document-router/get-document.types.ts b/packages/trpc/server/document-router/get-document.types.ts
new file mode 100644
index 000000000..08072f021
--- /dev/null
+++ b/packages/trpc/server/document-router/get-document.types.ts
@@ -0,0 +1,24 @@
+import { z } from 'zod';
+
+import { ZDocumentSchema } from '@documenso/lib/types/document';
+
+import type { TrpcRouteMeta } from '../trpc';
+
+export const getDocumentMeta: TrpcRouteMeta = {
+ openapi: {
+ method: 'GET',
+ path: '/document/{documentId}',
+ summary: 'Get document',
+ description: 'Returns a document given an ID',
+ tags: ['Document'],
+ },
+};
+
+export const ZGetDocumentRequestSchema = z.object({
+ documentId: z.number(),
+});
+
+export const ZGetDocumentResponseSchema = ZDocumentSchema;
+
+export type TGetDocumentRequest = z.infer;
+export type TGetDocumentResponse = z.infer;
diff --git a/packages/trpc/server/document-router/redistribute-document.ts b/packages/trpc/server/document-router/redistribute-document.ts
new file mode 100644
index 000000000..6bc06e189
--- /dev/null
+++ b/packages/trpc/server/document-router/redistribute-document.ts
@@ -0,0 +1,35 @@
+import { resendDocument } from '@documenso/lib/server-only/document/resend-document';
+
+import { authenticatedProcedure } from '../trpc';
+import {
+ ZRedistributeDocumentRequestSchema,
+ ZRedistributeDocumentResponseSchema,
+ redistributeDocumentMeta,
+} from './redistribute-document.types';
+import { ZGenericSuccessResponse } from './schema';
+
+export const redistributeDocumentRoute = authenticatedProcedure
+ .meta(redistributeDocumentMeta)
+ .input(ZRedistributeDocumentRequestSchema)
+ .output(ZRedistributeDocumentResponseSchema)
+ .mutation(async ({ input, ctx }) => {
+ const { teamId } = ctx;
+ const { documentId, recipients } = input;
+
+ ctx.logger.info({
+ input: {
+ documentId,
+ recipients,
+ },
+ });
+
+ await resendDocument({
+ userId: ctx.user.id,
+ teamId,
+ documentId,
+ recipients,
+ requestMetadata: ctx.metadata,
+ });
+
+ return ZGenericSuccessResponse;
+ });
diff --git a/packages/trpc/server/document-router/redistribute-document.types.ts b/packages/trpc/server/document-router/redistribute-document.types.ts
new file mode 100644
index 000000000..6444b6fe5
--- /dev/null
+++ b/packages/trpc/server/document-router/redistribute-document.types.ts
@@ -0,0 +1,28 @@
+import { z } from 'zod';
+
+import type { TrpcRouteMeta } from '../trpc';
+import { ZSuccessResponseSchema } from './schema';
+
+export const redistributeDocumentMeta: TrpcRouteMeta = {
+ openapi: {
+ method: 'POST',
+ path: '/document/redistribute',
+ summary: 'Redistribute document',
+ description:
+ 'Redistribute the document to the provided recipients who have not actioned the document. Will use the distribution method set in the document',
+ tags: ['Document'],
+ },
+};
+
+export const ZRedistributeDocumentRequestSchema = z.object({
+ documentId: z.number(),
+ recipients: z
+ .array(z.number())
+ .min(1)
+ .describe('The IDs of the recipients to redistribute the document to.'),
+});
+
+export const ZRedistributeDocumentResponseSchema = ZSuccessResponseSchema;
+
+export type TRedistributeDocumentRequest = z.infer;
+export type TRedistributeDocumentResponse = z.infer;
diff --git a/packages/trpc/server/document-router/router.ts b/packages/trpc/server/document-router/router.ts
index a833e813a..da5b8e769 100644
--- a/packages/trpc/server/document-router/router.ts
+++ b/packages/trpc/server/document-router/router.ts
@@ -1,709 +1,49 @@
-import { DocumentDataType } from '@prisma/client';
-import { DateTime } from 'luxon';
-
-import { getServerLimits } from '@documenso/ee/server-only/limits/server';
-import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
-import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
-import { encryptSecondaryData } from '@documenso/lib/server-only/crypto/encrypt';
-import { createDocumentData } from '@documenso/lib/server-only/document-data/create-document-data';
-import { upsertDocumentMeta } from '@documenso/lib/server-only/document-meta/upsert-document-meta';
-import { createDocument } from '@documenso/lib/server-only/document/create-document';
-import { createDocumentV2 } from '@documenso/lib/server-only/document/create-document-v2';
-import { deleteDocument } from '@documenso/lib/server-only/document/delete-document';
-import { duplicateDocument } from '@documenso/lib/server-only/document/duplicate-document-by-id';
-import { findDocumentAuditLogs } from '@documenso/lib/server-only/document/find-document-audit-logs';
-import { findDocuments } from '@documenso/lib/server-only/document/find-documents';
-import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id';
-import { getDocumentAndSenderByToken } from '@documenso/lib/server-only/document/get-document-by-token';
-import { getDocumentWithDetailsById } from '@documenso/lib/server-only/document/get-document-with-details-by-id';
-import type { GetStatsInput } from '@documenso/lib/server-only/document/get-stats';
-import { getStats } from '@documenso/lib/server-only/document/get-stats';
-import { resendDocument } from '@documenso/lib/server-only/document/resend-document';
-import { searchDocumentsWithKeyword } from '@documenso/lib/server-only/document/search-documents-with-keyword';
-import { sendDocument } from '@documenso/lib/server-only/document/send-document';
-import { getTeamById } from '@documenso/lib/server-only/team/get-team';
-import { getPresignPostUrl } from '@documenso/lib/universal/upload/server-actions';
-import { isDocumentCompleted } from '@documenso/lib/utils/document';
-import { isValidExpirySettings } from '@documenso/lib/utils/expiry';
-
-import { authenticatedProcedure, procedure, router } from '../trpc';
+import { router } from '../trpc';
+import { createDocumentRoute } from './create-document';
+import { createDocumentTemporaryRoute } from './create-document-temporary';
+import { deleteDocumentRoute } from './delete-document';
+import { distributeDocumentRoute } from './distribute-document';
import { downloadDocumentRoute } from './download-document';
+import { downloadDocumentAuditLogsRoute } from './download-document-audit-logs';
+import { downloadDocumentCertificateRoute } from './download-document-certificate';
+import { duplicateDocumentRoute } from './duplicate-document';
+import { findDocumentAuditLogsRoute } from './find-document-audit-logs';
+import { findDocumentsRoute } from './find-documents';
+import { findDocumentsInternalRoute } from './find-documents-internal';
import { findInboxRoute } from './find-inbox';
+import { getDocumentRoute } from './get-document';
+import { getDocumentByTokenRoute } from './get-document-by-token';
import { getInboxCountRoute } from './get-inbox-count';
-import {
- ZCreateDocumentRequestSchema,
- ZCreateDocumentV2RequestSchema,
- ZCreateDocumentV2ResponseSchema,
- ZDeleteDocumentMutationSchema,
- ZDistributeDocumentRequestSchema,
- ZDistributeDocumentResponseSchema,
- ZDownloadAuditLogsMutationSchema,
- ZDownloadCertificateMutationSchema,
- ZDuplicateDocumentRequestSchema,
- ZDuplicateDocumentResponseSchema,
- ZFindDocumentAuditLogsQuerySchema,
- ZFindDocumentsInternalRequestSchema,
- ZFindDocumentsInternalResponseSchema,
- ZFindDocumentsRequestSchema,
- ZFindDocumentsResponseSchema,
- ZGenericSuccessResponse,
- ZGetDocumentByIdQuerySchema,
- ZGetDocumentByTokenQuerySchema,
- ZGetDocumentWithDetailsByIdRequestSchema,
- ZGetDocumentWithDetailsByIdResponseSchema,
- ZResendDocumentMutationSchema,
- ZSearchDocumentsMutationSchema,
- ZSetSigningOrderForDocumentMutationSchema,
- ZSuccessResponseSchema,
-} from './schema';
+import { redistributeDocumentRoute } from './redistribute-document';
+import { searchDocumentRoute } from './search-document';
import { updateDocumentRoute } from './update-document';
export const documentRouter = router({
- inbox: {
+ get: getDocumentRoute,
+ find: findDocumentsRoute,
+ create: createDocumentRoute,
+ update: updateDocumentRoute,
+ delete: deleteDocumentRoute,
+ duplicate: duplicateDocumentRoute,
+ downloadCertificate: downloadDocumentCertificateRoute,
+ distribute: distributeDocumentRoute,
+ redistribute: redistributeDocumentRoute,
+ search: searchDocumentRoute,
+
+ // Temporary v2 beta routes to be removed once V2 is fully released.
+ download: downloadDocumentRoute,
+ createDocumentTemporary: createDocumentTemporaryRoute,
+
+ // Internal document routes for custom frontend requests.
+ getDocumentByToken: getDocumentByTokenRoute,
+ findDocumentsInternal: findDocumentsInternalRoute,
+
+ auditLog: {
+ find: findDocumentAuditLogsRoute,
+ download: downloadDocumentAuditLogsRoute,
+ },
+ inbox: router({
find: findInboxRoute,
getCount: getInboxCountRoute,
- },
- updateDocument: updateDocumentRoute,
- downloadDocument: downloadDocumentRoute,
-
- /**
- * @private
- */
- getDocumentById: authenticatedProcedure
- .input(ZGetDocumentByIdQuerySchema)
- .query(async ({ input, ctx }) => {
- const { teamId } = ctx;
- const { documentId } = input;
-
- ctx.logger.info({
- input: {
- documentId,
- },
- });
-
- return await getDocumentById({
- userId: ctx.user.id,
- teamId,
- documentId,
- });
- }),
-
- /**
- * @private
- */
- getDocumentByToken: procedure
- .input(ZGetDocumentByTokenQuerySchema)
- .query(async ({ input, ctx }) => {
- const { token } = input;
-
- return await getDocumentAndSenderByToken({
- token,
- userId: ctx.user?.id,
- });
- }),
-
- /**
- * @public
- */
- findDocuments: authenticatedProcedure
- .meta({
- openapi: {
- method: 'GET',
- path: '/document',
- summary: 'Find documents',
- description: 'Find documents based on a search criteria',
- tags: ['Document'],
- },
- })
- .input(ZFindDocumentsRequestSchema)
- .output(ZFindDocumentsResponseSchema)
- .query(async ({ input, ctx }) => {
- const { user, teamId } = ctx;
-
- const {
- query,
- templateId,
- page,
- perPage,
- orderByDirection,
- orderByColumn,
- source,
- status,
- folderId,
- } = input;
-
- const documents = await findDocuments({
- userId: user.id,
- teamId,
- templateId,
- query,
- source,
- status,
- page,
- perPage,
- folderId,
- orderBy: orderByColumn ? { column: orderByColumn, direction: orderByDirection } : undefined,
- });
-
- return documents;
- }),
-
- /**
- * Internal endpoint for /documents page to additionally return getStats.
- *
- * @private
- */
- findDocumentsInternal: authenticatedProcedure
- .input(ZFindDocumentsInternalRequestSchema)
- .output(ZFindDocumentsInternalResponseSchema)
- .query(async ({ input, ctx }) => {
- const { user, teamId } = ctx;
-
- const {
- query,
- templateId,
- page,
- perPage,
- orderByDirection,
- orderByColumn,
- source,
- status,
- period,
- senderIds,
- folderId,
- } = input;
-
- const getStatOptions: GetStatsInput = {
- user,
- period,
- search: query,
- folderId,
- };
-
- if (teamId) {
- const team = await getTeamById({ userId: user.id, teamId });
-
- getStatOptions.team = {
- teamId: team.id,
- teamEmail: team.teamEmail?.email,
- senderIds,
- currentTeamMemberRole: team.currentTeamRole,
- currentUserEmail: user.email,
- userId: user.id,
- };
- }
-
- const [stats, documents] = await Promise.all([
- getStats(getStatOptions),
- findDocuments({
- userId: user.id,
- teamId,
- query,
- templateId,
- page,
- perPage,
- source,
- status,
- period,
- senderIds,
- folderId,
- orderBy: orderByColumn
- ? { column: orderByColumn, direction: orderByDirection }
- : undefined,
- }),
- ]);
-
- return {
- ...documents,
- stats,
- };
- }),
-
- /**
- * @public
- *
- * Todo: Refactor to getDocumentById.
- */
- getDocumentWithDetailsById: authenticatedProcedure
- .meta({
- openapi: {
- method: 'GET',
- path: '/document/{documentId}',
- summary: 'Get document',
- description: 'Returns a document given an ID',
- tags: ['Document'],
- },
- })
- .input(ZGetDocumentWithDetailsByIdRequestSchema)
- .output(ZGetDocumentWithDetailsByIdResponseSchema)
- .query(async ({ input, ctx }) => {
- const { teamId, user } = ctx;
- const { documentId, folderId } = input;
-
- ctx.logger.info({
- input: {
- documentId,
- folderId,
- },
- });
-
- return await getDocumentWithDetailsById({
- userId: user.id,
- teamId,
- documentId,
- folderId,
- });
- }),
-
- /**
- * Temporariy endpoint for V2 Beta until we allow passthrough documents on create.
- *
- * @public
- * @deprecated
- */
- createDocumentTemporary: authenticatedProcedure
- .meta({
- openapi: {
- method: 'POST',
- path: '/document/create/beta',
- summary: 'Create document',
- description:
- 'You will need to upload the PDF to the provided URL returned. Note: Once V2 API is released, this will be removed since we will allow direct uploads, instead of using an upload URL.',
- tags: ['Document'],
- },
- })
- .input(ZCreateDocumentV2RequestSchema)
- .output(ZCreateDocumentV2ResponseSchema)
- .mutation(async ({ input, ctx }) => {
- const { teamId, user } = ctx;
-
- const {
- title,
- externalId,
- visibility,
- globalAccessAuth,
- globalActionAuth,
- recipients,
- meta,
- expiryAmount,
- expiryUnit,
- } = input;
-
- if ((expiryAmount || expiryUnit) && !isValidExpirySettings(expiryAmount, expiryUnit)) {
- throw new AppError(AppErrorCode.INVALID_REQUEST, {
- message: 'Invalid expiry settings. Please check your expiry configuration.',
- });
- }
-
- const { remaining } = await getServerLimits({ userId: user.id, teamId });
-
- if (remaining.documents <= 0) {
- throw new AppError(AppErrorCode.LIMIT_EXCEEDED, {
- message: 'You have reached your document limit for this month. Please upgrade your plan.',
- statusCode: 400,
- });
- }
-
- const fileName = title.endsWith('.pdf') ? title : `${title}.pdf`;
-
- const { url, key } = await getPresignPostUrl(fileName, 'application/pdf');
-
- const documentData = await createDocumentData({
- data: key,
- type: DocumentDataType.S3_PATH,
- });
-
- const createdDocument = await createDocumentV2({
- userId: ctx.user.id,
- teamId,
- documentDataId: documentData.id,
- normalizePdf: false, // Not normalizing because of presigned URL.
- data: {
- title,
- externalId,
- visibility,
- globalAccessAuth,
- globalActionAuth,
- recipients,
- expiryAmount,
- expiryUnit,
- },
- meta,
- requestMetadata: ctx.metadata,
- });
-
- return {
- document: createdDocument,
- folder: createdDocument.folder, // Todo: Remove this prior to api-v2 release.
- uploadUrl: url,
- };
- }),
-
- /**
- * Wait until RR7 so we can passthrough documents.
- *
- * @private
- */
- createDocument: authenticatedProcedure
- // .meta({
- // openapi: {
- // method: 'POST',
- // path: '/document/create',
- // summary: 'Create document',
- // tags: ['Document'],
- // },
- // })
- .input(ZCreateDocumentRequestSchema)
- .mutation(async ({ input, ctx }) => {
- const { user, teamId } = ctx;
- const { title, documentDataId, timezone, folderId, expiryAmount, expiryUnit } = input;
-
- // Validate expiry settings
- if ((expiryAmount || expiryUnit) && !isValidExpirySettings(expiryAmount, expiryUnit)) {
- throw new AppError(AppErrorCode.INVALID_REQUEST, {
- message: 'Invalid expiry settings. Please check your expiry configuration.',
- });
- }
-
- ctx.logger.info({
- input: {
- folderId,
- },
- });
-
- const { remaining } = await getServerLimits({ userId: user.id, teamId });
-
- if (remaining.documents <= 0) {
- throw new AppError(AppErrorCode.LIMIT_EXCEEDED, {
- message: 'You have reached your document limit for this month. Please upgrade your plan.',
- statusCode: 400,
- });
- }
-
- return await createDocument({
- userId: user.id,
- teamId,
- title,
- documentDataId,
- normalizePdf: true,
- userTimezone: timezone,
- requestMetadata: ctx.metadata,
- folderId,
- expiryAmount,
- expiryUnit,
- });
- }),
-
- /**
- * @public
- */
- deleteDocument: authenticatedProcedure
- .meta({
- openapi: {
- method: 'POST',
- path: '/document/delete',
- summary: 'Delete document',
- tags: ['Document'],
- },
- })
- .input(ZDeleteDocumentMutationSchema)
- .output(ZSuccessResponseSchema)
- .mutation(async ({ input, ctx }) => {
- const { teamId } = ctx;
- const { documentId } = input;
-
- ctx.logger.info({
- input: {
- documentId,
- },
- });
-
- const userId = ctx.user.id;
-
- await deleteDocument({
- id: documentId,
- userId,
- teamId,
- requestMetadata: ctx.metadata,
- });
-
- return ZGenericSuccessResponse;
- }),
-
- /**
- * @private
- *
- * Todo: Remove and use `updateDocument` endpoint instead.
- */
- setSigningOrderForDocument: authenticatedProcedure
- .input(ZSetSigningOrderForDocumentMutationSchema)
- .mutation(async ({ input, ctx }) => {
- const { teamId } = ctx;
- const { documentId, signingOrder } = input;
-
- ctx.logger.info({
- input: {
- documentId,
- signingOrder,
- },
- });
-
- return await upsertDocumentMeta({
- userId: ctx.user.id,
- teamId,
- documentId,
- signingOrder,
- requestMetadata: ctx.metadata,
- });
- }),
-
- /**
- * @public
- *
- * Todo: Refactor to distributeDocument.
- * Todo: Rework before releasing API.
- */
- sendDocument: authenticatedProcedure
- .meta({
- openapi: {
- method: 'POST',
- path: '/document/distribute',
- summary: 'Distribute document',
- description: 'Send the document out to recipients based on your distribution method',
- tags: ['Document'],
- },
- })
- .input(ZDistributeDocumentRequestSchema)
- .output(ZDistributeDocumentResponseSchema)
- .mutation(async ({ input, ctx }) => {
- const { teamId } = ctx;
- const { documentId, meta = {} } = input;
-
- ctx.logger.info({
- input: {
- documentId,
- },
- });
-
- if (Object.values(meta).length > 0) {
- await upsertDocumentMeta({
- userId: ctx.user.id,
- teamId,
- documentId,
- subject: meta.subject,
- message: meta.message,
- dateFormat: meta.dateFormat,
- timezone: meta.timezone,
- redirectUrl: meta.redirectUrl,
- distributionMethod: meta.distributionMethod,
- emailSettings: meta.emailSettings,
- language: meta.language,
- emailId: meta.emailId,
- emailReplyTo: meta.emailReplyTo,
- requestMetadata: ctx.metadata,
- });
- }
-
- return await sendDocument({
- userId: ctx.user.id,
- documentId,
- teamId,
- requestMetadata: ctx.metadata,
- });
- }),
-
- /**
- * @public
- *
- * Todo: Refactor to redistributeDocument.
- */
- resendDocument: authenticatedProcedure
- .meta({
- openapi: {
- method: 'POST',
- path: '/document/redistribute',
- summary: 'Redistribute document',
- description:
- 'Redistribute the document to the provided recipients who have not actioned the document. Will use the distribution method set in the document',
- tags: ['Document'],
- },
- })
- .input(ZResendDocumentMutationSchema)
- .output(ZSuccessResponseSchema)
- .mutation(async ({ input, ctx }) => {
- const { teamId } = ctx;
- const { documentId, recipients } = input;
-
- ctx.logger.info({
- input: {
- documentId,
- recipients,
- },
- });
-
- await resendDocument({
- userId: ctx.user.id,
- teamId,
- documentId,
- recipients,
- requestMetadata: ctx.metadata,
- });
-
- return ZGenericSuccessResponse;
- }),
-
- /**
- * @public
- */
- duplicateDocument: authenticatedProcedure
- .meta({
- openapi: {
- method: 'POST',
- path: '/document/duplicate',
- summary: 'Duplicate document',
- tags: ['Document'],
- },
- })
- .input(ZDuplicateDocumentRequestSchema)
- .output(ZDuplicateDocumentResponseSchema)
- .mutation(async ({ input, ctx }) => {
- const { teamId, user } = ctx;
- const { documentId } = input;
-
- ctx.logger.info({
- input: {
- documentId,
- },
- });
-
- return await duplicateDocument({
- userId: user.id,
- teamId,
- documentId,
- });
- }),
-
- /**
- * @private
- */
- searchDocuments: authenticatedProcedure
- .input(ZSearchDocumentsMutationSchema)
- .query(async ({ input, ctx }) => {
- const { query } = input;
-
- const documents = await searchDocumentsWithKeyword({
- query,
- userId: ctx.user.id,
- });
-
- return documents;
- }),
-
- /**
- * @private
- */
- findDocumentAuditLogs: authenticatedProcedure
- .input(ZFindDocumentAuditLogsQuerySchema)
- .query(async ({ input, ctx }) => {
- const { teamId } = ctx;
-
- const {
- page,
- perPage,
- documentId,
- cursor,
- filterForRecentActivity,
- orderByColumn,
- orderByDirection,
- } = input;
-
- ctx.logger.info({
- input: {
- documentId,
- },
- });
-
- return await findDocumentAuditLogs({
- userId: ctx.user.id,
- teamId,
- page,
- perPage,
- documentId,
- cursor,
- filterForRecentActivity,
- orderBy: orderByColumn ? { column: orderByColumn, direction: orderByDirection } : undefined,
- });
- }),
-
- /**
- * @private
- */
- downloadAuditLogs: authenticatedProcedure
- .input(ZDownloadAuditLogsMutationSchema)
- .mutation(async ({ input, ctx }) => {
- const { teamId } = ctx;
- const { documentId } = input;
-
- ctx.logger.info({
- input: {
- documentId,
- },
- });
-
- const document = await getDocumentById({
- documentId,
- userId: ctx.user.id,
- teamId,
- }).catch(() => null);
-
- if (!document || (teamId && document.teamId !== teamId)) {
- throw new AppError(AppErrorCode.UNAUTHORIZED, {
- message: 'You do not have access to this document.',
- });
- }
-
- const encrypted = encryptSecondaryData({
- data: document.id.toString(),
- expiresAt: DateTime.now().plus({ minutes: 5 }).toJSDate().valueOf(),
- });
-
- return {
- url: `${NEXT_PUBLIC_WEBAPP_URL()}/__htmltopdf/audit-log?d=${encrypted}`,
- };
- }),
-
- /**
- * @private
- */
- downloadCertificate: authenticatedProcedure
- .input(ZDownloadCertificateMutationSchema)
- .mutation(async ({ input, ctx }) => {
- const { teamId } = ctx;
- const { documentId } = input;
-
- ctx.logger.info({
- input: {
- documentId,
- },
- });
-
- const document = await getDocumentById({
- documentId,
- userId: ctx.user.id,
- teamId,
- });
-
- if (!isDocumentCompleted(document.status)) {
- throw new AppError('DOCUMENT_NOT_COMPLETE');
- }
-
- const encrypted = encryptSecondaryData({
- data: document.id.toString(),
- expiresAt: DateTime.now().plus({ minutes: 5 }).toJSDate().valueOf(),
- });
-
- return {
- url: `${NEXT_PUBLIC_WEBAPP_URL()}/__htmltopdf/certificate?d=${encrypted}`,
- };
- }),
+ }),
});
diff --git a/packages/trpc/server/document-router/schema.ts b/packages/trpc/server/document-router/schema.ts
index 70afb3e4d..8856fc352 100644
--- a/packages/trpc/server/document-router/schema.ts
+++ b/packages/trpc/server/document-router/schema.ts
@@ -1,39 +1,9 @@
-import {
- DocumentDistributionMethod,
- DocumentSigningOrder,
- DocumentSource,
- DocumentStatus,
- DocumentVisibility,
- FieldType,
-} from '@prisma/client';
+import { DocumentDistributionMethod, DocumentVisibility } from '@prisma/client';
import { z } from 'zod';
import { VALID_DATE_FORMAT_VALUES } from '@documenso/lib/constants/date-formats';
import { SUPPORTED_LANGUAGE_CODES } from '@documenso/lib/constants/i18n';
-import {
- ZDocumentLiteSchema,
- ZDocumentManySchema,
- ZDocumentSchema,
-} from '@documenso/lib/types/document';
-import {
- ZDocumentAccessAuthTypesSchema,
- ZDocumentActionAuthTypesSchema,
-} from '@documenso/lib/types/document-auth';
-import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
-import { ZDocumentFormValuesSchema } from '@documenso/lib/types/document-form-values';
-import {
- ZFieldHeightSchema,
- ZFieldPageNumberSchema,
- ZFieldPageXSchema,
- ZFieldPageYSchema,
- ZFieldWidthSchema,
-} from '@documenso/lib/types/field';
-import { ZFieldAndMetaSchema } from '@documenso/lib/types/field-meta';
-import { ZFindResultResponse, ZFindSearchParamsSchema } from '@documenso/lib/types/search-params';
import { isValidRedirectUrl } from '@documenso/lib/utils/is-valid-redirect-url';
-import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status';
-
-import { ZCreateRecipientSchema } from '../recipient-router/schema';
/**
* Required for empty responses since we currently can't 201 requests for our openapi setup.
@@ -126,256 +96,3 @@ export const ZDocumentExpiryAmountSchema = z
export const ZDocumentExpiryUnitSchema = z
.enum(['minutes', 'hours', 'days', 'weeks', 'months'])
.describe('The unit for expiry duration (e.g., "days" for "3 days").');
-
-export const ZFindDocumentsRequestSchema = ZFindSearchParamsSchema.extend({
- templateId: z
- .number()
- .describe('Filter documents by the template ID used to create it.')
- .optional(),
- source: z
- .nativeEnum(DocumentSource)
- .describe('Filter documents by how it was created.')
- .optional(),
- status: z
- .nativeEnum(DocumentStatus)
- .describe('Filter documents by the current status')
- .optional(),
- folderId: z.string().describe('Filter documents by folder ID').optional(),
- orderByColumn: z.enum(['createdAt']).optional(),
- orderByDirection: z.enum(['asc', 'desc']).describe('').default('desc'),
-});
-
-export const ZFindDocumentsResponseSchema = ZFindResultResponse.extend({
- data: ZDocumentManySchema.array(),
-});
-
-export type TFindDocumentsResponse = z.infer;
-
-export const ZFindDocumentsInternalRequestSchema = ZFindDocumentsRequestSchema.extend({
- period: z.enum(['7d', '14d', '30d']).optional(),
- senderIds: z.array(z.number()).optional(),
- status: z.nativeEnum(ExtendedDocumentStatus).optional(),
- folderId: z.string().optional(),
-});
-
-export const ZFindDocumentsInternalResponseSchema = ZFindResultResponse.extend({
- data: ZDocumentManySchema.array(),
- stats: z.object({
- [ExtendedDocumentStatus.DRAFT]: z.number(),
- [ExtendedDocumentStatus.PENDING]: z.number(),
- [ExtendedDocumentStatus.COMPLETED]: z.number(),
- [ExtendedDocumentStatus.REJECTED]: z.number(),
- [ExtendedDocumentStatus.INBOX]: z.number(),
- [ExtendedDocumentStatus.ALL]: z.number(),
- }),
-});
-
-export type TFindDocumentsInternalResponse = z.infer;
-
-export const ZFindDocumentAuditLogsQuerySchema = ZFindSearchParamsSchema.extend({
- documentId: z.number().min(1),
- cursor: z.string().optional(),
- filterForRecentActivity: z.boolean().optional(),
- orderByColumn: z.enum(['createdAt', 'type']).optional(),
- orderByDirection: z.enum(['asc', 'desc']).default('desc'),
-});
-
-export const ZGetDocumentByIdQuerySchema = z.object({
- documentId: z.number(),
-});
-
-export const ZDuplicateDocumentRequestSchema = z.object({
- documentId: z.number(),
-});
-
-export const ZDuplicateDocumentResponseSchema = z.object({
- documentId: z.number(),
-});
-
-export const ZGetDocumentByTokenQuerySchema = z.object({
- token: z.string().min(1),
-});
-
-export type TGetDocumentByTokenQuerySchema = z.infer;
-
-export const ZGetDocumentWithDetailsByIdRequestSchema = z.object({
- documentId: z.number(),
- folderId: z.string().describe('Filter documents by folder ID').optional(),
-});
-
-export const ZGetDocumentWithDetailsByIdResponseSchema = ZDocumentSchema;
-
-export const ZCreateDocumentRequestSchema = z.object({
- title: ZDocumentTitleSchema,
- documentDataId: z.string().min(1),
- timezone: ZDocumentMetaTimezoneSchema.optional(),
- folderId: z.string().describe('The ID of the folder to create the document in').optional(),
- expiryAmount: ZDocumentExpiryAmountSchema.optional(),
- expiryUnit: ZDocumentExpiryUnitSchema.optional(),
-});
-
-export const ZCreateDocumentV2RequestSchema = z.object({
- title: ZDocumentTitleSchema,
- externalId: ZDocumentExternalIdSchema.optional(),
- visibility: ZDocumentVisibilitySchema.optional(),
- globalAccessAuth: z.array(ZDocumentAccessAuthTypesSchema).optional(),
- globalActionAuth: z.array(ZDocumentActionAuthTypesSchema).optional(),
- formValues: ZDocumentFormValuesSchema.optional(),
- expiryAmount: ZDocumentExpiryAmountSchema.optional(),
- expiryUnit: ZDocumentExpiryUnitSchema.optional(),
- recipients: z
- .array(
- ZCreateRecipientSchema.extend({
- fields: ZFieldAndMetaSchema.and(
- z.object({
- pageNumber: ZFieldPageNumberSchema,
- pageX: ZFieldPageXSchema,
- pageY: ZFieldPageYSchema,
- width: ZFieldWidthSchema,
- height: ZFieldHeightSchema,
- }),
- )
- .array()
- .optional(),
- }),
- )
- .refine(
- (recipients) => {
- const emails = recipients.map((recipient) => recipient.email);
-
- return new Set(emails).size === emails.length;
- },
- { message: 'Recipients must have unique emails' },
- )
- .optional(),
- meta: z
- .object({
- subject: ZDocumentMetaSubjectSchema.optional(),
- message: ZDocumentMetaMessageSchema.optional(),
- timezone: ZDocumentMetaTimezoneSchema.optional(),
- dateFormat: ZDocumentMetaDateFormatSchema.optional(),
- distributionMethod: ZDocumentMetaDistributionMethodSchema.optional(),
- signingOrder: z.nativeEnum(DocumentSigningOrder).optional(),
- redirectUrl: ZDocumentMetaRedirectUrlSchema.optional(),
- language: ZDocumentMetaLanguageSchema.optional(),
- typedSignatureEnabled: ZDocumentMetaTypedSignatureEnabledSchema.optional(),
- drawSignatureEnabled: ZDocumentMetaDrawSignatureEnabledSchema.optional(),
- uploadSignatureEnabled: ZDocumentMetaUploadSignatureEnabledSchema.optional(),
- emailSettings: ZDocumentEmailSettingsSchema.optional(),
- })
- .optional(),
-});
-
-export type TCreateDocumentV2Request = z.infer;
-
-export const ZCreateDocumentV2ResponseSchema = z.object({
- document: ZDocumentSchema,
- uploadUrl: z
- .string()
- .describe(
- 'The URL to upload the document PDF to. Use a PUT request with the file via form-data',
- ),
-});
-
-export const ZSetFieldsForDocumentMutationSchema = z.object({
- documentId: z.number(),
- fields: z.array(
- z.object({
- id: z.number().nullish(),
- type: z.nativeEnum(FieldType),
- signerEmail: z.string().min(1),
- pageNumber: z.number().min(1),
- pageX: z.number().min(0),
- pageY: z.number().min(0),
- pageWidth: z.number().min(0),
- pageHeight: z.number().min(0),
- }),
- ),
-});
-
-export type TSetFieldsForDocumentMutationSchema = z.infer<
- typeof ZSetFieldsForDocumentMutationSchema
->;
-
-export const ZDistributeDocumentRequestSchema = z.object({
- documentId: z.number().describe('The ID of the document to send.'),
- meta: z
- .object({
- subject: ZDocumentMetaSubjectSchema.optional(),
- message: ZDocumentMetaMessageSchema.optional(),
- timezone: ZDocumentMetaTimezoneSchema.optional(),
- dateFormat: ZDocumentMetaDateFormatSchema.optional(),
- distributionMethod: ZDocumentMetaDistributionMethodSchema.optional(),
- redirectUrl: ZDocumentMetaRedirectUrlSchema.optional(),
- language: ZDocumentMetaLanguageSchema.optional(),
- emailId: z.string().nullish(),
- emailReplyTo: z.string().email().nullish(),
- emailSettings: ZDocumentEmailSettingsSchema.optional(),
- })
- .optional(),
-});
-
-export const ZDistributeDocumentResponseSchema = ZDocumentLiteSchema;
-
-export const ZSetPasswordForDocumentMutationSchema = z.object({
- documentId: z.number(),
- password: z.string(),
-});
-
-export type TSetPasswordForDocumentMutationSchema = z.infer<
- typeof ZSetPasswordForDocumentMutationSchema
->;
-
-export const ZSetSigningOrderForDocumentMutationSchema = z.object({
- documentId: z.number(),
- signingOrder: z.nativeEnum(DocumentSigningOrder),
-});
-
-export type TSetSigningOrderForDocumentMutationSchema = z.infer<
- typeof ZSetSigningOrderForDocumentMutationSchema
->;
-
-export const ZResendDocumentMutationSchema = z.object({
- documentId: z.number(),
- recipients: z
- .array(z.number())
- .min(1)
- .describe('The IDs of the recipients to redistribute the document to.'),
-});
-
-export const ZDeleteDocumentMutationSchema = z.object({
- documentId: z.number(),
-});
-
-export type TDeleteDocumentMutationSchema = z.infer;
-
-export const ZSearchDocumentsMutationSchema = z.object({
- query: z.string(),
-});
-
-export const ZDownloadAuditLogsMutationSchema = z.object({
- documentId: z.number(),
-});
-
-export const ZDownloadCertificateMutationSchema = z.object({
- documentId: z.number(),
-});
-
-export const ZDownloadDocumentRequestSchema = z.object({
- documentId: z.number().describe('The ID of the document to download.'),
- version: z
- .enum(['original', 'signed'])
- .describe(
- 'The version of the document to download. "signed" returns the completed document with signatures, "original" returns the original uploaded document.',
- )
- .default('signed'),
-});
-
-export const ZDownloadDocumentResponseSchema = z.object({
- downloadUrl: z.string().describe('Pre-signed URL for downloading the PDF file'),
- filename: z.string().describe('The filename of the PDF file'),
- contentType: z.string().describe('MIME type of the file'),
-});
-
-export type TDownloadDocumentRequest = z.infer;
-export type TDownloadDocumentResponse = z.infer;
diff --git a/packages/trpc/server/document-router/search-document.ts b/packages/trpc/server/document-router/search-document.ts
new file mode 100644
index 000000000..4acd3be69
--- /dev/null
+++ b/packages/trpc/server/document-router/search-document.ts
@@ -0,0 +1,21 @@
+import { searchDocumentsWithKeyword } from '@documenso/lib/server-only/document/search-documents-with-keyword';
+
+import { authenticatedProcedure } from '../trpc';
+import {
+ ZSearchDocumentRequestSchema,
+ ZSearchDocumentResponseSchema,
+} from './search-document.types';
+
+export const searchDocumentRoute = authenticatedProcedure
+ .input(ZSearchDocumentRequestSchema)
+ .output(ZSearchDocumentResponseSchema)
+ .query(async ({ input, ctx }) => {
+ const { query } = input;
+
+ const documents = await searchDocumentsWithKeyword({
+ query,
+ userId: ctx.user.id,
+ });
+
+ return documents;
+ });
diff --git a/packages/trpc/server/document-router/search-document.types.ts b/packages/trpc/server/document-router/search-document.types.ts
new file mode 100644
index 000000000..5a6903066
--- /dev/null
+++ b/packages/trpc/server/document-router/search-document.types.ts
@@ -0,0 +1,16 @@
+import { z } from 'zod';
+
+export const ZSearchDocumentRequestSchema = z.object({
+ query: z.string(),
+});
+
+export const ZSearchDocumentResponseSchema = z
+ .object({
+ title: z.string(),
+ path: z.string(),
+ value: z.string(),
+ })
+ .array();
+
+export type TSearchDocumentRequest = z.infer;
+export type TSearchDocumentResponse = z.infer;
diff --git a/packages/trpc/server/organisation-router/create-organisation-group.ts b/packages/trpc/server/organisation-router/create-organisation-group.ts
index b2dc3cde9..db87c8378 100644
--- a/packages/trpc/server/organisation-router/create-organisation-group.ts
+++ b/packages/trpc/server/organisation-router/create-organisation-group.ts
@@ -40,7 +40,13 @@ export const createOrganisationGroupRoute = authenticatedProcedure
groups: true,
members: {
include: {
- user: true,
+ user: {
+ select: {
+ id: true,
+ email: true,
+ name: true,
+ },
+ },
},
},
},
diff --git a/packages/trpc/server/organisation-router/update-organisation.ts b/packages/trpc/server/organisation-router/update-organisation.ts
index bc71b9de0..73f2445c8 100644
--- a/packages/trpc/server/organisation-router/update-organisation.ts
+++ b/packages/trpc/server/organisation-router/update-organisation.ts
@@ -1,5 +1,6 @@
import { ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP } from '@documenso/lib/constants/organisations';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
+import { stripe } from '@documenso/lib/server-only/stripe';
import { buildOrganisationWhereQuery } from '@documenso/lib/utils/organisations';
import { prisma } from '@documenso/prisma';
@@ -38,7 +39,7 @@ export const updateOrganisationRoute = authenticatedProcedure
});
}
- await prisma.organisation.update({
+ const updatedOrganisation = await prisma.organisation.update({
where: {
id: organisationId,
},
@@ -47,4 +48,12 @@ export const updateOrganisationRoute = authenticatedProcedure
url: data.url,
},
});
+
+ if (updatedOrganisation.customerId) {
+ await stripe.customers.update(updatedOrganisation.customerId, {
+ metadata: {
+ organisationName: data.name,
+ },
+ });
+ }
});
diff --git a/packages/trpc/server/profile-router/router.ts b/packages/trpc/server/profile-router/router.ts
index 927409817..3f903eb49 100644
--- a/packages/trpc/server/profile-router/router.ts
+++ b/packages/trpc/server/profile-router/router.ts
@@ -1,15 +1,16 @@
+import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import type { SetAvatarImageOptions } from '@documenso/lib/server-only/profile/set-avatar-image';
import { setAvatarImage } from '@documenso/lib/server-only/profile/set-avatar-image';
import { deleteUser } from '@documenso/lib/server-only/user/delete-user';
import { findUserSecurityAuditLogs } from '@documenso/lib/server-only/user/find-user-security-audit-logs';
-import { getUserById } from '@documenso/lib/server-only/user/get-user-by-id';
+import { submitSupportTicket } from '@documenso/lib/server-only/user/submit-support-ticket';
import { updateProfile } from '@documenso/lib/server-only/user/update-profile';
-import { adminProcedure, authenticatedProcedure, router } from '../trpc';
+import { authenticatedProcedure, router } from '../trpc';
import {
ZFindUserSecurityAuditLogsSchema,
- ZRetrieveUserByIdQuerySchema,
ZSetProfileImageMutationSchema,
+ ZSubmitSupportTicketMutationSchema,
ZUpdateProfileMutationSchema,
} from './schema';
@@ -23,24 +24,12 @@ export const profileRouter = router({
});
}),
- getUser: adminProcedure.input(ZRetrieveUserByIdQuerySchema).query(async ({ input, ctx }) => {
- const { id } = input;
-
- ctx.logger.info({
- input: {
- id,
- },
- });
-
- return await getUserById({ id });
- }),
-
updateProfile: authenticatedProcedure
.input(ZUpdateProfileMutationSchema)
.mutation(async ({ input, ctx }) => {
const { name, signature } = input;
- return await updateProfile({
+ await updateProfile({
userId: ctx.user.id,
name,
signature,
@@ -49,7 +38,7 @@ export const profileRouter = router({
}),
deleteAccount: authenticatedProcedure.mutation(async ({ ctx }) => {
- return await deleteUser({
+ await deleteUser({
id: ctx.user.id,
});
}),
@@ -91,4 +80,28 @@ export const profileRouter = router({
requestMetadata: ctx.metadata,
});
}),
+
+ submitSupportTicket: authenticatedProcedure
+ .input(ZSubmitSupportTicketMutationSchema)
+ .mutation(async ({ input, ctx }) => {
+ const { subject, message, organisationId, teamId } = input;
+
+ const userId = ctx.user.id;
+
+ const parsedTeamId = teamId ? Number(teamId) : null;
+
+ if (Number.isNaN(parsedTeamId)) {
+ throw new AppError(AppErrorCode.INVALID_BODY, {
+ message: 'Invalid team ID provided',
+ });
+ }
+
+ return await submitSupportTicket({
+ subject,
+ message,
+ userId,
+ organisationId,
+ teamId: parsedTeamId,
+ });
+ }),
});
diff --git a/packages/trpc/server/profile-router/schema.ts b/packages/trpc/server/profile-router/schema.ts
index 490ea8a8c..2d6a85dc3 100644
--- a/packages/trpc/server/profile-router/schema.ts
+++ b/packages/trpc/server/profile-router/schema.ts
@@ -7,12 +7,6 @@ export const ZFindUserSecurityAuditLogsSchema = z.object({
export type TFindUserSecurityAuditLogsSchema = z.infer;
-export const ZRetrieveUserByIdQuerySchema = z.object({
- id: z.number().min(1),
-});
-
-export type TRetrieveUserByIdQuerySchema = z.infer;
-
export const ZUpdateProfileMutationSchema = z.object({
name: z.string().min(1),
signature: z.string(),
@@ -27,3 +21,12 @@ export const ZSetProfileImageMutationSchema = z.object({
});
export type TSetProfileImageMutationSchema = z.infer;
+
+export const ZSubmitSupportTicketMutationSchema = z.object({
+ organisationId: z.string(),
+ teamId: z.string().min(1).nullish(),
+ subject: z.string().min(3, 'Subject is required'),
+ message: z.string().min(10, 'Message must be at least 10 characters'),
+});
+
+export type TSupportTicketRequest = z.infer;
diff --git a/packages/trpc/server/template-router/router.ts b/packages/trpc/server/template-router/router.ts
index c52f78223..a9041ad3e 100644
--- a/packages/trpc/server/template-router/router.ts
+++ b/packages/trpc/server/template-router/router.ts
@@ -339,8 +339,14 @@ export const templateRouter = router({
.output(ZCreateDocumentFromTemplateResponseSchema)
.mutation(async ({ ctx, input }) => {
const { teamId } = ctx;
- const { templateId, recipients, distributeDocument, customDocumentDataId, prefillFields } =
- input;
+ const {
+ templateId,
+ recipients,
+ distributeDocument,
+ customDocumentDataId,
+ prefillFields,
+ folderId,
+ } = input;
ctx.logger.info({
input: {
@@ -361,6 +367,7 @@ export const templateRouter = router({
recipients,
customDocumentDataId,
requestMetadata: ctx.metadata,
+ folderId,
prefillFields,
});
diff --git a/packages/trpc/server/template-router/schema.ts b/packages/trpc/server/template-router/schema.ts
index 31284ac58..c1100b99e 100644
--- a/packages/trpc/server/template-router/schema.ts
+++ b/packages/trpc/server/template-router/schema.ts
@@ -117,6 +117,12 @@ export const ZCreateDocumentFromTemplateRequestSchema = z.object({
'The data ID of an alternative PDF to use when creating the document. If not provided, the PDF attached to the template will be used.',
)
.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(),
prefillFields: z
.array(ZFieldMetaPrefillFieldsSchema)
.describe(
diff --git a/packages/ui/components/document/document-read-only-fields.tsx b/packages/ui/components/document/document-read-only-fields.tsx
index 0786357d1..d1c96f8a9 100644
--- a/packages/ui/components/document/document-read-only-fields.tsx
+++ b/packages/ui/components/document/document-read-only-fields.tsx
@@ -95,8 +95,10 @@ export const DocumentReadOnlyFields = ({
setHiddenFieldIds((prev) => ({ ...prev, [fieldId]: true }));
};
+ const highestPageNumber = Math.max(...fields.map((field) => field.page));
+
return (
-
+
{fields.map(
(field) =>
!hiddenFieldIds[field.secondaryId] && (
diff --git a/packages/ui/primitives/multiselect.tsx b/packages/ui/primitives/multiselect.tsx
index 3e8f0ffea..196e0a351 100644
--- a/packages/ui/primitives/multiselect.tsx
+++ b/packages/ui/primitives/multiselect.tsx
@@ -1,5 +1,3 @@
-'use client';
-
import * as React from 'react';
import { useEffect } from 'react';
diff --git a/render.yaml b/render.yaml
index 07a90f0b1..a3d7d2a8f 100644
--- a/render.yaml
+++ b/render.yaml
@@ -4,7 +4,7 @@ services:
name: documenso-app
plan: free
buildCommand: npm i && npm run build
- startCommand: npx prisma migrate deploy --schema packages/prisma/schema.prisma && npx turbo run start --filter=@documenso/web
+ startCommand: npx prisma migrate deploy --schema packages/prisma/schema.prisma && npx turbo run start --filter=@documenso/remix
healthCheckPath: /api/health
envVars:
diff --git a/turbo.json b/turbo.json
index 77c668b35..d767b4612 100644
--- a/turbo.json
+++ b/turbo.json
@@ -47,6 +47,7 @@
"NEXT_PUBLIC_POSTHOG_KEY",
"NEXT_PUBLIC_FEATURE_BILLING_ENABLED",
"NEXT_PUBLIC_DISABLE_SIGNUP",
+ "NEXT_PRIVATE_PLAIN_API_KEY",
"NEXT_PUBLIC_DOCUMENT_SIZE_UPLOAD_LIMIT",
"NEXT_PRIVATE_DATABASE_URL",
"NEXT_PRIVATE_DIRECT_DATABASE_URL",