Catch API endpoint errors.

This commit is contained in:
James Cole
2025-01-26 07:44:41 +01:00
parent c204533195
commit 1c19428a12
13 changed files with 86 additions and 87 deletions

View File

@@ -25,6 +25,7 @@ declare(strict_types=1);
namespace FireflyIII\Api\V1\Controllers\Models\Budget; namespace FireflyIII\Api\V1\Controllers\Models\Budget;
use FireflyIII\Api\V1\Controllers\Controller; use FireflyIII\Api\V1\Controllers\Controller;
use FireflyIII\Enums\TransactionTypeEnum;
use FireflyIII\Exceptions\FireflyException; use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Helpers\Collector\GroupCollectorInterface; use FireflyIII\Helpers\Collector\GroupCollectorInterface;
use FireflyIII\Models\Budget; use FireflyIII\Models\Budget;
@@ -208,6 +209,8 @@ class ListController extends Controller
$collector = app(GroupCollectorInterface::class); $collector = app(GroupCollectorInterface::class);
$collector $collector
->setUser($admin) ->setUser($admin)
// withdrawals only
->setTypes([TransactionTypeEnum::WITHDRAWAL->value])
// filter on budget. // filter on budget.
->withoutBudget() ->withoutBudget()
// all info needed for the API: // all info needed for the API:

View File

