diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 59a318b7f..60b385403 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,20 +1,32 @@ { - "name": "Documenso", - "image": "mcr.microsoft.com/devcontainers/base:bullseye", - "features": { - "ghcr.io/devcontainers/features/docker-in-docker:2": { - "version": "latest", - "enableNonRootDocker": "true", - "moby": "true" - }, - "ghcr.io/devcontainers/features/node:1": {} - }, + "name": "Documenso", + "image": "mcr.microsoft.com/devcontainers/base:bullseye", + "features": { + "ghcr.io/devcontainers/features/docker-in-docker:2": { + "version": "latest", + "enableNonRootDocker": "true", + "moby": "true" + }, + "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": [ + "aaron-bond.better-comments", + "bradlc.vscode-tailwindcss", + "dbaeumer.vscode-eslint", + "esbenp.prettier-vscode", + "mikestead.dotenv", + "unifiedjs.vscode-mdx", + "GitHub.copilot-chat", + "GitHub.copilot-labs", + "GitHub.copilot", + "GitHub.vscode-pull-request-github", + "Prisma.prisma", + "VisualStudioExptTeam.vscodeintellicode", + ] + } + } } diff --git a/.env.example b/.env.example index 6f32b5a63..fb22bbedf 100644 --- a/.env.example +++ b/.env.example @@ -7,8 +7,8 @@ NEXT_PRIVATE_GOOGLE_CLIENT_ID="" NEXT_PRIVATE_GOOGLE_CLIENT_SECRET="" # [[APP]] -NEXT_PUBLIC_SITE_URL="http://localhost:3000" -NEXT_PUBLIC_APP_URL="http://localhost:3000" +NEXT_PUBLIC_WEBAPP_URL="http://localhost:3000" +NEXT_PUBLIC_MARKETING_URL="http://localhost:3001" # [[DATABASE]] NEXT_PRIVATE_DATABASE_URL="postgres://documenso:password@127.0.0.1:54320/documenso" diff --git a/.eslintignore b/.eslintignore index f80dc7f80..b7f7e638f 100644 --- a/.eslintignore +++ b/.eslintignore @@ -5,3 +5,4 @@ # Statically hosted javascript files apps/*/public/*.js apps/*/public/*.cjs +scripts/ diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c15689123..1a5d4bbcf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,12 +22,18 @@ jobs: uses: actions/checkout@v3 with: fetch-depth: 2 + - name: Install Node.js uses: actions/setup-node@v3 with: node-version: 18 cache: npm + - name: Install dependencies run: npm ci + + - name: Copy env + run: cp .env.example .env + - name: Build run: npm run build diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 92a0936a5..c934272a4 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -32,7 +32,10 @@ jobs: - name: Install Dependencies run: npm ci - + + - name: Copy env + run: cp .env.example .env + - name: Build Documenso run: npm run build @@ -42,4 +45,4 @@ jobs: languages: ${{ matrix.language }} - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 \ No newline at end of file + uses: github/codeql-action/analyze@v2 diff --git a/apps/marketing/content/blog/deploy-with-vercel-supabase-resend.mdx b/apps/marketing/content/blog/deploy-with-vercel-supabase-resend.mdx new file mode 100644 index 000000000..fb00f40bf --- /dev/null +++ b/apps/marketing/content/blog/deploy-with-vercel-supabase-resend.mdx @@ -0,0 +1,198 @@ +--- +title: 'Deploying Documenso with Vercel, Supabase and Resend' +description: This is the first part of the new Building Documenso series, where I describe the challenges and design choices that we make while building the world’s most open signing platform. +authorName: 'Ephraim Atta-Duncan' +authorImage: '/blog/blog-author-duncan.jpeg' +authorRole: 'Software Engineer Intern' +date: 2023-09-08 +tags: + - Open Source + - Self Hosting + - Tutorial +--- + +In this article, we'll walk you through how to deploy and self-host Documenso using Vercel, Supabase, and Resend. + +You'll learn: + +- How to set up a Postgres database using Supabase, +- How to install SMTP with Resend, +- How to deploy your project with Vercel. + +If you don't know what [Documenso](https://documenso.com/) is, it's an open-source alternative to DocuSign, with the mission to create an open signing infrastructure while embracing openness, cooperation, and transparency. + +## Prerequisites + +Before we start, make sure you have a [GitHub](https://github.com/signup) account. You also need [Node.js](https://nodejs.org/en) and [npm](https://www.npmjs.com/) installed on your local machine (note: you also have the option to host it on a cloud environment using Gitpod for example; that would be another post). If you need accounts on Vercel, Supabase, and Resend, create them by visiting the [Vercel](https://vercel.com/), [Supabase](https://supabase.com/), and [Resend](https://resend.com/) websites. + +Checklist: + +- [ ] Have a GitHub account +- [ ] Install Node.js +- [ ] Install npm +- [ ] Have a Vercel account +- [ ] Have a Supabase account +- [ ] Have a Resend account + +## Step-by-Step guide to deploying Documenso with Vercel, Supabase, and Resend + +To deploy Documenso, we'll take the following steps: + +1. Fork the Documenso repository +2. Clone the forked repository and install dependencies +3. Create a new project on Supabase +4. Copy the Supabase Postgres database connection URL +5. Create a `.env` file +6. Run the migration on the Supabase Postgres Database +7. Get your SMTP Keys on Resend +8. Create a new project on Vercel +9. Add Environment Variables to your Vercel project + +So, you're ready? Let’s dive in! + +### Step 1: Fork the Documenso repository + +Start by creating a fork of Documenso on GitHub. You can do this by visiting the [Documenso repository](https://github.com/documenso/documenso) and clicking on the 'Fork' button. (Also, star the repo!) + +![Documenso](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wkcujctpf86p56bju3mq.png) + +Choose your GitHub profile as the owner and click on 'Create fork' to create a fork of the repo. + +![Fork the Documenso repository on GitHub](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xf49r2byu9nnd1465niy.png) + +### Step 2: Clone the forked repository and install dependencies + +Clone the forked repository to your local machine in any directory of your choice. Open your terminal and enter the following commands: + +```bash +# Clone the repo using Github CLI +gh repo clone [your_github_username]/documenso + +# Clone the repo using Git +git clone +``` + +You can now navigate into the directory and install the project’s dependencies: + +```bash +cd documenso +npm install +``` + +### Step 3: Create a new project on Supabase + +Now, let's set up the database. + +If you haven't already, create a new project on Supabase. This will automatically create a new Postgres database for you. + +On your Supabase dashboard, click the '**New project**' button and choose your organization. + +On the '**Create a new project**' page, set a database name of **documenso** and a secure password for your database. Choose a region closer to you, a pricing plan, and click on '**Create new project**' to create your project. + +![Create a new project on Supabase](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/w5lqz771iupjyi1ekfdz.png) + +### Step 4: Copy the Supabase Postgres database connection URL + +In your project, click the '**Settings**' icon at the bottom left. + +Under the '**Project Settings**' section, click '**Database**' and scroll down to the '**Connection string**' section. Copy the '**URI**' and update it with the password you chose in the previous step. + +![Copy the Supabase Postgres database connection URL](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/y1ldu3qrg9moednbzjij.png) + +### Step 5: Create a `.env` file + +Create a `.env` file in the root of your project by copying the contents of the `.env.example` file. + +Add the connection string you copied from your Supabase dashboard to the `DATABASE_URL` variable in the `.env` file. + +The `.env` should look like this: + +```bash +DATABASE_URL="postgres://postgres:[YOUR-PASSWORD]@db.[YOUR-PROJECT-REF].supabase.co:5432/postgres" +``` + +### Step 6: Run the migration on the Supabase Postgres Database + +Run the migration on the Supabase Postgres Database using the following command: + +```bash +npx prisma migrate deploy +``` + +### Step 7: Get your SMTP Keys on Resend + +So, you've just cloned Documenso, installed dependencies on your local machine, and set your database using Supabase. Now, SMTP is missing. Emails won't go out! Let's fix it with Resend. + +In the **[Resend](https://resend.com/)** dashboard, click 'Add API Key' to create a key for Resend SMTP. + +![Create a key for Resend SMTP](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/uh2rztgn09mlvecl34i5.png) + +Next, add and verify your domain in the '**Domains**' section on the sidebar. This will allow you to send emails from any address associated with your domain. + +![Verify your domain on Resend](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nxgie0esz530vq5a494o.png) + +You can update your `.env` file with the following: + +```jsx +SMTP_MAIL_HOST = 'smtp.resend.com'; +SMTP_MAIL_PORT = '25'; +SMTP_MAIL_USER = 'resend'; +SMTP_MAIL_PASSWORD = 'YOUR_RESEND_API_KEY'; +MAIL_FROM = 'noreply@[YOUR_DOMAIN]'; +``` + +### Step 8: Create a new project on Vercel + +You set the database with Supabase and are SMTP-ready with Resend. Almost there! The next step is to deploy the project — we'll use Vercel for that. + +On your Vercel dashboard, create a new project using the forked project from your GitHub repositories. Select the project among the options and click '**Import**' to start running Documenso. + +![Create a new project on Vercel](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gdy97tltpnu7vf4fc11f.png) + +### Step 9: Add Environment Variables to your Vercel project + +In the '**Configure Project**' page, adding the required Environmental Variables is essential to ensure the application deploys without any errors. + +Specifically, for the `NEXT_PUBLIC_WEBAPP_URL` and `NEXTAUTH_URL` variables, you must add `.vercel.app` to your Project Name. This will form the deployment URL, which will be in the format: `https://[project_name].vercel.app`. + +For example, in my case, the deployment URL is `https://documenso-supabase-web.vercel.app`. + +![Add Environment Variables to your Vercel project](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/aru33fk1i19h0valffow.png) + +This is a sample `.env` to deploy. Copy and paste it to auto-populate the fields and click ‘**Deploy.’** Now, you only need to wait for your project to deploy. You’re going live — enjoy! + +```bash +DATABASE_URL='postgresql://postgres:typeinastrongpassword@db.njuigobjlbteahssqbtw.supabase.co:5432/postgres' + +NEXT_PUBLIC_WEBAPP_URL='https://documenso-supabase-web.vercel.app' +NEXTAUTH_SECRET='something gibrish to encrypt your jwt tokens' +NEXTAUTH_URL='https://documenso-supabase-web.vercel.app' + +# Get a Sendgrid Api key here: +SENDGRID_API_KEY='' + +# Set SMTP credentials to use SMTP instead of the Sendgrid API. +SMTP_MAIL_HOST='smtp.resend.com' +SMTP_MAIL_PORT='25' +SMTP_MAIL_USER='resend' +SMTP_MAIL_PASSWORD='YOUR_RESEND_API_KEY' +MAIL_FROM='noreply@[YOUR_DOMAIN]' + +NEXT_PUBLIC_ALLOW_SIGNUP=true +``` + +## Wrapping up + +![Deploying Documenso](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/secg29j9j40o4u2oa8o8.png) + +Congratulations! 🎉 You've successfully deployed Documenso using Vercel, Supabase, and Resend. You're now ready to create and sign your own documents with your self-hosted Documenso! + +In this step-by-step guide, you learned how to: + +- set up a Postgres database using Supabase, +- install SMTP with Resend, +- deploy your project with Vercel. + +Over to you! How was the tutorial? If you enjoyed it, [please do share](https://twitter.com/documenso/status/1700141802693480482)! And if you have any questions or comments, please reach out to me on [Twitter / X](https://twitter.com/EphraimDuncan_) (DM open) or [Discord](https://documen.so/discord). + +We're building an open-source alternative to DocuSign and welcome every contribution. Head over to the GitHub repository and [leave us a Star](https://github.com/documenso/documenso)! diff --git a/apps/marketing/next.config.js b/apps/marketing/next.config.js index 97f904cf0..2783e4063 100644 --- a/apps/marketing/next.config.js +++ b/apps/marketing/next.config.js @@ -18,6 +18,40 @@ const config = { transform: 'lucide-react/dist/esm/icons/{{ kebabCase member }}', }, }, + async headers() { + return [ + { + source: '/:path*', + headers: [ + { + key: 'x-dns-prefetch-control', + value: 'on', + }, + { + key: 'strict-transport-security', + value: 'max-age=31536000; includeSubDomains; preload', + }, + { + key: 'x-frame-options', + value: 'SAMEORIGIN', + }, + { + key: 'x-content-type-options', + value: 'nosniff', + }, + { + key: 'referrer-policy', + value: 'strict-origin-when-cross-origin', + }, + { + key: 'permissions-policy', + value: + 'accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()', + }, + ], + }, + ]; + }, }; module.exports = withContentlayer(config); diff --git a/apps/marketing/process-env.d.ts b/apps/marketing/process-env.d.ts index ac170a616..3dfdcb30f 100644 --- a/apps/marketing/process-env.d.ts +++ b/apps/marketing/process-env.d.ts @@ -1,6 +1,7 @@ declare namespace NodeJS { export interface ProcessEnv { - NEXT_PUBLIC_SITE_URL?: string; + NEXT_PUBLIC_WEBAPP_URL?: string; + NEXT_PUBLIC_MARKETING_URL?: string; NEXT_PRIVATE_DATABASE_URL: string; diff --git a/apps/marketing/public/blog/blog-author-duncan.jpeg b/apps/marketing/public/blog/blog-author-duncan.jpeg new file mode 100644 index 000000000..a4a52711c Binary files /dev/null and b/apps/marketing/public/blog/blog-author-duncan.jpeg differ diff --git a/apps/marketing/src/app/(marketing)/claimed/page.tsx b/apps/marketing/src/app/(marketing)/claimed/page.tsx index f56ae2b26..b1636e2be 100644 --- a/apps/marketing/src/app/(marketing)/claimed/page.tsx +++ b/apps/marketing/src/app/(marketing)/claimed/page.tsx @@ -161,7 +161,7 @@ export default async function ClaimedPlanPage({ searchParams = {} }: ClaimedPlan

