From ed11c0ad11d2af9f0b0131cf4449117d42f0f032 Mon Sep 17 00:00:00 2001 From: sauls8t Date: Thu, 1 Mar 2018 19:14:27 +0000 Subject: [PATCH] add/remove group membership --- domain/auth/keycloak/endpoint.go | 2 +- domain/group/mysql/store.go | 22 +++ domain/storer.go | 3 +- domain/user/endpoint.go | 24 +++- domain/user/mysql/store.go | 22 ++- gui/app/components/customize/user-list.js | 62 +++++++-- gui/app/models/user.js | 2 +- gui/app/pods/customize/users/controller.js | 16 ++- gui/app/pods/customize/users/route.js | 4 +- gui/app/pods/customize/users/template.hbs | 3 +- gui/app/services/user.js | 9 +- gui/app/styles/view/customize.scss | 52 ++++++- .../components/customize/user-admin.hbs | 2 +- .../components/customize/user-groups.hbs | 8 +- .../components/customize/user-list.hbs | 131 +++++++++++++----- model/group/group.go | 10 ++ model/user/user.go | 2 + 17 files changed, 287 insertions(+), 87 deletions(-) diff --git a/domain/auth/keycloak/endpoint.go b/domain/auth/keycloak/endpoint.go index 2107aa23..3abfcb5b 100644 --- a/domain/auth/keycloak/endpoint.go +++ b/domain/auth/keycloak/endpoint.go @@ -94,7 +94,7 @@ func (h *Handler) Sync(w http.ResponseWriter, r *http.Request) { } // User list from Documize - dmzUsers, err := h.Store.User.GetUsersForOrganization(ctx) + dmzUsers, err := h.Store.User.GetUsersForOrganization(ctx, "") if err != nil { result.Message = "Error: unable to fetch Documize users" result.IsError = true diff --git a/domain/group/mysql/store.go b/domain/group/mysql/store.go index 3a7015c7..21fa8361 100644 --- a/domain/group/mysql/store.go +++ b/domain/group/mysql/store.go @@ -141,3 +141,25 @@ func (s Scope) LeaveGroup(ctx domain.RequestContext, groupID, userID string) (er return } + +// GetMembers returns members for every group. +// Useful when you need to bulk fetch membership records +// for subsequent processing. +func (s Scope) GetMembers(ctx domain.RequestContext) (r []group.Record, err error) { + err = s.Runtime.Db.Select(&r, + `SELECT a.id, a.orgid, a.roleid, a.userid, b.role as name, b.purpose + FROM rolemember a, role b + WHERE a.orgid=? AND a.roleid=b.refid + ORDER BY a.userid`, + ctx.OrgID) + + if err == sql.ErrNoRows || len(r) == 0 { + err = nil + r = []group.Record{} + } + if err != nil { + err = errors.Wrap(err, "select group members") + } + + return +} diff --git a/domain/storer.go b/domain/storer.go index 0562e8a7..42f91e14 100644 --- a/domain/storer.go +++ b/domain/storer.go @@ -110,7 +110,7 @@ type UserStorer interface { GetByToken(ctx RequestContext, token string) (u user.User, err error) GetBySerial(ctx RequestContext, serial string) (u user.User, err error) GetActiveUsersForOrganization(ctx RequestContext) (u []user.User, err error) - GetUsersForOrganization(ctx RequestContext) (u []user.User, err error) + GetUsersForOrganization(ctx RequestContext, filter string) (u []user.User, err error) GetSpaceUsers(ctx RequestContext, spaceID string) (u []user.User, err error) GetUsersForSpaces(ctx RequestContext, spaces []string) (u []user.User, err error) UpdateUser(ctx RequestContext, u user.User) (err error) @@ -277,6 +277,7 @@ type GroupStorer interface { Update(ctx RequestContext, g group.Group) (err error) Delete(ctx RequestContext, refID string) (rows int64, err error) GetGroupMembers(ctx RequestContext, groupID string) (m []group.Member, err error) + GetMembers(ctx RequestContext) (r []group.Record, err error) JoinGroup(ctx RequestContext, groupID, userID string) (err error) LeaveGroup(ctx RequestContext, groupID, userID string) (err error) } diff --git a/domain/user/endpoint.go b/domain/user/endpoint.go index d5b877ec..fcc8c672 100644 --- a/domain/user/endpoint.go +++ b/domain/user/endpoint.go @@ -35,6 +35,7 @@ import ( "github.com/documize/community/domain/organization" "github.com/documize/community/model/account" "github.com/documize/community/model/audit" + "github.com/documize/community/model/group" "github.com/documize/community/model/user" ) @@ -228,6 +229,8 @@ func (h *Handler) GetOrganizationUsers(w http.ResponseWriter, r *http.Request) { return } + filter := request.Query(r, "filter") + active, err := strconv.ParseBool(request.Query(r, "active")) if err != nil { active = false @@ -243,20 +246,33 @@ func (h *Handler) GetOrganizationUsers(w http.ResponseWriter, r *http.Request) { return } } else { - u, err = h.Store.User.GetUsersForOrganization(ctx) + u, err = h.Store.User.GetUsersForOrganization(ctx, filter) if err != nil && err != sql.ErrNoRows { response.WriteServerError(w, method, err) h.Runtime.Log.Error(method, err) return } } - - if len(u) == 0 { - u = []user.User{} + // prefetch all group membership records + groups, err := h.Store.Group.GetMembers(ctx) + if err != nil && err != sql.ErrNoRows { + response.WriteServerError(w, method, err) + h.Runtime.Log.Error(method, err) + return } + // for each user... for i := range u { + // 1. attach user accounts AttachUserAccounts(ctx, *h.Store, ctx.OrgID, &u[i]) + + // 2. attach user groups + u[i].Groups = []group.Record{} + for j := range groups { + if groups[j].UserID == u[i].RefID { + u[i].Groups = append(u[i].Groups, groups[j]) + } + } } response.WriteJSON(w, u) diff --git a/domain/user/mysql/store.go b/domain/user/mysql/store.go index 45a5d056..1d3b69f5 100644 --- a/domain/user/mysql/store.go +++ b/domain/user/mysql/store.go @@ -118,6 +118,11 @@ func (s Scope) GetActiveUsersForOrganization(ctx domain.RequestContext) (u []use ORDER BY u.firstname,u.lastname`, ctx.OrgID) + if err == sql.ErrNoRows || len(u) == 0 { + err = nil + u = []user.User{} + } + if err != nil { err = errors.Wrap(err, fmt.Sprintf("get active users by org %s", ctx.OrgID)) } @@ -127,13 +132,24 @@ func (s Scope) GetActiveUsersForOrganization(ctx domain.RequestContext) (u []use // GetUsersForOrganization returns a slice containing all of the user records for the organizaiton // identified in the Persister. -func (s Scope) GetUsersForOrganization(ctx domain.RequestContext) (u []user.User, err error) { +func (s Scope) GetUsersForOrganization(ctx domain.RequestContext, filter string) (u []user.User, err error) { + filter = strings.TrimSpace(strings.ToLower(filter)) + likeQuery := "" + if len(filter) > 0 { + likeQuery = " AND (LOWER(u.firstname) LIKE '%" + filter + "%' OR LOWER(u.lastname) LIKE '%" + filter + "%' OR LOWER(u.email) LIKE '%" + filter + "%') " + } + err = s.Runtime.Db.Select(&u, `SELECT u.id, u.refid, u.firstname, u.lastname, u.email, u.initials, u.password, u.salt, u.reset, u.created, u.revised, u.global, a.active, a.editor, a.admin, a.users as viewusers FROM user u, account a - WHERE u.refid=a.userid AND a.orgid=? - ORDER BY u.firstname, u.lastname`, ctx.OrgID) + WHERE u.refid=a.userid AND a.orgid=? `+likeQuery+ + `ORDER BY u.firstname, u.lastname LIMIT 100`, ctx.OrgID) + + if err == sql.ErrNoRows || len(u) == 0 { + err = nil + u = []user.User{} + } if err != nil { err = errors.Wrap(err, fmt.Sprintf(" get users for org %s", ctx.OrgID)) diff --git a/gui/app/components/customize/user-list.js b/gui/app/components/customize/user-list.js index e1fcd8d4..74d54113 100644 --- a/gui/app/components/customize/user-list.js +++ b/gui/app/components/customize/user-list.js @@ -10,12 +10,14 @@ // https://documize.com import $ from 'jquery'; -import Component from '@ember/component'; +import { inject as service } from '@ember/service'; import { schedule, debounce } from '@ember/runloop'; +import Component from '@ember/component'; import AuthProvider from '../../mixins/auth'; import ModalMixin from '../../mixins/modal'; export default Component.extend(AuthProvider, ModalMixin, { + groupSvc: service('group'), editUser: null, deleteUser: null, filter: '', @@ -25,13 +27,16 @@ export default Component.extend(AuthProvider, ModalMixin, { init() { this._super(...arguments); this.password = {}; - this.filteredUsers = []; this.selectedUsers = []; }, didReceiveAttrs() { this._super(...arguments); + this.get('groupSvc').getAll().then((groups) => { + this.set('groups', groups); + }); + let users = this.get('users'); users.forEach(user => { @@ -40,7 +45,6 @@ export default Component.extend(AuthProvider, ModalMixin, { }); this.set('users', users); - this.set('filteredUsers', users); }, onKeywordChange: function () { @@ -48,17 +52,7 @@ export default Component.extend(AuthProvider, ModalMixin, { }.observes('filter'), filterUsers() { - let users = this.get('users'); - let filteredUsers = []; - let filter = this.get('filter').toLowerCase(); - - users.forEach(user => { - if (user.get('fullname').toLowerCase().includes(filter) || user.get('email').toLowerCase().includes(filter)) { - filteredUsers.pushObject(user); - } - }); - - this.set('filteredUsers', filteredUsers); + this.get('onFilter')(this.get('filter')); }, actions: { @@ -184,6 +178,44 @@ export default Component.extend(AuthProvider, ModalMixin, { this.set('hasSelectedUsers', false); this.modalClose('#admin-user-delete-modal'); - } + }, + + onShowGroupsModal(userId) { + let user = this.get('users').findBy('id', userId); + this.set('selectedUser', user); + + let userGroups = user.get('groups'); + + // mark up groups user belongs to... + let groups = this.get('groups'); + groups.forEach((g) => { + console.log(userGroups); + let hasGroup = userGroups.findBy('roleId', g.get('id')); + g.set('isMember', is.not.undefined(hasGroup)); + }) + this.set('groups', groups); + + this.modalOpen("#group-member-modal", {"show": true}); + }, + + onLeaveGroup(groupId) { + let userId = this.get('selectedUser.id'); + let group = this.get('groups').findBy('id', groupId); + group.set('isMember', false); + + this.get('groupSvc').leave(groupId, userId).then(() => { + this.filterUsers(); + }); + }, + + onJoinGroup(groupId) { + let userId = this.get('selectedUser.id'); + let group = this.get('groups').findBy('id', groupId); + group.set('isMember', true); + + this.get('groupSvc').join(groupId, userId).then(() => { + this.filterUsers(); + }); + } } }); diff --git a/gui/app/models/user.js b/gui/app/models/user.js index 70afaa0b..ef23b526 100644 --- a/gui/app/models/user.js +++ b/gui/app/models/user.js @@ -12,7 +12,6 @@ import { computed } from '@ember/object'; import Model from 'ember-data/model'; import attr from 'ember-data/attr'; -// import { belongsTo, hasMany } from 'ember-data/relationships'; export default Model.extend({ firstname: attr('string'), @@ -25,6 +24,7 @@ export default Model.extend({ viewUsers: attr('boolean', { defaultValue: false }), global: attr('boolean', { defaultValue: false }), accounts: attr(), + groups: attr(), created: attr(), revised: attr(), diff --git a/gui/app/pods/customize/users/controller.js b/gui/app/pods/customize/users/controller.js index f599f3b1..da5fac75 100644 --- a/gui/app/pods/customize/users/controller.js +++ b/gui/app/pods/customize/users/controller.js @@ -15,8 +15,8 @@ import Controller from '@ember/controller'; export default Controller.extend({ userService: service('user'), - loadUsers() { - this.get('userService').getComplete().then((users) => { + loadUsers(filter) { + this.get('userService').getComplete(filter).then((users) => { this.set('model', users); }); }, @@ -30,26 +30,28 @@ export default Controller.extend({ onAddUsers(list) { return this.get('userService').addBulk(list).then(() => { - this.loadUsers(); + this.loadUsers(''); }); }, onDelete(userId) { this.get('userService').remove(userId).then( () => { - this.loadUsers(); + this.loadUsers(''); }); }, onSave(user) { this.get('userService').save(user).then(() => { - this.get('userService').getComplete().then((users) => { - this.set('model', users); - }); + this.loadUsers(''); }); }, onPassword(user, password) { this.get('userService').updatePassword(user.id, password); + }, + + onFilter(filter) { + this.loadUsers(filter); } } }); diff --git a/gui/app/pods/customize/users/route.js b/gui/app/pods/customize/users/route.js index 78fd6a47..bc06c4fa 100644 --- a/gui/app/pods/customize/users/route.js +++ b/gui/app/pods/customize/users/route.js @@ -30,12 +30,12 @@ export default Route.extend(AuthenticatedRouteMixin, { return new EmberPromise((resolve) => { if (this.get('appMeta.authProvider') == constants.AuthProvider.Keycloak) { this.get('global').syncExternalUsers().then(() => { - this.get('userService').getComplete().then((users) =>{ + this.get('userService').getComplete('').then((users) =>{ resolve(users); }); }); } else { - this.get('userService').getComplete().then((users) => { + this.get('userService').getComplete('').then((users) => { resolve(users); }); } diff --git a/gui/app/pods/customize/users/template.hbs b/gui/app/pods/customize/users/template.hbs index 875ef778..87f5571c 100644 --- a/gui/app/pods/customize/users/template.hbs +++ b/gui/app/pods/customize/users/template.hbs @@ -1,8 +1,9 @@ -{{customize/user-admin +{{customize/user-admin users=model onAddUser=(action 'onAddUser') onAddUsers=(action 'onAddUsers')}} {{customize/user-list users=model + onFilter=(action "onFilter") onDelete=(action "onDelete") onSave=(action "onSave") onPassword=(action "onPassword")}} diff --git a/gui/app/services/user.js b/gui/app/services/user.js index e3283bf1..9e2a3335 100644 --- a/gui/app/services/user.js +++ b/gui/app/services/user.js @@ -64,8 +64,13 @@ export default Service.extend({ }, // Returns all active and inactive users for organization. - getComplete() { - return this.get('ajax').request(`users?active=0`).then((response) => { + // Only available for admins and limits results to max. 100 users. + // Takes filter for user search criteria. + getComplete(filter) { + filter = filter.trim(); + if (filter.length > 0) filter = encodeURIComponent(filter); + + return this.get('ajax').request(`users?active=0&filter=${filter}`).then((response) => { return response.map((obj) => { let data = this.get('store').normalize('user', obj); return this.get('store').push(data); diff --git a/gui/app/styles/view/customize.scss b/gui/app/styles/view/customize.scss index 7cfa1516..b9748bbb 100644 --- a/gui/app/styles/view/customize.scss +++ b/gui/app/styles/view/customize.scss @@ -11,16 +11,37 @@ } .user-table { - .name { - font-size: 1rem; - color: $color-off-black; - margin: 0 0 0 30px; + tbody tr td, thead tr th { + border-top: none !important; + border-bottom: none !important; } - .email { - font-size: 0.9rem; + .name { + font-size: 1.2rem; + font-weight: bold; + color: $color-link; + margin: 0 0 0 10px; + display: inline-block; + cursor: pointer; + + > .email { + font-size: 0.9rem; + color: $color-off-black; + margin: 0; + display: inline-block; + font-weight: normal; + } + } + + .groups { + cursor: pointer; + margin: 5px 0 0 10px; + font-size: 1rem; color: $color-gray; - margin: 0 0 0 30px; + + &:hover { + color: $color-link; + } } .inactive-user @@ -76,6 +97,7 @@ } } + // used for group admin > .group-users-members { > .item { margin: 10px 0; @@ -86,4 +108,20 @@ } } } + + // used for user admin + > .group-membership { + > .item { + margin: 10px 0; + + > .group-name { + color: $color-primary; + font-size: 1.2rem; + + > .group-purpose { + font-size: 0.9rem; + } + } + } + } } diff --git a/gui/app/templates/components/customize/user-admin.hbs b/gui/app/templates/components/customize/user-admin.hbs index a8cb4078..240c2e23 100644 --- a/gui/app/templates/components/customize/user-admin.hbs +++ b/gui/app/templates/components/customize/user-admin.hbs @@ -2,7 +2,7 @@

