Compare commits

..

18 Commits

Author SHA1 Message Date
Timur Ercan
21fedab707 Merge branch 'feat/refresh' into feat/open-early-adopters 2023-09-28 12:16:27 +02:00
Lucas Smith
5bc9d625c2 Merge pull request #455 from documenso/fix/432-signee-doc-version-doesnt-have-sticky-signing-area
fix: resolve issues with signing document stickiness
2023-09-28 17:50:04 +10:00
Mythie
4885cf5154 fix: resolve issues with signing document stickiness 2023-09-28 17:42:01 +10:00
Lucas Smith
2f66eca925 Merge pull request #454 from documenso/fix/446-cancel-cta-does-nothing-when-a-signer-opens-the-document
fix: disable cancel button when there is no window history
2023-09-28 17:15:41 +10:00
Mythie
af042a62cd fix: disable cancel button when there is no window history 2023-09-28 15:45:22 +10:00
Lucas Smith
df8bdda718 Merge pull request #450 from documenso/feat/resend-transport
feat: add resend mail transport
2023-09-28 14:07:18 +10:00
David Nguyen
873f99ae86 fix: resolve document title inconsistency (#452) 2023-09-28 13:56:22 +10:00
Lucas Smith
17fe135027 Merge pull request #451 from documenso/fix/445-signer-name-not-persisting
fix: do not overwrite new names or emails for signers
2023-09-28 13:07:26 +10:00
David Nguyen
2eed0ae063 feat: add posthog reverse proxy (#449) 2023-09-28 12:56:53 +10:00
Mythie
f4ae0389d8 fix: do not overwrite new names or emails for signers 2023-09-28 12:35:21 +10:00
Mythie
9bdff9a61f feat: add resend mail transport 2023-09-28 12:27:04 +10:00
Timur Ercan
cdb71c3a62 chore: greetings 2023-09-27 12:30:11 +02:00
Timur Ercan
39d7b3ca58 Merge branch 'feat/refresh' of https://github.com/documenso/documenso into feat/refresh 2023-09-27 12:19:14 +02:00
Timur Ercan
748c3636d5 chore: sync shop article and add missing and updated assets 2023-09-27 12:15:44 +02:00
Timur Ercan
d950634aab Merge branch 'feat/refresh' into feat/open-early-adopters 2023-09-25 09:28:04 +02:00
Ephraim Atta-Duncan
d0fad4775a feat: add extra info for the early adopters 2023-09-23 12:02:37 +00:00
Ephraim Atta-Duncan
eef63c9adc refactor: metrics into reusable component 2023-09-22 14:08:25 +00:00
Ephraim Atta-Duncan
c3d52c68e7 feat: add early adopters graph 2023-09-22 13:27:10 +00:00
59 changed files with 411 additions and 872 deletions

View File

@@ -15,20 +15,6 @@ NEXT_PRIVATE_DATABASE_URL="postgres://documenso:password@127.0.0.1:54320/documen
# Defines the URL to use for the database when running migrations and other commands that won't work with a connection pool. # Defines the URL to use for the database when running migrations and other commands that won't work with a connection pool.
NEXT_PRIVATE_DIRECT_DATABASE_URL="postgres://documenso:password@127.0.0.1:54320/documenso" NEXT_PRIVATE_DIRECT_DATABASE_URL="postgres://documenso:password@127.0.0.1:54320/documenso"
# [[E2E Tests]]
E2E_TEST_SIGNER_NAME="Test Signer"
E2E_TEST_SIGNER_EMAIL="testsigner@mail.com"
E2E_TEST_SIGNING_SUBJECT="Test subject"
E2E_TEST_SIGNING_MESSAGE="Test message"
# User for the "auth.setup.ts" file and the authenticated tests
E2E_TEST_AUTHENTICATE_USERNAME="New user"
E2E_TEST_AUTHENTICATE_USER_EMAIL="mytestnewuser@mail.com"
E2E_TEST_AUTHENTICATE_USER_PASSWORD="new_test_password"
# User for the *.unauthenticated.ts files
E2E_TEST_USERNAME="Test"
E2E_TEST_USER_EMAIL="mytestuser@mail.com"
E2E_TEST_USER_PASSWORD="test_password"
# [[STORAGE]] # [[STORAGE]]
# OPTIONAL: Defines the storage transport to use. Available options: database (default) | s3 # OPTIONAL: Defines the storage transport to use. Available options: database (default) | s3
NEXT_PUBLIC_UPLOAD_TRANSPORT="database" NEXT_PUBLIC_UPLOAD_TRANSPORT="database"
@@ -64,7 +50,9 @@ NEXT_PRIVATE_SMTP_SECURE=
NEXT_PRIVATE_SMTP_FROM_NAME="No Reply @ Documenso" NEXT_PRIVATE_SMTP_FROM_NAME="No Reply @ Documenso"
# REQUIRED: Defines the email address to use as the from address. # REQUIRED: Defines the email address to use as the from address.
NEXT_PRIVATE_SMTP_FROM_ADDRESS="noreply@documenso.com" 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= NEXT_PRIVATE_MAILCHANNELS_API_KEY=
# OPTIONAL: The endpoint to use for the MailChannels API if using a proxy. # OPTIONAL: The endpoint to use for the MailChannels API if using a proxy.
NEXT_PRIVATE_MAILCHANNELS_ENDPOINT= NEXT_PRIVATE_MAILCHANNELS_ENDPOINT=

View File

@@ -1,53 +0,0 @@
name: Playwright Tests
on:
push:
branches: [feat/refresh, feat/add-e2e-testing]
pull_request:
branches: [feat/refresh, feat/add-e2e-testing]
jobs:
e2e_tests:
timeout-minutes: 60
runs-on: ubuntu-latest
services:
postgres:
image: postgres
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
env:
NEXT_PRIVATE_DATABASE_URL: postgresql://postgres:postgres@localhost:5432/documenso
NEXT_PRIVATE_DIRECT_DATABASE_URL: postgresql://postgres:postgres@localhost:5432/documenso
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18
- name: Generate package-lock.json
run: npm cache clean --force && npm install
- name: Install dependencies
run: npm ci
- name: Copy env
run: cp .env.example .env
- name: Install Playwright Browsers
run: npx playwright install --with-deps
- name: Install Prisma Client
run: npm install @prisma/client
- name: Generate Prisma Client
run: npm run prisma:generate -w @documenso/prisma
- name: Create the database
run: npm run prisma:migrate-dev -w @documenso/prisma
- name: Run Playwright tests
run: npm run ci
- uses: actions/upload-artifact@v3
if: always()
with:
name: playwright-report
path: playwright-report/
retention-days: 30

5
.gitignore vendored
View File

@@ -47,8 +47,3 @@ yarn-error.log*
!.vscode/tasks.json !.vscode/tasks.json
!.vscode/launch.json !.vscode/launch.json
!.vscode/extensions.json !.vscode/extensions.json
**/test-results/
/playwright-report/
/playwright/.cache/
playwright/.auth

View File

@@ -51,4 +51,7 @@ As Documenso 1.0 just hit the staging environment, we're calling a MALFUNCTION M
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. 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.** **[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.**

View File

@@ -1,12 +1,12 @@
--- ---
title: Shop and Limited Edition "Mania" Shirt title: Merch Mania
description: Happy Launch Week Day 3. The limited edition "Malfunction Mania" shirt is here. Grab it, while you can. description: Happy Launch Week Day 3. The limited edition "Malfunction Mania" shirt is here. Grab it, while you can.
authorName: 'Timur Ercan' authorName: 'Timur Ercan'
authorImage: '/blog/blog-author-timur.jpeg' authorImage: '/blog/blog-author-timur.jpeg'
authorRole: 'Co-Founder' authorRole: 'Co-Founder'
date: 2023-09-27 date: 2023-09-27
tags: tags:
- Testing - Merch
- Rewrite - Rewrite
- Bounties - Bounties
--- ---
@@ -24,13 +24,13 @@ tags:
</figcaption> </figcaption>
</figure> </figure>
> TLDR; We have a fancy limited edition shirt. Contribute to Malfunction Mania to get one. > 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 the limited edition shirt for Malfunction Mania. 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. 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 ## 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. 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.
<figure> <figure>
<MdxNextImage <MdxNextImage
@@ -45,7 +45,7 @@ The shirt will be available in our [merch shop](https://documen.so/shop) via a u
</figcaption> </figcaption>
</figure> </figure>
## How to get the shirt ## 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: 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:
@@ -56,7 +56,9 @@ If you have been following us, you know we are not big on formalities but highly
- Engage in discussion about the current version and its choices - 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) - 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 the version with a video, stream, or screenshots and post about it
- Review existing or create missing documenso - Review existing or create missing documentation
- ...
**[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.** 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.**

View File

@@ -56,6 +56,14 @@ const config = {
}, },
]; ];
}, },
async rewrites() {
return [
{
source: '/ingest/:path*',
destination: 'https://eu.posthog.com/:path*',
},
];
},
}; };
module.exports = withContentlayer(config); module.exports = withContentlayer(config);

Binary file not shown.

After

Width:  |  Height:  |  Size: 1007 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 956 KiB

