Collection Permissions (#829)
see https://github.com/outline/outline/issues/668
This commit is contained in:
@@ -6,6 +6,7 @@ import { DataTypes, sequelize } from '../sequelize';
|
||||
import { asyncLock } from '../redis';
|
||||
import events from '../events';
|
||||
import Document from './Document';
|
||||
import CollectionUser from './CollectionUser';
|
||||
import Event from './Event';
|
||||
import { welcomeMessage } from '../utils/onboarding';
|
||||
|
||||
@@ -26,6 +27,7 @@ const Collection = sequelize.define(
|
||||
name: DataTypes.STRING,
|
||||
description: DataTypes.STRING,
|
||||
color: DataTypes.STRING,
|
||||
private: DataTypes.BOOLEAN,
|
||||
type: {
|
||||
type: DataTypes.STRING,
|
||||
validate: { isIn: allowedCollectionTypes },
|
||||
@@ -85,6 +87,11 @@ Collection.associate = models => {
|
||||
foreignKey: 'collectionId',
|
||||
onDelete: 'cascade',
|
||||
});
|
||||
Collection.belongsToMany(models.User, {
|
||||
as: 'users',
|
||||
through: models.CollectionUser,
|
||||
foreignKey: 'collectionId',
|
||||
});
|
||||
Collection.belongsTo(models.User, {
|
||||
as: 'user',
|
||||
foreignKey: 'creatorId',
|
||||
@@ -92,16 +99,20 @@ Collection.associate = models => {
|
||||
Collection.belongsTo(models.Team, {
|
||||
as: 'team',
|
||||
});
|
||||
Collection.addScope('withRecentDocuments', {
|
||||
include: [
|
||||
{
|
||||
as: 'documents',
|
||||
limit: 10,
|
||||
model: models.Document,
|
||||
order: [['updatedAt', 'DESC']],
|
||||
},
|
||||
],
|
||||
});
|
||||
Collection.addScope(
|
||||
'defaultScope',
|
||||
{
|
||||
include: [
|
||||
{
|
||||
model: models.User,
|
||||
as: 'users',
|
||||
through: 'collection_users',
|
||||
paranoid: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
{ override: true }
|
||||
);
|
||||
};
|
||||
|
||||
Collection.addHook('afterDestroy', async model => {
|
||||
@@ -112,8 +123,6 @@ Collection.addHook('afterDestroy', async model => {
|
||||
});
|
||||
});
|
||||
|
||||
// Hooks
|
||||
|
||||
Collection.addHook('afterCreate', model =>
|
||||
events.add({ name: 'collections.create', model })
|
||||
);
|
||||
@@ -126,6 +135,22 @@ Collection.addHook('afterUpdate', model =>
|
||||
events.add({ name: 'collections.update', model })
|
||||
);
|
||||
|
||||
Collection.addHook('afterCreate', (model, options) => {
|
||||
if (model.private) {
|
||||
return CollectionUser.findOrCreate({
|
||||
where: {
|
||||
collectionId: model.id,
|
||||
userId: model.creatorId,
|
||||
},
|
||||
defaults: {
|
||||
permission: 'read_write',
|
||||
createdById: model.creatorId,
|
||||
},
|
||||
transaction: options.transaction,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Instance methods
|
||||
|
||||
Collection.prototype.addDocumentToStructure = async function(
|
||||
|
||||
34
server/models/CollectionUser.js
Normal file
34
server/models/CollectionUser.js
Normal file
@@ -0,0 +1,34 @@
|
||||
// @flow
|
||||
import { DataTypes, sequelize } from '../sequelize';
|
||||
|
||||
const CollectionUser = sequelize.define(
|
||||
'collection_user',
|
||||
{
|
||||
permission: {
|
||||
type: DataTypes.STRING,
|
||||
validate: {
|
||||
isIn: [['read', 'read_write']],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
}
|
||||
);
|
||||
|
||||
CollectionUser.associate = models => {
|
||||
CollectionUser.belongsTo(models.Collection, {
|
||||
as: 'collection',
|
||||
foreignKey: 'collectionId',
|
||||
});
|
||||
CollectionUser.belongsTo(models.User, {
|
||||
as: 'user',
|
||||
foreignKey: 'userId',
|
||||
});
|
||||
CollectionUser.belongsTo(models.User, {
|
||||
as: 'createdBy',
|
||||
foreignKey: 'createdById',
|
||||
});
|
||||
};
|
||||
|
||||
export default CollectionUser;
|
||||
@@ -220,7 +220,7 @@ Document.searchForUser = async (
|
||||
ts_headline('english', "text", plainto_tsquery('english', :query), 'MaxFragments=1, MinWords=20, MaxWords=30') as "searchContext"
|
||||
FROM documents
|
||||
WHERE "searchVector" @@ plainto_tsquery('english', :query) AND
|
||||
"teamId" = '${user.teamId}'::uuid AND
|
||||
"collectionId" IN(:collectionIds) AND
|
||||
"deletedAt" IS NULL AND
|
||||
("publishedAt" IS NOT NULL OR "createdById" = '${user.id}')
|
||||
ORDER BY
|
||||
@@ -230,20 +230,24 @@ Document.searchForUser = async (
|
||||
OFFSET :offset;
|
||||
`;
|
||||
|
||||
const collectionIds = await user.collectionIds();
|
||||
const results = await sequelize.query(sql, {
|
||||
type: sequelize.QueryTypes.SELECT,
|
||||
replacements: {
|
||||
query,
|
||||
limit,
|
||||
offset,
|
||||
collectionIds,
|
||||
},
|
||||
});
|
||||
|
||||
// Second query to get associated document data
|
||||
// Final query to get associated document data
|
||||
const documents = await Document.scope({
|
||||
method: ['withViews', user.id],
|
||||
}).findAll({
|
||||
where: { id: map(results, 'id') },
|
||||
where: {
|
||||
id: map(results, 'id'),
|
||||
},
|
||||
include: [
|
||||
{ model: Collection, as: 'collection' },
|
||||
{ model: User, as: 'createdBy', paranoid: false },
|
||||
|
||||
@@ -6,7 +6,7 @@ import subMinutes from 'date-fns/sub_minutes';
|
||||
import { DataTypes, sequelize, encryptedFields } from '../sequelize';
|
||||
import { publicS3Endpoint, uploadToS3FromUrl } from '../utils/s3';
|
||||
import { sendEmail } from '../mailer';
|
||||
import { Star, NotificationSetting, ApiKey } from '.';
|
||||
import { Star, Collection, NotificationSetting, ApiKey } from '.';
|
||||
|
||||
const User = sequelize.define(
|
||||
'user',
|
||||
@@ -54,6 +54,25 @@ User.associate = models => {
|
||||
};
|
||||
|
||||
// Instance methods
|
||||
User.prototype.collectionIds = async function() {
|
||||
let models = await Collection.findAll({
|
||||
attributes: ['id', 'private'],
|
||||
where: { teamId: this.teamId },
|
||||
include: [
|
||||
{
|
||||
model: User,
|
||||
through: 'collection_users',
|
||||
as: 'users',
|
||||
where: { id: this.id },
|
||||
required: false,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// Filter collections that are private and don't have an association
|
||||
return models.filter(c => !c.private || c.users.length).map(c => c.id);
|
||||
};
|
||||
|
||||
User.prototype.updateActiveAt = function(ip) {
|
||||
const fiveMinutesAgo = subMinutes(new Date(), 5);
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import ApiKey from './ApiKey';
|
||||
import Authentication from './Authentication';
|
||||
import Collection from './Collection';
|
||||
import CollectionUser from './CollectionUser';
|
||||
import Document from './Document';
|
||||
import Event from './Event';
|
||||
import Integration from './Integration';
|
||||
@@ -18,6 +19,7 @@ const models = {
|
||||
ApiKey,
|
||||
Authentication,
|
||||
Collection,
|
||||
CollectionUser,
|
||||
Document,
|
||||
Event,
|
||||
Integration,
|
||||
@@ -42,6 +44,7 @@ export {
|
||||
ApiKey,
|
||||
Authentication,
|
||||
Collection,
|
||||
CollectionUser,
|
||||
Document,
|
||||
Event,
|
||||
Integration,
|
||||
|
||||
Reference in New Issue
Block a user