Selaa lähdekoodia

feat: assets move + comments migration + admin users UI

NGPixel 5 vuotta sitten
vanhempi
sitoutus
f6bad765a2
65 muutettua tiedostoa jossa 270 lisäystä ja 213 poistoa
  1. 1 1
      client/components/admin/admin-analytics.vue
  2. 1 1
      client/components/admin/admin-api.vue
  3. 7 5
      client/components/admin/admin-auth.vue
  4. 7 5
      client/components/admin/admin-comments.vue
  5. 6 6
      client/components/admin/admin-contribute.vue
  6. 2 2
      client/components/admin/admin-dashboard.vue
  7. 1 1
      client/components/admin/admin-dev-flags.vue
  8. 1 1
      client/components/admin/admin-editor.vue
  9. 1 1
      client/components/admin/admin-extensions.vue
  10. 41 44
      client/components/admin/admin-general.vue
  11. 1 1
      client/components/admin/admin-groups-edit.vue
  12. 1 1
      client/components/admin/admin-groups.vue
  13. 1 1
      client/components/admin/admin-locale.vue
  14. 1 1
      client/components/admin/admin-logging.vue
  15. 1 1
      client/components/admin/admin-mail.vue
  16. 5 5
      client/components/admin/admin-navigation.vue
  17. 1 1
      client/components/admin/admin-pages-edit.vue
  18. 1 1
      client/components/admin/admin-pages-visualize.vue
  19. 1 1
      client/components/admin/admin-pages.vue
  20. 1 1
      client/components/admin/admin-rendering.vue
  21. 1 1
      client/components/admin/admin-search.vue
  22. 1 1
      client/components/admin/admin-security.vue
  23. 1 1
      client/components/admin/admin-ssl.vue
  24. 1 1
      client/components/admin/admin-storage.vue
  25. 1 1
      client/components/admin/admin-system.vue
  26. 1 1
      client/components/admin/admin-tags.vue
  27. 1 1
      client/components/admin/admin-theme.vue
  28. 15 2
      client/components/admin/admin-users-create.vue
  29. 1 1
      client/components/admin/admin-users-edit.vue
  30. 42 11
      client/components/admin/admin-users.vue
  31. 1 1
      client/components/admin/admin-utilities-importv1.vue
  32. 1 1
      client/components/admin/admin-utilities.vue
  33. 1 1
      client/components/admin/admin-webhooks.vue
  34. 3 3
      client/components/common/search-results.vue
  35. 2 2
      client/components/editor/editor-api.vue
  36. 1 1
      client/components/editor/editor-markdown.vue
  37. 3 3
      client/components/editor/editor-modal-editorselect.vue
  38. 1 1
      client/components/new-page.vue
  39. 1 1
      client/components/not-found.vue
  40. 1 1
      client/components/profile/pages.vue
  41. 1 1
      client/components/profile/profile.vue
  42. 3 3
      client/components/tags.vue
  43. 1 1
      client/components/unauthorized.vue
  44. 0 12
      client/graph/admin/users/users-query-list.gql
  45. 0 12
      client/graph/admin/users/users-query-strategies.gql
  46. 10 10
      client/scss/fonts/arabic.scss
  47. 14 14
      client/scss/fonts/default.scss
  48. 1 1
      client/static/browserconfig.xml
  49. 2 2
      client/static/manifest.json
  50. 4 3
      client/themes/default/components/page.vue
  51. 7 7
      dev/templates/master.pug
  52. 1 6
      dev/webpack/webpack.dev.js
  53. 8 8
      dev/webpack/webpack.prod.js
  54. 11 0
      server/db/migrations-sqlite/2.4.36.js
  55. 11 0
      server/db/migrations/2.4.36.js
  56. 1 1
      server/graph/resolvers/user.js
  57. 2 0
      server/graph/schemas/user.graphql
  58. 1 0
      server/helpers/common.js
  59. 1 1
      server/master.js
  60. 6 0
      server/modules/authentication/ldap/definition.yml
  61. 8 5
      server/modules/authentication/okta/definition.yml
  62. 2 0
      server/modules/authentication/saml/definition.yml
  63. 1 1
      server/modules/comments/commento/definition.yml
  64. 11 7
      server/modules/comments/default/definition.yml
  65. 1 1
      server/modules/comments/disqus/definition.yml

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

@@ -3,7 +3,7 @@
     v-layout(row, wrap)
       v-flex(xs12)
         .admin-header
-          img.animated.fadeInUp(src='/svg/icon-line-chart.svg', alt='Analytics', style='width: 80px;')
+          img.animated.fadeInUp(src='/_assets/svg/icon-line-chart.svg', alt='Analytics', style='width: 80px;')
           .admin-header-title
             .headline.primary--text.animated.fadeInLeft {{ $t('admin:analytics.title') }}
             .subtitle-1.grey--text.animated.fadeInLeft.wait-p4s {{ $t('admin:analytics.subtitle') }}

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

@@ -3,7 +3,7 @@
     v-layout(row, wrap)
       v-flex(xs12)
         .admin-header
-          img.animated.fadeInUp(src='/svg/icon-rest-api.svg', alt='API', style='width: 80px;')
+          img.animated.fadeInUp(src='/_assets/svg/icon-rest-api.svg', alt='API', style='width: 80px;')
           .admin-header-title
             .headline.primary--text.animated.fadeInLeft {{$t('admin:api.title')}}
             .subtitle-1.grey--text.animated.fadeInLeft {{$t('admin:api.subtitle')}}

+ 7 - 5
client/components/admin/admin-auth.vue

@@ -3,7 +3,7 @@
     v-layout(row, wrap)
       v-flex(xs12)
         .admin-header
-          img.animated.fadeInUp(src='/svg/icon-unlock.svg', alt='Authentication', style='width: 80px;')
+          img.animated.fadeInUp(src='/_assets/svg/icon-unlock.svg', alt='Authentication', style='width: 80px;')
           .admin-header-title
             .headline.primary--text.animated.fadeInLeft {{ $t('admin:auth.title') }}
             .subtitle-1.grey--text.animated.fadeInLeft.wait-p4s {{ $t('admin:auth.subtitle') }}
@@ -92,7 +92,7 @@
               .overline.my-5 {{$t('admin:auth.strategyConfiguration')}}
               .body-2.ml-3(v-if='!strategy.config || strategy.config.length < 1'): em {{$t('admin:auth.strategyNoConfiguration')}}
               template(v-else, v-for='cfg in strategy.config')
