diff --git a/app/Data/ApiKeyData.php b/app/Data/ApiKeyData.php new file mode 100644 index 0000000000..a8aafe7642 --- /dev/null +++ b/app/Data/ApiKeyData.php @@ -0,0 +1,40 @@ +|null $allowed_ips + */ + public function __construct( + public string $identifier, + public ?string $description, + public ?array $allowed_ips, + public ?string $last_used_at, + public string $created_at, + ) {} + + public static function fromModel(ApiKey $model): self + { + return new self( + identifier: $model->identifier, + description: $model->memo, + allowed_ips: $model->allowed_ips, + last_used_at: $model->last_used_at?->toAtomString(), + created_at: $model->created_at->toAtomString(), + ); + } + + public function getResourceName(): string + { + return static::getResourceNameStatic(); + } + + public static function getResourceNameStatic(): string + { + return ApiKey::RESOURCE_NAME; + } +} diff --git a/app/Data/CursorPaginatedDataCollection.php b/app/Data/CursorPaginatedDataCollection.php new file mode 100644 index 0000000000..67f0e616d0 --- /dev/null +++ b/app/Data/CursorPaginatedDataCollection.php @@ -0,0 +1,77 @@ +isFractal = $value; + + return $this; + } + + /** @var array */ + protected array $_additional = []; + + /** + * @param array $additional + */ + public function additional(array $additional): static + { + $this->_additional = array_merge($this->_additional, $additional); + + return $this; + } + + public function toArray(): array + { + $array = parent::toArray(); + if ($this->isFractal) { + $resourceKey = method_exists($this->dataClass, 'getResourceNameStatic') + ? ($this->dataClass)::getResourceNameStatic() + : strtolower(class_basename($this->dataClass)); + + $formattedData = []; + $dataItems = $array['data'] ?? []; + foreach ($dataItems as $item) { + if (is_array($item)) { + unset($item['_additional']); + } + $formattedData[] = [ + 'object' => $resourceKey, + 'attributes' => $item, + ]; + } + + $paginator = $this->items; + $cursor = [ + 'current' => $paginator->cursor() ? $paginator->cursor()->encode() : null, + 'prev' => $paginator->previousCursor() ? $paginator->previousCursor()->encode() : null, + 'next' => $paginator->nextCursor() ? $paginator->nextCursor()->encode() : null, + 'count' => $paginator->count(), + ]; + + $meta = $array['meta'] ?? []; + unset($meta['path'], $meta['per_page'], $meta['next_page_url'], $meta['prev_page_url']); + + if (isset($this->_additional['meta'])) { + $meta = array_merge($meta, $this->_additional['meta']); + } + + $meta['cursor'] = $cursor; + + return [ + 'object' => 'list', + 'data' => $formattedData, + 'meta' => $meta, + ]; + } + + return $array; + } +} diff --git a/app/Data/Data.php b/app/Data/Data.php new file mode 100644 index 0000000000..844ca3c79c --- /dev/null +++ b/app/Data/Data.php @@ -0,0 +1,86 @@ +isFractal = $value; + + return $this; + } + + public static function collection(mixed $items): DataCollection|PaginatedDataCollection|CursorPaginatedDataCollection + { + if ($items instanceof Paginator || $items instanceof AbstractPaginator) { + /** @var PaginatedDataCollection $collection */ + $collection = static::collect( + $items, + static::$_paginatedCollectionClass + ); + + return $collection; + } + + if ($items instanceof CursorPaginator || $items instanceof AbstractCursorPaginator) { + /** @var CursorPaginatedDataCollection $collection */ + $collection = static::collect( + $items, + static::$_cursorPaginatedCollectionClass + ); + + return $collection; + } + + /** @var DataCollection $collection */ + $collection = static::collect( + $items, + static::$_collectionClass + ); + + return $collection; + } + + public function toArray(): array + { + $array = parent::toArray(); + if ($this->isFractal) { + $resourceKey = method_exists($this, 'getResourceName') + ? $this->getResourceName() + : strtolower(class_basename($this)); + + $additional = $this->getAdditionalData(); + foreach ($additional as $key => $value) { + unset($array[$key]); + } + + $response = [ + 'object' => $resourceKey, + 'attributes' => $array, + ]; + + if (isset($additional['meta'])) { + $response['meta'] = $additional['meta']; + } + + return $response; + } + + return $array; + } +} diff --git a/app/Data/DataCollection.php b/app/Data/DataCollection.php new file mode 100644 index 0000000000..8818115042 --- /dev/null +++ b/app/Data/DataCollection.php @@ -0,0 +1,65 @@ +isFractal = $value; + + return $this; + } + + /** @var array */ + protected array $_additional = []; + + /** + * @param array $additional + */ + public function additional(array $additional): static + { + $this->_additional = array_merge($this->_additional, $additional); + + return $this; + } + + public function toArray(): array + { + $array = parent::toArray(); + if ($this->isFractal) { + $resourceKey = method_exists($this->dataClass, 'getResourceNameStatic') + ? ($this->dataClass)::getResourceNameStatic() + : strtolower(class_basename($this->dataClass)); + + $formattedData = []; + foreach ($array as $item) { + // If the item itself has additional data merged, remove it from attributes + if (is_array($item)) { + unset($item['_additional']); + } + $formattedData[] = [ + 'object' => $resourceKey, + 'attributes' => $item, + ]; + } + + $response = [ + 'object' => 'list', + 'data' => $formattedData, + ]; + + if (isset($this->_additional['meta'])) { + $response['meta'] = $this->_additional['meta']; + } + + return $response; + } + + return $array; + } +} diff --git a/app/Data/PaginatedDataCollection.php b/app/Data/PaginatedDataCollection.php new file mode 100644 index 0000000000..b0f35c4781 --- /dev/null +++ b/app/Data/PaginatedDataCollection.php @@ -0,0 +1,103 @@ +isFractal = $value; + + return $this; + } + + /** @var array */ + protected array $_additional = []; + + /** + * @param array $additional + */ + public function additional(array $additional): static + { + $this->_additional = array_merge($this->_additional, $additional); + + return $this; + } + + public function toArray(): array + { + $array = parent::toArray(); + if ($this->isFractal) { + $resourceKey = method_exists($this->dataClass, 'getResourceNameStatic') + ? ($this->dataClass)::getResourceNameStatic() + : strtolower(class_basename($this->dataClass)); + + $formattedData = []; + $dataItems = $array['data'] ?? []; + foreach ($dataItems as $item) { + if (is_array($item)) { + unset($item['_additional']); + } + $formattedData[] = [ + 'object' => $resourceKey, + 'attributes' => $item, + ]; + } + + $paginator = $this->items; + if (!$paginator instanceof LengthAwarePaginator) { + throw new \InvalidArgumentException('Paginator must be an instance of LengthAwarePaginator'); + } + + $pagination = [ + 'total' => $paginator->total(), + 'count' => $paginator->count(), + 'per_page' => $paginator->perPage(), + 'current_page' => $paginator->currentPage(), + 'total_pages' => $paginator->lastPage(), + 'links' => [], + ]; + if ($paginator->nextPageUrl()) { + $pagination['links']['next'] = $paginator->nextPageUrl(); + } + if ($paginator->previousPageUrl()) { + $pagination['links']['previous'] = $paginator->previousPageUrl(); + } + + $meta = $array['meta'] ?? []; + unset( + $meta['current_page'], + $meta['first_page_url'], + $meta['from'], + $meta['last_page'], + $meta['last_page_url'], + $meta['next_page_url'], + $meta['path'], + $meta['per_page'], + $meta['prev_page_url'], + $meta['to'], + $meta['total'] + ); + + // Merge custom additional meta + if (isset($this->_additional['meta'])) { + $meta = array_merge($meta, $this->_additional['meta']); + } + + $meta['pagination'] = $pagination; + + return [ + 'object' => 'list', + 'data' => $formattedData, + 'meta' => $meta, + ]; + } + + return $array; + } +} diff --git a/app/Data/UserData.php b/app/Data/UserData.php new file mode 100644 index 0000000000..cbd9c0ff6f --- /dev/null +++ b/app/Data/UserData.php @@ -0,0 +1,50 @@ +uuid, + username: $user->username, + email: $user->email, + language: $user->language, + image: 'https://gravatar.com/avatar/' . md5(Str::lower($user->email)), + admin: $user->isRootAdmin(), + root_admin: $user->isRootAdmin(), + two_fa_enabled: filled($user->mfa_app_secret), + created_at: $user->created_at->setTimezone('UTC')->toAtomString(), + updated_at: $user->updated_at->setTimezone('UTC')->toAtomString(), + ); + } + + public function getResourceName(): string + { + return static::getResourceNameStatic(); + } + + public static function getResourceNameStatic(): string + { + return User::RESOURCE_NAME; + } +} diff --git a/app/Data/UserSSHKeyData.php b/app/Data/UserSSHKeyData.php new file mode 100644 index 0000000000..1622ebabbf --- /dev/null +++ b/app/Data/UserSSHKeyData.php @@ -0,0 +1,35 @@ +name, + fingerprint: $model->fingerprint, + public_key: $model->public_key, + created_at: $model->created_at->toAtomString(), + ); + } + + public function getResourceName(): string + { + return static::getResourceNameStatic(); + } + + public static function getResourceNameStatic(): string + { + return UserSSHKey::RESOURCE_NAME; + } +} diff --git a/app/Http/Controllers/Api/Client/AccountController.php b/app/Http/Controllers/Api/Client/AccountController.php index 8b1ffa3a8c..a2d890549b 100644 --- a/app/Http/Controllers/Api/Client/AccountController.php +++ b/app/Http/Controllers/Api/Client/AccountController.php @@ -2,12 +2,12 @@ namespace App\Http\Controllers\Api\Client; +use App\Data\UserData; use App\Facades\Activity; use App\Http\Requests\Api\Client\Account\UpdateEmailRequest; use App\Http\Requests\Api\Client\Account\UpdatePasswordRequest; use App\Http\Requests\Api\Client\Account\UpdateUsernameRequest; use App\Services\Users\UserUpdateService; -use App\Transformers\Api\Client\UserTransformer; use Illuminate\Auth\AuthManager; use Illuminate\Auth\SessionGuard; use Illuminate\Http\JsonResponse; @@ -39,8 +39,8 @@ public function __construct(private AuthManager $manager, private UserUpdateServ */ public function index(Request $request): array { - return $this->fractal->item($request->user()) - ->transformWith($this->getTransformer(UserTransformer::class)) + return UserData::from($request->user()) + ->setFractal(true) ->toArray(); } diff --git a/app/Http/Controllers/Api/Client/ApiKeyController.php b/app/Http/Controllers/Api/Client/ApiKeyController.php index e6c738bb9c..6abda4962a 100644 --- a/app/Http/Controllers/Api/Client/ApiKeyController.php +++ b/app/Http/Controllers/Api/Client/ApiKeyController.php @@ -2,12 +2,12 @@ namespace App\Http\Controllers\Api\Client; +use App\Data\ApiKeyData; use App\Exceptions\DisplayException; use App\Facades\Activity; use App\Http\Requests\Api\Client\Account\StoreApiKeyRequest; use App\Http\Requests\Api\Client\ClientApiRequest; use App\Models\ApiKey; -use App\Transformers\Api\Client\ApiKeyTransformer; use Illuminate\Http\JsonResponse; class ApiKeyController extends ClientApiController @@ -21,8 +21,8 @@ class ApiKeyController extends ClientApiController */ public function index(ClientApiRequest $request): array { - return $this->fractal->collection($request->user()->apiKeys) - ->transformWith($this->getTransformer(ApiKeyTransformer::class)) + return ApiKeyData::collection($request->user()->apiKeys) + ->setFractal(true) ->toArray(); } @@ -49,9 +49,9 @@ public function store(StoreApiKeyRequest $request): array ->property('identifier', $token->accessToken->identifier) ->log(); - return $this->fractal->item($token->accessToken) - ->transformWith($this->getTransformer(ApiKeyTransformer::class)) - ->addMeta(['secret_token' => $token->plainTextToken]) + return ApiKeyData::from($token->accessToken) + ->setFractal(true) + ->additional(['meta' => ['secret_token' => $token->plainTextToken]]) ->toArray(); } diff --git a/app/Http/Controllers/Api/Client/SSHKeyController.php b/app/Http/Controllers/Api/Client/SSHKeyController.php index 445c2c7b84..f000541162 100644 --- a/app/Http/Controllers/Api/Client/SSHKeyController.php +++ b/app/Http/Controllers/Api/Client/SSHKeyController.php @@ -2,11 +2,11 @@ namespace App\Http\Controllers\Api\Client; +use App\Data\UserSSHKeyData; use App\Facades\Activity; use App\Http\Requests\Api\Client\Account\StoreSSHKeyRequest; use App\Http\Requests\Api\Client\ClientApiRequest; use App\Models\UserSSHKey; -use App\Transformers\Api\Client\UserSSHKeyTransformer; use Illuminate\Http\JsonResponse; class SSHKeyController extends ClientApiController @@ -20,8 +20,8 @@ class SSHKeyController extends ClientApiController */ public function index(ClientApiRequest $request): array { - return $this->fractal->collection($request->user()->sshKeys) - ->transformWith($this->getTransformer(UserSSHKeyTransformer::class)) + return UserSSHKeyData::collection($request->user()->sshKeys) + ->setFractal(true) ->toArray(); } @@ -45,8 +45,8 @@ public function store(StoreSSHKeyRequest $request): array ->property('fingerprint', $request->getKeyFingerprint()) ->log(); - return $this->fractal->item($model) - ->transformWith($this->getTransformer(UserSSHKeyTransformer::class)) + return UserSSHKeyData::from($model) + ->setFractal(true) ->toArray(); }