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

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

View File

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

View File

@ -8,6 +8,7 @@ import { IdentityProvider, TeamMemberInviteStatus } from '@documenso/prisma/clie
import { IS_BILLING_ENABLED } from '../../constants/app';
import { SALT_ROUNDS } from '../../constants/auth';
import { AppError, AppErrorCode } from '../../errors/app-error';
import { buildLogger } from '../../utils/logger';
export interface CreateUserOptions {
name: string;
@ -27,7 +28,7 @@ export const createUser = async ({ name, email, password, signature, url }: Crea
});
if (userExists) {
throw new Error('User already exists');
throw new AppError(AppErrorCode.ALREADY_EXISTS);
}
if (url) {
@ -134,6 +135,18 @@ export const createUser = async ({ name, email, password, signature, url }: Crea
return await getStripeCustomerByUser(user).then((session) => session.user);
} catch (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 { SALT_ROUNDS } from '../../constants/auth';
import { AppError, AppErrorCode } from '../../errors/app-error';
import type { RequestMetadata } from '../../universal/extract-request-metadata';
import { sendResetPassword } from '../auth/send-reset-password';
@ -15,7 +16,7 @@ export type ResetPasswordOptions = {
export const resetPassword = async ({ token, password, requestMetadata }: ResetPasswordOptions) => {
if (!token) {
throw new Error('Invalid token provided. Please try again.');
throw new AppError('INVALID_TOKEN');
}
const foundToken = await prisma.passwordResetToken.findFirst({
@ -28,19 +29,19 @@ export const resetPassword = async ({ token, password, requestMetadata }: ResetP
});
if (!foundToken) {
throw new Error('Invalid token provided. Please try again.');
throw new AppError('INVALID_TOKEN');
}
const now = new Date();
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 || '');
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);

View File

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

View File

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

View File

@ -1,5 +1,3 @@
import { TRPCError } from '@trpc/server';
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 { getUserTokens } from '@documenso/lib/server-only/public-api/get-all-user-tokens';
@ -14,78 +12,42 @@ import {
export const apiTokenRouter = router({
getTokens: authenticatedProcedure.query(async ({ ctx }) => {
try {
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.',
});
}
return await getUserTokens({ userId: ctx.user.id });
}),
getTokenById: authenticatedProcedure
.input(ZGetApiTokenByIdQuerySchema)
.query(async ({ input, ctx }) => {
try {
const { id } = input;
const { id } = input;
return await getApiTokenById({
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.',
});
}
return await getApiTokenById({
id,
userId: ctx.user.id,
});
}),
createToken: authenticatedProcedure
.input(ZCreateTokenMutationSchema)
.mutation(async ({ input, ctx }) => {
try {
const { tokenName, teamId, expirationDate } = input;
const { tokenName, teamId, expirationDate } = input;
return await createApiToken({
userId: ctx.user.id,
teamId,
tokenName,
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.',
});
}
return await createApiToken({
userId: ctx.user.id,
teamId,
tokenName,
expiresIn: expirationDate,
});
}),
deleteTokenById: authenticatedProcedure
.input(ZDeleteTokenByIdMutationSchema)
.mutation(async ({ input, ctx }) => {
try {
const { id, teamId } = input;
const { id, teamId } = input;
return await deleteTokenById({
id,
teamId,
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.',
});
}
return await deleteTokenById({
id,
teamId,
userId: ctx.user.id,
});
}),
});

View File

@ -33,53 +33,30 @@ const NEXT_PUBLIC_DISABLE_SIGNUP = () => env('NEXT_PUBLIC_DISABLE_SIGNUP');
export const authRouter = router({
signup: procedure.input(ZSignUpMutationSchema).mutation(async ({ input }) => {
try {
if (NEXT_PUBLIC_DISABLE_SIGNUP() === 'true') {
throw new TRPCError({
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,
if (NEXT_PUBLIC_DISABLE_SIGNUP() === 'true') {
throw new AppError('SIGNUP_DISABLED', {
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;
}),
verifyPassword: authenticatedProcedure
@ -104,56 +81,30 @@ export const authRouter = router({
createPasskey: authenticatedProcedure
.input(ZCreatePasskeyMutationSchema)
.mutation(async ({ ctx, input }) => {
try {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const verificationResponse = input.verificationResponse as RegistrationResponseJSON;
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const verificationResponse = input.verificationResponse as RegistrationResponseJSON;
return await createPasskey({
userId: ctx.user.id,
verificationResponse,
passkeyName: input.passkeyName,
requestMetadata: extractNextApiRequestMetadata(ctx.req),
});
} catch (err) {
console.error(err);
throw err;
}
return await createPasskey({
userId: ctx.user.id,
verificationResponse,
passkeyName: input.passkeyName,
requestMetadata: extractNextApiRequestMetadata(ctx.req),
});
}),
createPasskeyAuthenticationOptions: authenticatedProcedure
.input(ZCreatePasskeyAuthenticationOptionsMutationSchema)
.mutation(async ({ ctx, input }) => {
try {
return await createPasskeyAuthenticationOptions({
userId: ctx.user.id,
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.',
});
}
return await createPasskeyAuthenticationOptions({
userId: ctx.user.id,
preferredPasskeyId: input?.preferredPasskeyId,
});
}),
createPasskeyRegistrationOptions: authenticatedProcedure.mutation(async ({ ctx }) => {
try {
return await createPasskeyRegistrationOptions({
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.',
});
}
return await createPasskeyRegistrationOptions({
userId: ctx.user.id,
});
}),
createPasskeySigninOptions: procedure.mutation(async ({ ctx }) => {
@ -168,80 +119,44 @@ export const authRouter = router({
const [sessionId] = decodeURI(sessionIdToken).split('|');
try {
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.',
});
}
return await createPasskeySigninOptions({ sessionId });
}),
deletePasskey: authenticatedProcedure
.input(ZDeletePasskeyMutationSchema)
.mutation(async ({ ctx, input }) => {
try {
const { passkeyId } = input;
const { passkeyId } = input;
await deletePasskey({
userId: ctx.user.id,
passkeyId,
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.',
});
}
await deletePasskey({
userId: ctx.user.id,
passkeyId,
requestMetadata: extractNextApiRequestMetadata(ctx.req),
});
}),
findPasskeys: authenticatedProcedure
.input(ZFindPasskeysQuerySchema)
.query(async ({ input, ctx }) => {
try {
const { page, perPage, orderBy } = input;
const { page, perPage, orderBy } = input;
return await findPasskeys({
page,
perPage,
orderBy,
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.',
});
}
return await findPasskeys({
page,
perPage,
orderBy,
userId: ctx.user.id,
});
}),
updatePasskey: authenticatedProcedure
.input(ZUpdatePasskeyMutationSchema)
.mutation(async ({ ctx, input }) => {
try {
const { passkeyId, name } = input;
const { passkeyId, name } = input;
await updatePasskey({
userId: ctx.user.id,
passkeyId,
name,
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.',
});
}
await updatePasskey({
userId: ctx.user.id,
passkeyId,
name,
requestMetadata: extractNextApiRequestMetadata(ctx.req),
});
}),
});

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
.input(ZGetDocumentByIdQuerySchema)
.query(async ({ input, ctx }) => {
try {
return await getDocumentById({
...input,
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.',
});
}
return await getDocumentById({
...input,
userId: ctx.user.id,
});
}),
getDocumentByToken: procedure
.input(ZGetDocumentByTokenQuerySchema)
.query(async ({ input, ctx }) => {
try {
const { token } = input;
const { token } = input;
return await getDocumentAndSenderByToken({
token,
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.',
});
}
return await getDocumentAndSenderByToken({
token,
userId: ctx.user?.id,
});
}),
getDocumentWithDetailsById: authenticatedProcedure
.input(ZGetDocumentWithDetailsByIdQuerySchema)
.query(async ({ input, ctx }) => {
try {
return await getDocumentWithDetailsById({
...input,
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.',
});
}
return await getDocumentWithDetailsById({
...input,
userId: ctx.user.id,
});
}),
createDocument: authenticatedProcedure
.input(ZCreateDocumentMutationSchema)
.mutation(async ({ input, ctx }) => {
try {
const { title, documentDataId, teamId } = input;
const { title, documentDataId, teamId } = input;
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;
}
const { remaining } = await getServerLimits({ email: ctx.user.email, teamId });
if (remaining.documents <= 0) {
throw new TRPCError({
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
.input(ZDeleteDocumentMutationSchema)
.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({
id,
userId,
teamId,
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.',
});
}
return await deleteDocument({
id,
userId,
teamId,
requestMetadata: extractNextApiRequestMetadata(ctx.req),
});
}),
moveDocumentToTeam: authenticatedProcedure
.input(ZMoveDocumentsToTeamSchema)
.mutation(async ({ input, ctx }) => {
try {
const { documentId, teamId } = input;
const userId = ctx.user.id;
const { documentId, teamId } = input;
const userId = ctx.user.id;
return await moveDocumentToTeam({
documentId,
teamId,
userId,
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.',
});
}
return await moveDocumentToTeam({
documentId,
teamId,
userId,
requestMetadata: extractNextApiRequestMetadata(ctx.req),
});
}),
findDocuments: authenticatedProcedure
@ -199,94 +136,66 @@ export const documentRouter = router({
const { search, teamId, templateId, page, perPage, orderBy, source, status } = input;
try {
const documents = await findDocuments({
userId: user.id,
teamId,
templateId,
search,
source,
status,
page,
perPage,
orderBy,
});
const documents = await findDocuments({
userId: user.id,
teamId,
templateId,
search,
source,
status,
page,
perPage,
orderBy,
});
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.',
});
}
return documents;
}),
findDocumentAuditLogs: authenticatedProcedure
.input(ZFindDocumentAuditLogsQuerySchema)
.query(async ({ input, ctx }) => {
try {
const { page, perPage, documentId, cursor, filterForRecentActivity, orderBy } = input;
const { page, perPage, documentId, cursor, filterForRecentActivity, orderBy } = input;
return await findDocumentAuditLogs({
page,
perPage,
documentId,
cursor,
filterForRecentActivity,
orderBy,
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.',
});
}
return await findDocumentAuditLogs({
page,
perPage,
documentId,
cursor,
filterForRecentActivity,
orderBy,
userId: ctx.user.id,
});
}),
// Todo: Add API
setSettingsForDocument: authenticatedProcedure
.input(ZSetSettingsForDocumentMutationSchema)
.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) {
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,
if (meta.timezone || meta.dateFormat || meta.redirectUrl) {
await upsertDocumentMeta({
documentId,
data,
dateFormat: meta.dateFormat,
timezone: meta.timezone,
redirectUrl: meta.redirectUrl,
language: meta.language,
userId: ctx.user.id,
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
@ -296,197 +205,131 @@ export const documentRouter = router({
const userId = ctx.user.id;
try {
return await updateTitle({
title,
userId,
teamId,
documentId,
requestMetadata: extractNextApiRequestMetadata(ctx.req),
});
} catch (err) {
console.error(err);
throw err;
}
return await updateTitle({
title,
userId,
teamId,
documentId,
requestMetadata: extractNextApiRequestMetadata(ctx.req),
});
}),
setPasswordForDocument: authenticatedProcedure
.input(ZSetPasswordForDocumentMutationSchema)
.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) {
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.',
});
if (!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),
});
}),
setSigningOrderForDocument: authenticatedProcedure
.input(ZSetSigningOrderForDocumentMutationSchema)
.mutation(async ({ input, ctx }) => {
try {
const { documentId, signingOrder } = input;
const { documentId, signingOrder } = input;
return await upsertDocumentMeta({
documentId,
signingOrder,
userId: ctx.user.id,
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.',
});
}
return await upsertDocumentMeta({
documentId,
signingOrder,
userId: ctx.user.id,
requestMetadata: extractNextApiRequestMetadata(ctx.req),
});
}),
updateTypedSignatureSettings: authenticatedProcedure
.input(ZUpdateTypedSignatureSettingsMutationSchema)
.mutation(async ({ input, ctx }) => {
try {
const { documentId, teamId, typedSignatureEnabled } = input;
const { documentId, teamId, typedSignatureEnabled } = input;
const document = await getDocumentById({
id: documentId,
teamId,
userId: ctx.user.id,
}).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;
}
const document = await getDocumentById({
id: documentId,
teamId,
userId: ctx.user.id,
}).catch(() => null);
if (!document) {
throw new TRPCError({
code: 'BAD_REQUEST',
message:
'We were unable to update the settings for this document. Please try again later.',
code: 'NOT_FOUND',
message: 'Document not found',
});
}
return await upsertDocumentMeta({
documentId,
typedSignatureEnabled,
userId: ctx.user.id,
requestMetadata: extractNextApiRequestMetadata(ctx.req),
});
}),
sendDocument: authenticatedProcedure
.input(ZSendDocumentMutationSchema)
.mutation(async ({ input, ctx }) => {
try {
const { documentId, teamId, meta } = input;
const { documentId, teamId, meta } = input;
if (
meta.message ||
meta.subject ||
meta.timezone ||
meta.dateFormat ||
meta.redirectUrl ||
meta.distributionMethod ||
meta.emailSettings
) {
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,
if (
meta.message ||
meta.subject ||
meta.timezone ||
meta.dateFormat ||
meta.redirectUrl ||
meta.distributionMethod ||
meta.emailSettings
) {
await upsertDocumentMeta({
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),
});
} 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
.input(ZResendDocumentMutationSchema)
.mutation(async ({ input, ctx }) => {
try {
return await resendDocument({
userId: ctx.user.id,
...input,
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.',
});
}
return await resendDocument({
userId: ctx.user.id,
...input,
requestMetadata: extractNextApiRequestMetadata(ctx.req),
});
}),
duplicateDocument: authenticatedProcedure
.input(ZGetDocumentByIdQuerySchema)
.mutation(async ({ input, ctx }) => {
try {
return await duplicateDocumentById({
userId: ctx.user.id,
...input,
});
} catch (err) {
console.log(err);
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'We are unable to duplicate this document. Please try again later.',
});
}
return await duplicateDocumentById({
userId: ctx.user.id,
...input,
});
}),
searchDocuments: authenticatedProcedure
@ -494,93 +337,64 @@ export const documentRouter = router({
.query(async ({ input, ctx }) => {
const { query } = input;
try {
const documents = await searchDocumentsWithKeyword({
query,
userId: ctx.user.id,
});
const documents = await searchDocumentsWithKeyword({
query,
userId: ctx.user.id,
});
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.',
});
}
return documents;
}),
downloadAuditLogs: authenticatedProcedure
.input(ZDownloadAuditLogsMutationSchema)
.mutation(async ({ input, ctx }) => {
try {
const { documentId, teamId } = input;
const { documentId, teamId } = input;
const document = await getDocumentById({
id: documentId,
userId: ctx.user.id,
teamId,
}).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);
const document = await getDocumentById({
id: documentId,
userId: ctx.user.id,
teamId,
}).catch(() => null);
if (!document || (teamId && document.teamId !== teamId)) {
throw new TRPCError({
code: 'BAD_REQUEST',
message:
'We were unable to download the audit logs for this document. Please try again later.',
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}`,
};
}),
downloadCertificate: authenticatedProcedure
.input(ZDownloadCertificateMutationSchema)
.mutation(async ({ input, ctx }) => {
try {
const { documentId, teamId } = input;
const { documentId, teamId } = input;
const document = await getDocumentById({
id: documentId,
userId: ctx.user.id,
teamId,
});
const document = await getDocumentById({
id: documentId,
userId: ctx.user.id,
teamId,
});
if (document.status !== DocumentStatus.COMPLETED) {
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.',
});
if (document.status !== DocumentStatus.COMPLETED) {
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}`,
};
}),
});

View File

@ -1,5 +1,3 @@
import { TRPCError } from '@trpc/server';
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 { setFieldsForDocument } from '@documenso/lib/server-only/field/set-fields-for-document';
@ -20,33 +18,24 @@ export const fieldRouter = router({
addFields: authenticatedProcedure
.input(ZAddFieldsMutationSchema)
.mutation(async ({ input, ctx }) => {
try {
const { documentId, fields } = input;
const { documentId, fields } = input;
return await setFieldsForDocument({
documentId,
userId: ctx.user.id,
fields: fields.map((field) => ({
id: field.nativeId,
signerEmail: field.signerEmail,
type: field.type,
pageNumber: field.pageNumber,
pageX: field.pageX,
pageY: field.pageY,
pageWidth: field.pageWidth,
pageHeight: field.pageHeight,
fieldMeta: field.fieldMeta,
})),
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.',
});
}
return await setFieldsForDocument({
documentId,
userId: ctx.user.id,
fields: fields.map((field) => ({
id: field.nativeId,
signerEmail: field.signerEmail,
type: field.type,
pageNumber: field.pageNumber,
pageX: field.pageX,
pageY: field.pageY,
pageWidth: field.pageWidth,
pageHeight: field.pageHeight,
fieldMeta: field.fieldMeta,
})),
requestMetadata: extractNextApiRequestMetadata(ctx.req),
});
}),
addTemplateFields: authenticatedProcedure
@ -54,89 +43,59 @@ export const fieldRouter = router({
.mutation(async ({ input, ctx }) => {
const { templateId, fields } = input;
try {
return await setFieldsForTemplate({
userId: ctx.user.id,
templateId,
fields: fields.map((field) => ({
id: field.nativeId,
signerEmail: field.signerEmail,
type: field.type,
pageNumber: field.pageNumber,
pageX: field.pageX,
pageY: field.pageY,
pageWidth: field.pageWidth,
pageHeight: field.pageHeight,
fieldMeta: field.fieldMeta,
})),
});
} catch (err) {
console.error(err);
throw err;
}
return await setFieldsForTemplate({
userId: ctx.user.id,
templateId,
fields: fields.map((field) => ({
id: field.nativeId,
signerEmail: field.signerEmail,
type: field.type,
pageNumber: field.pageNumber,
pageX: field.pageX,
pageY: field.pageY,
pageWidth: field.pageWidth,
pageHeight: field.pageHeight,
fieldMeta: field.fieldMeta,
})),
});
}),
signFieldWithToken: procedure
.input(ZSignFieldWithTokenMutationSchema)
.mutation(async ({ input, ctx }) => {
try {
const { token, fieldId, value, isBase64, authOptions } = input;
const { token, fieldId, value, isBase64, authOptions } = input;
return await signFieldWithToken({
token,
fieldId,
value,
isBase64,
userId: ctx.user?.id,
authOptions,
requestMetadata: extractNextApiRequestMetadata(ctx.req),
});
} catch (err) {
console.error(err);
throw err;
}
return await signFieldWithToken({
token,
fieldId,
value,
isBase64,
userId: ctx.user?.id,
authOptions,
requestMetadata: extractNextApiRequestMetadata(ctx.req),
});
}),
removeSignedFieldWithToken: procedure
.input(ZRemovedSignedFieldWithTokenMutationSchema)
.mutation(async ({ input, ctx }) => {
try {
const { token, fieldId } = input;
const { token, fieldId } = input;
return await removeSignedFieldWithToken({
token,
fieldId,
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.',
});
}
return await removeSignedFieldWithToken({
token,
fieldId,
requestMetadata: extractNextApiRequestMetadata(ctx.req),
});
}),
getField: authenticatedProcedure.input(ZGetFieldQuerySchema).query(async ({ input, ctx }) => {
try {
const { fieldId, teamId } = input;
const { fieldId, teamId } = input;
return await getFieldById({
userId: ctx.user.id,
teamId,
fieldId,
});
} catch (err) {
console.error(err);
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'We were unable to find this field. Please try again.',
});
}
return await getFieldById({
userId: ctx.user.id,
teamId,
fieldId,
});
}),
// 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 { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { jobsClient } from '@documenso/lib/jobs/client';
@ -33,246 +31,122 @@ export const profileRouter = router({
findUserSecurityAuditLogs: authenticatedProcedure
.input(ZFindUserSecurityAuditLogsSchema)
.query(async ({ input, ctx }) => {
try {
return await findUserSecurityAuditLogs({
userId: ctx.user.id,
...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.',
});
}
return await findUserSecurityAuditLogs({
userId: ctx.user.id,
...input,
});
}),
getUser: adminProcedure.input(ZRetrieveUserByIdQuerySchema).query(async ({ input }) => {
try {
const { id } = input;
const { id } = input;
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.',
});
}
return await getUserById({ id });
}),
updateProfile: authenticatedProcedure
.input(ZUpdateProfileMutationSchema)
.mutation(async ({ input, ctx }) => {
try {
const { name, signature } = input;
const { name, signature } = input;
return await updateProfile({
userId: ctx.user.id,
name,
signature,
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.',
});
}
return await updateProfile({
userId: ctx.user.id,
name,
signature,
requestMetadata: extractNextApiRequestMetadata(ctx.req),
});
}),
updatePublicProfile: authenticatedProcedure
.input(ZUpdatePublicProfileMutationSchema)
.mutation(async ({ input, ctx }) => {
try {
const { url, bio, enabled } = input;
const { url, bio, enabled } = input;
if (IS_BILLING_ENABLED() && url !== undefined && url.length < 6) {
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({
if (IS_BILLING_ENABLED() && url !== undefined && url.length < 6) {
const subscriptions = await getSubscriptionsByUserId({
userId: ctx.user.id,
data: {
url,
bio,
enabled,
},
});
}).then((subscriptions) =>
subscriptions.filter((s) => s.status === SubscriptionStatus.ACTIVE),
);
return { success: true, url: user.url };
} catch (err) {
console.error(err);
const error = AppError.parseError(err);
if (error.code !== AppErrorCode.UNKNOWN_ERROR) {
throw error;
if (subscriptions.length === 0) {
throw new AppError(AppErrorCode.PREMIUM_PROFILE_URL, {
message: 'Only subscribers can have a username shorter than 6 characters',
});
}
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
.input(ZUpdatePasswordMutationSchema)
.mutation(async ({ input, ctx }) => {
try {
const { password, currentPassword } = input;
const { password, currentPassword } = input;
return await updatePassword({
userId: ctx.user.id,
password,
currentPassword,
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,
});
}
return await updatePassword({
userId: ctx.user.id,
password,
currentPassword,
requestMetadata: extractNextApiRequestMetadata(ctx.req),
});
}),
forgotPassword: procedure.input(ZForgotPasswordFormSchema).mutation(async ({ input }) => {
try {
const { email } = input;
const { email } = input;
return await forgotPassword({
email,
});
} catch (err) {
console.error(err);
}
return await forgotPassword({
email,
});
}),
resetPassword: procedure.input(ZResetPasswordFormSchema).mutation(async ({ input, ctx }) => {
try {
const { password, token } = input;
const { password, token } = input;
return await resetPassword({
token,
password,
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,
});
}
return await resetPassword({
token,
password,
requestMetadata: extractNextApiRequestMetadata(ctx.req),
});
}),
sendConfirmationEmail: procedure
.input(ZConfirmEmailMutationSchema)
.mutation(async ({ input }) => {
try {
const { email } = input;
const { email } = input;
await jobsClient.triggerJob({
name: 'send.signup.confirmation.email',
payload: {
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,
});
}
await jobsClient.triggerJob({
name: 'send.signup.confirmation.email',
payload: {
email,
},
});
}),
deleteAccount: authenticatedProcedure.mutation(async ({ ctx }) => {
try {
return await deleteUser({
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,
});
}
return await deleteUser({
id: ctx.user.id,
});
}),
setProfileImage: authenticatedProcedure
.input(ZSetProfileImageMutationSchema)
.mutation(async ({ input, ctx }) => {
try {
const { bytes, teamId } = input;
const { bytes, teamId } = input;
return await setAvatarImage({
userId: ctx.user.id,
teamId,
bytes,
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,
});
}
return await setAvatarImage({
userId: ctx.user.id,
teamId,
bytes,
requestMetadata: extractNextApiRequestMetadata(ctx.req),
});
}),
});

View File

@ -1,5 +1,3 @@
import { TRPCError } from '@trpc/server';
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 { setRecipientsForDocument } from '@documenso/lib/server-only/recipient/set-recipients-for-document';
@ -18,104 +16,68 @@ export const recipientRouter = router({
addSigners: authenticatedProcedure
.input(ZAddSignersMutationSchema)
.mutation(async ({ input, ctx }) => {
try {
const { documentId, teamId, signers } = input;
const { documentId, teamId, signers } = input;
return await setRecipientsForDocument({
userId: ctx.user.id,
documentId,
teamId,
recipients: signers.map((signer) => ({
id: signer.nativeId,
email: signer.email,
name: signer.name,
role: signer.role,
signingOrder: signer.signingOrder,
actionAuth: signer.actionAuth,
})),
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.',
});
}
return await setRecipientsForDocument({
userId: ctx.user.id,
documentId,
teamId,
recipients: signers.map((signer) => ({
id: signer.nativeId,
email: signer.email,
name: signer.name,
role: signer.role,
signingOrder: signer.signingOrder,
actionAuth: signer.actionAuth,
})),
requestMetadata: extractNextApiRequestMetadata(ctx.req),
});
}),
addTemplateSigners: authenticatedProcedure
.input(ZAddTemplateSignersMutationSchema)
.mutation(async ({ input, ctx }) => {
try {
const { templateId, signers, teamId } = input;
const { templateId, signers, teamId } = input;
return await setRecipientsForTemplate({
userId: ctx.user.id,
teamId,
templateId,
recipients: signers.map((signer) => ({
id: signer.nativeId,
email: signer.email,
name: signer.name,
role: signer.role,
signingOrder: signer.signingOrder,
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.',
});
}
return await setRecipientsForTemplate({
userId: ctx.user.id,
teamId,
templateId,
recipients: signers.map((signer) => ({
id: signer.nativeId,
email: signer.email,
name: signer.name,
role: signer.role,
signingOrder: signer.signingOrder,
actionAuth: signer.actionAuth,
})),
});
}),
completeDocumentWithToken: procedure
.input(ZCompleteDocumentWithTokenMutationSchema)
.mutation(async ({ input, ctx }) => {
try {
const { token, documentId, authOptions } = input;
const { token, documentId, authOptions } = input;
return await completeDocumentWithToken({
token,
documentId,
authOptions,
userId: ctx.user?.id,
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.',
});
}
return await completeDocumentWithToken({
token,
documentId,
authOptions,
userId: ctx.user?.id,
requestMetadata: extractNextApiRequestMetadata(ctx.req),
});
}),
rejectDocumentWithToken: procedure
.input(ZRejectDocumentWithTokenMutationSchema)
.mutation(async ({ input, ctx }) => {
try {
const { token, documentId, reason } = input;
const { token, documentId, reason } = input;
return await rejectDocumentWithToken({
token,
documentId,
reason,
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.',
});
}
return await rejectDocumentWithToken({
token,
documentId,
reason,
requestMetadata: extractNextApiRequestMetadata(ctx.req),
});
}),
});

View File

@ -1,7 +1,6 @@
import { adminRouter } from './admin-router/router';
import { apiTokenRouter } from './api-token-router/router';
import { authRouter } from './auth-router/router';
import { cryptoRouter } from './crypto/router';
import { documentRouter } from './document-router/router';
import { fieldRouter } from './field-router/router';
import { profileRouter } from './profile-router/router';
@ -16,7 +15,6 @@ import { webhookRouter } from './webhook-router/router';
export const appRouter = router({
auth: authRouter,
crypto: cryptoRouter,
profile: profileRouter,
document: documentRouter,
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 { procedure, router } from '../trpc';
@ -9,27 +7,18 @@ export const shareLinkRouter = router({
createOrGetShareLink: procedure
.input(ZCreateOrGetShareLinkMutationSchema)
.mutation(async ({ ctx, input }) => {
try {
const { documentId, token } = input;
const { documentId, token } = input;
if (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 (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 });
}),
});

View File

@ -30,159 +30,153 @@ export const singleplayerRouter = router({
createSinglePlayerDocument: procedure
.input(ZCreateSinglePlayerDocumentMutationSchema)
.mutation(async ({ input }) => {
try {
const { signer, fields, documentData, documentName, fieldMeta } = input;
const { signer, fields, documentData, documentName, fieldMeta } = input;
const document = await getFile({
data: documentData.data,
type: documentData.type,
const document = await getFile({
data: documentData.data,
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 signatureImageAsBase64 = isBase64 ? signer.signature : null;
const typedSignature = !isBase64 ? signer.signature : null;
const { token } = await prisma.$transaction(
async (tx) => {
const token = alphaid();
// 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,
// Fetch service user who will be the owner of the document.
const serviceUser = await tx.user.findFirstOrThrow({
where: {
email: SERVICE_USER_EMAIL,
},
});
}
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(
async (tx) => {
const token = alphaid();
// 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,
},
});
// Fetch service user who will be the owner of the document.
const serviceUser = await tx.user.findFirstOrThrow({
where: {
email: SERVICE_USER_EMAIL,
},
});
// Create fields and signatures.
await Promise.all(
fields.map(async (field) => {
const insertedField = await tx.field.create({
data: {
documentId: document.id,
recipientId: recipient.id,
...mapField(field, signer),
},
});
const { id: documentDataId } = await putPdfFile({
name: `${documentName}.pdf`,
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({
if (field.type === FieldType.SIGNATURE || field.type === FieldType.FREE_SIGNATURE) {
await tx.signature.create({
data: {
documentId: document.id,
fieldId: insertedField.id,
signatureImageAsBase64,
typedSignature,
recipientId: recipient.id,
...mapField(field, signer),
},
});
}
}),
);
if (field.type === FieldType.SIGNATURE || field.type === FieldType.FREE_SIGNATURE) {
await tx.signature.create({
data: {
fieldId: insertedField.id,
signatureImageAsBase64,
typedSignature,
recipientId: recipient.id,
},
});
}
}),
);
return { document, token };
},
{
maxWait: 5000,
timeout: 30000,
},
);
return { document, token };
},
{
maxWait: 5000,
timeout: 30000,
},
);
const template = createElement(DocumentSelfSignedEmailTemplate, {
documentName: documentName,
assetBaseUrl: NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000',
});
const template = createElement(DocumentSelfSignedEmailTemplate, {
documentName: documentName,
assetBaseUrl: NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000',
});
const [html, text] = await Promise.all([
renderEmailWithI18N(template),
renderEmailWithI18N(template, { plainText: true }),
]);
const [html, text] = await Promise.all([
renderEmailWithI18N(template),
renderEmailWithI18N(template, { plainText: true }),
]);
// Send email to signer.
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 }],
});
// Send email to signer.
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;
}
return token;
}),
});

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 { enableTwoFactorAuthentication } from '@documenso/lib/server-only/2fa/enable-2fa';
import { setupTwoFactorAuthentication } from '@documenso/lib/server-only/2fa/setup-2fa';
@ -16,89 +13,44 @@ import {
export const twoFactorAuthenticationRouter = router({
setup: authenticatedProcedure.mutation(async ({ ctx }) => {
try {
return await setupTwoFactorAuthentication({
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.',
});
}
return await setupTwoFactorAuthentication({
user: ctx.user,
});
}),
enable: authenticatedProcedure
.input(ZEnableTwoFactorAuthenticationMutationSchema)
.mutation(async ({ ctx, input }) => {
try {
const user = ctx.user;
const user = ctx.user;
const { code } = input;
const { code } = input;
return await enableTwoFactorAuthentication({
user,
code,
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.',
});
}
return await enableTwoFactorAuthentication({
user,
code,
requestMetadata: extractNextApiRequestMetadata(ctx.req),
});
}),
disable: authenticatedProcedure
.input(ZDisableTwoFactorAuthenticationMutationSchema)
.mutation(async ({ ctx, input }) => {
try {
const user = ctx.user;
const user = ctx.user;
return await disableTwoFactorAuthentication({
user,
totpCode: input.totpCode,
backupCode: input.backupCode,
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.',
});
}
return await disableTwoFactorAuthentication({
user,
totpCode: input.totpCode,
backupCode: input.backupCode,
requestMetadata: extractNextApiRequestMetadata(ctx.req),
});
}),
viewRecoveryCodes: authenticatedProcedure
.input(ZViewRecoveryCodesMutationSchema)
.mutation(async ({ ctx, input }) => {
try {
return await viewBackupCodes({
user: ctx.user,
token: input.token,
});
} catch (err) {
const error = AppError.parseError(err);
if (error.code !== 'INCORRECT_TWO_FACTOR_CODE') {
console.error(err);
}
throw error;
}
return await viewBackupCodes({
user: ctx.user,
token: input.token,
});
}),
});

View File

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