Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactoring backend codebase to use central error handling and logging #152

Merged
merged 8 commits into from
Dec 25, 2022
2 changes: 2 additions & 0 deletions backend/environment.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ declare global {
JWT_SIGNUP_SECRET: string;
MONGO_URL: string;
NODE_ENV: 'development' | 'staging' | 'testing' | 'production';
VERBOSE_ERROR_OUTPUT: string;
LOKI_HOST: string;
CLIENT_ID_HEROKU: string;
CLIENT_ID_VERCEL: string;
CLIENT_ID_NETLIFY: string;
Expand Down
11,475 changes: 404 additions & 11,071 deletions backend/package-lock.json

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@
"stripe": "^10.7.0",
"tweetnacl": "^1.0.3",
"tweetnacl-util": "^0.15.1",
"typescript": "^4.9.3"
"typescript": "^4.9.3",
"winston": "^3.8.2",
"winston-loki": "^6.0.6"
},
"name": "infisical-api",
"version": "1.0.0",
Expand Down
19 changes: 17 additions & 2 deletions backend/src/app.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
/* eslint-disable no-console */

import express from 'express';
import helmet from 'helmet';
Expand Down Expand Up @@ -29,6 +28,10 @@ import {
integration as integrationRouter,
integrationAuth as integrationAuthRouter
} from './routes';
import { getLogger } from './utils/logger';
import { RouteNotFoundError } from './utils/errors';
import { errorHandler } from '@sentry/node/types/handlers';
Zamion101 marked this conversation as resolved.
Show resolved Hide resolved
import { requestErrorHandler } from './middleware/requestErrorHandler';

export const app = express();

Expand All @@ -50,6 +53,7 @@ if (NODE_ENV === 'production') {
app.use(helmet());
}


// routers
app.use('/api/v1/signup', signupRouter);
app.use('/api/v1/auth', authRouter);
Expand All @@ -69,6 +73,17 @@ app.use('/api/v1/stripe', stripeRouter);
app.use('/api/v1/integration', integrationRouter);
app.use('/api/v1/integration-auth', integrationAuthRouter);


//* Handle unrouted requests and respond with proper error message as well as status code
app.use((req, res, next)=>{
Zamion101 marked this conversation as resolved.
Show resolved Hide resolved
if(res.headersSent) return next();
next(RouteNotFoundError({message: `The requested source '(${req.method})${req.url}' was not found`}))
})

//* Error Handling Middleware (must be after all routing logic)
app.use(requestErrorHandler)


