mirror of
https://github.com/firefly-iii/firefly-iii.git
synced 2025-08-16 10:54:39 +00:00
Compare commits
150 Commits
develop-20
...
v6.1.24
Author | SHA1 | Date | |
---|---|---|---|
|
17b0b1f43f | ||
|
b61df5ec19 | ||
|
1ac7275f83 | ||
|
cd10d04907 | ||
|
f9b76fcb8b | ||
|
093fa067e6 | ||
|
fa655f065b | ||
|
c8f2244912 | ||
|
f3a20e14a6 | ||
|
33ad47b115 | ||
|
775424d3b7 | ||
|
c9c86bbd1d | ||
|
f76a6ad85c | ||
|
2138b14d89 | ||
|
1bf61f57f5 | ||
|
07b55bd71f | ||
|
8d2d3d4002 | ||
|
d182b4b4a6 | ||
|
60f6a91fe4 | ||
|
ec89a2f956 | ||
|
87113d7181 | ||
|
59fae290e5 | ||
|
1a8ba2ce53 | ||
|
dddaa25d86 | ||
|
f28341587a | ||
|
5593bf3e08 | ||
|
92574a7a9d | ||
|
e049266f5d | ||
|
5b3e6fcb07 | ||
|
b0bfb556db | ||
|
484acbcb45 | ||
|
cdc802cfb8 | ||
|
582671ca84 | ||
|
22498b5804 | ||
|
87f277a482 | ||
|
ae0d74f57a | ||
|
0ae5593dde | ||
|
0d11769590 | ||
|
b7d8daf013 | ||
|
a9c0126b05 | ||
|
6bc5a57d10 | ||
|
2714ee96f1 | ||
|
524d382b7a | ||
|
2723e05d2a | ||
|
6dd9bda6b4 | ||
|
44449bc716 | ||
|
b17d8edb50 | ||
|
578072238a | ||
|
b4edd3dcc4 | ||
|
068094caac | ||
|
deb58e617d | ||
|
baca0c1120 | ||
|
02543438a4 | ||
|
d507e59038 | ||
|
9d0fd7ef1b | ||
|
dbef5e2143 | ||
|
04eca755d2 | ||
|
7883692196 | ||
|
8f64977cb9 | ||
|
f94fdc4979 | ||
|
a0a0e28447 | ||
|
f6f7783b94 | ||
|
d233cc1de8 | ||
|
37671499c8 | ||
|
c83b79998d | ||
|
ed842c2b42 | ||
|
8c5f114339 | ||
|
8b2f1d0b4f | ||
|
591a1b3050 | ||
|
42ec3fe02b | ||
|
370a398b5e | ||
|
554d89b6e9 | ||
|
cb049f5dda | ||
|
0728668d41 | ||
|
dfc187874e | ||
|
225588f3e7 | ||
|
06cc6c29aa | ||
|
b2d4469908 | ||
|
c398383905 | ||
|
7af9dce33b | ||
|
038790a5d6 | ||
|
fb3295bde1 | ||
|
43a4fd2ecb | ||
|
899c72d068 | ||
|
d118c0d886 | ||
|
6d4004d1ed | ||
|
ae60cd5b28 | ||
|
ab31a72199 | ||
|
2c1b9534f3 | ||
|
7028cb1546 | ||
|
dc1ecf6a42 | ||
|
3a27f9d02c | ||
|
4b27ab38f8 | ||
|
40de147611 | ||
|
df5756dc86 | ||
|
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 |
118
.ci/php-cs-fixer/composer.lock
generated
118
.ci/php-cs-fixer/composer.lock
generated
@@ -72,16 +72,16 @@
|
||||
},
|
||||
{
|
||||
"name": "composer/pcre",
|
||||
"version": "3.3.1",
|
||||
"version": "3.3.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/composer/pcre.git",
|
||||
"reference": "63aaeac21d7e775ff9bc9d45021e1745c97521c4"
|
||||
"reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/composer/pcre/zipball/63aaeac21d7e775ff9bc9d45021e1745c97521c4",
|
||||
"reference": "63aaeac21d7e775ff9bc9d45021e1745c97521c4",
|
||||
"url": "https://api.github.com/repos/composer/pcre/zipball/b2bed4734f0cc156ee1fe9c0da2550420d99a21e",
|
||||
"reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -91,8 +91,8 @@
|
||||
"phpstan/phpstan": "<1.11.10"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpstan/phpstan": "^1.11.10",
|
||||
"phpstan/phpstan-strict-rules": "^1.1",
|
||||
"phpstan/phpstan": "^1.12 || ^2",
|
||||
"phpstan/phpstan-strict-rules": "^1 || ^2",
|
||||
"phpunit/phpunit": "^8 || ^9"
|
||||
},
|
||||
"type": "library",
|
||||
@@ -131,7 +131,7 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/composer/pcre/issues",
|
||||
"source": "https://github.com/composer/pcre/tree/3.3.1"
|
||||
"source": "https://github.com/composer/pcre/tree/3.3.2"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -147,7 +147,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-08-27T18:44:43+00:00"
|
||||
"time": "2024-11-12T16:29:46+00:00"
|
||||
},
|
||||
{
|
||||
"name": "composer/semver",
|
||||
@@ -1259,16 +1259,16 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/console",
|
||||
"version": "v7.1.5",
|
||||
"version": "v7.1.8",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/console.git",
|
||||
"reference": "0fa539d12b3ccf068a722bbbffa07ca7079af9ee"
|
||||
"reference": "ff04e5b5ba043d2badfb308197b9e6b42883fcd5"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/console/zipball/0fa539d12b3ccf068a722bbbffa07ca7079af9ee",
|
||||
"reference": "0fa539d12b3ccf068a722bbbffa07ca7079af9ee",
|
||||
"url": "https://api.github.com/repos/symfony/console/zipball/ff04e5b5ba043d2badfb308197b9e6b42883fcd5",
|
||||
"reference": "ff04e5b5ba043d2badfb308197b9e6b42883fcd5",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -1332,7 +1332,7 @@
|
||||
"terminal"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/console/tree/v7.1.5"
|
||||
"source": "https://github.com/symfony/console/tree/v7.1.8"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -1348,7 +1348,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-09-20T08:28:38+00:00"
|
||||
"time": "2024-11-06T14:23:19+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/deprecation-contracts",
|
||||
@@ -1419,16 +1419,16 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/event-dispatcher",
|
||||
"version": "v7.1.1",
|
||||
"version": "v7.1.6",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/event-dispatcher.git",
|
||||
"reference": "9fa7f7a21beb22a39a8f3f28618b29e50d7a55a7"
|
||||
"reference": "87254c78dd50721cfd015b62277a8281c5589702"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/9fa7f7a21beb22a39a8f3f28618b29e50d7a55a7",
|
||||
"reference": "9fa7f7a21beb22a39a8f3f28618b29e50d7a55a7",
|
||||
"url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/87254c78dd50721cfd015b62277a8281c5589702",
|
||||
"reference": "87254c78dd50721cfd015b62277a8281c5589702",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -1479,7 +1479,7 @@
|
||||
"description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/event-dispatcher/tree/v7.1.1"
|
||||
"source": "https://github.com/symfony/event-dispatcher/tree/v7.1.6"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -1495,7 +1495,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-05-31T14:57:53+00:00"
|
||||
"time": "2024-09-25T14:20:29+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/event-dispatcher-contracts",
|
||||
@@ -1575,16 +1575,16 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/filesystem",
|
||||
"version": "v7.1.5",
|
||||
"version": "v7.1.6",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/filesystem.git",
|
||||
"reference": "61fe0566189bf32e8cfee78335d8776f64a66f5a"
|
||||
"reference": "c835867b3c62bb05c7fe3d637c871c7ae52024d4"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/filesystem/zipball/61fe0566189bf32e8cfee78335d8776f64a66f5a",
|
||||
"reference": "61fe0566189bf32e8cfee78335d8776f64a66f5a",
|
||||
"url": "https://api.github.com/repos/symfony/filesystem/zipball/c835867b3c62bb05c7fe3d637c871c7ae52024d4",
|
||||
"reference": "c835867b3c62bb05c7fe3d637c871c7ae52024d4",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -1621,7 +1621,7 @@
|
||||
"description": "Provides basic utilities for the filesystem",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/filesystem/tree/v7.1.5"
|
||||
"source": "https://github.com/symfony/filesystem/tree/v7.1.6"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -1637,20 +1637,20 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-09-17T09:16:35+00:00"
|
||||
"time": "2024-10-25T15:11:02+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/finder",
|
||||
"version": "v7.1.4",
|
||||
"version": "v7.1.6",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/finder.git",
|
||||
"reference": "d95bbf319f7d052082fb7af147e0f835a695e823"
|
||||
"reference": "2cb89664897be33f78c65d3d2845954c8d7a43b8"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/finder/zipball/d95bbf319f7d052082fb7af147e0f835a695e823",
|
||||
"reference": "d95bbf319f7d052082fb7af147e0f835a695e823",
|
||||
"url": "https://api.github.com/repos/symfony/finder/zipball/2cb89664897be33f78c65d3d2845954c8d7a43b8",
|
||||
"reference": "2cb89664897be33f78c65d3d2845954c8d7a43b8",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -1685,7 +1685,7 @@
|
||||
"description": "Finds files and directories via an intuitive fluent interface",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/finder/tree/v7.1.4"
|
||||
"source": "https://github.com/symfony/finder/tree/v7.1.6"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -1701,20 +1701,20 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-08-13T14:28:19+00:00"
|
||||
"time": "2024-10-01T08:31:23+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/options-resolver",
|
||||
"version": "v7.1.1",
|
||||
"version": "v7.1.6",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/options-resolver.git",
|
||||
"reference": "47aa818121ed3950acd2b58d1d37d08a94f9bf55"
|
||||
"reference": "85e95eeede2d41cd146146e98c9c81d9214cae85"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/options-resolver/zipball/47aa818121ed3950acd2b58d1d37d08a94f9bf55",
|
||||
"reference": "47aa818121ed3950acd2b58d1d37d08a94f9bf55",
|
||||
"url": "https://api.github.com/repos/symfony/options-resolver/zipball/85e95eeede2d41cd146146e98c9c81d9214cae85",
|
||||
"reference": "85e95eeede2d41cd146146e98c9c81d9214cae85",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -1752,7 +1752,7 @@
|
||||
"options"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/options-resolver/tree/v7.1.1"
|
||||
"source": "https://github.com/symfony/options-resolver/tree/v7.1.6"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -1768,7 +1768,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-05-31T14:57:53+00:00"
|
||||
"time": "2024-09-25T14:20:29+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-ctype",
|
||||
@@ -2246,16 +2246,16 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/process",
|
||||
"version": "v7.1.5",
|
||||
"version": "v7.1.8",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/process.git",
|
||||
"reference": "5c03ee6369281177f07f7c68252a280beccba847"
|
||||
"reference": "42783370fda6e538771f7c7a36e9fa2ee3a84892"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/process/zipball/5c03ee6369281177f07f7c68252a280beccba847",
|
||||
"reference": "5c03ee6369281177f07f7c68252a280beccba847",
|
||||
"url": "https://api.github.com/repos/symfony/process/zipball/42783370fda6e538771f7c7a36e9fa2ee3a84892",
|
||||
"reference": "42783370fda6e538771f7c7a36e9fa2ee3a84892",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -2287,7 +2287,7 @@
|
||||
"description": "Executes commands in sub-processes",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/process/tree/v7.1.5"
|
||||
"source": "https://github.com/symfony/process/tree/v7.1.8"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -2303,7 +2303,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-09-19T21:48:23+00:00"
|
||||
"time": "2024-11-06T14:23:19+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/service-contracts",
|
||||
@@ -2390,16 +2390,16 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/stopwatch",
|
||||
"version": "v7.1.1",
|
||||
"version": "v7.1.6",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/stopwatch.git",
|
||||
"reference": "5b75bb1ac2ba1b9d05c47fc4b3046a625377d23d"
|
||||
"reference": "8b4a434e6e7faf6adedffb48783a5c75409a1a05"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/stopwatch/zipball/5b75bb1ac2ba1b9d05c47fc4b3046a625377d23d",
|
||||
"reference": "5b75bb1ac2ba1b9d05c47fc4b3046a625377d23d",
|
||||
"url": "https://api.github.com/repos/symfony/stopwatch/zipball/8b4a434e6e7faf6adedffb48783a5c75409a1a05",
|
||||
"reference": "8b4a434e6e7faf6adedffb48783a5c75409a1a05",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -2432,7 +2432,7 @@
|
||||
"description": "Provides a way to profile code",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/stopwatch/tree/v7.1.1"
|
||||
"source": "https://github.com/symfony/stopwatch/tree/v7.1.6"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -2448,20 +2448,20 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-05-31T14:57:53+00:00"
|
||||
"time": "2024-09-25T14:20:29+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/string",
|
||||
"version": "v7.1.5",
|
||||
"version": "v7.1.8",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/string.git",
|
||||
"reference": "d66f9c343fa894ec2037cc928381df90a7ad4306"
|
||||
"reference": "591ebd41565f356fcd8b090fe64dbb5878f50281"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/string/zipball/d66f9c343fa894ec2037cc928381df90a7ad4306",
|
||||
"reference": "d66f9c343fa894ec2037cc928381df90a7ad4306",
|
||||
"url": "https://api.github.com/repos/symfony/string/zipball/591ebd41565f356fcd8b090fe64dbb5878f50281",
|
||||
"reference": "591ebd41565f356fcd8b090fe64dbb5878f50281",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -2519,7 +2519,7 @@
|
||||
"utf8"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/string/tree/v7.1.5"
|
||||
"source": "https://github.com/symfony/string/tree/v7.1.8"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -2535,16 +2535,16 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-09-20T08:28:38+00:00"
|
||||
"time": "2024-11-13T13:31:21+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/label-actions.yml
vendored
2
.github/label-actions.yml
vendored
@@ -4,6 +4,7 @@
|
||||
feature:
|
||||
issues:
|
||||
# Post a comment, `{issue-author}` is an optional placeholder
|
||||
unlabel: feature
|
||||
comment: |
|
||||
Hi there!
|
||||
|
||||
@@ -32,6 +33,7 @@ epic:
|
||||
Thank you for your contributions.
|
||||
|
||||
enhancement:
|
||||
unlabel: enhancement
|
||||
issues:
|
||||
# Post a comment, `{issue-author}` is an optional placeholder
|
||||
comment: |
|
||||
|
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"
|
||||
|
4
.github/workflows/stale.yml
vendored
4
.github/workflows/stale.yml
vendored
@@ -12,6 +12,7 @@ jobs:
|
||||
permissions:
|
||||
issues: write # for actions/stale to close stale issues
|
||||
pull-requests: write # for actions/stale to close stale PRs
|
||||
actions: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v9
|
||||
@@ -35,4 +36,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,8 @@ 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
|
||||
- Antônio Franco
|
||||
- yparitcher
|
||||
- Jhon Pedroza
|
||||
- mzhubail
|
||||
- tasnim
|
||||
|
@@ -112,7 +112,12 @@ class StoreController extends Controller
|
||||
|
||||
return response()->json([], 422);
|
||||
}
|
||||
$helper->saveAttachmentFromApi($attachment, $body);
|
||||
$result = $helper->saveAttachmentFromApi($attachment, $body);
|
||||
if (false === $result) {
|
||||
app('log')->error('Could not save attachment from API.');
|
||||
|
||||
return response()->json([], 422);
|
||||
}
|
||||
|
||||
return response()->json([], 204);
|
||||
}
|
||||
|
@@ -52,7 +52,7 @@ class AboutController extends Controller
|
||||
$data
|
||||
= [
|
||||
'version' => config('firefly.version'),
|
||||
'api_version' => config('firefly.api_version'),
|
||||
'api_version' => config('firefly.version'),
|
||||
'php_version' => $phpVersion,
|
||||
'os' => $phpOs,
|
||||
'driver' => $currentDriver,
|
||||
|
@@ -46,6 +46,8 @@ class DateRequest extends FormRequest
|
||||
{
|
||||
$start = $this->getCarbonDate('start');
|
||||
$end = $this->getCarbonDate('end');
|
||||
$start->startOfDay();
|
||||
$end->endOfDay();
|
||||
if ($start->diffInYears($end, true) > 5) {
|
||||
throw new FireflyException('Date range out of range.');
|
||||
}
|
||||
|
@@ -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',
|
||||
];
|
||||
}
|
||||
|
||||
|
@@ -138,7 +138,7 @@ class StoreRequest extends FormRequest
|
||||
// all custom fields:
|
||||
'internal_reference' => $this->clearString((string)$object['internal_reference']),
|
||||
'external_id' => $this->clearString((string)$object['external_id']),
|
||||
'original_source' => sprintf('ff3-v%s|api-v%s', config('firefly.version'), config('firefly.api_version')),
|
||||
'original_source' => sprintf('ff3-v%s', config('firefly.version')),
|
||||
'recurrence_id' => $this->integerFromValue($object['recurrence_id']),
|
||||
'bunq_payment_id' => $this->clearString((string)$object['bunq_payment_id']),
|
||||
'external_url' => $this->clearString((string)$object['external_url']),
|
||||
|
@@ -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
|
||||
|
@@ -44,7 +44,7 @@ class DateRequest extends FormRequest
|
||||
public function getAll(): array
|
||||
{
|
||||
return [
|
||||
'start' => $this->getCarbonDate('start'),
|
||||
'start' => $this->getCarbonDate('start')->startOfDay(),
|
||||
'end' => $this->getCarbonDate('end')->endOfDay(),
|
||||
];
|
||||
}
|
||||
|
@@ -147,7 +147,7 @@ class StoreRequest extends FormRequest
|
||||
// all custom fields:
|
||||
'internal_reference' => $this->clearString((string)$object['internal_reference']),
|
||||
'external_id' => $this->clearString((string)$object['external_id']),
|
||||
'original_source' => sprintf('ff3-v%s|api-v%s', config('firefly.version'), config('firefly.api_version')),
|
||||
'original_source' => sprintf('ff3-v%s', config('firefly.version')),
|
||||
'recurrence_id' => $this->integerFromValue($object['recurrence_id']),
|
||||
'bunq_payment_id' => $this->clearString((string)$object['bunq_payment_id']),
|
||||
'external_url' => $this->clearString((string)$object['external_url']),
|
||||
|
@@ -48,8 +48,19 @@ class StoreRequest extends FormRequest
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
$roles = [];
|
||||
foreach (UserRoleEnum::cases() as $role) {
|
||||
$roles[] = $role->value;
|
||||
}
|
||||
$string = implode(',', $roles);
|
||||
|
||||
return [
|
||||
'title' => 'unique:user_groups,title|required|min:1|max:255',
|
||||
'title' => 'unique:user_groups,title|required|min:1|max:255',
|
||||
'members' => 'required|min:1',
|
||||
'members.*.user_email' => 'email|missing_with:members.*.user_id',
|
||||
'members.*.user_id' => 'integer|exists:users,id|missing_with:members.*.user_email',
|
||||
'members.*.roles' => 'required|array|min:1',
|
||||
'members.*.roles.*' => sprintf('required|in:%s', $string),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
44
app/Casts/SeparateTimezoneCaster.php
Normal file
44
app/Casts/SeparateTimezoneCaster.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace FireflyIII\Casts;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
/**
|
||||
* Class SeparateTimezoneCaster
|
||||
*
|
||||
* Checks if the object has a separate _tz value. If it does, it will use that timezone to parse the date.
|
||||
* If it is NULL, it will use the system's timezone.
|
||||
*
|
||||
* At some point a user's database consists entirely of UTC dates, and we won't need this anymore. However,
|
||||
* the completeness of this migration is not yet guaranteed.
|
||||
*/
|
||||
class SeparateTimezoneCaster implements CastsAttributes
|
||||
{
|
||||
/**
|
||||
* @param array<string, mixed> $attributes
|
||||
*/
|
||||
public function get(Model $model, string $key, mixed $value, array $attributes): ?Carbon
|
||||
{
|
||||
if ('' === $value || null === $value) {
|
||||
return null;
|
||||
}
|
||||
$timeZone = $attributes[sprintf('%s_tz', $key)] ?? config('app.timezone');
|
||||
|
||||
return Carbon::parse($value, $timeZone)->setTimezone(config('app.timezone'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the given value for storage.
|
||||
*
|
||||
* @param array<string, mixed> $attributes
|
||||
*/
|
||||
public function set(Model $model, string $key, mixed $value, array $attributes): mixed
|
||||
{
|
||||
return $value;
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
|
117
app/Console/Commands/Integrity/AddTimezonesToDates.php
Normal file
117
app/Console/Commands/Integrity/AddTimezonesToDates.php
Normal file
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/*
|
||||
* AddTimezonesToDates.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/.
|
||||
*/
|
||||
|
||||
namespace FireflyIII\Console\Commands\Integrity;
|
||||
|
||||
use FireflyIII\Console\Commands\ShowsFriendlyMessages;
|
||||
use FireflyIII\Models\AccountBalance;
|
||||
use FireflyIII\Models\AvailableBudget;
|
||||
use FireflyIII\Models\Bill;
|
||||
use FireflyIII\Models\BudgetLimit;
|
||||
use FireflyIII\Models\CurrencyExchangeRate;
|
||||
use FireflyIII\Models\InvitedUser;
|
||||
use FireflyIII\Models\PiggyBank;
|
||||
use FireflyIII\Models\PiggyBankEvent;
|
||||
use FireflyIII\Models\PiggyBankRepetition;
|
||||
use FireflyIII\Models\Recurrence;
|
||||
use FireflyIII\Models\Tag;
|
||||
use FireflyIII\Models\TransactionJournal;
|
||||
use FireflyIII\Support\Facades\FireflyConfig;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Database\QueryException;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class AddTimezonesToDates extends Command
|
||||
{
|
||||
use ShowsFriendlyMessages;
|
||||
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'firefly-iii:add-timezones-to-dates';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Make sure all dates have a timezone.';
|
||||
|
||||
public static array $models = [
|
||||
AccountBalance::class => ['date'], // done
|
||||
AvailableBudget::class => ['start_date', 'end_date'], // done
|
||||
Bill::class => ['date', 'end_date', 'extension_date'], // done
|
||||
BudgetLimit::class => ['start_date', 'end_date'], // done
|
||||
CurrencyExchangeRate::class => ['date'], // done
|
||||
InvitedUser::class => ['expires'],
|
||||
PiggyBankEvent::class => ['date'],
|
||||
PiggyBankRepetition::class => ['startdate', 'targetdate'],
|
||||
PiggyBank::class => ['startdate', 'targetdate'], // done
|
||||
Recurrence::class => ['first_date', 'repeat_until', 'latest_date'],
|
||||
Tag::class => ['date'],
|
||||
TransactionJournal::class => ['date'],
|
||||
];
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle(): void
|
||||
{
|
||||
foreach (self::$models as $model => $fields) {
|
||||
$this->addTimezoneToModel($model, $fields);
|
||||
}
|
||||
// not yet in UTC mode
|
||||
FireflyConfig::set('utc', false);
|
||||
}
|
||||
|
||||
private function addTimezoneToModel(string $model, array $fields): void
|
||||
{
|
||||
foreach ($fields as $field) {
|
||||
$this->addTimezoneToModelField($model, $field);
|
||||
}
|
||||
}
|
||||
|
||||
private function addTimezoneToModelField(string $model, string $field): void
|
||||
{
|
||||
$shortModel = str_replace('FireflyIII\Models\\', '', $model);
|
||||
$timezoneField = sprintf('%s_tz', $field);
|
||||
$count = 0;
|
||||
|
||||
try {
|
||||
$count = $model::whereNull($timezoneField)->count();
|
||||
} catch (QueryException $e) {
|
||||
$this->friendlyError(sprintf('Cannot add timezone information to field "%s" of model "%s". Field does not exist.', $field, $shortModel));
|
||||
Log::error($e->getMessage());
|
||||
}
|
||||
if (0 === $count) {
|
||||
$this->friendlyPositive(sprintf('Timezone information is present in field "%s" of model "%s".', $field, $shortModel));
|
||||
|
||||
return;
|
||||
}
|
||||
$this->friendlyInfo(sprintf('Adding timezone information to field "%s" of model "%s".', $field, $shortModel));
|
||||
|
||||
$model::whereNull($timezoneField)->update([$timezoneField => config('app.timezone')]);
|
||||
}
|
||||
}
|
109
app/Console/Commands/Integrity/ConvertDatesToUTC.php
Normal file
109
app/Console/Commands/Integrity/ConvertDatesToUTC.php
Normal file
@@ -0,0 +1,109 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/*
|
||||
* ConvertDatesToUTC.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/.
|
||||
*/
|
||||
|
||||
namespace FireflyIII\Console\Commands\Integrity;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use FireflyIII\Console\Commands\ShowsFriendlyMessages;
|
||||
use FireflyIII\Support\Facades\FireflyConfig;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Database\QueryException;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class ConvertDatesToUTC extends Command
|
||||
{
|
||||
use ShowsFriendlyMessages;
|
||||
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'firefly-iii:migrate-to-utc';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Convert stored dates to UTC.';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle(): int
|
||||
{
|
||||
/**
|
||||
* @var string $model
|
||||
* @var array $fields
|
||||
*/
|
||||
foreach (AddTimezonesToDates::$models as $model => $fields) {
|
||||
$this->ConvertModeltoUTC($model, $fields);
|
||||
}
|
||||
// tell the system we are now in UTC mode.
|
||||
FireflyConfig::set('utc', true);
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
private function ConvertModeltoUTC(string $model, array $fields): void
|
||||
{
|
||||
/** @var string $field */
|
||||
foreach ($fields as $field) {
|
||||
$this->convertFieldtoUTC($model, $field);
|
||||
}
|
||||
}
|
||||
|
||||
private function convertFieldtoUTC(string $model, string $field): void
|
||||
{
|
||||
$this->info(sprintf('Converting %s.%s to UTC', $model, $field));
|
||||
$shortModel = str_replace('FireflyIII\Models\\', '', $model);
|
||||
$timezoneField = sprintf('%s_tz', $field);
|
||||
$items = new Collection();
|
||||
$timeZone = config('app.timezone');
|
||||
|
||||
try {
|
||||
$items = $model::where($timezoneField, $timeZone)->get();
|
||||
} catch (QueryException $e) {
|
||||
$this->friendlyError(sprintf('Cannot find timezone information to field "%s" of model "%s". Field does not exist.', $field, $shortModel));
|
||||
Log::error($e->getMessage());
|
||||
}
|
||||
if (0 === $items->count()) {
|
||||
$this->friendlyPositive(sprintf('All timezone information is UTC in field "%s" of model "%s".', $field, $shortModel));
|
||||
|
||||
return;
|
||||
}
|
||||
$this->friendlyInfo(sprintf('Converting field "%s" of model "%s" to UTC.', $field, $shortModel));
|
||||
$items->each(
|
||||
function ($item) use ($field, $timezoneField): void {
|
||||
/** @var Carbon $date */
|
||||
$date = Carbon::parse($item->{$field}, $item->{$timezoneField});
|
||||
$date->setTimezone('UTC');
|
||||
$item->{$field} = $date->format('Y-m-d H:i:s');
|
||||
$item->{$timezoneField} = 'UTC';
|
||||
$item->save();
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
@@ -47,6 +47,7 @@ class ReportIntegrity extends Command
|
||||
return 1;
|
||||
}
|
||||
$commands = [
|
||||
'firefly-iii:add-timezones-to-dates',
|
||||
'firefly-iii:create-group-memberships',
|
||||
'firefly-iii:report-empty-objects',
|
||||
'firefly-iii:report-sum',
|
||||
|
@@ -58,7 +58,7 @@ class ReportSum extends Command
|
||||
|
||||
/** @var User $user */
|
||||
foreach ($userRepository->all() as $user) {
|
||||
$sum = (string)$user->transactions()->sum('amount');
|
||||
$sum = (string)$user->transactions()->selectRaw('SUM(amount) + SUM(foreign_amount) as total')->value('total');
|
||||
if (!is_numeric($sum)) {
|
||||
$message = sprintf('Error: Transactions for user #%d (%s) have an invalid sum ("%s").', $user->id, $user->email, $sum);
|
||||
$this->friendlyError($message);
|
||||
|
@@ -80,7 +80,7 @@ class ForceMigration extends Command
|
||||
sleep(2);
|
||||
Schema::dropIfExists('migrations');
|
||||
$this->friendlyLine('Re-run all migrations...');
|
||||
Artisan::call('migrate', ['--seed' => true]);
|
||||
Artisan::call('migrate', ['--seed' => true, '--force' => true]);
|
||||
sleep(2);
|
||||
$this->friendlyLine('');
|
||||
$this->friendlyWarning('There is a good chance you just saw a lot of error messages.');
|
||||
|
@@ -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
|
||||
|
@@ -47,7 +47,7 @@ class FixPostgresSequences extends Command
|
||||
return 0;
|
||||
}
|
||||
$this->friendlyLine('Going to verify PostgreSQL table sequences.');
|
||||
$tablesToCheck = ['2fa_tokens', 'account_meta', 'account_types', 'accounts', 'attachments', 'auto_budgets', 'available_budgets', 'bills', 'budget_limits', 'budget_transaction', 'budget_transaction_journal', 'budgets', 'categories', 'category_transaction', 'category_transaction_journal', 'configuration', 'currency_exchange_rates', 'failed_jobs', 'group_journals', 'jobs', 'journal_links', 'journal_meta', 'limit_repetitions', 'link_types', 'locations', 'migrations', 'notes', 'oauth_clients', 'oauth_personal_access_clients', 'object_groups', 'permissions', 'piggy_bank_events', 'piggy_bank_repetitions', 'piggy_banks', 'preferences', 'recurrences', 'recurrences_meta', 'recurrences_repetitions', 'recurrences_transactions', 'roles', 'rt_meta', 'rule_actions', 'rule_groups', 'rule_triggers', 'rules', 'tag_transaction_journal', 'tags', 'transaction_currencies', 'transaction_groups', 'transaction_journals', 'transaction_types', 'transactions', 'users', 'webhook_attempts', 'webhook_messages', 'webhooks'];
|
||||
$tablesToCheck = ['2fa_tokens', 'account_meta', 'account_types', 'accounts', 'attachments', 'auto_budgets', 'available_budgets', 'bills', 'budget_limits', 'budget_transaction', 'budget_transaction_journal', 'budgets', 'categories', 'category_transaction', 'category_transaction_journal', 'configuration', 'currency_exchange_rates', 'failed_jobs', 'group_journals', 'jobs', 'journal_links', 'journal_meta', 'link_types', 'locations', 'migrations', 'notes', 'oauth_clients', 'oauth_personal_access_clients', 'object_groups', 'permissions', 'piggy_bank_events', 'piggy_bank_repetitions', 'piggy_banks', 'preferences', 'recurrences', 'recurrences_meta', 'recurrences_repetitions', 'recurrences_transactions', 'roles', 'rt_meta', 'rule_actions', 'rule_groups', 'rule_triggers', 'rules', 'tag_transaction_journal', 'tags', 'transaction_currencies', 'transaction_groups', 'transaction_journals', 'transaction_types', 'transactions', 'users', 'webhook_attempts', 'webhook_messages', 'webhooks'];
|
||||
|
||||
foreach ($tablesToCheck as $tableToCheck) {
|
||||
$this->friendlyLine(sprintf('Checking the next id sequence for table "%s".', $tableToCheck));
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
|
@@ -154,7 +154,7 @@ class TransactionIdentifier extends Command
|
||||
app('log')->error($e->getMessage());
|
||||
$this->friendlyError('Firefly III could not find the "identifier" field in the "transactions" table.');
|
||||
$this->friendlyError(sprintf('This field is required for Firefly III version %s to run.', config('firefly.version')));
|
||||
$this->friendlyError('Please run "php artisan migrate" to add this field to the table.');
|
||||
$this->friendlyError('Please run "php artisan migrate --force" to add this field to the table.');
|
||||
$this->friendlyError('Then, run "php artisan firefly:upgrade-database" to try again.');
|
||||
|
||||
return null;
|
||||
|
@@ -67,6 +67,7 @@ class UpgradeDatabase extends Command
|
||||
'firefly-iii:restore-oauth-keys',
|
||||
'firefly-iii:correct-account-balance',
|
||||
// also just in case, some integrity commands:
|
||||
'firefly-iii:add-timezones-to-dates',
|
||||
'firefly-iii:create-group-memberships',
|
||||
'firefly-iii:upgrade-group-information',
|
||||
'firefly-iii:upgrade-currency-preferences',
|
||||
|
@@ -30,6 +30,7 @@ namespace FireflyIII\Enums;
|
||||
enum UserRoleEnum: string
|
||||
{
|
||||
// most basic rights, cannot see other members, can see everything else.
|
||||
// includes reading of metadata
|
||||
case READ_ONLY = 'ro';
|
||||
|
||||
// required to even USE the group properly (in this order)
|
||||
@@ -38,6 +39,15 @@ enum UserRoleEnum: string
|
||||
// required to edit, add or change categories/tags/object-groups
|
||||
case MANAGE_META = 'mng_meta';
|
||||
|
||||
// read other objects and things.
|
||||
case READ_BUDGETS = 'read_budgets';
|
||||
case READ_PIGGY_BANKS = 'read_piggies';
|
||||
case READ_SUBSCRIPTIONS = 'read_subscriptions';
|
||||
case READ_RULES = 'read_rules';
|
||||
case READ_RECURRING = 'read_recurring';
|
||||
case READ_WEBHOOKS = 'read_webhooks';
|
||||
case READ_CURRENCIES = 'read_currencies';
|
||||
|
||||
// manage other financial objects:
|
||||
case MANAGE_BUDGETS = 'mng_budgets';
|
||||
case MANAGE_PIGGY_BANKS = 'mng_piggies';
|
||||
|
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;
|
||||
}
|
||||
}
|
||||
}
|
@@ -58,20 +58,23 @@ class BillFactory
|
||||
/** @var Bill $bill */
|
||||
$bill = Bill::create(
|
||||
[
|
||||
'name' => $data['name'],
|
||||
'match' => 'MIGRATED_TO_RULES',
|
||||
'amount_min' => $data['amount_min'],
|
||||
'user_id' => $this->user->id,
|
||||
'user_group_id' => $this->user->user_group_id,
|
||||
'transaction_currency_id' => $currency->id,
|
||||
'amount_max' => $data['amount_max'],
|
||||
'date' => $data['date'],
|
||||
'end_date' => $data['end_date'] ?? null,
|
||||
'extension_date' => $data['extension_date'] ?? null,
|
||||
'repeat_freq' => $data['repeat_freq'],
|
||||
'skip' => $skip,
|
||||
'automatch' => true,
|
||||
'active' => $active,
|
||||
'name' => $data['name'],
|
||||
'match' => 'MIGRATED_TO_RULES',
|
||||
'amount_min' => $data['amount_min'],
|
||||
'user_id' => $this->user->id,
|
||||
'user_group_id' => $this->user->user_group_id,
|
||||
'transaction_currency_id' => $currency->id,
|
||||
'amount_max' => $data['amount_max'],
|
||||
'date' => $data['date'],
|
||||
'date_tz' => $data['date']->format('e'),
|
||||
'end_date' => $data['end_date'] ?? null,
|
||||
'end_date_tz' => $data['end_date']?->format('e'),
|
||||
'extension_date' => $data['extension_date'] ?? null,
|
||||
'extension_date_tz' => $data['extension_date']?->format('e'),
|
||||
'repeat_freq' => $data['repeat_freq'],
|
||||
'skip' => $skip,
|
||||
'automatch' => true,
|
||||
'active' => $active,
|
||||
]
|
||||
);
|
||||
} catch (QueryException $e) {
|
||||
@@ -126,7 +129,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
|
||||
|
@@ -25,6 +25,7 @@ declare(strict_types=1);
|
||||
namespace FireflyIII\Factory;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use FireflyIII\Enums\TransactionTypeEnum;
|
||||
use FireflyIII\Exceptions\DuplicateTransactionException;
|
||||
use FireflyIII\Exceptions\FireflyException;
|
||||
use FireflyIII\Models\Account;
|
||||
@@ -43,6 +44,7 @@ use FireflyIII\Repositories\TransactionType\TransactionTypeRepositoryInterface;
|
||||
use FireflyIII\Repositories\UserGroups\Currency\CurrencyRepositoryInterface;
|
||||
use FireflyIII\Services\Internal\Destroy\JournalDestroyService;
|
||||
use FireflyIII\Services\Internal\Support\JournalServiceTrait;
|
||||
use FireflyIII\Support\Facades\FireflyConfig;
|
||||
use FireflyIII\Support\NullArrayObject;
|
||||
use FireflyIII\User;
|
||||
use FireflyIII\Validation\AccountValidator;
|
||||
@@ -157,7 +159,7 @@ class TransactionJournalFactory
|
||||
|
||||
$this->errorIfDuplicate($row['import_hash_v2']);
|
||||
|
||||
/** Some basic fields */
|
||||
// Some basic fields
|
||||
$type = $this->typeRepository->findTransactionType(null, $row['type']);
|
||||
$carbon = $row['date'] ?? today(config('app.timezone'));
|
||||
$order = $row['order'] ?? 0;
|
||||
@@ -170,6 +172,13 @@ class TransactionJournalFactory
|
||||
// Manipulate basic fields
|
||||
$carbon->setTimezone(config('app.timezone'));
|
||||
|
||||
// 2024-11-19, overrule timezone with UTC and store it as UTC.
|
||||
|
||||
if (FireflyConfig::get('utc', false)->data) {
|
||||
$carbon->setTimezone('UTC');
|
||||
}
|
||||
// $carbon->setTimezone('UTC');
|
||||
|
||||
try {
|
||||
// validate source and destination using a new Validator.
|
||||
$this->validateAccounts($row);
|
||||
@@ -205,7 +214,7 @@ class TransactionJournalFactory
|
||||
app('log')->debug('Done with getAccount(2x)');
|
||||
|
||||
// this is the moment for a reconciliation sanity check (again).
|
||||
if (TransactionType::RECONCILIATION === $type->type) {
|
||||
if (TransactionTypeEnum::RECONCILIATION->value === $type->type) {
|
||||
[$sourceAccount, $destinationAccount] = $this->reconciliationSanityCheck($sourceAccount, $destinationAccount);
|
||||
}
|
||||
|
||||
@@ -225,7 +234,8 @@ class TransactionJournalFactory
|
||||
'bill_id' => $billId,
|
||||
'transaction_currency_id' => $currency->id,
|
||||
'description' => substr($description, 0, 1000),
|
||||
'date' => $carbon->format('Y-m-d H:i:s'),
|
||||
'date' => $carbon,
|
||||
'date_tz' => $carbon->format('e'),
|
||||
'order' => $order,
|
||||
'tag_count' => 0,
|
||||
'completed' => 0,
|
||||
|
@@ -131,12 +131,14 @@ class BudgetLimitHandler
|
||||
app('log')->debug(sprintf('Will create AB for period %s to %s', $current->format('Y-m-d'), $currentEnd->format('Y-m-d')));
|
||||
$availableBudget = new AvailableBudget(
|
||||
[
|
||||
'user_id' => $budgetLimit->budget->user->id,
|
||||
'user_group_id' => $budgetLimit->budget->user->user_group_id,
|
||||
'transaction_currency_id' => $budgetLimit->transaction_currency_id,
|
||||
'start_date' => $current,
|
||||
'end_date' => $currentEnd,
|
||||
'amount' => $amount,
|
||||
'user_id' => $budgetLimit->budget->user->id,
|
||||
'user_group_id' => $budgetLimit->budget->user->user_group_id,
|
||||
'transaction_currency_id' => $budgetLimit->transaction_currency_id,
|
||||
'start_date' => $current,
|
||||
'start_date_tz' => $current->format('e'),
|
||||
'end_date' => $currentEnd,
|
||||
'end_date_tz' => $currentEnd->format('e'),
|
||||
'amount' => $amount,
|
||||
]
|
||||
);
|
||||
$availableBudget->save();
|
||||
|
@@ -58,6 +58,7 @@ class PiggyBankEventHandler
|
||||
'piggy_bank_id' => $event->piggyBank->id,
|
||||
'transaction_journal_id' => $journal?->id,
|
||||
'date' => $date->format('Y-m-d'),
|
||||
'date_tz' => $date->format('e'),
|
||||
'amount' => $event->amount,
|
||||
]
|
||||
);
|
||||
|
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());
|
||||
}
|
||||
}
|
||||
}
|
@@ -37,7 +37,9 @@ class PiggyBankObserver
|
||||
$repetition = new PiggyBankRepetition();
|
||||
$repetition->piggyBank()->associate($piggyBank);
|
||||
$repetition->startdate = $piggyBank->startdate;
|
||||
$repetition->startdate_tz = $piggyBank->startdate->format('e');
|
||||
$repetition->targetdate = $piggyBank->targetdate;
|
||||
$repetition->targetdate_tz = $piggyBank->targetdate?->format('e');
|
||||
$repetition->currentamount = '0';
|
||||
$repetition->save();
|
||||
}
|
||||
|
@@ -93,7 +93,7 @@ trait AttachmentCollection
|
||||
->where(
|
||||
static function (EloquentBuilder $q1): void { // @phpstan-ignore-line
|
||||
$q1->where('attachments.attachable_type', TransactionJournal::class);
|
||||
$q1->where('attachments.uploaded', true);
|
||||
// $q1->where('attachments.uploaded', true);
|
||||
$q1->whereNull('attachments.deleted_at');
|
||||
$q1->orWhereNull('attachments.attachable_type');
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -117,6 +117,7 @@ class GroupCollector implements GroupCollectorInterface
|
||||
'transaction_journals.transaction_type_id',
|
||||
'transaction_journals.description',
|
||||
'transaction_journals.date',
|
||||
'transaction_journals.date_tz',
|
||||
'transaction_journals.order',
|
||||
|
||||
// types
|
||||
@@ -156,7 +157,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 +165,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 +184,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 +192,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 +211,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 +219,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 +266,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 +274,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 +380,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 +388,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 +945,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 +953,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.');
|
||||
}
|
||||
}
|
||||
|
@@ -40,6 +40,7 @@ use FireflyIII\Support\Http\Controllers\ChartGeneration;
|
||||
use FireflyIII\Support\Http\Controllers\DateCalculation;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* Class AccountController.
|
||||
@@ -300,13 +301,13 @@ class AccountController extends Controller
|
||||
$start = clone session('start', today(config('app.timezone'))->startOfMonth());
|
||||
$end = clone session('end', today(config('app.timezone'))->endOfMonth());
|
||||
$defaultSet = $repository->getAccountsByType([AccountType::DEFAULT, AccountType::ASSET])->pluck('id')->toArray();
|
||||
app('log')->debug('Default set is ', $defaultSet);
|
||||
Log::debug('Default set is ', $defaultSet);
|
||||
$frontpage = app('preferences')->get('frontpageAccounts', $defaultSet);
|
||||
$frontpageArray = !is_array($frontpage->data) ? [] : $frontpage->data;
|
||||
app('log')->debug('Frontpage preference set is ', $frontpageArray);
|
||||
Log::debug('Frontpage preference set is ', $frontpageArray);
|
||||
if (0 === count($frontpageArray)) {
|
||||
app('preferences')->set('frontpageAccounts', $defaultSet);
|
||||
app('log')->debug('frontpage set is empty!');
|
||||
Log::debug('frontpage set is empty!');
|
||||
}
|
||||
$accounts = $repository->getAccountsById($frontpageArray);
|
||||
|
||||
@@ -414,7 +415,7 @@ class AccountController extends Controller
|
||||
*/
|
||||
private function periodByCurrency(Carbon $start, Carbon $end, Account $account, TransactionCurrency $currency): array
|
||||
{
|
||||
app('log')->debug(sprintf('Now in periodByCurrency("%s", "%s", %s, "%s")', $start->format('Y-m-d'), $end->format('Y-m-d'), $account->id, $currency->code));
|
||||
Log::debug(sprintf('Now in periodByCurrency("%s", "%s", %s, "%s")', $start->format('Y-m-d'), $end->format('Y-m-d'), $account->id, $currency->code));
|
||||
$locale = app('steam')->getLocale();
|
||||
$step = $this->calculateStep($start, $end);
|
||||
$result = [
|
||||
@@ -424,13 +425,13 @@ class AccountController extends Controller
|
||||
];
|
||||
$entries = [];
|
||||
$current = clone $start;
|
||||
app('log')->debug(sprintf('Step is %s', $step));
|
||||
Log::debug(sprintf('Step is %s', $step));
|
||||
|
||||
// fix for issue https://github.com/firefly-iii/firefly-iii/issues/8041
|
||||
// have to make sure this chart is always based on the balance at the END of the period.
|
||||
// This period depends on the size of the chart
|
||||
$current = app('navigation')->endOfX($current, $step, null);
|
||||
app('log')->debug(sprintf('$current date is %s', $current->format('Y-m-d')));
|
||||
Log::debug(sprintf('$current date is %s', $current->format('Y-m-d')));
|
||||
if ('1D' === $step) {
|
||||
// per day the entire period, balance for every day.
|
||||
$format = (string)trans('config.month_and_day_js', [], $locale);
|
||||
@@ -447,7 +448,7 @@ class AccountController extends Controller
|
||||
}
|
||||
if ('1W' === $step || '1M' === $step || '1Y' === $step) {
|
||||
while ($end >= $current) {
|
||||
app('log')->debug(sprintf('Current is: %s', $current->format('Y-m-d')));
|
||||
Log::debug(sprintf('Current is: %s', $current->format('Y-m-d')));
|
||||
$balance = (float)app('steam')->balance($account, $current, $currency);
|
||||
$label = app('navigation')->periodShow($current, $step);
|
||||
$entries[$label] = $balance;
|
||||
|
@@ -151,6 +151,7 @@ class CategoryController extends Controller
|
||||
*/
|
||||
private function reportPeriodChart(Collection $accounts, Carbon $start, Carbon $end, ?Category $category): array
|
||||
{
|
||||
|
||||
$income = [];
|
||||
$expenses = [];
|
||||
$categoryId = 0;
|
||||
@@ -169,8 +170,8 @@ class CategoryController extends Controller
|
||||
$categoryId = $category->id;
|
||||
// this gives us all currencies
|
||||
$collection = new Collection([$category]);
|
||||
$expenses = $opsRepository->listExpenses($start, $end, null, $collection);
|
||||
$income = $opsRepository->listIncome($start, $end, null, $collection);
|
||||
$expenses = $opsRepository->listExpenses($start, $end, $accounts, $collection);
|
||||
$income = $opsRepository->listIncome($start, $end, $accounts, $collection);
|
||||
}
|
||||
$currencies = array_unique(array_merge(array_keys($income), array_keys($expenses)));
|
||||
$periods = app('navigation')->listOfPeriods($start, $end);
|
||||
|
@@ -122,6 +122,7 @@ class CategoryReportController extends Controller
|
||||
|
||||
public function categoryIncome(Collection $accounts, Collection $categories, Carbon $start, Carbon $end): JsonResponse
|
||||
{
|
||||
|
||||
$result = [];
|
||||
$earned = $this->opsRepository->listIncome($start, $end, $accounts, $categories);
|
||||
|
||||
|
@@ -24,18 +24,19 @@ declare(strict_types=1);
|
||||
namespace FireflyIII\Http\Controllers\Chart;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use FireflyIII\Enums\TransactionTypeEnum;
|
||||
use FireflyIII\Generator\Chart\Basic\GeneratorInterface;
|
||||
use FireflyIII\Helpers\Collector\GroupCollectorInterface;
|
||||
use FireflyIII\Helpers\Report\NetWorthInterface;
|
||||
use FireflyIII\Http\Controllers\Controller;
|
||||
use FireflyIII\Models\Account;
|
||||
use FireflyIII\Models\TransactionType;
|
||||
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
|
||||
use FireflyIII\Support\CacheProperties;
|
||||
use FireflyIII\Support\Http\Controllers\BasicDataSupport;
|
||||
use FireflyIII\Support\Http\Controllers\ChartGeneration;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* Class ReportController.
|
||||
@@ -88,7 +89,7 @@ class ReportController extends Controller
|
||||
$includeNetWorth = $accountRepository->getMetaValue($account, 'include_net_worth');
|
||||
$result = null === $includeNetWorth ? true : '1' === $includeNetWorth;
|
||||
if (false === $result) {
|
||||
app('log')->debug(sprintf('Will not include "%s" in net worth charts.', $account->name));
|
||||
Log::debug(sprintf('Will not include "%s" in net worth charts.', $account->name));
|
||||
}
|
||||
|
||||
return $result;
|
||||
@@ -136,6 +137,7 @@ class ReportController extends Controller
|
||||
*/
|
||||
public function operations(Collection $accounts, Carbon $start, Carbon $end): JsonResponse
|
||||
{
|
||||
$end->endOfDay();
|
||||
// chart properties for cache:
|
||||
$cache = new CacheProperties();
|
||||
$cache->addProperty('chart.report.operations');
|
||||
@@ -146,7 +148,8 @@ class ReportController extends Controller
|
||||
// return response()->json($cache->get());
|
||||
}
|
||||
|
||||
app('log')->debug('Going to do operations for accounts ', $accounts->pluck('id')->toArray());
|
||||
Log::debug('Going to do operations for accounts ', $accounts->pluck('id')->toArray());
|
||||
Log::debug(sprintf('Period: %s to %s', $start->toW3cString(), $end->toW3cString()));
|
||||
$format = app('navigation')->preferredCarbonFormat($start, $end);
|
||||
$titleFormat = app('navigation')->preferredCarbonLocalizedFormat($start, $end);
|
||||
$preferredRange = app('navigation')->preferredRangeFormat($start, $end);
|
||||
@@ -158,7 +161,14 @@ class ReportController extends Controller
|
||||
$collector = app(GroupCollectorInterface::class);
|
||||
$collector->setRange($start, $end)->withAccountInformation();
|
||||
$collector->setXorAccounts($accounts);
|
||||
$collector->setTypes([TransactionType::WITHDRAWAL, TransactionType::DEPOSIT, TransactionType::RECONCILIATION, TransactionType::TRANSFER]);
|
||||
$collector->setTypes(
|
||||
[
|
||||
TransactionTypeEnum::WITHDRAWAL,
|
||||
TransactionTypeEnum::DEPOSIT,
|
||||
TransactionTypeEnum::RECONCILIATION,
|
||||
TransactionTypeEnum::TRANSFER,
|
||||
]
|
||||
);
|
||||
$journals = $collector->getExtractedJournals();
|
||||
|
||||
// loop. group by currency and by period.
|
||||
@@ -184,15 +194,25 @@ class ReportController extends Controller
|
||||
|
||||
// deposit = incoming
|
||||
// transfer or reconcile or opening balance, and these accounts are the destination.
|
||||
if (TransactionType::DEPOSIT === $journal['transaction_type_type'] || ((TransactionType::TRANSFER === $journal['transaction_type_type'] || TransactionType::RECONCILIATION === $journal['transaction_type_type'] || TransactionType::OPENING_BALANCE === $journal['transaction_type_type']) && in_array($journal['destination_account_id'], $ids, true))) {
|
||||
if (
|
||||
TransactionTypeEnum::DEPOSIT->value === $journal['transaction_type_type']
|
||||
|| ((
|
||||
TransactionTypeEnum::TRANSFER->value === $journal['transaction_type_type']
|
||||
|| TransactionTypeEnum::RECONCILIATION->value === $journal['transaction_type_type']
|
||||
|| TransactionTypeEnum::OPENING_BALANCE->value === $journal['transaction_type_type']
|
||||
)
|
||||
&& in_array($journal['destination_account_id'], $ids, true))) {
|
||||
$key = 'earned';
|
||||
}
|
||||
$data[$currencyId][$period][$key] = bcadd($data[$currencyId][$period][$key], $amount);
|
||||
}
|
||||
|
||||
// loop this data, make chart bars for each currency:
|
||||
Log::debug('Looping data');
|
||||
|
||||
/** @var array $currency */
|
||||
foreach ($data as $currency) {
|
||||
Log::debug(sprintf('Now processing currency "%s"', $currency['currency_name']));
|
||||
$income = [
|
||||
'label' => (string)trans('firefly.box_earned_in_currency', ['currency' => $currency['currency_name']]),
|
||||
'type' => 'bar',
|
||||
@@ -214,12 +234,15 @@ class ReportController extends Controller
|
||||
// loop all possible periods between $start and $end
|
||||
$currentStart = clone $start;
|
||||
$currentEnd = clone $end;
|
||||
Log::debug(sprintf('START current start and end: %s and %s', $currentStart->toW3cString(), $currentEnd->toW3cString()));
|
||||
|
||||
// #8374. Sloppy fix for yearly charts. Not really interested in a better fix with v2 layout and all.
|
||||
if ('1Y' === $preferredRange) {
|
||||
$currentEnd = app('navigation')->endOfPeriod($currentEnd, $preferredRange);
|
||||
}
|
||||
Log::debug('Start of sub-loop');
|
||||
while ($currentStart <= $currentEnd) {
|
||||
Log::debug(sprintf('Current start: %s', $currentStart->toW3cString()));
|
||||
$key = $currentStart->format($format);
|
||||
$title = $currentStart->isoFormat($titleFormat);
|
||||
// #8663 make sure the period exists in the data previously collected.
|
||||
@@ -227,12 +250,20 @@ class ReportController extends Controller
|
||||
$income['entries'][$title] = app('steam')->bcround($currency[$key]['earned'] ?? '0', $currency['currency_decimal_places']);
|
||||
$expense['entries'][$title] = app('steam')->bcround($currency[$key]['spent'] ?? '0', $currency['currency_decimal_places']);
|
||||
}
|
||||
// #9477 if the period is not in the data, add it with zero values.
|
||||
if (!array_key_exists($key, $currency)) {
|
||||
$income['entries'][$title] = '0';
|
||||
$expense['entries'][$title] = '0';
|
||||
|
||||
}
|
||||
$currentStart = app('navigation')->addPeriod($currentStart, $preferredRange, 0);
|
||||
}
|
||||
Log::debug('End of sub-loop');
|
||||
|
||||
$chartData[] = $income;
|
||||
$chartData[] = $expense;
|
||||
}
|
||||
Log::debug('End of loop');
|
||||
|
||||
$data = $this->generator->multiSet($chartData);
|
||||
$cache->store($data);
|
||||
|
@@ -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,16 @@ class ShowController extends Controller
|
||||
$array['attachments'][] = $item;
|
||||
}
|
||||
|
||||
$subTitle = (string)trans('firefly.overview_for_recurrence', ['title' => $recurrence->title]);
|
||||
if (null !== $array['nr_of_repetitions']) {
|
||||
$left = $array['nr_of_repetitions'] - $array['journal_count'];
|
||||
$left = max(0, $left);
|
||||
// limit each repetition to X occurrences:
|
||||
foreach ($array['repetitions'] as $index => $repetition) {
|
||||
$array['repetitions'][$index]['occurrences'] = array_slice($repetition['occurrences'], 0, $left);
|
||||
}
|
||||
}
|
||||
|
||||
$subTitle = (string)trans('firefly.overview_for_recurrence', ['title' => $recurrence->title]);
|
||||
|
||||
return view('recurring.show', compact('recurrence', 'subTitle', 'array', 'groups', 'today'));
|
||||
}
|
||||
|
@@ -40,24 +40,24 @@ class TriggerController extends Controller
|
||||
{
|
||||
public function trigger(Recurrence $recurrence, TriggerRecurrenceRequest $request): RedirectResponse
|
||||
{
|
||||
$all = $request->getAll();
|
||||
$date = $all['date'];
|
||||
$all = $request->getAll();
|
||||
$date = $all['date'];
|
||||
|
||||
// grab the date from the last time the recurrence fired:
|
||||
$backupDate = $recurrence->latest_date;
|
||||
$backupDate = $recurrence->latest_date;
|
||||
|
||||
// fire the recurring cron job on the given date, then post-date the created transaction.
|
||||
app('log')->info(sprintf('Trigger: will now fire recurring cron job task for date "%s".', $date->format('Y-m-d H:i:s')));
|
||||
|
||||
/** @var CreateRecurringTransactions $job */
|
||||
$job = app(CreateRecurringTransactions::class);
|
||||
$job = app(CreateRecurringTransactions::class);
|
||||
$job->setRecurrences(new Collection([$recurrence]));
|
||||
$job->setDate($date);
|
||||
$job->setForce(false);
|
||||
$job->handle();
|
||||
app('log')->debug('Done with recurrence.');
|
||||
|
||||
$groups = $job->getGroups();
|
||||
$groups = $job->getGroups();
|
||||
|
||||
/** @var TransactionGroup $group */
|
||||
foreach ($groups as $group) {
|
||||
@@ -68,7 +68,8 @@ class TriggerController extends Controller
|
||||
$journal->save();
|
||||
}
|
||||
}
|
||||
$recurrence->latest_date = $backupDate;
|
||||
$recurrence->latest_date = $backupDate;
|
||||
$recurrence->latest_date_tz = $backupDate?->format('e');
|
||||
$recurrence->save();
|
||||
app('preferences')->mark();
|
||||
|
||||
|
@@ -158,6 +158,11 @@ class Installer
|
||||
// version compare thing.
|
||||
$configVersion = (string)config('firefly.version');
|
||||
$dbVersion = (string)app('fireflyconfig')->getFresh('ff3_version', '1.0')->data;
|
||||
if (str_starts_with($configVersion, 'develop')) {
|
||||
Log::debug('Skipping version check for develop version.');
|
||||
|
||||
return false;
|
||||
}
|
||||
if (1 === version_compare($configVersion, $dbVersion)) {
|
||||
app('log')->warning(
|
||||
sprintf(
|
||||
|
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',
|
||||
];
|
||||
}
|
||||
|
||||
|
@@ -42,6 +42,7 @@ use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* Class CreateRecurringTransactions.
|
||||
@@ -88,7 +89,7 @@ class CreateRecurringTransactions implements ShouldQueue
|
||||
$this->recurrences = new Collection();
|
||||
$this->groups = new Collection();
|
||||
|
||||
app('log')->debug(sprintf('Created new CreateRecurringTransactions("%s")', $this->date->format('Y-m-d')));
|
||||
Log::debug(sprintf('Created new CreateRecurringTransactions("%s")', $this->date->format('Y-m-d')));
|
||||
}
|
||||
|
||||
public function getGroups(): Collection
|
||||
@@ -101,25 +102,25 @@ class CreateRecurringTransactions implements ShouldQueue
|
||||
*/
|
||||
public function handle(): void
|
||||
{
|
||||
app('log')->debug(sprintf('Now at start of CreateRecurringTransactions() job for %s.', $this->date->format('D d M Y')));
|
||||
Log::debug(sprintf('Now at start of CreateRecurringTransactions() job for %s.', $this->date->format('D d M Y')));
|
||||
|
||||
// only use recurrences from database if there is no collection submitted.
|
||||
if (0 !== count($this->recurrences)) {
|
||||
app('log')->debug('Using predetermined set of recurrences.');
|
||||
Log::debug('Using predetermined set of recurrences.');
|
||||
}
|
||||
if (0 === count($this->recurrences)) {
|
||||
app('log')->debug('Grab all recurrences from the database.');
|
||||
Log::debug('Grab all recurrences from the database.');
|
||||
$this->recurrences = $this->repository->getAll();
|
||||
}
|
||||
|
||||
$result = [];
|
||||
$count = $this->recurrences->count();
|
||||
$this->submitted = $count;
|
||||
app('log')->debug(sprintf('Count of collection is %d', $count));
|
||||
Log::debug(sprintf('Count of collection is %d', $count));
|
||||
|
||||
// filter recurrences:
|
||||
$filtered = $this->filterRecurrences($this->recurrences);
|
||||
app('log')->debug(sprintf('Left after filtering is %d', $filtered->count()));
|
||||
Log::debug(sprintf('Left after filtering is %d', $filtered->count()));
|
||||
|
||||
/** @var Recurrence $recurrence */
|
||||
foreach ($filtered as $recurrence) {
|
||||
@@ -133,20 +134,20 @@ class CreateRecurringTransactions implements ShouldQueue
|
||||
// clear cache for user
|
||||
app('preferences')->setForUser($recurrence->user, 'lastActivity', microtime());
|
||||
|
||||
app('log')->debug(sprintf('Now at recurrence #%d of user #%d', $recurrence->id, $recurrence->user_id));
|
||||
Log::debug(sprintf('Now at recurrence #%d of user #%d', $recurrence->id, $recurrence->user_id));
|
||||
$createdReps = $this->handleRepetitions($recurrence);
|
||||
app('log')->debug(sprintf('Done with recurrence #%d', $recurrence->id));
|
||||
Log::debug(sprintf('Done with recurrence #%d', $recurrence->id));
|
||||
$result[$recurrence->user_id] = $result[$recurrence->user_id]->merge($createdReps);
|
||||
++$this->executed;
|
||||
}
|
||||
|
||||
app('log')->debug('Now running report thing.');
|
||||
Log::debug('Now running report thing.');
|
||||
// will now send email to users.
|
||||
foreach ($result as $userId => $journals) {
|
||||
event(new RequestedReportOnJournals($userId, $journals));
|
||||
}
|
||||
|
||||
app('log')->debug('Done with handle()');
|
||||
Log::debug('Done with handle()');
|
||||
|
||||
// clear cache:
|
||||
app('preferences')->mark();
|
||||
@@ -166,10 +167,10 @@ class CreateRecurringTransactions implements ShouldQueue
|
||||
*/
|
||||
private function validRecurrence(Recurrence $recurrence): bool
|
||||
{
|
||||
app('log')->debug(sprintf('Now filtering recurrence #%d, owned by user #%d', $recurrence->id, $recurrence->user_id));
|
||||
Log::debug(sprintf('Now filtering recurrence #%d, owned by user #%d', $recurrence->id, $recurrence->user_id));
|
||||
// is not active.
|
||||
if (!$this->active($recurrence)) {
|
||||
app('log')->info(sprintf('Recurrence #%d is not active. Skipped.', $recurrence->id));
|
||||
Log::info(sprintf('Recurrence #%d is not active. Skipped.', $recurrence->id));
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -177,14 +178,15 @@ 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));
|
||||
Log::info(sprintf('Recurrence #%d has run %d times, so will run no longer.', $recurrence->id, $journalCount));
|
||||
|
||||
return false;
|
||||
}
|
||||
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)) {
|
||||
app('log')->info(
|
||||
Log::info(
|
||||
sprintf(
|
||||
'Recurrence #%d was set to run until %s, and today\'s date is %s. Skipped.',
|
||||
$recurrence->id,
|
||||
@@ -198,12 +200,12 @@ class CreateRecurringTransactions implements ShouldQueue
|
||||
|
||||
// first_date is in the future
|
||||
if ($this->hasNotStartedYet($recurrence)) {
|
||||
app('log')->info(
|
||||
Log::info(
|
||||
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')
|
||||
)
|
||||
);
|
||||
|
||||
@@ -212,11 +214,11 @@ class CreateRecurringTransactions implements ShouldQueue
|
||||
|
||||
// already fired today (with success):
|
||||
if (false === $this->force && $this->hasFiredToday($recurrence)) {
|
||||
app('log')->info(sprintf('Recurrence #%d has already fired today. Skipped.', $recurrence->id));
|
||||
Log::info(sprintf('Recurrence #%d has already fired today. Skipped.', $recurrence->id));
|
||||
|
||||
return false;
|
||||
}
|
||||
app('log')->debug('Will be included.');
|
||||
Log::debug('Will be included.');
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -244,7 +246,8 @@ 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')));
|
||||
Log::debug(sprintf('Start date is %s', $startDate->toW3cString()));
|
||||
Log::debug(sprintf('Ask date is %s', $this->date->toW3cString()));
|
||||
|
||||
return $startDate->gt($this->date);
|
||||
}
|
||||
@@ -283,7 +286,7 @@ class CreateRecurringTransactions implements ShouldQueue
|
||||
|
||||
/** @var RecurrenceRepetition $repetition */
|
||||
foreach ($recurrence->recurrenceRepetitions as $repetition) {
|
||||
app('log')->debug(
|
||||
Log::debug(
|
||||
sprintf(
|
||||
'Now repeating %s with value "%s", skips every %d time(s)',
|
||||
$repetition->repetition_type,
|
||||
@@ -338,63 +341,62 @@ class CreateRecurringTransactions implements ShouldQueue
|
||||
if ($date->ne($this->date)) {
|
||||
return null;
|
||||
}
|
||||
app('log')->debug(sprintf('%s IS today (%s)', $date->format('Y-m-d'), $this->date->format('Y-m-d')));
|
||||
Log::debug(sprintf('%s IS today (%s)', $date->format('Y-m-d'), $this->date->format('Y-m-d')));
|
||||
|
||||
// count created journals on THIS day.
|
||||
$journalCount = $this->repository->getJournalCount($recurrence, $date, $date);
|
||||
$journalCount = $this->repository->getJournalCount($recurrence, $date, $date);
|
||||
if ($journalCount > 0 && false === $this->force) {
|
||||
app('log')->info(sprintf('Already created %d journal(s) for date %s', $journalCount, $date->format('Y-m-d')));
|
||||
Log::info(sprintf('Already created %d journal(s) for date %s', $journalCount, $date->format('Y-m-d')));
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($this->repository->createdPreviously($recurrence, $date) && false === $this->force) {
|
||||
app('log')->info('There is a transaction already made for this date, so will not be created now');
|
||||
Log::info('There is a transaction already made for this date, so will not be created now');
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($journalCount > 0 && true === $this->force) {
|
||||
app('log')->warning(sprintf('Already created %d groups for date %s but FORCED to continue.', $journalCount, $date->format('Y-m-d')));
|
||||
Log::warning(sprintf('Already created %d groups for date %s but FORCED to continue.', $journalCount, $date->format('Y-m-d')));
|
||||
}
|
||||
|
||||
// create transaction array and send to factory.
|
||||
$groupTitle = null;
|
||||
$count = $recurrence->recurrenceTransactions->count();
|
||||
$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;
|
||||
}
|
||||
|
||||
if (0 === $count) {
|
||||
app('log')->error('No transactions to be created in this recurrence. Cannot continue.');
|
||||
Log::error('No transactions to be created in this recurrence. Cannot continue.');
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
$array = [
|
||||
$array = [
|
||||
'user' => $recurrence->user_id,
|
||||
'group_title' => $groupTitle,
|
||||
'transactions' => $this->getTransactionData($recurrence, $repetition, $date),
|
||||
];
|
||||
|
||||
/** @var TransactionGroup $group */
|
||||
$group = $this->groupRepository->store($array);
|
||||
$group = $this->groupRepository->store($array);
|
||||
++$this->created;
|
||||
app('log')->info(sprintf('Created new transaction group #%d', $group->id));
|
||||
Log::info(sprintf('Created new transaction group #%d', $group->id));
|
||||
|
||||
// trigger event:
|
||||
event(new StoredTransactionGroup($group, $recurrence->apply_rules, true));
|
||||
$this->groups->push($group);
|
||||
|
||||
// update recurring thing:
|
||||
$recurrence->latest_date = $date;
|
||||
$recurrence->latest_date = $date;
|
||||
$recurrence->latest_date_tz = $date?->format('e');
|
||||
$recurrence->save();
|
||||
|
||||
return $group;
|
||||
@@ -459,6 +461,7 @@ class CreateRecurringTransactions implements ShouldQueue
|
||||
{
|
||||
$newDate = clone $date;
|
||||
$newDate->startOfDay();
|
||||
Log::debug(sprintf('Overruled date to "%s', $newDate->format('Y-m-d H:i:s')));
|
||||
$this->date = $newDate;
|
||||
}
|
||||
|
||||
|
@@ -51,12 +51,13 @@ class Account extends Model
|
||||
|
||||
protected $casts
|
||||
= [
|
||||
'created_at' => 'datetime',
|
||||
'updated_at' => 'datetime',
|
||||
'user_id' => 'integer',
|
||||
'deleted_at' => 'datetime',
|
||||
'active' => 'boolean',
|
||||
'encrypted' => 'boolean',
|
||||
'created_at' => 'datetime',
|
||||
'updated_at' => 'datetime',
|
||||
'user_id' => 'integer',
|
||||
'deleted_at' => 'datetime',
|
||||
'active' => 'boolean',
|
||||
'encrypted' => 'boolean',
|
||||
'virtual_balance' => 'string',
|
||||
];
|
||||
|
||||
protected $fillable = ['user_id', 'user_group_id', 'account_type_id', 'name', 'active', 'virtual_balance', 'iban'];
|
||||
|
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace FireflyIII\Models;
|
||||
|
||||
use FireflyIII\Casts\SeparateTimezoneCaster;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
@@ -14,7 +15,15 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
class AccountBalance extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
protected $fillable = ['account_id', 'title', 'transaction_currency_id', 'balance'];
|
||||
protected $fillable = ['account_id', 'title', 'transaction_currency_id', 'balance', 'date', 'date_tz'];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'date' => SeparateTimezoneCaster::class,
|
||||
'balance' => 'string',
|
||||
];
|
||||
}
|
||||
|
||||
public function account(): BelongsTo
|
||||
{
|
||||
|
@@ -23,6 +23,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace FireflyIII\Models;
|
||||
|
||||
use FireflyIII\Enums\AccountTypeEnum;
|
||||
use FireflyIII\Support\Models\ReturnsIntegerIdTrait;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
@@ -34,19 +35,46 @@ class AccountType extends Model
|
||||
{
|
||||
use ReturnsIntegerIdTrait;
|
||||
|
||||
/** @deprecated */
|
||||
public const string ASSET = 'Asset account';
|
||||
|
||||
/** @deprecated */
|
||||
public const string BENEFICIARY = 'Beneficiary account';
|
||||
|
||||
/** @deprecated */
|
||||
public const string CASH = 'Cash account';
|
||||
|
||||
/** @deprecated */
|
||||
public const string CREDITCARD = 'Credit card';
|
||||
|
||||
/** @deprecated */
|
||||
public const string DEBT = 'Debt';
|
||||
|
||||
/** @deprecated */
|
||||
public const string DEFAULT = 'Default account';
|
||||
|
||||
/** @deprecated */
|
||||
public const string EXPENSE = 'Expense account';
|
||||
|
||||
/** @deprecated */
|
||||
public const string IMPORT = 'Import account';
|
||||
|
||||
/** @deprecated */
|
||||
public const string INITIAL_BALANCE = 'Initial balance account';
|
||||
|
||||
/** @deprecated */
|
||||
public const string LIABILITY_CREDIT = 'Liability credit account';
|
||||
|
||||
/** @deprecated */
|
||||
public const string LOAN = 'Loan';
|
||||
|
||||
/** @deprecated */
|
||||
public const string MORTGAGE = 'Mortgage';
|
||||
|
||||
/** @deprecated */
|
||||
public const string RECONCILIATION = 'Reconciliation account';
|
||||
|
||||
/** @deprecated */
|
||||
public const string REVENUE = 'Revenue account';
|
||||
|
||||
protected $casts
|
||||
@@ -61,4 +89,11 @@ class AccountType extends Model
|
||||
{
|
||||
return $this->hasMany(Account::class);
|
||||
}
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
// 'type' => AccountTypeEnum::class,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@@ -24,6 +24,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace FireflyIII\Models;
|
||||
|
||||
use FireflyIII\Enums\AutoBudgetType;
|
||||
use FireflyIII\Support\Models\ReturnsIntegerIdTrait;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
@@ -38,11 +39,20 @@ class AutoBudget extends Model
|
||||
use ReturnsIntegerIdTrait;
|
||||
use SoftDeletes;
|
||||
|
||||
/** @deprecated */
|
||||
public const int AUTO_BUDGET_ADJUSTED = 3;
|
||||
|
||||
/** @deprecated */
|
||||
public const int AUTO_BUDGET_RESET = 1;
|
||||
|
||||
/** @deprecated */
|
||||
public const int AUTO_BUDGET_ROLLOVER = 2;
|
||||
protected $fillable = ['budget_id', 'amount', 'period'];
|
||||
|
||||
protected $casts = [
|
||||
'amount' => 'string',
|
||||
];
|
||||
|
||||
public function budget(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Budget::class);
|
||||
@@ -53,6 +63,13 @@ class AutoBudget extends Model
|
||||
return $this->belongsTo(TransactionCurrency::class);
|
||||
}
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
// 'auto_budget_type' => AutoBudgetType::class,
|
||||
];
|
||||
}
|
||||
|
||||
protected function amount(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
|
@@ -49,9 +49,10 @@ class AvailableBudget extends Model
|
||||
'start_date' => 'date',
|
||||
'end_date' => 'date',
|
||||
'transaction_currency_id' => 'int',
|
||||
'amount' => 'string',
|
||||
];
|
||||
|
||||
protected $fillable = ['user_id', 'user_group_id', 'transaction_currency_id', 'amount', 'start_date', 'end_date'];
|
||||
protected $fillable = ['user_id', 'user_group_id', 'transaction_currency_id', 'amount', 'start_date', 'end_date', 'start_date_tz', 'end_date_tz'];
|
||||
|
||||
/**
|
||||
* Route binder. Converts the key in the URL to the specified object (or throw 404).
|
||||
|
@@ -23,6 +23,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace FireflyIII\Models;
|
||||
|
||||
use FireflyIII\Casts\SeparateTimezoneCaster;
|
||||
use FireflyIII\Support\Models\ReturnsIntegerIdTrait;
|
||||
use FireflyIII\Support\Models\ReturnsIntegerUserIdTrait;
|
||||
use FireflyIII\User;
|
||||
@@ -49,14 +50,16 @@ class Bill extends Model
|
||||
'created_at' => 'datetime',
|
||||
'updated_at' => 'datetime',
|
||||
'deleted_at' => 'datetime',
|
||||
'date' => 'date',
|
||||
'end_date' => 'date',
|
||||
'extension_date' => 'date',
|
||||
'date' => SeparateTimezoneCaster::class,
|
||||
'end_date' => SeparateTimezoneCaster::class,
|
||||
'extension_date' => SeparateTimezoneCaster::class,
|
||||
'skip' => 'int',
|
||||
'automatch' => 'boolean',
|
||||
'active' => 'boolean',
|
||||
'name_encrypted' => 'boolean',
|
||||
'match_encrypted' => 'boolean',
|
||||
'amount_min' => 'string',
|
||||
'amount_max' => 'string',
|
||||
];
|
||||
|
||||
protected $fillable
|
||||
@@ -68,6 +71,7 @@ class Bill extends Model
|
||||
'user_group_id',
|
||||
'amount_max',
|
||||
'date',
|
||||
'date_tz',
|
||||
'repeat_freq',
|
||||
'skip',
|
||||
'automatch',
|
||||
@@ -75,6 +79,8 @@ class Bill extends Model
|
||||
'transaction_currency_id',
|
||||
'end_date',
|
||||
'extension_date',
|
||||
'end_date_tz',
|
||||
'extension_date_tz',
|
||||
];
|
||||
|
||||
protected $hidden = ['amount_min_encrypted', 'amount_max_encrypted', 'name_encrypted', 'match_encrypted'];
|
||||
|
@@ -23,6 +23,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace FireflyIII\Models;
|
||||
|
||||
use FireflyIII\Casts\SeparateTimezoneCaster;
|
||||
use FireflyIII\Events\Model\BudgetLimit\Created;
|
||||
use FireflyIII\Events\Model\BudgetLimit\Deleted;
|
||||
use FireflyIII\Events\Model\BudgetLimit\Updated;
|
||||
@@ -43,9 +44,10 @@ class BudgetLimit extends Model
|
||||
= [
|
||||
'created_at' => 'datetime',
|
||||
'updated_at' => 'datetime',
|
||||
'start_date' => 'date',
|
||||
'end_date' => 'date',
|
||||
'start_date' => SeparateTimezoneCaster::class,
|
||||
'end_date' => SeparateTimezoneCaster::class,
|
||||
'auto_budget' => 'boolean',
|
||||
'amount' => 'string',
|
||||
];
|
||||
protected $dispatchesEvents
|
||||
= [
|
||||
@@ -54,7 +56,7 @@ class BudgetLimit extends Model
|
||||
'deleted' => Deleted::class,
|
||||
];
|
||||
|
||||
protected $fillable = ['budget_id', 'start_date', 'end_date', 'amount', 'transaction_currency_id'];
|
||||
protected $fillable = ['budget_id', 'start_date', 'end_date', 'start_date_tz', 'end_date_tz', 'amount', 'transaction_currency_id'];
|
||||
|
||||
/**
|
||||
* Route binder. Converts the key in the URL to the specified object (or throw 404).
|
||||
|
@@ -23,6 +23,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace FireflyIII\Models;
|
||||
|
||||
use FireflyIII\Casts\SeparateTimezoneCaster;
|
||||
use FireflyIII\Support\Models\ReturnsIntegerIdTrait;
|
||||
use FireflyIII\Support\Models\ReturnsIntegerUserIdTrait;
|
||||
use FireflyIII\User;
|
||||
@@ -47,9 +48,11 @@ class CurrencyExchangeRate extends Model
|
||||
'user_id' => 'int',
|
||||
'from_currency_id' => 'int',
|
||||
'to_currency_id' => 'int',
|
||||
'date' => 'datetime',
|
||||
'date' => SeparateTimezoneCaster::class,
|
||||
'rate' => 'string',
|
||||
'user_rate' => 'string',
|
||||
];
|
||||
protected $fillable = ['user_id', 'from_currency_id', 'to_currency_id', 'date', 'rate'];
|
||||
protected $fillable = ['user_id', 'from_currency_id', 'to_currency_id', 'date', 'date_tz', 'rate'];
|
||||
|
||||
public function fromCurrency(): BelongsTo
|
||||
{
|
||||
|
@@ -24,6 +24,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace FireflyIII\Models;
|
||||
|
||||
use FireflyIII\Casts\SeparateTimezoneCaster;
|
||||
use FireflyIII\Support\Models\ReturnsIntegerIdTrait;
|
||||
use FireflyIII\Support\Models\ReturnsIntegerUserIdTrait;
|
||||
use FireflyIII\User;
|
||||
@@ -41,10 +42,10 @@ class InvitedUser extends Model
|
||||
|
||||
protected $casts
|
||||
= [
|
||||
'expires' => 'datetime',
|
||||
'expires' => SeparateTimezoneCaster::class,
|
||||
'redeemed' => 'boolean',
|
||||
];
|
||||
protected $fillable = ['user_id', 'email', 'invite_code', 'expires', 'redeemed'];
|
||||
protected $fillable = ['user_id', 'email', 'invite_code', 'expires', 'expires_tz', 'redeemed'];
|
||||
|
||||
/**
|
||||
* Route binder. Converts the key in the URL to the specified object (or throw 404).
|
||||
|
@@ -43,17 +43,18 @@ class PiggyBank extends Model
|
||||
|
||||
protected $casts
|
||||
= [
|
||||
'created_at' => 'datetime',
|
||||
'updated_at' => 'datetime',
|
||||
'deleted_at' => 'datetime',
|
||||
'startdate' => 'date',
|
||||
'targetdate' => 'date',
|
||||
'order' => 'int',
|
||||
'active' => 'boolean',
|
||||
'encrypted' => 'boolean',
|
||||
'created_at' => 'datetime',
|
||||
'updated_at' => 'datetime',
|
||||
'deleted_at' => 'datetime',
|
||||
'startdate' => 'date',
|
||||
'targetdate' => 'date',
|
||||
'order' => 'int',
|
||||
'active' => 'boolean',
|
||||
'encrypted' => 'boolean',
|
||||
'targetamount' => 'string',
|
||||
];
|
||||
|
||||
protected $fillable = ['name', 'account_id', 'order', 'targetamount', 'startdate', 'targetdate', 'active'];
|
||||
protected $fillable = ['name', 'account_id', 'order', 'targetamount', 'startdate', 'startdate_tz', 'targetdate', 'targetdate_tz', 'active'];
|
||||
|
||||
protected $hidden = ['targetamount_encrypted', 'encrypted'];
|
||||
|
||||
|
@@ -23,6 +23,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace FireflyIII\Models;
|
||||
|
||||
use FireflyIII\Casts\SeparateTimezoneCaster;
|
||||
use FireflyIII\Support\Models\ReturnsIntegerIdTrait;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
@@ -39,10 +40,11 @@ class PiggyBankEvent extends Model
|
||||
= [
|
||||
'created_at' => 'datetime',
|
||||
'updated_at' => 'datetime',
|
||||
'date' => 'date',
|
||||
'date' => SeparateTimezoneCaster::class,
|
||||
'amount' => 'string',
|
||||
];
|
||||
|
||||
protected $fillable = ['piggy_bank_id', 'transaction_journal_id', 'date', 'amount'];
|
||||
protected $fillable = ['piggy_bank_id', 'transaction_journal_id', 'date', 'date_tz', 'amount'];
|
||||
|
||||
protected $hidden = ['amount_encrypted'];
|
||||
|
||||
|
@@ -24,6 +24,7 @@ declare(strict_types=1);
|
||||
namespace FireflyIII\Models;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use FireflyIII\Casts\SeparateTimezoneCaster;
|
||||
use FireflyIII\Support\Models\ReturnsIntegerIdTrait;
|
||||
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
@@ -39,13 +40,14 @@ class PiggyBankRepetition extends Model
|
||||
|
||||
protected $casts
|
||||
= [
|
||||
'created_at' => 'datetime',
|
||||
'updated_at' => 'datetime',
|
||||
'startdate' => 'date',
|
||||
'targetdate' => 'date',
|
||||
'created_at' => 'datetime',
|
||||
'updated_at' => 'datetime',
|
||||
'startdate' => SeparateTimezoneCaster::class,
|
||||
'targetdate' => SeparateTimezoneCaster::class,
|
||||
'virtual_balance' => 'string',
|
||||
];
|
||||
|
||||
protected $fillable = ['piggy_bank_id', 'startdate', 'targetdate', 'currentamount'];
|
||||
protected $fillable = ['piggy_bank_id', 'startdate', 'startdate_tz', 'targetdate', 'targetdate_tz', 'currentamount'];
|
||||
|
||||
public function piggyBank(): BelongsTo
|
||||
{
|
||||
|
@@ -23,6 +23,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace FireflyIII\Models;
|
||||
|
||||
use FireflyIII\Casts\SeparateTimezoneCaster;
|
||||
use FireflyIII\Support\Models\ReturnsIntegerIdTrait;
|
||||
use FireflyIII\Support\Models\ReturnsIntegerUserIdTrait;
|
||||
use FireflyIII\User;
|
||||
@@ -51,16 +52,16 @@ class Recurrence extends Model
|
||||
'title' => 'string',
|
||||
'id' => 'int',
|
||||
'description' => 'string',
|
||||
'first_date' => 'date',
|
||||
'repeat_until' => 'date',
|
||||
'latest_date' => 'date',
|
||||
'first_date' => SeparateTimezoneCaster::class,
|
||||
'repeat_until' => SeparateTimezoneCaster::class,
|
||||
'latest_date' => SeparateTimezoneCaster::class,
|
||||
'repetitions' => 'int',
|
||||
'active' => 'bool',
|
||||
'apply_rules' => 'bool',
|
||||
];
|
||||
|
||||
protected $fillable
|
||||
= ['user_id', 'transaction_type_id', 'title', 'description', 'first_date', 'repeat_until', 'latest_date', 'repetitions', 'apply_rules', 'active'];
|
||||
= ['user_id', 'transaction_type_id', 'title', 'description', 'first_date', 'first_date_tz', 'repeat_until', 'repeat_until_tz', 'latest_date', 'latest_date_tz', 'repetitions', 'apply_rules', 'active'];
|
||||
|
||||
/** @var string The table to store the data in */
|
||||
protected $table = 'recurrences';
|
||||
|
@@ -23,6 +23,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace FireflyIII\Models;
|
||||
|
||||
use FireflyIII\Enums\RecurrenceRepetitionWeekend;
|
||||
use FireflyIII\Support\Models\ReturnsIntegerIdTrait;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
@@ -37,9 +38,16 @@ class RecurrenceRepetition extends Model
|
||||
use ReturnsIntegerIdTrait;
|
||||
use SoftDeletes;
|
||||
|
||||
/** @deprecated */
|
||||
public const int WEEKEND_DO_NOTHING = 1;
|
||||
|
||||
/** @deprecated */
|
||||
public const int WEEKEND_SKIP_CREATION = 2;
|
||||
|
||||
/** @deprecated */
|
||||
public const int WEEKEND_TO_FRIDAY = 3;
|
||||
|
||||
/** @deprecated */
|
||||
public const int WEEKEND_TO_MONDAY = 4;
|
||||
|
||||
protected $casts
|
||||
@@ -58,6 +66,13 @@ class RecurrenceRepetition extends Model
|
||||
/** @var string The table to store the data in */
|
||||
protected $table = 'recurrences_repetitions';
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
// 'weekend' => RecurrenceRepetitionWeekend::class,
|
||||
];
|
||||
}
|
||||
|
||||
public function recurrence(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Recurrence::class);
|
||||
|
@@ -23,6 +23,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace FireflyIII\Models;
|
||||
|
||||
use FireflyIII\Casts\SeparateTimezoneCaster;
|
||||
use FireflyIII\Support\Models\ReturnsIntegerIdTrait;
|
||||
use FireflyIII\Support\Models\ReturnsIntegerUserIdTrait;
|
||||
use FireflyIII\User;
|
||||
@@ -47,13 +48,13 @@ class Tag extends Model
|
||||
'created_at' => 'datetime',
|
||||
'updated_at' => 'datetime',
|
||||
'deleted_at' => 'datetime',
|
||||
'date' => 'date',
|
||||
'date' => SeparateTimezoneCaster::class,
|
||||
'zoomLevel' => 'int',
|
||||
'latitude' => 'float',
|
||||
'longitude' => 'float',
|
||||
];
|
||||
|
||||
protected $fillable = ['user_id', 'user_group_id', 'tag', 'date', 'description', 'tagMode'];
|
||||
protected $fillable = ['user_id', 'user_group_id', 'tag', 'date', 'date_tz', 'description', 'tagMode'];
|
||||
|
||||
protected $hidden = ['zoomLevel', 'latitude', 'longitude'];
|
||||
|
||||
|
@@ -54,7 +54,11 @@ class Transaction extends Model
|
||||
'bill_name_encrypted' => 'boolean',
|
||||
'reconciled' => 'boolean',
|
||||
'balance_dirty' => 'boolean',
|
||||
'balance_before' => 'string',
|
||||
'balance_after' => 'string',
|
||||
'date' => 'datetime',
|
||||
'amount' => 'string',
|
||||
'foreign_amount' => 'string',
|
||||
];
|
||||
|
||||
protected $fillable
|
||||
|
@@ -24,6 +24,7 @@ declare(strict_types=1);
|
||||
namespace FireflyIII\Models;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use FireflyIII\Casts\SeparateTimezoneCaster;
|
||||
use FireflyIII\Support\Models\ReturnsIntegerIdTrait;
|
||||
use FireflyIII\Support\Models\ReturnsIntegerUserIdTrait;
|
||||
use FireflyIII\User;
|
||||
@@ -37,6 +38,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;
|
||||
|
||||
/**
|
||||
@@ -54,7 +56,7 @@ class TransactionJournal extends Model
|
||||
'created_at' => 'datetime',
|
||||
'updated_at' => 'datetime',
|
||||
'deleted_at' => 'datetime',
|
||||
'date' => 'datetime',
|
||||
'date' => SeparateTimezoneCaster::class,
|
||||
'interest_date' => 'date',
|
||||
'book_date' => 'date',
|
||||
'process_date' => 'date',
|
||||
@@ -76,6 +78,7 @@ class TransactionJournal extends Model
|
||||
'completed',
|
||||
'order',
|
||||
'date',
|
||||
'date_tz',
|
||||
];
|
||||
|
||||
protected $hidden = ['encrypted'];
|
||||
@@ -88,7 +91,7 @@ class TransactionJournal extends Model
|
||||
public static function routeBinder(string $value): self
|
||||
{
|
||||
if (auth()->check()) {
|
||||
$journalId = (int)$value;
|
||||
$journalId = (int) $value;
|
||||
|
||||
/** @var User $user */
|
||||
$user = auth()->user();
|
||||
@@ -167,12 +170,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
|
||||
@@ -238,14 +245,14 @@ class TransactionJournal extends Model
|
||||
protected function order(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: static fn ($value) => (int)$value,
|
||||
get: static fn ($value) => (int) $value,
|
||||
);
|
||||
}
|
||||
|
||||
protected function transactionTypeId(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: static fn ($value) => (int)$value,
|
||||
get: static fn ($value) => (int) $value,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -23,6 +23,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace FireflyIII\Models;
|
||||
|
||||
use FireflyIII\Enums\TransactionTypeEnum;
|
||||
use FireflyIII\Support\Models\ReturnsIntegerIdTrait;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
@@ -37,12 +38,25 @@ class TransactionType extends Model
|
||||
use ReturnsIntegerIdTrait;
|
||||
use SoftDeletes;
|
||||
|
||||
/** @deprecated */
|
||||
public const string DEPOSIT = 'Deposit';
|
||||
|
||||
/** @deprecated */
|
||||
public const string INVALID = 'Invalid';
|
||||
|
||||
/** @deprecated */
|
||||
public const string LIABILITY_CREDIT = 'Liability credit';
|
||||
|
||||
/** @deprecated */
|
||||
public const string OPENING_BALANCE = 'Opening balance';
|
||||
|
||||
/** @deprecated */
|
||||
public const string RECONCILIATION = 'Reconciliation';
|
||||
|
||||
/** @deprecated */
|
||||
public const string TRANSFER = 'Transfer';
|
||||
|
||||
/** @deprecated */
|
||||
public const string WITHDRAWAL = 'Withdrawal';
|
||||
|
||||
protected $casts
|
||||
@@ -53,6 +67,13 @@ class TransactionType extends Model
|
||||
];
|
||||
protected $fillable = ['type'];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
// 'type' => TransactionTypeEnum::class,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Route binder. Converts the key in the URL to the specified object (or throw 404).
|
||||
*
|
||||
|
@@ -53,6 +53,15 @@ class Webhook extends Model
|
||||
];
|
||||
protected $fillable = ['active', 'trigger', 'response', 'delivery', 'user_id', 'user_group_id', 'url', 'title', 'secret'];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
// 'delivery' => WebhookDelivery::class,
|
||||
// 'response' => WebhookResponse::class,
|
||||
// 'trigger' => WebhookTrigger::class,
|
||||
];
|
||||
}
|
||||
|
||||
public static function getDeliveries(): array
|
||||
{
|
||||
$array = [];
|
||||
@@ -130,7 +139,7 @@ class Webhook extends Model
|
||||
public static function routeBinder(string $value): self
|
||||
{
|
||||
if (auth()->check()) {
|
||||
$webhookId = (int)$value;
|
||||
$webhookId = (int) $value;
|
||||
|
||||
/** @var User $user */
|
||||
$user = auth()->user();
|
||||
|
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(
|
||||
[
|
||||
@@ -435,15 +437,17 @@ class BillRepository implements BillRepositoryInterface
|
||||
}
|
||||
// find the most recent date for this bill NOT in the future. Cache this date:
|
||||
$start = clone $bill->date;
|
||||
$start->startOfDay();
|
||||
app('log')->debug('nextExpectedMatch: Start is '.$start->format('Y-m-d'));
|
||||
|
||||
while ($start < $date) {
|
||||
app('log')->debug(sprintf('$start (%s) < $date (%s)', $start->format('Y-m-d'), $date->format('Y-m-d')));
|
||||
app('log')->debug(sprintf('$start (%s) < $date (%s)', $start->format('Y-m-d H:i:s'), $date->format('Y-m-d H:i:s')));
|
||||
$start = app('navigation')->addPeriod($start, $bill->repeat_freq, $bill->skip);
|
||||
app('log')->debug('Start is now '.$start->format('Y-m-d'));
|
||||
app('log')->debug('Start is now '.$start->format('Y-m-d H:i:s'));
|
||||
}
|
||||
|
||||
$end = app('navigation')->addPeriod($start, $bill->repeat_freq, $bill->skip);
|
||||
$end->endOfDay();
|
||||
|
||||
// see if the bill was paid in this period.
|
||||
$journalCount = $bill->transactionJournals()->before($end)->after($start)->count();
|
||||
@@ -485,7 +489,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
|
||||
|
@@ -198,11 +198,13 @@ class AvailableBudgetRepository implements AvailableBudgetRepositoryInterface
|
||||
->where('end_date', $end->format('Y-m-d'))->first()
|
||||
;
|
||||
if (null === $availableBudget) {
|
||||
$availableBudget = new AvailableBudget();
|
||||
$availableBudget = new AvailableBudget();
|
||||
$availableBudget->user()->associate($this->user);
|
||||
$availableBudget->transactionCurrency()->associate($currency);
|
||||
$availableBudget->start_date = $start->startOfDay()->format('Y-m-d'); // @phpstan-ignore-line
|
||||
$availableBudget->end_date = $end->endOfDay()->format('Y-m-d'); // @phpstan-ignore-line
|
||||
$availableBudget->start_date = $start->startOfDay()->format('Y-m-d'); // @phpstan-ignore-line
|
||||
$availableBudget->start_date_tz = $start->format('e');
|
||||
$availableBudget->end_date = $end->endOfDay()->format('Y-m-d'); // @phpstan-ignore-line
|
||||
$availableBudget->end_date_tz = $end->format('e');
|
||||
}
|
||||
$availableBudget->amount = $amount;
|
||||
$availableBudget->save();
|
||||
@@ -235,7 +237,9 @@ class AvailableBudgetRepository implements AvailableBudgetRepositoryInterface
|
||||
'transaction_currency_id' => $data['currency_id'],
|
||||
'amount' => $data['amount'],
|
||||
'start_date' => $start->format('Y-m-d'),
|
||||
'start_date_tz' => $start->format('e'),
|
||||
'end_date' => $end->format('Y-m-d'),
|
||||
'end_date_tz' => $end->format('e'),
|
||||
]
|
||||
);
|
||||
}
|
||||
@@ -255,8 +259,9 @@ class AvailableBudgetRepository implements AvailableBudgetRepositoryInterface
|
||||
if (array_key_exists('start', $data)) {
|
||||
$start = $data['start'];
|
||||
if ($start instanceof Carbon) {
|
||||
$start = $data['start']->startOfDay();
|
||||
$availableBudget->start_date = $start->format('Y-m-d');
|
||||
$start = $data['start']->startOfDay();
|
||||
$availableBudget->start_date = $start->format('Y-m-d');
|
||||
$availableBudget->start_date_tz = $start->format('e');
|
||||
$availableBudget->save();
|
||||
}
|
||||
}
|
||||
@@ -264,8 +269,9 @@ class AvailableBudgetRepository implements AvailableBudgetRepositoryInterface
|
||||
if (array_key_exists('end', $data)) {
|
||||
$end = $data['end'];
|
||||
if ($end instanceof Carbon) {
|
||||
$end = $data['end']->endOfDay();
|
||||
$availableBudget->end_date = $end->format('Y-m-d');
|
||||
$end = $data['end']->endOfDay();
|
||||
$availableBudget->end_date = $end->format('Y-m-d');
|
||||
$availableBudget->end_date_tz = $end->format('e');
|
||||
$availableBudget->save();
|
||||
}
|
||||
}
|
||||
|
@@ -277,7 +277,7 @@ class BudgetLimitRepository implements BudgetLimitRepositoryInterface
|
||||
$currency->save();
|
||||
|
||||
// find the budget:
|
||||
$budget = $this->user->budgets()->find((int)$data['budget_id']);
|
||||
$budget = $this->user->budgets()->find((int) $data['budget_id']);
|
||||
if (null === $budget) {
|
||||
throw new FireflyException('200004: Budget does not exist.');
|
||||
}
|
||||
@@ -323,8 +323,15 @@ class BudgetLimitRepository implements BudgetLimitRepositoryInterface
|
||||
{
|
||||
$budgetLimit->amount = array_key_exists('amount', $data) ? $data['amount'] : $budgetLimit->amount;
|
||||
$budgetLimit->budget_id = array_key_exists('budget_id', $data) ? $data['budget_id'] : $budgetLimit->budget_id;
|
||||
$budgetLimit->start_date = array_key_exists('start', $data) ? $data['start']->format('Y-m-d 00:00:00') : $budgetLimit->start_date;
|
||||
$budgetLimit->end_date = array_key_exists('end', $data) ? $data['end']->format('Y-m-d 23:59:59') : $budgetLimit->end_date;
|
||||
|
||||
if (array_key_exists('start', $data)) {
|
||||
$budgetLimit->start_date = $data['start']->startOfDay();
|
||||
$budgetLimit->start_date_tz = $data['start']->format('e');
|
||||
}
|
||||
if (array_key_exists('end', $data)) {
|
||||
$budgetLimit->end_date = $data['end']->endOfDay();
|
||||
$budgetLimit->end_date_tz = $data['end']->format('e');
|
||||
}
|
||||
|
||||
// if no currency has been provided, use the user's default currency:
|
||||
$currency = null;
|
||||
@@ -351,7 +358,7 @@ class BudgetLimitRepository implements BudgetLimitRepositoryInterface
|
||||
public function updateLimitAmount(Budget $budget, Carbon $start, Carbon $end, string $amount): ?BudgetLimit
|
||||
{
|
||||
// count the limits:
|
||||
$limits = $budget->budgetlimits()
|
||||
$limits = $budget->budgetlimits()
|
||||
->where('budget_limits.start_date', $start->format('Y-m-d 00:00:00'))
|
||||
->where('budget_limits.end_date', $end->format('Y-m-d 00:00:00'))
|
||||
->count('budget_limits.*')
|
||||
@@ -360,7 +367,7 @@ class BudgetLimitRepository implements BudgetLimitRepositoryInterface
|
||||
|
||||
// there might be a budget limit for these dates:
|
||||
/** @var null|BudgetLimit $limit */
|
||||
$limit = $budget->budgetlimits()
|
||||
$limit = $budget->budgetlimits()
|
||||
->where('budget_limits.start_date', $start->format('Y-m-d 00:00:00'))
|
||||
->where('budget_limits.end_date', $end->format('Y-m-d 00:00:00'))
|
||||
->first(['budget_limits.*'])
|
||||
@@ -395,11 +402,13 @@ class BudgetLimitRepository implements BudgetLimitRepositoryInterface
|
||||
}
|
||||
app('log')->debug('No existing budget limit, create a new one');
|
||||
// or create one and return it.
|
||||
$limit = new BudgetLimit();
|
||||
$limit = new BudgetLimit();
|
||||
$limit->budget()->associate($budget);
|
||||
$limit->start_date = $start->startOfDay();
|
||||
$limit->end_date = $end->startOfDay();
|
||||
$limit->amount = $amount;
|
||||
$limit->start_date = $start->startOfDay();
|
||||
$limit->start_date_tz = $start->format('e');
|
||||
$limit->end_date = $end->startOfDay();
|
||||
$limit->end_date_tz = $end->format('e');
|
||||
$limit->amount = $amount;
|
||||
$limit->save();
|
||||
app('log')->debug(sprintf('Created new budget limit with ID #%d and amount %s', $limit->id, $amount));
|
||||
|
||||
|
@@ -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();
|
||||
|
@@ -89,11 +89,12 @@ class CurrencyRepository implements CurrencyRepositoryInterface
|
||||
{
|
||||
return CurrencyExchangeRate::create(
|
||||
[
|
||||
'user_id' => $this->user->id,
|
||||
'from_currency_id' => $fromCurrency->id,
|
||||
'to_currency_id' => $toCurrency->id,
|
||||
'date' => $date,
|
||||
'rate' => $rate,
|
||||
'user_id' => $this->user->id,
|
||||
'from_currency_id' => $fromCurrency->id,
|
||||
'to_currency_id' => $toCurrency->id,
|
||||
'date' => $date,
|
||||
'date_tz' => $date->format('e'),
|
||||
'rate' => $rate,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user