浏览代码

feat: user menu + jwt certs + UI fixes

Nicolas Giard 6 年之前
父节点
当前提交
f856da074e

+ 2 - 0
client/client-app.js

@@ -48,6 +48,8 @@ window.Hammer = Hammer
 
 moment.locale(siteConfig.lang)
 
+store.commit('user/REFRESH_AUTH')
+
 // ====================================
 // Initialize Apollo Client (GraphQL)
 // ====================================

+ 12 - 1
client/components/admin.vue

@@ -155,6 +155,12 @@ export default {
 
 <style lang='scss'>
 
+.admin {
+  &.theme--light {
+    background-color: lighten(mc('grey', '200'), 2%);
+  }
+}
+
 .admin-router {
   &-enter-active, &-leave-active {
     transition: opacity .25s ease;
@@ -173,7 +179,7 @@ export default {
     background-color: rgba(mc('theme', 'primary'), .1);
 
     .v-icon {
-      color: mc('theme', 'primary')
+      color: mc('theme', 'primary');
     }
   }
 }
@@ -181,6 +187,11 @@ export default {
 .theme--dark {
   .admin-sidebar .v-list__tile--active {
     background-color: rgba(0,0,0, .2);
+    color: mc('blue', '500') !important;
+
+    .v-icon {
+      color: mc('blue', '500');
+    }
   }
 }
 

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

@@ -67,7 +67,7 @@
                         )
 
             v-tab-item(v-for='(strategy, n) in activeStrategies', :key='strategy.key', :transition='false', :reverse-transition='false')
-              v-card.pa-3(flat, tile)
+              v-card.wiki-form.pa-3(flat, tile)
                 v-form
                   .authlogo
                     img(:src='strategy.logo', :alt='strategy.title')

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

@@ -27,7 +27,7 @@
                   v-radio-group(v-model='selectedEditor')
                     v-radio(v-for='(editor, n) in editors', :key='n', :label='editor.text', :value='editor.value', color='primary')
             v-tab-item(key='code', :transition='false', :reverse-transition='false')
-              v-card.pa-3(flat, tile)
+              v-card.wiki-form.pa-3(flat, tile)
                 v-form
                   v-subheader Editor Configuration
                   .body-1.ml-3 This editor has no configuration options you can modify.

+ 5 - 10
client/components/admin/admin-general.vue

@@ -15,7 +15,7 @@
           v-layout(row wrap)
             v-flex(lg6 xs12)
               v-form
-                v-card
+                v-card.wiki-form
                   v-toolbar(color='primary', dark, dense, flat)
                     v-toolbar-title
                       .subheading {{ $t('admin:general.siteInfo') }}
@@ -23,7 +23,6 @@
                   .px-3.pb-3
                     v-text-field(
                       outline
-                      background-color='grey lighten-2'
                       label='Site Title'
                       required
                       :counter='50'
@@ -35,21 +34,18 @@
                   .px-3.pb-3
                     v-text-field(
                       outline
-                      background-color='grey lighten-2'
                       label='Site Description'
                       :counter='255'
                       prepend-icon='public'
                       )
                     v-text-field(
                       outline
-                      background-color='grey lighten-2'
                       label='Site Keywords'
                       :counter='255'
                       prepend-icon='public'
                       )
                     v-select(
                       outline
-                      background-color='grey lighten-2'
                       label='Meta Robots'
                       chips
                       tags
@@ -62,7 +58,6 @@
                   .px-3.pb-3
                     v-text-field(
                       outline
-                      background-color='grey lighten-2'
                       label='Google Analytics ID'
                       :counter='255'
                       prepend-icon='public'
@@ -74,7 +69,6 @@
                   .px-3.pb-3
                     v-text-field(
                       outline
-                      background-color='grey lighten-2'
                       label='Company / Organization Name'
                       v-model='company'
                       :counter='255'
@@ -83,7 +77,7 @@
                       hint='Name to use when displaying copyright notice in the footer. Leave empty to hide.'
                       )
             v-flex(lg6 xs12)
-              v-card
+              v-card.wiki-form
                 v-toolbar(color='primary', dark, dense, flat)
                   v-toolbar-title
                     .subheading {{ $t('admin:general.siteBranding') }}
@@ -116,7 +110,7 @@
                     hint='Uncheck this box if you don\'t want Henry, Wiki.js mascot, to be displayed on client-facing pages.'
                     )
 
-              v-card.mt-3
+              v-card.wiki-form.mt-3
                 v-toolbar(color='primary', dark, dense, flat)
                   v-toolbar-title
                     .subheading Features
@@ -141,7 +135,7 @@
 
 <script>
 
-import { sync } from 'vuex-pathify'
+import { get, sync } from 'vuex-pathify'
 
 export default {
   data() {
@@ -155,6 +149,7 @@ export default {
     }
   },
   computed: {
+    darkMode: get('site/dark'),
     siteTitle: sync('site/title'),
     company: sync('site/company')
   },

+ 3 - 3
client/components/admin/admin-locale.vue

@@ -14,14 +14,13 @@
         v-form.pt-3
           v-layout(row wrap)
             v-flex(lg6 xs12)
-              v-card
+              v-card.wiki-form
                 v-toolbar(color='primary', dark, dense, flat)
                   v-toolbar-title
                     .subheading {{ $t('admin:locale.settings') }}
                 v-card-text
                   v-select(
                     outline
-                    background-color='grey lighten-2'
                     :items='installedLocales'
                     prepend-icon='language'
                     v-model='selectedLocale'
@@ -49,7 +48,7 @@
                     :hint='namespacing ? $t("admin:locale.autoUpdate.hintWithNS") : $t("admin:locale.autoUpdate.hint")'
                   )
 
-              v-card.mt-3
+              v-card.wiki-form.mt-3
                 v-toolbar(color='primary', dark, dense, flat)
                   v-toolbar-title
                     .subheading {{ $t('admin:locale.namespacing') }}
@@ -71,6 +70,7 @@
                     .caption.grey--text {{ $t('admin:locale.namespacingPrefixWarning.subtitle') }}
                   v-divider.mt-3.mb-4
                   v-select(
+                    outline
                     :disabled='!namespacing'
                     :items='installedLocales'
                     prepend-icon='language'

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

@@ -37,7 +37,7 @@
                   )
 
             v-tab-item(v-for='(logger, n) in activeLoggers', :key='logger.key', :transition='false', :reverse-transition='false')
-              v-card.pa-3(flat, tile)
+              v-card.wiki-form.pa-3(flat, tile)
                 v-form
                   .loggerlogo
                     img(:src='logger.logo', :alt='logger.title')

+ 17 - 6
client/components/admin/admin-navigation.vue

@@ -61,40 +61,51 @@
                         v-list-tile-avatar: v-icon power_input
                         v-list-tile-title {{$t('navigation.divider')}}
             v-flex
-              v-card(v-if='current.kind === "link"')
+              v-card.wiki-form(v-if='current.kind === "link"')
                 v-toolbar(dense, color='blue', flat, dark)
                   .subheading {{$t('navigation.edit', { kind: $t('navigation.link') })}}
                 v-card-text
                   v-text-field(
                     outline
-                    background-color='grey lighten-2'
                     :label='$t("navigation.label")'
                     prepend-icon='title'
                     v-model='current.label'
                   )
                   v-text-field(
                     outline
-                    background-color='grey lighten-2'
                     :label='$t("navigation.icon")'
                     prepend-icon='casino'
                     v-model='current.icon'
                   )
                   v-select(
                     outline
-                    background-color='grey lighten-2'
                     :label='$t("navigation.targetType")'
                     prepend-icon='near_me'
                     :items='navTypes'
                     v-model='current.targetType'
                   )
                   v-text-field(
-                    v-if='current.targetType === "external"'
+                    v-if='current.targetType === `external`'
                     outline
-                    background-color='grey lighten-2'
                     :label='$t("navigation.target")'
                     prepend-icon='near_me'
                     v-model='current.target'
                   )
+                  v-btn(
+                    v-else-if='current.targetType === "page"'
+                    color='indigo'
+                    dark
+                    )
+                    v-icon(left) search
+                    span Select Page...
+                  v-text-field(
+                    v-else-if='current.targetType === `search`'
+                    outline
+                    :label='$t("navigation.navType.searchQuery")'
+                    prepend-icon='search'
+                    v-model='current.target'
+                  )
+
                 v-card-chin
                   v-spacer
                   v-btn(color='red', outline, @click='deleteItem(current)')

+ 1 - 3
client/components/admin/admin-rendering.vue

@@ -60,7 +60,7 @@
                 v-divider.my-0(v-if='n < core.children.length - 1')
 
       v-flex(lg9, xs12)
-        v-card
+        v-card.wiki-form
           v-toolbar(
             color='grey darken-1'
             dark
@@ -84,7 +84,6 @@
               v-select(
                 v-if='cfg.value.type === "string" && cfg.value.enum'
                 outline
-                background-color='grey lighten-2'
                 :items='cfg.value.enum'
                 :key='cfg.key'
                 :label='cfg.value.title'
@@ -105,7 +104,6 @@
               v-text-field(
                 v-else
                 outline
-                background-color='grey lighten-2'
                 :key='cfg.key'
                 :label='cfg.value.title'
                 v-model='cfg.value.value'

+ 2 - 2
client/components/admin/admin-theme.vue

@@ -14,7 +14,7 @@
         v-form.pt-3
           v-layout(row wrap)
             v-flex(lg6 xs12)
-              v-card
+              v-card.wiki-form
                 v-toolbar(color='primary', dark, dense, flat)
                   v-toolbar-title
                     .subheading Theme
@@ -43,7 +43,7 @@
                     persistent-hint
                     hint='Not recommended for accessibility. May not be supported by all themes.'
                     )
-              v-card.mt-3
+              v-card.wiki-form.mt-3
                 v-toolbar(color='primary', dark, dense, flat)
                   v-toolbar-title
                     .subheading Code Injection

+ 69 - 8
client/components/common/criterias-item.vue

@@ -1,7 +1,14 @@
 <template lang="pug">
   .criterias-item
     //- Type
-    v-select(solo, :items='filteredCriteriaTypes', v-model='item.type', placeholder='Rule Type', ref='typeSelect')
+    v-select(
+      solo
+      :items='filteredCriteriaTypes'
+      v-model='item.type'
+      placeholder='Rule Type'
+      ref='typeSelect'
+      hide-details
+      )
       template(slot='item', slot-scope='data')
         v-list-tile-avatar
           v-avatar(:color='data.item.color', size='40', tile): v-icon(color='white') {{ data.item.icon }}
@@ -10,7 +17,15 @@
           v-list-tile-sub-title.caption(v-html='data.item.description')
 
     //- Operator
-    v-select(solo, :items='filteredCriteriaOperators', v-model='item.operator', placeholder='Operator', :disabled='!item.type', :class='!item.type ? "blue-grey lighten-4" : ""')
+    v-select(
+      solo
+      :items='filteredCriteriaOperators'
+      v-model='item.operator'
+      placeholder='Operator'
+      :disabled='!item.type'
+      :class='!item.type ? "blue-grey lighten-4" : ""'
+      hide-details
+      )
       template(slot='item', slot-scope='data')
         v-list-tile-avatar
           v-avatar.white--text(color='blue', size='30', tile) {{ data.item.icon }}
@@ -18,12 +33,58 @@
           v-list-tile-title(v-html='data.item.text')
 
     //- Value
-    v-select(v-if='item.type === "country"', solo, :items='countries', v-model='item.value', placeholder='Countries...', multiple, item-text='name', item-value='code')
-    v-text-field(v-else-if='item.type === "path"', solo, v-model='item.value', label='Path (e.g. /section)')
-    v-text-field(v-else-if='item.type === "date"', solo, @click.native.stop='dateActivator = true', v-model='item.value', label='YYYY-MM-DD', readonly)
-    v-text-field(v-else-if='item.type === "time"', solo, @click.native.stop='timeActivator = true', v-model='item.value', label='HH:MM', readonly)
-    v-select(v-else-if='item.type === "group"', solo, :items='groups', v-model='item.value', placeholder='Group...', item-text='name', item-value='id')
-    v-text-field.blue-grey.lighten-4(v-else, solo, disabled)
+    v-select(
+      v-if='item.type === "country"'
+      solo
+      :items='countries'
+      v-model='item.value'
+      placeholder='Countries...'
+      multiple
+      item-text='name'
+      item-value='code'
+      hide-details
+      )
+    v-text-field(
+      v-else-if='item.type === "path"'
+      solo
+      v-model='item.value'
+      label='Path (e.g. /section)'
+      hide-details
+      )
+    v-text-field(
+      v-else-if='item.type === "date"'
+      solo
+      @click.native.stop='dateActivator = true'
+      v-model='item.value'
+      label='YYYY-MM-DD'
+      readonly
+      hide-details
+      )
+    v-text-field(
+      v-else-if='item.type === "time"'
+      solo
+      @click.native.stop='timeActivator = true'
+      v-model='item.value'
+      label='HH:MM'
+      readonly
+      hide-details
+      )
+    v-select(
+      v-else-if='item.type === "group"'
+      solo
+      :items='groups'
+      v-model='item.value'
+      placeholder='Group...'
+      item-text='name'
+      item-value='id'
+      hide-details
+      )
+    v-text-field.blue-grey.lighten-4(
+      v-else
+      solo
+      disabled
+      hide-details
+      )
 
     v-dialog(lazy, v-model='dateActivator', width='290px', ref='dateDialog')
       v-date-picker(v-model='item.value', scrollable, color='primary')

+ 60 - 17
client/components/common/nav-header.vue

@@ -87,29 +87,46 @@
       icon
       )
       v-icon(color='grey') search
-    v-tooltip(bottom)
+    v-tooltip(bottom, v-if='isAuthenticated && isAdmin')
       v-btn.btn-animate-rotate(icon, href='/a', slot='activator')
         v-icon(color='grey') settings
       span Admin
     v-menu(offset-y, min-width='300')
       v-tooltip(bottom, slot='activator')
-        v-btn.btn-animate-grow(icon, slot='activator', outline, color='grey darken-3')
+        v-btn.btn-animate-grow(icon, slot='activator', outline, :color='isAuthenticated ? `blue` : `grey darken-3`')
           v-icon(color='grey') account_circle
         span Account
-      v-list.py-0(:light='!$vuetify.dark')
-        v-list-tile.py-3(avatar)
-          v-list-tile-avatar
-            v-avatar.red(:size='40'): span.white--text.subheading JD
-          v-list-tile-content
-            v-list-tile-title John Doe
-            v-list-tile-sub-title john.doe@example.com
-        v-divider.my-0
-        v-list-tile(href='/p')
-          v-list-tile-action: v-icon(color='red') person
-          v-list-tile-title Profile
-        v-list-tile(@click='logout')
-          v-list-tile-action: v-icon(color='red') exit_to_app
-          v-list-tile-title Logout
+      v-list.py-0
+        template(v-if='isAuthenticated')
+          v-list-tile.py-3.grey(avatar, :class='$vuetify.dark ? `darken-4-l5` : `lighten-5`')
+            v-list-tile-avatar
+              v-avatar.blue(v-if='picture.kind === `initials`', :size='40')
+                span.white--text.subheading {{picture.initials}}
+              v-avatar(v-else-if='picture.kind === `image`', :size='40')
+                v-img(:src='picture.url')
+            v-list-tile-content
+              v-list-tile-title {{name}}
+              v-list-tile-sub-title {{email}}
+          v-divider.my-0
+          v-list-tile(href='/w')
+            v-list-tile-action: v-icon(color='blue') web
+            v-list-tile-title My Wiki
+          v-divider.my-0
+          v-list-tile(href='/p')
+            v-list-tile-action: v-icon(color='blue') person
+            v-list-tile-title Profile
+          v-divider.my-0
+          v-list-tile(@click='logout')
+            v-list-tile-action: v-icon(color='red') exit_to_app
+            v-list-tile-title Logout
+        template(v-else)
+          v-list-tile(href='/login')
+            v-list-tile-action: v-icon(color='grey') person
+            v-list-tile-title Login
+          v-divider.my-0
+          v-list-tile(href='/register')
+            v-list-tile-action: v-icon(color='grey') person_add
+            v-list-tile-title Register
 
     page-selector(mode='create', v-model='newPageModal')
 </template>
@@ -143,7 +160,33 @@ export default {
     isLoading: get('isLoading'),
     title: get('site/title'),
     path: get('page/path'),
-    mode: get('page/mode')
+    mode: get('page/mode'),
+    name: get('user/name'),
+    email: get('user/email'),
+    pictureUrl: get('user/pictureUrl'),
+    isAuthenticated: get('user/authenticated'),
+    permissions: get('user/permissions'),
+    picture() {
+      if (this.pictureUrl && this.pictureUrl.length > 1) {
+        return {
+          kind: 'image',
+          url: this.pictureUrl
+        }
+      } else {
+        const nameParts = this.name.toUpperCase().split(' ')
+        let initials = _.head(nameParts).charAt(0)
+        if (nameParts.length > 1) {
+          initials += _.last(nameParts).charAt(0)
+        }
+        return {
+          kind: 'initials',
+          initials
+        }
+      }
+    },
+    isAdmin() {
+      return _.includes(this.permissions, 'manage:system')
+    }
   },
   created() {
     if (this.hideSearch || this.dense || this.$vuetify.breakpoint.smAndDown) {

+ 1 - 1
client/components/editor/editor-modal-properties.vue

@@ -25,7 +25,7 @@
           v-list-tile
             v-list-tile-avatar: v-icon delete
             v-list-tile-title Delete Page
-    v-card(tile)
+    v-card.wiki-form(tile)
       v-card-text
         v-subheader.pl-0 Page Info
         v-text-field(

+ 1 - 1
client/scss/app.scss

@@ -11,7 +11,7 @@
 @import 'components/v-btn';
 @import 'components/v-data-table';
 @import 'components/v-dialog';
-@import 'components/vue-tree-navigation';
+@import 'components/v-form';
 
 @import 'layout/md2';
 

+ 32 - 0
client/scss/components/v-form.scss

@@ -0,0 +1,32 @@
+.wiki-form {
+
+  &.theme--light {
+    background-color: mc('grey', '50');
+  }
+
+  .v-text-field--outline {
+    .v-input__slot {
+      background-color: #FFF !important;
+      border-color: mc('grey', '300') !important;
+      border-radius: 7px;
+
+      @at-root .theme--dark & {
+        background-color: lighten(mc('grey', '900'), 5%) !important;
+        border-color: mc('grey', '700') !important;
+
+        .v-label.v-label--active.primary--text {
+          color: mc('blue', '500') !important;
+        }
+      }
+    }
+    &.v-input--is-focused .v-input__slot {
+      border-color: mc('blue', '500') !important;
+    }
+
+    @at-root .theme--dark & {
+      .v-icon.primary--text {
+        color: mc('blue', '500') !important;
+      }
+    }
+  }
+}

+ 0 - 27
client/scss/components/vue-tree-navigation.scss

@@ -1,27 +0,0 @@
-.TreeNavigation.treenav {
-  font-size: 13px;
-
-  li {
-    padding-left: 24px;
-  }
-  a {
-    text-decoration: none;
-    color: mc('grey', '800');
-  }
-
-  .NavigationLevel__parent {
-    // font-weight:    600;
-  }
-
-  .NavigationItem {
-    padding: 3px 0;
-  }
-
-  .NavigationItem--active {
-    color: #42b883;
-  }
-
-  .NavigationToggle__icon {
-    border-color: mc('blue', '600');
-  }
-}

+ 3 - 1
client/store/index.js

@@ -6,6 +6,7 @@ import { make } from 'vuex-pathify' // eslint-disable-line import/no-duplicates
 
 import page from './page'
 import site from './site'
+import user from './user'
 
 Vue.use(Vuex)
 
@@ -51,6 +52,7 @@ export default new Vuex.Store({
   actions: { },
   modules: {
     page,
-    site
+    site,
+    user
   }
 })

+ 44 - 0
client/store/user.js

@@ -0,0 +1,44 @@
+import { make } from 'vuex-pathify'
+import jwt from 'jsonwebtoken'
+import Cookies from 'js-cookie'
+
+const state = {
+  id: 0,
+  email: '',
+  name: '',
+  pictureUrl: '',
+  localeCode: '',
+  defaultEditor: '',
+  permissions: [],
+  iat: 0,
+  exp: 0,
+  authenticated: false
+}
+
+export default {
+  namespaced: true,
+  state,
+  mutations: {
+    ...make.mutations(state),
+    REFRESH_AUTH(state) {
+      const jwtCookie = Cookies.get('jwt')
+      if (jwtCookie) {
+        try {
+          const jwtData = jwt.decode(jwtCookie)
+          state.id = jwtData.id
+          state.email = jwtData.email
+          state.name = jwtData.name
+          state.pictureUrl = jwtData.pictureUrl
+          state.localeCode = jwtData.localeCode
+          state.defaultEditor = jwtData.defaultEditor
+          state.permissions = jwtData.permissions
+          state.iat = jwtData.iat
+          state.exp = jwtData.exp
+          state.authenticated = true
+        } catch (err) {
+          console.debug('Invalid JWT. Silent authentication skipped.')
+        }
+      }
+    }
+  }
+}

+ 9 - 14
client/themes/default/components/page.vue

@@ -83,20 +83,15 @@
               )
               .pb-2.caption.grey--text 5 votes
           v-divider
-          v-list.grey(dense, :class='darkMode ? `darken-3-d3` : `lighten-3`')
-            v-subheader.pl-4.teal--text Tags
-            v-list-tile
-              v-list-tile-avatar: v-icon(color='teal') label
-              v-list-tile-title Astrophysics
-            v-divider(inset)
-            v-list-tile
-              v-list-tile-avatar: v-icon(color='teal') label
-              v-list-tile-title Space
-            v-divider(inset)
-            v-list-tile
-              v-list-tile-avatar: v-icon(color='teal') label
-              v-list-tile-title Planets
-          v-divider
+          template(v-if='tags.length')
+            v-list.grey(dense, :class='darkMode ? `darken-3-d3` : `lighten-3`')
+              v-subheader.pl-4.teal--text Tags
+              template(v-for='(tag, idx) in tags')
+                v-list-tile(:href='`/t/` + tag.slug')
+                  v-list-tile-avatar: v-icon(color='teal') label
+                  v-list-tile-title {{tag.title}}
+                v-divider(inset, v-if='idx < tags.length - 1')
+            v-divider
           v-toolbar(:color='darkMode ? `grey darken-3` : `grey lighten-4`', flat, dense)
             v-spacer
             v-tooltip(bottom)

+ 1 - 0
package.json

@@ -136,6 +136,7 @@
     "passport-slack": "0.0.7",
     "passport-twitch": "1.0.3",
     "passport-windowslive": "1.0.2",
+    "pem-jwk": "1.5.1",
     "pg": "7.6.1",
     "pg-hstore": "2.3.2",
     "pm2": "3.2.2",

+ 10 - 94
server/controllers/auth.js

@@ -1,35 +1,7 @@
 /* global WIKI */
 
-const Promise = require('bluebird')
 const express = require('express')
 const router = express.Router()
-const ExpressBrute = require('express-brute')
-const ExpressBruteRedisStore = require('express-brute-redis')
-const jwt = require('jsonwebtoken')
-const moment = require('moment')
-const _ = require('lodash')
-
-/**
- * Setup Express-Brute
- */
-const EBstore = new ExpressBruteRedisStore({
-  client: WIKI.redis
-})
-const bruteforce = new ExpressBrute(EBstore, {
-  freeRetries: 5,
-  minWait: 60 * 1000,
-  maxWait: 5 * 60 * 1000,
-  refreshTimeoutOnRequest: false,
-  failCallback (req, res, next, nextValidRequestDate) {
-    req.flash('alert', {
-      class: 'error',
-      title: WIKI.lang.t('auth:errors.toomanyattempts'),
-      message: WIKI.lang.t('auth:errors.toomanyattemptsmsg', { time: moment(nextValidRequestDate).fromNow() }),
-      iconClass: 'fa-times'
-    })
-    res.redirect('/login')
-  }
-})
 
 /**
  * Login form
@@ -38,72 +10,6 @@ router.get('/login', function (req, res, next) {
   res.render('login')
 })
 
-router.post('/login', bruteforce.prevent, function (req, res, next) {
-  new Promise((resolve, reject) => {
-    // [1] LOCAL AUTHENTICATION
-    WIKI.auth.passport.authenticate('local', { session: false }, function (err, user, info) {
-      if (err) { return reject(err) }
-      if (!user) { return reject(new Error('INVALID_LOGIN')) }
-      resolve(user)
-    })(req, res, next)
-  }).catch({ message: 'INVALID_LOGIN' }, err => {
-    if (_.has(WIKI.config.auth.strategy, 'ldap')) {
-      // [2] LDAP AUTHENTICATION
-      return new Promise((resolve, reject) => {
-        WIKI.auth.passport.authenticate('ldapauth', { session: false }, function (err, user, info) {
-          if (err) { return reject(err) }
-          if (info && info.message) { return reject(new Error(info.message)) }
-          if (!user) { return reject(new Error('INVALID_LOGIN')) }
-          resolve(user)
-        })(req, res, next)
-      })
-    } else {
-      throw err
-    }
-  }).then((user) => {
-    // LOGIN SUCCESS
-    return req.logIn(user, { session: false }, function (err) {
-      if (err) { return next(err) }
-      req.brute.reset(function () {
-        return res.redirect('/')
-      })
-    })
-  }).catch(err => {
-    // LOGIN FAIL
-    if (err.message === 'INVALID_LOGIN') {
-      req.flash('alert', {
-        title: WIKI.lang.t('auth:errors.invalidlogin'),
-        message: WIKI.lang.t('auth:errors.invalidloginmsg')
-      })
-      return res.redirect('/login')
-    } else {
-      req.flash('alert', {
-        title: WIKI.lang.t('auth:errors.loginerror'),
-        message: err.message
-      })
-      return res.redirect('/login')
-    }
-  })
-})
-
-/**
- * Social Login
- */
-
-router.get('/login/ms', WIKI.auth.passport.authenticate('windowslive', { scope: ['wl.signin', 'wl.basic', 'wl.emails'] }))
-router.get('/login/google', WIKI.auth.passport.authenticate('google', { scope: ['profile', 'email'] }))
-router.get('/login/facebook', WIKI.auth.passport.authenticate('facebook', { scope: ['public_profile', 'email'] }))
-router.get('/login/github', WIKI.auth.passport.authenticate('github', { scope: ['user:email'] }))
-router.get('/login/slack', WIKI.auth.passport.authenticate('slack', { scope: ['identity.basic', 'identity.email'] }))
-router.get('/login/azure', WIKI.auth.passport.authenticate('azure_ad_oauth2'))
-
-router.get('/login/ms/callback', WIKI.auth.passport.authenticate('windowslive', { failureRedirect: '/login', successRedirect: '/' }))
-router.get('/login/google/callback', WIKI.auth.passport.authenticate('google', { failureRedirect: '/login', successRedirect: '/' }))
-router.get('/login/facebook/callback', WIKI.auth.passport.authenticate('facebook', { failureRedirect: '/login', successRedirect: '/' }))
-router.get('/login/github/callback', WIKI.auth.passport.authenticate('github', { failureRedirect: '/login', successRedirect: '/' }))
-router.get('/login/slack/callback', WIKI.auth.passport.authenticate('slack', { failureRedirect: '/login', successRedirect: '/' }))
-router.get('/login/azure/callback', WIKI.auth.passport.authenticate('azure_ad_oauth2', { failureRedirect: '/login', successRedirect: '/' }))
-
 /**
  * Logout
  */
@@ -112,4 +18,14 @@ router.get('/logout', function (req, res) {
   res.redirect('/')
 })
 
+/**
+ * JWT Public Endpoints
+ */
+router.get('/.well-known/jwk.json', function (req, res, next) {
+  res.json(WIKI.config.certs.jwk)
+})
+router.get('/.well-known/jwk.pem', function (req, res, next) {
+  res.send(WIKI.config.certs.public)
+})
+
 module.exports = router

+ 1 - 1
server/core/auth.js

@@ -45,7 +45,7 @@ module.exports = {
       // Load JWT
       passport.use('jwt', new passportJWT.Strategy({
         jwtFromRequest: securityHelper.extractJWT,
-        secretOrKey: WIKI.config.sessionSecret,
+        secretOrKey: WIKI.config.certs.public,
         audience: 'urn:wiki.js', // TODO: use value from admin
         issuer: 'urn:wiki.js'
       }, (jwtPayload, cb) => {

+ 5 - 1
server/models/users.js

@@ -253,7 +253,11 @@ module.exports = class User extends Model {
         localeCode: user.localeCode,
         defaultEditor: user.defaultEditor,
         permissions: ['manage:system']
-      }, WIKI.config.sessionSecret, {
+      }, {
+        key: WIKI.config.certs.private,
+        passphrase: WIKI.config.sessionSecret
+      }, {
+        algorithm: 'RS256',
         expiresIn: '30m',
         audience: 'urn:wiki.js', // TODO: use value from admin
         issuer: 'urn:wiki.js'

+ 24 - 1
server/setup.js

@@ -25,6 +25,7 @@ module.exports = () => {
   const _ = require('lodash')
   const cfgHelper = require('./helpers/config')
   const crypto = Promise.promisifyAll(require('crypto'))
+  const pem2jwk = require('pem-jwk').pem2jwk
 
   // ----------------------------------------
   // Define Express App
@@ -90,6 +91,7 @@ module.exports = () => {
       }
 
       // Create directory structure
+      WIKI.logger.info('Creating data directories...')
       const dataPath = path.join(process.cwd(), 'data')
       await fs.ensureDir(dataPath)
       await fs.ensureDir(path.join(dataPath, 'cache'))
@@ -110,6 +112,26 @@ module.exports = () => {
       _.set(WIKI.config, 'theming.darkMode', false)
       _.set(WIKI.config, 'title', 'Wiki.js')
 
+      // Generate certificates
+      WIKI.logger.info('Generating certificates...')
+      const certs = crypto.generateKeyPairSync('rsa', {
+        modulusLength: 2048,
+        publicKeyEncoding: {
+          type: 'pkcs1',
+          format: 'pem'
+        },
+        privateKeyEncoding: {
+          type: 'pkcs1',
+          format: 'pem',
+          cipher: 'aes-256-cbc',
+          passphrase: WIKI.config.sessionSecret
+        }
+      })
+
+      _.set(WIKI.config, 'certs.jwk', pem2jwk(certs.publicKey))
+      _.set(WIKI.config, 'certs.public', certs.publicKey)
+      _.set(WIKI.config, 'certs.private', certs.privateKey)
+
       // Save config to DB
       WIKI.logger.info('Persisting config to DB...')
       await WIKI.configSvc.saveToDb([
@@ -120,7 +142,8 @@ module.exports = () => {
         'sessionSecret',
         'telemetry',
         'theming',
-        'title'
+        'title',
+        'certs'
       ])
 
       // Create default locale

+ 22 - 0
yarn.lock

@@ -2280,6 +2280,16 @@ asap@~2.0.3:
   resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46"
   integrity sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=
 
+asn1.js@1.0.3:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-1.0.3.tgz#281ba3ec1f2448fe765f92a4eecf883fe1364b54"
+  integrity sha1-KBuj7B8kSP52X5Kk7s+IP+E2S1Q=
+  dependencies:
+    inherits "^2.0.1"
+    minimalistic-assert "^1.0.0"
+  optionalDependencies:
+    bn.js "^1.0.0"
+
 asn1.js@^4.0.0:
   version "4.10.1"
   resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-4.10.1.tgz#b9c2bf5805f1e64aadeed6df3a2bfafb5a73f5a0"
@@ -2793,6 +2803,11 @@ bluebird@^3.1.1, bluebird@^3.3.4, bluebird@^3.4.1, bluebird@^3.5.0, bluebird@^3.
   resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.1.tgz#d9551f9de98f1fcda1e683d17ee91a0602ee2eb9"
   integrity sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA==
 
+bn.js@^1.0.0:
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-1.3.0.tgz#0db4cbf96f8f23b742f5bcb9d1aa7a9994a05e83"
+  integrity sha1-DbTL+W+PI7dC9by50ap6mZSgXoM=
+
 bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0:
   version "4.11.8"
   resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f"
@@ -10718,6 +10733,13 @@ pbkdf2@^3.0.3:
     safe-buffer "^5.0.1"
     sha.js "^2.4.8"
 
+pem-jwk@1.5.1:
+  version "1.5.1"
+  resolved "https://registry.yarnpkg.com/pem-jwk/-/pem-jwk-1.5.1.tgz#7a8637fd2f67a827e57c0c42e1c23c3fd52cfb01"
+  integrity sha1-eoY3/S9nqCflfAxC4cI8P9Us+wE=
+  dependencies:
+    asn1.js "1.0.3"
+
 pend@~1.2.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50"