mirror of
https://github.com/firefly-iii/firefly-iii.git
synced 2026-01-06 06:01:21 +00:00
Compare commits
54 Commits
develop-20
...
develop-20
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6d4004d1ed | ||
|
|
ae60cd5b28 | ||
|
|
ab31a72199 | ||
|
|
2c1b9534f3 | ||
|
|
7028cb1546 | ||
|
|
dc1ecf6a42 | ||
|
|
3a27f9d02c | ||
|
|
4b27ab38f8 | ||
|
|
40de147611 | ||
|
|
bb4f90d730 | ||
|
|
d89d46aaec | ||
|
|
304d720c4c | ||
|
|
7eff160190 | ||
|
|
8b2e18ed9d | ||
|
|
7001051833 | ||
|
|
b4b9752c05 | ||
|
|
acadc89eaa | ||
|
|
6ff84b8e90 | ||
|
|
7f3e3fc3bf | ||
|
|
02233fd7a4 | ||
|
|
50d3db0643 | ||
|
|
3751831779 | ||
|
|
14a24e47fb | ||
|
|
b7e78cb0e6 | ||
|
|
a8f65f42fc | ||
|
|
d3385a116d | ||
|
|
e0c446dd13 | ||
|
|
33d11b4780 | ||
|
|
07c49d1d04 | ||
|
|
9463285ac9 | ||
|
|
b41fc43e64 | ||
|
|
562763c938 | ||
|
|
ec60194110 | ||
|
|
1e472ee095 | ||
|
|
5597327448 | ||
|
|
cdd5baf5be | ||
|
|
7b5978059b | ||
|
|
da0b41e45c | ||
|
|
d0be2afba5 | ||
|
|
d99851231a | ||
|
|
7e02c141f9 | ||
|
|
d03960e379 | ||
|
|
16d3984ffc | ||
|
|
856a194988 | ||
|
|
1bff966bfe | ||
|
|
1948b6118b | ||
|
|
20c25d3ca2 | ||
|
|
a153735ac3 | ||
|
|
62509f7c18 | ||
|
|
9b48b67158 | ||
|
|
cbd50634a4 | ||
|
|
f475393bc1 | ||
|
|
abcddb09bf | ||
|
|
cf71a0fc55 |
102
.ci/php-cs-fixer/composer.lock
generated
102
.ci/php-cs-fixer/composer.lock
generated
@@ -1259,16 +1259,16 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/console",
|
||||
"version": "v7.1.5",
|
||||
"version": "v7.1.6",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/console.git",
|
||||
"reference": "0fa539d12b3ccf068a722bbbffa07ca7079af9ee"
|
||||
"reference": "bb5192af6edc797cbab5c8e8ecfea2fe5f421e57"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/console/zipball/0fa539d12b3ccf068a722bbbffa07ca7079af9ee",
|
||||
"reference": "0fa539d12b3ccf068a722bbbffa07ca7079af9ee",
|
||||
"url": "https://api.github.com/repos/symfony/console/zipball/bb5192af6edc797cbab5c8e8ecfea2fe5f421e57",
|
||||
"reference": "bb5192af6edc797cbab5c8e8ecfea2fe5f421e57",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -1332,7 +1332,7 @@
|
||||
"terminal"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/console/tree/v7.1.5"
|
||||
"source": "https://github.com/symfony/console/tree/v7.1.6"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -1348,7 +1348,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-09-20T08:28:38+00:00"
|
||||
"time": "2024-10-09T08:46:59+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/deprecation-contracts",
|
||||
@@ -1419,16 +1419,16 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/event-dispatcher",
|
||||
"version": "v7.1.1",
|
||||
"version": "v7.1.6",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/event-dispatcher.git",
|
||||
"reference": "9fa7f7a21beb22a39a8f3f28618b29e50d7a55a7"
|
||||
"reference": "87254c78dd50721cfd015b62277a8281c5589702"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/9fa7f7a21beb22a39a8f3f28618b29e50d7a55a7",
|
||||
"reference": "9fa7f7a21beb22a39a8f3f28618b29e50d7a55a7",
|
||||
"url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/87254c78dd50721cfd015b62277a8281c5589702",
|
||||
"reference": "87254c78dd50721cfd015b62277a8281c5589702",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -1479,7 +1479,7 @@
|
||||
"description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/event-dispatcher/tree/v7.1.1"
|
||||
"source": "https://github.com/symfony/event-dispatcher/tree/v7.1.6"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -1495,7 +1495,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-05-31T14:57:53+00:00"
|
||||
"time": "2024-09-25T14:20:29+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/event-dispatcher-contracts",
|
||||
@@ -1575,16 +1575,16 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/filesystem",
|
||||
"version": "v7.1.5",
|
||||
"version": "v7.1.6",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/filesystem.git",
|
||||
"reference": "61fe0566189bf32e8cfee78335d8776f64a66f5a"
|
||||
"reference": "c835867b3c62bb05c7fe3d637c871c7ae52024d4"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/filesystem/zipball/61fe0566189bf32e8cfee78335d8776f64a66f5a",
|
||||
"reference": "61fe0566189bf32e8cfee78335d8776f64a66f5a",
|
||||
"url": "https://api.github.com/repos/symfony/filesystem/zipball/c835867b3c62bb05c7fe3d637c871c7ae52024d4",
|
||||
"reference": "c835867b3c62bb05c7fe3d637c871c7ae52024d4",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -1621,7 +1621,7 @@
|
||||
"description": "Provides basic utilities for the filesystem",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/filesystem/tree/v7.1.5"
|
||||
"source": "https://github.com/symfony/filesystem/tree/v7.1.6"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -1637,20 +1637,20 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-09-17T09:16:35+00:00"
|
||||
"time": "2024-10-25T15:11:02+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/finder",
|
||||
"version": "v7.1.4",
|
||||
"version": "v7.1.6",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/finder.git",
|
||||
"reference": "d95bbf319f7d052082fb7af147e0f835a695e823"
|
||||
"reference": "2cb89664897be33f78c65d3d2845954c8d7a43b8"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/finder/zipball/d95bbf319f7d052082fb7af147e0f835a695e823",
|
||||
"reference": "d95bbf319f7d052082fb7af147e0f835a695e823",
|
||||
"url": "https://api.github.com/repos/symfony/finder/zipball/2cb89664897be33f78c65d3d2845954c8d7a43b8",
|
||||
"reference": "2cb89664897be33f78c65d3d2845954c8d7a43b8",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -1685,7 +1685,7 @@
|
||||
"description": "Finds files and directories via an intuitive fluent interface",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/finder/tree/v7.1.4"
|
||||
"source": "https://github.com/symfony/finder/tree/v7.1.6"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -1701,20 +1701,20 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-08-13T14:28:19+00:00"
|
||||
"time": "2024-10-01T08:31:23+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/options-resolver",
|
||||
"version": "v7.1.1",
|
||||
"version": "v7.1.6",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/options-resolver.git",
|
||||
"reference": "47aa818121ed3950acd2b58d1d37d08a94f9bf55"
|
||||
"reference": "85e95eeede2d41cd146146e98c9c81d9214cae85"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/options-resolver/zipball/47aa818121ed3950acd2b58d1d37d08a94f9bf55",
|
||||
"reference": "47aa818121ed3950acd2b58d1d37d08a94f9bf55",
|
||||
"url": "https://api.github.com/repos/symfony/options-resolver/zipball/85e95eeede2d41cd146146e98c9c81d9214cae85",
|
||||
"reference": "85e95eeede2d41cd146146e98c9c81d9214cae85",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -1752,7 +1752,7 @@
|
||||
"options"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/options-resolver/tree/v7.1.1"
|
||||
"source": "https://github.com/symfony/options-resolver/tree/v7.1.6"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -1768,7 +1768,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-05-31T14:57:53+00:00"
|
||||
"time": "2024-09-25T14:20:29+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-ctype",
|
||||
@@ -2246,16 +2246,16 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/process",
|
||||
"version": "v7.1.5",
|
||||
"version": "v7.1.6",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/process.git",
|
||||
"reference": "5c03ee6369281177f07f7c68252a280beccba847"
|
||||
"reference": "6aaa189ddb4ff6b5de8fa3210f2fb42c87b4d12e"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/process/zipball/5c03ee6369281177f07f7c68252a280beccba847",
|
||||
"reference": "5c03ee6369281177f07f7c68252a280beccba847",
|
||||
"url": "https://api.github.com/repos/symfony/process/zipball/6aaa189ddb4ff6b5de8fa3210f2fb42c87b4d12e",
|
||||
"reference": "6aaa189ddb4ff6b5de8fa3210f2fb42c87b4d12e",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -2287,7 +2287,7 @@
|
||||
"description": "Executes commands in sub-processes",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/process/tree/v7.1.5"
|
||||
"source": "https://github.com/symfony/process/tree/v7.1.6"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -2303,7 +2303,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-09-19T21:48:23+00:00"
|
||||
"time": "2024-09-25T14:20:29+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/service-contracts",
|
||||
@@ -2390,16 +2390,16 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/stopwatch",
|
||||
"version": "v7.1.1",
|
||||
"version": "v7.1.6",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/stopwatch.git",
|
||||
"reference": "5b75bb1ac2ba1b9d05c47fc4b3046a625377d23d"
|
||||
"reference": "8b4a434e6e7faf6adedffb48783a5c75409a1a05"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/stopwatch/zipball/5b75bb1ac2ba1b9d05c47fc4b3046a625377d23d",
|
||||
"reference": "5b75bb1ac2ba1b9d05c47fc4b3046a625377d23d",
|
||||
"url": "https://api.github.com/repos/symfony/stopwatch/zipball/8b4a434e6e7faf6adedffb48783a5c75409a1a05",
|
||||
"reference": "8b4a434e6e7faf6adedffb48783a5c75409a1a05",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -2432,7 +2432,7 @@
|
||||
"description": "Provides a way to profile code",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/stopwatch/tree/v7.1.1"
|
||||
"source": "https://github.com/symfony/stopwatch/tree/v7.1.6"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -2448,20 +2448,20 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-05-31T14:57:53+00:00"
|
||||
"time": "2024-09-25T14:20:29+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/string",
|
||||
"version": "v7.1.5",
|
||||
"version": "v7.1.6",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/string.git",
|
||||
"reference": "d66f9c343fa894ec2037cc928381df90a7ad4306"
|
||||
"reference": "61b72d66bf96c360a727ae6232df5ac83c71f626"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/string/zipball/d66f9c343fa894ec2037cc928381df90a7ad4306",
|
||||
"reference": "d66f9c343fa894ec2037cc928381df90a7ad4306",
|
||||
"url": "https://api.github.com/repos/symfony/string/zipball/61b72d66bf96c360a727ae6232df5ac83c71f626",
|
||||
"reference": "61b72d66bf96c360a727ae6232df5ac83c71f626",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -2519,7 +2519,7 @@
|
||||
"utf8"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/string/tree/v7.1.5"
|
||||
"source": "https://github.com/symfony/string/tree/v7.1.6"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -2535,16 +2535,16 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-09-20T08:28:38+00:00"
|
||||
"time": "2024-09-25T14:20:29+00:00"
|
||||
}
|
||||
],
|
||||
"packages-dev": [],
|
||||
"aliases": [],
|
||||
"minimum-stability": "stable",
|
||||
"stability-flags": [],
|
||||
"stability-flags": {},
|
||||
"prefer-stable": false,
|
||||
"prefer-lowest": false,
|
||||
"platform": [],
|
||||
"platform-dev": [],
|
||||
"platform": {},
|
||||
"platform-dev": {},
|
||||
"plugin-api-version": "2.6.0"
|
||||
}
|
||||
|
||||
@@ -176,6 +176,7 @@ MAILGUN_ENDPOINT=api.mailgun.net
|
||||
# If you use Docker or similar, you can set these variables from a file by appending them with _FILE
|
||||
MANDRILL_SECRET=
|
||||
SPARKPOST_SECRET=
|
||||
MAILERSEND_API_KEY=
|
||||
|
||||
# Firefly III can send you the following messages.
|
||||
SEND_ERROR_MESSAGE=true
|
||||
|
||||
2
.github/workflows/close-duplicates.yml
vendored
2
.github/workflows/close-duplicates.yml
vendored
@@ -13,7 +13,7 @@ jobs:
|
||||
close_duplicates:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: github/command@v1.2.1
|
||||
- uses: github/command@v1.2.2
|
||||
id: command
|
||||
with:
|
||||
allowed_contexts: "issue"
|
||||
|
||||
3
.github/workflows/stale.yml
vendored
3
.github/workflows/stale.yml
vendored
@@ -35,4 +35,5 @@ jobs:
|
||||
Thank you for your contributions.
|
||||
days-before-stale: 14
|
||||
days-before-close: 7
|
||||
exempt-issue-labels: 'enhancement,feature,bug,announcement,epic,triage'
|
||||
exempt-all-milestones: true
|
||||
exempt-issue-labels: 'triage'
|
||||
|
||||
@@ -79,12 +79,12 @@ class StoreRequest extends FormRequest
|
||||
'currency_id' => 'numeric|exists:transaction_currencies,id',
|
||||
'currency_code' => 'min:3|max:51|exists:transaction_currencies,code',
|
||||
'date' => 'date|required',
|
||||
'end_date' => 'date|after:date',
|
||||
'extension_date' => 'date|after:date',
|
||||
'end_date' => 'nullable|date|after:date',
|
||||
'extension_date' => 'nullable|date|after:date',
|
||||
'repeat_freq' => 'in:weekly,monthly,quarterly,half-year,yearly|required',
|
||||
'skip' => 'min:0|max:31|numeric',
|
||||
'active' => [new IsBoolean()],
|
||||
'notes' => 'min:1|max:32768',
|
||||
'notes' => 'nullable|min:1|max:32768',
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -68,8 +68,8 @@ class AccountController extends Controller
|
||||
*/
|
||||
public function accounts(AutocompleteRequest $request): JsonResponse
|
||||
{
|
||||
$queryParameters = $request->getParameters();
|
||||
$result = $this->repository->searchAccount($queryParameters['query'], $queryParameters['account_types'], $queryParameters['size']);
|
||||
$params = $request->getParameters();
|
||||
$result = $this->repository->searchAccount($params['query'], $params['account_types'], $params['page'], $params['size']);
|
||||
$return = [];
|
||||
|
||||
/** @var Account $account */
|
||||
@@ -89,6 +89,7 @@ class AccountController extends Controller
|
||||
'title' => $account->name,
|
||||
'meta' => [
|
||||
'type' => $account->accountType->type,
|
||||
// TODO is multi currency property.
|
||||
'currency_id' => null === $currency ? null : (string) $currency->id,
|
||||
'currency_code' => $currency?->code,
|
||||
'currency_symbol' => $currency?->symbol,
|
||||
|
||||
@@ -84,10 +84,6 @@ class BalanceController extends Controller
|
||||
$queryParameters = $request->getParameters();
|
||||
$accounts = $this->getAccountList($queryParameters);
|
||||
|
||||
// move date to end of day
|
||||
$queryParameters['start']->startOfDay();
|
||||
$queryParameters['end']->endOfDay();
|
||||
|
||||
// prepare for currency conversion and data collection:
|
||||
/** @var TransactionCurrency $default */
|
||||
$default = app('amount')->getDefaultCurrency();
|
||||
|
||||
@@ -23,15 +23,12 @@ declare(strict_types=1);
|
||||
|
||||
namespace FireflyIII\Api\V2\Request\Autocomplete;
|
||||
|
||||
use FireflyIII\JsonApi\Rules\IsValidFilter;
|
||||
use FireflyIII\JsonApi\Rules\IsValidPage;
|
||||
use FireflyIII\Models\AccountType;
|
||||
use FireflyIII\Support\Http\Api\AccountFilter;
|
||||
use FireflyIII\Support\Http\Api\ParsesQueryFilters;
|
||||
use FireflyIII\Support\Request\ChecksLogin;
|
||||
use FireflyIII\Support\Request\ConvertsDataTypes;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use LaravelJsonApi\Core\Query\QueryParameters;
|
||||
use LaravelJsonApi\Validation\Rule as JsonApiRule;
|
||||
|
||||
/**
|
||||
* Class AutocompleteRequest
|
||||
@@ -51,35 +48,46 @@ class AutocompleteRequest extends FormRequest
|
||||
*/
|
||||
public function getParameters(): array
|
||||
{
|
||||
$queryParameters = QueryParameters::cast($this->all());
|
||||
|
||||
return [
|
||||
'date' => $this->dateOrToday($queryParameters, 'date'),
|
||||
'query' => $this->arrayOfStrings($queryParameters, 'query'),
|
||||
'size' => $this->integerFromQueryParams($queryParameters, 'size', 50),
|
||||
'account_types' => $this->getAccountTypeParameter($this->arrayOfStrings($queryParameters, 'account_types')),
|
||||
$array = [
|
||||
'date' => $this->convertDateTime('date'),
|
||||
'query' => $this->clearString((string) $this->get('query')),
|
||||
'size' => $this->integerFromValue('size'),
|
||||
'page' => $this->integerFromValue('page'),
|
||||
'account_types' => $this->arrayFromValue($this->get('account_types')),
|
||||
'transaction_types' => $this->arrayFromValue($this->get('transaction_types')),
|
||||
];
|
||||
$array['size'] = $array['size'] < 1 || $array['size'] > 100 ? 15 : $array['size'];
|
||||
$array['page'] = max($array['page'], 1);
|
||||
if (null === $array['account_types']) {
|
||||
$array['account_types'] = [];
|
||||
}
|
||||
if (null === $array['transaction_types']) {
|
||||
$array['transaction_types'] = [];
|
||||
}
|
||||
|
||||
// remove 'initial balance' from allowed types. its internal
|
||||
$array['account_types'] = array_diff($array['account_types'], [AccountType::INITIAL_BALANCE, AccountType::RECONCILIATION, AccountType::CREDITCARD]);
|
||||
$array['account_types'] = $this->getAccountTypeParameter($array['account_types']);
|
||||
|
||||
return $array;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
$valid = array_keys($this->types);
|
||||
|
||||
return [
|
||||
'fields' => JsonApiRule::notSupported(),
|
||||
'filter' => ['nullable', 'array', new IsValidFilter(['query', 'date', 'account_types'])],
|
||||
'include' => JsonApiRule::notSupported(),
|
||||
'page' => ['nullable', 'array', new IsValidPage(['size'])],
|
||||
'sort' => JsonApiRule::notSupported(),
|
||||
'date' => 'nullable|date|after:1900-01-01|before:2100-01-01',
|
||||
'query' => 'nullable|string',
|
||||
'size' => 'nullable|integer|min:1|max:100',
|
||||
'page' => 'nullable|integer|min:1',
|
||||
'account_types' => sprintf('nullable|in:%s', implode(',', $valid)),
|
||||
'transaction_types' => 'nullable|in:todo',
|
||||
];
|
||||
}
|
||||
|
||||
private function getAccountTypeParameter(mixed $types): array
|
||||
private function getAccountTypeParameter(array $types): array
|
||||
{
|
||||
if (is_string($types) && str_contains($types, ',')) {
|
||||
$types = explode(',', $types);
|
||||
}
|
||||
if (!is_iterable($types)) {
|
||||
$types = [$types];
|
||||
}
|
||||
$return = [];
|
||||
foreach ($types as $type) {
|
||||
$return = array_merge($return, $this->mapAccountTypes($type));
|
||||
|
||||
@@ -24,8 +24,6 @@ declare(strict_types=1);
|
||||
namespace FireflyIII\Api\V2\Request\Chart;
|
||||
|
||||
use FireflyIII\Enums\UserRoleEnum;
|
||||
use FireflyIII\JsonApi\Rules\IsValidFilter;
|
||||
use FireflyIII\Rules\IsFilterValueIn;
|
||||
use FireflyIII\Support\Http\Api\ParsesQueryFilters;
|
||||
use FireflyIII\Support\Http\Api\ValidatesUserGroupTrait;
|
||||
use FireflyIII\Support\Request\ChecksLogin;
|
||||
@@ -33,8 +31,6 @@ use FireflyIII\Support\Request\ConvertsDataTypes;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Validation\Validator;
|
||||
use LaravelJsonApi\Core\Query\QueryParameters;
|
||||
use LaravelJsonApi\Validation\Rule as JsonApiRule;
|
||||
|
||||
/**
|
||||
* Class ChartRequest
|
||||
@@ -50,50 +46,29 @@ class ChartRequest extends FormRequest
|
||||
|
||||
public function getParameters(): array
|
||||
{
|
||||
$queryParameters = QueryParameters::cast($this->all());
|
||||
|
||||
// $queryParameters = QueryParameters::cast($this->all());
|
||||
return [
|
||||
'start' => $this->dateOrToday($queryParameters, 'start'),
|
||||
'end' => $this->dateOrToday($queryParameters, 'end'),
|
||||
'preselected' => $this->stringFromQueryParams($queryParameters, 'preselected', 'empty'),
|
||||
'period' => $this->stringFromQueryParams($queryParameters, 'period', '1M'),
|
||||
'accounts' => $this->arrayOfStrings($queryParameters, 'accounts'),
|
||||
// preselected heeft maar een paar toegestane waardes, dat moet ook goed gaan.
|
||||
// 'query' => $this->arrayOfStrings($queryParameters, 'query'),
|
||||
// 'size' => $this->integerFromQueryParams($queryParameters,'size', 50),
|
||||
// 'account_types' => $this->getAccountTypeParameter($this->arrayOfStrings($queryParameters, 'account_types')),
|
||||
'start' => $this->convertDateTime('start')?->startOfDay(),
|
||||
'end' => $this->convertDateTime('end')?->endOfDay(),
|
||||
'preselected' => $this->convertString('preselected', 'empty'),
|
||||
'period' => $this->convertString('period', '1M'),
|
||||
'accounts' => $this->arrayFromValue($this->get('accounts')),
|
||||
];
|
||||
// collect accounts based on this list?
|
||||
}
|
||||
|
||||
// return [
|
||||
// 'accounts' => $this->getAccountList(),
|
||||
// 'preselected' => $this->convertString('preselected'),
|
||||
// ];
|
||||
// }
|
||||
|
||||
/**
|
||||
* The rules that the incoming request must be matched against.
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'fields' => JsonApiRule::notSupported(),
|
||||
'filter' => ['nullable', 'array',
|
||||
new IsValidFilter(['start', 'end', 'preselected', 'accounts']),
|
||||
new IsFilterValueIn('preselected', config('firefly.preselected_accounts')),
|
||||
],
|
||||
'include' => JsonApiRule::notSupported(),
|
||||
'page' => JsonApiRule::notSupported(),
|
||||
'sort' => JsonApiRule::notSupported(),
|
||||
'start' => 'required|date|after:1900-01-01|before:2099-12-31|before_or_equal:end',
|
||||
'end' => 'required|date|after:1900-01-01|before:2099-12-31|after_or_equal:start',
|
||||
'preselected' => sprintf('nullable|in:%s', implode(',', config('firefly.preselected_accounts'))),
|
||||
'period' => sprintf('nullable|in:%s', implode(',', config('firefly.valid_view_ranges'))),
|
||||
'accounts.*' => 'exists:accounts,id',
|
||||
];
|
||||
|
||||
// return [
|
||||
// 'start' => 'required|date|after:1900-01-01|before:2099-12-31',
|
||||
// 'end' => 'required|date|after_or_equal:start|before:2099-12-31|after:1900-01-01',
|
||||
// 'preselected' => sprintf('in:%s', implode(',', config('firefly.preselected_accounts'))),
|
||||
// 'accounts.*' => 'exists:accounts,id',
|
||||
// ];
|
||||
}
|
||||
|
||||
public function withValidator(Validator $validator): void
|
||||
|
||||
@@ -51,7 +51,11 @@ class FixUnevenAmount extends Command
|
||||
$this->convertOldStyleTransfers();
|
||||
$this->fixUnevenAmounts();
|
||||
$this->matchCurrencies();
|
||||
AccountBalanceCalculator::forceRecalculateAll();
|
||||
if (config('firefly.feature_flags.running_balance_column')) {
|
||||
$this->friendlyInfo('Will recalculate transaction running balance columns. This may take a LONG time. Please be patient.');
|
||||
AccountBalanceCalculator::recalculateAll(true);
|
||||
$this->friendlyInfo('Done recalculating transaction running balance columns.');
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -46,10 +46,15 @@ class Cron extends Command
|
||||
protected $signature = 'firefly-iii:cron
|
||||
{--F|force : Force the cron job(s) to execute.}
|
||||
{--date= : Set the date in YYYY-MM-DD to make Firefly III think that\'s the current date.}
|
||||
{--download-cer : Download exchange rates. Other tasks will be skipped unless also requested.}
|
||||
{--create-recurring : Create recurring transactions. Other tasks will be skipped unless also requested.}
|
||||
{--create-auto-budgets : Create auto budgets. Other tasks will be skipped unless also requested.}
|
||||
{--send-bill-warnings : Send bill warnings. Other tasks will be skipped unless also requested.}
|
||||
';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$doAll = !$this->option('download-cer') && !$this->option('create-recurring') && !$this->option('create-auto-budgets') && !$this->option('send-bill-warnings');
|
||||
$date = null;
|
||||
|
||||
try {
|
||||
@@ -60,7 +65,7 @@ class Cron extends Command
|
||||
$force = (bool)$this->option('force'); // @phpstan-ignore-line
|
||||
|
||||
// Fire exchange rates cron job.
|
||||
if (true === config('cer.download_enabled')) {
|
||||
if (true === config('cer.download_enabled') && ($doAll || $this->option('download-cer'))) {
|
||||
try {
|
||||
$this->exchangeRatesCronJob($force, $date);
|
||||
} catch (FireflyException $e) {
|
||||
@@ -71,6 +76,7 @@ class Cron extends Command
|
||||
}
|
||||
|
||||
// Fire recurring transaction cron job.
|
||||
if ($doAll || $this->option('create-recurring')) {
|
||||
try {
|
||||
$this->recurringCronJob($force, $date);
|
||||
} catch (FireflyException $e) {
|
||||
@@ -78,8 +84,10 @@ class Cron extends Command
|
||||
app('log')->error($e->getTraceAsString());
|
||||
$this->friendlyError($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// Fire auto-budget cron job:
|
||||
if ($doAll || $this->option('create-auto-budgets')) {
|
||||
try {
|
||||
$this->autoBudgetCronJob($force, $date);
|
||||
} catch (FireflyException $e) {
|
||||
@@ -87,8 +95,10 @@ class Cron extends Command
|
||||
app('log')->error($e->getTraceAsString());
|
||||
$this->friendlyError($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// Fire bill warning cron job
|
||||
if ($doAll || $this->option('send-bill-warnings')) {
|
||||
try {
|
||||
$this->billWarningCronJob($force, $date);
|
||||
} catch (FireflyException $e) {
|
||||
@@ -96,6 +106,7 @@ class Cron extends Command
|
||||
app('log')->error($e->getTraceAsString());
|
||||
$this->friendlyError($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
$this->friendlyInfo('More feedback on the cron jobs can be found in the log files.');
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ use Illuminate\Console\Command;
|
||||
class CorrectAccountBalance extends Command
|
||||
{
|
||||
use ShowsFriendlyMessages;
|
||||
|
||||
public const string CONFIG_NAME = '610_correct_balances';
|
||||
protected $description = 'Recalculate all account balance amounts';
|
||||
protected $signature = 'firefly-iii:correct-account-balance {--F|force : Force the execution of this command.}';
|
||||
@@ -44,16 +45,23 @@ class CorrectAccountBalance extends Command
|
||||
|
||||
return 0;
|
||||
}
|
||||
$this->friendlyWarning('This command has been disabled.');
|
||||
if (config('firefly.feature_flags.running_balance_column')) {
|
||||
$this->friendlyInfo('Will recalculate account balances. This may take a LONG time. Please be patient.');
|
||||
$this->markAsExecuted();
|
||||
$this->correctBalanceAmounts();
|
||||
$this->friendlyInfo('Done recalculating account balances.');
|
||||
|
||||
return 0;
|
||||
}
|
||||
$this->friendlyWarning('This command has been disabled.');
|
||||
|
||||
// $this->correctBalanceAmounts();
|
||||
return 0;
|
||||
}
|
||||
|
||||
private function correctBalanceAmounts(): void
|
||||
{
|
||||
AccountBalanceCalculator::recalculateAll();
|
||||
return;
|
||||
AccountBalanceCalculator::recalculateAll(true);
|
||||
}
|
||||
|
||||
private function isExecuted(): bool
|
||||
|
||||
@@ -54,6 +54,7 @@ class MigrateRuleActions extends Command
|
||||
}
|
||||
$this->replaceEqualSign();
|
||||
$this->replaceObsoleteActions();
|
||||
$this->markAsExecuted();
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -179,4 +180,9 @@ class MigrateRuleActions extends Command
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function markAsExecuted(): void
|
||||
{
|
||||
app('fireflyconfig')->set(self::CONFIG_NAME, true);
|
||||
}
|
||||
}
|
||||
|
||||
43
app/Events/Security/DisabledMFA.php
Normal file
43
app/Events/Security/DisabledMFA.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
/*
|
||||
* EnabledMFA.php
|
||||
* Copyright (c) 2024 james@firefly-iii.org.
|
||||
*
|
||||
* This file is part of Firefly III (https://github.com/firefly-iii).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace FireflyIII\Events\Security;
|
||||
|
||||
use FireflyIII\Events\Event;
|
||||
use FireflyIII\User;
|
||||
use Illuminate\Contracts\Auth\Authenticatable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class DisabledMFA extends Event
|
||||
{
|
||||
use SerializesModels;
|
||||
|
||||
public User $user;
|
||||
|
||||
public function __construct(null|Authenticatable|User $user)
|
||||
{
|
||||
if ($user instanceof User) {
|
||||
$this->user = $user;
|
||||
}
|
||||
}
|
||||
}
|
||||
43
app/Events/Security/EnabledMFA.php
Normal file
43
app/Events/Security/EnabledMFA.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
/*
|
||||
* EnabledMFA.php
|
||||
* Copyright (c) 2024 james@firefly-iii.org.
|
||||
*
|
||||
* This file is part of Firefly III (https://github.com/firefly-iii).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace FireflyIII\Events\Security;
|
||||
|
||||
use FireflyIII\Events\Event;
|
||||
use FireflyIII\User;
|
||||
use Illuminate\Contracts\Auth\Authenticatable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class EnabledMFA extends Event
|
||||
{
|
||||
use SerializesModels;
|
||||
|
||||
public User $user;
|
||||
|
||||
public function __construct(null|Authenticatable|User $user)
|
||||
{
|
||||
if ($user instanceof User) {
|
||||
$this->user = $user;
|
||||
}
|
||||
}
|
||||
}
|
||||
45
app/Events/Security/MFABackupFewLeft.php
Normal file
45
app/Events/Security/MFABackupFewLeft.php
Normal file
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
/*
|
||||
* EnabledMFA.php
|
||||
* Copyright (c) 2024 james@firefly-iii.org.
|
||||
*
|
||||
* This file is part of Firefly III (https://github.com/firefly-iii).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace FireflyIII\Events\Security;
|
||||
|
||||
use FireflyIII\Events\Event;
|
||||
use FireflyIII\User;
|
||||
use Illuminate\Contracts\Auth\Authenticatable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class MFABackupFewLeft extends Event
|
||||
{
|
||||
use SerializesModels;
|
||||
|
||||
public User $user;
|
||||
public int $count;
|
||||
|
||||
public function __construct(null|Authenticatable|User $user, int $count)
|
||||
{
|
||||
if ($user instanceof User) {
|
||||
$this->user = $user;
|
||||
}
|
||||
$this->count = $count;
|
||||
}
|
||||
}
|
||||
43
app/Events/Security/MFABackupNoLeft.php
Normal file
43
app/Events/Security/MFABackupNoLeft.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
/*
|
||||
* EnabledMFA.php
|
||||
* Copyright (c) 2024 james@firefly-iii.org.
|
||||
*
|
||||
* This file is part of Firefly III (https://github.com/firefly-iii).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace FireflyIII\Events\Security;
|
||||
|
||||
use FireflyIII\Events\Event;
|
||||
use FireflyIII\User;
|
||||
use Illuminate\Contracts\Auth\Authenticatable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class MFABackupNoLeft extends Event
|
||||
{
|
||||
use SerializesModels;
|
||||
|
||||
public User $user;
|
||||
|
||||
public function __construct(null|Authenticatable|User $user)
|
||||
{
|
||||
if ($user instanceof User) {
|
||||
$this->user = $user;
|
||||
}
|
||||
}
|
||||
}
|
||||
45
app/Events/Security/MFAManyFailedAttempts.php
Normal file
45
app/Events/Security/MFAManyFailedAttempts.php
Normal file
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
/*
|
||||
* EnabledMFA.php
|
||||
* Copyright (c) 2024 james@firefly-iii.org.
|
||||
*
|
||||
* This file is part of Firefly III (https://github.com/firefly-iii).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace FireflyIII\Events\Security;
|
||||
|
||||
use FireflyIII\Events\Event;
|
||||
use FireflyIII\User;
|
||||
use Illuminate\Contracts\Auth\Authenticatable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class MFAManyFailedAttempts extends Event
|
||||
{
|
||||
use SerializesModels;
|
||||
|
||||
public User $user;
|
||||
public int $count;
|
||||
|
||||
public function __construct(null|Authenticatable|User $user, int $count)
|
||||
{
|
||||
if ($user instanceof User) {
|
||||
$this->user = $user;
|
||||
}
|
||||
$this->count = $count;
|
||||
}
|
||||
}
|
||||
43
app/Events/Security/MFANewBackupCodes.php
Normal file
43
app/Events/Security/MFANewBackupCodes.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
/*
|
||||
* EnabledMFA.php
|
||||
* Copyright (c) 2024 james@firefly-iii.org.
|
||||
*
|
||||
* This file is part of Firefly III (https://github.com/firefly-iii).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace FireflyIII\Events\Security;
|
||||
|
||||
use FireflyIII\Events\Event;
|
||||
use FireflyIII\User;
|
||||
use Illuminate\Contracts\Auth\Authenticatable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class MFANewBackupCodes extends Event
|
||||
{
|
||||
use SerializesModels;
|
||||
|
||||
public User $user;
|
||||
|
||||
public function __construct(null|Authenticatable|User $user)
|
||||
{
|
||||
if ($user instanceof User) {
|
||||
$this->user = $user;
|
||||
}
|
||||
}
|
||||
}
|
||||
43
app/Events/Security/MFAUsedBackupCode.php
Normal file
43
app/Events/Security/MFAUsedBackupCode.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
/*
|
||||
* EnabledMFA.php
|
||||
* Copyright (c) 2024 james@firefly-iii.org.
|
||||
*
|
||||
* This file is part of Firefly III (https://github.com/firefly-iii).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace FireflyIII\Events\Security;
|
||||
|
||||
use FireflyIII\Events\Event;
|
||||
use FireflyIII\User;
|
||||
use Illuminate\Contracts\Auth\Authenticatable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class MFAUsedBackupCode extends Event
|
||||
{
|
||||
use SerializesModels;
|
||||
|
||||
public User $user;
|
||||
|
||||
public function __construct(null|Authenticatable|User $user)
|
||||
{
|
||||
if ($user instanceof User) {
|
||||
$this->user = $user;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -126,7 +126,7 @@ class BillFactory
|
||||
|
||||
public function findByName(string $name): ?Bill
|
||||
{
|
||||
return $this->user->bills()->where('name', 'LIKE', sprintf('%%%s%%', $name))->first();
|
||||
return $this->user->bills()->whereLike('name', sprintf('%%%s%%', $name))->first();
|
||||
}
|
||||
|
||||
public function setUser(User $user): void
|
||||
|
||||
220
app/Handlers/Events/Security/MFAHandler.php
Normal file
220
app/Handlers/Events/Security/MFAHandler.php
Normal file
@@ -0,0 +1,220 @@
|
||||
<?php
|
||||
/*
|
||||
* MFAHandler.php
|
||||
* Copyright (c) 2024 james@firefly-iii.org.
|
||||
*
|
||||
* This file is part of Firefly III (https://github.com/firefly-iii).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace FireflyIII\Handlers\Events\Security;
|
||||
|
||||
use FireflyIII\Events\Security\DisabledMFA;
|
||||
use FireflyIII\Events\Security\EnabledMFA;
|
||||
use FireflyIII\Events\Security\MFABackupFewLeft;
|
||||
use FireflyIII\Events\Security\MFABackupNoLeft;
|
||||
use FireflyIII\Events\Security\MFAManyFailedAttempts;
|
||||
use FireflyIII\Events\Security\MFANewBackupCodes;
|
||||
use FireflyIII\Events\Security\MFAUsedBackupCode;
|
||||
use FireflyIII\Notifications\Security\DisabledMFANotification;
|
||||
use FireflyIII\Notifications\Security\EnabledMFANotification;
|
||||
use FireflyIII\Notifications\Security\MFABackupFewLeftNotification;
|
||||
use FireflyIII\Notifications\Security\MFABackupNoLeftNotification;
|
||||
use FireflyIII\Notifications\Security\MFAManyFailedAttemptsNotification;
|
||||
use FireflyIII\Notifications\Security\MFAUsedBackupCodeNotification;
|
||||
use FireflyIII\Notifications\Security\NewBackupCodesNotification;
|
||||
use Illuminate\Support\Facades\Notification;
|
||||
|
||||
class MFAHandler
|
||||
{
|
||||
public function sendMFAEnabledMail(EnabledMFA $event): void
|
||||
{
|
||||
app('log')->debug(sprintf('Now in %s', __METHOD__));
|
||||
|
||||
$user = $event->user;
|
||||
|
||||
try {
|
||||
Notification::send($user, new EnabledMFANotification($user));
|
||||
} catch (\Exception $e) { // @phpstan-ignore-line
|
||||
$message = $e->getMessage();
|
||||
if (str_contains($message, 'Bcc')) {
|
||||
app('log')->warning('[Bcc] Could not send notification. Please validate your email settings, use the .env.example file as a guide.');
|
||||
|
||||
return;
|
||||
}
|
||||
if (str_contains($message, 'RFC 2822')) {
|
||||
app('log')->warning('[RFC] Could not send notification. Please validate your email settings, use the .env.example file as a guide.');
|
||||
|
||||
return;
|
||||
}
|
||||
app('log')->error($e->getMessage());
|
||||
app('log')->error($e->getTraceAsString());
|
||||
}
|
||||
}
|
||||
|
||||
public function sendNewMFABackupCodesMail(MFANewBackupCodes $event): void
|
||||
{
|
||||
app('log')->debug(sprintf('Now in %s', __METHOD__));
|
||||
|
||||
$user = $event->user;
|
||||
|
||||
try {
|
||||
Notification::send($user, new NewBackupCodesNotification($user));
|
||||
} catch (\Exception $e) { // @phpstan-ignore-line
|
||||
$message = $e->getMessage();
|
||||
if (str_contains($message, 'Bcc')) {
|
||||
app('log')->warning('[Bcc] Could not send notification. Please validate your email settings, use the .env.example file as a guide.');
|
||||
|
||||
return;
|
||||
}
|
||||
if (str_contains($message, 'RFC 2822')) {
|
||||
app('log')->warning('[RFC] Could not send notification. Please validate your email settings, use the .env.example file as a guide.');
|
||||
|
||||
return;
|
||||
}
|
||||
app('log')->error($e->getMessage());
|
||||
app('log')->error($e->getTraceAsString());
|
||||
}
|
||||
}
|
||||
|
||||
public function sendBackupFewLeftMail(MFABackupFewLeft $event): void
|
||||
{
|
||||
app('log')->debug(sprintf('Now in %s', __METHOD__));
|
||||
|
||||
$user = $event->user;
|
||||
$count = $event->count;
|
||||
|
||||
try {
|
||||
Notification::send($user, new MFABackupFewLeftNotification($user, $count));
|
||||
} catch (\Exception $e) { // @phpstan-ignore-line
|
||||
$message = $e->getMessage();
|
||||
if (str_contains($message, 'Bcc')) {
|
||||
app('log')->warning('[Bcc] Could not send notification. Please validate your email settings, use the .env.example file as a guide.');
|
||||
|
||||
return;
|
||||
}
|
||||
if (str_contains($message, 'RFC 2822')) {
|
||||
app('log')->warning('[RFC] Could not send notification. Please validate your email settings, use the .env.example file as a guide.');
|
||||
|
||||
return;
|
||||
}
|
||||
app('log')->error($e->getMessage());
|
||||
app('log')->error($e->getTraceAsString());
|
||||
}
|
||||
}
|
||||
|
||||
public function sendMFAFailedAttemptsMail(MFAManyFailedAttempts $event): void
|
||||
{
|
||||
app('log')->debug(sprintf('Now in %s', __METHOD__));
|
||||
|
||||
$user = $event->user;
|
||||
$count = $event->count;
|
||||
|
||||
try {
|
||||
Notification::send($user, new MFAManyFailedAttemptsNotification($user, $count));
|
||||
} catch (\Exception $e) { // @phpstan-ignore-line
|
||||
$message = $e->getMessage();
|
||||
if (str_contains($message, 'Bcc')) {
|
||||
app('log')->warning('[Bcc] Could not send notification. Please validate your email settings, use the .env.example file as a guide.');
|
||||
|
||||
return;
|
||||
}
|
||||
if (str_contains($message, 'RFC 2822')) {
|
||||
app('log')->warning('[RFC] Could not send notification. Please validate your email settings, use the .env.example file as a guide.');
|
||||
|
||||
return;
|
||||
}
|
||||
app('log')->error($e->getMessage());
|
||||
app('log')->error($e->getTraceAsString());
|
||||
}
|
||||
}
|
||||
|
||||
public function sendBackupNoLeftMail(MFABackupNoLeft $event): void
|
||||
{
|
||||
app('log')->debug(sprintf('Now in %s', __METHOD__));
|
||||
|
||||
$user = $event->user;
|
||||
|
||||
try {
|
||||
Notification::send($user, new MFABackupNoLeftNotification($user));
|
||||
} catch (\Exception $e) { // @phpstan-ignore-line
|
||||
$message = $e->getMessage();
|
||||
if (str_contains($message, 'Bcc')) {
|
||||
app('log')->warning('[Bcc] Could not send notification. Please validate your email settings, use the .env.example file as a guide.');
|
||||
|
||||
return;
|
||||
}
|
||||
if (str_contains($message, 'RFC 2822')) {
|
||||
app('log')->warning('[RFC] Could not send notification. Please validate your email settings, use the .env.example file as a guide.');
|
||||
|
||||
return;
|
||||
}
|
||||
app('log')->error($e->getMessage());
|
||||
app('log')->error($e->getTraceAsString());
|
||||
}
|
||||
}
|
||||
|
||||
public function sendUsedBackupCodeMail(MFAUsedBackupCode $event): void
|
||||
{
|
||||
app('log')->debug(sprintf('Now in %s', __METHOD__));
|
||||
|
||||
$user = $event->user;
|
||||
|
||||
try {
|
||||
Notification::send($user, new MFAUsedBackupCodeNotification($user));
|
||||
} catch (\Exception $e) { // @phpstan-ignore-line
|
||||
$message = $e->getMessage();
|
||||
if (str_contains($message, 'Bcc')) {
|
||||
app('log')->warning('[Bcc] Could not send notification. Please validate your email settings, use the .env.example file as a guide.');
|
||||
|
||||
return;
|
||||
}
|
||||
if (str_contains($message, 'RFC 2822')) {
|
||||
app('log')->warning('[RFC] Could not send notification. Please validate your email settings, use the .env.example file as a guide.');
|
||||
|
||||
return;
|
||||
}
|
||||
app('log')->error($e->getMessage());
|
||||
app('log')->error($e->getTraceAsString());
|
||||
}
|
||||
}
|
||||
|
||||
public function sendMFADisabledMail(DisabledMFA $event): void
|
||||
{
|
||||
app('log')->debug(sprintf('Now in %s', __METHOD__));
|
||||
|
||||
$user = $event->user;
|
||||
|
||||
try {
|
||||
Notification::send($user, new DisabledMFANotification($user));
|
||||
} catch (\Exception $e) { // @phpstan-ignore-line
|
||||
$message = $e->getMessage();
|
||||
if (str_contains($message, 'Bcc')) {
|
||||
app('log')->warning('[Bcc] Could not send notification. Please validate your email settings, use the .env.example file as a guide.');
|
||||
|
||||
return;
|
||||
}
|
||||
if (str_contains($message, 'RFC 2822')) {
|
||||
app('log')->warning('[RFC] Could not send notification. Please validate your email settings, use the .env.example file as a guide.');
|
||||
|
||||
return;
|
||||
}
|
||||
app('log')->error($e->getMessage());
|
||||
app('log')->error($e->getTraceAsString());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -200,7 +200,7 @@ trait MetaCollection
|
||||
|
||||
$this->joinMetaDataTables();
|
||||
$this->query->where('journal_meta.name', '=', 'internal_reference');
|
||||
$this->query->where('journal_meta.data', 'NOT LIKE', sprintf('%%%s%%', $internalReference));
|
||||
$this->query->whereNotLike('journal_meta.data', sprintf('%%%s%%', $internalReference));
|
||||
|
||||
return $this;
|
||||
}
|
||||
@@ -221,7 +221,7 @@ trait MetaCollection
|
||||
|
||||
$this->joinMetaDataTables();
|
||||
$this->query->where('journal_meta.name', '=', 'external_id');
|
||||
$this->query->where('journal_meta.data', 'LIKE', sprintf('%%%s%%', $externalId));
|
||||
$this->query->whereLike('journal_meta.data', sprintf('%%%s%%', $externalId));
|
||||
|
||||
return $this;
|
||||
}
|
||||
@@ -233,7 +233,7 @@ trait MetaCollection
|
||||
|
||||
$this->joinMetaDataTables();
|
||||
$this->query->where('journal_meta.name', '=', 'external_id');
|
||||
$this->query->where('journal_meta.data', 'NOT LIKE', sprintf('%%%s%%', $externalId));
|
||||
$this->query->whereNotLike('journal_meta.data', sprintf('%%%s%%', $externalId));
|
||||
|
||||
return $this;
|
||||
}
|
||||
@@ -245,7 +245,7 @@ trait MetaCollection
|
||||
|
||||
$this->joinMetaDataTables();
|
||||
$this->query->where('journal_meta.name', '=', 'external_id');
|
||||
$this->query->where('journal_meta.data', 'NOT LIKE', sprintf('%%%s"', $externalId));
|
||||
$this->query->whereNotLike('journal_meta.data', sprintf('%%%s"', $externalId));
|
||||
|
||||
return $this;
|
||||
}
|
||||
@@ -257,7 +257,7 @@ trait MetaCollection
|
||||
|
||||
$this->joinMetaDataTables();
|
||||
$this->query->where('journal_meta.name', '=', 'external_id');
|
||||
$this->query->where('journal_meta.data', 'LIKE', sprintf('"%s%%', $externalId));
|
||||
$this->query->whereLike('journal_meta.data', sprintf('"%s%%', $externalId));
|
||||
|
||||
return $this;
|
||||
}
|
||||
@@ -269,7 +269,7 @@ trait MetaCollection
|
||||
|
||||
$this->joinMetaDataTables();
|
||||
$this->query->where('journal_meta.name', '=', 'external_id');
|
||||
$this->query->where('journal_meta.data', 'LIKE', sprintf('%%%s"', $externalId));
|
||||
$this->query->whereLike('journal_meta.data', sprintf('%%%s"', $externalId));
|
||||
|
||||
return $this;
|
||||
}
|
||||
@@ -281,7 +281,7 @@ trait MetaCollection
|
||||
|
||||
$this->joinMetaDataTables();
|
||||
$this->query->where('journal_meta.name', '=', 'external_id');
|
||||
$this->query->where('journal_meta.data', 'LIKE', sprintf('"%s%%', $externalId));
|
||||
$this->query->whereLike('journal_meta.data', sprintf('"%s%%', $externalId));
|
||||
|
||||
return $this;
|
||||
}
|
||||
@@ -292,7 +292,7 @@ trait MetaCollection
|
||||
$url = (string)json_encode($url);
|
||||
$url = str_replace('\\', '\\\\', trim($url, '"'));
|
||||
$this->query->where('journal_meta.name', '=', 'external_url');
|
||||
$this->query->where('journal_meta.data', 'LIKE', sprintf('%%%s%%', $url));
|
||||
$this->query->whereLike('journal_meta.data', sprintf('%%%s%%', $url));
|
||||
|
||||
return $this;
|
||||
}
|
||||
@@ -303,7 +303,7 @@ trait MetaCollection
|
||||
$url = (string)json_encode($url);
|
||||
$url = str_replace('\\', '\\\\', trim($url, '"'));
|
||||
$this->query->where('journal_meta.name', '=', 'external_url');
|
||||
$this->query->where('journal_meta.data', 'NOT LIKE', sprintf('%%%s%%', $url));
|
||||
$this->query->whereNotLike('journal_meta.data', sprintf('%%%s%%', $url));
|
||||
|
||||
return $this;
|
||||
}
|
||||
@@ -314,7 +314,7 @@ trait MetaCollection
|
||||
$url = (string)json_encode($url);
|
||||
$url = str_replace('\\', '\\\\', ltrim($url, '"'));
|
||||
$this->query->where('journal_meta.name', '=', 'external_url');
|
||||
$this->query->where('journal_meta.data', 'NOT LIKE', sprintf('%%%s', $url));
|
||||
$this->query->whereNotLike('journal_meta.data', sprintf('%%%s', $url));
|
||||
|
||||
return $this;
|
||||
}
|
||||
@@ -327,7 +327,7 @@ trait MetaCollection
|
||||
// var_dump($url);
|
||||
|
||||
$this->query->where('journal_meta.name', '=', 'external_url');
|
||||
$this->query->where('journal_meta.data', 'NOT LIKE', sprintf('%s%%', $url));
|
||||
$this->query->whereNotLike('journal_meta.data', sprintf('%s%%', $url));
|
||||
|
||||
return $this;
|
||||
}
|
||||
@@ -338,7 +338,7 @@ trait MetaCollection
|
||||
$url = (string)json_encode($url);
|
||||
$url = str_replace('\\', '\\\\', ltrim($url, '"'));
|
||||
$this->query->where('journal_meta.name', '=', 'external_url');
|
||||
$this->query->where('journal_meta.data', 'LIKE', sprintf('%%%s', $url));
|
||||
$this->query->whereLike('journal_meta.data', sprintf('%%%s', $url));
|
||||
|
||||
return $this;
|
||||
}
|
||||
@@ -351,7 +351,7 @@ trait MetaCollection
|
||||
// var_dump($url);
|
||||
|
||||
$this->query->where('journal_meta.name', '=', 'external_url');
|
||||
$this->query->where('journal_meta.data', 'LIKE', sprintf('%s%%', $url));
|
||||
$this->query->whereLike('journal_meta.data', sprintf('%s%%', $url));
|
||||
|
||||
return $this;
|
||||
}
|
||||
@@ -404,7 +404,7 @@ trait MetaCollection
|
||||
|
||||
$this->joinMetaDataTables();
|
||||
$this->query->where('journal_meta.name', '=', 'internal_reference');
|
||||
$this->query->where('journal_meta.data', 'LIKE', sprintf('%%%s%%', $internalReference));
|
||||
$this->query->whereLike('journal_meta.data', sprintf('%%%s%%', $internalReference));
|
||||
|
||||
return $this;
|
||||
}
|
||||
@@ -416,7 +416,7 @@ trait MetaCollection
|
||||
|
||||
$this->joinMetaDataTables();
|
||||
$this->query->where('journal_meta.name', '=', 'internal_reference');
|
||||
$this->query->where('journal_meta.data', 'NOT LIKE', sprintf('%%%s%%', $internalReference));
|
||||
$this->query->whereNotLike('journal_meta.data', sprintf('%%%s%%', $internalReference));
|
||||
|
||||
return $this;
|
||||
}
|
||||
@@ -428,7 +428,7 @@ trait MetaCollection
|
||||
|
||||
$this->joinMetaDataTables();
|
||||
$this->query->where('journal_meta.name', '=', 'internal_reference');
|
||||
$this->query->where('journal_meta.data', 'NOT LIKE', sprintf('%%%s"', $internalReference));
|
||||
$this->query->whereNotLike('journal_meta.data', sprintf('%%%s"', $internalReference));
|
||||
|
||||
return $this;
|
||||
}
|
||||
@@ -440,7 +440,7 @@ trait MetaCollection
|
||||
|
||||
$this->joinMetaDataTables();
|
||||
$this->query->where('journal_meta.name', '=', 'internal_reference');
|
||||
$this->query->where('journal_meta.data', 'LIKE', sprintf('"%s%%', $internalReference));
|
||||
$this->query->whereLike('journal_meta.data', sprintf('"%s%%', $internalReference));
|
||||
|
||||
return $this;
|
||||
}
|
||||
@@ -452,7 +452,7 @@ trait MetaCollection
|
||||
|
||||
$this->joinMetaDataTables();
|
||||
$this->query->where('journal_meta.name', '=', 'internal_reference');
|
||||
$this->query->where('journal_meta.data', 'LIKE', sprintf('%%%s"', $internalReference));
|
||||
$this->query->whereLike('journal_meta.data', sprintf('%%%s"', $internalReference));
|
||||
|
||||
return $this;
|
||||
}
|
||||
@@ -464,7 +464,7 @@ trait MetaCollection
|
||||
|
||||
$this->joinMetaDataTables();
|
||||
$this->query->where('journal_meta.name', '=', 'internal_reference');
|
||||
$this->query->where('journal_meta.data', 'LIKE', sprintf('"%s%%', $internalReference));
|
||||
$this->query->whereLike('journal_meta.data', sprintf('"%s%%', $internalReference));
|
||||
|
||||
return $this;
|
||||
}
|
||||
@@ -472,7 +472,7 @@ trait MetaCollection
|
||||
public function notesContain(string $value): GroupCollectorInterface
|
||||
{
|
||||
$this->withNotes();
|
||||
$this->query->where('notes.text', 'LIKE', sprintf('%%%s%%', $value));
|
||||
$this->query->whereLike('notes.text', sprintf('%%%s%%', $value));
|
||||
|
||||
return $this;
|
||||
}
|
||||
@@ -502,7 +502,7 @@ trait MetaCollection
|
||||
$this->withNotes();
|
||||
$this->query->where(static function (Builder $q) use ($value): void { // @phpstan-ignore-line
|
||||
$q->whereNull('notes.text');
|
||||
$q->orWhere('notes.text', 'NOT LIKE', sprintf('%%%s%%', $value));
|
||||
$q->orWhereNotLike('notes.text', sprintf('%%%s%%', $value));
|
||||
});
|
||||
|
||||
return $this;
|
||||
@@ -513,7 +513,7 @@ trait MetaCollection
|
||||
$this->withNotes();
|
||||
$this->query->where(static function (Builder $q) use ($value): void { // @phpstan-ignore-line
|
||||
$q->whereNull('notes.text');
|
||||
$q->orWhere('notes.text', 'NOT LIKE', sprintf('%%%s', $value));
|
||||
$q->orWhereNotLike('notes.text', sprintf('%%%s', $value));
|
||||
});
|
||||
|
||||
return $this;
|
||||
@@ -524,7 +524,7 @@ trait MetaCollection
|
||||
$this->withNotes();
|
||||
$this->query->where(static function (Builder $q) use ($value): void { // @phpstan-ignore-line
|
||||
$q->whereNull('notes.text');
|
||||
$q->orWhere('notes.text', 'NOT LIKE', sprintf('%s%%', $value));
|
||||
$q->orWhereNotLike('notes.text', sprintf('%s%%', $value));
|
||||
});
|
||||
|
||||
return $this;
|
||||
@@ -533,7 +533,7 @@ trait MetaCollection
|
||||
public function notesEndWith(string $value): GroupCollectorInterface
|
||||
{
|
||||
$this->withNotes();
|
||||
$this->query->where('notes.text', 'LIKE', sprintf('%%%s', $value));
|
||||
$this->query->whereLike('notes.text', sprintf('%%%s', $value));
|
||||
|
||||
return $this;
|
||||
}
|
||||
@@ -560,7 +560,7 @@ trait MetaCollection
|
||||
public function notesStartWith(string $value): GroupCollectorInterface
|
||||
{
|
||||
$this->withNotes();
|
||||
$this->query->where('notes.text', 'LIKE', sprintf('%s%%', $value));
|
||||
$this->query->whereLike('notes.text', sprintf('%s%%', $value));
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
@@ -156,7 +156,7 @@ class GroupCollector implements GroupCollectorInterface
|
||||
static function (EloquentBuilder $q1) use ($array): void {
|
||||
foreach ($array as $word) {
|
||||
$keyword = sprintf('%%%s', $word);
|
||||
$q1->where('transaction_journals.description', 'NOT LIKE', $keyword);
|
||||
$q1->whereNotLike('transaction_journals.description', $keyword);
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -164,7 +164,7 @@ class GroupCollector implements GroupCollectorInterface
|
||||
static function (EloquentBuilder $q2) use ($array): void {
|
||||
foreach ($array as $word) {
|
||||
$keyword = sprintf('%%%s', $word);
|
||||
$q2->where('transaction_groups.title', 'NOT LIKE', $keyword);
|
||||
$q2->whereNotLike('transaction_groups.title', $keyword);
|
||||
$q2->orWhereNull('transaction_groups.title');
|
||||
}
|
||||
}
|
||||
@@ -183,7 +183,7 @@ class GroupCollector implements GroupCollectorInterface
|
||||
static function (EloquentBuilder $q1) use ($array): void {
|
||||
foreach ($array as $word) {
|
||||
$keyword = sprintf('%s%%', $word);
|
||||
$q1->where('transaction_journals.description', 'NOT LIKE', $keyword);
|
||||
$q1->whereNotLike('transaction_journals.description', $keyword);
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -191,7 +191,7 @@ class GroupCollector implements GroupCollectorInterface
|
||||
static function (EloquentBuilder $q2) use ($array): void {
|
||||
foreach ($array as $word) {
|
||||
$keyword = sprintf('%s%%', $word);
|
||||
$q2->where('transaction_groups.title', 'NOT LIKE', $keyword);
|
||||
$q2->whereNotLike('transaction_groups.title', $keyword);
|
||||
$q2->orWhereNull('transaction_groups.title');
|
||||
}
|
||||
}
|
||||
@@ -210,7 +210,7 @@ class GroupCollector implements GroupCollectorInterface
|
||||
static function (EloquentBuilder $q1) use ($array): void {
|
||||
foreach ($array as $word) {
|
||||
$keyword = sprintf('%%%s', $word);
|
||||
$q1->where('transaction_journals.description', 'LIKE', $keyword);
|
||||
$q1->whereLike('transaction_journals.description', $keyword);
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -218,7 +218,7 @@ class GroupCollector implements GroupCollectorInterface
|
||||
static function (EloquentBuilder $q2) use ($array): void {
|
||||
foreach ($array as $word) {
|
||||
$keyword = sprintf('%%%s', $word);
|
||||
$q2->where('transaction_groups.title', 'LIKE', $keyword);
|
||||
$q2->whereLike('transaction_groups.title', $keyword);
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -265,7 +265,7 @@ class GroupCollector implements GroupCollectorInterface
|
||||
static function (EloquentBuilder $q1) use ($array): void {
|
||||
foreach ($array as $word) {
|
||||
$keyword = sprintf('%s%%', $word);
|
||||
$q1->where('transaction_journals.description', 'LIKE', $keyword);
|
||||
$q1->whereLike('transaction_journals.description', $keyword);
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -273,7 +273,7 @@ class GroupCollector implements GroupCollectorInterface
|
||||
static function (EloquentBuilder $q2) use ($array): void {
|
||||
foreach ($array as $word) {
|
||||
$keyword = sprintf('%s%%', $word);
|
||||
$q2->where('transaction_groups.title', 'LIKE', $keyword);
|
||||
$q2->whereLike('transaction_groups.title', $keyword);
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -379,7 +379,7 @@ class GroupCollector implements GroupCollectorInterface
|
||||
static function (EloquentBuilder $q1) use ($array): void {
|
||||
foreach ($array as $word) {
|
||||
$keyword = sprintf('%%%s%%', $word);
|
||||
$q1->where('transaction_journals.description', 'NOT LIKE', $keyword);
|
||||
$q1->whereNotLike('transaction_journals.description', $keyword);
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -387,7 +387,7 @@ class GroupCollector implements GroupCollectorInterface
|
||||
static function (EloquentBuilder $q2) use ($array): void {
|
||||
foreach ($array as $word) {
|
||||
$keyword = sprintf('%%%s%%', $word);
|
||||
$q2->where('transaction_groups.title', 'NOT LIKE', $keyword);
|
||||
$q2->whereNotLike('transaction_groups.title', $keyword);
|
||||
$q2->orWhereNull('transaction_groups.title');
|
||||
}
|
||||
}
|
||||
@@ -944,7 +944,7 @@ class GroupCollector implements GroupCollectorInterface
|
||||
static function (EloquentBuilder $q1) use ($array): void {
|
||||
foreach ($array as $word) {
|
||||
$keyword = sprintf('%%%s%%', $word);
|
||||
$q1->where('transaction_journals.description', 'LIKE', $keyword);
|
||||
$q1->whereLike('transaction_journals.description', $keyword);
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -952,7 +952,7 @@ class GroupCollector implements GroupCollectorInterface
|
||||
static function (EloquentBuilder $q2) use ($array): void {
|
||||
foreach ($array as $word) {
|
||||
$keyword = sprintf('%%%s%%', $word);
|
||||
$q2->where('transaction_groups.title', 'LIKE', $keyword);
|
||||
$q2->whereLike('transaction_groups.title', $keyword);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@@ -103,7 +103,8 @@ class PopupReport implements PopupReportInterface
|
||||
|
||||
/** @var GroupCollectorInterface $collector */
|
||||
$collector = app(GroupCollectorInterface::class);
|
||||
$collector->setAccounts($attributes['accounts'])
|
||||
$collector
|
||||
->setAccounts($attributes['accounts'])
|
||||
->withAccountInformation()
|
||||
->withBudgetInformation()
|
||||
->withCategoryInformation()
|
||||
@@ -113,11 +114,10 @@ class PopupReport implements PopupReportInterface
|
||||
if (null !== $currency) {
|
||||
$collector->setCurrency($currency);
|
||||
}
|
||||
|
||||
if (null === $budget->id) {
|
||||
if (null === $budget->id || 0 === $budget->id) {
|
||||
$collector->setTypes([TransactionType::WITHDRAWAL])->withoutBudget();
|
||||
}
|
||||
if (null !== $budget->id) {
|
||||
if (null !== $budget->id && 0 !== $budget->id) {
|
||||
$collector->setBudget($budget);
|
||||
}
|
||||
|
||||
|
||||
@@ -77,12 +77,16 @@ class LoginController extends Controller
|
||||
*/
|
||||
public function login(Request $request): JsonResponse|RedirectResponse
|
||||
{
|
||||
Log::channel('audit')->info(sprintf('User is trying to login using "%s"', $request->get($this->username())));
|
||||
$username = $request->get($this->username());
|
||||
Log::channel('audit')->info(sprintf('User is trying to login using "%s"', $username));
|
||||
app('log')->debug('User is trying to login.');
|
||||
|
||||
try {
|
||||
$this->validateLogin($request);
|
||||
} catch (ValidationException $e) {
|
||||
// basic validation exception.
|
||||
// report the failed login to the user if the count is 2 or 5.
|
||||
// TODO here be warning.
|
||||
return redirect(route('login'))
|
||||
->withErrors(
|
||||
[
|
||||
|
||||
@@ -23,6 +23,10 @@ declare(strict_types=1);
|
||||
|
||||
namespace FireflyIII\Http\Controllers\Auth;
|
||||
|
||||
use FireflyIII\Events\Security\MFABackupFewLeft;
|
||||
use FireflyIII\Events\Security\MFABackupNoLeft;
|
||||
use FireflyIII\Events\Security\MFAManyFailedAttempts;
|
||||
use FireflyIII\Events\Security\MFAUsedBackupCode;
|
||||
use FireflyIII\Http\Controllers\Controller;
|
||||
use FireflyIII\User;
|
||||
use Illuminate\Contracts\View\Factory;
|
||||
@@ -30,6 +34,7 @@ use Illuminate\Contracts\View\View;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Routing\Redirector;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use PragmaRX\Google2FALaravel\Support\Authenticator;
|
||||
|
||||
/**
|
||||
@@ -72,10 +77,26 @@ class TwoFactorController extends Controller
|
||||
/** @var Authenticator $authenticator */
|
||||
$authenticator = app(Authenticator::class)->boot($request);
|
||||
|
||||
// if not OK, save error.
|
||||
if (!$authenticator->isAuthenticated()) {
|
||||
$user = auth()->user();
|
||||
$this->addToMFAFailureCounter();
|
||||
$counter = $this->getMFAFailureCounter();
|
||||
if (3 === $counter || 10 === $counter) {
|
||||
// do not reset MFA failure counter, but DO send a warning to the user.
|
||||
Log::channel('audit')->info(sprintf('User "%s" has had %d failed MFA attempts.', $user->email, $counter));
|
||||
event(new MFAManyFailedAttempts($user, $counter));
|
||||
}
|
||||
unset($user);
|
||||
}
|
||||
|
||||
if ($authenticator->isAuthenticated()) {
|
||||
// save MFA in preferences
|
||||
$this->addToMFAHistory($mfaCode);
|
||||
|
||||
// reset failure count
|
||||
$this->resetMFAFailureCounter();
|
||||
|
||||
// otp auth success!
|
||||
return redirect(route('home'));
|
||||
}
|
||||
@@ -85,7 +106,14 @@ class TwoFactorController extends Controller
|
||||
$this->removeFromBackupCodes($mfaCode);
|
||||
$authenticator->login();
|
||||
|
||||
// reset failure count
|
||||
$this->resetMFAFailureCounter();
|
||||
|
||||
session()->flash('info', trans('firefly.mfa_backup_code'));
|
||||
// send user notification.
|
||||
$user = auth()->user();
|
||||
Log::channel('audit')->info(sprintf('User "%s" has used a backup code.', $user->email));
|
||||
event(new MFAUsedBackupCode($user));
|
||||
|
||||
return redirect(route('home'));
|
||||
}
|
||||
@@ -175,6 +203,42 @@ class TwoFactorController extends Controller
|
||||
$list = [];
|
||||
}
|
||||
$newList = array_values(array_diff($list, [$mfaCode]));
|
||||
|
||||
// if the list is 3 or less, send a notification.
|
||||
if (count($newList) <= 3 && count($newList) > 0) {
|
||||
$user = auth()->user();
|
||||
Log::channel('audit')->info(sprintf('User "%s" has used a backup code. They have %d backup codes left.', $user->email, count($newList)));
|
||||
event(new MFABackupFewLeft($user, count($newList)));
|
||||
}
|
||||
// if the list is empty, send notification
|
||||
if (0 === count($newList)) {
|
||||
$user = auth()->user();
|
||||
Log::channel('audit')->info(sprintf('User "%s" has used their last backup code.', $user->email));
|
||||
event(new MFABackupNoLeft($user));
|
||||
}
|
||||
|
||||
app('preferences')->set('mfa_recovery', $newList);
|
||||
}
|
||||
|
||||
private function addToMFAFailureCounter(): void
|
||||
{
|
||||
$preference = (int) app('preferences')->get('mfa_failure_count', 0)->data;
|
||||
++$preference;
|
||||
Log::channel('audit')->info(sprintf('MFA failure count is set to %d.', $preference));
|
||||
app('preferences')->set('mfa_failure_count', $preference);
|
||||
}
|
||||
|
||||
private function getMFAFailureCounter(): int
|
||||
{
|
||||
$value = (int) app('preferences')->get('mfa_failure_count', 0)->data;
|
||||
Log::channel('audit')->info(sprintf('MFA failure count is %d.', $value));
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
private function resetMFAFailureCounter(): void
|
||||
{
|
||||
app('preferences')->set('mfa_failure_count', 0);
|
||||
Log::channel('audit')->info('MFA failure count is set to zero.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@ class DebugController extends Controller
|
||||
|
||||
// also do some recalculations.
|
||||
Artisan::call('firefly-iii:trigger-credit-recalculation');
|
||||
AccountBalanceCalculator::forceRecalculateAll();
|
||||
AccountBalanceCalculator::recalculateAll(true);
|
||||
|
||||
try {
|
||||
Artisan::call('twig:clean');
|
||||
|
||||
@@ -107,7 +107,6 @@ class JavascriptController extends Controller
|
||||
$lang = $pref->data;
|
||||
$dateRange = $this->getDateRangeConfig();
|
||||
$uid = substr(hash('sha256', sprintf('%s-%s-%s', (string)config('app.key'), auth()->user()->id, auth()->user()->email)), 0, 12);
|
||||
|
||||
$data = [
|
||||
'currencyCode' => $currency->code,
|
||||
'currencySymbol' => $currency->symbol,
|
||||
|
||||
@@ -65,6 +65,16 @@ class FrontpageController extends Controller
|
||||
$info[] = $entry;
|
||||
}
|
||||
}
|
||||
|
||||
// sort by current percentage (lowest at the top)
|
||||
uasort(
|
||||
$info,
|
||||
static function (array $a, array $b) {
|
||||
return $a['percentage'] <=> $b['percentage'];
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
$html = '';
|
||||
if (0 !== count($info)) {
|
||||
try {
|
||||
|
||||
342
app/Http/Controllers/Profile/MfaController.php
Normal file
342
app/Http/Controllers/Profile/MfaController.php
Normal file
@@ -0,0 +1,342 @@
|
||||
<?php
|
||||
/*
|
||||
* MfaController.php
|
||||
* Copyright (c) 2024 james@firefly-iii.org.
|
||||
*
|
||||
* This file is part of Firefly III (https://github.com/firefly-iii).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace FireflyIII\Http\Controllers\Profile;
|
||||
|
||||
use FireflyIII\Events\Security\DisabledMFA;
|
||||
use FireflyIII\Events\Security\EnabledMFA;
|
||||
use FireflyIII\Events\Security\MFANewBackupCodes;
|
||||
use FireflyIII\Exceptions\FireflyException;
|
||||
use FireflyIII\Http\Controllers\Controller;
|
||||
use FireflyIII\Http\Middleware\IsDemoUser;
|
||||
use FireflyIII\Http\Requests\ExistingTokenFormRequest;
|
||||
use FireflyIII\Http\Requests\TokenFormRequest;
|
||||
use FireflyIII\Repositories\User\UserRepositoryInterface;
|
||||
use FireflyIII\User;
|
||||
use Illuminate\Contracts\View\Factory;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Routing\Redirector;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\View\View;
|
||||
use PragmaRX\Recovery\Recovery;
|
||||
|
||||
/**
|
||||
* Class MfaController
|
||||
*
|
||||
* Enable MFA Flow:
|
||||
*
|
||||
* Page 1 (GET): Show QR code and the manual code. Secret keeps rotating.
|
||||
* POST: store secret, store response, validate password.
|
||||
* ---
|
||||
* Page 3 (GET): Confirm 2FA status and show recovery codes.
|
||||
* Same page as page 1, but when secret is present.
|
||||
*/
|
||||
class MfaController extends Controller
|
||||
{
|
||||
protected bool $internalAuth;
|
||||
|
||||
/**
|
||||
* ProfileController constructor.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
$this->middleware(
|
||||
static function ($request, $next) {
|
||||
app('view')->share('title', (string) trans('firefly.profile'));
|
||||
app('view')->share('mainTitleIcon', 'fa-user');
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
);
|
||||
$authGuard = config('firefly.authentication_guard');
|
||||
$this->internalAuth = 'web' === $authGuard;
|
||||
app('log')->debug(sprintf('ProfileController::__construct(). Authentication guard is "%s"', $authGuard));
|
||||
|
||||
$this->middleware(IsDemoUser::class)->except(['index']);
|
||||
|
||||
}
|
||||
|
||||
public function index(): Factory|RedirectResponse|View
|
||||
{
|
||||
if (!$this->internalAuth) {
|
||||
request()->session()->flash('error', trans('firefly.external_user_mgt_disabled'));
|
||||
|
||||
return redirect(route('profile.index'));
|
||||
}
|
||||
|
||||
$subTitle = (string)trans('firefly.mfa_index_title');
|
||||
$subTitleIcon = 'fa-calculator';
|
||||
$enabledMFA = null !== auth()->user()->mfa_secret;
|
||||
|
||||
return view('profile.mfa.index')->with(compact('subTitle', 'subTitleIcon', 'enabledMFA'));
|
||||
}
|
||||
|
||||
public function disableMFA(Request $request): Factory|RedirectResponse|View
|
||||
{
|
||||
if (!$this->internalAuth) {
|
||||
request()->session()->flash('error', trans('firefly.external_user_mgt_disabled'));
|
||||
|
||||
return redirect(route('profile.index'));
|
||||
}
|
||||
$enabledMFA = null !== auth()->user()->mfa_secret;
|
||||
if (false === $enabledMFA) {
|
||||
request()->session()->flash('info', trans('firefly.mfa_already_disabled'));
|
||||
|
||||
return redirect(route('profile.index'));
|
||||
}
|
||||
$subTitle = (string)trans('firefly.mfa_index_title');
|
||||
$subTitleIcon = 'fa-calculator';
|
||||
|
||||
return view('profile.mfa.disable-mfa')->with(compact('subTitle', 'subTitleIcon', 'enabledMFA'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete 2FA routine.
|
||||
*/
|
||||
public function disableMFAPost(ExistingTokenFormRequest $request): Redirector|RedirectResponse
|
||||
{
|
||||
if (!$this->internalAuth) {
|
||||
$request->session()->flash('error', trans('firefly.external_user_mgt_disabled'));
|
||||
|
||||
return redirect(route('profile.index'));
|
||||
}
|
||||
|
||||
/** @var UserRepositoryInterface $repository */
|
||||
$repository = app(UserRepositoryInterface::class);
|
||||
|
||||
/** @var User $user */
|
||||
$user = auth()->user();
|
||||
|
||||
app('preferences')->delete('temp-mfa-secret');
|
||||
app('preferences')->delete('temp-mfa-codes');
|
||||
$repository->setMFACode($user, null);
|
||||
app('preferences')->mark();
|
||||
|
||||
session()->flash('success', (string) trans('firefly.pref_two_factor_auth_disabled'));
|
||||
session()->flash('info', (string) trans('firefly.pref_two_factor_auth_remove_it'));
|
||||
|
||||
// also logout current 2FA tokens.
|
||||
$cookieName = config('google2fa.cookie_name', 'google2fa_token');
|
||||
\Cookie::forget($cookieName);
|
||||
|
||||
// send user notification.
|
||||
Log::channel('audit')->info(sprintf('User "%s" has disabled MFA', $user->email));
|
||||
event(new DisabledMFA($user));
|
||||
|
||||
return redirect(route('profile.index'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable 2FA screen.
|
||||
*/
|
||||
public function enableMFA(Request $request): Redirector|RedirectResponse|View
|
||||
{
|
||||
if (!$this->internalAuth) {
|
||||
$request->session()->flash('error', trans('firefly.external_user_mgt_disabled'));
|
||||
|
||||
return redirect(route('profile.index'));
|
||||
}
|
||||
|
||||
/** @var User $user */
|
||||
$user = auth()->user();
|
||||
$enabledMFA = null !== $user->mfa_secret;
|
||||
|
||||
// If FF3 already has a secret, just set the two-factor auth enabled to 1,
|
||||
// and let the user continue with the existing secret.
|
||||
if ($enabledMFA) {
|
||||
session()->flash('info', (string) trans('firefly.2fa_already_enabled'));
|
||||
|
||||
return redirect(route('profile.index'));
|
||||
}
|
||||
|
||||
$domain = $this->getDomain();
|
||||
$secret = \Google2FA::generateSecretKey();
|
||||
$image = \Google2FA::getQRCodeInline($domain, auth()->user()->email, (string) $secret);
|
||||
|
||||
app('preferences')->set('temp-mfa-secret', $secret);
|
||||
|
||||
|
||||
|
||||
return view('profile.mfa.enable-mfa', compact('image', 'secret'));
|
||||
|
||||
}
|
||||
|
||||
public function backupCodesPost(ExistingTokenFormRequest $request): Redirector|RedirectResponse|View
|
||||
{
|
||||
if (!$this->internalAuth) {
|
||||
$request->session()->flash('error', trans('firefly.external_user_mgt_disabled'));
|
||||
|
||||
return redirect(route('profile.index'));
|
||||
}
|
||||
$enabledMFA = null !== auth()->user()->mfa_secret;
|
||||
if (false === $enabledMFA) {
|
||||
request()->session()->flash('info', trans('firefly.mfa_not_enabled'));
|
||||
|
||||
return redirect(route('profile.index'));
|
||||
}
|
||||
// generate recovery codes:
|
||||
$recovery = app(Recovery::class);
|
||||
$recoveryCodes = $recovery->lowercase()
|
||||
->setCount(8) // Generate 8 codes
|
||||
->setBlocks(2) // Every code must have 2 blocks
|
||||
->setChars(6) // Each block must have 6 chars
|
||||
->toArray()
|
||||
;
|
||||
$codes = implode("\r\n", $recoveryCodes);
|
||||
|
||||
app('preferences')->set('mfa_recovery', $recoveryCodes);
|
||||
app('preferences')->mark();
|
||||
|
||||
// send user notification.
|
||||
$user = auth()->user();
|
||||
Log::channel('audit')->info(sprintf('User "%s" has generated new backup codes.', $user->email));
|
||||
event(new MFANewBackupCodes($user));
|
||||
|
||||
return view('profile.mfa.backup-codes-post')->with(compact('codes'));
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws FireflyException
|
||||
*/
|
||||
public function backupCodes(Request $request): Factory|RedirectResponse|View
|
||||
{
|
||||
if (!$this->internalAuth) {
|
||||
$request->session()->flash('error', trans('firefly.external_user_mgt_disabled'));
|
||||
|
||||
return redirect(route('profile.index'));
|
||||
}
|
||||
$enabledMFA = null !== auth()->user()->mfa_secret;
|
||||
if (false === $enabledMFA) {
|
||||
request()->session()->flash('info', trans('firefly.mfa_not_enabled'));
|
||||
|
||||
return redirect(route('profile.index'));
|
||||
}
|
||||
|
||||
return view('profile.mfa.backup-codes-intro');
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit 2FA for the first time.
|
||||
*
|
||||
* @return Redirector|RedirectResponse
|
||||
*
|
||||
* @throws FireflyException
|
||||
*/
|
||||
public function enableMFAPost(TokenFormRequest $request)
|
||||
{
|
||||
if (!$this->internalAuth) {
|
||||
$request->session()->flash('error', trans('firefly.external_user_mgt_disabled'));
|
||||
|
||||
return redirect(route('profile.index'));
|
||||
}
|
||||
|
||||
/** @var User $user */
|
||||
$user = auth()->user();
|
||||
|
||||
// verify password.
|
||||
$password = $request->get('password');
|
||||
if (!auth()->validate(['email' => $user->email, 'password' => $password])) {
|
||||
session()->flash('error', 'Bad user pw, no MFA for you!');
|
||||
|
||||
return redirect(route('profile.mfa.index'));
|
||||
}
|
||||
|
||||
/** @var UserRepositoryInterface $repository */
|
||||
$repository = app(UserRepositoryInterface::class);
|
||||
$secret = app('preferences')->get('temp-mfa-secret')?->data;
|
||||
if (is_array($secret)) {
|
||||
$secret = null;
|
||||
}
|
||||
$secret = (string) $secret;
|
||||
|
||||
$repository->setMFACode($user, $secret);
|
||||
|
||||
app('preferences')->delete('temp-mfa-secret');
|
||||
|
||||
session()->flash('success', (string) trans('firefly.saved_preferences'));
|
||||
app('preferences')->mark();
|
||||
|
||||
// also save the code so replay attack is prevented.
|
||||
$mfaCode = $request->get('code');
|
||||
$this->addToMFAHistory($mfaCode);
|
||||
|
||||
// make sure MFA is logged out.
|
||||
if ('testing' !== config('app.env')) {
|
||||
\Google2FA::logout();
|
||||
}
|
||||
|
||||
// drop all info from session:
|
||||
session()->forget(['temp-mfa-secret', 'two-factor-secret', 'two-factor-codes']);
|
||||
|
||||
// send user notification.
|
||||
Log::channel('audit')->info(sprintf('User "%s" has enabled MFA', $user->email));
|
||||
event(new EnabledMFA($user));
|
||||
|
||||
return redirect(route('profile.mfa.backup-codes'));
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO duplicate code.
|
||||
*
|
||||
* @throws FireflyException
|
||||
*/
|
||||
private function addToMFAHistory(string $mfaCode): void
|
||||
{
|
||||
/** @var array $mfaHistory */
|
||||
$mfaHistory = app('preferences')->get('mfa_history', [])->data;
|
||||
$entry = [
|
||||
'time' => time(),
|
||||
'code' => $mfaCode,
|
||||
];
|
||||
$mfaHistory[] = $entry;
|
||||
|
||||
app('preferences')->set('mfa_history', $mfaHistory);
|
||||
$this->filterMFAHistory();
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove old entries from the preferences array.
|
||||
*/
|
||||
private function filterMFAHistory(): void
|
||||
{
|
||||
/** @var array $mfaHistory */
|
||||
$mfaHistory = app('preferences')->get('mfa_history', [])->data;
|
||||
$newHistory = [];
|
||||
$now = time();
|
||||
foreach ($mfaHistory as $entry) {
|
||||
$time = $entry['time'];
|
||||
$code = $entry['code'];
|
||||
if ($now - $time <= 300) {
|
||||
$newHistory[] = [
|
||||
'time' => $time,
|
||||
'code' => $code,
|
||||
];
|
||||
}
|
||||
}
|
||||
app('preferences')->set('mfa_history', $newHistory);
|
||||
}
|
||||
}
|
||||
@@ -30,7 +30,6 @@ use FireflyIII\Http\Middleware\IsDemoUser;
|
||||
use FireflyIII\Http\Requests\DeleteAccountFormRequest;
|
||||
use FireflyIII\Http\Requests\EmailFormRequest;
|
||||
use FireflyIII\Http\Requests\ProfileFormRequest;
|
||||
use FireflyIII\Http\Requests\TokenFormRequest;
|
||||
use FireflyIII\Models\Preference;
|
||||
use FireflyIII\Repositories\User\UserRepositoryInterface;
|
||||
use FireflyIII\Support\Http\Controllers\CreateStuff;
|
||||
@@ -45,10 +44,6 @@ use Illuminate\Routing\Redirector;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\View\View;
|
||||
use Laravel\Passport\ClientRepository;
|
||||
use PragmaRX\Google2FA\Exceptions\IncompatibleWithGoogleAuthenticatorException;
|
||||
use PragmaRX\Google2FA\Exceptions\InvalidCharactersException;
|
||||
use PragmaRX\Google2FA\Exceptions\SecretKeyTooShortException;
|
||||
use PragmaRX\Recovery\Recovery;
|
||||
|
||||
/**
|
||||
* Class ProfileController.
|
||||
@@ -83,65 +78,6 @@ class ProfileController extends Controller
|
||||
$this->middleware(IsDemoUser::class)->except(['index']);
|
||||
}
|
||||
|
||||
/**
|
||||
* View that generates a 2FA code for the user.
|
||||
*
|
||||
* @throws IncompatibleWithGoogleAuthenticatorException
|
||||
* @throws InvalidCharactersException
|
||||
* @throws SecretKeyTooShortException
|
||||
*/
|
||||
public function code(Request $request): Factory|RedirectResponse|View
|
||||
{
|
||||
if (!$this->internalAuth) {
|
||||
$request->session()->flash('error', trans('firefly.external_user_mgt_disabled'));
|
||||
|
||||
return redirect(route('profile.index'));
|
||||
}
|
||||
$domain = $this->getDomain();
|
||||
$secretPreference = app('preferences')->get('temp-mfa-secret');
|
||||
$codesPreference = app('preferences')->get('temp-mfa-codes');
|
||||
|
||||
// generate secret if not in session
|
||||
if (null === $secretPreference) {
|
||||
// generate secret + store + flash
|
||||
$secret = \Google2FA::generateSecretKey();
|
||||
app('preferences')->set('temp-mfa-secret', $secret);
|
||||
}
|
||||
|
||||
// re-use secret if in session
|
||||
if (null !== $secretPreference) {
|
||||
// get secret from session and flash
|
||||
$secret = $secretPreference->data;
|
||||
}
|
||||
if (is_array($secret)) {
|
||||
$secret = '';
|
||||
}
|
||||
|
||||
// generate recovery codes if not in session:
|
||||
$recoveryCodes = '';
|
||||
|
||||
if (null === $codesPreference) {
|
||||
// generate codes + store + flash:
|
||||
$recovery = app(Recovery::class);
|
||||
$recoveryCodes = $recovery->lowercase()->setCount(8)->setBlocks(2)->setChars(6)->toArray();
|
||||
app('preferences')->set('temp-mfa-codes', $recoveryCodes);
|
||||
}
|
||||
|
||||
// get codes from session if present already:
|
||||
if (null !== $codesPreference) {
|
||||
$recoveryCodes = $codesPreference->data;
|
||||
}
|
||||
if (!is_array($recoveryCodes)) {
|
||||
$recoveryCodes = [];
|
||||
}
|
||||
|
||||
$codes = implode("\r\n", $recoveryCodes);
|
||||
|
||||
$image = \Google2FA::getQRCodeInline($domain, auth()->user()->email, (string)$secret);
|
||||
|
||||
return view('profile.code', compact('image', 'secret', 'codes'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Screen to confirm email change.
|
||||
*
|
||||
@@ -193,61 +129,6 @@ class ProfileController extends Controller
|
||||
return view('profile.delete-account', compact('title', 'subTitle', 'subTitleIcon'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete 2FA routine.
|
||||
*/
|
||||
public function deleteCode(Request $request): Redirector|RedirectResponse
|
||||
{
|
||||
if (!$this->internalAuth) {
|
||||
$request->session()->flash('error', trans('firefly.external_user_mgt_disabled'));
|
||||
|
||||
return redirect(route('profile.index'));
|
||||
}
|
||||
|
||||
/** @var UserRepositoryInterface $repository */
|
||||
$repository = app(UserRepositoryInterface::class);
|
||||
|
||||
/** @var User $user */
|
||||
$user = auth()->user();
|
||||
|
||||
app('preferences')->delete('temp-mfa-secret');
|
||||
app('preferences')->delete('temp-mfa-codes');
|
||||
$repository->setMFACode($user, null);
|
||||
app('preferences')->mark();
|
||||
|
||||
session()->flash('success', (string)trans('firefly.pref_two_factor_auth_disabled'));
|
||||
session()->flash('info', (string)trans('firefly.pref_two_factor_auth_remove_it'));
|
||||
|
||||
return redirect(route('profile.index'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable 2FA screen.
|
||||
*/
|
||||
public function enable2FA(Request $request): Redirector|RedirectResponse
|
||||
{
|
||||
if (!$this->internalAuth) {
|
||||
$request->session()->flash('error', trans('firefly.external_user_mgt_disabled'));
|
||||
|
||||
return redirect(route('profile.index'));
|
||||
}
|
||||
|
||||
/** @var User $user */
|
||||
$user = auth()->user();
|
||||
$enabledMFA = null !== $user->mfa_secret;
|
||||
|
||||
// if we don't have a valid secret yet, redirect to the code page to get one.
|
||||
if (!$enabledMFA) {
|
||||
return redirect(route('profile.code'));
|
||||
}
|
||||
|
||||
// If FF3 already has a secret, just set the two factor auth enabled to 1,
|
||||
// and let the user continue with the existing secret.
|
||||
session()->flash('info', (string)trans('firefly.2fa_already_enabled'));
|
||||
|
||||
return redirect(route('profile.index'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Index for profile.
|
||||
*
|
||||
@@ -298,33 +179,6 @@ class ProfileController extends Controller
|
||||
return view('profile.logout-other-sessions');
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws FireflyException
|
||||
*/
|
||||
public function newBackupCodes(Request $request): Factory|RedirectResponse|View
|
||||
{
|
||||
if (!$this->internalAuth) {
|
||||
$request->session()->flash('error', trans('firefly.external_user_mgt_disabled'));
|
||||
|
||||
return redirect(route('profile.index'));
|
||||
}
|
||||
|
||||
// generate recovery codes:
|
||||
$recovery = app(Recovery::class);
|
||||
$recoveryCodes = $recovery->lowercase()
|
||||
->setCount(8) // Generate 8 codes
|
||||
->setBlocks(2) // Every code must have 7 blocks
|
||||
->setChars(6) // Each block must have 16 chars
|
||||
->toArray()
|
||||
;
|
||||
$codes = implode("\r\n", $recoveryCodes);
|
||||
|
||||
app('preferences')->set('mfa_recovery', $recoveryCodes);
|
||||
app('preferences')->mark();
|
||||
|
||||
return view('profile.new-backup-codes', compact('codes'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit the change email form.
|
||||
*/
|
||||
@@ -442,99 +296,6 @@ class ProfileController extends Controller
|
||||
return view('profile.change-password', compact('title', 'subTitle', 'subTitleIcon'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit 2FA for the first time.
|
||||
*
|
||||
* @return Redirector|RedirectResponse
|
||||
*
|
||||
* @throws FireflyException
|
||||
*/
|
||||
public function postCode(TokenFormRequest $request)
|
||||
{
|
||||
if (!$this->internalAuth) {
|
||||
$request->session()->flash('error', trans('firefly.external_user_mgt_disabled'));
|
||||
|
||||
return redirect(route('profile.index'));
|
||||
}
|
||||
|
||||
/** @var User $user */
|
||||
$user = auth()->user();
|
||||
|
||||
/** @var UserRepositoryInterface $repository */
|
||||
$repository = app(UserRepositoryInterface::class);
|
||||
$secret = app('preferences')->get('temp-mfa-secret')?->data;
|
||||
if (is_array($secret)) {
|
||||
$secret = null;
|
||||
}
|
||||
$secret = (string)$secret;
|
||||
|
||||
$repository->setMFACode($user, $secret);
|
||||
|
||||
app('preferences')->delete('temp-mfa-secret');
|
||||
app('preferences')->delete('temp-mfa-codes');
|
||||
|
||||
session()->flash('success', (string)trans('firefly.saved_preferences'));
|
||||
app('preferences')->mark();
|
||||
|
||||
// also save the code so replay attack is prevented.
|
||||
$mfaCode = $request->get('code');
|
||||
$this->addToMFAHistory($mfaCode);
|
||||
|
||||
// save backup codes in preferences:
|
||||
app('preferences')->set('mfa_recovery', session()->get('temp-mfa-codes'));
|
||||
|
||||
// make sure MFA is logged out.
|
||||
if ('testing' !== config('app.env')) {
|
||||
\Google2FA::logout();
|
||||
}
|
||||
|
||||
// drop all info from session:
|
||||
session()->forget(['temp-mfa-secret', 'two-factor-secret', 'temp-mfa-codes', 'two-factor-codes']);
|
||||
|
||||
return redirect(route('profile.index'));
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO duplicate code.
|
||||
*
|
||||
* @throws FireflyException
|
||||
*/
|
||||
private function addToMFAHistory(string $mfaCode): void
|
||||
{
|
||||
/** @var array $mfaHistory */
|
||||
$mfaHistory = app('preferences')->get('mfa_history', [])->data;
|
||||
$entry = [
|
||||
'time' => time(),
|
||||
'code' => $mfaCode,
|
||||
];
|
||||
$mfaHistory[] = $entry;
|
||||
|
||||
app('preferences')->set('mfa_history', $mfaHistory);
|
||||
$this->filterMFAHistory();
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove old entries from the preferences array.
|
||||
*/
|
||||
private function filterMFAHistory(): void
|
||||
{
|
||||
/** @var array $mfaHistory */
|
||||
$mfaHistory = app('preferences')->get('mfa_history', [])->data;
|
||||
$newHistory = [];
|
||||
$now = time();
|
||||
foreach ($mfaHistory as $entry) {
|
||||
$time = $entry['time'];
|
||||
$code = $entry['code'];
|
||||
if ($now - $time <= 300) {
|
||||
$newHistory[] = [
|
||||
'time' => $time,
|
||||
'code' => $code,
|
||||
];
|
||||
}
|
||||
}
|
||||
app('preferences')->set('mfa_history', $newHistory);
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit delete account.
|
||||
*
|
||||
@@ -664,7 +425,7 @@ class ProfileController extends Controller
|
||||
$repository->changeEmail($user, $match);
|
||||
$repository->unblockUser($user);
|
||||
|
||||
// return to login.
|
||||
// return to login page.
|
||||
session()->flash('success', (string)trans('firefly.login_with_old_email'));
|
||||
|
||||
return redirect(route('login'));
|
||||
|
||||
@@ -88,6 +88,7 @@ class ShowController extends Controller
|
||||
$groups = $this->recurring->getTransactions($recurrence);
|
||||
$today = today(config('app.timezone'));
|
||||
$array['repeat_until'] = null !== $array['repeat_until'] ? new Carbon($array['repeat_until']) : null;
|
||||
$array['journal_count'] = $this->recurring->getJournalCount($recurrence);
|
||||
|
||||
// transform dates back to Carbon objects and expand information
|
||||
foreach ($array['repetitions'] as $index => $repetition) {
|
||||
@@ -114,6 +115,15 @@ class ShowController extends Controller
|
||||
$array['attachments'][] = $item;
|
||||
}
|
||||
|
||||
if (null !== $array['nr_of_repetitions']) {
|
||||
$left = $array['nr_of_repetitions'] - $array['journal_count'];
|
||||
$left = max(0, $left);
|
||||
// limit each repetition to X occurrences:
|
||||
foreach ($array['repetitions'] as $index => $repetition) {
|
||||
$array['repetitions'][$index]['occurrences'] = array_slice($repetition['occurrences'], 0, $left);
|
||||
}
|
||||
}
|
||||
|
||||
$subTitle = (string)trans('firefly.overview_for_recurrence', ['title' => $recurrence->title]);
|
||||
|
||||
return view('recurring.show', compact('recurrence', 'subTitle', 'array', 'groups', 'today'));
|
||||
|
||||
56
app/Http/Requests/ExistingTokenFormRequest.php
Normal file
56
app/Http/Requests/ExistingTokenFormRequest.php
Normal file
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* TokenFormRequest.php
|
||||
* Copyright (c) 2019 james@firefly-iii.org
|
||||
*
|
||||
* This file is part of Firefly III (https://github.com/firefly-iii).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace FireflyIII\Http\Requests;
|
||||
|
||||
use FireflyIII\Support\Request\ChecksLogin;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Validation\Validator;
|
||||
|
||||
/**
|
||||
* Class ExistingTokenFormRequest.
|
||||
*/
|
||||
class ExistingTokenFormRequest extends FormRequest
|
||||
{
|
||||
use ChecksLogin;
|
||||
|
||||
/**
|
||||
* Rules for this request.
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
// fixed
|
||||
return [
|
||||
'password' => 'required|currentPassword',
|
||||
'code' => 'required|existingMfaCode',
|
||||
];
|
||||
}
|
||||
|
||||
public function withValidator(Validator $validator): void
|
||||
{
|
||||
if ($validator->fails()) {
|
||||
Log::channel('audit')->error(sprintf('Validation errors in %s', __CLASS__), $validator->errors()->toArray());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -42,6 +42,7 @@ class TokenFormRequest extends FormRequest
|
||||
{
|
||||
// fixed
|
||||
return [
|
||||
'password' => 'required|currentPassword',
|
||||
'code' => 'required|2faCode',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -177,10 +177,11 @@ class CreateRecurringTransactions implements ShouldQueue
|
||||
// has repeated X times.
|
||||
$journalCount = $this->repository->getJournalCount($recurrence);
|
||||
if (0 !== $recurrence->repetitions && $journalCount >= $recurrence->repetitions && false === $this->force) {
|
||||
app('log')->info(sprintf('Recurrence #%d has run %d times, so will run no longer.', $recurrence->id, $recurrence->repetitions));
|
||||
app('log')->info(sprintf('Recurrence #%d has run %d times, so will run no longer.', $recurrence->id, $journalCount));
|
||||
|
||||
return false;
|
||||
}
|
||||
app('log')->debug(sprintf('Recurrence #%d has run %d times, max is %d times.', $recurrence->id, $journalCount, $recurrence->repetitions));
|
||||
|
||||
// is no longer running
|
||||
if ($this->repeatUntilHasPassed($recurrence)) {
|
||||
@@ -202,8 +203,8 @@ class CreateRecurringTransactions implements ShouldQueue
|
||||
sprintf(
|
||||
'Recurrence #%d is set to run on %s, and today\'s date is %s. Skipped.',
|
||||
$recurrence->id,
|
||||
$recurrence->first_date->format('Y-m-d'),
|
||||
$this->date->format('Y-m-d')
|
||||
$recurrence->first_date->format('Y-m-d H:i:s'),
|
||||
$this->date->format('Y-m-d H:i:s')
|
||||
)
|
||||
);
|
||||
|
||||
@@ -244,9 +245,10 @@ class CreateRecurringTransactions implements ShouldQueue
|
||||
private function hasNotStartedYet(Recurrence $recurrence): bool
|
||||
{
|
||||
$startDate = $this->getStartDate($recurrence);
|
||||
app('log')->debug(sprintf('Start date is %s', $startDate->format('Y-m-d')));
|
||||
app('log')->debug(sprintf('Start date is %s', $startDate->format('Y-m-d H:i:s')));
|
||||
app('log')->debug(sprintf('Ask date is %s', $this->date->format('Y-m-d H:i:s')));
|
||||
|
||||
return $startDate->gt($this->date);
|
||||
return $startDate->gte($this->date);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -362,11 +364,9 @@ class CreateRecurringTransactions implements ShouldQueue
|
||||
$groupTitle = null;
|
||||
$count = $recurrence->recurrenceTransactions->count();
|
||||
// #8844, if there is one recurrence transaction, use the first title as the title.
|
||||
if (1 === $count) {
|
||||
/** @var RecurrenceTransaction $first */
|
||||
$first = $recurrence->recurrenceTransactions()->first();
|
||||
$groupTitle = $first->description;
|
||||
}
|
||||
// #9305, if there is one recurrence transaction, group title must be NULL.
|
||||
$groupTitle = null;
|
||||
|
||||
// #8844, if there are more, use the recurrence transaction itself.
|
||||
if ($count > 1) {
|
||||
$groupTitle = $recurrence->title;
|
||||
|
||||
@@ -37,6 +37,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphMany;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
/**
|
||||
@@ -167,12 +168,16 @@ class TransactionJournal extends Model
|
||||
|
||||
public function scopeAfter(EloquentBuilder $query, Carbon $date): EloquentBuilder
|
||||
{
|
||||
return $query->where('transaction_journals.date', '>=', $date->format('Y-m-d 00:00:00'));
|
||||
Log::debug(sprintf('scopeAfter("%s")', $date->format('Y-m-d H:i:s')));
|
||||
|
||||
return $query->where('transaction_journals.date', '>=', $date->format('Y-m-d H:i:s'));
|
||||
}
|
||||
|
||||
public function scopeBefore(EloquentBuilder $query, Carbon $date): EloquentBuilder
|
||||
{
|
||||
return $query->where('transaction_journals.date', '<=', $date->format('Y-m-d 00:00:00'));
|
||||
Log::debug(sprintf('scopeBefore("%s")', $date->format('Y-m-d H:i:s')));
|
||||
|
||||
return $query->where('transaction_journals.date', '<=', $date->format('Y-m-d H:i:s'));
|
||||
}
|
||||
|
||||
public function scopeTransactionTypes(EloquentBuilder $query, array $types): void
|
||||
|
||||
117
app/Notifications/Security/DisabledMFANotification.php
Normal file
117
app/Notifications/Security/DisabledMFANotification.php
Normal file
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
/*
|
||||
* EnabledMFANotification.php
|
||||
* Copyright (c) 2024 james@firefly-iii.org.
|
||||
*
|
||||
* This file is part of Firefly III (https://github.com/firefly-iii).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace FireflyIII\Notifications\Security;
|
||||
|
||||
use FireflyIII\Support\Notifications\UrlValidator;
|
||||
use FireflyIII\User;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
use Illuminate\Notifications\Messages\SlackMessage;
|
||||
use Illuminate\Notifications\Notification;
|
||||
|
||||
class DisabledMFANotification extends Notification
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
private User $user;
|
||||
|
||||
/**
|
||||
* Create a new notification instance.
|
||||
*/
|
||||
public function __construct(User $user)
|
||||
{
|
||||
$this->user = $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the array representation of the notification.
|
||||
*
|
||||
* @param mixed $notifiable
|
||||
*
|
||||
* @return array
|
||||
*
|
||||
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
|
||||
*/
|
||||
public function toArray($notifiable)
|
||||
{
|
||||
return [
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the mail representation of the notification.
|
||||
*
|
||||
* @param mixed $notifiable
|
||||
*
|
||||
* @return MailMessage
|
||||
*
|
||||
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
|
||||
*/
|
||||
public function toMail($notifiable)
|
||||
{
|
||||
$subject = (string)trans('email.disabled_mfa_subject');
|
||||
|
||||
return (new MailMessage())->markdown('emails.security.disabled-mfa', ['user' => $this->user])->subject($subject);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Slack representation of the notification.
|
||||
*
|
||||
* @param mixed $notifiable
|
||||
*
|
||||
* @return SlackMessage
|
||||
*
|
||||
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
|
||||
*/
|
||||
public function toSlack($notifiable)
|
||||
{
|
||||
$message = (string)trans('email.disabled_mfa_slack', ['email' => $this->user->email]);
|
||||
|
||||
return (new SlackMessage())->content($message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the notification's delivery channels.
|
||||
*
|
||||
* @param mixed $notifiable
|
||||
*
|
||||
* @return array
|
||||
*
|
||||
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
|
||||
*/
|
||||
public function via($notifiable)
|
||||
{
|
||||
/** @var null|User $user */
|
||||
$user = auth()->user();
|
||||
$slackUrl = null === $user ? '' : app('preferences')->getForUser(auth()->user(), 'slack_webhook_url', '')->data;
|
||||
if (is_array($slackUrl)) {
|
||||
$slackUrl = '';
|
||||
}
|
||||
if (UrlValidator::isValidWebhookURL((string)$slackUrl)) {
|
||||
return ['mail', 'slack'];
|
||||
}
|
||||
|
||||
return ['mail'];
|
||||
}
|
||||
}
|
||||
117
app/Notifications/Security/EnabledMFANotification.php
Normal file
117
app/Notifications/Security/EnabledMFANotification.php
Normal file
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
/*
|
||||
* EnabledMFANotification.php
|
||||
* Copyright (c) 2024 james@firefly-iii.org.
|
||||
*
|
||||
* This file is part of Firefly III (https://github.com/firefly-iii).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace FireflyIII\Notifications\Security;
|
||||
|
||||
use FireflyIII\Support\Notifications\UrlValidator;
|
||||
use FireflyIII\User;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
use Illuminate\Notifications\Messages\SlackMessage;
|
||||
use Illuminate\Notifications\Notification;
|
||||
|
||||
class EnabledMFANotification extends Notification
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
private User $user;
|
||||
|
||||
/**
|
||||
* Create a new notification instance.
|
||||
*/
|
||||
public function __construct(User $user)
|
||||
{
|
||||
$this->user = $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the array representation of the notification.
|
||||
*
|
||||
* @param mixed $notifiable
|
||||
*
|
||||
* @return array
|
||||
*
|
||||
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
|
||||
*/
|
||||
public function toArray($notifiable)
|
||||
{
|
||||
return [
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the mail representation of the notification.
|
||||
*
|
||||
* @param mixed $notifiable
|
||||
*
|
||||
* @return MailMessage
|
||||
*
|
||||
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
|
||||
*/
|
||||
public function toMail($notifiable)
|
||||
{
|
||||
$subject = (string)trans('email.enabled_mfa_subject');
|
||||
|
||||
return (new MailMessage())->markdown('emails.security.enabled-mfa', ['user' => $this->user])->subject($subject);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Slack representation of the notification.
|
||||
*
|
||||
* @param mixed $notifiable
|
||||
*
|
||||
* @return SlackMessage
|
||||
*
|
||||
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
|
||||
*/
|
||||
public function toSlack($notifiable)
|
||||
{
|
||||
$message = (string)trans('email.enabled_mfa_slack', ['email' => $this->user->email]);
|
||||
|
||||
return (new SlackMessage())->content($message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the notification's delivery channels.
|
||||
*
|
||||
* @param mixed $notifiable
|
||||
*
|
||||
* @return array
|
||||
*
|
||||
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
|
||||
*/
|
||||
public function via($notifiable)
|
||||
{
|
||||
/** @var null|User $user */
|
||||
$user = auth()->user();
|
||||
$slackUrl = null === $user ? '' : app('preferences')->getForUser(auth()->user(), 'slack_webhook_url', '')->data;
|
||||
if (is_array($slackUrl)) {
|
||||
$slackUrl = '';
|
||||
}
|
||||
if (UrlValidator::isValidWebhookURL((string)$slackUrl)) {
|
||||
return ['mail', 'slack'];
|
||||
}
|
||||
|
||||
return ['mail'];
|
||||
}
|
||||
}
|
||||
119
app/Notifications/Security/MFABackupFewLeftNotification.php
Normal file
119
app/Notifications/Security/MFABackupFewLeftNotification.php
Normal file
@@ -0,0 +1,119 @@
|
||||
<?php
|
||||
/*
|
||||
* EnabledMFANotification.php
|
||||
* Copyright (c) 2024 james@firefly-iii.org.
|
||||
*
|
||||
* This file is part of Firefly III (https://github.com/firefly-iii).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace FireflyIII\Notifications\Security;
|
||||
|
||||
use FireflyIII\Support\Notifications\UrlValidator;
|
||||
use FireflyIII\User;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
use Illuminate\Notifications\Messages\SlackMessage;
|
||||
use Illuminate\Notifications\Notification;
|
||||
|
||||
class MFABackupFewLeftNotification extends Notification
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
private User $user;
|
||||
private int $count;
|
||||
|
||||
/**
|
||||
* Create a new notification instance.
|
||||
*/
|
||||
public function __construct(User $user, int $count)
|
||||
{
|
||||
$this->user = $user;
|
||||
$this->count = $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the array representation of the notification.
|
||||
*
|
||||
* @param mixed $notifiable
|
||||
*
|
||||
* @return array
|
||||
*
|
||||
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
|
||||
*/
|
||||
public function toArray($notifiable)
|
||||
{
|
||||
return [
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the mail representation of the notification.
|
||||
*
|
||||
* @param mixed $notifiable
|
||||
*
|
||||
* @return MailMessage
|
||||
*
|
||||
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
|
||||
*/
|
||||
public function toMail($notifiable)
|
||||
{
|
||||
$subject = (string)trans('email.mfa_few_backups_left_subject', ['count' => $this->count]);
|
||||
|
||||
return (new MailMessage())->markdown('emails.security.few-backup-codes', ['user' => $this->user, 'count' => $this->count])->subject($subject);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Slack representation of the notification.
|
||||
*
|
||||
* @param mixed $notifiable
|
||||
*
|
||||
* @return SlackMessage
|
||||
*
|
||||
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
|
||||
*/
|
||||
public function toSlack($notifiable)
|
||||
{
|
||||
$message = (string)trans('email.mfa_few_backups_left_slack', ['email' => $this->user->email, 'count' => $this->count]);
|
||||
|
||||
return (new SlackMessage())->content($message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the notification's delivery channels.
|
||||
*
|
||||
* @param mixed $notifiable
|
||||
*
|
||||
* @return array
|
||||
*
|
||||
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
|
||||
*/
|
||||
public function via($notifiable)
|
||||
{
|
||||
/** @var null|User $user */
|
||||
$user = auth()->user();
|
||||
$slackUrl = null === $user ? '' : app('preferences')->getForUser(auth()->user(), 'slack_webhook_url', '')->data;
|
||||
if (is_array($slackUrl)) {
|
||||
$slackUrl = '';
|
||||
}
|
||||
if (UrlValidator::isValidWebhookURL((string)$slackUrl)) {
|
||||
return ['mail', 'slack'];
|
||||
}
|
||||
|
||||
return ['mail'];
|
||||
}
|
||||
}
|
||||
117
app/Notifications/Security/MFABackupNoLeftNotification.php
Normal file
117
app/Notifications/Security/MFABackupNoLeftNotification.php
Normal file
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
/*
|
||||
* EnabledMFANotification.php
|
||||
* Copyright (c) 2024 james@firefly-iii.org.
|
||||
*
|
||||
* This file is part of Firefly III (https://github.com/firefly-iii).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace FireflyIII\Notifications\Security;
|
||||
|
||||
use FireflyIII\Support\Notifications\UrlValidator;
|
||||
use FireflyIII\User;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
use Illuminate\Notifications\Messages\SlackMessage;
|
||||
use Illuminate\Notifications\Notification;
|
||||
|
||||
class MFABackupNoLeftNotification extends Notification
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
private User $user;
|
||||
|
||||
/**
|
||||
* Create a new notification instance.
|
||||
*/
|
||||
public function __construct(User $user)
|
||||
{
|
||||
$this->user = $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the array representation of the notification.
|
||||
*
|
||||
* @param mixed $notifiable
|
||||
*
|
||||
* @return array
|
||||
*
|
||||
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
|
||||
*/
|
||||
public function toArray($notifiable)
|
||||
{
|
||||
return [
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the mail representation of the notification.
|
||||
*
|
||||
* @param mixed $notifiable
|
||||
*
|
||||
* @return MailMessage
|
||||
*
|
||||
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
|
||||
*/
|
||||
public function toMail($notifiable)
|
||||
{
|
||||
$subject = (string)trans('email.mfa_no_backups_left_subject');
|
||||
|
||||
return (new MailMessage())->markdown('emails.security.no-backup-codes', ['user' => $this->user])->subject($subject);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Slack representation of the notification.
|
||||
*
|
||||
* @param mixed $notifiable
|
||||
*
|
||||
* @return SlackMessage
|
||||
*
|
||||
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
|
||||
*/
|
||||
public function toSlack($notifiable)
|
||||
{
|
||||
$message = (string)trans('email.mfa_no_backups_left_slack', ['email' => $this->user->email]);
|
||||
|
||||
return (new SlackMessage())->content($message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the notification's delivery channels.
|
||||
*
|
||||
* @param mixed $notifiable
|
||||
*
|
||||
* @return array
|
||||
*
|
||||
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
|
||||
*/
|
||||
public function via($notifiable)
|
||||
{
|
||||
/** @var null|User $user */
|
||||
$user = auth()->user();
|
||||
$slackUrl = null === $user ? '' : app('preferences')->getForUser(auth()->user(), 'slack_webhook_url', '')->data;
|
||||
if (is_array($slackUrl)) {
|
||||
$slackUrl = '';
|
||||
}
|
||||
if (UrlValidator::isValidWebhookURL((string)$slackUrl)) {
|
||||
return ['mail', 'slack'];
|
||||
}
|
||||
|
||||
return ['mail'];
|
||||
}
|
||||
}
|
||||
119
app/Notifications/Security/MFAManyFailedAttemptsNotification.php
Normal file
119
app/Notifications/Security/MFAManyFailedAttemptsNotification.php
Normal file
@@ -0,0 +1,119 @@
|
||||
<?php
|
||||
/*
|
||||
* EnabledMFANotification.php
|
||||
* Copyright (c) 2024 james@firefly-iii.org.
|
||||
*
|
||||
* This file is part of Firefly III (https://github.com/firefly-iii).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace FireflyIII\Notifications\Security;
|
||||
|
||||
use FireflyIII\Support\Notifications\UrlValidator;
|
||||
use FireflyIII\User;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
use Illuminate\Notifications\Messages\SlackMessage;
|
||||
use Illuminate\Notifications\Notification;
|
||||
|
||||
class MFAManyFailedAttemptsNotification extends Notification
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
private User $user;
|
||||
private int $count;
|
||||
|
||||
/**
|
||||
* Create a new notification instance.
|
||||
*/
|
||||
public function __construct(User $user, int $count)
|
||||
{
|
||||
$this->user = $user;
|
||||
$this->count = $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the array representation of the notification.
|
||||
*
|
||||
* @param mixed $notifiable
|
||||
*
|
||||
* @return array
|
||||
*
|
||||
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
|
||||
*/
|
||||
public function toArray($notifiable)
|
||||
{
|
||||
return [
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the mail representation of the notification.
|
||||
*
|
||||
* @param mixed $notifiable
|
||||
*
|
||||
* @return MailMessage
|
||||
*
|
||||
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
|
||||
*/
|
||||
public function toMail($notifiable)
|
||||
{
|
||||
$subject = (string)trans('email.mfa_many_failed_subject', ['count' => $this->count]);
|
||||
|
||||
return (new MailMessage())->markdown('emails.security.many-failed-attempts', ['user' => $this->user, 'count' => $this->count])->subject($subject);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Slack representation of the notification.
|
||||
*
|
||||
* @param mixed $notifiable
|
||||
*
|
||||
* @return SlackMessage
|
||||
*
|
||||
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
|
||||
*/
|
||||
public function toSlack($notifiable)
|
||||
{
|
||||
$message = (string)trans('email.mfa_many_failed_slack', ['email' => $this->user->email, 'count' => $this->count]);
|
||||
|
||||
return (new SlackMessage())->content($message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the notification's delivery channels.
|
||||
*
|
||||
* @param mixed $notifiable
|
||||
*
|
||||
* @return array
|
||||
*
|
||||
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
|
||||
*/
|
||||
public function via($notifiable)
|
||||
{
|
||||
/** @var null|User $user */
|
||||
$user = auth()->user();
|
||||
$slackUrl = null === $user ? '' : app('preferences')->getForUser(auth()->user(), 'slack_webhook_url', '')->data;
|
||||
if (is_array($slackUrl)) {
|
||||
$slackUrl = '';
|
||||
}
|
||||
if (UrlValidator::isValidWebhookURL((string)$slackUrl)) {
|
||||
return ['mail', 'slack'];
|
||||
}
|
||||
|
||||
return ['mail'];
|
||||
}
|
||||
}
|
||||
117
app/Notifications/Security/MFAUsedBackupCodeNotification.php
Normal file
117
app/Notifications/Security/MFAUsedBackupCodeNotification.php
Normal file
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
/*
|
||||
* EnabledMFANotification.php
|
||||
* Copyright (c) 2024 james@firefly-iii.org.
|
||||
*
|
||||
* This file is part of Firefly III (https://github.com/firefly-iii).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace FireflyIII\Notifications\Security;
|
||||
|
||||
use FireflyIII\Support\Notifications\UrlValidator;
|
||||
use FireflyIII\User;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
use Illuminate\Notifications\Messages\SlackMessage;
|
||||
use Illuminate\Notifications\Notification;
|
||||
|
||||
class MFAUsedBackupCodeNotification extends Notification
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
private User $user;
|
||||
|
||||
/**
|
||||
* Create a new notification instance.
|
||||
*/
|
||||
public function __construct(User $user)
|
||||
{
|
||||
$this->user = $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the array representation of the notification.
|
||||
*
|
||||
* @param mixed $notifiable
|
||||
*
|
||||
* @return array
|
||||
*
|
||||
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
|
||||
*/
|
||||
public function toArray($notifiable)
|
||||
{
|
||||
return [
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the mail representation of the notification.
|
||||
*
|
||||
* @param mixed $notifiable
|
||||
*
|
||||
* @return MailMessage
|
||||
*
|
||||
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
|
||||
*/
|
||||
public function toMail($notifiable)
|
||||
{
|
||||
$subject = (string)trans('email.used_backup_code_subject');
|
||||
|
||||
return (new MailMessage())->markdown('emails.security.used-backup-code', ['user' => $this->user])->subject($subject);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Slack representation of the notification.
|
||||
*
|
||||
* @param mixed $notifiable
|
||||
*
|
||||
* @return SlackMessage
|
||||
*
|
||||
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
|
||||
*/
|
||||
public function toSlack($notifiable)
|
||||
{
|
||||
$message = (string)trans('email.used_backup_code_slack', ['email' => $this->user->email]);
|
||||
|
||||
return (new SlackMessage())->content($message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the notification's delivery channels.
|
||||
*
|
||||
* @param mixed $notifiable
|
||||
*
|
||||
* @return array
|
||||
*
|
||||
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
|
||||
*/
|
||||
public function via($notifiable)
|
||||
{
|
||||
/** @var null|User $user */
|
||||
$user = auth()->user();
|
||||
$slackUrl = null === $user ? '' : app('preferences')->getForUser(auth()->user(), 'slack_webhook_url', '')->data;
|
||||
if (is_array($slackUrl)) {
|
||||
$slackUrl = '';
|
||||
}
|
||||
if (UrlValidator::isValidWebhookURL((string)$slackUrl)) {
|
||||
return ['mail', 'slack'];
|
||||
}
|
||||
|
||||
return ['mail'];
|
||||
}
|
||||
}
|
||||
117
app/Notifications/Security/NewBackupCodesNotification.php
Normal file
117
app/Notifications/Security/NewBackupCodesNotification.php
Normal file
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
/*
|
||||
* EnabledMFANotification.php
|
||||
* Copyright (c) 2024 james@firefly-iii.org.
|
||||
*
|
||||
* This file is part of Firefly III (https://github.com/firefly-iii).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace FireflyIII\Notifications\Security;
|
||||
|
||||
use FireflyIII\Support\Notifications\UrlValidator;
|
||||
use FireflyIII\User;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
use Illuminate\Notifications\Messages\SlackMessage;
|
||||
use Illuminate\Notifications\Notification;
|
||||
|
||||
class NewBackupCodesNotification extends Notification
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
private User $user;
|
||||
|
||||
/**
|
||||
* Create a new notification instance.
|
||||
*/
|
||||
public function __construct(User $user)
|
||||
{
|
||||
$this->user = $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the array representation of the notification.
|
||||
*
|
||||
* @param mixed $notifiable
|
||||
*
|
||||
* @return array
|
||||
*
|
||||
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
|
||||
*/
|
||||
public function toArray($notifiable)
|
||||
{
|
||||
return [
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the mail representation of the notification.
|
||||
*
|
||||
* @param mixed $notifiable
|
||||
*
|
||||
* @return MailMessage
|
||||
*
|
||||
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
|
||||
*/
|
||||
public function toMail($notifiable)
|
||||
{
|
||||
$subject = (string)trans('email.new_backup_codes_subject');
|
||||
|
||||
return (new MailMessage())->markdown('emails.security.new-backup-codes', ['user' => $this->user])->subject($subject);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Slack representation of the notification.
|
||||
*
|
||||
* @param mixed $notifiable
|
||||
*
|
||||
* @return SlackMessage
|
||||
*
|
||||
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
|
||||
*/
|
||||
public function toSlack($notifiable)
|
||||
{
|
||||
$message = (string)trans('email.new_backup_codes_slack', ['email' => $this->user->email]);
|
||||
|
||||
return (new SlackMessage())->content($message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the notification's delivery channels.
|
||||
*
|
||||
* @param mixed $notifiable
|
||||
*
|
||||
* @return array
|
||||
*
|
||||
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
|
||||
*/
|
||||
public function via($notifiable)
|
||||
{
|
||||
/** @var null|User $user */
|
||||
$user = auth()->user();
|
||||
$slackUrl = null === $user ? '' : app('preferences')->getForUser(auth()->user(), 'slack_webhook_url', '')->data;
|
||||
if (is_array($slackUrl)) {
|
||||
$slackUrl = '';
|
||||
}
|
||||
if (UrlValidator::isValidWebhookURL((string)$slackUrl)) {
|
||||
return ['mail', 'slack'];
|
||||
}
|
||||
|
||||
return ['mail'];
|
||||
}
|
||||
}
|
||||
@@ -40,6 +40,13 @@ use FireflyIII\Events\RequestedNewPassword;
|
||||
use FireflyIII\Events\RequestedReportOnJournals;
|
||||
use FireflyIII\Events\RequestedSendWebhookMessages;
|
||||
use FireflyIII\Events\RequestedVersionCheckStatus;
|
||||
use FireflyIII\Events\Security\DisabledMFA;
|
||||
use FireflyIII\Events\Security\EnabledMFA;
|
||||
use FireflyIII\Events\Security\MFABackupFewLeft;
|
||||
use FireflyIII\Events\Security\MFABackupNoLeft;
|
||||
use FireflyIII\Events\Security\MFAManyFailedAttempts;
|
||||
use FireflyIII\Events\Security\MFANewBackupCodes;
|
||||
use FireflyIII\Events\Security\MFAUsedBackupCode;
|
||||
use FireflyIII\Events\StoredAccount;
|
||||
use FireflyIII\Events\StoredTransactionGroup;
|
||||
use FireflyIII\Events\TriggeredAuditLog;
|
||||
@@ -205,6 +212,29 @@ class EventServiceProvider extends ServiceProvider
|
||||
RuleActionFailedOnObject::class => [
|
||||
'FireflyIII\Handlers\Events\Model\RuleHandler@ruleActionFailedOnObject',
|
||||
],
|
||||
|
||||
// security related
|
||||
EnabledMFA::class => [
|
||||
'FireflyIII\Handlers\Events\Security\MFAHandler@sendMFAEnabledMail',
|
||||
],
|
||||
DisabledMFA::class => [
|
||||
'FireflyIII\Handlers\Events\Security\MFAHandler@sendMFADisabledMail',
|
||||
],
|
||||
MFANewBackupCodes::class => [
|
||||
'FireflyIII\Handlers\Events\Security\MFAHandler@sendNewMFABackupCodesMail',
|
||||
],
|
||||
MFAUsedBackupCode::class => [
|
||||
'FireflyIII\Handlers\Events\Security\MFAHandler@sendUsedBackupCodeMail',
|
||||
],
|
||||
MFABackupFewLeft::class => [
|
||||
'FireflyIII\Handlers\Events\Security\MFAHandler@sendBackupFewLeftMail',
|
||||
],
|
||||
MFABackupNoLeft::class => [
|
||||
'FireflyIII\Handlers\Events\Security\MFAHandler@sendBackupNoLeftMail',
|
||||
],
|
||||
MFAManyFailedAttempts::class => [
|
||||
'FireflyIII\Handlers\Events\Security\MFAHandler@sendMFAFailedAttemptsMail',
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -576,7 +576,7 @@ class AccountRepository implements AccountRepositoryInterface
|
||||
$parts = explode(' ', $query);
|
||||
foreach ($parts as $part) {
|
||||
$search = sprintf('%%%s%%', $part);
|
||||
$dbQuery->where('name', 'LIKE', $search);
|
||||
$dbQuery->whereLike('name', $search);
|
||||
}
|
||||
}
|
||||
if (0 !== count($types)) {
|
||||
@@ -604,11 +604,11 @@ class AccountRepository implements AccountRepositoryInterface
|
||||
$search = sprintf('%%%s%%', $part);
|
||||
$dbQuery->where(
|
||||
static function (EloquentBuilder $q1) use ($search): void { // @phpstan-ignore-line
|
||||
$q1->where('accounts.iban', 'LIKE', $search);
|
||||
$q1->whereLike('accounts.iban', $search);
|
||||
$q1->orWhere(
|
||||
static function (EloquentBuilder $q2) use ($search): void {
|
||||
$q2->where('account_meta.name', '=', 'account_number');
|
||||
$q2->where('account_meta.data', 'LIKE', $search);
|
||||
$q2->whereLike('account_meta.data', $search);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ class BillRepository implements BillRepositoryInterface
|
||||
{
|
||||
$search = $this->user->bills();
|
||||
if ('' !== $query) {
|
||||
$search->where('name', 'LIKE', sprintf('%%%s', $query));
|
||||
$search->whereLike('name', sprintf('%%%s', $query));
|
||||
}
|
||||
$search->orderBy('name', 'ASC')
|
||||
->where('active', true)
|
||||
@@ -70,7 +70,7 @@ class BillRepository implements BillRepositoryInterface
|
||||
{
|
||||
$search = $this->user->bills();
|
||||
if ('' !== $query) {
|
||||
$search->where('name', 'LIKE', sprintf('%s%%', $query));
|
||||
$search->whereLike('name', sprintf('%s%%', $query));
|
||||
}
|
||||
$search->orderBy('name', 'ASC')
|
||||
->where('active', true)
|
||||
@@ -306,6 +306,8 @@ class BillRepository implements BillRepositoryInterface
|
||||
{
|
||||
// app('log')->debug('Now in getPaidDatesInRange()');
|
||||
|
||||
Log::debug(sprintf('Search for linked journals between %s and %s', $start->toW3cString(), $end->toW3cString()));
|
||||
|
||||
return $bill->transactionJournals()
|
||||
->before($end)->after($start)->get(
|
||||
[
|
||||
@@ -485,7 +487,7 @@ class BillRepository implements BillRepositoryInterface
|
||||
{
|
||||
$query = sprintf('%%%s%%', $query);
|
||||
|
||||
return $this->user->bills()->where('name', 'LIKE', $query)->take($limit)->get();
|
||||
return $this->user->bills()->whereLike('name', $query)->take($limit)->get();
|
||||
}
|
||||
|
||||
public function setObjectGroup(Bill $bill, string $objectGroupTitle): Bill
|
||||
|
||||
@@ -57,7 +57,7 @@ class BudgetRepository implements BudgetRepositoryInterface
|
||||
{
|
||||
$search = $this->user->budgets();
|
||||
if ('' !== $query) {
|
||||
$search->where('name', 'LIKE', sprintf('%%%s', $query));
|
||||
$search->whereLike('name', sprintf('%%%s', $query));
|
||||
}
|
||||
$search->orderBy('order', 'ASC')
|
||||
->orderBy('name', 'ASC')->where('active', true)
|
||||
@@ -70,7 +70,7 @@ class BudgetRepository implements BudgetRepositoryInterface
|
||||
{
|
||||
$search = $this->user->budgets();
|
||||
if ('' !== $query) {
|
||||
$search->where('name', 'LIKE', sprintf('%s%%', $query));
|
||||
$search->whereLike('name', sprintf('%s%%', $query));
|
||||
}
|
||||
$search->orderBy('order', 'ASC')
|
||||
->orderBy('name', 'ASC')->where('active', true)
|
||||
@@ -512,7 +512,7 @@ class BudgetRepository implements BudgetRepositoryInterface
|
||||
}
|
||||
$query = sprintf('%%%s%%', $name);
|
||||
|
||||
return $this->user->budgets()->where('name', 'LIKE', $query)->first();
|
||||
return $this->user->budgets()->whereLike('name', $query)->first();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -577,7 +577,7 @@ class BudgetRepository implements BudgetRepositoryInterface
|
||||
{
|
||||
$search = $this->user->budgets();
|
||||
if ('' !== $query) {
|
||||
$search->where('name', 'LIKE', sprintf('%%%s%%', $query));
|
||||
$search->whereLike('name', sprintf('%%%s%%', $query));
|
||||
}
|
||||
$search->orderBy('order', 'ASC')
|
||||
->orderBy('name', 'ASC')->where('active', true)
|
||||
|
||||
@@ -49,7 +49,7 @@ class CategoryRepository implements CategoryRepositoryInterface
|
||||
{
|
||||
$search = $this->user->categories();
|
||||
if ('' !== $query) {
|
||||
$search->where('name', 'LIKE', sprintf('%%%s', $query));
|
||||
$search->whereLike('name', sprintf('%%%s', $query));
|
||||
}
|
||||
|
||||
return $search->take($limit)->get();
|
||||
@@ -59,7 +59,7 @@ class CategoryRepository implements CategoryRepositoryInterface
|
||||
{
|
||||
$search = $this->user->categories();
|
||||
if ('' !== $query) {
|
||||
$search->where('name', 'LIKE', sprintf('%s%%', $query));
|
||||
$search->whereLike('name', sprintf('%s%%', $query));
|
||||
}
|
||||
|
||||
return $search->take($limit)->get();
|
||||
@@ -344,7 +344,7 @@ class CategoryRepository implements CategoryRepositoryInterface
|
||||
{
|
||||
$search = $this->user->categories();
|
||||
if ('' !== $query) {
|
||||
$search->where('name', 'LIKE', sprintf('%%%s%%', $query));
|
||||
$search->whereLike('name', sprintf('%%%s%%', $query));
|
||||
}
|
||||
|
||||
return $search->take($limit)->get();
|
||||
|
||||
@@ -197,7 +197,7 @@ class JournalRepository implements JournalRepositoryInterface
|
||||
->orderBy('date', 'DESC')
|
||||
;
|
||||
if ('' !== $search) {
|
||||
$query->where('description', 'LIKE', sprintf('%%%s%%', $search));
|
||||
$query->whereLike('description', sprintf('%%%s%%', $search));
|
||||
}
|
||||
|
||||
return $query->take($limit)->get();
|
||||
|
||||
@@ -120,7 +120,7 @@ class ObjectGroupRepository implements ObjectGroupRepositoryInterface
|
||||
$parts = explode(' ', $query);
|
||||
foreach ($parts as $part) {
|
||||
$search = sprintf('%%%s%%', $part);
|
||||
$dbQuery->where('title', 'LIKE', $search);
|
||||
$dbQuery->whereLike('title', $search);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -343,7 +343,7 @@ class PiggyBankRepository implements PiggyBankRepositoryInterface
|
||||
{
|
||||
$search = $this->user->piggyBanks();
|
||||
if ('' !== $query) {
|
||||
$search->where('piggy_banks.name', 'LIKE', sprintf('%%%s%%', $query));
|
||||
$search->whereLike('piggy_banks.name', sprintf('%%%s%%', $query));
|
||||
}
|
||||
$search->orderBy('piggy_banks.order', 'ASC')
|
||||
->orderBy('piggy_banks.name', 'ASC')
|
||||
|
||||
@@ -204,6 +204,7 @@ class RecurringRepository implements RecurringRepositoryInterface
|
||||
*/
|
||||
public function getJournalCount(Recurrence $recurrence, ?Carbon $start = null, ?Carbon $end = null): int
|
||||
{
|
||||
Log::debug(sprintf('Now in getJournalCount(#%d, "%s", "%s")', $recurrence->id, $start?->format('Y-m-d H:i:s'), $end?->format('Y-m-d H:i:s')));
|
||||
$query = TransactionJournal::leftJoin('journal_meta', 'journal_meta.transaction_journal_id', '=', 'transaction_journals.id')
|
||||
->where('transaction_journals.user_id', $recurrence->user_id)
|
||||
->whereNull('transaction_journals.deleted_at')
|
||||
@@ -216,8 +217,10 @@ class RecurringRepository implements RecurringRepositoryInterface
|
||||
if (null !== $end) {
|
||||
$query->where('transaction_journals.date', '<=', $end->format('Y-m-d 00:00:00'));
|
||||
}
|
||||
$count = $query->count('transaction_journals.id');
|
||||
Log::debug(sprintf('Count is %d', $count));
|
||||
|
||||
return $query->count('transaction_journals.id');
|
||||
return $count;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -496,7 +499,7 @@ class RecurringRepository implements RecurringRepositoryInterface
|
||||
{
|
||||
$search = $this->user->recurrences();
|
||||
if ('' !== $query) {
|
||||
$search->where('recurrences.title', 'LIKE', sprintf('%%%s%%', $query));
|
||||
$search->whereLike('recurrences.title', sprintf('%%%s%%', $query));
|
||||
}
|
||||
$search
|
||||
->orderBy('recurrences.title', 'ASC')
|
||||
|
||||
@@ -213,7 +213,7 @@ class RuleRepository implements RuleRepositoryInterface
|
||||
{
|
||||
$search = $this->user->rules();
|
||||
if ('' !== $query) {
|
||||
$search->where('rules.title', 'LIKE', sprintf('%%%s%%', $query));
|
||||
$search->whereLike('rules.title', sprintf('%%%s%%', $query));
|
||||
}
|
||||
$search->orderBy('rules.order', 'ASC')
|
||||
->orderBy('rules.title', 'ASC')
|
||||
|
||||
@@ -371,7 +371,7 @@ class RuleGroupRepository implements RuleGroupRepositoryInterface
|
||||
{
|
||||
$search = $this->user->ruleGroups();
|
||||
if ('' !== $query) {
|
||||
$search->where('rule_groups.title', 'LIKE', sprintf('%%%s%%', $query));
|
||||
$search->whereLike('rule_groups.title', sprintf('%%%s%%', $query));
|
||||
}
|
||||
$search->orderBy('rule_groups.order', 'ASC')
|
||||
->orderBy('rule_groups.title', 'ASC')
|
||||
|
||||
@@ -205,7 +205,7 @@ class TagRepository implements TagRepositoryInterface
|
||||
{
|
||||
$search = sprintf('%%%s%%', $query);
|
||||
|
||||
return $this->user->tags()->where('tag', 'LIKE', $search)->get(['tags.*']);
|
||||
return $this->user->tags()->whereLike('tag', $search)->get(['tags.*']);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -217,7 +217,7 @@ class TagRepository implements TagRepositoryInterface
|
||||
$tags = $this->user->tags()->orderBy('tag', 'ASC');
|
||||
if ('' !== $query) {
|
||||
$search = sprintf('%%%s%%', $query);
|
||||
$tags->where('tag', 'LIKE', $search);
|
||||
$tags->whereLike('tag', $search);
|
||||
}
|
||||
|
||||
return $tags->take($limit)->get('tags.*');
|
||||
@@ -309,14 +309,14 @@ class TagRepository implements TagRepositoryInterface
|
||||
{
|
||||
$search = sprintf('%%%s', $query);
|
||||
|
||||
return $this->user->tags()->where('tag', 'LIKE', $search)->get(['tags.*']);
|
||||
return $this->user->tags()->whereLike('tag', $search)->get(['tags.*']);
|
||||
}
|
||||
|
||||
public function tagStartsWith(string $query): Collection
|
||||
{
|
||||
$search = sprintf('%s%%', $query);
|
||||
|
||||
return $this->user->tags()->where('tag', 'LIKE', $search)->get(['tags.*']);
|
||||
return $this->user->tags()->whereLike('tag', $search)->get(['tags.*']);
|
||||
}
|
||||
|
||||
public function transferredInPeriod(Tag $tag, Carbon $start, Carbon $end): array
|
||||
|
||||
@@ -62,6 +62,6 @@ class TransactionTypeRepository implements TransactionTypeRepositoryInterface
|
||||
return TransactionType::get();
|
||||
}
|
||||
|
||||
return TransactionType::where('type', 'LIKE', sprintf('%%%s%%', $query))->take($limit)->get();
|
||||
return TransactionType::whereLike('type', sprintf('%%%s%%', $query))->take($limit)->get();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -270,7 +270,7 @@ class AccountRepository implements AccountRepositoryInterface
|
||||
$query->where('accounts.active', $value);
|
||||
}
|
||||
if ('name' === $column) {
|
||||
$query->where('accounts.name', 'LIKE', sprintf('%%%s%%', $value));
|
||||
$query->whereLike('accounts.name', sprintf('%%%s%%', $value));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -298,34 +298,38 @@ class AccountRepository implements AccountRepositoryInterface
|
||||
return $query->get(['accounts.*']);
|
||||
}
|
||||
|
||||
public function searchAccount(array $query, array $types, int $limit): Collection
|
||||
public function searchAccount(string $query, array $types, int $page, int $limit): Collection
|
||||
{
|
||||
// search by group, not by user
|
||||
$dbQuery = $this->userGroup->accounts()
|
||||
->where('active', true)
|
||||
->orderBy('accounts.updated_at', 'ASC')
|
||||
->orderBy('accounts.order', 'ASC')
|
||||
->orderBy('accounts.account_type_id', 'ASC')
|
||||
->orderBy('accounts.name', 'ASC')
|
||||
->with(['accountType'])
|
||||
;
|
||||
if (count($query) > 0) {
|
||||
|
||||
// split query on spaces just in case:
|
||||
if ('' !== trim($query)) {
|
||||
$dbQuery->where(function (EloquentBuilder $q) use ($query): void {
|
||||
foreach ($query as $line) {
|
||||
$parts = explode(' ', $line);
|
||||
$parts = explode(' ', $query);
|
||||
foreach ($parts as $part) {
|
||||
$search = sprintf('%%%s%%', $part);
|
||||
$q->orWhere('name', 'LIKE', $search);
|
||||
}
|
||||
$q->orWhereLike('name', $search);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (0 !== count($types)) {
|
||||
$dbQuery->leftJoin('account_types', 'accounts.account_type_id', '=', 'account_types.id');
|
||||
$dbQuery->whereIn('account_types.type', $types);
|
||||
}
|
||||
|
||||
return $dbQuery->take($limit)->get(['accounts.*']);
|
||||
$dbQuery->skip(($page - 1) * $limit)->take($limit);
|
||||
|
||||
return $dbQuery->get(['accounts.*']);
|
||||
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
|
||||
@@ -80,7 +80,7 @@ interface AccountRepositoryInterface
|
||||
*/
|
||||
public function resetAccountOrder(): void;
|
||||
|
||||
public function searchAccount(array $query, array $types, int $limit): Collection;
|
||||
public function searchAccount(string $query, array $types, int $page, int $limit): Collection;
|
||||
|
||||
public function setUser(User $user): void;
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ class CategoryRepository implements CategoryRepositoryInterface
|
||||
$parts = explode(' ', $line);
|
||||
foreach ($parts as $part) {
|
||||
$search = sprintf('%%%s%%', $part);
|
||||
$q->orWhere('name', 'LIKE', $search);
|
||||
$q->orWhereLike('name', $search);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -320,7 +320,7 @@ class CurrencyRepository implements CurrencyRepositoryInterface
|
||||
{
|
||||
$query = TransactionCurrency::where('enabled', true);
|
||||
if ('' !== $search) {
|
||||
$query->where('name', 'LIKE', sprintf('%%%s%%', $search));
|
||||
$query->whereLike('name', sprintf('%%%s%%', $search));
|
||||
}
|
||||
|
||||
return $query->take($limit)->get();
|
||||
|
||||
@@ -46,7 +46,7 @@ class JournalRepository implements JournalRepositoryInterface
|
||||
$parts = explode(' ', $line);
|
||||
foreach ($parts as $part) {
|
||||
$search = sprintf('%%%s%%', $part);
|
||||
$q->orWhere('description', 'LIKE', $search);
|
||||
$q->orWhereLike('description', $search);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -44,7 +44,7 @@ class TagRepository implements TagRepositoryInterface
|
||||
$parts = explode(' ', $line);
|
||||
foreach ($parts as $part) {
|
||||
$search = sprintf('%%%s%%', $part);
|
||||
$q->orWhere('tag', 'LIKE', $search);
|
||||
$q->orWhereLike('tag', $search);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -242,6 +242,15 @@ class CreditRecalculateService
|
||||
private function processTransaction(Account $account, string $direction, Transaction $transaction, string $leftOfDebt): string
|
||||
{
|
||||
$journal = $transaction->transactionJournal;
|
||||
|
||||
// here be null pointers.
|
||||
if (null === $journal) {
|
||||
app('log')->warning(sprintf('Transaction #%d has no journal.', $transaction->id));
|
||||
|
||||
return $leftOfDebt;
|
||||
}
|
||||
|
||||
|
||||
$foreignCurrency = $transaction->foreignCurrency;
|
||||
$accountCurrency = $this->repository->getAccountCurrency($account);
|
||||
$type = $journal->transactionType->type;
|
||||
@@ -327,15 +336,17 @@ class CreditRecalculateService
|
||||
|
||||
if ($isSameAccount && $isDebit && $this->isTransferIn($usedAmount, $type)) { // case 9
|
||||
$usedAmount = app('steam')->positive($usedAmount);
|
||||
$result = bcadd($leftOfDebt, $usedAmount);
|
||||
app('log')->debug(sprintf('Case 9 (transfer into debit liability, means you owe more): %s + %s = %s', app('steam')->bcround($leftOfDebt, 2), app('steam')->bcround($usedAmount, 2), app('steam')->bcround($result, 2)));
|
||||
$result = bcsub($leftOfDebt, $usedAmount);
|
||||
// 2024-10-05, #9225 this used to say you would owe more, but a transfer INTO a debit from wherever means you owe LESS.
|
||||
app('log')->debug(sprintf('Case 9 (transfer into debit liability, means you owe LESS): %s - %s = %s', app('steam')->bcround($leftOfDebt, 2), app('steam')->bcround($usedAmount, 2), app('steam')->bcround($result, 2)));
|
||||
|
||||
return $result;
|
||||
}
|
||||
if ($isSameAccount && $isDebit && $this->isTransferOut($usedAmount, $type)) { // case 10
|
||||
$usedAmount = app('steam')->positive($usedAmount);
|
||||
$result = bcsub($leftOfDebt, $usedAmount);
|
||||
app('log')->debug(sprintf('Case 5 (transfer out of debit liability, means you owe less): %s - %s = %s', app('steam')->bcround($leftOfDebt, 2), app('steam')->bcround($usedAmount, 2), app('steam')->bcround($result, 2)));
|
||||
$result = bcadd($leftOfDebt, $usedAmount);
|
||||
// 2024-10-05, #9225 this used to say you would owe less, but a transfer OUT OF a debit from wherever means you owe MORE.
|
||||
app('log')->debug(sprintf('Case 10 (transfer out of debit liability, means you owe MORE): %s + %s = %s', app('steam')->bcround($leftOfDebt, 2), app('steam')->bcround($usedAmount, 2), app('steam')->bcround($result, 2)));
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ use FireflyIII\Models\RecurrenceTransaction;
|
||||
use FireflyIII\Models\RecurrenceTransactionMeta;
|
||||
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
|
||||
use FireflyIII\Validation\AccountValidator;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* Trait RecurringTransactionTrait
|
||||
@@ -212,10 +213,14 @@ trait RecurringTransactionTrait
|
||||
|
||||
private function setBudget(RecurrenceTransaction $transaction, int $budgetId): void
|
||||
{
|
||||
Log::debug(sprintf('Now in %s', __METHOD__));
|
||||
$budgetFactory = app(BudgetFactory::class);
|
||||
$budgetFactory->setUser($transaction->recurrence->user);
|
||||
$budget = $budgetFactory->find($budgetId, null);
|
||||
if (null === $budget) {
|
||||
// remove budget from recurring transaction:
|
||||
$transaction->recurrenceTransactionMeta()->where('name', 'budget_id')->delete();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -235,6 +240,9 @@ trait RecurringTransactionTrait
|
||||
$billFactory->setUser($transaction->recurrence->user);
|
||||
$bill = $billFactory->find($billId, null);
|
||||
if (null === $bill) {
|
||||
// remove bill from recurring transaction:
|
||||
$transaction->recurrenceTransactionMeta()->where('name', 'bill_id')->delete();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -77,9 +77,7 @@ class RecurringCronjob extends AbstractCronjob
|
||||
{
|
||||
app('log')->info(sprintf('Will now fire recurring cron job task for date "%s".', $this->date->format('Y-m-d H:i:s')));
|
||||
|
||||
/** @var CreateRecurringTransactions $job */
|
||||
$job = app(CreateRecurringTransactions::class);
|
||||
$job->setDate($this->date);
|
||||
$job = new CreateRecurringTransactions($this->date);
|
||||
$job->setForce($this->force);
|
||||
$job->handle();
|
||||
|
||||
|
||||
@@ -36,12 +36,14 @@ trait CollectsAccountsFromFilter
|
||||
$collection = new Collection();
|
||||
|
||||
// always collect from the query parameter, even when it's empty.
|
||||
if (null !== $queryParameters['accounts']) {
|
||||
foreach ($queryParameters['accounts'] as $accountId) {
|
||||
$account = $this->repository->find((int) $accountId);
|
||||
if (null !== $account) {
|
||||
$collection->push($account);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if no "preselected", and found accounts
|
||||
if ('empty' === $queryParameters['preselected'] && $collection->count() > 0) {
|
||||
|
||||
@@ -35,6 +35,7 @@ trait ParsesQueryFilters
|
||||
$date = today();
|
||||
|
||||
$value = $parameters->filter()?->value($field, date('Y-m-d'));
|
||||
|
||||
if (is_array($value)) {
|
||||
Log::error(sprintf('Multiple values for date field "%s". Using first value.', $field));
|
||||
$value = $value[0];
|
||||
@@ -65,4 +66,9 @@ trait ParsesQueryFilters
|
||||
{
|
||||
return (string) ($parameters->page()[$field] ?? $default);
|
||||
}
|
||||
|
||||
private function stringFromFilterParams(QueryParameters $parameters, string $field, string $default): string
|
||||
{
|
||||
return (string)$parameters->filter()?->value($field, $default) ?? $default;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,6 +115,7 @@ trait RenderPartialViews
|
||||
|
||||
$budget = $budgetRepository->find((int)$attributes['budgetId']);
|
||||
if (null === $budget) {
|
||||
// transactions without a budget.
|
||||
$budget = new Budget();
|
||||
}
|
||||
$journals = $popupHelper->byBudget($budget, $attributes);
|
||||
|
||||
@@ -120,7 +120,7 @@ trait ExpandsQuery
|
||||
Log::debug(sprintf('Add query filter "%s"', $key));
|
||||
// add type to query:
|
||||
foreach ($filter as $value) {
|
||||
$q->where($key, 'LIKE', sprintf('%%%s%%', $value));
|
||||
$q->whereLike($key, sprintf('%%%s%%', $value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,10 +24,10 @@ declare(strict_types=1);
|
||||
namespace FireflyIII\Support\Models;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use FireflyIII\Exceptions\FireflyException;
|
||||
use FireflyIII\Models\Account;
|
||||
use FireflyIII\Models\AccountBalance;
|
||||
use FireflyIII\Models\Transaction;
|
||||
use FireflyIII\Models\TransactionCurrency;
|
||||
use FireflyIII\Models\TransactionJournal;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
@@ -47,20 +47,15 @@ class AccountBalanceCalculator
|
||||
}
|
||||
|
||||
/**
|
||||
* Recalculate all balances.
|
||||
* Recalculate all account and transaction balances.
|
||||
*/
|
||||
public static function forceRecalculateAll(): void
|
||||
public static function recalculateAll(bool $forced): void
|
||||
{
|
||||
if ($forced) {
|
||||
Transaction::whereNull('deleted_at')->update(['balance_dirty' => true]);
|
||||
$object = new self();
|
||||
$object->optimizedCalculation(new Collection());
|
||||
// also delete account balances.
|
||||
AccountBalance::whereNotNull('created_at')->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Recalculate all balances.
|
||||
*/
|
||||
public static function recalculateAll(): void
|
||||
{
|
||||
$object = new self();
|
||||
$object->optimizedCalculation(new Collection());
|
||||
}
|
||||
@@ -130,12 +125,6 @@ class AccountBalanceCalculator
|
||||
private function optimizedCalculation(Collection $accounts, ?Carbon $notBefore = null): void
|
||||
{
|
||||
Log::debug('start of optimizedCalculation');
|
||||
if (false === config('firefly.feature_flags.running_balance_column')) {
|
||||
Log::debug('optimizedCalculation is disabled, return.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($accounts->count() > 0) {
|
||||
Log::debug(sprintf('Limited to %d account(s)', $accounts->count()));
|
||||
}
|
||||
@@ -162,14 +151,17 @@ class AccountBalanceCalculator
|
||||
|
||||
$set = $query->get(['transactions.id', 'transactions.balance_dirty', 'transactions.transaction_currency_id', 'transaction_journals.date', 'transactions.account_id', 'transactions.amount']);
|
||||
|
||||
// the balance value is an array.
|
||||
// first entry is the balance, second is the date.
|
||||
|
||||
/** @var Transaction $entry */
|
||||
foreach ($set as $entry) {
|
||||
// start with empty array:
|
||||
$balances[$entry->account_id] ??= [];
|
||||
$balances[$entry->account_id][$entry->transaction_currency_id] ??= $this->getLatestBalance($entry->account_id, $entry->transaction_currency_id, $notBefore);
|
||||
$balances[$entry->account_id][$entry->transaction_currency_id] ??= [$this->getLatestBalance($entry->account_id, $entry->transaction_currency_id, $notBefore), null];
|
||||
|
||||
// before and after are easy:
|
||||
$before = $balances[$entry->account_id][$entry->transaction_currency_id];
|
||||
$before = $balances[$entry->account_id][$entry->transaction_currency_id][0];
|
||||
$after = bcadd($before, $entry->amount);
|
||||
if (true === $entry->balance_dirty || $accounts->count() > 0) {
|
||||
// update the transaction:
|
||||
@@ -181,12 +173,13 @@ class AccountBalanceCalculator
|
||||
}
|
||||
|
||||
// then update the array:
|
||||
$balances[$entry->account_id][$entry->transaction_currency_id] = $after;
|
||||
$balances[$entry->account_id][$entry->transaction_currency_id] = [$after, $entry->date];
|
||||
}
|
||||
Log::debug(sprintf('end of optimizedCalculation, corrected %d balance(s)', $count));
|
||||
// then update all transactions.
|
||||
|
||||
// ?? something with accounts?
|
||||
// save all collected balances in their respective account objects.
|
||||
$this->storeAccountBalances($balances);
|
||||
}
|
||||
|
||||
private function getAccountBalanceByJournal(string $title, int $account, int $journal, int $currency): AccountBalance
|
||||
@@ -208,145 +201,182 @@ class AccountBalanceCalculator
|
||||
return $entry;
|
||||
}
|
||||
|
||||
private function recalculateLatest(?Account $account): void
|
||||
{
|
||||
$query = Transaction::groupBy(['transactions.account_id', 'transactions.transaction_currency_id', 'transactions.foreign_currency_id']);
|
||||
|
||||
if (null !== $account) {
|
||||
$query->where('transactions.account_id', $account->id);
|
||||
}
|
||||
$result = $query->get(['transactions.account_id', 'transactions.transaction_currency_id', 'transactions.foreign_currency_id', \DB::raw('SUM(transactions.amount) as sum_amount'), \DB::raw('SUM(transactions.foreign_amount) as sum_foreign_amount')]);
|
||||
|
||||
// reset account balances:
|
||||
$this->resetAccountBalancesByAccount('balance', $account);
|
||||
|
||||
/** @var \stdClass $row */
|
||||
foreach ($result as $row) {
|
||||
$account = (int) $row->account_id;
|
||||
$transactionCurrency = (int) $row->transaction_currency_id;
|
||||
$foreignCurrency = (int) $row->foreign_currency_id;
|
||||
$sumAmount = (string) $row->sum_amount;
|
||||
$sumForeignAmount = (string) $row->sum_foreign_amount;
|
||||
$sumAmount = '' === $sumAmount ? '0' : $sumAmount;
|
||||
$sumForeignAmount = '' === $sumForeignAmount ? '0' : $sumForeignAmount;
|
||||
|
||||
// at this point SQLite may return scientific notation because why not. Terrible.
|
||||
$sumAmount = app('steam')->floatalize($sumAmount);
|
||||
$sumForeignAmount = app('steam')->floatalize($sumForeignAmount);
|
||||
|
||||
// first create for normal currency:
|
||||
$entry = $this->getAccountBalanceByAccount($account, $transactionCurrency);
|
||||
|
||||
try {
|
||||
$entry->balance = bcadd((string) $entry->balance, $sumAmount);
|
||||
} catch (\ValueError $e) {
|
||||
$message = sprintf('[a] Could not add "%s" to "%s": %s', $entry->balance, $sumAmount, $e->getMessage());
|
||||
Log::error($message);
|
||||
|
||||
throw new FireflyException($message, 0, $e);
|
||||
}
|
||||
$entry->save();
|
||||
|
||||
// then do foreign amount, if present:
|
||||
if ($foreignCurrency > 0) {
|
||||
$entry = $this->getAccountBalanceByAccount($account, $foreignCurrency);
|
||||
|
||||
try {
|
||||
$entry->balance = bcadd((string) $entry->balance, $sumForeignAmount);
|
||||
} catch (\ValueError $e) {
|
||||
$message = sprintf('[b] Could not add "%s" to "%s": %s', $entry->balance, $sumForeignAmount, $e->getMessage());
|
||||
Log::error($message);
|
||||
|
||||
throw new FireflyException($message, 0, $e);
|
||||
}
|
||||
$entry->save();
|
||||
}
|
||||
}
|
||||
Log::debug(sprintf('Recalculated %d account balance(s)', $result->count()));
|
||||
}
|
||||
|
||||
private function resetAccountBalancesByAccount(string $title, ?Account $account): void
|
||||
// private function recalculateLatest(?Account $account): void
|
||||
// {
|
||||
// $query = Transaction::groupBy(['transactions.account_id', 'transactions.transaction_currency_id', 'transactions.foreign_currency_id']);
|
||||
//
|
||||
// if (null !== $account) {
|
||||
// $query->where('transactions.account_id', $account->id);
|
||||
// }
|
||||
// $result = $query->get(['transactions.account_id', 'transactions.transaction_currency_id', 'transactions.foreign_currency_id', \DB::raw('SUM(transactions.amount) as sum_amount'), \DB::raw('SUM(transactions.foreign_amount) as sum_foreign_amount')]);
|
||||
//
|
||||
// // reset account balances:
|
||||
// $this->resetAccountBalancesByAccount('balance', $account);
|
||||
//
|
||||
// /** @var \stdClass $row */
|
||||
// foreach ($result as $row) {
|
||||
// $account = (int) $row->account_id;
|
||||
// $transactionCurrency = (int) $row->transaction_currency_id;
|
||||
// $foreignCurrency = (int) $row->foreign_currency_id;
|
||||
// $sumAmount = (string) $row->sum_amount;
|
||||
// $sumForeignAmount = (string) $row->sum_foreign_amount;
|
||||
// $sumAmount = '' === $sumAmount ? '0' : $sumAmount;
|
||||
// $sumForeignAmount = '' === $sumForeignAmount ? '0' : $sumForeignAmount;
|
||||
//
|
||||
// // at this point SQLite may return scientific notation because why not. Terrible.
|
||||
// $sumAmount = app('steam')->floatalize($sumAmount);
|
||||
// $sumForeignAmount = app('steam')->floatalize($sumForeignAmount);
|
||||
//
|
||||
// // first create for normal currency:
|
||||
// $entry = $this->getAccountBalanceByAccount($account, $transactionCurrency);
|
||||
//
|
||||
// try {
|
||||
// $entry->balance = bcadd((string) $entry->balance, $sumAmount);
|
||||
// } catch (\ValueError $e) {
|
||||
// $message = sprintf('[a] Could not add "%s" to "%s": %s', $entry->balance, $sumAmount, $e->getMessage());
|
||||
// Log::error($message);
|
||||
//
|
||||
// throw new FireflyException($message, 0, $e);
|
||||
// }
|
||||
// $entry->save();
|
||||
//
|
||||
// // then do foreign amount, if present:
|
||||
// if ($foreignCurrency > 0) {
|
||||
// $entry = $this->getAccountBalanceByAccount($account, $foreignCurrency);
|
||||
//
|
||||
// try {
|
||||
// $entry->balance = bcadd((string) $entry->balance, $sumForeignAmount);
|
||||
// } catch (\ValueError $e) {
|
||||
// $message = sprintf('[b] Could not add "%s" to "%s": %s', $entry->balance, $sumForeignAmount, $e->getMessage());
|
||||
// Log::error($message);
|
||||
//
|
||||
// throw new FireflyException($message, 0, $e);
|
||||
// }
|
||||
// $entry->save();
|
||||
// }
|
||||
// }
|
||||
// Log::debug(sprintf('Recalculated %d account balance(s)', $result->count()));
|
||||
// }
|
||||
|
||||
// private function resetAccountBalancesByAccount(string $title, ?Account $account): void
|
||||
// {
|
||||
// if (null === $account) {
|
||||
// $count = AccountBalance::whereNotNull('updated_at')->where('title', $title)->update(['balance' => '0']);
|
||||
// Log::debug(sprintf('Set %d account balance(s) to zero.', $count));
|
||||
//
|
||||
// return;
|
||||
// }
|
||||
// $count = AccountBalance::where('account_id', $account->id)->where('title', $title)->update(['balance' => '0']);
|
||||
// Log::debug(sprintf('Set %d account balance(s) of account #%d to zero.', $count, $account->id));
|
||||
// }
|
||||
|
||||
// /**
|
||||
// * Als je alles opnieuw doet, verzamel je alle transactions en het bedrag en zet je dat neer als "balance after
|
||||
// * journal". Dat betekent, netjes op volgorde van datum en doorrekenen.
|
||||
// *
|
||||
// * Zodra je een transaction journal verplaatst (datum) moet je dat journal en alle latere journals opnieuw doen.
|
||||
// * Maar dan moet je van de account wel een beginnetje hebben, namelijk de balance tot en met dat moment.
|
||||
// *
|
||||
// * 1. Dus dan search je eerst naar die SUM, som alle transactions eerder dan (niet inclusief) de journal.
|
||||
// * 2. En vanaf daar pak je alle journals op of na de journal (dus ook de journal zelf) en begin je door te tellen.
|
||||
// * 3. Elke voorbij gaande journal entry "balance_after_journal" geef je een update of voeg je toe.
|
||||
// */
|
||||
// private function recalculateJournals(?Account $account, ?TransactionJournal $transactionJournal): void
|
||||
// {
|
||||
// $query = Transaction::groupBy(['transactions.account_id', 'transaction_journals.id', 'transactions.transaction_currency_id', 'transactions.foreign_currency_id']);
|
||||
// $query->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id');
|
||||
// $query->orderBy('transaction_journals.date', 'asc');
|
||||
// $amounts = [];
|
||||
// if (null !== $account) {
|
||||
// $query->where('transactions.account_id', $account->id);
|
||||
// }
|
||||
// if (null !== $account && null !== $transactionJournal) {
|
||||
// $query->where('transaction_journals.date', '>=', $transactionJournal->date);
|
||||
// $amounts = $this->getStartAmounts($account, $transactionJournal);
|
||||
// }
|
||||
// $result = $query->get(['transactions.account_id', 'transaction_journals.id', 'transactions.transaction_currency_id', 'transactions.foreign_currency_id', \DB::raw('SUM(transactions.amount) as sum_amount'), \DB::raw('SUM(transactions.foreign_amount) as sum_foreign_amount')]);
|
||||
//
|
||||
// /** @var \stdClass $row */
|
||||
// foreach ($result as $row) {
|
||||
// $account = (int) $row->account_id;
|
||||
// $transactionCurrency = (int) $row->transaction_currency_id;
|
||||
// $foreignCurrency = (int) $row->foreign_currency_id;
|
||||
// $sumAmount = (string) $row->sum_amount;
|
||||
// $sumForeignAmount = (string) $row->sum_foreign_amount;
|
||||
// $journalId = (int) $row->id;
|
||||
//
|
||||
// // check for empty strings
|
||||
// $sumAmount = '' === $sumAmount ? '0' : $sumAmount;
|
||||
// $sumForeignAmount = '' === $sumForeignAmount ? '0' : $sumForeignAmount;
|
||||
//
|
||||
// // new amounts:
|
||||
// $amounts[$account][$transactionCurrency] = bcadd($amounts[$account][$transactionCurrency] ?? '0', $sumAmount);
|
||||
// $amounts[$account][$foreignCurrency] = bcadd($amounts[$account][$foreignCurrency] ?? '0', $sumForeignAmount);
|
||||
//
|
||||
// // first create for normal currency:
|
||||
// $entry = self::getAccountBalanceByJournal('balance_after_journal', $account, $journalId, $transactionCurrency);
|
||||
// $entry->balance = $amounts[$account][$transactionCurrency];
|
||||
// $entry->save();
|
||||
//
|
||||
// // then do foreign amount, if present:
|
||||
// if ($foreignCurrency > 0) {
|
||||
// $entry = self::getAccountBalanceByJournal('balance_after_journal', $account, $journalId, $foreignCurrency);
|
||||
// $entry->balance = $amounts[$account][$foreignCurrency];
|
||||
// $entry->save();
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // select transactions.account_id, transaction_journals.id, transactions.transaction_currency_id, transactions.foreign_currency_id, sum(transactions.amount), sum(transactions.foreign_amount)
|
||||
// //
|
||||
// // from transactions
|
||||
// //
|
||||
// // left join transaction_journals ON transaction_journals.id = transactions.transaction_journal_id
|
||||
// //
|
||||
// // group by account_id, transaction_journals.id, transaction_currency_id, foreign_currency_id
|
||||
// // order by transaction_journals.date desc
|
||||
// }
|
||||
|
||||
// private function getStartAmounts(Account $account, TransactionJournal $journal): array
|
||||
// {
|
||||
// exit('here we are 1');
|
||||
//
|
||||
// return [];
|
||||
// }
|
||||
|
||||
private function storeAccountBalances(array $balances): void
|
||||
{
|
||||
/**
|
||||
* @var int $accountId
|
||||
* @var array $currencies
|
||||
*/
|
||||
foreach ($balances as $accountId => $currencies) {
|
||||
/** @var Account $account */
|
||||
$account = Account::find($accountId);
|
||||
if (null === $account) {
|
||||
$count = AccountBalance::whereNotNull('updated_at')->where('title', $title)->update(['balance' => '0']);
|
||||
Log::debug(sprintf('Set %d account balance(s) to zero.', $count));
|
||||
Log::error(sprintf('Could not find account #%d, will not save account balance.', $accountId));
|
||||
|
||||
return;
|
||||
}
|
||||
$count = AccountBalance::where('account_id', $account->id)->where('title', $title)->update(['balance' => '0']);
|
||||
Log::debug(sprintf('Set %d account balance(s) of account #%d to zero.', $count, $account->id));
|
||||
continue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Als je alles opnieuw doet, verzamel je alle transactions en het bedrag en zet je dat neer als "balance after
|
||||
* journal". Dat betekent, netjes op volgorde van datum en doorrekenen.
|
||||
*
|
||||
* Zodra je een transaction journal verplaatst (datum) moet je dat journal en alle latere journals opnieuw doen.
|
||||
* Maar dan moet je van de account wel een beginnetje hebben, namelijk de balance tot en met dat moment.
|
||||
*
|
||||
* 1. Dus dan search je eerst naar die SUM, som alle transactions eerder dan (niet inclusief) de journal.
|
||||
* 2. En vanaf daar pak je alle journals op of na de journal (dus ook de journal zelf) en begin je door te tellen.
|
||||
* 3. Elke voorbij gaande journal entry "balance_after_journal" geef je een update of voeg je toe.
|
||||
* @var int $currencyId
|
||||
* @var array $balance
|
||||
*/
|
||||
private function recalculateJournals(?Account $account, ?TransactionJournal $transactionJournal): void
|
||||
{
|
||||
$query = Transaction::groupBy(['transactions.account_id', 'transaction_journals.id', 'transactions.transaction_currency_id', 'transactions.foreign_currency_id']);
|
||||
$query->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id');
|
||||
$query->orderBy('transaction_journals.date', 'asc');
|
||||
$amounts = [];
|
||||
if (null !== $account) {
|
||||
$query->where('transactions.account_id', $account->id);
|
||||
}
|
||||
if (null !== $account && null !== $transactionJournal) {
|
||||
$query->where('transaction_journals.date', '>=', $transactionJournal->date);
|
||||
$amounts = $this->getStartAmounts($account, $transactionJournal);
|
||||
}
|
||||
$result = $query->get(['transactions.account_id', 'transaction_journals.id', 'transactions.transaction_currency_id', 'transactions.foreign_currency_id', \DB::raw('SUM(transactions.amount) as sum_amount'), \DB::raw('SUM(transactions.foreign_amount) as sum_foreign_amount')]);
|
||||
foreach ($currencies as $currencyId => $balance) {
|
||||
/** @var TransactionCurrency $currency */
|
||||
$currency = TransactionCurrency::find($currencyId);
|
||||
if (null === $currency) {
|
||||
Log::error(sprintf('Could not find currency #%d, will not save account balance.', $currencyId));
|
||||
|
||||
/** @var \stdClass $row */
|
||||
foreach ($result as $row) {
|
||||
$account = (int) $row->account_id;
|
||||
$transactionCurrency = (int) $row->transaction_currency_id;
|
||||
$foreignCurrency = (int) $row->foreign_currency_id;
|
||||
$sumAmount = (string) $row->sum_amount;
|
||||
$sumForeignAmount = (string) $row->sum_foreign_amount;
|
||||
$journalId = (int) $row->id;
|
||||
|
||||
// check for empty strings
|
||||
$sumAmount = '' === $sumAmount ? '0' : $sumAmount;
|
||||
$sumForeignAmount = '' === $sumForeignAmount ? '0' : $sumForeignAmount;
|
||||
|
||||
// new amounts:
|
||||
$amounts[$account][$transactionCurrency] = bcadd($amounts[$account][$transactionCurrency] ?? '0', $sumAmount);
|
||||
$amounts[$account][$foreignCurrency] = bcadd($amounts[$account][$foreignCurrency] ?? '0', $sumForeignAmount);
|
||||
|
||||
// first create for normal currency:
|
||||
$entry = self::getAccountBalanceByJournal('balance_after_journal', $account, $journalId, $transactionCurrency);
|
||||
$entry->balance = $amounts[$account][$transactionCurrency];
|
||||
$entry->save();
|
||||
|
||||
// then do foreign amount, if present:
|
||||
if ($foreignCurrency > 0) {
|
||||
$entry = self::getAccountBalanceByJournal('balance_after_journal', $account, $journalId, $foreignCurrency);
|
||||
$entry->balance = $amounts[$account][$foreignCurrency];
|
||||
$entry->save();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// select transactions.account_id, transaction_journals.id, transactions.transaction_currency_id, transactions.foreign_currency_id, sum(transactions.amount), sum(transactions.foreign_amount)
|
||||
//
|
||||
// from transactions
|
||||
//
|
||||
// left join transaction_journals ON transaction_journals.id = transactions.transaction_journal_id
|
||||
//
|
||||
// group by account_id, transaction_journals.id, transaction_currency_id, foreign_currency_id
|
||||
// order by transaction_journals.date desc
|
||||
}
|
||||
|
||||
private function getStartAmounts(Account $account, TransactionJournal $journal): array
|
||||
{
|
||||
exit('here we are 1');
|
||||
|
||||
return [];
|
||||
/** @var AccountBalance $object */
|
||||
$object = $account->accountBalances()->firstOrCreate(['title' => 'running_balance', 'balance' => '0', 'transaction_currency_id' => $currencyId, 'date' => $balance[1]]);
|
||||
$object->balance = $balance[0];
|
||||
$object->date = $balance[1];
|
||||
$object->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,7 +151,9 @@ class Preferences
|
||||
|
||||
public function beginsWith(User $user, string $search): Collection
|
||||
{
|
||||
return Preference::where('user_id', $user->id)->where('name', 'LIKE', $search.'%')->get();
|
||||
$value = sprintf('%s%%', $search);
|
||||
|
||||
return Preference::where('user_id', $user->id)->whereLike('name', $value)->get();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -105,11 +105,11 @@ trait ConvertsDataTypes
|
||||
/**
|
||||
* Return string value.
|
||||
*/
|
||||
public function convertString(string $field): string
|
||||
public function convertString(string $field, string $default = ''): string
|
||||
{
|
||||
$entry = $this->get($field);
|
||||
if (!is_scalar($entry)) {
|
||||
return '';
|
||||
return $default;
|
||||
}
|
||||
|
||||
return (string)$this->clearString((string)$entry);
|
||||
|
||||
@@ -73,9 +73,9 @@ class AccountSearch implements GenericSearchInterface
|
||||
case self::SEARCH_ALL:
|
||||
$searchQuery->where(
|
||||
static function (Builder $q) use ($like): void { // @phpstan-ignore-line
|
||||
$q->where('accounts.id', 'LIKE', $like);
|
||||
$q->orWhere('accounts.name', 'LIKE', $like);
|
||||
$q->orWhere('accounts.iban', 'LIKE', $like);
|
||||
$q->whereLike('accounts.id', $like);
|
||||
$q->orWhereLike('accounts.name', $like);
|
||||
$q->orWhereLike('accounts.iban', $like);
|
||||
}
|
||||
);
|
||||
// meta data:
|
||||
@@ -83,7 +83,7 @@ class AccountSearch implements GenericSearchInterface
|
||||
static function (Builder $q) use ($originalQuery): void { // @phpstan-ignore-line
|
||||
$json = json_encode($originalQuery, JSON_THROW_ON_ERROR);
|
||||
$q->where('account_meta.name', '=', 'account_number');
|
||||
$q->where('account_meta.data', 'LIKE', $json);
|
||||
$q->whereLike('account_meta.data', $json);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -95,12 +95,12 @@ class AccountSearch implements GenericSearchInterface
|
||||
break;
|
||||
|
||||
case self::SEARCH_NAME:
|
||||
$searchQuery->where('accounts.name', 'LIKE', $like);
|
||||
$searchQuery->whereLike('accounts.name', $like);
|
||||
|
||||
break;
|
||||
|
||||
case self::SEARCH_IBAN:
|
||||
$searchQuery->where('accounts.iban', 'LIKE', $like);
|
||||
$searchQuery->whereLike('accounts.iban', $like);
|
||||
|
||||
break;
|
||||
|
||||
|
||||
@@ -81,7 +81,7 @@ class UpdatePiggybank implements ActionInterface
|
||||
|
||||
if ($source->account_id === $piggyBank->account_id) {
|
||||
app('log')->debug('Piggy bank account is linked to source, so remove amount from piggy bank.');
|
||||
$this->removeAmount($piggyBank, $journalObj, $destination->amount);
|
||||
$this->removeAmount($piggyBank, $journal, $journalObj, $destination->amount);
|
||||
|
||||
event(
|
||||
new TriggeredAuditLog(
|
||||
@@ -102,7 +102,7 @@ class UpdatePiggybank implements ActionInterface
|
||||
}
|
||||
if ($destination->account_id === $piggyBank->account_id) {
|
||||
app('log')->debug('Piggy bank account is linked to source, so add amount to piggy bank.');
|
||||
$this->addAmount($piggyBank, $journalObj, $destination->amount);
|
||||
$this->addAmount($piggyBank, $journal, $journalObj, $destination->amount);
|
||||
|
||||
event(
|
||||
new TriggeredAuditLog(
|
||||
@@ -115,6 +115,7 @@ class UpdatePiggybank implements ActionInterface
|
||||
'decimal_places' => $journalObj->transactionCurrency->decimal_places,
|
||||
'amount' => $destination->amount,
|
||||
'piggy' => $piggyBank->name,
|
||||
'piggy_id' => $piggyBank->id,
|
||||
]
|
||||
)
|
||||
);
|
||||
@@ -138,7 +139,7 @@ class UpdatePiggybank implements ActionInterface
|
||||
return $user->piggyBanks()->where('piggy_banks.name', $name)->first();
|
||||
}
|
||||
|
||||
private function removeAmount(PiggyBank $piggyBank, TransactionJournal $journal, string $amount): void
|
||||
private function removeAmount(PiggyBank $piggyBank, array $array, TransactionJournal $journal, string $amount): void
|
||||
{
|
||||
$repository = app(PiggyBankRepositoryInterface::class);
|
||||
$repository->setUser($journal->user);
|
||||
@@ -154,6 +155,7 @@ class UpdatePiggybank implements ActionInterface
|
||||
// if amount is zero, stop.
|
||||
if (0 === bccomp('0', $amount)) {
|
||||
app('log')->warning('Amount left is zero, stop.');
|
||||
event(new RuleActionFailedOnArray($this->action, $array, trans('rules.cannot_remove_zero_piggy', ['name' => $piggyBank->name])));
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -161,6 +163,7 @@ class UpdatePiggybank implements ActionInterface
|
||||
// make sure we can remove amount:
|
||||
if (false === $repository->canRemoveAmount($piggyBank, $amount)) {
|
||||
app('log')->warning(sprintf('Cannot remove %s from piggy bank.', $amount));
|
||||
event(new RuleActionFailedOnArray($this->action, $array, trans('rules.cannot_remove_from_piggy', ['amount' => $amount, 'name' => $piggyBank->name])));
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -169,7 +172,7 @@ class UpdatePiggybank implements ActionInterface
|
||||
$repository->removeAmount($piggyBank, $amount, $journal);
|
||||
}
|
||||
|
||||
private function addAmount(PiggyBank $piggyBank, TransactionJournal $journal, string $amount): void
|
||||
private function addAmount(PiggyBank $piggyBank, array $array, TransactionJournal $journal, string $amount): void
|
||||
{
|
||||
$repository = app(PiggyBankRepositoryInterface::class);
|
||||
$repository->setUser($journal->user);
|
||||
@@ -190,6 +193,7 @@ class UpdatePiggybank implements ActionInterface
|
||||
// if amount is zero, stop.
|
||||
if (0 === bccomp('0', $amount)) {
|
||||
app('log')->warning('Amount left is zero, stop.');
|
||||
event(new RuleActionFailedOnArray($this->action, $array, trans('rules.cannot_add_zero_piggy', ['name' => $piggyBank->name])));
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -197,6 +201,7 @@ class UpdatePiggybank implements ActionInterface
|
||||
// make sure we can add amount:
|
||||
if (false === $repository->canAddAmount($piggyBank, $amount)) {
|
||||
app('log')->warning(sprintf('Cannot add %s to piggy bank.', $amount));
|
||||
event(new RuleActionFailedOnArray($this->action, $array, trans('rules.cannot_add_to_piggy', ['amount' => $amount, 'name' => $piggyBank->name])));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -194,11 +194,19 @@ class BillTransformer extends AbstractTransformer
|
||||
$searchStart = clone $start;
|
||||
$start->subDay();
|
||||
|
||||
app('log')->debug(sprintf('Parameters are start: %s end: %s', $start->format('Y-m-d'), $this->parameters->get('end')->format('Y-m-d')));
|
||||
/** @var Carbon $end */
|
||||
$end = clone $this->parameters->get('end');
|
||||
$searchEnd = clone $end;
|
||||
|
||||
// move the search dates to the start of the day.
|
||||
$searchStart->startOfDay();
|
||||
$searchEnd->endOfDay();
|
||||
|
||||
app('log')->debug(sprintf('Parameters are start: %s end: %s', $start->format('Y-m-d'), $end->format('Y-m-d')));
|
||||
app('log')->debug(sprintf('Search parameters are: start: %s', $searchStart->format('Y-m-d')));
|
||||
|
||||
// Get from database when bill was paid.
|
||||
$set = $this->repository->getPaidDatesInRange($bill, $searchStart, $this->parameters->get('end'));
|
||||
$set = $this->repository->getPaidDatesInRange($bill, $searchStart, $searchEnd);
|
||||
app('log')->debug(sprintf('Count %d entries in getPaidDatesInRange()', $set->count()));
|
||||
|
||||
// Grab from array the most recent payment. If none exist, fall back to the start date and pretend *that* was the last paid date.
|
||||
|
||||
@@ -86,7 +86,7 @@ class AccountValidator
|
||||
app('log')->debug('AccountValidator source is set to NULL');
|
||||
}
|
||||
if (null !== $account) {
|
||||
app('log')->debug(sprintf('AccountValidator source is set to #%d: "%s" (%s)', $account->id, $account->name, $account->accountType->type));
|
||||
app('log')->debug(sprintf('AccountValidator source is set to #%d: "%s" (%s)', $account->id, $account->name, $account->accountType?->type));
|
||||
}
|
||||
$this->source = $account;
|
||||
}
|
||||
|
||||
@@ -73,10 +73,27 @@ class FireflyValidator extends Validator
|
||||
if (is_array($secret)) {
|
||||
$secret = '';
|
||||
}
|
||||
$secret = (string) $secret;
|
||||
|
||||
return (bool) \Google2FA::verifyKey((string) $secret, $value);
|
||||
}
|
||||
|
||||
public function validateExistingMfaCode($attribute, $value): bool
|
||||
{
|
||||
if (!is_string($value) || 6 !== strlen($value)) {
|
||||
return false;
|
||||
}
|
||||
$user = auth()->user();
|
||||
if (null === $user) {
|
||||
app('log')->error('No user during validate2faCode');
|
||||
|
||||
return false;
|
||||
}
|
||||
$secret = (string)$user->mfa_secret;
|
||||
|
||||
return (bool) \Google2FA::verifyKey($secret, $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $attribute
|
||||
* @param mixed $value
|
||||
|
||||
@@ -30,6 +30,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
||||
- [Issue 9155](https://github.com/firefly-iii/firefly-iii/issues/9155) (internal_reference_is does not correctly match numeric internal references) reported by @Lrns123
|
||||
- [Issue 9275](https://github.com/firefly-iii/firefly-iii/issues/9275) (Long wait when editing a transaction) reported by @JC5
|
||||
- [Issue 9278](https://github.com/firefly-iii/firefly-iii/issues/9278) (Update to v6.1.20 changed Balance of Account) reported by @JeuJeus
|
||||
- [Issue 9281](https://github.com/firefly-iii/firefly-iii/issues/9281) (Update to v6.1.20 leads to a type error) reported by @krakonos1602
|
||||
|
||||
### API
|
||||
|
||||
|
||||
@@ -98,6 +98,7 @@
|
||||
"league/commonmark": "2.*",
|
||||
"league/csv": "^9.10",
|
||||
"league/fractal": "0.*",
|
||||
"mailersend/laravel-driver": "^2.7",
|
||||
"nunomaduro/collision": "^8",
|
||||
"pragmarx/google2fa": "^8.0",
|
||||
"predis/predis": "^2.2",
|
||||
@@ -194,7 +195,8 @@
|
||||
"optimize-autoloader": true,
|
||||
"allow-plugins": {
|
||||
"composer/package-versions-deprecated": true,
|
||||
"phpstan/extension-installer": true
|
||||
"phpstan/extension-installer": true,
|
||||
"php-http/discovery": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
1452
composer.lock
generated
1452
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -110,7 +110,7 @@ return [
|
||||
'running_balance_column' => env('USE_RUNNING_BALANCE', false),
|
||||
// see cer.php for exchange rates feature flag.
|
||||
],
|
||||
'version' => 'develop/2024-09-29',
|
||||
'version' => 'develop/2024-11-04',
|
||||
'api_version' => '2.1.0',
|
||||
'db_version' => 24,
|
||||
|
||||
|
||||
@@ -45,7 +45,9 @@ return [
|
||||
'timeout' => null,
|
||||
'verify_peer' => null !== env('MAIL_ENCRYPTION'),
|
||||
],
|
||||
|
||||
'mailersend' => [
|
||||
'transport' => 'mailersend',
|
||||
],
|
||||
'ses' => [
|
||||
'transport' => 'ses',
|
||||
],
|
||||
|
||||
2180
package-lock.json
generated
2180
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -131,7 +131,7 @@ function initRevenueACField(fieldName) {
|
||||
}
|
||||
});
|
||||
sourceNames.initialize();
|
||||
$('input[name="' + fieldName + '"]').typeahead({hint: true, highlight: true,}, {source: sourceNames, displayKey: 'name', autoSelect: false});
|
||||
$('input[name="' + fieldName + '"]').typeahead({hint: true, highlight: true,}, {source: sourceNames, displayKey: 'name', autoselect: true});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,5 +161,5 @@ function initCategoryAC() {
|
||||
}
|
||||
});
|
||||
categories.initialize();
|
||||
$('input[name="category"]').typeahead({hint: true, highlight: true,}, {source: categories, displayKey: 'name', autoSelect: false});
|
||||
$('input[name="category"]').typeahead({hint: true, highlight: true,}, {source: categories, displayKey: 'name', autoselect: true});
|
||||
}
|
||||
|
||||
@@ -337,6 +337,13 @@ function updateTriggerInput(selectList) {
|
||||
console.log('Select list value is ' + selectList.val() + ', so input needs auto complete.');
|
||||
createAutoComplete(inputResult, 'api/v1/autocomplete/tags');
|
||||
break;
|
||||
case 'bill_contains':
|
||||
case 'bill_ends':
|
||||
case 'bill_is':
|
||||
case 'bill_starts':
|
||||
console.log('Select list value is ' + selectList.val() + ', so input needs auto complete.');
|
||||
createAutoComplete(inputResult, 'api/v1/autocomplete/bills');
|
||||
break;
|
||||
case 'budget_is':
|
||||
console.log('Select list value is ' + selectList.val() + ', so input needs auto complete.');
|
||||
createAutoComplete(inputResult, 'api/v1/autocomplete/budgets');
|
||||
|
||||
6
public/v1/js/lib/typeahead/bloodhound.js
Normal file → Executable file
6
public/v1/js/lib/typeahead/bloodhound.js
Normal file → Executable file
@@ -1,7 +1,7 @@
|
||||
/*!
|
||||
* typeahead.js 1.3.1
|
||||
* typeahead.js 1.3.3
|
||||
* https://github.com/corejavascript/typeahead.js
|
||||
* Copyright 2013-2020 Twitter, Inc. and other contributors; Licensed MIT
|
||||
* Copyright 2013-2024 Twitter, Inc. and other contributors; Licensed MIT
|
||||
*/
|
||||
|
||||
|
||||
@@ -159,7 +159,7 @@
|
||||
noop: function() {}
|
||||
};
|
||||
}();
|
||||
var VERSION = "1.3.1";
|
||||
var VERSION = "1.3.3";
|
||||
var tokenizers = function() {
|
||||
"use strict";
|
||||
return {
|
||||
|
||||
6
public/v1/js/lib/typeahead/bloodhound.min.js
vendored
Normal file → Executable file
6
public/v1/js/lib/typeahead/bloodhound.min.js
vendored
Normal file → Executable file
File diff suppressed because one or more lines are too long
7
public/v1/js/lib/typeahead/typeahead.bundle.js
Normal file → Executable file
7
public/v1/js/lib/typeahead/typeahead.bundle.js
Normal file → Executable file
@@ -1,7 +1,7 @@
|
||||
/*!
|
||||
* typeahead.js 1.3.1
|
||||
* typeahead.js 1.3.3
|
||||
* https://github.com/corejavascript/typeahead.js
|
||||
* Copyright 2013-2020 Twitter, Inc. and other contributors; Licensed MIT
|
||||
* Copyright 2013-2024 Twitter, Inc. and other contributors; Licensed MIT
|
||||
*/
|
||||
|
||||
|
||||
@@ -159,7 +159,7 @@
|
||||
noop: function() {}
|
||||
};
|
||||
}();
|
||||
var VERSION = "1.3.1";
|
||||
var VERSION = "1.3.3";
|
||||
var tokenizers = function() {
|
||||
"use strict";
|
||||
return {
|
||||
@@ -1446,6 +1446,7 @@
|
||||
});
|
||||
this.$input.attr({
|
||||
"aria-owns": id + "_listbox",
|
||||
"aria-controls": id + "_listbox",
|
||||
role: "combobox",
|
||||
"aria-autocomplete": "list",
|
||||
"aria-expanded": false
|
||||
|
||||
6
public/v1/js/lib/typeahead/typeahead.bundle.min.js
vendored
Normal file → Executable file
6
public/v1/js/lib/typeahead/typeahead.bundle.min.js
vendored
Normal file → Executable file
File diff suppressed because one or more lines are too long
5
public/v1/js/lib/typeahead/typeahead.jquery.js
Normal file → Executable file
5
public/v1/js/lib/typeahead/typeahead.jquery.js
Normal file → Executable file
@@ -1,7 +1,7 @@
|
||||
/*!
|
||||
* typeahead.js 1.3.1
|
||||
* typeahead.js 1.3.3
|
||||
* https://github.com/corejavascript/typeahead.js
|
||||
* Copyright 2013-2020 Twitter, Inc. and other contributors; Licensed MIT
|
||||
* Copyright 2013-2024 Twitter, Inc. and other contributors; Licensed MIT
|
||||
*/
|
||||
|
||||
|
||||
@@ -499,6 +499,7 @@
|
||||
});
|
||||
this.$input.attr({
|
||||
"aria-owns": id + "_listbox",
|
||||
"aria-controls": id + "_listbox",
|
||||
role: "combobox",
|
||||
"aria-autocomplete": "list",
|
||||
"aria-expanded": false
|
||||
|
||||
6
public/v1/js/lib/typeahead/typeahead.jquery.min.js
vendored
Normal file → Executable file
6
public/v1/js/lib/typeahead/typeahead.jquery.min.js
vendored
Normal file → Executable file
File diff suppressed because one or more lines are too long
@@ -15,7 +15,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@johmun/vue-tags-input": "^2",
|
||||
"@vue/compiler-sfc": "^3.5.3",
|
||||
"@vue/compiler-sfc": "^3.5.11",
|
||||
"axios": "^1.7",
|
||||
"bootstrap-sass": "^3",
|
||||
"cross-env": "^7.0",
|
||||
|
||||
@@ -29,11 +29,11 @@
|
||||
"bootstrap5-tags": "^1.7",
|
||||
"chart.js": "^4.4.0",
|
||||
"chartjs-adapter-date-fns": "^3.0.0",
|
||||
"chartjs-chart-sankey": "^0.12.1",
|
||||
"chartjs-chart-sankey": "^0.13.0",
|
||||
"date-fns": "^4.0.0",
|
||||
"i18next": "^23.11.2",
|
||||
"i18next": "^23.15.2",
|
||||
"i18next-chained-backend": "^4.6.2",
|
||||
"i18next-http-backend": "^2.4.2",
|
||||
"i18next-http-backend": "^2.6.2",
|
||||
"i18next-localstorage-backend": "^4.2.0",
|
||||
"leaflet": "^1.9.4",
|
||||
"store": "^2.0.12"
|
||||
|
||||
@@ -80,4 +80,10 @@ return [
|
||||
'revenue_accounts' => 'Revenue accounts',
|
||||
'liabilities_accounts' => 'Liabilities',
|
||||
'placeholder' => '[Placeholder]',
|
||||
|
||||
// mfa
|
||||
'profile_mfa' => 'Multi-factor authentication',
|
||||
'mfa_enableMFA' => 'Enable multi-factor authentication',
|
||||
'mfa_backup_codes' => 'Backup codes',
|
||||
'mfa_disableMFA' => 'Disable multi-factor authentication',
|
||||
];
|
||||
|
||||
@@ -134,5 +134,46 @@ return [
|
||||
'bill_warning_end_date_zero' => 'Your bill **":name"** is due to end on :date. This moment will pass **TODAY!**',
|
||||
'bill_warning_extension_date_zero' => 'Your bill **":name"** is due to be extended or cancelled on :date. This moment will pass **TODAY!**',
|
||||
'bill_warning_please_action' => 'Please take the appropriate action.',
|
||||
|
||||
// user has enabled MFA
|
||||
'enabled_mfa_subject' => 'You have enabled multi-factor authentication',
|
||||
'enabled_mfa_slack' => 'You (:email) have enabled multi-factor authentication. Is this not correct? Check your settings!',
|
||||
'have_enabled_mfa' => 'You have enabled multi-factor authentication on your Firefly III account ":email". This means that you will need to use an authenticator app to log in from now on.',
|
||||
'enabled_mfa_warning' => 'If you did not enable this, please contact your administrator immediately or check out the Firefly III documentation.',
|
||||
|
||||
'disabled_mfa_subject' => 'You have disabled multi-factor authentication!',
|
||||
'disabled_mfa_slack' => 'You (:email) have disabled multi-factor authentication. Is this not correct? Check your settings!',
|
||||
'have_disabled_mfa' => 'You have disabled multi-factor authentication on your Firefly III account ":email".',
|
||||
'disabled_mfa_warning' => 'If you did not disable this, please contact your administrator immediately or check out the Firefly III documentation.',
|
||||
|
||||
'new_backup_codes_subject' => 'You have generated new back-up codes',
|
||||
'new_backup_codes_slack' => 'You (:email) have generated new back-up codes. These can be used to login to Firefly III. Is this not correct? Check your settings!',
|
||||
'new_backup_codes_intro' => 'You (:email) have generated new back-up codes. These can be used to login to Firefly III if you lose access to your authenticator app.',
|
||||
'new_backup_codes_warning' => 'Please store these codes in a safe place. If you lose them, you will not be able to log in to Firefly III. If you did not do this, please contact your administrator immediately or check out the Firefly III documentation.',
|
||||
|
||||
'used_backup_code_subject' => 'You have used a back-up code to login',
|
||||
'used_backup_code_slack' => 'You (:email) have used a back-up code to login',
|
||||
|
||||
'used_backup_code_intro' => 'You (:email) have used a back-up code to login to Firefly III. You now have one less back-up code to login with. Please remove it from your list.',
|
||||
'used_backup_code_warning' => 'If you did not do this, please contact your administrator immediately or check out the Firefly III documentation.',
|
||||
|
||||
// few left:
|
||||
'mfa_few_backups_left_subject' => 'You have only :count backup code(s) left!',
|
||||
'mfa_few_backups_left_slack' => 'You (:email) have only :count backup code(s) left!',
|
||||
'few_backup_codes_intro' => 'You (:email) have used most of your backup codes, and now have only :count left. Please generate new ones as soon as possible.',
|
||||
'few_backup_codes_warning' => 'Without backup codes, you cannot recover your MFA login if you lose access to your code generator.',
|
||||
|
||||
// NO left:
|
||||
'mfa_no_backups_left_subject' => 'You have NO backup codes left!',
|
||||
'mfa_no_backups_left_slack' => 'You (:email) NO backup codes left!',
|
||||
'no_backup_codes_intro' => 'You (:email) have used ALL of your backup codes. Please generate new ones as soon as possible.',
|
||||
'no_backup_codes_warning' => 'Without backup codes, you cannot recover your MFA login if you lose access to your code generator.',
|
||||
|
||||
// many failed MFA attempts
|
||||
'mfa_many_failed_subject' => 'You have tried and failed to use multi-factor authentication :count times now!',
|
||||
'mfa_many_failed_slack' => 'You (:email) have tried and failed to use multi-factor authentication :count times now. Is this not correct? Check your settings!',
|
||||
'mfa_many_failed_attempts_intro' => 'You (:email) have tried :count times to use a multi-factor authentication code, but these login attempts have failed. Are you sure you are using the right MFA code? Are you sure the time on the server is correct?',
|
||||
'mfa_many_failed_attempts_warning' => 'If you did not do this, please contact your administrator immediately or check out the Firefly III documentation.',
|
||||
|
||||
];
|
||||
// Ignore this comment
|
||||
|
||||
@@ -755,7 +755,7 @@ return [
|
||||
'instructions_rule_from_journal' => 'Create a rule based on one of your transactions. Complement or submit the form below.',
|
||||
'rule_is_strict' => 'strict rule',
|
||||
'rule_is_not_strict' => 'non-strict rule',
|
||||
'rule_help_stop_processing' => 'When you check this box, later rules in this group will not be executed.',
|
||||
'rule_help_stop_processing' => 'When you check this box, later rules in this group will not be executed if this particular rule is executed.',
|
||||
'rule_help_strict' => 'In strict rules ALL triggers must fire for the action(s) to be executed. In non-strict rules, ANY trigger is enough for the action(s) to be executed.',
|
||||
'rule_help_active' => 'Inactive rules will never fire.',
|
||||
'stored_new_rule' => 'Stored new rule with title ":title"',
|
||||
@@ -1320,18 +1320,18 @@ return [
|
||||
'pref_custom_fiscal_year_label' => 'Enabled',
|
||||
'pref_custom_fiscal_year_help' => 'In countries that use a financial year other than January 1 to December 31, you can switch this on and specify start / end days of the fiscal year',
|
||||
'pref_fiscal_year_start_label' => 'Fiscal year start date',
|
||||
'pref_two_factor_auth' => '2-step verification',
|
||||
'pref_two_factor_auth_help' => 'When you enable 2-step verification (also known as two-factor authentication), you add an extra layer of security to your account. You sign in with something you know (your password) and something you have (a verification code). Verification codes are generated by an application on your phone, such as Authy or Google Authenticator.',
|
||||
'pref_enable_two_factor_auth' => 'Enable 2-step verification',
|
||||
'pref_two_factor_auth_disabled' => '2-step verification code removed and disabled',
|
||||
'pref_two_factor_auth' => 'Multi-factor authentication',
|
||||
'pref_two_factor_auth_help' => 'When you enable multi-factor authentication (also known as two-factor authentication), you add an extra layer of security to your account. You sign in with something you know (your password) and something you have (a verification code). Verification codes are generated by an application on your phone, such as Authy or Google Authenticator.',
|
||||
'pref_enable_two_factor_auth' => 'Enable multi-factor authentication',
|
||||
'pref_two_factor_auth_disabled' => 'Multi-factor authentication verification code removed and disabled',
|
||||
'pref_two_factor_auth_remove_it' => 'Don\'t forget to remove the account from your authentication app!',
|
||||
'pref_two_factor_auth_code' => 'Verify code',
|
||||
'pref_two_factor_auth_code_help' => 'Scan the QR code with an application on your phone such as Authy or Google Authenticator and enter the generated code.',
|
||||
'pref_two_factor_auth_code_help' => 'Scan the QR code with an application on your phone such as Authy or Google Authenticator and enter the generated code. The QR code changes every time you visit this page. Make sure you use the most recent one.',
|
||||
'pref_two_factor_auth_reset_code' => 'Reset verification code',
|
||||
'pref_two_factor_auth_disable_2fa' => 'Disable 2FA',
|
||||
'pref_two_factor_auth_disable_2fa' => 'Disable MFA',
|
||||
'2fa_use_secret_instead' => 'If you cannot scan the QR code, feel free to use the secret instead: <code>:secret</code>.',
|
||||
'2fa_backup_codes' => 'Store these backup codes for access in case you lose your device.',
|
||||
'2fa_already_enabled' => '2-step verification is already enabled.',
|
||||
'2fa_already_enabled' => 'Multi-factor authentication verification is already enabled.',
|
||||
'wrong_mfa_code' => 'This MFA code is not valid.',
|
||||
'pref_save_settings' => 'Save settings',
|
||||
'saved_preferences' => 'Preferences saved!',
|
||||
@@ -1413,7 +1413,27 @@ return [
|
||||
'administration_role_view_reports' => 'View reports',
|
||||
'administration_role_full' => 'Full access',
|
||||
|
||||
// mfa
|
||||
'enable_mfa' => 'Enable multi-factor authentication',
|
||||
'mfa_index_title' => 'Multi-factor authentication',
|
||||
'mfa_index_intro' => 'Firefly III supports multi-factor authentication (MFA). You can enable MFA for your account to add an extra layer of security. Applications like Authy, Google Authenticator and FreeOTP can be used to generate the codes you need to log in. Security keys are not supported by Firefly III but you can use a security key as a storage device for your MFA secret.',
|
||||
'mfa_index_enabled' => 'Multi-factor authentication is enabled for your account.',
|
||||
'mfa_index_disabled' => 'Multi-factor authentication is not enabled for your account.',
|
||||
'mfa_index_owner' => 'The owner of this instance will always be able to disable multi-factor authentication for your account.',
|
||||
'current_password_confirm_mfa' => 'Enter your current password',
|
||||
'mfa_warning_code_changes' => 'You may get a MFA dialog after you entered your password and a MFA code. In that case, please wait for your application to generate a new MFA code, and do not recycle the one you just used.',
|
||||
'mfa_already_disabled' => 'Multi-factor authentication is not enabled, so you cannot disable it.',
|
||||
'disable_mfa_page' => 'Disable multi-factor authentication',
|
||||
'disable_mfa_intro' => 'You can disable multi-factor authentication. To do so, please enter your password and a multi-factor authentication code. If you want to disable multi-factor authentication because you have lost access to your code generator, <a href="https://docs.firefly-iii.org/references/faq/firefly-iii/using/#i-lost-my-2fa-token-generator-or-2fa-has-stopped-working">please refer to the documentation instead</a>.',
|
||||
'pref_disable_mfa' => 'Disable multi-factor authentication',
|
||||
'mfa_not_enabled' => 'Multi-factor authentication is not enabled.',
|
||||
'mfa_backup_codes_intro' => 'Firefly III can generate backup codes for you. These codes can be used to log in when you cannot use your code generator. You can generate a new set of codes at any time. If you generate a new set, the old set will be invalidated.',
|
||||
'mfa_backup_codes_quick' => 'If you are very fast coming from the setup page of multi-factor authentication, your app may not have generated a new code yet. Please know that MFA codes can only be used once. Make sure you use a different code from the previous one.',
|
||||
'mfa_backup_codes_title' => 'Multi-factor authentication backup codes',
|
||||
'mfa_backup_codes_post_title' => 'Multi-factor authentication backup codes',
|
||||
|
||||
// profile:
|
||||
'manage_mfa_settings' => 'Manage multi-factor authentication settings',
|
||||
'purge_data_title' => 'Purge data from Firefly III',
|
||||
'purge_data_expl' => '"Purging" means "deleting that which is already deleted". In normal circumstances, Firefly III deletes nothing permanently. It just hides it. The button below deletes all of these previously "deleted" records FOREVER.',
|
||||
'delete_stuff_header' => 'Delete and purge data',
|
||||
@@ -1471,6 +1491,7 @@ return [
|
||||
'delete_your_account_password' => 'Enter your password to continue.',
|
||||
'password' => 'Password',
|
||||
'are_you_sure' => 'Are you sure? You cannot undo this.',
|
||||
'are_you_sure_confirm' => 'Are you sure?',
|
||||
'delete_account_button' => 'DELETE your account',
|
||||
'invalid_current_password' => 'Invalid current password!',
|
||||
'password_changed' => 'Password changed!',
|
||||
@@ -2606,6 +2627,7 @@ return [
|
||||
'no_bills_create_default' => 'Create a bill',
|
||||
|
||||
// recurring transactions
|
||||
'recurrence_max_count' => 'This recurring transactions will be created at most :max time(s), and has been created :count time(s) already.',
|
||||
'create_right_now' => 'Create right now',
|
||||
'no_new_transaction_in_recurrence' => 'No new transaction was created. Perhaps it was already fired for this date?',
|
||||
'recurrences' => 'Recurring transactions',
|
||||
|
||||
@@ -71,4 +71,8 @@ return [
|
||||
'cannot_find_category' => 'Firefly III can\'t find category ":name"',
|
||||
'cannot_set_budget' => 'Firefly III can\'t set budget ":name" to a transaction of type ":type"',
|
||||
'journal_invalid_amount' => 'Firefly III can\'t set amount ":amount" because it is not a valid number.',
|
||||
'cannot_remove_zero_piggy' => 'Cannot remove zero amount from piggy bank ":name"',
|
||||
'cannot_remove_from_piggy' => 'Cannot remove ":amount" from piggy bank ":name"',
|
||||
'cannot_add_zero_piggy' => 'Cannot add zero amount to piggy bank ":name"',
|
||||
'cannot_add_to_piggy' => 'Cannot add ":amount" to piggy bank ":name"',
|
||||
];
|
||||
|
||||
@@ -272,6 +272,7 @@ return [
|
||||
'no_auth_user_group' => 'You have to be logged in to access 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.',
|
||||
'existing_mfa_code' => 'Please enter a valid code',
|
||||
];
|
||||
|
||||
// Ignore this comment
|
||||
|
||||
@@ -67,6 +67,18 @@
|
||||
{% endif %}
|
||||
{% if accounts.count() == 0 and page == 1 %}
|
||||
{% include 'partials.empty' with {objectType: objectType, type: 'accounts',route: route('accounts.create', [objectType])} %}
|
||||
|
||||
{% if inactiveCount > 0 %}
|
||||
<p class="text-center"><small>
|
||||
<em>
|
||||
<a href="{{ route('accounts.inactive.index', objectType) }}" class="text-muted">
|
||||
{{ trans_choice('firefly.inactive_account_link', inactiveCount) }}
|
||||
</a>
|
||||
</em>
|
||||
</small>
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user