WIP: Successful Google Auth, broke pretty much everything else in the process
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
// @flow
|
||||
import Router from 'koa-router';
|
||||
|
||||
import auth from './middlewares/authentication';
|
||||
import auth from '../middlewares/authentication';
|
||||
import pagination from './middlewares/pagination';
|
||||
import { presentApiKey } from '../presenters';
|
||||
import { ApiKey } from '../models';
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
// @flow
|
||||
import Router from 'koa-router';
|
||||
import auth from './middlewares/authentication';
|
||||
import auth from '../middlewares/authentication';
|
||||
import { presentUser, presentTeam } from '../presenters';
|
||||
import { Authentication, Integration, User, Team } from '../models';
|
||||
import * as Slack from '../slack';
|
||||
import { Team } from '../models';
|
||||
|
||||
const router = new Router();
|
||||
|
||||
@@ -19,126 +18,4 @@ router.post('auth.info', auth(), async ctx => {
|
||||
};
|
||||
});
|
||||
|
||||
router.post('auth.slack', async ctx => {
|
||||
const { code } = ctx.body;
|
||||
ctx.assertPresent(code, 'code is required');
|
||||
|
||||
const data = await Slack.oauthAccess(code);
|
||||
|
||||
let user = await User.findOne({ where: { slackId: data.user.id } });
|
||||
let team = await Team.findOne({ where: { slackId: data.team.id } });
|
||||
const isFirstUser = !team;
|
||||
|
||||
if (team) {
|
||||
team.name = data.team.name;
|
||||
team.slackData = data.team;
|
||||
await team.save();
|
||||
} else {
|
||||
team = await Team.create({
|
||||
name: data.team.name,
|
||||
slackId: data.team.id,
|
||||
slackData: data.team,
|
||||
});
|
||||
}
|
||||
|
||||
if (user) {
|
||||
user.slackAccessToken = data.access_token;
|
||||
user.slackData = data.user;
|
||||
await user.save();
|
||||
} else {
|
||||
user = await User.create({
|
||||
slackId: data.user.id,
|
||||
name: data.user.name,
|
||||
email: data.user.email,
|
||||
teamId: team.id,
|
||||
isAdmin: isFirstUser,
|
||||
slackData: data.user,
|
||||
slackAccessToken: data.access_token,
|
||||
});
|
||||
|
||||
// Set initial avatar
|
||||
await user.updateAvatar();
|
||||
await user.save();
|
||||
}
|
||||
|
||||
if (isFirstUser) {
|
||||
await team.createFirstCollection(user.id);
|
||||
}
|
||||
|
||||
// Signal to backend that the user is logged in.
|
||||
// This is only used to signal SSR rendering, not
|
||||
// used for auth.
|
||||
ctx.cookies.set('loggedIn', 'true', {
|
||||
httpOnly: false,
|
||||
expires: new Date('2100'),
|
||||
});
|
||||
|
||||
ctx.body = {
|
||||
data: {
|
||||
user: await presentUser(ctx, user),
|
||||
team: await presentTeam(ctx, team),
|
||||
accessToken: user.getJwtToken(),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
router.post('auth.slackCommands', auth(), async ctx => {
|
||||
const { code } = ctx.body;
|
||||
ctx.assertPresent(code, 'code is required');
|
||||
|
||||
const user = ctx.state.user;
|
||||
const endpoint = `${process.env.URL || ''}/auth/slack/commands`;
|
||||
const data = await Slack.oauthAccess(code, endpoint);
|
||||
const serviceId = 'slack';
|
||||
|
||||
const authentication = await Authentication.create({
|
||||
serviceId,
|
||||
userId: user.id,
|
||||
teamId: user.teamId,
|
||||
token: data.access_token,
|
||||
scopes: data.scope.split(','),
|
||||
});
|
||||
|
||||
await Integration.create({
|
||||
serviceId,
|
||||
type: 'command',
|
||||
userId: user.id,
|
||||
teamId: user.teamId,
|
||||
authenticationId: authentication.id,
|
||||
});
|
||||
});
|
||||
|
||||
router.post('auth.slackPost', auth(), async ctx => {
|
||||
const { code, collectionId } = ctx.body;
|
||||
ctx.assertPresent(code, 'code is required');
|
||||
|
||||
const user = ctx.state.user;
|
||||
const endpoint = `${process.env.URL || ''}/auth/slack/post`;
|
||||
const data = await Slack.oauthAccess(code, endpoint);
|
||||
const serviceId = 'slack';
|
||||
|
||||
const authentication = await Authentication.create({
|
||||
serviceId,
|
||||
userId: user.id,
|
||||
teamId: user.teamId,
|
||||
token: data.access_token,
|
||||
scopes: data.scope.split(','),
|
||||
});
|
||||
|
||||
await Integration.create({
|
||||
serviceId,
|
||||
type: 'post',
|
||||
userId: user.id,
|
||||
teamId: user.teamId,
|
||||
authenticationId: authentication.id,
|
||||
collectionId,
|
||||
events: [],
|
||||
settings: {
|
||||
url: data.incoming_webhook.url,
|
||||
channel: data.incoming_webhook.channel,
|
||||
channelId: data.incoming_webhook.channel_id,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -1,164 +0,0 @@
|
||||
/* eslint-disable flowtype/require-valid-file-annotation */
|
||||
import TestServer from 'fetch-test-server';
|
||||
import app from '..';
|
||||
import { flushdb, seed } from '../test/support';
|
||||
|
||||
const server = new TestServer(app.callback());
|
||||
|
||||
beforeEach(flushdb);
|
||||
afterAll(server.close);
|
||||
|
||||
describe.skip('#auth.signup', async () => {
|
||||
it('should signup a new user', async () => {
|
||||
const welcomeEmailMock = jest.fn();
|
||||
jest.doMock('../mailer', () => {
|
||||
return {
|
||||
welcome: welcomeEmailMock,
|
||||
};
|
||||
});
|
||||
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(welcomeEmailMock).toBeCalledWith('new.user@example.com');
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
describe.skip('#auth.login', () => {
|
||||
test('should login with email', async () => {
|
||||
await seed();
|
||||
const res = await server.post('/api/auth.login', {
|
||||
body: {
|
||||
username: '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();
|
||||
});
|
||||
|
||||
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();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.ok).toBe(true);
|
||||
expect(body.data.user).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();
|
||||
|
||||
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();
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,7 @@
|
||||
// @flow
|
||||
import Router from 'koa-router';
|
||||
|
||||
import auth from './middlewares/authentication';
|
||||
import auth from '../middlewares/authentication';
|
||||
import pagination from './middlewares/pagination';
|
||||
import { presentCollection } from '../presenters';
|
||||
import { Collection } from '../models';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// @flow
|
||||
import Router from 'koa-router';
|
||||
import Sequelize from 'sequelize';
|
||||
import auth from './middlewares/authentication';
|
||||
import auth from '../middlewares/authentication';
|
||||
import pagination from './middlewares/pagination';
|
||||
import { presentDocument, presentRevision } from '../presenters';
|
||||
import { Document, Collection, Share, Star, View, Revision } from '../models';
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
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';
|
||||
@@ -16,58 +14,24 @@ import shares from './shares';
|
||||
import team from './team';
|
||||
import integrations from './integrations';
|
||||
|
||||
import validation from './middlewares/validation';
|
||||
import methodOverride from '../middlewares/methodOverride';
|
||||
import cache from '../middlewares/cache';
|
||||
import errorHandling from './middlewares/errorHandling';
|
||||
import validation from '../middlewares/validation';
|
||||
import methodOverride from './middlewares/methodOverride';
|
||||
import cache from './middlewares/cache';
|
||||
import apiWrapper from './middlewares/apiWrapper';
|
||||
|
||||
const api = new Koa();
|
||||
const router = new Router();
|
||||
|
||||
// API error handler
|
||||
api.use(async (ctx, next) => {
|
||||
try {
|
||||
await next();
|
||||
} catch (err) {
|
||||
ctx.status = err.status || 500;
|
||||
let message = err.message || err.name;
|
||||
let error;
|
||||
|
||||
if (err instanceof Sequelize.ValidationError) {
|
||||
// super basic form error handling
|
||||
ctx.status = 400;
|
||||
if (err.errors && err.errors[0]) {
|
||||
message = `${err.errors[0].message} (${err.errors[0].path})`;
|
||||
}
|
||||
}
|
||||
|
||||
if (message.match('Authorization error')) {
|
||||
ctx.status = 403;
|
||||
error = 'authorization_error';
|
||||
}
|
||||
|
||||
if (ctx.status === 500) {
|
||||
message = 'Internal Server Error';
|
||||
error = 'internal_server_error';
|
||||
ctx.app.emit('error', err, ctx);
|
||||
}
|
||||
|
||||
ctx.body = {
|
||||
ok: false,
|
||||
error: _.snakeCase(err.id || error),
|
||||
status: err.status,
|
||||
message,
|
||||
data: err.errorData ? err.errorData : undefined,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// middlewares
|
||||
api.use(errorHandling());
|
||||
api.use(bodyParser());
|
||||
api.use(methodOverride());
|
||||
api.use(cache());
|
||||
api.use(validation());
|
||||
api.use(apiWrapper());
|
||||
|
||||
// routes
|
||||
router.use('/', auth.routes());
|
||||
router.use('/', user.routes());
|
||||
router.use('/', collections.routes());
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import Router from 'koa-router';
|
||||
import Integration from '../models/Integration';
|
||||
import pagination from './middlewares/pagination';
|
||||
import auth from './middlewares/authentication';
|
||||
import auth from '../middlewares/authentication';
|
||||
import { presentIntegration } from '../presenters';
|
||||
import policy from '../policies';
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { type Context } from 'koa';
|
||||
export default function apiWrapper() {
|
||||
return async function apiWrapperMiddleware(
|
||||
ctx: Context,
|
||||
next: () => Promise<void>
|
||||
next: () => Promise<*>
|
||||
) {
|
||||
await next();
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
// @flow
|
||||
import debug from 'debug';
|
||||
import { type Context } from 'koa';
|
||||
|
||||
const debugCache = debug('cache');
|
||||
|
||||
export default function cache() {
|
||||
return async function cacheMiddleware(ctx: Object, next: Function) {
|
||||
return async function cacheMiddleware(ctx: Context, next: () => Promise<*>) {
|
||||
ctx.cache = {};
|
||||
|
||||
ctx.cache.set = async (id, value) => {
|
||||
46
server/api/middlewares/errorHandling.js
Normal file
46
server/api/middlewares/errorHandling.js
Normal file
@@ -0,0 +1,46 @@
|
||||
// @flow
|
||||
import Sequelize from 'sequelize';
|
||||
import { snakeCase } from 'lodash';
|
||||
import { type Context } from 'koa';
|
||||
|
||||
export default function errorHandling() {
|
||||
return async function errorHandlingMiddleware(
|
||||
ctx: Context,
|
||||
next: () => Promise<*>
|
||||
) {
|
||||
try {
|
||||
await next();
|
||||
} catch (err) {
|
||||
ctx.status = err.status || 500;
|
||||
let message = err.message || err.name;
|
||||
let error;
|
||||
|
||||
if (err instanceof Sequelize.ValidationError) {
|
||||
// super basic form error handling
|
||||
ctx.status = 400;
|
||||
if (err.errors && err.errors[0]) {
|
||||
message = `${err.errors[0].message} (${err.errors[0].path})`;
|
||||
}
|
||||
}
|
||||
|
||||
if (message.match('Authorization error')) {
|
||||
ctx.status = 403;
|
||||
error = 'authorization_error';
|
||||
}
|
||||
|
||||
if (ctx.status === 500) {
|
||||
message = 'Internal Server Error';
|
||||
error = 'internal_server_error';
|
||||
ctx.app.emit('error', err, ctx);
|
||||
}
|
||||
|
||||
ctx.body = {
|
||||
ok: false,
|
||||
error: snakeCase(err.id || error),
|
||||
status: err.status,
|
||||
message,
|
||||
data: err.errorData ? err.errorData : undefined,
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import { type Context } from 'koa';
|
||||
export default function methodOverride() {
|
||||
return async function methodOverrideMiddleware(
|
||||
ctx: Context,
|
||||
next: () => Promise<void>
|
||||
next: () => Promise<*>
|
||||
) {
|
||||
if (ctx.method === 'POST') {
|
||||
// $FlowFixMe
|
||||
@@ -6,7 +6,7 @@ import { type Context } from 'koa';
|
||||
export default function pagination(options?: Object) {
|
||||
return async function paginationMiddleware(
|
||||
ctx: Context,
|
||||
next: () => Promise<void>
|
||||
next: () => Promise<*>
|
||||
) {
|
||||
const opts = {
|
||||
defaultLimit: 15,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// @flow
|
||||
import Router from 'koa-router';
|
||||
import auth from './middlewares/authentication';
|
||||
import auth from '../middlewares/authentication';
|
||||
import pagination from './middlewares/pagination';
|
||||
import { presentShare } from '../presenters';
|
||||
import { Document, User, Share } from '../models';
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import Router from 'koa-router';
|
||||
import { User } from '../models';
|
||||
|
||||
import auth from './middlewares/authentication';
|
||||
import auth from '../middlewares/authentication';
|
||||
import pagination from './middlewares/pagination';
|
||||
import { presentUser } from '../presenters';
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import Router from 'koa-router';
|
||||
import { makePolicy, signPolicy, publicS3Endpoint } from '../utils/s3';
|
||||
import { ValidationError } from '../errors';
|
||||
import { Event, User, Team } from '../models';
|
||||
import auth from './middlewares/authentication';
|
||||
import auth from '../middlewares/authentication';
|
||||
import { presentUser } from '../presenters';
|
||||
import policy from '../policies';
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// @flow
|
||||
import Router from 'koa-router';
|
||||
import auth from './middlewares/authentication';
|
||||
import auth from '../middlewares/authentication';
|
||||
import { presentView } from '../presenters';
|
||||
import { View, Document } from '../models';
|
||||
import policy from '../policies';
|
||||
|
||||
86
server/auth/google.js
Normal file
86
server/auth/google.js
Normal file
@@ -0,0 +1,86 @@
|
||||
// @flow
|
||||
import Router from 'koa-router';
|
||||
import addMonths from 'date-fns/add_months';
|
||||
import { OAuth2Client } from 'google-auth-library';
|
||||
import { User, Team } from '../models';
|
||||
|
||||
const router = new Router();
|
||||
const client = new OAuth2Client(
|
||||
process.env.GOOGLE_CLIENT_ID,
|
||||
process.env.GOOGLE_CLIENT_SECRET,
|
||||
`${process.env.URL}/auth/google.callback`
|
||||
);
|
||||
|
||||
// start the oauth process and redirect user to Google
|
||||
router.get('google', async ctx => {
|
||||
// Generate the url that will be used for the consent dialog.
|
||||
const authorizeUrl = client.generateAuthUrl({
|
||||
access_type: 'offline',
|
||||
scope: [
|
||||
'https://www.googleapis.com/auth/userinfo.profile',
|
||||
'https://www.googleapis.com/auth/userinfo.email',
|
||||
],
|
||||
prompt: 'consent',
|
||||
});
|
||||
ctx.redirect(authorizeUrl);
|
||||
});
|
||||
|
||||
// signin callback from Google
|
||||
router.get('google.callback', async ctx => {
|
||||
const { code } = ctx.request.query;
|
||||
ctx.assertPresent(code, 'code is required');
|
||||
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 [team, isFirstUser] = await Team.findOrCreate({
|
||||
where: {
|
||||
slackId: profile.data.hd,
|
||||
},
|
||||
defaults: {
|
||||
name: teamName,
|
||||
avatarUrl: `https://logo.clearbit.com/${profile.data.hd}`,
|
||||
},
|
||||
});
|
||||
|
||||
const [user, isFirstSignin] = await User.findOrCreate({
|
||||
where: {
|
||||
slackId: profile.data.id,
|
||||
teamId: team.id,
|
||||
},
|
||||
defaults: {
|
||||
name: profile.data.name,
|
||||
email: profile.data.email,
|
||||
isAdmin: isFirstUser,
|
||||
avatarUrl: profile.data.picture,
|
||||
},
|
||||
});
|
||||
|
||||
if (!isFirstSignin) {
|
||||
await user.save();
|
||||
}
|
||||
|
||||
if (isFirstUser) {
|
||||
await team.createFirstCollection(user.id);
|
||||
}
|
||||
|
||||
ctx.cookies.set('lastLoggedIn', 'google', {
|
||||
httpOnly: false,
|
||||
expires: new Date('2100'),
|
||||
});
|
||||
ctx.cookies.set('accessToken', user.getJwtToken(), {
|
||||
httpOnly: false,
|
||||
expires: addMonths(new Date(), 6),
|
||||
});
|
||||
|
||||
ctx.redirect('/');
|
||||
});
|
||||
|
||||
export default router;
|
||||
20
server/auth/index.js
Normal file
20
server/auth/index.js
Normal file
@@ -0,0 +1,20 @@
|
||||
// @flow
|
||||
import bodyParser from 'koa-bodyparser';
|
||||
import Koa from 'koa';
|
||||
import Router from 'koa-router';
|
||||
import validation from '../middlewares/validation';
|
||||
|
||||
import slack from './slack';
|
||||
import google from './google';
|
||||
|
||||
const auth = new Koa();
|
||||
const router = new Router();
|
||||
|
||||
router.use('/', slack.routes());
|
||||
router.use('/', google.routes());
|
||||
|
||||
auth.use(bodyParser());
|
||||
auth.use(validation());
|
||||
auth.use(router.routes());
|
||||
|
||||
export default auth;
|
||||
145
server/auth/slack.js
Normal file
145
server/auth/slack.js
Normal file
@@ -0,0 +1,145 @@
|
||||
// @flow
|
||||
import Router from 'koa-router';
|
||||
import auth from '../middlewares/authentication';
|
||||
import { slackAuth } from '../../shared/utils/routeHelpers';
|
||||
import { presentUser, presentTeam } from '../presenters';
|
||||
import { Authentication, Integration, User, Team } from '../models';
|
||||
import * as Slack from '../slack';
|
||||
|
||||
const router = new Router();
|
||||
|
||||
router.get('auth.slack', async ctx => {
|
||||
const state = Math.random()
|
||||
.toString(36)
|
||||
.substring(7);
|
||||
|
||||
ctx.cookies.set('state', state, {
|
||||
httpOnly: false,
|
||||
expires: new Date('2100'),
|
||||
});
|
||||
ctx.redirect(slackAuth(state));
|
||||
});
|
||||
|
||||
router.post('auth.slack', async ctx => {
|
||||
const { code } = ctx.body;
|
||||
ctx.assertPresent(code, 'code is required');
|
||||
|
||||
const data = await Slack.oauthAccess(code);
|
||||
|
||||
let user = await User.findOne({ where: { slackId: data.user.id } });
|
||||
let team = await Team.findOne({ where: { slackId: data.team.id } });
|
||||
const isFirstUser = !team;
|
||||
|
||||
if (team) {
|
||||
team.name = data.team.name;
|
||||
team.slackData = data.team;
|
||||
await team.save();
|
||||
} else {
|
||||
team = await Team.create({
|
||||
name: data.team.name,
|
||||
slackId: data.team.id,
|
||||
slackData: data.team,
|
||||
});
|
||||
}
|
||||
|
||||
if (user) {
|
||||
user.slackAccessToken = data.access_token;
|
||||
user.slackData = data.user;
|
||||
await user.save();
|
||||
} else {
|
||||
user = await User.create({
|
||||
slackId: data.user.id,
|
||||
name: data.user.name,
|
||||
email: data.user.email,
|
||||
teamId: team.id,
|
||||
isAdmin: isFirstUser,
|
||||
slackData: data.user,
|
||||
slackAccessToken: data.access_token,
|
||||
});
|
||||
|
||||
// Set initial avatar
|
||||
await user.updateAvatar();
|
||||
await user.save();
|
||||
}
|
||||
|
||||
if (isFirstUser) {
|
||||
await team.createFirstCollection(user.id);
|
||||
}
|
||||
|
||||
// Signal to backend that the user is logged in.
|
||||
// This is only used to signal SSR rendering, not
|
||||
// used for auth.
|
||||
ctx.cookies.set('loggedIn', 'true', {
|
||||
httpOnly: false,
|
||||
expires: new Date('2100'),
|
||||
});
|
||||
|
||||
ctx.body = {
|
||||
data: {
|
||||
user: await presentUser(ctx, user),
|
||||
team: await presentTeam(ctx, team),
|
||||
accessToken: user.getJwtToken(),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
router.post('auth.slackCommands', auth(), async ctx => {
|
||||
const { code } = ctx.body;
|
||||
ctx.assertPresent(code, 'code is required');
|
||||
|
||||
const user = ctx.state.user;
|
||||
const endpoint = `${process.env.URL || ''}/auth/slack/commands`;
|
||||
const data = await Slack.oauthAccess(code, endpoint);
|
||||
const serviceId = 'slack';
|
||||
|
||||
const authentication = await Authentication.create({
|
||||
serviceId,
|
||||
userId: user.id,
|
||||
teamId: user.teamId,
|
||||
token: data.access_token,
|
||||
scopes: data.scope.split(','),
|
||||
});
|
||||
|
||||
await Integration.create({
|
||||
serviceId,
|
||||
type: 'command',
|
||||
userId: user.id,
|
||||
teamId: user.teamId,
|
||||
authenticationId: authentication.id,
|
||||
});
|
||||
});
|
||||
|
||||
router.post('auth.slackPost', auth(), async ctx => {
|
||||
const { code, collectionId } = ctx.body;
|
||||
ctx.assertPresent(code, 'code is required');
|
||||
|
||||
const user = ctx.state.user;
|
||||
const endpoint = `${process.env.URL || ''}/auth/slack/post`;
|
||||
const data = await Slack.oauthAccess(code, endpoint);
|
||||
const serviceId = 'slack';
|
||||
|
||||
const authentication = await Authentication.create({
|
||||
serviceId,
|
||||
userId: user.id,
|
||||
teamId: user.teamId,
|
||||
token: data.access_token,
|
||||
scopes: data.scope.split(','),
|
||||
});
|
||||
|
||||
await Integration.create({
|
||||
serviceId,
|
||||
type: 'post',
|
||||
userId: user.id,
|
||||
teamId: user.teamId,
|
||||
authenticationId: authentication.id,
|
||||
collectionId,
|
||||
events: [],
|
||||
settings: {
|
||||
url: data.incoming_webhook.url,
|
||||
channel: data.incoming_webhook.channel,
|
||||
channelId: data.incoming_webhook.channel_id,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -8,6 +8,7 @@ import bugsnag from 'bugsnag';
|
||||
import onerror from 'koa-onerror';
|
||||
import updates from './utils/updates';
|
||||
|
||||
import auth from './auth';
|
||||
import api from './api';
|
||||
import emails from './emails';
|
||||
import routes from './routes';
|
||||
@@ -79,6 +80,7 @@ if (process.env.NODE_ENV === 'development') {
|
||||
}
|
||||
}
|
||||
|
||||
app.use(mount('/auth', auth));
|
||||
app.use(mount('/api', api));
|
||||
app.use(mount(routes));
|
||||
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
// @flow
|
||||
import JWT from 'jsonwebtoken';
|
||||
import { type Context } from 'koa';
|
||||
import { User, ApiKey } from '../../models';
|
||||
import { AuthenticationError, UserSuspendedError } from '../../errors';
|
||||
import { User, ApiKey } from '../models';
|
||||
import { AuthenticationError, UserSuspendedError } from '../errors';
|
||||
|
||||
export default function auth(options?: { required?: boolean } = {}) {
|
||||
return async function authMiddleware(
|
||||
ctx: Context,
|
||||
next: () => Promise<void>
|
||||
) {
|
||||
return async function authMiddleware(ctx: Context, next: () => Promise<*>) {
|
||||
let token;
|
||||
|
||||
const authorizationHeader = ctx.request.get('authorization');
|
||||
@@ -1,10 +1,11 @@
|
||||
// @flow
|
||||
import validator from 'validator';
|
||||
import { ParamRequiredError, ValidationError } from '../../errors';
|
||||
import { validateColorHex } from '../../../shared/utils/color';
|
||||
import { type Context } from 'koa';
|
||||
import { ParamRequiredError, ValidationError } from '../errors';
|
||||
import { validateColorHex } from '../../shared/utils/color';
|
||||
|
||||
export default function validation() {
|
||||
return function validationMiddleware(ctx: Object, next: Function) {
|
||||
return function validationMiddleware(ctx: Context, next: () => Promise<*>) {
|
||||
ctx.assertPresent = (value, message) => {
|
||||
if (value === undefined || value === null || value === '') {
|
||||
throw new ParamRequiredError(message);
|
||||
@@ -9,7 +9,11 @@ import SignupButton from './components/SignupButton';
|
||||
import { developers, githubUrl } from '../../shared/utils/routeHelpers';
|
||||
import { color } from '../../shared/styles/constants';
|
||||
|
||||
function Home() {
|
||||
type Props = {
|
||||
lastLoggedIn: string,
|
||||
};
|
||||
|
||||
function Home({ lastLoggedIn }: Props) {
|
||||
return (
|
||||
<span>
|
||||
<Helmet>
|
||||
@@ -23,7 +27,7 @@ function Home() {
|
||||
logs, brainstorming, & more…
|
||||
</HeroText>
|
||||
<p>
|
||||
<SignupButton />
|
||||
<SignupButton lastLoggedIn={lastLoggedIn} />
|
||||
</p>
|
||||
</Hero>
|
||||
<Features reverse={{ mobile: true, tablet: false, desktop: false }}>
|
||||
|
||||
@@ -2,15 +2,32 @@
|
||||
import * as React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { signin } from '../../../shared/utils/routeHelpers';
|
||||
import Flex from '../../../shared/components/Flex';
|
||||
import SlackLogo from '../../../shared/components/SlackLogo';
|
||||
import { color } from '../../../shared/styles/constants';
|
||||
|
||||
const SlackSignin = () => {
|
||||
type Props = {
|
||||
lastLoggedIn: string,
|
||||
};
|
||||
|
||||
const SlackSignin = ({ lastLoggedIn }: Props) => {
|
||||
return (
|
||||
<Button href={signin()}>
|
||||
<SlackLogo />
|
||||
<Spacer>Sign In with Slack</Spacer>
|
||||
</Button>
|
||||
<Flex justify="center">
|
||||
<Flex>
|
||||
<Button href={signin('slack')}>
|
||||
<SlackLogo />
|
||||
<Spacer>Sign In with Slack</Spacer>
|
||||
</Button>
|
||||
{lastLoggedIn === 'slack' && 'You signed in with Slack previously'}
|
||||
</Flex>
|
||||
|
||||
<Flex>
|
||||
<Button href={signin('google')}>
|
||||
<Spacer>Sign In with Google</Spacer>
|
||||
</Button>
|
||||
{lastLoggedIn === 'google' && 'You signed in with Google previously'}
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -19,8 +19,6 @@ export default (
|
||||
user: User,
|
||||
options: Options = {}
|
||||
): UserPresentation => {
|
||||
ctx.cache.set(user.id, user);
|
||||
|
||||
const userData = {};
|
||||
userData.id = user.id;
|
||||
userData.username = user.username;
|
||||
|
||||
@@ -7,7 +7,6 @@ import sendfile from 'koa-sendfile';
|
||||
import serve from 'koa-static';
|
||||
import subdomainRedirect from './middlewares/subdomainRedirect';
|
||||
import renderpage from './utils/renderpage';
|
||||
import { slackAuth } from '../shared/utils/routeHelpers';
|
||||
import { robotsResponse } from './utils/robots';
|
||||
import { NotFoundError } from './errors';
|
||||
|
||||
@@ -48,19 +47,6 @@ if (process.env.NODE_ENV === 'production') {
|
||||
});
|
||||
}
|
||||
|
||||
// slack direct install
|
||||
router.get('/auth/slack/install', async ctx => {
|
||||
const state = Math.random()
|
||||
.toString(36)
|
||||
.substring(7);
|
||||
|
||||
ctx.cookies.set('state', state, {
|
||||
httpOnly: false,
|
||||
expires: new Date('2100'),
|
||||
});
|
||||
ctx.redirect(slackAuth(state));
|
||||
});
|
||||
|
||||
// static pages
|
||||
router.get('/about', ctx => renderpage(ctx, <About />));
|
||||
router.get('/pricing', ctx => renderpage(ctx, <Pricing />));
|
||||
@@ -76,10 +62,14 @@ router.get('/changelog', async ctx => {
|
||||
|
||||
// home page
|
||||
router.get('/', async ctx => {
|
||||
if (ctx.cookies.get('loggedIn')) {
|
||||
const lastLoggedIn = ctx.cookies.get('lastLoggedIn');
|
||||
const accessToken = ctx.cookies.get('accessToken');
|
||||
console.log(lastLoggedIn, accessToken);
|
||||
|
||||
if (accessToken) {
|
||||
await renderapp(ctx);
|
||||
} else {
|
||||
await renderpage(ctx, <Home />);
|
||||
await renderpage(ctx, <Home lastLoggedIn={lastLoggedIn} />);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user