Allowed non-patreons to view, but not edit, sheets and libraries

This commit is contained in:
Thaum Rystra
2020-05-21 16:50:06 +02:00
parent 7a442d8fb9
commit 81131ddb9f
12 changed files with 303 additions and 173 deletions

View File

@@ -4,6 +4,7 @@ import deathSaveSchema from '/imports/api/properties/subSchemas/DeathSavesSchema
import ColorSchema from '/imports/api/properties/subSchemas/ColorSchema.js';
import SharingSchema from '/imports/api/sharing/SharingSchema.js';
import {assertEditPermission} from '/imports/api/sharing/sharingPermissions.js';
import { getUserTier } from '/imports/api/users/patreon/tiers.js';
import '/imports/api/creature/removeCreature.js';
@@ -108,6 +109,11 @@ const insertCreature = new ValidatedMethod({
throw new Meteor.Error('Creatures.methods.insert.denied',
'You need to be logged in to insert a creature');
}
let tier = getUserTier(this.userId);
if (!tier.paidBenefits){
throw new Meteor.Error('Creatures.methods.insert.denied',
`The ${tier.name} tier does not allow you to insert a creature`);
}
// Create the creature document
let charId = Creatures.insert({

View File

@@ -1,8 +1,10 @@
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import SimpleSchema from 'simpl-schema';
import SharingSchema from '/imports/api/sharing/SharingSchema.js';
import simpleSchemaMixin from '/imports/api/creature/mixins/simpleSchemaMixin.js';
import { assertEditPermission, assertOwnership } from '/imports/api/sharing/sharingPermissions.js';
import LibraryNodes from '/imports/api/library/LibraryNodes.js';
import { getUserTier } from '/imports/api/users/patreon/tiers.js'
/**
* Libraries are trees of library nodes where each node represents a character
@@ -38,6 +40,15 @@ const insertLibrary = new ValidatedMethod({
],
schema: LibrarySchema.omit('owner', 'isDefault'),
run(library) {
if (!this.userId) {
throw new Meteor.Error('Libraries.methods.insert.denied',
'You need to be logged in to insert a library');
}
let tier = getUserTier(this.userId);
if (!tier.paidBenefits){
throw new Meteor.Error('Libraries.methods.insert.denied',
`The ${tier.name} tier does not allow you to insert a library`);
}
library.owner = this.userId;
return Libraries.insert(library);
},

View File

@@ -1,17 +1,18 @@
import { _ } from 'meteor/underscore';
import fetchDocByRef from '/imports/api/parenting/fetchDocByRef.js';
import { getUserTier } from '/imports/api/users/patreon/tiers.js';
function assertIdValid(userId){
if (!userId || typeof userId !== 'string'){
throw new Meteor.Error("Permission denied",
"No user ID given for edit permission check");
throw new Meteor.Error('Permission denied',
'No user ID given for edit permission check');
}
}
function assertdocExists(doc){
if (!doc){
throw new Meteor.Error("Permission denied",
`No such document exists`);
throw new Meteor.Error('Permission denied',
'No such document exists');
}
}
@@ -21,8 +22,8 @@ export function assertOwnership(doc, userId){
if (doc.owner === userId ){
return true;
} else {
throw new Meteor.Error("Permission denied",
`You are not the owner of this document`);
throw new Meteor.Error('Permission denied',
'You are not the owner of this document');
}
}
@@ -35,11 +36,34 @@ export function assertOwnership(doc, userId){
export function assertEditPermission(doc, userId) {
assertIdValid(userId);
assertdocExists(doc);
if (doc.owner === userId || _.contains(doc.writers, userId)){
let user = Meteor.users.findOne(userId, {
fields: {
'services.patreon': 1,
'roles': 1,
}
});
// Admin override
if (user.roles && user.roles.includes('admin')){
return true;
}
// Ensure the user is of a tier with paid benefits
let tier = getUserTier(user);
if (!tier.paidBenefits){
throw new Meteor.Error('Edit permission denied',
`The ${tier.name} tier does not allow you to edit this document`);
}
// Ensure the user is authorized for this specific document
if (
doc.owner === userId ||
_.contains(doc.writers, userId)
){
return true;
} else {
throw new Meteor.Error("Edit permission denied",
`You do not have permission to edit this document`);
throw new Meteor.Error('Edit permission denied',
'You do not have permission to edit this document');
}
}
@@ -74,8 +98,8 @@ export function assertViewPermission(doc, userId) {
){
return true;
} else {
throw new Meteor.Error("View permission denied",
`You do not have permission to view this character`);
throw new Meteor.Error('View permission denied',
'You do not have permission to view this character');
}
}

View File

@@ -59,7 +59,11 @@ export function getTierByEntitledCents(entitledCents = 0){
export function getUserTier(user){
if (!user) throw 'user must be provided';
if (typeof user === 'string'){
user = Meteor.users.findOne(user);
user = Meteor.users.findOne(user, {
fields: {
'services.patreon': 1,
}
});
if (!user) throw 'User not found';
}
const entitledCents = getEntitledCents(user);

View File

@@ -1,6 +1,7 @@
<template lang="html">
<v-toolbar-items v-if="creature">
<v-btn
v-if="editPermission"
flat
icon
@click="recompute(creature._id)"

View File

@@ -9,8 +9,10 @@ import LibraryEditDialog from '/imports/ui/library/LibraryEditDialog.vue';
import LibraryNodeCreationDialog from '/imports/ui/library/LibraryNodeCreationDialog.vue';
import LibraryNodeEditDialog from '/imports/ui/library/LibraryNodeEditDialog.vue';
import ShareDialog from '/imports/ui/sharing/ShareDialog.vue';
import TierTooLowDialog from '/imports/ui/user/TierTooLowDialog.vue';
import UsernameDialog from '/imports/ui/user/UsernameDialog.vue';
export default {
CreatureFormDialog,
CreaturePropertyCreationDialog,
@@ -23,5 +25,6 @@ export default {
LibraryNodeCreationDialog,
LibraryNodeEditDialog,
ShareDialog,
TierTooLowDialog,
UsernameDialog,
};

View File

@@ -1,64 +1,72 @@
<template lang="html">
<div
style="
background-color: inherit;
overflow-y: auto;
"
>
<v-expansion-panel
style="box-shadow: none;"
v-model="expandedLibrary"
>
<v-expansion-panel-content
lazy
v-for="library in libraries"
:key="library._id"
:data-id="library._id"
>
<template v-slot:header>
<div class="title">{{library.name}}</div>
</template>
<v-card flat>
<library-contents-container
:library-id="library._id"
:organize-mode="organizeMode"
:edit-mode="editMode"
@selected="e => $emit('selected', e)"
:selected-node-id="selectedNodeId"
/>
<v-card-actions>
<v-btn
flat small
style="background-color: inherit; margin-top: 0;"
@click="insertLibraryNode(library._id)"
:data-id="`insert-node-${library._id}`"
>
<v-icon>add</v-icon>
New property
</v-btn>
<v-spacer/>
<v-btn flat small icon
@click="editLibrary(library._id)"
>
<v-icon>create</v-icon>
</v-btn>
</v-card-actions>
</v-card>
</v-expansion-panel-content>
</v-expansion-panel>
<v-btn
v-show="expandedLibrary === null"
v-if="editMode"
flat
color="primary"
style="background-color: inherit;"
@click="insertLibrary"
data-id="insert-library-button"
>
<v-icon>add</v-icon>
New library
</v-btn>
</div>
<div
style="
background-color: inherit;
overflow-y: auto;
"
>
<v-expansion-panel
v-model="expandedLibrary"
style="box-shadow: none;"
>
<v-expansion-panel-content
v-for="library in libraries"
:key="library._id"
lazy
:data-id="library._id"
>
<template #header>
<div class="title">
{{ library.name }}
</div>
</template>
<v-card flat>
<library-contents-container
:library-id="library._id"
:organize-mode="organizeMode"
:edit-mode="editMode"
:selected-node-id="selectedNodeId"
@selected="e => $emit('selected', e)"
/>
<v-card-actions>
<v-btn
flat
small
style="background-color: inherit; margin-top: 0;"
:disabled="!editPermission(library)"
:data-id="`insert-node-${library._id}`"
@click="insertLibraryNode(library._id)"
>
<v-icon>add</v-icon>
New property
</v-btn>
<v-spacer />
<v-btn
flat
small
icon
:disabled="!editPermission(library)"
@click="editLibrary(library._id)"
>
<v-icon>create</v-icon>
</v-btn>
</v-card-actions>
</v-card>
</v-expansion-panel-content>
</v-expansion-panel>
<v-btn
v-show="expandedLibrary === null"
v-if="editMode"
flat
color="primary"
style="background-color: inherit;"
data-id="insert-library-button"
@click="insertLibrary"
>
<v-icon>add</v-icon>
New library
</v-btn>
</div>
</template>
<script>
@@ -66,63 +74,91 @@ import LibraryContentsContainer from '/imports/ui/library/LibraryContentsContain
import { setDocToLastOrder } from '/imports/api/parenting/order.js';
import LibraryNodes, { insertNode } from '/imports/api/library/LibraryNodes.js';
import Libraries, { insertLibrary } from '/imports/api/library/Libraries.js';
import { getUserTier } from '/imports/api/users/patreon/tiers.js';
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
export default {
components: {
LibraryContentsContainer,
},
props: {
organizeMode: Boolean,
editMode: Boolean,
selectedNodeId: String,
},
data(){ return {
expandedLibrary: null,
};},
meteor: {
$subscribe: {
'libraries': [],
},
libraries(){
return Libraries.find({}, {
sort: {name: 1}
}).fetch();
},
},
methods: {
insertLibrary(){
this.$store.commit('pushDialogStack', {
component: 'library-creation-dialog',
elementId: 'insert-library-button',
callback(library){
if (!library) return;
let libraryId = insertLibrary.call(library);
return libraryId;
}
});
},
editLibrary(_id){
this.$store.commit('pushDialogStack', {
component: 'library-edit-dialog',
elementId: _id,
data: {_id},
});
},
insertLibraryNode(libraryId){
this.$store.commit('pushDialogStack', {
component: 'library-node-creation-dialog',
elementId: `insert-node-${libraryId}`,
callback(libraryNode){
if (!libraryNode) return;
libraryNode.parent = {collection: "libraries", id: libraryId};
libraryNode.ancestors = [ {collection: "libraries", id: libraryId}];
setDocToLastOrder({collection: LibraryNodes, doc: libraryNode});
let libraryNodeId = insertNode.call(libraryNode);
return `tree-node-${libraryNodeId}`;
}
});
},
},
components: {
LibraryContentsContainer,
},
props: {
organizeMode: Boolean,
editMode: Boolean,
selectedNodeId: String,
},
data(){ return {
expandedLibrary: null,
};},
meteor: {
$subscribe: {
'libraries': [],
},
libraries(){
return Libraries.find({}, {
sort: {name: 1}
}).fetch();
},
paidBenefits(){
let tier = getUserTier(Meteor.userId());
return tier && tier.paidBenefits;
},
},
methods: {
insertLibrary(){
if (this.paidBenefits){
this.$store.commit('pushDialogStack', {
component: 'library-creation-dialog',
elementId: 'insert-library-button',
callback(library){
if (!library) return;
let libraryId = insertLibrary.call(library);
return libraryId;
}
});
} else {
this.$store.commit('pushDialogStack', {
component: 'tier-too-low-dialog',
elementId: 'insert-library-button',
});
}
},
editPermission(library){
try {
assertEditPermission(library, Meteor.userId());
return true;
} catch (e) {
return false;
}
},
editLibrary(_id){
this.$store.commit('pushDialogStack', {
component: 'library-edit-dialog',
elementId: _id,
data: {_id},
});
},
insertLibraryNode(libraryId){
if (this.paidBenefits){
this.$store.commit('pushDialogStack', {
component: 'library-node-creation-dialog',
elementId: `insert-node-${libraryId}`,
callback(libraryNode){
if (!libraryNode) return;
libraryNode.parent = {collection: 'libraries', id: libraryId};
libraryNode.ancestors = [ {collection: 'libraries', id: libraryId}];
setDocToLastOrder({collection: LibraryNodes, doc: libraryNode});
let libraryNodeId = insertNode.call(libraryNode);
return `tree-node-${libraryNodeId}`;
}
});
} else {
this.$store.commit('pushDialogStack', {
component: 'tier-too-low-dialog',
elementId: `insert-node-${libraryId}`,
});
}
},
},
}
</script>

View File

@@ -53,6 +53,18 @@
</v-expansion-panel-content>
</v-expansion-panel>
</v-card>
<v-btn
color="accent"
fab
fixed
bottom
right
data-id="new-character-button"
@click="insertCharacter"
>
<v-icon>add</v-icon>
</v-btn>
<!--
<v-speed-dial
v-model="fab"
fixed
@@ -72,6 +84,7 @@
<labeled-fab
icon="face"
label="New Character"
data-id="new-character-button"
@click="insertCharacter"
/>
<labeled-fab
@@ -79,6 +92,7 @@
label="New Party"
/>
</v-speed-dial>
-->
</div>
</template>
@@ -86,6 +100,7 @@
import Creatures, {insertCreature} from '/imports/api/creature/Creatures.js';
import Parties from '/imports/api/campaign/Parties.js';
import LabeledFab from '/imports/ui/components/LabeledFab.vue';
import { getUserTier } from '/imports/api/users/patreon/tiers.js';
const characterTransform = function(char){
char.url = `/character/${char._id}/${char.urlName || '-'}`;
@@ -136,26 +151,21 @@
},
methods: {
insertCharacter(){
insertCreature.call((error, result) => {
if (error){
console.error(error);
} else {
this.$router.push({ path: `/character/${result}`})
}
});
/*
store.commit("pushDialogStack", {
component: CharacterCreationDialog,
data: {},
element: undefined,
returnElement: undefined,
callback(result){
if (!result) return;
insertCreature.call(result);
},
});
*/
let tier = getUserTier(Meteor.userId());
if (tier.paidBenefits){
insertCreature.call((error, result) => {
if (error){
console.error(error);
} else {
this.$router.push({ path: `/character/${result}`})
}
});
} else {
this.$store.commit('pushDialogStack', {
component: 'tier-too-low-dialog',
elementId: 'new-character-button',
});
}
},
}
};

