feat: add initial api logging (#1494)

Improve API logging and error handling between client and server side.
This commit is contained in:
David Nguyen
2024-11-28 16:05:37 +07:00
committed by GitHub
parent 04293968c6
commit 98d85b086d
53 changed files with 933 additions and 780 deletions

View File

@@ -139,3 +139,6 @@ E2E_TEST_AUTHENTICATE_USER_PASSWORD="test_Password123"
# [[REDIS]]
NEXT_PRIVATE_REDIS_URL=
NEXT_PRIVATE_REDIS_TOKEN=
# [[LOGGER]]
NEXT_PRIVATE_LOGGER_HONEY_BADGER_API_KEY=

View File

@@ -1,3 +1,5 @@
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { buildLogger } from '@documenso/lib/utils/logger';
import * as trpcNext from '@documenso/trpc/server/adapters/next';
import { createTrpcContext } from '@documenso/trpc/server/context';
import { appRouter } from '@documenso/trpc/server/router';
@@ -11,7 +13,44 @@ export const config = {
},
};
const logger = buildLogger();
export default trpcNext.createNextApiHandler({
router: appRouter,
createContext: async ({ req, res }) => createTrpcContext({ req, res }),
onError(opts) {
const { error, path } = opts;
// Currently trialing changes with template and team router only.
if (!path || (!path.startsWith('template') && !path.startsWith('team'))) {
return;
}
// Always log the error for now.
console.error(error);
const appError = AppError.parseError(error.cause || error);
const isAppError = error.cause instanceof AppError;
// Only log AppErrors that are explicitly set to 500 or the error code
// is in the errorCodesToAlertOn list.
const isLoggableAppError =
isAppError && (appError.statusCode === 500 || errorCodesToAlertOn.includes(appError.code));
// Only log TRPC errors that are in the `errorCodesToAlertOn` list and is
// not an AppError.
const isLoggableTrpcError = !isAppError && errorCodesToAlertOn.includes(error.code);
if (isLoggableAppError || isLoggableTrpcError) {
logger.error(error, {
method: path,
context: {
appError: AppError.toJSON(appError),
},
});
}
},
});
const errorCodesToAlertOn = [AppErrorCode.UNKNOWN_ERROR, 'INTERNAL_SERVER_ERROR'];

136
package-lock.json generated
View File

