Browse Source

feat: admin UI fixes + SAML strategy + dashboard gql

Nicolas Giard 6 years ago
parent
commit
5e109802c6

+ 18 - 2
client/components/admin.vue

@@ -1,7 +1,7 @@
 <template lang='pug'>
   v-app(:dark='darkMode').admin
     nav-header
-    v-navigation-drawer.pb-0(v-model='adminDrawerShown', app, fixed, clipped, left, permanent)
+    v-navigation-drawer.pb-0.admin-sidebar(v-model='adminDrawerShown', app, fixed, clipped, left, permanent)
       v-list(dense)
         v-list-tile.pt-2(to='/dashboard')
           v-list-tile-avatar: v-icon dashboard
@@ -71,7 +71,7 @@
           v-list-tile-avatar: v-icon favorite
           v-list-tile-title {{ $t('admin:contribute.title') }}
 
-    v-content
+    v-content(:class='darkMode ? "grey darken-4" : ""')
       transition(name='admin-router')
         router-view
 
@@ -138,4 +138,20 @@ export default {
   }
 }
 
+.admin-sidebar {
+  .v-list__tile--active {
+    background-color: rgba(mc('theme', 'primary'), .1);
+
+    .v-icon {
+      color: mc('theme', 'primary')
+    }
+  }
+}
+
+.theme--dark {
+  .admin-sidebar .v-list__tile--active {
+    background-color: rgba(0,0,0, .2);
+  }
+}
+
 </style>

+ 1 - 1
client/components/admin/admin-auth.vue

@@ -47,7 +47,7 @@
                 persistent-hint
                 :class='cfg.value.hint ? "mb-2" : ""'
               )
