diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index 60b385403..3471f4f88 100644
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -10,7 +10,13 @@
"ghcr.io/devcontainers/features/node:1": {}
},
"onCreateCommand": "./.devcontainer/on-create.sh",
- "forwardPorts": [3000, 54320, 9000, 2500, 1100],
+ "forwardPorts": [
+ 3000,
+ 54320,
+ 9000,
+ 2500,
+ 1100
+ ],
"customizations": {
"vscode": {
"extensions": [
@@ -25,8 +31,8 @@
"GitHub.copilot",
"GitHub.vscode-pull-request-github",
"Prisma.prisma",
- "VisualStudioExptTeam.vscodeintellicode",
+ "VisualStudioExptTeam.vscodeintellicode"
]
}
}
-}
+}
\ No newline at end of file
diff --git a/apps/marketing/src/app/(marketing)/singleplayer/client.tsx b/apps/marketing/src/app/(marketing)/singleplayer/client.tsx
index e0b55dbf5..9f1ebb289 100644
--- a/apps/marketing/src/app/(marketing)/singleplayer/client.tsx
+++ b/apps/marketing/src/app/(marketing)/singleplayer/client.tsx
@@ -256,6 +256,7 @@ export const SinglePlayerClient = () => {
fields={fields}
onSubmit={onSignSubmit}
requireName={Boolean(fields.find((field) => field.type === 'NAME'))}
+ requireCustomText={Boolean(fields.find((field) => field.type === 'TEXT'))}
requireSignature={Boolean(fields.find((field) => field.type === 'SIGNATURE'))}
/>
diff --git a/apps/marketing/src/app/layout.tsx b/apps/marketing/src/app/layout.tsx
index 57da42c3f..99a1a6483 100644
--- a/apps/marketing/src/app/layout.tsx
+++ b/apps/marketing/src/app/layout.tsx
@@ -2,6 +2,8 @@ import { Suspense } from 'react';
import { Caveat, Inter } from 'next/font/google';
+import { PublicEnvScript } from 'next-runtime-env';
+
import { FeatureFlagProvider } from '@documenso/lib/client-only/providers/feature-flag';
import { NEXT_PUBLIC_MARKETING_URL } from '@documenso/lib/constants/app';
import { getAllAnonymousFlags } from '@documenso/lib/universal/get-feature-flag';
@@ -62,6 +64,7 @@ export default async function RootLayout({ children }: { children: React.ReactNo
+
diff --git a/apps/marketing/src/components/(marketing)/pricing-table.tsx b/apps/marketing/src/components/(marketing)/pricing-table.tsx
index b2c53f28e..748f7307f 100644
--- a/apps/marketing/src/components/(marketing)/pricing-table.tsx
+++ b/apps/marketing/src/components/(marketing)/pricing-table.tsx
@@ -8,6 +8,7 @@ import Link from 'next/link';
import { AnimatePresence, motion } from 'framer-motion';
import { usePlausible } from 'next-plausible';
+import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button';
@@ -82,7 +83,7 @@ export const PricingTable = ({ className, ...props }: PricingTableProps) => {
-
+
Signup Now
@@ -113,7 +114,9 @@ export const PricingTable = ({ className, ...props }: PricingTableProps) => {
- Signup Now
+
+ Signup Now
+
diff --git a/apps/web/package.json b/apps/web/package.json
index fd4faa0c1..efd524992 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -14,6 +14,7 @@
"copy:pdfjs": "node ../../scripts/copy-pdfjs.cjs"
},
"dependencies": {
+ "@documenso/api": "*",
"@documenso/assets": "*",
"@documenso/ee": "*",
"@documenso/lib": "*",
@@ -42,6 +43,7 @@
"react-hotkeys-hook": "^4.4.1",
"react-icons": "^4.11.0",
"react-rnd": "^10.4.1",
+ "remeda": "^1.27.1",
"sharp": "0.33.1",
"ts-pattern": "^5.0.5",
"typescript": "5.2.2",
diff --git a/apps/web/src/app/(dashboard)/admin/nav.tsx b/apps/web/src/app/(dashboard)/admin/nav.tsx
index 089861069..b0d652283 100644
--- a/apps/web/src/app/(dashboard)/admin/nav.tsx
+++ b/apps/web/src/app/(dashboard)/admin/nav.tsx
@@ -1,11 +1,11 @@
'use client';
-import { HTMLAttributes } from 'react';
+import type { HTMLAttributes } from 'react';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
-import { BarChart3, FileStack, User2, Wallet2 } from 'lucide-react';
+import { BarChart3, FileStack, Settings, User2, Wallet2 } from 'lucide-react';
import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button';
@@ -78,6 +78,20 @@ export const AdminNav = ({ className, ...props }: AdminNavProps) => {
Subscriptions
+
+
+
+
+ Site Settings
+
+
);
};
diff --git a/apps/web/src/app/(dashboard)/admin/site-settings/banner-form.tsx b/apps/web/src/app/(dashboard)/admin/site-settings/banner-form.tsx
new file mode 100644
index 000000000..351e146ff
--- /dev/null
+++ b/apps/web/src/app/(dashboard)/admin/site-settings/banner-form.tsx
@@ -0,0 +1,200 @@
+'use client';
+
+import { useRouter } from 'next/navigation';
+
+import { zodResolver } from '@hookform/resolvers/zod';
+import { useForm } from 'react-hook-form';
+import type { z } from 'zod';
+
+import type { TSiteSettingsBannerSchema } from '@documenso/lib/server-only/site-settings/schemas/banner';
+import {
+ SITE_SETTINGS_BANNER_ID,
+ ZSiteSettingsBannerSchema,
+} from '@documenso/lib/server-only/site-settings/schemas/banner';
+import { TRPCClientError } from '@documenso/trpc/client';
+import { trpc as trpcReact } from '@documenso/trpc/react';
+import { Button } from '@documenso/ui/primitives/button';
+import { ColorPicker } from '@documenso/ui/primitives/color-picker';
+import {
+ Form,
+ FormControl,
+ FormDescription,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from '@documenso/ui/primitives/form/form';
+import { Switch } from '@documenso/ui/primitives/switch';
+import { Textarea } from '@documenso/ui/primitives/textarea';
+import { useToast } from '@documenso/ui/primitives/use-toast';
+
+const ZBannerFormSchema = ZSiteSettingsBannerSchema;
+
+type TBannerFormSchema = z.infer;
+
+export type BannerFormProps = {
+ banner?: TSiteSettingsBannerSchema;
+};
+
+export function BannerForm({ banner }: BannerFormProps) {
+ const router = useRouter();
+ const { toast } = useToast();
+
+ const form = useForm({
+ resolver: zodResolver(ZBannerFormSchema),
+ defaultValues: {
+ id: SITE_SETTINGS_BANNER_ID,
+ enabled: banner?.enabled ?? false,
+ data: {
+ content: banner?.data?.content ?? '',
+ bgColor: banner?.data?.bgColor ?? '#000000',
+ textColor: banner?.data?.textColor ?? '#FFFFFF',
+ },
+ },
+ });
+
+ const enabled = form.watch('enabled');
+
+ const { mutateAsync: updateSiteSetting, isLoading: isUpdateSiteSettingLoading } =
+ trpcReact.admin.updateSiteSetting.useMutation();
+
+ const onBannerUpdate = async ({ id, enabled, data }: TBannerFormSchema) => {
+ try {
+ await updateSiteSetting({
+ id,
+ enabled,
+ data,
+ });
+
+ toast({
+ title: 'Banner Updated',
+ description: 'Your banner has been updated successfully.',
+ duration: 5000,
+ });
+
+ router.refresh();
+ } catch (err) {
+ if (err instanceof TRPCClientError && err.data?.code === 'BAD_REQUEST') {
+ toast({
+ title: 'An error occurred',
+ description: err.message,
+ variant: 'destructive',
+ });
+ } else {
+ toast({
+ title: 'An unknown error occurred',
+ variant: 'destructive',
+ description:
+ 'We encountered an unknown error while attempting to update the banner. Please try again later.',
+ });
+ }
+ }
+ };
+
+ return (
+
+
Site Banner
+
+ The site banner is a message that is shown at the top of the site. It can be used to display
+ important information to your users.
+
+
+
+
+
+ );
+}
diff --git a/apps/web/src/app/(dashboard)/admin/site-settings/page.tsx b/apps/web/src/app/(dashboard)/admin/site-settings/page.tsx
new file mode 100644
index 000000000..bffb72ff0
--- /dev/null
+++ b/apps/web/src/app/(dashboard)/admin/site-settings/page.tsx
@@ -0,0 +1,24 @@
+import { getSiteSettings } from '@documenso/lib/server-only/site-settings/get-site-settings';
+import { SITE_SETTINGS_BANNER_ID } from '@documenso/lib/server-only/site-settings/schemas/banner';
+
+import { SettingsHeader } from '~/components/(dashboard)/settings/layout/header';
+
+import { BannerForm } from './banner-form';
+
+// import { BannerForm } from './banner-form';
+
+export default async function AdminBannerPage() {
+ const banner = await getSiteSettings().then((settings) =>
+ settings.find((setting) => setting.id === SITE_SETTINGS_BANNER_ID),
+ );
+
+ return (
+
+ );
+}
diff --git a/apps/web/src/app/(dashboard)/documents/[id]/document-page-view.tsx b/apps/web/src/app/(dashboard)/documents/[id]/document-page-view.tsx
index e12a745a2..e20c88a27 100644
--- a/apps/web/src/app/(dashboard)/documents/[id]/document-page-view.tsx
+++ b/apps/web/src/app/(dashboard)/documents/[id]/document-page-view.tsx
@@ -85,6 +85,7 @@ export const DocumentPageView = async ({ params, team }: DocumentPageViewProps)
const recipients = await getRecipientsForDocument({
documentId,
+ teamId: team?.id,
userId: user.id,
});
@@ -95,7 +96,7 @@ export const DocumentPageView = async ({ params, team }: DocumentPageViewProps)
return (
-
+
Documents
diff --git a/apps/web/src/app/(dashboard)/documents/[id]/edit-document.tsx b/apps/web/src/app/(dashboard)/documents/[id]/edit-document.tsx
index fe278486e..5d9fe78aa 100644
--- a/apps/web/src/app/(dashboard)/documents/[id]/edit-document.tsx
+++ b/apps/web/src/app/(dashboard)/documents/[id]/edit-document.tsx
@@ -30,6 +30,8 @@ import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
import { Stepper } from '@documenso/ui/primitives/stepper';
import { useToast } from '@documenso/ui/primitives/use-toast';
+import { useOptionalCurrentTeam } from '~/providers/team';
+
export type EditDocumentFormProps = {
className?: string;
user: User;
@@ -58,6 +60,7 @@ export const EditDocumentForm = ({
const router = useRouter();
const searchParams = useSearchParams();
+ const team = useOptionalCurrentTeam();
const { mutateAsync: addTitle } = trpc.document.setTitleForDocument.useMutation();
const { mutateAsync: addFields } = trpc.field.addFields.useMutation();
@@ -112,6 +115,7 @@ export const EditDocumentForm = ({
// Custom invocation server action
await addTitle({
documentId: document.id,
+ teamId: team?.id,
title: data.title,
});
@@ -134,6 +138,7 @@ export const EditDocumentForm = ({
// Custom invocation server action
await addSigners({
documentId: document.id,
+ teamId: team?.id,
signers: data.signers,
});
@@ -177,6 +182,7 @@ export const EditDocumentForm = ({
try {
await sendDocument({
documentId: document.id,
+ teamId: team?.id,
meta: {
subject,
message,
diff --git a/apps/web/src/app/(dashboard)/documents/[id]/edit/document-edit-page-view.tsx b/apps/web/src/app/(dashboard)/documents/[id]/edit/document-edit-page-view.tsx
index 87b3738bb..69122312e 100644
--- a/apps/web/src/app/(dashboard)/documents/[id]/edit/document-edit-page-view.tsx
+++ b/apps/web/src/app/(dashboard)/documents/[id]/edit/document-edit-page-view.tsx
@@ -74,6 +74,7 @@ export const DocumentEditPageView = async ({ params, team }: DocumentEditPageVie
getRecipientsForDocument({
documentId,
userId: user.id,
+ teamId: team?.id,
}),
getFieldsForDocument({
documentId,
diff --git a/apps/web/src/app/(dashboard)/documents/[id]/logs/document-logs-page-view.tsx b/apps/web/src/app/(dashboard)/documents/[id]/logs/document-logs-page-view.tsx
index e9627d2c7..019ced57e 100644
--- a/apps/web/src/app/(dashboard)/documents/[id]/logs/document-logs-page-view.tsx
+++ b/apps/web/src/app/(dashboard)/documents/[id]/logs/document-logs-page-view.tsx
@@ -44,6 +44,7 @@ export const DocumentLogsPageView = async ({ params, team }: DocumentLogsPageVie
getRecipientsForDocument({
documentId,
userId: user.id,
+ teamId: team?.id,
}),
]);
diff --git a/apps/web/src/app/(dashboard)/documents/data-table-action-dropdown.tsx b/apps/web/src/app/(dashboard)/documents/data-table-action-dropdown.tsx
index 6231b834c..a43d37af7 100644
--- a/apps/web/src/app/(dashboard)/documents/data-table-action-dropdown.tsx
+++ b/apps/web/src/app/(dashboard)/documents/data-table-action-dropdown.tsx
@@ -193,6 +193,7 @@ export const DataTableActionDropdown = ({ row, team }: DataTableActionDropdownPr
documentTitle={row.title}
open={isDeleteDialogOpen}
onOpenChange={setDeleteDialogOpen}
+ teamId={team?.id}
/>
)}
{isDuplicateDialogOpen && (
diff --git a/apps/web/src/app/(dashboard)/documents/delete-document-dialog.tsx b/apps/web/src/app/(dashboard)/documents/delete-document-dialog.tsx
index 38c01ed82..59fd21e60 100644
--- a/apps/web/src/app/(dashboard)/documents/delete-document-dialog.tsx
+++ b/apps/web/src/app/(dashboard)/documents/delete-document-dialog.tsx
@@ -16,12 +16,13 @@ import {
import { Input } from '@documenso/ui/primitives/input';
import { useToast } from '@documenso/ui/primitives/use-toast';
-type DeleteDraftDocumentDialogProps = {
+type DeleteDocumentDialogProps = {
id: number;
open: boolean;
onOpenChange: (_open: boolean) => void;
status: DocumentStatus;
documentTitle: string;
+ teamId?: number;
};
export const DeleteDocumentDialog = ({
@@ -30,7 +31,8 @@ export const DeleteDocumentDialog = ({
onOpenChange,
status,
documentTitle,
-}: DeleteDraftDocumentDialogProps) => {
+ teamId,
+}: DeleteDocumentDialogProps) => {
const router = useRouter();
const { toast } = useToast();
@@ -61,7 +63,7 @@ export const DeleteDocumentDialog = ({
const onDelete = async () => {
try {
- await deleteDocument({ id, status });
+ await deleteDocument({ id, teamId });
} catch {
toast({
title: 'Something went wrong',
diff --git a/apps/web/src/app/(dashboard)/layout.tsx b/apps/web/src/app/(dashboard)/layout.tsx
index 99db66c55..e3199c851 100644
--- a/apps/web/src/app/(dashboard)/layout.tsx
+++ b/apps/web/src/app/(dashboard)/layout.tsx
@@ -9,6 +9,7 @@ import { NEXT_AUTH_OPTIONS } from '@documenso/lib/next-auth/auth-options';
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
import { getTeams } from '@documenso/lib/server-only/team/get-teams';
+import { Banner } from '~/components/(dashboard)/layout/banner';
import { Header } from '~/components/(dashboard)/layout/header';
import { VerifyEmailBanner } from '~/components/(dashboard)/layout/verify-email-banner';
import { RefreshOnFocus } from '~/components/(dashboard)/refresh-on-focus/refresh-on-focus';
@@ -37,6 +38,8 @@ export default async function AuthenticatedDashboardLayout({
{!user.emailVerified && }
+
+
{children}
diff --git a/apps/web/src/app/(dashboard)/settings/profile/delete-account-dialog.tsx b/apps/web/src/app/(dashboard)/settings/profile/delete-account-dialog.tsx
new file mode 100644
index 000000000..933b37f31
--- /dev/null
+++ b/apps/web/src/app/(dashboard)/settings/profile/delete-account-dialog.tsx
@@ -0,0 +1,124 @@
+'use client';
+
+import { signOut } from 'next-auth/react';
+
+import type { User } from '@documenso/prisma/client';
+import { TRPCClientError } from '@documenso/trpc/client';
+import { trpc } from '@documenso/trpc/react';
+import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
+import { Button } from '@documenso/ui/primitives/button';
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from '@documenso/ui/primitives/dialog';
+import { useToast } from '@documenso/ui/primitives/use-toast';
+
+export type DeleteAccountDialogProps = {
+ className?: string;
+ user: User;
+};
+
+export const DeleteAccountDialog = ({ className, user }: DeleteAccountDialogProps) => {
+ const { toast } = useToast();
+
+ const hasTwoFactorAuthentication = user.twoFactorEnabled;
+
+ const { mutateAsync: deleteAccount, isLoading: isDeletingAccount } =
+ trpc.profile.deleteAccount.useMutation();
+
+ const onDeleteAccount = async () => {
+ try {
+ await deleteAccount();
+
+ toast({
+ title: 'Account deleted',
+ description: 'Your account has been deleted successfully.',
+ duration: 5000,
+ });
+
+ return await signOut({ callbackUrl: '/' });
+ } catch (err) {
+ if (err instanceof TRPCClientError && err.data?.code === 'BAD_REQUEST') {
+ toast({
+ title: 'An error occurred',
+ description: err.message,
+ variant: 'destructive',
+ });
+ } else {
+ toast({
+ title: 'An unknown error occurred',
+ variant: 'destructive',
+ description:
+ err.message ??
+ 'We encountered an unknown error while attempting to delete your account. Please try again later.',
+ });
+ }
+ }
+ };
+
+ return (
+
+
+
+
Delete Account
+
+ Delete your account and all its contents, including completed documents. This action is
+ irreversible and will cancel your subscription, so proceed with caution.
+
+
+
+
+
+
+ Delete Account
+
+
+
+ Delete Account
+
+
+
+ This action is not reversible. Please be certain.
+
+
+
+ {hasTwoFactorAuthentication && (
+
+
+ Disable Two Factor Authentication before deleting your account.
+
+
+ )}
+
+
+ Documenso will delete all of your documents
+ , along with all of your completed documents, signatures, and all other resources
+ belonging to your Account.
+
+
+
+
+
+ {isDeletingAccount ? 'Deleting account...' : 'Delete Account'}
+
+
+
+
+
+
+
+ );
+};
diff --git a/apps/web/src/app/(dashboard)/settings/profile/page.tsx b/apps/web/src/app/(dashboard)/settings/profile/page.tsx
index 2890eb5d5..11cfc8515 100644
--- a/apps/web/src/app/(dashboard)/settings/profile/page.tsx
+++ b/apps/web/src/app/(dashboard)/settings/profile/page.tsx
@@ -5,6 +5,8 @@ import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-
import { SettingsHeader } from '~/components/(dashboard)/settings/layout/header';
import { ProfileForm } from '~/components/forms/profile';
+import { DeleteAccountDialog } from './delete-account-dialog';
+
export const metadata: Metadata = {
title: 'Profile',
};
@@ -16,7 +18,9 @@ export default async function ProfileSettingsPage() {
);
}
diff --git a/apps/web/src/app/(dashboard)/settings/tokens/page.tsx b/apps/web/src/app/(dashboard)/settings/tokens/page.tsx
new file mode 100644
index 000000000..8951098c4
--- /dev/null
+++ b/apps/web/src/app/(dashboard)/settings/tokens/page.tsx
@@ -0,0 +1,74 @@
+import { DateTime } from 'luxon';
+
+import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
+import { getUserTokens } from '@documenso/lib/server-only/public-api/get-all-user-tokens';
+import { Button } from '@documenso/ui/primitives/button';
+
+import DeleteTokenDialog from '~/components/(dashboard)/settings/token/delete-token-dialog';
+import { LocaleDate } from '~/components/formatter/locale-date';
+import { ApiTokenForm } from '~/components/forms/token';
+
+export default async function ApiTokensPage() {
+ const { user } = await getRequiredServerComponentSession();
+
+ const tokens = await getUserTokens({ userId: user.id });
+
+ return (
+
+
API Tokens
+
+
+ On this page, you can create new API tokens and manage the existing ones.
+
+
+
+
+
+
+
+
+
Your existing tokens
+
+ {tokens.length === 0 && (
+
+
+ Your tokens will be shown here once you create them.
+
+
+ )}
+
+ {tokens.length > 0 && (
+
+ {tokens.map((token) => (
+
+
+
+
{token.name}
+
+
+ Created on
+
+ {token.expires ? (
+
+ Expires on
+
+ ) : (
+
+ Token doesn't have an expiration date
+
+ )}
+
+
+
+
+ Delete
+
+
+
+
+ ))}
+
+ )}
+
+ );
+}
diff --git a/apps/web/src/app/(dashboard)/templates/data-table-templates.tsx b/apps/web/src/app/(dashboard)/templates/data-table-templates.tsx
index 81bfd0ac2..e878d8df2 100644
--- a/apps/web/src/app/(dashboard)/templates/data-table-templates.tsx
+++ b/apps/web/src/app/(dashboard)/templates/data-table-templates.tsx
@@ -1,30 +1,31 @@
'use client';
-import { useState, useTransition } from 'react';
+import { useTransition } from 'react';
import Link from 'next/link';
-import { useRouter } from 'next/navigation';
-import { AlertTriangle, Loader, Plus } from 'lucide-react';
+import { AlertTriangle, Loader } from 'lucide-react';
import { useLimits } from '@documenso/ee/server-only/limits/provider/client';
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
-import type { Template } from '@documenso/prisma/client';
-import { trpc } from '@documenso/trpc/react';
+import type { Recipient, Template } from '@documenso/prisma/client';
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
-import { Button } from '@documenso/ui/primitives/button';
import { DataTable } from '@documenso/ui/primitives/data-table';
import { DataTablePagination } from '@documenso/ui/primitives/data-table-pagination';
-import { useToast } from '@documenso/ui/primitives/use-toast';
import { LocaleDate } from '~/components/formatter/locale-date';
import { TemplateType } from '~/components/formatter/template-type';
import { DataTableActionDropdown } from './data-table-action-dropdown';
import { DataTableTitle } from './data-table-title';
+import { UseTemplateDialog } from './use-template-dialog';
+
+type TemplateWithRecipient = Template & {
+ Recipient: Recipient[];
+};
type TemplatesDataTableProps = {
- templates: Template[];
+ templates: TemplateWithRecipient[];
perPage: number;
page: number;
totalPages: number;
@@ -47,14 +48,6 @@ export const TemplatesDataTable = ({
const { remaining } = useLimits();
- const router = useRouter();
-
- const { toast } = useToast();
- const [loadingStates, setLoadingStates] = useState<{ [key: string]: boolean }>({});
-
- const { mutateAsync: createDocumentFromTemplate } =
- trpc.template.createDocumentFromTemplate.useMutation();
-
const onPaginationChange = (page: number, perPage: number) => {
startTransition(() => {
updateSearchParams({
@@ -64,28 +57,6 @@ export const TemplatesDataTable = ({
});
};
- const onUseButtonClick = async (templateId: number) => {
- try {
- const { id } = await createDocumentFromTemplate({
- templateId,
- });
-
- toast({
- title: 'Document created',
- description: 'Your document has been created from the template successfully.',
- duration: 5000,
- });
-
- router.push(`${documentRootPath}/${id}/edit`);
- } catch (err) {
- toast({
- title: 'Error',
- description: 'An error occurred while creating document from template.',
- variant: 'destructive',
- });
- }
- };
-
return (
{remaining.documents === 0 && (
@@ -121,22 +92,13 @@ export const TemplatesDataTable = ({
header: 'Actions',
accessorKey: 'actions',
cell: ({ row }) => {
- const isRowLoading = loadingStates[row.original.id];
-
return (
-
{
- setLoadingStates((prev) => ({ ...prev, [row.original.id]: true }));
- await onUseButtonClick(row.original.id);
- setLoadingStates((prev) => ({ ...prev, [row.original.id]: false }));
- }}
- >
- {!isRowLoading && }
- Use Template
-
+
;
+
+export type UseTemplateDialogProps = {
+ templateId: number;
+ recipients: Recipient[];
+ documentRootPath: string;
+};
+
+export function UseTemplateDialog({
+ recipients,
+ documentRootPath,
+ templateId,
+}: UseTemplateDialogProps) {
+ const router = useRouter();
+ const { toast } = useToast();
+
+ const team = useOptionalCurrentTeam();
+
+ const {
+ control,
+ handleSubmit,
+ formState: { errors, isSubmitting },
+ } = useForm({
+ resolver: zodResolver(ZAddRecipientsForNewDocumentSchema),
+ defaultValues: {
+ recipients:
+ recipients.length > 0
+ ? recipients.map((recipient) => ({
+ nativeId: recipient.id,
+ formId: String(recipient.id),
+ name: recipient.name,
+ email: recipient.email,
+ role: recipient.role,
+ }))
+ : [
+ {
+ name: '',
+ email: '',
+ role: RecipientRole.SIGNER,
+ },
+ ],
+ },
+ });
+
+ const { mutateAsync: createDocumentFromTemplate, isLoading: isCreatingDocumentFromTemplate } =
+ trpc.template.createDocumentFromTemplate.useMutation();
+
+ const onSubmit = async (data: TAddRecipientsForNewDocumentSchema) => {
+ try {
+ const { id } = await createDocumentFromTemplate({
+ templateId,
+ teamId: team?.id,
+ recipients: data.recipients,
+ });
+
+ toast({
+ title: 'Document created',
+ description: 'Your document has been created from the template successfully.',
+ duration: 5000,
+ });
+
+ router.push(`${documentRootPath}/${id}`);
+ } catch (err) {
+ toast({
+ title: 'Error',
+ description: 'An error occurred while creating document from template.',
+ variant: 'destructive',
+ });
+ }
+ };
+
+ const onCreateDocumentFromTemplate = handleSubmit(onSubmit);
+
+ const { fields: formRecipients } = useFieldArray({
+ control,
+ name: 'recipients',
+ });
+
+ return (
+
+
+
+
+ Use Template
+
+
+
+
+ Document Recipients
+ Add the recipients to create the template with.
+
+
+ {formRecipients.map((recipient, index) => (
+
+
+
+ Email
+ *
+
+
+ (
+
+ )}
+ />
+
+
+
+ Name
+
+ (
+
+ )}
+ />
+
+
+
+
(
+ onChange(x)}>
+ {ROLE_ICONS[value]}
+
+
+
+
+ {ROLE_ICONS[RecipientRole.SIGNER]}
+ Signer
+
+
+
+
+
+ {ROLE_ICONS[RecipientRole.CC]}
+ Receives copy
+
+
+
+
+
+ {ROLE_ICONS[RecipientRole.APPROVER]}
+ Approver
+
+
+
+
+
+ {ROLE_ICONS[RecipientRole.VIEWER]}
+ Viewer
+
+
+
+
+ )}
+ />
+
+
+
+
+
+
+
+ ))}
+
+
+
+
+
+ Close
+
+
+
+
+ Create Document
+
+
+
+
+ );
+}
diff --git a/apps/web/src/app/(signing)/sign/[token]/page.tsx b/apps/web/src/app/(signing)/sign/[token]/page.tsx
index 99b9d1dd7..83cdb93e2 100644
--- a/apps/web/src/app/(signing)/sign/[token]/page.tsx
+++ b/apps/web/src/app/(signing)/sign/[token]/page.tsx
@@ -29,6 +29,7 @@ import { NameField } from './name-field';
import { NoLongerAvailable } from './no-longer-available';
import { SigningProvider } from './provider';
import { SignatureField } from './signature-field';
+import { TextField } from './text-field';
export type SigningPageProps = {
params: {
@@ -168,6 +169,9 @@ export default async function SigningPage({ params: { token } }: SigningPageProp
.with(FieldType.EMAIL, () => (
))
+ .with(FieldType.TEXT, () => (
+
+ ))
.otherwise(() => null),
)}
diff --git a/apps/web/src/app/(signing)/sign/[token]/text-field.tsx b/apps/web/src/app/(signing)/sign/[token]/text-field.tsx
new file mode 100644
index 000000000..0b91fa283
--- /dev/null
+++ b/apps/web/src/app/(signing)/sign/[token]/text-field.tsx
@@ -0,0 +1,166 @@
+'use client';
+
+import { useEffect, useState, useTransition } from 'react';
+
+import { useRouter } from 'next/navigation';
+
+import { Loader } from 'lucide-react';
+
+import type { Recipient } from '@documenso/prisma/client';
+import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
+import { trpc } from '@documenso/trpc/react';
+import { Button } from '@documenso/ui/primitives/button';
+import { Dialog, DialogContent, DialogFooter, DialogTitle } from '@documenso/ui/primitives/dialog';
+import { Input } from '@documenso/ui/primitives/input';
+import { Label } from '@documenso/ui/primitives/label';
+import { useToast } from '@documenso/ui/primitives/use-toast';
+
+import { SigningFieldContainer } from './signing-field-container';
+
+export type TextFieldProps = {
+ field: FieldWithSignature;
+ recipient: Recipient;
+};
+
+export const TextField = ({ field, recipient }: TextFieldProps) => {
+ const router = useRouter();
+
+ const { toast } = useToast();
+
+ const [isPending, startTransition] = useTransition();
+
+ const { mutateAsync: signFieldWithToken, isLoading: isSignFieldWithTokenLoading } =
+ trpc.field.signFieldWithToken.useMutation();
+
+ const {
+ mutateAsync: removeSignedFieldWithToken,
+ isLoading: isRemoveSignedFieldWithTokenLoading,
+ } = trpc.field.removeSignedFieldWithToken.useMutation();
+
+ const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading || isPending;
+
+ const [showCustomTextModal, setShowCustomTextModal] = useState(false);
+ const [localText, setLocalCustomText] = useState('');
+ const [isLocalSignatureSet, setIsLocalSignatureSet] = useState(false);
+
+ useEffect(() => {
+ if (!showCustomTextModal && !isLocalSignatureSet) {
+ setLocalCustomText('');
+ }
+ }, [showCustomTextModal, isLocalSignatureSet]);
+
+ const onSign = async () => {
+ try {
+ if (!localText) {
+ setIsLocalSignatureSet(false);
+ setShowCustomTextModal(true);
+ return;
+ }
+
+ if (!localText) {
+ return;
+ }
+
+ await signFieldWithToken({
+ token: recipient.token,
+ fieldId: field.id,
+ value: localText,
+ isBase64: true,
+ });
+
+ setLocalCustomText('');
+
+ startTransition(() => router.refresh());
+ } catch (err) {
+ console.error(err);
+
+ toast({
+ title: 'Error',
+ description: 'An error occurred while signing the document.',
+ variant: 'destructive',
+ });
+ }
+ };
+
+ const onRemove = async () => {
+ try {
+ await removeSignedFieldWithToken({
+ token: recipient.token,
+ fieldId: field.id,
+ });
+
+ startTransition(() => router.refresh());
+ } catch (err) {
+ console.error(err);
+
+ toast({
+ title: 'Error',
+ description: 'An error occurred while removing the text.',
+ variant: 'destructive',
+ });
+ }
+ };
+
+ return (
+
+ {isLoading && (
+
+
+
+ )}
+
+ {!field.inserted && (
+ Text
+ )}
+
+ {field.inserted && {field.customText}
}
+
+
+
+
+ Enter your Text ({recipient.email})
+
+
+
+ Custom Text
+
+ setLocalCustomText(e.target.value)}
+ />
+
+
+
+
+ {
+ setShowCustomTextModal(false);
+ setLocalCustomText('');
+ }}
+ >
+ Cancel
+
+
+ {
+ setShowCustomTextModal(false);
+ setIsLocalSignatureSet(true);
+ void onSign();
+ }}
+ >
+ Save Text
+
+
+
+
+
+
+ );
+};
diff --git a/apps/web/src/app/(teams)/t/[teamUrl]/layout.tsx b/apps/web/src/app/(teams)/t/[teamUrl]/layout.tsx
index 2883abc21..e0cd23acb 100644
--- a/apps/web/src/app/(teams)/t/[teamUrl]/layout.tsx
+++ b/apps/web/src/app/(teams)/t/[teamUrl]/layout.tsx
@@ -11,6 +11,7 @@ import { SubscriptionStatus } from '@documenso/prisma/client';
import { Header } from '~/components/(dashboard)/layout/header';
import { RefreshOnFocus } from '~/components/(dashboard)/refresh-on-focus/refresh-on-focus';
import { NextAuthProvider } from '~/providers/next-auth';
+import { TeamProvider } from '~/providers/team';
import { LayoutBillingBanner } from './layout-billing-banner';
@@ -56,7 +57,9 @@ export default async function AuthenticatedTeamsLayout({
- {children}
+
+ {children}
+
diff --git a/apps/web/src/app/(teams)/t/[teamUrl]/settings/tokens/page.tsx b/apps/web/src/app/(teams)/t/[teamUrl]/settings/tokens/page.tsx
new file mode 100644
index 000000000..2b775d32c
--- /dev/null
+++ b/apps/web/src/app/(teams)/t/[teamUrl]/settings/tokens/page.tsx
@@ -0,0 +1,85 @@
+import { DateTime } from 'luxon';
+
+import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
+import { getTeamTokens } from '@documenso/lib/server-only/public-api/get-all-team-tokens';
+import { getTeamByUrl } from '@documenso/lib/server-only/team/get-team';
+import { Button } from '@documenso/ui/primitives/button';
+
+import DeleteTokenDialog from '~/components/(dashboard)/settings/token/delete-token-dialog';
+import { LocaleDate } from '~/components/formatter/locale-date';
+import { ApiTokenForm } from '~/components/forms/token';
+
+type ApiTokensPageProps = {
+ params: {
+ teamUrl: string;
+ };
+};
+
+export default async function ApiTokensPage({ params }: ApiTokensPageProps) {
+ const { teamUrl } = params;
+
+ const { user } = await getRequiredServerComponentSession();
+
+ const team = await getTeamByUrl({ userId: user.id, teamUrl });
+
+ const tokens = await getTeamTokens({ userId: user.id, teamId: team.id });
+
+ return (
+
+
API Tokens
+
+
+ On this page, you can create new API tokens and manage the existing ones.
+
+
+
+
+
+
+
+
+
Your existing tokens
+
+ {tokens.length === 0 && (
+
+
+ Your tokens will be shown here once you create them.
+
+
+ )}
+
+ {tokens.length > 0 && (
+
+ {tokens.map((token) => (
+
+
+
+
{token.name}
+
+
+ Created on
+
+ {token.expires ? (
+
+ Expires on
+
+ ) : (
+
+ Token doesn't have an expiration date
+
+ )}
+
+
+
+
+ Delete
+
+
+
+
+ ))}
+
+ )}
+
+ );
+}
diff --git a/apps/web/src/app/api/v1/openapi/page.tsx b/apps/web/src/app/api/v1/openapi/page.tsx
new file mode 100644
index 000000000..ca5c3a5ed
--- /dev/null
+++ b/apps/web/src/app/api/v1/openapi/page.tsx
@@ -0,0 +1,3 @@
+'use client';
+
+export { OpenApiDocsPage as default } from '@documenso/api/v1/api-documentation';
diff --git a/apps/web/src/components/(dashboard)/layout/banner.tsx b/apps/web/src/components/(dashboard)/layout/banner.tsx
new file mode 100644
index 000000000..95a0de3dd
--- /dev/null
+++ b/apps/web/src/components/(dashboard)/layout/banner.tsx
@@ -0,0 +1,29 @@
+import { getSiteSettings } from '@documenso/lib/server-only/site-settings/get-site-settings';
+import { SITE_SETTINGS_BANNER_ID } from '@documenso/lib/server-only/site-settings/schemas/banner';
+
+export const Banner = async () => {
+ const banner = await getSiteSettings().then((settings) =>
+ settings.find((setting) => setting.id === SITE_SETTINGS_BANNER_ID),
+ );
+
+ return (
+ <>
+ {banner && banner.enabled && (
+
+ )}
+ >
+ );
+};
+
+// Banner
+// Custom Text
+// Custom Text with Custom Icon
diff --git a/apps/web/src/components/(dashboard)/layout/profile-dropdown.tsx b/apps/web/src/components/(dashboard)/layout/profile-dropdown.tsx
index a767b9700..6b9ec7fdf 100644
--- a/apps/web/src/components/(dashboard)/layout/profile-dropdown.tsx
+++ b/apps/web/src/components/(dashboard)/layout/profile-dropdown.tsx
@@ -3,6 +3,7 @@
import Link from 'next/link';
import {
+ Braces,
CreditCard,
FileSpreadsheet,
Lock,
@@ -98,6 +99,13 @@ export const ProfileDropdown = ({ user }: ProfileDropdownProps) => {
+
+
+
+ API Tokens
+
+
+
{isBillingEnabled && (
diff --git a/apps/web/src/components/(dashboard)/settings/layout/desktop-nav.tsx b/apps/web/src/components/(dashboard)/settings/layout/desktop-nav.tsx
index 572c91c76..e87c47b67 100644
--- a/apps/web/src/components/(dashboard)/settings/layout/desktop-nav.tsx
+++ b/apps/web/src/components/(dashboard)/settings/layout/desktop-nav.tsx
@@ -5,7 +5,7 @@ import type { HTMLAttributes } from 'react';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
-import { CreditCard, Lock, User, Users } from 'lucide-react';
+import { Braces, CreditCard, Lock, User, Users } from 'lucide-react';
import { useFeatureFlags } from '@documenso/lib/client-only/providers/feature-flag';
import { cn } from '@documenso/ui/lib/utils';
@@ -64,6 +64,19 @@ export const DesktopNav = ({ className, ...props }: DesktopNavProps) => {
+
+
+
+ API Tokens
+
+
+
{isBillingEnabled && (
{
+
+
+
+ API Tokens
+
+
+
{isBillingEnabled && (
;
+ onDelete?: () => void;
+ children?: React.ReactNode;
+};
+
+export default function DeleteTokenDialog({
+ teamId,
+ token,
+ onDelete,
+ children,
+}: DeleteTokenDialogProps) {
+ const router = useRouter();
+ const { toast } = useToast();
+
+ const [isOpen, setIsOpen] = useState(false);
+
+ const deleteMessage = `delete ${token.name}`;
+
+ const ZDeleteTokenDialogSchema = z.object({
+ tokenName: z.literal(deleteMessage, {
+ errorMap: () => ({ message: `You must enter '${deleteMessage}' to proceed` }),
+ }),
+ });
+
+ type TDeleteTokenByIdMutationSchema = z.infer;
+
+ const { mutateAsync: deleteTokenMutation } = trpc.apiToken.deleteTokenById.useMutation({
+ onSuccess() {
+ onDelete?.();
+ },
+ });
+
+ const form = useForm({
+ resolver: zodResolver(ZDeleteTokenDialogSchema),
+ values: {
+ tokenName: '',
+ },
+ });
+
+ const onSubmit = async () => {
+ try {
+ await deleteTokenMutation({
+ id: token.id,
+ teamId,
+ });
+
+ toast({
+ title: 'Token deleted',
+ description: 'The token was deleted successfully.',
+ duration: 5000,
+ });
+
+ setIsOpen(false);
+
+ router.refresh();
+ } catch (error) {
+ toast({
+ title: 'An unknown error occurred',
+ variant: 'destructive',
+ duration: 5000,
+ description:
+ 'We encountered an unknown error while attempting to delete this token. Please try again later.',
+ });
+ }
+ };
+
+ useEffect(() => {
+ if (!isOpen) {
+ form.reset();
+ }
+ }, [isOpen, form]);
+
+ return (
+ !form.formState.isSubmitting && setIsOpen(value)}
+ >
+
+ {children ?? (
+
+ Delete
+
+ )}
+
+
+
+
+ Are you sure you want to delete this token?
+
+
+ Please note that this action is irreversible. Once confirmed, your token will be
+ permanently deleted.
+
+
+
+
+
+
+ (
+
+
+ Confirm by typing:{' '}
+
+ {deleteMessage}
+
+
+
+
+
+
+
+
+ )}
+ />
+
+
+
+ setIsOpen(false)}
+ >
+ Cancel
+
+
+
+ I'm sure! Delete it
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/apps/web/src/components/(teams)/settings/layout/desktop-nav.tsx b/apps/web/src/components/(teams)/settings/layout/desktop-nav.tsx
index 98df7416e..20fe8cb2e 100644
--- a/apps/web/src/components/(teams)/settings/layout/desktop-nav.tsx
+++ b/apps/web/src/components/(teams)/settings/layout/desktop-nav.tsx
@@ -5,7 +5,7 @@ import type { HTMLAttributes } from 'react';
import Link from 'next/link';
import { useParams, usePathname } from 'next/navigation';
-import { CreditCard, Settings, Users } from 'lucide-react';
+import { Braces, CreditCard, Settings, Users } from 'lucide-react';
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
import { cn } from '@documenso/ui/lib/utils';
@@ -21,6 +21,7 @@ export const DesktopNav = ({ className, ...props }: DesktopNavProps) => {
const settingsPath = `/t/${teamUrl}/settings`;
const membersPath = `/t/${teamUrl}/settings/members`;
+ const tokensPath = `/t/${teamUrl}/settings/tokens`;
const billingPath = `/t/${teamUrl}/settings/billing`;
return (
@@ -48,6 +49,16 @@ export const DesktopNav = ({ className, ...props }: DesktopNavProps) => {
+
+
+
+ API Tokens
+
+
+
{IS_BILLING_ENABLED() && (
{
const settingsPath = `/t/${teamUrl}/settings`;
const membersPath = `/t/${teamUrl}/settings/members`;
+ const tokensPath = `/t/${teamUrl}/settings/tokens`;
const billingPath = `/t/${teamUrl}/settings/billing`;
return (
@@ -56,6 +57,16 @@ export const MobileNav = ({ className, ...props }: MobileNavProps) => {
+
+
+
+ API Tokens
+
+
+
{IS_BILLING_ENABLED() && (
;
export type TProfileFormSchema = z.infer;
export type ProfileFormProps = {
@@ -50,8 +55,11 @@ export const ProfileForm = ({ className, user }: ProfileFormProps) => {
});
const isSubmitting = form.formState.isSubmitting;
+ const hasTwoFactorAuthentication = user.twoFactorEnabled;
const { mutateAsync: updateProfile } = trpc.profile.updateProfile.useMutation();
+ const { mutateAsync: deleteAccount, isLoading: isDeletingAccount } =
+ trpc.profile.deleteAccount.useMutation();
const onFormSubmit = async ({ name, signature }: TProfileFormSchema) => {
try {
@@ -133,7 +141,7 @@ export const ProfileForm = ({ className, user }: ProfileFormProps) => {
/>
-
+
{isSubmitting ? 'Updating profile...' : 'Update profile'}
diff --git a/apps/web/src/components/forms/token.tsx b/apps/web/src/components/forms/token.tsx
new file mode 100644
index 000000000..7081ac3c9
--- /dev/null
+++ b/apps/web/src/components/forms/token.tsx
@@ -0,0 +1,257 @@
+'use client';
+
+import { useState } from 'react';
+
+import { useRouter } from 'next/navigation';
+
+import { zodResolver } from '@hookform/resolvers/zod';
+import { useForm } from 'react-hook-form';
+import { z } from 'zod';
+
+import { useCopyToClipboard } from '@documenso/lib/client-only/hooks/use-copy-to-clipboard';
+import { TRPCClientError } from '@documenso/trpc/client';
+import { trpc } from '@documenso/trpc/react';
+import type { TCreateTokenMutationSchema } from '@documenso/trpc/server/api-token-router/schema';
+import { ZCreateTokenMutationSchema } from '@documenso/trpc/server/api-token-router/schema';
+import { cn } from '@documenso/ui/lib/utils';
+import { Button } from '@documenso/ui/primitives/button';
+import { Card, CardContent } from '@documenso/ui/primitives/card';
+import {
+ Form,
+ FormControl,
+ FormDescription,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from '@documenso/ui/primitives/form/form';
+import { Input } from '@documenso/ui/primitives/input';
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from '@documenso/ui/primitives/select';
+import { Switch } from '@documenso/ui/primitives/switch';
+import { useToast } from '@documenso/ui/primitives/use-toast';
+
+import { EXPIRATION_DATES } from '../(dashboard)/settings/token/contants';
+
+const ZCreateTokenFormSchema = ZCreateTokenMutationSchema.extend({
+ enabled: z.boolean(),
+});
+
+type TCreateTokenFormSchema = z.infer;
+
+export type ApiTokenFormProps = {
+ className?: string;
+ teamId?: number;
+};
+
+export const ApiTokenForm = ({ className, teamId }: ApiTokenFormProps) => {
+ const router = useRouter();
+
+ const [, copy] = useCopyToClipboard();
+ const { toast } = useToast();
+
+ const [newlyCreatedToken, setNewlyCreatedToken] = useState('');
+ const [noExpirationDate, setNoExpirationDate] = useState(false);
+
+ const { mutateAsync: createTokenMutation } = trpc.apiToken.createToken.useMutation({
+ onSuccess(data) {
+ setNewlyCreatedToken(data.token);
+ },
+ });
+
+ const form = useForm({
+ resolver: zodResolver(ZCreateTokenFormSchema),
+ defaultValues: {
+ tokenName: '',
+ expirationDate: '',
+ enabled: false,
+ },
+ });
+
+ const copyToken = async (token: string) => {
+ try {
+ const copied = await copy(token);
+
+ if (!copied) {
+ throw new Error('Unable to copy the token');
+ }
+
+ toast({
+ title: 'Token copied to clipboard',
+ description: 'The token was copied to your clipboard.',
+ });
+ } catch (error) {
+ toast({
+ title: 'Unable to copy token',
+ description: 'We were unable to copy the token to your clipboard. Please try again.',
+ variant: 'destructive',
+ });
+ }
+ };
+
+ const onSubmit = async ({ tokenName, expirationDate }: TCreateTokenMutationSchema) => {
+ try {
+ await createTokenMutation({
+ teamId,
+ tokenName,
+ expirationDate: noExpirationDate ? null : expirationDate,
+ });
+
+ toast({
+ title: 'Token created',
+ description: 'A new token was created successfully.',
+ duration: 5000,
+ });
+
+ form.reset();
+
+ router.refresh();
+ } catch (error) {
+ if (error instanceof TRPCClientError && error.data?.code === 'BAD_REQUEST') {
+ toast({
+ title: 'An error occurred',
+ description: error.message,
+ variant: 'destructive',
+ });
+ } else {
+ toast({
+ title: 'An unknown error occurred',
+ variant: 'destructive',
+ duration: 5000,
+ description:
+ 'We encountered an unknown error while attempting create the new token. Please try again later.',
+ });
+ }
+ }
+ };
+
+ return (
+
+
+
+
+ (
+
+ Token name
+
+
+
+
+
+
+
+
+ Please enter a meaningful name for your token. This will help you identify it
+ later.
+
+
+
+
+ )}
+ />
+
+
+
(
+
+ Token expiration date
+
+
+
+
+
+
+
+
+ {Object.entries(EXPIRATION_DATES).map(([key, date]) => (
+
+ {date}
+
+ ))}
+
+
+
+
+
+
+
+ )}
+ />
+
+ (
+
+ Never expire
+
+
+ {
+ setNoExpirationDate((prev) => !prev);
+ field.onChange(val);
+ }}
+ />
+
+
+
+
+ )}
+ />
+
+
+
+ Create token
+
+
+
+
+ Create token
+
+
+
+
+
+
+ {newlyCreatedToken && (
+
+
+
+ Your token was created successfully! Make sure to copy it because you won't be able to
+ see it again!
+
+
+
+ {newlyCreatedToken}
+
+
+ void copyToken(newlyCreatedToken)}>
+ Copy token
+
+
+
+ )}
+
+ );
+};
diff --git a/apps/web/src/pages/api/v1/[...ts-rest].tsx b/apps/web/src/pages/api/v1/[...ts-rest].tsx
new file mode 100644
index 000000000..fcc5e4ffe
--- /dev/null
+++ b/apps/web/src/pages/api/v1/[...ts-rest].tsx
@@ -0,0 +1,17 @@
+import type { NextApiRequest, NextApiResponse } from 'next';
+
+import { createNextRouter } from '@documenso/api/next';
+import { ApiContractV1 } from '@documenso/api/v1/contract';
+import { ApiContractV1Implementation } from '@documenso/api/v1/implementation';
+
+const nextRouteHandler = createNextRouter(ApiContractV1, ApiContractV1Implementation, {
+ responseValidation: true,
+});
+
+export default async function handler(req: NextApiRequest, res: NextApiResponse) {
+ // TODO: Dirty hack to make ts-rest handler work with next.js in a more intuitive way.
+ req.query['ts-rest'] = Array.isArray(req.query['ts-rest']) ? req.query['ts-rest'] : []; // Make `ts-rest` an array.
+ req.query['ts-rest'].unshift('api', 'v1'); // Prepend our base path to the array.
+
+ return await nextRouteHandler(req, res);
+}
diff --git a/apps/web/src/pages/api/v1/openapi.json.ts b/apps/web/src/pages/api/v1/openapi.json.ts
new file mode 100644
index 000000000..e3ea15051
--- /dev/null
+++ b/apps/web/src/pages/api/v1/openapi.json.ts
@@ -0,0 +1,7 @@
+import type { NextApiRequest, NextApiResponse } from 'next';
+
+import { OpenAPIV1 } from '@documenso/api/v1/openapi';
+
+export default function handler(req: NextApiRequest, res: NextApiResponse) {
+ res.status(200).json(OpenAPIV1);
+}
diff --git a/apps/web/src/providers/team.tsx b/apps/web/src/providers/team.tsx
new file mode 100644
index 000000000..c09ce18b2
--- /dev/null
+++ b/apps/web/src/providers/team.tsx
@@ -0,0 +1,31 @@
+'use client';
+
+import { createContext, useContext } from 'react';
+import React from 'react';
+
+import type { Team } from '@documenso/prisma/client';
+
+interface TeamProviderProps {
+ children: React.ReactNode;
+ team: Team;
+}
+
+const TeamContext = createContext(null);
+
+export const useCurrentTeam = (): Team | null => {
+ const context = useContext(TeamContext);
+
+ if (!context) {
+ throw new Error('useCurrentTeam must be used within a TeamProvider');
+ }
+
+ return context;
+};
+
+export const useOptionalCurrentTeam = (): Team | null => {
+ return useContext(TeamContext);
+};
+
+export const TeamProvider = ({ children, team }: TeamProviderProps) => {
+ return {children} ;
+};
diff --git a/package-lock.json b/package-lock.json
index 3c136e801..27227172c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -130,6 +130,7 @@
"version": "1.2.3",
"license": "AGPL-3.0",
"dependencies": {
+ "@documenso/api": "*",
"@documenso/assets": "*",
"@documenso/ee": "*",
"@documenso/lib": "*",
@@ -158,6 +159,7 @@
"react-hotkeys-hook": "^4.4.1",
"react-icons": "^4.11.0",
"react-rnd": "^10.4.1",
+ "remeda": "^1.27.1",
"sharp": "0.33.1",
"ts-pattern": "^5.0.5",
"typescript": "5.2.2",
@@ -250,6 +252,18 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/@anatine/zod-openapi": {
+ "version": "1.14.2",
+ "resolved": "https://registry.npmjs.org/@anatine/zod-openapi/-/zod-openapi-1.14.2.tgz",
+ "integrity": "sha512-q0qHfnuNYVKu0Swrnnvfj9971AEyW7c8v9jCOZGCl5ZbyGMNG4RPyJkRcMi/JC8CRfdOe0IDfNm1nNsi2avprg==",
+ "dependencies": {
+ "ts-deepmerge": "^6.0.3"
+ },
+ "peerDependencies": {
+ "openapi3-ts": "^2.0.0 || ^3.0.0",
+ "zod": "^3.20.0"
+ }
+ },
"node_modules/@aws-crypto/crc32": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-3.0.0.tgz",
@@ -1360,6 +1374,18 @@
"node": ">=6.9.0"
}
},
+ "node_modules/@babel/runtime-corejs3": {
+ "version": "7.23.8",
+ "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.23.8.tgz",
+ "integrity": "sha512-2ZzmcDugdm0/YQKFVYsXiwUN7USPX8PM7cytpb4PFl87fM+qYPSvTZX//8tyeJB1j0YDmafBJEbl5f8NfLyuKw==",
+ "dependencies": {
+ "core-js-pure": "^3.30.2",
+ "regenerator-runtime": "^0.14.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
"node_modules/@babel/template": {
"version": "7.22.15",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz",
@@ -1453,6 +1479,11 @@
"node": ">=6.9.0"
}
},
+ "node_modules/@braintree/sanitize-url": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-7.0.0.tgz",
+ "integrity": "sha512-GMu2OJiTd1HSe74bbJYQnVvELANpYiGFZELyyTM1CR0sdv5ReQAcJ/c/8pIrPab3lO11+D+EpuGLUxqz+y832g=="
+ },
"node_modules/@commitlint/cli": {
"version": "17.8.1",
"resolved": "https://registry.npmjs.org/@commitlint/cli/-/cli-17.8.1.tgz",
@@ -1847,6 +1878,10 @@
"@jridgewell/sourcemap-codec": "^1.4.10"
}
},
+ "node_modules/@documenso/api": {
+ "resolved": "packages/api",
+ "link": true
+ },
"node_modules/@documenso/app-tests": {
"resolved": "packages/app-tests",
"link": true
@@ -2102,6 +2137,14 @@
"resolved": "https://registry.npmjs.org/@fal-works/esbuild-plugin-global-externals/-/esbuild-plugin-global-externals-2.1.2.tgz",
"integrity": "sha512-cEee/Z+I12mZcFJshKcCqC8tuX5hG3s+d+9nZ3LabqKF1vKdF41B92pJVCBggjAGORAeOzyyDDKrZwIkLffeOQ=="
},
+ "node_modules/@fastify/busboy": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.0.tgz",
+ "integrity": "sha512-+KpH+QxZU7O4675t3mnkQKcZZg56u+K/Ct2K+N2AZYNVK8kyeo/bI18tI8aPm3tvNNRyTWfj6s5tnGNlcbQRsA==",
+ "engines": {
+ "node": ">=14"
+ }
+ },
"node_modules/@floating-ui/core": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.5.0.tgz",
@@ -6224,6 +6267,472 @@
"node": ">=14.0.0"
}
},
+ "node_modules/@swagger-api/apidom-ast": {
+ "version": "0.92.0",
+ "resolved": "https://registry.npmjs.org/@swagger-api/apidom-ast/-/apidom-ast-0.92.0.tgz",
+ "integrity": "sha512-j9vuKaYZP3mAGXUcKeWIkSToxPPCBLJcLEfjSEh14P0n6NRJp7Yg19SA+IwHdIvOAfJonuebj/lhPOMjzd6P1g==",
+ "dependencies": {
+ "@babel/runtime-corejs3": "^7.20.7",
+ "@swagger-api/apidom-error": "^0.92.0",
+ "@types/ramda": "~0.29.6",
+ "ramda": "~0.29.1",
+ "ramda-adjunct": "^4.1.1",
+ "stampit": "^4.3.2",
+ "unraw": "^3.0.0"
+ }
+ },
+ "node_modules/@swagger-api/apidom-core": {
+ "version": "0.92.0",
+ "resolved": "https://registry.npmjs.org/@swagger-api/apidom-core/-/apidom-core-0.92.0.tgz",
+ "integrity": "sha512-PK1zlS0UCcE5dIPtSy8/+oWfXAVf7b/iM3LRaPgaFGF5b8qa6S/zmROTh10Yjug9v9Vnuq8opEhyHkGyl+WdSA==",
+ "dependencies": {
+ "@babel/runtime-corejs3": "^7.20.7",
+ "@swagger-api/apidom-ast": "^0.92.0",
+ "@swagger-api/apidom-error": "^0.92.0",
+ "@types/ramda": "~0.29.6",
+ "minim": "~0.23.8",
+ "ramda": "~0.29.1",
+ "ramda-adjunct": "^4.1.1",
+ "short-unique-id": "^5.0.2",
+ "stampit": "^4.3.2"
+ }
+ },
+ "node_modules/@swagger-api/apidom-error": {
+ "version": "0.92.0",
+ "resolved": "https://registry.npmjs.org/@swagger-api/apidom-error/-/apidom-error-0.92.0.tgz",
+ "integrity": "sha512-wo7xCvTpWr5Lpt/ly1L4bhZ6W7grgtAg7SK/d8FNZR85zPJXM4FPMpcRtKktfWJ/RikQJT/g5DjI33iTqB6z/w==",
+ "dependencies": {
+ "@babel/runtime-corejs3": "^7.20.7"
+ }
+ },
+ "node_modules/@swagger-api/apidom-json-pointer": {
+ "version": "0.92.0",
+ "resolved": "https://registry.npmjs.org/@swagger-api/apidom-json-pointer/-/apidom-json-pointer-0.92.0.tgz",
+ "integrity": "sha512-VmZ1EXE7BWX+ndeeh9t1uFRql5jbPRmAcglUfdtu3jlg6fOqXzzgx9qFpRz9GhpMHWEGFm1ymd8tMAa1CvgcHw==",
+ "dependencies": {
+ "@babel/runtime-corejs3": "^7.20.7",
+ "@swagger-api/apidom-core": "^0.92.0",
+ "@swagger-api/apidom-error": "^0.92.0",
+ "@types/ramda": "~0.29.6",
+ "ramda": "~0.29.1",
+ "ramda-adjunct": "^4.0.0"
+ }
+ },
+ "node_modules/@swagger-api/apidom-ns-api-design-systems": {
+ "version": "0.92.0",
+ "resolved": "https://registry.npmjs.org/@swagger-api/apidom-ns-api-design-systems/-/apidom-ns-api-design-systems-0.92.0.tgz",
+ "integrity": "sha512-wXEXhw0wDQIPTUqff953h44oQZr29DcoAzZfROWlGtOLItGDDMjhfIYiRg1406mXA4N7d5d0vNi9V/HXkxItQw==",
+ "optional": true,
+ "dependencies": {
+ "@babel/runtime-corejs3": "^7.20.7",
+ "@swagger-api/apidom-core": "^0.92.0",
+ "@swagger-api/apidom-error": "^0.92.0",
+ "@swagger-api/apidom-ns-openapi-3-1": "^0.92.0",
+ "@types/ramda": "~0.29.6",
+ "ramda": "~0.29.1",
+ "ramda-adjunct": "^4.1.1",
+ "stampit": "^4.3.2"
+ }
+ },
+ "node_modules/@swagger-api/apidom-ns-asyncapi-2": {
+ "version": "0.92.0",
+ "resolved": "https://registry.npmjs.org/@swagger-api/apidom-ns-asyncapi-2/-/apidom-ns-asyncapi-2-0.92.0.tgz",
+ "integrity": "sha512-FmJLT3GqzT4HK7Mwh54cXZ4PZt58yKVtJAKWKJ0dg2/Gim0AKJWf6t6B3Z9ZFUiKyehbqP4K7gSM7qGL0tKe2Q==",
+ "optional": true,
+ "dependencies": {
+ "@babel/runtime-corejs3": "^7.20.7",
+ "@swagger-api/apidom-core": "^0.92.0",
+ "@swagger-api/apidom-ns-json-schema-draft-7": "^0.92.0",
+ "@types/ramda": "~0.29.6",
+ "ramda": "~0.29.1",
+ "ramda-adjunct": "^4.1.1",
+ "stampit": "^4.3.2"
+ }
+ },
+ "node_modules/@swagger-api/apidom-ns-json-schema-draft-4": {
+ "version": "0.92.0",
+ "resolved": "https://registry.npmjs.org/@swagger-api/apidom-ns-json-schema-draft-4/-/apidom-ns-json-schema-draft-4-0.92.0.tgz",
+ "integrity": "sha512-7s2EKjCQwRXbK4Y4AGpVkyn1AANCxOUFSHebo1h2katyVeAopV0LJmbXH5yQedTltV0k3BIjnd7hS+7dI846Pw==",
+ "dependencies": {
+ "@babel/runtime-corejs3": "^7.20.7",
+ "@swagger-api/apidom-ast": "^0.92.0",
+ "@swagger-api/apidom-core": "^0.92.0",
+ "@types/ramda": "~0.29.6",
+ "ramda": "~0.29.1",
+ "ramda-adjunct": "^4.1.1",
+ "stampit": "^4.3.2"
+ }
+ },
+ "node_modules/@swagger-api/apidom-ns-json-schema-draft-6": {
+ "version": "0.92.0",
+ "resolved": "https://registry.npmjs.org/@swagger-api/apidom-ns-json-schema-draft-6/-/apidom-ns-json-schema-draft-6-0.92.0.tgz",
+ "integrity": "sha512-zur80x04jesXVzlU9sLZhW4giO9RfOouI7L/H8v2wUlcBvjaPBn1tIqrURw2VEHKAcJORhTRusQCR21vnFot2g==",
+ "optional": true,
+ "dependencies": {
+ "@babel/runtime-corejs3": "^7.20.7",
+ "@swagger-api/apidom-core": "^0.92.0",
+ "@swagger-api/apidom-error": "^0.92.0",
+ "@swagger-api/apidom-ns-json-schema-draft-4": "^0.92.0",
+ "@types/ramda": "~0.29.6",
+ "ramda": "~0.29.1",
+ "ramda-adjunct": "^4.1.1",
+ "stampit": "^4.3.2"
+ }
+ },
+ "node_modules/@swagger-api/apidom-ns-json-schema-draft-7": {
+ "version": "0.92.0",
+ "resolved": "https://registry.npmjs.org/@swagger-api/apidom-ns-json-schema-draft-7/-/apidom-ns-json-schema-draft-7-0.92.0.tgz",
+ "integrity": "sha512-DSY7lY98XHnc0wg0V38ZmBPs5HWuRuSb6G+n5Z+qs5RRodh1x5BrTIY6M0Yk3oJVbbEoFGmF0VlTe6vHf44pbw==",
+ "optional": true,
+ "dependencies": {
+ "@babel/runtime-corejs3": "^7.20.7",
+ "@swagger-api/apidom-core": "^0.92.0",
+ "@swagger-api/apidom-error": "^0.92.0",
+ "@swagger-api/apidom-ns-json-schema-draft-6": "^0.92.0",
+ "@types/ramda": "~0.29.6",
+ "ramda": "~0.29.1",
+ "ramda-adjunct": "^4.1.1",
+ "stampit": "^4.3.2"
+ }
+ },
+ "node_modules/@swagger-api/apidom-ns-openapi-2": {
+ "version": "0.92.0",
+ "resolved": "https://registry.npmjs.org/@swagger-api/apidom-ns-openapi-2/-/apidom-ns-openapi-2-0.92.0.tgz",
+ "integrity": "sha512-OJlSTvPzK+zqzd2xXeWkF50z08Wlpygc98eVzZjYI0Af8mz7x6R5T9BCP5p6ZlQoO9OTvk4gfv7ViWXCdamObg==",
+ "optional": true,
+ "dependencies": {
+ "@babel/runtime-corejs3": "^7.20.7",
+ "@swagger-api/apidom-core": "^0.92.0",
+ "@swagger-api/apidom-error": "^0.92.0",
+ "@swagger-api/apidom-ns-json-schema-draft-4": "^0.92.0",
+ "@types/ramda": "~0.29.6",
+ "ramda": "~0.29.1",
+ "ramda-adjunct": "^4.1.1",
+ "stampit": "^4.3.2"
+ }
+ },
+ "node_modules/@swagger-api/apidom-ns-openapi-3-0": {
+ "version": "0.92.0",
+ "resolved": "https://registry.npmjs.org/@swagger-api/apidom-ns-openapi-3-0/-/apidom-ns-openapi-3-0-0.92.0.tgz",
+ "integrity": "sha512-VGha4RRnoeoAZBWLGy37YsBzwICM3ZFNyCk2Dwpaqfg9zFN+E6BL2CtIbkxvFkMdwaMURmDItiQsw28pF0tOgQ==",
+ "dependencies": {
+ "@babel/runtime-corejs3": "^7.20.7",
+ "@swagger-api/apidom-core": "^0.92.0",
+ "@swagger-api/apidom-error": "^0.92.0",
+ "@swagger-api/apidom-ns-json-schema-draft-4": "^0.92.0",
+ "@types/ramda": "~0.29.6",
+ "ramda": "~0.29.1",
+ "ramda-adjunct": "^4.1.1",
+ "stampit": "^4.3.2"
+ }
+ },
+ "node_modules/@swagger-api/apidom-ns-openapi-3-1": {
+ "version": "0.92.0",
+ "resolved": "https://registry.npmjs.org/@swagger-api/apidom-ns-openapi-3-1/-/apidom-ns-openapi-3-1-0.92.0.tgz",
+ "integrity": "sha512-xZD+JxifYhDoTjn76K2ZT3xNoXBQChaKfSkJr4l5Xh9Guuk0IcsPTUDRpuytuZZXVez0O401XFoUso/mZRTjkA==",
+ "dependencies": {
+ "@babel/runtime-corejs3": "^7.20.7",
+ "@swagger-api/apidom-ast": "^0.92.0",
+ "@swagger-api/apidom-core": "^0.92.0",
+ "@swagger-api/apidom-ns-openapi-3-0": "^0.92.0",
+ "@types/ramda": "~0.29.6",
+ "ramda": "~0.29.1",
+ "ramda-adjunct": "^4.1.1",
+ "stampit": "^4.3.2"
+ }
+ },
+ "node_modules/@swagger-api/apidom-ns-workflows-1": {
+ "version": "0.92.0",
+ "resolved": "https://registry.npmjs.org/@swagger-api/apidom-ns-workflows-1/-/apidom-ns-workflows-1-0.92.0.tgz",
+ "integrity": "sha512-gl1dF+SrRHK4lLiwaK4PMjL9A5z28cW9xiMWCxRyppX/I2bVTVVOfgdAyqLWsFA0gopmITWesJxohRumG35fTw==",
+ "optional": true,
+ "dependencies": {
+ "@babel/runtime-corejs3": "^7.20.7",
+ "@swagger-api/apidom-core": "^0.92.0",
+ "@swagger-api/apidom-ns-openapi-3-1": "^0.92.0",
+ "@types/ramda": "~0.29.6",
+ "ramda": "~0.29.1",
+ "ramda-adjunct": "^4.1.1",
+ "stampit": "^4.3.2"
+ }
+ },
+ "node_modules/@swagger-api/apidom-parser-adapter-api-design-systems-json": {
+ "version": "0.92.0",
+ "resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-api-design-systems-json/-/apidom-parser-adapter-api-design-systems-json-0.92.0.tgz",
+ "integrity": "sha512-i07FeLdNobWzHT9LnfsdOix+XrlZN/KnQL1RODPzxWk7i7ya2e4uc3JemyHh4Tnv04G8JV32SQqtzOtMteJsdA==",
+ "optional": true,
+ "dependencies": {
+ "@babel/runtime-corejs3": "^7.20.7",
+ "@swagger-api/apidom-core": "^0.92.0",
+ "@swagger-api/apidom-ns-api-design-systems": "^0.92.0",
+ "@swagger-api/apidom-parser-adapter-json": "^0.92.0",
+ "@types/ramda": "~0.29.6",
+ "ramda": "~0.29.1",
+ "ramda-adjunct": "^4.0.0"
+ }
+ },
+ "node_modules/@swagger-api/apidom-parser-adapter-api-design-systems-yaml": {
+ "version": "0.92.0",
+ "resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-api-design-systems-yaml/-/apidom-parser-adapter-api-design-systems-yaml-0.92.0.tgz",
+ "integrity": "sha512-bbjFkU0D4zqaZnd8/m1Kyx2UuHpri8ZxLdT1TiXqHweSfRQcNt4VYt0bjWBnnGGBMkHElgYbX5ov6kHvPf3wJg==",
+ "optional": true,
+ "dependencies": {
+ "@babel/runtime-corejs3": "^7.20.7",
+ "@swagger-api/apidom-core": "^0.92.0",
+ "@swagger-api/apidom-ns-api-design-systems": "^0.92.0",
+ "@swagger-api/apidom-parser-adapter-yaml-1-2": "^0.92.0",
+ "@types/ramda": "~0.29.6",
+ "ramda": "~0.29.1",
+ "ramda-adjunct": "^4.0.0"
+ }
+ },
+ "node_modules/@swagger-api/apidom-parser-adapter-asyncapi-json-2": {
+ "version": "0.92.0",
+ "resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-asyncapi-json-2/-/apidom-parser-adapter-asyncapi-json-2-0.92.0.tgz",
+ "integrity": "sha512-Q7gudmGA5TUGbbr0QYNQkndktP91C0WE7uDDS2IwCBtHroRDiMPFCjzE9dsjIST5WnP+LUXmxG1Bv0NLTWcSUg==",
+ "optional": true,
+ "dependencies": {
+ "@babel/runtime-corejs3": "^7.20.7",
+ "@swagger-api/apidom-core": "^0.92.0",
+ "@swagger-api/apidom-ns-asyncapi-2": "^0.92.0",
+ "@swagger-api/apidom-parser-adapter-json": "^0.92.0",
+ "@types/ramda": "~0.29.6",
+ "ramda": "~0.29.1",
+ "ramda-adjunct": "^4.0.0"
+ }
+ },
+ "node_modules/@swagger-api/apidom-parser-adapter-asyncapi-yaml-2": {
+ "version": "0.92.0",
+ "resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-asyncapi-yaml-2/-/apidom-parser-adapter-asyncapi-yaml-2-0.92.0.tgz",
+ "integrity": "sha512-V5/VdDj0aeOKp+3AtvPSz2b0HosJfYkHPjNvPU5eafLSzqzMIR/evYq5BvKWoJL1IvLdjoEPqDVVaEZluHZTew==",
+ "optional": true,
+ "dependencies": {
+ "@babel/runtime-corejs3": "^7.20.7",
+ "@swagger-api/apidom-core": "^0.92.0",
+ "@swagger-api/apidom-ns-asyncapi-2": "^0.92.0",
+ "@swagger-api/apidom-parser-adapter-yaml-1-2": "^0.92.0",
+ "@types/ramda": "~0.29.6",
+ "ramda": "~0.29.1",
+ "ramda-adjunct": "^4.0.0"
+ }
+ },
+ "node_modules/@swagger-api/apidom-parser-adapter-json": {
+ "version": "0.92.0",
+ "resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-json/-/apidom-parser-adapter-json-0.92.0.tgz",
+ "integrity": "sha512-KA1Nn6FN0zTA5JhRazwYN9voTDlmExID7Jwz6GXmY826OXqeT4Yl0Egyo1aLYrfT0S73vhC4LVqpdORWLGdZtg==",
+ "optional": true,
+ "dependencies": {
+ "@babel/runtime-corejs3": "^7.20.7",
+ "@swagger-api/apidom-ast": "^0.92.0",
+ "@swagger-api/apidom-core": "^0.92.0",
+ "@swagger-api/apidom-error": "^0.92.0",
+ "@types/ramda": "~0.29.6",
+ "ramda": "~0.29.1",
+ "ramda-adjunct": "^4.1.1",
+ "tree-sitter": "=0.20.4",
+ "tree-sitter-json": "=0.20.1",
+ "web-tree-sitter": "=0.20.3"
+ }
+ },
+ "node_modules/@swagger-api/apidom-parser-adapter-openapi-json-2": {
+ "version": "0.92.0",
+ "resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-openapi-json-2/-/apidom-parser-adapter-openapi-json-2-0.92.0.tgz",
+ "integrity": "sha512-8OlvjcvI/GuOFJJxN+Mc4tJSo9UWuJdzQtQOtO4k3QwWwS28hGvRTjQ5PpsXAVZoLJMAbDuRdREYD9qeIKvM2g==",
+ "optional": true,
+ "dependencies": {
+ "@babel/runtime-corejs3": "^7.20.7",
+ "@swagger-api/apidom-core": "^0.92.0",
+ "@swagger-api/apidom-ns-openapi-2": "^0.92.0",
+ "@swagger-api/apidom-parser-adapter-json": "^0.92.0",
+ "@types/ramda": "~0.29.6",
+ "ramda": "~0.29.1",
+ "ramda-adjunct": "^4.0.0"
+ }
+ },
+ "node_modules/@swagger-api/apidom-parser-adapter-openapi-json-3-0": {
+ "version": "0.92.0",
+ "resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-openapi-json-3-0/-/apidom-parser-adapter-openapi-json-3-0-0.92.0.tgz",
+ "integrity": "sha512-kzE4COaNobKIUjGsdqqXgO/LruaQHs2kTzOzHPUTR1TH1ZlB2t8MTV+6LJzGNG3IB3QSfZDd7KBEYWklsCTyTA==",
+ "optional": true,
+ "dependencies": {
+ "@babel/runtime-corejs3": "^7.20.7",
+ "@swagger-api/apidom-core": "^0.92.0",
+ "@swagger-api/apidom-ns-openapi-3-0": "^0.92.0",
+ "@swagger-api/apidom-parser-adapter-json": "^0.92.0",
+ "@types/ramda": "~0.29.6",
+ "ramda": "~0.29.1",
+ "ramda-adjunct": "^4.0.0"
+ }
+ },
+ "node_modules/@swagger-api/apidom-parser-adapter-openapi-json-3-1": {
+ "version": "0.92.0",
+ "resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-openapi-json-3-1/-/apidom-parser-adapter-openapi-json-3-1-0.92.0.tgz",
+ "integrity": "sha512-4gkIXfKGwEKZQ6+kxp4EdFBlAc7Kjq8GAgaC7ilGTSSxIaz5hBHBOJoe3cXWpQ/WlXiOyNCy7WdbuKRpUDKIdg==",
+ "optional": true,
+ "dependencies": {
+ "@babel/runtime-corejs3": "^7.20.7",
+ "@swagger-api/apidom-core": "^0.92.0",
+ "@swagger-api/apidom-ns-openapi-3-1": "^0.92.0",
+ "@swagger-api/apidom-parser-adapter-json": "^0.92.0",
+ "@types/ramda": "~0.29.6",
+ "ramda": "~0.29.1",
+ "ramda-adjunct": "^4.0.0"
+ }
+ },
+ "node_modules/@swagger-api/apidom-parser-adapter-openapi-yaml-2": {
+ "version": "0.92.0",
+ "resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-openapi-yaml-2/-/apidom-parser-adapter-openapi-yaml-2-0.92.0.tgz",
+ "integrity": "sha512-TIY9cytYhA3yUf+5PcwsH9UjzKy5V4nGUtK6n5RvcL4btaGQA2LUB5CiV/1nSvYLNjYjGxhtB3haZDbHe3/gyw==",
+ "optional": true,
+ "dependencies": {
+ "@babel/runtime-corejs3": "^7.20.7",
+ "@swagger-api/apidom-core": "^0.92.0",
+ "@swagger-api/apidom-ns-openapi-2": "^0.92.0",
+ "@swagger-api/apidom-parser-adapter-yaml-1-2": "^0.92.0",
+ "@types/ramda": "~0.29.6",
+ "ramda": "~0.29.1",
+ "ramda-adjunct": "^4.0.0"
+ }
+ },
+ "node_modules/@swagger-api/apidom-parser-adapter-openapi-yaml-3-0": {
+ "version": "0.92.0",
+ "resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-openapi-yaml-3-0/-/apidom-parser-adapter-openapi-yaml-3-0-0.92.0.tgz",
+ "integrity": "sha512-AUwtAxeautYtiwifNCmv6Kjs7ksptRFxcQ3sgLv2bP3f9t5jzcI9NhmgJNdbRfohHYaHMwTuUESrfsTdBgKlAA==",
+ "optional": true,
+ "dependencies": {
+ "@babel/runtime-corejs3": "^7.20.7",
+ "@swagger-api/apidom-core": "^0.92.0",
+ "@swagger-api/apidom-ns-openapi-3-0": "^0.92.0",
+ "@swagger-api/apidom-parser-adapter-yaml-1-2": "^0.92.0",
+ "@types/ramda": "~0.29.6",
+ "ramda": "~0.29.1",
+ "ramda-adjunct": "^4.0.0"
+ }
+ },
+ "node_modules/@swagger-api/apidom-parser-adapter-openapi-yaml-3-1": {
+ "version": "0.92.0",
+ "resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-openapi-yaml-3-1/-/apidom-parser-adapter-openapi-yaml-3-1-0.92.0.tgz",
+ "integrity": "sha512-gMR4zUZ/RrjVJVr6DnqwsCsnlplGXJk6O9UKbkoBsiom81dkcHx68BmWA2oM2lYVGKx+G8WVmVDo2EJaZvZYGg==",
+ "optional": true,
+ "dependencies": {
+ "@babel/runtime-corejs3": "^7.20.7",
+ "@swagger-api/apidom-core": "^0.92.0",
+ "@swagger-api/apidom-ns-openapi-3-1": "^0.92.0",
+ "@swagger-api/apidom-parser-adapter-yaml-1-2": "^0.92.0",
+ "@types/ramda": "~0.29.6",
+ "ramda": "~0.29.1",
+ "ramda-adjunct": "^4.0.0"
+ }
+ },
+ "node_modules/@swagger-api/apidom-parser-adapter-workflows-json-1": {
+ "version": "0.92.0",
+ "resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-workflows-json-1/-/apidom-parser-adapter-workflows-json-1-0.92.0.tgz",
+ "integrity": "sha512-tyLiSxEKeU6mhClFjNxrTQJA2aSgfEF7LJ/ZcJgvREsvyk6ns3op9wN2SXw4UmD+657IgN0aUPihh92aEXKovA==",
+ "optional": true,
+ "dependencies": {
+ "@babel/runtime-corejs3": "^7.20.7",
+ "@swagger-api/apidom-core": "^0.92.0",
+ "@swagger-api/apidom-ns-workflows-1": "^0.92.0",
+ "@swagger-api/apidom-parser-adapter-json": "^0.92.0",
+ "@types/ramda": "~0.29.6",
+ "ramda": "~0.29.1",
+ "ramda-adjunct": "^4.0.0"
+ }
+ },
+ "node_modules/@swagger-api/apidom-parser-adapter-workflows-yaml-1": {
+ "version": "0.92.0",
+ "resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-workflows-yaml-1/-/apidom-parser-adapter-workflows-yaml-1-0.92.0.tgz",
+ "integrity": "sha512-0Nr+5oAocuw3SZXcO8WEqnU7GGWP7O6GrsFafD6KLBL05v3I0erPfmnWQjWh6jBeXv8r5W69WEQItzES0DBJjA==",
+ "optional": true,
+ "dependencies": {
+ "@babel/runtime-corejs3": "^7.20.7",
+ "@swagger-api/apidom-core": "^0.92.0",
+ "@swagger-api/apidom-ns-workflows-1": "^0.92.0",
+ "@swagger-api/apidom-parser-adapter-yaml-1-2": "^0.92.0",
+ "@types/ramda": "~0.29.6",
+ "ramda": "~0.29.1",
+ "ramda-adjunct": "^4.0.0"
+ }
+ },
+ "node_modules/@swagger-api/apidom-parser-adapter-yaml-1-2": {
+ "version": "0.92.0",
+ "resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-yaml-1-2/-/apidom-parser-adapter-yaml-1-2-0.92.0.tgz",
+ "integrity": "sha512-cFLqlhehMuY5WRdU1780Vno6iWpjMlr7CfOOloZW1rKf2lvojn0c4eDsyfWFaB2DgE+Xd4CWl55McuaPZMngsw==",
+ "optional": true,
+ "dependencies": {
+ "@babel/runtime-corejs3": "^7.20.7",
+ "@swagger-api/apidom-ast": "^0.92.0",
+ "@swagger-api/apidom-core": "^0.92.0",
+ "@swagger-api/apidom-error": "^0.92.0",
+ "@types/ramda": "~0.29.6",
+ "ramda": "~0.29.1",
+ "ramda-adjunct": "^4.1.1",
+ "tree-sitter": "=0.20.4",
+ "tree-sitter-yaml": "=0.5.0",
+ "web-tree-sitter": "=0.20.3"
+ }
+ },
+ "node_modules/@swagger-api/apidom-reference": {
+ "version": "0.92.0",
+ "resolved": "https://registry.npmjs.org/@swagger-api/apidom-reference/-/apidom-reference-0.92.0.tgz",
+ "integrity": "sha512-G/qJBTpXCdwPsc5dqPjX+vAfhvtnhIFqnKtEZ71wnEvF7TpIxdeZKKfqpg+Zxi7MSuZD/Gpkr4J/eP0lO0fAdA==",
+ "dependencies": {
+ "@babel/runtime-corejs3": "^7.20.7",
+ "@swagger-api/apidom-core": "^0.92.0",
+ "@types/ramda": "~0.29.6",
+ "axios": "^1.4.0",
+ "minimatch": "^7.4.3",
+ "process": "^0.11.10",
+ "ramda": "~0.29.1",
+ "ramda-adjunct": "^4.1.1",
+ "stampit": "^4.3.2"
+ },
+ "optionalDependencies": {
+ "@swagger-api/apidom-error": "^0.92.0",
+ "@swagger-api/apidom-json-pointer": "^0.92.0",
+ "@swagger-api/apidom-ns-asyncapi-2": "^0.92.0",
+ "@swagger-api/apidom-ns-openapi-2": "^0.92.0",
+ "@swagger-api/apidom-ns-openapi-3-0": "^0.92.0",
+ "@swagger-api/apidom-ns-openapi-3-1": "^0.92.0",
+ "@swagger-api/apidom-ns-workflows-1": "^0.92.0",
+ "@swagger-api/apidom-parser-adapter-api-design-systems-json": "^0.92.0",
+ "@swagger-api/apidom-parser-adapter-api-design-systems-yaml": "^0.92.0",
+ "@swagger-api/apidom-parser-adapter-asyncapi-json-2": "^0.92.0",
+ "@swagger-api/apidom-parser-adapter-asyncapi-yaml-2": "^0.92.0",
+ "@swagger-api/apidom-parser-adapter-json": "^0.92.0",
+ "@swagger-api/apidom-parser-adapter-openapi-json-2": "^0.92.0",
+ "@swagger-api/apidom-parser-adapter-openapi-json-3-0": "^0.92.0",
+ "@swagger-api/apidom-parser-adapter-openapi-json-3-1": "^0.92.0",
+ "@swagger-api/apidom-parser-adapter-openapi-yaml-2": "^0.92.0",
+ "@swagger-api/apidom-parser-adapter-openapi-yaml-3-0": "^0.92.0",
+ "@swagger-api/apidom-parser-adapter-openapi-yaml-3-1": "^0.92.0",
+ "@swagger-api/apidom-parser-adapter-workflows-json-1": "^0.92.0",
+ "@swagger-api/apidom-parser-adapter-workflows-yaml-1": "^0.92.0",
+ "@swagger-api/apidom-parser-adapter-yaml-1-2": "^0.92.0"
+ }
+ },
+ "node_modules/@swagger-api/apidom-reference/node_modules/brace-expansion": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
+ "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/@swagger-api/apidom-reference/node_modules/minimatch": {
+ "version": "7.4.6",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-7.4.6.tgz",
+ "integrity": "sha512-sBz8G/YjVniEz6lKPNpKxXwazJe4c19fEfV2GDMX6AjFz+MX9uDWIZW8XreVhkFW3fkIdTv/gxWr/Kks5FFAVw==",
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
"node_modules/@swc/helpers": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.2.tgz",
@@ -6392,6 +6901,31 @@
"node": ">=18.0.0"
}
},
+ "node_modules/@ts-rest/core": {
+ "version": "3.30.5",
+ "resolved": "https://registry.npmjs.org/@ts-rest/core/-/core-3.30.5.tgz",
+ "integrity": "sha512-j2sgvk3x8wZiCyhB3ij0I287FgkngCGRHXFBxQ9HtZ9mxQuIIDfibi1yD/ydNvNif0pA6BDdASGQY1WjfqUC3g==",
+ "peerDependencies": {
+ "zod": "^3.22.3"
+ },
+ "peerDependenciesMeta": {
+ "zod": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@ts-rest/open-api": {
+ "version": "3.33.0",
+ "resolved": "https://registry.npmjs.org/@ts-rest/open-api/-/open-api-3.33.0.tgz",
+ "integrity": "sha512-ZUhOWy7oIo9D53W4/DuJuum6RtwSrcxr7VrNTKOeUlq+uvx8yzW/cxaZEh/SrHnzXhOegWj+lWRCH32MS/CaNA==",
+ "dependencies": {
+ "@anatine/zod-openapi": "^1.12.0",
+ "openapi3-ts": "^2.0.2"
+ },
+ "peerDependencies": {
+ "zod": "^3.22.3"
+ }
+ },
"node_modules/@tsconfig/node10": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz",
@@ -6615,14 +7149,20 @@
"node_modules/@types/prop-types": {
"version": "15.7.11",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz",
- "integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==",
- "devOptional": true
+ "integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng=="
+ },
+ "node_modules/@types/ramda": {
+ "version": "0.29.9",
+ "resolved": "https://registry.npmjs.org/@types/ramda/-/ramda-0.29.9.tgz",
+ "integrity": "sha512-X3yEG6tQCWBcUAql+RPC/O1Hm9BSU+MXu2wJnCETuAgUlrEDwTA1kIOdEEE4YXDtf0zfQLHa9CCE7WYp9kqPIQ==",
+ "dependencies": {
+ "types-ramda": "^0.29.6"
+ }
},
"node_modules/@types/react": {
"version": "18.2.18",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.18.tgz",
"integrity": "sha512-da4NTSeBv/P34xoZPhtcLkmZuJ+oYaCxHmyHzwaDQo9RQPBeXV+06gEk2FpqEcsX9XrnNLvRpVh6bdavDSjtiQ==",
- "devOptional": true,
"dependencies": {
"@types/prop-types": "*",
"@types/scheduler": "*",
@@ -6646,14 +7186,21 @@
"node_modules/@types/scheduler": {
"version": "0.16.8",
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz",
- "integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==",
- "devOptional": true
+ "integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A=="
},
"node_modules/@types/semver": {
"version": "7.5.6",
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.6.tgz",
"integrity": "sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A=="
},
+ "node_modules/@types/swagger-ui-react": {
+ "version": "4.18.3",
+ "resolved": "https://registry.npmjs.org/@types/swagger-ui-react/-/swagger-ui-react-4.18.3.tgz",
+ "integrity": "sha512-Mo/R7IjDVwtiFPs84pWvh5pI9iyNGBjmfielxqbOh2Jv+8WVSDVe8Nu25kb5BOuV2xmGS3o33jr6nwDJMBcX+Q==",
+ "dependencies": {
+ "@types/react": "*"
+ }
+ },
"node_modules/@types/ua-parser-js": {
"version": "0.7.39",
"resolved": "https://registry.npmjs.org/@types/ua-parser-js/-/ua-parser-js-0.7.39.tgz",
@@ -6665,6 +7212,11 @@
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.10.tgz",
"integrity": "sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA=="
},
+ "node_modules/@types/use-sync-external-store": {
+ "version": "0.0.3",
+ "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz",
+ "integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA=="
+ },
"node_modules/@types/ws": {
"version": "8.5.10",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.10.tgz",
@@ -6872,6 +7424,11 @@
"resolved": "https://registry.npmjs.org/@vvo/tzdb/-/tzdb-6.117.0.tgz",
"integrity": "sha512-vZkfoag1kHqItK/zebxT0Fkt3R/zscjgD+Ib7kaAdum0Sz9psXDfVHPW1Benv91d02zPWlLIvZtjBmzX4a+6fw=="
},
+ "node_modules/@yarnpkg/lockfile": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz",
+ "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ=="
+ },
"node_modules/abbrev": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
@@ -7246,6 +7803,14 @@
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
+ "node_modules/at-least-node": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz",
+ "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==",
+ "engines": {
+ "node": ">= 4.0.0"
+ }
+ },
"node_modules/attr-accept": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.2.tgz",
@@ -7254,6 +7819,14 @@
"node": ">=4"
}
},
+ "node_modules/autolinker": {
+ "version": "3.16.2",
+ "resolved": "https://registry.npmjs.org/autolinker/-/autolinker-3.16.2.tgz",
+ "integrity": "sha512-JiYl7j2Z19F9NdTmirENSUUIIL/9MytEWtmzhfmsKPCp9E+G35Y0UNCMoM9tFigxT59qSc8Ml2dlZXOCVTYwuA==",
+ "dependencies": {
+ "tslib": "^2.3.0"
+ }
+ },
"node_modules/autoprefixer": {
"version": "10.4.16",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.16.tgz",
@@ -7813,6 +8386,20 @@
"node": ">=10"
}
},
+ "node_modules/ci-info": {
+ "version": "3.9.0",
+ "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz",
+ "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/sibiraj-s"
+ }
+ ],
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/class-variance-authority": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.6.1.tgz",
@@ -7824,6 +8411,11 @@
"url": "https://joebell.co.uk"
}
},
+ "node_modules/classnames": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz",
+ "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow=="
+ },
"node_modules/cli-cursor": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz",
@@ -8472,6 +9064,24 @@
"url": "https://github.com/sponsors/mesqueeb"
}
},
+ "node_modules/copy-to-clipboard": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz",
+ "integrity": "sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==",
+ "dependencies": {
+ "toggle-selection": "^1.0.6"
+ }
+ },
+ "node_modules/core-js-pure": {
+ "version": "3.35.0",
+ "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.35.0.tgz",
+ "integrity": "sha512-f+eRYmkou59uh7BPcyJ8MC76DiGhspj1KMxVIcF24tzP8NA9HVa1uC7BTW2tgx7E1QVCzDzsgp7kArrzhlz8Ew==",
+ "hasInstallScript": true,
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/core-js"
+ }
+ },
"node_modules/core-util-is": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
@@ -8542,6 +9152,11 @@
"resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz",
"integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q=="
},
+ "node_modules/css.escape": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz",
+ "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg=="
+ },
"node_modules/cssesc": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
@@ -8556,8 +9171,7 @@
"node_modules/csstype": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz",
- "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==",
- "devOptional": true
+ "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ=="
},
"node_modules/d3-array": {
"version": "3.2.4",
@@ -9052,6 +9666,11 @@
"url": "https://github.com/fb55/domhandler?sponsor=1"
}
},
+ "node_modules/dompurify": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.0.6.tgz",
+ "integrity": "sha512-ilkD8YEnnGh1zJ240uJsW7AzE+2qpbOUYjacomn3AvJ6J4JhKGSZ2nh4wUIXPZrEPppaCLx5jFe8T89Rk8tQ7w=="
+ },
"node_modules/domutils": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz",
@@ -9110,6 +9729,14 @@
"node": ">=12"
}
},
+ "node_modules/drange": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/drange/-/drange-1.1.1.tgz",
+ "integrity": "sha512-pYxfDYpued//QpnLIm4Avk7rsNtAtQkUES2cwAYSvD/wd2pKD71gN2Ebj3e7klzXwjocvE8c5vx/1fxwpqmSxA==",
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/duplexer": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz",
@@ -10486,6 +11113,11 @@
"node": ">=8.6.0"
}
},
+ "node_modules/fast-json-patch": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/fast-json-patch/-/fast-json-patch-3.1.1.tgz",
+ "integrity": "sha512-vf6IHUX2SBcA+5/+4883dsIjpBTqmfBjmYiWK1savxQmFk4JfBMLa7ynTYOs1Rolp/T1betJxHiGD3g1Mn8lUQ=="
+ },
"node_modules/fast-json-stable-stringify": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
@@ -10617,6 +11249,14 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/find-yarn-workspace-root": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/find-yarn-workspace-root/-/find-yarn-workspace-root-2.0.0.tgz",
+ "integrity": "sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ==",
+ "dependencies": {
+ "micromatch": "^4.0.2"
+ }
+ },
"node_modules/flat-cache": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz",
@@ -11485,6 +12125,14 @@
"node": ">=8"
}
},
+ "node_modules/highlight.js": {
+ "version": "10.7.3",
+ "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz",
+ "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==",
+ "engines": {
+ "node": "*"
+ }
+ },
"node_modules/hosted-git-info": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz",
@@ -11676,6 +12324,14 @@
"node": ">=14.0.0"
}
},
+ "node_modules/immutable": {
+ "version": "3.8.2",
+ "resolved": "https://registry.npmjs.org/immutable/-/immutable-3.8.2.tgz",
+ "integrity": "sha512-15gZoQ38eYjEjxkorfbcgBKBL6R7T459OuK+CpcWt7O3KF4uPCx2tD0uFETlUDIyo+1789crbMhTvQBSR5yBMg==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/import-fresh": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
@@ -11943,6 +12599,20 @@
"url": "https://github.com/sponsors/wooorm"
}
},
+ "node_modules/is-docker": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz",
+ "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==",
+ "bin": {
+ "is-docker": "cli.js"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/is-extendable": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz",
@@ -12267,6 +12937,17 @@
"node": ">=0.10.0"
}
},
+ "node_modules/is-wsl": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
+ "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==",
+ "dependencies": {
+ "is-docker": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/isarray": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
@@ -12445,6 +13126,11 @@
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
}
},
+ "node_modules/js-file-download": {
+ "version": "0.4.12",
+ "resolved": "https://registry.npmjs.org/js-file-download/-/js-file-download-0.4.12.tgz",
+ "integrity": "sha512-rML+NkoD08p5Dllpjo0ffy4jRHeY6Zsapvr/W86N7E0yuzAO6qa5X9+xog6zQNlH102J7IXljNY2FtS6Lj3ucg=="
+ },
"node_modules/js-sdsl": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.3.0.tgz",
@@ -12502,6 +13188,23 @@
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
"dev": true
},
+ "node_modules/json-stable-stringify": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.1.1.tgz",
+ "integrity": "sha512-SU/971Kt5qVQfJpyDveVhQ/vya+5hvrjClFOcr8c0Fq5aODJjMwutrOfCU+eCnVD5gpx1Q3fEqkyom77zH1iIg==",
+ "dependencies": {
+ "call-bind": "^1.0.5",
+ "isarray": "^2.0.5",
+ "jsonify": "^0.0.1",
+ "object-keys": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/json-stable-stringify-without-jsonify": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
@@ -12529,6 +13232,14 @@
"graceful-fs": "^4.1.6"
}
},
+ "node_modules/jsonify": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.1.tgz",
+ "integrity": "sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg==",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/jsonparse": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz",
@@ -12584,6 +13295,14 @@
"node": ">=0.10.0"
}
},
+ "node_modules/klaw-sync": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/klaw-sync/-/klaw-sync-6.0.0.tgz",
+ "integrity": "sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ==",
+ "dependencies": {
+ "graceful-fs": "^4.1.11"
+ }
+ },
"node_modules/kleur": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz",
@@ -12884,6 +13603,11 @@
"integrity": "sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==",
"dev": true
},
+ "node_modules/lodash.debounce": {
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
+ "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow=="
+ },
"node_modules/lodash.isfunction": {
"version": "3.0.9",
"resolved": "https://registry.npmjs.org/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz",
@@ -13037,6 +13761,31 @@
"tslib": "^2.0.3"
}
},
+ "node_modules/lowlight": {
+ "version": "1.20.0",
+ "resolved": "https://registry.npmjs.org/lowlight/-/lowlight-1.20.0.tgz",
+ "integrity": "sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==",
+ "dependencies": {
+ "fault": "^1.0.0",
+ "highlight.js": "~10.7.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/lowlight/node_modules/fault": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/fault/-/fault-1.0.4.tgz",
+ "integrity": "sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==",
+ "dependencies": {
+ "format": "^0.2.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
@@ -14077,6 +14826,17 @@
"node": ">=4"
}
},
+ "node_modules/minim": {
+ "version": "0.23.8",
+ "resolved": "https://registry.npmjs.org/minim/-/minim-0.23.8.tgz",
+ "integrity": "sha512-bjdr2xW1dBCMsMGGsUeqM4eFI60m94+szhxWys+B1ztIt6gWSfeGBdSVCIawezeHYLYn0j6zrsXdQS/JllBzww==",
+ "dependencies": {
+ "lodash": "^4.15.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
@@ -14439,6 +15199,11 @@
"tslib": "^2.0.3"
}
},
+ "node_modules/node-abort-controller": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz",
+ "integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ=="
+ },
"node_modules/node-addon-api": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz",
@@ -14479,6 +15244,22 @@
"url": "https://opencollective.com/node-fetch"
}
},
+ "node_modules/node-fetch-commonjs": {
+ "version": "3.3.2",
+ "resolved": "https://registry.npmjs.org/node-fetch-commonjs/-/node-fetch-commonjs-3.3.2.tgz",
+ "integrity": "sha512-VBlAiynj3VMLrotgwOS3OyECFxas5y7ltLcK4t41lMUZeaK15Ym4QRkqN0EQKAFL42q9i21EPKjzLUPfltR72A==",
+ "dependencies": {
+ "node-domexception": "^1.0.0",
+ "web-streams-polyfill": "^3.0.3"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/node-fetch"
+ }
+ },
"node_modules/node-forge": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz",
@@ -14760,6 +15541,37 @@
"node": ">= 14.17.0"
}
},
+ "node_modules/open": {
+ "version": "7.4.2",
+ "resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz",
+ "integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==",
+ "dependencies": {
+ "is-docker": "^2.0.0",
+ "is-wsl": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/openapi3-ts": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/openapi3-ts/-/openapi3-ts-2.0.2.tgz",
+ "integrity": "sha512-TxhYBMoqx9frXyOgnRHufjQfPXomTIHYKhSKJ6jHfj13kS8OEIhvmE8CTuQyKtjjWttAjX5DPxM1vmalEpo8Qw==",
+ "dependencies": {
+ "yaml": "^1.10.2"
+ }
+ },
+ "node_modules/openapi3-ts/node_modules/yaml": {
+ "version": "1.10.2",
+ "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
+ "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
"node_modules/openid-client": {
"version": "5.6.1",
"resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.6.1.tgz",
@@ -14849,6 +15661,14 @@
"node": ">=8"
}
},
+ "node_modules/os-tmpdir": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
+ "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/oslo": {
"version": "0.17.0",
"resolved": "https://registry.npmjs.org/oslo/-/oslo-0.17.0.tgz",
@@ -14983,6 +15803,68 @@
"tslib": "^2.0.3"
}
},
+ "node_modules/patch-package": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/patch-package/-/patch-package-8.0.0.tgz",
+ "integrity": "sha512-da8BVIhzjtgScwDJ2TtKsfT5JFWz1hYoBl9rUQ1f38MC2HwnEIkK8VN3dKMKcP7P7bvvgzNDbfNHtx3MsQb5vA==",
+ "dependencies": {
+ "@yarnpkg/lockfile": "^1.1.0",
+ "chalk": "^4.1.2",
+ "ci-info": "^3.7.0",
+ "cross-spawn": "^7.0.3",
+ "find-yarn-workspace-root": "^2.0.0",
+ "fs-extra": "^9.0.0",
+ "json-stable-stringify": "^1.0.2",
+ "klaw-sync": "^6.0.0",
+ "minimist": "^1.2.6",
+ "open": "^7.4.2",
+ "rimraf": "^2.6.3",
+ "semver": "^7.5.3",
+ "slash": "^2.0.0",
+ "tmp": "^0.0.33",
+ "yaml": "^2.2.2"
+ },
+ "bin": {
+ "patch-package": "index.js"
+ },
+ "engines": {
+ "node": ">=14",
+ "npm": ">5"
+ }
+ },
+ "node_modules/patch-package/node_modules/fs-extra": {
+ "version": "9.1.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
+ "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==",
+ "dependencies": {
+ "at-least-node": "^1.0.0",
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/patch-package/node_modules/rimraf": {
+ "version": "2.7.1",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
+ "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
+ "dependencies": {
+ "glob": "^7.1.3"
+ },
+ "bin": {
+ "rimraf": "bin.js"
+ }
+ },
+ "node_modules/patch-package/node_modules/slash": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz",
+ "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@@ -15557,6 +16439,14 @@
"node": ">=16.13"
}
},
+ "node_modules/prismjs": {
+ "version": "1.29.0",
+ "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz",
+ "integrity": "sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/process": {
"version": "0.11.10",
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
@@ -15667,6 +16557,11 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/querystringify": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
+ "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ=="
+ },
"node_modules/queue-microtask": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
@@ -15700,6 +16595,30 @@
"resolved": "https://registry.npmjs.org/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz",
"integrity": "sha512-cz93DjNeLY0idrCNOH6PviZGRN9GJhsdm9hpn1YCS879fj4W+x5IFJhhkRZcwVgMmFF7R82UA/7Oh+R8lLZg6A=="
},
+ "node_modules/ramda": {
+ "version": "0.29.1",
+ "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.29.1.tgz",
+ "integrity": "sha512-OfxIeWzd4xdUNxlWhgFazxsA/nl3mS4/jGZI5n00uWOoSSFRhC1b6gl6xvmzUamgmqELraWp0J/qqVlXYPDPyA==",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/ramda"
+ }
+ },
+ "node_modules/ramda-adjunct": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/ramda-adjunct/-/ramda-adjunct-4.1.1.tgz",
+ "integrity": "sha512-BnCGsZybQZMDGram9y7RiryoRHS5uwx8YeGuUeDKuZuvK38XO6JJfmK85BwRWAKFA6pZ5nZBO/HBFtExVaf31w==",
+ "engines": {
+ "node": ">=0.10.3"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/ramda-adjunct"
+ },
+ "peerDependencies": {
+ "ramda": ">= 0.29.0"
+ }
+ },
"node_modules/randexp": {
"version": "0.4.6",
"resolved": "https://registry.npmjs.org/randexp/-/randexp-0.4.6.tgz",
@@ -15712,6 +16631,14 @@
"node": ">=0.12"
}
},
+ "node_modules/randombytes": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
+ "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
+ "dependencies": {
+ "safe-buffer": "^5.1.0"
+ }
+ },
"node_modules/raw-body": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.1.tgz",
@@ -15749,6 +16676,15 @@
"node": ">=0.10.0"
}
},
+ "node_modules/react-colorful": {
+ "version": "5.6.1",
+ "resolved": "https://registry.npmjs.org/react-colorful/-/react-colorful-5.6.1.tgz",
+ "integrity": "sha512-1exovf0uGTGyq5mXQT0zgQ80uvj2PCwvF8zY1RN9/vbJVSjSo3fsB/4L3ObbF7u70NduSiK4xu4Y6q1MHoUGEw==",
+ "peerDependencies": {
+ "react": ">=16.8.0",
+ "react-dom": ">=16.8.0"
+ }
+ },
"node_modules/react-confetti": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/react-confetti/-/react-confetti-6.1.0.tgz",
@@ -15763,6 +16699,18 @@
"react": "^16.3.0 || ^17.0.1 || ^18.0.0"
}
},
+ "node_modules/react-copy-to-clipboard": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/react-copy-to-clipboard/-/react-copy-to-clipboard-5.1.0.tgz",
+ "integrity": "sha512-k61RsNgAayIJNoy9yDsYzDe/yAZAzEbEgcz3DZMhF686LEyukcE1hzurxe85JandPUG+yTfGVFzuEw3xt8WP/A==",
+ "dependencies": {
+ "copy-to-clipboard": "^3.3.1",
+ "prop-types": "^15.8.1"
+ },
+ "peerDependencies": {
+ "react": "^15.3.0 || 16 || 17 || 18"
+ }
+ },
"node_modules/react-day-picker": {
"version": "8.9.1",
"resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-8.9.1.tgz",
@@ -15776,6 +16724,18 @@
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
},
+ "node_modules/react-debounce-input": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/react-debounce-input/-/react-debounce-input-3.3.0.tgz",
+ "integrity": "sha512-VEqkvs8JvY/IIZvh71Z0TC+mdbxERvYF33RcebnodlsUZ8RSgyKe2VWaHXv4+/8aoOgXLxWrdsYs2hDhcwbUgA==",
+ "dependencies": {
+ "lodash.debounce": "^4",
+ "prop-types": "^15.8.1"
+ },
+ "peerDependencies": {
+ "react": "^15.3.0 || 16 || 17 || 18"
+ }
+ },
"node_modules/react-dom": {
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz",
@@ -16054,6 +17014,35 @@
"react": "*"
}
},
+ "node_modules/react-immutable-proptypes": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/react-immutable-proptypes/-/react-immutable-proptypes-2.2.0.tgz",
+ "integrity": "sha512-Vf4gBsePlwdGvSZoLSBfd4HAP93HDauMY4fDjXhreg/vg6F3Fj/MXDNyTbltPC/xZKmZc+cjLu3598DdYK6sgQ==",
+ "dependencies": {
+ "invariant": "^2.2.2"
+ },
+ "peerDependencies": {
+ "immutable": ">=3.6.2"
+ }
+ },
+ "node_modules/react-immutable-pure-component": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/react-immutable-pure-component/-/react-immutable-pure-component-2.2.2.tgz",
+ "integrity": "sha512-vkgoMJUDqHZfXXnjVlG3keCxSO/U6WeDQ5/Sl0GK2cH8TOxEzQ5jXqDXHEL/jqk6fsNxV05oH5kD7VNMUE2k+A==",
+ "peerDependencies": {
+ "immutable": ">= 2 || >= 4.0.0-rc",
+ "react": ">= 16.6",
+ "react-dom": ">= 16.6"
+ }
+ },
+ "node_modules/react-inspector": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/react-inspector/-/react-inspector-6.0.2.tgz",
+ "integrity": "sha512-x+b7LxhmHXjHoU/VrFAzw5iutsILRoYyDq97EDYdFpPLcvqtEzk4ZSZSQjnFPbr5T57tLXnHcqFYoN1pI6u8uQ==",
+ "peerDependencies": {
+ "react": "^16.8.4 || ^17.0.0 || ^18.0.0"
+ }
+ },
"node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
@@ -16213,6 +17202,21 @@
}
}
},
+ "node_modules/react-syntax-highlighter": {
+ "version": "15.5.0",
+ "resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-15.5.0.tgz",
+ "integrity": "sha512-+zq2myprEnQmH5yw6Gqc8lD55QHnpKaU8TOcFeC/Lg/MQSs8UknEA0JC4nTZGFAXC2J2Hyj/ijJ7NlabyPi2gg==",
+ "dependencies": {
+ "@babel/runtime": "^7.3.1",
+ "highlight.js": "^10.4.1",
+ "lowlight": "^1.17.0",
+ "prismjs": "^1.27.0",
+ "refractor": "^3.6.0"
+ },
+ "peerDependencies": {
+ "react": ">= 0.14.0"
+ }
+ },
"node_modules/react-transition-group": {
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-2.9.0.tgz",
@@ -16494,6 +17498,19 @@
"node": ">=8"
}
},
+ "node_modules/redux": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
+ "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w=="
+ },
+ "node_modules/redux-immutable": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/redux-immutable/-/redux-immutable-4.0.0.tgz",
+ "integrity": "sha512-SchSn/DWfGb3oAejd+1hhHx01xUoxY+V7TeK0BKqpkLKiQPVFf7DYzEaKmrEVxsWxielKfSK9/Xq66YyxgR1cg==",
+ "peerDependencies": {
+ "immutable": "^3.8.1 || ^4.0.0-rc.1"
+ }
+ },
"node_modules/reflect.getprototypeof": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.4.tgz",
@@ -16513,6 +17530,167 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/refractor": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/refractor/-/refractor-3.6.0.tgz",
+ "integrity": "sha512-MY9W41IOWxxk31o+YvFCNyNzdkc9M20NoZK5vq6jkv4I/uh2zkWcfudj0Q1fovjUQJrNewS9NMzeTtqPf+n5EA==",
+ "dependencies": {
+ "hastscript": "^6.0.0",
+ "parse-entities": "^2.0.0",
+ "prismjs": "~1.27.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/refractor/node_modules/character-entities": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-1.2.4.tgz",
+ "integrity": "sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/refractor/node_modules/character-entities-legacy": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz",
+ "integrity": "sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/refractor/node_modules/character-reference-invalid": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz",
+ "integrity": "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/refractor/node_modules/comma-separated-tokens": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz",
+ "integrity": "sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw==",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/refractor/node_modules/hast-util-parse-selector": {
+ "version": "2.2.5",
+ "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-2.2.5.tgz",
+ "integrity": "sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ==",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/refractor/node_modules/hastscript": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-6.0.0.tgz",
+ "integrity": "sha512-nDM6bvd7lIqDUiYEiu5Sl/+6ReP0BMk/2f4U/Rooccxkj0P5nm+acM5PrGJ/t5I8qPGiqZSE6hVAwZEdZIvP4w==",
+ "dependencies": {
+ "@types/hast": "^2.0.0",
+ "comma-separated-tokens": "^1.0.0",
+ "hast-util-parse-selector": "^2.0.0",
+ "property-information": "^5.0.0",
+ "space-separated-tokens": "^1.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/refractor/node_modules/is-alphabetical": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.4.tgz",
+ "integrity": "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/refractor/node_modules/is-alphanumerical": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz",
+ "integrity": "sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==",
+ "dependencies": {
+ "is-alphabetical": "^1.0.0",
+ "is-decimal": "^1.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/refractor/node_modules/is-decimal": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-1.0.4.tgz",
+ "integrity": "sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/refractor/node_modules/is-hexadecimal": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz",
+ "integrity": "sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/refractor/node_modules/parse-entities": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-2.0.0.tgz",
+ "integrity": "sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==",
+ "dependencies": {
+ "character-entities": "^1.0.0",
+ "character-entities-legacy": "^1.0.0",
+ "character-reference-invalid": "^1.0.0",
+ "is-alphanumerical": "^1.0.0",
+ "is-decimal": "^1.0.0",
+ "is-hexadecimal": "^1.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/refractor/node_modules/prismjs": {
+ "version": "1.27.0",
+ "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.27.0.tgz",
+ "integrity": "sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA==",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/refractor/node_modules/property-information": {
+ "version": "5.6.0",
+ "resolved": "https://registry.npmjs.org/property-information/-/property-information-5.6.0.tgz",
+ "integrity": "sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA==",
+ "dependencies": {
+ "xtend": "^4.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/refractor/node_modules/space-separated-tokens": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz",
+ "integrity": "sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA==",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/regenerator-runtime": {
"version": "0.14.0",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz",
@@ -16633,6 +17811,29 @@
"url": "https://opencollective.com/unified"
}
},
+ "node_modules/remarkable": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/remarkable/-/remarkable-2.0.1.tgz",
+ "integrity": "sha512-YJyMcOH5lrR+kZdmB0aJJ4+93bEojRZ1HGDn9Eagu6ibg7aVZhc3OWbbShRid+Q5eAfsEqWxpe+g5W5nYNfNiA==",
+ "dependencies": {
+ "argparse": "^1.0.10",
+ "autolinker": "^3.11.0"
+ },
+ "bin": {
+ "remarkable": "bin/remarkable.js"
+ },
+ "engines": {
+ "node": ">= 6.0.0"
+ }
+ },
+ "node_modules/remarkable/node_modules/argparse": {
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
+ "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
+ "dependencies": {
+ "sprintf-js": "~1.0.2"
+ }
+ },
"node_modules/remeda": {
"version": "1.29.0",
"resolved": "https://registry.npmjs.org/remeda/-/remeda-1.29.0.tgz",
@@ -16671,6 +17872,16 @@
"node": ">=0.10.5"
}
},
+ "node_modules/requires-port": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
+ "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ=="
+ },
+ "node_modules/reselect": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.0.tgz",
+ "integrity": "sha512-aw7jcGLDpSgNDyWBQLv2cedml85qd95/iszJjN988zX1t7AVRJi19d9kto5+W7oCfQ94gyo40dVbT6g2k4/kXg=="
+ },
"node_modules/resend": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/resend/-/resend-2.0.0.tgz",
@@ -16990,6 +18201,31 @@
"node": ">=10"
}
},
+ "node_modules/serialize-error": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-8.1.0.tgz",
+ "integrity": "sha512-3NnuWfM6vBYoy5gZFvHiYsVbafvI9vZv/+jlIigFn4oP4zjNPK3LhcY0xSCgeb1a5L8jO71Mit9LlNoi2UfDDQ==",
+ "dependencies": {
+ "type-fest": "^0.20.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/serialize-error/node_modules/type-fest": {
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
+ "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/set-blocking": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
@@ -17032,6 +18268,18 @@
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz",
"integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw=="
},
+ "node_modules/sha.js": {
+ "version": "2.4.11",
+ "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz",
+ "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==",
+ "dependencies": {
+ "inherits": "^2.0.1",
+ "safe-buffer": "^5.0.1"
+ },
+ "bin": {
+ "sha.js": "bin.js"
+ }
+ },
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -17067,6 +18315,15 @@
"node": ">=4"
}
},
+ "node_modules/short-unique-id": {
+ "version": "5.0.3",
+ "resolved": "https://registry.npmjs.org/short-unique-id/-/short-unique-id-5.0.3.tgz",
+ "integrity": "sha512-yhniEILouC0s4lpH0h7rJsfylZdca10W9mDJRAFh3EpcSUanCHGb0R7kcFOIUCZYSAPo0PUD5ZxWQdW0T4xaug==",
+ "bin": {
+ "short-unique-id": "bin/short-unique-id",
+ "suid": "bin/short-unique-id"
+ }
+ },
"node_modules/side-channel": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
@@ -17319,6 +18576,11 @@
"sql-formatter": "bin/sql-formatter-cli.cjs"
}
},
+ "node_modules/stampit": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/stampit/-/stampit-4.3.2.tgz",
+ "integrity": "sha512-pE2org1+ZWQBnIxRPrBM2gVupkuDD0TTNIo1H6GdT/vO82NXli2z8lRE8cu/nBIHrcOCXFBAHpb9ZldrB2/qOA=="
+ },
"node_modules/start-server-and-test": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/start-server-and-test/-/start-server-and-test-2.0.3.tgz",
@@ -17749,6 +19011,151 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/swagger-client": {
+ "version": "3.25.0",
+ "resolved": "https://registry.npmjs.org/swagger-client/-/swagger-client-3.25.0.tgz",
+ "integrity": "sha512-p143zWkIhgyh2E5+3HPFMlCw3WkV9RbX9HyftfBdiccCbOlmHdcJC0XEJZxcm+ZA+80DORs0F30/mzk7sx4iwA==",
+ "dependencies": {
+ "@babel/runtime-corejs3": "^7.22.15",
+ "@swagger-api/apidom-core": ">=0.90.0 <1.0.0",
+ "@swagger-api/apidom-error": ">=0.90.0 <1.0.0",
+ "@swagger-api/apidom-json-pointer": ">=0.90.0 <1.0.0",
+ "@swagger-api/apidom-ns-openapi-3-1": ">=0.90.0 <1.0.0",
+ "@swagger-api/apidom-reference": ">=0.90.0 <1.0.0",
+ "cookie": "~0.6.0",
+ "deepmerge": "~4.3.0",
+ "fast-json-patch": "^3.0.0-1",
+ "is-plain-object": "^5.0.0",
+ "js-yaml": "^4.1.0",
+ "node-abort-controller": "^3.1.1",
+ "node-fetch-commonjs": "^3.3.1",
+ "qs": "^6.10.2",
+ "traverse": "~0.6.6",
+ "undici": "^5.24.0"
+ }
+ },
+ "node_modules/swagger-client/node_modules/cookie": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
+ "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/swagger-client/node_modules/traverse": {
+ "version": "0.6.8",
+ "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.6.8.tgz",
+ "integrity": "sha512-aXJDbk6SnumuaZSANd21XAo15ucCDE38H4fkqiGsc3MhCK+wOlZvLP9cB/TvpHT0mOyWgC4Z8EwRlzqYSUzdsA==",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/swagger-ui-react": {
+ "version": "5.11.0",
+ "resolved": "https://registry.npmjs.org/swagger-ui-react/-/swagger-ui-react-5.11.0.tgz",
+ "integrity": "sha512-iqc5/Z8nvqOdjU2LuWYbREnDmKj5gndZSESTH9dXfymlzLc2NoPQmXZAw02U8kFgHyciX0yDMp3oaCw1zBdPSA==",
+ "dependencies": {
+ "@babel/runtime-corejs3": "^7.23.7",
+ "@braintree/sanitize-url": "=7.0.0",
+ "base64-js": "^1.5.1",
+ "classnames": "^2.5.1",
+ "css.escape": "1.5.1",
+ "deep-extend": "0.6.0",
+ "dompurify": "=3.0.6",
+ "ieee754": "^1.2.1",
+ "immutable": "^3.x.x",
+ "js-file-download": "^0.4.12",
+ "js-yaml": "=4.1.0",
+ "lodash": "^4.17.21",
+ "patch-package": "^8.0.0",
+ "prop-types": "^15.8.1",
+ "randexp": "^0.5.3",
+ "randombytes": "^2.1.0",
+ "react-copy-to-clipboard": "5.1.0",
+ "react-debounce-input": "=3.3.0",
+ "react-immutable-proptypes": "2.2.0",
+ "react-immutable-pure-component": "^2.2.0",
+ "react-inspector": "^6.0.1",
+ "react-redux": "^9.0.4",
+ "react-syntax-highlighter": "^15.5.0",
+ "redux": "^5.0.0",
+ "redux-immutable": "^4.0.0",
+ "remarkable": "^2.0.1",
+ "reselect": "^5.0.1",
+ "serialize-error": "^8.1.0",
+ "sha.js": "^2.4.11",
+ "swagger-client": "^3.25.0",
+ "url-parse": "^1.5.10",
+ "xml": "=1.0.1",
+ "xml-but-prettier": "^1.0.1",
+ "zenscroll": "^4.0.2"
+ },
+ "peerDependencies": {
+ "react": ">=16.8.0 <19",
+ "react-dom": ">=16.8.0 <19"
+ }
+ },
+ "node_modules/swagger-ui-react/node_modules/@types/react": {
+ "version": "18.2.48",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.48.tgz",
+ "integrity": "sha512-qboRCl6Ie70DQQG9hhNREz81jqC1cs9EVNcjQ1AU+jH6NFfSAhVVbrrY/+nSF+Bsk4AOwm9Qa61InvMCyV+H3w==",
+ "optional": true,
+ "peer": true,
+ "dependencies": {
+ "@types/prop-types": "*",
+ "@types/scheduler": "*",
+ "csstype": "^3.0.2"
+ }
+ },
+ "node_modules/swagger-ui-react/node_modules/randexp": {
+ "version": "0.5.3",
+ "resolved": "https://registry.npmjs.org/randexp/-/randexp-0.5.3.tgz",
+ "integrity": "sha512-U+5l2KrcMNOUPYvazA3h5ekF80FHTUG+87SEAmHZmolh1M+i/WyTCxVzmi+tidIa1tM4BSe8g2Y/D3loWDjj+w==",
+ "dependencies": {
+ "drange": "^1.0.2",
+ "ret": "^0.2.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/swagger-ui-react/node_modules/react-redux": {
+ "version": "9.1.0",
+ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.1.0.tgz",
+ "integrity": "sha512-6qoDzIO+gbrza8h3hjMA9aq4nwVFCKFtY2iLxCtVT38Swyy2C/dJCGBXHeHLtx6qlg/8qzc2MrhOeduf5K32wQ==",
+ "dependencies": {
+ "@types/use-sync-external-store": "^0.0.3",
+ "use-sync-external-store": "^1.0.0"
+ },
+ "peerDependencies": {
+ "@types/react": "^18.2.25",
+ "react": "^18.0",
+ "react-native": ">=0.69",
+ "redux": "^5.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "react-native": {
+ "optional": true
+ },
+ "redux": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/swagger-ui-react/node_modules/ret": {
+ "version": "0.2.2",
+ "resolved": "https://registry.npmjs.org/ret/-/ret-0.2.2.tgz",
+ "integrity": "sha512-M0b3YWQs7R3Z917WRQy1HHA7Ba7D8hvZg6UE5mLykJxQVE2ju0IXbGlaHPPlkY+WN7wFP+wUMXmBFA0aV6vYGQ==",
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/tailwind-merge": {
"version": "1.14.0",
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-1.14.0.tgz",
@@ -17920,6 +19327,17 @@
"resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz",
"integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA=="
},
+ "node_modules/tmp": {
+ "version": "0.0.33",
+ "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",
+ "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==",
+ "dependencies": {
+ "os-tmpdir": "~1.0.2"
+ },
+ "engines": {
+ "node": ">=0.6.0"
+ }
+ },
"node_modules/to-fast-properties": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
@@ -17939,6 +19357,11 @@
"node": ">=8.0"
}
},
+ "node_modules/toggle-selection": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz",
+ "integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ=="
+ },
"node_modules/toidentifier": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz",
@@ -17996,6 +19419,37 @@
"node": ">= 6"
}
},
+ "node_modules/tree-sitter": {
+ "version": "0.20.4",
+ "resolved": "https://registry.npmjs.org/tree-sitter/-/tree-sitter-0.20.4.tgz",
+ "integrity": "sha512-rjfR5dc4knG3jnJNN/giJ9WOoN1zL/kZyrS0ILh+eqq8RNcIbiXA63JsMEgluug0aNvfQvK4BfCErN1vIzvKog==",
+ "hasInstallScript": true,
+ "optional": true,
+ "dependencies": {
+ "nan": "^2.17.0",
+ "prebuild-install": "^7.1.1"
+ }
+ },
+ "node_modules/tree-sitter-json": {
+ "version": "0.20.1",
+ "resolved": "https://registry.npmjs.org/tree-sitter-json/-/tree-sitter-json-0.20.1.tgz",
+ "integrity": "sha512-482hf7J+aBwhksSw8yWaqI8nyP1DrSwnS4IMBShsnkFWD3SE8oalHnsEik59fEVi3orcTCUtMzSjZx+0Tpa6Vw==",
+ "hasInstallScript": true,
+ "optional": true,
+ "dependencies": {
+ "nan": "^2.18.0"
+ }
+ },
+ "node_modules/tree-sitter-yaml": {
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/tree-sitter-yaml/-/tree-sitter-yaml-0.5.0.tgz",
+ "integrity": "sha512-POJ4ZNXXSWIG/W4Rjuyg36MkUD4d769YRUGKRqN+sVaj/VCo6Dh6Pkssn1Rtewd5kybx+jT1BWMyWN0CijXnMA==",
+ "hasInstallScript": true,
+ "optional": true,
+ "dependencies": {
+ "nan": "^2.14.0"
+ }
+ },
"node_modules/trim-lines": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz",
@@ -18034,6 +19488,14 @@
"typescript": ">=4.2.0"
}
},
+ "node_modules/ts-deepmerge": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/ts-deepmerge/-/ts-deepmerge-6.2.0.tgz",
+ "integrity": "sha512-2qxI/FZVDPbzh63GwWIZYE7daWKtwXZYuyc8YNq0iTmMUwn4mL0jRLsp6hfFlgbdRSR4x2ppe+E86FnvEpN7Nw==",
+ "engines": {
+ "node": ">=14.13.1"
+ }
+ },
"node_modules/ts-interface-checker": {
"version": "0.1.13",
"resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
@@ -18087,6 +19549,11 @@
"resolved": "https://registry.npmjs.org/ts-pattern/-/ts-pattern-5.0.5.tgz",
"integrity": "sha512-tL0w8U/pgaacOmkb9fRlYzWEUDCfVjjv9dD4wHTgZ61MjhuMt46VNWTG747NqW6vRzoWIKABVhFSOJ82FvXrfA=="
},
+ "node_modules/ts-toolbelt": {
+ "version": "9.6.0",
+ "resolved": "https://registry.npmjs.org/ts-toolbelt/-/ts-toolbelt-9.6.0.tgz",
+ "integrity": "sha512-nsZd8ZeNUzukXPlJmTBwUAuABDe/9qtVDelJeT/qW0ow3ZS3BsQJtNkan1802aM9Uf68/Y8ljw86Hu0h5IUW3w=="
+ },
"node_modules/tsconfig-paths": {
"version": "3.14.2",
"resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz",
@@ -18549,6 +20016,14 @@
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
"integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA=="
},
+ "node_modules/types-ramda": {
+ "version": "0.29.6",
+ "resolved": "https://registry.npmjs.org/types-ramda/-/types-ramda-0.29.6.tgz",
+ "integrity": "sha512-VJoOk1uYNh9ZguGd3eZvqkdhD4hTGtnjRBUx5Zc0U9ftmnCgiWcSj/lsahzKunbiwRje1MxxNkEy1UdcXRCpYw==",
+ "dependencies": {
+ "ts-toolbelt": "^9.6.0"
+ }
+ },
"node_modules/typescript": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.2.tgz",
@@ -18597,6 +20072,17 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/undici": {
+ "version": "5.28.2",
+ "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.2.tgz",
+ "integrity": "sha512-wh1pHJHnUeQV5Xa8/kyQhO7WFa8M34l026L5P/+2TYiakvGy5Rdc8jWZVyG7ieht/0WgJLEd3kcU5gKx+6GC8w==",
+ "dependencies": {
+ "@fastify/busboy": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=14.0"
+ }
+ },
"node_modules/unified": {
"version": "10.1.2",
"resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz",
@@ -18744,6 +20230,11 @@
"node": ">= 0.8"
}
},
+ "node_modules/unraw": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/unraw/-/unraw-3.0.0.tgz",
+ "integrity": "sha512-08/DA66UF65OlpUDIQtbJyrqTR0jTAlJ+jsnkQ4jxR7+K5g5YG1APZKQSMCE1vqqmD+2pv6+IdEjmopFatacvg=="
+ },
"node_modules/unzipper": {
"version": "0.10.14",
"resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.14.tgz",
@@ -18840,6 +20331,15 @@
"punycode": "^2.1.0"
}
},
+ "node_modules/url-parse": {
+ "version": "1.5.10",
+ "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz",
+ "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==",
+ "dependencies": {
+ "querystringify": "^2.1.1",
+ "requires-port": "^1.0.0"
+ }
+ },
"node_modules/use-callback-ref": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.0.tgz",
@@ -19059,6 +20559,12 @@
"node": ">= 8"
}
},
+ "node_modules/web-tree-sitter": {
+ "version": "0.20.3",
+ "resolved": "https://registry.npmjs.org/web-tree-sitter/-/web-tree-sitter-0.20.3.tgz",
+ "integrity": "sha512-zKGJW9r23y3BcJusbgvnOH2OYAW40MXAOi9bi3Gcc7T4Gms9WWgXF8m6adsJWpGJEhgOzCrfiz1IzKowJWrtYw==",
+ "optional": true
+ },
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
@@ -19321,6 +20827,19 @@
}
}
},
+ "node_modules/xml": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz",
+ "integrity": "sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw=="
+ },
+ "node_modules/xml-but-prettier": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/xml-but-prettier/-/xml-but-prettier-1.0.1.tgz",
+ "integrity": "sha512-C2CJaadHrZTqESlH03WOyw0oZTtoy2uEg6dSDF6YRg+9GnYNub53RRemLpnvtbHDFelxMx4LajiFsYeR6XJHgQ==",
+ "dependencies": {
+ "repeat-string": "^1.5.2"
+ }
+ },
"node_modules/xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
@@ -19430,6 +20949,11 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/zenscroll": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/zenscroll/-/zenscroll-4.0.2.tgz",
+ "integrity": "sha512-jEA1znR7b4C/NnaycInCU6h/d15ZzCd1jmsruqOKnZP6WXQSMH3W2GL+OXbkruslU4h+Tzuos0HdswzRUk/Vgg=="
+ },
"node_modules/zod": {
"version": "3.22.4",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz",
@@ -19447,6 +20971,235 @@
"url": "https://github.com/sponsors/wooorm"
}
},
+ "packages/api": {
+ "name": "@documenso/api",
+ "version": "1.0.0",
+ "license": "MIT",
+ "dependencies": {
+ "@documenso/lib": "*",
+ "@documenso/prisma": "*",
+ "@ts-rest/core": "^3.30.5",
+ "@ts-rest/next": "^3.30.5",
+ "@ts-rest/open-api": "^3.33.0",
+ "@types/swagger-ui-react": "^4.18.3",
+ "luxon": "^3.4.0",
+ "superjson": "^1.13.1",
+ "swagger-ui-react": "^5.11.0",
+ "ts-pattern": "^5.0.5",
+ "zod": "^3.22.4"
+ }
+ },
+ "packages/api/node_modules/@next/env": {
+ "version": "13.5.6",
+ "resolved": "https://registry.npmjs.org/@next/env/-/env-13.5.6.tgz",
+ "integrity": "sha512-Yac/bV5sBGkkEXmAX5FWPS9Mmo2rthrOPRQQNfycJPkjUAUclomCPH7QFVCDQ4Mp2k2K1SSM6m0zrxYrOwtFQw==",
+ "peer": true
+ },
+ "packages/api/node_modules/@next/swc-darwin-arm64": {
+ "version": "13.5.6",
+ "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.5.6.tgz",
+ "integrity": "sha512-5nvXMzKtZfvcu4BhtV0KH1oGv4XEW+B+jOfmBdpFI3C7FrB/MfujRpWYSBBO64+qbW8pkZiSyQv9eiwnn5VIQA==",
+ "cpu": [
+ "arm64"
+ ],
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "packages/api/node_modules/@next/swc-darwin-x64": {
+ "version": "13.5.6",
+ "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-13.5.6.tgz",
+ "integrity": "sha512-6cgBfxg98oOCSr4BckWjLLgiVwlL3vlLj8hXg2b+nDgm4bC/qVXXLfpLB9FHdoDu4057hzywbxKvmYGmi7yUzA==",
+ "cpu": [
+ "x64"
+ ],
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "packages/api/node_modules/@next/swc-linux-arm64-gnu": {
+ "version": "13.5.6",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.5.6.tgz",
+ "integrity": "sha512-txagBbj1e1w47YQjcKgSU4rRVQ7uF29YpnlHV5xuVUsgCUf2FmyfJ3CPjZUvpIeXCJAoMCFAoGnbtX86BK7+sg==",
+ "cpu": [
+ "arm64"
+ ],
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "packages/api/node_modules/@next/swc-linux-arm64-musl": {
+ "version": "13.5.6",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.5.6.tgz",
+ "integrity": "sha512-cGd+H8amifT86ZldVJtAKDxUqeFyLWW+v2NlBULnLAdWsiuuN8TuhVBt8ZNpCqcAuoruoSWynvMWixTFcroq+Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "packages/api/node_modules/@next/swc-linux-x64-gnu": {
+ "version": "13.5.6",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.5.6.tgz",
+ "integrity": "sha512-Mc2b4xiIWKXIhBy2NBTwOxGD3nHLmq4keFk+d4/WL5fMsB8XdJRdtUlL87SqVCTSaf1BRuQQf1HvXZcy+rq3Nw==",
+ "cpu": [
+ "x64"
+ ],
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "packages/api/node_modules/@next/swc-linux-x64-musl": {
+ "version": "13.5.6",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.5.6.tgz",
+ "integrity": "sha512-CFHvP9Qz98NruJiUnCe61O6GveKKHpJLloXbDSWRhqhkJdZD2zU5hG+gtVJR//tyW897izuHpM6Gtf6+sNgJPQ==",
+ "cpu": [
+ "x64"
+ ],
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "packages/api/node_modules/@next/swc-win32-arm64-msvc": {
+ "version": "13.5.6",
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.5.6.tgz",
+ "integrity": "sha512-aFv1ejfkbS7PUa1qVPwzDHjQWQtknzAZWGTKYIAaS4NMtBlk3VyA6AYn593pqNanlicewqyl2jUhQAaFV/qXsg==",
+ "cpu": [
+ "arm64"
+ ],
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "packages/api/node_modules/@next/swc-win32-ia32-msvc": {
+ "version": "13.5.6",
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.5.6.tgz",
+ "integrity": "sha512-XqqpHgEIlBHvzwG8sp/JXMFkLAfGLqkbVsyN+/Ih1mR8INb6YCc2x/Mbwi6hsAgUnqQztz8cvEbHJUbSl7RHDg==",
+ "cpu": [
+ "ia32"
+ ],
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "packages/api/node_modules/@next/swc-win32-x64-msvc": {
+ "version": "13.5.6",
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.5.6.tgz",
+ "integrity": "sha512-Cqfe1YmOS7k+5mGu92nl5ULkzpKuxJrP3+4AEuPmrpFZ3BHxTY3TnHmU1On3bFmFFs6FbTcdF58CCUProGpIGQ==",
+ "cpu": [
+ "x64"
+ ],
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "packages/api/node_modules/@ts-rest/next": {
+ "version": "3.30.5",
+ "resolved": "https://registry.npmjs.org/@ts-rest/next/-/next-3.30.5.tgz",
+ "integrity": "sha512-NasfUN7SnwcjJNbxvvcemC4fOv4f4IF5I14wVqQODN0HWPokkrta6XLuv0eKQJYdB32AS7VINQhls8Sj1AIN0g==",
+ "peerDependencies": {
+ "@ts-rest/core": "3.30.5",
+ "next": "^12.0.0 || ^13.0.0",
+ "zod": "^3.22.3"
+ },
+ "peerDependenciesMeta": {
+ "zod": {
+ "optional": true
+ }
+ }
+ },
+ "packages/api/node_modules/next": {
+ "version": "13.5.6",
+ "resolved": "https://registry.npmjs.org/next/-/next-13.5.6.tgz",
+ "integrity": "sha512-Y2wTcTbO4WwEsVb4A8VSnOsG1I9ok+h74q0ZdxkwM3EODqrs4pasq7O0iUxbcS9VtWMicG7f3+HAj0r1+NtKSw==",
+ "peer": true,
+ "dependencies": {
+ "@next/env": "13.5.6",
+ "@swc/helpers": "0.5.2",
+ "busboy": "1.6.0",
+ "caniuse-lite": "^1.0.30001406",
+ "postcss": "8.4.31",
+ "styled-jsx": "5.1.1",
+ "watchpack": "2.4.0"
+ },
+ "bin": {
+ "next": "dist/bin/next"
+ },
+ "engines": {
+ "node": ">=16.14.0"
+ },
+ "optionalDependencies": {
+ "@next/swc-darwin-arm64": "13.5.6",
+ "@next/swc-darwin-x64": "13.5.6",
+ "@next/swc-linux-arm64-gnu": "13.5.6",
+ "@next/swc-linux-arm64-musl": "13.5.6",
+ "@next/swc-linux-x64-gnu": "13.5.6",
+ "@next/swc-linux-x64-musl": "13.5.6",
+ "@next/swc-win32-arm64-msvc": "13.5.6",
+ "@next/swc-win32-ia32-msvc": "13.5.6",
+ "@next/swc-win32-x64-msvc": "13.5.6"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.1.0",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "sass": "^1.3.0"
+ },
+ "peerDependenciesMeta": {
+ "@opentelemetry/api": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ }
+ }
+ },
"packages/app-tests": {
"name": "@documenso/app-tests",
"version": "1.0.0",
@@ -19762,6 +21515,8 @@
"@trpc/next": "^10.36.0",
"@trpc/react-query": "^10.36.0",
"@trpc/server": "^10.36.0",
+ "@ts-rest/core": "^3.30.5",
+ "@ts-rest/next": "^3.30.5",
"luxon": "^3.4.0",
"superjson": "^1.13.1",
"ts-pattern": "^5.0.5",
@@ -19769,6 +21524,217 @@
},
"devDependencies": {}
},
+ "packages/trpc/node_modules/@next/env": {
+ "version": "13.5.6",
+ "resolved": "https://registry.npmjs.org/@next/env/-/env-13.5.6.tgz",
+ "integrity": "sha512-Yac/bV5sBGkkEXmAX5FWPS9Mmo2rthrOPRQQNfycJPkjUAUclomCPH7QFVCDQ4Mp2k2K1SSM6m0zrxYrOwtFQw==",
+ "peer": true
+ },
+ "packages/trpc/node_modules/@next/swc-darwin-arm64": {
+ "version": "13.5.6",
+ "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.5.6.tgz",
+ "integrity": "sha512-5nvXMzKtZfvcu4BhtV0KH1oGv4XEW+B+jOfmBdpFI3C7FrB/MfujRpWYSBBO64+qbW8pkZiSyQv9eiwnn5VIQA==",
+ "cpu": [
+ "arm64"
+ ],
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "packages/trpc/node_modules/@next/swc-darwin-x64": {
+ "version": "13.5.6",
+ "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-13.5.6.tgz",
+ "integrity": "sha512-6cgBfxg98oOCSr4BckWjLLgiVwlL3vlLj8hXg2b+nDgm4bC/qVXXLfpLB9FHdoDu4057hzywbxKvmYGmi7yUzA==",
+ "cpu": [
+ "x64"
+ ],
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "packages/trpc/node_modules/@next/swc-linux-arm64-gnu": {
+ "version": "13.5.6",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.5.6.tgz",
+ "integrity": "sha512-txagBbj1e1w47YQjcKgSU4rRVQ7uF29YpnlHV5xuVUsgCUf2FmyfJ3CPjZUvpIeXCJAoMCFAoGnbtX86BK7+sg==",
+ "cpu": [
+ "arm64"
+ ],
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "packages/trpc/node_modules/@next/swc-linux-arm64-musl": {
+ "version": "13.5.6",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.5.6.tgz",
+ "integrity": "sha512-cGd+H8amifT86ZldVJtAKDxUqeFyLWW+v2NlBULnLAdWsiuuN8TuhVBt8ZNpCqcAuoruoSWynvMWixTFcroq+Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "packages/trpc/node_modules/@next/swc-linux-x64-gnu": {
+ "version": "13.5.6",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.5.6.tgz",
+ "integrity": "sha512-Mc2b4xiIWKXIhBy2NBTwOxGD3nHLmq4keFk+d4/WL5fMsB8XdJRdtUlL87SqVCTSaf1BRuQQf1HvXZcy+rq3Nw==",
+ "cpu": [
+ "x64"
+ ],
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "packages/trpc/node_modules/@next/swc-linux-x64-musl": {
+ "version": "13.5.6",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.5.6.tgz",
+ "integrity": "sha512-CFHvP9Qz98NruJiUnCe61O6GveKKHpJLloXbDSWRhqhkJdZD2zU5hG+gtVJR//tyW897izuHpM6Gtf6+sNgJPQ==",
+ "cpu": [
+ "x64"
+ ],
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "packages/trpc/node_modules/@next/swc-win32-arm64-msvc": {
+ "version": "13.5.6",
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.5.6.tgz",
+ "integrity": "sha512-aFv1ejfkbS7PUa1qVPwzDHjQWQtknzAZWGTKYIAaS4NMtBlk3VyA6AYn593pqNanlicewqyl2jUhQAaFV/qXsg==",
+ "cpu": [
+ "arm64"
+ ],
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "packages/trpc/node_modules/@next/swc-win32-ia32-msvc": {
+ "version": "13.5.6",
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.5.6.tgz",
+ "integrity": "sha512-XqqpHgEIlBHvzwG8sp/JXMFkLAfGLqkbVsyN+/Ih1mR8INb6YCc2x/Mbwi6hsAgUnqQztz8cvEbHJUbSl7RHDg==",
+ "cpu": [
+ "ia32"
+ ],
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "packages/trpc/node_modules/@next/swc-win32-x64-msvc": {
+ "version": "13.5.6",
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.5.6.tgz",
+ "integrity": "sha512-Cqfe1YmOS7k+5mGu92nl5ULkzpKuxJrP3+4AEuPmrpFZ3BHxTY3TnHmU1On3bFmFFs6FbTcdF58CCUProGpIGQ==",
+ "cpu": [
+ "x64"
+ ],
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "packages/trpc/node_modules/@ts-rest/next": {
+ "version": "3.30.5",
+ "resolved": "https://registry.npmjs.org/@ts-rest/next/-/next-3.30.5.tgz",
+ "integrity": "sha512-NasfUN7SnwcjJNbxvvcemC4fOv4f4IF5I14wVqQODN0HWPokkrta6XLuv0eKQJYdB32AS7VINQhls8Sj1AIN0g==",
+ "peerDependencies": {
+ "@ts-rest/core": "3.30.5",
+ "next": "^12.0.0 || ^13.0.0",
+ "zod": "^3.22.3"
+ },
+ "peerDependenciesMeta": {
+ "zod": {
+ "optional": true
+ }
+ }
+ },
+ "packages/trpc/node_modules/next": {
+ "version": "13.5.6",
+ "resolved": "https://registry.npmjs.org/next/-/next-13.5.6.tgz",
+ "integrity": "sha512-Y2wTcTbO4WwEsVb4A8VSnOsG1I9ok+h74q0ZdxkwM3EODqrs4pasq7O0iUxbcS9VtWMicG7f3+HAj0r1+NtKSw==",
+ "peer": true,
+ "dependencies": {
+ "@next/env": "13.5.6",
+ "@swc/helpers": "0.5.2",
+ "busboy": "1.6.0",
+ "caniuse-lite": "^1.0.30001406",
+ "postcss": "8.4.31",
+ "styled-jsx": "5.1.1",
+ "watchpack": "2.4.0"
+ },
+ "bin": {
+ "next": "dist/bin/next"
+ },
+ "engines": {
+ "node": ">=16.14.0"
+ },
+ "optionalDependencies": {
+ "@next/swc-darwin-arm64": "13.5.6",
+ "@next/swc-darwin-x64": "13.5.6",
+ "@next/swc-linux-arm64-gnu": "13.5.6",
+ "@next/swc-linux-arm64-musl": "13.5.6",
+ "@next/swc-linux-x64-gnu": "13.5.6",
+ "@next/swc-linux-x64-musl": "13.5.6",
+ "@next/swc-win32-arm64-msvc": "13.5.6",
+ "@next/swc-win32-ia32-msvc": "13.5.6",
+ "@next/swc-win32-x64-msvc": "13.5.6"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.1.0",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "sass": "^1.3.0"
+ },
+ "peerDependenciesMeta": {
+ "@opentelemetry/api": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ }
+ }
+ },
"packages/tsconfig": {
"name": "@documenso/tsconfig",
"version": "0.0.0",
@@ -19817,6 +21783,7 @@
"luxon": "^3.4.2",
"next": "14.0.3",
"pdfjs-dist": "3.6.172",
+ "react-colorful": "^5.6.1",
"react-day-picker": "^8.7.1",
"react-hook-form": "^7.45.4",
"react-pdf": "7.3.3",
diff --git a/packages/api/index.ts b/packages/api/index.ts
new file mode 100644
index 000000000..cb0ff5c3b
--- /dev/null
+++ b/packages/api/index.ts
@@ -0,0 +1 @@
+export {};
diff --git a/packages/api/next.ts b/packages/api/next.ts
new file mode 100644
index 000000000..5ac5aab45
--- /dev/null
+++ b/packages/api/next.ts
@@ -0,0 +1 @@
+export { createNextRouter } from '@ts-rest/next';
diff --git a/packages/api/package.json b/packages/api/package.json
new file mode 100644
index 000000000..aebb09c9b
--- /dev/null
+++ b/packages/api/package.json
@@ -0,0 +1,30 @@
+{
+ "name": "@documenso/api",
+ "version": "1.0.0",
+ "main": "./index.ts",
+ "types": "./index.ts",
+ "license": "MIT",
+ "scripts": {
+ "lint": "eslint .",
+ "lint:fix": "eslint . --fix",
+ "clean": "rimraf node_modules"
+ },
+ "files": [
+ "index.ts",
+ "next.ts",
+ "v1/"
+ ],
+ "dependencies": {
+ "@documenso/lib": "*",
+ "@documenso/prisma": "*",
+ "@ts-rest/core": "^3.30.5",
+ "@ts-rest/next": "^3.30.5",
+ "@ts-rest/open-api": "^3.33.0",
+ "@types/swagger-ui-react": "^4.18.3",
+ "luxon": "^3.4.0",
+ "superjson": "^1.13.1",
+ "swagger-ui-react": "^5.11.0",
+ "ts-pattern": "^5.0.5",
+ "zod": "^3.22.4"
+ }
+}
diff --git a/packages/api/tsconfig.json b/packages/api/tsconfig.json
new file mode 100644
index 000000000..dc21318a7
--- /dev/null
+++ b/packages/api/tsconfig.json
@@ -0,0 +1,8 @@
+{
+ "extends": "@documenso/tsconfig/react-library.json",
+ "include": ["."],
+ "exclude": ["dist", "build", "node_modules"],
+ "compilerOptions": {
+ "strict": true,
+ }
+}
diff --git a/packages/api/v1/api-documentation.tsx b/packages/api/v1/api-documentation.tsx
new file mode 100644
index 000000000..6082d2d7f
--- /dev/null
+++ b/packages/api/v1/api-documentation.tsx
@@ -0,0 +1,10 @@
+'use client';
+
+import SwaggerUI from 'swagger-ui-react';
+import 'swagger-ui-react/swagger-ui.css';
+
+import { OpenAPIV1 } from '@documenso/api/v1/openapi';
+
+export const OpenApiDocsPage = () => {
+ return ;
+};
diff --git a/packages/api/v1/contract.ts b/packages/api/v1/contract.ts
new file mode 100644
index 000000000..162cdcf9d
--- /dev/null
+++ b/packages/api/v1/contract.ts
@@ -0,0 +1,191 @@
+import { initContract } from '@ts-rest/core';
+
+import {
+ ZAuthorizationHeadersSchema,
+ ZCreateDocumentFromTemplateMutationResponseSchema,
+ ZCreateDocumentFromTemplateMutationSchema,
+ ZCreateDocumentMutationResponseSchema,
+ ZCreateDocumentMutationSchema,
+ ZCreateFieldMutationSchema,
+ ZCreateRecipientMutationSchema,
+ ZDeleteDocumentMutationSchema,
+ ZDeleteFieldMutationSchema,
+ ZDeleteRecipientMutationSchema,
+ ZGetDocumentsQuerySchema,
+ ZSendDocumentForSigningMutationSchema,
+ ZSuccessfulDocumentResponseSchema,
+ ZSuccessfulFieldResponseSchema,
+ ZSuccessfulGetDocumentResponseSchema,
+ ZSuccessfulRecipientResponseSchema,
+ ZSuccessfulResponseSchema,
+ ZSuccessfulSigningResponseSchema,
+ ZUnsuccessfulResponseSchema,
+ ZUpdateFieldMutationSchema,
+ ZUpdateRecipientMutationSchema,
+} from './schema';
+
+const c = initContract();
+
+export const ApiContractV1 = c.router(
+ {
+ getDocuments: {
+ method: 'GET',
+ path: '/api/v1/documents',
+ query: ZGetDocumentsQuerySchema,
+ responses: {
+ 200: ZSuccessfulResponseSchema,
+ 401: ZUnsuccessfulResponseSchema,
+ 404: ZUnsuccessfulResponseSchema,
+ },
+ summary: 'Get all documents',
+ },
+
+ getDocument: {
+ method: 'GET',
+ path: '/api/v1/documents/:id',
+ responses: {
+ 200: ZSuccessfulGetDocumentResponseSchema,
+ 401: ZUnsuccessfulResponseSchema,
+ 404: ZUnsuccessfulResponseSchema,
+ },
+ summary: 'Get a single document',
+ },
+
+ createDocument: {
+ method: 'POST',
+ path: '/api/v1/documents',
+ body: ZCreateDocumentMutationSchema,
+ responses: {
+ 200: ZCreateDocumentMutationResponseSchema,
+ 401: ZUnsuccessfulResponseSchema,
+ 404: ZUnsuccessfulResponseSchema,
+ },
+ summary: 'Upload a new document and get a presigned URL',
+ },
+
+ createDocumentFromTemplate: {
+ method: 'POST',
+ path: '/api/v1/templates/:templateId/create-document',
+ body: ZCreateDocumentFromTemplateMutationSchema,
+ responses: {
+ 200: ZCreateDocumentFromTemplateMutationResponseSchema,
+ 401: ZUnsuccessfulResponseSchema,
+ 404: ZUnsuccessfulResponseSchema,
+ },
+ summary: 'Create a new document from an existing template',
+ },
+
+ sendDocument: {
+ method: 'POST',
+ path: '/api/v1/documents/:id/send',
+ body: ZSendDocumentForSigningMutationSchema,
+ responses: {
+ 200: ZSuccessfulSigningResponseSchema,
+ 400: ZUnsuccessfulResponseSchema,
+ 401: ZUnsuccessfulResponseSchema,
+ 404: ZUnsuccessfulResponseSchema,
+ 500: ZUnsuccessfulResponseSchema,
+ },
+ summary: 'Send a document for signing',
+ },
+
+ deleteDocument: {
+ method: 'DELETE',
+ path: '/api/v1/documents/:id',
+ body: ZDeleteDocumentMutationSchema,
+ responses: {
+ 200: ZSuccessfulDocumentResponseSchema,
+ 401: ZUnsuccessfulResponseSchema,
+ 404: ZUnsuccessfulResponseSchema,
+ },
+ summary: 'Delete a document',
+ },
+
+ createRecipient: {
+ method: 'POST',
+ path: '/api/v1/documents/:id/recipients',
+ body: ZCreateRecipientMutationSchema,
+ responses: {
+ 200: ZSuccessfulRecipientResponseSchema,
+ 400: ZUnsuccessfulResponseSchema,
+ 401: ZUnsuccessfulResponseSchema,
+ 404: ZUnsuccessfulResponseSchema,
+ 500: ZUnsuccessfulResponseSchema,
+ },
+ summary: 'Create a recipient for a document',
+ },
+
+ updateRecipient: {
+ method: 'PATCH',
+ path: '/api/v1/documents/:id/recipients/:recipientId',
+ body: ZUpdateRecipientMutationSchema,
+ responses: {
+ 200: ZSuccessfulRecipientResponseSchema,
+ 400: ZUnsuccessfulResponseSchema,
+ 401: ZUnsuccessfulResponseSchema,
+ 404: ZUnsuccessfulResponseSchema,
+ 500: ZUnsuccessfulResponseSchema,
+ },
+ summary: 'Update a recipient for a document',
+ },
+
+ deleteRecipient: {
+ method: 'DELETE',
+ path: '/api/v1/documents/:id/recipients/:recipientId',
+ body: ZDeleteRecipientMutationSchema,
+ responses: {
+ 200: ZSuccessfulRecipientResponseSchema,
+ 400: ZUnsuccessfulResponseSchema,
+ 401: ZUnsuccessfulResponseSchema,
+ 404: ZUnsuccessfulResponseSchema,
+ 500: ZUnsuccessfulResponseSchema,
+ },
+ summary: 'Delete a recipient from a document',
+ },
+
+ createField: {
+ method: 'POST',
+ path: '/api/v1/documents/:id/fields',
+ body: ZCreateFieldMutationSchema,
+ responses: {
+ 200: ZSuccessfulFieldResponseSchema,
+ 400: ZUnsuccessfulResponseSchema,
+ 401: ZUnsuccessfulResponseSchema,
+ 404: ZUnsuccessfulResponseSchema,
+ 500: ZUnsuccessfulResponseSchema,
+ },
+ summary: 'Create a field for a document',
+ },
+
+ updateField: {
+ method: 'PATCH',
+ path: '/api/v1/documents/:id/fields/:fieldId',
+ body: ZUpdateFieldMutationSchema,
+ responses: {
+ 200: ZSuccessfulFieldResponseSchema,
+ 400: ZUnsuccessfulResponseSchema,
+ 401: ZUnsuccessfulResponseSchema,
+ 404: ZUnsuccessfulResponseSchema,
+ 500: ZUnsuccessfulResponseSchema,
+ },
+ summary: 'Update a field for a document',
+ },
+
+ deleteField: {
+ method: 'DELETE',
+ path: '/api/v1/documents/:id/fields/:fieldId',
+ body: ZDeleteFieldMutationSchema,
+ responses: {
+ 200: ZSuccessfulFieldResponseSchema,
+ 400: ZUnsuccessfulResponseSchema,
+ 401: ZUnsuccessfulResponseSchema,
+ 404: ZUnsuccessfulResponseSchema,
+ 500: ZUnsuccessfulResponseSchema,
+ },
+ summary: 'Delete a field from a document',
+ },
+ },
+ {
+ baseHeaders: ZAuthorizationHeadersSchema,
+ },
+);
diff --git a/packages/api/v1/examples/01-create-and-send-document.ts b/packages/api/v1/examples/01-create-and-send-document.ts
new file mode 100644
index 000000000..925d86656
--- /dev/null
+++ b/packages/api/v1/examples/01-create-and-send-document.ts
@@ -0,0 +1,59 @@
+import { initClient } from '@ts-rest/core';
+
+import { ApiContractV1 } from '../contract';
+
+const main = async () => {
+ const client = initClient(ApiContractV1, {
+ baseUrl: 'http://localhost:3000/api/v1',
+ baseHeaders: {
+ authorization: 'Bearer ',
+ },
+ });
+
+ const { status, body } = await client.createDocument({
+ body: {
+ title: 'My Document',
+ recipients: [
+ {
+ name: 'John Doe',
+ email: 'john@example.com',
+ role: 'SIGNER',
+ },
+ {
+ name: 'Jane Doe',
+ email: 'jane@example.com',
+ role: 'APPROVER',
+ },
+ ],
+ meta: {
+ subject: 'Please sign this document',
+ message: 'Hey {signer.name}, please sign the following document: {document.name}',
+ },
+ },
+ });
+
+ if (status !== 200) {
+ throw new Error('Failed to create document');
+ }
+
+ const { uploadUrl, documentId } = body;
+
+ await fetch(uploadUrl, {
+ method: 'PUT',
+ headers: {
+ 'Content-Type': 'application/octet-stream',
+ },
+ body: '',
+ });
+
+ await client.sendDocument({
+ params: {
+ id: documentId.toString(),
+ },
+ });
+};
+
+main().catch((error) => {
+ console.error(error);
+ process.exit(1);
+});
diff --git a/packages/api/v1/examples/02-add-a-field.ts b/packages/api/v1/examples/02-add-a-field.ts
new file mode 100644
index 000000000..6b186694a
--- /dev/null
+++ b/packages/api/v1/examples/02-add-a-field.ts
@@ -0,0 +1,43 @@
+import { initClient } from '@ts-rest/core';
+
+import { ApiContractV1 } from '../contract';
+
+const main = async () => {
+ const client = initClient(ApiContractV1, {
+ baseUrl: 'http://localhost:3000/api/v1',
+ baseHeaders: {
+ authorization: 'Bearer ',
+ },
+ });
+
+ const documentId = '1';
+ const recipientId = 1;
+
+ const { status, body } = await client.createField({
+ params: {
+ id: documentId,
+ },
+ body: {
+ type: 'SIGNATURE',
+ pageHeight: 2.5, // percent of page to occupy in height
+ pageWidth: 5, // percent of page to occupy in width
+ pageX: 10, // percent from left
+ pageY: 10, // percent from top
+ pageNumber: 1,
+ recipientId,
+ },
+ });
+
+ if (status !== 200) {
+ throw new Error('Failed to create field');
+ }
+
+ const { id: fieldId } = body;
+
+ console.log(`Field created with id: ${fieldId}`);
+};
+
+main().catch((error) => {
+ console.error(error);
+ process.exit(1);
+});
diff --git a/packages/api/v1/examples/03-update-a-field.ts b/packages/api/v1/examples/03-update-a-field.ts
new file mode 100644
index 000000000..d93831b7c
--- /dev/null
+++ b/packages/api/v1/examples/03-update-a-field.ts
@@ -0,0 +1,39 @@
+import { initClient } from '@ts-rest/core';
+
+import { ApiContractV1 } from '../contract';
+
+const main = async () => {
+ const client = initClient(ApiContractV1, {
+ baseUrl: 'http://localhost:3000/api/v1',
+ baseHeaders: {
+ authorization: 'Bearer ',
+ },
+ });
+
+ const documentId = '1';
+ const fieldId = '1';
+
+ const { status } = await client.updateField({
+ params: {
+ id: documentId,
+ fieldId,
+ },
+ body: {
+ type: 'SIGNATURE',
+ pageHeight: 2.5, // percent of page to occupy in height
+ pageWidth: 5, // percent of page to occupy in width
+ pageX: 10, // percent from left
+ pageY: 10, // percent from top
+ pageNumber: 1,
+ },
+ });
+
+ if (status !== 200) {
+ throw new Error('Failed to update field');
+ }
+};
+
+main().catch((error) => {
+ console.error(error);
+ process.exit(1);
+});
diff --git a/packages/api/v1/examples/04-remove-a-field.ts b/packages/api/v1/examples/04-remove-a-field.ts
new file mode 100644
index 000000000..d7f233940
--- /dev/null
+++ b/packages/api/v1/examples/04-remove-a-field.ts
@@ -0,0 +1,31 @@
+import { initClient } from '@ts-rest/core';
+
+import { ApiContractV1 } from '../contract';
+
+const main = async () => {
+ const client = initClient(ApiContractV1, {
+ baseUrl: 'http://localhost:3000/api/v1',
+ baseHeaders: {
+ authorization: 'Bearer ',
+ },
+ });
+
+ const documentId = '1';
+ const fieldId = '1';
+
+ const { status } = await client.deleteField({
+ params: {
+ id: documentId,
+ fieldId,
+ },
+ });
+
+ if (status !== 200) {
+ throw new Error('Failed to remove field');
+ }
+};
+
+main().catch((error) => {
+ console.error(error);
+ process.exit(1);
+});
diff --git a/packages/api/v1/examples/05-add-a-recipient.ts b/packages/api/v1/examples/05-add-a-recipient.ts
new file mode 100644
index 000000000..e63abd9e5
--- /dev/null
+++ b/packages/api/v1/examples/05-add-a-recipient.ts
@@ -0,0 +1,38 @@
+import { initClient } from '@ts-rest/core';
+
+import { ApiContractV1 } from '../contract';
+
+const main = async () => {
+ const client = initClient(ApiContractV1, {
+ baseUrl: 'http://localhost:3000/api/v1',
+ baseHeaders: {
+ authorization: 'Bearer ',
+ },
+ });
+
+ const documentId = '1';
+
+ const { status, body } = await client.createRecipient({
+ params: {
+ id: documentId,
+ },
+ body: {
+ name: 'John Doe',
+ email: 'john@example.com',
+ role: 'APPROVER',
+ },
+ });
+
+ if (status !== 200) {
+ throw new Error('Failed to add recipient');
+ }
+
+ const { id: recipientId } = body;
+
+ console.log(`Recipient added with id: ${recipientId}`);
+};
+
+main().catch((error) => {
+ console.error(error);
+ process.exit(1);
+});
diff --git a/packages/api/v1/examples/06-update-a-recipient.ts b/packages/api/v1/examples/06-update-a-recipient.ts
new file mode 100644
index 000000000..d9e8255e7
--- /dev/null
+++ b/packages/api/v1/examples/06-update-a-recipient.ts
@@ -0,0 +1,34 @@
+import { initClient } from '@ts-rest/core';
+
+import { ApiContractV1 } from '../contract';
+
+const main = async () => {
+ const client = initClient(ApiContractV1, {
+ baseUrl: 'http://localhost:3000/api/v1',
+ baseHeaders: {
+ authorization: 'Bearer ',
+ },
+ });
+
+ const documentId = '1';
+ const recipientId = '1';
+
+ const { status } = await client.updateRecipient({
+ params: {
+ id: documentId,
+ recipientId,
+ },
+ body: {
+ name: 'Johnathon Doe',
+ },
+ });
+
+ if (status !== 200) {
+ throw new Error('Failed to update recipient');
+ }
+};
+
+main().catch((error) => {
+ console.error(error);
+ process.exit(1);
+});
diff --git a/packages/api/v1/examples/07-remove-a-recipient.ts b/packages/api/v1/examples/07-remove-a-recipient.ts
new file mode 100644
index 000000000..956e7dcae
--- /dev/null
+++ b/packages/api/v1/examples/07-remove-a-recipient.ts
@@ -0,0 +1,31 @@
+import { initClient } from '@ts-rest/core';
+
+import { ApiContractV1 } from '../contract';
+
+const main = async () => {
+ const client = initClient(ApiContractV1, {
+ baseUrl: 'http://localhost:3000/api/v1',
+ baseHeaders: {
+ authorization: 'Bearer ',
+ },
+ });
+
+ const documentId = '1';
+ const recipientId = '1';
+
+ const { status } = await client.deleteRecipient({
+ params: {
+ id: documentId,
+ recipientId,
+ },
+ });
+
+ if (status !== 200) {
+ throw new Error('Failed to update recipient');
+ }
+};
+
+main().catch((error) => {
+ console.error(error);
+ process.exit(1);
+});
diff --git a/packages/api/v1/examples/08-get-a-document.ts b/packages/api/v1/examples/08-get-a-document.ts
new file mode 100644
index 000000000..eb69cc8e8
--- /dev/null
+++ b/packages/api/v1/examples/08-get-a-document.ts
@@ -0,0 +1,31 @@
+import { initClient } from '@ts-rest/core';
+
+import { ApiContractV1 } from '../contract';
+
+const main = async () => {
+ const client = initClient(ApiContractV1, {
+ baseUrl: 'http://localhost:3000/api/v1',
+ baseHeaders: {
+ authorization: 'Bearer ',
+ },
+ });
+
+ const documentId = '1';
+
+ const { status, body } = await client.getDocument({
+ params: {
+ id: documentId,
+ },
+ });
+
+ if (status !== 200) {
+ throw new Error('Failed to get document');
+ }
+
+ console.log(`Got document with id: ${documentId} and title: ${body.title}`);
+};
+
+main().catch((error) => {
+ console.error(error);
+ process.exit(1);
+});
diff --git a/packages/api/v1/examples/09-paginate-all-documents.ts b/packages/api/v1/examples/09-paginate-all-documents.ts
new file mode 100644
index 000000000..f0330b620
--- /dev/null
+++ b/packages/api/v1/examples/09-paginate-all-documents.ts
@@ -0,0 +1,37 @@
+import { initClient } from '@ts-rest/core';
+
+import { ApiContractV1 } from '../contract';
+
+const main = async () => {
+ const client = initClient(ApiContractV1, {
+ baseUrl: 'http://localhost:3000/api/v1',
+ baseHeaders: {
+ authorization: 'Bearer ',
+ },
+ });
+
+ const page = 1;
+ const perPage = 10;
+
+ const { status, body } = await client.getDocuments({
+ query: {
+ page,
+ perPage,
+ },
+ });
+
+ if (status !== 200) {
+ throw new Error('Failed to get documents');
+ }
+
+ for (const document of body.documents) {
+ console.log(`Got document with id: ${document.id} and title: ${document.title}`);
+ }
+
+ console.log(`Total documents: ${body.totalPages * perPage}`);
+};
+
+main().catch((error) => {
+ console.error(error);
+ process.exit(1);
+});
diff --git a/packages/api/v1/implementation.ts b/packages/api/v1/implementation.ts
new file mode 100644
index 000000000..675c3b532
--- /dev/null
+++ b/packages/api/v1/implementation.ts
@@ -0,0 +1,800 @@
+import { createNextRoute } from '@ts-rest/next';
+
+import { getServerLimits } from '@documenso/ee/server-only/limits/server';
+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 { deleteDocument } from '@documenso/lib/server-only/document/delete-document';
+import { findDocuments } from '@documenso/lib/server-only/document/find-documents';
+import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id';
+import { sendDocument } from '@documenso/lib/server-only/document/send-document';
+import { updateDocument } from '@documenso/lib/server-only/document/update-document';
+import { createField } from '@documenso/lib/server-only/field/create-field';
+import { deleteField } from '@documenso/lib/server-only/field/delete-field';
+import { getFieldById } from '@documenso/lib/server-only/field/get-field-by-id';
+import { updateField } from '@documenso/lib/server-only/field/update-field';
+import { deleteRecipient } from '@documenso/lib/server-only/recipient/delete-recipient';
+import { getRecipientById } from '@documenso/lib/server-only/recipient/get-recipient-by-id';
+import { getRecipientsForDocument } from '@documenso/lib/server-only/recipient/get-recipients-for-document';
+import { setRecipientsForDocument } from '@documenso/lib/server-only/recipient/set-recipients-for-document';
+import { updateRecipient } from '@documenso/lib/server-only/recipient/update-recipient';
+import { createDocumentFromTemplate } from '@documenso/lib/server-only/template/create-document-from-template';
+import { extractNextApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
+import { getPresignPostUrl } from '@documenso/lib/universal/upload/server-actions';
+import { DocumentDataType, DocumentStatus, SigningStatus } from '@documenso/prisma/client';
+
+import { ApiContractV1 } from './contract';
+import { authenticatedMiddleware } from './middleware/authenticated';
+
+export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
+ getDocuments: authenticatedMiddleware(async (args, user, team) => {
+ const page = Number(args.query.page) || 1;
+ const perPage = Number(args.query.perPage) || 10;
+
+ const { data: documents, totalPages } = await findDocuments({
+ page,
+ perPage,
+ userId: user.id,
+ teamId: team?.id,
+ });
+
+ return {
+ status: 200,
+ body: {
+ documents,
+ totalPages,
+ },
+ };
+ }),
+
+ getDocument: authenticatedMiddleware(async (args, user, team) => {
+ const { id: documentId } = args.params;
+
+ try {
+ const document = await getDocumentById({
+ id: Number(documentId),
+ userId: user.id,
+ teamId: team?.id,
+ });
+
+ const recipients = await getRecipientsForDocument({
+ documentId: Number(documentId),
+ teamId: team?.id,
+ userId: user.id,
+ });
+
+ return {
+ status: 200,
+ body: {
+ ...document,
+ recipients,
+ },
+ };
+ } catch (err) {
+ return {
+ status: 404,
+ body: {
+ message: 'Document not found',
+ },
+ };
+ }
+ }),
+
+ deleteDocument: authenticatedMiddleware(async (args, user, team) => {
+ const { id: documentId } = args.params;
+
+ try {
+ const document = await getDocumentById({
+ id: Number(documentId),
+ userId: user.id,
+ teamId: team?.id,
+ });
+
+ if (!document) {
+ return {
+ status: 404,
+ body: {
+ message: 'Document not found',
+ },
+ };
+ }
+
+ const deletedDocument = await deleteDocument({
+ id: document.id,
+ userId: user.id,
+ teamId: team?.id,
+ });
+
+ return {
+ status: 200,
+ body: deletedDocument,
+ };
+ } catch (err) {
+ return {
+ status: 404,
+ body: {
+ message: 'Document not found',
+ },
+ };
+ }
+ }),
+
+ createDocument: authenticatedMiddleware(async (args, user, team) => {
+ const { body } = args;
+
+ try {
+ if (process.env.NEXT_PUBLIC_UPLOAD_TRANSPORT !== 's3') {
+ return {
+ status: 500,
+ body: {
+ message: 'Create document is not available without S3 transport.',
+ },
+ };
+ }
+
+ const { remaining } = await getServerLimits({ email: user.email, teamId: team?.id });
+
+ if (remaining.documents <= 0) {
+ return {
+ status: 400,
+ body: {
+ message: 'You have reached the maximum number of documents allowed for this month',
+ },
+ };
+ }
+
+ const fileName = body.title.endsWith('.pdf') ? body.title : `${body.title}.pdf`;
+
+ const { url, key } = await getPresignPostUrl(fileName, 'application/pdf');
+
+ const documentData = await createDocumentData({
+ data: key,
+ type: DocumentDataType.S3_PATH,
+ });
+
+ const document = await createDocument({
+ title: body.title,
+ userId: user.id,
+ teamId: team?.id,
+ documentDataId: documentData.id,
+ requestMetadata: extractNextApiRequestMetadata(args.req),
+ });
+
+ const recipients = await setRecipientsForDocument({
+ userId: user.id,
+ teamId: team?.id,
+ documentId: document.id,
+ recipients: body.recipients,
+ requestMetadata: extractNextApiRequestMetadata(args.req),
+ });
+
+ return {
+ status: 200,
+ body: {
+ uploadUrl: url,
+ documentId: document.id,
+ recipients: recipients.map((recipient) => ({
+ recipientId: recipient.id,
+ name: recipient.name,
+ email: recipient.email,
+ token: recipient.token,
+ role: recipient.role,
+ })),
+ },
+ };
+ } catch (err) {
+ return {
+ status: 404,
+ body: {
+ message: 'An error has occured while uploading the file',
+ },
+ };
+ }
+ }),
+
+ createDocumentFromTemplate: authenticatedMiddleware(async (args, user, team) => {
+ const { body, params } = args;
+
+ const { remaining } = await getServerLimits({ email: user.email, teamId: team?.id });
+
+ if (remaining.documents <= 0) {
+ return {
+ status: 400,
+ body: {
+ message: 'You have reached the maximum number of documents allowed for this month',
+ },
+ };
+ }
+
+ const templateId = Number(params.templateId);
+
+ const fileName = body.title.endsWith('.pdf') ? body.title : `${body.title}.pdf`;
+
+ const document = await createDocumentFromTemplate({
+ templateId,
+ userId: user.id,
+ teamId: team?.id,
+ recipients: body.recipients,
+ });
+
+ await updateDocument({
+ documentId: document.id,
+ userId: user.id,
+ teamId: team?.id,
+ data: {
+ title: fileName,
+ },
+ });
+
+ if (body.meta) {
+ await upsertDocumentMeta({
+ documentId: document.id,
+ userId: user.id,
+ subject: body.meta.subject,
+ message: body.meta.message,
+ dateFormat: body.meta.dateFormat,
+ timezone: body.meta.timezone,
+ requestMetadata: extractNextApiRequestMetadata(args.req),
+ });
+ }
+
+ return {
+ status: 200,
+ body: {
+ documentId: document.id,
+ recipients: document.Recipient.map((recipient) => ({
+ recipientId: recipient.id,
+ name: recipient.name,
+ email: recipient.email,
+ token: recipient.token,
+ role: recipient.role,
+ })),
+ },
+ };
+ }),
+
+ sendDocument: authenticatedMiddleware(async (args, user, team) => {
+ const { id } = args.params;
+
+ const document = await getDocumentById({ id: Number(id), userId: user.id, teamId: team?.id });
+
+ if (!document) {
+ return {
+ status: 404,
+ body: {
+ message: 'Document not found',
+ },
+ };
+ }
+
+ if (document.status === DocumentStatus.COMPLETED) {
+ return {
+ status: 400,
+ body: {
+ message: 'Document is already complete',
+ },
+ };
+ }
+
+ try {
+ // await setRecipientsForDocument({
+ // userId: user.id,
+ // documentId: Number(id),
+ // recipients: [
+ // {
+ // email: body.signerEmail,
+ // name: body.signerName ?? '',
+ // },
+ // ],
+ // });
+
+ // await setFieldsForDocument({
+ // documentId: Number(id),
+ // userId: user.id,
+ // fields: body.fields.map((field) => ({
+ // signerEmail: body.signerEmail,
+ // type: field.fieldType,
+ // pageNumber: field.pageNumber,
+ // pageX: field.pageX,
+ // pageY: field.pageY,
+ // pageWidth: field.pageWidth,
+ // pageHeight: field.pageHeight,
+ // })),
+ // });
+
+ // if (body.emailBody || body.emailSubject) {
+ // await upsertDocumentMeta({
+ // documentId: Number(id),
+ // subject: body.emailSubject ?? '',
+ // message: body.emailBody ?? '',
+ // });
+ // }
+
+ await sendDocument({
+ documentId: Number(id),
+ userId: user.id,
+ teamId: team?.id,
+ requestMetadata: extractNextApiRequestMetadata(args.req),
+ });
+
+ return {
+ status: 200,
+ body: {
+ message: 'Document sent for signing successfully',
+ },
+ };
+ } catch (err) {
+ return {
+ status: 500,
+ body: {
+ message: 'An error has occured while sending the document for signing',
+ },
+ };
+ }
+ }),
+
+ createRecipient: authenticatedMiddleware(async (args, user, team) => {
+ const { id: documentId } = args.params;
+ const { name, email, role } = args.body;
+
+ const document = await getDocumentById({
+ id: Number(documentId),
+ userId: user.id,
+ teamId: team?.id,
+ });
+
+ if (!document) {
+ return {
+ status: 404,
+ body: {
+ message: 'Document not found',
+ },
+ };
+ }
+
+ if (document.status === DocumentStatus.COMPLETED) {
+ return {
+ status: 400,
+ body: {
+ message: 'Document is already completed',
+ },
+ };
+ }
+
+ const recipients = await getRecipientsForDocument({
+ documentId: Number(documentId),
+ userId: user.id,
+ teamId: team?.id,
+ });
+
+ const recipientAlreadyExists = recipients.some((recipient) => recipient.email === email);
+
+ if (recipientAlreadyExists) {
+ return {
+ status: 400,
+ body: {
+ message: 'Recipient already exists',
+ },
+ };
+ }
+
+ try {
+ const newRecipients = await setRecipientsForDocument({
+ documentId: Number(documentId),
+ userId: user.id,
+ teamId: team?.id,
+ recipients: [
+ ...recipients,
+ {
+ email,
+ name,
+ role,
+ },
+ ],
+ requestMetadata: extractNextApiRequestMetadata(args.req),
+ });
+
+ const newRecipient = newRecipients.find((recipient) => recipient.email === email);
+
+ if (!newRecipient) {
+ throw new Error('Recipient not found');
+ }
+
+ return {
+ status: 200,
+ body: {
+ ...newRecipient,
+ documentId: Number(documentId),
+ },
+ };
+ } catch (err) {
+ return {
+ status: 500,
+ body: {
+ message: 'An error has occured while creating the recipient',
+ },
+ };
+ }
+ }),
+
+ updateRecipient: authenticatedMiddleware(async (args, user, team) => {
+ const { id: documentId, recipientId } = args.params;
+ const { name, email, role } = args.body;
+
+ const document = await getDocumentById({
+ id: Number(documentId),
+ userId: user.id,
+ teamId: team?.id,
+ });
+
+ if (!document) {
+ return {
+ status: 404,
+ body: {
+ message: 'Document not found',
+ },
+ };
+ }
+
+ if (document.status === DocumentStatus.COMPLETED) {
+ return {
+ status: 400,
+ body: {
+ message: 'Document is already completed',
+ },
+ };
+ }
+
+ const updatedRecipient = await updateRecipient({
+ documentId: Number(documentId),
+ recipientId: Number(recipientId),
+ userId: user.id,
+ teamId: team?.id,
+ email,
+ name,
+ role,
+ requestMetadata: extractNextApiRequestMetadata(args.req),
+ }).catch(() => null);
+
+ if (!updatedRecipient) {
+ return {
+ status: 404,
+ body: {
+ message: 'Recipient not found',
+ },
+ };
+ }
+
+ return {
+ status: 200,
+ body: {
+ ...updatedRecipient,
+ documentId: Number(documentId),
+ },
+ };
+ }),
+
+ deleteRecipient: authenticatedMiddleware(async (args, user, team) => {
+ const { id: documentId, recipientId } = args.params;
+
+ const document = await getDocumentById({
+ id: Number(documentId),
+ userId: user.id,
+ teamId: team?.id,
+ });
+
+ if (!document) {
+ return {
+ status: 404,
+ body: {
+ message: 'Document not found',
+ },
+ };
+ }
+
+ if (document.status === DocumentStatus.COMPLETED) {
+ return {
+ status: 400,
+ body: {
+ message: 'Document is already completed',
+ },
+ };
+ }
+
+ const deletedRecipient = await deleteRecipient({
+ documentId: Number(documentId),
+ recipientId: Number(recipientId),
+ userId: user.id,
+ teamId: team?.id,
+ requestMetadata: extractNextApiRequestMetadata(args.req),
+ }).catch(() => null);
+
+ if (!deletedRecipient) {
+ return {
+ status: 400,
+ body: {
+ message: 'Unable to delete recipient',
+ },
+ };
+ }
+
+ return {
+ status: 200,
+ body: {
+ ...deletedRecipient,
+ documentId: Number(documentId),
+ },
+ };
+ }),
+
+ createField: authenticatedMiddleware(async (args, user, team) => {
+ const { id: documentId } = args.params;
+ const { recipientId, type, pageNumber, pageWidth, pageHeight, pageX, pageY } = args.body;
+
+ const document = await getDocumentById({
+ id: Number(documentId),
+ userId: user.id,
+ teamId: team?.id,
+ });
+
+ if (!document) {
+ return {
+ status: 404,
+ body: {
+ message: 'Document not found',
+ },
+ };
+ }
+
+ if (document.status === DocumentStatus.COMPLETED) {
+ return {
+ status: 400,
+ body: {
+ message: 'Document is already completed',
+ },
+ };
+ }
+
+ const recipient = await getRecipientById({
+ id: Number(recipientId),
+ documentId: Number(documentId),
+ }).catch(() => null);
+
+ if (!recipient) {
+ return {
+ status: 404,
+ body: {
+ message: 'Recipient not found',
+ },
+ };
+ }
+
+ if (recipient.signingStatus === SigningStatus.SIGNED) {
+ return {
+ status: 400,
+ body: {
+ message: 'Recipient has already signed the document',
+ },
+ };
+ }
+
+ const field = await createField({
+ documentId: Number(documentId),
+ recipientId: Number(recipientId),
+ userId: user.id,
+ teamId: team?.id,
+ type,
+ pageNumber,
+ pageX,
+ pageY,
+ pageWidth,
+ pageHeight,
+ requestMetadata: extractNextApiRequestMetadata(args.req),
+ });
+
+ const remappedField = {
+ id: field.id,
+ documentId: field.documentId,
+ recipientId: field.recipientId ?? -1,
+ type: field.type,
+ pageNumber: field.page,
+ pageX: Number(field.positionX),
+ pageY: Number(field.positionY),
+ pageWidth: Number(field.width),
+ pageHeight: Number(field.height),
+ customText: field.customText,
+ inserted: field.inserted,
+ };
+
+ return {
+ status: 200,
+ body: {
+ ...remappedField,
+ documentId: Number(documentId),
+ },
+ };
+ }),
+
+ updateField: authenticatedMiddleware(async (args, user, team) => {
+ const { id: documentId, fieldId } = args.params;
+ const { recipientId, type, pageNumber, pageWidth, pageHeight, pageX, pageY } = args.body;
+
+ const document = await getDocumentById({
+ id: Number(documentId),
+ userId: user.id,
+ teamId: team?.id,
+ });
+
+ if (!document) {
+ return {
+ status: 404,
+ body: {
+ message: 'Document not found',
+ },
+ };
+ }
+
+ if (document.status === DocumentStatus.COMPLETED) {
+ return {
+ status: 400,
+ body: {
+ message: 'Document is already completed',
+ },
+ };
+ }
+
+ const recipient = await getRecipientById({
+ id: Number(recipientId),
+ documentId: Number(documentId),
+ }).catch(() => null);
+
+ if (!recipient) {
+ return {
+ status: 404,
+ body: {
+ message: 'Recipient not found',
+ },
+ };
+ }
+
+ if (recipient.signingStatus === SigningStatus.SIGNED) {
+ return {
+ status: 400,
+ body: {
+ message: 'Recipient has already signed the document',
+ },
+ };
+ }
+
+ const updatedField = await updateField({
+ fieldId: Number(fieldId),
+ userId: user.id,
+ teamId: team?.id,
+ documentId: Number(documentId),
+ recipientId: recipientId ? Number(recipientId) : undefined,
+ type,
+ pageNumber,
+ pageX,
+ pageY,
+ pageWidth,
+ pageHeight,
+ requestMetadata: extractNextApiRequestMetadata(args.req),
+ });
+
+ const remappedField = {
+ id: updatedField.id,
+ documentId: updatedField.documentId,
+ recipientId: updatedField.recipientId ?? -1,
+ type: updatedField.type,
+ pageNumber: updatedField.page,
+ pageX: Number(updatedField.positionX),
+ pageY: Number(updatedField.positionY),
+ pageWidth: Number(updatedField.width),
+ pageHeight: Number(updatedField.height),
+ customText: updatedField.customText,
+ inserted: updatedField.inserted,
+ };
+
+ return {
+ status: 200,
+ body: {
+ ...remappedField,
+ documentId: Number(documentId),
+ },
+ };
+ }),
+
+ deleteField: authenticatedMiddleware(async (args, user, team) => {
+ const { id: documentId, fieldId } = args.params;
+
+ const document = await getDocumentById({
+ id: Number(documentId),
+ userId: user.id,
+ });
+
+ if (!document) {
+ return {
+ status: 404,
+ body: {
+ message: 'Document not found',
+ },
+ };
+ }
+
+ if (document.status === DocumentStatus.COMPLETED) {
+ return {
+ status: 400,
+ body: {
+ message: 'Document is already completed',
+ },
+ };
+ }
+
+ const field = await getFieldById({
+ fieldId: Number(fieldId),
+ documentId: Number(documentId),
+ }).catch(() => null);
+
+ if (!field) {
+ return {
+ status: 404,
+ body: {
+ message: 'Field not found',
+ },
+ };
+ }
+
+ const recipient = await getRecipientById({
+ id: Number(field.recipientId),
+ documentId: Number(documentId),
+ }).catch(() => null);
+
+ if (recipient?.signingStatus === SigningStatus.SIGNED) {
+ return {
+ status: 400,
+ body: {
+ message: 'Recipient has already signed the document',
+ },
+ };
+ }
+
+ const deletedField = await deleteField({
+ documentId: Number(documentId),
+ fieldId: Number(fieldId),
+ userId: user.id,
+ teamId: team?.id,
+ requestMetadata: extractNextApiRequestMetadata(args.req),
+ }).catch(() => null);
+
+ if (!deletedField) {
+ return {
+ status: 400,
+ body: {
+ message: 'Unable to delete field',
+ },
+ };
+ }
+
+ const remappedField = {
+ id: deletedField.id,
+ documentId: deletedField.documentId,
+ recipientId: deletedField.recipientId ?? -1,
+ type: deletedField.type,
+ pageNumber: deletedField.page,
+ pageX: Number(deletedField.positionX),
+ pageY: Number(deletedField.positionY),
+ pageWidth: Number(deletedField.width),
+ pageHeight: Number(deletedField.height),
+ customText: deletedField.customText,
+ inserted: deletedField.inserted,
+ };
+
+ return {
+ status: 200,
+ body: {
+ ...remappedField,
+ documentId: Number(documentId),
+ },
+ };
+ }),
+});
diff --git a/packages/api/v1/middleware/authenticated.ts b/packages/api/v1/middleware/authenticated.ts
new file mode 100644
index 000000000..dd7f5562e
--- /dev/null
+++ b/packages/api/v1/middleware/authenticated.ts
@@ -0,0 +1,41 @@
+import type { NextApiRequest } from 'next';
+
+import { getApiTokenByToken } from '@documenso/lib/server-only/public-api/get-api-token-by-token';
+import type { Team, User } from '@documenso/prisma/client';
+
+export const authenticatedMiddleware = <
+ T extends {
+ req: NextApiRequest;
+ },
+ R extends {
+ status: number;
+ body: unknown;
+ },
+>(
+ handler: (args: T, user: User, team?: Team | null) => Promise,
+) => {
+ return async (args: T) => {
+ try {
+ const { authorization } = args.req.headers;
+
+ // Support for both "Authorization: Bearer api_xxx" and "Authorization: api_xxx"
+ const [token] = (authorization || '').split('Bearer ').filter((s) => s.length > 0);
+
+ if (!token) {
+ throw new Error('Token was not provided for authenticated middleware');
+ }
+
+ const apiToken = await getApiTokenByToken({ token });
+
+ return await handler(args, apiToken.user, apiToken.team);
+ } catch (_err) {
+ console.log({ _err });
+ return {
+ status: 401,
+ body: {
+ message: 'Unauthorized',
+ },
+ } as const;
+ }
+ };
+};
diff --git a/packages/api/v1/openapi.ts b/packages/api/v1/openapi.ts
new file mode 100644
index 000000000..af0582195
--- /dev/null
+++ b/packages/api/v1/openapi.ts
@@ -0,0 +1,17 @@
+import { generateOpenApi } from '@ts-rest/open-api';
+
+import { ApiContractV1 } from './contract';
+
+export const OpenAPIV1 = generateOpenApi(
+ ApiContractV1,
+ {
+ info: {
+ title: 'Documenso API',
+ version: '1.0.0',
+ description: 'The Documenso API for retrieving, creating, updating and deleting documents.',
+ },
+ },
+ {
+ setOperationId: true,
+ },
+);
diff --git a/packages/api/v1/schema.ts b/packages/api/v1/schema.ts
new file mode 100644
index 000000000..fbe3ba5c1
--- /dev/null
+++ b/packages/api/v1/schema.ts
@@ -0,0 +1,241 @@
+import { z } from 'zod';
+
+import {
+ FieldType,
+ ReadStatus,
+ RecipientRole,
+ SendStatus,
+ SigningStatus,
+} from '@documenso/prisma/client';
+
+/**
+ * Documents
+ */
+export const ZGetDocumentsQuerySchema = z.object({
+ page: z.coerce.number().min(1).optional().default(1),
+ perPage: z.coerce.number().min(1).optional().default(1),
+});
+
+export type TGetDocumentsQuerySchema = z.infer;
+
+export const ZDeleteDocumentMutationSchema = null;
+
+export type TDeleteDocumentMutationSchema = typeof ZDeleteDocumentMutationSchema;
+
+export const ZSuccessfulDocumentResponseSchema = z.object({
+ id: z.number(),
+ userId: z.number(),
+ teamId: z.number().nullish(),
+ title: z.string(),
+ status: z.string(),
+ documentDataId: z.string(),
+ createdAt: z.date(),
+ updatedAt: z.date(),
+ completedAt: z.date().nullable(),
+});
+
+export const ZSuccessfulGetDocumentResponseSchema = ZSuccessfulDocumentResponseSchema.extend({
+ recipients: z.lazy(() => z.array(ZSuccessfulRecipientResponseSchema)),
+});
+
+export type TSuccessfulGetDocumentResponseSchema = z.infer<
+ typeof ZSuccessfulGetDocumentResponseSchema
+>;
+
+export type TSuccessfulDocumentResponseSchema = z.infer;
+
+export const ZSendDocumentForSigningMutationSchema = null;
+
+export type TSendDocumentForSigningMutationSchema = typeof ZSendDocumentForSigningMutationSchema;
+
+export const ZUploadDocumentSuccessfulSchema = z.object({
+ url: z.string(),
+ key: z.string(),
+});
+
+export type TUploadDocumentSuccessfulSchema = z.infer;
+
+export const ZCreateDocumentMutationSchema = z.object({
+ title: z.string().min(1),
+ recipients: z.array(
+ z.object({
+ name: z.string().min(1),
+ email: z.string().email().min(1),
+ role: z.nativeEnum(RecipientRole).optional().default(RecipientRole.SIGNER),
+ }),
+ ),
+ meta: z
+ .object({
+ subject: z.string(),
+ message: z.string(),
+ timezone: z.string(),
+ dateFormat: z.string(),
+ redirectUrl: z.string(),
+ })
+ .partial(),
+});
+
+export type TCreateDocumentMutationSchema = z.infer;
+
+export const ZCreateDocumentMutationResponseSchema = z.object({
+ uploadUrl: z.string().min(1),
+ documentId: z.number(),
+ recipients: z.array(
+ z.object({
+ recipientId: z.number(),
+ token: z.string(),
+ role: z.nativeEnum(RecipientRole),
+ }),
+ ),
+});
+
+export type TCreateDocumentMutationResponseSchema = z.infer<
+ typeof ZCreateDocumentMutationResponseSchema
+>;
+
+export const ZCreateDocumentFromTemplateMutationSchema = z.object({
+ title: z.string().min(1),
+ recipients: z.array(
+ z.object({
+ name: z.string().min(1),
+ email: z.string().email().min(1),
+ role: z.nativeEnum(RecipientRole).optional().default(RecipientRole.SIGNER),
+ }),
+ ),
+ meta: z
+ .object({
+ subject: z.string(),
+ message: z.string(),
+ timezone: z.string(),
+ dateFormat: z.string(),
+ redirectUrl: z.string(),
+ })
+ .partial()
+ .optional(),
+});
+
+export type TCreateDocumentFromTemplateMutationSchema = z.infer<
+ typeof ZCreateDocumentFromTemplateMutationSchema
+>;
+
+export const ZCreateDocumentFromTemplateMutationResponseSchema = z.object({
+ documentId: z.number(),
+ recipients: z.array(
+ z.object({
+ recipientId: z.number(),
+ name: z.string(),
+ email: z.string().email().min(1),
+ token: z.string(),
+ role: z.nativeEnum(RecipientRole).optional().default(RecipientRole.SIGNER),
+ }),
+ ),
+});
+
+export type TCreateDocumentFromTemplateMutationResponseSchema = z.infer<
+ typeof ZCreateDocumentFromTemplateMutationResponseSchema
+>;
+
+export const ZCreateRecipientMutationSchema = z.object({
+ name: z.string().min(1),
+ email: z.string().email().min(1),
+ role: z.nativeEnum(RecipientRole).optional().default(RecipientRole.SIGNER),
+});
+
+/**
+ * Recipients
+ */
+export type TCreateRecipientMutationSchema = z.infer;
+
+export const ZUpdateRecipientMutationSchema = ZCreateRecipientMutationSchema.partial();
+
+export type TUpdateRecipientMutationSchema = z.infer;
+
+export const ZDeleteRecipientMutationSchema = null;
+
+export type TDeleteRecipientMutationSchema = typeof ZDeleteRecipientMutationSchema;
+
+export const ZSuccessfulRecipientResponseSchema = z.object({
+ id: z.number(),
+ // !: This handles the fact that we have null documentId's for templates
+ // !: while we won't need the default we must add it to satisfy typescript
+ documentId: z.number().nullish().default(-1),
+ email: z.string().email().min(1),
+ name: z.string(),
+ role: z.nativeEnum(RecipientRole),
+ token: z.string(),
+ // !: Not used for now
+ // expired: z.string(),
+ signedAt: z.date().nullable(),
+ readStatus: z.nativeEnum(ReadStatus),
+ signingStatus: z.nativeEnum(SigningStatus),
+ sendStatus: z.nativeEnum(SendStatus),
+});
+
+export type TSuccessfulRecipientResponseSchema = z.infer;
+
+/**
+ * Fields
+ */
+export const ZCreateFieldMutationSchema = z.object({
+ recipientId: z.number(),
+ type: z.nativeEnum(FieldType),
+ pageNumber: z.number(),
+ pageX: z.number(),
+ pageY: z.number(),
+ pageWidth: z.number(),
+ pageHeight: z.number(),
+});
+
+export type TCreateFieldMutationSchema = z.infer;
+
+export const ZUpdateFieldMutationSchema = ZCreateFieldMutationSchema.partial();
+
+export type TUpdateFieldMutationSchema = z.infer;
+
+export const ZDeleteFieldMutationSchema = null;
+
+export type TDeleteFieldMutationSchema = typeof ZDeleteFieldMutationSchema;
+
+export const ZSuccessfulFieldResponseSchema = z.object({
+ id: z.number(),
+ documentId: z.number(),
+ recipientId: z.number(),
+ type: z.nativeEnum(FieldType),
+ pageNumber: z.number(),
+ pageX: z.number(),
+ pageY: z.number(),
+ pageWidth: z.number(),
+ pageHeight: z.number(),
+ customText: z.string(),
+ inserted: z.boolean(),
+});
+
+export type TSuccessfulFieldResponseSchema = z.infer;
+
+export const ZSuccessfulResponseSchema = z.object({
+ documents: ZSuccessfulDocumentResponseSchema.array(),
+ totalPages: z.number(),
+});
+
+export type TSuccessfulResponseSchema = z.infer;
+
+export const ZSuccessfulSigningResponseSchema = z.object({
+ message: z.string(),
+});
+
+export type TSuccessfulSigningResponseSchema = z.infer;
+
+/**
+ * General
+ */
+export const ZAuthorizationHeadersSchema = z.object({
+ authorization: z.string(),
+});
+
+export type TAuthorizationHeadersSchema = z.infer;
+
+export const ZUnsuccessfulResponseSchema = z.object({
+ message: z.string(),
+});
+
+export type TUnsuccessfulResponseSchema = z.infer;
diff --git a/packages/app-tests/e2e/templates/manage-templates.spec.ts b/packages/app-tests/e2e/templates/manage-templates.spec.ts
index 53edc705d..f89583097 100644
--- a/packages/app-tests/e2e/templates/manage-templates.spec.ts
+++ b/packages/app-tests/e2e/templates/manage-templates.spec.ts
@@ -107,6 +107,8 @@ test('[TEMPLATES]: delete template', async ({ page }) => {
await page.getByRole('menuitem', { name: 'Delete' }).click();
await page.getByRole('button', { name: 'Delete' }).click();
await expect(page.getByText('Template deleted').first()).toBeVisible();
+
+ await page.waitForTimeout(1000);
}
await unseedTeam(team.url);
@@ -187,15 +189,18 @@ test('[TEMPLATES]: use template', async ({ page }) => {
// Use personal template.
await page.getByRole('button', { name: 'Use Template' }).click();
+ await page.getByRole('button', { name: 'Create Document' }).click();
await page.waitForURL(/documents/);
await page.getByRole('main').getByRole('link', { name: 'Documents' }).click();
await page.waitForURL('/documents');
await expect(page.getByRole('main')).toContainText('Showing 1 result');
await page.goto(`${WEBAPP_BASE_URL}/t/${team.url}/templates`);
+ await page.waitForTimeout(1000);
// Use team template.
await page.getByRole('button', { name: 'Use Template' }).click();
+ await page.getByRole('button', { name: 'Create Document' }).click();
await page.waitForURL(/\/t\/.+\/documents/);
await page.getByRole('main').getByRole('link', { name: 'Documents' }).click();
await page.waitForURL(`/t/${team.url}/documents`);
diff --git a/packages/lib/constants/time.ts b/packages/lib/constants/time.ts
index e2581e14c..9b525b4b0 100644
--- a/packages/lib/constants/time.ts
+++ b/packages/lib/constants/time.ts
@@ -1,5 +1,11 @@
+import { Duration } from 'luxon';
+
export const ONE_SECOND = 1000;
export const ONE_MINUTE = ONE_SECOND * 60;
export const ONE_HOUR = ONE_MINUTE * 60;
export const ONE_DAY = ONE_HOUR * 24;
export const ONE_WEEK = ONE_DAY * 7;
+export const ONE_MONTH = Duration.fromObject({ months: 1 });
+export const THREE_MONTHS = Duration.fromObject({ months: 3 });
+export const SIX_MONTHS = Duration.fromObject({ months: 6 });
+export const ONE_YEAR = Duration.fromObject({ years: 1 });
diff --git a/packages/lib/server-only/2fa/validate-2fa.ts b/packages/lib/server-only/2fa/validate-2fa.ts
index 7fc76a8bb..33141c325 100644
--- a/packages/lib/server-only/2fa/validate-2fa.ts
+++ b/packages/lib/server-only/2fa/validate-2fa.ts
@@ -1,4 +1,4 @@
-import { User } from '@documenso/prisma/client';
+import type { User } from '@documenso/prisma/client';
import { ErrorCode } from '../../next-auth/error-codes';
import { verifyTwoFactorAuthenticationToken } from './verify-2fa-token';
diff --git a/packages/lib/server-only/2fa/verify-2fa-token.ts b/packages/lib/server-only/2fa/verify-2fa-token.ts
index fa9159517..0e8ec6afc 100644
--- a/packages/lib/server-only/2fa/verify-2fa-token.ts
+++ b/packages/lib/server-only/2fa/verify-2fa-token.ts
@@ -1,7 +1,7 @@
import { base32 } from '@scure/base';
import { TOTPController } from 'oslo/otp';
-import { User } from '@documenso/prisma/client';
+import type { User } from '@documenso/prisma/client';
import { DOCUMENSO_ENCRYPTION_KEY } from '../../constants/crypto';
import { symmetricDecrypt } from '../../universal/crypto';
diff --git a/packages/lib/server-only/admin/get-all-documents.ts b/packages/lib/server-only/admin/get-all-documents.ts
index cca1935a3..3c8d1d2ef 100644
--- a/packages/lib/server-only/admin/get-all-documents.ts
+++ b/packages/lib/server-only/admin/get-all-documents.ts
@@ -1,5 +1,5 @@
import { prisma } from '@documenso/prisma';
-import { Prisma } from '@documenso/prisma/client';
+import type { Prisma } from '@documenso/prisma/client';
export interface FindDocumentsOptions {
term?: string;
diff --git a/packages/lib/server-only/admin/update-user.ts b/packages/lib/server-only/admin/update-user.ts
index 9013899a7..a36b1a3d6 100644
--- a/packages/lib/server-only/admin/update-user.ts
+++ b/packages/lib/server-only/admin/update-user.ts
@@ -1,5 +1,5 @@
import { prisma } from '@documenso/prisma';
-import { Role } from '@documenso/prisma/client';
+import type { Role } from '@documenso/prisma/client';
export type UpdateUserOptions = {
id: number;
diff --git a/packages/lib/server-only/auth/hash.ts b/packages/lib/server-only/auth/hash.ts
index df9931c97..bb0b760fe 100644
--- a/packages/lib/server-only/auth/hash.ts
+++ b/packages/lib/server-only/auth/hash.ts
@@ -1,4 +1,5 @@
import { compareSync as bcryptCompareSync, hashSync as bcryptHashSync } from 'bcrypt';
+import crypto from 'crypto';
import { SALT_ROUNDS } from '../../constants/auth';
@@ -12,3 +13,7 @@ export const hashSync = (password: string) => {
export const compareSync = (password: string, hash: string) => {
return bcryptCompareSync(password, hash);
};
+
+export const hashString = (input: string) => {
+ return crypto.createHash('sha512').update(input).digest('hex');
+};
diff --git a/packages/lib/server-only/document/delete-document.ts b/packages/lib/server-only/document/delete-document.ts
index 5974be3f0..b0b1ad682 100644
--- a/packages/lib/server-only/document/delete-document.ts
+++ b/packages/lib/server-only/document/delete-document.ts
@@ -17,41 +17,47 @@ import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
export type DeleteDocumentOptions = {
id: number;
userId: number;
- status: DocumentStatus;
+ teamId?: number;
requestMetadata?: RequestMetadata;
};
export const deleteDocument = async ({
id,
userId,
- status,
+ teamId,
requestMetadata,
}: DeleteDocumentOptions) => {
- await prisma.document.findFirstOrThrow({
+ const document = await prisma.document.findUnique({
where: {
id,
- OR: [
- {
- userId,
- },
- {
- team: {
- members: {
- some: {
- userId,
+ ...(teamId
+ ? {
+ team: {
+ id: teamId,
+ members: {
+ some: {
+ userId,
+ },
},
},
- },
- },
- ],
+ }
+ : {
+ userId,
+ teamId: null,
+ }),
+ },
+ include: {
+ Recipient: true,
+ documentMeta: true,
+ User: true,
},
});
- const user = await prisma.user.findFirstOrThrow({
- where: {
- id: userId,
- },
- });
+ if (!document) {
+ throw new Error('Document not found');
+ }
+
+ const { status, User: user } = document;
// if the document is a draft, hard-delete
if (status === DocumentStatus.DRAFT) {
@@ -75,51 +81,33 @@ export const deleteDocument = async ({
}
// if the document is pending, send cancellation emails to all recipients
- if (status === DocumentStatus.PENDING) {
- const document = await prisma.document.findUnique({
- where: {
- id,
- status,
- userId,
- },
- include: {
- Recipient: true,
- documentMeta: true,
- },
- });
+ if (status === DocumentStatus.PENDING && document.Recipient.length > 0) {
+ await Promise.all(
+ document.Recipient.map(async (recipient) => {
+ const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000';
- if (!document) {
- throw new Error('Document not found');
- }
+ const template = createElement(DocumentCancelTemplate, {
+ documentName: document.title,
+ inviterName: user.name || undefined,
+ inviterEmail: user.email,
+ assetBaseUrl,
+ });
- if (document.Recipient.length > 0) {
- await Promise.all(
- document.Recipient.map(async (recipient) => {
- const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000';
-
- const template = createElement(DocumentCancelTemplate, {
- documentName: document.title,
- inviterName: user.name || undefined,
- inviterEmail: user.email,
- assetBaseUrl,
- });
-
- await mailer.sendMail({
- to: {
- address: recipient.email,
- name: recipient.name,
- },
- from: {
- name: FROM_NAME,
- address: FROM_ADDRESS,
- },
- subject: 'Document Cancelled',
- html: render(template),
- text: render(template, { plainText: true }),
- });
- }),
- );
- }
+ await mailer.sendMail({
+ to: {
+ address: recipient.email,
+ name: recipient.name,
+ },
+ from: {
+ name: FROM_NAME,
+ address: FROM_ADDRESS,
+ },
+ subject: 'Document Cancelled',
+ html: render(template),
+ text: render(template, { plainText: true }),
+ });
+ }),
+ );
}
// If the document is not a draft, only soft-delete.
diff --git a/packages/lib/server-only/document/send-document.tsx b/packages/lib/server-only/document/send-document.tsx
index 0fe53c798..8bb62b6dd 100644
--- a/packages/lib/server-only/document/send-document.tsx
+++ b/packages/lib/server-only/document/send-document.tsx
@@ -20,12 +20,14 @@ import {
export type SendDocumentOptions = {
documentId: number;
userId: number;
+ teamId?: number;
requestMetadata?: RequestMetadata;
};
export const sendDocument = async ({
documentId,
userId,
+ teamId,
requestMetadata,
}: SendDocumentOptions) => {
const user = await prisma.user.findFirstOrThrow({
@@ -42,20 +44,21 @@ export const sendDocument = async ({
const document = await prisma.document.findUnique({
where: {
id: documentId,
- OR: [
- {
- userId,
- },
- {
- team: {
- members: {
- some: {
- userId,
+ ...(teamId
+ ? {
+ team: {
+ id: teamId,
+ members: {
+ some: {
+ userId,
+ },
},
},
- },
- },
- ],
+ }
+ : {
+ userId,
+ teamId: null,
+ }),
},
include: {
Recipient: true,
diff --git a/packages/lib/server-only/document/update-document.ts b/packages/lib/server-only/document/update-document.ts
index 29ab2c998..3e35f52e4 100644
--- a/packages/lib/server-only/document/update-document.ts
+++ b/packages/lib/server-only/document/update-document.ts
@@ -5,16 +5,36 @@ import type { Prisma } from '@prisma/client';
import { prisma } from '@documenso/prisma';
export type UpdateDocumentOptions = {
+ documentId: number;
data: Prisma.DocumentUpdateInput;
userId: number;
- documentId: number;
+ teamId?: number;
};
-export const updateDocument = async ({ documentId, userId, data }: UpdateDocumentOptions) => {
+export const updateDocument = async ({
+ documentId,
+ userId,
+ teamId,
+ data,
+}: UpdateDocumentOptions) => {
return await prisma.document.update({
where: {
id: documentId,
- userId,
+ ...(teamId
+ ? {
+ team: {
+ id: teamId,
+ members: {
+ some: {
+ userId,
+ },
+ },
+ },
+ }
+ : {
+ userId,
+ teamId: null,
+ }),
},
data: {
...data,
diff --git a/packages/lib/server-only/document/update-title.ts b/packages/lib/server-only/document/update-title.ts
index f7f7a6b88..43e7c2d91 100644
--- a/packages/lib/server-only/document/update-title.ts
+++ b/packages/lib/server-only/document/update-title.ts
@@ -7,6 +7,7 @@ import { prisma } from '@documenso/prisma';
export type UpdateTitleOptions = {
userId: number;
+ teamId?: number;
documentId: number;
title: string;
requestMetadata?: RequestMetadata;
@@ -14,6 +15,7 @@ export type UpdateTitleOptions = {
export const updateTitle = async ({
userId,
+ teamId,
documentId,
title,
requestMetadata,
@@ -27,20 +29,21 @@ export const updateTitle = async ({
const document = await prisma.document.findFirstOrThrow({
where: {
id: documentId,
- OR: [
- {
- userId,
- },
- {
- team: {
- members: {
- some: {
- userId,
+ ...(teamId
+ ? {
+ team: {
+ id: teamId,
+ members: {
+ some: {
+ userId,
+ },
},
},
- },
- },
- ],
+ }
+ : {
+ userId,
+ teamId: null,
+ }),
},
});
diff --git a/packages/lib/server-only/field/create-field.ts b/packages/lib/server-only/field/create-field.ts
new file mode 100644
index 000000000..7a3aa3959
--- /dev/null
+++ b/packages/lib/server-only/field/create-field.ts
@@ -0,0 +1,126 @@
+import { prisma } from '@documenso/prisma';
+import type { FieldType, Team } from '@documenso/prisma/client';
+
+import type { RequestMetadata } from '../../universal/extract-request-metadata';
+import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
+
+export type CreateFieldOptions = {
+ documentId: number;
+ userId: number;
+ teamId?: number;
+ recipientId: number;
+ type: FieldType;
+ pageNumber: number;
+ pageX: number;
+ pageY: number;
+ pageWidth: number;
+ pageHeight: number;
+ requestMetadata?: RequestMetadata;
+};
+
+export const createField = async ({
+ documentId,
+ userId,
+ teamId,
+ recipientId,
+ type,
+ pageNumber,
+ pageX,
+ pageY,
+ pageWidth,
+ pageHeight,
+ requestMetadata,
+}: CreateFieldOptions) => {
+ const document = await prisma.document.findFirst({
+ select: {
+ id: true,
+ },
+ where: {
+ id: documentId,
+ ...(teamId
+ ? {
+ team: {
+ id: teamId,
+ members: {
+ some: {
+ userId,
+ },
+ },
+ },
+ }
+ : {
+ userId,
+ teamId: null,
+ }),
+ },
+ });
+
+ if (!document) {
+ throw new Error('Document not found');
+ }
+
+ const user = await prisma.user.findFirstOrThrow({
+ where: {
+ id: userId,
+ },
+ select: {
+ id: true,
+ name: true,
+ email: true,
+ },
+ });
+
+ let team: Team | null = null;
+
+ if (teamId) {
+ team = await prisma.team.findFirst({
+ where: {
+ id: teamId,
+ members: {
+ some: {
+ userId,
+ },
+ },
+ },
+ });
+ }
+
+ const field = await prisma.field.create({
+ data: {
+ documentId,
+ recipientId,
+ type,
+ page: pageNumber,
+ positionX: pageX,
+ positionY: pageY,
+ width: pageWidth,
+ height: pageHeight,
+ customText: '',
+ inserted: false,
+ },
+ include: {
+ Recipient: true,
+ },
+ });
+
+ await prisma.documentAuditLog.create({
+ data: createDocumentAuditLogData({
+ type: 'FIELD_CREATED',
+ documentId,
+ user: {
+ id: team?.id ?? user.id,
+ email: team?.name ?? user.email,
+ name: team ? '' : user.name,
+ },
+ data: {
+ fieldId: field.secondaryId,
+ fieldRecipientEmail: field.Recipient?.email ?? '',
+ fieldRecipientId: recipientId,
+ fieldType: field.type,
+ },
+ requestMetadata,
+ }),
+ });
+
+ return field;
+};
diff --git a/packages/lib/server-only/field/delete-field.ts b/packages/lib/server-only/field/delete-field.ts
new file mode 100644
index 000000000..67145de10
--- /dev/null
+++ b/packages/lib/server-only/field/delete-field.ts
@@ -0,0 +1,90 @@
+import { prisma } from '@documenso/prisma';
+import type { Team } from '@documenso/prisma/client';
+
+import type { RequestMetadata } from '../../universal/extract-request-metadata';
+import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
+
+export type DeleteFieldOptions = {
+ fieldId: number;
+ documentId: number;
+ userId: number;
+ teamId?: number;
+ requestMetadata?: RequestMetadata;
+};
+
+export const deleteField = async ({
+ fieldId,
+ userId,
+ teamId,
+ documentId,
+ requestMetadata,
+}: DeleteFieldOptions) => {
+ const field = await prisma.field.delete({
+ where: {
+ id: fieldId,
+ Document: {
+ id: documentId,
+ ...(teamId
+ ? {
+ team: {
+ id: teamId,
+ members: {
+ some: {
+ userId,
+ },
+ },
+ },
+ }
+ : {
+ userId,
+ teamId: null,
+ }),
+ },
+ },
+ include: {
+ Recipient: true,
+ },
+ });
+
+ const user = await prisma.user.findFirstOrThrow({
+ where: {
+ id: userId,
+ },
+ select: {
+ id: true,
+ name: true,
+ email: true,
+ },
+ });
+
+ let team: Team | null = null;
+
+ if (teamId) {
+ team = await prisma.team.findFirstOrThrow({
+ where: {
+ id: teamId,
+ },
+ });
+ }
+
+ await prisma.documentAuditLog.create({
+ data: createDocumentAuditLogData({
+ type: 'FIELD_DELETED',
+ documentId,
+ user: {
+ id: team?.id ?? user.id,
+ email: team?.name ?? user.email,
+ name: team ? '' : user.name,
+ },
+ data: {
+ fieldId: field.secondaryId,
+ fieldRecipientEmail: field.Recipient?.email ?? '',
+ fieldRecipientId: field.recipientId ?? -1,
+ fieldType: field.type,
+ },
+ requestMetadata,
+ }),
+ });
+
+ return field;
+};
diff --git a/packages/lib/server-only/field/get-field-by-id.ts b/packages/lib/server-only/field/get-field-by-id.ts
new file mode 100644
index 000000000..0e0f9b2dd
--- /dev/null
+++ b/packages/lib/server-only/field/get-field-by-id.ts
@@ -0,0 +1,17 @@
+import { prisma } from '@documenso/prisma';
+
+export type GetFieldByIdOptions = {
+ fieldId: number;
+ documentId: number;
+};
+
+export const getFieldById = async ({ fieldId, documentId }: GetFieldByIdOptions) => {
+ const field = await prisma.field.findFirst({
+ where: {
+ id: fieldId,
+ documentId,
+ },
+ });
+
+ return field;
+};
diff --git a/packages/lib/server-only/field/update-field.ts b/packages/lib/server-only/field/update-field.ts
new file mode 100644
index 000000000..b59760cd2
--- /dev/null
+++ b/packages/lib/server-only/field/update-field.ts
@@ -0,0 +1,122 @@
+import { prisma } from '@documenso/prisma';
+import type { FieldType, Team } from '@documenso/prisma/client';
+
+import type { RequestMetadata } from '../../universal/extract-request-metadata';
+import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
+
+export type UpdateFieldOptions = {
+ fieldId: number;
+ documentId: number;
+ userId: number;
+ teamId?: number;
+ recipientId?: number;
+ type?: FieldType;
+ pageNumber?: number;
+ pageX?: number;
+ pageY?: number;
+ pageWidth?: number;
+ pageHeight?: number;
+ requestMetadata?: RequestMetadata;
+};
+
+export const updateField = async ({
+ fieldId,
+ documentId,
+ userId,
+ teamId,
+ recipientId,
+ type,
+ pageNumber,
+ pageX,
+ pageY,
+ pageWidth,
+ pageHeight,
+ requestMetadata,
+}: UpdateFieldOptions) => {
+ const field = await prisma.field.update({
+ where: {
+ id: fieldId,
+ Document: {
+ id: documentId,
+ ...(teamId
+ ? {
+ team: {
+ id: teamId,
+ members: {
+ some: {
+ userId,
+ },
+ },
+ },
+ }
+ : {
+ userId,
+ teamId: null,
+ }),
+ },
+ },
+ data: {
+ recipientId,
+ type,
+ page: pageNumber,
+ positionX: pageX,
+ positionY: pageY,
+ width: pageWidth,
+ height: pageHeight,
+ },
+ include: {
+ Recipient: true,
+ },
+ });
+
+ if (!field) {
+ throw new Error('Field not found');
+ }
+
+ const user = await prisma.user.findFirstOrThrow({
+ where: {
+ id: userId,
+ },
+ select: {
+ id: true,
+ name: true,
+ email: true,
+ },
+ });
+
+ let team: Team | null = null;
+
+ if (teamId) {
+ team = await prisma.team.findFirst({
+ where: {
+ id: teamId,
+ members: {
+ some: {
+ userId,
+ },
+ },
+ },
+ });
+ }
+
+ await prisma.documentAuditLog.create({
+ data: createDocumentAuditLogData({
+ type: 'FIELD_UPDATED',
+ documentId,
+ user: {
+ id: team?.id ?? user.id,
+ email: team?.name ?? user.email,
+ name: team ? '' : user.name,
+ },
+ data: {
+ fieldId: field.secondaryId,
+ fieldRecipientEmail: field.Recipient?.email ?? '',
+ fieldRecipientId: recipientId ?? -1,
+ fieldType: field.type,
+ },
+ requestMetadata,
+ }),
+ });
+
+ return field;
+};
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 dde46ba6b..1aabf96b8 100644
--- a/packages/lib/server-only/pdf/insert-field-in-pdf.ts
+++ b/packages/lib/server-only/pdf/insert-field-in-pdf.ts
@@ -1,3 +1,4 @@
+// https://github.com/Hopding/pdf-lib/issues/20#issuecomment-412852821
import fontkit from '@pdf-lib/fontkit';
import { PDFDocument, StandardFonts } from 'pdf-lib';
@@ -73,13 +74,17 @@ export const insertFieldInPDF = async (pdf: PDFDocument, field: FieldWithSignatu
height: imageHeight,
});
} else {
- let textWidth = font.widthOfTextAtSize(field.customText, fontSize);
+ const longestLineInTextForWidth = field.customText
+ .split('\n')
+ .sort((a, b) => b.length - a.length)[0];
+
+ let textWidth = font.widthOfTextAtSize(longestLineInTextForWidth, fontSize);
const textHeight = font.heightAtSize(fontSize);
const scalingFactor = Math.min(fieldWidth / textWidth, fieldHeight / textHeight, 1);
fontSize = Math.max(Math.min(fontSize * scalingFactor, maxFontSize), minFontSize);
- textWidth = font.widthOfTextAtSize(field.customText, fontSize);
+ textWidth = font.widthOfTextAtSize(longestLineInTextForWidth, fontSize);
const textX = fieldX + (fieldWidth - textWidth) / 2;
let textY = fieldY + (fieldHeight - textHeight) / 2;
diff --git a/packages/lib/server-only/public-api/create-api-token.ts b/packages/lib/server-only/public-api/create-api-token.ts
new file mode 100644
index 000000000..ee6927878
--- /dev/null
+++ b/packages/lib/server-only/public-api/create-api-token.ts
@@ -0,0 +1,67 @@
+import type { Duration } from 'luxon';
+import { DateTime } from 'luxon';
+
+import { prisma } from '@documenso/prisma';
+import { TeamMemberRole } from '@documenso/prisma/client';
+
+// temporary choice for testing only
+import * as timeConstants from '../../constants/time';
+import { alphaid } from '../../universal/id';
+import { hashString } from '../auth/hash';
+
+type TimeConstants = typeof timeConstants & {
+ [key: string]: number | Duration;
+};
+
+type CreateApiTokenInput = {
+ userId: number;
+ teamId?: number;
+ tokenName: string;
+ expiresIn: string | null;
+};
+
+export const createApiToken = async ({
+ userId,
+ teamId,
+ tokenName,
+ expiresIn,
+}: CreateApiTokenInput) => {
+ const apiToken = `api_${alphaid(16)}`;
+
+ const hashedToken = hashString(apiToken);
+
+ const timeConstantsRecords: TimeConstants = timeConstants;
+
+ if (teamId) {
+ const member = await prisma.teamMember.findFirst({
+ where: {
+ userId,
+ teamId,
+ role: TeamMemberRole.ADMIN,
+ },
+ });
+
+ if (!member) {
+ throw new Error('You do not have permission to create a token for this team');
+ }
+ }
+
+ const storedToken = await prisma.apiToken.create({
+ data: {
+ name: tokenName,
+ token: hashedToken,
+ expires: expiresIn ? DateTime.now().plus(timeConstantsRecords[expiresIn]).toJSDate() : null,
+ userId: teamId ? null : userId,
+ teamId,
+ },
+ });
+
+ if (!storedToken) {
+ throw new Error('Failed to create the API token');
+ }
+
+ return {
+ id: storedToken.id,
+ token: apiToken,
+ };
+};
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
new file mode 100644
index 000000000..75d7fc385
--- /dev/null
+++ b/packages/lib/server-only/public-api/delete-api-token-by-id.ts
@@ -0,0 +1,32 @@
+import { prisma } from '@documenso/prisma';
+import { TeamMemberRole } from '@documenso/prisma/client';
+
+export type DeleteTokenByIdOptions = {
+ id: number;
+ userId: number;
+ teamId?: number;
+};
+
+export const deleteTokenById = async ({ id, userId, teamId }: DeleteTokenByIdOptions) => {
+ if (teamId) {
+ const member = await prisma.teamMember.findFirst({
+ where: {
+ userId,
+ teamId,
+ role: TeamMemberRole.ADMIN,
+ },
+ });
+
+ if (!member) {
+ throw new Error('You do not have permission to delete this token');
+ }
+ }
+
+ return await prisma.apiToken.delete({
+ where: {
+ id,
+ userId: teamId ? null : userId,
+ teamId,
+ },
+ });
+};
diff --git a/packages/lib/server-only/public-api/get-all-team-tokens.ts b/packages/lib/server-only/public-api/get-all-team-tokens.ts
new file mode 100644
index 000000000..86c13ed1d
--- /dev/null
+++ b/packages/lib/server-only/public-api/get-all-team-tokens.ts
@@ -0,0 +1,36 @@
+import { prisma } from '@documenso/prisma';
+import { TeamMemberRole } from '@documenso/prisma/client';
+
+export type GetUserTokensOptions = {
+ userId: number;
+ teamId: number;
+};
+
+export const getTeamTokens = async ({ userId, teamId }: GetUserTokensOptions) => {
+ const teamMember = await prisma.teamMember.findFirst({
+ where: {
+ userId,
+ teamId,
+ },
+ });
+
+ if (teamMember?.role !== TeamMemberRole.ADMIN) {
+ throw new Error('You do not have permission to view tokens for this team');
+ }
+
+ return await prisma.apiToken.findMany({
+ where: {
+ teamId,
+ },
+ select: {
+ id: true,
+ name: true,
+ algorithm: true,
+ createdAt: true,
+ expires: true,
+ },
+ orderBy: {
+ createdAt: 'desc',
+ },
+ });
+};
diff --git a/packages/lib/server-only/public-api/get-all-user-tokens.ts b/packages/lib/server-only/public-api/get-all-user-tokens.ts
new file mode 100644
index 000000000..1ba31a6cf
--- /dev/null
+++ b/packages/lib/server-only/public-api/get-all-user-tokens.ts
@@ -0,0 +1,23 @@
+import { prisma } from '@documenso/prisma';
+
+export type GetUserTokensOptions = {
+ userId: number;
+};
+
+export const getUserTokens = async ({ userId }: GetUserTokensOptions) => {
+ return await prisma.apiToken.findMany({
+ where: {
+ userId,
+ },
+ select: {
+ id: true,
+ name: true,
+ algorithm: true,
+ createdAt: true,
+ expires: true,
+ },
+ orderBy: {
+ createdAt: 'desc',
+ },
+ });
+};
diff --git a/packages/lib/server-only/public-api/get-api-token-by-id.ts b/packages/lib/server-only/public-api/get-api-token-by-id.ts
new file mode 100644
index 000000000..8b25717f9
--- /dev/null
+++ b/packages/lib/server-only/public-api/get-api-token-by-id.ts
@@ -0,0 +1,15 @@
+import { prisma } from '@documenso/prisma';
+
+export type GetApiTokenByIdOptions = {
+ id: number;
+ userId: number;
+};
+
+export const getApiTokenById = async ({ id, userId }: GetApiTokenByIdOptions) => {
+ return await prisma.apiToken.findFirstOrThrow({
+ where: {
+ id,
+ userId,
+ },
+ });
+};
diff --git a/packages/lib/server-only/public-api/get-api-token-by-token.ts b/packages/lib/server-only/public-api/get-api-token-by-token.ts
new file mode 100644
index 000000000..cc73c8bd9
--- /dev/null
+++ b/packages/lib/server-only/public-api/get-api-token-by-token.ts
@@ -0,0 +1,41 @@
+import { prisma } from '@documenso/prisma';
+
+import { hashString } from '../auth/hash';
+
+export const getApiTokenByToken = async ({ token }: { token: string }) => {
+ const hashedToken = hashString(token);
+
+ const apiToken = await prisma.apiToken.findFirst({
+ where: {
+ token: hashedToken,
+ },
+ include: {
+ team: true,
+ user: true,
+ },
+ });
+
+ if (!apiToken) {
+ throw new Error('Invalid token');
+ }
+
+ if (apiToken.expires && apiToken.expires < new Date()) {
+ throw new Error('Expired token');
+ }
+
+ if (apiToken.team) {
+ apiToken.user = await prisma.user.findFirst({
+ where: {
+ id: apiToken.team.ownerUserId,
+ },
+ });
+ }
+
+ const { user } = apiToken;
+
+ if (!user) {
+ throw new Error('Invalid token');
+ }
+
+ return { ...apiToken, user };
+};
diff --git a/packages/lib/server-only/recipient/delete-recipient.ts b/packages/lib/server-only/recipient/delete-recipient.ts
new file mode 100644
index 000000000..74fb4a8d2
--- /dev/null
+++ b/packages/lib/server-only/recipient/delete-recipient.ts
@@ -0,0 +1,106 @@
+import { prisma } from '@documenso/prisma';
+import type { Team } from '@documenso/prisma/client';
+import { SendStatus } from '@documenso/prisma/client';
+
+import type { RequestMetadata } from '../../universal/extract-request-metadata';
+import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
+
+export type DeleteRecipientOptions = {
+ documentId: number;
+ recipientId: number;
+ userId: number;
+ teamId?: number;
+ requestMetadata?: RequestMetadata;
+};
+
+export const deleteRecipient = async ({
+ documentId,
+ recipientId,
+ userId,
+ teamId,
+ requestMetadata,
+}: DeleteRecipientOptions) => {
+ const recipient = await prisma.recipient.findFirst({
+ where: {
+ id: recipientId,
+ Document: {
+ id: documentId,
+ ...(teamId
+ ? {
+ team: {
+ id: teamId,
+ members: {
+ some: {
+ userId,
+ },
+ },
+ },
+ }
+ : {
+ userId,
+ teamId: null,
+ }),
+ },
+ },
+ });
+
+ if (!recipient) {
+ throw new Error('Recipient not found');
+ }
+
+ if (recipient.sendStatus !== SendStatus.NOT_SENT) {
+ throw new Error('Can not delete a recipient that has already been sent a document');
+ }
+
+ const user = await prisma.user.findFirstOrThrow({
+ where: {
+ id: userId,
+ },
+ });
+
+ let team: Team | null = null;
+
+ if (teamId) {
+ team = await prisma.team.findFirst({
+ where: {
+ id: teamId,
+ members: {
+ some: {
+ userId,
+ },
+ },
+ },
+ });
+ }
+
+ const deletedRecipient = await prisma.$transaction(async (tx) => {
+ const deleted = await tx.recipient.delete({
+ where: {
+ id: recipient.id,
+ },
+ });
+
+ await tx.documentAuditLog.create({
+ data: createDocumentAuditLogData({
+ type: 'RECIPIENT_DELETED',
+ documentId,
+ user: {
+ id: team?.id ?? user.id,
+ email: team?.name ?? user.email,
+ name: team ? '' : user.name,
+ },
+ data: {
+ recipientEmail: recipient.email,
+ recipientName: recipient.name,
+ recipientId: recipient.id,
+ recipientRole: recipient.role,
+ },
+ requestMetadata,
+ }),
+ });
+
+ return deleted;
+ });
+
+ return deletedRecipient;
+};
diff --git a/packages/lib/server-only/recipient/get-recipient-by-email.ts b/packages/lib/server-only/recipient/get-recipient-by-email.ts
new file mode 100644
index 000000000..349149105
--- /dev/null
+++ b/packages/lib/server-only/recipient/get-recipient-by-email.ts
@@ -0,0 +1,21 @@
+import { prisma } from '@documenso/prisma';
+
+export type GetRecipientByEmailOptions = {
+ documentId: number;
+ email: string;
+};
+
+export const getRecipientByEmail = async ({ documentId, email }: GetRecipientByEmailOptions) => {
+ const recipient = await prisma.recipient.findFirst({
+ where: {
+ documentId,
+ email: email.toLowerCase(),
+ },
+ });
+
+ if (!recipient) {
+ throw new Error('Recipient not found');
+ }
+
+ return recipient;
+};
diff --git a/packages/lib/server-only/recipient/get-recipient-by-id.ts b/packages/lib/server-only/recipient/get-recipient-by-id.ts
new file mode 100644
index 000000000..0db306b80
--- /dev/null
+++ b/packages/lib/server-only/recipient/get-recipient-by-id.ts
@@ -0,0 +1,21 @@
+import { prisma } from '@documenso/prisma';
+
+export type GetRecipientByIdOptions = {
+ id: number;
+ documentId: number;
+};
+
+export const getRecipientById = async ({ documentId, id }: GetRecipientByIdOptions) => {
+ const recipient = await prisma.recipient.findFirst({
+ where: {
+ documentId,
+ id,
+ },
+ });
+
+ if (!recipient) {
+ throw new Error('Recipient not found');
+ }
+
+ return recipient;
+};
diff --git a/packages/lib/server-only/recipient/get-recipients-for-document.ts b/packages/lib/server-only/recipient/get-recipients-for-document.ts
index 80e408acc..03bc0e6c8 100644
--- a/packages/lib/server-only/recipient/get-recipients-for-document.ts
+++ b/packages/lib/server-only/recipient/get-recipients-for-document.ts
@@ -3,11 +3,13 @@ import { prisma } from '@documenso/prisma';
export interface GetRecipientsForDocumentOptions {
documentId: number;
userId: number;
+ teamId?: number;
}
export const getRecipientsForDocument = async ({
documentId,
userId,
+ teamId,
}: GetRecipientsForDocumentOptions) => {
const recipients = await prisma.recipient.findMany({
where: {
@@ -18,6 +20,7 @@ export const getRecipientsForDocument = async ({
userId,
},
{
+ teamId,
team: {
members: {
some: {
diff --git a/packages/lib/server-only/recipient/set-recipients-for-document.ts b/packages/lib/server-only/recipient/set-recipients-for-document.ts
index b18ea6420..2505e5261 100644
--- a/packages/lib/server-only/recipient/set-recipients-for-document.ts
+++ b/packages/lib/server-only/recipient/set-recipients-for-document.ts
@@ -11,6 +11,7 @@ import { SendStatus, SigningStatus } from '@documenso/prisma/client';
export interface SetRecipientsForDocumentOptions {
userId: number;
+ teamId?: number;
documentId: number;
recipients: {
id?: number | null;
@@ -23,6 +24,7 @@ export interface SetRecipientsForDocumentOptions {
export const setRecipientsForDocument = async ({
userId,
+ teamId,
documentId,
recipients,
requestMetadata,
@@ -30,20 +32,21 @@ export const setRecipientsForDocument = async ({
const document = await prisma.document.findFirst({
where: {
id: documentId,
- OR: [
- {
- userId,
- },
- {
- team: {
- members: {
- some: {
- userId,
+ ...(teamId
+ ? {
+ team: {
+ id: teamId,
+ members: {
+ some: {
+ userId,
+ },
},
},
- },
- },
- ],
+ }
+ : {
+ userId,
+ teamId: null,
+ }),
},
});
@@ -106,7 +109,7 @@ export const setRecipientsForDocument = async ({
});
const persistedRecipients = await prisma.$transaction(async (tx) => {
- await Promise.all(
+ return await Promise.all(
linkedRecipients.map(async (recipient) => {
const upsertedRecipient = await tx.recipient.upsert({
where: {
diff --git a/packages/lib/server-only/recipient/set-recipients-for-template.ts b/packages/lib/server-only/recipient/set-recipients-for-template.ts
index 7c96bcf44..5315711a5 100644
--- a/packages/lib/server-only/recipient/set-recipients-for-template.ts
+++ b/packages/lib/server-only/recipient/set-recipients-for-template.ts
@@ -1,4 +1,5 @@
import { prisma } from '@documenso/prisma';
+import type { RecipientRole } from '@documenso/prisma/client';
import { nanoid } from '../../universal/id';
@@ -9,6 +10,7 @@ export type SetRecipientsForTemplateOptions = {
id?: number;
email: string;
name: string;
+ role: RecipientRole;
}[];
};
@@ -84,11 +86,13 @@ export const setRecipientsForTemplate = async ({
update: {
name: recipient.name,
email: recipient.email,
+ role: recipient.role,
templateId,
},
create: {
name: recipient.name,
email: recipient.email,
+ role: recipient.role,
token: nanoid(),
templateId,
},
diff --git a/packages/lib/server-only/recipient/update-recipient.ts b/packages/lib/server-only/recipient/update-recipient.ts
new file mode 100644
index 000000000..6d7d6c7e8
--- /dev/null
+++ b/packages/lib/server-only/recipient/update-recipient.ts
@@ -0,0 +1,118 @@
+import { prisma } from '@documenso/prisma';
+import type { RecipientRole, Team } from '@documenso/prisma/client';
+
+import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs';
+import type { RequestMetadata } from '../../universal/extract-request-metadata';
+import { createDocumentAuditLogData, diffRecipientChanges } from '../../utils/document-audit-logs';
+
+export type UpdateRecipientOptions = {
+ documentId: number;
+ recipientId: number;
+ email?: string;
+ name?: string;
+ role?: RecipientRole;
+ userId: number;
+ teamId?: number;
+ requestMetadata?: RequestMetadata;
+};
+
+export const updateRecipient = async ({
+ documentId,
+ recipientId,
+ email,
+ name,
+ role,
+ userId,
+ teamId,
+ requestMetadata,
+}: UpdateRecipientOptions) => {
+ const recipient = await prisma.recipient.findFirst({
+ where: {
+ id: recipientId,
+ Document: {
+ id: documentId,
+ ...(teamId
+ ? {
+ team: {
+ id: teamId,
+ members: {
+ some: {
+ userId,
+ },
+ },
+ },
+ }
+ : {
+ userId,
+ teamId: null,
+ }),
+ },
+ },
+ });
+
+ let team: Team | null = null;
+
+ if (teamId) {
+ team = await prisma.team.findFirst({
+ where: {
+ id: teamId,
+ members: {
+ some: {
+ userId,
+ },
+ },
+ },
+ });
+ }
+
+ const user = await prisma.user.findFirstOrThrow({
+ where: {
+ id: userId,
+ },
+ });
+
+ if (!recipient) {
+ throw new Error('Recipient not found');
+ }
+
+ const updatedRecipient = await prisma.$transaction(async (tx) => {
+ const persisted = await prisma.recipient.update({
+ where: {
+ id: recipient.id,
+ },
+ data: {
+ email: email?.toLowerCase() ?? recipient.email,
+ name: name ?? recipient.name,
+ role: role ?? recipient.role,
+ },
+ });
+
+ const changes = diffRecipientChanges(recipient, persisted);
+
+ if (changes.length > 0) {
+ await tx.documentAuditLog.create({
+ data: createDocumentAuditLogData({
+ type: DOCUMENT_AUDIT_LOG_TYPE.RECIPIENT_UPDATED,
+ documentId: documentId,
+ user: {
+ id: team?.id ?? user.id,
+ name: team?.name ?? user.name,
+ email: team ? '' : user.email,
+ },
+ requestMetadata,
+ data: {
+ changes,
+ recipientId,
+ recipientEmail: persisted.email,
+ recipientName: persisted.name,
+ recipientRole: persisted.role,
+ },
+ }),
+ });
+
+ return persisted;
+ }
+ });
+
+ return updatedRecipient;
+};
diff --git a/packages/lib/server-only/site-settings/get-site-settings.ts b/packages/lib/server-only/site-settings/get-site-settings.ts
new file mode 100644
index 000000000..b0db542b8
--- /dev/null
+++ b/packages/lib/server-only/site-settings/get-site-settings.ts
@@ -0,0 +1,9 @@
+import { prisma } from '@documenso/prisma';
+
+import { ZSiteSettingsSchema } from './schema';
+
+export const getSiteSettings = async () => {
+ const settings = await prisma.siteSettings.findMany();
+
+ return ZSiteSettingsSchema.parse(settings);
+};
diff --git a/packages/lib/server-only/site-settings/schema.ts b/packages/lib/server-only/site-settings/schema.ts
new file mode 100644
index 000000000..ff6194219
--- /dev/null
+++ b/packages/lib/server-only/site-settings/schema.ts
@@ -0,0 +1,12 @@
+import { z } from 'zod';
+
+import { ZSiteSettingsBannerSchema } from './schemas/banner';
+
+// TODO: Use `z.union([...])` once we have more than one setting
+export const ZSiteSettingSchema = ZSiteSettingsBannerSchema;
+
+export type TSiteSettingSchema = z.infer;
+
+export const ZSiteSettingsSchema = z.array(ZSiteSettingSchema);
+
+export type TSiteSettingsSchema = z.infer;
diff --git a/packages/lib/server-only/site-settings/schemas/_base.ts b/packages/lib/server-only/site-settings/schemas/_base.ts
new file mode 100644
index 000000000..95cb93987
--- /dev/null
+++ b/packages/lib/server-only/site-settings/schemas/_base.ts
@@ -0,0 +1,9 @@
+import { z } from 'zod';
+
+export const ZSiteSettingsBaseSchema = z.object({
+ id: z.string().min(1),
+ enabled: z.boolean(),
+ data: z.never(),
+});
+
+export type TSiteSettingsBaseSchema = z.infer;
diff --git a/packages/lib/server-only/site-settings/schemas/banner.ts b/packages/lib/server-only/site-settings/schemas/banner.ts
new file mode 100644
index 000000000..5971192cc
--- /dev/null
+++ b/packages/lib/server-only/site-settings/schemas/banner.ts
@@ -0,0 +1,23 @@
+import { z } from 'zod';
+
+import { ZSiteSettingsBaseSchema } from './_base';
+
+export const SITE_SETTINGS_BANNER_ID = 'site.banner';
+
+export const ZSiteSettingsBannerSchema = ZSiteSettingsBaseSchema.extend({
+ id: z.literal(SITE_SETTINGS_BANNER_ID),
+ data: z
+ .object({
+ content: z.string(),
+ bgColor: z.string(),
+ textColor: z.string(),
+ })
+ .optional()
+ .default({
+ content: '',
+ bgColor: '#000000',
+ textColor: '#FFFFFF',
+ }),
+});
+
+export type TSiteSettingsBannerSchema = z.infer;
diff --git a/packages/lib/server-only/site-settings/upsert-site-setting.ts b/packages/lib/server-only/site-settings/upsert-site-setting.ts
new file mode 100644
index 000000000..6fc59b1d1
--- /dev/null
+++ b/packages/lib/server-only/site-settings/upsert-site-setting.ts
@@ -0,0 +1,33 @@
+import { prisma } from '@documenso/prisma';
+
+import type { TSiteSettingSchema } from './schema';
+
+export type UpsertSiteSettingOptions = TSiteSettingSchema & {
+ userId: number;
+};
+
+export const upsertSiteSetting = async ({
+ id,
+ enabled,
+ data,
+ userId,
+}: UpsertSiteSettingOptions) => {
+ return await prisma.siteSettings.upsert({
+ where: {
+ id,
+ },
+ create: {
+ id,
+ enabled,
+ data,
+ lastModifiedByUserId: userId,
+ lastModifiedAt: new Date(),
+ },
+ update: {
+ enabled,
+ data,
+ lastModifiedByUserId: userId,
+ lastModifiedAt: new Date(),
+ },
+ });
+};
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 c520d4ce1..55519a30e 100644
--- a/packages/lib/server-only/template/create-document-from-template.ts
+++ b/packages/lib/server-only/template/create-document-from-template.ts
@@ -1,32 +1,42 @@
import { nanoid } from '@documenso/lib/universal/id';
import { prisma } from '@documenso/prisma';
-import type { TCreateDocumentFromTemplateMutationSchema } from '@documenso/trpc/server/template-router/schema';
+import type { RecipientRole } from '@documenso/prisma/client';
-export type CreateDocumentFromTemplateOptions = TCreateDocumentFromTemplateMutationSchema & {
+export type CreateDocumentFromTemplateOptions = {
+ templateId: number;
userId: number;
+ teamId?: number;
+ recipients?: {
+ name?: string;
+ email: string;
+ role?: RecipientRole;
+ }[];
};
export const createDocumentFromTemplate = async ({
templateId,
userId,
+ teamId,
+ recipients,
}: CreateDocumentFromTemplateOptions) => {
const template = await prisma.template.findUnique({
where: {
id: templateId,
- OR: [
- {
- userId,
- },
- {
- team: {
- members: {
- some: {
- userId,
+ ...(teamId
+ ? {
+ team: {
+ id: teamId,
+ members: {
+ some: {
+ userId,
+ },
},
},
- },
- },
- ],
+ }
+ : {
+ userId,
+ teamId: null,
+ }),
},
include: {
Recipient: true,
@@ -57,13 +67,18 @@ export const createDocumentFromTemplate = async ({
create: template.Recipient.map((recipient) => ({
email: recipient.email,
name: recipient.name,
+ role: recipient.role,
token: nanoid(),
})),
},
},
include: {
- Recipient: true,
+ Recipient: {
+ orderBy: {
+ id: 'asc',
+ },
+ },
},
});
@@ -88,5 +103,34 @@ export const createDocumentFromTemplate = async ({
}),
});
+ if (recipients && recipients.length > 0) {
+ document.Recipient = await Promise.all(
+ recipients.map(async (recipient, index) => {
+ const existingRecipient = document.Recipient.at(index);
+
+ return await prisma.recipient.upsert({
+ where: {
+ documentId_email: {
+ documentId: document.id,
+ email: existingRecipient?.email ?? recipient.email,
+ },
+ },
+ update: {
+ name: recipient.name,
+ email: recipient.email,
+ role: recipient.role,
+ },
+ create: {
+ documentId: document.id,
+ email: recipient.email,
+ name: recipient.name,
+ role: recipient.role,
+ token: nanoid(),
+ },
+ });
+ }),
+ );
+ }
+
return document;
};
diff --git a/packages/lib/server-only/template/find-templates.ts b/packages/lib/server-only/template/find-templates.ts
index d453d28a0..69b43f9b9 100644
--- a/packages/lib/server-only/template/find-templates.ts
+++ b/packages/lib/server-only/template/find-templates.ts
@@ -38,6 +38,7 @@ export const findTemplates = async ({
include: {
templateDocumentData: true,
Field: true,
+ Recipient: true,
},
skip: Math.max(page - 1, 0) * perPage,
orderBy: {
diff --git a/packages/lib/server-only/user/delete-user.ts b/packages/lib/server-only/user/delete-user.ts
index df5132aff..d6d4284b4 100644
--- a/packages/lib/server-only/user/delete-user.ts
+++ b/packages/lib/server-only/user/delete-user.ts
@@ -1,4 +1,7 @@
import { prisma } from '@documenso/prisma';
+import { DocumentStatus } from '@documenso/prisma/client';
+
+import { deletedAccountServiceAccount } from './service-accounts/deleted-account';
export type DeleteUserOptions = {
email: string;
@@ -17,6 +20,22 @@ export const deleteUser = async ({ email }: DeleteUserOptions) => {
throw new Error(`User with email ${email} not found`);
}
+ const serviceAccount = await deletedAccountServiceAccount();
+
+ // TODO: Send out cancellations for all pending docs
+ await prisma.document.updateMany({
+ where: {
+ userId: user.id,
+ status: {
+ in: [DocumentStatus.PENDING, DocumentStatus.COMPLETED],
+ },
+ },
+ data: {
+ userId: serviceAccount.id,
+ deletedAt: new Date(),
+ },
+ });
+
return await prisma.user.delete({
where: {
id: user.id,
diff --git a/packages/lib/server-only/user/service-accounts/deleted-account.ts b/packages/lib/server-only/user/service-accounts/deleted-account.ts
new file mode 100644
index 000000000..6bfd6d25f
--- /dev/null
+++ b/packages/lib/server-only/user/service-accounts/deleted-account.ts
@@ -0,0 +1,17 @@
+import { prisma } from '@documenso/prisma';
+
+export const deletedAccountServiceAccount = async () => {
+ const serviceAccount = await prisma.user.findFirst({
+ where: {
+ email: 'deleted-account@documenso.com',
+ },
+ });
+
+ if (!serviceAccount) {
+ throw new Error(
+ 'Deleted account service account not found, have you ran the appropriate migrations?',
+ );
+ }
+
+ return serviceAccount;
+};
diff --git a/packages/prisma/migrations/20231123132053_public_api_api_token/migration.sql b/packages/prisma/migrations/20231123132053_public_api_api_token/migration.sql
new file mode 100644
index 000000000..d3c9106c4
--- /dev/null
+++ b/packages/prisma/migrations/20231123132053_public_api_api_token/migration.sql
@@ -0,0 +1,21 @@
+-- CreateEnum
+CREATE TYPE "ApiTokenAlgorithm" AS ENUM ('SHA512');
+
+-- CreateTable
+CREATE TABLE "ApiToken" (
+ "id" SERIAL NOT NULL,
+ "name" TEXT NOT NULL,
+ "token" TEXT NOT NULL,
+ "algorithm" "ApiTokenAlgorithm" NOT NULL DEFAULT 'SHA512',
+ "expires" TIMESTAMP(3) NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "userId" INTEGER NOT NULL,
+
+ CONSTRAINT "ApiToken_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateIndex
+CREATE UNIQUE INDEX "ApiToken_token_key" ON "ApiToken"("token");
+
+-- AddForeignKey
+ALTER TABLE "ApiToken" ADD CONSTRAINT "ApiToken_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
diff --git a/packages/prisma/migrations/20231220124343_add_cascade_delete_user_apitoken/migration.sql b/packages/prisma/migrations/20231220124343_add_cascade_delete_user_apitoken/migration.sql
new file mode 100644
index 000000000..4eb0b4760
--- /dev/null
+++ b/packages/prisma/migrations/20231220124343_add_cascade_delete_user_apitoken/migration.sql
@@ -0,0 +1,5 @@
+-- DropForeignKey
+ALTER TABLE "ApiToken" DROP CONSTRAINT "ApiToken_userId_fkey";
+
+-- AddForeignKey
+ALTER TABLE "ApiToken" ADD CONSTRAINT "ApiToken_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
diff --git a/packages/prisma/migrations/20240205120648_create_delete_account/migration.sql b/packages/prisma/migrations/20240205120648_create_delete_account/migration.sql
new file mode 100644
index 000000000..d001bc4ae
--- /dev/null
+++ b/packages/prisma/migrations/20240205120648_create_delete_account/migration.sql
@@ -0,0 +1,30 @@
+-- Create deleted@documenso.com
+DO $$
+BEGIN
+ IF NOT EXISTS (SELECT 1 FROM "public"."User" WHERE "email" = 'deleted-account@documenso.com') THEN
+ INSERT INTO
+ "public"."User" (
+ "email",
+ "emailVerified",
+ "password",
+ "createdAt",
+ "updatedAt",
+ "lastSignedIn",
+ "roles",
+ "identityProvider",
+ "twoFactorEnabled"
+ )
+ VALUES
+ (
+ 'deleted-account@documenso.com',
+ NOW(),
+ NULL,
+ NOW(),
+ NOW(),
+ NOW(),
+ ARRAY['USER'::TEXT]::"public"."Role" [],
+ CAST('GOOGLE'::TEXT AS "public"."IdentityProvider"),
+ FALSE
+ );
+ END IF;
+END $$
diff --git a/packages/prisma/migrations/20240208135802_make_expiry_date_optional_api_tokens/migration.sql b/packages/prisma/migrations/20240208135802_make_expiry_date_optional_api_tokens/migration.sql
new file mode 100644
index 000000000..02e291481
--- /dev/null
+++ b/packages/prisma/migrations/20240208135802_make_expiry_date_optional_api_tokens/migration.sql
@@ -0,0 +1,2 @@
+-- AlterTable
+ALTER TABLE "ApiToken" ALTER COLUMN "expires" DROP NOT NULL;
diff --git a/packages/prisma/migrations/20240221055920_support_team_tokens/migration.sql b/packages/prisma/migrations/20240221055920_support_team_tokens/migration.sql
new file mode 100644
index 000000000..553b7807f
--- /dev/null
+++ b/packages/prisma/migrations/20240221055920_support_team_tokens/migration.sql
@@ -0,0 +1,6 @@
+-- AlterTable
+ALTER TABLE "ApiToken" ADD COLUMN "teamId" INTEGER,
+ALTER COLUMN "userId" DROP NOT NULL;
+
+-- AddForeignKey
+ALTER TABLE "ApiToken" ADD CONSTRAINT "ApiToken_teamId_fkey" FOREIGN KEY ("teamId") REFERENCES "Team"("id") ON DELETE CASCADE ON UPDATE CASCADE;
diff --git a/packages/prisma/migrations/20240222183156_display_banner/migration.sql b/packages/prisma/migrations/20240222183156_display_banner/migration.sql
new file mode 100644
index 000000000..1625c7dc1
--- /dev/null
+++ b/packages/prisma/migrations/20240222183156_display_banner/migration.sql
@@ -0,0 +1,12 @@
+-- CreateTable
+CREATE TABLE "Banner" (
+ "id" SERIAL NOT NULL,
+ "text" TEXT NOT NULL,
+ "customHTML" TEXT NOT NULL,
+ "userId" INTEGER,
+
+ CONSTRAINT "Banner_pkey" PRIMARY KEY ("id")
+);
+
+-- AddForeignKey
+ALTER TABLE "Banner" ADD CONSTRAINT "Banner_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
diff --git a/packages/prisma/migrations/20240222183231_banner_show/migration.sql b/packages/prisma/migrations/20240222183231_banner_show/migration.sql
new file mode 100644
index 000000000..61b8f2dd1
--- /dev/null
+++ b/packages/prisma/migrations/20240222183231_banner_show/migration.sql
@@ -0,0 +1,2 @@
+-- AlterTable
+ALTER TABLE "Banner" ADD COLUMN "show" BOOLEAN NOT NULL DEFAULT false;
diff --git a/packages/prisma/migrations/20240222185936_remove_custom/migration.sql b/packages/prisma/migrations/20240222185936_remove_custom/migration.sql
new file mode 100644
index 000000000..67472035a
--- /dev/null
+++ b/packages/prisma/migrations/20240222185936_remove_custom/migration.sql
@@ -0,0 +1,8 @@
+/*
+ Warnings:
+
+ - You are about to drop the column `customHTML` on the `Banner` table. All the data in the column will be lost.
+
+*/
+-- AlterTable
+ALTER TABLE "Banner" DROP COLUMN "customHTML";
diff --git a/packages/prisma/migrations/20240222230527_change_banner_to_site_settings_model/migration.sql b/packages/prisma/migrations/20240222230527_change_banner_to_site_settings_model/migration.sql
new file mode 100644
index 000000000..6b1b5675e
--- /dev/null
+++ b/packages/prisma/migrations/20240222230527_change_banner_to_site_settings_model/migration.sql
@@ -0,0 +1,25 @@
+/*
+ Warnings:
+
+ - You are about to drop the `Banner` table. If the table is not empty, all the data it contains will be lost.
+
+*/
+-- DropForeignKey
+ALTER TABLE "Banner" DROP CONSTRAINT "Banner_userId_fkey";
+
+-- DropTable
+DROP TABLE "Banner";
+
+-- CreateTable
+CREATE TABLE "SiteSettings" (
+ "id" TEXT NOT NULL,
+ "enabled" BOOLEAN NOT NULL DEFAULT false,
+ "data" JSONB NOT NULL,
+ "lastModifiedByUserId" INTEGER,
+ "lastModifiedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+
+ CONSTRAINT "SiteSettings_pkey" PRIMARY KEY ("id")
+);
+
+-- AddForeignKey
+ALTER TABLE "SiteSettings" ADD CONSTRAINT "SiteSettings_lastModifiedByUserId_fkey" FOREIGN KEY ("lastModifiedByUserId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
diff --git a/packages/prisma/migrations/20240222230604_add_site_banner_to_site_settings/migration.sql b/packages/prisma/migrations/20240222230604_add_site_banner_to_site_settings/migration.sql
new file mode 100644
index 000000000..c6b50a6aa
--- /dev/null
+++ b/packages/prisma/migrations/20240222230604_add_site_banner_to_site_settings/migration.sql
@@ -0,0 +1,13 @@
+INSERT INTO "SiteSettings" ("id", "enabled", "data")
+VALUES (
+ 'site.banner',
+ FALSE,
+ jsonb_build_object(
+ 'content',
+ 'This is a test banner',
+ 'bgColor',
+ '#000000',
+ 'textColor',
+ '#ffffff'
+ )
+ );
\ No newline at end of file
diff --git a/packages/prisma/migrations/20240226035048_add_recipient_referential_action_for_fields/migration.sql b/packages/prisma/migrations/20240226035048_add_recipient_referential_action_for_fields/migration.sql
new file mode 100644
index 000000000..170c0976c
--- /dev/null
+++ b/packages/prisma/migrations/20240226035048_add_recipient_referential_action_for_fields/migration.sql
@@ -0,0 +1,5 @@
+-- DropForeignKey
+ALTER TABLE "Field" DROP CONSTRAINT "Field_recipientId_fkey";
+
+-- AddForeignKey
+ALTER TABLE "Field" ADD CONSTRAINT "Field_recipientId_fkey" FOREIGN KEY ("recipientId") REFERENCES "Recipient"("id") ON DELETE CASCADE ON UPDATE CASCADE;
diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma
index 2887cd6d2..300c9e238 100644
--- a/packages/prisma/schema.prisma
+++ b/packages/prisma/schema.prisma
@@ -19,19 +19,19 @@ enum Role {
}
model User {
- id Int @id @default(autoincrement())
+ id Int @id @default(autoincrement())
name String?
- customerId String? @unique
- email String @unique
+ customerId String? @unique
+ email String @unique
emailVerified DateTime?
password String?
source String?
signature String?
- createdAt DateTime @default(now())
- updatedAt DateTime @default(now()) @updatedAt
- lastSignedIn DateTime @default(now())
- roles Role[] @default([USER])
- identityProvider IdentityProvider @default(DOCUMENSO)
+ createdAt DateTime @default(now())
+ updatedAt DateTime @default(now()) @updatedAt
+ lastSignedIn DateTime @default(now())
+ roles Role[] @default([USER])
+ identityProvider IdentityProvider @default(DOCUMENSO)
accounts Account[]
sessions Session[]
Document Document[]
@@ -41,12 +41,14 @@ model User {
ownedPendingTeams TeamPending[]
teamMembers TeamMember[]
twoFactorSecret String?
- twoFactorEnabled Boolean @default(false)
+ twoFactorEnabled Boolean @default(false)
twoFactorBackupCodes String?
-
+
VerificationToken VerificationToken[]
+ ApiToken ApiToken[]
Template Template[]
securityAuditLogs UserSecurityAuditLog[]
+ siteSettings SiteSettings[]
@@index([email])
}
@@ -94,6 +96,23 @@ model VerificationToken {
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}
+enum ApiTokenAlgorithm {
+ SHA512
+}
+
+model ApiToken {
+ id Int @id @default(autoincrement())
+ name String
+ token String @unique
+ algorithm ApiTokenAlgorithm @default(SHA512)
+ expires DateTime?
+ createdAt DateTime @default(now())
+ userId Int?
+ user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
+ teamId Int?
+ team Team? @relation(fields: [teamId], references: [id], onDelete: Cascade)
+}
+
enum SubscriptionStatus {
ACTIVE
PAST_DUE
@@ -210,15 +229,15 @@ model DocumentData {
}
model DocumentMeta {
- id String @id @default(cuid())
- subject String?
- message String?
- timezone String? @default("Etc/UTC") @db.Text
- password String?
- dateFormat String? @default("yyyy-MM-dd hh:mm a") @db.Text
- documentId Int @unique
- document Document @relation(fields: [documentId], references: [id], onDelete: Cascade)
- redirectUrl String?
+ id String @id @default(cuid())
+ subject String?
+ message String?
+ timezone String? @default("Etc/UTC") @db.Text
+ password String?
+ dateFormat String? @default("yyyy-MM-dd hh:mm a") @db.Text
+ documentId Int @unique
+ document Document @relation(fields: [documentId], references: [id], onDelete: Cascade)
+ redirectUrl String?
}
enum ReadStatus {
@@ -293,7 +312,7 @@ model Field {
inserted Boolean
Document Document? @relation(fields: [documentId], references: [id], onDelete: Cascade)
Template Template? @relation(fields: [templateId], references: [id], onDelete: Cascade)
- Recipient Recipient? @relation(fields: [recipientId], references: [id])
+ Recipient Recipient? @relation(fields: [recipientId], references: [id], onDelete: Cascade)
Signature Signature?
@@index([documentId])
@@ -357,6 +376,7 @@ model Team {
document Document[]
templates Template[]
+ ApiToken ApiToken[]
}
model TeamPending {
@@ -450,3 +470,12 @@ model Template {
@@unique([templateDocumentDataId])
}
+
+model SiteSettings {
+ id String @id
+ enabled Boolean @default(false)
+ data Json
+ lastModifiedByUserId Int?
+ lastModifiedAt DateTime @default(now())
+ lastModifiedByUser User? @relation(fields: [lastModifiedByUserId], references: [id])
+}
diff --git a/packages/prisma/seed/templates.ts b/packages/prisma/seed/templates.ts
index 7f1b2f8e9..3feb82289 100644
--- a/packages/prisma/seed/templates.ts
+++ b/packages/prisma/seed/templates.ts
@@ -2,7 +2,7 @@ import fs from 'node:fs';
import path from 'node:path';
import { prisma } from '..';
-import { DocumentDataType } from '../client';
+import { DocumentDataType, ReadStatus, RecipientRole, SendStatus, SigningStatus } from '../client';
const examplePdf = fs
.readFileSync(path.join(__dirname, '../../../assets/example.pdf'))
@@ -28,9 +28,36 @@ export const seedTemplate = async (options: SeedTemplateOptions) => {
return await prisma.template.create({
data: {
title,
- templateDocumentDataId: documentData.id,
- userId: userId,
- teamId,
+ templateDocumentData: {
+ connect: {
+ id: documentData.id,
+ },
+ },
+ User: {
+ connect: {
+ id: userId,
+ },
+ },
+ Recipient: {
+ create: {
+ email: 'recipient.1@documenso.com',
+ name: 'Recipient 1',
+ token: Math.random().toString().slice(2, 7),
+ sendStatus: SendStatus.NOT_SENT,
+ signingStatus: SigningStatus.NOT_SIGNED,
+ readStatus: ReadStatus.NOT_OPENED,
+ role: RecipientRole.SIGNER,
+ },
+ },
+ ...(teamId
+ ? {
+ team: {
+ connect: {
+ id: teamId,
+ },
+ },
+ }
+ : {}),
},
});
};
diff --git a/packages/tailwind-config/index.cjs b/packages/tailwind-config/index.cjs
index 1564454d8..03c358dc3 100644
--- a/packages/tailwind-config/index.cjs
+++ b/packages/tailwind-config/index.cjs
@@ -45,6 +45,11 @@ module.exports = {
DEFAULT: 'hsl(var(--card))',
foreground: 'hsl(var(--card-foreground))',
},
+ 'field-card': {
+ DEFAULT: 'hsl(var(--field-card))',
+ border: 'hsl(var(--field-card-border))',
+ foreground: 'hsl(var(--field-card-foreground))',
+ },
widget: {
DEFAULT: 'hsl(var(--widget))',
// foreground: 'hsl(var(--widget-foreground))',
diff --git a/packages/trpc/package.json b/packages/trpc/package.json
index 54c1d5917..fb32bcdf3 100644
--- a/packages/trpc/package.json
+++ b/packages/trpc/package.json
@@ -17,6 +17,8 @@
"@trpc/next": "^10.36.0",
"@trpc/react-query": "^10.36.0",
"@trpc/server": "^10.36.0",
+ "@ts-rest/core": "^3.30.5",
+ "@ts-rest/next": "^3.30.5",
"luxon": "^3.4.0",
"superjson": "^1.13.1",
"ts-pattern": "^5.0.5",
diff --git a/packages/trpc/react/index.tsx b/packages/trpc/react/index.tsx
index 85161d0e8..ce80ba267 100644
--- a/packages/trpc/react/index.tsx
+++ b/packages/trpc/react/index.tsx
@@ -9,7 +9,7 @@ import SuperJSON from 'superjson';
import { getBaseUrl } from '@documenso/lib/universal/get-base-url';
-import { AppRouter } from '../server/router';
+import type { AppRouter } from '../server/router';
export const trpc = createTRPCReact({
unstable_overrides: {
diff --git a/packages/trpc/server/admin-router/router.ts b/packages/trpc/server/admin-router/router.ts
index 666e3f085..7d71ab346 100644
--- a/packages/trpc/server/admin-router/router.ts
+++ b/packages/trpc/server/admin-router/router.ts
@@ -1,9 +1,10 @@
import { TRPCError } from '@trpc/server';
import { updateUser } from '@documenso/lib/server-only/admin/update-user';
+import { upsertSiteSetting } from '@documenso/lib/server-only/site-settings/upsert-site-setting';
import { adminProcedure, router } from '../trpc';
-import { ZUpdateProfileMutationByAdminSchema } from './schema';
+import { ZUpdateProfileMutationByAdminSchema, ZUpdateSiteSettingMutationSchema } from './schema';
export const adminRouter = router({
updateUser: adminProcedure
@@ -20,4 +21,24 @@ export const adminRouter = router({
});
}
}),
+
+ updateSiteSetting: adminProcedure
+ .input(ZUpdateSiteSettingMutationSchema)
+ .mutation(async ({ ctx, input }) => {
+ try {
+ const { id, enabled, data } = input;
+
+ return await upsertSiteSetting({
+ id,
+ enabled,
+ data,
+ userId: ctx.user.id,
+ });
+ } catch (err) {
+ throw new TRPCError({
+ code: 'BAD_REQUEST',
+ message: 'We were unable to update the site setting provided.',
+ });
+ }
+ }),
});
diff --git a/packages/trpc/server/admin-router/schema.ts b/packages/trpc/server/admin-router/schema.ts
index a20d6f204..0b99c8372 100644
--- a/packages/trpc/server/admin-router/schema.ts
+++ b/packages/trpc/server/admin-router/schema.ts
@@ -1,6 +1,8 @@
import { Role } from '@prisma/client';
import z from 'zod';
+import { ZSiteSettingSchema } from '@documenso/lib/server-only/site-settings/schema';
+
export const ZUpdateProfileMutationByAdminSchema = z.object({
id: z.number().min(1),
name: z.string().nullish(),
@@ -11,3 +13,7 @@ export const ZUpdateProfileMutationByAdminSchema = z.object({
export type TUpdateProfileMutationByAdminSchema = z.infer<
typeof ZUpdateProfileMutationByAdminSchema
>;
+
+export const ZUpdateSiteSettingMutationSchema = ZSiteSettingSchema;
+
+export type TUpdateSiteSettingMutationSchema = z.infer;
diff --git a/packages/trpc/server/api-token-router/router.ts b/packages/trpc/server/api-token-router/router.ts
new file mode 100644
index 000000000..14e75e001
--- /dev/null
+++ b/packages/trpc/server/api-token-router/router.ts
@@ -0,0 +1,83 @@
+import { TRPCError } from '@trpc/server';
+
+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 { getUserTokens } from '@documenso/lib/server-only/public-api/get-all-user-tokens';
+import { getApiTokenById } from '@documenso/lib/server-only/public-api/get-api-token-by-id';
+
+import { authenticatedProcedure, router } from '../trpc';
+import {
+ ZCreateTokenMutationSchema,
+ ZDeleteTokenByIdMutationSchema,
+ ZGetApiTokenByIdQuerySchema,
+} from './schema';
+
+export const apiTokenRouter = router({
+ getTokens: authenticatedProcedure.query(async ({ ctx }) => {
+ try {
+ return await getUserTokens({ userId: ctx.user.id });
+ } catch (e) {
+ throw new TRPCError({
+ code: 'BAD_REQUEST',
+ message: 'We were unable to find your API tokens. Please try again.',
+ });
+ }
+ }),
+
+ getTokenById: authenticatedProcedure
+ .input(ZGetApiTokenByIdQuerySchema)
+ .query(async ({ input, ctx }) => {
+ try {
+ const { id } = input;
+
+ return await getApiTokenById({
+ id,
+ userId: ctx.user.id,
+ });
+ } catch (e) {
+ throw new TRPCError({
+ code: 'BAD_REQUEST',
+ message: 'We were unable to find this API token. Please try again.',
+ });
+ }
+ }),
+
+ createToken: authenticatedProcedure
+ .input(ZCreateTokenMutationSchema)
+ .mutation(async ({ input, ctx }) => {
+ try {
+ const { tokenName, teamId, expirationDate } = input;
+
+ return await createApiToken({
+ userId: ctx.user.id,
+ teamId,
+ tokenName,
+ expiresIn: expirationDate,
+ });
+ } catch (e) {
+ throw new TRPCError({
+ code: 'BAD_REQUEST',
+ message: 'We were unable to create an API token. Please try again.',
+ });
+ }
+ }),
+
+ deleteTokenById: authenticatedProcedure
+ .input(ZDeleteTokenByIdMutationSchema)
+ .mutation(async ({ input, ctx }) => {
+ try {
+ const { id, teamId } = input;
+
+ return await deleteTokenById({
+ id,
+ teamId,
+ userId: ctx.user.id,
+ });
+ } catch (e) {
+ throw new TRPCError({
+ code: 'BAD_REQUEST',
+ message: 'We were unable to delete this API Token. Please try again.',
+ });
+ }
+ }),
+});
diff --git a/packages/trpc/server/api-token-router/schema.ts b/packages/trpc/server/api-token-router/schema.ts
new file mode 100644
index 000000000..f03de73eb
--- /dev/null
+++ b/packages/trpc/server/api-token-router/schema.ts
@@ -0,0 +1,22 @@
+import { z } from 'zod';
+
+export const ZGetApiTokenByIdQuerySchema = z.object({
+ id: z.number().min(1),
+});
+
+export type TGetApiTokenByIdQuerySchema = z.infer;
+
+export const ZCreateTokenMutationSchema = z.object({
+ teamId: z.number().optional(),
+ 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().optional(),
+});
+
+export type TDeleteTokenByIdMutationSchema = z.infer;
diff --git a/packages/trpc/server/document-router/router.ts b/packages/trpc/server/document-router/router.ts
index eb833684a..26b547ac9 100644
--- a/packages/trpc/server/document-router/router.ts
+++ b/packages/trpc/server/document-router/router.ts
@@ -13,24 +13,20 @@ import { resendDocument } from '@documenso/lib/server-only/document/resend-docum
import { searchDocumentsWithKeyword } from '@documenso/lib/server-only/document/search-documents-with-keyword';
import { sendDocument } from '@documenso/lib/server-only/document/send-document';
import { updateTitle } from '@documenso/lib/server-only/document/update-title';
-import { setFieldsForDocument } from '@documenso/lib/server-only/field/set-fields-for-document';
-import { setRecipientsForDocument } from '@documenso/lib/server-only/recipient/set-recipients-for-document';
import { symmetricEncrypt } from '@documenso/lib/universal/crypto';
import { extractNextApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import { authenticatedProcedure, procedure, router } from '../trpc';
import {
ZCreateDocumentMutationSchema,
- ZDeleteDraftDocumentMutationSchema,
+ ZDeleteDraftDocumentMutationSchema as ZDeleteDocumentMutationSchema,
ZFindDocumentAuditLogsQuerySchema,
ZGetDocumentByIdQuerySchema,
ZGetDocumentByTokenQuerySchema,
ZResendDocumentMutationSchema,
ZSearchDocumentsMutationSchema,
ZSendDocumentMutationSchema,
- ZSetFieldsForDocumentMutationSchema,
ZSetPasswordForDocumentMutationSchema,
- ZSetRecipientsForDocumentMutationSchema,
ZSetTitleForDocumentMutationSchema,
} from './schema';
@@ -106,17 +102,17 @@ export const documentRouter = router({
}),
deleteDocument: authenticatedProcedure
- .input(ZDeleteDraftDocumentMutationSchema)
+ .input(ZDeleteDocumentMutationSchema)
.mutation(async ({ input, ctx }) => {
try {
- const { id, status } = input;
+ const { id, teamId } = input;
const userId = ctx.user.id;
return await deleteDocument({
id,
userId,
- status,
+ teamId,
requestMetadata: extractNextApiRequestMetadata(ctx.req),
});
} catch (err) {
@@ -157,63 +153,19 @@ export const documentRouter = router({
setTitleForDocument: authenticatedProcedure
.input(ZSetTitleForDocumentMutationSchema)
.mutation(async ({ input, ctx }) => {
- const { documentId, title } = input;
+ const { documentId, teamId, title } = input;
const userId = ctx.user.id;
return await updateTitle({
title,
userId,
+ teamId,
documentId,
requestMetadata: extractNextApiRequestMetadata(ctx.req),
});
}),
- setRecipientsForDocument: authenticatedProcedure
- .input(ZSetRecipientsForDocumentMutationSchema)
- .mutation(async ({ input, ctx }) => {
- try {
- const { documentId, recipients } = input;
-
- return await setRecipientsForDocument({
- userId: ctx.user.id,
- documentId,
- recipients,
- requestMetadata: extractNextApiRequestMetadata(ctx.req),
- });
- } catch (err) {
- console.error(err);
-
- throw new TRPCError({
- code: 'BAD_REQUEST',
- message:
- 'We were unable to set the recipients for this document. Please try again later.',
- });
- }
- }),
-
- setFieldsForDocument: authenticatedProcedure
- .input(ZSetFieldsForDocumentMutationSchema)
- .mutation(async ({ input, ctx }) => {
- try {
- const { documentId, fields } = input;
-
- return await setFieldsForDocument({
- userId: ctx.user.id,
- documentId,
- fields,
- requestMetadata: extractNextApiRequestMetadata(ctx.req),
- });
- } catch (err) {
- console.error(err);
-
- throw new TRPCError({
- code: 'BAD_REQUEST',
- message: 'We were unable to set the fields for this document. Please try again later.',
- });
- }
- }),
-
setPasswordForDocument: authenticatedProcedure
.input(ZSetPasswordForDocumentMutationSchema)
.mutation(async ({ input, ctx }) => {
@@ -251,7 +203,7 @@ export const documentRouter = router({
.input(ZSendDocumentMutationSchema)
.mutation(async ({ input, ctx }) => {
try {
- const { documentId, meta } = input;
+ const { documentId, teamId, meta } = input;
if (meta.message || meta.subject || meta.timezone || meta.dateFormat || meta.redirectUrl) {
await upsertDocumentMeta({
@@ -269,6 +221,7 @@ export const documentRouter = router({
return await sendDocument({
userId: ctx.user.id,
documentId,
+ teamId,
requestMetadata: extractNextApiRequestMetadata(ctx.req),
});
} catch (err) {
diff --git a/packages/trpc/server/document-router/schema.ts b/packages/trpc/server/document-router/schema.ts
index 83c05b3b3..34ddf1a5c 100644
--- a/packages/trpc/server/document-router/schema.ts
+++ b/packages/trpc/server/document-router/schema.ts
@@ -2,7 +2,7 @@ import { z } from 'zod';
import { URL_REGEX } from '@documenso/lib/constants/url-regex';
import { ZBaseTableSearchParamsSchema } from '@documenso/lib/types/search-params';
-import { DocumentStatus, FieldType, RecipientRole } from '@documenso/prisma/client';
+import { FieldType, RecipientRole } from '@documenso/prisma/client';
export const ZFindDocumentAuditLogsQuerySchema = ZBaseTableSearchParamsSchema.extend({
documentId: z.number().min(1),
@@ -39,6 +39,7 @@ export type TCreateDocumentMutationSchema = z.infer;
diff --git a/packages/trpc/server/profile-router/router.ts b/packages/trpc/server/profile-router/router.ts
index bceee020a..2f636d87d 100644
--- a/packages/trpc/server/profile-router/router.ts
+++ b/packages/trpc/server/profile-router/router.ts
@@ -1,5 +1,6 @@
import { TRPCError } from '@trpc/server';
+import { deleteUser } from '@documenso/lib/server-only/user/delete-user';
import { findUserSecurityAuditLogs } from '@documenso/lib/server-only/user/find-user-security-audit-logs';
import { forgotPassword } from '@documenso/lib/server-only/user/forgot-password';
import { getUserById } from '@documenso/lib/server-only/user/get-user-by-id';
@@ -155,4 +156,23 @@ export const profileRouter = router({
});
}
}),
+
+ deleteAccount: authenticatedProcedure.mutation(async ({ ctx }) => {
+ try {
+ const user = ctx.user;
+
+ return await deleteUser(user);
+ } catch (err) {
+ let message = 'We were unable to delete your account. Please try again.';
+
+ if (err instanceof Error) {
+ message = err.message;
+ }
+
+ throw new TRPCError({
+ code: 'BAD_REQUEST',
+ message,
+ });
+ }
+ }),
});
diff --git a/packages/trpc/server/recipient-router/router.ts b/packages/trpc/server/recipient-router/router.ts
index c36b09ec9..ac040f4f5 100644
--- a/packages/trpc/server/recipient-router/router.ts
+++ b/packages/trpc/server/recipient-router/router.ts
@@ -17,11 +17,12 @@ export const recipientRouter = router({
.input(ZAddSignersMutationSchema)
.mutation(async ({ input, ctx }) => {
try {
- const { documentId, signers } = input;
+ const { documentId, teamId, signers } = input;
return await setRecipientsForDocument({
userId: ctx.user.id,
documentId,
+ teamId,
recipients: signers.map((signer) => ({
id: signer.nativeId,
email: signer.email,
@@ -53,6 +54,7 @@ export const recipientRouter = router({
id: signer.nativeId,
email: signer.email,
name: signer.name,
+ role: signer.role,
})),
});
} catch (err) {
diff --git a/packages/trpc/server/recipient-router/schema.ts b/packages/trpc/server/recipient-router/schema.ts
index a6b4e0d11..6825137c4 100644
--- a/packages/trpc/server/recipient-router/schema.ts
+++ b/packages/trpc/server/recipient-router/schema.ts
@@ -5,6 +5,7 @@ import { RecipientRole } from '@documenso/prisma/client';
export const ZAddSignersMutationSchema = z
.object({
documentId: z.number(),
+ teamId: z.number().optional(),
signers: z.array(
z.object({
nativeId: z.number().optional(),
@@ -34,6 +35,7 @@ export const ZAddTemplateSignersMutationSchema = z
nativeId: z.number().optional(),
email: z.string().email().min(1),
name: z.string(),
+ role: z.nativeEnum(RecipientRole),
}),
),
})
diff --git a/packages/trpc/server/router.ts b/packages/trpc/server/router.ts
index aec70fd63..72fe0b2be 100644
--- a/packages/trpc/server/router.ts
+++ b/packages/trpc/server/router.ts
@@ -1,4 +1,5 @@
import { adminRouter } from './admin-router/router';
+import { apiTokenRouter } from './api-token-router/router';
import { authRouter } from './auth-router/router';
import { cryptoRouter } from './crypto/router';
import { documentRouter } from './document-router/router';
@@ -21,6 +22,7 @@ export const appRouter = router({
recipient: recipientRouter,
admin: adminRouter,
shareLink: shareLinkRouter,
+ apiToken: apiTokenRouter,
singleplayer: singleplayerRouter,
team: teamRouter,
template: templateRouter,
diff --git a/packages/trpc/server/singleplayer-router/helper.ts b/packages/trpc/server/singleplayer-router/helper.ts
index 0ec0ba42d..32d03c0ac 100644
--- a/packages/trpc/server/singleplayer-router/helper.ts
+++ b/packages/trpc/server/singleplayer-router/helper.ts
@@ -22,6 +22,7 @@ export const mapField = (
.with(FieldType.DATE, () => DateTime.now().toFormat('yyyy-MM-dd hh:mm a'))
.with(FieldType.EMAIL, () => signer.email)
.with(FieldType.NAME, () => signer.name)
+ .with(FieldType.TEXT, () => signer.customText)
.otherwise(() => '');
return {
diff --git a/packages/trpc/server/singleplayer-router/schema.ts b/packages/trpc/server/singleplayer-router/schema.ts
index 9fa56e7b1..412429fca 100644
--- a/packages/trpc/server/singleplayer-router/schema.ts
+++ b/packages/trpc/server/singleplayer-router/schema.ts
@@ -12,6 +12,7 @@ export const ZCreateSinglePlayerDocumentMutationSchema = z.object({
email: z.string().email().min(1),
name: z.string(),
signature: z.string(),
+ customText: z.string(),
}),
fields: z.array(
z.object({
diff --git a/packages/trpc/server/template-router/router.ts b/packages/trpc/server/template-router/router.ts
index 7417e7d00..2dd4d51c8 100644
--- a/packages/trpc/server/template-router/router.ts
+++ b/packages/trpc/server/template-router/router.ts
@@ -41,7 +41,7 @@ export const templateRouter = router({
.input(ZCreateDocumentFromTemplateMutationSchema)
.mutation(async ({ input, ctx }) => {
try {
- const { templateId } = input;
+ const { templateId, teamId } = input;
const limits = await getServerLimits({ email: ctx.user.email });
@@ -51,7 +51,9 @@ export const templateRouter = router({
return await createDocumentFromTemplate({
templateId,
+ teamId,
userId: ctx.user.id,
+ recipients: input.recipients,
});
} catch (err) {
throw new TRPCError({
diff --git a/packages/trpc/server/template-router/schema.ts b/packages/trpc/server/template-router/schema.ts
index 3d87d4b4f..3f16d7b39 100644
--- a/packages/trpc/server/template-router/schema.ts
+++ b/packages/trpc/server/template-router/schema.ts
@@ -1,5 +1,7 @@
import { z } from 'zod';
+import { RecipientRole } from '@documenso/prisma/client';
+
export const ZCreateTemplateMutationSchema = z.object({
title: z.string().min(1).trim(),
teamId: z.number().optional(),
@@ -8,6 +10,16 @@ export const ZCreateTemplateMutationSchema = z.object({
export const ZCreateDocumentFromTemplateMutationSchema = z.object({
templateId: z.number(),
+ teamId: z.number().optional(),
+ recipients: z
+ .array(
+ z.object({
+ email: z.string().email(),
+ name: z.string(),
+ role: z.nativeEnum(RecipientRole),
+ }),
+ )
+ .optional(),
});
export const ZDuplicateTemplateMutationSchema = z.object({
diff --git a/packages/trpc/tsconfig.json b/packages/trpc/tsconfig.json
index 4aefcb98c..dc21318a7 100644
--- a/packages/trpc/tsconfig.json
+++ b/packages/trpc/tsconfig.json
@@ -1,5 +1,8 @@
{
"extends": "@documenso/tsconfig/react-library.json",
"include": ["."],
- "exclude": ["dist", "build", "node_modules"]
+ "exclude": ["dist", "build", "node_modules"],
+ "compilerOptions": {
+ "strict": true,
+ }
}
diff --git a/packages/ui/package.json b/packages/ui/package.json
index 44d14cb82..90aa1bbda 100644
--- a/packages/ui/package.json
+++ b/packages/ui/package.json
@@ -64,6 +64,7 @@
"luxon": "^3.4.2",
"next": "14.0.3",
"pdfjs-dist": "3.6.172",
+ "react-colorful": "^5.6.1",
"react-day-picker": "^8.7.1",
"react-hook-form": "^7.45.4",
"react-pdf": "7.3.3",
diff --git a/packages/ui/primitives/button.tsx b/packages/ui/primitives/button.tsx
index 5fc3fc1bb..add486332 100644
--- a/packages/ui/primitives/button.tsx
+++ b/packages/ui/primitives/button.tsx
@@ -13,7 +13,8 @@ const buttonVariants = cva(
variants: {
variant: {
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
- destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
+ destructive:
+ 'bg-destructive text-destructive-foreground hover:bg-destructive/90 focus-visible:ring-destructive',
outline: 'border border-input hover:bg-accent hover:text-accent-foreground',
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground',
diff --git a/packages/ui/primitives/color-picker.tsx b/packages/ui/primitives/color-picker.tsx
new file mode 100644
index 000000000..b2c93ba8c
--- /dev/null
+++ b/packages/ui/primitives/color-picker.tsx
@@ -0,0 +1,82 @@
+import type { HTMLAttributes } from 'react';
+import React, { useState } from 'react';
+
+import { HexColorInput, HexColorPicker } from 'react-colorful';
+
+import { cn } from '../lib/utils';
+import { Popover, PopoverContent, PopoverTrigger } from './popover';
+
+export type ColorPickerProps = {
+ disabled?: boolean;
+ value: string;
+ defaultValue?: string;
+ onChange: (color: string) => void;
+} & HTMLAttributes;
+
+export const ColorPicker = ({
+ className,
+ disabled = false,
+ value,
+ defaultValue = '#000000',
+ onChange,
+ ...props
+}: ColorPickerProps) => {
+ const [color, setColor] = useState(value || defaultValue);
+ const [inputColor, setInputColor] = useState(value || defaultValue);
+
+ const onColorChange = (newColor: string) => {
+ setColor(newColor);
+ setInputColor(newColor);
+ onChange(newColor);
+ };
+
+ const onInputChange = (newColor: string) => {
+ setInputColor(newColor);
+ };
+
+ const onInputBlur = () => {
+ setColor(inputColor);
+ onChange(inputColor);
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ {
+ if (e.key === 'Enter') {
+ e.preventDefault();
+ onInputBlur();
+ }
+ }}
+ disabled={disabled}
+ />
+
+
+ );
+};
diff --git a/packages/ui/primitives/document-flow/add-fields.tsx b/packages/ui/primitives/document-flow/add-fields.tsx
index a6266d1aa..c4f58b83c 100644
--- a/packages/ui/primitives/document-flow/add-fields.tsx
+++ b/packages/ui/primitives/document-flow/add-fields.tsx
@@ -323,9 +323,9 @@ export const AddFieldsFormPartial = ({
{selectedField && (
-
+
{FRIENDLY_FIELD_TYPE[selectedField]}
@@ -552,6 +552,28 @@ export const AddFieldsFormPartial = ({
+
+ setSelectedField(FieldType.TEXT)}
+ onMouseDown={() => setSelectedField(FieldType.TEXT)}
+ data-selected={selectedField === FieldType.TEXT ? true : undefined}
+ >
+
+
+
+ {'Text'}
+
+
+ Custom Text
+
+
+
diff --git a/packages/ui/primitives/document-flow/add-signature.tsx b/packages/ui/primitives/document-flow/add-signature.tsx
index 5accdca16..f1ebc885e 100644
--- a/packages/ui/primitives/document-flow/add-signature.tsx
+++ b/packages/ui/primitives/document-flow/add-signature.tsx
@@ -44,6 +44,7 @@ export type AddSignatureFormProps = {
onSubmit: (_data: TAddSignatureFormSchema) => Promise | void;
requireName?: boolean;
+ requireCustomText?: boolean;
requireSignature?: boolean;
};
@@ -54,6 +55,7 @@ export const AddSignatureFormPartial = ({
onSubmit,
requireName = false,
+ requireCustomText = false,
requireSignature = true,
}: AddSignatureFormProps) => {
const { currentStep, totalSteps } = useStep();
@@ -70,6 +72,14 @@ export const AddSignatureFormPartial = ({
});
}
+ if (requireCustomText && val.customText.length === 0) {
+ ctx.addIssue({
+ path: ['customText'],
+ code: 'custom',
+ message: 'Text is required',
+ });
+ }
+
if (requireSignature && val.signature.length === 0) {
ctx.addIssue({
path: ['signature'],
@@ -85,6 +95,7 @@ export const AddSignatureFormPartial = ({
name: '',
email: '',
signature: '',
+ customText: '',
},
});
@@ -131,6 +142,11 @@ export const AddSignatureFormPartial = ({
return !form.formState.errors.email;
}
+ if (fieldType === FieldType.TEXT) {
+ await form.trigger('customText');
+ return !form.formState.errors.customText;
+ }
+
return true;
};
@@ -154,6 +170,11 @@ export const AddSignatureFormPartial = ({
customText: form.getValues('name'),
inserted: true,
}))
+ .with(FieldType.TEXT, () => ({
+ ...field,
+ customText: form.getValues('customText'),
+ inserted: true,
+ }))
.with(FieldType.SIGNATURE, () => {
const value = form.getValues('signature');
@@ -302,6 +323,29 @@ export const AddSignatureFormPartial = ({
)}
/>
)}
+
+ {requireCustomText && (
+ (
+
+ Custom Text
+
+ {
+ onFormValueChange(FieldType.TEXT);
+ field.onChange(value);
+ }}
+ />
+
+
+
+ )}
+ />
+ )}
@@ -330,7 +374,7 @@ export const AddSignatureFormPartial = ({
{localFields.map((field) =>
match(field.type)
- .with(FieldType.DATE, FieldType.EMAIL, FieldType.NAME, () => {
+ .with(FieldType.DATE, FieldType.TEXT, FieldType.EMAIL, FieldType.NAME, () => {
return (
= {
- SIGNER: ,
- APPROVER: ,
- CC: ,
- VIEWER: ,
-};
-
export type AddSignersFormProps = {
documentFlow: DocumentFlowStep;
recipients: Recipient[];
diff --git a/packages/ui/primitives/document-flow/add-subject.tsx b/packages/ui/primitives/document-flow/add-subject.tsx
index 40e42e3b3..bfc7f3fc5 100644
--- a/packages/ui/primitives/document-flow/add-subject.tsx
+++ b/packages/ui/primitives/document-flow/add-subject.tsx
@@ -226,7 +226,7 @@ export const AddSubjectFormPartial = ({
>
)}
-
+
diff --git a/packages/ui/primitives/document-flow/field-item.tsx b/packages/ui/primitives/document-flow/field-item.tsx
index 716768c18..9c2d5092c 100644
--- a/packages/ui/primitives/document-flow/field-item.tsx
+++ b/packages/ui/primitives/document-flow/field-item.tsx
@@ -128,24 +128,22 @@ export const FieldItem = ({
)}
{FRIENDLY_FIELD_TYPE[field.type]}
-
- {field.signerEmail}
-
+ {field.signerEmail}
,
diff --git a/packages/ui/primitives/document-flow/single-player-mode-fields.tsx b/packages/ui/primitives/document-flow/single-player-mode-fields.tsx
index 7cecd7131..2ef115e4b 100644
--- a/packages/ui/primitives/document-flow/single-player-mode-fields.tsx
+++ b/packages/ui/primitives/document-flow/single-player-mode-fields.tsx
@@ -172,6 +172,7 @@ export function SinglePlayerModeCustomTextField({
.with(FieldType.DATE, () => 'Date')
.with(FieldType.NAME, () => 'Name')
.with(FieldType.EMAIL, () => 'Email')
+ .with(FieldType.TEXT, () => 'Text')
.with(FieldType.SIGNATURE, FieldType.FREE_SIGNATURE, () => 'Signature')
.otherwise(() => '')}
diff --git a/packages/ui/primitives/recipient-role-icons.tsx b/packages/ui/primitives/recipient-role-icons.tsx
new file mode 100644
index 000000000..5bc4f34b9
--- /dev/null
+++ b/packages/ui/primitives/recipient-role-icons.tsx
@@ -0,0 +1,10 @@
+import { BadgeCheck, Copy, Eye, PencilLine } from 'lucide-react';
+
+import type { RecipientRole } from '.prisma/client';
+
+export const ROLE_ICONS: Record = {
+ SIGNER: ,
+ APPROVER: ,
+ CC: ,
+ VIEWER: ,
+};
diff --git a/packages/ui/primitives/template-flow/add-template-fields.tsx b/packages/ui/primitives/template-flow/add-template-fields.tsx
index bb9c304d9..b09d740f5 100644
--- a/packages/ui/primitives/template-flow/add-template-fields.tsx
+++ b/packages/ui/primitives/template-flow/add-template-fields.tsx
@@ -1,6 +1,6 @@
'use client';
-import { useCallback, useEffect, useRef, useState } from 'react';
+import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { Caveat } from 'next/font/google';
@@ -10,9 +10,10 @@ import { useFieldArray, useForm } from 'react-hook-form';
import { getBoundingClientRect } from '@documenso/lib/client-only/get-bounding-client-rect';
import { useDocumentElement } from '@documenso/lib/client-only/hooks/use-document-element';
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
+import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
import { nanoid } from '@documenso/lib/universal/id';
import type { Field, Recipient } from '@documenso/prisma/client';
-import { FieldType } from '@documenso/prisma/client';
+import { FieldType, RecipientRole } from '@documenso/prisma/client';
import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button';
import { Card, CardContent } from '@documenso/ui/primitives/card';
@@ -291,6 +292,28 @@ export const AddTemplateFieldsFormPartial = ({
setSelectedSigner(recipients[0]);
}, [recipients]);
+ const recipientsByRole = useMemo(() => {
+ const recipientsByRole: Record = {
+ CC: [],
+ VIEWER: [],
+ SIGNER: [],
+ APPROVER: [],
+ };
+
+ recipients.forEach((recipient) => {
+ recipientsByRole[recipient.role].push(recipient);
+ });
+
+ return recipientsByRole;
+ }, [recipients]);
+
+ const recipientsByRoleToDisplay = useMemo(() => {
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
+ return (Object.entries(recipientsByRole) as [RecipientRole, Recipient[]][]).filter(
+ ([role]) => role !== RecipientRole.CC && role !== RecipientRole.VIEWER,
+ );
+ }, [recipientsByRole]);
+
return (
<>
@@ -363,55 +386,49 @@ export const AddTemplateFieldsFormPartial = ({
-
- {recipients.map((recipient, index) => (
- {
- setSelectedSigner(recipient);
- setShowRecipientsSelector(false);
- }}
- >
- {/* {recipient.sendStatus !== SendStatus.SENT ? (
-
- ) : (
-
-
-
-
-
- This document has already been sent to this recipient. You can no
- longer edit this recipient.
-
-
- )} */}
+ {recipientsByRoleToDisplay.map(([role, recipients], roleIndex) => (
+
+
+ {`${RECIPIENT_ROLES_DESCRIPTION[role].roleName}s`}
+
- {recipient.name && (
+ {recipients.length === 0 && (
+
+ No recipients with this role
+
+ )}
+
+ {recipients.map((recipient) => (
+ {
+ setSelectedSigner(recipient);
+ setShowRecipientsSelector(false);
+ }}
+ >
- {recipient.name} ({recipient.email})
-
- )}
+ {recipient.name && (
+
+ {recipient.name} ({recipient.email})
+
+ )}
- {!recipient.name && (
-
- {recipient.email}
+ {!recipient.name && (
+ {recipient.email}
+ )}
- )}
-
- ))}
-
+
+ ))}
+
+ ))}
@@ -511,6 +528,28 @@ export const AddTemplateFieldsFormPartial = ({
+
+ setSelectedField(FieldType.TEXT)}
+ onMouseDown={() => setSelectedField(FieldType.TEXT)}
+ data-selected={selectedField === FieldType.TEXT ? true : undefined}
+ >
+
+
+
+ {'Text'}
+
+
+ Custom Text
+
+
+
diff --git a/packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx b/packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx
index ebe48b562..87ec48ad1 100644
--- a/packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx
+++ b/packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx
@@ -5,10 +5,10 @@ import React, { useId, useState } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { AnimatePresence, motion } from 'framer-motion';
import { Plus, Trash } from 'lucide-react';
-import { useFieldArray, useForm } from 'react-hook-form';
+import { Controller, useFieldArray, useForm } from 'react-hook-form';
import { nanoid } from '@documenso/lib/universal/id';
-import type { Field, Recipient } from '@documenso/prisma/client';
+import { type Field, type Recipient, RecipientRole } from '@documenso/prisma/client';
import { Button } from '@documenso/ui/primitives/button';
import { FormErrorMessage } from '@documenso/ui/primitives/form/form-error-message';
import { Input } from '@documenso/ui/primitives/input';
@@ -21,6 +21,8 @@ import {
DocumentFlowFormContainerStep,
} from '../document-flow/document-flow-root';
import type { DocumentFlowStep } from '../document-flow/types';
+import { ROLE_ICONS } from '../recipient-role-icons';
+import { Select, SelectContent, SelectItem, SelectTrigger } from '../select';
import { useStep } from '../stepper';
import type { TAddTemplatePlacholderRecipientsFormSchema } from './add-template-placeholder-recipients.types';
import { ZAddTemplatePlacholderRecipientsFormSchema } from './add-template-placeholder-recipients.types';
@@ -59,12 +61,14 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
formId: String(recipient.id),
name: recipient.name,
email: recipient.email,
+ role: recipient.role,
}))
: [
{
formId: initialId,
name: `Recipient 1`,
email: `recipient.1@documenso.com`,
+ role: RecipientRole.SIGNER,
},
],
},
@@ -86,6 +90,7 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
formId: nanoid(12),
name: `Recipient ${placeholderRecipientCount}`,
email: `recipient.${placeholderRecipientCount}@documenso.com`,
+ role: RecipientRole.SIGNER,
});
setPlaceholderRecipientCount((count) => count + 1);
@@ -95,12 +100,6 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
removeSigner(index);
};
- const onKeyDown = (event: React.KeyboardEvent
) => {
- if (event.key === 'Enter' && event.target instanceof HTMLInputElement) {
- onAddPlaceholderRecipient();
- }
- };
-
return (
<>
@@ -113,10 +112,7 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
className="flex flex-wrap items-end gap-x-4"
>
-
- Email
- *
-
+ Email
+
+
(
+ onChange(x)}>
+ {ROLE_ICONS[value]}
+
+
+
+
+ {ROLE_ICONS[RecipientRole.SIGNER]}
+ Signer
+
+
+
+
+
+ {ROLE_ICONS[RecipientRole.CC]}
+ Receives copy
+
+
+
+
+
+ {ROLE_ICONS[RecipientRole.APPROVER]}
+ Approver
+
+
+
+
+
+ {ROLE_ICONS[RecipientRole.VIEWER]}
+ Viewer
+
+
+
+
+ )}
+ />
+
+