Expand API administration validation

This commit is contained in:
James Cole 2024-04-07 06:06:40 +02:00
parent 2c4f2082fe
commit 74291b3870
No known key found for this signature in database
GPG Key ID: B49A324B7EAD6D80
12 changed files with 380 additions and 291 deletions

View File

@ -27,6 +27,7 @@ namespace FireflyIII\Api\V2\Controllers\Chart;
use Carbon\Carbon; use Carbon\Carbon;
use FireflyIII\Api\V2\Controllers\Controller; use FireflyIII\Api\V2\Controllers\Controller;
use FireflyIII\Api\V2\Request\Chart\DashboardChartRequest; use FireflyIII\Api\V2\Request\Chart\DashboardChartRequest;
use FireflyIII\Enums\UserRoleEnum;
use FireflyIII\Exceptions\FireflyException; use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Models\Account; use FireflyIII\Models\Account;
use FireflyIII\Models\AccountType; use FireflyIII\Models\AccountType;
@ -46,6 +47,7 @@ class AccountController extends Controller
use ValidatesUserGroupTrait; use ValidatesUserGroupTrait;
private AccountRepositoryInterface $repository; private AccountRepositoryInterface $repository;
protected array $acceptedRoles = [UserRoleEnum::READ_ONLY];
public function __construct() public function __construct()
{ {
@ -54,9 +56,7 @@ class AccountController extends Controller
function ($request, $next) { function ($request, $next) {
$this->repository = app(AccountRepositoryInterface::class); $this->repository = app(AccountRepositoryInterface::class);
$userGroup = $this->validateUserGroup($request); $userGroup = $this->validateUserGroup($request);
if (null !== $userGroup) { $this->repository->setUserGroup($userGroup);
$this->repository->setUserGroup($userGroup);
}
return $next($request); return $next($request);
} }
@ -81,15 +81,15 @@ class AccountController extends Controller
public function dashboard(DashboardChartRequest $request): JsonResponse public function dashboard(DashboardChartRequest $request): JsonResponse
{ {
/** @var Carbon $start */ /** @var Carbon $start */
$start = $this->parameters->get('start'); $start = $this->parameters->get('start');
/** @var Carbon $end */ /** @var Carbon $end */
$end = $this->parameters->get('end'); $end = $this->parameters->get('end');
$end->endOfDay(); $end->endOfDay();
/** @var TransactionCurrency $default */ /** @var TransactionCurrency $default */
$default = app('amount')->getDefaultCurrency(); $default = app('amount')->getDefaultCurrency();
$params = $request->getAll(); $params = $request->getAll();
/** @var Collection $accounts */ /** @var Collection $accounts */
$accounts = $params['accounts']; $accounts = $params['accounts'];
@ -105,7 +105,7 @@ class AccountController extends Controller
$frontpage->save(); $frontpage->save();
} }
$accounts = $this->repository->getAccountsById($frontpage->data); $accounts = $this->repository->getAccountsById($frontpage->data);
} }
// both options are overruled by "preselected" // both options are overruled by "preselected"
@ -121,11 +121,11 @@ class AccountController extends Controller
/** @var Account $account */ /** @var Account $account */
foreach ($accounts as $account) { foreach ($accounts as $account) {
$currency = $this->repository->getAccountCurrency($account); $currency = $this->repository->getAccountCurrency($account);
if (null === $currency) { if (null === $currency) {
$currency = $default; $currency = $default;
} }
$currentSet = [ $currentSet = [
'label' => $account->name, 'label' => $account->name,
// the currency that belongs to the account. // the currency that belongs to the account.
'currency_id' => (string)$currency->id, 'currency_id' => (string)$currency->id,
@ -144,25 +144,25 @@ class AccountController extends Controller
'entries' => [], 'entries' => [],
'native_entries' => [], 'native_entries' => [],
]; ];
$currentStart = clone $start; $currentStart = clone $start;
$range = app('steam')->balanceInRange($account, $start, clone $end, $currency); $range = app('steam')->balanceInRange($account, $start, clone $end, $currency);
$rangeConverted = app('steam')->balanceInRangeConverted($account, $start, clone $end, $default); $rangeConverted = app('steam')->balanceInRangeConverted($account, $start, clone $end, $default);
$previous = array_values($range)[0]; $previous = array_values($range)[0];
$previousConverted = array_values($rangeConverted)[0]; $previousConverted = array_values($rangeConverted)[0];
while ($currentStart <= $end) { while ($currentStart <= $end) {
$format = $currentStart->format('Y-m-d'); $format = $currentStart->format('Y-m-d');
$label = $currentStart->toAtomString(); $label = $currentStart->toAtomString();
$balance = array_key_exists($format, $range) ? $range[$format] : $previous; $balance = array_key_exists($format, $range) ? $range[$format] : $previous;
$balanceConverted = array_key_exists($format, $rangeConverted) ? $rangeConverted[$format] : $previousConverted; $balanceConverted = array_key_exists($format, $rangeConverted) ? $rangeConverted[$format] : $previousConverted;
$previous = $balance; $previous = $balance;
$previousConverted = $balanceConverted; $previousConverted = $balanceConverted;
$currentStart->addDay(); $currentStart->addDay();
$currentSet['entries'][$label] = $balance; $currentSet['entries'][$label] = $balance;
$currentSet['native_entries'][$label] = $balanceConverted; $currentSet['native_entries'][$label] = $balanceConverted;
} }
$chartData[] = $currentSet; $chartData[] = $currentSet;
} }
return response()->json($this->clean($chartData)); return response()->json($this->clean($chartData));

View File

