diff --git a/packages/create-strapi-app/package.json b/packages/create-strapi-app/package.json index 997f82e27d..957be0a255 100644 --- a/packages/create-strapi-app/package.json +++ b/packages/create-strapi-app/package.json @@ -20,7 +20,7 @@ "index.js" ], "dependencies": { - "commander": "^2.20.0", + "commander": "6.1.0", "strapi-generate-new": "3.2.3" }, "scripts": { diff --git a/packages/strapi-admin/services/__tests__/user.test.js b/packages/strapi-admin/services/__tests__/user.test.js index fbcf734654..e1f55ebc56 100644 --- a/packages/strapi-admin/services/__tests__/user.test.js +++ b/packages/strapi-admin/services/__tests__/user.test.js @@ -744,4 +744,90 @@ describe('User', () => { ); }); }); + + describe('resetPasswordByEmail', () => { + test('Throws on missing user', async () => { + const email = 'email@email.fr'; + const password = 'invalidpass'; + + const findOne = jest.fn(() => { + return null; + }); + + global.strapi = { + query() { + return { + findOne, + }; + }, + }; + + expect.hasAssertions(); + + await userService.resetPasswordByEmail(email, password).catch(error => { + expect(findOne).toHaveBeenCalledWith({ email }, undefined); + expect(error).toEqual(new Error(`User not found for email: ${email}`)); + }); + }); + + test.each(['abc', 'Abcd', 'Abcdefgh', 'Abcd123'])( + 'Throws on invalid password', + async password => { + const email = 'email@email.fr'; + + const findOne = jest.fn(() => ({ id: 1 })); + + global.strapi = { + query() { + return { + findOne, + }; + }, + }; + + expect.hasAssertions(); + + await userService.resetPasswordByEmail(email, password).catch(error => { + expect(findOne).toHaveBeenCalledWith({ email }, undefined); + expect(error).toEqual( + new Error( + 'Invalid password. Expected a minimum of 8 characters with at least one number and one uppercase letter' + ) + ); + }); + } + ); + }); + + test('Call the update function with the expected params', async () => { + const email = 'email@email.fr'; + const password = 'Testing1234'; + const hash = 'hash'; + const userId = 1; + + const findOne = jest.fn(() => ({ id: userId })); + const update = jest.fn(); + const hashPassword = jest.fn(() => hash); + + global.strapi = { + query() { + return { + findOne, + update, + }; + }, + admin: { + services: { + auth: { + hashPassword, + }, + }, + }, + }; + + await userService.resetPasswordByEmail(email, password); + expect(findOne).toHaveBeenCalledWith({ email }, undefined); + expect(update).toHaveBeenCalledWith({ id: userId }, { password: hash }); + expect(hashPassword).toHaveBeenCalledWith(password); + }); }); diff --git a/packages/strapi-admin/services/user.js b/packages/strapi-admin/services/user.js index 3937982a29..b80c0486ae 100644 --- a/packages/strapi-admin/services/user.js +++ b/packages/strapi-admin/services/user.js @@ -4,6 +4,7 @@ const _ = require('lodash'); const { stringIncludes } = require('strapi-utils'); const { createUser, hasSuperAdminRole } = require('../domain/user'); const { SUPER_ADMIN_CODE } = require('./constants'); +const { password: passwordValidator } = require('../validation/common-validators'); const sanitizeUserRoles = role => _.pick(role, ['id', 'name', 'description', 'code']); @@ -43,7 +44,7 @@ const create = async attributes => { /** * Update a user in database - * @param params query params to find the user to update + * @param id query params to find the user to update * @param attributes A partial user object * @returns {Promise} */ @@ -89,6 +90,31 @@ const updateById = async (id, attributes) => { return strapi.query('user', 'admin').update({ id }, attributes); }; +/** + * 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 findOne({ email }); + + if (!user) { + throw new Error(`User not found for email: ${email}`); + } + + try { + await passwordValidator.validate(password); + } catch (error) { + throw new Error( + 'Invalid password. Expected a minimum of 8 characters with at least one number and one uppercase letter' + ); + } + + const { id: userId } = user; + + await updateById(userId, { password }); +}; + /** * Check if a user is the last super admin * @param {int|string} userId user's id to look for @@ -320,4 +346,5 @@ module.exports = { assignARoleToAll, displayWarningIfUsersDontHaveRole, migrateUsers, + resetPasswordByEmail, }; diff --git a/packages/strapi/bin/strapi.js b/packages/strapi/bin/strapi.js index 7eee3fac47..d3593ce27c 100755 --- a/packages/strapi/bin/strapi.js +++ b/packages/strapi/bin/strapi.js @@ -4,17 +4,12 @@ const _ = require('lodash'); const resolveCwd = require('resolve-cwd'); const { yellow } = require('chalk'); -const program = require('commander'); +const { Command } = require('commander'); +const program = new Command(); const packageJSON = require('../package.json'); -// Allow us to display `help()`, but omit the wildcard (`*`) command. -program.Command.prototype.usageMinusWildcard = program.usageMinusWildcard = () => { - program.commands = _.reject(program.commands, { - _name: '*', - }); - program.help(); -}; +program.storeOptionsAsProperties(false).passCommandToAction(false); const checkCwdIsStrapiApp = name => { let logErrorAndExit = () => { @@ -72,20 +67,15 @@ const getLocalScript = name => (...args) => { program.allowUnknownOption(true); -// Expose version. -program.version(packageJSON.version, '-v, --version'); - -// Make `-v` option case-insensitive. -process.argv = _.map(process.argv, arg => { - return arg === '-V' ? '-v' : arg; -}); +program.option('-v, --version', 'output the version number'); // `$ strapi version` (--version synonym) program .command('version') .description('output your version of Strapi') .action(() => { - console.log(packageJSON.version); + process.stdout.write(packageJSON.version + '\n'); + process.exit(0); }); // `$ strapi console` @@ -126,7 +116,7 @@ program .command('develop') .alias('dev') .option('--no-build', 'Disable build', false) - .option('--watch-admin', 'Enable watch', true) + .option('--watch-admin', 'Enable watch', false) .option('--browser ', 'Open the browser', true) .description('Start your Strapi application in development mode') .action(getLocalScript('develop')); @@ -221,38 +211,25 @@ program program .command('configuration:dump') .alias('config:dump') + .description('Dump configurations of your application') .option('-f, --file ', 'Output file, default output is stdout') .action(getLocalScript('configurationDump')); program .command('configuration:restore') .alias('config:restore') + .description('Restore configurations of your application') .option('-f, --file ', 'Input file, default input is stdin') .option('-s, --strategy ', 'Strategy name, one of: "replace", "merge", "keep"') .action(getLocalScript('configurationRestore')); -/** - * Normalize help argument - */ - -// `$ strapi help` (--help synonym) +// Admin program - .command('help') - .description('output the help') - .action(program.usageMinusWildcard); + .command('admin:reset-password') + .alias('admin:reset') + .description('Reset an admin user password') + .option('-e, --email ', 'The user email') + .option('-p, --password ', 'New password for the user') + .action(getLocalScript('admin-reset')); -// `$ strapi ` -// Mask the '*' in `help`. -program.command('*').action(program.usageMinusWildcard); - -// Don't balk at unknown options. - -/** - * `$ strapi` - */ - -program.parse(process.argv); -const NO_COMMAND_SPECIFIED = program.args.length === 0; -if (NO_COMMAND_SPECIFIED) { - program.usageMinusWildcard(); -} +program.parseAsync(process.argv); diff --git a/packages/strapi/lib/commands/admin-reset.js b/packages/strapi/lib/commands/admin-reset.js new file mode 100644 index 0000000000..8810ebaaa5 --- /dev/null +++ b/packages/strapi/lib/commands/admin-reset.js @@ -0,0 +1,51 @@ +'use strict'; + +const _ = require('lodash'); +const inquirer = require('inquirer'); +const strapi = require('../index'); + +const promptQuestions = [ + { type: 'input', name: 'email', message: 'User email?' }, + { type: 'password', name: 'password', message: 'New password?' }, + { + type: 'confirm', + name: 'confirm', + message: "Do you really want to reset this user's password?", + }, +]; + +/** + * Reset user's password + * @param {Object} cmdOptions - command options + * @param {string} cmdOptions.email - user's email + * @param {string} cmdOptions.password - user's new password + */ +module.exports = async function(cmdOptions) { + const { email, password } = cmdOptions; + + if (_.isEmpty(email) && _.isEmpty(password) && process.stdin.isTTY) { + const inquiry = await inquirer.prompt(promptQuestions); + + if (!inquiry.confirm) { + process.exit(0); + } + + return changePassword(inquiry); + } + + if (_.isEmpty(email) || _.isEmpty(password)) { + console.error('Missing required options `email` or `password`'); + process.exit(1); + } + + return changePassword({ email, password }); +}; + +async function changePassword({ email, password }) { + const app = await strapi().load(); + + await app.admin.services.user.resetPasswordByEmail(email, password); + + console.log(`Successfully reset user's password`); + process.exit(0); +} diff --git a/packages/strapi/package.json b/packages/strapi/package.json index 398469e4d2..3077198539 100644 --- a/packages/strapi/package.json +++ b/packages/strapi/package.json @@ -19,7 +19,7 @@ "chokidar": "3.3.1", "ci-info": "2.0.0", "cli-table3": "^0.6.0", - "commander": "^2.20.0", + "commander": "6.1.0", "cross-spawn": "^6.0.5", "debug": "^4.1.1", "delegates": "^1.0.0", diff --git a/yarn.lock b/yarn.lock index 66b18c9ca4..1bbb6142f9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5651,6 +5651,11 @@ commander@2.3.0: resolved "https://registry.yarnpkg.com/commander/-/commander-2.3.0.tgz#fd430e889832ec353b9acd1de217c11cb3eef873" integrity sha1-/UMOiJgy7DU7ms0d4hfBHLPu+HM= +commander@6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-6.1.0.tgz#f8d722b78103141006b66f4c7ba1e97315ba75bc" + integrity sha512-wl7PNrYWd2y5mp1OK/LhTlv8Ff4kQJQRXXAvF+uU/TPNiVJUxZLRYGj/B0y/lPGAVcSbJqH2Za/cvHmrPMC8mA== + commander@^2.19.0, commander@^2.20.0, commander@^2.20.3: version "2.20.3" resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"