Post to Slack (#603)
* Migrations
* WIP: Integration model, slack perms / hooks
* So so rough it pains me. Building this new model is revealing just how much needs to be refactored
* Working connect and post
* Cleanup UI, upating documents
* Show when slack command is connected
* stash
* 💚
* Add documents.update trigger
* Authorization, tidying
* Fixed integration policy
* pick integration presenter keys
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
import Router from 'koa-router';
|
||||
import auth from './middlewares/authentication';
|
||||
import { presentUser, presentTeam } from '../presenters';
|
||||
import { Authentication, User, Team } from '../models';
|
||||
import { Authentication, Integration, User, Team } from '../models';
|
||||
import * as Slack from '../slack';
|
||||
|
||||
const router = new Router();
|
||||
@@ -89,14 +89,56 @@ router.post('auth.slackCommands', auth(), async ctx => {
|
||||
const user = ctx.state.user;
|
||||
const endpoint = `${process.env.URL || ''}/auth/slack/commands`;
|
||||
const data = await Slack.oauthAccess(code, endpoint);
|
||||
const serviceId = 'slack';
|
||||
|
||||
await Authentication.create({
|
||||
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;
|
||||
|
||||
@@ -5,7 +5,8 @@ import auth from './middlewares/authentication';
|
||||
import pagination from './middlewares/pagination';
|
||||
import { presentDocument, presentRevision } from '../presenters';
|
||||
import { Document, Collection, Star, View, Revision } from '../models';
|
||||
import { ValidationError, InvalidRequestError } from '../errors';
|
||||
import { InvalidRequestError } from '../errors';
|
||||
import events from '../events';
|
||||
import policy from '../policies';
|
||||
|
||||
const { authorize } = policy;
|
||||
@@ -302,7 +303,6 @@ router.post('documents.create', auth(), async ctx => {
|
||||
authorize(user, 'read', parentDocumentObj);
|
||||
}
|
||||
|
||||
const publishedAt = publish === false ? null : new Date();
|
||||
let document = await Document.create({
|
||||
parentDocumentId: parentDocumentObj.id,
|
||||
atlasId: collection.id,
|
||||
@@ -310,20 +310,19 @@ router.post('documents.create', auth(), async ctx => {
|
||||
userId: user.id,
|
||||
lastModifiedById: user.id,
|
||||
createdById: user.id,
|
||||
publishedAt,
|
||||
title,
|
||||
text,
|
||||
});
|
||||
|
||||
if (publishedAt && collection.type === 'atlas') {
|
||||
await collection.addDocumentToStructure(document, index);
|
||||
if (publish) {
|
||||
await document.publish();
|
||||
}
|
||||
|
||||
// reload to get all of the data needed to present (user, collection etc)
|
||||
// we need to specify publishedAt to bypass default scope that only returns
|
||||
// published documents
|
||||
document = await Document.find({
|
||||
where: { id: document.id, publishedAt },
|
||||
where: { id: document.id, publishedAt: document.publishedAt },
|
||||
});
|
||||
|
||||
ctx.body = {
|
||||
@@ -332,7 +331,7 @@ router.post('documents.create', auth(), async ctx => {
|
||||
});
|
||||
|
||||
router.post('documents.update', auth(), async ctx => {
|
||||
const { id, title, text, publish, lastRevision } = ctx.body;
|
||||
const { id, title, text, publish, done, lastRevision } = ctx.body;
|
||||
ctx.assertPresent(id, 'id is required');
|
||||
ctx.assertPresent(title || text, 'title or text is required');
|
||||
|
||||
@@ -346,24 +345,20 @@ router.post('documents.update', auth(), async ctx => {
|
||||
}
|
||||
|
||||
// Update document
|
||||
const previouslyPublished = !!document.publishedAt;
|
||||
if (publish) document.publishedAt = new Date();
|
||||
if (title) document.title = title;
|
||||
if (text) document.text = text;
|
||||
document.lastModifiedById = user.id;
|
||||
|
||||
await document.save();
|
||||
const collection = document.collection;
|
||||
if (collection.type === 'atlas') {
|
||||
if (previouslyPublished) {
|
||||
await collection.updateDocument(document);
|
||||
} else if (publish) {
|
||||
await collection.addDocumentToStructure(document);
|
||||
if (publish) {
|
||||
await document.publish();
|
||||
} else {
|
||||
await document.save();
|
||||
|
||||
if (document.publishedAt && done) {
|
||||
events.add({ name: 'documents.update', model: document });
|
||||
}
|
||||
}
|
||||
|
||||
document.collection = collection;
|
||||
|
||||
ctx.body = {
|
||||
data: await presentDocument(ctx, document),
|
||||
};
|
||||
|
||||
@@ -385,12 +385,7 @@ describe('#documents.create', async () => {
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
const newDocument = await Document.findOne({
|
||||
where: {
|
||||
id: body.data.id,
|
||||
},
|
||||
});
|
||||
|
||||
const newDocument = await Document.findById(body.data.id);
|
||||
expect(res.status).toEqual(200);
|
||||
expect(newDocument.parentDocumentId).toBe(null);
|
||||
expect(newDocument.collection.id).toBe(collection.id);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import Router from 'koa-router';
|
||||
import { AuthenticationError, InvalidRequestError } from '../errors';
|
||||
import { Authentication, Document, User } from '../models';
|
||||
import { presentSlackAttachment } from '../presenters';
|
||||
import * as Slack from '../slack';
|
||||
const router = new Router();
|
||||
|
||||
@@ -67,14 +68,7 @@ router.post('hooks.slack', async ctx => {
|
||||
if (documents.length) {
|
||||
const attachments = [];
|
||||
for (const document of documents) {
|
||||
attachments.push({
|
||||
color: document.collection.color,
|
||||
title: document.title,
|
||||
title_link: `${process.env.URL}${document.getUrl()}`,
|
||||
footer: document.collection.name,
|
||||
text: document.getSummary(),
|
||||
ts: document.getTimestamp(),
|
||||
});
|
||||
attachments.push(presentSlackAttachment(document));
|
||||
}
|
||||
|
||||
ctx.body = {
|
||||
|
||||
@@ -13,6 +13,7 @@ import views from './views';
|
||||
import hooks from './hooks';
|
||||
import apiKeys from './apiKeys';
|
||||
import team from './team';
|
||||
import integrations from './integrations';
|
||||
|
||||
import validation from './middlewares/validation';
|
||||
import methodOverride from '../middlewares/methodOverride';
|
||||
@@ -74,6 +75,7 @@ router.use('/', views.routes());
|
||||
router.use('/', hooks.routes());
|
||||
router.use('/', apiKeys.routes());
|
||||
router.use('/', team.routes());
|
||||
router.use('/', integrations.routes());
|
||||
|
||||
// Router is embedded in a Koa application wrapper, because koa-router does not
|
||||
// allow middleware to catch any routes which were not explicitly defined.
|
||||
|
||||
48
server/api/integrations.js
Normal file
48
server/api/integrations.js
Normal file
@@ -0,0 +1,48 @@
|
||||
// @flow
|
||||
import Router from 'koa-router';
|
||||
import Integration from '../models/Integration';
|
||||
import pagination from './middlewares/pagination';
|
||||
import auth from './middlewares/authentication';
|
||||
import { presentIntegration } from '../presenters';
|
||||
import policy from '../policies';
|
||||
|
||||
const { authorize } = policy;
|
||||
const router = new Router();
|
||||
|
||||
router.post('integrations.list', auth(), pagination(), async ctx => {
|
||||
let { sort = 'updatedAt', direction } = ctx.body;
|
||||
if (direction !== 'ASC') direction = 'DESC';
|
||||
|
||||
const user = ctx.state.user;
|
||||
const integrations = await Integration.findAll({
|
||||
where: { teamId: user.teamId },
|
||||
order: [[sort, direction]],
|
||||
offset: ctx.state.pagination.offset,
|
||||
limit: ctx.state.pagination.limit,
|
||||
});
|
||||
|
||||
const data = await Promise.all(
|
||||
integrations.map(integration => presentIntegration(ctx, integration))
|
||||
);
|
||||
|
||||
ctx.body = {
|
||||
pagination: ctx.state.pagination,
|
||||
data,
|
||||
};
|
||||
});
|
||||
|
||||
router.post('integrations.delete', auth(), async ctx => {
|
||||
const { id } = ctx.body;
|
||||
ctx.assertPresent(id, 'id is required');
|
||||
|
||||
const integration = await Integration.findById(id);
|
||||
authorize(ctx.state.user, 'delete', integration);
|
||||
|
||||
await integration.destroy();
|
||||
|
||||
ctx.body = {
|
||||
success: true,
|
||||
};
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -1,22 +1,21 @@
|
||||
// @flow
|
||||
import Queue from 'bull';
|
||||
import debug from 'debug';
|
||||
import services from '../services';
|
||||
import services from './services';
|
||||
import { Collection, Document } from './models';
|
||||
|
||||
type DocumentEvent = {
|
||||
name: 'documents.create',
|
||||
name: 'documents.create' | 'documents.update' | 'documents.publish',
|
||||
model: Document,
|
||||
};
|
||||
|
||||
type CollectionEvent = {
|
||||
name: 'collections.create',
|
||||
name: 'collections.create' | 'collections.update',
|
||||
model: Collection,
|
||||
};
|
||||
|
||||
export type Event = DocumentEvent | CollectionEvent;
|
||||
|
||||
const log = debug('events');
|
||||
const globalEventsQueue = new Queue('global events', process.env.REDIS_URL);
|
||||
const serviceEventsQueue = new Queue('service events', process.env.REDIS_URL);
|
||||
|
||||
@@ -37,7 +36,6 @@ serviceEventsQueue.process(async function(job) {
|
||||
const service = services[event.service];
|
||||
|
||||
if (service.on) {
|
||||
log(`Triggering ${event.name} for ${service.name}`);
|
||||
service.on(event);
|
||||
}
|
||||
});
|
||||
|
||||
67
server/migrations/20180212033504-add-integrations.js
Normal file
67
server/migrations/20180212033504-add-integrations.js
Normal file
@@ -0,0 +1,67 @@
|
||||
module.exports = {
|
||||
up: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.createTable('integrations', {
|
||||
id: {
|
||||
type: Sequelize.UUID,
|
||||
allowNull: false,
|
||||
primaryKey: true,
|
||||
},
|
||||
type: {
|
||||
type: Sequelize.STRING,
|
||||
allowNull: true,
|
||||
},
|
||||
userId: {
|
||||
type: Sequelize.UUID,
|
||||
allowNull: true,
|
||||
references: {
|
||||
model: 'users',
|
||||
},
|
||||
},
|
||||
teamId: {
|
||||
type: Sequelize.UUID,
|
||||
allowNull: true,
|
||||
references: {
|
||||
model: 'teams',
|
||||
},
|
||||
},
|
||||
serviceId: {
|
||||
type: Sequelize.STRING,
|
||||
allowNull: false,
|
||||
},
|
||||
collectionId: {
|
||||
type: Sequelize.UUID,
|
||||
allowNull: true,
|
||||
references: {
|
||||
model: 'collections',
|
||||
},
|
||||
},
|
||||
authenticationId: {
|
||||
type: Sequelize.UUID,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'authentications',
|
||||
},
|
||||
},
|
||||
events: {
|
||||
type: Sequelize.ARRAY(Sequelize.STRING),
|
||||
allowNull: true,
|
||||
},
|
||||
settings: {
|
||||
type: Sequelize.JSONB,
|
||||
allowNull: true,
|
||||
},
|
||||
createdAt: {
|
||||
type: Sequelize.DATE,
|
||||
allowNull: false,
|
||||
},
|
||||
updatedAt: {
|
||||
type: Sequelize.DATE,
|
||||
allowNull: false,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
down: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.dropTable('integrations');
|
||||
},
|
||||
};
|
||||
@@ -1,6 +1,7 @@
|
||||
/* eslint-disable flowtype/require-valid-file-annotation */
|
||||
import { flushdb, seed } from '../test/support';
|
||||
import { Collection, Document } from '../models';
|
||||
import uuid from 'uuid';
|
||||
|
||||
beforeEach(flushdb);
|
||||
beforeEach(jest.resetAllMocks);
|
||||
@@ -15,34 +16,37 @@ describe('#getUrl', () => {
|
||||
describe('#addDocumentToStructure', async () => {
|
||||
test('should add as last element without index', async () => {
|
||||
const { collection } = await seed();
|
||||
const id = uuid.v4();
|
||||
const newDocument = new Document({
|
||||
id: '5',
|
||||
id,
|
||||
title: 'New end node',
|
||||
parentDocumentId: null,
|
||||
});
|
||||
|
||||
await collection.addDocumentToStructure(newDocument);
|
||||
expect(collection.documentStructure.length).toBe(3);
|
||||
expect(collection.documentStructure[2].id).toBe('5');
|
||||
expect(collection.documentStructure[2].id).toBe(id);
|
||||
});
|
||||
|
||||
test('should add with an index', async () => {
|
||||
const { collection } = await seed();
|
||||
const id = uuid.v4();
|
||||
const newDocument = new Document({
|
||||
id: '5',
|
||||
id,
|
||||
title: 'New end node',
|
||||
parentDocumentId: null,
|
||||
});
|
||||
|
||||
await collection.addDocumentToStructure(newDocument, 1);
|
||||
expect(collection.documentStructure.length).toBe(3);
|
||||
expect(collection.documentStructure[1].id).toBe('5');
|
||||
expect(collection.documentStructure[1].id).toBe(id);
|
||||
});
|
||||
|
||||
test('should add as a child if with parent', async () => {
|
||||
const { collection, document } = await seed();
|
||||
const id = uuid.v4();
|
||||
const newDocument = new Document({
|
||||
id: '5',
|
||||
id,
|
||||
title: 'New end node',
|
||||
parentDocumentId: document.id,
|
||||
});
|
||||
@@ -51,18 +55,19 @@ describe('#addDocumentToStructure', async () => {
|
||||
expect(collection.documentStructure.length).toBe(2);
|
||||
expect(collection.documentStructure[1].id).toBe(document.id);
|
||||
expect(collection.documentStructure[1].children.length).toBe(1);
|
||||
expect(collection.documentStructure[1].children[0].id).toBe('5');
|
||||
expect(collection.documentStructure[1].children[0].id).toBe(id);
|
||||
});
|
||||
|
||||
test('should add as a child if with parent with index', async () => {
|
||||
const { collection, document } = await seed();
|
||||
const newDocument = new Document({
|
||||
id: '5',
|
||||
id: uuid.v4(),
|
||||
title: 'node',
|
||||
parentDocumentId: document.id,
|
||||
});
|
||||
const id = uuid.v4();
|
||||
const secondDocument = new Document({
|
||||
id: '6',
|
||||
id,
|
||||
title: 'New start node',
|
||||
parentDocumentId: document.id,
|
||||
});
|
||||
@@ -72,14 +77,15 @@ describe('#addDocumentToStructure', async () => {
|
||||
expect(collection.documentStructure.length).toBe(2);
|
||||
expect(collection.documentStructure[1].id).toBe(document.id);
|
||||
expect(collection.documentStructure[1].children.length).toBe(2);
|
||||
expect(collection.documentStructure[1].children[0].id).toBe('6');
|
||||
expect(collection.documentStructure[1].children[0].id).toBe(id);
|
||||
});
|
||||
|
||||
describe('options: documentJson', async () => {
|
||||
test("should append supplied json over document's own", async () => {
|
||||
const { collection } = await seed();
|
||||
const id = uuid.v4();
|
||||
const newDocument = new Document({
|
||||
id: '5',
|
||||
id: uuid.v4(),
|
||||
title: 'New end node',
|
||||
parentDocumentId: null,
|
||||
});
|
||||
@@ -88,7 +94,7 @@ describe('#addDocumentToStructure', async () => {
|
||||
documentJson: {
|
||||
children: [
|
||||
{
|
||||
id: '7',
|
||||
id,
|
||||
title: 'Totally fake',
|
||||
children: [],
|
||||
},
|
||||
@@ -96,7 +102,7 @@ describe('#addDocumentToStructure', async () => {
|
||||
},
|
||||
});
|
||||
expect(collection.documentStructure[2].children.length).toBe(1);
|
||||
expect(collection.documentStructure[2].children[0].id).toBe('7');
|
||||
expect(collection.documentStructure[2].children[0].id).toBe(id);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,6 +7,7 @@ import Plain from 'slate-plain-serializer';
|
||||
import { Op } from 'sequelize';
|
||||
|
||||
import isUUID from 'validator/lib/isUUID';
|
||||
import { Collection } from '../models';
|
||||
import { DataTypes, sequelize } from '../sequelize';
|
||||
import events from '../events';
|
||||
import parseTitle from '../../shared/utils/parseTitle';
|
||||
@@ -107,6 +108,10 @@ Document.associate = models => {
|
||||
foreignKey: 'atlasId',
|
||||
onDelete: 'cascade',
|
||||
});
|
||||
Document.belongsTo(models.Team, {
|
||||
as: 'team',
|
||||
foreignKey: 'teamId',
|
||||
});
|
||||
Document.belongsTo(models.User, {
|
||||
as: 'createdBy',
|
||||
foreignKey: 'createdById',
|
||||
@@ -223,23 +228,51 @@ Document.searchForUser = async (
|
||||
|
||||
// Hooks
|
||||
|
||||
Document.addHook('afterCreate', model =>
|
||||
events.add({ name: 'documents.create', model })
|
||||
);
|
||||
Document.addHook('beforeSave', async model => {
|
||||
if (!model.publishedAt) return;
|
||||
|
||||
const collection = await Collection.findById(model.atlasId);
|
||||
if (collection.type !== 'atlas') return;
|
||||
|
||||
await collection.updateDocument(model);
|
||||
model.collection = collection;
|
||||
});
|
||||
|
||||
Document.addHook('afterCreate', async model => {
|
||||
if (!model.publishedAt) return;
|
||||
|
||||
const collection = await Collection.findById(model.atlasId);
|
||||
if (collection.type !== 'atlas') return;
|
||||
|
||||
await collection.addDocumentToStructure(model);
|
||||
model.collection = collection;
|
||||
|
||||
events.add({ name: 'documents.create', model });
|
||||
return model;
|
||||
});
|
||||
|
||||
Document.addHook('afterDestroy', model =>
|
||||
events.add({ name: 'documents.delete', model })
|
||||
);
|
||||
|
||||
Document.addHook('afterUpdate', model => {
|
||||
if (!model.previous('publishedAt') && model.publishedAt) {
|
||||
events.add({ name: 'documents.publish', model });
|
||||
}
|
||||
events.add({ name: 'documents.update', model });
|
||||
});
|
||||
|
||||
// Instance methods
|
||||
|
||||
Document.prototype.publish = async function() {
|
||||
if (this.publishedAt) return this.save();
|
||||
|
||||
const collection = await Collection.findById(this.atlasId);
|
||||
if (collection.type !== 'atlas') return this.save();
|
||||
|
||||
await collection.addDocumentToStructure(this);
|
||||
|
||||
this.publishedAt = new Date();
|
||||
await this.save();
|
||||
this.collection = collection;
|
||||
|
||||
events.add({ name: 'documents.publish', model: this });
|
||||
return this;
|
||||
};
|
||||
|
||||
Document.prototype.getTimestamp = function() {
|
||||
return Math.round(new Date(this.updatedAt).getTime() / 1000);
|
||||
};
|
||||
|
||||
35
server/models/Integration.js
Normal file
35
server/models/Integration.js
Normal file
@@ -0,0 +1,35 @@
|
||||
// @flow
|
||||
import { DataTypes, sequelize } from '../sequelize';
|
||||
|
||||
const Integration = sequelize.define('integration', {
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true,
|
||||
},
|
||||
type: DataTypes.STRING,
|
||||
serviceId: DataTypes.STRING,
|
||||
settings: DataTypes.JSONB,
|
||||
events: DataTypes.ARRAY(DataTypes.STRING),
|
||||
});
|
||||
|
||||
Integration.associate = models => {
|
||||
Integration.belongsTo(models.User, {
|
||||
as: 'user',
|
||||
foreignKey: 'userId',
|
||||
});
|
||||
Integration.belongsTo(models.Team, {
|
||||
as: 'team',
|
||||
foreignKey: 'teamId',
|
||||
});
|
||||
Integration.belongsTo(models.Collection, {
|
||||
as: 'collection',
|
||||
foreignKey: 'collectionId',
|
||||
});
|
||||
Integration.belongsTo(models.Authentication, {
|
||||
as: 'authentication',
|
||||
foreignKey: 'authenticationId',
|
||||
});
|
||||
};
|
||||
|
||||
export default Integration;
|
||||
@@ -1,10 +1,11 @@
|
||||
/* eslint-disable flowtype/require-valid-file-annotation */
|
||||
import { flushdb, seed } from '../test/support';
|
||||
import { flushdb } from '../test/support';
|
||||
import { buildUser } from '../test/factories';
|
||||
|
||||
beforeEach(flushdb);
|
||||
|
||||
it('should set JWT secret and password digest', async () => {
|
||||
const { user } = await seed();
|
||||
const user = await buildUser({ password: 'test123!' });
|
||||
expect(user.passwordDigest).toBeTruthy();
|
||||
expect(user.getJwtToken()).toBeTruthy();
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// @flow
|
||||
import Authentication from './Authentication';
|
||||
import Integration from './Integration';
|
||||
import Event from './Event';
|
||||
import User from './User';
|
||||
import Team from './Team';
|
||||
@@ -12,6 +13,7 @@ import Star from './Star';
|
||||
|
||||
const models = {
|
||||
Authentication,
|
||||
Integration,
|
||||
Event,
|
||||
User,
|
||||
Team,
|
||||
@@ -32,6 +34,7 @@ Object.keys(models).forEach(modelName => {
|
||||
|
||||
export {
|
||||
Authentication,
|
||||
Integration,
|
||||
Event,
|
||||
User,
|
||||
Team,
|
||||
|
||||
@@ -3,6 +3,7 @@ import policy from './policy';
|
||||
import './apiKey';
|
||||
import './collection';
|
||||
import './document';
|
||||
import './integration';
|
||||
import './user';
|
||||
|
||||
export default policy;
|
||||
|
||||
21
server/policies/integration.js
Normal file
21
server/policies/integration.js
Normal file
@@ -0,0 +1,21 @@
|
||||
// @flow
|
||||
import policy from './policy';
|
||||
import { Integration, User } from '../models';
|
||||
import { AdminRequiredError } from '../errors';
|
||||
|
||||
const { allow } = policy;
|
||||
|
||||
allow(User, 'create', Integration);
|
||||
|
||||
allow(
|
||||
User,
|
||||
'read',
|
||||
Integration,
|
||||
(user, integration) => user.teamId === integration.teamId
|
||||
);
|
||||
|
||||
allow(User, ['update', 'delete'], Integration, (user, integration) => {
|
||||
if (!integration || user.teamId !== integration.teamId) return false;
|
||||
if (user.isAdmin) return true;
|
||||
throw new AdminRequiredError();
|
||||
});
|
||||
@@ -6,6 +6,8 @@ import presentRevision from './revision';
|
||||
import presentCollection from './collection';
|
||||
import presentApiKey from './apiKey';
|
||||
import presentTeam from './team';
|
||||
import presentIntegration from './integration';
|
||||
import presentSlackAttachment from './slackAttachment';
|
||||
|
||||
export {
|
||||
presentUser,
|
||||
@@ -15,4 +17,6 @@ export {
|
||||
presentCollection,
|
||||
presentApiKey,
|
||||
presentTeam,
|
||||
presentIntegration,
|
||||
presentSlackAttachment,
|
||||
};
|
||||
|
||||
20
server/presenters/integration.js
Normal file
20
server/presenters/integration.js
Normal file
@@ -0,0 +1,20 @@
|
||||
// @flow
|
||||
import { Integration } from '../models';
|
||||
|
||||
function present(ctx: Object, integration: Integration) {
|
||||
return {
|
||||
id: integration.id,
|
||||
type: integration.type,
|
||||
userId: integration.userId,
|
||||
teamId: integration.teamId,
|
||||
serviceId: integration.serviceId,
|
||||
collectionId: integration.collectionId,
|
||||
authenticationId: integration.authenticationId,
|
||||
events: integration.events,
|
||||
settings: integration.settings,
|
||||
createdAt: integration.createdAt,
|
||||
updatedAt: integration.updatedAt,
|
||||
};
|
||||
}
|
||||
|
||||
export default present;
|
||||
@@ -1,5 +1,4 @@
|
||||
// @flow
|
||||
import _ from 'lodash';
|
||||
import { Revision } from '../models';
|
||||
|
||||
function present(ctx: Object, revision: Revision) {
|
||||
|
||||
15
server/presenters/slackAttachment.js
Normal file
15
server/presenters/slackAttachment.js
Normal file
@@ -0,0 +1,15 @@
|
||||
// @flow
|
||||
import { Document } from '../models';
|
||||
|
||||
function present(document: Document) {
|
||||
return {
|
||||
color: document.collection.color,
|
||||
title: document.title,
|
||||
title_link: `${process.env.URL}${document.getUrl()}`,
|
||||
footer: document.collection.name,
|
||||
text: document.getSummary(),
|
||||
ts: document.getTimestamp(),
|
||||
};
|
||||
}
|
||||
|
||||
export default present;
|
||||
22
server/services/index.js
Normal file
22
server/services/index.js
Normal file
@@ -0,0 +1,22 @@
|
||||
// @flow
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
|
||||
const services = {};
|
||||
|
||||
fs
|
||||
.readdirSync(__dirname)
|
||||
.filter(file => file.indexOf('.') !== 0 && file !== path.basename(__filename))
|
||||
.forEach(name => {
|
||||
const servicePath = path.join(__dirname, name);
|
||||
// $FlowIssue
|
||||
const pkg = require(path.join(servicePath, 'package.json'));
|
||||
// $FlowIssue
|
||||
const hooks = require(servicePath).default;
|
||||
services[pkg.name] = {
|
||||
...pkg,
|
||||
...hooks,
|
||||
};
|
||||
});
|
||||
|
||||
export default services;
|
||||
43
server/services/slack/index.js
Normal file
43
server/services/slack/index.js
Normal file
@@ -0,0 +1,43 @@
|
||||
// @flow
|
||||
import type { Event } from '../../events';
|
||||
import { Document, Integration } from '../../models';
|
||||
import { presentSlackAttachment } from '../../presenters';
|
||||
|
||||
const Slack = {
|
||||
on: async (event: Event) => {
|
||||
if (event.name !== 'documents.publish' && event.name !== 'documents.update')
|
||||
return;
|
||||
|
||||
const document = await Document.findById(event.model.id);
|
||||
if (!document) return;
|
||||
|
||||
const integration = await Integration.findOne({
|
||||
where: {
|
||||
teamId: document.teamId,
|
||||
serviceId: 'slack',
|
||||
collectionId: document.atlasId,
|
||||
type: 'post',
|
||||
},
|
||||
});
|
||||
if (!integration) return;
|
||||
|
||||
let text = `${document.createdBy.name} published a new document`;
|
||||
|
||||
if (event.name === 'documents.update') {
|
||||
text = `${document.createdBy.name} updated a document`;
|
||||
}
|
||||
|
||||
await fetch(integration.settings.url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
text,
|
||||
attachments: [presentSlackAttachment(document)],
|
||||
}),
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export default Slack;
|
||||
4
server/services/slack/package.json
Normal file
4
server/services/slack/package.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"name": "slack",
|
||||
"description": "Hook up your Slack to your Outline."
|
||||
}
|
||||
@@ -51,7 +51,7 @@ const seed = async () => {
|
||||
},
|
||||
});
|
||||
|
||||
let collection = await Collection.create({
|
||||
const collection = await Collection.create({
|
||||
id: '26fde1d4-0050-428f-9f0b-0bf77f8bdf62',
|
||||
name: 'Collection',
|
||||
urlId: 'collection',
|
||||
@@ -71,12 +71,11 @@ const seed = async () => {
|
||||
title: 'Second document',
|
||||
text: '# Much guidance',
|
||||
});
|
||||
collection = await collection.addDocumentToStructure(document);
|
||||
|
||||
return {
|
||||
user,
|
||||
admin,
|
||||
collection,
|
||||
collection: document.collection,
|
||||
document,
|
||||
team,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user