mirror of
https://github.com/documenso/documenso.git
synced 2025-11-13 00:03:33 +10:00
chore: merged main
This commit is contained in:
@ -4,8 +4,10 @@ NEXTAUTH_SECRET="secret"
|
|||||||
|
|
||||||
# [[CRYPTO]]
|
# [[CRYPTO]]
|
||||||
# Application Key for symmetric encryption and decryption
|
# Application Key for symmetric encryption and decryption
|
||||||
# This should be a random string of at least 32 characters
|
# REQUIRED: This should be a random string of at least 32 characters
|
||||||
NEXT_PRIVATE_ENCRYPTION_KEY="CAFEBABE"
|
NEXT_PRIVATE_ENCRYPTION_KEY=""
|
||||||
|
# REQUIRED: This should be a random string of at least 32 characters
|
||||||
|
NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY=""
|
||||||
|
|
||||||
# [[AUTH OPTIONAL]]
|
# [[AUTH OPTIONAL]]
|
||||||
NEXT_PRIVATE_GOOGLE_CLIENT_ID=""
|
NEXT_PRIVATE_GOOGLE_CLIENT_ID=""
|
||||||
|
|||||||
@ -47,6 +47,14 @@ export const TEAM_MEMBERS = [
|
|||||||
engagement: 'Full-Time',
|
engagement: 'Full-Time',
|
||||||
joinDate: 'October 9th, 2023',
|
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 = [
|
export const FUNDING_RAISED = [
|
||||||
|
|||||||
@ -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 { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
|
||||||
|
|
||||||
import { AuthenticatorApp } from '~/components/forms/2fa/authenticator-app';
|
import { AuthenticatorApp } from '~/components/forms/2fa/authenticator-app';
|
||||||
@ -17,28 +18,43 @@ export default async function SecuritySettingsPage() {
|
|||||||
|
|
||||||
<hr className="my-4" />
|
<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">
|
<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
|
Add and manage your two factor security settings to add an extra layer of security to
|
||||||
account!
|
your account!
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="mt-4 max-w-xl">
|
<div className="mt-4 max-w-xl">
|
||||||
<h5 className="font-medium">Two-factor methods</h5>
|
<h5 className="font-medium">Two-factor methods</h5>
|
||||||
|
|
||||||
<AuthenticatorApp isTwoFactorEnabled={user.twoFactorEnabled} />
|
<AuthenticatorApp isTwoFactorEnabled={user.twoFactorEnabled} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{user.twoFactorEnabled && (
|
{user.twoFactorEnabled && (
|
||||||
<div className="mt-4 max-w-xl">
|
<div className="mt-4 max-w-xl">
|
||||||
<h5 className="font-medium">Recovery methods</h5>
|
<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>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -3,6 +3,8 @@ import { redirect } from 'next/navigation';
|
|||||||
|
|
||||||
import { env } from 'next-runtime-env';
|
import { env } from 'next-runtime-env';
|
||||||
|
|
||||||
|
import { IS_GOOGLE_SSO_ENABLED } from '@documenso/lib/constants/auth';
|
||||||
|
|
||||||
import { SignUpForm } from '~/components/forms/signup';
|
import { SignUpForm } from '~/components/forms/signup';
|
||||||
|
|
||||||
export default function SignUpPage() {
|
export default function SignUpPage() {
|
||||||
@ -21,7 +23,7 @@ export default function SignUpPage() {
|
|||||||
signing is within your grasp.
|
signing is within your grasp.
|
||||||
</p>
|
</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">
|
<p className="text-muted-foreground mt-6 text-center text-sm">
|
||||||
Already have an account?{' '}
|
Already have an account?{' '}
|
||||||
|
|||||||
@ -252,7 +252,11 @@ const ThemeCommands = ({ setTheme }: { setTheme: (_theme: string) => void }) =>
|
|||||||
);
|
);
|
||||||
|
|
||||||
return THEMES.map((theme) => (
|
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.icon className="mr-2" />
|
||||||
{theme.label}
|
{theme.label}
|
||||||
</CommandItem>
|
</CommandItem>
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import { useEffect, useState } from 'react';
|
|||||||
|
|
||||||
import { AlertTriangle } from 'lucide-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 { trpc } from '@documenso/trpc/react';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
import {
|
import {
|
||||||
@ -65,7 +65,7 @@ export const VerifyEmailBanner = ({ email }: VerifyEmailBannerProps) => {
|
|||||||
if (emailVerificationDialogLastShown) {
|
if (emailVerificationDialogLastShown) {
|
||||||
const lastShownTimestamp = parseInt(emailVerificationDialogLastShown);
|
const lastShownTimestamp = parseInt(emailVerificationDialogLastShown);
|
||||||
|
|
||||||
if (Date.now() - lastShownTimestamp < 24 * 60 * 60 * 1000) {
|
if (Date.now() - lastShownTimestamp < ONE_DAY) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -112,7 +112,6 @@ export const ProfileForm = ({ className, user }: ProfileFormProps) => {
|
|||||||
</Label>
|
</Label>
|
||||||
<Input id="email" type="email" className="bg-muted mt-2" value={user.email} disabled />
|
<Input id="email" type="email" className="bg-muted mt-2" value={user.email} disabled />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="signature"
|
name="signature"
|
||||||
@ -122,7 +121,10 @@ export const ProfileForm = ({ className, user }: ProfileFormProps) => {
|
|||||||
<FormControl>
|
<FormControl>
|
||||||
<SignaturePad
|
<SignaturePad
|
||||||
className="h-44 w-full"
|
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}
|
defaultValue={user.signature ?? undefined}
|
||||||
onChange={(v) => onChange(v ?? '')}
|
onChange={(v) => onChange(v ?? '')}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { signIn } from 'next-auth/react';
|
import { signIn } from 'next-auth/react';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { FcGoogle } from 'react-icons/fc';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
|
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 { SignaturePad } from '@documenso/ui/primitives/signature-pad';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
|
const SIGN_UP_REDIRECT_PATH = '/documents';
|
||||||
|
|
||||||
export const ZSignUpFormSchema = z.object({
|
export const ZSignUpFormSchema = z.object({
|
||||||
name: z.string().trim().min(1, { message: 'Please enter a valid name.' }),
|
name: z.string().trim().min(1, { message: 'Please enter a valid name.' }),
|
||||||
email: z.string().email().min(1),
|
email: z.string().email().min(1),
|
||||||
@ -37,9 +40,10 @@ export type TSignUpFormSchema = z.infer<typeof ZSignUpFormSchema>;
|
|||||||
|
|
||||||
export type SignUpFormProps = {
|
export type SignUpFormProps = {
|
||||||
className?: string;
|
className?: string;
|
||||||
|
isGoogleSSOEnabled?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SignUpForm = ({ className }: SignUpFormProps) => {
|
export const SignUpForm = ({ className, isGoogleSSOEnabled }: SignUpFormProps) => {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const analytics = useAnalytics();
|
const analytics = useAnalytics();
|
||||||
|
|
||||||
@ -64,7 +68,7 @@ export const SignUpForm = ({ className }: SignUpFormProps) => {
|
|||||||
await signIn('credentials', {
|
await signIn('credentials', {
|
||||||
email,
|
email,
|
||||||
password,
|
password,
|
||||||
callbackUrl: '/',
|
callbackUrl: SIGN_UP_REDIRECT_PATH,
|
||||||
});
|
});
|
||||||
|
|
||||||
analytics.capture('App: User Sign Up', {
|
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 (
|
return (
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
@ -166,6 +183,28 @@ export const SignUpForm = ({ className }: SignUpFormProps) => {
|
|||||||
>
|
>
|
||||||
{isSubmitting ? 'Signing up...' : 'Sign Up'}
|
{isSubmitting ? 'Signing up...' : 'Sign Up'}
|
||||||
</Button>
|
</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>
|
||||||
</Form>
|
</Form>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -10,7 +10,7 @@ export const TemplateFooter = ({ isDocument = true }: TemplateFooterProps) => {
|
|||||||
{isDocument && (
|
{isDocument && (
|
||||||
<Text className="my-4 text-base text-slate-400">
|
<Text className="my-4 text-base text-slate-400">
|
||||||
This document was sent using{' '}
|
This document was sent using{' '}
|
||||||
<Link className="text-[#7AC455]" href="https://documenso.com">
|
<Link className="text-[#7AC455]" href="https://documen.so/mail-footer">
|
||||||
Documenso.
|
Documenso.
|
||||||
</Link>
|
</Link>
|
||||||
</Text>
|
</Text>
|
||||||
|
|||||||
@ -1,5 +1,12 @@
|
|||||||
|
import { IdentityProvider } from '@documenso/prisma/client';
|
||||||
|
|
||||||
export const SALT_ROUNDS = 12;
|
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(
|
export const IS_GOOGLE_SSO_ENABLED = Boolean(
|
||||||
process.env.NEXT_PRIVATE_GOOGLE_CLIENT_ID && process.env.NEXT_PRIVATE_GOOGLE_CLIENT_SECRET,
|
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_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('*********************************************************************');
|
||||||
|
}
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import GoogleProvider from 'next-auth/providers/google';
|
|||||||
import { env } from 'next-runtime-env';
|
import { env } from 'next-runtime-env';
|
||||||
|
|
||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
|
import { IdentityProvider } from '@documenso/prisma/client';
|
||||||
|
|
||||||
import { isTwoFactorAuthenticationEnabled } from '../server-only/2fa/is-2fa-availble';
|
import { isTwoFactorAuthenticationEnabled } from '../server-only/2fa/is-2fa-availble';
|
||||||
import { validateTwoFactorAuthentication } from '../server-only/2fa/validate-2fa';
|
import { validateTwoFactorAuthentication } from '../server-only/2fa/validate-2fa';
|
||||||
@ -96,7 +97,7 @@ export const NEXT_AUTH_OPTIONS: AuthOptions = {
|
|||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
callbacks: {
|
callbacks: {
|
||||||
async jwt({ token, user }) {
|
async jwt({ token, user, trigger, account }) {
|
||||||
const merged = {
|
const merged = {
|
||||||
...token,
|
...token,
|
||||||
...user,
|
...user,
|
||||||
@ -141,6 +142,22 @@ export const NEXT_AUTH_OPTIONS: AuthOptions = {
|
|||||||
merged.emailVerified = user.emailVerified?.toISOString() ?? null;
|
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 {
|
return {
|
||||||
id: merged.id,
|
id: merged.id,
|
||||||
name: merged.name,
|
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,
|
_all: true,
|
||||||
},
|
},
|
||||||
where: {
|
where: {
|
||||||
|
User: {
|
||||||
|
email: {
|
||||||
|
not: user.email,
|
||||||
|
},
|
||||||
|
},
|
||||||
OR: [
|
OR: [
|
||||||
{
|
{
|
||||||
status: ExtendedDocumentStatus.PENDING,
|
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 { adminRouter } from './admin-router/router';
|
||||||
import { authRouter } from './auth-router/router';
|
import { authRouter } from './auth-router/router';
|
||||||
|
import { cryptoRouter } from './crypto/router';
|
||||||
import { documentRouter } from './document-router/router';
|
import { documentRouter } from './document-router/router';
|
||||||
import { fieldRouter } from './field-router/router';
|
import { fieldRouter } from './field-router/router';
|
||||||
import { profileRouter } from './profile-router/router';
|
import { profileRouter } from './profile-router/router';
|
||||||
@ -12,6 +13,7 @@ import { twoFactorAuthenticationRouter } from './two-factor-authentication-route
|
|||||||
|
|
||||||
export const appRouter = router({
|
export const appRouter = router({
|
||||||
auth: authRouter,
|
auth: authRouter,
|
||||||
|
crypto: cryptoRouter,
|
||||||
profile: profileRouter,
|
profile: profileRouter,
|
||||||
document: documentRouter,
|
document: documentRouter,
|
||||||
field: fieldRouter,
|
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_DATABASE_URL: string;
|
||||||
NEXT_PRIVATE_ENCRYPTION_KEY: string;
|
NEXT_PRIVATE_ENCRYPTION_KEY: string;
|
||||||
|
NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY: string;
|
||||||
|
|
||||||
NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID: string;
|
NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID: string;
|
||||||
|
|
||||||
|
|||||||
@ -35,7 +35,7 @@ const CommandDialog = ({ children, commandProps, ...props }: CommandDialogProps)
|
|||||||
<DialogContent className="overflow-hidden p-0 shadow-2xl">
|
<DialogContent className="overflow-hidden p-0 shadow-2xl">
|
||||||
<Command
|
<Command
|
||||||
{...commandProps}
|
{...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}
|
{children}
|
||||||
</Command>
|
</Command>
|
||||||
@ -92,7 +92,7 @@ const CommandGroup = React.forwardRef<
|
|||||||
<CommandPrimitive.Group
|
<CommandPrimitive.Group
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
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,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@ -121,7 +121,7 @@ const CommandItem = React.forwardRef<
|
|||||||
<CommandPrimitive.Item
|
<CommandPrimitive.Item
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
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,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@ -34,6 +34,7 @@
|
|||||||
"globalEnv": [
|
"globalEnv": [
|
||||||
"APP_VERSION",
|
"APP_VERSION",
|
||||||
"NEXT_PRIVATE_ENCRYPTION_KEY",
|
"NEXT_PRIVATE_ENCRYPTION_KEY",
|
||||||
|
"NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY",
|
||||||
"NEXTAUTH_URL",
|
"NEXTAUTH_URL",
|
||||||
"NEXTAUTH_SECRET",
|
"NEXTAUTH_SECRET",
|
||||||
"NEXT_PUBLIC_PROJECT",
|
"NEXT_PUBLIC_PROJECT",
|
||||||
|
|||||||
Reference in New Issue
Block a user