'use strict' const bcrypt = require('bcrypt') const crs = require('crypto-random-string') const appRoot = require('app-root-path') const knex = require(`${appRoot}/db/knex`) const { saveActivity } = require(`${appRoot}/modules/activitystream/services`) const Users = () => knex('users') const Acl = () => knex('server_acl') const debug = require('debug') const { deleteStream } = require('./streams') const changeUserRole = async ({ userId, role }) => await Acl().where({ userId: userId }).update({ role: role }) const countAdminUsers = async () => { let [{ count }] = await Acl().where({ role: 'server:admin' }).count() return parseInt(count) } const _ensureAtleastOneAdminRemains = async (userId) => { if ((await countAdminUsers()) === 1) { let currentAdmin = await Acl().where({ role: 'server:admin' }).first() if (currentAdmin.userId == userId) { throw new Error('Cannot remove the last admin role from the server') } } } const userByEmailQuery = (email) => Users().whereRaw('lower(email) = lower(?)', [email]) module.exports = { /* Users */ async createUser(user) { user.id = crs({ length: 10 }) user.email = user.email.toLowerCase() if (user.password) { if (user.password.length < 8) throw new Error('Password to short; needs to be 8 characters or longer.') user.passwordDigest = await bcrypt.hash(user.password, 10) } delete user.password let usr = await userByEmailQuery(user.email).select('id').first() if (usr) throw new Error('Email taken. Try logging in?') let res = await Users().returning('id').insert(user) let userRole = (await countAdminUsers()) === 0 ? 'server:admin' : 'server:user' await Acl().insert({ userId: res[0].id, role: userRole }) let loggedUser = { ...user } delete loggedUser.passwordDigest await saveActivity({ streamId: null, resourceType: 'user', resourceId: user.id, actionType: 'user_create', userId: user.id, info: { user: loggedUser }, message: 'User created' }) return res[0].id }, async findOrCreateUser({ user, rawProfile }) { let existingUser = await userByEmailQuery(user.email).select('id').first() if (existingUser) { if (user.suuid) { await module.exports.updateUser(existingUser.id, { suuid: user.suuid }) } existingUser.suuid = user.suuid return existingUser } user.password = crs({ length: 20 }) user.verified = true // because we trust the external identity provider, no? return { id: await module.exports.createUser(user), email: user.email } }, async getUserById({ userId }) { let user = await Users().where({ id: userId }).select('*').first() if (user) delete user.passwordDigest return user }, // TODO: deprecate async getUser(id) { let user = await Users().where({ id: id }).select('*').first() if (user) delete user.passwordDigest return user }, async getUserByEmail({ email }) { let user = await userByEmailQuery(email).select('*').first() if (!user) return null delete user.passwordDigest return user }, async getUserRole(id) { let { role } = await Acl().where({ userId: id }).select('role').first() return role }, async updateUser(id, user) { delete user.id delete user.passwordDigest delete user.password delete user.email await Users().where({ id: id }).update(user) }, async updateUserPassword({ id, newPassword }) { if (newPassword.length < 8) throw new Error('Password to short; needs to be 8 characters or longer.') let passwordDigest = await bcrypt.hash(newPassword, 10) await Users().where({ id: id }).update({ passwordDigest }) }, async searchUsers(searchQuery, limit, cursor) { limit = limit || 25 let query = Users() .select('id', 'name', 'bio', 'company', 'verified', 'avatar', 'createdAt') .where((queryBuilder) => { queryBuilder.where({ email: searchQuery }) //match full email or partial name queryBuilder.orWhere('name', 'ILIKE', `%${searchQuery}%`) }) if (cursor) query.andWhere('users.createdAt', '<', cursor) query.orderBy('users.createdAt', 'desc').limit(limit) let rows = await query return { users: rows, cursor: rows.length > 0 ? rows[rows.length - 1].createdAt.toISOString() : null } }, async validatePasssword({ email, password }) { let { passwordDigest } = await userByEmailQuery(email).select('passwordDigest').first() return bcrypt.compare(password, passwordDigest) }, async deleteUser(id) { //TODO: check for the last admin user to survive debug('speckle:db')('Deleting user ' + id) await _ensureAtleastOneAdminRemains(id) let streams = await knex.raw( ` -- Get the stream ids with only this user as owner SELECT "resourceId" as id FROM ( -- Compute (streamId, ownerCount) table for streams on which the user is owner SELECT acl."resourceId", count(*) as cnt FROM stream_acl acl INNER JOIN ( -- Get streams ids on which the user is owner SELECT "resourceId" FROM stream_acl WHERE role = 'stream:owner' AND "userId" = ? ) AS us ON acl."resourceId" = us."resourceId" WHERE acl.role = 'stream:owner' GROUP BY (acl."resourceId") ) AS soc WHERE cnt = 1 `, [id] ) for (let i in streams.rows) { await deleteStream({ streamId: streams.rows[i].id }) } return await Users().where({ id: id }).del() }, async getUsers(limit = 10, offset = 0, searchQuery = null) { // sanitize limit const maxLimit = 200 if (limit > maxLimit) limit = maxLimit let query = Users() if (searchQuery) { query.where((queryBuilder) => { queryBuilder .where('email', 'ILIKE', `%${searchQuery}%`) .orWhere('name', 'ILIKE', `%${searchQuery}%`) .orWhere('company', 'ILIKE', `%${searchQuery}%`) }) } let users = await query.limit(limit).offset(offset) users.map((user) => delete user.passwordDigest) return users }, async makeUserAdmin({ userId }) { await changeUserRole({ userId, role: 'server:admin' }) }, async unmakeUserAdmin({ userId }) { // dont delete last admin role await _ensureAtleastOneAdminRemains(userId) await changeUserRole({ userId, role: 'server:user' }) }, async archiveUser({ userId }) { // dont change last admin to archived await _ensureAtleastOneAdminRemains(userId) await changeUserRole({ userId, role: 'server:archived-user' }) }, async countUsers(searchQuery = null) { let query = Users() if (searchQuery) { query.where((queryBuilder) => { queryBuilder .where('email', 'ILIKE', `%${searchQuery}%`) .orWhere('name', 'ILIKE', `%${searchQuery}%`) .orWhere('company', 'ILIKE', `%${searchQuery}%`) }) } let [userCount] = await query.count() return parseInt(userCount.count) } }