From 7f9c3511e8a891b546aae8ffe582cf776f95127b Mon Sep 17 00:00:00 2001 From: NGPixel Date: Mon, 2 Oct 2023 06:00:37 +0000 Subject: [PATCH] feat: change own password dialog --- server/graph/resolvers/authentication.mjs | 15 +- server/graph/resolvers/user.mjs | 11 +- server/graph/schemas/authentication.graphql | 3 +- server/graph/schemas/user.graphql | 7 + server/locales/en.json | 4 +- server/models/users.mjs | 52 +++- .../icons/ultraviolet-good-pincode.svg | 1 + ux/public/_assets/icons/ultraviolet-lock.svg | 1 + ux/src/components/AuthLoginPanel.vue | 2 +- ux/src/components/ChangePwdDialog.vue | 248 ++++++++++++++++++ ux/src/components/UserEditOverlay.vue | 9 +- ux/src/pages/ProfileAuth.vue | 17 +- 12 files changed, 350 insertions(+), 20 deletions(-) create mode 100644 ux/public/_assets/icons/ultraviolet-good-pincode.svg create mode 100644 ux/public/_assets/icons/ultraviolet-lock.svg create mode 100644 ux/src/components/ChangePwdDialog.vue diff --git a/server/graph/resolvers/authentication.mjs b/server/graph/resolvers/authentication.mjs index 90cf3c27..c8d560b3 100644 --- a/server/graph/resolvers/authentication.mjs +++ b/server/graph/resolvers/authentication.mjs @@ -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) diff --git a/server/graph/resolvers/user.mjs b/server/graph/resolvers/user.mjs index d0db20ca..d2e76594 100644 --- a/server/graph/resolvers/user.mjs +++ b/server/graph/resolvers/user.mjs @@ -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') } diff --git a/server/graph/schemas/authentication.graphql b/server/graph/schemas/authentication.graphql index 79ae2c9d..56ecf12c 100644 --- a/server/graph/schemas/authentication.graphql +++ b/server/graph/schemas/authentication.graphql @@ -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( diff --git a/server/graph/schemas/user.graphql b/server/graph/schemas/user.graphql index 60f4e1a9..048f11db 100644 --- a/server/graph/schemas/user.graphql +++ b/server/graph/schemas/user.graphql @@ -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 +} diff --git a/server/locales/en.json b/server/locales/en.json index f42b754a..557ff87e 100644 --- a/server/locales/en.json +++ b/server/locales/en.json @@ -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", diff --git a/server/models/users.mjs b/server/models/users.mjs index 38d70c88..95a7edad 100644 --- a/server/models/users.mjs +++ b/server/models/users.mjs @@ -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 } diff --git a/ux/public/_assets/icons/ultraviolet-good-pincode.svg b/ux/public/_assets/icons/ultraviolet-good-pincode.svg new file mode 100644 index 00000000..04a4b2be --- /dev/null +++ b/ux/public/_assets/icons/ultraviolet-good-pincode.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ux/public/_assets/icons/ultraviolet-lock.svg b/ux/public/_assets/icons/ultraviolet-lock.svg new file mode 100644 index 00000000..363d1f2d --- /dev/null +++ b/ux/public/_assets/icons/ultraviolet-lock.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ux/src/components/AuthLoginPanel.vue b/ux/src/components/AuthLoginPanel.vue index 691a1b04..f16b3d87 100644 --- a/ux/src/components/AuthLoginPanel.vue +++ b/ux/src/components/AuthLoginPanel.vue @@ -703,7 +703,7 @@ async function changePwd () { $continuationToken: String $newPassword: String! $strategyId: UUID! - $siteId: UUID + $siteId: UUID! ) { changePassword ( continuationToken: $continuationToken diff --git a/ux/src/components/ChangePwdDialog.vue b/ux/src/components/ChangePwdDialog.vue new file mode 100644 index 00000000..5fe10830 --- /dev/null +++ b/ux/src/components/ChangePwdDialog.vue @@ -0,0 +1,248 @@ + + + diff --git a/ux/src/components/UserEditOverlay.vue b/ux/src/components/UserEditOverlay.vue index b3a88ee2..1d1fa3ea 100644 --- a/ux/src/components/UserEditOverlay.vue +++ b/ux/src/components/UserEditOverlay.vue @@ -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') diff --git a/ux/src/pages/ProfileAuth.vue b/ux/src/pages/ProfileAuth.vue index 327b2cf9..8175d838 100644 --- a/ux/src/pages/ProfileAuth.vue +++ b/ux/src/pages/ProfileAuth.vue @@ -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(() => {