Explorar o código

feat: import users from v1 - db + create users

Nick %!s(int64=5) %!d(string=hai) anos
pai
achega
82ba7d0a36

+ 295 - 33
client/components/admin/admin-utilities-importv1.vue

@@ -9,45 +9,171 @@
       v-divider.my-4
       .body-2 Data from a Wiki.js 1.x installation can easily be imported using this tool. What do you want to import?
       v-checkbox(
-        label='Content'
+        label='Content + Uploads'
         value='content'
         color='deep-orange darken-2'
         v-model='importFilters'
         hide-details
-      )
-      v-checkbox(
-        label='Uploads'
-        value='uploads'
-        color='deep-orange darken-2'
-        v-model='importFilters'
-        hide-details
-      )
+        )
+        template(v-slot:label)
+          strong.deep-orange--text.text--darken-2 Content + Uploads
+      .pl-8(v-if='wantContent')
+        v-radio-group(v-model='contentMode', hide-details)
+          v-radio(
+            value='git'
+            color='primary'
+            )
+            template(v-slot:label)
+              div
+                span Import from Git Connection
+                .caption: em #[strong.primary--text Recommended] | The Git storage module will also be configured for you.
+        .pl-8.mt-5(v-if='needGit')
+          v-row
+            v-col(cols='8')
+              v-select(
+                label='Authentication Mode'
+                :items='gitAuthModes'
+                v-model='gitAuthMode'
+                outlined
+                hide-details
+              )
+            v-col(cols='4')
+              v-switch(
+                label='Verify SSL Certificate'
+                v-model='gitVerifySSL'
+                hide-details
+                color='primary'
+              )
+            v-col(cols='8')
+              v-text-field(
+                outlined
+                label='Repository URL'
+                :placeholder='(gitAuthMode === `ssh`) ? `e.g. git@github.com:orgname/repo.git` : `e.g. https://github.com/orgname/repo.git`'
+                hide-details
+                v-model='gitRepoUrl'
+              )
+            v-col(cols='4')
+              v-text-field(
+                label='Branch'
+                placeholder='e.g. master'
+                v-model='gitRepoBranch'
+                outlined
+                hide-details
+              )
+            v-col(v-if='gitAuthMode === `ssh`', cols='12')
+              v-textarea(
+                outlined
+                label='Private Key'
+                placeholder='-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----'
+                hide-details
+                v-model='gitPrivKey'
+              )
+            template(v-else-if='gitAuthMode === `basic`')
+              v-col(cols='6')
+                v-text-field(
+                  label='Username'
+                  v-model='gitUserEmail'
+                  outlined
+                  hide-details
+                )
+              v-col(cols='6')
+                v-text-field(
+                  type='password'
+                  label='Password / PAT'
+                  v-model='gitUserName'
+                  outlined
+                  hide-details
+                )
+            v-col(cols='6')
+              v-text-field(
+                label='Default Author Email'
+                placeholder='e.g. name@company.com'
+                v-model='gitUserEmail'
+                outlined
+                hide-details
+              )
+            v-col(cols='6')
+              v-text-field(
+                label='Default Author Name'
+                placeholder='e.g. John Smith'
+                v-model='gitUserName'
+                outlined
+                hide-details
+              )
+            v-col(cols='12')
+              v-text-field(
+                label='Local Repository Path'
+                placeholder='e.g. ./data/repo'
+                v-model='gitRepoPath'
+                outlined
+                hide-details
+              )
+        v-radio-group(v-model='contentMode', hide-details)
+          v-divider
+          v-radio.mt-3(
+            value='local'
+            color='primary'
+            )
+            template(v-slot:label)
+              div
+                span Import from local folder
+                .caption: em Choose this option only if you didn't have git configured in your Wiki.js 1.x installation.
+        .pl-8.mt-5(v-if='needDisk')
+          v-text-field(
+            outlined
+            label='Content Repo Path'
+            hint='The absolute path to where the Wiki.js 1.x content is stored on disk.'
+            persistent-hint
+            v-model='contentPath'
+          )
+
       v-checkbox(
         label='Users'
         value='users'
         color='deep-orange darken-2'
         v-model='importFilters'
         hide-details
-      )
-      v-divider.my-5
-      v-text-field.mt-3(
-        outlined
-        label='MongoDB Connection String'
-        hint='The connection string to connect to the Wiki.js 1.x MongoDB database.'
-        persistent-hint
-        v-model='dbConnStr'
-        v-if='needDB'
-      )
-      v-text-field.mt-3(
-        outlined
-        label='Content Repo Path'
-        hint='The full path to where the Wiki.js 1.x content is stored on disk.'
-        persistent-hint
-        v-model='contentPath'
-        v-if='needDisk'
-      )
+        )
+        template(v-slot:label)
+          strong.deep-orange--text.text--darken-2 Users
+      .pl-8.mt-5(v-if='wantUsers')
+        v-text-field(
+          outlined
+          label='MongoDB Connection String'
+          hint='The connection string to connect to the Wiki.js 1.x MongoDB database.'
+          persistent-hint
+          v-model='dbConnStr'
+        )
+        v-radio-group(v-model='groupMode', hide-details, mandatory)
+          v-radio(
+            value='MULTI'
+            color='primary'
+            )
+            template(v-slot:label)
+              div
+                span Create groups for each unique user permissions configuration
+                .caption: em Note that this can result in a large amount of groups being created.
+          v-divider
+          v-radio.mt-3(
+            value='SINGLE'
+            color='primary'
+            )
+            template(v-slot:label)
+              div
+                span Create a single group with all imported users
+                .caption: em #[strong.primary--text Recommended] | The new group will have read permissions enabled by default.
+          v-divider
+          v-radio.mt-3(
+            value='NONE'
+            color='primary'
+            )
+            template(v-slot:label)
+              div
+                span Don't create any group
+                .caption: em Users will not be able to access your wiki until they are assigned to a group.
+
     v-card-chin