-                v-select(
+                v-select.mb-3(
                   v-if='cfg.value.type === "string" && cfg.value.enum'
                   outlined
                   :items='cfg.value.enum'
@@ -103,8 +103,9 @@
                   :hint='cfg.value.hint ? cfg.value.hint : ""'
                   persistent-hint
                   :class='cfg.value.hint ? "mb-2" : ""'
+                  :style='cfg.value.maxWidth > 0 ? `max-width:` + cfg.value.maxWidth + `px;` : ``'
                 )
-                v-switch.mb-3(
+                v-switch.mb-6(
                   v-else-if='cfg.value.type === "boolean"'
                   :key='cfg.key'
                   :label='cfg.value.title'
@@ -115,7 +116,7 @@
                   persistent-hint
                   inset
                   )
-                v-textarea(
+                v-textarea.mb-3(
                   v-else-if='cfg.value.type === "string" && cfg.value.multiline'
                   outlined
                   :key='cfg.key'
@@ -126,7 +127,7 @@
                   persistent-hint
                   :class='cfg.value.hint ? "mb-2" : ""'
                   )
-                v-text-field(
+                v-text-field.mb-3(
                   v-else
                   outlined
                   :key='cfg.key'
@@ -136,6 +137,7 @@
                   :hint='cfg.value.hint ? cfg.value.hint : ""'
                   persistent-hint
                   :class='cfg.value.hint ? "mb-2" : ""'
+                  :style='cfg.value.maxWidth > 0 ? `max-width:` + cfg.value.maxWidth + `px;` : ``'
                   )
               v-divider.mt-3
               .overline.my-5 {{$t('admin:auth.registration')}}

+ 7 - 5
client/components/admin/admin-comments.vue

@@ -3,7 +3,7 @@
     v-layout(row, wrap)
       v-flex(xs12)
         .admin-header
-          img.animated.fadeInUp(src='/svg/icon-chat-bubble.svg', alt='Comments', style='width: 80px;')
+          img.animated.fadeInUp(src='/_assets/svg/icon-chat-bubble.svg', alt='Comments', style='width: 80px;')
           .admin-header-title
             .headline.primary--text.animated.fadeInLeft {{$t('admin:comments.title')}}
             .subtitle-1.grey--text.animated.fadeInLeft.wait-p2s {{$t('admin:comments.subtitle')}}
@@ -45,7 +45,7 @@
             .overline.my-5 {{$t('admin:comments.providerConfig')}}
             .body-2.ml-3(v-if='!provider.config || provider.config.length < 1'): em {{$t('admin:comments.providerNoConfig')}}
             template(v-else, v-for='cfg in provider.config')
-              v-select(
+              v-select.mb-3(
                 v-if='cfg.value.type === "string" && cfg.value.enum'
                 outlined
                 :items='cfg.value.enum'
@@ -56,8 +56,9 @@
                 :hint='cfg.value.hint ? cfg.value.hint : ""'
                 persistent-hint
                 :class='cfg.value.hint ? "mb-2" : ""'
+                :style='cfg.value.maxWidth > 0 ? `max-width:` + cfg.value.maxWidth + `px;` : ``'
               )
-              v-switch.mb-3(
+              v-switch.mb-6(
                 v-else-if='cfg.value.type === "boolean"'
                 :key='cfg.key'
                 :label='cfg.value.title'
@@ -68,7 +69,7 @@
                 persistent-hint
                 inset
                 )
-              v-textarea(
+              v-textarea.mb-3(
                 v-else-if='cfg.value.type === "string" && cfg.value.multiline'
                 outlined
                 :key='cfg.key'
@@ -79,7 +80,7 @@
                 persistent-hint
                 :class='cfg.value.hint ? "mb-2" : ""'
                 )
-              v-text-field(
+              v-text-field.mb-3(
                 v-else
                 outlined
                 :key='cfg.key'
@@ -89,6 +90,7 @@
                 :hint='cfg.value.hint ? cfg.value.hint : ""'
                 persistent-hint
                 :class='cfg.value.hint ? "mb-2" : ""'
+                :style='cfg.value.maxWidth > 0 ? `max-width:` + cfg.value.maxWidth + `px;` : ``'
                 )
 </template>
 

+ 6 - 6
client/components/admin/admin-contribute.vue

@@ -3,7 +3,7 @@
     v-layout(row, wrap)
       v-flex(xs12)
         .admin-header
-          img.animated.fadeInUp(src='/svg/icon-heart-health.svg', alt='Contribute', style='width: 80px;')
+          img.animated.fadeInUp(src='/_assets/svg/icon-heart-health.svg', alt='Contribute', style='width: 80px;')
           .admin-header-title
             .headline.primary--text.animated.fadeInLeft {{ $t('admin:contribute.title') }}
             .subtitle-1.grey--text.animated.fadeInLeft.wait-p4s {{ $t('admin:contribute.subtitle') }}
@@ -30,19 +30,19 @@
                 v-icon.my-1(size='24') mdi-github-circle
               v-tab
                 span Patreon
-                img.my-1(src='/svg/icon-patreon.svg', style='height: 24px;')
+                img.my-1(src='/_assets/svg/icon-patreon.svg', style='height: 24px;')
               v-tab
                 span OpenCollective
-                img.my-1(src='/svg/icon-opencollective.svg', style='height: 24px;')
+                img.my-1(src='/_assets/svg/icon-opencollective.svg', style='height: 24px;')
               v-tab
                 span PayPal
-                img.my-1(src='/svg/icon-paypal.svg', style='height: 24px;')
+                img.my-1(src='/_assets/svg/icon-paypal.svg', style='height: 24px;')
               v-tab
                 span Ethereum
-                img.my-1(src='/svg/icon-ethereum.svg', style='height: 24px;')
+                img.my-1(src='/_assets/svg/icon-ethereum.svg', style='height: 24px;')
               v-tab
                 span T-Shirts
-                img.my-1(src='/svg/icon-t-shirt.svg', style='height: 24px;')
+                img.my-1(src='/_assets/svg/icon-t-shirt.svg', style='height: 24px;')
               v-tab-item(:transition='false', :reverse-transition='false')
                 .body-2.pa-3 {{ $t('admin:contribute.github') }}
                 a.ml-3(href='https://github.com/users/NGPixel/sponsorship', :title='$t(`admin:contribute.becomeASponsor`)')

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

@@ -3,7 +3,7 @@
     v-layout(row, wrap)
       v-flex(xs12)
         .admin-header
-          img.animated.fadeInUp(src='/svg/icon-browse-page.svg', alt='Dashboard', style='width: 80px;')
+          img.animated.fadeInUp(src='/_assets/svg/icon-browse-page.svg', alt='Dashboard', style='width: 80px;')
           .admin-header-title
             .headline.primary--text.animated.fadeInLeft {{ $t('admin:dashboard.title') }}
             .subtitle-1.grey--text.animated.fadeInLeft.wait-p2s {{ $t('admin:dashboard.subtitle') }}
@@ -95,7 +95,7 @@
       v-flex(xs12)
         v-card.dashboard-contribute.animated.fadeInUp.wait-p4s
           v-card-text
-            img(src='/svg/icon-heart-health.svg', alt='Contribute', style='height: 80px;')
+            img(src='/_assets/svg/icon-heart-health.svg', alt='Contribute', style='height: 80px;')
             .pl-5
               .subtitle-1 {{$t('admin:contribute.title')}}
               .body-2.mt-3: strong {{$t('admin:dashboard.contributeSubtitle')}}

+ 1 - 1
client/components/admin/admin-dev-flags.vue

@@ -3,7 +3,7 @@
     v-layout(row, wrap)
       v-flex(xs12)
         .admin-header
-          img(src='/svg/icon-console.svg', alt='Developer Tools', style='width: 80px;')
+          img(src='/_assets/svg/icon-console.svg', alt='Developer Tools', style='width: 80px;')
           .admin-header-title
             .headline.primary--text Developer Tools
             .subtitle-1.grey--text Flags

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

@@ -3,7 +3,7 @@
     v-layout(row, wrap)
       v-flex(xs12)
         .admin-header
-          img(src='/svg/icon-web-design.svg', alt='Editor', style='width: 80px;')
+          img(src='/_assets/svg/icon-web-design.svg', alt='Editor', style='width: 80px;')
           .admin-header-title
             .headline.primary--text Editor
             .subtitle-1.grey--text Configure the content editors #[v-chip(label, color='primary', small).white--text coming soon]

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

@@ -3,7 +3,7 @@
     v-layout(row wrap)
       v-flex(xs12)
         .admin-header
-          img.animated.fadeInUp(src='/svg/icon-installing-updates.svg', alt='Extensions', style='width: 80px;')
+          img.animated.fadeInUp(src='/_assets/svg/icon-installing-updates.svg', alt='Extensions', style='width: 80px;')
           .admin-header-title
             .headline.primary--text.animated.fadeInLeft {{ $t('admin:extensions.title') }}
             .subtitle-1.grey--text.animated.fadeInLeft {{ $t('admin:extensions.subtitle') }}

+ 41 - 44
client/components/admin/admin-general.vue

@@ -3,7 +3,7 @@
     v-layout(row wrap)
       v-flex(xs12)
         .admin-header
-          img.animated.fadeInUp(src='/svg/icon-categorize.svg', alt='General', style='width: 80px;')
+          img.animated.fadeInUp(src='/_assets/svg/icon-categorize.svg', alt='General', style='width: 80px;')
           .admin-header-title
             .headline.primary--text.animated.fadeInLeft {{ $t('admin:general.title') }}
             .subtitle-1.grey--text.animated.fadeInLeft {{ $t('admin:general.subtitle') }}
@@ -111,41 +111,39 @@
               v-card.animated.fadeInUp.wait-p4s
                 v-toolbar(color='indigo', dark, dense, flat)
                   v-toolbar-title.subtitle-1 Features
-                  v-spacer
-                  v-chip(label, color='white', small).indigo--text coming soon
                 v-card-text
-                  v-switch(
-                    inset
-                    label='Asset Image Optimization'
-                    color='indigo'
-                    v-model='config.featureTinyPNG'
-                    persistent-hint
-                    hint='Image optimization tool to reduce filesize and bandwidth costs.'
-                    disabled
-                    )
-                  v-text-field.mt-3(
-                    outlined
-                    label='TinyPNG API Key'
-                    :counter='255'
-                    v-model='config.description'
-                    prepend-icon='mdi-subdirectory-arrow-right'
-                    hint='Get your API key at https://tinypng.com/developers'
-                    persistent-hint
-                    disabled
-                    )
+                  //- v-switch(
+                  //-   inset
+                  //-   label='Asset Image Optimization'
+                  //-   color='indigo'
+                  //-   v-model='config.featureTinyPNG'
+                  //-   persistent-hint
+                  //-   hint='Image optimization tool to reduce filesize and bandwidth costs.'
+                  //-   disabled
+                  //-   )
+                  //- v-text-field.mt-3(
+                  //-   outlined
+                  //-   label='TinyPNG API Key'
+                  //-   :counter='255'
+                  //-   v-model='config.description'
+                  //-   prepend-icon='mdi-subdirectory-arrow-right'
+                  //-   hint='Get your API key at https://tinypng.com/developers'
+                  //-   persistent-hint
+                  //-   disabled
+                  //-   )
 
-                  v-divider.mt-3
-                  v-switch(
-                    inset
-                    label='Page Ratings'
-                    color='indigo'
-                    v-model='config.featurePageRatings'
-                    persistent-hint
-                    hint='Allow users to rate pages.'
-                    disabled
-                    )
+                  //- v-divider.mt-3
+                  //- v-switch(
+                  //-   inset
+                  //-   label='Page Ratings'
+                  //-   color='indigo'
+                  //-   v-model='config.featurePageRatings'
+                  //-   persistent-hint
+                  //-   hint='Allow users to rate pages.'
+                  //-   disabled
+                  //-   )
 
-                  v-divider.mt-3
+                  //- v-divider.mt-3
                   v-switch(
                     inset
                     label='Page Comments'
@@ -153,19 +151,18 @@
                     v-model='config.featurePageComments'
                     persistent-hint
                     hint='Allow users to leave comments on pages.'
-                    disabled
                     )
 
-                  v-divider.mt-3
-                  v-switch(
-                    inset
-                    label='Personal Wikis'
-                    color='indigo'
-                    v-model='config.featurePersonalWikis'
-                    persistent-hint
-                    hint='Allow users to have their own personal wiki.'
-                    disabled
-                    )
+                  //- v-divider.mt-3
+                  //- v-switch(
+                  //-   inset
+                  //-   label='Personal Wikis'
+                  //-   color='indigo'
+                  //-   v-model='config.featurePersonalWikis'
+                  //-   persistent-hint
+                  //-   hint='Allow users to have their own personal wiki.'
+                  //-   disabled
+                  //-   )
 
     component(:is='activeModal')
 

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

@@ -3,7 +3,7 @@
     v-layout(row wrap)
       v-flex(xs12)
         .admin-header
-          img(src='/svg/icon-social-group.svg', alt='Edit Group', style='width: 80px;')
+          img(src='/_assets/svg/icon-social-group.svg', alt='Edit Group', style='width: 80px;')
           .admin-header-title
             .headline.blue--text.text--darken-2 Edit Group
             .subtitle-1.grey--text {{group.name}}

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

@@ -3,7 +3,7 @@
     v-layout(row wrap)
       v-flex(xs12)
         .admin-header
-          img.animated.fadeInUp(src='/svg/icon-people.svg', alt='Groups', style='width: 80px;')
+          img.animated.fadeInUp(src='/_assets/svg/icon-people.svg', alt='Groups', style='width: 80px;')
           .admin-header-title
             .headline.blue--text.text--darken-2.animated.fadeInLeft Groups
             .subtitle-1.grey--text.animated.fadeInLeft.wait-p4s Manage groups and their permissions

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

@@ -3,7 +3,7 @@
     v-layout(row, wrap)
       v-flex(xs12)
         .admin-header
-          img.animated.fadeInUp(src='/svg/icon-globe-earth.svg', alt='Locale', style='width: 80px;')
+          img.animated.fadeInUp(src='/_assets/svg/icon-globe-earth.svg', alt='Locale', style='width: 80px;')
           .admin-header-title
             .headline.primary--text.animated.fadeInLeft {{ $t('admin:locale.title') }}
             .subtitle-1.grey--text.animated.fadeInLeft.wait-p4s {{ $t('admin:locale.subtitle') }}

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

@@ -3,7 +3,7 @@
     v-layout(row, wrap)
       v-flex(xs12)
         .admin-header
-          img(src='/svg/icon-registry-editor.svg', alt='Logging', style='width: 80px;')
+          img(src='/_assets/svg/icon-registry-editor.svg', alt='Logging', style='width: 80px;')
           .admin-header-title
             .headline.primary--text Logging
             .subtitle-1.grey--text Configure the system logger(s) #[v-chip(label, color='primary', small).white--text coming soon]

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

@@ -3,7 +3,7 @@
     v-layout(row, wrap)
       v-flex(xs12)
         .admin-header
-          img.animated.fadeInUp(src='/svg/icon-new-post.svg', alt='Mail', style='width: 80px;')
+          img.animated.fadeInUp(src='/_assets/svg/icon-new-post.svg', alt='Mail', style='width: 80px;')
           .admin-header-title
             .headline.primary--text.animated.fadeInLeft {{ $t('admin:mail.title') }}
             .subtitle-1.grey--text.animated.fadeInLeft.wait-p4s {{ $t('admin:mail.subtitle') }}

+ 5 - 5
client/components/admin/admin-navigation.vue

@@ -3,7 +3,7 @@
     v-layout(row wrap)
       v-flex(xs12)
         .admin-header
-          img.animated.fadeInUp(src='/svg/icon-triangle-arrow.svg', alt='Navigation', style='width: 80px;')
+          img.animated.fadeInUp(src='/_assets/svg/icon-triangle-arrow.svg', alt='Navigation', style='width: 80px;')
           .admin-header-title
             .headline.primary--text.animated.fadeInLeft {{$t('navigation.title')}}
             .subtitle-1.grey--text.animated.fadeInLeft.wait-p4s {{$t('navigation.subtitle')}}
@@ -23,7 +23,7 @@
                   v-list-item-group(v-model='config.mode', mandatory, :color='$vuetify.theme.dark ? `teal lighten-3` : `teal`')
                     v-list-item(value='TREE')
                       v-list-item-avatar
-                        img(src='/svg/icon-tree-structure-dotted.svg', alt='Site Tree')
+                        img(src='/_assets/svg/icon-tree-structure-dotted.svg', alt='Site Tree')
                       v-list-item-content
                         v-list-item-title {{$t('admin:navigation.modeSiteTree.title')}}
                         v-list-item-subtitle {{$t('admin:navigation.modeSiteTree.description')}}
@@ -32,7 +32,7 @@
                         v-icon(v-else, :color='config.mode === `TREE` ? `teal` : `grey lighten-3`') mdi-check-circle
                     v-list-item(value='MIXED')
                       v-list-item-avatar
-                        img(src='/svg/icon-user-menu-male-dotted.svg', alt='Custom Navigation')
+                        img(src='/_assets/svg/icon-user-menu-male-dotted.svg', alt='Custom Navigation')
                       v-list-item-content
                         v-list-item-title {{$t('admin:navigation.modeCustom.title')}}
                         v-list-item-subtitle {{$t('admin:navigation.modeCustom.description')}}
@@ -41,7 +41,7 @@
                         v-icon(v-else, :color='config.mode === `MIXED` ? `teal` : `grey lighten-3`') mdi-check-circle
                     v-list-item(value='STATIC')
                       v-list-item-avatar
-                        img(src='/svg/icon-features-list.svg', alt='Static Navigation')
+                        img(src='/_assets/svg/icon-features-list.svg', alt='Static Navigation')
                       v-list-item-content
                         v-list-item-title {{$t('admin:navigation.modeStatic.title')}}
                         v-list-item-subtitle {{$t('admin:navigation.modeStatic.description')}}
@@ -50,7 +50,7 @@
                         v-icon(v-else, :color='config.mode === `STATIC` ? `teal` : `grey lighten-3`') mdi-check-circle
                     v-list-item(value='NONE')
                       v-list-item-avatar
-                        img(src='/svg/icon-cancel-dotted.svg', alt='None')
+                        img(src='/_assets/svg/icon-cancel-dotted.svg', alt='None')
                       v-list-item-content
                         v-list-item-title {{$t('admin:navigation.modeNone.title')}}
                         v-list-item-subtitle {{$t('admin:navigation.modeNone.description')}}

+ 1 - 1
client/components/admin/admin-pages-edit.vue

@@ -3,7 +3,7 @@
     v-layout(row, wrap, v-if='page.id')
       v-flex(xs12)
         .admin-header
-          img.animated.fadeInUp(src='/svg/icon-view-details.svg', alt='Edit Page', style='width: 80px;')
+          img.animated.fadeInUp(src='/_assets/svg/icon-view-details.svg', alt='Edit Page', style='width: 80px;')
           .admin-header-title
             .headline.blue--text.text--darken-2.animated.fadeInLeft Page Details
             .subtitle-1.grey--text.animated.fadeInLeft.wait-p2s

+ 1 - 1
client/components/admin/admin-pages-visualize.vue

@@ -3,7 +3,7 @@
     v-layout(row wrap)
       v-flex(xs12)
         .admin-header
-          img.animated.fadeInUp(src='/svg/icon-venn-diagram.svg', alt='Visualize Pages', style='width: 80px;')
+          img.animated.fadeInUp(src='/_assets/svg/icon-venn-diagram.svg', alt='Visualize Pages', style='width: 80px;')
           .admin-header-title
             .headline.blue--text.text--darken-2.animated.fadeInLeft Visualize Pages
             .subtitle-1.grey--text.animated.fadeInLeft.wait-p2s Dendrogram representation of your pages

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

@@ -3,7 +3,7 @@
     v-layout(row wrap)
       v-flex(xs12)
         .admin-header
-          img.animated.fadeInUp(src='/svg/icon-file.svg', alt='Page', style='width: 80px;')
+          img.animated.fadeInUp(src='/_assets/svg/icon-file.svg', alt='Page', style='width: 80px;')
           .admin-header-title
             .headline.blue--text.text--darken-2.animated.fadeInLeft Pages
             .subtitle-1.grey--text.animated.fadeInLeft.wait-p2s Manage pages

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

@@ -3,7 +3,7 @@
     v-layout(row, wrap)
       v-flex(xs12)
         .admin-header
-          img.animated.fadeInUp(src='/svg/icon-process.svg', alt='Rendering', style='width: 80px;')
+          img.animated.fadeInUp(src='/_assets/svg/icon-process.svg', alt='Rendering', style='width: 80px;')
           .admin-header-title
             .headline.primary--text.animated.fadeInLeft Rendering
           v-spacer

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

@@ -3,7 +3,7 @@
     v-layout(row, wrap)
       v-flex(xs12)
         .admin-header
-          img.animated.fadeInUp(src='/svg/icon-search.svg', alt='Search Engine', style='width: 80px;')
+          img.animated.fadeInUp(src='/_assets/svg/icon-search.svg', alt='Search Engine', style='width: 80px;')
           .admin-header-title
             .headline.primary--text.animated.fadeInLeft {{$t('admin:search.title')}}
             .subtitle-1.grey--text.animated.fadeInLeft.wait-p2s {{$t('admin:search.subtitle')}}

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

@@ -3,7 +3,7 @@
     v-layout(row wrap)
       v-flex(xs12)
         .admin-header
-          img.animated.fadeInUp(src='/svg/icon-private.svg', alt='Security', style='width: 80px;')
+          img.animated.fadeInUp(src='/_assets/svg/icon-private.svg', alt='Security', style='width: 80px;')
           .admin-header-title
             .headline.primary--text.animated.fadeInLeft {{ $t('admin:security.title') }}
             .subtitle-1.grey--text.animated.fadeInLeft {{ $t('admin:security.subtitle') }}

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

@@ -3,7 +3,7 @@
     v-layout(row wrap)
       v-flex(xs12)
         .admin-header
-          img.animated.fadeInUp(src='/svg/icon-validation.svg', alt='SSL', style='width: 80px;')
+          img.animated.fadeInUp(src='/_assets/svg/icon-validation.svg', alt='SSL', style='width: 80px;')
           .admin-header-title
             .headline.primary--text.animated.fadeInLeft {{ $t('admin:ssl.title') }}
             .subtitle-1.grey--text.animated.fadeInLeft {{ $t('admin:ssl.subtitle') }}

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

@@ -3,7 +3,7 @@
     v-layout(row, wrap)
       v-flex(xs12)
         .admin-header
-          img.animated.fadeInUp(src='/svg/icon-cloud-storage.svg', alt='Storage', style='width: 80px;')
+          img.animated.fadeInUp(src='/_assets/svg/icon-cloud-storage.svg', alt='Storage', style='width: 80px;')
           .admin-header-title
             .headline.primary--text.animated.fadeInLeft {{$t('admin:storage.title')}}
             .subtitle-1.grey--text.animated.fadeInLeft.wait-p4s {{$t('admin:storage.subtitle')}}

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

@@ -3,7 +3,7 @@
     v-layout(row, wrap)
       v-flex(xs12)
         .admin-header
-          img.animated.fadeInUp(src='/svg/icon-tune.svg', alt='System Info', style='width: 80px;')
+          img.animated.fadeInUp(src='/_assets/svg/icon-tune.svg', alt='System Info', style='width: 80px;')
           .admin-header-title
             .headline.primary--text.animated.fadeInLeft {{ $t('admin:system.title') }}
             .subtitle-1.grey--text.animated.fadeInLeft.wait-p2s {{ $t('admin:system.subtitle') }}

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

@@ -3,7 +3,7 @@
     v-layout(row wrap)
       v-flex(xs12)
         .admin-header
-          img.animated.fadeInUp(src='/svg/icon-tags.svg', alt='Tags', style='width: 80px;')
+          img.animated.fadeInUp(src='/_assets/svg/icon-tags.svg', alt='Tags', style='width: 80px;')
           .admin-header-title
             .headline.primary--text.animated.fadeInLeft {{$t('tags.title')}}
             .subtitle-1.grey--text.animated.fadeInLeft.wait-p4s {{$t('tags.subtitle')}}

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

@@ -3,7 +3,7 @@
     v-layout(row wrap)
       v-flex(xs12)
         .admin-header
-          img.animated.fadeInUp(src='/svg/icon-paint-palette.svg', alt='Theme', style='width: 80px;')
+          img.animated.fadeInUp(src='/_assets/svg/icon-paint-palette.svg', alt='Theme', style='width: 80px;')
           .admin-header-title
             .headline.primary--text.animated.fadeInLeft {{$t('admin:theme.title')}}
             .subtitle-1.grey--text.animated.fadeInLeft.wait-p2s {{$t('admin:theme.subtitle')}}

+ 15 - 2
client/components/admin/admin-users-create.vue

@@ -91,9 +91,9 @@
 <script>
 import _ from 'lodash'
 import validate from 'validate.js'
+import gql from 'graphql-tag'
 
 import createUserMutation from 'gql/admin/users/users-mutation-create.gql'
-import providersQuery from 'gql/admin/users/users-query-strategies.gql'
 import groupsQuery from 'gql/admin/users/users-query-groups.gql'
 
 export default {
@@ -227,7 +227,20 @@ export default {
   },
   apollo: {
     providers: {
-      query: providersQuery,
+      query: gql`
+        query {
+          authentication {
+            strategies(
+              isEnabled: true
+            ) {
+              key
+              title
+              icon
+              color
+            }
+          }
+        }
+      `,
       fetchPolicy: 'network-only',
       update: (data) => data.authentication.strategies,
       watchLoading (isLoading) {

+ 1 - 1
client/components/admin/admin-users-edit.vue

@@ -3,7 +3,7 @@
     v-layout(row, wrap)
       v-flex(xs12)
         .admin-header
-          img.animated.fadeInUp(src='/svg/icon-male-user.svg', :alt='$t(`admin:users.edit`)', style='width: 80px;')
+          img.animated.fadeInUp(src='/_assets/svg/icon-male-user.svg', :alt='$t(`admin:users.edit`)', style='width: 80px;')
           .admin-header-title
             .headline.blue--text.text--darken-2.animated.fadeInLeft {{$t('admin:users.edit')}}
             .subtitle-1.grey--text.animated.fadeInLeft.wait-p2s {{user.name}}

+ 42 - 11
client/components/admin/admin-users.vue

@@ -3,7 +3,7 @@
     v-layout(row, wrap)
       v-flex(xs12)
         .admin-header
-          img.animated.fadeInUp(src='/svg/icon-customer.svg', alt='Users', style='width: 80px;')
+          img.animated.fadeInUp(src='/_assets/svg/icon-customer.svg', alt='Users', style='width: 80px;')
           .admin-header-title
             .headline.blue--text.text--darken-2.animated.fadeInLeft Users
             .subtitle-1.grey--text.animated.fadeInLeft.wait-p2s Manage users
@@ -55,10 +55,12 @@
                 td {{ props.item.providerKey }}
                 td {{ props.item.createdAt | moment('from') }}
                 td
-                  v-tooltip(left, v-if='props.item.isSystem')
-                    template(v-slot:activator='{ on }')
-                      v-icon(v-on='{ on }') mdi-lock-outline
-                    span System User
+                  span(v-if='props.item.lastLoginAt') {{ props.item.lastLoginAt | moment('from') }}
+                  em.grey--text(v-else) Never
+                td.text-right
+                  v-icon.mr-3(v-if='props.item.isSystem') mdi-lock-outline
+                  status-indicator(positive, pulse, v-if='props.item.isActive')
+                  status-indicator(negative, pulse, v-else)
             template(slot='no-data')
               .pa-3
                 v-alert.text-left(icon='mdi-alert', outlined, color='grey')
@@ -73,14 +75,14 @@
 
 <script>
 import _ from 'lodash'
+import gql from 'graphql-tag'
 
-import usersQuery from 'gql/admin/users/users-query-list.gql'
-import providersQuery from 'gql/admin/users/users-query-strategies.gql'
-
+import { StatusIndicator } from 'vue-status-indicator'
 import UserCreate from './admin-users-create.vue'
 
 export default {
   components: {
+    StatusIndicator,
     UserCreate
   },
   data() {
@@ -95,7 +97,8 @@ export default {
         { text: 'Email', value: 'email', sortable: true },
         { text: 'Provider', value: 'provider', sortable: true },
         { text: 'Created', value: 'createdAt', sortable: true },
-        { text: '', value: 'actions', sortable: false, width: 50 }
+        { text: 'Last Login', value: 'lastLoginAt', sortable: true },
+        { text: '', value: 'actions', sortable: false, width: 80 }
       ],
       strategies: [],
       filterStrategy: 'all',
@@ -127,7 +130,22 @@ export default {
   },
   apollo: {
     users: {
-      query: usersQuery,
+      query: gql`
+        query {
+          users {
+            list {
+              id
+              name
+              email
+              providerKey
+              isSystem
+              isActive
+              createdAt
+              lastLoginAt
+            }
+          }
+        }
+      `,
       fetchPolicy: 'network-only',
       update: (data) => data.users.list,
       watchLoading (isLoading) {
@@ -136,7 +154,20 @@ export default {
       }
     },
     strategies: {
-      query: providersQuery,
+      query: gql`
+        query {
+          authentication {
+            strategies(
+              isEnabled: true
+            ) {
+              key
+              title
+              icon
+              color
+            }
+          }
+        }
+      `,
       fetchPolicy: 'network-only',
       update: (data) => {
         return _.concat({

+ 1 - 1
client/components/admin/admin-utilities-importv1.vue

@@ -4,7 +4,7 @@
       .subtitle-1 {{ $t('admin:utilities.importv1Title') }}
     v-card-text
       .text-center
-        img.animated.fadeInUp.wait-p1s(src='/svg/icon-software.svg')
+        img.animated.fadeInUp.wait-p1s(src='/_assets/svg/icon-software.svg')
         .body-2 Import from Wiki.js 1.x
       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?

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

@@ -3,7 +3,7 @@
     v-layout(row, wrap)
       v-flex(xs12)
         .admin-header
-          img(src='/svg/icon-maintenance.svg', alt='Utilities', style='width: 80px;')
+          img(src='/_assets/svg/icon-maintenance.svg', alt='Utilities', style='width: 80px;')
           .admin-header-title
             .headline.primary--text {{$t('admin:utilities.title')}}
             .subtitle-1.grey--text {{$t('admin:utilities.subtitle')}}

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

@@ -3,7 +3,7 @@
     v-layout(row, wrap)
       v-flex(xs12)
         .admin-header
-          img.animated.fadeInUp(src='/svg/icon-winter.svg', alt='Mail', style='width: 80px;')
+          img.animated.fadeInUp(src='/_assets/svg/icon-winter.svg', alt='Mail', style='width: 80px;')
           .admin-header-title
             .headline.primary--text.animated.fadeInLeft {{ $t('admin:webhooks.title') }}
             .subtitle-1.grey--text.animated.fadeInLeft.wait-p4s {{ $t('admin:webhooks.subtitle') }}

+ 3 - 3
client/components/common/search-results.vue

@@ -2,7 +2,7 @@
   .search-results(v-if='searchIsFocused || (search && search.length > 1)')
     .search-results-container
       .search-results-help(v-if='!search || (search && search.length < 2)')
-        img(src='/svg/icon-search-alt.svg')
+        img(src='/_assets/svg/icon-search-alt.svg')
         .mt-4 {{$t('common:header.searchHint')}}
       .search-results-loader(v-else-if='searchIsLoading && (!results || results.length < 1)')
         orbit-spinner(
@@ -12,7 +12,7 @@
         )
         .headline.mt-5 {{$t('common:header.searchLoading')}}
       .search-results-none(v-else-if='!searchIsLoading && (!results || results.length < 1)')
-        img(src='/svg/icon-no-results.svg', alt='No Results')
+        img(src='/_assets/svg/icon-no-results.svg', alt='No Results')
         .subheading {{$t('common:header.searchNoResult')}}
       template(v-if='results && results.length > 0')
         v-subheader.white--text {{$t('common:header.searchResultsCount', { total: response.totalHits })}}
@@ -20,7 +20,7 @@
           template(v-for='(item, idx) of results')
             v-list-item(@click='goToPage(item)', :key='item.id', :class='idx === cursor ? `highlighted` : ``')
               v-list-item-avatar(tile)
-                img(src='/svg/icon-selective-highlighting.svg')
+                img(src='/_assets/svg/icon-selective-highlighting.svg')
               v-list-item-content
                 v-list-item-title(v-html='item.title')
                 v-list-item-subtitle.caption(v-html='item.description')

+ 2 - 2
client/components/editor/editor-api.vue

@@ -59,7 +59,7 @@
                       v-list-item-group(v-model='kind', mandatory, color='primary')
                         v-list-item(value='rest')
                           v-list-item-avatar
-                            img(src='/svg/icon-transaction-list.svg', alt='REST')
+                            img(src='/_assets/svg/icon-transaction-list.svg', alt='REST')
                           v-list-item-content
                             v-list-item-title REST API
                             v-list-item-subtitle Classic REST Endpoints
@@ -67,7 +67,7 @@
                             v-icon(:color='kind === `rest` ? `primary` : `grey lighten-3`') mdi-check-circle
                         v-list-item(value='graphql', disabled)
                           v-list-item-avatar
-                            img(src='/svg/icon-graphql.svg', alt='GraphQL')
+                            img(src='/_assets/svg/icon-graphql.svg', alt='GraphQL')
                           v-list-item-content
                             v-list-item-title GraphQL
                             v-list-item-subtitle.grey--text.text--lighten-1 Schema-based API

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

@@ -241,7 +241,7 @@ import katexHelper from './common/katex'
 const CtrlKey = /Mac/.test(navigator.platform) ? 'Cmd' : 'Ctrl'
 
 // Prism Config
-Prism.plugins.autoloader.languages_path = '/js/prism/'
+Prism.plugins.autoloader.languages_path = '/_assets/js/prism/'
 Prism.plugins.NormalizeWhitespace.setDefaults({
   'remove-trailing': true,
   'remove-indent': true,

+ 3 - 3
client/components/editor/editor-modal-editorselect.vue

@@ -112,7 +112,7 @@
                     ripple
                     )
                     v-card-text.text-center(@click='fromTemplate')
-                      img(src='/svg/icon-cube.svg', alt='From Template', style='width: 42px; opacity: .5;')
+                      img(src='/_assets/svg/icon-cube.svg', alt='From Template', style='width: 42px; opacity: .5;')
                       .body-2.mt-1.teal--text From Template
                       .caption.grey--text Use an existing page...
             v-flex(xs4)
@@ -124,7 +124,7 @@
                     ripple
                     )
                     v-card-text.text-center(@click='selectEditor("redirect")')
-                      img(src='/svg/icon-route.svg', alt='Redirection', style='width: 42px; opacity: .5;')
+                      img(src='/_assets/svg/icon-route.svg', alt='Redirection', style='width: 42px; opacity: .5;')
                       .body-2.mt-1.teal--text Redirection
                       .caption.grey--text Redirect the user to...
             v-flex(xs4)
@@ -136,7 +136,7 @@
                     ripple
                     )
                     v-card-text.text-center(@click='')
-                      img(src='/svg/icon-sewing-patch.svg', alt='Code', style='width: 42px; opacity: .5;')
+                      img(src='/_assets/svg/icon-sewing-patch.svg', alt='Code', style='width: 42px; opacity: .5;')
                       .body-2.mt-1.teal--text.text--lighten-2 Embed
                       .caption.teal--text.text--lighten-1 Include external pages
                     v-fade-transition

+ 1 - 1
client/components/new-page.vue

@@ -2,7 +2,7 @@
   v-app
     .newpage
       .newpage-content
-        img.animated.fadeIn(src='/svg/icon-delete-file.svg', alt='Not Found')
+        img.animated.fadeIn(src='/_assets/svg/icon-delete-file.svg', alt='Not Found')
         .headline {{ $t('newpage.title') }}
         .subtitle-1.mt-3 {{ $t('newpage.subtitle') }}
         v-btn.mt-5(:href='`/e/` + locale + `/` + path', x-large)

+ 1 - 1
client/components/not-found.vue

@@ -2,7 +2,7 @@
   v-app
     .notfound
       .notfound-content
-        img.animated.fadeIn(src='/svg/icon-delete-file.svg', alt='Not Found')
+        img.animated.fadeIn(src='/_assets/svg/icon-delete-file.svg', alt='Not Found')
         .headline {{$t('notfound.title')}}
         .subheading.mt-3 {{$t('notfound.subtitle')}}
         v-btn.mt-5(color='red lighten-4', href='/', large, outlined)

+ 1 - 1
client/components/profile/pages.vue

@@ -3,7 +3,7 @@
     v-layout(row wrap)
       v-flex(xs12)
         .profile-header
-          img.animated.fadeInUp(src='/svg/icon-file.svg', alt='Users', style='width: 80px;')
+          img.animated.fadeInUp(src='/_assets/svg/icon-file.svg', alt='Users', style='width: 80px;')
           .profile-header-title
             .headline.primary--text.animated.fadeInLeft {{$t('profile:pages.title')}}
             .subheading.grey--text.animated.fadeInLeft {{$t('profile:pages.subtitle')}}

+ 1 - 1
client/components/profile/profile.vue

@@ -3,7 +3,7 @@
     v-layout(row wrap)
       v-flex(xs12)
         .profile-header
-          img.animated.fadeInUp(src='/svg/icon-profile.svg', alt='Users', style='width: 80px;')
+          img.animated.fadeInUp(src='/_assets/svg/icon-profile.svg', alt='Users', style='width: 80px;')
           .profile-header-title
             .headline.primary--text.animated.fadeInLeft {{$t('profile:title')}}
             .subheading.grey--text.animated.fadeInLeft {{$t('profile:subtitle')}}

+ 3 - 3
client/components/tags.vue

@@ -89,7 +89,7 @@
           v-btn(text, height='40'): v-icon(size='20') mdi-chevron-double-down
       v-divider
       .text-center.pt-10(v-if='selection.length < 1')
-        img(src='/svg/icon-price-tag.svg')
+        img(src='/_assets/svg/icon-price-tag.svg')
         .subtitle-2.grey--text {{$t('tags:selectOneMoreTagsHint')}}
       .px-5.py-2(v-else)
         v-data-iterator(
@@ -112,11 +112,11 @@
               .subtitle-2.grey--text.mt-5 {{$t('tags:retrievingResultsLoading')}}
           template(v-slot:no-data)
             .text-center.pt-10
-              img(src='/svg/icon-info.svg')
+              img(src='/_assets/svg/icon-info.svg')
               .subtitle-2.grey--text {{$t('tags:noResults')}}
           template(v-slot:no-results)
             .text-center.pt-10
-              img(src='/svg/icon-info.svg')
+              img(src='/_assets/svg/icon-info.svg')
               .subtitle-2.grey--text {{$t('tags:noResultsWithFilter')}}
           template(v-slot:default='props')
             v-row(align='stretch')

+ 1 - 1
client/components/unauthorized.vue

@@ -2,7 +2,7 @@
   v-app
     .unauthorized
       .unauthorized-content
-        img.animated.fadeIn(src='/svg/icon-delete-shield.svg', alt='Unauthorized')
+        img.animated.fadeIn(src='/_assets/svg/icon-delete-shield.svg', alt='Unauthorized')
         .headline {{$t('unauthorized.title')}}
         .subtitle-1.mt-3 {{$t('unauthorized.action.' + action)}}
         v-btn.mt-5(href='/login', x-large)

+ 0 - 12
client/graph/admin/users/users-query-list.gql

@@ -1,12 +0,0 @@
-query {
-  users {
-    list {
-      id
-      name
-      email
-      providerKey
-      isSystem
-      createdAt
-    }
-  }
-}

+ 0 - 12
client/graph/admin/users/users-query-strategies.gql

@@ -1,12 +0,0 @@
-query {
-  authentication {
-    strategies(
-      isEnabled: true
-    ) {
-      key
-      title
-      icon
-      color
-    }
-  }
-}

+ 10 - 10
client/scss/fonts/arabic.scss

@@ -1,39 +1,39 @@
 @font-face {
   font-family: 'Tajawal';
-  src: url('/fonts/arabic/Tajawal-Bold.woff2') format('woff2'),
-      url('/fonts/arabic/Tajawal-Bold.woff') format('woff');
+  src: url('/_assets/fonts/arabic/Tajawal-Bold.woff2') format('woff2'),
+      url('/_assets/fonts/arabic/Tajawal-Bold.woff') format('woff');
   font-weight: bold;
   font-style: normal;
 }
 
 @font-face {
   font-family: 'Tajawal';
-  src: url('/fonts/arabic/Tajawal-Regular.woff2') format('woff2'),
-      url('/fonts/arabic/Tajawal-Regular.woff') format('woff');
+  src: url('/_assets/fonts/arabic/Tajawal-Regular.woff2') format('woff2'),
+      url('/_assets/fonts/arabic/Tajawal-Regular.woff') format('woff');
   font-weight: normal;
   font-style: normal;
 }
 
 @font-face {
   font-family: 'Tajawal';
-  src: url('/fonts/arabic/Tajawal-Medium.woff2') format('woff2'),
-      url('/fonts/arabic/Tajawal-Medium.woff') format('woff');
+  src: url('/_assets/fonts/arabic/Tajawal-Medium.woff2') format('woff2'),
+      url('/_assets/fonts/arabic/Tajawal-Medium.woff') format('woff');
   font-weight: 500;
   font-style: normal;
 }
 
 @font-face {
   font-family: 'BalooBhaijaan';
-  src: url('/fonts/arabic/BalooBhaijaan-Regular.woff2') format('woff2'),
-      url('/fonts/arabic/BalooBhaijaan-Regular.woff') format('woff');
+  src: url('/_assets/fonts/arabic/BalooBhaijaan-Regular.woff2') format('woff2'),
+      url('/_assets/fonts/arabic/BalooBhaijaan-Regular.woff') format('woff');
   font-weight: normal;
   font-style: normal;
 }
 
 @font-face {
   font-family: 'Roboto Mono';
-  src: url('/fonts/arabic/RobotoMono-Regular.woff2') format('woff2'),
-      url('/fonts/arabic/RobotoMono-Regular.woff') format('woff');
+  src: url('/_assets/fonts/arabic/RobotoMono-Regular.woff2') format('woff2'),
+      url('/_assets/fonts/arabic/RobotoMono-Regular.woff') format('woff');
   font-weight: normal;
   font-style: normal;
 }

+ 14 - 14
client/scss/fonts/default.scss

@@ -1,55 +1,55 @@
 @font-face {
   font-family: 'Roboto';
-  src: url('/fonts/default/Roboto-MediumItalic.woff2') format('woff2'),
-      url('/fonts/default/Roboto-MediumItalic.woff') format('woff');
+  src: url('/_assets/fonts/default/Roboto-MediumItalic.woff2') format('woff2'),
+      url('/_assets/fonts/default/Roboto-MediumItalic.woff') format('woff');
   font-weight: 500;
   font-style: italic;
 }
 
 @font-face {
   font-family: 'Roboto';
-  src: url('/fonts/default/Roboto-Italic.woff2') format('woff2'),
-      url('/fonts/default/Roboto-Italic.woff') format('woff');
+  src: url('/_assets/fonts/default/Roboto-Italic.woff2') format('woff2'),
+      url('/_assets/fonts/default/Roboto-Italic.woff') format('woff');
   font-weight: normal;
   font-style: italic;
 }
 
 @font-face {
   font-family: 'Roboto';
-  src: url('/fonts/default/Roboto-Bold.woff2') format('woff2'),
-      url('/fonts/default/Roboto-Bold.woff') format('woff');
+  src: url('/_assets/fonts/default/Roboto-Bold.woff2') format('woff2'),
+      url('/_assets/fonts/default/Roboto-Bold.woff') format('woff');
   font-weight: bold;
   font-style: normal;
 }
 
 @font-face {
   font-family: 'Roboto';
-  src: url('/fonts/default/Roboto-Regular.woff2') format('woff2'),
-      url('/fonts/default/Roboto-Regular.woff') format('woff');
+  src: url('/_assets/fonts/default/Roboto-Regular.woff2') format('woff2'),
+      url('/_assets/fonts/default/Roboto-Regular.woff') format('woff');
   font-weight: normal;
   font-style: normal;
 }
 
 @font-face {
   font-family: 'Roboto';
-  src: url('/fonts/default/Roboto-BoldItalic.woff2') format('woff2'),
-      url('/fonts/default/Roboto-BoldItalic.woff') format('woff');
+  src: url('/_assets/fonts/default/Roboto-BoldItalic.woff2') format('woff2'),
+      url('/_assets/fonts/default/Roboto-BoldItalic.woff') format('woff');
   font-weight: bold;
   font-style: italic;
 }
 
 @font-face {
   font-family: 'Roboto';
-  src: url('/fonts/default/Roboto-Medium.woff2') format('woff2'),
-      url('/fonts/default/Roboto-Medium.woff') format('woff');
+  src: url('/_assets/fonts/default/Roboto-Medium.woff2') format('woff2'),
+      url('/_assets/fonts/default/Roboto-Medium.woff') format('woff');
   font-weight: 500;
   font-style: normal;
 }
 
 @font-face {
   font-family: 'Roboto Mono';
-  src: url('/fonts/default/RobotoMono-Regular.woff2') format('woff2'),
-      url('/fonts/default/RobotoMono-Regular.woff') format('woff');
+  src: url('/_assets/fonts/default/RobotoMono-Regular.woff2') format('woff2'),
+      url('/_assets/fonts/default/RobotoMono-Regular.woff') format('woff');
   font-weight: normal;
   font-style: normal;
 }

+ 1 - 1
client/static/browserconfig.xml

@@ -2,7 +2,7 @@
 <browserconfig>
   <msapplication>
     <tile>
-      <square150x150logo src="/favicons/ms-icon-150x150.png"/>
+      <square150x150logo src="/_assets/favicons/ms-icon-150x150.png"/>
       <TileColor>#1976d2</TileColor>
     </tile>
   </msapplication>

+ 2 - 2
client/static/manifest.json

@@ -4,12 +4,12 @@
   "start_url": "/",
   "icons": [
       {
-          "src": "/favicons/android-chrome-192x192.png",
+          "src": "/_assets/favicons/android-chrome-192x192.png",
           "sizes": "192x192",
           "type": "image/png"
       },
       {
-          "src": "/favicons/android-chrome-256x256.png",
+          "src": "/_assets/favicons/android-chrome-256x256.png",
           "sizes": "256x256",
           "type": "image/png"
       }

+ 4 - 3
client/themes/default/components/page.vue

@@ -105,6 +105,7 @@
                     span 334
                 .d-flex
                   v-btn.text-none(
+                    :href='"/c/" + locale + "/" + path'
                     :color='$vuetify.theme.dark ? `pink` : `pink darken-3`'
                     outlined
                     style='flex: 1 1 100%;'
@@ -114,7 +115,7 @@
                   v-tooltip(right, v-if='isAuthenticated')
                     template(v-slot:activator='{ on }')
                       v-btn.ml-2(
-                        :href='"/h/" + locale + "/" + path'
+                        :href='"/c/" + locale + "/" + path + `?new`'
                         v-on='on'
                         outlined
                         small
@@ -133,7 +134,7 @@
                       v-btn.btn-animate-edit(icon, :href='"/h/" + locale + "/" + path', v-on='on', x-small)
                         v-icon(color='indigo', dense) mdi-history
                     span {{$t('common:header.history')}}
-                .body-2.grey--text(:class='darkMode ? `` : `text--darken-3`') {{ authorName }}
+                .body-2.grey--text(:class='$vuetify.theme.dark ? `` : `text--darken-3`') {{ authorName }}
                 .caption.grey--text.text--darken-1 {{ updatedAt | moment('calendar') }}
 
             //- v-card.mb-5
@@ -293,7 +294,7 @@ import Vue from 'vue'
 
 Vue.component('tabset', Tabset)
 
-Prism.plugins.autoloader.languages_path = '/js/prism/'
+Prism.plugins.autoloader.languages_path = '/_assets/js/prism/'
 Prism.plugins.NormalizeWhitespace.setDefaults({
   'remove-trailing': true,
   'remove-indent': true,

+ 7 - 7
dev/templates/master.pug

@@ -6,7 +6,7 @@ html(lang=siteConfig.lang)
     meta(name='viewport', content='user-scalable=yes, width=device-width, initial-scale=1, maximum-scale=5')
     meta(name='theme-color', content='#1976d2')
     meta(name='msapplication-TileColor', content='#1976d2')
-    meta(name='msapplication-TileImage', content='/favicons/mstile-150x150.png')
+    meta(name='msapplication-TileImage', content='/_assets/favicons/mstile-150x150.png')
 
     title= pageMeta.title + ' | ' + config.title
 
@@ -20,12 +20,12 @@ html(lang=siteConfig.lang)
     meta(property='og:site_name', content=config.title)
 
     //- Favicon
-    link(rel='apple-touch-icon', sizes='180x180', href='/apple-touch-icon.png')
-    link(rel='icon', type='image/png', sizes='192x192', href='/favicons/android-icon-192x192.png')
-    link(rel='icon', type='image/png', sizes='32x32', href='/favicons/favicon-32x32.png')
-    link(rel='icon', type='image/png', sizes='16x16', href='/favicons/favicon-16x16.png')
-    link(rel='mask-icon', href='/favicons/safari-pinned-tab.svg', color='#1976d2')
-    link(rel='manifest', href='/manifest.json')
+    link(rel='apple-touch-icon', sizes='180x180', href='/_assets/favicons/apple-touch-icon.png')
+    link(rel='icon', type='image/png', sizes='192x192', href='/_assets/favicons/android-icon-192x192.png')
+    link(rel='icon', type='image/png', sizes='32x32', href='/_assets/favicons/favicon-32x32.png')
+    link(rel='icon', type='image/png', sizes='16x16', href='/_assets/favicons/favicon-16x16.png')
+    link(rel='mask-icon', href='/_assets/favicons/safari-pinned-tab.svg', color='#1976d2')
+    link(rel='manifest', href='/_assets/manifest.json')
 
     //- Site Properties
     script.

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

@@ -9,7 +9,6 @@ const CopyWebpackPlugin = require('copy-webpack-plugin')
 const HtmlWebpackPlugin = require('html-webpack-plugin')
 const HtmlWebpackPugPlugin = require('html-webpack-pug-plugin')
 const MomentTimezoneDataPlugin = require('moment-timezone-data-webpack-plugin')
-const SriWebpackPlugin = require('webpack-subresource-integrity')
 const VuetifyLoaderPlugin = require('vuetify-loader/lib/plugin')
 const WriteFilePlugin = require('write-file-webpack-plugin')
 const WebpackBarPlugin = require('webpackbar')
@@ -31,7 +30,7 @@ module.exports = {
   },
   output: {
     path: path.join(process.cwd(), 'assets'),
-    publicPath: '/',
+    publicPath: '/_assets/',
     filename: 'js/[name].js',
     chunkFilename: 'js/[name].js',
     globalObject: 'this',
@@ -210,10 +209,6 @@ module.exports = {
       excludeChunks: ['app', 'legacy']
     }),
     new HtmlWebpackPugPlugin(),
-    new SriWebpackPlugin({
-      hashFuncNames: ['sha256', 'sha512'],
-      enabled: false
-    }),
     new WebpackBarPlugin({
       name: 'Client Assets'
     }),

+ 8 - 8
dev/webpack/webpack.prod.js

@@ -15,7 +15,8 @@ const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin')
 const ScriptExtHtmlWebpackPlugin = require('script-ext-html-webpack-plugin')
 const VuetifyLoaderPlugin = require('vuetify-loader/lib/plugin')
 const WebpackBarPlugin = require('webpackbar')
-// const SriWebpackPlugin = require('webpack-subresource-integrity')
+
+const now = Math.round(Date.now() / 1000)
 
 const babelConfig = fs.readJsonSync(path.join(process.cwd(), '.babelrc'))
 const cacheDir = '.webpack-cache/cache'
@@ -34,9 +35,9 @@ module.exports = {
   },
   output: {
     path: path.join(process.cwd(), 'assets'),
-    publicPath: '/',
-    filename: 'js/[name].[hash].js',
-    chunkFilename: 'js/[name].[chunkhash].js',
+    publicPath: '/_assets/',
+    filename: `js/[name].js?${now}`,
+    chunkFilename: `js/[name].js?${now}`,
     globalObject: 'this',
     crossOriginLoading: 'use-credentials'
   },
@@ -223,10 +224,6 @@ module.exports = {
       sync: 'runtime.js',
       defaultAttribute: 'async'
     }),
-    // new SriWebpackPlugin({
-    //   hashFuncNames: ['sha256', 'sha512'],
-    //   enabled: true
-    // }),
     new WebpackBarPlugin({
       name: 'Client Assets'
     }),
@@ -238,6 +235,9 @@ module.exports = {
     new webpack.DefinePlugin({
       'process.env.NODE_ENV': JSON.stringify('production'),
       'process.env.CURRENT_THEME': JSON.stringify(_.defaultTo(yargs.theme, 'default'))
+    }),
+    new webpack.optimize.MinChunkSizePlugin({
+      minChunkSize: 50000
     })
   ],
   optimization: {

+ 11 - 0
server/db/migrations-sqlite/2.4.36.js

@@ -0,0 +1,11 @@
+exports.up = knex => {
+  return knex.schema
+    .alterTable('comments', table => {
+      table.text('render').notNullable().defaultTo('')
+      table.string('name').notNullable().defaultTo('')
+      table.string('email').notNullable().defaultTo('')
+      table.string('ip').notNullable().defaultTo('')
+    })
+}
+
+exports.down = knex => { }

+ 11 - 0
server/db/migrations/2.4.36.js

@@ -0,0 +1,11 @@
+exports.up = knex => {
+  return knex.schema
+    .alterTable('comments', table => {
+      table.text('render').notNullable().defaultTo('')
+      table.string('name').notNullable().defaultTo('')
+      table.string('email').notNullable().defaultTo('')
+      table.string('ip').notNullable().defaultTo('')
+    })
+}
+
+exports.down = knex => { }

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

@@ -13,7 +13,7 @@ module.exports = {
   UserQuery: {
     async list(obj, args, context, info) {
       return WIKI.models.users.query()
-        .select('id', 'email', 'name', 'providerKey', 'isSystem', 'createdAt')
+        .select('id', 'email', 'name', 'providerKey', 'isSystem', 'isActive', 'createdAt', 'lastLoginAt')
     },
     async search(obj, args, context, info) {
       return WIKI.models.users.query()

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

@@ -117,7 +117,9 @@ type UserMinimal {
   email: String!
   providerKey: String!
   isSystem: Boolean!
+  isActive: Boolean!
   createdAt: Date!
+  lastLoginAt: Date
 }
 
 type User {

+ 1 - 0
server/helpers/common.js

@@ -33,6 +33,7 @@ module.exports = {
         enum: value.enum || false,
         multiline: value.multiline || false,
         sensitive: value.sensitive || false,
+        maxWidth: value.maxWidth || 0,
         order: value.order || 100
       })
       return result

+ 1 - 1
server/master.js

@@ -53,7 +53,7 @@ module.exports = async () => {
   // ----------------------------------------
 
   app.use(favicon(path.join(WIKI.ROOTPATH, 'assets', 'favicon.ico')))
-  app.use(express.static(path.join(WIKI.ROOTPATH, 'assets'), {
+  app.use('/_assets', express.static(path.join(WIKI.ROOTPATH, 'assets'), {
     index: false,
     maxAge: '7d'
   }))

+ 6 - 0
server/modules/authentication/ldap/definition.yml

@@ -19,11 +19,13 @@ props:
     type: String
     default: cn='root'
     hint: The dstinguished name (dn) of the account used for binding.
+    maxWidth: 600
     order: 2
   bindCredentials:
     title: Admin Bind Credentials
     type: String
     hint: The password of the account used above for binding.
+    maxWidth: 600
     order: 3
   searchBase:
     title: Search Base
@@ -57,22 +59,26 @@ props:
     type: String
     default: 'uid'
     hint: The field storing the user unique identifier. Usually "uid" or "sAMAccountName".
+    maxWidth: 500
     order: 20
   mappingEmail:
     title: Email Field Mapping
     type: String
     default: 'mail'
     hint: The field storing the user email. Usually "mail".
+    maxWidth: 500
     order: 21
   mappingDisplayName:
     title: Display Name Field Mapping
     type: String
     default: 'displayName'
     hint: The field storing the user display name. Usually "displayName" or "cn".
+    maxWidth: 500
     order: 22
   mappingPicture:
     title: Avatar Picture Field Mapping
     type: String
     default: 'jpegPhoto'
     hint: The field storing the user avatar picture. Usually "jpegPhoto" or "thumbnailPhoto".
+    maxWidth: 500
     order: 23

+ 8 - 5
server/modules/authentication/okta/definition.yml

@@ -12,23 +12,26 @@ scopes:
   - email
   - openid
 props:
+  audience:
+    title: Org URL
+    type: String
+    hint: Okta organization URL (e.g. https://example.okta.com, https://example.oktapreview.com), found on the Developer Dashboard, in the upper right.
+    order: 1
   clientId:
     title: Client ID
     type: String
     hint: 20 chars alphanumeric string
+    maxWidth: 400
     order: 2
   clientSecret:
     title: Client Secret
     type: String
     hint: 40 chars alphanumeric string with a hyphen(s)
+    maxWidth: 600
     order: 3
   idp:
     title: Identity Provider ID (idp)
     type: String
     hint: (Optional) - 20 chars alphanumeric string
+    maxWidth: 400
     order: 4
-  audience:
-    title: Org URL
-    type: String
-    hint: Okta organization URL (e.g. https://example.okta.com, https://example.oktapreview.com), found on the Developer Dashboard, in the upper right.
-    order: 1

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

@@ -45,6 +45,7 @@ props:
     type: String
     title: Signature Algorithm
     hint: Signature algorithm used for signing requests
+    maxWidth: 400
     order: 7
     default: sha1
     enum:
@@ -96,6 +97,7 @@ props:
     type: String
     title: Request Binding
     hint: Binding used for request authentication from IDP.
+    maxWidth: 400
     order: 15
     default: 'HTTP-POST'
     enum:

+ 1 - 1
server/modules/comments/commento/definition.yml

@@ -6,7 +6,7 @@ logo: https://static.requarks.io/logo/commento.svg
 website: https://commento.io/
 displayMode: footer
 codeTemplate: true
-isAvailable: true
+isAvailable: false
 props:
   instanceUrl:
     type: String

+ 11 - 7
server/modules/comments/default/definition.yml

@@ -8,12 +8,16 @@ displayMode: dynamic
 codeTemplate: false
 isAvailable: true
 props:
-  displayMode:
+  akismet:
     type: String
-    title: Display mode
-    default: 'page'
-    enum:
-      - inline
-      - page
-    hint: Whether to display the comments under the content (inline) or on a dedicated page (page).
+    title: Akismet API Key
+    default: ''
+    hint: 'Prevent spam by using the Akismet service. Enter your API key here to enable. Leave empty to disable.'
     order: 1
+  minDelay:
+    type: Number
+    title: Post delay
+    default: 30
+    hint: 'Minimum delay (in seconds) between comments per IP address.'
+    maxWidth: 400
+    order: 2

+ 1 - 1
server/modules/comments/disqus/definition.yml

@@ -6,7 +6,7 @@ logo: https://static.requarks.io/logo/disqus.svg
 website: https://disqus.com/
 displayMode: footer
 codeTemplate: true
-isAvailable: true
+isAvailable: false
 props:
   accountName:
     type: String