fix: refactor trpc errors (#1511)

This commit is contained in:
David Nguyen
2024-12-06 16:01:24 +09:00
committed by GitHub
parent 3b6b96f551
commit 904948e2bc
27 changed files with 806 additions and 1518 deletions

View File

@@ -7,7 +7,6 @@ import { useLingui } from '@lingui/react';
import { signOut } from 'next-auth/react'; import { signOut } from 'next-auth/react';
import type { User } from '@documenso/prisma/client'; import type { User } from '@documenso/prisma/client';
import { TRPCClientError } from '@documenso/trpc/client';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert'; import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
@@ -52,30 +51,20 @@ export const DeleteAccountDialog = ({ className, user }: DeleteAccountDialogProp
return await signOut({ callbackUrl: '/' }); return await signOut({ callbackUrl: '/' });
} catch (err) { } catch (err) {
if (err instanceof TRPCClientError && err.data?.code === 'BAD_REQUEST') { toast({
toast({ title: _(msg`An unknown error occurred`),
title: _(msg`An error occurred`), variant: 'destructive',
description: err.message, description: _(
variant: 'destructive', msg`We encountered an unknown error while attempting to delete your account. Please try again later.`,
}); ),
} else { });
toast({
title: _(msg`An unknown error occurred`),
variant: 'destructive',
description:
err.message ??
_(
msg`We encountered an unknown error while attempting to delete your account. Please try again later.`,
),
});
}
} }
}; };
return ( return (
<div className={className}> <div className={className}>
<Alert <Alert
className="flex flex-col items-center justify-between gap-4 p-6 md:flex-row " className="flex flex-col items-center justify-between gap-4 p-6 md:flex-row"
variant="neutral" variant="neutral"
> >
<div> <div>

View File

@@ -9,7 +9,7 @@ import { useForm } from 'react-hook-form';
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';
import { TRPCClientError } from '@documenso/trpc/client'; import { AppError } from '@documenso/lib/errors/app-error';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import { ZPasswordSchema } from '@documenso/trpc/server/auth-router/schema'; import { ZPasswordSchema } from '@documenso/trpc/server/auth-router/schema';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
@@ -25,6 +25,8 @@ import { Input } from '@documenso/ui/primitives/input';
import { PasswordInput } from '@documenso/ui/primitives/password-input'; import { PasswordInput } from '@documenso/ui/primitives/password-input';
import { useToast } from '@documenso/ui/primitives/use-toast'; import { useToast } from '@documenso/ui/primitives/use-toast';
import { signupErrorMessages } from '~/components/forms/v2/signup';
export type ClaimAccountProps = { export type ClaimAccountProps = {
defaultName: string; defaultName: string;
defaultEmail: string; defaultEmail: string;
@@ -33,7 +35,10 @@ export type ClaimAccountProps = {
export const ZClaimAccountFormSchema = z export const ZClaimAccountFormSchema = z
.object({ .object({
name: z.string().trim().min(1, { message: 'Please enter a valid name.' }), name: z
.string()
.trim()
.min(1, { message: msg`Please enter a valid name.`.id }),
email: z.string().email().min(1), email: z.string().email().min(1),
password: ZPasswordSchema, password: ZPasswordSchema,
}) })
@@ -43,7 +48,7 @@ export const ZClaimAccountFormSchema = z
return !password.includes(name) && !password.includes(email.split('@')[0]); return !password.includes(name) && !password.includes(email.split('@')[0]);
}, },
{ {
message: 'Password should not be common or based on personal information', message: msg`Password should not be common or based on personal information`.id,
path: ['password'], path: ['password'],
}, },
); );
@@ -86,22 +91,16 @@ export const ClaimAccount = ({ defaultName, defaultEmail }: ClaimAccountProps) =
email, email,
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
}); });
} catch (error) { } catch (err) {
if (error instanceof TRPCClientError && error.data?.code === 'BAD_REQUEST') { const error = AppError.parseError(err);
toast({
title: _(msg`An error occurred`), const errorMessage = signupErrorMessages[error.code] ?? signupErrorMessages.INVALID_REQUEST;
description: error.message,
variant: 'destructive', toast({
}); title: _(msg`An error occurred`),
} else { description: _(errorMessage),
toast({ variant: 'destructive',
title: _(msg`An unknown error occurred`), });
description: _(
msg`We encountered an unknown error while attempting to sign you up. Please try again later.`,
),
variant: 'destructive',
});
}
} }
}; };

View File

@@ -13,10 +13,10 @@ import { match } from 'ts-pattern';
import { z } from 'zod'; import { z } from 'zod';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app'; import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { AppError } from '@documenso/lib/errors/app-error';
import { base64 } from '@documenso/lib/universal/base64'; import { base64 } from '@documenso/lib/universal/base64';
import { extractInitials } from '@documenso/lib/utils/recipient-formatter'; import { extractInitials } from '@documenso/lib/utils/recipient-formatter';
import type { Team, User } from '@documenso/prisma/client'; import type { Team, User } from '@documenso/prisma/client';
import { TRPCClientError } from '@documenso/trpc/client';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import { cn } from '@documenso/ui/lib/utils'; import { cn } from '@documenso/ui/lib/utils';
import { Avatar, AvatarFallback, AvatarImage } from '@documenso/ui/primitives/avatar'; import { Avatar, AvatarFallback, AvatarImage } from '@documenso/ui/primitives/avatar';
@@ -111,21 +111,18 @@ export const AvatarImageForm = ({ className, user, team }: AvatarImageFormProps)
router.refresh(); router.refresh();
} catch (err) { } catch (err) {
if (err instanceof TRPCClientError && err.data?.code === 'BAD_REQUEST') { const error = AppError.parseError(err);
toast({
title: _(msg`An error occurred`), const errorMessage = match(error.code).otherwise(
description: err.message, () =>
variant: 'destructive', msg`We encountered an unknown error while attempting to update your password. Please try again later.`,
}); );
} else {
toast({ toast({
title: _(msg`An unknown error occurred`), title: _(msg`An error occurred`),
description: _( description: _(errorMessage),
msg`We encountered an unknown error while attempting to update the avatar. Please try again later.`, variant: 'destructive',
), });
variant: 'destructive',
});
}
} }
}; };

View File

@@ -4,10 +4,11 @@ import { zodResolver } from '@hookform/resolvers/zod';
import { Trans, msg } from '@lingui/macro'; import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { match } from 'ts-pattern';
import { z } from 'zod'; import { z } from 'zod';
import { AppError } from '@documenso/lib/errors/app-error';
import type { User } from '@documenso/prisma/client'; import type { User } from '@documenso/prisma/client';
import { TRPCClientError } from '@documenso/trpc/client';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import { ZCurrentPasswordSchema, ZPasswordSchema } from '@documenso/trpc/server/auth-router/schema'; import { ZCurrentPasswordSchema, ZPasswordSchema } from '@documenso/trpc/server/auth-router/schema';
import { cn } from '@documenso/ui/lib/utils'; import { cn } from '@documenso/ui/lib/utils';
@@ -73,21 +74,25 @@ export const PasswordForm = ({ className }: PasswordFormProps) => {
duration: 5000, duration: 5000,
}); });
} catch (err) { } catch (err) {
if (err instanceof TRPCClientError && err.data?.code === 'BAD_REQUEST') { const error = AppError.parseError(err);
toast({
title: _(msg`An error occurred`), const errorMessage = match(error.code)
description: err.message, .with('NO_PASSWORD', () => msg`User has no password.`)
variant: 'destructive', .with('INCORRECT_PASSWORD', () => msg`Current password is incorrect.`)
}); .with(
} else { 'SAME_PASSWORD',
toast({ () => msg`Your new password cannot be the same as your old password.`,
title: _(msg`An unknown error occurred`), )
description: _( .otherwise(
() =>
msg`We encountered an unknown error while attempting to update your password. Please try again later.`, msg`We encountered an unknown error while attempting to update your password. Please try again later.`,
), );
variant: 'destructive',
}); toast({
} title: _(msg`An error occurred`),
description: _(errorMessage),
variant: 'destructive',
});
} }
}; };

View File

@@ -6,9 +6,10 @@ import { zodResolver } from '@hookform/resolvers/zod';
import { Trans, msg } from '@lingui/macro'; import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { match } from 'ts-pattern';
import { z } from 'zod'; import { z } from 'zod';
import { TRPCClientError } from '@documenso/trpc/client'; import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import { ZPasswordSchema } from '@documenso/trpc/server/auth-router/schema'; import { ZPasswordSchema } from '@documenso/trpc/server/auth-router/schema';
import { cn } from '@documenso/ui/lib/utils'; import { cn } from '@documenso/ui/lib/utils';
@@ -76,21 +77,25 @@ export const ResetPasswordForm = ({ className, token }: ResetPasswordFormProps)
router.push('/signin'); router.push('/signin');
} catch (err) { } catch (err) {
if (err instanceof TRPCClientError && err.data?.code === 'BAD_REQUEST') { const error = AppError.parseError(err);
toast({
title: _(msg`An error occurred`), const errorMessage = match(error.code)
description: err.message, .with(AppErrorCode.EXPIRED_CODE, () => msg`Token has expired. Please try again.`)
variant: 'destructive', .with('INVALID_TOKEN', () => msg`Invalid token provided. Please try again.`)
}); .with(
} else { 'SAME_PASSWORD',
toast({ () => msg`Your new password cannot be the same as your old password.`,
title: _(msg`An unknown error occurred`), )
description: _( .otherwise(
() =>
msg`We encountered an unknown error while attempting to reset your password. Please try again later.`, msg`We encountered an unknown error while attempting to reset your password. Please try again later.`,
), );
variant: 'destructive',
}); toast({
} title: _(msg`An error occurred`),
description: _(errorMessage),
variant: 'destructive',
});
} }
}; };

View File

@@ -11,7 +11,7 @@ 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';
import { TRPCClientError } from '@documenso/trpc/client'; import { AppError } from '@documenso/lib/errors/app-error';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import { ZPasswordSchema } from '@documenso/trpc/server/auth-router/schema'; import { ZPasswordSchema } from '@documenso/trpc/server/auth-router/schema';
import { cn } from '@documenso/ui/lib/utils'; import { cn } from '@documenso/ui/lib/utils';
@@ -29,6 +29,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';
import { signupErrorMessages } from './v2/signup';
const SIGN_UP_REDIRECT_PATH = '/documents'; const SIGN_UP_REDIRECT_PATH = '/documents';
export const ZSignUpFormSchema = z export const ZSignUpFormSchema = z
@@ -102,21 +104,15 @@ export const SignUpForm = ({
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
}); });
} catch (err) { } catch (err) {
if (err instanceof TRPCClientError && err.data?.code === 'BAD_REQUEST') { const error = AppError.parseError(err);
toast({
title: _(msg`An error occurred`), const errorMessage = signupErrorMessages[error.code] ?? signupErrorMessages.INVALID_REQUEST;
description: err.message,
variant: 'destructive', toast({
}); title: _(msg`An error occurred`),
} else { description: _(errorMessage),
toast({ variant: 'destructive',
title: _(msg`An unknown error occurred`), });
description: _(
msg`We encountered an unknown error while attempting to sign you up. Please try again later.`,
),
variant: 'destructive',
});
}
} }
}; };

View File