diff --git a/apps/marketing/src/app/layout.tsx b/apps/marketing/src/app/layout.tsx index ea21ed3c3..46d9a3d32 100644 --- a/apps/marketing/src/app/layout.tsx +++ b/apps/marketing/src/app/layout.tsx @@ -21,12 +21,12 @@ export const metadata = { description: 'Join Documenso, the open signing infrastructure, and get a 10x better signing experience. Pricing starts at $30/mo. forever! Sign in now and enjoy a faster, smarter, and more beautiful document signing process. Integrates with your favorite tools, customizable, and expandable. Support our mission and become a part of our open-source community.', type: 'website', - images: [`${process.env.NEXT_PUBLIC_SITE_URL}/opengraph-image.jpg`], + images: [`${process.env.NEXT_PUBLIC_MARKETING_URL}/opengraph-image.jpg`], }, twitter: { site: '@documenso', card: 'summary_large_image', - images: [`${process.env.NEXT_PUBLIC_SITE_URL}/opengraph-image.jpg`], + images: [`${process.env.NEXT_PUBLIC_MARKETING_URL}/opengraph-image.jpg`], description: 'Join Documenso, the open signing infrastructure, and get a 10x better signing experience. Pricing starts at $30/mo. forever! Sign in now and enjoy a faster, smarter, and more beautiful document signing process. Integrates with your favorite tools, customizable, and expandable. Support our mission and become a part of our open-source community.', }, diff --git a/apps/marketing/src/app/not-found.tsx b/apps/marketing/src/app/not-found.tsx new file mode 100644 index 000000000..9f87cdc88 --- /dev/null +++ b/apps/marketing/src/app/not-found.tsx @@ -0,0 +1,65 @@ +'use client'; + +import Image from 'next/image'; +import Link from 'next/link'; +import { useRouter } from 'next/navigation'; + +import { motion } from 'framer-motion'; +import { ChevronLeft } from 'lucide-react'; + +import { cn } from '@documenso/ui/lib/utils'; +import { Button } from '@documenso/ui/primitives/button'; + +import backgroundPattern from '~/assets/background-pattern.png'; + +export default function NotFound() { + const router = useRouter(); + + return ( +
+
+ + background pattern + +
+ +
+
+

404 Page not found

+ +

Oops! Something went wrong.

+ +

+ The page you are looking for was moved, removed, renamed or might never have existed. +

+ +
+ + + +
+
+
+
+ ); +} diff --git a/apps/marketing/src/app/robots.ts b/apps/marketing/src/app/robots.ts index de74ea314..cc718ff25 100644 --- a/apps/marketing/src/app/robots.ts +++ b/apps/marketing/src/app/robots.ts @@ -4,11 +4,11 @@ import { getBaseUrl } from '@documenso/lib/universal/get-base-url'; export default function robots(): MetadataRoute.Robots { return { - rules: { - userAgent: '*', - allow: '/*', - disallow: ['/_next/*'], - }, + rules: [ + { + userAgent: '*', + }, + ], sitemap: `${getBaseUrl()}/sitemap.xml`, }; } diff --git a/apps/marketing/src/pages/api/claim-plan/index.ts b/apps/marketing/src/pages/api/claim-plan/index.ts index abad354a8..3d2d8679b 100644 --- a/apps/marketing/src/pages/api/claim-plan/index.ts +++ b/apps/marketing/src/pages/api/claim-plan/index.ts @@ -43,7 +43,7 @@ export default async function handler( if (user && user.Subscription.length > 0) { return res.status(200).json({ - redirectUrl: `${process.env.NEXT_PUBLIC_APP_URL}/login`, + redirectUrl: `${process.env.NEXT_PUBLIC_WEBAPP_URL}/login`, }); } @@ -103,8 +103,8 @@ export default async function handler( mode: 'subscription', metadata, allow_promotion_codes: true, - success_url: `${process.env.NEXT_PUBLIC_SITE_URL}/claimed?sessionId={CHECKOUT_SESSION_ID}`, - cancel_url: `${process.env.NEXT_PUBLIC_SITE_URL}/pricing?email=${encodeURIComponent( + success_url: `${process.env.NEXT_PUBLIC_MARKETING_URL}/claimed?sessionId={CHECKOUT_SESSION_ID}`, + cancel_url: `${process.env.NEXT_PUBLIC_MARKETING_URL}/pricing?email=${encodeURIComponent( email, )}&name=${encodeURIComponent(name)}&planId=${planId}&cancelled=true`, }); diff --git a/apps/web/process-env.d.ts b/apps/web/process-env.d.ts index 1cb0018ac..4149423dd 100644 --- a/apps/web/process-env.d.ts +++ b/apps/web/process-env.d.ts @@ -1,6 +1,7 @@ declare namespace NodeJS { export interface ProcessEnv { - NEXT_PUBLIC_SITE_URL?: string; + NEXT_PUBLIC_WEBAPP_URL?: string; + NEXT_PUBLIC_MARKETING_URL?: string; NEXT_PRIVATE_DATABASE_URL: string; diff --git a/apps/web/src/app/(dashboard)/documents/empty-state.tsx b/apps/web/src/app/(dashboard)/documents/empty-state.tsx new file mode 100644 index 000000000..bdfef3bbe --- /dev/null +++ b/apps/web/src/app/(dashboard)/documents/empty-state.tsx @@ -0,0 +1,50 @@ +import { Bird, CheckCircle2 } from 'lucide-react'; +import { match } from 'ts-pattern'; + +import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status'; + +export type EmptyDocumentProps = { status: ExtendedDocumentStatus }; + +export const EmptyDocumentState = ({ status }: EmptyDocumentProps) => { + const { + title, + message, + icon: Icon, + } = match(status) + .with(ExtendedDocumentStatus.COMPLETED, () => ({ + title: 'Nothing to do', + message: + 'There are no completed documents yet. Documents that you have created or received that become completed will appear here later.', + icon: CheckCircle2, + })) + .with(ExtendedDocumentStatus.DRAFT, () => ({ + title: 'No active drafts', + message: + 'There are no active drafts at then current moment. You can upload a document to start drafting.', + icon: CheckCircle2, + })) + .with(ExtendedDocumentStatus.ALL, () => ({ + title: "We're all empty", + message: + 'You have not yet created or received any documents. To create a document please upload one.', + icon: Bird, + })) + .otherwise(() => ({ + title: 'Nothing to do', + message: + 'All documents are currently actioned. Any new documents are sent or recieved they will start to appear here.', + icon: CheckCircle2, + })); + + return ( +
+ + +
+

{title}

+ +

{message}

+
+
+ ); +}; diff --git a/apps/web/src/app/(dashboard)/documents/page.tsx b/apps/web/src/app/(dashboard)/documents/page.tsx index 32ec4f814..9ae16e44b 100644 --- a/apps/web/src/app/(dashboard)/documents/page.tsx +++ b/apps/web/src/app/(dashboard)/documents/page.tsx @@ -12,6 +12,7 @@ import { PeriodSelectorValue } from '~/components/(dashboard)/period-selector/ty import { DocumentStatus } from '~/components/formatter/document-status'; import { DocumentsDataTable } from './data-table'; +import { EmptyDocumentState } from './empty-state'; import { UploadDocument } from './upload-document'; export type DocumentsPageProps = { @@ -96,7 +97,8 @@ export default async function DocumentsPage({ searchParams = {} }: DocumentsPage
- + {results.count > 0 && } + {results.count === 0 && }
); diff --git a/apps/web/src/app/(dashboard)/documents/upload-document.tsx b/apps/web/src/app/(dashboard)/documents/upload-document.tsx index b472c606b..12df84475 100644 --- a/apps/web/src/app/(dashboard)/documents/upload-document.tsx +++ b/apps/web/src/app/(dashboard)/documents/upload-document.tsx @@ -68,7 +68,7 @@ export const UploadDocument = ({ className }: UploadDocumentProps) => { {isLoading && (
- +
)} diff --git a/apps/web/src/app/(dashboard)/settings/billing/page.tsx b/apps/web/src/app/(dashboard)/settings/billing/page.tsx index 555c645ce..7e6694491 100644 --- a/apps/web/src/app/(dashboard)/settings/billing/page.tsx +++ b/apps/web/src/app/(dashboard)/settings/billing/page.tsx @@ -35,7 +35,7 @@ export default async function BillingSettingsPage() { if (subscription.customerId) { billingPortalUrl = await getPortalSession({ customerId: subscription.customerId, - returnUrl: `${process.env.NEXT_PUBLIC_SITE_URL}/settings/billing`, + returnUrl: `${process.env.NEXT_PUBLIC_WEBAPP_URL}/settings/billing`, }); } @@ -43,7 +43,7 @@ export default async function BillingSettingsPage() {

Billing

-

+

Your subscription is{' '} {subscription.status !== SubscriptionStatus.INACTIVE ? 'active' : 'inactive'}. {subscription?.periodEnd && ( @@ -67,7 +67,7 @@ export default async function BillingSettingsPage() { )} {!billingPortalUrl && ( -

+

You do not currently have a customer record, this should not happen. Please contact support for assistance.

diff --git a/apps/web/src/app/(dashboard)/settings/password/page.tsx b/apps/web/src/app/(dashboard)/settings/password/page.tsx index 305cc3d0c..b89e74f3c 100644 --- a/apps/web/src/app/(dashboard)/settings/password/page.tsx +++ b/apps/web/src/app/(dashboard)/settings/password/page.tsx @@ -9,7 +9,7 @@ export default async function PasswordSettingsPage() {

Password

-

Here you can update your password.

+

Here you can update your password.


diff --git a/apps/web/src/app/(dashboard)/settings/profile/page.tsx b/apps/web/src/app/(dashboard)/settings/profile/page.tsx index ba461e871..ee0087a9a 100644 --- a/apps/web/src/app/(dashboard)/settings/profile/page.tsx +++ b/apps/web/src/app/(dashboard)/settings/profile/page.tsx @@ -9,7 +9,7 @@ export default async function ProfileSettingsPage() {

Profile

-

Here you can edit your personal details.

+

Here you can edit your personal details.


diff --git a/apps/web/src/app/(unauthenticated)/check-email/page.tsx b/apps/web/src/app/(unauthenticated)/check-email/page.tsx new file mode 100644 index 000000000..fffbc44c1 --- /dev/null +++ b/apps/web/src/app/(unauthenticated)/check-email/page.tsx @@ -0,0 +1,20 @@ +import Link from 'next/link'; + +import { Button } from '@documenso/ui/primitives/button'; + +export default function ForgotPasswordPage() { + return ( +
+

Email sent!

+ +

+ A password reset email has been sent, if you have an account you should see it in your inbox + shortly. +

+ + +
+ ); +} diff --git a/apps/web/src/app/(unauthenticated)/forgot-password/page.tsx b/apps/web/src/app/(unauthenticated)/forgot-password/page.tsx new file mode 100644 index 000000000..4f0617f7c --- /dev/null +++ b/apps/web/src/app/(unauthenticated)/forgot-password/page.tsx @@ -0,0 +1,25 @@ +import Link from 'next/link'; + +import { ForgotPasswordForm } from '~/components/forms/forgot-password'; + +export default function ForgotPasswordPage() { + return ( +
+

Forgotten 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 + +

+
+ ); +} diff --git a/apps/web/src/app/(unauthenticated)/layout.tsx b/apps/web/src/app/(unauthenticated)/layout.tsx new file mode 100644 index 000000000..c88b9fb2e --- /dev/null +++ b/apps/web/src/app/(unauthenticated)/layout.tsx @@ -0,0 +1,27 @@ +import React from 'react'; + +import Image from 'next/image'; + +import backgroundPattern from '~/assets/background-pattern.png'; + +type UnauthenticatedLayoutProps = { + children: React.ReactNode; +}; + +export default function UnauthenticatedLayout({ children }: UnauthenticatedLayoutProps) { + return ( +
+
+
+ background pattern +
+ +
{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 new file mode 100644 index 000000000..04afd2c4d --- /dev/null +++ b/apps/web/src/app/(unauthenticated)/reset-password/[token]/page.tsx @@ -0,0 +1,37 @@ +import Link from 'next/link'; +import { redirect } from 'next/navigation'; + +import { getResetTokenValidity } from '@documenso/lib/server-only/user/get-reset-token-validity'; + +import { ResetPasswordForm } from '~/components/forms/reset-password'; + +type ResetPasswordPageProps = { + params: { + token: string; + }; +}; + +export default async function ResetPasswordPage({ params: { token } }: ResetPasswordPageProps) { + const isValid = await getResetTokenValidity({ token }); + + if (!isValid) { + redirect('/reset-password'); + } + + return ( +
+

Reset Password

+ +

Please choose your new password

+ + + +

+ 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 new file mode 100644 index 000000000..c4f521363 --- /dev/null +++ b/apps/web/src/app/(unauthenticated)/reset-password/page.tsx @@ -0,0 +1,20 @@ +import Link from 'next/link'; + +import { Button } from '@documenso/ui/primitives/button'; + +export default function ResetPasswordPage() { + return ( +
+

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. +

+ + +
+ ); +} diff --git a/apps/web/src/app/(unauthenticated)/signin/page.tsx b/apps/web/src/app/(unauthenticated)/signin/page.tsx index 800ff9b9a..868b0471d 100644 --- a/apps/web/src/app/(unauthenticated)/signin/page.tsx +++ b/apps/web/src/app/(unauthenticated)/signin/page.tsx @@ -1,43 +1,33 @@ -import Image from 'next/image'; import Link from 'next/link'; -import backgroundPattern from '~/assets/background-pattern.png'; -import connections from '~/assets/card-sharing-figure.png'; import { SignInForm } from '~/components/forms/signin'; export default function SignInPage() { return ( -
-
-
- background pattern -
+
+

Sign in to your account

-
-

Sign in to your account

+

+ Welcome back, we are lucky to have you. +

-

- Welcome back, we are lucky to have you. -

+ - +

+ Don't have an account?{' '} + + Sign up + +

-

- Don't have an account?{' '} - - Sign up - -

-
- -
- documenso connections -
-
-
+

+ + Forgotten your password? + +

+
); } diff --git a/apps/web/src/app/(unauthenticated)/signup/page.tsx b/apps/web/src/app/(unauthenticated)/signup/page.tsx index b398e6990..0d82e5c4f 100644 --- a/apps/web/src/app/(unauthenticated)/signup/page.tsx +++ b/apps/web/src/app/(unauthenticated)/signup/page.tsx @@ -1,44 +1,25 @@ -import Image from 'next/image'; import Link from 'next/link'; -import backgroundPattern from '~/assets/background-pattern.png'; -import connections from '~/assets/connections.png'; import { SignUpForm } from '~/components/forms/signup'; export default function SignUpPage() { return ( -
-
-
- background pattern -
+
+

Create a new account

-
-

Create a shiny, new Documenso Account ✨

+

+ Create your account and start using state-of-the-art document signing. Open and beautiful + signing is within your grasp. +

-

- 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 - -

-
- -
- documenso connections -
-
-
+

+ Already have an account?{' '} + + Sign in instead + +

+
); } diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx index 2ce8744d4..2a1d082f9 100644 --- a/apps/web/src/app/layout.tsx +++ b/apps/web/src/app/layout.tsx @@ -33,12 +33,12 @@ export const metadata = { description: 'Join Documenso, the open signing infrastructure, and get a 10x better signing experience. Pricing starts at $30/mo. forever! Sign in now and enjoy a faster, smarter, and more beautiful document signing process. Integrates with your favorite tools, customizable, and expandable. Support our mission and become a part of our open-source community.', type: 'website', - images: [`${process.env.NEXT_PUBLIC_SITE_URL}/opengraph-image.jpg`], + images: [`${process.env.NEXT_PUBLIC_WEBAPP_URL}/opengraph-image.jpg`], }, twitter: { site: '@documenso', card: 'summary_large_image', - images: [`${process.env.NEXT_PUBLIC_SITE_URL}/opengraph-image.jpg`], + images: [`${process.env.NEXT_PUBLIC_WEBAPP_URL}/opengraph-image.jpg`], description: 'Join Documenso, the open signing infrastructure, and get a 10x better signing experience. Pricing starts at $30/mo. forever! Sign in now and enjoy a faster, smarter, and more beautiful document signing process. Integrates with your favorite tools, customizable, and expandable. Support our mission and become a part of our open-source community.', }, diff --git a/apps/web/src/app/not-found.tsx b/apps/web/src/app/not-found.tsx new file mode 100644 index 000000000..c8dc15086 --- /dev/null +++ b/apps/web/src/app/not-found.tsx @@ -0,0 +1,26 @@ +import Link from 'next/link'; + +import { getServerComponentSession } from '@documenso/lib/next-auth/get-server-session'; +import { Button } from '@documenso/ui/primitives/button'; + +import NotFoundPartial from '~/components/partials/not-found'; + +export default async function NotFound() { + const session = await getServerComponentSession(); + + return ( + + {session && ( + + )} + + {!session && ( + + )} + + ); +} diff --git a/apps/web/src/components/(dashboard)/layout/profile-dropdown.tsx b/apps/web/src/components/(dashboard)/layout/profile-dropdown.tsx index 9ae9b4297..91b045feb 100644 --- a/apps/web/src/components/(dashboard)/layout/profile-dropdown.tsx +++ b/apps/web/src/components/(dashboard)/layout/profile-dropdown.tsx @@ -10,6 +10,7 @@ import { User as LucideUser, Monitor, Moon, + Palette, Sun, UserCog, } from 'lucide-react'; @@ -26,7 +27,13 @@ import { DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, + DropdownMenuPortal, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, DropdownMenuSeparator, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, DropdownMenuTrigger, } from '@documenso/ui/primitives/dropdown-menu'; @@ -37,8 +44,8 @@ export type ProfileDropdownProps = { }; export const ProfileDropdown = ({ user }: ProfileDropdownProps) => { - const { theme, setTheme } = useTheme(); const { getFlag } = useFeatureFlags(); + const { theme, setTheme } = useTheme(); const isUserAdmin = isAdmin(user); const isBillingEnabled = getFlag('app_billing'); @@ -98,28 +105,30 @@ export const ProfileDropdown = ({ user }: ProfileDropdownProps) => { - {theme === 'light' ? null : ( - setTheme('light')}> - - Light Mode - - )} - {theme === 'dark' ? null : ( - setTheme('dark')}> - - Dark Mode - - )} - - {theme === 'system' ? null : ( - setTheme('system')}> - - System Theme - - )} - + + + + Themes + + + + + + Light + + + + Dark + + + + System + + + + + - diff --git a/apps/web/src/components/(dashboard)/period-selector/period-selector.tsx b/apps/web/src/components/(dashboard)/period-selector/period-selector.tsx index afabefc3f..caeb780d0 100644 --- a/apps/web/src/components/(dashboard)/period-selector/period-selector.tsx +++ b/apps/web/src/components/(dashboard)/period-selector/period-selector.tsx @@ -44,7 +44,7 @@ export const PeriodSelector = () => { return ( + + +
+ + + + ); +}; diff --git a/apps/web/src/components/forms/password.tsx b/apps/web/src/components/forms/password.tsx index 508579b78..8b6a58a06 100644 --- a/apps/web/src/components/forms/password.tsx +++ b/apps/web/src/components/forms/password.tsx @@ -1,7 +1,9 @@ 'use client'; +import { useState } from 'react'; + import { zodResolver } from '@hookform/resolvers/zod'; -import { Loader } from 'lucide-react'; +import { Eye, EyeOff, Loader } from 'lucide-react'; import { useForm } from 'react-hook-form'; import { z } from 'zod'; @@ -36,6 +38,9 @@ export type PasswordFormProps = { export const PasswordForm = ({ className }: PasswordFormProps) => { const { toast } = useToast(); + const [showPassword, setShowPassword] = useState(false); + const [showConfirmPassword, setShowConfirmPassword] = useState(false); + const { register, handleSubmit, @@ -88,37 +93,69 @@ export const PasswordForm = ({ className }: PasswordFormProps) => { onSubmit={handleSubmit(onFormSubmit)} >
-
-
diff --git a/apps/web/src/components/forms/profile.tsx b/apps/web/src/components/forms/profile.tsx index 8255742c9..0082147b4 100644 --- a/apps/web/src/components/forms/profile.tsx +++ b/apps/web/src/components/forms/profile.tsx @@ -89,7 +89,7 @@ export const ProfileForm = ({ className, user }: ProfileFormProps) => { onSubmit={handleSubmit(onFormSubmit)} >
-
-
-
diff --git a/apps/web/src/components/motion.tsx b/apps/web/src/components/motion.tsx deleted file mode 100644 index 2e9d19eae..000000000 --- a/apps/web/src/components/motion.tsx +++ /dev/null @@ -1,7 +0,0 @@ -'use client'; - -import { motion } from 'framer-motion'; - -export * from 'framer-motion'; - -export const MotionDiv = motion.div; diff --git a/apps/web/src/components/partials/not-found.tsx b/apps/web/src/components/partials/not-found.tsx new file mode 100644 index 000000000..0b5c2ad18 --- /dev/null +++ b/apps/web/src/components/partials/not-found.tsx @@ -0,0 +1,66 @@ +'use client'; + +import Image from 'next/image'; +import { useRouter } from 'next/navigation'; + +import { motion } from 'framer-motion'; +import { ChevronLeft } from 'lucide-react'; + +import { cn } from '@documenso/ui/lib/utils'; +import { Button } from '@documenso/ui/primitives/button'; + +import backgroundPattern from '~/assets/background-pattern.png'; + +export type NotFoundPartialProps = { + children?: React.ReactNode; +}; + +export default function NotFoundPartial({ children }: NotFoundPartialProps) { + const router = useRouter(); + + return ( +
+
+ + background pattern + +
+ +
+
+

404 Page not found

+ +

Oops! Something went wrong.

+ +

+ The page you are looking for was moved, removed, renamed or might never have existed. +

+ +
+ + + {children} +
+
+
+
+ ); +} diff --git a/apps/web/src/helpers/get-feature-flag.ts b/apps/web/src/helpers/get-feature-flag.ts index 3b6c66528..d5cd26c33 100644 --- a/apps/web/src/helpers/get-feature-flag.ts +++ b/apps/web/src/helpers/get-feature-flag.ts @@ -21,7 +21,7 @@ export const getFlag = async ( return LOCAL_FEATURE_FLAGS[flag] ?? true; } - const url = new URL(`${process.env.NEXT_PUBLIC_SITE_URL}/api/feature-flag/get`); + const url = new URL(`${process.env.NEXT_PUBLIC_WEBAPP_URL}/api/feature-flag/get`); url.searchParams.set('flag', flag); const response = await fetch(url, { @@ -54,7 +54,7 @@ export const getAllFlags = async ( return LOCAL_FEATURE_FLAGS; } - const url = new URL(`${process.env.NEXT_PUBLIC_SITE_URL}/api/feature-flag/all`); + const url = new URL(`${process.env.NEXT_PUBLIC_WEBAPP_URL}/api/feature-flag/all`); return fetch(url, { headers: { diff --git a/apps/web/src/pages/api/claim-plan/index.ts b/apps/web/src/pages/api/claim-plan/index.ts index abad354a8..3d2d8679b 100644 --- a/apps/web/src/pages/api/claim-plan/index.ts +++ b/apps/web/src/pages/api/claim-plan/index.ts @@ -43,7 +43,7 @@ export default async function handler( if (user && user.Subscription.length > 0) { return res.status(200).json({ - redirectUrl: `${process.env.NEXT_PUBLIC_APP_URL}/login`, + redirectUrl: `${process.env.NEXT_PUBLIC_WEBAPP_URL}/login`, }); } @@ -103,8 +103,8 @@ export default async function handler( mode: 'subscription', metadata, allow_promotion_codes: true, - success_url: `${process.env.NEXT_PUBLIC_SITE_URL}/claimed?sessionId={CHECKOUT_SESSION_ID}`, - cancel_url: `${process.env.NEXT_PUBLIC_SITE_URL}/pricing?email=${encodeURIComponent( + success_url: `${process.env.NEXT_PUBLIC_MARKETING_URL}/claimed?sessionId={CHECKOUT_SESSION_ID}`, + cancel_url: `${process.env.NEXT_PUBLIC_MARKETING_URL}/pricing?email=${encodeURIComponent( email, )}&name=${encodeURIComponent(name)}&planId=${planId}&cancelled=true`, }); diff --git a/assets/example.pdf b/assets/example.pdf new file mode 100644 index 000000000..f908d84e1 Binary files /dev/null and b/assets/example.pdf differ diff --git a/package-lock.json b/package-lock.json index 348a046de..c6558b466 100644 --- a/package-lock.json +++ b/package-lock.json @@ -905,7 +905,6 @@ "version": "7.12.11", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz", "integrity": "sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==", - "dev": true, "dependencies": { "@babel/highlight": "^7.10.4" } @@ -2589,6 +2588,145 @@ "node": ">=12" } }, + "node_modules/@manypkg/find-root": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@manypkg/find-root/-/find-root-2.1.0.tgz", + "integrity": "sha512-NEYRVlZCJYhRTqQURhv+WBpDcvmsp/M423Wcdvggv8lYJYD4GtqnTMLrQaTjA10fYt/PIc3tSdwV+wxJnWqPfQ==", + "dependencies": { + "@manypkg/tools": "^1.0.0", + "@types/node": "^12.7.1", + "find-up": "^4.1.0", + "fs-extra": "^8.1.0" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/@manypkg/find-root/node_modules/@types/node": { + "version": "12.20.55", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz", + "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==" + }, + "node_modules/@manypkg/find-root/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@manypkg/find-root/node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/@manypkg/find-root/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@manypkg/find-root/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@manypkg/find-root/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@manypkg/find-root/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@manypkg/find-root/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/@manypkg/tools": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@manypkg/tools/-/tools-1.1.0.tgz", + "integrity": "sha512-SkAyKAByB9l93Slyg8AUHGuM2kjvWioUTCckT/03J09jYnfEzMO/wSXmEhnKGYs6qx9De8TH4yJCl0Y9lRgnyQ==", + "dependencies": { + "fs-extra": "^8.1.0", + "globby": "^11.0.0", + "jju": "^1.4.0", + "read-yaml-file": "^1.1.0" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/@manypkg/tools/node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/@manypkg/tools/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@manypkg/tools/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "engines": { + "node": ">= 4.0.0" + } + }, "node_modules/@mapbox/node-pre-gyp": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", @@ -2859,6 +2997,163 @@ "node": ">= 8" } }, + "node_modules/@octokit/auth-token": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-3.0.4.tgz", + "integrity": "sha512-TWFX7cZF2LXoCvdmJWY7XVPi74aSY0+FfBZNSXEXFkMpjcqsQwDSYVv5FhRFaI0V1ECnwbz4j59T/G+rXNWaIQ==", + "engines": { + "node": ">= 14" + } + }, + "node_modules/@octokit/core": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-4.2.4.tgz", + "integrity": "sha512-rYKilwgzQ7/imScn3M9/pFfUf4I1AZEH3KhyJmtPdE2zfaXAn2mFfUy4FbKewzc2We5y/LlKLj36fWJLKC2SIQ==", + "dependencies": { + "@octokit/auth-token": "^3.0.0", + "@octokit/graphql": "^5.0.0", + "@octokit/request": "^6.0.0", + "@octokit/request-error": "^3.0.0", + "@octokit/types": "^9.0.0", + "before-after-hook": "^2.2.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@octokit/endpoint": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-7.0.6.tgz", + "integrity": "sha512-5L4fseVRUsDFGR00tMWD/Trdeeihn999rTMGRMC1G/Ldi1uWlWJzI98H4Iak5DB/RVvQuyMYKqSK/R6mbSOQyg==", + "dependencies": { + "@octokit/types": "^9.0.0", + "is-plain-object": "^5.0.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@octokit/graphql": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-5.0.6.tgz", + "integrity": "sha512-Fxyxdy/JH0MnIB5h+UQ3yCoh1FG4kWXfFKkpWqjZHw/p+Kc8Y44Hu/kCgNBT6nU1shNumEchmW/sUO1JuQnPcw==", + "dependencies": { + "@octokit/request": "^6.0.0", + "@octokit/types": "^9.0.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@octokit/openapi-types": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-18.0.0.tgz", + "integrity": "sha512-V8GImKs3TeQRxRtXFpG2wl19V7444NIOTDF24AWuIbmNaNYOQMWRbjcGDXV5B+0n887fgDcuMNOmlul+k+oJtw==" + }, + "node_modules/@octokit/plugin-paginate-rest": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-6.1.2.tgz", + "integrity": "sha512-qhrmtQeHU/IivxucOV1bbI/xZyC/iOBhclokv7Sut5vnejAIAEXVcGQeRpQlU39E0WwK9lNvJHphHri/DB6lbQ==", + "dependencies": { + "@octokit/tsconfig": "^1.0.2", + "@octokit/types": "^9.2.3" + }, + "engines": { + "node": ">= 14" + }, + "peerDependencies": { + "@octokit/core": ">=4" + } + }, + "node_modules/@octokit/plugin-request-log": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-1.0.4.tgz", + "integrity": "sha512-mLUsMkgP7K/cnFEw07kWqXGF5LKrOkD+lhCrKvPHXWDywAwuDUeDwWBpc69XK3pNX0uKiVt8g5z96PJ6z9xCFA==", + "peerDependencies": { + "@octokit/core": ">=3" + } + }, + "node_modules/@octokit/plugin-rest-endpoint-methods": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-7.2.3.tgz", + "integrity": "sha512-I5Gml6kTAkzVlN7KCtjOM+Ruwe/rQppp0QU372K1GP7kNOYEKe8Xn5BW4sE62JAHdwpq95OQK/qGNyKQMUzVgA==", + "dependencies": { + "@octokit/types": "^10.0.0" + }, + "engines": { + "node": ">= 14" + }, + "peerDependencies": { + "@octokit/core": ">=3" + } + }, + "node_modules/@octokit/plugin-rest-endpoint-methods/node_modules/@octokit/types": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-10.0.0.tgz", + "integrity": "sha512-Vm8IddVmhCgU1fxC1eyinpwqzXPEYu0NrYzD3YZjlGjyftdLBTeqNblRC0jmJmgxbJIsQlyogVeGnrNaaMVzIg==", + "dependencies": { + "@octokit/openapi-types": "^18.0.0" + } + }, + "node_modules/@octokit/request": { + "version": "6.2.8", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-6.2.8.tgz", + "integrity": "sha512-ow4+pkVQ+6XVVsekSYBzJC0VTVvh/FCTUUgTsboGq+DTeWdyIFV8WSCdo0RIxk6wSkBTHqIK1mYuY7nOBXOchw==", + "dependencies": { + "@octokit/endpoint": "^7.0.0", + "@octokit/request-error": "^3.0.0", + "@octokit/types": "^9.0.0", + "is-plain-object": "^5.0.0", + "node-fetch": "^2.6.7", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@octokit/request-error": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-3.0.3.tgz", + "integrity": "sha512-crqw3V5Iy2uOU5Np+8M/YexTlT8zxCfI+qu+LxUB7SZpje4Qmx3mub5DfEKSO8Ylyk0aogi6TYdf6kxzh2BguQ==", + "dependencies": { + "@octokit/types": "^9.0.0", + "deprecation": "^2.0.0", + "once": "^1.4.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@octokit/rest": { + "version": "19.0.7", + "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-19.0.7.tgz", + "integrity": "sha512-HRtSfjrWmWVNp2uAkEpQnuGMJsu/+dBr47dRc5QVgsCbnIc1+GFEaoKBWkYG+zjrsHpSqcAElMio+n10c0b5JA==", + "dependencies": { + "@octokit/core": "^4.1.0", + "@octokit/plugin-paginate-rest": "^6.0.0", + "@octokit/plugin-request-log": "^1.0.4", + "@octokit/plugin-rest-endpoint-methods": "^7.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@octokit/tsconfig": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@octokit/tsconfig/-/tsconfig-1.0.2.tgz", + "integrity": "sha512-I0vDR0rdtP8p2lGMzvsJzbhdOWy405HcGovrspJ8RRibHnyRgggUSNO5AIox5LmqiwmatHKYsvj6VGFHkqS7lA==" + }, + "node_modules/@octokit/types": { + "version": "9.3.2", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-9.3.2.tgz", + "integrity": "sha512-D4iHGTdAnEEVsB8fl95m1hiz7D5YiRdQ9b/OEb3BYRVwbLsGHcRVPz+u+BgRLNk0Q0/4iZCBqDN96j2XNxfXrA==", + "dependencies": { + "@octokit/openapi-types": "^18.0.0" + } + }, "node_modules/@one-ini/wasm": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/@one-ini/wasm/-/wasm-0.1.1.tgz", @@ -3371,12 +3666,12 @@ } }, "node_modules/@prisma/client": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.0.0.tgz", - "integrity": "sha512-XlO5ELNAQ7rV4cXIDJUNBEgdLwX3pjtt9Q/RHqDpGf43szpNJx2hJnggfFs7TKNx0cOFsl6KJCSfqr5duEU/bQ==", + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.3.1.tgz", + "integrity": "sha512-ArOKjHwdFZIe1cGU56oIfy7wRuTn0FfZjGuU/AjgEBOQh+4rDkB6nF+AGHP8KaVpkBIiHGPQh3IpwQ3xDMdO0Q==", "hasInstallScript": true, "dependencies": { - "@prisma/engines-version": "4.17.0-26.6b0aef69b7cdfc787f822ecd7cdc76d5f1991584" + "@prisma/engines-version": "5.3.1-2.61e140623197a131c2a6189271ffee05a7aa9a59" }, "engines": { "node": ">=16.13" @@ -3391,15 +3686,15 @@ } }, "node_modules/@prisma/engines": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.0.0.tgz", - "integrity": "sha512-kyT/8fd0OpWmhAU5YnY7eP31brW1q1YrTGoblWrhQJDiN/1K+Z8S1kylcmtjqx5wsUGcP1HBWutayA/jtyt+sg==", + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.3.1.tgz", + "integrity": "sha512-6QkILNyfeeN67BNEPEtkgh3Xo2tm6D7V+UhrkBbRHqKw9CTaz/vvTP/ROwYSP/3JT2MtIutZm/EnhxUiuOPVDA==", "hasInstallScript": true }, "node_modules/@prisma/engines-version": { - "version": "4.17.0-26.6b0aef69b7cdfc787f822ecd7cdc76d5f1991584", - "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-4.17.0-26.6b0aef69b7cdfc787f822ecd7cdc76d5f1991584.tgz", - "integrity": "sha512-HHiUF6NixsldsP3JROq07TYBLEjXFKr6PdH8H4gK/XAoTmIplOJBCgrIUMrsRAnEuGyRoRLXKXWUb943+PFoKQ==" + "version": "5.3.1-2.61e140623197a131c2a6189271ffee05a7aa9a59", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.3.1-2.61e140623197a131c2a6189271ffee05a7aa9a59.tgz", + "integrity": "sha512-y5qbUi3ql2Xg7XraqcXEdMHh0MocBfnBzDn5GbV1xk23S3Mq8MGs+VjacTNiBh3dtEdUERCrUUG7Z3QaJ+h79w==" }, "node_modules/@protobufjs/aspromise": { "version": "1.1.2", @@ -6023,8 +6318,7 @@ "node_modules/@types/normalize-package-data": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz", - "integrity": "sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==", - "dev": true + "integrity": "sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==" }, "node_modules/@types/parse5": { "version": "6.0.3", @@ -6802,6 +7096,11 @@ "node": ">= 10.0.0" } }, + "node_modules/before-after-hook": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", + "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==" + }, "node_modules/big-integer": { "version": "1.6.51", "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz", @@ -6810,6 +7109,18 @@ "node": ">=0.6" } }, + "node_modules/binary": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz", + "integrity": "sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg==", + "dependencies": { + "buffers": "~0.1.1", + "chainsaw": "~0.1.0" + }, + "engines": { + "node": "*" + } + }, "node_modules/binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -6828,6 +7139,11 @@ "readable-stream": "^3.4.0" } }, + "node_modules/bluebird": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", + "integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==" + }, "node_modules/bowser": { "version": "2.11.0", "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.11.0.tgz", @@ -6923,6 +7239,22 @@ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" }, + "node_modules/buffer-indexof-polyfill": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.2.tgz", + "integrity": "sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A==", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/buffers": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz", + "integrity": "sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ==", + "engines": { + "node": ">=0.2.0" + } + }, "node_modules/bundle-name": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-3.0.0.tgz", @@ -7095,6 +7427,17 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/chainsaw": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz", + "integrity": "sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==", + "dependencies": { + "traverse": ">=0.3.0 <0.4" + }, + "engines": { + "node": "*" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -7211,6 +7554,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/cli-spinners": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.1.tgz", + "integrity": "sha512-jHgecW0pxkonBJdrKsqxgRX9AcG+u/5k0Q7WPDfi8AogLAdwxEkyYYNWwZ5GvVFoFx2uiY1eNcSK00fh+1+FyQ==", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/cli-truncate": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-3.1.0.tgz", @@ -7340,6 +7694,14 @@ "node": ">=12" } }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "engines": { + "node": ">=0.8" + } + }, "node_modules/clsx": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", @@ -8247,6 +8609,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/define-lazy-prop": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", @@ -8302,6 +8675,11 @@ "node": ">= 0.6" } }, + "node_modules/deprecation": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", + "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==" + }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -8323,6 +8701,99 @@ "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==" }, + "node_modules/detect-package-manager": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/detect-package-manager/-/detect-package-manager-2.0.1.tgz", + "integrity": "sha512-j/lJHyoLlWi6G1LDdLgvUtz60Zo5GEj+sVYtTVXnYLDPuzgC3llMxonXym9zIwhhUII8vjdw0LXxavpLqTbl1A==", + "dependencies": { + "execa": "^5.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/detect-package-manager/node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/detect-package-manager/node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/detect-package-manager/node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/detect-package-manager/node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-package-manager/node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-package-manager/node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/detect-package-manager/node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "engines": { + "node": ">=6" + } + }, "node_modules/detective": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/detective/-/detective-5.2.1.tgz", @@ -8523,6 +8994,46 @@ "node": ">=12" } }, + "node_modules/duplexer2": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", + "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", + "dependencies": { + "readable-stream": "^2.0.2" + } + }, + "node_modules/duplexer2/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, + "node_modules/duplexer2/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/duplexer2/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/duplexer2/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/duplexify": { "version": "3.7.1", "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", @@ -8671,7 +9182,6 @@ "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "dev": true, "dependencies": { "is-arrayish": "^0.2.1" } @@ -9762,6 +10272,18 @@ "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==" }, + "node_modules/fast-folder-size": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/fast-folder-size/-/fast-folder-size-1.6.1.tgz", + "integrity": "sha512-F3tRpfkAzb7TT2JNKaJUglyuRjRa+jelQD94s9OSqkfEeytLmupCqQiD+H2KoIXGtp4pB5m4zNmv5m2Ktcr+LA==", + "hasInstallScript": true, + "dependencies": { + "unzipper": "^0.10.11" + }, + "bin": { + "fast-folder-size": "cli.js" + } + }, "node_modules/fast-glob": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", @@ -10097,6 +10619,42 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/fstream": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", + "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", + "dependencies": { + "graceful-fs": "^4.1.2", + "inherits": "~2.0.0", + "mkdirp": ">=0.5 0", + "rimraf": "2" + }, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/fstream/node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/fstream/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/function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", @@ -10895,6 +11453,14 @@ "node": ">=12" } }, + "node_modules/interpret": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", + "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/invariant": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", @@ -10941,8 +11507,7 @@ "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" }, "node_modules/is-bigint": { "version": "1.0.4", @@ -11123,6 +11688,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "engines": { + "node": ">=8" + } + }, "node_modules/is-negative-zero": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", @@ -11184,6 +11757,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-reference": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.1.tgz", @@ -11283,6 +11864,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-weakref": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", @@ -11378,6 +11970,11 @@ "jiti": "bin/jiti.js" } }, + "node_modules/jju": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/jju/-/jju-1.4.0.tgz", + "integrity": "sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==" + }, "node_modules/jose": { "version": "4.14.4", "resolved": "https://registry.npmjs.org/jose/-/jose-4.14.4.tgz", @@ -11510,8 +12107,7 @@ "node_modules/json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" }, "node_modules/json-schema-traverse": { "version": "0.4.1", @@ -11538,7 +12134,6 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "dev": true, "dependencies": { "universalify": "^2.0.0" }, @@ -11703,6 +12298,11 @@ "node": ">=16" } }, + "node_modules/listenercount": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz", + "integrity": "sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ==" + }, "node_modules/listr2": { "version": "6.6.1", "resolved": "https://registry.npmjs.org/listr2/-/listr2-6.6.1.tgz", @@ -11905,6 +12505,21 @@ "integrity": "sha512-sReKOYJIJf74dhJONhU4e0/shzi1trVbSWDOhKYE5XV2O+H7Sb2Dihwuc7xWxVl+DgFPyTqIN3zMfT9cq5iWDg==", "dev": true }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/log-update": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/log-update/-/log-update-5.0.1.tgz", @@ -13917,6 +14532,73 @@ "node": ">= 0.8.0" } }, + "node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ora/node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/ora/node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -13949,7 +14631,6 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true, "engines": { "node": ">=6" } @@ -14004,7 +14685,6 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", @@ -14641,18 +15321,29 @@ "node": ">=0.10.0" } }, + "node_modules/pretty-bytes": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", + "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/pretty-format": { "version": "3.8.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-3.8.0.tgz", "integrity": "sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==" }, "node_modules/prisma": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.0.0.tgz", - "integrity": "sha512-KYWk83Fhi1FH59jSpavAYTt2eoMVW9YKgu8ci0kuUnt6Dup5Qy47pcB4/TLmiPAbhGrxxSz7gsSnJcCmkyPANA==", + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.3.1.tgz", + "integrity": "sha512-Wp2msQIlMPHe+5k5Od6xnsI/WNG7UJGgFUJgqv/ygc7kOECZapcSz/iU4NIEzISs3H1W9sFLjAPbg/gOqqtB7A==", "hasInstallScript": true, "dependencies": { - "@prisma/engines": "5.0.0" + "@prisma/engines": "5.3.1" }, "bin": { "prisma": "build/index.js" @@ -14922,6 +15613,481 @@ "react": ">= 16.8 || 18.0.0" } }, + "node_modules/react-email": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/react-email/-/react-email-1.9.4.tgz", + "integrity": "sha512-DNUQb7xzAlMga2ZppG57bnWhJnqOEcTYzxNvLA4IVCiYJkgPNVukFMOZVG2OuQ0W8ddiF6bLZBKDZHnnIenbpw==", + "dependencies": { + "@commander-js/extra-typings": "9.4.1", + "@manypkg/find-root": "2.1.0", + "@octokit/rest": "19.0.7", + "@react-email/render": "0.0.6", + "chokidar": "3.5.3", + "commander": "9.4.1", + "detect-package-manager": "2.0.1", + "esbuild": "0.16.4", + "fs-extra": "11.1.0", + "glob": "8.0.3", + "log-symbols": "4.1.0", + "normalize-path": "3.0.0", + "ora": "5.4.1", + "read-pkg": "5.2.0", + "shelljs": "0.8.5", + "tree-node-cli": "1.6.0" + }, + "bin": { + "email": "dist/source/index.js" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/react-email/node_modules/@commander-js/extra-typings": { + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/@commander-js/extra-typings/-/extra-typings-9.4.1.tgz", + "integrity": "sha512-v0BqORYamk1koxDon6femDGLWSL7P78vYTyOU5nFaALnmNALL+ktgdHvWbxzzBBJIKS7kv3XvM/DqNwiLcgFTA==", + "peerDependencies": { + "commander": "9.4.x" + } + }, + "node_modules/react-email/node_modules/@esbuild/android-arm": { + "version": "0.16.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.16.4.tgz", + "integrity": "sha512-rZzb7r22m20S1S7ufIc6DC6W659yxoOrl7sKP1nCYhuvUlnCFHVSbATG4keGUtV8rDz11sRRDbWkvQZpzPaHiw==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/react-email/node_modules/@esbuild/android-arm64": { + "version": "0.16.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.16.4.tgz", + "integrity": "sha512-VPuTzXFm/m2fcGfN6CiwZTlLzxrKsWbPkG7ArRFpuxyaHUm/XFHQPD4xNwZT6uUmpIHhnSjcaCmcla8COzmZ5Q==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/react-email/node_modules/@esbuild/android-x64": { + "version": "0.16.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.16.4.tgz", + "integrity": "sha512-MW+B2O++BkcOfMWmuHXB15/l1i7wXhJFqbJhp82IBOais8RBEQv2vQz/jHrDEHaY2X0QY7Wfw86SBL2PbVOr0g==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/react-email/node_modules/@esbuild/darwin-arm64": { + "version": "0.16.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.16.4.tgz", + "integrity": "sha512-a28X1O//aOfxwJVZVs7ZfM8Tyih2Za4nKJrBwW5Wm4yKsnwBy9aiS/xwpxiiTRttw3EaTg4Srerhcm6z0bu9Wg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/react-email/node_modules/@esbuild/darwin-x64": { + "version": "0.16.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.16.4.tgz", + "integrity": "sha512-e3doCr6Ecfwd7VzlaQqEPrnbvvPjE9uoTpxG5pyLzr2rI2NMjDHmvY1E5EO81O/e9TUOLLkXA5m6T8lfjK9yAA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/react-email/node_modules/@esbuild/freebsd-arm64": { + "version": "0.16.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.16.4.tgz", + "integrity": "sha512-Oup3G/QxBgvvqnXWrBed7xxkFNwAwJVHZcklWyQt7YCAL5bfUkaa6FVWnR78rNQiM8MqqLiT6ZTZSdUFuVIg1w==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/react-email/node_modules/@esbuild/freebsd-x64": { + "version": "0.16.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.16.4.tgz", + "integrity": "sha512-vAP+eYOxlN/Bpo/TZmzEQapNS8W1njECrqkTpNgvXskkkJC2AwOXwZWai/Kc2vEFZUXQttx6UJbj9grqjD/+9Q==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/react-email/node_modules/@esbuild/linux-arm": { + "version": "0.16.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.16.4.tgz", + "integrity": "sha512-A47ZmtpIPyERxkSvIv+zLd6kNIOtJH03XA0Hy7jaceRDdQaQVGSDt4mZqpWqJYgDk9rg96aglbF6kCRvPGDSUA==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/react-email/node_modules/@esbuild/linux-arm64": { + "version": "0.16.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.16.4.tgz", + "integrity": "sha512-2zXoBhv4r5pZiyjBKrOdFP4CXOChxXiYD50LRUU+65DkdS5niPFHbboKZd/c81l0ezpw7AQnHeoCy5hFrzzs4g==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/react-email/node_modules/@esbuild/linux-ia32": { + "version": "0.16.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.16.4.tgz", + "integrity": "sha512-uxdSrpe9wFhz4yBwt2kl2TxS/NWEINYBUFIxQtaEVtglm1eECvsj1vEKI0KX2k2wCe17zDdQ3v+jVxfwVfvvjw==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/react-email/node_modules/@esbuild/linux-loong64": { + "version": "0.16.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.16.4.tgz", + "integrity": "sha512-peDrrUuxbZ9Jw+DwLCh/9xmZAk0p0K1iY5d2IcwmnN+B87xw7kujOkig6ZRcZqgrXgeRGurRHn0ENMAjjD5DEg==", + "cpu": [ + "loong64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/react-email/node_modules/@esbuild/linux-mips64el": { + "version": "0.16.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.16.4.tgz", + "integrity": "sha512-sD9EEUoGtVhFjjsauWjflZklTNr57KdQ6xfloO4yH1u7vNQlOfAlhEzbyBKfgbJlW7rwXYBdl5/NcZ+Mg2XhQA==", + "cpu": [ + "mips64el" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/react-email/node_modules/@esbuild/linux-ppc64": { + "version": "0.16.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.16.4.tgz", + "integrity": "sha512-X1HSqHUX9D+d0l6/nIh4ZZJ94eQky8d8z6yxAptpZE3FxCWYWvTDd9X9ST84MGZEJx04VYUD/AGgciddwO0b8g==", + "cpu": [ + "ppc64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/react-email/node_modules/@esbuild/linux-riscv64": { + "version": "0.16.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.16.4.tgz", + "integrity": "sha512-97ANpzyNp0GTXCt6SRdIx1ngwncpkV/z453ZuxbnBROCJ5p/55UjhbaG23UdHj88fGWLKPFtMoU4CBacz4j9FA==", + "cpu": [ + "riscv64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/react-email/node_modules/@esbuild/linux-s390x": { + "version": "0.16.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.16.4.tgz", + "integrity": "sha512-pUvPQLPmbEeJRPjP0DYTC1vjHyhrnCklQmCGYbipkep+oyfTn7GTBJXoPodR7ZS5upmEyc8lzAkn2o29wD786A==", + "cpu": [ + "s390x" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/react-email/node_modules/@esbuild/linux-x64": { + "version": "0.16.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.16.4.tgz", + "integrity": "sha512-N55Q0mJs3Sl8+utPRPBrL6NLYZKBCLLx0bme/+RbjvMforTGGzFvsRl4xLTZMUBFC1poDzBEPTEu5nxizQ9Nlw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/react-email/node_modules/@esbuild/netbsd-x64": { + "version": "0.16.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.16.4.tgz", + "integrity": "sha512-LHSJLit8jCObEQNYkgsDYBh2JrJT53oJO2HVdkSYLa6+zuLJh0lAr06brXIkljrlI+N7NNW1IAXGn/6IZPi3YQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/react-email/node_modules/@esbuild/openbsd-x64": { + "version": "0.16.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.16.4.tgz", + "integrity": "sha512-nLgdc6tWEhcCFg/WVFaUxHcPK3AP/bh+KEwKtl69Ay5IBqUwKDaq/6Xk0E+fh/FGjnLwqFSsarsbPHeKM8t8Sw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/react-email/node_modules/@esbuild/sunos-x64": { + "version": "0.16.4", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.16.4.tgz", + "integrity": "sha512-08SluG24GjPO3tXKk95/85n9kpyZtXCVwURR2i4myhrOfi3jspClV0xQQ0W0PYWHioJj+LejFMt41q+PG3mlAQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/react-email/node_modules/@esbuild/win32-arm64": { + "version": "0.16.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.16.4.tgz", + "integrity": "sha512-yYiRDQcqLYQSvNQcBKN7XogbrSvBE45FEQdH8fuXPl7cngzkCvpsG2H9Uey39IjQ6gqqc+Q4VXYHsQcKW0OMjQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/react-email/node_modules/@esbuild/win32-ia32": { + "version": "0.16.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.16.4.tgz", + "integrity": "sha512-5rabnGIqexekYkh9zXG5waotq8mrdlRoBqAktjx2W3kb0zsI83mdCwrcAeKYirnUaTGztR5TxXcXmQrEzny83w==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/react-email/node_modules/@esbuild/win32-x64": { + "version": "0.16.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.16.4.tgz", + "integrity": "sha512-sN/I8FMPtmtT2Yw+Dly8Ur5vQ5a/RmC8hW7jO9PtPSQUPkowxWpcUZnqOggU7VwyT3Xkj6vcXWd3V/qTXwultQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/react-email/node_modules/@react-email/render": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@react-email/render/-/render-0.0.6.tgz", + "integrity": "sha512-6zs7WZbd37TcPT1OmMPH/kcBpv0QSi+k3om7LyDnbdIcrbwOO/OstVwUaa/6zgvDvnq9Y2wOosbru7j5kUrW9A==", + "dependencies": { + "html-to-text": "9.0.3", + "pretty": "2.0.0", + "react": "18.2.0", + "react-dom": "18.2.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/react-email/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/react-email/node_modules/commander": { + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.4.1.tgz", + "integrity": "sha512-5EEkTNyHNGFPD2H+c/dXXfQZYa/scCKasxWcXJaWnNJ99pnQN9Vnmqow+p+PlFPE63Q6mThaZws1T+HxfpgtPw==", + "engines": { + "node": "^12.20.0 || >=14" + } + }, + "node_modules/react-email/node_modules/esbuild": { + "version": "0.16.4", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.16.4.tgz", + "integrity": "sha512-qQrPMQpPTWf8jHugLWHoGqZjApyx3OEm76dlTXobHwh/EBbavbRdjXdYi/GWr43GyN0sfpap14GPkb05NH3ROA==", + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.16.4", + "@esbuild/android-arm64": "0.16.4", + "@esbuild/android-x64": "0.16.4", + "@esbuild/darwin-arm64": "0.16.4", + "@esbuild/darwin-x64": "0.16.4", + "@esbuild/freebsd-arm64": "0.16.4", + "@esbuild/freebsd-x64": "0.16.4", + "@esbuild/linux-arm": "0.16.4", + "@esbuild/linux-arm64": "0.16.4", + "@esbuild/linux-ia32": "0.16.4", + "@esbuild/linux-loong64": "0.16.4", + "@esbuild/linux-mips64el": "0.16.4", + "@esbuild/linux-ppc64": "0.16.4", + "@esbuild/linux-riscv64": "0.16.4", + "@esbuild/linux-s390x": "0.16.4", + "@esbuild/linux-x64": "0.16.4", + "@esbuild/netbsd-x64": "0.16.4", + "@esbuild/openbsd-x64": "0.16.4", + "@esbuild/sunos-x64": "0.16.4", + "@esbuild/win32-arm64": "0.16.4", + "@esbuild/win32-ia32": "0.16.4", + "@esbuild/win32-x64": "0.16.4" + } + }, + "node_modules/react-email/node_modules/fs-extra": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.1.0.tgz", + "integrity": "sha512-0rcTq621PD5jM/e0a3EJoGC/1TC5ZBCERW82LQuwfGnCa1V8w7dpYH1yNu+SLb6E5dkeCBzKEyLGlFrnr+dUyw==", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/react-email/node_modules/glob": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.0.3.tgz", + "integrity": "sha512-ull455NHSHI/Y1FqGaaYFaLGkNMMJbavMrEGFXG/PGrg6y7sutWHUHrz6gy6WEBH6akM1M414dWKCNs+IhKdiQ==", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/react-email/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/react-hook-form": { "version": "7.45.2", "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.45.2.tgz", @@ -15143,7 +16309,6 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", - "dev": true, "dependencies": { "@types/normalize-package-data": "^2.4.0", "normalize-package-data": "^2.5.0", @@ -15235,14 +16400,12 @@ "node_modules/read-pkg/node_modules/hosted-git-info": { "version": "2.8.9", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", - "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", - "dev": true + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==" }, "node_modules/read-pkg/node_modules/normalize-package-data": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", - "dev": true, "dependencies": { "hosted-git-info": "^2.1.4", "resolve": "^1.10.0", @@ -15254,7 +16417,6 @@ "version": "5.7.2", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "dev": true, "bin": { "semver": "bin/semver" } @@ -15263,11 +16425,32 @@ "version": "0.6.0", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", - "dev": true, "engines": { "node": ">=8" } }, + "node_modules/read-yaml-file": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/read-yaml-file/-/read-yaml-file-1.1.0.tgz", + "integrity": "sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA==", + "dependencies": { + "graceful-fs": "^4.1.5", + "js-yaml": "^3.6.1", + "pify": "^4.0.1", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/read-yaml-file/node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "engines": { + "node": ">=6" + } + }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -15324,6 +16507,17 @@ "decimal.js-light": "^2.4.1" } }, + "node_modules/rechoir": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", + "integrity": "sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==", + "dependencies": { + "resolve": "^1.1.6" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/redent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", @@ -15894,6 +17088,11 @@ "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==" + }, "node_modules/setprototypeof": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", @@ -15994,6 +17193,22 @@ "node": ">=8" } }, + "node_modules/shelljs": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.5.tgz", + "integrity": "sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==", + "dependencies": { + "glob": "^7.0.0", + "interpret": "^1.0.0", + "rechoir": "^0.6.2" + }, + "bin": { + "shjs": "bin/shjs" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/side-channel": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", @@ -16109,7 +17324,6 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", - "dev": true, "dependencies": { "spdx-expression-parse": "^3.0.0", "spdx-license-ids": "^3.0.0" @@ -16118,14 +17332,12 @@ "node_modules/spdx-exceptions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", - "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", - "dev": true + "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==" }, "node_modules/spdx-expression-parse": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", - "dev": true, "dependencies": { "spdx-exceptions": "^2.1.0", "spdx-license-ids": "^3.0.0" @@ -16134,8 +17346,7 @@ "node_modules/spdx-license-ids": { "version": "3.0.13", "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.13.tgz", - "integrity": "sha512-XkD+zwiqXHikFZm4AX/7JSCXA98U5Db4AFd5XUg/+9UNtnH75+Z9KxtpYiJZx36mUDVOwH83pl7yvCer6ewM3w==", - "dev": true + "integrity": "sha512-XkD+zwiqXHikFZm4AX/7JSCXA98U5Db4AFd5XUg/+9UNtnH75+Z9KxtpYiJZx36mUDVOwH83pl7yvCer6ewM3w==" }, "node_modules/split2": { "version": "3.2.2", @@ -16757,6 +17968,14 @@ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" }, + "node_modules/traverse": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", + "integrity": "sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==", + "engines": { + "node": "*" + } + }, "node_modules/tree-kill": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", @@ -16766,6 +17985,28 @@ "tree-kill": "cli.js" } }, + "node_modules/tree-node-cli": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/tree-node-cli/-/tree-node-cli-1.6.0.tgz", + "integrity": "sha512-M8um5Lbl76rWU5aC8oOeEhruiCM29lFCKnwpxrwMjpRicHXJx+bb9Cak11G3zYLrMb6Glsrhnn90rHIzDJrjvg==", + "dependencies": { + "commander": "^5.0.0", + "fast-folder-size": "1.6.1", + "pretty-bytes": "^5.6.0" + }, + "bin": { + "tree": "bin/tree.js", + "treee": "bin/tree.js" + } + }, + "node_modules/tree-node-cli/node_modules/commander": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", + "engines": { + "node": ">= 6" + } + }, "node_modules/trim-lines": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", @@ -17643,11 +18884,15 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/universal-user-agent": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz", + "integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w==" + }, "node_modules/universalify": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", - "dev": true, "engines": { "node": ">= 10.0.0" } @@ -17668,6 +18913,55 @@ "node": ">=8" } }, + "node_modules/unzipper": { + "version": "0.10.14", + "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.14.tgz", + "integrity": "sha512-ti4wZj+0bQTiX2KmKWuwj7lhV+2n//uXEotUmGuQqrbVZSEGFMbI68+c6JCQ8aAmUWYvtHEz2A8K6wXvueR/6g==", + "dependencies": { + "big-integer": "^1.6.17", + "binary": "~0.3.0", + "bluebird": "~3.4.1", + "buffer-indexof-polyfill": "~1.0.0", + "duplexer2": "~0.1.4", + "fstream": "^1.0.12", + "graceful-fs": "^4.2.2", + "listenercount": "~1.0.1", + "readable-stream": "~2.3.6", + "setimmediate": "~1.0.4" + } + }, + "node_modules/unzipper/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, + "node_modules/unzipper/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/unzipper/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/unzipper/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/update-browserslist-db": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz", @@ -17794,7 +19088,6 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", - "dev": true, "dependencies": { "spdx-correct": "^3.0.0", "spdx-expression-parse": "^3.0.0" @@ -17874,6 +19167,14 @@ "node": ">=10.13.0" } }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dependencies": { + "defaults": "^1.0.3" + } + }, "node_modules/web-namespaces": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", @@ -18120,7 +19421,8 @@ "license": "MIT", "dependencies": { "@react-email/components": "^0.0.7", - "nodemailer": "^6.9.3" + "nodemailer": "^6.9.3", + "react-email": "^1.9.4" }, "devDependencies": { "@documenso/tailwind-config": "*", @@ -18198,8 +19500,12 @@ "version": "1.0.0", "license": "MIT", "dependencies": { - "@prisma/client": "5.0.0", - "prisma": "5.0.0" + "@prisma/client": "5.3.1", + "prisma": "5.3.1" + }, + "devDependencies": { + "ts-node": "^10.9.1", + "typescript": "^5.1.6" } }, "packages/tailwind-config": { diff --git a/packages/email/package.json b/packages/email/package.json index c1751dce6..20e214fee 100644 --- a/packages/email/package.json +++ b/packages/email/package.json @@ -17,11 +17,12 @@ }, "dependencies": { "@react-email/components": "^0.0.7", - "nodemailer": "^6.9.3" + "nodemailer": "^6.9.3", + "react-email": "^1.9.4" }, "devDependencies": { - "@documenso/tsconfig": "*", "@documenso/tailwind-config": "*", + "@documenso/tsconfig": "*", "@types/nodemailer": "^6.4.8", "tsup": "^7.1.0" } diff --git a/packages/email/template-components/template-document-completed.tsx b/packages/email/template-components/template-document-completed.tsx index b64b13cff..91d8fa29d 100644 --- a/packages/email/template-components/template-document-completed.tsx +++ b/packages/email/template-components/template-document-completed.tsx @@ -1,4 +1,4 @@ -import { Button, Img, Section, Tailwind, Text } from '@react-email/components'; +import { Button, Column, Img, Row, Section, Tailwind, Text } from '@react-email/components'; import * as config from '@documenso/tailwind-config'; @@ -29,11 +29,23 @@ export const TemplateDocumentCompleted = ({ }, }} > -
-
- Documenso -
+
+ + + + Documenso + + + + +
+ +
Completed diff --git a/packages/email/template-components/template-document-invite.tsx b/packages/email/template-components/template-document-invite.tsx index bf2fb905e..fcfba406d 100644 --- a/packages/email/template-components/template-document-invite.tsx +++ b/packages/email/template-components/template-document-invite.tsx @@ -1,4 +1,4 @@ -import { Button, Img, Section, Tailwind, Text } from '@react-email/components'; +import { Button, Column, Img, Row, Section, Tailwind, Text } from '@react-email/components'; import * as config from '@documenso/tailwind-config'; @@ -30,13 +30,26 @@ export const TemplateDocumentInvite = ({ }, }} > -
-
- Documenso -
+
+ + + + Documenso + + + + +
+ +
- {inviterName} has invited you to sign "{documentName}" + {inviterName} has invited you to sign +
"{documentName}"
diff --git a/packages/email/template-components/template-document-pending.tsx b/packages/email/template-components/template-document-pending.tsx index 80387b783..f9fc8648a 100644 --- a/packages/email/template-components/template-document-pending.tsx +++ b/packages/email/template-components/template-document-pending.tsx @@ -1,4 +1,4 @@ -import { Img, Section, Tailwind, Text } from '@react-email/components'; +import { Column, Img, Row, Section, Tailwind, Text } from '@react-email/components'; import * as config from '@documenso/tailwind-config'; @@ -25,11 +25,23 @@ export const TemplateDocumentPending = ({ }, }} > -
-
- Documenso -
+
+ + + + Documenso + + + + +
+ +
Waiting for others diff --git a/packages/email/template-components/template-footer.tsx b/packages/email/template-components/template-footer.tsx index ee395a1e9..7f93f0063 100644 --- a/packages/email/template-components/template-footer.tsx +++ b/packages/email/template-components/template-footer.tsx @@ -1,14 +1,20 @@ import { Link, Section, Text } from '@react-email/components'; -export const TemplateFooter = () => { +export type TemplateFooterProps = { + isDocument?: boolean; +}; + +export const TemplateFooter = ({ isDocument = true }: TemplateFooterProps) => { return (
- - This document was sent using{' '} - - Documenso. - - + {isDocument && ( + + This document was sent using{' '} + + Documenso. + + + )} Documenso diff --git a/packages/email/template-components/template-forgot-password.tsx b/packages/email/template-components/template-forgot-password.tsx new file mode 100644 index 000000000..45560a026 --- /dev/null +++ b/packages/email/template-components/template-forgot-password.tsx @@ -0,0 +1,54 @@ +import { Button, Img, Section, Tailwind, Text } from '@react-email/components'; + +import * as config from '@documenso/tailwind-config'; + +export type TemplateForgotPasswordProps = { + resetPasswordLink: string; + assetBaseUrl: string; +}; + +export const TemplateForgotPassword = ({ + resetPasswordLink, + assetBaseUrl, +}: TemplateForgotPasswordProps) => { + const getAssetUrl = (path: string) => { + return new URL(path, assetBaseUrl).toString(); + }; + + return ( + +
+
+ Documenso +
+ + + Forgot your password? + + + + That's okay, it happens! Click the button below to reset your password. + + +
+ +
+
+
+ ); +}; + +export default TemplateForgotPassword; diff --git a/packages/email/template-components/template-reset-password.tsx b/packages/email/template-components/template-reset-password.tsx new file mode 100644 index 000000000..ee2e8e7b1 --- /dev/null +++ b/packages/email/template-components/template-reset-password.tsx @@ -0,0 +1,43 @@ +import { Img, Section, Tailwind, Text } from '@react-email/components'; + +import * as config from '@documenso/tailwind-config'; + +export interface TemplateResetPasswordProps { + userName: string; + userEmail: string; + assetBaseUrl: string; +} + +export const TemplateResetPassword = ({ assetBaseUrl }: TemplateResetPasswordProps) => { + const getAssetUrl = (path: string) => { + return new URL(path, assetBaseUrl).toString(); + }; + + return ( + +
+
+ Documenso +
+ + + Password updated! + + + + Your password has been updated. + +
+
+ ); +}; + +export default TemplateResetPassword; diff --git a/packages/email/templates/document-invite.tsx b/packages/email/templates/document-invite.tsx index 465685649..661a2fd5f 100644 --- a/packages/email/templates/document-invite.tsx +++ b/packages/email/templates/document-invite.tsx @@ -20,7 +20,9 @@ import { } from '../template-components/template-document-invite'; import TemplateFooter from '../template-components/template-footer'; -export type DocumentInviteEmailTemplateProps = Partial; +export type DocumentInviteEmailTemplateProps = Partial & { + customBody?: string; +}; export const DocumentInviteEmailTemplate = ({ inviterName = 'Lucas Smith', @@ -28,6 +30,7 @@ export const DocumentInviteEmailTemplate = ({ documentName = 'Open Source Pledge.pdf', signDocumentLink = 'https://documenso.com', assetBaseUrl = 'http://localhost:3002', + customBody, }: DocumentInviteEmailTemplateProps) => { const previewText = `Completed Document`; @@ -78,7 +81,11 @@ export const DocumentInviteEmailTemplate = ({
- {inviterName} has invited you to sign the document "{documentName}". + {customBody ? ( +
{customBody}
+ ) : ( + `${inviterName} has invited you to sign the document "${documentName}".` + )}
diff --git a/packages/email/templates/forgot-password.tsx b/packages/email/templates/forgot-password.tsx new file mode 100644 index 000000000..ab3db58f5 --- /dev/null +++ b/packages/email/templates/forgot-password.tsx @@ -0,0 +1,74 @@ +import { + Body, + Container, + Head, + Html, + Img, + Preview, + Section, + Tailwind, +} from '@react-email/components'; + +import config from '@documenso/tailwind-config'; + +import TemplateFooter from '../template-components/template-footer'; +import { + TemplateForgotPassword, + TemplateForgotPasswordProps, +} from '../template-components/template-forgot-password'; + +export type ForgotPasswordTemplateProps = Partial; + +export const ForgotPasswordTemplate = ({ + resetPasswordLink = 'https://documenso.com', + assetBaseUrl = 'http://localhost:3002', +}: ForgotPasswordTemplateProps) => { + const previewText = `Password Reset Requested`; + + const getAssetUrl = (path: string) => { + return new URL(path, assetBaseUrl).toString(); + }; + + return ( + + + {previewText} + + +
+ +
+ Documenso Logo + + +
+
+ +
+ + + + +
+ +
+ + ); +}; + +export default ForgotPasswordTemplate; diff --git a/packages/email/templates/reset-password.tsx b/packages/email/templates/reset-password.tsx new file mode 100644 index 000000000..35881fe72 --- /dev/null +++ b/packages/email/templates/reset-password.tsx @@ -0,0 +1,102 @@ +import { + Body, + Container, + Head, + Hr, + Html, + Img, + Link, + Preview, + Section, + Tailwind, + Text, +} from '@react-email/components'; + +import config from '@documenso/tailwind-config'; + +import TemplateFooter from '../template-components/template-footer'; +import { + TemplateResetPassword, + TemplateResetPasswordProps, +} from '../template-components/template-reset-password'; + +export type ResetPasswordTemplateProps = Partial; + +export const ResetPasswordTemplate = ({ + userName = 'Lucas Smith', + userEmail = 'lucas@documenso.com', + assetBaseUrl = 'http://localhost:3002', +}: ResetPasswordTemplateProps) => { + const previewText = `Password Reset Successful`; + + const getAssetUrl = (path: string) => { + return new URL(path, assetBaseUrl).toString(); + }; + + return ( + + + {previewText} + + +
+ +
+ Documenso Logo + + +
+
+ + +
+ + Hi, {userName}{' '} + + ({userEmail}) + + + + + We've changed your password as you asked. You can now sign in with your new + password. + + + Didn't request a password change? We are here to help you secure your account, + just{' '} + + contact us. + + +
+
+ +
+ + + + +
+ +
+ + ); +}; + +export default ResetPasswordTemplate; diff --git a/packages/lib/server-only/auth/send-forgot-password.ts b/packages/lib/server-only/auth/send-forgot-password.ts new file mode 100644 index 000000000..e62d5e176 --- /dev/null +++ b/packages/lib/server-only/auth/send-forgot-password.ts @@ -0,0 +1,53 @@ +import { createElement } from 'react'; + +import { mailer } from '@documenso/email/mailer'; +import { render } from '@documenso/email/render'; +import { ForgotPasswordTemplate } from '@documenso/email/templates/forgot-password'; +import { prisma } from '@documenso/prisma'; + +export interface SendForgotPasswordOptions { + userId: number; +} + +export const sendForgotPassword = async ({ userId }: SendForgotPasswordOptions) => { + const user = await prisma.user.findFirstOrThrow({ + where: { + id: userId, + }, + include: { + PasswordResetToken: { + orderBy: { + createdAt: 'desc', + }, + take: 1, + }, + }, + }); + + if (!user) { + throw new Error('User not found'); + } + + const token = user.PasswordResetToken[0].token; + const assetBaseUrl = process.env.NEXT_PUBLIC_WEBAPP_URL || 'http://localhost:3000'; + const resetPasswordLink = `${process.env.NEXT_PUBLIC_WEBAPP_URL}/reset-password/${token}`; + + const template = createElement(ForgotPasswordTemplate, { + assetBaseUrl, + resetPasswordLink, + }); + + return await mailer.sendMail({ + to: { + address: user.email, + name: user.name || '', + }, + from: { + name: process.env.NEXT_PRIVATE_SMTP_FROM_NAME || 'Documenso', + address: process.env.NEXT_PRIVATE_SMTP_FROM_ADDRESS || 'noreply@documenso.com', + }, + subject: 'Forgot Password?', + html: render(template), + text: render(template, { plainText: true }), + }); +}; diff --git a/packages/lib/server-only/auth/send-reset-password.ts b/packages/lib/server-only/auth/send-reset-password.ts new file mode 100644 index 000000000..303ceb821 --- /dev/null +++ b/packages/lib/server-only/auth/send-reset-password.ts @@ -0,0 +1,42 @@ +import { createElement } from 'react'; + +import { mailer } from '@documenso/email/mailer'; +import { render } from '@documenso/email/render'; +import { ResetPasswordTemplate } from '@documenso/email/templates/reset-password'; +import { prisma } from '@documenso/prisma'; + +export interface SendResetPasswordOptions { + userId: number; +} + +export const sendResetPassword = async ({ userId }: SendResetPasswordOptions) => { + const user = await prisma.user.findFirstOrThrow({ + where: { + id: userId, + }, + }); + + const assetBaseUrl = process.env.NEXT_PUBLIC_WEBAPP_URL || 'http://localhost:3000'; + + console.log({ assetBaseUrl }); + + const template = createElement(ResetPasswordTemplate, { + assetBaseUrl, + userEmail: user.email, + userName: user.name || '', + }); + + return await mailer.sendMail({ + to: { + address: user.email, + name: user.name || '', + }, + from: { + name: process.env.NEXT_PRIVATE_SMTP_FROM_NAME || 'Documenso', + address: process.env.NEXT_PRIVATE_SMTP_FROM_ADDRESS || 'noreply@documenso.com', + }, + subject: 'Password Reset Success!', + html: render(template), + text: render(template, { plainText: true }), + }); +}; diff --git a/packages/lib/server-only/document-meta/upsert-document-meta.ts b/packages/lib/server-only/document-meta/upsert-document-meta.ts new file mode 100644 index 000000000..e3cce2ea2 --- /dev/null +++ b/packages/lib/server-only/document-meta/upsert-document-meta.ts @@ -0,0 +1,30 @@ +'use server'; + +import { prisma } from '@documenso/prisma'; + +export type CreateDocumentMetaOptions = { + documentId: number; + subject: string; + message: string; +}; + +export const upsertDocumentMeta = async ({ + subject, + message, + documentId, +}: CreateDocumentMetaOptions) => { + return await prisma.documentMeta.upsert({ + where: { + documentId, + }, + create: { + subject, + message, + documentId, + }, + update: { + subject, + message, + }, + }); +}; 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 a013d5e69..2712c56fa 100644 --- a/packages/lib/server-only/document/complete-document-with-token.ts +++ b/packages/lib/server-only/document/complete-document-with-token.ts @@ -4,6 +4,7 @@ import { prisma } from '@documenso/prisma'; import { DocumentStatus, SigningStatus } from '@documenso/prisma/client'; import { sealDocument } from './seal-document'; +import { sendPendingEmail } from './send-pending-email'; export type CompleteDocumentWithTokenOptions = { token: string; @@ -69,6 +70,19 @@ export const completeDocumentWithToken = async ({ }, }); + const pendingRecipients = await prisma.recipient.count({ + where: { + documentId: document.id, + signingStatus: { + not: SigningStatus.SIGNED, + }, + }, + }); + + if (pendingRecipients > 0) { + await sendPendingEmail({ documentId, recipientId: recipient.id }); + } + const documents = await prisma.document.updateMany({ where: { id: document.id, diff --git a/packages/lib/server-only/document/get-document-by-id.ts b/packages/lib/server-only/document/get-document-by-id.ts index 0fce1af4d..0b599a71c 100644 --- a/packages/lib/server-only/document/get-document-by-id.ts +++ b/packages/lib/server-only/document/get-document-by-id.ts @@ -13,6 +13,7 @@ export const getDocumentById = async ({ id, userId }: GetDocumentByIdOptions) => }, include: { documentData: true, + documentMeta: true, }, }); }; diff --git a/packages/lib/server-only/document/seal-document.ts b/packages/lib/server-only/document/seal-document.ts index 883d13e6f..d551e4adf 100644 --- a/packages/lib/server-only/document/seal-document.ts +++ b/packages/lib/server-only/document/seal-document.ts @@ -9,6 +9,7 @@ import { DocumentStatus, SigningStatus } from '@documenso/prisma/client'; import { getFile } from '../../universal/upload/get-file'; import { putFile } from '../../universal/upload/put-file'; import { insertFieldInPDF } from '../pdf/insert-field-in-pdf'; +import { sendCompletedEmail } from './send-completed-email'; export type SealDocumentOptions = { documentId: number; @@ -86,4 +87,6 @@ export const sealDocument = async ({ documentId }: SealDocumentOptions) => { data: newData, }, }); + + await sendCompletedEmail({ documentId }); }; diff --git a/packages/lib/server-only/document/send-completed-email.ts b/packages/lib/server-only/document/send-completed-email.ts new file mode 100644 index 000000000..9d0d2d499 --- /dev/null +++ b/packages/lib/server-only/document/send-completed-email.ts @@ -0,0 +1,57 @@ +import { createElement } from 'react'; + +import { mailer } from '@documenso/email/mailer'; +import { render } from '@documenso/email/render'; +import { DocumentCompletedEmailTemplate } from '@documenso/email/templates/document-completed'; +import { prisma } from '@documenso/prisma'; + +export interface SendDocumentOptions { + documentId: number; +} + +export const sendCompletedEmail = async ({ documentId }: SendDocumentOptions) => { + const document = await prisma.document.findUnique({ + where: { + id: documentId, + }, + include: { + Recipient: true, + }, + }); + + if (!document) { + throw new Error('Document not found'); + } + + if (document.Recipient.length === 0) { + throw new Error('Document has no recipients'); + } + + await Promise.all([ + document.Recipient.map(async (recipient) => { + const { email, name, token } = recipient; + + const assetBaseUrl = process.env.NEXT_PUBLIC_WEBAPP_URL || 'http://localhost:3000'; + + const template = createElement(DocumentCompletedEmailTemplate, { + documentName: document.title, + assetBaseUrl, + downloadLink: `${process.env.NEXT_PUBLIC_WEBAPP_URL}/sign/${token}/complete`, + }); + + await mailer.sendMail({ + to: { + address: email, + name, + }, + from: { + name: process.env.NEXT_PRIVATE_SMTP_FROM_NAME || 'Documenso', + address: process.env.NEXT_PRIVATE_SMTP_FROM_ADDRESS || 'noreply@documenso.com', + }, + subject: 'Signing Complete!', + html: render(template), + text: render(template, { plainText: true }), + }); + }), + ]); +}; diff --git a/packages/lib/server-only/document/send-document.tsx b/packages/lib/server-only/document/send-document.tsx index 37ecc66b7..fcc0f829c 100644 --- a/packages/lib/server-only/document/send-document.tsx +++ b/packages/lib/server-only/document/send-document.tsx @@ -3,13 +3,14 @@ import { createElement } from 'react'; import { mailer } from '@documenso/email/mailer'; import { render } from '@documenso/email/render'; import { DocumentInviteEmailTemplate } from '@documenso/email/templates/document-invite'; +import { renderCustomEmailTemplate } from '@documenso/lib/utils/render-custom-email-template'; import { prisma } from '@documenso/prisma'; import { DocumentStatus, SendStatus } from '@documenso/prisma/client'; -export interface SendDocumentOptions { +export type SendDocumentOptions = { documentId: number; userId: number; -} +}; export const sendDocument = async ({ documentId, userId }: SendDocumentOptions) => { const user = await prisma.user.findFirstOrThrow({ @@ -25,9 +26,12 @@ export const sendDocument = async ({ documentId, userId }: SendDocumentOptions) }, include: { Recipient: true, + documentMeta: true, }, }); + const customEmail = document?.documentMeta; + if (!document) { throw new Error('Document not found'); } @@ -44,12 +48,18 @@ export const sendDocument = async ({ documentId, userId }: SendDocumentOptions) document.Recipient.map(async (recipient) => { const { email, name } = recipient; + const customEmailTemplate = { + 'signer.name': name, + 'signer.email': email, + 'document.name': document.title, + }; + if (recipient.sendStatus === SendStatus.SENT) { return; } - const assetBaseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000'; - const signDocumentLink = `${process.env.NEXT_PUBLIC_SITE_URL}/sign/${recipient.token}`; + const assetBaseUrl = process.env.NEXT_PUBLIC_WEBAPP_URL || 'http://localhost:3000'; + const signDocumentLink = `${process.env.NEXT_PUBLIC_WEBAPP_URL}/sign/${recipient.token}`; const template = createElement(DocumentInviteEmailTemplate, { documentName: document.title, @@ -57,6 +67,7 @@ export const sendDocument = async ({ documentId, userId }: SendDocumentOptions) inviterEmail: user.email, assetBaseUrl, signDocumentLink, + customBody: renderCustomEmailTemplate(customEmail?.message || '', customEmailTemplate), }); await mailer.sendMail({ @@ -68,7 +79,9 @@ export const sendDocument = async ({ documentId, userId }: SendDocumentOptions) name: process.env.NEXT_PRIVATE_SMTP_FROM_NAME || 'Documenso', address: process.env.NEXT_PRIVATE_SMTP_FROM_ADDRESS || 'noreply@documenso.com', }, - subject: 'Please sign this document', + subject: customEmail?.subject + ? renderCustomEmailTemplate(customEmail.subject, customEmailTemplate) + : 'Please sign this document', html: render(template), text: render(template, { plainText: true }), }); diff --git a/packages/lib/server-only/document/send-pending-email.ts b/packages/lib/server-only/document/send-pending-email.ts new file mode 100644 index 000000000..75861be78 --- /dev/null +++ b/packages/lib/server-only/document/send-pending-email.ts @@ -0,0 +1,64 @@ +import { createElement } from 'react'; + +import { mailer } from '@documenso/email/mailer'; +import { render } from '@documenso/email/render'; +import { DocumentPendingEmailTemplate } from '@documenso/email/templates/document-pending'; +import { prisma } from '@documenso/prisma'; + +export interface SendPendingEmailOptions { + documentId: number; + recipientId: number; +} + +export const sendPendingEmail = async ({ documentId, recipientId }: SendPendingEmailOptions) => { + const document = await prisma.document.findFirst({ + where: { + id: documentId, + Recipient: { + some: { + id: recipientId, + }, + }, + }, + include: { + Recipient: { + where: { + id: recipientId, + }, + }, + }, + }); + + if (!document) { + throw new Error('Document not found'); + } + + if (document.Recipient.length === 0) { + throw new Error('Document has no recipients'); + } + + const [recipient] = document.Recipient; + + const { email, name } = recipient; + + const assetBaseUrl = process.env.NEXT_PUBLIC_WEBAPP_URL || 'http://localhost:3000'; + + const template = createElement(DocumentPendingEmailTemplate, { + documentName: document.title, + assetBaseUrl, + }); + + await mailer.sendMail({ + to: { + address: email, + name, + }, + from: { + name: process.env.NEXT_PRIVATE_SMTP_FROM_NAME || 'Documenso', + address: process.env.NEXT_PRIVATE_SMTP_FROM_ADDRESS || 'noreply@documenso.com', + }, + subject: 'Waiting for others to complete signing.', + html: render(template), + text: render(template, { plainText: true }), + }); +}; diff --git a/packages/lib/server-only/document/update-document.ts b/packages/lib/server-only/document/update-document.ts new file mode 100644 index 000000000..7793c990a --- /dev/null +++ b/packages/lib/server-only/document/update-document.ts @@ -0,0 +1,21 @@ +'use server'; + +import { Prisma } from '@prisma/client'; + +import { prisma } from '@documenso/prisma'; + +export type UpdateDocumentOptions = { + documentId: number; + data: Prisma.DocumentUpdateInput; +}; + +export const updateDocument = async ({ documentId, data }: UpdateDocumentOptions) => { + return await prisma.document.update({ + where: { + id: documentId, + }, + data: { + ...data, + }, + }); +}; 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 61726f53c..e7b1e7c5a 100644 --- a/packages/lib/server-only/pdf/insert-field-in-pdf.ts +++ b/packages/lib/server-only/pdf/insert-field-in-pdf.ts @@ -50,10 +50,10 @@ export const insertFieldInPDF = async (pdf: PDFDocument, field: FieldWithSignatu let imageWidth = image.width; let imageHeight = image.height; - const initialDimensions = { - width: imageWidth, - height: imageHeight, - }; + // const initialDimensions = { + // width: imageWidth, + // height: imageHeight, + // }; const scalingFactor = Math.min(fieldWidth / imageWidth, fieldHeight / imageHeight, 1); @@ -76,10 +76,10 @@ export const insertFieldInPDF = async (pdf: PDFDocument, field: FieldWithSignatu let textWidth = font.widthOfTextAtSize(field.customText, fontSize); const textHeight = font.heightAtSize(fontSize); - const initialDimensions = { - width: textWidth, - height: textHeight, - }; + // const initialDimensions = { + // width: textWidth, + // height: textHeight, + // }; const scalingFactor = Math.min(fieldWidth / textWidth, fieldHeight / textHeight, 1); diff --git a/packages/lib/server-only/user/forgot-password.ts b/packages/lib/server-only/user/forgot-password.ts new file mode 100644 index 000000000..14ae48453 --- /dev/null +++ b/packages/lib/server-only/user/forgot-password.ts @@ -0,0 +1,53 @@ +import crypto from 'crypto'; + +import { prisma } from '@documenso/prisma'; +import { TForgotPasswordFormSchema } from '@documenso/trpc/server/profile-router/schema'; + +import { ONE_DAY, ONE_HOUR } from '../../constants/time'; +import { sendForgotPassword } from '../auth/send-forgot-password'; + +export const forgotPassword = async ({ email }: TForgotPasswordFormSchema) => { + const user = await prisma.user.findFirst({ + where: { + email: { + equals: email, + mode: 'insensitive', + }, + }, + }); + + if (!user) { + return; + } + + // Find a token that was created in the last hour and hasn't expired + const existingToken = await prisma.passwordResetToken.findFirst({ + where: { + userId: user.id, + expiry: { + gt: new Date(), + }, + createdAt: { + gt: new Date(Date.now() - ONE_HOUR), + }, + }, + }); + + if (existingToken) { + return; + } + + const token = crypto.randomBytes(18).toString('hex'); + + await prisma.passwordResetToken.create({ + data: { + token, + expiry: new Date(Date.now() + ONE_DAY), + userId: user.id, + }, + }); + + await sendForgotPassword({ + userId: user.id, + }).catch((err) => console.error(err)); +}; diff --git a/packages/lib/server-only/user/get-reset-token-validity.ts b/packages/lib/server-only/user/get-reset-token-validity.ts new file mode 100644 index 000000000..8abd9e37a --- /dev/null +++ b/packages/lib/server-only/user/get-reset-token-validity.ts @@ -0,0 +1,19 @@ +import { prisma } from '@documenso/prisma'; + +type GetResetTokenValidityOptions = { + token: string; +}; + +export const getResetTokenValidity = async ({ token }: GetResetTokenValidityOptions) => { + const found = await prisma.passwordResetToken.findFirst({ + select: { + id: true, + expiry: true, + }, + where: { + token, + }, + }); + + return !!found && found.expiry > new Date(); +}; diff --git a/packages/lib/server-only/user/reset-password.ts b/packages/lib/server-only/user/reset-password.ts new file mode 100644 index 000000000..2233894d8 --- /dev/null +++ b/packages/lib/server-only/user/reset-password.ts @@ -0,0 +1,62 @@ +import { compare, hash } from 'bcrypt'; + +import { prisma } from '@documenso/prisma'; + +import { SALT_ROUNDS } from '../../constants/auth'; +import { sendResetPassword } from '../auth/send-reset-password'; + +export type ResetPasswordOptions = { + token: string; + password: string; +}; + +export const resetPassword = async ({ token, password }: ResetPasswordOptions) => { + if (!token) { + throw new Error('Invalid token provided. Please try again.'); + } + + const foundToken = await prisma.passwordResetToken.findFirst({ + where: { + token, + }, + include: { + User: true, + }, + }); + + if (!foundToken) { + throw new Error('Invalid token provided. Please try again.'); + } + + const now = new Date(); + + if (now > foundToken.expiry) { + throw new Error('Token has expired. Please try again.'); + } + + const isSamePassword = await compare(password, foundToken.User.password || ''); + + if (isSamePassword) { + throw new Error('Your new password cannot be the same as your old password.'); + } + + const hashedPassword = await hash(password, SALT_ROUNDS); + + await prisma.$transaction([ + prisma.user.update({ + where: { + id: foundToken.userId, + }, + data: { + password: hashedPassword, + }, + }), + prisma.passwordResetToken.deleteMany({ + where: { + userId: foundToken.userId, + }, + }), + ]); + + await sendResetPassword({ userId: foundToken.userId }); +}; diff --git a/packages/lib/universal/get-base-url.ts b/packages/lib/universal/get-base-url.ts index aa8884088..2120c9f54 100644 --- a/packages/lib/universal/get-base-url.ts +++ b/packages/lib/universal/get-base-url.ts @@ -8,8 +8,8 @@ export const getBaseUrl = () => { return `https://${process.env.VERCEL_URL}`; } - if (process.env.NEXT_PUBLIC_SITE_URL) { - return `https://${process.env.NEXT_PUBLIC_SITE_URL}`; + if (process.env.NEXT_PUBLIC_WEBAPP_URL) { + return process.env.NEXT_PUBLIC_WEBAPP_URL; } return `http://localhost:${process.env.PORT ?? 3000}`; diff --git a/packages/lib/utils/render-custom-email-template.ts b/packages/lib/utils/render-custom-email-template.ts new file mode 100644 index 000000000..e3fdf5c7b --- /dev/null +++ b/packages/lib/utils/render-custom-email-template.ts @@ -0,0 +1,12 @@ +export const renderCustomEmailTemplate = >( + template: string, + variables: T, +): string => { + return template.replace(/\{(\S+)\}/g, (_, key) => { + if (key in variables) { + return variables[key]; + } + + return key; + }); +}; diff --git a/packages/prisma/helper.ts b/packages/prisma/helper.ts new file mode 100644 index 000000000..865e16239 --- /dev/null +++ b/packages/prisma/helper.ts @@ -0,0 +1,52 @@ +/// + +export const getDatabaseUrl = () => { + if (process.env.NEXT_PRIVATE_DATABASE_URL) { + return process.env.NEXT_PRIVATE_DATABASE_URL; + } + + if (process.env.POSTGRES_URL) { + process.env.NEXT_PRIVATE_DATABASE_URL = process.env.POSTGRES_URL; + process.env.NEXT_PRIVATE_DIRECT_DATABASE_URL = process.env.POSTGRES_URL; + } + + if (process.env.DATABASE_URL) { + process.env.NEXT_PRIVATE_DATABASE_URL = process.env.DATABASE_URL; + process.env.NEXT_PRIVATE_DIRECT_DATABASE_URL = process.env.DATABASE_URL; + } + + if (process.env.POSTGRES_PRISMA_URL) { + process.env.NEXT_PRIVATE_DATABASE_URL = process.env.POSTGRES_PRISMA_URL; + } + + if (process.env.POSTGRES_URL_NON_POOLING) { + process.env.NEXT_PRIVATE_DIRECT_DATABASE_URL = process.env.POSTGRES_URL_NON_POOLING; + } + + // We change the protocol from `postgres:` to `https:` so we can construct a easily + // mofifiable URL. + const url = new URL(process.env.NEXT_PRIVATE_DATABASE_URL.replace('postgres://', 'https://')); + + // If we're using a connection pool, we need to let Prisma know that + // we're using PgBouncer. + if (process.env.NEXT_PRIVATE_DATABASE_URL !== process.env.NEXT_PRIVATE_DIRECT_DATABASE_URL) { + url.searchParams.set('pgbouncer', 'true'); + + process.env.NEXT_PRIVATE_DATABASE_URL = url.toString().replace('https://', 'postgres://'); + } + + // Support for neon.tech (Neon Database) + if (url.hostname.endsWith('neon.tech')) { + const [projectId, ...rest] = url.hostname.split('.'); + + if (!projectId.endsWith('-pooler')) { + url.hostname = `${projectId}-pooler.${rest.join('.')}`; + } + + url.searchParams.set('pgbouncer', 'true'); + + process.env.NEXT_PRIVATE_DATABASE_URL = url.toString().replace('https://', 'postgres://'); + } + + return process.env.NEXT_PRIVATE_DATABASE_URL; +}; diff --git a/packages/prisma/index.ts b/packages/prisma/index.ts index 93a334caa..b9e290add 100644 --- a/packages/prisma/index.ts +++ b/packages/prisma/index.ts @@ -1,5 +1,7 @@ import { PrismaClient } from '@prisma/client'; +import { getDatabaseUrl } from './helper'; + declare global { // We need `var` to declare a global variable in TypeScript // eslint-disable-next-line no-var @@ -7,9 +9,13 @@ declare global { } if (!globalThis.prisma) { - globalThis.prisma = new PrismaClient(); + globalThis.prisma = new PrismaClient({ datasourceUrl: getDatabaseUrl() }); } -export const prisma = globalThis.prisma || new PrismaClient(); +export const prisma = + globalThis.prisma || + new PrismaClient({ + datasourceUrl: getDatabaseUrl(), + }); export const getPrismaClient = () => prisma; diff --git a/packages/prisma/migrations/20230917190854_password_reset_token/migration.sql b/packages/prisma/migrations/20230917190854_password_reset_token/migration.sql new file mode 100644 index 000000000..d22107691 --- /dev/null +++ b/packages/prisma/migrations/20230917190854_password_reset_token/migration.sql @@ -0,0 +1,16 @@ +-- CreateTable +CREATE TABLE "PasswordResetToken" ( + "id" SERIAL NOT NULL, + "token" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "expiry" TIMESTAMP(3) NOT NULL, + "userId" INTEGER NOT NULL, + + CONSTRAINT "PasswordResetToken_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "PasswordResetToken_token_key" ON "PasswordResetToken"("token"); + +-- AddForeignKey +ALTER TABLE "PasswordResetToken" ADD CONSTRAINT "PasswordResetToken_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/packages/prisma/migrations/20230920052232_document_meta/migration.sql b/packages/prisma/migrations/20230920052232_document_meta/migration.sql new file mode 100644 index 000000000..00e9db735 --- /dev/null +++ b/packages/prisma/migrations/20230920052232_document_meta/migration.sql @@ -0,0 +1,14 @@ +-- AlterTable +ALTER TABLE "Document" ADD COLUMN "documentMetaId" TEXT; + +-- CreateTable +CREATE TABLE "DocumentMeta" ( + "id" TEXT NOT NULL, + "customEmailSubject" TEXT, + "customEmailBody" TEXT, + + CONSTRAINT "DocumentMeta_pkey" PRIMARY KEY ("id") +); + +-- AddForeignKey +ALTER TABLE "Document" ADD CONSTRAINT "Document_documentMetaId_fkey" FOREIGN KEY ("documentMetaId") REFERENCES "DocumentMeta"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/packages/prisma/migrations/20230920124941_fix_documentmeta_relation/migration.sql b/packages/prisma/migrations/20230920124941_fix_documentmeta_relation/migration.sql new file mode 100644 index 000000000..69b4591c5 --- /dev/null +++ b/packages/prisma/migrations/20230920124941_fix_documentmeta_relation/migration.sql @@ -0,0 +1,8 @@ +/* + Warnings: + + - A unique constraint covering the columns `[documentMetaId]` on the table `Document` will be added. If there are existing duplicate values, this will fail. + +*/ +-- CreateIndex +CREATE UNIQUE INDEX "Document_documentMetaId_key" ON "Document"("documentMetaId"); diff --git a/packages/prisma/migrations/20230922121421_fix_document_meta_schema/migration.sql b/packages/prisma/migrations/20230922121421_fix_document_meta_schema/migration.sql new file mode 100644 index 000000000..42c20c112 --- /dev/null +++ b/packages/prisma/migrations/20230922121421_fix_document_meta_schema/migration.sql @@ -0,0 +1,52 @@ +/* + Warnings: + + - You are about to drop the column `documentMetaId` on the `Document` table. All the data in the column will be lost. + - You are about to drop the column `customEmailBody` on the `DocumentMeta` table. All the data in the column will be lost. + - You are about to drop the column `customEmailSubject` on the `DocumentMeta` table. All the data in the column will be lost. + - A unique constraint covering the columns `[documentId]` on the table `DocumentMeta` will be added. If there are existing duplicate values, this will fail. + - Added the required column `documentId` to the `DocumentMeta` table without a default value. This is not possible if the table is not empty. + +*/ +-- DropForeignKey +ALTER TABLE "Document" DROP CONSTRAINT "Document_documentMetaId_fkey"; + +-- DropIndex +DROP INDEX "Document_documentMetaId_key"; + +-- AlterTable +ALTER TABLE "DocumentMeta" +ADD COLUMN "documentId" INTEGER, +ADD COLUMN "message" TEXT, +ADD COLUMN "subject" TEXT; + +-- Migrate data +UPDATE "DocumentMeta" SET "documentId" = ( + SELECT "id" FROM "Document" WHERE "Document"."documentMetaId" = "DocumentMeta"."id" +); + +-- Migrate data +UPDATE "DocumentMeta" SET "message" = "customEmailBody"; + +-- Migrate data +UPDATE "DocumentMeta" SET "subject" = "customEmailSubject"; + +-- Prune data +DELETE FROM "DocumentMeta" WHERE "documentId" IS NULL; + +-- AlterTable +ALTER TABLE "Document" DROP COLUMN "documentMetaId"; + +-- AlterTable +ALTER TABLE "DocumentMeta" +DROP COLUMN "customEmailBody", +DROP COLUMN "customEmailSubject"; + +-- AlterColumn +ALTER TABLE "DocumentMeta" ALTER COLUMN "documentId" SET NOT NULL; + +-- CreateIndex +CREATE UNIQUE INDEX "DocumentMeta_documentId_key" ON "DocumentMeta"("documentId"); + +-- AddForeignKey +ALTER TABLE "DocumentMeta" ADD CONSTRAINT "DocumentMeta_documentId_fkey" FOREIGN KEY ("documentId") REFERENCES "Document"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/packages/prisma/package.json b/packages/prisma/package.json index 3ef12787a..958bcde17 100644 --- a/packages/prisma/package.json +++ b/packages/prisma/package.json @@ -9,10 +9,18 @@ "format": "prisma format", "prisma:generate": "prisma generate", "prisma:migrate-dev": "prisma migrate dev", - "prisma:migrate-deploy": "prisma migrate deploy" + "prisma:migrate-deploy": "prisma migrate deploy", + "prisma:seed": "prisma db seed" + }, + "prisma": { + "seed": "ts-node --transpileOnly --skipProject ./seed-database.ts" }, "dependencies": { - "@prisma/client": "5.0.0", - "prisma": "5.0.0" + "@prisma/client": "5.3.1", + "prisma": "5.3.1" + }, + "devDependencies": { + "ts-node": "^10.9.1", + "typescript": "^5.1.6" } } diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma index 1ff3d7a75..6576da8e2 100644 --- a/packages/prisma/schema.prisma +++ b/packages/prisma/schema.prisma @@ -19,19 +19,29 @@ enum Role { } model User { - id Int @id @default(autoincrement()) - name String? - email String @unique - emailVerified DateTime? - password String? - source String? - signature String? - roles Role[] @default([USER]) - identityProvider IdentityProvider @default(DOCUMENSO) - accounts Account[] - sessions Session[] - Document Document[] - Subscription Subscription[] + id Int @id @default(autoincrement()) + name String? + email String @unique + emailVerified DateTime? + password String? + source String? + signature String? + roles Role[] @default([USER]) + identityProvider IdentityProvider @default(DOCUMENSO) + accounts Account[] + sessions Session[] + Document Document[] + Subscription Subscription[] + PasswordResetToken PasswordResetToken[] +} + +model PasswordResetToken { + id Int @id @default(autoincrement()) + token String @unique + createdAt DateTime @default(now()) + expiry DateTime + userId Int + User User @relation(fields: [userId], references: [id]) } enum SubscriptionStatus { @@ -100,6 +110,7 @@ model Document { Field Field[] documentDataId String documentData DocumentData @relation(fields: [documentDataId], references: [id], onDelete: Cascade) + documentMeta DocumentMeta? createdAt DateTime @default(now()) updatedAt DateTime @default(now()) @updatedAt @@ -120,6 +131,14 @@ model DocumentData { Document Document? } +model DocumentMeta { + id String @id @default(cuid()) + subject String? + message String? + documentId Int @unique + document Document @relation(fields: [documentId], references: [id], onDelete: Cascade) +} + enum ReadStatus { NOT_OPENED OPENED diff --git a/packages/prisma/seed-database.ts b/packages/prisma/seed-database.ts new file mode 100644 index 000000000..65daa357e --- /dev/null +++ b/packages/prisma/seed-database.ts @@ -0,0 +1,82 @@ +import { DocumentDataType, Role } from '@prisma/client'; +import fs from 'node:fs'; +import path from 'node:path'; + +import { hashSync } from '@documenso/lib/server-only/auth/hash'; + +import { prisma } from './index'; + +const seedDatabase = async () => { + const examplePdf = fs + .readFileSync(path.join(__dirname, '../../assets/example.pdf')) + .toString('base64'); + + const exampleUser = await prisma.user.upsert({ + where: { + email: 'example@documenso.com', + }, + create: { + name: 'Example User', + email: 'example@documenso.com', + password: hashSync('password'), + roles: [Role.USER], + }, + update: {}, + }); + + const adminUser = await prisma.user.upsert({ + where: { + email: 'admin@documenso.com', + }, + create: { + name: 'Admin User', + email: 'admin@documenso.com', + password: hashSync('password'), + roles: [Role.USER, Role.ADMIN], + }, + update: {}, + }); + + const examplePdfData = await prisma.documentData.upsert({ + where: { + id: 'clmn0kv5k0000pe04vcqg5zla', + }, + create: { + id: 'clmn0kv5k0000pe04vcqg5zla', + type: DocumentDataType.BYTES_64, + data: examplePdf, + initialData: examplePdf, + }, + update: {}, + }); + + await prisma.document.upsert({ + where: { + id: 1, + }, + create: { + id: 1, + title: 'Example Document', + documentDataId: examplePdfData.id, + userId: exampleUser.id, + Recipient: { + create: { + name: String(adminUser.name), + email: adminUser.email, + token: Math.random().toString(36).slice(2, 9), + }, + }, + }, + update: {}, + }); +}; + +seedDatabase() + .then(() => { + console.log('Database seeded'); + process.exit(0); + }) + .catch((error) => { + console.error(error); + process.exit(1); + }); diff --git a/packages/prisma/types/document-with-data.ts b/packages/prisma/types/document-with-data.ts index d52987552..d8dd8a888 100644 --- a/packages/prisma/types/document-with-data.ts +++ b/packages/prisma/types/document-with-data.ts @@ -1,5 +1,6 @@ -import { Document, DocumentData } from '@documenso/prisma/client'; +import { Document, DocumentData, DocumentMeta } from '@documenso/prisma/client'; export type DocumentWithData = Document & { documentData?: DocumentData | null; + documentMeta?: DocumentMeta | null; }; diff --git a/packages/trpc/server/profile-router/router.ts b/packages/trpc/server/profile-router/router.ts index 1ca8a0cf2..bbeff675b 100644 --- a/packages/trpc/server/profile-router/router.ts +++ b/packages/trpc/server/profile-router/router.ts @@ -1,10 +1,17 @@ import { TRPCError } from '@trpc/server'; +import { forgotPassword } from '@documenso/lib/server-only/user/forgot-password'; +import { resetPassword } from '@documenso/lib/server-only/user/reset-password'; import { updatePassword } from '@documenso/lib/server-only/user/update-password'; import { updateProfile } from '@documenso/lib/server-only/user/update-profile'; -import { authenticatedProcedure, router } from '../trpc'; -import { ZUpdatePasswordMutationSchema, ZUpdateProfileMutationSchema } from './schema'; +import { authenticatedProcedure, procedure, router } from '../trpc'; +import { + ZForgotPasswordFormSchema, + ZResetPasswordFormSchema, + ZUpdatePasswordMutationSchema, + ZUpdateProfileMutationSchema, +} from './schema'; export const profileRouter = router({ updateProfile: authenticatedProcedure @@ -53,4 +60,38 @@ export const profileRouter = router({ }); } }), + + forgotPassword: procedure.input(ZForgotPasswordFormSchema).mutation(async ({ input }) => { + try { + const { email } = input; + + return await forgotPassword({ + email, + }); + } catch (err) { + console.error(err); + } + }), + + resetPassword: procedure.input(ZResetPasswordFormSchema).mutation(async ({ input }) => { + try { + const { password, token } = input; + + return await resetPassword({ + token, + password, + }); + } catch (err) { + let message = 'We were unable to reset your password. 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 0533d40e5..641227684 100644 --- a/packages/trpc/server/profile-router/schema.ts +++ b/packages/trpc/server/profile-router/schema.ts @@ -5,10 +5,20 @@ export const ZUpdateProfileMutationSchema = z.object({ signature: z.string(), }); -export type TUpdateProfileMutationSchema = z.infer; - export const ZUpdatePasswordMutationSchema = z.object({ password: z.string().min(6), }); +export const ZForgotPasswordFormSchema = z.object({ + email: z.string().email().min(1), +}); + +export const ZResetPasswordFormSchema = z.object({ + password: z.string().min(6), + token: z.string().min(1), +}); + +export type TUpdateProfileMutationSchema = z.infer; export type TUpdatePasswordMutationSchema = z.infer; +export type TForgotPasswordFormSchema = z.infer; +export type TResetPasswordFormSchema = z.infer; diff --git a/packages/tsconfig/process-env.d.ts b/packages/tsconfig/process-env.d.ts index b0852b4f4..d32b9984a 100644 --- a/packages/tsconfig/process-env.d.ts +++ b/packages/tsconfig/process-env.d.ts @@ -1,6 +1,7 @@ declare namespace NodeJS { export interface ProcessEnv { - NEXT_PUBLIC_SITE_URL?: string; + NEXT_PUBLIC_WEBAPP_URL?: string; + NEXT_PUBLIC_MARKETING_URL?: string; NEXT_PRIVATE_GOOGLE_CLIENT_ID?: string; NEXT_PRIVATE_GOOGLE_CLIENT_SECRET?: string; @@ -40,5 +41,19 @@ declare namespace NodeJS { NEXT_PRIVATE_SMTP_FROM_NAME?: string; NEXT_PRIVATE_SMTP_FROM_ADDRESS?: string; + + /** + * Vercel environment variables + */ + VERCEL?: string; + VERCEL_ENV?: 'production' | 'development' | 'preview'; + VERCEL_URL?: string; + + DEPLOYMENT_TARGET?: 'webapp' | 'marketing'; + + POSTGRES_URL?: string; + DATABASE_URL?: string; + POSTGRES_PRISMA_URL?: string; + POSTGRES_URL_NON_POOLING?: string; } } diff --git a/packages/ui/primitives/document-flow/add-subject.tsx b/packages/ui/primitives/document-flow/add-subject.tsx index 312a4eea2..1bf3b2cb4 100644 --- a/packages/ui/primitives/document-flow/add-subject.tsx +++ b/packages/ui/primitives/document-flow/add-subject.tsx @@ -2,7 +2,8 @@ import { useForm } from 'react-hook-form'; -import { Document, DocumentStatus, Field, Recipient } from '@documenso/prisma/client'; +import { DocumentStatus, Field, Recipient } from '@documenso/prisma/client'; +import { DocumentWithData } from '@documenso/prisma/types/document-with-data'; import { FormErrorMessage } from '@documenso/ui/primitives/form/form-error-message'; import { Input } from '@documenso/ui/primitives/input'; import { Label } from '@documenso/ui/primitives/label'; @@ -21,7 +22,7 @@ export type AddSubjectFormProps = { documentFlow: DocumentFlowStep; recipients: Recipient[]; fields: Field[]; - document: Document; + document: DocumentWithData; numberOfSteps: number; onSubmit: (_data: TAddSubjectFormSchema) => void; }; @@ -41,8 +42,8 @@ export const AddSubjectFormPartial = ({ } = useForm({ defaultValues: { email: { - subject: '', - message: '', + subject: document.documentMeta?.subject ?? '', + message: document.documentMeta?.message ?? '', }, }, }); diff --git a/scripts/remap-vercel-env.cjs b/scripts/remap-vercel-env.cjs new file mode 100644 index 000000000..95e60cde8 --- /dev/null +++ b/scripts/remap-vercel-env.cjs @@ -0,0 +1,45 @@ +/** @typedef {import('@documenso/tsconfig/process-env')} */ + +/** + * Remap Vercel environment variables to our defined Next.js environment variables. + * + * @deprecated This is no longer needed because we can't inject runtime environment variables via next.config.js + * + * @returns {void} + */ +const remapVercelEnv = () => { + if (!process.env.VERCEL || !process.env.DEPLOYMENT_TARGET) { + return; + } + + if (process.env.POSTGRES_URL) { + process.env.NEXT_PRIVATE_DATABASE_URL = process.env.POSTGRES_URL; + } + + if (process.env.POSTGRES_URL_NON_POOLING) { + process.env.NEXT_PRIVATE_DIRECT_DATABASE_URL = process.env.POSTGRES_URL_NON_POOLING; + } + + // If we're using a connection pool, we need to let Prisma know that + // we're using PgBouncer. + if (process.env.NEXT_PRIVATE_DATABASE_URL !== process.env.NEXT_PRIVATE_DIRECT_DATABASE_URL) { + const url = new URL(process.env.NEXT_PRIVATE_DATABASE_URL); + + url.searchParams.set('pgbouncer', 'true'); + + process.env.NEXT_PRIVATE_DATABASE_URL = url.toString(); + } + + if (process.env.VERCEL_ENV !== 'production' && process.env.DEPLOYMENT_TARGET === 'webapp') { + process.env.NEXTAUTH_URL = `https://${process.env.VERCEL_URL}`; + process.env.NEXT_PUBLIC_WEBAPP_URL = `https://${process.env.VERCEL_URL}`; + } + + if (process.env.VERCEL_ENV !== 'production' && process.env.DEPLOYMENT_TARGET === 'marketing') { + process.env.NEXT_PUBLIC_MARKETING_URL = `https://${process.env.VERCEL_URL}`; + } +}; + +module.exports = { + remapVercelEnv, +}; diff --git a/scripts/vercel.sh b/scripts/vercel.sh new file mode 100755 index 000000000..e4ab23622 --- /dev/null +++ b/scripts/vercel.sh @@ -0,0 +1,107 @@ +#!/usr/bin/env bash + +# Exit on error. +set -eo pipefail + +# Get the directory of this script, regardless of where it is called from. +SCRIPT_DIR="$(readlink -f "$(dirname "$0")")" + + +function log() { + echo "[VercelBuild]: $1" +} + +function build_webapp() { + log "Building webapp for $VERCEL_ENV" + + remap_database_integration + + npm run prisma:generate --workspace=@documenso/prisma + npm run prisma:migrate-deploy --workspace=@documenso/prisma + + if [[ "$VERCEL_ENV" != "production" ]]; then + log "Seeding database for $VERCEL_ENV" + + npm run prisma:seed --workspace=@documenso/prisma + fi + + npm run build -- --filter @documenso/web +} + +function remap_webapp_env() { + if [[ "$VERCEL_ENV" != "production" ]]; then + log "Remapping webapp environment variables for $VERCEL_ENV" + + export NEXTAUTH_URL="https://$VERCEL_URL" + export NEXT_PUBLIC_WEBAPP_URL="https://$VERCEL_URL" + fi +} + +function build_marketing() { + log "Building marketing for $VERCEL_ENV" + + remap_database_integration + + npm run prisma:generate --workspace=@documenso/prisma + npm run build -- --filter @documenso/marketing +} + +function remap_marketing_env() { + if [[ "$VERCEL_ENV" != "production" ]]; then + log "Remapping marketing environment variables for $VERCEL_ENV" + + export NEXT_PUBLIC_MARKETING_URL="https://$VERCEL_URL" + fi +} + +function remap_database_integration() { + log "Remapping Supabase integration for $VERCEL_ENV" + + if [[ ! -z "$POSTGRES_URL" ]]; then + export NEXT_PRIVATE_DATABASE_URL="$POSTGRES_URL" + export NEXT_PRIVATE_DIRECT_DATABASE_URL="$POSTGRES_URL" + fi + + if [[ ! -z "$DATABASE_URL" ]]; then + export NEXT_PRIVATE_DATABASE_URL="$DATABASE_URL" + export NEXT_PRIVATE_DIRECT_DATABASE_URL="$DATABASE_URL" + fi + + if [[ ! -z "$POSTGRES_URL_NON_POOLING" ]]; then + export NEXT_PRIVATE_DATABASE_URL="$POSTGRES_URL?pgbouncer=true" + export NEXT_PRIVATE_DIRECT_DATABASE_URL="$POSTGRES_URL_NON_POOLING" + fi + + + if [[ "$NEXT_PRIVATE_DATABASE_URL" == *"neon.tech"* ]]; then + log "Remapping for Neon integration" + + PROJECT_ID="$(echo "$PGHOST" | cut -d'.' -f1)" + PGBOUNCER_HOST="$(echo "$PGHOST" | sed "s/${PROJECT_ID}/${PROJECT_ID}-pooler/")" + + export NEXT_PRIVATE_DATABASE_URL="postgres://${PGUSER}:${PGPASSWORD}@${PGBOUNCER_HOST}/${PGDATABASE}?pgbouncer=true" + fi +} + +# Navigate to the root of the project. +cd "$SCRIPT_DIR/.." + +# Check if the script is running on Vercel. +if [[ -z "$VERCEL" ]]; then + log "ERROR - This script must be run as part of the Vercel build process." + exit 1 +fi + +case "$DEPLOYMENT_TARGET" in + "webapp") + build_webapp + ;; + "marketing") + build_marketing + ;; + *) + log "ERROR - Missing or invalid DEPLOYMENT_TARGET environment variable." + log "ERROR - DEPLOYMENT_TARGET must be either 'webapp' or 'marketing'." + exit 1 + ;; +esac diff --git a/turbo.json b/turbo.json index a5b333c66..01b8bd487 100644 --- a/turbo.json +++ b/turbo.json @@ -2,8 +2,13 @@ "$schema": "https://turbo.build/schema.json", "pipeline": { "build": { - "dependsOn": ["^build"], - "outputs": [".next/**", "!.next/cache/**"] + "dependsOn": [ + "^build" + ], + "outputs": [ + ".next/**", + "!.next/cache/**" + ] }, "lint": {}, "dev": { @@ -11,20 +16,22 @@ "persistent": true } }, - "globalDependencies": ["**/.env.*local"], + "globalDependencies": [ + "**/.env.*local" + ], "globalEnv": [ "APP_VERSION", "NEXTAUTH_URL", "NEXTAUTH_SECRET", - "NEXT_PUBLIC_APP_URL", - "NEXT_PUBLIC_SITE_URL", + "NEXT_PUBLIC_WEBAPP_URL", + "NEXT_PUBLIC_MARKETING_URL", "NEXT_PUBLIC_POSTHOG_KEY", "NEXT_PUBLIC_POSTHOG_HOST", "NEXT_PUBLIC_FEATURE_BILLING_ENABLED", "NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_YEARLY_PRICE_ID", "NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID", "NEXT_PRIVATE_DATABASE_URL", - "NEXT_PRIVATE_NEXT_AUTH_SECRET", + "NEXT_PRIVATE_DIRECT_DATABASE_URL", "NEXT_PRIVATE_GOOGLE_CLIENT_ID", "NEXT_PRIVATE_GOOGLE_CLIENT_SECRET", "NEXT_PUBLIC_UPLOAD_TRANSPORT", @@ -48,6 +55,15 @@ "NEXT_PRIVATE_SMTP_SECURE", "NEXT_PRIVATE_SMTP_FROM_NAME", "NEXT_PRIVATE_SMTP_FROM_ADDRESS", - "NEXT_PRIVATE_STRIPE_API_KEY" + "NEXT_PRIVATE_STRIPE_API_KEY", + + "VERCEL", + "VERCEL_ENV", + "VERCEL_URL", + "DEPLOYMENT_TARGET", + "POSTGRES_URL", + "DATABASE_URL", + "POSTGRES_PRISMA_URL", + "POSTGRES_URL_NON_POOLING" ] }