diff --git a/app/imports/api/tabletop/Tabletops.ts b/app/imports/api/tabletop/Tabletops.ts index 8e53f85d..fdd0b8ef 100644 --- a/app/imports/api/tabletop/Tabletops.ts +++ b/app/imports/api/tabletop/Tabletops.ts @@ -110,5 +110,6 @@ import '/imports/api/tabletop/methods/removeTabletop'; import '/imports/api/tabletop/methods/insertTabletop'; import '/imports/api/tabletop/methods/updateTabletop'; import '/imports/api/tabletop/methods/addCreaturesToTabletop'; +import '/imports/api/tabletop/methods/updateTabletopSharing'; export default Tabletops; diff --git a/app/imports/api/tabletop/methods/shared/tabletopPermissions.ts b/app/imports/api/tabletop/methods/shared/tabletopPermissions.ts index a7827390..7305c527 100644 --- a/app/imports/api/tabletop/methods/shared/tabletopPermissions.ts +++ b/app/imports/api/tabletop/methods/shared/tabletopPermissions.ts @@ -1,41 +1,56 @@ import Tabletops, { Tabletop } from '/imports/api/tabletop/Tabletops'; +type TabletopOrId = Tabletop | string | undefined; + function assertTabletopExists(tabletop: Tabletop | undefined): asserts tabletop is Tabletop { if (!tabletop) { throw new Meteor.Error('Tabletop does not exist', - 'No tabletop could be found for the given tabletop id'); + 'Tabletop not found'); } } -export function assertUserInTabletop(tabletopId, userId) { - const tabletop = Tabletops.findOne(tabletopId, { - fields: { gameMasters: 1, players: 1 } - }); +function getTabletop(tabletop: TabletopOrId): Tabletop | undefined { + if (typeof tabletop === 'string') { + return Tabletops.findOne(tabletop, { + fields: { gameMasters: 1, players: 1, owner: 1, spectators: 1 } + }); + } else { + return tabletop + } +} + +export function assertUserInTabletop(tabletopOrId: TabletopOrId, userId: string) { + const tabletop = getTabletop(tabletopOrId); assertTabletopExists(tabletop); if (!tabletop.gameMasters.includes(userId) && !tabletop.players.includes(userId)) { throw new Meteor.Error('Not in tabletop', - 'The user is not a game master or a player in the given tabletop'); + 'You are not a game master or a player in the tabletop'); } } -export function assertUserGameMasterOfTabletop(tabletopId, userId) { - const tabletop = Tabletops.findOne(tabletopId, { - fields: { gameMasters: 1 }, - }); +export function assertUserGameMasterOfTabletop(tabletopOrId: TabletopOrId, userId: string) { + const tabletop = getTabletop(tabletopOrId); assertTabletopExists(tabletop); if (tabletop.gameMasters.includes(userId)) { throw new Meteor.Error('not-game-master', - 'The user is not a game master in the given tabletop'); + 'You are not a game master in the tabletop'); } } -export function assertUserIsTabletopOwner(tabletopId, userId) { - const tabletop = Tabletops.findOne(tabletopId, { - fields: { owner: 1 }, - }); +export function assertCanEditTabletop(tabletopOrId: TabletopOrId, userId: string) { + const tabletop = getTabletop(tabletopOrId); assertTabletopExists(tabletop); - if (tabletop.owner === userId) { - throw new Meteor.Error('not-owner', - 'The user is not the owner of the given tabletop'); + if (tabletop.owner !== userId && tabletop.gameMasters.includes(userId)) { + throw new Meteor.Error('not-editor', + 'You are not an owner or game master of the tabletop'); + } +} + +export function assertUserIsTabletopOwner(tabletopOrId: TabletopOrId, userId: string) { + const tabletop = getTabletop(tabletopOrId); + assertTabletopExists(tabletop); + if (tabletop.owner !== userId) { + throw new Meteor.Error('not-owner', + 'You are not the owner of the tabletop'); } } diff --git a/app/imports/api/tabletop/methods/updateTabletop.js b/app/imports/api/tabletop/methods/updateTabletop.js index b7bcc7f0..d0a7620b 100644 --- a/app/imports/api/tabletop/methods/updateTabletop.js +++ b/app/imports/api/tabletop/methods/updateTabletop.js @@ -2,9 +2,9 @@ import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; import Tabletops from '../Tabletops'; import { assertUserHasPaidBenefits } from '/imports/api/users/patreon/tiers'; -import { assertUserIsTabletopOwner } from './shared/tabletopPermissions'; +import { assertCanEditTabletop } from './shared/tabletopPermissions'; -const removeTabletop = new ValidatedMethod({ +const updateTabletop = new ValidatedMethod({ name: 'tabletops.update', @@ -15,6 +15,7 @@ const removeTabletop = new ValidatedMethod({ 'name', 'description', 'imageUrl', + 'public', ]; if (!allowedFields.includes(path[0])) { throw new Meteor.Error('tabletops.update.denied', @@ -31,11 +32,11 @@ const removeTabletop = new ValidatedMethod({ run({ _id, path, value }) { if (!this.userId) { - throw new Meteor.Error('tabletops.remove.denied', - 'You need to be logged in to remove a tabletop'); + throw new Meteor.Error('tabletops.update.denied', + 'You need to be logged in to update a tabletop'); } assertUserHasPaidBenefits(this.userId); - assertUserIsTabletopOwner(_id, this.userId); + assertCanEditTabletop(_id, this.userId); if (value === undefined || value === null) { Tabletops.update(_id, { @@ -50,4 +51,4 @@ const removeTabletop = new ValidatedMethod({ }); -export default removeTabletop; +export default updateTabletop; diff --git a/app/imports/api/tabletop/methods/updateTabletopSharing.js b/app/imports/api/tabletop/methods/updateTabletopSharing.js new file mode 100644 index 00000000..795571ee --- /dev/null +++ b/app/imports/api/tabletop/methods/updateTabletopSharing.js @@ -0,0 +1,121 @@ +import { ValidatedMethod } from 'meteor/mdg:validated-method'; +import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; +import Tabletops from '../Tabletops'; +import { assertUserHasPaidBenefits } from '/imports/api/users/patreon/tiers'; +import { assertCanEditTabletop, assertUserIsTabletopOwner } from './shared/tabletopPermissions'; + +const updateTabletopSharing = new ValidatedMethod({ + + name: 'tabletops.updateSharing', + + validate({ tabletopId, userId, role }) { + if (!userId) return false; + if (!tabletopId) return false; + // Allowed fields + const roles = [ + 'owner', + 'gameMaster', + 'player', + 'spectator', + 'none', + ]; + if (!roles.includes(role)) { + throw new Meteor.Error('tabletops.updateSharing.denied', + 'Invalid role selected'); + } + }, + + mixins: [RateLimiterMixin], + // @ts-expect-error Rate limit not defined + rateLimit: { + numRequests: 5, + timeInterval: 5000, + }, + + run({ tabletopId, userId, role }) { + if (!this.userId) { + throw new Meteor.Error('tabletops.update.denied', + 'You need to be logged in to update a tabletop'); + } + const tabletop = Tabletops.findOne(tabletopId); + assertUserHasPaidBenefits(this.userId); + assertCanEditTabletop(tabletop, this.userId); + + if (role === 'owner') { + assertUserIsTabletopOwner(tabletop, this.userId); + } + + // Check that the new user exists + if (Meteor.isServer) { + const userToAdd = Meteor.users.findOne({ _id: userId }, { fields: { _id: 1 } }); + if (!userToAdd) { + throw new Meteor.Error('User not found', + 'The user could not be found' + ); + } + } + + let update; + switch (role) { + case 'owner': + update = { + $set: { owner: userId }, + $addToSet: { + gameMasters: this.userId, + }, + $pull: { + players: this.userId, + spectators: this.userId, + }, + }; + break; + case 'gameMaster': + update = { + $addToSet: { + gameMasters: userId, + }, + $pull: { + players: userId, + spectators: userId, + }, + }; + break; + case 'player': + update = { + $addToSet: { + players: userId, + }, + $pull: { + gameMasters: userId, + spectators: userId, + }, + }; + break; + case 'spectator': + update = { + $addToSet: { + spectators: userId, + }, + $pull: { + gameMasters: userId, + players: userId, + }, + }; + break; + case 'none': + update = { + $pull: { + gameMasters: userId, + players: userId, + spectators: userId, + }, + }; + break; + } + if (!update) return; + return Tabletops.update(tabletopId, update) + }, + +}); + +export default updateTabletopSharing; diff --git a/app/imports/client/ui/dialogStack/DialogComponentIndex.js b/app/imports/client/ui/dialogStack/DialogComponentIndex.js index 18281615..6a14e4d1 100644 --- a/app/imports/client/ui/dialogStack/DialogComponentIndex.js +++ b/app/imports/client/ui/dialogStack/DialogComponentIndex.js @@ -32,6 +32,7 @@ const LibraryNodeDialog = () => import('/imports/client/ui/library/LibraryNodeDi const MoveLibraryNodeDialog = () => import('/imports/client/ui/library/MoveLibraryNodeDialog.vue'); const SelectCreaturesDialog = () => import('/imports/client/ui/tabletop/SelectCreaturesDialog.vue'); const ShareDialog = () => import('/imports/client/ui/sharing/ShareDialog.vue'); +const TabletopDialog = () => import('/imports/client/ui/tabletop/TabletopDialog.vue'); const UsernameDialog = () => import('/imports/client/ui/user/UsernameDialog.vue'); export default { @@ -66,5 +67,6 @@ export default { SlotFillDialog, TierTooLowDialog, TransferOwnershipDialog, + TabletopDialog, UsernameDialog, }; diff --git a/app/imports/client/ui/layouts/AppLayout.vue b/app/imports/client/ui/layouts/AppLayout.vue index 0126419c..e5123d30 100644 --- a/app/imports/client/ui/layouts/AppLayout.vue +++ b/app/imports/client/ui/layouts/AppLayout.vue @@ -50,7 +50,7 @@ - + diff --git a/app/imports/client/ui/pages/NotImplemented.vue b/app/imports/client/ui/pages/NotImplemented.vue index e3a3c471..465bd06c 100644 --- a/app/imports/client/ui/pages/NotImplemented.vue +++ b/app/imports/client/ui/pages/NotImplemented.vue @@ -8,3 +8,9 @@ + + diff --git a/app/imports/client/ui/pages/Tabletop.vue b/app/imports/client/ui/pages/Tabletop.vue index 2bbcb82e..431a4bef 100644 --- a/app/imports/client/ui/pages/Tabletop.vue +++ b/app/imports/client/ui/pages/Tabletop.vue @@ -1,6 +1,7 @@ diff --git a/app/imports/client/ui/router.js b/app/imports/client/ui/router.js index 8e987c49..a9c1d0f4 100644 --- a/app/imports/client/ui/router.js +++ b/app/imports/client/ui/router.js @@ -207,6 +207,9 @@ RouterFactory.configure(router => { name: 'tabletops', component: Tabletops, beforeEnter: ensureLoggedIn, + meta: { + title: 'Tabletops', + }, }, { path: '/tabletop/:id', name: 'tabletop', diff --git a/app/imports/client/ui/sharing/ShareDialog.vue b/app/imports/client/ui/sharing/ShareDialog.vue index adc5dbde..0ab55b6c 100644 --- a/app/imports/client/ui/sharing/ShareDialog.vue +++ b/app/imports/client/ui/sharing/ShareDialog.vue @@ -27,7 +27,7 @@ v-if="model.public && docRef.collection === 'libraries'" readonly label="Link" - :value="'https://dicecloud.com' + $router.resolve({ + :value="window.location.origin + $router.resolve({ name: 'singleLibrary', params: { id: model._id }, }).href" diff --git a/app/imports/client/ui/tabletop/TabletopDialog.vue b/app/imports/client/ui/tabletop/TabletopDialog.vue new file mode 100644 index 00000000..c131c4c6 --- /dev/null +++ b/app/imports/client/ui/tabletop/TabletopDialog.vue @@ -0,0 +1,217 @@ + + + + diff --git a/app/imports/client/ui/tabletop/TabletopForm.vue b/app/imports/client/ui/tabletop/TabletopForm.vue new file mode 100644 index 00000000..5b622baa --- /dev/null +++ b/app/imports/client/ui/tabletop/TabletopForm.vue @@ -0,0 +1,265 @@ + + + + diff --git a/app/imports/client/ui/tabletop/TabletopUserList.vue b/app/imports/client/ui/tabletop/TabletopUserList.vue new file mode 100644 index 00000000..dc7bb606 --- /dev/null +++ b/app/imports/client/ui/tabletop/TabletopUserList.vue @@ -0,0 +1,122 @@ + + + \ No newline at end of file diff --git a/app/imports/client/ui/tabletop/TabletopViewer.vue b/app/imports/client/ui/tabletop/TabletopViewer.vue new file mode 100644 index 00000000..22d46d7f --- /dev/null +++ b/app/imports/client/ui/tabletop/TabletopViewer.vue @@ -0,0 +1,103 @@ + + + + + diff --git a/app/imports/server/publications/tabletops.js b/app/imports/server/publications/tabletops.js index 440e03d3..01b3e5c8 100644 --- a/app/imports/server/publications/tabletops.js +++ b/app/imports/server/publications/tabletops.js @@ -20,6 +20,26 @@ Meteor.publish('tabletops', function () { }); }); +Meteor.publish('tabletopUsers', function (tabletopId) { + if (!tabletopId) return []; + const tabletop = Tabletops.findOne(tabletopId); + if (!tabletop) return []; + const userIds = [ + tabletop.owner, + ...tabletop.gameMasters, + ...tabletop.players, + ...tabletop.spectators, + ] + return Meteor.users.find({ + _id: { $in: userIds }, + }, { + fields: { + username: 1, + }, + limit: 500, + }); +}) + Meteor.publish('tabletop', function (tabletopId) { var userId = this.userId; if (!userId) {