@@ -7,6 +7,7 @@ import Link from 'next/link';
import { useRouter, useSearchParams } from 'next/navigation'; import { useRouter, useSearchParams } from 'next/navigation';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import type { MessageDescriptor } from '@lingui/core';
import { Trans, msg } from '@lingui/macro'; import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import { AnimatePresence, motion } from 'framer-motion'; import { AnimatePresence, motion } from 'framer-motion';
@@ -20,7 +21,6 @@ import communityCardsImage from '@documenso/assets/images/community-cards.png';
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics'; import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app'; import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { TRPCClientError } from '@documenso/trpc/client';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import { ZPasswordSchema } from '@documenso/trpc/server/auth-router/schema'; import { ZPasswordSchema } from '@documenso/trpc/server/auth-router/schema';
import { cn } from '@documenso/ui/lib/utils'; import { cn } from '@documenso/ui/lib/utils';
@@ -47,17 +47,20 @@ type SignUpStep = 'BASIC_DETAILS' | 'CLAIM_USERNAME';
export const ZSignUpFormV2Schema = z export const ZSignUpFormV2Schema = z
.object({ .object({
name: z.string().trim().min(1, { message: 'Please enter a valid name.' }), name: z
.string()
.trim()
.min(1, { message: msg`Please enter a valid name.`.id }),
email: z.string().email().min(1), email: z.string().email().min(1),
password: ZPasswordSchema, password: ZPasswordSchema,
signature: z.string().min(1, { message: 'We need your signature to sign documents' }), signature: z.string().min(1, { message: msg`We need your signature to sign documents`.id }),
url: z url: z
.string() .string()
.trim() .trim()
.toLowerCase() .toLowerCase()
.min(1, { message: 'We need a username to create your profile' }) .min(1, { message: msg`We need a username to create your profile`.id })
.regex(/^[a-z0-9-]+$/, { .regex(/^[a-z0-9-]+$/, {
message: 'Username can only container alphanumeric characters and dashes.', message: msg`Username can only container alphanumeric characters and dashes.`.id,
}), }),
}) })
.refine( .refine(
@@ -66,10 +69,18 @@ export const ZSignUpFormV2Schema = z
return !password.includes(name) && !password.includes(email.split('@')[0]); return !password.includes(name) && !password.includes(email.split('@')[0]);
}, },
{ {
message: 'Password should not be common or based on personal information', message: msg`Password should not be common or based on personal information`.id,
}, },
); );
export const signupErrorMessages: Record<string, MessageDescriptor> = {
SIGNUP_DISABLED: msg`Signups are disabled.`,
[AppErrorCode.ALREADY_EXISTS]: msg`User with this email already exists. Please use a different email address.`,
[AppErrorCode.INVALID_REQUEST]: msg`We were unable to create your account. Please review the information you provided and try again.`,
[AppErrorCode.PROFILE_URL_TAKEN]: msg`This username has already been taken`,
[AppErrorCode.PREMIUM_PROFILE_URL]: msg`Only subscribers can have a username shorter than 6 characters`,
};
export type TSignUpFormV2Schema = z.infer<typeof ZSignUpFormV2Schema>; export type TSignUpFormV2Schema = z.infer<typeof ZSignUpFormV2Schema>;
export type SignUpFormV2Props = { export type SignUpFormV2Props = {
@@ -139,28 +150,20 @@ export const SignUpFormV2 = ({
} catch (err) { } catch (err) {
const error = AppError.parseError(err); const error = AppError.parseError(err);
if (error.code === AppErrorCode.PROFILE_URL_TAKEN) { const errorMessage = signupErrorMessages[error.code] ?? signupErrorMessages.INVALID_REQUEST;
if (
error.code === AppErrorCode.PROFILE_URL_TAKEN ||
error.code === AppErrorCode.PREMIUM_PROFILE_URL
) {
form.setError('url', { form.setError('url', {
type: 'manual', type: 'manual',
message: _(msg`This username has already been taken`), message: _(errorMessage),
});
} else if (error.code === AppErrorCode.PREMIUM_PROFILE_URL) {
form.setError('url', {
type: 'manual',
message: error.message,
});
} else if (err instanceof TRPCClientError && err.data?.code === 'BAD_REQUEST') {
toast({
title: _(msg`An error occurred`),
description: err.message,
variant: 'destructive',
}); });
} else { } else {
toast({ toast({
title: _(msg`An unknown error occurred`), title: _(msg`An error occurred`),
description: _( description: _(errorMessage),
msg`We encountered an unknown error while attempting to sign you up. Please try again later.`,
),
variant: 'destructive', variant: 'destructive',
}); });
} }

View File

@@ -21,8 +21,7 @@ export default trpcNext.createNextApiHandler({
onError(opts) { onError(opts) {
const { error, path } = opts; const { error, path } = opts;
// Currently trialing changes with template and team router only. if (!path) {
if (!path || (!path.startsWith('template') && !path.startsWith('team'))) {
return; return;
} }

View File

@@ -2,7 +2,6 @@ import { z } from 'zod';
import { DOCUMENSO_ENCRYPTION_SECONDARY_KEY } from '@documenso/lib/constants/crypto'; import { DOCUMENSO_ENCRYPTION_SECONDARY_KEY } from '@documenso/lib/constants/crypto';
import { symmetricEncrypt } from '@documenso/lib/universal/crypto'; import { symmetricEncrypt } from '@documenso/lib/universal/crypto';
import type { TEncryptSecondaryDataMutationSchema } from '@documenso/trpc/server/crypto/schema';
export const ZEncryptedDataSchema = z.object({ export const ZEncryptedDataSchema = z.object({
data: z.string(), data: z.string(),
@@ -25,7 +24,7 @@ export type EncryptDataOptions = {
* *
* @returns The encrypted data. * @returns The encrypted data.
*/ */
export const encryptSecondaryData = ({ data, expiresAt }: TEncryptSecondaryDataMutationSchema) => { export const encryptSecondaryData = ({ data, expiresAt }: EncryptDataOptions) => {
if (!DOCUMENSO_ENCRYPTION_SECONDARY_KEY) { if (!DOCUMENSO_ENCRYPTION_SECONDARY_KEY) {
throw new Error('Missing encryption key'); throw new Error('Missing encryption key');
} }

View File

@@ -2,6 +2,7 @@ import sharp from 'sharp';
import { prisma } from '@documenso/prisma'; import { prisma } from '@documenso/prisma';
import { AppError, AppErrorCode } from '../../errors/app-error';
import type { RequestMetadata } from '../../universal/extract-request-metadata'; import type { RequestMetadata } from '../../universal/extract-request-metadata';
export type SetAvatarImageOptions = { export type SetAvatarImageOptions = {
@@ -29,7 +30,9 @@ export const setAvatarImage = async ({
}); });
if (!user) { if (!user) {
throw new Error('User not found'); throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'User not found',
});
} }
oldAvatarImageId = user.avatarImageId; oldAvatarImageId = user.avatarImageId;
@@ -47,7 +50,9 @@ export const setAvatarImage = async ({
}); });
if (!team) { if (!team) {
throw new Error('Team not found'); throw new AppError('TEAM_NOT_FOUND', {
statusCode: 404,
});
} }
oldAvatarImageId = team.avatarImageId; oldAvatarImageId = team.avatarImageId;

View File

@@ -8,6 +8,7 @@ import { IdentityProvider, TeamMemberInviteStatus } from '@documenso/prisma/clie
import { IS_BILLING_ENABLED } from '../../constants/app'; import { IS_BILLING_ENABLED } from '../../constants/app';
import { SALT_ROUNDS } from '../../constants/auth'; import { SALT_ROUNDS } from '../../constants/auth';
import { AppError, AppErrorCode } from '../../errors/app-error'; import { AppError, AppErrorCode } from '../../errors/app-error';
import { buildLogger } from '../../utils/logger';
export interface CreateUserOptions { export interface CreateUserOptions {
name: string; name: string;
@@ -27,7 +28,7 @@ export const createUser = async ({ name, email, password, signature, url }: Crea
}); });
if (userExists) { if (userExists) {
throw new Error('User already exists'); throw new AppError(AppErrorCode.ALREADY_EXISTS);
} }
if (url) { if (url) {
@@ -134,6 +135,18 @@ export const createUser = async ({ name, email, password, signature, url }: Crea
return await getStripeCustomerByUser(user).then((session) => session.user); return await getStripeCustomerByUser(user).then((session) => session.user);
} catch (err) { } catch (err) {
console.error(err); console.error(err);
const error = AppError.parseError(err);
const logger = buildLogger();
logger.error(error, {
method: 'createUser',
context: {
appError: AppError.toJSON(error),
userId: user.id,
},
});
} }
} }

View File

@@ -4,6 +4,7 @@ import { prisma } from '@documenso/prisma';
import { UserSecurityAuditLogType } from '@documenso/prisma/client'; import { UserSecurityAuditLogType } from '@documenso/prisma/client';
import { SALT_ROUNDS } from '../../constants/auth'; import { SALT_ROUNDS } from '../../constants/auth';
import { AppError, AppErrorCode } from '../../errors/app-error';
import type { RequestMetadata } from '../../universal/extract-request-metadata'; import type { RequestMetadata } from '../../universal/extract-request-metadata';
import { sendResetPassword } from '../auth/send-reset-password'; import { sendResetPassword } from '../auth/send-reset-password';
@@ -15,7 +16,7 @@ export type ResetPasswordOptions = {
export const resetPassword = async ({ token, password, requestMetadata }: ResetPasswordOptions) => { export const resetPassword = async ({ token, password, requestMetadata }: ResetPasswordOptions) => {
if (!token) { if (!token) {
throw new Error('Invalid token provided. Please try again.'); throw new AppError('INVALID_TOKEN');
} }
const foundToken = await prisma.passwordResetToken.findFirst({ const foundToken = await prisma.passwordResetToken.findFirst({
@@ -28,19 +29,19 @@ export const resetPassword = async ({ token, password, requestMetadata }: ResetP
}); });
if (!foundToken) { if (!foundToken) {
throw new Error('Invalid token provided. Please try again.'); throw new AppError('INVALID_TOKEN');
} }
const now = new Date(); const now = new Date();
if (now > foundToken.expiry) { if (now > foundToken.expiry) {
throw new Error('Token has expired. Please try again.'); throw new AppError(AppErrorCode.EXPIRED_CODE);
} }
const isSamePassword = await compare(password, foundToken.User.password || ''); const isSamePassword = await compare(password, foundToken.User.password || '');
if (isSamePassword) { if (isSamePassword) {
throw new Error('Your new password cannot be the same as your old password.'); throw new AppError('SAME_PASSWORD');
} }
const hashedPassword = await hash(password, SALT_ROUNDS); const hashedPassword = await hash(password, SALT_ROUNDS);

View File

@@ -5,6 +5,8 @@ import type { RequestMetadata } from '@documenso/lib/universal/extract-request-m
import { prisma } from '@documenso/prisma'; import { prisma } from '@documenso/prisma';
import { UserSecurityAuditLogType } from '@documenso/prisma/client'; import { UserSecurityAuditLogType } from '@documenso/prisma/client';
import { AppError } from '../../errors/app-error';
export type UpdatePasswordOptions = { export type UpdatePasswordOptions = {
userId: number; userId: number;
password: string; password: string;
@@ -26,18 +28,18 @@ export const updatePassword = async ({
}); });
if (!user.password) { if (!user.password) {
throw new Error('User has no password'); throw new AppError('NO_PASSWORD');
} }
const isCurrentPasswordValid = await compare(currentPassword, user.password); const isCurrentPasswordValid = await compare(currentPassword, user.password);
if (!isCurrentPasswordValid) { if (!isCurrentPasswordValid) {
throw new Error('Current password is incorrect.'); throw new AppError('INCORRECT_PASSWORD');
} }
// Compare the new password with the old password // Compare the new password with the old password
const isSamePassword = await compare(password, user.password); const isSamePassword = await compare(password, user.password);
if (isSamePassword) { if (isSamePassword) {
throw new Error('Your new password cannot be the same as your old password.'); throw new AppError('SAME_PASSWORD');
} }
const hashedNewPassword = await hash(password, SALT_ROUNDS); const hashedNewPassword = await hash(password, SALT_ROUNDS);

View File

@@ -1,5 +1,3 @@
import { TRPCError } from '@trpc/server';
import { findDocuments } from '@documenso/lib/server-only/admin/get-all-documents'; import { findDocuments } from '@documenso/lib/server-only/admin/get-all-documents';
import { getEntireDocument } from '@documenso/lib/server-only/admin/get-entire-document'; import { getEntireDocument } from '@documenso/lib/server-only/admin/get-entire-document';
import { updateRecipient } from '@documenso/lib/server-only/admin/update-recipient'; import { updateRecipient } from '@documenso/lib/server-only/admin/update-recipient';
@@ -28,16 +26,7 @@ export const adminRouter = router({
findDocuments: adminProcedure.input(ZAdminFindDocumentsQuerySchema).query(async ({ input }) => { findDocuments: adminProcedure.input(ZAdminFindDocumentsQuerySchema).query(async ({ input }) => {
const { term, page, perPage } = input; const { term, page, perPage } = input;
try { return await findDocuments({ term, page, perPage });
return await findDocuments({ term, page, perPage });
} catch (err) {
console.error(err);
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'We were unable to retrieve the documents. Please try again.',
});
}
}), }),
updateUser: adminProcedure updateUser: adminProcedure
@@ -45,16 +34,7 @@ export const adminRouter = router({
.mutation(async ({ input }) => { .mutation(async ({ input }) => {
const { id, name, email, roles } = input; const { id, name, email, roles } = input;
try { return await updateUser({ id, name, email, roles });
return await updateUser({ id, name, email, roles });
} catch (err) {
console.error(err);
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'We were unable to retrieve the specified account. Please try again.',
});
}
}), }),
updateRecipient: adminProcedure updateRecipient: adminProcedure
@@ -62,38 +42,20 @@ export const adminRouter = router({
.mutation(async ({ input }) => { .mutation(async ({ input }) => {
const { id, name, email } = input; const { id, name, email } = input;
try { return await updateRecipient({ id, name, email });
return await updateRecipient({ id, name, email });
} catch (err) {
console.error(err);
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'We were unable to update the recipient provided.',
});
}
}), }),
updateSiteSetting: adminProcedure updateSiteSetting: adminProcedure
.input(ZAdminUpdateSiteSettingMutationSchema) .input(ZAdminUpdateSiteSettingMutationSchema)
.mutation(async ({ ctx, input }) => { .mutation(async ({ ctx, input }) => {
try { const { id, enabled, data } = input;
const { id, enabled, data } = input;
return await upsertSiteSetting({ return await upsertSiteSetting({
id, id,
enabled, enabled,
data, data,
userId: ctx.user.id, userId: ctx.user.id,
}); });
} catch (err) {
console.error(err);
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'We were unable to update the site setting provided.',
});
}
}), }),
resealDocument: adminProcedure resealDocument: adminProcedure
@@ -101,61 +63,34 @@ export const adminRouter = router({
.mutation(async ({ input }) => { .mutation(async ({ input }) => {
const { id } = input; const { id } = input;
try { const document = await getEntireDocument({ id });
const document = await getEntireDocument({ id });
const isResealing = document.status === DocumentStatus.COMPLETED; const isResealing = document.status === DocumentStatus.COMPLETED;
return await sealDocument({ documentId: id, isResealing }); return await sealDocument({ documentId: id, isResealing });
} catch (err) {
console.error('resealDocument error', err);
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'We were unable to reseal the document provided.',
});
}
}), }),
deleteUser: adminProcedure.input(ZAdminDeleteUserMutationSchema).mutation(async ({ input }) => { deleteUser: adminProcedure.input(ZAdminDeleteUserMutationSchema).mutation(async ({ input }) => {
const { id, email } = input; const { id, email } = input;
try { const user = await getUserById({ id });
const user = await getUserById({ id });
if (user.email !== email) { if (user.email !== email) {
throw new Error('Email does not match'); throw new Error('Email does not match');
}
return await deleteUser({ id });
} catch (err) {
console.error(err);
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'We were unable to delete the specified account. Please try again.',
});
} }
return await deleteUser({ id });
}), }),
deleteDocument: adminProcedure deleteDocument: adminProcedure
.input(ZAdminDeleteDocumentMutationSchema) .input(ZAdminDeleteDocumentMutationSchema)
.mutation(async ({ ctx, input }) => { .mutation(async ({ ctx, input }) => {
const { id, reason } = input; const { id, reason } = input;
try { await sendDeleteEmail({ documentId: id, reason });
await sendDeleteEmail({ documentId: id, reason });
return await superDeleteDocument({ return await superDeleteDocument({
id, id,
requestMetadata: extractNextApiRequestMetadata(ctx.req), requestMetadata: extractNextApiRequestMetadata(ctx.req),
}); });
} catch (err) {
console.error(err);
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'We were unable to delete the specified document. Please try again.',
});
}
}), }),
}); });

View File

@@ -1,5 +1,3 @@
import { TRPCError } from '@trpc/server';
import { createApiToken } from '@documenso/lib/server-only/public-api/create-api-token'; import { createApiToken } from '@documenso/lib/server-only/public-api/create-api-token';
import { deleteTokenById } from '@documenso/lib/server-only/public-api/delete-api-token-by-id'; import { deleteTokenById } from '@documenso/lib/server-only/public-api/delete-api-token-by-id';
import { getUserTokens } from '@documenso/lib/server-only/public-api/get-all-user-tokens'; import { getUserTokens } from '@documenso/lib/server-only/public-api/get-all-user-tokens';
@@ -14,78 +12,42 @@ import {
export const apiTokenRouter = router({ export const apiTokenRouter = router({
getTokens: authenticatedProcedure.query(async ({ ctx }) => { getTokens: authenticatedProcedure.query(async ({ ctx }) => {
try { return await getUserTokens({ userId: ctx.user.id });
return await getUserTokens({ userId: ctx.user.id });
} catch (err) {
console.error(err);
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'We were unable to find your API tokens. Please try again.',
});
}
}), }),
getTokenById: authenticatedProcedure getTokenById: authenticatedProcedure
.input(ZGetApiTokenByIdQuerySchema) .input(ZGetApiTokenByIdQuerySchema)
.query(async ({ input, ctx }) => { .query(async ({ input, ctx }) => {
try { const { id } = input;
const { id } = input;
return await getApiTokenById({ return await getApiTokenById({
id, id,
userId: ctx.user.id, userId: ctx.user.id,
}); });
} catch (err) {
console.error(err);
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'We were unable to find this API token. Please try again.',
});
}
}), }),
createToken: authenticatedProcedure createToken: authenticatedProcedure
.input(ZCreateTokenMutationSchema) .input(ZCreateTokenMutationSchema)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
try { const { tokenName, teamId, expirationDate } = input;
const { tokenName, teamId, expirationDate } = input;
return await createApiToken({ return await createApiToken({
userId: ctx.user.id, userId: ctx.user.id,
teamId, teamId,
tokenName, tokenName,
expiresIn: expirationDate, expiresIn: expirationDate,
}); });
} catch (err) {
console.error(err);
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'We were unable to create an API token. Please try again.',
});
}
}), }),
deleteTokenById: authenticatedProcedure deleteTokenById: authenticatedProcedure
.input(ZDeleteTokenByIdMutationSchema) .input(ZDeleteTokenByIdMutationSchema)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
try { const { id, teamId } = input;
const { id, teamId } = input;
return await deleteTokenById({ return await deleteTokenById({
id, id,
teamId, teamId,
userId: ctx.user.id, userId: ctx.user.id,
}); });
} catch (err) {
console.error(err);
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'We were unable to delete this API Token. Please try again.',
});
}
}), }),
}); });

