From 51f7ba05097f9df6f7631eaa2a759f96bfcc1312 Mon Sep 17 00:00:00 2001 From: Constantin Graf Date: Tue, 11 Feb 2025 14:44:59 -0500 Subject: [PATCH] Fixed api token endpoint documentation --- .../Controllers/Api/V1/ApiTokenController.php | 12 +++++++---- .../V1/ApiToken/ApiTokenResource.php | 6 ++++++ .../ApiTokenWithAccessTokenResource.php | 21 +++++++++++++++---- app/Providers/AppServiceProvider.php | 2 ++ routes/api.php | 4 ++-- .../Endpoint/Api/V1/ApiTokenEndpointTest.php | 6 +++--- 6 files changed, 38 insertions(+), 13 deletions(-) diff --git a/app/Http/Controllers/Api/V1/ApiTokenController.php b/app/Http/Controllers/Api/V1/ApiTokenController.php index f590568f..9c7aadf0 100644 --- a/app/Http/Controllers/Api/V1/ApiTokenController.php +++ b/app/Http/Controllers/Api/V1/ApiTokenController.php @@ -59,11 +59,13 @@ class ApiTokenController extends Controller * * @throws AuthorizationException */ - public function revoke(string $apiTokenId): JsonResponse + public function revoke(Token $apiToken): JsonResponse { $user = $this->user(); - $apiToken = $user->tokens()->where('id', $apiTokenId)->firstOrFail(); + if ($apiToken->user_id !== $user->getKey()) { + throw new AuthorizationException('API token does not belong to user'); + } $apiToken->revoke(); @@ -77,11 +79,13 @@ class ApiTokenController extends Controller * * @throws AuthorizationException */ - public function destroy(string $apiTokenId): JsonResponse + public function destroy(Token $apiToken): JsonResponse { $user = $this->user(); - $apiToken = $user->tokens()->where('id', $apiTokenId)->firstOrFail(); + if ($apiToken->user_id !== $user->getKey()) { + throw new AuthorizationException('API token does not belong to user'); + } $apiToken->delete(); diff --git a/app/Http/Resources/V1/ApiToken/ApiTokenResource.php b/app/Http/Resources/V1/ApiToken/ApiTokenResource.php index 773e9afe..7a55b4cd 100644 --- a/app/Http/Resources/V1/ApiToken/ApiTokenResource.php +++ b/app/Http/Resources/V1/ApiToken/ApiTokenResource.php @@ -21,11 +21,17 @@ class ApiTokenResource extends BaseResource public function toArray(Request $request): array { return [ + /** @var string $id ID of the API token, this ID is NOT a UUID */ 'id' => $this->resource->id, + /** @var string $name Name of the API token */ 'name' => $this->resource->name, + /** @var bool $revoked Whether the API token is revoked */ 'revoked' => $this->resource->revoked, + /** @var array $scopes List of scopes that the API token has */ 'scopes' => $this->resource->scopes, + /** @var string $created_at When the API token was created (ISO 8601 format, UTC timezone, example: 2024-02-26T17:17:17Z) */ 'created_at' => $this->formatDateTime($this->resource->created_at), + /** @var string|null $expires_at At what time the API token expires (ISO 8601 format, UTC timezone, example: 2024-02-26T17:17:17Z) */ 'expires_at' => $this->formatDateTime($this->resource->expires_at), ]; } diff --git a/app/Http/Resources/V1/ApiToken/ApiTokenWithAccessTokenResource.php b/app/Http/Resources/V1/ApiToken/ApiTokenWithAccessTokenResource.php index b4b2ab39..8da5150f 100644 --- a/app/Http/Resources/V1/ApiToken/ApiTokenWithAccessTokenResource.php +++ b/app/Http/Resources/V1/ApiToken/ApiTokenWithAccessTokenResource.php @@ -4,13 +4,14 @@ declare(strict_types=1); namespace App\Http\Resources\V1\ApiToken; +use App\Http\Resources\V1\BaseResource; use App\Models\Passport\Token; use Illuminate\Http\Request; /** * @property-read Token $resource */ -class ApiTokenWithAccessTokenResource extends ApiTokenResource +class ApiTokenWithAccessTokenResource extends BaseResource { private string $accessToken; @@ -27,9 +28,21 @@ class ApiTokenWithAccessTokenResource extends ApiTokenResource */ public function toArray(Request $request): array { - $parent = parent::toArray($request); - - return $parent + [ + return [ + /** @var string $id ID of the API token, this ID is NOT a UUID */ + 'id' => $this->resource->id, + /** @var string $name Name of the API token */ + 'name' => $this->resource->name, + /** @var bool $revoked Whether the API token is revoked */ + 'revoked' => $this->resource->revoked, + /** @var array $scopes List of scopes that the API token has */ + 'scopes' => $this->resource->scopes, + /** @var string $created_at When the API token was created (ISO 8601 format, UTC timezone, example: 2024-02-26T17:17:17Z) */ + 'created_at' => $this->formatDateTime($this->resource->created_at), + /** @var string|null $expires_at At what time the API token expires (ISO 8601 format, UTC timezone, example: 2024-02-26T17:17:17Z) */ + 'expires_at' => $this->formatDateTime($this->resource->expires_at), + // Additional fields + /** @var string $access_token Access token that can be used to authenticate requests */ 'access_token' => $this->accessToken, ]; } diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 84bd8f66..9856f21f 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -9,6 +9,7 @@ use App\Models\FailedJob; use App\Models\Member; use App\Models\Organization; use App\Models\OrganizationInvitation; +use App\Models\Passport\Token; use App\Models\Project; use App\Models\ProjectMember; use App\Models\Tag; @@ -100,5 +101,6 @@ class AppServiceProvider extends ServiceProvider // Routing Route::model('member', Member::class); Route::model('invitation', OrganizationInvitation::class); + Route::model('apiToken', Token::class); } } diff --git a/routes/api.php b/routes/api.php index 12fb239e..ac3fc5c3 100644 --- a/routes/api.php +++ b/routes/api.php @@ -62,8 +62,8 @@ Route::prefix('v1')->name('v1.')->group(static function (): void { Route::name('api-tokens.')->group(static function (): void { Route::get('/users/me/api-tokens', [ApiTokenController::class, 'index'])->name('index'); Route::post('/users/me/api-tokens', [ApiTokenController::class, 'store'])->name('store'); - Route::post('/users/me/api-tokens/{apiTokenId}/revoke', [ApiTokenController::class, 'revoke'])->name('revoke'); - Route::delete('/users/me/api-tokens/{apiTokenId}', [ApiTokenController::class, 'destroy'])->name('destroy'); + Route::post('/users/me/api-tokens/{apiToken}/revoke', [ApiTokenController::class, 'revoke'])->name('revoke'); + Route::delete('/users/me/api-tokens/{apiToken}', [ApiTokenController::class, 'destroy'])->name('destroy'); }); // User Member routes diff --git a/tests/Unit/Endpoint/Api/V1/ApiTokenEndpointTest.php b/tests/Unit/Endpoint/Api/V1/ApiTokenEndpointTest.php index 3ad238dc..e7c3ec9e 100644 --- a/tests/Unit/Endpoint/Api/V1/ApiTokenEndpointTest.php +++ b/tests/Unit/Endpoint/Api/V1/ApiTokenEndpointTest.php @@ -54,7 +54,7 @@ class ApiTokenEndpointTest extends ApiEndpointTestAbstract Passport::actingAs($data->user); // Act - $response = $this->withoutExceptionHandling()->postJson(route('api.v1.api-tokens.store'), [ + $response = $this->postJson(route('api.v1.api-tokens.store'), [ 'name' => 'Test Token', ]); @@ -118,7 +118,7 @@ class ApiTokenEndpointTest extends ApiEndpointTestAbstract $response = $this->postJson(route('api.v1.api-tokens.revoke', $token->id)); // Assert - $this->assertResponseCode($response, 404); + $this->assertResponseCode($response, 403); $this->assertDatabaseHas(Token::class, [ 'id' => $token->id, 'revoked' => false, @@ -167,7 +167,7 @@ class ApiTokenEndpointTest extends ApiEndpointTestAbstract $response = $this->deleteJson(route('api.v1.api-tokens.destroy', $token->id)); // Assert - $this->assertResponseCode($response, 404); + $this->assertResponseCode($response, 403); $this->assertDatabaseHas(Token::class, [ 'id' => $token->id, ]);