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/README.md b/README.md
index 6d2fab334..cdb687264 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
->We are nominated for a Product Hunt Gold Kitty ๐บโจ and appreciate any support: https://documen.so/kitty
+> ๐จ It is Launch Week #2 - Day 1: We launches teams ๐ https://documen.so/day1
@@ -107,7 +107,7 @@ Contact us if you are interested in our Enterprise plan for large organizations
To run Documenso locally, you will need
-- Node.js
+- Node.js (v18 or above)
- Postgres SQL Database
- Docker (optional)
diff --git a/apps/marketing/content/blog/early-adopters.mdx b/apps/marketing/content/blog/early-adopters.mdx
index 2ff7ae1f6..ddd779fbc 100644
--- a/apps/marketing/content/blog/early-adopters.mdx
+++ b/apps/marketing/content/blog/early-adopters.mdx
@@ -24,6 +24,8 @@ tags:
+> ๐ UPDATE: We launched teams and the early adopters plan will be replaced by the new teams pricing as soon as all availible early adopters seats are filled.
+
## Community-Driven Development
As we ramp up hiring and development speed for Documenso, I want to discuss how we plan to build its core version.
diff --git a/apps/marketing/content/blog/launch-week-2-day-1.mdx b/apps/marketing/content/blog/launch-week-2-day-1.mdx
new file mode 100644
index 000000000..2799baafe
--- /dev/null
+++ b/apps/marketing/content/blog/launch-week-2-day-1.mdx
@@ -0,0 +1,64 @@
+---
+title: Launch Week II - Day 1 - Teams
+description: Teams for Documenso are here. And they come free for early adopters!
+authorName: 'Timur Ercan'
+authorImage: '/blog/blog-author-timur.jpeg'
+authorRole: 'Co-Founder'
+date: 2024-02-26
+tags:
+ - Launch Week
+ - Teams
+ - Early Adopter Perks
+---
+
+
+
+> TLDR; Docucmenso now supports teams that share documents, templates and a team mail address. Early Adopter get UNLIMITED1 Users.
+
+## Kicking off Launch Week II - "Connected"
+
+The day has come! Roughly 5 months after kicked off our first launch week with open sourcing our design and Malfunction Mania, Launch Week #2 is here ๐ This Launch Week's theme is "connected", since this is all about connecting humans, machines and documents.
+
+Working with documents and getting that signature is a team sport. This is why we are kicking it off today with a very long-awaited feature: Documenso now supports teams!
+
+## Introducing Teams for Documenso
+
+You can now create teams next to your personal account: Simply invite your colleagues, and you can include everyone you like in working with your documents. With teams, you can:
+
+- Send unlimited signature requests with unlimited recipients
+- Create, view, edit and sign documents owned by the team
+- Define a dedicated team email, to receive signing requests into a team inbox for the owner to sign
+- Manage team roles: Member (Create+Edit), Managers (+Manage Team Members), Owner (+Transfer Team +Delete Team + Sign Documents sent to team email)
+
+## Pricing
+
+Together with Teams, we are announcing the new teams pricing:
+
+- $10 per seat per month
+- 5 seats minimum
+- You can add seats dynamically as needed
+
+This pricing will take effect, as soon as the early adopter seats run out. Want to check out teams: [https://documen.so/teams](https://documen.so/teams).
+
+## Early Adopter Perks
+
+There is one more point on pricing I have been looking forward to for a long time:
+
+All early adopter plans now include **UNLIMITED teams and users**1 . We appreciate your support so far very much, and I'm happy to announce this first of more early adopter perks to come. We have roughly 48 early adopter plans left, so if you plan to onboard your team, now is a great time to [grab your early adopter seat.](https://documen.so/claim-early-adopters-plan)
+
+We are eager to hear from all teams users how you like this addition and what we can add to make it even better. Connect with us on [Twitter / X](https://twitter.com/eltimuro) (DM open) or [Discord](https://documen.so/discord) if you have any questions or comments! We're always here, and we would love to hear from you :)
+
+> ๐จ We need you help to help us to make this the biggest launch week yet: Support us on Twitter or anywhere to spread awareness for open signing! The best posts will receive merch codes ๐
+
+Best from Hamburg\
+Timur
+
+\
+[1] Within reason. If you are unsure what that means, feel free to contact hi@documenso.com and ask for clarification if it's more than 100.
diff --git a/apps/marketing/content/blog/launch-week-2-day-2.mdx b/apps/marketing/content/blog/launch-week-2-day-2.mdx
new file mode 100644
index 000000000..3a67977ec
--- /dev/null
+++ b/apps/marketing/content/blog/launch-week-2-day-2.mdx
@@ -0,0 +1,76 @@
+---
+title: Launch Week II - Day 2 - Templates
+description: Templates help you prepare regular documents faster. And you can share them with your team!
+authorName: 'Timur Ercan'
+authorImage: '/blog/blog-author-timur.jpeg'
+authorRole: 'Co-Founder'
+date: 2024-02-27
+tags:
+ - Launch Week
+ - Templates
+---
+
+
+
+> TLDR; You can now reuse documents via templates. More field types coming soon as well.
+
+## Introducing Templates
+
+It's day 2 of Launch Week, everybody ๐ After introducing [Teams](https://documenso.com/blog/launch-week-2-day-1) yesterday, today we are looking at making Documenso faster for daily use:
+We are launching templates for Documenso! Templates are an easy way to reuse documents you send out often with just a few clicks. With templates, you can:
+
+
+
+
+
+ Quickly fill out recipients, when creating from a template
+
+
+
+- Save often-uploaded documents for reuse
+- Pre-define fields, so you just have to send the document
+- Quickly fill out recipients and roles for new documents
+- Share templates with your team to make working together even easier
+
+
+
+
+
+ POV: You are a diligent german and create custom receipts with Documenso
+
+
+
+## Pricing
+
+Templates are **included in all Documenso Plans!** That includes our free tier: The limit of 5 documents per month still applies, but you are free to reach it with less friction using templates. Sharing templates with other users is only possible with the teams plan. If you want to share templates with people not in your team, we might have something coming up later this week ๐
+
+## What's Next for Templates
+
+We have a lot of great stuff coming up for templates as well:
+
+- More Field Types are in the pipeline
+- Sharing Templates Externally ๐
+
+Check out templates [here](https://documen.so/templates) and let us know what you think and what we can improve. Which field types are you missing? Connect with us on [Twitter / X](https://twitter.com/eltimuro) (DM open) or [Discord](https://documen.so/discord) if you have any questions or comments! We're always here and would love to hear from you :)
+
+> ๐จ We need you help to help us to make this the biggest launch week yet: Support us on Twitter or anywhere to spread awareness for open signing! The best posts will receive merch codes ๐
+
+Best from Hamburg\
+Timur
diff --git a/apps/marketing/content/blog/launch-week-2-day-3.mdx b/apps/marketing/content/blog/launch-week-2-day-3.mdx
new file mode 100644
index 000000000..6ea0db9b9
--- /dev/null
+++ b/apps/marketing/content/blog/launch-week-2-day-3.mdx
@@ -0,0 +1,53 @@
+---
+title: Launch Week II - Day 3 - API
+description: Documenso's mission is to create a plattform developers all around the world can build upon. Today we are releasing the first version of our public API, included in all plans!
+authorName: 'Timur Ercan'
+authorImage: '/blog/blog-author-timur.jpeg'
+authorRole: 'Co-Founder'
+date: 2024-02-28
+tags:
+ - Launch Week
+ - API
+---
+
+
+
+> TLDR; The public API is now availible for all plans.
+
+## Introducing the public Documenso API
+
+Launch. Week. Day. 3 ๐ Documenso's mission is to create a platform that developers all around the world can build upon. Today we are releasing the first version of our public API, and we are pumped. Since this is the first version, we focused on the basics. With the new API you can:
+
+- Get Documents (Individual or all Accessible)
+- Upload Documents
+- Delete Documents
+- Create Documents from Templates
+- Trigger Sending Documents for Singing
+
+You can check out the detailed API documentation here:
+
+> API DOCUMENTATION: [https://app.documenso.com/api/v1/openapi](https://app.documenso.com/api/v1/openapi)
+
+## Pricing
+
+We are building Documenso to be an open and extendable platform; therefore the API is included in all current plans. The API is authenticated via auth tokens, which every user can create at no extra cost, as can teams. Existing limits still apply (i.e., the number of included documents for the free plan). While we don't have all the details yet, we don't intend to price the API usage in itself (rather the accounts using it) since we want you to build on Documenso without being smothered by API costs.
+
+> Try the API here for free: [https://documen.so/api](https://documen.so/api)
+
+## What's next for the API
+
+You tell us. This is by far the most requested feature, so we would like to hear from you. What should we add? How can we integrate even better?
+
+Connect with us on [Twitter / X](https://twitter.com/eltimuro) (DM open) or [Discord](https://documen.so/discord) if you have any questions or comments! We're always here and would love to hear from you :)
+
+> ๐จ We need you help to help us to make this the biggest launch week yet: Support us on Twitter or anywhere to spread awareness for open signing! The best posts will receive merch codes ๐
+
+Best from Hamburg\
+Timur
diff --git a/apps/marketing/content/blog/launch-week-2-day-4.mdx b/apps/marketing/content/blog/launch-week-2-day-4.mdx
new file mode 100644
index 000000000..b6f5691fd
--- /dev/null
+++ b/apps/marketing/content/blog/launch-week-2-day-4.mdx
@@ -0,0 +1,63 @@
+---
+title: Launch Week II - Day 4 - Webhooks and Zapier
+description: If you want to integrate Documenso without fiddling with the API, we got you as well. You can now integrate Documenso via Zapier, included in all plans!
+authorName: 'Timur Ercan'
+authorImage: '/blog/blog-author-timur.jpeg'
+authorRole: 'Co-Founder'
+date: 2024-02-29
+tags:
+ - Launch Week
+ - Zapier
+ - Webhooks
+---
+
+
+
+> TLDR; Zapier Integration is now available for all plans.
+
+## Introducing Zapier for Documenso
+
+Day 4 ๐ฅณ Yesterday we introduced our [public API](https://documen.so/day3) for developers to build on Documenso. If you are not a developer or simple want a quicker integration this is for you: Documenso now support Zapier Integrations! Just connect your Documenso account via a simple login flow with Zapier and you will have access to Zapier's universe of integrations ๐ซ The integration currently supports:
+
+- Document Created ([https://documen.so/zapier-created](https://documen.so/zapier-created))
+- Document Sent ([Chttps://documen.so/zapier-sent](https://documen.so/zapier-sent))
+- Document Opened ([https://documen.so/zapier-opened](https://documen.so/zapier-opened))
+- Document Signed ([https://documen.so/zapier-signed](https://documen.so/zapier-signed))
+- Document Completed ([https://documen.so/zapier-completed](https://documen.so/zapier-completed))
+
+> โก๏ธ You can create your own Zaps here: https://zapier.com/apps/documenso/integrations
+
+Each event comes with extensive meta-data for you to use in Zapier. Missing something? Reach out on [Twitter / X](https://twitter.com/eltimuro) (DM open) or [Discord](https://documen.so/discord). We're always here and would love to hear from you :)
+
+## Also Introducing for Documenso: Webhooks
+
+To build the Zapier Integration, we needed a good webhooks concept, so we added that as well. Together with your Zaps, you can also now create customer webhooks in Documenso. You can try webhooks here for free: [https://documen.so/webhooks](https://documen.so/webhooks)
+
+
+
+
+
+ Create unlimited custom webhooks with each plan.
+
+
+
+## Pricing
+
+Just like the API, we consider the Zapier integration and webhooks part of the open Documenso platform. Zapier is **available for all Documenso plans**, including free! [Login now](https://documen.so/login) to check it out.
+
+> ๐จ We need you help to help us to make this the biggest launch week yet: Support us on Twitter or anywhere to spread awareness for open signing! The best posts will receive merch codes ๐
+
+Best from Hamburg\
+Timur
diff --git a/apps/marketing/content/blog/launch-week-2-day-5.mdx b/apps/marketing/content/blog/launch-week-2-day-5.mdx
new file mode 100644
index 000000000..04d639206
--- /dev/null
+++ b/apps/marketing/content/blog/launch-week-2-day-5.mdx
@@ -0,0 +1,61 @@
+---
+title: Launch Week II - Day 5 - Documenso Profiles
+description: Documenso profiles allow you to send signing links to people so they can sign anytime and see who you are. Documenso Profile Usernames can be claimed starting today. Long ones free, short ones paid. Profiles will launch as soon as they are shiny.
+authorName: 'Timur Ercan'
+authorImage: '/blog/blog-author-timur.jpeg'
+authorRole: 'Co-Founder'
+date: 2024-03-01
+tags:
+ - Launch Week
+ - Profiles
+---
+
+
+
+> TLDR; Documenso profiles allow you to send signing links to people so they can sign anytime and see who you are. Documenso Profile Usernames can be claimed starting today. Long ones free, short ones paid. Profiles launch as soon as they are shiny.
+
+## Introducing Documenso Profile Links
+
+Day 5 - The Finale ๐ฅ
+
+Signing documents has always been between humans, and signing something together should be as frictionless as possible. It should also be async, so you don't force your counterpart to jog to their device to send something when you are ready. Today we are announcing the new Documenso Profiles:
+
+
+
+
+
+ Async > Sync: Add public templates to your Documenso Link and let people sign whenever they are ready.
+
+
+
+Documenso profiles work with your existing templates. You can just add them to your public profile to let everyone with your link sign them. With profiles, we want to bring back the human aspect of signing.
+
+By making profiles public, you can always access what your counterparty offers and make them more visible in the process. Long-term, we plan to add more to profiles to help you ensure the person you are dealing with is who they claim to be. Documenso wants to be the trust layer of the internet, and we want to start at the very fundamental level: The individual transaction.
+
+Profiles are our first step towards bringing more trust into everything, simply by making the use of signing more frictionless. As there is more and more content of questionable origin out there, we want to support you in making it clear what you send out and what not.
+
+## Pricing and Claiming
+
+Documenso profile username can be claimed starting today. Documenso profiles will launch as soon as we are happy with the details โจ
+
+- Long usernames (6 characters or more) come free with every account, e.g. **documenso.com/u/timurercan**
+- Short usernames (5 characters or fewer) or less require any paid account ([Early Adopter](https://documen.so/claim-early-adopters-plan), [Teams](https://documen.so/teams) or Enterprise): **e.g., documenso.com/u/timur**
+
+You can claim your username here: [https://documen.so/claim](https://documen.so/claim)
+
+> ๐จ We need you help to help us to make this the biggest launch week yet: Support us on Twitter or anywhere to spread awareness for open signing! The best posts will receive merch codes ๐
+
+Best from Hamburg\
+Timur
diff --git a/apps/marketing/public/blog/hooks.png b/apps/marketing/public/blog/hooks.png
new file mode 100644
index 000000000..9c324db0b
Binary files /dev/null and b/apps/marketing/public/blog/hooks.png differ
diff --git a/apps/marketing/public/blog/profile.png b/apps/marketing/public/blog/profile.png
new file mode 100644
index 000000000..b216e9758
Binary files /dev/null and b/apps/marketing/public/blog/profile.png differ
diff --git a/apps/marketing/public/blog/quickfill.png b/apps/marketing/public/blog/quickfill.png
new file mode 100644
index 000000000..17765b643
Binary files /dev/null and b/apps/marketing/public/blog/quickfill.png differ
diff --git a/apps/marketing/public/blog/template.png b/apps/marketing/public/blog/template.png
new file mode 100644
index 000000000..74dd48de9
Binary files /dev/null and b/apps/marketing/public/blog/template.png differ
diff --git a/apps/marketing/src/app/(marketing)/layout.tsx b/apps/marketing/src/app/(marketing)/layout.tsx
index dd1a46418..c5f761853 100644
--- a/apps/marketing/src/app/(marketing)/layout.tsx
+++ b/apps/marketing/src/app/(marketing)/layout.tsx
@@ -2,8 +2,12 @@
import React, { useEffect, useState } from 'react';
+import Image from 'next/image';
import { usePathname } from 'next/navigation';
+import launchWeekTwoImage from '@documenso/assets/images/background-lw-2.png';
+import { useFeatureFlags } from '@documenso/lib/client-only/providers/feature-flag';
+import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { cn } from '@documenso/ui/lib/utils';
import { Footer } from '~/components/(marketing)/footer';
@@ -17,6 +21,10 @@ export default function MarketingLayout({ children }: MarketingLayoutProps) {
const [scrollY, setScrollY] = useState(0);
const pathname = usePathname();
+ const { getFlag } = useFeatureFlags();
+
+ const showProfilesAnnouncementBar = getFlag('marketing_profiles_announcement_bar');
+
useEffect(() => {
const onScroll = () => {
setScrollY(window.scrollY);
@@ -38,6 +46,31 @@ export default function MarketingLayout({ children }: MarketingLayoutProps) {
'bg-background/50 backdrop-blur-md': scrollY > 5,
})}
>
+ {showProfilesAnnouncementBar && (
+
+
+
+
+
+
+ Claim your documenso public profile username now!{' '}
+
documenso.com/u/yourname
+
+
+
+ )}
+
diff --git a/apps/marketing/src/app/(marketing)/singleplayer/[token]/success/page.tsx b/apps/marketing/src/app/(marketing)/singleplayer/[token]/success/page.tsx
index fbf020c38..51fbaff36 100644
--- a/apps/marketing/src/app/(marketing)/singleplayer/[token]/success/page.tsx
+++ b/apps/marketing/src/app/(marketing)/singleplayer/[token]/success/page.tsx
@@ -27,7 +27,7 @@ export default async function SinglePlayerModeSuccessPage({
return notFound();
}
- const signatures = await getRecipientSignatures({ recipientId: document.Recipient.id });
+ const signatures = await getRecipientSignatures({ recipientId: document.Recipient[0].id });
return ;
}
diff --git a/apps/marketing/src/app/(marketing)/singleplayer/client.tsx b/apps/marketing/src/app/(marketing)/singleplayer/client.tsx
index 1d6604a87..74158249a 100644
--- a/apps/marketing/src/app/(marketing)/singleplayer/client.tsx
+++ b/apps/marketing/src/app/(marketing)/singleplayer/client.tsx
@@ -191,7 +191,7 @@ export const SinglePlayerClient = () => {
Create a{' '}
@@ -257,6 +257,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)/callout.tsx b/apps/marketing/src/components/(marketing)/callout.tsx
index 72ae3907b..990aa163b 100644
--- a/apps/marketing/src/components/(marketing)/callout.tsx
+++ b/apps/marketing/src/components/(marketing)/callout.tsx
@@ -40,9 +40,9 @@ export const Callout = ({ starCount }: CalloutProps) => {
className="rounded-full bg-transparent backdrop-blur-sm"
onClick={onSignUpClick}
>
- Get the Early Adopters Plan
-
- $30/mo. forever!
+ Claim Community Plan
+
+ $30/mo
diff --git a/apps/marketing/src/components/(marketing)/faster-smarter-beautiful-bento.tsx b/apps/marketing/src/components/(marketing)/faster-smarter-beautiful-bento.tsx
index ee123d7ad..b80b2fe8c 100644
--- a/apps/marketing/src/components/(marketing)/faster-smarter-beautiful-bento.tsx
+++ b/apps/marketing/src/components/(marketing)/faster-smarter-beautiful-bento.tsx
@@ -1,4 +1,4 @@
-import { HTMLAttributes } from 'react';
+import type { HTMLAttributes } from 'react';
import Image from 'next/image';
diff --git a/apps/marketing/src/components/(marketing)/header.tsx b/apps/marketing/src/components/(marketing)/header.tsx
index e1813f7f6..915c13852 100644
--- a/apps/marketing/src/components/(marketing)/header.tsx
+++ b/apps/marketing/src/components/(marketing)/header.tsx
@@ -9,6 +9,7 @@ import Link from 'next/link';
import LogoImage from '@documenso/assets/logo.png';
import { useFeatureFlags } from '@documenso/lib/client-only/providers/feature-flag';
import { cn } from '@documenso/ui/lib/utils';
+import { Button } from '@documenso/ui/primitives/button';
import { HamburgerMenu } from './mobile-hamburger';
import { MobileNavigation } from './mobile-navigation';
@@ -68,12 +69,18 @@ export const Header = ({ className, ...props }: HeaderProps) => {
Sign in
+
+
+
+ Sign up
+
+
{
className="rounded-full bg-transparent backdrop-blur-sm"
onClick={onSignUpClick}
>
- Get the Early Adopters Plan
-
- $30/mo. forever!
+ Claim Community Plan
+
+ $30/mo
@@ -224,8 +225,7 @@ export const Hero = ({ className, ...props }: HeroProps) => {
(in a non-legally binding, but heartfelt way)
{' '}
- and lock in the early supporter plan for forever, including everything we build this
- year.
+ and lock in the community plan for forever, including everything we build this year.
diff --git a/apps/marketing/src/components/(marketing)/mobile-navigation.tsx b/apps/marketing/src/components/(marketing)/mobile-navigation.tsx
index 982e2967a..434b30053 100644
--- a/apps/marketing/src/components/(marketing)/mobile-navigation.tsx
+++ b/apps/marketing/src/components/(marketing)/mobile-navigation.tsx
@@ -47,9 +47,13 @@ export const MENU_NAVIGATION_LINKS = [
text: 'Privacy',
},
{
- href: 'https://app.documenso.com/signin',
+ href: 'https://app.documenso.com/signin?utm_source=marketing-header',
text: 'Sign in',
},
+ {
+ href: 'https://app.documenso.com/signup?utm_source=marketing-header',
+ text: 'Sign up',
+ },
];
export const MobileNavigation = ({ isMenuOpen, onMenuOpenChange }: MobileNavigationProps) => {
diff --git a/apps/marketing/src/components/(marketing)/pricing-table.tsx b/apps/marketing/src/components/(marketing)/pricing-table.tsx
index ce4695199..ab35bcc90 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,11 @@ export const PricingTable = ({ className, ...props }: PricingTableProps) => {
-
+
Signup Now
@@ -113,33 +118,31 @@ export const PricingTable = ({ className, ...props }: PricingTableProps) => {
- Signup Now
+
+ Signup Now
+
+
@@ -65,7 +65,7 @@ export const SinglePlayerModeSuccess = ({
@@ -86,7 +86,7 @@ export const SinglePlayerModeSuccess = ({
Create a{' '}
diff --git a/apps/marketing/src/components/(marketing)/widget.tsx b/apps/marketing/src/components/(marketing)/widget.tsx
index fe7502d27..15e3fbdeb 100644
--- a/apps/marketing/src/components/(marketing)/widget.tsx
+++ b/apps/marketing/src/components/(marketing)/widget.tsx
@@ -199,7 +199,7 @@ export const Widget = ({ className, children, ...props }: WidgetProps) => {
className="bg-foreground/5 col-span-12 flex flex-col rounded-2xl p-6 lg:col-span-5"
onSubmit={handleSubmit(onFormSubmit)}
>
-
Sign up for the early adopters plan
+
Sign up to Community Plan
with Timur Ercan & Lucas Smith from Documenso
@@ -208,7 +208,7 @@ export const Widget = ({ className, children, ...props }: WidgetProps) => {
-
+
Whatโs your email?
@@ -220,7 +220,7 @@ export const Widget = ({ className, children, ...props }: WidgetProps) => {
@@ -265,11 +265,8 @@ export const Widget = ({ className, children, ...props }: WidgetProps) => {
transform: 'translateX(25%)',
}}
>
-
- and your name?
+
+ And your name?
{
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)/documents/page.tsx b/apps/web/src/app/(dashboard)/documents/page.tsx
index 67f432a13..6ce0ce2c0 100644
--- a/apps/web/src/app/(dashboard)/documents/page.tsx
+++ b/apps/web/src/app/(dashboard)/documents/page.tsx
@@ -1,7 +1,10 @@
import type { Metadata } from 'next';
+import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
+
import type { DocumentsPageViewProps } from './documents-page-view';
import { DocumentsPageView } from './documents-page-view';
+import { UpcomingProfileClaimTeaser } from './upcoming-profile-claim-teaser';
export type DocumentsPageProps = {
searchParams?: DocumentsPageViewProps['searchParams'];
@@ -11,6 +14,12 @@ export const metadata: Metadata = {
title: 'Documents',
};
-export default function DocumentsPage({ searchParams = {} }: DocumentsPageProps) {
- return
;
+export default async function DocumentsPage({ searchParams = {} }: DocumentsPageProps) {
+ const { user } = await getRequiredServerComponentSession();
+ return (
+ <>
+
+
+ >
+ );
}
diff --git a/apps/web/src/app/(dashboard)/documents/upcoming-profile-claim-teaser.tsx b/apps/web/src/app/(dashboard)/documents/upcoming-profile-claim-teaser.tsx
new file mode 100644
index 000000000..a2b3aea69
--- /dev/null
+++ b/apps/web/src/app/(dashboard)/documents/upcoming-profile-claim-teaser.tsx
@@ -0,0 +1,52 @@
+'use client';
+
+import { useCallback, useEffect, useState } from 'react';
+
+import type { User } from '@documenso/prisma/client';
+import { useToast } from '@documenso/ui/primitives/use-toast';
+
+import { ClaimPublicProfileDialogForm } from '~/components/forms/public-profile-claim-dialog';
+
+export type UpcomingProfileClaimTeaserProps = {
+ user: User;
+};
+
+export const UpcomingProfileClaimTeaser = ({ user }: UpcomingProfileClaimTeaserProps) => {
+ const { toast } = useToast();
+
+ const [open, setOpen] = useState(false);
+ const [claimed, setClaimed] = useState(false);
+
+ const onOpenChange = useCallback(
+ (open: boolean) => {
+ if (!open && !claimed) {
+ toast({
+ title: 'Claim your profile later',
+ description: 'You can claim your profile later on by going to your profile settings!',
+ });
+ }
+
+ setOpen(open);
+ localStorage.setItem('app.hasShownProfileClaimDialog', 'true');
+ },
+ [claimed, toast],
+ );
+
+ useEffect(() => {
+ const hasShownProfileClaimDialog =
+ localStorage.getItem('app.hasShownProfileClaimDialog') === 'true';
+
+ if (!user.url && !hasShownProfileClaimDialog) {
+ onOpenChange(true);
+ }
+ }, [onOpenChange, user.url]);
+
+ return (
+
setClaimed(true)}
+ user={user}
+ />
+ );
+};
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/claim-profile-alert-dialog.tsx b/apps/web/src/app/(dashboard)/settings/profile/claim-profile-alert-dialog.tsx
new file mode 100644
index 000000000..827ea1cbe
--- /dev/null
+++ b/apps/web/src/app/(dashboard)/settings/profile/claim-profile-alert-dialog.tsx
@@ -0,0 +1,45 @@
+'use client';
+
+import { useState } from 'react';
+
+import type { User } from '@documenso/prisma/client';
+import { cn } from '@documenso/ui/lib/utils';
+import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
+import { Button } from '@documenso/ui/primitives/button';
+
+import { ClaimPublicProfileDialogForm } from '~/components/forms/public-profile-claim-dialog';
+
+export type ClaimProfileAlertDialogProps = {
+ className?: string;
+ user: User;
+};
+
+export const ClaimProfileAlertDialog = ({ className, user }: ClaimProfileAlertDialogProps) => {
+ const [open, setOpen] = useState(false);
+
+ return (
+ <>
+
+
+
Claim your profile
+
+ Profiles are coming soon! Claim your profile username now to reserve your corner of the
+ signing revolution.
+
+
+
+
+ setOpen(true)}>Claim Now
+
+
+
+
+ >
+ );
+};
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..669c149b5 100644
--- a/apps/web/src/app/(dashboard)/settings/profile/page.tsx
+++ b/apps/web/src/app/(dashboard)/settings/profile/page.tsx
@@ -5,6 +5,9 @@ import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-
import { SettingsHeader } from '~/components/(dashboard)/settings/layout/header';
import { ProfileForm } from '~/components/forms/profile';
+import { ClaimProfileAlertDialog } from './claim-profile-alert-dialog';
+import { DeleteAccountDialog } from './delete-account-dialog';
+
export const metadata: Metadata = {
title: 'Profile',
};
@@ -16,7 +19,13 @@ export default async function ProfileSettingsPage() {
);
}
diff --git a/apps/web/src/app/(dashboard)/settings/security/activity/page.tsx b/apps/web/src/app/(dashboard)/settings/security/activity/page.tsx
index 6e183b0c7..2b5906177 100644
--- a/apps/web/src/app/(dashboard)/settings/security/activity/page.tsx
+++ b/apps/web/src/app/(dashboard)/settings/security/activity/page.tsx
@@ -1,5 +1,8 @@
import type { Metadata } from 'next';
+import { SettingsHeader } from '~/components/(dashboard)/settings/layout/header';
+
+import ActivityPageBackButton from '../../../../../components/(dashboard)/settings/layout/activity-back';
import { UserSecurityActivityDataTable } from './user-security-activity-data-table';
export const metadata: Metadata = {
@@ -9,11 +12,14 @@ export const metadata: Metadata = {
export default function SettingsSecurityActivityPage() {
return (
-
Security activity
-
-
- View all recent security activity related to your account.
-
+
+
+
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..a62775522
--- /dev/null
+++ b/apps/web/src/app/(dashboard)/settings/tokens/page.tsx
@@ -0,0 +1,83 @@
+import { DateTime } from 'luxon';
+
+import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
+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.
+ You can view our swagger docs{' '}
+
+ here
+
+
+
+
+
+
+
+
+
+
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)/settings/webhooks/[id]/page.tsx b/apps/web/src/app/(dashboard)/settings/webhooks/[id]/page.tsx
new file mode 100644
index 000000000..53ec24827
--- /dev/null
+++ b/apps/web/src/app/(dashboard)/settings/webhooks/[id]/page.tsx
@@ -0,0 +1,201 @@
+'use client';
+
+import { useRouter } from 'next/navigation';
+
+import { zodResolver } from '@hookform/resolvers/zod';
+import { Loader } from 'lucide-react';
+import { useForm } from 'react-hook-form';
+import type { z } from 'zod';
+
+import { trpc } from '@documenso/trpc/react';
+import { ZEditWebhookMutationSchema } from '@documenso/trpc/server/webhook-router/schema';
+import { Button } from '@documenso/ui/primitives/button';
+import {
+ Form,
+ FormControl,
+ FormDescription,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from '@documenso/ui/primitives/form/form';
+import { Input } from '@documenso/ui/primitives/input';
+import { PasswordInput } from '@documenso/ui/primitives/password-input';
+import { Switch } from '@documenso/ui/primitives/switch';
+import { useToast } from '@documenso/ui/primitives/use-toast';
+
+import { SettingsHeader } from '~/components/(dashboard)/settings/layout/header';
+import { TriggerMultiSelectCombobox } from '~/components/(dashboard)/settings/webhooks/trigger-multiselect-combobox';
+
+const ZEditWebhookFormSchema = ZEditWebhookMutationSchema.omit({ id: true });
+
+type TEditWebhookFormSchema = z.infer
;
+
+export type WebhookPageOptions = {
+ params: {
+ id: string;
+ };
+};
+
+export default function WebhookPage({ params }: WebhookPageOptions) {
+ const { toast } = useToast();
+ const router = useRouter();
+
+ const { data: webhook, isLoading } = trpc.webhook.getWebhookById.useQuery(
+ {
+ id: params.id,
+ },
+ { enabled: !!params.id },
+ );
+
+ const { mutateAsync: updateWebhook } = trpc.webhook.editWebhook.useMutation();
+
+ const form = useForm({
+ resolver: zodResolver(ZEditWebhookFormSchema),
+ values: {
+ webhookUrl: webhook?.webhookUrl ?? '',
+ eventTriggers: webhook?.eventTriggers ?? [],
+ secret: webhook?.secret ?? '',
+ enabled: webhook?.enabled ?? true,
+ },
+ });
+
+ const onSubmit = async (data: TEditWebhookFormSchema) => {
+ try {
+ await updateWebhook({
+ id: params.id,
+ ...data,
+ });
+
+ toast({
+ title: 'Webhook updated',
+ description: 'The webhook has been updated successfully.',
+ duration: 5000,
+ });
+
+ router.refresh();
+ } catch (err) {
+ toast({
+ title: 'Failed to update webhook',
+ description: 'We encountered an error while updating the webhook. Please try again later.',
+ variant: 'destructive',
+ });
+ }
+ };
+
+ return (
+
+
+
+ {isLoading && (
+
+
+
+ )}
+
+
+
+
+
+
+ (
+
+ Triggers
+
+ {
+ onChange(values);
+ }}
+ />
+
+
+
+ The events that will trigger a webhook to be sent to your URL.
+
+
+
+
+ )}
+ />
+
+ (
+
+ Secret
+
+
+
+
+
+ A secret that will be sent to your URL so you can verify that the request has
+ been sent by Documenso.
+
+
+
+ )}
+ />
+
+
+
+ Update webhook
+
+
+
+
+
+
+ );
+}
diff --git a/apps/web/src/app/(dashboard)/settings/webhooks/page.tsx b/apps/web/src/app/(dashboard)/settings/webhooks/page.tsx
new file mode 100644
index 000000000..01196544d
--- /dev/null
+++ b/apps/web/src/app/(dashboard)/settings/webhooks/page.tsx
@@ -0,0 +1,101 @@
+'use client';
+
+import Link from 'next/link';
+
+import { Loader } from 'lucide-react';
+import { DateTime } from 'luxon';
+
+import { toFriendlyWebhookEventName } from '@documenso/lib/universal/webhook/to-friendly-webhook-event-name';
+import { trpc } from '@documenso/trpc/react';
+import { cn } from '@documenso/ui/lib/utils';
+import { Badge } from '@documenso/ui/primitives/badge';
+import { Button } from '@documenso/ui/primitives/button';
+
+import { SettingsHeader } from '~/components/(dashboard)/settings/layout/header';
+import { CreateWebhookDialog } from '~/components/(dashboard)/settings/webhooks/create-webhook-dialog';
+import { DeleteWebhookDialog } from '~/components/(dashboard)/settings/webhooks/delete-webhook-dialog';
+import { LocaleDate } from '~/components/formatter/locale-date';
+
+export default function WebhookPage() {
+ const { data: webhooks, isLoading } = trpc.webhook.getWebhooks.useQuery();
+
+ return (
+
+
+
+
+
+ {isLoading && (
+
+
+
+ )}
+
+ {webhooks && webhooks.length === 0 && (
+ // TODO: Perhaps add some illustrations here to make the page more engaging
+
+
+ You have no webhooks yet. Your webhooks will be shown here once you create them.
+
+
+ )}
+
+ {webhooks && webhooks.length > 0 && (
+
+ {webhooks?.map((webhook) => (
+
+
+
+
{webhook.id}
+
+
+
+ {webhook.webhookUrl}
+
+
+
+ {webhook.enabled ? 'Enabled' : 'Disabled'}
+
+
+
+
+ Listening to{' '}
+ {webhook.eventTriggers
+ .map((trigger) => toFriendlyWebhookEventName(trigger))
+ .join(', ')}
+
+
+
+ Created on{' '}
+
+
+
+
+
+
+ Edit
+
+
+ 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..eedae29d1
--- /dev/null
+++ b/apps/web/src/app/(teams)/t/[teamUrl]/settings/tokens/page.tsx
@@ -0,0 +1,94 @@
+import { DateTime } from 'luxon';
+
+import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
+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.
+ You can view our swagger docs{' '}
+
+ here
+
+
+
+
+
+
+
+
+
+
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/(teams)/t/[teamUrl]/settings/webhooks/[id]/page.tsx b/apps/web/src/app/(teams)/t/[teamUrl]/settings/webhooks/[id]/page.tsx
new file mode 100644
index 000000000..cc7261bda
--- /dev/null
+++ b/apps/web/src/app/(teams)/t/[teamUrl]/settings/webhooks/[id]/page.tsx
@@ -0,0 +1,206 @@
+'use client';
+
+import { useRouter } from 'next/navigation';
+
+import { zodResolver } from '@hookform/resolvers/zod';
+import { Loader } from 'lucide-react';
+import { useForm } from 'react-hook-form';
+import type { z } from 'zod';
+
+import { trpc } from '@documenso/trpc/react';
+import { ZEditWebhookMutationSchema } from '@documenso/trpc/server/webhook-router/schema';
+import { Button } from '@documenso/ui/primitives/button';
+import {
+ Form,
+ FormControl,
+ FormDescription,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from '@documenso/ui/primitives/form/form';
+import { Input } from '@documenso/ui/primitives/input';
+import { PasswordInput } from '@documenso/ui/primitives/password-input';
+import { Switch } from '@documenso/ui/primitives/switch';
+import { useToast } from '@documenso/ui/primitives/use-toast';
+
+import { SettingsHeader } from '~/components/(dashboard)/settings/layout/header';
+import { TriggerMultiSelectCombobox } from '~/components/(dashboard)/settings/webhooks/trigger-multiselect-combobox';
+import { useCurrentTeam } from '~/providers/team';
+
+const ZEditWebhookFormSchema = ZEditWebhookMutationSchema.omit({ id: true });
+
+type TEditWebhookFormSchema = z.infer;
+
+export type WebhookPageOptions = {
+ params: {
+ id: string;
+ };
+};
+
+export default function WebhookPage({ params }: WebhookPageOptions) {
+ const { toast } = useToast();
+ const router = useRouter();
+
+ const team = useCurrentTeam();
+
+ const { data: webhook, isLoading } = trpc.webhook.getWebhookById.useQuery(
+ {
+ id: params.id,
+ teamId: team.id,
+ },
+ { enabled: !!params.id && !!team.id },
+ );
+
+ const { mutateAsync: updateWebhook } = trpc.webhook.editWebhook.useMutation();
+
+ const form = useForm({
+ resolver: zodResolver(ZEditWebhookFormSchema),
+ values: {
+ webhookUrl: webhook?.webhookUrl ?? '',
+ eventTriggers: webhook?.eventTriggers ?? [],
+ secret: webhook?.secret ?? '',
+ enabled: webhook?.enabled ?? true,
+ },
+ });
+
+ const onSubmit = async (data: TEditWebhookFormSchema) => {
+ try {
+ await updateWebhook({
+ id: params.id,
+ teamId: team.id,
+ ...data,
+ });
+
+ toast({
+ title: 'Webhook updated',
+ description: 'The webhook has been updated successfully.',
+ duration: 5000,
+ });
+
+ router.refresh();
+ } catch (err) {
+ toast({
+ title: 'Failed to update webhook',
+ description: 'We encountered an error while updating the webhook. Please try again later.',
+ variant: 'destructive',
+ });
+ }
+ };
+
+ return (
+
+
+
+ {isLoading && (
+
+
+
+ )}
+
+
+
+
+
+
+ (
+
+ Triggers
+
+ {
+ onChange(values);
+ }}
+ />
+
+
+
+ The events that will trigger a webhook to be sent to your URL.
+
+
+
+
+ )}
+ />
+
+ (
+
+ Secret
+
+
+
+
+
+ A secret that will be sent to your URL so you can verify that the request has
+ been sent by Documenso.
+
+
+
+ )}
+ />
+
+
+
+ Update webhook
+
+
+
+
+
+
+ );
+}
diff --git a/apps/web/src/app/(teams)/t/[teamUrl]/settings/webhooks/page.tsx b/apps/web/src/app/(teams)/t/[teamUrl]/settings/webhooks/page.tsx
new file mode 100644
index 000000000..054664624
--- /dev/null
+++ b/apps/web/src/app/(teams)/t/[teamUrl]/settings/webhooks/page.tsx
@@ -0,0 +1,106 @@
+'use client';
+
+import Link from 'next/link';
+
+import { Loader } from 'lucide-react';
+import { DateTime } from 'luxon';
+
+import { toFriendlyWebhookEventName } from '@documenso/lib/universal/webhook/to-friendly-webhook-event-name';
+import { trpc } from '@documenso/trpc/react';
+import { cn } from '@documenso/ui/lib/utils';
+import { Badge } from '@documenso/ui/primitives/badge';
+import { Button } from '@documenso/ui/primitives/button';
+
+import { SettingsHeader } from '~/components/(dashboard)/settings/layout/header';
+import { CreateWebhookDialog } from '~/components/(dashboard)/settings/webhooks/create-webhook-dialog';
+import { DeleteWebhookDialog } from '~/components/(dashboard)/settings/webhooks/delete-webhook-dialog';
+import { LocaleDate } from '~/components/formatter/locale-date';
+import { useCurrentTeam } from '~/providers/team';
+
+export default function WebhookPage() {
+ const team = useCurrentTeam();
+
+ const { data: webhooks, isLoading } = trpc.webhook.getTeamWebhooks.useQuery({
+ teamId: team.id,
+ });
+
+ return (
+
+
+
+
+
+ {isLoading && (
+
+
+
+ )}
+
+ {webhooks && webhooks.length === 0 && (
+ // TODO: Perhaps add some illustrations here to make the page more engaging
+
+
+ You have no webhooks yet. Your webhooks will be shown here once you create them.
+
+
+ )}
+
+ {webhooks && webhooks.length > 0 && (
+
+ {webhooks?.map((webhook) => (
+
+
+
+
{webhook.id}
+
+
+
+ {webhook.webhookUrl}
+
+
+
+ {webhook.enabled ? 'Enabled' : 'Disabled'}
+
+
+
+
+ Listening to{' '}
+ {webhook.eventTriggers
+ .map((trigger) => toFriendlyWebhookEventName(trigger))
+ .join(', ')}
+
+
+
+ Created on{' '}
+
+
+
+
+
+
+ Edit
+
+
+ Delete
+
+
+
+
+ ))}
+
+ )}
+
+ );
+}
diff --git a/apps/web/src/app/(unauthenticated)/check-email/page.tsx b/apps/web/src/app/(unauthenticated)/check-email/page.tsx
index 94b410a8e..01f2b389d 100644
--- a/apps/web/src/app/(unauthenticated)/check-email/page.tsx
+++ b/apps/web/src/app/(unauthenticated)/check-email/page.tsx
@@ -9,17 +9,19 @@ export const metadata: Metadata = {
export default function ForgotPasswordPage() {
return (
-
-
Email sent!
+
+
+
Email sent!
-
- A password reset email has been sent, if you have an account you should see it in your inbox
- shortly.
-
+
+ A password reset email has been sent, if you have an account you should see it in your
+ inbox shortly.
+
-
- Return to sign in
-
+
+ Return to sign in
+
+
);
}
diff --git a/apps/web/src/app/(unauthenticated)/forgot-password/page.tsx b/apps/web/src/app/(unauthenticated)/forgot-password/page.tsx
index 36c023027..e93c8947c 100644
--- a/apps/web/src/app/(unauthenticated)/forgot-password/page.tsx
+++ b/apps/web/src/app/(unauthenticated)/forgot-password/page.tsx
@@ -9,22 +9,24 @@ export const metadata: Metadata = {
export default function ForgotPasswordPage() {
return (
-
-
Forgot your password?
+
+
+
Forgot your password?
-
- No worries, it happens! Enter your email and we'll email you a special link to reset your
- password.
-
+
+ No worries, it happens! Enter your email and we'll email you a special link to reset your
+ password.
+
-
+
-
- Remembered your password?{' '}
-
- Sign In
-
-
+
+ Remembered your password?{' '}
+
+ Sign In
+
+
+
);
}
diff --git a/apps/web/src/app/(unauthenticated)/layout.tsx b/apps/web/src/app/(unauthenticated)/layout.tsx
index 43c6d291f..03a73278f 100644
--- a/apps/web/src/app/(unauthenticated)/layout.tsx
+++ b/apps/web/src/app/(unauthenticated)/layout.tsx
@@ -10,9 +10,9 @@ type UnauthenticatedLayoutProps = {
export default function UnauthenticatedLayout({ children }: UnauthenticatedLayoutProps) {
return (
-
-
-
+
+
+
-
{children}
+
{children}
);
diff --git a/apps/web/src/app/(unauthenticated)/reset-password/[token]/page.tsx b/apps/web/src/app/(unauthenticated)/reset-password/[token]/page.tsx
index 04afd2c4d..1d469eb74 100644
--- a/apps/web/src/app/(unauthenticated)/reset-password/[token]/page.tsx
+++ b/apps/web/src/app/(unauthenticated)/reset-password/[token]/page.tsx
@@ -19,19 +19,21 @@ export default async function ResetPasswordPage({ params: { token } }: ResetPass
}
return (
-
-
Reset Password
+
+
+
Reset Password
-
Please choose your new password
+
Please choose your new password
-
+
-
- Don't have an account?{' '}
-
- Sign up
-
-
+
+ Don't have an account?{' '}
+
+ Sign up
+
+
+
);
}
diff --git a/apps/web/src/app/(unauthenticated)/reset-password/page.tsx b/apps/web/src/app/(unauthenticated)/reset-password/page.tsx
index 93cd41ebb..20d4bfe57 100644
--- a/apps/web/src/app/(unauthenticated)/reset-password/page.tsx
+++ b/apps/web/src/app/(unauthenticated)/reset-password/page.tsx
@@ -9,17 +9,19 @@ export const metadata: Metadata = {
export default function ResetPasswordPage() {
return (
-
-
Unable to reset password
+
+
+
Unable to reset password
-
- The token you have used to reset your password is either expired or it never existed. If you
- have still forgotten your password, please request a new reset link.
-
+
+ The token you have used to reset your password is either expired or it never existed. If
+ you have still forgotten your password, please request a new reset link.
+
-
- Return to sign in
-
+
+ Return to sign in
+
+
);
}
diff --git a/apps/web/src/app/(unauthenticated)/signin/page.tsx b/apps/web/src/app/(unauthenticated)/signin/page.tsx
index 50356a5bb..21136f2e6 100644
--- a/apps/web/src/app/(unauthenticated)/signin/page.tsx
+++ b/apps/web/src/app/(unauthenticated)/signin/page.tsx
@@ -30,36 +30,27 @@ export default function SignInPage({ searchParams }: SignInPageProps) {
}
return (
-
-
Sign in to your account
+
+
+
Sign in to your account
-
- Welcome back, we are lucky to have you.
-
-
-
-
- {NEXT_PUBLIC_DISABLE_SIGNUP !== 'true' && (
-
- Don't have an account?{' '}
-
- Sign up
-
+
+ Welcome back, we are lucky to have you.
- )}
-
-
- Forgot your password?
-
-
+
+
+
+
+ {NEXT_PUBLIC_DISABLE_SIGNUP !== 'true' && (
+
+ Don't have an account?{' '}
+
+ Sign up
+
+
+ )}
+
);
}
diff --git a/apps/web/src/app/(unauthenticated)/signup/page.tsx b/apps/web/src/app/(unauthenticated)/signup/page.tsx
index b9365e1d5..ad758a8e9 100644
--- a/apps/web/src/app/(unauthenticated)/signup/page.tsx
+++ b/apps/web/src/app/(unauthenticated)/signup/page.tsx
@@ -1,5 +1,4 @@
import type { Metadata } from 'next';
-import Link from 'next/link';
import { redirect } from 'next/navigation';
import { env } from 'next-runtime-env';
@@ -7,7 +6,7 @@ import { env } from 'next-runtime-env';
import { IS_GOOGLE_SSO_ENABLED } from '@documenso/lib/constants/auth';
import { decryptSecondaryData } from '@documenso/lib/server-only/crypto/decrypt';
-import { SignUpForm } from '~/components/forms/signup';
+import { SignUpFormV2 } from '~/components/forms/v2/signup';
export const metadata: Metadata = {
title: 'Sign Up',
@@ -34,26 +33,10 @@ export default function SignUpPage({ searchParams }: SignUpPageProps) {
}
return (
-
-
Create a new account
-
-
- Create your account and start using state-of-the-art document signing. Open and beautiful
- signing is within your grasp.
-
-
-
-
-
- Already have an account?{' '}
-
- Sign in instead
-
-
-
+
);
}
diff --git a/apps/web/src/app/(unauthenticated)/team/invite/[token]/page.tsx b/apps/web/src/app/(unauthenticated)/team/invite/[token]/page.tsx
index 634416fe3..289364ede 100644
--- a/apps/web/src/app/(unauthenticated)/team/invite/[token]/page.tsx
+++ b/apps/web/src/app/(unauthenticated)/team/invite/[token]/page.tsx
@@ -29,16 +29,18 @@ export default async function AcceptInvitationPage({
if (!teamMemberInvite) {
return (
-
-
Invalid token
+
+
+
Invalid token
-
- This token is invalid or has expired. Please contact your team for a new invitation.
-
+
+ This token is invalid or has expired. Please contact your team for a new invitation.
+
-
- Return
-
+
+ Return
+
+
);
}
diff --git a/apps/web/src/app/(unauthenticated)/team/verify/email/[token]/page.tsx b/apps/web/src/app/(unauthenticated)/team/verify/email/[token]/page.tsx
index 53ad4461b..8d67ca218 100644
--- a/apps/web/src/app/(unauthenticated)/team/verify/email/[token]/page.tsx
+++ b/apps/web/src/app/(unauthenticated)/team/verify/email/[token]/page.tsx
@@ -22,16 +22,18 @@ export default async function VerifyTeamEmailPage({ params: { token } }: VerifyT
if (!teamEmailVerification || isTokenExpired(teamEmailVerification.expiresAt)) {
return (
-
-
Invalid link
+
+
+
Invalid link
-
- This link is invalid or has expired. Please contact your team to resend a verification.
-
+
+ This link is invalid or has expired. Please contact your team to resend a verification.
+
-
- Return
-
+
+ Return
+
+
);
}
diff --git a/apps/web/src/app/(unauthenticated)/team/verify/transfer/[token]/page.tsx b/apps/web/src/app/(unauthenticated)/team/verify/transfer/[token]/page.tsx
index 819b7e970..719ec5b76 100644
--- a/apps/web/src/app/(unauthenticated)/team/verify/transfer/[token]/page.tsx
+++ b/apps/web/src/app/(unauthenticated)/team/verify/transfer/[token]/page.tsx
@@ -25,17 +25,19 @@ export default async function VerifyTeamTransferPage({
if (!teamTransferVerification || isTokenExpired(teamTransferVerification.expiresAt)) {
return (
-
-
Invalid link
+
+
+
Invalid link
-
- This link is invalid or has expired. Please contact your team to resend a transfer
- request.
-
+
+ This link is invalid or has expired. Please contact your team to resend a transfer
+ request.
+
-
- Return
-
+
+ Return
+
+
);
}
diff --git a/apps/web/src/app/(unauthenticated)/unverified-account/page.tsx b/apps/web/src/app/(unauthenticated)/unverified-account/page.tsx
index f4b8b90d7..c5b6fbcff 100644
--- a/apps/web/src/app/(unauthenticated)/unverified-account/page.tsx
+++ b/apps/web/src/app/(unauthenticated)/unverified-account/page.tsx
@@ -4,23 +4,25 @@ import { SendConfirmationEmailForm } from '~/components/forms/send-confirmation-
export default function UnverifiedAccount() {
return (
-
-
-
-
-
-
Confirm email
+
+
+
+
+
+
+
Confirm email
-
- To gain access to your account, please confirm your email address by clicking on the
- confirmation link from your inbox.
-
+
+ To gain access to your account, please confirm your email address by clicking on the
+ confirmation link from your inbox.
+
-
- If you don't find the confirmation link in your inbox, you can request a new one below.
-
+
+ If you don't find the confirmation link in your inbox, you can request a new one below.
+
-
+
+
);
diff --git a/apps/web/src/app/(unauthenticated)/verify-email/[token]/page.tsx b/apps/web/src/app/(unauthenticated)/verify-email/[token]/page.tsx
index f671fb101..9536f937c 100644
--- a/apps/web/src/app/(unauthenticated)/verify-email/[token]/page.tsx
+++ b/apps/web/src/app/(unauthenticated)/verify-email/[token]/page.tsx
@@ -14,15 +14,17 @@ export type PageProps = {
export default async function VerifyEmailPage({ params: { token } }: PageProps) {
if (!token) {
return (
-
-
-
-
+
+
+
+
+
-
No token provided
-
- It seems that there is no token provided. Please check your email and try again.
-
+
No token provided
+
+ It seems that there is no token provided. Please check your email and try again.
+
+
);
}
@@ -31,22 +33,24 @@ export default async function VerifyEmailPage({ params: { token } }: PageProps)
if (verified === null) {
return (
-
-
+
+
+
-
-
Something went wrong
+
+
Something went wrong
-
- We were unable to verify your email. If your email is not verified already, please try
- again.
-
+
+ We were unable to verify your email. If your email is not verified already, please try
+ again.
+
-
- Go back home
-
+
+ Go back home
+
+
);
@@ -54,17 +58,41 @@ export default async function VerifyEmailPage({ params: { token } }: PageProps)
if (!verified) {
return (
+
+
+
+
+
+
+
+
Your token has expired!
+
+
+ It seems that the provided token has expired. We've just sent you another token,
+ please check your email and try again.
+
+
+
+ Go back home
+
+
+
+
+ );
+ }
+
+ return (
+
-
+
-
Your token has expired!
+
Email Confirmed!
- It seems that the provided token has expired. We've just sent you another token, please
- check your email and try again.
+ Your email has been successfully confirmed! You can now use all features of Documenso.
@@ -72,26 +100,6 @@ export default async function VerifyEmailPage({ params: { token } }: PageProps)
- );
- }
-
- return (
-
-
-
-
-
-
-
Email Confirmed!
-
-
- Your email has been successfully confirmed! You can now use all features of Documenso.
-
-
-
- Go back home
-
-
);
}
diff --git a/apps/web/src/app/(unauthenticated)/verify-email/page.tsx b/apps/web/src/app/(unauthenticated)/verify-email/page.tsx
index 30d2baf16..f002ffda6 100644
--- a/apps/web/src/app/(unauthenticated)/verify-email/page.tsx
+++ b/apps/web/src/app/(unauthenticated)/verify-email/page.tsx
@@ -11,22 +11,26 @@ export const metadata: Metadata = {
export default function EmailVerificationWithoutTokenPage() {
return (
-
-
-
-
+
+
+
+
+
-
-
Uh oh! Looks like you're missing a token
+
+
+ Uh oh! Looks like you're missing a token
+
-
- It seems that there is no token provided, if you are trying to verify your email please
- follow the link in your email.
-
+
+ It seems that there is no token provided, if you are trying to verify your email please
+ follow the link in your email.
+
-
- Go back home
-
+
+ Go back home
+
+
);
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..24e14c958
--- /dev/null
+++ b/apps/web/src/app/api/v1/openapi/page.tsx
@@ -0,0 +1,11 @@
+'use client';
+
+import dynamic from 'next/dynamic';
+
+const Docs = dynamic(async () => import('@documenso/api/v1/api-documentation'), {
+ ssr: false,
+});
+
+export default function OpenApiDocsPage() {
+ return
;
+}
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/activity-back.tsx b/apps/web/src/components/(dashboard)/settings/layout/activity-back.tsx
new file mode 100644
index 000000000..d6ab1d080
--- /dev/null
+++ b/apps/web/src/components/(dashboard)/settings/layout/activity-back.tsx
@@ -0,0 +1,22 @@
+'use client';
+
+import { useRouter } from 'next/navigation';
+
+import { Button } from '@documenso/ui/primitives/button';
+
+export default function ActivityPageBackButton() {
+ const router = useRouter();
+ return (
+
+ {
+ void router.back();
+ }}
+ >
+ Back
+
+
+ );
+}
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..6109d1f3d 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, Webhook } from 'lucide-react';
import { useFeatureFlags } from '@documenso/lib/client-only/providers/feature-flag';
import { cn } from '@documenso/ui/lib/utils';
@@ -64,6 +64,32 @@ export const DesktopNav = ({ className, ...props }: DesktopNavProps) => {
+
+
+
+ API Tokens
+
+
+
+
+
+
+ Webhooks
+
+
+
{isBillingEnabled && (
{
+export const SettingsHeader = ({ children, title, subtitle, className }: SettingsHeaderProps) => {
return (
<>
-
+
{title}
diff --git a/apps/web/src/components/(dashboard)/settings/layout/mobile-nav.tsx b/apps/web/src/components/(dashboard)/settings/layout/mobile-nav.tsx
index 291c941f6..f8d57e1dd 100644
--- a/apps/web/src/components/(dashboard)/settings/layout/mobile-nav.tsx
+++ b/apps/web/src/components/(dashboard)/settings/layout/mobile-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, Webhook } from 'lucide-react';
import { useFeatureFlags } from '@documenso/lib/client-only/providers/feature-flag';
import { cn } from '@documenso/ui/lib/utils';
@@ -67,6 +67,32 @@ export const MobileNav = ({ className, ...props }: MobileNavProps) => {
+
+
+
+ API Tokens
+
+
+
+
+
+
+ Webhooks
+
+
+
{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/(dashboard)/settings/webhooks/create-webhook-dialog.tsx b/apps/web/src/components/(dashboard)/settings/webhooks/create-webhook-dialog.tsx
new file mode 100644
index 000000000..727054655
--- /dev/null
+++ b/apps/web/src/components/(dashboard)/settings/webhooks/create-webhook-dialog.tsx
@@ -0,0 +1,232 @@
+'use client';
+
+import { useState } from 'react';
+
+import { useRouter } from 'next/navigation';
+
+import { zodResolver } from '@hookform/resolvers/zod';
+import type * as DialogPrimitive from '@radix-ui/react-dialog';
+import { useForm } from 'react-hook-form';
+import type { z } from 'zod';
+
+import { trpc } from '@documenso/trpc/react';
+import { ZCreateWebhookMutationSchema } from '@documenso/trpc/server/webhook-router/schema';
+import { Button } from '@documenso/ui/primitives/button';
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from '@documenso/ui/primitives/dialog';
+import {
+ Form,
+ FormControl,
+ FormDescription,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from '@documenso/ui/primitives/form/form';
+import { Input } from '@documenso/ui/primitives/input';
+import { PasswordInput } from '@documenso/ui/primitives/password-input';
+import { Switch } from '@documenso/ui/primitives/switch';
+import { useToast } from '@documenso/ui/primitives/use-toast';
+
+import { useOptionalCurrentTeam } from '~/providers/team';
+
+import { TriggerMultiSelectCombobox } from './trigger-multiselect-combobox';
+
+const ZCreateWebhookFormSchema = ZCreateWebhookMutationSchema.omit({ teamId: true });
+
+type TCreateWebhookFormSchema = z.infer;
+
+export type CreateWebhookDialogProps = {
+ trigger?: React.ReactNode;
+} & Omit;
+
+export const CreateWebhookDialog = ({ trigger, ...props }: CreateWebhookDialogProps) => {
+ const router = useRouter();
+ const { toast } = useToast();
+
+ const team = useOptionalCurrentTeam();
+
+ const [open, setOpen] = useState(false);
+
+ const form = useForm({
+ resolver: zodResolver(ZCreateWebhookFormSchema),
+ values: {
+ webhookUrl: '',
+ eventTriggers: [],
+ secret: '',
+ enabled: true,
+ },
+ });
+
+ const { mutateAsync: createWebhook } = trpc.webhook.createWebhook.useMutation();
+
+ const onSubmit = async ({
+ enabled,
+ eventTriggers,
+ secret,
+ webhookUrl,
+ }: TCreateWebhookFormSchema) => {
+ try {
+ await createWebhook({
+ enabled,
+ eventTriggers,
+ secret,
+ webhookUrl,
+ teamId: team?.id,
+ });
+
+ setOpen(false);
+
+ toast({
+ title: 'Webhook created',
+ description: 'The webhook was successfully created.',
+ });
+
+ form.reset();
+
+ router.refresh();
+ } catch (err) {
+ toast({
+ title: 'Error',
+ description: 'An error occurred while creating the webhook. Please try again.',
+ variant: 'destructive',
+ });
+ }
+ };
+
+ return (
+ !form.formState.isSubmitting && setOpen(value)}
+ {...props}
+ >
+ e.stopPropagation()} asChild>
+ {trigger ?? Create Webhook }
+
+
+
+
+ Create webhook
+ On this page, you can create a new webhook.
+
+
+
+
+
+
+
+ (
+
+ Triggers
+
+ {
+ onChange(values);
+ }}
+ />
+
+
+
+ The events that will trigger a webhook to be sent to your URL.
+
+
+
+
+ )}
+ />
+
+ (
+
+ Secret
+
+
+
+
+
+ A secret that will be sent to your URL so you can verify that the request has
+ been sent by Documenso.
+
+
+
+ )}
+ />
+
+
+
+ setOpen(false)}>
+ Cancel
+
+
+ Create
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/apps/web/src/components/(dashboard)/settings/webhooks/delete-webhook-dialog.tsx b/apps/web/src/components/(dashboard)/settings/webhooks/delete-webhook-dialog.tsx
new file mode 100644
index 000000000..e65ae78b8
--- /dev/null
+++ b/apps/web/src/components/(dashboard)/settings/webhooks/delete-webhook-dialog.tsx
@@ -0,0 +1,172 @@
+'use effect';
+
+import { useEffect, 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 type { Webhook } from '@documenso/prisma/client';
+import { trpc } from '@documenso/trpc/react';
+import { Button } from '@documenso/ui/primitives/button';
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from '@documenso/ui/primitives/dialog';
+import {
+ Form,
+ FormControl,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from '@documenso/ui/primitives/form/form';
+import { Input } from '@documenso/ui/primitives/input';
+import { useToast } from '@documenso/ui/primitives/use-toast';
+
+import { useOptionalCurrentTeam } from '~/providers/team';
+
+export type DeleteWebhookDialogProps = {
+ webhook: Pick;
+ onDelete?: () => void;
+ children: React.ReactNode;
+};
+
+export const DeleteWebhookDialog = ({ webhook, children }: DeleteWebhookDialogProps) => {
+ const router = useRouter();
+ const { toast } = useToast();
+
+ const team = useOptionalCurrentTeam();
+
+ const [open, setOpen] = useState(false);
+
+ const deleteMessage = `delete ${webhook.webhookUrl}`;
+
+ const ZDeleteWebhookFormSchema = z.object({
+ webhookUrl: z.literal(deleteMessage, {
+ errorMap: () => ({ message: `You must enter '${deleteMessage}' to proceed` }),
+ }),
+ });
+
+ type TDeleteWebhookFormSchema = z.infer;
+
+ const { mutateAsync: deleteWebhook } = trpc.webhook.deleteWebhook.useMutation();
+
+ const form = useForm({
+ resolver: zodResolver(ZDeleteWebhookFormSchema),
+ values: {
+ webhookUrl: '',
+ },
+ });
+
+ const onSubmit = async () => {
+ try {
+ await deleteWebhook({ id: webhook.id, teamId: team?.id });
+
+ toast({
+ title: 'Webhook deleted',
+ duration: 5000,
+ description: 'The webhook has been successfully deleted.',
+ });
+
+ setOpen(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 it. Please try again later.',
+ });
+ }
+ };
+
+ useEffect(() => {
+ if (!open) {
+ form.reset();
+ }
+ }, [open, form]);
+
+ return (
+ !form.formState.isSubmitting && setOpen(value)}>
+
+ {children ?? (
+
+ Delete
+
+ )}
+
+
+
+
+ Delete Webhook
+
+
+ Please note that this action is irreversible. Once confirmed, your webhook will be
+ permanently deleted.
+
+
+
+
+
+
+ (
+
+
+ Confirm by typing:{' '}
+
+ {deleteMessage}
+
+
+
+
+
+
+
+ )}
+ />
+
+
+
+ setOpen(false)}
+ >
+ Cancel
+
+
+
+ I'm sure! Delete it
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/apps/web/src/components/(dashboard)/settings/webhooks/trigger-multiselect-combobox.tsx b/apps/web/src/components/(dashboard)/settings/webhooks/trigger-multiselect-combobox.tsx
new file mode 100644
index 000000000..5636f1931
--- /dev/null
+++ b/apps/web/src/components/(dashboard)/settings/webhooks/trigger-multiselect-combobox.tsx
@@ -0,0 +1,93 @@
+import { useEffect, useState } from 'react';
+
+import { WebhookTriggerEvents } from '@prisma/client/';
+import { Check, ChevronsUpDown } from 'lucide-react';
+
+import { toFriendlyWebhookEventName } from '@documenso/lib/universal/webhook/to-friendly-webhook-event-name';
+import { cn } from '@documenso/ui/lib/utils';
+import { Button } from '@documenso/ui/primitives/button';
+import {
+ Command,
+ CommandEmpty,
+ CommandGroup,
+ CommandInput,
+ CommandItem,
+} from '@documenso/ui/primitives/command';
+import { Popover, PopoverContent, PopoverTrigger } from '@documenso/ui/primitives/popover';
+
+import { truncateTitle } from '~/helpers/truncate-title';
+
+type TriggerMultiSelectComboboxProps = {
+ listValues: string[];
+ onChange: (_values: string[]) => void;
+};
+
+export const TriggerMultiSelectCombobox = ({
+ listValues,
+ onChange,
+}: TriggerMultiSelectComboboxProps) => {
+ const [isOpen, setIsOpen] = useState(false);
+ const [selectedValues, setSelectedValues] = useState([]);
+
+ const triggerEvents = Object.values(WebhookTriggerEvents);
+
+ useEffect(() => {
+ setSelectedValues(listValues);
+ }, [listValues]);
+
+ const allEvents = [...new Set([...triggerEvents, ...selectedValues])];
+
+ const handleSelect = (currentValue: string) => {
+ let newSelectedValues;
+
+ if (selectedValues.includes(currentValue)) {
+ newSelectedValues = selectedValues.filter((value) => value !== currentValue);
+ } else {
+ newSelectedValues = [...selectedValues, currentValue];
+ }
+
+ setSelectedValues(newSelectedValues);
+ onChange(newSelectedValues);
+ setIsOpen(false);
+ };
+
+ return (
+
+
+
+ {selectedValues.length > 0 ? selectedValues.length + ' selected...' : 'Select values...'}
+
+
+
+
+
+ toFriendlyWebhookEventName(v)).join(', '),
+ 15,
+ )}
+ />
+ No value found.
+
+ {allEvents.map((value: string, i: number) => (
+ handleSelect(value)}>
+
+ {toFriendlyWebhookEventName(value)}
+
+ ))}
+
+
+
+
+ );
+};
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..6964b2cee 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, Webhook } from 'lucide-react';
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
import { cn } from '@documenso/ui/lib/utils';
@@ -21,6 +21,8 @@ export const DesktopNav = ({ className, ...props }: DesktopNavProps) => {
const settingsPath = `/t/${teamUrl}/settings`;
const membersPath = `/t/${teamUrl}/settings/members`;
+ const tokensPath = `/t/${teamUrl}/settings/tokens`;
+ const webhooksPath = `/t/${teamUrl}/settings/webhooks`;
const billingPath = `/t/${teamUrl}/settings/billing`;
return (
@@ -48,6 +50,29 @@ export const DesktopNav = ({ className, ...props }: DesktopNavProps) => {
+
+
+
+ API Tokens
+
+
+
+
+
+
+ Webhooks
+
+
+
{IS_BILLING_ENABLED() && (
{
const settingsPath = `/t/${teamUrl}/settings`;
const membersPath = `/t/${teamUrl}/settings/members`;
+ const tokensPath = `/t/${teamUrl}/settings/tokens`;
+ const webhooksPath = `/t/${teamUrl}/settings/webhooks`;
const billingPath = `/t/${teamUrl}/settings/billing`;
return (
@@ -56,6 +58,29 @@ export const MobileNav = ({ className, ...props }: MobileNavProps) => {
+
+
+
+ API Tokens
+
+
+
+
+
+
+ Webhooks
+
+
+
{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/public-profile-claim-dialog.tsx b/apps/web/src/components/forms/public-profile-claim-dialog.tsx
new file mode 100644
index 000000000..dbd52fd27
--- /dev/null
+++ b/apps/web/src/components/forms/public-profile-claim-dialog.tsx
@@ -0,0 +1,197 @@
+'use client';
+
+import React, { useState } from 'react';
+
+import Image from 'next/image';
+
+import { zodResolver } from '@hookform/resolvers/zod';
+import { useForm } from 'react-hook-form';
+import { z } from 'zod';
+
+import profileClaimTeaserImage from '@documenso/assets/images/profile-claim-teaser.png';
+import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
+import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
+import type { User } from '@documenso/prisma/client';
+import { trpc } from '@documenso/trpc/react';
+import { cn } from '@documenso/ui/lib/utils';
+import { Button } from '@documenso/ui/primitives/button';
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogHeader,
+ DialogTitle,
+} from '@documenso/ui/primitives/dialog';
+import {
+ Form,
+ FormControl,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from '@documenso/ui/primitives/form/form';
+import { Input } from '@documenso/ui/primitives/input';
+import { useToast } from '@documenso/ui/primitives/use-toast';
+
+import { UserProfileSkeleton } from '../ui/user-profile-skeleton';
+
+export const ZClaimPublicProfileFormSchema = z.object({
+ url: z
+ .string()
+ .trim()
+ .toLowerCase()
+ .min(1, { message: 'Please enter a valid username.' })
+ .regex(/^[a-z0-9-]+$/, {
+ message: 'Username can only container alphanumeric characters and dashes.',
+ }),
+});
+
+export type TClaimPublicProfileFormSchema = z.infer;
+
+export type ClaimPublicProfileDialogFormProps = {
+ open: boolean;
+ onOpenChange?: (open: boolean) => void;
+ onClaimed?: () => void;
+ user: User;
+};
+
+export const ClaimPublicProfileDialogForm = ({
+ open,
+ onOpenChange,
+ onClaimed,
+ user,
+}: ClaimPublicProfileDialogFormProps) => {
+ const { toast } = useToast();
+
+ const [claimed, setClaimed] = useState(false);
+
+ const baseUrl = new URL(NEXT_PUBLIC_WEBAPP_URL() ?? 'http://localhost:3000');
+
+ const form = useForm({
+ values: {
+ url: user.url || '',
+ },
+ resolver: zodResolver(ZClaimPublicProfileFormSchema),
+ });
+
+ const { mutateAsync: updatePublicProfile } = trpc.profile.updatePublicProfile.useMutation();
+
+ const isSubmitting = form.formState.isSubmitting;
+
+ const onFormSubmit = async ({ url }: TClaimPublicProfileFormSchema) => {
+ try {
+ await updatePublicProfile({
+ url,
+ });
+
+ setClaimed(true);
+ onClaimed?.();
+ } catch (err) {
+ const error = AppError.parseError(err);
+
+ if (error.code === AppErrorCode.PROFILE_URL_TAKEN) {
+ form.setError('url', {
+ type: 'manual',
+ message: 'This username is already taken',
+ });
+ } else if (error.code === AppErrorCode.PREMIUM_PROFILE_URL) {
+ form.setError('url', {
+ type: 'manual',
+ message: error.message,
+ });
+ } else if (error.code !== AppErrorCode.UNKNOWN_ERROR) {
+ toast({
+ title: 'An error occurred',
+ description: error.userMessage ?? error.message,
+ variant: 'destructive',
+ });
+ } else {
+ toast({
+ title: 'An unknown error occurred',
+ variant: 'destructive',
+ description:
+ 'We encountered an unknown error while attempting to save your details. Please try again later.',
+ });
+ }
+ }
+ };
+
+ return (
+
+
+ {!claimed && (
+ <>
+
+
+ Introducing public profiles!
+
+
+
+ Reserve your Documenso public profile username
+
+
+
+
+
+
+
+
+ (
+
+ Public profile username
+
+
+
+
+
+
+
+
+ {baseUrl.host}/u/{field.value || ''}
+
+
+ )}
+ />
+
+
+
+
+ Claim your username
+
+
+
+
+ >
+ )}
+
+ {claimed && (
+ <>
+
+ All set!
+
+
+ We will let you know as soon as this features is launched
+
+
+
+
+
+
+ onOpenChange?.(false)}>
+ Can't wait!
+
+
+ >
+ )}
+
+
+ );
+};
diff --git a/apps/web/src/components/forms/signin.tsx b/apps/web/src/components/forms/signin.tsx
index ec690a568..1d6d32f1f 100644
--- a/apps/web/src/components/forms/signin.tsx
+++ b/apps/web/src/components/forms/signin.tsx
@@ -2,6 +2,7 @@
import { useState } from 'react';
+import Link from 'next/link';
import { useRouter } from 'next/navigation';
import { zodResolver } from '@hookform/resolvers/zod';
@@ -195,9 +196,11 @@ export const SignInForm = ({ className, initialEmail, isGoogleSSOEnabled }: Sign
render={({ field }) => (
Email
+
+
)}
@@ -209,9 +212,19 @@ export const SignInForm = ({ className, initialEmail, isGoogleSSOEnabled }: Sign
render={({ field }) => (
Password
+
+
+
+
+ Forgot your password?
+
+
)}
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/components/forms/v2/signup.tsx b/apps/web/src/components/forms/v2/signup.tsx
new file mode 100644
index 000000000..713bde4b4
--- /dev/null
+++ b/apps/web/src/components/forms/v2/signup.tsx
@@ -0,0 +1,463 @@
+'use client';
+
+import { useState } from 'react';
+
+import Image from 'next/image';
+import Link from 'next/link';
+import { useRouter, useSearchParams } from 'next/navigation';
+
+import { zodResolver } from '@hookform/resolvers/zod';
+import { AnimatePresence, motion } from 'framer-motion';
+import { signIn } from 'next-auth/react';
+import { useForm } from 'react-hook-form';
+import { FcGoogle } from 'react-icons/fc';
+import { z } from 'zod';
+
+import communityCardsImage from '@documenso/assets/images/community-cards.png';
+import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
+import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
+import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
+import { TRPCClientError } from '@documenso/trpc/client';
+import { trpc } from '@documenso/trpc/react';
+import { ZPasswordSchema } from '@documenso/trpc/server/auth-router/schema';
+import { cn } from '@documenso/ui/lib/utils';
+import { Button } from '@documenso/ui/primitives/button';
+import {
+ Form,
+ FormControl,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from '@documenso/ui/primitives/form/form';
+import { Input } from '@documenso/ui/primitives/input';
+import { PasswordInput } from '@documenso/ui/primitives/password-input';
+import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
+import { useToast } from '@documenso/ui/primitives/use-toast';
+
+import { UserProfileSkeleton } from '~/components/ui/user-profile-skeleton';
+import { UserProfileTimur } from '~/components/ui/user-profile-timur';
+
+const SIGN_UP_REDIRECT_PATH = '/documents';
+
+type SignUpStep = 'BASIC_DETAILS' | 'CLAIM_USERNAME';
+
+export const ZSignUpFormV2Schema = z
+ .object({
+ name: z.string().trim().min(1, { message: 'Please enter a valid name.' }),
+ email: z.string().email().min(1),
+ password: ZPasswordSchema,
+ signature: z.string().min(1, { message: 'We need your signature to sign documents' }),
+ url: z
+ .string()
+ .trim()
+ .toLowerCase()
+ .min(1, { message: 'We need a username to create your profile' })
+ .regex(/^[a-z0-9-]+$/, {
+ message: 'Username can only container alphanumeric characters and dashes.',
+ }),
+ })
+ .refine(
+ (data) => {
+ const { name, email, password } = data;
+ return !password.includes(name) && !password.includes(email.split('@')[0]);
+ },
+ {
+ message: 'Password should not be common or based on personal information',
+ },
+ );
+
+export type TSignUpFormV2Schema = z.infer;
+
+export type SignUpFormV2Props = {
+ className?: string;
+ initialEmail?: string;
+ isGoogleSSOEnabled?: boolean;
+};
+
+export const SignUpFormV2 = ({
+ className,
+ initialEmail,
+ isGoogleSSOEnabled,
+}: SignUpFormV2Props) => {
+ const { toast } = useToast();
+ const analytics = useAnalytics();
+ const router = useRouter();
+ const searchParams = useSearchParams();
+
+ const [step, setStep] = useState('BASIC_DETAILS');
+
+ const utmSrc = searchParams?.get('utm_source') ?? null;
+
+ const baseUrl = new URL(NEXT_PUBLIC_WEBAPP_URL() ?? 'http://localhost:3000');
+
+ const form = useForm({
+ values: {
+ name: '',
+ email: initialEmail ?? '',
+ password: '',
+ signature: '',
+ url: '',
+ },
+ mode: 'onBlur',
+ resolver: zodResolver(ZSignUpFormV2Schema),
+ });
+
+ const isSubmitting = form.formState.isSubmitting;
+
+ const name = form.watch('name');
+ const url = form.watch('url');
+
+ // To continue we need to make sure name, email, password and signature are valid
+ const canContinue =
+ form.formState.dirtyFields.name &&
+ form.formState.errors.name === undefined &&
+ form.formState.dirtyFields.email &&
+ form.formState.errors.email === undefined &&
+ form.formState.dirtyFields.password &&
+ form.formState.errors.password === undefined &&
+ form.formState.dirtyFields.signature &&
+ form.formState.errors.signature === undefined;
+
+ console.log({ formSTate: form.formState });
+
+ const { mutateAsync: signup } = trpc.auth.signup.useMutation();
+
+ const onFormSubmit = async ({ name, email, password, signature, url }: TSignUpFormV2Schema) => {
+ try {
+ await signup({ name, email, password, signature, url });
+
+ router.push(`/unverified-account`);
+
+ toast({
+ title: 'Registration Successful',
+ description:
+ 'You have successfully registered. Please verify your account by clicking on the link you received in the email.',
+ duration: 5000,
+ });
+
+ analytics.capture('App: User Sign Up', {
+ email,
+ timestamp: new Date().toISOString(),
+ custom_campaign_params: { src: utmSrc },
+ });
+ } catch (err) {
+ const error = AppError.parseError(err);
+
+ if (error.code === AppErrorCode.PROFILE_URL_TAKEN) {
+ form.setError('url', {
+ type: 'manual',
+ message: 'This username has already been taken',
+ });
+ } else if (error.code === AppErrorCode.PREMIUM_PROFILE_URL) {
+ form.setError('url', {
+ type: 'manual',
+ message: error.message,
+ });
+ } else 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',
+ description:
+ 'We encountered an unknown error while attempting to sign you up. Please try again later.',
+ variant: 'destructive',
+ });
+ }
+ }
+ };
+
+ const onSignUpWithGoogleClick = async () => {
+ try {
+ await signIn('google', { callbackUrl: SIGN_UP_REDIRECT_PATH });
+ } catch (err) {
+ toast({
+ title: 'An unknown error occurred',
+ description:
+ 'We encountered an unknown error while attempting to sign you Up. Please try again later.',
+ variant: 'destructive',
+ });
+ }
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ User profiles are coming soon!
+
+
+
+ {step === 'BASIC_DETAILS' ? (
+
+
+
+ ) : (
+
+
+
+ )}
+
+
+
+
+
+
+
+ {step === 'BASIC_DETAILS' && (
+
+
Create a new account
+
+
+ Create your account and start using state-of-the-art document signing. Open and
+ beautiful signing is within your grasp.
+
+
+ )}
+
+ {step === 'CLAIM_USERNAME' && (
+
+
Claim your username now
+
+
+ You will get notified & be able to set up your documenso public profile when we launch
+ the feature.
+
+
+ )}
+
+
+
+
+
+ {step === 'BASIC_DETAILS' && (
+
+ (
+
+ Full Name
+
+
+
+
+
+ )}
+ />
+
+ (
+
+ Email Address
+
+
+
+
+
+ )}
+ />
+
+ (
+
+ Password
+
+
+
+
+
+
+
+ )}
+ />
+
+ (
+
+ Sign Here
+
+ onChange(v ?? '')}
+ />
+
+
+
+
+ )}
+ />
+
+ {isGoogleSSOEnabled && (
+ <>
+
+
+
+
+ Sign Up with Google
+
+ >
+ )}
+
+
+ Already have an account?{' '}
+
+ Sign in instead
+
+
+
+ )}
+
+ {step === 'CLAIM_USERNAME' && (
+
+ (
+
+ Public profile username
+
+
+
+
+
+
+
+
+ {baseUrl.host}/u/{field.value || ''}
+
+
+ )}
+ />
+
+ )}
+
+
+ {step === 'BASIC_DETAILS' && (
+
+ Basic details 1/2
+
+ )}
+
+ {step === 'CLAIM_USERNAME' && (
+
+ Claim username 2/2
+
+ )}
+
+
+
+
+
+
+
+ {/* Go back button, disabled if step is basic details */}
+ setStep('BASIC_DETAILS')}
+ >
+ Back
+
+
+ {/* Continue button */}
+ {step === 'BASIC_DETAILS' && (
+ setStep('CLAIM_USERNAME')}
+ >
+ Next
+
+ )}
+
+ {/* Sign up button */}
+ {step === 'CLAIM_USERNAME' && (
+
+ Complete
+
+ )}
+
+
+
+
+
+ );
+};
diff --git a/apps/web/src/components/forms/webhook.tsx b/apps/web/src/components/forms/webhook.tsx
new file mode 100644
index 000000000..e69de29bb
diff --git a/apps/web/src/components/ui/user-profile-skeleton.tsx b/apps/web/src/components/ui/user-profile-skeleton.tsx
new file mode 100644
index 000000000..1c8f35b64
--- /dev/null
+++ b/apps/web/src/components/ui/user-profile-skeleton.tsx
@@ -0,0 +1,84 @@
+'use client';
+
+import { File, User2 } from 'lucide-react';
+
+import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
+import type { User } from '@documenso/prisma/client';
+import { VerifiedIcon } from '@documenso/ui/icons/verified';
+import { cn } from '@documenso/ui/lib/utils';
+import { Button } from '@documenso/ui/primitives/button';
+
+export type UserProfileSkeletonProps = {
+ className?: string;
+ user: Pick;
+ rows?: number;
+};
+
+export const UserProfileSkeleton = ({ className, user, rows = 2 }: UserProfileSkeletonProps) => {
+ const baseUrl = new URL(NEXT_PUBLIC_WEBAPP_URL() ?? 'http://localhost:3000');
+
+ return (
+
+
+ {baseUrl.host}/u/
+ {user.url}
+
+
+
+
+
+
+
{user.name}
+
+
+
+
+
+
+
+
+
+
+
+ Documents
+
+
+ {Array(rows)
+ .fill(0)
+ .map((_, index) => (
+
+ ))}
+
+
+
+ );
+};
diff --git a/apps/web/src/components/ui/user-profile-timur.tsx b/apps/web/src/components/ui/user-profile-timur.tsx
new file mode 100644
index 000000000..e99a314b4
--- /dev/null
+++ b/apps/web/src/components/ui/user-profile-timur.tsx
@@ -0,0 +1,87 @@
+'use client';
+
+import Image from 'next/image';
+
+import { File } from 'lucide-react';
+
+import timurImage from '@documenso/assets/images/timur.png';
+import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
+import { VerifiedIcon } from '@documenso/ui/icons/verified';
+import { cn } from '@documenso/ui/lib/utils';
+import { Button } from '@documenso/ui/primitives/button';
+
+export type UserProfileTimurProps = {
+ className?: string;
+ rows?: number;
+};
+
+export const UserProfileTimur = ({ className, rows = 2 }: UserProfileTimurProps) => {
+ const baseUrl = new URL(NEXT_PUBLIC_WEBAPP_URL() ?? 'http://localhost:3000');
+
+ return (
+
+
+ {baseUrl.host}/u/timur
+
+
+
+
+
+
+
+
+
Timur Ercan
+
+
+
+
+
Hey Iโm Timur
+
+
+ Pick any of the following agreements below and start signing to get started
+
+
+
+
+
+
+ Documents
+
+
+ {Array(rows)
+ .fill(0)
+ .map((_, index) => (
+
+ ))}
+
+
+
+ );
+};
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/me/index.ts b/apps/web/src/pages/api/v1/me/index.ts
new file mode 100644
index 000000000..a877c11d0
--- /dev/null
+++ b/apps/web/src/pages/api/v1/me/index.ts
@@ -0,0 +1,3 @@
+import { testCredentialsHandler } from '@documenso/lib/server-only/public-api/test-credentials';
+
+export default testCredentialsHandler;
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/pages/api/v1/zapier/list-documents/index.ts b/apps/web/src/pages/api/v1/zapier/list-documents/index.ts
new file mode 100644
index 000000000..ba2a35b43
--- /dev/null
+++ b/apps/web/src/pages/api/v1/zapier/list-documents/index.ts
@@ -0,0 +1,3 @@
+import { listDocumentsHandler } from '@documenso/lib/server-only/webhooks/zapier/list-documents';
+
+export default listDocumentsHandler;
diff --git a/apps/web/src/pages/api/v1/zapier/subscribe/index.ts b/apps/web/src/pages/api/v1/zapier/subscribe/index.ts
new file mode 100644
index 000000000..6bcfe9e74
--- /dev/null
+++ b/apps/web/src/pages/api/v1/zapier/subscribe/index.ts
@@ -0,0 +1,3 @@
+import { subscribeHandler } from '@documenso/lib/server-only/webhooks/zapier/subscribe';
+
+export default subscribeHandler;
diff --git a/apps/web/src/pages/api/v1/zapier/unsubscribe/index.ts b/apps/web/src/pages/api/v1/zapier/unsubscribe/index.ts
new file mode 100644
index 000000000..f93dd6af7
--- /dev/null
+++ b/apps/web/src/pages/api/v1/zapier/unsubscribe/index.ts
@@ -0,0 +1,3 @@
+import { unsubscribeHandler } from '@documenso/lib/server-only/webhooks/zapier/unsubscribe';
+
+export default unsubscribeHandler;
diff --git a/apps/web/src/pages/api/webhook/trigger.ts b/apps/web/src/pages/api/webhook/trigger.ts
new file mode 100644
index 000000000..88abbb3b6
--- /dev/null
+++ b/apps/web/src/pages/api/webhook/trigger.ts
@@ -0,0 +1,12 @@
+import { handlerTriggerWebhooks } from '@documenso/lib/server-only/webhooks/trigger/handler';
+
+export const config = {
+ maxDuration: 300,
+ api: {
+ bodyParser: {
+ sizeLimit: '50mb',
+ },
+ },
+};
+
+export default handlerTriggerWebhooks;
diff --git a/apps/web/src/providers/posthog.tsx b/apps/web/src/providers/posthog.tsx
index 4bed6960e..dd90c813b 100644
--- a/apps/web/src/providers/posthog.tsx
+++ b/apps/web/src/providers/posthog.tsx
@@ -32,6 +32,7 @@ export function PostHogPageview() {
// Do nothing.
});
},
+ custom_campaign_params: ['src'],
});
}
diff --git a/apps/web/src/providers/team.tsx b/apps/web/src/providers/team.tsx
new file mode 100644
index 000000000..ef77578b3
--- /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 = () => {
+ const context = useContext(TeamContext);
+
+ if (!context) {
+ throw new Error('useCurrentTeam must be used within a TeamProvider');
+ }
+
+ return context;
+};
+
+export const useOptionalCurrentTeam = () => {
+ 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..fe394e603
--- /dev/null
+++ b/packages/api/v1/api-documentation.tsx
@@ -0,0 +1,12 @@
+'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 ;
+};
+
+export default OpenApiDocsPage;
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/fixtures/authentication.ts b/packages/app-tests/e2e/fixtures/authentication.ts
index f1926fb2a..d59fccd1c 100644
--- a/packages/app-tests/e2e/fixtures/authentication.ts
+++ b/packages/app-tests/e2e/fixtures/authentication.ts
@@ -34,6 +34,7 @@ export const manualLogin = async ({
};
export const manualSignout = async ({ page }: ManualLoginOptions) => {
+ await page.waitForTimeout(1000);
await page.getByTestId('menu-switcher').click();
await page.getByRole('menuitem', { name: 'Sign Out' }).click();
await page.waitForURL(`${WEBAPP_BASE_URL}/signin`);
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/app-tests/e2e/test-auth-flow.spec.ts b/packages/app-tests/e2e/test-auth-flow.spec.ts
index 57c25bb26..9c9500053 100644
--- a/packages/app-tests/e2e/test-auth-flow.spec.ts
+++ b/packages/app-tests/e2e/test-auth-flow.spec.ts
@@ -29,7 +29,10 @@ test('user can sign up with email and password', async ({ page }: { page: Page }
await page.mouse.up();
}
- await page.getByRole('button', { name: 'Sign Up', exact: true }).click();
+ await page.getByRole('button', { name: 'Next', exact: true }).click();
+ await page.getByLabel('Public profile username').fill('username-123');
+
+ await page.getByRole('button', { name: 'Complete', exact: true }).click();
await page.waitForURL('/unverified-account');
diff --git a/packages/assets/images/background-lw-2.png b/packages/assets/images/background-lw-2.png
new file mode 100644
index 000000000..f65793d6a
Binary files /dev/null and b/packages/assets/images/background-lw-2.png differ
diff --git a/packages/assets/images/community-cards.png b/packages/assets/images/community-cards.png
new file mode 100644
index 000000000..fe9b7edb4
Binary files /dev/null and b/packages/assets/images/community-cards.png differ
diff --git a/packages/assets/images/profile-claim-teaser.png b/packages/assets/images/profile-claim-teaser.png
new file mode 100644
index 000000000..b388de0d2
Binary files /dev/null and b/packages/assets/images/profile-claim-teaser.png differ
diff --git a/packages/assets/images/timur.png b/packages/assets/images/timur.png
new file mode 100644
index 000000000..2adf31596
Binary files /dev/null and b/packages/assets/images/timur.png differ
diff --git a/packages/lib/constants/feature-flags.ts b/packages/lib/constants/feature-flags.ts
index ac476bc70..ebd09c73a 100644
--- a/packages/lib/constants/feature-flags.ts
+++ b/packages/lib/constants/feature-flags.ts
@@ -25,6 +25,7 @@ export const LOCAL_FEATURE_FLAGS: Record = {
app_teams: true,
app_document_page_view_history_sheet: false,
marketing_header_single_player_mode: false,
+ marketing_profiles_announcement_bar: true,
} as const;
/**
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/errors/app-error.ts b/packages/lib/errors/app-error.ts
index 3337bab4c..f43f9c3ba 100644
--- a/packages/lib/errors/app-error.ts
+++ b/packages/lib/errors/app-error.ts
@@ -18,6 +18,8 @@ export enum AppErrorCode {
'RETRY_EXCEPTION' = 'RetryException',
'SCHEMA_FAILED' = 'SchemaFailed',
'TOO_MANY_REQUESTS' = 'TooManyRequests',
+ 'PROFILE_URL_TAKEN' = 'ProfileUrlTaken',
+ 'PREMIUM_PROFILE_URL' = 'PremiumProfileUrl',
}
const genericErrorCodeToTrpcErrorCodeMap: Record = {
@@ -32,6 +34,8 @@ const genericErrorCodeToTrpcErrorCodeMap: Record = {
[AppErrorCode.RETRY_EXCEPTION]: 'INTERNAL_SERVER_ERROR',
[AppErrorCode.SCHEMA_FAILED]: 'INTERNAL_SERVER_ERROR',
[AppErrorCode.TOO_MANY_REQUESTS]: 'TOO_MANY_REQUESTS',
+ [AppErrorCode.PROFILE_URL_TAKEN]: 'BAD_REQUEST',
+ [AppErrorCode.PREMIUM_PROFILE_URL]: 'BAD_REQUEST',
};
export const ZAppErrorJsonSchema = z.object({
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/crypto/sign.ts b/packages/lib/server-only/crypto/sign.ts
new file mode 100644
index 000000000..18c111c7b
--- /dev/null
+++ b/packages/lib/server-only/crypto/sign.ts
@@ -0,0 +1,12 @@
+import { hashString } from '../auth/hash';
+import { encryptSecondaryData } from './encrypt';
+
+export const sign = (data: unknown) => {
+ const stringified = JSON.stringify(data);
+
+ const hashed = hashString(stringified);
+
+ const signature = encryptSecondaryData({ data: hashed });
+
+ return signature;
+};
diff --git a/packages/lib/server-only/crypto/verify.ts b/packages/lib/server-only/crypto/verify.ts
new file mode 100644
index 000000000..7658e8b5e
--- /dev/null
+++ b/packages/lib/server-only/crypto/verify.ts
@@ -0,0 +1,12 @@
+import { hashString } from '../auth/hash';
+import { decryptSecondaryData } from './decrypt';
+
+export const verify = (data: unknown, signature: string) => {
+ const stringified = JSON.stringify(data);
+
+ const hashed = hashString(stringified);
+
+ const decrypted = decryptSecondaryData(signature);
+
+ return decrypted === hashed;
+};
diff --git a/packages/lib/server-only/document/complete-document-with-token.ts b/packages/lib/server-only/document/complete-document-with-token.ts
index b0e7e024f..5f58c5183 100644
--- a/packages/lib/server-only/document/complete-document-with-token.ts
+++ b/packages/lib/server-only/document/complete-document-with-token.ts
@@ -5,7 +5,9 @@ import type { RequestMetadata } from '@documenso/lib/universal/extract-request-m
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
import { prisma } from '@documenso/prisma';
import { DocumentStatus, SigningStatus } from '@documenso/prisma/client';
+import { WebhookTriggerEvents } from '@documenso/prisma/client';
+import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
import { sealDocument } from './seal-document';
import { sendPendingEmail } from './send-pending-email';
@@ -15,14 +17,8 @@ export type CompleteDocumentWithTokenOptions = {
requestMetadata?: RequestMetadata;
};
-export const completeDocumentWithToken = async ({
- token,
- documentId,
- requestMetadata,
-}: CompleteDocumentWithTokenOptions) => {
- 'use server';
-
- const document = await prisma.document.findFirstOrThrow({
+const getDocument = async ({ token, documentId }: CompleteDocumentWithTokenOptions) => {
+ return await prisma.document.findFirstOrThrow({
where: {
id: documentId,
Recipient: {
@@ -39,6 +35,16 @@ export const completeDocumentWithToken = async ({
},
},
});
+};
+
+export const completeDocumentWithToken = async ({
+ token,
+ documentId,
+ requestMetadata,
+}: CompleteDocumentWithTokenOptions) => {
+ 'use server';
+
+ const document = await getDocument({ token, documentId });
if (document.status === DocumentStatus.COMPLETED) {
throw new Error(`Document ${document.id} has already been completed`);
@@ -124,4 +130,13 @@ export const completeDocumentWithToken = async ({
if (documents.count > 0) {
await sealDocument({ documentId: document.id, requestMetadata });
}
+
+ const updatedDocument = await getDocument({ token, documentId });
+
+ await triggerWebhook({
+ event: WebhookTriggerEvents.DOCUMENT_SIGNED,
+ data: updatedDocument,
+ userId: updatedDocument.userId,
+ teamId: updatedDocument.teamId ?? undefined,
+ });
};
diff --git a/packages/lib/server-only/document/create-document.ts b/packages/lib/server-only/document/create-document.ts
index 7243652f0..ce1f16670 100644
--- a/packages/lib/server-only/document/create-document.ts
+++ b/packages/lib/server-only/document/create-document.ts
@@ -5,6 +5,9 @@ import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-log
import type { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
import { prisma } from '@documenso/prisma';
+import { WebhookTriggerEvents } from '@documenso/prisma/client';
+
+import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
export type CreateDocumentOptions = {
title: string;
@@ -63,6 +66,13 @@ export const createDocument = async ({
}),
});
+ await triggerWebhook({
+ event: WebhookTriggerEvents.DOCUMENT_CREATED,
+ data: document,
+ userId,
+ teamId,
+ });
+
return document;
});
};
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/get-document-by-token.ts b/packages/lib/server-only/document/get-document-by-token.ts
index 18f9a5161..d242e72fd 100644
--- a/packages/lib/server-only/document/get-document-by-token.ts
+++ b/packages/lib/server-only/document/get-document-by-token.ts
@@ -70,6 +70,6 @@ export const getDocumentAndRecipientByToken = async ({
return {
...result,
- Recipient: result.Recipient[0],
+ Recipient: result.Recipient,
};
};
diff --git a/packages/lib/server-only/document/seal-document.ts b/packages/lib/server-only/document/seal-document.ts
index 09832db7d..8f39e3d25 100644
--- a/packages/lib/server-only/document/seal-document.ts
+++ b/packages/lib/server-only/document/seal-document.ts
@@ -9,12 +9,14 @@ import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-log
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
import { prisma } from '@documenso/prisma';
import { DocumentStatus, RecipientRole, SigningStatus } from '@documenso/prisma/client';
+import { WebhookTriggerEvents } from '@documenso/prisma/client';
import { signPdf } from '@documenso/signing';
import type { RequestMetadata } from '../../universal/extract-request-metadata';
import { getFile } from '../../universal/upload/get-file';
import { putFile } from '../../universal/upload/put-file';
import { insertFieldInPDF } from '../pdf/insert-field-in-pdf';
+import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
import { sendCompletedEmail } from './send-completed-email';
export type SealDocumentOptions = {
@@ -36,6 +38,7 @@ export const sealDocument = async ({
},
include: {
documentData: true,
+ Recipient: true,
},
});
@@ -134,4 +137,11 @@ export const sealDocument = async ({
if (sendEmail) {
await sendCompletedEmail({ documentId, requestMetadata });
}
+
+ await triggerWebhook({
+ event: WebhookTriggerEvents.DOCUMENT_COMPLETED,
+ data: document,
+ userId: document.userId,
+ teamId: document.teamId ?? undefined,
+ });
};
diff --git a/packages/lib/server-only/document/send-document.tsx b/packages/lib/server-only/document/send-document.tsx
index 0fe53c798..7c928f9a9 100644
--- a/packages/lib/server-only/document/send-document.tsx
+++ b/packages/lib/server-only/document/send-document.tsx
@@ -10,22 +10,26 @@ import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-
import { renderCustomEmailTemplate } from '@documenso/lib/utils/render-custom-email-template';
import { prisma } from '@documenso/prisma';
import { DocumentStatus, RecipientRole, SendStatus } from '@documenso/prisma/client';
+import { WebhookTriggerEvents } from '@documenso/prisma/client';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
import {
RECIPIENT_ROLES_DESCRIPTION,
RECIPIENT_ROLE_TO_EMAIL_TYPE,
} from '../../constants/recipient-roles';
+import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
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 +46,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,
@@ -177,8 +182,18 @@ export const sendDocument = async ({
data: {
status: DocumentStatus.PENDING,
},
+ include: {
+ Recipient: true,
+ },
});
});
+ await triggerWebhook({
+ event: WebhookTriggerEvents.DOCUMENT_SENT,
+ data: updatedDocument,
+ userId,
+ teamId,
+ });
+
return updatedDocument;
};
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/document/viewed-document.ts b/packages/lib/server-only/document/viewed-document.ts
index 452da1460..9722b4fbf 100644
--- a/packages/lib/server-only/document/viewed-document.ts
+++ b/packages/lib/server-only/document/viewed-document.ts
@@ -3,6 +3,10 @@ import type { RequestMetadata } from '@documenso/lib/universal/extract-request-m
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
import { prisma } from '@documenso/prisma';
import { ReadStatus } from '@documenso/prisma/client';
+import { WebhookTriggerEvents } from '@documenso/prisma/client';
+
+import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
+import { getDocumentAndRecipientByToken } from './get-document-by-token';
export type ViewedDocumentOptions = {
token: string;
@@ -51,4 +55,13 @@ export const viewedDocument = async ({ token, requestMetadata }: ViewedDocumentO
}),
});
});
+
+ const document = await getDocumentAndRecipientByToken({ token });
+
+ await triggerWebhook({
+ event: WebhookTriggerEvents.DOCUMENT_OPENED,
+ data: document,
+ userId: document.userId,
+ teamId: document.teamId ?? undefined,
+ });
};
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/public-api/get-user-by-token.ts b/packages/lib/server-only/public-api/get-user-by-token.ts
new file mode 100644
index 000000000..5fe50f336
--- /dev/null
+++ b/packages/lib/server-only/public-api/get-user-by-token.ts
@@ -0,0 +1,37 @@
+import { prisma } from '@documenso/prisma';
+
+import { hashString } from '../auth/hash';
+
+export const getUserByApiToken = async ({ token }: { token: string }) => {
+ const hashedToken = hashString(token);
+
+ const user = await prisma.user.findFirst({
+ where: {
+ ApiToken: {
+ some: {
+ token: hashedToken,
+ },
+ },
+ },
+ include: {
+ ApiToken: true,
+ },
+ });
+
+ if (!user) {
+ throw new Error('Invalid token');
+ }
+
+ const retrievedToken = user.ApiToken.find((apiToken) => apiToken.token === hashedToken);
+
+ // This should be impossible but we need to satisfy TypeScript
+ if (!retrievedToken) {
+ throw new Error('Invalid token');
+ }
+
+ if (retrievedToken.expires && retrievedToken.expires < new Date()) {
+ throw new Error('Expired token');
+ }
+
+ return user;
+};
diff --git a/packages/lib/server-only/public-api/test-credentials.ts b/packages/lib/server-only/public-api/test-credentials.ts
new file mode 100644
index 000000000..2e20d79c4
--- /dev/null
+++ b/packages/lib/server-only/public-api/test-credentials.ts
@@ -0,0 +1,19 @@
+import type { NextApiRequest, NextApiResponse } from 'next';
+
+import { validateApiToken } from '@documenso/lib/server-only/webhooks/zapier/validateApiToken';
+
+export const testCredentialsHandler = async (req: NextApiRequest, res: NextApiResponse) => {
+ try {
+ const { authorization } = req.headers;
+
+ const result = await validateApiToken({ authorization });
+
+ return res.status(200).json({
+ name: result.team?.name ?? result.user.name,
+ });
+ } catch (err) {
+ return res.status(500).json({
+ message: 'Internal Server Error',
+ });
+ }
+};
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/create-user.ts b/packages/lib/server-only/user/create-user.ts
index 1852dc12e..dbcec9efb 100644
--- a/packages/lib/server-only/user/create-user.ts
+++ b/packages/lib/server-only/user/create-user.ts
@@ -7,15 +7,17 @@ import { IdentityProvider, Prisma, TeamMemberInviteStatus } from '@documenso/pri
import { IS_BILLING_ENABLED } from '../../constants/app';
import { SALT_ROUNDS } from '../../constants/auth';
+import { AppError, AppErrorCode } from '../../errors/app-error';
export interface CreateUserOptions {
name: string;
email: string;
password: string;
signature?: string | null;
+ url?: string;
}
-export const createUser = async ({ name, email, password, signature }: CreateUserOptions) => {
+export const createUser = async ({ name, email, password, signature, url }: CreateUserOptions) => {
const hashedPassword = await hash(password, SALT_ROUNDS);
const userExists = await prisma.user.findFirst({
@@ -28,6 +30,22 @@ export const createUser = async ({ name, email, password, signature }: CreateUse
throw new Error('User already exists');
}
+ if (url) {
+ const urlExists = await prisma.user.findFirst({
+ where: {
+ url,
+ },
+ });
+
+ if (urlExists) {
+ throw new AppError(
+ AppErrorCode.PROFILE_URL_TAKEN,
+ 'Profile username is taken',
+ 'The profile username is already taken',
+ );
+ }
+ }
+
const user = await prisma.user.create({
data: {
name,
@@ -35,6 +53,7 @@ export const createUser = async ({ name, email, password, signature }: CreateUse
password: hashedPassword,
signature,
identityProvider: IdentityProvider.DOCUMENSO,
+ url,
},
});
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/lib/server-only/user/update-public-profile.ts b/packages/lib/server-only/user/update-public-profile.ts
new file mode 100644
index 000000000..f70f02cf2
--- /dev/null
+++ b/packages/lib/server-only/user/update-public-profile.ts
@@ -0,0 +1,49 @@
+import { prisma } from '@documenso/prisma';
+
+import { AppError, AppErrorCode } from '../../errors/app-error';
+
+export type UpdatePublicProfileOptions = {
+ userId: number;
+ url: string;
+};
+
+export const updatePublicProfile = async ({ userId, url }: UpdatePublicProfileOptions) => {
+ const isUrlTaken = await prisma.user.findFirst({
+ select: {
+ id: true,
+ },
+ where: {
+ id: {
+ not: userId,
+ },
+ url,
+ },
+ });
+
+ if (isUrlTaken) {
+ throw new AppError(
+ AppErrorCode.PROFILE_URL_TAKEN,
+ 'Profile username is taken',
+ 'The profile username is already taken',
+ );
+ }
+
+ return await prisma.user.update({
+ where: {
+ id: userId,
+ },
+ data: {
+ url,
+ userProfile: {
+ upsert: {
+ create: {
+ bio: '',
+ },
+ update: {
+ bio: '',
+ },
+ },
+ },
+ },
+ });
+};
diff --git a/packages/lib/server-only/webhooks/create-webhook.ts b/packages/lib/server-only/webhooks/create-webhook.ts
new file mode 100644
index 000000000..0eff215af
--- /dev/null
+++ b/packages/lib/server-only/webhooks/create-webhook.ts
@@ -0,0 +1,44 @@
+import { prisma } from '@documenso/prisma';
+import type { WebhookTriggerEvents } from '@documenso/prisma/client';
+
+export interface CreateWebhookOptions {
+ webhookUrl: string;
+ eventTriggers: WebhookTriggerEvents[];
+ secret: string | null;
+ enabled: boolean;
+ userId: number;
+ teamId?: number;
+}
+
+export const createWebhook = async ({
+ webhookUrl,
+ eventTriggers,
+ secret,
+ enabled,
+ userId,
+ teamId,
+}: CreateWebhookOptions) => {
+ if (teamId) {
+ await prisma.team.findFirstOrThrow({
+ where: {
+ id: teamId,
+ members: {
+ some: {
+ userId,
+ },
+ },
+ },
+ });
+ }
+
+ return await prisma.webhook.create({
+ data: {
+ webhookUrl,
+ eventTriggers,
+ secret,
+ enabled,
+ userId,
+ teamId,
+ },
+ });
+};
diff --git a/packages/lib/server-only/webhooks/delete-webhook-by-id.ts b/packages/lib/server-only/webhooks/delete-webhook-by-id.ts
new file mode 100644
index 000000000..9af93bc50
--- /dev/null
+++ b/packages/lib/server-only/webhooks/delete-webhook-by-id.ts
@@ -0,0 +1,30 @@
+import { prisma } from '@documenso/prisma';
+
+export type DeleteWebhookByIdOptions = {
+ id: string;
+ userId: number;
+ teamId?: number;
+};
+
+export const deleteWebhookById = async ({ id, userId, teamId }: DeleteWebhookByIdOptions) => {
+ return await prisma.webhook.delete({
+ where: {
+ id,
+ ...(teamId
+ ? {
+ team: {
+ id: teamId,
+ members: {
+ some: {
+ userId,
+ },
+ },
+ },
+ }
+ : {
+ userId,
+ teamId: null,
+ }),
+ },
+ });
+};
diff --git a/packages/lib/server-only/webhooks/edit-webhook.ts b/packages/lib/server-only/webhooks/edit-webhook.ts
new file mode 100644
index 000000000..e582ebebe
--- /dev/null
+++ b/packages/lib/server-only/webhooks/edit-webhook.ts
@@ -0,0 +1,36 @@
+import type { Prisma } from '@prisma/client';
+
+import { prisma } from '@documenso/prisma';
+
+export type EditWebhookOptions = {
+ id: string;
+ data: Omit;
+ userId: number;
+ teamId?: number;
+};
+
+export const editWebhook = async ({ id, data, userId, teamId }: EditWebhookOptions) => {
+ return await prisma.webhook.update({
+ where: {
+ id,
+ ...(teamId
+ ? {
+ team: {
+ id: teamId,
+ members: {
+ some: {
+ userId,
+ },
+ },
+ },
+ }
+ : {
+ userId,
+ teamId: null,
+ }),
+ },
+ data: {
+ ...data,
+ },
+ });
+};
diff --git a/packages/lib/server-only/webhooks/get-all-webhooks-by-event-trigger.ts b/packages/lib/server-only/webhooks/get-all-webhooks-by-event-trigger.ts
new file mode 100644
index 000000000..f2dac459b
--- /dev/null
+++ b/packages/lib/server-only/webhooks/get-all-webhooks-by-event-trigger.ts
@@ -0,0 +1,38 @@
+import { prisma } from '@documenso/prisma';
+import type { WebhookTriggerEvents } from '@documenso/prisma/client';
+
+export type GetAllWebhooksByEventTriggerOptions = {
+ event: WebhookTriggerEvents;
+ userId: number;
+ teamId?: number;
+};
+
+export const getAllWebhooksByEventTrigger = async ({
+ event,
+ userId,
+ teamId,
+}: GetAllWebhooksByEventTriggerOptions) => {
+ return prisma.webhook.findMany({
+ where: {
+ enabled: true,
+ eventTriggers: {
+ has: event,
+ },
+ ...(teamId
+ ? {
+ team: {
+ id: teamId,
+ members: {
+ some: {
+ userId,
+ },
+ },
+ },
+ }
+ : {
+ userId,
+ teamId: null,
+ }),
+ },
+ });
+};
diff --git a/packages/lib/server-only/webhooks/get-webhook-by-id.ts b/packages/lib/server-only/webhooks/get-webhook-by-id.ts
new file mode 100644
index 000000000..fe2ff62ff
--- /dev/null
+++ b/packages/lib/server-only/webhooks/get-webhook-by-id.ts
@@ -0,0 +1,30 @@
+import { prisma } from '@documenso/prisma';
+
+export type GetWebhookByIdOptions = {
+ id: string;
+ userId: number;
+ teamId?: number;
+};
+
+export const getWebhookById = async ({ id, userId, teamId }: GetWebhookByIdOptions) => {
+ return await prisma.webhook.findFirstOrThrow({
+ where: {
+ id,
+ ...(teamId
+ ? {
+ team: {
+ id: teamId,
+ members: {
+ some: {
+ userId,
+ },
+ },
+ },
+ }
+ : {
+ userId,
+ teamId: null,
+ }),
+ },
+ });
+};
diff --git a/packages/lib/server-only/webhooks/get-webhooks-by-team-id.ts b/packages/lib/server-only/webhooks/get-webhooks-by-team-id.ts
new file mode 100644
index 000000000..82737a46d
--- /dev/null
+++ b/packages/lib/server-only/webhooks/get-webhooks-by-team-id.ts
@@ -0,0 +1,19 @@
+import { prisma } from '@documenso/prisma';
+
+export const getWebhooksByTeamId = async (teamId: number, userId: number) => {
+ return await prisma.webhook.findMany({
+ where: {
+ team: {
+ id: teamId,
+ members: {
+ some: {
+ userId,
+ },
+ },
+ },
+ },
+ orderBy: {
+ createdAt: 'desc',
+ },
+ });
+};
diff --git a/packages/lib/server-only/webhooks/get-webhooks-by-user-id.ts b/packages/lib/server-only/webhooks/get-webhooks-by-user-id.ts
new file mode 100644
index 000000000..121fc670d
--- /dev/null
+++ b/packages/lib/server-only/webhooks/get-webhooks-by-user-id.ts
@@ -0,0 +1,12 @@
+import { prisma } from '@documenso/prisma';
+
+export const getWebhooksByUserId = async (userId: number) => {
+ return await prisma.webhook.findMany({
+ where: {
+ userId,
+ },
+ orderBy: {
+ createdAt: 'desc',
+ },
+ });
+};
diff --git a/packages/lib/server-only/webhooks/trigger/execute-webhook.ts b/packages/lib/server-only/webhooks/trigger/execute-webhook.ts
new file mode 100644
index 000000000..cfc828a7f
--- /dev/null
+++ b/packages/lib/server-only/webhooks/trigger/execute-webhook.ts
@@ -0,0 +1,58 @@
+import { prisma } from '@documenso/prisma';
+import {
+ Prisma,
+ type Webhook,
+ WebhookCallStatus,
+ type WebhookTriggerEvents,
+} from '@documenso/prisma/client';
+
+export type ExecuteWebhookOptions = {
+ event: WebhookTriggerEvents;
+ webhook: Webhook;
+ data: unknown;
+};
+
+export const executeWebhook = async ({ event, webhook, data }: ExecuteWebhookOptions) => {
+ const { webhookUrl: url, secret } = webhook;
+
+ console.log('Executing webhook', { event, url });
+
+ const payload = {
+ event,
+ payload: data,
+ createdAt: new Date().toISOString(),
+ webhookEndpoint: url,
+ };
+
+ const response = await fetch(url, {
+ method: 'POST',
+ body: JSON.stringify(payload),
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-Documenso-Secret': secret ?? '',
+ },
+ });
+
+ const body = await response.text();
+
+ let responseBody: Prisma.InputJsonValue | Prisma.JsonNullValueInput = Prisma.JsonNull;
+
+ try {
+ responseBody = JSON.parse(body);
+ } catch (err) {
+ responseBody = body;
+ }
+
+ await prisma.webhookCall.create({
+ data: {
+ url,
+ event,
+ status: response.ok ? WebhookCallStatus.SUCCESS : WebhookCallStatus.FAILED,
+ requestBody: payload as Prisma.InputJsonValue,
+ responseCode: response.status,
+ responseBody,
+ responseHeaders: Object.fromEntries(response.headers.entries()),
+ webhookId: webhook.id,
+ },
+ });
+};
diff --git a/packages/lib/server-only/webhooks/trigger/handler.ts b/packages/lib/server-only/webhooks/trigger/handler.ts
new file mode 100644
index 000000000..4e705efea
--- /dev/null
+++ b/packages/lib/server-only/webhooks/trigger/handler.ts
@@ -0,0 +1,58 @@
+import type { NextApiRequest, NextApiResponse } from 'next';
+
+import { verify } from '../../crypto/verify';
+import { getAllWebhooksByEventTrigger } from '../get-all-webhooks-by-event-trigger';
+import { executeWebhook } from './execute-webhook';
+import { ZTriggerWebhookBodySchema } from './schema';
+
+export type HandlerTriggerWebhooksResponse =
+ | {
+ success: true;
+ message: string;
+ }
+ | {
+ success: false;
+ error: string;
+ };
+
+export const handlerTriggerWebhooks = async (
+ req: NextApiRequest,
+ res: NextApiResponse,
+) => {
+ const signature = req.headers['x-webhook-signature'];
+
+ if (typeof signature !== 'string') {
+ console.log('Missing signature');
+ return res.status(400).json({ success: false, error: 'Missing signature' });
+ }
+
+ const valid = verify(req.body, signature);
+
+ if (!valid) {
+ console.log('Invalid signature');
+ return res.status(400).json({ success: false, error: 'Invalid signature' });
+ }
+
+ const result = ZTriggerWebhookBodySchema.safeParse(req.body);
+
+ if (!result.success) {
+ console.log('Invalid request body');
+ return res.status(400).json({ success: false, error: 'Invalid request body' });
+ }
+
+ const { event, data, userId, teamId } = result.data;
+
+ const allWebhooks = await getAllWebhooksByEventTrigger({ event, userId, teamId });
+
+ await Promise.allSettled(
+ allWebhooks.map(async (webhook) =>
+ executeWebhook({
+ event,
+ webhook,
+ data,
+ }),
+ ),
+ );
+
+ return res.status(200).json({ success: true, message: 'Webhooks executed successfully' });
+};
diff --git a/packages/lib/server-only/webhooks/trigger/schema.ts b/packages/lib/server-only/webhooks/trigger/schema.ts
new file mode 100644
index 000000000..ee6d0e48d
--- /dev/null
+++ b/packages/lib/server-only/webhooks/trigger/schema.ts
@@ -0,0 +1,12 @@
+import { z } from 'zod';
+
+import { WebhookTriggerEvents } from '@documenso/prisma/client';
+
+export const ZTriggerWebhookBodySchema = z.object({
+ event: z.nativeEnum(WebhookTriggerEvents),
+ data: z.unknown(),
+ userId: z.number(),
+ teamId: z.number().optional(),
+});
+
+export type TTriggerWebhookBodySchema = z.infer;
diff --git a/packages/lib/server-only/webhooks/trigger/trigger-webhook.ts b/packages/lib/server-only/webhooks/trigger/trigger-webhook.ts
new file mode 100644
index 000000000..e0a5eaaaf
--- /dev/null
+++ b/packages/lib/server-only/webhooks/trigger/trigger-webhook.ts
@@ -0,0 +1,47 @@
+import type { WebhookTriggerEvents } from '@documenso/prisma/client';
+
+import { NEXT_PUBLIC_WEBAPP_URL } from '../../../constants/app';
+import { sign } from '../../crypto/sign';
+import { getAllWebhooksByEventTrigger } from '../get-all-webhooks-by-event-trigger';
+
+export type TriggerWebhookOptions = {
+ event: WebhookTriggerEvents;
+ data: Record;
+ userId: number;
+ teamId?: number;
+};
+
+export const triggerWebhook = async ({ event, data, userId, teamId }: TriggerWebhookOptions) => {
+ try {
+ const body = {
+ event,
+ data,
+ userId,
+ teamId,
+ };
+
+ const registeredWebhooks = await getAllWebhooksByEventTrigger({ event, userId, teamId });
+
+ if (registeredWebhooks.length === 0) {
+ return;
+ }
+
+ const signature = sign(body);
+
+ await Promise.race([
+ fetch(`${NEXT_PUBLIC_WEBAPP_URL()}/api/webhook/trigger`, {
+ method: 'POST',
+ headers: {
+ 'content-type': 'application/json',
+ 'x-webhook-signature': signature,
+ },
+ body: JSON.stringify(body),
+ }),
+ new Promise((_, reject) => {
+ setTimeout(() => reject(new Error('Request timeout')), 500);
+ }),
+ ]).catch(() => null);
+ } catch (err) {
+ throw new Error(`Failed to trigger webhook`);
+ }
+};
diff --git a/packages/lib/server-only/webhooks/zapier/list-documents.ts b/packages/lib/server-only/webhooks/zapier/list-documents.ts
new file mode 100644
index 000000000..56649fac8
--- /dev/null
+++ b/packages/lib/server-only/webhooks/zapier/list-documents.ts
@@ -0,0 +1,67 @@
+import type { NextApiRequest, NextApiResponse } from 'next';
+
+import { findDocuments } from '@documenso/lib/server-only/document/find-documents';
+import { getRecipientsForDocument } from '@documenso/lib/server-only/recipient/get-recipients-for-document';
+import type { Webhook } from '@documenso/prisma/client';
+
+import { getWebhooksByTeamId } from '../get-webhooks-by-team-id';
+import { getWebhooksByUserId } from '../get-webhooks-by-user-id';
+import { validateApiToken } from './validateApiToken';
+
+export const listDocumentsHandler = async (req: NextApiRequest, res: NextApiResponse) => {
+ try {
+ const { authorization } = req.headers;
+ const { user, userId, teamId } = await validateApiToken({ authorization });
+
+ let allWebhooks: Webhook[] = [];
+
+ const documents = await findDocuments({
+ userId: userId ?? user.id,
+ teamId: teamId ?? undefined,
+ perPage: 1,
+ });
+
+ const recipients = await getRecipientsForDocument({
+ documentId: documents.data[0].id,
+ userId: userId ?? user.id,
+ teamId: teamId ?? undefined,
+ });
+
+ if (userId) {
+ allWebhooks = await getWebhooksByUserId(userId);
+ }
+
+ if (teamId) {
+ allWebhooks = await getWebhooksByTeamId(teamId, user.id);
+ }
+
+ if (documents && documents.data.length > 0 && allWebhooks.length > 0 && recipients.length > 0) {
+ const testWebhook = {
+ event: allWebhooks[0].eventTriggers.toString(),
+ createdAt: allWebhooks[0].createdAt,
+ webhookEndpoint: allWebhooks[0].webhookUrl,
+ payload: {
+ id: documents.data[0].id,
+ userId: documents.data[0].userId,
+ title: documents.data[0].title,
+ status: documents.data[0].status,
+ documentDataId: documents.data[0].documentDataId,
+ createdAt: documents.data[0].createdAt,
+ updatedAt: documents.data[0].updatedAt,
+ completedAt: documents.data[0].completedAt,
+ deletedAt: documents.data[0].deletedAt,
+ teamId: documents.data[0].teamId,
+ Recipient: recipients,
+ },
+ };
+
+ return res.status(200).json([testWebhook]);
+ }
+
+ return res.status(200).json([]);
+ } catch (err) {
+ return res.status(500).json({
+ message: 'Internal Server Error',
+ });
+ }
+};
diff --git a/packages/lib/server-only/webhooks/zapier/subscribe.ts b/packages/lib/server-only/webhooks/zapier/subscribe.ts
new file mode 100644
index 000000000..90c68e063
--- /dev/null
+++ b/packages/lib/server-only/webhooks/zapier/subscribe.ts
@@ -0,0 +1,32 @@
+import type { NextApiRequest, NextApiResponse } from 'next';
+
+import { prisma } from '@documenso/prisma';
+
+import { validateApiToken } from './validateApiToken';
+
+export const subscribeHandler = async (req: NextApiRequest, res: NextApiResponse) => {
+ try {
+ const { authorization } = req.headers;
+
+ const { webhookUrl, eventTrigger } = req.body;
+
+ const result = await validateApiToken({ authorization });
+
+ const createdWebhook = await prisma.webhook.create({
+ data: {
+ webhookUrl,
+ eventTriggers: [eventTrigger],
+ secret: null,
+ enabled: true,
+ userId: result.userId ?? result.user.id,
+ teamId: result.teamId ?? undefined,
+ },
+ });
+
+ return res.status(200).json(createdWebhook);
+ } catch (err) {
+ return res.status(500).json({
+ message: 'Internal Server Error',
+ });
+ }
+};
diff --git a/packages/lib/server-only/webhooks/zapier/unsubscribe.ts b/packages/lib/server-only/webhooks/zapier/unsubscribe.ts
new file mode 100644
index 000000000..07fa75e11
--- /dev/null
+++ b/packages/lib/server-only/webhooks/zapier/unsubscribe.ts
@@ -0,0 +1,29 @@
+import type { NextApiRequest, NextApiResponse } from 'next';
+
+import { prisma } from '@documenso/prisma';
+
+import { validateApiToken } from './validateApiToken';
+
+export const unsubscribeHandler = async (req: NextApiRequest, res: NextApiResponse) => {
+ try {
+ const { authorization } = req.headers;
+
+ const { webhookId } = req.body;
+
+ const result = await validateApiToken({ authorization });
+
+ const deletedWebhook = await prisma.webhook.delete({
+ where: {
+ id: webhookId,
+ userId: result.userId ?? result.user.id,
+ teamId: result.teamId ?? undefined,
+ },
+ });
+
+ return res.status(200).json(deletedWebhook);
+ } catch (err) {
+ return res.status(500).json({
+ message: 'Internal Server Error',
+ });
+ }
+};
diff --git a/packages/lib/server-only/webhooks/zapier/validateApiToken.ts b/packages/lib/server-only/webhooks/zapier/validateApiToken.ts
new file mode 100644
index 000000000..45e2b7522
--- /dev/null
+++ b/packages/lib/server-only/webhooks/zapier/validateApiToken.ts
@@ -0,0 +1,20 @@
+import { getApiTokenByToken } from '../../public-api/get-api-token-by-token';
+
+type ValidateApiTokenOptions = {
+ authorization: string | undefined;
+};
+
+export const validateApiToken = async ({ authorization }: ValidateApiTokenOptions) => {
+ try {
+ // 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('Missing API token');
+ }
+
+ return await getApiTokenByToken({ token });
+ } catch (err) {
+ throw new Error(`Failed to validate API token`);
+ }
+};
diff --git a/packages/lib/universal/webhook/to-friendly-webhook-event-name.ts b/packages/lib/universal/webhook/to-friendly-webhook-event-name.ts
new file mode 100644
index 000000000..5af3a2782
--- /dev/null
+++ b/packages/lib/universal/webhook/to-friendly-webhook-event-name.ts
@@ -0,0 +1,3 @@
+export const toFriendlyWebhookEventName = (eventName: string) => {
+ return eventName.replace(/_/g, '.').toLowerCase();
+};
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/20240206131417_add_user_webhooks/migration.sql b/packages/prisma/migrations/20240206131417_add_user_webhooks/migration.sql
new file mode 100644
index 000000000..7bf4e190f
--- /dev/null
+++ b/packages/prisma/migrations/20240206131417_add_user_webhooks/migration.sql
@@ -0,0 +1,19 @@
+-- CreateEnum
+CREATE TYPE "WebhookTriggerEvents" AS ENUM ('DOCUMENT_CREATED', 'DOCUMENT_SIGNED');
+
+-- CreateTable
+CREATE TABLE "Webhook" (
+ "id" SERIAL NOT NULL,
+ "webhookUrl" TEXT NOT NULL,
+ "eventTriggers" "WebhookTriggerEvents"[],
+ "secret" TEXT,
+ "enabled" BOOLEAN NOT NULL DEFAULT true,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "userId" INTEGER NOT NULL,
+
+ CONSTRAINT "Webhook_pkey" PRIMARY KEY ("id")
+);
+
+-- AddForeignKey
+ALTER TABLE "Webhook" ADD CONSTRAINT "Webhook_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
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/20240220115435_add_public_profile_url_bio/migration.sql b/packages/prisma/migrations/20240220115435_add_public_profile_url_bio/migration.sql
new file mode 100644
index 000000000..719968aff
--- /dev/null
+++ b/packages/prisma/migrations/20240220115435_add_public_profile_url_bio/migration.sql
@@ -0,0 +1,25 @@
+/*
+ Warnings:
+
+ - A unique constraint covering the columns `[profileURL]` on the table `User` will be added. If there are existing duplicate values, this will fail.
+
+*/
+-- AlterTable
+ALTER TABLE "User" ADD COLUMN "profileURL" TEXT;
+
+-- CreateTable
+CREATE TABLE "UserProfile" (
+ "profileURL" TEXT NOT NULL,
+ "profileBio" TEXT,
+
+ CONSTRAINT "UserProfile_pkey" PRIMARY KEY ("profileURL")
+);
+
+-- CreateIndex
+CREATE UNIQUE INDEX "UserProfile_profileURL_key" ON "UserProfile"("profileURL");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "User_profileURL_key" ON "User"("profileURL");
+
+-- AddForeignKey
+ALTER TABLE "User" ADD CONSTRAINT "User_profileURL_fkey" FOREIGN KEY ("profileURL") REFERENCES "UserProfile"("profileURL") ON DELETE CASCADE ON UPDATE CASCADE;
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/20240224085633_extend_webhook_trigger_events/migration.sql b/packages/prisma/migrations/20240224085633_extend_webhook_trigger_events/migration.sql
new file mode 100644
index 000000000..8733b4c9e
--- /dev/null
+++ b/packages/prisma/migrations/20240224085633_extend_webhook_trigger_events/migration.sql
@@ -0,0 +1,11 @@
+-- AlterEnum
+-- This migration adds more than one value to an enum.
+-- With PostgreSQL versions 11 and earlier, this is not possible
+-- in a single migration. This can be worked around by creating
+-- multiple migrations, each migration adding only one value to
+-- the enum.
+
+
+ALTER TYPE "WebhookTriggerEvents" ADD VALUE 'DOCUMENT_SENT';
+ALTER TYPE "WebhookTriggerEvents" ADD VALUE 'DOCUMENT_OPENED';
+ALTER TYPE "WebhookTriggerEvents" ADD VALUE 'DOCUMENT_COMPLETED';
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/migrations/20240227003622_migrate_to_cuids_for_webhooks/migration.sql b/packages/prisma/migrations/20240227003622_migrate_to_cuids_for_webhooks/migration.sql
new file mode 100644
index 000000000..cd8fd9589
--- /dev/null
+++ b/packages/prisma/migrations/20240227003622_migrate_to_cuids_for_webhooks/migration.sql
@@ -0,0 +1,12 @@
+/*
+ Warnings:
+
+ - The primary key for the `Webhook` table will be changed. If it partially fails, the table could be left without primary key constraint.
+
+*/
+-- AlterTable
+ALTER TABLE "Webhook" DROP CONSTRAINT "Webhook_pkey",
+ALTER COLUMN "id" DROP DEFAULT,
+ALTER COLUMN "id" SET DATA TYPE TEXT,
+ADD CONSTRAINT "Webhook_pkey" PRIMARY KEY ("id");
+DROP SEQUENCE "Webhook_id_seq";
diff --git a/packages/prisma/migrations/20240227015420_add_webhook_call_table/migration.sql b/packages/prisma/migrations/20240227015420_add_webhook_call_table/migration.sql
new file mode 100644
index 000000000..5abac6d6a
--- /dev/null
+++ b/packages/prisma/migrations/20240227015420_add_webhook_call_table/migration.sql
@@ -0,0 +1,20 @@
+-- CreateEnum
+CREATE TYPE "WebhookCallStatus" AS ENUM ('SUCCESS', 'FAILED');
+
+-- CreateTable
+CREATE TABLE "WebhookCall" (
+ "id" TEXT NOT NULL,
+ "status" "WebhookCallStatus" NOT NULL,
+ "url" TEXT NOT NULL,
+ "requestBody" JSONB NOT NULL,
+ "responseCode" INTEGER NOT NULL,
+ "responseHeaders" JSONB,
+ "responseBody" JSONB,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "webhookId" TEXT NOT NULL,
+
+ CONSTRAINT "WebhookCall_pkey" PRIMARY KEY ("id")
+);
+
+-- AddForeignKey
+ALTER TABLE "WebhookCall" ADD CONSTRAINT "WebhookCall_webhookId_fkey" FOREIGN KEY ("webhookId") REFERENCES "Webhook"("id") ON DELETE CASCADE ON UPDATE CASCADE;
diff --git a/packages/prisma/migrations/20240227023747_add_team_webhooks/migration.sql b/packages/prisma/migrations/20240227023747_add_team_webhooks/migration.sql
new file mode 100644
index 000000000..62e556228
--- /dev/null
+++ b/packages/prisma/migrations/20240227023747_add_team_webhooks/migration.sql
@@ -0,0 +1,5 @@
+-- AlterTable
+ALTER TABLE "Webhook" ADD COLUMN "teamId" INTEGER;
+
+-- AddForeignKey
+ALTER TABLE "Webhook" ADD CONSTRAINT "Webhook_teamId_fkey" FOREIGN KEY ("teamId") REFERENCES "Team"("id") ON DELETE CASCADE ON UPDATE CASCADE;
diff --git a/packages/prisma/migrations/20240227031228_add_event_to_webhook_call_model/migration.sql b/packages/prisma/migrations/20240227031228_add_event_to_webhook_call_model/migration.sql
new file mode 100644
index 000000000..a56b5750a
--- /dev/null
+++ b/packages/prisma/migrations/20240227031228_add_event_to_webhook_call_model/migration.sql
@@ -0,0 +1,8 @@
+/*
+ Warnings:
+
+ - Added the required column `event` to the `WebhookCall` table without a default value. This is not possible if the table is not empty.
+
+*/
+-- AlterTable
+ALTER TABLE "WebhookCall" ADD COLUMN "event" "WebhookTriggerEvents" NOT NULL;
diff --git a/packages/prisma/migrations/20240227111633_rework_user_profiles/migration.sql b/packages/prisma/migrations/20240227111633_rework_user_profiles/migration.sql
new file mode 100644
index 000000000..6bf9c0759
--- /dev/null
+++ b/packages/prisma/migrations/20240227111633_rework_user_profiles/migration.sql
@@ -0,0 +1,37 @@
+/*
+ Warnings:
+
+ - You are about to drop the column `profileURL` on the `User` table. All the data in the column will be lost.
+ - The primary key for the `UserProfile` table will be changed. If it partially fails, the table could be left without primary key constraint.
+ - You are about to drop the column `profileBio` on the `UserProfile` table. All the data in the column will be lost.
+ - You are about to drop the column `profileURL` on the `UserProfile` table. All the data in the column will be lost.
+ - A unique constraint covering the columns `[url]` on the table `User` will be added. If there are existing duplicate values, this will fail.
+ - Added the required column `id` to the `UserProfile` table without a default value. This is not possible if the table is not empty.
+
+*/
+-- DropForeignKey
+ALTER TABLE "User" DROP CONSTRAINT "User_profileURL_fkey";
+
+-- DropIndex
+DROP INDEX "User_profileURL_key";
+
+-- DropIndex
+DROP INDEX "UserProfile_profileURL_key";
+
+-- AlterTable
+ALTER TABLE "User" DROP COLUMN "profileURL",
+ADD COLUMN "url" TEXT;
+
+-- AlterTable
+ALTER TABLE "UserProfile" DROP CONSTRAINT "UserProfile_pkey",
+DROP COLUMN "profileBio",
+DROP COLUMN "profileURL",
+ADD COLUMN "bio" TEXT,
+ADD COLUMN "id" INTEGER NOT NULL,
+ADD CONSTRAINT "UserProfile_pkey" PRIMARY KEY ("id");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "User_url_key" ON "User"("url");
+
+-- AddForeignKey
+ALTER TABLE "UserProfile" ADD CONSTRAINT "UserProfile_id_fkey" FOREIGN KEY ("id") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma
index 2887cd6d2..b1bf9f985 100644
--- a/packages/prisma/schema.prisma
+++ b/packages/prisma/schema.prisma
@@ -43,14 +43,26 @@ model User {
twoFactorSecret String?
twoFactorEnabled Boolean @default(false)
twoFactorBackupCodes String?
+ url String? @unique
+ userProfile UserProfile?
VerificationToken VerificationToken[]
+ ApiToken ApiToken[]
Template Template[]
securityAuditLogs UserSecurityAuditLog[]
+ Webhooks Webhook[]
+ siteSettings SiteSettings[]
@@index([email])
}
+model UserProfile {
+ id Int @id
+ bio String?
+
+ User User? @relation(fields: [id], references: [id], onDelete: Cascade)
+}
+
enum UserSecurityAuditLogType {
ACCOUNT_PROFILE_UPDATE
ACCOUNT_SSO_LINK
@@ -94,6 +106,65 @@ model VerificationToken {
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}
+enum WebhookTriggerEvents {
+ DOCUMENT_CREATED
+ DOCUMENT_SENT
+ DOCUMENT_OPENED
+ DOCUMENT_SIGNED
+ DOCUMENT_COMPLETED
+}
+
+model Webhook {
+ id String @id @default(cuid())
+ webhookUrl String
+ eventTriggers WebhookTriggerEvents[]
+ secret String?
+ enabled Boolean @default(true)
+ createdAt DateTime @default(now())
+ updatedAt DateTime @default(now()) @updatedAt
+ userId Int
+ User User @relation(fields: [userId], references: [id], onDelete: Cascade)
+ teamId Int?
+ team Team? @relation(fields: [teamId], references: [id], onDelete: Cascade)
+ WebhookCall WebhookCall[]
+}
+
+enum WebhookCallStatus {
+ SUCCESS
+ FAILED
+}
+
+model WebhookCall {
+ id String @id @default(cuid())
+ status WebhookCallStatus
+ url String
+ event WebhookTriggerEvents
+ requestBody Json
+ responseCode Int
+ responseHeaders Json?
+ responseBody Json?
+ createdAt DateTime @default(now())
+ webhookId String
+ webhook Webhook @relation(fields: [webhookId], 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 +281,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 +364,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 +428,8 @@ model Team {
document Document[]
templates Template[]
+ ApiToken ApiToken[]
+ Webhook Webhook[]
}
model TeamPending {
@@ -450,3 +523,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/pr-711-deletion-of-documents.ts b/packages/prisma/seed/pr-711-deletion-of-documents.ts
index 7542cdb84..5365ecf47 100644
--- a/packages/prisma/seed/pr-711-deletion-of-documents.ts
+++ b/packages/prisma/seed/pr-711-deletion-of-documents.ts
@@ -49,6 +49,7 @@ export const seedDatabase = async () => {
email: u.email,
password: hashSync(u.password),
emailVerified: new Date(),
+ url: u.email,
},
}),
),
diff --git a/packages/prisma/seed/pr-713-add-document-search-to-command-menu.ts b/packages/prisma/seed/pr-713-add-document-search-to-command-menu.ts
index 22e8897a9..0fe27b703 100644
--- a/packages/prisma/seed/pr-713-add-document-search-to-command-menu.ts
+++ b/packages/prisma/seed/pr-713-add-document-search-to-command-menu.ts
@@ -49,6 +49,7 @@ export const seedDatabase = async () => {
email: u.email,
password: hashSync(u.password),
emailVerified: new Date(),
+ url: u.email,
},
}),
),
diff --git a/packages/prisma/seed/pr-718-add-stepper-component.ts b/packages/prisma/seed/pr-718-add-stepper-component.ts
index 57a0ddc61..d436a97b1 100644
--- a/packages/prisma/seed/pr-718-add-stepper-component.ts
+++ b/packages/prisma/seed/pr-718-add-stepper-component.ts
@@ -23,6 +23,7 @@ export const seedDatabase = async () => {
email: TEST_USER.email,
password: hashSync(TEST_USER.password),
emailVerified: new Date(),
+ url: TEST_USER.email,
},
});
};
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/prisma/seed/users.ts b/packages/prisma/seed/users.ts
index f4dd714ed..353683a1d 100644
--- a/packages/prisma/seed/users.ts
+++ b/packages/prisma/seed/users.ts
@@ -21,6 +21,7 @@ export const seedUser = async ({
email,
password: hashSync(password),
emailVerified: verified ? new Date() : undefined,
+ url: name,
},
});
};
diff --git a/packages/prisma/types/document-with-recipient.ts b/packages/prisma/types/document-with-recipient.ts
index c55b99e67..32ecd29cb 100644
--- a/packages/prisma/types/document-with-recipient.ts
+++ b/packages/prisma/types/document-with-recipient.ts
@@ -5,6 +5,6 @@ export type DocumentWithRecipients = Document & {
};
export type DocumentWithRecipient = Document & {
- Recipient: Recipient;
+ Recipient: Recipient[];
documentData: DocumentData;
};
diff --git a/packages/tailwind-config/index.cjs b/packages/tailwind-config/index.cjs
index 1564454d8..81706fd37 100644
--- a/packages/tailwind-config/index.cjs
+++ b/packages/tailwind-config/index.cjs
@@ -11,6 +11,9 @@ module.exports = {
sans: ['var(--font-sans)', ...fontFamily.sans],
signature: ['var(--font-signature)'],
},
+ zIndex: {
+ 9999: '9999',
+ },
colors: {
border: 'hsl(var(--border))',
input: 'hsl(var(--input))',
@@ -45,6 +48,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/auth-router/router.ts b/packages/trpc/server/auth-router/router.ts
index 65fe8d296..3f199ac11 100644
--- a/packages/trpc/server/auth-router/router.ts
+++ b/packages/trpc/server/auth-router/router.ts
@@ -1,6 +1,8 @@
import { TRPCError } from '@trpc/server';
import { env } from 'next-runtime-env';
+import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
+import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { ErrorCode } from '@documenso/lib/next-auth/error-codes';
import { compareSync } from '@documenso/lib/server-only/auth/hash';
import { createUser } from '@documenso/lib/server-only/user/create-user';
@@ -21,14 +23,29 @@ export const authRouter = router({
});
}
- const { name, email, password, signature } = input;
+ const { name, email, password, signature, url } = input;
- const user = await createUser({ name, email, password, signature });
+ if ((true || IS_BILLING_ENABLED()) && url && url.length <= 6) {
+ throw new AppError(
+ AppErrorCode.PREMIUM_PROFILE_URL,
+ 'Only subscribers can have a username shorter than 6 characters',
+ );
+ }
+
+ const user = await createUser({ name, email, password, signature, url });
await sendConfirmationToken({ email: user.email });
return user;
} catch (err) {
+ console.log(err);
+
+ const error = AppError.parseError(err);
+
+ if (error.code !== AppErrorCode.UNKNOWN_ERROR) {
+ throw AppError.parseErrorToTRPCError(error);
+ }
+
let message =
'We were unable to create your account. Please review the information you provided and try again.';
diff --git a/packages/trpc/server/auth-router/schema.ts b/packages/trpc/server/auth-router/schema.ts
index dbe42a25c..9a52f7fc2 100644
--- a/packages/trpc/server/auth-router/schema.ts
+++ b/packages/trpc/server/auth-router/schema.ts
@@ -21,6 +21,15 @@ export const ZSignUpMutationSchema = z.object({
email: z.string().email(),
password: ZPasswordSchema,
signature: z.string().min(1, { message: 'A signature is required.' }),
+ url: z
+ .string()
+ .trim()
+ .toLowerCase()
+ .min(1)
+ .regex(/^[a-z0-9-]+$/, {
+ message: 'Username can only container alphanumeric characters and dashes.',
+ })
+ .optional(),
});
export type TSignUpMutationSchema = 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..f9f409aa6 100644
--- a/packages/trpc/server/profile-router/router.ts
+++ b/packages/trpc/server/profile-router/router.ts
@@ -1,5 +1,9 @@
import { TRPCError } from '@trpc/server';
+import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
+import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
+import { getSubscriptionsByUserId } from '@documenso/lib/server-only/subscription/get-subscriptions-by-user-id';
+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';
@@ -7,7 +11,9 @@ import { resetPassword } from '@documenso/lib/server-only/user/reset-password';
import { sendConfirmationToken } from '@documenso/lib/server-only/user/send-confirmation-token';
import { updatePassword } from '@documenso/lib/server-only/user/update-password';
import { updateProfile } from '@documenso/lib/server-only/user/update-profile';
+import { updatePublicProfile } from '@documenso/lib/server-only/user/update-public-profile';
import { extractNextApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
+import { SubscriptionStatus } from '@documenso/prisma/client';
import { adminProcedure, authenticatedProcedure, procedure, router } from '../trpc';
import {
@@ -18,6 +24,7 @@ import {
ZRetrieveUserByIdQuerySchema,
ZUpdatePasswordMutationSchema,
ZUpdateProfileMutationSchema,
+ ZUpdatePublicProfileMutationSchema,
} from './schema';
export const profileRouter = router({
@@ -73,6 +80,48 @@ export const profileRouter = router({
}
}),
+ updatePublicProfile: authenticatedProcedure
+ .input(ZUpdatePublicProfileMutationSchema)
+ .mutation(async ({ input, ctx }) => {
+ try {
+ const { url } = input;
+
+ if (IS_BILLING_ENABLED() && url.length <= 6) {
+ const subscriptions = await getSubscriptionsByUserId({
+ userId: ctx.user.id,
+ }).then((subscriptions) =>
+ subscriptions.filter((s) => s.status === SubscriptionStatus.ACTIVE),
+ );
+
+ if (subscriptions.length === 0) {
+ throw new AppError(
+ AppErrorCode.PREMIUM_PROFILE_URL,
+ 'Only subscribers can have a username shorter than 6 characters',
+ );
+ }
+ }
+
+ const user = await updatePublicProfile({
+ userId: ctx.user.id,
+ url,
+ });
+
+ return { success: true, url: user.url };
+ } catch (err) {
+ const error = AppError.parseError(err);
+
+ if (error.code !== AppErrorCode.UNKNOWN_ERROR) {
+ throw AppError.parseErrorToTRPCError(error);
+ }
+
+ throw new TRPCError({
+ code: 'BAD_REQUEST',
+ message:
+ 'We were unable to update your public profile. Please review the information you provided and try again.',
+ });
+ }
+ }),
+
updatePassword: authenticatedProcedure
.input(ZUpdatePasswordMutationSchema)
.mutation(async ({ input, ctx }) => {
@@ -155,4 +204,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/profile-router/schema.ts b/packages/trpc/server/profile-router/schema.ts
index 522b13552..dc62f83ba 100644
--- a/packages/trpc/server/profile-router/schema.ts
+++ b/packages/trpc/server/profile-router/schema.ts
@@ -16,6 +16,17 @@ export const ZUpdateProfileMutationSchema = z.object({
signature: z.string(),
});
+export const ZUpdatePublicProfileMutationSchema = z.object({
+ url: z
+ .string()
+ .trim()
+ .toLowerCase()
+ .min(1, { message: 'Please enter a valid username.' })
+ .regex(/^[a-z0-9-]+$/, {
+ message: 'Username can only container alphanumeric characters and dashes.',
+ }),
+});
+
export const ZUpdatePasswordMutationSchema = z.object({
currentPassword: ZCurrentPasswordSchema,
password: ZPasswordSchema,
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..16f79f712 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';
@@ -11,6 +12,7 @@ import { teamRouter } from './team-router/router';
import { templateRouter } from './template-router/router';
import { router } from './trpc';
import { twoFactorAuthenticationRouter } from './two-factor-authentication-router/router';
+import { webhookRouter } from './webhook-router/router';
export const appRouter = router({
auth: authRouter,
@@ -21,9 +23,11 @@ export const appRouter = router({
recipient: recipientRouter,
admin: adminRouter,
shareLink: shareLinkRouter,
+ apiToken: apiTokenRouter,
singleplayer: singleplayerRouter,
team: teamRouter,
template: templateRouter,
+ webhook: webhookRouter,
twoFactorAuthentication: twoFactorAuthenticationRouter,
});
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/server/webhook-router/router.ts b/packages/trpc/server/webhook-router/router.ts
new file mode 100644
index 000000000..08b1b9bce
--- /dev/null
+++ b/packages/trpc/server/webhook-router/router.ts
@@ -0,0 +1,125 @@
+import { TRPCError } from '@trpc/server';
+
+import { createWebhook } from '@documenso/lib/server-only/webhooks/create-webhook';
+import { deleteWebhookById } from '@documenso/lib/server-only/webhooks/delete-webhook-by-id';
+import { editWebhook } from '@documenso/lib/server-only/webhooks/edit-webhook';
+import { getWebhookById } from '@documenso/lib/server-only/webhooks/get-webhook-by-id';
+import { getWebhooksByTeamId } from '@documenso/lib/server-only/webhooks/get-webhooks-by-team-id';
+import { getWebhooksByUserId } from '@documenso/lib/server-only/webhooks/get-webhooks-by-user-id';
+
+import { authenticatedProcedure, router } from '../trpc';
+import {
+ ZCreateWebhookMutationSchema,
+ ZDeleteWebhookMutationSchema,
+ ZEditWebhookMutationSchema,
+ ZGetTeamWebhooksQuerySchema,
+ ZGetWebhookByIdQuerySchema,
+} from './schema';
+
+export const webhookRouter = router({
+ getWebhooks: authenticatedProcedure.query(async ({ ctx }) => {
+ try {
+ return await getWebhooksByUserId(ctx.user.id);
+ } catch (err) {
+ throw new TRPCError({
+ code: 'BAD_REQUEST',
+ message: 'We were unable to fetch your webhooks. Please try again later.',
+ });
+ }
+ }),
+
+ getTeamWebhooks: authenticatedProcedure
+ .input(ZGetTeamWebhooksQuerySchema)
+ .query(async ({ ctx, input }) => {
+ const { teamId } = input;
+
+ try {
+ return await getWebhooksByTeamId(teamId, ctx.user.id);
+ } catch (err) {
+ throw new TRPCError({
+ code: 'BAD_REQUEST',
+ message: 'We were unable to fetch your webhooks. Please try again later.',
+ });
+ }
+ }),
+
+ getWebhookById: authenticatedProcedure
+ .input(ZGetWebhookByIdQuerySchema)
+ .query(async ({ input, ctx }) => {
+ try {
+ const { id, teamId } = input;
+
+ return await getWebhookById({
+ id,
+ userId: ctx.user.id,
+ teamId,
+ });
+ } catch (err) {
+ throw new TRPCError({
+ code: 'BAD_REQUEST',
+ message: 'We were unable to fetch your webhook. Please try again later.',
+ });
+ }
+ }),
+
+ createWebhook: authenticatedProcedure
+ .input(ZCreateWebhookMutationSchema)
+ .mutation(async ({ input, ctx }) => {
+ const { enabled, eventTriggers, secret, webhookUrl, teamId } = input;
+
+ try {
+ return await createWebhook({
+ enabled,
+ secret,
+ webhookUrl,
+ eventTriggers,
+ teamId,
+ userId: ctx.user.id,
+ });
+ } catch (err) {
+ throw new TRPCError({
+ code: 'BAD_REQUEST',
+ message: 'We were unable to create this webhook. Please try again later.',
+ });
+ }
+ }),
+
+ deleteWebhook: authenticatedProcedure
+ .input(ZDeleteWebhookMutationSchema)
+ .mutation(async ({ input, ctx }) => {
+ try {
+ const { id, teamId } = input;
+
+ return await deleteWebhookById({
+ id,
+ teamId,
+ userId: ctx.user.id,
+ });
+ } catch (err) {
+ throw new TRPCError({
+ code: 'BAD_REQUEST',
+ message: 'We were unable to create this webhook. Please try again later.',
+ });
+ }
+ }),
+
+ editWebhook: authenticatedProcedure
+ .input(ZEditWebhookMutationSchema)
+ .mutation(async ({ input, ctx }) => {
+ try {
+ const { id, teamId, ...data } = input;
+
+ return await editWebhook({
+ id,
+ data,
+ userId: ctx.user.id,
+ teamId,
+ });
+ } catch (err) {
+ throw new TRPCError({
+ code: 'BAD_REQUEST',
+ message: 'We were unable to create this webhook. Please try again later.',
+ });
+ }
+ }),
+});
diff --git a/packages/trpc/server/webhook-router/schema.ts b/packages/trpc/server/webhook-router/schema.ts
new file mode 100644
index 000000000..fe153ba1f
--- /dev/null
+++ b/packages/trpc/server/webhook-router/schema.ts
@@ -0,0 +1,41 @@
+import { z } from 'zod';
+
+import { WebhookTriggerEvents } from '@documenso/prisma/client';
+
+export const ZGetTeamWebhooksQuerySchema = z.object({
+ teamId: z.number(),
+});
+
+export type TGetTeamWebhooksQuerySchema = z.infer;
+
+export const ZCreateWebhookMutationSchema = z.object({
+ webhookUrl: z.string().url(),
+ eventTriggers: z
+ .array(z.nativeEnum(WebhookTriggerEvents))
+ .min(1, { message: 'At least one event trigger is required' }),
+ secret: z.string().nullable(),
+ enabled: z.boolean(),
+ teamId: z.number().optional(),
+});
+
+export type TCreateWebhookFormSchema = z.infer;
+
+export const ZGetWebhookByIdQuerySchema = z.object({
+ id: z.string(),
+ teamId: z.number().optional(),
+});
+
+export type TGetWebhookByIdQuerySchema = z.infer;
+
+export const ZEditWebhookMutationSchema = ZCreateWebhookMutationSchema.extend({
+ id: z.string(),
+});
+
+export type TEditWebhookMutationSchema = z.infer;
+
+export const ZDeleteWebhookMutationSchema = z.object({
+ id: z.string(),
+ teamId: z.number().optional(),
+});
+
+export type TDeleteWebhookMutationSchema = z.infer;
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/icons/verified.tsx b/packages/ui/icons/verified.tsx
new file mode 100644
index 000000000..5984e603d
--- /dev/null
+++ b/packages/ui/icons/verified.tsx
@@ -0,0 +1,31 @@
+import { forwardRef } from 'react';
+
+import type { LucideIcon } from 'lucide-react/dist/lucide-react';
+
+export const VerifiedIcon: LucideIcon = forwardRef(
+ ({ size = 24, color = 'currentColor', ...props }, ref) => {
+ return (
+
+
+
+
+
+ );
+ },
+);
+
+VerifiedIcon.displayName = 'VerifiedIcon';
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/badge.tsx b/packages/ui/primitives/badge.tsx
index 3569a4a7e..57418dab6 100644
--- a/packages/ui/primitives/badge.tsx
+++ b/packages/ui/primitives/badge.tsx
@@ -6,7 +6,7 @@ import { cva } from 'class-variance-authority';
import { cn } from '../lib/utils';
const badgeVariants = cva(
- 'inline-flex items-center rounded-md px-2 py-1.5 text-xs font-medium ring-1 ring-inset w-fit',
+ 'inline-flex items-center rounded-md text-xs font-medium ring-1 ring-inset w-fit',
{
variants: {
variant: {
@@ -21,9 +21,15 @@ const badgeVariants = cva(
secondary:
'bg-blue-50 text-blue-700 ring-blue-700/10 dark:bg-blue-400/10 dark:text-blue-400 dark:ring-blue-400/30',
},
+ size: {
+ small: 'px-1.5 py-0.5 text-xs',
+ default: 'px-2 py-1.5 text-xs',
+ large: 'px-3 py-2 text-sm',
+ },
},
defaultVariants: {
variant: 'default',
+ size: 'default',
},
},
);
@@ -32,8 +38,8 @@ export interface BadgeProps
extends React.HTMLAttributes,
VariantProps {}
-function Badge({ className, variant, ...props }: BadgeProps) {
- return
;
+function Badge({ className, variant, size, ...props }: BadgeProps) {
+ return
;
}
export { Badge, badgeVariants };
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 a098fb75c..f3e4c3544 100644
--- a/packages/ui/primitives/document-flow/add-fields.tsx
+++ b/packages/ui/primitives/document-flow/add-fields.tsx
@@ -326,9 +326,9 @@ export const AddFieldsFormPartial = ({
{selectedField && (
-
+
{FRIENDLY_FIELD_TYPE[selectedField]}
@@ -555,6 +555,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/input.tsx b/packages/ui/primitives/input.tsx
index 1a5fba1bb..71b3cb521 100644
--- a/packages/ui/primitives/input.tsx
+++ b/packages/ui/primitives/input.tsx
@@ -10,7 +10,7 @@ const Input = React.forwardRef(
= {
+ 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
+
+
+
+
+ )}
+ />
+
+