Ver código fonte

feat: login screen UI + server code cleanup

Nicolas Giard 2 anos atrás
pai
commit
d9dbd0f62f
67 arquivos alterados com 1076 adições e 1847 exclusões
  1. 1 1
      .editorconfig
  2. 0 1
      .nvmrc
  3. 1 1
      client/client-app.js
  4. 0 21
      client/client-setup.js
  5. 23 15
      client/components/login.vue
  6. 0 4
      client/index-legacy.js
  7. 0 11
      client/index-setup.js
  8. 1 1
      dev/index.js
  9. 1 0
      dev/templates/base.pug
  10. 0 48
      dev/templates/setup.pug
  11. 4 21
      dev/webpack/webpack.dev.js
  12. 4 21
      dev/webpack/webpack.prod.js
  13. 1 1
      package.json
  14. 15 28
      server/core/kernel.js
  15. 1 1
      server/core/system.js
  16. 0 144
      server/core/telemetry.js
  17. 5 4
      server/graph/resolvers/storage.js
  18. 1 0
      server/graph/schemas/authentication.graphql
  19. 2 0
      server/index.js
  20. 22 0
      server/models/sites.js
  21. 0 450
      server/setup.js
  22. 0 25
      server/themes/default/theme.yml
  23. BIN
      server/themes/default/thumbnail.png
  24. 1 1
      server/views/admin.pug
  25. 26 17
      server/views/base.pug
  26. 1 1
      server/views/editor.pug
  27. 1 1
      server/views/error.pug
  28. 1 1
      server/views/history.pug
  29. 0 23
      server/views/legacy/login.pug
  30. 0 60
      server/views/legacy/page.pug
  31. 1 1
      server/views/login.pug
  32. 1 1
      server/views/new.pug
  33. 1 1
      server/views/notfound.pug
  34. 1 1
      server/views/page.pug
  35. 1 1
      server/views/profile.pug
  36. 1 1
      server/views/register.pug
  37. 1 1
      server/views/source.pug
  38. 1 1
      server/views/tags.pug
  39. 1 1
      server/views/unauthorized.pug
  40. 1 1
      server/views/welcome.pug
  41. 13 8
      server/web.js
  42. 1 0
      ux/.eslintrc.js
  43. 3 0
      ux/index.html
  44. 14 15
      ux/package.json
  45. 1 1
      ux/quasar.config.js
  46. 29 14
      ux/src/App.vue
  47. 1 2
      ux/src/components/GroupEditOverlay.vue
  48. 1 1
      ux/src/components/IconPickerDialog.vue
  49. 1 2
      ux/src/components/PageDataTemplateDialog.vue
  50. 1 2
      ux/src/components/PageRelationDialog.vue
  51. 1 1
      ux/src/components/SiteActivateDialog.vue
  52. 1 1
      ux/src/components/UserChangePwdDialog.vue
  53. 1 2
      ux/src/components/UserCreateDialog.vue
  54. 1 6
      ux/src/components/UserEditOverlay.vue
  55. 1 1
      ux/src/components/WebhookEditDialog.vue
  56. 2 2
      ux/src/css/app.scss
  57. 4 0
      ux/src/css/quasar.variables.scss
  58. 2 2
      ux/src/i18n/locales/en.json
  59. 1 1
      ux/src/layouts/AdminLayout.vue
  60. 63 30
      ux/src/layouts/AuthLayout.vue
  61. 24 17
      ux/src/pages/AdminNavigation.vue
  62. 1 8
      ux/src/pages/AdminRendering.vue
  63. 487 552
      ux/src/pages/Login.vue
  64. 7 7
      ux/src/router/routes.js
  65. 1 1
      ux/src/stores/admin.js
  66. 1 1
      ux/src/stores/site.js
  67. 291 258
      ux/yarn.lock

+ 1 - 1
.editorconfig

@@ -8,7 +8,7 @@ trim_trailing_whitespace = true
 end_of_line = lf
 insert_final_newline = true
 
-[*.{jade,pug,md}]
+[*.{pug,md}]
 trim_trailing_whitespace = false
 
 [Makefile]

+ 0 - 1
.nvmrc

@@ -1 +0,0 @@
-v12.16.3

+ 1 - 1
client/client-app.js

@@ -47,7 +47,7 @@ store.commit('user/REFRESH_AUTH')
 // Initialize Apollo Client (GraphQL)
 // ====================================
 
