Merge pull request #257 from YunoHost/enh-permissions

Add permission and group management
This commit is contained in:
Alexandre Aubin 2019-10-31 18:18:29 +01:00 committed by GitHub
commit 7397504d81
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 493 additions and 283 deletions

View file

@ -5,7 +5,7 @@
"private": true,
"dependencies": {
"bootstrap": "3.3.6",
"font-awesome": "4.5.0",
"fork-awesome": "1.1.7",
"handlebars-helper-intl": "1.1.2",
"handlebars": "4.0.11",
"sammy": "0.7.6",

View file

@ -25,7 +25,7 @@
/*
* FontAwesome
*/
@import "../bower_components/font-awesome/less/font-awesome.less";
@import "../bower_components/fork-awesome/less/fork-awesome.less";
// Fixes
@ -101,6 +101,10 @@ button {
color: transparent;
}
.label {
border-radius: 1px;
}
/*
* The top heading of the doc
*
@ -698,6 +702,62 @@ input[type='radio'].nice-radio {
}
/** Groups View **/
#view-groups {
.panel-heading a {
text-decoration: none;
&.group-delete {
float:right;
color:lighten(@label-danger-bg, 20%);
:hover {
color:@label-danger-bg;
}
}
}
.panel-body {
h3 {
margin-top:0;
}
button.dropdown-toggle {
line-height: 15.666px;
top: -1.666px;
}
.dropdown-menu {
max-height: 200px;
overflow-y: auto;
}
.label-removable {
// The following match properties from regular btn's
display:inline-block;
font-size:14px;
color:#333;
background-color:#f8f8f8;
border: #ccc 1px solid;
font-weight: normal;
margin-bottom:0;
position: relative;
top: -1.666px;
height: 29.666px;
vertical-align: middle;
padding: 6px 12px;
margin-right:7px; // Spacing between labels
> a {
margin-left:6px;
padding-left:6px;
border-left: #ccc 1px solid;
color:lighten(@label-info-bg,20);
text-decoration: none;
}
> a:hover {
color:@label-info-bg;
}
}
}
}
/** Flash messages **/
#flashMessage {
max-height: 120px;

View file

