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\PostResource::class,
|
||||||
Resource\DiscussionResource::class,
|
Resource\DiscussionResource::class,
|
||||||
Resource\NotificationResource::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\HasAuthorization;
|
||||||
use Flarum\Api\Endpoint\Concerns\HasCustomRoute;
|
use Flarum\Api\Endpoint\Concerns\HasCustomRoute;
|
||||||
|
use Flarum\Api\Resource\Contracts\Deletable;
|
||||||
use Nyholm\Psr7\Response;
|
use Nyholm\Psr7\Response;
|
||||||
use Psr\Http\Message\ResponseInterface;
|
use Psr\Http\Message\ResponseInterface;
|
||||||
use RuntimeException;
|
use RuntimeException;
|
||||||
use Tobyz\JsonApiServer\Context;
|
use Tobyz\JsonApiServer\Context;
|
||||||
use Tobyz\JsonApiServer\Endpoint\Delete as BaseDelete;
|
use Tobyz\JsonApiServer\Endpoint\Delete as BaseDelete;
|
||||||
use Tobyz\JsonApiServer\Exception\ForbiddenException;
|
use Tobyz\JsonApiServer\Exception\ForbiddenException;
|
||||||
use Tobyz\JsonApiServer\Resource\Deletable;
|
|
||||||
use function Tobyz\JsonApiServer\json_api_response;
|
use function Tobyz\JsonApiServer\json_api_response;
|
||||||
|
|
||||||
class Delete extends BaseDelete implements Endpoint
|
class Delete extends BaseDelete implements Endpoint
|
||||||
@@ -56,7 +56,7 @@ class Delete extends BaseDelete implements Endpoint
|
|||||||
throw new ForbiddenException();
|
throw new ForbiddenException();
|
||||||
}
|
}
|
||||||
|
|
||||||
$resource->delete($model, $context);
|
$resource->deleteAction($model, $context);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@@ -14,6 +14,7 @@ use Flarum\Api\Resource\Contracts\{
|
|||||||
use Flarum\Api\Resource\Concerns\Bootable;
|
use Flarum\Api\Resource\Concerns\Bootable;
|
||||||
use Flarum\Api\Resource\Concerns\ResolvesValidationFactory;
|
use Flarum\Api\Resource\Concerns\ResolvesValidationFactory;
|
||||||
use Flarum\Foundation\DispatchEventsTrait;
|
use Flarum\Foundation\DispatchEventsTrait;
|
||||||
|
use Flarum\User\User;
|
||||||
use Illuminate\Contracts\Events\Dispatcher;
|
use Illuminate\Contracts\Events\Dispatcher;
|
||||||
use Illuminate\Support\Arr;
|
use Illuminate\Support\Arr;
|
||||||
use RuntimeException;
|
use RuntimeException;
|
||||||
@@ -30,7 +31,9 @@ abstract class AbstractDatabaseResource extends BaseResource implements
|
|||||||
Deletable
|
Deletable
|
||||||
{
|
{
|
||||||
use Bootable;
|
use Bootable;
|
||||||
use DispatchEventsTrait;
|
use DispatchEventsTrait {
|
||||||
|
dispatchEventsFor as traitDispatchEventsFor;
|
||||||
|
}
|
||||||
use ResolvesValidationFactory;
|
use ResolvesValidationFactory;
|
||||||
|
|
||||||
abstract public function model(): string;
|
abstract public function model(): string;
|
||||||
@@ -74,11 +77,11 @@ abstract class AbstractDatabaseResource extends BaseResource implements
|
|||||||
return $model;
|
return $model;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function delete(object $model, Context $context): void
|
public function deleteAction(object $model, Context $context): void
|
||||||
{
|
{
|
||||||
$this->deleting($model, $context);
|
$this->deleting($model, $context);
|
||||||
|
|
||||||
parent::delete($model, $context);
|
$this->delete($model, $context);
|
||||||
|
|
||||||
$this->deleted($model, $context);
|
$this->deleted($model, $context);
|
||||||
|
|
||||||
@@ -130,6 +133,13 @@ abstract class AbstractDatabaseResource extends BaseResource implements
|
|||||||
return null;
|
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
|
public function mutateDataBeforeValidation(Context $context, array $data): array
|
||||||
{
|
{
|
||||||
$dirty = $context->model->getDirty();
|
$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;
|
namespace Flarum\Api\Resource\Contracts;
|
||||||
|
|
||||||
|
use Tobyz\JsonApiServer\Context;
|
||||||
use Tobyz\JsonApiServer\Resource\Deletable as BaseDeletable;
|
use Tobyz\JsonApiServer\Resource\Deletable as BaseDeletable;
|
||||||
|
|
||||||
interface Deletable extends 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)
|
$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
|
// Create authentication token
|
||||||
$map->post(
|
$map->post(
|
||||||
'/token',
|
'/token',
|
||||||
|
@@ -75,6 +75,14 @@ class AccessToken extends AbstractModel
|
|||||||
* Generate an access token for the specified user.
|
* Generate an access token for the specified user.
|
||||||
*/
|
*/
|
||||||
public static function generate(int $userId): static
|
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) {
|
if (static::class === self::class) {
|
||||||
throw new \Exception('Use of AccessToken::generate() is not allowed: use the `generate` method on one of the subclasses.');
|
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->user_id = $userId;
|
||||||
$token->created_at = Carbon::now();
|
$token->created_at = Carbon::now();
|
||||||
$token->last_activity_at = Carbon::now();
|
$token->last_activity_at = Carbon::now();
|
||||||
$token->save();
|
|
||||||
|
|
||||||
return $token;
|
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(
|
$response = $this->send(
|
||||||
$this->request('POST', '/api/access-tokens', [
|
$this->request('POST', '/api/access-tokens', [
|
||||||
'authenticatedAs' => 1,
|
'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
|
public function canCreateTokens(): array
|
||||||
|
Reference in New Issue
Block a user