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 afdb235f48eb0072d88de45f3a1e0151281095b3.

* 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
v1.18.x
Julian Lam 5 years ago committed by GitHub
parent 7f6ff0b1a5
commit a82e9bd7f6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,6 +1,7 @@
{
"global": "Global",
"global.no-users": "No user-specific global privileges.",
"admin": "Admin",
"group-privileges": "Group Privileges",
"user-privileges": "User Privileges",
"chat": "Chat",
@ -31,5 +32,11 @@
"downvote-posts": "Downvote Posts",
"delete-topics": "Delete Topics",
"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:
type: string
description: An ISO 8601 formatted date string (complementing `timestamp`)
showSystemControls:
type: boolean
- $ref: components/schemas/CommonProps.yaml#/CommonProps
/api/admin/settings/languages:
get:

@ -11,12 +11,15 @@ define('admin/manage/privileges', [
var cid;
Privileges.init = function () {
cid = ajaxify.data.cid || 0;
cid = ajaxify.data.cid || 'admin';
categorySelector.init($('[component="category-selector"]'), function (category) {
var cid = parseInt(category.cid, 10);
ajaxify.go('admin/manage/privileges/' + (cid || ''));
cid = parseInt(category.cid, 10);
cid = isNaN(cid) ? 'admin' : cid;
Privileges.refreshPrivilegeTable();
ajaxify.updateHistory('admin/manage/privileges/' + (cid || ''));
});
Privileges.setupPrivilegeTable();
};
@ -81,7 +84,8 @@ define('admin/manage/privileges', [
if (err) {
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, {
privileges: privileges,
}, function (html) {
@ -117,7 +121,7 @@ define('admin/manage/privileges', [
Privileges.setPrivilege = function (member, privilege, state, checkboxEl) {
socket.emit('admin.categories.setPrivilege', {
cid: cid,
cid: isNaN(cid) ? 0 : cid,
privilege: privilege,
set: state,
member: member,
@ -143,9 +147,14 @@ define('admin/manage/privileges', [
inputEl.focus();
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', {
cid: cid,
cid: isNaN(cid) ? 0 : cid,
privilege: defaultPrivileges,
set: true,
member: ui.item.user.uid,
@ -170,11 +179,18 @@ define('admin/manage/privileges', [
modal.on('shown.bs.modal', function () {
var inputEl = modal.find('input');
inputEl.focus();
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', {
cid: cid,
cid: isNaN(cid) ? 0 : cid,
privilege: defaultPrivileges,
set: true,
member: ui.item.group.name,

@ -46,6 +46,10 @@ define('admin/modules/search', ['mousetrap'], function (mousetrap) {
}
search.init = function () {
if (!app.user.privileges['admin:settings']) {
return;
}
socket.emit('admin.getSearchDict', {}, function (err, dict) {
if (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 globalModDisabled = ['groups:moderate'];
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 === 'Global Moderators' && globalModDisabled.includes(priv.name));

@ -1,5 +1,8 @@
'use strict';
const privileges = require('../privileges');
const helpers = require('./helpers');
var adminController = {
dashboard: require('./admin/dashboard'),
categories: require('./admin/categories'),
@ -28,5 +31,22 @@ var adminController = {
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;

@ -16,11 +16,12 @@ const utils = require('../../utils');
const dashboardController = module.exports;
dashboardController.get = async function (req, res) {
const [stats, notices, latestVersion, lastrestart] = await Promise.all([
const [stats, notices, latestVersion, lastrestart, isAdmin] = await Promise.all([
getStats(),
getNotices(),
getLatestVersion(),
getLastRestart(),
user.isAdministrator(),
]);
const version = nconf.get('version');
@ -34,6 +35,7 @@ dashboardController.get = async function (req, res) {
stats: stats,
canRestart: !!process.send,
lastrestart: lastrestart,
showSystemControls: isAdmin,
});
};

@ -6,9 +6,18 @@ const privileges = require('../../privileges');
const privilegesController = module.exports;
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([
cid ? privileges.categories.list(cid) : privileges.global.list(),
method(),
categories.buildForSelectAll(),
]);
@ -16,12 +25,16 @@ privilegesController.get = async function (req, res) {
cid: 0,
name: '[[admin/manage/privileges:global]]',
icon: 'fa-list',
}, {
cid: 'admin', // what do?
name: '[[admin/manage/privileges:admin]]',
icon: 'fa-lock',
});
let selectedCategory;
categoriesData.forEach(function (category) {
if (category) {
category.selected = category.cid === cid;
category.selected = category.cid === (!isAdminPriv ? cid : 'admin');
if (category.selected) {
selectedCategory = category;

@ -8,6 +8,7 @@ var semver = require('semver');
var user = require('../user');
var meta = require('../meta');
var plugins = require('../plugins');
var privileges = require('../privileges');
var utils = require('../../public/src/utils');
var versions = require('../admin/versions');
var helpers = require('./helpers');
@ -43,11 +44,13 @@ module.exports = function (middleware) {
custom_header: plugins.fireHook('filter:admin.header.build', custom_header),
configs: meta.configs.list(),
latestVersion: getLatestVersion(),
privileges: privileges.admin.get(req.uid),
});
var userData = results.userData;
userData.uid = req.uid;
userData['email:confirmed'] = userData['email:confirmed'] === 1;
userData.privileges = results.privileges;
var acpPath = req.path.slice(1).split('/');
acpPath.forEach(function (path, i) {
@ -103,4 +106,26 @@ module.exports = function (middleware) {
middleware.admin.renderFooter = async function (req, res, 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();
};
};

@ -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),
]);
const privData = _.zipObject(privs, userPrivileges);
const combined = userPrivileges.map(allowed => allowed || isAdministrator);
const privData = _.zipObject(privs, combined);
const isAdminOrMod = isAdministrator || isModerator;
return await plugins.fireHook('filter:privileges.categories.get', {
'topics:create': privData['topics:create'] || isAdministrator,
'topics:read': privData['topics:read'] || isAdministrator,
'topics:tag': privData['topics:tag'] || isAdministrator,
read: privData.read || isAdministrator,
...privData,
cid: cid,
uid: uid,
editable: isAdminOrMod,

@ -71,20 +71,10 @@ module.exports = function (privileges) {
user.isAdministrator(uid),
]);
const privData = _.zipObject(privileges.global.userPrivilegeList, userPrivileges);
return await plugins.fireHook('filter:privileges.global.get', {
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,
});
const combined = userPrivileges.map(allowed => allowed || isAdministrator);
const privData = _.zipObject(privileges.global.userPrivilegeList, combined);
return await plugins.fireHook('filter:privileges.global.get', privData);
};
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);
require('./global')(privileges);
require('./admin')(privileges);
require('./categories')(privileges);
require('./topics')(privileges);
require('./posts')(privileges);

@ -5,7 +5,7 @@ const helpers = require('./helpers');
module.exports = function (app, middleware, controllers) {
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);

@ -98,8 +98,8 @@ module.exports = async function (app, middleware) {
var ensureLoggedIn = require('connect-ensure-login');
router.all('(/+api|/+api/*?)', middleware.prepareAPI);
router.all('(/+api/admin|/+api/admin/*?)', middleware.isAdmin);
router.all('(/+admin|/+admin/*?)', ensureLoggedIn.ensureLoggedIn(nconf.get('relative_path') + '/login?local=1'), middleware.applyCSRF, 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.admin.checkPrivileges);
app.use(middleware.stripLeadingSlashes);

@ -6,6 +6,7 @@ const meta = require('../meta');
const user = require('../user');
const events = require('../events');
const db = require('../database');
const privileges = require('../privileges');
const websockets = require('./index');
const index = require('./index');
const getAdminSearchDict = require('../admin/search').getDictionary;
@ -37,6 +38,13 @@ SocketAdmin.before = async function (socket, method) {
if (isAdmin) {
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 + ')');
throw new Error('[[error:no-privileges]]');
};

@ -77,7 +77,9 @@ Categories.setPrivilege = async function (socket, data) {
};
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.categories.list(cid);

@ -124,6 +124,7 @@
</div>
<div class="col-lg-3">
{{{ if showSystemControls }}}
<div class="panel panel-default">
<div class="panel-heading">[[admin/dashboard:control-panel]]</div>
<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>
</div>
</div>
{{{ end }}}
<div class="panel panel-default">
<div class="panel-heading">[[admin/dashboard:active-users]]</div>

@ -11,11 +11,11 @@
</div>
<div class="privilege-table-container">
<!-- IF cid -->
<!-- IMPORT admin/partials/categories/privileges.tpl -->
<!-- ELSE -->
<!-- IMPORT admin/partials/global/privileges.tpl -->
<!-- ENDIF cid -->
{{{ if cid }}}
<!-- IMPORT admin/partials/privileges/category.tpl -->
{{{ else }}}
<!-- IMPORT admin/partials/privileges/global.tpl -->
{{{ endif }}}
</div>
</div>
</form>

@ -12,9 +12,10 @@
<section class="menu-section">
<h3 class="menu-section-title">[[admin/menu:section-manage]]</h3>
<ul class="menu-section-list">
<li><a href="{relative_path}/admin/manage/categories">[[admin/menu:manage/categories]]</a></li>
<li><a href="{relative_path}/admin/manage/privileges">[[admin/menu:manage/privileges]]</a></li>
<li><a href="{relative_path}/admin/manage/users">[[admin/menu:manage/users]]</a></li>
{{{ if user.privileges.admin:categories }}}<li><a href="{relative_path}/admin/manage/categories">[[admin/menu:manage/categories]]</a></li>{{{ end }}}
{{{ if user.privileges.admin:privileges }}}<li><a href="{relative_path}/admin/manage/privileges">[[admin/menu:manage/privileges]]</a></li>{{{ end }}}
{{{ 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/admins-mods">[[admin/menu:manage/admins-mods]]</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}/ip-blacklist">[[admin/menu:manage/ip-blacklist]] <i class="fa fa-external-link"></i></a></li>
{{{ end }}}
</ul>
</section>
{{{ if user.privileges.admin:settings }}}
<section class="menu-section">
<h3 class="menu-section-title">[[admin/menu:section-settings]]</h3>
<ul class="menu-section-list">
@ -52,7 +55,6 @@
<li><a href="{relative_path}/admin/settings/advanced">[[admin/menu:settings/advanced]]</a></li>
</ul>
</section>
<section class="menu-section">
<h3 class="menu-section-title">[[admin/menu:section-appearance]]</h3>
<ul class="menu-section-list">
@ -96,7 +98,9 @@
</ul>
</section>
<!-- ENDIF authentication.length -->
{{{ end }}}
{{{ if user.privileges.superadmin }}}
<section class="menu-section">
<h3 class="menu-section-title">[[admin/menu:section-advanced]]</h3>
<ul class="menu-section-list">
@ -111,6 +115,7 @@
<!-- ENDIF env -->
</ul>
</section>
{{{ end }}}
</nav>
<main id="panel">
@ -127,6 +132,7 @@
<ul class="quick-actions hidden-xs hidden-sm">
<!-- IMPORT admin/partials/quick_actions/buttons.tpl -->
{{{ if user.privileges.admin:settings }}}
<form role="search">
<div id="acp-search" >
<div class="dropdown">
@ -151,6 +157,7 @@
</div>
</div>
</form>
{{{ end }}}
<!-- IMPORT admin/partials/quick_actions/alerts.tpl -->
@ -163,15 +170,19 @@
<ul id="main-menu">
{{{ if user.privileges.admin:dashboard }}}
<li class="menu-item">
<a href="{relative_path}/admin/dashboard">[[admin/menu:dashboard]]</a>
</li>
{{{ end }}}
<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>
<ul class="dropdown-menu" role="menu">
<li><a id="manage-categories" href="{relative_path}/admin/manage/categories">[[admin/menu:manage/categories]]</a></li>
<li><a href="{relative_path}/admin/manage/privileges">[[admin/menu:manage/privileges]]</a></li>
<li><a id="manage-users" href="{relative_path}/admin/manage/users">[[admin/menu:manage/users]]</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 }}}
{{{ if user.privileges.admin:privileges }}}<li><a href="{relative_path}/admin/manage/privileges">[[admin/menu:manage/privileges]]</a></li>{{{ end }}}
{{{ 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/admins-mods">[[admin/menu:manage/admins-mods]]</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><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>
{{{ end }}}
</ul>
</li>
{{{ if user.privileges.admin:settings }}}
<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>
<ul class="dropdown-menu" role="menu">
@ -224,7 +238,6 @@
<li><a href="{relative_path}/admin/extend/rewards">[[admin/menu:extend/rewards]]</a></li>
</ul>
</li>
<!-- IF plugins.length -->
<li class="dropdown menu-item">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false">[[admin/menu:section-plugins]]</a>
@ -253,6 +266,9 @@
</ul>
</li>
<!-- ENDIF plugins.length -->
{{{ end }}}
{{{ if user.privileges.superadmin }}}
<li class="dropdown menu-item">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false">[[admin/menu:section-advanced]]</a>
<ul class="dropdown-menu" role="menu">
@ -267,5 +283,6 @@
<!-- ENDIF env -->
</ul>
</li>
{{{ end }}}
</ul>
</nav>

@ -1,9 +1,6 @@
<label>[[admin/manage/privileges:group-privileges]]</label>
<table class="table table-striped privilege-table">
<thead>
<tr class="privilege-table-header">
<th colspan="15"></th>
</tr><tr><!-- zebrastripe reset --></tr>
<tr>
<th colspan="2">[[admin/manage/categories:privileges.section-group]]</th>
<!-- BEGIN privileges.labels.groups -->

@ -3,6 +3,8 @@
<i class="fa fw-fw fa-sign-out"></i>
</a>
</li>
{{{ if user.privileges.superadmin }}}
<li>
<a href="#" class="restart" data-toggle="tooltip" data-placement="bottom" title="[[admin/menu:restart-forum]]">
<i class="fa fa-fw fa-repeat"></i>
@ -13,6 +15,7 @@
<i class="fa fa-fw fa-refresh"></i>
</a>
</li>
{{{ end }}}
<li>
<a href="{config.relative_path}/" data-toggle="tooltip" data-placement="bottom" title="[[admin/menu:view-forum]]">

Loading…
Cancel
Save