feat: jwt renew via graphql + users create/authorize UI
This commit is contained in:
@@ -7,10 +7,11 @@ import VueSimpleBreakpoints from 'vue-simple-breakpoints'
|
||||
import VeeValidate from 'vee-validate'
|
||||
import { ApolloClient } from 'apollo-client'
|
||||
import { createPersistedQueryLink } from 'apollo-link-persisted-queries'
|
||||
// import { BatchHttpLink } from 'apollo-link-batch-http'
|
||||
import { split } from 'apollo-link'
|
||||
import { createHttpLink } from 'apollo-link-http'
|
||||
import { BatchHttpLink } from 'apollo-link-batch-http'
|
||||
import { ApolloLink, split } from 'apollo-link'
|
||||
// import { createHttpLink } from 'apollo-link-http'
|
||||
import { WebSocketLink } from 'apollo-link-ws'
|
||||
import { ErrorLink } from 'apollo-link-error'
|
||||
import { InMemoryCache } from 'apollo-cache-inmemory'
|
||||
import { getMainDefinition } from 'apollo-utilities'
|
||||
import VueApollo from 'vue-apollo'
|
||||
@@ -54,24 +55,47 @@ moment.locale(siteConfig.lang)
|
||||
const graphQLEndpoint = window.location.protocol + '//' + window.location.host + '/graphql'
|
||||
const graphQLWSEndpoint = ((window.location.protocol === 'https:') ? 'wss:' : 'ws:') + '//' + window.location.host + '/graphql-subscriptions'
|
||||
|
||||
const graphQLLink = createPersistedQueryLink().concat(
|
||||
createHttpLink({
|
||||
const graphQLLink = ApolloLink.from([
|
||||
new ErrorLink(({ graphQLErrors, networkError }) => {
|
||||
if (graphQLErrors) {
|
||||
graphQLErrors.map(({ message, locations, path }) =>
|
||||
console.error(
|
||||
`[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`
|
||||
)
|
||||
)
|
||||
store.commit('showNotification', {
|
||||
style: 'red',
|
||||
message: `An expected error occured.`,
|
||||
icon: 'warning'
|
||||
})
|
||||
}
|
||||
if (networkError) {
|
||||
console.error(networkError)
|
||||
store.commit('showNotification', {
|
||||
style: 'red',
|
||||
message: `Network Error: ${networkError.message}`,
|
||||
icon: 'error'
|
||||
})
|
||||
}
|
||||
}),
|
||||
createPersistedQueryLink(),
|
||||
new BatchHttpLink({
|
||||
includeExtensions: true,
|
||||
uri: graphQLEndpoint,
|
||||
credentials: 'include',
|
||||
fetch: (uri, options) => {
|
||||
fetch: async (uri, options) => {
|
||||
// Strip __typename fields from variables
|
||||
let body = JSON.parse(options.body)
|
||||
// body = body.map(bd => {
|
||||
// return ({
|
||||
// ...bd,
|
||||
// variables: JSON.parse(JSON.stringify(bd.variables), (key, value) => { return key === '__typename' ? undefined : value })
|
||||
// })
|
||||
// })
|
||||
body = {
|
||||
...body,
|
||||
variables: JSON.parse(JSON.stringify(body.variables), (key, value) => { return key === '__typename' ? undefined : value })
|
||||
}
|
||||
body = body.map(bd => {
|
||||
return ({
|
||||
...bd,
|
||||
variables: JSON.parse(JSON.stringify(bd.variables), (key, value) => { return key === '__typename' ? undefined : value })
|
||||
})
|
||||
})
|
||||
// body = {
|
||||
// ...body,
|
||||
// variables: JSON.parse(JSON.stringify(body.variables), (key, value) => { return key === '__typename' ? undefined : value })
|
||||
// }
|
||||
options.body = JSON.stringify(body)
|
||||
|
||||
// Inject authentication token
|
||||
@@ -80,10 +104,17 @@ const graphQLLink = createPersistedQueryLink().concat(
|
||||
options.headers.Authorization = `Bearer ${jwtToken}`
|
||||
}
|
||||
|
||||
return fetch(uri, options)
|
||||
const resp = await fetch(uri, options)
|
||||
|
||||
// Handle renewed JWT
|
||||
const newJWT = resp.headers.get('new-jwt')
|
||||
if (newJWT) {
|
||||
Cookies.set('jwt', newJWT, { expires: 365 })
|
||||
}
|
||||
return resp
|
||||
}
|
||||
})
|
||||
)
|
||||
])
|
||||
|
||||
const graphQLWSLink = new WebSocketLink({
|
||||
uri: graphQLWSEndpoint,
|
||||
|
||||
@@ -7,109 +7,121 @@
|
||||
.admin-header-title
|
||||
.headline.blue--text.text--darken-2 Edit Group
|
||||
.subheading.grey--text {{name}}
|
||||
v-btn(color='primary', fab, absolute, bottom, right, small, to='/groups'): v-icon arrow_upward
|
||||
v-tabs(v-model='tab', :color='$vuetify.dark ? "primary" : "grey lighten-4"', fixed-tabs, :slider-color='$vuetify.dark ? "white" : "primary"', show-arrows)
|
||||
v-tab(key='properties') Properties
|
||||
v-tab(key='rights') Permissions
|
||||
v-tab(key='users') Users
|
||||
v-spacer
|
||||
v-btn(color='indigo', large, outline, to='/groups')
|
||||
v-icon arrow_back
|
||||
v-dialog(v-model='deleteGroupDialog', max-width='500')
|
||||
v-btn(color='red', large, outline, slot='activator')
|
||||
v-icon(color='red') delete
|
||||
v-card
|
||||
.dialog-header.is-red Delete Group?
|
||||
v-card-text Are you sure you want to delete group #[strong {{ name }}]? All users will be unassigned from this group.
|
||||
v-card-actions
|
||||
v-spacer
|
||||
v-btn(flat, @click='deleteGroupDialog = false') Cancel
|
||||
v-btn(color='red', dark, @click='deleteGroup') Delete
|
||||
v-btn(color='primary', large, depressed, @click='updateGroup')
|
||||
v-icon(left) check
|
||||
span Update Group
|
||||
v-card.mt-3
|
||||
v-tabs(v-model='tab', :color='$vuetify.dark ? "primary" : "grey lighten-4"', fixed-tabs, :slider-color='$vuetify.dark ? "white" : "primary"', show-arrows)
|
||||
v-tab(key='properties') Properties
|
||||
v-tab(key='permissions') Permissions
|
||||
v-tab(key='rules') Page Rules
|
||||
v-tab(key='users') Users
|
||||
|
||||
v-tab-item(key='properties', :transition='false', :reverse-transition='false')
|
||||
v-card
|
||||
v-card-text
|
||||
v-text-field(v-model='name', label='Group Name', counter='255', prepend-icon='people')
|
||||
v-card-actions.pa-3
|
||||
v-btn(color='primary', @click='updateGroup')
|
||||
v-icon(left) check
|
||||
| Save Changes
|
||||
.caption.ml-4.grey--text ID: {{group.id}}
|
||||
v-spacer
|
||||
v-dialog(v-model='deleteGroupDialog', max-width='500')
|
||||
v-btn(color='red', flat, @click='', slot='activator')
|
||||
v-icon(left) delete
|
||||
| Delete Group
|
||||
v-tab-item(key='properties', :transition='false', :reverse-transition='false')
|
||||
v-card
|
||||
.dialog-header.is-red Delete Group?
|
||||
v-card-text Are you sure you want to delete group #[strong {{ name }}]? All users will be unassigned from this group.
|
||||
v-card-actions
|
||||
v-card-text
|
||||
v-text-field(
|
||||
outline
|
||||
background-color='grey lighten-3'
|
||||
v-model='name'
|
||||
label='Group Name'
|
||||
counter='255'
|
||||
prepend-icon='people'
|
||||
)
|
||||
v-divider
|
||||
.caption.mt-3.grey--text ID: {{group.id}}
|
||||
|
||||
v-tab-item(key='permissions', :transition='false', :reverse-transition='false')
|
||||
v-card
|
||||
|
||||
v-tab-item(key='rules', :transition='false', :reverse-transition='false')
|
||||
v-card
|
||||
v-card-title.pb-0
|
||||
v-subheader
|
||||
v-icon.mr-2 border_color
|
||||
.subheading Read and Write
|
||||
v-spacer
|
||||
v-btn(flat, @click='deleteGroupDialog = false') Cancel
|
||||
v-btn(color='red', dark, @click='deleteGroup') Delete
|
||||
v-btn(flat, outline)
|
||||
v-icon(left) arrow_drop_down
|
||||
| Load Preset
|
||||
v-btn(flat, outline)
|
||||
v-icon(left) vertical_align_bottom
|
||||
| Import Rules
|
||||
.pa-3.pl-4
|
||||
criterias
|
||||
v-divider.my-0
|
||||
v-card-title.pb-0
|
||||
v-subheader
|
||||
v-icon.mr-2 pageview
|
||||
.subheading Read Only
|
||||
v-spacer
|
||||
v-btn(flat, outline)
|
||||
v-icon(left) arrow_drop_down
|
||||
| Load Preset
|
||||
v-btn(flat, outline)
|
||||
v-icon(left) vertical_align_bottom
|
||||
| Import Rules
|
||||
.pa-3.pl-4
|
||||
criterias
|
||||
v-divider.my-0
|
||||
v-card-title.pb-0
|
||||
v-subheader Legend
|
||||
.px-4.pb-4
|
||||
.body-1.px-1.py-2 Any number of rules can be used at the same time. However, some rules requires more processing time than others. Rule types are color-coded as followed:
|
||||
.caption
|
||||
v-icon(color='blue') stop
|
||||
span Fast rules. None or insignificant latency introduced to all page loads.
|
||||
.caption
|
||||
v-icon(color='orange') stop
|
||||
span Medium rules. Some latency added to all page loads.
|
||||
.caption
|
||||
v-icon(color='red') stop
|
||||
span Slow rules. May adds noticeable latency to all page loads. Avoid using in multiple rules.
|
||||
|
||||
v-tab-item(key='rights', :transition='false', :reverse-transition='false')
|
||||
v-card
|
||||
v-card-title.pb-0
|
||||
v-subheader
|
||||
v-icon.mr-2 border_color
|
||||
.subheading Read and Write
|
||||
v-spacer
|
||||
v-btn(flat, outline)
|
||||
v-icon(left) arrow_drop_down
|
||||
| Load Preset
|
||||
v-btn(flat, outline)
|
||||
v-icon(left) vertical_align_bottom
|
||||
| Import Rules
|
||||
.pa-3.pl-4
|
||||
criterias
|
||||
v-divider.my-0
|
||||
v-card-title.pb-0
|
||||
v-subheader
|
||||
v-icon.mr-2 pageview
|
||||
.subheading Read Only
|
||||
v-spacer
|
||||
v-btn(flat, outline)
|
||||
v-icon(left) arrow_drop_down
|
||||
| Load Preset
|
||||
v-btn(flat, outline)
|
||||
v-icon(left) vertical_align_bottom
|
||||
| Import Rules
|
||||
.pa-3.pl-4
|
||||
criterias
|
||||
v-divider.my-0
|
||||
v-card-title.pb-0
|
||||
v-subheader Legend
|
||||
.px-4.pb-4
|
||||
.body-1.px-1.py-2 Any number of rules can be used at the same time. However, some rules requires more processing time than others. Rule types are color-coded as followed:
|
||||
.caption
|
||||
v-icon(color='blue') stop
|
||||
span Fast rules. None or insignificant latency introduced to all page loads.
|
||||
.caption
|
||||
v-icon(color='orange') stop
|
||||
span Medium rules. Some latency added to all page loads.
|
||||
.caption
|
||||
v-icon(color='red') stop
|
||||
span Slow rules. May adds noticeable latency to all page loads. Avoid using in multiple rules.
|
||||
|
||||
v-tab-item(key='users', :transition='false', :reverse-transition='false')
|
||||
v-card
|
||||
v-card-title.pb-0
|
||||
v-btn(color='primary', @click='searchUserDialog = true')
|
||||
v-icon(left) assignment_ind
|
||||
| Assign User
|
||||
v-data-table(
|
||||
:items='group.users',
|
||||
:headers='headers',
|
||||
:search='search',
|
||||
:pagination.sync='pagination',
|
||||
:rows-per-page-items='[15]'
|
||||
hide-actions
|
||||
)
|
||||
template(slot='items', slot-scope='props')
|
||||
tr(:active='props.selected')
|
||||
td.text-xs-right {{ props.item.id }}
|
||||
td {{ props.item.name }}
|
||||
td {{ props.item.email }}
|
||||
td
|
||||
v-menu(bottom, right, min-width='200')
|
||||
v-btn(icon, slot='activator'): v-icon.grey--text.text--darken-1 more_horiz
|
||||
v-list
|
||||
v-list-tile(@click='unassignUser(props.item.id)')
|
||||
v-list-tile-action: v-icon(color='orange') highlight_off
|
||||
v-list-tile-content
|
||||
v-list-tile-title Unassign
|
||||
template(slot='no-data')
|
||||
v-alert.ma-3(icon='warning', :value='true', outline) No users to display.
|
||||
.text-xs-center.py-2(v-if='users.length > 15')
|
||||
v-pagination(v-model='pagination.page', :length='pages')
|
||||
v-tab-item(key='users', :transition='false', :reverse-transition='false')
|
||||
v-card
|
||||
v-card-title.pb-0
|
||||
v-btn(color='primary', @click='searchUserDialog = true')
|
||||
v-icon(left) assignment_ind
|
||||
| Assign User
|
||||
v-data-table(
|
||||
:items='group.users',
|
||||
:headers='headers',
|
||||
:search='search',
|
||||
:pagination.sync='pagination',
|
||||
:rows-per-page-items='[15]'
|
||||
hide-actions
|
||||
)
|
||||
template(slot='items', slot-scope='props')
|
||||
tr(:active='props.selected')
|
||||
td.text-xs-right {{ props.item.id }}
|
||||
td {{ props.item.name }}
|
||||
td {{ props.item.email }}
|
||||
td
|
||||
v-menu(bottom, right, min-width='200')
|
||||
v-btn(icon, slot='activator'): v-icon.grey--text.text--darken-1 more_horiz
|
||||
v-list
|
||||
v-list-tile(@click='unassignUser(props.item.id)')
|
||||
v-list-tile-action: v-icon(color='orange') highlight_off
|
||||
v-list-tile-content
|
||||
v-list-tile-title Unassign
|
||||
template(slot='no-data')
|
||||
v-alert.ma-3(icon='warning', :value='true', outline) No users to display.
|
||||
.text-xs-center.py-2(v-if='users.length > 15')
|
||||
v-pagination(v-model='pagination.page', :length='pages')
|
||||
|
||||
user-search(v-model='searchUserDialog', @select='assignUser')
|
||||
</template>
|
||||
|
||||
115
client/components/admin/admin-users-authorize.vue
Normal file
115
client/components/admin/admin-users-authorize.vue
Normal file
@@ -0,0 +1,115 @@
|
||||
<template lang="pug">
|
||||
v-dialog(v-model='isShown', max-width='550')
|
||||
v-card
|
||||
.dialog-header.is-short Authorize Social User
|
||||
v-card-text
|
||||
v-select.md2(
|
||||
:items='providers'
|
||||
item-text='title'
|
||||
item-value='key'
|
||||
solo
|
||||
flat
|
||||
background-color='grey lighten-4'
|
||||
prepend-icon='business'
|
||||
v-model='provider'
|
||||
label='Provider'
|
||||
)
|
||||
v-text-field.md2(
|
||||
solo
|
||||
flat
|
||||
background-color='grey lighten-4'
|
||||
prepend-icon='email'
|
||||
v-model='email'
|
||||
label='Email Address'
|
||||
ref='emailInput'
|
||||
)
|
||||
v-text-field.md2(
|
||||
solo
|
||||
flat
|
||||
background-color='grey lighten-4'
|
||||
prepend-icon='person'
|
||||
v-model='name'
|
||||
label='Name'
|
||||
)
|
||||
v-text-field.md2(
|
||||
solo
|
||||
flat
|
||||
background-color='grey lighten-4'
|
||||
prepend-icon='title'
|
||||
v-model='jobTitle'
|
||||
label='Job Title'
|
||||
counter='255'
|
||||
hint='Optional'
|
||||
persistent-hint
|
||||
)
|
||||
v-text-field.md2(
|
||||
solo
|
||||
flat
|
||||
background-color='grey lighten-4'
|
||||
prepend-icon='public'
|
||||
v-model='location'
|
||||
label='Location'
|
||||
counter='255'
|
||||
hint='Optional'
|
||||
persistent-hint
|
||||
)
|
||||
v-card-chin
|
||||
v-spacer
|
||||
v-btn(flat, @click='isShown = false') Cancel
|
||||
v-btn(color='primary', @click='authorizeUser') Authorize
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
|
||||
import providersQuery from 'gql/admin/users/users-query-strategies.gql'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
value: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
providers: [],
|
||||
provider: '',
|
||||
email: '',
|
||||
name: '',
|
||||
jobTitle: '',
|
||||
location: ''
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isShown: {
|
||||
get() { return this.value },
|
||||
set(val) { this.$emit('input', val) }
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
value(newValue, oldValue) {
|
||||
if (newValue) {
|
||||
this.$nextTick(() => {
|
||||
this.$refs.emailInput.focus()
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async authorizeUser() {
|
||||
|
||||
}
|
||||
},
|
||||
apollo: {
|
||||
providers: {
|
||||
query: providersQuery,
|
||||
fetchPolicy: 'network-only',
|
||||
update: (data) => _.reject(data.authentication.strategies, ['key', 'local']),
|
||||
watchLoading (isLoading) {
|
||||
this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-users-strategies-refresh')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
105
client/components/admin/admin-users-create.vue
Normal file
105
client/components/admin/admin-users-create.vue
Normal file
@@ -0,0 +1,105 @@
|
||||
<template lang="pug">
|
||||
v-dialog(v-model='isShown', max-width='550')
|
||||
v-card
|
||||
.dialog-header.is-short New Local User
|
||||
v-card-text
|
||||
v-text-field.md2(
|
||||
solo
|
||||
flat
|
||||
background-color='grey lighten-4'
|
||||
prepend-icon='email'
|
||||
v-model='email'
|
||||
label='Email Address'
|
||||
ref='emailInput'
|
||||
)
|
||||
v-text-field.md2(
|
||||
solo
|
||||
flat
|
||||
background-color='grey lighten-4'
|
||||
prepend-icon='person'
|
||||
v-model='name'
|
||||
label='Name'
|
||||
)
|
||||
v-text-field.md2(
|
||||
solo
|
||||
flat
|
||||
background-color='grey lighten-4'
|
||||
prepend-icon='lock'
|
||||
append-icon='casino'
|
||||
v-model='password'
|
||||
label='Password'
|
||||
counter='255'
|
||||
@click:append='generatePwd'
|
||||
)
|
||||
v-text-field.md2(
|
||||
solo
|
||||
flat
|
||||
background-color='grey lighten-4'
|
||||
prepend-icon='title'
|
||||
v-model='jobTitle'
|
||||
label='Job Title'
|
||||
counter='255'
|
||||
hint='Optional'
|
||||
persistent-hint
|
||||
)
|
||||
v-text-field.md2(
|
||||
solo
|
||||
flat
|
||||
background-color='grey lighten-4'
|
||||
prepend-icon='public'
|
||||
v-model='location'
|
||||
label='Location'
|
||||
counter='255'
|
||||
hint='Optional'
|
||||
persistent-hint
|
||||
)
|
||||
v-card-chin
|
||||
v-spacer
|
||||
v-btn(flat, @click='isShown = false') Cancel
|
||||
v-btn(color='primary', @click='createUser') Create
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import uuidv4 from 'uuid/v4'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
value: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
email: '',
|
||||
name: '',
|
||||
password: '',
|
||||
jobTitle: '',
|
||||
location: ''
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isShown: {
|
||||
get() { return this.value },
|
||||
set(val) { this.$emit('input', val) }
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
value(newValue, oldValue) {
|
||||
if (newValue) {
|
||||
this.$nextTick(() => {
|
||||
this.$refs.emailInput.focus()
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async createUser() {
|
||||
|
||||
},
|
||||
generatePwd() {
|
||||
this.password = uuidv4().slice(-12)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -8,99 +8,97 @@
|
||||
.headline.blue--text.text--darken-2 Users
|
||||
.subheading.grey--text Manage users
|
||||
v-spacer
|
||||
v-btn(outline, color='grey', large)
|
||||
v-btn(outline, color='grey', large, @click='refresh')
|
||||
v-icon refresh
|
||||
v-btn(color='primary', large, depressed)
|
||||
v-btn(color='primary', large, depressed, @click='authorizeUser')
|
||||
v-icon(left) lock_outline
|
||||
span Authorize User
|
||||
v-btn(color='primary', large, depressed)
|
||||
span Authorize Social User
|
||||
v-btn(color='primary', large, depressed, @click='createUser')
|
||||
v-icon(left) add
|
||||
span New User
|
||||
span New Local User
|
||||
v-card.mt-3
|
||||
v-data-table(
|
||||
v-model='selected'
|
||||
:items='items',
|
||||
:items='users',
|
||||
:headers='headers',
|
||||
:search='search',
|
||||
:pagination.sync='pagination',
|
||||
:rows-per-page-items='[15]'
|
||||
select-all,
|
||||
hide-actions,
|
||||
disable-initial-sort
|
||||
)
|
||||
template(slot='headers', slot-scope='props')
|
||||
tr
|
||||
th(width='50')
|
||||
th.text-xs-right(
|
||||
width='80'
|
||||
:class='[`column sortable`, pagination.descending ? `desc` : `asc`, pagination.sortBy === `id` ? `active` : ``]'
|
||||
@click='changeSort(`id`)'
|
||||
)
|
||||
v-icon(small) arrow_upward
|
||||
| ID
|
||||
//- th(width='50')
|
||||
th.text-xs-left(
|
||||
v-for='header in props.headers'
|
||||
:key='header.text'
|
||||
:width='header.width'
|
||||
:class='[`column sortable`, pagination.descending ? `desc` : `asc`, header.value === pagination.sortBy ? `active` : ``]'
|
||||
:class='[`column`, header.sortable ? `sortable` : ``, pagination.descending ? `desc` : `asc`, header.value === pagination.sortBy ? `active` : ``]'
|
||||
@click='changeSort(header.value)'
|
||||
)
|
||||
| {{ header.text }}
|
||||
v-icon(small) arrow_upward
|
||||
v-icon(small, v-if='header.sortable') arrow_upward
|
||||
template(slot='items', slot-scope='props')
|
||||
tr(:active='props.selected')
|
||||
td
|
||||
//- td
|
||||
v-checkbox(hide-details, :input-value='props.selected', color='blue darken-2', @click='props.selected = !props.selected')
|
||||
td.text-xs-right {{ props.item.id }}
|
||||
td: strong {{ props.item.name }}
|
||||
td {{ props.item.email }}
|
||||
td {{ props.item.name }}
|
||||
td {{ props.item.provider }}
|
||||
td {{ props.item.createdOn }}
|
||||
td {{ props.item.updatedOn }}
|
||||
td: v-btn(icon): v-icon.grey--text.text--darken-1 more_horiz
|
||||
td {{ props.item.providerKey }}
|
||||
td {{ props.item.createdAt | moment('from') }}
|
||||
td
|
||||
v-menu(bottom, right, min-width='200')
|
||||
v-btn(icon, slot='activator'): v-icon.grey--text.text--darken-1 more_horiz
|
||||
v-list
|
||||
v-list-tile(@click='')
|
||||
v-list-tile-action
|
||||
v-icon(color='primary') edit
|
||||
v-list-tile-content
|
||||
v-list-tile-title Edit
|
||||
v-list-tile(@click='')
|
||||
v-list-tile-action
|
||||
v-icon(color='red') block
|
||||
v-list-tile-content
|
||||
v-list-tile-title Block
|
||||
template(slot='no-data')
|
||||
v-alert(icon='warning', :value='true') No users to display!
|
||||
.pa-3
|
||||
v-alert(icon='warning', :value='true', outline) No users to display!
|
||||
.text-xs-center.py-2
|
||||
v-pagination(v-model='pagination.page', :length='pages')
|
||||
|
||||
user-authorize(v-model='isAuthorizeDialogShown')
|
||||
user-create(v-model='isCreateDialogShown')
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import usersQuery from 'gql/admin/users/users-query-list.gql'
|
||||
|
||||
import UserAuthorize from './admin-users-authorize.vue'
|
||||
import UserCreate from './admin-users-create.vue'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
UserAuthorize,
|
||||
UserCreate
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selected: [],
|
||||
pagination: {},
|
||||
items: [
|
||||
{ id: 1, email: 'user@test.com', name: 'John Doe', provider: 'local' },
|
||||
{ id: 2, email: 'dude@test.com', name: 'John Doe', provider: 'local' },
|
||||
{ id: 3, email: 'dude@test.com', name: 'John Doe', provider: 'local' },
|
||||
{ id: 4, email: 'dude@test.com', name: 'John Doe', provider: 'local' },
|
||||
{ id: 5, email: 'dude@test.com', name: 'John Doe', provider: 'local' },
|
||||
{ id: 6, email: 'dude@test.com', name: 'John Doe', provider: 'local' },
|
||||
{ id: 7, email: 'dude@test.com', name: 'John Doe', provider: 'local' },
|
||||
{ id: 8, email: 'dude@test.com', name: 'John Doe', provider: 'local' },
|
||||
{ id: 9, email: 'dude@test.com', name: 'John Doe', provider: 'local' },
|
||||
{ id: 10, email: 'dude@test.com', name: 'John Doe', provider: 'local' },
|
||||
{ id: 11, email: 'dude@test.com', name: 'John Doe', provider: 'local' },
|
||||
{ id: 12, email: 'dude@test.com', name: 'John Doe', provider: 'local' },
|
||||
{ id: 13, email: 'dude@test.com', name: 'John Doe', provider: 'local' },
|
||||
{ id: 14, email: 'dude@test.com', name: 'John Doe', provider: 'local' },
|
||||
{ id: 15, email: 'dude@test.com', name: 'John Doe', provider: 'local' },
|
||||
{ id: 16, email: 'dude@test.com', name: 'John Doe', provider: 'local' },
|
||||
{ id: 17, email: 'dude@test.com', name: 'John Doe', provider: 'local' },
|
||||
{ id: 18, email: 'dude@test.com', name: 'John Doe', provider: 'local' },
|
||||
{ id: 19, email: 'dude@test.com', name: 'John Doe', provider: 'local' },
|
||||
{ id: 20, email: 'dude@test.com', name: 'John Doe', provider: 'local' }
|
||||
],
|
||||
users: [],
|
||||
headers: [
|
||||
{ text: 'Email', value: 'email' },
|
||||
{ text: 'Name', value: 'name' },
|
||||
{ text: 'Provider', value: 'provider' },
|
||||
{ text: 'Created On', value: 'createdOn' },
|
||||
{ text: 'Updated On', value: 'updatedOn' },
|
||||
{ text: 'ID', value: 'id', width: 80, sortable: true },
|
||||
{ text: 'Name', value: 'name', sortable: true },
|
||||
{ text: 'Email', value: 'email', sortable: true },
|
||||
{ text: 'Provider', value: 'provider', sortable: true },
|
||||
{ text: 'Created', value: 'createdAt', sortable: true },
|
||||
{ text: '', value: 'actions', sortable: false, width: 50 }
|
||||
],
|
||||
search: ''
|
||||
search: '',
|
||||
isAuthorizeDialogShown: false,
|
||||
isCreateDialogShown: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -113,6 +111,20 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
authorizeUser() {
|
||||
this.isAuthorizeDialogShown = true
|
||||
},
|
||||
createUser() {
|
||||
this.isCreateDialogShown = true
|
||||
},
|
||||
async refresh() {
|
||||
await this.$apollo.queries.users.refetch()
|
||||
this.$store.commit('showNotification', {
|
||||
message: 'Users list has been refreshed.',
|
||||
style: 'success',
|
||||
icon: 'cached'
|
||||
})
|
||||
},
|
||||
changeSort (column) {
|
||||
if (this.pagination.sortBy === column) {
|
||||
this.pagination.descending = !this.pagination.descending
|
||||
@@ -128,6 +140,16 @@ export default {
|
||||
this.selected = this.items.slice()
|
||||
}
|
||||
}
|
||||
},
|
||||
apollo: {
|
||||
users: {
|
||||
query: usersQuery,
|
||||
fetchPolicy: 'network-only',
|
||||
update: (data) => data.users.list,
|
||||
watchLoading (isLoading) {
|
||||
this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-users-refresh')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -5,9 +5,7 @@ query {
|
||||
name
|
||||
email
|
||||
providerKey
|
||||
role
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
12
client/graph/admin/users/users-query-strategies.gql
Normal file
12
client/graph/admin/users/users-query-strategies.gql
Normal file
@@ -0,0 +1,12 @@
|
||||
query {
|
||||
authentication {
|
||||
strategies(
|
||||
isEnabled: true
|
||||
) {
|
||||
key
|
||||
title
|
||||
icon
|
||||
color
|
||||
}
|
||||
}
|
||||
}
|
||||
58
package.json
58
package.json
@@ -63,26 +63,26 @@
|
||||
"diff2html": "2.4.0",
|
||||
"dotize": "^0.2.0",
|
||||
"execa": "1.0.0",
|
||||
"express": "4.16.3",
|
||||
"express": "4.16.4",
|
||||
"express-brute": "1.0.1",
|
||||
"express-brute-redis": "0.0.1",
|
||||
"express-session": "1.15.6",
|
||||
"file-type": "9.0.0",
|
||||
"file-type": "10.0.0",
|
||||
"filesize.js": "1.0.2",
|
||||
"follow-redirects": "1.5.8",
|
||||
"follow-redirects": "1.5.9",
|
||||
"fs-extra": "7.0.0",
|
||||
"getos": "3.1.0",
|
||||
"graphql": "14.0.2",
|
||||
"graphql-list-fields": "2.0.2",
|
||||
"graphql-subscriptions": "1.0.0",
|
||||
"graphql-tools": "4.0.0",
|
||||
"highlight.js": "9.12.0",
|
||||
"i18next": "11.9.0",
|
||||
"i18next-express-middleware": "1.4.0",
|
||||
"graphql-tools": "4.0.1",
|
||||
"highlight.js": "9.13.0",
|
||||
"i18next": "11.9.1",
|
||||
"i18next-express-middleware": "1.4.1",
|
||||
"i18next-localstorage-cache": "1.1.1",
|
||||
"i18next-node-fs-backend": "2.1.0",
|
||||
"image-size": "0.6.3",
|
||||
"ioredis": "4.0.0",
|
||||
"ioredis": "4.0.2",
|
||||
"js-binary": "1.2.0",
|
||||
"js-yaml": "3.12.0",
|
||||
"jsonwebtoken": "8.3.0",
|
||||
@@ -107,9 +107,9 @@
|
||||
"mime-types": "2.1.20",
|
||||
"moment": "2.22.2",
|
||||
"moment-timezone": "0.5.21",
|
||||
"mongodb": "3.1.6",
|
||||
"mongodb": "3.1.8",
|
||||
"mssql": "4.2.1",
|
||||
"multer": "1.4.0",
|
||||
"multer": "1.4.1",
|
||||
"mysql2": "1.6.1",
|
||||
"node-2fa": "1.1.2",
|
||||
"node-cache": "4.2.0",
|
||||
@@ -135,9 +135,9 @@
|
||||
"passport-slack": "0.0.7",
|
||||
"passport-twitch": "1.0.3",
|
||||
"passport-windowslive": "1.0.2",
|
||||
"pg": "7.4.3",
|
||||
"pg": "7.5.0",
|
||||
"pg-hstore": "2.3.2",
|
||||
"pm2": "3.1.3",
|
||||
"pm2": "3.2.2",
|
||||
"pug": "2.0.3",
|
||||
"qr-image": "3.2.0",
|
||||
"raven": "2.6.4",
|
||||
@@ -146,7 +146,7 @@
|
||||
"request": "2.88.0",
|
||||
"request-promise": "4.2.2",
|
||||
"scim-query-filter-parser": "1.1.0",
|
||||
"semver": "5.5.1",
|
||||
"semver": "5.6.0",
|
||||
"serve-favicon": "2.5.0",
|
||||
"sqlite3": "4.0.2",
|
||||
"subscriptions-transport-ws": "0.9.15",
|
||||
@@ -172,9 +172,9 @@
|
||||
"@babel/polyfill": "^7.0.0",
|
||||
"@babel/preset-env": "^7.1.0",
|
||||
"@panter/vue-i18next": "0.13.0",
|
||||
"@vue/cli": "3.0.4",
|
||||
"@vue/cli": "3.0.5",
|
||||
"animated-number-vue": "0.1.3",
|
||||
"apollo-cache-inmemory": "1.3.0",
|
||||
"apollo-cache-inmemory": "1.3.5",
|
||||
"apollo-client": "2.4.2",
|
||||
"apollo-fetch": "0.7.0",
|
||||
"apollo-link": "1.2.3",
|
||||
@@ -195,12 +195,12 @@
|
||||
"cache-loader": "1.2.2",
|
||||
"chart.js": "2.7.2",
|
||||
"clean-webpack-plugin": "0.1.19",
|
||||
"copy-webpack-plugin": "4.5.2",
|
||||
"copy-webpack-plugin": "4.5.3",
|
||||
"css-loader": "1.0.0",
|
||||
"cssnano": "4.1.4",
|
||||
"duplicate-package-checker-webpack-plugin": "3.0.0",
|
||||
"epic-spinners": "1.0.3",
|
||||
"eslint": "5.6.1",
|
||||
"eslint": "5.7.0",
|
||||
"eslint-config-requarks": "1.0.7",
|
||||
"eslint-config-standard": "12.0.0",
|
||||
"eslint-plugin-import": "2.14.0",
|
||||
@@ -212,15 +212,15 @@
|
||||
"grapesjs": "0.14.33",
|
||||
"graphiql": "0.12.0",
|
||||
"graphql-persisted-document-loader": "1.0.1",
|
||||
"graphql-tag": "^2.9.2",
|
||||
"graphql-voyager": "1.0.0-rc.25",
|
||||
"graphql-tag": "^2.10.0",
|
||||
"graphql-voyager": "1.0.0-rc.26",
|
||||
"hammerjs": "2.0.8",
|
||||
"html-webpack-plugin": "3.2.0",
|
||||
"html-webpack-pug-plugin": "0.3.0",
|
||||
"i18next-xhr-backend": "1.5.1",
|
||||
"ignore-loader": "0.1.2",
|
||||
"js-cookie": "2.2.0",
|
||||
"mini-css-extract-plugin": "0.4.3",
|
||||
"mini-css-extract-plugin": "0.4.4",
|
||||
"node-sass": "4.9.3",
|
||||
"offline-plugin": "5.0.5",
|
||||
"optimize-css-assets-webpack-plugin": "5.0.1",
|
||||
@@ -229,7 +229,7 @@
|
||||
"postcss-flexibility": "2.0.0",
|
||||
"postcss-import": "12.0.0",
|
||||
"postcss-loader": "3.0.0",
|
||||
"postcss-preset-env": "6.0.7",
|
||||
"postcss-preset-env": "6.1.1",
|
||||
"postcss-selector-parser": "5.0.0-rc.3",
|
||||
"pug-lint": "2.5.0",
|
||||
"pug-loader": "2.4.0",
|
||||
@@ -242,12 +242,12 @@
|
||||
"sass-resources-loader": "1.3.3",
|
||||
"script-ext-html-webpack-plugin": "2.0.1",
|
||||
"simple-progress-webpack-plugin": "1.1.2",
|
||||
"style-loader": "0.23.0",
|
||||
"style-loader": "0.23.1",
|
||||
"stylus": "0.54.5",
|
||||
"stylus-loader": "3.0.2",
|
||||
"twemoji-awesome": "1.0.6",
|
||||
"url-loader": "1.1.1",
|
||||
"vee-validate": "2.1.0-beta.9",
|
||||
"url-loader": "1.1.2",
|
||||
"vee-validate": "2.1.0-beta.11",
|
||||
"velocity-animate": "1.5.2",
|
||||
"viz.js": "2.0.0",
|
||||
"vue": "2.5.17",
|
||||
@@ -257,7 +257,7 @@
|
||||
"vue-codemirror": "4.0.5",
|
||||
"vue-hot-reload-api": "2.3.1",
|
||||
"vue-loader": "15.4.2",
|
||||
"vue-material-design-icons": "2.1.1",
|
||||
"vue-material-design-icons": "2.3.0",
|
||||
"vue-moment": "4.0.0",
|
||||
"vue-router": "3.0.1",
|
||||
"vue-simple-breakpoints": "1.0.3",
|
||||
@@ -267,7 +267,7 @@
|
||||
"vue-tree-navigation": "3.0.1",
|
||||
"vue2-animate": "2.1.0",
|
||||
"vuedraggable": "2.16.0",
|
||||
"vuetify": "1.2.5",
|
||||
"vuetify": "1.2.9",
|
||||
"vuex": "3.0.1",
|
||||
"vuex-pathify": "1.1.3",
|
||||
"vuex-persistedstate": "2.5.4",
|
||||
@@ -275,12 +275,12 @@
|
||||
"webpack-bundle-analyzer": "3.0.2",
|
||||
"webpack-cli": "3.1.2",
|
||||
"webpack-dev-middleware": "3.4.0",
|
||||
"webpack-hot-middleware": "2.24.2",
|
||||
"webpack-hot-middleware": "2.24.3",
|
||||
"webpack-merge": "4.1.4",
|
||||
"webpack-subresource-integrity": "1.1.0-rc.6",
|
||||
"webpack-subresource-integrity": "1.2.0",
|
||||
"whatwg-fetch": "3.0.0",
|
||||
"write-file-webpack-plugin": "4.4.1",
|
||||
"xterm": "3.7.0"
|
||||
"xterm": "3.8.0"
|
||||
},
|
||||
"browserslist": [
|
||||
"> 1%",
|
||||
|
||||
@@ -11,14 +11,14 @@ module.exports = {
|
||||
UserQuery: {
|
||||
async list(obj, args, context, info) {
|
||||
return WIKI.models.users.query()
|
||||
.select('id', 'email', 'name', 'providerKey', 'role', 'createdAt', 'updatedAt')
|
||||
.select('id', 'email', 'name', 'providerKey', 'createdAt')
|
||||
},
|
||||
async search(obj, args, context, info) {
|
||||
return WIKI.models.users.query()
|
||||
.where('email', 'like', `%${args.query}%`)
|
||||
.orWhere('name', 'like', `%${args.query}%`)
|
||||
.limit(10)
|
||||
.select('id', 'email', 'name', 'providerKey', 'role', 'createdAt', 'updatedAt')
|
||||
.select('id', 'email', 'name', 'providerKey', 'createdAt')
|
||||
},
|
||||
async single(obj, args, context, info) {
|
||||
let usr = await WIKI.models.users.query().findById(args.id)
|
||||
|
||||
@@ -86,6 +86,7 @@ type UserMinimal {
|
||||
name: String!
|
||||
email: String!
|
||||
providerKey: String!
|
||||
createdAt: Date!
|
||||
}
|
||||
|
||||
type User {
|
||||
|
||||
@@ -22,7 +22,7 @@ module.exports = {
|
||||
|
||||
// Try headers, otherwise cookies for response
|
||||
if (req.get('content-type') === 'application/json') {
|
||||
res.headers('new-jwt', newToken.token)
|
||||
res.set('new-jwt', newToken.token)
|
||||
} else {
|
||||
res.cookie('jwt', newToken.token, { expires: moment().add(365, 'days').toDate() })
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user