View File

@@ -33,53 +33,30 @@ const NEXT_PUBLIC_DISABLE_SIGNUP = () => env('NEXT_PUBLIC_DISABLE_SIGNUP');
export const authRouter = router({ export const authRouter = router({
signup: procedure.input(ZSignUpMutationSchema).mutation(async ({ input }) => { signup: procedure.input(ZSignUpMutationSchema).mutation(async ({ input }) => {
try { if (NEXT_PUBLIC_DISABLE_SIGNUP() === 'true') {
if (NEXT_PUBLIC_DISABLE_SIGNUP() === 'true') { throw new AppError('SIGNUP_DISABLED', {
throw new TRPCError({ message: 'Signups are disabled.',
code: 'BAD_REQUEST',
message: 'Signups are disabled.',
});
}
const { name, email, password, signature, url } = input;
if (IS_BILLING_ENABLED() && url && url.length < 6) {
throw new AppError(AppErrorCode.PREMIUM_PROFILE_URL, {
message: 'Only subscribers can have a username shorter than 6 characters',
});
}
const user = await createUser({ name, email, password, signature, url });
await jobsClient.triggerJob({
name: 'send.signup.confirmation.email',
payload: {
email: user.email,
},
});
return user;
} catch (err) {
console.error(err);
const error = AppError.parseError(err);
if (error.code !== AppErrorCode.UNKNOWN_ERROR) {
throw error;
}
let message =
'We were unable to create your account. Please review the information you provided and try again.';
if (err instanceof Error && err.message === 'User already exists') {
message = 'User with this email already exists. Please use a different email address.';
}
throw new TRPCError({
code: 'BAD_REQUEST',
message,
}); });
} }
const { name, email, password, signature, url } = input;
if (IS_BILLING_ENABLED() && url && url.length < 6) {
throw new AppError(AppErrorCode.PREMIUM_PROFILE_URL, {
message: 'Only subscribers can have a username shorter than 6 characters',
});
}
const user = await createUser({ name, email, password, signature, url });
await jobsClient.triggerJob({
name: 'send.signup.confirmation.email',
payload: {
email: user.email,
},
});
return user;
}), }),
verifyPassword: authenticatedProcedure verifyPassword: authenticatedProcedure
@@ -104,56 +81,30 @@ export const authRouter = router({
createPasskey: authenticatedProcedure createPasskey: authenticatedProcedure
.input(ZCreatePasskeyMutationSchema) .input(ZCreatePasskeyMutationSchema)
.mutation(async ({ ctx, input }) => { .mutation(async ({ ctx, input }) => {
try { // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions const verificationResponse = input.verificationResponse as RegistrationResponseJSON;
const verificationResponse = input.verificationResponse as RegistrationResponseJSON;
return await createPasskey({ return await createPasskey({
userId: ctx.user.id, userId: ctx.user.id,
verificationResponse, verificationResponse,
passkeyName: input.passkeyName, passkeyName: input.passkeyName,
requestMetadata: extractNextApiRequestMetadata(ctx.req), requestMetadata: extractNextApiRequestMetadata(ctx.req),
}); });
} catch (err) {
console.error(err);
throw err;
}
}), }),
createPasskeyAuthenticationOptions: authenticatedProcedure createPasskeyAuthenticationOptions: authenticatedProcedure
.input(ZCreatePasskeyAuthenticationOptionsMutationSchema) .input(ZCreatePasskeyAuthenticationOptionsMutationSchema)
.mutation(async ({ ctx, input }) => { .mutation(async ({ ctx, input }) => {
try { return await createPasskeyAuthenticationOptions({
return await createPasskeyAuthenticationOptions({ userId: ctx.user.id,
userId: ctx.user.id, preferredPasskeyId: input?.preferredPasskeyId,
preferredPasskeyId: input?.preferredPasskeyId, });
});
} catch (err) {
console.error(err);
throw new TRPCError({
code: 'BAD_REQUEST',
message:
'We were unable to create the authentication options for the passkey. Please try again later.',
});
}
}), }),
createPasskeyRegistrationOptions: authenticatedProcedure.mutation(async ({ ctx }) => { createPasskeyRegistrationOptions: authenticatedProcedure.mutation(async ({ ctx }) => {
try { return await createPasskeyRegistrationOptions({
return await createPasskeyRegistrationOptions({ userId: ctx.user.id,
userId: ctx.user.id, });
});
} catch (err) {
console.error(err);
throw new TRPCError({
code: 'BAD_REQUEST',
message:
'We were unable to create the registration options for the passkey. Please try again later.',
});
}
}), }),
createPasskeySigninOptions: procedure.mutation(async ({ ctx }) => { createPasskeySigninOptions: procedure.mutation(async ({ ctx }) => {
@@ -168,80 +119,44 @@ export const authRouter = router({
const [sessionId] = decodeURI(sessionIdToken).split('|'); const [sessionId] = decodeURI(sessionIdToken).split('|');
try { return await createPasskeySigninOptions({ sessionId });
return await createPasskeySigninOptions({ sessionId });
} catch (err) {
console.error(err);
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'We were unable to create the options for passkey signin. Please try again later.',
});
}
}), }),
deletePasskey: authenticatedProcedure deletePasskey: authenticatedProcedure
.input(ZDeletePasskeyMutationSchema) .input(ZDeletePasskeyMutationSchema)
.mutation(async ({ ctx, input }) => { .mutation(async ({ ctx, input }) => {
try { const { passkeyId } = input;
const { passkeyId } = input;
await deletePasskey({ await deletePasskey({
userId: ctx.user.id, userId: ctx.user.id,
passkeyId, passkeyId,
requestMetadata: extractNextApiRequestMetadata(ctx.req), requestMetadata: extractNextApiRequestMetadata(ctx.req),
}); });
} catch (err) {
console.error(err);
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'We were unable to delete this passkey. Please try again later.',
});
}
}), }),
findPasskeys: authenticatedProcedure findPasskeys: authenticatedProcedure
.input(ZFindPasskeysQuerySchema) .input(ZFindPasskeysQuerySchema)
.query(async ({ input, ctx }) => { .query(async ({ input, ctx }) => {
try { const { page, perPage, orderBy } = input;
const { page, perPage, orderBy } = input;
return await findPasskeys({ return await findPasskeys({
page, page,
perPage, perPage,
orderBy, orderBy,
userId: ctx.user.id, userId: ctx.user.id,
}); });
} catch (err) {
console.error(err);
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'We were unable to find passkeys. Please try again later.',
});
}
}), }),
updatePasskey: authenticatedProcedure updatePasskey: authenticatedProcedure
.input(ZUpdatePasskeyMutationSchema) .input(ZUpdatePasskeyMutationSchema)
.mutation(async ({ ctx, input }) => { .mutation(async ({ ctx, input }) => {
try { const { passkeyId, name } = input;
const { passkeyId, name } = input;
await updatePasskey({ await updatePasskey({
userId: ctx.user.id, userId: ctx.user.id,
passkeyId, passkeyId,
name, name,
requestMetadata: extractNextApiRequestMetadata(ctx.req), requestMetadata: extractNextApiRequestMetadata(ctx.req),
}); });
} catch (err) {
console.error(err);
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'We were unable to update this passkey. Please try again later.',
});
}
}), }),
}); });

