mirror of
https://github.com/wekan/wekan.git
synced 2026-11-05 01:20:07 -05:00
Add User board access roles to Admin Panel / People / Roles.
A new global "Roles" tab in Admin Panel / People lets a site admin choose which
board roles are allowed to invite users to a board ("Allow Invite to Board").
Each board role has its own toggle — Board Admin, Normal, Worker, Comment only,
No comments, Only Assigned Normal, Only Assigned Comment, Read Only and Only
Assigned Read — plus an "All Board Members" master toggle that selects them all.
The policy is enforced server-side in the `inviteUserToBoard` and `searchUsers`
methods, and the add-member button in the board sidebar is shown only to roles
the policy allows. Global Admin Panel users (site admins) always have all rights
and cannot be restricted here; this is kept clearly distinct in code from the
per-board "Board Admin" role. Secure default: only Board Admin and Normal may invite.
Thanks to xet7.
This commit is contained in:
1 parent
f9b790668d
commit
c956ab5a4e
15 files changed
+313
-6
No files matched your search
@@ -142,6 +142,7 @@ Template.board.helpers({
|
||||
|
||||
Template.boardBody.onCreated(function () {
|
||||
Meteor.subscribe('tableVisibilityModeSettings');
|
||||
Meteor.subscribe('inviteToBoardRolesSettings');
|
||||
this.showOverlay = new ReactiveVar(false);
|
||||
this.draggingActive = new ReactiveVar(false);
|
||||
this._isDragging = false;
|
||||
|
||||
@@ -354,3 +354,27 @@ table td:first-child {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Roles tab. The .main-body scrolls horizontally and is wider than the visible
|
||||
area (the side-menu takes ~250px), so by default a long description would run
|
||||
off the right edge and only be fully readable at the smallest (mobile) width.
|
||||
Pin the heading/description to the visible left edge and wrap them within the
|
||||
viewport so they stay visible at every width. */
|
||||
.roles-settings {
|
||||
padding: 0 10px;
|
||||
}
|
||||
.roles-settings h3,
|
||||
.roles-settings h4,
|
||||
.roles-settings .roles-description {
|
||||
position: sticky;
|
||||
left: 0;
|
||||
/* Cap to ~60 characters, and never wider than the viewport, so the text wraps
|
||||
into the visible left area at every width (desktop, mid and mobile) instead
|
||||
of running off the horizontally-scrolling main-body. */
|
||||
max-width: min(60ch, 88vw);
|
||||
white-space: normal;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
.roles-settings .roles-description {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
@@ -63,6 +63,11 @@ template(name="people")
|
||||
i.fa.fa-lock
|
||||
unless isMiniScreen
|
||||
| {{_ 'accounts-lockout-locked-users'}}
|
||||
else if rolesSetting.get
|
||||
span
|
||||
i.fa.fa-key
|
||||
unless isMiniScreen
|
||||
| {{_ 'roles'}}
|
||||
|
||||
.content-body
|
||||
.side-menu
|
||||
@@ -84,6 +89,10 @@ template(name="people")
|
||||
span.emoji-icon.text-red
|
||||
i.fa.fa-lock
|
||||
| {{_ 'accounts-lockout-locked-users'}}
|
||||
li
|
||||
a.js-roles-menu(data-id="roles-setting")
|
||||
i.fa.fa-key
|
||||
| {{_ 'roles'}}
|
||||
.main-body
|
||||
if loading.get
|
||||
+spinner
|
||||
@@ -95,6 +104,8 @@ template(name="people")
|
||||
+peopleGeneral(peopleList=peopleList peopleCurrentPage=peopleCurrentPage peopleTotalPages=peopleTotalPages hasPeoplePrevPage=hasPeoplePrevPage hasPeopleNextPage=hasPeopleNextPage)
|
||||
else if lockedUsersSetting.get
|
||||
+lockedUsersGeneral
|
||||
else if rolesSetting.get
|
||||
+rolesGeneral
|
||||
|
||||
|
||||
template(name="orgGeneral")
|
||||
@@ -719,3 +730,22 @@ template(name="lockedUsersGeneral")
|
||||
input.wekan-form-control#unknown-failure-window(type="number", min="1", max="60", placeholder="15" value="{{unknownFailureWindow}}")
|
||||
|
||||
button.js-lockout-save.primary {{_ 'save'}}
|
||||
|
||||
template(name="rolesGeneral")
|
||||
.roles-settings
|
||||
h3 {{_ 'roles'}}
|
||||
p.roles-description {{_ 'roles-info'}}
|
||||
|
||||
h4 {{_ 'allow-invite-to-board'}}
|
||||
ul#roles-setting.setting-detail
|
||||
li
|
||||
a.flex.js-toggle-all-roles
|
||||
.materialCheckBox(class="{{#if allRolesAllowed}}is-checked{{/if}}")
|
||||
span {{_ 'all-board-members'}}
|
||||
each roleOptions
|
||||
li
|
||||
a.flex.js-toggle-role(data-role="{{key}}")
|
||||
.materialCheckBox(class="{{#if allowed}}is-checked{{/if}}")
|
||||
span {{_ label}}
|
||||
|
||||
button.js-roles-save.primary {{_ 'save'}}
|
||||
@@ -6,6 +6,10 @@ import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
|
||||
import Org from '/models/org';
|
||||
import Team from '/models/team';
|
||||
import Users from '/models/users';
|
||||
import InviteToBoardRolesSettings, {
|
||||
INVITE_TO_BOARD_ROLES,
|
||||
INVITE_TO_BOARD_ROLES_ID,
|
||||
} from '/models/inviteToBoardRolesSettings';
|
||||
|
||||
const orgsPerPage = 25;
|
||||
const teamsPerPage = 25;
|
||||
@@ -22,6 +26,8 @@ Template.people.onCreated(function () {
|
||||
this.teamSetting = new ReactiveVar(false);
|
||||
this.peopleSetting = new ReactiveVar(false);
|
||||
this.lockedUsersSetting = new ReactiveVar(false);
|
||||
this.rolesSetting = new ReactiveVar(false);
|
||||
this.subscribe('inviteToBoardRolesSettings');
|
||||
this.findOrgsOptions = new ReactiveVar({});
|
||||
this.findTeamsOptions = new ReactiveVar({});
|
||||
this.findUsersOptions = new ReactiveVar({});
|
||||
@@ -191,6 +197,7 @@ Template.people.onCreated(function () {
|
||||
this.teamSetting.set('team-setting' === targetID);
|
||||
this.peopleSetting.set('people-setting' === targetID);
|
||||
this.lockedUsersSetting.set('locked-users-setting' === targetID);
|
||||
this.rolesSetting.set('roles-setting' === targetID);
|
||||
|
||||
// When switching to locked users tab, refresh the locked users list
|
||||
if ('locked-users-setting' === targetID) {
|
||||
@@ -263,6 +270,9 @@ Template.people.helpers({
|
||||
lockedUsersSetting() {
|
||||
return Template.instance().lockedUsersSetting;
|
||||
},
|
||||
rolesSetting() {
|
||||
return Template.instance().rolesSetting;
|
||||
},
|
||||
orgList() {
|
||||
const tpl = Template.instance();
|
||||
const page = tpl.orgPage.get();
|
||||
@@ -521,6 +531,66 @@ Template.people.events({
|
||||
'click a.js-locked-users-menu'(event, tpl) {
|
||||
tpl.switchMenu(event);
|
||||
},
|
||||
'click a.js-roles-menu'(event, tpl) {
|
||||
tpl.switchMenu(event);
|
||||
},
|
||||
});
|
||||
|
||||
Template.rolesGeneral.onCreated(function () {
|
||||
// Working copy of the allowed-roles set; null until the published doc loads.
|
||||
this.workingRoles = new ReactiveVar(null);
|
||||
this.autorun(() => {
|
||||
if (this.workingRoles.get() === null) {
|
||||
const doc = InviteToBoardRolesSettings.findOne(INVITE_TO_BOARD_ROLES_ID);
|
||||
if (doc) {
|
||||
this.workingRoles.set((doc.allowedRoles || []).slice());
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Template.rolesGeneral.helpers({
|
||||
roleOptions() {
|
||||
const working = Template.instance().workingRoles.get() || [];
|
||||
// The role key doubles as the i18n key. 'board-admin' renders as
|
||||
// "Board Admin", deliberately distinct from the global Admin Panel admin.
|
||||
return INVITE_TO_BOARD_ROLES.map((key) => ({
|
||||
key,
|
||||
label: key,
|
||||
allowed: working.includes(key),
|
||||
}));
|
||||
},
|
||||
allRolesAllowed() {
|
||||
const working = Template.instance().workingRoles.get() || [];
|
||||
return INVITE_TO_BOARD_ROLES.every((key) => working.includes(key));
|
||||
},
|
||||
});
|
||||
|
||||
Template.rolesGeneral.events({
|
||||
'click a.js-toggle-role'(event, tpl) {
|
||||
event.preventDefault();
|
||||
const role = $(event.currentTarget).data('role');
|
||||
const working = (tpl.workingRoles.get() || []).slice();
|
||||
const idx = working.indexOf(role);
|
||||
if (idx >= 0) {
|
||||
working.splice(idx, 1);
|
||||
} else {
|
||||
working.push(role);
|
||||
}
|
||||
tpl.workingRoles.set(working);
|
||||
},
|
||||
'click a.js-toggle-all-roles'(event, tpl) {
|
||||
event.preventDefault();
|
||||
const working = tpl.workingRoles.get() || [];
|
||||
const allOn = INVITE_TO_BOARD_ROLES.every((key) => working.includes(key));
|
||||
tpl.workingRoles.set(allOn ? [] : INVITE_TO_BOARD_ROLES.slice());
|
||||
},
|
||||
'click .js-roles-save'(event, tpl) {
|
||||
event.preventDefault();
|
||||
InviteToBoardRolesSettings.update(INVITE_TO_BOARD_ROLES_ID, {
|
||||
$set: { allowedRoles: tpl.workingRoles.get() || [] },
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
Template.orgRow.helpers({
|
||||
|
||||
@@ -82,7 +82,7 @@ template(name="membersWidget")
|
||||
if currentUser.isBoardMember
|
||||
a.member.add-member.sandstorm-powerbox-request-identity(title="{{_ 'add-members'}}")
|
||||
i.fa.fa-plus
|
||||
else if currentUser.isBoardAdmin
|
||||
else if currentUser.canInviteToBoard
|
||||
a.member.add-member.js-manage-board-members(title="{{_ 'add-members'}}")
|
||||
i.fa.fa-plus
|
||||
.clearfix
|
||||
|
||||
@@ -447,6 +447,7 @@
|
||||
"error-board-doesNotExist": "This board does not exist",
|
||||
"error-board-notAdmin": "You need to be admin of this board to do that",
|
||||
"error-board-notAMember": "You need to be a member of this board to do that",
|
||||
"error-notAllowed": "Your board role is not allowed to do that",
|
||||
"error-json-malformed": "Your text is not valid JSON",
|
||||
"error-json-schema": "Your JSON data does not include the proper information in the correct format",
|
||||
"error-csv-schema": "Your CSV(Comma Separated Values)/TSV (Tab Separated Values) does not include the proper information in the correct format ",
|
||||
@@ -1078,6 +1079,11 @@
|
||||
"remove-all-read": "Remove all read",
|
||||
"allow-rename": "Allow Rename",
|
||||
"allowRenamePopup-title": "Allow Rename",
|
||||
"roles": "Roles",
|
||||
"roles-info": "Choose which board roles are allowed to perform each action. Admin Panel users (global admins) always have all rights and cannot be restricted here.",
|
||||
"allow-invite-to-board": "Allow Invite to Board",
|
||||
"all-board-members": "All Board Members",
|
||||
"board-admin": "Board Admin",
|
||||
"start-day-of-week": "Set day of the week start",
|
||||
"monday": "Monday",
|
||||
"tuesday": "Tuesday",
|
||||
|
||||
@@ -20,6 +20,7 @@ import '/models/customFields';
|
||||
import '/models/impersonatedUsers';
|
||||
import '/models/integrations';
|
||||
import '/models/invitationCodes';
|
||||
import '/models/inviteToBoardRolesSettings';
|
||||
import '/models/lists';
|
||||
import '/models/lockoutSettings';
|
||||
import '/models/org';
|
||||
|
||||
@@ -1242,6 +1242,25 @@ Boards.helpers({
|
||||
});
|
||||
},
|
||||
|
||||
// Returns the single BOARD-level role key of an active board member (see
|
||||
// INVITE_TO_BOARD_ROLES). Returns null if the user is not an active member.
|
||||
// Note: 'board-admin' is the board administrator role and is distinct from
|
||||
// the global site-admin flag `user.isAdmin`. A member with no restriction
|
||||
// flag set is a plain 'normal' member.
|
||||
memberRole(memberId) {
|
||||
const member = findWhere(this.members, { userId: memberId, isActive: true });
|
||||
if (!member) return null;
|
||||
if (member.isAdmin) return 'board-admin';
|
||||
if (member.isWorker) return 'worker';
|
||||
if (member.isCommentOnly) return 'comment-only';
|
||||
if (member.isNoComments) return 'no-comments';
|
||||
if (member.isNormalAssignedOnly) return 'normal-assigned-only';
|
||||
if (member.isCommentAssignedOnly) return 'comment-assigned-only';
|
||||
if (member.isReadOnly) return 'read-only';
|
||||
if (member.isReadAssignedOnly) return 'read-assigned-only';
|
||||
return 'normal';
|
||||
},
|
||||
|
||||
hasAnyAllowsDate() {
|
||||
const ret = this.allowsReceivedDate || this.allowsStartDate || this.allowsDueDate || this.allowsEndDate;
|
||||
return ret;
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import { Mongo } from 'meteor/mongo';
|
||||
const { SimpleSchema } = require('/imports/simpleSchema');
|
||||
|
||||
// All board-member role keys that a global admin may grant the "invite to
|
||||
// board" capability to. Order is the display order in the Admin Panel / People
|
||||
// / Roles tab. NOTE: these are BOARD-level roles, deliberately kept distinct
|
||||
// from the global site-admin flag `user.isAdmin`. 'board-admin' is the board
|
||||
// administrator role; 'normal' is a plain member with no restriction flags.
|
||||
export const INVITE_TO_BOARD_ROLES = [
|
||||
'board-admin',
|
||||
'normal',
|
||||
'worker',
|
||||
'comment-only',
|
||||
'no-comments',
|
||||
'normal-assigned-only',
|
||||
'comment-assigned-only',
|
||||
'read-only',
|
||||
'read-assigned-only',
|
||||
];
|
||||
|
||||
// The single settings document id.
|
||||
export const INVITE_TO_BOARD_ROLES_ID = 'inviteToBoardRoles';
|
||||
|
||||
// Secure default: only board admins and plain normal members may invite.
|
||||
export const INVITE_TO_BOARD_ROLES_DEFAULT = ['board-admin', 'normal'];
|
||||
|
||||
const InviteToBoardRolesSettings = new Mongo.Collection('inviteToBoardRolesSettings');
|
||||
|
||||
InviteToBoardRolesSettings.attachSchema(
|
||||
new SimpleSchema({
|
||||
_id: {
|
||||
type: String,
|
||||
},
|
||||
allowedRoles: {
|
||||
type: Array,
|
||||
optional: true,
|
||||
},
|
||||
'allowedRoles.$': {
|
||||
type: String,
|
||||
allowedValues: INVITE_TO_BOARD_ROLES,
|
||||
},
|
||||
sort: {
|
||||
type: Number,
|
||||
optional: true,
|
||||
},
|
||||
createdAt: {
|
||||
type: Date,
|
||||
optional: true,
|
||||
// eslint-disable-next-line consistent-return
|
||||
autoValue() {
|
||||
if (this.isInsert) {
|
||||
return new Date();
|
||||
} else if (this.isUpsert) {
|
||||
return { $setOnInsert: new Date() };
|
||||
} else {
|
||||
this.unset();
|
||||
}
|
||||
},
|
||||
},
|
||||
modifiedAt: {
|
||||
type: Date,
|
||||
// eslint-disable-next-line consistent-return
|
||||
autoValue() {
|
||||
if (this.isInsert || this.isUpsert || this.isUpdate) {
|
||||
return new Date();
|
||||
} else {
|
||||
this.unset();
|
||||
}
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
InviteToBoardRolesSettings.helpers({
|
||||
isRoleAllowed(role) {
|
||||
return (this.allowedRoles || []).includes(role);
|
||||
},
|
||||
});
|
||||
|
||||
// Returns the configured set of allowed roles, falling back to the secure
|
||||
// default if the settings document does not exist yet.
|
||||
InviteToBoardRolesSettings.allowedRoles = async function() {
|
||||
const doc = await InviteToBoardRolesSettings.findOneAsync(INVITE_TO_BOARD_ROLES_ID);
|
||||
return (doc && doc.allowedRoles) || INVITE_TO_BOARD_ROLES_DEFAULT;
|
||||
};
|
||||
|
||||
export default InviteToBoardRolesSettings;
|
||||
@@ -1,6 +1,10 @@
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { ReactiveCache, ReactiveMiniMongoIndex } from '/imports/reactiveCache';
|
||||
import Boards from '/models/boards';
|
||||
import InviteToBoardRolesSettings, {
|
||||
INVITE_TO_BOARD_ROLES_ID,
|
||||
INVITE_TO_BOARD_ROLES_DEFAULT,
|
||||
} from '/models/inviteToBoardRolesSettings';
|
||||
// import { Index, MongoDBEngine } from 'meteor/easy:search'; // Temporarily disabled due to compatibility issues
|
||||
const { SimpleSchema } = require('/imports/simpleSchema');
|
||||
const Users = Meteor.users;
|
||||
@@ -837,6 +841,28 @@ if (Meteor.isClient) {
|
||||
}
|
||||
return board && board.hasAdmin(this._id);
|
||||
},
|
||||
|
||||
// Whether this user may invite members to the board, mirroring the
|
||||
// server-side check in the inviteUserToBoard method: global site admins and
|
||||
// board admins always may; other board roles only if allowed by the
|
||||
// Admin Panel / People / Roles policy. Used to gate the UI add-member button.
|
||||
canInviteToBoard(boardId) {
|
||||
if (this.isAdmin) return true;
|
||||
let board;
|
||||
if (boardId) {
|
||||
board = ReactiveCache.getBoard(boardId);
|
||||
} else {
|
||||
const Utils = getUtils();
|
||||
board = Utils.getCurrentBoard();
|
||||
}
|
||||
if (!board) return false;
|
||||
if (board.hasAdmin(this._id)) return true;
|
||||
const role = board.memberRole(this._id);
|
||||
if (!role) return false;
|
||||
const doc = InviteToBoardRolesSettings.findOne(INVITE_TO_BOARD_ROLES_ID);
|
||||
const allowed = (doc && doc.allowedRoles) || INVITE_TO_BOARD_ROLES_DEFAULT;
|
||||
return allowed.includes(role);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -198,6 +198,7 @@ import '/server/publications/cards';
|
||||
import '/server/publications/cronJobs';
|
||||
import '/server/publications/cronMigrationStatus';
|
||||
import '/server/publications/customUI';
|
||||
import '/server/publications/inviteToBoardRolesSettings';
|
||||
import '/server/publications/lockoutSettings';
|
||||
import '/server/publications/migrationProgress';
|
||||
import '/server/publications/notifications';
|
||||
@@ -247,6 +248,7 @@ import '/server/permissions/checklists';
|
||||
import '/server/permissions/customFields';
|
||||
import '/server/permissions/integrations';
|
||||
import '/server/permissions/invitationCodes';
|
||||
import '/server/permissions/inviteToBoardRolesSettings';
|
||||
import '/server/permissions/lists';
|
||||
import '/server/permissions/lockoutSettings';
|
||||
import '/server/permissions/org';
|
||||
|
||||
@@ -10,6 +10,10 @@ import PositionHistory from '/models/positionHistory';
|
||||
import Presences from '/models/presences';
|
||||
import Rules from '/models/rules';
|
||||
import TableVisibilityModeSettings from '/models/tableVisibilityModeSettings';
|
||||
import InviteToBoardRolesSettings, {
|
||||
INVITE_TO_BOARD_ROLES_ID,
|
||||
INVITE_TO_BOARD_ROLES_DEFAULT,
|
||||
} from '/models/inviteToBoardRolesSettings';
|
||||
import Triggers from '/models/triggers';
|
||||
import UnsavedEditCollection from '/models/unsavedEdits';
|
||||
|
||||
@@ -130,6 +134,12 @@ Meteor.startup(async () => {
|
||||
{ $setOnInsert: { booleanValue: false, sort: 0 } },
|
||||
);
|
||||
|
||||
await InviteToBoardRolesSettings._collection.createIndexAsync({ modifiedAt: -1 });
|
||||
await InviteToBoardRolesSettings.upsertAsync(
|
||||
{ _id: INVITE_TO_BOARD_ROLES_ID },
|
||||
{ $setOnInsert: { allowedRoles: INVITE_TO_BOARD_ROLES_DEFAULT, sort: 0 } },
|
||||
);
|
||||
|
||||
await Triggers._collection.createIndexAsync({ modifiedAt: -1 });
|
||||
|
||||
await UnsavedEditCollection._collection.createIndexAsync({ modifiedAt: -1 });
|
||||
|
||||
+21
-5
@@ -14,6 +14,7 @@ import EmailLocalization from '/server/lib/emailLocalization';
|
||||
import ImpersonatedUsers from '/models/impersonatedUsers';
|
||||
import Boards from '/models/boards';
|
||||
import InvitationCodes from '/models/invitationCodes';
|
||||
import InviteToBoardRolesSettings from '/models/inviteToBoardRolesSettings';
|
||||
import Lists from '/models/lists';
|
||||
import Swimlanes from '/models/swimlanes';
|
||||
import Users, { allowedSortValues } from '/models/users';
|
||||
@@ -614,8 +615,19 @@ Meteor.methods({
|
||||
}
|
||||
const inviter = await ReactiveCache.getCurrentUser();
|
||||
const board = await ReactiveCache.getBoard(boardId);
|
||||
const member = board.members.find(memberItem => memberItem.userId === inviter._id);
|
||||
if (!member || !member.isActive) throw new Meteor.Error('error-board-notAMember');
|
||||
|
||||
// Global site admins (Admin Panel users) always have all rights and bypass
|
||||
// both the board-membership and the configurable per-role check below.
|
||||
if (!inviter.isAdmin) {
|
||||
const member = board.members.find(memberItem => memberItem.userId === inviter._id);
|
||||
if (!member || !member.isActive) throw new Meteor.Error('error-board-notAMember');
|
||||
// Enforce the Admin Panel / People / Roles policy: only the configured
|
||||
// board roles are allowed to invite users to a board.
|
||||
const allowedRoles = await InviteToBoardRolesSettings.allowedRoles();
|
||||
if (!allowedRoles.includes(board.memberRole(inviter._id))) {
|
||||
throw new Meteor.Error('error-notAllowed');
|
||||
}
|
||||
}
|
||||
|
||||
this.unblock();
|
||||
|
||||
@@ -1505,9 +1517,13 @@ Meteor.methods({
|
||||
|
||||
const currentUser = await ReactiveCache.getCurrentUser();
|
||||
const board = await ReactiveCache.getBoard(boardId);
|
||||
const member = board.members.find(memberItem => memberItem.userId === currentUser._id);
|
||||
if (!member || !member.isActive) {
|
||||
throw new Meteor.Error('not-authorized', 'User is not a member of this board');
|
||||
// Global site admins bypass the board-membership check, mirroring
|
||||
// inviteUserToBoard.
|
||||
if (!currentUser.isAdmin) {
|
||||
const member = board.members.find(memberItem => memberItem.userId === currentUser._id);
|
||||
if (!member || !member.isActive) {
|
||||
throw new Meteor.Error('not-authorized', 'User is not a member of this board');
|
||||
}
|
||||
}
|
||||
|
||||
if (query.length < 2) {
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import InviteToBoardRolesSettings from '/models/inviteToBoardRolesSettings';
|
||||
|
||||
// Only global site admins (Admin Panel users) may change which board roles are
|
||||
// allowed to invite users to a board.
|
||||
InviteToBoardRolesSettings.allow({
|
||||
async update(userId) {
|
||||
const user = await Meteor.users.findOneAsync(userId);
|
||||
return user && user.isAdmin;
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,5 @@
|
||||
import InviteToBoardRolesSettings from '/models/inviteToBoardRolesSettings';
|
||||
|
||||
Meteor.publish('inviteToBoardRolesSettings', function() {
|
||||
return InviteToBoardRolesSettings.find();
|
||||
});
|
||||
Reference in new issue
Block a user