feat: handle http exceptions (#456)

This commit is contained in:
Ahmed Bouhuolia
2024-05-22 19:30:41 +02:00
committed by GitHub
parent 1227111fae
commit 0836fe14e0
14 changed files with 102 additions and 66 deletions

View File

@@ -207,7 +207,6 @@ export default class AccountsController extends BaseController {
tenantId, tenantId,
accountDTO accountDTO
); );
return res.status(200).send({ return res.status(200).send({
id: account.id, id: account.id,
message: 'The account has been created successfully.', message: 'The account has been created successfully.',

View File

@@ -14,7 +14,7 @@ export class ExportController extends BaseController {
/** /**
* Router constructor method. * Router constructor method.
*/ */
router() { public router() {
const router = Router(); const router = Router();
router.get( router.get(
@@ -53,6 +53,7 @@ export class ExportController extends BaseController {
query.resource, query.resource,
acceptType === ACCEPT_TYPE.APPLICATION_XLSX ? 'xlsx' : 'csv' acceptType === ACCEPT_TYPE.APPLICATION_XLSX ? 'xlsx' : 'csv'
); );
// Retrieves the csv format.
if (ACCEPT_TYPE.APPLICATION_CSV === acceptType) { if (ACCEPT_TYPE.APPLICATION_CSV === acceptType) {
res.setHeader('Content-Disposition', 'attachment; filename=output.csv'); res.setHeader('Content-Disposition', 'attachment; filename=output.csv');
res.setHeader('Content-Type', 'text/csv'); res.setHeader('Content-Type', 'text/csv');

View File

@@ -16,7 +16,7 @@ export class ImportController extends BaseController {
/** /**
* Router constructor method. * Router constructor method.
*/ */
router() { public router() {
const router = Router(); const router = Router();
router.post( router.post(
@@ -240,11 +240,7 @@ export class ImportController extends BaseController {
errors: [{ type: 'IMPORTED_FILE_EXTENSION_INVALID' }], errors: [{ type: 'IMPORTED_FILE_EXTENSION_INVALID' }],
}); });
} }
return res.status(400).send({
errors: [{ type: error.errorType }],
});
} }
next(error); next(error);
} }
} }

View File

@@ -77,14 +77,14 @@ export default class ManualJournalsController extends BaseController {
/** /**
* Specific manual journal id param validation schema. * Specific manual journal id param validation schema.
*/ */
get manualJournalParamSchema() { private get manualJournalParamSchema() {
return [param('id').exists().isNumeric().toInt()]; return [param('id').exists().isNumeric().toInt()];
} }
/** /**
* Manual journal DTO schema. * Manual journal DTO schema.
*/ */
get manualJournalValidationSchema() { private get manualJournalValidationSchema() {
return [ return [
check('date').exists().isISO8601(), check('date').exists().isISO8601(),
check('currency_code').optional(), check('currency_code').optional(),
@@ -154,7 +154,7 @@ export default class ManualJournalsController extends BaseController {
/** /**
* Manual journals list validation schema. * Manual journals list validation schema.
*/ */
get manualJournalsListSchema() { private get manualJournalsListSchema() {
return [ return [
query('page').optional().isNumeric().toInt(), query('page').optional().isNumeric().toInt(),
query('page_size').optional().isNumeric().toInt(), query('page_size').optional().isNumeric().toInt(),
@@ -320,7 +320,7 @@ export default class ManualJournalsController extends BaseController {
* @param {Response} res * @param {Response} res
* @param {NextFunction} next * @param {NextFunction} next
*/ */
getManualJournalsList = async ( private getManualJournalsList = async (
req: Request, req: Request,
res: Response, res: Response,
next: NextFunction next: NextFunction

View File

@@ -33,17 +33,17 @@ export default class OrganizationDashboardController extends BaseController {
} }
/** /**
* * Detarmines whether the current authed organization to able to change its currency/.
* @param req * @param {Request} req
* @param res * @param {Response} res
* @param next * @param {NextFunction} next
* @returns * @returns {Response|void}
*/ */
private async baseCurrencyMutateAbility( private async baseCurrencyMutateAbility(
req: Request, req: Request,
res: Response, res: Response,
next: Function next: Function
) { ): Promise<Response|void> {
const { tenantId } = req; const { tenantId } = req;
try { try {

View File

@@ -29,8 +29,7 @@ export class ProjectsController extends BaseController {
check('cost_estimate').exists().isDecimal(), check('cost_estimate').exists().isDecimal(),
], ],
this.validationResult, this.validationResult,
asyncMiddleware(this.createProject.bind(this)), asyncMiddleware(this.createProject.bind(this))
this.catchServiceErrors
); );
router.post( router.post(
'/:id', '/:id',
@@ -43,8 +42,7 @@ export class ProjectsController extends BaseController {
check('cost_estimate').exists().isDecimal(), check('cost_estimate').exists().isDecimal(),
], ],
this.validationResult, this.validationResult,
asyncMiddleware(this.editProject.bind(this)), asyncMiddleware(this.editProject.bind(this))
this.catchServiceErrors
); );
router.patch( router.patch(
'/:projectId/status', '/:projectId/status',
@@ -56,16 +54,14 @@ export class ProjectsController extends BaseController {
.isIn([IProjectStatus.InProgress, IProjectStatus.Closed]), .isIn([IProjectStatus.InProgress, IProjectStatus.Closed]),
], ],
this.validationResult, this.validationResult,
asyncMiddleware(this.editProject.bind(this)), asyncMiddleware(this.editProject.bind(this))
this.catchServiceErrors
); );
router.get( router.get(
'/:id', '/:id',
CheckPolicies(ProjectAction.VIEW, AbilitySubject.Project), CheckPolicies(ProjectAction.VIEW, AbilitySubject.Project),
[param('id').exists().isInt().toInt()], [param('id').exists().isInt().toInt()],
this.validationResult, this.validationResult,
asyncMiddleware(this.getProject.bind(this)), asyncMiddleware(this.getProject.bind(this))
this.catchServiceErrors
); );
router.get( router.get(
'/:projectId/billable/entries', '/:projectId/billable/entries',
@@ -76,24 +72,21 @@ export class ProjectsController extends BaseController {
query('to_date').optional().isISO8601(), query('to_date').optional().isISO8601(),
], ],
this.validationResult, this.validationResult,
asyncMiddleware(this.projectBillableEntries.bind(this)), asyncMiddleware(this.projectBillableEntries.bind(this))
this.catchServiceErrors
); );
router.get( router.get(
'/', '/',
CheckPolicies(ProjectAction.VIEW, AbilitySubject.Project), CheckPolicies(ProjectAction.VIEW, AbilitySubject.Project),
[], [],
this.validationResult, this.validationResult,
asyncMiddleware(this.getProjects.bind(this)), asyncMiddleware(this.getProjects.bind(this))
this.catchServiceErrors
); );
router.delete( router.delete(
'/:id', '/:id',
CheckPolicies(ProjectAction.DELETE, AbilitySubject.Project), CheckPolicies(ProjectAction.DELETE, AbilitySubject.Project),
[param('id').exists().isInt().toInt()], [param('id').exists().isInt().toInt()],
this.validationResult, this.validationResult,
asyncMiddleware(this.deleteProject.bind(this)), asyncMiddleware(this.deleteProject.bind(this))
this.catchServiceErrors
); );
return router; return router;
} }
@@ -252,22 +245,4 @@ export class ProjectsController extends BaseController {
next(error); next(error);
} }
}; };
/**
* Transforms service errors to response.
* @param {Error}
* @param {Request} req
* @param {Response} res
* @param {ServiceError} error
*/
private catchServiceErrors(
error,
req: Request,
res: Response,
next: NextFunction
) {
if (error instanceof ServiceError) {
}
next(error);
}
} }

View File

@@ -155,6 +155,7 @@ export default class UsersController extends BaseController {
try { try {
const user = await this.usersService.getUser(tenantId, userId); const user = await this.usersService.getUser(tenantId, userId);
return res.status(200).send({ user }); return res.status(200).send({ user });
} catch (error) { } catch (error) {
next(error); next(error);
@@ -229,7 +230,7 @@ export default class UsersController extends BaseController {
* @param {Response} res * @param {Response} res
* @param {NextFunction} next * @param {NextFunction} next
*/ */
catchServiceErrors( private catchServiceErrors(
error: Error, error: Error,
req: Request, req: Request,
res: Response, res: Response,

View File

@@ -17,7 +17,7 @@ export class WarehousesController extends BaseController {
* *
* @returns * @returns
*/ */
router() { public router() {
const router = Router(); const router = Router();
router.post( router.post(

View File

@@ -34,14 +34,21 @@ export class Webhooks extends BaseController {
* @param {Response} res * @param {Response} res
* @returns {Response} * @returns {Response}
*/ */
public async lemonWebhooks(req: Request, res: Response) { public async lemonWebhooks(req: Request, res: Response, next: any) {
const data = req.body; const data = req.body;
const signature = req.headers['x-signature'] ?? ''; const signature = req.headers['x-signature'] ?? '';
const rawBody = req.rawBody; const rawBody = req.rawBody;
await this.lemonWebhooksService.handlePostWebhook(rawBody, data, signature); try {
await this.lemonWebhooksService.handlePostWebhook(
rawBody,
data,
signature
);
return res.status(200).send(); return res.status(200).send();
} catch (error) {
next(error);
}
} }
/** /**

View File

@@ -0,0 +1,20 @@
import { Request, Response, NextFunction } from 'express';
/**
* Global error handler.
* @param {Error} err
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
export function GlobalErrorException(
err: Error,
req: Request,
res: Response,
next: NextFunction
) {
console.error(err.stack);
res.status(500);
res.boom.badImplementation('', { stack: err.stack });
}

View File

@@ -10,8 +10,14 @@ import {
DataError, DataError,
} from 'objection'; } from 'objection';
// In this example `res` is an express response object. /**
export default function ObjectionErrorHandlerMiddleware( * Handles the Objection error exception.
* @param {Error} err
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
export function ObjectionErrorException(
err: Error, err: Error,
req: Request, req: Request,
res: Response, res: Response,
@@ -108,6 +114,7 @@ export default function ObjectionErrorHandlerMiddleware(
type: 'UnknownDatabaseError', type: 'UnknownDatabaseError',
data: {}, data: {},
}); });
} } else {
next(err); next(err);
}
} }

View File

@@ -0,0 +1,25 @@
import { NextFunction, Request, Response } from 'express';
import { ServiceError } from '@/exceptions';
/**
* Handles service error exception.
* @param {Error | ServiceError} err
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
export function ServiceErrorException(
err: Error | ServiceError,
req: Request,
res: Response,
next: NextFunction
) {
if (err instanceof ServiceError) {
res.boom.badRequest('', {
errors: [{ type: err.errorType, message: err.message }],
type: 'ServiceError',
});
} else {
next(err);
}
}

View File

@@ -17,7 +17,9 @@ import {
} from '@/api/middleware/JSONResponseTransformer'; } from '@/api/middleware/JSONResponseTransformer';
import config from '@/config'; import config from '@/config';
import path from 'path'; import path from 'path';
import ObjectionErrorHandlerMiddleware from '@/api/middleware/ObjectionErrorHandlerMiddleware'; import { ObjectionErrorException } from '@/api/exceptions/ObjectionErrorException';
import { ServiceErrorException } from '@/api/exceptions/ServiceErrorException';
import { GlobalErrorException } from '@/api/exceptions/GlobalErrorException';
export default ({ app }) => { export default ({ app }) => {
// Express configuration. // Express configuration.
@@ -30,9 +32,6 @@ export default ({ app }) => {
// Helmet helps you secure your Express apps by setting various HTTP headers. // Helmet helps you secure your Express apps by setting various HTTP headers.
app.use(helmet()); app.use(helmet());
// Allow to full error stack traces and internal details
app.use(errorHandler());
// Boom response objects. // Boom response objects.
app.use(boom()); app.use(boom());
@@ -65,8 +64,10 @@ export default ({ app }) => {
// Agendash application load. // Agendash application load.
app.use('/agendash', AgendashController.router()); app.use('/agendash', AgendashController.router());
// Handles objectionjs errors. // Handles errors.
app.use(ObjectionErrorHandlerMiddleware); app.use(ObjectionErrorException);
app.use(ServiceErrorException);
app.use(GlobalErrorException);
// catch 404 and forward to error handler // catch 404 and forward to error handler
app.use((req: Request, res: Response, next: NextFunction) => { app.use((req: Request, res: Response, next: NextFunction) => {

View File

@@ -10,6 +10,7 @@ import {
} from './utils'; } from './utils';
import { Plan } from '@/system/models'; import { Plan } from '@/system/models';
import { Subscription } from './Subscription'; import { Subscription } from './Subscription';
import { isEmpty } from 'lodash';
@Service() @Service()
export class LemonSqueezyWebhooks { export class LemonSqueezyWebhooks {
@@ -32,6 +33,9 @@ export class LemonSqueezyWebhooks {
if (!config.lemonSqueezy.webhookSecret) { if (!config.lemonSqueezy.webhookSecret) {
throw new Error('Lemon Squeezy Webhook Secret not set in .env'); throw new Error('Lemon Squeezy Webhook Secret not set in .env');
} }
if (!signature) {
throw new Error('Request signature is required.');
}
const secret = config.lemonSqueezy.webhookSecret; const secret = config.lemonSqueezy.webhookSecret;
const hmacSignature = createHmacSignature(secret, rawData); const hmacSignature = createHmacSignature(secret, rawData);
@@ -96,7 +100,7 @@ export class LemonSqueezyWebhooks {
if (webhookEvent === 'subscription_created') { if (webhookEvent === 'subscription_created') {
await this.subscriptionService.newSubscribtion( await this.subscriptionService.newSubscribtion(
tenantId, tenantId,
'early-adaptor', 'early-adaptor'
); );
} }
} }