Replace old errors with new exceptions

This commit is contained in:
rijkvanzanten
2020-06-29 13:27:42 -04:00
parent a167f6de1a
commit d0aafecfa7
18 changed files with 52 additions and 193 deletions

View File

@@ -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);
}
}

View File

@@ -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;

View File

@@ -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';

View File

@@ -0,0 +1,7 @@
import { BaseException } from './base';
export class InvalidCredentialsException extends BaseException {
constructor(message = 'Invalid user credentials.') {
super(message, 401, 'INVALID_CREDENTIALS');
}
}

View File

@@ -0,0 +1,7 @@
import { BaseException } from './base';
export class InvalidPayloadException extends BaseException {
constructor(message: string) {
super(message, 400, 'INVALID_PAYLOAD');
}
}

View File

@@ -0,0 +1,7 @@
import { BaseException } from './base';
export class ItemLimitException extends BaseException {
constructor(message: string) {
super(message, 400, 'ITEM_LIMIT_REACHED');
}
}

View File

@@ -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');
}
}

View File

@@ -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');
}
}

View File

@@ -1,6 +0,0 @@
export type Code =
| 'COLLECTION_NOT_FOUND'
| 'FIELD_NOT_FOUND'
| 'ROUTE_NOT_FOUND'
| 'NOT_FOUND'
| 'INVALID_QUERY';

View File

@@ -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;

View File

@@ -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}.`);
});
}

View File

@@ -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}"`
);
});

View File

@@ -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);

View File

@@ -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;

View File

@@ -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 = {

View File

@@ -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);
}

View File

@@ -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<string, any>, 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));

View File

@@ -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);