View File

@@ -12,6 +12,12 @@
You need to be at least Adventurer tier (or be invited by a Patron of
a higher tier) to access this beta
</h3>
<v-btn
href="https://www.patreon.com/join/dicecloud/checkout?rid=3002853"
color="accent"
>
Join now
</v-btn>
</v-layout>
</div>
</template>

View File

@@ -28,7 +28,7 @@
<template #activator="{ on }">
<v-btn
:loading="addResourceLoading"
:disabled="addResourceLoading"
:disabled="addResourceLoading || context.editPermission === false"
icon
large
outline
@@ -62,6 +62,9 @@
AttributesConsumedListForm,
ItemsConsumedListForm,
},
inject: {
context: { default: {} }
},
mixins: [propertyFormMixin],
props: {
parentTarget: {

View File

@@ -1,5 +1,4 @@
import { RouterFactory, nativeScrollBehavior } from 'meteor/akryum:vue-router2';
import { getUserTier } from '/imports/api/users/patreon/tiers.js';
import LAUNCH_DATE from '/imports/constants/LAUNCH_DATE.js';
import { acceptInviteToken } from '/imports/api/users/Invites.js';
@@ -48,25 +47,6 @@ function ensureLoggedIn(to, from, next){
});
}
function ensurePaidFeatures(to, from, next){
Tracker.autorun((computation) => {
if (userSubscription.ready()){
computation.stop();
const user = Meteor.user();
if (!user){
next({ name: 'signIn', query: { redirect: to.path} });
return;
}
let tier = getUserTier(user);
if (tier && tier.paidBenefits){
next();
} else {
next('/patreon-level-too-low');
}
}
});
}
function claimInvite(to, from, next){
Tracker.autorun((computation) => {
if (userSubscription.ready()){
@@ -118,7 +98,7 @@ RouterFactory.configure(factory => {
meta: {
title: 'Character List',
},
beforeEnter: ensurePaidFeatures,
beforeEnter: ensureLoggedIn,
},{
path: '/library',
components: {
@@ -127,7 +107,7 @@ RouterFactory.configure(factory => {
meta: {
title: 'Library',
},
beforeEnter: ensurePaidFeatures,
beforeEnter: ensureLoggedIn,
},{
name: 'singleLibrary',
path: '/library/:id',
@@ -138,7 +118,6 @@ RouterFactory.configure(factory => {
meta: {
title: 'Library',
},
beforeEnter: ensurePaidFeatures,
},{
path: '/character/:id/:urlName',
components: {
@@ -149,7 +128,6 @@ RouterFactory.configure(factory => {
meta: {
title: 'Character Sheet',
},
beforeEnter: ensurePaidFeatures,
},{
path: '/character/:id',
components: {
@@ -160,7 +138,6 @@ RouterFactory.configure(factory => {
meta: {
title: 'Character Sheet',
},
beforeEnter: ensurePaidFeatures,
},{
path: '/friends',
components: {

View File

@@ -0,0 +1,49 @@
<template lang="html">
<dialog-base>
<v-layout
column
align-center
justify-center
>
<h2 style="margin: 48px 28px 16px">
Your current Patreon tier is {{ tier.name }}
</h2>
<h3>
You need to be at least Adventurer tier (or be invited by a Patron of
a higher tier) to perform this action
</h3>
<v-btn
href="https://www.patreon.com/join/dicecloud/checkout?rid=3002853"
color="accent"
>
Join now
</v-btn>
</v-layout>
<v-spacer slot="actions" />
<v-btn
slot="actions"
flat
@click="$store.dispatch('popDialogStack')"
>
Cancel
</v-btn>
</dialog-base>
</template>
<script>
import TIERS, { getUserTier } from '/imports/api/users/patreon/tiers.js';
import DialogBase from '/imports/ui/dialogStack/DialogBase.vue';
export default {
components: {
DialogBase,
},
meteor: {
tier(){
let user = Meteor.user();
if (!user) return TIERS[0];
return getUserTier(user);
}
},
}
</script>