Added copy-to and related sharing permissions
This commit is contained in:
@@ -1,10 +1,12 @@
|
||||
import { ValidatedMethod } from 'meteor/mdg:validated-method';
|
||||
import SimpleSchema from 'simpl-schema';
|
||||
i
|
||||
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
|
||||
import { RefSchema } from '/imports/api/parenting/ChildSchema.js';
|
||||
import LibraryNodes from '/imports/api/library/LibraryNodes.js';
|
||||
import { assertDocEditPermission } from '/imports/api/sharing/sharingPermissions.js';
|
||||
import {
|
||||
assertDocCopyPermission,
|
||||
assertDocEditPermission
|
||||
} from '/imports/api/sharing/sharingPermissions.js';
|
||||
import {
|
||||
setLineageOfDocs,
|
||||
renewDocIds
|
||||
@@ -19,7 +21,7 @@ if (Meteor.isClient) {
|
||||
).snackbar
|
||||
}
|
||||
|
||||
const DUPLICATE_CHILDREN_LIMIT = 100;
|
||||
const DUPLICATE_CHILDREN_LIMIT = 500;
|
||||
|
||||
const copyLibraryNodeTo = new ValidatedMethod({
|
||||
name: 'libraryNodes.copyTo',
|
||||
@@ -35,7 +37,7 @@ const copyLibraryNodeTo = new ValidatedMethod({
|
||||
mixins: [RateLimiterMixin],
|
||||
rateLimit: {
|
||||
numRequests: 1,
|
||||
timeInterval: 5000,
|
||||
timeInterval: 10000,
|
||||
},
|
||||
run({ _id, parent }) {
|
||||
if (parent.collection !== 'libraryNodes' && parent.collection !== 'libraries') {
|
||||
@@ -45,11 +47,8 @@ const copyLibraryNodeTo = new ValidatedMethod({
|
||||
}
|
||||
const libraryNode = LibraryNodes.findOne(_id);
|
||||
const parentDoc = fetchDocByRef(parent);
|
||||
assertDocEditPermission(libraryNode, this.userId);
|
||||
|
||||
let randomSrc = DDP.randomStream('copyLibraryNodeTo');
|
||||
let libraryNodeId = randomSrc.id();
|
||||
libraryNode._id = libraryNodeId;
|
||||
assertDocCopyPermission(libraryNode, this.userId);
|
||||
assertDocEditPermission(parentDoc, this.userId);
|
||||
|
||||
let decendants = LibraryNodes.find({
|
||||
'ancestors.id': _id,
|
||||
@@ -68,7 +67,7 @@ const copyLibraryNodeTo = new ValidatedMethod({
|
||||
}
|
||||
}
|
||||
|
||||
const nodes = [libraryNode, decendants];
|
||||
const nodes = [libraryNode, ...decendants];
|
||||
|
||||
const newAncestry = parentDoc.ancestors || [];
|
||||
newAncestry.push(parent);
|
||||
@@ -92,9 +91,7 @@ const copyLibraryNodeTo = new ValidatedMethod({
|
||||
collection: LibraryNodes,
|
||||
ancestorId: parent.collection === 'libraries' ? parent.id : parentDoc.ancestors[0].id,
|
||||
});
|
||||
|
||||
return libraryNodeId;
|
||||
},
|
||||
});
|
||||
|
||||
export default duplicateLibraryNode;
|
||||
export default copyLibraryNodeTo;
|
||||
|
||||
@@ -16,7 +16,7 @@ if (Meteor.isClient) {
|
||||
).snackbar
|
||||
}
|
||||
|
||||
const DUPLICATE_CHILDREN_LIMIT = 100;
|
||||
const DUPLICATE_CHILDREN_LIMIT = 500;
|
||||
|
||||
const duplicateLibraryNode = new ValidatedMethod({
|
||||
name: 'libraryNodes.duplicate',
|
||||
@@ -28,7 +28,7 @@ const duplicateLibraryNode = new ValidatedMethod({
|
||||
}).validator(),
|
||||
mixins: [RateLimiterMixin],
|
||||
rateLimit: {
|
||||
numRequests: 5,
|
||||
numRequests: 1,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({ _id }) {
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
import '/imports/api/library/methods/copyLibraryNodeTo.js';
|
||||
import '/imports/api/library/methods/duplicateLibraryNode.js';
|
||||
import '/imports/api/library/methods/updateReferenceNode.js';
|
||||
|
||||
@@ -33,6 +33,10 @@ let SharingSchema = new SimpleSchema({
|
||||
defaultValue: false,
|
||||
index: 1,
|
||||
},
|
||||
readersCanCopy: {
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
},
|
||||
});
|
||||
|
||||
export default SharingSchema;
|
||||
|
||||
@@ -27,6 +27,26 @@ const setPublic = new ValidatedMethod({
|
||||
},
|
||||
});
|
||||
|
||||
const setReadersCanCopy = new ValidatedMethod({
|
||||
name: 'sharing.setReadersCanCopy',
|
||||
validate: new SimpleSchema({
|
||||
docRef: RefSchema,
|
||||
readersCanCopy: { type: Boolean },
|
||||
}).validator(),
|
||||
mixins: [RateLimiterMixin],
|
||||
rateLimit: {
|
||||
numRequests: 5,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({ docRef, readersCanCopy }) {
|
||||
let doc = fetchDocByRef(docRef);
|
||||
assertOwnership(doc, this.userId);
|
||||
return getCollectionByName(docRef.collection).update(docRef.id, {
|
||||
$set: { readersCanCopy },
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const updateUserSharePermissions = new ValidatedMethod({
|
||||
name: 'sharing.updateUserSharePermissions',
|
||||
validate: new SimpleSchema({
|
||||
@@ -129,4 +149,4 @@ const transferOwnership = new ValidatedMethod({
|
||||
},
|
||||
});
|
||||
|
||||
export { setPublic, updateUserSharePermissions, transferOwnership };
|
||||
export { setPublic, setReadersCanCopy, updateUserSharePermissions, transferOwnership };
|
||||
|
||||
@@ -1,24 +1,25 @@
|
||||
import { _ } from 'meteor/underscore';
|
||||
import fetchDocByRef from '/imports/api/parenting/fetchDocByRef.js';
|
||||
|
||||
function assertIdValid(userId){
|
||||
if (!userId || typeof userId !== 'string'){
|
||||
function assertIdValid(userId) {
|
||||
if (!userId || typeof userId !== 'string') {
|
||||
throw new Meteor.Error('Permission denied',
|
||||
'No user ID. Are you logged in?');
|
||||
}
|
||||
}
|
||||
|
||||
function assertdocExists(doc){
|
||||
if (!doc){
|
||||
function assertdocExists(doc) {
|
||||
if (!doc) {
|
||||
throw new Meteor.Error('Permission denied',
|
||||
'Permission denied: No such document exists');
|
||||
}
|
||||
}
|
||||
|
||||
export function assertOwnership(doc, userId){
|
||||
export function assertOwnership(doc, userId) {
|
||||
assertIdValid(userId);
|
||||
assertdocExists(doc);
|
||||
if (doc.owner === userId ){
|
||||
|
||||
if (doc.owner === userId) {
|
||||
return true;
|
||||
} else {
|
||||
throw new Meteor.Error('Permission denied',
|
||||
@@ -37,13 +38,12 @@ export function assertEditPermission(doc, userId) {
|
||||
assertdocExists(doc);
|
||||
const user = Meteor.users.findOne(userId, {
|
||||
fields: {
|
||||
'services.patreon': 1,
|
||||
'roles': 1,
|
||||
}
|
||||
});
|
||||
|
||||
// Admin override
|
||||
if (user.roles && user.roles.includes('admin')){
|
||||
if (user.roles && user.roles.includes('admin')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ export function assertEditPermission(doc, userId) {
|
||||
if (
|
||||
doc.owner === userId ||
|
||||
_.contains(doc.writers, userId)
|
||||
){
|
||||
) {
|
||||
return true;
|
||||
} else {
|
||||
throw new Meteor.Error('Edit permission denied',
|
||||
@@ -59,9 +59,46 @@ export function assertEditPermission(doc, userId) {
|
||||
}
|
||||
}
|
||||
|
||||
function getRoot(doc){
|
||||
/**
|
||||
* Assert that the user can edit the root document which manages its own sharing
|
||||
* permissions.
|
||||
*
|
||||
* Warning: the doc and userId must be set by a trusted source
|
||||
*/
|
||||
export function assertCopyPermission(doc, userId) {
|
||||
assertIdValid(userId);
|
||||
assertdocExists(doc);
|
||||
if (doc.ancestors && doc.ancestors.length && doc.ancestors[0]){
|
||||
const user = Meteor.users.findOne(userId, {
|
||||
fields: {
|
||||
'roles': 1,
|
||||
}
|
||||
});
|
||||
|
||||
// Admin override
|
||||
if (user.roles && user.roles.includes('admin')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Ensure the user is authorized for this specific document
|
||||
if (
|
||||
doc.owner === userId ||
|
||||
_.contains(doc.writers, userId)
|
||||
) {
|
||||
return true;
|
||||
} else if (
|
||||
(_.contains(doc.readers, userId) || doc.public) &&
|
||||
doc.readersCanCopy
|
||||
) {
|
||||
return true;
|
||||
} else {
|
||||
throw new Meteor.Error('Copy permission denied',
|
||||
'You do not have permission to copy this document');
|
||||
}
|
||||
}
|
||||
|
||||
function getRoot(doc) {
|
||||
assertdocExists(doc);
|
||||
if (doc.ancestors && doc.ancestors.length && doc.ancestors[0]) {
|
||||
return fetchDocByRef(doc.ancestors[0]);
|
||||
} else {
|
||||
return doc;
|
||||
@@ -74,11 +111,22 @@ function getRoot(doc){
|
||||
*
|
||||
* Warning: the doc and userId must be set by a trusted source
|
||||
*/
|
||||
export function assertDocEditPermission(doc, userId){
|
||||
export function assertDocEditPermission(doc, userId) {
|
||||
let root = getRoot(doc);
|
||||
assertEditPermission(root, userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that the user can copy a descendant document whose root ancestor
|
||||
* implements sharing permissions.
|
||||
*
|
||||
* Warning: the doc and userId must be set by a trusted source
|
||||
*/
|
||||
export function assertDocCopyPermission(doc, userId) {
|
||||
let root = getRoot(doc);
|
||||
assertCopyPermission(root, userId);
|
||||
}
|
||||
|
||||
export function assertViewPermission(doc, userId) {
|
||||
assertdocExists(doc);
|
||||
if (doc.public) return true;
|
||||
@@ -88,17 +136,17 @@ export function assertViewPermission(doc, userId) {
|
||||
doc.owner === userId ||
|
||||
_.contains(doc.readers, userId) ||
|
||||
_.contains(doc.writers, userId)
|
||||
){
|
||||
) {
|
||||
return true;
|
||||
} else {
|
||||
|
||||
|
||||
// Admin override
|
||||
const user = Meteor.users.findOne(userId, {
|
||||
fields: {
|
||||
'roles': 1,
|
||||
}
|
||||
});
|
||||
if (user.roles && user.roles.includes('admin')){
|
||||
if (user.roles && user.roles.includes('admin')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -113,20 +161,20 @@ export function assertViewPermission(doc, userId) {
|
||||
*
|
||||
* Warning: the doc and userId must be set by a trusted source
|
||||
*/
|
||||
export function assertDocViewPermission(doc, userId){
|
||||
export function assertDocViewPermission(doc, userId) {
|
||||
let root = getRoot(doc);
|
||||
assertViewPermission(root, userId);
|
||||
}
|
||||
|
||||
export function assertAdmin(userId){
|
||||
export function assertAdmin(userId) {
|
||||
assertIdValid(userId);
|
||||
let user = Meteor.users.findOne(userId, {fields: {roles: 1}});
|
||||
if (!user){
|
||||
let user = Meteor.users.findOne(userId, { fields: { roles: 1 } });
|
||||
if (!user) {
|
||||
throw new Meteor.Error('Permission denied',
|
||||
'UserId does not match any existing user');
|
||||
}
|
||||
let isAdmin = user.roles && user.roles.includes('admin')
|
||||
if (!isAdmin){
|
||||
if (!isAdmin) {
|
||||
throw new Meteor.Error('Permission denied',
|
||||
'User does not have the admin role');
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
:outlined="!!label"
|
||||
:icon="!label"
|
||||
:min-width="label && 108"
|
||||
:disabled="context.editPermission === false"
|
||||
v-on="on"
|
||||
>
|
||||
{{ label }}
|
||||
@@ -124,6 +125,9 @@
|
||||
}
|
||||
|
||||
export default {
|
||||
inject: {
|
||||
context: { default: {} }
|
||||
},
|
||||
props: {
|
||||
//hex string
|
||||
value: {
|
||||
|
||||
@@ -69,6 +69,7 @@
|
||||
</v-list-item>
|
||||
<v-list-item
|
||||
v-if="$listeners && $listeners.duplicate"
|
||||
:disabled="context.editPermission === false"
|
||||
@click="$emit('duplicate')"
|
||||
>
|
||||
<v-list-item-content>
|
||||
@@ -80,8 +81,23 @@
|
||||
<v-icon>mdi-content-copy</v-icon>
|
||||
</v-list-item-action>
|
||||
</v-list-item>
|
||||
<v-list-item
|
||||
v-if="$listeners && $listeners.copy"
|
||||
:disabled="context.copyPermission === false"
|
||||
@click="$emit('copy')"
|
||||
>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>
|
||||
Copy To
|
||||
</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
<v-list-item-action>
|
||||
<v-icon>mdi-content-duplicate</v-icon>
|
||||
</v-list-item-action>
|
||||
</v-list-item>
|
||||
<v-list-item
|
||||
v-if="$listeners && $listeners.move"
|
||||
:disabled="context.editPermission === false"
|
||||
@click="$emit('move')"
|
||||
>
|
||||
<v-list-item-content>
|
||||
@@ -95,6 +111,7 @@
|
||||
</v-list-item>
|
||||
<v-list-item
|
||||
v-if="$listeners && $listeners.remove"
|
||||
:disabled="context.editPermission === false"
|
||||
@click="$emit('remove')"
|
||||
>
|
||||
<v-list-item-content>
|
||||
@@ -157,6 +174,9 @@ export default {
|
||||
PropertyIcon,
|
||||
ColorPicker,
|
||||
},
|
||||
inject: {
|
||||
context: { default: {} }
|
||||
},
|
||||
props: {
|
||||
model: {
|
||||
type: Object,
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
:embedded="embedded"
|
||||
@duplicate="duplicate"
|
||||
@move="move"
|
||||
@copy="copy"
|
||||
@remove="remove"
|
||||
@toggle-editing="editing = !editing"
|
||||
@color-changed="value => change({path: ['color'], value})"
|
||||
@@ -95,10 +96,13 @@
|
||||
import propertyFormIndex from '/imports/ui/properties/forms/shared/propertyFormIndex.js';
|
||||
import propertyViewerIndex from '/imports/ui/properties/viewers/shared/propertyViewerIndex.js';
|
||||
import { get } from 'lodash';
|
||||
import { assertDocEditPermission } from '/imports/api/sharing/sharingPermissions.js';
|
||||
import {
|
||||
assertDocEditPermission, assertDocCopyPermission
|
||||
} from '/imports/api/sharing/sharingPermissions.js';
|
||||
import { organizeDoc } from '/imports/api/parenting/organizeMethods.js';
|
||||
import { snackbar } from '/imports/ui/components/snackbars/SnackbarQueue.js';
|
||||
import getPropertyTitle from '/imports/ui/properties/shared/getPropertyTitle.js';
|
||||
import copyLibraryNodeTo from '/imports/api/library/methods/copyLibraryNodeTo.js';
|
||||
|
||||
let formIndex = {};
|
||||
for (let key in propertyFormIndex){
|
||||
@@ -126,7 +130,7 @@
|
||||
},
|
||||
reactiveProvide: {
|
||||
name: 'context',
|
||||
include: ['editPermission', 'isLibraryForm'],
|
||||
include: ['editPermission', 'copyPermission', 'isLibraryForm'],
|
||||
},
|
||||
data(){return {
|
||||
editing: !!this.startInEditTab,
|
||||
@@ -162,6 +166,14 @@
|
||||
return false;
|
||||
}
|
||||
},
|
||||
copyPermission(){
|
||||
try {
|
||||
assertDocCopyPermission(this.model, Meteor.userId());
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
getPropertyName,
|
||||
@@ -200,6 +212,37 @@
|
||||
}
|
||||
});
|
||||
},
|
||||
copy(){
|
||||
const thisId = this._id;
|
||||
this.$store.commit('pushDialogStack', {
|
||||
component: 'move-library-node-dialog',
|
||||
elementId: 'property-toolbar-menu-button',
|
||||
data: {
|
||||
action: 'Copy',
|
||||
},
|
||||
callback(parentId){
|
||||
if (!parentId) return;
|
||||
copyLibraryNodeTo.call({
|
||||
_id: thisId,
|
||||
parent: {
|
||||
collection: 'libraryNodes',
|
||||
id: parentId
|
||||
},
|
||||
}, (error) => {
|
||||
if (error) {
|
||||
console.error(error);
|
||||
snackbar({
|
||||
text: error.reason || error.message || error.toString(),
|
||||
});
|
||||
} else {
|
||||
snackbar({
|
||||
text: 'Copied successfully',
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
change({path, value, ack}){
|
||||
updateLibraryNode.call({_id: this.currentId, path, value}, (error) =>{
|
||||
if (ack){
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
color="primary"
|
||||
@click="$store.dispatch('popDialogStack', node._id)"
|
||||
>
|
||||
Move
|
||||
{{ action || 'Move' }}
|
||||
</v-btn>
|
||||
</template>
|
||||
</dialog-base>
|
||||
@@ -30,6 +30,12 @@ export default {
|
||||
DialogBase,
|
||||
LibraryAndNode,
|
||||
},
|
||||
props: {
|
||||
action: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
node: undefined,
|
||||
|
||||
@@ -13,6 +13,16 @@
|
||||
:value="!!model.public + ''"
|
||||
@change="(value, ack) => setSheetPublic({value, ack})"
|
||||
/>
|
||||
<smart-select
|
||||
v-if="docRef.collection === 'libraries'"
|
||||
label="Who can copy from this library"
|
||||
:items="[
|
||||
{text: 'Only people with edit permission', value: 'false'},
|
||||
{text: 'Anyone with read permission', value: 'true'}
|
||||
]"
|
||||
:value="!!model.readersCanCopy + ''"
|
||||
@change="(value, ack) => setReadersCanCopy({value, ack})"
|
||||
/>
|
||||
<text-field
|
||||
v-if="model.public && docRef.collection === 'libraries'"
|
||||
readonly
|
||||
@@ -30,6 +40,7 @@
|
||||
@change="(value, ack) => getUser({value, ack})"
|
||||
/>
|
||||
<v-btn
|
||||
class="ml-2 mt-2"
|
||||
:disabled="userFoundState !== 'found'"
|
||||
@click="updateSharing(userId, 'reader')"
|
||||
>
|
||||
@@ -126,6 +137,7 @@
|
||||
<script lang="js">
|
||||
import {
|
||||
setPublic,
|
||||
setReadersCanCopy,
|
||||
updateUserSharePermissions
|
||||
} from '/imports/api/sharing/sharing.js';
|
||||
import fetchDocByRef from '/imports/api/parenting/fetchDocByRef.js';
|
||||
@@ -157,6 +169,14 @@ export default {
|
||||
ack(error && error.reason || error);
|
||||
});
|
||||
},
|
||||
setReadersCanCopy({ value, ack }) {
|
||||
setReadersCanCopy.call({
|
||||
docRef: this.docRef,
|
||||
readersCanCopy: value === 'true',
|
||||
}, (error) => {
|
||||
ack(error && error.reason || error);
|
||||
});
|
||||
},
|
||||
getUser({ value, ack }) {
|
||||
this.userSearched = value;
|
||||
if (!value) {
|
||||
|
||||
Reference in New Issue
Block a user