diff --git a/app/components/Auth.js b/app/components/Auth.js index 2218461d..81388c01 100644 --- a/app/components/Auth.js +++ b/app/components/Auth.js @@ -16,8 +16,10 @@ let authenticatedStores; const Auth = ({ children }: Props) => { if (stores.auth.authenticated) { + stores.auth.fetch(); + + // TODO: Show loading state if (!stores.auth.team || !stores.auth.user) { - stores.auth.fetch(); return null; } diff --git a/server/api/hooks.js b/server/api/hooks.js index c59017d5..8d9b9174 100644 --- a/server/api/hooks.js +++ b/server/api/hooks.js @@ -14,7 +14,9 @@ router.post('hooks.unfurl', async ctx => { throw new AuthenticationError('Invalid token'); // TODO: Everything from here onwards will get moved to an async job - const user = await User.find({ where: { slackId: event.user } }); + const user = await User.find({ + where: { service: 'slack', serviceId: event.user }, + }); if (!user) return; const auth = await Authentication.find({ @@ -55,7 +57,8 @@ router.post('hooks.slack', async ctx => { const user = await User.find({ where: { - slackId: user_id, + service: 'slack', + serviceId: user_id, }, }); diff --git a/server/api/hooks.test.js b/server/api/hooks.test.js index b0283533..0d7add05 100644 --- a/server/api/hooks.test.js +++ b/server/api/hooks.test.js @@ -32,7 +32,7 @@ describe('#hooks.unfurl', async () => { event: { type: 'link_shared', channel: 'Cxxxxxx', - user: user.slackId, + user: user.serviceId, message_ts: '123456789.9875', links: [ { @@ -55,7 +55,7 @@ describe('#hooks.slack', async () => { const res = await server.post('/api/hooks.slack', { body: { token: process.env.SLACK_VERIFICATION_TOKEN, - user_id: user.slackId, + user_id: user.serviceId, text: 'dsfkndfskndsfkn', }, }); @@ -70,7 +70,7 @@ describe('#hooks.slack', async () => { const res = await server.post('/api/hooks.slack', { body: { token: process.env.SLACK_VERIFICATION_TOKEN, - user_id: user.slackId, + user_id: user.serviceId, text: document.title, }, }); @@ -98,7 +98,7 @@ describe('#hooks.slack', async () => { const res = await server.post('/api/hooks.slack', { body: { token: 'wrong-verification-token', - user_id: user.slackId, + user_id: user.serviceId, text: 'Welcome', }, }); diff --git a/server/auth/google.js b/server/auth/google.js index 330601c8..1f1c5f7e 100644 --- a/server/auth/google.js +++ b/server/auth/google.js @@ -1,6 +1,7 @@ // @flow import Router from 'koa-router'; import addMonths from 'date-fns/add_months'; +import { capitalize } from 'lodash'; import { OAuth2Client } from 'google-auth-library'; import { User, Team } from '../models'; @@ -32,17 +33,14 @@ router.get('google.callback', async ctx => { const response = await client.getToken(code); client.setCredentials(response.tokens); - console.log('Tokens acquired.'); - console.log(response.tokens); - const profile = await client.request({ url: 'https://www.googleapis.com/oauth2/v1/userinfo', }); - const teamName = profile.data.hd.split('.')[0]; + const teamName = capitalize(profile.data.hd.split('.')[0]); const [team, isFirstUser] = await Team.findOrCreate({ where: { - slackId: profile.data.hd, + googleId: profile.data.hd, }, defaults: { name: teamName, @@ -50,9 +48,10 @@ router.get('google.callback', async ctx => { }, }); - const [user, isFirstSignin] = await User.findOrCreate({ + const [user] = await User.findOrCreate({ where: { - slackId: profile.data.id, + service: 'google', + serviceId: profile.data.id, teamId: team.id, }, defaults: { @@ -63,10 +62,6 @@ router.get('google.callback', async ctx => { }, }); - if (!isFirstSignin) { - await user.save(); - } - if (isFirstUser) { await team.createFirstCollection(user.id); } @@ -77,7 +72,7 @@ router.get('google.callback', async ctx => { }); ctx.cookies.set('accessToken', user.getJwtToken(), { httpOnly: false, - expires: addMonths(new Date(), 6), + expires: addMonths(new Date(), 1), }); ctx.redirect('/'); diff --git a/server/auth/slack.js b/server/auth/slack.js index dede5531..eb3eb58f 100644 --- a/server/auth/slack.js +++ b/server/auth/slack.js @@ -26,7 +26,9 @@ router.post('auth.slack', async ctx => { const data = await Slack.oauthAccess(code); - let user = await User.findOne({ where: { slackId: data.user.id } }); + let user = await User.findOne({ + where: { service: 'slack', serviceId: data.user.id }, + }); let team = await Team.findOne({ where: { slackId: data.team.id } }); const isFirstUser = !team; @@ -48,7 +50,8 @@ router.post('auth.slack', async ctx => { await user.save(); } else { user = await User.create({ - slackId: data.user.id, + service: 'slack', + serviceId: data.user.id, name: data.user.name, email: data.user.email, teamId: team.id, diff --git a/server/migrations/20180528233909-google-auth.js b/server/migrations/20180528233909-google-auth.js new file mode 100644 index 00000000..c68f8520 --- /dev/null +++ b/server/migrations/20180528233909-google-auth.js @@ -0,0 +1,27 @@ +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.addColumn('teams', 'googleId', { + type: Sequelize.STRING, + allowNull: true, + unique: true + }); + await queryInterface.addColumn('teams', 'avatarUrl', { + type: Sequelize.STRING, + allowNull: true, + }); + await queryInterface.addColumn('users', 'service', { + type: Sequelize.STRING, + allowNull: true, + defaultValue: 'slack' + }); + await queryInterface.renameColumn('users', 'slackId', 'serviceId'); + await queryInterface.addIndex('teams', ['googleId']); + }, + down: async (queryInterface, Sequelize) => { + await queryInterface.removeColumn('teams', 'googleId'); + await queryInterface.removeColumn('teams', 'avatarUrl'); + await queryInterface.removeColumn('users', 'service'); + await queryInterface.renameColumn('users', 'serviceId', 'slackId'); + await queryInterface.removeIndex('teams', ['googleId']); + } +} \ No newline at end of file diff --git a/server/models/Team.js b/server/models/Team.js index 598af8d7..1ce90d28 100644 --- a/server/models/Team.js +++ b/server/models/Team.js @@ -13,6 +13,8 @@ const Team = sequelize.define( }, name: DataTypes.STRING, slackId: { type: DataTypes.STRING, allowNull: true }, + googleId: { type: DataTypes.STRING, allowNull: true }, + avatarUrl: { type: DataTypes.STRING, allowNull: true }, slackData: DataTypes.JSONB, }, { diff --git a/server/models/User.js b/server/models/User.js index 878757e4..4c6e68ce 100644 --- a/server/models/User.js +++ b/server/models/User.js @@ -25,7 +25,8 @@ const User = sequelize.define( passwordDigest: DataTypes.STRING, isAdmin: DataTypes.BOOLEAN, slackAccessToken: encryptedFields.vault('slackAccessToken'), - slackId: { type: DataTypes.STRING, allowNull: true, unique: true }, + service: { type: DataTypes.STRING, allowNull: true, unique: true }, + serviceId: { type: DataTypes.STRING, allowNull: true, unique: true }, slackData: DataTypes.JSONB, jwtSecret: encryptedFields.vault('jwtSecret'), suspendedAt: DataTypes.DATE, diff --git a/server/pages/components/SignupButton.js b/server/pages/components/SignupButton.js index 5c6c1504..3c06a0e8 100644 --- a/server/pages/components/SignupButton.js +++ b/server/pages/components/SignupButton.js @@ -3,6 +3,7 @@ import * as React from 'react'; import styled from 'styled-components'; import { signin } from '../../../shared/utils/routeHelpers'; import Flex from '../../../shared/components/Flex'; +import GoogleLogo from '../../../shared/components/GoogleLogo'; import SlackLogo from '../../../shared/components/SlackLogo'; import { color } from '../../../shared/styles/constants'; @@ -10,22 +11,27 @@ type Props = { lastLoggedIn: string, }; -const SlackSignin = ({ lastLoggedIn }: Props) => { +const SignupButton = ({ lastLoggedIn }: Props) => { return ( - + - {lastLoggedIn === 'slack' && 'You signed in with Slack previously'} + + {lastLoggedIn === 'slack' && 'You signed in with Slack previously'} +   - + - {lastLoggedIn === 'google' && 'You signed in with Google previously'} + + {lastLoggedIn === 'google' && 'You signed in with Google previously'} + ); @@ -43,6 +49,13 @@ const Button = styled.a` background: ${color.black}; border-radius: 4px; font-weight: 600; + height: 56px; `; -export default SlackSignin; +const LastLogin = styled.p` + font-size: 12px; + color: ${color.slate}; + padding-top: 4px; +`; + +export default SignupButton; diff --git a/server/test/factories.js b/server/test/factories.js index b7620908..0d8b4ebe 100644 --- a/server/test/factories.js +++ b/server/test/factories.js @@ -40,7 +40,8 @@ export async function buildUser(overrides: Object = {}) { username: `user${count}`, name: `User ${count}`, password: 'test123!', - slackId: uuid.v4(), + service: 'slack', + serviceId: uuid.v4(), ...overrides, }); } diff --git a/server/test/support.js b/server/test/support.js index d448b44b..826c8ffa 100644 --- a/server/test/support.js +++ b/server/test/support.js @@ -30,7 +30,8 @@ const seed = async () => { name: 'User 1', password: 'test123!', teamId: team.id, - slackId: 'U2399UF2P', + service: 'slack', + serviceId: 'U2399UF2P', slackData: { id: 'U2399UF2P', image_192: 'http://example.com/avatar.png', @@ -45,7 +46,8 @@ const seed = async () => { password: 'test123!', teamId: team.id, isAdmin: true, - slackId: 'U2399UF1P', + service: 'slack', + serviceId: 'U2399UF1P', slackData: { id: 'U2399UF1P', image_192: 'http://example.com/avatar.png', diff --git a/shared/components/GoogleLogo.js b/shared/components/GoogleLogo.js new file mode 100644 index 00000000..85ffe7ab --- /dev/null +++ b/shared/components/GoogleLogo.js @@ -0,0 +1,27 @@ +// @flow +import * as React from 'react'; + +type Props = { + size?: number, + fill?: string, + className?: string, +}; + +function GoogleLogo({ size = 34, fill = '#FFF', className }: Props) { + return ( + + + + + + ); +} + +export default GoogleLogo;