Compare commits

..

1 Commits

Author SHA1 Message Date
a01385b15f exp: millionjs on marketing page 2024-01-20 14:22:24 +00:00
149 changed files with 961 additions and 2362 deletions

View File

@ -4,10 +4,8 @@ NEXTAUTH_SECRET="secret"
# [[CRYPTO]]
# Application Key for symmetric encryption and decryption
# REQUIRED: This should be a random string of at least 32 characters
# This should be a random string of at least 32 characters
NEXT_PRIVATE_ENCRYPTION_KEY="CAFEBABE"
# REQUIRED: This should be a random string of at least 32 characters
NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY="DEADBEEF"
# [[AUTH OPTIONAL]]
NEXT_PRIVATE_GOOGLE_CLIENT_ID=""
@ -25,7 +23,7 @@ NEXT_PRIVATE_DIRECT_DATABASE_URL="postgres://documenso:password@127.0.0.1:54320/
# [[E2E Tests]]
E2E_TEST_AUTHENTICATE_USERNAME="Test User"
E2E_TEST_AUTHENTICATE_USER_EMAIL="testuser@mail.com"
E2E_TEST_AUTHENTICATE_USER_PASSWORD="test_Password123"
E2E_TEST_AUTHENTICATE_USER_PASSWORD="test_password"
# [[STORAGE]]
# OPTIONAL: Defines the storage transport to use. Available options: database (default) | s3
@ -74,8 +72,6 @@ NEXT_PRIVATE_MAILCHANNELS_DKIM_DOMAIN=
NEXT_PRIVATE_MAILCHANNELS_DKIM_SELECTOR=
# OPTIONAL: The private key to use for DKIM signing.
NEXT_PRIVATE_MAILCHANNELS_DKIM_PRIVATE_KEY=
# OPTIONAL: Displays the maximum document upload limit to the user in MBs
NEXT_PUBLIC_DOCUMENT_SIZE_UPLOAD_LIMIT=5
# [[STRIPE]]
NEXT_PRIVATE_STRIPE_API_KEY=

View File

@ -33,4 +33,3 @@ body:
- label: I have explained the use case or scenario for this feature.
- label: I have included any relevant technical details or design suggestions.
- label: I understand that this is a suggestion and that there is no guarantee of implementation.
- label: I want to work on creating a PR for this issue if approved

View File