View File

@@ -1,8 +0,0 @@
import { procedure, router } from '../trpc';
import { ZEncryptSecondaryDataMutationSchema } from './schema';
export const cryptoRouter = router({
encryptSecondaryData: procedure.input(ZEncryptSecondaryDataMutationSchema).mutation(() => {
throw new Error('Public usage of encryptSecondaryData is no longer permitted');
}),
});

View File

@@ -1,15 +0,0 @@
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>;

View File

@@ -51,145 +51,82 @@ export const documentRouter = router({
getDocumentById: authenticatedProcedure getDocumentById: authenticatedProcedure
.input(ZGetDocumentByIdQuerySchema) .input(ZGetDocumentByIdQuerySchema)
.query(async ({ input, ctx }) => { .query(async ({ input, ctx }) => {
try { return await getDocumentById({
return await getDocumentById({ ...input,
...input, userId: ctx.user.id,
userId: ctx.user.id, });
});
} catch (err) {
console.error(err);
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'We were unable to find this document. Please try again later.',
});
}
}), }),
getDocumentByToken: procedure getDocumentByToken: procedure
.input(ZGetDocumentByTokenQuerySchema) .input(ZGetDocumentByTokenQuerySchema)
.query(async ({ input, ctx }) => { .query(async ({ input, ctx }) => {
try { const { token } = input;
const { token } = input;
return await getDocumentAndSenderByToken({ return await getDocumentAndSenderByToken({
token, token,
userId: ctx.user?.id, userId: ctx.user?.id,
}); });
} catch (err) {
console.error(err);
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'We were unable to find this document. Please try again later.',
});
}
}), }),
getDocumentWithDetailsById: authenticatedProcedure getDocumentWithDetailsById: authenticatedProcedure
.input(ZGetDocumentWithDetailsByIdQuerySchema) .input(ZGetDocumentWithDetailsByIdQuerySchema)
.query(async ({ input, ctx }) => { .query(async ({ input, ctx }) => {
try { return await getDocumentWithDetailsById({
return await getDocumentWithDetailsById({ ...input,
...input, userId: ctx.user.id,
userId: ctx.user.id, });
});
} catch (err) {
console.error(err);
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'We were unable to find this document. Please try again later.',
});
}
}), }),
createDocument: authenticatedProcedure createDocument: authenticatedProcedure
.input(ZCreateDocumentMutationSchema) .input(ZCreateDocumentMutationSchema)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
try { const { title, documentDataId, teamId } = input;
const { title, documentDataId, teamId } = input;
const { remaining } = await getServerLimits({ email: ctx.user.email, teamId }); const { remaining } = await getServerLimits({ email: ctx.user.email, teamId });
if (remaining.documents <= 0) {
throw new TRPCError({
code: 'BAD_REQUEST',
message:
'You have reached your document limit for this month. Please upgrade your plan.',
});
}
return await createDocument({
userId: ctx.user.id,
teamId,
title,
documentDataId,
requestMetadata: extractNextApiRequestMetadata(ctx.req),
});
} catch (err) {
console.error(err);
if (err instanceof TRPCError) {
throw err;
}
if (remaining.documents <= 0) {
throw new TRPCError({ throw new TRPCError({
code: 'BAD_REQUEST', code: 'BAD_REQUEST',
message: 'We were unable to create this document. Please try again later.', message: 'You have reached your document limit for this month. Please upgrade your plan.',
}); });
} }
return await createDocument({
userId: ctx.user.id,
teamId,
title,
documentDataId,
requestMetadata: extractNextApiRequestMetadata(ctx.req),
});
}), }),
deleteDocument: authenticatedProcedure deleteDocument: authenticatedProcedure
.input(ZDeleteDocumentMutationSchema) .input(ZDeleteDocumentMutationSchema)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
try { const { id, teamId } = input;
const { id, teamId } = input;
const userId = ctx.user.id; const userId = ctx.user.id;
return await deleteDocument({ return await deleteDocument({
id, id,
userId, userId,
teamId, teamId,
requestMetadata: extractNextApiRequestMetadata(ctx.req), requestMetadata: extractNextApiRequestMetadata(ctx.req),
}); });
} catch (err) {
console.error(err);
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'We were unable to delete this document. Please try again later.',
});
}
}), }),
moveDocumentToTeam: authenticatedProcedure moveDocumentToTeam: authenticatedProcedure
.input(ZMoveDocumentsToTeamSchema) .input(ZMoveDocumentsToTeamSchema)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
try { const { documentId, teamId } = input;
const { documentId, teamId } = input; const userId = ctx.user.id;
const userId = ctx.user.id;
return await moveDocumentToTeam({ return await moveDocumentToTeam({
documentId, documentId,
teamId, teamId,
userId, userId,
requestMetadata: extractNextApiRequestMetadata(ctx.req), requestMetadata: extractNextApiRequestMetadata(ctx.req),
}); });
} catch (err) {
console.error(err);
if (err instanceof TRPCError) {
throw err;
}
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'We were unable to move this document. Please try again later.',
});
}
}), }),
findDocuments: authenticatedProcedure findDocuments: authenticatedProcedure
@@ -199,94 +136,66 @@ export const documentRouter = router({
const { search, teamId, templateId, page, perPage, orderBy, source, status } = input; const { search, teamId, templateId, page, perPage, orderBy, source, status } = input;
try { const documents = await findDocuments({
const documents = await findDocuments({ userId: user.id,
userId: user.id, teamId,
teamId, templateId,
templateId, search,
search, source,
source, status,
status, page,
page, perPage,
perPage, orderBy,
orderBy, });
});
return documents; return documents;
} catch (err) {
console.error(err);
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'We are unable to search for documents. Please try again later.',
});
}
}), }),
findDocumentAuditLogs: authenticatedProcedure findDocumentAuditLogs: authenticatedProcedure
.input(ZFindDocumentAuditLogsQuerySchema) .input(ZFindDocumentAuditLogsQuerySchema)
.query(async ({ input, ctx }) => { .query(async ({ input, ctx }) => {
try { const { page, perPage, documentId, cursor, filterForRecentActivity, orderBy } = input;
const { page, perPage, documentId, cursor, filterForRecentActivity, orderBy } = input;
return await findDocumentAuditLogs({ return await findDocumentAuditLogs({
page, page,
perPage, perPage,
documentId, documentId,
cursor, cursor,
filterForRecentActivity, filterForRecentActivity,
orderBy, orderBy,
userId: ctx.user.id, userId: ctx.user.id,
}); });
} catch (err) {
console.error(err);
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'We were unable to find audit logs for this document. Please try again later.',
});
}
}), }),
// Todo: Add API // Todo: Add API
setSettingsForDocument: authenticatedProcedure setSettingsForDocument: authenticatedProcedure
.input(ZSetSettingsForDocumentMutationSchema) .input(ZSetSettingsForDocumentMutationSchema)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
try { const { documentId, teamId, data, meta } = input;
const { documentId, teamId, data, meta } = input;
const userId = ctx.user.id; const userId = ctx.user.id;
const requestMetadata = extractNextApiRequestMetadata(ctx.req); const requestMetadata = extractNextApiRequestMetadata(ctx.req);
if (meta.timezone || meta.dateFormat || meta.redirectUrl) { if (meta.timezone || meta.dateFormat || meta.redirectUrl) {
await upsertDocumentMeta({ await upsertDocumentMeta({
documentId,
dateFormat: meta.dateFormat,
timezone: meta.timezone,
redirectUrl: meta.redirectUrl,
language: meta.language,
userId: ctx.user.id,
requestMetadata,
});
}
return await updateDocumentSettings({
userId,
teamId,
documentId, documentId,
data, dateFormat: meta.dateFormat,
timezone: meta.timezone,
redirectUrl: meta.redirectUrl,
language: meta.language,
userId: ctx.user.id,
requestMetadata, requestMetadata,
}); });
} catch (err) {
console.error(err);
throw new TRPCError({
code: 'BAD_REQUEST',
message:
'We were unable to update the settings for this document. Please try again later.',
});
} }
return await updateDocumentSettings({
userId,
teamId,
documentId,
data,
requestMetadata,
});
}), }),
setTitleForDocument: authenticatedProcedure setTitleForDocument: authenticatedProcedure
@@ -296,197 +205,131 @@ export const documentRouter = router({
const userId = ctx.user.id; const userId = ctx.user.id;
try { return await updateTitle({
return await updateTitle({ title,
title, userId,
userId, teamId,
teamId, documentId,
documentId, requestMetadata: extractNextApiRequestMetadata(ctx.req),
requestMetadata: extractNextApiRequestMetadata(ctx.req), });
});
} catch (err) {
console.error(err);
throw err;
}
}), }),
setPasswordForDocument: authenticatedProcedure setPasswordForDocument: authenticatedProcedure
.input(ZSetPasswordForDocumentMutationSchema) .input(ZSetPasswordForDocumentMutationSchema)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
try { const { documentId, password } = input;
const { documentId, password } = input;
const key = DOCUMENSO_ENCRYPTION_KEY; const key = DOCUMENSO_ENCRYPTION_KEY;
if (!key) { if (!key) {
throw new Error('Missing encryption key'); throw new Error('Missing encryption key');
}
const securePassword = symmetricEncrypt({
data: password,
key,
});
await upsertDocumentMeta({
documentId,
password: securePassword,
userId: ctx.user.id,
requestMetadata: extractNextApiRequestMetadata(ctx.req),
});
} catch (err) {
console.error(err);
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'We were unable to set the password for this document. Please try again later.',
});
} }
const securePassword = symmetricEncrypt({
data: password,
key,
});
await upsertDocumentMeta({
documentId,
password: securePassword,
userId: ctx.user.id,
requestMetadata: extractNextApiRequestMetadata(ctx.req),
});
}), }),
setSigningOrderForDocument: authenticatedProcedure setSigningOrderForDocument: authenticatedProcedure
.input(ZSetSigningOrderForDocumentMutationSchema) .input(ZSetSigningOrderForDocumentMutationSchema)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
try { const { documentId, signingOrder } = input;
const { documentId, signingOrder } = input;
return await upsertDocumentMeta({ return await upsertDocumentMeta({
documentId, documentId,
signingOrder, signingOrder,
userId: ctx.user.id, userId: ctx.user.id,
requestMetadata: extractNextApiRequestMetadata(ctx.req), requestMetadata: extractNextApiRequestMetadata(ctx.req),
}); });
} catch (err) {
console.error(err);
throw new TRPCError({
code: 'BAD_REQUEST',
message:
'We were unable to update the settings for this document. Please try again later.',
});
}
}), }),
updateTypedSignatureSettings: authenticatedProcedure updateTypedSignatureSettings: authenticatedProcedure
.input(ZUpdateTypedSignatureSettingsMutationSchema) .input(ZUpdateTypedSignatureSettingsMutationSchema)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
try { const { documentId, teamId, typedSignatureEnabled } = input;
const { documentId, teamId, typedSignatureEnabled } = input;
const document = await getDocumentById({ const document = await getDocumentById({
id: documentId, id: documentId,
teamId, teamId,
userId: ctx.user.id, userId: ctx.user.id,
}).catch(() => null); }).catch(() => null);
if (!document) {
throw new TRPCError({
code: 'NOT_FOUND',
message: 'Document not found',
});
}
return await upsertDocumentMeta({
documentId,
typedSignatureEnabled,
userId: ctx.user.id,
requestMetadata: extractNextApiRequestMetadata(ctx.req),
});
} catch (err) {
console.error(err);
if (err instanceof TRPCError) {
throw err;
}
if (!document) {
throw new TRPCError({ throw new TRPCError({
code: 'BAD_REQUEST', code: 'NOT_FOUND',
message: message: 'Document not found',
'We were unable to update the settings for this document. Please try again later.',
}); });
} }
return await upsertDocumentMeta({
documentId,
typedSignatureEnabled,
userId: ctx.user.id,
requestMetadata: extractNextApiRequestMetadata(ctx.req),
});
}), }),
sendDocument: authenticatedProcedure sendDocument: authenticatedProcedure
.input(ZSendDocumentMutationSchema) .input(ZSendDocumentMutationSchema)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
try { const { documentId, teamId, meta } = input;
const { documentId, teamId, meta } = input;
if ( if (
meta.message || meta.message ||
meta.subject || meta.subject ||
meta.timezone || meta.timezone ||
meta.dateFormat || meta.dateFormat ||
meta.redirectUrl || meta.redirectUrl ||
meta.distributionMethod || meta.distributionMethod ||
meta.emailSettings meta.emailSettings
) { ) {
await upsertDocumentMeta({ await upsertDocumentMeta({
documentId,
subject: meta.subject,
message: meta.message,
dateFormat: meta.dateFormat,
timezone: meta.timezone,
redirectUrl: meta.redirectUrl,
distributionMethod: meta.distributionMethod,
userId: ctx.user.id,
emailSettings: meta.emailSettings,
requestMetadata: extractNextApiRequestMetadata(ctx.req),
});
}
return await sendDocument({
userId: ctx.user.id,
documentId, documentId,
teamId, subject: meta.subject,
message: meta.message,
dateFormat: meta.dateFormat,
timezone: meta.timezone,
redirectUrl: meta.redirectUrl,
distributionMethod: meta.distributionMethod,
userId: ctx.user.id,
emailSettings: meta.emailSettings,
requestMetadata: extractNextApiRequestMetadata(ctx.req), requestMetadata: extractNextApiRequestMetadata(ctx.req),
}); });
} catch (err) {
console.error(err);
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'We were unable to send this document. Please try again later.',
});
} }
return await sendDocument({
userId: ctx.user.id,
documentId,
teamId,
requestMetadata: extractNextApiRequestMetadata(ctx.req),
});
}), }),
resendDocument: authenticatedProcedure resendDocument: authenticatedProcedure
.input(ZResendDocumentMutationSchema) .input(ZResendDocumentMutationSchema)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
try { return await resendDocument({
return await resendDocument({ userId: ctx.user.id,
userId: ctx.user.id, ...input,
...input, requestMetadata: extractNextApiRequestMetadata(ctx.req),
requestMetadata: extractNextApiRequestMetadata(ctx.req), });
});
} catch (err) {
console.error(err);
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'We were unable to resend this document. Please try again later.',
});
}
}), }),
duplicateDocument: authenticatedProcedure duplicateDocument: authenticatedProcedure
.input(ZGetDocumentByIdQuerySchema) .input(ZGetDocumentByIdQuerySchema)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
try { return await duplicateDocumentById({
return await duplicateDocumentById({ userId: ctx.user.id,
userId: ctx.user.id, ...input,
...input, });
});
} catch (err) {
console.log(err);
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'We are unable to duplicate this document. Please try again later.',
});
}
}), }),
searchDocuments: authenticatedProcedure searchDocuments: authenticatedProcedure
@@ -494,93 +337,64 @@ export const documentRouter = router({
.query(async ({ input, ctx }) => { .query(async ({ input, ctx }) => {
const { query } = input; const { query } = input;
try { const documents = await searchDocumentsWithKeyword({
const documents = await searchDocumentsWithKeyword({ query,
query, userId: ctx.user.id,
userId: ctx.user.id, });
});
return documents; return documents;
} catch (err) {
console.error(err);
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'We are unable to search for documents. Please try again later.',
});
}
}), }),
downloadAuditLogs: authenticatedProcedure downloadAuditLogs: authenticatedProcedure
.input(ZDownloadAuditLogsMutationSchema) .input(ZDownloadAuditLogsMutationSchema)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
try { const { documentId, teamId } = input;
const { documentId, teamId } = input;
const document = await getDocumentById({ const document = await getDocumentById({
id: documentId, id: documentId,
userId: ctx.user.id, userId: ctx.user.id,
teamId, teamId,
}).catch(() => null); }).catch(() => null);
if (!document || (teamId && document.teamId !== teamId)) {
throw new TRPCError({
code: 'FORBIDDEN',
message: 'You do not have access to this document.',
});
}
const encrypted = encryptSecondaryData({
data: document.id.toString(),
expiresAt: DateTime.now().plus({ minutes: 5 }).toJSDate().valueOf(),
});
return {
url: `${NEXT_PUBLIC_WEBAPP_URL()}/__htmltopdf/audit-log?d=${encrypted}`,
};
} catch (err) {
console.error(err);
if (!document || (teamId && document.teamId !== teamId)) {
throw new TRPCError({ throw new TRPCError({
code: 'BAD_REQUEST', code: 'FORBIDDEN',
message: message: 'You do not have access to this document.',
'We were unable to download the audit logs for this document. Please try again later.',
}); });
} }
const encrypted = encryptSecondaryData({
data: document.id.toString(),
expiresAt: DateTime.now().plus({ minutes: 5 }).toJSDate().valueOf(),
});
return {
url: `${NEXT_PUBLIC_WEBAPP_URL()}/__htmltopdf/audit-log?d=${encrypted}`,
};
}), }),
downloadCertificate: authenticatedProcedure downloadCertificate: authenticatedProcedure
.input(ZDownloadCertificateMutationSchema) .input(ZDownloadCertificateMutationSchema)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
try { const { documentId, teamId } = input;
const { documentId, teamId } = input;
const document = await getDocumentById({ const document = await getDocumentById({
id: documentId, id: documentId,
userId: ctx.user.id, userId: ctx.user.id,
teamId, teamId,
}); });
if (document.status !== DocumentStatus.COMPLETED) { if (document.status !== DocumentStatus.COMPLETED) {
throw new AppError('DOCUMENT_NOT_COMPLETE'); throw new AppError('DOCUMENT_NOT_COMPLETE');
}
const encrypted = encryptSecondaryData({
data: document.id.toString(),
expiresAt: DateTime.now().plus({ minutes: 5 }).toJSDate().valueOf(),
});
return {
url: `${NEXT_PUBLIC_WEBAPP_URL()}/__htmltopdf/certificate?d=${encrypted}`,
};
} catch (err) {
console.error(err);
throw new TRPCError({
code: 'BAD_REQUEST',
message:
'We were unable to download the audit logs for this document. Please try again later.',
});
} }
const encrypted = encryptSecondaryData({
data: document.id.toString(),
expiresAt: DateTime.now().plus({ minutes: 5 }).toJSDate().valueOf(),
});
return {
url: `${NEXT_PUBLIC_WEBAPP_URL()}/__htmltopdf/certificate?d=${encrypted}`,
};
}), }),
}); });

