From bf84f792e2417703026486d99040f847a2b24bd4 Mon Sep 17 00:00:00 2001 From: Jori Lallo Date: Sun, 11 Sep 2016 15:48:43 -0700 Subject: [PATCH 01/11] lint --- server/models/Document.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/models/Document.js b/server/models/Document.js index ce2f82f21..d5ecb648d 100644 --- a/server/models/Document.js +++ b/server/models/Document.js @@ -126,6 +126,6 @@ Document.searchForUser = async (user, query, options = {}) => { ); return documents; -} +}; export default Document; From 48a9a9f285a483ffeee39beb42190108ae3bd6b8 Mon Sep 17 00:00:00 2001 From: Jori Lallo Date: Sun, 11 Sep 2016 16:34:57 -0700 Subject: [PATCH 02/11] Added signup API endpoint --- server/api/__snapshots__/auth.test.js.snap | 27 ++++++ server/api/auth.js | 30 +++++++ server/api/auth.test.js | 85 +++++++++++++++++++ .../20160911230444-user-optional-slack-id.js | 46 ++++++++++ .../20160911232911-user-unique-fields.js | 46 ++++++++++ server/models/Team.js | 2 +- server/models/User.js | 6 +- .../__snapshots__/user.test.js.snap | 9 ++ server/presenters/user.js | 4 +- server/presenters/user.test.js | 14 +++ server/test/helper.js | 4 +- 11 files changed, 265 insertions(+), 8 deletions(-) create mode 100644 server/api/__snapshots__/auth.test.js.snap create mode 100644 server/api/auth.test.js create mode 100644 server/migrations/20160911230444-user-optional-slack-id.js create mode 100644 server/migrations/20160911232911-user-unique-fields.js diff --git a/server/api/__snapshots__/auth.test.js.snap b/server/api/__snapshots__/auth.test.js.snap new file mode 100644 index 000000000..f921db9f4 --- /dev/null +++ b/server/api/__snapshots__/auth.test.js.snap @@ -0,0 +1,27 @@ +exports[`test should require params 1`] = ` +Object { + "error": "name is required", + "ok": false +} +`; + +exports[`test should require unique email 1`] = ` +Object { + "error": "User already exists with this email", + "ok": false +} +`; + +exports[`test should require unique username 1`] = ` +Object { + "error": "User already exists with this username", + "ok": false +} +`; + +exports[`test should require valid email 1`] = ` +Object { + "error": "email is invalid", + "ok": false +} +`; diff --git a/server/api/auth.js b/server/api/auth.js index 351331ad4..6783bd7b1 100644 --- a/server/api/auth.js +++ b/server/api/auth.js @@ -8,6 +8,36 @@ import { User, Team } from '../models'; const router = new Router(); +router.post('auth.signup', async (ctx) => { + const { username, name, email, password } = ctx.request.body; + + ctx.assertPresent(username, 'name is required'); + ctx.assertPresent(name, 'name is required'); + ctx.assertPresent(email, 'email is required'); + ctx.assertEmail(email, 'email is invalid'); + ctx.assertPresent(password, 'password is required'); + + if (await User.findOne({ where: { email } })) { + throw httpErrors.BadRequest('User already exists with this email'); + } + + if (await User.findOne({ where: { username } })) { + throw httpErrors.BadRequest('User already exists with this username'); + } + + const user = await User.create({ + username, + name, + email, + password, + }); + + ctx.body = { data: { + user: await presentUser(ctx, user), + accessToken: user.getJwtToken(), + } }; +}); + router.post('auth.slack', async (ctx) => { const { code } = ctx.body; ctx.assertPresent(code, 'code is required'); diff --git a/server/api/auth.test.js b/server/api/auth.test.js new file mode 100644 index 000000000..03fa35b1d --- /dev/null +++ b/server/api/auth.test.js @@ -0,0 +1,85 @@ +import TestServer from 'fetch-test-server'; +import app from '..'; +import { flushdb, sequelize, seed } from '../test/support'; + +const server = new TestServer(app.callback()); + +beforeEach(flushdb); +afterAll(() => server.close()); +afterAll(() => sequelize.close()); + +it('should signup a new user', async () => { + const res = await server.post('/api/auth.signup', { + body: { + username: 'testuser', + name: 'Test User', + email: 'new.user@example.com', + password: 'test123!', + }, + }); + const body = await res.json(); + + expect(res.status).toEqual(200); + expect(body.ok).toBe(true); + expect(body.data.user).toBeTruthy(); +}); + +it('should require params', async () => { + const res = await server.post('/api/auth.signup', { + body: { + username: 'testuser', + }, + }); + const body = await res.json(); + + expect(res.status).toEqual(400); + expect(body).toMatchSnapshot(); +}); + + +it('should require valid email', async () => { + const res = await server.post('/api/auth.signup', { + body: { + username: 'testuser', + name: 'Test User', + email: 'example.com', + password: 'test123!', + }, + }); + const body = await res.json(); + + expect(res.status).toEqual(400); + expect(body).toMatchSnapshot(); +}); + +it('should require unique email', async () => { + await seed(); + const res = await server.post('/api/auth.signup', { + body: { + username: 'testuser', + name: 'Test User', + email: 'user1@example.com', + password: 'test123!', + }, + }); + const body = await res.json(); + + expect(res.status).toEqual(400); + expect(body).toMatchSnapshot(); +}); + +it('should require unique username', async () => { + await seed(); + const res = await server.post('/api/auth.signup', { + body: { + username: 'user1', + name: 'Test User', + email: 'userone@example.com', + password: 'test123!', + }, + }); + const body = await res.json(); + + expect(res.status).toEqual(400); + expect(body).toMatchSnapshot(); +}); diff --git a/server/migrations/20160911230444-user-optional-slack-id.js b/server/migrations/20160911230444-user-optional-slack-id.js new file mode 100644 index 000000000..b1bf1f188 --- /dev/null +++ b/server/migrations/20160911230444-user-optional-slack-id.js @@ -0,0 +1,46 @@ +/* eslint-disable */ +'use strict'; + +module.exports = { + up: function (queryInterface, Sequelize) { + queryInterface.changeColumn( + 'users', + 'slackId', + { + type: Sequelize.STRING, + unique: false, + allowNull: true, + } + ); + queryInterface.changeColumn( + 'teams', + 'slackId', + { + type: Sequelize.STRING, + unique: false, + allowNull: true, + } + ); + }, + + down: function (queryInterface, Sequelize) { + queryInterface.changeColumn( + 'users', + 'slackId', + { + type: Sequelize.STRING, + unique: true, + allowNull: false, + } + ); + queryInterface.changeColumn( + 'teams', + 'slackId', + { + type: Sequelize.STRING, + unique: true, + allowNull: false, + } + ); + }, +}; diff --git a/server/migrations/20160911232911-user-unique-fields.js b/server/migrations/20160911232911-user-unique-fields.js new file mode 100644 index 000000000..d394960f9 --- /dev/null +++ b/server/migrations/20160911232911-user-unique-fields.js @@ -0,0 +1,46 @@ +'use strict'; + +module.exports = { + up: function (queryInterface, Sequelize) { + queryInterface.changeColumn( + 'users', + 'email', + { + type: Sequelize.STRING, + unique: true, + allowNull: false, + } + ); + queryInterface.changeColumn( + 'users', + 'username', + { + type: Sequelize.STRING, + unique: true, + allowNull: false, + } + ); + }, + + down: function (queryInterface, Sequelize) { + queryInterface.changeColumn( + 'users', + 'email', + { + type: Sequelize.STRING, + unique: false, + allowNull: true, + } + ); + + queryInterface.changeColumn( + 'users', + 'username', + { + type: Sequelize.STRING, + unique: false, + allowNull: true, + } + ); + } +}; diff --git a/server/models/Team.js b/server/models/Team.js index 275fd2ef5..eda54bfc6 100644 --- a/server/models/Team.js +++ b/server/models/Team.js @@ -9,7 +9,7 @@ import User from './User'; const Team = sequelize.define('team', { id: { type: DataTypes.UUID, defaultValue: DataTypes.UUIDV4, primaryKey: true }, name: DataTypes.STRING, - slackId: { type: DataTypes.STRING, unique: true }, + slackId: { type: DataTypes.STRING, allowNull: true }, slackData: DataTypes.JSONB, }, { instanceMethods: { diff --git a/server/models/User.js b/server/models/User.js index 896636f86..4968e9a30 100644 --- a/server/models/User.js +++ b/server/models/User.js @@ -9,12 +9,12 @@ import JWT from 'jsonwebtoken'; const User = sequelize.define('user', { id: { type: DataTypes.UUID, defaultValue: DataTypes.UUIDV4, primaryKey: true }, - email: DataTypes.STRING, - username: DataTypes.STRING, + email: { type: DataTypes.STRING, unique: true }, + username: { type: DataTypes.STRING, unique: true }, name: DataTypes.STRING, isAdmin: DataTypes.BOOLEAN, slackAccessToken: encryptedFields.vault('slackAccessToken'), - slackId: { type: DataTypes.STRING, unique: true }, + slackId: { type: DataTypes.STRING, allowNull: true }, slackData: DataTypes.JSONB, jwtSecret: encryptedFields.vault('jwtSecret'), }, { diff --git a/server/presenters/__snapshots__/user.test.js.snap b/server/presenters/__snapshots__/user.test.js.snap index eb254de7b..26af224b1 100644 --- a/server/presenters/__snapshots__/user.test.js.snap +++ b/server/presenters/__snapshots__/user.test.js.snap @@ -6,3 +6,12 @@ Object { "username": "testuser" } `; + +exports[`test presents a user without slack data 1`] = ` +Object { + "avatarUrl": null, + "id": "123", + "name": "Test User", + "username": "testuser" +} +`; diff --git a/server/presenters/user.js b/server/presenters/user.js index 9c22daac5..b8d997d91 100644 --- a/server/presenters/user.js +++ b/server/presenters/user.js @@ -4,9 +4,9 @@ const presentUser = (ctx, user) => { return new Promise(async (resolve, _reject) => { const data = { id: user.id, - name: user.name, username: user.username, - avatarUrl: user.slackData.image_192, + name: user.name, + avatarUrl: user.slackData ? user.slackData.image_192 : null, }; resolve(data); }); diff --git a/server/presenters/user.test.js b/server/presenters/user.test.js index 879670214..a26c10fdb 100644 --- a/server/presenters/user.test.js +++ b/server/presenters/user.test.js @@ -17,3 +17,17 @@ it('presents a user', async () => { expect(user).toMatchSnapshot(); }); + +it('presents a user without slack data', async () => { + const user = await presentUser( + ctx, + { + id: '123', + name: 'Test User', + username: 'testuser', + slackData: null, + }, + ); + + expect(user).toMatchSnapshot(); +}); diff --git a/server/test/helper.js b/server/test/helper.js index f37bc82bb..2823ad678 100644 --- a/server/test/helper.js +++ b/server/test/helper.js @@ -21,9 +21,9 @@ function runMigrations() { path: './server/migrations', }, }); - umzug.up() + return umzug.up() .then(() => { - sequelize.close(); + return sequelize.close(); }); } From 43e60aacafa95a892a4201369bca326397d43545 Mon Sep 17 00:00:00 2001 From: Jori Lallo Date: Sun, 11 Sep 2016 16:37:20 -0700 Subject: [PATCH 03/11] lint --- server/api/auth.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/api/auth.js b/server/api/auth.js index 6783bd7b1..281fbb02f 100644 --- a/server/api/auth.js +++ b/server/api/auth.js @@ -64,11 +64,11 @@ router.post('auth.slack', async (ctx) => { if (!allowedSlackIds.includes(data.team.id)) throw httpErrors.BadRequest('Invalid Slack team'); // User - let user = await User.findOne({ where: { slackId: data.user.id }}); + let user = await User.findOne({ where: { slackId: data.user.id } }); // Team let team = await Team.findOne({ where: { slackId: data.team.id } }); - let teamExisted = !!team; + const teamExisted = !!team; if (!team) { team = await Team.create({ name: data.team.name, From 0e7c21673563ecb639789d9919f63f69f5e1fcb2 Mon Sep 17 00:00:00 2001 From: Jori Lallo Date: Sun, 11 Sep 2016 17:47:22 -0700 Subject: [PATCH 04/11] Added user passwords --- package.json | 1 + .../20160911232911-user-unique-fields.js | 4 +- .../20160911234928-user-password.js | 12 ++++++ server/models/User.js | 40 +++++++++++++++++++ server/models/User.test.js | 22 ++++++++++ 5 files changed, 76 insertions(+), 3 deletions(-) create mode 100644 server/migrations/20160911234928-user-password.js create mode 100644 server/models/User.test.js diff --git a/package.json b/package.json index 07e7b8943..c101eedfd 100644 --- a/package.json +++ b/package.json @@ -62,6 +62,7 @@ "babel-preset-react-hmre": "1.1.1", "babel-preset-stage-0": "6.5.0", "babel-regenerator-runtime": "6.5.0", + "bcrypt": "^0.8.7", "bugsnag": "^1.7.0", "classnames": "2.2.3", "codemirror": "5.16.0", diff --git a/server/migrations/20160911232911-user-unique-fields.js b/server/migrations/20160911232911-user-unique-fields.js index d394960f9..5786ff4d0 100644 --- a/server/migrations/20160911232911-user-unique-fields.js +++ b/server/migrations/20160911232911-user-unique-fields.js @@ -1,5 +1,3 @@ -'use strict'; - module.exports = { up: function (queryInterface, Sequelize) { queryInterface.changeColumn( @@ -42,5 +40,5 @@ module.exports = { allowNull: true, } ); - } + }, }; diff --git a/server/migrations/20160911234928-user-password.js b/server/migrations/20160911234928-user-password.js new file mode 100644 index 000000000..f477dcd19 --- /dev/null +++ b/server/migrations/20160911234928-user-password.js @@ -0,0 +1,12 @@ +module.exports = { + up: (queryInterface, Sequelize) => { + queryInterface.addColumn('users', 'passwordDigest', { + type: Sequelize.STRING, + allowNull: true, + }); + }, + + down: (queryInterface, _Sequelize) => { + queryInterface.removeColumn('users', 'passwordDigest'); + }, +}; diff --git a/server/models/User.js b/server/models/User.js index 4968e9a30..0d6c2b3b5 100644 --- a/server/models/User.js +++ b/server/models/User.js @@ -1,4 +1,5 @@ import crypto from 'crypto'; +import bcrypt from 'bcrypt'; import { DataTypes, sequelize, @@ -7,11 +8,15 @@ import { import JWT from 'jsonwebtoken'; +const BCRYPT_COST = process.env.NODE_ENV !== 'production' ? 4 : 12; + const User = sequelize.define('user', { id: { type: DataTypes.UUID, defaultValue: DataTypes.UUIDV4, primaryKey: true }, email: { type: DataTypes.STRING, unique: true }, username: { type: DataTypes.STRING, unique: true }, name: DataTypes.STRING, + password: DataTypes.VIRTUAL, + passwordDigest: DataTypes.STRING, isAdmin: DataTypes.BOOLEAN, slackAccessToken: encryptedFields.vault('slackAccessToken'), slackId: { type: DataTypes.STRING, allowNull: true }, @@ -25,6 +30,23 @@ const User = sequelize.define('user', { async getTeam() { return this.team; }, + verifyPassword(password) { + return new Promise((resolve, reject) => { + if (!this.passwordDigest) { + resolve(false); + return; + } + + bcrypt.compare(password, this.passwordDigest, (err, ok) => { + if (err) { + reject(err); + return; + } + + resolve(ok); + }); + }); + }, }, indexes: [ { @@ -36,7 +58,25 @@ const User = sequelize.define('user', { const setRandomJwtSecret = (model) => { model.jwtSecret = crypto.randomBytes(64).toString('hex'); }; +const hashPassword = function hashPassword(model) { + if (!model.password) { + return null; + } + return new Promise((resolve, reject) => { + bcrypt.hash(model.password, BCRYPT_COST, (err, digest) => { + if (err) { + reject(err); + return; + } + + model.passwordDigest = digest; + resolve(); + }); + }); +}; +User.beforeCreate(hashPassword); +User.beforeUpdate(hashPassword); User.beforeCreate(setRandomJwtSecret); export default User; diff --git a/server/models/User.test.js b/server/models/User.test.js new file mode 100644 index 000000000..e130e8a22 --- /dev/null +++ b/server/models/User.test.js @@ -0,0 +1,22 @@ +import { User } from '.'; + +import { flushdb, sequelize } from '../test/support'; + +beforeEach(flushdb); +afterAll(() => sequelize.close()); + +it('should set JWT secret and password digest', async () => { + const user = User.build({ + username: 'user', + name: 'User', + email: 'user1@example.com', + password: 'test123!', + }); + await user.save(); + + expect(user.passwordDigest).toBeTruthy(); + expect(user.getJwtToken()).toBeTruthy(); + + expect(await user.verifyPassword('test123!')).toBe(true); + expect(await user.verifyPassword('badPasswd')).toBe(false); +}); From 39ce7dc9d1700e45ef62f76d999ae475ac463450 Mon Sep 17 00:00:00 2001 From: Jori Lallo Date: Sun, 11 Sep 2016 17:47:27 -0700 Subject: [PATCH 05/11] lint --- server/routes.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/server/routes.js b/server/routes.js index d98cf6f7f..3266e832a 100644 --- a/server/routes.js +++ b/server/routes.js @@ -1,4 +1,4 @@ -const path = require('path'); +import path from 'path'; import httpErrors from 'http-errors'; import Koa from 'koa'; import Router from 'koa-router'; @@ -21,21 +21,21 @@ router.get('/_health', ctx => (ctx.body = 'OK')); if (process.env.NODE_ENV === 'production') { router.get('/static/*', async (ctx) => { ctx.set({ - 'Cache-Control': `max-age=${356*24*60*60}`, + 'Cache-Control': `max-age=${356 * 24 * 60 * 60}`, }); - const stats = await sendfile(ctx, path.join(__dirname, '../dist/', ctx.path.substring(8))); + await sendfile(ctx, path.join(__dirname, '../dist/', ctx.path.substring(8))); }); router.get('*', async (ctx) => { - const stats = await sendfile(ctx, path.join(__dirname, '../dist/index.html')); + await sendfile(ctx, path.join(__dirname, '../dist/index.html')); if (!ctx.status) ctx.throw(httpErrors.NotFound()); }); koa.use(subdomainRedirect()); } else { router.get('*', async (ctx) => { - const stats = await sendfile(ctx, path.join(__dirname, './static/dev.html')); + await sendfile(ctx, path.join(__dirname, './static/dev.html')); if (!ctx.status) ctx.throw(httpErrors.NotFound()); }); } From 969243c3e441faf228977d60babe081d4fcd0739 Mon Sep 17 00:00:00 2001 From: Jori Lallo Date: Sun, 11 Sep 2016 22:44:44 -0700 Subject: [PATCH 06/11] Added auth.login API --- server/api/__snapshots__/auth.test.js.snap | 47 ++++- server/api/auth.js | 24 +++ server/api/auth.test.js | 193 ++++++++++++++------- server/test/support.js | 1 + 4 files changed, 201 insertions(+), 64 deletions(-) diff --git a/server/api/__snapshots__/auth.test.js.snap b/server/api/__snapshots__/auth.test.js.snap index f921db9f4..c132cff7e 100644 --- a/server/api/__snapshots__/auth.test.js.snap +++ b/server/api/__snapshots__/auth.test.js.snap @@ -1,27 +1,66 @@ -exports[`test should require params 1`] = ` +exports[`#auth.signup should require params 1`] = ` Object { "error": "name is required", "ok": false } `; -exports[`test should require unique email 1`] = ` +exports[`#auth.signup should require unique email 1`] = ` Object { "error": "User already exists with this email", "ok": false } `; -exports[`test should require unique username 1`] = ` +exports[`#auth.signup should require unique username 1`] = ` Object { "error": "User already exists with this username", "ok": false } `; -exports[`test should require valid email 1`] = ` +exports[`#auth.signup should require valid email 1`] = ` Object { "error": "email is invalid", "ok": false } `; + +exports[`#login should login with email 1`] = ` +Object { + "avatarUrl": "http://example.com/avatar.png", + "id": "86fde1d4-0050-428f-9f0b-0bf77f8bdf61", + "name": "User 1", + "username": "user1" +} +`; + +exports[`#login should login with username 1`] = ` +Object { + "avatarUrl": "http://example.com/avatar.png", + "id": "86fde1d4-0050-428f-9f0b-0bf77f8bdf61", + "name": "User 1", + "username": "user1" +} +`; + +exports[`#login should require either username or email 1`] = ` +Object { + "error": "username or email is required", + "ok": false +} +`; + +exports[`#login should require password 1`] = ` +Object { + "error": "password is required", + "ok": false +} +`; + +exports[`#login should validate password 1`] = ` +Object { + "error": "Invalid password", + "ok": false +} +`; diff --git a/server/api/auth.js b/server/api/auth.js index 281fbb02f..5a7545b0f 100644 --- a/server/api/auth.js +++ b/server/api/auth.js @@ -38,6 +38,30 @@ router.post('auth.signup', async (ctx) => { } }; }); +router.post('auth.login', async (ctx) => { + const { username, email, password } = ctx.request.body; + + 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 } }); + } else { + throw httpErrors.BadRequest('username or email is required'); + } + + if (!await user.verifyPassword(password)) { + throw httpErrors.BadRequest('Invalid password'); + } + + ctx.body = { data: { + user: await presentUser(ctx, user), + accessToken: user.getJwtToken(), + } }; +}); + router.post('auth.slack', async (ctx) => { const { code } = ctx.body; ctx.assertPresent(code, 'code is required'); diff --git a/server/api/auth.test.js b/server/api/auth.test.js index 03fa35b1d..283a92328 100644 --- a/server/api/auth.test.js +++ b/server/api/auth.test.js @@ -8,78 +8,151 @@ beforeEach(flushdb); afterAll(() => server.close()); afterAll(() => sequelize.close()); -it('should signup a new user', async () => { - const res = await server.post('/api/auth.signup', { - body: { - username: 'testuser', - name: 'Test User', - email: 'new.user@example.com', - password: 'test123!', - }, - }); - const body = await res.json(); +describe('#auth.signup', async () => { + it('should signup a new user', async () => { + const res = await server.post('/api/auth.signup', { + body: { + username: 'testuser', + name: 'Test User', + email: 'new.user@example.com', + password: 'test123!', + }, + }); + const body = await res.json(); - expect(res.status).toEqual(200); - expect(body.ok).toBe(true); - expect(body.data.user).toBeTruthy(); + expect(res.status).toEqual(200); + expect(body.ok).toBe(true); + expect(body.data.user).toBeTruthy(); + }); + + it('should require params', async () => { + const res = await server.post('/api/auth.signup', { + body: { + username: 'testuser', + }, + }); + const body = await res.json(); + + expect(res.status).toEqual(400); + expect(body).toMatchSnapshot(); + }); + + + it('should require valid email', async () => { + const res = await server.post('/api/auth.signup', { + body: { + username: 'testuser', + name: 'Test User', + email: 'example.com', + password: 'test123!', + }, + }); + const body = await res.json(); + + expect(res.status).toEqual(400); + expect(body).toMatchSnapshot(); + }); + + it('should require unique email', async () => { + await seed(); + const res = await server.post('/api/auth.signup', { + body: { + username: 'testuser', + name: 'Test User', + email: 'user1@example.com', + password: 'test123!', + }, + }); + const body = await res.json(); + + expect(res.status).toEqual(400); + expect(body).toMatchSnapshot(); + }); + + it('should require unique username', async () => { + await seed(); + const res = await server.post('/api/auth.signup', { + body: { + username: 'user1', + name: 'Test User', + email: 'userone@example.com', + password: 'test123!', + }, + }); + const body = await res.json(); + + expect(res.status).toEqual(400); + expect(body).toMatchSnapshot(); + }); }); -it('should require params', async () => { - const res = await server.post('/api/auth.signup', { - body: { - username: 'testuser', - }, +describe('#login', () => { + test('should login with email', async () => { + await seed(); + const res = await server.post('/api/auth.login', { + body: { + email: 'user1@example.com', + password: 'test123!', + }, + }); + const body = await res.json(); + + expect(res.status).toEqual(200); + expect(body.ok).toBe(true); + expect(body.data.user).toMatchSnapshot(); }); - const body = await res.json(); - expect(res.status).toEqual(400); - expect(body).toMatchSnapshot(); -}); + test('should login with username', async () => { + await seed(); + const res = await server.post('/api/auth.login', { + body: { + username: 'user1', + password: 'test123!', + }, + }); + const body = await res.json(); - -it('should require valid email', async () => { - const res = await server.post('/api/auth.signup', { - body: { - username: 'testuser', - name: 'Test User', - email: 'example.com', - password: 'test123!', - }, + expect(res.status).toEqual(200); + expect(body.ok).toBe(true); + expect(body.data.user).toMatchSnapshot(); }); - const body = await res.json(); - expect(res.status).toEqual(400); - expect(body).toMatchSnapshot(); -}); + test('should validate password', async () => { + await seed(); + const res = await server.post('/api/auth.login', { + body: { + email: 'user1@example.com', + password: 'bad_password', + }, + }); + const body = await res.json(); -it('should require unique email', async () => { - await seed(); - const res = await server.post('/api/auth.signup', { - body: { - username: 'testuser', - name: 'Test User', - email: 'user1@example.com', - password: 'test123!', - }, + expect(res.status).toEqual(400); + expect(body).toMatchSnapshot(); }); - const body = await res.json(); - expect(res.status).toEqual(400); - expect(body).toMatchSnapshot(); -}); + test('should require either username or email', async () => { + const res = await server.post('/api/auth.login', { + body: { + password: 'test123!', + }, + }); + const body = await res.json(); -it('should require unique username', async () => { - await seed(); - const res = await server.post('/api/auth.signup', { - body: { - username: 'user1', - name: 'Test User', - email: 'userone@example.com', - password: 'test123!', - }, + expect(res.status).toEqual(400); + expect(body).toMatchSnapshot(); }); - const body = await res.json(); - expect(res.status).toEqual(400); - expect(body).toMatchSnapshot(); + test('should require password', async () => { + await seed(); + const res = await server.post('/api/auth.login', { + body: { + email: 'user1@example.com', + }, + }); + const body = await res.json(); + + expect(res.status).toEqual(400); + expect(body).toMatchSnapshot(); + }); }); diff --git a/server/test/support.js b/server/test/support.js index 4a379484c..c1646d62b 100644 --- a/server/test/support.js +++ b/server/test/support.js @@ -16,6 +16,7 @@ const seed = async () => { email: 'user1@example.com', username: 'user1', name: 'User 1', + password: 'test123!', slackId: '123', slackData: { image_192: 'http://example.com/avatar.png', From ef91cc17d55d682dbe85180c2f7490a33a41fb1e Mon Sep 17 00:00:00 2001 From: Jori Lallo Date: Sun, 11 Sep 2016 23:14:43 -0700 Subject: [PATCH 07/11] More api/auth tests --- server/api/__snapshots__/auth.test.js.snap | 78 +++++++++++----------- server/api/__snapshots__/user.test.js.snap | 4 +- server/api/auth.test.js | 2 +- server/api/user.test.js | 44 ++++++------ 4 files changed, 65 insertions(+), 63 deletions(-) diff --git a/server/api/__snapshots__/auth.test.js.snap b/server/api/__snapshots__/auth.test.js.snap index c132cff7e..716f58eb7 100644 --- a/server/api/__snapshots__/auth.test.js.snap +++ b/server/api/__snapshots__/auth.test.js.snap @@ -1,3 +1,42 @@ +exports[`#auth.login should login with email 1`] = ` +Object { + "avatarUrl": "http://example.com/avatar.png", + "id": "86fde1d4-0050-428f-9f0b-0bf77f8bdf61", + "name": "User 1", + "username": "user1" +} +`; + +exports[`#auth.login should login with username 1`] = ` +Object { + "avatarUrl": "http://example.com/avatar.png", + "id": "86fde1d4-0050-428f-9f0b-0bf77f8bdf61", + "name": "User 1", + "username": "user1" +} +`; + +exports[`#auth.login should require either username or email 1`] = ` +Object { + "error": "username or email is required", + "ok": false +} +`; + +exports[`#auth.login should require password 1`] = ` +Object { + "error": "password is required", + "ok": false +} +`; + +exports[`#auth.login should validate password 1`] = ` +Object { + "error": "Invalid password", + "ok": false +} +`; + exports[`#auth.signup should require params 1`] = ` Object { "error": "name is required", @@ -25,42 +64,3 @@ Object { "ok": false } `; - -exports[`#login should login with email 1`] = ` -Object { - "avatarUrl": "http://example.com/avatar.png", - "id": "86fde1d4-0050-428f-9f0b-0bf77f8bdf61", - "name": "User 1", - "username": "user1" -} -`; - -exports[`#login should login with username 1`] = ` -Object { - "avatarUrl": "http://example.com/avatar.png", - "id": "86fde1d4-0050-428f-9f0b-0bf77f8bdf61", - "name": "User 1", - "username": "user1" -} -`; - -exports[`#login should require either username or email 1`] = ` -Object { - "error": "username or email is required", - "ok": false -} -`; - -exports[`#login should require password 1`] = ` -Object { - "error": "password is required", - "ok": false -} -`; - -exports[`#login should validate password 1`] = ` -Object { - "error": "Invalid password", - "ok": false -} -`; diff --git a/server/api/__snapshots__/user.test.js.snap b/server/api/__snapshots__/user.test.js.snap index 5406bc5d2..1fd030521 100644 --- a/server/api/__snapshots__/user.test.js.snap +++ b/server/api/__snapshots__/user.test.js.snap @@ -1,11 +1,11 @@ -exports[`test should require authentication 1`] = ` +exports[`#user.info should require authentication 1`] = ` Object { "error": "Authentication required", "ok": false } `; -exports[`test should return known user 1`] = ` +exports[`#user.info should return known user 1`] = ` Object { "data": Object { "avatarUrl": "http://example.com/avatar.png", diff --git a/server/api/auth.test.js b/server/api/auth.test.js index 283a92328..fec61a649 100644 --- a/server/api/auth.test.js +++ b/server/api/auth.test.js @@ -86,7 +86,7 @@ describe('#auth.signup', async () => { }); }); -describe('#login', () => { +describe('#auth.login', () => { test('should login with email', async () => { await seed(); const res = await server.post('/api/auth.login', { diff --git a/server/api/user.test.js b/server/api/user.test.js index 3768321be..3c7075829 100644 --- a/server/api/user.test.js +++ b/server/api/user.test.js @@ -11,28 +11,30 @@ beforeEach(flushdb); afterAll(() => server.close()); afterAll(() => sequelize.close()); -it('should return known user', async () => { - await seed(); - const user = await User.findOne({ - where: { - email: 'user1@example.com', - }, +describe('#user.info', async () => { + it('should return known user', async () => { + await seed(); + const user = await User.findOne({ + where: { + email: 'user1@example.com', + }, + }); + + const res = await server.post('/api/user.info', { + body: { token: user.getJwtToken() }, + }); + const body = await res.json(); + + expect(res.status).toEqual(200); + expect(body).toMatchSnapshot(); }); - const res = await server.post('/api/user.info', { - body: { token: user.getJwtToken() }, + it('should require authentication', async () => { + await seed(); + const res = await server.post('/api/user.info'); + const body = await res.json(); + + expect(res.status).toEqual(401); + expect(body).toMatchSnapshot(); }); - const body = await res.json(); - - expect(res.status).toEqual(200); - expect(body).toMatchSnapshot(); -}); - -it('should require authentication', async () => { - await seed(); - const res = await server.post('/api/user.info'); - const body = await res.json(); - - expect(res.status).toEqual(401); - expect(body).toMatchSnapshot(); }); From e7ff801b97f445f0118a4cc7d902eecfd984be98 Mon Sep 17 00:00:00 2001 From: Jori Lallo Date: Mon, 12 Sep 2016 22:19:46 -0700 Subject: [PATCH 08/11] Upgraded packages --- package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index c101eedfd..1d5f17322 100644 --- a/package.json +++ b/package.json @@ -108,9 +108,9 @@ "lodash.orderby": "4.4.0", "marked": "0.3.6", "marked-sanitized": "^0.1.1", - "mobx": "2.4.3", + "mobx": "2.5.1", "mobx-react": "3.5.5", - "mobx-react-devtools": "4.2.0", + "mobx-react-devtools": "4.2.4", "moment": "2.13.0", "node-dev": "3.1.0", "node-sass": "3.8.0", @@ -128,7 +128,7 @@ "react-dropzone": "3.6.0", "react-helmet": "3.1.0", "react-keydown": "^1.6.1", - "react-router": "2.5.1", + "react-router": "2.8.0", "rebass": "0.2.6", "redis": "^2.6.2", "redis-lock": "^0.1.0", From 4493325429545c1c49ea4fd2b0d36a5c620fe829 Mon Sep 17 00:00:00 2001 From: Jori Lallo Date: Mon, 12 Sep 2016 22:19:59 -0700 Subject: [PATCH 09/11] Layout changes --- frontend/components/Layout/Layout.js | 60 ++++++++++++++-------------- frontend/scenes/Home/Home.js | 45 +++++++++++---------- 2 files changed, 56 insertions(+), 49 deletions(-) diff --git a/frontend/components/Layout/Layout.js b/frontend/components/Layout/Layout.js index 6bec7cc94..891f2cb74 100644 --- a/frontend/components/Layout/Layout.js +++ b/frontend/components/Layout/Layout.js @@ -36,13 +36,13 @@ class Layout extends React.Component { @keydown(['/', 't']) search() { - // if (!this.props.search) return; + if (!this.props.user) return; _.defer(() => browserHistory.push('/search')); } @keydown(['d']) dashboard() { - // if (!this.props.search) return; + if (!this.props.user) return; _.defer(() => browserHistory.push('/')); } @@ -75,34 +75,36 @@ class Layout extends React.Component { - { user.user && ( - - - { this.props.actions } - - { this.props.search && ( - -
- Search -
-
- ) } - } - > - Settings - Logout - + + + { this.props.actions } - ) } + { user.user && ( + + { this.props.search && ( + +
+ Search +
+
+ ) } + } + > + Settings + Logout + +
+ ) } +
diff --git a/frontend/scenes/Home/Home.js b/frontend/scenes/Home/Home.js index a39f60226..9bc841fc3 100644 --- a/frontend/scenes/Home/Home.js +++ b/frontend/scenes/Home/Home.js @@ -2,6 +2,9 @@ import React from 'react'; import { observer } from 'mobx-react'; import { browserHistory } from 'react-router' +import { Flex } from 'reflexbox'; +import Layout from 'components/Layout'; +import CenteredContent from 'components/CenteredContent'; import SlackAuthLink from 'components/SlackAuthLink'; import styles from './Home.scss'; @@ -20,26 +23,28 @@ export default class Home extends React.Component { render() { return ( -
-
-
-

Atlas

-

Simple, fast, markdown.

-

We're building a modern wiki for engineering teams.

-
-
- - Sign in with Slack - -
-
-
+ + + +
+

Atlas

+

Simple, fast, markdown.

+

We're building a modern wiki for engineering teams.

+
+
+ + Sign in with Slack + +
+
+
+
); } } From 39bb6de11b3adacf25ded942e1dbcc2a6ac25171 Mon Sep 17 00:00:00 2001 From: Jori Lallo Date: Mon, 12 Sep 2016 22:21:11 -0700 Subject: [PATCH 10/11] Cleaned styles --- frontend/scenes/Home/Home.scss | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/frontend/scenes/Home/Home.scss b/frontend/scenes/Home/Home.scss index 52a2f6d54..ca656c2a8 100644 --- a/frontend/scenes/Home/Home.scss +++ b/frontend/scenes/Home/Home.scss @@ -1,17 +1,5 @@ -.container { - display: flex; - flex: 1; - justify-content: space-around; -} - -.content { - margin: 40px; - max-width: 640px; -} - .intro { - font-family: "Atlas Typewriter", Monaco, monospace; font-size: 1.4em; line-height: 1.6em; margin-bottom: 40px; -} \ No newline at end of file +} From 1002a5b4a1b7d2d2523f1f1d2699a07422d9f7f2 Mon Sep 17 00:00:00 2001 From: Jori Lallo Date: Mon, 12 Sep 2016 22:26:40 -0700 Subject: [PATCH 11/11] home styles --- frontend/scenes/Home/Home.js | 5 ++--- frontend/scenes/Home/Home.scss | 12 ++++++++---- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/frontend/scenes/Home/Home.js b/frontend/scenes/Home/Home.js index 9bc841fc3..057b38cf0 100644 --- a/frontend/scenes/Home/Home.js +++ b/frontend/scenes/Home/Home.js @@ -27,9 +27,8 @@ export default class Home extends React.Component {
-

Atlas

-

Simple, fast, markdown.

-

We're building a modern wiki for engineering teams.

+

Simple, fast, markdown.

+

We're building a modern wiki for engineering teams.

diff --git a/frontend/scenes/Home/Home.scss b/frontend/scenes/Home/Home.scss index ca656c2a8..0aa7fd743 100644 --- a/frontend/scenes/Home/Home.scss +++ b/frontend/scenes/Home/Home.scss @@ -1,5 +1,9 @@ -.intro { - font-size: 1.4em; - line-height: 1.6em; - margin-bottom: 40px; +.title { + font-size: 36px; + margin-bottom: 24px; +} + +.copy { + font-size: 20px; + margin-bottom: 36px; }