diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a638c90..7b645870 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). -## [2.0.0-beta.XX] - 2018-XX-XX +## [2.0.0-beta.42] - 2018-02-17 ### Added - Added Patreon link in Contribute admin page - Added Theme Code Injection functionality @@ -27,5 +27,5 @@ This project adheres to [Semantic Versioning](http://semver.org/). ## [2.0.0-beta.11] - 2018-01-20 - First beta release -[2.0.0-beta.12]: https://github.com/Requarks/wiki/releases/tag/2.0.0-beta.12 +[2.0.0-beta.42]: https://github.com/Requarks/wiki/releases/tag/2.0.0-beta.42 [2.0.0-beta.11]: https://github.com/Requarks/wiki/releases/tag/2.0.0-beta.11 diff --git a/dev/docker-sqlite/Dockerfile b/dev/docker-sqlite/Dockerfile index 26f32650..c9a64df8 100644 --- a/dev/docker-sqlite/Dockerfile +++ b/dev/docker-sqlite/Dockerfile @@ -11,7 +11,6 @@ RUN apk update && \ WORKDIR /wiki COPY package.json . RUN yarn --silent -COPY ./dev/docker-sqlite/init.sh ./init.sh ENV dockerdev 1 ENV DEVDB sqlite diff --git a/server/core/kernel.js b/server/core/kernel.js index 994a2133..b11728b7 100644 --- a/server/core/kernel.js +++ b/server/core/kernel.js @@ -11,8 +11,16 @@ module.exports = { WIKI.models = require('./db').init() - await WIKI.models.onReady - await WIKI.configSvc.loadFromDb() + try { + await WIKI.models.onReady + await WIKI.configSvc.loadFromDb() + } catch (err) { + WIKI.logger.error('Database Initialization Error: ' + err.message) + if (WIKI.IS_DEBUG) { + console.error(err) + } + process.exit(1) + } this.bootMaster() }, diff --git a/server/db/migrations-sqlite/2.0.0-beta.1.js b/server/db/migrations-sqlite/2.0.0-beta.1.js new file mode 100644 index 00000000..42581ec1 --- /dev/null +++ b/server/db/migrations-sqlite/2.0.0-beta.1.js @@ -0,0 +1,317 @@ +exports.up = knex => { + const dbCompat = { + charset: (WIKI.config.db.type === `mysql` || WIKI.config.db.type === `mariadb`), + noForeign: WIKI.config.db.type === 'sqlite' + } + 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() + + table.integer('folderId').unsigned().references('id').inTable('assetFolders') + table.integer('authorId').unsigned().references('id').inTable('users') + }) + // 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() + + table.integer('pageId').unsigned().references('id').inTable('pages') + table.integer('authorId').unsigned().references('id').inTable('users') + }) + // 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() + + 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') + }) + // 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() + + 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') + }) + // 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') + + table.integer('parent').unsigned().references('id').inTable('pageTree') + table.integer('pageId').unsigned().references('id').inTable('pages') + table.string('localeCode', 2).references('code').inTable('locales') + }) + // 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() + + table.integer('userId').unsigned().references('id').inTable('users') + }) + // 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() + + 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') + }) + // ===================================== + // 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 => { + // dbCompat.noForeign ? table.integer('folderId').unsigned() : table.integer('folderId').unsigned().references('id').inTable('assetFolders') + // dbCompat.noForeign ? table.integer('authorId').unsigned() : table.integer('authorId').unsigned().references('id').inTable('users') + // }) + // .table('comments', table => { + // dbCompat.noForeign ? table.integer('pageId').unsigned() : table.integer('pageId').unsigned().references('id').inTable('pages') + // dbCompat.noForeign ? table.integer('authorId').unsigned() : table.integer('authorId').unsigned().references('id').inTable('users') + // }) + // .table('pageHistory', table => { + // dbCompat.noForeign ? table.integer('pageId').unsigned() : table.integer('pageId').unsigned().references('id').inTable('pages') + // dbCompat.noForeign ? table.string('editorKey') : table.string('editorKey').references('key').inTable('editors') + // dbCompat.noForeign ? table.string('localeCode', 2) : table.string('localeCode', 2).references('code').inTable('locales') + // dbCompat.noForeign ? table.integer('authorId').unsigned() : table.integer('authorId').unsigned().references('id').inTable('users') + // }) + // .table('pages', table => { + // dbCompat.noForeign ? table.string('editorKey') : table.string('editorKey').references('key').inTable('editors') + // dbCompat.noForeign ? table.string('localeCode', 2) : table.string('localeCode', 2).references('code').inTable('locales') + // dbCompat.noForeign ? table.integer('authorId').unsigned() : table.integer('authorId').unsigned().references('id').inTable('users') + // dbCompat.noForeign ? table.integer('creatorId').unsigned() : table.integer('creatorId').unsigned().references('id').inTable('users') + // }) + // .table('pageTree', table => { + // dbCompat.noForeign ? table.integer('parent').unsigned() : table.integer('parent').unsigned().references('id').inTable('pageTree') + // dbCompat.noForeign ? table.integer('pageId').unsigned() : table.integer('pageId').unsigned().references('id').inTable('pages') + // dbCompat.noForeign ? table.string('localeCode', 2) : table.string('localeCode', 2).references('code').inTable('locales') + // }) + // .table('userKeys', table => { + // dbCompat.noForeign ? table.integer('userId').unsigned() : table.integer('userId').unsigned().references('id').inTable('users') + // }) + .table('users', table => { + // dbCompat.noForeign ? table.string('providerKey') : table.string('providerKey').references('key').inTable('authentication').notNullable().defaultTo('local') + // dbCompat.noForeign ? table.string('localeCode', 2) : table.string('localeCode', 2).references('code').inTable('locales').notNullable().defaultTo('en') + // dbCompat.noForeign ? table.string('defaultEditor') : 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') +} diff --git a/server/db/migrations-sqlite/2.0.0-beta.11.js b/server/db/migrations-sqlite/2.0.0-beta.11.js new file mode 100644 index 00000000..998730fa --- /dev/null +++ b/server/db/migrations-sqlite/2.0.0-beta.11.js @@ -0,0 +1,51 @@ +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.string('editorKey').references('key').inTable('editors') + table.string('localeCode', 2).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,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', 2).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') +} diff --git a/server/db/migrations-sqlite/2.0.0-beta.38.js b/server/db/migrations-sqlite/2.0.0-beta.38.js new file mode 100644 index 00000000..1c7f5343 --- /dev/null +++ b/server/db/migrations-sqlite/2.0.0-beta.38.js @@ -0,0 +1,15 @@ +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') + }) +} diff --git a/server/db/migrator-source.js b/server/db/migrator-source.js index debed19d..67bb3daa 100644 --- a/server/db/migrator-source.js +++ b/server/db/migrator-source.js @@ -2,6 +2,8 @@ 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') + /* global WIKI */ module.exports = { @@ -10,11 +12,10 @@ module.exports = { * @returns Promise */ async getMigrations() { - const absoluteDir = path.join(WIKI.SERVERPATH, 'db/migrations') - const migrationFiles = await fs.readdir(absoluteDir) + const migrationFiles = await fs.readdir(baseMigrationPath) return migrationFiles.sort(semver.compare).map(m => ({ file: m, - directory: absoluteDir + directory: baseMigrationPath })) }, @@ -23,6 +24,6 @@ module.exports = { }, getMigration(migration) { - return require(path.join(WIKI.SERVERPATH, 'db/migrations', migration.file)); + return require(path.join(baseMigrationPath, migration.file)); } } diff --git a/server/models/pages.js b/server/models/pages.js index a819d2dc..7e922999 100644 --- a/server/models/pages.js +++ b/server/models/pages.js @@ -174,6 +174,7 @@ module.exports = class Page extends Model { } await WIKI.models.pageHistory.addVersion({ ...ogPage, + isPublished: ogPage.isPublished === true || ogPage.isPublished === 1, action: 'updated' }) await WIKI.models.pages.query().patch({ diff --git a/server/modules/authentication/local/authentication.js b/server/modules/authentication/local/authentication.js index 5f4120e9..57f4dc47 100644 --- a/server/modules/authentication/local/authentication.js +++ b/server/modules/authentication/local/authentication.js @@ -12,30 +12,28 @@ module.exports = { new LocalStrategy({ usernameField: 'email', passwordField: 'password' - }, (uEmail, uPassword, done) => { - WIKI.models.users.query().findOne({ - email: uEmail, - providerKey: 'local' - }).then((user) => { + }, async (uEmail, uPassword, done) => { + try { + const user = await WIKI.models.users.query().findOne({ + email: uEmail, + providerKey: 'local' + }) if (user) { - return user.verifyPassword(uPassword).then(() => { - if (!user.isActive) { - done(new WIKI.Error.AuthAccountBanned(), null) - } else if (!user.isVerified) { - done(new WIKI.Error.AuthAccountNotVerified(), null) - } else { - done(null, user) - } - }).catch((err) => { - done(err, null) - }) + await user.verifyPassword(uPassword) + if (!user.isActive) { + done(new WIKI.Error.AuthAccountBanned(), null) + } else if (!user.isVerified) { + done(new WIKI.Error.AuthAccountNotVerified(), null) + } else { + done(null, user) + } } else { done(new WIKI.Error.AuthLoginFailed(), null) } - }).catch((err) => { + } catch (err) { done(err, null) - }) - } - )) + } + }) + ) } } diff --git a/wiki.js b/wiki.js index dfd92677..9d59b7e9 100644 --- a/wiki.js +++ b/wiki.js @@ -124,28 +124,29 @@ const init = { console.warn(chalk.yellow('--- Closing DB connections...')) await global.WIKI.models.knex.destroy() console.warn(chalk.yellow('--- Closing Server connections...')) - global.WIKI.server.destroy(() => { - console.warn(chalk.yellow('--- Purging node modules cache...')) + if (global.WIKI.server) { + await new Promise((resolve, reject) => global.WIKI.server.destroy(resolve)) + } + console.warn(chalk.yellow('--- Purging node modules cache...')) - global.WIKI = {} - Object.keys(require.cache).forEach(id => { - if (/[/\\]server[/\\]/.test(id)) { - delete require.cache[id] - } - }) - Object.keys(module.constructor._pathCache).forEach(cacheKey => { - if (/[/\\]server[/\\]/.test(cacheKey)) { - delete module.constructor._pathCache[cacheKey] - } - }) - - console.warn(chalk.yellow('--- Unregistering process listeners...')) - - process.removeAllListeners('unhandledRejection') - process.removeAllListeners('uncaughtException') - - require('./server') + global.WIKI = {} + Object.keys(require.cache).forEach(id => { + if (/[/\\]server[/\\]/.test(id)) { + delete require.cache[id] + } }) + Object.keys(module.constructor._pathCache).forEach(cacheKey => { + if (/[/\\]server[/\\]/.test(cacheKey)) { + delete module.constructor._pathCache[cacheKey] + } + }) + + console.warn(chalk.yellow('--- Unregistering process listeners...')) + + process.removeAllListeners('unhandledRejection') + process.removeAllListeners('uncaughtException') + + require('./server') } }