-      v-btn.px-3(depressed, color='deep-orange darken-2', :disabled='!needDB && !needDisk', @click='startImport').ml-0
+      v-btn.px-3(depressed, color='deep-orange darken-2', :disabled='!wantUsers && !wantContent', @click='startImport').ml-0
         v-icon(left, color='white') mdi-database-import
         span.white--text Start Import
     v-dialog(
@@ -65,34 +191,170 @@
           )
           .mt-5.body-1.white--text Importing from Wiki.js 1.x...
           .caption Please wait
+          v-progress-linear.mt-5(
+            color='white'
+            :value='progress'
+            stream
+            rounded
+            :buffer-value='0'
+          )
+    v-dialog(
+      v-model='isSuccess'
+      persistent
+      max-width='350'
+      )
+      v-card(color='green darken-2', dark)
+        v-card-text.pa-10.text-center
+          v-icon(size='60') mdi-check-circle-outline
+          .my-5.body-1.white--text Import completed
+          template(v-if='wantUsers')
+            .body-2
+              span #[strong {{successUsers}}] users imported
+              v-btn.text-none.ml-3(
+                v-if='failedUsers.length > 0'
+                text
+                color='white'
+                dark
+                @click='showFailedUsers = true'
+                )
+                v-icon(left) mdi-alert
+                span {{failedUsers.length}} failed
+            .body-2 #[strong {{successGroups}}] groups created
+          template(v-if='wantContent')
+            .body-2 #[strong {{successPages}}] pages
+            .body-2 #[strong {{successAssets}}] assets
+        v-card-actions.green.darken-1
+          v-spacer
+          v-btn.px-5(
+            color='white'
+            outlined
+            @click='isSuccess = false'
+          ) Close
+          v-spacer
+    v-dialog(
+      v-model='showFailedUsers'
+      persistent
+      max-width='800'
+      )
+      v-card(color='red darken-2', dark)
+        v-toolbar(color='red darken-2', dense)
+          v-icon mdi-alert
+          .body-2.pl-3 Failed User Imports
+          v-spacer
+          v-btn.px-5(
+            color='white'
+            text
+            @click='showFailedUsers = false'
+            ) Close
+        v-simple-table(dense, fixed-header, height='300px')
+          template(v-slot:default)
+            thead
+              tr
+                th Provider
+                th Email
+                th Error
+            tbody
+              tr(v-for='(fusr, idx) in failedUsers', :key='`fusr-` + idx')
+                td {{fusr.provider}}
+                td {{fusr.email}}
+                td {{fusr.error}}
 </template>
 
 <script>
+import _ from 'lodash'
+
 import { SemipolarSpinner } from 'epic-spinners'
 