@ -27,6 +27,7 @@ namespace FireflyIII\Api\V2\Controllers;
use Carbon\Carbon; use Carbon\Carbon;
use Carbon\Exceptions\InvalidDateException; use Carbon\Exceptions\InvalidDateException;
use Carbon\Exceptions\InvalidFormatException; use Carbon\Exceptions\InvalidFormatException;
use FireflyIII\Enums\UserRoleEnum;
use FireflyIII\Support\Http\Api\ValidatesUserGroupTrait; use FireflyIII\Support\Http\Api\ValidatesUserGroupTrait;
use FireflyIII\Transformers\V2\AbstractTransformer; use FireflyIII\Transformers\V2\AbstractTransformer;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
@ -55,6 +56,7 @@ class Controller extends BaseController
protected const string CONTENT_TYPE = 'application/vnd.api+json'; protected const string CONTENT_TYPE = 'application/vnd.api+json';
protected ParameterBag $parameters; protected ParameterBag $parameters;
protected array $acceptedRoles = [UserRoleEnum::READ_ONLY];
public function __construct() public function __construct()
{ {

View File

@ -26,6 +26,7 @@ namespace FireflyIII\Api\V2\Controllers\Model\Account;
use FireflyIII\Api\V2\Controllers\Controller; use FireflyIII\Api\V2\Controllers\Controller;
use FireflyIII\Api\V2\Request\Model\Account\IndexRequest; use FireflyIII\Api\V2\Request\Model\Account\IndexRequest;
use FireflyIII\Api\V2\Request\Model\Transaction\InfiniteListRequest; use FireflyIII\Api\V2\Request\Model\Transaction\InfiniteListRequest;
use FireflyIII\Enums\UserRoleEnum;
use FireflyIII\Repositories\UserGroups\Account\AccountRepositoryInterface; use FireflyIII\Repositories\UserGroups\Account\AccountRepositoryInterface;
use FireflyIII\Transformers\V2\AccountTransformer; use FireflyIII\Transformers\V2\AccountTransformer;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
@ -36,6 +37,7 @@ class IndexController extends Controller
public const string RESOURCE_KEY = 'accounts'; public const string RESOURCE_KEY = 'accounts';
private AccountRepositoryInterface $repository; private AccountRepositoryInterface $repository;
protected array $acceptedRoles = [UserRoleEnum::READ_ONLY, UserRoleEnum::MANAGE_TRANSACTIONS];
/** /**
* AccountController constructor. * AccountController constructor.
@ -47,10 +49,8 @@ class IndexController extends Controller
function ($request, $next) { function ($request, $next) {
$this->repository = app(AccountRepositoryInterface::class); $this->repository = app(AccountRepositoryInterface::class);
// new way of user group validation // new way of user group validation
$userGroup = $this->validateUserGroup($request); $userGroup = $this->validateUserGroup($request);
if (null !== $userGroup) { $this->repository->setUserGroup($userGroup);
$this->repository->setUserGroup($userGroup);
}
return $next($request); return $next($request);
} }
@ -77,8 +77,7 @@ class IndexController extends Controller
return response() return response()
->json($this->jsonApiList('accounts', $paginator, $transformer)) ->json($this->jsonApiList('accounts', $paginator, $transformer))
->header('Content-Type', self::CONTENT_TYPE) ->header('Content-Type', self::CONTENT_TYPE);
;
} }
public function infiniteList(InfiniteListRequest $request): JsonResponse public function infiniteList(InfiniteListRequest $request): JsonResponse
@ -86,7 +85,7 @@ class IndexController extends Controller
$this->repository->resetAccountOrder(); $this->repository->resetAccountOrder();
// get accounts of the specified type, and return. // get accounts of the specified type, and return.
$types = $request->getAccountTypes(); $types = $request->getAccountTypes();
// get from repository // get from repository
$accounts = $this->repository->getAccountsInOrder($types, $request->getSortInstructions('accounts'), $request->getStartRow(), $request->getEndRow()); $accounts = $this->repository->getAccountsInOrder($types, $request->getSortInstructions('accounts'), $request->getStartRow(), $request->getEndRow());
@ -98,7 +97,6 @@ class IndexController extends Controller
return response() return response()
->json($this->jsonApiList(self::RESOURCE_KEY, $paginator, $transformer)) ->json($this->jsonApiList(self::RESOURCE_KEY, $paginator, $transformer))
->header('Content-Type', self::CONTENT_TYPE) ->header('Content-Type', self::CONTENT_TYPE);
;
} }
} }

View File

@ -25,7 +25,9 @@ declare(strict_types=1);
namespace FireflyIII\Api\V2\Controllers\Model\Account; namespace FireflyIII\Api\V2\Controllers\Model\Account;
use FireflyIII\Api\V2\Controllers\Controller; use FireflyIII\Api\V2\Controllers\Controller;
use FireflyIII\Enums\UserRoleEnum;
use FireflyIII\Models\Account; use FireflyIII\Models\Account;
use FireflyIII\Repositories\UserGroups\Account\AccountRepositoryInterface;
use FireflyIII\Transformers\V2\AccountTransformer; use FireflyIII\Transformers\V2\AccountTransformer;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
@ -36,6 +38,28 @@ use Illuminate\Http\JsonResponse;
*/ */
class ShowController extends Controller class ShowController extends Controller
{ {
public const string RESOURCE_KEY = 'accounts';
private AccountRepositoryInterface $repository;
protected array $acceptedRoles = [UserRoleEnum::READ_ONLY, UserRoleEnum::MANAGE_TRANSACTIONS];
/**
* AccountController constructor.
*/
public function __construct()
{
parent::__construct();
$this->middleware(
function ($request, $next) {
$this->repository = app(AccountRepositoryInterface::class);
// new way of user group validation
$userGroup = $this->validateUserGroup($request);
$this->repository->setUserGroup($userGroup);
return $next($request);
}
);
}
/** /**
* TODO this endpoint is not yet reachable. * TODO this endpoint is not yet reachable.
*/ */

View File

@ -76,14 +76,12 @@ class BasicController extends Controller
$this->currencyRepos = app(CurrencyRepositoryInterface::class); $this->currencyRepos = app(CurrencyRepositoryInterface::class);
$this->opsRepository = app(OperationsRepositoryInterface::class); $this->opsRepository = app(OperationsRepositoryInterface::class);
$userGroup = $this->validateUserGroup($request); $userGroup = $this->validateUserGroup($request);
if (null !== $userGroup) { $this->abRepository->setUserGroup($userGroup);
$this->abRepository->setUserGroup($userGroup); $this->accountRepository->setUserGroup($userGroup);
$this->accountRepository->setUserGroup($userGroup); $this->billRepository->setUserGroup($userGroup);
$this->billRepository->setUserGroup($userGroup); $this->budgetRepository->setUserGroup($userGroup);
$this->budgetRepository->setUserGroup($userGroup); $this->opsRepository->setUserGroup($userGroup);
$this->opsRepository->setUserGroup($userGroup);
}
return $next($request); return $next($request);
} }
@ -101,8 +99,8 @@ class BasicController extends Controller
public function basic(DateRequest $request): JsonResponse public function basic(DateRequest $request): JsonResponse
{ {
// parameters for boxes: // parameters for boxes:
$start = $this->parameters->get('start'); $start = $this->parameters->get('start');
$end = $this->parameters->get('end'); $end = $this->parameters->get('end');
// balance information: // balance information:
$balanceData = $this->getBalanceInformation($start, $end); $balanceData = $this->getBalanceInformation($start, $end);
@ -119,13 +117,13 @@ class BasicController extends Controller
*/ */
private function getBalanceInformation(Carbon $start, Carbon $end): array private function getBalanceInformation(Carbon $start, Carbon $end): array
{ {
$object = new SummaryBalanceGrouped(); $object = new SummaryBalanceGrouped();
$default = app('amount')->getDefaultCurrency(); $default = app('amount')->getDefaultCurrency();
$object->setDefault($default); $object->setDefault($default);
/** @var User $user */ /** @var User $user */
$user = auth()->user(); $user = auth()->user();
// collect income of user using the new group collector. // collect income of user using the new group collector.
/** @var GroupCollectorInterface $collector */ /** @var GroupCollectorInterface $collector */
@ -137,10 +135,9 @@ class BasicController extends Controller
->setPage($this->parameters->get('page')) ->setPage($this->parameters->get('page'))
// set types of transactions to return. // set types of transactions to return.
->setTypes([TransactionType::DEPOSIT]) ->setTypes([TransactionType::DEPOSIT])
->setRange($start, $end) ->setRange($start, $end);
;
$set = $collector->getExtractedJournals(); $set = $collector->getExtractedJournals();
$object->groupTransactions('income', $set); $object->groupTransactions('income', $set);
// collect expenses of user using the new group collector. // collect expenses of user using the new group collector.
@ -153,9 +150,8 @@ class BasicController extends Controller
->setPage($this->parameters->get('page')) ->setPage($this->parameters->get('page'))
// set types of transactions to return. // set types of transactions to return.
->setTypes([TransactionType::WITHDRAWAL]) ->setTypes([TransactionType::WITHDRAWAL])
->setRange($start, $end) ->setRange($start, $end);
; $set = $collector->getExtractedJournals();
$set = $collector->getExtractedJournals();
$object->groupTransactions('expense', $set); $object->groupTransactions('expense', $set);
return $object->groupData(); return $object->groupData();
@ -170,7 +166,7 @@ class BasicController extends Controller
$paidAmount = $this->billRepository->sumPaidInRange($start, $end); $paidAmount = $this->billRepository->sumPaidInRange($start, $end);
$unpaidAmount = $this->billRepository->sumUnpaidInRange($start, $end); $unpaidAmount = $this->billRepository->sumUnpaidInRange($start, $end);
$return = []; $return = [];
/** /**
* @var array $info * @var array $info
@ -230,14 +226,14 @@ class BasicController extends Controller
{ {
Log::debug(sprintf('Created new ExchangeRateConverter in %s', __METHOD__)); Log::debug(sprintf('Created new ExchangeRateConverter in %s', __METHOD__));
app('log')->debug('Now in getLeftToSpendInfo'); app('log')->debug('Now in getLeftToSpendInfo');
$return = []; $return = [];
$today = today(config('app.timezone')); $today = today(config('app.timezone'));
$available = $this->abRepository->getAvailableBudgetWithCurrency($start, $end); $available = $this->abRepository->getAvailableBudgetWithCurrency($start, $end);
$budgets = $this->budgetRepository->getActiveBudgets(); $budgets = $this->budgetRepository->getActiveBudgets();
$spent = $this->opsRepository->listExpenses($start, $end, null, $budgets); $spent = $this->opsRepository->listExpenses($start, $end, null, $budgets);
$default = app('amount')->getDefaultCurrency(); $default = app('amount')->getDefaultCurrency();
$currencies = []; $currencies = [];
$converter = new ExchangeRateConverter(); $converter = new ExchangeRateConverter();
// native info: // native info:
$nativeLeft = [ $nativeLeft = [
@ -263,8 +259,8 @@ class BasicController extends Controller
*/ */
foreach ($spent as $currencyId => $row) { foreach ($spent as $currencyId => $row) {
app('log')->debug(sprintf('Processing spent array in currency #%d', $currencyId)); app('log')->debug(sprintf('Processing spent array in currency #%d', $currencyId));
$spent = '0'; $spent = '0';
$spentNative = '0'; $spentNative = '0';
// get the sum from the array of transactions (double loop but who cares) // get the sum from the array of transactions (double loop but who cares)
/** @var array $budget */ /** @var array $budget */
@ -281,8 +277,8 @@ class BasicController extends Controller
if ((int)$journal['foreign_currency_id'] === $default->id) { if ((int)$journal['foreign_currency_id'] === $default->id) {
$amountNative = $journal['foreign_amount']; $amountNative = $journal['foreign_amount'];
} }
$spent = bcadd($spent, $amount); $spent = bcadd($spent, $amount);
$spentNative = bcadd($spentNative, $amountNative); $spentNative = bcadd($spentNative, $amountNative);
} }
app('log')->debug(sprintf('Total spent in budget "%s" is %s', $budget['name'], $spent)); app('log')->debug(sprintf('Total spent in budget "%s" is %s', $budget['name'], $spent));
} }
@ -298,9 +294,9 @@ class BasicController extends Controller
app('log')->debug(sprintf('Amount left is %s', $left)); app('log')->debug(sprintf('Amount left is %s', $left));
// how much left per day? // how much left per day?
$days = (int)$today->diffInDays($end, true) + 1; $days = (int)$today->diffInDays($end, true) + 1;
$perDay = '0'; $perDay = '0';
$perDayNative = '0'; $perDayNative = '0';
if (0 !== $days && bccomp($left, '0') > -1) { if (0 !== $days && bccomp($left, '0') > -1) {
$perDay = bcdiv($left, (string)$days); $perDay = bcdiv($left, (string)$days);
} }
@ -309,7 +305,7 @@ class BasicController extends Controller
} }
// left // left
$return[] = [ $return[] = [
'key' => sprintf('left-to-spend-in-%s', $row['currency_code']), 'key' => sprintf('left-to-spend-in-%s', $row['currency_code']),
'value' => $left, 'value' => $left,
'currency_id' => (string)$row['currency_id'], 'currency_id' => (string)$row['currency_id'],
@ -318,10 +314,10 @@ class BasicController extends Controller
'currency_decimal_places' => (int)$row['currency_decimal_places'], 'currency_decimal_places' => (int)$row['currency_decimal_places'],
]; ];
// left (native) // left (native)
$nativeLeft['value'] = $leftNative; $nativeLeft['value'] = $leftNative;
// left per day: // left per day:
$return[] = [ $return[] = [
'key' => sprintf('left-per-day-to-spend-in-%s', $row['currency_code']), 'key' => sprintf('left-per-day-to-spend-in-%s', $row['currency_code']),
'value' => $perDay, 'value' => $perDay,
'currency_id' => (string)$row['currency_id'], 'currency_id' => (string)$row['currency_id'],
@ -331,10 +327,10 @@ class BasicController extends Controller
]; ];
// left per day (native) // left per day (native)
$nativePerDay['value'] = $perDayNative; $nativePerDay['value'] = $perDayNative;
} }
$return[] = $nativeLeft; $return[] = $nativeLeft;
$return[] = $nativePerDay; $return[] = $nativePerDay;
$converter->summarize(); $converter->summarize();
return $return; return $return;
@ -343,8 +339,8 @@ class BasicController extends Controller
private function getNetWorthInfo(Carbon $start, Carbon $end): array private function getNetWorthInfo(Carbon $start, Carbon $end): array
{ {
/** @var UserGroup $userGroup */ /** @var UserGroup $userGroup */
$userGroup = auth()->user()->userGroup; $userGroup = auth()->user()->userGroup;
$date = today(config('app.timezone'))->startOfDay(); $date = today(config('app.timezone'))->startOfDay();
// start and end in the future? use $end // start and end in the future? use $end
if ($this->notInDateRange($date, $start, $end)) { if ($this->notInDateRange($date, $start, $end)) {
/** @var Carbon $date */ /** @var Carbon $date */
@ -354,12 +350,12 @@ class BasicController extends Controller
/** @var NetWorthInterface $netWorthHelper */ /** @var NetWorthInterface $netWorthHelper */
$netWorthHelper = app(NetWorthInterface::class); $netWorthHelper = app(NetWorthInterface::class);
$netWorthHelper->setUserGroup($userGroup); $netWorthHelper->setUserGroup($userGroup);
$allAccounts = $this->accountRepository->getActiveAccountsByType( $allAccounts = $this->accountRepository->getActiveAccountsByType(
[AccountType::ASSET, AccountType::DEFAULT, AccountType::LOAN, AccountType::MORTGAGE, AccountType::DEBT] [AccountType::ASSET, AccountType::DEFAULT, AccountType::LOAN, AccountType::MORTGAGE, AccountType::DEBT]
); );
// filter list on preference of being included. // filter list on preference of being included.
$filtered = $allAccounts->filter( $filtered = $allAccounts->filter(
function (Account $account) { function (Account $account) {
$includeNetWorth = $this->accountRepository->getMetaValue($account, 'include_net_worth'); $includeNetWorth = $this->accountRepository->getMetaValue($account, 'include_net_worth');
@ -367,10 +363,10 @@ class BasicController extends Controller
} }
); );
$netWorthSet = $netWorthHelper->byAccounts($filtered, $date); $netWorthSet = $netWorthHelper->byAccounts($filtered, $date);
$return = []; $return = [];
// in native amount // in native amount
$return[] = [ $return[] = [
'key' => 'net-worth-in-native', 'key' => 'net-worth-in-native',
'value' => $netWorthSet['native']['balance'], 'value' => $netWorthSet['native']['balance'],
'currency_id' => (string)$netWorthSet['native']['currency_id'], 'currency_id' => (string)$netWorthSet['native']['currency_id'],

View File

@ -23,6 +23,7 @@ declare(strict_types=1);
namespace FireflyIII\Api\V2\Request\Chart; namespace FireflyIII\Api\V2\Request\Chart;
use FireflyIII\Enums\UserRoleEnum;
use FireflyIII\Support\Http\Api\ValidatesUserGroupTrait; use FireflyIII\Support\Http\Api\ValidatesUserGroupTrait;
use FireflyIII\Support\Request\ChecksLogin; use FireflyIII\Support\Request\ChecksLogin;
use FireflyIII\Support\Request\ConvertsDataTypes; use FireflyIII\Support\Request\ConvertsDataTypes;
@ -39,6 +40,8 @@ class DashboardChartRequest extends FormRequest
use ConvertsDataTypes; use ConvertsDataTypes;
use ValidatesUserGroupTrait; use ValidatesUserGroupTrait;
protected array $acceptedRoles = [UserRoleEnum::READ_ONLY];
/** /**
* Get all data from the request. * Get all data from the request.
*/ */

View File

@ -25,6 +25,7 @@ declare(strict_types=1);
namespace FireflyIII\Exceptions; namespace FireflyIII\Exceptions;
use FireflyIII\Jobs\MailError; use FireflyIII\Jobs\MailError;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Auth\AuthenticationException; use Illuminate\Auth\AuthenticationException;
use Illuminate\Database\QueryException; use Illuminate\Database\QueryException;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler; use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
@ -98,6 +99,13 @@ class Handler extends ExceptionHandler
return response()->json(['message' => 'Resource not found', 'exception' => 'NotFoundHttpException'], 404); return response()->json(['message' => 'Resource not found', 'exception' => 'NotFoundHttpException'], 404);
} }
if ($e instanceof AuthorizationException && $expectsJson) {
// somehow Laravel handler does not catch this:
app('log')->debug('Return JSON unauthorized error.');
return response()->json(['message' => $e->getMessage(), 'exception' => 'AuthorizationException'], 401);
}
if ($e instanceof AuthenticationException && $expectsJson) { if ($e instanceof AuthenticationException && $expectsJson) {
// somehow Laravel handler does not catch this: // somehow Laravel handler does not catch this:
app('log')->debug('Return JSON unauthenticated error.'); app('log')->debug('Return JSON unauthenticated error.');

View File

@ -32,6 +32,7 @@ use FireflyIII\Http\Middleware\Installer;
use FireflyIII\Models\AccountType; use FireflyIII\Models\AccountType;
use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\Repositories\Account\AccountRepositoryInterface;
use FireflyIII\Repositories\Bill\BillRepositoryInterface; use FireflyIII\Repositories\Bill\BillRepositoryInterface;
use FireflyIII\Repositories\UserGroups\Account\AccountRepositoryInterface as UserGroupAccountRepositoryInterface;
use FireflyIII\User; use FireflyIII\User;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
@ -61,8 +62,8 @@ class HomeController extends Controller
*/ */
public function dateRange(Request $request): JsonResponse public function dateRange(Request $request): JsonResponse
{ {
$stringStart = ''; $stringStart = '';
$stringEnd = ''; $stringEnd = '';
try { try {
$stringStart = e((string)$request->get('start')); $stringStart = e((string)$request->get('start'));
@ -97,7 +98,7 @@ class HomeController extends Controller
app('log')->debug('Range is now marked as "custom".'); app('log')->debug('Range is now marked as "custom".');
} }
$diff = $start->diffInDays($end, true) + 1; $diff = $start->diffInDays($end, true) + 1;
if ($diff > 366) { if ($diff > 366) {
$request->session()->flash('warning', (string)trans('firefly.warning_much_data', ['days' => (int)$diff])); $request->session()->flash('warning', (string)trans('firefly.warning_much_data', ['days' => (int)$diff]));
@ -120,13 +121,27 @@ class HomeController extends Controller
*/ */
public function index(AccountRepositoryInterface $repository): mixed public function index(AccountRepositoryInterface $repository): mixed
{ {
$types = config('firefly.accountTypesByIdentifier.asset'); $types = config('firefly.accountTypesByIdentifier.asset');
$count = $repository->count($types); $count = $repository->count($types);
Log::channel('audit')->info('User visits homepage.'); Log::channel('audit')->info('User visits homepage.');
if (0 === $count) { if (0 === $count) {
return redirect(route('new-user.index')); return redirect(route('new-user.index'));
} }
if ('v1' === (string)config('view.layout')) {
return $this->indexV1($repository);
}
if ('v2' === (string)config('view.layout')) {
return $this->indexV2();
}
throw new FireflyException('Invalid layout configuration');
}
private function indexV1(AccountRepositoryInterface $repository): mixed
{
$types = config('firefly.accountTypesByIdentifier.asset');
$count = $repository->count($types);
$subTitle = (string)trans('firefly.welcome_back'); $subTitle = (string)trans('firefly.welcome_back');
$transactions = []; $transactions = [];
$frontpage = app('preferences')->getFresh('frontpageAccounts', $repository->getAccountsByType([AccountType::ASSET])->pluck('id')->toArray()); $frontpage = app('preferences')->getFresh('frontpageAccounts', $repository->getAccountsByType([AccountType::ASSET])->pluck('id')->toArray());
@ -136,15 +151,12 @@ class HomeController extends Controller
} }
/** @var Carbon $start */ /** @var Carbon $start */
$start = session('start', today(config('app.timezone'))->startOfMonth());
/** @var Carbon $end */ /** @var Carbon $end */
$end = session('end', today(config('app.timezone'))->endOfMonth()); $start = session('start', today(config('app.timezone'))->startOfMonth());
$accounts = $repository->getAccountsById($frontpageArray); $end = session('end', today(config('app.timezone'))->endOfMonth());
$today = today(config('app.timezone')); $accounts = $repository->getAccountsById($frontpageArray);
$today = today(config('app.timezone'));
// sort frontpage accounts by order $accounts = $accounts->sortBy('order'); // sort frontpage accounts by order
$accounts = $accounts->sortBy('order');
app('log')->debug('Frontpage accounts are ', $frontpageArray); app('log')->debug('Frontpage accounts are ', $frontpageArray);
@ -154,16 +166,30 @@ class HomeController extends Controller
// collect groups for each transaction. // collect groups for each transaction.
foreach ($accounts as $account) { foreach ($accounts as $account) {
/** @var GroupCollectorInterface $collector */ /** @var GroupCollectorInterface $collector */
$collector = app(GroupCollectorInterface::class); $collector = app(GroupCollectorInterface::class);
$collector->setAccounts(new Collection([$account]))->withAccountInformation()->setRange($start, $end)->setLimit(10)->setPage(1); $collector->setAccounts(new Collection([$account]))->withAccountInformation()->setRange($start, $end)->setLimit(10)->setPage(1);
$set = $collector->getExtractedJournals(); $set = $collector->getExtractedJournals();
$transactions[] = ['transactions' => $set, 'account' => $account]; $transactions[] = ['transactions' => $set, 'account' => $account];
} }
/** @var User $user */ /** @var User $user */
$user = auth()->user(); $user = auth()->user();
event(new RequestedVersionCheckStatus($user)); event(new RequestedVersionCheckStatus($user));
return view('index', compact('count', 'subTitle', 'transactions', 'billCount', 'start', 'end', 'today')); return view('index', compact('count', 'subTitle', 'transactions', 'billCount', 'start', 'end', 'today'));
} }
private function indexV2(): mixed
{
$subTitle = (string)trans('firefly.welcome_back');
$start = session('start', today(config('app.timezone'))->startOfMonth());
$end = session('end', today(config('app.timezone'))->endOfMonth());
/** @var User $user */
$user = auth()->user();
event(new RequestedVersionCheckStatus($user));
return view('index', compact( 'subTitle','start','end'));
}
} }

View File

@ -23,11 +23,14 @@ declare(strict_types=1);
namespace FireflyIII\Support\Http\Api; namespace FireflyIII\Support\Http\Api;
use FireflyIII\Exceptions\FireflyException; use FireflyIII\Enums\UserRoleEnum;
use FireflyIII\Models\GroupMembership; use FireflyIII\Models\GroupMembership;
use FireflyIII\Models\UserGroup; use FireflyIII\Models\UserGroup;
use FireflyIII\User; use FireflyIII\User;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Auth\AuthenticationException;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Collection;
/** /**
* Trait ValidatesUserGroupTrait * Trait ValidatesUserGroupTrait
@ -35,37 +38,63 @@ use Illuminate\Http\Request;
trait ValidatesUserGroupTrait trait ValidatesUserGroupTrait
{ {
/** /**
* This check does not validate which rights the user has, that comes later. * @throws AuthorizationException
* * @throws AuthenticationException
* @throws FireflyException
*/ */
protected function validateUserGroup(Request $request): ?UserGroup protected function validateUserGroup(Request $request): UserGroup
{ {
app('log')->debug(sprintf('validateUserGroup: %s', get_class($this)));
if (!auth()->check()) { if (!auth()->check()) {
app('log')->debug('validateUserGroup: user is not logged in, return NULL.'); app('log')->debug('validateUserGroup: user is not logged in, return NULL.');
return null; throw new AuthenticationException();
} }
/** @var User $user */ /** @var User $user */
$user = auth()->user(); $user = auth()->user();
$groupId = 0;
if (!$request->has('user_group_id')) { if (!$request->has('user_group_id')) {
$group = $user->userGroup; $groupId = $user->user_group_id;
app('log')->debug(sprintf('validateUserGroup: no user group submitted, return default group #%d.', $group?->id)); app('log')->debug(sprintf('validateUserGroup: no user group submitted, use default group #%d.', $groupId));
return $group;
} }
$groupId = (int)$request->get('user_group_id'); if ($request->has('user_group_id')) {
$groupId = (int)$request->get('user_group_id');
/** @var null|GroupMembership $membership */ app('log')->debug(sprintf('validateUserGroup: user group submitted, search for memberships in group #%d.', $groupId));
}
/** @var GroupMembership|null $membership */
$membership = $user->groupMemberships()->where('user_group_id', $groupId)->first(); $membership = $user->groupMemberships()->where('user_group_id', $groupId)->first();
if (null === $membership) { if (null === $membership) {
app('log')->debug('validateUserGroup: user has no access to this group.'); app('log')->debug(sprintf('validateUserGroup: user has no access to group #%d.', $groupId));
throw new AuthorizationException((string)trans('validation.no_access_group'));
throw new FireflyException((string)trans('validation.belongs_user_or_user_group'));
} }
app('log')->debug(sprintf('validateUserGroup: user has role "%s" in group #%d.', $membership->userRole->title, $membership->userGroup->id));
return $membership->userGroup; // need to get the group from the membership:
/** @var UserGroup|null $group */
$group = $membership->userGroup;
if (null === $group) {
app('log')->debug(sprintf('validateUserGroup: group #%d does not exist.', $groupId));
throw new AuthorizationException((string)trans('validation.belongs_user_or_user_group'));
}
app('log')->debug(sprintf('validateUserGroup: validate access of user to group #%d ("%s").', $groupId, $group->title));
$roles = property_exists($this, 'acceptedRoles') ? $this->acceptedRoles : [];
if(0 === count($roles)) {
app('log')->debug('validateUserGroup: no roles defined, so no access.');
throw new AuthorizationException((string)trans('validation.no_accepted_roles_defined'));
}
app('log')->debug(sprintf('validateUserGroup: have %d roles to check.', count($roles)), $roles);
/** @var UserRoleEnum $role */
foreach($roles as $role) {
if($user->hasRoleInGroupOrOwner($group, $role)) {
app('log')->debug(sprintf('validateUserGroup: User has role "%s" in group #%d, return the group.', $role->value, $groupId));
return $group;
}
app('log')->debug(sprintf('validateUserGroup: User does NOT have role "%s" in group #%d, continue searching.', $role->value, $groupId));
}
app('log')->debug('validateUserGroup: User does NOT have enough rights to access endpoint.');
throw new AuthorizationException((string)trans('validation.belongs_user_or_user_group'));
} }
} }

View File

@ -39,7 +39,7 @@ return [
| the usual Laravel view path has already been registered for you. | the usual Laravel view path has already been registered for you.
| |
*/ */
'layout' => env('FIREFLY_III_LAYOUT', 'v1'),
'paths' => $paths, 'paths' => $paths,
/* /*

View File

@ -25,156 +25,158 @@
declare(strict_types=1); declare(strict_types=1);
return [ return [
'bad_type_source' => 'Firefly III can\'t determine the transaction type based on this source account.', 'bad_type_source' => 'Firefly III can\'t determine the transaction type based on this source account.',
'bad_type_destination' => 'Firefly III can\'t determine the transaction type based on this destination account.', 'bad_type_destination' => 'Firefly III can\'t determine the transaction type based on this destination account.',
'missing_where' => 'Array is missing "where"-clause', 'missing_where' => 'Array is missing "where"-clause',
'missing_update' => 'Array is missing "update"-clause', 'missing_update' => 'Array is missing "update"-clause',
'invalid_where_key' => 'JSON contains an invalid key for the "where"-clause', 'invalid_where_key' => 'JSON contains an invalid key for the "where"-clause',
'invalid_update_key' => 'JSON contains an invalid key for the "update"-clause', 'invalid_update_key' => 'JSON contains an invalid key for the "update"-clause',
'invalid_query_data' => 'There is invalid data in the %s:%s field of your query.', 'invalid_query_data' => 'There is invalid data in the %s:%s field of your query.',
'invalid_query_account_type' => 'Your query contains accounts of different types, which is not allowed.', 'invalid_query_account_type' => 'Your query contains accounts of different types, which is not allowed.',
'invalid_query_currency' => 'Your query contains accounts that have different currency settings, which is not allowed.', 'invalid_query_currency' => 'Your query contains accounts that have different currency settings, which is not allowed.',
'iban' => 'This is not a valid IBAN.', 'iban' => 'This is not a valid IBAN.',
'zero_or_more' => 'The value cannot be negative.', 'zero_or_more' => 'The value cannot be negative.',
'more_than_zero' => 'The value must be more than zero.', 'more_than_zero' => 'The value must be more than zero.',
'more_than_zero_correct' => 'The value must be zero or more.', 'more_than_zero_correct' => 'The value must be zero or more.',
'no_asset_account' => 'This is not an asset account.', 'no_asset_account' => 'This is not an asset account.',
'date_or_time' => 'The value must be a valid date or time value (ISO 8601).', 'date_or_time' => 'The value must be a valid date or time value (ISO 8601).',
'source_equals_destination' => 'The source account equals the destination account.', 'source_equals_destination' => 'The source account equals the destination account.',
'unique_account_number_for_user' => 'It looks like this account number is already in use.', 'unique_account_number_for_user' => 'It looks like this account number is already in use.',
'unique_iban_for_user' => 'It looks like this IBAN is already in use.', 'unique_iban_for_user' => 'It looks like this IBAN is already in use.',
'reconciled_forbidden_field' => 'This transaction is already reconciled, you cannot change the ":field"', 'reconciled_forbidden_field' => 'This transaction is already reconciled, you cannot change the ":field"',
'deleted_user' => 'Due to security constraints, you cannot register using this email address.', 'deleted_user' => 'Due to security constraints, you cannot register using this email address.',
'rule_trigger_value' => 'This value is invalid for the selected trigger.', 'rule_trigger_value' => 'This value is invalid for the selected trigger.',
'rule_action_expression' => 'Invalid expression. :error', 'rule_action_expression' => 'Invalid expression. :error',
'rule_action_value' => 'This value is invalid for the selected action.', 'rule_action_value' => 'This value is invalid for the selected action.',
'file_already_attached' => 'Uploaded file ":name" is already attached to this object.', 'file_already_attached' => 'Uploaded file ":name" is already attached to this object.',
'file_attached' => 'Successfully uploaded file ":name".', 'file_attached' => 'Successfully uploaded file ":name".',
'file_zero' => 'The file is zero bytes in size.', 'file_zero' => 'The file is zero bytes in size.',
'must_exist' => 'The ID in field :attribute does not exist in the database.', 'must_exist' => 'The ID in field :attribute does not exist in the database.',
'all_accounts_equal' => 'All accounts in this field must be equal.', 'all_accounts_equal' => 'All accounts in this field must be equal.',
'group_title_mandatory' => 'A group title is mandatory when there is more than one transaction.', 'group_title_mandatory' => 'A group title is mandatory when there is more than one transaction.',
'transaction_types_equal' => 'All splits must be of the same type.', 'transaction_types_equal' => 'All splits must be of the same type.',
'invalid_transaction_type' => 'Invalid transaction type.', 'invalid_transaction_type' => 'Invalid transaction type.',
'invalid_selection' => 'Your selection is invalid.', 'invalid_selection' => 'Your selection is invalid.',
'belongs_user' => 'This value is linked to an object that does not seem to exist.', 'belongs_user' => 'This value is linked to an object that does not seem to exist.',
'belongs_user_or_user_group' => 'This value is linked to an object that does not seem to exist in your current financial administration.', 'belongs_user_or_user_group' => 'This value is linked to an object that does not seem to exist in your current financial administration.',
'at_least_one_transaction' => 'Need at least one transaction.', 'no_access_group' => 'The user has no access to this user group.',
'recurring_transaction_id' => 'Need at least one transaction.', 'no_accepted_roles_defined' => 'No access roles have been defined for this endpoint, access denied.',
'need_id_to_match' => 'You need to submit this entry with an ID for the API to be able to match it.', 'at_least_one_transaction' => 'Need at least one transaction.',
'too_many_unmatched' => 'Too many submitted transactions cannot be matched to their respective database entries. Make sure existing entries have a valid ID.', 'recurring_transaction_id' => 'Need at least one transaction.',
'id_does_not_match' => 'Submitted ID #:id does not match expected ID. Make sure it matches or omit the field.', 'need_id_to_match' => 'You need to submit this entry with an ID for the API to be able to match it.',
'at_least_one_repetition' => 'Need at least one repetition.', 'too_many_unmatched' => 'Too many submitted transactions cannot be matched to their respective database entries. Make sure existing entries have a valid ID.',
'require_repeat_until' => 'Require either a number of repetitions, or an end date (repeat_until). Not both.', 'id_does_not_match' => 'Submitted ID #:id does not match expected ID. Make sure it matches or omit the field.',
'require_currency_info' => 'The content of this field is invalid without currency information.', 'at_least_one_repetition' => 'Need at least one repetition.',
'not_transfer_account' => 'This account is not an account that can be used for transfers.', 'require_repeat_until' => 'Require either a number of repetitions, or an end date (repeat_until). Not both.',
'require_currency_amount' => 'The content of this field is invalid without foreign amount information.', 'require_currency_info' => 'The content of this field is invalid without currency information.',
'require_foreign_currency' => 'This field requires a number', 'not_transfer_account' => 'This account is not an account that can be used for transfers.',
'require_foreign_dest' => 'This field value must match the currency of the destination account.', 'require_currency_amount' => 'The content of this field is invalid without foreign amount information.',
'require_foreign_src' => 'This field value must match the currency of the source account.', 'require_foreign_currency' => 'This field requires a number',
'equal_description' => 'Transaction description should not equal global description.', 'require_foreign_dest' => 'This field value must match the currency of the destination account.',
'file_invalid_mime' => 'File ":name" is of type ":mime" which is not accepted as a new upload.', 'require_foreign_src' => 'This field value must match the currency of the source account.',
'file_too_large' => 'File ":name" is too large.', 'equal_description' => 'Transaction description should not equal global description.',
'belongs_to_user' => 'The value of :attribute is unknown.', 'file_invalid_mime' => 'File ":name" is of type ":mime" which is not accepted as a new upload.',
'accepted' => 'The :attribute must be accepted.', 'file_too_large' => 'File ":name" is too large.',
'bic' => 'This is not a valid BIC.', 'belongs_to_user' => 'The value of :attribute is unknown.',
'at_least_one_trigger' => 'Rule must have at least one trigger.', 'accepted' => 'The :attribute must be accepted.',
'at_least_one_active_trigger' => 'Rule must have at least one active trigger.', 'bic' => 'This is not a valid BIC.',
'at_least_one_action' => 'Rule must have at least one action.', 'at_least_one_trigger' => 'Rule must have at least one trigger.',
'at_least_one_active_action' => 'Rule must have at least one active action.', 'at_least_one_active_trigger' => 'Rule must have at least one active trigger.',
'base64' => 'This is not valid base64 encoded data.', 'at_least_one_action' => 'Rule must have at least one action.',
'model_id_invalid' => 'The given ID seems invalid for this model.', 'at_least_one_active_action' => 'Rule must have at least one active action.',
'less' => ':attribute must be less than 10,000,000', 'base64' => 'This is not valid base64 encoded data.',
'active_url' => 'The :attribute is not a valid URL.', 'model_id_invalid' => 'The given ID seems invalid for this model.',
'after' => 'The :attribute must be a date after :date.', 'less' => ':attribute must be less than 10,000,000',
'date_after' => 'The start date must be before the end date.', 'active_url' => 'The :attribute is not a valid URL.',
'alpha' => 'The :attribute may only contain letters.', 'after' => 'The :attribute must be a date after :date.',
'alpha_dash' => 'The :attribute may only contain letters, numbers, and dashes.', 'date_after' => 'The start date must be before the end date.',
'alpha_num' => 'The :attribute may only contain letters and numbers.', 'alpha' => 'The :attribute may only contain letters.',
'array' => 'The :attribute must be an array.', 'alpha_dash' => 'The :attribute may only contain letters, numbers, and dashes.',
'unique_for_user' => 'There already is an entry with this :attribute.', 'alpha_num' => 'The :attribute may only contain letters and numbers.',
'before' => 'The :attribute must be a date before :date.', 'array' => 'The :attribute must be an array.',
'unique_object_for_user' => 'This name is already in use.', 'unique_for_user' => 'There already is an entry with this :attribute.',
'unique_account_for_user' => 'This account name is already in use.', 'before' => 'The :attribute must be a date before :date.',
'unique_object_for_user' => 'This name is already in use.',
'unique_account_for_user' => 'This account name is already in use.',
// Ignore this comment // Ignore this comment
'between.numeric' => 'The :attribute must be between :min and :max.', 'between.numeric' => 'The :attribute must be between :min and :max.',
'between.file' => 'The :attribute must be between :min and :max kilobytes.', 'between.file' => 'The :attribute must be between :min and :max kilobytes.',
'between.string' => 'The :attribute must be between :min and :max characters.', 'between.string' => 'The :attribute must be between :min and :max characters.',
'between.array' => 'The :attribute must have between :min and :max items.', 'between.array' => 'The :attribute must have between :min and :max items.',
'boolean' => 'The :attribute field must be true or false.', 'boolean' => 'The :attribute field must be true or false.',
'confirmed' => 'The :attribute confirmation does not match.', 'confirmed' => 'The :attribute confirmation does not match.',
'date' => 'The :attribute is not a valid date.', 'date' => 'The :attribute is not a valid date.',
'date_format' => 'The :attribute does not match the format :format.', 'date_format' => 'The :attribute does not match the format :format.',
'different' => 'The :attribute and :other must be different.', 'different' => 'The :attribute and :other must be different.',
'digits' => 'The :attribute must be :digits digits.', 'digits' => 'The :attribute must be :digits digits.',
'digits_between' => 'The :attribute must be between :min and :max digits.', 'digits_between' => 'The :attribute must be between :min and :max digits.',
'email' => 'The :attribute must be a valid email address.', 'email' => 'The :attribute must be a valid email address.',
'filled' => 'The :attribute field is required.', 'filled' => 'The :attribute field is required.',
'exists' => 'The selected :attribute is invalid.', 'exists' => 'The selected :attribute is invalid.',
'image' => 'The :attribute must be an image.', 'image' => 'The :attribute must be an image.',
'in' => 'The selected :attribute is invalid.', 'in' => 'The selected :attribute is invalid.',
'integer' => 'The :attribute must be an integer.', 'integer' => 'The :attribute must be an integer.',
'ip' => 'The :attribute must be a valid IP address.', 'ip' => 'The :attribute must be a valid IP address.',
'json' => 'The :attribute must be a valid JSON string.', 'json' => 'The :attribute must be a valid JSON string.',
'max.numeric' => 'The :attribute may not be greater than :max.', 'max.numeric' => 'The :attribute may not be greater than :max.',
'max.file' => 'The :attribute may not be greater than :max kilobytes.', 'max.file' => 'The :attribute may not be greater than :max kilobytes.',
'max.string' => 'The :attribute may not be greater than :max characters.', 'max.string' => 'The :attribute may not be greater than :max characters.',
'max.array' => 'The :attribute may not have more than :max items.', 'max.array' => 'The :attribute may not have more than :max items.',
'mimes' => 'The :attribute must be a file of type: :values.', 'mimes' => 'The :attribute must be a file of type: :values.',
'min.numeric' => 'The :attribute must be at least :min.', 'min.numeric' => 'The :attribute must be at least :min.',
'lte.numeric' => 'The :attribute must be less than or equal :value.', 'lte.numeric' => 'The :attribute must be less than or equal :value.',
'min.file' => 'The :attribute must be at least :min kilobytes.', 'min.file' => 'The :attribute must be at least :min kilobytes.',
'min.string' => 'The :attribute must be at least :min characters.', 'min.string' => 'The :attribute must be at least :min characters.',
'min.array' => 'The :attribute must have at least :min items.', 'min.array' => 'The :attribute must have at least :min items.',
'not_in' => 'The selected :attribute is invalid.', 'not_in' => 'The selected :attribute is invalid.',
'numeric' => 'The :attribute must be a number.', 'numeric' => 'The :attribute must be a number.',
'scientific_notation' => 'The :attribute cannot use the scientific notation.', 'scientific_notation' => 'The :attribute cannot use the scientific notation.',
'numeric_native' => 'The native amount must be a number.', 'numeric_native' => 'The native amount must be a number.',
'numeric_destination' => 'The destination amount must be a number.', 'numeric_destination' => 'The destination amount must be a number.',
'numeric_source' => 'The source amount must be a number.', 'numeric_source' => 'The source amount must be a number.',
'regex' => 'The :attribute format is invalid.', 'regex' => 'The :attribute format is invalid.',
'required' => 'The :attribute field is required.', 'required' => 'The :attribute field is required.',
'required_if' => 'The :attribute field is required when :other is :value.', 'required_if' => 'The :attribute field is required when :other is :value.',
'required_unless' => 'The :attribute field is required unless :other is in :values.', 'required_unless' => 'The :attribute field is required unless :other is in :values.',
'required_with' => 'The :attribute field is required when :values is present.', 'required_with' => 'The :attribute field is required when :values is present.',
'required_with_all' => 'The :attribute field is required when :values is present.', 'required_with_all' => 'The :attribute field is required when :values is present.',
'required_without' => 'The :attribute field is required when :values is not present.', 'required_without' => 'The :attribute field is required when :values is not present.',
'required_without_all' => 'The :attribute field is required when none of :values are present.', 'required_without_all' => 'The :attribute field is required when none of :values are present.',
'same' => 'The :attribute and :other must match.', 'same' => 'The :attribute and :other must match.',
'size.numeric' => 'The :attribute must be :size.', 'size.numeric' => 'The :attribute must be :size.',
'amount_min_over_max' => 'The minimum amount cannot be larger than the maximum amount.', 'amount_min_over_max' => 'The minimum amount cannot be larger than the maximum amount.',
'size.file' => 'The :attribute must be :size kilobytes.', 'size.file' => 'The :attribute must be :size kilobytes.',
'size.string' => 'The :attribute must be :size characters.', 'size.string' => 'The :attribute must be :size characters.',
'size.array' => 'The :attribute must contain :size items.', 'size.array' => 'The :attribute must contain :size items.',
'unique' => 'The :attribute has already been taken.', 'unique' => 'The :attribute has already been taken.',
'string' => 'The :attribute must be a string.', 'string' => 'The :attribute must be a string.',
'url' => 'The :attribute format is invalid.', 'url' => 'The :attribute format is invalid.',
'timezone' => 'The :attribute must be a valid zone.', 'timezone' => 'The :attribute must be a valid zone.',
'2fa_code' => 'The :attribute field is invalid.', '2fa_code' => 'The :attribute field is invalid.',
'dimensions' => 'The :attribute has invalid image dimensions.', 'dimensions' => 'The :attribute has invalid image dimensions.',
'distinct' => 'The :attribute field has a duplicate value.', 'distinct' => 'The :attribute field has a duplicate value.',
'file' => 'The :attribute must be a file.', 'file' => 'The :attribute must be a file.',
'in_array' => 'The :attribute field does not exist in :other.', 'in_array' => 'The :attribute field does not exist in :other.',
'present' => 'The :attribute field must be present.', 'present' => 'The :attribute field must be present.',
'amount_zero' => 'The total amount cannot be zero.', 'amount_zero' => 'The total amount cannot be zero.',
'current_target_amount' => 'The current amount must be less than the target amount.', 'current_target_amount' => 'The current amount must be less than the target amount.',
'unique_piggy_bank_for_user' => 'The name of the piggy bank must be unique.', 'unique_piggy_bank_for_user' => 'The name of the piggy bank must be unique.',
'unique_object_group' => 'The group name must be unique', 'unique_object_group' => 'The group name must be unique',
'starts_with' => 'The value must start with :values.', 'starts_with' => 'The value must start with :values.',
'unique_webhook' => 'You already have a webhook with this combination of URL, trigger, response and delivery.', 'unique_webhook' => 'You already have a webhook with this combination of URL, trigger, response and delivery.',
'unique_existing_webhook' => 'You already have another webhook with this combination of URL, trigger, response and delivery.', 'unique_existing_webhook' => 'You already have another webhook with this combination of URL, trigger, response and delivery.',
'same_account_type' => 'Both accounts must be of the same account type', 'same_account_type' => 'Both accounts must be of the same account type',
'same_account_currency' => 'Both accounts must have the same currency setting', 'same_account_currency' => 'Both accounts must have the same currency setting',
// Ignore this comment // Ignore this comment
'secure_password' => 'This is not a secure password. Please try again. For more information, visit https://bit.ly/FF3-password', 'secure_password' => 'This is not a secure password. Please try again. For more information, visit https://bit.ly/FF3-password',
'valid_recurrence_rep_type' => 'Invalid repetition type for recurring transactions.', 'valid_recurrence_rep_type' => 'Invalid repetition type for recurring transactions.',
'valid_recurrence_rep_moment' => 'Invalid repetition moment for this type of repetition.', 'valid_recurrence_rep_moment' => 'Invalid repetition moment for this type of repetition.',
'invalid_account_info' => 'Invalid account information.', 'invalid_account_info' => 'Invalid account information.',
'attributes' => [ 'attributes' => [
'email' => 'email address', 'email' => 'email address',
'description' => 'description', 'description' => 'description',
'amount' => 'amount', 'amount' => 'amount',
@ -213,58 +215,58 @@ return [
], ],
// validation of accounts: // validation of accounts:
'withdrawal_source_need_data' => 'Need to get a valid source account ID and/or valid source account name to continue.', 'withdrawal_source_need_data' => 'Need to get a valid source account ID and/or valid source account name to continue.',
'withdrawal_source_bad_data' => '[a] Could not find a valid source account when searching for ID ":id" or name ":name".', 'withdrawal_source_bad_data' => '[a] Could not find a valid source account when searching for ID ":id" or name ":name".',
'withdrawal_dest_need_data' => '[a] Need to get a valid destination account ID and/or valid destination account name to continue.', 'withdrawal_dest_need_data' => '[a] Need to get a valid destination account ID and/or valid destination account name to continue.',
'withdrawal_dest_bad_data' => 'Could not find a valid destination account when searching for ID ":id" or name ":name".', 'withdrawal_dest_bad_data' => 'Could not find a valid destination account when searching for ID ":id" or name ":name".',
'withdrawal_dest_iban_exists' => 'This destination account IBAN is already in use by an asset account or a liability and cannot be used as a withdrawal destination.', 'withdrawal_dest_iban_exists' => 'This destination account IBAN is already in use by an asset account or a liability and cannot be used as a withdrawal destination.',
'deposit_src_iban_exists' => 'This source account IBAN is already in use by an asset account or a liability and cannot be used as a deposit source.', 'deposit_src_iban_exists' => 'This source account IBAN is already in use by an asset account or a liability and cannot be used as a deposit source.',
'reconciliation_source_bad_data' => 'Could not find a valid reconciliation account when searching for ID ":id" or name ":name".', 'reconciliation_source_bad_data' => 'Could not find a valid reconciliation account when searching for ID ":id" or name ":name".',
'generic_source_bad_data' => '[e] Could not find a valid source account when searching for ID ":id" or name ":name".', 'generic_source_bad_data' => '[e] Could not find a valid source account when searching for ID ":id" or name ":name".',
'deposit_source_need_data' => 'Need to get a valid source account ID and/or valid source account name to continue.', 'deposit_source_need_data' => 'Need to get a valid source account ID and/or valid source account name to continue.',
'deposit_source_bad_data' => '[b] Could not find a valid source account when searching for ID ":id" or name ":name".', 'deposit_source_bad_data' => '[b] Could not find a valid source account when searching for ID ":id" or name ":name".',
'deposit_dest_need_data' => '[b] Need to get a valid destination account ID and/or valid destination account name to continue.', 'deposit_dest_need_data' => '[b] Need to get a valid destination account ID and/or valid destination account name to continue.',
'deposit_dest_bad_data' => 'Could not find a valid destination account when searching for ID ":id" or name ":name".', 'deposit_dest_bad_data' => 'Could not find a valid destination account when searching for ID ":id" or name ":name".',
'deposit_dest_wrong_type' => 'The submitted destination account is not of the right type.', 'deposit_dest_wrong_type' => 'The submitted destination account is not of the right type.',
// Ignore this comment // Ignore this comment
'transfer_source_need_data' => 'Need to get a valid source account ID and/or valid source account name to continue.', 'transfer_source_need_data' => 'Need to get a valid source account ID and/or valid source account name to continue.',
'transfer_source_bad_data' => '[c] Could not find a valid source account when searching for ID ":id" or name ":name".', 'transfer_source_bad_data' => '[c] Could not find a valid source account when searching for ID ":id" or name ":name".',
'transfer_dest_need_data' => '[c] Need to get a valid destination account ID and/or valid destination account name to continue.', 'transfer_dest_need_data' => '[c] Need to get a valid destination account ID and/or valid destination account name to continue.',
'transfer_dest_bad_data' => 'Could not find a valid destination account when searching for ID ":id" or name ":name".', 'transfer_dest_bad_data' => 'Could not find a valid destination account when searching for ID ":id" or name ":name".',
'need_id_in_edit' => 'Each split must have transaction_journal_id (either valid ID or 0).', 'need_id_in_edit' => 'Each split must have transaction_journal_id (either valid ID or 0).',
'ob_source_need_data' => 'Need to get a valid source account ID and/or valid source account name to continue.', 'ob_source_need_data' => 'Need to get a valid source account ID and/or valid source account name to continue.',
'lc_source_need_data' => 'Need to get a valid source account ID to continue.', 'lc_source_need_data' => 'Need to get a valid source account ID to continue.',
'ob_dest_need_data' => '[d] Need to get a valid destination account ID and/or valid destination account name to continue.', 'ob_dest_need_data' => '[d] Need to get a valid destination account ID and/or valid destination account name to continue.',
'ob_dest_bad_data' => 'Could not find a valid destination account when searching for ID ":id" or name ":name".', 'ob_dest_bad_data' => 'Could not find a valid destination account when searching for ID ":id" or name ":name".',
'reconciliation_either_account' => 'To submit a reconciliation, you must submit either a source or a destination account. Not both, not neither.', 'reconciliation_either_account' => 'To submit a reconciliation, you must submit either a source or a destination account. Not both, not neither.',
'generic_invalid_source' => 'You can\'t use this account as the source account.', 'generic_invalid_source' => 'You can\'t use this account as the source account.',
'generic_invalid_destination' => 'You can\'t use this account as the destination account.', 'generic_invalid_destination' => 'You can\'t use this account as the destination account.',
'generic_no_source' => 'You must submit source account information or submit a transaction journal ID.', 'generic_no_source' => 'You must submit source account information or submit a transaction journal ID.',
'generic_no_destination' => 'You must submit destination account information or submit a transaction journal ID.', 'generic_no_destination' => 'You must submit destination account information or submit a transaction journal ID.',
'gte.numeric' => 'The :attribute must be greater than or equal to :value.', 'gte.numeric' => 'The :attribute must be greater than or equal to :value.',
'gt.numeric' => 'The :attribute must be greater than :value.', 'gt.numeric' => 'The :attribute must be greater than :value.',
'gte.file' => 'The :attribute must be greater than or equal to :value kilobytes.', 'gte.file' => 'The :attribute must be greater than or equal to :value kilobytes.',
'gte.string' => 'The :attribute must be greater than or equal to :value characters.', 'gte.string' => 'The :attribute must be greater than or equal to :value characters.',
'gte.array' => 'The :attribute must have :value items or more.', 'gte.array' => 'The :attribute must have :value items or more.',
'amount_required_for_auto_budget' => 'The amount is required.', 'amount_required_for_auto_budget' => 'The amount is required.',
'auto_budget_amount_positive' => 'The amount must be more than zero.', 'auto_budget_amount_positive' => 'The amount must be more than zero.',
'auto_budget_period_mandatory' => 'The auto budget period is a mandatory field.', 'auto_budget_period_mandatory' => 'The auto budget period is a mandatory field.',
// no access to administration: // no access to administration:
'no_access_user_group' => 'You do not have the correct access rights for this administration.', 'no_access_user_group' => 'You do not have the correct access rights for this administration.',
'administration_owner_rename' => 'You can\'t rename your standard administration.', 'administration_owner_rename' => 'You can\'t rename your standard administration.',
]; ];
// Ignore this comment // Ignore this comment

View File

@ -0,0 +1 @@
This feature is only available in the v2 layout.