diff --git a/frontend/components/MarkdownEditor/MarkdownEditor.js b/frontend/components/MarkdownEditor/MarkdownEditor.js index f5f4619a..c527cf2c 100644 --- a/frontend/components/MarkdownEditor/MarkdownEditor.js +++ b/frontend/components/MarkdownEditor/MarkdownEditor.js @@ -73,7 +73,7 @@ class MarkdownEditor extends React.Component { formData.append('file', file); } - fetch(data.upload_url, { + fetch(data.uploadUrl, { method: 'post', body: formData, }) diff --git a/frontend/utils/ApiClient.js b/frontend/utils/ApiClient.js index ec2ddee9..c4d4c35e 100644 --- a/frontend/utils/ApiClient.js +++ b/frontend/utils/ApiClient.js @@ -64,22 +64,14 @@ class ApiClient { // Handle 401, log out user if (response.status === 401) { - stores.user.logout(); + return stores.user.logout(); } // Handle failed responses - let error; - try { - // Expect API to return JSON - error = JSON.parse(response); - } catch (e) { - // Expect call to fail without JSON response - error = { error: response.statusText }; - } - + const error = {}; error.statusCode = response.status; error.response = response; - return reject(error); + throw error; }) .then((response) => { return response.json(); @@ -91,8 +83,12 @@ class ApiClient { } resolve(json); }) - .catch(() => { - reject({ error: 'Unknown error' }); + .catch(error => { + error.response.json() + .then(json => { + error.data = json; + reject(error); + }); }); }); } diff --git a/server/api/__snapshots__/auth.test.js.snap b/server/api/__snapshots__/auth.test.js.snap index 716f58eb..8114c061 100644 --- a/server/api/__snapshots__/auth.test.js.snap +++ b/server/api/__snapshots__/auth.test.js.snap @@ -18,49 +18,63 @@ Object { exports[`#auth.login should require either username or email 1`] = ` Object { - "error": "username or email is required", - "ok": false + "error": "validation_error", + "message": "username/email is required", + "ok": false, + "status": 400 } `; exports[`#auth.login should require password 1`] = ` Object { - "error": "password is required", - "ok": false + "error": "validation_error", + "message": "username/email is required", + "ok": false, + "status": 400 } `; exports[`#auth.login should validate password 1`] = ` Object { - "error": "Invalid password", - "ok": false + "error": "validation_error", + "message": "username/email is required", + "ok": false, + "status": 400 } `; exports[`#auth.signup should require params 1`] = ` Object { - "error": "name is required", - "ok": false + "error": "validation_error", + "message": "name is required", + "ok": false, + "status": 400 } `; exports[`#auth.signup should require unique email 1`] = ` Object { - "error": "User already exists with this email", - "ok": false + "error": "user_exists_with_email", + "message": "User already exists with this email", + "ok": false, + "status": 400 } `; exports[`#auth.signup should require unique username 1`] = ` Object { - "error": "User already exists with this username", - "ok": false + "error": "user_exists_with_username", + "message": "User already exists with this username", + "ok": false, + "status": 400 } `; exports[`#auth.signup should require valid email 1`] = ` Object { - "error": "email is invalid", - "ok": false + "error": "validation_error", + "message": "email is invalid", + "ok": false, + "status": 400 } `; diff --git a/server/api/__snapshots__/user.test.js.snap b/server/api/__snapshots__/user.test.js.snap index 1fd03052..9f908c99 100644 --- a/server/api/__snapshots__/user.test.js.snap +++ b/server/api/__snapshots__/user.test.js.snap @@ -1,7 +1,9 @@ exports[`#user.info should require authentication 1`] = ` Object { - "error": "Authentication required", - "ok": false + "error": "authentication_required", + "message": "Authentication required", + "ok": false, + "status": 401 } `; @@ -13,6 +15,7 @@ Object { "name": "User 1", "username": "user1" }, - "ok": true + "ok": true, + "status": 200 } `; diff --git a/server/api/auth.js b/server/api/auth.js index 5a7545b0..34506747 100644 --- a/server/api/auth.js +++ b/server/api/auth.js @@ -1,5 +1,6 @@ import Router from 'koa-router'; -import httpErrors from 'http-errors'; +import Sequelize from 'sequelize'; +import apiError, { httpErrors } from '../errors'; import fetch from 'isomorphic-fetch'; import querystring from 'querystring'; @@ -18,11 +19,11 @@ router.post('auth.signup', async (ctx) => { ctx.assertPresent(password, 'password is required'); if (await User.findOne({ where: { email } })) { - throw httpErrors.BadRequest('User already exists with this email'); + throw apiError(400, 'user_exists_with_email', 'User already exists with this email'); } if (await User.findOne({ where: { username } })) { - throw httpErrors.BadRequest('User already exists with this username'); + throw apiError(400, 'user_exists_with_username', 'User already exists with this username'); } const user = await User.create({ @@ -39,21 +40,31 @@ router.post('auth.signup', async (ctx) => { }); router.post('auth.login', async (ctx) => { - const { username, email, password } = ctx.request.body; + const { username, password } = ctx.request.body; + ctx.assertPresent(username, 'username/email is required'); ctx.assertPresent(password, 'password is required'); let user; if (username) { - user = await User.findOne({ where: { username } }); - } else if (email) { - user = await User.findOne({ where: { email } }); + user = await User.findOne({ where: Sequelize.or( + { email: username }, + { username }, + ) }); } else { - throw httpErrors.BadRequest('username or email is required'); + throw apiError(400, 'invalid_credentials', 'username or email is invalid'); + } + + if (!user) { + throw apiError(400, 'username or email is invalid'); + } + + if (!user.passwordDigest) { + throw apiError(400, 'no_password', 'No password set'); } if (!await user.verifyPassword(password)) { - throw httpErrors.BadRequest('Invalid password'); + throw apiError(400, 'invalid_password', 'Invalid password'); } ctx.body = { data: { @@ -152,8 +163,6 @@ router.post('auth.slackCommands', async (ctx) => { } if (!data.ok) throw httpErrors.BadRequest(data.error); - - ctx.body = { success: true }; }); diff --git a/server/api/auth.test.js b/server/api/auth.test.js index fec61a64..68c43f00 100644 --- a/server/api/auth.test.js +++ b/server/api/auth.test.js @@ -91,7 +91,7 @@ describe('#auth.login', () => { await seed(); const res = await server.post('/api/auth.login', { body: { - email: 'user1@example.com', + username: 'user1@example.com', password: 'test123!', }, }); diff --git a/server/api/index.js b/server/api/index.js index d56501bc..d37b9c13 100644 --- a/server/api/index.js +++ b/server/api/index.js @@ -2,6 +2,7 @@ import bodyParser from 'koa-bodyparser'; import Koa from 'koa'; import Router from 'koa-router'; import Sequelize from 'sequelize'; +import _ from 'lodash'; import auth from './auth'; import user from './user'; @@ -41,7 +42,9 @@ api.use(async (ctx, next) => { ctx.body = { ok: false, - error: message, + error: _.snakeCase(err.id || err.message), + status: err.status, + message, }; } }); diff --git a/server/api/middlewares/apiWrapper.js b/server/api/middlewares/apiWrapper.js index 70674cdf..bf49bc7a 100644 --- a/server/api/middlewares/apiWrapper.js +++ b/server/api/middlewares/apiWrapper.js @@ -6,6 +6,7 @@ export default function apiWrapper(_options) { ctx.body = { ...ctx.body, + status: ctx.status, ok, }; }; diff --git a/server/api/middlewares/validation.js b/server/api/middlewares/validation.js index 019a8b01..a99d6a59 100644 --- a/server/api/middlewares/validation.js +++ b/server/api/middlewares/validation.js @@ -1,23 +1,23 @@ -import httpErrors from 'http-errors'; +import apiError from '../../errors'; import validator from 'validator'; export default function validation() { return function validationMiddleware(ctx, next) { ctx.assertPresent = function assertPresent(value, message) { if (value === undefined || value === null || value === '') { - throw httpErrors.BadRequest(message); + throw apiError(400, 'validation_error', message); } }; ctx.assertEmail = function assertEmail(value, message) { if (!validator.isEmail(value)) { - throw httpErrors.BadRequest(message); + throw apiError(400, 'validation_error', message); } }; ctx.assertUuid = function assertUuid(value, message) { if (!validator.isUUID(value)) { - throw httpErrors.BadRequest(message); + throw apiError(400, 'validation_error', message); } }; diff --git a/server/api/user.js b/server/api/user.js index 5ebbb945..cad6d4be 100644 --- a/server/api/user.js +++ b/server/api/user.js @@ -25,7 +25,7 @@ router.post('user.s3Upload', auth(), async (ctx) => { const policy = makePolicy(); ctx.body = { data: { - max_upload_size: process.env.AWS_S3_UPLOAD_MAX_SIZE, + maxUploadSize: process.env.AWS_S3_UPLOAD_MAX_SIZE, upload_url: process.env.AWS_S3_UPLOAD_BUCKET_URL, form: { AWSAccessKeyId: process.env.AWS_ACCESS_KEY_ID, @@ -37,7 +37,7 @@ router.post('user.s3Upload', auth(), async (ctx) => { policy, }, asset: { - content_type: kind, + contentType: kind, url: `${process.env.AWS_S3_UPLOAD_BUCKET_URL}${s3Key}/${filename}`, name: filename, size, diff --git a/server/errors.js b/server/errors.js new file mode 100644 index 00000000..dae99fee --- /dev/null +++ b/server/errors.js @@ -0,0 +1,10 @@ +import httpErrors from 'http-errors'; + +const apiError = (code, id, message) => { + return httpErrors(code, message, { id }); +}; + +export default apiError; +export { + httpErrors, +};