mirror of
https://github.com/flarum/core.git
synced 2025-08-05 07:57:46 +02:00
feat: AccessTokenResource
This commit is contained in:
@@ -40,6 +40,7 @@ class ApiServiceProvider extends AbstractServiceProvider
|
||||
Resource\PostResource::class,
|
||||
Resource\DiscussionResource::class,
|
||||
Resource\NotificationResource::class,
|
||||
Resource\AccessTokenResource::class,
|
||||
];
|
||||
});
|
||||
|
||||
|
@@ -1,60 +0,0 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Api\Controller;
|
||||
|
||||
use Flarum\Api\Serializer\AccessTokenSerializer;
|
||||
use Flarum\Http\DeveloperAccessToken;
|
||||
use Flarum\Http\Event\DeveloperTokenCreated;
|
||||
use Flarum\Http\RequestUtil;
|
||||
use Illuminate\Contracts\Events\Dispatcher;
|
||||
use Illuminate\Contracts\Validation\Factory;
|
||||
use Illuminate\Support\Arr;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Tobscure\JsonApi\Document;
|
||||
|
||||
/**
|
||||
* Not to be confused with the CreateTokenController,
|
||||
* this controller is used by the actor to manually create a developer type access token.
|
||||
*/
|
||||
class CreateAccessTokenController extends AbstractCreateController
|
||||
{
|
||||
public ?string $serializer = AccessTokenSerializer::class;
|
||||
|
||||
public function __construct(
|
||||
protected Dispatcher $events,
|
||||
protected Factory $validation
|
||||
) {
|
||||
}
|
||||
|
||||
public function data(ServerRequestInterface $request, Document $document): DeveloperAccessToken
|
||||
{
|
||||
$actor = RequestUtil::getActor($request);
|
||||
|
||||
$actor->assertRegistered();
|
||||
$actor->assertCan('createAccessToken');
|
||||
|
||||
$title = Arr::get($request->getParsedBody(), 'data.attributes.title');
|
||||
|
||||
$this->validation->make(compact('title'), [
|
||||
'title' => 'required|string|max:255',
|
||||
])->validate();
|
||||
|
||||
$token = DeveloperAccessToken::generate($actor->id);
|
||||
|
||||
$token->title = $title;
|
||||
$token->last_activity_at = null;
|
||||
|
||||
$token->save();
|
||||
|
||||
$this->events->dispatch(new DeveloperTokenCreated($token));
|
||||
|
||||
return $token;
|
||||
}
|
||||
}
|
@@ -1,46 +0,0 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Api\Controller;
|
||||
|
||||
use Flarum\Http\AccessToken;
|
||||
use Flarum\Http\RequestUtil;
|
||||
use Flarum\User\Exception\PermissionDeniedException;
|
||||
use Illuminate\Contracts\Session\Session;
|
||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||
use Illuminate\Support\Arr;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
|
||||
class DeleteAccessTokenController extends AbstractDeleteController
|
||||
{
|
||||
protected function delete(ServerRequestInterface $request): void
|
||||
{
|
||||
$actor = RequestUtil::getActor($request);
|
||||
$id = Arr::get($request->getQueryParams(), 'id');
|
||||
|
||||
$actor->assertRegistered();
|
||||
|
||||
$token = AccessToken::query()->findOrFail($id);
|
||||
|
||||
/** @var Session|null $session */
|
||||
$session = $request->getAttribute('session');
|
||||
|
||||
// Current session should only be terminated through logout.
|
||||
if ($session && $token->token === $session->get('access_token')) {
|
||||
throw new PermissionDeniedException();
|
||||
}
|
||||
|
||||
// Don't give away the existence of the token.
|
||||
if ($actor->cannot('revoke', $token)) {
|
||||
throw new ModelNotFoundException();
|
||||
}
|
||||
|
||||
$token->delete();
|
||||
}
|
||||
}
|
@@ -1,53 +0,0 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Api\Controller;
|
||||
|
||||
use Flarum\Api\Serializer\AccessTokenSerializer;
|
||||
use Flarum\Http\AccessToken;
|
||||
use Flarum\Http\RequestUtil;
|
||||
use Flarum\Http\UrlGenerator;
|
||||
use Flarum\Search\SearchCriteria;
|
||||
use Flarum\Search\SearchManager;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Tobscure\JsonApi\Document;
|
||||
|
||||
class ListAccessTokensController extends AbstractListController
|
||||
{
|
||||
public ?string $serializer = AccessTokenSerializer::class;
|
||||
|
||||
public function __construct(
|
||||
protected UrlGenerator $url,
|
||||
protected SearchManager $search
|
||||
) {
|
||||
}
|
||||
|
||||
protected function data(ServerRequestInterface $request, Document $document): iterable
|
||||
{
|
||||
$actor = RequestUtil::getActor($request);
|
||||
|
||||
$actor->assertRegistered();
|
||||
|
||||
$offset = $this->extractOffset($request);
|
||||
$limit = $this->extractLimit($request);
|
||||
$filter = $this->extractFilter($request);
|
||||
|
||||
$tokens = $this->search->query(AccessToken::class, new SearchCriteria($actor, $filter, $limit, $offset));
|
||||
|
||||
$document->addPaginationLinks(
|
||||
$this->url->to('api')->route('access-tokens.index'),
|
||||
$request->getQueryParams(),
|
||||
$offset,
|
||||
$limit,
|
||||
$tokens->areMoreResults() ? null : 0
|
||||
);
|
||||
|
||||
return $tokens->getResults();
|
||||
}
|
||||
}
|
@@ -4,13 +4,13 @@ namespace Flarum\Api\Endpoint;
|
||||
|
||||
use Flarum\Api\Endpoint\Concerns\HasAuthorization;
|
||||
use Flarum\Api\Endpoint\Concerns\HasCustomRoute;
|
||||
use Flarum\Api\Resource\Contracts\Deletable;
|
||||
use Nyholm\Psr7\Response;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use RuntimeException;
|
||||
use Tobyz\JsonApiServer\Context;
|
||||
use Tobyz\JsonApiServer\Endpoint\Delete as BaseDelete;
|
||||
use Tobyz\JsonApiServer\Exception\ForbiddenException;
|
||||
use Tobyz\JsonApiServer\Resource\Deletable;
|
||||
use function Tobyz\JsonApiServer\json_api_response;
|
||||
|
||||
class Delete extends BaseDelete implements Endpoint
|
||||
@@ -56,7 +56,7 @@ class Delete extends BaseDelete implements Endpoint
|
||||
throw new ForbiddenException();
|
||||
}
|
||||
|
||||
$resource->delete($model, $context);
|
||||
$resource->deleteAction($model, $context);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
@@ -14,6 +14,7 @@ use Flarum\Api\Resource\Contracts\{
|
||||
use Flarum\Api\Resource\Concerns\Bootable;
|
||||
use Flarum\Api\Resource\Concerns\ResolvesValidationFactory;
|
||||
use Flarum\Foundation\DispatchEventsTrait;
|
||||
use Flarum\User\User;
|
||||
use Illuminate\Contracts\Events\Dispatcher;
|
||||
use Illuminate\Support\Arr;
|
||||
use RuntimeException;
|
||||
@@ -30,7 +31,9 @@ abstract class AbstractDatabaseResource extends BaseResource implements
|
||||
Deletable
|
||||
{
|
||||
use Bootable;
|
||||
use DispatchEventsTrait;
|
||||
use DispatchEventsTrait {
|
||||
dispatchEventsFor as traitDispatchEventsFor;
|
||||
}
|
||||
use ResolvesValidationFactory;
|
||||
|
||||
abstract public function model(): string;
|
||||
@@ -74,11 +77,11 @@ abstract class AbstractDatabaseResource extends BaseResource implements
|
||||
return $model;
|
||||
}
|
||||
|
||||
public function delete(object $model, Context $context): void
|
||||
public function deleteAction(object $model, Context $context): void
|
||||
{
|
||||
$this->deleting($model, $context);
|
||||
|
||||
parent::delete($model, $context);
|
||||
$this->delete($model, $context);
|
||||
|
||||
$this->deleted($model, $context);
|
||||
|
||||
@@ -130,6 +133,13 @@ abstract class AbstractDatabaseResource extends BaseResource implements
|
||||
return null;
|
||||
}
|
||||
|
||||
public function dispatchEventsFor(mixed $entity, User $actor = null): void
|
||||
{
|
||||
if (method_exists($entity, 'releaseEvents')) {
|
||||
$this->traitDispatchEventsFor($entity, $actor);
|
||||
}
|
||||
}
|
||||
|
||||
public function mutateDataBeforeValidation(Context $context, array $data): array
|
||||
{
|
||||
$dirty = $context->model->getDirty();
|
||||
|
129
framework/core/src/Api/Resource/AccessTokenResource.php
Normal file
129
framework/core/src/Api/Resource/AccessTokenResource.php
Normal file
@@ -0,0 +1,129 @@
|
||||
<?php
|
||||
|
||||
namespace Flarum\Api\Resource;
|
||||
|
||||
use Flarum\Api\Context;
|
||||
use Flarum\Api\Endpoint;
|
||||
use Flarum\Api\Schema;
|
||||
use Flarum\Http\AccessToken;
|
||||
use Flarum\Http\DeveloperAccessToken;
|
||||
use Flarum\Http\Event\DeveloperTokenCreated;
|
||||
use Flarum\Http\RememberAccessToken;
|
||||
use Flarum\Http\SessionAccessToken;
|
||||
use Flarum\Locale\TranslatorInterface;
|
||||
use Flarum\User\Exception\PermissionDeniedException;
|
||||
use Illuminate\Contracts\Session\Session;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||
use Jenssegers\Agent\Agent;
|
||||
|
||||
class AccessTokenResource extends AbstractDatabaseResource
|
||||
{
|
||||
public function type(): string
|
||||
{
|
||||
return 'access-tokens';
|
||||
}
|
||||
|
||||
public function model(): string
|
||||
{
|
||||
return AccessToken::class;
|
||||
}
|
||||
|
||||
public function scope(Builder $query, \Tobyz\JsonApiServer\Context $context): void
|
||||
{
|
||||
$query->whereVisibleTo($context->getActor());
|
||||
}
|
||||
|
||||
public function newModel(\Tobyz\JsonApiServer\Context $context): object
|
||||
{
|
||||
if ($context->endpoint instanceof Endpoint\Create && $context->collection instanceof self) {
|
||||
$token = DeveloperAccessToken::make($context->getActor()->id);
|
||||
$token->last_activity_at = null;
|
||||
return $token;
|
||||
}
|
||||
|
||||
return parent::newModel($context);
|
||||
}
|
||||
|
||||
public function endpoints(): array
|
||||
{
|
||||
return [
|
||||
Endpoint\Create::make()
|
||||
->authenticated()
|
||||
->can('createAccessToken'),
|
||||
Endpoint\Delete::make()
|
||||
->authenticated(),
|
||||
Endpoint\Index::make()
|
||||
->authenticated()
|
||||
->paginate(),
|
||||
];
|
||||
}
|
||||
|
||||
public function fields(): array
|
||||
{
|
||||
return [
|
||||
Schema\Str::make('token')
|
||||
->visible(function (AccessToken $token, Context $context) {
|
||||
return $context->getActor()->id === $token->user_id && ! in_array('token', $token->getHidden(), true);
|
||||
}),
|
||||
Schema\Integer::make('userId'),
|
||||
Schema\DateTime::make('createdAt'),
|
||||
Schema\DateTime::make('lastActivityAt'),
|
||||
Schema\Boolean::make('isCurrent')
|
||||
->get(function (AccessToken $token, Context $context) {
|
||||
return $token->token === $context->request->getAttribute('session')->get('access_token');
|
||||
}),
|
||||
Schema\Boolean::make('isSessionToken')
|
||||
->get(function (AccessToken $token) {
|
||||
return in_array($token->type, [SessionAccessToken::$type, RememberAccessToken::$type], true);
|
||||
}),
|
||||
Schema\Str::make('title')
|
||||
->writableOnCreate()
|
||||
->requiredOnCreate()
|
||||
->maxLength(255),
|
||||
Schema\Str::make('lastIpAddress'),
|
||||
Schema\Str::make('device')
|
||||
->get(function (AccessToken $token) {
|
||||
$translator = resolve(TranslatorInterface::class);
|
||||
|
||||
$agent = new Agent();
|
||||
$agent->setUserAgent($token->last_user_agent);
|
||||
|
||||
return $translator->trans('core.forum.security.browser_on_operating_system', [
|
||||
'browser' => $agent->browser(),
|
||||
'os' => $agent->platform(),
|
||||
]);
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
public function created(object $model, \Tobyz\JsonApiServer\Context $context): ?object
|
||||
{
|
||||
$this->events->dispatch(new DeveloperTokenCreated($model));
|
||||
|
||||
return parent::created($model, $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param AccessToken $model
|
||||
* @param \Flarum\Api\Context $context
|
||||
* @throws PermissionDeniedException
|
||||
*/
|
||||
public function delete(object $model, \Tobyz\JsonApiServer\Context $context): void
|
||||
{
|
||||
/** @var Session|null $session */
|
||||
$session = $context->request->getAttribute('session');
|
||||
|
||||
// Current session should only be terminated through logout.
|
||||
if ($session && $model->token === $session->get('access_token')) {
|
||||
throw new PermissionDeniedException();
|
||||
}
|
||||
|
||||
// Don't give away the existence of the token.
|
||||
if ($context->getActor()->cannot('revoke', $model)) {
|
||||
throw new ModelNotFoundException();
|
||||
}
|
||||
|
||||
$model->delete();
|
||||
}
|
||||
}
|
@@ -2,9 +2,10 @@
|
||||
|
||||
namespace Flarum\Api\Resource\Contracts;
|
||||
|
||||
use Tobyz\JsonApiServer\Context;
|
||||
use Tobyz\JsonApiServer\Resource\Deletable as BaseDeletable;
|
||||
|
||||
interface Deletable extends BaseDeletable
|
||||
{
|
||||
//
|
||||
public function deleteAction(object $model, Context $context): void;
|
||||
}
|
||||
|
@@ -19,27 +19,6 @@ return function (RouteCollection $map, RouteHandlerFactory $route) {
|
||||
$route->toController(Controller\ShowForumController::class)
|
||||
);
|
||||
|
||||
// List access tokens
|
||||
$map->get(
|
||||
'/access-tokens',
|
||||
'access-tokens.index',
|
||||
$route->toController(Controller\ListAccessTokensController::class)
|
||||
);
|
||||
|
||||
// Create access token
|
||||
$map->post(
|
||||
'/access-tokens',
|
||||
'access-tokens.create',
|
||||
$route->toController(Controller\CreateAccessTokenController::class)
|
||||
);
|
||||
|
||||
// Delete access token
|
||||
$map->delete(
|
||||
'/access-tokens/{id}',
|
||||
'access-tokens.delete',
|
||||
$route->toController(Controller\DeleteAccessTokenController::class)
|
||||
);
|
||||
|
||||
// Create authentication token
|
||||
$map->post(
|
||||
'/token',
|
||||
|
@@ -75,6 +75,14 @@ class AccessToken extends AbstractModel
|
||||
* Generate an access token for the specified user.
|
||||
*/
|
||||
public static function generate(int $userId): static
|
||||
{
|
||||
$token = static::make($userId);
|
||||
$token->save();
|
||||
|
||||
return $token;
|
||||
}
|
||||
|
||||
public static function make(int $userId): static
|
||||
{
|
||||
if (static::class === self::class) {
|
||||
throw new \Exception('Use of AccessToken::generate() is not allowed: use the `generate` method on one of the subclasses.');
|
||||
@@ -87,7 +95,6 @@ class AccessToken extends AbstractModel
|
||||
$token->user_id = $userId;
|
||||
$token->created_at = Carbon::now();
|
||||
$token->last_activity_at = Carbon::now();
|
||||
$token->save();
|
||||
|
||||
return $token;
|
||||
}
|
||||
|
@@ -61,7 +61,7 @@ class CreateTest extends TestCase
|
||||
])
|
||||
);
|
||||
|
||||
$this->assertEquals(201, $response->getStatusCode());
|
||||
$this->assertEquals(201, $response->getStatusCode(), (string) $response->getBody());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -84,7 +84,7 @@ class CreateTest extends TestCase
|
||||
])
|
||||
);
|
||||
|
||||
$this->assertEquals(403, $response->getStatusCode());
|
||||
$this->assertEquals(403, $response->getStatusCode(), (string) $response->getBody());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -95,10 +95,16 @@ class CreateTest extends TestCase
|
||||
$response = $this->send(
|
||||
$this->request('POST', '/api/access-tokens', [
|
||||
'authenticatedAs' => 1,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'access-tokens',
|
||||
'attributes' => []
|
||||
]
|
||||
]
|
||||
])
|
||||
);
|
||||
|
||||
$this->assertEquals(422, $response->getStatusCode());
|
||||
$this->assertEquals(422, $response->getStatusCode(), (string) $response->getBody());
|
||||
}
|
||||
|
||||
public function canCreateTokens(): array
|
||||
|
Reference in New Issue
Block a user