Skip to content

Commit b2abd2c

Browse files
authored
Merge pull request #10 from TomHAnderson/feature/regenerate
Add command to regenerate api_key
2 parents 03dcd32 + 7257968 commit b2abd2c

File tree

6 files changed

+150
-3
lines changed

6 files changed

+150
-3
lines changed

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,11 @@ Unassign a Scope from an ApiKey
142142
php artisan apikey:scope:remove {apiKeyName} {scopeName}
143143
```
144144

145+
Regenerate an ApiKey (assign a new Bearer token)
146+
```shell
147+
php artisan apikey:regenerate {name}
148+
```
149+
145150
Delete a Scope
146151
```shell
147152
php artisan apikey:scope:delete {scopeName}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace ApiSkeletons\Laravel\Doctrine\ApiKey\Console\Command;
6+
7+
use ApiSkeletons\Laravel\Doctrine\ApiKey\Entity\ApiKey;
8+
9+
// phpcs:disable SlevomatCodingStandard.TypeHints.PropertyTypeHint.MissingAnyTypeHint
10+
final class RegenerateApiKey extends Command
11+
{
12+
/**
13+
* The name and signature of the console command.
14+
*/
15+
protected $signature = 'apikey:regenerate {name}';
16+
17+
/**
18+
* The console command description.
19+
*/
20+
protected $description = 'Regenerate an apikey';
21+
22+
/**
23+
* Execute the console command.
24+
*/
25+
public function handle(): mixed
26+
{
27+
$name = $this->argument('name');
28+
29+
$apiKeyRepository = $this->apiKeyService->getEntityManager()
30+
->getRepository(ApiKey::class);
31+
32+
$apiKey = $apiKeyRepository->findOneBy(['name' => $name]);
33+
34+
if (! $apiKey) {
35+
$this->error('ApiKey not found by name: ' . $name);
36+
37+
return 1;
38+
}
39+
40+
$apiKey = $apiKeyRepository->regenerate($apiKey);
41+
42+
$this->apiKeyService->getEntityManager()->flush();
43+
$this->printApiKeys([$apiKey]);
44+
45+
return 0;
46+
}
47+
}

src/Repository/ApiKeyRepository.php

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,21 @@
1313
use ApiSkeletons\Laravel\Doctrine\ApiKey\Exception\InvalidName;
1414
use DateTime;
1515
use Doctrine\ORM\EntityRepository;
16+
use Doctrine\ORM\ORMException;
1617
use Illuminate\Support\Str;
1718

1819
use function preg_match;
1920
use function request;
2021

2122
class ApiKeyRepository extends EntityRepository
2223
{
24+
/**
25+
* Create a new ApiKey entity
26+
*
27+
* @throws DuplicateName
28+
* @throws InvalidName
29+
* @throws ORMException
30+
*/
2331
public function generate(string $name): ApiKey
2432
{
2533
// Verify name is unique
@@ -33,9 +41,7 @@ public function generate(string $name): ApiKey
3341
throw new InvalidName('Please provide a valid name: [a-z0-9-]');
3442
}
3543

36-
do {
37-
$key = Str::random(64);
38-
} while ($this->findBy(['api_key' => $key]));
44+
$key = $this->generateKey();
3945

4046
$apiKey = new ApiKey();
4147
$apiKey
@@ -51,6 +57,31 @@ public function generate(string $name): ApiKey
5157
return $apiKey;
5258
}
5359

60+
/**
61+
* Assign a new api_key to an existing ApiKey entity
62+
*/
63+
public function regenerate(ApiKey $apiKey): ApiKey
64+
{
65+
$apiKey->setApiKey($this->generateKey());
66+
67+
return $apiKey;
68+
}
69+
70+
/**
71+
* Generate a unique api_key
72+
*/
73+
protected function generateKey(): string
74+
{
75+
do {
76+
$key = Str::random(64);
77+
} while ($this->findBy(['api_key' => $key]));
78+
79+
return $key;
80+
}
81+
82+
/**
83+
* Change the active status of an ApiKey entity
84+
*/
5485
public function updateActive(ApiKey $apiKey, bool $status): ApiKey
5586
{
5687
$apiKey
@@ -63,6 +94,11 @@ public function updateActive(ApiKey $apiKey, bool $status): ApiKey
6394
return $apiKey;
6495
}
6596

