WIP: Successful Google Auth, broke pretty much everything else in the process
This commit is contained in:
parent
1ba5c1cf96
commit
ddd2b82d20
|
@ -14,10 +14,13 @@ DEPLOYMENT=self
|
||||||
ENABLE_UPDATES=true
|
ENABLE_UPDATES=true
|
||||||
DEBUG=sql,cache,presenters,events
|
DEBUG=sql,cache,presenters,events
|
||||||
|
|
||||||
# Third party credentials (required)
|
# Slack signin credentials (at least one is required)
|
||||||
SLACK_KEY=71315967491.XXXXXXXXXX
|
SLACK_KEY=71315967491.XXXXXXXXXX
|
||||||
SLACK_SECRET=d2dc414f9953226bad0a356cXXXXYYYY
|
SLACK_SECRET=d2dc414f9953226bad0a356cXXXXYYYY
|
||||||
|
|
||||||
|
GOOGLE_CLIENT_ID=
|
||||||
|
GOOGLE_CLIENT_SECRET=
|
||||||
|
|
||||||
# Third party credentials (optional)
|
# Third party credentials (optional)
|
||||||
SLACK_VERIFICATION_TOKEN=PLxk6OlXXXXXVj3YYYY
|
SLACK_VERIFICATION_TOKEN=PLxk6OlXXXXXVj3YYYY
|
||||||
SLACK_APP_ID=A0XXXXXXX
|
SLACK_APP_ID=A0XXXXXXX
|
||||||
|
|
|
@ -15,7 +15,12 @@ type Props = {
|
||||||
let authenticatedStores;
|
let authenticatedStores;
|
||||||
|
|
||||||
const Auth = ({ children }: Props) => {
|
const Auth = ({ children }: Props) => {
|
||||||
if (stores.auth.authenticated && stores.auth.team && stores.auth.user) {
|
if (stores.auth.authenticated) {
|
||||||
|
if (!stores.auth.team || !stores.auth.user) {
|
||||||
|
stores.auth.fetch();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
// Only initialize stores once. Kept in global scope because otherwise they
|
// Only initialize stores once. Kept in global scope because otherwise they
|
||||||
// will get overridden on route change
|
// will get overridden on route change
|
||||||
if (!authenticatedStores) {
|
if (!authenticatedStores) {
|
||||||
|
@ -42,7 +47,6 @@ const Auth = ({ children }: Props) => {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
stores.auth.fetch();
|
|
||||||
authenticatedStores.collections.fetchPage({ limit: 100 });
|
authenticatedStores.collections.fetchPage({ limit: 100 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -123,7 +123,9 @@ class AuthStore {
|
||||||
}
|
}
|
||||||
this.user = data.user;
|
this.user = data.user;
|
||||||
this.team = data.team;
|
this.team = data.team;
|
||||||
this.token = data.token;
|
this.token = Cookie.get('accessToken') || data.token;
|
||||||
|
console.log('TOKEN', this.token);
|
||||||
|
|
||||||
this.oauthState = data.oauthState;
|
this.oauthState = data.oauthState;
|
||||||
|
|
||||||
autorun(() => {
|
autorun(() => {
|
||||||
|
|
|
@ -99,6 +99,7 @@
|
||||||
"file-loader": "^1.1.6",
|
"file-loader": "^1.1.6",
|
||||||
"flow-typed": "^2.4.0",
|
"flow-typed": "^2.4.0",
|
||||||
"fs-extra": "^4.0.2",
|
"fs-extra": "^4.0.2",
|
||||||
|
"google-auth-library": "^1.5.0",
|
||||||
"history": "3.0.0",
|
"history": "3.0.0",
|
||||||
"html-webpack-plugin": "2.17.0",
|
"html-webpack-plugin": "2.17.0",
|
||||||
"http-errors": "1.4.0",
|
"http-errors": "1.4.0",
|
||||||
|
@ -168,11 +169,11 @@
|
||||||
"styled-components-breakpoint": "^1.0.1",
|
"styled-components-breakpoint": "^1.0.1",
|
||||||
"styled-components-grid": "^1.0.0-preview.15",
|
"styled-components-grid": "^1.0.0-preview.15",
|
||||||
"styled-normalize": "^2.2.1",
|
"styled-normalize": "^2.2.1",
|
||||||
|
"uglifyjs-webpack-plugin": "1.2.5",
|
||||||
"url-loader": "^0.6.2",
|
"url-loader": "^0.6.2",
|
||||||
"uuid": "2.0.2",
|
"uuid": "2.0.2",
|
||||||
"validator": "5.2.0",
|
"validator": "5.2.0",
|
||||||
"webpack": "3.10.0",
|
"webpack": "3.10.0",
|
||||||
"uglifyjs-webpack-plugin": "1.2.5",
|
|
||||||
"webpack-manifest-plugin": "^1.3.2"
|
"webpack-manifest-plugin": "^1.3.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
// @flow
|
// @flow
|
||||||
import Router from 'koa-router';
|
import Router from 'koa-router';
|
||||||
|
|
||||||
import auth from './middlewares/authentication';
|
import auth from '../middlewares/authentication';
|
||||||
import pagination from './middlewares/pagination';
|
import pagination from './middlewares/pagination';
|
||||||
import { presentApiKey } from '../presenters';
|
import { presentApiKey } from '../presenters';
|
||||||
import { ApiKey } from '../models';
|
import { ApiKey } from '../models';
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
// @flow
|
// @flow
|
||||||
import Router from 'koa-router';
|
import Router from 'koa-router';
|
||||||
import auth from './middlewares/authentication';
|
import auth from '../middlewares/authentication';
|
||||||
import { presentUser, presentTeam } from '../presenters';
|
import { presentUser, presentTeam } from '../presenters';
|
||||||
import { Authentication, Integration, User, Team } from '../models';
|
import { Team } from '../models';
|
||||||
import * as Slack from '../slack';
|
|
||||||
|
|
||||||
const router = new Router();
|
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;
|
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
|
// @flow
|
||||||
import Router from 'koa-router';
|
import Router from 'koa-router';
|
||||||
|
|
||||||
import auth from './middlewares/authentication';
|
import auth from '../middlewares/authentication';
|
||||||
import pagination from './middlewares/pagination';
|
import pagination from './middlewares/pagination';
|
||||||
import { presentCollection } from '../presenters';
|
import { presentCollection } from '../presenters';
|
||||||
import { Collection } from '../models';
|
import { Collection } from '../models';
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
// @flow
|
// @flow
|
||||||
import Router from 'koa-router';
|
import Router from 'koa-router';
|
||||||
import Sequelize from 'sequelize';
|
import Sequelize from 'sequelize';
|
||||||
import auth from './middlewares/authentication';
|
import auth from '../middlewares/authentication';
|
||||||
import pagination from './middlewares/pagination';
|
import pagination from './middlewares/pagination';
|
||||||
import { presentDocument, presentRevision } from '../presenters';
|
import { presentDocument, presentRevision } from '../presenters';
|
||||||
import { Document, Collection, Share, Star, View, Revision } from '../models';
|
import { Document, Collection, Share, Star, View, Revision } from '../models';
|
||||||
|
|
|
@ -2,8 +2,6 @@
|
||||||
import bodyParser from 'koa-bodyparser';
|
import bodyParser from 'koa-bodyparser';
|
||||||
import Koa from 'koa';
|
import Koa from 'koa';
|
||||||
import Router from 'koa-router';
|
import Router from 'koa-router';
|
||||||
import Sequelize from 'sequelize';
|
|
||||||
import _ from 'lodash';
|
|
||||||
|
|
||||||
import auth from './auth';
|
import auth from './auth';
|
||||||
import user from './user';
|
import user from './user';
|
||||||
|
@ -16,58 +14,24 @@ import shares from './shares';
|
||||||
import team from './team';
|
import team from './team';
|
||||||
import integrations from './integrations';
|
import integrations from './integrations';
|
||||||
|
|
||||||
import validation from './middlewares/validation';
|
import errorHandling from './middlewares/errorHandling';
|
||||||
import methodOverride from '../middlewares/methodOverride';
|
import validation from '../middlewares/validation';
|
||||||
import cache from '../middlewares/cache';
|
import methodOverride from './middlewares/methodOverride';
|
||||||
|
import cache from './middlewares/cache';
|
||||||
import apiWrapper from './middlewares/apiWrapper';
|
import apiWrapper from './middlewares/apiWrapper';
|
||||||
|
|
||||||
const api = new Koa();
|
const api = new Koa();
|
||||||
const router = new Router();
|
const router = new Router();
|
||||||
|
|
||||||
// API error handler
|
// middlewares
|
||||||
api.use(async (ctx, next) => {
|
api.use(errorHandling());
|
||||||
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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
api.use(bodyParser());
|
api.use(bodyParser());
|
||||||
api.use(methodOverride());
|
api.use(methodOverride());
|
||||||
api.use(cache());
|
api.use(cache());
|
||||||
api.use(validation());
|
api.use(validation());
|
||||||
api.use(apiWrapper());
|
api.use(apiWrapper());
|
||||||
|
|
||||||
|
// routes
|
||||||
router.use('/', auth.routes());
|
router.use('/', auth.routes());
|
||||||
router.use('/', user.routes());
|
router.use('/', user.routes());
|
||||||
router.use('/', collections.routes());
|
router.use('/', collections.routes());
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
import Router from 'koa-router';
|
import Router from 'koa-router';
|
||||||
import Integration from '../models/Integration';
|
import Integration from '../models/Integration';
|
||||||
import pagination from './middlewares/pagination';
|
import pagination from './middlewares/pagination';
|
||||||
import auth from './middlewares/authentication';
|
import auth from '../middlewares/authentication';
|
||||||
import { presentIntegration } from '../presenters';
|
import { presentIntegration } from '../presenters';
|
||||||
import policy from '../policies';
|
import policy from '../policies';
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { type Context } from 'koa';
|
||||||
export default function apiWrapper() {
|
export default function apiWrapper() {
|
||||||
return async function apiWrapperMiddleware(
|
return async function apiWrapperMiddleware(
|
||||||
ctx: Context,
|
ctx: Context,
|
||||||
next: () => Promise<void>
|
next: () => Promise<*>
|
||||||
) {
|
) {
|
||||||
await next();
|
await next();
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
// @flow
|
// @flow
|
||||||
import debug from 'debug';
|
import debug from 'debug';
|
||||||
|
import { type Context } from 'koa';
|
||||||
|
|
||||||
const debugCache = debug('cache');
|
const debugCache = debug('cache');
|
||||||
|
|
||||||
export default function 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 = {};
|
||||||
|
|
||||||
ctx.cache.set = async (id, value) => {
|
ctx.cache.set = async (id, value) => {
|
|
@ -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() {
|
export default function methodOverride() {
|
||||||
return async function methodOverrideMiddleware(
|
return async function methodOverrideMiddleware(
|
||||||
ctx: Context,
|
ctx: Context,
|
||||||
next: () => Promise<void>
|
next: () => Promise<*>
|
||||||
) {
|
) {
|
||||||
if (ctx.method === 'POST') {
|
if (ctx.method === 'POST') {
|
||||||
// $FlowFixMe
|
// $FlowFixMe
|
|
@ -6,7 +6,7 @@ import { type Context } from 'koa';
|
||||||
export default function pagination(options?: Object) {
|
export default function pagination(options?: Object) {
|
||||||
return async function paginationMiddleware(
|
return async function paginationMiddleware(
|
||||||
ctx: Context,
|
ctx: Context,
|
||||||
next: () => Promise<void>
|
next: () => Promise<*>
|
||||||
) {
|
) {
|
||||||
const opts = {
|
const opts = {
|
||||||
defaultLimit: 15,
|
defaultLimit: 15,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
// @flow
|
// @flow
|
||||||
import Router from 'koa-router';
|
import Router from 'koa-router';
|
||||||
import auth from './middlewares/authentication';
|
import auth from '../middlewares/authentication';
|
||||||
import pagination from './middlewares/pagination';
|
import pagination from './middlewares/pagination';
|
||||||
import { presentShare } from '../presenters';
|
import { presentShare } from '../presenters';
|
||||||
import { Document, User, Share } from '../models';
|
import { Document, User, Share } from '../models';
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
import Router from 'koa-router';
|
import Router from 'koa-router';
|
||||||
import { User } from '../models';
|
import { User } from '../models';
|
||||||
|
|
||||||
import auth from './middlewares/authentication';
|
import auth from '../middlewares/authentication';
|
||||||
import pagination from './middlewares/pagination';
|
import pagination from './middlewares/pagination';
|
||||||
import { presentUser } from '../presenters';
|
import { presentUser } from '../presenters';
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ import Router from 'koa-router';
|
||||||
import { makePolicy, signPolicy, publicS3Endpoint } from '../utils/s3';
|
import { makePolicy, signPolicy, publicS3Endpoint } from '../utils/s3';
|
||||||
import { ValidationError } from '../errors';
|
import { ValidationError } from '../errors';
|
||||||
import { Event, User, Team } from '../models';
|
import { Event, User, Team } from '../models';
|
||||||
import auth from './middlewares/authentication';
|
import auth from '../middlewares/authentication';
|
||||||
import { presentUser } from '../presenters';
|
import { presentUser } from '../presenters';
|
||||||
import policy from '../policies';
|
import policy from '../policies';
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
// @flow
|
// @flow
|
||||||
import Router from 'koa-router';
|
import Router from 'koa-router';
|
||||||
import auth from './middlewares/authentication';
|
import auth from '../middlewares/authentication';
|
||||||
import { presentView } from '../presenters';
|
import { presentView } from '../presenters';
|
||||||
import { View, Document } from '../models';
|
import { View, Document } from '../models';
|
||||||
import policy from '../policies';
|
import policy from '../policies';
|
||||||
|
|
|
@ -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;
|
|
@ -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;
|
|
@ -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 onerror from 'koa-onerror';
|
||||||
import updates from './utils/updates';
|
import updates from './utils/updates';
|
||||||
|
|
||||||
|
import auth from './auth';
|
||||||
import api from './api';
|
import api from './api';
|
||||||
import emails from './emails';
|
import emails from './emails';
|
||||||
import routes from './routes';
|
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('/api', api));
|
||||||
app.use(mount(routes));
|
app.use(mount(routes));
|
||||||
|
|
||||||
|
|
|
@ -1,14 +1,11 @@
|
||||||
// @flow
|
// @flow
|
||||||
import JWT from 'jsonwebtoken';
|
import JWT from 'jsonwebtoken';
|
||||||
import { type Context } from 'koa';
|
import { type Context } from 'koa';
|
||||||
import { User, ApiKey } from '../../models';
|
import { User, ApiKey } from '../models';
|
||||||
import { AuthenticationError, UserSuspendedError } from '../../errors';
|
import { AuthenticationError, UserSuspendedError } from '../errors';
|
||||||
|
|
||||||
export default function auth(options?: { required?: boolean } = {}) {
|
export default function auth(options?: { required?: boolean } = {}) {
|
||||||
return async function authMiddleware(
|
return async function authMiddleware(ctx: Context, next: () => Promise<*>) {
|
||||||
ctx: Context,
|
|
||||||
next: () => Promise<void>
|
|
||||||
) {
|
|
||||||
let token;
|
let token;
|
||||||
|
|
||||||
const authorizationHeader = ctx.request.get('authorization');
|
const authorizationHeader = ctx.request.get('authorization');
|
|
@ -1,10 +1,11 @@
|
||||||
// @flow
|
// @flow
|
||||||
import validator from 'validator';
|
import validator from 'validator';
|
||||||
import { ParamRequiredError, ValidationError } from '../../errors';
|
import { type Context } from 'koa';
|
||||||
import { validateColorHex } from '../../../shared/utils/color';
|
import { ParamRequiredError, ValidationError } from '../errors';
|
||||||
|
import { validateColorHex } from '../../shared/utils/color';
|
||||||
|
|
||||||
export default function validation() {
|
export default function validation() {
|
||||||
return function validationMiddleware(ctx: Object, next: Function) {
|
return function validationMiddleware(ctx: Context, next: () => Promise<*>) {
|
||||||
ctx.assertPresent = (value, message) => {
|
ctx.assertPresent = (value, message) => {
|
||||||
if (value === undefined || value === null || value === '') {
|
if (value === undefined || value === null || value === '') {
|
||||||
throw new ParamRequiredError(message);
|
throw new ParamRequiredError(message);
|
|
@ -9,7 +9,11 @@ import SignupButton from './components/SignupButton';
|
||||||
import { developers, githubUrl } from '../../shared/utils/routeHelpers';
|
import { developers, githubUrl } from '../../shared/utils/routeHelpers';
|
||||||
import { color } from '../../shared/styles/constants';
|
import { color } from '../../shared/styles/constants';
|
||||||
|
|
||||||
function Home() {
|
type Props = {
|
||||||
|
lastLoggedIn: string,
|
||||||
|
};
|
||||||
|
|
||||||
|
function Home({ lastLoggedIn }: Props) {
|
||||||
return (
|
return (
|
||||||
<span>
|
<span>
|
||||||
<Helmet>
|
<Helmet>
|
||||||
|
@ -23,7 +27,7 @@ function Home() {
|
||||||
logs, brainstorming, & more…
|
logs, brainstorming, & more…
|
||||||
</HeroText>
|
</HeroText>
|
||||||
<p>
|
<p>
|
||||||
<SignupButton />
|
<SignupButton lastLoggedIn={lastLoggedIn} />
|
||||||
</p>
|
</p>
|
||||||
</Hero>
|
</Hero>
|
||||||
<Features reverse={{ mobile: true, tablet: false, desktop: false }}>
|
<Features reverse={{ mobile: true, tablet: false, desktop: false }}>
|
||||||
|
|
|
@ -2,15 +2,32 @@
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { signin } from '../../../shared/utils/routeHelpers';
|
import { signin } from '../../../shared/utils/routeHelpers';
|
||||||
|
import Flex from '../../../shared/components/Flex';
|
||||||
import SlackLogo from '../../../shared/components/SlackLogo';
|
import SlackLogo from '../../../shared/components/SlackLogo';
|
||||||
import { color } from '../../../shared/styles/constants';
|
import { color } from '../../../shared/styles/constants';
|
||||||
|
|
||||||
const SlackSignin = () => {
|
type Props = {
|
||||||
|
lastLoggedIn: string,
|
||||||
|
};
|
||||||
|
|
||||||
|
const SlackSignin = ({ lastLoggedIn }: Props) => {
|
||||||
return (
|
return (
|
||||||
<Button href={signin()}>
|
<Flex justify="center">
|
||||||
<SlackLogo />
|
<Flex>
|
||||||
<Spacer>Sign In with Slack</Spacer>
|
<Button href={signin('slack')}>
|
||||||
</Button>
|
<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,
|
user: User,
|
||||||
options: Options = {}
|
options: Options = {}
|
||||||
): UserPresentation => {
|
): UserPresentation => {
|
||||||
ctx.cache.set(user.id, user);
|
|
||||||
|
|
||||||
const userData = {};
|
const userData = {};
|
||||||
userData.id = user.id;
|
userData.id = user.id;
|
||||||
userData.username = user.username;
|
userData.username = user.username;
|
||||||
|
|
|
@ -7,7 +7,6 @@ import sendfile from 'koa-sendfile';
|
||||||
import serve from 'koa-static';
|
import serve from 'koa-static';
|
||||||
import subdomainRedirect from './middlewares/subdomainRedirect';
|
import subdomainRedirect from './middlewares/subdomainRedirect';
|
||||||
import renderpage from './utils/renderpage';
|
import renderpage from './utils/renderpage';
|
||||||
import { slackAuth } from '../shared/utils/routeHelpers';
|
|
||||||
import { robotsResponse } from './utils/robots';
|
import { robotsResponse } from './utils/robots';
|
||||||
import { NotFoundError } from './errors';
|
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
|
// static pages
|
||||||
router.get('/about', ctx => renderpage(ctx, <About />));
|
router.get('/about', ctx => renderpage(ctx, <About />));
|
||||||
router.get('/pricing', ctx => renderpage(ctx, <Pricing />));
|
router.get('/pricing', ctx => renderpage(ctx, <Pricing />));
|
||||||
|
@ -76,10 +62,14 @@ router.get('/changelog', async ctx => {
|
||||||
|
|
||||||
// home page
|
// home page
|
||||||
router.get('/', async ctx => {
|
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);
|
await renderapp(ctx);
|
||||||
} else {
|
} else {
|
||||||
await renderpage(ctx, <Home />);
|
await renderpage(ctx, <Home lastLoggedIn={lastLoggedIn} />);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -57,8 +57,8 @@ export function changelog(): string {
|
||||||
return '/changelog';
|
return '/changelog';
|
||||||
}
|
}
|
||||||
|
|
||||||
export function signin(): string {
|
export function signin(service: string = 'slack'): string {
|
||||||
return '/auth/slack';
|
return `/auth/${service}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function about(): string {
|
export function about(): string {
|
||||||
|
|
66
yarn.lock
66
yarn.lock
|
@ -490,6 +490,13 @@ aws4@^1.2.1, aws4@^1.6.0:
|
||||||
version "1.6.0"
|
version "1.6.0"
|
||||||
resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.6.0.tgz#83ef5ca860b2b32e4a0deedee8c771b9db57471e"
|
resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.6.0.tgz#83ef5ca860b2b32e4a0deedee8c771b9db57471e"
|
||||||
|
|
||||||
|
axios@^0.18.0:
|
||||||
|
version "0.18.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/axios/-/axios-0.18.0.tgz#32d53e4851efdc0a11993b6cd000789d70c05102"
|
||||||
|
dependencies:
|
||||||
|
follow-redirects "^1.3.0"
|
||||||
|
is-buffer "^1.1.5"
|
||||||
|
|
||||||
axobject-query@^0.1.0:
|
axobject-query@^0.1.0:
|
||||||
version "0.1.0"
|
version "0.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-0.1.0.tgz#62f59dbc59c9f9242759ca349960e7a2fe3c36c0"
|
resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-0.1.0.tgz#62f59dbc59c9f9242759ca349960e7a2fe3c36c0"
|
||||||
|
@ -3441,7 +3448,7 @@ express-session@~1.11.3:
|
||||||
uid-safe "~2.0.0"
|
uid-safe "~2.0.0"
|
||||||
utils-merge "1.0.0"
|
utils-merge "1.0.0"
|
||||||
|
|
||||||
extend@^3.0.0, extend@~3.0.0, extend@~3.0.1:
|
extend@^3.0.0, extend@^3.0.1, extend@~3.0.0, extend@~3.0.1:
|
||||||
version "3.0.1"
|
version "3.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444"
|
resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444"
|
||||||
|
|
||||||
|
@ -3739,6 +3746,12 @@ flush-write-stream@^1.0.0:
|
||||||
inherits "^2.0.1"
|
inherits "^2.0.1"
|
||||||
readable-stream "^2.0.4"
|
readable-stream "^2.0.4"
|
||||||
|
|
||||||
|
follow-redirects@^1.3.0:
|
||||||
|
version "1.5.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.5.0.tgz#234f49cf770b7f35b40e790f636ceba0c3a0ab77"
|
||||||
|
dependencies:
|
||||||
|
debug "^3.1.0"
|
||||||
|
|
||||||
for-in@^1.0.1:
|
for-in@^1.0.1:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80"
|
resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80"
|
||||||
|
@ -3927,6 +3940,14 @@ gaze@^0.5.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
globule "~0.1.0"
|
globule "~0.1.0"
|
||||||
|
|
||||||
|
gcp-metadata@^0.6.3:
|
||||||
|
version "0.6.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/gcp-metadata/-/gcp-metadata-0.6.3.tgz#4550c08859c528b370459bd77a7187ea0bdbc4ab"
|
||||||
|
dependencies:
|
||||||
|
axios "^0.18.0"
|
||||||
|
extend "^3.0.1"
|
||||||
|
retry-axios "0.3.2"
|
||||||
|
|
||||||
generic-pool@2.4.3:
|
generic-pool@2.4.3:
|
||||||
version "2.4.3"
|
version "2.4.3"
|
||||||
resolved "https://registry.yarnpkg.com/generic-pool/-/generic-pool-2.4.3.tgz#780c36f69dfad05a5a045dd37be7adca11a4f6ff"
|
resolved "https://registry.yarnpkg.com/generic-pool/-/generic-pool-2.4.3.tgz#780c36f69dfad05a5a045dd37be7adca11a4f6ff"
|
||||||
|
@ -4099,6 +4120,18 @@ good-listener@^1.2.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
delegate "^3.1.2"
|
delegate "^3.1.2"
|
||||||
|
|
||||||
|
google-auth-library@^1.5.0:
|
||||||
|
version "1.5.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/google-auth-library/-/google-auth-library-1.5.0.tgz#d9068f8bad9017224a4c41abcdcb6cf6a704e83b"
|
||||||
|
dependencies:
|
||||||
|
axios "^0.18.0"
|
||||||
|
gcp-metadata "^0.6.3"
|
||||||
|
gtoken "^2.3.0"
|
||||||
|
jws "^3.1.4"
|
||||||
|
lodash.isstring "^4.0.1"
|
||||||
|
lru-cache "^4.1.2"
|
||||||
|
retry-axios "^0.3.2"
|
||||||
|
|
||||||
google-closure-compiler-js@^20170423.0.0:
|
google-closure-compiler-js@^20170423.0.0:
|
||||||
version "20170423.0.0"
|
version "20170423.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/google-closure-compiler-js/-/google-closure-compiler-js-20170423.0.0.tgz#e9e8b40dadfdf0e64044c9479b5d26d228778fbc"
|
resolved "https://registry.yarnpkg.com/google-closure-compiler-js/-/google-closure-compiler-js-20170423.0.0.tgz#e9e8b40dadfdf0e64044c9479b5d26d228778fbc"
|
||||||
|
@ -4107,6 +4140,13 @@ google-closure-compiler-js@^20170423.0.0:
|
||||||
vinyl "^2.0.1"
|
vinyl "^2.0.1"
|
||||||
webpack-core "^0.6.8"
|
webpack-core "^0.6.8"
|
||||||
|
|
||||||
|
google-p12-pem@^1.0.0:
|
||||||
|
version "1.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/google-p12-pem/-/google-p12-pem-1.0.2.tgz#c8a3843504012283a0dbffc7430b7c753ecd4b07"
|
||||||
|
dependencies:
|
||||||
|
node-forge "^0.7.4"
|
||||||
|
pify "^3.0.0"
|
||||||
|
|
||||||
got@^3.2.0:
|
got@^3.2.0:
|
||||||
version "3.3.1"
|
version "3.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/got/-/got-3.3.1.tgz#e5d0ed4af55fc3eef4d56007769d98192bcb2eca"
|
resolved "https://registry.yarnpkg.com/got/-/got-3.3.1.tgz#e5d0ed4af55fc3eef4d56007769d98192bcb2eca"
|
||||||
|
@ -4163,6 +4203,16 @@ growly@^1.2.0, growly@^1.3.0:
|
||||||
version "1.3.0"
|
version "1.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081"
|
resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081"
|
||||||
|
|
||||||
|
gtoken@^2.3.0:
|
||||||
|
version "2.3.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/gtoken/-/gtoken-2.3.0.tgz#4e0ffc16432d7041a1b3dbc1d97aac17a5dc964a"
|
||||||
|
dependencies:
|
||||||
|
axios "^0.18.0"
|
||||||
|
google-p12-pem "^1.0.0"
|
||||||
|
jws "^3.1.4"
|
||||||
|
mime "^2.2.0"
|
||||||
|
pify "^3.0.0"
|
||||||
|
|
||||||
gulp-help@~1.6.1:
|
gulp-help@~1.6.1:
|
||||||
version "1.6.1"
|
version "1.6.1"
|
||||||
resolved "https://registry.yarnpkg.com/gulp-help/-/gulp-help-1.6.1.tgz#261db186e18397fef3f6a2c22e9c315bfa88ae0c"
|
resolved "https://registry.yarnpkg.com/gulp-help/-/gulp-help-1.6.1.tgz#261db186e18397fef3f6a2c22e9c315bfa88ae0c"
|
||||||
|
@ -6406,7 +6456,7 @@ lru-cache@^4.0.1:
|
||||||
pseudomap "^1.0.2"
|
pseudomap "^1.0.2"
|
||||||
yallist "^2.1.2"
|
yallist "^2.1.2"
|
||||||
|
|
||||||
lru-cache@^4.1.1:
|
lru-cache@^4.1.1, lru-cache@^4.1.2:
|
||||||
version "4.1.3"
|
version "4.1.3"
|
||||||
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.3.tgz#a1175cf3496dfc8436c156c334b4955992bce69c"
|
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.3.tgz#a1175cf3496dfc8436c156c334b4955992bce69c"
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -6603,6 +6653,10 @@ mime@^1.4.1:
|
||||||
version "1.6.0"
|
version "1.6.0"
|
||||||
resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
|
resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
|
||||||
|
|
||||||
|
mime@^2.2.0:
|
||||||
|
version "2.3.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/mime/-/mime-2.3.1.tgz#b1621c54d63b97c47d3cfe7f7215f7d64517c369"
|
||||||
|
|
||||||
mimic-fn@^1.0.0:
|
mimic-fn@^1.0.0:
|
||||||
version "1.1.0"
|
version "1.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.1.0.tgz#e667783d92e89dbd342818b5230b9d62a672ad18"
|
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.1.0.tgz#e667783d92e89dbd342818b5230b9d62a672ad18"
|
||||||
|
@ -6855,6 +6909,10 @@ node-fetch@^1.0.1, node-fetch@^1.5.1:
|
||||||
encoding "^0.1.11"
|
encoding "^0.1.11"
|
||||||
is-stream "^1.0.1"
|
is-stream "^1.0.1"
|
||||||
|
|
||||||
|
node-forge@^0.7.4:
|
||||||
|
version "0.7.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.7.5.tgz#6c152c345ce11c52f465c2abd957e8639cd674df"
|
||||||
|
|
||||||
node-int64@^0.4.0:
|
node-int64@^0.4.0:
|
||||||
version "0.4.0"
|
version "0.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b"
|
resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b"
|
||||||
|
@ -8762,6 +8820,10 @@ retry-as-promised@^2.3.1:
|
||||||
bluebird "^3.4.6"
|
bluebird "^3.4.6"
|
||||||
debug "^2.6.9"
|
debug "^2.6.9"
|
||||||
|
|
||||||
|
retry-axios@0.3.2, retry-axios@^0.3.2:
|
||||||
|
version "0.3.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/retry-axios/-/retry-axios-0.3.2.tgz#5757c80f585b4cc4c4986aa2ffd47a60c6d35e13"
|
||||||
|
|
||||||
rich-markdown-editor@1.1.2:
|
rich-markdown-editor@1.1.2:
|
||||||
version "1.1.2"
|
version "1.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/rich-markdown-editor/-/rich-markdown-editor-1.1.2.tgz#c44f14425b5b5f0da3adce8bf389ed6e20b705a4"
|
resolved "https://registry.yarnpkg.com/rich-markdown-editor/-/rich-markdown-editor-1.1.2.tgz#c44f14425b5b5f0da3adce8bf389ed6e20b705a4"
|
||||||
|
|
Reference in New Issue