export const server = app.listen(PORT, () => {
console.log(`Listening on PORT ${[PORT]}`);
getLogger("backend-main").info(`Server started listening at port ${PORT}`)
});
4 changes: 4 additions & 0 deletions backend/src/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ const JWT_SIGNUP_LIFETIME = process.env.JWT_SIGNUP_LIFETIME! || '15m';
const JWT_SIGNUP_SECRET = process.env.JWT_SIGNUP_SECRET!;
const MONGO_URL = process.env.MONGO_URL!;
const NODE_ENV = process.env.NODE_ENV! || 'production';
const VERBOSE_ERROR_OUTPUT = process.env.VERBOSE_ERROR_OUTPUT! !== 'true' && true;
const LOKI_HOST = process.env.LOKI_HOST || undefined;
const CLIENT_SECRET_HEROKU = process.env.CLIENT_SECRET_HEROKU!;
const CLIENT_ID_HEROKU = process.env.CLIENT_ID_HEROKU!;
const CLIENT_ID_VERCEL = process.env.CLIENT_ID_VERCEL!;
Expand Down Expand Up @@ -51,6 +53,8 @@ export {
JWT_SIGNUP_SECRET,
MONGO_URL,
NODE_ENV,
VERBOSE_ERROR_OUTPUT,
LOKI_HOST,
CLIENT_ID_HEROKU,
CLIENT_ID_VERCEL,
CLIENT_ID_NETLIFY,
Expand Down
22 changes: 15 additions & 7 deletions backend/src/helpers/integration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,18 @@ import * as Sentry from '@sentry/node';
import {
Bot,
Integration,
IIntegration,
IntegrationAuth,
IIntegrationAuth
} from '../models';
import { exchangeCode, exchangeRefresh, syncSecrets } from '../integrations';
import { BotService, IntegrationService } from '../services';
import { BotService } from '../services';
import {
ENV_DEV,
EVENT_PUSH_SECRETS,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY
} from '../variables';
import { UnauthorizedRequestError } from '../utils/errors';
import RequestError from '../utils/requestError';

interface Update {
workspace: string;
Expand Down Expand Up @@ -176,12 +176,13 @@ const syncIntegrationsHelper = async ({
*/
const getIntegrationAuthRefreshHelper = async ({ integrationAuthId }: { integrationAuthId: string }) => {
let refreshToken;

try {
const integrationAuth = await IntegrationAuth
.findById(integrationAuthId)
.select('+refreshCiphertext +refreshIV +refreshTag');

if (!integrationAuth) throw new Error('Failed to find integration auth');
if (!integrationAuth) throw UnauthorizedRequestError({message: 'Failed to locate Integration Authentication credentials'});

refreshToken = await BotService.decryptSymmetric({
workspaceId: integrationAuth.workspace.toString(),
Expand All @@ -193,7 +194,10 @@ const syncIntegrationsHelper = async ({
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to get integration refresh token');
if(err instanceof RequestError)
throw err
else
throw new Error('Failed to get integration refresh token');
}

return refreshToken;
Expand All @@ -209,12 +213,13 @@ const syncIntegrationsHelper = async ({
*/
const getIntegrationAuthAccessHelper = async ({ integrationAuthId }: { integrationAuthId: string }) => {
let accessToken;

try {
const integrationAuth = await IntegrationAuth
.findById(integrationAuthId)
.select('workspace integration +accessCiphertext +accessIV +accessTag +accessExpiresAt + refreshCiphertext');

if (!integrationAuth) throw new Error('Failed to find integration auth');
if (!integrationAuth) throw UnauthorizedRequestError({message: 'Failed to locate Integration Authentication credentials'});

accessToken = await BotService.decryptSymmetric({
workspaceId: integrationAuth.workspace.toString(),
Expand All @@ -240,7 +245,10 @@ const getIntegrationAuthAccessHelper = async ({ integrationAuthId }: { integrati
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to get integration access token');
if(err instanceof RequestError)
throw err
else
throw new Error('Failed to get integration access token');
}

return accessToken;
Expand Down
1 change: 1 addition & 0 deletions backend/src/helpers/membership.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const validateMembership = async ({
}) => {

let membership;
//TODO: Refactor code to take advantage of using RequestError. It's possible to create new types of errors for more detailed errors
try {
membership = await Membership.findOne({
user: userId,
Expand Down
1 change: 1 addition & 0 deletions backend/src/integrations/refresh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ const exchangeRefreshHeroku = async ({
refreshToken: string;
}) => {
let accessToken;
//TODO: Refactor code to take advantage of using RequestError. It's possible to create new types of errors for more detailed errors
try {
const res = await axios.post(
INTEGRATION_HEROKU_TOKEN_URL,
Expand Down
29 changes: 29 additions & 0 deletions backend/src/middleware/requestErrorHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { ErrorRequestHandler } from "express";

import * as Sentry from '@sentry/node';
import { InternalServerError } from "../utils/errors";
import { getLogger } from "../utils/logger";
import RequestError, { LogLevel } from "../utils/requestError";


export const requestErrorHandler: ErrorRequestHandler = (error: RequestError|Error, req, res, next) => {
if(res.headersSent) return next();
//TODO: Find better way to type check for error. In current setting you need to cast type to get the functions and variables from RequestError
if(!(error instanceof RequestError)){
error = InternalServerError({context: {exception: error.message}, stack: error.stack})
getLogger('backend-main').log((<RequestError>error).levelName.toLowerCase(), (<RequestError>error).message)
}

//* Set Sentry user identification if req.user is populated
if(req.user !== undefined && req.user !== null){
Sentry.setUser({ email: req.user.email })
}
//* Only sent error to Sentry if LogLevel is one of the following level 'ERROR', 'EMERGENCY' or 'CRITICAL'
//* with this we will eliminate false-positive errors like 'BadRequestError', 'UnauthorizedRequestError' and so on
if([LogLevel.ERROR, LogLevel.EMERGENCY, LogLevel.CRITICAL].includes((<RequestError>error).level)){
Sentry.captureException(error)
}

res.status((<RequestError>error).statusCode).json((<RequestError>error).format(req))
next()
}
39 changes: 16 additions & 23 deletions backend/src/middleware/requireAuth.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import jwt from 'jsonwebtoken';
import { Request, Response, NextFunction } from 'express';
import * as Sentry from '@sentry/node';
import { User } from '../models';
import { JWT_AUTH_SECRET } from '../config';
import { AccountNotFoundError, BadRequestError, UnauthorizedRequestError } from '../utils/errors';

declare module 'jsonwebtoken' {
export interface UserIDJwtPayload extends jwt.JwtPayload {
Expand All @@ -20,32 +20,25 @@ declare module 'jsonwebtoken' {
*/
const requireAuth = async (req: Request, res: Response, next: NextFunction) => {
// JWT authentication middleware
try {
if (!req.headers?.authorization)
throw new Error('Failed to locate authorization header');
const [ AUTH_TOKEN_TYPE, AUTH_TOKEN_VALUE ] = <[string, string]>req.headers['authorization']?.split(' ', 2) ?? [null, null]
if(AUTH_TOKEN_TYPE === null) return next(BadRequestError({message: `Missing Authorization Header in the request header.`}))
if(AUTH_TOKEN_TYPE.toLowerCase() !== 'bearer') return next(BadRequestError({message: `The provided authentication type '${AUTH_TOKEN_TYPE}' is not supported.`}))
if(AUTH_TOKEN_VALUE === null) return next(BadRequestError({message: 'Missing Authorization Body in the request header'}))

const token = req.headers.authorization.split(' ')[1];
const decodedToken = <jwt.UserIDJwtPayload>(
jwt.verify(token, JWT_AUTH_SECRET)
);
const decodedToken = <jwt.UserIDJwtPayload>(
jwt.verify(AUTH_TOKEN_VALUE, JWT_AUTH_SECRET)
);

const user = await User.findOne({
_id: decodedToken.userId
}).select('+publicKey');
const user = await User.findOne({
_id: decodedToken.userId
}).select('+publicKey');

if (!user) throw new Error('Failed to authenticate unfound user');
if (!user?.publicKey)
throw new Error('Failed to authenticate not fully set up account');
if (!user) return next(AccountNotFoundError({message: 'Failed to locate User account'}))
if (!user?.publicKey)
return next(UnauthorizedRequestError({message: 'Unable to authenticate due to partially set up account'}))

req.user = user;
return next();
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
return res.status(401).send({
error: 'Failed to authenticate user. Try logging in'
});
}
req.user = user;
return next();
};

export default requireAuth;
40 changes: 16 additions & 24 deletions backend/src/middleware/requireBotAuth.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as Sentry from '@sentry/node';
import { Request, Response, NextFunction } from 'express';
import { Bot } from '../models';
import { validateMembership } from '../helpers/membership';
import { AccountNotFoundError } from '../utils/errors';

type req = 'params' | 'body' | 'query';

Expand All @@ -15,30 +15,22 @@ const requireBotAuth = ({
location?: req;
}) => {
return async (req: Request, res: Response, next: NextFunction) => {
try {
const bot = await Bot.findOne({ _id: req[location].botId });

if (!bot) {
throw new Error('Failed to find bot');
}

await validateMembership({
userId: req.user._id.toString(),
workspaceId: bot.workspace.toString(),
acceptedRoles,
acceptedStatuses
});

req.bot = bot;

next();
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
return res.status(401).send({
error: 'Failed bot authorization'
});
const bot = await Bot.findOne({ _id: req[location].botId });

if (!bot) {
return next(AccountNotFoundError({message: 'Failed to locate Bot account'}))
}

await validateMembership({
userId: req.user._id.toString(),
workspaceId: bot.workspace.toString(),
acceptedRoles,
acceptedStatuses
});

req.bot = bot;

next();
}
}

Expand Down
Loading