97+
/**
98+
* Add an existing scope to an existing ApiKey entity
99+
*
100+
* @throws DuplicateScopeForApiKey
101+
*/
66102
public function addScope(ApiKey $apiKey, Scope $scope): ApiKey
67103
{
68104
// Do not add scopes twice
@@ -80,6 +116,11 @@ public function addScope(ApiKey $apiKey, Scope $scope): ApiKey
80116
return $apiKey;
81117
}
82118

119+
/**
120+
* Remove a scope from an ApiKey
121+
*
122+
* @throws ApiKeyDoesNotHaveScope
123+
*/
83124
public function removeScope(ApiKey $apiKey, Scope $scope): ApiKey
84125
{
85126
$found = false;
@@ -104,11 +145,17 @@ public function removeScope(ApiKey $apiKey, Scope $scope): ApiKey
104145
return $apiKey;
105146
}
106147

148+
/**
149+
* Validate an API key name
150+
*/
107151
public function isValidName(string $name): bool
108152
{
109153
return (bool) preg_match('/^[a-z0-9-]{1,255}$/', $name);
110154
}
111155

156+
/**
157+
* Create a new entity for logging admin events whenever one is triggered
158+
*/
112159
protected function logAdminEvent(ApiKey $apiKey, string $eventName): AdminEvent
113160
{
114161
$adminEvent = (new AdminEvent())

src/ServiceProvider.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ public function boot(): void
3535
Console\Command\GenerateScope::class,
3636
Console\Command\PrintApiKey::class,
3737
Console\Command\PrintScope::class,
38+
Console\Command\RegenerateApiKey::class,
3839
Console\Command\RemoveScope::class,
3940
]);
4041
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
3+
namespace ApiSkeletonsTest\Laravel\Doctrine\ApiKey\Feature\Console\Command;
4+
5+
use ApiSkeletons\Laravel\Doctrine\ApiKey\Entity\ApiKey;
6+
use ApiSkeletons\Laravel\Doctrine\ApiKey\Entity\Scope;
7+
use ApiSkeletonsTest\Laravel\Doctrine\ApiKey\TestCase;
8+
use DateTime;
9+
10+
final class ReenerateApiKeyTest extends TestCase
11+
{
12+
public function testReenerateApiKey(): void
13+
{
14+
$entityManager = $this->createDatabase(app('em'));
15+
16+
$apiKey = $entityManager->getRepository(ApiKey::class)
17+
->generate('testing');
18+
$entityManager->flush();
19+
20+
$this->artisan('apikey:regenerate', [
21+
'name' => 'testing',
22+
])->assertExitCode(0);
23+
}
24+
25+
public function testInvalidNameThrowsError(): void
26+
{
27+
$this->createDatabase(app('em'));
28+
29+
$this->artisan('apikey:regenerate', [
30+
'name' => 'test^ing',
31+
])->assertExitCode(1);
32+
}
33+
}

test/Feature/Repository/ApiKeyRepositoryTest.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,20 @@ public function testGenerate(): void
3636
}
3737
}
3838

39+
public function testRegenerate(): void
40+
{
41+
$entityManager = $this->createDatabase(app('em'));
42+
$repository = $entityManager->getRepository(ApiKey::class);
43+
44+
$apiKey = $repository->generate('testing');
45+
$entityManager->flush();
46+
47+
$oldKey = $apiKey->getApiKey();
48+
$apiKey = $repository->regenerate($apiKey);
49+
50+
$this->assertNotEquals($oldKey, $apiKey->getApiKey());
51+
}
52+
3953
public function testGenerateValidatesName(): void
4054
{
4155
$this->expectException(InvalidName::class);

0 commit comments

Comments
 (0)