feat: update user

This commit is contained in:
Nick 2019-08-17 18:29:58 -04:00
parent 379d58d069
commit 823ff1bc61
14 changed files with 255 additions and 65 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -14,7 +14,7 @@
v-icon mdi-arrow-left v-icon mdi-arrow-left
v-dialog(v-model='deleteUserDialog', max-width='500', v-if='user.id !== currentUserId && !user.isSystem') v-dialog(v-model='deleteUserDialog', max-width='500', v-if='user.id !== currentUserId && !user.isSystem')
template(v-slot:activator='{ on }') 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-icon(color='red') mdi-trash-can-outline
v-card v-card
.dialog-header.is-red Delete User? .dialog-header.is-red Delete User?
@ -113,15 +113,35 @@
v-list-item-title Password v-list-item-title Password
v-list-item-subtitle &bull;&bull;&bull;&bull;&bull;&bull;&bull;&bull; v-list-item-subtitle &bull;&bull;&bull;&bull;&bull;&bull;&bull;&bull;
v-list-item-action v-list-item-action
v-tooltip(top) v-menu(
template(v-slot:activator='{ on }') v-model='editPop.newPassword'
v-btn(icon, color='grey', x-small, v-on='on') :close-on-content-click='false'
v-icon mdi-cached min-width='350'
span Change Password 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-list-item-action
v-tooltip(top) v-tooltip(top)
template(v-slot:activator='{ on }') 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 v-icon mdi-email
span Send Password Reset Email span Send Password Reset Email
v-divider v-divider
@ -151,22 +171,37 @@
span User Groups span User Groups
v-list(dense) v-list(dense)
template(v-for='(group, idx) in user.groups') 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-list-item-avatar(size='32')
v-icon mdi-account-group-outline v-icon mdi-account-group-outline
v-list-item-content v-list-item-content
v-list-item-title {{group.name}} v-list-item-title {{group.name}}
v-list-item-action(v-if='!user.isSystem') 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-icon mdi-close
v-divider(v-if='idx < user.groups.length - 1') 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') 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. .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-card-chin(v-if='!user.isSystem')
v-spacer v-spacer
v-btn(color='primary', text) v-select(
v-icon(left) mdi-clipboard-account ref='iptAssignGroup'
span Assign to group :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-flex(xs6)
v-card.animated.fadeInUp.wait-p2s v-card.animated.fadeInUp.wait-p2s
v-toolbar(color='primary', dense, dark, flat) v-toolbar(color='primary', dense, dark, flat)
@ -274,6 +309,8 @@ import _ from 'lodash'
import { get } from 'vuex-pathify' import { get } from 'vuex-pathify'
import userQuery from 'gql/admin/users/users-query-single.gql' 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 { export default {
data() { data() {
@ -285,10 +322,18 @@ export default {
pwd: false, pwd: false,
location: false, location: false,
jobTitle: false, jobTitle: false,
timezone: false timezone: false,
newPassword: false,
assignGroup: false
}, },
newGroup: 0,
newPassword: '',
user: { user: {
email: '',
name: '', name: '',
location: '',
jobTitle: '',
timezone: '',
groups: [] groups: []
}, },
timezones: [ timezones: [
@ -550,13 +595,58 @@ export default {
}, },
methods: { methods: {
deleteUser() {}, 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) { focusField (ipt) {
this.$nextTick(() => { this.$nextTick(() => {
_.delay(() => { _.delay(() => {
this.$refs[ipt].focus() this.$refs[ipt].focus()
}, 200) }, 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: { apollo: {
@ -572,6 +662,14 @@ export default {
watchLoading (isLoading) { watchLoading (isLoading) {
this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-users-refresh') 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')
}
} }
} }
} }

View File

@ -40,8 +40,8 @@
v-list-item-avatar(size='24'): v-icon(color='indigo') mdi-file-document-edit-outline 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-title.body-2 {{$t('common:header.edit')}}
v-list-item.pl-4(@click='pageHistory', v-if='mode !== `history`') 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-avatar(size='24'): v-icon(color='grey lighten-2') mdi-history
v-list-item-title.body-2 {{$t('common:header.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.pl-4(@click='pageSource', v-if='mode !== `source`')
v-list-item-avatar(size='24'): v-icon(color='indigo') mdi-code-tags v-list-item-avatar(size='24'): v-icon(color='indigo') mdi-code-tags
v-list-item-title.body-2 {{$t('common:header.viewSource')}} v-list-item-title.body-2 {{$t('common:header.viewSource')}}
@ -309,7 +309,12 @@ export default {
window.location.assign(`/e/${this.locale}/${this.path}`) window.location.assign(`/e/${this.locale}/${this.path}`)
}, },
pageHistory () { 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 () { pageSource () {
window.location.assign(`/s/${this.locale}/${this.path}`) window.location.assign(`/s/${this.locale}/${this.path}`)

View File

@ -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
}
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -374,6 +374,11 @@ module.exports = class User extends Model {
throw new WIKI.Error.AuthTFAInvalid() throw new WIKI.Error.AuthTFAInvalid()
} }
/**
* Create a new user
*
* @param {Object} param0 User Fields
*/
static async createNewUser ({ providerKey, email, passwordRaw, name, groups, mustChangePassword, sendWelcomeEmail }) { static async createNewUser ({ providerKey, email, passwordRaw, name, groups, mustChangePassword, sendWelcomeEmail }) {
// Input sanitization // Input sanitization
email = _.toLower(email) 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) { static async register ({ email, password, name, verify = false, bypassChecks = false }, context) {
const localStrg = await WIKI.models.authentication.getStrategy('local') const localStrg = await WIKI.models.authentication.getStrategy('local')
// Check if self-registration is enabled // Check if self-registration is enabled