瀏覽代碼

feat: login change password step

NGPixel 1 年之前
父節點
當前提交
c5a441c946

+ 1 - 2
server/db/migrations/3.0.0.mjs

@@ -735,8 +735,7 @@ export async function up (knex) {
       auth: {
         [authModuleId]: {
           password: await bcrypt.hash(process.env.ADMIN_PASS || '12345678', 12),
-          mustChangePwd: false, // TODO: Revert to true (below) once change password flow is implemented
-          // mustChangePwd: !process.env.ADMIN_PASS,
+          mustChangePwd: !process.env.ADMIN_PASS,
           restrictLogin: false,
           tfaIsActive: false,
           tfaRequired: false,

+ 36 - 36
server/graph/resolvers/user.mjs

@@ -283,42 +283,42 @@ export default {
         return generateError(err)
       }
     },
-    async changePassword (obj, args, context) {
-      try {
-        if (!context.req.user || context.req.user.id < 1 || context.req.user.id === 2) {
-          throw new WIKI.Error.AuthRequired()
-        }
-        const usr = await WIKI.db.users.query().findById(context.req.user.id)
-        if (!usr.isActive) {
-          throw new WIKI.Error.AuthAccountBanned()
-        }
-        if (!usr.isVerified) {
-          throw new WIKI.Error.AuthAccountNotVerified()
-        }
-        if (usr.providerKey !== 'local') {
-          throw new WIKI.Error.AuthProviderInvalid()
-        }
-        try {
-          await usr.verifyPassword(args.current)
-        } catch (err) {
-          throw new WIKI.Error.AuthPasswordInvalid()
-        }
-
-        await WIKI.db.users.updateUser({
-          id: usr.id,
-          newPassword: args.new
-        })
-
-        const newToken = await WIKI.db.users.refreshToken(usr)
-
-        return {
-          responseResult: generateSuccess('Password changed successfully'),
-          jwt: newToken.token
-        }
-      } catch (err) {
-        return generateError(err)
-      }
-    },
+    // async changePassword (obj, args, context) {
+    //   try {
+    //     if (!context.req.user || context.req.user.id < 1 || context.req.user.id === 2) {
+    //       throw new WIKI.Error.AuthRequired()
+    //     }
+    //     const usr = await WIKI.db.users.query().findById(context.req.user.id)
+    //     if (!usr.isActive) {
+    //       throw new WIKI.Error.AuthAccountBanned()
+    //     }
+    //     if (!usr.isVerified) {
+    //       throw new WIKI.Error.AuthAccountNotVerified()
+    //     }
+    //     if (usr.providerKey !== 'local') {
+    //       throw new WIKI.Error.AuthProviderInvalid()
+    //     }
+    //     try {
+    //       await usr.verifyPassword(args.current)
+    //     } catch (err) {
+    //       throw new WIKI.Error.AuthPasswordInvalid()
+    //     }
+
+    //     await WIKI.db.users.updateUser({
+    //       id: usr.id,
+    //       newPassword: args.new
+    //     })
+
+    //     const newToken = await WIKI.db.users.refreshToken(usr)
+
+    //     return {
+    //       responseResult: generateSuccess('Password changed successfully'),
+    //       jwt: newToken.token
+    //     }
+    //   } catch (err) {
+    //     return generateError(err)
+    //   }
+    // },
     /**
      * UPLOAD USER AVATAR
      */

+ 2 - 0
server/locales/en.json

@@ -1129,6 +1129,7 @@
   "auth.changePwd.newPasswordVerify": "Verify New Password",
   "auth.changePwd.proceed": "Change Password",
   "auth.changePwd.subtitle": "Choose a new password",
+  "auth.changePwd.success": "Password updated successfully.",
   "auth.enterCredentials": "Enter your credentials",
   "auth.errors.forgotPassword": "Missing or invalid email address.",
   "auth.errors.invalidEmail": "Email is invalid.",
@@ -1198,6 +1199,7 @@
   "auth.tfaSetupInstrSecond": "Enter the security code generated from your trusted device:",
   "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",

+ 2 - 2
server/models/userKeys.mjs

@@ -66,14 +66,14 @@ export class UserKey extends Model {
         await WIKI.db.userKeys.query().deleteById(res.id)
       }
       if (DateTime.utc() > DateTime.fromISO(res.validUntil)) {
-        throw new WIKI.Error.AuthValidationTokenInvalid()
+        throw new Error('ERR_EXPIRED_VALIDATION_TOKEN')
       }
       return {
         ...res.meta,
         user: res.user
       }
     } else {
-      throw new WIKI.Error.AuthValidationTokenInvalid()
+      throw new Error('ERR_INVALID_VALIDATION_TOKEN')
     }
   }
 