View File

@@ -1,5 +1,3 @@
import { TRPCError } from '@trpc/server';
import { getFieldById } from '@documenso/lib/server-only/field/get-field-by-id'; import { getFieldById } from '@documenso/lib/server-only/field/get-field-by-id';
import { removeSignedFieldWithToken } from '@documenso/lib/server-only/field/remove-signed-field-with-token'; import { removeSignedFieldWithToken } from '@documenso/lib/server-only/field/remove-signed-field-with-token';
import { setFieldsForDocument } from '@documenso/lib/server-only/field/set-fields-for-document'; import { setFieldsForDocument } from '@documenso/lib/server-only/field/set-fields-for-document';
@@ -20,33 +18,24 @@ export const fieldRouter = router({
addFields: authenticatedProcedure addFields: authenticatedProcedure
.input(ZAddFieldsMutationSchema) .input(ZAddFieldsMutationSchema)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
try { const { documentId, fields } = input;
const { documentId, fields } = input;
return await setFieldsForDocument({ return await setFieldsForDocument({
documentId, documentId,
userId: ctx.user.id, userId: ctx.user.id,
fields: fields.map((field) => ({ fields: fields.map((field) => ({
id: field.nativeId, id: field.nativeId,
signerEmail: field.signerEmail, signerEmail: field.signerEmail,
type: field.type, type: field.type,
pageNumber: field.pageNumber, pageNumber: field.pageNumber,
pageX: field.pageX, pageX: field.pageX,
pageY: field.pageY, pageY: field.pageY,
pageWidth: field.pageWidth, pageWidth: field.pageWidth,
pageHeight: field.pageHeight, pageHeight: field.pageHeight,
fieldMeta: field.fieldMeta, fieldMeta: field.fieldMeta,
})), })),
requestMetadata: extractNextApiRequestMetadata(ctx.req), requestMetadata: extractNextApiRequestMetadata(ctx.req),
}); });
} catch (err) {
console.error(err);
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'We were unable to set this field. Please try again later.',
});
}
}), }),
addTemplateFields: authenticatedProcedure addTemplateFields: authenticatedProcedure
@@ -54,89 +43,59 @@ export const fieldRouter = router({
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
const { templateId, fields } = input; const { templateId, fields } = input;
try { return await setFieldsForTemplate({
return await setFieldsForTemplate({ userId: ctx.user.id,
userId: ctx.user.id, templateId,
templateId, fields: fields.map((field) => ({
fields: fields.map((field) => ({ id: field.nativeId,
id: field.nativeId, signerEmail: field.signerEmail,
signerEmail: field.signerEmail, type: field.type,
type: field.type, pageNumber: field.pageNumber,
pageNumber: field.pageNumber, pageX: field.pageX,
pageX: field.pageX, pageY: field.pageY,
pageY: field.pageY, pageWidth: field.pageWidth,
pageWidth: field.pageWidth, pageHeight: field.pageHeight,
pageHeight: field.pageHeight, fieldMeta: field.fieldMeta,
fieldMeta: field.fieldMeta, })),
})), });
});
} catch (err) {
console.error(err);
throw err;
}
}), }),
signFieldWithToken: procedure signFieldWithToken: procedure
.input(ZSignFieldWithTokenMutationSchema) .input(ZSignFieldWithTokenMutationSchema)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
try { const { token, fieldId, value, isBase64, authOptions } = input;
const { token, fieldId, value, isBase64, authOptions } = input;
return await signFieldWithToken({ return await signFieldWithToken({
token, token,
fieldId, fieldId,
value, value,
isBase64, isBase64,
userId: ctx.user?.id, userId: ctx.user?.id,
authOptions, authOptions,
requestMetadata: extractNextApiRequestMetadata(ctx.req), requestMetadata: extractNextApiRequestMetadata(ctx.req),
}); });
} catch (err) {
console.error(err);
throw err;
}
}), }),
removeSignedFieldWithToken: procedure removeSignedFieldWithToken: procedure
.input(ZRemovedSignedFieldWithTokenMutationSchema) .input(ZRemovedSignedFieldWithTokenMutationSchema)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
try { const { token, fieldId } = input;
const { token, fieldId } = input;
return await removeSignedFieldWithToken({ return await removeSignedFieldWithToken({
token, token,
fieldId, fieldId,
requestMetadata: extractNextApiRequestMetadata(ctx.req), requestMetadata: extractNextApiRequestMetadata(ctx.req),
}); });
} catch (err) {
console.error(err);
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'We were unable to remove the signature for this field. Please try again later.',
});
}
}), }),
getField: authenticatedProcedure.input(ZGetFieldQuerySchema).query(async ({ input, ctx }) => { getField: authenticatedProcedure.input(ZGetFieldQuerySchema).query(async ({ input, ctx }) => {
try { const { fieldId, teamId } = input;
const { fieldId, teamId } = input;
return await getFieldById({ return await getFieldById({
userId: ctx.user.id, userId: ctx.user.id,
teamId, teamId,
fieldId, fieldId,
}); });
} catch (err) {
console.error(err);
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'We were unable to find this field. Please try again.',
});
}
}), }),
// This doesn't appear to be used anywhere, and it doesn't seem to support updating template fields // This doesn't appear to be used anywhere, and it doesn't seem to support updating template fields

