diff --git a/.env.example b/.env.example
index d188894de..06498f2bc 100644
--- a/.env.example
+++ b/.env.example
@@ -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=""
diff --git a/apps/web/src/app/(unauthenticated)/signup/page.tsx b/apps/web/src/app/(unauthenticated)/signup/page.tsx
index 353716d9b..05b9caf21 100644
--- a/apps/web/src/app/(unauthenticated)/signup/page.tsx
+++ b/apps/web/src/app/(unauthenticated)/signup/page.tsx
@@ -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.
-
+
Already have an account?{' '}
diff --git a/apps/web/src/components/forms/signup.tsx b/apps/web/src/components/forms/signup.tsx
index b91b4a9fd..3f2723ec8 100644
--- a/apps/web/src/components/forms/signup.tsx
+++ b/apps/web/src/components/forms/signup.tsx
@@ -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;
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 (
);
diff --git a/packages/lib/constants/crypto.ts b/packages/lib/constants/crypto.ts
index d911cd6cf..40d3ef113 100644
--- a/packages/lib/constants/crypto.ts
+++ b/packages/lib/constants/crypto.ts
@@ -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('*********************************************************************');
+}
diff --git a/packages/lib/next-auth/auth-options.ts b/packages/lib/next-auth/auth-options.ts
index 3b9492807..50240174c 100644
--- a/packages/lib/next-auth/auth-options.ts
+++ b/packages/lib/next-auth/auth-options.ts
@@ -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,
diff --git a/packages/lib/server-only/crypto/decrypt.ts b/packages/lib/server-only/crypto/decrypt.ts
new file mode 100644
index 000000000..7b4db9894
--- /dev/null
+++ b/packages/lib/server-only/crypto/decrypt.ts
@@ -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;
+};
diff --git a/packages/lib/server-only/crypto/encrypt.ts b/packages/lib/server-only/crypto/encrypt.ts
new file mode 100644
index 000000000..83de19cc2
--- /dev/null
+++ b/packages/lib/server-only/crypto/encrypt.ts
@@ -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 = {
+ data,
+ expiresAt,
+ };
+
+ return symmetricEncrypt({
+ key: DOCUMENSO_ENCRYPTION_SECONDARY_KEY,
+ data: JSON.stringify(dataToEncrypt),
+ });
+};
diff --git a/packages/lib/server-only/document/get-stats.ts b/packages/lib/server-only/document/get-stats.ts
index a446b0007..044d9a2dc 100644
--- a/packages/lib/server-only/document/get-stats.ts
+++ b/packages/lib/server-only/document/get-stats.ts
@@ -42,6 +42,11 @@ export const getStats = async ({ user }: GetStatsInput) => {
_all: true,
},
where: {
+ User: {
+ email: {
+ not: user.email,
+ },
+ },
OR: [
{
status: ExtendedDocumentStatus.PENDING,
diff --git a/packages/trpc/server/crypto/router.ts b/packages/trpc/server/crypto/router.ts
new file mode 100644
index 000000000..db9616436
--- /dev/null
+++ b/packages/trpc/server/crypto/router.ts
@@ -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');
+ }
+ }),
+});
diff --git a/packages/trpc/server/crypto/schema.ts b/packages/trpc/server/crypto/schema.ts
new file mode 100644
index 000000000..ee4b49d53
--- /dev/null
+++ b/packages/trpc/server/crypto/schema.ts
@@ -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;
diff --git a/packages/trpc/server/router.ts b/packages/trpc/server/router.ts
index 77d18e06d..3ed2a0d05 100644
--- a/packages/trpc/server/router.ts
+++ b/packages/trpc/server/router.ts
@@ -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,
diff --git a/packages/tsconfig/process-env.d.ts b/packages/tsconfig/process-env.d.ts
index badc05931..d7fc44ef7 100644
--- a/packages/tsconfig/process-env.d.ts
+++ b/packages/tsconfig/process-env.d.ts
@@ -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;
diff --git a/turbo.json b/turbo.json
index 3a96c2a07..b78d7c9d0 100644
--- a/turbo.json
+++ b/turbo.json
@@ -34,6 +34,7 @@
"globalEnv": [
"APP_VERSION",
"NEXT_PRIVATE_ENCRYPTION_KEY",
+ "NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY",
"NEXTAUTH_URL",
"NEXTAUTH_SECRET",
"NEXT_PUBLIC_PROJECT",