diff --git a/.env.example b/.env.example index 3dc0985cb..065976bc5 100644 --- a/.env.example +++ b/.env.example @@ -50,7 +50,9 @@ NEXT_PRIVATE_SMTP_SECURE= NEXT_PRIVATE_SMTP_FROM_NAME="No Reply @ Documenso" # REQUIRED: Defines the email address to use as the from address. NEXT_PRIVATE_SMTP_FROM_ADDRESS="noreply@documenso.com" -# OPTIONAL: The API key to use for the MailChannels proxy endpoint. +# OPTIONAL: The API key to use for Resend.com +NEXT_PRIVATE_RESEND_API_KEY= +# OPTIONAL: The API key to use for MailChannels. NEXT_PRIVATE_MAILCHANNELS_API_KEY= # OPTIONAL: The endpoint to use for the MailChannels API if using a proxy. NEXT_PRIVATE_MAILCHANNELS_ENDPOINT= diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 9a564b058..74fcb319b 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -9,7 +9,7 @@ updates: labels: - "ci dependencies" - "ci" - open-pull-requests-limit: 10 + open-pull-requests-limit: 2 - package-ecosystem: "npm" directory: "/apps/marketing" @@ -19,7 +19,7 @@ updates: labels: - "npm dependencies" - "frontend" - open-pull-requests-limit: 10 + open-pull-requests-limit: 2 - package-ecosystem: "npm" directory: "/apps/web" @@ -29,4 +29,4 @@ updates: labels: - "npm dependencies" - "frontend" - open-pull-requests-limit: 10 + open-pull-requests-limit: 2 diff --git a/README.md b/README.md index 29ffb0d65..aa00aed54 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@

- Documenso Logo + Documenso Logo

@@ -26,37 +26,30 @@ Commits-per-month