View File

@@ -1,5 +1,3 @@
import { TRPCError } from '@trpc/server';
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app'; import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { jobsClient } from '@documenso/lib/jobs/client'; import { jobsClient } from '@documenso/lib/jobs/client';
@@ -33,246 +31,122 @@ export const profileRouter = router({
findUserSecurityAuditLogs: authenticatedProcedure findUserSecurityAuditLogs: authenticatedProcedure
.input(ZFindUserSecurityAuditLogsSchema) .input(ZFindUserSecurityAuditLogsSchema)
.query(async ({ input, ctx }) => { .query(async ({ input, ctx }) => {
try { return await findUserSecurityAuditLogs({
return await findUserSecurityAuditLogs({ userId: ctx.user.id,
userId: ctx.user.id, ...input,
...input, });
});
} catch (err) {
console.error(err);
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'We were unable to find user security audit logs. Please try again.',
});
}
}), }),
getUser: adminProcedure.input(ZRetrieveUserByIdQuerySchema).query(async ({ input }) => { getUser: adminProcedure.input(ZRetrieveUserByIdQuerySchema).query(async ({ input }) => {
try { const { id } = input;
const { id } = input;
return await getUserById({ id }); return await getUserById({ id });
} catch (err) {
console.error(err);
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'We were unable to retrieve the specified account. Please try again.',
});
}
}), }),
updateProfile: authenticatedProcedure updateProfile: authenticatedProcedure
.input(ZUpdateProfileMutationSchema) .input(ZUpdateProfileMutationSchema)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
try { const { name, signature } = input;
const { name, signature } = input;
return await updateProfile({ return await updateProfile({
userId: ctx.user.id, userId: ctx.user.id,
name, name,
signature, signature,
requestMetadata: extractNextApiRequestMetadata(ctx.req), requestMetadata: extractNextApiRequestMetadata(ctx.req),
}); });
} catch (err) {
console.error(err);
throw new TRPCError({
code: 'BAD_REQUEST',
message:
'We were unable to update your profile. Please review the information you provided and try again.',
});
}
}), }),
updatePublicProfile: authenticatedProcedure updatePublicProfile: authenticatedProcedure
.input(ZUpdatePublicProfileMutationSchema) .input(ZUpdatePublicProfileMutationSchema)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
try { const { url, bio, enabled } = input;
const { url, bio, enabled } = input;
if (IS_BILLING_ENABLED() && url !== undefined && url.length < 6) { if (IS_BILLING_ENABLED() && url !== undefined && url.length < 6) {
const subscriptions = await getSubscriptionsByUserId({ const subscriptions = await getSubscriptionsByUserId({
userId: ctx.user.id,
}).then((subscriptions) =>
subscriptions.filter((s) => s.status === SubscriptionStatus.ACTIVE),
);
if (subscriptions.length === 0) {
throw new AppError(AppErrorCode.PREMIUM_PROFILE_URL, {
message: 'Only subscribers can have a username shorter than 6 characters',
});
}
}
const user = await updatePublicProfile({
userId: ctx.user.id, userId: ctx.user.id,
data: { }).then((subscriptions) =>
url, subscriptions.filter((s) => s.status === SubscriptionStatus.ACTIVE),
bio, );
enabled,
},
});
return { success: true, url: user.url }; if (subscriptions.length === 0) {
} catch (err) { throw new AppError(AppErrorCode.PREMIUM_PROFILE_URL, {
console.error(err); message: 'Only subscribers can have a username shorter than 6 characters',
});
const error = AppError.parseError(err);
if (error.code !== AppErrorCode.UNKNOWN_ERROR) {
throw error;
} }
throw new TRPCError({
code: 'BAD_REQUEST',
message:
'We were unable to update your public profile. Please review the information you provided and try again.',
});
} }
const user = await updatePublicProfile({
userId: ctx.user.id,
data: {
url,
bio,
enabled,
},
});
return { success: true, url: user.url };
}), }),
updatePassword: authenticatedProcedure updatePassword: authenticatedProcedure
.input(ZUpdatePasswordMutationSchema) .input(ZUpdatePasswordMutationSchema)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
try { const { password, currentPassword } = input;
const { password, currentPassword } = input;
return await updatePassword({ return await updatePassword({
userId: ctx.user.id, userId: ctx.user.id,
password, password,
currentPassword, currentPassword,
requestMetadata: extractNextApiRequestMetadata(ctx.req), requestMetadata: extractNextApiRequestMetadata(ctx.req),
}); });
} catch (err) {
console.error(err);
let message =
'We were unable to update your profile. Please review the information you provided and try again.';
if (err instanceof Error) {
message = err.message;
}
throw new TRPCError({
code: 'BAD_REQUEST',
message,
});
}
}), }),
forgotPassword: procedure.input(ZForgotPasswordFormSchema).mutation(async ({ input }) => { forgotPassword: procedure.input(ZForgotPasswordFormSchema).mutation(async ({ input }) => {
try { const { email } = input;
const { email } = input;
return await forgotPassword({ return await forgotPassword({
email, email,
}); });
} catch (err) {
console.error(err);
}
}), }),
resetPassword: procedure.input(ZResetPasswordFormSchema).mutation(async ({ input, ctx }) => { resetPassword: procedure.input(ZResetPasswordFormSchema).mutation(async ({ input, ctx }) => {
try { const { password, token } = input;
const { password, token } = input;
return await resetPassword({ return await resetPassword({
token, token,
password, password,
requestMetadata: extractNextApiRequestMetadata(ctx.req), requestMetadata: extractNextApiRequestMetadata(ctx.req),
}); });
} catch (err) {
console.error(err);
let message = 'We were unable to reset your password. Please try again.';
if (err instanceof Error) {
message = err.message;
}
throw new TRPCError({
code: 'BAD_REQUEST',
message,
});
}
}), }),
sendConfirmationEmail: procedure sendConfirmationEmail: procedure
.input(ZConfirmEmailMutationSchema) .input(ZConfirmEmailMutationSchema)
.mutation(async ({ input }) => { .mutation(async ({ input }) => {
try { const { email } = input;
const { email } = input;
await jobsClient.triggerJob({ await jobsClient.triggerJob({
name: 'send.signup.confirmation.email', name: 'send.signup.confirmation.email',
payload: { payload: {
email, email,
}, },
}); });
} catch (err) {
console.error(err);
let message = 'We were unable to send a confirmation email. Please try again.';
if (err instanceof Error) {
message = err.message;
}
throw new TRPCError({
code: 'BAD_REQUEST',
message,
});
}
}), }),
deleteAccount: authenticatedProcedure.mutation(async ({ ctx }) => { deleteAccount: authenticatedProcedure.mutation(async ({ ctx }) => {
try { return await deleteUser({
return await deleteUser({ id: ctx.user.id,
id: ctx.user.id, });
});
} catch (err) {
console.error(err);
let message = 'We were unable to delete your account. Please try again.';
if (err instanceof Error) {
message = err.message;
}
throw new TRPCError({
code: 'BAD_REQUEST',
message,
});
}
}), }),
setProfileImage: authenticatedProcedure setProfileImage: authenticatedProcedure
.input(ZSetProfileImageMutationSchema) .input(ZSetProfileImageMutationSchema)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
try { const { bytes, teamId } = input;
const { bytes, teamId } = input;
return await setAvatarImage({ return await setAvatarImage({
userId: ctx.user.id, userId: ctx.user.id,
teamId, teamId,
bytes, bytes,
requestMetadata: extractNextApiRequestMetadata(ctx.req), requestMetadata: extractNextApiRequestMetadata(ctx.req),
}); });
} catch (err) {
console.error(err);
let message = 'We were unable to update your profile image. Please try again.';
if (err instanceof Error) {
message = err.message;
}
throw new TRPCError({
code: 'BAD_REQUEST',
message,
});
}
}), }),
}); });