@@ -2,7 +2,7 @@
/* /*
* DestroyController.php * 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). * This file is part of Firefly III (https://github.com/firefly-iii).
* *
@@ -22,7 +22,7 @@
declare(strict_types=1); 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\Controllers\Controller;
use FireflyIII\Api\V2\Request\Model\ExchangeRate\DestroyRequest; use FireflyIII\Api\V2\Request\Model\ExchangeRate\DestroyRequest;

View File

@@ -1,8 +1,8 @@
<?php <?php
/* /*
* ShowController.php * IndexController.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). * This file is part of Firefly III (https://github.com/firefly-iii).
* *
@@ -17,12 +17,12 @@
* GNU Affero General Public License for more details. * GNU Affero General Public License for more details.
* *
* You should have received a copy of the GNU Affero General Public License * You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see https://www.gnu.org/licenses/.
*/ */
declare(strict_types=1); 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\Controllers\Controller;
use FireflyIII\Repositories\UserGroups\ExchangeRate\ExchangeRateRepositoryInterface; use FireflyIII\Repositories\UserGroups\ExchangeRate\ExchangeRateRepositoryInterface;
@@ -38,7 +38,7 @@ class IndexController extends Controller
{ {
use ValidatesUserGroupTrait; use ValidatesUserGroupTrait;
public const string RESOURCE_KEY = 'exchange-rates'; public const string RESOURCE_KEY = 'exchange_rates';
private ExchangeRateRepositoryInterface $repository; private ExchangeRateRepositoryInterface $repository;

View File

@@ -2,7 +2,7 @@
/* /*
* ShowController.php * 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). * This file is part of Firefly III (https://github.com/firefly-iii).
* *
@@ -17,12 +17,12 @@
* GNU Affero General Public License for more details. * GNU Affero General Public License for more details.
* *
* You should have received a copy of the GNU Affero General Public License * You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see https://www.gnu.org/licenses/.
*/ */
declare(strict_types=1); 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\Controllers\Controller;
use FireflyIII\Models\TransactionCurrency; use FireflyIII\Models\TransactionCurrency;

View File

@@ -1,8 +1,8 @@
<?php <?php
/* /*
* DestroyController.php * StoreController.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). * This file is part of Firefly III (https://github.com/firefly-iii).
* *
@@ -22,7 +22,7 @@
declare(strict_types=1); 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\Controllers\Controller;
use FireflyIII\Api\V2\Request\Model\ExchangeRate\StoreRequest; use FireflyIII\Api\V2\Request\Model\ExchangeRate\StoreRequest;

View File

@@ -1,8 +1,8 @@
<?php <?php
/* /*
* DestroyController.php * UpdateController.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). * This file is part of Firefly III (https://github.com/firefly-iii).
* *
@@ -22,7 +22,7 @@
declare(strict_types=1); 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\Controllers\Controller;
use FireflyIII\Api\V2\Request\Model\ExchangeRate\UpdateRequest; use FireflyIII\Api\V2\Request\Model\ExchangeRate\UpdateRequest;

View File

@@ -98,34 +98,6 @@ class PreferencesController extends Controller
return response()->json($manager->createData($resource)->toArray())->header('Content-Type', self::CONTENT_TYPE); return response()->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<int, Preference> $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: * This endpoint is documented at:
* https://api-docs.firefly-iii.org/?urls.primaryName=2.0.0%20(v1)#/preferences/storePreference * https://api-docs.firefly-iii.org/?urls.primaryName=2.0.0%20(v1)#/preferences/storePreference

View File

@@ -31,6 +31,8 @@ use FireflyIII\Support\Request\ConvertsDataTypes;
use Illuminate\Foundation\Http\FormRequest; use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use Illuminate\Validation\Validator; use Illuminate\Validation\Validator;
use TypeError;
use ValueError;
/** /**
* Class StoreRequest * Class StoreRequest
@@ -95,16 +97,38 @@ class StoreRequest extends FormRequest
{ {
$validator->after( $validator->after(
static function (Validator $validator): void { static function (Validator $validator): void {
$data = $validator->getData(); $data = $validator->getData();
$min = (string) ($data['amount_min'] ?? '0'); $min = $data['amount_min'] ?? '0';
$max = (string) ($data['amount_max'] ?? '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')); $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()); Log::channel('audit')->error(sprintf('Validation errors in %s', __CLASS__), $validator->errors()->toArray());
} }
} }

View File

@@ -38,6 +38,15 @@ class IsValidPositiveAmount implements ValidationRule
*/ */
public function validate(string $attribute, mixed $value, \Closure $fail): void 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; $value = (string) $value;
// must not be empty: // must not be empty:
if ($this->emptyString($value)) { if ($this->emptyString($value)) {

View File

@@ -113,14 +113,14 @@ class UserGroupTransformer extends AbstractTransformer
'created_at' => $userGroup->created_at->toAtomString(), 'created_at' => $userGroup->created_at->toAtomString(),
'updated_at' => $userGroup->updated_at->toAtomString(), 'updated_at' => $userGroup->updated_at->toAtomString(),
'in_use' => $this->inUse[$userGroup->id] ?? false, 'in_use' => $this->inUse[$userGroup->id] ?? false,
'title' => $userGroup->title,
'can_see_members' => $this->membershipsVisible[$userGroup->id] ?? false, '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_id' => (string) $currency->id,
'native_currency_name' => $currency->name, 'native_currency_name' => $currency->name,
'native_currency_code' => $currency->code, 'native_currency_code' => $currency->code,
'native_currency_symbol' => $currency->symbol, 'native_currency_symbol' => $currency->symbol,
'native_currency_decimal_places' => $currency->decimal_places, '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. // if the user has a specific role in this group, then collect the memberships.
} }

View File

@@ -68,7 +68,7 @@ export default {
this.downloadCurrencies(1); this.downloadCurrencies(1);
}, },
downloadCurrencies: function (page) { 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) { for (let i in response.data.data) {
if (response.data.data.hasOwnProperty(i)) { if (response.data.data.hasOwnProperty(i)) {
let current = response.data.data[i]; let current = response.data.data[i];

View File

@@ -183,7 +183,7 @@ export default {
if(e) e.preventDefault(); if(e) e.preventDefault();
this.posting = true; this.posting = true;
axios.post("./api/v2/exchange-rates", { axios.post("./api/v1/exchange-rates", {
from: this.from_code, from: this.from_code,
to: this.to_code, to: this.to_code,
rate: this.newRate, rate: this.newRate,
@@ -214,7 +214,7 @@ export default {
// console.log('Rate is ' + this.rates[index].rate); // console.log('Rate is ' + this.rates[index].rate);
// console.log('ID is ' + this.rates[index].rate_id); // console.log('ID is ' + this.rates[index].rate_id);
this.updating = true; 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(() => { .then(() => {
this.updating = false; this.updating = false;
}); });
@@ -224,7 +224,7 @@ export default {
// console.log('Inverse is ' + this.rates[index].inverse); // console.log('Inverse is ' + this.rates[index].inverse);
// console.log('Inverse ID is ' + this.rates[index].inverse_id); // console.log('Inverse ID is ' + this.rates[index].inverse_id);
this.updating = true; 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(() => { .then(() => {
this.updating = false; this.updating = false;
}); });
@@ -239,9 +239,9 @@ export default {
// console.log(parts); // console.log(parts);
// delete A to B // 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. // 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); this.rates.splice(index, 1);
}, },
@@ -263,14 +263,14 @@ export default {
}, },
downloadCurrencies: function () { downloadCurrencies: function () {
this.loading = true; 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 = { this.from = {
id: response.data.data.id, id: response.data.data.id,
code: response.data.data.attributes.code, code: response.data.data.attributes.code,
name: response.data.data.attributes.name, 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); // console.log(response.data.data);
this.to = { this.to = {
id: response.data.data.id, id: response.data.data.id,
@@ -283,7 +283,7 @@ export default {
this.tempRates = {}; this.tempRates = {};
this.rates = []; this.rates = [];
this.loading = true; 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) { for (let i in response.data.data) {
if (response.data.data.hasOwnProperty(i)) { if (response.data.data.hasOwnProperty(i)) {
let current = response.data.data[i]; let current = response.data.data[i];

View File

@@ -22,12 +22,6 @@
declare(strict_types=1); 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 // V2 API route for Summary boxes
// BASIC // 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 ROUTES.
// Chart accounts // Chart accounts
Route::group( Route::group(
@@ -804,6 +794,7 @@ Route::group(
Route::get('', ['uses' => 'ShowController@index', 'as' => 'index']); Route::get('', ['uses' => 'ShowController@index', 'as' => 'index']);
Route::post('', ['uses' => 'StoreController@store', 'as' => 'store']); Route::post('', ['uses' => 'StoreController@store', 'as' => 'store']);
Route::get('default', ['uses' => 'ShowController@showDefault', 'as' => 'show.default']); 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::get('{currency_code}', ['uses' => 'ShowController@show', 'as' => 'show']);
Route::put('{currency_code}', ['uses' => 'UpdateController@update', 'as' => 'update']); Route::put('{currency_code}', ['uses' => 'UpdateController@update', 'as' => 'update']);
Route::delete('{currency_code}', ['uses' => 'DestroyController@destroy', 'as' => 'delete']); Route::delete('{currency_code}', ['uses' => 'DestroyController@destroy', 'as' => 'delete']);