diff --git a/docs/v3.x/cli/CLI.md b/docs/v3.x/cli/CLI.md index afc9535d2d..63080c91a6 100644 --- a/docs/v3.x/cli/CLI.md +++ b/docs/v3.x/cli/CLI.md @@ -32,7 +32,9 @@ options: [--no-run|--use-npm|--debug|--quickstart|--dbclient= --dbhost - **<dbssl>** and **<dbauth>** are available only for `mongo` and are optional. - **--dbforce** Allows you to overwrite content if the provided database is not empty. Only available for `postgres`, `mysql`, and is optional. -## strapi develop|dev +## strapi develop + +**Alias**: `dev` Start a Strapi application with autoReload enabled. @@ -89,7 +91,9 @@ options: [--no-optimization] - **strapi build --no-optimization**
Builds the administration panel without minimizing the assets. The build duration is faster. -## strapi configuration:dump|config:dump +## strapi configuration:dump + +**Alias**: `config:dump` Dumps configurations to a file or stdout to help you migrate to production. @@ -120,7 +124,9 @@ In case of doubt, you should avoid committing the dump file into a versioning sy ::: -## strapi configuration:restore|config:restore +## strapi configuration:restore + +**Alias**: `config:restore` Restores a configuration dump into your application. @@ -151,6 +157,27 @@ When running the restore command, you can choose from three different strategies - **merge**: Will create missing keys and merge existing keys with their new value. - **keep**: Will create missing keys and keep existing keys as is. +## strapi admin:reset-user-password + +**Alias** `admin:reset-password` + +Reset an admin user's password. +You can pass the email and new password as options or set them interactivly if you call the command without passing the options. + +**Example** + +```bash +strapi admin:reset-user-password --email=chef@strapi.io --password=Gourmet1234 +``` + +**Options** + +| Option | Type | Description | +| -------------- | ------ | ------------------------- | +| -e, --email | string | The user email | +| -p, --password | string | New password for the user | +| -h, --help | | display help for command | + ## strapi generate:api Scaffold a complete API with its configurations, controller, model and service. 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..8e8fee8e39 100644 --- a/packages/strapi-admin/services/__tests__/user.test.js +++ b/packages/strapi-admin/services/__tests__/user.test.js @@ -744,4 +744,86 @@ 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, + }; + }, + }; + + await expect(userService.resetPasswordByEmail(email, password)).rejects.toEqual( + new Error(`User not found for email: ${email}`) + ); + + expect(findOne).toHaveBeenCalledWith({ email }, undefined); + }); + + 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, + }; + }, + }; + + await expect(userService.resetPasswordByEmail(email, password)).rejects.toEqual( + new Error( + 'Invalid password. Expected a minimum of 8 characters with at least one number and one uppercase letter' + ) + ); + + expect(findOne).toHaveBeenCalledWith({ email }, undefined); + } + ); + }); + + 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..0fb4a1642d 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,29 @@ 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' + ); + } + + await updateById(user.id, { password }); +}; + /** * Check if a user is the last super admin * @param {int|string} userId user's id to look for @@ -320,4 +344,5 @@ module.exports = { assignARoleToAll, displayWarningIfUsersDontHaveRole, migrateUsers, + resetPasswordByEmail, }; diff --git a/packages/strapi/bin/strapi.js b/packages/strapi/bin/strapi.js index 7eee3fac47..469a6d47f2 100755 --- a/packages/strapi/bin/strapi.js +++ b/packages/strapi/bin/strapi.js @@ -4,18 +4,11 @@ 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(); -}; - const checkCwdIsStrapiApp = name => { let logErrorAndExit = () => { console.log( @@ -61,37 +54,29 @@ const getLocalScript = name => (...args) => { }); }; -/** - * Normalize version argument - * - * `$ strapi -v` - * `$ strapi -V` - * `$ strapi --version` - * `$ strapi version` - */ +// Initial program setup +program + .storeOptionsAsProperties(false) + .passCommandToAction(false) + .allowUnknownOption(true); -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.helpOption('-h, --help', 'Display help for command'); +program.addHelpCommand('help [command]', 'Display help for command'); // `$ strapi version` (--version synonym) +program.option('-v, --version', 'Output the version number'); program .command('version') - .description('output your version of Strapi') + .description('Output your version of Strapi') .action(() => { - console.log(packageJSON.version); + process.stdout.write(packageJSON.version + '\n'); + process.exit(0); }); // `$ strapi console` program .command('console') - .description('open the Strapi framework console') + .description('Open the Strapi framework console') .action(getLocalScript('console')); // `$ strapi new` @@ -112,7 +97,7 @@ program .option('--dbauth ', 'Authentication Database') .option('--dbfile ', 'Database file path for sqlite') .option('--dbforce', 'Overwrite database content if any') - .description('create a new application') + .description('Create a new application') .action(require('../lib/commands/new')); // `$ strapi start` @@ -125,8 +110,8 @@ program program .command('develop') .alias('dev') - .option('--no-build', 'Disable build', false) - .option('--watch-admin', 'Enable watch', true) + .option('--no-build', 'Disable build') + .option('--watch-admin', 'Enable watch', false) .option('--browser ', 'Open the browser', true) .description('Start your Strapi application in development mode') .action(getLocalScript('develop')); @@ -138,8 +123,8 @@ program .option('-p, --plugin ', 'Name of the local plugin') .option('-e, --extend ', 'Name of the plugin to extend') .option('-c, --connection ', 'The name of the connection to use') - .option('--draft-and-publish ', 'Enable draft/publish', false) - .description('generate a basic API') + .option('--draft-and-publish', 'Enable draft/publish', false) + .description('Generate a basic API') .action((id, attributes, cliArguments) => { cliArguments.attributes = attributes; getLocalScript('generate')(id, cliArguments); @@ -151,7 +136,7 @@ program .option('-a, --api ', 'API name to generate the files in') .option('-p, --plugin ', 'Name of the local plugin') .option('-e, --extend ', 'Name of the plugin to extend') - .description('generate a controller for an API') + .description('Generate a controller for an API') .action(getLocalScript('generate')); // `$ strapi generate:model` @@ -160,8 +145,8 @@ program .option('-a, --api ', 'API name to generate a sub API') .option('-p, --plugin ', 'plugin name') .option('-c, --connection ', 'The name of the connection to use') - .option('--draft-and-publish ', 'Enable draft/publish', false) - .description('generate a model for an API') + .option('--draft-and-publish', 'Enable draft/publish', false) + .description('Generate a model for an API') .action((id, attributes, cliArguments) => { cliArguments.attributes = attributes; getLocalScript('generate')(id, cliArguments); @@ -172,7 +157,7 @@ program .command('generate:policy ') .option('-a, --api ', 'API name') .option('-p, --plugin ', 'plugin name') - .description('generate a policy for an API') + .description('Generate a policy for an API') .action(getLocalScript('generate')); // `$ strapi generate:service` @@ -181,33 +166,33 @@ program .option('-a, --api ', 'API name') .option('-p, --plugin ', 'plugin name') .option('-t, --tpl