Skip to content

Commit

Permalink
Added API endpoints for user API tokens
Browse files Browse the repository at this point in the history
  • Loading branch information
korridor committed Feb 11, 2025
1 parent d924fa7 commit ade60a0
Show file tree
Hide file tree
Showing 21 changed files with 654 additions and 11 deletions.
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ S3_URL=http://storage.solidtime.test/local
S3_ENDPOINT=http://storage.solidtime.test
S3_USE_PATH_STYLE_ENDPOINT=true

# Passport
PASSPORT_PERSONAL_ACCESS_CLIENT_ID="9e27f54d-5dfb-4dde-99d7-834518236c92"
PASSPORT_PERSONAL_ACCESS_CLIENT_SECRET="EL5mXp3aF8ITjcwoOXRpbSK7zGrWhW4zTDpQXTkf"

# Services
GOTENBERG_URL=http://gotenberg:3000

Expand Down
84 changes: 84 additions & 0 deletions app/Http/Controllers/Api/V1/ApiTokenController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<?php

declare(strict_types=1);

namespace App\Http\Controllers\Api\V1;

use App\Http\Requests\V1\ApiToken\ApiTokenStoreRequest;
use App\Http\Resources\V1\ApiToken\ApiTokenCollection;
use App\Http\Resources\V1\ApiToken\ApiTokenWithAccessTokenResource;
use App\Models\Passport\Token;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Http\JsonResponse;

class ApiTokenController extends Controller
{
/**
* List all api token of the currently authenticated user
*
* This endpoint is independent of organization.
*
* @operationId getApiTokens
*
* @throws AuthorizationException
*/
public function index(): ApiTokenCollection
{
$user = $this->user();

$tokens = $user->tokens()->get();

return new ApiTokenCollection($tokens);
}

/**
* Create a new api token for the currently authenticated user
*
* The response will contain the access token that can be used to send authenticated API requests.
* Please note that the access token is only shown in this response and cannot be retrieved later.
*
* @throws AuthorizationException
*/
public function store(ApiTokenStoreRequest $request): ApiTokenWithAccessTokenResource
{
$user = $this->user();

$token = $user->createToken($request->getName(), ['*']);
/** @var Token $tokenModel */
$tokenModel = $token->token;

return new ApiTokenWithAccessTokenResource($tokenModel, $token->accessToken);
}

/**
* Revoke an api token
*
* @throws AuthorizationException
*/
public function revoke(string $apiTokenId): JsonResponse
{
$user = $this->user();

$apiToken = $user->tokens()->where('id', $apiTokenId)->firstOrFail();

$apiToken->revoke();

return response()->json(null, 204);
}

/**
* Delete an api token
*
* @throws AuthorizationException
*/
public function destroy(string $apiTokenId): JsonResponse
{
$user = $this->user();

$apiToken = $user->tokens()->where('id', $apiTokenId)->firstOrFail();

$apiToken->delete();

return response()->json(null, 204);
}
}
32 changes: 32 additions & 0 deletions app/Http/Requests/V1/ApiToken/ApiTokenStoreRequest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

declare(strict_types=1);

namespace App\Http\Requests\V1\ApiToken;

use Illuminate\Foundation\Http\FormRequest;

class ApiTokenStoreRequest extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array<string, array<string>>
*/
public function rules(): array
{
return [
'name' => [
'required',
'string',
'min:1',
'max:255',
],
];
}

public function getName(): string
{
return $this->input('name');
}
}
17 changes: 17 additions & 0 deletions app/Http/Resources/V1/ApiToken/ApiTokenCollection.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

declare(strict_types=1);

namespace App\Http\Resources\V1\ApiToken;

use Illuminate\Http\Resources\Json\ResourceCollection;

class ApiTokenCollection extends ResourceCollection
{
/**
* The resource that this resource collects.
*
* @var string
*/
public $collects = ApiTokenResource::class;
}
32 changes: 32 additions & 0 deletions app/Http/Resources/V1/ApiToken/ApiTokenResource.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

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 ApiTokenResource extends BaseResource
{
/**
* Transform the resource into an array.
*
* @return array<string, string|bool|int|null|array<string>>
*/
public function toArray(Request $request): array
{
return [
'id' => $this->resource->id,
'name' => $this->resource->name,
'revoked' => $this->resource->revoked,
'scopes' => $this->resource->scopes,
'created_at' => $this->formatDateTime($this->resource->created_at),
'expires_at' => $this->formatDateTime($this->resource->expires_at),
];
}
}
36 changes: 36 additions & 0 deletions app/Http/Resources/V1/ApiToken/ApiTokenWithAccessTokenResource.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

declare(strict_types=1);

namespace App\Http\Resources\V1\ApiToken;

use App\Models\Passport\Token;
use Illuminate\Http\Request;

