Files
wekan/server/methods/instanceFeatures.js
Lauri Ojansivu f4a3690256 Rules can be shown to board members, read-only, when the Admin Panel allows it.
Admin Panel / Settings / Visibility / Rules gets "Boards may show their
Rules to members, read-only", off by default (Settings
rulesReadOnlyForMembers, site admin only). With it on, a board admin ticks
"Show these Rules to board members, read-only" on the board's Rules page
(rulesVisibleToMembers). Members of that board then get Rules in Board
Settings and see the rules list and each rule's details, with no control
that changes a rule; the methods that change rules keep their board-admin
checks, so nothing is granted. Without both ticks only board and site
admins see the Rules page, which until now drew its list for any member
who opened the address.

models/lib/rulesReadOnly.js decides 'edit', 'read' or 'none', and
client/lib/rulesAccess.js is the one place the Rules page, Board Settings
and rule details ask. tests/rulesReadOnly.test.cjs covers both switches,
each alone, a non-member, and that a reader's page has no edit control.
boardMenuOrder now leaves the reader's alternative branch out of the
admin's sequence and pins it alone; instanceFeatures counts the fourth
site-admin method; roleHelperRegressions follows the canAdmin check to
rulesAccess.js. The new strings are English placeholders until the next
translation pass.

Thanks to justinr1234 and xet7 !

Fixes #2217.
2026-10-14 14:23:31 +03:00

91 lines
4.1 KiB
JavaScript

// #6736: Admin Panel / Settings / Visibility / Features. Site admin only.
// The decisions are models/lib/instanceFeatures.js; this stores them and the
// pilot users who preview disabled features (a flag on the user, published
// only to that user, so nobody learns who the pilots are).
import { Meteor } from 'meteor/meteor';
import { check, Match } from 'meteor/check';
import Settings from '/models/settings';
const { featureSettingsModifier, parsePilotUsernames } = require('/models/lib/instanceFeatures');
const { cardFieldStatesValue } = require('/models/lib/cardFieldVisibility');
const { cleanOptionalApis } = require('/models/lib/optionalApis');
async function requireSiteAdmin() {
const user = await Meteor.userAsync();
if (user?.isAdmin !== true) throw new Meteor.Error('error-notAuthorized');
return user;
}
Meteor.methods({
// The usernames of the current pilot users, for the page's text box.
async getFeaturePilotUsernames() {
await requireSiteAdmin();
const users = await Meteor.users.find({ featurePreview: true },
{ fields: { username: 1 }, sort: { username: 1 }, limit: 500 }).fetchAsync();
return users.map(user => user.username).filter(Boolean);
},
// Saves every feature's decision, the two policies and the pilot users.
// Unknown usernames are returned rather than silently ignored.
// Admin Panel / Settings / Visibility / Optional APIs: which compatible
// APIs (Trello, Jira, GitHub, Gitea) answer. Site admins only; unknown keys
// are dropped and every value is a boolean (models/lib/optionalApis.js).
async saveOptionalApis(input) {
await requireSiteAdmin();
check(input, Object);
const setting = await Settings.findOneAsync({});
if (!setting) throw new Meteor.Error('settings-not-found');
const optionalApis = cleanOptionalApis(input);
await Settings.updateAsync(setting._id, { $set: { optionalApis } });
return optionalApis;
},
// #2217: Admin Panel / Settings / Visibility - boards may show their Rules
// to members, read-only (models/lib/rulesReadOnly.js). Site admins only.
async saveRulesReadOnlyForMembers(enabled) {
await requireSiteAdmin();
check(enabled, Boolean);
const setting = await Settings.findOneAsync({});
if (!setting) throw new Meteor.Error('settings-not-found');
await Settings.updateAsync(setting._id, { $set: { rulesReadOnlyForMembers: enabled } });
return enabled;
},
async saveInstanceFeatures(input) {
await requireSiteAdmin();
check(input, {
states: Object,
approvalRequired: Boolean,
previewAdmins: Boolean,
pilotUsernames: Match.Optional(String),
// The card fields every board shows (models/lib/cardFieldVisibility.js).
cardFields: Match.Optional(Object),
});
const { pilotUsernames, cardFields, ...decisions } = input;
const modifier = featureSettingsModifier(decisions);
if (modifier.error) throw new Meteor.Error('invalid-features', modifier.error);
if (cardFields !== undefined) {
const value = cardFieldStatesValue(cardFields);
if (value.error) throw new Meteor.Error('invalid-card-fields', value.error);
modifier.$set.cardFieldStates = value.cardFieldStates;
}
const setting = await Settings.findOneAsync({});
if (!setting) throw new Meteor.Error('settings-not-found');
await Settings.updateAsync(setting._id, modifier);
let unknownUsernames = [];
if (typeof pilotUsernames === 'string') {
const names = parsePilotUsernames(pilotUsernames);
const pilots = names.length ? await Meteor.users.find({ username: { $in: names } },
{ fields: { _id: 1, username: 1 } }).fetchAsync() : [];
const ids = pilots.map(user => user._id);
unknownUsernames = names.filter(name => !pilots.some(user => user.username === name));
await Meteor.users.updateAsync({ featurePreview: true, _id: { $nin: ids } },
{ $unset: { featurePreview: '' } }, { multi: true });
if (ids.length) {
await Meteor.users.updateAsync({ _id: { $in: ids } }, { $set: { featurePreview: true } }, { multi: true });
}
}
return { saved: true, unknownUsernames };
},
});