@ -1,87 +0,0 @@
---
title: Commodifying Signing
description: We are creating signing as a public good and are commoditizing it to make it cheaper and better.
authorName: 'Timur Ercan'
authorImage: '/blog/blog-author-timur.jpeg'
authorRole: 'Co-Founder'
date: 2024-01-25
Tags:
- Vision
- Mission
- Open Source
---
<figure>
<MdxNextImage
src="/blog/lighthouse.jpeg"
width="650"
height="650"
alt="A lighthouse on a tiny island."
/>
<figcaption className="text-center">
Lighthouses are often used as an example of a public good; As they benefit all maritime users, but no one can be excluded from using them as a navigational aid. Use by one person neither prevents access by other people, nor does it reduce availability to others.
</figcaption>
</figure>
# Commodifying Signing
> TLDR; We are creating signing as a public good and are commoditizing it to make it cheaper and better.
While we are in full-on building mode with Documenso, I think a lot about the big picture of what we are attempting to do. One phrase that keeps popping up is, "We are commodifying signing." Let's dig deeper into what that means.
Let's start with why we are doing this. Documenso's mission is to solve the domain of signing once and for all for everyone. In so many calls, I hear stories about how organizations build their own solution because the existing ones are too expensive or need to be more flexible. That means not hundreds but probably thousands of companies worldwide have done the same. This is simply wasting humanity's time. Since digital signing systems are understood well enough that seemingly "everyone" can build them, given enough pain, It's time to do it once correctly.
## Is signing already a commodity?
> In economics, a **commodity** is an economic good, usually a resource, that has explicitly full or substantial fungibility: that is, the market treats instances of the good as equivalent or nearly so with no regard to who produced them.
That sounds like the signing market today. There is no shortage of signing providers, and you can get similar signing services from many places. So why is this different from what we want, and why does this not satisfy the market?
- Signing is expensive and painful when you are locked into your vendor, and they charge by signing volume.
- Signing is also expensive and painful when you have to build it yourself since no vendor fits your requirements or you are not allowed to
To understand why, we need to look at the landscape as it is today:
- **Commodity**: Signing SaaS
- **Private Goods**: Signing Code Base, Regulatory Know-How
- **Public Goods**: Web Tech, Digital Signature Algorithms and Standards
What the current players have done is to commodify the listed public goods into commercial products:
> […]the action and process of transforming goods, services, ideas, nature, personal information, people, or animals into commodities.
> (Let's ignore the end of that list for now and what it says about humanity, yikes)
While this paradigm brought digital signing to many businesses worldwide, we aim for a different future. To solve signing once and for all, we need to achieve two core points:
- Making it cheaper so it's profitable for everyone to use
- Making it more accessible so everyone can use it (e.g. regulated industries) and flexible enough (extendable, open).
To achieve this, we must transform the landscape to look like this:
- **Commodities**: Enterprise Components, Support, Hosting, Self-Host Licenses
- **Public Goods**: (no longer private): OS (Open Source) Signing Code Base, OS Regulatory Know-How
- **Public Goods**: OS Web Tech, Digital Signature Algorithms and Standards
## Raising the Bar
Before creating a commodity, we are raising the bar of what the underlying public good is. Having an open source singing framework you can extend, self-host, and understand makes the resulting solution much more accessible and extendable for everyone. Now for the final feat of making signing cheaper:
As we have seen, signing has already been commodified. But since it was done by a closed source and, frankly, a very opaque industry, no downward price spiral has ensued. By building Documenso open source with an open culture, we can pierce the veil and trigger what the space has been missing for a long time: Commoditization. If you had to read that again, so did I:
> In business literature, **commoditization** is defined as the process by which goods that have economic value and are distinguishable in terms of attributes (uniqueness or brand) become simple commodities in the eyes of the market or consumers.
By only selling what creates value for the customer (hosting a highly available service, keeping it compliant, supporting with technical issues and challenges, preparing industry-specific components), we are commoditizing signing since everyone can do it now: The resources enabling it are public goods, aka. open source. A leveled playing field, as described above, is the perfect environment for a community-first, technology-first, and value-first company like Documenso to flourish.
## Changing the Game
In this new world, a company needing signing (literally every company) can decide if the ROI (Return on Investment) of building signing themselves is greater than simply paying for the value-added activities they will need anyway. Pricing our offering not on volume but fixed is a nice additional wedge into the market we intend to use here.
The market dynamic now changes to who can offer the greatest value added to the public goods, driving the price down as this can be done much more efficiently than locking customers into closed source SaaS. Documenso, being a lean company, which we intend to stay with for a long time, will help kickstart this effect. Open Source capital efficiency is real. Our planned enterprise components, hosting support, and partner ecosystem will all leverage this effect.
We will grow our community around the public good, the open-source repo, and create an ecosystem around the commodities built on top of it (components, hosting, compliance, support). We will solve signing once and for all, and the world will be better for it. Onwards.
As always, feel free to connect on [Twitter / X](https://twitter.com/eltimuro) (DM open) or [Discord](https://documen.so/discord) if you have any questions or comments.
Best from Hamburg\
Timur

View File

@ -1,115 +0,0 @@
---
title: Moving from Linear to GitHub & LIVE Roadmap 2.0
description: We are leaving linear and are going all in on GitHub. Here is how we do it.
authorName: 'Timur Ercan'
authorImage: '/blog/blog-author-timur.jpeg'
authorRole: 'Co-Founder'
date: 2024-01-10
Tags:
- GitHub
- Backlog
- Roadmap
---
# From Linear to GitHub
> TLDR; We are leaving Linear and are using only GitHub going forward. We no longer communicate feature timelines, only what we are working on and what's next.
If you follow us, you know we have been in full-on build mode. We are building, the community is building, it's great. Building is our daily business, so we think a lot about improving our approach to doing it.
Our most recent approach is to reduce the number of tools and platforms we use. Every tool we use
- Reduces the average time you spend on the tool
- Reduces your focus
- Increases mental load to keep all points of interest in mind
We thought about where we spend the most time, and hardly surprising: it's GitHub. Not only do we spend a lot of time there, but we also WANT to spend a lot of time there because:
- It's where the community contributes, and we are all about community
- It's where we show the world what we are working on
# The old structure
So far, we have been using Linear for our Backlog/ Task Management and synced issues we want to showcase or work on with the community via synclinear.com. Not only did we have our development issues there, but since
we have our own resident founding designer, we created a proper design backlog to structure our design workflows.
# The new structure
We moved everything to GitHub once we realized our focus was already there. This has a few key benefits:
- Reducing dilution of attention and time: You can hang out on GitHub without risk of missing much
- Putting different aspects of Documenso close to each other: Development, Design, Community
- Keep long-term, niche, and very abstract issues out of the main repo so we don't get desensitized by large issue numbers
To achieve this, we created a few GitHub repositories to host issues, with the main repository remaining the central point of interest, especially for the community.
## 1. Main Repository - Day to day Issues and the shorter-term roadmap (LIVE Roadmap 2.0)
> [github.com/documenso/documenso](https://github.com/documenso/documenso)
Apart from the source code of the Documenso app and website, the main repo houses issues raised by the community and issues where we invite the community to participate.
With the overhauling of our issue management, we are also updating our progress communication. While the software and product development process is highly complex,
we try to give as much insight into what we do as possible. To that end, we went through 3 phases, three being what we do now.
1. **One extensive roadmap**: Initially we had one roadmap and were (very) slowly checking off boxes there (via a "Roadmap" milestone). While this is easy, it's also pretty imprecise and not practical as the project grows
2. **Estimated releases per quarter**: To give better guidance, we tried communicating our goals for the quarter; a pretty big window we thought we could roughly "hit". While the idea of not being too detailed was good, it is tough to estimate when some significant things are done if you do a lot of minor/ other things in parallel,
like working with the community and tuning things you go. Hitting time targets is tricky because there may be better things to do than sticking to that time target. This is always much easier to grasp for the people closely involved. The fallacy is to assume the thing you plan for exists in a vacuum.
3. Since we do not want to limit ourselves in choosing the most effective course but still give some insight into what's going on and what's coming up, we updated the live roadmap [https://documen.so/live](https://documen.so/live). It now shows what we are currently working on and what we plan on doing next. We do not provide
a specific timeline anymore since we couldn't even if we wanted to. Of course, we set our short-term goals based on what's best for the community. We give updates on the issues being worked on as well as possible.
## 2. Public Backlog - The longer-term roadmap
> [github.com/documenso/backlog](https://github.com/documenso/backlog)
The public backlog houses everything we want to build eventually. We do not provide a specific timeline of when that might happen. If we decide against something, it will be removed from the public backlog, as we consider this our long-term vision for Documenso. If you are interested in something on the roadmap, comment on the issue or post on Discord. This helps us gauge interest in specific features.
**Issues in the public backlog are not** available to be worked on. For issues to work on, please check the main repository issues. The issues found here are scoped broader since they are not meant for immediate execution but rather give a sense of where Documenso is going and what we consider part of our domain.
## 3. Internal Backlog
> github.com/documenso/backlog-internal
<figure>
<MdxNextImage
src="/blog/gh1.png"
width="1260"
height="630"
alt="GitHub: Development Board"
/>
<figcaption className="text-center">
Our internal Kanban for development
</figcaption>
</figure>
This serves as the direct replacement for our Linear backlog. Here, we manage issues that are either too small or short-term for inclusion in the long-term roadmap, yet too specialized or fundamental to be integrated into the main repository. Our development Kanban board is implemented using a GitHub project.
## 4. Internal Design Backlog
> github.com/documenso/design-internal
<figure>
<MdxNextImage
src="/blog/gh2.png"
width="1260"
height="630"
alt="GitHub: Design Board"
/>
<figcaption className="text-center">
Our internal Kanban for design
</figcaption>
</figure>
This is the design equivalent of the internal backlog. The internal design backlog houses our design projects that include the exploration of new features, detailed UI designs, and improving the platform overall.
It's similar to the Kanban board for the development backlog.
## 5. Public Design Repository
> [github.com/documenso/backlog-design](https://github.com/documenso/design)
While the internal design backlog also existed in Linear, the public design repository is new. Since designing in the open is tricky, we opted to publish the detailed design artifacts with the corresponding feature instead.
We already have design.documenso.com housing our general design system. Here, we will publish the specifics of how we applied this to each feature. We will publish the first artifacts here soon, what may be in the cards can be found on the [LIVE Roadmap](https://documen.so/live).
Feel free to connect with us on [Twitter / X](https://twitter.com/eltimuro) (DM open) or [Discord](https://documen.so/discord) if you have any questions or comments! We're always here to help and would love to hear from you :)
Best from Hamburg\
Timur

View File

@ -7,8 +7,6 @@ authorRole: 'Co-Founder'
date: 2023-07-13
tags:
- Manifesto
- Open Source
- Vision
---
<figure>

View File

@ -1,3 +1,4 @@
const million = require('million/compiler');
/* eslint-disable @typescript-eslint/no-var-requires */
const fs = require('fs');
const path = require('path');
@ -5,11 +6,11 @@ const { withContentlayer } = require('next-contentlayer');
const ENV_FILES = ['.env', '.env.local', `.env.${process.env.NODE_ENV || 'development'}`];
ENV_FILES.forEach((file) => {
for (file of ENV_FILES) {
require('dotenv').config({
path: path.join(__dirname, `../../${file}`),
});
});
}
// !: This is a temp hack to get caveat working without placing it back in the public directory.
// !: By inlining this at build time we should be able to sign faster.
@ -94,4 +95,6 @@ const config = {
},
};
module.exports = withContentlayer(config);
module.exports = million.next(
withContentlayer(config), { auto: { rsc: true } }
);

View File

@ -24,6 +24,7 @@
"lucide-react": "^0.279.0",
"luxon": "^3.4.0",
"micro": "^10.0.1",
"million": "^2.6.4",
"next": "14.0.3",
"next-auth": "4.24.5",
"next-contentlayer": "^0.3.4",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 175 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 331 KiB

View File

@ -15,12 +15,12 @@ export const generateMetadata = ({ params }: { params: { content: string } }) =>
notFound();
}
return { title: document.title };
return { title: `Documenso - ${document.title}` };
};
const mdxComponents: MDXComponents = {
MdxNextImage: (props: { width: number; height: number; alt?: string; src: string }) => (
<Image {...props} alt={props.alt ?? ''} />
<Image width={props.width} height={props.height} src={props.src} alt={props.alt ?? ''} />
),
};

View File

@ -18,16 +18,14 @@ export const generateMetadata = ({ params }: { params: { post: string } }) => {
}
return {
title: {
absolute: `${blogPost.title} - Documenso Blog`,
},
title: `Documenso - ${blogPost.title}`,
description: blogPost.description,
};
};
const mdxComponents: MDXComponents = {
MdxNextImage: (props: { width: number; height: number; alt?: string; src: string }) => (
<Image {...props} alt={props.alt ?? ''} />
<Image width={props.width} height={props.height} src={props.src} alt={props.alt ?? ''} />
),
};

View File

@ -1,10 +1,5 @@
import type { Metadata } from 'next';
import { allBlogPosts } from 'contentlayer/generated';
export const metadata: Metadata = {
title: 'Blog',
};
export default function BlogPage() {
const blogPosts = allBlogPosts.sort((a, b) => {
const dateA = new Date(a.date);

View File

@ -1,6 +1,6 @@
'use client';
import { HTMLAttributes } from 'react';
import type { HTMLAttributes } from 'react';
import { Bar, BarChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts';
@ -24,7 +24,6 @@ export const BarMetric = <T extends Record<string, Record<keyof T[string], unkno
label,
chartHeight = 400,
extraInfo,
...props
}: BarMetricProps<T>) => {
const formattedData = Object.keys(data)
.map((key) => ({
@ -34,7 +33,7 @@ export const BarMetric = <T extends Record<string, Record<keyof T[string], unkno
.reverse();
return (
<div className={cn('flex flex-col', className)} {...props}>
<div className={cn('flex flex-col', className)}>
<div className="flex items-center px-4">
<h3 className="text-lg font-semibold">{title}</h3>
<span>{extraInfo}</span>

View File

@ -1,6 +1,7 @@
'use client';
import { HTMLAttributes, useEffect, useState } from 'react';
import type { HTMLAttributes } from 'react';
import { useEffect, useState } from 'react';
import { Cell, Legend, Pie, PieChart, Tooltip } from 'recharts';
@ -41,14 +42,14 @@ const renderCustomizedLabel = ({
export type CapTableProps = HTMLAttributes<HTMLDivElement>;
export const CapTable = ({ className, ...props }: CapTableProps) => {
export const CapTable = ({ className }: CapTableProps) => {
const [isSSR, setIsSSR] = useState(true);
useEffect(() => {
setIsSSR(false);
}, []);
return (
<div className={cn('flex flex-col', className)} {...props}>
<div className={cn('flex flex-col', className)}>
<h3 className="px-4 text-lg font-semibold">Cap Table</h3>
<div className="border-border mt-2.5 flex flex-1 items-center justify-center rounded-2xl border shadow-sm hover:shadow">
@ -76,7 +77,7 @@ export const CapTable = ({ className, ...props }: CapTableProps) => {
/>
<Tooltip
formatter={(percent: number, name, props) => {
return [`${percent}%`, name || props['name'] || props['payload']['name']];
return [`${percent}%`, name || props.name || props.payload.name];
}}
/>
</PieChart>

View File

@ -47,14 +47,6 @@ export const TEAM_MEMBERS = [
engagement: 'Full-Time',
joinDate: 'October 9th, 2023',
},
{
name: 'Adithya Krishna',
role: 'Software Engineer - II',
salary: '-',
location: 'India',
engagement: 'Full-Time',
joinDate: 'December 1st, 2023',
},
];
export const FUNDING_RAISED = [

View File

@ -1,6 +1,6 @@
'use client';
import { HTMLAttributes } from 'react';
import type { HTMLAttributes } from 'react';
import { Bar, BarChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts';
@ -11,14 +11,14 @@ export type FundingRaisedProps = HTMLAttributes<HTMLDivElement> & {
data: Record<string, string | number>[];
};
export const FundingRaised = ({ className, data, ...props }: FundingRaisedProps) => {
export const FundingRaised = ({ className, data }: FundingRaisedProps) => {
const formattedData = data.map((item) => ({
amount: Number(item.amount),
date: formatMonth(item.date as string),
}));
return (
<div className={cn('flex flex-col', className)} {...props}>
<div className={cn('flex flex-col', className)}>
<h3 className="px-4 text-lg font-semibold">Total Funding Raised</h3>
<div className="border-border mt-2.5 flex flex-1 flex-col items-center justify-center rounded-2xl border p-6 pl-2 pt-12 shadow-sm hover:shadow">

View File

@ -1,4 +1,4 @@
import { HTMLAttributes } from 'react';
import type { HTMLAttributes } from 'react';
import { cn } from '@documenso/ui/lib/utils';
@ -7,9 +7,9 @@ export type MetricCardProps = HTMLAttributes<HTMLDivElement> & {
value: string;
};
export const MetricCard = ({ className, title, value, ...props }: MetricCardProps) => {
export const MetricCard = ({ className, title, value }: MetricCardProps) => {
return (
<div className={cn('rounded-md border p-4 shadow-sm hover:shadow', className)} {...props}>
<div className={cn('rounded-md border p-4 shadow-sm hover:shadow', className)}>
<h4 className="text-muted-foreground text-sm font-medium">{title}</h4>
<p className="mb-2 mt-6 text-4xl font-bold">{value}</p>

View File

@ -1,5 +1,3 @@
import type { Metadata } from 'next';
import { z } from 'zod';
import { getUserMonthlyGrowth } from '@documenso/lib/server-only/user/get-user-monthly-growth';
@ -16,10 +14,6 @@ import { MonthlyTotalUsersChart } from './monthly-total-users-chart';
import { TeamMembers } from './team-members';
import { OpenPageTooltip } from './tooltip';
export const metadata: Metadata = {
title: 'Open Startup',
};
export const revalidate = 3600;
export const dynamic = 'force-dynamic';
@ -147,7 +141,12 @@ export default async function OpenPage() {
<p className="text-muted-foreground mt-4 max-w-[60ch] text-center text-lg leading-normal">
All our metrics, finances, and learnings are public. We believe in transparency and want
to share our journey with you. You can read more about why here:{' '}
<a className="font-bold" href="https://documenso.com/blog/pre-seed" target="_blank">
<a
className="font-bold"
href="https://documenso.com/blog/pre-seed"
target="_blank"
rel="noreferrer"
>
Announcing Open Metrics
</a>
</p>

View File

@ -1,4 +1,4 @@
import { HTMLAttributes } from 'react';
import type { HTMLAttributes } from 'react';
import { cn } from '@documenso/ui/lib/utils';
import {
@ -14,9 +14,9 @@ import { SALARY_BANDS } from '~/app/(marketing)/open/data';
export type SalaryBandsProps = HTMLAttributes<HTMLDivElement>;
export const SalaryBands = ({ className, ...props }: SalaryBandsProps) => {
export const SalaryBands = ({ className }: SalaryBandsProps) => {
return (
<div className={cn('flex flex-col', className)} {...props}>
<div className={cn('flex flex-col', className)}>
<h3 className="px-4 text-lg font-semibold">Global Salary Bands</h3>
<div className="border-border mt-2.5 flex-1 rounded-2xl border shadow-sm hover:shadow">
@ -30,7 +30,7 @@ export const SalaryBands = ({ className, ...props }: SalaryBandsProps) => {
</TableHeader>
<TableBody>
{SALARY_BANDS.map((band, index) => (
<TableRow key={index}>
<TableRow key={band.title + index.toString()}>
<TableCell className="font-medium">{band.title}</TableCell>
<TableCell>{band.seniority}</TableCell>
<TableCell className="text-right">

View File

@ -1,4 +1,4 @@
import { HTMLAttributes } from 'react';
import type { HTMLAttributes } from 'react';
import { cn } from '@documenso/ui/lib/utils';
import {
@ -14,9 +14,9 @@ import { TEAM_MEMBERS } from './data';
export type TeamMembersProps = HTMLAttributes<HTMLDivElement>;
export const TeamMembers = ({ className, ...props }: TeamMembersProps) => {
export const TeamMembers = ({ className }: TeamMembersProps) => {
return (
<div className={cn('flex flex-col', className)} {...props}>
<div className={cn('flex flex-col', className)}>
<h2 className="px-4 text-2xl font-semibold">Team</h2>
<div className="border-border mt-2.5 flex-1 rounded-2xl border shadow-sm hover:shadow">

View File

@ -1,4 +1,3 @@
import type { Metadata } from 'next';
import Image from 'next/image';
import { z } from 'zod';
@ -6,12 +5,7 @@ import { z } from 'zod';
import backgroundPattern from '@documenso/assets/images/background-pattern.png';
import { OSSFriendsContainer } from './container';
import type { TOSSFriendsSchema } from './schema';
import { ZOSSFriendsSchema } from './schema';
export const metadata: Metadata = {
title: 'OSS Friends',
};
import { TOSSFriendsSchema, ZOSSFriendsSchema } from './schema';
export default async function OSSFriendsPage() {
const ossFriends: TOSSFriendsSchema = await fetch('https://formbricks.com/api/oss-friends', {

View File

@ -1,5 +1,4 @@
/* eslint-disable no-unused-vars, @typescript-eslint/no-unused-vars */
import type { Metadata } from 'next';
import { Caveat } from 'next/font/google';
import { cn } from '@documenso/ui/lib/utils';
@ -11,11 +10,6 @@ import { OpenBuildTemplateBento } from '~/components/(marketing)/open-build-temp
import { ShareConnectPaidWidgetBento } from '~/components/(marketing)/share-connect-paid-widget-bento';
export const revalidate = 600;
export const metadata: Metadata = {
title: {
absolute: 'Documenso - The Open Source DocuSign Alternative',
},
};
const fontCaveat = Caveat({
weight: ['500'],

View File

@ -1,4 +1,5 @@
import type { Metadata } from 'next';
'use client';
import Link from 'next/link';
import {
@ -11,10 +12,6 @@ import { Button } from '@documenso/ui/primitives/button';
import { PricingTable } from '~/components/(marketing)/pricing-table';
export const metadata: Metadata = {
title: 'Pricing',
};
export type PricingPageProps = {
searchParams?: {
planId?: string;

View File

@ -158,7 +158,6 @@ export const SinglePlayerClient = () => {
readStatus: 'OPENED',
signingStatus: 'NOT_SIGNED',
sendStatus: 'NOT_SENT',
role: 'SIGNER',
};
const onFileDrop = async (file: File) => {

View File

@ -1,11 +1,5 @@
import type { Metadata } from 'next';
import { SinglePlayerClient } from './client';
export const metadata: Metadata = {
title: 'Singleplayer',
};
export const revalidate = 0;
// !: This entire file is a hack to get around failed prerendering of

View File

@ -18,10 +18,7 @@ const fontInter = Inter({ subsets: ['latin'], variable: '--font-sans' });
const fontCaveat = Caveat({ subsets: ['latin'], variable: '--font-signature' });
export const metadata = {
title: {
template: '%s - Documenso',
default: 'Documenso',
},
title: 'Documenso - The Open Source DocuSign Alternative',
description:
'Join Documenso, the open signing infrastructure, and get a 10x better signing experience. Pricing starts at $30/mo. forever! Sign in now and enjoy a faster, smarter, and more beautiful document signing process. Integrates with your favorite tools, customizable, and expandable. Support our mission and become a part of our open-source community.',
keywords:

View File

@ -34,13 +34,7 @@ export const ConfettiScreen = ({
}
return createPortal(
<Confetti
{...props}
className="w-full"
numberOfPieces={numberOfPieces}
width={width}
height={height}
/>,
<Confetti className="w-full" numberOfPieces={numberOfPieces} width={width} height={height} />,
document.body,
);
};

View File

@ -1,4 +1,4 @@
import { HTMLAttributes } from 'react';
import type { HTMLAttributes } from 'react';
import Image from 'next/image';
@ -11,12 +11,9 @@ import { Card, CardContent } from '@documenso/ui/primitives/card';
export type FasterSmarterBeautifulBentoProps = HTMLAttributes<HTMLDivElement>;
export const FasterSmarterBeautifulBento = ({
className,
...props
}: FasterSmarterBeautifulBentoProps) => {
export const FasterSmarterBeautifulBento = ({ className }: FasterSmarterBeautifulBentoProps) => {
return (
<div className={cn('relative', className)} {...props}>
<div className={cn('relative', className)}>
<div className="absolute inset-0 -z-10 flex items-center justify-center">
<Image
src={backgroundPattern}

View File

@ -35,9 +35,9 @@ const FOOTER_LINKS = [
{ href: '/privacy', text: 'Privacy' },
];
export const Footer = ({ className, ...props }: FooterProps) => {
export const Footer = ({ className }: FooterProps) => {
return (
<div className={cn('border-t py-12', className)} {...props}>
<div className={cn('border-t py-12', className)}>
<div className="mx-auto flex w-full max-w-screen-xl flex-wrap items-start justify-between gap-8 px-8">
<div className="flex-shrink-0">
<Link href="/">
@ -53,7 +53,7 @@ export const Footer = ({ className, ...props }: FooterProps) => {
<div className="mt-4 flex flex-wrap items-center gap-x-4 gap-y-4">
{SOCIAL_LINKS.map((link, index) => (
<Link
key={index}
key={link.href + index.toString()}
href={link.href}
target="_blank"
className="text-muted-foreground hover:text-muted-foreground/80"
@ -67,7 +67,7 @@ export const Footer = ({ className, ...props }: FooterProps) => {
<div className="grid w-full max-w-sm grid-cols-2 gap-x-4 gap-y-2 md:w-auto md:gap-x-8">
{FOOTER_LINKS.map((link, index) => (
<Link
key={index}
key={link.href + index.toString()}
href={link.href}
target={link.target}
className="text-muted-foreground hover:text-muted-foreground/80 flex-shrink-0 break-words text-sm"

View File

@ -15,7 +15,7 @@ import { MobileNavigation } from './mobile-navigation';
export type HeaderProps = HTMLAttributes<HTMLElement>;
export const Header = ({ className, ...props }: HeaderProps) => {
export const Header = ({ className }: HeaderProps) => {
const [isHamburgerMenuOpen, setIsHamburgerMenuOpen] = useState(false);
const { getFlag } = useFeatureFlags();
@ -23,7 +23,7 @@ export const Header = ({ className, ...props }: HeaderProps) => {
const isSinglePlayerModeMarketingEnabled = getFlag('marketing_header_single_player_mode');
return (
<header className={cn('flex items-center justify-between', className)} {...props}>
<header className={cn('flex items-center justify-between', className)}>
<div className="flex items-center space-x-4">
<Link href="/" className="z-10" onClick={() => setIsHamburgerMenuOpen(false)}>
<Image

View File

@ -3,7 +3,8 @@
import Image from 'next/image';
import Link from 'next/link';
import { Variants, motion } from 'framer-motion';
import type { Variants } from 'framer-motion';
import { motion } from 'framer-motion';
import { usePlausible } from 'next-plausible';
import { LuGithub } from 'react-icons/lu';
import { match } from 'ts-pattern';
@ -49,7 +50,7 @@ const HeroTitleVariants: Variants = {
},
};
export const Hero = ({ className, ...props }: HeroProps) => {
export const Hero = ({ className }: HeroProps) => {
const event = usePlausible();
const { getFlag } = useFeatureFlags();
@ -74,7 +75,7 @@ export const Hero = ({ className, ...props }: HeroProps) => {
};
return (
<motion.div className={cn('relative', className)} {...props}>
<motion.div className={cn('relative', className)}>
<div className="absolute -inset-24 -z-10">
<motion.div
className="flex h-full w-full origin-top-right items-center justify-center"

View File

@ -1,4 +1,4 @@
import { HTMLAttributes } from 'react';
import type { HTMLAttributes } from 'react';
import Image from 'next/image';
@ -11,9 +11,9 @@ import { Card, CardContent } from '@documenso/ui/primitives/card';
export type OpenBuildTemplateBentoProps = HTMLAttributes<HTMLDivElement>;
export const OpenBuildTemplateBento = ({ className, ...props }: OpenBuildTemplateBentoProps) => {
export const OpenBuildTemplateBento = ({ className }: OpenBuildTemplateBentoProps) => {
return (
<div className={cn('relative', className)} {...props}>
<div className={cn('relative', className)}>
<div className="absolute inset-0 -z-10 flex items-center justify-center">
<Image
src={backgroundPattern}

View File

@ -15,13 +15,13 @@ export type PricingTableProps = HTMLAttributes<HTMLDivElement>;
const SELECTED_PLAN_BAR_LAYOUT_ID = 'selected-plan-bar';
export const PricingTable = ({ className, ...props }: PricingTableProps) => {
export const PricingTable = ({ className }: PricingTableProps) => {
const event = usePlausible();
const [period, setPeriod] = useState<'MONTHLY' | 'YEARLY'>('MONTHLY');
return (
<div className={cn('', className)} {...props}>
<div className={cn('', className)}>
<div className="flex items-center justify-center gap-x-6">
<AnimatePresence>
<motion.button

View File

@ -1,4 +1,4 @@
import { HTMLAttributes } from 'react';
import type { HTMLAttributes } from 'react';
import Image from 'next/image';
@ -12,12 +12,9 @@ import { Card, CardContent } from '@documenso/ui/primitives/card';
export type ShareConnectPaidWidgetBentoProps = HTMLAttributes<HTMLDivElement>;
export const ShareConnectPaidWidgetBento = ({
className,
...props
}: ShareConnectPaidWidgetBentoProps) => {
export const ShareConnectPaidWidgetBento = ({ className }: ShareConnectPaidWidgetBentoProps) => {
return (
<div className={cn('relative', className)} {...props}>
<div className={cn('relative', className)}>
<div className="absolute inset-0 -z-10 flex items-center justify-center">
<Image
src={backgroundPattern}

View File

@ -55,7 +55,7 @@ type StepValues = (typeof STEP)[StepKeys];
export type WidgetProps = HTMLAttributes<HTMLDivElement>;
export const Widget = ({ className, children, ...props }: WidgetProps) => {
export const Widget = ({ className, children }: WidgetProps) => {
const { toast } = useToast();
const event = usePlausible();
@ -148,19 +148,19 @@ export const Widget = ({ className, children, ...props }: WidgetProps) => {
const claimPlanInput = signatureDataUrl
? {
name,
email,
planId,
signatureDataUrl: signatureDataUrl,
signatureText: null,
}
name,
email,
planId,
signatureDataUrl: signatureDataUrl,
signatureText: null,
}
: {
name,
email,
planId,
signatureDataUrl: null,
signatureText: signatureText ?? '',
};
name,
email,
planId,
signatureDataUrl: null,
signatureText: signatureText ?? '',
};
const [result] = await Promise.all([claimPlan(claimPlanInput), delay]);
@ -183,7 +183,6 @@ export const Widget = ({ className, children, ...props }: WidgetProps) => {
<Card
className={cn('mx-auto w-full max-w-4xl rounded-3xl before:rounded-3xl', className)}
gradient
{...props}
>
<div className="grid grid-cols-12 gap-y-8 overflow-hidden p-2 lg:gap-x-8">
<div className="text-muted-foreground col-span-12 flex flex-col gap-y-4 p-4 text-xs leading-relaxed lg:col-span-7">
@ -399,7 +398,6 @@ export const Widget = ({ className, children, ...props }: WidgetProps) => {
</DialogDescription>
<SignaturePad
disabled={isSubmitting}
className="aspect-video w-full rounded-md border"
defaultValue={signatureDataUrl || ''}
onChange={setDraftSignatureDataUrl}

View File

@ -1,4 +1,4 @@
import { SVGAttributes } from 'react';
import type { SVGAttributes } from 'react';
export type BackgroundProps = Omit<SVGAttributes<SVGElement>, 'viewBox'>;

View File

@ -3,7 +3,7 @@
import * as React from 'react';
import { ThemeProvider as NextThemesProvider } from 'next-themes';
import { ThemeProviderProps } from 'next-themes/dist/types';
import type { ThemeProviderProps } from 'next-themes/dist/types';
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;

View File

@ -45,7 +45,6 @@
"sharp": "0.33.1",
"ts-pattern": "^5.0.5",
"typescript": "5.2.2",
"ua-parser-js": "^1.0.37",
"uqr": "^0.1.2",
"zod": "^3.22.4"
},
@ -54,8 +53,7 @@
"@types/luxon": "^3.3.1",
"@types/node": "20.1.0",
"@types/react": "18.2.18",
"@types/react-dom": "18.2.7",
"@types/ua-parser-js": "^0.7.39"
"@types/react-dom": "18.2.7"
},
"overrides": {
"next-auth": {

View File

@ -218,9 +218,9 @@ export const EditDocumentForm = ({
<AddTitleFormPartial
key={recipients.length}
documentFlow={documentFlow.title}
document={document}
recipients={recipients}
fields={fields}
document={document}
onSubmit={onAddTitleFormSubmit}
/>

View File

@ -2,13 +2,13 @@
import Link from 'next/link';
import { CheckCircle, Download, Edit, EyeIcon, Pencil } from 'lucide-react';
import { Download, Edit, Pencil } from 'lucide-react';
import { useSession } from 'next-auth/react';
import { match } from 'ts-pattern';
import { downloadPDF } from '@documenso/lib/client-only/download-pdf';
import type { Document, Recipient, User } from '@documenso/prisma/client';
import { DocumentStatus, RecipientRole, SigningStatus } from '@documenso/prisma/client';
import { DocumentStatus, SigningStatus } from '@documenso/prisma/client';
import type { DocumentWithData } from '@documenso/prisma/types/document-with-data';
import { trpc as trpcClient } from '@documenso/trpc/client';
import { Button } from '@documenso/ui/primitives/button';
@ -37,7 +37,6 @@ export const DataTableActionButton = ({ row }: DataTableActionButtonProps) => {
const isPending = row.status === DocumentStatus.PENDING;
const isComplete = row.status === DocumentStatus.COMPLETED;
const isSigned = recipient?.signingStatus === SigningStatus.SIGNED;
const role = recipient?.role;
const onDownloadClick = async () => {
try {
@ -69,11 +68,6 @@ export const DataTableActionButton = ({ row }: DataTableActionButtonProps) => {
}
};
// TODO: Consider if want to keep this logic for hiding viewing for CC'ers
if (recipient?.role === RecipientRole.CC && isComplete === false) {
return null;
}
return match({
isOwner,
isRecipient,
@ -93,32 +87,15 @@ export const DataTableActionButton = ({ row }: DataTableActionButtonProps) => {
.with({ isRecipient: true, isPending: true, isSigned: false }, () => (
<Button className="w-32" asChild>
<Link href={`/sign/${recipient?.token}`}>
{match(role)
.with(RecipientRole.SIGNER, () => (
<>
<Pencil className="-ml-1 mr-2 h-4 w-4" />
Sign
</>
))
.with(RecipientRole.APPROVER, () => (
<>
<CheckCircle className="-ml-1 mr-2 h-4 w-4" />
Approve
</>
))
.otherwise(() => (
<>
<EyeIcon className="-ml-1 mr-2 h-4 w-4" />
View
</>
))}
<Pencil className="-ml-1 mr-2 h-4 w-4" />
Sign
</Link>
</Button>
))
.with({ isPending: true, isSigned: true }, () => (
<Button className="w-32" disabled={true}>
<EyeIcon className="-ml-1 mr-2 h-4 w-4" />
View
<Pencil className="-ml-1 mr-2 inline h-4 w-4" />
Sign
</Button>
))
.with({ isComplete: true }, () => (

View File

@ -5,11 +5,9 @@ import { useState } from 'react';
import Link from 'next/link';
import {
CheckCircle,
Copy,
Download,
Edit,
EyeIcon,
Loader,
MoreHorizontal,
Pencil,
@ -21,7 +19,7 @@ import { useSession } from 'next-auth/react';
import { downloadPDF } from '@documenso/lib/client-only/download-pdf';
import type { Document, Recipient, User } from '@documenso/prisma/client';
import { DocumentStatus, RecipientRole } from '@documenso/prisma/client';
import { DocumentStatus } from '@documenso/prisma/client';
import type { DocumentWithData } from '@documenso/prisma/types/document-with-data';
import { trpc as trpcClient } from '@documenso/trpc/client';
import { DocumentShareButton } from '@documenso/ui/components/document/document-share-button';
@ -107,32 +105,12 @@ export const DataTableActionDropdown = ({ row }: DataTableActionDropdownProps) =
<DropdownMenuContent className="w-52" align="start" forceMount>
<DropdownMenuLabel>Action</DropdownMenuLabel>
{recipient?.role !== RecipientRole.CC && (
<DropdownMenuItem disabled={!recipient || isComplete} asChild>
<Link href={`/sign/${recipient?.token}`}>
{recipient?.role === RecipientRole.VIEWER && (
<>
<EyeIcon className="mr-2 h-4 w-4" />
View
</>
)}
{recipient?.role === RecipientRole.SIGNER && (
<>
<Pencil className="mr-2 h-4 w-4" />
Sign
</>
)}
{recipient?.role === RecipientRole.APPROVER && (
<>
<CheckCircle className="mr-2 h-4 w-4" />
Approve
</>
)}
</Link>
</DropdownMenuItem>
)}
<DropdownMenuItem disabled={!recipient || isComplete} asChild>
<Link href={`/sign/${recipient?.token}`}>
<Pencil className="mr-2 h-4 w-4" />
Sign
</Link>
</DropdownMenuItem>
<DropdownMenuItem disabled={!isOwner || isComplete} asChild>
<Link href={`/documents/${row.id}`}>

View File

@ -1,8 +1,6 @@
import type { Metadata } from 'next';
import Link from 'next/link';
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
import type { PeriodSelectorValue } from '@documenso/lib/server-only/document/find-documents';
import { findDocuments } from '@documenso/lib/server-only/document/find-documents';
import { getStats } from '@documenso/lib/server-only/document/get-stats';
import { isExtendedDocumentStatus } from '@documenso/prisma/guards/is-extended-document-status';
@ -10,6 +8,7 @@ import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-documen
import { Tabs, TabsList, TabsTrigger } from '@documenso/ui/primitives/tabs';
import { PeriodSelector } from '~/components/(dashboard)/period-selector/period-selector';
import type { PeriodSelectorValue } from '~/components/(dashboard)/period-selector/types';
import { isPeriodSelectorValue } from '~/components/(dashboard)/period-selector/types';
import { DocumentStatus } from '~/components/formatter/document-status';
@ -26,22 +25,18 @@ export type DocumentsPageProps = {
};
};
export const metadata: Metadata = {
title: 'Documents',
};
export default async function DocumentsPage({ searchParams = {} }: DocumentsPageProps) {
const { user } = await getRequiredServerComponentSession();
const stats = await getStats({
user,
});
const status = isExtendedDocumentStatus(searchParams.status) ? searchParams.status : 'ALL';
const period = isPeriodSelectorValue(searchParams.period) ? searchParams.period : '';
const page = Number(searchParams.page) || 1;
const perPage = Number(searchParams.perPage) || 20;
const stats = await getStats({
user,
period,
});
const results = await findDocuments({
userId: user.id,
status,
@ -74,7 +69,7 @@ export default async function DocumentsPage({ searchParams = {} }: DocumentsPage
<h1 className="text-4xl font-semibold">Documents</h1>
<div className="-m-1 flex flex-wrap gap-x-4 gap-y-6 overflow-hidden p-1">
<Tabs value={status} className="overflow-x-auto">
<Tabs defaultValue={status} className="overflow-x-auto">
<TabsList>
{[
ExtendedDocumentStatus.INBOX,

View File

@ -10,7 +10,6 @@ import { useSession } from 'next-auth/react';
import { useLimits } from '@documenso/ee/server-only/limits/provider/client';
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT } from '@documenso/lib/constants/app';
import { createDocumentData } from '@documenso/lib/server-only/document-data/create-document-data';
import { putFile } from '@documenso/lib/universal/upload/put-file';
import { TRPCClientError } from '@documenso/trpc/client';
@ -97,15 +96,6 @@ export const UploadDocument = ({ className }: UploadDocumentProps) => {
}
};
const onFileDropRejected = () => {
toast({
title: 'Your document failed to upload.',
description: `File cannot be larger than ${APP_DOCUMENT_UPLOAD_SIZE_LIMIT}MB`,
duration: 5000,
variant: 'destructive',
});
};
return (
<div className={cn('relative', className)}>
<DocumentDropzone
@ -113,7 +103,6 @@ export const UploadDocument = ({ className }: UploadDocumentProps) => {
disabled={remaining.documents === 0 || !session?.user.emailVerified}
disabledMessage={disabledMessage}
onDrop={onFileDrop}
onDropRejected={onFileDropRejected}
/>
<div className="absolute -bottom-6 right-0">

View File

@ -1,4 +1,3 @@
import type { Metadata } from 'next';
import { redirect } from 'next/navigation';
import { match } from 'ts-pattern';
@ -18,10 +17,6 @@ import { LocaleDate } from '~/components/formatter/locale-date';
import { BillingPlans } from './billing-plans';
import { BillingPortalButton } from './billing-portal-button';
export const metadata: Metadata = {
title: 'Billing',
};
export default async function BillingSettingsPage() {
let { user } = await getRequiredServerComponentSession();

View File

@ -1,13 +1,7 @@
import type { Metadata } from 'next';
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
import { ProfileForm } from '~/components/forms/profile';
export const metadata: Metadata = {
title: 'Profile',
};
export default async function ProfileSettingsPage() {
const { user } = await getRequiredServerComponentSession();

View File

@ -1,23 +0,0 @@
import type { Metadata } from 'next';
import { UserSecurityActivityDataTable } from './user-security-activity-data-table';
export const metadata: Metadata = {
title: 'Security activity',
};
export default function SettingsSecurityActivityPage() {
return (
<div>
<h3 className="text-2xl font-semibold">Security activity</h3>
<p className="text-muted-foreground mt-2 text-sm">
View all recent security activity related to your account.
</p>
<hr className="my-4" />
<UserSecurityActivityDataTable />
</div>
);
}

View File

@ -1,156 +0,0 @@
'use client';
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
import type { DateTimeFormatOptions } from 'luxon';
import { DateTime } from 'luxon';
import { UAParser } from 'ua-parser-js';
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
import { USER_SECURITY_AUDIT_LOG_MAP } from '@documenso/lib/constants/auth';
import { ZBaseTableSearchParamsSchema } from '@documenso/lib/types/search-params';
import { trpc } from '@documenso/trpc/react';
import { DataTable } from '@documenso/ui/primitives/data-table';
import { DataTablePagination } from '@documenso/ui/primitives/data-table-pagination';
import { Skeleton } from '@documenso/ui/primitives/skeleton';
import { TableCell } from '@documenso/ui/primitives/table';
import { LocaleDate } from '~/components/formatter/locale-date';
const dateFormat: DateTimeFormatOptions = {
...DateTime.DATETIME_SHORT,
hourCycle: 'h12',
};
export const UserSecurityActivityDataTable = () => {
const parser = new UAParser();
const pathname = usePathname();
const router = useRouter();
const searchParams = useSearchParams();
const updateSearchParams = useUpdateSearchParams();
const parsedSearchParams = ZBaseTableSearchParamsSchema.parse(
Object.fromEntries(searchParams ?? []),
);
const { data, isLoading, isInitialLoading, isLoadingError } =
trpc.profile.findUserSecurityAuditLogs.useQuery(
{
page: parsedSearchParams.page,
perPage: parsedSearchParams.perPage,
},
{
keepPreviousData: true,
},
);
const onPaginationChange = (page: number, perPage: number) => {
updateSearchParams({
page,
perPage,
});
};
const results = data ?? {
data: [],
perPage: 10,
currentPage: 1,
totalPages: 1,
};
return (
<DataTable
columns={[
{
header: 'Date',
accessorKey: 'createdAt',
cell: ({ row }) => <LocaleDate format={dateFormat} date={row.original.createdAt} />,
},
{
header: 'Device',
cell: ({ row }) => {
if (!row.original.userAgent) {
return 'N/A';
}
parser.setUA(row.original.userAgent);
const result = parser.getResult();
let output = result.os.name;
if (!output) {
return 'N/A';
}
if (result.os.version) {
output += ` (${result.os.version})`;
}
return output;
},
},
{
header: 'Browser',
cell: ({ row }) => {
if (!row.original.userAgent) {
return 'N/A';
}
parser.setUA(row.original.userAgent);
const result = parser.getResult();
return result.browser.name ?? 'N/A';
},
},
{
header: 'IP Address',
accessorKey: 'ipAddress',
cell: ({ row }) => row.original.ipAddress ?? 'N/A',
},
{
header: 'Action',
accessorKey: 'type',
cell: ({ row }) => USER_SECURITY_AUDIT_LOG_MAP[row.original.type],
},
]}
data={results.data}
perPage={results.perPage}
currentPage={results.currentPage}
totalPages={results.totalPages}
onPaginationChange={onPaginationChange}
hasFilters={parsedSearchParams.page !== undefined || parsedSearchParams.perPage !== undefined}
onClearFilters={() => router.push(pathname ?? '/')}
error={{
enable: isLoadingError,
}}
skeleton={{
enable: isLoading && isInitialLoading,
rows: 3,
component: (
<>
<TableCell>
<Skeleton className="h-4 w-20 rounded-full" />
</TableCell>
<TableCell>
<Skeleton className="h-4 w-12 rounded-full" />
</TableCell>
<TableCell>
<Skeleton className="h-4 w-12 rounded-full" />
</TableCell>
<TableCell>
<Skeleton className="h-4 w-12 rounded-full" />
</TableCell>
<TableCell>
<Skeleton className="h-4 w-12 rounded-full" />
</TableCell>
</>
),
}}
>
{(table) => <DataTablePagination table={table} />}
</DataTable>
);
};

View File

@ -1,19 +1,9 @@
import type { Metadata } from 'next';
import Link from 'next/link';
import { IDENTITY_PROVIDER_NAME } from '@documenso/lib/constants/auth';
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
import { Button } from '@documenso/ui/primitives/button';
import { AuthenticatorApp } from '~/components/forms/2fa/authenticator-app';
import { RecoveryCodes } from '~/components/forms/2fa/recovery-codes';
import { PasswordForm } from '~/components/forms/password';
export const metadata: Metadata = {
title: 'Security',
};
export default async function SecuritySettingsPage() {
const { user } = await getRequiredServerComponentSession();
@ -27,76 +17,30 @@ export default async function SecuritySettingsPage() {
<hr className="my-4" />
{user.identityProvider === 'DOCUMENSO' ? (
<div>
<PasswordForm user={user} />
<PasswordForm user={user} className="max-w-xl" />
<hr className="border-border/50 mt-6" />
<hr className="mb-4 mt-8" />
<Alert
className="mt-6 flex flex-col justify-between p-6 sm:flex-row sm:items-center"
variant="neutral"
>
<div className="mb-4 sm:mb-0">
<AlertTitle>Two factor authentication</AlertTitle>
<h4 className="text-lg font-medium">Two Factor Authentication</h4>
<AlertDescription className="mr-4">
Create one-time passwords that serve as a secondary authentication method for
confirming your identity when requested during the sign-in process.
</AlertDescription>
</div>
<p className="text-muted-foreground mt-2 text-sm">
Add and manage your two factor security settings to add an extra layer of security to your
account!
</p>
<AuthenticatorApp isTwoFactorEnabled={user.twoFactorEnabled} />
</Alert>
<div className="mt-4 max-w-xl">
<h5 className="font-medium">Two-factor methods</h5>
{user.twoFactorEnabled && (
<Alert
className="mt-6 flex flex-col justify-between p-6 sm:flex-row sm:items-center"
variant="neutral"
>
<div className="mb-4 sm:mb-0">
<AlertTitle>Recovery codes</AlertTitle>
<AuthenticatorApp isTwoFactorEnabled={user.twoFactorEnabled} />
</div>
<AlertDescription className="mr-4">
Two factor authentication recovery codes are used to access your account in the
event that you lose access to your authenticator app.
</AlertDescription>
</div>
{user.twoFactorEnabled && (
<div className="mt-4 max-w-xl">
<h5 className="font-medium">Recovery methods</h5>
<RecoveryCodes isTwoFactorEnabled={user.twoFactorEnabled} />
</Alert>
)}
<RecoveryCodes isTwoFactorEnabled={user.twoFactorEnabled} />
</div>
) : (
<Alert className="p-6" variant="neutral">
<AlertTitle>
Your account is managed by {IDENTITY_PROVIDER_NAME[user.identityProvider]}
</AlertTitle>
<AlertDescription>
To update your password, enable two-factor authentication, and manage other security
settings, please go to your {IDENTITY_PROVIDER_NAME[user.identityProvider]} account
settings.
</AlertDescription>
</Alert>
)}
<Alert
className="mt-6 flex flex-col justify-between p-6 sm:flex-row sm:items-center"
variant="neutral"
>
<div className="mb-4 mr-4 sm:mb-0">
<AlertTitle>Recent activity</AlertTitle>
<AlertDescription className="mr-2">
View all recent security activity related to your account.
</AlertDescription>
</div>
<Button asChild>
<Link href="/settings/security/activity">View activity</Link>
</Button>
</Alert>
</div>
);
}

View File

@ -2,16 +2,13 @@
import { useState, useTransition } from 'react';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
import { AlertTriangle, Loader, Plus } from 'lucide-react';
import { Loader, Plus } from 'lucide-react';
import { useLimits } from '@documenso/ee/server-only/limits/provider/client';
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
import type { Template } from '@documenso/prisma/client';
import { trpc } from '@documenso/trpc/react';
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
import { Button } from '@documenso/ui/primitives/button';
import { DataTable } from '@documenso/ui/primitives/data-table';
import { DataTablePagination } from '@documenso/ui/primitives/data-table-pagination';
@ -39,8 +36,6 @@ export const TemplatesDataTable = ({
const [isPending, startTransition] = useTransition();
const updateSearchParams = useUpdateSearchParams();
const { remaining } = useLimits();
const router = useRouter();
const { toast } = useToast();
@ -82,19 +77,6 @@ export const TemplatesDataTable = ({
return (
<div className="relative">
{remaining.documents === 0 && (
<Alert className="mb-4 mt-5">
<AlertTriangle className="h-4 w-4" />
<AlertTitle>Document Limit Exceeded!</AlertTitle>
<AlertDescription className="mt-2">
You have reached your document limit.{' '}
<Link className="underline underline-offset-4" href="/settings/billing">
Upgrade your account to continue!
</Link>
</AlertDescription>
</Alert>
)}
<DataTable
columns={[
{
@ -120,7 +102,7 @@ export const TemplatesDataTable = ({
return (
<div className="flex items-center gap-x-4">
<Button
disabled={isRowLoading || remaining.documents === 0}
disabled={isRowLoading}
loading={isRowLoading}
onClick={async () => {
setLoadingStates((prev) => ({ ...prev, [row.original.id]: true }));

View File

@ -1,7 +1,5 @@
import React from 'react';
import type { Metadata } from 'next';
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
import { getTemplates } from '@documenso/lib/server-only/template/get-templates';
@ -16,10 +14,6 @@ type TemplatesPageProps = {
};
};
export const metadata: Metadata = {
title: 'Templates',
};
export default async function TemplatesPage({ searchParams = {} }: TemplatesPageProps) {
const { user } = await getRequiredServerComponentSession();
const page = Number(searchParams.page) || 1;

View File

@ -1,39 +0,0 @@
'use client';
import { useState } from 'react';
import { FileSearch } from 'lucide-react';
import type { DocumentData } from '@documenso/prisma/client';
import DocumentDialog from '@documenso/ui/components/document/document-dialog';
import type { ButtonProps } from '@documenso/ui/primitives/button';
import { Button } from '@documenso/ui/primitives/button';
export type DocumentPreviewButtonProps = {
className?: string;
documentData: DocumentData;
} & ButtonProps;
export const DocumentPreviewButton = ({
className,
documentData,
...props
}: DocumentPreviewButtonProps) => {
const [showDialog, setShowDialog] = useState(false);
return (
<>
<Button
className={className}
variant="outline"
onClick={() => setShowDialog((visible) => !visible)}
{...props}
>
<FileSearch className="mr-2 h-5 w-5" strokeWidth={1.7} />
View Original Document
</Button>
<DocumentDialog documentData={documentData} open={showDialog} onOpenChange={setShowDialog} />
</>
);
};

View File

@ -1,17 +0,0 @@
import React from 'react';
import { RefreshOnFocus } from '~/components/(dashboard)/refresh-on-focus/refresh-on-focus';
export type SigningLayoutProps = {
children: React.ReactNode;
};
export default function SigningLayout({ children }: SigningLayoutProps) {
return (
<div>
{children}
<RefreshOnFocus />
</div>
);
}

View File

@ -10,15 +10,13 @@ import { getDocumentAndSenderByToken } from '@documenso/lib/server-only/document
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 { getRecipientSignatures } from '@documenso/lib/server-only/recipient/get-recipient-signatures';
import { DocumentStatus, FieldType, RecipientRole } from '@documenso/prisma/client';
import { DocumentStatus, FieldType } from '@documenso/prisma/client';
import { DocumentDownloadButton } from '@documenso/ui/components/document/document-download-button';
import { DocumentShareButton } from '@documenso/ui/components/document/document-share-button';
import { SigningCard3D } from '@documenso/ui/components/signing-card';
import { truncateTitle } from '~/helpers/truncate-title';
import { DocumentPreviewButton } from './document-preview-button';
export type CompletedSigningPageProps = {
params: {
token?: string;
@ -94,10 +92,7 @@ export default async function CompletedSigningPage({
))}
<h2 className="mt-6 max-w-[35ch] text-center text-2xl font-semibold leading-normal md:text-3xl lg:text-4xl">
You have
{recipient.role === RecipientRole.SIGNER && ' signed '}
{recipient.role === RecipientRole.VIEWER && ' viewed '}
{recipient.role === RecipientRole.APPROVER && ' approved '}
You have signed
<span className="mt-1.5 block">"{truncatedTitle}"</span>
</h2>
@ -122,20 +117,12 @@ export default async function CompletedSigningPage({
<div className="mt-8 flex w-full max-w-sm items-center justify-center gap-4">
<DocumentShareButton documentId={document.id} token={recipient.token} />
{document.status === DocumentStatus.COMPLETED ? (
<DocumentDownloadButton
className="flex-1"
fileName={document.title}
documentData={documentData}
disabled={document.status !== DocumentStatus.COMPLETED}
/>
) : (
<DocumentPreviewButton
className="text-[11px]"
title="Signatures will appear once the document has been completed"
documentData={documentData}
/>
)}
<DocumentDownloadButton
className="flex-1"
fileName={document.title}
documentData={documentData}
disabled={document.status !== DocumentStatus.COMPLETED}
/>
</div>
{isLoggedIn ? (

View File

@ -9,7 +9,7 @@ import { useForm } from 'react-hook-form';
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
import { sortFieldsByPosition, validateFieldsInserted } from '@documenso/lib/utils/fields';
import { type Document, type Field, type Recipient, RecipientRole } from '@documenso/prisma/client';
import type { Document, Field, Recipient } from '@documenso/prisma/client';
import { trpc } from '@documenso/trpc/react';
import { FieldToolTip } from '@documenso/ui/components/field/field-tooltip';
import { cn } from '@documenso/ui/lib/utils';
@ -96,114 +96,73 @@ export const SigningForm = ({ document, recipient, fields }: SigningFormProps) =
<fieldset
disabled={isSubmitting}
className={cn(
'custom-scrollbar -mx-2 flex flex-1 flex-col overflow-y-auto overflow-x-hidden px-2',
)}
className={cn('-mx-2 flex flex-1 flex-col overflow-hidden px-2')}
>
<div className={cn('flex flex-1 flex-col')}>
<h3 className="text-foreground text-2xl font-semibold">
{recipient.role === RecipientRole.VIEWER && 'View Document'}
{recipient.role === RecipientRole.SIGNER && 'Sign Document'}
{recipient.role === RecipientRole.APPROVER && 'Approve Document'}
</h3>
{recipient.role === RecipientRole.VIEWER ? (
<>
<p className="text-muted-foreground mt-2 text-sm">
Please mark as viewed to complete
</p>
<hr className="border-border mb-8 mt-4" />
<div className="-mx-2 flex flex-1 flex-col gap-4 overflow-y-auto px-2">
<div className="flex flex-1 flex-col gap-y-4" />
<div className="flex flex-col gap-4 md:flex-row">
<Button
type="button"
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
</Button>
<SignDialog
isSubmitting={isSubmitting}
onSignatureComplete={handleSubmit(onFormSubmit)}
document={document}
fields={fields}
fieldsValidated={fieldsValidated}
role={recipient.role}
/>
</div>
</div>
</>
) : (
<>
<p className="text-muted-foreground mt-2 text-sm">
Please review the document before signing.
</p>
<hr className="border-border mb-8 mt-4" />
<div className="-mx-2 flex flex-1 flex-col gap-4 overflow-y-auto px-2">
<div className="flex flex-1 flex-col gap-y-4">
<div>
<Label htmlFor="full-name">Full Name</Label>
<Input
type="text"
id="full-name"
className="bg-background mt-2"
value={fullName}
onChange={(e) => setFullName(e.target.value.trimStart())}
/>
</div>
<div>
<Label htmlFor="Signature">Signature</Label>
<Card className="mt-2" gradient degrees={-120}>
<CardContent className="p-0">
<SignaturePad
className="h-44 w-full"
disabled={isSubmitting}
defaultValue={signature ?? undefined}
onChange={(value) => {
setSignature(value);
}}
/>
</CardContent>
</Card>
</div>
</div>
<div className="flex flex-col gap-4 md:flex-row">
<Button
type="button"
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
</Button>
<SignDialog
isSubmitting={isSubmitting}
onSignatureComplete={handleSubmit(onFormSubmit)}
document={document}
fields={fields}
fieldsValidated={fieldsValidated}
role={recipient.role}
/>
</div>
</div>
</>
<div
className={cn(
'custom-scrollbar -mx-2 flex flex-1 flex-col overflow-y-auto overflow-x-hidden px-2',
)}
>
<h3 className="text-foreground text-2xl font-semibold">Sign Document</h3>
<p className="text-muted-foreground mt-2 text-sm">
Please review the document before signing.
</p>
<hr className="border-border mb-8 mt-4" />
<div className="-mx-2 flex flex-1 flex-col gap-4 overflow-y-auto px-2">
<div className="flex flex-1 flex-col gap-y-4">
<div>
<Label htmlFor="full-name">Full Name</Label>
<Input
type="text"
id="full-name"
className="bg-background mt-2"
value={fullName}
onChange={(e) => setFullName(e.target.value.trimStart())}
/>
</div>
<div>
<Label htmlFor="Signature">Signature</Label>
<Card className="mt-2" gradient degrees={-120}>
<CardContent className="p-0">
<SignaturePad
className="h-44 w-full"
defaultValue={signature ?? undefined}
onChange={(value) => {
setSignature(value);
}}
/>
</CardContent>
</Card>
</div>
</div>
<div className="flex flex-col gap-4 md:flex-row">
<Button
type="button"
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
</Button>
<SignDialog
isSubmitting={isSubmitting}
onSignatureComplete={handleSubmit(onFormSubmit)}
document={document}
fields={fields}
fieldsValidated={fieldsValidated}
/>
</div>
</div>
</div>
</fieldset>
</form>

View File

@ -14,7 +14,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 { getRecipientSignatures } from '@documenso/lib/server-only/recipient/get-recipient-signatures';
import { symmetricDecrypt } from '@documenso/lib/universal/crypto';
import { DocumentStatus, FieldType, RecipientRole, SigningStatus } 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';
@ -110,10 +110,7 @@ export default async function SigningPage({ params: { token } }: SigningPageProp
<div className="mt-2.5 flex items-center gap-x-6">
<p className="text-muted-foreground">
{document.User.name} ({document.User.email}) has invited you to{' '}
{recipient.role === RecipientRole.VIEWER && 'view'}
{recipient.role === RecipientRole.SIGNER && 'sign'}
{recipient.role === RecipientRole.APPROVER && 'approve'} this document.
{document.User.name} ({document.User.email}) has invited you to sign this document.
</p>
</div>

View File

@ -1,7 +1,6 @@
import { useState } from 'react';
import type { Document, Field } from '@documenso/prisma/client';
import { RecipientRole } from '@documenso/prisma/client';
import { Button } from '@documenso/ui/primitives/button';
import {
Dialog,
@ -18,7 +17,6 @@ export type SignDialogProps = {
fields: Field[];
fieldsValidated: () => void | Promise<void>;
onSignatureComplete: () => void | Promise<void>;
role: RecipientRole;
};
export const SignDialog = ({
@ -27,7 +25,6 @@ export const SignDialog = ({
fields,
fieldsValidated,
onSignatureComplete,
role,
}: SignDialogProps) => {
const [showDialog, setShowDialog] = useState(false);
const truncatedTitle = truncateTitle(document.title);
@ -48,18 +45,9 @@ export const SignDialog = ({
</DialogTrigger>
<DialogContent>
<div className="text-center">
<div className="text-foreground text-xl font-semibold">
{role === RecipientRole.VIEWER && 'Mark Document as Viewed'}
{role === RecipientRole.SIGNER && 'Sign Document'}
{role === RecipientRole.APPROVER && 'Approve Document'}
</div>
<div className="text-xl font-semibold text-neutral-800">Sign Document</div>
<div className="text-muted-foreground mx-auto w-4/5 py-2 text-center">
{role === RecipientRole.VIEWER &&
`You are about to finish viewing "${truncatedTitle}". Are you sure?`}
{role === RecipientRole.SIGNER &&
`You are about to finish signing "${truncatedTitle}". Are you sure?`}
{role === RecipientRole.APPROVER &&
`You are about to finish approving "${truncatedTitle}". Are you sure?`}
You are about to finish signing "{truncatedTitle}". Are you sure?
</div>
</div>
@ -83,9 +71,7 @@ export const SignDialog = ({
loading={isSubmitting}
onClick={onSignatureComplete}
>
{role === RecipientRole.VIEWER && 'Mark as Viewed'}
{role === RecipientRole.SIGNER && 'Sign'}
{role === RecipientRole.APPROVER && 'Approve'}
Sign
</Button>
</div>
</DialogFooter>

View File

@ -1,12 +1,7 @@
import type { Metadata } from 'next';
import Link from 'next/link';
import { Button } from '@documenso/ui/primitives/button';
export const metadata: Metadata = {
title: 'Forgot password',
};
export default function ForgotPasswordPage() {
return (
<div>

View File

@ -1,12 +1,7 @@
import type { Metadata } from 'next';
import Link from 'next/link';
import { ForgotPasswordForm } from '~/components/forms/forgot-password';
export const metadata: Metadata = {
title: 'Forgot Password',
};
export default function ForgotPasswordPage() {
return (
<div>

View File

@ -1,12 +1,7 @@
import type { Metadata } from 'next';
import Link from 'next/link';
import { Button } from '@documenso/ui/primitives/button';
export const metadata: Metadata = {
title: 'Reset Password',
};
export default function ResetPasswordPage() {
return (
<div>

View File

@ -1,14 +1,7 @@
import type { Metadata } from 'next';
import Link from 'next/link';
import { IS_GOOGLE_SSO_ENABLED } from '@documenso/lib/constants/auth';
import { SignInForm } from '~/components/forms/signin';
export const metadata: Metadata = {
title: 'Sign In',
};
export default function SignInPage() {
return (
<div>
@ -18,7 +11,7 @@ export default function SignInPage() {
Welcome back, we are lucky to have you.
</p>
<SignInForm className="mt-4" isGoogleSSOEnabled={IS_GOOGLE_SSO_ENABLED} />
<SignInForm className="mt-4" />
{process.env.NEXT_PUBLIC_DISABLE_SIGNUP !== 'true' && (
<p className="text-muted-foreground mt-6 text-center text-sm">

View File

@ -1,15 +1,8 @@
import type { Metadata } from 'next';
import Link from 'next/link';
import { redirect } from 'next/navigation';
import { IS_GOOGLE_SSO_ENABLED } from '@documenso/lib/constants/auth';
import { SignUpForm } from '~/components/forms/signup';
export const metadata: Metadata = {
title: 'Sign Up',
};
export default function SignUpPage() {
if (process.env.NEXT_PUBLIC_DISABLE_SIGNUP === 'true') {
redirect('/signin');
@ -24,7 +17,7 @@ export default function SignUpPage() {
signing is within your grasp.
</p>
<SignUpForm className="mt-4" isGoogleSSOEnabled={IS_GOOGLE_SSO_ENABLED} />
<SignUpForm className="mt-4" />
<p className="text-muted-foreground mt-6 text-center text-sm">
Already have an account?{' '}

View File

@ -1,14 +1,9 @@
import type { Metadata } from 'next';
import Link from 'next/link';
import { XCircle } from 'lucide-react';
import { Button } from '@documenso/ui/primitives/button';
export const metadata: Metadata = {
title: 'Verify Email',
};
export default function EmailVerificationWithoutTokenPage() {
return (
<div className="flex w-full items-start">

View File

@ -20,10 +20,7 @@ const fontInter = Inter({ subsets: ['latin'], variable: '--font-sans' });
const fontCaveat = Caveat({ subsets: ['latin'], variable: '--font-signature' });
export const metadata = {
title: {
template: '%s - Documenso',
default: 'Documenso',
},
title: 'Documenso - The Open Source DocuSign Alternative',
description:
'Join Documenso, the open signing infrastructure, and get a 10x better signing experience. Pricing starts at $30/mo. forever! Sign in now and enjoy a faster, smarter, and more beautiful document signing process. Integrates with your favorite tools, customizable, and expandable. Support our mission and become a part of our open-source community.',
keywords:

View File

@ -4,10 +4,8 @@ import React from 'react';
import { useCopyToClipboard } from '@documenso/lib/client-only/hooks/use-copy-to-clipboard';
import { getRecipientType } from '@documenso/lib/client-only/recipient-type';
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
import { recipientAbbreviation } from '@documenso/lib/utils/recipient-formatter';
import type { Recipient } from '@documenso/prisma/client';
import { cn } from '@documenso/ui/lib/utils';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { StackAvatar } from './stack-avatar';
@ -21,10 +19,6 @@ export function AvatarWithRecipient({ recipient }: AvatarWithRecipientProps) {
const { toast } = useToast();
const onRecipientClick = () => {
if (!recipient.token) {
return;
}
void copy(`${process.env.NEXT_PUBLIC_WEBAPP_URL}/sign/${recipient.token}`).then(() => {
toast({
title: 'Copied to clipboard',
@ -34,31 +28,19 @@ export function AvatarWithRecipient({ recipient }: AvatarWithRecipientProps) {
};
return (
<div
className={cn('my-1 flex items-center gap-2', {
'cursor-pointer hover:underline': recipient.token,
})}
role={recipient.token ? 'button' : undefined}
title={recipient.token && 'Click to copy signing link for sending to recipient'}
onClick={onRecipientClick}
>
<div className="my-1 flex cursor-pointer items-center gap-2" onClick={onRecipientClick}>
<StackAvatar
first={true}
key={recipient.id}
type={getRecipientType(recipient)}
fallbackText={recipientAbbreviation(recipient)}
/>
<div>
<div
className="text-muted-foreground text-sm"
title="Click to copy signing link for sending to recipient"
>
<p>{recipient.email} </p>
<p className="text-muted-foreground/70 text-xs">
{RECIPIENT_ROLES_DESCRIPTION[recipient.role].roleName}
</p>
</div>
</div>
<span
className="text-muted-foreground text-sm hover:underline"
title="Click to copy signing link for sending to recipient"
>
{recipient.email}
</span>
</div>
);
}

View File

@ -1,5 +1,4 @@
import { getRecipientType } from '@documenso/lib/client-only/recipient-type';
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
import { recipientAbbreviation } from '@documenso/lib/utils/recipient-formatter';
import type { Recipient } from '@documenso/prisma/client';
import {
@ -60,12 +59,7 @@ export const StackAvatarsWithTooltip = ({
type={getRecipientType(recipient)}
fallbackText={recipientAbbreviation(recipient)}
/>
<div className="">
<p className="text-muted-foreground text-sm">{recipient.email}</p>
<p className="text-muted-foreground/70 text-xs">
{RECIPIENT_ROLES_DESCRIPTION[recipient.role].roleName}
</p>
</div>
<span className="text-muted-foreground text-sm">{recipient.email}</span>
</div>
))}
</div>

View File

@ -252,11 +252,7 @@ const ThemeCommands = ({ setTheme }: { setTheme: (_theme: string) => void }) =>
);
return THEMES.map((theme) => (
<CommandItem
key={theme.theme}
onSelect={() => setTheme(theme.theme)}
className="mx-2 first:mt-2 last:mb-2"
>
<CommandItem key={theme.theme} onSelect={() => setTheme(theme.theme)}>
<theme.icon className="mr-2" />
{theme.label}
</CommandItem>

View File

@ -33,7 +33,7 @@ export const Header = ({ className, user, ...props }: HeaderProps) => {
return (
<header
className={cn(
'supports-backdrop-blur:bg-background/60 bg-background/95 sticky top-0 z-[60] flex h-16 w-full items-center border-b border-b-transparent backdrop-blur duration-200',
'supports-backdrop-blur:bg-background/60 bg-background/95 sticky top-0 z-[50] flex h-16 w-full items-center border-b border-b-transparent backdrop-blur duration-200',
scrollY > 5 && 'border-b-border',
className,
)}

View File

@ -68,7 +68,7 @@ export const ProfileDropdown = ({ user }: ProfileDropdownProps) => {
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="z-[60] w-56" align="end" forceMount>
<DropdownMenuContent className="w-56" align="end" forceMount>
<DropdownMenuLabel>Account</DropdownMenuLabel>
{isUserAdmin && (
@ -122,7 +122,7 @@ export const ProfileDropdown = ({ user }: ProfileDropdownProps) => {
Themes
</DropdownMenuSubTrigger>
<DropdownMenuPortal>
<DropdownMenuSubContent className="z-[60]">
<DropdownMenuSubContent>
<DropdownMenuRadioGroup value={theme} onValueChange={setTheme}>
<DropdownMenuRadioItem value="light">
<Sun className="mr-2 h-4 w-4" /> Light
@ -141,11 +141,7 @@ export const ProfileDropdown = ({ user }: ProfileDropdownProps) => {
</DropdownMenuSub>
<DropdownMenuSeparator />
<DropdownMenuItem asChild>
<Link
href="https://github.com/documenso/documenso"
className="cursor-pointer"
target="_blank"
>
<Link href="https://github.com/documenso/documenso" className="cursor-pointer">
<LuGithub className="mr-2 h-4 w-4" />
Star on Github
</Link>

View File

@ -4,7 +4,7 @@ import { useEffect, useState } from 'react';
import { AlertTriangle } from 'lucide-react';
import { ONE_DAY, ONE_SECOND } from '@documenso/lib/constants/time';
import { ONE_SECOND } from '@documenso/lib/constants/time';
import { trpc } from '@documenso/trpc/react';
import { Button } from '@documenso/ui/primitives/button';
import {
@ -65,7 +65,7 @@ export const VerifyEmailBanner = ({ email }: VerifyEmailBannerProps) => {
if (emailVerificationDialogLastShown) {
const lastShownTimestamp = parseInt(emailVerificationDialogLastShown);
if (Date.now() - lastShownTimestamp < ONE_DAY) {
if (Date.now() - lastShownTimestamp < 24 * 60 * 60 * 1000) {
return;
}
}

View File

@ -1,4 +1,4 @@
import type { PeriodSelectorValue } from '@documenso/lib/server-only/document/find-documents';
export type PeriodSelectorValue = '' | '7d' | '14d' | '30d';
export const isPeriodSelectorValue = (value: unknown): value is PeriodSelectorValue => {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions

View File

@ -19,14 +19,27 @@ export const AuthenticatorApp = ({ isTwoFactorEnabled }: AuthenticatorAppProps)
return (
<>
<div className="flex-shrink-0">
{isTwoFactorEnabled ? (
<Button variant="destructive" onClick={() => setModalState('disable')}>
Disable 2FA
</Button>
) : (
<Button onClick={() => setModalState('enable')}>Enable 2FA</Button>
)}
<div className="mt-4 flex flex-col justify-between gap-4 rounded-lg border p-4 md:flex-row md:items-center md:gap-8">
<div className="flex-1">
<p>Authenticator app</p>
<p className="text-muted-foreground mt-2 max-w-[50ch] text-sm">
Create one-time passwords that serve as a secondary authentication method for confirming
your identity when requested during the sign-in process.
</p>
</div>
<div>
{isTwoFactorEnabled ? (
<Button variant="destructive" onClick={() => setModalState('disable')} size="sm">
Disable 2FA
</Button>
) : (
<Button onClick={() => setModalState('enable')} size="sm">
Enable 2FA
</Button>
)}
</div>
</div>
<EnableAuthenticatorAppDialog

View File

@ -11,7 +11,6 @@ import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@documenso/ui/primitives/dialog';
@ -146,8 +145,8 @@ export const DisableAuthenticatorAppDialog = ({
/>
</fieldset>
<DialogFooter>
<Button type="button" variant="secondary" onClick={() => onOpenChange(false)}>
<div className="flex w-full items-center justify-between">
<Button type="button" variant="ghost" onClick={() => onOpenChange(false)}>
Cancel
</Button>
@ -158,7 +157,7 @@ export const DisableAuthenticatorAppDialog = ({
>
Disable 2FA
</Button>
</DialogFooter>
</div>
</form>
</Form>
</DialogContent>

View File

@ -15,7 +15,6 @@ import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@documenso/ui/primitives/dialog';
@ -191,15 +190,15 @@ export const EnableAuthenticatorAppDialog = ({
)}
/>
<DialogFooter>
<Button type="button" variant="secondary" onClick={() => onOpenChange(false)}>
<div className="flex w-full items-center justify-between">
<Button type="button" variant="ghost" onClick={() => onOpenChange(false)}>
Cancel
</Button>
<Button type="submit" loading={isSetupTwoFactorAuthenticationSubmitting}>
Continue
</Button>
</DialogFooter>
</div>
</form>
</Form>
);
@ -252,15 +251,15 @@ export const EnableAuthenticatorAppDialog = ({
)}
/>
<DialogFooter>
<Button type="button" variant="secondary" onClick={() => onOpenChange(false)}>
<div className="flex w-full items-center justify-between">
<Button type="button" variant="ghost" onClick={() => onOpenChange(false)}>
Cancel
</Button>
<Button type="submit" loading={isEnableTwoFactorAuthenticationSubmitting}>
Enable 2FA
</Button>
</DialogFooter>
</div>
</form>
</Form>
))

View File

@ -7,6 +7,7 @@ import { Button } from '@documenso/ui/primitives/button';
import { ViewRecoveryCodesDialog } from './view-recovery-codes-dialog';
type RecoveryCodesProps = {
// backupCodes: string[] | null;
isTwoFactorEnabled: boolean;
};
@ -15,13 +16,22 @@ export const RecoveryCodes = ({ isTwoFactorEnabled }: RecoveryCodesProps) => {
return (
<>
<Button
className="flex-shrink-0"
onClick={() => setIsOpen(true)}
disabled={!isTwoFactorEnabled}
>
View Codes
</Button>
<div className="mt-4 flex flex-col justify-between gap-4 rounded-lg border p-4 md:flex-row md:items-center md:gap-8">
<div className="flex-1">
<p>Recovery Codes</p>
<p className="text-muted-foreground mt-2 max-w-[50ch] text-sm">
Recovery codes are used to access your account in the event that you lose access to your
authenticator app.
</p>
</div>
<div>
<Button onClick={() => setIsOpen(true)} disabled={!isTwoFactorEnabled} size="sm">
View Codes
</Button>
</div>
</div>
<ViewRecoveryCodesDialog
key={isOpen ? 'open' : 'closed'}

View File

@ -11,7 +11,6 @@ import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@documenso/ui/primitives/dialog';
@ -120,15 +119,15 @@ export const ViewRecoveryCodesDialog = ({ open, onOpenChange }: ViewRecoveryCode
)}
/>
<DialogFooter>
<Button type="button" variant="secondary" onClick={() => onOpenChange(false)}>
<div className="flex w-full items-center justify-between">
<Button type="button" variant="ghost" onClick={() => onOpenChange(false)}>
Cancel
</Button>
<Button type="submit" loading={isViewRecoveryCodesSubmitting}>
Continue
</Button>
</DialogFooter>
</div>
</form>
</Form>
);

View File

@ -7,7 +7,6 @@ import { z } from 'zod';
import type { User } from '@documenso/prisma/client';
import { TRPCClientError } from '@documenso/trpc/client';
import { trpc } from '@documenso/trpc/react';
import { ZCurrentPasswordSchema, ZPasswordSchema } from '@documenso/trpc/server/auth-router/schema';
import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button';
import {
@ -23,9 +22,18 @@ import { useToast } from '@documenso/ui/primitives/use-toast';
export const ZPasswordFormSchema = z
.object({
currentPassword: ZCurrentPasswordSchema,
password: ZPasswordSchema,
repeatedPassword: ZPasswordSchema,
currentPassword: z
.string()
.min(6, { message: 'Password should contain at least 6 characters' })
.max(72, { message: 'Password should not contain more than 72 characters' }),
password: z
.string()
.min(6, { message: 'Password should contain at least 6 characters' })
.max(72, { message: 'Password should not contain more than 72 characters' }),
repeatedPassword: z
.string()
.min(6, { message: 'Password should contain at least 6 characters' })
.max(72, { message: 'Password should not contain more than 72 characters' }),
})
.refine((data) => data.password === data.repeatedPassword, {
message: 'Passwords do not match',
@ -137,7 +145,7 @@ export const PasswordForm = ({ className }: PasswordFormProps) => {
/>
</fieldset>
<div className="ml-auto mt-4">
<div className="mt-4">
<Button type="submit" loading={isSubmitting}>
{isSubmitting ? 'Updating password...' : 'Update password'}
</Button>

View File

@ -112,6 +112,7 @@ export const ProfileForm = ({ className, user }: ProfileFormProps) => {
</Label>
<Input id="email" type="email" className="bg-muted mt-2" value={user.email} disabled />
</div>
<FormField
control={form.control}
name="signature"
@ -121,8 +122,7 @@ export const ProfileForm = ({ className, user }: ProfileFormProps) => {
<FormControl>
<SignaturePad
className="h-44 w-full"
disabled={isSubmitting}
containerClassName={cn('rounded-lg border bg-background')}
containerClassName="rounded-lg border bg-background"
defaultValue={user.signature ?? undefined}
onChange={(v) => onChange(v ?? '')}
/>

View File

@ -8,7 +8,6 @@ import { z } from 'zod';
import { TRPCClientError } from '@documenso/trpc/client';
import { trpc } from '@documenso/trpc/react';
import { ZPasswordSchema } from '@documenso/trpc/server/auth-router/schema';
import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button';
import {
@ -24,8 +23,8 @@ import { useToast } from '@documenso/ui/primitives/use-toast';
export const ZResetPasswordFormSchema = z
.object({
password: ZPasswordSchema,
repeatedPassword: ZPasswordSchema,
password: z.string().min(6).max(72),
repeatedPassword: z.string().min(6).max(72),
})
.refine((data) => data.password === data.repeatedPassword, {
path: ['repeatedPassword'],

View File

@ -9,16 +9,9 @@ import { FcGoogle } from 'react-icons/fc';
import { z } from 'zod';
import { ErrorCode, isErrorCode } from '@documenso/lib/next-auth/error-codes';
import { ZCurrentPasswordSchema } from '@documenso/trpc/server/auth-router/schema';
import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button';
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@documenso/ui/primitives/dialog';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@documenso/ui/primitives/dialog';
import {
Form,
FormControl,
@ -46,7 +39,7 @@ const LOGIN_REDIRECT_PATH = '/documents';
export const ZSignInFormSchema = z.object({
email: z.string().email().min(1),
password: ZCurrentPasswordSchema,
password: z.string().min(6).max(72),
totpCode: z.string().trim().optional(),
backupCode: z.string().trim().optional(),
});
@ -55,10 +48,9 @@ export type TSignInFormSchema = z.infer<typeof ZSignInFormSchema>;
export type SignInFormProps = {
className?: string;
isGoogleSSOEnabled?: boolean;
};
export const SignInForm = ({ className, isGoogleSSOEnabled }: SignInFormProps) => {
export const SignInForm = ({ className }: SignInFormProps) => {
const { toast } = useToast();
const [isTwoFactorAuthenticationDialogOpen, setIsTwoFactorAuthenticationDialogOpen] =
useState(false);
@ -117,6 +109,7 @@ export const SignInForm = ({ className, isGoogleSSOEnabled }: SignInFormProps) =
const result = await signIn('credentials', {
...credentials,
callbackUrl: LOGIN_REDIRECT_PATH,
redirect: false,
});
@ -210,29 +203,24 @@ export const SignInForm = ({ className, isGoogleSSOEnabled }: SignInFormProps) =
{isSubmitting ? 'Signing in...' : 'Sign In'}
</Button>
{isGoogleSSOEnabled && (
<>
<div className="relative flex items-center justify-center gap-x-4 py-2 text-xs uppercase">
<div className="bg-border h-px flex-1" />
<span className="text-muted-foreground bg-transparent">Or continue with</span>
<div className="bg-border h-px flex-1" />
</div>
<div className="relative flex items-center justify-center gap-x-4 py-2 text-xs uppercase">
<div className="bg-border h-px flex-1" />
<span className="text-muted-foreground bg-transparent">Or continue with</span>
<div className="bg-border h-px flex-1" />
</div>
<Button
type="button"
size="lg"
variant="outline"
className="bg-background text-muted-foreground border"
disabled={isSubmitting}
onClick={onSignInWithGoogleClick}
>
<FcGoogle className="mr-2 h-5 w-5" />
Google
</Button>
</>
)}
<Button
type="button"
size="lg"
variant={'outline'}
className="bg-background text-muted-foreground border"
disabled={isSubmitting}
onClick={onSignInWithGoogleClick}
>
<FcGoogle className="mr-2 h-5 w-5" />
Google
</Button>
</form>
<Dialog
open={isTwoFactorAuthenticationDialogOpen}
onOpenChange={onCloseTwoFactorAuthenticationDialog}
@ -275,23 +263,21 @@ export const SignInForm = ({ className, isGoogleSSOEnabled }: SignInFormProps) =
)}
/>
)}
<DialogFooter className="mt-4">
<Button
type="button"
variant="secondary"
onClick={onToggleTwoFactorAuthenticationMethodClick}
>
{twoFactorAuthenticationMethod === 'totp'
? 'Use Backup Code'
: 'Use Authenticator'}
</Button>
<Button type="submit" loading={isSubmitting}>
{isSubmitting ? 'Signing in...' : 'Sign In'}
</Button>
</DialogFooter>
</fieldset>
<div className="mt-4 flex items-center justify-between">
<Button
type="button"
variant="ghost"
onClick={onToggleTwoFactorAuthenticationMethodClick}
>
{twoFactorAuthenticationMethod === 'totp' ? 'Use Backup Code' : 'Use Authenticator'}
</Button>
<Button type="submit" loading={isSubmitting}>
{isSubmitting ? 'Signing in...' : 'Sign In'}
</Button>
</div>
</form>
</DialogContent>
</Dialog>

View File

@ -3,13 +3,11 @@
import { zodResolver } from '@hookform/resolvers/zod';
import { signIn } from 'next-auth/react';
import { useForm } from 'react-hook-form';
import { FcGoogle } from 'react-icons/fc';
import { z } from 'zod';
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
import { TRPCClientError } from '@documenso/trpc/client';
import { trpc } from '@documenso/trpc/react';
import { ZPasswordSchema } from '@documenso/trpc/server/auth-router/schema';
import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button';
import {
@ -25,33 +23,23 @@ import { PasswordInput } from '@documenso/ui/primitives/password-input';
import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
import { useToast } from '@documenso/ui/primitives/use-toast';
const SIGN_UP_REDIRECT_PATH = '/documents';
export const ZSignUpFormSchema = z
.object({
name: z.string().trim().min(1, { message: 'Please enter a valid name.' }),
email: z.string().email().min(1),
password: ZPasswordSchema,
signature: z.string().min(1, { message: 'We need your signature to sign documents' }),
})
.refine(
(data) => {
const { name, email, password } = data;
return !password.includes(name) && !password.includes(email.split('@')[0]);
},
{
message: 'Password should not be common or based on personal information',
},
);
export const ZSignUpFormSchema = z.object({
name: z.string().trim().min(1, { message: 'Please enter a valid name.' }),
email: z.string().email().min(1),
password: z
.string()
.min(6, { message: 'Password should contain at least 6 characters' })
.max(72, { message: 'Password should not contain more than 72 characters' }),
signature: z.string().min(1, { message: 'We need your signature to sign documents' }),
});
export type TSignUpFormSchema = z.infer<typeof ZSignUpFormSchema>;
export type SignUpFormProps = {
className?: string;
isGoogleSSOEnabled?: boolean;
};
export const SignUpForm = ({ className, isGoogleSSOEnabled }: SignUpFormProps) => {
export const SignUpForm = ({ className }: SignUpFormProps) => {
const { toast } = useToast();
const analytics = useAnalytics();
@ -76,7 +64,7 @@ export const SignUpForm = ({ className, isGoogleSSOEnabled }: SignUpFormProps) =
await signIn('credentials', {
email,
password,
callbackUrl: SIGN_UP_REDIRECT_PATH,
callbackUrl: '/',
});
analytics.capture('App: User Sign Up', {
@ -101,19 +89,6 @@ export const SignUpForm = ({ className, isGoogleSSOEnabled }: SignUpFormProps) =
}
};
const onSignUpWithGoogleClick = async () => {
try {
await signIn('google', { callbackUrl: SIGN_UP_REDIRECT_PATH });
} catch (err) {
toast({
title: 'An unknown error occurred',
description:
'We encountered an unknown error while attempting to sign you Up. Please try again later.',
variant: 'destructive',
});
}
};
return (
<Form {...form}>
<form
@ -172,7 +147,6 @@ export const SignUpForm = ({ className, isGoogleSSOEnabled }: SignUpFormProps) =
<FormControl>
<SignaturePad
className="h-36 w-full"
disabled={isSubmitting}
containerClassName="mt-2 rounded-lg border bg-background"
onChange={(v) => onChange(v ?? '')}
/>
@ -192,28 +166,6 @@ export const SignUpForm = ({ className, isGoogleSSOEnabled }: SignUpFormProps) =
>
{isSubmitting ? 'Signing up...' : 'Sign Up'}
</Button>
{isGoogleSSOEnabled && (
<>
<div className="relative flex items-center justify-center gap-x-4 py-2 text-xs uppercase">
<div className="bg-border h-px flex-1" />
<span className="text-muted-foreground bg-transparent">Or</span>
<div className="bg-border h-px flex-1" />
</div>
<Button
type="button"
size="lg"
variant={'outline'}
className="bg-background text-muted-foreground border"
disabled={isSubmitting}
onClick={onSignUpWithGoogleClick}
>
<FcGoogle className="mr-2 h-5 w-5" />
Sign Up with Google
</Button>
</>
)}
</form>
</Form>
);

View File

@ -1,65 +1,17 @@
import type { NextApiRequest, NextApiResponse } from 'next';
// import { NextApiRequest, NextApiResponse } from 'next';
import NextAuth from 'next-auth';
import { NEXT_AUTH_OPTIONS } from '@documenso/lib/next-auth/auth-options';
import { extractNextApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import { prisma } from '@documenso/prisma';
import { UserSecurityAuditLogType } from '@documenso/prisma/client';
export default async function auth(req: NextApiRequest, res: NextApiResponse) {
const { ipAddress, userAgent } = extractNextApiRequestMetadata(req);
export default NextAuth({
...NEXT_AUTH_OPTIONS,
pages: {
signIn: '/signin',
signOut: '/signout',
error: '/signin',
},
});
return await NextAuth(req, res, {
...NEXT_AUTH_OPTIONS,
pages: {
signIn: '/signin',
signOut: '/signout',
error: '/signin',
},
events: {
signIn: async ({ user }) => {
await prisma.userSecurityAuditLog.create({
data: {
userId: user.id,
ipAddress,
userAgent,
type: UserSecurityAuditLogType.SIGN_IN,
},
});
},
signOut: async ({ token }) => {
const userId = typeof token.id === 'string' ? parseInt(token.id) : token.id;
if (isNaN(userId)) {
return;
}
await prisma.userSecurityAuditLog.create({
data: {
userId,
ipAddress,
userAgent,
type: UserSecurityAuditLogType.SIGN_OUT,
},
});
},
linkAccount: async ({ user }) => {
const userId = typeof user.id === 'string' ? parseInt(user.id) : user.id;
if (isNaN(userId)) {
return;
}
await prisma.userSecurityAuditLog.create({
data: {
userId,
ipAddress,
userAgent,
type: UserSecurityAuditLogType.ACCOUNT_SSO_LINK,
},
});
},
},
});
}
// export default async function handler(_req: NextApiRequest, res: NextApiResponse) {
// res.json({ hello: 'world' });
// }

View File

@ -39,14 +39,6 @@ ENV HUSKY 0
ENV DOCKER_OUTPUT 1
ENV NEXT_TELEMETRY_DISABLED 1
# Encryption keys
ARG NEXT_PRIVATE_ENCRYPTION_KEY="CAFEBABE"
ENV NEXT_PRIVATE_ENCRYPTION_KEY="$NEXT_PRIVATE_ENCRYPTION_KEY"
ARG NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY="DEADBEEF"
ENV NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY="$NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY"
# Uncomment and use build args to enable remote caching
# ARG TURBO_TEAM
# ENV TURBO_TEAM=$TURBO_TEAM

View File

@ -23,8 +23,7 @@ services:
- database
- inbucket
environment:
- NEXT_PRIVATE_DATABASE_URL=postgres://documenso:password@database:5432/documenso
- NEXT_PRIVATE_DIRECT_DATABASE_URL=postgres://documenso:password@database:5432/documenso
- DATABASE_URL=postgres://documenso:password@database:5432/documenso
- NEXT_PUBLIC_WEBAPP_URL=http://localhost:3000
- NEXTAUTH_SECRET=my-super-secure-secret
- NEXTAUTH_URL=http://localhost:3000

498
package-lock.json generated
View File

@ -43,6 +43,7 @@
"lucide-react": "^0.279.0",
"luxon": "^3.4.0",
"micro": "^10.0.1",
"million": "^2.6.4",
"next": "14.0.3",
"next-auth": "4.24.5",
"next-contentlayer": "^0.3.4",
@ -158,7 +159,6 @@
"sharp": "0.33.1",
"ts-pattern": "^5.0.5",
"typescript": "5.2.2",
"ua-parser-js": "^1.0.37",
"uqr": "^0.1.2",
"zod": "^3.22.4"
},
@ -167,8 +167,7 @@
"@types/luxon": "^3.3.1",
"@types/node": "20.1.0",
"@types/react": "18.2.18",
"@types/react-dom": "18.2.7",
"@types/ua-parser-js": "^0.7.39"
"@types/react-dom": "18.2.7"
}
},
"apps/web/node_modules/@types/node": {
@ -247,6 +246,18 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@ampproject/remapping": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz",
"integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==",
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.0",
"@jridgewell/trace-mapping": "^0.3.9"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@aws-crypto/crc32": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-3.0.0.tgz",
@ -1065,9 +1076,9 @@
}
},
"node_modules/@babel/code-frame": {
"version": "7.23.4",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.4.tgz",
"integrity": "sha512-r1IONyb6Ia+jYR2vvIDhdWdlTGhqbBoFqLTQidzZ4kepUFH15ejXvFHxCVbtl7BOXIudsIubf4E81xeA3h3IXA==",
"version": "7.23.5",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz",
"integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==",
"dependencies": {
"@babel/highlight": "^7.23.4",
"chalk": "^2.4.2"
@ -1140,6 +1151,117 @@
"node": ">=4"
}
},
"node_modules/@babel/compat-data": {
"version": "7.23.5",
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.5.tgz",
"integrity": "sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw==",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/core": {
"version": "7.23.7",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.7.tgz",
"integrity": "sha512-+UpDgowcmqe36d4NwqvKsyPMlOLNGMsfMmQ5WGCu+siCe3t3dfe9njrzGfdN4qq+bcNUt0+Vw6haRxBOycs4dw==",
"dependencies": {
"@ampproject/remapping": "^2.2.0",
"@babel/code-frame": "^7.23.5",
"@babel/generator": "^7.23.6",
"@babel/helper-compilation-targets": "^7.23.6",
"@babel/helper-module-transforms": "^7.23.3",
"@babel/helpers": "^7.23.7",
"@babel/parser": "^7.23.6",
"@babel/template": "^7.22.15",
"@babel/traverse": "^7.23.7",
"@babel/types": "^7.23.6",
"convert-source-map": "^2.0.0",
"debug": "^4.1.0",
"gensync": "^1.0.0-beta.2",
"json5": "^2.2.3",
"semver": "^6.3.1"
},
"engines": {
"node": ">=6.9.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/babel"
}
},
"node_modules/@babel/core/node_modules/@babel/generator": {
"version": "7.23.6",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz",
"integrity": "sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==",
"dependencies": {
"@babel/types": "^7.23.6",
"@jridgewell/gen-mapping": "^0.3.2",
"@jridgewell/trace-mapping": "^0.3.17",
"jsesc": "^2.5.1"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/core/node_modules/@babel/traverse": {
"version": "7.23.7",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.7.tgz",
"integrity": "sha512-tY3mM8rH9jM0YHFGyfC0/xf+SB5eKUu7HPj7/k3fpi9dAlsMc5YbQvDi0Sh2QTPXqMhyaAtzAr807TIyfQrmyg==",
"dependencies": {
"@babel/code-frame": "^7.23.5",
"@babel/generator": "^7.23.6",
"@babel/helper-environment-visitor": "^7.22.20",
"@babel/helper-function-name": "^7.23.0",
"@babel/helper-hoist-variables": "^7.22.5",
"@babel/helper-split-export-declaration": "^7.22.6",
"@babel/parser": "^7.23.6",
"@babel/types": "^7.23.6",
"debug": "^4.3.1",
"globals": "^11.1.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/core/node_modules/@babel/types": {
"version": "7.23.6",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz",
"integrity": "sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==",
"dependencies": {
"@babel/helper-string-parser": "^7.23.4",
"@babel/helper-validator-identifier": "^7.22.20",
"to-fast-properties": "^2.0.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/core/node_modules/globals": {
"version": "11.12.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
"integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
"engines": {
"node": ">=4"
}
},
"node_modules/@babel/core/node_modules/json5": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
"bin": {
"json5": "lib/cli.js"
},
"engines": {
"node": ">=6"
}
},
"node_modules/@babel/core/node_modules/semver": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
"bin": {
"semver": "bin/semver.js"
}
},
"node_modules/@babel/generator": {
"version": "7.17.7",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.17.7.tgz",
@ -1161,6 +1283,42 @@
"node": ">=0.10.0"
}
},
"node_modules/@babel/helper-compilation-targets": {
"version": "7.23.6",
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz",
"integrity": "sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==",
"dependencies": {
"@babel/compat-data": "^7.23.5",
"@babel/helper-validator-option": "^7.23.5",
"browserslist": "^4.22.2",
"lru-cache": "^5.1.1",
"semver": "^6.3.1"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
"integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
"dependencies": {
"yallist": "^3.0.2"
}
},
"node_modules/@babel/helper-compilation-targets/node_modules/semver": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
"bin": {
"semver": "bin/semver.js"
}
},
"node_modules/@babel/helper-compilation-targets/node_modules/yallist": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="
},
"node_modules/@babel/helper-environment-visitor": {
"version": "7.22.20",
"resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz",
@ -1218,6 +1376,80 @@
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-module-imports": {
"version": "7.22.15",
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz",
"integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==",
"dependencies": {
"@babel/types": "^7.22.15"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-module-imports/node_modules/@babel/types": {
"version": "7.23.6",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz",
"integrity": "sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==",
"dependencies": {
"@babel/helper-string-parser": "^7.23.4",
"@babel/helper-validator-identifier": "^7.22.20",
"to-fast-properties": "^2.0.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-module-transforms": {
"version": "7.23.3",
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz",
"integrity": "sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==",
"dependencies": {
"@babel/helper-environment-visitor": "^7.22.20",
"@babel/helper-module-imports": "^7.22.15",
"@babel/helper-simple-access": "^7.22.5",
"@babel/helper-split-export-declaration": "^7.22.6",
"@babel/helper-validator-identifier": "^7.22.20"
},
"engines": {
"node": ">=6.9.0"
},
"peerDependencies": {
"@babel/core": "^7.0.0"
}
},
"node_modules/@babel/helper-plugin-utils": {
"version": "7.22.5",
"resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz",
"integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-simple-access": {
"version": "7.22.5",
"resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz",
"integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==",
"dependencies": {
"@babel/types": "^7.22.5"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-simple-access/node_modules/@babel/types": {
"version": "7.23.6",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz",
"integrity": "sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==",
"dependencies": {
"@babel/helper-string-parser": "^7.23.4",
"@babel/helper-validator-identifier": "^7.22.20",
"to-fast-properties": "^2.0.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-split-export-declaration": {
"version": "7.22.6",
"resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz",
@ -1258,6 +1490,82 @@
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-validator-option": {
"version": "7.23.5",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz",
"integrity": "sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helpers": {
"version": "7.23.8",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.8.tgz",
"integrity": "sha512-KDqYz4PiOWvDFrdHLPhKtCThtIcKVy6avWD2oG4GEvyQ+XDZwHD4YQd+H2vNMnq2rkdxsDkU82T+Vk8U/WXHRQ==",
"dependencies": {
"@babel/template": "^7.22.15",
"@babel/traverse": "^7.23.7",
"@babel/types": "^7.23.6"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helpers/node_modules/@babel/generator": {
"version": "7.23.6",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz",
"integrity": "sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==",
"dependencies": {
"@babel/types": "^7.23.6",
"@jridgewell/gen-mapping": "^0.3.2",
"@jridgewell/trace-mapping": "^0.3.17",
"jsesc": "^2.5.1"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helpers/node_modules/@babel/traverse": {
"version": "7.23.7",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.7.tgz",
"integrity": "sha512-tY3mM8rH9jM0YHFGyfC0/xf+SB5eKUu7HPj7/k3fpi9dAlsMc5YbQvDi0Sh2QTPXqMhyaAtzAr807TIyfQrmyg==",
"dependencies": {
"@babel/code-frame": "^7.23.5",
"@babel/generator": "^7.23.6",
"@babel/helper-environment-visitor": "^7.22.20",
"@babel/helper-function-name": "^7.23.0",
"@babel/helper-hoist-variables": "^7.22.5",
"@babel/helper-split-export-declaration": "^7.22.6",
"@babel/parser": "^7.23.6",
"@babel/types": "^7.23.6",
"debug": "^4.3.1",
"globals": "^11.1.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helpers/node_modules/@babel/types": {
"version": "7.23.6",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz",
"integrity": "sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==",
"dependencies": {
"@babel/helper-string-parser": "^7.23.4",
"@babel/helper-validator-identifier": "^7.22.20",
"to-fast-properties": "^2.0.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helpers/node_modules/globals": {
"version": "11.12.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
"integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
"engines": {
"node": ">=4"
}
},
"node_modules/@babel/highlight": {
"version": "7.23.4",
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz",
@ -1336,9 +1644,9 @@
}
},
"node_modules/@babel/parser": {
"version": "7.23.4",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.4.tgz",
"integrity": "sha512-vf3Xna6UEprW+7t6EtOmFpHNAuxw3xqPZghy+brsnusscJRW5BMUzzHZc5ICjULee81WeUV2jjakG09MDglJXQ==",
"version": "7.23.6",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.6.tgz",
"integrity": "sha512-Z2uID7YJ7oNvAI20O9X0bblw7Qqs8Q2hFy0R9tAfnfLkp5MW0UH9eUvnDSnFwKZ0AvgS1ucqR4KzvVHgnke1VQ==",
"bin": {
"parser": "bin/babel-parser.js"
},
@ -1346,6 +1654,34 @@
"node": ">=6.0.0"
}
},
"node_modules/@babel/plugin-syntax-jsx": {
"version": "7.23.3",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.23.3.tgz",
"integrity": "sha512-EB2MELswq55OHUoRZLGg/zC7QWUKfNLpE57m/S2yr1uEneIgsTgrSzXP3NXEsMkVn76OlaVVnzN+ugObuYGwhg==",
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5"
},
"engines": {
"node": ">=6.9.0"
},
"peerDependencies": {
"@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/plugin-syntax-typescript": {
"version": "7.23.3",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.23.3.tgz",
"integrity": "sha512-9EiNjVJOMwCO+43TqoTrgQ8jMwcAd0sWyXi9RPfIsLTj4R2MADDDQXELhffaUx/uJv2AYcxBgPwH6j4TIA4ytQ==",
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5"
},
"engines": {
"node": ">=6.9.0"
},
"peerDependencies": {
"@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/runtime": {
"version": "7.23.4",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.4.tgz",
@ -6758,12 +7094,6 @@
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.6.tgz",
"integrity": "sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A=="
},
"node_modules/@types/ua-parser-js": {
"version": "0.7.39",
"resolved": "https://registry.npmjs.org/@types/ua-parser-js/-/ua-parser-js-0.7.39.tgz",
"integrity": "sha512-P/oDfpofrdtF5xw433SPALpdSchtJmY7nsJItf8h3KXqOslkbySh8zq4dSWXH2oTjRvJ5PczVEoCZPow6GicLg==",
"dev": true
},
"node_modules/@types/unist": {
"version": "2.0.10",
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.10.tgz",
@ -7589,9 +7919,9 @@
}
},
"node_modules/browserslist": {
"version": "4.22.1",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.1.tgz",
"integrity": "sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ==",
"version": "4.22.2",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.2.tgz",
"integrity": "sha512-0UgcrvQmBDvZHFGdYUehrCNIazki7/lUP3kkoi/r3YB2amZbFM9J43ZRkJTXBUZK4gmx56+Sqk9+Vs9mwZx9+A==",
"funding": [
{
"type": "opencollective",
@ -7607,9 +7937,9 @@
}
],
"dependencies": {
"caniuse-lite": "^1.0.30001541",
"electron-to-chromium": "^1.4.535",
"node-releases": "^2.0.13",
"caniuse-lite": "^1.0.30001565",
"electron-to-chromium": "^1.4.601",
"node-releases": "^2.0.14",
"update-browserslist-db": "^1.0.13"
},
"bin": {
@ -7771,9 +8101,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001564",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001564.tgz",
"integrity": "sha512-DqAOf+rhof+6GVx1y+xzbFPeOumfQnhYzVnZD6LAXijR77yPtm9mfOcqOnT3mpnJiZVT+kwLAFnRlZcIz+c6bg==",
"version": "1.0.30001579",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001579.tgz",
"integrity": "sha512-u5AUVkixruKHJjw/pj9wISlcMpgFWzSrczLZbrqBSxukQixmg0SJ5sZTpvaFvxU0HoQKd4yoyAogyrAz9pzJnA==",
"funding": [
{
"type": "opencollective",
@ -8554,6 +8884,11 @@
"node": ">=14"
}
},
"node_modules/convert-source-map": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="
},
"node_modules/cookie": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
@ -9355,9 +9690,9 @@
}
},
"node_modules/electron-to-chromium": {
"version": "1.4.593",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.593.tgz",
"integrity": "sha512-c7+Hhj87zWmdpmjDONbvNKNo24tvmD4mjal1+qqTYTrlF0/sNpAcDlU0Ki84ftA/5yj3BF2QhSGEC0Rky6larg=="
"version": "1.4.637",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.637.tgz",
"integrity": "sha512-G7j3UCOukFtxVO1vWrPQUoDk3kL70mtvjc/DC/k2o7lE0wAdq+Vwp1ipagOow+BH0uVztFysLWbkM/RTIrbK3w=="
},
"node_modules/emoji-regex": {
"version": "9.2.2",
@ -11064,6 +11399,14 @@
"node": ">=8"
}
},
"node_modules/gensync": {
"version": "1.0.0-beta.2",
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
"integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/get-caller-file": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
@ -14133,6 +14476,54 @@
"node": ">=8.6"
}
},
"node_modules/million": {
"version": "2.6.4",
"resolved": "https://registry.npmjs.org/million/-/million-2.6.4.tgz",
"integrity": "sha512-voUkdd/jHWrG+7NS+mX49Pat+POKdgGW78V7pYMSrTaOjUitR6ySEcAci8hn17Rsx1IMI3+5w41dkADM1J1ZEg==",
"dependencies": {
"@babel/core": "^7.21.0",
"@babel/generator": "^7.22.7",
"@babel/plugin-syntax-jsx": "^7.21.4",
"@babel/plugin-syntax-typescript": "^7.21.4",
"@babel/types": "^7.21.3",
"kleur": "^4.1.5",
"rollup": "^3.28.0",
"unplugin": "^1.3.1"
},
"bin": {
"million": "packages/cli/dist/index.js"
},
"funding": {
"url": "https://github.com/sponsors/aidenybai"
}
},
"node_modules/million/node_modules/@babel/generator": {
"version": "7.23.6",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz",
"integrity": "sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==",
"dependencies": {
"@babel/types": "^7.23.6",
"@jridgewell/gen-mapping": "^0.3.2",
"@jridgewell/trace-mapping": "^0.3.17",
"jsesc": "^2.5.1"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/million/node_modules/@babel/types": {
"version": "7.23.6",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz",
"integrity": "sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==",
"dependencies": {
"@babel/helper-string-parser": "^7.23.4",
"@babel/helper-validator-identifier": "^7.22.20",
"to-fast-properties": "^2.0.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
@ -14579,9 +14970,9 @@
}
},
"node_modules/node-releases": {
"version": "2.0.13",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz",
"integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ=="
"version": "2.0.14",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz",
"integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw=="
},
"node_modules/node-signpdf": {
"version": "2.0.0",
@ -16923,7 +17314,6 @@
"version": "3.29.4",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz",
"integrity": "sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==",
"dev": true,
"bin": {
"rollup": "dist/bin/rollup"
},
@ -18651,28 +19041,6 @@
"node": ">=14.17"
}
},
"node_modules/ua-parser-js": {
"version": "1.0.37",
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.37.tgz",
"integrity": "sha512-bhTyI94tZofjo+Dn8SN6Zv8nBDvyXTymAdM3LDI/0IboIUwTu1rEhW7v2TfiVsoYWgkQ4kOVqnI8APUFbIQIFQ==",
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/ua-parser-js"
},
{
"type": "paypal",
"url": "https://paypal.me/faisalman"
},
{
"type": "github",
"url": "https://github.com/sponsors/faisalman"
}
],
"engines": {
"node": "*"
}
},
"node_modules/unbox-primitive": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz",
@ -18834,6 +19202,17 @@
"node": ">= 0.8"
}
},
"node_modules/unplugin": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.6.0.tgz",
"integrity": "sha512-BfJEpWBu3aE/AyHx8VaNE/WgouoQxgH9baAiH82JjX8cqVyi3uJQstqwD5J+SZxIK326SZIhsSZlALXVBCknTQ==",
"dependencies": {
"acorn": "^8.11.2",
"chokidar": "^3.5.3",
"webpack-sources": "^3.2.3",
"webpack-virtual-modules": "^0.6.1"
}
},
"node_modules/unzipper": {
"version": "0.10.14",
"resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.14.tgz",
@ -19154,6 +19533,19 @@
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
},
"node_modules/webpack-sources": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz",
"integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==",
"engines": {
"node": ">=10.13.0"
}
},
"node_modules/webpack-virtual-modules": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.1.tgz",
"integrity": "sha512-poXpCylU7ExuvZK8z+On3kX+S8o/2dQ/SVYueKA0D4WEMXROXgY8Ez50/bQEUmvoSMMrWcrJqCHuhAbsiwg7Dg=="
},
"node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",

View File

@ -12,7 +12,7 @@ test.describe.configure({ mode: 'serial' });
const username = 'Test User';
const email = 'test-user@auth-flow.documenso.com';
const password = 'Password123#';
const password = 'Password123';
test('user can sign up with email and password', async ({ page }: { page: Page }) => {
await page.goto('/signup');

View File

@ -1,6 +1,3 @@
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
import type { RecipientRole } from '@documenso/prisma/client';
import { Button, Section, Text } from '../components';
import { TemplateDocumentImage } from './template-document-image';
@ -10,7 +7,6 @@ export interface TemplateDocumentInviteProps {
documentName: string;
signDocumentLink: string;
assetBaseUrl: string;
role: RecipientRole;
}
export const TemplateDocumentInvite = ({
@ -18,22 +14,19 @@ export const TemplateDocumentInvite = ({
documentName,
signDocumentLink,
assetBaseUrl,
role,
}: TemplateDocumentInviteProps) => {
const { actionVerb, progressiveVerb } = RECIPIENT_ROLES_DESCRIPTION[role];
return (
<>
<TemplateDocumentImage className="mt-6" assetBaseUrl={assetBaseUrl} />
<Section>
<Text className="text-primary mx-auto mb-0 max-w-[80%] text-center text-lg font-semibold">
{inviterName} has invited you to {actionVerb.toLowerCase()}
{inviterName} has invited you to sign
<br />"{documentName}"
</Text>
<Text className="my-1 text-center text-base text-slate-400">
Continue by {progressiveVerb.toLowerCase()} the document.
Continue by signing the document.
</Text>
<Section className="mb-6 mt-8 text-center">
@ -41,7 +34,7 @@ export const TemplateDocumentInvite = ({
className="bg-documenso-500 inline-flex items-center justify-center rounded-lg px-6 py-3 text-center text-sm font-medium text-black no-underline"
href={signDocumentLink}
>
{actionVerb} Document
Sign Document
</Button>
</Section>
</Section>

View File

@ -10,7 +10,7 @@ export const TemplateFooter = ({ isDocument = true }: TemplateFooterProps) => {
{isDocument && (
<Text className="my-4 text-base text-slate-400">
This document was sent using{' '}
<Link className="text-[#7AC455]" href="https://documen.so/mail-footer">
<Link className="text-[#7AC455]" href="https://documenso.com">
Documenso.
</Link>
</Text>

View File

@ -1,5 +1,3 @@
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
import type { RecipientRole } from '@documenso/prisma/client';
import config from '@documenso/tailwind-config';
import {
@ -21,7 +19,6 @@ import { TemplateFooter } from '../template-components/template-footer';
export type DocumentInviteEmailTemplateProps = Partial<TemplateDocumentInviteProps> & {
customBody?: string;
role: RecipientRole;
};
export const DocumentInviteEmailTemplate = ({
@ -31,11 +28,8 @@ export const DocumentInviteEmailTemplate = ({
signDocumentLink = 'https://documenso.com',
assetBaseUrl = 'http://localhost:3002',
customBody,
role,
}: DocumentInviteEmailTemplateProps) => {
const action = RECIPIENT_ROLES_DESCRIPTION[role].actionVerb.toLowerCase();
const previewText = `${inviterName} has invited you to ${action} ${documentName}`;
const previewText = `${inviterName} has invited you to sign ${documentName}`;
const getAssetUrl = (path: string) => {
return new URL(path, assetBaseUrl).toString();
@ -70,7 +64,6 @@ export const DocumentInviteEmailTemplate = ({
documentName={documentName}
signDocumentLink={signDocumentLink}
assetBaseUrl={assetBaseUrl}
role={role}
/>
</Section>
</Container>
@ -88,7 +81,7 @@ export const DocumentInviteEmailTemplate = ({
{customBody ? (
<pre className="font-sans text-base text-slate-400">{customBody}</pre>
) : (
`${inviterName} has invited you to ${action} the document "${documentName}".`
`${inviterName} has invited you to sign the document "${documentName}".`
)}
</Text>
</Section>

View File

@ -1,10 +1,10 @@
import type { Recipient } from '@documenso/prisma/client';
import { ReadStatus, RecipientRole, SendStatus, SigningStatus } from '@documenso/prisma/client';
import { ReadStatus, SendStatus, SigningStatus } from '@documenso/prisma/client';
export const getRecipientType = (recipient: Recipient) => {
if (
recipient.role === RecipientRole.CC ||
(recipient.sendStatus === SendStatus.SENT && recipient.signingStatus === SigningStatus.SIGNED)
recipient.sendStatus === SendStatus.SENT &&
recipient.signingStatus === SigningStatus.SIGNED
) {
return 'completed';
}

View File

@ -6,6 +6,3 @@ export const APP_FOLDER = IS_APP_MARKETING ? 'marketing' : 'web';
export const APP_BASE_URL = IS_APP_WEB
? process.env.NEXT_PUBLIC_WEBAPP_URL
: process.env.NEXT_PUBLIC_MARKETING_URL;
export const APP_DOCUMENT_UPLOAD_SIZE_LIMIT =
Number(process.env.NEXT_PUBLIC_DOCUMENT_SIZE_UPLOAD_LIMIT) || 50;

View File

@ -1,25 +1 @@
import { IdentityProvider, UserSecurityAuditLogType } from '@documenso/prisma/client';
export const SALT_ROUNDS = 12;
export const IDENTITY_PROVIDER_NAME: { [key in IdentityProvider]: string } = {
[IdentityProvider.DOCUMENSO]: 'Documenso',
[IdentityProvider.GOOGLE]: 'Google',
};
export const IS_GOOGLE_SSO_ENABLED = Boolean(
process.env.NEXT_PRIVATE_GOOGLE_CLIENT_ID && process.env.NEXT_PRIVATE_GOOGLE_CLIENT_SECRET,
);
export const USER_SECURITY_AUDIT_LOG_MAP: { [key in UserSecurityAuditLogType]: string } = {
[UserSecurityAuditLogType.ACCOUNT_SSO_LINK]: 'Linked account to SSO',
[UserSecurityAuditLogType.ACCOUNT_PROFILE_UPDATE]: 'Profile updated',
[UserSecurityAuditLogType.AUTH_2FA_DISABLE]: '2FA Disabled',
[UserSecurityAuditLogType.AUTH_2FA_ENABLE]: '2FA Enabled',
[UserSecurityAuditLogType.PASSWORD_RESET]: 'Password reset',
[UserSecurityAuditLogType.PASSWORD_UPDATE]: 'Password updated',
[UserSecurityAuditLogType.SIGN_OUT]: 'Signed Out',
[UserSecurityAuditLogType.SIGN_IN]: 'Signed In',
[UserSecurityAuditLogType.SIGN_IN_FAIL]: 'Sign in attempt failed',
[UserSecurityAuditLogType.SIGN_IN_2FA_FAIL]: 'Sign in 2FA attempt failed',
};

View File

@ -1,25 +1 @@
export const DOCUMENSO_ENCRYPTION_KEY = process.env.NEXT_PRIVATE_ENCRYPTION_KEY;
export const DOCUMENSO_ENCRYPTION_SECONDARY_KEY = process.env.NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY;
if (typeof window === 'undefined') {
if (!DOCUMENSO_ENCRYPTION_KEY || !DOCUMENSO_ENCRYPTION_SECONDARY_KEY) {
throw new Error('Missing DOCUMENSO_ENCRYPTION_KEY or DOCUMENSO_ENCRYPTION_SECONDARY_KEY keys');
}
if (DOCUMENSO_ENCRYPTION_KEY === DOCUMENSO_ENCRYPTION_SECONDARY_KEY) {
throw new Error(
'DOCUMENSO_ENCRYPTION_KEY and DOCUMENSO_ENCRYPTION_SECONDARY_KEY cannot be equal',
);
}
}
if (DOCUMENSO_ENCRYPTION_KEY === 'CAFEBABE') {
console.warn('*********************************************************************');
console.warn('*');
console.warn('*');
console.warn('Please change the encryption key from the default value of "CAFEBABE"');
console.warn('*');
console.warn('*');
console.warn('*********************************************************************');
}

View File

@ -1,26 +0,0 @@
import { RecipientRole } from '@documenso/prisma/client';
export const RECIPIENT_ROLES_DESCRIPTION: {
[key in RecipientRole]: { actionVerb: string; progressiveVerb: string; roleName: string };
} = {
[RecipientRole.APPROVER]: {
actionVerb: 'Approve',
progressiveVerb: 'Approving',
roleName: 'Approver',
},
[RecipientRole.CC]: {
actionVerb: 'CC',
progressiveVerb: 'CC',
roleName: 'CC',
},
[RecipientRole.SIGNER]: {
actionVerb: 'Sign',
progressiveVerb: 'Signing',
roleName: 'Signer',
},
[RecipientRole.VIEWER]: {
actionVerb: 'View',
progressiveVerb: 'Viewing',
roleName: 'Viewer',
},
};

View File

@ -9,12 +9,10 @@ import type { GoogleProfile } from 'next-auth/providers/google';
import GoogleProvider from 'next-auth/providers/google';
import { prisma } from '@documenso/prisma';
import { IdentityProvider, UserSecurityAuditLogType } from '@documenso/prisma/client';
import { isTwoFactorAuthenticationEnabled } from '../server-only/2fa/is-2fa-availble';
import { validateTwoFactorAuthentication } from '../server-only/2fa/validate-2fa';
import { getUserByEmail } from '../server-only/user/get-user-by-email';
import { extractNextAuthRequestMetadata } from '../universal/extract-request-metadata';
import { ErrorCode } from './error-codes';
export const NEXT_AUTH_OPTIONS: AuthOptions = {
@ -36,7 +34,7 @@ export const NEXT_AUTH_OPTIONS: AuthOptions = {
},
backupCode: { label: 'Backup Code', type: 'input', placeholder: 'Two-factor backup code' },
},
authorize: async (credentials, req) => {
authorize: async (credentials, _req) => {
if (!credentials) {
throw new Error(ErrorCode.CREDENTIALS_NOT_FOUND);
}
@ -52,18 +50,8 @@ export const NEXT_AUTH_OPTIONS: AuthOptions = {
}
const isPasswordsSame = await compare(password, user.password);
const requestMetadata = extractNextAuthRequestMetadata(req);
if (!isPasswordsSame) {
await prisma.userSecurityAuditLog.create({
data: {
userId: user.id,
ipAddress: requestMetadata.ipAddress,
userAgent: requestMetadata.userAgent,
type: UserSecurityAuditLogType.SIGN_IN_FAIL,
},
});
throw new Error(ErrorCode.INCORRECT_EMAIL_PASSWORD);
}
@ -73,15 +61,6 @@ export const NEXT_AUTH_OPTIONS: AuthOptions = {
const isValid = await validateTwoFactorAuthentication({ backupCode, totpCode, user });
if (!isValid) {
await prisma.userSecurityAuditLog.create({
data: {
userId: user.id,
ipAddress: requestMetadata.ipAddress,
userAgent: requestMetadata.userAgent,
type: UserSecurityAuditLogType.SIGN_IN_2FA_FAIL,
},
});
throw new Error(
totpCode
? ErrorCode.INCORRECT_TWO_FACTOR_CODE
@ -114,7 +93,7 @@ export const NEXT_AUTH_OPTIONS: AuthOptions = {
}),
],
callbacks: {
async jwt({ token, user, trigger, account }) {
async jwt({ token, user }) {
const merged = {
...token,
...user,
@ -159,22 +138,6 @@ export const NEXT_AUTH_OPTIONS: AuthOptions = {
merged.emailVerified = user.emailVerified?.toISOString() ?? null;
}
if ((trigger === 'signIn' || trigger === 'signUp') && account?.provider === 'google') {
merged.emailVerified = user?.emailVerified
? new Date(user.emailVerified).toISOString()
: new Date().toISOString();
await prisma.user.update({
where: {
id: Number(merged.id),
},
data: {
emailVerified: merged.emailVerified,
identityProvider: IdentityProvider.GOOGLE,
},
});
}
return {
id: merged.id,
name: merged.name,
@ -212,5 +175,4 @@ export const NEXT_AUTH_OPTIONS: AuthOptions = {
return true;
},
},
// Note: `events` are handled in `apps/web/src/pages/api/auth/[...nextauth].ts` to allow access to the request.
};

View File

@ -1,25 +1,21 @@
import { compare } from 'bcrypt';
import { prisma } from '@documenso/prisma';
import type { User } from '@documenso/prisma/client';
import { UserSecurityAuditLogType } from '@documenso/prisma/client';
import { User } from '@documenso/prisma/client';
import { ErrorCode } from '../../next-auth/error-codes';
import type { RequestMetadata } from '../../universal/extract-request-metadata';
import { validateTwoFactorAuthentication } from './validate-2fa';
type DisableTwoFactorAuthenticationOptions = {
user: User;
backupCode: string;
password: string;
requestMetadata?: RequestMetadata;
};
export const disableTwoFactorAuthentication = async ({
backupCode,
user,
password,
requestMetadata,
}: DisableTwoFactorAuthenticationOptions) => {
if (!user.password) {
throw new Error(ErrorCode.USER_MISSING_PASSWORD);
@ -37,26 +33,15 @@ export const disableTwoFactorAuthentication = async ({
throw new Error(ErrorCode.INCORRECT_TWO_FACTOR_BACKUP_CODE);
}
await prisma.$transaction(async (tx) => {
await tx.user.update({
where: {
id: user.id,
},
data: {
twoFactorEnabled: false,
twoFactorBackupCodes: null,
twoFactorSecret: null,
},
});
await tx.userSecurityAuditLog.create({
data: {
userId: user.id,
type: UserSecurityAuditLogType.AUTH_2FA_DISABLE,
userAgent: requestMetadata?.userAgent,
ipAddress: requestMetadata?.ipAddress,
},
});
await prisma.user.update({
where: {
id: user.id,
},
data: {
twoFactorEnabled: false,
twoFactorBackupCodes: null,
twoFactorSecret: null,
},
});
return true;

View File

@ -1,21 +1,18 @@
import { ErrorCode } from '@documenso/lib/next-auth/error-codes';
import { prisma } from '@documenso/prisma';
import { type User, UserSecurityAuditLogType } from '@documenso/prisma/client';
import { User } from '@documenso/prisma/client';
import type { RequestMetadata } from '../../universal/extract-request-metadata';
import { getBackupCodes } from './get-backup-code';
import { verifyTwoFactorAuthenticationToken } from './verify-2fa-token';
type EnableTwoFactorAuthenticationOptions = {
user: User;
code: string;
requestMetadata?: RequestMetadata;
};
export const enableTwoFactorAuthentication = async ({
user,
code,
requestMetadata,
}: EnableTwoFactorAuthenticationOptions) => {
if (user.identityProvider !== 'DOCUMENSO') {
throw new Error(ErrorCode.INCORRECT_IDENTITY_PROVIDER);
@ -35,24 +32,13 @@ export const enableTwoFactorAuthentication = async ({
throw new Error(ErrorCode.INCORRECT_TWO_FACTOR_CODE);
}
const updatedUser = await prisma.$transaction(async (tx) => {
await tx.userSecurityAuditLog.create({
data: {
userId: user.id,
type: UserSecurityAuditLogType.AUTH_2FA_ENABLE,
userAgent: requestMetadata?.userAgent,
ipAddress: requestMetadata?.ipAddress,
},
});
return await tx.user.update({
where: {
id: user.id,
},
data: {
twoFactorEnabled: true,
},
});
const updatedUser = await prisma.user.update({
where: {
id: user.id,
},
data: {
twoFactorEnabled: true,
},
});
const recoveryCodes = getBackupCodes({ user: updatedUser });

View File

@ -5,7 +5,7 @@ import { createTOTPKeyURI } from 'oslo/otp';
import { ErrorCode } from '@documenso/lib/next-auth/error-codes';
import { prisma } from '@documenso/prisma';
import { type User } from '@documenso/prisma/client';
import { User } from '@documenso/prisma/client';
import { DOCUMENSO_ENCRYPTION_KEY } from '../../constants/crypto';
import { symmetricEncrypt } from '../../universal/crypto';

View File

@ -1,33 +0,0 @@
import { DOCUMENSO_ENCRYPTION_SECONDARY_KEY } from '@documenso/lib/constants/crypto';
import { ZEncryptedDataSchema } from '@documenso/lib/server-only/crypto/encrypt';
import { symmetricDecrypt } from '@documenso/lib/universal/crypto';
/**
* Decrypt the passed in data. This uses the secondary encrypt key for miscellaneous data.
*
* @param encryptedData The data encrypted with the `encryptSecondaryData` function.
* @returns The decrypted value, or `null` if the data is invalid or expired.
*/
export const decryptSecondaryData = (encryptedData: string): string | null => {
if (!DOCUMENSO_ENCRYPTION_SECONDARY_KEY) {
throw new Error('Missing encryption key');
}
const decryptedBufferValue = symmetricDecrypt({
key: DOCUMENSO_ENCRYPTION_SECONDARY_KEY,
data: encryptedData,
});
const decryptedValue = Buffer.from(decryptedBufferValue).toString('utf-8');
const result = ZEncryptedDataSchema.safeParse(JSON.parse(decryptedValue));
if (!result.success) {
return null;
}
if (result.data.expiresAt !== undefined && result.data.expiresAt < Date.now()) {
return null;
}
return result.data.data;
};

View File

@ -1,42 +0,0 @@
import { z } from 'zod';
import { DOCUMENSO_ENCRYPTION_SECONDARY_KEY } from '@documenso/lib/constants/crypto';
import { symmetricEncrypt } from '@documenso/lib/universal/crypto';
import type { TEncryptSecondaryDataMutationSchema } from '@documenso/trpc/server/crypto/schema';
export const ZEncryptedDataSchema = z.object({
data: z.string(),
expiresAt: z.number().optional(),
});
export type EncryptDataOptions = {
data: string;
/**
* When the data should no longer be allowed to be decrypted.
*
* Leave this empty to never expire the data.
*/
expiresAt?: number;
};
/**
* Encrypt the passed in data. This uses the secondary encrypt key for miscellaneous data.
*
* @returns The encrypted data.
*/
export const encryptSecondaryData = ({ data, expiresAt }: TEncryptSecondaryDataMutationSchema) => {
if (!DOCUMENSO_ENCRYPTION_SECONDARY_KEY) {
throw new Error('Missing encryption key');
}
const dataToEncrypt: z.infer<typeof ZEncryptedDataSchema> = {
data,
expiresAt,
};
return symmetricEncrypt({
key: DOCUMENSO_ENCRYPTION_SECONDARY_KEY,
data: JSON.stringify(dataToEncrypt),
});
};

Some files were not shown because too many files have changed in this diff Show More