-
Notifications
You must be signed in to change notification settings - Fork 27
Expand file tree
/
Copy pathCreateApiTokenCommand.php
More file actions
97 lines (81 loc) · 3.37 KB
/
Copy pathCreateApiTokenCommand.php
File metadata and controls
97 lines (81 loc) · 3.37 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
<?php
/*
* Copyright (c) 2026 Netresearch DTT GmbH
* SPDX-License-Identifier: AGPL-3.0-only
*/
declare(strict_types=1);
namespace App\Command;
use App\Entity\User;
use App\Service\ApiToken\ApiTokenService;
use App\Service\ClockInterface;
use App\ValueObject\ApiScope;
use DateMalformedStringException;
use Doctrine\ORM\EntityManagerInterface;
use InvalidArgumentException;
use Symfony\Component\Console\Attribute\Argument;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Attribute\Option;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Style\SymfonyStyle;
use function implode;
use function sprintf;
/**
* Mint an API personal access token for a user (ADR-021), for cron/bootstrap use.
* The plaintext is printed ONCE — it is not recoverable afterwards. Scopes narrow
* the user's access; enforcement is the Bearer firewall (Phase 2).
*/
#[AsCommand(name: 'app:api-token:create', description: 'Create an API personal access token for a user')]
final readonly class CreateApiTokenCommand
{
public function __construct(
private EntityManagerInterface $entityManager,
private ApiTokenService $apiTokenService,
private ClockInterface $clock,
) {
}
/**
* @param list<string> $scope
*/
public function __invoke(
SymfonyStyle $io,
#[Argument(description: 'Login username the token acts as')]
string $username,
#[Argument(description: 'A label for the token (e.g. "ci-worklog")')]
string $name,
#[Option(description: 'Scope in resource:action form; repeatable. Use "*" for all the user can grant.', name: 'scope')]
array $scope = [],
#[Option(description: 'Optional expiry, e.g. "+90 days" or "2026-12-31"')]
?string $expires = null,
): int {
$user = $this->entityManager->getRepository(User::class)->findOneBy(['username' => $username]);
if (!$user instanceof User) {
$io->error(sprintf('No user "%s".', $username));
return Command::FAILURE;
}
if ([] === $scope) {
$io->error('At least one --scope is required. Valid scopes: ' . implode(', ', ApiScope::all()));
return Command::FAILURE;
}
$expiresAt = null;
if (null !== $expires && '' !== $expires) {
try {
// Relative to the injected clock (not the system clock) so the
// stored expiry matches the app's time and stays testable.
$expiresAt = $this->clock->now()->modify($expires);
} catch (DateMalformedStringException) {
$io->error('Invalid --expires. Use e.g. "+90 days" or "2026-12-31".');
return Command::FAILURE;
}
}
try {
[$token, $plaintext] = $this->apiTokenService->create($user, $name, $scope, $expiresAt);
} catch (InvalidArgumentException $invalidArgumentException) {
$io->error($invalidArgumentException->getMessage() . ' Valid scopes: ' . implode(', ', ApiScope::all()));
return Command::FAILURE;
}
$io->success(sprintf('Created token "%s" for %s (scopes: %s).', $name, $username, implode(', ', $token->getScopes())));
$io->writeln('Token (shown once — store it now):');
$io->writeln(' <info>' . $plaintext . '</info>');
return Command::SUCCESS;
}
}