( - Component: React$ComponentType<{| ...ContextRouter, ...P |}> - ): React$ComponentType
; - - declare type MatchPathOptions = { - path?: string, - exact?: boolean, - sensitive?: boolean, - strict?: boolean - }; - - declare export function matchPath( - pathname: string, - options?: MatchPathOptions | string - ): null | Match; -} diff --git a/server/api/shares.js b/server/api/shares.js index 35b9f345..be3c8c26 100644 --- a/server/api/shares.js +++ b/server/api/shares.js @@ -4,7 +4,7 @@ import Sequelize from 'sequelize'; import auth from '../middlewares/authentication'; import pagination from './middlewares/pagination'; import { presentShare } from '../presenters'; -import { Document, User, Share } from '../models'; +import { Document, User, Share, Team } from '../models'; import policy from '../policies'; const Op = Sequelize.Op; @@ -57,7 +57,9 @@ router.post('shares.create', auth(), async ctx => { const user = ctx.state.user; const document = await Document.findById(documentId); + const team = await Team.findById(user.teamId); authorize(user, 'share', document); + authorize(user, 'share', team); const [share] = await Share.findOrCreate({ where: { diff --git a/server/api/shares.test.js b/server/api/shares.test.js index fd65a08e..b5c01e33 100644 --- a/server/api/shares.test.js +++ b/server/api/shares.test.js @@ -122,6 +122,15 @@ describe('#shares.create', async () => { expect(body.data.id).toBe(share.id); }); + it('should not allow creating a share record if disabled', async () => { + const { user, document, team } = await seed(); + await team.update({ sharing: false }); + const res = await server.post('/api/shares.create', { + body: { token: user.getJwtToken(), documentId: document.id }, + }); + expect(res.status).toEqual(403); + }); + it('should require authentication', async () => { const { document } = await seed(); const res = await server.post('/api/shares.create', { diff --git a/server/api/team.js b/server/api/team.js index d04c729c..3bc4a1b8 100644 --- a/server/api/team.js +++ b/server/api/team.js @@ -12,7 +12,7 @@ const { authorize } = policy; const router = new Router(); router.post('team.update', auth(), async ctx => { - const { name, avatarUrl } = ctx.body; + const { name, avatarUrl, sharing } = ctx.body; const endpoint = publicS3Endpoint(); const user = ctx.state.user; @@ -20,6 +20,7 @@ router.post('team.update', auth(), async ctx => { authorize(user, 'update', team); if (name) team.name = name; + if (sharing !== undefined) team.sharing = sharing; if (avatarUrl && avatarUrl.startsWith(`${endpoint}/uploads/${user.id}`)) { team.avatarUrl = avatarUrl; } diff --git a/server/migrations/20180819054252-disable-sharing.js b/server/migrations/20180819054252-disable-sharing.js new file mode 100644 index 00000000..38246eee --- /dev/null +++ b/server/migrations/20180819054252-disable-sharing.js @@ -0,0 +1,12 @@ +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.addColumn('teams', 'sharing', { + type: Sequelize.BOOLEAN, + allowNull: false, + defaultValue: true + }); + }, + down: async (queryInterface, Sequelize) => { + await queryInterface.removeColumn('teams', 'sharing'); + } +} \ No newline at end of file diff --git a/server/models/Team.js b/server/models/Team.js index 808e85b5..93330385 100644 --- a/server/models/Team.js +++ b/server/models/Team.js @@ -17,6 +17,7 @@ const Team = sequelize.define( slackId: { type: DataTypes.STRING, allowNull: true }, googleId: { type: DataTypes.STRING, allowNull: true }, avatarUrl: { type: DataTypes.STRING, allowNull: true }, + sharing: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: true }, slackData: DataTypes.JSONB, }, { @@ -39,11 +40,16 @@ const uploadAvatar = async model => { const endpoint = publicS3Endpoint(); if (model.avatarUrl && !model.avatarUrl.startsWith(endpoint)) { - const newUrl = await uploadToS3FromUrl( - model.avatarUrl, - `avatars/${model.id}/${uuid.v4()}` - ); - if (newUrl) model.avatarUrl = newUrl; + try { + const newUrl = await uploadToS3FromUrl( + model.avatarUrl, + `avatars/${model.id}/${uuid.v4()}` + ); + if (newUrl) model.avatarUrl = newUrl; + } catch (err) { + // we can try again next time + console.error(err); + } } }; diff --git a/server/policies/team.js b/server/policies/team.js index acd07777..414a9cc3 100644 --- a/server/policies/team.js +++ b/server/policies/team.js @@ -7,6 +7,11 @@ const { allow } = policy; allow(User, 'read', Team, (user, team) => team && user.teamId === team.id); +allow(User, 'share', Team, (user, team) => { + if (!team || user.teamId !== team.id) return false; + return team.sharing; +}); + allow(User, ['update', 'export'], Team, (user, team) => { if (!team || user.teamId !== team.id) return false; if (user.isAdmin) return true; diff --git a/server/presenters/team.js b/server/presenters/team.js index 863de4a3..a600ac2c 100644 --- a/server/presenters/team.js +++ b/server/presenters/team.js @@ -11,6 +11,7 @@ function present(ctx: Object, team: Team) { team.avatarUrl || (team.slackData ? team.slackData.image_88 : null), slackConnected: !!team.slackId, googleConnected: !!team.googleId, + sharing: team.sharing, }; } diff --git a/shared/styles/theme.js b/shared/styles/theme.js index cfa9b4b4..b8dc020c 100644 --- a/shared/styles/theme.js +++ b/shared/styles/theme.js @@ -11,7 +11,7 @@ const theme = { placeholder: '#b1becc', danger: '#D0021B', warning: '#f08a24', - success: '#1AB6FF', + success: '#2f3336', info: '#a0d3e8', slate: '#9BA6B2',