@@ -3498,6 +3498,34 @@
"resolved": "https://registry.npmjs.org/@hexagon/base64/-/base64-1.1.28.tgz",
"integrity": "sha512-lhqDEAvWixy3bZ+UOYbPwUbBkwBq5C1LAJ/xPC8Oi+lL54oyakv/npbA0aU2hgCsx/1NUd4IBvV03+aUBWxerw=="
},
"node_modules/@honeybadger-io/core": {
"version": "6.6.0",
"resolved": "https://registry.npmjs.org/@honeybadger-io/core/-/core-6.6.0.tgz",
"integrity": "sha512-B5X05huAsDs7NJOYm4bwHf2v0tMuTjBWLfumHH9DCblq8E1XrujlbbNkIdEHlzc01K9oAXuvsaBwVkE7G5+aLQ==",
"dependencies": {
"json-nd": "^1.0.0",
"stacktrace-parser": "^0.1.10"
},
"engines": {
"node": ">=14"
}
},
"node_modules/@honeybadger-io/js": {
"version": "6.10.1",
"resolved": "https://registry.npmjs.org/@honeybadger-io/js/-/js-6.10.1.tgz",
"integrity": "sha512-T5WAhYHWHXFMxjY4NSawSY945i8ISIL5/gsjN3m0xO+oXrBAFaul3wY5p/FGH6r6RfCrjHoHl9Iu7Ed9aO9Ehg==",
"dependencies": {
"@honeybadger-io/core": "^6.6.0",
"@types/aws-lambda": "^8.10.89",
"@types/express": "^4.17.13"
},
"bin": {
"honeybadger-checkins-sync": "scripts/check-ins-sync-bin.js"
},
"engines": {
"node": ">=14"
}
},
"node_modules/@hookform/resolvers": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.3.2.tgz",
@@ -10956,6 +10984,20 @@
"@types/estree": "*"
}
},
"node_modules/@types/aws-lambda": {
"version": "8.10.146",
"resolved": "https://registry.npmjs.org/@types/aws-lambda/-/aws-lambda-8.10.146.tgz",
"integrity": "sha512-3BaDXYTh0e6UCJYL/jwV/3+GRslSc08toAiZSmleYtkAUyV5rtvdPYxrG/88uqvTuT6sb27WE9OS90ZNTIuQ0g=="
},
"node_modules/@types/body-parser": {
"version": "1.19.5",
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz",
"integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==",
"dependencies": {
"@types/connect": "*",
"@types/node": "*"
}
},
"node_modules/@types/cacheable-request": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz",
@@ -10968,6 +11010,14 @@
"@types/responselike": "^1.0.0"
}
},
"node_modules/@types/connect": {
"version": "3.4.38",
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
"integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/cookie": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz",
@@ -11090,6 +11140,28 @@
"@types/estree": "*"
}
},
"node_modules/@types/express": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz",
"integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==",
"dependencies": {
"@types/body-parser": "*",
"@types/express-serve-static-core": "^4.17.33",
"@types/qs": "*",
"@types/serve-static": "*"
}
},
"node_modules/@types/express-serve-static-core": {
"version": "4.19.6",
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz",
"integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==",
"dependencies": {
"@types/node": "*",
"@types/qs": "*",
"@types/range-parser": "*",
"@types/send": "*"
}
},
"node_modules/@types/formidable": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/@types/formidable/-/formidable-2.0.6.tgz",
@@ -11132,6 +11204,11 @@
"integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==",
"dev": true
},
"node_modules/@types/http-errors": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz",
"integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA=="
},
"node_modules/@types/istanbul-lib-coverage": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz",
@@ -11201,6 +11278,11 @@
"resolved": "https://registry.npmjs.org/@types/mdx/-/mdx-2.0.10.tgz",
"integrity": "sha512-Rllzc5KHk0Al5/WANwgSPl1/CwjqCy+AZrGd78zuK+jO9aDM6ffblZ+zIjgPNAaEBmlO0RYDvLNh7wD0zKVgEg=="
},
"node_modules/@types/mime": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
"integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w=="
},
"node_modules/@types/minimatch": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz",
@@ -11338,6 +11420,11 @@
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz",
"integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng=="
},
"node_modules/@types/qs": {
"version": "6.9.17",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.17.tgz",
"integrity": "sha512-rX4/bPcfmvxHDv0XjfJELTTr+iB+tn032nPILqHm5wbthUUUuVtNGGqzhya9XUxjTP8Fpr0qYgSZZKxGY++svQ=="
},
"node_modules/@types/ramda": {
"version": "0.29.9",
"resolved": "https://registry.npmjs.org/@types/ramda/-/ramda-0.29.9.tgz",
@@ -11346,6 +11433,11 @@
"types-ramda": "^0.29.6"
}
},
"node_modules/@types/range-parser": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz",
"integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="
},
"node_modules/@types/react": {
"version": "18.2.18",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.18.tgz",
@@ -11401,6 +11493,25 @@
"integrity": "sha512-T+YwkslhsM+CeuhYUxyAjWm7mJ5am/K10UX40RuA6k6Lc7eGtq8iY2xOzy7Vq0GOqhl/xZl5l2FwURZMTPTUww==",
"dev": true
},
"node_modules/@types/send": {
"version": "0.17.4",
"resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz",
"integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==",
"dependencies": {
"@types/mime": "^1",
"@types/node": "*"
}
},
"node_modules/@types/serve-static": {
"version": "1.15.7",
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz",
"integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==",
"dependencies": {
"@types/http-errors": "*",
"@types/node": "*",
"@types/send": "*"
}
},
"node_modules/@types/swagger-ui-react": {
"version": "4.18.3",
"resolved": "https://registry.npmjs.org/@types/swagger-ui-react/-/swagger-ui-react-4.18.3.tgz",
@@ -21011,6 +21122,11 @@
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
"integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="
},
"node_modules/json-nd": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-nd/-/json-nd-1.0.0.tgz",
"integrity": "sha512-8TIp0HZAY0VVrwRQJJPb4+nOTSPoOWZeEKBTLizUfQO4oym5Fc/MKqN8vEbLCxcyxDf2vwNxOQ1q84O49GWPyQ=="
},
"node_modules/json-parse-even-better-errors": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
@@ -31249,6 +31365,25 @@
"integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
"dev": true
},
"node_modules/stacktrace-parser": {
"version": "0.1.10",
"resolved": "https://registry.npmjs.org/stacktrace-parser/-/stacktrace-parser-0.1.10.tgz",
"integrity": "sha512-KJP1OCML99+8fhOHxwwzyWrlUuVX5GQ0ZpJTd1DFXhdkrvg1szxfHhawXUZ3g9TkXORQd4/WG68jMlQZ2p8wlg==",
"dependencies": {
"type-fest": "^0.7.1"
},
"engines": {
"node": ">=6"
}
},
"node_modules/stacktrace-parser/node_modules/type-fest": {
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.7.1.tgz",
"integrity": "sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==",
"engines": {
"node": ">=8"
}
},
"node_modules/stampit": {
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/stampit/-/stampit-4.3.2.tgz",
@@ -36484,6 +36619,7 @@
"@documenso/email": "*",
"@documenso/prisma": "*",
"@documenso/signing": "*",
"@honeybadger-io/js": "^6.10.1",
"@lingui/core": "^4.11.3",
"@lingui/macro": "^4.11.3",
"@lingui/react": "^4.11.3",

View File

@@ -13,7 +13,9 @@ export const getTeamPrices = async () => {
const priceIds = prices.map((price) => price.id);
if (!monthlyPrice || !yearlyPrice) {
throw new AppError('INVALID_CONFIG', 'Missing monthly or yearly price');
throw new AppError('INVALID_CONFIG', {
message: 'Missing monthly or yearly price',
});
}
return {

View File

@@ -43,7 +43,9 @@ export const transferTeamSubscription = async ({
const teamCustomerId = team.customerId;
if (!teamCustomerId) {
throw new AppError(AppErrorCode.NOT_FOUND, 'Missing customer ID.');
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Missing customer ID.',
});
}
const [teamRelatedPlanPriceIds, teamSeatPrices] = await Promise.all([

View File

@@ -1,4 +1,4 @@
import { TRPCError } from '@trpc/server';
import type { TRPCError } from '@trpc/server';
import { match } from 'ts-pattern';
import { z } from 'zod';
@@ -8,46 +8,69 @@ import { TRPCClientError } from '@documenso/trpc/client';
* Generic application error codes.
*/
export enum AppErrorCode {
'ALREADY_EXISTS' = 'AlreadyExists',
'EXPIRED_CODE' = 'ExpiredCode',
'INVALID_BODY' = 'InvalidBody',
'INVALID_REQUEST' = 'InvalidRequest',
'LIMIT_EXCEEDED' = 'LimitExceeded',
'NOT_FOUND' = 'NotFound',
'NOT_SETUP' = 'NotSetup',
'UNAUTHORIZED' = 'Unauthorized',
'UNKNOWN_ERROR' = 'UnknownError',
'RETRY_EXCEPTION' = 'RetryException',
'SCHEMA_FAILED' = 'SchemaFailed',
'TOO_MANY_REQUESTS' = 'TooManyRequests',
'PROFILE_URL_TAKEN' = 'ProfileUrlTaken',
'PREMIUM_PROFILE_URL' = 'PremiumProfileUrl',
'ALREADY_EXISTS' = 'ALREADY_EXISTS',
'EXPIRED_CODE' = 'EXPIRED_CODE',
'INVALID_BODY' = 'INVALID_BODY',
'INVALID_REQUEST' = 'INVALID_REQUEST',
'LIMIT_EXCEEDED' = 'LIMIT_EXCEEDED',
'NOT_FOUND' = 'NOT_FOUND',
'NOT_SETUP' = 'NOT_SETUP',
'UNAUTHORIZED' = 'UNAUTHORIZED',
'UNKNOWN_ERROR' = 'UNKNOWN_ERROR',
'RETRY_EXCEPTION' = 'RETRY_EXCEPTION',
'SCHEMA_FAILED' = 'SCHEMA_FAILED',
'TOO_MANY_REQUESTS' = 'TOO_MANY_REQUESTS',
'PROFILE_URL_TAKEN' = 'PROFILE_URL_TAKEN',
'PREMIUM_PROFILE_URL' = 'PREMIUM_PROFILE_URL',
}
const genericErrorCodeToTrpcErrorCodeMap: Record<string, TRPCError['code']> = {
[AppErrorCode.ALREADY_EXISTS]: 'BAD_REQUEST',
[AppErrorCode.EXPIRED_CODE]: 'BAD_REQUEST',
[AppErrorCode.INVALID_BODY]: 'BAD_REQUEST',
[AppErrorCode.INVALID_REQUEST]: 'BAD_REQUEST',
[AppErrorCode.NOT_FOUND]: 'NOT_FOUND',
[AppErrorCode.NOT_SETUP]: 'BAD_REQUEST',
[AppErrorCode.UNAUTHORIZED]: 'UNAUTHORIZED',
[AppErrorCode.UNKNOWN_ERROR]: 'INTERNAL_SERVER_ERROR',
[AppErrorCode.RETRY_EXCEPTION]: 'INTERNAL_SERVER_ERROR',
[AppErrorCode.SCHEMA_FAILED]: 'INTERNAL_SERVER_ERROR',
[AppErrorCode.TOO_MANY_REQUESTS]: 'TOO_MANY_REQUESTS',
[AppErrorCode.PROFILE_URL_TAKEN]: 'BAD_REQUEST',
[AppErrorCode.PREMIUM_PROFILE_URL]: 'BAD_REQUEST',
export const genericErrorCodeToTrpcErrorCodeMap: Record<
string,
{ code: TRPCError['code']; status: number }
> = {
[AppErrorCode.ALREADY_EXISTS]: { code: 'BAD_REQUEST', status: 400 },
[AppErrorCode.EXPIRED_CODE]: { code: 'BAD_REQUEST', status: 400 },
[AppErrorCode.INVALID_BODY]: { code: 'BAD_REQUEST', status: 400 },
[AppErrorCode.INVALID_REQUEST]: { code: 'BAD_REQUEST', status: 400 },
[AppErrorCode.NOT_FOUND]: { code: 'NOT_FOUND', status: 404 },
[AppErrorCode.NOT_SETUP]: { code: 'BAD_REQUEST', status: 400 },
[AppErrorCode.UNAUTHORIZED]: { code: 'UNAUTHORIZED', status: 401 },
[AppErrorCode.UNKNOWN_ERROR]: { code: 'INTERNAL_SERVER_ERROR', status: 500 },
[AppErrorCode.RETRY_EXCEPTION]: { code: 'INTERNAL_SERVER_ERROR', status: 500 },
[AppErrorCode.SCHEMA_FAILED]: { code: 'INTERNAL_SERVER_ERROR', status: 500 },
[AppErrorCode.TOO_MANY_REQUESTS]: { code: 'TOO_MANY_REQUESTS', status: 429 },
[AppErrorCode.PROFILE_URL_TAKEN]: { code: 'BAD_REQUEST', status: 400 },
[AppErrorCode.PREMIUM_PROFILE_URL]: { code: 'BAD_REQUEST', status: 400 },
};
export const ZAppErrorJsonSchema = z.object({
code: z.string(),
message: z.string().optional(),
userMessage: z.string().optional(),
statusCode: z.number().optional(),
});
export type TAppErrorJsonSchema = z.infer<typeof ZAppErrorJsonSchema>;
type AppErrorOptions = {
/**
* An internal message for logging.
*/
message?: string;
/**
* A message which can be potientially displayed to the user.
*/
userMessage?: string;
/**
* The status code to be associated with the error.
*
* Mainly used for API -> Frontend communication and logging filtering.
*/
statusCode?: number;
};
export class AppError extends Error {
/**
* The error code.
@@ -59,6 +82,11 @@ export class AppError extends Error {
*/
userMessage?: string;
/**
* The status code to be associated with the error.
*/
statusCode?: number;
/**
* Create a new AppError.
*
@@ -66,10 +94,12 @@ export class AppError extends Error {
* @param message An internal error message.
* @param userMessage A error message which can be displayed to the user.
*/
public constructor(errorCode: string, message?: string, userMessage?: string) {
super(message || errorCode);
public constructor(errorCode: string, options?: AppErrorOptions) {
super(options?.message || errorCode);
this.code = errorCode;
this.userMessage = userMessage;
this.userMessage = options?.userMessage;
this.statusCode = options?.statusCode;
}
/**
@@ -84,16 +114,21 @@ export class AppError extends Error {
// Handle TRPC errors.
if (error instanceof TRPCClientError) {
const parsedJsonError = AppError.parseFromJSONString(error.message);
return parsedJsonError || new AppError('UnknownError', error.message);
const parsedJsonError = AppError.parseFromJSON(error.data?.appError);
const fallbackError = new AppError(AppErrorCode.UNKNOWN_ERROR, {
message: error.message,
});
return parsedJsonError || fallbackError;
}
// Handle completely unknown errors.
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const { code, message, userMessage } = error as {
const { code, message, userMessage, statusCode } = error as {
code: unknown;
message: unknown;
status: unknown;
statusCode: unknown;
userMessage: unknown;
};
@@ -102,16 +137,15 @@ export class AppError extends Error {
const validUserMessage: string | undefined =
typeof userMessage === 'string' ? userMessage : undefined;
return new AppError(validCode, validMessage, validUserMessage);
}
const validStatusCode = typeof statusCode === 'number' ? statusCode : undefined;
static parseErrorToTRPCError(error: unknown): TRPCError {
const appError = AppError.parseError(error);
const options: AppErrorOptions = {
message: validMessage,
userMessage: validUserMessage,
statusCode: validStatusCode,
};
return new TRPCError({
code: genericErrorCodeToTrpcErrorCodeMap[appError.code] || 'BAD_REQUEST',
message: AppError.toJSONString(appError),
});
return new AppError(validCode, options);
}
/**
@@ -120,12 +154,26 @@ export class AppError extends Error {
* @param appError The AppError to convert to JSON.
* @returns A JSON object representing the AppError.
*/
static toJSON({ code, message, userMessage }: AppError): TAppErrorJsonSchema {
return {
static toJSON({ code, message, userMessage, statusCode }: AppError): TAppErrorJsonSchema {
const data: TAppErrorJsonSchema = {
code,
message,
userMessage,
};
// Explicity only set values if it exists, since TRPC will add meta for undefined
// values which clutters up API responses.
if (message) {
data.message = message;
}
if (userMessage) {
data.userMessage = userMessage;
}
if (statusCode) {
data.statusCode = statusCode;
}
return data;
}
/**
@@ -138,15 +186,21 @@ export class AppError extends Error {
return JSON.stringify(AppError.toJSON(appError));
}
static parseFromJSONString(jsonString: string): AppError | null {
static parseFromJSON(value: unknown): AppError | null {
try {
const parsed = ZAppErrorJsonSchema.safeParse(JSON.parse(jsonString));
const parsed = ZAppErrorJsonSchema.safeParse(value);
if (!parsed.success) {
return null;
}
return new AppError(parsed.data.code, parsed.data.message, parsed.data.userMessage);
const { message, userMessage, statusCode } = parsed.data;
return new AppError(parsed.data.code, {
message,
userMessage,
statusCode,
});
} catch {
return null;
}

View File

@@ -25,6 +25,7 @@
"@documenso/email": "*",
"@documenso/prisma": "*",
"@documenso/signing": "*",
"@honeybadger-io/js": "^6.10.1",
"@lingui/core": "^4.11.3",
"@lingui/macro": "^4.11.3",
"@lingui/react": "^4.11.3",
@@ -62,4 +63,4 @@
"@types/luxon": "^3.3.1",
"@types/pg": "^8.11.4"
}
}
}

View File

@@ -40,7 +40,9 @@ export const createPasskeyAuthenticationOptions = async ({
});
if (!preferredPasskey) {
throw new AppError(AppErrorCode.NOT_FOUND, 'Requested passkey not found');
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Requested passkey not found',
});
}
}

View File

@@ -50,7 +50,9 @@ export const createPasskey = async ({
});
if (!verificationToken) {
throw new AppError(AppErrorCode.NOT_FOUND, 'Challenge token not found');
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Challenge token not found',
});
}
await prisma.verificationToken.deleteMany({
@@ -61,7 +63,9 @@ export const createPasskey = async ({
});
if (verificationToken.expires < new Date()) {
throw new AppError(AppErrorCode.EXPIRED_CODE, 'Challenge token expired');
throw new AppError(AppErrorCode.EXPIRED_CODE, {
message: 'Challenge token expired',
});
}
const { rpId: expectedRPID, origin: expectedOrigin } = getAuthenticatorOptions();
@@ -74,7 +78,9 @@ export const createPasskey = async ({
});
if (!verification.verified || !verification.registrationInfo) {
throw new AppError(AppErrorCode.UNAUTHORIZED, 'Verification failed');
throw new AppError(AppErrorCode.UNAUTHORIZED, {
message: 'Verification failed',
});
}
const { credentialPublicKey, credentialID, counter, credentialDeviceType, credentialBackedUp } =

View File

@@ -47,7 +47,9 @@ export const createDocument = async ({
teamId !== undefined &&
!user.teamMembers.some((teamMember) => teamMember.teamId === teamId)
) {
throw new AppError(AppErrorCode.NOT_FOUND, 'Team not found');
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Team not found',
});
}
let team: (Team & { teamGlobalSettings: TeamGlobalSettings | null }) | null = null;

View File

@@ -4,6 +4,7 @@ import { prisma } from '@documenso/prisma';
import type { Prisma } from '@documenso/prisma/client';
import { TeamMemberRole } from '@documenso/prisma/client';
import { AppError, AppErrorCode } from '../../errors/app-error';
import { DocumentVisibility } from '../../types/document-visibility';
import { getTeamById } from '../team/get-team';
@@ -20,7 +21,7 @@ export const getDocumentById = async ({ id, userId, teamId }: GetDocumentByIdOpt
teamId,
});
return await prisma.document.findFirstOrThrow({
const document = await prisma.document.findFirst({
where: documentWhereInput,
include: {
documentData: true,
@@ -45,6 +46,14 @@ export const getDocumentById = async ({ id, userId, teamId }: GetDocumentByIdOpt
},
},
});
if (!document) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Document could not be found',
});
}
return document;
};
export type GetDocumentWhereInputOptions = {

View File

@@ -107,7 +107,9 @@ export const getDocumentAndSenderByToken = async ({
}
if (!documentAccessValid) {
throw new AppError(AppErrorCode.UNAUTHORIZED, 'Invalid access values');
throw new AppError(AppErrorCode.UNAUTHORIZED, {
message: 'Invalid access values',
});
}
return {
@@ -167,7 +169,9 @@ export const getDocumentAndRecipientByToken = async ({
}
if (!documentAccessValid) {
throw new AppError(AppErrorCode.UNAUTHORIZED, 'Invalid access values');
throw new AppError(AppErrorCode.UNAUTHORIZED, {
message: 'Invalid access values',
});
}
return {

View File

@@ -106,7 +106,9 @@ export const isRecipientAuthorized = async ({
// Should not be possible.
if (!user) {
throw new AppError(AppErrorCode.NOT_FOUND, 'User not found');
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'User not found',
});
}
return await verifyTwoFactorAuthenticationToken({
@@ -164,7 +166,9 @@ const verifyPasskey = async ({
});
if (!passkey) {
throw new AppError(AppErrorCode.NOT_FOUND, 'Passkey not found');
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Passkey not found',
});
}
const verificationToken = await prisma.verificationToken
@@ -177,11 +181,15 @@ const verifyPasskey = async ({
.catch(() => null);
if (!verificationToken) {
throw new AppError(AppErrorCode.NOT_FOUND, 'Token not found');
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Token not found',
});
}
if (verificationToken.expires < new Date()) {
throw new AppError(AppErrorCode.EXPIRED_CODE, 'Token expired');
throw new AppError(AppErrorCode.EXPIRED_CODE, {
message: 'Token expired',
});
}
const { rpId, origin } = getAuthenticatorOptions();
@@ -199,7 +207,9 @@ const verifyPasskey = async ({
}).catch(() => null); // May want to log this for insights.
if (verification?.verified !== true) {
throw new AppError(AppErrorCode.UNAUTHORIZED, 'User is not authorized');
throw new AppError(AppErrorCode.UNAUTHORIZED, {
message: 'User is not authorized',
});
}
await prisma.passkey.update({

View File

@@ -37,7 +37,9 @@ export const updateDocumentSettings = async ({
requestMetadata,
}: UpdateDocumentSettingsOptions) => {
if (!data.title && !data.globalAccessAuth && !data.globalActionAuth) {
throw new AppError(AppErrorCode.INVALID_BODY, 'Missing data to update');
throw new AppError(AppErrorCode.INVALID_BODY, {
message: 'Missing data to update',
});
}
const user = await prisma.user.findFirstOrThrow({
@@ -96,10 +98,9 @@ export const updateDocumentSettings = async ({
!allowedVisibilities.includes(document.visibility) ||
(data.visibility && !allowedVisibilities.includes(data.visibility))
) {
throw new AppError(
AppErrorCode.UNAUTHORIZED,
'You do not have permission to update the document visibility',
);
throw new AppError(AppErrorCode.UNAUTHORIZED, {
message: 'You do not have permission to update the document visibility',
});
}
})
.with(TeamMemberRole.MEMBER, () => {
@@ -107,17 +108,15 @@ export const updateDocumentSettings = async ({
document.visibility !== DocumentVisibility.EVERYONE ||
(data.visibility && data.visibility !== DocumentVisibility.EVERYONE)
) {
throw new AppError(
AppErrorCode.UNAUTHORIZED,
'You do not have permission to update the document visibility',
);
throw new AppError(AppErrorCode.UNAUTHORIZED, {
message: 'You do not have permission to update the document visibility',
});
}
})
.otherwise(() => {
throw new AppError(
AppErrorCode.UNAUTHORIZED,
'You do not have permission to update the document',
);
throw new AppError(AppErrorCode.UNAUTHORIZED, {
message: 'You do not have permission to update the document',
});
});
}
@@ -142,10 +141,9 @@ export const updateDocumentSettings = async ({
});
if (!isDocumentEnterprise) {
throw new AppError(
AppErrorCode.UNAUTHORIZED,
'You do not have permission to set the action auth',
);
throw new AppError(AppErrorCode.UNAUTHORIZED, {
message: 'You do not have permission to set the action auth',
});
}
}
@@ -161,10 +159,9 @@ export const updateDocumentSettings = async ({
const auditLogs: CreateDocumentAuditLogDataResponse[] = [];
if (!isTitleSame && document.status !== DocumentStatus.DRAFT) {
throw new AppError(
AppErrorCode.INVALID_BODY,
'You cannot update the title if the document has been sent',
);
throw new AppError(AppErrorCode.INVALID_BODY, {
message: 'You cannot update the title if the document has been sent',
});
}
if (!isTitleSame) {

View File

@@ -45,7 +45,9 @@ export const validateFieldAuth = async ({
});
if (!isValid) {
throw new AppError(AppErrorCode.UNAUTHORIZED, 'Invalid authentication values');
throw new AppError(AppErrorCode.UNAUTHORIZED, {
message: 'Invalid authentication values',
});
}
return derivedRecipientActionAuth;

View File

@@ -104,7 +104,9 @@ export const setFieldsForDocument = async ({
// Each field MUST have a recipient associated with it.
if (!recipient) {
throw new AppError(AppErrorCode.INVALID_REQUEST, `Recipient not found for field ${field.id}`);
throw new AppError(AppErrorCode.INVALID_REQUEST, {
message: `Recipient not found for field ${field.id}`,
});
}
// Check whether the existing field can be modified.
@@ -113,10 +115,10 @@ export const setFieldsForDocument = async ({
hasFieldBeenChanged(existing, field) &&
!canRecipientFieldsBeModified(recipient, existingFields)
) {
throw new AppError(
AppErrorCode.INVALID_REQUEST,
'Cannot modify a field where the recipient has already interacted with the document',
);
throw new AppError(AppErrorCode.INVALID_REQUEST, {
message:
'Cannot modify a field where the recipient has already interacted with the document',
});
}
return {

View File

@@ -115,7 +115,9 @@ export const getPublicProfileByUrl = async ({
// Log as critical error.
if (user?.profile && team?.profile) {
console.error('Profile URL is ambiguous', { profileUrl, userId: user.id, teamId: team.id });
throw new AppError(AppErrorCode.INVALID_REQUEST, 'Profile URL is ambiguous');
throw new AppError(AppErrorCode.INVALID_REQUEST, {
message: 'Profile URL is ambiguous',
});
}
if (user?.profile?.enabled) {
@@ -177,5 +179,7 @@ export const getPublicProfileByUrl = async ({
};
}
throw new AppError(AppErrorCode.NOT_FOUND, 'Profile not found');
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Profile not found',
});
};

View File

@@ -18,10 +18,9 @@ export const getTeamTokens = async ({ userId, teamId }: GetUserTokensOptions) =>
});
if (teamMember?.role !== TeamMemberRole.ADMIN) {
throw new AppError(
AppErrorCode.UNAUTHORIZED,
'You do not have the required permissions to view this page.',
);
throw new AppError(AppErrorCode.UNAUTHORIZED, {
message: 'You do not have the required permissions to view this page.',
});
}
return await prisma.apiToken.findMany({

View File

@@ -105,10 +105,9 @@ export const setRecipientsForDocument = async ({
});
if (!isDocumentEnterprise) {
throw new AppError(
AppErrorCode.UNAUTHORIZED,
'You do not have permission to set the action auth',
);
throw new AppError(AppErrorCode.UNAUTHORIZED, {
message: 'You do not have permission to set the action auth',
});
}
}
@@ -142,10 +141,9 @@ export const setRecipientsForDocument = async ({
hasRecipientBeenChanged(existing, recipient) &&
!canRecipientBeModified(existing, document.Field)
) {
throw new AppError(
AppErrorCode.INVALID_REQUEST,
'Cannot modify a recipient who has already interacted with the document',
);
throw new AppError(AppErrorCode.INVALID_REQUEST, {
message: 'Cannot modify a recipient who has already interacted with the document',
});
}
return {

View File

@@ -72,10 +72,9 @@ export const setRecipientsForTemplate = async ({
});
if (!isDocumentEnterprise) {
throw new AppError(
AppErrorCode.UNAUTHORIZED,
'You do not have permission to set the action auth',
);
throw new AppError(AppErrorCode.UNAUTHORIZED, {
message: 'You do not have permission to set the action auth',
});
}
}
@@ -119,14 +118,15 @@ export const setRecipientsForTemplate = async ({
);
if (updatedDirectRecipient?.role === RecipientRole.CC) {
throw new AppError(AppErrorCode.INVALID_BODY, 'Cannot set direct recipient as CC');
throw new AppError(AppErrorCode.INVALID_BODY, {
message: 'Cannot set direct recipient as CC',
});
}
if (deletedDirectRecipient) {
throw new AppError(
AppErrorCode.INVALID_BODY,
'Cannot delete direct recipient while direct template exists',
);
throw new AppError(AppErrorCode.INVALID_BODY, {
message: 'Cannot delete direct recipient while direct template exists',
});
}
}

View File

@@ -96,10 +96,9 @@ export const updateRecipient = async ({
});
if (!isDocumentEnterprise) {
throw new AppError(
AppErrorCode.UNAUTHORIZED,
'You do not have permission to set the action auth',
);
throw new AppError(AppErrorCode.UNAUTHORIZED, {
message: 'You do not have permission to set the action auth',
});
}
}

View File

@@ -47,6 +47,8 @@ export const createTeamPendingCheckoutSession = async ({
console.error(e);
// Absorb all the errors incase Stripe throws something sensitive.
throw new AppError(AppErrorCode.UNKNOWN_ERROR, 'Something went wrong.');
throw new AppError(AppErrorCode.UNKNOWN_ERROR, {
message: 'Something went wrong.',
});
}
};

View File

@@ -55,10 +55,9 @@ export const createTeamEmailVerification = async ({
});
if (team.teamEmail || team.emailVerification) {
throw new AppError(
AppErrorCode.INVALID_REQUEST,
'Team already has an email or existing email verification.',
);
throw new AppError(AppErrorCode.INVALID_REQUEST, {
message: 'Team already has an email or existing email verification.',
});
}
const existingTeamEmail = await tx.teamEmail.findFirst({
@@ -68,7 +67,9 @@ export const createTeamEmailVerification = async ({
});
if (existingTeamEmail) {
throw new AppError(AppErrorCode.ALREADY_EXISTS, 'Email already taken by another team.');
throw new AppError(AppErrorCode.ALREADY_EXISTS, {
message: 'Email already taken by another team.',
});
}
const { token, expiresAt } = createTokenVerification({ hours: 1 });
@@ -97,7 +98,9 @@ export const createTeamEmailVerification = async ({
const target = z.array(z.string()).safeParse(err.meta?.target);
if (err.code === 'P2002' && target.success && target.data.includes('email')) {
throw new AppError(AppErrorCode.ALREADY_EXISTS, 'Email already taken by another team.');
throw new AppError(AppErrorCode.ALREADY_EXISTS, {
message: 'Email already taken by another team.',
});
}
throw err;

View File

@@ -69,7 +69,9 @@ export const createTeamMemberInvites = async ({
const currentTeamMember = team.members.find((member) => member.user.id === userId);
if (!currentTeamMember) {
throw new AppError(AppErrorCode.UNAUTHORIZED, 'User not part of team.');
throw new AppError(AppErrorCode.UNAUTHORIZED, {
message: 'User not part of team.',
});
}
const usersToInvite = invitations.filter((invitation) => {
@@ -91,10 +93,9 @@ export const createTeamMemberInvites = async ({
);
if (unauthorizedRoleAccess) {
throw new AppError(
AppErrorCode.UNAUTHORIZED,
'User does not have permission to set high level roles',
);
throw new AppError(AppErrorCode.UNAUTHORIZED, {
message: 'User does not have permission to set high level roles',
});
}
const teamMemberInvites = usersToInvite.map(({ email, role }) => ({
@@ -127,11 +128,10 @@ export const createTeamMemberInvites = async ({
if (sendEmailResultErrorList.length > 0) {
console.error(JSON.stringify(sendEmailResultErrorList));
throw new AppError(
'EmailDeliveryFailed',
'Failed to send invite emails to one or more users.',
`Failed to send invites to ${sendEmailResultErrorList.length}/${teamMemberInvites.length} users.`,
);
throw new AppError('EmailDeliveryFailed', {
message: 'Failed to send invite emails to one or more users.',
userMessage: `Failed to send invites to ${sendEmailResultErrorList.length}/${teamMemberInvites.length} users.`,
});
}
};

View File

@@ -87,7 +87,9 @@ export const createTeam = async ({
});
if (existingUserProfileWithUrl) {
throw new AppError(AppErrorCode.ALREADY_EXISTS, 'URL already taken.');
throw new AppError(AppErrorCode.ALREADY_EXISTS, {
message: 'URL already taken.',
});
}
await tx.team.create({
@@ -131,15 +133,21 @@ export const createTeam = async ({
});
if (existingUserProfileWithUrl) {
throw new AppError(AppErrorCode.ALREADY_EXISTS, 'URL already taken.');
throw new AppError(AppErrorCode.ALREADY_EXISTS, {
message: 'URL already taken.',
});
}
if (existingTeamWithUrl) {
throw new AppError(AppErrorCode.ALREADY_EXISTS, 'Team URL already exists.');
throw new AppError(AppErrorCode.ALREADY_EXISTS, {
message: 'Team URL already exists.',
});
}
if (!customerId) {
throw new AppError(AppErrorCode.UNKNOWN_ERROR, 'Missing customer ID for pending teams.');
throw new AppError(AppErrorCode.UNKNOWN_ERROR, {
message: 'Missing customer ID for pending teams.',
});
}
return await tx.teamPending.create({
@@ -166,7 +174,9 @@ export const createTeam = async ({
const target = z.array(z.string()).safeParse(err.meta?.target);
if (err.code === 'P2002' && target.success && target.data.includes('url')) {
throw new AppError(AppErrorCode.ALREADY_EXISTS, 'Team URL already exists.');
throw new AppError(AppErrorCode.ALREADY_EXISTS, {
message: 'Team URL already exists.',
});
}
throw err;

View File

@@ -60,11 +60,13 @@ export const deleteTeamMembers = async ({
);
if (!currentTeamMember) {
throw new AppError(AppErrorCode.NOT_FOUND, 'Team member record does not exist');
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Team member record does not exist',
});
}
if (teamMembersToRemove.find((member) => member.userId === team.ownerUserId)) {
throw new AppError(AppErrorCode.UNAUTHORIZED, 'Cannot remove the team owner');
throw new AppError(AppErrorCode.UNAUTHORIZED, { message: 'Cannot remove the team owner' });
}
const isMemberToRemoveHigherRole = teamMembersToRemove.some(
@@ -72,7 +74,9 @@ export const deleteTeamMembers = async ({
);
if (isMemberToRemoveHigherRole) {
throw new AppError(AppErrorCode.UNAUTHORIZED, 'Cannot remove a member with a higher role');
throw new AppError(AppErrorCode.UNAUTHORIZED, {
message: 'Cannot remove a member with a higher role',
});
}
// Remove the team members.

View File

@@ -24,7 +24,9 @@ export const findTeamInvoices = async ({ userId, teamId }: FindTeamInvoicesOptio
});
if (!team.customerId) {
throw new AppError(AppErrorCode.NOT_FOUND, 'Team has no customer ID.');
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Team has no customer ID.',
});
}
const results = await getInvoices({ customerId: team.customerId });

View File

@@ -33,7 +33,9 @@ export const getTeamPublicProfile = async ({
});
if (!team) {
throw new AppError(AppErrorCode.NOT_FOUND, 'Team not found');
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Team not found',
});
}
// Create and return the public profile.
@@ -47,7 +49,9 @@ export const getTeamPublicProfile = async ({
});
if (!profile) {
throw new AppError(AppErrorCode.NOT_FOUND, 'Failed to create public profile');
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Failed to create public profile',
});
}
return {

View File

@@ -38,16 +38,17 @@ export const resendTeamEmailVerification = async ({
});
if (!team) {
throw new AppError('TeamNotFound', 'User is not a member of the team.');
throw new AppError('TeamNotFound', {
message: 'User is not a member of the team.',
});
}
const { emailVerification } = team;
if (!emailVerification) {
throw new AppError(
'VerificationNotFound',
'No team email verification exists for this team.',
);
throw new AppError('VerificationNotFound', {
message: 'No team email verification exists for this team.',
});
}
const { token, expiresAt } = createTokenVerification({ hours: 1 });

View File

@@ -55,7 +55,7 @@ export const resendTeamMemberInvitation = async ({
});
if (!team) {
throw new AppError('TeamNotFound', 'User is not a valid member of the team.');
throw new AppError('TeamNotFound', { message: 'User is not a valid member of the team.' });
}
const teamMemberInvite = await tx.teamMemberInvite.findUniqueOrThrow({
@@ -66,7 +66,7 @@ export const resendTeamMemberInvitation = async ({
});
if (!teamMemberInvite) {
throw new AppError('InviteNotFound', 'No invite exists for this user.');
throw new AppError('InviteNotFound', { message: 'No invite exists for this user.' });
}
await sendTeamMemberInviteEmail({

View File

@@ -48,11 +48,11 @@ export const updateTeamMember = async ({
const teamMemberToUpdate = team.members.find((member) => member.id === teamMemberId);
if (!teamMemberToUpdate || !currentTeamMember) {
throw new AppError(AppErrorCode.NOT_FOUND, 'Team member does not exist');
throw new AppError(AppErrorCode.NOT_FOUND, { message: 'Team member does not exist' });
}
if (teamMemberToUpdate.userId === team.ownerUserId) {
throw new AppError(AppErrorCode.UNAUTHORIZED, 'Cannot update the owner');
throw new AppError(AppErrorCode.UNAUTHORIZED, { message: 'Cannot update the owner' });
}
const isMemberToUpdateHigherRole = !isTeamRoleWithinUserHierarchy(
@@ -61,7 +61,9 @@ export const updateTeamMember = async ({
);
if (isMemberToUpdateHigherRole) {
throw new AppError(AppErrorCode.UNAUTHORIZED, 'Cannot update a member with a higher role');
throw new AppError(AppErrorCode.UNAUTHORIZED, {
message: 'Cannot update a member with a higher role',
});
}
const isNewMemberRoleHigherThanCurrentRole = !isTeamRoleWithinUserHierarchy(
@@ -70,10 +72,9 @@ export const updateTeamMember = async ({
);
if (isNewMemberRoleHigherThanCurrentRole) {
throw new AppError(
AppErrorCode.UNAUTHORIZED,
'Cannot give a member a role higher than the user initating the update',
);
throw new AppError(AppErrorCode.UNAUTHORIZED, {
message: 'Cannot give a member a role higher than the user initating the update',
});
}
return await tx.teamMember.update({

View File

@@ -24,7 +24,9 @@ export const updateTeam = async ({ userId, teamId, data }: UpdateTeamOptions) =>
});
if (foundPendingTeamWithUrl) {
throw new AppError(AppErrorCode.ALREADY_EXISTS, 'Team URL already exists.');
throw new AppError(AppErrorCode.ALREADY_EXISTS, {
message: 'Team URL already exists.',
});
}
const team = await tx.team.update({
@@ -57,7 +59,9 @@ export const updateTeam = async ({ userId, teamId, data }: UpdateTeamOptions) =>
const target = z.array(z.string()).safeParse(err.meta?.target);
if (err.code === 'P2002' && target.success && target.data.includes('url')) {
throw new AppError(AppErrorCode.ALREADY_EXISTS, 'Team URL already exists.');
throw new AppError(AppErrorCode.ALREADY_EXISTS, {
message: 'Team URL already exists.',
});
}
throw err;

View File

@@ -101,7 +101,7 @@ export const createDocumentFromDirectTemplate = async ({
});
if (!template?.directLink?.enabled) {
throw new AppError(AppErrorCode.INVALID_REQUEST, 'Invalid or missing template');
throw new AppError(AppErrorCode.INVALID_REQUEST, { message: 'Invalid or missing template' });
}
const { Recipient: recipients, directLink, User: templateOwner } = template;
@@ -111,15 +111,19 @@ export const createDocumentFromDirectTemplate = async ({
);
if (!directTemplateRecipient || directTemplateRecipient.role === RecipientRole.CC) {
throw new AppError(AppErrorCode.INVALID_REQUEST, 'Invalid or missing direct recipient');
throw new AppError(AppErrorCode.INVALID_REQUEST, {
message: 'Invalid or missing direct recipient',
});
}
if (template.updatedAt.getTime() !== templateUpdatedAt.getTime()) {
throw new AppError(AppErrorCode.INVALID_REQUEST, 'Template no longer matches');
throw new AppError(AppErrorCode.INVALID_REQUEST, { message: 'Template no longer matches' });
}
if (user && user.email !== directRecipientEmail) {
throw new AppError(AppErrorCode.INVALID_REQUEST, 'Email must match if you are logged in');
throw new AppError(AppErrorCode.INVALID_REQUEST, {
message: 'Email must match if you are logged in',
});
}
const { derivedRecipientAccessAuth, documentAuthOption: templateAuthOptions } =
@@ -136,7 +140,7 @@ export const createDocumentFromDirectTemplate = async ({
.exhaustive();
if (!isAccessAuthValid) {
throw new AppError(AppErrorCode.UNAUTHORIZED, 'You must be logged in');
throw new AppError(AppErrorCode.UNAUTHORIZED, { message: 'You must be logged in' });
}
const directTemplateRecipientAuthOptions = ZRecipientAuthOptionsSchema.parse(
@@ -163,7 +167,9 @@ export const createDocumentFromDirectTemplate = async ({
);
if (!signedFieldValue) {
throw new AppError(AppErrorCode.INVALID_BODY, 'Invalid, missing or changed fields');
throw new AppError(AppErrorCode.INVALID_BODY, {
message: 'Invalid, missing or changed fields',
});
}
if (templateField.type === FieldType.NAME && directRecipientName === undefined) {

View File

@@ -120,7 +120,9 @@ export const createDocumentFromTemplate = async ({
});
if (!template) {
throw new AppError(AppErrorCode.NOT_FOUND, 'Template not found');
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Template not found',
});
}
// Check that all the passed in recipient IDs can be associated with a template recipient.
@@ -130,10 +132,9 @@ export const createDocumentFromTemplate = async ({
);
if (!foundRecipient) {
throw new AppError(
AppErrorCode.INVALID_BODY,
`Recipient with ID ${recipient.id} not found in the template.`,
);
throw new AppError(AppErrorCode.INVALID_BODY, {
message: `Recipient with ID ${recipient.id} not found in the template.`,
});
}
});

View File

@@ -47,18 +47,18 @@ export const createTemplateDirectLink = async ({
});
if (!template) {
throw new AppError(AppErrorCode.NOT_FOUND, 'Template not found');
throw new AppError(AppErrorCode.NOT_FOUND, { message: 'Template not found' });
}
if (template.directLink) {
throw new AppError(AppErrorCode.ALREADY_EXISTS, 'Direct template already exists');
throw new AppError(AppErrorCode.ALREADY_EXISTS, { message: 'Direct template already exists' });
}
if (
directRecipientId &&
!template.Recipient.find((recipient) => recipient.id === directRecipientId)
) {
throw new AppError(AppErrorCode.NOT_FOUND, 'Recipient not found');
throw new AppError(AppErrorCode.NOT_FOUND, { message: 'Recipient not found' });
}
if (
@@ -67,7 +67,9 @@ export const createTemplateDirectLink = async ({
(recipient) => recipient.email.toLowerCase() === DIRECT_TEMPLATE_RECIPIENT_EMAIL,
)
) {
throw new AppError(AppErrorCode.INVALID_BODY, 'Cannot generate placeholder direct recipient');
throw new AppError(AppErrorCode.INVALID_BODY, {
message: 'Cannot generate placeholder direct recipient',
});
}
return await prisma.$transaction(async (tx) => {

View File

@@ -39,7 +39,9 @@ export const deleteTemplateDirectLink = async ({
});
if (!template) {
throw new AppError(AppErrorCode.NOT_FOUND, 'Template not found');
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Template not found',
});
}
const { directLink } = template;

View File

@@ -53,7 +53,9 @@ export const getTemplateById = async ({ id, userId, teamId }: GetTemplateByIdOpt
});
if (!template) {
throw new AppError(AppErrorCode.NOT_FOUND, 'Template not found');
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Template not found',
});
}
return template;

View File

@@ -1,6 +1,8 @@
import { prisma } from '@documenso/prisma';
import type { TemplateWithDetails } from '@documenso/prisma/types/template';
import { AppError, AppErrorCode } from '../../errors/app-error';
export type GetTemplateWithDetailsByIdOptions = {
id: number;
userId: number;
@@ -10,7 +12,7 @@ export const getTemplateWithDetailsById = async ({
id,
userId,
}: GetTemplateWithDetailsByIdOptions): Promise<TemplateWithDetails> => {
return await prisma.template.findFirstOrThrow({
const template = await prisma.template.findFirst({
where: {
id,
OR: [
@@ -36,4 +38,12 @@ export const getTemplateWithDetailsById = async ({
Field: true,
},
});
if (!template) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Template not found',
});
}
return template;
};

View File

@@ -40,13 +40,17 @@ export const toggleTemplateDirectLink = async ({
});
if (!template) {
throw new AppError(AppErrorCode.NOT_FOUND, 'Template not found');
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Template not found',
});
}
const { directLink } = template;
if (!directLink) {
throw new AppError(AppErrorCode.NOT_FOUND, 'Direct template link not found');
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Direct template link not found',
});
}
return await prisma.templateDirectLink.update({

View File

@@ -34,7 +34,9 @@ export const updateTemplateSettings = async ({
data,
}: UpdateTemplateSettingsOptions) => {
if (Object.values(data).length === 0 && Object.keys(meta ?? {}).length === 0) {
throw new AppError(AppErrorCode.INVALID_BODY, 'Missing data to update');
throw new AppError(AppErrorCode.INVALID_BODY, {
message: 'Missing data to update',
});
}
const template = await prisma.template.findFirstOrThrow({
@@ -82,10 +84,9 @@ export const updateTemplateSettings = async ({
});
if (!isDocumentEnterprise) {
throw new AppError(
AppErrorCode.UNAUTHORIZED,
'You do not have permission to set the action auth',
);
throw new AppError(AppErrorCode.UNAUTHORIZED, {
message: 'You do not have permission to set the action auth',
});
}
}

View File

@@ -38,11 +38,10 @@ export const createUser = async ({ name, email, password, signature, url }: Crea
});
if (urlExists) {
throw new AppError(
AppErrorCode.PROFILE_URL_TAKEN,
'Profile username is taken',
'The profile username is already taken',
);
throw new AppError(AppErrorCode.PROFILE_URL_TAKEN, {
message: 'Profile username is taken',
userMessage: 'The profile username is already taken',
});
}
}

View File

@@ -26,7 +26,7 @@ export const getUserPublicProfile = async ({
});
if (!user) {
throw new AppError(AppErrorCode.NOT_FOUND, 'User not found');
throw new AppError(AppErrorCode.NOT_FOUND, { message: 'User not found' });
}
// Create and return the public profile.
@@ -39,7 +39,7 @@ export const getUserPublicProfile = async ({
});
if (!profile) {
throw new AppError(AppErrorCode.NOT_FOUND, 'Failed to create public profile');
throw new AppError(AppErrorCode.NOT_FOUND, { message: 'Failed to create public profile' });
}
return {

View File

@@ -13,7 +13,7 @@ export type UpdatePublicProfileOptions = {
export const updatePublicProfile = async ({ userId, data }: UpdatePublicProfileOptions) => {
if (Object.values(data).length === 0) {
throw new AppError(AppErrorCode.INVALID_BODY, 'Missing data to update');
throw new AppError(AppErrorCode.INVALID_BODY, { message: 'Missing data to update' });
}
const { url, bio, enabled } = data;
@@ -25,13 +25,15 @@ export const updatePublicProfile = async ({ userId, data }: UpdatePublicProfileO
});
if (!user) {
throw new AppError(AppErrorCode.NOT_FOUND, 'User not found');
throw new AppError(AppErrorCode.NOT_FOUND, { message: 'User not found' });
}
const finalUrl = url ?? user.url;
if (!finalUrl && enabled) {
throw new AppError(AppErrorCode.INVALID_REQUEST, 'Cannot enable a profile without a URL');
throw new AppError(AppErrorCode.INVALID_REQUEST, {
message: 'Cannot enable a profile without a URL',
});
}
if (url) {
@@ -57,7 +59,9 @@ export const updatePublicProfile = async ({ userId, data }: UpdatePublicProfileO
});
if (isUrlTakenByAnotherUser || isUrlTakenByAnotherTeam) {
throw new AppError(AppErrorCode.PROFILE_URL_TAKEN, 'The profile username is already taken');
throw new AppError(AppErrorCode.PROFILE_URL_TAKEN, {
message: 'The profile username is already taken',
});
}
}

View File

@@ -0,0 +1,108 @@
import Honeybadger from '@honeybadger-io/js';
export const buildLogger = () => {
if (process.env.NEXT_PRIVATE_LOGGER_HONEY_BADGER_API_KEY) {
return new HoneybadgerLogger();
}
return new DefaultLogger();
};
interface LoggerDescriptionOptions {
method?: string;
path?: string;
context?: Record<string, unknown>;
/**
* The type of log to be captured.
*
* Defaults to `info`.
*/
level?: 'info' | 'error' | 'critical';
}
/**
* Basic logger implementation intended to be used in the server side for capturing
* explicit errors and other logs.
*
* Not intended to capture the request and responses.
*/
interface Logger {
log(message: string, options?: LoggerDescriptionOptions): void;
error(error: Error, options?: LoggerDescriptionOptions): void;
}
class DefaultLogger implements Logger {
log(_message: string, _options?: LoggerDescriptionOptions) {
// Do nothing.
}
error(_error: Error, _options?: LoggerDescriptionOptions): void {
// Do nothing.
}
}
class HoneybadgerLogger implements Logger {
constructor() {
if (!process.env.NEXT_PRIVATE_LOGGER_HONEY_BADGER_API_KEY) {
throw new Error('NEXT_PRIVATE_LOGGER_HONEY_BADGER_API_KEY is not set');
}
Honeybadger.configure({
apiKey: process.env.NEXT_PRIVATE_LOGGER_HONEY_BADGER_API_KEY,
});
}
/**
* Honeybadger doesn't really have a non-error logging system.
*/
log(message: string, options?: LoggerDescriptionOptions) {
const { context = {}, level = 'info' } = options || {};
try {
Honeybadger.event({
message,
context: {
level,
...context,
},
});
} catch (err) {
console.error(err);
// Do nothing.
}
}
error(error: Error, options?: LoggerDescriptionOptions): void {
const { context = {}, level = 'error', method, path } = options || {};
const tags = [`level:${level}`];
let errorMessage = error.message;
if (method) {
tags.push(`method:${method}`);
errorMessage = `[${method}]: ${error.message}`;
}
if (path) {
tags.push(`path:${path}`);
}
try {
Honeybadger.notify(errorMessage, {
context: {
level,
...context,
},
tags,
});
} catch (err) {
console.error(err);
// Do nothing.
}
}
}

View File

@@ -44,10 +44,9 @@ export const authRouter = router({
const { name, email, password, signature, url } = input;
if (IS_BILLING_ENABLED() && url && url.length < 6) {
throw new AppError(
AppErrorCode.PREMIUM_PROFILE_URL,
'Only subscribers can have a username shorter than 6 characters',
);
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 });
@@ -66,7 +65,7 @@ export const authRouter = router({
const error = AppError.parseError(err);
if (error.code !== AppErrorCode.UNKNOWN_ERROR) {
throw AppError.parseErrorToTRPCError(error);
throw error;
}
let message =
@@ -118,7 +117,7 @@ export const authRouter = router({
} catch (err) {
console.error(err);
throw AppError.parseErrorToTRPCError(err);
throw err;
}
}),

View File

@@ -1,6 +1,5 @@
import { TRPCError } from '@trpc/server';
import { AppError } from '@documenso/lib/errors/app-error';
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';
@@ -96,7 +95,7 @@ export const fieldRouter = router({
} catch (err) {
console.error(err);
throw AppError.parseErrorToTRPCError(err);
throw err;
}
}),

View File

@@ -100,10 +100,9 @@ export const profileRouter = router({
);
if (subscriptions.length === 0) {
throw new AppError(
AppErrorCode.PREMIUM_PROFILE_URL,
'Only subscribers can have a username shorter than 6 characters',
);
throw new AppError(AppErrorCode.PREMIUM_PROFILE_URL, {
message: 'Only subscribers can have a username shorter than 6 characters',
});
}
}
@@ -123,7 +122,7 @@ export const profileRouter = router({
const error = AppError.parseError(err);
if (error.code !== AppErrorCode.UNKNOWN_ERROR) {
throw AppError.parseErrorToTRPCError(error);
throw error;
}
throw new TRPCError({

View File

@@ -76,412 +76,238 @@ export const teamRouter = router({
acceptTeamInvitation: authenticatedProcedure
.input(ZAcceptTeamInvitationMutationSchema)
.mutation(async ({ input, ctx }) => {
try {
return await acceptTeamInvitation({
teamId: input.teamId,
userId: ctx.user.id,
});
} catch (err) {
console.error(err);
throw AppError.parseErrorToTRPCError(err);
}
return await acceptTeamInvitation({
teamId: input.teamId,
userId: ctx.user.id,
});
}),
declineTeamInvitation: authenticatedProcedure
.input(ZDeclineTeamInvitationMutationSchema)
.mutation(async ({ input, ctx }) => {
try {
return await declineTeamInvitation({
teamId: input.teamId,
userId: ctx.user.id,
});
} catch (err) {
console.error(err);
throw AppError.parseErrorToTRPCError(err);
}
return await declineTeamInvitation({
teamId: input.teamId,
userId: ctx.user.id,
});
}),
createBillingPortal: authenticatedProcedure
.input(ZCreateTeamBillingPortalMutationSchema)
.mutation(async ({ input, ctx }) => {
try {
return await createTeamBillingPortal({
userId: ctx.user.id,
...input,
});
} catch (err) {
console.error(err);
throw AppError.parseErrorToTRPCError(err);
}
return await createTeamBillingPortal({
userId: ctx.user.id,
...input,
});
}),
createTeam: authenticatedProcedure
.input(ZCreateTeamMutationSchema)
.mutation(async ({ input, ctx }) => {
try {
return await createTeam({
userId: ctx.user.id,
...input,
});
} catch (err) {
console.error(err);
throw AppError.parseErrorToTRPCError(err);
}
return await createTeam({
userId: ctx.user.id,
...input,
});
}),
createTeamEmailVerification: authenticatedProcedure
.input(ZCreateTeamEmailVerificationMutationSchema)
.mutation(async ({ input, ctx }) => {
try {
return await createTeamEmailVerification({
teamId: input.teamId,
userId: ctx.user.id,
data: {
email: input.email,
name: input.name,
},
});
} catch (err) {
console.error(err);
throw AppError.parseErrorToTRPCError(err);
}
return await createTeamEmailVerification({
teamId: input.teamId,
userId: ctx.user.id,
data: {
email: input.email,
name: input.name,
},
});
}),
createTeamMemberInvites: authenticatedProcedure
.input(ZCreateTeamMemberInvitesMutationSchema)
.mutation(async ({ input, ctx }) => {
try {
return await createTeamMemberInvites({
userId: ctx.user.id,
userName: ctx.user.name ?? '',
...input,
});
} catch (err) {
console.error(err);
throw AppError.parseErrorToTRPCError(err);
}
return await createTeamMemberInvites({
userId: ctx.user.id,
userName: ctx.user.name ?? '',
...input,
});
}),
createTeamPendingCheckout: authenticatedProcedure
.input(ZCreateTeamPendingCheckoutMutationSchema)
.mutation(async ({ input, ctx }) => {
try {
return await createTeamPendingCheckoutSession({
userId: ctx.user.id,
...input,
});
} catch (err) {
console.error(err);
throw AppError.parseErrorToTRPCError(err);
}
return await createTeamPendingCheckoutSession({
userId: ctx.user.id,
...input,
});
}),
deleteTeam: authenticatedProcedure
.input(ZDeleteTeamMutationSchema)
.mutation(async ({ input, ctx }) => {
try {
return await deleteTeam({
userId: ctx.user.id,
...input,
});
} catch (err) {
console.error(err);
throw AppError.parseErrorToTRPCError(err);
}
return await deleteTeam({
userId: ctx.user.id,
...input,
});
}),
deleteTeamEmail: authenticatedProcedure
.input(ZDeleteTeamEmailMutationSchema)
.mutation(async ({ input, ctx }) => {
try {
return await deleteTeamEmail({
userId: ctx.user.id,
userEmail: ctx.user.email,
...input,
});
} catch (err) {
console.error(err);
throw AppError.parseErrorToTRPCError(err);
}
return await deleteTeamEmail({
userId: ctx.user.id,
userEmail: ctx.user.email,
...input,
});
}),
deleteTeamEmailVerification: authenticatedProcedure
.input(ZDeleteTeamEmailVerificationMutationSchema)
.mutation(async ({ input, ctx }) => {
try {
return await deleteTeamEmailVerification({
userId: ctx.user.id,
...input,
});
} catch (err) {
console.error(err);
throw AppError.parseErrorToTRPCError(err);
}
return await deleteTeamEmailVerification({
userId: ctx.user.id,
...input,
});
}),
deleteTeamMemberInvitations: authenticatedProcedure
.input(ZDeleteTeamMemberInvitationsMutationSchema)
.mutation(async ({ input, ctx }) => {
try {
return await deleteTeamMemberInvitations({
userId: ctx.user.id,
...input,
});
} catch (err) {
console.error(err);
throw AppError.parseErrorToTRPCError(err);
}
return await deleteTeamMemberInvitations({
userId: ctx.user.id,
...input,
});
}),
deleteTeamMembers: authenticatedProcedure
.input(ZDeleteTeamMembersMutationSchema)
.mutation(async ({ input, ctx }) => {
try {
return await deleteTeamMembers({
userId: ctx.user.id,
...input,
});
} catch (err) {
console.error(err);
throw AppError.parseErrorToTRPCError(err);
}
return await deleteTeamMembers({
userId: ctx.user.id,
...input,
});
}),
deleteTeamPending: authenticatedProcedure
.input(ZDeleteTeamPendingMutationSchema)
.mutation(async ({ input, ctx }) => {
try {
return await deleteTeamPending({
userId: ctx.user.id,
...input,
});
} catch (err) {
console.error(err);
throw AppError.parseErrorToTRPCError(err);
}
return await deleteTeamPending({
userId: ctx.user.id,
...input,
});
}),
deleteTeamTransferRequest: authenticatedProcedure
.input(ZDeleteTeamTransferRequestMutationSchema)
.mutation(async ({ input, ctx }) => {
try {
return await deleteTeamTransferRequest({
userId: ctx.user.id,
...input,
});
} catch (err) {
console.error(err);
throw AppError.parseErrorToTRPCError(err);
}
return await deleteTeamTransferRequest({
userId: ctx.user.id,
...input,
});
}),
findTeamInvoices: authenticatedProcedure
.input(ZFindTeamInvoicesQuerySchema)
.query(async ({ input, ctx }) => {
try {
return await findTeamInvoices({
userId: ctx.user.id,
...input,
});
} catch (err) {
console.error(err);
throw AppError.parseErrorToTRPCError(err);
}
return await findTeamInvoices({
userId: ctx.user.id,
...input,
});
}),
findTeamMemberInvites: authenticatedProcedure
.input(ZFindTeamMemberInvitesQuerySchema)
.query(async ({ input, ctx }) => {
try {
return await findTeamMemberInvites({
userId: ctx.user.id,
...input,
});
} catch (err) {
console.error(err);
throw AppError.parseErrorToTRPCError(err);
}
return await findTeamMemberInvites({
userId: ctx.user.id,
...input,
});
}),
findTeamMembers: authenticatedProcedure
.input(ZFindTeamMembersQuerySchema)
.query(async ({ input, ctx }) => {
try {
return await findTeamMembers({
userId: ctx.user.id,
...input,
});
} catch (err) {
console.error(err);
throw AppError.parseErrorToTRPCError(err);
}
}),
findTeams: authenticatedProcedure.input(ZFindTeamsQuerySchema).query(async ({ input, ctx }) => {
try {
return await findTeams({
return await findTeamMembers({
userId: ctx.user.id,
...input,
});
} catch (err) {
console.error(err);
}),
throw AppError.parseErrorToTRPCError(err);
}
findTeams: authenticatedProcedure.input(ZFindTeamsQuerySchema).query(async ({ input, ctx }) => {
return await findTeams({
userId: ctx.user.id,
...input,
});
}),
findTeamsPending: authenticatedProcedure
.input(ZFindTeamsPendingQuerySchema)
.query(async ({ input, ctx }) => {
try {
return await findTeamsPending({
userId: ctx.user.id,
...input,
});
} catch (err) {
console.error(err);
throw AppError.parseErrorToTRPCError(err);
}
return await findTeamsPending({
userId: ctx.user.id,
...input,
});
}),
getTeam: authenticatedProcedure.input(ZGetTeamQuerySchema).query(async ({ input, ctx }) => {
try {
return await getTeamById({ teamId: input.teamId, userId: ctx.user.id });
} catch (err) {
console.error(err);
throw AppError.parseErrorToTRPCError(err);
}
return await getTeamById({ teamId: input.teamId, userId: ctx.user.id });
}),
getTeamEmailByEmail: authenticatedProcedure.query(async ({ ctx }) => {
try {
return await getTeamEmailByEmail({ email: ctx.user.email });
} catch (err) {
console.error(err);
throw AppError.parseErrorToTRPCError(err);
}
return await getTeamEmailByEmail({ email: ctx.user.email });
}),
getTeamInvitations: authenticatedProcedure.query(async ({ ctx }) => {
try {
return await getTeamInvitations({ email: ctx.user.email });
} catch (err) {
console.error(err);
throw AppError.parseErrorToTRPCError(err);
}
return await getTeamInvitations({ email: ctx.user.email });
}),
getTeamMembers: authenticatedProcedure
.input(ZGetTeamMembersQuerySchema)
.query(async ({ input, ctx }) => {
try {
return await getTeamMembers({ teamId: input.teamId, userId: ctx.user.id });
} catch (err) {
console.error(err);
throw AppError.parseErrorToTRPCError(err);
}
return await getTeamMembers({ teamId: input.teamId, userId: ctx.user.id });
}),
getTeamPrices: authenticatedProcedure.query(async () => {
try {
return await getTeamPrices();
} catch (err) {
console.error(err);
throw AppError.parseErrorToTRPCError(err);
}
return await getTeamPrices();
}),
getTeams: authenticatedProcedure.query(async ({ ctx }) => {
try {
return await getTeams({ userId: ctx.user.id });
} catch (err) {
console.error(err);
throw AppError.parseErrorToTRPCError(err);
}
return await getTeams({ userId: ctx.user.id });
}),
leaveTeam: authenticatedProcedure
.input(ZLeaveTeamMutationSchema)
.mutation(async ({ input, ctx }) => {
try {
return await leaveTeam({
userId: ctx.user.id,
...input,
});
} catch (err) {
console.error(err);
throw AppError.parseErrorToTRPCError(err);
}
return await leaveTeam({
userId: ctx.user.id,
...input,
});
}),
updateTeam: authenticatedProcedure
.input(ZUpdateTeamMutationSchema)
.mutation(async ({ input, ctx }) => {
try {
return await updateTeam({
userId: ctx.user.id,
...input,
});
} catch (err) {
console.error(err);
throw AppError.parseErrorToTRPCError(err);
}
return await updateTeam({
userId: ctx.user.id,
...input,
});
}),
updateTeamEmail: authenticatedProcedure
.input(ZUpdateTeamEmailMutationSchema)
.mutation(async ({ input, ctx }) => {
try {
return await updateTeamEmail({
userId: ctx.user.id,
...input,
});
} catch (err) {
console.error(err);
throw AppError.parseErrorToTRPCError(err);
}
return await updateTeamEmail({
userId: ctx.user.id,
...input,
});
}),
updateTeamMember: authenticatedProcedure
.input(ZUpdateTeamMemberMutationSchema)
.mutation(async ({ input, ctx }) => {
try {
return await updateTeamMember({
userId: ctx.user.id,
...input,
});
} catch (err) {
console.error(err);
throw AppError.parseErrorToTRPCError(err);
}
return await updateTeamMember({
userId: ctx.user.id,
...input,
});
}),
updateTeamPublicProfile: authenticatedProcedure
@@ -506,7 +332,7 @@ export const teamRouter = router({
const error = AppError.parseError(err);
if (error.code !== AppErrorCode.UNKNOWN_ERROR) {
throw AppError.parseErrorToTRPCError(error);
throw error;
}
throw new TRPCError({
@@ -520,48 +346,30 @@ export const teamRouter = router({
requestTeamOwnershipTransfer: authenticatedProcedure
.input(ZRequestTeamOwnerhsipTransferMutationSchema)
.mutation(async ({ input, ctx }) => {
try {
return await requestTeamOwnershipTransfer({
userId: ctx.user.id,
userName: ctx.user.name ?? '',
...input,
});
} catch (err) {
console.error(err);
throw AppError.parseErrorToTRPCError(err);
}
return await requestTeamOwnershipTransfer({
userId: ctx.user.id,
userName: ctx.user.name ?? '',
...input,
});
}),
resendTeamEmailVerification: authenticatedProcedure
.input(ZResendTeamEmailVerificationMutationSchema)
.mutation(async ({ input, ctx }) => {
try {
await resendTeamEmailVerification({
userId: ctx.user.id,
...input,
});
} catch (err) {
console.error(err);
throw AppError.parseErrorToTRPCError(err);
}
await resendTeamEmailVerification({
userId: ctx.user.id,
...input,
});
}),
resendTeamMemberInvitation: authenticatedProcedure
.input(ZResendTeamMemberInvitationMutationSchema)
.mutation(async ({ input, ctx }) => {
try {
await resendTeamMemberInvitation({
userId: ctx.user.id,
userName: ctx.user.name ?? '',
...input,
});
} catch (err) {
console.error(err);
throw AppError.parseErrorToTRPCError(err);
}
await resendTeamMemberInvitation({
userId: ctx.user.id,
userName: ctx.user.name ?? '',
...input,
});
}),
updateTeamBrandingSettings: authenticatedProcedure
@@ -569,17 +377,11 @@ export const teamRouter = router({
.mutation(async ({ ctx, input }) => {
const { teamId, settings } = input;
try {
return await updateTeamBrandingSettings({
userId: ctx.user.id,
teamId,
settings,
});
} catch (err) {
console.error(err);
throw AppError.parseErrorToTRPCError(err);
}
return await updateTeamBrandingSettings({
userId: ctx.user.id,
teamId,
settings,
});
}),
updateTeamDocumentSettings: authenticatedProcedure
@@ -587,16 +389,10 @@ export const teamRouter = router({
.mutation(async ({ ctx, input }) => {
const { teamId, settings } = input;
try {
return await updateTeamDocumentSettings({
userId: ctx.user.id,
teamId,
settings,
});
} catch (err) {
console.error(err);
throw AppError.parseErrorToTRPCError(err);
}
return await updateTeamDocumentSettings({
userId: ctx.user.id,
teamId,
settings,
});
}),
});

View File

@@ -42,323 +42,214 @@ export const templateRouter = router({
createTemplate: authenticatedProcedure
.input(ZCreateTemplateMutationSchema)
.mutation(async ({ input, ctx }) => {
try {
const { teamId, title, templateDocumentDataId } = input;
const { teamId, title, templateDocumentDataId } = input;
return await createTemplate({
userId: ctx.user.id,
teamId,
title,
templateDocumentDataId,
});
} catch (err) {
console.error(err);
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'We were unable to create this template. Please try again later.',
});
}
return await createTemplate({
userId: ctx.user.id,
teamId,
title,
templateDocumentDataId,
});
}),
createDocumentFromDirectTemplate: maybeAuthenticatedProcedure
.input(ZCreateDocumentFromDirectTemplateMutationSchema)
.mutation(async ({ input, ctx }) => {
try {
const {
directRecipientName,
directRecipientEmail,
directTemplateToken,
directTemplateExternalId,
signedFieldValues,
templateUpdatedAt,
} = input;
const {
directRecipientName,
directRecipientEmail,
directTemplateToken,
directTemplateExternalId,
signedFieldValues,
templateUpdatedAt,
} = input;
const requestMetadata = extractNextApiRequestMetadata(ctx.req);
const requestMetadata = extractNextApiRequestMetadata(ctx.req);
return await createDocumentFromDirectTemplate({
directRecipientName,
directRecipientEmail,
directTemplateToken,
directTemplateExternalId,
signedFieldValues,
templateUpdatedAt,
user: ctx.user
? {
id: ctx.user.id,
name: ctx.user.name || undefined,
email: ctx.user.email,
}
: undefined,
requestMetadata,
});
} catch (err) {
console.error(err);
throw AppError.parseErrorToTRPCError(err);
}
return await createDocumentFromDirectTemplate({
directRecipientName,
directRecipientEmail,
directTemplateToken,
directTemplateExternalId,
signedFieldValues,
templateUpdatedAt,
user: ctx.user
? {
id: ctx.user.id,
name: ctx.user.name || undefined,
email: ctx.user.email,
}
: undefined,
requestMetadata,
});
}),
createDocumentFromTemplate: authenticatedProcedure
.input(ZCreateDocumentFromTemplateMutationSchema)
.mutation(async ({ input, ctx }) => {
try {
const { templateId, teamId, recipients } = input;
const { templateId, teamId, recipients } = input;
const limits = await getServerLimits({ email: ctx.user.email, teamId });
const limits = await getServerLimits({ email: ctx.user.email, teamId });
if (limits.remaining.documents === 0) {
throw new Error('You have reached your document limit.');
}
const requestMetadata = extractNextApiRequestMetadata(ctx.req);
let document: Document = await createDocumentFromTemplate({
templateId,
teamId,
userId: ctx.user.id,
recipients,
requestMetadata,
});
if (input.distributeDocument) {
document = await sendDocument({
documentId: document.id,
userId: ctx.user.id,
teamId,
requestMetadata,
}).catch((err) => {
console.error(err);
throw new AppError('DOCUMENT_SEND_FAILED');
});
}
return document;
} catch (err) {
console.error(err);
throw AppError.parseErrorToTRPCError(err);
if (limits.remaining.documents === 0) {
throw new Error('You have reached your document limit.');
}
const requestMetadata = extractNextApiRequestMetadata(ctx.req);
let document: Document = await createDocumentFromTemplate({
templateId,
teamId,
userId: ctx.user.id,
recipients,
requestMetadata,
});
if (input.distributeDocument) {
document = await sendDocument({
documentId: document.id,
userId: ctx.user.id,
teamId,
requestMetadata,
}).catch((err) => {
console.error(err);
throw new AppError('DOCUMENT_SEND_FAILED');
});
}
return document;
}),
duplicateTemplate: authenticatedProcedure
.input(ZDuplicateTemplateMutationSchema)
.mutation(async ({ input, ctx }) => {
try {
const { teamId, templateId } = input;
const { teamId, templateId } = input;
return await duplicateTemplate({
userId: ctx.user.id,
teamId,
templateId,
});
} catch (err) {
console.error(err);
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'We were unable to duplicate the template. Please try again later.',
});
}
return await duplicateTemplate({
userId: ctx.user.id,
teamId,
templateId,
});
}),
deleteTemplate: authenticatedProcedure
.input(ZDeleteTemplateMutationSchema)
.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 deleteTemplate({ userId, id, teamId });
} catch (err) {
console.error(err);
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'We were unable to delete this template. Please try again later.',
});
}
return await deleteTemplate({ userId, id, teamId });
}),
getTemplateWithDetailsById: authenticatedProcedure
.input(ZGetTemplateWithDetailsByIdQuerySchema)
.query(async ({ input, ctx }) => {
try {
return await getTemplateWithDetailsById({
id: input.id,
userId: ctx.user.id,
});
} catch (err) {
console.error(err);
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'We were unable to find this template. Please try again later.',
});
}
return await getTemplateWithDetailsById({
id: input.id,
userId: ctx.user.id,
});
}),
// Todo: Add API
updateTemplateSettings: authenticatedProcedure
.input(ZUpdateTemplateSettingsMutationSchema)
.mutation(async ({ input, ctx }) => {
try {
const { templateId, teamId, data, meta } = input;
const { templateId, teamId, data, meta } = input;
const userId = ctx.user.id;
const userId = ctx.user.id;
const requestMetadata = extractNextApiRequestMetadata(ctx.req);
const requestMetadata = extractNextApiRequestMetadata(ctx.req);
return await updateTemplateSettings({
userId,
teamId,
templateId,
data,
meta: {
...meta,
language: isValidLanguageCode(meta?.language) ? meta?.language : undefined,
},
requestMetadata,
});
} catch (err) {
console.error(err);
throw new TRPCError({
code: 'BAD_REQUEST',
message:
'We were unable to update the settings for this template. Please try again later.',
});
}
return await updateTemplateSettings({
userId,
teamId,
templateId,
data,
meta: {
...meta,
language: isValidLanguageCode(meta?.language) ? meta?.language : undefined,
},
requestMetadata,
});
}),
setSigningOrderForTemplate: authenticatedProcedure
.input(ZSetSigningOrderForTemplateMutationSchema)
.mutation(async ({ input, ctx }) => {
try {
const { templateId, teamId, signingOrder } = input;
const { templateId, teamId, signingOrder } = input;
return await updateTemplateSettings({
templateId,
teamId,
data: {},
meta: { 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 updateTemplateSettings({
templateId,
teamId,
data: {},
meta: { signingOrder },
userId: ctx.user.id,
requestMetadata: extractNextApiRequestMetadata(ctx.req),
});
}),
findTemplates: authenticatedProcedure
.input(ZFindTemplatesQuerySchema)
.query(async ({ input, ctx }) => {
try {
return await findTemplates({
userId: ctx.user.id,
...input,
});
} catch (err) {
console.error(err);
throw AppError.parseErrorToTRPCError(err);
}
return await findTemplates({
userId: ctx.user.id,
...input,
});
}),
createTemplateDirectLink: authenticatedProcedure
.input(ZCreateTemplateDirectLinkMutationSchema)
.mutation(async ({ input, ctx }) => {
try {
const { templateId, teamId, directRecipientId } = input;
const { templateId, teamId, directRecipientId } = input;
const userId = ctx.user.id;
const userId = ctx.user.id;
const template = await getTemplateById({ id: templateId, teamId, userId: ctx.user.id });
const template = await getTemplateById({ id: templateId, teamId, userId: ctx.user.id });
const limits = await getServerLimits({ email: ctx.user.email, teamId: template.teamId });
const limits = await getServerLimits({ email: ctx.user.email, teamId: template.teamId });
if (limits.remaining.directTemplates === 0) {
throw new AppError(
AppErrorCode.LIMIT_EXCEEDED,
'You have reached your direct templates limit.',
);
}
return await createTemplateDirectLink({ userId, templateId, directRecipientId });
} catch (err) {
console.error(err);
const error = AppError.parseError(err);
throw AppError.parseErrorToTRPCError(error);
if (limits.remaining.directTemplates === 0) {
throw new AppError(AppErrorCode.LIMIT_EXCEEDED, {
message: 'You have reached your direct templates limit.',
});
}
return await createTemplateDirectLink({ userId, templateId, directRecipientId });
}),
deleteTemplateDirectLink: authenticatedProcedure
.input(ZDeleteTemplateDirectLinkMutationSchema)
.mutation(async ({ input, ctx }) => {
try {
const { templateId } = input;
const { templateId } = input;
const userId = ctx.user.id;
const userId = ctx.user.id;
return await deleteTemplateDirectLink({ userId, templateId });
} catch (err) {
console.error(err);
const error = AppError.parseError(err);
throw AppError.parseErrorToTRPCError(error);
}
return await deleteTemplateDirectLink({ userId, templateId });
}),
toggleTemplateDirectLink: authenticatedProcedure
.input(ZToggleTemplateDirectLinkMutationSchema)
.mutation(async ({ input, ctx }) => {
try {
const { templateId, enabled } = input;
const { templateId, enabled } = input;
const userId = ctx.user.id;
const userId = ctx.user.id;
return await toggleTemplateDirectLink({ userId, templateId, enabled });
} catch (err) {
console.error(err);
const error = AppError.parseError(err);
throw AppError.parseErrorToTRPCError(error);
}
return await toggleTemplateDirectLink({ userId, templateId, enabled });
}),
moveTemplateToTeam: authenticatedProcedure
.input(ZMoveTemplatesToTeamSchema)
.mutation(async ({ input, ctx }) => {
try {
const { templateId, teamId } = input;
const userId = ctx.user.id;
const { templateId, teamId } = input;
const userId = ctx.user.id;
return await moveTemplateToTeam({
templateId,
teamId,
userId,
});
} catch (err) {
console.error(err);
if (err instanceof TRPCError) {
throw err;
}
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'We were unable to move this template. Please try again later.',
});
}
return await moveTemplateToTeam({
templateId,
teamId,
userId,
});
}),
updateTemplateTypedSignatureSettings: authenticatedProcedure

View File

@@ -1,12 +1,37 @@
import { TRPCError, initTRPC } from '@trpc/server';
import SuperJSON from 'superjson';
import { AppError, genericErrorCodeToTrpcErrorCodeMap } from '@documenso/lib/errors/app-error';
import { isAdmin } from '@documenso/lib/next-auth/guards/is-admin';
import type { TrpcContext } from './context';
const t = initTRPC.context<TrpcContext>().create({
transformer: SuperJSON,
errorFormatter(opts) {
const { shape, error } = opts;
const originalError = error.cause;
let data: Record<string, unknown> = shape.data;
if (originalError instanceof AppError) {
data = {
...data,
appError: AppError.toJSON(originalError),
code: originalError.code,
httpStatus:
originalError.statusCode ??
genericErrorCodeToTrpcErrorCodeMap[originalError.code]?.status ??
500,
};
}
return {
...shape,
data,
};
},
});
/**

View File

@@ -98,7 +98,7 @@ export const twoFactorAuthenticationRouter = router({
console.error(err);
}
throw AppError.parseErrorToTRPCError(err);
throw error;
}
}),
});

View File

@@ -88,6 +88,8 @@ declare namespace NodeJS {
NEXT_PRIVATE_INNGEST_APP_ID?: string;
NEXT_PRIVATE_INNGEST_EVENT_KEY?: string;
NEXT_PRIVATE_LOGGER_HONEY_BADGER_API_KEY?: string;
/**
* Vercel environment variables
*/

View File

@@ -114,6 +114,7 @@
"NEXT_PRIVATE_TRIGGER_API_KEY",
"NEXT_PRIVATE_TRIGGER_API_URL",
"NEXT_PRIVATE_INNGEST_APP_ID",
"NEXT_PRIVATE_LOGGER_HONEY_BADGER_API_KEY",
"INNGEST_EVENT_KEY",
"NEXT_PRIVATE_INNGEST_EVENT_KEY",
"CI",