View File

@@ -1,5 +1,3 @@
import { TRPCError } from '@trpc/server';
import { completeDocumentWithToken } from '@documenso/lib/server-only/document/complete-document-with-token'; import { completeDocumentWithToken } from '@documenso/lib/server-only/document/complete-document-with-token';
import { rejectDocumentWithToken } from '@documenso/lib/server-only/document/reject-document-with-token'; import { rejectDocumentWithToken } from '@documenso/lib/server-only/document/reject-document-with-token';
import { setRecipientsForDocument } from '@documenso/lib/server-only/recipient/set-recipients-for-document'; import { setRecipientsForDocument } from '@documenso/lib/server-only/recipient/set-recipients-for-document';
@@ -18,104 +16,68 @@ export const recipientRouter = router({
addSigners: authenticatedProcedure addSigners: authenticatedProcedure
.input(ZAddSignersMutationSchema) .input(ZAddSignersMutationSchema)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
try { const { documentId, teamId, signers } = input;
const { documentId, teamId, signers } = input;
return await setRecipientsForDocument({ return await setRecipientsForDocument({
userId: ctx.user.id, userId: ctx.user.id,
documentId, documentId,
teamId, teamId,
recipients: signers.map((signer) => ({ recipients: signers.map((signer) => ({
id: signer.nativeId, id: signer.nativeId,
email: signer.email, email: signer.email,
name: signer.name, name: signer.name,
role: signer.role, role: signer.role,
signingOrder: signer.signingOrder, signingOrder: signer.signingOrder,
actionAuth: signer.actionAuth, actionAuth: signer.actionAuth,
})), })),
requestMetadata: extractNextApiRequestMetadata(ctx.req), requestMetadata: extractNextApiRequestMetadata(ctx.req),
}); });
} catch (err) {
console.error(err);
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'We were unable to set this field. Please try again later.',
});
}
}), }),
addTemplateSigners: authenticatedProcedure addTemplateSigners: authenticatedProcedure
.input(ZAddTemplateSignersMutationSchema) .input(ZAddTemplateSignersMutationSchema)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
try { const { templateId, signers, teamId } = input;
const { templateId, signers, teamId } = input;
return await setRecipientsForTemplate({ return await setRecipientsForTemplate({
userId: ctx.user.id, userId: ctx.user.id,
teamId, teamId,
templateId, templateId,
recipients: signers.map((signer) => ({ recipients: signers.map((signer) => ({
id: signer.nativeId, id: signer.nativeId,
email: signer.email, email: signer.email,
name: signer.name, name: signer.name,
role: signer.role, role: signer.role,
signingOrder: signer.signingOrder, signingOrder: signer.signingOrder,
actionAuth: signer.actionAuth, actionAuth: signer.actionAuth,
})), })),
}); });
} catch (err) {
console.error(err);
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'We were unable to set this field. Please try again later.',
});
}
}), }),
completeDocumentWithToken: procedure completeDocumentWithToken: procedure
.input(ZCompleteDocumentWithTokenMutationSchema) .input(ZCompleteDocumentWithTokenMutationSchema)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
try { const { token, documentId, authOptions } = input;
const { token, documentId, authOptions } = input;
return await completeDocumentWithToken({ return await completeDocumentWithToken({
token, token,
documentId, documentId,
authOptions, authOptions,
userId: ctx.user?.id, userId: ctx.user?.id,
requestMetadata: extractNextApiRequestMetadata(ctx.req), requestMetadata: extractNextApiRequestMetadata(ctx.req),
}); });
} catch (err) {
console.error(err);
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'We were unable to sign this field. Please try again later.',
});
}
}), }),
rejectDocumentWithToken: procedure rejectDocumentWithToken: procedure
.input(ZRejectDocumentWithTokenMutationSchema) .input(ZRejectDocumentWithTokenMutationSchema)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
try { const { token, documentId, reason } = input;
const { token, documentId, reason } = input;
return await rejectDocumentWithToken({ return await rejectDocumentWithToken({
token, token,
documentId, documentId,
reason, reason,
requestMetadata: extractNextApiRequestMetadata(ctx.req), requestMetadata: extractNextApiRequestMetadata(ctx.req),
}); });
} catch (err) {
console.error(err);
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'We were unable to handle this request. Please try again later.',
});
}
}), }),
}); });

View File

@@ -1,7 +1,6 @@
import { adminRouter } from './admin-router/router'; import { adminRouter } from './admin-router/router';
import { apiTokenRouter } from './api-token-router/router'; import { apiTokenRouter } from './api-token-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';
@@ -16,7 +15,6 @@ import { webhookRouter } from './webhook-router/router';
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,

View File

@@ -1,5 +1,3 @@
import { TRPCError } from '@trpc/server';
import { createOrGetShareLink } from '@documenso/lib/server-only/share/create-or-get-share-link'; import { createOrGetShareLink } from '@documenso/lib/server-only/share/create-or-get-share-link';
import { procedure, router } from '../trpc'; import { procedure, router } from '../trpc';
@@ -9,27 +7,18 @@ export const shareLinkRouter = router({
createOrGetShareLink: procedure createOrGetShareLink: procedure
.input(ZCreateOrGetShareLinkMutationSchema) .input(ZCreateOrGetShareLinkMutationSchema)
.mutation(async ({ ctx, input }) => { .mutation(async ({ ctx, input }) => {
try { const { documentId, token } = input;
const { documentId, token } = input;
if (token) { if (token) {
return await createOrGetShareLink({ documentId, token }); return await createOrGetShareLink({ documentId, token });
}
if (!ctx.user?.id) {
throw new Error(
'You must either provide a token or be logged in to create a sharing link.',
);
}
return await createOrGetShareLink({ documentId, userId: ctx.user.id });
} catch (err) {
console.error(err);
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'We were unable to create a sharing link.',
});
} }
if (!ctx.user?.id) {
throw new Error(
'You must either provide a token or be logged in to create a sharing link.',
);
}
return await createOrGetShareLink({ documentId, userId: ctx.user.id });
}), }),
}); });

View File

