Explorar o código

refactor: initial import from v3 prototype

NGPixel %!s(int64=3) %!d(string=hai) anos
pai
achega
3a57145e3d
Modificáronse 100 ficheiros con 1540 adicións e 2202 borrados
  1. 7 30
      config.sample.yml
  2. 4 6
      package.json
  3. 4 4
      server/app/data.yml
  4. 13 86
      server/core/db.js
  5. 0 115
      server/db/beta/index.js
  6. 0 259
      server/db/beta/migrations-sqlite/2.0.0-beta.1.js
  7. 0 52
      server/db/beta/migrations-sqlite/2.0.0-beta.11.js
  8. 0 15
      server/db/beta/migrations-sqlite/2.0.0-beta.127.js
  9. 0 13
      server/db/beta/migrations-sqlite/2.0.0-beta.205.js
  10. 0 13
      server/db/beta/migrations-sqlite/2.0.0-beta.217.js
  11. 0 13
      server/db/beta/migrations-sqlite/2.0.0-beta.242.js
  12. 0 17
      server/db/beta/migrations-sqlite/2.0.0-beta.293.js
  13. 0 15
      server/db/beta/migrations-sqlite/2.0.0-beta.38.js
  14. 0 12
      server/db/beta/migrations-sqlite/2.0.0-beta.99.js
  15. 0 35
      server/db/beta/migrations-sqlite/2.0.0-rc.2.js
  16. 0 292
      server/db/beta/migrations/2.0.0-beta.1.js
  17. 0 15
      server/db/beta/migrations/2.0.0-beta.11.js
  18. 0 15
      server/db/beta/migrations/2.0.0-beta.127.js
  19. 0 23
      server/db/beta/migrations/2.0.0-beta.148.js
  20. 0 19
      server/db/beta/migrations/2.0.0-beta.205.js
  21. 0 13
      server/db/beta/migrations/2.0.0-beta.217.js
  22. 0 13
      server/db/beta/migrations/2.0.0-beta.242.js
  23. 0 23
      server/db/beta/migrations/2.0.0-beta.293.js
  24. 0 15
      server/db/beta/migrations/2.0.0-beta.38.js
  25. 0 18
      server/db/beta/migrations/2.0.0-beta.99.js
  26. 0 54
      server/db/beta/migrations/2.0.0-rc.2.js
  27. 0 20
      server/db/beta/migrations/2.0.0-rc.29.js
  28. 32 0
      server/db/legacy/index.js
  29. 0 268
      server/db/migrations-sqlite/2.0.0.js
  30. 0 9
      server/db/migrations-sqlite/2.2.17.js
  31. 0 14
      server/db/migrations-sqlite/2.2.3.js
  32. 0 8
      server/db/migrations-sqlite/2.3.10.js
  33. 0 10
      server/db/migrations-sqlite/2.3.14.js
  34. 0 8
      server/db/migrations-sqlite/2.3.23.js
  35. 0 15
      server/db/migrations-sqlite/2.4.13.js
  36. 0 11
      server/db/migrations-sqlite/2.4.36.js
  37. 0 8
      server/db/migrations-sqlite/2.4.61.js
  38. 0 34
      server/db/migrations-sqlite/2.5.1.js
  39. 0 14
      server/db/migrations-sqlite/2.5.108.js
  40. 0 6
      server/db/migrations-sqlite/2.5.118.js
  41. 0 8
      server/db/migrations-sqlite/2.5.12.js
  42. 0 9
      server/db/migrations-sqlite/2.5.122.js
  43. 0 7
      server/db/migrations-sqlite/2.5.128.js
  44. 0 325
      server/db/migrations/2.0.0.js
  45. 0 19
      server/db/migrations/2.1.85.js
  46. 0 37
      server/db/migrations/2.2.17.js
  47. 0 20
      server/db/migrations/2.2.3.js
  48. 0 8
      server/db/migrations/2.3.10.js
  49. 0 8
      server/db/migrations/2.3.23.js
  50. 0 25
      server/db/migrations/2.4.13.js
  51. 0 16
      server/db/migrations/2.4.14.js
  52. 0 11
      server/db/migrations/2.4.36.js
  53. 0 8
      server/db/migrations/2.4.61.js
  54. 0 34
      server/db/migrations/2.5.1.js
  55. 0 14
      server/db/migrations/2.5.108.js
  56. 0 6
      server/db/migrations/2.5.118.js
  57. 0 8
      server/db/migrations/2.5.12.js
  58. 0 20
      server/db/migrations/2.5.122.js
  59. 0 7
      server/db/migrations/2.5.128.js
  60. 589 0
      server/db/migrations/3.0.0.js
  61. 1 1
      server/db/migrator-source.js
  62. 172 0
      server/graph/resolvers/site.js
  63. 59 0
      server/graph/scalars/json.js
  64. 39 0
      server/graph/scalars/uuid.js
  65. 5 0
      server/graph/schemas/common.graphql
  66. 3 0
      server/graph/schemas/scalars.graphql
  67. 167 1
      server/graph/schemas/site.graphql
  68. 9 0
      ux/.editorconfig
  69. 6 0
      ux/.eslintignore
  70. 75 0
      ux/.eslintrc.js
  71. 29 0
      ux/.gitignore
  72. 14 0
      ux/.vscode/extensions.json
  73. 16 0
      ux/.vscode/settings.json
  74. 33 0
      ux/README.md
  75. 14 0
      ux/apollo.config.js
  76. 74 0
      ux/index.html
  77. 39 0
      ux/jsconfig.json
  78. 102 0
      ux/package.json
  79. 27 0
      ux/postcss.config.js
  80. BIN=BIN
      ux/public/_assets/bg/login-v3.jpg
  81. BIN=BIN
      ux/public/_assets/bg/login.jpg
  82. BIN=BIN
      ux/public/_assets/bg/test-1.jpg
  83. BIN=BIN
      ux/public/_assets/bg/test-2.jpg
  84. 1 0
      ux/public/_assets/icons/color-blog.svg
  85. 0 0
      ux/public/_assets/icons/color-nodejs.svg
  86. 0 0
      ux/public/_assets/icons/color-postgresql.svg
  87. 0 0
      ux/public/_assets/icons/fluent-account.svg
  88. 0 0
      ux/public/_assets/icons/fluent-advance.svg
  89. 0 0
      ux/public/_assets/icons/fluent-api.svg
  90. 0 0
      ux/public/_assets/icons/fluent-apps-tab.svg
  91. 1 0
      ux/public/_assets/icons/fluent-bar-chart.svg
  92. 0 0
      ux/public/_assets/icons/fluent-bunch-of-keys.svg
  93. 1 0
      ux/public/_assets/icons/fluent-bursts.svg
  94. 1 0
      ux/public/_assets/icons/fluent-cashbook.svg
  95. 0 0
      ux/public/_assets/icons/fluent-change-theme.svg
  96. 0 0
      ux/public/_assets/icons/fluent-color-wheel.svg
  97. 1 0
      ux/public/_assets/icons/fluent-comments.svg
  98. 1 0
      ux/public/_assets/icons/fluent-database.svg
  99. 0 0
      ux/public/_assets/icons/fluent-delete-bin.svg
  100. 1 0
      ux/public/_assets/icons/fluent-down.svg

+ 7 - 30
config.sample.yml

@@ -2,7 +2,7 @@
 # Wiki.js - CONFIGURATION                                             #
 #######################################################################
 # Full documentation + examples:
-# https://docs.requarks.io/install
+# https://docs.js.wiki/install
 
 # ---------------------------------------------------------------------
 # Port the server should listen to
@@ -13,25 +13,20 @@ port: 3000
 # ---------------------------------------------------------------------
 # Database
 # ---------------------------------------------------------------------
-# Supported Database Engines:
-# - postgres = PostgreSQL 9.5 or later
-# - mysql = MySQL 8.0 or later (5.7.8 partially supported, refer to docs)
-# - mariadb = MariaDB 10.2.7 or later
-# - mssql = MS SQL Server 2012 or later
-# - sqlite = SQLite 3.9 or later
+# PostgreSQL 9.6 or later required
 
 db:
-  type: postgres
-
-  # PostgreSQL / MySQL / MariaDB / MS SQL Server only:
   host: localhost
   port: 5432
   user: wikijs
   pass: wikijsrocks
   db: wiki
+  schemas:
+    wiki: wiki
+    scheduler: scheduler
   ssl: false
 
-  # Optional - PostgreSQL / MySQL / MariaDB only:
+  # Optional
   # -> Uncomment lines you need below and set `auto` to false
   # -> Full list of accepted options: https://nodejs.org/api/tls.html#tls_tls_createsecurecontext_options
   sslOptions:
@@ -43,12 +38,6 @@ db:
     # pfx: path/to/cert.pfx
     # passphrase: xyz123
 
-  # Optional - PostgreSQL only:
-  schema: public
-
-  # SQLite only:
-  storage: path/to/database.sqlite
-
 #######################################################################
 # ADVANCED OPTIONS                                                    #
 #######################################################################
@@ -102,15 +91,12 @@ pool:
 bindIP: 0.0.0.0
 
 # ---------------------------------------------------------------------
-# Log Level
+# Logging
 # ---------------------------------------------------------------------
 # Possible values: error, warn, info (default), verbose, debug, silly
 
 logLevel: info
 
-# ---------------------------------------------------------------------
-# Log Format
-# ---------------------------------------------------------------------
 # Output format for logging, possible values: default, json
 
 logFormat: default
@@ -123,15 +109,6 @@ logFormat: default
 
 offline: false
 
-# ---------------------------------------------------------------------
-# High-Availability
-# ---------------------------------------------------------------------
-# Set to true if you have multiple concurrent instances running off the
-# same DB (e.g. Kubernetes pods / load balanced instances). Leave false
-# otherwise. You MUST be using PostgreSQL to use this feature.
-
-ha: false
-
 # ---------------------------------------------------------------------
 # Data Path
 # ---------------------------------------------------------------------

+ 4 - 6
package.json

