From 1c19428a12bd2d485bc28b3022b6c22cbc227318 Mon Sep 17 00:00:00 2001 From: James Cole Date: Sun, 26 Jan 2025 07:44:41 +0100 Subject: [PATCH] Catch API endpoint errors. --- .../Models/Budget/ListController.php | 3 ++ .../DestroyController.php | 4 +- .../CurrencyExchangeRate}/IndexController.php | 10 ++--- .../CurrencyExchangeRate}/ShowController.php | 6 +-- .../CurrencyExchangeRate}/StoreController.php | 6 +-- .../UpdateController.php | 6 +-- .../User/PreferencesController.php | 28 ------------ .../V1/Requests/Models/Bill/StoreRequest.php | 34 +++++++++++--- app/Rules/IsValidPositiveAmount.php | 9 ++++ app/Transformers/UserGroupTransformer.php | 4 +- .../src/components/exchange-rates/Index.vue | 2 +- .../src/components/exchange-rates/Rates.vue | 16 +++---- routes/api.php | 45 ++++++++----------- 13 files changed, 86 insertions(+), 87 deletions(-) rename app/Api/{V2/Controllers/Model/ExchangeRate => V1/Controllers/Models/CurrencyExchangeRate}/DestroyController.php (95%) rename app/Api/{V2/Controllers/Model/ExchangeRate => V1/Controllers/Models/CurrencyExchangeRate}/IndexController.php (89%) rename app/Api/{V2/Controllers/Model/ExchangeRate => V1/Controllers/Models/CurrencyExchangeRate}/ShowController.php (93%) rename app/Api/{V2/Controllers/Model/ExchangeRate => V1/Controllers/Models/CurrencyExchangeRate}/StoreController.php (95%) rename app/Api/{V2/Controllers/Model/ExchangeRate => V1/Controllers/Models/CurrencyExchangeRate}/UpdateController.php (94%) diff --git a/app/Api/V1/Controllers/Models/Budget/ListController.php b/app/Api/V1/Controllers/Models/Budget/ListController.php index 709d092afe..5e5bb38ac6 100644 --- a/app/Api/V1/Controllers/Models/Budget/ListController.php +++ b/app/Api/V1/Controllers/Models/Budget/ListController.php @@ -25,6 +25,7 @@ declare(strict_types=1); namespace FireflyIII\Api\V1\Controllers\Models\Budget; use FireflyIII\Api\V1\Controllers\Controller; +use FireflyIII\Enums\TransactionTypeEnum; use FireflyIII\Exceptions\FireflyException; use FireflyIII\Helpers\Collector\GroupCollectorInterface; use FireflyIII\Models\Budget; @@ -208,6 +209,8 @@ class ListController extends Controller $collector = app(GroupCollectorInterface::class); $collector ->setUser($admin) + // withdrawals only + ->setTypes([TransactionTypeEnum::WITHDRAWAL->value]) // filter on budget. ->withoutBudget() // all info needed for the API: diff --git a/app/Api/V2/Controllers/Model/ExchangeRate/DestroyController.php b/app/Api/V1/Controllers/Models/CurrencyExchangeRate/DestroyController.php similarity index 95% rename from app/Api/V2/Controllers/Model/ExchangeRate/DestroyController.php rename to app/Api/V1/Controllers/Models/CurrencyExchangeRate/DestroyController.php index e46cc5375d..ef262dd73c 100644 --- a/app/Api/V2/Controllers/Model/ExchangeRate/DestroyController.php +++ b/app/Api/V1/Controllers/Models/CurrencyExchangeRate/DestroyController.php @@ -2,7 +2,7 @@ /* * DestroyController.php - * Copyright (c) 2024 james@firefly-iii.org. + * Copyright (c) 2025 james@firefly-iii.org. * * This file is part of Firefly III (https://github.com/firefly-iii). * @@ -22,7 +22,7 @@ declare(strict_types=1); -namespace FireflyIII\Api\V2\Controllers\Model\ExchangeRate; +namespace FireflyIII\Api\V1\Controllers\Models\CurrencyExchangeRate; use FireflyIII\Api\V2\Controllers\Controller; use FireflyIII\Api\V2\Request\Model\ExchangeRate\DestroyRequest; diff --git a/app/Api/V2/Controllers/Model/ExchangeRate/IndexController.php b/app/Api/V1/Controllers/Models/CurrencyExchangeRate/IndexController.php similarity index 89% rename from app/Api/V2/Controllers/Model/ExchangeRate/IndexController.php rename to app/Api/V1/Controllers/Models/CurrencyExchangeRate/IndexController.php index d800678946..108d4cae1b 100644 --- a/app/Api/V2/Controllers/Model/ExchangeRate/IndexController.php +++ b/app/Api/V1/Controllers/Models/CurrencyExchangeRate/IndexController.php @@ -1,8 +1,8 @@ . + * along with this program. If not, see https://www.gnu.org/licenses/. */ declare(strict_types=1); -namespace FireflyIII\Api\V2\Controllers\Model\ExchangeRate; +namespace FireflyIII\Api\V1\Controllers\Models\CurrencyExchangeRate; use FireflyIII\Api\V2\Controllers\Controller; use FireflyIII\Repositories\UserGroups\ExchangeRate\ExchangeRateRepositoryInterface; @@ -38,7 +38,7 @@ class IndexController extends Controller { use ValidatesUserGroupTrait; - public const string RESOURCE_KEY = 'exchange-rates'; + public const string RESOURCE_KEY = 'exchange_rates'; private ExchangeRateRepositoryInterface $repository; diff --git a/app/Api/V2/Controllers/Model/ExchangeRate/ShowController.php b/app/Api/V1/Controllers/Models/CurrencyExchangeRate/ShowController.php similarity index 93% rename from app/Api/V2/Controllers/Model/ExchangeRate/ShowController.php rename to app/Api/V1/Controllers/Models/CurrencyExchangeRate/ShowController.php index 5a4f40c545..a8e2226385 100644 --- a/app/Api/V2/Controllers/Model/ExchangeRate/ShowController.php +++ b/app/Api/V1/Controllers/Models/CurrencyExchangeRate/ShowController.php @@ -2,7 +2,7 @@ /* * ShowController.php - * Copyright (c) 2023 james@firefly-iii.org + * Copyright (c) 2025 james@firefly-iii.org. * * This file is part of Firefly III (https://github.com/firefly-iii). * @@ -17,12 +17,12 @@ * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . + * along with this program. If not, see https://www.gnu.org/licenses/. */ declare(strict_types=1); -namespace FireflyIII\Api\V2\Controllers\Model\ExchangeRate; +namespace FireflyIII\Api\V1\Controllers\Models\CurrencyExchangeRate; use FireflyIII\Api\V2\Controllers\Controller; use FireflyIII\Models\TransactionCurrency; diff --git a/app/Api/V2/Controllers/Model/ExchangeRate/StoreController.php b/app/Api/V1/Controllers/Models/CurrencyExchangeRate/StoreController.php similarity index 95% rename from app/Api/V2/Controllers/Model/ExchangeRate/StoreController.php rename to app/Api/V1/Controllers/Models/CurrencyExchangeRate/StoreController.php index 73e14892a7..df9a44796f 100644 --- a/app/Api/V2/Controllers/Model/ExchangeRate/StoreController.php +++ b/app/Api/V1/Controllers/Models/CurrencyExchangeRate/StoreController.php @@ -1,8 +1,8 @@ json($manager->createData($resource)->toArray())->header('Content-Type', self::CONTENT_TYPE); } - /** - * TODO This endpoint is not documented. - * - * Return a single preference by name. - * - * @param Collection $collection - */ - public function showList(Collection $collection): JsonResponse - { - $manager = $this->getManager(); - $count = $collection->count(); - $pageSize = $this->parameters->get('limit'); - $preferences = $collection->slice(($this->parameters->get('page') - 1) * $pageSize, $pageSize); - - // make paginator: - $paginator = new LengthAwarePaginator($preferences, $count, $pageSize, $this->parameters->get('page')); - $paginator->setPath(route('api.v1.preferences.show-list').$this->buildParams()); - - /** @var PreferenceTransformer $transformer */ - $transformer = app(PreferenceTransformer::class); - $transformer->setParameters($this->parameters); - - $resource = new FractalCollection($preferences, $transformer, self::RESOURCE_KEY); - $resource->setPaginator(new IlluminatePaginatorAdapter($paginator)); - - return response()->json($manager->createData($resource)->toArray())->header('Content-Type', self::CONTENT_TYPE); - } - /** * This endpoint is documented at: * https://api-docs.firefly-iii.org/?urls.primaryName=2.0.0%20(v1)#/preferences/storePreference diff --git a/app/Api/V1/Requests/Models/Bill/StoreRequest.php b/app/Api/V1/Requests/Models/Bill/StoreRequest.php index e6b82c2216..7e0ba7bd80 100644 --- a/app/Api/V1/Requests/Models/Bill/StoreRequest.php +++ b/app/Api/V1/Requests/Models/Bill/StoreRequest.php @@ -31,6 +31,8 @@ use FireflyIII\Support\Request\ConvertsDataTypes; use Illuminate\Foundation\Http\FormRequest; use Illuminate\Support\Facades\Log; use Illuminate\Validation\Validator; +use TypeError; +use ValueError; /** * Class StoreRequest @@ -95,16 +97,38 @@ class StoreRequest extends FormRequest { $validator->after( static function (Validator $validator): void { - $data = $validator->getData(); - $min = (string) ($data['amount_min'] ?? '0'); - $max = (string) ($data['amount_max'] ?? '0'); + $data = $validator->getData(); + $min = $data['amount_min'] ?? '0'; + $max = $data['amount_max'] ?? '0'; - if (1 === bccomp($min, $max)) { + if(is_array($min) || is_array($max)) { + $validator->errors()->add('amount_min', (string) trans('validation.generic_invalid')); + $validator->errors()->add('amount_max', (string) trans('validation.generic_invalid')); + $min ='0'; + $max = '0'; + } + $result = false; + try { + $result = bccomp($min, $max); + } catch (ValueError $e) { + Log::error($e->getMessage()); + $validator->errors()->add('amount_min', (string) trans('validation.generic_invalid')); + $validator->errors()->add('amount_max', (string) trans('validation.generic_invalid')); + } + + if (1 === $result) { $validator->errors()->add('amount_min', (string) trans('validation.amount_min_over_max')); } } ); - if ($validator->fails()) { + $failed = false; + try { + $failed = $validator->fails(); + } catch (TypeError $e) { + Log::error($e->getMessage()); + $failed = false; + } + if ($failed) { Log::channel('audit')->error(sprintf('Validation errors in %s', __CLASS__), $validator->errors()->toArray()); } } diff --git a/app/Rules/IsValidPositiveAmount.php b/app/Rules/IsValidPositiveAmount.php index 233fab613d..0e09647f35 100644 --- a/app/Rules/IsValidPositiveAmount.php +++ b/app/Rules/IsValidPositiveAmount.php @@ -38,6 +38,15 @@ class IsValidPositiveAmount implements ValidationRule */ public function validate(string $attribute, mixed $value, \Closure $fail): void { + if(is_array($value)) { + $fail('validation.numeric')->translate(); + $message = sprintf('IsValidPositiveAmount: "%s" is not a number.', json_encode($value)); + Log::debug($message); + Log::channel('audit')->info($message); + + return; + } + $value = (string) $value; // must not be empty: if ($this->emptyString($value)) { diff --git a/app/Transformers/UserGroupTransformer.php b/app/Transformers/UserGroupTransformer.php index a001ca7c60..95f8a39613 100644 --- a/app/Transformers/UserGroupTransformer.php +++ b/app/Transformers/UserGroupTransformer.php @@ -113,14 +113,14 @@ class UserGroupTransformer extends AbstractTransformer 'created_at' => $userGroup->created_at->toAtomString(), 'updated_at' => $userGroup->updated_at->toAtomString(), 'in_use' => $this->inUse[$userGroup->id] ?? false, - 'title' => $userGroup->title, 'can_see_members' => $this->membershipsVisible[$userGroup->id] ?? false, - 'members' => array_values($this->memberships[$userGroup->id] ?? []), + 'title' => $userGroup->title, 'native_currency_id' => (string) $currency->id, 'native_currency_name' => $currency->name, 'native_currency_code' => $currency->code, 'native_currency_symbol' => $currency->symbol, 'native_currency_decimal_places' => $currency->decimal_places, + 'members' => array_values($this->memberships[$userGroup->id] ?? []), ]; // if the user has a specific role in this group, then collect the memberships. } diff --git a/resources/assets/v1/src/components/exchange-rates/Index.vue b/resources/assets/v1/src/components/exchange-rates/Index.vue index b06dd5f683..38f44a84ad 100644 --- a/resources/assets/v1/src/components/exchange-rates/Index.vue +++ b/resources/assets/v1/src/components/exchange-rates/Index.vue @@ -68,7 +68,7 @@ export default { this.downloadCurrencies(1); }, downloadCurrencies: function (page) { - axios.get("./api/v2/currencies?enabled=1&page=" + page).then((response) => { + axios.get("./api/v1/currencies?enabled=1&page=" + page).then((response) => { for (let i in response.data.data) { if (response.data.data.hasOwnProperty(i)) { let current = response.data.data[i]; diff --git a/resources/assets/v1/src/components/exchange-rates/Rates.vue b/resources/assets/v1/src/components/exchange-rates/Rates.vue index 502fe7dd6b..3c8250756e 100644 --- a/resources/assets/v1/src/components/exchange-rates/Rates.vue +++ b/resources/assets/v1/src/components/exchange-rates/Rates.vue @@ -183,7 +183,7 @@ export default { if(e) e.preventDefault(); this.posting = true; - axios.post("./api/v2/exchange-rates", { + axios.post("./api/v1/exchange-rates", { from: this.from_code, to: this.to_code, rate: this.newRate, @@ -214,7 +214,7 @@ export default { // console.log('Rate is ' + this.rates[index].rate); // console.log('ID is ' + this.rates[index].rate_id); this.updating = true; - axios.put("./api/v2/exchange-rates/" + this.rates[index].rate_id, {rate: this.rates[index].rate}) + axios.put("./api/v1/exchange-rates/" + this.rates[index].rate_id, {rate: this.rates[index].rate}) .then(() => { this.updating = false; }); @@ -224,7 +224,7 @@ export default { // console.log('Inverse is ' + this.rates[index].inverse); // console.log('Inverse ID is ' + this.rates[index].inverse_id); this.updating = true; - axios.put("./api/v2/exchange-rates/" + this.rates[index].inverse_id, {rate: this.rates[index].inverse}) + axios.put("./api/v1/exchange-rates/" + this.rates[index].inverse_id, {rate: this.rates[index].inverse}) .then(() => { this.updating = false; }); @@ -239,9 +239,9 @@ export default { // console.log(parts); // delete A to B - axios.delete("./api/v2/exchange-rates/rates/" + parts.from + '/' + parts.to + '?date=' + format(parts.date, 'yyyy-MM-dd')); + axios.delete("./api/v1/exchange-rates/rates/" + parts.from + '/' + parts.to + '?date=' + format(parts.date, 'yyyy-MM-dd')); // delete B to A. - axios.delete("./api/v2/exchange-rates/rates/" + parts.to + '/' + parts.from + '?date=' + format(parts.date, 'yyyy-MM-dd')); + axios.delete("./api/v1/exchange-rates/rates/" + parts.to + '/' + parts.from + '?date=' + format(parts.date, 'yyyy-MM-dd')); this.rates.splice(index, 1); }, @@ -263,14 +263,14 @@ export default { }, downloadCurrencies: function () { this.loading = true; - axios.get("./api/v2/currencies/" + this.from_code).then((response) => { + axios.get("./api/v1/currencies/" + this.from_code).then((response) => { this.from = { id: response.data.data.id, code: response.data.data.attributes.code, name: response.data.data.attributes.name, } }); - axios.get("./api/v2/currencies/" + this.to_code).then((response) => { + axios.get("./api/v1/currencies/" + this.to_code).then((response) => { // console.log(response.data.data); this.to = { id: response.data.data.id, @@ -283,7 +283,7 @@ export default { this.tempRates = {}; this.rates = []; this.loading = true; - axios.get("./api/v2/exchange-rates/rates/" + this.from_code + '/' + this.to_code + '?page=' + page).then((response) => { + axios.get("./api/v1/exchange-rates/rates/" + this.from_code + '/' + this.to_code + '?page=' + page).then((response) => { for (let i in response.data.data) { if (response.data.data.hasOwnProperty(i)) { let current = response.data.data[i]; diff --git a/routes/api.php b/routes/api.php index 6f296533d6..01ffe8ca3f 100644 --- a/routes/api.php +++ b/routes/api.php @@ -22,12 +22,6 @@ declare(strict_types=1); -use FireflyIII\Api\V2\Controllers\JsonApi\AccountController; -use LaravelJsonApi\Laravel\Facades\JsonApiRoute; -use LaravelJsonApi\Laravel\Http\Controllers\JsonApiController; -use LaravelJsonApi\Laravel\Routing\Relationships; -use LaravelJsonApi\Laravel\Routing\ResourceRegistrar; - /* * * ____ ____ ___ .______ ______ __ __ .___________. _______ _______. @@ -105,27 +99,6 @@ Route::group( } ); -// exchange rates -Route::group( - [ - 'namespace' => 'FireflyIII\Api\V2\Controllers\Model\ExchangeRate', - 'prefix' => 'v2/exchange-rates', - 'as' => 'api.v2.exchange-rates.', - ], - static function (): void { - Route::get('', ['uses' => 'IndexController@index', 'as' => 'index']); - Route::get('rates/{fromCurrencyCode}/{toCurrencyCode}', ['uses' => 'ShowController@show', 'as' => 'show']); - Route::delete('rates/{fromCurrencyCode}/{toCurrencyCode}', ['uses' => 'DestroyController@destroy', 'as' => 'destroy']); - Route::put('{userGroupExchangeRate}', ['uses' => 'UpdateController@update', 'as' => 'update']); - Route::post('', ['uses' => 'StoreController@store', 'as' => 'store']); - // - // Route::put('{userGroup}', ['uses' => 'UpdateController@update', 'as' => 'update']); - // Route::post('{userGroup}/use', ['uses' => 'UpdateController@useUserGroup', 'as' => 'use']); - // Route::put('{userGroup}/update-membership', ['uses' => 'UpdateController@updateMembership', 'as' => 'updateMembership']); - // Route::delete('{userGroup}', ['uses' => 'DestroyController@destroy', 'as' => 'destroy']); - } -); - // V2 API route for Summary boxes // BASIC @@ -336,6 +309,23 @@ Route::group( } ); +// exchange rates +Route::group( + [ + 'namespace' => 'FireflyIII\Api\V1\Controllers\Model\CurrencyExchangeRate', + 'prefix' => 'v1/exchange-rates', + 'as' => 'api.v1.exchange-rates.', + ], + static function (): void { + Route::get('', ['uses' => 'IndexController@index', 'as' => 'index']); + Route::get('rates/{fromCurrencyCode}/{toCurrencyCode}', ['uses' => 'ShowController@show', 'as' => 'show']); + Route::get('{userGroupExchangeRate}', ['uses' => 'ShowController@showSingle', 'as' => 'show.single']); + Route::delete('rates/{fromCurrencyCode}/{toCurrencyCode}', ['uses' => 'DestroyController@destroy', 'as' => 'destroy']); + Route::put('{userGroupExchangeRate}', ['uses' => 'UpdateController@update', 'as' => 'update']); + Route::post('', ['uses' => 'StoreController@store', 'as' => 'store']); + } +); + // CHART ROUTES. // Chart accounts Route::group( @@ -804,6 +794,7 @@ Route::group( Route::get('', ['uses' => 'ShowController@index', 'as' => 'index']); Route::post('', ['uses' => 'StoreController@store', 'as' => 'store']); Route::get('default', ['uses' => 'ShowController@showDefault', 'as' => 'show.default']); + Route::get('native', ['uses' => 'ShowController@showDefault', 'as' => 'show.native']); Route::get('{currency_code}', ['uses' => 'ShowController@show', 'as' => 'show']); Route::put('{currency_code}', ['uses' => 'UpdateController@update', 'as' => 'update']); Route::delete('{currency_code}', ['uses' => 'DestroyController@destroy', 'as' => 'delete']);