person($request); $tokens = ApiToken::query() ->with('user') ->unless($request->boolean('all') && $user->hasRole('super_admin'), fn ($q) => $q->whereBelongsTo($user)) ->latest('id') ->paginate(perPage: min($request->integer('per_page', 25), 100)) ->withQueryString(); return ApiTokenResource::collection($tokens); } public function store(Request $request): JsonResponse { $user = $this->person($request); $data = $request->validate([ 'name' => ['required', 'string', 'max:120'], 'abilities' => ['required', 'array', 'min:1'], 'abilities.*' => [ 'string', 'distinct', // Only what the person could do themselves: a token narrows, // it never widens. function (string $attribute, mixed $value, Closure $fail) use ($user): void { $matches = array_filter(Permissions::all(), fn (string $permission) => $permission === $value || Str::is((string) $value, $permission)); if ($matches === [] || ! collect($matches)->contains(fn (string $permission) => $user->can($permission))) { $fail(__('rules.tokens.ability', ['ability' => $value])); } }, ], 'expires_at' => ['sometimes', 'date', 'after:now', 'before_or_equal:'.now()->addDays(ApiToken::MAX_DAYS)->toIso8601String()], ]); [$token, $plain] = ApiToken::issue( $user, $data['name'], $data['abilities'], isset($data['expires_at']) ? CarbonImmutable::parse($data['expires_at']) : now()->addDays(90), ); return ApiTokenResource::make($token->load('user')) ->additional(['token' => $plain]) ->response() ->setStatusCode(201); } /** * Revoke. Kept in the list afterwards, so it is clear what stopped working. */ public function destroy(Request $request, ApiToken $token): JsonResponse { $user = $this->person($request); abort_unless($token->user_id === $user->id || $user->hasRole('super_admin'), 403); $token->revoke(); return response()->json(status: 204); } protected function person(Request $request): User { $user = $request->user(); abort_if($user->currentAccessToken() !== null, 403, __('rules.tokens.machine')); return $user; } }