Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 131
0.00% covered (danger)
0.00%
0 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
CreateUserCommand
0.00% covered (danger)
0.00%
0 / 131
0.00% covered (danger)
0.00%
0 / 4
306
0.00% covered (danger)
0.00%
0 / 1
 buildOptionParser
0.00% covered (danger)
0.00%
0 / 34
0.00% covered (danger)
0.00%
0 / 1
2
 execute
0.00% covered (danger)
0.00%
0 / 28
0.00% covered (danger)
0.00%
0 / 1
132
 createUser
0.00% covered (danger)
0.00%
0 / 38
0.00% covered (danger)
0.00%
0 / 1
6
 updateUserPassword
0.00% covered (danger)
0.00%
0 / 31
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2declare(strict_types=1);
3
4namespace App\Command;
5
6use Cake\Command\Command;
7use Cake\Console\Arguments;
8use Cake\Console\ConsoleIo;
9use Cake\Console\ConsoleOptionParser;
10use Cake\Log\LogTrait;
11use Cake\ORM\Table; // Added for type hinting
12
13/**
14 * Command for creating a user or updating a user's password in the database.
15 */
16class CreateUserCommand extends Command
17{
18    use LogTrait;
19
20    /**
21     * Builds the option parser for the command.
22     *
23     * @param \Cake\Console\ConsoleOptionParser $parser The console option parser.
24     * @return \Cake\Console\ConsoleOptionParser The configured console option parser.
25     */
26    protected function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser
27    {
28        $parser = parent::buildOptionParser($parser);
29
30        $parser
31            ->setDescription('Creates a user or updates an existing user\'s password.')
32            ->addOption('update-password', [
33                'help' => 'Flag to update password for existing user. If set, 
34                    --email and --password are used to find and update.',
35                'boolean' => true,
36                'default' => false,
37            ])
38            ->addOption('username', [
39                'short' => 'u',
40                'help' => 'Username for the user (required for creation).',
41                'default' => null,
42                'required' => false, // Made false, will be validated in execute
43            ])
44            ->addOption('password', [
45                'short' => 'p',
46                'help' => 'Password for the user (or new password if updating). Required.',
47                'default' => null,
48                'required' => true, // Always required (for create or for new password in update)
49            ])
50            ->addOption('email', [
51                'short' => 'e',
52                'help' => 'Email for the user (used to find user if updating). Required.',
53                'default' => null,
54                'required' => true, // Always required (for create or to find user in update)
55            ])
56            ->addOption('is_admin', [
57                'short' => 'a',
58                'help' => 'Is the user an admin? (1 for true, 0 for false; required for creation).',
59                'default' => null,
60                'required' => false, // Made false, will be validated in execute
61            ]);
62
63        return $parser;
64    }
65
66    /**
67     * Executes the command.
68     *
69     * @param \Cake\Console\Arguments $args The command arguments.
70     * @param \Cake\Console\ConsoleIo $io The console I/O.
71     * @return int The exit code.
72     */
73    public function execute(Arguments $args, ConsoleIo $io): int
74    {
75        $usersTable = $this->fetchTable('Users');
76
77        if ($args->getOption('update-password')) {
78            // Validate required args for update
79            if (!$args->getOption('email') || !$args->getOption('password')) {
80                $io->error('For password update, --email and --password (new password) are required.');
81                $this->abort();
82            }
83            if ($this->updateUserPassword($args, $io, $usersTable)) {
84                $io->success('User password updated successfully.');
85
86                return static::CODE_SUCCESS;
87            }
88            $io->error('Failed to update user password.');
89
90            return static::CODE_ERROR;
91        } else {
92            // Validate required args for create
93            $missingCreateArgs = [];
94            if (!$args->getOption('username')) {
95                $missingCreateArgs[] = '--username';
96            }
97            if (!$args->getOption('password')) {
98                $missingCreateArgs[] = '--password';
99            }
100            if (!$args->getOption('email')) {
101                $missingCreateArgs[] = '--email';
102            }
103            if ($args->getOption('is_admin') === null) {
104                $missingCreateArgs[] = '--is_admin';
105            }
106
107            if (!empty($missingCreateArgs)) {
108                $io->error('For user creation, the following options are required: '
109                . implode(', ', $missingCreateArgs));
110                $this->abort();
111            }
112
113            if ($this->createUser($args, $io, $usersTable)) {
114                $io->success('User created successfully.');
115
116                return static::CODE_SUCCESS;
117            }
118            $io->error('Failed to create user.');
119
120            return static::CODE_ERROR;
121        }
122    }
123
124    /**
125     * Creates a user with the provided arguments.
126     *
127     * @param \Cake\Console\Arguments $args The command arguments.
128     * @param \Cake\Console\ConsoleIo $io The console I/O.
129     * @param \Cake\ORM\Table $usersTable The users table.
130     * @return bool True if the user was created successfully, false otherwise.
131     */
132    private function createUser(Arguments $args, ConsoleIo $io, Table $usersTable): bool
133    {
134        $data = [
135            'username' => $args->getOption('username'),
136            'password' => $args->getOption('password'),
137            'confirm_password' => $args->getOption('password'),
138            'email' => $args->getOption('email'),
139            'is_admin' => in_array($args->getOption('is_admin'), ['1', 1, true], true), // flexible boolean check
140            'active' => 1,
141        ];
142
143        $logData = $data;
144        unset($logData['password']);
145
146        $this->log(
147            sprintf('Attempting to create user with data: %s', json_encode($logData)),
148            'info',
149            ['scope' => ['user_management', 'user_creation']],
150        );
151
152        /** @var \App\Model\Entity\User $user */
153        $user = $usersTable->newEmptyEntity();
154        // Allow mass assignment for these fields during creation
155        $user->setAccess('is_admin', true);
156        $user->setAccess('active', true);
157        $user = $usersTable->patchEntity($user, $data);
158
159        if ($usersTable->save($user)) {
160            $this->log(
161                sprintf('User created successfully: %s (ID: %s)', $user->username, $user->id),
162                'info',
163                ['scope' => ['user_management', 'user_creation']],
164            );
165            $io->out(sprintf('User "%s" created with ID: %s', $user->username, $user->id));
166
167            return true;
168        }
169
170        $this->log(
171            sprintf(
172                'Failed to create user: %s. Errors: %s',
173                $data['username'],
174                json_encode($user->getErrors()),
175            ),
176            'error',
177            ['scope' => ['user_management', 'user_creation']],
178        );
179        $io->error(sprintf('Could not create user. Errors: %s', json_encode($user->getErrors())));
180
181        return false;
182    }
183
184    /**
185     * Updates the password for an existing user found by email.
186     *
187     * @param \Cake\Console\Arguments $args The command arguments.
188     * @param \Cake\Console\ConsoleIo $io The console I/O.
189     * @param \Cake\ORM\Table $usersTable The users table.
190     * @return bool True if the user password was updated successfully, false otherwise.
191     */
192    private function updateUserPassword(Arguments $args, ConsoleIo $io, Table $usersTable): bool
193    {
194        $email = $args->getOption('email');
195        $newPassword = $args->getOption('password');
196
197        /** @var \App\Model\Entity\User|null $user */
198        $user = $usersTable->findByEmail($email)->first();
199
200        if (!$user) { // Check if user was found
201            $io->warning(sprintf('User with email "%s" not found.', $email));
202            $this->log(
203                sprintf('Password update failed: User with email "%s" not found.', $email),
204                'warning',
205                ['scope' => ['user_management', 'password_update']],
206            );
207
208            return false;
209        }
210
211        // Patch entity with the new password.
212        // The User entity's setter for 'password' should handle hashing.
213        $usersTable->patchEntity($user, ['password' => $newPassword]);
214
215        // Ensure no other fields are accidentally changed if they were passed
216        // (e.g. if username was passed, it shouldn't update username here)
217        // For password update, we only care about the password field.
218        // $user->set('password', $newPassword); // This is another way if you directly want to set it.
219                                              // The patchEntity approach is fine if _setPassword handles hashing.
220
221        if ($usersTable->save($user)) {
222            $this->log(
223                sprintf('Password updated successfully for user: %s (ID: %s)', $user->email, $user->id),
224                'info',
225                ['scope' => ['user_management', 'password_update']],
226            );
227            $io->out(sprintf('Password updated for user with email: %s', $email));
228
229            return true;
230        }
231
232        $this->log(
233            sprintf(
234                'Failed to update password for user: %s. Errors: %s',
235                $user->email,
236                json_encode($user->getErrors()),
237            ),
238            'error',
239            ['scope' => ['user_management', 'password_update']],
240        );
241        $io->error(sprintf('Could not update password. Errors: %s', json_encode($user->getErrors())));
242
243        return false;
244    }
245}