From d0aafecfa7d9dcb960ca9bd3709740db8b82988e Mon Sep 17 00:00:00 2001 From: rijkvanzanten Date: Mon, 29 Jun 2020 13:27:42 -0400 Subject: [PATCH] Replace old errors with new exceptions --- src/error.ts | 123 -------------------------- src/exceptions/base.ts | 6 +- src/exceptions/index.ts | 5 +- src/exceptions/invalid-credentials.ts | 7 ++ src/exceptions/invalid-payload.ts | 7 ++ src/exceptions/item-limit.ts | 7 ++ src/exceptions/not-found.ts | 19 ---- src/exceptions/route-not-found.ts | 7 ++ src/exceptions/types.ts | 6 -- src/middleware/validate-collection.ts | 4 +- src/middleware/validate-query.ts | 10 +-- src/middleware/validate-singleton.ts | 5 +- src/routes/extensions.ts | 7 +- src/routes/not-found.ts | 4 +- src/services/auth.ts | 6 +- src/services/extensions.ts | 11 +-- src/services/users.ts | 10 +-- src/utils/list-folders.ts | 1 - 18 files changed, 52 insertions(+), 193 deletions(-) delete mode 100644 src/error.ts create mode 100644 src/exceptions/invalid-credentials.ts create mode 100644 src/exceptions/invalid-payload.ts create mode 100644 src/exceptions/item-limit.ts delete mode 100644 src/exceptions/not-found.ts create mode 100644 src/exceptions/route-not-found.ts delete mode 100644 src/exceptions/types.ts diff --git a/src/error.ts b/src/error.ts deleted file mode 100644 index 64521515cf..0000000000 --- a/src/error.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { ErrorRequestHandler } from 'express'; -import { ValidationError } from '@hapi/joi'; -import { TokenExpiredError } from 'jsonwebtoken'; -import logger from './logger'; - -export enum ErrorCode { - NOT_FOUND = 'NOT_FOUND', - FIELD_NOT_FOUND = 'FIELD_NOT_FOUND', - INTERNAL_SERVER_ERROR = 'INTERNAL_SERVER_ERROR', - ENOENT = 'ENOENT', - EXTENSION_ILLEGAL_TYPE = 'EXTENSION_ILLEGAL_TYPE', - INVALID_QUERY = 'INVALID_QUERY', - INVALID_USER_CREDENTIALS = 'INVALID_USER_CREDENTIALS', - USER_NOT_FOUND = 'USER_NOT_FOUND', - FAILED_VALIDATION = 'FAILED_VALIDATION', - MALFORMED_JSON = 'MALFORMED_JSON', - TOKEN_EXPIRED = 'TOKEN_EXPIRED', - TOO_MANY_ITEMS = 'TOO_MANY_ITEMS', -} - -enum HTTPStatus { - NOT_FOUND = 404, - FIELD_NOT_FOUND = 400, - INTERNAL_SERVER_ERROR = 500, - ENOENT = 501, - EXTENSION_ILLEGAL_TYPE = 400, - INVALID_QUERY = 400, - INVALID_USER_CREDENTIALS = 401, - USER_NOT_FOUND = 401, - FAILED_VALIDATION = 422, - MALFORMED_JSON = 400, - TOKEN_EXPIRED = 401, - TOO_MANY_ITEMS = 400, -} - -export const errorHandler: ErrorRequestHandler = ( - error: APIError | ValidationError | Error, - req, - res, - next -) => { - let response: any = {}; - - if (error instanceof APIError) { - logger.debug(error); - - res.status(error.status); - - response = { - error: { - code: error.code, - message: error.message, - }, - }; - } else if (error instanceof ValidationError) { - logger.debug(error); - - res.status(HTTPStatus.FAILED_VALIDATION); - - response = { - error: { - code: ErrorCode.FAILED_VALIDATION, - message: error.message, - }, - }; - } else if (error instanceof TokenExpiredError) { - logger.debug(error); - res.status(HTTPStatus.TOKEN_EXPIRED); - response = { - error: { - code: ErrorCode.TOKEN_EXPIRED, - message: 'The provided token is expired.', - }, - }; - } - - // Syntax errors are most likely thrown by Body Parser when misaligned JSON is sent to the API - else if (error instanceof SyntaxError && 'entity.parse.failed') { - logger.debug(error); - - res.status(HTTPStatus.MALFORMED_JSON); - - response = { - error: { - code: ErrorCode.MALFORMED_JSON, - message: error.message, - }, - }; - } else { - logger.error(error); - - res.status(500); - - response = { - error: { - code: ErrorCode.INTERNAL_SERVER_ERROR, - }, - }; - - if (process.env.NODE_ENV === 'development') { - response.error.message = error.message; - } - } - - if (process.env.NODE_ENV === 'development') { - response.error.stack = error.stack; - } - - return res.json(response); -}; - -export default class APIError extends Error { - status: HTTPStatus; - code: ErrorCode; - - constructor(code: ErrorCode, message: string) { - super(message); - this.status = HTTPStatus[code] || 500; - this.code = code; - - Error.captureStackTrace(this, this.constructor); - } -} diff --git a/src/exceptions/base.ts b/src/exceptions/base.ts index 41d738805d..27fcdb62d2 100644 --- a/src/exceptions/base.ts +++ b/src/exceptions/base.ts @@ -1,10 +1,8 @@ -import { Code } from './types'; - export class BaseException extends Error { status: number; - code: Code; + code: string; - constructor(message: string, status: number, code: Code) { + constructor(message: string, status: number, code: string) { super(message); this.status = status; this.code = code; diff --git a/src/exceptions/index.ts b/src/exceptions/index.ts index de52e37e1c..ee859330e3 100644 --- a/src/exceptions/index.ts +++ b/src/exceptions/index.ts @@ -1,3 +1,6 @@ -export * from './not-found'; export * from './base'; +export * from './invalid-credentials'; +export * from './invalid-payload'; export * from './invalid-query'; +export * from './item-limit'; +export * from './route-not-found'; diff --git a/src/exceptions/invalid-credentials.ts b/src/exceptions/invalid-credentials.ts new file mode 100644 index 0000000000..015cd3b012 --- /dev/null +++ b/src/exceptions/invalid-credentials.ts @@ -0,0 +1,7 @@ +import { BaseException } from './base'; + +export class InvalidCredentialsException extends BaseException { + constructor(message = 'Invalid user credentials.') { + super(message, 401, 'INVALID_CREDENTIALS'); + } +} diff --git a/src/exceptions/invalid-payload.ts b/src/exceptions/invalid-payload.ts new file mode 100644 index 0000000000..f831ed9e4a --- /dev/null +++ b/src/exceptions/invalid-payload.ts @@ -0,0 +1,7 @@ +import { BaseException } from './base'; + +export class InvalidPayloadException extends BaseException { + constructor(message: string) { + super(message, 400, 'INVALID_PAYLOAD'); + } +} diff --git a/src/exceptions/item-limit.ts b/src/exceptions/item-limit.ts new file mode 100644 index 0000000000..bba74c942d --- /dev/null +++ b/src/exceptions/item-limit.ts @@ -0,0 +1,7 @@ +import { BaseException } from './base'; + +export class ItemLimitException extends BaseException { + constructor(message: string) { + super(message, 400, 'ITEM_LIMIT_REACHED'); + } +} diff --git a/src/exceptions/not-found.ts b/src/exceptions/not-found.ts deleted file mode 100644 index 08c1180d2c..0000000000 --- a/src/exceptions/not-found.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { BaseException } from './base'; - -export class RouteNotFound extends BaseException { - constructor(path: string) { - super(`Route ${path} doesn't exist.`, 404, 'ROUTE_NOT_FOUND'); - } -} - -export class CollectionNotFoundException extends BaseException { - constructor(collection: string) { - super(`Collection ${collection} can't be found.`, 404, 'COLLECTION_NOT_FOUND'); - } -} - -export class FieldNotFoundException extends BaseException { - constructor(field: string) { - super(`Field ${field} can't be found.`, 404, 'FIELD_NOT_FOUND'); - } -} diff --git a/src/exceptions/route-not-found.ts b/src/exceptions/route-not-found.ts new file mode 100644 index 0000000000..6f054af4b1 --- /dev/null +++ b/src/exceptions/route-not-found.ts @@ -0,0 +1,7 @@ +import { BaseException } from './base'; + +export class RouteNotFoundException extends BaseException { + constructor(path: string) { + super(`Route ${path} doesn't exist.`, 404, 'ROUTE_NOT_FOUND'); + } +} diff --git a/src/exceptions/types.ts b/src/exceptions/types.ts deleted file mode 100644 index b879dbe61f..0000000000 --- a/src/exceptions/types.ts +++ /dev/null @@ -1,6 +0,0 @@ -export type Code = - | 'COLLECTION_NOT_FOUND' - | 'FIELD_NOT_FOUND' - | 'ROUTE_NOT_FOUND' - | 'NOT_FOUND' - | 'INVALID_QUERY'; diff --git a/src/middleware/validate-collection.ts b/src/middleware/validate-collection.ts index a1f451bf11..5bd96bc445 100644 --- a/src/middleware/validate-collection.ts +++ b/src/middleware/validate-collection.ts @@ -5,7 +5,7 @@ import { RequestHandler } from 'express'; import asyncHandler from 'express-async-handler'; import database from '../database'; -import APIError, { ErrorCode } from '../error'; +import { RouteNotFoundException } from '../exceptions'; const validateCollection: RequestHandler = asyncHandler(async (req, res, next) => { if (!req.params.collection) return next(); @@ -17,7 +17,7 @@ const validateCollection: RequestHandler = asyncHandler(async (req, res, next) = return next(); } - throw new APIError(ErrorCode.NOT_FOUND, `Collection "${req.params.collection}" doesn't exist.`); + throw new RouteNotFoundException(req.path); }); export default validateCollection; diff --git a/src/middleware/validate-query.ts b/src/middleware/validate-query.ts index 92b06c1c32..2d95da3687 100644 --- a/src/middleware/validate-query.ts +++ b/src/middleware/validate-query.ts @@ -9,7 +9,7 @@ import { RequestHandler } from 'express'; import { Query } from '../types/query'; import { hasFields } from '../services/schema'; import asyncHandler from 'express-async-handler'; -import APIError, { ErrorCode } from '../error'; +import { InvalidQueryException } from '../exceptions'; const validateQuery: RequestHandler = asyncHandler(async (req, res, next) => { if (!req.params.collection) return next(); @@ -28,8 +28,7 @@ const validateQuery: RequestHandler = asyncHandler(async (req, res, next) => { async function validateParams(collection: string, query: Query) { if (query.offset && query.page) { - throw new APIError( - ErrorCode.INVALID_QUERY, + throw new InvalidQueryException( `You can't have both the offset and page query parameters enabled.` ); } @@ -51,10 +50,7 @@ async function validateFields(collection: string, query: Query) { Array.from(fieldsToCheck).forEach((field, index) => { const exists = fieldsExist[index]; if (exists === false) - throw new APIError( - ErrorCode.FIELD_NOT_FOUND, - `Field ${field} doesn't exist in ${collection}.` - ); + throw new InvalidQueryException(`Field ${field} doesn't exist in ${collection}.`); }); } diff --git a/src/middleware/validate-singleton.ts b/src/middleware/validate-singleton.ts index 480f59ba62..25d8b91bd5 100644 --- a/src/middleware/validate-singleton.ts +++ b/src/middleware/validate-singleton.ts @@ -5,7 +5,7 @@ import { RequestHandler } from 'express'; import asyncHandler from 'express-async-handler'; import database from '../database'; -import APIError, { ErrorCode } from '../error'; +import { ItemLimitException } from '../exceptions'; const validateCollection: RequestHandler = asyncHandler(async (req, res, next) => { if (!req.collection) return next(); @@ -24,8 +24,7 @@ const validateCollection: RequestHandler = asyncHandler(async (req, res, next) = if (Number(count) === 0) return next(); - throw new APIError( - ErrorCode.TOO_MANY_ITEMS, + throw new ItemLimitException( `You can only create a single item in singleton "${req.collection}"` ); }); diff --git a/src/routes/extensions.ts b/src/routes/extensions.ts index 1e973bfcaf..7a1946aace 100644 --- a/src/routes/extensions.ts +++ b/src/routes/extensions.ts @@ -1,7 +1,7 @@ import { Router } from 'express'; import asyncHandler from 'express-async-handler'; import * as ExtensionsService from '../services/extensions'; -import APIError, { ErrorCode } from '../error'; +import { RouteNotFoundException } from '../exceptions'; const router = Router(); @@ -11,10 +11,7 @@ router.get( const typeAllowList = ['interfaces', 'layouts', 'displays', 'modules']; if (typeAllowList.includes(req.params.type) === false) { - throw new APIError( - ErrorCode.EXTENSION_ILLEGAL_TYPE, - `${req.params.type} is not an extension type.` - ); + throw new RouteNotFoundException(req.path); } const interfaces = await ExtensionsService.listExtensions(req.params.type); diff --git a/src/routes/not-found.ts b/src/routes/not-found.ts index 3df2ef86dc..b427eafa82 100644 --- a/src/routes/not-found.ts +++ b/src/routes/not-found.ts @@ -1,8 +1,8 @@ import { RequestHandler } from 'express'; -import APIError, { ErrorCode } from '../error'; +import { RouteNotFoundException } from '../exceptions'; const notFound: RequestHandler = (req, res, next) => { - throw new APIError(ErrorCode.NOT_FOUND, `Route ${req.path} not found.`); + throw new RouteNotFoundException(req.path); }; export default notFound; diff --git a/src/services/auth.ts b/src/services/auth.ts index 9794e5470d..36ded9e920 100644 --- a/src/services/auth.ts +++ b/src/services/auth.ts @@ -1,7 +1,7 @@ import database from '../database'; -import APIError, { ErrorCode } from '../error'; import jwt from 'jsonwebtoken'; import bcrypt from 'bcrypt'; +import { InvalidCredentialsException } from '../exceptions'; export const authenticate = async (email: string, password?: string) => { const user = await database @@ -11,7 +11,7 @@ export const authenticate = async (email: string, password?: string) => { .first(); if (!user) { - throw new APIError(ErrorCode.INVALID_USER_CREDENTIALS, 'Invalid user credentials'); + throw new InvalidCredentialsException(); } /** @@ -22,7 +22,7 @@ export const authenticate = async (email: string, password?: string) => { * signal the difference */ if (password !== undefined && (await bcrypt.compare(password, user.password)) === false) { - throw new APIError(ErrorCode.INVALID_USER_CREDENTIALS, 'Invalid user credentials'); + throw new InvalidCredentialsException(); } const payload = { diff --git a/src/services/extensions.ts b/src/services/extensions.ts index edde0634b4..2c76ce0413 100644 --- a/src/services/extensions.ts +++ b/src/services/extensions.ts @@ -1,18 +1,9 @@ import listFolders from '../utils/list-folders'; import path from 'path'; -import APIError, { ErrorCode } from '../error'; export async function listExtensions(type: string) { const extensionsPath = process.env.EXTENSIONS_PATH; const location = path.join(extensionsPath, type); - try { - return await listFolders(location); - } catch (err) { - if (err.code === 'ENOENT') { - throw new APIError(ErrorCode.ENOENT, `The ${type} folder doesn't exist.`); - } - - throw err; - } + return await listFolders(location); } diff --git a/src/services/users.ts b/src/services/users.ts index a2669356a9..401eba29b4 100644 --- a/src/services/users.ts +++ b/src/services/users.ts @@ -3,9 +3,9 @@ import * as ItemsService from './items'; import jwt from 'jsonwebtoken'; import { sendInviteMail } from '../mail'; import database from '../database'; -import APIError, { ErrorCode } from '../error'; import bcrypt from 'bcrypt'; import * as PayloadService from '../services/payload'; +import { InvalidPayloadException } from '../exceptions'; export const createUser = async (data: Record, query?: Query) => { return await ItemsService.createItem('directus_users', data, query); @@ -50,12 +50,8 @@ export const acceptInvite = async (token: string, password: string) => { .where({ email }) .first(); - if (!user) { - throw new APIError(ErrorCode.USER_NOT_FOUND, `Email address ${email} hasn't been invited.`); - } - - if (user.status !== 'invited') { - throw new APIError(ErrorCode.USER_NOT_FOUND, `Email address ${email} hasn't been invited.`); + if (!user || user.status !== 'invited') { + throw new InvalidPayloadException(`Email address ${email} hasn't been invited.`); } const passwordHashed = await bcrypt.hash(password, Number(process.env.SALT_ROUNDS)); diff --git a/src/utils/list-folders.ts b/src/utils/list-folders.ts index 705d9b181a..89542ac4ef 100644 --- a/src/utils/list-folders.ts +++ b/src/utils/list-folders.ts @@ -1,7 +1,6 @@ import fs from 'fs'; import path from 'path'; import { promisify } from 'util'; -import APIError, { ErrorCode } from '../error'; const readdir = promisify(fs.readdir); const stat = promisify(fs.stat);