Pārlūkot izejas kodu

feat: update user

Nick 5 gadi atpakaļ
vecāks
revīzija
823ff1bc61

+ 1 - 1
client/components/admin/admin-groups-edit.vue

@@ -14,7 +14,7 @@
             v-icon mdi-arrow-left
           v-dialog(v-model='deleteGroupDialog', max-width='500', v-if='!group.isSystem')
             template(v-slot:activator='{ on }')
-              v-btn(color='red', large, outlined, v-on='{ on }')
+              v-btn.ml-2(color='red', large, outlined, v-on='{ on }')
                 v-icon(color='red') mdi-trash-can-outline
             v-card
               .dialog-header.is-red Delete Group?

+ 1 - 1
client/components/admin/admin-locale.vue

@@ -120,7 +120,7 @@
                     v-btn(v-else-if='item.isInstalled && item.installDate < item.updatedAt', icon, small, @click='download(item)')
                       v-icon.blue--text mdi-cached
                     v-btn(v-else-if='item.isInstalled', icon, small, @click='download(item)')
-                      v-icon.green--text mdi-check
+                      v-icon.green--text mdi-check-bold
                     v-btn(v-else, icon, small, @click='download(item)')
                       v-icon.grey--text mdi-cloud-download
               v-card.wiki-form.mt-3.animated.fadeInUp.wait-p5s

+ 2 - 2
client/components/admin/admin-theme.vue

@@ -114,7 +114,7 @@
                   item-key='value',
                   :items-per-page='1000'
                 )
-                  template(v-slot:items='thm')
+                  template(v-slot:item='thm')
                     td
                       strong {{thm.item.text}}
                     td
@@ -124,7 +124,7 @@
                       v-btn(v-else-if='thm.item.isInstalled && thm.item.installDate < thm.item.updatedAt', icon)
                         v-icon.blue--text mdi-cached
                       v-btn(v-else-if='thm.item.isInstalled', icon)
-                        v-icon.green--text mdi-check
+                        v-icon.green--text mdi-check-bold
                       v-btn(v-else, icon)
                         v-icon.grey--text mdi-cloud-download
 </template>

+ 3 - 1
client/components/admin/admin-users-create.vue

@@ -67,10 +67,12 @@
           :items='groups'
           item-text='name'
           item-value='id'
+          item-disabled='isSystem'
           outlined
           prepend-icon='mdi-account-group'
           v-model='group'
           label='Assign to Group(s)...'
+          dense
           clearable
           multiple
           )
@@ -104,7 +106,7 @@ import _ from 'lodash'
 
 import createUserMutation from 'gql/admin/users/users-mutation-create.gql'
 import providersQuery from 'gql/admin/users/users-query-strategies.gql'
