diff --git a/app/Http/ApiControllers/Controller.php b/app/Http/ApiControllers/Controller.php new file mode 100644 index 0000000..bc0e11d --- /dev/null +++ b/app/Http/ApiControllers/Controller.php @@ -0,0 +1,8 @@ +user()); + } +} \ No newline at end of file diff --git a/app/Http/ApiControllers/PortfolioController.php b/app/Http/ApiControllers/PortfolioController.php new file mode 100644 index 0000000..72299a4 --- /dev/null +++ b/app/Http/ApiControllers/PortfolioController.php @@ -0,0 +1,24 @@ +setScopes(['myPortfolios']); + $filterRequest->setEagerRelations(['users', 'transactions', 'holdings']); + $filterRequest->setFilterableRelations(['holdings' => 'symbol', 'transactions' => 'symbol']); + $filterRequest->setSearchableColumns(['title', 'notes']); + + return PortfolioResource::collection($filterRequest->get()); + } +} \ No newline at end of file diff --git a/app/Http/ApiControllers/TransactionController.php b/app/Http/ApiControllers/TransactionController.php new file mode 100644 index 0000000..b482720 --- /dev/null +++ b/app/Http/ApiControllers/TransactionController.php @@ -0,0 +1,15 @@ +user()); + } +} \ No newline at end of file diff --git a/app/Http/ApiControllers/UserController.php b/app/Http/ApiControllers/UserController.php new file mode 100644 index 0000000..468ca52 --- /dev/null +++ b/app/Http/ApiControllers/UserController.php @@ -0,0 +1,15 @@ +user()); + } +} \ No newline at end of file diff --git a/app/Http/Controllers/ConnectedAccountController.php b/app/Http/Controllers/ConnectedAccountController.php index 8333b01..0551b89 100644 --- a/app/Http/Controllers/ConnectedAccountController.php +++ b/app/Http/Controllers/ConnectedAccountController.php @@ -6,7 +6,6 @@ use Exception; use App\Models\User; use App\Models\ConnectedAccount; use Illuminate\Support\MessageBag; -use App\Http\Controllers\Controller; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Blade; use Laravel\Socialite\Facades\Socialite; diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php index 8677cd5..71116b2 100644 --- a/app/Http/Controllers/Controller.php +++ b/app/Http/Controllers/Controller.php @@ -1,8 +1,8 @@ + */ + public function toArray(Request $request): array + { + return parent::toArray($request); + } +} diff --git a/app/Http/Resources/PortfolioResource.php b/app/Http/Resources/PortfolioResource.php new file mode 100644 index 0000000..8a71071 --- /dev/null +++ b/app/Http/Resources/PortfolioResource.php @@ -0,0 +1,28 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->id, + 'title' => $this->title, + 'wishlist' => $this->wishlist, + 'owner' => UserResource::make($this->owner), + 'transactions' => TransactionResource::collection($this->whenLoaded('transactions')), + 'holdings' => HoldingResource::collection($this->whenLoaded('holdings')), + 'created_at' => $this->created_at, + 'updated_at' => $this->updated_at, + ]; + } +} diff --git a/app/Http/Resources/TransactionResource.php b/app/Http/Resources/TransactionResource.php new file mode 100644 index 0000000..358bfe5 --- /dev/null +++ b/app/Http/Resources/TransactionResource.php @@ -0,0 +1,19 @@ + + */ + public function toArray(Request $request): array + { + return parent::toArray($request); + } +} diff --git a/app/Http/Resources/UserResource.php b/app/Http/Resources/UserResource.php new file mode 100644 index 0000000..b68e3fa --- /dev/null +++ b/app/Http/Resources/UserResource.php @@ -0,0 +1,27 @@ + + */ + public function toArray(Request $request): array + { + + return [ + 'id' => $this->id, + 'name' => $this->name, + 'email' => $this->email, + 'profile_photo_url' => $this->profile_photo_url, + 'created_at' => $this->created_at, + 'updated_at' => $this->updated_at, + ]; + } +} diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 0450ca0..39f79b0 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -3,6 +3,7 @@ namespace App\Providers; use Illuminate\Support\ServiceProvider; +use Illuminate\Http\Resources\Json\JsonResource; class AppServiceProvider extends ServiceProvider { @@ -22,6 +23,6 @@ class AppServiceProvider extends ServiceProvider */ public function boot(): void { - // + JsonResource::withoutWrapping(); } } diff --git a/app/Providers/JetstreamServiceProvider.php b/app/Providers/JetstreamServiceProvider.php index f931bf3..f7a2ed0 100644 --- a/app/Providers/JetstreamServiceProvider.php +++ b/app/Providers/JetstreamServiceProvider.php @@ -43,13 +43,8 @@ class JetstreamServiceProvider extends ServiceProvider */ protected function configurePermissions(): void { - Jetstream::defaultApiTokenPermissions(['read']); + Jetstream::defaultApiTokenPermissions([]); - Jetstream::permissions([ - 'create', - 'read', - 'update', - 'delete', - ]); + Jetstream::permissions([]); } } diff --git a/app/Support/FilterRequest.php b/app/Support/FilterRequest.php new file mode 100644 index 0000000..07b64ef --- /dev/null +++ b/app/Support/FilterRequest.php @@ -0,0 +1,205 @@ +query = (new $modelClass)->query(); + } + + /** + * Sets eager loads on the underlying query + * + * @param array $scopes + * @return void + */ + public function setEagerRelations(string|array $relations) + { + $this->query = $this->query->with($relations); + } + + /** + * Sets scopes on the underlying query + * + * @param array $scopes + * @return void + */ + public function setScopes(string|array $scopes): void + { + $this->query = $this->query->scopes($scopes); + } + + /** + * Allows nested json data to be aliased into a virtual column for the query + * + * @param array $columns can be an array of keys (e.g. `body.total_amount`) + * @return void + */ + public function setVirtualColumns(array $columns) + { + foreach($columns as $column) { + $column = Str::replace('.', '->', $column); + $alias = Str::snake(Str::afterLast($column, '->')); + + array_push($this->select, $column . ' as ' . $alias); + } + } + + /** + * Set columns that should be searched + * + * @param array $columns can be an array of keys (e.g. `user.name` or a nested array with + * `relation`, `table`, and `column` attributes) + * @return void + */ + public function setSearchableColumns(array $columns) + { + $this->searchableColumns = $columns; + } + + /** + * Set relations that can be filtered + * + * @param array $relations is an array of keys (relations) and values (columns) + * @return void + */ + public function setFilterableRelations(array $relations) + { + $this->filterableRelations = $relations; + } + + /** + * Set related columns using aggregate function (e.g. `package.label` would become `package_label`) + * which enables filtering, searching, and sorting on the front end + * + * @param array $columns can be an array of keys (e.g. `user.name` or a nested array with + * `relation`, `table`, and `column` attributes) + * @return void + */ + public function setRelationshipColumns(array $columns) + { + foreach($columns as $column) { + + // advanced + if (is_array($column)) { + + $this->query->withAggregate($column['relation'], $column['table'].'.'.$column['column']); + + continue; + } + + // not a relationship + if(!Str::contains($column, '.')) { + continue; + } + + // normal rx + $relationship = Str::before($column, '.'); + $key = Str::after($column, '.'); + + $this->query->withAggregate($relationship, $key); + } + } + + /** + * Get the resulting paginated collection + * + * @return LengthAwarePaginator + */ + public function get(): LengthAwarePaginator + { + // handle sort + if (!empty(request()->query('sortBy'))) { + if (Str::contains(request()->query('sortBy'), '.')) { + $this->query->joinRelation(Str::before(request()->query('sortBy'), '.')); + } + $this->query->orderBy( + request()->query('sortBy'), + request()->query('sortDesc', false) == "true" ? 'DESC' : 'ASC' + ); + } + + // handle filter + if (request()->has('filter')) { + + foreach(request()->query('filter') as $filter => $params) { + + if (array_key_exists($filter, $this->filterableRelations)) { + + // filtered rx + foreach(explode(',', $params) as $param) { + $this->query->whereHas($filter, function ($query) use ($filter, $param) { + $query->where($this->filterableRelations[$filter], $param); + }); + } + + } else { + // traditional filter + foreach(explode(',', $params) as $param) { + $this->query->having($filter, $param); + } + + } + } + } + + // handle search + if (request()->has('search') && !empty($this->searchableColumns)) { + // make searchable relationships aggregate columns + $this->setRelationshipColumns($this->searchableColumns); + + $this->query->where(function($query) { + + foreach($this->searchableColumns as $column) { + + // advanced + if (is_array($column)) { + $query->orWhereHas($column['relation'], function($query) use ($column) { + $query->where($column['table'].'.'.$column['column'], "like", '%' . request()->query('search') . '%'); + }); + + continue; + } + + // normal RX + if(Str::contains($column, '.')) { + + $query->orWhereHas(Str::before($column, '.'), function($query) use ($column) { + $query->where(Str::after($column, '.'), "like", '%' . request()->query('search') . '%'); + }); + + continue; + } + + // not rx + $query->orWhere($column, "like", '%' . request()->query('search') . '%'); + } + }); + } + + // handle per page + if (request()->query('itemsPerPage') == "-1") { + $perPage = $this->query->count(); + } else { + $perPage = request()->query('itemsPerPage', 15); + } + + // run + return $this->query->addSelect($this->select)->paginate($perPage); + } +} diff --git a/config/jetstream.php b/config/jetstream.php index 7b9d0f6..b1f2c73 100644 --- a/config/jetstream.php +++ b/config/jetstream.php @@ -60,7 +60,7 @@ return [ 'features' => [ Features::termsAndPrivacyPolicy(), Features::profilePhotos(), - // Features::api(), + Features::api(), // Features::teams(['invitations' => true]), Features::accountDeletion(), ], diff --git a/lang/en.json b/lang/en.json index f40abc4..c2dead1 100644 --- a/lang/en.json +++ b/lang/en.json @@ -70,7 +70,7 @@ "API Token": "API Token", "Please copy your new API token. For your security, it won\\'t be shown again.": "Please copy your new API token. For your security, it won\\'t be shown again.", "API Token Permissions": "API Token Permissions", - "API tokens allow third-party services to authenticate with our application on your behalf.": "API tokens allow third-party services to authenticate with our application on your behalf.", + "API tokens allow third-party services to authenticate with Investbrain on your behalf.": "API tokens allow third-party services to authenticate with Investbrain on your behalf.", "Delete API Token": "Delete API Token", "Are you sure you would like to delete this API token?": "Are you sure you would like to delete this API token?", "This is a secure area of the application. Please confirm your password before continuing.": "This is a secure area of the application. Please confirm your password before continuing.", diff --git a/lang/es.json b/lang/es.json index 13a81b2..04ef548 100644 --- a/lang/es.json +++ b/lang/es.json @@ -70,7 +70,7 @@ "API Token": "Token API", "Please copy your new API token. For your security, it won't be shown again.": "Por favor, copia tu nuevo token API. Por seguridad, no se mostrará nuevamente.", "API Token Permissions": "Permisos del Token API", - "API tokens allow third-party services to authenticate with our application on your behalf.": "Los tokens API permiten que servicios de terceros se autentiquen con nuestra aplicación en tu nombre.", + "API tokens allow third-party services to authenticate with Investbrain on your behalf.": "Los tokens API permiten que servicios de terceros se autentiquen con Investbrain en tu nombre.", "Delete API Token": "Eliminar Token API", "Are you sure you would like to delete this API token?": "¿Estás seguro de que deseas eliminar este token API?", "This is a secure area of the application. Please confirm your password before continuing.": "Esta es un área segura de la aplicación. Por favor, confirma tu contraseña antes de continuar.", diff --git a/resources/views/api/api-token-manager.blade.php b/resources/views/api/api-token-manager.blade.php index d161ca0..aad3ece 100644 --- a/resources/views/api/api-token-manager.blade.php +++ b/resources/views/api/api-token-manager.blade.php @@ -6,7 +6,7 @@ - {{ __('API tokens allow third-party services to authenticate with our application on your behalf.') }} + {{ __('API tokens allow third-party services to authenticate with Investbrain on your behalf.') }} diff --git a/routes/api.php b/routes/api.php index ccc387f..d19bba6 100644 --- a/routes/api.php +++ b/routes/api.php @@ -1,8 +1,18 @@ user(); -})->middleware('auth:sanctum'); +Route::middleware(['auth:sanctum'])->group(function () { + + // user + Route::get('/me', [UserController::class, 'me']); + + // portfolio + Route::get('/portfolio', [PortfolioController::class, 'index']); + + // transaction + + // holding +}); \ No newline at end of file diff --git a/routes/web.php b/routes/web.php index d961542..0c60cd7 100644 --- a/routes/web.php +++ b/routes/web.php @@ -5,8 +5,8 @@ use Illuminate\Support\Facades\Route; use App\Http\Controllers\HoldingController; use App\Http\Controllers\DashboardController; use App\Http\Controllers\PortfolioController; -use App\Http\Controllers\ConnectedAccountController; use App\Http\Controllers\TransactionController; +use App\Http\Controllers\ConnectedAccountController; use App\Http\Controllers\InvitedOnboardingController; use Laravel\Jetstream\Http\Controllers\Livewire\PrivacyPolicyController; use Laravel\Jetstream\Http\Controllers\Livewire\TermsOfServiceController;