From 44e4e91e3410d476267ee7eb93f309bc17eb9155 Mon Sep 17 00:00:00 2001 From: Sebastian Krupinski Date: Mon, 20 Jul 2026 06:58:44 -0400 Subject: [PATCH] feat: user management console commands Signed-off-by: Sebastian Krupinski --- core/lib/Console/User/UserCreateCommand.php | 128 ++++++++++++++++++++ core/lib/Console/User/UserDeleteCommand.php | 93 ++++++++++++++ core/lib/Console/User/UserListCommand.php | 83 +++++++++++++ core/lib/Module/Module.php | 18 ++- 4 files changed, 316 insertions(+), 6 deletions(-) create mode 100644 core/lib/Console/User/UserCreateCommand.php create mode 100644 core/lib/Console/User/UserDeleteCommand.php create mode 100644 core/lib/Console/User/UserListCommand.php diff --git a/core/lib/Console/User/UserCreateCommand.php b/core/lib/Console/User/UserCreateCommand.php new file mode 100644 index 0000000..f76c11d --- /dev/null +++ b/core/lib/Console/User/UserCreateCommand.php @@ -0,0 +1,128 @@ +addArgument('tenant', InputArgument::REQUIRED, 'Tenant identifier') + ->addArgument('identity', InputArgument::REQUIRED, 'User identity (email/username)') + ->addOption('label', 'l', InputOption::VALUE_REQUIRED, 'Display label for the user') + ->addOption('role', 'r', InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Role id(s) to assign', []) + ->addOption('uid', null, InputOption::VALUE_REQUIRED, 'Explicit user id (defaults to a generated UUID)') + ->addOption('disabled', null, InputOption::VALUE_NONE, 'Create the account in a disabled state') + ->setHelp('This command creates a new user account in a tenant. Authentication credentials are managed separately by authentication provider modules.') + ; + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output); + $tenant = $input->getArgument('tenant'); + $identity = $input->getArgument('identity'); + $roles = $input->getOption('role'); + + $io->title('Create User'); + + try { + // Ensure the tenant exists + if (!$this->tenantService->fetchById($tenant)) { + $io->error("Tenant '{$tenant}' not found."); + return Command::FAILURE; + } + + // Ensure identity is unique within the tenant + if ($this->userStore->fetchByIdentity($tenant, $identity)) { + $io->error("User '{$identity}' already exists in tenant '{$tenant}'."); + return Command::FAILURE; + } + + // Ensure assigned roles exist + foreach ($roles as $role) { + if (!$this->rolesStore->fetchByRid($tenant, $role)) { + $io->error("Role '{$role}' not found in tenant '{$tenant}'."); + return Command::FAILURE; + } + } + + $userData = [ + 'identity' => $identity, + 'label' => $input->getOption('label') ?? $identity, + 'enabled' => !$input->getOption('disabled'), + 'roles' => $roles, + 'profile' => [], + 'settings' => [], + 'provider' => null, + 'provider_subject' => null, + 'provider_managed_fields' => [], + ]; + + if ($input->getOption('uid')) { + $userData['uid'] = $input->getOption('uid'); + } + + $user = $this->userStore->createUser($tenant, $userData); + + $this->logger->info('User created via console', [ + 'tenant' => $tenant, + 'identity' => $identity, + 'uid' => $user['uid'] ?? null, + 'command' => $this->getName(), + ]); + + $io->success("User '{$identity}' created successfully!"); + $io->definitionList( + ['Uid' => $user['uid'] ?? ''], + ['Identity' => $user['identity'] ?? ''], + ['Label' => $user['label'] ?? ''], + ['Enabled' => !empty($user['enabled']) ? 'yes' : 'no'], + ['Roles' => implode(', ', (array)($user['roles'] ?? []))], + ); + + return Command::SUCCESS; + + } catch (\Throwable $e) { + $io->error('Failed to create user: ' . $e->getMessage()); + $this->logger->error('User create failed', [ + 'tenant' => $tenant, + 'identity' => $identity, + 'error' => $e->getMessage(), + ]); + return Command::FAILURE; + } + } +} diff --git a/core/lib/Console/User/UserDeleteCommand.php b/core/lib/Console/User/UserDeleteCommand.php new file mode 100644 index 0000000..642ca35 --- /dev/null +++ b/core/lib/Console/User/UserDeleteCommand.php @@ -0,0 +1,93 @@ +addArgument('tenant', InputArgument::REQUIRED, 'Tenant identifier') + ->addArgument('identity', InputArgument::REQUIRED, 'User identity (email/username)') + ->addOption('force', 'f', InputOption::VALUE_NONE, 'Skip confirmation prompt') + ->setHelp('This command deletes a user account from a tenant. Credentials stored by authentication provider modules are not removed.') + ; + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output); + $tenant = $input->getArgument('tenant'); + $identity = $input->getArgument('identity'); + $force = $input->getOption('force'); + + $io->title('Delete User'); + + try { + $user = $this->userStore->fetchByIdentity($tenant, $identity); + + if (!$user) { + $io->error("User '{$identity}' not found in tenant '{$tenant}'."); + return Command::FAILURE; + } + + if (!$force && !$io->confirm("Are you sure you want to delete user '{$identity}' from tenant '{$tenant}'?", false)) { + $io->text('Delete cancelled.'); + return Command::SUCCESS; + } + + if (!$this->userStore->deleteUser($tenant, $user['uid'])) { + $io->error("Failed to delete user '{$identity}'."); + return Command::FAILURE; + } + + $this->logger->info('User deleted via console', [ + 'tenant' => $tenant, + 'identity' => $identity, + 'uid' => $user['uid'], + 'command' => $this->getName(), + ]); + + $io->success("User '{$identity}' deleted successfully!"); + + return Command::SUCCESS; + + } catch (\Throwable $e) { + $io->error('Failed to delete user: ' . $e->getMessage()); + $this->logger->error('User delete failed', [ + 'tenant' => $tenant, + 'identity' => $identity, + 'error' => $e->getMessage(), + ]); + return Command::FAILURE; + } + } +} diff --git a/core/lib/Console/User/UserListCommand.php b/core/lib/Console/User/UserListCommand.php new file mode 100644 index 0000000..57ff142 --- /dev/null +++ b/core/lib/Console/User/UserListCommand.php @@ -0,0 +1,83 @@ +addArgument('tenant', InputArgument::REQUIRED, 'Tenant identifier') + ->setHelp('This command lists all user accounts in a tenant.') + ; + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output); + $tenant = $input->getArgument('tenant'); + + $io->title("Users in tenant '{$tenant}'"); + + try { + if (!$this->tenantService->fetchById($tenant)) { + $io->error("Tenant '{$tenant}' not found."); + return Command::FAILURE; + } + + $users = $this->userStore->listUsers($tenant); + + if (empty($users)) { + $io->text('No users found.'); + return Command::SUCCESS; + } + + $rows = []; + foreach ($users as $user) { + $rows[] = [ + $user['uid'] ?? '', + $user['identity'] ?? '', + $user['label'] ?? '', + !empty($user['enabled']) ? 'yes' : 'no', + implode(', ', (array)($user['roles'] ?? [])), + ]; + } + + $io->table(['Uid', 'Identity', 'Label', 'Enabled', 'Roles'], $rows); + $io->text(sprintf('Total: %d user(s)', count($rows))); + + return Command::SUCCESS; + + } catch (\Throwable $e) { + $io->error('Failed to list users: ' . $e->getMessage()); + return Command::FAILURE; + } + } +} diff --git a/core/lib/Module/Module.php b/core/lib/Module/Module.php index 9f85755..2ec73e5 100644 --- a/core/lib/Module/Module.php +++ b/core/lib/Module/Module.php @@ -99,12 +99,18 @@ class Module extends ModuleInstanceAbstract implements ModuleConsoleInterface, M public function registerCI(): array { return [ - \KTXC\Console\ModuleListCommand::class, - \KTXC\Console\ModuleEnableCommand::class, - \KTXC\Console\ModuleDisableCommand::class, - \KTXC\Console\ModuleInstallCommand::class, - \KTXC\Console\ModuleUninstallCommand::class, - \KTXC\Console\ModuleUpgradeCommand::class, + \KTXC\Console\Module\ModuleListCommand::class, + \KTXC\Console\Module\ModuleEnableCommand::class, + \KTXC\Console\Module\ModuleDisableCommand::class, + \KTXC\Console\Module\ModuleInstallCommand::class, + \KTXC\Console\Module\ModuleUninstallCommand::class, + \KTXC\Console\Module\ModuleUpgradeCommand::class, + \KTXC\Console\Tenant\TenantCreateCommand::class, + \KTXC\Console\Tenant\TenantListCommand::class, + \KTXC\Console\Tenant\TenantDeleteCommand::class, + \KTXC\Console\User\UserCreateCommand::class, + \KTXC\Console\User\UserListCommand::class, + \KTXC\Console\User\UserDeleteCommand::class, ]; }