@@ -1,15 +1,13 @@
 {
   "name": "wiki",
-  "version": "2.0.0",
-  "releaseDate": "2019-01-01T01:01:01.000Z",
-  "description": "A modern, lightweight and powerful wiki app built on NodeJS, Git and Markdown",
+  "version": "3.0.0",
+  "releaseDate": "2022-01-01T01:01:01.000Z",
+  "description": "The most powerful and extensible open source Wiki software",
   "main": "wiki.js",
   "dev": true,
   "scripts": {
     "start": "node server",
     "dev": "node dev",
-    "build": "webpack --profile --config dev/webpack/webpack.prod.js",
-    "watch": "webpack --config dev/webpack/webpack.dev.js",
     "test": "eslint --format codeframe --ext .js,.vue . && pug-lint server/views && jest",
     "cypress:open": "cypress open"
   },
@@ -33,7 +31,7 @@
   },
   "homepage": "https://github.com/Requarks/wiki#readme",
   "engines": {
-    "node": ">=10.12"
+    "node": ">=16.0"
   },
   "dependencies": {
     "@azure/storage-blob": "12.2.1",

+ 4 - 4
server/app/data.yml

@@ -8,16 +8,17 @@ defaults:
     # File defaults
     port: 80
     db:
-      type: postgres
       host: localhost
       port: 5432
       user: wikijs
       pass: wikijsrocks
       db: wiki
       ssl: false
-      storage: ./db.sqlite
       sslOptions:
         auto: true
+      schemas:
+        wiki: wiki
+        scheduler: scheduler
     ssl:
       enabled: false
     pool:
@@ -26,7 +27,6 @@ defaults:
     logLevel: info
     logFormat: default
     offline: false
-    ha: false
     bodyParserLimit: 5mb
     # DB defaults
     api:
@@ -88,7 +88,7 @@ defaults:
       ldapdebug: false
       sqllog: false
     # System defaults
-    channel: STABLE
+    channel: NEXT
     setup: false
     dataPath: ./data
     cors:

+ 13 - 86
server/core/db.js

@@ -7,7 +7,7 @@ const fs = require('fs')
 const Objection = require('objection')
 
 const migrationSource = require('../db/migrator-source')
-const migrateFromBeta = require('../db/beta')
+const migrateFromLegacy = require('../db/legacy')
 
 /* global WIKI */
 
@@ -20,15 +20,12 @@ module.exports = {
   listener: null,
   /**
    * Initialize DB
-   *
-   * @return     {Object}  DB instance
    */
   init() {
     let self = this
 
     // Fetch DB Config
 
-    let dbClient = null
     let dbConfig = (!_.isEmpty(process.env.DATABASE_URL)) ? process.env.DATABASE_URL : {
       host: WIKI.config.db.host.toString(),
       user: WIKI.config.db.user.toString(),
@@ -74,84 +71,22 @@ module.exports = {
       }
     }
 
-    // Engine-specific config
-    switch (WIKI.config.db.type) {
-      case 'postgres':
-        dbClient = 'pg'
-
-        if (dbUseSSL && _.isPlainObject(dbConfig)) {
-          dbConfig.ssl = (sslOptions === true) ? { rejectUnauthorized: true } : sslOptions
-        }
-        break
-      case 'mariadb':
-      case 'mysql':
-        dbClient = 'mysql2'
-
-        if (dbUseSSL && _.isPlainObject(dbConfig)) {
-          dbConfig.ssl = sslOptions
-        }
-
-        // Fix mysql boolean handling...
-        dbConfig.typeCast = (field, next) => {
-          if (field.type === 'TINY' && field.length === 1) {
-            let value = field.string()
-            return value ? (value === '1') : null
-          }
-          return next()
-        }
-        break
-      case 'mssql':
-        dbClient = 'mssql'
-
-        if (_.isPlainObject(dbConfig)) {
-          dbConfig.appName = 'Wiki.js'
-          _.set(dbConfig, 'options.appName', 'Wiki.js')
-
-          dbConfig.enableArithAbort = true
-          _.set(dbConfig, 'options.enableArithAbort', true)
-
-          if (dbUseSSL) {
-            dbConfig.encrypt = true
-            _.set(dbConfig, 'options.encrypt', true)
-          }
-        }
-        break
-      case 'sqlite':
-        dbClient = 'sqlite3'
-        dbConfig = { filename: WIKI.config.db.storage }
-        break
-      default:
-        WIKI.logger.error('Invalid DB Type')
-        process.exit(1)
+    if (dbUseSSL && _.isPlainObject(dbConfig)) {
+      dbConfig.ssl = (sslOptions === true) ? { rejectUnauthorized: true } : sslOptions
     }
 
     // Initialize Knex
     this.knex = Knex({
-      client: dbClient,
+      client: 'pg',
       useNullAsDefault: true,
       asyncStackTraces: WIKI.IS_DEBUG,
       connection: dbConfig,
+      searchPath: [WIKI.config.db.schemas.wiki],
       pool: {
         ...WIKI.config.pool,
         async afterCreate(conn, done) {
           // -> Set Connection App Name
-          switch (WIKI.config.db.type) {
-            case 'postgres':
-              await conn.query(`set application_name = 'Wiki.js'`)
-              // -> Set schema if it's not public             
-              if (WIKI.config.db.schema && WIKI.config.db.schema !== 'public') {
-                await conn.query(`set search_path TO ${WIKI.config.db.schema}, public;`)
-              }
-              done()
-              break
-            case 'mysql':
-              await conn.promise().query(`set autocommit = 1`)
-              done()
-              break
-            default:
-              done()
-              break
-          }
+          await conn.query(`set application_name = 'Wiki.js'`)
         }
       },
       debug: WIKI.IS_DEBUG
@@ -191,18 +126,19 @@ module.exports = {
       async syncSchemas () {
         return self.knex.migrate.latest({
           tableName: 'migrations',
-          migrationSource
+          migrationSource,
+          schemaName: WIKI.config.db.schemas.wiki
         })
       },
-      // -> Migrate DB Schemas from beta
-      async migrateFromBeta () {
-        return migrateFromBeta.migrate(self.knex)
+      // -> Migrate DB Schemas from 2.x
+      async migrateFromLegacy () {
+        return migrateFromLegacy.migrate(self.knex)
       }
     }
 
     let initTasksQueue = (WIKI.IS_MASTER) ? [
       initTasks.connect,
-      initTasks.migrateFromBeta,
+      initTasks.migrateFromLegacy,
       initTasks.syncSchemas
     ] : [
       () => { return Promise.resolve() }
@@ -210,7 +146,6 @@ module.exports = {
 
     // Perform init tasks
 
-    WIKI.logger.info(`Using database driver ${dbClient} for ${WIKI.config.db.type} [ OK ]`)
     this.onReady = Promise.each(initTasksQueue, t => t()).return(true)
 
     return {
@@ -222,14 +157,6 @@ module.exports = {
    * Subscribe to database LISTEN / NOTIFY for multi-instances events
    */
   async subscribeToNotifications () {
-    const useHA = (WIKI.config.ha === true || WIKI.config.ha === 'true' || WIKI.config.ha === 1 || WIKI.config.ha === '1')
-    if (!useHA) {
-      return
-    } else if (WIKI.config.db.type !== 'postgres') {
-      WIKI.logger.warn(`Database engine doesn't support pub/sub. Will not handle concurrent instances: [ DISABLED ]`)
-      return
-    }
-
     const PGPubSub = require('pg-pubsub')
 
     this.listener = new PGPubSub(this.knex.client.connectionSettings, {
@@ -254,7 +181,7 @@ module.exports = {
     WIKI.configSvc.subscribeToEvents()
     WIKI.models.pages.subscribeToEvents()
 
-    WIKI.logger.info(`High-Availability Listener initialized successfully: [ OK ]`)
+    WIKI.logger.info(`PG PubSub Listener initialized successfully: [ OK ]`)
   },
   /**
    * Unsubscribe from database LISTEN / NOTIFY

+ 0 - 115
server/db/beta/index.js

@@ -1,115 +0,0 @@
-const _ = require('lodash')
-const path = require('path')
-const fs = require('fs-extra')
-const semver = require('semver')
-
-/* global WIKI */
-
-module.exports = {
-  async migrate (knex) {
-    const migrationsTableExists = await knex.schema.hasTable('migrations')
-    if (!migrationsTableExists) {
-      return
-    }
-
-    const dbCompat = {
-      charset: (WIKI.config.db.type === `mysql` || WIKI.config.db.type === `mariadb`)
-    }
-
-    const migrations = await knex('migrations')
-    if (_.some(migrations, m => m.name.indexOf('2.0.0-beta') >= 0)) {
-      // -> Pre-beta.241 locale field length fix
-      const localeColnInfo = await knex('pages').columnInfo('localeCode')
-      if (WIKI.config.db.type !== 'sqlite' && localeColnInfo.maxLength === 2) {
-        // -> Load locales
-        const locales = await knex('locales')
-        await knex.schema
-          // -> Remove constraints
-          .table('users', table => {
-            table.dropForeign('localeCode')
-          })
-          .table('pages', table => {
-            table.dropForeign('localeCode')
-          })
-          .table('pageHistory', table => {
-            table.dropForeign('localeCode')
-          })
-          .table('pageTree', table => {
-            table.dropForeign('localeCode')
-          })
-          // -> Recreate locales table
-          .dropTable('locales')
-          .createTable('locales', table => {
-            if (dbCompat.charset) { table.charset('utf8mb4') }
-            table.string('code', 5).notNullable().primary()
-            table.json('strings')
-            table.boolean('isRTL').notNullable().defaultTo(false)
-            table.string('name').notNullable()
-            table.string('nativeName').notNullable()
-            table.integer('availability').notNullable().defaultTo(0)
-            table.string('createdAt').notNullable()
-            table.string('updatedAt').notNullable()
-          })
-        await knex('locales').insert(locales)
-        // -> Alter columns length
-        await knex.schema
-          .table('users', table => {
-            table.string('localeCode', 5).notNullable().defaultTo('en').alter()
-          })
-          .table('pages', table => {
-            table.string('localeCode', 5).alter()
-          })
-          .table('pageHistory', table => {
-            table.string('localeCode', 5).alter()
-          })
-          .table('pageTree', table => {
-            table.string('localeCode', 5).alter()
-          })
-          // -> Restore restraints
-          .table('users', table => {
-            table.foreign('localeCode').references('code').inTable('locales')
-          })
-          .table('pages', table => {
-            table.foreign('localeCode').references('code').inTable('locales')
-          })
-          .table('pageHistory', table => {
-            table.foreign('localeCode').references('code').inTable('locales')
-          })
-          .table('pageTree', table => {
-            table.foreign('localeCode').references('code').inTable('locales')
-          })
-      }
-
-      // -> Advance to latest beta/rc migration state
-      const baseMigrationPath = path.join(WIKI.SERVERPATH, (WIKI.config.db.type !== 'sqlite') ? 'db/beta/migrations' : 'db/beta/migrations-sqlite')
-      await knex.migrate.latest({
-        tableName: 'migrations',
-        migrationSource: {
-          async getMigrations() {
-            const migrationFiles = await fs.readdir(baseMigrationPath)
-            return migrationFiles.sort(semver.compare).map(m => ({
-              file: m,
-              directory: baseMigrationPath
-            }))
-          },
-          getMigrationName(migration) {
-            return migration.file
-          },
-          getMigration(migration) {
-            return require(path.join(baseMigrationPath, migration.file))
-          }
-        }
-      })
-
-      // -> Cleanup migration table
-      await knex('migrations').truncate()
-
-      // -> Advance to stable 2.0 migration state
-      await knex('migrations').insert({
-        name: '2.0.0.js',
-        batch: 1,
-        migration_time: knex.fn.now()
-      })
-    }
-  }
-}

+ 0 - 259
server/db/beta/migrations-sqlite/2.0.0-beta.1.js

@@ -1,259 +0,0 @@
-exports.up = knex => {
-  return knex.schema
-    // =====================================
-    // MODEL TABLES
-    // =====================================
-    // ASSETS ------------------------------
-    .createTable('assets', table => {
-      table.increments('id').primary()
-      table.string('filename').notNullable()
-      table.string('basename').notNullable()
-      table.string('ext').notNullable()
-      table.enum('kind', ['binary', 'image']).notNullable().defaultTo('binary')
-      table.string('mime').notNullable().defaultTo('application/octet-stream')
-      table.integer('fileSize').unsigned().comment('In kilobytes')
-      table.json('metadata')
-      table.string('createdAt').notNullable()
-      table.string('updatedAt').notNullable()
-
-      table.integer('folderId').unsigned().references('id').inTable('assetFolders')
-      table.integer('authorId').unsigned().references('id').inTable('users')
-    })
-    // ASSET FOLDERS -----------------------
-    .createTable('assetFolders', table => {
-      table.increments('id').primary()
-      table.string('name').notNullable()
-      table.string('slug').notNullable()
-      table.integer('parentId').unsigned().references('id').inTable('assetFolders')
-    })
-    // AUTHENTICATION ----------------------
-    .createTable('authentication', table => {
-      table.string('key').notNullable().primary()
-      table.boolean('isEnabled').notNullable().defaultTo(false)
-      table.json('config').notNullable()
-      table.boolean('selfRegistration').notNullable().defaultTo(false)
-      table.json('domainWhitelist').notNullable()
-      table.json('autoEnrollGroups').notNullable()
-    })
-    // COMMENTS ----------------------------
-    .createTable('comments', table => {
-      table.increments('id').primary()
-      table.text('content').notNullable()
-      table.string('createdAt').notNullable()
-      table.string('updatedAt').notNullable()
-
-      table.integer('pageId').unsigned().references('id').inTable('pages')
-      table.integer('authorId').unsigned().references('id').inTable('users')
-    })
-    // EDITORS -----------------------------
-    .createTable('editors', table => {
-      table.string('key').notNullable().primary()
-      table.boolean('isEnabled').notNullable().defaultTo(false)
-      table.json('config').notNullable()
-    })
-    // GROUPS ------------------------------
-    .createTable('groups', table => {
-      table.increments('id').primary()
-      table.string('name').notNullable()
-      table.json('permissions').notNullable()
-      table.json('pageRules').notNullable()
-      table.boolean('isSystem').notNullable().defaultTo(false)
-      table.string('createdAt').notNullable()
-      table.string('updatedAt').notNullable()
-    })
-    // LOCALES -----------------------------
-    .createTable('locales', table => {
-      table.string('code', 5).notNullable().primary()
-      table.json('strings')
-      table.boolean('isRTL').notNullable().defaultTo(false)
-      table.string('name').notNullable()
-      table.string('nativeName').notNullable()
-      table.string('createdAt').notNullable()
-      table.string('updatedAt').notNullable()
-    })
-    // LOGGING ----------------------------
-    .createTable('loggers', table => {
-      table.string('key').notNullable().primary()
-      table.boolean('isEnabled').notNullable().defaultTo(false)
-      table.string('level').notNullable().defaultTo('warn')
-      table.json('config')
-    })
-    // NAVIGATION ----------------------------
-    .createTable('navigation', table => {
-      table.string('key').notNullable().primary()
-      table.json('config')
-    })
-    // PAGE HISTORY ------------------------
-    .createTable('pageHistory', table => {
-      table.increments('id').primary()
-      table.string('path').notNullable()
-      table.string('hash').notNullable()
-      table.string('title').notNullable()
-      table.string('description')
-      table.boolean('isPrivate').notNullable().defaultTo(false)
-      table.boolean('isPublished').notNullable().defaultTo(false)
-      table.string('publishStartDate')
-      table.string('publishEndDate')
-      table.text('content')
-      table.string('contentType').notNullable()
-      table.string('createdAt').notNullable()
-
-      table.integer('pageId').unsigned().references('id').inTable('pages')
-      table.string('editorKey').references('key').inTable('editors')
-      table.string('localeCode', 5).references('code').inTable('locales')
-      table.integer('authorId').unsigned().references('id').inTable('users')
-    })
-    // PAGES -------------------------------
-    .createTable('pages', table => {
-      table.increments('id').primary()
-      table.string('path').notNullable()
-      table.string('hash').notNullable()
-      table.string('title').notNullable()
-      table.string('description')
-      table.boolean('isPrivate').notNullable().defaultTo(false)
-      table.boolean('isPublished').notNullable().defaultTo(false)
-      table.string('privateNS')
-      table.string('publishStartDate')
-      table.string('publishEndDate')
-      table.text('content')
-      table.text('render')
-      table.json('toc')
-      table.string('contentType').notNullable()
-      table.string('createdAt').notNullable()
-      table.string('updatedAt').notNullable()
-
-      table.string('editorKey').references('key').inTable('editors')
-      table.string('localeCode', 5).references('code').inTable('locales')
-      table.integer('authorId').unsigned().references('id').inTable('users')
-      table.integer('creatorId').unsigned().references('id').inTable('users')
-    })
-    // PAGE TREE ---------------------------
-    .createTable('pageTree', table => {
-      table.increments('id').primary()
-      table.string('path').notNullable()
-      table.integer('depth').unsigned().notNullable()
-      table.string('title').notNullable()
-      table.boolean('isPrivate').notNullable().defaultTo(false)
-      table.boolean('isFolder').notNullable().defaultTo(false)
-      table.string('privateNS')
-
-      table.integer('parent').unsigned().references('id').inTable('pageTree')
-      table.integer('pageId').unsigned().references('id').inTable('pages')
-      table.string('localeCode', 5).references('code').inTable('locales')
-    })
-    // RENDERERS ---------------------------
-    .createTable('renderers', table => {
-      table.string('key').notNullable().primary()
-      table.boolean('isEnabled').notNullable().defaultTo(false)
-      table.json('config')
-    })
-    // SEARCH ------------------------------
-    .createTable('searchEngines', table => {
-      table.string('key').notNullable().primary()
-      table.boolean('isEnabled').notNullable().defaultTo(false)
-      table.json('config')
-    })
-    // SETTINGS ----------------------------
-    .createTable('settings', table => {
-      table.string('key').notNullable().primary()
-      table.json('value')
-      table.string('updatedAt').notNullable()
-    })
-    // STORAGE -----------------------------
-    .createTable('storage', table => {
-      table.string('key').notNullable().primary()
-      table.boolean('isEnabled').notNullable().defaultTo(false)
-      table.string('mode', ['sync', 'push', 'pull']).notNullable().defaultTo('push')
-      table.json('config')
-    })
-    // TAGS --------------------------------
-    .createTable('tags', table => {
-      table.increments('id').primary()
-      table.string('tag').notNullable().unique()
-      table.string('title')
-      table.string('createdAt').notNullable()
-      table.string('updatedAt').notNullable()
-    })
-    // USER KEYS ---------------------------
-    .createTable('userKeys', table => {
-      table.increments('id').primary()
-      table.string('kind').notNullable()
-      table.string('token').notNullable()
-      table.string('createdAt').notNullable()
-      table.string('validUntil').notNullable()
-
-      table.integer('userId').unsigned().references('id').inTable('users')
-    })
-    // USERS -------------------------------
-    .createTable('users', table => {
-      table.increments('id').primary()
-      table.string('email').notNullable()
-      table.string('name').notNullable()
-      table.string('providerId')
-      table.string('password')
-      table.boolean('tfaIsActive').notNullable().defaultTo(false)
-      table.string('tfaSecret')
-      table.string('jobTitle').defaultTo('')
-      table.string('location').defaultTo('')
-      table.string('pictureUrl')
-      table.string('timezone').notNullable().defaultTo('America/New_York')
-      table.boolean('isSystem').notNullable().defaultTo(false)
-      table.boolean('isActive').notNullable().defaultTo(false)
-      table.boolean('isVerified').notNullable().defaultTo(false)
-      table.string('createdAt').notNullable()
-      table.string('updatedAt').notNullable()
-
-      table.string('providerKey').references('key').inTable('authentication').notNullable().defaultTo('local')
-      table.string('localeCode', 5).references('code').inTable('locales').notNullable().defaultTo('en')
-      table.string('defaultEditor').references('key').inTable('editors').notNullable().defaultTo('markdown')
-    })
-    // =====================================
-    // RELATION TABLES
-    // =====================================
-    // PAGE HISTORY TAGS ---------------------------
-    .createTable('pageHistoryTags', table => {
-      table.increments('id').primary()
-      table.integer('pageId').unsigned().references('id').inTable('pageHistory').onDelete('CASCADE')
-      table.integer('tagId').unsigned().references('id').inTable('tags').onDelete('CASCADE')
-    })
-    // PAGE TAGS ---------------------------
-    .createTable('pageTags', table => {
-      table.increments('id').primary()
-      table.integer('pageId').unsigned().references('id').inTable('pages').onDelete('CASCADE')
-      table.integer('tagId').unsigned().references('id').inTable('tags').onDelete('CASCADE')
-    })
-    // USER GROUPS -------------------------
-    .createTable('userGroups', table => {
-      table.increments('id').primary()
-      table.integer('userId').unsigned().references('id').inTable('users').onDelete('CASCADE')
-      table.integer('groupId').unsigned().references('id').inTable('groups').onDelete('CASCADE')
-    })
-    // =====================================
-    // REFERENCES
-    // =====================================
-    .table('users', table => {
-      table.unique(['providerKey', 'email'])
-    })
-}
-
-exports.down = knex => {
-  return knex.schema
-    .dropTableIfExists('userGroups')
-    .dropTableIfExists('pageHistoryTags')
-    .dropTableIfExists('pageHistory')
-    .dropTableIfExists('pageTags')
-    .dropTableIfExists('assets')
-    .dropTableIfExists('assetFolders')
-    .dropTableIfExists('comments')
-    .dropTableIfExists('editors')
-    .dropTableIfExists('groups')
-    .dropTableIfExists('locales')
-    .dropTableIfExists('navigation')
-    .dropTableIfExists('pages')
-    .dropTableIfExists('renderers')
-    .dropTableIfExists('settings')
-    .dropTableIfExists('storage')
-    .dropTableIfExists('tags')
-    .dropTableIfExists('userKeys')
-    .dropTableIfExists('users')
-}

+ 0 - 52
server/db/beta/migrations-sqlite/2.0.0-beta.11.js

@@ -1,52 +0,0 @@
-exports.up = knex => {
-  return knex.schema
-    .renameTable('pageHistory', 'pageHistory_old')
-    .createTable('pageHistory', table => {
-      table.increments('id').primary()
-      table.string('path').notNullable()
-      table.string('hash').notNullable()
-      table.string('title').notNullable()
-      table.string('description')
-      table.boolean('isPrivate').notNullable().defaultTo(false)
-      table.boolean('isPublished').notNullable().defaultTo(false)
-      table.string('publishStartDate')
-      table.string('publishEndDate')
-      table.text('content')
-      table.string('contentType').notNullable()
-      table.string('createdAt').notNullable()
-      table.string('action').defaultTo('updated')
-
-      table.integer('pageId').unsigned()
-      table.string('editorKey').references('key').inTable('editors')
-      table.string('localeCode', 5).references('code').inTable('locales')
-      table.integer('authorId').unsigned().references('id').inTable('users')
-    })
-    .raw(`INSERT INTO pageHistory SELECT id,path,hash,title,description,isPrivate,isPublished,publishStartDate,publishEndDate,content,contentType,createdAt,'updated' AS action,pageId,editorKey,localeCode,authorId FROM pageHistory_old;`)
-    .dropTable('pageHistory_old')
-}
-
-exports.down = knex => {
-  return knex.schema
-    .renameTable('pageHistory', 'pageHistory_old')
-    .createTable('pageHistory', table => {
-      table.increments('id').primary()
-      table.string('path').notNullable()
-      table.string('hash').notNullable()
-      table.string('title').notNullable()
-      table.string('description')
-      table.boolean('isPrivate').notNullable().defaultTo(false)
-      table.boolean('isPublished').notNullable().defaultTo(false)
-      table.string('publishStartDate')
-      table.string('publishEndDate')
-      table.text('content')
-      table.string('contentType').notNullable()
-      table.string('createdAt').notNullable()
-
-      table.integer('pageId').unsigned().references('id').inTable('pages')
-      table.string('editorKey').references('key').inTable('editors')
-      table.string('localeCode', 5).references('code').inTable('locales')
-      table.integer('authorId').unsigned().references('id').inTable('users')
-    })
-    .raw('INSERT INTO pageHistory SELECT id,path,hash,title,description,isPrivate,isPublished,publishStartDate,publishEndDate,content,contentType,createdAt,NULL as pageId,editorKey,localeCode,authorId FROM pageHistory_old;')
-    .dropTable('pageHistory_old')
-}

+ 0 - 15
server/db/beta/migrations-sqlite/2.0.0-beta.127.js

@@ -1,15 +0,0 @@
-exports.up = knex => {
-  return knex.schema
-    .table('assets', table => {
-      table.dropColumn('basename')
-      table.string('hash').notNullable().defaultTo('')
-    })
-}
-
-exports.down = knex => {
-  return knex.schema
-    .table('assets', table => {
-      table.dropColumn('hash')
-      table.string('basename').notNullable().defaultTo('')
-    })
-}

+ 0 - 13
server/db/beta/migrations-sqlite/2.0.0-beta.205.js

@@ -1,13 +0,0 @@
-exports.up = knex => {
-  return knex.schema
-    .createTable('analytics', table => {
-      table.string('key').notNullable().primary()
-      table.boolean('isEnabled').notNullable().defaultTo(false)
-      table.json('config').notNullable()
-    })
-}
-
-exports.down = knex => {
-  return knex.schema
-    .dropTableIfExists('analytics')
-}

+ 0 - 13
server/db/beta/migrations-sqlite/2.0.0-beta.217.js

@@ -1,13 +0,0 @@
-exports.up = knex => {
-  return knex.schema
-    .table('locales', table => {
-      table.integer('availability').notNullable().defaultTo(0)
-    })
-}
-
-exports.down = knex => {
-  return knex.schema
-    .table('locales', table => {
-      table.dropColumn('availability')
-    })
-}

+ 0 - 13
server/db/beta/migrations-sqlite/2.0.0-beta.242.js

@@ -1,13 +0,0 @@
-exports.up = knex => {
-  return knex.schema
-    .table('users', table => {
-      table.boolean('mustChangePwd').notNullable().defaultTo(false)
-    })
-}
-
-exports.down = knex => {
-  return knex.schema
-    .table('users', table => {
-      table.dropColumn('mustChangePwd')
-    })
-}

+ 0 - 17
server/db/beta/migrations-sqlite/2.0.0-beta.293.js

@@ -1,17 +0,0 @@
-exports.up = knex => {
-  return knex.schema
-    .createTable('pageLinks', table => {
-      table.increments('id').primary()
-      table.integer('pageId').unsigned().references('id').inTable('pages').onDelete('CASCADE')
-      table.string('path').notNullable()
-      table.string('localeCode', 5).notNullable()
-    })
-    .table('pageLinks', table => {
-      table.index(['path', 'localeCode'])
-    })
-}
-
-exports.down = knex => {
-  return knex.schema
-    .dropTableIfExists('pageLinks')
-}

+ 0 - 15
server/db/beta/migrations-sqlite/2.0.0-beta.38.js

@@ -1,15 +0,0 @@
-exports.up = knex => {
-  return knex.schema
-    .table('storage', table => {
-      table.string('syncInterval')
-      table.json('state')
-    })
-}
-
-exports.down = knex => {
-  return knex.schema
-    .table('storage', table => {
-      table.dropColumn('syncInterval')
-      table.dropColumn('state')
-    })
-}

+ 0 - 12
server/db/beta/migrations-sqlite/2.0.0-beta.99.js

@@ -1,12 +0,0 @@
-exports.up = knex => {
-  return knex.schema
-    .createTable('assetData', table => {
-      table.integer('id').primary()
-      table.binary('data').notNullable()
-    })
-}
-
-exports.down = knex => {
-  return knex.schema
-    .dropTableIfExists('assetData')
-}

+ 0 - 35
server/db/beta/migrations-sqlite/2.0.0-rc.2.js

@@ -1,35 +0,0 @@
-exports.up = knex => {
-  return knex.schema
-    .dropTable('pageTree')
-    .createTable('pageTree', table => {
-      table.integer('id').primary()
-      table.string('path').notNullable()
-      table.integer('depth').unsigned().notNullable()
-      table.string('title').notNullable()
-      table.boolean('isPrivate').notNullable().defaultTo(false)
-      table.boolean('isFolder').notNullable().defaultTo(false)
-      table.string('privateNS')
-
-      table.integer('parent').unsigned().references('id').inTable('pageTree').onDelete('CASCADE')
-      table.integer('pageId').unsigned().references('id').inTable('pages').onDelete('CASCADE')
-      table.string('localeCode', 5).references('code').inTable('locales')
-    })
-}
-
-exports.down = knex => {
-  return knex.schema
-    .dropTable('pageTree')
-    .createTable('pageTree', table => {
-      table.integer('id').primary()
-      table.string('path').notNullable()
-      table.integer('depth').unsigned().notNullable()
-      table.string('title').notNullable()
-      table.boolean('isPrivate').notNullable().defaultTo(false)
-      table.boolean('isFolder').notNullable().defaultTo(false)
-      table.string('privateNS')
-
-      table.integer('parent').unsigned().references('id').inTable('pageTree')
-      table.integer('pageId').unsigned().references('id').inTable('pages')
-      table.string('localeCode', 5).references('code').inTable('locales')
-    })
-}

+ 0 - 292
server/db/beta/migrations/2.0.0-beta.1.js

@@ -1,292 +0,0 @@
-/* global WIKI */
-
-exports.up = knex => {
-  const dbCompat = {
-    charset: (WIKI.config.db.type === `mysql` || WIKI.config.db.type === `mariadb`)
-  }
-  return knex.schema
-    // =====================================
-    // MODEL TABLES
-    // =====================================
-    // ASSETS ------------------------------
-    .createTable('assets', table => {
-      if (dbCompat.charset) { table.charset('utf8mb4') }
-      table.increments('id').primary()
-      table.string('filename').notNullable()
-      table.string('basename').notNullable()
-      table.string('ext').notNullable()
-      table.enum('kind', ['binary', 'image']).notNullable().defaultTo('binary')
-      table.string('mime').notNullable().defaultTo('application/octet-stream')
-      table.integer('fileSize').unsigned().comment('In kilobytes')
-      table.json('metadata')
-      table.string('createdAt').notNullable()
-      table.string('updatedAt').notNullable()
-    })
-    // ASSET FOLDERS -----------------------
-    .createTable('assetFolders', table => {
-      if (dbCompat.charset) { table.charset('utf8mb4') }
-      table.increments('id').primary()
-      table.string('name').notNullable()
-      table.string('slug').notNullable()
-      table.integer('parentId').unsigned().references('id').inTable('assetFolders')
-    })
-    // AUTHENTICATION ----------------------
-    .createTable('authentication', table => {
-      if (dbCompat.charset) { table.charset('utf8mb4') }
-      table.string('key').notNullable().primary()
-      table.boolean('isEnabled').notNullable().defaultTo(false)
-      table.json('config').notNullable()
-      table.boolean('selfRegistration').notNullable().defaultTo(false)
-      table.json('domainWhitelist').notNullable()
-      table.json('autoEnrollGroups').notNullable()
-    })
-    // COMMENTS ----------------------------
-    .createTable('comments', table => {
-      if (dbCompat.charset) { table.charset('utf8mb4') }
-      table.increments('id').primary()
-      table.text('content').notNullable()
-      table.string('createdAt').notNullable()
-      table.string('updatedAt').notNullable()
-    })
-    // EDITORS -----------------------------
-    .createTable('editors', table => {
-      if (dbCompat.charset) { table.charset('utf8mb4') }
-      table.string('key').notNullable().primary()
-      table.boolean('isEnabled').notNullable().defaultTo(false)
-      table.json('config').notNullable()
-    })
-    // GROUPS ------------------------------
-    .createTable('groups', table => {
-      if (dbCompat.charset) { table.charset('utf8mb4') }
-      table.increments('id').primary()
-      table.string('name').notNullable()
-      table.json('permissions').notNullable()
-      table.json('pageRules').notNullable()
-      table.boolean('isSystem').notNullable().defaultTo(false)
-      table.string('createdAt').notNullable()
-      table.string('updatedAt').notNullable()
-    })
-    // LOCALES -----------------------------
-    .createTable('locales', table => {
-      if (dbCompat.charset) { table.charset('utf8mb4') }
-      table.string('code', 2).notNullable().primary()
-      table.json('strings')
-      table.boolean('isRTL').notNullable().defaultTo(false)
-      table.string('name').notNullable()
-      table.string('nativeName').notNullable()
-      table.string('createdAt').notNullable()
-      table.string('updatedAt').notNullable()
-    })
-    // LOGGING ----------------------------
-    .createTable('loggers', table => {
-      if (dbCompat.charset) { table.charset('utf8mb4') }
-      table.string('key').notNullable().primary()
-      table.boolean('isEnabled').notNullable().defaultTo(false)
-      table.string('level').notNullable().defaultTo('warn')
-      table.json('config')
-    })
-    // NAVIGATION ----------------------------
-    .createTable('navigation', table => {
-      if (dbCompat.charset) { table.charset('utf8mb4') }
-      table.string('key').notNullable().primary()
-      table.json('config')
-    })
-    // PAGE HISTORY ------------------------
-    .createTable('pageHistory', table => {
-      if (dbCompat.charset) { table.charset('utf8mb4') }
-      table.increments('id').primary()
-      table.string('path').notNullable()
-      table.string('hash').notNullable()
-      table.string('title').notNullable()
-      table.string('description')
-      table.boolean('isPrivate').notNullable().defaultTo(false)
-      table.boolean('isPublished').notNullable().defaultTo(false)
-      table.string('publishStartDate')
-      table.string('publishEndDate')
-      table.text('content')
-      table.string('contentType').notNullable()
-      table.string('createdAt').notNullable()
-    })
-    // PAGES -------------------------------
-    .createTable('pages', table => {
-      if (dbCompat.charset) { table.charset('utf8mb4') }
-      table.increments('id').primary()
-      table.string('path').notNullable()
-      table.string('hash').notNullable()
-      table.string('title').notNullable()
-      table.string('description')
-      table.boolean('isPrivate').notNullable().defaultTo(false)
-      table.boolean('isPublished').notNullable().defaultTo(false)
-      table.string('privateNS')
-      table.string('publishStartDate')
-      table.string('publishEndDate')
-      table.text('content')
-      table.text('render')
-      table.json('toc')
-      table.string('contentType').notNullable()
-      table.string('createdAt').notNullable()
-      table.string('updatedAt').notNullable()
-    })
-    // PAGE TREE ---------------------------
-    .createTable('pageTree', table => {
-      if (dbCompat.charset) { table.charset('utf8mb4') }
-      table.increments('id').primary()
-      table.string('path').notNullable()
-      table.integer('depth').unsigned().notNullable()
-      table.string('title').notNullable()
-      table.boolean('isPrivate').notNullable().defaultTo(false)
-      table.boolean('isFolder').notNullable().defaultTo(false)
-      table.string('privateNS')
-    })
-    // RENDERERS ---------------------------
-    .createTable('renderers', table => {
-      if (dbCompat.charset) { table.charset('utf8mb4') }
-      table.string('key').notNullable().primary()
-      table.boolean('isEnabled').notNullable().defaultTo(false)
-      table.json('config')
-    })
-    // SEARCH ------------------------------
-    .createTable('searchEngines', table => {
-      if (dbCompat.charset) { table.charset('utf8mb4') }
-      table.string('key').notNullable().primary()
-      table.boolean('isEnabled').notNullable().defaultTo(false)
-      table.json('config')
-    })
-    // SETTINGS ----------------------------
-    .createTable('settings', table => {
-      if (dbCompat.charset) { table.charset('utf8mb4') }
-      table.string('key').notNullable().primary()
-      table.json('value')
-      table.string('updatedAt').notNullable()
-    })
-    // STORAGE -----------------------------
-    .createTable('storage', table => {
-      if (dbCompat.charset) { table.charset('utf8mb4') }
-      table.string('key').notNullable().primary()
-      table.boolean('isEnabled').notNullable().defaultTo(false)
-      table.string('mode', ['sync', 'push', 'pull']).notNullable().defaultTo('push')
-      table.json('config')
-    })
-    // TAGS --------------------------------
-    .createTable('tags', table => {
-      if (dbCompat.charset) { table.charset('utf8mb4') }
-      table.increments('id').primary()
-      table.string('tag').notNullable().unique()
-      table.string('title')
-      table.string('createdAt').notNullable()
-      table.string('updatedAt').notNullable()
-    })
-    // USER KEYS ---------------------------
-    .createTable('userKeys', table => {
-      if (dbCompat.charset) { table.charset('utf8mb4') }
-      table.increments('id').primary()
-      table.string('kind').notNullable()
-      table.string('token').notNullable()
-      table.string('createdAt').notNullable()
-      table.string('validUntil').notNullable()
-    })
-    // USERS -------------------------------
-    .createTable('users', table => {
-      if (dbCompat.charset) { table.charset('utf8mb4') }
-      table.increments('id').primary()
-      table.string('email').notNullable()
-      table.string('name').notNullable()
-      table.string('providerId')
-      table.string('password')
-      table.boolean('tfaIsActive').notNullable().defaultTo(false)
-      table.string('tfaSecret')
-      table.string('jobTitle').defaultTo('')
-      table.string('location').defaultTo('')
-      table.string('pictureUrl')
-      table.string('timezone').notNullable().defaultTo('America/New_York')
-      table.boolean('isSystem').notNullable().defaultTo(false)
-      table.boolean('isActive').notNullable().defaultTo(false)
-      table.boolean('isVerified').notNullable().defaultTo(false)
-      table.string('createdAt').notNullable()
-      table.string('updatedAt').notNullable()
-    })
-    // =====================================
-    // RELATION TABLES
-    // =====================================
-    // PAGE HISTORY TAGS ---------------------------
-    .createTable('pageHistoryTags', table => {
-      if (dbCompat.charset) { table.charset('utf8mb4') }
-      table.increments('id').primary()
-      table.integer('pageId').unsigned().references('id').inTable('pageHistory').onDelete('CASCADE')
-      table.integer('tagId').unsigned().references('id').inTable('tags').onDelete('CASCADE')
-    })
-    // PAGE TAGS ---------------------------
-    .createTable('pageTags', table => {
-      if (dbCompat.charset) { table.charset('utf8mb4') }
-      table.increments('id').primary()
-      table.integer('pageId').unsigned().references('id').inTable('pages').onDelete('CASCADE')
-      table.integer('tagId').unsigned().references('id').inTable('tags').onDelete('CASCADE')
-    })
-    // USER GROUPS -------------------------
-    .createTable('userGroups', table => {
-      if (dbCompat.charset) { table.charset('utf8mb4') }
-      table.increments('id').primary()
-      table.integer('userId').unsigned().references('id').inTable('users').onDelete('CASCADE')
-      table.integer('groupId').unsigned().references('id').inTable('groups').onDelete('CASCADE')
-    })
-    // =====================================
-    // REFERENCES
-    // =====================================
-    .table('assets', table => {
-      table.integer('folderId').unsigned().references('id').inTable('assetFolders')
-      table.integer('authorId').unsigned().references('id').inTable('users')
-    })
-    .table('comments', table => {
-      table.integer('pageId').unsigned().references('id').inTable('pages')
-      table.integer('authorId').unsigned().references('id').inTable('users')
-    })
-    .table('pageHistory', table => {
-      table.integer('pageId').unsigned().references('id').inTable('pages')
-      table.string('editorKey').references('key').inTable('editors')
-      table.string('localeCode', 2).references('code').inTable('locales')
-      table.integer('authorId').unsigned().references('id').inTable('users')
-    })
-    .table('pages', table => {
-      table.string('editorKey').references('key').inTable('editors')
-      table.string('localeCode', 2).references('code').inTable('locales')
-      table.integer('authorId').unsigned().references('id').inTable('users')
-      table.integer('creatorId').unsigned().references('id').inTable('users')
-    })
-    .table('pageTree', table => {
-      table.integer('parent').unsigned().references('id').inTable('pageTree')
-      table.integer('pageId').unsigned().references('id').inTable('pages')
-      table.string('localeCode', 2).references('code').inTable('locales')
-    })
-    .table('userKeys', table => {
-      table.integer('userId').unsigned().references('id').inTable('users')
-    })
-    .table('users', table => {
-      table.string('providerKey').references('key').inTable('authentication').notNullable().defaultTo('local')
-      table.string('localeCode', 2).references('code').inTable('locales').notNullable().defaultTo('en')
-      table.string('defaultEditor').references('key').inTable('editors').notNullable().defaultTo('markdown')
-
-      table.unique(['providerKey', 'email'])
-    })
-}
-
-exports.down = knex => {
-  return knex.schema
-    .dropTableIfExists('userGroups')
-    .dropTableIfExists('pageHistoryTags')
-    .dropTableIfExists('pageHistory')
-    .dropTableIfExists('pageTags')
-    .dropTableIfExists('assets')
-    .dropTableIfExists('assetFolders')
-    .dropTableIfExists('comments')
-    .dropTableIfExists('editors')
-    .dropTableIfExists('groups')
-    .dropTableIfExists('locales')
-    .dropTableIfExists('navigation')
-    .dropTableIfExists('pages')
-    .dropTableIfExists('renderers')
-    .dropTableIfExists('settings')
-    .dropTableIfExists('storage')
-    .dropTableIfExists('tags')
-    .dropTableIfExists('userKeys')
-    .dropTableIfExists('users')
-}

+ 0 - 15
server/db/beta/migrations/2.0.0-beta.11.js

@@ -1,15 +0,0 @@
-exports.up = knex => {
-  return knex.schema
-    .table('pageHistory', table => {
-      table.string('action').defaultTo('updated')
-      table.dropForeign('pageId')
-    })
-}
-
-exports.down = knex => {
-  return knex.schema
-    .table('pageHistory', table => {
-      table.dropColumn('action')
-      table.integer('pageId').unsigned().references('id').inTable('pages')
-    })
-}

+ 0 - 15
server/db/beta/migrations/2.0.0-beta.127.js

@@ -1,15 +0,0 @@
-exports.up = knex => {
-  return knex.schema
-    .table('assets', table => {
-      table.dropColumn('basename')
-      table.string('hash').notNullable()
-    })
-}
-
-exports.down = knex => {
-  return knex.schema
-    .table('assets', table => {
-      table.dropColumn('hash')
-      table.string('basename').notNullable()
-    })
-}

+ 0 - 23
server/db/beta/migrations/2.0.0-beta.148.js

@@ -1,23 +0,0 @@
-/* global WIKI */
-
-exports.up = knex => {
-  const dbCompat = {
-    blobLength: (WIKI.config.db.type === `mysql` || WIKI.config.db.type === `mariadb`)
-  }
-  return knex.schema
-    .table('assetData', table => {
-      if (dbCompat.blobLength) {
-        table.dropColumn('data')
-      }
-    })
-    .table('assetData', table => {
-      if (dbCompat.blobLength) {
-        table.specificType('data', 'LONGBLOB').notNullable()
-      }
-    })
-}
-
-exports.down = knex => {
-  return knex.schema
-    .table('assetData', table => {})
-}

+ 0 - 19
server/db/beta/migrations/2.0.0-beta.205.js

@@ -1,19 +0,0 @@
-/* global WIKI */
-
-exports.up = knex => {
-  const dbCompat = {
-    charset: (WIKI.config.db.type === `mysql` || WIKI.config.db.type === `mariadb`)
-  }
-  return knex.schema
-    .createTable('analytics', table => {
-      if (dbCompat.charset) { table.charset('utf8mb4') }
-      table.string('key').notNullable().primary()
-      table.boolean('isEnabled').notNullable().defaultTo(false)
-      table.json('config').notNullable()
-    })
-}
-
-exports.down = knex => {
-  return knex.schema
-    .dropTableIfExists('analytics')
-}

+ 0 - 13
server/db/beta/migrations/2.0.0-beta.217.js

@@ -1,13 +0,0 @@
-exports.up = knex => {
-  return knex.schema
-    .table('locales', table => {
-      table.integer('availability').notNullable().defaultTo(0)
-    })
-}
-
-exports.down = knex => {
-  return knex.schema
-    .table('locales', table => {
-      table.dropColumn('availability')
-    })
-}

+ 0 - 13
server/db/beta/migrations/2.0.0-beta.242.js

@@ -1,13 +0,0 @@
-exports.up = knex => {
-  return knex.schema
-    .table('users', table => {
-      table.boolean('mustChangePwd').notNullable().defaultTo(false)
-    })
-}
-
-exports.down = knex => {
-  return knex.schema
-    .table('users', table => {
-      table.dropColumn('mustChangePwd')
-    })
-}

+ 0 - 23
server/db/beta/migrations/2.0.0-beta.293.js

@@ -1,23 +0,0 @@
-/* global WIKI */
-
-exports.up = knex => {
-  const dbCompat = {
-    charset: (WIKI.config.db.type === `mysql` || WIKI.config.db.type === `mariadb`)
-  }
-  return knex.schema
-    .createTable('pageLinks', table => {
-      if (dbCompat.charset) { table.charset('utf8mb4') }
-      table.increments('id').primary()
-      table.integer('pageId').unsigned().references('id').inTable('pages').onDelete('CASCADE')
-      table.string('path').notNullable()
-      table.string('localeCode', 5).notNullable()
-    })
-    .table('pageLinks', table => {
-      table.index(['path', 'localeCode'])
-    })
-}
-
-exports.down = knex => {
-  return knex.schema
-    .dropTableIfExists('pageLinks')
-}

+ 0 - 15
server/db/beta/migrations/2.0.0-beta.38.js

@@ -1,15 +0,0 @@
-exports.up = knex => {
-  return knex.schema
-    .table('storage', table => {
-      table.string('syncInterval')
-      table.json('state')
-    })
-}
-
-exports.down = knex => {
-  return knex.schema
-    .table('storage', table => {
-      table.dropColumn('syncInterval')
-      table.dropColumn('state')
-    })
-}

+ 0 - 18
server/db/beta/migrations/2.0.0-beta.99.js

@@ -1,18 +0,0 @@
-/* global WIKI */
-
-exports.up = knex => {
-  const dbCompat = {
-    charset: (WIKI.config.db.type === `mysql` || WIKI.config.db.type === `mariadb`)
-  }
-  return knex.schema
-    .createTable('assetData', table => {
-      if (dbCompat.charset) { table.charset('utf8mb4') }
-      table.integer('id').primary()
-      table.binary('data').notNullable()
-    })
-}
-
-exports.down = knex => {
-  return knex.schema
-    .dropTableIfExists('assetData')
-}

+ 0 - 54
server/db/beta/migrations/2.0.0-rc.2.js

@@ -1,54 +0,0 @@
-/* global WIKI */
-
-exports.up = async knex => {
-  const dbCompat = {
-    charset: (WIKI.config.db.type === `mysql` || WIKI.config.db.type === `mariadb`),
-    selfCascadeDelete: WIKI.config.db.type !== 'mssql'
-  }
-
-  return knex.schema
-    .dropTable('pageTree')
-    .createTable('pageTree', table => {
-      if (dbCompat.charset) { table.charset('utf8mb4') }
-      table.integer('id').unsigned().primary()
-      table.string('path').notNullable()
-      table.integer('depth').unsigned().notNullable()
-      table.string('title').notNullable()
-      table.boolean('isPrivate').notNullable().defaultTo(false)
-      table.boolean('isFolder').notNullable().defaultTo(false)
-      table.string('privateNS')
-    })
-    .table('pageTree', table => {
-      if (dbCompat.selfCascadeDelete) {
-        table.integer('parent').unsigned().references('id').inTable('pageTree').onDelete('CASCADE')
-      } else {
-        table.integer('parent').unsigned()
-      }
-      table.integer('pageId').unsigned().references('id').inTable('pages').onDelete('CASCADE')
-      table.string('localeCode', 5).references('code').inTable('locales')
-    })
-}
-
-exports.down = knex => {
-  const dbCompat = {
-    charset: (WIKI.config.db.type === `mysql` || WIKI.config.db.type === `mariadb`),
-    selfCascadeDelete: WIKI.config.db.type !== 'mssql'
-  }
-  return knex.schema
-    .dropTable('pageTree')
-    .createTable('pageTree', table => {
-      if (dbCompat.charset) { table.charset('utf8mb4') }
-      table.integer('id').primary()
-      table.string('path').notNullable()
-      table.integer('depth').unsigned().notNullable()
-      table.string('title').notNullable()
-      table.boolean('isPrivate').notNullable().defaultTo(false)
-      table.boolean('isFolder').notNullable().defaultTo(false)
-      table.string('privateNS')
-    })
-    .table('pageTree', table => {
-      table.integer('parent').unsigned().references('id').inTable('pageTree')
-      table.integer('pageId').unsigned().references('id').inTable('pages')
-      table.string('localeCode', 5).references('code').inTable('locales')
-    })
-}

+ 0 - 20
server/db/beta/migrations/2.0.0-rc.29.js

@@ -1,20 +0,0 @@
-/* global WIKI */
-
-exports.up = knex => {
-  return knex.schema
-    .table('pages', table => {
-      switch (WIKI.config.db.type) {
-        case 'mariadb':
-        case 'mysql':
-          table.specificType('content', 'LONGTEXT').alter()
-          table.specificType('render', 'LONGTEXT').alter()
-          break
-        case 'mssql':
-          table.specificType('content', 'VARCHAR(max)').alter()
-          table.specificType('render', 'VARCHAR(max)').alter()
-          break
-      }
-    })
-}
-
-exports.down = knex => { }

+ 32 - 0
server/db/legacy/index.js

@@ -0,0 +1,32 @@
+const _ = require('lodash')
+
+/* global WIKI */
+
+module.exports = {
+  async migrate (knex) {
+    const migrationsTableExists = await knex.schema.hasTable('migrations')
+    if (!migrationsTableExists) {
+      return
+    }
+
+    const migrations = await knex('migrations')
+    if (_.some(migrations, m => m.name.indexOf('2.5.128') >= 0)) {
+      // TODO: 2.x MIGRATIONS for 3.0
+      WIKI.logger.error('Upgrading from 2.x is not yet supported. A future release will allow for upgrade from 2.x. Exiting...')
+      process.exit(1)
+
+      // -> Cleanup migration table
+      await knex('migrations').truncate()
+
+      // -> Advance to stable 3.0 migration state
+      await knex('migrations').insert({
+        name: '3.0.0.js',
+        batch: 1,
+        migration_time: knex.fn.now()
+      })
+    } else {
+      console.error('CANNOT UPGRADE FROM OLDER UNSUPPORTED VERSION. UPGRADE TO THE LATEST 2.X VERSION FIRST! Exiting...')
+      process.exit(1)
+    }
+  }
+}

+ 0 - 268
server/db/migrations-sqlite/2.0.0.js

@@ -1,268 +0,0 @@
-exports.up = knex => {
-  return knex.schema
-    // =====================================
-    // MODEL TABLES
-    // =====================================
-    // ANALYTICS ---------------------------
-    .createTable('analytics', table => {
-      table.string('key').notNullable().primary()
-      table.boolean('isEnabled').notNullable().defaultTo(false)
-      table.json('config').notNullable()
-    })
-    // ASSETS ------------------------------
-    .createTable('assets', table => {
-      table.increments('id').primary()
-      table.string('filename').notNullable()
-      table.string('hash').notNullable().defaultTo('')
-      table.string('ext').notNullable()
-      table.enum('kind', ['binary', 'image']).notNullable().defaultTo('binary')
-      table.string('mime').notNullable().defaultTo('application/octet-stream')
-      table.integer('fileSize').unsigned().comment('In kilobytes')
-      table.json('metadata')
-      table.string('createdAt').notNullable()
-      table.string('updatedAt').notNullable()
-
-      table.integer('folderId').unsigned().references('id').inTable('assetFolders')
-      table.integer('authorId').unsigned().references('id').inTable('users')
-    })
-    // ASSET DATA --------------------------
-    .createTable('assetData', table => {
-      table.integer('id').primary()
-      table.binary('data').notNullable()
-    })
-    // ASSET FOLDERS -----------------------
-    .createTable('assetFolders', table => {
-      table.increments('id').primary()
-      table.string('name').notNullable()
-      table.string('slug').notNullable()
-      table.integer('parentId').unsigned().references('id').inTable('assetFolders')
-    })
-    // AUTHENTICATION ----------------------
-    .createTable('authentication', table => {
-      table.string('key').notNullable().primary()
-      table.boolean('isEnabled').notNullable().defaultTo(false)
-      table.json('config').notNullable()
-      table.boolean('selfRegistration').notNullable().defaultTo(false)
-      table.json('domainWhitelist').notNullable()
-      table.json('autoEnrollGroups').notNullable()
-    })
-    // COMMENTS ----------------------------
-    .createTable('comments', table => {
-      table.increments('id').primary()
-      table.text('content').notNullable()
-      table.string('createdAt').notNullable()
-      table.string('updatedAt').notNullable()
-
-      table.integer('pageId').unsigned().references('id').inTable('pages')
-      table.integer('authorId').unsigned().references('id').inTable('users')
-    })
-    // EDITORS -----------------------------
-    .createTable('editors', table => {
-      table.string('key').notNullable().primary()
-      table.boolean('isEnabled').notNullable().defaultTo(false)
-      table.json('config').notNullable()
-    })
-    // GROUPS ------------------------------
-    .createTable('groups', table => {
-      table.increments('id').primary()
-      table.string('name').notNullable()
-      table.json('permissions').notNullable()
-      table.json('pageRules').notNullable()
-      table.boolean('isSystem').notNullable().defaultTo(false)
-      table.string('createdAt').notNullable()
-      table.string('updatedAt').notNullable()
-    })
-    // LOCALES -----------------------------
-    .createTable('locales', table => {
-      table.string('code', 5).notNullable().primary()
-      table.json('strings')
-      table.boolean('isRTL').notNullable().defaultTo(false)
-      table.string('name').notNullable()
-      table.string('nativeName').notNullable()
-      table.integer('availability').notNullable().defaultTo(0)
-      table.string('createdAt').notNullable()
-      table.string('updatedAt').notNullable()
-    })
-    // LOGGING ----------------------------
-    .createTable('loggers', table => {
-      table.string('key').notNullable().primary()
-      table.boolean('isEnabled').notNullable().defaultTo(false)
-      table.string('level').notNullable().defaultTo('warn')
-      table.json('config')
-    })
-    // NAVIGATION ----------------------------
-    .createTable('navigation', table => {
-      table.string('key').notNullable().primary()
-      table.json('config')
-    })
-    // PAGE HISTORY ------------------------
-    .createTable('pageHistory', table => {
-      table.increments('id').primary()
-      table.string('path').notNullable()
-      table.string('hash').notNullable()
-      table.string('title').notNullable()
-      table.string('description')
-      table.boolean('isPrivate').notNullable().defaultTo(false)
-      table.boolean('isPublished').notNullable().defaultTo(false)
-      table.string('publishStartDate')
-      table.string('publishEndDate')
-      table.text('content')
-      table.string('contentType').notNullable()
-      table.string('createdAt').notNullable()
-      table.string('action').defaultTo('updated')
-
-      table.integer('pageId').unsigned()
-      table.string('editorKey').references('key').inTable('editors')
-      table.string('localeCode', 5).references('code').inTable('locales')
-      table.integer('authorId').unsigned().references('id').inTable('users')
-    })
-    // PAGE LINKS --------------------------
-    .createTable('pageLinks', table => {
-      table.increments('id').primary()
-      table.integer('pageId').unsigned().references('id').inTable('pages').onDelete('CASCADE')
-      table.string('path').notNullable()
-      table.string('localeCode', 5).notNullable()
-    })
-    // PAGES -------------------------------
-    .createTable('pages', table => {
-      table.increments('id').primary()
-      table.string('path').notNullable()
-      table.string('hash').notNullable()
-      table.string('title').notNullable()
-      table.string('description')
-      table.boolean('isPrivate').notNullable().defaultTo(false)
-      table.boolean('isPublished').notNullable().defaultTo(false)
-      table.string('privateNS')
-      table.string('publishStartDate')
-      table.string('publishEndDate')
-      table.text('content')
-      table.text('render')
-      table.json('toc')
-      table.string('contentType').notNullable()
-      table.string('createdAt').notNullable()
-      table.string('updatedAt').notNullable()
-
-      table.string('editorKey').references('key').inTable('editors')
-      table.string('localeCode', 5).references('code').inTable('locales')
-      table.integer('authorId').unsigned().references('id').inTable('users')
-      table.integer('creatorId').unsigned().references('id').inTable('users')
-    })
-    // PAGE TREE ---------------------------
-    .createTable('pageTree', table => {
-      table.integer('id').primary()
-      table.string('path').notNullable()
-      table.integer('depth').unsigned().notNullable()
-      table.string('title').notNullable()
-      table.boolean('isPrivate').notNullable().defaultTo(false)
-      table.boolean('isFolder').notNullable().defaultTo(false)
-      table.string('privateNS')
-
-      table.integer('parent').unsigned().references('id').inTable('pageTree').onDelete('CASCADE')
-      table.integer('pageId').unsigned().references('id').inTable('pages').onDelete('CASCADE')
-      table.string('localeCode', 5).references('code').inTable('locales')
-    })
-    // RENDERERS ---------------------------
-    .createTable('renderers', table => {
-      table.string('key').notNullable().primary()
-      table.boolean('isEnabled').notNullable().defaultTo(false)
-      table.json('config')
-    })
-    // SEARCH ------------------------------
-    .createTable('searchEngines', table => {
-      table.string('key').notNullable().primary()
-      table.boolean('isEnabled').notNullable().defaultTo(false)
-      table.json('config')
-    })
-    // SETTINGS ----------------------------
-    .createTable('settings', table => {
-      table.string('key').notNullable().primary()
-      table.json('value')
-      table.string('updatedAt').notNullable()
-    })
-    // STORAGE -----------------------------
-    .createTable('storage', table => {
-      table.string('key').notNullable().primary()
-      table.boolean('isEnabled').notNullable().defaultTo(false)
-      table.string('mode', ['sync', 'push', 'pull']).notNullable().defaultTo('push')
-      table.json('config')
-      table.string('syncInterval')
-      table.json('state')
-    })
-    // TAGS --------------------------------
-    .createTable('tags', table => {
-      table.increments('id').primary()
-      table.string('tag').notNullable().unique()
-      table.string('title')
-      table.string('createdAt').notNullable()
-      table.string('updatedAt').notNullable()
-    })
-    // USER KEYS ---------------------------
-    .createTable('userKeys', table => {
-      table.increments('id').primary()
-      table.string('kind').notNullable()
-      table.string('token').notNullable()
-      table.string('createdAt').notNullable()
-      table.string('validUntil').notNullable()
-
-      table.integer('userId').unsigned().references('id').inTable('users')
-    })
-    // USERS -------------------------------
-    .createTable('users', table => {
-      table.increments('id').primary()
-      table.string('email').notNullable()
-      table.string('name').notNullable()
-      table.string('providerId')
-      table.string('password')
-      table.boolean('tfaIsActive').notNullable().defaultTo(false)
-      table.string('tfaSecret')
-      table.string('jobTitle').defaultTo('')
-      table.string('location').defaultTo('')
-      table.string('pictureUrl')
-      table.string('timezone').notNullable().defaultTo('America/New_York')
-      table.boolean('isSystem').notNullable().defaultTo(false)
-      table.boolean('isActive').notNullable().defaultTo(false)
-      table.boolean('isVerified').notNullable().defaultTo(false)
-      table.boolean('mustChangePwd').notNullable().defaultTo(false)
-      table.string('createdAt').notNullable()
-      table.string('updatedAt').notNullable()
-
-      table.string('providerKey').references('key').inTable('authentication').notNullable().defaultTo('local')
-      table.string('localeCode', 5).references('code').inTable('locales').notNullable().defaultTo('en')
-      table.string('defaultEditor').references('key').inTable('editors').notNullable().defaultTo('markdown')
-    })
-    // =====================================
-    // RELATION TABLES
-    // =====================================
-    // PAGE HISTORY TAGS ---------------------------
-    .createTable('pageHistoryTags', table => {
-      table.increments('id').primary()
-      table.integer('pageId').unsigned().references('id').inTable('pageHistory').onDelete('CASCADE')
-      table.integer('tagId').unsigned().references('id').inTable('tags').onDelete('CASCADE')
-    })
-    // PAGE TAGS ---------------------------
-    .createTable('pageTags', table => {
-      table.increments('id').primary()
-      table.integer('pageId').unsigned().references('id').inTable('pages').onDelete('CASCADE')
-      table.integer('tagId').unsigned().references('id').inTable('tags').onDelete('CASCADE')
-    })
-    // USER GROUPS -------------------------
-    .createTable('userGroups', table => {
-      table.increments('id').primary()
-      table.integer('userId').unsigned().references('id').inTable('users').onDelete('CASCADE')
-      table.integer('groupId').unsigned().references('id').inTable('groups').onDelete('CASCADE')
-    })
-    // =====================================
-    // REFERENCES
-    // =====================================
-    .table('users', table => {
-      table.unique(['providerKey', 'email'])
-    })
-    // =====================================
-    // INDEXES
-    // =====================================
-    .table('pageLinks', table => {
-      table.index(['path', 'localeCode'])
-    })
-}
-
-exports.down = knex => { }

+ 0 - 9
server/db/migrations-sqlite/2.2.17.js

@@ -1,9 +0,0 @@
-exports.up = knex => {
-  return knex.schema
-    .alterTable('pageHistory', table => {
-      table.string('versionDate').notNullable().defaultTo('')
-    })
-    .raw(`UPDATE pageHistory AS h1 SET versionDate = COALESCE((SELECT createdAt FROM pageHistory AS h2 WHERE h2.pageId = h1.pageId AND h2.id < h1.id ORDER BY h2.id DESC LIMIT 1), h1.createdAt, '')`)
-}
-
-exports.down = knex => { }

+ 0 - 14
server/db/migrations-sqlite/2.2.3.js

@@ -1,14 +0,0 @@
-exports.up = knex => {
-  return knex.schema
-    .createTable('apiKeys', table => {
-      table.increments('id').primary()
-      table.string('name').notNullable()
-      table.text('key').notNullable()
-      table.string('expiration').notNullable()
-      table.boolean('isRevoked').notNullable().defaultTo(false)
-      table.string('createdAt').notNullable()
-      table.string('updatedAt').notNullable()
-    })
-}
-
-exports.down = knex => { }

+ 0 - 8
server/db/migrations-sqlite/2.3.10.js

@@ -1,8 +0,0 @@
-exports.up = knex => {
-  return knex.schema
-    .alterTable('users', table => {
-      table.string('lastLoginAt')
-    })
-}
-
-exports.down = knex => { }

+ 0 - 10
server/db/migrations-sqlite/2.3.14.js

@@ -1,10 +0,0 @@
-exports.up = knex => {
-  return knex.schema
-    .createTable('commentProviders', table => {
-      table.string('key').notNullable().primary()
-      table.boolean('isEnabled').notNullable().defaultTo(false)
-      table.json('config').notNullable()
-    })
-}
-
-exports.down = knex => { }

+ 0 - 8
server/db/migrations-sqlite/2.3.23.js

@@ -1,8 +0,0 @@
-exports.up = knex => {
-  return knex.schema
-    .alterTable('pageTree', table => {
-      table.json('ancestors')
-    })
-}
-
-exports.down = knex => { }

+ 0 - 15
server/db/migrations-sqlite/2.4.13.js

@@ -1,15 +0,0 @@
-exports.up = knex => {
-  return knex.schema
-    .alterTable('pages', table => {
-      table.json('extra').notNullable().defaultTo('{}')
-    })
-    .alterTable('pageHistory', table => {
-      table.json('extra').notNullable().defaultTo('{}')
-    })
-    .alterTable('users', table => {
-      table.string('dateFormat').notNullable().defaultTo('')
-      table.string('appearance').notNullable().defaultTo('')
-    })
-}
-
-exports.down = knex => { }

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

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

+ 0 - 8
server/db/migrations-sqlite/2.4.61.js

@@ -1,8 +0,0 @@
-exports.up = knex => {
-  return knex.schema
-    .alterTable('comments', table => {
-      table.integer('replyTo').unsigned().notNullable().defaultTo(0)
-    })
-}
-
-exports.down = knex => { }

+ 0 - 34
server/db/migrations-sqlite/2.5.1.js

@@ -1,34 +0,0 @@
-exports.up = async knex => {
-  // Check for users using disabled strategies
-  let protectedStrategies = []
-  const disabledStrategies = await knex('authentication').where('isEnabled', false)
-  if (disabledStrategies) {
-    const incompatibleUsers = await knex('users').distinct('providerKey').whereIn('providerKey', disabledStrategies.map(s => s.key))
-    if (incompatibleUsers && incompatibleUsers.length > 0) {
-      protectedStrategies = incompatibleUsers.map(u => u.providerKey)
-    }
-  }
-
-  // Delete disabled strategies
-  await knex('authentication').whereNotIn('key', protectedStrategies).andWhere('isEnabled', false).del()
-
-  // Update table schema
-  await knex.schema
-    .alterTable('authentication', table => {
-      table.integer('order').unsigned().notNullable().defaultTo(0)
-      table.string('strategyKey').notNullable().defaultTo('')
-      table.string('displayName').notNullable().defaultTo('')
-    })
-
-  // Fix pre-2.5 strategies
-  const strategies = await knex('authentication')
-  let idx = 1
-  for (const strategy of strategies) {
-    await knex('authentication').where('key', strategy.key).update({
-      strategyKey: strategy.key,
-      order: (strategy.key === 'local') ? 0 : idx++
-    })
-  }
-}
-
-exports.down = knex => { }

+ 0 - 14
server/db/migrations-sqlite/2.5.108.js

@@ -1,14 +0,0 @@
-const has = require('lodash/has')
-
-exports.up = async knex => {
-  // -> Fix 2.5.1 added isEnabled columns for beta users
-  const localStrategy = await knex('authentication').where('key', 'local').first()
-  if (localStrategy && !has(localStrategy, 'isEnabled')) {
-    await knex.schema
-      .alterTable('authentication', table => {
-        table.boolean('isEnabled').notNullable().defaultTo(true)
-      })
-  }
-}
-
-exports.down = knex => { }

+ 0 - 6
server/db/migrations-sqlite/2.5.118.js

@@ -1,6 +0,0 @@
-exports.up = async knex => {
-  // -> Fix 2.5.117 new installations without isEnabled on local auth (#2382)
-  await knex('authentication').where('key', 'local').update({ isEnabled: true })
-}
-
-exports.down = knex => { }

+ 0 - 8
server/db/migrations-sqlite/2.5.12.js

@@ -1,8 +0,0 @@
-exports.up = async knex => {
-  await knex.schema
-    .alterTable('groups', table => {
-      table.string('redirectOnLogin').notNullable().defaultTo('/')
-    })
-}
-
-exports.down = knex => { }

+ 0 - 9
server/db/migrations-sqlite/2.5.122.js

@@ -1,9 +0,0 @@
-exports.up = knex => {
-  return knex.schema
-    .createTable('userAvatars', table => {
-      table.integer('id').primary()
-      table.binary('data').notNullable()
-    })
-}
-
-exports.down = knex => { }

+ 0 - 7
server/db/migrations-sqlite/2.5.128.js

@@ -1,7 +0,0 @@
-exports.up = async knex => {
-  await knex('users').update({
-    email: knex.raw('LOWER(email)')
-  })
-}
-
-exports.down = knex => { }

+ 0 - 325
server/db/migrations/2.0.0.js

@@ -1,325 +0,0 @@
-/* global WIKI */
-
-exports.up = knex => {
-  const dbCompat = {
-    blobLength: (WIKI.config.db.type === `mysql` || WIKI.config.db.type === `mariadb`),
-    charset: (WIKI.config.db.type === `mysql` || WIKI.config.db.type === `mariadb`),
-    selfCascadeDelete: WIKI.config.db.type !== 'mssql'
-  }
-  return knex.schema
-    // =====================================
-    // MODEL TABLES
-    // =====================================
-    // ANALYTICS ---------------------------
-    .createTable('analytics', table => {
-      if (dbCompat.charset) { table.charset('utf8mb4') }
-      table.string('key').notNullable().primary()
-      table.boolean('isEnabled').notNullable().defaultTo(false)
-      table.json('config').notNullable()
-    })
-    // ASSETS ------------------------------
-    .createTable('assets', table => {
-      if (dbCompat.charset) { table.charset('utf8mb4') }
-      table.increments('id').primary()
-      table.string('filename').notNullable()
-      table.string('hash').notNullable()
-      table.string('ext').notNullable()
-      table.enum('kind', ['binary', 'image']).notNullable().defaultTo('binary')
-      table.string('mime').notNullable().defaultTo('application/octet-stream')
-      table.integer('fileSize').unsigned().comment('In kilobytes')
-      table.json('metadata')
-      table.string('createdAt').notNullable()
-      table.string('updatedAt').notNullable()
-    })
-    // ASSET DATA --------------------------
-    .createTable('assetData', table => {
-      if (dbCompat.charset) { table.charset('utf8mb4') }
-      table.integer('id').primary()
-      if (dbCompat.blobLength) {
-        table.specificType('data', 'LONGBLOB').notNullable()
-      } else {
-        table.binary('data').notNullable()
-      }
-    })
-    // ASSET FOLDERS -----------------------
-    .createTable('assetFolders', table => {
-      if (dbCompat.charset) { table.charset('utf8mb4') }
-      table.increments('id').primary()
-      table.string('name').notNullable()
-      table.string('slug').notNullable()
-      table.integer('parentId').unsigned().references('id').inTable('assetFolders')
-    })
-    // AUTHENTICATION ----------------------
-    .createTable('authentication', table => {
-      if (dbCompat.charset) { table.charset('utf8mb4') }
-      table.string('key').notNullable().primary()
-      table.boolean('isEnabled').notNullable().defaultTo(false)
-      table.json('config').notNullable()
-      table.boolean('selfRegistration').notNullable().defaultTo(false)
-      table.json('domainWhitelist').notNullable()
-      table.json('autoEnrollGroups').notNullable()
-    })
-    // COMMENTS ----------------------------
-    .createTable('comments', table => {
-      if (dbCompat.charset) { table.charset('utf8mb4') }
-      table.increments('id').primary()
-      table.text('content').notNullable()
-      table.string('createdAt').notNullable()
-      table.string('updatedAt').notNullable()
-    })
-    // EDITORS -----------------------------
-    .createTable('editors', table => {
-      if (dbCompat.charset) { table.charset('utf8mb4') }
-      table.string('key').notNullable().primary()
-      table.boolean('isEnabled').notNullable().defaultTo(false)
-      table.json('config').notNullable()
-    })
-    // GROUPS ------------------------------
-    .createTable('groups', table => {
-      if (dbCompat.charset) { table.charset('utf8mb4') }
-      table.increments('id').primary()
-      table.string('name').notNullable()
-      table.json('permissions').notNullable()
-      table.json('pageRules').notNullable()
-      table.boolean('isSystem').notNullable().defaultTo(false)
-      table.string('createdAt').notNullable()
-      table.string('updatedAt').notNullable()
-    })
-    // LOCALES -----------------------------
-    .createTable('locales', table => {
-      if (dbCompat.charset) { table.charset('utf8mb4') }
-      table.string('code', 5).notNullable().primary()
-      table.json('strings')
-      table.boolean('isRTL').notNullable().defaultTo(false)
-      table.string('name').notNullable()
-      table.string('nativeName').notNullable()
-      table.integer('availability').notNullable().defaultTo(0)
-      table.string('createdAt').notNullable()
-      table.string('updatedAt').notNullable()
-    })
-    // LOGGING ----------------------------
-    .createTable('loggers', table => {
-      if (dbCompat.charset) { table.charset('utf8mb4') }
-      table.string('key').notNullable().primary()
-      table.boolean('isEnabled').notNullable().defaultTo(false)
-      table.string('level').notNullable().defaultTo('warn')
-      table.json('config')
-    })
-    // NAVIGATION ----------------------------
-    .createTable('navigation', table => {
-      if (dbCompat.charset) { table.charset('utf8mb4') }
-      table.string('key').notNullable().primary()
-      table.json('config')
-    })
-    // PAGE HISTORY ------------------------
-    .createTable('pageHistory', table => {
-      if (dbCompat.charset) { table.charset('utf8mb4') }
-      table.increments('id').primary()
-      table.string('path').notNullable()
-      table.string('hash').notNullable()
-      table.string('title').notNullable()
-      table.string('description')
-      table.boolean('isPrivate').notNullable().defaultTo(false)
-      table.boolean('isPublished').notNullable().defaultTo(false)
-      table.string('publishStartDate')
-      table.string('publishEndDate')
-      table.string('action').defaultTo('updated')
-      table.integer('pageId').unsigned()
-      table.text('content')
-      table.string('contentType').notNullable()
-      table.string('createdAt').notNullable()
-    })
-    // PAGE LINKS --------------------------
-    .createTable('pageLinks', table => {
-      if (dbCompat.charset) { table.charset('utf8mb4') }
-      table.increments('id').primary()
-      table.string('path').notNullable()
-      table.string('localeCode', 5).notNullable()
-    })
-    // PAGES -------------------------------
-    .createTable('pages', table => {
-      if (dbCompat.charset) { table.charset('utf8mb4') }
-      table.increments('id').primary()
-      table.string('path').notNullable()
-      table.string('hash').notNullable()
-      table.string('title').notNullable()
-      table.string('description')
-      table.boolean('isPrivate').notNullable().defaultTo(false)
-      table.boolean('isPublished').notNullable().defaultTo(false)
-      table.string('privateNS')
-      table.string('publishStartDate')
-      table.string('publishEndDate')
-      switch (WIKI.config.db.type) {
-        case 'postgres':
-        case 'sqlite':
-          table.text('content')
-          table.text('render')
-          break
-        case 'mariadb':
-        case 'mysql':
-          table.specificType('content', 'LONGTEXT')
-          table.specificType('render', 'LONGTEXT')
-          break
-        case 'mssql':
-          table.specificType('content', 'VARCHAR(max)')
-          table.specificType('render', 'VARCHAR(max)')
-          break
-      }
-      table.json('toc')
-      table.string('contentType').notNullable()
-      table.string('createdAt').notNullable()
-      table.string('updatedAt').notNullable()
-    })
-    // PAGE TREE ---------------------------
-    .createTable('pageTree', table => {
-      if (dbCompat.charset) { table.charset('utf8mb4') }
-      table.integer('id').unsigned().primary()
-      table.string('path').notNullable()
-      table.integer('depth').unsigned().notNullable()
-      table.string('title').notNullable()
-      table.boolean('isPrivate').notNullable().defaultTo(false)
-      table.boolean('isFolder').notNullable().defaultTo(false)
-      table.string('privateNS')
-    })
-    // RENDERERS ---------------------------
-    .createTable('renderers', table => {
-      if (dbCompat.charset) { table.charset('utf8mb4') }
-      table.string('key').notNullable().primary()
-      table.boolean('isEnabled').notNullable().defaultTo(false)
-      table.json('config')
-    })
-    // SEARCH ------------------------------
-    .createTable('searchEngines', table => {
-      if (dbCompat.charset) { table.charset('utf8mb4') }
-      table.string('key').notNullable().primary()
-      table.boolean('isEnabled').notNullable().defaultTo(false)
-      table.json('config')
-    })
-    // SETTINGS ----------------------------
-    .createTable('settings', table => {
-      if (dbCompat.charset) { table.charset('utf8mb4') }
-      table.string('key').notNullable().primary()
-      table.json('value')
-      table.string('updatedAt').notNullable()
-    })
-    // STORAGE -----------------------------
-    .createTable('storage', table => {
-      if (dbCompat.charset) { table.charset('utf8mb4') }
-      table.string('key').notNullable().primary()
-      table.boolean('isEnabled').notNullable().defaultTo(false)
-      table.string('mode', ['sync', 'push', 'pull']).notNullable().defaultTo('push')
-      table.json('config')
-      table.string('syncInterval')
-      table.json('state')
-    })
-    // TAGS --------------------------------
-    .createTable('tags', table => {
-      if (dbCompat.charset) { table.charset('utf8mb4') }
-      table.increments('id').primary()
-      table.string('tag').notNullable().unique()
-      table.string('title')
-      table.string('createdAt').notNullable()
-      table.string('updatedAt').notNullable()
-    })
-    // USER KEYS ---------------------------
-    .createTable('userKeys', table => {
-      if (dbCompat.charset) { table.charset('utf8mb4') }
-      table.increments('id').primary()
-      table.string('kind').notNullable()
-      table.string('token').notNullable()
-      table.string('createdAt').notNullable()
-      table.string('validUntil').notNullable()
-    })
-    // USERS -------------------------------
-    .createTable('users', table => {
-      if (dbCompat.charset) { table.charset('utf8mb4') }
-      table.increments('id').primary()
-      table.string('email').notNullable()
-      table.string('name').notNullable()
-      table.string('providerId')
-      table.string('password')
-      table.boolean('tfaIsActive').notNullable().defaultTo(false)
-      table.string('tfaSecret')
-      table.string('jobTitle').defaultTo('')
-      table.string('location').defaultTo('')
-      table.string('pictureUrl')
-      table.string('timezone').notNullable().defaultTo('America/New_York')
-      table.boolean('isSystem').notNullable().defaultTo(false)
-      table.boolean('isActive').notNullable().defaultTo(false)
-      table.boolean('isVerified').notNullable().defaultTo(false)
-      table.boolean('mustChangePwd').notNullable().defaultTo(false)
-      table.string('createdAt').notNullable()
-      table.string('updatedAt').notNullable()
-    })
-    // =====================================
-    // RELATION TABLES
-    // =====================================
-    // PAGE HISTORY TAGS ---------------------------
-    .createTable('pageHistoryTags', table => {
-      if (dbCompat.charset) { table.charset('utf8mb4') }
-      table.increments('id').primary()
-      table.integer('pageId').unsigned().references('id').inTable('pageHistory').onDelete('CASCADE')
-      table.integer('tagId').unsigned().references('id').inTable('tags').onDelete('CASCADE')
-    })
-    // PAGE TAGS ---------------------------
-    .createTable('pageTags', table => {
-      if (dbCompat.charset) { table.charset('utf8mb4') }
-      table.increments('id').primary()
-      table.integer('pageId').unsigned().references('id').inTable('pages').onDelete('CASCADE')
-      table.integer('tagId').unsigned().references('id').inTable('tags').onDelete('CASCADE')
-    })
-    // USER GROUPS -------------------------
-    .createTable('userGroups', table => {
-      if (dbCompat.charset) { table.charset('utf8mb4') }
-      table.increments('id').primary()
-      table.integer('userId').unsigned().references('id').inTable('users').onDelete('CASCADE')
-      table.integer('groupId').unsigned().references('id').inTable('groups').onDelete('CASCADE')
-    })
-    // =====================================
-    // REFERENCES
-    // =====================================
-    .table('assets', table => {
-      table.integer('folderId').unsigned().references('id').inTable('assetFolders')
-      table.integer('authorId').unsigned().references('id').inTable('users')
-    })
-    .table('comments', table => {
-      table.integer('pageId').unsigned().references('id').inTable('pages')
-      table.integer('authorId').unsigned().references('id').inTable('users')
-    })
-    .table('pageHistory', table => {
-      table.string('editorKey').references('key').inTable('editors')
-      table.string('localeCode', 5).references('code').inTable('locales')
-      table.integer('authorId').unsigned().references('id').inTable('users')
-    })
-    .table('pageLinks', table => {
-      table.integer('pageId').unsigned().references('id').inTable('pages').onDelete('CASCADE')
-      table.index(['path', 'localeCode'])
-    })
-    .table('pages', table => {
-      table.string('editorKey').references('key').inTable('editors')
-      table.string('localeCode', 5).references('code').inTable('locales')
-      table.integer('authorId').unsigned().references('id').inTable('users')
-      table.integer('creatorId').unsigned().references('id').inTable('users')
-    })
-    .table('pageTree', table => {
-      if (dbCompat.selfCascadeDelete) {
-        table.integer('parent').unsigned().references('id').inTable('pageTree').onDelete('CASCADE')
-      } else {
-        table.integer('parent').unsigned()
-      }
-      table.integer('pageId').unsigned().references('id').inTable('pages').onDelete('CASCADE')
-      table.string('localeCode', 5).references('code').inTable('locales')
-    })
-    .table('userKeys', table => {
-      table.integer('userId').unsigned().references('id').inTable('users')
-    })
-    .table('users', table => {
-      table.string('providerKey').references('key').inTable('authentication').notNullable().defaultTo('local')
-      table.string('localeCode', 5).references('code').inTable('locales').notNullable().defaultTo('en')
-      table.string('defaultEditor').references('key').inTable('editors').notNullable().defaultTo('markdown')
-
-      table.unique(['providerKey', 'email'])
-    })
-}
-
-exports.down = knex => { }

+ 0 - 19
server/db/migrations/2.1.85.js

@@ -1,19 +0,0 @@
-/* global WIKI */
-
-exports.up = knex => {
-  return knex.schema
-    .alterTable('pageHistory', table => {
-      switch (WIKI.config.db.type) {
-        // No change needed for PostgreSQL and SQLite
-        case 'mariadb':
-        case 'mysql':
-          table.specificType('content', 'LONGTEXT').alter()
-          break
-        case 'mssql':
-          table.specificType('content', 'VARCHAR(max)').alter()
-          break
-      }
-    })
-}
-
-exports.down = knex => { }

+ 0 - 37
server/db/migrations/2.2.17.js

@@ -1,37 +0,0 @@
-const _ = require('lodash')
-
-/* global WIKI */
-
-exports.up = async knex => {
-  let sqlVersionDate = ''
-  switch (WIKI.config.db.type) {
-    case 'postgres':
-      sqlVersionDate = 'UPDATE "pageHistory" h1 SET "versionDate" = COALESCE((SELECT prev."createdAt" FROM "pageHistory" prev WHERE prev."pageId" = h1."pageId" AND prev.id < h1.id ORDER BY prev.id DESC LIMIT 1), h1."createdAt")'
-      break
-    case 'mssql':
-      sqlVersionDate = 'UPDATE h1 SET "versionDate" = COALESCE((SELECT TOP 1 prev."createdAt" FROM "pageHistory" prev WHERE prev."pageId" = h1."pageId" AND prev.id < h1.id ORDER BY prev.id DESC), h1."createdAt") FROM "pageHistory" h1'
-      break
-    case 'mysql':
-    case 'mariadb':
-      // -> Fix for 2.2.50 failed migration
-      const pageHistoryColumns = await knex.schema.raw('SHOW COLUMNS FROM pageHistory')
-      if (_.some(pageHistoryColumns[0], ['Field', 'versionDate'])) {
-        console.info('MySQL 2.2.50 Migration Fix - Dropping failed versionDate column...')
-        await knex.schema.raw('ALTER TABLE pageHistory DROP COLUMN versionDate')
-        console.info('versionDate column dropped successfully.')
-      }
-
-      sqlVersionDate = `UPDATE pageHistory AS h1 INNER JOIN pageHistory AS h2 ON h2.id = (SELECT prev.id FROM (SELECT * FROM pageHistory) AS prev WHERE prev.pageId = h1.pageId AND prev.id < h1.id ORDER BY prev.id DESC LIMIT 1) SET h1.versionDate = h2.createdAt`
-      break
-    // case 'mariadb':
-    //   sqlVersionDate = `UPDATE pageHistory AS h1 INNER JOIN pageHistory AS h2 ON h2.id = (SELECT prev.id FROM pageHistory AS prev WHERE prev.pageId = h1.pageId AND prev.id < h1.id ORDER BY prev.id DESC LIMIT 1) SET h1.versionDate = h2.createdAt`
-    //   break
-  }
-  await knex.schema
-    .alterTable('pageHistory', table => {
-      table.string('versionDate').notNullable().defaultTo('')
-    })
-    .raw(sqlVersionDate)
-}
-
-exports.down = knex => { }

+ 0 - 20
server/db/migrations/2.2.3.js

@@ -1,20 +0,0 @@
-/* global WIKI */
-
-exports.up = knex => {
-  const dbCompat = {
-    charset: (WIKI.config.db.type === `mysql` || WIKI.config.db.type === `mariadb`)
-  }
-  return knex.schema
-    .createTable('apiKeys', table => {
-      if (dbCompat.charset) { table.charset('utf8mb4') }
-      table.increments('id').primary()
-      table.string('name').notNullable()
-      table.text('key').notNullable()
-      table.string('expiration').notNullable()
-      table.boolean('isRevoked').notNullable().defaultTo(false)
-      table.string('createdAt').notNullable()
-      table.string('updatedAt').notNullable()
-    })
-}
-
-exports.down = knex => { }

+ 0 - 8
server/db/migrations/2.3.10.js

@@ -1,8 +0,0 @@
-exports.up = knex => {
-  return knex.schema
-    .alterTable('users', table => {
-      table.string('lastLoginAt')
-    })
-}
-
-exports.down = knex => { }

+ 0 - 8
server/db/migrations/2.3.23.js

@@ -1,8 +0,0 @@
-exports.up = knex => {
-  return knex.schema
-    .alterTable('pageTree', table => {
-      table.json('ancestors')
-    })
-}
-
-exports.down = knex => { }

+ 0 - 25
server/db/migrations/2.4.13.js

@@ -1,25 +0,0 @@
-/* global WIKI */
-
-exports.up = knex => {
-  return knex.schema
-    .alterTable('pages', table => {
-      if (WIKI.config.db.type === 'mysql') {
-        table.json('extra')
-      } else {
-        table.json('extra').notNullable().defaultTo('{}')
-      }
-    })
-    .alterTable('pageHistory', table => {
-      if (WIKI.config.db.type === 'mysql') {
-        table.json('extra')
-      } else {
-        table.json('extra').notNullable().defaultTo('{}')
-      }
-    })
-    .alterTable('users', table => {
-      table.string('dateFormat').notNullable().defaultTo('')
-      table.string('appearance').notNullable().defaultTo('')
-    })
-}
-
-exports.down = knex => { }

+ 0 - 16
server/db/migrations/2.4.14.js

@@ -1,16 +0,0 @@
-/* global WIKI */
-
-exports.up = knex => {
-  const dbCompat = {
-    charset: (WIKI.config.db.type === `mysql` || WIKI.config.db.type === `mariadb`)
-  }
-  return knex.schema
-    .createTable('commentProviders', table => {
-      if (dbCompat.charset) { table.charset('utf8mb4') }
-      table.string('key').notNullable().primary()
-      table.boolean('isEnabled').notNullable().defaultTo(false)
-      table.json('config').notNullable()
-    })
-}
-
-exports.down = knex => { }

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

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

+ 0 - 8
server/db/migrations/2.4.61.js

@@ -1,8 +0,0 @@
-exports.up = knex => {
-  return knex.schema
-    .alterTable('comments', table => {
-      table.integer('replyTo').unsigned().notNullable().defaultTo(0)
-    })
-}
-
-exports.down = knex => { }

+ 0 - 34
server/db/migrations/2.5.1.js

@@ -1,34 +0,0 @@
-exports.up = async knex => {
-  // Check for users using disabled strategies
-  let protectedStrategies = []
-  const disabledStrategies = await knex('authentication').where('isEnabled', false)
-  if (disabledStrategies) {
-    const incompatibleUsers = await knex('users').distinct('providerKey').whereIn('providerKey', disabledStrategies.map(s => s.key))
-    if (incompatibleUsers && incompatibleUsers.length > 0) {
-      protectedStrategies = incompatibleUsers.map(u => u.providerKey)
-    }
-  }
-
-  // Delete disabled strategies
-  await knex('authentication').whereNotIn('key', protectedStrategies).andWhere('isEnabled', false).del()
-
-  // Update table schema
-  await knex.schema
-    .alterTable('authentication', table => {
-      table.integer('order').unsigned().notNullable().defaultTo(0)
-      table.string('strategyKey').notNullable().defaultTo('')
-      table.string('displayName').notNullable().defaultTo('')
-    })
-
-  // Fix pre-2.5 strategies
-  const strategies = await knex('authentication')
-  let idx = 1
-  for (const strategy of strategies) {
-    await knex('authentication').where('key', strategy.key).update({
-      strategyKey: strategy.key,
-      order: (strategy.key === 'local') ? 0 : idx++
-    })
-  }
-}
-
-exports.down = knex => { }

+ 0 - 14
server/db/migrations/2.5.108.js

@@ -1,14 +0,0 @@
-const has = require('lodash/has')
-
-exports.up = async knex => {
-  // -> Fix 2.5.1 added isEnabled columns for beta users
-  const localStrategy = await knex('authentication').where('key', 'local').first()
-  if (localStrategy && !has(localStrategy, 'isEnabled')) {
-    await knex.schema
-      .alterTable('authentication', table => {
-        table.boolean('isEnabled').notNullable().defaultTo(true)
-      })
-  }
-}
-
-exports.down = knex => { }

+ 0 - 6
server/db/migrations/2.5.118.js

@@ -1,6 +0,0 @@
-exports.up = async knex => {
-  // -> Fix 2.5.117 new installations without isEnabled on local auth (#2382)
-  await knex('authentication').where('key', 'local').update({ isEnabled: true })
-}
-
-exports.down = knex => { }

+ 0 - 8
server/db/migrations/2.5.12.js

@@ -1,8 +0,0 @@
-exports.up = async knex => {
-  await knex.schema
-    .alterTable('groups', table => {
-      table.string('redirectOnLogin').notNullable().defaultTo('/')
-    })
-}
-
-exports.down = knex => { }

+ 0 - 20
server/db/migrations/2.5.122.js

@@ -1,20 +0,0 @@
-/* global WIKI */
-
-exports.up = knex => {
-  const dbCompat = {
-    blobLength: (WIKI.config.db.type === `mysql` || WIKI.config.db.type === `mariadb`),
-    charset: (WIKI.config.db.type === `mysql` || WIKI.config.db.type === `mariadb`)
-  }
-  return knex.schema
-    .createTable('userAvatars', table => {
-      if (dbCompat.charset) { table.charset('utf8mb4') }
-      table.integer('id').primary()
-      if (dbCompat.blobLength) {
-        table.specificType('data', 'LONGBLOB').notNullable()
-      } else {
-        table.binary('data').notNullable()
-      }
-    })
-}
-
-exports.down = knex => { }

+ 0 - 7
server/db/migrations/2.5.128.js

@@ -1,7 +0,0 @@
-exports.up = async knex => {
-  await knex('users').update({
-    email: knex.raw('LOWER(??)', ['email'])
-  })
-}
-
-exports.down = knex => { }

+ 589 - 0
server/db/migrations/3.0.0.js

@@ -0,0 +1,589 @@
+const { v4: uuid } = require('uuid')
+const bcrypt = require('bcryptjs-then')
+const crypto = require('crypto')
+const pem2jwk = require('pem-jwk').pem2jwk
+
+/* global WIKI */
+
+exports.up = async knex => {
+  WIKI.logger.info('Running 3.0.0 database migration...')
+
+  // =====================================
+  // PG EXTENSIONS
+  // =====================================
+  await knex.raw('CREATE EXTENSION IF NOT EXISTS pgcrypto;')
+
+  await knex.schema
+    // =====================================
+    // MODEL TABLES
+    // =====================================
+    // ANALYTICS ---------------------------
+    .createTable('analytics', table => {
+      table.uuid('id').notNullable().primary().defaultTo(knex.raw('gen_random_uuid()'))
+      table.string('module').notNullable()
+      table.boolean('isEnabled').notNullable().defaultTo(false)
+      table.jsonb('config').notNullable()
+    })
+    // API KEYS ----------------------------
+    .createTable('apiKeys', table => {
+      table.uuid('id').notNullable().primary().defaultTo(knex.raw('gen_random_uuid()'))
+      table.string('name').notNullable()
+      table.text('key').notNullable()
+      table.string('expiration').notNullable()
+      table.boolean('isRevoked').notNullable().defaultTo(false)
+      table.timestamp('createdAt').notNullable().defaultTo(knex.fn.now())
+      table.timestamp('updatedAt').notNullable().defaultTo(knex.fn.now())
+    })
+    // ASSETS ------------------------------
+    .createTable('assets', table => {
+      table.uuid('id').notNullable().primary().defaultTo(knex.raw('gen_random_uuid()'))
+      table.string('filename').notNullable()
+      table.string('hash').notNullable().index()
+      table.string('ext').notNullable()
+      table.enum('kind', ['binary', 'image']).notNullable().defaultTo('binary')
+      table.string('mime').notNullable().defaultTo('application/octet-stream')
+      table.integer('fileSize').unsigned().comment('In kilobytes')
+      table.jsonb('metadata')
+      table.timestamp('createdAt').notNullable().defaultTo(knex.fn.now())
+      table.timestamp('updatedAt').notNullable().defaultTo(knex.fn.now())
+    })
+    // ASSET DATA --------------------------
+    .createTable('assetData', table => {
+      table.uuid('id').notNullable().index()
+      table.binary('data').notNullable()
+    })
+    // ASSET FOLDERS -----------------------
+    .createTable('assetFolders', table => {
+      table.uuid('id').notNullable().primary().defaultTo(knex.raw('gen_random_uuid()'))
+      table.string('name').notNullable()
+      table.string('slug').notNullable()
+    })
+    // AUTHENTICATION ----------------------
+    .createTable('authentication', table => {
+      table.uuid('id').notNullable().primary().defaultTo(knex.raw('gen_random_uuid()'))
+      table.string('module').notNullable()
+      table.boolean('isEnabled').notNullable().defaultTo(false)
+      table.integer('order').unsigned().notNullable().defaultTo(0)
+      table.string('displayName').notNullable().defaultTo('')
+      table.jsonb('config').notNullable().defaultTo('{}')
+      table.boolean('selfRegistration').notNullable().defaultTo(false)
+      table.jsonb('domainWhitelist').notNullable().defaultTo('[]')
+      table.jsonb('autoEnrollGroups').notNullable().defaultTo('[]')
+      table.jsonb('hideOnSites').notNullable().defaultTo('[]')
+    })
+    // COMMENTS ----------------------------
+    .createTable('comments', table => {
+      table.uuid('id').notNullable().primary().defaultTo(knex.raw('gen_random_uuid()'))
+      table.uuid('replyTo')
+      table.text('content').notNullable()
+      table.text('render').notNullable().defaultTo('')
+      table.string('name').notNullable().defaultTo('')
+      table.string('email').notNullable().defaultTo('')
+      table.string('ip').notNullable().defaultTo('')
+      table.timestamp('createdAt').notNullable().defaultTo(knex.fn.now())
+      table.timestamp('updatedAt').notNullable().defaultTo(knex.fn.now())
+    })
+    // GROUPS ------------------------------
+    .createTable('groups', table => {
+      table.uuid('id').notNullable().primary().defaultTo(knex.raw('gen_random_uuid()'))
+      table.string('name').notNullable()
+      table.jsonb('permissions').notNullable()
+      table.jsonb('rules').notNullable()
+      table.string('redirectOnLogin').notNullable().defaultTo('')
+      table.string('redirectOnFirstLogin').notNullable().defaultTo('')
+      table.string('redirectOnLogout').notNullable().defaultTo('')
+      table.boolean('isSystem').notNullable().defaultTo(false)
+      table.timestamp('createdAt').notNullable().defaultTo(knex.fn.now())
+      table.timestamp('updatedAt').notNullable().defaultTo(knex.fn.now())
+    })
+    // HOOKS -------------------------------
+    .createTable('hooks', table => {
+      table.uuid('id').notNullable().primary().defaultTo(knex.raw('gen_random_uuid()'))
+      table.string('name').notNullable()
+      table.jsonb('events').notNullable().defaultTo('[]')
+      table.string('url').notNullable()
+      table.boolean('includeMetadata').notNullable().defaultTo(false)
+      table.boolean('includeContent').notNullable().defaultTo(false)
+      table.boolean('acceptUntrusted').notNullable().defaultTo(false)
+      table.string('authHeader')
+      table.enum('state', ['pending', 'error', 'success']).notNullable().defaultTo('pending')
+      table.string('lastErrorMessage')
+      table.timestamp('createdAt').notNullable().defaultTo(knex.fn.now())
+      table.timestamp('updatedAt').notNullable().defaultTo(knex.fn.now())
+    })
+    // LOCALES -----------------------------
+    .createTable('locales', table => {
+      table.string('code', 5).notNullable().primary()
+      table.jsonb('strings')
+      table.boolean('isRTL').notNullable().defaultTo(false)
+      table.string('name').notNullable()
+      table.string('nativeName').notNullable()
+      table.integer('availability').notNullable().defaultTo(0)
+      table.timestamp('createdAt').notNullable().defaultTo(knex.fn.now())
+      table.timestamp('updatedAt').notNullable().defaultTo(knex.fn.now())
+    })
+    // NAVIGATION ----------------------------
+    .createTable('navigation', table => {
+      table.string('key').notNullable().primary()
+      table.jsonb('config')
+    })
+    // PAGE HISTORY ------------------------
+    .createTable('pageHistory', table => {
+      table.uuid('id').notNullable().primary().defaultTo(knex.raw('gen_random_uuid()'))
+      table.uuid('pageId').notNullable().index()
+      table.string('path').notNullable()
+      table.string('hash').notNullable()
+      table.string('title').notNullable()
+      table.string('description')
+      table.enu('publishState', ['draft', 'published', 'scheduled']).notNullable().defaultTo('draft')
+      table.timestamp('publishStartDate')
+      table.timestamp('publishEndDate')
+      table.string('action').defaultTo('updated')
+      table.text('content')
+      table.string('contentType').notNullable()
+      table.jsonb('extra').notNullable().defaultTo('{}')
+      table.jsonb('tags').defaultTo('[]')
+      table.timestamp('versionDate').notNullable().defaultTo(knex.fn.now())
+      table.timestamp('createdAt').notNullable().defaultTo(knex.fn.now())
+    })
+    // PAGE LINKS --------------------------
+    .createTable('pageLinks', table => {
+      table.increments('id').primary()
+      table.string('path').notNullable()
+      table.string('localeCode', 5).notNullable()
+    })
+    // PAGES -------------------------------
+    .createTable('pages', table => {
+      table.uuid('id').notNullable().primary().defaultTo(knex.raw('gen_random_uuid()'))
+      table.string('slug')
+      table.string('path').notNullable()
+      table.string('hash').notNullable()
+      table.string('title').notNullable()
+      table.string('description')
+      table.enu('publishState', ['draft', 'published', 'scheduled']).notNullable().defaultTo('draft')
+      table.timestamp('publishStartDate')
+      table.timestamp('publishEndDate')
+      table.text('content')
+      table.text('render')
+      table.jsonb('toc')
+      table.string('contentType').notNullable()
+      table.jsonb('extra').notNullable().defaultTo('{}')
+      table.timestamp('createdAt').notNullable().defaultTo(knex.fn.now())
+      table.timestamp('updatedAt').notNullable().defaultTo(knex.fn.now())
+    })
+    // PAGE TREE ---------------------------
+    .createTable('pageTree', table => {
+      table.integer('id').unsigned().primary()
+      table.string('path').notNullable()
+      table.integer('depth').unsigned().notNullable()
+      table.string('title').notNullable()
+      table.boolean('isFolder').notNullable().defaultTo(false)
+      table.jsonb('ancestors')
+    })
+    // RENDERERS ---------------------------
+    .createTable('renderers', table => {
+      table.uuid('id').notNullable().primary().defaultTo(knex.raw('gen_random_uuid()'))
+      table.string('module').notNullable()
+      table.boolean('isEnabled').notNullable().defaultTo(false)
+      table.jsonb('config')
+    })
+    // SETTINGS ----------------------------
+    .createTable('settings', table => {
+      table.string('key').notNullable().primary()
+      table.jsonb('value')
+    })
+    // SITES -------------------------------
+    .createTable('sites', table => {
+      table.uuid('id').notNullable().primary().defaultTo(knex.raw('gen_random_uuid()'))
+      table.string('hostname').notNullable()
+      table.boolean('isEnabled').notNullable().defaultTo(false)
+      table.jsonb('config').notNullable()
+      table.timestamp('createdAt').notNullable().defaultTo(knex.fn.now())
+    })
+    // STORAGE -----------------------------
+    .createTable('storage', table => {
+      table.uuid('id').notNullable().primary().defaultTo(knex.raw('gen_random_uuid()'))
+      table.string('module').notNullable()
+      table.boolean('isEnabled').notNullable().defaultTo(false)
+      table.jsonb('contentTypes')
+      table.jsonb('assetDelivery')
+      table.jsonb('versioning')
+      table.jsonb('schedule')
+      table.jsonb('config')
+      table.jsonb('state')
+    })
+    // TAGS --------------------------------
+    .createTable('tags', table => {
+      table.uuid('id').notNullable().primary().defaultTo(knex.raw('gen_random_uuid()'))
+      table.string('tag').notNullable()
+      table.jsonb('display').notNullable().defaultTo('{}')
+      table.timestamp('createdAt').notNullable().defaultTo(knex.fn.now())
+      table.timestamp('updatedAt').notNullable().defaultTo(knex.fn.now())
+    })
+    // USER AVATARS ------------------------
+    .createTable('userAvatars', table => {
+      table.uuid('id').notNullable().primary()
+      table.binary('data').notNullable()
+    })
+    // USER KEYS ---------------------------
+    .createTable('userKeys', table => {
+      table.uuid('id').notNullable().primary().defaultTo(knex.raw('gen_random_uuid()'))
+      table.string('kind').notNullable()
+      table.string('token').notNullable()
+      table.timestamp('validUntil').notNullable()
+      table.timestamp('createdAt').notNullable().defaultTo(knex.fn.now())
+    })
+    // USERS -------------------------------
+    .createTable('users', table => {
+      table.uuid('id').notNullable().primary().defaultTo(knex.raw('gen_random_uuid()'))
+      table.string('email').notNullable()
+      table.string('name').notNullable()
+      table.jsonb('auth')
+      table.jsonb('tfa')
+      table.jsonb('meta')
+      table.jsonb('prefs')
+      table.string('pictureUrl')
+      table.boolean('isSystem').notNullable().defaultTo(false)
+      table.boolean('isActive').notNullable().defaultTo(false)
+      table.boolean('isVerified').notNullable().defaultTo(false)
+      table.timestamp('lastLoginAt').index()
+      table.timestamp('createdAt').notNullable().defaultTo(knex.fn.now())
+      table.timestamp('updatedAt').notNullable().defaultTo(knex.fn.now())
+    })
+    // =====================================
+    // RELATION TABLES
+    // =====================================
+    // PAGE TAGS ---------------------------
+    .createTable('pageTags', table => {
+      table.increments('id').primary()
+      table.uuid('pageId').references('id').inTable('pages').onDelete('CASCADE')
+      table.uuid('tagId').references('id').inTable('tags').onDelete('CASCADE')
+    })
+    // USER GROUPS -------------------------
+    .createTable('userGroups', table => {
+      table.increments('id').primary()
+      table.uuid('userId').references('id').inTable('users').onDelete('CASCADE')
+      table.uuid('groupId').references('id').inTable('groups').onDelete('CASCADE')
+    })
+    // =====================================
+    // REFERENCES
+    // =====================================
+    .table('analytics', table => {
+      table.uuid('siteId').notNullable().references('id').inTable('sites')
+    })
+    .table('assets', table => {
+      table.uuid('folderId').notNullable().references('id').inTable('assetFolders').index()
+      table.uuid('authorId').notNullable().references('id').inTable('users')
+      table.uuid('siteId').notNullable().references('id').inTable('sites').index()
+    })
+    .table('assetFolders', table => {
+      table.uuid('parentId').references('id').inTable('assetFolders').index()
+    })
+    .table('comments', table => {
+      table.uuid('pageId').notNullable().references('id').inTable('pages').index()
+      table.uuid('authorId').notNullable().references('id').inTable('users').index()
+    })
+    .table('navigation', table => {
+      table.uuid('siteId').notNullable().references('id').inTable('sites').index()
+    })
+    .table('pageHistory', table => {
+      table.string('localeCode', 5).references('code').inTable('locales')
+      table.uuid('authorId').notNullable().references('id').inTable('users')
+      table.uuid('siteId').notNullable().references('id').inTable('sites').index()
+    })
+    .table('pageLinks', table => {
+      table.uuid('pageId').notNullable().references('id').inTable('pages').onDelete('CASCADE')
+      table.index(['path', 'localeCode'])
+    })
+    .table('pages', table => {
+      table.string('localeCode', 5).references('code').inTable('locales').index()
+      table.uuid('authorId').notNullable().references('id').inTable('users').index()
+      table.uuid('creatorId').notNullable().references('id').inTable('users').index()
+      table.uuid('siteId').notNullable().references('id').inTable('sites').index()
+    })
+    .table('pageTree', table => {
+      table.integer('parent').unsigned().references('id').inTable('pageTree').onDelete('CASCADE')
+      table.uuid('pageId').notNullable().references('id').inTable('pages').onDelete('CASCADE')
+      table.string('localeCode', 5).references('code').inTable('locales')
+    })
+    .table('storage', table => {
+      table.uuid('siteId').notNullable().references('id').inTable('sites')
+    })
+    .table('tags', table => {
+      table.uuid('siteId').notNullable().references('id').inTable('sites')
+      table.unique(['siteId', 'tag'])
+    })
+    .table('userKeys', table => {
+      table.uuid('userId').notNullable().references('id').inTable('users')
+    })
+    .table('users', table => {
+      table.string('localeCode', 5).references('code').inTable('locales').notNullable().defaultTo('en')
+    })
+
+  // =====================================
+  // DEFAULT DATA
+  // =====================================
+
+  // -> SYSTEM CONFIG
+
+  await knex('settings').insert([
+    {
+      key: 'update',
+      value: {
+        locales: true
+      }
+    },
+    {
+      key: 'mail',
+      value: {
+        senderName: '',
+        senderEmail: '',
+        host: '',
+        port: 465,
+        secure: true,
+        verifySSL: true,
+        user: '',
+        pass: '',
+        useDKIM: false,
+        dkimDomainName: '',
+        dkimKeySelector: '',
+        dkimPrivateKey: ''
+      }
+    },
+    {
+      key: 'security',
+      value: {
+        corsConfig: '',
+        corsMode: 'OFF',
+        cspDirectives: '',
+        disallowFloc: true,
+        disallowIframe: true,
+        disallowOpenRedirect: true,
+        enforceCsp: false,
+        enforceHsts: false,
+        enforceSameOriginReferrerPolicy: true,
+        forceAssetDownload: true,
+        hstsDuration: 0,
+        trustProxy: false,
+        authJwtAudience: 'urn:wiki.js',
+        authJwtExpiration: '30m',
+        authJwtRenewablePeriod: '14d',
+        uploadMaxFileSize: 10485760,
+        uploadMaxFiles: 20,
+        uploadScanSVG: true
+      }
+    }
+  ])
+
+  // -> DEFAULT LOCALE
+
+  await knex('locales').insert({
+    code: 'en',
+    strings: {},
+    isRTL: false,
+    name: 'English',
+    nativeName: 'English'
+  })
+
+  // -> DEFAULT SITE
+
+  WIKI.logger.info('Generating certificates...')
+  const secret = crypto.randomBytes(32).toString('hex')
+  const certs = crypto.generateKeyPairSync('rsa', {
+    modulusLength: 2048,
+    publicKeyEncoding: {
+      type: 'pkcs1',
+      format: 'pem'
+    },
+    privateKeyEncoding: {
+      type: 'pkcs1',
+      format: 'pem',
+      cipher: 'aes-256-cbc',
+      passphrase: secret
+    }
+  })
+
+  const siteId = uuid()
+  await knex('sites').insert({
+    id: siteId,
+    hostname: '*',
+    isEnabled: true,
+    config: {
+      auth: {
+        audience: 'urn:wiki.js',
+        tokenExpiration: '30m',
+        tokenRenewal: '14d',
+        certs: {
+          jwk: pem2jwk(certs.publicKey),
+          public: certs.publicKey,
+          private: certs.privateKey
+        },
+        secret
+      },
+      title: 'My Wiki Site',
+      description: '',
+      company: '',
+      contentLicense: '',
+      defaults: {
+        timezone: 'America/New_York',
+        dateFormat: 'YYYY-MM-DD',
+        timeFormat: '12h'
+      },
+      features: {
+        ratings: false,
+        ratingsMode: 'off',
+        comments: false,
+        contributions: false,
+        profile: true,
+        search: true
+      },
+      logoText: true,
+      robots: {
+        index: true,
+        follow: true
+      },
+      locale: 'en',
+      localeNamespacing: false,
+      localeNamespaces: [],
+      theme: {
+        dark: false,
+        colorPrimary: '#1976d2',
+        colorSecondary: '#02c39a',
+        colorAccent: '#f03a47',
+        colorHeader: '#000000',
+        colorSidebar: '#1976d2',
+        injectCSS: '',
+        injectHead: '',
+        injectBody: '',
+        sidebarPosition: 'left',
+        tocPosition: 'right',
+        showSharingMenu: true,
+        showPrintBtn: true
+      }
+    }
+  })
+
+  // -> DEFAULT GROUPS
+
+  const groupAdminId = uuid()
+  const groupGuestId = '10000000-0000-4000-0000-000000000001'
+  await knex('groups').insert([
+    {
+      id: groupAdminId,
+      name: 'Administrators',
+      permissions: JSON.stringify(['manage:system']),
+      rules: JSON.stringify([]),
+      isSystem: true
+    },
+    {
+      id: groupGuestId,
+      name: 'Guests',
+      permissions: JSON.stringify(['read:pages', 'read:assets', 'read:comments']),
+      rules: JSON.stringify([
+        {
+          id: uuid(),
+          name: 'Default Rule',
+          roles: ['read:pages', 'read:assets', 'read:comments'],
+          match: 'START',
+          mode: 'DENY',
+          path: '',
+          locales: [],
+          sites: []
+        }
+      ]),
+      isSystem: true
+    }
+  ])
+
+  // -> AUTHENTICATION MODULE
+
+  const authModuleId = uuid()
+  await knex('authentication').insert({
+    id: authModuleId,
+    module: 'local',
+    isEnabled: true,
+    displayName: 'Local Authentication'
+  })
+
+  // -> USERS
+
+  const userAdminId = uuid()
+  const userGuestId = uuid()
+  await knex('users').insert([
+    {
+      id: userAdminId,
+      email: process.env.ADMIN_EMAIL ?? 'admin@example.com',
+      auth: {
+        [authModuleId]: {
+          password: await bcrypt.hash(process.env.ADMIN_PASS || '12345678', 12),
+          mustChangePwd: !process.env.ADMIN_PASS,
+          restrictLogin: false,
+          tfaRequired: false,
+          tfaSecret: ''
+        }
+      },
+      name: 'Administrator',
+      isSystem: false,
+      isActive: true,
+      isVerified: true,
+      meta: {
+        location: '',
+        jobTitle: '',
+        pronouns: ''
+      },
+      prefs: {
+        timezone: 'America/New_York',
+        dateFormat: 'YYYY-MM-DD',
+        timeFormat: '12h',
+        darkMode: false
+      },
+      localeCode: 'en'
+    },
+    {
+      id: userGuestId,
+      email: 'guest@example.com',
+      name: 'Guest',
+      isSystem: true,
+      isActive: true,
+      isVerified: true,
+      localeCode: 'en'
+    }
+  ])
+
+  await knex('userGroups').insert([
+    {
+      userId: userAdminId,
+      groupId: groupAdminId
+    },
+    {
+      userId: userGuestId,
+      groupId: groupGuestId
+    }
+  ])
+
+  // -> STORAGE MODULE
+
+  await knex('storage').insert({
+    module: 'db',
+    siteId,
+    isEnabled: true,
+    contentTypes: {
+      activeTypes: ['pages', 'images', 'documents', 'others', 'large'],
+      largeThreshold: '5MB'
+    },
+    assetDelivery: {
+      streaming: true,
+      directAccess: false
+    },
+    versioning: {
+      enabled: false
+    },
+    state: {
+      current: 'ok'
+    }
+  })
+
+  WIKI.logger.info('Completed 3.0.0 database migration.')
+}
+
+exports.down = knex => { }

+ 1 - 1
server/db/migrator-source.js

@@ -2,7 +2,7 @@ const path = require('path')
 const fs = require('fs-extra')
 const semver = require('semver')
 
-const baseMigrationPath = path.join(WIKI.SERVERPATH, (WIKI.config.db.type !== 'sqlite') ? 'db/migrations' : 'db/migrations-sqlite')
+const baseMigrationPath = path.join(WIKI.SERVERPATH, 'db/migrations')
 
 /* global WIKI */
 

+ 172 - 0
server/graph/resolvers/site.js

@@ -1,13 +1,185 @@
 const graphHelper = require('../../helpers/graph')
 const _ = require('lodash')
+const CleanCSS = require('clean-css')
+const path = require('path')
 
 /* global WIKI */
 
 module.exports = {
   Query: {
+    async sites () {
+      const sites = await WIKI.models.sites.query()
+      return sites.map(s => ({
+        ...s.config,
+        id: s.id,
+        hostname: s.hostname,
+        isEnabled: s.isEnabled
+      }))
+    },
+    async siteById (obj, args) {
+      const site = await WIKI.models.sites.query().findById(args.id)
+      return site ? {
+        ...site.config,
+        id: site.id,
+        hostname: site.hostname,
+        isEnabled: site.isEnabled
+      } : null
+    },
+    async siteByHostname (obj, args) {
+      let site = await WIKI.models.sites.query().where({
+        hostname: args.hostname
+      }).first()
+      if (!site && !args.exact) {
+        site = await WIKI.models.sites.query().where({
+          hostname: '*'
+        }).first()
+      }
+      return site ? {
+        ...site.config,
+        id: site.id,
+        hostname: site.hostname,
+        isEnabled: site.isEnabled
+      } : null
+    },
+    // LEGACY
     async site() { return {} }
   },
   Mutation: {
+    /**
+     * CREATE SITE
+     */
+    async createSite (obj, args) {
+      try {
+        // -> Validate inputs
+        if (!args.hostname || args.hostname.length < 1 || !/^(\\*)|([a-z0-9\-.:]+)$/.test(args.hostname)) {
+          throw WIKI.ERROR(new Error('Invalid Site Hostname'), 'SiteCreateInvalidHostname')
+        }
+        if (!args.title || args.title.length < 1 || !/^[^<>"]+$/.test(args.title)) {
+          throw WIKI.ERROR(new Error('Invalid Site Title'), 'SiteCreateInvalidTitle')
+        }
+        // -> Check for duplicate catch-all
+        if (args.hostname === '*') {
+          const site = await WIKI.models.sites.query().where({
+            hostname: args.hostname
+          }).first()
+          if (site) {
+            throw WIKI.ERROR(new Error('A site with a catch-all hostname already exists! Cannot have 2 catch-all hostnames.'), 'SiteCreateDuplicateCatchAll')
+          }
+        }
+        // -> Create site
+        const newSite = await WIKI.models.sites.createSite(args.hostname, {
+          title: args.title
+        })
+        return {
+          status: graphHelper.generateSuccess('Site created successfully'),
+          site: newSite
+        }
+      } catch (err) {
+        return graphHelper.generateError(err)
+      }
+    },
+    /**
+     * UPDATE SITE
+     */
+    async updateSite (obj, args) {
+      try {
+        // -> Load site
+        const site = await WIKI.models.sites.query().findById(args.id)
+        if (!site) {
+          throw WIKI.ERROR(new Error('Invalid Site ID'), 'SiteInvalidId')
+        }
+        // -> Check for bad input
+        if (_.has(args.patch, 'hostname') && _.trim(args.patch.hostname).length < 1) {
+          throw WIKI.ERROR(new Error('Hostname is invalid.'), 'SiteInvalidHostname')
+        }
+        // -> Check for duplicate catch-all
+        if (args.patch.hostname === '*' && site.hostname !== '*') {
+          const dupSite = await WIKI.models.sites.query().where({ hostname: '*' }).first()
+          if (dupSite) {
+            throw WIKI.ERROR(new Error(`Site ${dupSite.config.title} with a catch-all hostname already exists! Cannot have 2 catch-all hostnames.`), 'SiteUpdateDuplicateCatchAll')
+          }
+        }
+        // -> Format Code
+        if (args.patch?.theme?.injectCSS) {
+          args.patch.theme.injectCSS = new CleanCSS({ inline: false }).minify(args.patch.theme.injectCSS).styles
+        }
+        // -> Update site
+        await WIKI.models.sites.updateSite(args.id, {
+          hostname: args.patch.hostname ?? site.hostname,
+          isEnabled: args.patch.isEnabled ?? site.isEnabled,
+          config: _.defaultsDeep(_.omit(args.patch, ['hostname', 'isEnabled']), site.config)
+        })
+
+        return {
+          status: graphHelper.generateSuccess('Site updated successfully')
+        }
+      } catch (err) {
+        WIKI.logger.warn(err)
+        return graphHelper.generateError(err)
+      }
+    },
+    /**
+     * DELETE SITE
+     */
+    async deleteSite (obj, args) {
+      try {
+        // -> Ensure site isn't last one
+        const sitesCount = await WIKI.models.sites.query().count('id').first()
+        if (sitesCount?.count && _.toNumber(sitesCount?.count) <= 1) {
+          throw WIKI.ERROR(new Error('Cannot delete the last site. At least 1 site must exists at all times.'), 'SiteDeleteLastSite')
+        }
+        // -> Delete site
+        await WIKI.models.sites.deleteSite(args.id)
+        return {
+          status: graphHelper.generateSuccess('Site deleted successfully')
+        }
+      } catch (err) {
+        WIKI.logger.warn(err)
+        return graphHelper.generateError(err)
+      }
+    },
+    /**
+     * UPLOAD LOGO
+     */
+    async uploadSiteLogo (obj, args) {
+      try {
+        const { filename, mimetype, createReadStream } = await args.image
+        WIKI.logger.info(`Processing site logo ${filename} of type ${mimetype}...`)
+        if (!WIKI.extensions.ext.sharp.isInstalled) {
+          throw new Error('This feature requires the Sharp extension but it is not installed.')
+        }
+        console.info(mimetype)
+        const destFormat = mimetype.startsWith('image/svg') ? 'svg' : 'png'
+        const destPath = path.resolve(
+          process.cwd(),
+          WIKI.config.dataPath,
+          `assets/logo.${destFormat}`
+        )
+        await WIKI.extensions.ext.sharp.resize({
+          format: destFormat,
+          inputStream: createReadStream(),
+          outputPath: destPath,
+          width: 100
+        })
+        WIKI.logger.info('New site logo processed successfully.')
+        return {
+          status: graphHelper.generateSuccess('Site logo uploaded successfully')
+        }
+      } catch (err) {
+        return graphHelper.generateError(err)
+      }
+    },
+    /**
+     * UPLOAD FAVICON
+     */
+    async uploadSiteFavicon (obj, args) {
+      const { filename, mimetype, createReadStream } = await args.image
+      console.info(filename, mimetype)
+      return {
+        status: graphHelper.generateSuccess('Site favicon uploaded successfully')
+      }
+    },
+    // LEGACY
     async site() { return {} }
   },
   SiteQuery: {

+ 59 - 0
server/graph/scalars/json.js

@@ -0,0 +1,59 @@
+const { Kind, GraphQLScalarType } = require('graphql')
+
+function ensureObject (value) {
+  if (typeof value !== 'object' || value === null || Array.isArray(value)) {
+    throw new TypeError(`JSONObject cannot represent non-object value: ${value}`)
+  }
+
+  return value
+}
+
+function parseLiteral (typeName, ast, variables) {
+  switch (ast.kind) {
+    case Kind.STRING:
+    case Kind.BOOLEAN:
+      return ast.value
+    case Kind.INT:
+    case Kind.FLOAT:
+      return parseFloat(ast.value)
+    case Kind.OBJECT:
+      return parseObject(typeName, ast, variables)
+    case Kind.LIST:
+      return ast.values.map((n) => parseLiteral(typeName, n, variables))
+    case Kind.NULL:
+      return null
+    case Kind.VARIABLE:
+      return variables ? variables[ast.name.value] : undefined
+    default:
+      throw new TypeError(`${typeName} cannot represent value: ${ast}`)
+  }
+}
+
+function parseObject (typeName, ast, variables) {
+  const value = Object.create(null)
+  ast.fields.forEach((field) => {
+    // eslint-disable-next-line no-use-before-define
+    value[field.name.value] = parseLiteral(typeName, field.value, variables)
+  })
+
+  return value
+}
+
+module.exports = {
+  JSON: new GraphQLScalarType({
+    name: 'JSON',
+    description:
+      'The `JSON` scalar type represents JSON objects as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf).',
+    specifiedByUrl:
+      'http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf',
+    serialize: ensureObject,
+    parseValue: ensureObject,
+    parseLiteral: (ast, variables) => {
+      if (ast.kind !== Kind.OBJECT) {
+        throw new TypeError(`JSONObject cannot represent non-object value: ${ast}`)
+      }
+
+      return parseObject('JSONObject', ast, variables)
+    }
+  })
+}

+ 39 - 0
server/graph/scalars/uuid.js

@@ -0,0 +1,39 @@
+const { Kind, GraphQLScalarType } = require('graphql')
+// const { Kind } = require('graphql/language')
+
+const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
+const nilUUID = '00000000-0000-0000-0000-000000000000'
+
+function isUUID (value) {
+  return uuidRegex.test(value) || nilUUID === value
+}
+
+module.exports = {
+  UUID: new GraphQLScalarType({
+    name: 'UUID',
+    description: 'The `UUID` scalar type represents UUID values as specified by [RFC 4122](https://tools.ietf.org/html/rfc4122).',
+    serialize: (value) => {
+      if (!isUUID(value)) {
+        throw new TypeError(`UUID cannot represent non-UUID value: ${value}`)
+      }
+
+      return value.toLowerCase()
+    },
+    parseValue: (value) => {
+      if (!isUUID(value)) {
+        throw new TypeError(`UUID cannot represent non-UUID value: ${value}`)
+      }
+
+      return value.toLowerCase()
+    },
+    parseLiteral: (ast) => {
+      if (ast.kind === Kind.STRING) {
+        if (isUUID(ast.value)) {
+          return ast.value
+        }
+      }
+
+      return undefined
+    }
+  })
+}

+ 5 - 0
server/graph/schemas/common.graphql

@@ -34,6 +34,11 @@ type ResponseStatus {
   message: String
 }
 
+enum OrderByDirection {
+  asc
+  desc
+}
+
 # ROOT
 # ----
 

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

@@ -1,3 +1,6 @@
 # SCALARS
 
 scalar Date
+scalar JSON
+# scalar Upload
+scalar UUID

+ 167 - 1
server/graph/schemas/site.graphql

@@ -1,12 +1,49 @@
 # ===============================================
-# SITE
+# SITES
 # ===============================================
 
 extend type Query {
+  sites: [Site] @auth(requires: ["manage:system"])
+
+  siteById (
+    id: UUID!
+  ): Site @auth(requires: ["manage:system"])
+
+  siteByHostname (
+    hostname: String!
+    exact: Boolean!
+  ): Site @auth(requires: ["manage:system"])
+
+  # Legacy
   site: SiteQuery
 }
 
 extend type Mutation {
+  createSite (
+    hostname: String!
+    title: String!
+  ): SiteCreateResponse @auth(requires: ["manage:system"])
+
+  updateSite (
+    id: UUID!
+    patch: SiteUpdateInput!
+  ): DefaultResponse @auth(requires: ["manage:system"])
+
+  uploadSiteLogo (
+    id: UUID!
+    image: Upload!
+  ): DefaultResponse @auth(requires: ["manage:system"])
+
+  uploadSiteFavicon (
+    id: UUID!
+    image: Upload!
+  ): DefaultResponse @auth(requires: ["manage:system"])
+
+  deleteSite (
+    id: UUID!
+  ): DefaultResponse @auth(requires: ["manage:system"])
+
+  # Legacy
   site: SiteMutation
 }
 
@@ -64,6 +101,135 @@ type SiteMutation {
 # TYPES
 # -----------------------------------------------
 
+type Site {
+  id: UUID
+  hostname: String
+  isEnabled: Boolean
+  title: String
+  description: String
+  company: String
+  contentLicense: String
+  logoText: Boolean
+  robots: SiteRobots
+  features: SiteFeatures
+  defaults: SiteDefaults
+  locale: String
+  localeNamespaces: [String]
+  localeNamespacing: Boolean
+  theme: SiteTheme
+}
+
+type SiteRobots {
+  index: Boolean
+  follow: Boolean
+}
+
+type SiteFeatures {
+  ratings: Boolean
+  ratingsMode: SitePageRatingModes
+  comments: Boolean
+  contributions: Boolean
+  profile: Boolean
+  search: Boolean
+}
+
+type SiteDefaults {
+  timezone: String
+  dateFormat: String
+  timeFormat: String
+}
+
+type SiteLocale {
+  locale: String
+  autoUpdate: Boolean
+  namespacing: Boolean
+  namespaces: [String]
+}
+
+type SiteTheme {
+  dark: Boolean
+  colorPrimary: String
+  colorSecondary: String
+  colorAccent: String
+  colorHeader: String
+  colorSidebar: String
+  injectCSS: String
+  injectHead: String
+  injectBody: String
+  sidebarPosition: SiteThemePosition
+  tocPosition: SiteThemePosition
+  showSharingMenu: Boolean
+  showPrintBtn: Boolean
+}
+
+enum SiteThemePosition {
+  left
+  right
+}
+
+enum SitePageRatingModes {
+  off
+  thumbs
+  stars
+}
+
+type SiteCreateResponse {
+  status: ResponseStatus
+  site: Site
+}
+
+input SiteUpdateInput {
+  hostname: String
+  isEnabled: Boolean
+  title: String
+  description: String
+  company: String
+  contentLicense: String
+  logoText: Boolean
+  robots: SiteRobotsInput
+  features: SiteFeaturesInput
+  defaults: SiteDefaultsInput
+  theme: SiteThemeInput
+}
+
+input SiteRobotsInput {
+  index: Boolean
+  follow: Boolean
+}
+
+input SiteFeaturesInput {
+  ratings: Boolean
+  ratingsMode: SitePageRatingModes
+  comments: Boolean
+  contributions: Boolean
+  profile: Boolean
+  search: Boolean
+}
+
+input SiteDefaultsInput {
+  timezone: String
+  dateFormat: String
+  timeFormat: String
+}
+
+input SiteThemeInput {
+  dark: Boolean
+  colorPrimary: String
+  colorSecondary: String
+  colorAccent: String
+  colorHeader: String
+  colorSidebar: String
+  injectCSS: String
+  injectHead: String
+  injectBody: String
+  sidebarPosition: SiteThemePosition
+  tocPosition: SiteThemePosition
+  showSharingMenu: Boolean
+  showPrintBtn: Boolean
+}
+
+# LEGACY
+
 type SiteConfig {
   host: String
   title: String

+ 9 - 0
ux/.editorconfig

@@ -0,0 +1,9 @@
+root = true
+
+[*]
+charset = utf-8
+indent_style = space
+indent_size = 2
+end_of_line = lf
+insert_final_newline = true
+trim_trailing_whitespace = true

+ 6 - 0
ux/.eslintignore

@@ -0,0 +1,6 @@
+/dist
+/src-capacitor
+/src-cordova
+/.quasar
+/node_modules
+.eslintrc.js

+ 75 - 0
ux/.eslintrc.js

@@ -0,0 +1,75 @@
+module.exports = {
+  // https://eslint.org/docs/user-guide/configuring#configuration-cascading-and-hierarchy
+  // This option interrupts the configuration hierarchy at this file
+  // Remove this if you have an higher level ESLint config file (it usually happens into a monorepos)
+  root: true,
+
+  parserOptions: {
+    ecmaVersion: '2021' // Allows for the parsing of modern ECMAScript features
+  },
+
+  env: {
+    node: true,
+    browser: true,
+    'vue/setup-compiler-macros': true
+  },
+
+  // Rules order is important, please avoid shuffling them
+  extends: [
+    // Base ESLint recommended rules
+    // 'eslint:recommended',
+
+    // Uncomment any of the lines below to choose desired strictness,
+    // but leave only one uncommented!
+    // See https://eslint.vuejs.org/rules/#available-rules
+    'plugin:vue/vue3-essential', // Priority A: Essential (Error Prevention)
+    'plugin:vue/vue3-strongly-recommended', // Priority B: Strongly Recommended (Improving Readability)
+    // 'plugin:vue/vue3-recommended', // Priority C: Recommended (Minimizing Arbitrary Choices and Cognitive Overhead)
+
+    'standard'
+  ],
+
+  plugins: [
+    // https://eslint.vuejs.org/user-guide/#why-doesn-t-it-work-on-vue-files
+    // required to lint *.vue files
+    'vue'
+  ],
+
+  globals: {
+    ga: 'readonly', // Google Analytics
+    __statics: 'readonly',
+    __QUASAR_SSR__: 'readonly',
+    __QUASAR_SSR_SERVER__: 'readonly',
+    __QUASAR_SSR_CLIENT__: 'readonly',
+    __QUASAR_SSR_PWA__: 'readonly',
+    process: 'readonly',
+    APOLLO_CLIENT: 'readonly'
+  },
+
+  // add your custom rules here
+  rules: {
+    // allow async-await
+    'generator-star-spacing': 'off',
+    // allow paren-less arrow functions
+    'arrow-parens': 'off',
+    'one-var': 'off',
+    'no-void': 'off',
+    'multiline-ternary': 'off',
+
+    'import/first': 'off',
+    'import/named': 'error',
+    'import/namespace': 'error',
+    'import/default': 'error',
+    'import/export': 'error',
+    'import/extensions': 'off',
+    'import/no-unresolved': 'off',
+    'import/no-extraneous-dependencies': 'off',
+
+    'prefer-promise-reject-errors': 'off',
+
+    'no-unused-vars': 'off',
+
+    // allow debugger during development only
+    'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off'
+  }
+}

+ 29 - 0
ux/.gitignore

@@ -0,0 +1,29 @@
+.DS_Store
+.thumbs.db
+node_modules
+
+# Quasar core related directories
+.quasar
+/dist
+
+# Cordova related directories and files
+/src-cordova/node_modules
+/src-cordova/platforms
+/src-cordova/plugins
+/src-cordova/www
+
+# Capacitor related directories and files
+/src-capacitor/www
+/src-capacitor/node_modules
+
+# Log files
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# Editor directories and files
+.idea
+*.suo
+*.ntvs*
+*.njsproj
+*.sln

+ 14 - 0
ux/.vscode/extensions.json

@@ -0,0 +1,14 @@
+{
+  "recommendations": [
+    "dbaeumer.vscode-eslint",
+    "editorconfig.editorconfig",
+    "johnsoncodehk.volar",
+    "wayou.vscode-todo-highlight"
+  ],
+  "unwantedRecommendations": [
+    "octref.vetur",
+    "hookyqr.beautify",
+    "dbaeumer.jshint",
+    "ms-vscode.vscode-typescript-tslint-plugin"
+  ]
+}

+ 16 - 0
ux/.vscode/settings.json

@@ -0,0 +1,16 @@
+{
+  "editor.bracketPairColorization.enabled": true,
+  "editor.guides.bracketPairs": true,
+  "editor.formatOnSave": true,
+  "editor.defaultFormatter": "dbaeumer.vscode-eslint",
+  "editor.codeActionsOnSave": [
+    "source.fixAll.eslint"
+  ],
+  "eslint.validate": [
+    "javascript",
+    "javascriptreact",
+    "typescript",
+    "vue"
+  ],
+  "i18n-ally.localesPaths": "src/i18n/locales"
+}

+ 33 - 0
ux/README.md

@@ -0,0 +1,33 @@
+# Wiki.js (ux)
+
+The most powerful and extensible open source Wiki software
+
+## Install the dependencies
+```bash
+yarn
+# or
+npm install
+```
+
+### Start the app in development mode (hot-code reloading, error reporting, etc.)
+```bash
+quasar dev
+```
+
+
+### Lint the files
+```bash
+yarn lint
+# or
+npm run lint
+```
+
+
+
+### Build the app for production
+```bash
+quasar build
+```
+
+### Customize the configuration
+See [Configuring quasar.config.js](https://v2.quasar.dev/quasar-cli-vite/quasar-config-js).

+ 14 - 0
ux/apollo.config.js

@@ -0,0 +1,14 @@
+module.exports = {
+  client: {
+    service: {
+      name: 'wiki-core',
+      // URL to the GraphQL API
+      url: 'http://localhost:11511'
+    },
+    // Files processed by the extension
+    includes: [
+      'src/**/*.vue',
+      'src/**/*.js'
+    ]
+  }
+}

+ 74 - 0
ux/index.html

@@ -0,0 +1,74 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <link rel="icon" href="/favicon.ico" />
+    <meta name="format-detection" content="telephone=no">
+    <meta name="msapplication-tap-highlight" content="no">
+    <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-->
+    <style type="text/css">
+      @keyframes initspinner {
+        to { transform: rotate(360deg); }
+      }
+
+      .init-loading {
+        position: fixed;
+        top: 0;
+        left: 0;
+        width: 100%;
+        height: 100%;
+        background-color: rgba(255,255,255,.75);
+        z-index: 2000000000;
+        backdrop-filter: blur(10px);
+      }
+
+      .body--dark .init-loading {
+        background-color: rgba(0,0,0,.75);
+      }
+
+      .init-loading:before {
+        content: '';
+        box-sizing: border-box;
+        position: absolute;
+        top: 50%;
+        left: 50%;
+        width: 50px;
+        height: 50px;
+        margin-top: -25px;
+        margin-left: -25px;
+        border-radius: 50%;
+        border-top: 2px solid #1976D2;
+        border-right: 2px solid transparent;
+        animation: initspinner .6s linear infinite;
+        z-index: 2000000000;
+      }
+    </style>
+
+    <noscript>
+      <style type="text/css">
+        .init-loading {
+          display: none !important;
+        }
+
+        .q-drawer-container {
+          display: none !important;
+        }
+
+        .scroll.relative-position {
+          position: static !important;
+        }
+
+        .scroll.relative-position > .absolute {
+          position: relative !important;
+        }
+      </style>
+    </noscript>
+  </head>
+  <body>
+    <div class="init-loading"></div>
+    <div id="app"><!-- quasar:entry-point --></div>
+    <script type="module" src="/entry-client.js"></script>
+  </body>
+</html>

+ 39 - 0
ux/jsconfig.json

@@ -0,0 +1,39 @@
+{
+  "compilerOptions": {
+    "baseUrl": ".",
+    "paths": {
+      "src/*": [
+        "src/*"
+      ],
+      "app/*": [
+        "*"
+      ],
+      "components/*": [
+        "src/components/*"
+      ],
+      "layouts/*": [
+        "src/layouts/*"
+      ],
+      "pages/*": [
+        "src/pages/*"
+      ],
+      "assets/*": [
+        "src/assets/*"
+      ],
+      "boot/*": [
+        "src/boot/*"
+      ],
+      "stores/*": [
+        "src/stores/*"
+      ],
+      "vue$": [
+        "node_modules/vue/dist/vue.runtime.esm-bundler.js"
+      ]
+    }
+  },
+  "exclude": [
+    "dist",
+    ".quasar",
+    "node_modules"
+  ]
+}

+ 102 - 0
ux/package.json

@@ -0,0 +1,102 @@
+{
+  "name": "ux",
+  "version": "0.0.1",
+  "description": "The most powerful and extensible open source Wiki software",
+  "productName": "Wiki.js",
+  "author": "Nicolas Giard <nick@requarks.io>",
+  "private": true,
+  "scripts": {
+    "dev": "quasar dev",
+    "build": "quasar build",
+    "lint": "eslint --ext .js,.vue ./"
+  },
+  "dependencies": {
+    "@apollo/client": "3.5.10",
+    "@codemirror/autocomplete": "0.19.15",
+    "@codemirror/basic-setup": "0.19.1",
+    "@codemirror/closebrackets": "0.19.1",
+    "@codemirror/commands": "0.19.8",
+    "@codemirror/comment": "0.19.1",
+    "@codemirror/fold": "0.19.3",
+    "@codemirror/gutter": "0.19.9",
+    "@codemirror/highlight": "0.19.8",
+    "@codemirror/history": "0.19.2",
+    "@codemirror/lang-css": "0.19.3",
+    "@codemirror/lang-html": "0.19.4",
+    "@codemirror/lang-javascript": "0.19.7",
+    "@codemirror/lang-json": "0.19.2",
+    "@codemirror/lang-markdown": "0.19.6",
+    "@codemirror/matchbrackets": "0.19.4",
+    "@codemirror/search": "0.19.9",
+    "@codemirror/state": "0.19.9",
+    "@codemirror/tooltip": "0.19.16",
+    "@codemirror/view": "0.19.47",
+    "@lezer/common": "0.15.12",
+    "@quasar/extras": "1.13.5",
+    "@tiptap/core": "2.0.0-beta.174",
+    "@tiptap/extension-code-block": "2.0.0-beta.37",
+    "@tiptap/extension-code-block-lowlight": "2.0.0-beta.68",
+    "@tiptap/extension-color": "2.0.0-beta.9",
+    "@tiptap/extension-dropcursor": "2.0.0-beta.25",
+    "@tiptap/extension-font-family": "2.0.0-beta.21",
+    "@tiptap/extension-gapcursor": "2.0.0-beta.34",
+    "@tiptap/extension-hard-break": "2.0.0-beta.30",
+    "@tiptap/extension-highlight": "2.0.0-beta.33",
+    "@tiptap/extension-history": "2.0.0-beta.21",
+    "@tiptap/extension-image": "2.0.0-beta.27",
+    "@tiptap/extension-mention": "2.0.0-beta.95",
+    "@tiptap/extension-placeholder": "2.0.0-beta.48",
+    "@tiptap/extension-table": "2.0.0-beta.48",
+    "@tiptap/extension-table-cell": "2.0.0-beta.20",
+    "@tiptap/extension-table-header": "2.0.0-beta.22",
+    "@tiptap/extension-table-row": "2.0.0-beta.19",
+    "@tiptap/extension-task-item": "2.0.0-beta.31",
+    "@tiptap/extension-task-list": "2.0.0-beta.26",
+    "@tiptap/extension-text-align": "2.0.0-beta.29",
+    "@tiptap/extension-text-style": "2.0.0-beta.23",
+    "@tiptap/extension-typography": "2.0.0-beta.20",
+    "@tiptap/starter-kit": "2.0.0-beta.183",
+    "@tiptap/vue-3": "2.0.0-beta.90",
+    "@vue/apollo-option": "4.0.0-alpha.16",
+    "apollo-upload-client": "17.0.0",
+    "browser-fs-access": "0.26.1",
+    "clipboard": "2.0.10",
+    "filesize": "8.0.7",
+    "filesize-parser": "1.5.0",
+    "graphql": "16.3.0",
+    "graphql-tag": "2.12.6",
+    "js-cookie": "3.0.1",
+    "jwt-decode": "3.1.2",
+    "lodash": "4.17.21",
+    "luxon": "2.3.1",
+    "pinia": "2.0.13",
+    "pug": "3.0.2",
+    "quasar": "2.6.5",
+    "tippy.js": "6.3.7",
+    "uuid": "8.3.2",
+    "v-network-graph": "0.5.9",
+    "vue": "3.2.31",
+    "vue-i18n": "9.1.9",
+    "vue-router": "4.0.14",
+    "vuedraggable": "4.1.0",
+    "zxcvbn": "4.4.2"
+  },
+  "devDependencies": {
+    "@intlify/vite-plugin-vue-i18n": "3.4.0",
+    "@quasar/app-vite": "1.0.0-beta.13",
+    "@types/lodash": "4.14.181",
+    "autoprefixer": "10.4.4",
+    "eslint": "8.12.0",
+    "eslint-config-standard": "17.0.0-1",
+    "eslint-plugin-import": "2.26.0",
+    "eslint-plugin-n": "15.1.0",
+    "eslint-plugin-promise": "6.0.0",
+    "eslint-plugin-vue": "8.6.0"
+  },
+  "engines": {
+    "node": "^18 || ^16",
+    "npm": ">= 6.13.4",
+    "yarn": ">= 1.21.1"
+  },
+  "eslint.packageManager": "yarn"
+}

+ 27 - 0
ux/postcss.config.js

@@ -0,0 +1,27 @@
+/* eslint-disable */
+// https://github.com/michael-ciniawsky/postcss-load-config
+
+module.exports = {
+  plugins: [
+    // https://github.com/postcss/autoprefixer
+    require('autoprefixer')({
+      overrideBrowserslist: [
+        'last 4 Chrome versions',
+        'last 4 Firefox versions',
+        'last 4 Edge versions',
+        'last 4 Safari versions',
+        'last 4 Android versions',
+        'last 4 ChromeAndroid versions',
+        'last 4 FirefoxAndroid versions',
+        'last 4 iOS versions'
+      ]
+    })
+
+    // https://github.com/elchininet/postcss-rtlcss
+    // If you want to support RTL css, then
+    // 1. yarn/npm install postcss-rtlcss
+    // 2. optionally set quasar.config.js > framework > lang to an RTL language
+    // 3. uncomment the following line:
+    // require('postcss-rtlcss')
+  ]
+}

BIN=BIN
ux/public/_assets/bg/login-v3.jpg


BIN=BIN
ux/public/_assets/bg/login.jpg


BIN=BIN
ux/public/_assets/bg/test-1.jpg


BIN=BIN
ux/public/_assets/bg/test-2.jpg


+ 1 - 0
ux/public/_assets/icons/color-blog.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg"  viewBox="0 0 48 48" width="96px" height="96px"><path fill="#546E7A" d="M5 22H7V26H5zM41 22H43V26H41z"/><path fill="#FFC107" d="M40,19H8v20c0,2.209,1.791,4,4,4h24c2.209,0,4-1.791,4-4V19z"/><path fill="#FFECB3" d="M36,31H12v-2c0,0,0-1,1-1s21,0,22,0s1,1,1,1V31z"/><path fill="#37474F" d="M10,19v1c0,1.104,0.896,2,2,2h24c1.104,0,2-0.896,2-2v-1H10z"/><path fill="#37474F" d="M28.619 19h-9.238C17.924 19.733 17 20.804 17 22c0 2.209 3.134 4 7 4s7-1.791 7-4C31 20.804 30.076 19.733 28.619 19zM36 38c0 1.104-.896 2-2 2H14c-1.104 0-2-.896-2-2v-7h24V38zM7 23H8V25H7zM40 23H41V25H40z"/><g><path fill="#90CAF9" d="M12 5H36V20H12z"/><path fill="#90CAF9" d="M24 18.57A3 1.715 0 1 0 24 22A3 1.715 0 1 0 24 18.57Z"/></g><g><path fill="#1976D2" d="M16 9H32V11H16zM16 13H28V15H16z"/></g><g><path fill="#B0BEC5" d="M15 33A1 1 0 1 0 15 35 1 1 0 1 0 15 33zM15 36A1 1 0 1 0 15 38 1 1 0 1 0 15 36zM33 33A1 1 0 1 0 33 35 1 1 0 1 0 33 33zM33 36A1 1 0 1 0 33 38 1 1 0 1 0 33 36zM18 33A1 1 0 1 0 18 35 1 1 0 1 0 18 33zM21 33A1 1 0 1 0 21 35 1 1 0 1 0 21 33zM24 33A1 1 0 1 0 24 35 1 1 0 1 0 24 33zM27 33A1 1 0 1 0 27 35 1 1 0 1 0 27 33zM30 33A1 1 0 1 0 30 35 1 1 0 1 0 30 33zM31 37c0 .553-.447 1-1 1H18c-.552 0-1-.447-1-1l0 0c0-.553.448-1 1-1h12C30.553 36 31 36.447 31 37L31 37z"/></g></svg>

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 0
ux/public/_assets/icons/color-nodejs.svg


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 0
ux/public/_assets/icons/color-postgresql.svg


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 0
ux/public/_assets/icons/fluent-account.svg


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 0
ux/public/_assets/icons/fluent-advance.svg


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 0
ux/public/_assets/icons/fluent-api.svg


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 0
ux/public/_assets/icons/fluent-apps-tab.svg


+ 1 - 0
ux/public/_assets/icons/fluent-bar-chart.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg"  viewBox="0 0 48 48" width="96px" height="96px"><path fill="#50e6ff" d="M44,8v33h-8V8c0-0.552,0.448-1,1-1h6C43.552,7,44,7.448,44,8z"/><path fill="#35c1f1" d="M36,15v26h-8V15H36z"/><path fill="#199be2" d="M28,13v28h-8V13c0-0.552,0.448-1,1-1h6C27.552,12,28,12.448,28,13z"/><path fill="#0078d4" d="M20,20v21h-8V20H20z"/><path fill="#0d62ab" d="M12,17v24H4V17c0-0.552,0.448-1,1-1h6C11.552,16,12,16.448,12,17z"/></svg>

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 0
ux/public/_assets/icons/fluent-bunch-of-keys.svg


+ 1 - 0
ux/public/_assets/icons/fluent-bursts.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg"  viewBox="0 0 48 48" width="96px" height="96px"><path fill="#50e6ff" d="M6.673,11.767l24-8.308C31.322,3.235,32,3.717,32,4.404v18.884c0,0.426-0.27,0.806-0.673,0.945	l-24,8.308C6.678,32.765,6,32.283,6,31.596V12.712C6,12.286,6.27,11.906,6.673,11.767z"/><path d="M31.327,24.233C31.73,24.094,32,23.714,32,23.288V8.672l-20.653,7.149	C10.541,16.1,10,16.859,10,17.712v13.903L31.327,24.233z" opacity=".05"/><path d="M31.327,24.233C31.73,24.094,32,23.714,32,23.288V9.202l-20.49,7.093	c-0.604,0.208-1.01,0.778-1.01,1.417v13.73L31.327,24.233z" opacity=".05"/><path fill="#199be2" d="M11.673,16.767l24-8.308C36.322,8.235,37,8.717,37,9.404v18.884c0,0.426-0.27,0.806-0.673,0.945	l-24,8.308C11.678,37.765,11,37.283,11,36.596V17.712C11,17.286,11.27,16.906,11.673,16.767z"/><path d="M36.327,29.233C36.73,29.094,37,28.714,37,28.288V13.672l-20.653,7.149	C15.541,21.1,15,21.859,15,22.712v13.903L36.327,29.233z" opacity=".05"/><path d="M36.327,29.233C36.73,29.094,37,28.714,37,28.288V14.202l-20.49,7.093	c-0.604,0.208-1.01,0.778-1.01,1.417v13.73L36.327,29.233z" opacity=".05"/><path fill="#0d62ab" d="M16.673,21.767l24-8.308C41.322,13.235,42,13.717,42,14.404v18.884c0,0.426-0.27,0.806-0.673,0.945	l-24,8.308C16.678,42.765,16,42.283,16,41.596V22.712C16,22.286,16.27,21.906,16.673,21.767z"/></svg>

+ 1 - 0
ux/public/_assets/icons/fluent-cashbook.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg"  viewBox="0 0 48 48" width="96px" height="96px"><path fill="#7b83eb" d="M24,13H3v24c0,1.105,0.895,2,2,2h19V13z"/><path fill="#5059c9" d="M45,13H24v26h19c1.105,0,2-0.895,2-2V13z"/><path fill="#3a41ac" d="M43,9H5c-1.105,0-2,0.895-2,2v2h42v-2C45,9.895,44.105,9,43,9z"/><circle cx="7" cy="18" r="1" fill="#3a41ac"/><circle cx="7" cy="22" r="1" fill="#3a41ac"/><circle cx="7" cy="26" r="1" fill="#3a41ac"/><circle cx="7" cy="34" r="1" fill="#3a41ac"/><path fill="#3a41ac" d="M20,19h-7c-0.552,0-1-0.448-1-1v0c0-0.552,0.448-1,1-1h7c0.552,0,1,0.448,1,1v0	C21,18.552,20.552,19,20,19z"/><path fill="#3a41ac" d="M20,27h-7c-0.552,0-1-0.448-1-1v0c0-0.552,0.448-1,1-1h7c0.552,0,1,0.448,1,1v0	C21,26.552,20.552,27,20,27z"/><path fill="#3a41ac" d="M20,23h-7c-0.552,0-1-0.448-1-1v0c0-0.552,0.448-1,1-1h7c0.552,0,1,0.448,1,1v0	C21,22.552,20.552,23,20,23z"/><path fill="#3a41ac" d="M20,35h-7c-0.552,0-1-0.448-1-1l0,0c0-0.552,0.448-1,1-1h7c0.552,0,1,0.448,1,1l0,0	C21,34.552,20.552,35,20,35z"/><path fill="#3a41ac" d="M20,31h-2c-0.552,0-1-0.448-1-1v0c0-0.552,0.448-1,1-1h2c0.552,0,1,0.448,1,1v0	C21,30.552,20.552,31,20,31z"/><circle cx="28" cy="18" r="1" fill="#7b83eb"/><circle cx="28" cy="22" r="1" fill="#7b83eb"/><circle cx="28" cy="26" r="1" fill="#7b83eb"/><circle cx="28" cy="34" r="1" fill="#7b83eb"/><path fill="#7b83eb" d="M41,19h-7c-0.552,0-1-0.448-1-1v0c0-0.552,0.448-1,1-1h7c0.552,0,1,0.448,1,1v0	C42,18.552,41.552,19,41,19z"/><path fill="#7b83eb" d="M41,27h-7c-0.552,0-1-0.448-1-1v0c0-0.552,0.448-1,1-1h7c0.552,0,1,0.448,1,1v0	C42,26.552,41.552,27,41,27z"/><path fill="#7b83eb" d="M41,23h-7c-0.552,0-1-0.448-1-1v0c0-0.552,0.448-1,1-1h7c0.552,0,1,0.448,1,1v0	C42,22.552,41.552,23,41,23z"/><path fill="#7b83eb" d="M41,35h-7c-0.552,0-1-0.448-1-1l0,0c0-0.552,0.448-1,1-1h7c0.552,0,1,0.448,1,1l0,0	C42,34.552,41.552,35,41,35z"/><path fill="#7b83eb" d="M41,31h-2c-0.552,0-1-0.448-1-1v0c0-0.552,0.448-1,1-1h2c0.552,0,1,0.448,1,1v0	C42,30.552,41.552,31,41,31z"/></svg>

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 0
ux/public/_assets/icons/fluent-change-theme.svg


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 0
ux/public/_assets/icons/fluent-color-wheel.svg


+ 1 - 0
ux/public/_assets/icons/fluent-comments.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg"  viewBox="0 0 48 48" width="96px" height="96px"><linearGradient id="CrkWqRsuEmwpViPxws7A9a" x1="12.686" x2="35.58" y1="4.592" y2="41.841" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#737be6"/><stop offset="1" stop-color="#4750b3"/></linearGradient><path fill="url(#CrkWqRsuEmwpViPxws7A9a)" d="M42,8H6c-1.105,0-2,0.895-2,2v26c0,1.105,0.895,2,2,2h8v7.998	c0,0.891,1.077,1.337,1.707,0.707L24.412,38H42c1.105,0,2-0.895,2-2V10C44,8.895,43.105,8,42,8z"/><path d="M12,22h24c1.105,0,2-0.895,2-2v-2c0-1.105-0.895-2-2-2H12c-1.105,0-2,0.895-2,2v2	C10,21.105,10.895,22,12,22z" opacity=".05"/><path d="M12,21.5h24c0.828,0,1.5-0.672,1.5-1.5v-2c0-0.828-0.672-1.5-1.5-1.5H12c-0.828,0-1.5,0.672-1.5,1.5	v2C10.5,20.828,11.172,21.5,12,21.5z" opacity=".07"/><path d="M12,30h18c1.105,0,2-0.895,2-2v-2c0-1.105-0.895-2-2-2H12c-1.105,0-2,0.895-2,2v2	C10,29.105,10.895,30,12,30z" opacity=".05"/><path d="M12,29.5h18c0.828,0,1.5-0.672,1.5-1.5v-2c0-0.828-0.672-1.5-1.5-1.5H12c-0.828,0-1.5,0.672-1.5,1.5	v2C10.5,28.828,11.172,29.5,12,29.5z" opacity=".07"/><path fill="#fff" d="M31,26v2c0,0.552-0.448,1-1,1H12c-0.552,0-1-0.448-1-1v-2c0-0.552,0.448-1,1-1h18	C30.552,25,31,25.448,31,26z"/><path fill="#fff" d="M37,18v2c0,0.552-0.448,1-1,1H12c-0.552,0-1-0.448-1-1v-2c0-0.552,0.448-1,1-1h24	C36.552,17,37,17.448,37,18z"/></svg>

+ 1 - 0
ux/public/_assets/icons/fluent-database.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg"  viewBox="0 0 48 48" width="48px" height="48px"><linearGradient id="LkaBH78Qy0LlLxZFYVKUda" x1="8" x2="40" y1="35.5" y2="35.5" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#11408a"/><stop offset="1" stop-color="#103f8f"/></linearGradient><path fill="url(#LkaBH78Qy0LlLxZFYVKUda)" d="M40,28H8c0,0,0,10.271,0,11c0,2.209,7.163,4,16,4s16-1.791,16-4C40,38.271,40,28,40,28z"/><linearGradient id="LkaBH78Qy0LlLxZFYVKUdb" x1="8" x2="40" y1="25.5" y2="25.5" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#1d59b3"/><stop offset="1" stop-color="#195bbc"/></linearGradient><path fill="url(#LkaBH78Qy0LlLxZFYVKUdb)" d="M40,18H8c0,0,0,10.271,0,11c0,2.209,7.163,4,16,4s16-1.791,16-4C40,28.271,40,18,40,18z"/><linearGradient id="LkaBH78Qy0LlLxZFYVKUdc" x1="8" x2="40" y1="15" y2="15" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#3079d6"/><stop offset="1" stop-color="#297cd2"/></linearGradient><path fill="url(#LkaBH78Qy0LlLxZFYVKUdc)" d="M40,8H8c0,0,0,9.756,0,10.5c0,1.933,7.163,3.5,16,3.5s16-1.567,16-3.5C40,17.756,40,8,40,8z"/><linearGradient id="LkaBH78Qy0LlLxZFYVKUdd" x1="8" x2="40" y1="8" y2="8" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#42a3f2"/><stop offset="1" stop-color="#42a4eb"/></linearGradient><ellipse cx="24" cy="8" fill="url(#LkaBH78Qy0LlLxZFYVKUdd)" rx="16" ry="3"/></svg>

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 0
ux/public/_assets/icons/fluent-delete-bin.svg


+ 1 - 0
ux/public/_assets/icons/fluent-down.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg"  viewBox="0 0 48 48" width="48px" height="48px"><linearGradient id="YGGk5xCyNietrYbhNX6WKa" x1="13.25" x2="13.25" y1="42.071" y2="28.237" gradientTransform="rotate(-90 24 24)" gradientUnits="userSpaceOnUse"><stop offset=".365" stop-color="#199ae0"/><stop offset=".699" stop-color="#1898de"/><stop offset=".819" stop-color="#1691d8"/><stop offset=".905" stop-color="#1186cc"/><stop offset=".974" stop-color="#0a75bc"/><stop offset="1" stop-color="#076cb3"/></linearGradient><path fill="url(#YGGk5xCyNietrYbhNX6WKa)" d="M25.168,45.516l15.849-15.849c0.645-0.645,0.645-1.69,0-2.335l-3.349-3.349	c-0.645-0.645-1.69-0.645-2.335,0L19.484,39.832c-0.645,0.645-0.645,1.69,0,2.335l3.349,3.349	C23.477,46.161,24.523,46.161,25.168,45.516z"/><linearGradient id="YGGk5xCyNietrYbhNX6WKb" x1="11.984" x2="30.101" y1="2.622" y2="45.327" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#32bdef"/><stop offset="1" stop-color="#1ea2e4"/></linearGradient><path fill="url(#YGGk5xCyNietrYbhNX6WKb)" d="M27,2h-6c-1.105,0-2,0.895-2,2v26.316l-6.333-6.333c-0.645-0.645-1.69-0.645-2.335,0 l-3.349,3.349c-0.645,0.645-0.645,1.69,0,2.335l15.849,15.849c0.645,0.645,1.69,0.645,2.335,0l3.349-3.349 C28.839,41.845,29,41.423,29,41V4C29,2.895,28.105,2,27,2z"/></svg>

Algúns arquivos non se mostraron porque demasiados arquivos cambiaron neste cambio