mirror of
https://github.com/firefly-iii/firefly-iii.git
synced 2025-12-01 02:21:45 +00:00
Compare commits
134 Commits
develop-20
...
develop-20
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
78253f9e1e | ||
|
|
ebd0848c7f | ||
|
|
c8461eb0b5 | ||
|
|
a4cbdeaeac | ||
|
|
3e1ce69d52 | ||
|
|
08a26b976e | ||
|
|
5fc55381a2 | ||
|
|
dbf3d24ae7 | ||
|
|
cc7c6e02c5 | ||
|
|
b45aa85853 | ||
|
|
e7526ac5e3 | ||
|
|
441ada70b8 | ||
|
|
dedc06a46b | ||
|
|
b0adf1b277 | ||
|
|
28f65e9f44 | ||
|
|
a013af5f0d | ||
|
|
9552701662 | ||
|
|
ef52f0aad1 | ||
|
|
0b6f04905a | ||
|
|
cdb36357d4 | ||
|
|
8938622bd9 | ||
|
|
b210294aa9 | ||
|
|
5b02f20775 | ||
|
|
fac382a5df | ||
|
|
88d88bebc9 | ||
|
|
755fb9c29b | ||
|
|
51a835ab51 | ||
|
|
c9895ab182 | ||
|
|
e71d46a4e5 | ||
|
|
8d1d5f37c1 | ||
|
|
525a68682d | ||
|
|
715648d0d8 | ||
|
|
9452e93f22 | ||
|
|
a6aa145471 | ||
|
|
25aa6dcb59 | ||
|
|
bb2270b274 | ||
|
|
d7f6b4143e | ||
|
|
0cf0e26fa8 | ||
|
|
cc23197d60 | ||
|
|
bc1721d95e | ||
|
|
0d19173da6 | ||
|
|
1983f07d3c | ||
|
|
aad1b91cc2 | ||
|
|
8cb1057a33 | ||
|
|
b178032985 | ||
|
|
561213e95d | ||
|
|
44fa7c4306 | ||
|
|
e2169563e2 | ||
|
|
845344e003 | ||
|
|
cdb48453e8 | ||
|
|
9669cef518 | ||
|
|
f962f71ed7 | ||
|
|
94ed4021fb | ||
|
|
1765855c57 | ||
|
|
55cf3e7d44 | ||
|
|
9f1840dc05 | ||
|
|
78dab2e5f9 | ||
|
|
103b9d5005 | ||
|
|
1b75b778d8 | ||
|
|
7e665dbdfc | ||
|
|
b6897ec3a9 | ||
|
|
660260174a | ||
|
|
78d32865b5 | ||
|
|
edfa92c1aa | ||
|
|
63012f269c | ||
|
|
7d0e7f779f | ||
|
|
b8e18f80f4 | ||
|
|
481b01e4f7 | ||
|
|
edf2030251 | ||
|
|
bd1cfffb61 | ||
|
|
629f70d27d | ||
|
|
57f5ebc0f9 | ||
|
|
b4f51e7b47 | ||
|
|
d78d254e86 | ||
|
|
eb0c113699 | ||
|
|
23045ebd59 | ||
|
|
2e5931f304 | ||
|
|
a620b07c00 | ||
|
|
cb724145f2 | ||
|
|
8ef17f6686 | ||
|
|
debfd9160c | ||
|
|
f2482e4ace |
254
.ci/php-cs-fixer/composer.lock
generated
254
.ci/php-cs-fixer/composer.lock
generated
@@ -72,26 +72,26 @@
|
||||
},
|
||||
{
|
||||
"name": "composer/pcre",
|
||||
"version": "3.2.0",
|
||||
"version": "3.3.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/composer/pcre.git",
|
||||
"reference": "ea4ab6f9580a4fd221e0418f2c357cdd39102a90"
|
||||
"reference": "63aaeac21d7e775ff9bc9d45021e1745c97521c4"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/composer/pcre/zipball/ea4ab6f9580a4fd221e0418f2c357cdd39102a90",
|
||||
"reference": "ea4ab6f9580a4fd221e0418f2c357cdd39102a90",
|
||||
"url": "https://api.github.com/repos/composer/pcre/zipball/63aaeac21d7e775ff9bc9d45021e1745c97521c4",
|
||||
"reference": "63aaeac21d7e775ff9bc9d45021e1745c97521c4",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^7.4 || ^8.0"
|
||||
},
|
||||
"conflict": {
|
||||
"phpstan/phpstan": "<1.11.8"
|
||||
"phpstan/phpstan": "<1.11.10"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpstan/phpstan": "^1.11.8",
|
||||
"phpstan/phpstan": "^1.11.10",
|
||||
"phpstan/phpstan-strict-rules": "^1.1",
|
||||
"phpunit/phpunit": "^8 || ^9"
|
||||
},
|
||||
@@ -131,7 +131,7 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/composer/pcre/issues",
|
||||
"source": "https://github.com/composer/pcre/tree/3.2.0"
|
||||
"source": "https://github.com/composer/pcre/tree/3.3.1"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -147,28 +147,28 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-07-25T09:36:02+00:00"
|
||||
"time": "2024-08-27T18:44:43+00:00"
|
||||
},
|
||||
{
|
||||
"name": "composer/semver",
|
||||
"version": "3.4.2",
|
||||
"version": "3.4.3",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/composer/semver.git",
|
||||
"reference": "c51258e759afdb17f1fd1fe83bc12baaef6309d6"
|
||||
"reference": "4313d26ada5e0c4edfbd1dc481a92ff7bff91f12"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/composer/semver/zipball/c51258e759afdb17f1fd1fe83bc12baaef6309d6",
|
||||
"reference": "c51258e759afdb17f1fd1fe83bc12baaef6309d6",
|
||||
"url": "https://api.github.com/repos/composer/semver/zipball/4313d26ada5e0c4edfbd1dc481a92ff7bff91f12",
|
||||
"reference": "4313d26ada5e0c4edfbd1dc481a92ff7bff91f12",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^5.3.2 || ^7.0 || ^8.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpstan/phpstan": "^1.4",
|
||||
"symfony/phpunit-bridge": "^4.2 || ^5"
|
||||
"phpstan/phpstan": "^1.11",
|
||||
"symfony/phpunit-bridge": "^3 || ^7"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
@@ -212,7 +212,7 @@
|
||||
"support": {
|
||||
"irc": "ircs://irc.libera.chat:6697/composer",
|
||||
"issues": "https://github.com/composer/semver/issues",
|
||||
"source": "https://github.com/composer/semver/tree/3.4.2"
|
||||
"source": "https://github.com/composer/semver/tree/3.4.3"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -228,7 +228,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-07-12T11:35:52+00:00"
|
||||
"time": "2024-09-19T14:15:21+00:00"
|
||||
},
|
||||
{
|
||||
"name": "composer/xdebug-handler",
|
||||
@@ -345,16 +345,16 @@
|
||||
},
|
||||
{
|
||||
"name": "fidry/cpu-core-counter",
|
||||
"version": "1.1.0",
|
||||
"version": "1.2.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/theofidry/cpu-core-counter.git",
|
||||
"reference": "f92996c4d5c1a696a6a970e20f7c4216200fcc42"
|
||||
"reference": "8520451a140d3f46ac33042715115e290cf5785f"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/f92996c4d5c1a696a6a970e20f7c4216200fcc42",
|
||||
"reference": "f92996c4d5c1a696a6a970e20f7c4216200fcc42",
|
||||
"url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/8520451a140d3f46ac33042715115e290cf5785f",
|
||||
"reference": "8520451a140d3f46ac33042715115e290cf5785f",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -394,7 +394,7 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/theofidry/cpu-core-counter/issues",
|
||||
"source": "https://github.com/theofidry/cpu-core-counter/tree/1.1.0"
|
||||
"source": "https://github.com/theofidry/cpu-core-counter/tree/1.2.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -402,20 +402,20 @@
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2024-02-07T09:43:46+00:00"
|
||||
"time": "2024-08-06T10:04:20+00:00"
|
||||
},
|
||||
{
|
||||
"name": "friendsofphp/php-cs-fixer",
|
||||
"version": "v3.61.1",
|
||||
"version": "v3.64.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git",
|
||||
"reference": "94a87189f55814e6cabca2d9a33b06de384a2ab8"
|
||||
"reference": "58dd9c931c785a79739310aef5178928305ffa67"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/94a87189f55814e6cabca2d9a33b06de384a2ab8",
|
||||
"reference": "94a87189f55814e6cabca2d9a33b06de384a2ab8",
|
||||
"url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/58dd9c931c785a79739310aef5178928305ffa67",
|
||||
"reference": "58dd9c931c785a79739310aef5178928305ffa67",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -497,7 +497,7 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues",
|
||||
"source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.61.1"
|
||||
"source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.64.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -505,7 +505,7 @@
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2024-07-31T14:33:15+00:00"
|
||||
"time": "2024-08-30T23:09:38+00:00"
|
||||
},
|
||||
{
|
||||
"name": "psr/container",
|
||||
@@ -612,16 +612,16 @@
|
||||
},
|
||||
{
|
||||
"name": "psr/log",
|
||||
"version": "3.0.0",
|
||||
"version": "3.0.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/php-fig/log.git",
|
||||
"reference": "fe5ea303b0887d5caefd3d431c3e61ad47037001"
|
||||
"reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/php-fig/log/zipball/fe5ea303b0887d5caefd3d431c3e61ad47037001",
|
||||
"reference": "fe5ea303b0887d5caefd3d431c3e61ad47037001",
|
||||
"url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3",
|
||||
"reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -656,9 +656,9 @@
|
||||
"psr-3"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/php-fig/log/tree/3.0.0"
|
||||
"source": "https://github.com/php-fig/log/tree/3.0.2"
|
||||
},
|
||||
"time": "2021-07-14T16:46:02+00:00"
|
||||
"time": "2024-09-11T13:17:53+00:00"
|
||||
},
|
||||
{
|
||||
"name": "react/cache",
|
||||
@@ -1259,16 +1259,16 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/console",
|
||||
"version": "v7.1.3",
|
||||
"version": "v7.1.6",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/console.git",
|
||||
"reference": "cb1dcb30ebc7005c29864ee78adb47b5fb7c3cd9"
|
||||
"reference": "bb5192af6edc797cbab5c8e8ecfea2fe5f421e57"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/console/zipball/cb1dcb30ebc7005c29864ee78adb47b5fb7c3cd9",
|
||||
"reference": "cb1dcb30ebc7005c29864ee78adb47b5fb7c3cd9",
|
||||
"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.3"
|
||||
"source": "https://github.com/symfony/console/tree/v7.1.6"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -1348,7 +1348,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-07-26T12:41:01+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.2",
|
||||
"version": "v7.1.6",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/filesystem.git",
|
||||
"reference": "92a91985250c251de9b947a14bb2c9390b1a562c"
|
||||
"reference": "c835867b3c62bb05c7fe3d637c871c7ae52024d4"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/filesystem/zipball/92a91985250c251de9b947a14bb2c9390b1a562c",
|
||||
"reference": "92a91985250c251de9b947a14bb2c9390b1a562c",
|
||||
"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.2"
|
||||
"source": "https://github.com/symfony/filesystem/tree/v7.1.6"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -1637,20 +1637,20 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-06-28T10:03:55+00:00"
|
||||
"time": "2024-10-25T15:11:02+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/finder",
|
||||
"version": "v7.1.3",
|
||||
"version": "v7.1.6",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/finder.git",
|
||||
"reference": "717c6329886f32dc65e27461f80f2a465412fdca"
|
||||
"reference": "2cb89664897be33f78c65d3d2845954c8d7a43b8"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/finder/zipball/717c6329886f32dc65e27461f80f2a465412fdca",
|
||||
"reference": "717c6329886f32dc65e27461f80f2a465412fdca",
|
||||
"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.3"
|
||||
"source": "https://github.com/symfony/finder/tree/v7.1.6"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -1701,20 +1701,20 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-07-24T07:08:44+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,24 +1768,24 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-05-31T14:57:53+00:00"
|
||||
"time": "2024-09-25T14:20:29+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-ctype",
|
||||
"version": "v1.30.0",
|
||||
"version": "v1.31.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-ctype.git",
|
||||
"reference": "0424dff1c58f028c451efff2045f5d92410bd540"
|
||||
"reference": "a3cc8b044a6ea513310cbd48ef7333b384945638"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/0424dff1c58f028c451efff2045f5d92410bd540",
|
||||
"reference": "0424dff1c58f028c451efff2045f5d92410bd540",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638",
|
||||
"reference": "a3cc8b044a6ea513310cbd48ef7333b384945638",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.1"
|
||||
"php": ">=7.2"
|
||||
},
|
||||
"provide": {
|
||||
"ext-ctype": "*"
|
||||
@@ -1831,7 +1831,7 @@
|
||||
"portable"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.30.0"
|
||||
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.31.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -1847,24 +1847,24 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-05-31T15:07:36+00:00"
|
||||
"time": "2024-09-09T11:45:10+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-intl-grapheme",
|
||||
"version": "v1.30.0",
|
||||
"version": "v1.31.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-intl-grapheme.git",
|
||||
"reference": "64647a7c30b2283f5d49b874d84a18fc22054b7a"
|
||||
"reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/64647a7c30b2283f5d49b874d84a18fc22054b7a",
|
||||
"reference": "64647a7c30b2283f5d49b874d84a18fc22054b7a",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe",
|
||||
"reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.1"
|
||||
"php": ">=7.2"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-intl": "For best performance"
|
||||
@@ -1909,7 +1909,7 @@
|
||||
"shim"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.30.0"
|
||||
"source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.31.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -1925,24 +1925,24 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-05-31T15:07:36+00:00"
|
||||
"time": "2024-09-09T11:45:10+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-intl-normalizer",
|
||||
"version": "v1.30.0",
|
||||
"version": "v1.31.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-intl-normalizer.git",
|
||||
"reference": "a95281b0be0d9ab48050ebd988b967875cdb9fdb"
|
||||
"reference": "3833d7255cc303546435cb650316bff708a1c75c"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/a95281b0be0d9ab48050ebd988b967875cdb9fdb",
|
||||
"reference": "a95281b0be0d9ab48050ebd988b967875cdb9fdb",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c",
|
||||
"reference": "3833d7255cc303546435cb650316bff708a1c75c",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.1"
|
||||
"php": ">=7.2"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-intl": "For best performance"
|
||||
@@ -1990,7 +1990,7 @@
|
||||
"shim"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.30.0"
|
||||
"source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.31.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -2006,24 +2006,24 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-05-31T15:07:36+00:00"
|
||||
"time": "2024-09-09T11:45:10+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-mbstring",
|
||||
"version": "v1.30.0",
|
||||
"version": "v1.31.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-mbstring.git",
|
||||
"reference": "fd22ab50000ef01661e2a31d850ebaa297f8e03c"
|
||||
"reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/fd22ab50000ef01661e2a31d850ebaa297f8e03c",
|
||||
"reference": "fd22ab50000ef01661e2a31d850ebaa297f8e03c",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/85181ba99b2345b0ef10ce42ecac37612d9fd341",
|
||||
"reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.1"
|
||||
"php": ">=7.2"
|
||||
},
|
||||
"provide": {
|
||||
"ext-mbstring": "*"
|
||||
@@ -2070,7 +2070,7 @@
|
||||
"shim"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.30.0"
|
||||
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.31.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -2086,24 +2086,24 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-06-19T12:30:46+00:00"
|
||||
"time": "2024-09-09T11:45:10+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-php80",
|
||||
"version": "v1.30.0",
|
||||
"version": "v1.31.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-php80.git",
|
||||
"reference": "77fa7995ac1b21ab60769b7323d600a991a90433"
|
||||
"reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/77fa7995ac1b21ab60769b7323d600a991a90433",
|
||||
"reference": "77fa7995ac1b21ab60769b7323d600a991a90433",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/60328e362d4c2c802a54fcbf04f9d3fb892b4cf8",
|
||||
"reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.1"
|
||||
"php": ">=7.2"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
@@ -2150,7 +2150,7 @@
|
||||
"shim"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-php80/tree/v1.30.0"
|
||||
"source": "https://github.com/symfony/polyfill-php80/tree/v1.31.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -2166,24 +2166,24 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-05-31T15:07:36+00:00"
|
||||
"time": "2024-09-09T11:45:10+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-php81",
|
||||
"version": "v1.30.0",
|
||||
"version": "v1.31.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-php81.git",
|
||||
"reference": "3fb075789fb91f9ad9af537c4012d523085bd5af"
|
||||
"reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/3fb075789fb91f9ad9af537c4012d523085bd5af",
|
||||
"reference": "3fb075789fb91f9ad9af537c4012d523085bd5af",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c",
|
||||
"reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.1"
|
||||
"php": ">=7.2"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
@@ -2226,7 +2226,7 @@
|
||||
"shim"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-php81/tree/v1.30.0"
|
||||
"source": "https://github.com/symfony/polyfill-php81/tree/v1.31.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -2242,20 +2242,20 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-06-19T12:30:46+00:00"
|
||||
"time": "2024-09-09T11:45:10+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/process",
|
||||
"version": "v7.1.3",
|
||||
"version": "v7.1.6",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/process.git",
|
||||
"reference": "7f2f542c668ad6c313dc4a5e9c3321f733197eca"
|
||||
"reference": "6aaa189ddb4ff6b5de8fa3210f2fb42c87b4d12e"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/process/zipball/7f2f542c668ad6c313dc4a5e9c3321f733197eca",
|
||||
"reference": "7f2f542c668ad6c313dc4a5e9c3321f733197eca",
|
||||
"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.3"
|
||||
"source": "https://github.com/symfony/process/tree/v7.1.6"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -2303,7 +2303,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-07-26T12:44:47+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.3",
|
||||
"version": "v7.1.6",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/string.git",
|
||||
"reference": "ea272a882be7f20cad58d5d78c215001617b7f07"
|
||||
"reference": "61b72d66bf96c360a727ae6232df5ac83c71f626"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/string/zipball/ea272a882be7f20cad58d5d78c215001617b7f07",
|
||||
"reference": "ea272a882be7f20cad58d5d78c215001617b7f07",
|
||||
"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.3"
|
||||
"source": "https://github.com/symfony/string/tree/v7.1.6"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -2535,16 +2535,16 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-07-22T10:25:37+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
|
||||
@@ -312,6 +313,12 @@ PUSHER_ID=
|
||||
DEMO_USERNAME=
|
||||
DEMO_PASSWORD=
|
||||
|
||||
#
|
||||
# Disable or enable the running balance column data
|
||||
# Please disable this. It's a very experimental feature.
|
||||
#
|
||||
USE_RUNNING_BALANCE=false
|
||||
|
||||
#
|
||||
# The v2 layout is very experimental. If it breaks you get to keep both parts.
|
||||
# Be wary of data loss.
|
||||
|
||||
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'
|
||||
|
||||
@@ -4,6 +4,9 @@ Over time, many people have contributed to Firefly III. Their efforts are not al
|
||||
Please find below all the people who contributed to the Firefly III code. Their names are mentioned in the year of their first contribution.
|
||||
|
||||
## 2024
|
||||
- Jhon Pedroza
|
||||
- mzhubail
|
||||
- tasnim
|
||||
- withbest
|
||||
- Steve Wasiura
|
||||
- imlonghao
|
||||
|
||||
@@ -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,9 +68,9 @@ class AccountController extends Controller
|
||||
*/
|
||||
public function accounts(AutocompleteRequest $request): JsonResponse
|
||||
{
|
||||
$queryParameters = $request->getParameters();
|
||||
$result = $this->repository->searchAccount($queryParameters['query'], $queryParameters['account_types'], $queryParameters['size']);
|
||||
$return = [];
|
||||
$params = $request->getParameters();
|
||||
$result = $this->repository->searchAccount($params['query'], $params['account_types'], $params['page'], $params['size']);
|
||||
$return = [];
|
||||
|
||||
/** @var Account $account */
|
||||
foreach ($result as $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,30 +76,36 @@ class Cron extends Command
|
||||
}
|
||||
|
||||
// Fire recurring transaction cron job.
|
||||
try {
|
||||
$this->recurringCronJob($force, $date);
|
||||
} catch (FireflyException $e) {
|
||||
app('log')->error($e->getMessage());
|
||||
app('log')->error($e->getTraceAsString());
|
||||
$this->friendlyError($e->getMessage());
|
||||
if ($doAll || $this->option('create-recurring')) {
|
||||
try {
|
||||
$this->recurringCronJob($force, $date);
|
||||
} catch (FireflyException $e) {
|
||||
app('log')->error($e->getMessage());
|
||||
app('log')->error($e->getTraceAsString());
|
||||
$this->friendlyError($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// Fire auto-budget cron job:
|
||||
try {
|
||||
$this->autoBudgetCronJob($force, $date);
|
||||
} catch (FireflyException $e) {
|
||||
app('log')->error($e->getMessage());
|
||||
app('log')->error($e->getTraceAsString());
|
||||
$this->friendlyError($e->getMessage());
|
||||
if ($doAll || $this->option('create-auto-budgets')) {
|
||||
try {
|
||||
$this->autoBudgetCronJob($force, $date);
|
||||
} catch (FireflyException $e) {
|
||||
app('log')->error($e->getMessage());
|
||||
app('log')->error($e->getTraceAsString());
|
||||
$this->friendlyError($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// Fire bill warning cron job
|
||||
try {
|
||||
$this->billWarningCronJob($force, $date);
|
||||
} catch (FireflyException $e) {
|
||||
app('log')->error($e->getMessage());
|
||||
app('log')->error($e->getTraceAsString());
|
||||
$this->friendlyError($e->getMessage());
|
||||
if ($doAll || $this->option('send-bill-warnings')) {
|
||||
try {
|
||||
$this->billWarningCronJob($force, $date);
|
||||
} catch (FireflyException $e) {
|
||||
app('log')->error($e->getMessage());
|
||||
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,23 +45,30 @@ class CorrectAccountBalance extends Command
|
||||
|
||||
return 0;
|
||||
}
|
||||
$this->friendlyWarning('This command has been disabled.');
|
||||
$this->markAsExecuted();
|
||||
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
|
||||
{
|
||||
$configVar = app('fireflyconfig')->get(self::CONFIG_NAME, false);
|
||||
|
||||
return (bool)$configVar?->data;
|
||||
return (bool) $configVar?->data;
|
||||
}
|
||||
|
||||
private function markAsExecuted(): void
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -717,7 +717,7 @@ trait MetaCollection
|
||||
|
||||
$this->joinMetaDataTables();
|
||||
$this->query->where('journal_meta.name', '=', 'internal_reference');
|
||||
$this->query->where('journal_meta.data', '=', $internalReference);
|
||||
$this->query->where('journal_meta.data', '=', sprintf('%s', json_encode($internalReference)));
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
@@ -47,7 +52,7 @@ class TwoFactorController extends Controller
|
||||
/** @var User $user */
|
||||
$user = auth()->user();
|
||||
$siteOwner = config('firefly.site_owner');
|
||||
$title = (string)trans('firefly.two_factor_forgot_title');
|
||||
$title = (string) trans('firefly.two_factor_forgot_title');
|
||||
|
||||
return view('auth.lost-two-factor', compact('user', 'siteOwner', 'title'));
|
||||
}
|
||||
@@ -59,7 +64,7 @@ class TwoFactorController extends Controller
|
||||
{
|
||||
/** @var array $mfaHistory */
|
||||
$mfaHistory = app('preferences')->get('mfa_history', [])->data;
|
||||
$mfaCode = (string)$request->get('one_time_password');
|
||||
$mfaCode = (string) $request->get('one_time_password');
|
||||
|
||||
// is in history? then refuse to use it.
|
||||
if ($this->inMFAHistory($mfaCode, $mfaHistory)) {
|
||||
@@ -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'));
|
||||
|
||||
@@ -77,17 +77,18 @@ class ShowController extends Controller
|
||||
*/
|
||||
public function show(Recurrence $recurrence)
|
||||
{
|
||||
$repos = app(AttachmentRepositoryInterface::class);
|
||||
$repos = app(AttachmentRepositoryInterface::class);
|
||||
|
||||
/** @var RecurrenceTransformer $transformer */
|
||||
$transformer = app(RecurrenceTransformer::class);
|
||||
$transformer = app(RecurrenceTransformer::class);
|
||||
$transformer->setParameters(new ParameterBag());
|
||||
|
||||
$array = $transformer->transform($recurrence);
|
||||
$array = $transformer->transform($recurrence);
|
||||
|
||||
$groups = $this->recurring->getTransactions($recurrence);
|
||||
$today = today(config('app.timezone'));
|
||||
$array['repeat_until'] = null !== $array['repeat_until'] ? new Carbon($array['repeat_until']) : null;
|
||||
$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) {
|
||||
@@ -103,9 +104,9 @@ class ShowController extends Controller
|
||||
}
|
||||
|
||||
// add attachments to the recurrence object.
|
||||
$attachments = $recurrence->attachments()->get();
|
||||
$array['attachments'] = [];
|
||||
$attachmentTransformer = app(AttachmentTransformer::class);
|
||||
$attachments = $recurrence->attachments()->get();
|
||||
$array['attachments'] = [];
|
||||
$attachmentTransformer = app(AttachmentTransformer::class);
|
||||
|
||||
/** @var Attachment $attachment */
|
||||
foreach ($attachments as $attachment) {
|
||||
@@ -114,7 +115,7 @@ class ShowController extends Controller
|
||||
$array['attachments'][] = $item;
|
||||
}
|
||||
|
||||
$subTitle = (string)trans('firefly.overview_for_recurrence', ['title' => $recurrence->title]);
|
||||
$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,7 +42,8 @@ class TokenFormRequest extends FormRequest
|
||||
{
|
||||
// fixed
|
||||
return [
|
||||
'code' => 'required|2faCode',
|
||||
'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;
|
||||
|
||||
@@ -9,11 +9,14 @@ use FireflyIII\Rules\IsBoolean;
|
||||
use FireflyIII\Rules\IsValidPositiveAmount;
|
||||
use FireflyIII\Rules\UniqueAccountNumber;
|
||||
use FireflyIII\Rules\UniqueIban;
|
||||
use FireflyIII\Support\Request\ConvertsDataTypes;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use LaravelJsonApi\Laravel\Http\Requests\ResourceRequest;
|
||||
|
||||
class AccountRequest extends ResourceRequest
|
||||
{
|
||||
use ConvertsDataTypes;
|
||||
|
||||
/**
|
||||
* Get the validation rules for the resource.
|
||||
*/
|
||||
|
||||
@@ -28,8 +28,6 @@ class AccountSchema extends Schema
|
||||
*/
|
||||
public function fields(): array
|
||||
{
|
||||
Log::debug(__METHOD__);
|
||||
|
||||
return [
|
||||
ID::make(),
|
||||
Attribute::make('created_at'),
|
||||
|
||||
@@ -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;
|
||||
@@ -93,7 +100,7 @@ class EventServiceProvider extends ServiceProvider
|
||||
protected $listen
|
||||
= [
|
||||
// is a User related event.
|
||||
RegisteredUser::class => [
|
||||
RegisteredUser::class => [
|
||||
'FireflyIII\Handlers\Events\UserEventHandler@sendRegistrationMail',
|
||||
'FireflyIII\Handlers\Events\UserEventHandler@sendAdminRegistrationNotification',
|
||||
'FireflyIII\Handlers\Events\UserEventHandler@attachUserRole',
|
||||
@@ -101,110 +108,133 @@ class EventServiceProvider extends ServiceProvider
|
||||
'FireflyIII\Handlers\Events\UserEventHandler@createExchangeRates',
|
||||
],
|
||||
// is a User related event.
|
||||
Login::class => [
|
||||
Login::class => [
|
||||
'FireflyIII\Handlers\Events\UserEventHandler@checkSingleUserIsAdmin',
|
||||
'FireflyIII\Handlers\Events\UserEventHandler@demoUserBackToEnglish',
|
||||
],
|
||||
ActuallyLoggedIn::class => [
|
||||
ActuallyLoggedIn::class => [
|
||||
'FireflyIII\Handlers\Events\UserEventHandler@storeUserIPAddress',
|
||||
],
|
||||
DetectedNewIPAddress::class => [
|
||||
DetectedNewIPAddress::class => [
|
||||
'FireflyIII\Handlers\Events\UserEventHandler@notifyNewIPAddress',
|
||||
],
|
||||
RequestedVersionCheckStatus::class => [
|
||||
RequestedVersionCheckStatus::class => [
|
||||
'FireflyIII\Handlers\Events\VersionCheckEventHandler@checkForUpdates',
|
||||
],
|
||||
RequestedReportOnJournals::class => [
|
||||
RequestedReportOnJournals::class => [
|
||||
'FireflyIII\Handlers\Events\AutomationHandler@reportJournals',
|
||||
],
|
||||
|
||||
// is a User related event.
|
||||
RequestedNewPassword::class => [
|
||||
RequestedNewPassword::class => [
|
||||
'FireflyIII\Handlers\Events\UserEventHandler@sendNewPassword',
|
||||
],
|
||||
// is a User related event.
|
||||
UserChangedEmail::class => [
|
||||
UserChangedEmail::class => [
|
||||
'FireflyIII\Handlers\Events\UserEventHandler@sendEmailChangeConfirmMail',
|
||||
'FireflyIII\Handlers\Events\UserEventHandler@sendEmailChangeUndoMail',
|
||||
],
|
||||
// admin related
|
||||
AdminRequestedTestMessage::class => [
|
||||
AdminRequestedTestMessage::class => [
|
||||
'FireflyIII\Handlers\Events\AdminEventHandler@sendTestMessage',
|
||||
],
|
||||
NewVersionAvailable::class => [
|
||||
NewVersionAvailable::class => [
|
||||
'FireflyIII\Handlers\Events\AdminEventHandler@sendNewVersion',
|
||||
],
|
||||
InvitationCreated::class => [
|
||||
InvitationCreated::class => [
|
||||
'FireflyIII\Handlers\Events\AdminEventHandler@sendInvitationNotification',
|
||||
'FireflyIII\Handlers\Events\UserEventHandler@sendRegistrationInvite',
|
||||
],
|
||||
|
||||
// is a Transaction Journal related event.
|
||||
StoredTransactionGroup::class => [
|
||||
StoredTransactionGroup::class => [
|
||||
'FireflyIII\Handlers\Events\StoredGroupEventHandler@processRules',
|
||||
'FireflyIII\Handlers\Events\StoredGroupEventHandler@recalculateCredit',
|
||||
'FireflyIII\Handlers\Events\StoredGroupEventHandler@triggerWebhooks',
|
||||
],
|
||||
// is a Transaction Journal related event.
|
||||
UpdatedTransactionGroup::class => [
|
||||
UpdatedTransactionGroup::class => [
|
||||
'FireflyIII\Handlers\Events\UpdatedGroupEventHandler@unifyAccounts',
|
||||
'FireflyIII\Handlers\Events\UpdatedGroupEventHandler@processRules',
|
||||
'FireflyIII\Handlers\Events\UpdatedGroupEventHandler@recalculateCredit',
|
||||
'FireflyIII\Handlers\Events\UpdatedGroupEventHandler@triggerWebhooks',
|
||||
],
|
||||
DestroyedTransactionGroup::class => [
|
||||
DestroyedTransactionGroup::class => [
|
||||
'FireflyIII\Handlers\Events\DestroyedGroupEventHandler@triggerWebhooks',
|
||||
],
|
||||
// API related events:
|
||||
AccessTokenCreated::class => [
|
||||
AccessTokenCreated::class => [
|
||||
'FireflyIII\Handlers\Events\APIEventHandler@accessTokenCreated',
|
||||
],
|
||||
|
||||
// Webhook related event:
|
||||
RequestedSendWebhookMessages::class => [
|
||||
RequestedSendWebhookMessages::class => [
|
||||
'FireflyIII\Handlers\Events\WebhookEventHandler@sendWebhookMessages',
|
||||
],
|
||||
|
||||
// account related events:
|
||||
StoredAccount::class => [
|
||||
StoredAccount::class => [
|
||||
'FireflyIII\Handlers\Events\StoredAccountEventHandler@recalculateCredit',
|
||||
],
|
||||
UpdatedAccount::class => [
|
||||
UpdatedAccount::class => [
|
||||
'FireflyIII\Handlers\Events\UpdatedAccountEventHandler@recalculateCredit',
|
||||
],
|
||||
|
||||
// bill related events:
|
||||
WarnUserAboutBill::class => [
|
||||
WarnUserAboutBill::class => [
|
||||
'FireflyIII\Handlers\Events\BillEventHandler@warnAboutBill',
|
||||
],
|
||||
|
||||
// audit log events:
|
||||
TriggeredAuditLog::class => [
|
||||
TriggeredAuditLog::class => [
|
||||
'FireflyIII\Handlers\Events\AuditEventHandler@storeAuditEvent',
|
||||
],
|
||||
// piggy bank related events:
|
||||
ChangedAmount::class => [
|
||||
ChangedAmount::class => [
|
||||
'FireflyIII\Handlers\Events\Model\PiggyBankEventHandler@changePiggyAmount',
|
||||
],
|
||||
|
||||
// budget related events: CRUD budget limit
|
||||
Created::class => [
|
||||
Created::class => [
|
||||
'FireflyIII\Handlers\Events\Model\BudgetLimitHandler@created',
|
||||
],
|
||||
Updated::class => [
|
||||
Updated::class => [
|
||||
'FireflyIII\Handlers\Events\Model\BudgetLimitHandler@updated',
|
||||
],
|
||||
Deleted::class => [
|
||||
Deleted::class => [
|
||||
'FireflyIII\Handlers\Events\Model\BudgetLimitHandler@deleted',
|
||||
],
|
||||
|
||||
// rule actions
|
||||
RuleActionFailedOnArray::class => [
|
||||
RuleActionFailedOnArray::class => [
|
||||
'FireflyIII\Handlers\Events\Model\RuleHandler@ruleActionFailedOnArray',
|
||||
],
|
||||
RuleActionFailedOnObject::class => [
|
||||
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')
|
||||
|
||||
@@ -67,13 +67,13 @@ class RecurringRepository implements RecurringRepositoryInterface
|
||||
$set
|
||||
= TransactionJournalMeta::where(static function (Builder $q1) use ($recurrence): void {
|
||||
$q1->where('name', 'recurrence_id');
|
||||
$q1->where('data', json_encode((string)$recurrence->id));
|
||||
$q1->where('data', json_encode((string) $recurrence->id));
|
||||
})->get(['journal_meta.transaction_journal_id']);
|
||||
|
||||
// there are X journals made for this recurrence. Any of them meant for today?
|
||||
foreach ($set as $journalMeta) {
|
||||
$count = TransactionJournalMeta::where(static function (Builder $q2) use ($date): void {
|
||||
$string = (string)$date;
|
||||
$string = (string) $date;
|
||||
app('log')->debug(sprintf('Search for date: %s', json_encode($string)));
|
||||
$q2->where('name', 'recurrence_date');
|
||||
$q2->where('data', json_encode($string));
|
||||
@@ -141,7 +141,7 @@ class RecurringRepository implements RecurringRepositoryInterface
|
||||
/** @var RecurrenceTransactionMeta $meta */
|
||||
foreach ($recTransaction->recurrenceTransactionMeta as $meta) {
|
||||
if ('bill_id' === $meta->name) {
|
||||
$return = (int)$meta->value;
|
||||
$return = (int) $meta->value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,7 +158,7 @@ class RecurringRepository implements RecurringRepositoryInterface
|
||||
/** @var RecurrenceTransactionMeta $meta */
|
||||
foreach ($recTransaction->recurrenceTransactionMeta as $meta) {
|
||||
if ('budget_id' === $meta->name) {
|
||||
$return = (int)$meta->value;
|
||||
$return = (int) $meta->value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,7 +175,7 @@ class RecurringRepository implements RecurringRepositoryInterface
|
||||
/** @var RecurrenceTransactionMeta $meta */
|
||||
foreach ($recTransaction->recurrenceTransactionMeta as $meta) {
|
||||
if ('category_id' === $meta->name) {
|
||||
$return = (int)$meta->value;
|
||||
$return = (int) $meta->value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,7 +192,7 @@ class RecurringRepository implements RecurringRepositoryInterface
|
||||
/** @var RecurrenceTransactionMeta $meta */
|
||||
foreach ($recTransaction->recurrenceTransactionMeta as $meta) {
|
||||
if ('category_name' === $meta->name) {
|
||||
$return = (string)$meta->value;
|
||||
$return = (string) $meta->value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -228,7 +231,7 @@ class RecurringRepository implements RecurringRepositoryInterface
|
||||
return TransactionJournalMeta::leftJoin('transaction_journals', 'transaction_journals.id', '=', 'journal_meta.transaction_journal_id')
|
||||
->where('transaction_journals.user_id', $this->user->id)
|
||||
->where('journal_meta.name', '=', 'recurrence_id')
|
||||
->where('journal_meta.data', '=', json_encode((string)$recurrence->id))
|
||||
->where('journal_meta.data', '=', json_encode((string) $recurrence->id))
|
||||
->get(['journal_meta.transaction_journal_id'])->pluck('transaction_journal_id')->toArray()
|
||||
;
|
||||
}
|
||||
@@ -241,7 +244,7 @@ class RecurringRepository implements RecurringRepositoryInterface
|
||||
/** @var null|Note $note */
|
||||
$note = $recurrence->notes()->first();
|
||||
|
||||
return (string)$note?->text;
|
||||
return (string) $note?->text;
|
||||
}
|
||||
|
||||
public function getPiggyBank(RecurrenceTransaction $transaction): ?int
|
||||
@@ -251,7 +254,7 @@ class RecurringRepository implements RecurringRepositoryInterface
|
||||
/** @var RecurrenceTransactionMeta $metaEntry */
|
||||
foreach ($meta as $metaEntry) {
|
||||
if ('piggy_bank_id' === $metaEntry->name) {
|
||||
return (int)$metaEntry->value;
|
||||
return (int) $metaEntry->value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -281,12 +284,12 @@ class RecurringRepository implements RecurringRepositoryInterface
|
||||
->whereNull('transaction_journals.deleted_at')
|
||||
->where('transaction_journals.user_id', $this->user->id)
|
||||
->where('name', 'recurrence_id')
|
||||
->where('data', json_encode((string)$recurrence->id))
|
||||
->where('data', json_encode((string) $recurrence->id))
|
||||
->get()->pluck('transaction_journal_id')->toArray()
|
||||
;
|
||||
$search = [];
|
||||
foreach ($journalMeta as $journalId) {
|
||||
$search[] = (int)$journalId;
|
||||
$search[] = (int) $journalId;
|
||||
}
|
||||
|
||||
/** @var GroupCollectorInterface $collector */
|
||||
@@ -314,13 +317,13 @@ class RecurringRepository implements RecurringRepositoryInterface
|
||||
->whereNull('transaction_journals.deleted_at')
|
||||
->where('transaction_journals.user_id', $this->user->id)
|
||||
->where('name', 'recurrence_id')
|
||||
->where('data', json_encode((string)$recurrence->id))
|
||||
->where('data', json_encode((string) $recurrence->id))
|
||||
->get()->pluck('transaction_journal_id')->toArray()
|
||||
;
|
||||
$search = [];
|
||||
|
||||
foreach ($journalMeta as $journalId) {
|
||||
$search[] = (int)$journalId;
|
||||
$search[] = (int) $journalId;
|
||||
}
|
||||
if (0 === count($search)) {
|
||||
return new Collection();
|
||||
@@ -442,28 +445,28 @@ class RecurringRepository implements RecurringRepositoryInterface
|
||||
if (is_array($language)) {
|
||||
$language = 'en_US';
|
||||
}
|
||||
$language = (string)$language;
|
||||
$language = (string) $language;
|
||||
if ('daily' === $repetition->repetition_type) {
|
||||
return (string)trans('firefly.recurring_daily', [], $language);
|
||||
return (string) trans('firefly.recurring_daily', [], $language);
|
||||
}
|
||||
if ('weekly' === $repetition->repetition_type) {
|
||||
$dayOfWeek = trans(sprintf('config.dow_%s', $repetition->repetition_moment), [], $language);
|
||||
if ($repetition->repetition_skip > 0) {
|
||||
return (string)trans('firefly.recurring_weekly_skip', ['weekday' => $dayOfWeek, 'skip' => $repetition->repetition_skip + 1], $language);
|
||||
return (string) trans('firefly.recurring_weekly_skip', ['weekday' => $dayOfWeek, 'skip' => $repetition->repetition_skip + 1], $language);
|
||||
}
|
||||
|
||||
return (string)trans('firefly.recurring_weekly', ['weekday' => $dayOfWeek], $language);
|
||||
return (string) trans('firefly.recurring_weekly', ['weekday' => $dayOfWeek], $language);
|
||||
}
|
||||
if ('monthly' === $repetition->repetition_type) {
|
||||
if ($repetition->repetition_skip > 0) {
|
||||
return (string)trans(
|
||||
return (string) trans(
|
||||
'firefly.recurring_monthly_skip',
|
||||
['dayOfMonth' => $repetition->repetition_moment, 'skip' => $repetition->repetition_skip + 1],
|
||||
$language
|
||||
);
|
||||
}
|
||||
|
||||
return (string)trans(
|
||||
return (string) trans(
|
||||
'firefly.recurring_monthly',
|
||||
['dayOfMonth' => $repetition->repetition_moment, 'skip' => $repetition->repetition_skip - 1],
|
||||
$language
|
||||
@@ -474,7 +477,7 @@ class RecurringRepository implements RecurringRepositoryInterface
|
||||
// first part is number of week, second is weekday.
|
||||
$dayOfWeek = trans(sprintf('config.dow_%s', $parts[1]), [], $language);
|
||||
|
||||
return (string)trans('firefly.recurring_ndom', ['weekday' => $dayOfWeek, 'dayOfMonth' => $parts[0]], $language);
|
||||
return (string) trans('firefly.recurring_ndom', ['weekday' => $dayOfWeek, 'dayOfMonth' => $parts[0]], $language);
|
||||
}
|
||||
if ('yearly' === $repetition->repetition_type) {
|
||||
$today = today(config('app.timezone'))->endOfYear();
|
||||
@@ -482,11 +485,11 @@ class RecurringRepository implements RecurringRepositoryInterface
|
||||
if (null === $repDate) {
|
||||
$repDate = clone $today;
|
||||
}
|
||||
$diffInYears = (int)$today->diffInYears($repDate, true);
|
||||
$diffInYears = (int) $today->diffInYears($repDate, true);
|
||||
$repDate->addYears($diffInYears); // technically not necessary.
|
||||
$string = $repDate->isoFormat((string)trans('config.month_and_day_no_year_js'));
|
||||
$string = $repDate->isoFormat((string) trans('config.month_and_day_no_year_js'));
|
||||
|
||||
return (string)trans('firefly.recurring_yearly', ['date' => $string], $language);
|
||||
return (string) trans('firefly.recurring_yearly', ['date' => $string], $language);
|
||||
}
|
||||
|
||||
return '';
|
||||
@@ -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')
|
||||
@@ -520,16 +523,16 @@ class RecurringRepository implements RecurringRepositoryInterface
|
||||
public function totalTransactions(Recurrence $recurrence, RecurrenceRepetition $repetition): int
|
||||
{
|
||||
// if repeat = null just return 0.
|
||||
if (null === $recurrence->repeat_until && 0 === (int)$recurrence->repetitions) {
|
||||
if (null === $recurrence->repeat_until && 0 === (int) $recurrence->repetitions) {
|
||||
return 0;
|
||||
}
|
||||
// expect X transactions then stop. Return that number
|
||||
if (null === $recurrence->repeat_until && 0 !== (int)$recurrence->repetitions) {
|
||||
return (int)$recurrence->repetitions;
|
||||
if (null === $recurrence->repeat_until && 0 !== (int) $recurrence->repetitions) {
|
||||
return (int) $recurrence->repetitions;
|
||||
}
|
||||
|
||||
// need to calculate, this depends on the repetition:
|
||||
if (null !== $recurrence->repeat_until && 0 === (int)$recurrence->repetitions) {
|
||||
if (null !== $recurrence->repeat_until && 0 === (int) $recurrence->repetitions) {
|
||||
$occurrences = $this->getOccurrencesInRange($repetition, $recurrence->first_date ?? today(), $recurrence->repeat_until);
|
||||
|
||||
return count($occurrences);
|
||||
|
||||
@@ -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:
|
||||
|
||||
// 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);
|
||||
foreach ($parts as $part) {
|
||||
$search = sprintf('%%%s%%', $part);
|
||||
$q->orWhere('name', 'LIKE', $search);
|
||||
}
|
||||
$parts = explode(' ', $query);
|
||||
foreach ($parts as $part) {
|
||||
$search = sprintf('%%%s%%', $part);
|
||||
$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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -161,7 +161,7 @@ class CreditRecalculateService
|
||||
app('log')->debug(sprintf('Now processing account #%d ("%s"). All amounts with 2 decimals!', $account->id, $account->name));
|
||||
// get opening balance (if present)
|
||||
$this->repository->setUser($account->user);
|
||||
$direction = (string)$this->repository->getMetaValue($account, 'liability_direction');
|
||||
$direction = (string) $this->repository->getMetaValue($account, 'liability_direction');
|
||||
$openingBalance = $this->repository->getOpeningBalance($account);
|
||||
if (null !== $openingBalance) {
|
||||
app('log')->debug(sprintf('Found opening balance transaction journal #%d', $openingBalance->id));
|
||||
@@ -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,10 +36,12 @@ trait CollectsAccountsFromFilter
|
||||
$collection = new Collection();
|
||||
|
||||
// always collect from the query parameter, even when it's empty.
|
||||
foreach ($queryParameters['accounts'] as $accountId) {
|
||||
$account = $this->repository->find((int) $accountId);
|
||||
if (null !== $account) {
|
||||
$collection->push($account);
|
||||
if (null !== $queryParameters['accounts']) {
|
||||
foreach ($queryParameters['accounts'] as $accountId) {
|
||||
$account = $this->repository->find((int) $accountId);
|
||||
if (null !== $account) {
|
||||
$collection->push($account);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,9 +41,8 @@ trait DateCalculation
|
||||
{
|
||||
$difference = (int)($start->diffInDays($end, true) + 1);
|
||||
$today = today(config('app.timezone'))->startOfDay();
|
||||
|
||||
if ($start->lte($today) && $end->gte($today)) {
|
||||
$difference = $today->diffInDays($end);
|
||||
$difference = $today->diffInDays($end) + 1;
|
||||
}
|
||||
|
||||
return (int)(0 === $difference ? 1 : $difference);
|
||||
|
||||
@@ -130,6 +130,7 @@ trait GetConfigurationData
|
||||
|
||||
/** @var Carbon $todayEnd */
|
||||
$todayEnd = app('navigation')->endOfPeriod($todayStart, $viewRange);
|
||||
|
||||
if ($todayStart->ne($start) || $todayEnd->ne($end)) {
|
||||
$ranges[ucfirst((string)trans('firefly.today'))] = [$todayStart, $todayEnd];
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,10 +23,11 @@ declare(strict_types=1);
|
||||
|
||||
namespace FireflyIII\Support\Models;
|
||||
|
||||
use FireflyIII\Exceptions\FireflyException;
|
||||
use Carbon\Carbon;
|
||||
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;
|
||||
@@ -46,20 +47,15 @@ class AccountBalanceCalculator
|
||||
}
|
||||
|
||||
/**
|
||||
* Recalculate all balances.
|
||||
* Recalculate all account and transaction balances.
|
||||
*/
|
||||
public static function forceRecalculateAll(): void
|
||||
{
|
||||
Transaction::whereNull('deleted_at')->update(['balance_dirty' => true]);
|
||||
$object = new self();
|
||||
$object->optimizedCalculation(new Collection());
|
||||
}
|
||||
|
||||
/**
|
||||
* Recalculate all balances.
|
||||
*/
|
||||
public static function recalculateAll(): void
|
||||
public static function recalculateAll(bool $forced): void
|
||||
{
|
||||
if ($forced) {
|
||||
Transaction::whereNull('deleted_at')->update(['balance_dirty' => true]);
|
||||
// also delete account balances.
|
||||
AccountBalance::whereNotNull('created_at')->delete();
|
||||
}
|
||||
$object = new self();
|
||||
$object->optimizedCalculation(new Collection());
|
||||
}
|
||||
@@ -74,7 +70,35 @@ class AccountBalanceCalculator
|
||||
foreach ($transactionJournal->transactions as $transaction) {
|
||||
$accounts->push($transaction->account);
|
||||
}
|
||||
$object->optimizedCalculation($accounts);
|
||||
$object->optimizedCalculation($accounts, $transactionJournal->date);
|
||||
}
|
||||
|
||||
private function getLatestBalance(int $accountId, int $currencyId, ?Carbon $notBefore): string
|
||||
{
|
||||
if (null === $notBefore) {
|
||||
return '0';
|
||||
}
|
||||
Log::debug(sprintf('getLatestBalance: notBefore date is "%s", calculating', $notBefore->format('Y-m-d')));
|
||||
$query = Transaction::leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id')
|
||||
->whereNull('transactions.deleted_at')
|
||||
->where('transaction_journals.transaction_currency_id', $currencyId)
|
||||
->whereNull('transaction_journals.deleted_at')
|
||||
// this order is the same as GroupCollector
|
||||
->orderBy('transaction_journals.date', 'DESC')
|
||||
->orderBy('transaction_journals.order', 'ASC')
|
||||
->orderBy('transaction_journals.id', 'DESC')
|
||||
->orderBy('transaction_journals.description', 'DESC')
|
||||
->orderBy('transactions.amount', 'DESC')
|
||||
->where('transactions.account_id', $accountId)
|
||||
;
|
||||
$notBefore->startOfDay();
|
||||
$query->where('transaction_journals.date', '<', $notBefore);
|
||||
|
||||
$first = $query->first(['transactions.id', 'transactions.balance_dirty', 'transactions.transaction_currency_id', 'transaction_journals.date', 'transactions.account_id', 'transactions.amount', 'transactions.balance_after']);
|
||||
$balance = $first->balance_after ?? '0';
|
||||
Log::debug(sprintf('getLatestBalance: found balance: %s in transaction #%d', $balance, $first->id ?? 0));
|
||||
|
||||
return $balance;
|
||||
}
|
||||
|
||||
private function getAccountBalanceByAccount(int $account, int $currency): AccountBalance
|
||||
@@ -98,7 +122,7 @@ class AccountBalanceCalculator
|
||||
return $entry;
|
||||
}
|
||||
|
||||
private function optimizedCalculation(Collection $accounts): void
|
||||
private function optimizedCalculation(Collection $accounts, ?Carbon $notBefore = null): void
|
||||
{
|
||||
Log::debug('start of optimizedCalculation');
|
||||
if ($accounts->count() > 0) {
|
||||
@@ -120,17 +144,24 @@ class AccountBalanceCalculator
|
||||
if ($accounts->count() > 0) {
|
||||
$query->whereIn('transactions.account_id', $accounts->pluck('id')->toArray());
|
||||
}
|
||||
if (null !== $notBefore) {
|
||||
$notBefore->startOfDay();
|
||||
$query->where('transaction_journals.date', '>=', $notBefore);
|
||||
}
|
||||
|
||||
$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] ??= '0';
|
||||
$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:
|
||||
@@ -142,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
|
||||
@@ -169,145 +201,182 @@ class AccountBalanceCalculator
|
||||
return $entry;
|
||||
}
|
||||
|
||||
private function recalculateLatest(?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
|
||||
{
|
||||
$query = Transaction::groupBy(['transactions.account_id', 'transactions.transaction_currency_id', 'transactions.foreign_currency_id']);
|
||||
/**
|
||||
* @var int $accountId
|
||||
* @var array $currencies
|
||||
*/
|
||||
foreach ($balances as $accountId => $currencies) {
|
||||
/** @var Account $account */
|
||||
$account = Account::find($accountId);
|
||||
if (null === $account) {
|
||||
Log::error(sprintf('Could not find account #%d, will not save account balance.', $accountId));
|
||||
|
||||
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);
|
||||
continue;
|
||||
}
|
||||
$entry->save();
|
||||
|
||||
// then do foreign amount, if present:
|
||||
if ($foreignCurrency > 0) {
|
||||
$entry = $this->getAccountBalanceByAccount($account, $foreignCurrency);
|
||||
/**
|
||||
* @var int $currencyId
|
||||
* @var array $balance
|
||||
*/
|
||||
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));
|
||||
|
||||
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);
|
||||
continue;
|
||||
}
|
||||
$entry->save();
|
||||
|
||||
/** @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();
|
||||
}
|
||||
}
|
||||
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 [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,6 +177,7 @@ class Navigation
|
||||
'year' => 'startOfYear',
|
||||
'yearly' => 'startOfYear',
|
||||
'1Y' => 'startOfYear',
|
||||
'MTD' => 'startOfMonth',
|
||||
];
|
||||
|
||||
$parameterMap = [
|
||||
@@ -274,13 +275,21 @@ class Navigation
|
||||
|
||||
/** @var Carbon $tEnd */
|
||||
$tEnd = session('end', today(config('app.timezone'))->endOfMonth());
|
||||
$diffInDays = (int)$tStart->diffInDays($tEnd, true);
|
||||
$diffInDays = (int) $tStart->diffInDays($tEnd, true);
|
||||
}
|
||||
Log::debug(sprintf('Diff in days is %d', $diffInDays));
|
||||
$currentEnd->addDays($diffInDays);
|
||||
|
||||
return $currentEnd;
|
||||
}
|
||||
if ('MTD' === $repeatFreq) {
|
||||
$today = today();
|
||||
if ($today->isSameMonth($end)) {
|
||||
return $today->endOfDay();
|
||||
}
|
||||
|
||||
return $end->endOfMonth();
|
||||
}
|
||||
|
||||
$result = match ($repeatFreq) {
|
||||
'last7' => $currentEnd->addDays(7)->startOfDay(),
|
||||
@@ -327,7 +336,7 @@ class Navigation
|
||||
{
|
||||
$endOfMonth = $date->copy()->endOfMonth();
|
||||
|
||||
return (int)$date->diffInDays($endOfMonth, true);
|
||||
return (int) $date->diffInDays($endOfMonth, true);
|
||||
}
|
||||
|
||||
public function diffInPeriods(string $period, int $skip, Carbon $beginning, Carbon $end): int
|
||||
@@ -382,7 +391,7 @@ class Navigation
|
||||
));
|
||||
}
|
||||
|
||||
return (int)$diff;
|
||||
return (int) $diff;
|
||||
}
|
||||
|
||||
public function endOfX(Carbon $theCurrentEnd, string $repeatFreq, ?Carbon $maxDate): Carbon
|
||||
@@ -428,7 +437,7 @@ class Navigation
|
||||
if (is_array($range)) {
|
||||
$range = '1M';
|
||||
}
|
||||
$range = (string)$range;
|
||||
$range = (string) $range;
|
||||
if (!$correct) {
|
||||
return $range;
|
||||
}
|
||||
@@ -463,18 +472,18 @@ class Navigation
|
||||
// define period to increment
|
||||
$increment = 'addDay';
|
||||
$format = $this->preferredCarbonFormat($start, $end);
|
||||
$displayFormat = (string)trans('config.month_and_day_js', [], $locale);
|
||||
$displayFormat = (string) trans('config.month_and_day_js', [], $locale);
|
||||
$diff = $start->diffInMonths($end, true);
|
||||
// increment by month (for year)
|
||||
if ($diff >= 1.0001) {
|
||||
$increment = 'addMonth';
|
||||
$displayFormat = (string)trans('config.month_js');
|
||||
$displayFormat = (string) trans('config.month_js');
|
||||
}
|
||||
|
||||
// increment by year (for multi-year)
|
||||
if ($diff >= 12.0001) {
|
||||
$increment = 'addYear';
|
||||
$displayFormat = (string)trans('config.year_js');
|
||||
$displayFormat = (string) trans('config.year_js');
|
||||
}
|
||||
$begin = clone $start;
|
||||
$entries = [];
|
||||
@@ -514,19 +523,19 @@ class Navigation
|
||||
{
|
||||
$date = clone $theDate;
|
||||
$formatMap = [
|
||||
'1D' => (string)trans('config.specific_day_js'),
|
||||
'daily' => (string)trans('config.specific_day_js'),
|
||||
'custom' => (string)trans('config.specific_day_js'),
|
||||
'1W' => (string)trans('config.week_in_year_js'),
|
||||
'week' => (string)trans('config.week_in_year_js'),
|
||||
'weekly' => (string)trans('config.week_in_year_js'),
|
||||
'1M' => (string)trans('config.month_js'),
|
||||
'month' => (string)trans('config.month_js'),
|
||||
'monthly' => (string)trans('config.month_js'),
|
||||
'1Y' => (string)trans('config.year_js'),
|
||||
'year' => (string)trans('config.year_js'),
|
||||
'yearly' => (string)trans('config.year_js'),
|
||||
'6M' => (string)trans('config.half_year_js'),
|
||||
'1D' => (string) trans('config.specific_day_js'),
|
||||
'daily' => (string) trans('config.specific_day_js'),
|
||||
'custom' => (string) trans('config.specific_day_js'),
|
||||
'1W' => (string) trans('config.week_in_year_js'),
|
||||
'week' => (string) trans('config.week_in_year_js'),
|
||||
'weekly' => (string) trans('config.week_in_year_js'),
|
||||
'1M' => (string) trans('config.month_js'),
|
||||
'month' => (string) trans('config.month_js'),
|
||||
'monthly' => (string) trans('config.month_js'),
|
||||
'1Y' => (string) trans('config.year_js'),
|
||||
'year' => (string) trans('config.year_js'),
|
||||
'yearly' => (string) trans('config.year_js'),
|
||||
'6M' => (string) trans('config.half_year_js'),
|
||||
];
|
||||
|
||||
if (array_key_exists($repeatFrequency, $formatMap)) {
|
||||
@@ -567,13 +576,13 @@ class Navigation
|
||||
public function preferredCarbonLocalizedFormat(Carbon $start, Carbon $end): string
|
||||
{
|
||||
$locale = app('steam')->getLocale();
|
||||
$format = (string)trans('config.month_and_day_js', [], $locale);
|
||||
$format = (string) trans('config.month_and_day_js', [], $locale);
|
||||
if ($start->diffInMonths($end, true) > 1) {
|
||||
$format = (string)trans('config.month_js', [], $locale);
|
||||
$format = (string) trans('config.month_js', [], $locale);
|
||||
}
|
||||
|
||||
if ($start->diffInMonths($end, true) > 12) {
|
||||
$format = (string)trans('config.year_js', [], $locale);
|
||||
$format = (string) trans('config.year_js', [], $locale);
|
||||
}
|
||||
|
||||
return $format;
|
||||
@@ -586,11 +595,11 @@ class Navigation
|
||||
public function preferredEndOfPeriod(Carbon $start, Carbon $end): string
|
||||
{
|
||||
$format = 'endOfDay';
|
||||
if ((int)$start->diffInMonths($end, true) > 1) {
|
||||
if ((int) $start->diffInMonths($end, true) > 1) {
|
||||
$format = 'endOfMonth';
|
||||
}
|
||||
|
||||
if ((int)$start->diffInMonths($end, true) > 12) {
|
||||
if ((int) $start->diffInMonths($end, true) > 12) {
|
||||
$format = 'endOfYear';
|
||||
}
|
||||
|
||||
@@ -604,11 +613,11 @@ class Navigation
|
||||
public function preferredRangeFormat(Carbon $start, Carbon $end): string
|
||||
{
|
||||
$format = '1D';
|
||||
if ((int)$start->diffInMonths($end, true) > 1) {
|
||||
if ((int) $start->diffInMonths($end, true) > 1) {
|
||||
$format = '1M';
|
||||
}
|
||||
|
||||
if ((int)$start->diffInMonths($end, true) > 12) {
|
||||
if ((int) $start->diffInMonths($end, true) > 12) {
|
||||
$format = '1Y';
|
||||
}
|
||||
|
||||
@@ -622,11 +631,11 @@ class Navigation
|
||||
public function preferredSqlFormat(Carbon $start, Carbon $end): string
|
||||
{
|
||||
$format = '%Y-%m-%d';
|
||||
if ((int)$start->diffInMonths($end, true) > 1) {
|
||||
if ((int) $start->diffInMonths($end, true) > 1) {
|
||||
$format = '%Y-%m';
|
||||
}
|
||||
|
||||
if ((int)$start->diffInMonths($end, true) > 12) {
|
||||
if ((int) $start->diffInMonths($end, true) > 12) {
|
||||
$format = '%Y';
|
||||
}
|
||||
|
||||
@@ -682,7 +691,7 @@ class Navigation
|
||||
|
||||
/** @var Carbon $tEnd */
|
||||
$tEnd = session('end', today(config('app.timezone'))->endOfMonth());
|
||||
$diffInDays = (int)$tStart->diffInDays($tEnd, true);
|
||||
$diffInDays = (int) $tStart->diffInDays($tEnd, true);
|
||||
$date->subDays($diffInDays * $subtract);
|
||||
|
||||
return $date;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -269,7 +269,7 @@ class Steam
|
||||
*/
|
||||
public function balanceInRangeConverted(Account $account, Carbon $start, Carbon $end, TransactionCurrency $native): array
|
||||
{
|
||||
Log::warning(sprintf('Deprecated method %s, do not use.', __METHOD__));
|
||||
// Log::warning(sprintf('Deprecated method %s, do not use.', __METHOD__));
|
||||
$cache = new CacheProperties();
|
||||
$cache->addProperty($account->id);
|
||||
$cache->addProperty('balance-in-range-converted');
|
||||
@@ -541,7 +541,7 @@ class Steam
|
||||
*/
|
||||
public function balancesByAccounts(Collection $accounts, Carbon $date): array
|
||||
{
|
||||
Log::warning(sprintf('Deprecated method %s, do not use.', __METHOD__));
|
||||
// Log::warning(sprintf('Deprecated method %s, do not use.', __METHOD__));
|
||||
$ids = $accounts->pluck('id')->toArray();
|
||||
// cache this property.
|
||||
$cache = new CacheProperties();
|
||||
@@ -572,7 +572,7 @@ class Steam
|
||||
*/
|
||||
public function balancesByAccountsConverted(Collection $accounts, Carbon $date): array
|
||||
{
|
||||
Log::warning(sprintf('Deprecated method %s, do not use.', __METHOD__));
|
||||
// Log::warning(sprintf('Deprecated method %s, do not use.', __METHOD__));
|
||||
$ids = $accounts->pluck('id')->toArray();
|
||||
// cache this property.
|
||||
$cache = new CacheProperties();
|
||||
@@ -606,7 +606,7 @@ class Steam
|
||||
*/
|
||||
public function balancesPerCurrencyByAccounts(Collection $accounts, Carbon $date): array
|
||||
{
|
||||
Log::warning(sprintf('Deprecated method %s, do not use.', __METHOD__));
|
||||
// Log::warning(sprintf('Deprecated method %s, do not use.', __METHOD__));
|
||||
$ids = $accounts->pluck('id')->toArray();
|
||||
// cache this property.
|
||||
$cache = new CacheProperties();
|
||||
|
||||
@@ -29,7 +29,6 @@ use FireflyIII\Repositories\Account\AccountRepositoryInterface;
|
||||
use FireflyIII\Repositories\User\UserRepositoryInterface;
|
||||
use FireflyIII\Support\Search\OperatorQuerySearch;
|
||||
use League\CommonMark\GithubFlavoredMarkdownConverter;
|
||||
use Route;
|
||||
use Twig\Extension\AbstractExtension;
|
||||
use Twig\TwigFilter;
|
||||
use Twig\TwigFunction;
|
||||
@@ -63,12 +62,29 @@ class General extends AbstractExtension
|
||||
}
|
||||
|
||||
/** @var Carbon $date */
|
||||
$date = session('end', today(config('app.timezone'))->endOfMonth());
|
||||
$info = app('steam')->balanceByTransactions($account, $date, null);
|
||||
$date = session('end', today(config('app.timezone'))->endOfMonth());
|
||||
$runningBalance = config('firefly.feature_flags.running_balance_column');
|
||||
$info = [];
|
||||
if (true === $runningBalance) {
|
||||
$info = app('steam')->balanceByTransactions($account, $date, null);
|
||||
}
|
||||
if (false === $runningBalance) {
|
||||
$info[] = app('steam')->balance($account, $date);
|
||||
}
|
||||
|
||||
$strings = [];
|
||||
$strings = [];
|
||||
foreach ($info as $currencyId => $balance) {
|
||||
$strings[] = app('amount')->formatByCurrencyId($currencyId, $balance, false);
|
||||
$balance = (string) $balance;
|
||||
if (0 === $currencyId) {
|
||||
// not good code but OK
|
||||
/** @var AccountRepositoryInterface $accountRepos */
|
||||
$accountRepos = app(AccountRepositoryInterface::class);
|
||||
$currency = $accountRepos->getAccountCurrency($account) ?? app('amount')->getDefaultCurrency();
|
||||
$strings[] = app('amount')->formatAnything($currency, $balance, false);
|
||||
}
|
||||
if (0 !== $currencyId) {
|
||||
$strings[] = app('amount')->formatByCurrencyId($currencyId, $balance, false);
|
||||
}
|
||||
}
|
||||
|
||||
return implode(', ', $strings);
|
||||
@@ -198,7 +214,7 @@ class General extends AbstractExtension
|
||||
]
|
||||
);
|
||||
|
||||
return (string)$converter->convert($text);
|
||||
return (string) $converter->convert($text);
|
||||
},
|
||||
['is_safe' => ['html']]
|
||||
);
|
||||
@@ -212,8 +228,8 @@ class General extends AbstractExtension
|
||||
return new TwigFilter(
|
||||
'phphost',
|
||||
static function (string $string): string {
|
||||
$proto = (string)parse_url($string, PHP_URL_SCHEME);
|
||||
$host = (string)parse_url($string, PHP_URL_HOST);
|
||||
$proto = (string) parse_url($string, PHP_URL_SCHEME);
|
||||
$host = (string) parse_url($string, PHP_URL_HOST);
|
||||
|
||||
return e(sprintf('%s://%s', $proto, $host));
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
@@ -111,10 +111,11 @@ class UpdatePiggybank implements ActionInterface
|
||||
'add_to_piggy',
|
||||
null,
|
||||
[
|
||||
'currency_symbol' => $journalObj->transactionCurrency->symbol,
|
||||
'decimal_places' => $journalObj->transactionCurrency->decimal_places,
|
||||
'amount' => $destination->amount,
|
||||
'piggy' => $piggyBank->name,
|
||||
'currency_symbol' => $journalObj->transactionCurrency->symbol,
|
||||
'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,8 +73,25 @@ class FireflyValidator extends Validator
|
||||
if (is_array($secret)) {
|
||||
$secret = '';
|
||||
}
|
||||
$secret = (string) $secret;
|
||||
|
||||
return (bool)\Google2FA::verifyKey((string)$secret, $value);
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -88,7 +105,7 @@ class FireflyValidator extends Validator
|
||||
{
|
||||
$field = $parameters[1] ?? 'id';
|
||||
|
||||
if (0 === (int)$value) {
|
||||
if (0 === (int) $value) {
|
||||
return true;
|
||||
}
|
||||
$count = \DB::table($parameters[0])->where('user_id', auth()->user()->id)->where($field, $value)->count();
|
||||
@@ -179,7 +196,7 @@ class FireflyValidator extends Validator
|
||||
$value = strtoupper($value);
|
||||
|
||||
// replace characters outside of ASCI range.
|
||||
$value = (string)iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $value);
|
||||
$value = (string) iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $value);
|
||||
$search = [' ', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'];
|
||||
$replace = ['', '10', '11', '12', '13', '14', '15', '16', '17', '18', '19', '20', '21', '22', '23', '24', '25', '26', '27', '28', '29', '30', '31', '32', '33', '34', '35'];
|
||||
|
||||
@@ -202,7 +219,7 @@ class FireflyValidator extends Validator
|
||||
return false;
|
||||
}
|
||||
|
||||
return 1 === (int)$checksum;
|
||||
return 1 === (int) $checksum;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -217,7 +234,7 @@ class FireflyValidator extends Validator
|
||||
/** @var mixed $compare */
|
||||
$compare = $parameters[0] ?? '0';
|
||||
|
||||
return bccomp((string)$value, (string)$compare) < 0;
|
||||
return bccomp((string) $value, (string) $compare) < 0;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -232,7 +249,7 @@ class FireflyValidator extends Validator
|
||||
/** @var mixed $compare */
|
||||
$compare = $parameters[0] ?? '0';
|
||||
|
||||
return bccomp((string)$value, (string)$compare) > 0;
|
||||
return bccomp((string) $value, (string) $compare) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -246,7 +263,7 @@ class FireflyValidator extends Validator
|
||||
{
|
||||
$field = $parameters[1] ?? 'id';
|
||||
|
||||
if (0 === (int)$value) {
|
||||
if (0 === (int) $value) {
|
||||
return true;
|
||||
}
|
||||
$count = \DB::table($parameters[0])->where($field, $value)->count();
|
||||
@@ -259,7 +276,7 @@ class FireflyValidator extends Validator
|
||||
// first, get the index from this string:
|
||||
$value ??= '';
|
||||
$parts = explode('.', $attribute);
|
||||
$index = (int)($parts[1] ?? '0');
|
||||
$index = (int) ($parts[1] ?? '0');
|
||||
|
||||
// get the name of the trigger from the data array:
|
||||
$actionType = $this->data['actions'][$index]['type'] ?? 'invalid';
|
||||
@@ -329,7 +346,7 @@ class FireflyValidator extends Validator
|
||||
{
|
||||
// first, get the index from this string:
|
||||
$parts = explode('.', $attribute);
|
||||
$index = (int)($parts[1] ?? '0');
|
||||
$index = (int) ($parts[1] ?? '0');
|
||||
|
||||
// get the name of the trigger from the data array:
|
||||
$triggerType = $this->data['triggers'][$index]['type'] ?? 'invalid';
|
||||
@@ -391,7 +408,7 @@ class FireflyValidator extends Validator
|
||||
// check if it's an existing account.
|
||||
// TODO create a helper to automatically return these.
|
||||
if (in_array($triggerType, ['destination_account_id', 'source_account_id'], true)) {
|
||||
return is_numeric($value) && (int)$value > 0;
|
||||
return is_numeric($value) && (int) $value > 0;
|
||||
}
|
||||
|
||||
// check transaction type.
|
||||
@@ -430,7 +447,7 @@ class FireflyValidator extends Validator
|
||||
{
|
||||
$verify = false;
|
||||
if (array_key_exists('verify_password', $this->data)) {
|
||||
$verify = 1 === (int)$this->data['verify_password'];
|
||||
$verify = 1 === (int) $this->data['verify_password'];
|
||||
}
|
||||
if ($verify) {
|
||||
/** @var Verifier $service */
|
||||
@@ -465,7 +482,7 @@ class FireflyValidator extends Validator
|
||||
if (array_key_exists('type', $this->data)) {
|
||||
app('log')->debug('validateUniqueAccountForUser::typeString');
|
||||
|
||||
return $this->validateByAccountTypeString($value, $parameters, (string)$this->data['type']);
|
||||
return $this->validateByAccountTypeString($value, $parameters, (string) $this->data['type']);
|
||||
}
|
||||
if (array_key_exists('account_type_id', $this->data)) {
|
||||
app('log')->debug('validateUniqueAccountForUser::typeId');
|
||||
@@ -476,7 +493,7 @@ class FireflyValidator extends Validator
|
||||
if (null !== $parameterId) {
|
||||
app('log')->debug('validateUniqueAccountForUser::paramId');
|
||||
|
||||
return $this->validateByParameterId((int)$parameterId, $value);
|
||||
return $this->validateByParameterId((int) $parameterId, $value);
|
||||
}
|
||||
if (array_key_exists('id', $this->data)) {
|
||||
app('log')->debug('validateUniqueAccountForUser::accountId');
|
||||
@@ -517,7 +534,7 @@ class FireflyValidator extends Validator
|
||||
}
|
||||
|
||||
$accountTypes = AccountType::whereIn('type', $search)->get();
|
||||
$ignore = (int)($parameters[0] ?? 0.0);
|
||||
$ignore = (int) ($parameters[0] ?? 0.0);
|
||||
$accountTypeIds = $accountTypes->pluck('id')->toArray();
|
||||
|
||||
/** @var null|Account $result */
|
||||
@@ -536,7 +553,7 @@ class FireflyValidator extends Validator
|
||||
private function validateByAccountTypeId($value, $parameters): bool
|
||||
{
|
||||
$type = AccountType::find($this->data['account_type_id'])->first();
|
||||
$ignore = (int)($parameters[0] ?? 0.0);
|
||||
$ignore = (int) ($parameters[0] ?? 0.0);
|
||||
|
||||
/** @var null|Account $result */
|
||||
$result = auth()->user()->accounts()->where('account_type_id', $type->id)->where('id', '!=', $ignore)
|
||||
@@ -599,9 +616,9 @@ class FireflyValidator extends Validator
|
||||
*/
|
||||
public function validateUniqueAccountNumberForUser($attribute, $value, $parameters): bool
|
||||
{
|
||||
$accountId = (int)($this->data['id'] ?? 0.0);
|
||||
$accountId = (int) ($this->data['id'] ?? 0.0);
|
||||
if (0 === $accountId) {
|
||||
$accountId = (int)($parameters[0] ?? 0.0);
|
||||
$accountId = (int) ($parameters[0] ?? 0.0);
|
||||
}
|
||||
|
||||
$query = AccountMeta::leftJoin('accounts', 'accounts.id', '=', 'account_meta.account_id')
|
||||
@@ -636,7 +653,7 @@ class FireflyValidator extends Validator
|
||||
/** @var AccountMeta $entry */
|
||||
foreach ($set as $entry) {
|
||||
$otherAccount = $entry->account;
|
||||
$otherType = (string)config(sprintf('firefly.shortNamesByFullName.%s', $otherAccount->accountType->type));
|
||||
$otherType = (string) config(sprintf('firefly.shortNamesByFullName.%s', $otherAccount->accountType->type));
|
||||
if (('expense' === $otherType || 'revenue' === $otherType) && $otherType !== $type) {
|
||||
app('log')->debug(sprintf('The other account with this account number is a "%s" so return true.', $otherType));
|
||||
|
||||
@@ -653,7 +670,7 @@ class FireflyValidator extends Validator
|
||||
*/
|
||||
public function validateUniqueCurrencyCode(?string $attribute, ?string $value): bool
|
||||
{
|
||||
return $this->validateUniqueCurrency('code', (string)$attribute, (string)$value);
|
||||
return $this->validateUniqueCurrency('code', (string) $attribute, (string) $value);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -666,12 +683,12 @@ class FireflyValidator extends Validator
|
||||
|
||||
public function validateUniqueCurrencyName(?string $attribute, ?string $value): bool
|
||||
{
|
||||
return $this->validateUniqueCurrency('name', (string)$attribute, (string)$value);
|
||||
return $this->validateUniqueCurrency('name', (string) $attribute, (string) $value);
|
||||
}
|
||||
|
||||
public function validateUniqueCurrencySymbol(?string $attribute, ?string $value): bool
|
||||
{
|
||||
return $this->validateUniqueCurrency('symbol', (string)$attribute, (string)$value);
|
||||
return $this->validateUniqueCurrency('symbol', (string) $attribute, (string) $value);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -683,7 +700,7 @@ class FireflyValidator extends Validator
|
||||
*/
|
||||
public function validateUniqueExistingWebhook($value, $parameters, $something): bool
|
||||
{
|
||||
$existingId = (int)($something[0] ?? 0);
|
||||
$existingId = (int) ($something[0] ?? 0);
|
||||
$trigger = 0;
|
||||
$response = 0;
|
||||
$delivery = 0;
|
||||
@@ -739,15 +756,15 @@ class FireflyValidator extends Validator
|
||||
public function validateUniqueObjectForUser($attribute, $value, $parameters): bool
|
||||
{
|
||||
[$table, $field] = $parameters;
|
||||
$exclude = (int)($parameters[2] ?? 0.0);
|
||||
$exclude = (int) ($parameters[2] ?? 0.0);
|
||||
|
||||
/*
|
||||
* If other data (in $this->getData()) contains
|
||||
* ID field, set that field to be the $exclude.
|
||||
*/
|
||||
$data = $this->getData();
|
||||
if (!array_key_exists(2, $parameters) && array_key_exists('id', $data) && (int)$data['id'] > 0) {
|
||||
$exclude = (int)$data['id'];
|
||||
if (!array_key_exists(2, $parameters) && array_key_exists('id', $data) && (int) $data['id'] > 0) {
|
||||
$exclude = (int) $data['id'];
|
||||
}
|
||||
// get entries from table
|
||||
$result = \DB::table($table)->where('user_id', auth()->user()->id)->whereNull('deleted_at')
|
||||
@@ -779,7 +796,7 @@ class FireflyValidator extends Validator
|
||||
->where('object_groups.title', $value)
|
||||
;
|
||||
if (null !== $exclude) {
|
||||
$query->where('object_groups.id', '!=', (int)$exclude);
|
||||
$query->where('object_groups.id', '!=', (int) $exclude);
|
||||
}
|
||||
|
||||
return 0 === $query->count();
|
||||
@@ -799,7 +816,7 @@ class FireflyValidator extends Validator
|
||||
->leftJoin('accounts', 'accounts.id', '=', 'piggy_banks.account_id')->where('accounts.user_id', auth()->user()->id)
|
||||
;
|
||||
if (null !== $exclude) {
|
||||
$query->where('piggy_banks.id', '!=', (int)$exclude);
|
||||
$query->where('piggy_banks.id', '!=', (int) $exclude);
|
||||
}
|
||||
$query->where('piggy_banks.name', $value);
|
||||
|
||||
|
||||
63
changelog.md
63
changelog.md
@@ -3,6 +3,69 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
This project adheres to [Semantic Versioning](http://semver.org/).
|
||||
|
||||
## 6.1.21 - 2024-09-30
|
||||
|
||||
### Added
|
||||
|
||||
- Enabled the expression engine built by @michaelhthomas. Read more about it in [the documentation](https://docs.firefly-iii.org/references/firefly-iii/rule-expressions/).
|
||||
- Add running balance data, see if it can be used in the layout in the future.
|
||||
- [PR 9160](https://github.com/firefly-iii/firefly-iii/pull/9160) (add test cases for api/v1/autocomplete/CategoryController) reported by @tasnim0tantawi
|
||||
- [PR 9178](https://github.com/firefly-iii/firefly-iii/pull/9178) (Add test cases for Api\V1\Controllers\Autocomplete\BillController & BudgetController) reported by @tasnim0tantawi
|
||||
- [PR 9171](https://github.com/firefly-iii/firefly-iii/pull/9171) (Add about test) reported by @mzhubail
|
||||
|
||||
### Changed
|
||||
|
||||
- [PR 9096](https://github.com/firefly-iii/firefly-iii/pull/9096) (chore: fix some comments) reported by @withbest
|
||||
|
||||
### Fixed
|
||||
|
||||
- [Issue 9078](https://github.com/firefly-iii/firefly-iii/issues/9078) (bcadd exception while using POST transactions) reported by @dbtdsilva
|
||||
- [Discussion 9080](https://github.com/orgs/firefly-iii/discussions/9080) (Incorrect sorting on expense accounts) started by @pc-zookeeper
|
||||
- [Issue 9084](https://github.com/firefly-iii/firefly-iii/issues/9084) (API Call for bills/nextExpectedMatch does not update) reported by @marcelweikum
|
||||
- [Issue 9103](https://github.com/firefly-iii/firefly-iii/issues/9103) (Default Currency does not apply to Accounts.) reported by @chrisgriff1512
|
||||
- [Issue 9140](https://github.com/firefly-iii/firefly-iii/issues/9140) (Dashboard 'Today' option chooses 1st of month (not current date)) reported by @PAS-BC
|
||||
- [PR 9179](https://github.com/firefly-iii/firefly-iii/pull/9179) (fix Navigation.php MTD logic to make tests pass.) reported by @tasnim0tantawi
|
||||
- [PR 9239](https://github.com/firefly-iii/firefly-iii/pull/9239) (Fix webhook index page when Firefly is not served at root) reported by @jfpedroza
|
||||
- [Issue 9168](https://github.com/firefly-iii/firefly-iii/issues/9168) (Custom logout URL doesn't work.) reported by @JC5
|
||||
- [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
|
||||
|
||||
- Expand v2 API
|
||||
|
||||
## 6.1.20 - 2024-09-29
|
||||
|
||||
### Added
|
||||
|
||||
- Enabled the expression engine built by @michaelhthomas. Read more about it in [the documentation](https://docs.firefly-iii.org/references/firefly-iii/rule-expressions/).
|
||||
- Add running balance data, see if it can be used in the layout in the future.
|
||||
- [PR 9160](https://github.com/firefly-iii/firefly-iii/pull/9160) (add test cases for api/v1/autocomplete/CategoryController) reported by @tasnim0tantawi
|
||||
- [PR 9178](https://github.com/firefly-iii/firefly-iii/pull/9178) (Add test cases for Api\V1\Controllers\Autocomplete\BillController & BudgetController) reported by @tasnim0tantawi
|
||||
- [PR 9171](https://github.com/firefly-iii/firefly-iii/pull/9171) (Add about test) reported by @mzhubail
|
||||
|
||||
### Changed
|
||||
|
||||
- [PR 9096](https://github.com/firefly-iii/firefly-iii/pull/9096) (chore: fix some comments) reported by @withbest
|
||||
|
||||
### Fixed
|
||||
|
||||
- [Issue 9078](https://github.com/firefly-iii/firefly-iii/issues/9078) (bcadd exception while using POST transactions) reported by @dbtdsilva
|
||||
- [Discussion 9080](https://github.com/orgs/firefly-iii/discussions/9080) (Incorrect sorting on expense accounts) started by @pc-zookeeper
|
||||
- [Issue 9084](https://github.com/firefly-iii/firefly-iii/issues/9084) (API Call for bills/nextExpectedMatch does not update) reported by @marcelweikum
|
||||
- [Issue 9103](https://github.com/firefly-iii/firefly-iii/issues/9103) (Default Currency does not apply to Accounts.) reported by @chrisgriff1512
|
||||
- [Issue 9140](https://github.com/firefly-iii/firefly-iii/issues/9140) (Dashboard 'Today' option chooses 1st of month (not current date)) reported by @PAS-BC
|
||||
- [PR 9179](https://github.com/firefly-iii/firefly-iii/pull/9179) (fix Navigation.php MTD logic to make tests pass.) reported by @tasnim0tantawi
|
||||
- [PR 9239](https://github.com/firefly-iii/firefly-iii/pull/9239) (Fix webhook index page when Firefly is not served at root) reported by @jfpedroza
|
||||
- [Issue 9168](https://github.com/firefly-iii/firefly-iii/issues/9168) (Custom logout URL doesn't work.) reported by @JC5
|
||||
- [Issue 9155](https://github.com/firefly-iii/firefly-iii/issues/9155) (internal_reference_is does not correctly match numeric internal references) reported by @Lrns123
|
||||
|
||||
### API
|
||||
|
||||
- Expand v2 API
|
||||
|
||||
## 6.1.19 - 2024-07-20
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -93,11 +93,12 @@
|
||||
"laravel/framework": "^11",
|
||||
"laravel/passport": "^12",
|
||||
"laravel/sanctum": "^4",
|
||||
"laravel/slack-notification-channel": "^3.0",
|
||||
"laravel/slack-notification-channel": "^3.3",
|
||||
"laravel/ui": "^4.2",
|
||||
"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",
|
||||
@@ -120,10 +121,10 @@
|
||||
"larastan/larastan": "^2",
|
||||
"laravel-json-api/testing": "^3.0",
|
||||
"mockery/mockery": "1.*",
|
||||
"phpstan/extension-installer": "^1.3",
|
||||
"phpstan/extension-installer": "^1.4",
|
||||
"phpstan/phpstan": "^1.10",
|
||||
"phpstan/phpstan-deprecation-rules": "^1.1",
|
||||
"phpstan/phpstan-strict-rules": "^1.4",
|
||||
"phpstan/phpstan-strict-rules": "^1.6",
|
||||
"phpunit/phpunit": "^10",
|
||||
"thecodingmachine/phpstan-strict-rules": "^1.0"
|
||||
},
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
2047
composer.lock
generated
2047
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -65,10 +65,6 @@ use FireflyIII\Support\Binder\UserGroupAccount;
|
||||
use FireflyIII\Support\Binder\UserGroupBill;
|
||||
use FireflyIII\Support\Binder\UserGroupTransaction;
|
||||
use FireflyIII\TransactionRules\Actions\AddTag;
|
||||
use FireflyIII\TransactionRules\Actions\AppendDescription;
|
||||
use FireflyIII\TransactionRules\Actions\AppendDescriptionToNotes;
|
||||
use FireflyIII\TransactionRules\Actions\AppendNotes;
|
||||
use FireflyIII\TransactionRules\Actions\AppendNotesToDescription;
|
||||
use FireflyIII\TransactionRules\Actions\ClearBudget;
|
||||
use FireflyIII\TransactionRules\Actions\ClearCategory;
|
||||
use FireflyIII\TransactionRules\Actions\ClearNotes;
|
||||
@@ -77,10 +73,6 @@ use FireflyIII\TransactionRules\Actions\ConvertToTransfer;
|
||||
use FireflyIII\TransactionRules\Actions\ConvertToWithdrawal;
|
||||
use FireflyIII\TransactionRules\Actions\DeleteTransaction;
|
||||
use FireflyIII\TransactionRules\Actions\LinkToBill;
|
||||
use FireflyIII\TransactionRules\Actions\MoveDescriptionToNotes;
|
||||
use FireflyIII\TransactionRules\Actions\MoveNotesToDescription;
|
||||
use FireflyIII\TransactionRules\Actions\PrependDescription;
|
||||
use FireflyIII\TransactionRules\Actions\PrependNotes;
|
||||
use FireflyIII\TransactionRules\Actions\RemoveAllTags;
|
||||
use FireflyIII\TransactionRules\Actions\RemoveTag;
|
||||
use FireflyIII\TransactionRules\Actions\SetAmount;
|
||||
@@ -110,14 +102,15 @@ return [
|
||||
],
|
||||
// some feature flags:
|
||||
'feature_flags' => [
|
||||
'export' => true,
|
||||
'telemetry' => false,
|
||||
'webhooks' => true,
|
||||
'handle_debts' => true,
|
||||
'expression_engine' => true,
|
||||
'export' => true,
|
||||
'telemetry' => false,
|
||||
'webhooks' => true,
|
||||
'handle_debts' => true,
|
||||
'expression_engine' => true,
|
||||
'running_balance_column' => env('USE_RUNNING_BALANCE', false),
|
||||
// see cer.php for exchange rates feature flag.
|
||||
],
|
||||
'version' => 'develop/2024-08-05',
|
||||
'version' => 'develop/2024-11-03',
|
||||
'api_version' => '2.1.0',
|
||||
'db_version' => 24,
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ return [
|
||||
'default' => envNonEmpty('MAIL_MAILER', 'log'),
|
||||
|
||||
'mailers' => [
|
||||
'smtp' => [
|
||||
'smtp' => [
|
||||
'transport' => 'smtp',
|
||||
'host' => envNonEmpty('MAIL_HOST', 'smtp.mailtrap.io'),
|
||||
'port' => (int)env('MAIL_PORT', 2525),
|
||||
@@ -45,39 +45,41 @@ return [
|
||||
'timeout' => null,
|
||||
'verify_peer' => null !== env('MAIL_ENCRYPTION'),
|
||||
],
|
||||
|
||||
'ses' => [
|
||||
'mailersend' => [
|
||||
'transport' => 'mailersend',
|
||||
],
|
||||
'ses' => [
|
||||
'transport' => 'ses',
|
||||
],
|
||||
|
||||
'mailgun' => [
|
||||
'mailgun' => [
|
||||
'transport' => 'mailgun',
|
||||
],
|
||||
|
||||
'mandrill' => [
|
||||
'mandrill' => [
|
||||
'transport' => 'mandrill',
|
||||
],
|
||||
|
||||
'postmark' => [
|
||||
'postmark' => [
|
||||
'transport' => 'postmark',
|
||||
],
|
||||
|
||||
'sendmail' => [
|
||||
'sendmail' => [
|
||||
'transport' => 'sendmail',
|
||||
'path' => envNonEmpty('MAIL_SENDMAIL_COMMAND', '/usr/sbin/sendmail -bs'),
|
||||
],
|
||||
'log' => [
|
||||
'log' => [
|
||||
'transport' => 'log',
|
||||
'channel' => env('MAIL_LOG_CHANNEL', 'stack'),
|
||||
'level' => 'info',
|
||||
],
|
||||
'null' => [
|
||||
'null' => [
|
||||
'transport' => 'log',
|
||||
'channel' => env('MAIL_LOG_CHANNEL', 'stack'),
|
||||
'level' => 'notice',
|
||||
],
|
||||
|
||||
'array' => [
|
||||
'array' => [
|
||||
'transport' => 'array',
|
||||
],
|
||||
],
|
||||
|
||||
2540
package-lock.json
generated
2540
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -5,5 +5,8 @@
|
||||
"workspaces": [
|
||||
"resources/assets/v1",
|
||||
"resources/assets/v2"
|
||||
]
|
||||
],
|
||||
"devDependencies": {
|
||||
"postcss": "^8.4.47"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
diff --git a/node_modules/admin-lte/src/scss/_app-sidebar.scss b/node_modules/admin-lte/src/scss/_app-sidebar.scss
|
||||
index 437946b..040bf5d 100644
|
||||
old mode 100644
|
||||
new mode 100755
|
||||
index 69dfc16..dc341eb
|
||||
--- a/node_modules/admin-lte/src/scss/_app-sidebar.scss
|
||||
+++ b/node_modules/admin-lte/src/scss/_app-sidebar.scss
|
||||
@@ -582,7 +582,6 @@ body:not(.app-loaded) {
|
||||
@@ -577,7 +577,6 @@ body:not(.app-loaded) {
|
||||
|
||||
@if $enable-dark-mode {
|
||||
@include color-mode(dark) {
|
||||
@@ -73,6 +73,7 @@ function formatLabel(str, maxwidth) {
|
||||
}
|
||||
|
||||
var defaultChartOptions = {
|
||||
|
||||
elements: {
|
||||
line: {
|
||||
cubicInterpolationMode: 'monotone'
|
||||
@@ -158,4 +159,4 @@ var neutralDefaultPieOptions = {
|
||||
},
|
||||
maintainAspectRatio: true,
|
||||
responsive: true
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user