-const graphQLEndpoint = window.location.protocol + '//' + window.location.host + '/graphql'
+const graphQLEndpoint = window.location.protocol + '//' + window.location.host + '/_graphql'
 
 const graphQLLink = ApolloLink.from([
   new ErrorLink(({ graphQLErrors, networkError }) => {

+ 0 - 21
client/client-setup.js

@@ -1,21 +0,0 @@
-/* eslint-disable import/first */
-import Vue from 'vue'
-import Vuetify from 'vuetify/lib'
-import boot from './modules/boot'
-/* eslint-enable import/first */
-
-window.WIKI = null
-window.boot = boot
-
-Vue.use(Vuetify)
-
-Vue.component('setup', () => import(/* webpackMode: "eager" */ './components/setup.vue'))
-
-let bootstrap = () => {
-  window.WIKI = new Vue({
-    el: '#root',
-    vuetify: new Vuetify()
-  })
-}
-
-window.boot.onDOMReady(bootstrap)

+ 23 - 15
client/components/login.vue

@@ -662,26 +662,34 @@ export default {
   apollo: {
     strategies: {
       query: gql`
-        {
-          authentication {
-            activeStrategies(enabledOnly: true) {
+        query loginFetchSiteStrategies(
+          $siteId: UUID
+        ) {
+          authStrategies(
+            siteId: $siteId
+            enabledOnly: true
+            ) {
+            key
+            strategy {
               key
-              strategy {
-                key
-                logo
-                color
-                icon
-                useForm
-                usernameType
-              }
-              displayName
-              order
-              selfRegistration
+              logo
+              color
+              icon
+              useForm
+              usernameType
             }
+            displayName
+            order
+            selfRegistration
           }
         }
       `,
-      update: (data) => _.sortBy(data.authentication.activeStrategies, ['order']),
+      variables () {
+        return {
+          siteId: siteConfig.id
+        }
+      },
+      update: (data) => _.sortBy(data.authStrategies, ['order']),
       watchLoading (isLoading) {
         this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'login-strategies-refresh')
       }

+ 0 - 4
client/index-legacy.js

@@ -1,4 +0,0 @@
-require('./scss/legacy.scss')
-require('./scss/fonts/default.scss')
-
-window.WIKI = null

+ 0 - 11
client/index-setup.js

@@ -1,11 +0,0 @@
-require('core-js/stable')
-require('regenerator-runtime/runtime')
-
-/* eslint-disable no-unused-expressions */
-
-require('./scss/app.scss')
-import(/* webpackChunkName: "mdi" */ '@mdi/font/css/materialdesignicons.css')
-
-require('./helpers/compatibility.js')
-
-require('./client-setup.js')

+ 1 - 1
dev/index.js

@@ -60,7 +60,7 @@ const init = {
   },
   async reload() {
     console.warn(chalk.yellow('--- Gracefully stopping server...'))
-    await global.WIKI.kernel.shutdown()
+    await global.WIKI.kernel.shutdown(true)
 
     console.warn(chalk.yellow('--- Purging node modules cache...'))
 

+ 1 - 0
dev/templates/master.pug → dev/templates/base.pug

@@ -29,6 +29,7 @@ html(lang=siteConfig.lang)
 
     //- Site Properties
     script.
+      var siteId = "!{siteId}"
       var siteConfig = !{JSON.stringify(siteConfig)}
       var siteLangs = !{JSON.stringify(langs)}
 

+ 0 - 48
dev/templates/setup.pug

@@ -1,48 +0,0 @@
-doctype html
-html
-  head
-    meta(http-equiv='X-UA-Compatible', content='IE=edge')
-    meta(charset='UTF-8')
-    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='/_assets/favicons/mstile-150x150.png')
-    title Wiki.js Setup
-
-    //- Favicon
-    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-chrome-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 Lang
-    script.
-      var siteConfig = !{JSON.stringify({ title: config.title })}
-
-    //- Dev Mode Warning
-    if devMode
-      script.
-        siteConfig.devMode = true
-
-    //- CSS
-    <% for (var index in htmlWebpackPlugin.files.css) { %>
-    link(
-      type='text/css'
-      rel='stylesheet'
-      href='<%= htmlWebpackPlugin.files.css[index] %>'
-    )
-    <% } %>
-
-    //- JS
-    <% for (var index in htmlWebpackPlugin.files.js) { %>
-    script(
-      type='text/javascript'
-      src='<%= htmlWebpackPlugin.files.js[index] %>'
-      )
-    <% } %>
-
-  body
-    #root
-      setup(telemetry-id=telemetryClientID, wiki-version=packageObj.version)

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

@@ -24,9 +24,7 @@ fs.emptyDirSync(path.join(process.cwd(), 'assets-legacy'))
 module.exports = {
   mode: 'development',
   entry: {
-    app: ['./client/index-app.js', 'webpack-hot-middleware/client'],
-    legacy: ['./client/index-legacy.js', 'webpack-hot-middleware/client'],
-    setup: ['./client/index-setup.js', 'webpack-hot-middleware/client']
+    app: ['./client/index-app.js', 'webpack-hot-middleware/client']
   },
   output: {
     path: path.join(process.cwd(), 'assets-legacy'),
@@ -197,25 +195,10 @@ module.exports = {
       ]
     }),
     new HtmlWebpackPlugin({
-      template: 'dev/templates/master.pug',
-      filename: '../server/views/master.pug',
+      template: 'dev/templates/base.pug',
+      filename: '../server/views/base.pug',
       hash: false,
-      inject: false,
-      excludeChunks: ['setup', 'legacy']
-    }),
-    new HtmlWebpackPlugin({
-      template: 'dev/templates/legacy.pug',
-      filename: '../server/views/legacy/master.pug',
-      hash: false,
-      inject: false,
-      excludeChunks: ['setup', 'app']
-    }),
-    new HtmlWebpackPlugin({
-      template: 'dev/templates/setup.pug',
-      filename: '../server/views/setup.pug',
-      hash: false,
-      inject: false,
-      excludeChunks: ['app', 'legacy']
+      inject: false
     }),
     new HtmlWebpackPugPlugin(),
     new WebpackBarPlugin({

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

@@ -29,9 +29,7 @@ fs.emptyDirSync(path.join(process.cwd(), 'assets-legacy'))
 module.exports = {
   mode: 'production',
   entry: {
-    app: './client/index-app.js',
-    legacy: './client/index-legacy.js',
-    setup: './client/index-setup.js'
+    app: './client/index-app.js'
   },
   output: {
     path: path.join(process.cwd(), 'assets-legacy'),
@@ -208,25 +206,10 @@ module.exports = {
       chunkFilename: 'css/[name].[chunkhash].css'
     }),
     new HtmlWebpackPlugin({
-      template: 'dev/templates/master.pug',
-      filename: '../server/views/master.pug',
+      template: 'dev/templates/base.pug',
+      filename: '../server/views/base.pug',
       hash: false,
-      inject: false,
-      excludeChunks: ['setup', 'legacy']
-    }),
-    new HtmlWebpackPlugin({
-      template: 'dev/templates/legacy.pug',
-      filename: '../server/views/legacy/master.pug',
-      hash: false,
-      inject: false,
-      excludeChunks: ['setup', 'app']
-    }),
-    new HtmlWebpackPlugin({
-      template: 'dev/templates/setup.pug',
-      filename: '../server/views/setup.pug',
-      hash: false,
-      inject: false,
-      excludeChunks: ['app', 'legacy']
+      inject: false
     }),
     new HtmlWebpackPugPlugin(),
     new ScriptExtHtmlWebpackPlugin({

+ 1 - 1
package.json

@@ -8,7 +8,7 @@
   "scripts": {
     "start": "node server",
     "dev": "nodemon server",
-    "legacy:dev": "node dev",
+    "legacy:dev": "NODE_OPTIONS=--openssl-legacy-provider node dev",
     "legacy:build": "NODE_OPTIONS=--openssl-legacy-provider webpack --profile --config dev/webpack/webpack.prod.js",
     "test": "eslint --format codeframe --ext .js,.vue . && pug-lint server/views && jest",
     "cypress:open": "cypress open"

+ 15 - 28
server/core/kernel.js

@@ -24,14 +24,13 @@ module.exports = {
       process.exit(1)
     }
 
-    this.bootMaster()
+    this.bootWeb()
   },
   /**
-   * Pre-Master Boot Sequence
+   * Pre-Web Boot Sequence
    */
-  async preBootMaster() {
+  async preBootWeb() {
     try {
-      await this.initTelemetry()
       WIKI.sideloader = await require('./sideloader').init()
       WIKI.cache = require('./cache').init()
       WIKI.scheduler = require('./scheduler').init()
@@ -48,22 +47,22 @@ module.exports = {
     }
   },
   /**
-   * Boot Master Process
+   * Boot Web Process
    */
-  async bootMaster() {
+  async bootWeb() {
     try {
-      await this.preBootMaster()
-      await require('../master')()
-      this.postBootMaster()
+      await this.preBootWeb()
+      await require('../web')()
+      this.postBootWeb()
     } catch (err) {
       WIKI.logger.error(err)
       process.exit(1)
     }
   },
   /**
-   * Post-Master Boot Sequence
+   * Post-Web Boot Sequence
    */
-  async postBootMaster() {
+  async postBootWeb() {
     await WIKI.models.analytics.refreshProvidersFromDisk()
     await WIKI.models.authentication.refreshStrategiesFromDisk()
     await WIKI.models.commentProviders.refreshProvidersFromDisk()
@@ -74,30 +73,16 @@ module.exports = {
 
     await WIKI.auth.activateStrategies()
     await WIKI.models.commentProviders.initProvider()
+    await WIKI.models.sites.reloadCache()
     await WIKI.models.storage.initTargets()
     // WIKI.scheduler.start()
 
     await WIKI.models.subscribeToNotifications()
   },
-  /**
-   * Init Telemetry
-   */
-  async initTelemetry() {
-    require('./telemetry').init()
-
-    process.on('unhandledRejection', (err) => {
-      WIKI.logger.warn(err)
-      WIKI.telemetry.sendError(err)
-    })
-    process.on('uncaughtException', (err) => {
-      WIKI.logger.warn(err)
-      WIKI.telemetry.sendError(err)
-    })
-  },
   /**
    * Graceful shutdown
    */
-  async shutdown () {
+  async shutdown (devMode = false) {
     if (WIKI.servers) {
       await WIKI.servers.stopServers()
     }
@@ -113,6 +98,8 @@ module.exports = {
     if (WIKI.asar) {
       await WIKI.asar.unload()
     }
-    process.exit(0)
+    if (!devMode) {
+      process.exit(0)
+    }
   }
 }

+ 1 - 1
server/core/system.js

@@ -11,7 +11,7 @@ module.exports = {
     minimumVersionRequired: '3.0.0-beta.0',
     minimumNodeRequired: '18.0.0'
   },
-  init() {
+  init () {
     // Clear content cache
     fs.emptyDir(path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, 'cache'))
 

+ 0 - 144
server/core/telemetry.js

@@ -1,144 +0,0 @@
-const _ = require('lodash')
-const { createApolloFetch } = require('apollo-fetch')
-const { v4: uuid } = require('uuid')
-const os = require('os')
-const fs = require('fs-extra')
-
-/* global WIKI */
-
-module.exports = {
-  enabled: false,
-  init() {
-    WIKI.telemetry = this
-
-    if (_.get(WIKI.config, 'telemetry.isEnabled', false) === true && WIKI.config.offline !== true) {
-      this.enabled = true
-      this.sendInstanceEvent('STARTUP')
-    }
-  },
-  sendError(err) {
-    // TODO
-  },
-  sendEvent(eventCategory, eventAction, eventLabel) {
-    // TODO
-  },
-  async sendInstanceEvent(eventType) {
-    if (WIKI.devMode || !this.enabled) { return }
-
-    try {
-      const apollo = createApolloFetch({
-        uri: WIKI.config.graphEndpoint
-      })
-
-      // Platform detection
-      let platform = 'LINUX'
-      let isDockerized = false
-      let osname = `${os.type()} ${os.release()}`
-      switch (os.platform()) {
-        case 'win32':
-          platform = 'WINDOWS'
-          break
-        case 'darwin':
-          platform = 'MACOS'
-          break
-        default:
-          platform = 'LINUX'
-          isDockerized = await fs.pathExists('/.dockerenv')
-          if (isDockerized) {
-            osname = 'Docker'
-          }
-          break
-      }
-
-      // DB Version detection
-      let dbVersion = 'Unknown'
-      switch (WIKI.config.db.type) {
-        case 'mariadb':
-        case 'mysql':
-          const resultMYSQL = await WIKI.models.knex.raw('SELECT VERSION() as version;')
-          dbVersion = _.get(resultMYSQL, '[0][0].version', 'Unknown')
-          break
-        case 'mssql':
-          const resultMSSQL = await WIKI.models.knex.raw('SELECT @@VERSION as version;')
-          dbVersion = _.get(resultMSSQL, '[0].version', 'Unknown')
-          break
-        case 'postgres':
-          dbVersion = _.get(WIKI.models, 'knex.client.version', 'Unknown')
-          break
-        case 'sqlite':
-          dbVersion = _.get(WIKI.models, 'knex.client.driver.VERSION', 'Unknown')
-          break
-      }
-
-      let arch = os.arch().toUpperCase()
-      if (['ARM', 'ARM64', 'X32', 'X64'].indexOf(arch) < 0) {
-        arch = 'OTHER'
-      }
-
-      // Send Event
-      const respStrings = await apollo({
-        query: `mutation (
-          $version: String!
-          $platform: TelemetryPlatform!
-          $os: String!
-          $architecture: TelemetryArchitecture!
-          $dbType: TelemetryDBType!
-          $dbVersion: String!
-          $nodeVersion: String!
-          $cpuCores: Int!
-          $ramMBytes: Int!,
-          $clientId: String!,
-          $event: TelemetryInstanceEvent!
-          ) {
-          telemetry {
-            instance(
-              version: $version
-              platform: $platform
-              os: $os
-              architecture: $architecture
-              dbType: $dbType
-              dbVersion: $dbVersion
-              nodeVersion: $nodeVersion
-              cpuCores: $cpuCores
-              ramMBytes: $ramMBytes
-              clientId: $clientId
-              event: $event
-            ) {
-              responseResult {
-                succeeded
-                errorCode
-                slug
-                message
-              }
-            }
-          }
-        }`,
-        variables: {
-          version: WIKI.version,
-          platform,
-          os: osname,
-          architecture: arch,
-          dbType: WIKI.config.db.type.toUpperCase(),
-          dbVersion,
-          nodeVersion: process.version.substr(1),
-          cpuCores: os.cpus().length,
-          ramMBytes: Math.round(os.totalmem() / 1024 / 1024),
-          clientId: WIKI.config.telemetry.clientId,
-          event: eventType
-        }
-      })
-      const telemetryResponse = _.get(respStrings, 'data.telemetry.instance.responseResult', { succeeded: false, message: 'Unexpected Error' })
-      if (!telemetryResponse.succeeded) {
-        WIKI.logger.warn('Failed to send instance telemetry: ' + telemetryResponse.message)
-      } else {
-        WIKI.logger.info('Telemetry is active: [ OK ]')
-      }
-    } catch (err) {
-      WIKI.logger.warn(err)
-    }
-  },
-  generateClientId() {
-    _.set(WIKI.config, 'telemetry.clientId', uuid())
-    return WIKI.config.telemetry.clientId
-  }
-}

+ 5 - 4
server/graph/resolvers/storage.js

@@ -32,6 +32,7 @@ module.exports = {
       // }), ['title', 'key'])
       return _.sortBy(WIKI.storage.defs.map(md => {
         const dbTarget = dbTargets.find(tg => tg.module === md.key)
+        console.info(md.actions)
         return {
           id: dbTarget?.id ?? uuid(),
           isEnabled: dbTarget?.isEnabled ?? false,
@@ -62,12 +63,12 @@ module.exports = {
           setup: {
             handler: md?.setup?.handler,
             state: dbTarget?.state?.setup ?? 'notconfigured',
-            values: md.setup?.handler
-              ? _.transform(md.setup.defaultValues,
+            values: md.setup?.handler ?
+              _.transform(md.setup.defaultValues,
                 (r, v, k) => {
                   r[k] = dbTarget?.config?.[k] ?? v
-                }, {})
-              : {}
+                }, {}) :
+              {}
           },
           config: _.transform(md.props, (r, v, k) => {
             const cfValue = dbTarget?.config?.[k] ?? v.default

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

@@ -8,6 +8,7 @@ extend type Query {
   apiState: Boolean
 
   authStrategies(
+    siteId: UUID
     enabledOnly: Boolean
   ): [AuthenticationStrategy]
 }

+ 2 - 0
server/index.js

@@ -22,6 +22,8 @@ let WIKI = {
   Error: require('./helpers/error'),
   configSvc: require('./core/config'),
   kernel: require('./core/kernel'),
+  sites: {},
+  sitesMappings: {},
   startedAt: DateTime.utc(),
   storage: {
     defs: [],

+ 22 - 0
server/models/sites.js

@@ -28,6 +28,28 @@ module.exports = class Site extends Model {
     return ['config']
   }
 
+  static async getSiteByHostname ({ hostname, forceReload = false }) {
+    if (forceReload) {
+      await WIKI.models.sites.reloadCache()
+    }
+    const siteId = WIKI.sitesMappings[hostname] || WIKI.sitesMappings['*']
+    if (siteId) {
+      return WIKI.sites[siteId]
+    }
+    return null
+  }
+
+  static async reloadCache () {
+    WIKI.logger.info('Reloading site configurations...')
+    const sites = await WIKI.models.sites.query().orderBy('id')
+    WIKI.sites = _.keyBy(sites, 'id')
+    WIKI.sitesMappings = {}
+    for (const site of sites) {
+      WIKI.sitesMappings[site.hostname] = site.id
+    }
+    WIKI.logger.info(`Loaded ${sites.length} site configurations [ OK ]`)
+  }
+
   static async createSite (hostname, config) {
     const newSite = await WIKI.models.sites.query().insertAndFetch({
       hostname,

+ 0 - 450
server/setup.js

@@ -1,450 +0,0 @@
-const path = require('path')
-const { v4: uuid } = require('uuid')
-const bodyParser = require('body-parser')
-const compression = require('compression')
-const express = require('express')
-const favicon = require('serve-favicon')
-const http = require('http')
-const Promise = require('bluebird')
-const fs = require('fs-extra')
-const _ = require('lodash')
-const crypto = Promise.promisifyAll(require('crypto'))
-const pem2jwk = require('pem-jwk').pem2jwk
-const semver = require('semver')
-
-/* global WIKI */
-
-module.exports = () => {
-  WIKI.config.site = {
-    path: '',
-    title: 'Wiki.js'
-  }
-
-  WIKI.system = require('./core/system')
-
-  // ----------------------------------------
-  // Define Express App
-  // ----------------------------------------
-
-  let app = express()
-  app.use(compression())
-
-  // ----------------------------------------
-  // Public Assets
-  // ----------------------------------------
-
-  app.use(favicon(path.join(WIKI.ROOTPATH, 'assets', 'favicon.ico')))
-  app.use('/_assets', express.static(path.join(WIKI.ROOTPATH, 'assets')))
-
-  // ----------------------------------------
-  // View Engine Setup
-  // ----------------------------------------
-
-  app.set('views', path.join(WIKI.SERVERPATH, 'views'))
-  app.set('view engine', 'pug')
-
-  app.use(bodyParser.json())
-  app.use(bodyParser.urlencoded({ extended: false }))
-
-  app.locals.config = WIKI.config
-  app.locals.data = WIKI.data
-  app.locals._ = require('lodash')
-  app.locals.devMode = WIKI.devMode
-
-  // ----------------------------------------
-  // HMR (Dev Mode Only)
-  // ----------------------------------------
-
-  if (global.DEV) {
-    app.use(global.WP_DEV.devMiddleware)
-    app.use(global.WP_DEV.hotMiddleware)
-  }
-
-  // ----------------------------------------
-  // Controllers
-  // ----------------------------------------
-
-  app.get('*', async (req, res) => {
-    let packageObj = await fs.readJson(path.join(WIKI.ROOTPATH, 'package.json'))
-    res.render('setup', { packageObj })
-  })
-
-  /**
-   * Finalize
-   */
-  app.post('/finalize', async (req, res) => {
-    try {
-      // Set config
-      _.set(WIKI.config, 'auth', {
-        audience: 'urn:wiki.js',
-        tokenExpiration: '30m',
-        tokenRenewal: '14d'
-      })
-      _.set(WIKI.config, 'company', '')
-      _.set(WIKI.config, 'features', {
-        featurePageRatings: true,
-        featurePageComments: true,
-        featurePersonalWikis: true
-      })
-      _.set(WIKI.config, 'graphEndpoint', 'https://graph.requarks.io')
-      _.set(WIKI.config, 'host', req.body.siteUrl)
-      _.set(WIKI.config, 'lang', {
-        code: 'en',
-        autoUpdate: true,
-        namespacing: false,
-        namespaces: []
-      })
-      _.set(WIKI.config, 'logo', {
-        hasLogo: false,
-        logoIsSquare: false
-      })
-      _.set(WIKI.config, 'mail', {
-        senderName: '',
-        senderEmail: '',
-        host: '',
-        port: 465,
-        secure: true,
-        verifySSL: true,
-        user: '',
-        pass: '',
-        useDKIM: false,
-        dkimDomainName: '',
-        dkimKeySelector: '',
-        dkimPrivateKey: ''
-      })
-      _.set(WIKI.config, 'seo', {
-        description: '',
-        robots: ['index', 'follow'],
-        analyticsService: '',
-        analyticsId: ''
-      })
-      _.set(WIKI.config, 'sessionSecret', (await crypto.randomBytesAsync(32)).toString('hex'))
-      _.set(WIKI.config, 'telemetry', {
-        isEnabled: req.body.telemetry === true,
-        clientId: uuid()
-      })
-      _.set(WIKI.config, 'theming', {
-        theme: 'default',
-        darkMode: false,
-        iconset: 'mdi',
-        injectCSS: '',
-        injectHead: '',
-        injectBody: ''
-      })
-      _.set(WIKI.config, 'title', 'Wiki.js')
-
-      // Init Telemetry
-      WIKI.kernel.initTelemetry()
-      // WIKI.telemetry.sendEvent('setup', 'install-start')
-
-      // Basic checks
-      if (!semver.satisfies(process.version, '>=10.12')) {
-        throw new Error('Node.js 10.12.x or later required!')
-      }
-
-      // Create directory structure
-      WIKI.logger.info('Creating data directories...')
-      await fs.ensureDir(path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath))
-      await fs.emptyDir(path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, 'cache'))
-      await fs.ensureDir(path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, 'uploads'))
-
-      // 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),
-        public: certs.publicKey,
-        private: certs.privateKey
-      })
-
-      // Save config to DB
-      WIKI.logger.info('Persisting config to DB...')
-      await WIKI.configSvc.saveToDb([
-        'auth',
-        'certs',
-        'company',
-        'features',
-        'graphEndpoint',
-        'host',
-        'lang',
-        'logo',
-        'mail',
-        'seo',
-        'sessionSecret',
-        'telemetry',
-        'theming',
-        'uploads',
-        'title'
-      ], false)
-
-      // Truncate tables (reset from previous failed install)
-      await WIKI.models.locales.query().where('code', '!=', 'x').del()
-      await WIKI.models.navigation.query().truncate()
-      switch (WIKI.config.db.type) {
-        case 'postgres':
-          await WIKI.models.knex.raw('TRUNCATE groups, users CASCADE')
-          break
-        case 'mysql':
-        case 'mariadb':
-          await WIKI.models.groups.query().where('id', '>', 0).del()
-          await WIKI.models.users.query().where('id', '>', 0).del()
-          await WIKI.models.knex.raw('ALTER TABLE `groups` AUTO_INCREMENT = 1')
-          await WIKI.models.knex.raw('ALTER TABLE `users` AUTO_INCREMENT = 1')
-          break
-        case 'mssql':
-          await WIKI.models.groups.query().del()
-          await WIKI.models.users.query().del()
-          await WIKI.models.knex.raw(`
-            IF EXISTS (SELECT * FROM sys.identity_columns WHERE OBJECT_NAME(OBJECT_ID) = 'groups' AND last_value IS NOT NULL)
-              DBCC CHECKIDENT ([groups], RESEED, 0)
-          `)
-          await WIKI.models.knex.raw(`
-            IF EXISTS (SELECT * FROM sys.identity_columns WHERE OBJECT_NAME(OBJECT_ID) = 'users' AND last_value IS NOT NULL)
-              DBCC CHECKIDENT ([users], RESEED, 0)
-          `)
-          break
-        case 'sqlite':
-          await WIKI.models.groups.query().truncate()
-          await WIKI.models.users.query().truncate()
-          break
-      }
-
-      // Create default locale
-      WIKI.logger.info('Installing default locale...')
-      await WIKI.models.locales.query().insert({
-        code: 'en',
-        strings: {},
-        isRTL: false,
-        name: 'English',
-        nativeName: 'English'
-      })
-
-      // Create default groups
-
-      WIKI.logger.info('Creating default groups...')
-      const adminGroup = await WIKI.models.groups.query().insert({
-        name: 'Administrators',
-        permissions: JSON.stringify(['manage:system']),
-        pageRules: JSON.stringify([]),
-        isSystem: true
-      })
-      const guestGroup = await WIKI.models.groups.query().insert({
-        name: 'Guests',
-        permissions: JSON.stringify(['read:pages', 'read:assets', 'read:comments']),
-        pageRules: JSON.stringify([
-          { id: 'guest', roles: ['read:pages', 'read:assets', 'read:comments'], match: 'START', deny: false, path: '', locales: [] }
-        ]),
-        isSystem: true
-      })
-      if (adminGroup.id !== 1 || guestGroup.id !== 2) {
-        throw new Error('Incorrect groups auto-increment configuration! Should start at 0 and increment by 1. Contact your database administrator.')
-      }
-
-      // Load local authentication strategy
-      await WIKI.models.authentication.query().insert({
-        key: 'local',
-        config: {},
-        selfRegistration: false,
-        isEnabled: true,
-        domainWhitelist: {v: []},
-        autoEnrollGroups: {v: []},
-        order: 0,
-        strategyKey: 'local',
-        displayName: 'Local'
-      })
-
-      // Load editors + enable default
-      await WIKI.models.editors.refreshEditorsFromDisk()
-      await WIKI.models.editors.query().patch({ isEnabled: true }).where('key', 'markdown')
-
-      // Load loggers
-      await WIKI.models.loggers.refreshLoggersFromDisk()
-
-      // Load renderers
-      await WIKI.models.renderers.refreshRenderersFromDisk()
-
-      // Load search engines + enable default
-      await WIKI.models.searchEngines.refreshSearchEnginesFromDisk()
-      await WIKI.models.searchEngines.query().patch({ isEnabled: true }).where('key', 'db')
-
-      // WIKI.telemetry.sendEvent('setup', 'install-loadedmodules')
-
-      // Load storage targets
-      await WIKI.models.storage.refreshTargetsFromDisk()
-
-      // Create root administrator
-      WIKI.logger.info('Creating root administrator...')
-      const adminUser = await WIKI.models.users.query().insert({
-        email: req.body.adminEmail.toLowerCase(),
-        provider: 'local',
-        password: req.body.adminPassword,
-        name: 'Administrator',
-        locale: 'en',
-        defaultEditor: 'markdown',
-        tfaIsActive: false,
-        isActive: true,
-        isVerified: true
-      })
-      await adminUser.$relatedQuery('groups').relate(adminGroup.id)
-
-      // Create Guest account
-      WIKI.logger.info('Creating guest account...')
-      const guestUser = await WIKI.models.users.query().insert({
-        provider: 'local',
-        email: 'guest@example.com',
-        name: 'Guest',
-        password: '',
-        locale: 'en',
-        defaultEditor: 'markdown',
-        tfaIsActive: false,
-        isSystem: true,
-        isActive: true,
-        isVerified: true
-      })
-      await guestUser.$relatedQuery('groups').relate(guestGroup.id)
-      if (adminUser.id !== 1 || guestUser.id !== 2) {
-        throw new Error('Incorrect users auto-increment configuration! Should start at 0 and increment by 1. Contact your database administrator.')
-      }
-
-      // Create site nav
-
-      WIKI.logger.info('Creating default site navigation')
-      await WIKI.models.navigation.query().insert({
-        key: 'site',
-        config: [
-          {
-            locale: 'en',
-            items: [
-              {
-                id: uuid(),
-                icon: 'mdi-home',
-                kind: 'link',
-                label: 'Home',
-                target: '/',
-                targetType: 'home',
-                visibilityMode: 'all',
-                visibilityGroups: null
-              }
-            ]
-          }
-        ]
-      })
-
-      WIKI.logger.info('Setup is complete!')
-      // WIKI.telemetry.sendEvent('setup', 'install-completed')
-      res.json({
-        ok: true,
-        redirectPath: '/',
-        redirectPort: WIKI.config.port
-      }).end()
-
-      if (WIKI.config.telemetry.isEnabled) {
-        await WIKI.telemetry.sendInstanceEvent('INSTALL')
-      }
-
-      WIKI.config.setup = false
-
-      WIKI.logger.info('Stopping Setup...')
-      WIKI.server.destroy(() => {
-        WIKI.logger.info('Setup stopped. Starting Wiki.js...')
-        _.delay(() => {
-          WIKI.kernel.bootMaster()
-        }, 1000)
-      })
-    } catch (err) {
-      try {
-        await WIKI.models.knex('settings').truncate()
-      } catch (err) {}
-      WIKI.telemetry.sendError(err)
-      res.json({ ok: false, error: err.message })
-    }
-  })
-
-  // ----------------------------------------
-  // Error handling
-  // ----------------------------------------
-
-  app.use(function (req, res, next) {
-    const err = new Error('Not Found')
-    err.status = 404
-    next(err)
-  })
-
-  app.use(function (err, req, res, next) {
-    res.status(err.status || 500)
-    res.send({
-      message: err.message,
-      error: WIKI.IS_DEBUG ? err : {}
-    })
-    WIKI.logger.error(err.message)
-    WIKI.telemetry.sendError(err)
-  })
-
-  // ----------------------------------------
-  // Start HTTP server
-  // ----------------------------------------
-
-  WIKI.logger.info(`Starting HTTP server on port ${WIKI.config.port}...`)
-
-  app.set('port', WIKI.config.port)
-
-  WIKI.logger.info(`HTTP Server on port: [ ${WIKI.config.port} ]`)
-  WIKI.server = http.createServer(app)
-  WIKI.server.listen(WIKI.config.port, WIKI.config.bindIP)
-
-  var openConnections = []
-
-  WIKI.server.on('connection', (conn) => {
-    let key = conn.remoteAddress + ':' + conn.remotePort
-    openConnections[key] = conn
-    conn.on('close', () => {
-      openConnections.splice(key, 1)
-    })
-  })
-
-  WIKI.server.destroy = (cb) => {
-    WIKI.server.close(cb)
-    for (let key in openConnections) {
-      openConnections[key].destroy()
-    }
-  }
-
-  WIKI.server.on('error', (error) => {
-    if (error.syscall !== 'listen') {
-      throw error
-    }
-
-    switch (error.code) {
-      case 'EACCES':
-        WIKI.logger.error('Listening on port ' + WIKI.config.port + ' requires elevated privileges!')
-        return process.exit(1)
-      case 'EADDRINUSE':
-        WIKI.logger.error('Port ' + WIKI.config.port + ' is already in use!')
-        return process.exit(1)
-      default:
-        throw error
-    }
-  })
-
-  WIKI.server.on('listening', () => {
-    WIKI.logger.info('HTTP Server: [ RUNNING ]')
-    WIKI.logger.info('🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻')
-    WIKI.logger.info('')
-    WIKI.logger.info(`Browse to http://YOUR-SERVER-IP:${WIKI.config.port}/ to complete setup!`)
-    WIKI.logger.info('')
-    WIKI.logger.info('🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺')
-  })
-}

+ 0 - 25
server/themes/default/theme.yml

@@ -1,25 +0,0 @@
-name: Default
-author: requarks.io
-site: https://wiki.requarks.io/
-version: 1.0.0
-requirements:
-  minimum: '>= 2.0.0'
-  maximum: '< 3.0.0'
-props:
-  accentColor:
-    type: String
-    title: Accent Color
-    hint: Color used in the sidebar navigation and other elements.
-    order: 1
-    default: blue darken-2
-    control: color-material
-  tocPosition:
-    type: String
-    title: Table of Contents Position
-    hint: Select whether the table of contents is shown on the left, right or not at all.
-    order: 2
-    default: left
-    enum:
-      - left
-      - right
-      - hidden

BIN
server/themes/default/thumbnail.png


+ 1 - 1
server/views/admin.pug

@@ -1,4 +1,4 @@
-extends master.pug
+extends base.pug
 
 block body
   #root

+ 26 - 17
dev/templates/legacy.pug → server/views/base.pug

@@ -1,5 +1,5 @@
 doctype html
-html
+html(lang=siteConfig.lang)
   head
     meta(http-equiv='X-UA-Compatible', content='IE=edge')
     meta(charset='UTF-8')
@@ -21,12 +21,23 @@ html
 
     //- Favicon
     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='192x192', href='/_assets/favicons/android-chrome-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.
+      var siteId = "!{siteId}"
+      var siteConfig = !{JSON.stringify(siteConfig)}
+      var siteLangs = !{JSON.stringify(langs)}
+
+    //- Dev Mode Warning
+    if devMode
+      script.
+        siteConfig.devMode = true
+
     //- Icon Set
     if config.theming.iconset === 'fa'
       link(
@@ -42,26 +53,24 @@ html
         )
 
     //- CSS
-    <% for (var index in htmlWebpackPlugin.files.css) { %>
-    link(
-      type='text/css'
-      rel='stylesheet'
-      href='<%= htmlWebpackPlugin.files.css[index] %>'
-    )
-    <% } %>
-
-    script(
-      crossorigin='anonymous'
-      src='https://polyfill.io/v3/polyfill.min.js?features=EventSource'
-    )
+    
 
     //- JS
-    <% for (var index in htmlWebpackPlugin.files.js) { %>
+    
+      
+    script(
+      type='text/javascript'
+      src='/_assets-legacy/js/runtime.js'
+      )
+      
+    
+      
     script(
       type='text/javascript'
-      src='<%= htmlWebpackPlugin.files.js[index] %>'
+      src='/_assets-legacy/js/app.js'
       )
-    <% } %>
+      
+    
 
     != analyticsCode.head
 

+ 1 - 1
server/views/editor.pug

@@ -1,4 +1,4 @@
-extends master.pug
+extends base.pug
 
 block head
   if injectCode.css

+ 1 - 1
server/views/error.pug

@@ -1,4 +1,4 @@
-extends master.pug
+extends base.pug
 
 block body
   #root.is-fullscreen

+ 1 - 1
server/views/history.pug

@@ -1,4 +1,4 @@
-extends master.pug
+extends base.pug
 
 block head
 

+ 0 - 23
server/views/legacy/login.pug

@@ -1,23 +0,0 @@
-extends master.pug
-
-block body
-  #root
-    .login-deprecated!= t('outdatedBrowserWarning', { modernBrowser: '<a href="https://bestvpn.org/outdatedbrowser/en" rel="nofollow">' + t('modernBrowser') + '</a>', interpolation: { escapeValue: false } })
-    .login
-      .login-dialog
-        if err
-          .login-error= err.message
-        form(method='post', action='/login')
-          h1= config.title
-          select(name='strategy')
-            each str in formStrategies
-              option(value=str.key, selected)= str.title
-          input(type='text', name='user', placeholder=t('auth:fields.emailUser'))
-          input(type='password', name='pass', placeholder=t('auth:fields.password'))
-          button(type='submit')= t('auth:actions.login')
-        if socialStrategies.length
-          .login-social
-            h2= t('auth:orLoginUsingStrategy')
-            each str in socialStrategies
-              a.login-social-icon(href='/login/' + str.key, class=str.color)
-                != str.icon

+ 0 - 60
server/views/legacy/page.pug

@@ -1,60 +0,0 @@
-extends master.pug
-
-block head
-  if injectCode.css
-    style(type='text/css')!= injectCode.css
-  if injectCode.head
-    != injectCode.head
-
-block body
-  #root
-    .header
-      span.header-title= siteConfig.title
-      span.header-deprecated!= t('outdatedBrowserWarning', { modernBrowser: '<a href="https://bestvpn.org/outdatedbrowser/en" rel="nofollow">' + t('modernBrowser') + '</a>', interpolation: { escapeValue: false } })
-      span.header-login
-        if !isAuthenticated
-          a(href='/login', title='Login')
-            i.mdi.mdi-account-circle
-        else
-          a(href='/logout', title='Logout')
-            i.mdi.mdi-logout
-    .main
-      .sidebar
-        each navItem in sidebar
-          if navItem.k === 'link'
-            a.sidebar-link(href=navItem.t)
-              i.mdi(class=navItem.c)
-              span= navItem.l
-          else if navItem.k === 'divider'
-            .sidebar-divider
-          else if navItem.k === 'header'
-            .sidebar-title= navItem.l
-      .main-container
-        .page-header
-          .page-header-left
-            h1= page.title
-            h2= page.description
-          //- .page-header-right
-          //-   .page-header-right-title Last edited by
-          //-   .page-header-right-author= page.authorName
-          //-   .page-header-right-updated= page.updatedAt
-        .page-contents.v-content
-          .contents
-            div!= page.render
-          if page.toc.length
-            .toc
-              .toc-title= t('page.toc')
-              each tocItem, tocIdx in page.toc
-                a.toc-tile(href=tocItem.anchor)
-                  i.mdi.mdi-chevron-right
-                  span= tocItem.title
-                if tocIdx < page.toc.length - 1 || tocItem.children.length
-                  .toc-divider
-                each tocSubItem in tocItem.children
-                  a.toc-tile.inset(href=tocSubItem.anchor)
-                    i.mdi.mdi-chevron-right
-                    span= tocSubItem.title
-                  if tocIdx < page.toc.length - 1
-                    .toc-divider.inset
-  if injectCode.body
-    != injectCode.body

+ 1 - 1
server/views/login.pug

@@ -1,4 +1,4 @@
-extends master.pug
+extends base.pug
 
 block body
   #root.is-fullscreen

+ 1 - 1
server/views/new.pug

@@ -1,4 +1,4 @@
-extends master.pug
+extends base.pug
 
 block body
   #root.is-fullscreen

+ 1 - 1
server/views/notfound.pug

@@ -1,4 +1,4 @@
-extends master.pug
+extends base.pug
 
 block body
   #root.is-fullscreen

+ 1 - 1
server/views/page.pug

@@ -1,4 +1,4 @@
-extends master.pug
+extends base.pug
 
 block head
   if injectCode.css

+ 1 - 1
server/views/profile.pug

@@ -1,4 +1,4 @@
-extends master.pug
+extends base.pug
 
 block body
   #root

+ 1 - 1
server/views/register.pug

@@ -1,4 +1,4 @@
-extends master.pug
+extends base.pug
 
 block body
   #root.is-fullscreen

+ 1 - 1
server/views/source.pug

@@ -1,4 +1,4 @@
-extends master.pug
+extends base.pug
 
 block head
 

+ 1 - 1
server/views/tags.pug

@@ -1,4 +1,4 @@
-extends master.pug
+extends base.pug
 
 block body
   #root

+ 1 - 1
server/views/unauthorized.pug

@@ -1,4 +1,4 @@
-extends master.pug
+extends base.pug
 
 block body
   #root.is-fullscreen

+ 1 - 1
server/views/welcome.pug

@@ -1,4 +1,4 @@
-extends master.pug
+extends base.pug
 
 block body
   #root.is-fullscreen

+ 13 - 8
server/master.js → server/web.js

@@ -149,15 +149,20 @@ module.exports = async () => {
   // ----------------------------------------
 
   app.use(async (req, res, next) => {
+    const currentSite = await WIKI.models.sites.getSiteByHostname({ hostname: req.hostname })
+    if (!currentSite) {
+      return res.status(404).send('Site Not Found')
+    }
+
     res.locals.siteConfig = {
-      title: WIKI.config.title,
-      theme: WIKI.config.theming.theme,
-      darkMode: WIKI.config.theming.darkMode,
-      lang: WIKI.config.lang.code,
-      rtl: WIKI.config.lang.rtl,
-      company: WIKI.config.company,
-      contentLicense: WIKI.config.contentLicense,
-      logoUrl: WIKI.config.logoUrl
+      id: currentSite.id,
+      title: currentSite.config.title,
+      darkMode: currentSite.config.theme.dark,
+      lang: currentSite.config.locale,
+      rtl: false, // TODO: handle RTL
+      company: currentSite.config.company,
+      contentLicense: currentSite.config.contentLicense,
+      logoUrl: currentSite.config.logoUrl
     }
     res.locals.langs = await WIKI.models.locales.getNavLocales({ cache: true })
     res.locals.analyticsCode = await WIKI.models.analytics.getCode({ cache: true })

+ 1 - 0
ux/.eslintrc.js

@@ -68,6 +68,7 @@ module.exports = {
     'prefer-promise-reject-errors': 'off',
 
     'no-unused-vars': 'off',
+    'vue/multi-word-component-names': 'off',
 
     // allow debugger during development only
     'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off'

+ 3 - 0
ux/index.html

@@ -8,6 +8,9 @@
     <meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width">
     <title>Wiki.js</title>
     <!--preload-links-->
+    <link rel="preconnect" href="https://fonts.googleapis.com">
+    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
+    <link href="https://fonts.googleapis.com/css2?family=Rubik:wght@300..900&display=swap" rel="stylesheet">
     <style type="text/css">
       @keyframes initspinner {
         to { transform: rotate(360deg); }

+ 14 - 15
ux/package.json

@@ -11,28 +11,28 @@
     "lint": "eslint --ext .js,.vue ./"
   },
   "dependencies": {
-    "@apollo/client": "3.6.8",
+    "@apollo/client": "3.6.9",
     "@codemirror/autocomplete": "6.0.2",
     "@codemirror/basic-setup": "0.20.0",
     "@codemirror/closebrackets": "0.19.2",
-    "@codemirror/commands": "6.0.0",
+    "@codemirror/commands": "6.0.1",
     "@codemirror/comment": "0.19.1",
     "@codemirror/fold": "0.19.4",
     "@codemirror/gutter": "0.19.9",
     "@codemirror/highlight": "0.19.8",
     "@codemirror/history": "0.19.2",
     "@codemirror/lang-css": "6.0.0",
-    "@codemirror/lang-html": "6.0.0",
-    "@codemirror/lang-javascript": "6.0.0",
+    "@codemirror/lang-html": "6.1.0",
+    "@codemirror/lang-javascript": "6.0.1",
     "@codemirror/lang-json": "6.0.0",
     "@codemirror/lang-markdown": "6.0.0",
     "@codemirror/matchbrackets": "0.19.4",
     "@codemirror/search": "6.0.0",
     "@codemirror/state": "6.0.1",
     "@codemirror/tooltip": "0.19.16",
-    "@codemirror/view": "6.0.1",
+    "@codemirror/view": "6.0.2",
     "@lezer/common": "1.0.0",
-    "@quasar/extras": "1.14.0",
+    "@quasar/extras": "1.14.2",
     "@tiptap/core": "2.0.0-beta.176",
     "@tiptap/extension-code-block": "2.0.0-beta.37",
     "@tiptap/extension-code-block-lowlight": "2.0.0-beta.68",
@@ -59,24 +59,23 @@
     "@tiptap/vue-3": "2.0.0-beta.91",
     "@vue/apollo-option": "4.0.0-alpha.17",
     "apollo-upload-client": "17.0.0",
-    "browser-fs-access": "0.29.6",
+    "browser-fs-access": "0.30.2",
     "clipboard": "2.0.11",
-    "codemirror": "6.0.0",
-    "filesize": "9.0.9",
+    "codemirror": "6.0.1",
+    "filesize": "9.0.11",
     "filesize-parser": "1.5.0",
     "graphql": "16.5.0",
     "graphql-tag": "2.12.6",
     "js-cookie": "3.0.1",
     "jwt-decode": "3.1.2",
-    "lodash": "4.17.21",
     "lodash-es": "4.17.21",
     "luxon": "2.4.0",
     "pinia": "2.0.14",
     "pug": "3.0.2",
-    "quasar": "2.7.3",
+    "quasar": "2.7.4",
     "tippy.js": "6.3.7",
     "uuid": "8.3.2",
-    "v-network-graph": "0.5.19",
+    "v-network-graph": "0.6.3",
     "vue": "3.2.37",
     "vue-codemirror": "6.0.0",
     "vue-i18n": "9.1.10",
@@ -86,10 +85,10 @@
   },
   "devDependencies": {
     "@intlify/vite-plugin-vue-i18n": "3.4.0",
-    "@quasar/app-vite": "1.0.2",
+    "@quasar/app-vite": "1.0.4",
     "@types/lodash": "4.14.182",
-    "autoprefixer": "10.4.7",
-    "eslint": "8.18.0",
+    "browserlist": "latest",
+    "eslint": "8.19.0",
     "eslint-config-standard": "17.0.0",
     "eslint-plugin-import": "2.26.0",
     "eslint-plugin-n": "15.2.3",

+ 1 - 1
ux/quasar.config.js

@@ -63,7 +63,7 @@ module.exports = configure(function (/* ctx */) {
       vueRouterMode: 'history', // available values: 'hash', 'history'
       // vueRouterBase,
       // vueDevtools,
-      vueOptionsAPI: true,
+      // vueOptionsAPI: true,
 
       rebuildCache: true, // rebuilds Vite/linter/etc cache on startup
 

+ 29 - 14
ux/src/App.vue

@@ -1,18 +1,33 @@
-<template>
-  <router-view />
+<template lang="pug">
+router-view
 </template>
 
-<script>
-import { defineComponent, nextTick, onMounted } from 'vue'
-
-export default defineComponent({
-  name: 'App',
-  setup () {
-    onMounted(() => {
-      nextTick(() => {
-        document.querySelector('.init-loading').remove()
-      })
-    })
-  }
+<script setup>
+import { nextTick, onMounted } from 'vue'
+import { useSiteStore } from 'src/stores/site'
+
+/* global siteConfig */
+
+// STORES
+
+const siteStore = useSiteStore()
+
+// INIT SITE STORE
+
+if (typeof siteConfig !== 'undefined') {
+  siteStore.$patch({
+    id: siteConfig.id,
+    title: siteConfig.title
+  })
+} else {
+  siteStore.loadSite(window.location.hostname)
+}
+
+// MOUNTED
+
+onMounted(async () => {
+  nextTick(() => {
+    document.querySelector('.init-loading').remove()
+  })
 })
 </script>

+ 1 - 2
ux/src/components/GroupEditOverlay.vue

@@ -494,9 +494,8 @@ q-layout(view='hHh lpR fFf', container)
 <script setup>
 import gql from 'graphql-tag'
 import { DateTime } from 'luxon'
-import cloneDeep from 'lodash/cloneDeep'
-import some from 'lodash/some'
 import { v4 as uuid } from 'uuid'
+import { cloneDeep, some } from 'lodash-es'
 import { fileOpen } from 'browser-fs-access'
 
 import { useI18n } from 'vue-i18n'

+ 1 - 1
ux/src/components/IconPickerDialog.vue

@@ -119,7 +119,7 @@ q-card.icon-picker(flat, style='width: 400px;')
 </template>
 
 <script>
-import find from 'lodash/find'
+import { find } from 'lodash-es'
 
 export default {
   props: {

+ 1 - 2
ux/src/components/PageDataTemplateDialog.vue

@@ -161,8 +161,7 @@ q-card.page-datatmpl-dialog(style='width: 1100px; max-width: 1100px;')
 <script>
 import { get, sync } from 'vuex-pathify'
 import { v4 as uuid } from 'uuid'
-import cloneDeep from 'lodash/cloneDeep'
-import sortBy from 'lodash/sortBy'
+import { cloneDeep, sortBy } from 'lodash-es'
 import draggable from 'vuedraggable'
 
 export default {

+ 1 - 2
ux/src/components/PageRelationDialog.vue

@@ -114,8 +114,7 @@ q-card.page-relation-dialog(style='width: 500px;')
 
 <script>
 import { v4 as uuid } from 'uuid'
-import find from 'lodash/find'
-import cloneDeep from 'lodash/cloneDeep'
+import { cloneDeep, find } from 'lodash-es'
 
 import IconPickerDialog from './IconPickerDialog.vue'
 

+ 1 - 1
ux/src/components/SiteActivateDialog.vue

@@ -30,7 +30,7 @@ q-dialog(ref='dialogRef', @hide='onDialogHide')
 
 <script setup>
 import gql from 'graphql-tag'
-import cloneDeep from 'lodash/cloneDeep'
+import { cloneDeep } from 'lodash-es'
 import { useI18n } from 'vue-i18n'
 import { useDialogPluginComponent, useQuasar } from 'quasar'
 import { reactive, ref } from 'vue'

+ 1 - 1
ux/src/components/UserChangePwdDialog.vue

@@ -69,7 +69,7 @@ q-dialog(ref='dialogRef', @hide='onDialogHide')
 
 <script setup>
 import gql from 'graphql-tag'
-import sampleSize from 'lodash/sampleSize'
+import { sampleSize } from 'lodash-es'
 import zxcvbn from 'zxcvbn'
 
 import { useI18n } from 'vue-i18n'

+ 1 - 2
ux/src/components/UserCreateDialog.vue

@@ -161,9 +161,8 @@ q-dialog(ref='dialogRef', @hide='onDialogHide')
 
 <script setup>
 import gql from 'graphql-tag'
-import sampleSize from 'lodash/sampleSize'
+import { cloneDeep, sampleSize } from 'lodash-es'
 import zxcvbn from 'zxcvbn'
-import cloneDeep from 'lodash/cloneDeep'
 import { useI18n } from 'vue-i18n'
 import { useDialogPluginComponent, useQuasar } from 'quasar'
 import { computed, onMounted, reactive, ref } from 'vue'

+ 1 - 6
ux/src/components/UserEditOverlay.vue

@@ -488,12 +488,7 @@ q-layout(view='hHh lpR fFf', container)
 
 <script setup>
 import gql from 'graphql-tag'
-import cloneDeep from 'lodash/cloneDeep'
-import some from 'lodash/some'
-import find from 'lodash/find'
-import findKey from 'lodash/findKey'
-import _get from 'lodash/get'
-import map from 'lodash/map'
+import { cloneDeep, find, findKey, map, some } from 'lodash-es'
 import { DateTime } from 'luxon'
 
 import { useI18n } from 'vue-i18n'

+ 1 - 1
ux/src/components/WebhookEditDialog.vue

@@ -194,7 +194,7 @@ q-dialog(ref='dialogRef', @hide='onDialogHide')
 
 <script setup>
 import gql from 'graphql-tag'
-import cloneDeep from 'lodash/cloneDeep'
+import { cloneDeep } from 'lodash-es'
 import { useI18n } from 'vue-i18n'
 import { useDialogPluginComponent, useQuasar } from 'quasar'
 import { computed, onMounted, reactive, ref } from 'vue'

+ 2 - 2
ux/src/css/app.scss

@@ -33,7 +33,7 @@ body::-webkit-scrollbar-thumb {
 }
 
 .font-poppins {
-  font-family: 'Poppins', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
+  font-family: $font-poppins;
 }
 
 @font-face {
@@ -117,7 +117,7 @@ body::-webkit-scrollbar-thumb {
 // BUTTONS
 // ------------------------------------------------------------------
 
-body.desktop .acrylic-btn {
+#app .acrylic-btn {
   .q-focus-helper {
     background-color: currentColor;
     opacity: .1;

+ 4 - 0
ux/src/css/quasar.variables.scss

@@ -30,3 +30,7 @@ $dark-6: #070a0d;
 $dark-5: #0d1117;
 $dark-4: #161b22;
 $dark-3: #1e232a;
+
+// -- FONTS --
+
+$font-poppins: 'Poppins', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;

+ 2 - 2
ux/src/i18n/locales/en.json

@@ -300,7 +300,7 @@
   "admin.login.logoutRedirect": "Logout Redirect",
   "admin.login.logoutRedirectHint": "Optionally redirect the user to a specific page when he/she logouts. This can be overridden at the group level.",
   "admin.login.providers": "Login Providers",
-  "admin.login.providersVisbleWarning": "Note that you can always temporarily show all hidden providers by adding ?all to the url. This is useful to login as local admin while hiding it from normal users.",
+  "admin.login.providersVisbleWarning": "Note that you can always temporarily show all hidden providers by adding ?all=1 to the url. This is useful to login as local admin while hiding it from normal users.",
   "admin.login.subtitle": "Configure the login user experience of your wiki site",
   "admin.login.title": "Login",
   "admin.login.welcomeRedirect": "First-time Login Redirect",
@@ -671,7 +671,7 @@
   "admin.theme.bodyHtmlInjectionHint": "HTML code to be injected just before the closing body tag.",
   "admin.theme.codeInjection": "Code Injection",
   "admin.theme.cssOverride": "CSS Override",
-  "admin.theme.cssOverrideHint": "CSS code to inject after system default CSS. Consider using custom themes if you have a large amount of css code. Injecting too much CSS code will result in poor page load performance! CSS will automatically be minified.",
+  "admin.theme.cssOverrideHint": "CSS code to inject after system default CSS. Injecting too much CSS code can result in poor page load performance! CSS will automatically be minified.",
   "admin.theme.cssOverrideWarning": "{caution} When adding styles for page content, you must scope them to the {cssClass} class. Omitting this could break the layout of the editor!",
   "admin.theme.cssOverrideWarningCaution": "CAUTION:",
   "admin.theme.darkMode": "Dark Mode",

+ 1 - 1
ux/src/layouts/AdminLayout.vue

@@ -79,7 +79,7 @@ q-layout.admin(view='hHh Lpr lff')
           q-item-section(avatar)
             q-icon(name='img:/_assets/icons/fluent-comments.svg')
           q-item-section {{ t('admin.comments.title') }}
-        q-item(:to='`/_admin/` + adminStore.currentSiteId + `/editors`', v-ripple, active-class='bg-primary text-white')
+        q-item(:to='`/_admin/` + adminStore.currentSiteId + `/editors`', v-ripple, active-class='bg-primary text-white', disabled)
           q-item-section(avatar)
             q-icon(name='img:/_assets/icons/fluent-cashbook.svg')
           q-item-section {{ t('admin.editors.title') }}

+ 63 - 30
ux/src/layouts/AuthLayout.vue

@@ -4,45 +4,78 @@ q-layout(view='hHh lpr lff')
     router-view
 </template>
 
-<script>
-
-export default {
-  name: 'AuthLayout',
-  data () {
-    return {
-      bgUrl: '_assets/bg/login-v3.jpg'
-      // bgUrl: 'https://docs.requarks.io/_assets/img/splash/1.jpg'
-    }
-  }
-}
+<script setup>
+
 </script>
 
 <style lang="scss">
   .auth {
     background-color: #FFF;
-    background-size: cover;
-    background-position: center center;
-    height: 100vh;
     display: flex;
-    justify-content: center;
-    align-items: center;
-
-    &-box {
-      background-color: rgba(255,255,255,.25);
-      backdrop-filter: blur(5px);
-      -webkit-backdrop-filter: blur(5px);
-      padding: 32px;
-      border-radius: 8px;
-      width: 500px;
-      max-width: 95vw;
-
-      @at-root .no-backdropfilter & {
-        background-color: rgba(255,255,255,.95);
+    font-family: 'Rubik', sans-serif;
+
+    @at-root .body--dark & {
+      background-color: $dark-6;
+    }
+
+    &-content {
+      flex: 1 0 100%;
+      width: 100%;
+      max-width: 500px;
+      padding: 3rem 4rem;
+      display: flex;
+      flex-direction: column;
+      justify-content: center;
+      align-items: stretch;
+
+      @media (max-width: $breakpoint-xs-max) {
+        padding: 1rem 2rem;
+        max-width: 100vw;
+      }
+    }
+
+    &-logo {
+      img {
+        height: 72px;
+      }
+    }
+
+    &-site-title {
+      font-size: 1.875rem;
+      line-height: 2.25rem;
+      font-weight: 700;
+      margin: 0;
+      color: $blue-grey-9;
+
+      @at-root .body--dark & {
+        color: $blue-grey-1;
       }
+    }
+
+    &-strategies {
+      display: grid;
+      grid-template-columns: repeat(auto-fit, minmax(45%, 1fr));
+      gap: 10px;
+    }
+
+    &-bg {
+      flex: 1;
+      flex-basis: 0;
+      position: relative;
+      height: 100vh;
+      overflow: hidden;
 
-      @media (max-width: $breakpoint-md-max) {
-        margin-left: 0;
+      img {
+        position: relative;
         width: 100%;
+        height: 100%;
+        object-fit: cover;
+        top: 0;
+        bottom: 0;
+        left: 0;
+        right: 0;
+        margin: 0;
+        padding: 0;
       }
     }
   }

+ 24 - 17
ux/src/pages/AdminNavigation.vue

@@ -32,23 +32,30 @@ q-page.admin-navigation
       )
   q-separator(inset)
   .row.q-pa-md.q-col-gutter-md
-    .col-6
-//- v-container(fluid, grid-list-lg)
-//-   v-layout(row wrap)
-//-     v-flex(xs12)
-//-       .admin-header
-//-         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')}}
-//-         v-spacer
-//-         v-btn.animated.fadeInDown.wait-p3s(icon, outlined, color='grey', href='https://docs.requarks.io/navigation', target='_blank')
-//-           v-icon mdi-help-circle
-//-         v-btn.mx-3.animated.fadeInDown.wait-p2s.mr-3(icon, outlined, color='grey', @click='refresh')
-//-           v-icon mdi-refresh
-//-         v-btn.animated.fadeInDown(color='success', depressed, @click='save', large)
-//-           v-icon(left) mdi-check
-//-           span {{$t('common.actions.apply')}}
+    .col-auto
+      q-card.q-mt-sm {{t('admin.navigation.mode')}}
+
+      q-card.bg-dark.q-mt-sm
+        q-list(
+          style='min-width: 350px;'
+          padding
+          dark
+          )
+          q-item
+            q-item-section
+              q-select(
+                dark
+                outlined
+                option-value='value'
+                option-label='text'
+                emit-value
+                map-options
+                dense
+                options-dense
+                :label='t(`admin.navigation.mode`)'
+                :aria-label='t(`admin.navigation.mode`)'
+                )
+
 //-       v-container.pa-0.mt-3(fluid, grid-list-lg)
 //-         v-row(dense)
 //-           v-col(cols='3')

+ 1 - 8
ux/src/pages/AdminRendering.vue

@@ -165,14 +165,7 @@ q-page.admin-mail
 </template>
 
 <script>
-import cloneDeep from 'lodash/cloneDeep'
-import concat from 'lodash/concat'
-import filter from 'lodash/filter'
-import find from 'lodash/find'
-import findIndex from 'lodash/findIndex'
-import reduce from 'lodash/reduce'
-import reverse from 'lodash/reverse'
-import sortBy from 'lodash/sortBy'
+import { cloneDeep, concat, filter, find, findIndex, reduce, reverse, sortBy } from 'lodash-es'
 import { DepGraph } from 'dependency-graph'
 import gql from 'graphql-tag'
 

+ 487 - 552
ux/src/pages/Login.vue

@@ -1,604 +1,539 @@
 <template lang='pug'>
-.auth(:style='`background-image: url(` + bgUrl + `);`')
-  .auth-box
-    .flex.mb-5
-      .auth-login-logo
-        q-avatar(square, size='34px')
-          q-img(:src='logoUrl')
-      .auth-login-title
-        .text-h6.text-grey-9 {{ siteTitle }}
-
-    q-banner.bg-red-7.text-white.q-mt-md(
-      v-if='errorShown'
-      transition='slide-y-reverse-transition'
-      dense
+.auth
+  .auth-content
+    .auth-logo
+      img(src='/_assets/logo-wikijs.svg' :alt='siteStore.title')
+    h2.auth-site-title {{ siteStore.title }}
+    p.text-grey-7 Login to continue
+    template(v-if='state.strategies?.length > 1')
+    p Sign in with
+    .auth-strategies
+      q-btn(
+        label='GitHub'
+        icon='lab la-github'
+        push
+        no-caps
+        :color='$q.dark.isActive ? `blue-grey-9` : `grey-1`'
+        :text-color='$q.dark.isActive ? `white` : `blue-grey-9`'
+        )
+      q-btn(
+        label='Google'
+        icon='lab la-google-plus'
+        push
+        no-caps
+        :color='$q.dark.isActive ? `blue-grey-9` : `grey-1`'
+        :text-color='$q.dark.isActive ? `white` : `blue-grey-9`'
+        )
+      q-btn(
+        label='Twitter'
+        icon='lab la-twitter'
+        push
+        no-caps
+        :color='$q.dark.isActive ? `blue-grey-9` : `grey-1`'
+        :text-color='$q.dark.isActive ? `white` : `blue-grey-9`'
+        )
+      q-btn(
+        label='Local'
+        icon='las la-seedling'
+        push
+        color='primary'
+        no-caps
+        )
+    q-form.q-mt-md
+      q-input(
+        outlined
+        label='Email Address'
+        autocomplete='email'
+        )
+        template(#prepend)
+          i.las.la-user
+      q-input.q-mt-sm(
+        outlined
+        label='Password'
+        type='password'
+        autocomplete='current-password'
+        )
+        template(#prepend)
+          i.las.la-key
+      q-btn.full-width.q-mt-sm(
+        push
+        color='primary'
+        label='Login'
+        no-caps
+        icon='las la-sign-in-alt'
       )
-      template(v-slot:avatar)
-        q-icon.q-pl-sm(name='las la-exclamation-triangle', size='sm')
-      span {{errorMessage}}
-    //-------------------------------------------------
-    //- PROVIDERS LIST
-    //-------------------------------------------------
-    template(v-if='screen === `login` && strategies.length > 1')
-      .auth-login-subtitle
-        .text-subtitle1 {{$t('auth.selectAuthProvider')}}
-      .auth-login-list
-        q-list.bg-white.shadow-2.rounded-borders.q-pa-sm(separator)
-          q-item.rounded-borders(
-            clickable
-            v-ripple
-            v-for='(stg, idx) of filteredStrategies'
-            :key='stg.key'
-            @click='selectedStrategyKey = stg.key'
-            :class='stg.key === selectedStrategyKey ? `bg-primary text-white` : ``'
-            )
-            q-item-section(avatar)
-              q-avatar.mr-3(:color='stg.strategy.color', rounded, size='32px')
-                div(v-html='stg.strategy.icon')
-            q-item-section
-              span.text-none {{stg.displayName}}
-    //-------------------------------------------------
-    //- LOGIN FORM
-    //-------------------------------------------------
-    template(v-if='screen === `login` && selectedStrategy.strategy.useForm')
-      .auth-login-subtitle
-        .text-subtitle1 {{$t('auth.enterCredentials')}}
-      .auth-login-form
-        q-input.text-black(
-          outlined
-          bg-color='white'
-          ref='iptEmail'
-          v-model='username'
-          :label='isUsernameEmail ? $t(`auth.fields.email`) : $t(`auth.fields.username`)'
-          :type='isUsernameEmail ? `email` : `text`'
-          :autocomplete='isUsernameEmail ? `email` : `username`'
-          )
-          template(v-slot:prepend)
-            q-icon(name='las la-user-circle', color='primary')
-        q-input.q-mt-sm(
-          outlined
-          bg-color='white'
-          ref='iptPassword'
-          v-model='password'
-          :type='hidePassword ? "password" : "text"'
-          :label='$t("auth:fields.password")'
-          autocomplete='current-password'
-          @keyup.enter='login'
-          )
-          template(v-slot:prepend)
-            q-icon(name='las la-key', color='primary')
-          template(v-slot:append)
-            q-icon.cursor-pointer(
-              :name='hidePassword ? "las la-eye-slash" : "las la-eye"'
-              @click='() => (hidePassword = !hidePassword)'
-              )
-        q-btn.q-mt-sm.q-py-xs.full-width(
-          no-caps
-          color='blue-7'
-          push
-          @click='login'
-          :loading='isLoading'
-          :label='$t(`auth.actions.login`)'
-          icon='las la-arrow-right'
-          )
-        .text-center.q-mt-lg
-          q-btn(
-            flat
-            no-caps
-            rounded
-            color='grey-8'
-            @click.stop.prevent='forgotPassword'
-            href='#forgot'
-            ): .text-caption {{ $t('auth.forgotPasswordLink') }}
-          q-btn(
-            v-if='selectedStrategyKey === `local` && selectedStrategy.selfRegistration'
-            color='indigo darken-2'
-            flat
-            no-caps
-            rounded
-            href='/register'
-            ): .text-caption {{ $t('auth.switchToRegister.link') }}
+    q-separator.q-my-md
+    q-btn.acrylic-btn.full-width(
+      flat
+      color='primary'
+      label='Create an Account'
+      no-caps
+      icon='las la-user-plus'
+    )
+    q-btn.acrylic-btn.full-width.q-mt-sm(
+      flat
+      color='primary'
+      label='Forgot Password'
+      no-caps
+      icon='las la-life-ring'
+    )
+  .auth-bg(aria-hidden="true")
+    img(src='https://docs.requarks.io/_assets/img/splash/1.jpg' alt='')
 </template>
 
-<script>
-import { get } from 'vuex-pathify'
+<script setup>
 import gql from 'graphql-tag'
-import find from 'lodash/find'
-import _get from 'lodash/get'
-import has from 'lodash/has'
-import head from 'lodash/head'
-import reject from 'lodash/reject'
-import sortBy from 'lodash/sortBy'
+import { find, has, head, reject, sortBy } from 'lodash-es'
 import Cookies from 'js-cookie'
 
-export default {
-  name: 'PageLogin',
-  i18nOptions: { namespaces: 'auth' },
-  data () {
-    return {
-      error: false,
-      strategies: [],
-      selectedStrategyKey: 'unselected',
-      selectedStrategy: { key: 'unselected', strategy: { useForm: false, usernameType: 'email' } },
-      screen: 'login',
-      username: '',
-      password: '',
-      hidePassword: true,
-      securityCode: '',
-      continuationToken: '',
-      isLoading: false,
-      loaderColor: 'grey darken-4',
-      loaderTitle: 'Working...',
-      isShown: false,
-      newPassword: '',
-      newPasswordVerify: '',
-      isTFAShown: false,
-      isTFASetupShown: false,
-      tfaQRImage: '',
-      errorShown: false,
-      errorMessage: '',
-      bgUrl: '_assets/bg/login-v3.jpg'
-    }
-  },
-  computed: {
-    logoUrl: get('site/logoUrl', false),
-    siteTitle: get('site/title', false),
-    isSocialShown () {
-      return this.strategies.length > 1
-    },
-    filteredStrategies () {
-      const qParams = new URLSearchParams(!import.meta.env.SSR ? window.location.search : '')
-      if (this.hideLocal && !qParams.has('all')) {
-        return reject(this.strategies, ['key', 'local'])
-      } else {
-        return this.strategies
-      }
-    },
-    isUsernameEmail () {
-      return this.selectedStrategy.strategy.usernameType === 'email'
-    }
-  },
-  watch: {
-    filteredStrategies (newValue, oldValue) {
-      if (head(newValue).strategy.useForm) {
-        this.selectedStrategyKey = head(newValue).key
-      }
-    },
-    selectedStrategyKey (newValue, oldValue) {
-      this.selectedStrategy = find(this.strategies, ['key', newValue])
-      if (this.screen === 'changePwd') {
-        return
-      }
-      this.screen = 'login'
-      if (!this.selectedStrategy.strategy.useForm) {
-        this.isLoading = true
-        window.location.assign('/login/' + newValue)
-      } else {
-        this.$nextTick(() => {
-          this.$refs.iptEmail.focus()
-        })
+import { useI18n } from 'vue-i18n'
+import { useMeta, useQuasar } from 'quasar'
+import { onMounted, reactive, watch } from 'vue'
+
+import { useSiteStore } from 'src/stores/site'
+import { useDataStore } from 'src/stores/data'
+
+// QUASAR
+
+const $q = useQuasar()
+
+// STORES
+
+const siteStore = useSiteStore()
+const dataStore = useDataStore()
+
+// I18N
+
+const { t } = useI18n()
+
+// META
+
+useMeta({
+  title: t('auth.login.title')
+})
+
+// DATA
+
+const state = reactive({
+  error: false,
+  strategies: [],
+  selectedStrategyKey: 'unselected',
+  selectedStrategy: { key: 'unselected', strategy: { useForm: false, usernameType: 'email' } },
+  screen: 'login',
+  username: '',
+  password: '',
+  hidePassword: true,
+  securityCode: '',
+  continuationToken: '',
+  isLoading: false,
+  loaderColor: 'grey darken-4',
+  loaderTitle: 'Working...',
+  isShown: false,
+  newPassword: '',
+  newPasswordVerify: '',
+  isTFAShown: false,
+  isTFASetupShown: false,
+  tfaQRImage: '',
+  errorShown: false,
+  errorMessage: '',
+  bgUrl: '_assets/bg/login-v3.jpg'
+})
+
+// isSocialShown () {
+//   return this.strategies.length > 1
+// }
+// filteredStrategies () {
+//   const qParams = new URLSearchParams(!import.meta.env.SSR ? window.location.search : '')
+//   if (this.hideLocal && !qParams.has('all')) {
+//     return reject(this.strategies, ['key', 'local'])
+//   } else {
+//     return this.strategies
+//   }
+// }
+// isUsernameEmail () {
+//   return this.selectedStrategy.strategy.usernameType === 'email'
+// }
+
+// filteredStrategies (newValue, oldValue) {
+//   if (head(newValue).strategy.useForm) {
+//     this.selectedStrategyKey = head(newValue).key
+//   }
+// }
+// selectedStrategyKey (newValue, oldValue) {
+//   this.selectedStrategy = find(this.strategies, ['key', newValue])
+//   if (this.screen === 'changePwd') {
+//     return
+//   }
+//   this.screen = 'login'
+//   if (!this.selectedStrategy.strategy.useForm) {
+//     this.isLoading = true
+//     window.location.assign('/login/' + newValue)
+//   } else {
+//     this.$nextTick(() => {
+//       this.$refs.iptEmail.focus()
+//     })
+//   }
+// }
+
+//   mounted () {
+//     this.isShown = true
+//     if (this.changePwdContinuationToken) {
+//       this.screen = 'changePwd'
+//       this.continuationToken = this.changePwdContinuationToken
+//     }
+//   }
+
+// METHODS
+
+async function fetchStrategies () {
+  const resp = await APOLLO_CLIENT.query({
+    query: gql`
+      query loginFetchSiteStrategies(
+        $siteId: UUID
+      ) {
+        authStrategies(
+          siteId: $siteId
+          enabledOnly: true
+          ) {
+          key
+          strategy {
+            key
+            logo
+            color
+            icon
+            useForm
+            usernameType
+          }
+          displayName
+          order
+          selfRegistration
+        }
       }
+    `,
+    variables: {
+      siteId: siteStore.id
     }
-  },
-  mounted () {
-    this.isShown = true
-    if (this.changePwdContinuationToken) {
-      this.screen = 'changePwd'
-      this.continuationToken = this.changePwdContinuationToken
-    }
-  },
-  methods: {
-    /**
-     * LOGIN
-     */
-    async login () {
-      this.errorShown = false
-      if (this.username.length < 2) {
-        this.errorMessage = this.$t('auth.invalidEmailUsername')
-        this.errorShown = true
-        this.$refs.iptEmail.focus()
-      } else if (this.password.length < 2) {
-        this.errorMessage = this.$t('auth.invalidPassword')
-        this.errorShown = true
-        this.$refs.iptPassword.focus()
-      } else {
-        this.loaderColor = 'grey darken-4'
-        this.loaderTitle = this.$t('auth.signingIn')
-        this.isLoading = true
-        try {
-          const resp = await this.$apollo.mutate({
-            mutation: gql`
-              mutation($username: String!, $password: String!, $strategy: String!) {
-                authentication {
-                  login(username: $username, password: $password, strategy: $strategy) {
-                    responseResult {
-                      succeeded
-                      errorCode
-                      slug
-                      message
-                    }
-                    jwt
-                    mustChangePwd
-                    mustProvideTFA
-                    mustSetupTFA
-                    continuationToken
-                    redirect
-                    tfaQRImage
-                  }
+  })
+}
+
+/**
+ * LOGIN
+ */
+async function login () {
+  this.errorShown = false
+  if (this.username.length < 2) {
+    this.errorMessage = t('auth.invalidEmailUsername')
+    this.errorShown = true
+    this.$refs.iptEmail.focus()
+  } else if (this.password.length < 2) {
+    this.errorMessage = t('auth.invalidPassword')
+    this.errorShown = true
+    this.$refs.iptPassword.focus()
+  } else {
+    this.loaderColor = 'grey darken-4'
+    this.loaderTitle = t('auth.signingIn')
+    this.isLoading = true
+    try {
+      const resp = await this.$apollo.mutate({
+        mutation: gql`
+          mutation($username: String!, $password: String!, $strategy: String!) {
+            authentication {
+              login(username: $username, password: $password, strategy: $strategy) {
+                responseResult {
+                  succeeded
+                  errorCode
+                  slug
+                  message
                 }
+                jwt
+                mustChangePwd
+                mustProvideTFA
+                mustSetupTFA
+                continuationToken
+                redirect
+                tfaQRImage
               }
-            `,
-            variables: {
-              username: this.username,
-              password: this.password,
-              strategy: this.selectedStrategy.key
-            }
-          })
-          if (has(resp, 'data.authentication.login')) {
-            const respObj = _get(resp, 'data.authentication.login', {})
-            if (respObj.responseResult.succeeded === true) {
-              this.handleLoginResponse(respObj)
-            } else {
-              throw new Error(respObj.responseResult.message)
             }
-          } else {
-            throw new Error(this.$t('auth.genericError'))
           }
-        } catch (err) {
-          console.error(err)
-          this.$q.notify({
-            type: 'negative',
-            message: err.message
-          })
-          this.isLoading = false
+        `,
+        variables: {
+          username: this.username,
+          password: this.password,
+          strategy: this.selectedStrategy.key
         }
-      }
-    },
-    /**
-     * VERIFY TFA CODE
-     */
-    async verifySecurityCode (setup = false) {
-      if (this.securityCode.length !== 6) {
-        this.$store.commit('showNotification', {
-          style: 'red',
-          message: 'Enter a valid security code.',
-          icon: 'alert'
-        })
-        if (setup) {
-          this.$refs.iptTFASetup.focus()
+      })
+      if (has(resp, 'data.authentication.login')) {
+        const respObj = resp?.data?.authentication?.login ?? {}
+        if (respObj.responseResult.succeeded === true) {
+          this.handleLoginResponse(respObj)
         } else {
-          this.$refs.iptTFA.focus()
+          throw new Error(respObj.responseResult.message)
         }
       } else {
-        this.loaderColor = 'grey darken-4'
-        this.loaderTitle = this.$t('auth.signingIn')
-        this.isLoading = true
-        try {
-          const resp = await this.$apollo.mutate({
-            mutation: gql`
-              mutation(
-                $continuationToken: String!
-                $securityCode: String!
-                $setup: Boolean
-                ) {
-                authentication {
-                  loginTFA(
-                    continuationToken: $continuationToken
-                    securityCode: $securityCode
-                    setup: $setup
-                    ) {
-                    responseResult {
-                      succeeded
-                      errorCode
-                      slug
-                      message
-                    }
-                    jwt
-                    mustChangePwd
-                    continuationToken
-                    redirect
-                  }
-                }
-              }
-            `,
-            variables: {
-              continuationToken: this.continuationToken,
-              securityCode: this.securityCode,
-              setup
-            }
-          })
-          if (has(resp, 'data.authentication.loginTFA')) {
-            const respObj = _get(resp, 'data.authentication.loginTFA', {})
-            if (respObj.responseResult.succeeded === true) {
-              this.handleLoginResponse(respObj)
-            } else {
-              if (!setup) {
-                this.isTFAShown = false
-              }
-              throw new Error(respObj.responseResult.message)
-            }
-          } else {
-            throw new Error(this.$t('auth.genericError'))
-          }
-        } catch (err) {
-          console.error(err)
-          this.$q.notify({
-            type: 'negative',
-            message: err.message
-          })
-          this.isLoading = false
-        }
+        throw new Error(t('auth.genericError'))
       }
-    },
-    /**
-     * CHANGE PASSWORD
-     */
-    async changePassword () {
-      this.loaderColor = 'grey darken-4'
-      this.loaderTitle = this.$t('auth.changePwd.loading')
-      this.isLoading = true
-      try {
-        const resp = await this.$apollo.mutate({
-          mutation: gql`
-            mutation (
-              $continuationToken: String!
-              $newPassword: String!
+    } catch (err) {
+      console.error(err)
+      this.$q.notify({
+        type: 'negative',
+        message: err.message
+      })
+      this.isLoading = false
+    }
+  }
+}
+
+/**
+ * VERIFY TFA CODE
+ */
+async function verifySecurityCode (setup = false) {
+  if (this.securityCode.length !== 6) {
+    this.$store.commit('showNotification', {
+      style: 'red',
+      message: 'Enter a valid security code.',
+      icon: 'alert'
+    })
+    if (setup) {
+      this.$refs.iptTFASetup.focus()
+    } else {
+      this.$refs.iptTFA.focus()
+    }
+  } else {
+    this.loaderColor = 'grey darken-4'
+    this.loaderTitle = t('auth.signingIn')
+    this.isLoading = true
+    try {
+      const resp = await this.$apollo.mutate({
+        mutation: gql`
+          mutation(
+            $continuationToken: String!
+            $securityCode: String!
+            $setup: Boolean
             ) {
-              authentication {
-                loginChangePassword (
-                  continuationToken: $continuationToken
-                  newPassword: $newPassword
+            authentication {
+              loginTFA(
+                continuationToken: $continuationToken
+                securityCode: $securityCode
+                setup: $setup
                 ) {
-                  responseResult {
-                    succeeded
-                    errorCode
-                    slug
-                    message
-                  }
-                  jwt
-                  continuationToken
-                  redirect
+                responseResult {
+                  succeeded
+                  errorCode
+                  slug
+                  message
                 }
+                jwt
+                mustChangePwd
+                continuationToken
+                redirect
               }
             }
-          `,
-          variables: {
-            continuationToken: this.continuationToken,
-            newPassword: this.newPassword
-          }
-        })
-        if (has(resp, 'data.authentication.loginChangePassword')) {
-          const respObj = _get(resp, 'data.authentication.loginChangePassword', {})
-          if (respObj.responseResult.succeeded === true) {
-            this.handleLoginResponse(respObj)
-          } else {
-            throw new Error(respObj.responseResult.message)
           }
+        `,
+        variables: {
+          continuationToken: this.continuationToken,
+          securityCode: this.securityCode,
+          setup
+        }
+      })
+      if (has(resp, 'data.authentication.loginTFA')) {
+        const respObj = resp?.data?.authentication?.loginTFA ?? {}
+        if (respObj.responseResult.succeeded === true) {
+          this.handleLoginResponse(respObj)
         } else {
-          throw new Error(this.$t('auth.genericError'))
+          if (!setup) {
+            this.isTFAShown = false
+          }
+          throw new Error(respObj.responseResult.message)
         }
-      } catch (err) {
-        console.error(err)
-        this.$store.commit('showNotification', {
-          style: 'red',
-          message: err.message,
-          icon: 'alert'
-        })
-        this.isLoading = false
+      } else {
+        throw new Error(t('auth.genericError'))
       }
-    },
-    /**
-     * SWITCH TO FORGOT PASSWORD SCREEN
-     */
-    forgotPassword () {
-      this.screen = 'forgot'
-      this.$nextTick(() => {
-        this.$refs.iptForgotPwdEmail.focus()
+    } catch (err) {
+      console.error(err)
+      this.$q.notify({
+        type: 'negative',
+        message: err.message
       })
-    },
-    /**
-     * FORGOT PASSWORD SUBMIT
-     */
-    async forgotPasswordSubmit () {
-      this.loaderColor = 'grey darken-4'
-      this.loaderTitle = this.$t('auth.forgotPasswordLoading')
-      this.isLoading = true
-      try {
-        const resp = await this.$apollo.mutate({
-          mutation: gql`
-            mutation (
-              $email: String!
+      this.isLoading = false
+    }
+  }
+}
+
+/**
+ * CHANGE PASSWORD
+ */
+async function changePassword () {
+  this.loaderColor = 'grey darken-4'
+  this.loaderTitle = t('auth.changePwd.loading')
+  this.isLoading = true
+  try {
+    const resp = await this.$apollo.mutate({
+      mutation: gql`
+        mutation (
+          $continuationToken: String!
+          $newPassword: String!
+        ) {
+          authentication {
+            loginChangePassword (
+              continuationToken: $continuationToken
+              newPassword: $newPassword
             ) {
-              authentication {
-                forgotPassword (
-                  email: $email
-                ) {
-                  responseResult {
-                    succeeded
-                    errorCode
-                    slug
-                    message
-                  }
-                }
+              responseResult {
+                succeeded
+                errorCode
+                slug
+                message
               }
+              jwt
+              continuationToken
+              redirect
             }
-          `,
-          variables: {
-            email: this.username
           }
-        })
-        if (has(resp, 'data.authentication.forgotPassword.responseResult')) {
-          const respObj = _get(resp, 'data.authentication.forgotPassword.responseResult', {})
-          if (respObj.succeeded === true) {
-            this.$store.commit('showNotification', {
-              style: 'success',
-              message: this.$t('auth.forgotPasswordSuccess'),
-              icon: 'email'
-            })
-            this.screen = 'login'
-          } else {
-            throw new Error(respObj.message)
-          }
-        } else {
-          throw new Error(this.$t('auth.genericError'))
         }
-      } catch (err) {
-        console.error(err)
-        this.$store.commit('showNotification', {
-          style: 'red',
-          message: err.message,
-          icon: 'alert'
-        })
+      `,
+      variables: {
+        continuationToken: this.continuationToken,
+        newPassword: this.newPassword
       }
-      this.isLoading = false
-    },
-    handleLoginResponse (respObj) {
-      this.continuationToken = respObj.continuationToken
-      if (respObj.mustChangePwd === true) {
-        this.screen = 'changePwd'
-        this.$nextTick(() => {
-          this.$refs.iptNewPassword.focus()
-        })
-        this.isLoading = false
-      } else if (respObj.mustProvideTFA === true) {
-        this.securityCode = ''
-        this.isTFAShown = true
-        setTimeout(() => {
-          this.$refs.iptTFA.focus()
-        }, 500)
-        this.isLoading = false
-      } else if (respObj.mustSetupTFA === true) {
-        this.securityCode = ''
-        this.isTFASetupShown = true
-        this.tfaQRImage = respObj.tfaQRImage
-        setTimeout(() => {
-          this.$refs.iptTFASetup.focus()
-        }, 500)
-        this.isLoading = false
+    })
+    if (has(resp, 'data.authentication.loginChangePassword')) {
+      const respObj = resp?.data?.authentication?.loginChangePassword ?? {}
+      if (respObj.responseResult.succeeded === true) {
+        this.handleLoginResponse(respObj)
       } else {
-        this.loaderColor = 'green darken-1'
-        this.loaderTitle = this.$t('auth.loginSuccess')
-        Cookies.set('jwt', respObj.jwt, { expires: 365 })
-        setTimeout(() => {
-          const loginRedirect = Cookies.get('loginRedirect')
-          if (loginRedirect === '/' && respObj.redirect) {
-            Cookies.remove('loginRedirect')
-            window.location.replace(respObj.redirect)
-          } else if (loginRedirect) {
-            Cookies.remove('loginRedirect')
-            window.location.replace(loginRedirect)
-          } else if (respObj.redirect) {
-            window.location.replace(respObj.redirect)
-          } else {
-            window.location.replace('/')
-          }
-        }, 1000)
+        throw new Error(respObj.responseResult.message)
       }
+    } else {
+      throw new Error(t('auth.genericError'))
     }
-  },
-  apollo: {
-    strategies: {
-      prefetch: false,
-      query: gql`
-        {
+  } catch (err) {
+    console.error(err)
+    this.$store.commit('showNotification', {
+      style: 'red',
+      message: err.message,
+      icon: 'alert'
+    })
+    this.isLoading = false
+  }
+}
+
+/**
+ * SWITCH TO FORGOT PASSWORD SCREEN
+ */
+function forgotPassword () {
+  this.screen = 'forgot'
+  this.$nextTick(() => {
+    this.$refs.iptForgotPwdEmail.focus()
+  })
+}
+
+/**
+ * FORGOT PASSWORD SUBMIT
+ */
+async function forgotPasswordSubmit () {
+  this.loaderColor = 'grey darken-4'
+  this.loaderTitle = t('auth.forgotPasswordLoading')
+  this.isLoading = true
+  try {
+    const resp = await this.$apollo.mutate({
+      mutation: gql`
+        mutation (
+          $email: String!
+        ) {
           authentication {
-            activeStrategies(enabledOnly: true) {
-              key
-              strategy {
-                key
-                logo
-                color
-                icon
-                useForm
-                usernameType
+            forgotPassword (
+              email: $email
+            ) {
+              responseResult {
+                succeeded
+                errorCode
+                slug
+                message
               }
-              displayName
-              order
-              selfRegistration
             }
           }
         }
       `,
-      update: (data) => sortBy(data.authentication.activeStrategies, ['order']),
-      watchLoading (isLoading) {
-        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'login-strategies-refresh')
+      variables: {
+        email: this.username
+      }
+    })
+    if (has(resp, 'data.authentication.forgotPassword.responseResult')) {
+      const respObj = resp?.data?.authentication?.forgotPassword?.responseResult ?? {}
+      if (respObj.succeeded === true) {
+        this.$store.commit('showNotification', {
+          style: 'success',
+          message: t('auth.forgotPasswordSuccess'),
+          icon: 'email'
+        })
+        this.screen = 'login'
+      } else {
+        throw new Error(respObj.message)
       }
+    } else {
+      throw new Error(t('auth.genericError'))
     }
+  } catch (err) {
+    console.error(err)
+    this.$store.commit('showNotification', {
+      style: 'red',
+      message: err.message,
+      icon: 'alert'
+    })
   }
+  this.isLoading = false
 }
-</script>
-
-<style lang="scss">
-  .auth-login {
-    &-logo {
-      padding: 12px 0 0 12px;
-      width: 58px;
-      height: 58px;
-      background-color: #000;
-      margin-left: 12px;
-      border-radius: 7px;
-    }
-
-    &-title {
-      height: 58px;
-      padding-left: 12px;
-      display: flex;
-      align-items: center;
-      text-shadow: .5px .5px #FFF;
-    }
-
-    &-subtitle {
-      padding: 24px 12px 12px 12px;
-      color: #111;
-      font-weight: 500;
-      text-shadow: 1px 1px rgba(255,255,255,.5);
-      background-image: linear-gradient(to bottom, rgba(0,0,0,0), rgba(0,0,0,.15));
-      text-align: center;
-      border-bottom: 1px solid rgba(0,0,0,.3);
-    }
-
-    &-info {
-      border-top: 1px solid rgba(255,255,255,.85);
-      background-color: rgba(255,255,255,.15);
-      border-bottom: 1px solid rgba(0,0,0,.15);
-      padding: 12px;
-      font-size: 13px;
-      text-align: center;
-      color: mc('grey', '900');
-    }
-
-    &-list {
-      border-top: 1px solid rgba(255,255,255,.85);
-      padding: 12px;
-    }
 
-    &-form {
-      padding: 12px;
-      border-top: 1px solid rgba(255,255,255,.85);
-    }
-
-    &-main {
-      flex: 1 0 100vw;
-      height: 100vh;
-    }
-
-    &-tfa {
-      background-color: #EEE;
-      border: 7px solid #FFF;
-
-      &-field input {
-        text-align: center;
-      }
-
-      &-qr {
-        background-color: #FFF;
-        padding: 5px;
-        border-radius: 5px;
-        width: 200px;
-        height: 200px;
-        margin: 0 auto;
+function handleLoginResponse (respObj) {
+  this.continuationToken = respObj.continuationToken
+  if (respObj.mustChangePwd === true) {
+    this.screen = 'changePwd'
+    this.$nextTick(() => {
+      this.$refs.iptNewPassword.focus()
+    })
+    this.isLoading = false
+  } else if (respObj.mustProvideTFA === true) {
+    this.securityCode = ''
+    this.isTFAShown = true
+    setTimeout(() => {
+      this.$refs.iptTFA.focus()
+    }, 500)
+    this.isLoading = false
+  } else if (respObj.mustSetupTFA === true) {
+    this.securityCode = ''
+    this.isTFASetupShown = true
+    this.tfaQRImage = respObj.tfaQRImage
+    setTimeout(() => {
+      this.$refs.iptTFASetup.focus()
+    }, 500)
+    this.isLoading = false
+  } else {
+    this.loaderColor = 'green darken-1'
+    this.loaderTitle = t('auth.loginSuccess')
+    Cookies.set('jwt', respObj.jwt, { expires: 365 })
+    setTimeout(() => {
+      const loginRedirect = Cookies.get('loginRedirect')
+      if (loginRedirect === '/' && respObj.redirect) {
+        Cookies.remove('loginRedirect')
+        window.location.replace(respObj.redirect)
+      } else if (loginRedirect) {
+        Cookies.remove('loginRedirect')
+        window.location.replace(loginRedirect)
+      } else if (respObj.redirect) {
+        window.location.replace(respObj.redirect)
+      } else {
+        window.location.replace('/')
       }
-    }
+    }, 1000)
   }
+}
+
+onMounted(() => {
+  fetchStrategies()
+})
+</script>
+
+<style lang="scss">
 
 </style>

+ 7 - 7
ux/src/router/routes.js

@@ -8,13 +8,13 @@ const routes = [
   //     { path: 'n/:editor?', component: () => import('../pages/Index.vue') }
   //   ]
   // },
-  // {
-  //   path: '/login',
-  //   component: () => import('../layouts/AuthLayout.vue'),
-  //   children: [
-  //     { path: '', component: () => import('../pages/Login.vue') }
-  //   ]
-  // },
+  {
+    path: '/login',
+    component: () => import('../layouts/AuthLayout.vue'),
+    children: [
+      { path: '', component: () => import('../pages/Login.vue') }
+    ]
+  },
   // {
   //   path: '/p',
   //   component: () => import('../layouts/ProfileLayout.vue'),

+ 1 - 1
ux/src/stores/admin.js

@@ -1,6 +1,6 @@
 import { defineStore } from 'pinia'
 import gql from 'graphql-tag'
-import cloneDeep from 'lodash/cloneDeep'
+import { cloneDeep } from 'lodash-es'
 
 /* global APOLLO_CLIENT */
 

+ 1 - 1
ux/src/stores/site.js

@@ -1,6 +1,6 @@
 import { defineStore } from 'pinia'
 import gql from 'graphql-tag'
-import clone from 'lodash/clone'
+import { clone } from 'lodash-es'
 
 export const useSiteStore = defineStore('site', {
   state: () => ({

Diferenças do arquivo suprimidas por serem muito extensas
+ 291 - 258
ux/yarn.lock


Alguns arquivos não foram mostrados porque muitos arquivos mudaram nesse diff