瀏覽代碼

feat: delete user with replace target

NGPixel 5 年之前
父節點
當前提交
1f9e5b3fd0

+ 1 - 1
client/components/admin/admin-groups-edit-rules.vue

@@ -1,5 +1,5 @@
 <template lang="pug">
-  v-card
+  v-card(flat)
     v-card-text(v-if='group.id === 1')
       v-alert.radius-7(
         :class='$vuetify.theme.dark ? "grey darken-4" : "orange lighten-5"'

+ 5 - 3
client/components/admin/admin-groups-edit-users.vue

@@ -1,5 +1,5 @@
 <template lang="pug">
-  v-card
+  v-card(flat)
     v-card-title.pb-4(:class='$vuetify.theme.dark ? `grey darken-3-d3` : `grey lighten-5`')
       v-text-field(
         outlined
@@ -8,6 +8,8 @@
         v-model='search'
         label='Search Group Users...'
         hide-details
+        dense
+        style='max-width: 450px;'
       )
       v-spacer
       v-btn(color='primary', depressed, @click='searchUserDialog = true', :disabled='group.id === 2')
@@ -65,7 +67,7 @@ export default {
   data() {
     return {
       headers: [
-        { text: 'ID', value: 'id', width: 50 },
+        { text: 'ID', value: 'id', width: 70 },
         { text: 'Name', value: 'name' },
         { text: 'Email', value: 'email' },
         { text: 'Actions', value: 'actions', sortable: false, width: 50 }
@@ -90,7 +92,7 @@ export default {
     }
   },
   methods: {
-    async assignUser(id) {
+    async assignUser({ id, email, name }) {
       try {
         await this.$apollo.mutate({
           mutation: assignUserMutation,

+ 56 - 10
client/components/admin/admin-users-edit.vue

@@ -43,7 +43,7 @@
                 v-list-item-icon
                   v-icon(color='blue') mdi-account-check
                 v-list-item-title Set as Verified
-              v-list-item(@click='deleteUserDialog = true', :disabled='user.id == currentUserId || user.isSystem')
+              v-list-item(@click='deleteUserConfirm', :disabled='user.id == currentUserId || user.isSystem')
                 v-list-item-icon
                   v-icon(color='red') mdi-trash-can-outline
                 v-list-item-title Delete
@@ -221,8 +221,9 @@
               hide-details
               @keydown.esc='editPop.assignGroup = false'
               style='max-width: 300px;'
+              dense
             )
-            v-btn.ml-2.px-4(depressed, color='primary', height='48', @click='assignGroup', :disabled='newGroup === 0')
+            v-btn.ml-2.px-4(depressed, color='primary', @click='assignGroup', :disabled='newGroup === 0')
               v-icon(left) mdi-clipboard-account-outline
               span {{$t('admin:users.groupAssign')}}
           v-system-bar(window, :color='$vuetify.theme.dark ? `grey darken-4-l3` : `grey lighten-3`')
@@ -349,20 +350,32 @@
         v-card-text.pt-5
           i18next(path='admin:users.deleteConfirmText', tag='span')
             strong(place='username') {{ user.email }}
-          .caption.mt-3 {{$t('admin:users.deleteConfirmForeignNotice')}}
-        v-card-actions
+          .mt-3 {{$t('admin:users.deleteConfirmReplaceWarn')}}
+          v-divider.my-3
+          .d-flex.align-center.mt-3
+            v-btn.text-none(color='primary', depressed, @click='deleteSearchUserDialog = true')
+              v-icon(left) mdi-clipboard-account
+              | Select User...
+            .caption.pl-3
+              strong ID {{deleteReplaceUser.id}}
+              .caption {{deleteReplaceUser.name}}
+              em {{deleteReplaceUser.email}}
+        v-card-chin
           v-spacer
           v-btn(text, @click='deleteUserDialog = false') {{$t('common:actions.cancel')}}
           v-btn(color='red', dark, @click='deleteUser') {{$t('common:actions.delete')}}
 
+        user-search(v-model='deleteSearchUserDialog', @select='assignDeleteUser')
+
 </template>
 <script>
 import _ from 'lodash'
 import { get } from 'vuex-pathify'
 import gql from 'graphql-tag'
-
 import { StatusIndicator } from 'vue-status-indicator'
 
+import UserSearch from '../common/user-search.vue'
+
 import groupsQuery from 'gql/admin/users/users-query-groups.gql'
 
 export default {
@@ -370,11 +383,18 @@ export default {
     namespaces: ['admin', 'profile']
   },
   components: {
-    StatusIndicator
+    StatusIndicator,
+    UserSearch
   },
-  data() {
+  data () {
     return {
       deleteUserDialog: false,
+      deleteSearchUserDialog: false,
+      deleteReplaceUser: {
+        id: 1,
+        name: '',
+        email: ''
+      },
       editPop: {
         email: false,
         name: false,
@@ -738,13 +758,21 @@ export default {
     /**
      * Delete a user
      */
+    deleteUserConfirm () {
+      this.deleteUserDialog = true
+      this.deleteReplaceUser = {
+        id: this.currentUserId,
+        name: this.$store.get('user/name'),
+        email: this.$store.get('user/email')
+      }
+    },
     async deleteUser () {
       this.$store.commit(`loadingStart`, 'admin-users-delete')
       const resp = await this.$apollo.mutate({
         mutation: gql`
-          mutation ($id: Int!) {
+          mutation ($id: Int!, $replaceId: Int!) {
             users {
-              delete(id: $id) {
+              delete(id: $id, replaceId: $replaceId) {
                 responseResult {
                   succeeded
                   errorCode
@@ -756,7 +784,8 @@ export default {
           }
         `,
         variables: {
-          id: this.user.id
+          id: this.user.id,
+          replaceId: this.deleteReplaceUser.id
         }
       })
       if (_.get(resp, 'data.users.delete.responseResult.succeeded', false)) {
@@ -776,6 +805,23 @@ export default {
       this.deleteUserDialog = false
       this.$store.commit(`loadingStop`, 'admin-users-delete')
     },
+    assignDeleteUser (selUsr) {
+      if (selUsr.id === this.user.id) {
+        this.$store.commit('showNotification', {
+          style: 'red',
+          message: 'You cannot select the account you\'re about to delete!',
+          icon: 'warning'
+        })
+      } else if (selUsr.id === 2) {
+        this.$store.commit('showNotification', {
+          style: 'red',
+          message: 'You cannot use the guest account for this operation.',
+          icon: 'warning'
+        })
+      } else {
+        this.deleteReplaceUser = selUsr
+      }
+    },
     /**
      * Update a user
      */

+ 16 - 6
client/components/common/user-search.vue

@@ -30,7 +30,7 @@
           dense
           )
           template(v-for='(usr, idx) in items')
-            v-list-item(:key='usr.id', @click='setUser(usr.id)')
+            v-list-item(:key='usr.id', @click='setUser(usr)')
               v-list-item-avatar(size='40', color='primary')
                 span.body-1.white--text {{usr.name | initials}}
               v-list-item-content
@@ -50,8 +50,7 @@
 
 <script>
 import _ from 'lodash'
-
-import searchUsersQuery from 'gql/common/common-users-query-search.gql'
+import gql from 'graphql-tag'
 
 export default {
   filters: {
@@ -96,8 +95,8 @@ export default {
     close() {
       this.$emit('input', false)
     },
-    setUser(id) {
-      this.$emit('select', id)
+    setUser(usr) {
+      this.$emit('select', usr)
       this.close()
     },
     searchFilter(item, queryText, itemText) {
@@ -106,7 +105,18 @@ export default {
   },
   apollo: {
     items: {
-      query: searchUsersQuery,
+      query: gql`
+        query ($query: String!) {
+          users {
+            search(query:$query) {
+              id
+              name
+              email
+              providerKey
+            }
+          }
+        }
+      `,
       variables() {
         return {
           query: this.search

+ 0 - 10
client/graph/common/common-users-query-search.gql

@@ -1,10 +0,0 @@
-query ($query: String!) {
-  users {
-    search(query:$query) {
-      id
-      name
-      email
-      providerKey
-    }
-  }
-}

+ 6 - 4
dev/webpack/webpack.dev.js

@@ -183,10 +183,12 @@ module.exports = {
       startYear: 2017,
       endYear: (new Date().getFullYear()) + 5
     }),
-    new CopyWebpackPlugin([
-      { from: 'client/static' },
-      { from: './node_modules/prismjs/components', to: 'js/prism' }
-    ], {}),
+    new CopyWebpackPlugin({
+      patterns: [
+        { from: 'client/static' },
+        { from: './node_modules/prismjs/components', to: 'js/prism' }
+      ]
+    }),
     new HtmlWebpackPlugin({
       template: 'dev/templates/master.pug',
       filename: '../server/views/master.pug',

+ 6 - 4
dev/webpack/webpack.prod.js

@@ -190,10 +190,12 @@ module.exports = {
       startYear: 2017,
       endYear: (new Date().getFullYear()) + 5
     }),
-    new CopyWebpackPlugin([
-      { from: 'client/static' },
-      { from: './node_modules/prismjs/components', to: 'js/prism' }
-    ], {}),
+    new CopyWebpackPlugin({
+      patterns: [
+        { from: 'client/static' },
+        { from: './node_modules/prismjs/components', to: 'js/prism' }
+      ]
+    }),
     new MiniCssExtractPlugin({
       filename: 'css/bundle.[hash].css',
       chunkFilename: 'css/[name].[chunkhash].css'

+ 46 - 46
package.json

@@ -36,8 +36,8 @@
   },
   "dependencies": {
     "@aoberoi/passport-slack": "1.0.5",
-    "@azure/storage-blob": "12.1.1",
-    "@bugsnag/js": "7.0.2",
+    "@azure/storage-blob": "12.1.2",
+    "@bugsnag/js": "7.1.1",
     "@exlinc/keycloak-passport": "1.0.2",
     "@root/csr": "0.8.1",
     "@root/keypairs": "0.9.0",
@@ -49,7 +49,7 @@
     "apollo-server": "2.13.1",
     "apollo-server-express": "2.13.1",
     "auto-load": "3.0.4",
-    "aws-sdk": "2.678.0",
+    "aws-sdk": "2.686.0",
     "azure-search-client": "3.1.5",
     "bcryptjs-then": "1.0.1",
     "bluebird": "3.7.2",
@@ -74,14 +74,14 @@
     "elasticsearch6": "npm:@elastic/elasticsearch@6",
     "elasticsearch7": "npm:@elastic/elasticsearch@7",
     "emoji-regex": "9.0.0",
-    "eventemitter2": "6.4.0",
+    "eventemitter2": "6.4.1",
     "express": "4.17.1",
     "express-brute": "1.0.1",
     "express-session": "1.17.1",
-    "file-type": "14.4.0",
+    "file-type": "14.5.0",
     "filesize": "6.1.0",
     "fs-extra": "9.0.0",
-    "getos": "3.2.0",
+    "getos": "3.2.1",
     "graphql": "14.6.0",
     "graphql-list-fields": "2.0.2",
     "graphql-rate-limit-directive": "1.2.1",
@@ -89,22 +89,22 @@
     "graphql-tools": "4.0.7",
     "he": "1.2.0",
     "highlight.js": "10.0.3",
-    "i18next": "19.4.4",
+    "i18next": "19.4.5",
     "i18next-express-middleware": "2.0.0",
     "i18next-node-fs-backend": "2.1.3",
     "image-size": "0.8.3",
     "js-base64": "2.5.2",
     "js-binary": "1.2.0",
-    "js-yaml": "3.13.1",
+    "js-yaml": "3.14.0",
     "jsdom": "16.2.2",
     "jsonwebtoken": "8.5.1",
     "katex": "0.11.1",
     "klaw": "3.0.0",
     "knex": "0.21.1",
     "lodash": "4.17.15",
-    "markdown-it": "10.0.0",
+    "markdown-it": "11.0.0",
     "markdown-it-abbr": "1.0.4",
-    "markdown-it-attrs": "3.0.2",
+    "markdown-it-attrs": "3.0.3",
     "markdown-it-emoji": "1.4.0",
     "markdown-it-expand-tabs": "1.0.13",
     "markdown-it-external-links": "0.0.6",
@@ -117,18 +117,18 @@
     "markdown-it-task-lists": "2.1.1",
     "mathjax": "3.0.5",
     "mime-types": "2.1.27",
-    "moment": "2.25.3",
-    "moment-timezone": "0.5.29",
-    "mongodb": "3.5.7",
+    "moment": "2.26.0",
+    "moment-timezone": "0.5.31",
+    "mongodb": "3.5.8",
     "ms": "2.1.2",
     "mssql": "6.2.0",
     "multer": "1.4.2",
     "mysql2": "2.1.0",
-    "nanoid": "3.1.8",
+    "nanoid": "3.1.9",
     "node-2fa": "1.1.2",
     "node-cache": "5.1.0",
-    "nodemailer": "6.4.6",
-    "objection": "2.1.3",
+    "nodemailer": "6.4.8",
+    "objection": "2.1.5",
     "passport": "0.4.1",
     "passport-auth0": "1.3.2",
     "passport-azure-ad": "4.2.1",
@@ -154,7 +154,7 @@
     "pg-pubsub": "0.5.0",
     "pg-query-stream": "3.1.1",
     "pg-tsquery": "8.1.0",
-    "pug": "2.0.4",
+    "pug": "3.0.0",
     "punycode": "2.1.1",
     "qr-image": "3.2.0",
     "raven": "2.6.4",
@@ -166,7 +166,7 @@
     "scim-query-filter-parser": "2.0.4",
     "semver": "7.3.2",
     "serve-favicon": "2.5.0",
-    "simple-git": "2.4.0",
+    "simple-git": "2.5.0",
     "solr-node": "1.2.1",
     "sqlite3": "4.2.0",
     "ssh2": "0.8.9",
@@ -176,30 +176,30 @@
     "tar-fs": "2.1.0",
     "twemoji": "13.0.0",
     "uslug": "1.0.4",
-    "uuid": "8.0.0",
+    "uuid": "8.1.0",
     "validate.js": "0.13.1",
     "winston": "3.2.1",
     "xss": "1.0.6",
     "yargs": "15.3.1"
   },
   "devDependencies": {
-    "@babel/cli": "^7.8.4",
-    "@babel/core": "^7.9.6",
-    "@babel/plugin-proposal-class-properties": "^7.8.3",
-    "@babel/plugin-proposal-decorators": "^7.8.3",
-    "@babel/plugin-proposal-export-namespace-from": "^7.8.3",
-    "@babel/plugin-proposal-function-sent": "^7.8.3",
-    "@babel/plugin-proposal-json-strings": "^7.8.3",
-    "@babel/plugin-proposal-numeric-separator": "^7.8.3",
-    "@babel/plugin-proposal-throw-expressions": "^7.8.3",
+    "@babel/cli": "^7.10.1",
+    "@babel/core": "^7.10.1",
+    "@babel/plugin-proposal-class-properties": "^7.10.1",
+    "@babel/plugin-proposal-decorators": "^7.10.1",
+    "@babel/plugin-proposal-export-namespace-from": "^7.10.1",
+    "@babel/plugin-proposal-function-sent": "^7.10.1",
+    "@babel/plugin-proposal-json-strings": "^7.10.1",
+    "@babel/plugin-proposal-numeric-separator": "^7.10.1",
+    "@babel/plugin-proposal-throw-expressions": "^7.10.1",
     "@babel/plugin-syntax-dynamic-import": "^7.8.3",
-    "@babel/plugin-syntax-import-meta": "^7.8.3",
-    "@babel/polyfill": "^7.8.7",
-    "@babel/preset-env": "^7.9.6",
-    "@mdi/font": "5.2.45",
+    "@babel/plugin-syntax-import-meta": "^7.10.1",
+    "@babel/polyfill": "^7.10.1",
+    "@babel/preset-env": "^7.10.1",
+    "@mdi/font": "5.3.45",
     "@panter/vue-i18next": "0.15.2",
     "@requarks/ckeditor5": "12.4.0-wiki.16",
-    "@vue/babel-preset-app": "4.3.1",
+    "@vue/babel-preset-app": "4.4.1",
     "animate-sass": "0.8.2",
     "animated-number-vue": "1.0.0",
     "apollo-cache-inmemory": "1.6.6",
@@ -211,7 +211,7 @@
     "apollo-link-persisted-queries": "0.2.2",
     "apollo-link-ws": "1.0.20",
     "apollo-utilities": "1.3.4",
-    "autoprefixer": "9.7.6",
+    "autoprefixer": "9.8.0",
     "babel-eslint": "10.1.0",
     "babel-jest": "26.0.1",
     "babel-loader": "^8.1.0",
@@ -224,15 +224,15 @@
     "chart.js": "2.9.3",
     "clean-webpack-plugin": "3.0.0",
     "clipboard": "2.0.6",
-    "codemirror": "5.53.2",
-    "copy-webpack-plugin": "5.1.1",
+    "codemirror": "5.54.0",
+    "copy-webpack-plugin": "6.0.1",
     "core-js": "3.6.5",
     "css-loader": "3.5.3",
     "cssnano": "4.1.10",
     "d3": "5.16.0",
     "duplicate-package-checker-webpack-plugin": "3.0.0",
     "epic-spinners": "1.1.0",
-    "eslint": "7.0.0",
+    "eslint": "7.1.0",
     "eslint-config-requarks": "1.0.7",
     "eslint-config-standard": "14.1.1",
     "eslint-plugin-import": "2.20.2",
@@ -242,7 +242,7 @@
     "eslint-plugin-vue": "6.2.2",
     "fibers": "5.0.0",
     "file-loader": "6.0.0",
-    "filepond": "4.13.6",
+    "filepond": "4.14.0",
     "filepond-plugin-file-validate-type": "1.2.5",
     "filesize.js": "2.0.0",
     "graphql-persisted-document-loader": "2.0.0",
@@ -257,7 +257,7 @@
     "jest": "26.0.1",
     "js-beautify": "1.11.0",
     "js-cookie": "2.2.1",
-    "mermaid": "8.5.0",
+    "mermaid": "8.5.1",
     "mini-css-extract-plugin": "0.9.0",
     "moment-duration-format": "2.3.2",
     "moment-timezone-data-webpack-plugin": "1.3.0",
@@ -277,13 +277,13 @@
     "pug-plain-loader": "1.0.0",
     "raw-loader": "4.0.1",
     "resolve-url-loader": "3.1.1",
-    "sass": "1.26.5",
+    "sass": "1.26.7",
     "sass-loader": "8.0.2",
     "sass-resources-loader": "2.0.3",
     "script-ext-html-webpack-plugin": "2.1.4",
     "simple-progress-webpack-plugin": "1.1.2",
     "style-loader": "1.2.1",
-    "terser": "4.6.13",
+    "terser": "4.7.0",
     "twemoji-awesome": "1.0.6",
     "url-loader": "4.1.0",
     "velocity-animate": "1.5.2",
@@ -296,19 +296,19 @@
     "vue-hot-reload-api": "2.3.4",
     "vue-loader": "15.9.2",
     "vue-moment": "4.1.0",
-    "vue-router": "3.1.6",
+    "vue-router": "3.3.2",
     "vue-status-indicator": "1.2.1",
     "vue-template-compiler": "2.6.11",
     "vue2-animate": "2.1.3",
     "vuedraggable": "2.23.2",
-    "vuescroll": "4.15.0",
-    "vuetify": "2.2.28",
-    "vuetify-loader": "1.4.3",
+    "vuescroll": "4.15.1",
+    "vuetify": "2.2.30",
+    "vuetify-loader": "1.4.4",
     "vuex": "3.4.0",
     "vuex-pathify": "1.4.1",
     "vuex-persistedstate": "3.0.1",
     "webpack": "4.43.0",
-    "webpack-bundle-analyzer": "3.7.0",
+    "webpack-bundle-analyzer": "3.8.0",
     "webpack-cli": "3.3.11",
     "webpack-dev-middleware": "3.7.2",
     "webpack-hot-middleware": "2.25.0",

+ 1 - 1
server/graph/resolvers/user.js

@@ -72,7 +72,7 @@ module.exports = {
         if (args.id <= 2) {
           throw new WIKI.Error.UserDeleteProtected()
         }
-        await WIKI.models.users.deleteUser(args.id)
+        await WIKI.models.users.deleteUser(args.id, args.replaceId)
         return {
           responseResult: graphHelper.generateSuccess('User deleted successfully')
         }

+ 1 - 0
server/graph/schemas/user.graphql

@@ -63,6 +63,7 @@ type UserMutation {
 
   delete(
     id: Int!
+    replaceId: Int!
   ): DefaultResponse @auth(requires: ["manage:users", "manage:system"])
 
   verify(

+ 7 - 1
server/helpers/page.js

@@ -71,13 +71,19 @@ module.exports = {
       ['description', page.description],
       ['published', page.isPublished.toString()],
       ['date', page.updatedAt],
-      ['tags', page.tags ? page.tags.map(t => t.tag).join(', ') : '']
+      ['tags', page.tags ? page.tags.map(t => t.tag).join(', ') : ''],
+      ['editor', page.editorKey]
     ]
     switch (page.contentType) {
       case 'markdown':
         return '---\n' + meta.map(mt => `${mt[0]}: ${mt[1]}`).join('\n') + '\n---\n\n' + page.content
       case 'html':
         return '<!--\n' + meta.map(mt => `${mt[0]}: ${mt[1]}`).join('\n') + '\n-->\n\n' + page.content
+      case 'json':
+        return {
+          ...page.content,
+          _meta: _.fromPairs(meta)
+        }
       default:
         return page.content
     }

+ 3 - 1
server/models/commentProviders.js

@@ -128,8 +128,10 @@ module.exports = class CommentProvider extends Model {
       } else {
         WIKI.data.commentProvider = {
           ...WIKI.data.commentProvider,
-          ...require(`../modules/comments/${commentProvider.key}/comment`)
+          ...require(`../modules/comments/${commentProvider.key}/comment`),
+          config: commentProvider.config
         }
+        await WIKI.data.commentProvider.init()
       }
       WIKI.data.commentProvider.config = commentProvider.config
     }

+ 7 - 1
server/models/users.js

@@ -613,9 +613,15 @@ module.exports = class User extends Model {
    *
    * @param {*} id User ID
    */
-  static async deleteUser (id) {
+  static async deleteUser (id, replaceId) {
     const usr = await WIKI.models.users.query().findById(id)
     if (usr) {
+      await WIKI.models.assets.query().patch({ authorId: replaceId }).where('authorId', id)
+      await WIKI.models.comments.query().patch({ authorId: replaceId }).where('authorId', id)
+      await WIKI.models.pageHistory.query().patch({ authorId: replaceId }).where('authorId', id)
+      await WIKI.models.pages.query().patch({ authorId: replaceId }).where('authorId', id)
+      await WIKI.models.pages.query().patch({ creatorId: replaceId }).where('creatorId', id)
+
       await WIKI.models.userKeys.query().delete().where('userId', id)
       await WIKI.models.users.query().deleteById(id)
     } else {

+ 10 - 4
server/modules/comments/default/comment.js

@@ -10,8 +10,6 @@ const { AkismetClient } = require('akismet-api')
 const window = new JSDOM('').window
 const DOMPurify = createDOMPurify(window)
 
-md.use(mdEmoji)
-
 let akismetClient = null
 
 // ------------------------------------
@@ -23,6 +21,7 @@ module.exports = {
    * Init
    */
   async init (config) {
+    WIKI.logger.info('(COMMENTS/DEFAULT) Initializing...')
     if (WIKI.data.commentProvider.config.akismet && WIKI.data.commentProvider.config.akismet.length > 2) {
       akismetClient = new AkismetClient({
         key: WIKI.data.commentProvider.config.akismet,
@@ -33,14 +32,19 @@ module.exports = {
       try {
         const isValid = await akismetClient.verifyKey()
         if (!isValid) {
-          WIKI.logger.warn('Akismet Key is invalid!')
+          akismetClient = null
+          WIKI.logger.warn('(COMMENTS/DEFAULT) Akismet Key is invalid! [ DISABLED ]')
+        } else {
+          WIKI.logger.info('(COMMENTS/DEFAULT) Akismet key is valid. [ OK ]')
         }
       } catch (err) {
-        WIKI.logger.warn('Unable to verify Akismet Key: ' + err.message)
+        akismetClient = null
+        WIKI.logger.warn('(COMMENTS/DEFAULT) Unable to verify Akismet Key: ' + err.message)
       }
     } else {
       akismetClient = null
     }
+    WIKI.logger.info('(COMMENTS/DEFAULT) Initialization completed.')
   },
   /**
    * Create New Comment
@@ -56,6 +60,8 @@ module.exports = {
       }
     })
 
+    mkdown.use(mdEmoji)
+
     // -> Build New Comment
     const newComment = {
       content,

File diff suppressed because it is too large
+ 466 - 152
yarn.lock


Some files were not shown because too many files changed in this diff