From 25d5142f129671bccfba4e5014a0fb4246c1fded Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Mon, 29 Jun 2026 05:30:13 -0600 Subject: [PATCH] Fix PAT + oauth routes --- .../PersonalAccessTokenController.php | 139 ++++++++++++++++++ routes/web.php | 20 ++- 2 files changed, 151 insertions(+), 8 deletions(-) create mode 100644 app/Http/Controllers/PersonalAccessTokenController.php diff --git a/app/Http/Controllers/PersonalAccessTokenController.php b/app/Http/Controllers/PersonalAccessTokenController.php new file mode 100644 index 000000000..23c65cb11 --- /dev/null +++ b/app/Http/Controllers/PersonalAccessTokenController.php @@ -0,0 +1,139 @@ +filter(function ($scope) use ($request) { + return $this->userCanUseScope($request->user(), $scope->id); + }) + ->map(function ($scope) { + return [ + 'id' => $scope->id, + 'description' => $scope->description, + ]; + }) + ->values(); + + return response()->json($scopes); + } + + public function index(Request $request): JsonResponse + { + $tokens = $request->user() + ->tokens() + ->with('client') + ->where('revoked', false) + ->where(function ($query) { + $query->whereNull('expires_at') + ->orWhere('expires_at', '>', now()); + }) + ->latest('created_at') + ->get() + ->filter(function (Token $token) { + return $this->isPersonalAccessToken($token); + }) + ->map(function (Token $token) { + return $this->serializeToken($token); + }) + ->values(); + + return response()->json($tokens); + } + + public function store(Request $request): JsonResponse + { + $allowedScopes = collect(Passport::scopeIds()) + ->filter(function (string $scope) use ($request) { + return $this->userCanUseScope($request->user(), $scope); + }) + ->values() + ->all(); + + $validated = $request->validate([ + 'name' => ['required', 'string', 'min:2', 'max:255'], + 'scopes' => ['nullable', 'array'], + 'scopes.*' => ['string', Rule::in($allowedScopes)], + ]); + + $scopes = array_values(array_unique($validated['scopes'] ?? [])); + + $result = $request->user()->createToken( + $validated['name'], + $scopes + ); + + return response()->json([ + 'accessToken' => $result->accessToken, + 'token' => $this->serializeToken($result->token), + ]); + } + + public function destroy(Request $request, string $token): JsonResponse + { + $token = $request->user() + ->tokens() + ->with('client') + ->whereKey($token) + ->firstOrFail(); + + abort_unless($this->isPersonalAccessToken($token), 404); + + $token->revoke(); + + return response()->noContent(); + } + + private function serializeToken(Token $token): array + { + return [ + 'id' => $token->id, + 'name' => $token->name, + 'scopes' => $token->scopes ?? [], + 'revoked' => (bool) $token->revoked, + 'created_at' => optional($token->created_at)->toJSON(), + 'updated_at' => optional($token->updated_at)->toJSON(), + 'expires_at' => optional($token->expires_at)->toJSON(), + ]; + } + + private function isPersonalAccessToken(Token $token): bool + { + $client = $token->client; + + if (! $client) { + return false; + } + + if (method_exists($client, 'hasGrantType')) { + return $client->hasGrantType('personal_access'); + } + + if (isset($client->personal_access_client)) { + return (bool) $client->personal_access_client; + } + + return false; + } + + private function userCanUseScope($user, string $scope): bool + { + if (str_starts_with($scope, 'admin:')) { + return (bool) ( + $user->is_admin + ?? false + ); + } + + return true; + } +} diff --git a/routes/web.php b/routes/web.php index 2c8f8e3b2..49636f862 100644 --- a/routes/web.php +++ b/routes/web.php @@ -1,5 +1,7 @@ middleware(['validemail', 'twofactor', 'localization'])->group(function () { Route::get('/', 'SiteController@home')->name('timeline.personal'); Route::redirect('/home', '/')->name('home'); @@ -51,23 +53,23 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact Route::group([ 'as' => 'passport.', - 'prefix' => config('passport.path', 'oauth'), + 'prefix' => 'oauth', ], function () { Route::post('/token', [ 'uses' => '\App\Http\Controllers\OAuth\ApiTokenController@issueToken', 'as' => 'token', - 'middleware' => 'throttle', + 'middleware' => 'throttle:10,1', ]); Route::get('/authorize', [ 'uses' => '\Laravel\Passport\Http\Controllers\AuthorizationController@authorize', 'as' => 'authorizations.authorize', - 'middleware' => 'web', + 'middleware' => ['web', 'throttle:10,1'] ]); $guard = config('passport.guard', null); - Route::middleware(['web', $guard ? 'auth:'.$guard : 'auth'])->group(function () { + Route::middleware(['web', $guard ? 'auth:'.$guard : 'auth', 'validemail'])->group(function () { Route::post('/token/refresh', [ 'uses' => '\Laravel\Passport\Http\Controllers\TransientTokenController@refresh', 'as' => 'token.refresh', @@ -114,23 +116,25 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact ]); Route::get('/scopes', [ - 'uses' => '\Laravel\Passport\Http\Controllers\ScopeController@all', + 'uses' => 'PersonalAccessTokenController@scopes', 'as' => 'scopes.index', ]); Route::get('/personal-access-tokens', [ - 'uses' => '\Laravel\Passport\Http\Controllers\PersonalAccessTokenController@forUser', + 'uses' => 'PersonalAccessTokenController@index', 'as' => 'personal.tokens.index', ]); Route::post('/personal-access-tokens', [ - 'uses' => '\Laravel\Passport\Http\Controllers\PersonalAccessTokenController@store', + 'uses' => 'PersonalAccessTokenController@store', 'as' => 'personal.tokens.store', + 'middleware' => 'twofactor', ]); Route::delete('/personal-access-tokens/{token_id}', [ - 'uses' => '\Laravel\Passport\Http\Controllers\PersonalAccessTokenController@destroy', + 'uses' => 'PersonalAccessTokenController@destroy', 'as' => 'personal.tokens.destroy', + 'middleware' => 'twofactor', ]); });