-import groupsQuery from 'gql/admin/auth/auth-query-groups.gql'
+import groupsQuery from 'gql/admin/users/users-query-groups.gql'
 
 export default {
   props: {

+ 112 - 14
client/components/admin/admin-users-edit.vue

@@ -14,7 +14,7 @@
             v-icon mdi-arrow-left
           v-dialog(v-model='deleteUserDialog', max-width='500', v-if='user.id !== currentUserId && !user.isSystem')
             template(v-slot:activator='{ on }')
-              v-btn.ml-3.animated.fadeInDown.wait-p1s(color='red', large, outlined, v-on='on')
+              v-btn.ml-3.animated.fadeInDown.wait-p1s(color='red', large, outlined, v-on='on', disabled)
                 v-icon(color='red') mdi-trash-can-outline
             v-card
               .dialog-header.is-red Delete User?
@@ -113,15 +113,35 @@
                   v-list-item-title Password
                   v-list-item-subtitle &bull;&bull;&bull;&bull;&bull;&bull;&bull;&bull;
                 v-list-item-action
-                  v-tooltip(top)
-                    template(v-slot:activator='{ on }')
-                      v-btn(icon, color='grey', x-small, v-on='on')
-                        v-icon mdi-cached
-                    span Change Password
+                  v-menu(
+                    v-model='editPop.newPassword'
+                    :close-on-content-click='false'
+                    min-width='350'
+                    left
+                    )
+                    template(v-slot:activator='{ on: menu }')
+                      v-tooltip(top)
+                        template(v-slot:activator='{ on: tooltip }')
+                          v-btn(icon, color='grey', x-small, v-on='{ ...menu, ...tooltip }', @click='focusField(`iptNewPassword`)')
+                            v-icon mdi-cached
+                        span Change Password
+                    v-card
+                      v-text-field(
+                        ref='iptNewPassword'
+                        v-model='newPassword'
+                        label='New Password'
+                        solo
+                        hide-details
+                        append-icon='mdi-check'
+                        type='password'
+                        @click:append='editPop.newPassword = false'
+                        @keydown.enter='editPop.newPassword = false'
+                        @keydown.esc='editPop.newPassword = false'
+                      )
                 v-list-item-action
                   v-tooltip(top)
                     template(v-slot:activator='{ on }')
-                      v-btn(icon, color='grey', x-small, v-on='on')
+                      v-btn(icon, color='grey', x-small, v-on='on', disabled)
                         v-icon mdi-email
                     span Send Password Reset Email
               v-divider
@@ -151,22 +171,37 @@
             span User Groups
           v-list(dense)
             template(v-for='(group, idx) in user.groups')
-              v-list-item
+              v-list-item(:key='`group-` + group.id')
                 v-list-item-avatar(size='32')
                   v-icon mdi-account-group-outline
                 v-list-item-content
                   v-list-item-title {{group.name}}
                 v-list-item-action(v-if='!user.isSystem')
-                  v-btn(icon, color='red', x-small)
+                  v-btn(icon, color='red', x-small, @click='unassignGroup(group.id)')
                     v-icon mdi-close
               v-divider(v-if='idx < user.groups.length - 1')
           v-alert.mx-3(v-if='user.groups.length < 1', outlined, color='grey darken-1', icon='mdi-alert')
             .caption This user is not assigned to any group yet. You must assign at least 1 group to a user.
           v-card-chin(v-if='!user.isSystem')
             v-spacer
-            v-btn(color='primary', text)
-              v-icon(left) mdi-clipboard-account
-              span Assign to group
+            v-select(
+              ref='iptAssignGroup'
+              :items='groups'
+              v-model='newGroup'
+              label='Select Group...'
+              item-value='id'
+              item-text='name'
+              item-disabled='isSystem'
+              solo
+              flat
+              dense
+              hide-details
+              @keydown.esc='editPop.assignGroup = false'
+              style='max-width: 300px;'
+            )
+            v-btn.ml-2.px-4(depressed, color='primary', height='48', @click='assignGroup', :disabled='newGroup === 0')
+              v-icon(left) mdi-clipboard-account-outline
+              span Assign
       v-flex(xs6)
         v-card.animated.fadeInUp.wait-p2s
           v-toolbar(color='primary', dense, dark, flat)
@@ -274,6 +309,8 @@ import _ from 'lodash'
 import { get } from 'vuex-pathify'
 
 import userQuery from 'gql/admin/users/users-query-single.gql'
+import groupsQuery from 'gql/admin/users/users-query-groups.gql'
+import updateUserMutation from 'gql/admin/users/users-mutation-update.gql'
 
 export default {
   data() {
@@ -285,10 +322,18 @@ export default {
         pwd: false,
         location: false,
         jobTitle: false,
-        timezone: false
+        timezone: false,
+        newPassword: false,
+        assignGroup: false
       },
+      newGroup: 0,
+      newPassword: '',
       user: {
+        email: '',
         name: '',
+        location: '',
+        jobTitle: '',
+        timezone: '',
         groups: []
       },
       timezones: [
@@ -550,13 +595,58 @@ export default {
   },
   methods: {
     deleteUser() {},
-    updateUser() {},
+    async updateUser() {
+      this.$store.commit(`loadingStart`, 'admin-users-update')
+      const resp = await this.$apollo.mutate({
+        mutation: updateUserMutation,
+        variables: {
+          id: this.user.id,
+          email: this.user.email,
+          name: this.user.name,
+          newPassword: this.newPassword,
+          groups: _.map(this.user.groups, 'id'),
+          location: this.user.location,
+          jobTitle: this.user.jobTitle,
+          timezone: this.user.timezone
+        }
+      })
+      if (_.get(resp, 'data.users.update.responseResult.succeeded', false)) {
+        this.$store.commit('showNotification', {
+          style: 'success',
+          message: 'User updated successfully.',
+          icon: 'check'
+        })
+        this.$router.push('/users')
+      } else {
+        this.$store.commit('showNotification', {
+          style: 'red',
+          message: _.get(resp, 'data.users.update.responseResult.message', 'An unexpected error occured.'),
+          icon: 'warning'
+        })
+      }
+      this.$store.commit(`loadingStop`, 'admin-users-update')
+    },
     focusField (ipt) {
       this.$nextTick(() => {
         _.delay(() => {
           this.$refs[ipt].focus()
         }, 200)
       })
+    },
+    assignGroup() {
+      if (_.some(this.user.groups, ['id', this.newGroup])) {
+        this.$store.commit('showNotification', {
+          message: 'User is already assigned to this group!',
+          style: 'error',
+          icon: 'alert'
+        })
+      } else {
+        this.user.groups.push(_.find(this.groups, ['id', this.newGroup]))
+        this.newGroup = 0
+      }
+    },
+    unassignGroup(gid) {
+      this.user.groups = _.reject(this.user.groups, ['id', gid])
     }
   },
   apollo: {
@@ -572,6 +662,14 @@ export default {
       watchLoading (isLoading) {
         this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-users-refresh')
       }
+    },
+    groups: {
+      query: groupsQuery,
+      fetchPolicy: 'network-only',
+      update: (data) => data.groups.list,
+      watchLoading (isLoading) {
+        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-groups-refresh')
+      }
     }
   }
 }

+ 8 - 3
client/components/common/nav-header.vue

@@ -40,8 +40,8 @@
                   v-list-item-avatar(size='24'): v-icon(color='indigo') mdi-file-document-edit-outline
                   v-list-item-title.body-2 {{$t('common:header.edit')}}
                 v-list-item.pl-4(@click='pageHistory', v-if='mode !== `history`')
-                  v-list-item-avatar(size='24'): v-icon(color='indigo') mdi-history
-                  v-list-item-title.body-2 {{$t('common:header.history')}}
+                  v-list-item-avatar(size='24'): v-icon(color='grey lighten-2') mdi-history
+                  v-list-item-title.body-2.grey--text.text--ligten-2 {{$t('common:header.history')}}
                 v-list-item.pl-4(@click='pageSource', v-if='mode !== `source`')
                   v-list-item-avatar(size='24'): v-icon(color='indigo') mdi-code-tags
                   v-list-item-title.body-2 {{$t('common:header.viewSource')}}
@@ -309,7 +309,12 @@ export default {
       window.location.assign(`/e/${this.locale}/${this.path}`)
     },
     pageHistory () {
-      window.location.assign(`/h/${this.locale}/${this.path}`)
+      this.$store.commit('showNotification', {
+        style: 'indigo',
+        message: `Coming soon...`,
+        icon: 'ferry'
+      })
+      // window.location.assign(`/h/${this.locale}/${this.path}`)
     },
     pageSource () {
       window.location.assign(`/s/${this.locale}/${this.path}`)

+ 12 - 0
client/graph/admin/users/users-mutation-update.gql

@@ -0,0 +1,12 @@
+mutation ($id: Int!, $email: String, $name: String, $newPassword: String, $groups: [Int], $location: String, $jobTitle: String, $timezone: String) {
+  users {
+    update(id: $id, email: $email, name: $name, newPassword: $newPassword, groups: $groups, location: $location, jobTitle: $jobTitle, timezone: $timezone) {
+      responseResult {
+        succeeded
+        errorCode
+        slug
+        message
+      }
+    }
+  }
+}

+ 9 - 0
client/graph/admin/users/users-query-groups.gql

@@ -0,0 +1,9 @@
+query {
+  groups {
+    list {
+      id
+      name
+      isSystem
+    }
+  }
+}

+ 7 - 7
client/themes/default/components/page.vue

@@ -47,15 +47,15 @@
             .caption.red--text {{$t('common:page.unpublished')}}
             status-indicator.ml-3(negative, pulse)
         v-divider
-      v-toolbar.px-2(:color='darkMode ? `grey darken-4-l3` : `grey lighten-4`', flat, :height='90')
-        div(style='padding-left: 376px;')
-          .headline.grey--text(:class='darkMode ? `text--lighten-2` : `text--darken-3`') {{title}}
-          .caption.grey--text.text--darken-1 {{description}}
-        v-spacer
+      v-container.grey.pa-0(fluid, :class='darkMode ? `darken-4-l3` : `lighten-4`')
+        v-row(no-gutters, align-content='center', style='height: 90px;')
+          v-col.pl-4(offset-xl='2', offset-lg='3')
+            .headline.grey--text(:class='darkMode ? `text--lighten-2` : `text--darken-3`') {{title}}
+            .caption.grey--text.text--darken-1 {{description}}
       v-divider
-      v-container.pl-5.pt-2(fill-height, fluid, grid-list-xl)
+      v-container.pl-5.pt-4(fluid, grid-list-xl)
         v-layout(row)
-          v-flex.page-col-sd(lg3, xl2, fill-height, v-if='$vuetify.breakpoint.lgAndUp', style='margin-top: -90px;')
+          v-flex.page-col-sd(lg3, xl2, v-if='$vuetify.breakpoint.lgAndUp', style='margin-top: -90px;')
             v-card(v-if='toc.length')
               .overline.pa-5.pb-0(:class='darkMode ? `blue--text text--lighten-2` : `primary--text`') {{$t('common:page.toc')}}
               v-list.pb-3(dense, nav, :class='darkMode ? `darken-3-d3` : ``')

+ 12 - 18
client/themes/default/scss/app.scss

@@ -25,6 +25,16 @@
   h1, h2, h3, h4, h5, h6 {
     position: relative;
 
+    &:before {
+      display: block;
+      content: " ";
+      width: 1px;
+      margin-top: -75px;
+      height: 75px;
+      visibility: hidden;
+      z-index: -1;
+    }
+
     &:first-child {
       padding-top: 0;
     }
@@ -84,7 +94,6 @@
   }
   h2 {
     margin: 1rem 0 0 0;
-    padding: 8px 0 0 0;
     color: mc('grey', '800');
     position: relative;
 
@@ -114,8 +123,7 @@
     }
   }
   h3 {
-    margin: 0;
-    padding: 8px 0 0 0;
+    margin: 8px 0 0 0;
     color: mc('grey', '700');
     position: relative;
 
@@ -135,8 +143,7 @@
   }
   h4, h5, h6 {
     font-size: 1rem;
-    margin: 0;
-    padding: 8px 0 0 0;
+    margin: 8px 0 0 0;
     color: mc('grey', '700');
     position: relative;
 
@@ -165,19 +172,6 @@
     }
   }
 
-  // scroll offset fix
-
-  h1:before, h2:before, h3:before, h4:before, h5:before, h6:before {
-    display: block;
-    content: " ";
-    width: 1px;
-    height: 1px;
-    margin-top: -75px;
-    height: 75px;
-    visibility: hidden;
-    z-index: -1;
-  }
-
   // ---------------------------------
   // PARAGRAPHS
   // ---------------------------------

+ 10 - 10
server/graph/resolvers/user.js

@@ -43,19 +43,19 @@ module.exports = {
     delete(obj, args) {
       return WIKI.models.users.query().deleteById(args.id)
     },
-    update(obj, args) {
-      return WIKI.models.users.query().patch({
-        email: args.email,
-        name: args.name,
-        provider: args.provider,
-        providerId: args.providerId
-      }).where('id', args.id)
+    async update(obj, args) {
+      try {
+        await WIKI.models.users.updateUser(args)
+
+        return {
+          responseResult: graphHelper.generateSuccess('User created successfully')
+        }
+      } catch (err) {
+        return graphHelper.generateError(err)
+      }
     },
     resetPassword(obj, args) {
       return false
-    },
-    setPassword(obj, args) {
-      return false
     }
   },
   User: {

+ 6 - 8
server/graph/schemas/user.graphql

@@ -48,9 +48,12 @@ type UserMutation {
     id: Int!
     email: String
     name: String
-    providerKey: String
-    providerId: String
-  ): UserResponse @auth(requires: ["manage:users", "manage:system"])
+    newPassword: String
+    groups: [Int]
+    location: String
+    jobTitle: String
+    timezone: String
+  ): DefaultResponse @auth(requires: ["manage:users", "manage:system"])
 
   delete(
     id: Int!
@@ -59,11 +62,6 @@ type UserMutation {
   resetPassword(
     id: Int!
   ): DefaultResponse
-
-  setPassword(
-    id: Int!
-    passwordRaw: String!
-  ): DefaultResponse
 }
 
 # -----------------------------------------------

+ 4 - 0
server/helpers/error.js

@@ -140,5 +140,9 @@ module.exports = {
   UserCreationFailed: CustomError('UserCreationFailed', {
     message: 'An unexpected error occured during user creation.',
     code: 1009
+  }),
+  UserNotFound: CustomError('UserNotFound', {
+    message: 'This user does not exist.',
+    code: 1016
   })
 }

+ 68 - 0
server/models/users.js

@@ -374,6 +374,11 @@ module.exports = class User extends Model {
     throw new WIKI.Error.AuthTFAInvalid()
   }
 
+  /**
+   * Create a new user
+   *
+   * @param {Object} param0 User Fields
+   */
   static async createNewUser ({ providerKey, email, passwordRaw, name, groups, mustChangePassword, sendWelcomeEmail }) {
     // Input sanitization
     email = _.toLower(email)
@@ -487,6 +492,69 @@ module.exports = class User extends Model {
     }
   }
 
+  /**
+   * Update an existing user
+   *
+   * @param {Object} param0 User ID and fields to update
+   */
+  static async updateUser ({ id, email, name, newPassword, groups, location, jobTitle, timezone }) {
+    const usr = await WIKI.models.users.query().findById(id)
+    if (usr) {
+      let usrData = {}
+      if (!_.isEmpty(email) && email !== usr.email) {
+        const dupUsr = await WIKI.models.users.query().select('id').where({
+          email,
+          providerKey: usr.providerKey
+        })
+        if (dupUsr) {
+          throw new WIKI.Error.AuthAccountAlreadyExists()
+        }
+        usrData.email = email
+      }
+      if (!_.isEmpty(name) && name !== usr.name) {
+        usrData.name = _.trim(name)
+      }
+      if (!_.isEmpty(newPassword)) {
+        if (newPassword.length < 6) {
+          throw new WIKI.Error.InputInvalid('Password must be at least 6 characters!')
+        }
+        usrData.password = newPassword
+      }
+      if (!_.isEmpty(groups)) {
+        const usrGroupsRaw = await usr.$relatedQuery('groups')
+        const usrGroups = _.map(usrGroupsRaw, 'id')
+        // Relate added groups
+        const addUsrGroups = _.difference(groups, usrGroups)
+        for (const grp of addUsrGroups) {
+          await usr.$relatedQuery('groups').relate(grp)
+        }
+        // Unrelate removed groups
+        const remUsrGroups = _.difference(usrGroups, groups)
+        for (const grp of remUsrGroups) {
+          await usr.$relatedQuery('groups').unrelate().where('groupId', grp)
+        }
+      }
+      if (!_.isEmpty(location) && location !== usr.location) {
+        usrData.location = _.trim(location)
+      }
+      if (!_.isEmpty(jobTitle) && jobTitle !== usr.jobTitle) {
+        usrData.jobTitle = _.trim(jobTitle)
+      }
+      if (!_.isEmpty(timezone) && timezone !== usr.timezone) {
+        usrData.timezone = timezone
+      }
+      await WIKI.models.users.query().patch(usrData).findById(id)
+    } else {
+      throw new WIKI.Error.UserNotFound()
+    }
+  }
+
+  /**
+   * Register a new user (client-side registration)
+   *
+   * @param {Object} param0 User fields
+   * @param {Object} context GraphQL Context
+   */
   static async register ({ email, password, name, verify = false, bypassChecks = false }, context) {
     const localStrg = await WIKI.models.authentication.getStrategy('local')
     // Check if self-registration is enabled