Users

-

Set basic information, passwords and permissions for {{model.length}} users

+

Set basic information, passwords and permissions for {{users.length}} users

{{#if isAuthProviderDocumize}}
Add user
diff --git a/gui/app/templates/components/customize/user-groups.hbs b/gui/app/templates/components/customize/user-groups.hbs index 5d047a69..a020795a 100644 --- a/gui/app/templates/components/customize/user-groups.hbs +++ b/gui/app/templates/components/customize/user-groups.hbs @@ -115,7 +115,7 @@
{{input id="group-members-search" type="text" class="form-control mousetrap" placeholder="Search members and users..." value=searchText key-up=(action 'onSearch')}} - matches firstname, lastname, email + search firstname, lastname, email
@@ -124,7 +124,7 @@
{{member.fullname}}
- +
{{/each}} @@ -135,9 +135,9 @@
{{user.firstname}} {{user.lastname}}
{{#if user.isMember}} - + {{else}} - + {{/if}}
diff --git a/gui/app/templates/components/customize/user-list.hbs b/gui/app/templates/components/customize/user-list.hbs index 2fb2022e..b92fd0a6 100644 --- a/gui/app/templates/components/customize/user-list.hbs +++ b/gui/app/templates/components/customize/user-list.hbs @@ -1,37 +1,45 @@ -
-

Users

- +
+ +
+ Spaces +   — can add spaces, both personal and shared with others +
+
+ Visible +   — can see names of users and groups, can disable for external users like customers/partners +
+
+ Admin +   — can manage all aspects of Documize, like this screen +
+
+ Active +   — can login and use Documize +
+ +
+ {{focus-input type="text" class="form-control" placeholder="filter users" value=filter}} + search firstname, lastname, email +
+ +
- - - + + + - {{#each filteredUsers key="id" as |user|}} + {{#each users key="id" as |user|}} {{/each}} @@ -151,3 +159,50 @@

Are you sure you want to delete {{deleteUser.fullname}}?

{{/ui/ui-dialog}} + + + \ No newline at end of file diff --git a/model/group/group.go b/model/group/group.go index e20258c7..75f4f3be 100644 --- a/model/group/group.go +++ b/model/group/group.go @@ -31,3 +31,13 @@ type Member struct { Firstname string `json:"firstname"` //read-only info Lastname string `json:"lastname"` //read-only info } + +// Record details user membership of a user group. +type Record struct { + ID uint64 `json:"id"` + OrgID string `json:"orgId"` + RoleID string `json:"roleId"` + UserID string `json:"userId"` + Name string `json:"name"` + Purpose string `json:"purpose"` +} diff --git a/model/user/user.go b/model/user/user.go index e3e7f3e6..20bf36fe 100644 --- a/model/user/user.go +++ b/model/user/user.go @@ -16,6 +16,7 @@ import ( "github.com/documize/community/model" "github.com/documize/community/model/account" + "github.com/documize/community/model/group" ) // User defines a login. @@ -34,6 +35,7 @@ type User struct { Salt string `json:"-"` Reset string `json:"-"` Accounts []account.Account `json:"accounts"` + Groups []group.Record `json:"groups"` } // ProtectSecrets blanks sensitive data.
{{input type="text" class="form-control" placeholder="filter users" value=filter}}Add SpaceView Users + {{#if hasSelectedUsers}} + + {{/if}} + SpacesVisible Admin Active - {{#if hasSelectedUsers}} - - - - {{/if}}
@@ -44,8 +52,16 @@ {{/if}}
-
{{ user.fullname }}
- +
{{user.fullname}}
+
+ {{#each user.groups as |group|}} + + {{group.name}}{{#if (not-eq group user.groups.lastObject)}}, {{/if}} + + {{else}} + <no groups> + {{/each}} +
@@ -81,19 +97,11 @@ {{/if}} - {{#if user.me}} -
- edit -
- {{else}} -
- edit -
-
-
+ {{#unless user.me}} +
delete
- {{/if}} + {{/unless}}