diff --git a/console.yml b/console.yml index 10a9693..ee17af7 100644 --- a/console.yml +++ b/console.yml @@ -13,6 +13,8 @@ Commands: - SilverLeague\Console\Command\Member\ChangeGroupsCommand - SilverLeague\Console\Command\Member\ChangePasswordCommand - SilverLeague\Console\Command\Member\CreateCommand + - SilverLeague\Console\Command\Member\LockCommand + - SilverLeague\Console\Command\Member\UnlockCommand - SilverLeague\Console\Command\Object\ChildrenCommand - SilverLeague\Console\Command\Object\DebugCommand - SilverLeague\Console\Command\Object\ExtensionsCommand diff --git a/docs/en/README.md b/docs/en/README.md index e856a3e..677ce37 100644 --- a/docs/en/README.md +++ b/docs/en/README.md @@ -26,6 +26,8 @@ This documentation section contains instructions and examples of the commands bu * [`member:change-groups`](commands/member-change-groups.md): Change a member's groups * [`member:change-password`](commands/member-change-password.md): Change a member's password * [`member:create`](commands/member-create.md): Create a new member, and optionally add them to groups +* [`member:lock`](commands/member-lock.md): Lock a member +* [`member:unlock`](commands/member-unlock.md): Unlock a member ### Object debugging diff --git a/docs/en/commands/member-lock.md b/docs/en/commands/member-lock.md new file mode 100644 index 0000000..7ba050f --- /dev/null +++ b/docs/en/commands/member-lock.md @@ -0,0 +1,22 @@ +# Command: `member:lock` + +Locks a member for the duration of `Member.lock_out_delay_mins` minutes. + +## Usage + +```shell +$ ssconsole member:lock [] +``` + +## Options + +| Type | Name | Required | Description | Options | Default | +| --- | --- | --- | --- | --- | --- | +| Argument | `email` | Yes | The email address for the member | _string_ | Prompt | + +## Example + +``` +$ ssconsole member:lock my@user.com +Member my@user.com locked for 15 mins. +``` diff --git a/docs/en/commands/member-unlock.md b/docs/en/commands/member-unlock.md new file mode 100644 index 0000000..086bca0 --- /dev/null +++ b/docs/en/commands/member-unlock.md @@ -0,0 +1,22 @@ +# Command: `member:unlock` + +Unlocks a member. + +## Usage + +```shell +$ ssconsole member:unlock [] +``` + +## Options + +| Type | Name | Required | Description | Options | Default | +| --- | --- | --- | --- | --- | --- | +| Argument | `email` | Yes | The email address for the member | _string_ | Prompt | + +## Example + +``` +$ ssconsole member:unlock my@user.com +Member my@user.com unlocked. +``` diff --git a/docs/en/installation.md b/docs/en/installation.md index acfa63e..35c7492 100644 --- a/docs/en/installation.md +++ b/docs/en/installation.md @@ -7,7 +7,7 @@ It is recommended to install this module globally with composer: ```shell -composer global require silverleague/console +composer global require silverleague/ssconsole ``` Ensure your composer's `bin` folder has been added to your system path. @@ -17,7 +17,7 @@ Ensure your composer's `bin` folder has been added to your system path. You can still require this module as a project dependency if you don't want to install it globally, of course: ```shell -composer require --dev silverleague/console +composer require --dev silverleague/ssconsole $ vendor/bin/ssconsole ``` diff --git a/src/Command/Member/AbstractMemberCommand.php b/src/Command/Member/AbstractMemberCommand.php new file mode 100644 index 0000000..53cda5c --- /dev/null +++ b/src/Command/Member/AbstractMemberCommand.php @@ -0,0 +1,43 @@ + + */ +abstract class AbstractMemberCommand extends SilverStripeCommand +{ + /** + * Get a member by the provided email address, output an error message if not found + * + * @param InputInterface $input + * @param OutputInterface $output + * @return Member|false + */ + protected function getMember(InputInterface $input, OutputInterface $output) + { + $email = $this->getOrAskForArgument($input, $output, 'email', 'Enter email address: '); + if (empty($email)) { + $output->writeln('Please enter an email address.'); + return false; + } + + /** @var Member $member */ + $member = Member::get()->filter('email', $email)->first(); + if (!$member) { + $output->writeln('Member with email "' . $email . '" was not found.'); + return false; + } + + return $member; + } +} + diff --git a/src/Command/Member/ChangeGroupsCommand.php b/src/Command/Member/ChangeGroupsCommand.php index ce27435..0f5812d 100644 --- a/src/Command/Member/ChangeGroupsCommand.php +++ b/src/Command/Member/ChangeGroupsCommand.php @@ -2,9 +2,7 @@ namespace SilverLeague\Console\Command\Member; -use SilverLeague\Console\Command\SilverStripeCommand; use SilverStripe\Security\Group; -use SilverStripe\Security\Member; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; @@ -16,7 +14,7 @@ * @package silverstripe-console * @author Robbie Averill */ -class ChangeGroupsCommand extends SilverStripeCommand +class ChangeGroupsCommand extends AbstractMemberCommand { /** * {@inheritDoc} @@ -34,16 +32,14 @@ protected function configure() */ protected function execute(InputInterface $input, OutputInterface $output) { - $email = $this->getOrAskForArgument($input, $output, 'email', 'Enter email address: '); - $member = Member::get()->filter('email', $email)->first(); + $member = $this->getMember($input, $output); if (!$member) { - $output->writeln('Member with email "' . $email . '" was not found.'); return; } if ($member->Groups()->count()) { $output->writeln( - 'Member ' . $email . ' is already in the following groups (will be overwritten):' + 'Member ' . $member->Email . ' is already in the following groups (will be overwritten):' ); $output->writeln(' ' . implode(', ', $member->Groups()->column('Code'))); $output->writeln(''); @@ -55,7 +51,7 @@ protected function execute(InputInterface $input, OutputInterface $output) $newGroups = $this->getHelper('question')->ask($input, $output, $question); - $output->writeln('Adding ' . $email . ' to groups: ' . implode(', ', $newGroups)); + $output->writeln('Adding ' . $member->Email . ' to groups: ' . implode(', ', $newGroups)); // $member->Groups()->removeAll(); foreach ($newGroups as $group) { $member->addToGroupByCode($group); diff --git a/src/Command/Member/ChangePasswordCommand.php b/src/Command/Member/ChangePasswordCommand.php index dbca444..7acfd00 100644 --- a/src/Command/Member/ChangePasswordCommand.php +++ b/src/Command/Member/ChangePasswordCommand.php @@ -2,9 +2,7 @@ namespace SilverLeague\Console\Command\Member; -use SilverLeague\Console\Command\SilverStripeCommand; use SilverStripe\ORM\ValidationResult; -use SilverStripe\Security\Member; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; @@ -15,7 +13,7 @@ * @package silverstripe-console * @author Robbie Averill */ -class ChangePasswordCommand extends SilverStripeCommand +class ChangePasswordCommand extends AbstractMemberCommand { /** * {@inheritDoc} @@ -41,10 +39,8 @@ protected function execute(InputInterface $input, OutputInterface $output) return; } - /** @var Member $member */ - $member = Member::get()->filter('email', $email)->first(); + $member = $this->getMember($input, $output); if (!$member) { - $output->writeln('Member with email "' . $email . '" was not found.'); return; } diff --git a/src/Command/Member/CreateCommand.php b/src/Command/Member/CreateCommand.php index e23fd30..ff61b2d 100644 --- a/src/Command/Member/CreateCommand.php +++ b/src/Command/Member/CreateCommand.php @@ -49,6 +49,13 @@ protected function execute(InputInterface $input, OutputInterface $output) return; } + // Check for existing member + $member = Member::get()->filter(['Email' => $data['Email']])->first(); + if ($member) { + $output->writeln('Member already exists with email address: ' . $data['Email']); + return; + } + $member = Member::create(); foreach ($data as $key => $value) { $member->setField($key, $value); diff --git a/src/Command/Member/LockCommand.php b/src/Command/Member/LockCommand.php new file mode 100644 index 0000000..dffd64c --- /dev/null +++ b/src/Command/Member/LockCommand.php @@ -0,0 +1,42 @@ + + */ +class LockCommand extends AbstractMemberCommand +{ + protected function configure() + { + $this + ->setName('member:lock') + ->setDescription('Lock a member account') + ->addArgument('email', InputArgument::OPTIONAL, 'Email address'); + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $member = $this->getMember($input, $output); + if (!$member) { + return; + } + + $lockoutMins = Member::config()->get('lock_out_delay_mins'); + $member->LockedOutUntil = date('Y-m-d H:i:s', DBDatetime::now()->getTimestamp() + $lockoutMins * 60); + $member->FailedLoginCount = 0; + $member->write(); + + $output->writeln('Member ' . $member->Email . ' locked for ' . $lockoutMins . ' mins.'); + } +} + diff --git a/src/Command/Member/UnlockCommand.php b/src/Command/Member/UnlockCommand.php new file mode 100644 index 0000000..ca55102 --- /dev/null +++ b/src/Command/Member/UnlockCommand.php @@ -0,0 +1,39 @@ + + */ +class UnlockCommand extends AbstractMemberCommand +{ + protected function configure() + { + $this + ->setName('member:unlock') + ->setDescription('Unlock a member account') + ->addArgument('email', InputArgument::OPTIONAL, 'Email address'); + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $member = $this->getMember($input, $output); + if (!$member) { + return; + } + + $member->LockedOutUntil = null; + $member->FailedLoginCount = 0; + $member->write(); + + $output->writeln('Member ' . $member->Email . ' unlocked.'); + } +} + diff --git a/tests/Command/Member/CreateCommandTest.php b/tests/Command/Member/CreateCommandTest.php index 1445900..0eedf6e 100644 --- a/tests/Command/Member/CreateCommandTest.php +++ b/tests/Command/Member/CreateCommandTest.php @@ -50,14 +50,19 @@ public function testExecute() $this->command->getApplication()->getHelperSet()->set($questionHelper, 'question'); - $tester = $this->executeTest( - [ - 'email' => 'unittest@example.com', - 'password' => 'OpenSe$am3!' - ] - ); + $tester = $this->executeTest([ + 'email' => 'unittest@example.com', + 'password' => 'OpenSe$am3!', + ]); $output = $tester->getDisplay(); $this->assertContains('Member created', $output); + + $tester = $this->executeTest([ + 'email' => 'unittest@example.com', + 'password' => 'OpenSe$am3!', + ]); + $output = $tester->getDisplay(); + $this->assertContains('Member already exists', $output); } /** diff --git a/tests/Command/Member/LockCommandTest.php b/tests/Command/Member/LockCommandTest.php new file mode 100644 index 0000000..2f9929c --- /dev/null +++ b/tests/Command/Member/LockCommandTest.php @@ -0,0 +1,65 @@ + + */ +class LockCommandTest extends AbstractCommandTest +{ + /** + * Delete fixtured members after tests have run + */ + protected function tearDown() + { + parent::tearDown(); + + $testMember = Member::get()->filter(['Email' => 'sometestuser@example.com'])->first(); + if ($testMember && $testMember->exists()) { + $testMember->delete(); + } + } + + protected function getTestCommand() + { + return 'member:lock'; + } + + public function testExecute() + { + $member = $this->createMember(); + $this->assertFalse($member->isLockedOut()); + + $tester = $this->executeTest(['email' => 'sometestuser@example.com']); + /** @var Member $member */ + $member = Member::get()->byID($member->ID); + $this->assertContains('Member sometestuser@example.com locked for', $tester->getDisplay()); + $this->assertTrue($member->isLockedOut()); + } + + public function testMemberNotFound() + { + $result = $this->executeTest(['email' => 'pleasedontfindme@example.com']); + $this->assertContains('Member with email "pleasedontfindme@example.com" was not found.', $result->getDisplay()); + } + + /** + * Creates a dummy user for testing with + * + * @return Member + */ + protected function createMember() + { + $member = Member::create(); + $member->Email = 'sometestuser@example.com'; + $member->Password = 'Opensesame1'; + $member->write(); + return $member; + } +} diff --git a/tests/Command/Member/UnlockCommandTest.php b/tests/Command/Member/UnlockCommandTest.php new file mode 100644 index 0000000..e081bf0 --- /dev/null +++ b/tests/Command/Member/UnlockCommandTest.php @@ -0,0 +1,66 @@ + + */ +class UnlockCommandTest extends AbstractCommandTest +{ + /** + * Delete fixtured members after tests have run + */ + protected function tearDown() + { + parent::tearDown(); + + $testMember = Member::get()->filter(['Email' => 'somelockeduser@example.com'])->first(); + if ($testMember && $testMember->exists()) { + $testMember->delete(); + } + } + + protected function getTestCommand() + { + return 'member:unlock'; + } + + public function testExecute() + { + $member = $this->createMember(); + $this->assertTrue($member->isLockedOut()); + + $tester = $this->executeTest(['email' => 'somelockeduser@example.com']); + /** @var Member $member */ + $member = Member::get()->byID($member->ID); + $this->assertContains('Member somelockeduser@example.com unlocked', $tester->getDisplay()); + $this->assertFalse($member->isLockedOut()); + } + + public function testMemberNotFound() + { + $result = $this->executeTest(['email' => 'pleasedontfindme@example.com']); + $this->assertContains('Member with email "pleasedontfindme@example.com" was not found.', $result->getDisplay()); + } + + /** + * Creates a dummy user for testing with + * + * @return Member + */ + protected function createMember() + { + $member = Member::create(); + $member->Email = 'somelockeduser@example.com'; + $member->Password = 'Opensesame1'; + $member->LockedOutUntil = '2099-01-01 01:02:03'; + $member->write(); + return $member; + } +} diff --git a/tests/Command/Object/ExtensionsCommandTest.php b/tests/Command/Object/ExtensionsCommandTest.php index 0d75680..a6b5f3b 100644 --- a/tests/Command/Object/ExtensionsCommandTest.php +++ b/tests/Command/Object/ExtensionsCommandTest.php @@ -4,8 +4,7 @@ use SilverLeague\Console\Tests\Command\AbstractCommandTest; use SilverStripe\Assets\AssetControlExtension; -use SilverStripe\Forms\GridField\GridFieldDetailForm; -use SilverStripe\Versioned\VersionedGridFieldDetailForm; +use SilverStripe\Security\Member; /** * @coversDefaultClass \SilverLeague\Console\Command\Object\ExtensionsCommand @@ -26,10 +25,10 @@ protected function getTestCommand() */ public function testExecute() { - $tester = $this->executeTest(['object' => GridFieldDetailForm::class]); + $tester = $this->executeTest(['object' => Member::class]); $output = $tester->getDisplay(); - $this->assertContains(VersionedGridFieldDetailForm::class, $output); - $this->assertContains('silverstripe/versioned', $output); + $this->assertContains(AssetControlExtension::class, $output); + $this->assertContains('silverstripe/assets', $output); } /**