+import utilityImportv1UsersMutation from 'gql/admin/utilities/utilities-mutation-importv1-users.gql'
+
 export default {
   components: {
     SemipolarSpinner
   },
   data() {
     return {
-      importFilters: ['content', 'uploads', 'users'],
+      importFilters: ['content', 'users'],
+      groupMode: 'SINGLE',
+      contentMode: 'git',
       dbConnStr: 'mongodb://',
       contentPath: '/wiki-v1/repo',
-      isLoading: false
+      isLoading: false,
+      isSuccess: false,
+      gitAuthMode: 'ssh',
+      gitAuthModes: [
+        { text: 'SSH', value: 'ssh' },
+        { text: 'Basic', value: 'basic' }
+      ],
+      gitVerifySSL: true,
+      gitRepoUrl: '',
+      gitRepoBranch: 'master',
+      gitPrivKey: '',
+      gitUserEmail: '',
+      gitUserName: '',
+      gitRepoPath: './data/repo',
+      progress: 0,
+      successUsers: 0,
+      successPages: 0,
+      successGroups: 0,
+      successAssets: 0,
+      showFailedUsers: false,
+      failedUsers: []
     }
   },
   computed: {
-    needDB() {
+    wantContent () {
+      return this.importFilters.indexOf('content') >= 0
+    },
+    wantUsers () {
       return this.importFilters.indexOf('users') >= 0
     },
-    needDisk() {
-      return this.importFilters.indexOf('content') >= 0 || this.importFilters.indexOf('uploads') >= 0
+    needDisk () {
+      return this.contentMode === `local`
+    },
+    needGit () {
+      return this.contentMode === `git`
     }
   },
   methods: {
     async startImport () {
       this.isLoading = true
+      this.progress = 0
+      this.failedUsers = []
+
+      // -> Import Users
+
+      if (this.wantUsers) {
+        try {
+          const resp = await this.$apollo.mutate({
+            mutation: utilityImportv1UsersMutation,
+            variables: {
+              mongoDbConnString: this.dbConnStr,
+              groupMode: this.groupMode
+            }
+          })
+          const respObj = _.get(resp, 'data.system.importUsersFromV1', {})
+          if (!_.get(respObj, 'responseResult.succeeded', false)) {
+            throw new Error(_.get(respObj, 'responseResult.message', 'An unexpected error occured'))
+          }
+          this.successUsers = _.get(respObj, 'usersCount', 0)
+          this.successGroups = _.get(respObj, 'groupsCount', 0)
+          this.failedUsers = _.get(respObj, 'failed', [])
+          this.progress += 50
+        } catch (err) {
+          this.$store.commit('pushGraphError', err)
+          this.isLoading = false
+          return
+        }
+      }
+
+      // -> Import Content
+
+      if (this.wantContent) {
+
+      }
+
+      this.isLoading = false
+      this.isSuccess = true
     }
   }
 }

+ 5 - 2
client/components/editor/editor-markdown.vue

@@ -597,6 +597,7 @@ $editor-height-mobile: calc(100vh - 112px - 16px);
     position: relative;
     height: $editor-height;
     overflow: hidden;
+    padding: 1rem;
 
     @at-root .theme--dark & {
       background-color: mc('grey', '900');
@@ -622,8 +623,8 @@ $editor-height-mobile: calc(100vh - 112px - 16px);
     &-content {
       height: $editor-height;
       overflow-y: scroll;
-      padding: 1rem 1rem 1rem 1rem;
-      width: calc(100% + 1rem + 17px);
+      padding: 0;
+      width: calc(100% + 17px);
       // -ms-overflow-style: none;
 
       // &::-webkit-scrollbar {
@@ -701,6 +702,8 @@ $editor-height-mobile: calc(100vh - 112px - 16px);
 
   .CodeMirror {
     height: auto;
+    font-family: 'Roboto Mono', monospace;
+    font-size: .9rem;
 
     .cm-header-1 {
       font-size: 1.5rem;

+ 19 - 0
client/graph/admin/utilities/utilities-mutation-importv1-users.gql

@@ -0,0 +1,19 @@
+mutation($mongoDbConnString: String!, $groupMode: SystemImportUsersGroupMode!) {
+  system {
+    importUsersFromV1(mongoDbConnString: $mongoDbConnString, groupMode: $groupMode) {
+      responseResult {
+        succeeded
+        errorCode
+        slug
+        message
+      }
+      usersCount
+      groupsCount
+      failed {
+        provider
+        email
+        error
+      }
+    }
+  }
+}

+ 6 - 6
client/themes/default/scss/app.scss

@@ -196,7 +196,7 @@
   // ---------------------------------
 
   p {
-    padding: 1rem 24px 0 24px;
+    padding: 1rem 0 0 0;
     margin: 0;
     text-align: justify;
 
@@ -319,7 +319,7 @@
   // ---------------------------------
 
   ol, ul {
-    padding: 1rem 24px 0 24px;
+    padding: 1rem 0 0 0;
     list-style-position: inside;
 
     li + li {
@@ -396,7 +396,7 @@
     }
 
     &.grid-list {
-      margin: 1rem 24px 0 24px;
+      margin: 1rem 0 0 0;
       background-color: #FFF;
       border: 1px solid mc('grey', '200');
       padding: 1px;
@@ -476,7 +476,7 @@
     box-shadow: initial;
     background-color: mc('grey', '900');
     padding: 1rem 1rem 1rem 3rem;
-    margin: 1rem 24px;
+    margin: 1rem 0;
 
     @at-root .theme--dark & {
       background-color: darken(mc('grey', '900'), 5%);
@@ -604,12 +604,12 @@
       border: 1px solid mc('grey', '400');
     }
     &.uml-diagram {
-      margin: 1rem;
+      margin: 1rem 0;
     }
   }
 
   figure.image {
-    margin: 1rem 24px 0 24px;
+    margin: 1rem 0 0 0;
 
     img {
       margin: 0 auto;

+ 50 - 0
server/graph/resolvers/system.js

@@ -84,6 +84,56 @@ module.exports = {
       } catch (err) {
         return graphHelper.generateError(err)
       }
+    },
+    async importUsersFromV1(obj, args, context) {
+      try {
+        const MongoClient = require('mongodb').MongoClient
+        if (args.mongoDbConnString && args.mongoDbConnString.length > 10) {
+          const client = await MongoClient.connect(args.mongoDbConnString, {
+            appname: `Wiki.js ${WIKI.version} Migration Tool`
+          })
+          const dbUsers = client.db().collection('users')
+          const userCursor = dbUsers.find({ email: { '$ne': 'guest' } })
+
+          let failed = []
+          let usersCount = 0
+          let groupsCount = 0
+
+          while (await userCursor.hasNext()) {
+            const usr = await userCursor.next()
+            try {
+              await WIKI.models.users.createNewUser({
+                providerKey: usr.provider,
+                email: usr.email,
+                name: usr.name,
+                passwordRaw: usr.password,
+                mustChangePassword: false,
+                sendWelcomeEmail: false
+              })
+              usersCount++
+            } catch (err) {
+              failed.push({
+                provider: usr.provider,
+                email: usr.email,
+                error: err.message
+              })
+              WIKI.logger.warn(`${usr.email}: ${err}`)
+            }
+          }
+
+          client.close()
+          return {
+            responseResult: graphHelper.generateSuccess('Import completed.'),
+            usersCount: usersCount,
+            groupsCount: groupsCount,
+            failed: failed
+          }
+        } else {
+          throw new Error('MongoDB Connection String is missing or invalid.')
+        }
+      } catch (err) {
+        return graphHelper.generateError(err)
+      }
     }
   },
   SystemInfo: {

+ 24 - 0
server/graph/schemas/system.graphql

@@ -35,6 +35,11 @@ type SystemMutation {
   ): DefaultResponse @auth(requires: ["manage:system"])
 
   performUpgrade: DefaultResponse @auth(requires: ["manage:system"])
+
+  importUsersFromV1(
+    mongoDbConnString: String!
+    groupMode: SystemImportUsersGroupMode!
+  ): SystemImportUsersResponse @auth(requires:  ["manage:system"])
 }
 
 # -----------------------------------------------
@@ -73,3 +78,22 @@ type SystemInfo {
   usersTotal: Int @auth(requires: ["manage:system", "manage:navigation", "manage:groups", "write:groups", "manage:users", "write:users"])
   workingDirectory: String @auth(requires: ["manage:system"])
 }
+
+enum SystemImportUsersGroupMode {
+  MULTI
+  SINGLE
+  NONE
+}
+
+type SystemImportUsersResponse {
+  responseResult: ResponseStatus
+  usersCount: Int
+  groupsCount: Int
+  failed: [SystemImportUsersResponseFailed]
+}
+
+type SystemImportUsersResponseFailed {
+  provider: String
+  email: String
+  error: String
+}