| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933 | 
							- /* global WIKI */
 
- const bcrypt = require('bcryptjs-then')
 
- const _ = require('lodash')
 
- const tfa = require('node-2fa')
 
- const jwt = require('jsonwebtoken')
 
- const Model = require('objection').Model
 
- const validate = require('validate.js')
 
- const qr = require('qr-image')
 
- 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'],
 
-       properties: {
 
-         id: {type: 'integer'},
 
-         email: {type: 'string', format: 'email'},
 
-         name: {type: 'string', minLength: 1, maxLength: 255},
 
-         providerId: {type: 'string'},
 
-         password: {type: 'string'},
 
-         tfaIsActive: {type: 'boolean', default: false},
 
-         tfaSecret: {type: ['string', null]},
 
-         jobTitle: {type: 'string'},
 
-         location: {type: 'string'},
 
-         pictureUrl: {type: 'string'},
 
-         isSystem: {type: 'boolean'},
 
-         isActive: {type: 'boolean'},
 
-         isVerified: {type: 'boolean'},
 
-         createdAt: {type: 'string'},
 
-         updatedAt: {type: 'string'}
 
-       }
 
-     }
 
-   }
 
-   static get relationMappings() {
 
-     return {
 
-       groups: {
 
-         relation: Model.ManyToManyRelation,
 
-         modelClass: require('./groups'),
 
-         join: {
 
-           from: 'users.id',
 
-           through: {
 
-             from: 'userGroups.userId',
 
-             to: 'userGroups.groupId'
 
-           },
 
-           to: 'groups.id'
 
-         }
 
-       },
 
-       provider: {
 
-         relation: Model.BelongsToOneRelation,
 
-         modelClass: require('./authentication'),
 
-         join: {
 
-           from: 'users.providerKey',
 
-           to: 'authentication.key'
 
-         }
 
-       },
 
-       defaultEditor: {
 
-         relation: Model.BelongsToOneRelation,
 
-         modelClass: require('./editors'),
 
-         join: {
 
-           from: 'users.editorKey',
 
-           to: 'editors.key'
 
-         }
 
-       },
 
-       locale: {
 
-         relation: Model.BelongsToOneRelation,
 
-         modelClass: require('./locales'),
 
-         join: {
 
-           from: 'users.localeCode',
 
-           to: 'locales.code'
 
-         }
 
-       }
 
-     }
 
-   }
 
-   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()
 
-   }
 
-   // ------------------------------------------------
 
-   // Instance Methods
 
-   // ------------------------------------------------
 
-   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(pwd, this.password) === true) {
 
-       return true
 
-     } else {
 
-       throw new WIKI.Error.AuthLoginFailed()
 
-     }
 
-   }
 
