diff --git a/src/bower.json b/src/bower.json index 0d0e89f5..1928e289 100644 --- a/src/bower.json +++ b/src/bower.json @@ -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", diff --git a/src/css/style.less b/src/css/style.less index d5565166..21b27fa8 100644 --- a/src/css/style.less +++ b/src/css/style.less @@ -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; diff --git a/src/gulpfile.js b/src/gulpfile.js index 5875faf9..e5f183e8 100644 --- a/src/gulpfile.js +++ b/src/gulpfile.js @@ -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', diff --git a/src/js/yunohost/controllers/apps.js b/src/js/yunohost/controllers/apps.js index 26630ddc..3bff1be4 100644 --- a/src/js/yunohost/controllers/apps.js +++ b/src/js/yunohost/controllers/apps.js @@ -202,20 +202,23 @@ // 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) { - // Multilingual description - data.description = (typeof data.manifest.description[y18n.locale] !== 'undefined') ? - data.manifest.description[y18n.locale] : - data.manifest.description['en'] - ; + // Permissions + data.permissions = data_permissions.permissions[c.params['app']+".main"]["allowed"]; - // Multi Instance settings - data.manifest.multi_instance = data.manifest.multi_instance ? y18n.t('yes') : y18n.t('no'); - data.install_time = new Date(data.settings.install_time * 1000); + // Multilingual description + data.description = (typeof data.manifest.description[y18n.locale] !== 'undefined') ? + data.manifest.description[y18n.locale] : + data.manifest.description['en'] + ; - c.view('app/app_info', data); + // Multi Instance settings + data.manifest.multi_instance = data.manifest.multi_instance ? y18n.t('yes') : y18n.t('no'); + data.install_time = new Date(data.settings.install_time * 1000); + + c.view('app/app_info', data); + }); }); }); @@ -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( diff --git a/src/js/yunohost/controllers/users.js b/src/js/yunohost/controllers/users.js index 92e235ad..655ff4c5 100644 --- a/src/js/yunohost/controllers/users.js +++ b/src/js/yunohost/controllers/users.js @@ -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.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 = $('
'+ y18n.t('confirm_delete', [c.params['group']]) +'
'); + + // 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 * @@ -234,5 +424,8 @@ }); }); + + + })(); diff --git a/src/js/yunohost/main.js b/src/js/yunohost/main.js index ea00fe6c..4d6ef4e4 100644 --- a/src/js/yunohost/main.js +++ b/src/js/yunohost/main.js @@ -101,12 +101,43 @@ // equality stuff because mustache/Handlebars is lame // source https://stackoverflow.com/a/31632215 - Handlebars.registerHelper('eq', function(a, b) { - return 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) { diff --git a/src/locales/en.json b/src/locales/en.json index a4a70211..f6c5c577 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -5,22 +5,13 @@ "advanced": "Advanced", "remove": "Remove", "administration_password": "Administration password", - "allowed_users": "Allowed users", - "all_apps": "All apps", + "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 create a user first.", "write": "Write", "wrong_password": "Wrong password", diff --git a/src/views/app/app_access.ms b/src/views/app/app_access.ms deleted file mode 100644 index 72a2d4f8..00000000 --- a/src/views/app/app_access.ms +++ /dev/null @@ -1,97 +0,0 @@ -
- {{t 'home'}} - - - {{settings.label}} - {{t 'app_access'}} -
- -
- -
-
-

{{t 'allowed_users'}}

-
- {{#if settings.allowed_users}} -
- {{#each settings.allowed_users}} - - {{/each}} -
- {{else}} -
- {{#if settings.allow_everyone}} -

{{t 'everyone_has_access'}}

- {{else}} -

{{t 'no_allowed_users'}}

- {{/if}} -
- {{/if}} - -
- -
-
- -
-
- {{#if users}} -
-

{{t 'app_access_addall_desc' settings.label}}

- - {{t 'app_access_addall_btn'}} - -
-
- {{/if}} - {{#if settings.allowed_users}} -
-

{{t 'app_access_removeall_desc' settings.label}}

- - {{t 'app_access_removeall_btn'}} - -
-
- {{/if}} - {{#unless settings.allow_everyone}} -
-

{{t 'app_access_clearall_desc' settings.label}}

- - {{t 'app_access_clearall_btn'}} - -
- {{/unless}} -
-
-
-
diff --git a/src/views/app/app_info.ms b/src/views/app/app_info.ms index 4ccfa1dc..53525c05 100644 --- a/src/views/app/app_info.ms +++ b/src/views/app/app_info.ms @@ -48,9 +48,10 @@
-

{{t 'app_info_access_desc' settings.allowed_users}}

- - {{t 'app_access'}} +

{{t 'app_info_access_desc'}} {{#each permissions}} {{ucwords .}}{{#unless @last}}, {{/unless}} {{ else }} {{t 'nobody'}} {{/each}} +

+
+ {{t 'groups_and_permissions_manage'}}

diff --git a/src/views/user/group_create.ms b/src/views/user/group_create.ms new file mode 100644 index 00000000..db60c2dc --- /dev/null +++ b/src/views/user/group_create.ms @@ -0,0 +1,30 @@ +
+ {{t 'home'}} + + + + + {{t 'group_new'}} +
+ +
+ +
+ +
+
+
+ +
+ +
{{t 'group_format_name_help'}}
+
+
+
+
+ +
+ +
+ +
diff --git a/src/views/user/group_list.ms b/src/views/user/group_list.ms new file mode 100644 index 00000000..7b68e404 --- /dev/null +++ b/src/views/user/group_list.ms @@ -0,0 +1,134 @@ +
+ {{t 'home'}} + {{t 'users'}} + {{t 'groups_and_permissions'}} +
+ +
+ + {{t 'group_new'}} + +
+ +
+ +{{!-- ======================== Partial inline view ======================= --}} +{{#*inline "label"}} + + + {{text}} + + + {{t 'delete'}} + + +{{/inline}} + +{{#*inline "labelsLine"}} + {{#each items}} + {{> label text=(call ../display .) value=. icon=../icon type=../type item=. group=../group}} + {{/each}} + {{#if inv}} +
+ + +
+ {{/if}} +{{/inline}} +
+ +{{!-- ======================== Groups ======================= --}} +{{#each groups}} +{{#unless primary}} +
+ +
+
+
+
+

{{t 'users'}}

+
+
+ {{#if special}} +
{{t (concat 'group_explain_' @key)}}
+ {{else}} + {{> labelsLine display=../displayUser icon="user" type="member" items=members inv=membersInv group=@key}} + {{/if}} +
+
+
+
+
+

{{t 'permissions'}}

+
+
+ {{> labelsLine display=../displayPermission icon="key-modern" type="permission" items=permissions inv=permissionsInv group=@key}} +
+
+
+
+
+{{/unless}} +{{/each}} + + +{{!-- ====================== User specific permissions ==================== --}} +
+ +
+
+ {{#each groups}} + {{#if (or (and primary permissions) display)}} +
+
+

{{@key}}

+
+
+ {{> labelsLine display=../displayPermission icon="key-modern" type="permission" items=permissions inv=permissionsInv group=@key}} +
+
+
+ {{/if}} + {{/each}} +
+ + +
+
+
+
+
diff --git a/src/views/user/user_list.ms b/src/views/user/user_list.ms index b2aebff6..5ab00db6 100644 --- a/src/views/user/user_list.ms +++ b/src/views/user/user_list.ms @@ -4,6 +4,9 @@
+ + {{t 'groups_and_permissions_manage'}} + {{t 'users_new'}}