Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
W
wiki-js
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
1
Issues
1
List
Board
Labels
Milestones
Merge Requests
1
Merge Requests
1
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Registry
Registry
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Jacklull
wiki-js
Commits
47ed7b37
Unverified
Commit
47ed7b37
authored
May 29, 2022
by
Nicolas Giard
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat(admin): migrate users to vue 3 composable
parent
6e303ac6
Hide whitespace changes
Inline
Side-by-side
Showing
10 changed files
with
990 additions
and
855 deletions
+990
-855
user.js
server/graph/resolvers/user.js
+9
-9
package.json
ux/package.json
+1
-0
GroupEditOverlay.vue
ux/src/components/GroupEditOverlay.vue
+1
-1
UserChangePwdDialog.vue
ux/src/components/UserChangePwdDialog.vue
+146
-126
UserCreateDialog.vue
ux/src/components/UserCreateDialog.vue
+220
-192
UserEditOverlay.vue
ux/src/components/UserEditOverlay.vue
+444
-390
AdminGroups.vue
ux/src/pages/AdminGroups.vue
+3
-3
AdminUsers.vue
ux/src/pages/AdminUsers.vue
+165
-133
routes.js
ux/src/router/routes.js
+1
-1
yarn.lock
ux/yarn.lock
+0
-0
No files found.
server/graph/resolvers/user.js
View file @
47ed7b37
...
@@ -89,7 +89,7 @@ module.exports = {
...
@@ -89,7 +89,7 @@ module.exports = {
await
WIKI
.
models
.
users
.
createNewUser
({
...
args
,
passwordRaw
:
args
.
password
,
isVerified
:
true
})
await
WIKI
.
models
.
users
.
createNewUser
({
...
args
,
passwordRaw
:
args
.
password
,
isVerified
:
true
})
return
{
return
{
status
:
graphHelper
.
generateSuccess
(
'User created successfully'
)
operation
:
graphHelper
.
generateSuccess
(
'User created successfully'
)
}
}
}
catch
(
err
)
{
}
catch
(
err
)
{
return
graphHelper
.
generateError
(
err
)
return
graphHelper
.
generateError
(
err
)
...
@@ -106,7 +106,7 @@ module.exports = {
...
@@ -106,7 +106,7 @@ module.exports = {
WIKI
.
events
.
outbound
.
emit
(
'addAuthRevoke'
,
{
id
:
args
.
id
,
kind
:
'u'
})
WIKI
.
events
.
outbound
.
emit
(
'addAuthRevoke'
,
{
id
:
args
.
id
,
kind
:
'u'
})
return
{
return
{
status
:
graphHelper
.
generateSuccess
(
'User deleted successfully'
)
operation
:
graphHelper
.
generateSuccess
(
'User deleted successfully'
)
}
}
}
catch
(
err
)
{
}
catch
(
err
)
{
if
(
err
.
message
.
indexOf
(
'foreign'
)
>=
0
)
{
if
(
err
.
message
.
indexOf
(
'foreign'
)
>=
0
)
{
...
@@ -121,7 +121,7 @@ module.exports = {
...
@@ -121,7 +121,7 @@ module.exports = {
await
WIKI
.
models
.
users
.
updateUser
(
args
.
id
,
args
.
patch
)
await
WIKI
.
models
.
users
.
updateUser
(
args
.
id
,
args
.
patch
)
return
{
return
{
status
:
graphHelper
.
generateSuccess
(
'User updated successfully'
)
operation
:
graphHelper
.
generateSuccess
(
'User updated successfully'
)
}
}
}
catch
(
err
)
{
}
catch
(
err
)
{
return
graphHelper
.
generateError
(
err
)
return
graphHelper
.
generateError
(
err
)
...
@@ -132,7 +132,7 @@ module.exports = {
...
@@ -132,7 +132,7 @@ module.exports = {
await
WIKI
.
models
.
users
.
query
().
patch
({
isVerified
:
true
}).
findById
(
args
.
id
)
await
WIKI
.
models
.
users
.
query
().
patch
({
isVerified
:
true
}).
findById
(
args
.
id
)
return
{
return
{
status
:
graphHelper
.
generateSuccess
(
'User verified successfully'
)
operation
:
graphHelper
.
generateSuccess
(
'User verified successfully'
)
}
}
}
catch
(
err
)
{
}
catch
(
err
)
{
return
graphHelper
.
generateError
(
err
)
return
graphHelper
.
generateError
(
err
)
...
@@ -143,7 +143,7 @@ module.exports = {
...
@@ -143,7 +143,7 @@ module.exports = {
await
WIKI
.
models
.
users
.
query
().
patch
({
isActive
:
true
}).
findById
(
args
.
id
)
await
WIKI
.
models
.
users
.
query
().
patch
({
isActive
:
true
}).
findById
(
args
.
id
)
return
{
return
{
status
:
graphHelper
.
generateSuccess
(
'User activated successfully'
)
operation
:
graphHelper
.
generateSuccess
(
'User activated successfully'
)
}
}
}
catch
(
err
)
{
}
catch
(
err
)
{
return
graphHelper
.
generateError
(
err
)
return
graphHelper
.
generateError
(
err
)
...
@@ -160,7 +160,7 @@ module.exports = {
...
@@ -160,7 +160,7 @@ module.exports = {
WIKI
.
events
.
outbound
.
emit
(
'addAuthRevoke'
,
{
id
:
args
.
id
,
kind
:
'u'
})
WIKI
.
events
.
outbound
.
emit
(
'addAuthRevoke'
,
{
id
:
args
.
id
,
kind
:
'u'
})
return
{
return
{
status
:
graphHelper
.
generateSuccess
(
'User deactivated successfully'
)
operation
:
graphHelper
.
generateSuccess
(
'User deactivated successfully'
)
}
}
}
catch
(
err
)
{
}
catch
(
err
)
{
return
graphHelper
.
generateError
(
err
)
return
graphHelper
.
generateError
(
err
)
...
@@ -171,7 +171,7 @@ module.exports = {
...
@@ -171,7 +171,7 @@ module.exports = {
await
WIKI
.
models
.
users
.
query
().
patch
({
tfaIsActive
:
true
,
tfaSecret
:
null
}).
findById
(
args
.
id
)
await
WIKI
.
models
.
users
.
query
().
patch
({
tfaIsActive
:
true
,
tfaSecret
:
null
}).
findById
(
args
.
id
)
return
{
return
{
status
:
graphHelper
.
generateSuccess
(
'User 2FA enabled successfully'
)
operation
:
graphHelper
.
generateSuccess
(
'User 2FA enabled successfully'
)
}
}
}
catch
(
err
)
{
}
catch
(
err
)
{
return
graphHelper
.
generateError
(
err
)
return
graphHelper
.
generateError
(
err
)
...
@@ -182,7 +182,7 @@ module.exports = {
...
@@ -182,7 +182,7 @@ module.exports = {
await
WIKI
.
models
.
users
.
query
().
patch
({
tfaIsActive
:
false
,
tfaSecret
:
null
}).
findById
(
args
.
id
)
await
WIKI
.
models
.
users
.
query
().
patch
({
tfaIsActive
:
false
,
tfaSecret
:
null
}).
findById
(
args
.
id
)
return
{
return
{
status
:
graphHelper
.
generateSuccess
(
'User 2FA disabled successfully'
)
operation
:
graphHelper
.
generateSuccess
(
'User 2FA disabled successfully'
)
}
}
}
catch
(
err
)
{
}
catch
(
err
)
{
return
graphHelper
.
generateError
(
err
)
return
graphHelper
.
generateError
(
err
)
...
@@ -225,7 +225,7 @@ module.exports = {
...
@@ -225,7 +225,7 @@ module.exports = {
const
newToken
=
await
WIKI
.
models
.
users
.
refreshToken
(
usr
.
id
)
const
newToken
=
await
WIKI
.
models
.
users
.
refreshToken
(
usr
.
id
)
return
{
return
{
status
:
graphHelper
.
generateSuccess
(
'User profile updated successfully'
),
operation
:
graphHelper
.
generateSuccess
(
'User profile updated successfully'
),
jwt
:
newToken
.
token
jwt
:
newToken
.
token
}
}
}
catch
(
err
)
{
}
catch
(
err
)
{
...
...
ux/package.json
View file @
47ed7b37
...
@@ -76,6 +76,7 @@
...
@@ -76,6 +76,7 @@
"uuid"
:
"8.3.2"
,
"uuid"
:
"8.3.2"
,
"v-network-graph"
:
"0.5.16"
,
"v-network-graph"
:
"0.5.16"
,
"vue"
:
"3.2.31"
,
"vue"
:
"3.2.31"
,
"vue-codemirror"
:
"5.0.1"
,
"vue-i18n"
:
"9.1.10"
,
"vue-i18n"
:
"9.1.10"
,
"vue-router"
:
"4.0.15"
,
"vue-router"
:
"4.0.15"
,
"vuedraggable"
:
"4.1.0"
,
"vuedraggable"
:
"4.1.0"
,
...
...
ux/src/components/GroupEditOverlay.vue
View file @
47ed7b37
...
@@ -501,7 +501,7 @@ import { fileOpen } from 'browser-fs-access'
...
@@ -501,7 +501,7 @@ import { fileOpen } from 'browser-fs-access'
import
{
useI18n
}
from
'vue-i18n'
import
{
useI18n
}
from
'vue-i18n'
import
{
exportFile
,
useQuasar
}
from
'quasar'
import
{
exportFile
,
useQuasar
}
from
'quasar'
import
{
computed
,
on
BeforeUnmount
,
on
Mounted
,
reactive
,
watch
}
from
'vue'
import
{
computed
,
onMounted
,
reactive
,
watch
}
from
'vue'
import
{
useRouter
,
useRoute
}
from
'vue-router'
import
{
useRouter
,
useRoute
}
from
'vue-router'
import
{
useAdminStore
}
from
'src/stores/admin'
import
{
useAdminStore
}
from
'src/stores/admin'
...
...
ux/src/components/UserChangePwdDialog.vue
View file @
47ed7b37
<
template
lang=
"pug"
>
<
template
lang=
"pug"
>
q-dialog(ref='dialog', @hide='onDialogHide')
q-dialog(ref='dialog
Ref
', @hide='onDialogHide')
q-card(style='min-width: 650px;')
q-card(style='min-width: 650px;')
q-card-section.card-header
q-card-section.card-header
q-icon(name='img:/_assets/icons/fluent-password-reset.svg', left, size='sm')
q-icon(name='img:/_assets/icons/fluent-password-reset.svg', left, size='sm')
span
{{
$
t
(
`admin.users.changePassword`
)
}}
span
{{
t
(
`admin.users.changePassword`
)
}}
q-form.q-py-sm(ref='changeUserPwdForm', @submit='save')
q-form.q-py-sm(ref='changeUserPwdForm', @submit='save')
q-item
q-item
blueprint-icon(icon='password')
blueprint-icon(icon='password')
q-item-section
q-item-section
q-input(
q-input(
outlined
outlined
v-model='userPassword'
v-model='
state.
userPassword'
dense
dense
:rules=`[
:rules='userPasswordValidation'
val => val.length > 0 || $t('admin.users.passwordMissing'),
val => val.length >= 8 || $t('admin.users.passwordTooShort')
]`
hide-bottom-space
hide-bottom-space
:label='
$
t(`admin.users.password`)'
:label='t(`admin.users.password`)'
:aria-label='
$
t(`admin.users.password`)'
:aria-label='t(`admin.users.password`)'
lazy-rules='ondemand'
lazy-rules='ondemand'
autofocus
autofocus
)
)
...
@@ -41,159 +38,182 @@ q-dialog(ref='dialog', @hide='onDialogHide')
...
@@ -41,159 +38,182 @@ q-dialog(ref='dialog', @hide='onDialogHide')
q-item(tag='label', v-ripple)
q-item(tag='label', v-ripple)
blueprint-icon(icon='password-reset')
blueprint-icon(icon='password-reset')
q-item-section
q-item-section
q-item-label
{{
$
t
(
`admin.users.mustChangePwd`
)
}}
q-item-label
{{
t
(
`admin.users.mustChangePwd`
)
}}
q-item-label(caption)
{{
$
t
(
`admin.users.mustChangePwdHint`
)
}}
q-item-label(caption)
{{
t
(
`admin.users.mustChangePwdHint`
)
}}
q-item-section(avatar)
q-item-section(avatar)
q-toggle(
q-toggle(
v-model='userMustChangePassword'
v-model='
state.
userMustChangePassword'
color='primary'
color='primary'
checked-icon='las la-check'
checked-icon='las la-check'
unchecked-icon='las la-times'
unchecked-icon='las la-times'
:aria-label='
$
t(`admin.users.mustChangePwd`)'
:aria-label='t(`admin.users.mustChangePwd`)'
)
)
q-card-actions.card-actions
q-card-actions.card-actions
q-space
q-space
q-btn.acrylic-btn(
q-btn.acrylic-btn(
flat
flat
:label='
$
t(`common.actions.cancel`)'
:label='t(`common.actions.cancel`)'
color='grey'
color='grey'
padding='xs md'
padding='xs md'
@click='
hide
'
@click='
onDialogCancel
'
)
)
q-btn(
q-btn(
unelevated
unelevated
:label='
$
t(`common.actions.update`)'
:label='t(`common.actions.update`)'
color='primary'
color='primary'
padding='xs md'
padding='xs md'
@click='save'
@click='save'
:loading='isLoading'
:loading='
state.
isLoading'
)
)
</
template
>
</
template
>
<
script
>
<
script
setup
>
import
gql
from
'graphql-tag'
import
gql
from
'graphql-tag'
import
sampleSize
from
'lodash/sampleSize'
import
sampleSize
from
'lodash/sampleSize'
import
zxcvbn
from
'zxcvbn'
import
zxcvbn
from
'zxcvbn'
export
default
{
import
{
useI18n
}
from
'vue-i18n'
props
:
{
import
{
useDialogPluginComponent
,
useQuasar
}
from
'quasar'
userId
:
{
import
{
computed
,
reactive
,
ref
}
from
'vue'
type
:
String
,
required
:
true
// PROPS
}
},
const
props
=
defineProps
({
emits
:
[
'ok'
,
'hide'
],
userId
:
{
data
()
{
type
:
String
,
required
:
true
}
})
// EMITS
defineEmits
([
...
useDialogPluginComponent
.
emits
])
// QUASAR
const
{
dialogRef
,
onDialogHide
,
onDialogOK
,
onDialogCancel
}
=
useDialogPluginComponent
()
const
$q
=
useQuasar
()
// I18N
const
{
t
}
=
useI18n
()
// DATA
const
state
=
reactive
({
userPassword
:
''
,
userMustChangePassword
:
false
,
isLoading
:
false
})
// REFS
const
changeUserPwdForm
=
ref
(
null
)
// COMPUTED
const
passwordStrength
=
computed
(()
=>
{
if
(
state
.
userPassword
.
length
<
8
)
{
return
{
return
{
userPassword
:
''
,
color
:
'negative'
,
userMustChangePassword
:
false
,
label
:
t
(
'admin.users.pwdStrengthWeak'
)
isLoading
:
false
}
}
},
}
else
{
computed
:
{
switch
(
zxcvbn
(
state
.
userPassword
).
score
)
{
passwordStrength
()
{
case
1
:
if
(
this
.
userPassword
.
length
<
8
)
{
return
{
return
{
color
:
'
negative
'
,
color
:
'
deep-orange-7
'
,
label
:
t
his
.
$t
(
'admin.users.pwdStrengthWeak
'
)
label
:
t
(
'admin.users.pwdStrengthPoor
'
)
}
}
}
else
{
case
2
:
switch
(
zxcvbn
(
this
.
userPassword
).
score
)
{
return
{
case
1
:
color
:
'purple-7'
,
return
{
label
:
t
(
'admin.users.pwdStrengthMedium'
)
color
:
'deep-orange-7'
,
label
:
this
.
$t
(
'admin.users.pwdStrengthPoor'
)
}
case
2
:
return
{
color
:
'purple-7'
,
label
:
this
.
$t
(
'admin.users.pwdStrengthMedium'
)
}
case
3
:
return
{
color
:
'blue-7'
,
label
:
this
.
$t
(
'admin.users.pwdStrengthGood'
)
}
case
4
:
return
{
color
:
'green-7'
,
label
:
this
.
$t
(
'admin.users.pwdStrengthStrong'
)
}
default
:
return
{
color
:
'negative'
,
label
:
this
.
$t
(
'admin.users.pwdStrengthWeak'
)
}
}
}
}
case
3
:
}
return
{
},
color
:
'blue-7'
,
methods
:
{
label
:
t
(
'admin.users.pwdStrengthGood'
)
show
()
{
this
.
$refs
.
dialog
.
show
()
},
hide
()
{
this
.
$refs
.
dialog
.
hide
()
},
onDialogHide
()
{
this
.
$emit
(
'hide'
)
},
randomizePassword
()
{
const
pwdChars
=
'abcdefghkmnpqrstuvwxyzABCDEFHJKLMNPQRSTUVWXYZ23456789_*=?#!()+'
this
.
userPassword
=
sampleSize
(
pwdChars
,
16
).
join
(
''
)
},
async
save
()
{
this
.
isLoading
=
true
try
{
const
isFormValid
=
await
this
.
$refs
.
changeUserPwdForm
.
validate
(
true
)
if
(
!
isFormValid
)
{
throw
new
Error
(
this
.
$t
(
'admin.users.createInvalidData'
))
}
}
const
resp
=
await
this
.
$apollo
.
mutate
({
case
4
:
mutation
:
gql
`
return
{
mutation adminUpdateUserPwd (
color
:
'green-7'
,
$id: UUID!
label
:
t
(
'admin.users.pwdStrengthStrong'
)
$patch: UserUpdateInput!
}
) {
default
:
updateUser (
return
{
id: $id
color
:
'negative'
,
patch: $patch
label
:
t
(
'admin.users.pwdStrengthWeak'
)
) {
}
status {
}
succeeded
}
message
})
}
}
// VALIDATION RULES
}
`
,
const
userPasswordValidation
=
[
variables
:
{
val
=>
val
.
length
>
0
||
t
(
'admin.users.passwordMissing'
),
id
:
this
.
userId
,
val
=>
val
.
length
>=
8
||
t
(
'admin.users.passwordTooShort'
)
patch
:
{
]
newPassword
:
this
.
userPassword
,
mustChangePassword
:
this
.
userMustChangePassword
// METHODS
function
randomizePassword
()
{
const
pwdChars
=
'abcdefghkmnpqrstuvwxyzABCDEFHJKLMNPQRSTUVWXYZ23456789_*=?#!()+'
state
.
userPassword
=
sampleSize
(
pwdChars
,
16
).
join
(
''
)
}
async
function
save
()
{
state
.
isLoading
=
true
try
{
const
isFormValid
=
await
changeUserPwdForm
.
value
.
validate
(
true
)
if
(
!
isFormValid
)
{
throw
new
Error
(
t
(
'admin.users.createInvalidData'
))
}
const
resp
=
await
APOLLO_CLIENT
.
mutate
({
mutation
:
gql
`
mutation adminUpdateUserPwd (
$id: UUID!
$patch: UserUpdateInput!
) {
updateUser (
id: $id
patch: $patch
) {
operation {
succeeded
message
}
}
}
}
})
if
(
resp
?.
data
?.
updateUser
?.
status
?.
succeeded
)
{
this
.
$q
.
notify
({
type
:
'positive'
,
message
:
this
.
$t
(
'admin.users.createSuccess'
)
})
this
.
$emit
(
'ok'
,
{
mustChangePassword
:
this
.
userMustChangePassword
})
this
.
hide
()
}
else
{
throw
new
Error
(
resp
?.
data
?.
updateUser
?.
status
?.
message
||
'An unexpected error occured.'
)
}
}
}
catch
(
err
)
{
`
,
this
.
$q
.
notify
({
variables
:
{
type
:
'negative'
,
id
:
props
.
userId
,
message
:
err
.
message
patch
:
{
})
newPassword
:
state
.
userPassword
,
mustChangePassword
:
state
.
userMustChangePassword
}
}
}
this
.
isLoading
=
false
})
if
(
resp
?.
data
?.
updateUser
?.
operation
?.
succeeded
)
{
$q
.
notify
({
type
:
'positive'
,
message
:
t
(
'admin.users.createSuccess'
)
})
onDialogOK
({
mustChangePassword
:
state
.
userMustChangePassword
})
}
else
{
throw
new
Error
(
resp
?.
data
?.
updateUser
?.
operation
?.
message
||
'An unexpected error occured.'
)
}
}
}
catch
(
err
)
{
$q
.
notify
({
type
:
'negative'
,
message
:
err
.
message
})
}
}
state
.
isLoading
=
false
}
}
</
script
>
</
script
>
ux/src/components/UserCreateDialog.vue
View file @
47ed7b37
<
template
lang=
"pug"
>
<
template
lang=
"pug"
>
q-dialog(ref='dialog', @hide='onDialogHide')
q-dialog(ref='dialog
Ref
', @hide='onDialogHide')
q-card(style='min-width: 650px;')
q-card(style='min-width: 650px;')
q-card-section.card-header
q-card-section.card-header
q-icon(name='img:/_assets/icons/fluent-plus-plus.svg', left, size='sm')
q-icon(name='img:/_assets/icons/fluent-plus-plus.svg', left, size='sm')
span
{{
$
t
(
`admin.users.create`
)
}}
span
{{
t
(
`admin.users.create`
)
}}
q-form.q-py-sm(ref='createUserForm', @submit='create')
q-form.q-py-sm(ref='createUserForm', @submit='create')
q-item
q-item
blueprint-icon(icon='person')
blueprint-icon(icon='person')
q-item-section
q-item-section
q-input(
q-input(
outlined
outlined
v-model='userName'
v-model='
state.
userName'
dense
dense
:rules=`[
:rules='userNameValidation'
val => val.length > 0 || $t('admin.users.nameMissing'),
val => /^[^<>"]+$/.test(val) || $t('admin.users.nameInvalidChars')
]`
hide-bottom-space
hide-bottom-space
:label='
$
t(`common.field.name`)'
:label='t(`common.field.name`)'
:aria-label='
$
t(`common.field.name`)'
:aria-label='t(`common.field.name`)'
lazy-rules='ondemand'
lazy-rules='ondemand'
autofocus
autofocus
ref='iptName'
ref='iptName'
...
@@ -28,16 +25,13 @@ q-dialog(ref='dialog', @hide='onDialogHide')
...
@@ -28,16 +25,13 @@ q-dialog(ref='dialog', @hide='onDialogHide')
q-item-section
q-item-section
q-input(
q-input(
outlined
outlined
v-model='userEmail'
v-model='
state.
userEmail'
dense
dense
type='email'
type='email'
:rules=`[
:rules='userEmailValidation'
val => val.length > 0 || $t('admin.users.emailMissing'),
val => /^.+\@.+\..+$/.test(val) || $t('admin.users.emailInvalid')
]`
hide-bottom-space
hide-bottom-space
:label='
$
t(`admin.users.email`)'
:label='t(`admin.users.email`)'
:aria-label='
$
t(`admin.users.email`)'
:aria-label='t(`admin.users.email`)'
lazy-rules='ondemand'
lazy-rules='ondemand'
autofocus
autofocus
)
)
...
@@ -46,15 +40,12 @@ q-dialog(ref='dialog', @hide='onDialogHide')
...
@@ -46,15 +40,12 @@ q-dialog(ref='dialog', @hide='onDialogHide')
q-item-section
q-item-section
q-input(
q-input(
outlined
outlined
v-model='userPassword'
v-model='
state.
userPassword'
dense
dense
:rules=`[
:rules='userPasswordValidation'
val => val.length > 0 || $t('admin.users.passwordMissing'),
val => val.length >= 8 || $t('admin.users.passwordTooShort')
]`
hide-bottom-space
hide-bottom-space
:label='
$
t(`admin.users.password`)'
:label='t(`admin.users.password`)'
:aria-label='
$
t(`admin.users.password`)'
:aria-label='t(`admin.users.password`)'
lazy-rules='ondemand'
lazy-rules='ondemand'
autofocus
autofocus
)
)
...
@@ -79,8 +70,8 @@ q-dialog(ref='dialog', @hide='onDialogHide')
...
@@ -79,8 +70,8 @@ q-dialog(ref='dialog', @hide='onDialogHide')
q-item-section
q-item-section
q-select(
q-select(
outlined
outlined
:options='groups'
:options='
state.
groups'
v-model='userGroups'
v-model='
state.
userGroups'
multiple
multiple
map-options
map-options
emit-value
emit-value
...
@@ -88,29 +79,26 @@ q-dialog(ref='dialog', @hide='onDialogHide')
...
@@ -88,29 +79,26 @@ q-dialog(ref='dialog', @hide='onDialogHide')
option-label='name'
option-label='name'
options-dense
options-dense
dense
dense
:rules=`[
:rules='userGroupsValidation'
val => val.length > 0 || $t('admin.users.groupsMissing')
]`
hide-bottom-space
hide-bottom-space
:label='
$
t(`admin.users.groups`)'
:label='t(`admin.users.groups`)'
:aria-label='
$
t(`admin.users.groups`)'
:aria-label='t(`admin.users.groups`)'
lazy-rules='ondemand'
lazy-rules='ondemand'
:loading='loadingGroups'
:loading='
state.
loadingGroups'
)
)
template(v-slot:selected)
template(v-slot:selected)
.text-caption(v-if='userGroups.length > 1')
.text-caption(v-if='
state.
userGroups.length > 1')
i18n-t(keypath='admin.users.groupsSelected')
i18n-t(keypath='admin.users.groupsSelected')
template(#count)
template(#count)
strong
{{
userGroups
.
length
}}
strong
{{
state
.
userGroups
.
length
}}
.text-caption(v-else-if='userGroups.length === 1')
.text-caption(v-else-if='
state.
userGroups.length === 1')
i18n-t(keypath='admin.users.groupSelected')
i18n-t(keypath='admin.users.groupSelected')
template(#group)
template(#group)
strong
{{
selectedGroupName
}}
strong
{{
selectedGroupName
}}
span(v-else)
span(v-else)
template(v-slot:option='{ itemProps,
itemEvents,
opt, selected, toggleOption }')
template(v-slot:option='{ itemProps, opt, selected, toggleOption }')
q-item(
q-item(
v-bind='itemProps'
v-bind='itemProps'
v-on='itemEvents'
)
)
q-item-section(side)
q-item-section(side)
q-checkbox(
q-checkbox(
...
@@ -123,214 +111,254 @@ q-dialog(ref='dialog', @hide='onDialogHide')
...
@@ -123,214 +111,254 @@ q-dialog(ref='dialog', @hide='onDialogHide')
q-item(tag='label', v-ripple)
q-item(tag='label', v-ripple)
blueprint-icon(icon='password-reset')
blueprint-icon(icon='password-reset')
q-item-section
q-item-section
q-item-label
{{
$
t
(
`admin.users.mustChangePwd`
)
}}
q-item-label
{{
t
(
`admin.users.mustChangePwd`
)
}}
q-item-label(caption)
{{
$
t
(
`admin.users.mustChangePwdHint`
)
}}
q-item-label(caption)
{{
t
(
`admin.users.mustChangePwdHint`
)
}}
q-item-section(avatar)
q-item-section(avatar)
q-toggle(
q-toggle(
v-model='userMustChangePassword'
v-model='
state.
userMustChangePassword'
color='primary'
color='primary'
checked-icon='las la-check'
checked-icon='las la-check'
unchecked-icon='las la-times'
unchecked-icon='las la-times'
:aria-label='
$
t(`admin.users.mustChangePwd`)'
:aria-label='t(`admin.users.mustChangePwd`)'
)
)
q-item(tag='label', v-ripple)
q-item(tag='label', v-ripple)
blueprint-icon(icon='email-open')
blueprint-icon(icon='email-open')
q-item-section
q-item-section
q-item-label
{{
$
t
(
`admin.users.sendWelcomeEmail`
)
}}
q-item-label
{{
t
(
`admin.users.sendWelcomeEmail`
)
}}
q-item-label(caption)
{{
$
t
(
`admin.users.sendWelcomeEmailHint`
)
}}
q-item-label(caption)
{{
t
(
`admin.users.sendWelcomeEmailHint`
)
}}
q-item-section(avatar)
q-item-section(avatar)
q-toggle(
q-toggle(
v-model='userSendWelcomeEmail'
v-model='
state.
userSendWelcomeEmail'
color='primary'
color='primary'
checked-icon='las la-check'
checked-icon='las la-check'
unchecked-icon='las la-times'
unchecked-icon='las la-times'
:aria-label='
$
t(`admin.users.sendWelcomeEmail`)'
:aria-label='t(`admin.users.sendWelcomeEmail`)'
)
)
q-card-actions.card-actions
q-card-actions.card-actions
q-checkbox(
q-checkbox(
v-model='keepOpened'
v-model='
state.
keepOpened'
color='primary'
color='primary'
:label='
$
t(`admin.users.createKeepOpened`)'
:label='t(`admin.users.createKeepOpened`)'
size='sm'
size='sm'
)
)
q-space
q-space
q-btn.acrylic-btn(
q-btn.acrylic-btn(
flat
flat
:label='
$
t(`common.actions.cancel`)'
:label='t(`common.actions.cancel`)'
color='grey'
color='grey'
padding='xs md'
padding='xs md'
@click='
hide
'
@click='
onDialogCancel
'
)
)
q-btn(
q-btn(
unelevated
unelevated
:label='
$
t(`common.actions.create`)'
:label='t(`common.actions.create`)'
color='primary'
color='primary'
padding='xs md'
padding='xs md'
@click='create'
@click='create'
:loading='loading > 0'
:loading='
state.
loading > 0'
)
)
</
template
>
</
template
>
<
script
>
<
script
setup
>
import
gql
from
'graphql-tag'
import
gql
from
'graphql-tag'
import
sampleSize
from
'lodash/sampleSize'
import
sampleSize
from
'lodash/sampleSize'
import
zxcvbn
from
'zxcvbn'
import
zxcvbn
from
'zxcvbn'
import
cloneDeep
from
'lodash/cloneDeep'
import
cloneDeep
from
'lodash/cloneDeep'
import
{
useI18n
}
from
'vue-i18n'
import
{
useDialogPluginComponent
,
useQuasar
}
from
'quasar'
import
{
computed
,
onMounted
,
reactive
,
ref
}
from
'vue'
export
default
{
// EMITS
emits
:
[
'ok'
,
'hide'
],
data
()
{
defineEmits
([
...
useDialogPluginComponent
.
emits
])
// QUASAR
const
{
dialogRef
,
onDialogHide
,
onDialogOK
,
onDialogCancel
}
=
useDialogPluginComponent
()
const
$q
=
useQuasar
()
// I18N
const
{
t
}
=
useI18n
()
// DATA
const
state
=
reactive
({
userName
:
''
,
userEmail
:
''
,
userPassword
:
''
,
userGroups
:
[],
userMustChangePassword
:
false
,
userSendWelcomeEmail
:
false
,
keepOpened
:
false
,
groups
:
[],
loadingGroups
:
false
,
loading
:
false
})
// REFS
const
createUserForm
=
ref
(
null
)
const
iptName
=
ref
(
null
)
// COMPUTED
const
passwordStrength
=
computed
(()
=>
{
if
(
state
.
userPassword
.
length
<
8
)
{
return
{
return
{
userName
:
''
,
color
:
'negative'
,
userEmail
:
''
,
label
:
t
(
'admin.users.pwdStrengthWeak'
)
userPassword
:
''
,
userGroups
:
[],
userMustChangePassword
:
false
,
userSendWelcomeEmail
:
false
,
keepOpened
:
false
,
groups
:
[],
loadingGroups
:
false
,
loading
:
false
}
}
},
}
else
{
computed
:
{
switch
(
zxcvbn
(
state
.
userPassword
).
score
)
{
passwordStrength
()
{
case
1
:
if
(
this
.
userPassword
.
length
<
8
)
{
return
{
return
{
color
:
'
negative
'
,
color
:
'
deep-orange-7
'
,
label
:
t
his
.
$t
(
'admin.users.pwdStrengthWeak
'
)
label
:
t
(
'admin.users.pwdStrengthPoor
'
)
}
}
}
else
{
case
2
:
switch
(
zxcvbn
(
this
.
userPassword
).
score
)
{
return
{
case
1
:
color
:
'purple-7'
,
return
{
label
:
t
(
'admin.users.pwdStrengthMedium'
)
color
:
'deep-orange-7'
,
}
label
:
this
.
$t
(
'admin.users.pwdStrengthPoor'
)
case
3
:
}
return
{
case
2
:
color
:
'blue-7'
,
return
{
label
:
t
(
'admin.users.pwdStrengthGood'
)
color
:
'purple-7'
,
}
label
:
this
.
$t
(
'admin.users.pwdStrengthMedium'
)
case
4
:
}
return
{
case
3
:
color
:
'green-7'
,
return
{
label
:
t
(
'admin.users.pwdStrengthStrong'
)
color
:
'blue-7'
,
}
label
:
this
.
$t
(
'admin.users.pwdStrengthGood'
)
default
:
}
return
{
case
4
:
color
:
'negative'
,
return
{
label
:
t
(
'admin.users.pwdStrengthWeak'
)
color
:
'green-7'
,
label
:
this
.
$t
(
'admin.users.pwdStrengthStrong'
)
}
default
:
return
{
color
:
'negative'
,
label
:
this
.
$t
(
'admin.users.pwdStrengthWeak'
)
}
}
}
}
},
selectedGroupName
()
{
return
this
.
groups
.
filter
(
g
=>
g
.
id
===
this
.
userGroups
[
0
])[
0
]?.
name
}
}
},
}
methods
:
{
})
async
show
()
{
const
selectedGroupName
=
computed
(()
=>
{
this
.
$refs
.
dialog
.
show
()
return
state
.
groups
.
filter
(
g
=>
g
.
id
===
state
.
userGroups
[
0
])[
0
]?.
name
})
this
.
loading
++
// VALIDATION RULES
this
.
loadingGroups
=
true
const
resp
=
await
this
.
$apollo
.
query
({
const
userNameValidation
=
[
query
:
gql
`
val
=>
val
.
length
>
0
||
t
(
'admin.users.nameMissing'
),
query getGroupsForCreateUser {
val
=>
/^
[^
<>"
]
+$/
.
test
(
val
)
||
t
(
'admin.users.nameInvalidChars'
)
groups {
]
id
name
const
userEmailValidation
=
[
}
val
=>
val
.
length
>
0
||
t
(
'admin.users.emailMissing'
),
}
val
=>
/^.+@.+
\.
.+$/
.
test
(
val
)
||
t
(
'admin.users.emailInvalid'
)
`
,
]
fetchPolicy
:
'network-only'
})
const
userPasswordValidation
=
[
this
.
groups
=
cloneDeep
(
resp
?.
data
?.
groups
?.
filter
(
g
=>
g
.
id
!==
'10000000-0000-4000-0000-000000000001'
)
??
[])
val
=>
val
.
length
>
0
||
t
(
'admin.users.passwordMissing'
),
this
.
loadingGroups
=
false
val
=>
val
.
length
>=
8
||
t
(
'admin.users.passwordTooShort'
)
this
.
loading
--
]
},
hide
()
{
const
userGroupsValidation
=
[
this
.
$refs
.
dialog
.
hide
()
val
=>
val
.
length
>
0
||
t
(
'admin.users.groupsMissing'
)
},
]
onDialogHide
()
{
this
.
$emit
(
'hide'
)
// METHODS
},
randomizePassword
()
{
async
function
loadGroups
()
{
const
pwdChars
=
'abcdefghkmnpqrstuvwxyzABCDEFHJKLMNPQRSTUVWXYZ23456789_*=?#!()+'
state
.
loading
++
this
.
userPassword
=
sampleSize
(
pwdChars
,
16
).
join
(
''
)
state
.
loadingGroups
=
true
},
const
resp
=
await
APOLLO_CLIENT
.
query
({
async
create
()
{
query
:
gql
`
this
.
loading
++
query getGroupsForCreateUser {
try
{
groups {
const
isFormValid
=
await
this
.
$refs
.
createUserForm
.
validate
(
true
)
id
if
(
!
isFormValid
)
{
name
throw
new
Error
(
this
.
$t
(
'admin.users.createInvalidData'
))
}
}
const
resp
=
await
this
.
$apollo
.
mutate
({
}
mutation
:
gql
`
`
,
mutation createUser (
fetchPolicy
:
'network-only'
$name: String!
})
$email: String!
state
.
groups
=
cloneDeep
(
resp
?.
data
?.
groups
?.
filter
(
g
=>
g
.
id
!==
'10000000-0000-4000-8000-000000000001'
)
??
[])
$password: String!
state
.
loadingGroups
=
false
$groups: [UUID]!
state
.
loading
--
$mustChangePassword: Boolean!
}
$sendWelcomeEmail: Boolean!
) {
function
randomizePassword
()
{
createUser (
const
pwdChars
=
'abcdefghkmnpqrstuvwxyzABCDEFHJKLMNPQRSTUVWXYZ23456789_*=?#!()+'
name: $name
state
.
userPassword
=
sampleSize
(
pwdChars
,
16
).
join
(
''
)
email: $email
}
password: $password
groups: $groups
async
function
create
()
{
mustChangePassword: $mustChangePassword
state
.
loading
++
sendWelcomeEmail: $sendWelcomeEmail
try
{
) {
const
isFormValid
=
await
createUserForm
.
value
.
validate
(
true
)
status {
if
(
!
isFormValid
)
{
succeeded
throw
new
Error
(
t
(
'admin.users.createInvalidData'
))
message
}
}
const
resp
=
await
APOLLO_CLIENT
.
mutate
({
}
mutation
:
gql
`
mutation createUser (
$name: String!
$email: String!
$password: String!
$groups: [UUID]!
$mustChangePassword: Boolean!
$sendWelcomeEmail: Boolean!
) {
createUser (
name: $name
email: $email
password: $password
groups: $groups
mustChangePassword: $mustChangePassword
sendWelcomeEmail: $sendWelcomeEmail
) {
operation {
succeeded
message
}
}
`
,
variables
:
{
name
:
this
.
userName
,
email
:
this
.
userEmail
,
password
:
this
.
userPassword
,
groups
:
this
.
userGroups
,
mustChangePassword
:
this
.
userMustChangePassword
,
sendWelcomeEmail
:
this
.
userSendWelcomeEmail
}
}
})
if
(
resp
?.
data
?.
createUser
?.
status
?.
succeeded
)
{
this
.
$q
.
notify
({
type
:
'positive'
,
message
:
this
.
$t
(
'admin.users.createSuccess'
)
})
if
(
this
.
keepOpened
)
{
this
.
userName
=
''
this
.
userEmail
=
''
this
.
userPassword
=
''
this
.
$refs
.
iptName
.
focus
()
}
else
{
this
.
$emit
(
'ok'
)
this
.
hide
()
}
}
else
{
throw
new
Error
(
resp
?.
data
?.
createUser
?.
status
?.
message
||
'An unexpected error occured.'
)
}
}
}
catch
(
err
)
{
`
,
this
.
$q
.
notify
({
variables
:
{
type
:
'negative'
,
name
:
state
.
userName
,
message
:
err
.
message
email
:
state
.
userEmail
,
})
password
:
state
.
userPassword
,
groups
:
state
.
userGroups
,
mustChangePassword
:
state
.
userMustChangePassword
,
sendWelcomeEmail
:
state
.
userSendWelcomeEmail
}
})
if
(
resp
?.
data
?.
createUser
?.
operation
?.
succeeded
)
{
$q
.
notify
({
type
:
'positive'
,
message
:
t
(
'admin.users.createSuccess'
)
})
if
(
state
.
keepOpened
)
{
state
.
userName
=
''
state
.
userEmail
=
''
state
.
userPassword
=
''
iptName
.
value
.
focus
()
}
else
{
onDialogOK
()
}
}
this
.
loading
--
}
else
{
throw
new
Error
(
resp
?.
data
?.
createUser
?.
operation
?.
message
||
'An unexpected error occured.'
)
}
}
}
catch
(
err
)
{
$q
.
notify
({
type
:
'negative'
,
message
:
err
.
message
})
}
}
state
.
loading
--
}
}
// MOUNTED
onMounted
(
loadGroups
)
</
script
>
</
script
>
ux/src/components/UserEditOverlay.vue
View file @
47ed7b37
...
@@ -3,26 +3,26 @@ q-layout(view='hHh lpR fFf', container)
...
@@ -3,26 +3,26 @@ q-layout(view='hHh lpR fFf', container)
q-header.card-header.q-px-md.q-py-sm
q-header.card-header.q-px-md.q-py-sm
q-icon(name='img:/_assets/icons/fluent-account.svg', left, size='md')
q-icon(name='img:/_assets/icons/fluent-account.svg', left, size='md')
div
div
span
{{
$
t
(
`admin.users.edit`
)
}}
span
{{
t
(
`admin.users.edit`
)
}}
.text-caption
{{
user
.
name
}}
.text-caption
{{
state
.
user
.
name
}}
q-space
q-space
q-btn-group(push)
q-btn-group(push)
q-btn(
q-btn(
push
push
color='grey-6'
color='grey-6'
text-color='white'
text-color='white'
:aria-label='
$
t(`common.actions.refresh`)'
:aria-label='t(`common.actions.refresh`)'
icon='las la-redo-alt'
icon='las la-redo-alt'
@click='
load
'
@click='
fetchUser
'
:loading='loading > 0'
:loading='
state.
loading > 0'
)
)
q-tooltip(anchor='center left', self='center right')
{{
$
t
(
`common.actions.refresh`
)
}}
q-tooltip(anchor='center left', self='center right')
{{
t
(
`common.actions.refresh`
)
}}
q-btn(
q-btn(
push
push
color='white'
color='white'
text-color='grey-7'
text-color='grey-7'
:label='
$
t(`common.actions.close`)'
:label='t(`common.actions.close`)'
:aria-label='
$
t(`common.actions.close`)'
:aria-label='t(`common.actions.close`)'
icon='las la-times'
icon='las la-times'
@click='close'
@click='close'
)
)
...
@@ -30,14 +30,14 @@ q-layout(view='hHh lpR fFf', container)
...
@@ -30,14 +30,14 @@ q-layout(view='hHh lpR fFf', container)
push
push
color='positive'
color='positive'
text-color='white'
text-color='white'
:label='
$
t(`common.actions.save`)'
:label='t(`common.actions.save`)'
:aria-label='
$
t(`common.actions.save`)'
:aria-label='t(`common.actions.save`)'
icon='las la-check'
icon='las la-check'
@click='save()'
@click='save()'
:disabled='loading > 0'
:disabled='
state.
loading > 0'
)
)
q-drawer.bg-dark-6(:model-value='true', :width='250', dark)
q-drawer.bg-dark-6(:model-value='true', :width='250', dark)
q-list(padding, v-if='loading < 1')
q-list(padding, v-if='
state.
loading < 1')
q-item(
q-item(
v-for='sc of sections'
v-for='sc of sections'
:key='`section-` + sc.key'
:key='`section-` + sc.key'
...
@@ -50,111 +50,111 @@ q-layout(view='hHh lpR fFf', container)
...
@@ -50,111 +50,111 @@ q-layout(view='hHh lpR fFf', container)
q-icon(:name='sc.icon', color='white')
q-icon(:name='sc.icon', color='white')
q-item-section
{{
sc
.
text
}}
q-item-section
{{
sc
.
text
}}
q-page-container
q-page-container
q-page(v-if='loading > 0')
q-page(v-if='
state.
loading > 0')
.flex.q-pa-lg.items-center
.flex.q-pa-lg.items-center
q-spinner-tail(color='primary', size='32px', :thickness='2')
q-spinner-tail(color='primary', size='32px', :thickness='2')
.text-caption.text-primary.q-pl-md: strong
{{
$
t
(
'admin.users.loading'
)
}}
.text-caption.text-primary.q-pl-md: strong
{{
t
(
'admin.users.loading'
)
}}
q-page(v-else-if='
$
route.params.section === `overview`')
q-page(v-else-if='route.params.section === `overview`')
.q-pa-md
.q-pa-md
.row.q-col-gutter-md
.row.q-col-gutter-md
.col-12.col-lg-8
.col-12.col-lg-8
q-card.shadow-1.q-pb-sm
q-card.shadow-1.q-pb-sm
q-card-section
q-card-section
.text-subtitle1
{{
$
t
(
'admin.users.profile'
)
}}
.text-subtitle1
{{
t
(
'admin.users.profile'
)
}}
q-item
q-item
blueprint-icon(icon='contact')
blueprint-icon(icon='contact')
q-item-section
q-item-section
q-item-label
{{
$
t
(
`admin.users.name`
)
}}
q-item-label
{{
t
(
`admin.users.name`
)
}}
q-item-label(caption)
{{
$
t
(
`admin.users.nameHint`
)
}}
q-item-label(caption)
{{
t
(
`admin.users.nameHint`
)
}}
q-item-section
q-item-section
q-input(
q-input(
outlined
outlined
v-model='user.name'
v-model='
state.
user.name'
dense
dense
:rules=`[
:rules=`[
val => invalidCharsRegex.test(val) ||
$
t('admin.users.nameInvalidChars')
val => invalidCharsRegex.test(val) || t('admin.users.nameInvalidChars')
]`
]`
hide-bottom-space
hide-bottom-space
:aria-label='
$
t(`admin.users.name`)'
:aria-label='t(`admin.users.name`)'
)
)
q-separator.q-my-sm(inset)
q-separator.q-my-sm(inset)
q-item
q-item
blueprint-icon(icon='envelope')
blueprint-icon(icon='envelope')
q-item-section
q-item-section
q-item-label
{{
$
t
(
`admin.users.email`
)
}}
q-item-label
{{
t
(
`admin.users.email`
)
}}
q-item-label(caption)
{{
$
t
(
`admin.users.emailHint`
)
}}
q-item-label(caption)
{{
t
(
`admin.users.emailHint`
)
}}
q-item-section
q-item-section
q-input(
q-input(
outlined
outlined
v-model='user.email'
v-model='
state.
user.email'
dense
dense
:aria-label='
$
t(`admin.users.email`)'
:aria-label='t(`admin.users.email`)'
)
)
template(v-if='user.meta')
template(v-if='
state.
user.meta')
q-separator.q-my-sm(inset)
q-separator.q-my-sm(inset)
q-item
q-item
blueprint-icon(icon='address')
blueprint-icon(icon='address')
q-item-section
q-item-section
q-item-label
{{
$
t
(
`admin.users.location`
)
}}
q-item-label
{{
t
(
`admin.users.location`
)
}}
q-item-label(caption)
{{
$
t
(
`admin.users.locationHint`
)
}}
q-item-label(caption)
{{
t
(
`admin.users.locationHint`
)
}}
q-item-section
q-item-section
q-input(
q-input(
outlined
outlined
v-model='user.meta.location'
v-model='
state.
user.meta.location'
dense
dense
:aria-label='
$
t(`admin.users.location`)'
:aria-label='t(`admin.users.location`)'
)
)
q-separator.q-my-sm(inset)
q-separator.q-my-sm(inset)
q-item
q-item
blueprint-icon(icon='new-job')
blueprint-icon(icon='new-job')
q-item-section
q-item-section
q-item-label
{{
$
t
(
`admin.users.jobTitle`
)
}}
q-item-label
{{
t
(
`admin.users.jobTitle`
)
}}
q-item-label(caption)
{{
$
t
(
`admin.users.jobTitleHint`
)
}}
q-item-label(caption)
{{
t
(
`admin.users.jobTitleHint`
)
}}
q-item-section
q-item-section
q-input(
q-input(
outlined
outlined
v-model='user.meta.jobTitle'
v-model='
state.
user.meta.jobTitle'
dense
dense
:aria-label='
$
t(`admin.users.jobTitle`)'
:aria-label='t(`admin.users.jobTitle`)'
)
)
q-card.shadow-1.q-pb-sm.q-mt-md(v-if='user.meta')
q-card.shadow-1.q-pb-sm.q-mt-md(v-if='
state.
user.meta')
q-card-section
q-card-section
.text-subtitle1
{{
$
t
(
'admin.users.preferences'
)
}}
.text-subtitle1
{{
t
(
'admin.users.preferences'
)
}}
q-item
q-item
blueprint-icon(icon='timezone')
blueprint-icon(icon='timezone')
q-item-section
q-item-section
q-item-label
{{
$
t
(
`admin.users.timezone`
)
}}
q-item-label
{{
t
(
`admin.users.timezone`
)
}}
q-item-label(caption)
{{
$
t
(
`admin.users.timezoneHint`
)
}}
q-item-label(caption)
{{
t
(
`admin.users.timezoneHint`
)
}}
q-item-section
q-item-section
q-select(
q-select(
outlined
outlined
v-model='user.prefs.timezone'
v-model='
state.
user.prefs.timezone'
:options='timezones'
:options='
dataStore.
timezones'
option-value='value'
option-value='value'
option-label='text'
option-label='text'
emit-value
emit-value
map-options
map-options
dense
dense
options-dense
options-dense
:aria-label='
$
t(`admin.users.timezone`)'
:aria-label='t(`admin.users.timezone`)'
)
)
q-separator.q-my-sm(inset)
q-separator.q-my-sm(inset)
q-item
q-item
blueprint-icon(icon='calendar')
blueprint-icon(icon='calendar')
q-item-section
q-item-section
q-item-label
{{
$
t
(
`admin.users.dateFormat`
)
}}
q-item-label
{{
t
(
`admin.users.dateFormat`
)
}}
q-item-label(caption)
{{
$
t
(
`admin.users.dateFormatHint`
)
}}
q-item-label(caption)
{{
t
(
`admin.users.dateFormatHint`
)
}}
q-item-section
q-item-section
q-select(
q-select(
outlined
outlined
v-model='user.prefs.dateFormat'
v-model='
state.
user.prefs.dateFormat'
emit-value
emit-value
map-options
map-options
dense
dense
:aria-label='
$
t(`admin.users.dateFormat`)'
:aria-label='t(`admin.users.dateFormat`)'
:options=`[
:options=`[
{ label:
$
t('profile.localeDefault'), value: '' },
{ label: t('profile.localeDefault'), value: '' },
{ label: 'DD/MM/YYYY', value: 'DD/MM/YYYY' },
{ label: 'DD/MM/YYYY', value: 'DD/MM/YYYY' },
{ label: 'DD.MM.YYYY', value: 'DD.MM.YYYY' },
{ label: 'DD.MM.YYYY', value: 'DD.MM.YYYY' },
{ label: 'MM/DD/YYYY', value: 'MM/DD/YYYY' },
{ label: 'MM/DD/YYYY', value: 'MM/DD/YYYY' },
...
@@ -166,168 +166,168 @@ q-layout(view='hHh lpR fFf', container)
...
@@ -166,168 +166,168 @@ q-layout(view='hHh lpR fFf', container)
q-item
q-item
blueprint-icon(icon='clock')
blueprint-icon(icon='clock')
q-item-section
q-item-section
q-item-label
{{
$
t
(
`admin.users.timeFormat`
)
}}
q-item-label
{{
t
(
`admin.users.timeFormat`
)
}}
q-item-label(caption)
{{
$
t
(
`admin.users.timeFormatHint`
)
}}
q-item-label(caption)
{{
t
(
`admin.users.timeFormatHint`
)
}}
q-item-section.col-auto
q-item-section.col-auto
q-btn-toggle(
q-btn-toggle(
v-model='user.prefs.timeFormat'
v-model='
state.
user.prefs.timeFormat'
push
push
glossy
glossy
no-caps
no-caps
toggle-color='primary'
toggle-color='primary'
:options=`[
:options=`[
{ label:
$
t('profile.timeFormat12h'), value: '12h' },
{ label: t('profile.timeFormat12h'), value: '12h' },
{ label:
$
t('profile.timeFormat24h'), value: '24h' }
{ label: t('profile.timeFormat24h'), value: '24h' }
]`
]`
)
)
q-separator.q-my-sm(inset)
q-separator.q-my-sm(inset)
q-item(tag='label', v-ripple)
q-item(tag='label', v-ripple)
blueprint-icon(icon='light-on')
blueprint-icon(icon='light-on')
q-item-section
q-item-section
q-item-label
{{
$
t
(
`admin.users.darkMode`
)
}}
q-item-label
{{
t
(
`admin.users.darkMode`
)
}}
q-item-label(caption)
{{
$
t
(
`admin.users.darkModeHint`
)
}}
q-item-label(caption)
{{
t
(
`admin.users.darkModeHint`
)
}}
q-item-section(avatar)
q-item-section(avatar)
q-toggle(
q-toggle(
v-model='user.prefs.darkMode'
v-model='
state.
user.prefs.darkMode'
color='primary'
color='primary'
checked-icon='las la-check'
checked-icon='las la-check'
unchecked-icon='las la-times'
unchecked-icon='las la-times'
:aria-label='
$
t(`admin.users.darkMode`)'
:aria-label='t(`admin.users.darkMode`)'
)
)
.col-12.col-lg-4
.col-12.col-lg-4
q-card.shadow-1.q-pb-sm
q-card.shadow-1.q-pb-sm
q-card-section
q-card-section
.text-subtitle1
{{
$
t
(
'admin.users.info'
)
}}
.text-subtitle1
{{
t
(
'admin.users.info'
)
}}
q-item
q-item
blueprint-icon(icon='person', :hue-rotate='-45')
blueprint-icon(icon='person', :hue-rotate='-45')
q-item-section
q-item-section
q-item-label
{{
$
t
(
`common.field.id`
)
}}
q-item-label
{{
t
(
`common.field.id`
)
}}
q-item-label: strong
{{
userI
d
}}
q-item-label: strong
{{
state
.
user
.
i
d
}}
q-separator.q-my-sm(inset)
q-separator.q-my-sm(inset)
q-item
q-item
blueprint-icon(icon='calendar-plus', :hue-rotate='-45')
blueprint-icon(icon='calendar-plus', :hue-rotate='-45')
q-item-section
q-item-section
q-item-label
{{
$
t
(
`common.field.createdOn`
)
}}
q-item-label
{{
t
(
`common.field.createdOn`
)
}}
q-item-label: strong
{{
humanizeDate
(
user
.
createdAt
)
}}
q-item-label: strong
{{
humanizeDate
(
state
.
user
.
createdAt
)
}}
q-separator.q-my-sm(inset)
q-separator.q-my-sm(inset)
q-item
q-item
blueprint-icon(icon='summertime', :hue-rotate='-45')
blueprint-icon(icon='summertime', :hue-rotate='-45')
q-item-section
q-item-section
q-item-label
{{
$
t
(
`common.field.lastUpdated`
)
}}
q-item-label
{{
t
(
`common.field.lastUpdated`
)
}}
q-item-label: strong
{{
humanizeDate
(
user
.
updatedAt
)
}}
q-item-label: strong
{{
humanizeDate
(
state
.
user
.
updatedAt
)
}}
q-separator.q-my-sm(inset)
q-separator.q-my-sm(inset)
q-item
q-item
blueprint-icon(icon='enter', :hue-rotate='-45')
blueprint-icon(icon='enter', :hue-rotate='-45')
q-item-section
q-item-section
q-item-label
{{
$
t
(
`admin.users.lastLoginAt`
)
}}
q-item-label
{{
t
(
`admin.users.lastLoginAt`
)
}}
q-item-label: strong
{{
humanizeDate
(
user
.
lastLoginAt
)
}}
q-item-label: strong
{{
humanizeDate
(
state
.
user
.
lastLoginAt
)
}}
q-card.shadow-1.q-pb-sm.q-mt-md(v-if='user.meta')
q-card.shadow-1.q-pb-sm.q-mt-md(v-if='
state.
user.meta')
q-card-section
q-card-section
.text-subtitle1
{{
$
t
(
'admin.users.notes'
)
}}
.text-subtitle1
{{
t
(
'admin.users.notes'
)
}}
q-input.q-mt-sm(
q-input.q-mt-sm(
outlined
outlined
v-model='user.meta.notes'
v-model='
state.
user.meta.notes'
type='textarea'
type='textarea'
:aria-label='
$
t(`admin.users.notes`)'
:aria-label='t(`admin.users.notes`)'
input-style='min-height: 243px'
input-style='min-height: 243px'
:hint='
$
t(`admin.users.noteHint`)'
:hint='t(`admin.users.noteHint`)'
)
)
q-page(v-else-if='
$
route.params.section === `activity`')
q-page(v-else-if='route.params.section === `activity`')
span ---
span ---
q-page(v-else-if='
$
route.params.section === `auth`')
q-page(v-else-if='route.params.section === `auth`')
.q-pa-md
.q-pa-md
.row.q-col-gutter-md
.row.q-col-gutter-md
.col-12.col-lg-7
.col-12.col-lg-7
q-card.shadow-1.q-pb-sm
q-card.shadow-1.q-pb-sm
q-card-section
q-card-section
.text-subtitle1
{{
$
t
(
'admin.users.passAuth'
)
}}
.text-subtitle1
{{
t
(
'admin.users.passAuth'
)
}}
q-item
q-item
blueprint-icon(icon='password', :hue-rotate='45')
blueprint-icon(icon='password', :hue-rotate='45')
q-item-section
q-item-section
q-item-label
{{
$
t
(
`admin.users.changePassword`
)
}}
q-item-label
{{
t
(
`admin.users.changePassword`
)
}}
q-item-label(caption)
{{
$
t
(
`admin.users.changePasswordHint`
)
}}
q-item-label(caption)
{{
t
(
`admin.users.changePasswordHint`
)
}}
q-item-label(caption): strong(:class='localAuth.password ? `text-positive` : `text-negative`')
{{
localAuth
.
password
?
$t
(
`admin.users.pwdSet`
)
:
$
t
(
`admin.users.pwdNotSet`
)
}}
q-item-label(caption): strong(:class='localAuth.password ? `text-positive` : `text-negative`')
{{
localAuth
.
password
?
t
(
`admin.users.pwdSet`
)
:
t
(
`admin.users.pwdNotSet`
)
}}
q-item-section(side)
q-item-section(side)
q-btn.acrylic-btn(
q-btn.acrylic-btn(
flat
flat
icon='las la-arrow-circle-right'
icon='las la-arrow-circle-right'
color='primary'
color='primary'
@click='changePassword'
@click='changePassword'
:label='
$
t(`common.actions.proceed`)'
:label='t(`common.actions.proceed`)'
)
)
q-separator.q-my-sm(inset)
q-separator.q-my-sm(inset)
q-item(tag='label', v-ripple)
q-item(tag='label', v-ripple)
blueprint-icon(icon='password-reset')
blueprint-icon(icon='password-reset')
q-item-section
q-item-section
q-item-label
{{
$
t
(
`admin.users.mustChangePwd`
)
}}
q-item-label
{{
t
(
`admin.users.mustChangePwd`
)
}}
q-item-label(caption)
{{
$
t
(
`admin.users.mustChangePwdHint`
)
}}
q-item-label(caption)
{{
t
(
`admin.users.mustChangePwdHint`
)
}}
q-item-section(avatar)
q-item-section(avatar)
q-toggle(
q-toggle(
v-model='localAuth.mustChangePwd'
v-model='localAuth.mustChangePwd'
color='primary'
color='primary'
checked-icon='las la-check'
checked-icon='las la-check'
unchecked-icon='las la-times'
unchecked-icon='las la-times'
:aria-label='
$
t(`admin.users.mustChangePwd`)'
:aria-label='t(`admin.users.mustChangePwd`)'
)
)
q-separator.q-my-sm(inset)
q-separator.q-my-sm(inset)
q-item(tag='label', v-ripple)
q-item(tag='label', v-ripple)
blueprint-icon(icon='key')
blueprint-icon(icon='key')
q-item-section
q-item-section
q-item-label
{{
$
t
(
`admin.users.pwdAuthRestrict`
)
}}
q-item-label
{{
t
(
`admin.users.pwdAuthRestrict`
)
}}
q-item-label(caption)
{{
$
t
(
`admin.users.pwdAuthRestrictHint`
)
}}
q-item-label(caption)
{{
t
(
`admin.users.pwdAuthRestrictHint`
)
}}
q-item-section(avatar)
q-item-section(avatar)
q-toggle(
q-toggle(
v-model='localAuth.restrictLogin'
v-model='localAuth.restrictLogin'
color='primary'
color='primary'
checked-icon='las la-check'
checked-icon='las la-check'
unchecked-icon='las la-times'
unchecked-icon='las la-times'
:aria-label='
$
t(`admin.users.pwdAuthRestrict`)'
:aria-label='t(`admin.users.pwdAuthRestrict`)'
)
)
q-card.shadow-1.q-pb-sm.q-mt-md
q-card.shadow-1.q-pb-sm.q-mt-md
q-card-section
q-card-section
.text-subtitle1
{{
$
t
(
'admin.users.tfa'
)
}}
.text-subtitle1
{{
t
(
'admin.users.tfa'
)
}}
q-item(tag='label', v-ripple)
q-item(tag='label', v-ripple)
blueprint-icon(icon='key')
blueprint-icon(icon='key')
q-item-section
q-item-section
q-item-label
{{
$
t
(
`admin.users.tfaRequired`
)
}}
q-item-label
{{
t
(
`admin.users.tfaRequired`
)
}}
q-item-label(caption)
{{
$
t
(
`admin.users.tfaRequiredHint`
)
}}
q-item-label(caption)
{{
t
(
`admin.users.tfaRequiredHint`
)
}}
q-item-section(avatar)
q-item-section(avatar)
q-toggle(
q-toggle(
v-model='localAuth.tfaRequired'
v-model='localAuth.tfaRequired'
color='primary'
color='primary'
checked-icon='las la-check'
checked-icon='las la-check'
unchecked-icon='las la-times'
unchecked-icon='las la-times'
:aria-label='
$
t(`admin.users.tfaRequired`)'
:aria-label='t(`admin.users.tfaRequired`)'
)
)
q-separator.q-my-sm(inset)
q-separator.q-my-sm(inset)
q-item
q-item
blueprint-icon(icon='password', :hue-rotate='45')
blueprint-icon(icon='password', :hue-rotate='45')
q-item-section
q-item-section
q-item-label
{{
$
t
(
`admin.users.tfaInvalidate`
)
}}
q-item-label
{{
t
(
`admin.users.tfaInvalidate`
)
}}
q-item-label(caption)
{{
$
t
(
`admin.users.tfaInvalidateHint`
)
}}
q-item-label(caption)
{{
t
(
`admin.users.tfaInvalidateHint`
)
}}
q-item-label(caption): strong(:class='localAuth.tfaSecret ? `text-positive` : `text-negative`')
{{
localAuth
.
tfaSecret
?
$t
(
`admin.users.tfaSet`
)
:
$
t
(
`admin.users.tfaNotSet`
)
}}
q-item-label(caption): strong(:class='localAuth.tfaSecret ? `text-positive` : `text-negative`')
{{
localAuth
.
tfaSecret
?
t
(
`admin.users.tfaSet`
)
:
t
(
`admin.users.tfaNotSet`
)
}}
q-item-section(side)
q-item-section(side)
q-btn.acrylic-btn(
q-btn.acrylic-btn(
flat
flat
icon='las la-arrow-circle-right'
icon='las la-arrow-circle-right'
color='primary'
color='primary'
@click='invalidateTFA'
@click='invalidateTFA'
:label='
$
t(`common.actions.proceed`)'
:label='t(`common.actions.proceed`)'
)
)
.col-12.col-lg-5
.col-12.col-lg-5
q-card.shadow-1.q-pb-sm
q-card.shadow-1.q-pb-sm
q-card-section
q-card-section
.text-subtitle1
{{
$
t
(
'admin.users.linkedProviders'
)
}}
.text-subtitle1
{{
t
(
'admin.users.linkedProviders'
)
}}
q-banner.q-mt-md(
q-banner.q-mt-md(
v-if='linkedAuthProviders.length < 1'
v-if='linkedAuthProviders.length < 1'
rounded
rounded
:class='$q.dark.isActive ? `bg-negative text-white` : `bg-grey-2 text-grey-7`'
:class='$q.dark.isActive ? `bg-negative text-white` : `bg-grey-2 text-grey-7`'
)
{{
$
t
(
'admin.users.noLinkedProviders'
)
}}
)
{{
t
(
'admin.users.noLinkedProviders'
)
}}
template(
template(
v-for='(prv, idx) in linkedAuthProviders'
v-for='(prv, idx) in linkedAuthProviders'
:key='prv._id'
:key='prv._id'
...
@@ -339,15 +339,15 @@ q-layout(view='hHh lpR fFf', container)
...
@@ -339,15 +339,15 @@ q-layout(view='hHh lpR fFf', container)
q-item-label
{{
prv
.
_moduleName
}}
q-item-label
{{
prv
.
_moduleName
}}
q-item-label(caption)
{{
prv
.
key
}}
q-item-label(caption)
{{
prv
.
key
}}
q-page(v-else-if='
$
route.params.section === `groups`')
q-page(v-else-if='route.params.section === `groups`')
.q-pa-md
.q-pa-md
.row.q-col-gutter-md
.row.q-col-gutter-md
.col-12.col-lg-8
.col-12.col-lg-8
q-card.shadow-1.q-pb-sm
q-card.shadow-1.q-pb-sm
q-card-section
q-card-section
.text-subtitle1
{{
$
t
(
'admin.users.groups'
)
}}
.text-subtitle1
{{
t
(
'admin.users.groups'
)
}}
template(
template(
v-for='(grp, idx) of user.groups'
v-for='(grp, idx) of
state.
user.groups'
:key='grp.id'
:key='grp.id'
)
)
q-separator.q-my-sm(inset, v-if='idx > 0')
q-separator.q-my-sm(inset, v-if='idx > 0')
...
@@ -361,17 +361,17 @@ q-layout(view='hHh lpR fFf', container)
...
@@ -361,17 +361,17 @@ q-layout(view='hHh lpR fFf', container)
icon='las la-times'
icon='las la-times'
color='accent'
color='accent'
@click='unassignGroup(grp.id)'
@click='unassignGroup(grp.id)'
:aria-label='
$
t(`admin.users.unassignGroup`)'
:aria-label='t(`admin.users.unassignGroup`)'
)
)
q-tooltip(anchor='center left' self='center right')
{{
$
t
(
'admin.users.unassignGroup'
)
}}
q-tooltip(anchor='center left' self='center right')
{{
t
(
'admin.users.unassignGroup'
)
}}
q-card.shadow-1.q-py-sm.q-mt-md
q-card.shadow-1.q-py-sm.q-mt-md
q-item
q-item
blueprint-icon(icon='join')
blueprint-icon(icon='join')
q-item-section
q-item-section
q-select(
q-select(
outlined
outlined
:options='groups'
:options='
state.
groups'
v-model='groupToAdd'
v-model='
state.
groupToAdd'
map-options
map-options
emit-value
emit-value
option-value='id'
option-value='id'
...
@@ -379,33 +379,33 @@ q-layout(view='hHh lpR fFf', container)
...
@@ -379,33 +379,33 @@ q-layout(view='hHh lpR fFf', container)
options-dense
options-dense
dense
dense
hide-bottom-space
hide-bottom-space
:label='
$
t(`admin.users.groups`)'
:label='t(`admin.users.groups`)'
:aria-label='
$
t(`admin.users.groups`)'
:aria-label='t(`admin.users.groups`)'
:loading='loading > 0'
:loading='
state.
loading > 0'
)
)
q-item-section(side)
q-item-section(side)
q-btn(
q-btn(
unelevated
unelevated
icon='las la-plus'
icon='las la-plus'
:label='
$
t(`admin.users.assignGroup`)'
:label='t(`admin.users.assignGroup`)'
color='primary'
color='primary'
@click='assignGroup'
@click='assignGroup'
)
)
q-page(v-else-if='
$
route.params.section === `metadata`')
q-page(v-else-if='route.params.section === `metadata`')
.q-pa-md
.q-pa-md
.row.q-col-gutter-md
.row.q-col-gutter-md
.col-12.col-lg-8
.col-12.col-lg-8
q-card.shadow-1.q-pb-sm
q-card.shadow-1.q-pb-sm
q-card-section.flex.items-center
q-card-section.flex.items-center
.text-subtitle1
{{
$
t
(
'admin.users.metadata'
)
}}
.text-subtitle1
{{
t
(
'admin.users.metadata'
)
}}
q-space
q-space
q-badge(
q-badge(
v-if='metadataInvalidJSON'
v-if='
state.
metadataInvalidJSON'
color='negative'
color='negative'
)
)
q-icon.q-mr-xs(name='las la-exclamation-triangle', size='20px')
q-icon.q-mr-xs(name='las la-exclamation-triangle', size='20px')
span
{{
$
t
(
'admin.users.invalidJSON'
)
}}
span
{{
t
(
'admin.users.invalidJSON'
)
}}
q-badge.q-py-xs(
q-badge.q-py-xs(
v-else
v-else
label='JSON'
label='JSON'
...
@@ -413,80 +413,79 @@ q-layout(view='hHh lpR fFf', container)
...
@@ -413,80 +413,79 @@ q-layout(view='hHh lpR fFf', container)
)
)
q-item
q-item
q-item-section
q-item-section
q-no-ssr(:placeholder='
$
t(`common.loading`)')
q-no-ssr(:placeholder='t(`common.loading`)')
util-code-editor.admin-theme-cm
(
codemirror.metadata-codemirror
(
v-model='metadata'
v-model='metadata'
language='json'
:extensions='[json()]'
:min-height='500'
)
)
q-page(v-else-if='
$
route.params.section === `operations`')
q-page(v-else-if='route.params.section === `operations`')
.q-pa-md
.q-pa-md
.row.q-col-gutter-md
.row.q-col-gutter-md
.col-12.col-lg-8
.col-12.col-lg-8
q-card.shadow-1.q-pb-sm
q-card.shadow-1.q-pb-sm
q-card-section
q-card-section
.text-subtitle1
{{
$
t
(
'admin.users.operations'
)
}}
.text-subtitle1
{{
t
(
'admin.users.operations'
)
}}
q-item
q-item
blueprint-icon(icon='email-open', :hue-rotate='45')
blueprint-icon(icon='email-open', :hue-rotate='45')
q-item-section
q-item-section
q-item-label
{{
$
t
(
`admin.users.sendWelcomeEmail`
)
}}
q-item-label
{{
t
(
`admin.users.sendWelcomeEmail`
)
}}
q-item-label(caption)
{{
$
t
(
`admin.users.sendWelcomeEmailAltHint`
)
}}
q-item-label(caption)
{{
t
(
`admin.users.sendWelcomeEmailAltHint`
)
}}
q-item-section(side)
q-item-section(side)
q-btn.acrylic-btn(
q-btn.acrylic-btn(
flat
flat
icon='las la-arrow-circle-right'
icon='las la-arrow-circle-right'
color='primary'
color='primary'
@click='sendWelcomeEmail'
@click='sendWelcomeEmail'
:label='
$
t(`common.actions.proceed`)'
:label='t(`common.actions.proceed`)'
)
)
q-separator.q-my-sm(inset)
q-separator.q-my-sm(inset)
q-item
q-item
blueprint-icon(icon='apply', :hue-rotate='45')
blueprint-icon(icon='apply', :hue-rotate='45')
q-item-section
q-item-section
q-item-label
{{
user
.
isVerified
?
$t
(
`admin.users.unverify`
)
:
$
t
(
`admin.users.verify`
)
}}
q-item-label
{{
state
.
user
.
isVerified
?
t
(
`admin.users.unverify`
)
:
t
(
`admin.users.verify`
)
}}
q-item-label(caption)
{{
user
.
isVerified
?
$t
(
`admin.users.unverifyHint`
)
:
$
t
(
`admin.users.verifyHint`
)
}}
q-item-label(caption)
{{
state
.
user
.
isVerified
?
t
(
`admin.users.unverifyHint`
)
:
t
(
`admin.users.verifyHint`
)
}}
q-item-label(caption): strong(:class='
user.isVerified ? `text-positive` : `text-negative`')
{{
user
.
isVerified
?
$t
(
`admin.users.verified`
)
:
$
t
(
`admin.users.unverified`
)
}}
q-item-label(caption): strong(:class='
state.user.isVerified ? `text-positive` : `text-negative`')
{{
state
.
user
.
isVerified
?
t
(
`admin.users.verified`
)
:
t
(
`admin.users.unverified`
)
}}
q-item-section(side)
q-item-section(side)
q-btn.acrylic-btn(
q-btn.acrylic-btn(
flat
flat
icon='las la-arrow-circle-right'
icon='las la-arrow-circle-right'
color='primary'
color='primary'
@click='toggleVerified'
@click='toggleVerified'
:label='
$
t(`common.actions.proceed`)'
:label='t(`common.actions.proceed`)'
)
)
q-separator.q-my-sm(inset)
q-separator.q-my-sm(inset)
q-item
q-item
blueprint-icon(icon='unfriend', :hue-rotate='45')
blueprint-icon(icon='unfriend', :hue-rotate='45')
q-item-section
q-item-section
q-item-label
{{
user
.
isActive
?
$t
(
`admin.users.ban`
)
:
$
t
(
`admin.users.unban`
)
}}
q-item-label
{{
state
.
user
.
isActive
?
t
(
`admin.users.ban`
)
:
t
(
`admin.users.unban`
)
}}
q-item-label(caption)
{{
user
.
isActive
?
$t
(
`admin.users.banHint`
)
:
$
t
(
`admin.users.unbanHint`
)
}}
q-item-label(caption)
{{
state
.
user
.
isActive
?
t
(
`admin.users.banHint`
)
:
t
(
`admin.users.unbanHint`
)
}}
q-item-label(caption): strong(:class='
user.isActive ? `text-positive` : `text-negative`')
{{
user
.
isActive
?
$t
(
`admin.users.active`
)
:
$
t
(
`admin.users.banned`
)
}}
q-item-label(caption): strong(:class='
state.user.isActive ? `text-positive` : `text-negative`')
{{
state
.
user
.
isActive
?
t
(
`admin.users.active`
)
:
t
(
`admin.users.banned`
)
}}
q-item-section(side)
q-item-section(side)
q-btn.acrylic-btn(
q-btn.acrylic-btn(
flat
flat
icon='las la-arrow-circle-right'
icon='las la-arrow-circle-right'
color='primary'
color='primary'
@click='toggleBan'
@click='toggleBan'
:label='
$
t(`common.actions.proceed`)'
:label='t(`common.actions.proceed`)'
)
)
q-card.shadow-1.q-py-sm.q-mt-md
q-card.shadow-1.q-py-sm.q-mt-md
q-item
q-item
blueprint-icon(icon='denied', :hue-rotate='140')
blueprint-icon(icon='denied', :hue-rotate='140')
q-item-section
q-item-section
q-item-label
{{
$
t
(
`admin.users.delete`
)
}}
q-item-label
{{
t
(
`admin.users.delete`
)
}}
q-item-label(caption)
{{
$
t
(
`admin.users.deleteHint`
)
}}
q-item-label(caption)
{{
t
(
`admin.users.deleteHint`
)
}}
q-item-section(side)
q-item-section(side)
q-btn.acrylic-btn(
q-btn.acrylic-btn(
flat
flat
icon='las la-arrow-circle-right'
icon='las la-arrow-circle-right'
color='negative'
color='negative'
@click='deleteUser'
@click='deleteUser'
:label='
$
t(`common.actions.proceed`)'
:label='t(`common.actions.proceed`)'
)
)
</
template
>
</
template
>
<
script
>
<
script
setup
>
import
gql
from
'graphql-tag'
import
gql
from
'graphql-tag'
import
cloneDeep
from
'lodash/cloneDeep'
import
cloneDeep
from
'lodash/cloneDeep'
import
some
from
'lodash/some'
import
some
from
'lodash/some'
...
@@ -494,284 +493,339 @@ import find from 'lodash/find'
...
@@ -494,284 +493,339 @@ import find from 'lodash/find'
import
findKey
from
'lodash/findKey'
import
findKey
from
'lodash/findKey'
import
_get
from
'lodash/get'
import
_get
from
'lodash/get'
import
map
from
'lodash/map'
import
map
from
'lodash/map'
import
{
get
}
from
'vuex-pathify'
import
{
DateTime
}
from
'luxon'
import
{
DateTime
}
from
'luxon'
import
UtilCodeEditor
from
'./UtilCodeEditor.vue'
import
{
useI18n
}
from
'vue-i18n'
import
{
useQuasar
}
from
'quasar'
import
{
computed
,
onMounted
,
reactive
,
watch
}
from
'vue'
import
{
useRouter
,
useRoute
}
from
'vue-router'
import
{
useAdminStore
}
from
'src/stores/admin'
import
{
useDataStore
}
from
'src/stores/data'
import
UserChangePwdDialog
from
'./UserChangePwdDialog.vue'
import
UserChangePwdDialog
from
'./UserChangePwdDialog.vue'
import
{
Codemirror
}
from
'vue-codemirror'
import
{
json
}
from
'@codemirror/lang-json'
// import { oneDark } from '@codemirror/theme-one-dark'
// QUASAR
const
$q
=
useQuasar
()
// STORES
const
adminStore
=
useAdminStore
()
const
dataStore
=
useDataStore
()
export
default
{
// ROUTER
components
:
{
UtilCodeEditor
const
router
=
useRouter
()
const
route
=
useRoute
()
// I18N
const
{
t
}
=
useI18n
()
// DATA
const
state
=
reactive
({
invalidCharsRegex
:
/^
[^
<>"
]
+$/
,
user
:
{
meta
:
{},
prefs
:
{},
groups
:
[]
},
},
data
()
{
groups
:
[],
return
{
groupToAdd
:
null
,
invalidCharsRegex
:
/^
[^
<>"
]
+$/
,
loading
:
0
,
sections
:
[
metadataInvalidJSON
:
false
{
key
:
'overview'
,
text
:
this
.
$t
(
'admin.users.overview'
),
icon
:
'las la-user'
},
})
{
key
:
'activity'
,
text
:
this
.
$t
(
'admin.users.activity'
),
icon
:
'las la-chart-area'
},
{
key
:
'auth'
,
text
:
this
.
$t
(
'admin.users.auth'
),
icon
:
'las la-key'
},
const
sections
=
[
{
key
:
'groups'
,
text
:
this
.
$t
(
'admin.users.groups'
),
icon
:
'las la-users'
},
{
key
:
'overview'
,
text
:
t
(
'admin.users.overview'
),
icon
:
'las la-user'
},
{
key
:
'metadata'
,
text
:
this
.
$t
(
'admin.users.metadata'
),
icon
:
'las la-clipboard-list'
},
{
key
:
'activity'
,
text
:
t
(
'admin.users.activity'
),
icon
:
'las la-chart-area'
},
{
key
:
'operations'
,
text
:
this
.
$t
(
'admin.users.operations'
),
icon
:
'las la-tools'
}
{
key
:
'auth'
,
text
:
t
(
'admin.users.auth'
),
icon
:
'las la-key'
},
],
{
key
:
'groups'
,
text
:
t
(
'admin.users.groups'
),
icon
:
'las la-users'
},
user
:
{
{
key
:
'metadata'
,
text
:
t
(
'admin.users.metadata'
),
icon
:
'las la-clipboard-list'
},
meta
:
{},
{
key
:
'operations'
,
text
:
t
(
'admin.users.operations'
),
icon
:
'las la-tools'
}
prefs
:
{},
]
groups
:
[]
},
// COMPUTED
groups
:
[],
groupToAdd
:
null
,
const
metadata
=
computed
({
loading
:
0
,
get
()
{
return
JSON
.
stringify
(
state
.
user
.
meta
??
{},
null
,
2
)
},
metadataInvalidJSON
:
false
set
(
val
)
{
try
{
state
.
user
.
meta
=
JSON
.
parse
(
val
)
state
.
metadataInvalidJSON
=
false
}
catch
(
err
)
{
state
.
metadataInvalidJSON
=
true
}
}
}
})
const
localAuthId
=
computed
(()
=>
{
return
findKey
(
state
.
user
.
auth
,
[
'module'
,
'local'
])
})
const
localAuth
=
computed
({
get
()
{
return
localAuthId
.
value
?
_get
(
state
.
user
.
auth
,
localAuthId
.
value
,
{})
:
{}
},
},
computed
:
{
set
(
val
)
{
timezones
:
get
(
'data/timezones'
,
false
),
if
(
localAuthId
.
value
)
{
userId
:
get
(
'admin/overlayOpts@id'
,
false
),
state
.
user
.
auth
[
localAuthId
.
value
]
=
val
metadata
:
{
get
()
{
return
JSON
.
stringify
(
this
.
user
.
meta
??
{},
null
,
2
)
},
set
(
val
)
{
try
{
this
.
user
.
meta
=
JSON
.
parse
(
val
)
this
.
metadataInvalidJSON
=
false
}
catch
(
err
)
{
this
.
metadataInvalidJSON
=
true
}
}
},
localAuthId
()
{
return
findKey
(
this
.
user
.
auth
,
[
'module'
,
'local'
])
},
localAuth
:
{
get
()
{
return
this
.
localAuthId
?
_get
(
this
.
user
.
auth
,
this
.
localAuthId
,
{})
:
{}
},
set
(
val
)
{
if
(
this
.
localAuthId
)
{
this
.
user
.
auth
[
this
.
localAuthId
]
=
val
}
}
},
linkedAuthProviders
()
{
if
(
!
this
.
user
?.
auth
)
{
return
[]
}
return
map
(
this
.
user
.
auth
,
(
obj
,
key
)
=>
{
return
{
...
obj
,
_id
:
key
}
}).
filter
(
prv
=>
prv
.
module
!==
'local'
)
}
}
},
}
watch
:
{
})
$route
:
'checkRoute'
},
const
linkedAuthProviders
=
computed
(()
=>
{
mounted
()
{
if
(
!
state
.
user
?.
auth
)
{
return
[]
}
this
.
checkRoute
()
this
.
load
()
return
map
(
state
.
user
.
auth
,
(
obj
,
key
)
=>
{
},
return
{
methods
:
{
...
obj
,
async
load
()
{
_id
:
key
this
.
loading
++
}
this
.
$q
.
loading
.
show
()
}).
filter
(
prv
=>
prv
.
module
!==
'local'
)
try
{
})
const
resp
=
await
this
.
$apollo
.
query
({
query
:
gql
`
// WATCHERS
query adminFetchUser (
$id: UUID!
watch
(()
=>
route
.
params
.
section
,
checkRoute
)
) {
groups {
// METHODS
id
name
async
function
fetchUser
()
{
}
state
.
loading
++
userById(
$q
.
loading
.
show
()
id: $id
try
{
) {
const
resp
=
await
APOLLO_CLIENT
.
query
({
id
query
:
gql
`
email
query adminFetchUser (
name
$id: UUID!
isSystem
) {
isVerified
groups {
isActive
id
auth
name
meta
}
prefs
userById(
lastLoginAt
id: $id
createdAt
) {
updatedAt
id
groups {
email
id
name
name
isSystem
}
isVerified
}
isActive
auth
meta
prefs
lastLoginAt
createdAt
updatedAt
groups {
id
name
}
}
`
,
}
variables
:
{
id
:
this
.
userId
},
fetchPolicy
:
'network-only'
})
this
.
groups
=
resp
?.
data
?.
groups
?.
filter
(
g
=>
g
.
id
!==
'10000000-0000-4000-0000-000000000001'
)
??
[]
if
(
resp
?.
data
?.
userById
)
{
this
.
user
=
cloneDeep
(
resp
.
data
.
userById
)
}
else
{
throw
new
Error
(
'An unexpected error occured while fetching user details.'
)
}
}
catch
(
err
)
{
this
.
$q
.
notify
({
type
:
'negative'
,
message
:
err
.
message
})
}
this
.
$q
.
loading
.
hide
()
this
.
loading
--
},
close
()
{
this
.
$store
.
set
(
'admin/overlay'
,
''
)
},
checkRoute
()
{
if
(
!
this
.
$route
.
params
.
section
)
{
this
.
$router
.
replace
({
params
:
{
section
:
'overview'
}
})
}
if
(
this
.
$route
.
params
.
section
===
'metadata'
)
{
this
.
metadataInvalidJSON
=
false
}
},
humanizeDate
(
val
)
{
if
(
!
val
)
{
return
'---'
}
return
DateTime
.
fromISO
(
val
).
toLocaleString
(
DateTime
.
DATETIME_FULL
)
},
assignGroup
()
{
if
(
!
this
.
groupToAdd
)
{
this
.
$q
.
notify
({
type
:
'negative'
,
message
:
this
.
$t
(
'admin.users.noGroupSelected'
)
})
}
else
if
(
some
(
this
.
user
.
groups
,
gr
=>
gr
.
id
===
this
.
groupToAdd
))
{
this
.
$q
.
notify
({
type
:
'warning'
,
message
:
this
.
$t
(
'admin.users.groupAlreadyAssigned'
)
})
}
else
{
const
newGroup
=
find
(
this
.
groups
,
[
'id'
,
this
.
groupToAdd
])
this
.
user
.
groups
=
[...
this
.
user
.
groups
,
newGroup
]
}
},
unassignGroup
(
id
)
{
if
(
this
.
user
.
groups
.
length
<=
1
)
{
this
.
$q
.
notify
({
type
:
'negative'
,
message
:
this
.
$t
(
'admin.users.minimumGroupRequired'
)
})
}
else
{
this
.
user
.
groups
=
this
.
user
.
groups
.
filter
(
gr
=>
gr
.
id
===
id
)
}
},
async
save
(
patch
,
{
silent
,
keepOpen
}
=
{
silent
:
false
,
keepOpen
:
false
})
{
this
.
$q
.
loading
.
show
()
if
(
!
patch
)
{
patch
=
{
name
:
this
.
user
.
name
,
email
:
this
.
user
.
email
,
isVerified
:
this
.
user
.
isVerified
,
isActive
:
this
.
user
.
isActive
,
meta
:
this
.
user
.
meta
,
prefs
:
this
.
user
.
prefs
,
groups
:
this
.
user
.
groups
.
map
(
gr
=>
gr
.
id
)
}
}
}
`
,
try
{
variables
:
{
const
resp
=
await
this
.
$apollo
.
mutate
({
id
:
adminStore
.
overlayOpts
.
id
mutation
:
gql
`
},
mutation adminSaveUser (
fetchPolicy
:
'network-only'
$id: UUID!
})
$patch: UserUpdateInput!
state
.
groups
=
resp
?.
data
?.
groups
?.
filter
(
g
=>
g
.
id
!==
'10000000-0000-4000-0000-000000000001'
)
??
[]
) {
if
(
resp
?.
data
?.
userById
)
{
updateUser (
state
.
user
=
cloneDeep
(
resp
.
data
.
userById
)
id: $id
}
else
{
patch: $patch
throw
new
Error
(
'An unexpected error occured while fetching user details.'
)
) {
}
status {
}
catch
(
err
)
{
succeeded
$q
.
notify
({
message
type
:
'negative'
,
}
message
:
err
.
message
}
})
}
$q
.
loading
.
hide
()
state
.
loading
--
}
function
close
()
{
adminStore
.
$patch
({
overlay
:
''
})
}
function
checkRoute
()
{
if
(
!
route
.
params
.
section
)
{
router
.
replace
({
params
:
{
section
:
'overview'
}
})
}
if
(
route
.
params
.
section
===
'metadata'
)
{
state
.
metadataInvalidJSON
=
false
}
}
function
humanizeDate
(
val
)
{
if
(
!
val
)
{
return
'---'
}
return
DateTime
.
fromISO
(
val
).
toLocaleString
(
DateTime
.
DATETIME_FULL
)
}
function
assignGroup
()
{
if
(
!
state
.
groupToAdd
)
{
$q
.
notify
({
type
:
'negative'
,
message
:
t
(
'admin.users.noGroupSelected'
)
})
}
else
if
(
some
(
state
.
user
.
groups
,
gr
=>
gr
.
id
===
state
.
groupToAdd
))
{
$q
.
notify
({
type
:
'warning'
,
message
:
t
(
'admin.users.groupAlreadyAssigned'
)
})
}
else
{
const
newGroup
=
find
(
state
.
groups
,
[
'id'
,
state
.
groupToAdd
])
state
.
user
.
groups
=
[...
state
.
user
.
groups
,
newGroup
]
}
}
function
unassignGroup
(
id
)
{
if
(
state
.
user
.
groups
.
length
<=
1
)
{
$q
.
notify
({
type
:
'negative'
,
message
:
t
(
'admin.users.minimumGroupRequired'
)
})
}
else
{
state
.
user
.
groups
=
state
.
user
.
groups
.
filter
(
gr
=>
gr
.
id
===
id
)
}
}
async
function
save
(
patch
,
{
silent
,
keepOpen
}
=
{
silent
:
false
,
keepOpen
:
false
})
{
$q
.
loading
.
show
()
if
(
!
patch
)
{
patch
=
{
name
:
state
.
user
.
name
,
email
:
state
.
user
.
email
,
isVerified
:
state
.
user
.
isVerified
,
isActive
:
state
.
user
.
isActive
,
meta
:
state
.
user
.
meta
,
prefs
:
state
.
user
.
prefs
,
groups
:
state
.
user
.
groups
.
map
(
gr
=>
gr
.
id
)
}
}
try
{
const
resp
=
await
APOLLO_CLIENT
.
mutate
({
mutation
:
gql
`
mutation adminSaveUser (
$id: UUID!
$patch: UserUpdateInput!
) {
updateUser (
id: $id
patch: $patch
) {
operation {
succeeded
message
}
}
`
,
variables
:
{
id
:
this
.
userId
,
patch
}
}
})
if
(
resp
?.
data
?.
updateUser
?.
status
?.
succeeded
)
{
if
(
!
silent
)
{
this
.
$q
.
notify
({
type
:
'positive'
,
message
:
this
.
$t
(
'admin.users.saveSuccess'
)
})
}
if
(
!
keepOpen
)
{
this
.
close
()
}
}
else
{
throw
new
Error
(
resp
?.
data
?.
updateUser
?.
status
?.
message
||
'An unexpected error occured.'
)
}
}
}
catch
(
err
)
{
`
,
this
.
$q
.
notify
({
variables
:
{
type
:
'negative'
,
id
:
adminStore
.
overlayOpts
.
id
,
message
:
err
.
message
patch
})
}
}
this
.
$q
.
loading
.
hide
()
})
},
if
(
resp
?.
data
?.
updateUser
?.
operation
?.
succeeded
)
{
changePassword
()
{
if
(
!
silent
)
{
this
.
$q
.
dialog
({
$q
.
notify
({
component
:
UserChangePwdDialog
,
componentProps
:
{
userId
:
this
.
userId
}
}).
onOk
(({
mustChangePassword
})
=>
{
this
.
localAuth
=
{
...
this
.
localAuth
,
mustChangePwd
:
mustChangePassword
}
})
},
invalidateTFA
()
{
this
.
$q
.
dialog
({
title
:
this
.
$t
(
'admin.users.tfaInvalidate'
),
message
:
this
.
$t
(
'admin.users.tfaInvalidateConfirm'
),
cancel
:
true
,
persistent
:
true
,
ok
:
{
label
:
this
.
$t
(
'common.actions.confirm'
)
}
}).
onOk
(()
=>
{
this
.
localAuth
.
tfaSecret
=
''
this
.
$q
.
notify
({
type
:
'positive'
,
type
:
'positive'
,
message
:
t
his
.
$t
(
'admin.users.tfaInvalidat
eSuccess'
)
message
:
t
(
'admin.users.sav
eSuccess'
)
})
})
})
}
},
if
(
!
keepOpen
)
{
async
sendWelcomeEmail
()
{
close
()
}
},
}
else
{
toggleVerified
()
{
throw
new
Error
(
resp
?.
data
?.
updateUser
?.
operation
?.
message
||
'An unexpected error occured.'
)
this
.
user
.
isVerified
=
!
this
.
user
.
isVerified
this
.
save
({
isVerified
:
this
.
user
.
isVerified
},
{
silent
:
true
,
keepOpen
:
true
})
},
toggleBan
()
{
this
.
user
.
isActive
=
!
this
.
user
.
isActive
this
.
save
({
isActive
:
this
.
user
.
isActive
},
{
silent
:
true
,
keepOpen
:
true
})
},
async
deleteUser
()
{
}
}
}
catch
(
err
)
{
$q
.
notify
({
type
:
'negative'
,
message
:
err
.
message
})
}
}
$q
.
loading
.
hide
()
}
function
changePassword
()
{
$q
.
dialog
({
component
:
UserChangePwdDialog
,
componentProps
:
{
userId
:
adminStore
.
overlayOpts
.
id
}
}).
onOk
(({
mustChangePassword
})
=>
{
localAuth
.
value
=
{
...
localAuth
.
value
,
mustChangePwd
:
mustChangePassword
}
})
}
function
invalidateTFA
()
{
$q
.
dialog
({
title
:
t
(
'admin.users.tfaInvalidate'
),
message
:
t
(
'admin.users.tfaInvalidateConfirm'
),
cancel
:
true
,
persistent
:
true
,
ok
:
{
label
:
t
(
'common.actions.confirm'
)
}
}).
onOk
(()
=>
{
localAuth
.
value
.
tfaSecret
=
''
$q
.
notify
({
type
:
'positive'
,
message
:
t
(
'admin.users.tfaInvalidateSuccess'
)
})
})
}
async
function
sendWelcomeEmail
()
{
}
}
function
toggleVerified
()
{
state
.
user
.
isVerified
=
!
state
.
user
.
isVerified
save
({
isVerified
:
state
.
user
.
isVerified
},
{
silent
:
true
,
keepOpen
:
true
})
}
function
toggleBan
()
{
state
.
user
.
isActive
=
!
state
.
user
.
isActive
save
({
isActive
:
state
.
user
.
isActive
},
{
silent
:
true
,
keepOpen
:
true
})
}
async
function
deleteUser
()
{
}
// MOUNTED
onMounted
(()
=>
{
checkRoute
()
fetchUser
()
})
</
script
>
</
script
>
<
style
lang=
"scss"
scoped
>
.metadata-codemirror
{
&
:deep
(
.cm-editor
)
{
height
:
150px
;
min-height
:
100px
;
border-radius
:
5px
;
border
:
1px
solid
#CCC
;
}
}
</
style
>
ux/src/pages/AdminGroups.vue
View file @
47ed7b37
...
@@ -168,6 +168,8 @@ const headers = [
...
@@ -168,6 +168,8 @@ const headers = [
}
}
]
]
// WATCHERS
watch
(()
=>
adminStore
.
overlay
,
(
newValue
,
oldValue
)
=>
{
watch
(()
=>
adminStore
.
overlay
,
(
newValue
,
oldValue
)
=>
{
if
(
newValue
===
''
&&
oldValue
===
'GroupEditOverlay'
)
{
if
(
newValue
===
''
&&
oldValue
===
'GroupEditOverlay'
)
{
router
.
push
(
'/_admin/groups'
)
router
.
push
(
'/_admin/groups'
)
...
@@ -175,9 +177,7 @@ watch(() => adminStore.overlay, (newValue, oldValue) => {
...
@@ -175,9 +177,7 @@ watch(() => adminStore.overlay, (newValue, oldValue) => {
}
}
}
)
}
)
watch
(()
=>
route
.
params
.
id
,
()
=>
{
watch
(()
=>
route
.
params
.
id
,
checkOverlay
)
checkOverlay
()
}
)
// METHODS
// METHODS
...
...
ux/src/pages/AdminUsers.vue
View file @
47ed7b37
...
@@ -4,12 +4,12 @@ q-page.admin-groups
...
@@ -4,12 +4,12 @@ q-page.admin-groups
.col-auto
.col-auto
img.admin-icon.animated.fadeInLeft(src='/_assets/icons/fluent-account.svg')
img.admin-icon.animated.fadeInLeft(src='/_assets/icons/fluent-account.svg')
.col.q-pl-md
.col.q-pl-md
.text-h5.text-primary.animated.fadeInLeft
{{
$
t
(
'admin.users.title'
)
}}
.text-h5.text-primary.animated.fadeInLeft
{{
t
(
'admin.users.title'
)
}}
.text-subtitle1.text-grey.animated.fadeInLeft.wait-p2s
{{
$
t
(
'admin.users.subtitle'
)
}}
.text-subtitle1.text-grey.animated.fadeInLeft.wait-p2s
{{
t
(
'admin.users.subtitle'
)
}}
.col-auto.flex.items-center
.col-auto.flex.items-center
q-input.denser.q-mr-sm(
q-input.denser.q-mr-sm(
outlined
outlined
v-model='search'
v-model='s
tate.s
earch'
dense
dense
:class='$q.dark.isActive ? `bg-dark` : `bg-white`'
:class='$q.dark.isActive ? `bg-dark` : `bg-white`'
)
)
...
@@ -28,29 +28,29 @@ q-page.admin-groups
...
@@ -28,29 +28,29 @@ q-page.admin-groups
flat
flat
color='secondary'
color='secondary'
@click='load'
@click='load'
:loading='loading > 0'
:loading='
state.
loading > 0'
)
)
q-btn(
q-btn(
unelevated
unelevated
icon='las la-plus'
icon='las la-plus'
:label='
$
t(`admin.users.create`)'
:label='t(`admin.users.create`)'
color='primary'
color='primary'
@click='createUser'
@click='createUser'
:disabled='loading > 0'
:disabled='
state.
loading > 0'
)
)
q-separator(inset)
q-separator(inset)
.row.q-pa-md.q-col-gutter-md
.row.q-pa-md.q-col-gutter-md
.col-12
.col-12
q-card.shadow-1
q-card.shadow-1
q-table(
q-table(
:rows='users'
:rows='
state.
users'
:columns='headers'
:columns='headers'
row-key='id'
row-key='id'
flat
flat
hide-header
hide-header
hide-bottom
hide-bottom
:rows-per-page-options='[0]'
:rows-per-page-options='[0]'
:loading='loading > 0'
:loading='
state.
loading > 0'
)
)
template(v-slot:body-cell-id='props')
template(v-slot:body-cell-id='props')
q-td(:props='props')
q-td(:props='props')
...
@@ -92,7 +92,7 @@ q-page.admin-groups
...
@@ -92,7 +92,7 @@ q-page.admin-groups
:to='`/_admin/users/` + props.row.id'
:to='`/_admin/users/` + props.row.id'
icon='las la-pen'
icon='las la-pen'
color='indigo'
color='indigo'
:label='
$
t(`common.actions.edit`)'
:label='t(`common.actions.edit`)'
no-caps
no-caps
)
)
q-btn.acrylic-btn(
q-btn.acrylic-btn(
...
@@ -100,146 +100,178 @@ q-page.admin-groups
...
@@ -100,146 +100,178 @@ q-page.admin-groups
flat
flat
icon='las la-trash'
icon='las la-trash'
color='accent'
color='accent'
@click='delete
Group
(props.row)'
@click='delete
User
(props.row)'
)
)
</
template
>
</
template
>
<
script
>
<
script
setup
>
import
gql
from
'graphql-tag'
import
gql
from
'graphql-tag'
import
cloneDeep
from
'lodash/cloneDeep'
import
cloneDeep
from
'lodash/cloneDeep'
import
{
DateTime
}
from
'luxon'
import
{
DateTime
}
from
'luxon'
import
{
sync
}
from
'vuex-pathify'
import
{
useI18n
}
from
'vue-i18n'
import
{
createMetaMixin
}
from
'quasar'
import
{
useMeta
,
useQuasar
}
from
'quasar'
import
{
onBeforeUnmount
,
onMounted
,
reactive
,
watch
}
from
'vue'
import
{
useRouter
,
useRoute
}
from
'vue-router'
import
{
useAdminStore
}
from
'src/stores/admin'
import
UserCreateDialog
from
'../components/UserCreateDialog.vue'
import
UserCreateDialog
from
'../components/UserCreateDialog.vue'
export
default
{
// QUASAR
mixins
:
[
createMetaMixin
(
function
()
{
const
$q
=
useQuasar
()
return
{
title
:
this
.
$t
(
'admin.users.title'
)
// STORES
}
})
const
adminStore
=
useAdminStore
()
],
data
()
{
// ROUTER
return
{
users
:
[],
const
router
=
useRouter
()
loading
:
0
,
const
route
=
useRoute
()
search
:
''
}
// I18N
},
computed
:
{
const
{
t
}
=
useI18n
()
overlay
:
sync
(
'admin/overlay'
,
false
),
headers
()
{
// META
return
[
{
useMeta
({
align
:
'center'
,
title
:
t
(
'admin.users.title'
)
field
:
'id'
,
})
name
:
'id'
,
sortable
:
false
,
// DATA
style
:
'width: 20px'
},
const
state
=
reactive
({
{
users
:
[],
label
:
this
.
$t
(
'common.field.name'
),
loading
:
0
,
align
:
'left'
,
search
:
''
field
:
'name'
,
})
name
:
'name'
,
sortable
:
true
const
headers
=
[
},
{
{
align
:
'center'
,
label
:
this
.
$t
(
'admin.users.email'
),
field
:
'id'
,
align
:
'left'
,
name
:
'id'
,
field
:
'email'
,
sortable
:
false
,
name
:
'email'
,
style
:
'width: 20px'
sortable
:
false
},
{
align
:
'left'
,
field
:
'createdAt'
,
name
:
'date'
,
sortable
:
false
},
{
label
:
''
,
align
:
'right'
,
field
:
'edit'
,
name
:
'edit'
,
sortable
:
false
,
style
:
'width: 250px'
}
]
}
},
},
watch
:
{
{
overlay
(
newValue
,
oldValue
)
{
label
:
t
(
'common.field.name'
),
if
(
newValue
===
''
&&
oldValue
===
'UserEditOverlay'
)
{
align
:
'left'
,
this
.
$router
.
push
(
'/_admin/users'
)
field
:
'name'
,
this
.
load
()
name
:
'name'
,
}
sortable
:
true
},
$route
:
'checkOverlay'
},
},
mounted
()
{
{
this
.
checkOverlay
()
label
:
t
(
'admin.users.email'
),
this
.
load
()
align
:
'left'
,
field
:
'email'
,
name
:
'email'
,
sortable
:
false
},
},
beforeUnmount
()
{
{
this
.
overlay
=
''
align
:
'left'
,
field
:
'createdAt'
,
name
:
'date'
,
sortable
:
false
},
},
methods
:
{
{
async
load
()
{
label
:
''
,
this
.
loading
++
align
:
'right'
,
this
.
$q
.
loading
.
show
()
field
:
'edit'
,
const
resp
=
await
this
.
$apollo
.
query
({
name
:
'edit'
,
query
:
gql
`
sortable
:
false
,
query getUsers {
style
:
'width: 250px'
users {
}
id
]
name
email
// WATCHERS
isSystem
isActive
watch
(()
=>
adminStore
.
overlay
,
(
newValue
,
oldValue
)
=>
{
createdAt
if
(
newValue
===
''
&&
oldValue
===
'UserEditOverlay'
)
{
lastLoginAt
router
.
push
(
'/_admin/users'
)
}
load
()
}
}
`
,
})
fetchPolicy
:
'network-only'
})
watch
(()
=>
route
.
params
.
id
,
checkOverlay
)
this
.
users
=
cloneDeep
(
resp
?.
data
?.
users
)
this
.
$q
.
loading
.
hide
()
// METHODS
this
.
loading
--
},
async
function
load
()
{
humanizeDate
(
val
)
{
state
.
loading
++
return
DateTime
.
fromISO
(
val
).
toRelative
()
$q
.
loading
.
show
()
},
const
resp
=
await
APOLLO_CLIENT
.
query
({
checkOverlay
()
{
query
:
gql
`
if
(
this
.
$route
.
params
&&
this
.
$route
.
params
.
id
)
{
query getUsers {
this
.
$store
.
set
(
'admin/overlayOpts'
,
{
id
:
this
.
$route
.
params
.
id
})
users {
this
.
$store
.
set
(
'admin/overlay'
,
'UserEditOverlay'
)
id
}
else
{
name
this
.
$store
.
set
(
'admin/overlay'
,
''
)
email
}
isSystem
},
isActive
createUser
()
{
createdAt
this
.
$q
.
dialog
({
lastLoginAt
component
:
UserCreateDialog
}).
onOk
(()
=>
{
this
.
load
()
})
},
deleteUser
(
gr
)
{
this
.
$q
.
dialog
({
// component: UserDeleteDialog,
componentProps
:
{
group
:
gr
}
}
}).
onOk
(()
=>
{
}
this
.
load
()
`
,
})
fetchPolicy
:
'network-only'
}
})
state
.
users
=
cloneDeep
(
resp
?.
data
?.
users
)
$q
.
loading
.
hide
()
state
.
loading
--
}
function
humanizeDate
(
val
)
{
return
DateTime
.
fromISO
(
val
).
toRelative
()
}
function
checkOverlay
()
{
if
(
route
.
params
?.
id
)
{
adminStore
.
$patch
({
overlayOpts
:
{
id
:
route
.
params
.
id
},
overlay
:
'UserEditOverlay'
})
}
else
{
adminStore
.
$patch
({
overlay
:
''
})
}
}
}
}
function
createUser
()
{
$q
.
dialog
({
component
:
UserCreateDialog
}).
onOk
(()
=>
{
this
.
load
()
})
}
function
deleteUser
(
usr
)
{
$q
.
dialog
({
// component: UserDeleteDialog,
componentProps
:
{
user
:
usr
}
}).
onOk
(
load
)
}
// MOUNTED
onMounted
(()
=>
{
checkOverlay
()
load
()
})
// BEFORE UNMOUNT
onBeforeUnmount
(()
=>
{
adminStore
.
$patch
({
overlay
:
''
})
})
</
script
>
</
script
>
<
style
lang=
'scss'
>
<
style
lang=
'scss'
>
...
...
ux/src/router/routes.js
View file @
47ed7b37
...
@@ -42,7 +42,7 @@ const routes = [
...
@@ -42,7 +42,7 @@ const routes = [
// -> Users
// -> Users
// { path: 'auth', component: () => import('../pages/AdminAuth.vue') },
// { path: 'auth', component: () => import('../pages/AdminAuth.vue') },
{
path
:
'groups/:id?/:section?'
,
component
:
()
=>
import
(
'../pages/AdminGroups.vue'
)
},
{
path
:
'groups/:id?/:section?'
,
component
:
()
=>
import
(
'../pages/AdminGroups.vue'
)
},
//
{ path: 'users/:id?/:section?', component: () => import('../pages/AdminUsers.vue') },
{
path
:
'users/:id?/:section?'
,
component
:
()
=>
import
(
'../pages/AdminUsers.vue'
)
},
// -> System
// -> System
// { path: 'api', component: () => import('../pages/AdminApi.vue') },
// { path: 'api', component: () => import('../pages/AdminApi.vue') },
{
path
:
'extensions'
,
component
:
()
=>
import
(
'../pages/AdminExtensions.vue'
)
},
{
path
:
'extensions'
,
component
:
()
=>
import
(
'../pages/AdminExtensions.vue'
)
},
...
...
ux/yarn.lock
View file @
47ed7b37
This diff was suppressed by a .gitattributes entry.
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment