diff --git a/bin/froxlor-cli b/bin/froxlor-cli index 26bdc2b7..b7e6d566 100755 --- a/bin/froxlor-cli +++ b/bin/froxlor-cli @@ -34,6 +34,7 @@ use Froxlor\Cli\SwitchServerIp; use Froxlor\Cli\UpdateCommand; use Froxlor\Cli\InstallCommand; use Froxlor\Cli\MasterCron; +use Froxlor\Cli\UserCommand; use Froxlor\Froxlor; // validate correct php version @@ -57,4 +58,5 @@ $application->add(new SwitchServerIp()); $application->add(new UpdateCommand()); $application->add(new InstallCommand()); $application->add(new MasterCron()); +$application->add(new UserCommand()); $application->run(); diff --git a/lib/Froxlor/Cli/CliCommand.php b/lib/Froxlor/Cli/CliCommand.php index 5c154a27..8cc93a1c 100644 --- a/lib/Froxlor/Cli/CliCommand.php +++ b/lib/Froxlor/Cli/CliCommand.php @@ -25,6 +25,7 @@ namespace Froxlor\Cli; +use PDO; use Exception; use Froxlor\Froxlor; use Froxlor\Settings; @@ -61,6 +62,60 @@ class CliCommand extends Command return self::SUCCESS; } + protected function getUserByName(?string $loginname, bool $deactivated_check = true): array + { + if (empty($loginname)) { + throw new Exception("Empty username"); + } + + $stmt = Database::prepare(" + SELECT `loginname` AS `customer` + FROM `" . TABLE_PANEL_CUSTOMERS . "` + WHERE `loginname`= :loginname + "); + Database::pexecute($stmt, [ + "loginname" => $loginname + ]); + $row = $stmt->fetch(PDO::FETCH_ASSOC); + + if ($row && $row['customer'] == $loginname) { + $table = "`" . TABLE_PANEL_CUSTOMERS . "`"; + $adminsession = '0'; + } else { + $stmt = Database::prepare(" + SELECT `loginname` AS `admin` FROM `" . TABLE_PANEL_ADMINS . "` + WHERE `loginname`= :loginname + "); + Database::pexecute($stmt, [ + "loginname" => $loginname + ]); + $row = $stmt->fetch(PDO::FETCH_ASSOC); + + if ($row && $row['admin'] == $loginname) { + $table = "`" . TABLE_PANEL_ADMINS . "`"; + $adminsession = '1'; + } else { + throw new Exception("Unknown user '" . $loginname . "'"); + } + } + + $userinfo_stmt = Database::prepare(" + SELECT * FROM $table + WHERE `loginname`= :loginname + "); + Database::pexecute($userinfo_stmt, [ + "loginname" => $loginname + ]); + $userinfo = $userinfo_stmt->fetch(PDO::FETCH_ASSOC); + $userinfo['adminsession'] = $adminsession; + + if ($deactivated_check && $userinfo['deactivated']) { + throw new Exception("User '" . $loginname . "' is currently deactivated"); + } + + return $userinfo; + } + private function runUpdate(OutputInterface $output): int { $output->writeln('Automatic update is activated and we are going to proceed without any notices'); diff --git a/lib/Froxlor/Cli/RunApiCommand.php b/lib/Froxlor/Cli/RunApiCommand.php index 8ed316fc..5a070f42 100644 --- a/lib/Froxlor/Cli/RunApiCommand.php +++ b/lib/Froxlor/Cli/RunApiCommand.php @@ -126,57 +126,4 @@ final class RunApiCommand extends CliCommand return ['class' => $command[0], 'function' => $command[1]]; } - private function getUserByName(?string $loginname): array - { - if (empty($loginname)) { - throw new Exception("Empty username"); - } - - $stmt = Database::prepare(" - SELECT `loginname` AS `customer` - FROM `" . TABLE_PANEL_CUSTOMERS . "` - WHERE `loginname`= :loginname - "); - Database::pexecute($stmt, [ - "loginname" => $loginname - ]); - $row = $stmt->fetch(PDO::FETCH_ASSOC); - - if ($row && $row['customer'] == $loginname) { - $table = "`" . TABLE_PANEL_CUSTOMERS . "`"; - $adminsession = '0'; - } else { - $stmt = Database::prepare(" - SELECT `loginname` AS `admin` FROM `" . TABLE_PANEL_ADMINS . "` - WHERE `loginname`= :loginname - "); - Database::pexecute($stmt, [ - "loginname" => $loginname - ]); - $row = $stmt->fetch(PDO::FETCH_ASSOC); - - if ($row && $row['admin'] == $loginname) { - $table = "`" . TABLE_PANEL_ADMINS . "`"; - $adminsession = '1'; - } else { - throw new Exception("Unknown user '" . $loginname . "'"); - } - } - - $userinfo_stmt = Database::prepare(" - SELECT * FROM $table - WHERE `loginname`= :loginname - "); - Database::pexecute($userinfo_stmt, [ - "loginname" => $loginname - ]); - $userinfo = $userinfo_stmt->fetch(PDO::FETCH_ASSOC); - $userinfo['adminsession'] = $adminsession; - - if ($userinfo['deactivated']) { - throw new Exception("User '" . $loginname . "' is currently deactivated"); - } - - return $userinfo; - } } diff --git a/lib/Froxlor/Cli/UserCommand.php b/lib/Froxlor/Cli/UserCommand.php new file mode 100644 index 00000000..33caef39 --- /dev/null +++ b/lib/Froxlor/Cli/UserCommand.php @@ -0,0 +1,135 @@ + + * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 + */ + +namespace Froxlor\Cli; + +use Exception; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Style\SymfonyStyle; +use Froxlor\Api\Commands\Admins; +use Froxlor\Api\Commands\Customers; +use Froxlor\System\Crypt; +use Froxlor\Froxlor; + +final class UserCommand extends CliCommand +{ + + protected function configure() + { + $this->setName('froxlor:user'); + $this->setDescription('Various user actions'); + $this->addArgument('user', InputArgument::REQUIRED, 'Loginname of the target user') + ->addArgument('admin', InputArgument::OPTIONAL, 'Loginname of the executing admin/reseller user', 'admin'); + $this->addOption('unlock', 'u', InputOption::VALUE_NONE, 'Unock user after too many failed login attempts') + ->addOption('change-passwd', 'p', InputOption::VALUE_NONE, 'Set new password for given user') + ->addOption('show-info', 's', InputOption::VALUE_NONE, 'Output information details of given user'); + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $result = self::SUCCESS; + + $result = $this->validateRequirements($input, $output); + + require Froxlor::getInstallDir() . '/lib/functions.php'; + + // set error-handler + @set_error_handler([ + '\\Froxlor\\Api\\Api', + 'phpErrHandler' + ]); + + if ($result == self::SUCCESS) { + try { + $adminname = $input->getArgument('admin'); + $admininfo = $this->getUserByName($adminname); + $loginname = $input->getArgument('user'); + $userinfo = $this->getUserByName($loginname, false); + + $do_unlock = $input->getOption('unlock'); + $do_passwd = $input->getOption('change-passwd'); + $do_show = $input->getOption('show-info'); + + if ($do_unlock === false && $do_passwd === false && $do_show === false) { + $output->writeln('No option given, nothing to do.'); + $result = self::INVALID; + } + + if ($do_unlock !== false) { + // unlock given user + if ((int)$userinfo['adminsession'] == 1) { + Admins::getLocal($admininfo, ['loginname' => $loginname])->unlock(); + } else { + Customers::getLocal($admininfo, ['loginname' => $loginname])->unlock(); + } + $output->writeln('User ' . $loginname . ' unlocked successfully'); + $result = self::SUCCESS; + } + if ($result == self::SUCCESS && $do_passwd !== false) { + $io = new SymfonyStyle($input, $output); + $passwd = $io->askHidden("Enter new password", function ($value) { + if (empty($value)) { + throw new \RuntimeException('You must enter a value.'); + } + $value = Crypt::validatePassword($value, 'new password'); + return $value; + }); + $passwd2 = $io->askHidden("Confirm new password", function ($value) use ($passwd) { + if (empty($value)) { + throw new \RuntimeException('You must enter a value.'); + } elseif ($value != $passwd) { + throw new \RuntimeException('Passwords do not match'); + } + return $value; + }); + if ((int)$userinfo['adminsession'] == 1) { + Admins::getLocal($admininfo, ['id' => $userinfo['adminid'], 'admin_password' => $passwd])->update(); + } else { + Customers::getLocal($admininfo, ['id' => $userinfo['customerid'], 'new_customer_password' => $passwd])->update(); + } + $output->writeln('Changed password for ' . $loginname . ''); + $result = self::SUCCESS; + } + if ($result == self::SUCCESS && $do_show !== false) { + $userinfo['password'] = '[hidden]'; + $userinfo['data_2fa'] = '[hidden]'; + $io = new SymfonyStyle($input, $output); + $io->horizontalTable( + array_keys($userinfo), + [array_values($userinfo)] + ); + } + } catch (Exception $e) { + $output->writeln('' . $e->getMessage() . ''); + $result = self::FAILURE; + } + } + + return $result; + } +}