After

Width:  |  Height:  |  Size: 258 KiB

View File

@@ -7,26 +7,25 @@ import { Bar, BarChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recha
import { formatMonth } from '@documenso/lib/client-only/format-month'; import { formatMonth } from '@documenso/lib/client-only/format-month';
import { cn } from '@documenso/ui/lib/utils'; import { cn } from '@documenso/ui/lib/utils';
import { StargazersType } from './page'; export type BarMetricProps<T extends Record<string, unknown>> = HTMLAttributes<HTMLDivElement> & {
data: T;
export type MetricsDataKey = 'stars' | 'forks' | 'mergedPRs' | 'openIssues'; metricKey: keyof T[string];
export type GithubMetricProps = HTMLAttributes<HTMLDivElement> & {
data: StargazersType;
metricKey: MetricsDataKey;
title: string; title: string;
label: string; label: string;
chartHeight?: number; chartHeight?: number;
extraInfo?: JSX.Element;
}; };
export const GithubMetric = ({ export const BarMetric = <T extends Record<string, Record<keyof T[string], unknown>>>({
className, className,
data, data,
metricKey, metricKey,
title, title,
label, label,
chartHeight = 400, chartHeight = 400,
extraInfo,
...props ...props
}: GithubMetricProps) => { }: BarMetricProps<T>) => {
const formattedData = Object.keys(data) const formattedData = Object.keys(data)
.map((key) => ({ .map((key) => ({
month: formatMonth(key), month: formatMonth(key),
@@ -36,7 +35,10 @@ export const GithubMetric = ({
return ( return (
<div className={cn('flex flex-col', className)} {...props}> <div className={cn('flex flex-col', className)} {...props}>
<h3 className="px-4 text-lg font-semibold">{title}</h3> <div className="flex items-center px-4">
<h3 className="text-lg font-semibold">{title}</h3>
<span>{extraInfo}</span>
</div>
<div className="border-border mt-2.5 flex flex-1 items-center justify-center rounded-2xl border pr-2 shadow-sm hover:shadow"> <div className="border-border mt-2.5 flex flex-1 items-center justify-center rounded-2xl border pr-2 shadow-sm hover:shadow">
<ResponsiveContainer width="100%" height={chartHeight}> <ResponsiveContainer width="100%" height={chartHeight}>
@@ -50,7 +52,7 @@ export const GithubMetric = ({
formatter={(value) => [Number(value), label]} formatter={(value) => [Number(value), label]}
cursor={{ fill: 'hsl(var(--primary) / 10%)' }} cursor={{ fill: 'hsl(var(--primary) / 10%)' }}
/> />
<Bar dataKey={metricKey} fill="hsl(var(--primary))" label={label} /> <Bar dataKey={metricKey as string} fill="hsl(var(--primary))" label={label} />{' '}
</BarChart> </BarChart>
</ResponsiveContainer> </ResponsiveContainer>
</div> </div>

View File

@@ -7,14 +7,14 @@ import { Bar, BarChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recha
import { formatMonth } from '@documenso/lib/client-only/format-month'; import { formatMonth } from '@documenso/lib/client-only/format-month';
import { cn } from '@documenso/ui/lib/utils'; import { cn } from '@documenso/ui/lib/utils';
import { FUNDING_RAISED } from '~/app/(marketing)/open/data'; export type FundingRaisedProps = HTMLAttributes<HTMLDivElement> & {
data: Record<string, string | number>[];
};
export type FundingRaisedProps = HTMLAttributes<HTMLDivElement>; export const FundingRaised = ({ className, data, ...props }: FundingRaisedProps) => {
const formattedData = data.map((item) => ({
export const FundingRaised = ({ className, ...props }: FundingRaisedProps) => {
const formattedData = FUNDING_RAISED.map((item) => ({
amount: Number(item.amount), amount: Number(item.amount),
date: formatMonth(item.date), date: formatMonth(item.date as string),
})); }));
return ( return (

View File

@@ -1,12 +1,14 @@
import { z } from 'zod'; import { z } from 'zod';
import { FUNDING_RAISED } from '~/app/(marketing)/open/data';
import { MetricCard } from '~/app/(marketing)/open/metric-card'; import { MetricCard } from '~/app/(marketing)/open/metric-card';
import { SalaryBands } from '~/app/(marketing)/open/salary-bands'; import { SalaryBands } from '~/app/(marketing)/open/salary-bands';
import { BarMetric } from './bar-metrics';
import { CapTable } from './cap-table'; import { CapTable } from './cap-table';
import { FundingRaised } from './funding-raised'; import { FundingRaised } from './funding-raised';
import { GithubMetric } from './gh-metrics';
import { TeamMembers } from './team-members'; import { TeamMembers } from './team-members';
import { OpenPageTooltip } from './tooltip';
export const revalidate = 3600; export const revalidate = 3600;
@@ -29,9 +31,16 @@ const ZStargazersLiveResponse = z.record(
}), }),
); );
export type StargazersType = z.infer<typeof ZStargazersLiveResponse>; const ZEarlyAdoptersResponse = z.record(
z.object({
id: z.number(),
time: z.string().datetime(),
earlyAdopters: z.number(),
}),
);
// const ZOpenPullRequestsResponse = ZMergedPullRequestsResponse; export type StargazersType = z.infer<typeof ZStargazersLiveResponse>;
export type EarlyAdoptersType = z.infer<typeof ZEarlyAdoptersResponse>;
export default async function OpenPage() { export default async function OpenPage() {
const { const {
@@ -65,6 +74,14 @@ export default async function OpenPage() {
.then(async (res) => res.json()) .then(async (res) => res.json())
.then((res) => ZStargazersLiveResponse.parse(res)); .then((res) => ZStargazersLiveResponse.parse(res));
const EARLY_ADOPTERS_DATA = await fetch('https://stargrazer-live.onrender.com/api/stats/stripe', {
headers: {
accept: 'application/json',
},
})
.then(async (res) => res.json())
.then((res) => ZEarlyAdoptersResponse.parse(res));
return ( return (
<div className="mx-auto mt-6 max-w-screen-lg sm:mt-12"> <div className="mx-auto mt-6 max-w-screen-lg sm:mt-12">
<div className="flex flex-col items-center justify-center"> <div className="flex flex-col items-center justify-center">
@@ -107,10 +124,20 @@ export default async function OpenPage() {
<SalaryBands className="col-span-12 lg:col-span-6" /> <SalaryBands className="col-span-12 lg:col-span-6" />
<FundingRaised className="col-span-12 lg:col-span-6" /> <FundingRaised data={FUNDING_RAISED} className="col-span-12 lg:col-span-6" />
<CapTable className="col-span-12 lg:col-span-6" /> <CapTable className="col-span-12 lg:col-span-6" />
<GithubMetric
<BarMetric<EarlyAdoptersType>
data={EARLY_ADOPTERS_DATA}
metricKey="earlyAdopters"
title="Early Adopters"
label="Early Adopters"
className="col-span-12 lg:col-span-6"
extraInfo={<OpenPageTooltip />}
/>
<BarMetric<StargazersType>
data={STARGAZERS_DATA} data={STARGAZERS_DATA}
metricKey="stars" metricKey="stars"
title="Github: Total Stars" title="Github: Total Stars"
@@ -118,29 +145,31 @@ export default async function OpenPage() {
className="col-span-12 lg:col-span-6" className="col-span-12 lg:col-span-6"
/> />
<GithubMetric <BarMetric<StargazersType>
data={STARGAZERS_DATA} data={STARGAZERS_DATA}
metricKey="mergedPRs" metricKey="mergedPRs"
title="Github: Total Merged PRs" title="Github: Total Merged PRs"
label="Merged PRs" label="Merged PRs"
chartHeight={300} chartHeight={300}
className="col-span-12 lg:col-span-4" className="col-span-12 lg:col-span-6"
/> />
<GithubMetric
<BarMetric<StargazersType>
data={STARGAZERS_DATA} data={STARGAZERS_DATA}
metricKey="forks" metricKey="forks"
title="Github: Total Forks" title="Github: Total Forks"
label="Forks" label="Forks"
chartHeight={300} chartHeight={300}
className="col-span-12 lg:col-span-4" className="col-span-12 lg:col-span-6"
/> />
<GithubMetric
<BarMetric<StargazersType>
data={STARGAZERS_DATA} data={STARGAZERS_DATA}
metricKey="openIssues" metricKey="openIssues"
title="Github: Total Open Issues" title="Github: Total Open Issues"
label="Open Issues" label="Open Issues"
chartHeight={300} chartHeight={300}
className="col-span-12 lg:col-span-4" className="col-span-12 lg:col-span-6"
/> />
<div className="col-span-12 mt-12 flex flex-col items-center justify-center"> <div className="col-span-12 mt-12 flex flex-col items-center justify-center">

View File

@@ -0,0 +1,40 @@
import React from 'react';
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from '@documenso/ui/primitives/tooltip';
export function OpenPageTooltip() {
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<svg
width="16"
height="16"
viewBox="0 0 16 16"
className="ml-2 mt-2.5"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M7.49991 0.876892C3.84222 0.876892 0.877075 3.84204 0.877075 7.49972C0.877075 11.1574 3.84222 14.1226 7.49991 14.1226C11.1576 14.1226 14.1227 11.1574 14.1227 7.49972C14.1227 3.84204 11.1576 0.876892 7.49991 0.876892ZM1.82707 7.49972C1.82707 4.36671 4.36689 1.82689 7.49991 1.82689C10.6329 1.82689 13.1727 4.36671 13.1727 7.49972C13.1727 10.6327 10.6329 13.1726 7.49991 13.1726C4.36689 13.1726 1.82707 10.6327 1.82707 7.49972ZM8.24992 4.49999C8.24992 4.9142 7.91413 5.24999 7.49992 5.24999C7.08571 5.24999 6.74992 4.9142 6.74992 4.49999C6.74992 4.08577 7.08571 3.74999 7.49992 3.74999C7.91413 3.74999 8.24992 4.08577 8.24992 4.49999ZM6.00003 5.99999H6.50003H7.50003C7.77618 5.99999 8.00003 6.22384 8.00003 6.49999V9.99999H8.50003H9.00003V11H8.50003H7.50003H6.50003H6.00003V9.99999H6.50003H7.00003V6.99999H6.50003H6.00003V5.99999Z"
fill="currentColor"
fill-rule="evenodd"
clip-rule="evenodd"
></path>
</svg>
</TooltipTrigger>
<TooltipContent>
<p>
August and earlier: Active subscribers. September and beyond: Numbers of active
subscriptions.
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
}

3
apps/web/.gitignore vendored
View File

@@ -1,3 +0,0 @@
node_modules/
playwright-report/
playwright/.auth

View File

@@ -1,16 +0,0 @@
import { deleteUserAndItsData } from '@documenso/lib/server-only/user/delete-user-and-data';
async function teardown() {
if (!process.env.E2E_TEST_USERNAME || !process.env.E2E_TEST_AUTHENTICATE_USERNAME) {
return;
}
try {
await deleteUserAndItsData(process.env.E2E_TEST_USERNAME);
await deleteUserAndItsData(process.env.E2E_TEST_AUTHENTICATE_USERNAME);
} catch (e) {
throw new Error(`Error deleting user: ${e}`);
}
}
export default teardown;

View File

@@ -29,6 +29,14 @@ const config = {
transform: 'lucide-react/dist/esm/icons/{{ kebabCase member }}', transform: 'lucide-react/dist/esm/icons/{{ kebabCase member }}',
}, },
}, },
async rewrites() {
return [
{
source: '/ingest/:path*',
destination: 'https://eu.posthog.com/:path*',
},
];
},
}; };
module.exports = config; module.exports = config;

View File

@@ -8,8 +8,6 @@
"build": "next build", "build": "next build",
"start": "next start", "start": "next start",
"lint": "next lint", "lint": "next lint",
"test:dev": "playwright test",
"test:e2e": "start-server-and-test \"npm run start\" http://localhost:3000 \"playwright test\"",
"clean": "rimraf .next && rimraf node_modules", "clean": "rimraf .next && rimraf node_modules",
"copy:pdfjs": "node ../../scripts/copy-pdfjs.cjs" "copy:pdfjs": "node ../../scripts/copy-pdfjs.cjs"
}, },
@@ -43,11 +41,9 @@
"sharp": "0.32.5", "sharp": "0.32.5",
"ts-pattern": "^5.0.5", "ts-pattern": "^5.0.5",
"typescript": "5.1.6", "typescript": "5.1.6",
"zod": "^3.21.4", "zod": "^3.21.4"
"start-server-and-test": "^2.0.1"
}, },
"devDependencies": { "devDependencies": {
"@playwright/test": "^1.38.0",
"@types/formidable": "^2.0.6", "@types/formidable": "^2.0.6",
"@types/luxon": "^3.3.1", "@types/luxon": "^3.3.1",
"@types/node": "20.1.0", "@types/node": "20.1.0",

View File

@@ -1,66 +0,0 @@
import { defineConfig, devices } from '@playwright/test';
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
import { config as dotenvConfig } from 'dotenv';
dotenvConfig();
export const STORAGE_STATE = 'playwright/.auth/user.json';
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: './src/tests/e2e',
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: 'html',
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
baseURL: 'http://localhost:3000',
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
navigationTimeout: 60 * 1000,
},
/* Configure projects for major browsers */
projects: [
{ name: 'setup', testMatch: '**/*.setup.ts' },
{
name: 'Authenticated User Tests',
testMatch: '*.authenticated.spec.ts',
dependencies: ['setup', 'cleanup db'],
use: {
...devices['Desktop Chrome'],
storageState: STORAGE_STATE,
},
},
{
name: 'Unauthenticated User Tests',
dependencies: ['cleanup db'],
use: {
...devices['Desktop Chrome'],
},
testMatch: '*.unauthenticated.spec.ts',
testIgnore: ['*.setup.ts', '*.authenticated.spec.ts'],
},
{
name: 'cleanup db',
testMatch: /global.teardown\.ts/,
},
],
globalTeardown: './global.teardown.ts',
});

View File

@@ -12,7 +12,7 @@ export type AdminSectionLayoutProps = {
}; };
export default async function AdminSectionLayout({ children }: AdminSectionLayoutProps) { export default async function AdminSectionLayout({ children }: AdminSectionLayoutProps) {
const user = await getRequiredServerComponentSession(); const { user } = await getRequiredServerComponentSession();
if (!isAdmin(user)) { if (!isAdmin(user)) {
redirect('/documents'); redirect('/documents');

View File

@@ -10,7 +10,7 @@ export default function Loading() {
Documents Documents
</Link> </Link>
<h1 className="mt-4 max-w-xs grow-0 truncate text-2xl font-semibold md:text-3xl"> <h1 className="mt-4 grow-0 truncate text-2xl font-semibold md:text-3xl">
Loading Document... Loading Document...
</h1> </h1>
<div className="mt-8 grid h-[80vh] max-h-[60rem] w-full grid-cols-12 gap-x-8"> <div className="mt-8 grid h-[80vh] max-h-[60rem] w-full grid-cols-12 gap-x-8">

View File

@@ -30,11 +30,11 @@ export default async function DocumentPage({ params }: DocumentPageProps) {
redirect('/documents'); redirect('/documents');
} }
const session = await getRequiredServerComponentSession(); const { user } = await getRequiredServerComponentSession();
const document = await getDocumentById({ const document = await getDocumentById({
id: documentId, id: documentId,
userId: session.id, userId: user.id,
}).catch(() => null); }).catch(() => null);
if (!document || !document.documentData) { if (!document || !document.documentData) {
@@ -50,11 +50,11 @@ export default async function DocumentPage({ params }: DocumentPageProps) {
const [recipients, fields] = await Promise.all([ const [recipients, fields] = await Promise.all([
await getRecipientsForDocument({ await getRecipientsForDocument({
documentId, documentId,
userId: session.id, userId: user.id,
}), }),
await getFieldsForDocument({ await getFieldsForDocument({
documentId, documentId,
userId: session.id, userId: user.id,
}), }),
]); ]);
@@ -65,10 +65,7 @@ export default async function DocumentPage({ params }: DocumentPageProps) {
Documents Documents
</Link> </Link>
<h1 <h1 className="mt-4 truncate text-2xl font-semibold md:text-3xl" title={document.title}>
className="mt-4 max-w-xs truncate text-2xl font-semibold md:text-3xl"
title={document.title}
>
{document.title} {document.title}
</h1> </h1>
@@ -90,7 +87,7 @@ export default async function DocumentPage({ params }: DocumentPageProps) {
<EditDocumentForm <EditDocumentForm
className="mt-8" className="mt-8"
document={document} document={document}
user={session} user={user}
recipients={recipients} recipients={recipients}
fields={fields} fields={fields}
dataUrl={documentDataUrl} dataUrl={documentDataUrl}

View File

@@ -10,7 +10,7 @@ export default function DocumentSentPage() {
Documents Documents
</Link> </Link>
<h1 className="mt-4 max-w-xs grow-0 truncate text-2xl font-semibold md:text-3xl"> <h1 className="mt-4 grow-0 truncate text-2xl font-semibold md:text-3xl">
Loading Document... Loading Document...
</h1> </h1>
</div> </div>

View File

@@ -25,7 +25,7 @@ export type DocumentsPageProps = {
}; };
export default async function DocumentsPage({ searchParams = {} }: DocumentsPageProps) { export default async function DocumentsPage({ searchParams = {} }: DocumentsPageProps) {
const user = await getRequiredServerComponentSession(); const { user } = await getRequiredServerComponentSession();
const stats = await getStats({ const stats = await getStats({
user, user,

View File

@@ -24,7 +24,7 @@ export default async function AuthenticatedDashboardLayout({
redirect('/signin'); redirect('/signin');
} }
const user = await getRequiredServerComponentSession(); const { user } = await getRequiredServerComponentSession();
return ( return (
<NextAuthProvider session={session}> <NextAuthProvider session={session}>

View File

@@ -12,7 +12,7 @@ import { Button } from '@documenso/ui/primitives/button';
import { LocaleDate } from '~/components/formatter/locale-date'; import { LocaleDate } from '~/components/formatter/locale-date';
export default async function BillingSettingsPage() { export default async function BillingSettingsPage() {
const user = await getRequiredServerComponentSession(); const { user } = await getRequiredServerComponentSession();
const isBillingEnabled = await getServerComponentFlag('app_billing'); const isBillingEnabled = await getServerComponentFlag('app_billing');

View File

@@ -3,7 +3,7 @@ import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-
import { PasswordForm } from '~/components/forms/password'; import { PasswordForm } from '~/components/forms/password';
export default async function PasswordSettingsPage() { export default async function PasswordSettingsPage() {
const user = await getRequiredServerComponentSession(); const { user } = await getRequiredServerComponentSession();
return ( return (
<div> <div>

View File

@@ -3,7 +3,7 @@ import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-
import { ProfileForm } from '~/components/forms/profile'; import { ProfileForm } from '~/components/forms/profile';
export default async function ProfileSettingsPage() { export default async function ProfileSettingsPage() {
const user = await getRequiredServerComponentSession(); const { user } = await getRequiredServerComponentSession();
return ( return (
<div> <div>

View File

@@ -53,7 +53,7 @@ export default async function CompletedSigningPage({
recipient.email; recipient.email;
return ( return (
<div className="flex flex-col items-center pt-24 lg:pt-36 xl:pt-44"> <div className="-mx-4 flex max-w-[100vw] flex-col items-center overflow-x-hidden px-4 pt-24 md:-mx-8 md:px-8 lg:pt-36 xl:pt-44">
{/* Card with recipient */} {/* Card with recipient */}
<SigningCard3D name={recipientName} signingCelebrationImage={signingCelebration} /> <SigningCard3D name={recipientName} signingCelebrationImage={signingCelebration} />

View File

@@ -106,7 +106,7 @@ export const ShareButton = ({ token, documentId }: ShareButtonProps) => {
</Button> </Button>
</DialogTrigger> </DialogTrigger>
<DialogContent> <DialogContent position="end">
<DialogHeader> <DialogHeader>
<DialogTitle>Share</DialogTitle> <DialogTitle>Share</DialogTitle>
@@ -119,7 +119,7 @@ export const ShareButton = ({ token, documentId }: ShareButtonProps) => {
<span className="font-medium text-blue-400">@documenso</span> <span className="font-medium text-blue-400">@documenso</span>
. Check it out! . Check it out!
<span className="mt-2 block" /> <span className="mt-2 block" />
<span className="font-medium text-blue-400"> <span className="break-all font-medium text-blue-400">
{window.location.origin}/share/{shareLink?.slug || '...'} {window.location.origin}/share/{shareLink?.slug || '...'}
</span> </span>
</div> </div>

View File

@@ -4,6 +4,7 @@ import { useMemo, useState } from 'react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useSession } from 'next-auth/react';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { completeDocumentWithToken } from '@documenso/lib/server-only/document/complete-document-with-token'; 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) => { export const SigningForm = ({ document, recipient, fields }: SigningFormProps) => {
const router = useRouter(); const router = useRouter();
const { data: session } = useSession();
const { fullName, signature, setFullName, setSignature } = useRequiredSigningContext(); const { fullName, signature, setFullName, setSignature } = useRequiredSigningContext();
@@ -60,7 +62,11 @@ export const SigningForm = ({ document, recipient, fields }: SigningFormProps) =
return ( return (
<form <form
className={cn( className={cn(
'dark:bg-background border-border bg-widget sticky top-20 flex h-full max-h-[80rem] flex-col rounded-xl border px-4 py-6', 'dark:bg-background border-border bg-widget sticky flex h-full flex-col rounded-xl border px-4 py-6',
{
'top-20 max-h-[min(68rem,calc(100vh-6rem))]': session,
'top-4 max-h-[min(68rem,calc(100vh-2rem))]': !session,
},
)} )}
onSubmit={handleSubmit(onFormSubmit)} onSubmit={handleSubmit(onFormSubmit)}
> >
@@ -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" className="dark:bg-muted dark:hover:bg-muted/80 w-full bg-black/5 hover:bg-black/10"
variant="secondary" variant="secondary"
size="lg" size="lg"
disabled={typeof window !== 'undefined' && window.history.length <= 1}
onClick={() => router.back()} onClick={() => router.back()}
> >
Cancel Cancel

View File

@@ -10,11 +10,11 @@ export type SigningLayoutProps = {
}; };
export default async function SigningLayout({ children }: SigningLayoutProps) { export default async function SigningLayout({ children }: SigningLayoutProps) {
const user = await getServerComponentSession(); const { user, session } = await getServerComponentSession();
return ( return (
<NextAuthProvider> <NextAuthProvider session={session}>
<div className="min-h-screen overflow-hidden"> <div className="min-h-screen">
{user && <AuthenticatedHeader user={user} />} {user && <AuthenticatedHeader user={user} />}
<main className="mb-8 mt-8 px-4 md:mb-12 md:mt-12 md:px-8">{children}</main> <main className="mb-8 mt-8 px-4 md:mb-12 md:mt-12 md:px-8">{children}</main>

View File

@@ -51,7 +51,7 @@ export default async function SigningPage({ params: { token } }: SigningPageProp
.then((buffer) => Buffer.from(buffer).toString('base64')) .then((buffer) => Buffer.from(buffer).toString('base64'))
.then((data) => `data:application/pdf;base64,${data}`); .then((data) => `data:application/pdf;base64,${data}`);
const user = await getServerComponentSession(); const { user } = await getServerComponentSession();
if ( if (
document.status === DocumentStatus.COMPLETED || document.status === DocumentStatus.COMPLETED ||
@@ -62,7 +62,7 @@ export default async function SigningPage({ params: { token } }: SigningPageProp
return ( return (
<SigningProvider email={recipient.email} fullName={recipient.name} signature={user?.signature}> <SigningProvider email={recipient.email} fullName={recipient.name} signature={user?.signature}>
<div className="mx-auto w-full max-w-screen-xl px-4 md:px-8"> <div className="mx-auto w-full max-w-screen-xl">
<h1 className="mt-4 truncate text-2xl font-semibold md:text-3xl" title={document.title}> <h1 className="mt-4 truncate text-2xl font-semibold md:text-3xl" title={document.title}>
{document.title} {document.title}
</h1> </h1>

View File

@@ -6,7 +6,7 @@ import { Button } from '@documenso/ui/primitives/button';
import NotFoundPartial from '~/components/partials/not-found'; import NotFoundPartial from '~/components/partials/not-found';
export default async function NotFound() { export default async function NotFound() {
const session = await getServerComponentSession(); const { session } = await getServerComponentSession();
return ( return (
<NotFoundPartial> <NotFoundPartial>

View File

@@ -11,10 +11,10 @@ export type AddFieldsActionInput = TAddFieldsFormSchema & {
export const addFields = async ({ documentId, fields }: AddFieldsActionInput) => { export const addFields = async ({ documentId, fields }: AddFieldsActionInput) => {
'use server'; 'use server';
const { id: userId } = await getRequiredServerComponentSession(); const { user } = await getRequiredServerComponentSession();
await setFieldsForDocument({ await setFieldsForDocument({
userId, userId: user.id,
documentId, documentId,
fields: fields.map((field) => ({ fields: fields.map((field) => ({
id: field.nativeId, id: field.nativeId,

View File

@@ -11,10 +11,10 @@ export type AddSignersActionInput = TAddSignersFormSchema & {
export const addSigners = async ({ documentId, signers }: AddSignersActionInput) => { export const addSigners = async ({ documentId, signers }: AddSignersActionInput) => {
'use server'; 'use server';
const { id: userId } = await getRequiredServerComponentSession(); const { user } = await getRequiredServerComponentSession();
await setRecipientsForDocument({ await setRecipientsForDocument({
userId, userId: user.id,
documentId, documentId,
recipients: signers.map((signer) => ({ recipients: signers.map((signer) => ({
id: signer.nativeId, id: signer.nativeId,

View File

@@ -12,7 +12,7 @@ export type CompleteDocumentActionInput = TAddSubjectFormSchema & {
export const completeDocument = async ({ documentId, email }: CompleteDocumentActionInput) => { export const completeDocument = async ({ documentId, email }: CompleteDocumentActionInput) => {
'use server'; 'use server';
const { id: userId } = await getRequiredServerComponentSession(); const { user } = await getRequiredServerComponentSession();
if (email.message || email.subject) { if (email.message || email.subject) {
await upsertDocumentMeta({ await upsertDocumentMeta({
@@ -23,7 +23,7 @@ export const completeDocument = async ({ documentId, email }: CompleteDocumentAc
} }
return await sendDocument({ return await sendDocument({
userId, userId: user.id,
documentId, documentId,
}); });
}; };

View File

@@ -1,35 +0,0 @@
import { type Page, expect, test as setup } from '@playwright/test';
import { STORAGE_STATE } from '../../../playwright.config';
const username = process.env.E2E_TEST_AUTHENTICATE_USERNAME || '';
const email = process.env.E2E_TEST_AUTHENTICATE_USER_EMAIL || '';
const password = process.env.E2E_TEST_AUTHENTICATE_USER_PASSWORD || '';
setup('authenticate', async ({ page }: { page: Page }) => {
await page.goto('/signup');
await page.getByLabel('Name').fill(username);
await page.getByLabel('Email').fill(email);
await page.getByLabel('Password', { exact: true }).fill(password);
const canvas = page.locator('canvas');
const box = await canvas.boundingBox();
if (box) {
await page.mouse.move(box.x + box.width / 8, box.y + box.height / 6);
await page.mouse.down();
await page.mouse.move(box.x + box.width / 8, box.y + box.height / 6);
await page.mouse.up();
}
await page.getByRole('button', { name: 'Sign Up' }).click();
await page.goto('/');
await page.getByLabel('Email').fill(email);
await page.getByLabel('Password', { exact: true }).fill(password);
await page.getByRole('button', { name: 'Sign In' }).click();
await page.waitForURL('/documents');
await expect(page).toHaveURL('/documents');
await page.context().storageState({ path: STORAGE_STATE });
});

View File

@@ -1,57 +0,0 @@
%PDF-1.4
%<25><><EFBFBD><EFBFBD>
1 0 obj
<</Title (documenso)
/Producer (Skia/PDF m118 Google Docs Renderer)>>
endobj
3 0 obj
<</ca 1
/BM /Normal>>
endobj
4 0 obj
<</Length 84>> stream
1 0 0 -1 0 842 cm
q
.75 0 0 .75 0 0 cm
1 1 1 RG 1 1 1 rg
/G3 gs
0 0 794 1123 re
f
Q
endstream
endobj
2 0 obj
<</Type /Page
/Resources <</ProcSet [/PDF /Text /ImageB /ImageC /ImageI]
/ExtGState <</G3 3 0 R>>>>
/MediaBox [0 0 596 842]
/Contents 4 0 R
/StructParents 0
/Parent 5 0 R>>
endobj
5 0 obj
<</Type /Pages
/Count 1
/Kids [2 0 R]>>
endobj
6 0 obj
<</Type /Catalog
/Pages 5 0 R>>
endobj
xref
0 7
0000000000 65535 f
0000000015 00000 n
0000000269 00000 n
0000000100 00000 n
0000000137 00000 n
0000000457 00000 n
0000000512 00000 n
trailer
<</Size 7
/Root 6 0 R
/Info 1 0 R>>
startxref
559
%%EOF

View File

@@ -1,45 +0,0 @@
import { type Page, expect, test } from '@playwright/test';
test.use({ storageState: { cookies: [], origins: [] } });
/*
Using them sequentially so the 2nd test
uses the details from the 1st (registration) test
*/
test.describe.configure({ mode: 'serial' });
const username = process.env.E2E_TEST_USERNAME || '';
const email = process.env.E2E_TEST_USER_EMAIL || '';
const password = process.env.E2E_TEST_USER_PASSWORD || '';
test('user can sign up with email and password', async ({ page }: { page: Page }) => {
await page.goto('/signup');
await page.getByLabel('Name').fill(username);
await page.getByLabel('Email').fill(email);
await page.getByLabel('Password', { exact: true }).fill(password);
const canvas = page.locator('canvas');
const box = await canvas.boundingBox();
if (box) {
await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
await page.mouse.down();
await page.mouse.move(box.x + box.width / 4, box.y + box.height / 4);
await page.mouse.up();
}
await page.getByRole('button', { name: 'Sign Up' }).click();
await page.waitForURL('/documents');
await expect(page).toHaveURL('/documents');
});
test('user can login with user and password', async ({ page }: { page: Page }) => {
await page.goto('/signin');
await page.getByLabel('Email').fill(email);
await page.getByLabel('Password', { exact: true }).fill(password);
await page.getByRole('button', { name: 'Sign In' }).click();
await page.waitForURL('/documents');
await expect(page).toHaveURL('/documents');
});

View File

@@ -1,38 +0,0 @@
import path from 'path';
import { expect, test } from '../test-fixtures/documents-page/documents-page';
const signer_name = process.env.E2E_TEST_SIGNER_NAME;
const signer_email = process.env.E2E_TEST_SIGNER_EMAIL;
const signing_subject = process.env.E2E_TEST_SIGNING_SUBJECT;
const signing_message = process.env.E2E_TEST_SIGNING_MESSAGE;
if (!signer_name || !signer_email || !signing_subject || !signing_message) {
throw new Error('Required environment variables for tests are not defined');
}
test.describe('Document upload test', () => {
test.beforeEach(async ({ documentsPage }) => {
await documentsPage.uploadDocument(path.join(__dirname, './documenso.pdf'));
});
test('user can see /documents page', async ({ page, documentsPage }) => {
await documentsPage.goToDocumentsPage();
await expect(page).toHaveTitle('Documenso - The Open Source DocuSign Alternative');
});
test('user can upload a document succesfully', async ({ page }) => {
await expect(page.getByText('Drag & drop your document here.')).toBeVisible();
});
test('user can send the document for signing succesfully', async ({ documentsPage, page }) => {
await documentsPage.addSigner(signer_email, signer_name);
await documentsPage.addSignatureField(signer_name);
await documentsPage.addSubjectAndMessage(signing_subject, signing_message);
await expect(page).toHaveURL('/documents');
await expect(page.getByRole('status').locator('div').nth(2)).toHaveText(
'Your document has been sent successfully.',
);
});
});

View File

@@ -1,49 +0,0 @@
import type { Locator, Page } from '@playwright/test';
export class DocumentsPage {
private readonly fileInput: Locator;
private readonly subject: Locator;
private readonly message: Locator;
// eslint-disable-next-line no-unused-vars
constructor(public readonly page: Page) {
this.fileInput = this.page.locator('input[type=file]');
this.subject = this.page
.locator('div')
.filter({ hasText: /^Subject \(Optional\)$/ })
.locator('input');
this.message = this.page
.locator('div')
.filter({ hasText: /^Message \(Optional\)$/ })
.locator('textarea');
}
async goToDocumentsPage() {
await this.page.goto('/documents');
}
async uploadDocument(filePath: string) {
await this.goToDocumentsPage();
await this.fileInput.setInputFiles(filePath);
}
async addSigner(email: string, name: string) {
await this.page.getByLabel('Email*').fill(email);
await this.page.getByLabel('Name').fill(name);
await this.page.getByRole('button', { name: 'Continue' }).click();
}
async addSignatureField(name: string) {
await this.page
.getByRole('button', { name: `${name} Signature` })
.dragTo(this.page.locator('canvas'));
await this.page.getByRole('button', { name: 'Continue' }).click();
}
async addSubjectAndMessage(subject: string, message: string) {
await this.subject.fill(subject);
await this.message.fill(message);
await this.page.getByRole('button', { name: 'Send' }).click();
}
}

View File

@@ -1,13 +0,0 @@
import { test as base } from '@playwright/test';
import { DocumentsPage } from './DocumentsPageObject';
export const test = base.extend<{ documentsPage: DocumentsPage }>({
documentsPage: async ({ page }, use) => {
const documentsPage = new DocumentsPage(page);
await use(documentsPage);
},
});
export { expect } from '@playwright/test';

346
package-lock.json generated
View File

@@ -98,13 +98,11 @@
"react-icons": "^4.8.0", "react-icons": "^4.8.0",
"react-rnd": "^10.4.1", "react-rnd": "^10.4.1",
"sharp": "0.32.5", "sharp": "0.32.5",
"start-server-and-test": "^2.0.1",
"ts-pattern": "^5.0.5", "ts-pattern": "^5.0.5",
"typescript": "5.1.6", "typescript": "5.1.6",
"zod": "^3.21.4" "zod": "^3.21.4"
}, },
"devDependencies": { "devDependencies": {
"@playwright/test": "^1.38.0",
"@types/formidable": "^2.0.6", "@types/formidable": "^2.0.6",
"@types/luxon": "^3.3.1", "@types/luxon": "^3.3.1",
"@types/node": "20.1.0", "@types/node": "20.1.0",
@@ -2452,19 +2450,6 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/@hapi/hoek": {
"version": "9.3.0",
"resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz",
"integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ=="
},
"node_modules/@hapi/topo": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz",
"integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==",
"dependencies": {
"@hapi/hoek": "^9.0.0"
}
},
"node_modules/@hookform/resolvers": { "node_modules/@hookform/resolvers": {
"version": "3.3.0", "version": "3.3.0",
"resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.3.0.tgz", "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.3.0.tgz",
@@ -3801,21 +3786,6 @@
"url": "https://opencollective.com/unts" "url": "https://opencollective.com/unts"
} }
}, },
"node_modules/@playwright/test": {
"version": "1.38.1",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.38.1.tgz",
"integrity": "sha512-NqRp8XMwj3AK+zKLbZShl0r/9wKgzqI/527bkptKXomtuo+dOjU9NdMASQ8DNC9z9zLOMbG53T4eihYr3XR+BQ==",
"dev": true,
"dependencies": {
"playwright": "1.38.1"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=16"
}
},
"node_modules/@prisma/client": { "node_modules/@prisma/client": {
"version": "5.3.1", "version": "5.3.1",
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.3.1.tgz", "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.3.1.tgz",
@@ -5465,24 +5435,6 @@
"url": "https://ko-fi.com/killymxi" "url": "https://ko-fi.com/killymxi"
} }
}, },
"node_modules/@sideway/address": {
"version": "4.1.4",
"resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.4.tgz",
"integrity": "sha512-7vwq+rOHVWjyXxVlR76Agnvhy8I9rpzjosTESvmhNeXOXdZZB15Fl+TI9x1SiHZH5Jv2wTGduSxFDIaq0m3DUw==",
"dependencies": {
"@hapi/hoek": "^9.0.0"
}
},
"node_modules/@sideway/formula": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz",
"integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg=="
},
"node_modules/@sideway/pinpoint": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz",
"integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ=="
},
"node_modules/@sindresorhus/slugify": { "node_modules/@sindresorhus/slugify": {
"version": "2.2.1", "version": "2.2.1",
"resolved": "https://registry.npmjs.org/@sindresorhus/slugify/-/slugify-2.2.1.tgz", "resolved": "https://registry.npmjs.org/@sindresorhus/slugify/-/slugify-2.2.1.tgz",
@@ -7667,14 +7619,6 @@
"url": "https://github.com/sponsors/wooorm" "url": "https://github.com/sponsors/wooorm"
} }
}, },
"node_modules/check-more-types": {
"version": "2.24.0",
"resolved": "https://registry.npmjs.org/check-more-types/-/check-more-types-2.24.0.tgz",
"integrity": "sha512-Pj779qHxV2tuapviy1bSZNEL1maXr13bPYpsvSDB68HlYcYuhlDrmGd63i0JHMCLKzc7rUSNIrpdJlhVlNwrxA==",
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/chokidar": { "node_modules/chokidar": {
"version": "3.5.3", "version": "3.5.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
@@ -9180,11 +9124,6 @@
"node": ">=12" "node": ">=12"
} }
}, },
"node_modules/duplexer": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz",
"integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg=="
},
"node_modules/duplexer2": { "node_modules/duplexer2": {
"version": "0.1.4", "version": "0.1.4",
"resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz",
@@ -10389,20 +10328,6 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/event-stream": {
"version": "3.3.4",
"resolved": "https://registry.npmjs.org/event-stream/-/event-stream-3.3.4.tgz",
"integrity": "sha512-QHpkERcGsR0T7Qm3HNJSyXKEEj8AHNxkY3PK8TS2KJvQ7NiSHe3DDpwVKKtoYprL/AreyzFBeIkBIWChAqn60g==",
"dependencies": {
"duplexer": "~0.1.1",
"from": "~0",
"map-stream": "~0.1.0",
"pause-stream": "0.0.11",
"split": "0.3",
"stream-combiner": "~0.0.4",
"through": "~2.3.1"
}
},
"node_modules/eventemitter3": { "node_modules/eventemitter3": {
"version": "4.0.7", "version": "4.0.7",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
@@ -10802,11 +10727,6 @@
} }
} }
}, },
"node_modules/from": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/from/-/from-0.1.7.tgz",
"integrity": "sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g=="
},
"node_modules/fs-constants": { "node_modules/fs-constants": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
@@ -12245,18 +12165,6 @@
"resolved": "https://registry.npmjs.org/jju/-/jju-1.4.0.tgz", "resolved": "https://registry.npmjs.org/jju/-/jju-1.4.0.tgz",
"integrity": "sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==" "integrity": "sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA=="
}, },
"node_modules/joi": {
"version": "17.10.2",
"resolved": "https://registry.npmjs.org/joi/-/joi-17.10.2.tgz",
"integrity": "sha512-hcVhjBxRNW/is3nNLdGLIjkgXetkeGc2wyhydhz8KumG23Aerk4HPjU5zaPAMRqXQFc0xNqXTC7+zQjxr0GlKA==",
"dependencies": {
"@hapi/hoek": "^9.0.0",
"@hapi/topo": "^5.0.0",
"@sideway/address": "^4.1.3",
"@sideway/formula": "^3.0.1",
"@sideway/pinpoint": "^2.0.0"
}
},
"node_modules/jose": { "node_modules/jose": {
"version": "4.14.4", "version": "4.14.4",
"resolved": "https://registry.npmjs.org/jose/-/jose-4.14.4.tgz", "resolved": "https://registry.npmjs.org/jose/-/jose-4.14.4.tgz",
@@ -12491,14 +12399,6 @@
"language-subtag-registry": "~0.3.2" "language-subtag-registry": "~0.3.2"
} }
}, },
"node_modules/lazy-ass": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/lazy-ass/-/lazy-ass-1.6.0.tgz",
"integrity": "sha512-cc8oEVoctTvsFZ/Oje/kGnHbpWHYBe8IAJe4C0QNc3t8uM/0Y8+erSz/7Y1ALuXTEZTMvxXwO6YbX1ey3ujiZw==",
"engines": {
"node": "> 0.8"
}
},
"node_modules/leac": { "node_modules/leac": {
"version": "0.6.0", "version": "0.6.0",
"resolved": "https://registry.npmjs.org/leac/-/leac-0.6.0.tgz", "resolved": "https://registry.npmjs.org/leac/-/leac-0.6.0.tgz",
@@ -13046,11 +12946,6 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/map-stream": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.1.0.tgz",
"integrity": "sha512-CkYQrPYZfWnu/DAmVCpTSX/xHpKZ80eKh2lAkyA6AJTef6bW+6JpbQZN5rofum7da+SyN1bi5ctTm+lTfcCW3g=="
},
"node_modules/markdown-extensions": { "node_modules/markdown-extensions": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/markdown-extensions/-/markdown-extensions-1.1.1.tgz", "resolved": "https://registry.npmjs.org/markdown-extensions/-/markdown-extensions-1.1.1.tgz",
@@ -15104,14 +14999,6 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/pause-stream": {
"version": "0.0.11",
"resolved": "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz",
"integrity": "sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A==",
"dependencies": {
"through": "~2.3"
}
},
"node_modules/pdf-lib": { "node_modules/pdf-lib": {
"version": "1.17.1", "version": "1.17.1",
"resolved": "https://registry.npmjs.org/pdf-lib/-/pdf-lib-1.17.1.tgz", "resolved": "https://registry.npmjs.org/pdf-lib/-/pdf-lib-1.17.1.tgz",
@@ -15210,36 +15097,6 @@
"node": ">= 6" "node": ">= 6"
} }
}, },
"node_modules/playwright": {
"version": "1.38.1",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.38.1.tgz",
"integrity": "sha512-oRMSJmZrOu1FP5iu3UrCx8JEFRIMxLDM0c/3o4bpzU5Tz97BypefWf7TuTNPWeCe279TPal5RtPPZ+9lW/Qkow==",
"dev": true,
"dependencies": {
"playwright-core": "1.38.1"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=16"
},
"optionalDependencies": {
"fsevents": "2.3.2"
}
},
"node_modules/playwright-core": {
"version": "1.38.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.38.1.tgz",
"integrity": "sha512-tQqNFUKa3OfMf4b2jQ7aGLB8o9bS3bOY0yMEtldtC2+spf8QXG9zvXLTXUeRsoNuxEYMgLYR+NXfAa1rjKRcrg==",
"dev": true,
"bin": {
"playwright-core": "cli.js"
},
"engines": {
"node": ">=16"
}
},
"node_modules/postcss": { "node_modules/postcss": {
"version": "8.4.27", "version": "8.4.27",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.27.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.27.tgz",
@@ -15790,20 +15647,6 @@
"resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz",
"integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==" "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q=="
}, },
"node_modules/ps-tree": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/ps-tree/-/ps-tree-1.2.0.tgz",
"integrity": "sha512-0VnamPPYHl4uaU/nSFeZZpR21QAWRz+sRv4iW9+v/GS/J5U5iZB5BNN6J0RMoOvdx2gWM2+ZFMIm58q24e4UYA==",
"dependencies": {
"event-stream": "=3.3.4"
},
"bin": {
"ps-tree": "bin/ps-tree.js"
},
"engines": {
"node": ">= 0.10"
}
},
"node_modules/pump": { "node_modules/pump": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
@@ -17115,6 +16958,29 @@
"node": ">=0.10.5" "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": { "node_modules/resolve": {
"version": "1.22.2", "version": "1.22.2",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz",
@@ -17424,14 +17290,6 @@
"resolved": "https://registry.npmjs.org/rusha/-/rusha-0.8.14.tgz", "resolved": "https://registry.npmjs.org/rusha/-/rusha-0.8.14.tgz",
"integrity": "sha512-cLgakCUf6PedEu15t8kbsjnwIFFR2D4RfL+W3iWFJ4iac7z4B0ZI8fxy4R3J956kAI68HclCFGL8MPoUVC3qVA==" "integrity": "sha512-cLgakCUf6PedEu15t8kbsjnwIFFR2D4RfL+W3iWFJ4iac7z4B0ZI8fxy4R3J956kAI68HclCFGL8MPoUVC3qVA=="
}, },
"node_modules/rxjs": {
"version": "7.8.1",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
"integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==",
"dependencies": {
"tslib": "^2.1.0"
}
},
"node_modules/sade": { "node_modules/sade": {
"version": "1.8.1", "version": "1.8.1",
"resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz",
@@ -17807,17 +17665,6 @@
"resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.13.tgz", "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.13.tgz",
"integrity": "sha512-XkD+zwiqXHikFZm4AX/7JSCXA98U5Db4AFd5XUg/+9UNtnH75+Z9KxtpYiJZx36mUDVOwH83pl7yvCer6ewM3w==" "integrity": "sha512-XkD+zwiqXHikFZm4AX/7JSCXA98U5Db4AFd5XUg/+9UNtnH75+Z9KxtpYiJZx36mUDVOwH83pl7yvCer6ewM3w=="
}, },
"node_modules/split": {
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/split/-/split-0.3.3.tgz",
"integrity": "sha512-wD2AeVmxXRBoX44wAycgjVpMhvbwdI2aZjCkvfNcH1YqHQvJVa1duWc73OyVGJUc05fhFaTZeQ/PYsrmyH0JVA==",
"dependencies": {
"through": "2"
},
"engines": {
"node": "*"
}
},
"node_modules/split2": { "node_modules/split2": {
"version": "3.2.2", "version": "3.2.2",
"resolved": "https://registry.npmjs.org/split2/-/split2-3.2.2.tgz", "resolved": "https://registry.npmjs.org/split2/-/split2-3.2.2.tgz",
@@ -17849,121 +17696,6 @@
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
}, },
"node_modules/start-server-and-test": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/start-server-and-test/-/start-server-and-test-2.0.1.tgz",
"integrity": "sha512-8PFo4DLLLCDMuS51/BEEtE1m9CAXw1LNVtZSS1PzkYQh6Qf9JUwM4huYeSoUumaaoAyuwYBwCa9OsrcpMqcOdQ==",
"dependencies": {
"arg": "^5.0.2",
"bluebird": "3.7.2",
"check-more-types": "2.24.0",
"debug": "4.3.4",
"execa": "5.1.1",
"lazy-ass": "1.6.0",
"ps-tree": "1.2.0",
"wait-on": "7.0.1"
},
"bin": {
"server-test": "src/bin/start.js",
"start-server-and-test": "src/bin/start.js",
"start-test": "src/bin/start.js"
},
"engines": {
"node": ">=16"
}
},
"node_modules/start-server-and-test/node_modules/arg": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
"integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="
},
"node_modules/start-server-and-test/node_modules/bluebird": {
"version": "3.7.2",
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
"integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg=="
},
"node_modules/start-server-and-test/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/start-server-and-test/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/start-server-and-test/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/start-server-and-test/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/start-server-and-test/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/start-server-and-test/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/start-server-and-test/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/statuses": { "node_modules/statuses": {
"version": "1.5.0", "version": "1.5.0",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
@@ -17972,14 +17704,6 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/stream-combiner": {
"version": "0.0.4",
"resolved": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.0.4.tgz",
"integrity": "sha512-rT00SPnTVyRsaSz5zgSPma/aHSOic5U1prhYdRy5HS2kTZviFpmDgzilbtsJsxiroqACmayynDN/9VzIbX5DOw==",
"dependencies": {
"duplexer": "~0.1.1"
}
},
"node_modules/stream-shift": { "node_modules/stream-shift": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz",
@@ -18525,7 +18249,8 @@
"node_modules/through": { "node_modules/through": {
"version": "2.3.8", "version": "2.3.8",
"resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
"integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==" "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==",
"dev": true
}, },
"node_modules/through2": { "node_modules/through2": {
"version": "4.0.2", "version": "4.0.2",
@@ -19786,24 +19511,6 @@
"d3-timer": "^3.0.1" "d3-timer": "^3.0.1"
} }
}, },
"node_modules/wait-on": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/wait-on/-/wait-on-7.0.1.tgz",
"integrity": "sha512-9AnJE9qTjRQOlTZIldAaf/da2eW0eSRSgcqq85mXQja/DW3MriHxkpODDSUEg+Gri/rKEcXUZHe+cevvYItaog==",
"dependencies": {
"axios": "^0.27.2",
"joi": "^17.7.0",
"lodash": "^4.17.21",
"minimist": "^1.2.7",
"rxjs": "^7.8.0"
},
"bin": {
"wait-on": "bin/wait-on"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/watchpack": { "node_modules/watchpack": {
"version": "2.4.0", "version": "2.4.0",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz",
@@ -20089,7 +19796,8 @@
"dependencies": { "dependencies": {
"@react-email/components": "^0.0.7", "@react-email/components": "^0.0.7",
"nodemailer": "^6.9.3", "nodemailer": "^6.9.3",
"react-email": "^1.9.4" "react-email": "^1.9.4",
"resend": "^1.1.0"
}, },
"devDependencies": { "devDependencies": {
"@documenso/tailwind-config": "*", "@documenso/tailwind-config": "*",

View File

@@ -4,12 +4,10 @@
"build": "turbo run build", "build": "turbo run build",
"dev": "turbo run dev --filter=@documenso/web --filter=@documenso/marketing", "dev": "turbo run dev --filter=@documenso/web --filter=@documenso/marketing",
"start": "cd apps && cd web && next start", "start": "cd apps && cd web && next start",
"test": "cd apps && cd web && npm run test:e2e",
"lint": "turbo run lint", "lint": "turbo run lint",
"format": "prettier --write \"**/*.{js,jsx,cjs,mjs,ts,tsx,cts,mts,mdx}\"", "format": "prettier --write \"**/*.{js,jsx,cjs,mjs,ts,tsx,cts,mts,mdx}\"",
"prepare": "husky install", "prepare": "husky install",
"commitlint": "commitlint --edit", "commitlint": "commitlint --edit",
"ci": "turbo run build test:e2e lint",
"clean": "turbo run clean && rimraf node_modules" "clean": "turbo run clean && rimraf node_modules"
}, },
"engines": { "engines": {

View File

@@ -1,6 +1,7 @@
import { createTransport } from 'nodemailer'; import { createTransport } from 'nodemailer';
import { MailChannelsTransport } from './transports/mailchannels'; import { MailChannelsTransport } from './transports/mailchannels';
import { ResendTransport } from './transports/resend';
const getTransport = () => { const getTransport = () => {
const transport = process.env.NEXT_PRIVATE_SMTP_TRANSPORT ?? 'smtp-auth'; 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 (transport === 'smtp-api') {
if (!process.env.NEXT_PRIVATE_SMTP_HOST || !process.env.NEXT_PRIVATE_SMTP_APIKEY) { if (!process.env.NEXT_PRIVATE_SMTP_HOST || !process.env.NEXT_PRIVATE_SMTP_APIKEY) {
throw new Error( throw new Error(

View File

@@ -19,7 +19,8 @@
"dependencies": { "dependencies": {
"@react-email/components": "^0.0.7", "@react-email/components": "^0.0.7",
"nodemailer": "^6.9.3", "nodemailer": "^6.9.3",
"react-email": "^1.9.4" "react-email": "^1.9.4",
"resend": "^1.1.0"
}, },
"devDependencies": { "devDependencies": {
"@documenso/tailwind-config": "*", "@documenso/tailwind-config": "*",

View File

@@ -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<SentMessageInfo> {
public name = 'ResendMailTransport';
public version = VERSION;
private _client: Resend;
private _options: ResendTransportOptions;
public static makeTransport(options: Partial<ResendTransportOptions>) {
return new ResendTransport(options);
}
constructor(options: Partial<ResendTransportOptions>) {
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');
});
}
}

View File

@@ -1,5 +1,10 @@
{ {
"extends": "@documenso/tsconfig/react-library.json", "extends": "@documenso/tsconfig/react-library.json",
"compilerOptions": {
"types": [
"@documenso/tsconfig/process-env.d.ts",
]
},
"include": ["**/*.ts", "**/*.tsx", "**/*.d.ts", "**/*.json"], "include": ["**/*.ts", "**/*.tsx", "**/*.d.ts", "**/*.json"],
"exclude": ["dist", "build", "node_modules"] "exclude": ["dist", "build", "node_modules"]
} }

View File

@@ -1,3 +1,5 @@
import { APP_BASE_URL } from './app';
/** /**
* The flag name for global session recording feature flag. * The flag name for global session recording feature flag.
*/ */
@@ -23,7 +25,7 @@ export const LOCAL_FEATURE_FLAGS: Record<string, boolean> = {
*/ */
export function extractPostHogConfig(): { key: string; host: string } | null { export function extractPostHogConfig(): { key: string; host: string } | null {
const postHogKey = process.env.NEXT_PUBLIC_POSTHOG_KEY; 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) { if (!postHogKey || !postHogHost) {
return null; return null;

View File

@@ -15,7 +15,7 @@ export const getServerSession = async ({ req, res }: GetServerSessionOptions) =>
const session = await getNextAuthServerSession(req, res, NEXT_AUTH_OPTIONS); const session = await getNextAuthServerSession(req, res, NEXT_AUTH_OPTIONS);
if (!session || !session.user?.email) { if (!session || !session.user?.email) {
return null; return { user: null, session: null };
} }
const user = await prisma.user.findFirstOrThrow({ 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 () => { export const getServerComponentSession = async () => {
const session = await getNextAuthServerSession(NEXT_AUTH_OPTIONS); const session = await getNextAuthServerSession(NEXT_AUTH_OPTIONS);
if (!session || !session.user?.email) { if (!session || !session.user?.email) {
return null; return { user: null, session: null };
} }
const user = await prisma.user.findFirstOrThrow({ const user = await prisma.user.findFirstOrThrow({
@@ -40,15 +40,15 @@ export const getServerComponentSession = async () => {
}, },
}); });
return user; return { user, session };
}; };
export const getRequiredServerComponentSession = async () => { export const getRequiredServerComponentSession = async () => {
const session = await getServerComponentSession(); const { user, session } = await getServerComponentSession();
if (!session) { if (!user || !session) {
throw new Error('No session found'); throw new Error('No session found');
} }
return session; return { user, session };
}; };

View File

@@ -14,7 +14,6 @@ export const viewedDocument = async ({ token }: ViewedDocumentOptions) => {
}); });
if (!recipient) { if (!recipient) {
console.warn(`No recipient found for token ${token}`);
return; return;
} }

View File

@@ -57,12 +57,13 @@ export const setRecipientsForDocument = async ({
return { return {
...recipient, ...recipient,
...existing, _persisted: existing,
}; };
}) })
.filter((recipient) => { .filter((recipient) => {
return ( 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) => linkedRecipients.map((recipient) =>
prisma.recipient.upsert({ prisma.recipient.upsert({
where: { where: {
id: recipient.id ?? -1, id: recipient._persisted?.id ?? -1,
documentId, documentId,
}, },
update: { update: {

View File

@@ -1,41 +0,0 @@
import { prisma } from '@documenso/prisma';
export const deleteUserAndItsData = async (name: string) => {
const user = await prisma.user.findFirst({
where: {
name: {
contains: name,
},
},
});
if (!user) {
throw new Error(`User with name ${name} not found`);
}
const document = await prisma.document.findMany({
where: {
userId: user.id,
},
select: {
documentData: {
select: {
data: true,
},
},
},
});
return Promise.all([
await prisma.user.delete({
where: {
id: user.id,
},
}),
await prisma.documentData.deleteMany({
where: {
data: document[0]?.documentData.data,
},
}),
]);
};

View File

@@ -17,7 +17,7 @@ import { alphaid } from '../id';
export const getPresignPostUrl = async (fileName: string, contentType: string) => { export const getPresignPostUrl = async (fileName: string, contentType: string) => {
const client = getS3Client(); const client = getS3Client();
const user = await getServerComponentSession(); const { user } = await getServerComponentSession();
// Get the basename and extension for the file // Get the basename and extension for the file
const { name, ext } = path.parse(fileName); const { name, ext } = path.parse(fileName);

View File

@@ -3,7 +3,7 @@ import { CreateNextContextOptions } from '@trpc/server/adapters/next';
import { getServerSession } from '@documenso/lib/next-auth/get-server-session'; import { getServerSession } from '@documenso/lib/next-auth/get-server-session';
export const createTrpcContext = async ({ req, res }: CreateNextContextOptions) => { export const createTrpcContext = async ({ req, res }: CreateNextContextOptions) => {
const session = await getServerSession({ req, res }); const { session, user } = await getServerSession({ req, res });
if (!session) { if (!session) {
return { return {
@@ -12,9 +12,16 @@ export const createTrpcContext = async ({ req, res }: CreateNextContextOptions)
}; };
} }
if (!user) {
return {
session: null,
user: null,
};
}
return { return {
session, session,
user: session, user,
}; };
}; };

View File

@@ -27,7 +27,9 @@ declare namespace NodeJS {
NEXT_PRIVATE_SIGNING_LOCAL_FILE_CONTENTS?: string; NEXT_PRIVATE_SIGNING_LOCAL_FILE_CONTENTS?: string;
NEXT_PRIVATE_SIGNING_LOCAL_FILE_ENCODING?: 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_API_KEY?: string;
NEXT_PRIVATE_MAILCHANNELS_DKIM_DOMAIN?: string; NEXT_PRIVATE_MAILCHANNELS_DKIM_DOMAIN?: string;

View File

@@ -11,9 +11,19 @@ const Dialog = DialogPrimitive.Root;
const DialogTrigger = DialogPrimitive.Trigger; const DialogTrigger = DialogPrimitive.Trigger;
const DialogPortal = ({ className, children, ...props }: DialogPrimitive.DialogPortalProps) => ( const DialogPortal = ({
className,
children,
position = 'start',
...props
}: DialogPrimitive.DialogPortalProps & { position?: 'start' | 'end' }) => (
<DialogPrimitive.Portal className={cn(className)} {...props}> <DialogPrimitive.Portal className={cn(className)} {...props}>
<div className="fixed inset-0 z-50 flex items-start justify-center sm:items-center"> <div
className={cn('fixed inset-0 z-50 flex justify-center sm:items-center', {
'items-start': position === 'start',
'items-end': position === 'end',
})}
>
{children} {children}
</div> </div>
</DialogPrimitive.Portal> </DialogPrimitive.Portal>
@@ -39,14 +49,14 @@ DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
const DialogContent = React.forwardRef< const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>, React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> & { position?: 'start' | 'end' }
>(({ className, children, ...props }, ref) => ( >(({ className, children, position = 'start', ...props }, ref) => (
<DialogPortal> <DialogPortal position={position}>
<DialogOverlay /> <DialogOverlay />
<DialogPrimitive.Content <DialogPrimitive.Content
ref={ref} ref={ref}
className={cn( className={cn(
'bg-background animate-in data-[state=open]:fade-in-90 data-[state=open]:slide-in-from-bottom-10 sm:zoom-in-90 data-[state=open]:sm:slide-in-from-bottom-0 fixed z-50 grid w-full gap-4 rounded-b-lg border p-6 shadow-lg sm:max-w-lg sm:rounded-lg', 'bg-background animate-in data-[state=open]:fade-in-90 sm:zoom-in-90 data-[state=open]:slide-in-from-bottom-10 data-[state=open]:sm:slide-in-from-bottom-0 fixed z-50 grid w-full gap-4 rounded-b-lg border p-6 shadow-lg sm:max-w-lg sm:rounded-lg',
className, className,
)} )}
{...props} {...props}

View File

@@ -2,8 +2,13 @@
"$schema": "https://turbo.build/schema.json", "$schema": "https://turbo.build/schema.json",
"pipeline": { "pipeline": {
"build": { "build": {
"dependsOn": ["^build"], "dependsOn": [
"outputs": [".next/**", "!.next/cache/**"] "^build"
],
"outputs": [
".next/**",
"!.next/cache/**"
]
}, },
"lint": {}, "lint": {},
"clean": { "clean": {
@@ -12,26 +17,13 @@
"dev": { "dev": {
"cache": false, "cache": false,
"persistent": true "persistent": true
},
"test:e2e": {
"dependsOn": ["^build"]
} }
}, },
"globalDependencies": ["**/.env.*local"], "globalDependencies": [
"**/.env.*local"
],
"globalEnv": [ "globalEnv": [
"APP_VERSION", "APP_VERSION",
"E2E_TEST_USERNAME",
"E2E_TEST_USER_EMAIL",
"E2E_TEST_USER_PASSWORD",
"E2E_TEST_SIGNER_NAME",
"E2E_TEST_SIGNER_EMAIL",
"E2E_TEST_SIGNING_SUBJECT",
"E2E_TEST_SIGNING_MESSAGE",
"E2E_TEST_AUTHENTICATE_USERNAME",
"E2E_TEST_AUTHENTICATE_USER_EMAIL",
"E2E_TEST_AUTHENTICATE_USER_PASSWORD",
"NEXTAUTH_URL", "NEXTAUTH_URL",
"NEXTAUTH_SECRET", "NEXTAUTH_SECRET",
"NEXT_PUBLIC_PROJECT", "NEXT_PUBLIC_PROJECT",
@@ -58,6 +50,7 @@
"NEXT_PRIVATE_SIGNING_LOCAL_FILE_CONTENTS", "NEXT_PRIVATE_SIGNING_LOCAL_FILE_CONTENTS",
"NEXT_PRIVATE_SIGNING_LOCAL_FILE_ENCODING", "NEXT_PRIVATE_SIGNING_LOCAL_FILE_ENCODING",
"NEXT_PRIVATE_SMTP_TRANSPORT", "NEXT_PRIVATE_SMTP_TRANSPORT",
"NEXT_PRIVATE_RESEND_API_KEY",
"NEXT_PRIVATE_MAILCHANNELS_API_KEY", "NEXT_PRIVATE_MAILCHANNELS_API_KEY",
"NEXT_PRIVATE_MAILCHANNELS_ENDPOINT", "NEXT_PRIVATE_MAILCHANNELS_ENDPOINT",
"NEXT_PRIVATE_MAILCHANNELS_DKIM_DOMAIN", "NEXT_PRIVATE_MAILCHANNELS_DKIM_DOMAIN",