+ 35 - 22
server/models/users.mjs

@@ -325,7 +325,10 @@ export class User extends Model {
         try {
           const tfaToken = await WIKI.db.userKeys.generateToken({
             kind: 'tfa',
-            userId: user.id
+            userId: user.id,
+            meta: {
+              strategyId
+            }
           })
           return {
             nextAction: 'provideTfa',
@@ -341,7 +344,10 @@ export class User extends Model {
           const tfaQRImage = await user.generateTFA(strategyId, siteId)
           const tfaToken = await WIKI.db.userKeys.generateToken({
             kind: 'tfaSetup',
-            userId: user.id
+            userId: user.id,
+            meta: {
+              strategyId
+            }
           })
           return {
             nextAction: 'setupTfa',
@@ -361,7 +367,10 @@ export class User extends Model {
       try {
         const pwdChangeToken = await WIKI.db.userKeys.generateToken({
           kind: 'changePwd',
-          userId: user.id
+          userId: user.id,
+          meta: {
+            strategyId
+          }
         })
 
         return {
@@ -435,11 +444,16 @@ export class User extends Model {
    */
   static async loginTFA ({ strategyId, siteId, securityCode, continuationToken, setup }, context) {
     if (securityCode.length === 6 && continuationToken.length > 1) {
-      const { user } = await WIKI.db.userKeys.validateToken({
+      const { user, strategyId: expectedStrategyId } = await WIKI.db.userKeys.validateToken({
         kind: setup ? 'tfaSetup' : 'tfa',
         token: continuationToken,
         skipDelete: setup
       })
+
+      if (strategyId !== expectedStrategyId) {
+        throw new Error('ERR_UNEXPECTED_STRATEGY_ID')
+      }
+
       if (user) {
         if (user.verifyTFA(strategyId, securityCode)) {
           if (setup) {
@@ -447,40 +461,39 @@ export class User extends Model {
           }
           return WIKI.db.users.afterLoginChecks(user, strategyId, context, { siteId, skipTFA: true })
         } else {
-          throw new WIKI.Error.AuthTFAFailed()
+          throw new Error('ERR_INCORRECT_TFA_TOKEN')
         }
       }
     }
-    throw new WIKI.Error.AuthTFAInvalid()
+    throw new Error('ERR_INVALID_TFA_REQUEST')
   }
 
   /**
    * Change Password from a Mandatory Password Change after Login
    */
-  static async loginChangePassword ({ continuationToken, newPassword }, context) {
-    if (!newPassword || newPassword.length < 6) {
-      throw new WIKI.Error.InputInvalid('Password must be at least 6 characters!')
+  static async loginChangePassword ({ strategyId, siteId, continuationToken, newPassword }, context) {
+    if (!newPassword || newPassword.length < 8) {
+      throw new Error('ERR_PASSWORD_TOO_SHORT')
     }
-    const usr = await WIKI.db.userKeys.validateToken({
+    const { user, strategyId: expectedStrategyId } = await WIKI.db.userKeys.validateToken({
       kind: 'changePwd',
       token: continuationToken
     })
 
-    if (usr) {
-      await WIKI.db.users.query().patch({
-        password: newPassword,
-        mustChangePwd: false
-      }).findById(usr.id)
+    if (strategyId !== expectedStrategyId) {
+      throw new Error('ERR_UNEXPECTED_STRATEGY_ID')
+    }
 
-      return new Promise((resolve, reject) => {
-        context.req.logIn(usr, { session: false }, async err => {
-          if (err) { return reject(err) }
-          const jwtToken = await WIKI.db.users.refreshToken(usr)
-          resolve({ jwt: jwtToken.token })
-        })
+    if (user) {
+      user.auth[strategyId].password = await bcrypt.hash(newPassword, 12),
+      user.auth[strategyId].mustChangePwd = false
+      await user.$query().patch({
+        auth: user.auth
       })
+
+      return WIKI.db.users.afterLoginChecks(user, strategyId, context, { siteId, skipChangePwd: true, skipTFA: true })
     } else {
-      throw new WIKI.Error.UserNotFound()
+      throw new Error('ERR_INVALID_USER')
     }
   }
 

+ 7 - 5
server/modules/authentication/local/authentication.mjs

@@ -21,18 +21,20 @@ export default {
           if (user) {
             const authStrategyData = user.auth[strategyId]
             if (!authStrategyData) {
-              throw new WIKI.Error.AuthLoginFailed()
+              throw new Error('ERR_INVALID_STRATEGY_ID')
             } else if (await bcrypt.compare(uPassword, authStrategyData.password) !== true) {
-              throw new WIKI.Error.AuthLoginFailed()
+              throw new Error('ERR_AUTH_FAILED')
             } else if (!user.isActive) {
-              throw new WIKI.Error.AuthAccountBanned()
+              throw new Error('ERR_INACTIVE_USER')
+            } else if (authStrategyData.restrictLogin) {
+              throw new Error('ERR_LOGIN_RESTRICTED')
             } else if (!user.isVerified) {
-              throw new WIKI.Error.AuthAccountNotVerified()
+              throw new Error('ERR_USER_NOT_VERIFIED')
             } else {
               done(null, user)
             }
           } else {
-            throw new WIKI.Error.AuthLoginFailed()
+            throw new Error('ERR_AUTH_FAILED')
           }
         } catch (err) {
           done(err, null)

+ 17 - 12
ux/src/components/AuthLoginPanel.vue

@@ -620,6 +620,11 @@ async function forgotPassword () {
     if (!isFormValid) {
       throw new Error(t('auth.errors.forgotPassword'))
     }
+    // TODO: Implement forgot password
+    $q.notify({
+      type: 'negative',
+      message: 'Not implemented yet.'
+    })
   } catch (err) {
     $q.notify({
       type: 'negative',
@@ -695,17 +700,13 @@ async function changePwd () {
     const resp = await APOLLO_CLIENT.mutate({
       mutation: gql`
         mutation (
-          $userId: UUID!
           $continuationToken: String
-          $currentPassword: String
           $newPassword: String!
           $strategyId: UUID!
           $siteId: UUID
         ) {
           changePassword (
-            userId: $userId
             continuationToken: $continuationToken
-            currentPassword: $currentPassword
             newPassword: $newPassword
             strategyId: $strategyId
             siteId: $siteId
@@ -715,9 +716,7 @@ async function changePwd () {
               message
             }
             jwt
-            mustChangePwd
-            mustProvideTFA
-            mustSetupTFA
+            nextAction
             continuationToken
             redirect
             tfaQRImage
@@ -725,19 +724,21 @@ async function changePwd () {
         }
       `,
       variables: {
-        userId: userStore.id,
         continuationToken: state.continuationToken,
-        currentPassword: state.password,
         newPassword: state.newPassword,
         strategyId: state.selectedStrategyId,
         siteId: siteStore.id
       }
     })
-    if (resp.data?.login?.operation?.succeeded) {
+    if (resp.data?.changePassword?.operation?.succeeded) {
       state.password = ''
-      await handleLoginResponse(resp.data.login)
+      $q.notify({
+        type: 'positive',
+        message: t('auth.changePwd.success')
+      })
+      await handleLoginResponse(resp.data.changePassword)
     } else {
-      throw new Error(resp.data?.login?.operation?.message || t('auth.errors.loginError'))
+      throw new Error(resp.data?.changePassword?.operation?.message || t('auth.errors.loginError'))
     }
   } catch (err) {
     $q.notify({
@@ -855,6 +856,10 @@ async function finishSetupTFA () {
     if (resp.data?.loginTFA?.operation?.succeeded) {
       state.continuationToken = ''
       state.securityCode = ''
+      $q.notify({
+        type: 'positive',
+        message: t('auth.tfaSetupSuccess')
+      })
       await handleLoginResponse(resp.data.loginTFA)
     } else {
       throw new Error(resp.data?.loginTFA?.operation?.message || t('auth.errors.loginError'))