refactor: migrate to objection.js + knex

This commit is contained in:
NGPixel 2018-05-19 16:40:07 -04:00
parent e03e6826a8
commit c9b643fbf0
46 changed files with 1260 additions and 959 deletions

View File

@ -220,10 +220,11 @@
)
v-flex(xs6)
v-text-field(
ref='adminPasswordConfirm',
v-model='conf.adminPasswordConfirm',
label='Confirm Password',
hint='Verify your password again.',
v-validate='{ required: true, confirmed: `$adminPassword` }',
v-validate='{ required: true, min: 8 }',
data-vv-name='adminPasswordConfirm',
data-vv-as='Confirm Password',
data-vv-scope='admin',
@ -308,10 +309,6 @@ export default {
wikiVersion: {
type: String,
required: true
},
langs: {
type: Array,
required: true
}
},
data() {
@ -394,7 +391,7 @@ export default {
async proceedToUpgrade () {
if (this.state < 5) {
const validationSuccess = await this.$validator.validateAll('admin')
if (!validationSuccess) {
if (!validationSuccess || this.conf.adminPassword !== this.conf.adminPasswordConfirm) {
this.state = 4
return
}

View File

@ -23,12 +23,13 @@ paths:
# ---------------------------------------------------------------------
# Supported Database Engines:
# - postgres = PostgreSQL 9.5 or later
# - mysql = MySQL 5.7.8 or later
# - mysql = MySQL 8.0 / MariaDB 10.2.7 or later
# - mssql = MS SQL Server 2012 or later
# - sqlite = SQLite 3.9 or later
db:
type: postgres
# PostgreSQL and MySQL only:
# PostgreSQL / MySQL / MariaDB / MS SQL Server only:
host: localhost
port: 5432
user: wikijs

View File

@ -9,7 +9,6 @@
"restart": "node wiki restart",
"dev": "node wiki dev",
"build": "webpack --profile --config dev/webpack/webpack.prod.js",
"build:locales": "node dev/tasks/localization",
"watch": "webpack --config dev/webpack/webpack.dev.js",
"test": "eslint --format codeframe --ext .js,.vue . && pug-lint server/views && jest"
},
@ -43,9 +42,9 @@
"axios": "0.18.0",
"bcryptjs-then": "1.0.1",
"bluebird": "3.5.1",
"body-parser": "1.18.2",
"body-parser": "1.18.3",
"bugsnag": "2.3.1",
"bull": "3.4.1",
"bull": "3.4.2",
"cheerio": "1.0.0-rc.2",
"child-process-promise": "2.2.1",
"chokidar": "2.0.3",
@ -61,15 +60,15 @@
"express-brute": "1.0.1",
"express-brute-redis": "0.0.1",
"express-session": "1.15.6",
"file-type": "7.7.1",
"file-type": "8.0.0",
"filesize.js": "1.0.2",
"follow-redirects": "1.4.1",
"follow-redirects": "1.5.0",
"fs-extra": "6.0.1",
"getos": "3.1.0",
"graphql": "0.13.2",
"graphql-list-fields": "2.0.2",
"graphql-tools": "3.0.1",
"i18next": "11.3.1",
"i18next": "11.3.2",
"i18next-express-middleware": "1.1.1",
"i18next-localstorage-cache": "1.1.1",
"i18next-node-fs-backend": "1.0.0",
@ -78,6 +77,7 @@
"js-yaml": "3.11.0",
"jsonwebtoken": "8.2.1",
"klaw": "2.1.1",
"knex": "0.14.6",
"lodash": "4.17.10",
"markdown-it": "8.4.1",
"markdown-it-abbr": "1.0.4",
@ -96,12 +96,14 @@
"mathjax-node": "2.1.0",
"mime-types": "2.1.18",
"moment": "2.22.1",
"moment-timezone": "0.5.16",
"mongodb": "3.0.7",
"moment-timezone": "0.5.17",
"mongodb": "3.1.0-beta4",
"mssql": "4.1.0",
"multer": "1.3.0",
"mysql2": "1.5.3",
"node-2fa": "1.1.2",
"oauth2orize": "1.11.0",
"objection": "1.1.8",
"ora": "2.1.0",
"passport": "0.4.0",
"passport-auth0": "0.6.1",
@ -117,20 +119,18 @@
"passport-slack": "0.0.7",
"passport-twitch": "1.0.3",
"passport-windowslive": "1.0.2",
"pg": "6.4.2",
"pg": "7.4.3",
"pg-hstore": "2.3.2",
"pg-promise": "7.5.3",
"pm2": "2.10.3",
"pm2": "2.10.4",
"pug": "2.0.3",
"qr-image": "3.2.0",
"raven": "2.6.1",
"raven": "2.6.2",
"read-chunk": "2.1.0",
"remove-markdown": "0.2.2",
"request": "2.85.0",
"request": "2.86.0",
"request-promise": "4.2.2",
"scim-query-filter-parser": "1.1.0",
"semver": "5.5.0",
"sequelize": "4.37.7",
"serve-favicon": "2.5.0",
"sqlite3": "4.0.0",
"uuid": "3.2.1",
@ -145,7 +145,7 @@
"apollo-client-preset": "1.0.8",
"apollo-fetch": "0.7.0",
"apollo-link-batch-http": "1.2.2",
"autoprefixer": "8.4.1",
"autoprefixer": "8.5.0",
"babel-cli": "6.26.0",
"babel-core": "6.26.3",
"babel-eslint": "8.2.3",
@ -169,7 +169,7 @@
"eslint": "4.19.1",
"eslint-config-requarks": "1.0.7",
"eslint-config-standard": "11.0.0",
"eslint-plugin-import": "2.11.0",
"eslint-plugin-import": "2.12.0",
"eslint-plugin-node": "6.0.1",
"eslint-plugin-promise": "3.7.0",
"eslint-plugin-standard": "3.1.0",
@ -183,8 +183,8 @@
"html-webpack-pug-plugin": "0.3.0",
"i18next-xhr-backend": "1.5.1",
"ignore-loader": "0.1.2",
"jest": "22.4.3",
"jest-junit": "3.7.0",
"jest": "22.4.4",
"jest-junit": "4.0.0",
"js-cookie": "2.2.0",
"lodash-webpack-plugin": "0.11.5",
"mini-css-extract-plugin": "0.4.0",
@ -196,7 +196,7 @@
"postcss-flexibility": "2.0.0",
"postcss-import": "11.1.0",
"postcss-loader": "2.1.5",
"postcss-selector-parser": "4.0.0",
"postcss-selector-parser": "5.0.0-rc.3",
"pug-lint": "2.5.0",
"pug-loader": "2.4.0",
"pug-plain-loader": "1.0.0",
@ -218,23 +218,23 @@
"vue-clipboards": "1.2.4",
"vue-codemirror": "4.0.5",
"vue-hot-reload-api": "2.3.0",
"vue-loader": "15.0.10",
"vue-loader": "15.1.0",
"vue-material-design-icons": "1.4.0",
"vue-moment": "3.2.0",
"vue-moment": "4.0.0-0",
"vue-router": "3.0.1",
"vue-simple-breakpoints": "1.0.3",
"vue-template-compiler": "2.5.16",
"vuetify": "1.0.17",
"vuetify": "1.0.18",
"vuex": "3.0.1",
"vuex-persistedstate": "2.5.2",
"webpack": "4.8.2",
"webpack-bundle-analyzer": "2.11.2",
"vuex-persistedstate": "2.5.4",
"webpack": "4.8.3",
"webpack-bundle-analyzer": "2.12.0",
"webpack-cli": "2.1.3",
"webpack-dev-middleware": "3.1.3",
"webpack-hot-middleware": "2.22.1",
"webpack-hot-middleware": "2.22.2",
"webpack-merge": "4.1.2",
"whatwg-fetch": "2.0.4",
"write-file-webpack-plugin": "4.2.0"
"write-file-webpack-plugin": "4.3.2"
},
"browserslist": [
"> 1%",

View File

@ -18,7 +18,7 @@ module.exports = {
})
passport.deserializeUser(function (id, done) {
WIKI.db.User.findById(id).then((user) => {
WIKI.db.users.query().findById(id).then((user) => {
if (user) {
done(null, user)
} else {
@ -58,57 +58,6 @@ module.exports = {
WIKI.logger.info(`Authentication Provider ${strategy.title}: [ OK ]`)
})
// Create Guest account for first-time
WIKI.db.User.findOne({
where: {
provider: 'local',
email: 'guest@example.com'
}
}).then((c) => {
if (c < 1) {
return WIKI.db.User.create({
provider: 'local',
email: 'guest@example.com',
name: 'Guest',
password: '',
role: 'guest'
}).then(() => {
WIKI.logger.info('[AUTH] Guest account created successfully!')
return true
}).catch((err) => {
WIKI.logger.error('[AUTH] An error occured while creating guest account:')
WIKI.logger.error(err)
return err
})
}
})
// .then(() => {
// if (process.env.WIKI_JS_HEROKU) {
// return WIKI.db.User.findOne({ provider: 'local', email: process.env.WIKI_ADMIN_EMAIL }).then((c) => {
// if (c < 1) {
// // Create root admin account (HEROKU ONLY)
// return WIKI.db.User.create({
// provider: 'local',
// email: process.env.WIKI_ADMIN_EMAIL,
// name: 'Administrator',
// password: '$2a$04$MAHRw785Xe/Jd5kcKzr3D.VRZDeomFZu2lius4gGpZZ9cJw7B7Mna', // admin123 (default)
// role: 'admin'
// }).then(() => {
// WIKI.logger.info('[AUTH] Root admin account created successfully!')
// return true
// }).catch((err) => {
// WIKI.logger.error('[AUTH] An error occured while creating root admin account:')
// WIKI.logger.error(err)
// return err
// })
// } else { return true }
// })
// } else { return true }
// })
return this
}
}

View File

@ -59,17 +59,10 @@ module.exports = {
subsets = WIKI.data.configNamespaces
}
let results = await WIKI.db.Setting.findAll({
attributes: ['key', 'config'],
where: {
key: {
$in: subsets
}
}
})
let results = await WIKI.db.settings.query().select(['key', 'value']).whereIn('key', subsets)
if (_.isArray(results) && results.length === subsets.length) {
results.forEach(result => {
WIKI.config[result.key] = result.config
WIKI.config[result.key] = result.value
})
return true
} else {
@ -88,14 +81,18 @@ module.exports = {
subsets = WIKI.data.configNamespaces
}
let trx = await WIKI.db.Objection.transaction.start(WIKI.db.knex)
try {
for (let set of subsets) {
await WIKI.db.Setting.upsert({
key: set,
config: _.get(WIKI.config, set, {})
})
console.info(set)
await WIKI.db.settings.query(trx).patch({
value: _.get(WIKI.config, set, {})
}).where('key', set)
}
await trx.commit()
} catch (err) {
await trx.rollback(err)
WIKI.logger.error(`Failed to save configuration to DB: ${err.message}`)
return false
}

View File

@ -1,55 +1,18 @@
const _ = require('lodash')
const fs = require('fs')
const autoload = require('auto-load')
const path = require('path')
const Promise = require('bluebird')
const Sequelize = require('sequelize')
const Knex = require('knex')
const Objection = require('objection')
/* global WIKI */
const operatorsAliases = {
$eq: Sequelize.Op.eq,
$ne: Sequelize.Op.ne,
$gte: Sequelize.Op.gte,
$gt: Sequelize.Op.gt,
$lte: Sequelize.Op.lte,
$lt: Sequelize.Op.lt,
$not: Sequelize.Op.not,
$in: Sequelize.Op.in,
$notIn: Sequelize.Op.notIn,
$is: Sequelize.Op.is,
$like: Sequelize.Op.like,
$notLike: Sequelize.Op.notLike,
$iLike: Sequelize.Op.iLike,
$notILike: Sequelize.Op.notILike,
$regexp: Sequelize.Op.regexp,
$notRegexp: Sequelize.Op.notRegexp,
$iRegexp: Sequelize.Op.iRegexp,
$notIRegexp: Sequelize.Op.notIRegexp,
$between: Sequelize.Op.between,
$notBetween: Sequelize.Op.notBetween,
$overlap: Sequelize.Op.overlap,
$contains: Sequelize.Op.contains,
$contained: Sequelize.Op.contained,
$adjacent: Sequelize.Op.adjacent,
$strictLeft: Sequelize.Op.strictLeft,
$strictRight: Sequelize.Op.strictRight,
$noExtendRight: Sequelize.Op.noExtendRight,
$noExtendLeft: Sequelize.Op.noExtendLeft,
$and: Sequelize.Op.and,
$or: Sequelize.Op.or,
$any: Sequelize.Op.any,
$all: Sequelize.Op.all,
$values: Sequelize.Op.values,
$col: Sequelize.Op.col
}
/**
* PostgreSQL DB module
*/
module.exports = {
Sequelize,
Op: Sequelize.Op,
Objection,
knex: null,
/**
* Initialize DB
*
@ -57,65 +20,63 @@ module.exports = {
*/
init() {
let self = this
let dbModelsPath = path.join(WIKI.SERVERPATH, 'models')
// Define Sequelize instance
this.inst = new this.Sequelize(WIKI.config.db.db, WIKI.config.db.user, WIKI.config.db.pass, {
let dbClient = null
const dbConfig = (!_.isEmpty(process.env.WIKI_DB_CONNSTR)) ? process.env.WIKI_DB_CONNSTR : {
host: WIKI.config.db.host,
user: WIKI.config.db.user,
password: WIKI.config.db.pass,
database: WIKI.config.db.db,
port: WIKI.config.db.port,
dialect: WIKI.config.db.type,
storage: WIKI.config.db.storage,
pool: {
max: 10,
min: 0,
idle: 10000
},
logging: log => { WIKI.logger.log('debug', log) },
operatorsAliases
filename: WIKI.config.db.storage
}
switch (WIKI.config.db.type) {
case 'postgres':
dbClient = 'pg'
break
case 'mysql':
dbClient = 'mysql2'
break
case 'mssql':
dbClient = 'mssql'
break
case 'sqlite':
dbClient = 'sqlite3'
break
default:
WIKI.logger.error('Invalid DB Type')
process.exit(1)
}
this.knex = Knex({
client: dbClient,
useNullAsDefault: true,
connection: dbConfig,
debug: WIKI.IS_DEBUG
})
// Attempt to connect and authenticate to DB
this.inst.authenticate().then(() => {
WIKI.logger.info(`Database (${WIKI.config.db.type}) connection: [ OK ]`)
}).catch(err => {
WIKI.logger.error(`Failed to connect to ${WIKI.config.db.type} instance.`)
WIKI.logger.error(err)
process.exit(1)
})
Objection.Model.knex(this.knex)
// Load DB Models
fs
.readdirSync(dbModelsPath)
.filter(file => {
return (file.indexOf('.') !== 0 && file.indexOf('_') !== 0)
})
.forEach(file => {
let modelName = _.upperFirst(_.camelCase(_.split(file, '.')[0]))
self[modelName] = self.inst.import(path.join(dbModelsPath, file))
})
// Associate DB Models
require(path.join(dbModelsPath, '_relations.js'))(self)
const models = autoload(path.join(WIKI.SERVERPATH, 'db/models'))
// Set init tasks
let initTasks = {
// -> Sync DB Schemas
syncSchemas() {
return self.inst.sync({
force: false,
logging: log => { WIKI.logger.log('debug', log) }
// -> Migrate DB Schemas
async syncSchemas() {
return self.knex.migrate.latest({
directory: path.join(WIKI.SERVERPATH, 'db/migrations'),
tableName: 'migrations'
})
},
// -> Set Connection App Name
setAppName() {
async setAppName() {
switch (WIKI.config.db.type) {
case 'postgres':
return self.inst.query(`set application_name = 'WIKI.js'`, { raw: true })
return self.knex.raw(`set application_name = 'Wiki.js'`)
}
}
}
@ -131,6 +92,9 @@ module.exports = {
this.onReady = Promise.each(initTasksQueue, t => t()).return(true)
return this
return {
...this,
...models
}
}
}

View File

@ -52,11 +52,7 @@ module.exports = {
}
},
async loadLocale(locale, opts = { silent: false }) {
const res = await WIKI.db.Locale.findOne({
where: {
code: locale
}
})
const res = await WIKI.db.locales.query().findOne('code', locale)
if (res) {
if (_.isPlainObject(res.strings)) {
_.forOwn(res.strings, (data, ns) => {

View File

@ -0,0 +1,75 @@
exports.up = knex => {
return knex.schema
// -------------------------------------
// GROUPS
// -------------------------------------
.createTable('groups', table => {
table.increments('id').primary()
table.string('name').notNullable()
table.string('createdAt').notNullable()
table.string('updatedAt').notNullable()
})
// -------------------------------------
// LOCALES
// -------------------------------------
.createTable('locales', table => {
table.increments('id').primary()
table.string('code', 2).notNullable().unique()
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()
})
// -------------------------------------
// SETTINGS
// -------------------------------------
.createTable('settings', table => {
table.increments('id').primary()
table.string('key').notNullable().unique()
table.json('value')
table.string('createdAt').notNullable()
table.string('updatedAt').notNullable()
})
// -------------------------------------
// USERS
// -------------------------------------
.createTable('users', table => {
table.increments('id').primary()
table.string('email').notNullable()
table.string('name').notNullable()
table.string('provider').notNullable().defaultTo('local')
table.string('providerId')
table.string('password')
table.boolean('tfaIsActive').notNullable().defaultTo(false)
table.string('tfaSecret')
table.enum('role', ['admin', 'guest', 'user']).notNullable().defaultTo('guest')
table.string('createdAt').notNullable()
table.string('updatedAt').notNullable()
table.unique(['provider', 'email'])
})
// -------------------------------------
// USER GROUPS
// -------------------------------------
.createTable('userGroups', table => {
table.increments('id').primary()
table.integer('userId').unsigned().references('id').inTable('users')
table.integer('groupId').unsigned().references('id').inTable('groups')
})
}
exports.down = knex => {
return knex.schema
.dropTableIfExists('userGroups')
.dropTableIfExists('groups')
.dropTableIfExists('locales')
.dropTableIfExists('settings')
.dropTableIfExists('users')
}

View File

@ -0,0 +1,48 @@
const Model = require('objection').Model
/**
* Settings model
*/
module.exports = class Group extends Model {
static get tableName() { return 'groups' }
static get jsonSchema () {
return {
type: 'object',
required: ['name'],
properties: {
id: {type: 'integer'},
name: {type: 'string'},
createdAt: {type: 'string'},
updatedAt: {type: 'string'}
}
}
}
static get relationMappings() {
const User = require('./users')
return {
users: {
relation: Model.ManyToManyRelation,
modelClass: User,
join: {
from: 'groups.id',
through: {
from: 'userGroups.groupId',
to: 'userGroups.userId'
},
to: 'users.id'
}
}
}
}
$beforeUpdate() {
this.updatedAt = new Date().toISOString()
}
$beforeInsert() {
this.createdAt = new Date().toISOString()
this.updatedAt = new Date().toISOString()
}
}

View File

@ -0,0 +1,34 @@
const Model = require('objection').Model
/**
* Locales model
*/
module.exports = class User extends Model {
static get tableName() { return 'locales' }
static get jsonSchema () {
return {
type: 'object',
required: ['code', 'name'],
properties: {
id: {type: 'integer'},
code: {type: 'string'},
strings: {type: 'object'},
isRTL: {type: 'boolean', default: false},
name: {type: 'string'},
nativeName: {type: 'string'},
createdAt: {type: 'string'},
updatedAt: {type: 'string'}
}
}
}
$beforeUpdate() {
this.updatedAt = new Date().toISOString()
}
$beforeInsert() {
this.createdAt = new Date().toISOString()
this.updatedAt = new Date().toISOString()
}
}

View File

@ -0,0 +1,31 @@
const Model = require('objection').Model
/**
* Settings model
*/
module.exports = class User extends Model {
static get tableName() { return 'settings' }
static get jsonSchema () {
return {
type: 'object',
required: ['key', 'value'],
properties: {
id: {type: 'integer'},
key: {type: 'string'},
value: {type: 'object'},
createdAt: {type: 'string'},
updatedAt: {type: 'string'}
}
}
}
$beforeUpdate() {
this.updatedAt = new Date().toISOString()
}
$beforeInsert() {
this.createdAt = new Date().toISOString()
this.updatedAt = new Date().toISOString()
}
}

235
server/db/models/users.js Normal file
View File

@ -0,0 +1,235 @@
/* global WIKI */
const bcrypt = require('bcryptjs-then')
const _ = require('lodash')
const tfa = require('node-2fa')
const securityHelper = require('../../helpers/security')
const Model = require('objection').Model
const bcryptRegexp = /^\$2[ayb]\$[0-9]{2}\$[A-Za-z0-9./]{53}$/
/**
* Users model
*/
module.exports = class User extends Model {
static get tableName() { return 'users' }
static get jsonSchema () {
return {
type: 'object',
required: ['email', 'name', 'provider'],
properties: {
id: {type: 'integer'},
email: {type: 'string', format: 'email'},
name: {type: 'string', minLength: 1, maxLength: 255},
provider: {type: 'string', minLength: 1, maxLength: 255},
providerId: {type: 'number'},
password: {type: 'string'},
role: {type: 'string', enum: ['admin', 'guest', 'user']},
tfaIsActive: {type: 'boolean', default: false},
tfaSecret: {type: 'string'},
createdAt: {type: 'string'},
updatedAt: {type: 'string'}
}
}
}
static get relationMappings() {
const Group = require('./groups')
return {
groups: {
relation: Model.ManyToManyRelation,
modelClass: Group,
join: {
from: 'users.id',
through: {
from: 'userGroups.userId',
to: 'userGroups.groupId'
},
to: 'groups.id'
}
}
}
}
async $beforeUpdate(opt, context) {
await super.$beforeUpdate(opt, context)
this.updatedAt = new Date().toISOString()
if (!(opt.patch && this.password === undefined)) {
await this.generateHash()
}
}
async $beforeInsert(context) {
await super.$beforeInsert(context)
this.createdAt = new Date().toISOString()
this.updatedAt = new Date().toISOString()
await this.generateHash()
}
async generateHash() {
if (this.password) {
if (bcryptRegexp.test(this.password)) { return }
this.password = await bcrypt.hash(this.password, 12)
}
}
async verifyPassword(pwd) {
if (await bcrypt.compare(this.password, pwd) === true) {
return true
} else {
throw new WIKI.Error.AuthLoginFailed()
}
}
async enableTFA() {
let tfaInfo = tfa.generateSecret({
name: WIKI.config.site.title
})
return this.$query.patch({
tfaIsActive: true,
tfaSecret: tfaInfo.secret
})
}
async disableTFA() {
return this.$query.patch({
tfaIsActive: false,
tfaSecret: ''
})
}
async verifyTFA(code) {
let result = tfa.verifyToken(this.tfaSecret, code)
return (result && _.has(result, 'delta') && result.delta === 0)
}
static async processProfile(profile) {
let primaryEmail = ''
if (_.isArray(profile.emails)) {
let e = _.find(profile.emails, ['primary', true])
primaryEmail = (e) ? e.value : _.first(profile.emails).value
} else if (_.isString(profile.email) && profile.email.length > 5) {
primaryEmail = profile.email
} else if (_.isString(profile.mail) && profile.mail.length > 5) {
primaryEmail = profile.mail
} else if (profile.user && profile.user.email && profile.user.email.length > 5) {
primaryEmail = profile.user.email
} else {
return Promise.reject(new Error(WIKI.lang.t('auth:errors.invaliduseremail')))
}
profile.provider = _.lowerCase(profile.provider)
primaryEmail = _.toLower(primaryEmail)
let user = await WIKI.db.users.query().findOne({
email: primaryEmail,
provider: profile.provider
})
if (user) {
user.$query().patchAdnFetch({
email: primaryEmail,
provider: profile.provider,
providerId: profile.id,
name: profile.displayName || _.split(primaryEmail, '@')[0]
})
} else {
user = await WIKI.db.users.query().insertAndFetch({
email: primaryEmail,
provider: profile.provider,
providerId: profile.id,
name: profile.displayName || _.split(primaryEmail, '@')[0]
})
}
// Handle unregistered accounts
// if (!user && profile.provider !== 'local' && (WIKI.config.auth.defaultReadAccess || profile.provider === 'ldap' || profile.provider === 'azure')) {
// let nUsr = {
// email: primaryEmail,
// provider: profile.provider,
// providerId: profile.id,
// password: '',
// name: profile.displayName || profile.name || profile.cn,
// rights: [{
// role: 'read',
// path: '/',
// exact: false,
// deny: false
// }]
// }
// return WIKI.db.users.query().insert(nUsr)
// }
return user
}
static async login (opts, context) {
if (_.has(WIKI.config.auth.strategies, opts.provider)) {
_.set(context.req, 'body.email', opts.username)
_.set(context.req, 'body.password', opts.password)
// Authenticate
return new Promise((resolve, reject) => {
WIKI.auth.passport.authenticate(opts.provider, async (err, user, info) => {
if (err) { return reject(err) }
if (!user) { return reject(new WIKI.Error.AuthLoginFailed()) }
// Is 2FA required?
if (user.tfaIsActive) {
try {
let loginToken = await securityHelper.generateToken(32)
await WIKI.redis.set(`tfa:${loginToken}`, user.id, 'EX', 600)
return resolve({
tfaRequired: true,
tfaLoginToken: loginToken
})
} catch (err) {
WIKI.logger.warn(err)
return reject(new WIKI.Error.AuthGenericError())
}
} else {
// No 2FA, log in user
return context.req.logIn(user, err => {
if (err) { return reject(err) }
resolve({
tfaRequired: false
})
})
}
})(context.req, context.res, () => {})
})
} else {
throw new WIKI.Error.AuthProviderInvalid()
}
}
static async loginTFA(opts, context) {
if (opts.securityCode.length === 6 && opts.loginToken.length === 64) {
let result = await WIKI.redis.get(`tfa:${opts.loginToken}`)
if (result) {
let userId = _.toSafeInteger(result)
if (userId && userId > 0) {
let user = await WIKI.db.users.query().findById(userId)
if (user && user.verifyTFA(opts.securityCode)) {
return Promise.fromCallback(clb => {
context.req.logIn(user, clb)
}).return({
succeeded: true,
message: 'Login Successful'
}).catch(err => {
WIKI.logger.warn(err)
throw new WIKI.Error.AuthGenericError()
})
} else {
throw new WIKI.Error.AuthTFAFailed()
}
}
}
}
throw new WIKI.Error.AuthTFAInvalid()
}
}

View File

@ -0,0 +1,11 @@
exports.seed = (knex, Promise) => {
return knex('settings')
.insert([
{ key: 'auth', value: {} },
{ key: 'features', value: {} },
{ key: 'logging', value: {} },
{ key: 'site', value: {} },
{ key: 'theme', value: {} },
{ key: 'uploads', value: {} }
])
}

View File

@ -32,7 +32,7 @@ module.exports = {
AuthenticationMutation: {
async login(obj, args, context) {
try {
let authResult = await WIKI.db.User.login(args, context)
let authResult = await WIKI.db.users.login(args, context)
return {
...authResult,
responseResult: graphHelper.generateSuccess('Login success')
@ -43,7 +43,7 @@ module.exports = {
},
async loginTFA(obj, args, context) {
try {
let authResult = await WIKI.db.User.loginTFA(args, context)
let authResult = await WIKI.db.users.loginTFA(args, context)
return {
...authResult,
responseResult: graphHelper.generateSuccess('TFA success')

View File

@ -13,43 +13,32 @@ module.exports = {
},
GroupQuery: {
async list(obj, args, context, info) {
return WIKI.db.Group.findAll({
attributes: {
include: [[WIKI.db.inst.fn('COUNT', WIKI.db.inst.col('users.id')), 'userCount']]
},
include: [{
model: WIKI.db.User,
attributes: [],
through: {
attributes: []
}
}],
raw: true,
// TODO: Figure out how to exclude these extra fields...
group: ['group.id', 'users->userGroups.createdAt', 'users->userGroups.updatedAt', 'users->userGroups.version', 'users->userGroups.userId', 'users->userGroups.groupId']
})
return WIKI.db.groups.query().select(
'groups.*',
WIKI.db.groups.relatedQuery('users').count().as('userCount')
)
},
async single(obj, args, context, info) {
return WIKI.db.Group.findById(args.id)
return WIKI.db.groups.query().findById(args.id)
}
},
GroupMutation: {
async assignUser(obj, args) {
const grp = await WIKI.db.Group.findById(args.groupId)
const grp = await WIKI.db.groups.query().findById(args.groupId)
if (!grp) {
throw new gql.GraphQLError('Invalid Group ID')
}
const usr = await WIKI.db.User.findById(args.userId)
const usr = await WIKI.db.users.query().findById(args.userId)
if (!usr) {
throw new gql.GraphQLError('Invalid User ID')
}
await grp.addUser(usr)
await grp.$relatedQuery('users').relate(usr.id)
return {
responseResult: graphHelper.generateSuccess('User has been assigned to group.')
}
},
async create(obj, args) {
const group = await WIKI.db.Group.create({
const group = await WIKI.db.groups.query().insertAndFetch({
name: args.name
})
return {
@ -58,36 +47,27 @@ module.exports = {
}
},
async delete(obj, args) {
await WIKI.db.Group.destroy({
where: {
id: args.id
},
limit: 1
})
await WIKI.db.groups.query().deleteById(args.id)
return {
responseResult: graphHelper.generateSuccess('Group has been deleted.')
}
},
async unassignUser(obj, args) {
const grp = await WIKI.db.Group.findById(args.groupId)
const grp = await WIKI.db.groups.query().findById(args.groupId)
if (!grp) {
throw new gql.GraphQLError('Invalid Group ID')
}
const usr = await WIKI.db.User.findById(args.userId)
const usr = await WIKI.db.users.query().findById(args.userId)
if (!usr) {
throw new gql.GraphQLError('Invalid User ID')
}
await grp.removeUser(usr)
await grp.$relatedQuery('users').unrelate().where('userId', usr.id)
return {
responseResult: graphHelper.generateSuccess('User has been unassigned from group.')
}
},
async update(obj, args) {
await WIKI.db.Group.update({
name: args.name
}, {
where: { id: args.id }
})
await WIKI.db.groups.query().patch({ name: args.name }).where('id', args.id)
return {
responseResult: graphHelper.generateSuccess('Group has been updated.')
}
@ -95,7 +75,7 @@ module.exports = {
},
Group: {
users(grp) {
return grp.getUsers()
return grp.$relatedQuery('users')
}
}
}

View File

@ -13,12 +13,7 @@ module.exports = {
LocalizationQuery: {
async locales(obj, args, context, info) {
let remoteLocales = await WIKI.redis.get('locales')
let localLocales = await WIKI.db.Locale.findAll({
attributes: {
exclude: ['strings']
},
raw: true
})
let localLocales = await WIKI.db.locales.query().select('id', 'code', 'isRTL', 'name', 'nativeName', 'createdAt', 'updatedAt')
remoteLocales = (remoteLocales) ? JSON.parse(remoteLocales) : localLocales
return _.map(remoteLocales, rl => {
let isInstalled = _.some(localLocales, ['code', rl.code])

View File

@ -10,7 +10,8 @@ const path = require('path')
const dbTypes = {
mysql: 'MySQL / MariaDB',
postgres: 'PostgreSQL',
sqlite: 'SQLite'
sqlite: 'SQLite',
mssql: 'MS SQL Server'
}
module.exports = {
@ -28,12 +29,14 @@ module.exports = {
osLabel = `${os.type()} - ${osInfo.dist} (${osInfo.codename || os.platform()}) ${osInfo.release || os.release()} ${os.arch()}`
}
console.info(WIKI.db.knex.client)
return {
configFile: path.join(process.cwd(), 'config.yml'),
currentVersion: WIKI.version,
dbType: _.get(dbTypes, WIKI.config.db.type, 'Unknown DB'),
dbVersion: WIKI.db.inst.options.databaseVersion,
dbHost: WIKI.db.inst.options.host,
dbVersion: _.get(WIKI.db, 'knex.client.version', 'Unknown version'),
dbHost: WIKI.config.db.host,
latestVersion: WIKI.version, // TODO
latestVersionReleaseDate: new Date(), // TODO
operatingSystem: osLabel,

View File

@ -10,52 +10,35 @@ module.exports = {
},
UserQuery: {
async list(obj, args, context, info) {
return WIKI.db.User.findAll({
attributes: {
exclude: ['password', 'tfaSecret']
},
raw: true
})
return WIKI.db.users.query()
.select('id', 'email', 'name', 'provider', 'role', 'createdAt', 'updatedAt')
},
async search(obj, args, context, info) {
return WIKI.db.User.findAll({
where: {
$or: [
{ email: { $like: `%${args.query}%` } },
{ name: { $like: `%${args.query}%` } }
]
},
limit: 10,
attributes: ['id', 'email', 'name', 'provider', 'role', 'createdAt', 'updatedAt'],
raw: true
})
return WIKI.db.users.query()
.where('email', 'like', `%${args.query}%`)
.orWhere('name', 'like', `%${args.query}%`)
.limit(10)
.select('id', 'email', 'name', 'provider', 'role', 'createdAt', 'updatedAt')
},
async single(obj, args, context, info) {
return WIKI.db.User.findById(args.id)
return WIKI.db.users.query().findById(args.id)
}
},
UserMutation: {
create(obj, args) {
return WIKI.db.User.create(args)
return WIKI.db.users.query().insertAndFetch(args)
},
delete(obj, args) {
return WIKI.db.User.destroy({
where: {
id: args.id
},
limit: 1
})
return WIKI.db.users.query().deleteById(args.id)
},
update(obj, args) {
return WIKI.db.User.update({
return WIKI.db.users.query().patch({
email: args.email,
name: args.name,
provider: args.provider,
providerId: args.providerId,
role: args.role
}, {
where: { id: args.id }
})
}).where('id', args.id)
},
resetPassword(obj, args) {
return false

View File

@ -38,7 +38,8 @@ module.exports = async (job) => {
const locales = await WIKI.redis.get('locales')
if (locales) {
const currentLocale = _.find(JSON.parse(locales), ['code', job.data.locale]) || {}
await WIKI.db.Locale.upsert({
await WIKI.db.locales.query().delete().where('code', job.data.locale)
await WIKI.db.locales.query().insert({
code: job.data.locale,
strings: lcObj,
isRTL: currentLocale.isRTL,

View File

@ -60,13 +60,13 @@ module.exports = async (job) => {
_.set(lcObj, row.key.replace(':', '.'), row.value)
})
await WIKI.db.Locale.upsert({
await WIKI.db.locales.query().update({
code: WIKI.config.site.lang,
strings: lcObj,
isRTL: currentLocale.isRTL,
name: currentLocale.name,
nativeName: currentLocale.nativeName
})
}).where('code', WIKI.config.site.lang)
}
WIKI.logger.info('Syncing locales with Graph endpoint: [ COMPLETED ]')

View File

@ -1,210 +0,0 @@
/* global WIKI */
const Promise = require('bluebird')
const bcrypt = require('bcryptjs-then')
const _ = require('lodash')
const tfa = require('node-2fa')
const securityHelper = require('../helpers/security')
/**
* Users schema
*/
module.exports = (sequelize, DataTypes) => {
let userSchema = sequelize.define('user', {
email: {
type: DataTypes.STRING,
allowNull: false,
validate: {
isEmail: true
}
},
provider: {
type: DataTypes.STRING,
allowNull: false
},
providerId: {
type: DataTypes.STRING,
allowNull: true
},
password: {
type: DataTypes.STRING,
allowNull: true
},
name: {
type: DataTypes.STRING,
allowNull: true
},
role: {
type: DataTypes.ENUM('admin', 'user', 'guest'),
allowNull: false
},
tfaIsActive: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: false
},
tfaSecret: {
type: DataTypes.STRING,
allowNull: true
}
}, {
timestamps: true,
version: true,
indexes: [
{
unique: true,
fields: ['provider', 'email']
}
]
})
userSchema.prototype.validatePassword = async function (rawPwd) {
if (await bcrypt.compare(rawPwd, this.password) === true) {
return true
} else {
throw new WIKI.Error.AuthLoginFailed()
}
}
userSchema.prototype.enableTFA = async function () {
let tfaInfo = tfa.generateSecret({
name: WIKI.config.site.title
})
this.tfaIsActive = true
this.tfaSecret = tfaInfo.secret
return this.save()
}
userSchema.prototype.disableTFA = async function () {
this.tfaIsActive = false
this.tfaSecret = ''
return this.save()
}
userSchema.prototype.verifyTFA = function (code) {
let result = tfa.verifyToken(this.tfaSecret, code)
return (result && _.has(result, 'delta') && result.delta === 0)
}
userSchema.login = async (opts, context) => {
if (_.has(WIKI.config.auth.strategies, opts.provider)) {
_.set(context.req, 'body.email', opts.username)
_.set(context.req, 'body.password', opts.password)
// Authenticate
return new Promise((resolve, reject) => {
WIKI.auth.passport.authenticate(opts.provider, async (err, user, info) => {
if (err) { return reject(err) }
if (!user) { return reject(new WIKI.Error.AuthLoginFailed()) }
// Is 2FA required?
if (user.tfaIsActive) {
try {
let loginToken = await securityHelper.generateToken(32)
await WIKI.redis.set(`tfa:${loginToken}`, user.id, 'EX', 600)
return resolve({
tfaRequired: true,
tfaLoginToken: loginToken
})
} catch (err) {
WIKI.logger.warn(err)
return reject(new WIKI.Error.AuthGenericError())
}
} else {
// No 2FA, log in user
return context.req.logIn(user, err => {
if (err) { return reject(err) }
resolve({
tfaRequired: false
})
})
}
})(context.req, context.res, () => {})
})
} else {
throw new WIKI.Error.AuthProviderInvalid()
}
}
userSchema.loginTFA = async (opts, context) => {
if (opts.securityCode.length === 6 && opts.loginToken.length === 64) {
let result = await WIKI.redis.get(`tfa:${opts.loginToken}`)
if (result) {
let userId = _.toSafeInteger(result)
if (userId && userId > 0) {
let user = await WIKI.db.User.findById(userId)
if (user && user.verifyTFA(opts.securityCode)) {
return Promise.fromCallback(clb => {
context.req.logIn(user, clb)
}).return({
succeeded: true,
message: 'Login Successful'
}).catch(err => {
WIKI.logger.warn(err)
throw new WIKI.Error.AuthGenericError()
})
} else {
throw new WIKI.Error.AuthTFAFailed()
}
}
}
}
throw new WIKI.Error.AuthTFAInvalid()
}
userSchema.processProfile = (profile) => {
let primaryEmail = ''
if (_.isArray(profile.emails)) {
let e = _.find(profile.emails, ['primary', true])
primaryEmail = (e) ? e.value : _.first(profile.emails).value
} else if (_.isString(profile.email) && profile.email.length > 5) {
primaryEmail = profile.email
} else if (_.isString(profile.mail) && profile.mail.length > 5) {
primaryEmail = profile.mail
} else if (profile.user && profile.user.email && profile.user.email.length > 5) {
primaryEmail = profile.user.email
} else {
return Promise.reject(new Error(WIKI.lang.t('auth:errors.invaliduseremail')))
}
profile.provider = _.lowerCase(profile.provider)
primaryEmail = _.toLower(primaryEmail)
return WIKI.db.User.findOneAndUpdate({
email: primaryEmail,
provider: profile.provider
}, {
email: primaryEmail,
provider: profile.provider,
providerId: profile.id,
name: profile.displayName || _.split(primaryEmail, '@')[0]
}, {
new: true
}).then((user) => {
// Handle unregistered accounts
if (!user && profile.provider !== 'local' && (WIKI.config.auth.defaultReadAccess || profile.provider === 'ldap' || profile.provider === 'azure')) {
let nUsr = {
email: primaryEmail,
provider: profile.provider,
providerId: profile.id,
password: '',
name: profile.displayName || profile.name || profile.cn,
rights: [{
role: 'read',
path: '/',
exact: false,
deny: false
}]
}
return WIKI.db.User.create(nUsr)
}
return user || Promise.reject(new Error(WIKI.lang.t('auth:errors:notyetauthorized')))
})
}
userSchema.hashPassword = (rawPwd) => {
return bcrypt.hash(rawPwd)
}
return userSchema
}

View File

@ -19,7 +19,7 @@ module.exports = {
clientSecret: conf.clientSecret,
callbackURL: conf.callbackURL
}, function (accessToken, refreshToken, profile, cb) {
WIKI.db.User.processProfile(profile).then((user) => {
WIKI.db.users.processProfile(profile).then((user) => {
return cb(null, user) || true
}).catch((err) => {
return cb(err, null) || true

View File

@ -24,7 +24,7 @@ module.exports = {
let waadProfile = jwt.decode(params.id_token)
waadProfile.id = waadProfile.oid
waadProfile.provider = 'azure'
WIKI.db.User.processProfile(waadProfile).then((user) => {
WIKI.db.users.processProfile(waadProfile).then((user) => {
return cb(null, user) || true
}).catch((err) => {
return cb(err, null) || true

View File

@ -19,7 +19,7 @@ module.exports = {
callbackURL: conf.callbackURL,
scope: 'identify email'
}, function (accessToken, refreshToken, profile, cb) {
WIKI.db.User.processProfile(profile).then((user) => {
WIKI.db.users.processProfile(profile).then((user) => {
return cb(null, user) || true
}).catch((err) => {
return cb(err, null) || true

View File

@ -19,7 +19,7 @@ module.exports = {
clientSecret: conf.clientSecret,
callbackURL: conf.callbackURL
}, (accessToken, refreshToken, profile, cb) => {
WIKI.db.User.processProfile(profile).then((user) => {
WIKI.db.users.processProfile(profile).then((user) => {
return cb(null, user) || true
}).catch((err) => {
return cb(err, null) || true

View File

@ -19,7 +19,7 @@ module.exports = {
callbackURL: conf.callbackURL,
profileFields: ['id', 'displayName', 'email']
}, function (accessToken, refreshToken, profile, cb) {
WIKI.db.User.processProfile(profile).then((user) => {
WIKI.db.users.processProfile(profile).then((user) => {
return cb(null, user) || true
}).catch((err) => {
return cb(err, null) || true

View File

@ -19,7 +19,7 @@ module.exports = {
callbackURL: conf.callbackURL,
scope: ['user:email']
}, (accessToken, refreshToken, profile, cb) => {
WIKI.db.User.processProfile(profile).then((user) => {
WIKI.db.users.processProfile(profile).then((user) => {
return cb(null, user) || true
}).catch((err) => {
return cb(err, null) || true

View File

@ -18,7 +18,7 @@ module.exports = {
clientSecret: conf.clientSecret,
callbackURL: conf.callbackURL
}, (accessToken, refreshToken, profile, cb) => {
WIKI.db.User.processProfile(profile).then((user) => {
WIKI.db.users.processProfile(profile).then((user) => {
return cb(null, user) || true
}).catch((err) => {
return cb(err, null) || true

View File

@ -33,7 +33,7 @@ module.exports = {
}, (profile, cb) => {
profile.provider = 'ldap'
profile.id = profile.dn
WIKI.db.User.processProfile(profile).then((user) => {
WIKI.db.users.processProfile(profile).then((user) => {
return cb(null, user) || true
}).catch((err) => {
return cb(err, null) || true

View File

@ -17,14 +17,12 @@ module.exports = {
usernameField: 'email',
passwordField: 'password'
}, (uEmail, uPassword, done) => {
WIKI.db.User.findOne({
where: {
email: uEmail,
provider: 'local'
}
WIKI.db.users.query().findOne({
email: uEmail,
provider: 'local'
}).then((user) => {
if (user) {
return user.validatePassword(uPassword).then(() => {
return user.verifyPassword(uPassword).then(() => {
return done(null, user) || true
}).catch((err) => {
return done(err, null)

View File

@ -18,7 +18,7 @@ module.exports = {
clientSecret: conf.clientSecret,
callbackURL: conf.callbackURL
}, function (accessToken, refreshToken, profile, cb) {
WIKI.db.User.processProfile(profile).then((user) => {
WIKI.db.users.processProfile(profile).then((user) => {
return cb(null, user) || true
}).catch((err) => {
return cb(err, null) || true

View File

@ -20,7 +20,7 @@ module.exports = {
clientSecret: conf.clientSecret,
callbackURL: conf.callbackURL
}, (accessToken, refreshToken, profile, cb) => {
WIKI.db.User.processProfile(profile).then((user) => {
WIKI.db.users.processProfile(profile).then((user) => {
return cb(null, user) || true
}).catch((err) => {
return cb(err, null) || true

View File

@ -18,7 +18,7 @@ module.exports = {
clientSecret: conf.clientSecret,
callbackURL: conf.callbackURL
}, (accessToken, refreshToken, profile, cb) => {
WIKI.db.User.processProfile(profile).then((user) => {
WIKI.db.users.processProfile(profile).then((user) => {
return cb(null, user) || true
}).catch((err) => {
return cb(err, null) || true

View File

@ -19,7 +19,7 @@ module.exports = {
callbackURL: conf.callbackURL,
scope: 'user_read'
}, function (accessToken, refreshToken, profile, cb) {
WIKI.db.User.processProfile(profile).then((user) => {
WIKI.db.users.processProfile(profile).then((user) => {
return cb(null, user) || true
}).catch((err) => {
return cb(err, null) || true

View File

@ -5,7 +5,7 @@ const path = require('path')
module.exports = () => {
WIKI.config.site = {
path: '',
title: 'WIKI.js'
title: 'Wiki.js'
}
WIKI.system = require('./core/system')
@ -298,22 +298,46 @@ module.exports = () => {
// Save config to DB
WIKI.logger.info('Persisting config to DB...')
await WIKI.configSvc.saveToDb()
await WIKI.db.settings.query().insert([
{ key: 'auth', value: WIKI.config.auth },
{ key: 'features', value: WIKI.config.features },
{ key: 'logging', value: WIKI.config.logging },
{ key: 'site', value: WIKI.config.site },
{ key: 'theme', value: WIKI.config.theme },
{ key: 'uploads', value: WIKI.config.uploads }
])
// Create root administrator
WIKI.logger.info('Creating root administrator...')
await WIKI.db.User.upsert({
await WIKI.db.users.query().insert({
email: req.body.adminEmail,
provider: 'local',
password: await WIKI.db.User.hashPassword(req.body.adminPassword),
password: req.body.adminPassword,
name: 'Administrator',
role: 'admin',
tfaIsActive: false
})
// Create Guest account
WIKI.logger.info('Creating root administrator...')
const guestUsr = await WIKI.db.users.query().findOne({
provider: 'local',
email: 'guest@example.com'
})
if (!guestUsr) {
await WIKI.db.users.query().insert({
provider: 'local',
email: 'guest@example.com',
name: 'Guest',
password: '',
role: 'guest',
tfaIsActive: false
})
}
// Create default locale
WIKI.logger.info('Installing default locale...')
await WIKI.db.Locale.upsert({
await WIKI.db.locales.query().insert({
code: 'en',
strings: require('./locales/default.json'),
isRTL: false,
@ -330,7 +354,7 @@ module.exports = () => {
WIKI.logger.info('Stopping Setup...')
WIKI.server.destroy(() => {
WIKI.logger.info('Setup stopped. Starting WIKI.js...')
WIKI.logger.info('Setup stopped. Starting Wiki.js...')
_.delay(() => {
WIKI.kernel.bootMaster()
}, 1000)

1103
yarn.lock

File diff suppressed because it is too large Load Diff