-              v-switch(
+              v-switch.mb-3(
                 v-else-if='cfg.value.type === "boolean"'
                 :key='cfg.key'
                 :label='cfg.value.title'

+ 85 - 11
client/components/admin/admin-dashboard.vue

@@ -6,68 +6,142 @@
     v-container(fluid, grid-list-lg)
       v-layout(row, wrap)
         v-flex(xs12 md6 lg4 xl3 d-flex)
-          v-card.primary(dark)
+          v-card.primary.dashboard-card(dark)
             v-card-text
               v-icon.dashboard-icon insert_drive_file
               .subheading Pages
               animated-number.display-1(
-                :value='357'
+                :value='info.pagesTotal'
                 :duration='2000'
                 :formatValue='round'
                 easing='easeOutQuint'
                 )
         v-flex(xs12 md6 lg4 xl3 d-flex)
-          v-card.indigo.lighten-1(dark)
+          v-card.indigo.lighten-1.dashboard-card(dark)
             v-card-text
               v-icon.dashboard-icon person
               .subheading Users
               animated-number.display-1(
-                :value='34'
+                :value='info.usersTotal'
                 :duration='2000'
                 :formatValue='round'
                 easing='easeOutQuint'
                 )
         v-flex(xs12 md6 lg4 xl3 d-flex)
-          v-card.indigo.lighten-2(dark)
+          v-card.indigo.lighten-2.dashboard-card(dark)
             v-card-text
               v-icon.dashboard-icon people
               .subheading Groups
               animated-number.display-1(
-                :value='5'
+                :value='info.groupsTotal'
                 :duration='2000'
                 :formatValue='round'
                 easing='easeOutQuint'
                 )
         v-flex(xs12 md6 lg12 xl3 d-flex)
-          v-card.teal.lighten-2(dark)
+          v-card.dashboard-card(
+            :class='isLatestVersion ? "teal lighten-2" : "red lighten-2"'
+            dark
+            )
+            v-btn(fab, absolute, right, top, small, light, to='system')
+              v-icon(v-if='isLatestVersion', color='teal') build
+              v-icon(v-else, color='red darken-4') get_app
             v-card-text
               v-icon.dashboard-icon blur_on
-              .subheading Wiki.js 2.0.0
-              .body-2 You are running the latest version.
+              .subheading Wiki.js {{info.currentVersion}}
+              .body-2(v-if='isLatestVersion') You are running the latest version.
+              .body-2(v-else) A new version is available: {{info.latestVersion}}
         v-flex(xs12)
           v-card
-            v-card-text ---
+            v-card-title.subheading Recent Pages
+            v-data-table.pb-2(
+              :items='recentPages'
+              hide-actions
+              hide-headers
+              )
+              template(slot='items' slot-scope='props')
+                td(width='20', style='padding-right: 0;'): v-icon insert_drive_file
+                td
+                  .body-2.primary--text {{ props.item.title }}
+                  .caption.grey--text.text--darken-2 {{ props.item.description }}
+                td.caption /{{ props.item.path }}
+                td.grey--text.text--darken-2(width='250')
+                  .caption: strong Updated {{ props.item.updatedAt | moment('from') }}
+                  .caption Created {{ props.item.createdAt | moment('calendar') }}
+        v-flex(xs12)
+          v-card
+            v-card-title.subheading Most Popular Pages
+            v-data-table.pb-2(
+              :items='popularPages'
+              hide-actions
+              hide-headers
+              )
+              template(slot='items' slot-scope='props')
+                td(width='20', style='padding-right: 0;'): v-icon insert_drive_file
+                td
+                  .body-2.primary--text {{ props.item.title }}
+                  .caption.grey--text.text--darken-2 {{ props.item.description }}
+                td.caption /{{ props.item.path }}
+                td.grey--text.text--darken-2(width='250')
+                  .caption: strong Updated {{ props.item.updatedAt | moment('from') }}
+                  .caption Created {{ props.item.createdAt | moment('calendar') }}
 
 </template>
 
 <script>
 import AnimatedNumber from 'animated-number-vue'
 
+import statsQuery from 'gql/admin/dashboard/dashboard-query-stats.gql'
+
 export default {
   components: {
     AnimatedNumber
   },
   data() {
-    return {}
+    return {
+      info: {
+        currentVersion: 'n/a',
+        latestVersion: 'n/a',
+        groupsTotal: 0,
+        pagesTotal: 0,
+        usersTotal: 0
+      },
+      recentPages: [],
+      popularPages: []
+    }
+  },
+  computed: {
+    isLatestVersion() {
+      return this.info.currentVersion === this.info.latestVersion
+    }
   },
   methods: {
     round(val) { return Math.round(val) }
+  },
+  apollo: {
+    info: {
+      query: statsQuery,
+      fetchPolicy: 'network-only',
+      update: (data) => data.system.info,
+      watchLoading (isLoading) {
+        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-system-refresh')
+      }
+    }
   }
 }
 </script>
 
 <style lang='scss'>
 
+.dashboard-card {
+  display: flex;
+
+  .v-card__text {
+    overflow: hidden;
+    position: relative;
+  }
+}
+
 .dashboard-icon {
   position: absolute;
   right: 0;

+ 24 - 15
client/components/admin/admin-rendering.vue

@@ -14,7 +14,7 @@
               )
               v-icon.mr-2 line_weight
               .subheading Pipeline
-            v-expansion-panel.adm-rendering-pipeline
+            v-expansion-panel.adm-rendering-pipeline(v-model='selectedCore')
               v-expansion-panel-content(
                 hide-actions
                 v-for='core in cores'
@@ -31,20 +31,21 @@
                   v-icon.mx-2 arrow_forward
                   .caption {{core.output}}
                 v-list(two-line, dense)
-                  v-list-tile(
-                    avatar
-                    v-for='rdr in core.children'
-                    :key='rdr.key'
-                    )
-                    v-list-tile-avatar
-                      v-icon(color='grey') {{rdr.icon}}
-                    v-list-tile-content
-                      v-list-tile-title {{rdr.title}}
-                      v-list-tile-sub-title {{rdr.description}}
-                    v-list-tile-avatar
-                      v-icon(color='green', small, v-if='rdr.isEnabled') lens
-                      v-icon(color='red', small, v-else) trip_origin
-                  v-divider.my-0
+                  template(v-for='(rdr, n) in core.children')
+                    v-list-tile(
+                      avatar
+                      :key='rdr.key'
+                      @click=''
+                      )
+                      v-list-tile-avatar
+                        v-icon(color='grey') {{rdr.icon}}
+                      v-list-tile-content
+                        v-list-tile-title {{rdr.title}}
+                        v-list-tile-sub-title {{rdr.description}}
+                      v-list-tile-avatar
+                        v-icon(color='green', small, v-if='rdr.isEnabled') lens
+                        v-icon(color='red', small, v-else) trip_origin
+                    v-divider.my-0(v-if='n < core.children.length - 1')
 
           v-flex(lg9 xs12)
             v-card
@@ -112,6 +113,7 @@ import renderersQuery from 'gql/admin/rendering/rendering-query-renderers.gql'
 export default {
   data() {
     return {
+      selectedCore: 0,
       linkify: true,
       codeTheme: 'Light',
       renderers: []
@@ -125,6 +127,13 @@ export default {
       })
     }
   },
+  watch: {
+    renderers(newValue, oldValue) {
+      _.delay(() => {
+        this.selectedCore = _.findIndex(this.cores, ['key', 'markdownCore'])
+      }, 500)
+    }
+  },
   apollo: {
     renderers: {
       query: renderersQuery,

+ 11 - 0
client/graph/admin/dashboard/dashboard-query-stats.gql

@@ -0,0 +1,11 @@
+query {
+  system {
+    info {
+      currentVersion
+      latestVersion
+      groupsTotal
+      pagesTotal
+      usersTotal
+    }
+  }
+}

+ 1 - 0
package.json

@@ -127,6 +127,7 @@
     "passport-oauth2": "1.4.0",
     "passport-okta-oauth": "0.0.1",
     "passport-openidconnect": "0.0.2",
+    "passport-saml": "0.35.0",
     "passport-slack": "0.0.7",
     "passport-twitch": "1.0.3",
     "passport-windowslive": "1.0.2",

+ 67 - 23
server/graph/resolvers/system.js

@@ -22,33 +22,77 @@ module.exports = {
     async system() { return {} }
   },
   SystemQuery: {
-    async info(obj, args, context, info) {
+    async info() { return {} }
+  },
+  SystemMutation: { },
+  SystemInfo: {
+    configFile() {
+      return path.join(process.cwd(), 'config.yml')
+    },
+    currentVersion() {
+      return WIKI.version
+    },
+    dbType() {
+      return _.get(dbTypes, WIKI.config.db.type, 'Unknown DB')
+    },
+    dbVersion() {
+      return _.get(WIKI.models, 'knex.client.version', 'Unknown version')
+    },
+    dbHost() {
+      return WIKI.config.db.host
+    },
+    latestVersion() {
+      return '2.0.0' // TODO
+    },
+    latestVersionReleaseDate() {
+      return new Date() // TODO
+    },
+    async operatingSystem() {
       let osLabel = `${os.type()} (${os.platform()}) ${os.release()} ${os.arch()}`
       if (os.platform() === 'linux') {
         const osInfo = await getos()
         osLabel = `${os.type()} - ${osInfo.dist} (${osInfo.codename || os.platform()}) ${osInfo.release || os.release()} ${os.arch()}`
       }
-
-      return {
-        configFile: path.join(process.cwd(), 'config.yml'),
-        currentVersion: WIKI.version,
-        dbType: _.get(dbTypes, WIKI.config.db.type, 'Unknown DB'),
-        dbVersion: _.get(WIKI.models, 'knex.client.version', 'Unknown version'),
-        dbHost: WIKI.config.db.host,
-        latestVersion: WIKI.version, // TODO
-        latestVersionReleaseDate: new Date(), // TODO
-        operatingSystem: osLabel,
-        hostname: os.hostname(),
-        cpuCores: os.cpus().length,
-        ramTotal: filesize(os.totalmem()),
-        workingDirectory: process.cwd(),
-        nodeVersion: process.version.substr(1),
-        redisVersion: WIKI.redis.serverInfo.redis_version,
-        redisUsedRAM: WIKI.redis.serverInfo.used_memory_human,
-        redisTotalRAM: _.get(WIKI.redis.serverInfo, 'total_system_memory_human', 'N/A'),
-        redisHost: WIKI.redis.options.host
-      }
+      return osLabel
+    },
+    hostname() {
+      return os.hostname()
+    },
+    cpuCores() {
+      return os.cpus().length
+    },
+    ramTotal() {
+      return filesize(os.totalmem())
+    },
+    workingDirectory() {
+      return process.cwd()
+    },
+    nodeVersion() {
+      return process.version.substr(1)
+    },
+    redisVersion() {
+      return WIKI.redis.serverInfo.redis_version
+    },
+    redisUsedRAM() {
+      return WIKI.redis.serverInfo.used_memory_human
+    },
+    redisTotalRAM() {
+      return _.get(WIKI.redis.serverInfo, 'total_system_memory_human', 'N/A')
+    },
+    redisHost() {
+      return WIKI.redis.options.host
+    },
+    async groupsTotal() {
+      const total = await WIKI.models.groups.query().count('* as total').first().pluck('total')
+      return _.toSafeInteger(total)
+    },
+    async pagesTotal() {
+      const total = await WIKI.models.pages.query().count('* as total').first().pluck('total')
+      return _.toSafeInteger(total)
+    },
+    async usersTotal() {
+      const total = await WIKI.models.users.query().count('* as total').first().pluck('total')
+      return _.toSafeInteger(total)
     }
-  },
-  SystemMutation: { }
+  }
 }

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

@@ -37,15 +37,18 @@ type SystemInfo {
   dbHost: String
   dbType: String
   dbVersion: String
+  groupsTotal: Int
   hostname: String
   latestVersion: String
   latestVersionReleaseDate: Date
   nodeVersion: String
   operatingSystem: String
+  pagesTotal: Int
   ramTotal: String
   redisHost: String
   redisTotalRAM: String
   redisUsedRAM: String
   redisVersion: String
+  usersTotal: Int
   workingDirectory: String
 }

+ 40 - 0
server/modules/authentication/saml/authentication.js

@@ -0,0 +1,40 @@
+const _ = require('lodash')
+
+/* global WIKI */
+
+// ------------------------------------
+// SAML Account
+// ------------------------------------
+
+const SAMLStrategy = require('passport-saml').Strategy
+
+module.exports = {
+  init (passport, conf) {
+    passport.use('saml',
+      new SAMLStrategy({
+        callbackURL: conf.callbackURL,
+        entryPoint: conf.entryPoint,
+        issuer: conf.issuer,
+        audience: conf.audience,
+        cert: _.split(conf.cert, '|'),
+        privateCert: conf.privateCert,
+        decryptionPvk: conf.decryptionPvk,
+        signatureAlgorithm: conf.signatureAlgorithm,
+        identifierFormat: conf.identifierFormat,
+        acceptedClockSkewMs: _.toSafeInteger(conf.acceptedClockSkewMs),
+        disableRequestedAuthnContext: conf.disableRequestedAuthnContext,
+        authnContext: conf.authnContext,
+        forceAuthn: conf.forceAuthn,
+        providerName: conf.providerName,
+        skipRequestCompression: conf.skipRequestCompression,
+        authnRequestBinding: conf.authnRequestBinding
+      }, (profile, cb) => {
+        WIKI.models.users.processProfile(profile).then((user) => {
+          return cb(null, user) || true
+        }).catch((err) => {
+          return cb(err, null) || true
+        })
+      })
+    )
+  }
+}

+ 83 - 0
server/modules/authentication/saml/definition.yml

@@ -0,0 +1,83 @@
+key: saml
+title: SAML 2.0
+description: Security Assertion Markup Language 2.0 (SAML 2.0) is a version of the SAML standard for exchanging authentication and authorization data between security domains.
+author: requarks.io
+logo: https://static.requarks.io/logo/saml.svg
+website: https://wiki.oasis-open.org/security/FrontPage
+useForm: false
+props:
+  entryPoint:
+    type: String
+    title: Entry Point
+    hint: Identity provider entrypoint (URL)
+  issuer:
+    type: String
+    title: Issuer
+    hint: Issuer string to supply to Identity Provider
+  audience:
+    type: String
+    title: Audience
+    hint: Expected SAML response Audience (if not provided, Audience won't be verified)
+  cert:
+    type: String
+    title: Certificate
+    hint: Public PEM-encoded X.509 signing certificate contents in base64 (e.g. 'MIICizCCAfQCCQCY8tKaMc0BMjANBgkqh ... W=='). If the provider has multiple certificates that are valid, join them together using the | pipe symbol.
+  privateCert:
+    type: String
+    title: Private Certificate
+    hint: PEM formatted key used to sign the certificate.
+  decryptionPvk:
+    type: String
+    title: Decryption Private Key
+    hint: (optional) Private key that will be used to attempt to decrypt any encrypted assertions that are received.
+  signatureAlgorithm:
+    type: String
+    title: Signature Algorithm
+    hint: Signature algorithm used for signing requests
+    default: sha1
+    enum:
+      - sha1
+      - sha256
+      - sha512
+  identifierFormat:
+    type: String
+    title: Name Identifier format
+    default: 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress'
+  acceptedClockSkewMs:
+    type: Number
+    title: Accepted Clock Skew Milleseconds
+    hint: Time in milliseconds of skew that is acceptable between client and server when checking OnBefore and NotOnOrAfter assertion condition validity timestamps. Setting to -1 will disable checking these conditions entirely.
+    default: 0
+  disableRequestedAuthnContext:
+    type: Boolean
+    title: Disable Requested Auth Context
+    hint: If enabled, do not request a specific authentication context. This is known to help when authenticating against Active Directory (AD FS) servers.
+    default: false
+  authnContext:
+    type: String
+    title: Auth Context
+    hint: Name identifier format to request auth context.
+    default: urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport
+  forceAuthn:
+    type: Boolean
+    title: Force Initial Re-authentication
+    hint: If enabled, the initial SAML request from the service provider specifies that the IdP should force re-authentication of the user, even if they possess a valid session.
+    default: false
+  providerName:
+    type: String
+    title: Provider Name
+    hint: Optional human-readable name of the requester for use by the presenter's user agent or the identity provider.
+    default: wiki.js
+  skipRequestCompression:
+    type: Boolean
+    title: Skip Request Compression
+    hint: If enabled, the SAML request from the service provider won't be compressed.
+    default: false
+  authnRequestBinding:
+    type: String
+    title: Request Binding
+    hint: Binding used for request authentication from IDP.
+    default: 'HTTP-Redirect'
+    enum:
+      - HTTP-Redirect
+      - HTTP-POST

File diff suppressed because it is too large
+ 36 - 576
yarn.lock


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