-   async generateTFA() {
 
-     let tfaInfo = tfa.generateSecret({
 
-       name: WIKI.config.title,
 
-       account: this.email
 
-     })
 
-     await WIKI.models.users.query().findById(this.id).patch({
 
-       tfaIsActive: false,
 
-       tfaSecret: tfaInfo.secret
 
-     })
 
-     const safeTitle = WIKI.config.title.replace(/[\s-.,=!@#$%?&*()+[\]{}/\\;<>]/g, '')
 
-     return qr.imageSync(`otpauth://totp/${safeTitle}:${this.email}?secret=${tfaInfo.secret}`, { type: 'svg' })
 
-   }
 
-   async enableTFA() {
 
-     return WIKI.models.users.query().findById(this.id).patch({
 
-       tfaIsActive: true
 
-     })
 
-   }
 
-   async disableTFA() {
 
-     return this.$query.patch({
 
-       tfaIsActive: false,
 
-       tfaSecret: ''
 
-     })
 
-   }
 
-   verifyTFA(code) {
 
-     let result = tfa.verifyToken(this.tfaSecret, code)
 
-     return (result && _.has(result, 'delta') && result.delta === 0)
 
-   }
 
-   getGlobalPermissions() {
 
-     return _.uniq(_.flatten(_.map(this.groups, 'permissions')))
 
-   }
 
-   getGroups() {
 
-     return _.uniq(_.map(this.groups, 'id'))
 
-   }
 
-   // ------------------------------------------------
 
-   // Model Methods
 
-   // ------------------------------------------------
 
-   static async processProfile({ profile, providerKey }) {
 
-     const provider = _.get(WIKI.auth.strategies, providerKey, {})
 
-     provider.info = _.find(WIKI.data.authentication, ['key', provider.stategyKey])
 
-     // Find existing user
 
-     let user = await WIKI.models.users.query().findOne({
 
-       providerId: _.toString(profile.id),
 
-       providerKey
 
-     })
 
-     // Parse email
 
-     let primaryEmail = ''
 
-     if (_.isArray(profile.emails)) {
 
-       const e = _.find(profile.emails, ['primary', true])
 
-       primaryEmail = (e) ? e.value : _.first(profile.emails).value
 
-     } else if (_.isArray(profile.email)) {
 
-       primaryEmail = _.first(_.flattenDeep([profile.email]))
 
-     } 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 {
 
-       throw new Error('Missing or invalid email address from profile.')
 
-     }
 
-     primaryEmail = _.toLower(primaryEmail)
 
-     // Find pending social user
 
-     if (!user) {
 
-       user = await WIKI.models.users.query().findOne({
 
-         email: primaryEmail,
 
-         providerId: null,
 
-         providerKey
 
-       })
 
-       if (user) {
 
-         user = await user.$query().patchAndFetch({
 
-           providerId: _.toString(profile.id)
 
-         })
 
-       }
 
-     }
 
-     // Parse display name
 
-     let displayName = ''
 
-     if (_.isString(profile.displayName) && profile.displayName.length > 0) {
 
-       displayName = profile.displayName
 
-     } else if (_.isString(profile.name) && profile.name.length > 0) {
 
-       displayName = profile.name
 
-     } else {
 
-       displayName = primaryEmail.split('@')[0]
 
-     }
 
-     // Parse picture URL / Data
 
-     let pictureUrl = ''
 
-     if (profile.picture && Buffer.isBuffer(profile.picture)) {
 
-       pictureUrl = 'internal'
 
-     } else {
 
-       pictureUrl = _.truncate(_.get(profile, 'picture', _.get(user, 'pictureUrl', null)), {
 
-         length: 255,
 
-         omission: ''
 
-       })
 
-     }
 
-     // Update existing user
 
-     if (user) {
 
-       if (!user.isActive) {
 
-         throw new WIKI.Error.AuthAccountBanned()
 
-       }
 
-       if (user.isSystem) {
 
-         throw new Error('This is a system reserved account and cannot be used.')
 
-       }
 
-       user = await user.$query().patchAndFetch({
 
-         email: primaryEmail,
 
-         name: displayName,
 
-         pictureUrl: pictureUrl
 
-       })
 
-       if (pictureUrl === 'internal') {
 
-         await WIKI.models.users.updateUserAvatarData(user.id, profile.picture)
 
-       }
 
-       return user
 
-     }
 
-     // Self-registration
 
-     if (provider.selfRegistration) {
 
-       // Check if email domain is whitelisted
 
-       if (_.get(provider, 'domainWhitelist', []).length > 0) {
 
-         const emailDomain = _.last(primaryEmail.split('@'))
 
-         if (!_.includes(provider.domainWhitelist, emailDomain)) {
 
-           throw new WIKI.Error.AuthRegistrationDomainUnauthorized()
 
-         }
 
-       }
 
-       // Create account
 
-       user = await WIKI.models.users.query().insertAndFetch({
 
-         providerKey: providerKey,
 
-         providerId: _.toString(profile.id),
 
-         email: primaryEmail,
 
-         name: displayName,
 
-         pictureUrl: pictureUrl,
 
-         localeCode: WIKI.config.lang.code,
 
-         defaultEditor: 'markdown',
 
-         tfaIsActive: false,
 
-         isSystem: false,
 
-         isActive: true,
 
-         isVerified: true
 
-       })
 
-       // Assign to group(s)
 
-       if (provider.autoEnrollGroups.length > 0) {
 
-         await user.$relatedQuery('groups').relate(provider.autoEnrollGroups)
 
-       }
 
-       if (pictureUrl === 'internal') {
 
-         await WIKI.models.users.updateUserAvatarData(user.id, profile.picture)
 
-       }
 
-       return user
 
-     }
 
-     throw new Error('You are not authorized to login.')
 
-   }
 
-   /**
 
-    * Login a user
 
-    */
 
-   static async login (opts, context) {
 
-     if (_.has(WIKI.auth.strategies, opts.strategy)) {
 
-       const selStrategy = _.get(WIKI.auth.strategies, opts.strategy)
 
-       if (!selStrategy.isEnabled) {
 
-         throw new WIKI.Error.AuthProviderInvalid()
 
-       }
 
-       const strInfo = _.find(WIKI.data.authentication, ['key', selStrategy.strategyKey])
 
-       // Inject form user/pass
 
-       if (strInfo.useForm) {
 
-         _.set(context.req, 'body.email', opts.username)
 
-         _.set(context.req, 'body.password', opts.password)
 
-         _.set(context.req.params, 'strategy', opts.strategy)
 
-       }
 
-       // Authenticate
 
-       return new Promise((resolve, reject) => {
 
-         WIKI.auth.passport.authenticate(selStrategy.key, {
 
-           session: !strInfo.useForm,
 
-           scope: strInfo.scopes ? strInfo.scopes : null
 
-         }, async (err, user, info) => {
 
-           if (err) { return reject(err) }
 
-           if (!user) { return reject(new WIKI.Error.AuthLoginFailed()) }
 
-           try {
 
-             const resp = await WIKI.models.users.afterLoginChecks(user, context, {
 
-               skipTFA: !strInfo.useForm,
 
-               skipChangePwd: !strInfo.useForm
 
-             })
 
-             resolve(resp)
 
-           } catch (err) {
 
-             reject(err)
 
-           }
 
-         })(context.req, context.res, () => {})
 
-       })
 
-     } else {
 
-       throw new WIKI.Error.AuthProviderInvalid()
 
-     }
 
-   }
 
-   /**
 
-    * Perform post-login checks
 
-    */
 
-   static async afterLoginChecks (user, context, { skipTFA, skipChangePwd } = { skipTFA: false, skipChangePwd: false }) {
 
-     // Get redirect target
 
-     user.groups = await user.$relatedQuery('groups').select('groups.id', 'permissions', 'redirectOnLogin')
 
-     let redirect = '/'
 
-     if (user.groups && user.groups.length > 0) {
 
-       for (const grp of user.groups) {
 
-         if (!_.isEmpty(grp.redirectOnLogin) && grp.redirectOnLogin !== '/') {
 
-           redirect = grp.redirectOnLogin
 
-           break
 
-         }
 
-       }
 
-     }
 
-     // Is 2FA required?
 
-     if (!skipTFA) {
 
-       if (user.tfaIsActive && user.tfaSecret) {
 
-         try {
 
-           const tfaToken = await WIKI.models.userKeys.generateToken({
 
-             kind: 'tfa',
 
-             userId: user.id
 
-           })
 
-           return {
 
-             mustProvideTFA: true,
 
-             continuationToken: tfaToken,
 
-             redirect
 
-           }
 
-         } catch (errc) {
 
-           WIKI.logger.warn(errc)
 
-           throw new WIKI.Error.AuthGenericError()
 
-         }
 
-       } else if (WIKI.config.auth.enforce2FA || (user.tfaIsActive && !user.tfaSecret)) {
 
-         try {
 
-           const tfaQRImage = await user.generateTFA()
 
-           const tfaToken = await WIKI.models.userKeys.generateToken({
 
-             kind: 'tfaSetup',
 
-             userId: user.id
 
-           })
 
-           return {
 
-             mustSetupTFA: true,
 
-             continuationToken: tfaToken,
 
-             tfaQRImage,
 
-             redirect
 
-           }
 
-         } catch (errc) {
 
-           WIKI.logger.warn(errc)
 
-           throw new WIKI.Error.AuthGenericError()
 
-         }
 
-       }
 
-     }
 
-     // Must Change Password?
 
-     if (!skipChangePwd && user.mustChangePwd) {
 
-       try {
 
-         const pwdChangeToken = await WIKI.models.userKeys.generateToken({
 
-           kind: 'changePwd',
 
-           userId: user.id
 
-         })
 
-         return {
 
-           mustChangePwd: true,
 
-           continuationToken: pwdChangeToken,
 
-           redirect
 
-         }
 
-       } catch (errc) {
 
-         WIKI.logger.warn(errc)
 
-         throw new WIKI.Error.AuthGenericError()
 
-       }
 
-     }
 
-     return new Promise((resolve, reject) => {
 
-       context.req.login(user, { session: false }, async errc => {
 
-         if (errc) { return reject(errc) }
 
-         const jwtToken = await WIKI.models.users.refreshToken(user)
 
-         resolve({ jwt: jwtToken.token, redirect })
 
-       })
 
-     })
 
-   }
 
-   /**
 
-    * Generate a new token for a user
 
-    */
 
-   static async refreshToken(user) {
 
-     if (_.isSafeInteger(user)) {
 
-       user = await WIKI.models.users.query().findById(user).withGraphFetched('groups').modifyGraph('groups', builder => {
 
-         builder.select('groups.id', 'permissions')
 
-       })
 
-       if (!user) {
 
-         WIKI.logger.warn(`Failed to refresh token for user ${user}: Not found.`)
 
-         throw new WIKI.Error.AuthGenericError()
 
-       }
 
-       if (!user.isActive) {
 
-         WIKI.logger.warn(`Failed to refresh token for user ${user}: Inactive.`)
 
-         throw new WIKI.Error.AuthAccountBanned()
 
-       }
 
-     } else if (_.isNil(user.groups)) {
 
-       user.groups = await user.$relatedQuery('groups').select('groups.id', 'permissions')
 
-     }
 
-     // Update Last Login Date
 
-     // -> Bypass Objection.js to avoid updating the updatedAt field
 
-     await WIKI.models.knex('users').where('id', user.id).update({ lastLoginAt: new Date().toISOString() })
 
-     return {
 
-       token: jwt.sign({
 
-         id: user.id,
 
-         email: user.email,
 
-         name: user.name,
 
-         av: user.pictureUrl,
 
-         tz: user.timezone,
 
-         lc: user.localeCode,
 
-         df: user.dateFormat,
 
-         ap: user.appearance,
 
-         // defaultEditor: user.defaultEditor,
 
-         permissions: user.getGlobalPermissions(),
 
-         groups: user.getGroups()
 
-       }, {
 
-         key: WIKI.config.certs.private,
 
-         passphrase: WIKI.config.sessionSecret
 
-       }, {
 
-         algorithm: 'RS256',
 
-         expiresIn: WIKI.config.auth.tokenExpiration,
 
-         audience: WIKI.config.auth.audience,
 
-         issuer: 'urn:wiki.js'
 
-       }),
 
-       user
 
-     }
 
-   }
 
-   /**
 
-    * Verify a TFA login
 
-    */
 
-   static async loginTFA ({ securityCode, continuationToken, setup }, context) {
 
-     if (securityCode.length === 6 && continuationToken.length > 1) {
 
-       const user = await WIKI.models.userKeys.validateToken({
 
-         kind: setup ? 'tfaSetup' : 'tfa',
 
-         token: continuationToken,
 
-         skipDelete: setup
 
-       })
 
-       if (user) {
 
-         if (user.verifyTFA(securityCode)) {
 
-           if (setup) {
 
-             await user.enableTFA()
 
-           }
 
-           return WIKI.models.users.afterLoginChecks(user, context, { skipTFA: true })
 
-         } else {
 
-           throw new WIKI.Error.AuthTFAFailed()
 
-         }
 
-       }
 
-     }
 
-     throw new WIKI.Error.AuthTFAInvalid()
 
-   }
 
-   /**
 
-    * Change Password from a Mandatory Password Change after Login
 
-    */
 
-   static async loginChangePassword ({ continuationToken, newPassword }, context) {
 
-     if (!newPassword || newPassword.length < 6) {
 
-       throw new WIKI.Error.InputInvalid('Password must be at least 6 characters!')
 
-     }
 
-     const usr = await WIKI.models.userKeys.validateToken({
 
-       kind: 'changePwd',
 
-       token: continuationToken
 
-     })
 
-     if (usr) {
 
-       await WIKI.models.users.query().patch({
 
-         password: newPassword,
 
-         mustChangePwd: false
 
-       }).findById(usr.id)
 
-       return new Promise((resolve, reject) => {
 
-         context.req.logIn(usr, { session: false }, async err => {
 
-           if (err) { return reject(err) }
 
-           const jwtToken = await WIKI.models.users.refreshToken(usr)
 
-           resolve({ jwt: jwtToken.token })
 
-         })
 
-       })
 
-     } else {
 
-       throw new WIKI.Error.UserNotFound()
 
-     }
 
-   }
 
-   /**
 
-    * Send a password reset request
 
-    */
 
-   static async loginForgotPassword ({ email }, context) {
 
-     const usr = await WIKI.models.users.query().where({
 
-       email,
 
-       providerKey: 'local'
 
-     }).first()
 
-     if (!usr) {
 
-       WIKI.logger.debug(`Password reset attempt on nonexistant local account ${email}: [DISCARDED]`)
 
-       return
 
-     }
 
-     const resetToken = await WIKI.models.userKeys.generateToken({
 
-       userId: usr.id,
 
-       kind: 'resetPwd'
 
-     })
 
-     await WIKI.mail.send({
 
-       template: 'accountResetPwd',
 
-       to: email,
 
-       subject: `Password Reset Request`,
 
-       data: {
 
-         preheadertext: `A password reset was requested for ${WIKI.config.title}`,
 
-         title: `A password reset was requested for ${WIKI.config.title}`,
 
-         content: `Click the button below to reset your password. If you didn't request this password reset, simply discard this email.`,
 
-         buttonLink: `${WIKI.config.host}/login-reset/${resetToken}`,
 
-         buttonText: 'Reset Password'
 
-       },
 
-       text: `A password reset was requested for wiki ${WIKI.config.title}. Open the following link to proceed: ${WIKI.config.host}/login-reset/${resetToken}`
 
-     })
 
-   }
 
-   /**
 
-    * Create a new user
 
-    *
 
-    * @param {Object} param0 User Fields
 
-    */
 
-   static async createNewUser ({ providerKey, email, passwordRaw, name, groups, mustChangePassword, sendWelcomeEmail }) {
 
-     // Input sanitization
 
-     email = _.toLower(email)
 
-     // Input validation
 
-     let validation = null
 
-     if (providerKey === 'local') {
 
-       validation = validate({
 
-         email,
 
-         passwordRaw,
 
-         name
 
-       }, {
 
-         email: {
 
-           email: true,
 
-           length: {
 
-             maximum: 255
 
-           }
 
-         },
 
-         passwordRaw: {
 
-           presence: {
 
-             allowEmpty: false
 
-           },
 
-           length: {
 
-             minimum: 6
 
-           }
 
-         },
 
-         name: {
 
-           presence: {
 
-             allowEmpty: false
 
-           },
 
-           length: {
 
-             minimum: 2,
 
-             maximum: 255
 
-           }
 
-         }
 
-       }, { format: 'flat' })
 
-     } else {
 
-       validation = validate({
 
-         email,
 
-         name
 
-       }, {
 
-         email: {
 
-           email: true,
 
-           length: {
 
-             maximum: 255
 
-           }
 
-         },
 
-         name: {
 
-           presence: {
 
-             allowEmpty: false
 
-           },
 
-           length: {
 
-             minimum: 2,
 
-             maximum: 255
 
-           }
 
-         }
 
-       }, { format: 'flat' })
 
-     }
 
-     if (validation && validation.length > 0) {
 
-       throw new WIKI.Error.InputInvalid(validation[0])
 
-     }
 
-     // Check if email already exists
 
-     const usr = await WIKI.models.users.query().findOne({ email, providerKey })
 
-     if (!usr) {
 
-       // Create the account
 
-       let newUsrData = {
 
-         providerKey,
 
-         email,
 
-         name,
 
-         locale: 'en',
 
-         defaultEditor: 'markdown',
 
-         tfaIsActive: false,
 
-         isSystem: false,
 
-         isActive: true,
 
-         isVerified: true,
 
-         mustChangePwd: false
 
-       }
 
-       if (providerKey === `local`) {
 
-         newUsrData.password = passwordRaw
 
-         newUsrData.mustChangePwd = (mustChangePassword === true)
 
-       }
 
-       const newUsr = await WIKI.models.users.query().insert(newUsrData)
 
-       // Assign to group(s)
 
-       if (groups.length > 0) {
 
-         await newUsr.$relatedQuery('groups').relate(groups)
 
-       }
 
-       if (sendWelcomeEmail) {
 
-         // Send welcome email
 
-         await WIKI.mail.send({
 
-           template: 'accountWelcome',
 
-           to: email,
 
-           subject: `Welcome to the wiki ${WIKI.config.title}`,
 
-           data: {
 
-             preheadertext: `You've been invited to the wiki ${WIKI.config.title}`,
 
-             title: `You've been invited to the wiki ${WIKI.config.title}`,
 
-             content: `Click the button below to access the wiki.`,
 
-             buttonLink: `${WIKI.config.host}/login`,
 
-             buttonText: 'Login'
 
-           },
 
-           text: `You've been invited to the wiki ${WIKI.config.title}: ${WIKI.config.host}/login`
 
-         })
 
-       }
 
-     } else {
 
-       throw new WIKI.Error.AuthAccountAlreadyExists()
 
-     }
 
-   }
 
-   /**
 
-    * Update an existing user
 
-    *
 
-    * @param {Object} param0 User ID and fields to update
 
-    */
 
-   static async updateUser ({ id, email, name, newPassword, groups, location, jobTitle, timezone, dateFormat, appearance }) {
 
-     const usr = await WIKI.models.users.query().findById(id)
 
-     if (usr) {
 
-       let usrData = {}
 
-       if (!_.isEmpty(email) && email !== usr.email) {
 
-         const dupUsr = await WIKI.models.users.query().select('id').where({
 
-           email,
 
-           providerKey: usr.providerKey
 
-         }).first()
 
-         if (dupUsr) {
 
-           throw new WIKI.Error.AuthAccountAlreadyExists()
 
-         }
 
-         usrData.email = _.toLower(email)
 
-       }
 
-       if (!_.isEmpty(name) && name !== usr.name) {
 
-         usrData.name = _.trim(name)
 
-       }
 
-       if (!_.isEmpty(newPassword)) {
 
-         if (newPassword.length < 6) {
 
-           throw new WIKI.Error.InputInvalid('Password must be at least 6 characters!')
 
-         }
 
-         usrData.password = newPassword
 
-       }
 
-       if (_.isArray(groups)) {
 
-         const usrGroupsRaw = await usr.$relatedQuery('groups')
 
-         const usrGroups = _.map(usrGroupsRaw, 'id')
 
-         // Relate added groups
 
-         const addUsrGroups = _.difference(groups, usrGroups)
 
-         for (const grp of addUsrGroups) {
 
-           await usr.$relatedQuery('groups').relate(grp)
 
-         }
 
-         // Unrelate removed groups
 
-         const remUsrGroups = _.difference(usrGroups, groups)
 
-         for (const grp of remUsrGroups) {
 
-           await usr.$relatedQuery('groups').unrelate().where('groupId', grp)
 
-         }
 
-       }
 
-       if (!_.isEmpty(location) && location !== usr.location) {
 
-         usrData.location = _.trim(location)
 
-       }
 
-       if (!_.isEmpty(jobTitle) && jobTitle !== usr.jobTitle) {
 
-         usrData.jobTitle = _.trim(jobTitle)
 
-       }
 
-       if (!_.isEmpty(timezone) && timezone !== usr.timezone) {
 
-         usrData.timezone = timezone
 
-       }
 
-       if (!_.isNil(dateFormat) && dateFormat !== usr.dateFormat) {
 
-         usrData.dateFormat = dateFormat
 
-       }
 
-       if (!_.isNil(appearance) && appearance !== usr.appearance) {
 
-         usrData.appearance = appearance
 
-       }
 
-       await WIKI.models.users.query().patch(usrData).findById(id)
 
-     } else {
 
-       throw new WIKI.Error.UserNotFound()
 
-     }
 
-   }
 
-   /**
 
-    * Delete a User
 
-    *
 
-    * @param {*} id User ID
 
-    */
 
-   static async deleteUser (id, replaceId) {
 
-     const usr = await WIKI.models.users.query().findById(id)
 
-     if (usr) {
 
-       await WIKI.models.assets.query().patch({ authorId: replaceId }).where('authorId', id)
 
-       await WIKI.models.comments.query().patch({ authorId: replaceId }).where('authorId', id)
 
-       await WIKI.models.pageHistory.query().patch({ authorId: replaceId }).where('authorId', id)
 
-       await WIKI.models.pages.query().patch({ authorId: replaceId }).where('authorId', id)
 
-       await WIKI.models.pages.query().patch({ creatorId: replaceId }).where('creatorId', id)
 
-       await WIKI.models.userKeys.query().delete().where('userId', id)
 
-       await WIKI.models.users.query().deleteById(id)
 
-     } else {
 
-       throw new WIKI.Error.UserNotFound()
 
-     }
 
-   }
 
-   /**
 
-    * Register a new user (client-side registration)
 
-    *
 
-    * @param {Object} param0 User fields
 
-    * @param {Object} context GraphQL Context
 
-    */
 
-   static async register ({ email, password, name, verify = false, bypassChecks = false }, context) {
 
-     const localStrg = await WIKI.models.authentication.getStrategy('local')
 
-     // Check if self-registration is enabled
 
-     if (localStrg.selfRegistration || bypassChecks) {
 
-       // Input sanitization
 
-       email = _.toLower(email)
 
-       // Input validation
 
-       const validation = validate({
 
-         email,
 
-         password,
 
-         name
 
-       }, {
 
-         email: {
 
-           email: true,
 
-           length: {
 
-             maximum: 255
 
-           }
 
-         },
 
-         password: {
 
-           presence: {
 
-             allowEmpty: false
 
-           },
 
-           length: {
 
-             minimum: 6
 
-           }
 
-         },
 
-         name: {
 
-           presence: {
 
-             allowEmpty: false
 
-           },
 
-           length: {
 
-             minimum: 2,
 
-             maximum: 255
 
-           }
 
-         }
 
-       }, { format: 'flat' })
 
-       if (validation && validation.length > 0) {
 
-         throw new WIKI.Error.InputInvalid(validation[0])
 
-       }
 
-       // Check if email domain is whitelisted
 
-       if (_.get(localStrg, 'domainWhitelist.v', []).length > 0 && !bypassChecks) {
 
-         const emailDomain = _.last(email.split('@'))
 
-         if (!_.includes(localStrg.domainWhitelist.v, emailDomain)) {
 
-           throw new WIKI.Error.AuthRegistrationDomainUnauthorized()
 
-         }
 
-       }
 
-       // Check if email already exists
 
-       const usr = await WIKI.models.users.query().findOne({ email, providerKey: 'local' })
 
-       if (!usr) {
 
-         // Create the account
 
-         const newUsr = await WIKI.models.users.query().insert({
 
-           provider: 'local',
 
-           email,
 
-           name,
 
-           password,
 
-           locale: 'en',
 
-           defaultEditor: 'markdown',
 
-           tfaIsActive: false,
 
-           isSystem: false,
 
-           isActive: true,
 
-           isVerified: false
 
-         })
 
-         // Assign to group(s)
 
-         if (_.get(localStrg, 'autoEnrollGroups.v', []).length > 0) {
 
-           await newUsr.$relatedQuery('groups').relate(localStrg.autoEnrollGroups.v)
 
-         }
 
-         if (verify) {
 
-           // Create verification token
 
-           const verificationToken = await WIKI.models.userKeys.generateToken({
 
-             kind: 'verify',
 
-             userId: newUsr.id
 
-           })
 
-           // Send verification email
 
-           await WIKI.mail.send({
 
-             template: 'accountVerify',
 
-             to: email,
 
-             subject: 'Verify your account',
 
-             data: {
 
-               preheadertext: 'Verify your account in order to gain access to the wiki.',
 
-               title: 'Verify your account',
 
-               content: 'Click the button below in order to verify your account and gain access to the wiki.',
 
-               buttonLink: `${WIKI.config.host}/verify/${verificationToken}`,
 
-               buttonText: 'Verify'
 
-             },
 
-             text: `You must open the following link in your browser to verify your account and gain access to the wiki: ${WIKI.config.host}/verify/${verificationToken}`
 
-           })
 
-         }
 
-         return true
 
-       } else {
 
-         throw new WIKI.Error.AuthAccountAlreadyExists()
 
-       }
 
-     } else {
 
-       throw new WIKI.Error.AuthRegistrationDisabled()
 
-     }
 
-   }
 
-   /**
 
-    * Logout the current user
 
-    */
 
-   static async logout (context) {
 
-     if (!context.req.user || context.req.user.id === WIKI.config.auth.guestUserId) {
 
-       return '/'
 
-     }
 
-     const usr = await WIKI.models.users.query().findById(context.req.user.id).select('providerKey')
 
-     const provider = _.find(WIKI.auth.strategies, ['key', usr.providerKey])
 
-     return provider.logout ? provider.logout(provider.config) : '/'
 
-   }
 
-   static async getGuestUser () {
 
-     const user = await WIKI.models.users.query().findById(WIKI.config.auth.guestUserId).withGraphJoined('groups').modifyGraph('groups', builder => {
 
-       builder.select('groups.id', 'permissions')
 
-     })
 
-     if (!user) {
 
-       WIKI.logger.error('CRITICAL ERROR: Guest user is missing!')
 
-       process.exit(1)
 
-     }
 
-     user.permissions = user.getGlobalPermissions()
 
-     return user
 
-   }
 
-   static async getRootUser () {
 
-     let user = await WIKI.models.users.query().findById(WIKI.config.auth.rootAdminUserId)
 
-     if (!user) {
 
-       WIKI.logger.error('CRITICAL ERROR: Root Administrator user is missing!')
 
-       process.exit(1)
 
-     }
 
-     user.permissions = ['manage:system']
 
-     return user
 
-   }
 
-   /**
 
-    * Add / Update User Avatar Data
 
-    */
 
-   static async updateUserAvatarData (userId, data) {
 
-     try {
 
-       WIKI.logger.debug(`Updating user ${userId} avatar data...`)
 
-       if (data.length > 1024 * 1024) {
 
-         throw new Error('Avatar image filesize is too large. 1MB max.')
 
-       }
 
-       const existing = await WIKI.models.knex('userAvatars').select('id').where('id', userId).first()
 
-       if (existing) {
 
-         await WIKI.models.knex('userAvatars').where({
 
-           id: userId
 
-         }).update({
 
-           data
 
-         })
 
-       } else {
 
-         await WIKI.models.knex('userAvatars').insert({
 
-           id: userId,
 
-           data
 
-         })
 
-       }
 
-     } catch (err) {
 
-       WIKI.logger.warn(`Failed to process binary thumbnail data for user ${userId}: ${err.message}`)
 
-     }
 
-   }
 
-   static async getUserAvatarData (userId) {
 
-     try {
 
-       const usrData = await WIKI.models.knex('userAvatars').where('id', userId).first()
 
-       if (usrData) {
 
-         return usrData.data
 
-       } else {
 
-         return null
 
-       }
 
-     } catch (err) {
 
-       WIKI.logger.warn(`Failed to process binary thumbnail data for user ${userId}`)
 
-     }
 
-   }
 
- }
 
 
  |