mirror of
https://github.com/requarks/wiki.git
synced 2026-08-26 13:17:10 -05:00
feat: change own password dialog
This commit is contained in:
@@ -127,10 +127,17 @@ export default {
|
||||
*/
|
||||
async changePassword (obj, args, context) {
|
||||
try {
|
||||
const authResult = await WIKI.db.users.loginChangePassword(args, context)
|
||||
return {
|
||||
...authResult,
|
||||
operation: generateSuccess('Password changed successfully')
|
||||
if (args.continuationToken) {
|
||||
const authResult = await WIKI.db.users.loginChangePassword(args, context)
|
||||
return {
|
||||
...authResult,
|
||||
operation: generateSuccess('Password set successfully')
|
||||
}
|
||||
} else {
|
||||
await WIKI.db.users.changePassword(args, context)
|
||||
return {
|
||||
operation: generateSuccess('Password changed successfully')
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
WIKI.logger.debug(err)
|
||||
|
||||
@@ -41,7 +41,7 @@ export default {
|
||||
const usr = await WIKI.db.users.query().findById(args.id)
|
||||
|
||||
if (!usr) {
|
||||
throw new Error('Invalid User')
|
||||
throw new Error('ERR_INVALID_USER')
|
||||
}
|
||||
|
||||
// const str = _.get(WIKI.auth.strategies, usr.providerKey)
|
||||
@@ -51,10 +51,11 @@ export default {
|
||||
|
||||
usr.auth = _.mapValues(usr.auth, (auth, providerKey) => {
|
||||
if (auth.password) {
|
||||
auth.password = '***'
|
||||
auth.password = 'redacted'
|
||||
}
|
||||
if (auth.tfaSecret) {
|
||||
auth.tfaSecret = 'redacted'
|
||||
}
|
||||
auth.module = providerKey === '00910749-8ab6-498a-9be0-f4ca28ea5e52' ? 'google' : 'local'
|
||||
auth._moduleName = providerKey === '00910749-8ab6-498a-9be0-f4ca28ea5e52' ? 'Google' : 'Local'
|
||||
return auth
|
||||
})
|
||||
|
||||
@@ -211,7 +212,7 @@ export default {
|
||||
},
|
||||
async changeUserPassword (obj, args, context) {
|
||||
try {
|
||||
if (args.newPassword?.length < 6) {
|
||||
if (args.newPassword?.length < 8) {
|
||||
throw new Error('ERR_PASSWORD_TOO_SHORT')
|
||||
}
|
||||
|
||||
|
||||
@@ -42,12 +42,11 @@ extend type Mutation {
|
||||
): AuthenticationAuthResponse @rateLimit(limit: 5, duration: 60)
|
||||
|
||||
changePassword(
|
||||
userId: UUID
|
||||
continuationToken: String
|
||||
currentPassword: String
|
||||
newPassword: String!
|
||||
strategyId: UUID!
|
||||
siteId: UUID
|
||||
siteId: UUID!
|
||||
): AuthenticationAuthResponse @rateLimit(limit: 5, duration: 60)
|
||||
|
||||
forgotPassword(
|
||||
|
||||
@@ -189,8 +189,15 @@ input UserUpdateInput {
|
||||
email: String
|
||||
name: String
|
||||
groups: [UUID!]
|
||||
auth: UserAuthUpdateInput
|
||||
isActive: Boolean
|
||||
isVerified: Boolean
|
||||
meta: JSON
|
||||
prefs: JSON
|
||||
}
|
||||
|
||||
input UserAuthUpdateInput {
|
||||
tfaRequired: Boolean
|
||||
mustChangePwd: Boolean
|
||||
restrictLogin: Boolean
|
||||
}
|
||||
|
||||
@@ -1152,6 +1152,7 @@
|
||||
"auth.errors.tooManyAttempts": "Too many attempts!",
|
||||
"auth.errors.tooManyAttemptsMsg": "You've made too many failed attempts in a short period of time, please try again {time}.",
|
||||
"auth.errors.userNotFound": "User not found",
|
||||
"auth.errors.fields": "One or more fields are invalid.",
|
||||
"auth.fields.email": "Email Address",
|
||||
"auth.fields.emailUser": "Email / Username",
|
||||
"auth.fields.name": "Name",
|
||||
@@ -1197,9 +1198,9 @@
|
||||
"auth.tfaFormTitle": "Enter the security code generated from your trusted device:",
|
||||
"auth.tfaSetupInstrFirst": "Scan the QR code below from your mobile 2FA application:",
|
||||
"auth.tfaSetupInstrSecond": "Enter the security code generated from your trusted device:",
|
||||
"auth.tfaSetupSuccess": "2FA enabled successfully on your account.",
|
||||
"auth.tfaSetupTitle": "Your administrator has required Two-Factor Authentication (2FA) to be enabled on your account.",
|
||||
"auth.tfaSetupVerifying": "Verifying...",
|
||||
"auth.tfaSetupSuccess": "2FA enabled successfully on your account.",
|
||||
"common.actions.activate": "Activate",
|
||||
"common.actions.add": "Add",
|
||||
"common.actions.apply": "Apply",
|
||||
@@ -1746,6 +1747,7 @@
|
||||
"profile.appearanceLight": "Light",
|
||||
"profile.auth": "Authentication",
|
||||
"profile.authChangePassword": "Change Password",
|
||||
"profile.authDisableTfa": "Turn Off 2FA",
|
||||
"profile.authInfo": "Your account is associated with the following authentication methods:",
|
||||
"profile.authLoadingFailed": "Failed to load authentication methods.",
|
||||
"profile.authModifyTfa": "Modify 2FA",
|
||||
|
||||
+50
-2
@@ -497,6 +497,42 @@ export class User extends Model {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Change Password from Profile
|
||||
*/
|
||||
static async changePassword ({ strategyId, siteId, currentPassword, newPassword }, context) {
|
||||
const userId = context.req.user?.id
|
||||
if (!userId) {
|
||||
throw new Error('ERR_USER_NOT_AUTHENTICATED')
|
||||
}
|
||||
|
||||
const user = await WIKI.db.users.query().findById(userId)
|
||||
if (!user) {
|
||||
throw new Error('ERR_USER_NOT_FOUND')
|
||||
}
|
||||
|
||||
if (!newPassword || newPassword.length < 8) {
|
||||
throw new Error('ERR_PASSWORD_TOO_SHORT')
|
||||
}
|
||||
|
||||
if (!user.auth[strategyId]?.password) {
|
||||
throw new Error('ERR_UNEXPECTED_STRATEGY_ID')
|
||||
}
|
||||
|
||||
if (await bcrypt.compare(currentPassword, user.auth[strategyId].password) !== true) {
|
||||
throw new Error('ERR_INCORRECT_CURRENT_PASSWORD')
|
||||
}
|
||||
|
||||
user.auth[strategyId].password = await bcrypt.hash(newPassword, 12)
|
||||
user.auth[strategyId].mustChangePwd = false
|
||||
|
||||
await user.$query().patch({
|
||||
auth: user.auth
|
||||
})
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a password reset request
|
||||
*/
|
||||
@@ -686,14 +722,14 @@ export class User extends Model {
|
||||
*
|
||||
* @param {Object} param0 User ID and fields to update
|
||||
*/
|
||||
static async updateUser (id, { email, name, groups, isVerified, isActive, meta, prefs }) {
|
||||
static async updateUser (id, { email, name, groups, auth, isVerified, isActive, meta, prefs }) {
|
||||
const usr = await WIKI.db.users.query().findById(id)
|
||||
if (usr) {
|
||||
let usrData = {}
|
||||
if (!isEmpty(email) && email !== usr.email) {
|
||||
const dupUsr = await WIKI.db.users.query().select('id').where({ email }).first()
|
||||
if (dupUsr) {
|
||||
throw new WIKI.Error.AuthAccountAlreadyExists()
|
||||
throw new Error('ERR_DUPLICATE_ACCOUNT_EMAIL')
|
||||
}
|
||||
usrData.email = email.toLowerCase()
|
||||
}
|
||||
@@ -714,6 +750,18 @@ export class User extends Model {
|
||||
await usr.$relatedQuery('groups').unrelate().where('groupId', grp)
|
||||
}
|
||||
}
|
||||
if (!isNil(auth?.tfaRequired)) {
|
||||
usr.auth[WIKI.data.systemIds.localAuthId].tfaRequired = auth.tfaRequired
|
||||
usrData.auth = usr.auth
|
||||
}
|
||||
if (!isNil(auth?.mustChangePwd)) {
|
||||
usr.auth[WIKI.data.systemIds.localAuthId].mustChangePwd = auth.mustChangePwd
|
||||
usrData.auth = usr.auth
|
||||
}
|
||||
if (!isNil(auth?.restrictLogin)) {
|
||||
usr.auth[WIKI.data.systemIds.localAuthId].restrictLogin = auth.restrictLogin
|
||||
usrData.auth = usr.auth
|
||||
}
|
||||
if (!isNil(isVerified)) {
|
||||
usrData.isVerified = isVerified
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40" width="80px" height="80px"><path fill="#98ccfd" d="M0.5 2.5H39.5V19.5H0.5z"/><path fill="#4788c7" d="M39,3v16H1V3H39 M40,2H0v18h40V2L40,2z"/><path fill="#fff" d="M18 11c0 1.133-.867 2-2 2s-2-.867-2-2 .867-2 2-2S18 9.867 18 11zM8 9c-1.133 0-2 .867-2 2s.867 2 2 2 2-.867 2-2S9.133 9 8 9zM32 9c-1.133 0-2 .867-2 2s.867 2 2 2c1.133 0 2-.867 2-2S33.133 9 32 9zM24 9c-1.133 0-2 .867-2 2s.867 2 2 2 2-.867 2-2S25.133 9 24 9z"/><g><path fill="#dff0fe" d="M10.707 31L13.015 28.692 17.087 32.763 27.072 22.779 29.293 25 17 37.293z"/><path fill="#4788c7" d="M27.072,23.487L28.586,25L17,36.586L11.414,31l1.601-1.601l3.365,3.364l0.707,0.707l0.707-0.707 L27.072,23.487 M27.073,22.073l-9.986,9.983l-4.072-4.071L10,31l7,7.001L30,25L27.073,22.073L27.073,22.073z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 818 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40" width="80px" height="80px"><path fill="none" stroke="#4788c7" stroke-miterlimit="10" stroke-width="2" d="M30,17.714c0,0,0-5.306,0-5.714 c0-5.523-4.477-10-10-10S10,6.477,10,12c0,0.408,0,5.714,0,5.714"/><path fill="#dff0fe" d="M2.5,37.5V22c0-3.584,2.916-6.5,6.5-6.5h22c3.584,0,6.5,2.916,6.5,6.5v15.5H2.5z"/><path fill="#4788c7" d="M31,16c3.308,0,6,2.692,6,6v15H3V22c0-3.308,2.692-6,6-6H31 M31,15H9c-3.866,0-7,3.134-7,7v16h36V22 C38,18.134,34.866,15,31,15L31,15z"/><g><path fill="#b6dcfe" d="M17.59,32.5l0.891-5.343l-0.289-0.176C17.133,26.336,16.5,25.222,16.5,24c0-1.93,1.57-3.5,3.5-3.5 s3.5,1.57,3.5,3.5c0,1.222-0.633,2.336-1.691,2.981l-0.289,0.176L22.41,32.5H17.59z"/><path fill="#4788c7" d="M20,21c1.654,0,3,1.346,3,3c0,1.046-0.543,2.001-1.452,2.554l-0.578,0.352l0.111,0.667L21.82,32 H18.18l0.738-4.427l0.111-0.667l-0.578-0.352C17.543,26.001,17,25.046,17,24C17,22.346,18.346,21,20,21 M20,20 c-2.209,0-4,1.791-4,4c0,1.449,0.778,2.707,1.932,3.408L17,33h6l-0.932-5.592C23.222,26.707,24,25.449,24,24 C24,21.791,22.209,20,20,20L20,20z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -703,7 +703,7 @@ async function changePwd () {
|
||||
$continuationToken: String
|
||||
$newPassword: String!
|
||||
$strategyId: UUID!
|
||||
$siteId: UUID
|
||||
$siteId: UUID!
|
||||
) {
|
||||
changePassword (
|
||||
continuationToken: $continuationToken
|
||||
|
||||
@@ -0,0 +1,248 @@
|
||||
<template lang="pug">
|
||||
q-dialog(ref='dialogRef', @hide='onDialogHide')
|
||||
q-card(style='min-width: 650px;')
|
||||
q-card-section.card-header
|
||||
q-icon(name='img:/_assets/icons/fluent-password-reset.svg', left, size='sm')
|
||||
span {{t(`admin.users.changePassword`)}}
|
||||
q-form.q-py-sm(ref='changeUserPwdForm', @submit='save')
|
||||
q-item
|
||||
blueprint-icon(icon='lock')
|
||||
q-item-section
|
||||
q-input(
|
||||
outlined
|
||||
v-model='state.currentPassword'
|
||||
dense
|
||||
:rules='currentPasswordValidation'
|
||||
hide-bottom-space
|
||||
:label='t(`auth.changePwd.currentPassword`)'
|
||||
:aria-label='t(`auth.changePwd.currentPassword`)'
|
||||
lazy-rules='ondemand'
|
||||
autofocus
|
||||
)
|
||||
q-item
|
||||
blueprint-icon(icon='password')
|
||||
q-item-section
|
||||
q-input(
|
||||
outlined
|
||||
v-model='state.newPassword'
|
||||
dense
|
||||
:rules='newPasswordValidation'
|
||||
hide-bottom-space
|
||||
:label='t(`auth.changePwd.newPassword`)'
|
||||
:aria-label='t(`auth.changePwd.newPassword`)'
|
||||
lazy-rules='ondemand'
|
||||
autofocus
|
||||
)
|
||||
template(#append)
|
||||
.flex.items-center
|
||||
q-badge(
|
||||
:color='passwordStrength.color'
|
||||
:label='passwordStrength.label'
|
||||
)
|
||||
q-separator.q-mx-sm(vertical)
|
||||
q-btn(
|
||||
flat
|
||||
dense
|
||||
padding='none xs'
|
||||
color='brown'
|
||||
@click='randomizePassword'
|
||||
)
|
||||
q-icon(name='las la-dice-d6')
|
||||
.q-pl-xs.text-caption: strong Generate
|
||||
q-item
|
||||
blueprint-icon(icon='good-pincode')
|
||||
q-item-section
|
||||
q-input(
|
||||
outlined
|
||||
v-model='state.verifyPassword'
|
||||
dense
|
||||
:rules='verifyPasswordValidation'
|
||||
hide-bottom-space
|
||||
:label='t(`auth.changePwd.newPasswordVerify`)'
|
||||
:aria-label='t(`auth.changePwd.newPasswordVerify`)'
|
||||
lazy-rules='ondemand'
|
||||
autofocus
|
||||
)
|
||||
q-card-actions.card-actions
|
||||
q-space
|
||||
q-btn.acrylic-btn(
|
||||
flat
|
||||
:label='t(`common.actions.cancel`)'
|
||||
color='grey'
|
||||
padding='xs md'
|
||||
@click='onDialogCancel'
|
||||
)
|
||||
q-btn(
|
||||
unelevated
|
||||
:label='t(`common.actions.update`)'
|
||||
color='primary'
|
||||
padding='xs md'
|
||||
@click='save'
|
||||
:loading='state.isLoading'
|
||||
)
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import gql from 'graphql-tag'
|
||||
import zxcvbn from 'zxcvbn'
|
||||
import { sampleSize } from 'lodash-es'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useDialogPluginComponent, useQuasar } from 'quasar'
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
|
||||
import { useSiteStore } from 'src/stores/site'
|
||||
|
||||
// PROPS
|
||||
|
||||
const props = defineProps({
|
||||
strategyId: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
// EMITS
|
||||
|
||||
defineEmits([
|
||||
...useDialogPluginComponent.emits
|
||||
])
|
||||
|
||||
// QUASAR
|
||||
|
||||
const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginComponent()
|
||||
const $q = useQuasar()
|
||||
|
||||
// STORES
|
||||
|
||||
const siteStore = useSiteStore()
|
||||
|
||||
// I18N
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
// DATA
|
||||
|
||||
const state = reactive({
|
||||
currentPassword: '',
|
||||
newPassword: '',
|
||||
verifyPassword: '',
|
||||
isLoading: false
|
||||
})
|
||||
|
||||
// REFS
|
||||
|
||||
const changeUserPwdForm = ref(null)
|
||||
|
||||
// COMPUTED
|
||||
|
||||
const passwordStrength = computed(() => {
|
||||
if (state.newPassword.length < 8) {
|
||||
return {
|
||||
color: 'negative',
|
||||
label: t('admin.users.pwdStrengthWeak')
|
||||
}
|
||||
} else {
|
||||
switch (zxcvbn(state.newPassword).score) {
|
||||
case 1:
|
||||
return {
|
||||
color: 'deep-orange-7',
|
||||
label: t('admin.users.pwdStrengthPoor')
|
||||
}
|
||||
case 2:
|
||||
return {
|
||||
color: 'purple-7',
|
||||
label: t('admin.users.pwdStrengthMedium')
|
||||
}
|
||||
case 3:
|
||||
return {
|
||||
color: 'blue-7',
|
||||
label: t('admin.users.pwdStrengthGood')
|
||||
}
|
||||
case 4:
|
||||
return {
|
||||
color: 'green-7',
|
||||
label: t('admin.users.pwdStrengthStrong')
|
||||
}
|
||||
default:
|
||||
return {
|
||||
color: 'negative',
|
||||
label: t('admin.users.pwdStrengthWeak')
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// VALIDATION RULES
|
||||
|
||||
const currentPasswordValidation = [
|
||||
val => val.length > 0 || t('auth.errors.missingPassword')
|
||||
]
|
||||
const newPasswordValidation = [
|
||||
val => val.length > 0 || t('auth.errors.missingPassword'),
|
||||
val => val.length >= 8 || t('auth.errors.passwordTooShort')
|
||||
]
|
||||
const verifyPasswordValidation = [
|
||||
val => val.length > 0 || t('auth.errors.missingVerifyPassword'),
|
||||
val => val === state.newPassword || t('auth.errors.passwordsNotMatch')
|
||||
]
|
||||
|
||||
// METHODS
|
||||
|
||||
function randomizePassword () {
|
||||
const pwdChars = 'abcdefghkmnpqrstuvwxyzABCDEFHJKLMNPQRSTUVWXYZ23456789_*=?#!()+'
|
||||
state.newPassword = sampleSize(pwdChars, 16).join('')
|
||||
}
|
||||
|
||||
async function save () {
|
||||
state.isLoading = true
|
||||
try {
|
||||
const isFormValid = await changeUserPwdForm.value.validate(true)
|
||||
if (!isFormValid) {
|
||||
throw new Error(t('auth.errors.fields'))
|
||||
}
|
||||
const resp = await APOLLO_CLIENT.mutate({
|
||||
mutation: gql`
|
||||
mutation changePwd (
|
||||
$currentPassword: String
|
||||
$newPassword: String!
|
||||
$strategyId: UUID!
|
||||
$siteId: UUID!
|
||||
) {
|
||||
changePassword (
|
||||
currentPassword: $currentPassword
|
||||
newPassword: $newPassword
|
||||
strategyId: $strategyId
|
||||
siteId: $siteId
|
||||
) {
|
||||
operation {
|
||||
succeeded
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
currentPassword: state.currentPassword,
|
||||
newPassword: state.newPassword,
|
||||
strategyId: props.strategyId,
|
||||
siteId: siteStore.id
|
||||
}
|
||||
})
|
||||
if (resp?.data?.changePassword?.operation?.succeeded) {
|
||||
$q.notify({
|
||||
type: 'positive',
|
||||
message: t('auth.changePwd.success')
|
||||
})
|
||||
onDialogOK()
|
||||
} else {
|
||||
throw new Error(resp?.data?.changePassword?.operation?.message || 'An unexpected error occured.')
|
||||
}
|
||||
} catch (err) {
|
||||
$q.notify({
|
||||
type: 'negative',
|
||||
message: err.message
|
||||
})
|
||||
}
|
||||
state.isLoading = false
|
||||
}
|
||||
</script>
|
||||
@@ -744,7 +744,12 @@ async function save (patch, { silent, keepOpen } = { silent: false, keepOpen: fa
|
||||
isActive: state.user.isActive,
|
||||
meta: state.user.meta,
|
||||
prefs: state.user.prefs,
|
||||
groups: state.user.groups.map(gr => gr.id)
|
||||
groups: state.user.groups.map(gr => gr.id),
|
||||
auth: {
|
||||
tfaRequired: localAuth.value.isTfaRequired,
|
||||
mustChangePwd: localAuth.value.mustChangePwd,
|
||||
restrictLogin: localAuth.value.restrictLogin
|
||||
}
|
||||
}
|
||||
}
|
||||
try {
|
||||
@@ -816,7 +821,7 @@ function invalidateTFA () {
|
||||
label: t('common.actions.confirm')
|
||||
}
|
||||
}).onOk(() => {
|
||||
localAuth.value.tfaSecret = ''
|
||||
// TODO: invalidate user 2FA
|
||||
$q.notify({
|
||||
type: 'positive',
|
||||
message: t('admin.users.tfaInvalidateSuccess')
|
||||
|
||||
@@ -25,8 +25,8 @@ q-page.q-py-md(:style-fn='pageStyle')
|
||||
q-btn(
|
||||
icon='las la-fingerprint'
|
||||
unelevated
|
||||
:label='t(`profile.authModifyTfa`)'
|
||||
color='primary'
|
||||
:label='t(`profile.authDisableTfa`)'
|
||||
color='negative'
|
||||
@click=''
|
||||
)
|
||||
q-item-section(v-else, side)
|
||||
@@ -43,7 +43,7 @@ q-page.q-py-md(:style-fn='pageStyle')
|
||||
unelevated
|
||||
:label='t(`profile.authChangePassword`)'
|
||||
color='primary'
|
||||
@click=''
|
||||
@click='changePassword(auth.authId)'
|
||||
)
|
||||
|
||||
q-inner-loading(:showing='state.loading > 0')
|
||||
@@ -57,6 +57,8 @@ import { onMounted, reactive } from 'vue'
|
||||
|
||||
import { useUserStore } from 'src/stores/user'
|
||||
|
||||
import ChangePwdDialog from 'src/components/ChangePwdDialog.vue'
|
||||
|
||||
// QUASAR
|
||||
|
||||
const $q = useQuasar()
|
||||
@@ -128,6 +130,15 @@ async function fetchAuthMethods () {
|
||||
state.loading--
|
||||
}
|
||||
|
||||
function changePassword (strategyId) {
|
||||
$q.dialog({
|
||||
component: ChangePwdDialog,
|
||||
componentProps: {
|
||||
strategyId
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// MOUNTED
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
Reference in New Issue
Block a user