/* 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 }