@@ -30,159 +30,153 @@ export const singleplayerRouter = router({
createSinglePlayerDocument: procedure createSinglePlayerDocument: procedure
.input(ZCreateSinglePlayerDocumentMutationSchema) .input(ZCreateSinglePlayerDocumentMutationSchema)
.mutation(async ({ input }) => { .mutation(async ({ input }) => {
try { const { signer, fields, documentData, documentName, fieldMeta } = input;
const { signer, fields, documentData, documentName, fieldMeta } = input;
const document = await getFile({ const document = await getFile({
data: documentData.data, data: documentData.data,
type: documentData.type, type: documentData.type,
});
const doc = await PDFDocument.load(document);
const createdAt = new Date();
const isBase64 = signer.signature.startsWith('data:image/png;base64,');
const signatureImageAsBase64 = isBase64 ? signer.signature : null;
const typedSignature = !isBase64 ? signer.signature : null;
// Update the document with the fields inserted.
for (const field of fields) {
const isSignatureField = field.type === FieldType.SIGNATURE;
await insertFieldInPDF(doc, {
...mapField(field, signer),
Signature: isSignatureField
? {
created: createdAt,
signatureImageAsBase64,
typedSignature,
// Dummy data.
id: -1,
recipientId: -1,
fieldId: -1,
}
: null,
// Dummy data.
id: -1,
secondaryId: '-1',
documentId: -1,
templateId: null,
recipientId: -1,
fieldMeta: fieldMeta || null,
}); });
}
const doc = await PDFDocument.load(document); const unsignedPdfBytes = await doc.save();
const createdAt = new Date(); const signedPdfBuffer = await signPdf({ pdf: Buffer.from(unsignedPdfBytes) });
const isBase64 = signer.signature.startsWith('data:image/png;base64,'); const { token } = await prisma.$transaction(
const signatureImageAsBase64 = isBase64 ? signer.signature : null; async (tx) => {
const typedSignature = !isBase64 ? signer.signature : null; const token = alphaid();
// Update the document with the fields inserted. // Fetch service user who will be the owner of the document.
for (const field of fields) { const serviceUser = await tx.user.findFirstOrThrow({
const isSignatureField = field.type === FieldType.SIGNATURE; where: {
email: SERVICE_USER_EMAIL,
await insertFieldInPDF(doc, { },
...mapField(field, signer),
Signature: isSignatureField
? {
created: createdAt,
signatureImageAsBase64,
typedSignature,
// Dummy data.
id: -1,
recipientId: -1,
fieldId: -1,
}
: null,
// Dummy data.
id: -1,
secondaryId: '-1',
documentId: -1,
templateId: null,
recipientId: -1,
fieldMeta: fieldMeta || null,
}); });
}
const unsignedPdfBytes = await doc.save(); const { id: documentDataId } = await putPdfFile({
name: `${documentName}.pdf`,
type: 'application/pdf',
arrayBuffer: async () => Promise.resolve(signedPdfBuffer),
});
const signedPdfBuffer = await signPdf({ pdf: Buffer.from(unsignedPdfBytes) }); // Create document.
const document = await tx.document.create({
data: {
source: DocumentSource.DOCUMENT,
title: documentName,
status: DocumentStatus.COMPLETED,
documentDataId,
userId: serviceUser.id,
createdAt,
},
});
const { token } = await prisma.$transaction( // Create recipient.
async (tx) => { const recipient = await tx.recipient.create({
const token = alphaid(); data: {
documentId: document.id,
name: signer.name,
email: signer.email,
token,
signedAt: createdAt,
readStatus: ReadStatus.OPENED,
signingStatus: SigningStatus.SIGNED,
sendStatus: SendStatus.SENT,
},
});
// Fetch service user who will be the owner of the document. // Create fields and signatures.
const serviceUser = await tx.user.findFirstOrThrow({ await Promise.all(
where: { fields.map(async (field) => {
email: SERVICE_USER_EMAIL, const insertedField = await tx.field.create({
}, data: {
}); documentId: document.id,
recipientId: recipient.id,
...mapField(field, signer),
},
});
const { id: documentDataId } = await putPdfFile({ if (field.type === FieldType.SIGNATURE || field.type === FieldType.FREE_SIGNATURE) {
name: `${documentName}.pdf`, await tx.signature.create({
type: 'application/pdf',
arrayBuffer: async () => Promise.resolve(signedPdfBuffer),
});
// Create document.
const document = await tx.document.create({
data: {
source: DocumentSource.DOCUMENT,
title: documentName,
status: DocumentStatus.COMPLETED,
documentDataId,
userId: serviceUser.id,
createdAt,
},
});
// Create recipient.
const recipient = await tx.recipient.create({
data: {
documentId: document.id,
name: signer.name,
email: signer.email,
token,
signedAt: createdAt,
readStatus: ReadStatus.OPENED,
signingStatus: SigningStatus.SIGNED,
sendStatus: SendStatus.SENT,
},
});
// Create fields and signatures.
await Promise.all(
fields.map(async (field) => {
const insertedField = await tx.field.create({
data: { data: {
documentId: document.id, fieldId: insertedField.id,
signatureImageAsBase64,
typedSignature,
recipientId: recipient.id, recipientId: recipient.id,
...mapField(field, signer),
}, },
}); });
}
}),
);
if (field.type === FieldType.SIGNATURE || field.type === FieldType.FREE_SIGNATURE) { return { document, token };
await tx.signature.create({ },
data: { {
fieldId: insertedField.id, maxWait: 5000,
signatureImageAsBase64, timeout: 30000,
typedSignature, },
recipientId: recipient.id, );
},
});
}
}),
);
return { document, token }; const template = createElement(DocumentSelfSignedEmailTemplate, {
}, documentName: documentName,
{ assetBaseUrl: NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000',
maxWait: 5000, });
timeout: 30000,
},
);
const template = createElement(DocumentSelfSignedEmailTemplate, { const [html, text] = await Promise.all([
documentName: documentName, renderEmailWithI18N(template),
assetBaseUrl: NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000', renderEmailWithI18N(template, { plainText: true }),
}); ]);
const [html, text] = await Promise.all([ // Send email to signer.
renderEmailWithI18N(template), await mailer.sendMail({
renderEmailWithI18N(template, { plainText: true }), to: {
]); address: signer.email,
name: signer.name,
},
from: {
name: FROM_NAME,
address: FROM_ADDRESS,
},
subject: 'Document signed',
html,
text,
attachments: [{ content: signedPdfBuffer, filename: documentName }],
});
// Send email to signer. return token;
await mailer.sendMail({
to: {
address: signer.email,
name: signer.name,
},
from: {
name: FROM_NAME,
address: FROM_ADDRESS,
},
subject: 'Document signed',
html,
text,
attachments: [{ content: signedPdfBuffer, filename: documentName }],
});
return token;
} catch (err) {
console.error(err);
throw err;
}
}), }),
}); });

View File

@@ -1,6 +1,3 @@
import { TRPCError } from '@trpc/server';
import { AppError } from '@documenso/lib/errors/app-error';
import { disableTwoFactorAuthentication } from '@documenso/lib/server-only/2fa/disable-2fa'; import { disableTwoFactorAuthentication } from '@documenso/lib/server-only/2fa/disable-2fa';
import { enableTwoFactorAuthentication } from '@documenso/lib/server-only/2fa/enable-2fa'; import { enableTwoFactorAuthentication } from '@documenso/lib/server-only/2fa/enable-2fa';
import { setupTwoFactorAuthentication } from '@documenso/lib/server-only/2fa/setup-2fa'; import { setupTwoFactorAuthentication } from '@documenso/lib/server-only/2fa/setup-2fa';
@@ -16,89 +13,44 @@ import {
export const twoFactorAuthenticationRouter = router({ export const twoFactorAuthenticationRouter = router({
setup: authenticatedProcedure.mutation(async ({ ctx }) => { setup: authenticatedProcedure.mutation(async ({ ctx }) => {
try { return await setupTwoFactorAuthentication({
return await setupTwoFactorAuthentication({ user: ctx.user,
user: ctx.user, });
});
} catch (err) {
console.error(err);
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'We were unable to setup two-factor authentication. Please try again later.',
});
}
}), }),
enable: authenticatedProcedure enable: authenticatedProcedure
.input(ZEnableTwoFactorAuthenticationMutationSchema) .input(ZEnableTwoFactorAuthenticationMutationSchema)
.mutation(async ({ ctx, input }) => { .mutation(async ({ ctx, input }) => {
try { const user = ctx.user;
const user = ctx.user;
const { code } = input; const { code } = input;
return await enableTwoFactorAuthentication({ return await enableTwoFactorAuthentication({
user, user,
code, code,
requestMetadata: extractNextApiRequestMetadata(ctx.req), requestMetadata: extractNextApiRequestMetadata(ctx.req),
}); });
} catch (err) {
const error = AppError.parseError(err);
if (error.code !== 'INCORRECT_TWO_FACTOR_CODE') {
console.error(err);
}
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'We were unable to enable two-factor authentication. Please try again later.',
});
}
}), }),
disable: authenticatedProcedure disable: authenticatedProcedure
.input(ZDisableTwoFactorAuthenticationMutationSchema) .input(ZDisableTwoFactorAuthenticationMutationSchema)
.mutation(async ({ ctx, input }) => { .mutation(async ({ ctx, input }) => {
try { const user = ctx.user;
const user = ctx.user;
return await disableTwoFactorAuthentication({ return await disableTwoFactorAuthentication({
user, user,
totpCode: input.totpCode, totpCode: input.totpCode,
backupCode: input.backupCode, backupCode: input.backupCode,
requestMetadata: extractNextApiRequestMetadata(ctx.req), requestMetadata: extractNextApiRequestMetadata(ctx.req),
}); });
} catch (err) {
const error = AppError.parseError(err);
if (error.code !== 'INCORRECT_TWO_FACTOR_CODE') {
console.error(err);
}
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'We were unable to disable two-factor authentication. Please try again later.',
});
}
}), }),
viewRecoveryCodes: authenticatedProcedure viewRecoveryCodes: authenticatedProcedure
.input(ZViewRecoveryCodesMutationSchema) .input(ZViewRecoveryCodesMutationSchema)
.mutation(async ({ ctx, input }) => { .mutation(async ({ ctx, input }) => {
try { return await viewBackupCodes({
return await viewBackupCodes({ user: ctx.user,
user: ctx.user, token: input.token,
token: input.token, });
});
} catch (err) {
const error = AppError.parseError(err);
if (error.code !== 'INCORRECT_TWO_FACTOR_CODE') {
console.error(err);
}
throw error;
}
}), }),
}); });

View File

@@ -1,5 +1,3 @@
import { TRPCError } from '@trpc/server';
import { createWebhook } from '@documenso/lib/server-only/webhooks/create-webhook'; import { createWebhook } from '@documenso/lib/server-only/webhooks/create-webhook';
import { deleteWebhookById } from '@documenso/lib/server-only/webhooks/delete-webhook-by-id'; import { deleteWebhookById } from '@documenso/lib/server-only/webhooks/delete-webhook-by-id';
import { editWebhook } from '@documenso/lib/server-only/webhooks/edit-webhook'; import { editWebhook } from '@documenso/lib/server-only/webhooks/edit-webhook';
@@ -18,16 +16,7 @@ import {
export const webhookRouter = router({ export const webhookRouter = router({
getWebhooks: authenticatedProcedure.query(async ({ ctx }) => { getWebhooks: authenticatedProcedure.query(async ({ ctx }) => {
try { return await getWebhooksByUserId(ctx.user.id);
return await getWebhooksByUserId(ctx.user.id);
} catch (err) {
console.error(err);
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'We were unable to fetch your webhooks. Please try again later.',
});
}
}), }),
getTeamWebhooks: authenticatedProcedure getTeamWebhooks: authenticatedProcedure
@@ -35,37 +24,19 @@ export const webhookRouter = router({
.query(async ({ ctx, input }) => { .query(async ({ ctx, input }) => {
const { teamId } = input; const { teamId } = input;
try { return await getWebhooksByTeamId(teamId, ctx.user.id);
return await getWebhooksByTeamId(teamId, ctx.user.id);
} catch (err) {
console.error(err);
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'We were unable to fetch your webhooks. Please try again later.',
});
}
}), }),
getWebhookById: authenticatedProcedure getWebhookById: authenticatedProcedure
.input(ZGetWebhookByIdQuerySchema) .input(ZGetWebhookByIdQuerySchema)
.query(async ({ input, ctx }) => { .query(async ({ input, ctx }) => {
try { const { id, teamId } = input;
const { id, teamId } = input;
return await getWebhookById({ return await getWebhookById({
id, id,
userId: ctx.user.id, userId: ctx.user.id,
teamId, teamId,
}); });
} catch (err) {
console.error(err);
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'We were unable to fetch your webhook. Please try again later.',
});
}
}), }),
createWebhook: authenticatedProcedure createWebhook: authenticatedProcedure
@@ -73,65 +44,38 @@ export const webhookRouter = router({
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
const { enabled, eventTriggers, secret, webhookUrl, teamId } = input; const { enabled, eventTriggers, secret, webhookUrl, teamId } = input;
try { return await createWebhook({
return await createWebhook({ enabled,
enabled, secret,
secret, webhookUrl,
webhookUrl, eventTriggers,
eventTriggers, teamId,
teamId, userId: ctx.user.id,
userId: ctx.user.id, });
});
} catch (err) {
console.error(err);
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'We were unable to create this webhook. Please try again later.',
});
}
}), }),
deleteWebhook: authenticatedProcedure deleteWebhook: authenticatedProcedure
.input(ZDeleteWebhookMutationSchema) .input(ZDeleteWebhookMutationSchema)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
try { const { id, teamId } = input;
const { id, teamId } = input;
return await deleteWebhookById({ return await deleteWebhookById({
id, id,
teamId, teamId,
userId: ctx.user.id, userId: ctx.user.id,
}); });
} catch (err) {
console.error(err);
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'We were unable to create this webhook. Please try again later.',
});
}
}), }),
editWebhook: authenticatedProcedure editWebhook: authenticatedProcedure
.input(ZEditWebhookMutationSchema) .input(ZEditWebhookMutationSchema)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
try { const { id, teamId, ...data } = input;
const { id, teamId, ...data } = input;
return await editWebhook({ return await editWebhook({
id, id,
data, data,
userId: ctx.user.id, userId: ctx.user.id,
teamId, teamId,
}); });
} catch (err) {
console.error(err);
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'We were unable to create this webhook. Please try again later.',
});
}
}), }),
}); });