Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
92.31% covered (success)
92.31%
24 / 26
66.67% covered (warning)
66.67%
2 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
GrantAdminCommand
92.31% covered (success)
92.31%
24 / 26
66.67% covered (warning)
66.67%
2 / 3
8.03
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 configure
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 execute
90.91% covered (success)
90.91%
20 / 22
0.00% covered (danger)
0.00%
0 / 1
6.03
1<?php
2
3declare(strict_types=1);
4
5namespace App\Identity\Infrastructure\Console;
6
7use App\Identity\Domain\UserRepository;
8use App\Identity\Domain\ValueObject\Email;
9use InvalidArgumentException;
10use Symfony\Component\Console\Attribute\AsCommand;
11use Symfony\Component\Console\Command\Command;
12use Symfony\Component\Console\Input\InputArgument;
13use Symfony\Component\Console\Input\InputInterface;
14use Symfony\Component\Console\Input\InputOption;
15use Symfony\Component\Console\Output\OutputInterface;
16use Symfony\Component\Console\Style\SymfonyStyle;
17
18/**
19 * Bootstraps the first admin, and provides the out-of-band escape hatch when no
20 * admin is reachable (the admin API can only be used by an existing admin).
21 *
22 * Deliberately a command and not a migration seeding a fixed email: a hardcoded
23 * address is wrong in every environment but one, replays on every fresh database,
24 * and cannot be undone per environment. This runs where and when an operator says so.
25 */
26#[AsCommand(
27    name: 'app:user:grant-admin',
28    description: 'Grant (or revoke, with --revoke) admin rights to the user with the given email.',
29)]
30final class GrantAdminCommand extends Command
31{
32    public function __construct(private readonly UserRepository $users)
33    {
34        parent::__construct();
35    }
36
37    protected function configure(): void
38    {
39        $this
40            ->addArgument('email', InputArgument::REQUIRED, 'Email of an existing user')
41            ->addOption('revoke', null, InputOption::VALUE_NONE, 'Revoke admin instead of granting it');
42    }
43
44    protected function execute(InputInterface $input, OutputInterface $output): int
45    {
46        $io = new SymfonyStyle($input, $output);
47
48        $address = $input->getArgument('email');
49        if (!is_string($address)) {
50            $io->error('The email argument must be a string.');
51
52            return Command::INVALID;
53        }
54
55        try {
56            $email = new Email($address);
57        } catch (InvalidArgumentException $e) {
58            $io->error($e->getMessage());
59
60            return Command::INVALID;
61        }
62
63        $user = $this->users->ofEmail($email);
64        if ($user === null) {
65            $io->error(sprintf('No user registered with <%s>.', $address));
66
67            return Command::FAILURE;
68        }
69
70        $revoke = $input->getOption('revoke') === true;
71        $revoke ? $user->revokeAdmin() : $user->grantAdmin();
72        $this->users->save($user);
73
74        $io->success(sprintf(
75            'Admin %s for <%s>.',
76            $revoke ? 'revoked' : 'granted',
77            $address,
78        ));
79
80        return Command::SUCCESS;
81    }
82}