mirror of
https://github.com/NodeBB/NodeBB.git
synced 2025-10-26 16:46:12 +01:00
feat: privileges for Admin Control Panel (#8355)
* feat: acp privileges (WIP)
* fix: restore global privilege hooks
* refactor: using cid 0 in admin privs
* fix: no need for zebrastripe-reset
* feat: manage:categories privilege WIP
* feat: renamed prefix to admin:, settigns and dashboard privs
* fix: nofocus on acp privs group find modal
* refactor: privileges.x.get() to not used hardcoded privs
* fix: crash if unable to get latest version
* feat: setting acp priv
* Revert "fix: crash if unable to get latest version"
This reverts commit afdb235f48.
* feat: user/privilege acp privs
* fix: category selector in manage/privileges
* fix: guests potentially becoming admins
* fix: bug in setting admin privs
* fix: some last minute things + api docs
* fix: some more last minute fixes
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"global": "Global",
|
"global": "Global",
|
||||||
"global.no-users": "No user-specific global privileges.",
|
"global.no-users": "No user-specific global privileges.",
|
||||||
|
"admin": "Admin",
|
||||||
"group-privileges": "Group Privileges",
|
"group-privileges": "Group Privileges",
|
||||||
"user-privileges": "User Privileges",
|
"user-privileges": "User Privileges",
|
||||||
"chat": "Chat",
|
"chat": "Chat",
|
||||||
@@ -31,5 +32,11 @@
|
|||||||
"downvote-posts": "Downvote Posts",
|
"downvote-posts": "Downvote Posts",
|
||||||
"delete-topics": "Delete Topics",
|
"delete-topics": "Delete Topics",
|
||||||
"purge": "Purge",
|
"purge": "Purge",
|
||||||
"moderate": "Moderate"
|
"moderate": "Moderate",
|
||||||
|
|
||||||
|
"admin-dashboard": "Dashboard",
|
||||||
|
"admin-categories": "Categories",
|
||||||
|
"admin-privileges": "Privileges",
|
||||||
|
"admin-users": "Users",
|
||||||
|
"admin-settings": "Settings"
|
||||||
}
|
}
|
||||||
@@ -354,6 +354,8 @@ paths:
|
|||||||
timestampISO:
|
timestampISO:
|
||||||
type: string
|
type: string
|
||||||
description: An ISO 8601 formatted date string (complementing `timestamp`)
|
description: An ISO 8601 formatted date string (complementing `timestamp`)
|
||||||
|
showSystemControls:
|
||||||
|
type: boolean
|
||||||
- $ref: components/schemas/CommonProps.yaml#/CommonProps
|
- $ref: components/schemas/CommonProps.yaml#/CommonProps
|
||||||
/api/admin/settings/languages:
|
/api/admin/settings/languages:
|
||||||
get:
|
get:
|
||||||
|
|||||||
@@ -11,12 +11,15 @@ define('admin/manage/privileges', [
|
|||||||
var cid;
|
var cid;
|
||||||
|
|
||||||
Privileges.init = function () {
|
Privileges.init = function () {
|
||||||
cid = ajaxify.data.cid || 0;
|
cid = ajaxify.data.cid || 'admin';
|
||||||
|
|
||||||
categorySelector.init($('[component="category-selector"]'), function (category) {
|
categorySelector.init($('[component="category-selector"]'), function (category) {
|
||||||
var cid = parseInt(category.cid, 10);
|
cid = parseInt(category.cid, 10);
|
||||||
ajaxify.go('admin/manage/privileges/' + (cid || ''));
|
cid = isNaN(cid) ? 'admin' : cid;
|
||||||
|
Privileges.refreshPrivilegeTable();
|
||||||
|
ajaxify.updateHistory('admin/manage/privileges/' + (cid || ''));
|
||||||
});
|
});
|
||||||
|
|
||||||
Privileges.setupPrivilegeTable();
|
Privileges.setupPrivilegeTable();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -81,7 +84,8 @@ define('admin/manage/privileges', [
|
|||||||
if (err) {
|
if (err) {
|
||||||
return app.alertError(err.message);
|
return app.alertError(err.message);
|
||||||
}
|
}
|
||||||
var tpl = cid ? 'admin/partials/categories/privileges' : 'admin/partials/global/privileges';
|
|
||||||
|
var tpl = parseInt(cid, 10) ? 'admin/partials/privileges/category' : 'admin/partials/privileges/global';
|
||||||
Benchpress.parse(tpl, {
|
Benchpress.parse(tpl, {
|
||||||
privileges: privileges,
|
privileges: privileges,
|
||||||
}, function (html) {
|
}, function (html) {
|
||||||
@@ -117,7 +121,7 @@ define('admin/manage/privileges', [
|
|||||||
|
|
||||||
Privileges.setPrivilege = function (member, privilege, state, checkboxEl) {
|
Privileges.setPrivilege = function (member, privilege, state, checkboxEl) {
|
||||||
socket.emit('admin.categories.setPrivilege', {
|
socket.emit('admin.categories.setPrivilege', {
|
||||||
cid: cid,
|
cid: isNaN(cid) ? 0 : cid,
|
||||||
privilege: privilege,
|
privilege: privilege,
|
||||||
set: state,
|
set: state,
|
||||||
member: member,
|
member: member,
|
||||||
@@ -143,9 +147,14 @@ define('admin/manage/privileges', [
|
|||||||
inputEl.focus();
|
inputEl.focus();
|
||||||
|
|
||||||
autocomplete.user(inputEl, function (ev, ui) {
|
autocomplete.user(inputEl, function (ev, ui) {
|
||||||
var defaultPrivileges = cid ? ['find', 'read', 'topics:read'] : ['chat'];
|
var defaultPrivileges;
|
||||||
|
if (ajaxify.data.url === '/admin/manage/privileges/admin') {
|
||||||
|
defaultPrivileges = ['admin:dashboard'];
|
||||||
|
} else {
|
||||||
|
defaultPrivileges = cid ? ['find', 'read', 'topics:read'] : ['chat'];
|
||||||
|
}
|
||||||
socket.emit('admin.categories.setPrivilege', {
|
socket.emit('admin.categories.setPrivilege', {
|
||||||
cid: cid,
|
cid: isNaN(cid) ? 0 : cid,
|
||||||
privilege: defaultPrivileges,
|
privilege: defaultPrivileges,
|
||||||
set: true,
|
set: true,
|
||||||
member: ui.item.user.uid,
|
member: ui.item.user.uid,
|
||||||
@@ -170,11 +179,18 @@ define('admin/manage/privileges', [
|
|||||||
|
|
||||||
modal.on('shown.bs.modal', function () {
|
modal.on('shown.bs.modal', function () {
|
||||||
var inputEl = modal.find('input');
|
var inputEl = modal.find('input');
|
||||||
|
inputEl.focus();
|
||||||
|
|
||||||
autocomplete.group(inputEl, function (ev, ui) {
|
autocomplete.group(inputEl, function (ev, ui) {
|
||||||
var defaultPrivileges = cid ? ['groups:find', 'groups:read', 'groups:topics:read'] : ['groups:chat'];
|
var defaultPrivileges;
|
||||||
|
if (ajaxify.data.url === '/admin/manage/privileges/admin') {
|
||||||
|
defaultPrivileges = ['groups:admin:dashboard'];
|
||||||
|
} else {
|
||||||
|
defaultPrivileges = cid ? ['groups:find', 'groups:read', 'groups:topics:read'] : ['groups:chat'];
|
||||||
|
}
|
||||||
|
|
||||||
socket.emit('admin.categories.setPrivilege', {
|
socket.emit('admin.categories.setPrivilege', {
|
||||||
cid: cid,
|
cid: isNaN(cid) ? 0 : cid,
|
||||||
privilege: defaultPrivileges,
|
privilege: defaultPrivileges,
|
||||||
set: true,
|
set: true,
|
||||||
member: ui.item.group.name,
|
member: ui.item.group.name,
|
||||||
|
|||||||
@@ -46,6 +46,10 @@ define('admin/modules/search', ['mousetrap'], function (mousetrap) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
search.init = function () {
|
search.init = function () {
|
||||||
|
if (!app.user.privileges['admin:settings']) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
socket.emit('admin.getSearchDict', {}, function (err, dict) {
|
socket.emit('admin.getSearchDict', {}, function (err, dict) {
|
||||||
if (err) {
|
if (err) {
|
||||||
app.alertError(err);
|
app.alertError(err);
|
||||||
|
|||||||
@@ -188,7 +188,7 @@
|
|||||||
var spidersEnabled = ['groups:find', 'groups:read', 'groups:topics:read', 'groups:view:users', 'groups:view:tags', 'groups:view:groups'];
|
var spidersEnabled = ['groups:find', 'groups:read', 'groups:topics:read', 'groups:view:users', 'groups:view:tags', 'groups:view:groups'];
|
||||||
var globalModDisabled = ['groups:moderate'];
|
var globalModDisabled = ['groups:moderate'];
|
||||||
var disabled =
|
var disabled =
|
||||||
(member === 'guests' && guestDisabled.includes(priv.name)) ||
|
(member === 'guests' && (guestDisabled.includes(priv.name) || priv.name.startsWith('groups:admin:'))) ||
|
||||||
(member === 'spiders' && !spidersEnabled.includes(priv.name)) ||
|
(member === 'spiders' && !spidersEnabled.includes(priv.name)) ||
|
||||||
(member === 'Global Moderators' && globalModDisabled.includes(priv.name));
|
(member === 'Global Moderators' && globalModDisabled.includes(priv.name));
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
const privileges = require('../privileges');
|
||||||
|
const helpers = require('./helpers');
|
||||||
|
|
||||||
var adminController = {
|
var adminController = {
|
||||||
dashboard: require('./admin/dashboard'),
|
dashboard: require('./admin/dashboard'),
|
||||||
categories: require('./admin/categories'),
|
categories: require('./admin/categories'),
|
||||||
@@ -28,5 +31,22 @@ var adminController = {
|
|||||||
info: require('./admin/info'),
|
info: require('./admin/info'),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
adminController.routeIndex = async (req, res) => {
|
||||||
|
const privilegeSet = await privileges.admin.get(req.uid);
|
||||||
|
|
||||||
|
if (privilegeSet.superadmin || privilegeSet['admin:dashboard']) {
|
||||||
|
return adminController.dashboard.get(req, res);
|
||||||
|
} else if (privilegeSet['admin:categories']) {
|
||||||
|
return helpers.redirect(res, 'admin/manage/categories');
|
||||||
|
} else if (privilegeSet['admin:privileges']) {
|
||||||
|
return helpers.redirect(res, 'admin/manage/privileges');
|
||||||
|
} else if (privilegeSet['admin:users']) {
|
||||||
|
return helpers.redirect(res, 'admin/manage/users');
|
||||||
|
} else if (privilegeSet['admin:settings']) {
|
||||||
|
return helpers.redirect(res, 'admin/settings/general');
|
||||||
|
}
|
||||||
|
|
||||||
|
return helpers.notAllowed(req, res);
|
||||||
|
};
|
||||||
|
|
||||||
module.exports = adminController;
|
module.exports = adminController;
|
||||||
|
|||||||
@@ -16,11 +16,12 @@ const utils = require('../../utils');
|
|||||||
const dashboardController = module.exports;
|
const dashboardController = module.exports;
|
||||||
|
|
||||||
dashboardController.get = async function (req, res) {
|
dashboardController.get = async function (req, res) {
|
||||||
const [stats, notices, latestVersion, lastrestart] = await Promise.all([
|
const [stats, notices, latestVersion, lastrestart, isAdmin] = await Promise.all([
|
||||||
getStats(),
|
getStats(),
|
||||||
getNotices(),
|
getNotices(),
|
||||||
getLatestVersion(),
|
getLatestVersion(),
|
||||||
getLastRestart(),
|
getLastRestart(),
|
||||||
|
user.isAdministrator(),
|
||||||
]);
|
]);
|
||||||
const version = nconf.get('version');
|
const version = nconf.get('version');
|
||||||
|
|
||||||
@@ -34,6 +35,7 @@ dashboardController.get = async function (req, res) {
|
|||||||
stats: stats,
|
stats: stats,
|
||||||
canRestart: !!process.send,
|
canRestart: !!process.send,
|
||||||
lastrestart: lastrestart,
|
lastrestart: lastrestart,
|
||||||
|
showSystemControls: isAdmin,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -6,9 +6,18 @@ const privileges = require('../../privileges');
|
|||||||
const privilegesController = module.exports;
|
const privilegesController = module.exports;
|
||||||
|
|
||||||
privilegesController.get = async function (req, res) {
|
privilegesController.get = async function (req, res) {
|
||||||
const cid = req.params.cid ? parseInt(req.params.cid, 10) : 0;
|
const cid = req.params.cid ? parseInt(req.params.cid, 10) || 0 : 0;
|
||||||
|
const isAdminPriv = req.params.cid === 'admin';
|
||||||
|
|
||||||
|
let method;
|
||||||
|
if (cid > 0) {
|
||||||
|
method = privileges.categories.list.bind(null, cid);
|
||||||
|
} else if (cid === 0) {
|
||||||
|
method = isAdminPriv ? privileges.admin.list : privileges.global.list;
|
||||||
|
}
|
||||||
|
|
||||||
const [privilegesData, categoriesData] = await Promise.all([
|
const [privilegesData, categoriesData] = await Promise.all([
|
||||||
cid ? privileges.categories.list(cid) : privileges.global.list(),
|
method(),
|
||||||
categories.buildForSelectAll(),
|
categories.buildForSelectAll(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -16,12 +25,16 @@ privilegesController.get = async function (req, res) {
|
|||||||
cid: 0,
|
cid: 0,
|
||||||
name: '[[admin/manage/privileges:global]]',
|
name: '[[admin/manage/privileges:global]]',
|
||||||
icon: 'fa-list',
|
icon: 'fa-list',
|
||||||
|
}, {
|
||||||
|
cid: 'admin', // what do?
|
||||||
|
name: '[[admin/manage/privileges:admin]]',
|
||||||
|
icon: 'fa-lock',
|
||||||
});
|
});
|
||||||
|
|
||||||
let selectedCategory;
|
let selectedCategory;
|
||||||
categoriesData.forEach(function (category) {
|
categoriesData.forEach(function (category) {
|
||||||
if (category) {
|
if (category) {
|
||||||
category.selected = category.cid === cid;
|
category.selected = category.cid === (!isAdminPriv ? cid : 'admin');
|
||||||
|
|
||||||
if (category.selected) {
|
if (category.selected) {
|
||||||
selectedCategory = category;
|
selectedCategory = category;
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ var semver = require('semver');
|
|||||||
var user = require('../user');
|
var user = require('../user');
|
||||||
var meta = require('../meta');
|
var meta = require('../meta');
|
||||||
var plugins = require('../plugins');
|
var plugins = require('../plugins');
|
||||||
|
var privileges = require('../privileges');
|
||||||
var utils = require('../../public/src/utils');
|
var utils = require('../../public/src/utils');
|
||||||
var versions = require('../admin/versions');
|
var versions = require('../admin/versions');
|
||||||
var helpers = require('./helpers');
|
var helpers = require('./helpers');
|
||||||
@@ -43,11 +44,13 @@ module.exports = function (middleware) {
|
|||||||
custom_header: plugins.fireHook('filter:admin.header.build', custom_header),
|
custom_header: plugins.fireHook('filter:admin.header.build', custom_header),
|
||||||
configs: meta.configs.list(),
|
configs: meta.configs.list(),
|
||||||
latestVersion: getLatestVersion(),
|
latestVersion: getLatestVersion(),
|
||||||
|
privileges: privileges.admin.get(req.uid),
|
||||||
});
|
});
|
||||||
|
|
||||||
var userData = results.userData;
|
var userData = results.userData;
|
||||||
userData.uid = req.uid;
|
userData.uid = req.uid;
|
||||||
userData['email:confirmed'] = userData['email:confirmed'] === 1;
|
userData['email:confirmed'] = userData['email:confirmed'] === 1;
|
||||||
|
userData.privileges = results.privileges;
|
||||||
|
|
||||||
var acpPath = req.path.slice(1).split('/');
|
var acpPath = req.path.slice(1).split('/');
|
||||||
acpPath.forEach(function (path, i) {
|
acpPath.forEach(function (path, i) {
|
||||||
@@ -103,4 +106,26 @@ module.exports = function (middleware) {
|
|||||||
middleware.admin.renderFooter = async function (req, res, data) {
|
middleware.admin.renderFooter = async function (req, res, data) {
|
||||||
return await req.app.renderAsync('admin/footer', data);
|
return await req.app.renderAsync('admin/footer', data);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
middleware.admin.checkPrivileges = async (req, res, next) => {
|
||||||
|
// Kick out guests, obviously
|
||||||
|
if (!req.uid) {
|
||||||
|
return controllers.helpers.notAllowed(req, res);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Users in "administrators" group are considered super admins
|
||||||
|
const isAdmin = await user.isAdministrator(req.uid);
|
||||||
|
if (isAdmin) {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, check for privilege based on page (if not in mapping, deny access)
|
||||||
|
const path = req.path.replace(/^(\/api)?\/admin\/?/g, '');
|
||||||
|
const privilege = privileges.admin.resolve(path);
|
||||||
|
if (!privilege || !await privileges.admin.can(privilege, req.uid)) {
|
||||||
|
return controllers.helpers.notAllowed(req, res);
|
||||||
|
}
|
||||||
|
|
||||||
|
return next();
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
183
src/privileges/admin.js
Normal file
183
src/privileges/admin.js
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const _ = require('lodash');
|
||||||
|
|
||||||
|
const user = require('../user');
|
||||||
|
const groups = require('../groups');
|
||||||
|
const helpers = require('./helpers');
|
||||||
|
const plugins = require('../plugins');
|
||||||
|
const utils = require('../utils');
|
||||||
|
|
||||||
|
module.exports = function (privileges) {
|
||||||
|
privileges.admin = {};
|
||||||
|
|
||||||
|
privileges.admin.privilegeLabels = [
|
||||||
|
{ name: '[[admin/manage/privileges:admin-dashboard]]' },
|
||||||
|
{ name: '[[admin/manage/privileges:admin-categories]]' },
|
||||||
|
{ name: '[[admin/manage/privileges:admin-privileges]]' },
|
||||||
|
{ name: '[[admin/manage/privileges:admin-users]]' },
|
||||||
|
{ name: '[[admin/manage/privileges:admin-settings]]' },
|
||||||
|
];
|
||||||
|
|
||||||
|
privileges.admin.userPrivilegeList = [
|
||||||
|
'admin:dashboard',
|
||||||
|
'admin:categories',
|
||||||
|
'admin:privileges',
|
||||||
|
'admin:users',
|
||||||
|
'admin:settings',
|
||||||
|
];
|
||||||
|
|
||||||
|
privileges.admin.groupPrivilegeList = privileges.admin.userPrivilegeList.map(privilege => 'groups:' + privilege);
|
||||||
|
|
||||||
|
// Mapping for a page route (via direct match or regexp) to a privilege
|
||||||
|
privileges.admin.routeMap = {
|
||||||
|
dashboard: 'admin:dashboard',
|
||||||
|
'manage/categories': 'admin:categories',
|
||||||
|
'manage/privileges': 'admin:privileges',
|
||||||
|
'manage/users': 'admin:users',
|
||||||
|
'extend/plugins': 'admin:settings',
|
||||||
|
'extend/widgets': 'admin:settings',
|
||||||
|
'extend/rewards': 'admin:settings',
|
||||||
|
};
|
||||||
|
privileges.admin.routeRegexpMap = {
|
||||||
|
'^manage/categories/\\d+': 'admin:categories',
|
||||||
|
'^manage/privileges/\\d+': 'admin:privileges',
|
||||||
|
'^settings/[\\w\\-]+$': 'admin:settings',
|
||||||
|
'^appearance/[\\w]+$': 'admin:settings',
|
||||||
|
'^plugins/[\\w\\-]+$': 'admin:settings',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mapping for socket call methods to a privilege
|
||||||
|
// In NodeBB v2, these socket calls will be removed in favour of xhr calls
|
||||||
|
privileges.admin.socketMap = {
|
||||||
|
'admin.rooms.getAll': 'admin:dashboard',
|
||||||
|
'admin.analytics.get': 'admin:dashboard',
|
||||||
|
|
||||||
|
'admin.categories.getAll': 'admin:categories',
|
||||||
|
'admin.categories.create': 'admin:categories',
|
||||||
|
'admin.categories.update': 'admin:categories',
|
||||||
|
'admin.categories.purge': 'admin:categories',
|
||||||
|
'admin.categories.copySettingsFrom': 'admin:categories',
|
||||||
|
|
||||||
|
'admin.categories.getPrivilegeSettings': 'admin:privileges',
|
||||||
|
'admin.categories.setPrivilege': 'admin:privileges',
|
||||||
|
'admin.categories.copyPrivilegesToChildren': 'admin:privileges',
|
||||||
|
'admin.categories.copyPrivilegesFrom': 'admin:privileges',
|
||||||
|
'admin.categories.copyPrivilegesToAllCategories': 'admin:privileges',
|
||||||
|
|
||||||
|
'admin.user.loadGroups': 'admin:users',
|
||||||
|
'admin.groups.join': 'admin:users',
|
||||||
|
'admin.groups.leave': 'admin:users',
|
||||||
|
'user.banUsers': 'admin:users',
|
||||||
|
'user.unbanUsers': 'admin:users',
|
||||||
|
'admin.user.resetLockouts': 'admin:users',
|
||||||
|
'admin.user.validateEmail': 'admin:users',
|
||||||
|
'admin.user.sendValidationEmail': 'admin:users',
|
||||||
|
'admin.user.sendPasswordResetEmail': 'admin:users',
|
||||||
|
'admin.user.forcePasswordReset': 'admin:users',
|
||||||
|
'admin.user.deleteUsers': 'admin:users',
|
||||||
|
'admin.user.deleteUsersAndContent': 'admin:users',
|
||||||
|
'admin.user.createUser': 'admin:users',
|
||||||
|
'admin.user.search': 'admin:users',
|
||||||
|
'admin.user.invite': 'admin:users',
|
||||||
|
|
||||||
|
'admin.getSearchDict': 'admin:settings',
|
||||||
|
'admin.config.setMultiple': 'admin:settings',
|
||||||
|
'admin.config.remove': 'admin:settings',
|
||||||
|
'admin.themes.getInstalled': 'admin:settings',
|
||||||
|
'admin.themes.set': 'admin:settings',
|
||||||
|
'admin.reloadAllSessions': 'admin:settings',
|
||||||
|
'admin.settings.get': 'admin:settings',
|
||||||
|
};
|
||||||
|
|
||||||
|
privileges.admin.resolve = (path) => {
|
||||||
|
if (privileges.admin.routeMap[path]) {
|
||||||
|
return privileges.admin.routeMap[path];
|
||||||
|
} else if (path === '') {
|
||||||
|
return 'manage:dashboard';
|
||||||
|
}
|
||||||
|
|
||||||
|
let privilege;
|
||||||
|
Object.keys(privileges.admin.routeRegexpMap).forEach((regexp) => {
|
||||||
|
if (!privilege) {
|
||||||
|
if (new RegExp(regexp).test(path)) {
|
||||||
|
privilege = privileges.admin.routeRegexpMap[regexp];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return privilege;
|
||||||
|
};
|
||||||
|
|
||||||
|
privileges.admin.list = async function () {
|
||||||
|
async function getLabels() {
|
||||||
|
return await utils.promiseParallel({
|
||||||
|
users: plugins.fireHook('filter:privileges.admin.list_human', privileges.admin.privilegeLabels.slice()),
|
||||||
|
groups: plugins.fireHook('filter:privileges.admin.groups.list_human', privileges.admin.privilegeLabels.slice()),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const payload = await utils.promiseParallel({
|
||||||
|
labels: getLabels(),
|
||||||
|
users: helpers.getUserPrivileges(0, 'filter:privileges.admin.list', privileges.admin.userPrivilegeList),
|
||||||
|
groups: helpers.getGroupPrivileges(0, 'filter:privileges.admin.groups.list', privileges.admin.groupPrivilegeList),
|
||||||
|
});
|
||||||
|
// This is a hack because I can't do {labels.users.length} to echo the count in templates.js
|
||||||
|
payload.columnCount = payload.labels.users.length + 2;
|
||||||
|
return payload;
|
||||||
|
};
|
||||||
|
|
||||||
|
privileges.admin.get = async function (uid) {
|
||||||
|
const [userPrivileges, isAdministrator] = await Promise.all([
|
||||||
|
helpers.isUserAllowedTo(privileges.admin.userPrivilegeList, uid, 0),
|
||||||
|
user.isAdministrator(uid),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const combined = userPrivileges.map(allowed => allowed || isAdministrator);
|
||||||
|
const privData = _.zipObject(privileges.admin.userPrivilegeList, combined);
|
||||||
|
|
||||||
|
privData.superadmin = isAdministrator;
|
||||||
|
return await plugins.fireHook('filter:privileges.admin.get', privData);
|
||||||
|
};
|
||||||
|
|
||||||
|
privileges.admin.can = async function (privilege, uid) {
|
||||||
|
const isUserAllowedTo = await helpers.isUserAllowedTo(privilege, uid, [0]);
|
||||||
|
return isUserAllowedTo[0];
|
||||||
|
};
|
||||||
|
|
||||||
|
// privileges.admin.canGroup = async function (privilege, groupName) {
|
||||||
|
// return await groups.isMember(groupName, 'cid:0:privileges:groups:' + privilege);
|
||||||
|
// };
|
||||||
|
|
||||||
|
privileges.admin.give = async function (privileges, groupName) {
|
||||||
|
await helpers.giveOrRescind(groups.join, privileges, 'admin', groupName);
|
||||||
|
plugins.fireHook('action:privileges.admin.give', {
|
||||||
|
privileges: privileges,
|
||||||
|
groupNames: Array.isArray(groupName) ? groupName : [groupName],
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
privileges.admin.rescind = async function (privileges, groupName) {
|
||||||
|
await helpers.giveOrRescind(groups.leave, privileges, 'admin', groupName);
|
||||||
|
plugins.fireHook('action:privileges.admin.rescind', {
|
||||||
|
privileges: privileges,
|
||||||
|
groupNames: Array.isArray(groupName) ? groupName : [groupName],
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// privileges.admin.userPrivileges = async function (uid) {
|
||||||
|
// const tasks = {};
|
||||||
|
// privileges.admin.userPrivilegeList.forEach(function (privilege) {
|
||||||
|
// tasks[privilege] = groups.isMember(uid, 'cid:0:privileges:' + privilege);
|
||||||
|
// });
|
||||||
|
// return await utils.promiseParallel(tasks);
|
||||||
|
// };
|
||||||
|
|
||||||
|
// privileges.admin.groupPrivileges = async function (groupName) {
|
||||||
|
// const tasks = {};
|
||||||
|
// privileges.admin.groupPrivilegeList.forEach(function (privilege) {
|
||||||
|
// tasks[privilege] = groups.isMember(groupName, 'cid:0:privileges:' + privilege);
|
||||||
|
// });
|
||||||
|
// return await utils.promiseParallel(tasks);
|
||||||
|
// };
|
||||||
|
};
|
||||||
@@ -45,14 +45,12 @@ module.exports = function (privileges) {
|
|||||||
user.isModerator(uid, cid),
|
user.isModerator(uid, cid),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const privData = _.zipObject(privs, userPrivileges);
|
const combined = userPrivileges.map(allowed => allowed || isAdministrator);
|
||||||
|
const privData = _.zipObject(privs, combined);
|
||||||
const isAdminOrMod = isAdministrator || isModerator;
|
const isAdminOrMod = isAdministrator || isModerator;
|
||||||
|
|
||||||
return await plugins.fireHook('filter:privileges.categories.get', {
|
return await plugins.fireHook('filter:privileges.categories.get', {
|
||||||
'topics:create': privData['topics:create'] || isAdministrator,
|
...privData,
|
||||||
'topics:read': privData['topics:read'] || isAdministrator,
|
|
||||||
'topics:tag': privData['topics:tag'] || isAdministrator,
|
|
||||||
read: privData.read || isAdministrator,
|
|
||||||
cid: cid,
|
cid: cid,
|
||||||
uid: uid,
|
uid: uid,
|
||||||
editable: isAdminOrMod,
|
editable: isAdminOrMod,
|
||||||
|
|||||||
@@ -71,20 +71,10 @@ module.exports = function (privileges) {
|
|||||||
user.isAdministrator(uid),
|
user.isAdministrator(uid),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const privData = _.zipObject(privileges.global.userPrivilegeList, userPrivileges);
|
const combined = userPrivileges.map(allowed => allowed || isAdministrator);
|
||||||
|
const privData = _.zipObject(privileges.global.userPrivilegeList, combined);
|
||||||
|
|
||||||
return await plugins.fireHook('filter:privileges.global.get', {
|
return await plugins.fireHook('filter:privileges.global.get', privData);
|
||||||
chat: privData.chat || isAdministrator,
|
|
||||||
'upload:post:image': privData['upload:post:image'] || isAdministrator,
|
|
||||||
'upload:post:file': privData['upload:post:file'] || isAdministrator,
|
|
||||||
'search:content': privData['search:content'] || isAdministrator,
|
|
||||||
'search:users': privData['search:users'] || isAdministrator,
|
|
||||||
'search:tags': privData['search:tags'] || isAdministrator,
|
|
||||||
'view:users': privData['view:users'] || isAdministrator,
|
|
||||||
'view:tags': privData['view:tags'] || isAdministrator,
|
|
||||||
'view:groups': privData['view:groups'] || isAdministrator,
|
|
||||||
'view:users:info': privData['view:users:info'] || isAdministrator,
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
privileges.global.can = async function (privilege, uid) {
|
privileges.global.can = async function (privilege, uid) {
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ privileges.groupPrivilegeList = privileges.userPrivilegeList.map(privilege => 'g
|
|||||||
privileges.privilegeList = privileges.userPrivilegeList.concat(privileges.groupPrivilegeList);
|
privileges.privilegeList = privileges.userPrivilegeList.concat(privileges.groupPrivilegeList);
|
||||||
|
|
||||||
require('./global')(privileges);
|
require('./global')(privileges);
|
||||||
|
require('./admin')(privileges);
|
||||||
require('./categories')(privileges);
|
require('./categories')(privileges);
|
||||||
require('./topics')(privileges);
|
require('./topics')(privileges);
|
||||||
require('./posts')(privileges);
|
require('./posts')(privileges);
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ const helpers = require('./helpers');
|
|||||||
module.exports = function (app, middleware, controllers) {
|
module.exports = function (app, middleware, controllers) {
|
||||||
const middlewares = [middleware.pluginHooks];
|
const middlewares = [middleware.pluginHooks];
|
||||||
|
|
||||||
helpers.setupAdminPageRoute(app, '/admin', middleware, middlewares, controllers.admin.dashboard.get);
|
helpers.setupAdminPageRoute(app, '/admin', middleware, middlewares, controllers.admin.routeIndex);
|
||||||
|
|
||||||
helpers.setupAdminPageRoute(app, '/admin/dashboard', middleware, middlewares, controllers.admin.dashboard.get);
|
helpers.setupAdminPageRoute(app, '/admin/dashboard', middleware, middlewares, controllers.admin.dashboard.get);
|
||||||
|
|
||||||
|
|||||||
@@ -98,8 +98,8 @@ module.exports = async function (app, middleware) {
|
|||||||
var ensureLoggedIn = require('connect-ensure-login');
|
var ensureLoggedIn = require('connect-ensure-login');
|
||||||
|
|
||||||
router.all('(/+api|/+api/*?)', middleware.prepareAPI);
|
router.all('(/+api|/+api/*?)', middleware.prepareAPI);
|
||||||
router.all('(/+api/admin|/+api/admin/*?)', middleware.isAdmin);
|
router.all('(/+api/admin|/+api/admin/*?)', middleware.admin.checkPrivileges);
|
||||||
router.all('(/+admin|/+admin/*?)', ensureLoggedIn.ensureLoggedIn(nconf.get('relative_path') + '/login?local=1'), middleware.applyCSRF, middleware.isAdmin);
|
router.all('(/+admin|/+admin/*?)', ensureLoggedIn.ensureLoggedIn(nconf.get('relative_path') + '/login?local=1'), middleware.applyCSRF, middleware.admin.checkPrivileges);
|
||||||
|
|
||||||
app.use(middleware.stripLeadingSlashes);
|
app.use(middleware.stripLeadingSlashes);
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ const meta = require('../meta');
|
|||||||
const user = require('../user');
|
const user = require('../user');
|
||||||
const events = require('../events');
|
const events = require('../events');
|
||||||
const db = require('../database');
|
const db = require('../database');
|
||||||
|
const privileges = require('../privileges');
|
||||||
const websockets = require('./index');
|
const websockets = require('./index');
|
||||||
const index = require('./index');
|
const index = require('./index');
|
||||||
const getAdminSearchDict = require('../admin/search').getDictionary;
|
const getAdminSearchDict = require('../admin/search').getDictionary;
|
||||||
@@ -37,6 +38,13 @@ SocketAdmin.before = async function (socket, method) {
|
|||||||
if (isAdmin) {
|
if (isAdmin) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check admin privileges mapping (if not in mapping, deny access)
|
||||||
|
const privilege = privileges.admin.socketMap[method];
|
||||||
|
if (privilege && await privileges.admin.can(privilege, socket.uid)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
winston.warn('[socket.io] Call to admin method ( ' + method + ' ) blocked (accessed by uid ' + socket.uid + ')');
|
winston.warn('[socket.io] Call to admin method ( ' + method + ' ) blocked (accessed by uid ' + socket.uid + ')');
|
||||||
throw new Error('[[error:no-privileges]]');
|
throw new Error('[[error:no-privileges]]');
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -77,7 +77,9 @@ Categories.setPrivilege = async function (socket, data) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
Categories.getPrivilegeSettings = async function (socket, cid) {
|
Categories.getPrivilegeSettings = async function (socket, cid) {
|
||||||
if (!parseInt(cid, 10)) {
|
if (cid === 'admin') {
|
||||||
|
return await privileges.admin.list();
|
||||||
|
} else if (!parseInt(cid, 10)) {
|
||||||
return await privileges.global.list();
|
return await privileges.global.list();
|
||||||
}
|
}
|
||||||
return await privileges.categories.list(cid);
|
return await privileges.categories.list(cid);
|
||||||
|
|||||||
@@ -124,6 +124,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-lg-3">
|
<div class="col-lg-3">
|
||||||
|
{{{ if showSystemControls }}}
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading">[[admin/dashboard:control-panel]]</div>
|
<div class="panel-heading">[[admin/dashboard:control-panel]]</div>
|
||||||
<div class="panel-body text-center">
|
<div class="panel-body text-center">
|
||||||
@@ -152,6 +153,7 @@
|
|||||||
<span id="toggle-realtime">[[admin/dashboard:realtime-chart-updates]] <strong>OFF</strong> <i class="fa fa fa-toggle-off pointer"></i></span>
|
<span id="toggle-realtime">[[admin/dashboard:realtime-chart-updates]] <strong>OFF</strong> <i class="fa fa fa-toggle-off pointer"></i></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{{{ end }}}
|
||||||
|
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading">[[admin/dashboard:active-users]]</div>
|
<div class="panel-heading">[[admin/dashboard:active-users]]</div>
|
||||||
|
|||||||
@@ -11,11 +11,11 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="privilege-table-container">
|
<div class="privilege-table-container">
|
||||||
<!-- IF cid -->
|
{{{ if cid }}}
|
||||||
<!-- IMPORT admin/partials/categories/privileges.tpl -->
|
<!-- IMPORT admin/partials/privileges/category.tpl -->
|
||||||
<!-- ELSE -->
|
{{{ else }}}
|
||||||
<!-- IMPORT admin/partials/global/privileges.tpl -->
|
<!-- IMPORT admin/partials/privileges/global.tpl -->
|
||||||
<!-- ENDIF cid -->
|
{{{ endif }}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -12,9 +12,10 @@
|
|||||||
<section class="menu-section">
|
<section class="menu-section">
|
||||||
<h3 class="menu-section-title">[[admin/menu:section-manage]]</h3>
|
<h3 class="menu-section-title">[[admin/menu:section-manage]]</h3>
|
||||||
<ul class="menu-section-list">
|
<ul class="menu-section-list">
|
||||||
<li><a href="{relative_path}/admin/manage/categories">[[admin/menu:manage/categories]]</a></li>
|
{{{ if user.privileges.admin:categories }}}<li><a href="{relative_path}/admin/manage/categories">[[admin/menu:manage/categories]]</a></li>{{{ end }}}
|
||||||
<li><a href="{relative_path}/admin/manage/privileges">[[admin/menu:manage/privileges]]</a></li>
|
{{{ if user.privileges.admin:privileges }}}<li><a href="{relative_path}/admin/manage/privileges">[[admin/menu:manage/privileges]]</a></li>{{{ end }}}
|
||||||
<li><a href="{relative_path}/admin/manage/users">[[admin/menu:manage/users]]</a></li>
|
{{{ if user.privileges.admin:users }}}<li><a href="{relative_path}/admin/manage/users">[[admin/menu:manage/users]]</a></li>{{{ end }}}
|
||||||
|
{{{ if user.privileges.superadmin }}}
|
||||||
<li><a href="{relative_path}/admin/manage/registration">[[admin/menu:manage/registration]]</a></li>
|
<li><a href="{relative_path}/admin/manage/registration">[[admin/menu:manage/registration]]</a></li>
|
||||||
<li><a href="{relative_path}/admin/manage/admins-mods">[[admin/menu:manage/admins-mods]]</a></li>
|
<li><a href="{relative_path}/admin/manage/admins-mods">[[admin/menu:manage/admins-mods]]</a></li>
|
||||||
<li><a href="{relative_path}/admin/manage/groups">[[admin/menu:manage/groups]]</a></li>
|
<li><a href="{relative_path}/admin/manage/groups">[[admin/menu:manage/groups]]</a></li>
|
||||||
@@ -24,9 +25,11 @@
|
|||||||
|
|
||||||
<li><a target="_top" href="{relative_path}/post-queue">[[admin/menu:manage/post-queue]] <i class="fa fa-external-link"></i></a></li>
|
<li><a target="_top" href="{relative_path}/post-queue">[[admin/menu:manage/post-queue]] <i class="fa fa-external-link"></i></a></li>
|
||||||
<li><a target="_top" href="{relative_path}/ip-blacklist">[[admin/menu:manage/ip-blacklist]] <i class="fa fa-external-link"></i></a></li>
|
<li><a target="_top" href="{relative_path}/ip-blacklist">[[admin/menu:manage/ip-blacklist]] <i class="fa fa-external-link"></i></a></li>
|
||||||
|
{{{ end }}}
|
||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{{{ if user.privileges.admin:settings }}}
|
||||||
<section class="menu-section">
|
<section class="menu-section">
|
||||||
<h3 class="menu-section-title">[[admin/menu:section-settings]]</h3>
|
<h3 class="menu-section-title">[[admin/menu:section-settings]]</h3>
|
||||||
<ul class="menu-section-list">
|
<ul class="menu-section-list">
|
||||||
@@ -52,7 +55,6 @@
|
|||||||
<li><a href="{relative_path}/admin/settings/advanced">[[admin/menu:settings/advanced]]</a></li>
|
<li><a href="{relative_path}/admin/settings/advanced">[[admin/menu:settings/advanced]]</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="menu-section">
|
<section class="menu-section">
|
||||||
<h3 class="menu-section-title">[[admin/menu:section-appearance]]</h3>
|
<h3 class="menu-section-title">[[admin/menu:section-appearance]]</h3>
|
||||||
<ul class="menu-section-list">
|
<ul class="menu-section-list">
|
||||||
@@ -96,7 +98,9 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
<!-- ENDIF authentication.length -->
|
<!-- ENDIF authentication.length -->
|
||||||
|
{{{ end }}}
|
||||||
|
|
||||||
|
{{{ if user.privileges.superadmin }}}
|
||||||
<section class="menu-section">
|
<section class="menu-section">
|
||||||
<h3 class="menu-section-title">[[admin/menu:section-advanced]]</h3>
|
<h3 class="menu-section-title">[[admin/menu:section-advanced]]</h3>
|
||||||
<ul class="menu-section-list">
|
<ul class="menu-section-list">
|
||||||
@@ -111,6 +115,7 @@
|
|||||||
<!-- ENDIF env -->
|
<!-- ENDIF env -->
|
||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
|
{{{ end }}}
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<main id="panel">
|
<main id="panel">
|
||||||
@@ -127,6 +132,7 @@
|
|||||||
<ul class="quick-actions hidden-xs hidden-sm">
|
<ul class="quick-actions hidden-xs hidden-sm">
|
||||||
<!-- IMPORT admin/partials/quick_actions/buttons.tpl -->
|
<!-- IMPORT admin/partials/quick_actions/buttons.tpl -->
|
||||||
|
|
||||||
|
{{{ if user.privileges.admin:settings }}}
|
||||||
<form role="search">
|
<form role="search">
|
||||||
<div id="acp-search" >
|
<div id="acp-search" >
|
||||||
<div class="dropdown">
|
<div class="dropdown">
|
||||||
@@ -151,6 +157,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
{{{ end }}}
|
||||||
|
|
||||||
<!-- IMPORT admin/partials/quick_actions/alerts.tpl -->
|
<!-- IMPORT admin/partials/quick_actions/alerts.tpl -->
|
||||||
|
|
||||||
@@ -163,15 +170,19 @@
|
|||||||
|
|
||||||
|
|
||||||
<ul id="main-menu">
|
<ul id="main-menu">
|
||||||
|
{{{ if user.privileges.admin:dashboard }}}
|
||||||
<li class="menu-item">
|
<li class="menu-item">
|
||||||
<a href="{relative_path}/admin/dashboard">[[admin/menu:dashboard]]</a>
|
<a href="{relative_path}/admin/dashboard">[[admin/menu:dashboard]]</a>
|
||||||
</li>
|
</li>
|
||||||
|
{{{ end }}}
|
||||||
|
|
||||||
<li class="dropdown menu-item">
|
<li class="dropdown menu-item">
|
||||||
<a id="manage-menu" href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false">[[admin/menu:section-manage]]</a>
|
<a id="manage-menu" href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false">[[admin/menu:section-manage]]</a>
|
||||||
<ul class="dropdown-menu" role="menu">
|
<ul class="dropdown-menu" role="menu">
|
||||||
<li><a id="manage-categories" href="{relative_path}/admin/manage/categories">[[admin/menu:manage/categories]]</a></li>
|
{{{ if user.privileges.admin:categories }}}<li><a id="manage-categories" href="{relative_path}/admin/manage/categories">[[admin/menu:manage/categories]]</a></li>{{{ end }}}
|
||||||
<li><a href="{relative_path}/admin/manage/privileges">[[admin/menu:manage/privileges]]</a></li>
|
{{{ if user.privileges.admin:privileges }}}<li><a href="{relative_path}/admin/manage/privileges">[[admin/menu:manage/privileges]]</a></li>{{{ end }}}
|
||||||
<li><a id="manage-users" href="{relative_path}/admin/manage/users">[[admin/menu:manage/users]]</a></li>
|
{{{ if user.privileges.admin:users }}}<li><a id="manage-users" href="{relative_path}/admin/manage/users">[[admin/menu:manage/users]]</a></li>{{{ end }}}
|
||||||
|
{{{ if user.privileges.superadmin }}}
|
||||||
<li><a href="{relative_path}/admin/manage/registration">[[admin/menu:manage/registration]]</a></li>
|
<li><a href="{relative_path}/admin/manage/registration">[[admin/menu:manage/registration]]</a></li>
|
||||||
<li><a href="{relative_path}/admin/manage/admins-mods">[[admin/menu:manage/admins-mods]]</a></li>
|
<li><a href="{relative_path}/admin/manage/admins-mods">[[admin/menu:manage/admins-mods]]</a></li>
|
||||||
<li><a href="{relative_path}/admin/manage/groups">[[admin/menu:manage/groups]]</a></li>
|
<li><a href="{relative_path}/admin/manage/groups">[[admin/menu:manage/groups]]</a></li>
|
||||||
@@ -181,8 +192,11 @@
|
|||||||
<li role="separator" class="divider"></li>
|
<li role="separator" class="divider"></li>
|
||||||
<li><a target="_top" href="{relative_path}/post-queue">[[admin/menu:manage/post-queue]] <i class="fa fa-external-link"></i></a></li>
|
<li><a target="_top" href="{relative_path}/post-queue">[[admin/menu:manage/post-queue]] <i class="fa fa-external-link"></i></a></li>
|
||||||
<li><a target="_top" href="{relative_path}/ip-blacklist">[[admin/menu:manage/ip-blacklist]] <i class="fa fa-external-link"></i></a></li>
|
<li><a target="_top" href="{relative_path}/ip-blacklist">[[admin/menu:manage/ip-blacklist]] <i class="fa fa-external-link"></i></a></li>
|
||||||
|
{{{ end }}}
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
{{{ if user.privileges.admin:settings }}}
|
||||||
<li class="dropdown menu-item">
|
<li class="dropdown menu-item">
|
||||||
<a id="settings-menu" href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false">[[admin/menu:section-settings]]</a>
|
<a id="settings-menu" href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false">[[admin/menu:section-settings]]</a>
|
||||||
<ul class="dropdown-menu" role="menu">
|
<ul class="dropdown-menu" role="menu">
|
||||||
@@ -224,7 +238,6 @@
|
|||||||
<li><a href="{relative_path}/admin/extend/rewards">[[admin/menu:extend/rewards]]</a></li>
|
<li><a href="{relative_path}/admin/extend/rewards">[[admin/menu:extend/rewards]]</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<!-- IF plugins.length -->
|
<!-- IF plugins.length -->
|
||||||
<li class="dropdown menu-item">
|
<li class="dropdown menu-item">
|
||||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false">[[admin/menu:section-plugins]]</a>
|
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false">[[admin/menu:section-plugins]]</a>
|
||||||
@@ -253,6 +266,9 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<!-- ENDIF plugins.length -->
|
<!-- ENDIF plugins.length -->
|
||||||
|
{{{ end }}}
|
||||||
|
|
||||||
|
{{{ if user.privileges.superadmin }}}
|
||||||
<li class="dropdown menu-item">
|
<li class="dropdown menu-item">
|
||||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false">[[admin/menu:section-advanced]]</a>
|
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false">[[admin/menu:section-advanced]]</a>
|
||||||
<ul class="dropdown-menu" role="menu">
|
<ul class="dropdown-menu" role="menu">
|
||||||
@@ -267,5 +283,6 @@
|
|||||||
<!-- ENDIF env -->
|
<!-- ENDIF env -->
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
|
{{{ end }}}
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
@@ -1,9 +1,6 @@
|
|||||||
<label>[[admin/manage/privileges:group-privileges]]</label>
|
<label>[[admin/manage/privileges:group-privileges]]</label>
|
||||||
<table class="table table-striped privilege-table">
|
<table class="table table-striped privilege-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr class="privilege-table-header">
|
|
||||||
<th colspan="15"></th>
|
|
||||||
</tr><tr><!-- zebrastripe reset --></tr>
|
|
||||||
<tr>
|
<tr>
|
||||||
<th colspan="2">[[admin/manage/categories:privileges.section-group]]</th>
|
<th colspan="2">[[admin/manage/categories:privileges.section-group]]</th>
|
||||||
<!-- BEGIN privileges.labels.groups -->
|
<!-- BEGIN privileges.labels.groups -->
|
||||||
@@ -3,6 +3,8 @@
|
|||||||
<i class="fa fw-fw fa-sign-out"></i>
|
<i class="fa fw-fw fa-sign-out"></i>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
{{{ if user.privileges.superadmin }}}
|
||||||
<li>
|
<li>
|
||||||
<a href="#" class="restart" data-toggle="tooltip" data-placement="bottom" title="[[admin/menu:restart-forum]]">
|
<a href="#" class="restart" data-toggle="tooltip" data-placement="bottom" title="[[admin/menu:restart-forum]]">
|
||||||
<i class="fa fa-fw fa-repeat"></i>
|
<i class="fa fa-fw fa-repeat"></i>
|
||||||
@@ -13,6 +15,7 @@
|
|||||||
<i class="fa fa-fw fa-refresh"></i>
|
<i class="fa fa-fw fa-refresh"></i>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
{{{ end }}}
|
||||||
|
|
||||||
<li>
|
<li>
|
||||||
<a href="{config.relative_path}/" data-toggle="tooltip" data-placement="bottom" title="[[admin/menu:view-forum]]">
|
<a href="{config.relative_path}/" data-toggle="tooltip" data-placement="bottom" title="[[admin/menu:view-forum]]">
|
||||||
|
|||||||
Reference in New Issue
Block a user