/**
* @property-read Token $resource
*/
class ApiTokenWithAccessTokenResource extends ApiTokenResource
{
private string $accessToken;

public function __construct(Token $resource, string $accessToken)
{
$this->accessToken = $accessToken;
parent::__construct($resource);
}

/**
* Transform the resource into an array.
*
* @return array<string, string|bool|int|null|array<string>>
*/
public function toArray(Request $request): array
{
$parent = parent::toArray($request);

return $parent + [
'access_token' => $this->accessToken,
];
}
}
9 changes: 9 additions & 0 deletions app/Models/Passport/AuthCode.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

declare(strict_types=1);

namespace App\Models\Passport;

use Laravel\Passport\AuthCode as PassportAuthCode;

class AuthCode extends PassportAuthCode {}
26 changes: 26 additions & 0 deletions app/Models/Passport/Client.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

declare(strict_types=1);

namespace App\Models\Passport;

use Database\Factories\Passport\ClientFactory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Laravel\Passport\Client as PassportClient;

/**
* @property string $id
* @property string|null $user_id
* @property string $name
* @property string|null $secret
* @property string|null $provider
* @property string $redirect
* @property bool $personal_access_client
* @property bool $password_client
* @property bool $revoked
*/
class Client extends PassportClient
{
/** @use HasFactory<ClientFactory> */
use HasFactory;
}
9 changes: 9 additions & 0 deletions app/Models/Passport/PersonalAccessClient.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

declare(strict_types=1);

namespace App\Models\Passport;

use Laravel\Passport\PersonalAccessClient as PassportPersonalAccessClient;

class PersonalAccessClient extends PassportPersonalAccessClient {}
9 changes: 9 additions & 0 deletions app/Models/Passport/RefreshToken.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

declare(strict_types=1);

namespace App\Models\Passport;

use Laravel\Passport\RefreshToken as PassportRefreshToken;

class RefreshToken extends PassportRefreshToken {}
27 changes: 27 additions & 0 deletions app/Models/Passport/Token.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

declare(strict_types=1);

namespace App\Models\Passport;

use Database\Factories\Passport\TokenFactory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Support\Carbon;
use Laravel\Passport\Token as PassportToken;

/**
* @property string $id
* @property null|string $user_id
* @property string $client_id
* @property null|string $name
* @property array<string> $scopes
* @property bool $revoked
* @property Carbon|null $created_at
* @property Carbon|null $updated_at
* @property Carbon|null $expires_at
*/
class Token extends PassportToken
{
/** @use HasFactory<TokenFactory> */
use HasFactory;
}
14 changes: 13 additions & 1 deletion app/Models/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use App\Enums\Weekday;
use App\Models\Concerns\CustomAuditable;
use App\Models\Concerns\HasUuids;
use App\Models\Passport\Token;
use Database\Factories\UserFactory;
use Filament\Models\Contracts\FilamentUser;
use Filament\Panel;
Expand All @@ -27,7 +28,6 @@
use Laravel\Jetstream\HasTeams;
use Laravel\Passport\AuthCode;
use Laravel\Passport\HasApiTokens;
use Laravel\Passport\Token;
use OwenIt\Auditing\Contracts\Auditable as AuditableContract;

/**
Expand All @@ -44,6 +44,7 @@
* @property-read Organization|null $currentOrganization
* @property-read Organization|null $currentTeam
* @property-read string $profile_photo_url
* @property-read Collection<int, Token> $tokens
* @property Carbon|null $created_at
* @property Carbon|null $updated_at
* @property string|null $current_team_id
Expand Down Expand Up @@ -196,6 +197,17 @@ public function authCodes(): HasMany
return $this->hasMany(AuthCode::class);
}

/**
* Get the access tokens for the user.
*
* @return HasMany<Token>
*/
public function tokens(): HasMany
{
return $this->hasMany(Token::class, 'user_id')
->orderBy('created_at', 'desc');
}

/**
* @param Builder<User> $builder
*/
Expand Down
15 changes: 15 additions & 0 deletions app/Providers/AuthServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@
namespace App\Providers;

use App\Models\Organization;
use App\Models\Passport\AuthCode;
use App\Models\Passport\Client;
use App\Models\Passport\PersonalAccessClient;
use App\Models\Passport\RefreshToken;
use App\Models\Passport\Token;
use App\Policies\OrganizationPolicy;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
use Laravel\Jetstream\Jetstream;
Expand Down Expand Up @@ -42,6 +47,16 @@ public function boot(): void
// 'delete',
]);

Passport::useTokenModel(Token::class);
Passport::useRefreshTokenModel(RefreshToken::class);
Passport::useAuthCodeModel(AuthCode::class);
Passport::useClientModel(Client::class);
Passport::usePersonalAccessClientModel(PersonalAccessClient::class);

// Passport::tokensExpireIn(now()->addDays(15));
// Passport::refreshTokensExpireIn(now()->addDays(30));
Passport::personalAccessTokensExpireIn(now()->addMonths(12));

// same as passport default above
Jetstream::defaultApiTokenPermissions(['read']);

Expand Down
2 changes: 1 addition & 1 deletion config/passport.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
|
*/

'guard' => 'web',
'guard' => 'api',

/*
|--------------------------------------------------------------------------
Expand Down
Loading

0 comments on commit ade60a0

Please sign in to comment.