diff --git a/app/Api/V2/Controllers/Chart/AccountController.php b/app/Api/V2/Controllers/Chart/AccountController.php index 40f3906b9d..0ef9b6f797 100644 --- a/app/Api/V2/Controllers/Chart/AccountController.php +++ b/app/Api/V2/Controllers/Chart/AccountController.php @@ -27,6 +27,7 @@ namespace FireflyIII\Api\V2\Controllers\Chart; use Carbon\Carbon; use FireflyIII\Api\V2\Controllers\Controller; use FireflyIII\Api\V2\Request\Chart\DashboardChartRequest; +use FireflyIII\Enums\UserRoleEnum; use FireflyIII\Exceptions\FireflyException; use FireflyIII\Models\Account; use FireflyIII\Models\AccountType; @@ -46,6 +47,7 @@ class AccountController extends Controller use ValidatesUserGroupTrait; private AccountRepositoryInterface $repository; + protected array $acceptedRoles = [UserRoleEnum::READ_ONLY]; public function __construct() { @@ -54,9 +56,7 @@ class AccountController extends Controller function ($request, $next) { $this->repository = app(AccountRepositoryInterface::class); $userGroup = $this->validateUserGroup($request); - if (null !== $userGroup) { - $this->repository->setUserGroup($userGroup); - } + $this->repository->setUserGroup($userGroup); return $next($request); } @@ -81,15 +81,15 @@ class AccountController extends Controller public function dashboard(DashboardChartRequest $request): JsonResponse { /** @var Carbon $start */ - $start = $this->parameters->get('start'); + $start = $this->parameters->get('start'); /** @var Carbon $end */ - $end = $this->parameters->get('end'); + $end = $this->parameters->get('end'); $end->endOfDay(); /** @var TransactionCurrency $default */ - $default = app('amount')->getDefaultCurrency(); - $params = $request->getAll(); + $default = app('amount')->getDefaultCurrency(); + $params = $request->getAll(); /** @var Collection $accounts */ $accounts = $params['accounts']; @@ -105,7 +105,7 @@ class AccountController extends Controller $frontpage->save(); } - $accounts = $this->repository->getAccountsById($frontpage->data); + $accounts = $this->repository->getAccountsById($frontpage->data); } // both options are overruled by "preselected" @@ -121,11 +121,11 @@ class AccountController extends Controller /** @var Account $account */ foreach ($accounts as $account) { - $currency = $this->repository->getAccountCurrency($account); + $currency = $this->repository->getAccountCurrency($account); if (null === $currency) { $currency = $default; } - $currentSet = [ + $currentSet = [ 'label' => $account->name, // the currency that belongs to the account. 'currency_id' => (string)$currency->id, @@ -144,25 +144,25 @@ class AccountController extends Controller 'entries' => [], 'native_entries' => [], ]; - $currentStart = clone $start; - $range = app('steam')->balanceInRange($account, $start, clone $end, $currency); - $rangeConverted = app('steam')->balanceInRangeConverted($account, $start, clone $end, $default); + $currentStart = clone $start; + $range = app('steam')->balanceInRange($account, $start, clone $end, $currency); + $rangeConverted = app('steam')->balanceInRangeConverted($account, $start, clone $end, $default); $previous = array_values($range)[0]; $previousConverted = array_values($rangeConverted)[0]; while ($currentStart <= $end) { - $format = $currentStart->format('Y-m-d'); - $label = $currentStart->toAtomString(); - $balance = array_key_exists($format, $range) ? $range[$format] : $previous; - $balanceConverted = array_key_exists($format, $rangeConverted) ? $rangeConverted[$format] : $previousConverted; - $previous = $balance; - $previousConverted = $balanceConverted; + $format = $currentStart->format('Y-m-d'); + $label = $currentStart->toAtomString(); + $balance = array_key_exists($format, $range) ? $range[$format] : $previous; + $balanceConverted = array_key_exists($format, $rangeConverted) ? $rangeConverted[$format] : $previousConverted; + $previous = $balance; + $previousConverted = $balanceConverted; $currentStart->addDay(); $currentSet['entries'][$label] = $balance; $currentSet['native_entries'][$label] = $balanceConverted; } - $chartData[] = $currentSet; + $chartData[] = $currentSet; } return response()->json($this->clean($chartData)); diff --git a/app/Api/V2/Controllers/Controller.php b/app/Api/V2/Controllers/Controller.php index 4029d9b21b..2624f90aba 100644 --- a/app/Api/V2/Controllers/Controller.php +++ b/app/Api/V2/Controllers/Controller.php @@ -27,6 +27,7 @@ namespace FireflyIII\Api\V2\Controllers; use Carbon\Carbon; use Carbon\Exceptions\InvalidDateException; use Carbon\Exceptions\InvalidFormatException; +use FireflyIII\Enums\UserRoleEnum; use FireflyIII\Support\Http\Api\ValidatesUserGroupTrait; use FireflyIII\Transformers\V2\AbstractTransformer; use Illuminate\Database\Eloquent\Model; @@ -55,6 +56,7 @@ class Controller extends BaseController protected const string CONTENT_TYPE = 'application/vnd.api+json'; protected ParameterBag $parameters; + protected array $acceptedRoles = [UserRoleEnum::READ_ONLY]; public function __construct() { diff --git a/app/Api/V2/Controllers/Model/Account/IndexController.php b/app/Api/V2/Controllers/Model/Account/IndexController.php index 2fba3ff1de..0d04cac3ab 100644 --- a/app/Api/V2/Controllers/Model/Account/IndexController.php +++ b/app/Api/V2/Controllers/Model/Account/IndexController.php @@ -26,6 +26,7 @@ namespace FireflyIII\Api\V2\Controllers\Model\Account; use FireflyIII\Api\V2\Controllers\Controller; use FireflyIII\Api\V2\Request\Model\Account\IndexRequest; use FireflyIII\Api\V2\Request\Model\Transaction\InfiniteListRequest; +use FireflyIII\Enums\UserRoleEnum; use FireflyIII\Repositories\UserGroups\Account\AccountRepositoryInterface; use FireflyIII\Transformers\V2\AccountTransformer; use Illuminate\Http\JsonResponse; @@ -36,6 +37,7 @@ class IndexController extends Controller public const string RESOURCE_KEY = 'accounts'; private AccountRepositoryInterface $repository; + protected array $acceptedRoles = [UserRoleEnum::READ_ONLY, UserRoleEnum::MANAGE_TRANSACTIONS]; /** * AccountController constructor. @@ -47,10 +49,8 @@ class IndexController extends Controller function ($request, $next) { $this->repository = app(AccountRepositoryInterface::class); // new way of user group validation - $userGroup = $this->validateUserGroup($request); - if (null !== $userGroup) { - $this->repository->setUserGroup($userGroup); - } + $userGroup = $this->validateUserGroup($request); + $this->repository->setUserGroup($userGroup); return $next($request); } @@ -77,8 +77,7 @@ class IndexController extends Controller return response() ->json($this->jsonApiList('accounts', $paginator, $transformer)) - ->header('Content-Type', self::CONTENT_TYPE) - ; + ->header('Content-Type', self::CONTENT_TYPE); } public function infiniteList(InfiniteListRequest $request): JsonResponse @@ -86,7 +85,7 @@ class IndexController extends Controller $this->repository->resetAccountOrder(); // get accounts of the specified type, and return. - $types = $request->getAccountTypes(); + $types = $request->getAccountTypes(); // get from repository $accounts = $this->repository->getAccountsInOrder($types, $request->getSortInstructions('accounts'), $request->getStartRow(), $request->getEndRow()); @@ -98,7 +97,6 @@ class IndexController extends Controller return response() ->json($this->jsonApiList(self::RESOURCE_KEY, $paginator, $transformer)) - ->header('Content-Type', self::CONTENT_TYPE) - ; + ->header('Content-Type', self::CONTENT_TYPE); } } diff --git a/app/Api/V2/Controllers/Model/Account/ShowController.php b/app/Api/V2/Controllers/Model/Account/ShowController.php index e6c43c7cf1..6027aceea6 100644 --- a/app/Api/V2/Controllers/Model/Account/ShowController.php +++ b/app/Api/V2/Controllers/Model/Account/ShowController.php @@ -25,7 +25,9 @@ declare(strict_types=1); namespace FireflyIII\Api\V2\Controllers\Model\Account; use FireflyIII\Api\V2\Controllers\Controller; +use FireflyIII\Enums\UserRoleEnum; use FireflyIII\Models\Account; +use FireflyIII\Repositories\UserGroups\Account\AccountRepositoryInterface; use FireflyIII\Transformers\V2\AccountTransformer; use Illuminate\Http\JsonResponse; @@ -36,6 +38,28 @@ use Illuminate\Http\JsonResponse; */ 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. */ diff --git a/app/Api/V2/Controllers/Summary/BasicController.php b/app/Api/V2/Controllers/Summary/BasicController.php index 4126913cdf..9f99e64e28 100644 --- a/app/Api/V2/Controllers/Summary/BasicController.php +++ b/app/Api/V2/Controllers/Summary/BasicController.php @@ -76,14 +76,12 @@ class BasicController extends Controller $this->currencyRepos = app(CurrencyRepositoryInterface::class); $this->opsRepository = app(OperationsRepositoryInterface::class); - $userGroup = $this->validateUserGroup($request); - if (null !== $userGroup) { - $this->abRepository->setUserGroup($userGroup); - $this->accountRepository->setUserGroup($userGroup); - $this->billRepository->setUserGroup($userGroup); - $this->budgetRepository->setUserGroup($userGroup); - $this->opsRepository->setUserGroup($userGroup); - } + $userGroup = $this->validateUserGroup($request); + $this->abRepository->setUserGroup($userGroup); + $this->accountRepository->setUserGroup($userGroup); + $this->billRepository->setUserGroup($userGroup); + $this->budgetRepository->setUserGroup($userGroup); + $this->opsRepository->setUserGroup($userGroup); return $next($request); } @@ -101,8 +99,8 @@ class BasicController extends Controller public function basic(DateRequest $request): JsonResponse { // parameters for boxes: - $start = $this->parameters->get('start'); - $end = $this->parameters->get('end'); + $start = $this->parameters->get('start'); + $end = $this->parameters->get('end'); // balance information: $balanceData = $this->getBalanceInformation($start, $end); @@ -119,13 +117,13 @@ class BasicController extends Controller */ private function getBalanceInformation(Carbon $start, Carbon $end): array { - $object = new SummaryBalanceGrouped(); - $default = app('amount')->getDefaultCurrency(); + $object = new SummaryBalanceGrouped(); + $default = app('amount')->getDefaultCurrency(); $object->setDefault($default); /** @var User $user */ - $user = auth()->user(); + $user = auth()->user(); // collect income of user using the new group collector. /** @var GroupCollectorInterface $collector */ @@ -137,10 +135,9 @@ class BasicController extends Controller ->setPage($this->parameters->get('page')) // set types of transactions to return. ->setTypes([TransactionType::DEPOSIT]) - ->setRange($start, $end) - ; + ->setRange($start, $end); - $set = $collector->getExtractedJournals(); + $set = $collector->getExtractedJournals(); $object->groupTransactions('income', $set); // collect expenses of user using the new group collector. @@ -153,9 +150,8 @@ class BasicController extends Controller ->setPage($this->parameters->get('page')) // set types of transactions to return. ->setTypes([TransactionType::WITHDRAWAL]) - ->setRange($start, $end) - ; - $set = $collector->getExtractedJournals(); + ->setRange($start, $end); + $set = $collector->getExtractedJournals(); $object->groupTransactions('expense', $set); return $object->groupData(); @@ -170,7 +166,7 @@ class BasicController extends Controller $paidAmount = $this->billRepository->sumPaidInRange($start, $end); $unpaidAmount = $this->billRepository->sumUnpaidInRange($start, $end); - $return = []; + $return = []; /** * @var array $info @@ -230,14 +226,14 @@ class BasicController extends Controller { Log::debug(sprintf('Created new ExchangeRateConverter in %s', __METHOD__)); app('log')->debug('Now in getLeftToSpendInfo'); - $return = []; - $today = today(config('app.timezone')); - $available = $this->abRepository->getAvailableBudgetWithCurrency($start, $end); - $budgets = $this->budgetRepository->getActiveBudgets(); - $spent = $this->opsRepository->listExpenses($start, $end, null, $budgets); - $default = app('amount')->getDefaultCurrency(); - $currencies = []; - $converter = new ExchangeRateConverter(); + $return = []; + $today = today(config('app.timezone')); + $available = $this->abRepository->getAvailableBudgetWithCurrency($start, $end); + $budgets = $this->budgetRepository->getActiveBudgets(); + $spent = $this->opsRepository->listExpenses($start, $end, null, $budgets); + $default = app('amount')->getDefaultCurrency(); + $currencies = []; + $converter = new ExchangeRateConverter(); // native info: $nativeLeft = [ @@ -263,8 +259,8 @@ class BasicController extends Controller */ foreach ($spent as $currencyId => $row) { app('log')->debug(sprintf('Processing spent array in currency #%d', $currencyId)); - $spent = '0'; - $spentNative = '0'; + $spent = '0'; + $spentNative = '0'; // get the sum from the array of transactions (double loop but who cares) /** @var array $budget */ @@ -281,8 +277,8 @@ class BasicController extends Controller if ((int)$journal['foreign_currency_id'] === $default->id) { $amountNative = $journal['foreign_amount']; } - $spent = bcadd($spent, $amount); - $spentNative = bcadd($spentNative, $amountNative); + $spent = bcadd($spent, $amount); + $spentNative = bcadd($spentNative, $amountNative); } 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)); // how much left per day? - $days = (int)$today->diffInDays($end, true) + 1; - $perDay = '0'; - $perDayNative = '0'; + $days = (int)$today->diffInDays($end, true) + 1; + $perDay = '0'; + $perDayNative = '0'; if (0 !== $days && bccomp($left, '0') > -1) { $perDay = bcdiv($left, (string)$days); } @@ -309,7 +305,7 @@ class BasicController extends Controller } // left - $return[] = [ + $return[] = [ 'key' => sprintf('left-to-spend-in-%s', $row['currency_code']), 'value' => $left, 'currency_id' => (string)$row['currency_id'], @@ -318,10 +314,10 @@ class BasicController extends Controller 'currency_decimal_places' => (int)$row['currency_decimal_places'], ]; // left (native) - $nativeLeft['value'] = $leftNative; + $nativeLeft['value'] = $leftNative; // left per day: - $return[] = [ + $return[] = [ 'key' => sprintf('left-per-day-to-spend-in-%s', $row['currency_code']), 'value' => $perDay, 'currency_id' => (string)$row['currency_id'], @@ -331,10 +327,10 @@ class BasicController extends Controller ]; // left per day (native) - $nativePerDay['value'] = $perDayNative; + $nativePerDay['value'] = $perDayNative; } - $return[] = $nativeLeft; - $return[] = $nativePerDay; + $return[] = $nativeLeft; + $return[] = $nativePerDay; $converter->summarize(); return $return; @@ -343,8 +339,8 @@ class BasicController extends Controller private function getNetWorthInfo(Carbon $start, Carbon $end): array { /** @var UserGroup $userGroup */ - $userGroup = auth()->user()->userGroup; - $date = today(config('app.timezone'))->startOfDay(); + $userGroup = auth()->user()->userGroup; + $date = today(config('app.timezone'))->startOfDay(); // start and end in the future? use $end if ($this->notInDateRange($date, $start, $end)) { /** @var Carbon $date */ @@ -354,12 +350,12 @@ class BasicController extends Controller /** @var NetWorthInterface $netWorthHelper */ $netWorthHelper = app(NetWorthInterface::class); $netWorthHelper->setUserGroup($userGroup); - $allAccounts = $this->accountRepository->getActiveAccountsByType( + $allAccounts = $this->accountRepository->getActiveAccountsByType( [AccountType::ASSET, AccountType::DEFAULT, AccountType::LOAN, AccountType::MORTGAGE, AccountType::DEBT] ); // filter list on preference of being included. - $filtered = $allAccounts->filter( + $filtered = $allAccounts->filter( function (Account $account) { $includeNetWorth = $this->accountRepository->getMetaValue($account, 'include_net_worth'); @@ -367,10 +363,10 @@ class BasicController extends Controller } ); - $netWorthSet = $netWorthHelper->byAccounts($filtered, $date); - $return = []; + $netWorthSet = $netWorthHelper->byAccounts($filtered, $date); + $return = []; // in native amount - $return[] = [ + $return[] = [ 'key' => 'net-worth-in-native', 'value' => $netWorthSet['native']['balance'], 'currency_id' => (string)$netWorthSet['native']['currency_id'], diff --git a/app/Api/V2/Request/Chart/DashboardChartRequest.php b/app/Api/V2/Request/Chart/DashboardChartRequest.php index 4b708a161d..e9d12b8d62 100644 --- a/app/Api/V2/Request/Chart/DashboardChartRequest.php +++ b/app/Api/V2/Request/Chart/DashboardChartRequest.php @@ -23,6 +23,7 @@ declare(strict_types=1); namespace FireflyIII\Api\V2\Request\Chart; +use FireflyIII\Enums\UserRoleEnum; use FireflyIII\Support\Http\Api\ValidatesUserGroupTrait; use FireflyIII\Support\Request\ChecksLogin; use FireflyIII\Support\Request\ConvertsDataTypes; @@ -39,6 +40,8 @@ class DashboardChartRequest extends FormRequest use ConvertsDataTypes; use ValidatesUserGroupTrait; + protected array $acceptedRoles = [UserRoleEnum::READ_ONLY]; + /** * Get all data from the request. */ diff --git a/app/Exceptions/Handler.php b/app/Exceptions/Handler.php index f43d151f90..e49cc180fc 100644 --- a/app/Exceptions/Handler.php +++ b/app/Exceptions/Handler.php @@ -25,6 +25,7 @@ declare(strict_types=1); namespace FireflyIII\Exceptions; use FireflyIII\Jobs\MailError; +use Illuminate\Auth\Access\AuthorizationException; use Illuminate\Auth\AuthenticationException; use Illuminate\Database\QueryException; 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); } + 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) { // somehow Laravel handler does not catch this: app('log')->debug('Return JSON unauthenticated error.'); diff --git a/app/Http/Controllers/HomeController.php b/app/Http/Controllers/HomeController.php index fb8cebdd40..34f21c8053 100644 --- a/app/Http/Controllers/HomeController.php +++ b/app/Http/Controllers/HomeController.php @@ -32,6 +32,7 @@ use FireflyIII\Http\Middleware\Installer; use FireflyIII\Models\AccountType; use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\Repositories\Bill\BillRepositoryInterface; +use FireflyIII\Repositories\UserGroups\Account\AccountRepositoryInterface as UserGroupAccountRepositoryInterface; use FireflyIII\User; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; @@ -61,8 +62,8 @@ class HomeController extends Controller */ public function dateRange(Request $request): JsonResponse { - $stringStart = ''; - $stringEnd = ''; + $stringStart = ''; + $stringEnd = ''; try { $stringStart = e((string)$request->get('start')); @@ -97,7 +98,7 @@ class HomeController extends Controller app('log')->debug('Range is now marked as "custom".'); } - $diff = $start->diffInDays($end, true) + 1; + $diff = $start->diffInDays($end, true) + 1; if ($diff > 366) { $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 { - $types = config('firefly.accountTypesByIdentifier.asset'); - $count = $repository->count($types); + $types = config('firefly.accountTypesByIdentifier.asset'); + $count = $repository->count($types); Log::channel('audit')->info('User visits homepage.'); if (0 === $count) { 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'); $transactions = []; $frontpage = app('preferences')->getFresh('frontpageAccounts', $repository->getAccountsByType([AccountType::ASSET])->pluck('id')->toArray()); @@ -136,15 +151,12 @@ class HomeController extends Controller } /** @var Carbon $start */ - $start = session('start', today(config('app.timezone'))->startOfMonth()); - /** @var Carbon $end */ - $end = session('end', today(config('app.timezone'))->endOfMonth()); - $accounts = $repository->getAccountsById($frontpageArray); - $today = today(config('app.timezone')); - - // sort frontpage accounts by order - $accounts = $accounts->sortBy('order'); + $start = session('start', today(config('app.timezone'))->startOfMonth()); + $end = session('end', today(config('app.timezone'))->endOfMonth()); + $accounts = $repository->getAccountsById($frontpageArray); + $today = today(config('app.timezone')); + $accounts = $accounts->sortBy('order'); // sort frontpage accounts by order app('log')->debug('Frontpage accounts are ', $frontpageArray); @@ -154,16 +166,30 @@ class HomeController extends Controller // collect groups for each transaction. foreach ($accounts as $account) { /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->setAccounts(new Collection([$account]))->withAccountInformation()->setRange($start, $end)->setLimit(10)->setPage(1); $set = $collector->getExtractedJournals(); $transactions[] = ['transactions' => $set, 'account' => $account]; } /** @var User $user */ - $user = auth()->user(); + $user = auth()->user(); event(new RequestedVersionCheckStatus($user)); 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')); + } } diff --git a/app/Support/Http/Api/ValidatesUserGroupTrait.php b/app/Support/Http/Api/ValidatesUserGroupTrait.php index 5a698ce75c..8f83f035cf 100644 --- a/app/Support/Http/Api/ValidatesUserGroupTrait.php +++ b/app/Support/Http/Api/ValidatesUserGroupTrait.php @@ -23,11 +23,14 @@ declare(strict_types=1); namespace FireflyIII\Support\Http\Api; -use FireflyIII\Exceptions\FireflyException; +use FireflyIII\Enums\UserRoleEnum; use FireflyIII\Models\GroupMembership; use FireflyIII\Models\UserGroup; use FireflyIII\User; +use Illuminate\Auth\Access\AuthorizationException; +use Illuminate\Auth\AuthenticationException; use Illuminate\Http\Request; +use Illuminate\Support\Collection; /** * Trait ValidatesUserGroupTrait @@ -35,37 +38,63 @@ use Illuminate\Http\Request; trait ValidatesUserGroupTrait { /** - * This check does not validate which rights the user has, that comes later. - * - * @throws FireflyException + * @throws AuthorizationException + * @throws AuthenticationException */ - protected function validateUserGroup(Request $request): ?UserGroup + protected function validateUserGroup(Request $request): UserGroup { + app('log')->debug(sprintf('validateUserGroup: %s', get_class($this))); if (!auth()->check()) { app('log')->debug('validateUserGroup: user is not logged in, return NULL.'); - return null; + throw new AuthenticationException(); } /** @var User $user */ - $user = auth()->user(); + $user = auth()->user(); + $groupId = 0; if (!$request->has('user_group_id')) { - $group = $user->userGroup; - app('log')->debug(sprintf('validateUserGroup: no user group submitted, return default group #%d.', $group?->id)); - - return $group; + $groupId = $user->user_group_id; + app('log')->debug(sprintf('validateUserGroup: no user group submitted, use default group #%d.', $groupId)); } - $groupId = (int)$request->get('user_group_id'); - - /** @var null|GroupMembership $membership */ + if ($request->has('user_group_id')) { + $groupId = (int)$request->get('user_group_id'); + 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(); + if (null === $membership) { - app('log')->debug('validateUserGroup: user has no access to this group.'); - - throw new FireflyException((string)trans('validation.belongs_user_or_user_group')); + app('log')->debug(sprintf('validateUserGroup: user has no access to group #%d.', $groupId)); + throw new AuthorizationException((string)trans('validation.no_access_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')); } } diff --git a/config/view.php b/config/view.php index cb8bb9e057..09093477b7 100644 --- a/config/view.php +++ b/config/view.php @@ -39,7 +39,7 @@ return [ | the usual Laravel view path has already been registered for you. | */ - + 'layout' => env('FIREFLY_III_LAYOUT', 'v1'), 'paths' => $paths, /* diff --git a/resources/lang/en_US/validation.php b/resources/lang/en_US/validation.php index bbebb1f705..6d762c78ff 100644 --- a/resources/lang/en_US/validation.php +++ b/resources/lang/en_US/validation.php @@ -25,156 +25,158 @@ declare(strict_types=1); return [ - '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.', - 'missing_where' => 'Array is missing "where"-clause', - 'missing_update' => 'Array is missing "update"-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_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_currency' => 'Your query contains accounts that have different currency settings, which is not allowed.', - 'iban' => 'This is not a valid IBAN.', - 'zero_or_more' => 'The value cannot be negative.', - 'more_than_zero' => 'The value must be more than zero.', - 'more_than_zero_correct' => 'The value must be zero or more.', - 'no_asset_account' => 'This is not an asset account.', - '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.', - '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.', - '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.', - 'rule_trigger_value' => 'This value is invalid for the selected trigger.', - 'rule_action_expression' => 'Invalid expression. :error', - 'rule_action_value' => 'This value is invalid for the selected action.', - 'file_already_attached' => 'Uploaded file ":name" is already attached to this object.', - 'file_attached' => 'Successfully uploaded file ":name".', - 'file_zero' => 'The file is zero bytes in size.', - 'must_exist' => 'The ID in field :attribute does not exist in the database.', - '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.', - 'transaction_types_equal' => 'All splits must be of the same type.', - 'invalid_transaction_type' => 'Invalid transaction type.', - 'invalid_selection' => 'Your selection is invalid.', - '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.', - 'at_least_one_transaction' => 'Need at least one transaction.', - 'recurring_transaction_id' => 'Need at least one transaction.', - 'need_id_to_match' => 'You need to submit this entry with an ID for the API to be able to match it.', - 'too_many_unmatched' => 'Too many submitted transactions cannot be matched to their respective database entries. Make sure existing entries have a valid ID.', - 'id_does_not_match' => 'Submitted ID #:id does not match expected ID. Make sure it matches or omit the field.', - 'at_least_one_repetition' => 'Need at least one repetition.', - 'require_repeat_until' => 'Require either a number of repetitions, or an end date (repeat_until). Not both.', - 'require_currency_info' => 'The content of this field is invalid without currency information.', - 'not_transfer_account' => 'This account is not an account that can be used for transfers.', - 'require_currency_amount' => 'The content of this field is invalid without foreign amount information.', - 'require_foreign_currency' => 'This field requires a number', - 'require_foreign_dest' => 'This field value must match the currency of the destination account.', - 'require_foreign_src' => 'This field value must match the currency of the source account.', - 'equal_description' => 'Transaction description should not equal global description.', - 'file_invalid_mime' => 'File ":name" is of type ":mime" which is not accepted as a new upload.', - 'file_too_large' => 'File ":name" is too large.', - 'belongs_to_user' => 'The value of :attribute is unknown.', - 'accepted' => 'The :attribute must be accepted.', - 'bic' => 'This is not a valid BIC.', - 'at_least_one_trigger' => 'Rule must have at least one trigger.', - 'at_least_one_active_trigger' => 'Rule must have at least one active trigger.', - 'at_least_one_action' => 'Rule must have at least one action.', - 'at_least_one_active_action' => 'Rule must have at least one active action.', - 'base64' => 'This is not valid base64 encoded data.', - 'model_id_invalid' => 'The given ID seems invalid for this model.', - 'less' => ':attribute must be less than 10,000,000', - 'active_url' => 'The :attribute is not a valid URL.', - 'after' => 'The :attribute must be a date after :date.', - 'date_after' => 'The start date must be before the end date.', - 'alpha' => 'The :attribute may only contain letters.', - 'alpha_dash' => 'The :attribute may only contain letters, numbers, and dashes.', - 'alpha_num' => 'The :attribute may only contain letters and numbers.', - 'array' => 'The :attribute must be an array.', - 'unique_for_user' => 'There already is an entry with this :attribute.', - '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.', + '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.', + 'missing_where' => 'Array is missing "where"-clause', + 'missing_update' => 'Array is missing "update"-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_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_currency' => 'Your query contains accounts that have different currency settings, which is not allowed.', + 'iban' => 'This is not a valid IBAN.', + 'zero_or_more' => 'The value cannot be negative.', + 'more_than_zero' => 'The value must be more than zero.', + 'more_than_zero_correct' => 'The value must be zero or more.', + 'no_asset_account' => 'This is not an asset account.', + '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.', + '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.', + '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.', + 'rule_trigger_value' => 'This value is invalid for the selected trigger.', + 'rule_action_expression' => 'Invalid expression. :error', + 'rule_action_value' => 'This value is invalid for the selected action.', + 'file_already_attached' => 'Uploaded file ":name" is already attached to this object.', + 'file_attached' => 'Successfully uploaded file ":name".', + 'file_zero' => 'The file is zero bytes in size.', + 'must_exist' => 'The ID in field :attribute does not exist in the database.', + '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.', + 'transaction_types_equal' => 'All splits must be of the same type.', + 'invalid_transaction_type' => 'Invalid transaction type.', + 'invalid_selection' => 'Your selection is invalid.', + '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.', + 'no_access_group' => 'The user has no access to this user group.', + 'no_accepted_roles_defined' => 'No access roles have been defined for this endpoint, access denied.', + 'at_least_one_transaction' => 'Need at least one transaction.', + 'recurring_transaction_id' => 'Need at least one transaction.', + 'need_id_to_match' => 'You need to submit this entry with an ID for the API to be able to match it.', + 'too_many_unmatched' => 'Too many submitted transactions cannot be matched to their respective database entries. Make sure existing entries have a valid ID.', + 'id_does_not_match' => 'Submitted ID #:id does not match expected ID. Make sure it matches or omit the field.', + 'at_least_one_repetition' => 'Need at least one repetition.', + 'require_repeat_until' => 'Require either a number of repetitions, or an end date (repeat_until). Not both.', + 'require_currency_info' => 'The content of this field is invalid without currency information.', + 'not_transfer_account' => 'This account is not an account that can be used for transfers.', + 'require_currency_amount' => 'The content of this field is invalid without foreign amount information.', + 'require_foreign_currency' => 'This field requires a number', + 'require_foreign_dest' => 'This field value must match the currency of the destination account.', + 'require_foreign_src' => 'This field value must match the currency of the source account.', + 'equal_description' => 'Transaction description should not equal global description.', + 'file_invalid_mime' => 'File ":name" is of type ":mime" which is not accepted as a new upload.', + 'file_too_large' => 'File ":name" is too large.', + 'belongs_to_user' => 'The value of :attribute is unknown.', + 'accepted' => 'The :attribute must be accepted.', + 'bic' => 'This is not a valid BIC.', + 'at_least_one_trigger' => 'Rule must have at least one trigger.', + 'at_least_one_active_trigger' => 'Rule must have at least one active trigger.', + 'at_least_one_action' => 'Rule must have at least one action.', + 'at_least_one_active_action' => 'Rule must have at least one active action.', + 'base64' => 'This is not valid base64 encoded data.', + 'model_id_invalid' => 'The given ID seems invalid for this model.', + 'less' => ':attribute must be less than 10,000,000', + 'active_url' => 'The :attribute is not a valid URL.', + 'after' => 'The :attribute must be a date after :date.', + 'date_after' => 'The start date must be before the end date.', + 'alpha' => 'The :attribute may only contain letters.', + 'alpha_dash' => 'The :attribute may only contain letters, numbers, and dashes.', + 'alpha_num' => 'The :attribute may only contain letters and numbers.', + 'array' => 'The :attribute must be an array.', + 'unique_for_user' => 'There already is an entry with this :attribute.', + '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 - 'between.numeric' => 'The :attribute must be between :min and :max.', - 'between.file' => 'The :attribute must be between :min and :max kilobytes.', - 'between.string' => 'The :attribute must be between :min and :max characters.', - 'between.array' => 'The :attribute must have between :min and :max items.', - 'boolean' => 'The :attribute field must be true or false.', - 'confirmed' => 'The :attribute confirmation does not match.', - 'date' => 'The :attribute is not a valid date.', - 'date_format' => 'The :attribute does not match the format :format.', - 'different' => 'The :attribute and :other must be different.', - 'digits' => 'The :attribute must be :digits digits.', - 'digits_between' => 'The :attribute must be between :min and :max digits.', - 'email' => 'The :attribute must be a valid email address.', - 'filled' => 'The :attribute field is required.', - 'exists' => 'The selected :attribute is invalid.', - 'image' => 'The :attribute must be an image.', - 'in' => 'The selected :attribute is invalid.', - 'integer' => 'The :attribute must be an integer.', - 'ip' => 'The :attribute must be a valid IP address.', - 'json' => 'The :attribute must be a valid JSON string.', - 'max.numeric' => 'The :attribute may not be greater than :max.', - 'max.file' => 'The :attribute may not be greater than :max kilobytes.', - 'max.string' => 'The :attribute may not be greater than :max characters.', - 'max.array' => 'The :attribute may not have more than :max items.', - 'mimes' => 'The :attribute must be a file of type: :values.', - 'min.numeric' => 'The :attribute must be at least :min.', - 'lte.numeric' => 'The :attribute must be less than or equal :value.', - 'min.file' => 'The :attribute must be at least :min kilobytes.', - 'min.string' => 'The :attribute must be at least :min characters.', - 'min.array' => 'The :attribute must have at least :min items.', - 'not_in' => 'The selected :attribute is invalid.', - 'numeric' => 'The :attribute must be a number.', - 'scientific_notation' => 'The :attribute cannot use the scientific notation.', - 'numeric_native' => 'The native amount must be a number.', - 'numeric_destination' => 'The destination amount must be a number.', - 'numeric_source' => 'The source amount must be a number.', - 'regex' => 'The :attribute format is invalid.', - 'required' => 'The :attribute field is required.', - 'required_if' => 'The :attribute field is required when :other is :value.', - '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_all' => 'The :attribute field is required when :values is 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.', - 'same' => 'The :attribute and :other must match.', - 'size.numeric' => 'The :attribute must be :size.', - 'amount_min_over_max' => 'The minimum amount cannot be larger than the maximum amount.', - 'size.file' => 'The :attribute must be :size kilobytes.', - 'size.string' => 'The :attribute must be :size characters.', - 'size.array' => 'The :attribute must contain :size items.', - 'unique' => 'The :attribute has already been taken.', - 'string' => 'The :attribute must be a string.', - 'url' => 'The :attribute format is invalid.', - 'timezone' => 'The :attribute must be a valid zone.', - '2fa_code' => 'The :attribute field is invalid.', - 'dimensions' => 'The :attribute has invalid image dimensions.', - 'distinct' => 'The :attribute field has a duplicate value.', - 'file' => 'The :attribute must be a file.', - 'in_array' => 'The :attribute field does not exist in :other.', - 'present' => 'The :attribute field must be present.', - 'amount_zero' => 'The total amount cannot be zero.', - '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_object_group' => 'The group name must be unique', - '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_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_currency' => 'Both accounts must have the same currency setting', + 'between.numeric' => 'The :attribute must be between :min and :max.', + 'between.file' => 'The :attribute must be between :min and :max kilobytes.', + 'between.string' => 'The :attribute must be between :min and :max characters.', + 'between.array' => 'The :attribute must have between :min and :max items.', + 'boolean' => 'The :attribute field must be true or false.', + 'confirmed' => 'The :attribute confirmation does not match.', + 'date' => 'The :attribute is not a valid date.', + 'date_format' => 'The :attribute does not match the format :format.', + 'different' => 'The :attribute and :other must be different.', + 'digits' => 'The :attribute must be :digits digits.', + 'digits_between' => 'The :attribute must be between :min and :max digits.', + 'email' => 'The :attribute must be a valid email address.', + 'filled' => 'The :attribute field is required.', + 'exists' => 'The selected :attribute is invalid.', + 'image' => 'The :attribute must be an image.', + 'in' => 'The selected :attribute is invalid.', + 'integer' => 'The :attribute must be an integer.', + 'ip' => 'The :attribute must be a valid IP address.', + 'json' => 'The :attribute must be a valid JSON string.', + 'max.numeric' => 'The :attribute may not be greater than :max.', + 'max.file' => 'The :attribute may not be greater than :max kilobytes.', + 'max.string' => 'The :attribute may not be greater than :max characters.', + 'max.array' => 'The :attribute may not have more than :max items.', + 'mimes' => 'The :attribute must be a file of type: :values.', + 'min.numeric' => 'The :attribute must be at least :min.', + 'lte.numeric' => 'The :attribute must be less than or equal :value.', + 'min.file' => 'The :attribute must be at least :min kilobytes.', + 'min.string' => 'The :attribute must be at least :min characters.', + 'min.array' => 'The :attribute must have at least :min items.', + 'not_in' => 'The selected :attribute is invalid.', + 'numeric' => 'The :attribute must be a number.', + 'scientific_notation' => 'The :attribute cannot use the scientific notation.', + 'numeric_native' => 'The native amount must be a number.', + 'numeric_destination' => 'The destination amount must be a number.', + 'numeric_source' => 'The source amount must be a number.', + 'regex' => 'The :attribute format is invalid.', + 'required' => 'The :attribute field is required.', + 'required_if' => 'The :attribute field is required when :other is :value.', + '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_all' => 'The :attribute field is required when :values is 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.', + 'same' => 'The :attribute and :other must match.', + 'size.numeric' => 'The :attribute must be :size.', + 'amount_min_over_max' => 'The minimum amount cannot be larger than the maximum amount.', + 'size.file' => 'The :attribute must be :size kilobytes.', + 'size.string' => 'The :attribute must be :size characters.', + 'size.array' => 'The :attribute must contain :size items.', + 'unique' => 'The :attribute has already been taken.', + 'string' => 'The :attribute must be a string.', + 'url' => 'The :attribute format is invalid.', + 'timezone' => 'The :attribute must be a valid zone.', + '2fa_code' => 'The :attribute field is invalid.', + 'dimensions' => 'The :attribute has invalid image dimensions.', + 'distinct' => 'The :attribute field has a duplicate value.', + 'file' => 'The :attribute must be a file.', + 'in_array' => 'The :attribute field does not exist in :other.', + 'present' => 'The :attribute field must be present.', + 'amount_zero' => 'The total amount cannot be zero.', + '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_object_group' => 'The group name must be unique', + '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_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_currency' => 'Both accounts must have the same currency setting', // Ignore this comment - '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_moment' => 'Invalid repetition moment for this type of repetition.', - 'invalid_account_info' => 'Invalid account information.', - 'attributes' => [ + '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_moment' => 'Invalid repetition moment for this type of repetition.', + 'invalid_account_info' => 'Invalid account information.', + 'attributes' => [ 'email' => 'email address', 'description' => 'description', 'amount' => 'amount', @@ -213,58 +215,58 @@ return [ ], // 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_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_bad_data' => 'Could not find a valid destination account when searching for ID ":id" or name ":name".', + '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_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_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.', + '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.', - '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_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_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_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_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_wrong_type' => 'The submitted destination account is not of the right type.', // 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_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_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).', + '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_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".', + '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.', - '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_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.', + '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.', + '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".', + '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_destination' => 'You can\'t use this account as the destination 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_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_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.', - 'gte.numeric' => 'The :attribute must be greater than or equal to :value.', - 'gt.numeric' => 'The :attribute must be greater than :value.', - '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.array' => 'The :attribute must have :value items or more.', + 'gte.numeric' => 'The :attribute must be greater than or equal to :value.', + 'gt.numeric' => 'The :attribute must be greater than :value.', + '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.array' => 'The :attribute must have :value items or more.', 'amount_required_for_auto_budget' => 'The amount is required.', '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_user_group' => 'You do not have the correct access rights for this administration.', - 'administration_owner_rename' => 'You can\'t rename your standard 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.', ]; // Ignore this comment diff --git a/resources/views/administrations/index.twig b/resources/views/administrations/index.twig new file mode 100644 index 0000000000..ce4b29c209 --- /dev/null +++ b/resources/views/administrations/index.twig @@ -0,0 +1 @@ +This feature is only available in the v2 layout.