mirror of
https://github.com/documenso/documenso.git
synced 2025-11-10 04:22:32 +10:00
Compare commits
25 Commits
v1.3.1-rc.
...
fix/delete
| Author | SHA1 | Date | |
|---|---|---|---|
| a4ae811d24 | |||
| ca6a9da1ab | |||
| fd2a61f651 | |||
| 56f65f3bb3 | |||
| 75ad8a4885 | |||
| db36f69273 | |||
| d766b58f42 | |||
| e90dd518df | |||
| ee0af566a9 | |||
| 11dd93451a | |||
| 2be022b9fc | |||
| 0fac7d7b70 | |||
| b115d85fb7 | |||
| 51d140cf9a | |||
| caec2895cc | |||
| 61967b22c1 | |||
| 8b8ca8578b | |||
| 576544344f | |||
| 1efadb19f5 | |||
| bc1d5cea0a | |||
| 6aed075c56 | |||
| e63122a718 | |||
| 08011f9545 | |||
| 4909eee401 | |||
| 9cc8bbdfc3 |
@ -4,8 +4,10 @@ NEXTAUTH_SECRET="secret"
|
||||
|
||||
# [[CRYPTO]]
|
||||
# Application Key for symmetric encryption and decryption
|
||||
# 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_KEY=""
|
||||
# REQUIRED: This should be a random string of at least 32 characters
|
||||
NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY=""
|
||||
|
||||
# [[AUTH OPTIONAL]]
|
||||
NEXT_PRIVATE_GOOGLE_CLIENT_ID=""
|
||||
|
||||
1
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
1
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
@ -33,3 +33,4 @@ 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
|
||||
|
||||
84
apps/marketing/content/blog/commodifying-signing.mdx
Normal file
84
apps/marketing/content/blog/commodifying-signing.mdx
Normal file
@ -0,0 +1,84 @@
|
||||
---
|
||||
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
|
||||
@ -7,6 +7,8 @@ authorRole: 'Co-Founder'
|
||||
date: 2023-07-13
|
||||
tags:
|
||||
- Manifesto
|
||||
- Open Source
|
||||
- Vision
|
||||
---
|
||||
|
||||
<figure>
|
||||
|
||||
BIN
apps/marketing/public/blog/lighthouse.jpeg
Normal file
BIN
apps/marketing/public/blog/lighthouse.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 331 KiB |
@ -47,6 +47,14 @@ 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 = [
|
||||
|
||||
@ -55,14 +55,14 @@ export const DataTableActionDropdown = ({ row }: DataTableActionDropdownProps) =
|
||||
}
|
||||
|
||||
const recipient = row.Recipient.find((recipient) => recipient.email === session.user.email);
|
||||
|
||||
const isOwner = row.User.id === session.user.id;
|
||||
|
||||
// const isRecipient = !!recipient;
|
||||
const isDraft = row.status === DocumentStatus.DRAFT;
|
||||
// const isPending = row.status === DocumentStatus.PENDING;
|
||||
const isComplete = row.status === DocumentStatus.COMPLETED;
|
||||
// const isSigned = recipient?.signingStatus === SigningStatus.SIGNED;
|
||||
const isDocumentDeletable = isOwner;
|
||||
const isDocumentDeletable = isOwner && !recipient;
|
||||
|
||||
const onDownloadClick = async () => {
|
||||
try {
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { IDENTITY_PROVIDER_NAME } from '@documenso/lib/constants/auth';
|
||||
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
|
||||
|
||||
import { AuthenticatorApp } from '~/components/forms/2fa/authenticator-app';
|
||||
@ -17,28 +18,43 @@ export default async function SecuritySettingsPage() {
|
||||
|
||||
<hr className="my-4" />
|
||||
|
||||
<PasswordForm user={user} className="max-w-xl" />
|
||||
{user.identityProvider === 'DOCUMENSO' ? (
|
||||
<div>
|
||||
<PasswordForm user={user} className="max-w-xl" />
|
||||
|
||||
<hr className="mb-4 mt-8" />
|
||||
<hr className="mb-4 mt-8" />
|
||||
|
||||
<h4 className="text-lg font-medium">Two Factor Authentication</h4>
|
||||
<h4 className="text-lg font-medium">Two Factor Authentication</h4>
|
||||
|
||||
<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>
|
||||
<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>
|
||||
|
||||
<div className="mt-4 max-w-xl">
|
||||
<h5 className="font-medium">Two-factor methods</h5>
|
||||
<div className="mt-4 max-w-xl">
|
||||
<h5 className="font-medium">Two-factor methods</h5>
|
||||
|
||||
<AuthenticatorApp isTwoFactorEnabled={user.twoFactorEnabled} />
|
||||
</div>
|
||||
<AuthenticatorApp isTwoFactorEnabled={user.twoFactorEnabled} />
|
||||
</div>
|
||||
|
||||
{user.twoFactorEnabled && (
|
||||
<div className="mt-4 max-w-xl">
|
||||
<h5 className="font-medium">Recovery methods</h5>
|
||||
{user.twoFactorEnabled && (
|
||||
<div className="mt-4 max-w-xl">
|
||||
<h5 className="font-medium">Recovery methods</h5>
|
||||
|
||||
<RecoveryCodes isTwoFactorEnabled={user.twoFactorEnabled} />
|
||||
<RecoveryCodes isTwoFactorEnabled={user.twoFactorEnabled} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<h4 className="text-lg font-medium">
|
||||
Your account is managed by {IDENTITY_PROVIDER_NAME[user.identityProvider]}
|
||||
</h4>
|
||||
<p className="text-muted-foreground mt-2 text-sm">
|
||||
To update your password, enable two-factor authentication, and manage other security
|
||||
settings, please go to your {IDENTITY_PROVIDER_NAME[user.identityProvider]} account
|
||||
settings.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -0,0 +1,39 @@
|
||||
'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 Document
|
||||
</Button>
|
||||
|
||||
<DocumentDialog documentData={documentData} open={showDialog} onOpenChange={setShowDialog} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
17
apps/web/src/app/(signing)/sign/[token]/complete/layout.tsx
Normal file
17
apps/web/src/app/(signing)/sign/[token]/complete/layout.tsx
Normal file
@ -0,0 +1,17 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@ -17,6 +17,8 @@ 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;
|
||||
@ -117,12 +119,20 @@ 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} />
|
||||
|
||||
<DocumentDownloadButton
|
||||
className="flex-1"
|
||||
fileName={document.title}
|
||||
documentData={documentData}
|
||||
disabled={document.status !== DocumentStatus.COMPLETED}
|
||||
/>
|
||||
{document.status === DocumentStatus.COMPLETED ? (
|
||||
<DocumentDownloadButton
|
||||
className="flex-1"
|
||||
fileName={document.title}
|
||||
documentData={documentData}
|
||||
disabled={document.status !== DocumentStatus.COMPLETED}
|
||||
/>
|
||||
) : (
|
||||
<DocumentPreviewButton
|
||||
className="flex-1"
|
||||
title="Signatures will appear once the document has been completed"
|
||||
documentData={documentData}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isLoggedIn ? (
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import Link from 'next/link';
|
||||
|
||||
import { IS_GOOGLE_SSO_ENABLED } from '@documenso/lib/constants/auth';
|
||||
|
||||
import { SignInForm } from '~/components/forms/signin';
|
||||
|
||||
export default function SignInPage() {
|
||||
@ -11,7 +13,7 @@ export default function SignInPage() {
|
||||
Welcome back, we are lucky to have you.
|
||||
</p>
|
||||
|
||||
<SignInForm className="mt-4" />
|
||||
<SignInForm className="mt-4" isGoogleSSOEnabled={IS_GOOGLE_SSO_ENABLED} />
|
||||
|
||||
{process.env.NEXT_PUBLIC_DISABLE_SIGNUP !== 'true' && (
|
||||
<p className="text-muted-foreground mt-6 text-center text-sm">
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
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 default function SignUpPage() {
|
||||
@ -17,7 +19,7 @@ export default function SignUpPage() {
|
||||
signing is within your grasp.
|
||||
</p>
|
||||
|
||||
<SignUpForm className="mt-4" />
|
||||
<SignUpForm className="mt-4" isGoogleSSOEnabled={IS_GOOGLE_SSO_ENABLED} />
|
||||
|
||||
<p className="text-muted-foreground mt-6 text-center text-sm">
|
||||
Already have an account?{' '}
|
||||
|
||||
@ -252,7 +252,11 @@ const ThemeCommands = ({ setTheme }: { setTheme: (_theme: string) => void }) =>
|
||||
);
|
||||
|
||||
return THEMES.map((theme) => (
|
||||
<CommandItem key={theme.theme} onSelect={() => setTheme(theme.theme)}>
|
||||
<CommandItem
|
||||
key={theme.theme}
|
||||
onSelect={() => setTheme(theme.theme)}
|
||||
className="mx-2 first:mt-2 last:mb-2"
|
||||
>
|
||||
<theme.icon className="mr-2" />
|
||||
{theme.label}
|
||||
</CommandItem>
|
||||
|
||||
@ -141,7 +141,11 @@ export const ProfileDropdown = ({ user }: ProfileDropdownProps) => {
|
||||
</DropdownMenuSub>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem asChild>
|
||||
<Link href="https://github.com/documenso/documenso" className="cursor-pointer">
|
||||
<Link
|
||||
href="https://github.com/documenso/documenso"
|
||||
className="cursor-pointer"
|
||||
target="_blank"
|
||||
>
|
||||
<LuGithub className="mr-2 h-4 w-4" />
|
||||
Star on Github
|
||||
</Link>
|
||||
|
||||
@ -4,7 +4,7 @@ import { useEffect, useState } from 'react';
|
||||
|
||||
import { AlertTriangle } from 'lucide-react';
|
||||
|
||||
import { ONE_SECOND } from '@documenso/lib/constants/time';
|
||||
import { ONE_DAY, 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 < 24 * 60 * 60 * 1000) {
|
||||
if (Date.now() - lastShownTimestamp < ONE_DAY) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@ -112,7 +112,6 @@ 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"
|
||||
@ -122,7 +121,10 @@ export const ProfileForm = ({ className, user }: ProfileFormProps) => {
|
||||
<FormControl>
|
||||
<SignaturePad
|
||||
className="h-44 w-full"
|
||||
containerClassName="rounded-lg border bg-background"
|
||||
containerClassName={cn(
|
||||
'rounded-lg border bg-background',
|
||||
isSubmitting ? 'pointer-events-none opacity-50' : null,
|
||||
)}
|
||||
defaultValue={user.signature ?? undefined}
|
||||
onChange={(v) => onChange(v ?? '')}
|
||||
/>
|
||||
|
||||
@ -48,9 +48,10 @@ export type TSignInFormSchema = z.infer<typeof ZSignInFormSchema>;
|
||||
|
||||
export type SignInFormProps = {
|
||||
className?: string;
|
||||
isGoogleSSOEnabled?: boolean;
|
||||
};
|
||||
|
||||
export const SignInForm = ({ className }: SignInFormProps) => {
|
||||
export const SignInForm = ({ className, isGoogleSSOEnabled }: SignInFormProps) => {
|
||||
const { toast } = useToast();
|
||||
const [isTwoFactorAuthenticationDialogOpen, setIsTwoFactorAuthenticationDialogOpen] =
|
||||
useState(false);
|
||||
@ -203,24 +204,29 @@ export const SignInForm = ({ className }: SignInFormProps) => {
|
||||
{isSubmitting ? 'Signing in...' : 'Sign In'}
|
||||
</Button>
|
||||
|
||||
<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>
|
||||
{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>
|
||||
|
||||
<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}
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
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';
|
||||
@ -23,6 +24,8 @@ 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),
|
||||
@ -37,9 +40,10 @@ export type TSignUpFormSchema = z.infer<typeof ZSignUpFormSchema>;
|
||||
|
||||
export type SignUpFormProps = {
|
||||
className?: string;
|
||||
isGoogleSSOEnabled?: boolean;
|
||||
};
|
||||
|
||||
export const SignUpForm = ({ className }: SignUpFormProps) => {
|
||||
export const SignUpForm = ({ className, isGoogleSSOEnabled }: SignUpFormProps) => {
|
||||
const { toast } = useToast();
|
||||
const analytics = useAnalytics();
|
||||
|
||||
@ -64,7 +68,7 @@ export const SignUpForm = ({ className }: SignUpFormProps) => {
|
||||
await signIn('credentials', {
|
||||
email,
|
||||
password,
|
||||
callbackUrl: '/',
|
||||
callbackUrl: SIGN_UP_REDIRECT_PATH,
|
||||
});
|
||||
|
||||
analytics.capture('App: User Sign Up', {
|
||||
@ -89,6 +93,19 @@ export const SignUpForm = ({ className }: 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
|
||||
@ -166,6 +183,28 @@ export const SignUpForm = ({ className }: 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>
|
||||
);
|
||||
|
||||
@ -23,7 +23,8 @@ services:
|
||||
- database
|
||||
- inbucket
|
||||
environment:
|
||||
- DATABASE_URL=postgres://documenso:password@database:5432/documenso
|
||||
- NEXT_PRIVATE_DATABASE_URL=postgres://documenso:password@database:5432/documenso
|
||||
- NEXT_PRIVATE_DIRECT_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
|
||||
|
||||
@ -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://documenso.com">
|
||||
<Link className="text-[#7AC455]" href="https://documen.so/mail-footer">
|
||||
Documenso.
|
||||
</Link>
|
||||
</Text>
|
||||
|
||||
@ -1 +1,12 @@
|
||||
import { IdentityProvider } 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,
|
||||
);
|
||||
|
||||
@ -1 +1,23 @@
|
||||
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 (!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('*********************************************************************');
|
||||
}
|
||||
|
||||
@ -9,6 +9,7 @@ import type { GoogleProfile } from 'next-auth/providers/google';
|
||||
import GoogleProvider from 'next-auth/providers/google';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { IdentityProvider } from '@documenso/prisma/client';
|
||||
|
||||
import { isTwoFactorAuthenticationEnabled } from '../server-only/2fa/is-2fa-availble';
|
||||
import { validateTwoFactorAuthentication } from '../server-only/2fa/validate-2fa';
|
||||
@ -93,7 +94,7 @@ export const NEXT_AUTH_OPTIONS: AuthOptions = {
|
||||
}),
|
||||
],
|
||||
callbacks: {
|
||||
async jwt({ token, user }) {
|
||||
async jwt({ token, user, trigger, account }) {
|
||||
const merged = {
|
||||
...token,
|
||||
...user,
|
||||
@ -138,6 +139,22 @@ 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,
|
||||
|
||||
33
packages/lib/server-only/crypto/decrypt.ts
Normal file
33
packages/lib/server-only/crypto/decrypt.ts
Normal file
@ -0,0 +1,33 @@
|
||||
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;
|
||||
};
|
||||
42
packages/lib/server-only/crypto/encrypt.ts
Normal file
42
packages/lib/server-only/crypto/encrypt.ts
Normal file
@ -0,0 +1,42 @@
|
||||
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),
|
||||
});
|
||||
};
|
||||
@ -42,6 +42,11 @@ export const getStats = async ({ user }: GetStatsInput) => {
|
||||
_all: true,
|
||||
},
|
||||
where: {
|
||||
User: {
|
||||
email: {
|
||||
not: user.email,
|
||||
},
|
||||
},
|
||||
OR: [
|
||||
{
|
||||
status: ExtendedDocumentStatus.PENDING,
|
||||
|
||||
17
packages/trpc/server/crypto/router.ts
Normal file
17
packages/trpc/server/crypto/router.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { encryptSecondaryData } from '@documenso/lib/server-only/crypto/encrypt';
|
||||
|
||||
import { procedure, router } from '../trpc';
|
||||
import { ZEncryptSecondaryDataMutationSchema } from './schema';
|
||||
|
||||
export const cryptoRouter = router({
|
||||
encryptSecondaryData: procedure
|
||||
.input(ZEncryptSecondaryDataMutationSchema)
|
||||
.mutation(({ input }) => {
|
||||
try {
|
||||
return encryptSecondaryData(input);
|
||||
} catch {
|
||||
// Never leak errors for crypto.
|
||||
throw new Error('Failed to encrypt data');
|
||||
}
|
||||
}),
|
||||
});
|
||||
15
packages/trpc/server/crypto/schema.ts
Normal file
15
packages/trpc/server/crypto/schema.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const ZEncryptSecondaryDataMutationSchema = z.object({
|
||||
data: z.string(),
|
||||
expiresAt: z.number().optional(),
|
||||
});
|
||||
|
||||
export const ZDecryptDataMutationSchema = z.object({
|
||||
data: z.string(),
|
||||
});
|
||||
|
||||
export type TEncryptSecondaryDataMutationSchema = z.infer<
|
||||
typeof ZEncryptSecondaryDataMutationSchema
|
||||
>;
|
||||
export type TDecryptDataMutationSchema = z.infer<typeof ZDecryptDataMutationSchema>;
|
||||
@ -1,5 +1,6 @@
|
||||
import { adminRouter } from './admin-router/router';
|
||||
import { authRouter } from './auth-router/router';
|
||||
import { cryptoRouter } from './crypto/router';
|
||||
import { documentRouter } from './document-router/router';
|
||||
import { fieldRouter } from './field-router/router';
|
||||
import { profileRouter } from './profile-router/router';
|
||||
@ -12,6 +13,7 @@ import { twoFactorAuthenticationRouter } from './two-factor-authentication-route
|
||||
|
||||
export const appRouter = router({
|
||||
auth: authRouter,
|
||||
crypto: cryptoRouter,
|
||||
profile: profileRouter,
|
||||
document: documentRouter,
|
||||
field: fieldRouter,
|
||||
|
||||
1
packages/tsconfig/process-env.d.ts
vendored
1
packages/tsconfig/process-env.d.ts
vendored
@ -8,6 +8,7 @@ declare namespace NodeJS {
|
||||
|
||||
NEXT_PRIVATE_DATABASE_URL: string;
|
||||
NEXT_PRIVATE_ENCRYPTION_KEY: string;
|
||||
NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY: string;
|
||||
|
||||
NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID: string;
|
||||
|
||||
|
||||
@ -5,7 +5,7 @@ import { useState } from 'react';
|
||||
import * as DialogPrimitive from '@radix-ui/react-dialog';
|
||||
import { X } from 'lucide-react';
|
||||
|
||||
import { DocumentData } from '@documenso/prisma/client';
|
||||
import type { DocumentData } from '@documenso/prisma/client';
|
||||
|
||||
import { cn } from '../../lib/utils';
|
||||
import { Dialog, DialogOverlay, DialogPortal } from '../../primitives/dialog';
|
||||
|
||||
@ -35,7 +35,7 @@ const CommandDialog = ({ children, commandProps, ...props }: CommandDialogProps)
|
||||
<DialogContent className="overflow-hidden p-0 shadow-2xl">
|
||||
<Command
|
||||
{...commandProps}
|
||||
className="[&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-4 [&_[cmdk-item]_svg]:w-4"
|
||||
className="[&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group-heading]]:px-0 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-4 [&_[cmdk-item]_svg]:w-4"
|
||||
>
|
||||
{children}
|
||||
</Command>
|
||||
@ -92,7 +92,7 @@ const CommandGroup = React.forwardRef<
|
||||
<CommandPrimitive.Group
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium',
|
||||
'text-foreground [&_[cmdk-group-heading]]:text-muted-foreground mx-2 overflow-hidden border-b pb-2 last:border-0 [&_[cmdk-group-heading]]:mt-2 [&_[cmdk-group-heading]]:px-0 [&_[cmdk-group-heading]]:py-2 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-normal [&_[cmdk-group-heading]]:opacity-50 ',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@ -121,7 +121,7 @@ const CommandItem = React.forwardRef<
|
||||
<CommandPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'aria-selected:bg-accent aria-selected:text-accent-foreground relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||
'aria-selected:bg-accent aria-selected:text-accent-foreground relative -mx-2 -my-1 flex cursor-default select-none items-center rounded-lg px-2 py-1.5 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@ -34,6 +34,7 @@
|
||||
"globalEnv": [
|
||||
"APP_VERSION",
|
||||
"NEXT_PRIVATE_ENCRYPTION_KEY",
|
||||
"NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY",
|
||||
"NEXTAUTH_URL",
|
||||
"NEXTAUTH_SECRET",
|
||||
"NEXT_PUBLIC_PROJECT",
|
||||
|
||||
Reference in New Issue
Block a user