@ -71,7 +71,7 @@ gulp.task('js-lint', function() {
// Fonts
gulp.task('fonts', function() {
return gulp.src([
'bower_components/font-awesome/fonts/*',
'bower_components/fork-awesome/fonts/*',
'bower_components/source-code-pro/EOT/*.eot',
'bower_components/source-code-pro/OTF/*.otf',
'bower_components/source-code-pro/TTF/*.ttf',

View file

@ -202,8 +202,10 @@
// Get app information
app.get('#/apps/:app', function (c) {
c.api('/apps/'+c.params['app']+'?raw', function(data) { // http://api.yunohost.org/#!/app/app_info_get_9
// Presentation
data.settings.allowed_users = (data.settings.allowed_users) ? data.settings.allowed_users.replace(',', ', ')+"." : y18n.t('everyone_has_access');
c.api('/users/permissions', function(data_permissions) {
// Permissions
data.permissions = data_permissions.permissions[c.params['app']+".main"]["allowed"];
// Multilingual description
data.description = (typeof data.manifest.description[y18n.locale] !== 'undefined') ?
@ -218,6 +220,7 @@
c.view('app/app_info', data);
});
});
});
// Get app debug page
app.get('#/apps/:app/debug', function (c) {
@ -619,150 +622,6 @@
);
});
// Manage app access
app.get('#/apps/:app/access', function (c) {
c.api('/apps/'+c.params['app']+'?raw', function(data) { // http://api.yunohost.org/#!/app/app_info_get_9
c.api('/users', function(dataUsers) {
// allowed_users as array
if (typeof data.settings.allowed_users !== 'undefined') {
if (data.settings.allowed_users.length === 0) {
// Force empty array, means no user has access
data.settings.allowed_users = [];
}
else {
data.settings.allowed_users = data.settings.allowed_users.split(',');
}
} else {
data.settings.allowed_users = []; // Force array
// if 'allowed_users' is undefined, everyone has access
// that means that undefined is different from empty array
data.settings.allow_everyone = true;
}
// Available users
data.users = [];
$.each(dataUsers.users, function(username, user){
// Do not list allowed_users in select list
if ( data.settings.allowed_users.indexOf(username) === -1 ) {
data.users.push({
value: username,
label: user.fullname+' ('+user.mail+')'
});
} else {
// Complete allowed_users data
data.settings.allowed_users[data.settings.allowed_users.indexOf(username)] = {
username: username,
fullname: user.fullname,
mail: user.mail,
};
}
});
c.view('app/app_access', data);
});
});
});
// Remove all access
app.get('#/apps/:app/access/remove', function (c) {
c.confirm(
y18n.t('applications'),
y18n.t('confirm_access_remove_all', [c.params['app']]),
function() {
var params = {
apps: c.params['app'],
users: []
};
c.api('/access?'+c.serialize(params), function(data) { // http://api.yunohost.org/#!/app/app_removeaccess_delete_12
store.clear('slide');
c.redirect('#/apps/'+ c.params['app']+ '/access');
}, 'DELETE', params);
},
function() {
store.clear('slide');
c.redirect('#/apps/'+ c.params['app']+ '/access');
}
);
});
// Remove access to a specific user
app.get('#/apps/:app/access/remove/:user', function (c) {
c.confirm(
y18n.t('applications'),
y18n.t('confirm_access_remove_user', [c.params['app'], c.params['user']]),
function() {
var params = {
apps: c.params['app'],
users: c.params['user']
};
c.api('/access?'+c.serialize(params), function(data) { // http://api.yunohost.org/#!/app/app_removeaccess_delete_12
store.clear('slide');
c.redirect('#/apps/'+ c.params['app']+ '/access');
}, 'DELETE', params); // passing 'params' here is useless because jQuery doesn't handle ajax datas for DELETE requests. Passing parameters through uri.
},
function() {
store.clear('slide');
c.redirect('#/apps/'+ c.params['app']+ '/access');
}
);
});
// Grant all access
app.get('#/apps/:app/access/add', function (c) {
c.confirm(
y18n.t('applications'),
y18n.t('confirm_access_add', [c.params['app']]),
function() {
var params = {
apps: c.params['app'],
users: null
};
c.api('/access', function() { // http://api.yunohost.org/#!/app/app_addaccess_put_13
store.clear('slide');
c.redirect('#/apps/'+ c.params['app'] +'/access');
}, 'PUT', params);
},
function() {
store.clear('slide');
c.redirect('#/apps/'+ c.params['app']+ '/access');
}
);
});
// Grant access for a specific user
app.post('#/apps/:app/access/add', function (c) {
var params = {
users: c.params['user'],
apps: c.params['app']
};
c.api('/access', function() { // http://api.yunohost.org/#!/app/app_addaccess_put_13
store.clear('slide');
c.redirect('#/apps/'+ c.params['app'] +'/access');
}, 'PUT', params);
});
// Clear access (reset)
app.get('#/apps/:app/access/clear', function (c) {
c.confirm(
y18n.t('applications'),
y18n.t('confirm_access_clear', [c.params['app']]),
function() {
var params = {
apps: c.params['app']
};
c.api('/access', function() { //
store.clear('slide');
c.redirect('#/apps/'+ c.params['app'] +'/access');
}, 'POST', params);
},
function() {
store.clear('slide');
c.redirect('#/apps/'+ c.params['app']+ '/access');
}
);
});
// Make app default
app.get('#/apps/:app/default', function (c) {
c.confirm(

View file

@ -5,6 +5,196 @@
var PASSWORD_MIN_LENGTH = 4;
// A small utility to convert a string to title case
// e.g. "hAvE a NicE dAy" --> "Have A Nice Day"
// Savagely stolen from https://stackoverflow.com/a/196991
function toTitleCase(str) {
return str.replace(
/\w\S*/g,
function(txt) {
return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();
}
);
}
/**
* Groups and permissions
*
*/
/**
* Update group or permissions
*
* @model data organize in the same way than /users/groups?full&include_primary_groups
* @params.operation "add"|"remove"
* @params.type "members"|"permissions"
* @param.item Name of the user or the permission to add or remove
* @param.group Name of the group affected
*
* This function is built to be apply with params generated by the use of
* HTML dataset attributes (e.g. link in the partial inline view "label" in group_list.ms)
*
* @return void
**/
function updateGroup(model, params) {
var type = params.type;
var operation = params.operation;
var item = params.item;
var groupname = params.group;
var group = data.groups[groupname];
var to = (operation == 'add')?group[type]:group[type + 'Inv'];
var from = (operation == 'add')?group[type+'Inv']:group[type];
// Do nothing, if array of destination already contains the item
if (from.indexOf(item) === -1) return;
// Hack to disable pacman loader if any
if ($('div.loader').length === 0) {
$('#main').append('<div class="loader loader-content" style="display: none"></div>');
}
$('div.loader').css('display', 'none');
// Update group
var params = {}; var url;
if (type == 'members') {
url = '/users/groups/' + groupname;
params[operation] = [item];
}
else {
url = '/users/permissions/' + item;
params[operation] = [groupname];
}
c.api(url, function(data_update) {
to.push(item);
from.splice(from.indexOf(item), 1);
updateView(data);
}, 'PUT', params);
}
/**
* Update the view with the new model
*
* @model data organize in the same way than /users/groups?full&include_primary_groups
*
* @return void
**/
function updateView(model) {
// Sort in aphanumerical order to improve user experience
for (var group in model.groups) {
model.groups[group].permissions.sort();
model.groups[group].permissionsInv.sort();
model.groups[group].members.sort();
model.groups[group].membersInv.sort();
}
// Manual render, we don't use c.view to avoid scrollTop and other
// uneeded behaviour
var rendered = c.render('views/user/group_list.ms', model);
rendered.swap(function () {
// Add click event to get a nice "reactive" interface
jQuery(".group-update").on('click', function (e) {
updateGroup(model, jQuery(this)[0].dataset);
return false;
});
jQuery(".group-add-user").on('click', function (e) {
data.groups[$(this)[0].dataset.user].display = true;
updateView(data);
return false;
});
});
}
app.get('#/groups', function (c) {
c.api('/users/groups?full&include_primary_groups', function(data_groups) {
c.api('/users', function(data_users) {
c.api('/users/permissions?short', function(data_permissions) {
//var perms = data_permissions.permissions;
var specific_perms = {};
var all_perms = data_permissions.permissions;
var users = Object.keys(data_users.users);
// Enrich groups data with primary group indicator and inversed items list
for (var group in data_groups.groups) {
data_groups.groups[group].primary = users.indexOf(group) !== -1;
data_groups.groups[group].permissionsInv = all_perms.filter(function(item) {
return data_groups.groups[group].permissions.indexOf(item) === -1;
}).filter(function(item) {
return group != "visitors" || (item != "mail.main" && item != "xmpp.main"); // Remove 'email' and 'xmpp' in visitors's permission choice list
});
data_groups.groups[group].membersInv = users.filter(function(item) {
return data_groups.groups[group].members.indexOf(item) === -1;
});
}
// Declare all_users and visitors has special
data_groups.groups['all_users'].special = true;
data_groups.groups['visitors'].special = true;
// Data given to the view with 2 functions to convert technical
// permission id to display names
data = {
'groups':data_groups.groups,
'displayPermission': function (text) {
// Display a permission correctly for a human
text = text.replace('.main', '');
if (text.indexOf('.') > -1)
text = text.replace('.', ' (') + ')';
if (text == "mail")
text = "E-mail";
else if (text == "xmpp")
text = "XMPP";
else
text = toTitleCase(text);
return text;
},
'displayUser': function (text) {
return text;
},
};
updateView(data);
});
});
});
});
// Create a new group
app.get('#/groups/create', function (c) {
c.view('user/group_create', {});
});
app.post('#/groups/create', function (c) {
c.params['groupname'] = c.params['groupname'].replace(' ', '_').toLowerCase();
c.api('/users/groups', function(data) {
c.redirect('#/groups');
}, 'POST', c.params.toHash());
});
app.get('#/groups/:group/delete', function (c) {
var params = {};
// make confirm content
var confirmModalContent = $('<div>'+ y18n.t('confirm_delete', [c.params['group']]) +'</div>');
// display confirm modal
c.confirm(
y18n.t('groups'),
confirmModalContent,
function(){
c.api('/users/groups/'+ c.params['group'], function(data) {
c.redirect('#/groups');
}, 'DELETE', params);
},
function(){
//store.clear('slide');
c.redirect('#/groups');
}
);
});
/**
* Users
*
@ -235,4 +425,7 @@
});
})();

View file

@ -101,12 +101,43 @@
// equality stuff because mustache/Handlebars is lame
// source https://stackoverflow.com/a/31632215
Handlebars.registerHelper('eq', function(a, b) {
Handlebars.registerHelper({
eq: function(a, b) {
return a === b;
},
neq: function(a, b) {
return a !== b;
},
lt: function (v1, v2) {
return v1 < v2;
},
gt: function (v1, v2) {
return v1 > v2;
},
lte: function (v1, v2) {
return v1 <= v2;
},
gte: function (v1, v2) {
return v1 >= v2;
},
and: function () {
return Array.prototype.slice.call(arguments).every(function (arg) {
return (Array.isArray(arg))?arg.length !== 0:arg;
});
},
or: function () {
return Array.prototype.slice.call(arguments, 0, -1).some(function (arg) {
return (Array.isArray(arg))?arg.length !== 0:arg;
});
}
});
Handlebars.registerHelper('neq', function(a, b) {
return a !== b;
// Be able to call a function given in context
Handlebars.registerHelper('call', function () {
var args = Array.prototype.slice.call(arguments);
var func = args.shift();
args.pop();
return func.apply(null, args);
});
Handlebars.registerHelper('in', function(a) {

View file

@ -5,22 +5,13 @@
"advanced": "Advanced",
"remove": "Remove",
"administration_password": "Administration password",
"allowed_users": "Allowed users",
"all_apps": "All apps",
"api_not_responding": "The YunoHost API is not responding. Maybe 'yunohost-api' is down or got restarted?",
"app_access": "Access",
"app_access_addall_btn": "Enable access to all",
"app_access_addall_desc": "All existing users will have access to %s.",
"app_access_clearall_btn": "Clear all access",
"app_access_clearall_desc": "Every user will have access to %s.",
"app_access_removeall_btn": "Remove all access",
"app_access_removeall_desc": "No users will have access to %s.",
"app_access_title": "%s access",
"app_change_label": "Change Label",
"app_change_url": "Change URL",
"app_debug_no_logs": "Application's logs are not available",
"app_debug_tab": "Display debug information",
"app_info_access_desc": "Manage user access. Allowed users: %s",
"app_info_access_desc": "Groups / users currently allowed to access this app:",
"app_info_changelabel_desc": "Change app label in the portal.",
"app_info_debug_desc": "Display debugging information for this application.",
"app_info_default_desc": "Redirect domain root to this application (%s).",
@ -69,10 +60,6 @@
"check_mx": "MX record",
"check_stmp": "port 25 access",
"close": "Close",
"confirm_access_add": "Are you sure you want to add access to %s for all users?",
"confirm_access_clear": "Are you sure you want to clear all access to %s?",
"confirm_access_remove_all": "Are you sure you want to remove all access to %s?",
"confirm_access_remove_user": "Are you sure you want to remove access to %s for %s?",
"confirm_app_change_url": "Are you sure you want to change the app access URL?",
"confirm_app_default": "Are you sure you want to make this app default?",
"confirm_change_maindomain": "Are you sure you want to change the main domain?",
@ -146,7 +133,6 @@
"error_server": "Server error",
"error_server_unexpected": "Unexpected server error (%s)",
"error_connection_interrupted": "The server closed the connection instead of answering it. Has nginx or the yunohost-api been restarted or stoppted for some reason? (Error code/message: %s)",
"everyone_has_access": "Everyone has access.",
"experimental_warning": "Warning: this feature is experimental and not consider stable, you shouldn't be using it except if you know what you are doing.",
"filesystem": "Filesystem",
"firewall": "Firewall",
@ -158,6 +144,20 @@
"gateway": "Gateway: ",
"good_practices_about_admin_password": "You are now about to define a new admin password. The password should be at least 8 characters - though it is good practice to use longer password (i.e. a passphrase) and/or to use various kind of characters (uppercase, lowercase, digits and special characters).",
"good_practices_about_user_password": "You are now about to define a new user password. The password should be at least 8 characters - though it is good practice to use longer password (i.e. a passphrase) and/or to use various kind of characters (uppercase, lowercase, digits and special characters).",
"group": "Group",
"group_name": "Group name",
"group_all_users": "All users",
"group_visitors": "Visitors",
"group_format_name_help": "You can use alpha-numeric chars and space",
"group_add_member": "Add a user",
"group_add_permission": "Add a permission",
"group_new": "New group",
"group_explain_all_users": "This is a special group containing all users accounts on the server",
"group_explain_visitors": "This is a special group representing anonymous visitors",
"group_specific_permissions": "User specific permissions",
"groups_and_permissions": "Groups and permissions",
"groups_and_permissions_manage": "Manage groups and permissions",
"permissions": "Permissions",
"home": "Home",
"hook_adminjs_group_configuration": "System configurations",
"hook_conf_cron": "Automatic tasks",
@ -227,10 +227,9 @@
"network": "Network",
"next": "Next",
"no": "No",
"no_allowed_users": "No allowed users.",
"no_installed_apps": "No installed apps.",
"no_log": "No log.",
"no_user_to_add": "No more users to add.",
"nobody": "Nobody",
"non_compatible_api": "Non-compatible API",
"ok": "OK",
"only_highquality_apps": "Only high-quality apps",
@ -288,7 +287,6 @@
"read_more": "Read more",
"reception": "Reception",
"refresh_app_list": "Refresh list",
"remove_access": "Remove access",
"request_adoption": "waiting adoption",
"request_adoption_details": "The current maintainer would like to stop maintaining this app. Feel free to propose yourself as the new maintainer!",
"request_help": "need help",
@ -298,7 +296,6 @@
"running": "Running",
"save": "Save",
"search_for_apps": "Search for apps...",
"select_user": "Select user",
"select_all": "Select all",
"select_none": "Select none",
"service_description": "Description:",
@ -385,7 +382,6 @@
"users_no": "No users.",
"versions": "Versions",
"version": "Version",
"view_user_profile": "View %s's profile",
"warning_first_user": "You probably need to <a href='#/users/create' class='alert-link'>create a user</a> first.",
"write": "Write",
"wrong_password": "Wrong password",

View file

@ -1,97 +0,0 @@
<div class="btn-breadcrumb">
<a href="#/" ><i class="fa-home"></i><span class="sr-only">{{t 'home'}}</span></a>
<a href="#/apps" class="hidden-xs">{{t 'applications'}}</a>
<a href="#/apps" class="visible-xs">&hellip;</a>
<a href="#/apps/{{settings.id}}">{{settings.label}}</a>
<a href="#/apps/{{settings.id}}/access">{{t 'app_access'}}</a>
</div>
<div class="separator"></div>
<div class="panel panel-default">
<div class="panel-heading">
<h2 class="panel-title"><span class="fa-fw fa-users"></span> {{t 'allowed_users'}}</h2>
</div>
{{#if settings.allowed_users}}
<div class="list-group">
{{#each settings.allowed_users}}
<div class="list-group-item">
<a role="button" href="#/apps/{{settings.id}}/access/remove/{{username}}" class="btn btn-danger slide back pull-right">
<span class="fa-trash-o"></span> {{t 'remove_access'}}
</a>
<h3 class="list-group-item-heading">{{fullname}} ({{mail}})</h3>
<a href="#/users/{{username}}">{{t 'view_user_profile' username}}</a>
</div>
{{/each}}
</div>
{{else}}
<div class="panel-body">
{{#if settings.allow_everyone}}
<p class="text-success">{{t 'everyone_has_access'}}</p>
{{else}}
<p class="text-warning">{{t 'no_allowed_users'}}</p>
{{/if}}
</div>
{{/if}}
<div class="panel-footer">
{{#if users}}
<form method="POST" action="#/apps/{{settings.id}}/access/add" class="row">
<input type="hidden" name="app" value="{{settings.id}}">
<div class="col-sm-6">
<select name="user" required class="form-control">
<option value="" default disabled selected>{{t 'select_user'}}</option>
{{#users}}<option value="{{value}}">{{label}}</option>{{/users}}
</select>
</div>
<div class="col-sm-6">
<br class="visible-xs" />
<input type="submit" class="btn btn-success slide back" value="{{t 'add'}}">
</div>
</form>
{{else}}
<p class="text-warning">{{t 'no_user_to_add'}}</p>
{{/if}}
</div>
</div>
<div class="panel-group" id="accordion">
<div class="panel panel-default">
<div class="panel-heading">
<h2 class="panel-title">
<a data-toggle="collapse" data-parent="#accordion" href="#collapseOne">
<span class="fa-fw fa-wrench"></span> {{t 'operations'}}
</a>
</h2>
</div>
<div id="collapseOne" class="panel-collapse collapse">
<div class="panel-body">
{{#if users}}
<div class="container">
<p>{{t 'app_access_addall_desc' settings.label}}</p>
<a role="button" href="#/apps/{{settings.id}}/access/add" class="btn btn-success slide back">
<span class="fa-plus"></span> {{t 'app_access_addall_btn'}}
</a>
</div>
<hr>
{{/if}}
{{#if settings.allowed_users}}
<div class="container">
<p>{{t 'app_access_removeall_desc' settings.label}}</p>
<a role="button" href="#/apps/{{settings.id}}/access/remove" class="btn btn-danger slide back">
<span class="fa-trash-o"></span> {{t 'app_access_removeall_btn'}}
</a>
</div>
<hr>
{{/if}}
{{#unless settings.allow_everyone}}
<div class="container">
<p>{{t 'app_access_clearall_desc' settings.label}}</p>
<a role="button" href="#/apps/{{settings.id}}/access/clear" class="btn btn-primary slide back">
<span class="fa-unlock-alt"></span> {{t 'app_access_clearall_btn'}}
</a>
</div>
{{/unless}}
</div>
</div>
</div>
</div>

View file

@ -48,9 +48,10 @@
</div>
<hr>
<div class="container">
<p>{{t 'app_info_access_desc' settings.allowed_users}}</p>
<a role="button" href="#/apps/{{settings.id}}/access" class="btn btn-info slide">
<span class="fa-lock"></span> {{t 'app_access'}}
<p>{{t 'app_info_access_desc'}} {{#each permissions}} {{ucwords .}}{{#unless @last}}, {{/unless}} {{ else }} {{t 'nobody'}} {{/each}}
</p>
<a role="button" href="#/groups" class="btn btn-info slide">
<span class="fa-key-modern"></span> {{t 'groups_and_permissions_manage'}}
</a>
</div>
<hr>

View file

@ -0,0 +1,30 @@
<div class="btn-breadcrumb">
<a href="#/" ><i class="fa-home"></i><span class="sr-only">{{t 'home'}}</span></a>
<a href="#/users" class="visible-xs">&hellip;</a>
<a href="#/users" class="hidden-xs">{{t 'users'}}</a>
<a href="#/groups" class="visible-xs">&hellip;</a>
<a href="#/groups" class="hidden-xs">{{t 'group_permissions'}}</a>
<a href="#/groups/create">{{t 'group_new'}}</a>
</div>
<div class="separator"></div>
<form action="#/groups/create" method="POST" class="form-horizontal">
<div class="panel panel-default">
<div class="panel-body">
<div class="form-group">
<label for="groupname" class="col-sm-3 control-label">{{t 'group_name'}}</label>
<div class="col-sm-9">
<input type="text" id="groupname" name="groupname" class="form-control" placeholder="my group name" required pattern="[A-Za-z0-9_ ]+">
<div class="help-block">{{t 'group_format_name_help'}}</div>
</div>
</div>
</div>
</div>
<div class="text-center">
<input type="submit" role="button" class="btn btn-success slide back" value="{{t 'save'}}">
</div>
</form>

View file

@ -0,0 +1,134 @@
<div class="btn-breadcrumb">
<a href="#/"><i class="fa-home"></i><span class="sr-only">{{t 'home'}}</span></a>
<a href="#/users">{{t 'users'}}</a>
<a href="#/groups">{{t 'groups_and_permissions'}}</a>
</div>
<div class="actions-group">
<a role="button" href="#/groups/create" class="btn btn-success slide">
<span class="fa-plus"></span> {{t 'group_new'}}
</a>
</div>
<div class="separator"></div>
{{!-- ======================== Partial inline view ======================= --}}
{{#*inline "label"}}
<span class="label label-default label-removable">
<span class="fa-fw fa-{{icon}}"></span>
{{text}}
<a role="button" data-type="{{type}}s" data-operation="remove" data-item="{{value}}" data-group="{{group}}" class="group-update">
<span class="fa-close" style="margin-left:5px"></span>
<span class="sr-only">{{t 'delete'}}</span>
</a>
</span>
{{/inline}}
{{#*inline "labelsLine"}}
{{#each items}}
{{> label text=(call ../display .) value=. icon=../icon type=../type item=. group=../group}}
{{/each}}
{{#if inv}}
<div class="btn-group">
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="fa-plus"></span> {{t (concat 'group_add_' type)}}
</button>
<ul class="dropdown-menu">
{{#each inv}}
<li><a href="#" data-type="{{../type}}s" data-operation="add" data-item="{{.}}" data-group="{{../group}}" class="group-update">{{call ../display .}}</a></li>
{{/each}}
</ul>
</div>
{{/if}}
{{/inline}}
<div id="view-groups">
{{!-- ======================== Groups ======================= --}}
{{#each groups}}
{{#unless primary}}
<div class="panel panel-default">
<div class="panel-heading" role="tab" id="heading-context-group-{{@key}}">
<h2 class="panel-title">
<a role="button" data-toggle="collapse" href="#collapse-group-{{@key}}" aria-expanded="false" aria-controls="collapse-group-{{@key}}">
<span class="fa-fw fa-group"></span> {{#if special}}{{t (concat 'group_' @key)}}{{else}}{{t 'group'}} "{{ucwords @key}}"{{/if}}
</a>
{{#unless special}}
<a href="#/groups/{{@key}}/delete" role="button" class="group-delete">
<span class="fa-close"></span>
<span class="sr-only">{{t 'delete'}}</span>
</a>
{{/unless}}
</h2>
</div>
<div id="collapse-group-{{@key}}" class="panel-collapse collapse in" role="tabpanel" aria-labelledby="heading-context-group-{{@key}}">
<div class="panel-body">
<div class="row">
<div class="col-sm-2">
<h3>{{t 'users'}}</h3>
</div>
<div class="col-sm-10">
{{#if special}}
<div style="font-style:italic"><span class="fa-info-circle"></span> {{t (concat 'group_explain_' @key)}}</div>
{{else}}
{{> labelsLine display=../displayUser icon="user" type="member" items=members inv=membersInv group=@key}}
{{/if}}
</div>
</div>
<hr />
<div class="row">
<div class="col-sm-2">
<h3>{{t 'permissions'}}</h3>
</div>
<div class="col-sm-10">
{{> labelsLine display=../displayPermission icon="key-modern" type="permission" items=permissions inv=permissionsInv group=@key}}
</div>
</div>
</div>
</div>
</div>
{{/unless}}
{{/each}}
{{!-- ====================== User specific permissions ==================== --}}
<div class="panel panel-info">
<div class="panel-heading" role="tab" id="heading-context-specific">
<h2 class="panel-title">
<a role="button" data-toggle="collapse" href="#collapse-specific" aria-expanded="false" aria-controls="collapse-specific">
<span class="fa-fw fa-group"></span> {{t 'group_specific_permissions'}}
</a>
</h2>
</div>
<div id="collapse-specific" class="panel-collapse collapse in" role="tabpanel" aria-labelledby="heading-context-specific">
<div class="panel-body">
{{#each groups}}
{{#if (or (and primary permissions) display)}}
<div class="row">
<div class="col-sm-2">
<h3><span class="fa-fw fa-user"></span> {{@key}}</h3>
</div>
<div class="col-sm-10">
{{> labelsLine display=../displayPermission icon="key-modern" type="permission" items=permissions inv=permissionsInv group=@key}}
</div>
</div>
<hr />
{{/if}}
{{/each}}
<div class="btn-group">
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="fa-plus"></span> {{t 'group_add_member'}}
</button>
<ul class="dropdown-menu">
{{#each groups}}
{{#if primary}}
{{#unless (or permissions display)}}
<li><a href="#" data-user="{{@key}}" class="group-add-user">{{@key}}</a></li>
{{/unless}}
{{/if}}
{{/each}}
</ul>
</div>
</div>
</div>
</div>
</div>

View file

@ -4,6 +4,9 @@
</div>
<div class="actions-group">
<a href="#/groups" class="btn btn-info">
<span class="fa-key-modern"></span> {{t 'groups_and_permissions_manage'}}
</a>
<a role="button" href="#/users/create" class="btn btn-success slide">
<span class="fa-plus"></span> {{t 'users_new'}}
</a>