From 30849fc56048c9c6d6757324d4498795beab39e2 Mon Sep 17 00:00:00 2001 From: topaztee Date: Mon, 29 Jul 2024 15:01:13 +0300 Subject: [PATCH] change app error signature --- .gitignore | 1 + services/api/src/app.ts | 1 - services/api/src/errors/index.ts | 65 +++++++++++-------- services/api/src/middlewares/auth.ts | 6 +- services/api/src/middlewares/errors.ts | 24 +++---- services/api/src/middlewares/slack.ts | 8 ++- services/api/src/middlewares/webhooks.ts | 16 +++-- services/api/src/routers/chat.ts | 61 ++++++++++------- services/api/src/routers/db-index.ts | 39 +++++++---- services/api/src/routers/integrations.ts | 45 +++++++++---- services/api/src/routers/invite.ts | 46 +++++++++---- services/api/src/routers/oauth/atlassian.ts | 24 ++++--- services/api/src/routers/oauth/github.ts | 25 ++++--- services/api/src/routers/oauth/notion.ts | 25 ++++--- services/api/src/routers/oauth/pagerduty.ts | 25 ++++--- services/api/src/routers/oauth/slack.ts | 25 ++++--- services/api/src/routers/organizations.ts | 15 ++++- services/api/src/routers/users.ts | 35 +++++++--- .../routers/webhooks/alertmanager/router.ts | 16 ++++- .../api/src/routers/webhooks/github/router.ts | 8 +-- .../api/src/routers/webhooks/ory/router.ts | 9 ++- .../src/routers/webhooks/pagerduty/utils.ts | 17 ++--- services/api/src/services/oauth/atlassian.ts | 12 +++- services/api/src/services/oauth/pagerduty.ts | 12 +++- services/api/src/utils/errors.ts | 5 +- 25 files changed, 370 insertions(+), 195 deletions(-) diff --git a/.gitignore b/.gitignore index bd68e5a..7cb9003 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,7 @@ data *.launch .settings/ *.sublime-workspace +merlinn.iml # IDE - VSCode .vscode/* diff --git a/services/api/src/app.ts b/services/api/src/app.ts index db0bae2..1385a7f 100644 --- a/services/api/src/app.ts +++ b/services/api/src/app.ts @@ -42,7 +42,6 @@ app.use("/invite", inviteRouter); app.use("/organizations", organizationsRouter); app.use("/index", indexRouter); app.use("/features", featuresRouter); - app.all("*", invalidPathHandler); // Handle 404 // Global error handler diff --git a/services/api/src/errors/index.ts b/services/api/src/errors/index.ts index 6822c97..ea39e92 100644 --- a/services/api/src/errors/index.ts +++ b/services/api/src/errors/index.ts @@ -10,32 +10,41 @@ export enum ErrorCode { NO_INDEX = 37, } -export class AppError extends Error { - public override readonly message: string; - public readonly statusCode: number; - public readonly status: "fail" | "error"; - public readonly internalCode?: ErrorCode; - constructor( - message: string, - statusCode: number, - internalCode?: ErrorCode, - stack?: string, - ) { - super(message); - this.stack = stack; - this.message = message; - this.statusCode = statusCode; - this.status = `${statusCode}`.startsWith("4") ? "fail" : "error"; - this.internalCode = internalCode; - Error.captureStackTrace(this, this.constructor); - } - - public toJSON() { - return { - message: this.message, - status: this.status, - statusCode: this.statusCode, - internalCode: this.internalCode, - }; - } +export interface ErrorPayload { + message: string; + statusCode: number; + internalCode?: ErrorCode; + stack?: string; + context?: Record; } + +export const AppError = ({ + message, + statusCode, + internalCode, + stack, + context, +}: ErrorPayload) => { + const status = `${statusCode}`.startsWith("4") ? "fail" : "error"; + + const appError = { + message, + statusCode, + status, + internalCode, + context, + stack, + toJSON() { + return { + message: this.message, + status: this.status, + statusCode: this.statusCode, + internalCode: this.internalCode, + }; + }, + }; + + Error.captureStackTrace(appError, AppError); + + return appError; +}; diff --git a/services/api/src/middlewares/auth.ts b/services/api/src/middlewares/auth.ts index 6e73855..177fc99 100644 --- a/services/api/src/middlewares/auth.ts +++ b/services/api/src/middlewares/auth.ts @@ -39,7 +39,11 @@ export const getDBUser = catchAsync( }, }); if (!user) { - throw new AppError("No internal user", 401, ErrorCode.NO_INTERNAL_USER); + throw AppError({ + message: "No internal user", + statusCode: 401, + internalCode: ErrorCode.NO_INTERNAL_USER, + }); } req.user = user as IUser; next(); diff --git a/services/api/src/middlewares/errors.ts b/services/api/src/middlewares/errors.ts index 17396ac..5e97e2d 100644 --- a/services/api/src/middlewares/errors.ts +++ b/services/api/src/middlewares/errors.ts @@ -1,9 +1,9 @@ import { Request, Response, NextFunction } from "express"; -import { AppError } from "../errors"; +import { AppError, ErrorPayload } from "../errors"; import { PostHogClient } from "../telemetry/posthog"; import { uuid } from "uuidv4"; -const captureErrorInTelemetry = (error: AppError, req: Request) => { +const captureErrorInTelemetry = (error: ErrorPayload, req: Request) => { const posthog = new PostHogClient(); const distinctId = req.user?._id.toString() || uuid(); @@ -13,28 +13,29 @@ const captureErrorInTelemetry = (error: AppError, req: Request) => { distinctId, properties: { message: error.message, + context: error.context, }, }); }; -const productionError = (error: AppError, req: Request, res: Response) => { +const productionError = (error: ErrorPayload, req: Request, res: Response) => { captureErrorInTelemetry(error, req); // Send a lean error message res.status(error.statusCode).json({ - status: error.status, + status: error.statusCode, message: error.message, code: error.internalCode, }); }; // Send a detailed error message, for debugging purposes -const developmentError = (error: AppError, req: Request, res: Response) => { +const developmentError = (error: ErrorPayload, req: Request, res: Response) => { console.error("developmentError error: ", error); captureErrorInTelemetry(error, req); res.status(error.statusCode).json({ - status: error.status, + status: error.statusCode, message: error.message, code: error.internalCode, error: error, @@ -43,7 +44,7 @@ const developmentError = (error: AppError, req: Request, res: Response) => { }; export const errorHandler = ( - error: AppError, + error: ErrorPayload, req: Request, res: Response, // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -65,9 +66,10 @@ export const invalidPathHandler = ( _res: Response, next: NextFunction, ) => { - const error = new AppError( - `Path ${req.originalUrl} does not exist for ${req.method} method`, - 404, - ); + const error = AppError({ + message: `Path ${req.originalUrl} does not exist for ${req.method} method`, + statusCode: 404, + internalCode: undefined, + }); next(error); }; diff --git a/services/api/src/middlewares/slack.ts b/services/api/src/middlewares/slack.ts index 0563e8c..e3bc7e9 100644 --- a/services/api/src/middlewares/slack.ts +++ b/services/api/src/middlewares/slack.ts @@ -8,7 +8,7 @@ export const getSlackUser = catchAsync( const claimedToken = req.headers["x-slack-app-token"]; const actualToken = process.env.SLACK_APP_TOKEN as string; if (claimedToken !== actualToken) { - throw new AppError("Token is invalid", 401); + throw AppError({ message: "Token is invalid", statusCode: 401 }); } const email = req.headers["x-slack-email"]; const team = req.headers["x-slack-team"]; @@ -18,7 +18,11 @@ export const getSlackUser = catchAsync( "metadata.team.id": team, }); if (!slackIntegration) { - throw new AppError("No slack integration", 401, ErrorCode.NO_INTEGRATION); + throw AppError({ + message: "No slack integration", + statusCode: 401, + internalCode: ErrorCode.NO_INTEGRATION, + }); } const { organization } = slackIntegration; diff --git a/services/api/src/middlewares/webhooks.ts b/services/api/src/middlewares/webhooks.ts index 4bc8974..9402d99 100644 --- a/services/api/src/middlewares/webhooks.ts +++ b/services/api/src/middlewares/webhooks.ts @@ -13,15 +13,19 @@ export function getSecretFromRequest(req: Request) { } else { const authHeader = req.headers["authorization"] as string; if (!authHeader) { - throw new AppError( - "Request does not contain a secret header (either custom or auth header)", - 400, - ); + throw AppError({ + message: + "Request does not contain a secret header (either custom or auth header)", + statusCode: 400, + }); } // Check it's a valid Bearer token if (!authHeader.startsWith("Bearer ")) { - throw new AppError("Request does not contain a valid Bearer token", 400); + throw AppError({ + message: "Request does not contain a valid Bearer token", + statusCode: 400, + }); } const [, authHeaderSecret] = authHeader.split(" "); @@ -41,7 +45,7 @@ export const checkWebhookSecret = catchAsync( .populate("organization"); if (!webhook) { - throw new AppError("Secret is invalid", 400); + throw AppError({ message: "Secret is invalid", statusCode: 400 }); } req.webhook = webhook as IWebhook; diff --git a/services/api/src/routers/chat.ts b/services/api/src/routers/chat.ts index 767d5da..3a1e94b 100644 --- a/services/api/src/routers/chat.ts +++ b/services/api/src/routers/chat.ts @@ -23,13 +23,17 @@ const router = express.Router(); const getCompletions = async (req: Request, res: Response) => { if (!req.user) { - throw new AppError("No internal user", 403, ErrorCode.NO_INTERNAL_USER); + throw AppError({ + message: "No internal user", + statusCode: 403, + internalCode: ErrorCode.NO_INTERNAL_USER, + }); } else if (req.user.status === "invited") { - throw new AppError( - "User hasn't accepted the invitation yet", - 403, - ErrorCode.INVITATION_NOT_ACCEPTED, - ); + throw AppError({ + message: "User hasn't accepted the invitation yet", + statusCode: 403, + internalCode: ErrorCode.INVITATION_NOT_ACCEPTED, + }); } if (isEnterprise()) { @@ -39,11 +43,11 @@ const getCompletions = async (req: Request, res: Response) => { userId: String(req.user!._id), }); if (!queriesState.isAllowed) { - throw new AppError( - `You have exceeded your queries' quota`, - 429, - ErrorCode.QUOTA_EXCEEDED, - ); + throw AppError({ + message: `You have exceeded your queries' quota`, + statusCode: 429, + internalCode: ErrorCode.QUOTA_EXCEEDED, + }); } // Update quota @@ -65,7 +69,11 @@ const getCompletions = async (req: Request, res: Response) => { }) .populate("vendor")) as IIntegration[]; if (!integrations.length) { - throw new AppError("No integrations at all", 404, ErrorCode.NO_INTEGRATION); + throw AppError({ + message: "No integrations at all", + statusCode: 404, + internalCode: ErrorCode.NO_INTEGRATION, + }); } let output: string | null = null; @@ -78,7 +86,7 @@ const getCompletions = async (req: Request, res: Response) => { // const moderationResult = await validateModeration(message.content as string); // if (!moderationResult) { - // throw new AppError( + // throw AppError({ message: // "Text was found that violates our content policy", // 400, // ErrorCode.MODERATION_FAILED, @@ -133,12 +141,12 @@ const getCompletions = async (req: Request, res: Response) => { // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (error: any) { console.error(error); - throw new AppError( - error.message, - 500, - ErrorCode.AGENT_RUN_FAILED, - error.stack, - ); + throw AppError({ + message: error.message, + statusCode: 500, + internalCode: ErrorCode.AGENT_RUN_FAILED, + stack: error.stack, + }); } } else { try { @@ -153,7 +161,11 @@ const getCompletions = async (req: Request, res: Response) => { observationId = result.observationId!; // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (error: any) { - throw new AppError(error.message, 500, ErrorCode.MODEL_RUN_FAILED); + throw AppError({ + message: error.message, + statusCode: 500, + internalCode: ErrorCode.MODEL_RUN_FAILED, + }); } } @@ -211,10 +223,11 @@ router.post( const { traceId, observationId, value, text } = req.body; if (isLangfuseEnabled()) { if (!traceId || !observationId || !value) { - throw new AppError( - "Bad request. Need to supply traceId, observationId and value", - 400, - ); + throw AppError({ + message: + "Bad request. Need to supply traceId, observationId and value", + statusCode: 400, + }); } const langfuse = new Langfuse({ secretKey: process.env.LANGFUSE_SECRET_KEY as string, diff --git a/services/api/src/routers/db-index.ts b/services/api/src/routers/db-index.ts index c7d9e94..949c985 100644 --- a/services/api/src/routers/db-index.ts +++ b/services/api/src/routers/db-index.ts @@ -23,7 +23,10 @@ router.get( "/", catchAsync(async (req: Request, res: Response) => { if (req.user!.role !== "owner") { - throw new AppError("Only owners can access indexes", 403); + throw AppError({ + message: "Only owners can access indexes", + statusCode: 403, + }); } const index = await indexModel.getOne({ @@ -38,7 +41,10 @@ router.post( "/", catchAsync(async (req: Request, res: Response) => { if (req.user!.role !== "owner") { - throw new AppError("Only owners are allowed to create indexes", 403); + throw AppError({ + message: "Only owners are allowed to create indexes", + statusCode: 403, + }); } if (isEnterprise()) { @@ -47,18 +53,18 @@ router.post( organizationId: String(req.user!.organization._id), }); if (!attemptsState.isAllowed) { - throw new AppError( - `You have exceeded your indexing attempts' quota`, - 429, - ErrorCode.QUOTA_EXCEEDED, - ); + throw AppError({ + message: `You have exceeded your indexing attempts' quota`, + statusCode: 429, + internalCode: ErrorCode.QUOTA_EXCEEDED, + }); } } // TODO: use a proper messaging solution instead of plain API request const { dataSources } = req.body; if (!dataSources) { - throw new AppError("No data sources provided", 400); + throw AppError({ message: "No data sources provided", statusCode: 400 }); } const integrations = await Promise.all( @@ -70,7 +76,10 @@ router.post( }, ); if (!integration) { - throw new AppError(`No such integration "${source}"`, 404); + throw AppError({ + message: `No such integration "${source}"`, + statusCode: 404, + }); } return integration; }), @@ -118,15 +127,21 @@ router.delete( "/:id", catchAsync(async (req: Request, res: Response) => { if (req.user!.role !== "owner") { - throw new AppError("Only owners can delete indexes", 403); + throw AppError({ + message: "Only owners can delete indexes", + statusCode: 403, + }); } const { id } = req.params; const index = await indexModel.getOneById(id); if (!index) { - throw new AppError("No such embeddings db", 404); + throw AppError({ message: "No such embeddings db", statusCode: 404 }); } else if (!req.user!.organization._id.equals(index.organization._id)) { - throw new AppError("User is not a member of this organization", 403); + throw AppError({ + message: "User is not a member of this organization", + statusCode: 403, + }); } try { diff --git a/services/api/src/routers/integrations.ts b/services/api/src/routers/integrations.ts index 3789265..2c89591 100644 --- a/services/api/src/routers/integrations.ts +++ b/services/api/src/routers/integrations.ts @@ -25,16 +25,23 @@ router.post( const vendor = await vendorModel.getOne({ name: vendorName }); const organization = await organizationModel.getOneById(organizationId); if (!vendor) { - throw new AppError( - "Could not find a PagerDuty vendor. Make sure a vendor is defined.", - 404, - ); + throw AppError({ + message: + "Could not find a PagerDuty vendor. Make sure a vendor is defined.", + statusCode: 404, + }); } else if (!organization) { - throw new AppError("Could not find the given organization.", 404); + throw AppError({ + message: "Could not find the given organization.", + statusCode: 404, + }); } if (!req.user!.organization._id.equals(organizationId)) { - throw new AppError("User is not a member of this organization", 403); + throw AppError({ + message: "User is not a member of this organization", + statusCode: 403, + }); } const extendedMetadata: Record = {}; @@ -103,13 +110,19 @@ router.put( const integration = await integrationModel.getOneById(id); if (!integration) { - throw new AppError("No such integration", 404); + throw AppError({ message: "No such integration", statusCode: 404 }); } if (!req.user!.organization._id.equals(integration.organization._id)) { - throw new AppError("User is not a member of this organization", 403); + throw AppError({ + message: "User is not a member of this organization", + statusCode: 403, + }); } else if (req.user!.role !== "owner") { - throw new AppError("User is not allowed to perform this action", 403); + throw AppError({ + message: "User is not allowed to perform this action", + statusCode: 403, + }); } return res.status(200).json({ integration }); @@ -141,7 +154,7 @@ router.get( const actualServiceKey = req.headers["x-slackbot-service-key"]; const expectedServiceKey = process.env.SLACKBOT_SERVICE_KEY as string; if (actualServiceKey !== expectedServiceKey) { - throw new AppError("Unauthorized", 403); + throw AppError({ message: "Unauthorized", statusCode: 403 }); } const integrations = await integrationModel @@ -162,7 +175,10 @@ router.delete( getDBUser, catchAsync(async (req: Request, res: Response) => { if (req.user!.role !== "owner") { - throw new AppError("Only owners can delete integrations", 403); + throw AppError({ + message: "Only owners can delete integrations", + statusCode: 403, + }); } const integration = await integrationModel.getOne({ @@ -171,11 +187,14 @@ router.delete( }); if (!integration) { - throw new AppError("No such integration", 404); + throw AppError({ message: "No such integration", statusCode: 404 }); } else if ( !req.user!.organization._id.equals(integration.organization._id) ) { - throw new AppError("User is not a member of this organization", 403); + throw AppError({ + message: "User is not a member of this organization", + statusCode: 403, + }); } await secretManager.deleteCredentials([integration]); diff --git a/services/api/src/routers/invite.ts b/services/api/src/routers/invite.ts index dfdf589..e2ccb78 100644 --- a/services/api/src/routers/invite.ts +++ b/services/api/src/routers/invite.ts @@ -25,17 +25,20 @@ router.get( "/import", catchAsync(async (req: Request, res: Response) => { if (req.user!.role !== "owner") { - throw new AppError("Only owners can invite members", 403); + throw AppError({ + message: "Only owners can invite members", + statusCode: 403, + }); } const { source } = req.query; const allowedSources = ["PagerDuty", "Opsgenie"]; if (!allowedSources.includes(source as string)) { - throw new AppError( - `Source is invalid. Allowed sources: ${allowedSources.join(", ")}`, - 400, - ); + throw AppError({ + message: `Source is invalid. Allowed sources: ${allowedSources.join(", ")}`, + statusCode: 400, + }); } let integration = (await integrationModel.getIntegrationByName( @@ -45,10 +48,10 @@ router.get( }, )) as IIntegration; if (!integration) { - throw new AppError( - `Your organization do not have an integration with ${source}`, - 404, - ); + throw AppError({ + message: `Your organization do not have an integration with ${source}`, + statusCode: 404, + }); } switch (source) { @@ -62,7 +65,10 @@ router.get( const usersData = await opsgenieClient.getUsers(); if (!usersData) { - throw new AppError(`Could not fetch users from ${source}`, 500); + throw AppError({ + message: `Could not fetch users from ${source}`, + statusCode: 500, + }); } return res.status(200).json({ users: usersData.data }); } @@ -77,12 +83,18 @@ router.get( const pagerdutyClient = new PagerDutyClient(access_token); const users = await pagerdutyClient.getUsers(); if (!users) { - throw new AppError(`Could not fetch users from ${source}`, 500); + throw AppError({ + message: `Could not fetch users from ${source}`, + statusCode: 500, + }); } return res.status(200).json({ users }); } default: { - throw new AppError(`Source ${source} is not supported`, 400); + throw AppError({ + message: `Source ${source} is not supported`, + statusCode: 400, + }); } } }), @@ -92,7 +104,10 @@ router.post( "/", catchAsync(async (req: Request, res: Response) => { if (req.user!.role !== "owner") { - throw new AppError("Only owners can invite members", 403); + throw AppError({ + message: "Only owners can invite members", + statusCode: 403, + }); } const emails = req.body.emails as string[]; @@ -103,7 +118,10 @@ router.post( }); if (seatsState.value + emails.length > seatsState.limit) { - throw new AppError("You have exceeded your plan's seats", 400); + throw AppError({ + message: "You have exceeded your plan's seats", + statusCode: 400, + }); } await incrementPlanFieldState({ fieldCode: PlanFieldCode.seats, diff --git a/services/api/src/routers/oauth/atlassian.ts b/services/api/src/routers/oauth/atlassian.ts index 2b75d80..93f4052 100644 --- a/services/api/src/routers/oauth/atlassian.ts +++ b/services/api/src/routers/oauth/atlassian.ts @@ -20,9 +20,9 @@ router.get( "https" + "://" + req.get("host") + "/oauth/atlassian/callback"; if (!code) { - throw new AppError("No code was provided", 400); + throw AppError({ message: "No code was provided", statusCode: 400 }); } else if (!state) { - throw new AppError("No state was provided", 400); + throw AppError({ message: "No state was provided", statusCode: 400 }); } try { const clientId = process.env.ATLASSIAN_CLIENT_ID as string; @@ -48,15 +48,18 @@ router.get( name: vendorName, }); if (!vendor) { - throw new AppError( - `Could not find an ${vendorName} vendor. Make sure a vendor is defined.`, - 404, - ); + throw AppError({ + message: `Could not find an ${vendorName} vendor. Make sure a vendor is defined.`, + statusCode: 404, + }); } const organization = await organizationModel.getOneById(state as string); if (!organization) { - throw new AppError("Could not find the given organization.", 404); + throw AppError({ + message: "Could not find the given organization.", + statusCode: 404, + }); } const formattedCredentials = (await secretManager.createCredentials( @@ -81,9 +84,12 @@ router.get( } if (error instanceof AxiosError) { if (error.response) { - throw new AppError(JSON.stringify(error.response.data), 500); + throw AppError({ + message: JSON.stringify(error.response.data), + statusCode: 500, + }); } - throw new AppError(error.message, 500); + throw AppError({ message: error.message, statusCode: 500 }); } throw error; } diff --git a/services/api/src/routers/oauth/github.ts b/services/api/src/routers/oauth/github.ts index c0470ee..09996fc 100644 --- a/services/api/src/routers/oauth/github.ts +++ b/services/api/src/routers/oauth/github.ts @@ -12,9 +12,9 @@ router.get( catchAsync(async (req: Request, res: Response) => { const { code, state } = req.query; if (!code) { - throw new AppError("No code was provided", 400); + throw AppError({ message: "No code was provided", statusCode: 400 }); } else if (!state) { - throw new AppError("No state was provided", 400); + throw AppError({ message: "No state was provided", statusCode: 400 }); } try { @@ -42,12 +42,16 @@ router.get( const vendor = await vendorModel.getOne({ name: "Github" }); const organization = await organizationModel.getOneById(state as string); if (!vendor) { - throw new AppError( - "Could not find a Github vendor. Make sure a vendor is defined.", - 404, - ); + throw AppError({ + message: + "Could not find a Github vendor. Make sure a vendor is defined.", + statusCode: 404, + }); } else if (!organization) { - throw new AppError("Could not find the given organization.", 404); + throw AppError({ + message: "Could not find the given organization.", + statusCode: 404, + }); } const { access_token, ...metadata } = credentials; @@ -90,9 +94,12 @@ router.get( } if (error instanceof AxiosError) { if (error.response) { - throw new AppError(JSON.stringify(error.response.data), 500); + throw AppError({ + message: JSON.stringify(error.response.data), + statusCode: 500, + }); } - throw new AppError(error.message, 500); + throw AppError({ message: error.message, statusCode: 500 }); } throw error; } diff --git a/services/api/src/routers/oauth/notion.ts b/services/api/src/routers/oauth/notion.ts index 4ac793d..bf023bc 100644 --- a/services/api/src/routers/oauth/notion.ts +++ b/services/api/src/routers/oauth/notion.ts @@ -19,9 +19,9 @@ router.get( "https" + "://" + req.get("host") + "/oauth/notion/callback"; if (!code) { - throw new AppError("No code was provided", 400); + throw AppError({ message: "No code was provided", statusCode: 400 }); } else if (!state) { - throw new AppError("No state was provided", 400); + throw AppError({ message: "No state was provided", statusCode: 400 }); } try { const clientId = process.env.NOTION_CLIENT_ID as string; @@ -53,12 +53,16 @@ router.get( const vendor = await vendorModel.getOne({ name: "Notion" }); const organization = await organizationModel.getOneById(state as string); if (!vendor) { - throw new AppError( - "Could not find a Notion vendor. Make sure a vendor is defined.", - 404, - ); + throw AppError({ + message: + "Could not find a Notion vendor. Make sure a vendor is defined.", + statusCode: 404, + }); } else if (!organization) { - throw new AppError("Could not find the given organization.", 404); + throw AppError({ + message: "Could not find the given organization.", + statusCode: 404, + }); } const formattedCredentials = (await secretManager.createCredentials( @@ -83,9 +87,12 @@ router.get( } if (error instanceof AxiosError) { if (error.response) { - throw new AppError(JSON.stringify(error.response.data), 500); + throw AppError({ + message: JSON.stringify(error.response.data), + statusCode: 500, + }); } - throw new AppError(error.message, 500); + throw AppError({ message: error.message, statusCode: 500 }); } throw error; } diff --git a/services/api/src/routers/oauth/pagerduty.ts b/services/api/src/routers/oauth/pagerduty.ts index eb8ae9d..6f5eb1e 100644 --- a/services/api/src/routers/oauth/pagerduty.ts +++ b/services/api/src/routers/oauth/pagerduty.ts @@ -19,9 +19,9 @@ router.get( "https" + "://" + req.get("host") + "/oauth/pagerduty/callback"; if (!code) { - throw new AppError("No code was provided", 400); + throw AppError({ message: "No code was provided", statusCode: 400 }); } else if (!state) { - throw new AppError("No state was provided", 400); + throw AppError({ message: "No state was provided", statusCode: 400 }); } try { @@ -36,12 +36,16 @@ router.get( const vendor = await vendorModel.getOne({ name: "PagerDuty" }); const organization = await organizationModel.getOneById(state as string); if (!vendor) { - throw new AppError( - "Could not find a PagerDuty vendor. Make sure a vendor is defined.", - 404, - ); + throw AppError({ + message: + "Could not find a PagerDuty vendor. Make sure a vendor is defined.", + statusCode: 404, + }); } else if (!organization) { - throw new AppError("Could not find the given organization.", 404); + throw AppError({ + message: "Could not find the given organization.", + statusCode: 404, + }); } const { id_token, access_token, refresh_token, ...metadata } = @@ -73,9 +77,12 @@ router.get( } if (error instanceof AxiosError) { if (error.response) { - throw new AppError(JSON.stringify(error.response.data), 500); + throw AppError({ + message: JSON.stringify(error.response.data), + statusCode: 500, + }); } - throw new AppError(error.message, 500); + throw AppError({ message: error.message, statusCode: 500 }); } throw error; } diff --git a/services/api/src/routers/oauth/slack.ts b/services/api/src/routers/oauth/slack.ts index 81c1e30..566d995 100644 --- a/services/api/src/routers/oauth/slack.ts +++ b/services/api/src/routers/oauth/slack.ts @@ -23,9 +23,9 @@ router.get( "https" + "://" + req.get("host") + "/oauth/slack/callback"; if (!code) { - throw new AppError("No code was provided", 400); + throw AppError({ message: "No code was provided", statusCode: 400 }); } else if (!state) { - throw new AppError("No state was provided", 400); + throw AppError({ message: "No state was provided", statusCode: 400 }); } try { const params = new URLSearchParams(); @@ -58,12 +58,16 @@ router.get( const vendor = await vendorModel.getOne({ name: "Slack" }); const organization = await organizationModel.getOneById(state as string); if (!vendor) { - throw new AppError( - "Could not find a Slack vendor. Make sure a vendor is defined.", - 404, - ); + throw AppError({ + message: + "Could not find a Slack vendor. Make sure a vendor is defined.", + statusCode: 404, + }); } else if (!organization) { - throw new AppError("Could not find the given organization.", 404); + throw AppError({ + message: "Could not find the given organization.", + statusCode: 404, + }); } const credentials = (await secretManager.createCredentials( @@ -88,9 +92,12 @@ router.get( } if (error instanceof AxiosError) { if (error.response) { - throw new AppError(JSON.stringify(error.response.data), 500); + throw AppError({ + message: JSON.stringify(error.response.data), + statusCode: 500, + }); } - throw new AppError(error.message, 500); + throw AppError({ message: error.message, statusCode: 500 }); } throw error; } diff --git a/services/api/src/routers/organizations.ts b/services/api/src/routers/organizations.ts index c7ca990..ceb2523 100644 --- a/services/api/src/routers/organizations.ts +++ b/services/api/src/routers/organizations.ts @@ -27,7 +27,10 @@ router.post( const { name } = req.body; if (req.user!.organization) { - throw new AppError("You already belong to an organization", 400); + throw AppError({ + message: "You already belong to an organization", + statusCode: 400, + }); } const plan = await planModel.getOne({ name: "free" }); @@ -73,7 +76,10 @@ router.put( "/:id", catchAsync(async (req: Request, res: Response) => { if (req.user!.role !== "owner") { - throw new AppError("Only owners can update organization data", 403); + throw AppError({ + message: "Only owners can update organization data", + statusCode: 403, + }); } const { id } = req.params; @@ -91,7 +97,10 @@ router.delete( "/:id", catchAsync(async (req: Request, res: Response) => { if (req.user!.role !== "owner") { - throw new AppError("Only owners can delete organizations", 403); + throw AppError({ + message: "Only owners can delete organizations", + statusCode: 403, + }); } const { id } = req.params; diff --git a/services/api/src/routers/users.ts b/services/api/src/routers/users.ts index 7c00afe..14f4d35 100644 --- a/services/api/src/routers/users.ts +++ b/services/api/src/routers/users.ts @@ -68,12 +68,15 @@ router.post( catchAsync(async (req: Request, res: Response) => { const { oryId, email } = req.body; if (!oryId || !email) { - throw new AppError("Payload must contain the Ory ID", 400); + throw AppError({ + message: "Payload must contain the Ory ID", + statusCode: 400, + }); } const existingUser = await userModel.getOne({ oryId }); if (existingUser) { - throw new AppError("user already exists", 400); + throw AppError({ message: "user already exists", statusCode: 400 }); } else { const user = await userModel.create({ oryId, @@ -109,9 +112,12 @@ router.put( const { id } = req.params; if (!id) { - throw new AppError("Payload must contain an ID", 400); + throw AppError({ + message: "Payload must contain an ID", + statusCode: 400, + }); } else if (!Object.keys(data).length) { - throw new AppError("Payload must not be empty", 400); + throw AppError({ message: "Payload must not be empty", statusCode: 400 }); } const user = await userModel @@ -119,7 +125,7 @@ router.put( .populate("organization"); if (!user) { - throw new AppError("User was not found", 404); + throw AppError({ message: "User was not found", statusCode: 404 }); } return res.status(200).json(user); }), @@ -131,7 +137,10 @@ router.put( const { id } = req.params; if (!id) { - throw new AppError("Payload must contain an ID", 400); + throw AppError({ + message: "Payload must contain an ID", + statusCode: 400, + }); } const user = await userModel @@ -139,7 +148,7 @@ router.put( .populate("organization"); if (!user) { - throw new AppError("User was not found", 404); + throw AppError({ message: "User was not found", statusCode: 404 }); } const event: SystemEvent = { @@ -165,11 +174,17 @@ router.delete( const user = await userModel.getOneById(id); if (!user) { - throw new AppError("User was not found", 404); + throw AppError({ message: "User was not found", statusCode: 404 }); } else if (!req.user!.organization._id.equals(user.organization._id)) { - throw new AppError("Users not in the same organization", 403); + throw AppError({ + message: "Users not in the same organization", + statusCode: 403, + }); } else if (req.user!.role !== "owner") { - throw new AppError("Only owners can delete other users", 403); + throw AppError({ + message: "Only owners can delete other users", + statusCode: 403, + }); } await userModel.deleteOneById(id); diff --git a/services/api/src/routers/webhooks/alertmanager/router.ts b/services/api/src/routers/webhooks/alertmanager/router.ts index 9fd9c9e..63691dd 100644 --- a/services/api/src/routers/webhooks/alertmanager/router.ts +++ b/services/api/src/routers/webhooks/alertmanager/router.ts @@ -53,9 +53,15 @@ router.post( (integration) => integration.vendor.name === "Prometheus", ) as PrometheusIntegration; if (!prometheusIntegration) { - throw new AppError("Prometheus integration was not found", 500); + throw AppError({ + message: "Prometheus integration was not found", + statusCode: 500, + }); } else if (!slackIntegration) { - throw new AppError("Slack integration was not found", 500); + throw AppError({ + message: "Slack integration was not found", + statusCode: 500, + }); } slackIntegration = ( @@ -147,7 +153,11 @@ router.post( return res.status(200).send("ok"); // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (error: any) { - throw new AppError(error.message, 500, ErrorCode.AGENT_RUN_FAILED); + throw AppError({ + message: error.message, + statusCode: 500, + internalCode: ErrorCode.AGENT_RUN_FAILED, + }); } }), ); diff --git a/services/api/src/routers/webhooks/github/router.ts b/services/api/src/routers/webhooks/github/router.ts index 98e4c90..dc2a451 100644 --- a/services/api/src/routers/webhooks/github/router.ts +++ b/services/api/src/routers/webhooks/github/router.ts @@ -26,7 +26,7 @@ const verifySignature = catchAsync( .digest("hex"); if (!req.headers["x-hub-signature-256"]) { - throw new AppError("Missing signature", 400); + throw AppError({ message: "Missing signature", statusCode: 400 }); } const trusted = Buffer.from(`sha256=${signature}`, "ascii"); const untrusted = Buffer.from( @@ -49,7 +49,7 @@ router.post( catchAsync(async (req: Request, res: Response) => { const githubOrgId = req.body.organization.id; if (!githubOrgId) { - throw new AppError("Missing installation ID", 400); + throw AppError({ message: "Missing installation ID", statusCode: 400 }); } const { action, issue, sender, repository, comment } = req.body; if (sender.type === "Bot") { @@ -65,7 +65,7 @@ router.post( .populate("organization")) as GithubIntegration; if (!githubIntegration) { - throw new AppError("No github integration", 404); + throw AppError({ message: "No github integration", statusCode: 404 }); } githubIntegration = ( @@ -125,7 +125,7 @@ router.post( // ); // if (!moderationResult) { - // throw new AppError( + // throw AppError({ message: // "Text was found that violates our content policy", // 400, // ErrorCode.MODERATION_FAILED, diff --git a/services/api/src/routers/webhooks/ory/router.ts b/services/api/src/routers/webhooks/ory/router.ts index 0df67a1..bc8cbd5 100644 --- a/services/api/src/routers/webhooks/ory/router.ts +++ b/services/api/src/routers/webhooks/ory/router.ts @@ -9,7 +9,7 @@ router.post("/after-signup", async (req: Request, res: Response) => { const actualKey = req.headers["authorization"]; const expectedKey = process.env.ORY_WEBHOOK_SECRET as string; if (actualKey !== expectedKey) { - throw new AppError("Unauthorized", 403); + throw AppError({ message: "Unauthorized", statusCode: 403 }); } const { @@ -17,7 +17,10 @@ router.post("/after-signup", async (req: Request, res: Response) => { traits: { email }, } = req.body; if (!oryId || !email) { - throw new AppError("Missing required fields: oryId, email", 400); + throw AppError({ + message: "Missing required fields: oryId, email", + statusCode: 400, + }); } const user = await userModel.getOne({ oryId }); if (!user) { @@ -44,7 +47,7 @@ router.post("/after-signup", async (req: Request, res: Response) => { .populate("organization"); if (!user) { - throw new AppError("User was not found", 404); + throw AppError({ message: "User was not found", statusCode: 404 }); } const event: SystemEvent = { diff --git a/services/api/src/routers/webhooks/pagerduty/utils.ts b/services/api/src/routers/webhooks/pagerduty/utils.ts index 536b0ad..0cc450d 100644 --- a/services/api/src/routers/webhooks/pagerduty/utils.ts +++ b/services/api/src/routers/webhooks/pagerduty/utils.ts @@ -8,18 +8,19 @@ export const checkPagerDutySignature = catchAsync( const signatures = req.headers["x-pagerduty-signature"] as string; if (!signatures) { - throw new AppError( - "Unauthorized webhook request. Signatures are not present in the request", - 403, - ); + throw AppError({ + message: + "Unauthorized webhook request. Signatures are not present in the request", + statusCode: 403, + }); } const isValid = pdVerifier.verify(JSON.stringify(req.body), signatures); if (!isValid) { - throw new AppError( - "Unauthorized webhook request. Signatures are invalid", - 403, - ); + throw AppError({ + message: "Unauthorized webhook request. Signatures are invalid", + statusCode: 403, + }); } return next(); diff --git a/services/api/src/services/oauth/atlassian.ts b/services/api/src/services/oauth/atlassian.ts index ada4169..a4c11b9 100644 --- a/services/api/src/services/oauth/atlassian.ts +++ b/services/api/src/services/oauth/atlassian.ts @@ -11,7 +11,10 @@ export async function refreshToken(integrationId: string) { integrationId, )) as AtlassianIntegration; if (!integration) { - throw new AppError("Could not find the given integration.", 404); + throw AppError({ + message: "Could not find the given integration.", + statusCode: 404, + }); } const populatedIntegration = ( await secretManager.populateCredentials([integration]) @@ -48,9 +51,12 @@ export async function refreshToken(integrationId: string) { } if (error instanceof AxiosError) { if (error.response) { - throw new AppError(JSON.stringify(error.response.data), 500); + throw AppError({ + message: JSON.stringify(error.response.data), + statusCode: 500, + }); } - throw new AppError(error.message, 500); + throw AppError({ message: error.message, statusCode: 500 }); } throw error; } diff --git a/services/api/src/services/oauth/pagerduty.ts b/services/api/src/services/oauth/pagerduty.ts index d1f53ec..be8fd24 100644 --- a/services/api/src/services/oauth/pagerduty.ts +++ b/services/api/src/services/oauth/pagerduty.ts @@ -11,7 +11,10 @@ export async function refreshToken(integrationId: string) { integrationId, )) as PagerDutyIntegration; if (!integration) { - throw new AppError("Could not find the given integration.", 404); + throw AppError({ + message: "Could not find the given integration.", + statusCode: 404, + }); } const populatedIntegration = ( await secretManager.populateCredentials([integration]) @@ -51,9 +54,12 @@ export async function refreshToken(integrationId: string) { } if (error instanceof AxiosError) { if (error.response) { - throw new AppError(JSON.stringify(error.response.data), 500); + throw AppError({ + message: JSON.stringify(error.response.data), + statusCode: 500, + }); } - throw new AppError(error.message, 500); + throw AppError({ message: error.message, statusCode: 500 }); } throw error; } diff --git a/services/api/src/utils/errors.ts b/services/api/src/utils/errors.ts index b2aee64..75ee6fe 100644 --- a/services/api/src/utils/errors.ts +++ b/services/api/src/utils/errors.ts @@ -13,7 +13,10 @@ export const catchAsync = ( if (error instanceof AppError) { next(error); } else { - const newError = new AppError(error.message || "Internal error", 500); + const newError = AppError({ + message: error.message || "Internal error", + statusCode: 500, + }); newError.stack = error.stack; next(newError); }