-> **🚧 We're currently working on a large scale refactor which can be found on the [feat/refresh](https://github.com/documenso/documenso/tree/feat/refresh) branch.** +> 🦺 Documenso 1.0 is deployed to our Staging Environment. > -> **[Read more on why 👀](https://documenso.com/blog/why-were-doing-a-rewrite)** +> The code can be found on the [feat/refresh](https://github.com/documenso/documenso/tree/feat/refresh) branch. +> +> The new Version will be released after the current testing phase. -# Documenso 0.9 - Developer Preview +# Join us in testing Documenso 1.0 during [MALFUNCTION MANIA](https://documenso.com/blog/malfunction-mania)
+ src="https://github.com/documenso/documenso/assets/1309312/67e08c98-c153-4115-aa2d-77979bb12c94)"> + src="https://github.com/documenso/documenso/assets/1309312/040cfbae-3438-4ca3-87f2-ce52c793dcaf"> + src="https://github.com/documenso/documenso/assets/1309312/72d445be-41e5-4936-bdba-87ef8e70fa09"> + src="https://github.com/documenso/documenso/assets/1309312/d7b86c0f-a755-4476-a022-a608db2c4633"> - - - + src=https://github.com/documenso/documenso/assets/1309312/c0f55116-ab82-433f-a266-f3fc8571d69f">
-> **Note** -> This project is currently under community review and will publish it's first production release soon™. - ## About this project -Signing documents digitally is fast, easy and should be best practice for every document signed worldwide. This is technically quite easy today, but it also introduces a new party to every signature: The signing tool providers. While this is not a problem in itself, it should make us think about how we want these providers of trust to work. Documenso aims to be the world's most trusted document signing tool. This trust is built by empowering you to self-host Documenso and review how it works under the hood. Join us in creating the next generation of open trust infrastructure. +Signing documents digitally is fast and easy and should be the best practice for every document signed worldwide. This is technically quite easy today, but it also introduces a new party to every signature: The signing tool providers. While this is not a problem in itself, it should make us think about how we want these providers of trust to work. Documenso aims to be the world's most trusted document-signing tool. This trust is built by empowering you to self-host Documenso and review how it works under the hood. Join us in creating the next generation of open trust infrastructure. ## Recognition @@ -65,13 +58,13 @@ Signing documents digitally is fast, easy and should be best practice for every ## Community and Next Steps 🎯 -We're currently working on a redesign of the application including a revamp of the codebase so Documenso can be more intuitive to use and robust to develop upon. +We're currently working on a redesign of the application, including a revamp of the codebase so Documenso can be more intuitive to use and robust to develop upon. - Check out the first source code release in this repository and test it - Tell us what you think in the current [Discussions](https://github.com/documenso/documenso/discussions) - Join the [Discord server](https://documen.so/discord) for any questions and getting to know to other community members - ⭐ the repository to help us raise awareness -- Spread the word on Twitter, that Documenso is working towards a more open signing tool +- Spread the word on Twitter that Documenso is working towards a more open signing tool - Fix or create [issues](https://github.com/documenso/documenso/issues), that are needed for the first production release ## Contributing diff --git a/apps/marketing/content/blog/malfunction-mania.mdx b/apps/marketing/content/blog/malfunction-mania.mdx new file mode 100644 index 000000000..c1f5732f0 --- /dev/null +++ b/apps/marketing/content/blog/malfunction-mania.mdx @@ -0,0 +1,57 @@ +--- +title: Announcing Malfunction Mania +description: Launch Week Day 2 Y'all! We're getting ready to release Documenso 1.0! Join in on the fun of making sure the open-source alternative to DocuSign is on point. We're calling a 'MALFUNCTION MANIA.' +authorName: 'Timur Ercan' +authorImage: '/blog/blog-author-timur.jpeg' +authorRole: 'Co-Founder' +date: 2023-09-26 +tags: + - Testing + - Rewrite + - Bounties +--- + +
+ + +
+ We're calling a MALFUNCTION MANIA! 🚨 +
+
+ +> TLDR; Documenso 1.0 is in the [staging environment](https://documen.so/staging). Go check it out. + +It's been a minute since Lucas proclaimed, ["We're doing a rewrite"](https://documenso.com/blog/why-were-doing-a-rewrite), and many of you have been asking when the new version will be available. I'm happy to say that the wait has come to an end. The work on Documenso 1.0 has reached a level we feel comfortable going into the next phase with. We had a lot of community feedback, contributions, and moral support to get us this far, which is why we're excited to announce the most extensive community project yet: + +As Documenso 1.0 just hit the staging environment, we're calling a MALFUNCTION MANIA. An enormous, public testing phase, where we invite everyone to try out the new version, hunt down, report, and fix any malfunctions (aka bugs), and give feedback before release. Malfunction Mania will happen alongside our internal testing, and by combining the two, we want to ensure the best possible release we can have for Version 1.0. We know many of you have been eager to contribute; this is your chance (the first of many to come). + +## As part of Malfunction Mania, we're offering special bug bounties + +- We award $25 - $100 per report/ issue/ fix, depending on the severity and if the problem is already known +- Bounties will be awarded for fixing reported or other critical issues via accepted Pull Requests (PR) +- Just reporting issues in a reproducible way can also be awarded +- Smaller but notable contributions like minor issues and documentation will be awarded with exclusive merch as we see fit. + +## What you can do + +- Head over to the [staging environment](https://documen.so/staging), check out the new version and give it a spin; +- Check out the [source code](https://github.com/documenso/documenso) on GitHub and look it over; +- Spin up the new version locally and try it out. + +## How to get the bounties + +- Report bugs by creating an issue here: [documen.so/issues](https://documen.so/issues); +- Fix bugs by creating a Pull Request (PR); +- Look over and add missing documentation/ Quickstarts and other useful resources. + +We don't have a specific end date for Malfunction Mania. We plan to move the staging version into the production environment by the end of the month once we're happy with the results. Bug reports and fixes are, of course, always welcome going forward. + +Best from Hamburg +Timur + +**[Follow Documenso on Twitter / X](https://documen.so/tw) and [join the Discord server](https://documen.so/discord) to get the latest about Malfunction Mania.** diff --git a/apps/marketing/content/blog/next.mdx b/apps/marketing/content/blog/next.mdx index c241cf3eb..2f83fe3ef 100644 --- a/apps/marketing/content/blog/next.mdx +++ b/apps/marketing/content/blog/next.mdx @@ -20,11 +20,11 @@ Today, I'm pleased to share with you a preview of the next Documenso. We redesigned the whole signing flow to make it more appealing and more convenient. -We improved the overall look and feel by making it more elegant and appropriately playful. Focused on the task at hand, but explicitly enjoying doing it. +We improved the overall look and feel by making it more elegant and appropriately playful. Focused on the task at hand, but explicitly enjoying doing it. **We call it happy minimalism.** -We paid particular attention to the moment of signing, which should be celebrated. +We paid particular attention to the moment of signing, which should be celebrated. The image below is the final bloom of the completion celebration we added: diff --git a/apps/marketing/content/blog/shop.mdx b/apps/marketing/content/blog/shop.mdx new file mode 100644 index 000000000..fafd98a40 --- /dev/null +++ b/apps/marketing/content/blog/shop.mdx @@ -0,0 +1,64 @@ +--- +title: Merch Mania +description: Happy Launch Week Day 3. The limited edition "Malfunction Mania" shirt is here. Grab it, while you can. +authorName: 'Timur Ercan' +authorImage: '/blog/blog-author-timur.jpeg' +authorRole: 'Co-Founder' +date: 2023-09-27 +tags: + - Merch + - Rewrite + - Bounties +--- + +
+ + +
+ The Limited Edition "Malfunction Mania" Shirt - Only during Malfunction Mania +
+
+ +> TLDR; We have a fancy limited edition shirt. Contribute to [Malfunction Mania](https://documenso.com/blog/malfunction-mania) to get one. + +We kicked off [Malfunction Mania](https://documenso.com/blog/malfunction-mania) yesterday, and the first [issues](https://github.com/documenso/documenso/issues) are coming in. As mentioned, there will be dollar bounties, but we also wanted to celebrate entering the final stage of version 1.0 with something special. This is why we created this limited edition shirt. It will only be available during the runtime of Malfunction Mania. We have yet to set an exact end date, the next event in October, however, is looming, ready to end MM. + +## Documenso Merch Shop + +The shirt will be available in our [merch shop](https://documen.so/shop) via a unique discount code. While the shirt will be gone after Malfunction Mania, the shop is here to stay and provide a well-deserved reward for great community members and contributors. All items can be earned by contrinuting to Documenso. + +
+ + +
+ Merch at Documenso is always given to those who deserve it. +
+
+ +## How earn the shirt + +If you have been following us, you know we are not big on formalities but highly value rewarding merit. That being said, any worthwhile contribution has a chance to get one. To inspire, here are a few ideas on how to contribute to securing one: + +- Report a bug with detailed reproduction details +- Fix a bug (you or somebody else reported) +- Analyze and describe a usability or user experience shortcoming +- Test the product in a systematic and least somewhat documented way +- Engage in discussion about the current version and its choices +- Raise awareness for Malfunction Mania and try out the [version currently in staging](https://documen.so/staging) +- Review the version with a video, stream, or screenshots and post about it +- Review existing or create missing documentation + +Best from Hamburg +Timur + +**[Follow Documenso on Twitter / X](https://documen.so/tw) and [join the Discord server](https://documen.so/discord) to get the latest updates about Malfunction Mania.** diff --git a/apps/marketing/next.config.js b/apps/marketing/next.config.js index e74f7d545..aeb06bdfd 100644 --- a/apps/marketing/next.config.js +++ b/apps/marketing/next.config.js @@ -56,6 +56,14 @@ const config = { }, ]; }, + async rewrites() { + return [ + { + source: '/ingest/:path*', + destination: 'https://eu.posthog.com/:path*', + }, + ]; + }, }; module.exports = withContentlayer(config); diff --git a/apps/marketing/public/blog/blog-author-lucas.png b/apps/marketing/public/blog/blog-author-lucas.png new file mode 100644 index 000000000..7fa901359 Binary files /dev/null and b/apps/marketing/public/blog/blog-author-lucas.png differ diff --git a/apps/marketing/public/blog/blog-banner-rewrite.png b/apps/marketing/public/blog/blog-banner-rewrite.png new file mode 100644 index 000000000..922b800cf Binary files /dev/null and b/apps/marketing/public/blog/blog-banner-rewrite.png differ diff --git a/apps/marketing/public/blog/designsystem.png b/apps/marketing/public/blog/designsystem.png new file mode 100644 index 000000000..dbbaa9e0f Binary files /dev/null and b/apps/marketing/public/blog/designsystem.png differ diff --git a/apps/marketing/public/blog/mania-shirt.png b/apps/marketing/public/blog/mania-shirt.png new file mode 100644 index 000000000..eab20119e Binary files /dev/null and b/apps/marketing/public/blog/mania-shirt.png differ diff --git a/apps/marketing/public/blog/mm.png b/apps/marketing/public/blog/mm.png new file mode 100644 index 000000000..19477f0f7 Binary files /dev/null and b/apps/marketing/public/blog/mm.png differ diff --git a/apps/marketing/public/blog/shop.png b/apps/marketing/public/blog/shop.png new file mode 100644 index 000000000..d13e88748 Binary files /dev/null and b/apps/marketing/public/blog/shop.png differ diff --git a/apps/marketing/src/components/(marketing)/single-player-mode/single-player-mode-success.tsx b/apps/marketing/src/components/(marketing)/single-player-mode/single-player-mode-success.tsx index 70bf58926..0c85fc65c 100644 --- a/apps/marketing/src/components/(marketing)/single-player-mode/single-player-mode-success.tsx +++ b/apps/marketing/src/components/(marketing)/single-player-mode/single-player-mode-success.tsx @@ -73,7 +73,7 @@ export const SinglePlayerModeSuccess = ({ className, document }: SinglePlayerMod )} -

+

You have signed {document.title}

@@ -84,17 +84,17 @@ export const SinglePlayerModeSuccess = ({ className, document }: SinglePlayerMod signingCelebrationImage={signingCelebration} /> -
+
{/* TODO: Hook this up */} - -

+

Loading Document...

diff --git a/apps/web/src/app/(dashboard)/documents/[id]/page.tsx b/apps/web/src/app/(dashboard)/documents/[id]/page.tsx index 3d6dbb954..d44ac2800 100644 --- a/apps/web/src/app/(dashboard)/documents/[id]/page.tsx +++ b/apps/web/src/app/(dashboard)/documents/[id]/page.tsx @@ -30,11 +30,11 @@ export default async function DocumentPage({ params }: DocumentPageProps) { redirect('/documents'); } - const session = await getRequiredServerComponentSession(); + const { user } = await getRequiredServerComponentSession(); const document = await getDocumentById({ id: documentId, - userId: session.id, + userId: user.id, }).catch(() => null); if (!document || !document.documentData) { @@ -50,11 +50,11 @@ export default async function DocumentPage({ params }: DocumentPageProps) { const [recipients, fields] = await Promise.all([ await getRecipientsForDocument({ documentId, - userId: session.id, + userId: user.id, }), await getFieldsForDocument({ documentId, - userId: session.id, + userId: user.id, }), ]); @@ -65,10 +65,7 @@ export default async function DocumentPage({ params }: DocumentPageProps) { Documents -

+

{document.title}

@@ -90,7 +87,7 @@ export default async function DocumentPage({ params }: DocumentPageProps) { -

+

Loading Document...

diff --git a/apps/web/src/app/(dashboard)/documents/page.tsx b/apps/web/src/app/(dashboard)/documents/page.tsx index a9cffe546..d200fe262 100644 --- a/apps/web/src/app/(dashboard)/documents/page.tsx +++ b/apps/web/src/app/(dashboard)/documents/page.tsx @@ -25,7 +25,7 @@ export type DocumentsPageProps = { }; export default async function DocumentsPage({ searchParams = {} }: DocumentsPageProps) { - const user = await getRequiredServerComponentSession(); + const { user } = await getRequiredServerComponentSession(); const stats = await getStats({ user, diff --git a/apps/web/src/app/(dashboard)/layout.tsx b/apps/web/src/app/(dashboard)/layout.tsx index b1f7d1a1a..19f27b788 100644 --- a/apps/web/src/app/(dashboard)/layout.tsx +++ b/apps/web/src/app/(dashboard)/layout.tsx @@ -24,7 +24,7 @@ export default async function AuthenticatedDashboardLayout({ redirect('/signin'); } - const user = await getRequiredServerComponentSession(); + const { user } = await getRequiredServerComponentSession(); return ( diff --git a/apps/web/src/app/(dashboard)/settings/billing/page.tsx b/apps/web/src/app/(dashboard)/settings/billing/page.tsx index 28c8b8122..a5c672971 100644 --- a/apps/web/src/app/(dashboard)/settings/billing/page.tsx +++ b/apps/web/src/app/(dashboard)/settings/billing/page.tsx @@ -12,7 +12,7 @@ import { Button } from '@documenso/ui/primitives/button'; import { LocaleDate } from '~/components/formatter/locale-date'; export default async function BillingSettingsPage() { - const user = await getRequiredServerComponentSession(); + const { user } = await getRequiredServerComponentSession(); const isBillingEnabled = await getServerComponentFlag('app_billing'); diff --git a/apps/web/src/app/(dashboard)/settings/password/page.tsx b/apps/web/src/app/(dashboard)/settings/password/page.tsx index b89e74f3c..90fcbe25d 100644 --- a/apps/web/src/app/(dashboard)/settings/password/page.tsx +++ b/apps/web/src/app/(dashboard)/settings/password/page.tsx @@ -3,7 +3,7 @@ import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get- import { PasswordForm } from '~/components/forms/password'; export default async function PasswordSettingsPage() { - const user = await getRequiredServerComponentSession(); + const { user } = await getRequiredServerComponentSession(); return (
diff --git a/apps/web/src/app/(dashboard)/settings/profile/page.tsx b/apps/web/src/app/(dashboard)/settings/profile/page.tsx index ee0087a9a..716f3c39c 100644 --- a/apps/web/src/app/(dashboard)/settings/profile/page.tsx +++ b/apps/web/src/app/(dashboard)/settings/profile/page.tsx @@ -3,7 +3,7 @@ import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get- import { ProfileForm } from '~/components/forms/profile'; export default async function ProfileSettingsPage() { - const user = await getRequiredServerComponentSession(); + const { user } = await getRequiredServerComponentSession(); return (
diff --git a/apps/web/src/app/(share)/share/[slug]/opengraph-image.tsx b/apps/web/src/app/(share)/share/[slug]/opengraph/route.tsx similarity index 94% rename from apps/web/src/app/(share)/share/[slug]/opengraph-image.tsx rename to apps/web/src/app/(share)/share/[slug]/opengraph/route.tsx index 8751f407e..d6a2c6397 100644 --- a/apps/web/src/app/(share)/share/[slug]/opengraph-image.tsx +++ b/apps/web/src/app/(share)/share/[slug]/opengraph/route.tsx @@ -1,4 +1,4 @@ -import { ImageResponse } from 'next/server'; +import { ImageResponse, NextResponse } from 'next/server'; import { P, match } from 'ts-pattern'; @@ -21,7 +21,7 @@ type SharePageOpenGraphImageProps = { params: { slug: string }; }; -export default async function Image({ params: { slug } }: SharePageOpenGraphImageProps) { +export async function GET(_request: Request, { params: { slug } }: SharePageOpenGraphImageProps) { const [interSemiBold, interRegular, caveatRegular, shareFrameImage] = await Promise.all([ getAssetBuffer('/fonts/inter-semibold.ttf'), getAssetBuffer('/fonts/inter-regular.ttf'), @@ -32,7 +32,7 @@ export default async function Image({ params: { slug } }: SharePageOpenGraphImag const recipientOrSender = await getRecipientOrSenderByShareLinkSlug({ slug }).catch(() => null); if (!recipientOrSender) { - return null; + return NextResponse.json({ error: 'Not found' }, { status: 404 }); } const isRecipient = 'Signature' in recipientOrSender; diff --git a/apps/web/src/app/(share)/share/[slug]/page.tsx b/apps/web/src/app/(share)/share/[slug]/page.tsx index 63449f29f..51684d384 100644 --- a/apps/web/src/app/(share)/share/[slug]/page.tsx +++ b/apps/web/src/app/(share)/share/[slug]/page.tsx @@ -1,11 +1,39 @@ import { Metadata } from 'next'; +import { headers } from 'next/headers'; +import { redirect } from 'next/navigation'; -import { Redirect } from './redirect'; +import { APP_BASE_URL } from '@documenso/lib/constants/app'; -export const metadata: Metadata = { - title: 'Documenso - Share', +type SharePageProps = { + params: { slug: string }; }; -export default function SharePage() { - return ; +export function generateMetadata({ params: { slug } }: SharePageProps) { + return { + title: 'Documenso - Share', + description: 'I just signed a document with Documenso!', + openGraph: { + title: 'Documenso - Join the open source signing revolution', + description: 'I just signed with Documenso!', + type: 'website', + images: [`${APP_BASE_URL}/share/${slug}/opengraph`], + }, + twitter: { + site: '@documenso', + card: 'summary_large_image', + images: [`${APP_BASE_URL}/share/${slug}/opengraph`], + description: 'I just signed with Documenso!', + }, + } satisfies Metadata; +} + +export default function SharePage() { + const userAgent = headers().get('User-Agent') ?? ''; + + // https://stackoverflow.com/questions/47026171/how-to-detect-bots-for-open-graph-with-user-agent + if (/bot|facebookexternalhit|WhatsApp|google|bing|duckduckbot|MetaInspector/i.test(userAgent)) { + return null; + } + + redirect(process.env.NEXT_PUBLIC_MARKETING_URL ?? 'http://localhost:3001'); } diff --git a/apps/web/src/app/(share)/share/[slug]/redirect.tsx b/apps/web/src/app/(share)/share/[slug]/redirect.tsx deleted file mode 100644 index 5b3af0771..000000000 --- a/apps/web/src/app/(share)/share/[slug]/redirect.tsx +++ /dev/null @@ -1,11 +0,0 @@ -'use client'; - -import { useEffect } from 'react'; - -export const Redirect = () => { - useEffect(() => { - window.location.href = process.env.NEXT_PUBLIC_MARKETING_URL ?? 'http://localhost:3001'; - }, []); - - return null; -}; diff --git a/apps/web/src/app/(signing)/sign/[token]/complete/page.tsx b/apps/web/src/app/(signing)/sign/[token]/complete/page.tsx index a89f1bb3f..414a6624b 100644 --- a/apps/web/src/app/(signing)/sign/[token]/complete/page.tsx +++ b/apps/web/src/app/(signing)/sign/[token]/complete/page.tsx @@ -9,7 +9,7 @@ import { getFieldsForToken } from '@documenso/lib/server-only/field/get-fields-f import { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-recipient-by-token'; import { DocumentStatus, FieldType } from '@documenso/prisma/client'; import { DocumentDownloadButton } from '@documenso/ui/components/document/document-download-button'; -import { SigningCard } from '@documenso/ui/components/signing-card'; +import { SigningCard3D } from '@documenso/ui/components/signing-card'; import signingCelebration from '~/assets/signing-celebration.png'; @@ -53,11 +53,11 @@ export default async function CompletedSigningPage({ recipient.email; return ( -
+
{/* Card with recipient */} - + -
+
{match(document.status) .with(DocumentStatus.COMPLETED, () => (
@@ -71,41 +71,44 @@ export default async function CompletedSigningPage({ Waiting for others to sign
))} + +

+ You have signed "{document.title}" +

+ + {match(document.status) + .with(DocumentStatus.COMPLETED, () => ( +

+ Everyone has signed! You will receive an Email copy of the signed document. +

+ )) + .otherwise(() => ( +

+ You will receive an Email copy of the signed document once everyone has signed. +

+ ))} + +
+ + + +
+ +

+ Want to send slick signing links like this one?{' '} + + Check out Documenso. + +

- -

- You have signed "{document.title}" -

- - {match(document.status) - .with(DocumentStatus.COMPLETED, () => ( -

- Everyone has signed! You will receive an Email copy of the signed document. -

- )) - .otherwise(() => ( -

- You will receive an Email copy of the signed document once everyone has signed. -

- ))} - -
- - - -
- -

- Want to send slick signing links like this one?{' '} - - Check out Documenso. - -

); } diff --git a/apps/web/src/app/(signing)/sign/[token]/complete/share-button.tsx b/apps/web/src/app/(signing)/sign/[token]/complete/share-button.tsx index c13b8a7ab..caa27cc50 100644 --- a/apps/web/src/app/(signing)/sign/[token]/complete/share-button.tsx +++ b/apps/web/src/app/(signing)/sign/[token]/complete/share-button.tsx @@ -1,11 +1,20 @@ 'use client'; -import { HTMLAttributes } from 'react'; +import { HTMLAttributes, useState } from 'react'; -import { Share } from 'lucide-react'; +import { Copy, Share, Twitter } from 'lucide-react'; +import { generateTwitterIntent } from '@documenso/lib/universal/generate-twitter-intent'; import { trpc } from '@documenso/trpc/react'; import { Button } from '@documenso/ui/primitives/button'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from '@documenso/ui/primitives/dialog'; import { useToast } from '@documenso/ui/primitives/use-toast'; import { useCopyToClipboard } from '~/hooks/use-copy-to-clipboard'; @@ -19,14 +28,36 @@ export const ShareButton = ({ token, documentId }: ShareButtonProps) => { const { toast } = useToast(); const [, copyToClipboard] = useCopyToClipboard(); - const { mutateAsync: createOrGetShareLink, isLoading } = - trpc.shareLink.createOrGetShareLink.useMutation(); + const [isOpen, setIsOpen] = useState(false); - const onShareClick = async () => { - const { slug } = await createOrGetShareLink({ - token: token, - documentId, - }); + const { + mutateAsync: createOrGetShareLink, + data: shareLink, + isLoading, + } = trpc.shareLink.createOrGetShareLink.useMutation(); + + const onOpenChange = (nextOpen: boolean) => { + if (nextOpen) { + void createOrGetShareLink({ + token, + documentId, + }); + } + + setIsOpen(nextOpen); + }; + + const onCopyClick = async () => { + let { slug = '' } = shareLink || {}; + + if (!slug) { + const result = await createOrGetShareLink({ + token, + documentId, + }); + + slug = result.slug; + } await copyToClipboard(`${window.location.origin}/share/${slug}`).catch(() => null); @@ -34,18 +65,82 @@ export const ShareButton = ({ token, documentId }: ShareButtonProps) => { title: 'Copied to clipboard', description: 'The sharing link has been copied to your clipboard.', }); + + setIsOpen(false); + }; + + const onTweetClick = async () => { + let { slug = '' } = shareLink || {}; + + if (!slug) { + const result = await createOrGetShareLink({ + token, + documentId, + }); + + slug = result.slug; + } + + window.open( + generateTwitterIntent( + `I just ${token ? 'signed' : 'sent'} a document with @documenso. Check it out!`, + `${window.location.origin}/share/${slug}`, + ), + '_blank', + ); + + setIsOpen(false); }; return ( - + + + + + + + + Share + + Share your signing experience! + + +
+
+ I just {token ? 'signed' : 'sent'} a document with{' '} + @documenso + . Check it out! + + + {window.location.origin}/share/{shareLink?.slug || '...'} + +
+ + + +
+
+ Or +
+
+ + +
+ +
); }; diff --git a/apps/web/src/app/(signing)/sign/[token]/form.tsx b/apps/web/src/app/(signing)/sign/[token]/form.tsx index 74384fd89..36a54879c 100644 --- a/apps/web/src/app/(signing)/sign/[token]/form.tsx +++ b/apps/web/src/app/(signing)/sign/[token]/form.tsx @@ -4,6 +4,7 @@ import { useMemo, useState } from 'react'; import { useRouter } from 'next/navigation'; +import { useSession } from 'next-auth/react'; import { useForm } from 'react-hook-form'; import { completeDocumentWithToken } from '@documenso/lib/server-only/document/complete-document-with-token'; @@ -27,6 +28,7 @@ export type SigningFormProps = { export const SigningForm = ({ document, recipient, fields }: SigningFormProps) => { const router = useRouter(); + const { data: session } = useSession(); const { fullName, signature, setFullName, setSignature } = useRequiredSigningContext(); @@ -60,7 +62,11 @@ export const SigningForm = ({ document, recipient, fields }: SigningFormProps) = return (
@@ -120,6 +126,7 @@ export const SigningForm = ({ document, recipient, fields }: SigningFormProps) = className="dark:bg-muted dark:hover:bg-muted/80 w-full bg-black/5 hover:bg-black/10" variant="secondary" size="lg" + disabled={typeof window !== 'undefined' && window.history.length <= 1} onClick={() => router.back()} > Cancel diff --git a/apps/web/src/app/(signing)/sign/[token]/layout.tsx b/apps/web/src/app/(signing)/sign/[token]/layout.tsx index 3c56c1718..a25c16c0d 100644 --- a/apps/web/src/app/(signing)/sign/[token]/layout.tsx +++ b/apps/web/src/app/(signing)/sign/[token]/layout.tsx @@ -10,11 +10,11 @@ export type SigningLayoutProps = { }; export default async function SigningLayout({ children }: SigningLayoutProps) { - const user = await getServerComponentSession(); + const { user, session } = await getServerComponentSession(); return ( - -
+ +
{user && }
{children}
diff --git a/apps/web/src/app/(signing)/sign/[token]/page.tsx b/apps/web/src/app/(signing)/sign/[token]/page.tsx index 2d61f096e..40a650afd 100644 --- a/apps/web/src/app/(signing)/sign/[token]/page.tsx +++ b/apps/web/src/app/(signing)/sign/[token]/page.tsx @@ -1,4 +1,4 @@ -import { notFound } from 'next/navigation'; +import { notFound, redirect } from 'next/navigation'; import { match } from 'ts-pattern'; @@ -9,7 +9,7 @@ import { viewedDocument } from '@documenso/lib/server-only/document/viewed-docum import { getFieldsForToken } from '@documenso/lib/server-only/field/get-fields-for-token'; import { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-recipient-by-token'; import { getFile } from '@documenso/lib/universal/upload/get-file'; -import { FieldType } from '@documenso/prisma/client'; +import { DocumentStatus, FieldType, SigningStatus } from '@documenso/prisma/client'; import { Card, CardContent } from '@documenso/ui/primitives/card'; import { ElementVisible } from '@documenso/ui/primitives/element-visible'; import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer'; @@ -51,11 +51,18 @@ export default async function SigningPage({ params: { token } }: SigningPageProp .then((buffer) => Buffer.from(buffer).toString('base64')) .then((data) => `data:application/pdf;base64,${data}`); - const user = await getServerComponentSession(); + const { user } = await getServerComponentSession(); + + if ( + document.status === DocumentStatus.COMPLETED || + recipient.signingStatus === SigningStatus.SIGNED + ) { + redirect(`/sign/${token}/complete`); + } return ( -
+

{document.title}

diff --git a/apps/web/src/app/not-found.tsx b/apps/web/src/app/not-found.tsx index c8dc15086..f580655af 100644 --- a/apps/web/src/app/not-found.tsx +++ b/apps/web/src/app/not-found.tsx @@ -6,7 +6,7 @@ import { Button } from '@documenso/ui/primitives/button'; import NotFoundPartial from '~/components/partials/not-found'; export default async function NotFound() { - const session = await getServerComponentSession(); + const { session } = await getServerComponentSession(); return ( diff --git a/apps/web/src/components/(dashboard)/avatar/stack-avatars-with-tooltip.tsx b/apps/web/src/components/(dashboard)/avatar/stack-avatars-with-tooltip.tsx index 9ef2cd105..8d611c2d1 100644 --- a/apps/web/src/components/(dashboard)/avatar/stack-avatars-with-tooltip.tsx +++ b/apps/web/src/components/(dashboard)/avatar/stack-avatars-with-tooltip.tsx @@ -40,7 +40,7 @@ export const StackAvatarsWithTooltip = ({ return ( - + {children || } diff --git a/apps/web/src/components/forms/edit-document/add-fields.action.ts b/apps/web/src/components/forms/edit-document/add-fields.action.ts index d5f2f5fcc..c07758b9f 100644 --- a/apps/web/src/components/forms/edit-document/add-fields.action.ts +++ b/apps/web/src/components/forms/edit-document/add-fields.action.ts @@ -11,10 +11,10 @@ export type AddFieldsActionInput = TAddFieldsFormSchema & { export const addFields = async ({ documentId, fields }: AddFieldsActionInput) => { 'use server'; - const { id: userId } = await getRequiredServerComponentSession(); + const { user } = await getRequiredServerComponentSession(); await setFieldsForDocument({ - userId, + userId: user.id, documentId, fields: fields.map((field) => ({ id: field.nativeId, diff --git a/apps/web/src/components/forms/edit-document/add-signers.action.ts b/apps/web/src/components/forms/edit-document/add-signers.action.ts index 4e6739bd6..05151498a 100644 --- a/apps/web/src/components/forms/edit-document/add-signers.action.ts +++ b/apps/web/src/components/forms/edit-document/add-signers.action.ts @@ -11,10 +11,10 @@ export type AddSignersActionInput = TAddSignersFormSchema & { export const addSigners = async ({ documentId, signers }: AddSignersActionInput) => { 'use server'; - const { id: userId } = await getRequiredServerComponentSession(); + const { user } = await getRequiredServerComponentSession(); await setRecipientsForDocument({ - userId, + userId: user.id, documentId, recipients: signers.map((signer) => ({ id: signer.nativeId, diff --git a/apps/web/src/components/forms/edit-document/add-subject.action.ts b/apps/web/src/components/forms/edit-document/add-subject.action.ts index 14ddef867..8fe37fecc 100644 --- a/apps/web/src/components/forms/edit-document/add-subject.action.ts +++ b/apps/web/src/components/forms/edit-document/add-subject.action.ts @@ -12,7 +12,7 @@ export type CompleteDocumentActionInput = TAddSubjectFormSchema & { export const completeDocument = async ({ documentId, email }: CompleteDocumentActionInput) => { 'use server'; - const { id: userId } = await getRequiredServerComponentSession(); + const { user } = await getRequiredServerComponentSession(); if (email.message || email.subject) { await upsertDocumentMeta({ @@ -23,7 +23,7 @@ export const completeDocument = async ({ documentId, email }: CompleteDocumentAc } return await sendDocument({ - userId, + userId: user.id, documentId, }); }; diff --git a/apps/web/src/components/forms/signin.tsx b/apps/web/src/components/forms/signin.tsx index 2a209ddbe..aed0a4d3d 100644 --- a/apps/web/src/components/forms/signin.tsx +++ b/apps/web/src/components/forms/signin.tsx @@ -1,8 +1,6 @@ 'use client'; -import { useEffect, useState } from 'react'; - -import { useSearchParams } from 'next/navigation'; +import { useState } from 'react'; import { zodResolver } from '@hookform/resolvers/zod'; import { Eye, EyeOff, Loader } from 'lucide-react'; @@ -40,8 +38,6 @@ export type SignInFormProps = { }; export const SignInForm = ({ className }: SignInFormProps) => { - const searchParams = useSearchParams(); - const { toast } = useToast(); const [showPassword, setShowPassword] = useState(false); @@ -57,36 +53,29 @@ export const SignInForm = ({ className }: SignInFormProps) => { resolver: zodResolver(ZSignInFormSchema), }); - const errorCode = searchParams?.get('error'); - - useEffect(() => { - let timeout: NodeJS.Timeout | null = null; - - if (isErrorCode(errorCode)) { - timeout = setTimeout(() => { - toast({ - variant: 'destructive', - description: ERROR_MESSAGES[errorCode] ?? 'An unknown error occurred', - }); - }, 0); - } - - return () => { - if (timeout) { - clearTimeout(timeout); - } - }; - }, [errorCode, toast]); - const onFormSubmit = async ({ email, password }: TSignInFormSchema) => { try { - await signIn('credentials', { + const result = await signIn('credentials', { email, password, callbackUrl: LOGIN_REDIRECT_PATH, - }).catch((err) => { - console.error(err); + redirect: false, }); + + if (result?.error && isErrorCode(result.error)) { + toast({ + variant: 'destructive', + description: ERROR_MESSAGES[result.error], + }); + + return; + } + + if (!result?.url) { + throw new Error('An unknown error occurred'); + } + + window.location.href = result.url; } catch (err) { toast({ title: 'An unknown error occurred', diff --git a/package-lock.json b/package-lock.json index 451b7c2ec..eb9568b3b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16958,6 +16958,29 @@ "node": ">=0.10.5" } }, + "node_modules/resend": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/resend/-/resend-1.1.0.tgz", + "integrity": "sha512-it8TIDVT+/gAiJsUlv2tdHuvzwCCv4Zwu+udDqIm/dIuByQwe68TtFDcPccxqpSVVrNCBxxXLzsdT1tsV+P3GA==", + "dependencies": { + "@react-email/render": "0.0.7", + "type-fest": "3.13.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/resend/node_modules/type-fest": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.13.0.tgz", + "integrity": "sha512-Gur3yQGM9qiLNs0KPP7LPgeRbio2QTt4xXouobMCarR0/wyW3F+F/+OWwshg3NG0Adon7uQfSZBpB46NfhoF1A==", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/resolve": { "version": "1.22.2", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz", @@ -19773,7 +19796,8 @@ "dependencies": { "@react-email/components": "^0.0.7", "nodemailer": "^6.9.3", - "react-email": "^1.9.4" + "react-email": "^1.9.4", + "resend": "^1.1.0" }, "devDependencies": { "@documenso/tailwind-config": "*", @@ -19809,6 +19833,7 @@ "@aws-sdk/signature-v4-crt": "^3.410.0", "@documenso/email": "*", "@documenso/prisma": "*", + "@documenso/signing": "*", "@next-auth/prisma-adapter": "1.0.7", "@pdf-lib/fontkit": "^1.1.1", "@scure/base": "^1.1.3", @@ -19862,17 +19887,23 @@ "packages/signing": { "name": "@documenso/signing", "version": "1.0.0", - "license": "MIT", + "license": "AGPLv3", "dependencies": { "@documenso/tsconfig": "*", "node-forge": "^1.3.1", "node-signpdf": "^2.0.0", - "pdf-lib": "^1.17.1" + "pdf-lib": "^1.17.1", + "ts-pattern": "^5.0.5" }, "devDependencies": { "@types/node-forge": "^1.3.4" } }, + "packages/signing/node_modules/ts-pattern": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/ts-pattern/-/ts-pattern-5.0.5.tgz", + "integrity": "sha512-tL0w8U/pgaacOmkb9fRlYzWEUDCfVjjv9dD4wHTgZ61MjhuMt46VNWTG747NqW6vRzoWIKABVhFSOJ82FvXrfA==" + }, "packages/tailwind-config": { "name": "@documenso/tailwind-config", "version": "0.0.0", diff --git a/packages/email/mailer.ts b/packages/email/mailer.ts index 073f96680..cf359e648 100644 --- a/packages/email/mailer.ts +++ b/packages/email/mailer.ts @@ -1,6 +1,7 @@ import { createTransport } from 'nodemailer'; import { MailChannelsTransport } from './transports/mailchannels'; +import { ResendTransport } from './transports/resend'; const getTransport = () => { const transport = process.env.NEXT_PRIVATE_SMTP_TRANSPORT ?? 'smtp-auth'; @@ -14,6 +15,14 @@ const getTransport = () => { ); } + if (transport === 'resend') { + return createTransport( + ResendTransport.makeTransport({ + apiKey: process.env.NEXT_PRIVATE_RESEND_API_KEY || '', + }), + ); + } + if (transport === 'smtp-api') { if (!process.env.NEXT_PRIVATE_SMTP_HOST || !process.env.NEXT_PRIVATE_SMTP_APIKEY) { throw new Error( diff --git a/packages/email/package.json b/packages/email/package.json index 7bc6d9e5a..f9ce13279 100644 --- a/packages/email/package.json +++ b/packages/email/package.json @@ -19,7 +19,8 @@ "dependencies": { "@react-email/components": "^0.0.7", "nodemailer": "^6.9.3", - "react-email": "^1.9.4" + "react-email": "^1.9.4", + "resend": "^1.1.0" }, "devDependencies": { "@documenso/tailwind-config": "*", diff --git a/packages/email/transports/resend.ts b/packages/email/transports/resend.ts new file mode 100644 index 000000000..ab2f0959d --- /dev/null +++ b/packages/email/transports/resend.ts @@ -0,0 +1,145 @@ +import { type SentMessageInfo, type Transport } from 'nodemailer'; +import type Mail from 'nodemailer/lib/mailer'; +import type MailMessage from 'nodemailer/lib/mailer/mail-message'; +import { Resend } from 'resend'; + +const VERSION = '1.0.0'; + +type ResendTransportOptions = { + apiKey: string; +}; + +type ResendResponseError = { + statusCode: number; + name: string; + message: string; +}; + +const isResendResponseError = (error: unknown): error is ResendResponseError => { + // We could use Zod here, but it's not worth the extra bundle size + return ( + typeof error === 'object' && + error !== null && + 'statusCode' in error && + typeof error.statusCode === 'number' && + 'name' in error && + typeof error.name === 'string' && + 'message' in error && + typeof error.message === 'string' + ); +}; + +/** + * Transport for sending email via the Resend SDK. + */ +export class ResendTransport implements Transport { + public name = 'ResendMailTransport'; + public version = VERSION; + + private _client: Resend; + private _options: ResendTransportOptions; + + public static makeTransport(options: Partial) { + return new ResendTransport(options); + } + + constructor(options: Partial) { + const { apiKey = '' } = options; + + this._options = { + apiKey, + }; + + this._client = new Resend(apiKey); + } + + public send(mail: MailMessage, callback: (_err: Error | null, _info: SentMessageInfo) => void) { + if (!mail.data.to || !mail.data.from) { + return callback(new Error('Missing required fields "to" or "from"'), null); + } + + this._client + .sendEmail({ + subject: mail.data.subject ?? '', + from: this.toResendFromAddress(mail.data.from), + to: this.toResendAddresses(mail.data.to), + cc: this.toResendAddresses(mail.data.cc), + bcc: this.toResendAddresses(mail.data.bcc), + html: mail.data.html?.toString() || '', + text: mail.data.text?.toString() || '', + attachments: this.toResendAttachments(mail.data.attachments), + }) + .then((response) => { + if (isResendResponseError(response)) { + throw new Error(`[${response.statusCode}]: ${response.name} ${response.message}`); + } + + callback(null, response); + }) + .catch((error) => { + callback(error, null); + }); + } + + private toResendAddresses(addresses: Mail.Options['to']) { + if (!addresses) { + return []; + } + + if (typeof addresses === 'string') { + return [addresses]; + } + + if (Array.isArray(addresses)) { + return addresses.map((address) => { + if (typeof address === 'string') { + return address; + } + + return address.address; + }); + } + + return [addresses.address]; + } + + private toResendFromAddress(address: Mail.Options['from']) { + if (!address) { + return ''; + } + + if (typeof address === 'string') { + return address; + } + + return `${address.name} <${address.address}>`; + } + + private toResendAttachments(attachments: Mail.Options['attachments']) { + if (!attachments) { + return []; + } + + return attachments.map((attachment) => { + if (!attachment.filename || !attachment.content) { + throw new Error('Attachment is missing filename or content'); + } + + if (typeof attachment.content === 'string') { + return { + filename: attachment.filename, + content: Buffer.from(attachment.content), + }; + } + + if (attachment.content instanceof Buffer) { + return { + filename: attachment.filename, + content: attachment.content, + }; + } + + throw new Error('Attachment content must be a string or a buffer'); + }); + } +} diff --git a/packages/email/tsconfig.json b/packages/email/tsconfig.json index 893a50e65..d211eb6ff 100644 --- a/packages/email/tsconfig.json +++ b/packages/email/tsconfig.json @@ -1,5 +1,10 @@ { "extends": "@documenso/tsconfig/react-library.json", + "compilerOptions": { + "types": [ + "@documenso/tsconfig/process-env.d.ts", + ] + }, "include": ["**/*.ts", "**/*.tsx", "**/*.d.ts", "**/*.json"], "exclude": ["dist", "build", "node_modules"] } diff --git a/packages/lib/constants/feature-flags.ts b/packages/lib/constants/feature-flags.ts index e23f59eba..e972b47c2 100644 --- a/packages/lib/constants/feature-flags.ts +++ b/packages/lib/constants/feature-flags.ts @@ -1,3 +1,5 @@ +import { APP_BASE_URL } from './app'; + /** * The flag name for global session recording feature flag. */ @@ -23,7 +25,7 @@ export const LOCAL_FEATURE_FLAGS: Record = { */ export function extractPostHogConfig(): { key: string; host: string } | null { const postHogKey = process.env.NEXT_PUBLIC_POSTHOG_KEY; - const postHogHost = process.env.NEXT_PUBLIC_POSTHOG_HOST; + const postHogHost = `${APP_BASE_URL}/ingest`; if (!postHogKey || !postHogHost) { return null; diff --git a/packages/lib/next-auth/get-server-session.ts b/packages/lib/next-auth/get-server-session.ts index 56df8e7eb..f9196369f 100644 --- a/packages/lib/next-auth/get-server-session.ts +++ b/packages/lib/next-auth/get-server-session.ts @@ -15,7 +15,7 @@ export const getServerSession = async ({ req, res }: GetServerSessionOptions) => const session = await getNextAuthServerSession(req, res, NEXT_AUTH_OPTIONS); if (!session || !session.user?.email) { - return null; + return { user: null, session: null }; } const user = await prisma.user.findFirstOrThrow({ @@ -24,14 +24,14 @@ export const getServerSession = async ({ req, res }: GetServerSessionOptions) => }, }); - return user; + return { user, session }; }; export const getServerComponentSession = async () => { const session = await getNextAuthServerSession(NEXT_AUTH_OPTIONS); if (!session || !session.user?.email) { - return null; + return { user: null, session: null }; } const user = await prisma.user.findFirstOrThrow({ @@ -40,15 +40,15 @@ export const getServerComponentSession = async () => { }, }); - return user; + return { user, session }; }; export const getRequiredServerComponentSession = async () => { - const session = await getServerComponentSession(); + const { user, session } = await getServerComponentSession(); - if (!session) { + if (!user || !session) { throw new Error('No session found'); } - return session; + return { user, session }; }; diff --git a/packages/lib/server-only/document/viewed-document.ts b/packages/lib/server-only/document/viewed-document.ts index b16ac138e..5944d4841 100644 --- a/packages/lib/server-only/document/viewed-document.ts +++ b/packages/lib/server-only/document/viewed-document.ts @@ -14,7 +14,6 @@ export const viewedDocument = async ({ token }: ViewedDocumentOptions) => { }); if (!recipient) { - console.warn(`No recipient found for token ${token}`); return; } diff --git a/packages/lib/server-only/recipient/set-recipients-for-document.ts b/packages/lib/server-only/recipient/set-recipients-for-document.ts index 17263a693..198f79be1 100644 --- a/packages/lib/server-only/recipient/set-recipients-for-document.ts +++ b/packages/lib/server-only/recipient/set-recipients-for-document.ts @@ -57,12 +57,13 @@ export const setRecipientsForDocument = async ({ return { ...recipient, - ...existing, + _persisted: existing, }; }) .filter((recipient) => { return ( - recipient.sendStatus !== SendStatus.SENT && recipient.signingStatus !== SigningStatus.SIGNED + recipient._persisted?.sendStatus !== SendStatus.SENT && + recipient._persisted?.signingStatus !== SigningStatus.SIGNED ); }); @@ -72,7 +73,7 @@ export const setRecipientsForDocument = async ({ linkedRecipients.map((recipient) => prisma.recipient.upsert({ where: { - id: recipient.id ?? -1, + id: recipient._persisted?.id ?? -1, documentId, }, update: { diff --git a/packages/lib/universal/generate-twitter-intent.ts b/packages/lib/universal/generate-twitter-intent.ts new file mode 100644 index 000000000..29774fb32 --- /dev/null +++ b/packages/lib/universal/generate-twitter-intent.ts @@ -0,0 +1,5 @@ +export const generateTwitterIntent = (text: string, shareUrl: string) => { + return `https://twitter.com/intent/tweet?text=${encodeURIComponent( + text, + )}%0A%0A${encodeURIComponent(shareUrl)}`; +}; diff --git a/packages/lib/universal/upload/server-actions.ts b/packages/lib/universal/upload/server-actions.ts index 629d62a2a..5b255fa4f 100644 --- a/packages/lib/universal/upload/server-actions.ts +++ b/packages/lib/universal/upload/server-actions.ts @@ -17,7 +17,7 @@ import { alphaid } from '../id'; export const getPresignPostUrl = async (fileName: string, contentType: string) => { const client = getS3Client(); - const user = await getServerComponentSession(); + const { user } = await getServerComponentSession(); // Get the basename and extension for the file const { name, ext } = path.parse(fileName); diff --git a/packages/prisma/migrations/20230605122017_password_reset/migration.sql b/packages/prisma/migrations/20230605122017_password_reset/migration.sql new file mode 100644 index 000000000..782a60880 --- /dev/null +++ b/packages/prisma/migrations/20230605122017_password_reset/migration.sql @@ -0,0 +1,15 @@ +-- CreateTable +CREATE TABLE "PasswordResetToken" ( + "id" SERIAL NOT NULL, + "token" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "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/20230605164015_expire_password_reset_token/migration.sql b/packages/prisma/migrations/20230605164015_expire_password_reset_token/migration.sql new file mode 100644 index 000000000..a3a70e575 --- /dev/null +++ b/packages/prisma/migrations/20230605164015_expire_password_reset_token/migration.sql @@ -0,0 +1,8 @@ +/* + Warnings: + + - Added the required column `expiry` to the `PasswordResetToken` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "PasswordResetToken" ADD COLUMN "expiry" TIMESTAMP(3) NOT NULL; diff --git a/packages/prisma/migrations/20230917190854_password_reset_token/migration.sql b/packages/prisma/migrations/20230917190854_password_reset_token/migration.sql index d22107691..2d127d917 100644 --- a/packages/prisma/migrations/20230917190854_password_reset_token/migration.sql +++ b/packages/prisma/migrations/20230917190854_password_reset_token/migration.sql @@ -1,3 +1,5 @@ +DROP TABLE IF EXISTS "PasswordResetToken" CASCADE; + -- CreateTable CREATE TABLE "PasswordResetToken" ( "id" SERIAL NOT NULL, diff --git a/packages/trpc/server/context.ts b/packages/trpc/server/context.ts index 252a09317..e1973f08b 100644 --- a/packages/trpc/server/context.ts +++ b/packages/trpc/server/context.ts @@ -3,7 +3,7 @@ import { CreateNextContextOptions } from '@trpc/server/adapters/next'; import { getServerSession } from '@documenso/lib/next-auth/get-server-session'; export const createTrpcContext = async ({ req, res }: CreateNextContextOptions) => { - const session = await getServerSession({ req, res }); + const { session, user } = await getServerSession({ req, res }); if (!session) { return { @@ -12,9 +12,16 @@ export const createTrpcContext = async ({ req, res }: CreateNextContextOptions) }; } + if (!user) { + return { + session: null, + user: null, + }; + } + return { session, - user: session, + user, }; }; diff --git a/packages/tsconfig/process-env.d.ts b/packages/tsconfig/process-env.d.ts index 86c5c7f64..1db1ec36a 100644 --- a/packages/tsconfig/process-env.d.ts +++ b/packages/tsconfig/process-env.d.ts @@ -27,7 +27,9 @@ declare namespace NodeJS { NEXT_PRIVATE_SIGNING_LOCAL_FILE_CONTENTS?: string; NEXT_PRIVATE_SIGNING_LOCAL_FILE_ENCODING?: string; - NEXT_PRIVATE_SMTP_TRANSPORT?: 'mailchannels' | 'smtp-auth' | 'smtp-api'; + NEXT_PRIVATE_SMTP_TRANSPORT?: 'mailchannels' | 'resend' | 'smtp-auth' | 'smtp-api'; + + NEXT_PRIVATE_RESEND_API_KEY?: string; NEXT_PRIVATE_MAILCHANNELS_API_KEY?: string; NEXT_PRIVATE_MAILCHANNELS_DKIM_DOMAIN?: string; diff --git a/packages/ui/components/signing-card.tsx b/packages/ui/components/signing-card.tsx index 496e451d0..a2dd66bae 100644 --- a/packages/ui/components/signing-card.tsx +++ b/packages/ui/components/signing-card.tsx @@ -148,7 +148,7 @@ const SigningCardContent = ({ className, name }: SigningCardContentProps) => { return ( { return ( background pattern ( +const DialogPortal = ({ + className, + children, + position = 'start', + ...props +}: DialogPrimitive.DialogPortalProps & { position?: 'start' | 'end' }) => ( -
+
{children}
@@ -39,14 +49,14 @@ DialogOverlay.displayName = DialogPrimitive.Overlay.displayName; const DialogContent = React.forwardRef< React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, children, ...props }, ref) => ( - + React.ComponentPropsWithoutRef & { position?: 'start' | 'end' } +>(({ className, children, position = 'start', ...props }, ref) => ( +