'use strict'; const _ = require('lodash'); const { defaults } = require('lodash/fp'); const { stringIncludes } = require('@strapi/utils'); const { ValidationError } = require('@strapi/utils').errors; const { createUser, hasSuperAdminRole } = require('../domain/user'); const { password: passwordValidator } = require('../validation/common-validators'); const { getService } = require('../utils'); const { SUPER_ADMIN_CODE } = require('./constants'); const sanitizeUserRoles = (role) => _.pick(role, ['id', 'name', 'description', 'code']); /** * Remove private user fields * @param {Object} user - user to sanitize */ const sanitizeUser = (user) => { return { ..._.omit(user, ['password', 'resetPasswordToken', 'registrationToken', 'roles']), roles: user.roles && user.roles.map(sanitizeUserRoles), }; }; /** * Create and save a user in database * @param attributes A partial user object * @returns {Promise} */ const create = async (attributes) => { const userInfo = { registrationToken: getService('token').createToken(), ...attributes, }; if (_.has(attributes, 'password')) { userInfo.password = await getService('auth').hashPassword(attributes.password); } const user = createUser(userInfo); const createdUser = await strapi.query('admin::user').create({ data: user, populate: ['roles'] }); getService('metrics').sendDidInviteUser(); strapi.eventHub.emit('user.create', { user: sanitizeUser(createdUser) }); return createdUser; }; /** * Update a user in database * @param id query params to find the user to update * @param attributes A partial user object * @returns {Promise} */ const updateById = async (id, attributes) => { // Check at least one super admin remains if (_.has(attributes, 'roles')) { const lastAdminUser = await isLastSuperAdminUser(id); const superAdminRole = await getService('role').getSuperAdminWithUsersCount(); const willRemoveSuperAdminRole = !stringIncludes(attributes.roles, superAdminRole.id); if (lastAdminUser && willRemoveSuperAdminRole) { throw new ValidationError('You must have at least one user with super admin role.'); } } // cannot disable last super admin if (attributes.isActive === false) { const lastAdminUser = await isLastSuperAdminUser(id); if (lastAdminUser) { throw new ValidationError('You must have at least one user with super admin role.'); } } // hash password if a new one is sent if (_.has(attributes, 'password')) { const hashedPassword = await getService('auth').hashPassword(attributes.password); const updatedUser = await strapi.query('admin::user').update({ where: { id }, data: { ...attributes, password: hashedPassword, }, populate: ['roles'], }); strapi.eventHub.emit('user.update', { user: sanitizeUser(updatedUser) }); return updatedUser; } const updatedUser = await strapi.query('admin::user').update({ where: { id }, data: attributes, populate: ['roles'], }); if (updatedUser) { strapi.eventHub.emit('user.update', { user: sanitizeUser(updatedUser) }); } return updatedUser; }; /** * Reset a user password by email. (Used in admin:reset CLI) * @param {string} email - user email * @param {string} password - new password */ const resetPasswordByEmail = async (email, password) => { const user = await strapi.query('admin::user').findOne({ where: { email }, populate: ['roles'] }); if (!user) { throw new Error(`User not found for email: ${email}`); } try { await passwordValidator.validate(password); } catch (error) { throw new ValidationError( 'Invalid password. Expected a minimum of 8 characters with at least one number and one uppercase letter' ); } await updateById(user.id, { password }); }; /** * Check if a user is the last super admin * @param {int|string} userId user's id to look for */ const isLastSuperAdminUser = async (userId) => { const user = await findOne(userId); const superAdminRole = await getService('role').getSuperAdminWithUsersCount(); return superAdminRole.usersCount === 1 && hasSuperAdminRole(user); }; /** * Check if a user with specific attributes exists in the database * @param attributes A partial user object * @returns {Promise} */ const exists = async (attributes = {}) => { return (await strapi.query('admin::user').count({ where: attributes })) > 0; }; /** * Returns a user registration info * @param {string} registrationToken - a user registration token * @returns {Promise} - Returns user email, firstname and lastname */ const findRegistrationInfo = async (registrationToken) => { const user = await strapi.query('admin::user').findOne({ where: { registrationToken } }); if (!user) { return undefined; } return _.pick(user, ['email', 'firstname', 'lastname']); }; /** * Registers a user based on a registrationToken and some informations to update * @param {Object} params * @param {Object} params.registrationToken registration token * @param {Object} params.userInfo user info */ const register = async ({ registrationToken, userInfo }) => { const matchingUser = await strapi.query('admin::user').findOne({ where: { registrationToken } }); if (!matchingUser) { throw new ValidationError('Invalid registration info'); } return getService('user').updateById(matchingUser.id, { password: userInfo.password, firstname: userInfo.firstname, lastname: userInfo.lastname, registrationToken: null, isActive: true, }); }; /** * Find one user */ const findOne = async (id, populate = ['roles']) => { return strapi.entityService.findOne('admin::user', id, { populate }); }; /** * Find one user by its email * @param {string} id email * @param {string || string[] || object} populate * @returns */ const findOneByEmail = async (email, populate = []) => { return strapi.query('admin::user').findOne({ where: { email }, populate, }); }; /** Find many users (paginated) * @param query * @returns {Promise} */ const findPage = async (query = {}) => { const enrichedQuery = defaults({ populate: ['roles'] }, query); return strapi.entityService.findPage('admin::user', enrichedQuery); }; /** Delete a user * @param id id of the user to delete * @returns {Promise} */ const deleteById = async (id) => { // Check at least one super admin remains const userToDelete = await strapi.query('admin::user').findOne({ where: { id }, populate: ['roles'], }); if (!userToDelete) { return null; } if (userToDelete) { if (userToDelete.roles.some((r) => r.code === SUPER_ADMIN_CODE)) { const superAdminRole = await getService('role').getSuperAdminWithUsersCount(); if (superAdminRole.usersCount === 1) { throw new ValidationError('You must have at least one user with super admin role.'); } } } const deletedUser = await strapi .query('admin::user') .delete({ where: { id }, populate: ['roles'] }); strapi.eventHub.emit('user.delete', { user: sanitizeUser(deletedUser) }); return deletedUser; }; /** Delete a user * @param ids ids of the users to delete * @returns {Promise} */ const deleteByIds = async (ids) => { // Check at least one super admin remains const superAdminRole = await getService('role').getSuperAdminWithUsersCount(); const nbOfSuperAdminToDelete = await strapi.query('admin::user').count({ where: { id: ids, roles: { id: superAdminRole.id }, }, }); if (superAdminRole.usersCount === nbOfSuperAdminToDelete) { throw new ValidationError('You must have at least one user with super admin role.'); } const deletedUsers = []; for (const id of ids) { const deletedUser = await strapi.query('admin::user').delete({ where: { id }, populate: ['roles'], }); deletedUsers.push(deletedUser); } strapi.eventHub.emit('user.delete', { users: deletedUsers.map((deletedUser) => sanitizeUser(deletedUser)), }); return deletedUsers; }; /** Count the users that don't have any associated roles * @returns {Promise} */ const countUsersWithoutRole = async () => { return strapi.query('admin::user').count({ where: { roles: { id: { $null: true }, }, }, }); }; /** * Count the number of users based on search params * @param params params used for the query * @returns {Promise} */ const count = async (where = {}) => { return strapi.query('admin::user').count({ where }); }; /** Assign some roles to several users * @returns {undefined} */ const assignARoleToAll = async (roleId) => { const users = await strapi.query('admin::user').findMany({ select: ['id'], where: { roles: { id: { $null: true } }, }, }); await Promise.all( users.map((user) => { return strapi.query('admin::user').update({ where: { id: user.id }, data: { roles: [roleId] }, }); }) ); }; /** Display a warning if some users don't have at least one role * @returns {Promise<>} */ const displayWarningIfUsersDontHaveRole = async () => { const count = await countUsersWithoutRole(); if (count > 0) { strapi.log.warn(`Some users (${count}) don't have any role.`); } }; /** Returns an array of interface languages currently used by users * @returns {Promise>} */ const getLanguagesInUse = async () => { const users = await strapi.query('admin::user').findMany({ select: ['preferedLanguage'] }); return users.map((user) => user.preferedLanguage || 'en'); }; module.exports = { create, updateById, exists, findRegistrationInfo, register, sanitizeUser, findOne, findOneByEmail, findPage, deleteById, deleteByIds, countUsersWithoutRole, count, assignARoleToAll, displayWarningIfUsersDontHaveRole, resetPasswordByEmail, getLanguagesInUse, };