mirror of
https://github.com/documenso/documenso.git
synced 2025-11-16 01:32:06 +10:00
feat: add initial api logging (#1494)
Improve API logging and error handling between client and server side.
This commit is contained in:
@ -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;
|
||||
}
|
||||
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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 } =
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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 = {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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',
|
||||
});
|
||||
};
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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.',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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.`,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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 });
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 });
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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.`,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
};
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
108
packages/lib/utils/logger.ts
Normal file
108
packages/lib/utils/logger.ts
Normal 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.
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user