| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427 | /* global WIKI */const bcrypt = require('bcryptjs-then')const _ = require('lodash')const tfa = require('node-2fa')const securityHelper = require('../helpers/security')const jwt = require('jsonwebtoken')const Model = require('objection').Modelconst validate = require('validate.js')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},        providerId: {type: 'number'},        password: {type: 'string'},        role: {type: 'string', enum: ['admin', 'guest', 'user']},        tfaIsActive: {type: 'boolean', default: false},        tfaSecret: {type: 'string'},        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 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)  }  getGlobalPermissions() {    return _.uniq(_.flatten(_.map(this.groups, 'permissions')))  }  getGroups() {    return _.uniq(_.map(this.groups, 'id'))  }  // ------------------------------------------------  // Model Methods  // ------------------------------------------------  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.models.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.models.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.models.users.query().insert(nUsr)    // }    return user  }  static async login (opts, context) {    if (_.has(WIKI.auth.strategies, opts.strategy)) {      _.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.strategy, { session: false }, 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, { session: false }, async err => {              if (err) { return reject(err) }              const jwtToken = await WIKI.models.users.refreshToken(user)              resolve({                jwt: jwtToken.token,                tfaRequired: false              })            })          }        })(context.req, context.res, () => {})      })    } else {      throw new WIKI.Error.AuthProviderInvalid()    }  }  static async refreshToken(user) {    if (_.isSafeInteger(user)) {      user = await WIKI.models.users.query().findById(user).eager('groups').modifyEager('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()      }    } else if(_.isNil(user.groups)) {      await user.$relatedQuery('groups').select('groups.id', 'permissions')    }    return {      token: jwt.sign({        id: user.id,        email: user.email,        name: user.name,        pictureUrl: user.pictureUrl,        timezone: user.timezone,        localeCode: user.localeCode,        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    }  }  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.models.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()  }  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        })        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()    }  }  static async getGuestUser () {    const user = await WIKI.models.users.query().findById(2).eager('groups').modifyEager('groups', builder => {      builder.select('groups.id', 'permissions')    })    if (!user) {      WIKI.logger.error('CRITICAL ERROR: Guest user is missing!')      process.exit(1)    }    return user  }}
 |