mirror of
https://github.com/firefly-iii/firefly-iii.git
synced 2025-08-21 12:54:32 +00:00
Compare commits
141 Commits
develop-20
...
develop-20
Author | SHA1 | Date | |
---|---|---|---|
|
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 | ||
|
e7526ac5e3 | ||
|
441ada70b8 | ||
|
dedc06a46b | ||
|
b0adf1b277 | ||
|
28f65e9f44 | ||
|
a013af5f0d | ||
|
9552701662 | ||
|
ef52f0aad1 | ||
|
0b6f04905a | ||
|
cdb36357d4 | ||
|
8938622bd9 | ||
|
b210294aa9 | ||
|
5b02f20775 | ||
|
fac382a5df | ||
|
88d88bebc9 | ||
|
755fb9c29b | ||
|
51a835ab51 | ||
|
c9895ab182 | ||
|
e71d46a4e5 | ||
|
8d1d5f37c1 | ||
|
525a68682d | ||
|
715648d0d8 | ||
|
9452e93f22 | ||
|
a6aa145471 | ||
|
25aa6dcb59 | ||
|
bb2270b274 | ||
|
d7f6b4143e | ||
|
0cf0e26fa8 | ||
|
cc23197d60 | ||
|
bc1721d95e | ||
|
0d19173da6 | ||
|
1983f07d3c | ||
|
aad1b91cc2 | ||
|
8cb1057a33 | ||
|
b178032985 | ||
|
561213e95d | ||
|
44fa7c4306 | ||
|
e2169563e2 | ||
|
845344e003 | ||
|
cdb48453e8 | ||
|
9669cef518 | ||
|
f962f71ed7 | ||
|
94ed4021fb | ||
|
1765855c57 | ||
|
55cf3e7d44 | ||
|
9f1840dc05 | ||
|
78dab2e5f9 | ||
|
103b9d5005 | ||
|
1b75b778d8 | ||
|
edf2030251 |
214
.ci/php-cs-fixer/composer.lock
generated
214
.ci/php-cs-fixer/composer.lock
generated
@@ -151,24 +151,24 @@
|
||||
},
|
||||
{
|
||||
"name": "composer/semver",
|
||||
"version": "3.4.2",
|
||||
"version": "3.4.3",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/composer/semver.git",
|
||||
"reference": "c51258e759afdb17f1fd1fe83bc12baaef6309d6"
|
||||
"reference": "4313d26ada5e0c4edfbd1dc481a92ff7bff91f12"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/composer/semver/zipball/c51258e759afdb17f1fd1fe83bc12baaef6309d6",
|
||||
"reference": "c51258e759afdb17f1fd1fe83bc12baaef6309d6",
|
||||
"url": "https://api.github.com/repos/composer/semver/zipball/4313d26ada5e0c4edfbd1dc481a92ff7bff91f12",
|
||||
"reference": "4313d26ada5e0c4edfbd1dc481a92ff7bff91f12",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^5.3.2 || ^7.0 || ^8.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpstan/phpstan": "^1.4",
|
||||
"symfony/phpunit-bridge": "^4.2 || ^5"
|
||||
"phpstan/phpstan": "^1.11",
|
||||
"symfony/phpunit-bridge": "^3 || ^7"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
@@ -212,7 +212,7 @@
|
||||
"support": {
|
||||
"irc": "ircs://irc.libera.chat:6697/composer",
|
||||
"issues": "https://github.com/composer/semver/issues",
|
||||
"source": "https://github.com/composer/semver/tree/3.4.2"
|
||||
"source": "https://github.com/composer/semver/tree/3.4.3"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -228,7 +228,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-07-12T11:35:52+00:00"
|
||||
"time": "2024-09-19T14:15:21+00:00"
|
||||
},
|
||||
{
|
||||
"name": "composer/xdebug-handler",
|
||||
@@ -612,16 +612,16 @@
|
||||
},
|
||||
{
|
||||
"name": "psr/log",
|
||||
"version": "3.0.1",
|
||||
"version": "3.0.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/php-fig/log.git",
|
||||
"reference": "79dff0b268932c640297f5208d6298f71855c03e"
|
||||
"reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/php-fig/log/zipball/79dff0b268932c640297f5208d6298f71855c03e",
|
||||
"reference": "79dff0b268932c640297f5208d6298f71855c03e",
|
||||
"url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3",
|
||||
"reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -656,9 +656,9 @@
|
||||
"psr-3"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/php-fig/log/tree/3.0.1"
|
||||
"source": "https://github.com/php-fig/log/tree/3.0.2"
|
||||
},
|
||||
"time": "2024-08-21T13:31:24+00:00"
|
||||
"time": "2024-09-11T13:17:53+00:00"
|
||||
},
|
||||
{
|
||||
"name": "react/cache",
|
||||
@@ -1259,16 +1259,16 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/console",
|
||||
"version": "v7.1.4",
|
||||
"version": "v7.1.7",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/console.git",
|
||||
"reference": "1eed7af6961d763e7832e874d7f9b21c3ea9c111"
|
||||
"reference": "3284aafcac338b6e86fd955ee4d794cbe434151a"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/console/zipball/1eed7af6961d763e7832e874d7f9b21c3ea9c111",
|
||||
"reference": "1eed7af6961d763e7832e874d7f9b21c3ea9c111",
|
||||
"url": "https://api.github.com/repos/symfony/console/zipball/3284aafcac338b6e86fd955ee4d794cbe434151a",
|
||||
"reference": "3284aafcac338b6e86fd955ee4d794cbe434151a",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -1332,7 +1332,7 @@
|
||||
"terminal"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/console/tree/v7.1.4"
|
||||
"source": "https://github.com/symfony/console/tree/v7.1.7"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -1348,7 +1348,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-08-15T22:48:53+00:00"
|
||||
"time": "2024-11-05T15:34:55+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/deprecation-contracts",
|
||||
@@ -1419,16 +1419,16 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/event-dispatcher",
|
||||
"version": "v7.1.1",
|
||||
"version": "v7.1.6",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/event-dispatcher.git",
|
||||
"reference": "9fa7f7a21beb22a39a8f3f28618b29e50d7a55a7"
|
||||
"reference": "87254c78dd50721cfd015b62277a8281c5589702"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/9fa7f7a21beb22a39a8f3f28618b29e50d7a55a7",
|
||||
"reference": "9fa7f7a21beb22a39a8f3f28618b29e50d7a55a7",
|
||||
"url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/87254c78dd50721cfd015b62277a8281c5589702",
|
||||
"reference": "87254c78dd50721cfd015b62277a8281c5589702",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -1479,7 +1479,7 @@
|
||||
"description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/event-dispatcher/tree/v7.1.1"
|
||||
"source": "https://github.com/symfony/event-dispatcher/tree/v7.1.6"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -1495,7 +1495,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-05-31T14:57:53+00:00"
|
||||
"time": "2024-09-25T14:20:29+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/event-dispatcher-contracts",
|
||||
@@ -1575,16 +1575,16 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/filesystem",
|
||||
"version": "v7.1.2",
|
||||
"version": "v7.1.6",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/filesystem.git",
|
||||
"reference": "92a91985250c251de9b947a14bb2c9390b1a562c"
|
||||
"reference": "c835867b3c62bb05c7fe3d637c871c7ae52024d4"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/filesystem/zipball/92a91985250c251de9b947a14bb2c9390b1a562c",
|
||||
"reference": "92a91985250c251de9b947a14bb2c9390b1a562c",
|
||||
"url": "https://api.github.com/repos/symfony/filesystem/zipball/c835867b3c62bb05c7fe3d637c871c7ae52024d4",
|
||||
"reference": "c835867b3c62bb05c7fe3d637c871c7ae52024d4",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -1621,7 +1621,7 @@
|
||||
"description": "Provides basic utilities for the filesystem",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/filesystem/tree/v7.1.2"
|
||||
"source": "https://github.com/symfony/filesystem/tree/v7.1.6"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -1637,20 +1637,20 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-06-28T10:03:55+00:00"
|
||||
"time": "2024-10-25T15:11:02+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/finder",
|
||||
"version": "v7.1.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,24 +1768,24 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-05-31T14:57:53+00:00"
|
||||
"time": "2024-09-25T14:20:29+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-ctype",
|
||||
"version": "v1.30.0",
|
||||
"version": "v1.31.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-ctype.git",
|
||||
"reference": "0424dff1c58f028c451efff2045f5d92410bd540"
|
||||
"reference": "a3cc8b044a6ea513310cbd48ef7333b384945638"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/0424dff1c58f028c451efff2045f5d92410bd540",
|
||||
"reference": "0424dff1c58f028c451efff2045f5d92410bd540",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638",
|
||||
"reference": "a3cc8b044a6ea513310cbd48ef7333b384945638",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.1"
|
||||
"php": ">=7.2"
|
||||
},
|
||||
"provide": {
|
||||
"ext-ctype": "*"
|
||||
@@ -1831,7 +1831,7 @@
|
||||
"portable"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.30.0"
|
||||
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.31.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -1847,24 +1847,24 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-05-31T15:07:36+00:00"
|
||||
"time": "2024-09-09T11:45:10+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-intl-grapheme",
|
||||
"version": "v1.30.0",
|
||||
"version": "v1.31.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-intl-grapheme.git",
|
||||
"reference": "64647a7c30b2283f5d49b874d84a18fc22054b7a"
|
||||
"reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/64647a7c30b2283f5d49b874d84a18fc22054b7a",
|
||||
"reference": "64647a7c30b2283f5d49b874d84a18fc22054b7a",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe",
|
||||
"reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.1"
|
||||
"php": ">=7.2"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-intl": "For best performance"
|
||||
@@ -1909,7 +1909,7 @@
|
||||
"shim"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.30.0"
|
||||
"source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.31.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -1925,24 +1925,24 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-05-31T15:07:36+00:00"
|
||||
"time": "2024-09-09T11:45:10+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-intl-normalizer",
|
||||
"version": "v1.30.0",
|
||||
"version": "v1.31.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-intl-normalizer.git",
|
||||
"reference": "a95281b0be0d9ab48050ebd988b967875cdb9fdb"
|
||||
"reference": "3833d7255cc303546435cb650316bff708a1c75c"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/a95281b0be0d9ab48050ebd988b967875cdb9fdb",
|
||||
"reference": "a95281b0be0d9ab48050ebd988b967875cdb9fdb",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c",
|
||||
"reference": "3833d7255cc303546435cb650316bff708a1c75c",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.1"
|
||||
"php": ">=7.2"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-intl": "For best performance"
|
||||
@@ -1990,7 +1990,7 @@
|
||||
"shim"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.30.0"
|
||||
"source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.31.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -2006,24 +2006,24 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-05-31T15:07:36+00:00"
|
||||
"time": "2024-09-09T11:45:10+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-mbstring",
|
||||
"version": "v1.30.0",
|
||||
"version": "v1.31.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-mbstring.git",
|
||||
"reference": "fd22ab50000ef01661e2a31d850ebaa297f8e03c"
|
||||
"reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/fd22ab50000ef01661e2a31d850ebaa297f8e03c",
|
||||
"reference": "fd22ab50000ef01661e2a31d850ebaa297f8e03c",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/85181ba99b2345b0ef10ce42ecac37612d9fd341",
|
||||
"reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.1"
|
||||
"php": ">=7.2"
|
||||
},
|
||||
"provide": {
|
||||
"ext-mbstring": "*"
|
||||
@@ -2070,7 +2070,7 @@
|
||||
"shim"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.30.0"
|
||||
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.31.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -2086,24 +2086,24 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-06-19T12:30:46+00:00"
|
||||
"time": "2024-09-09T11:45:10+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-php80",
|
||||
"version": "v1.30.0",
|
||||
"version": "v1.31.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-php80.git",
|
||||
"reference": "77fa7995ac1b21ab60769b7323d600a991a90433"
|
||||
"reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/77fa7995ac1b21ab60769b7323d600a991a90433",
|
||||
"reference": "77fa7995ac1b21ab60769b7323d600a991a90433",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/60328e362d4c2c802a54fcbf04f9d3fb892b4cf8",
|
||||
"reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.1"
|
||||
"php": ">=7.2"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
@@ -2150,7 +2150,7 @@
|
||||
"shim"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-php80/tree/v1.30.0"
|
||||
"source": "https://github.com/symfony/polyfill-php80/tree/v1.31.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -2166,24 +2166,24 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-05-31T15:07:36+00:00"
|
||||
"time": "2024-09-09T11:45:10+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-php81",
|
||||
"version": "v1.30.0",
|
||||
"version": "v1.31.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-php81.git",
|
||||
"reference": "3fb075789fb91f9ad9af537c4012d523085bd5af"
|
||||
"reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/3fb075789fb91f9ad9af537c4012d523085bd5af",
|
||||
"reference": "3fb075789fb91f9ad9af537c4012d523085bd5af",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c",
|
||||
"reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.1"
|
||||
"php": ">=7.2"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
@@ -2226,7 +2226,7 @@
|
||||
"shim"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-php81/tree/v1.30.0"
|
||||
"source": "https://github.com/symfony/polyfill-php81/tree/v1.31.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -2242,20 +2242,20 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-06-19T12:30:46+00:00"
|
||||
"time": "2024-09-09T11:45:10+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/process",
|
||||
"version": "v7.1.3",
|
||||
"version": "v7.1.7",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/process.git",
|
||||
"reference": "7f2f542c668ad6c313dc4a5e9c3321f733197eca"
|
||||
"reference": "9b8a40b7289767aa7117e957573c2a535efe6585"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/process/zipball/7f2f542c668ad6c313dc4a5e9c3321f733197eca",
|
||||
"reference": "7f2f542c668ad6c313dc4a5e9c3321f733197eca",
|
||||
"url": "https://api.github.com/repos/symfony/process/zipball/9b8a40b7289767aa7117e957573c2a535efe6585",
|
||||
"reference": "9b8a40b7289767aa7117e957573c2a535efe6585",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -2287,7 +2287,7 @@
|
||||
"description": "Executes commands in sub-processes",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/process/tree/v7.1.3"
|
||||
"source": "https://github.com/symfony/process/tree/v7.1.7"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -2303,7 +2303,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-07-26T12:44:47+00:00"
|
||||
"time": "2024-11-06T09:25:12+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.4",
|
||||
"version": "v7.1.6",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/string.git",
|
||||
"reference": "6cd670a6d968eaeb1c77c2e76091c45c56bc367b"
|
||||
"reference": "61b72d66bf96c360a727ae6232df5ac83c71f626"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/string/zipball/6cd670a6d968eaeb1c77c2e76091c45c56bc367b",
|
||||
"reference": "6cd670a6d968eaeb1c77c2e76091c45c56bc367b",
|
||||
"url": "https://api.github.com/repos/symfony/string/zipball/61b72d66bf96c360a727ae6232df5ac83c71f626",
|
||||
"reference": "61b72d66bf96c360a727ae6232df5ac83c71f626",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -2519,7 +2519,7 @@
|
||||
"utf8"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/string/tree/v7.1.4"
|
||||
"source": "https://github.com/symfony/string/tree/v7.1.6"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -2535,16 +2535,16 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-08-12T09:59:40+00:00"
|
||||
"time": "2024-09-25T14:20:29+00:00"
|
||||
}
|
||||
],
|
||||
"packages-dev": [],
|
||||
"aliases": [],
|
||||
"minimum-stability": "stable",
|
||||
"stability-flags": [],
|
||||
"stability-flags": {},
|
||||
"prefer-stable": false,
|
||||
"prefer-lowest": false,
|
||||
"platform": [],
|
||||
"platform-dev": [],
|
||||
"platform": {},
|
||||
"platform-dev": {},
|
||||
"plugin-api-version": "2.6.0"
|
||||
}
|
||||
|
@@ -176,6 +176,7 @@ MAILGUN_ENDPOINT=api.mailgun.net
|
||||
# If you use Docker or similar, you can set these variables from a file by appending them with _FILE
|
||||
MANDRILL_SECRET=
|
||||
SPARKPOST_SECRET=
|
||||
MAILERSEND_API_KEY=
|
||||
|
||||
# Firefly III can send you the following messages.
|
||||
SEND_ERROR_MESSAGE=true
|
||||
@@ -312,6 +313,12 @@ PUSHER_ID=
|
||||
DEMO_USERNAME=
|
||||
DEMO_PASSWORD=
|
||||
|
||||
#
|
||||
# Disable or enable the running balance column data
|
||||
# Please disable this. It's a very experimental feature.
|
||||
#
|
||||
USE_RUNNING_BALANCE=false
|
||||
|
||||
#
|
||||
# The v2 layout is very experimental. If it breaks you get to keep both parts.
|
||||
# Be wary of data loss.
|
||||
|
2
.github/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"
|
||||
|
3
.github/workflows/stale.yml
vendored
3
.github/workflows/stale.yml
vendored
@@ -35,4 +35,5 @@ jobs:
|
||||
Thank you for your contributions.
|
||||
days-before-stale: 14
|
||||
days-before-close: 7
|
||||
exempt-issue-labels: 'enhancement,feature,bug,announcement,epic,triage'
|
||||
exempt-all-milestones: true
|
||||
exempt-issue-labels: 'triage'
|
||||
|
@@ -4,6 +4,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
|
||||
- Jhon Pedroza
|
||||
- mzhubail
|
||||
- tasnim
|
||||
- withbest
|
||||
- Steve Wasiura
|
||||
|
@@ -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']),
|
||||
|
@@ -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 Illuminate\Console\Command;
|
||||
use Illuminate\Database\QueryException;
|
||||
use Illuminate\Support\Collection;
|
||||
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.';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle(): void
|
||||
{
|
||||
$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'],
|
||||
];
|
||||
foreach ($models as $model => $fields) {
|
||||
$this->addTimezoneToModel($model, $fields);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
$items = new Collection();
|
||||
|
||||
try {
|
||||
$items = $model::whereNull($timezoneField)->get();
|
||||
} 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 === $items->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));
|
||||
|
||||
foreach ($items as $item) {
|
||||
$item->{$timezoneField} = config('app.timezone');
|
||||
$item->saveQuietly();
|
||||
}
|
||||
}
|
||||
}
|
@@ -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',
|
||||
|
@@ -46,10 +46,15 @@ class Cron extends Command
|
||||
protected $signature = 'firefly-iii:cron
|
||||
{--F|force : Force the cron job(s) to execute.}
|
||||
{--date= : Set the date in YYYY-MM-DD to make Firefly III think that\'s the current date.}
|
||||
{--download-cer : Download exchange rates. Other tasks will be skipped unless also requested.}
|
||||
{--create-recurring : Create recurring transactions. Other tasks will be skipped unless also requested.}
|
||||
{--create-auto-budgets : Create auto budgets. Other tasks will be skipped unless also requested.}
|
||||
{--send-bill-warnings : Send bill warnings. Other tasks will be skipped unless also requested.}
|
||||
';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$doAll = !$this->option('download-cer') && !$this->option('create-recurring') && !$this->option('create-auto-budgets') && !$this->option('send-bill-warnings');
|
||||
$date = null;
|
||||
|
||||
try {
|
||||
@@ -60,7 +65,7 @@ class Cron extends Command
|
||||
$force = (bool)$this->option('force'); // @phpstan-ignore-line
|
||||
|
||||
// Fire exchange rates cron job.
|
||||
if (true === config('cer.download_enabled')) {
|
||||
if (true === config('cer.download_enabled') && ($doAll || $this->option('download-cer'))) {
|
||||
try {
|
||||
$this->exchangeRatesCronJob($force, $date);
|
||||
} catch (FireflyException $e) {
|
||||
@@ -71,30 +76,36 @@ class Cron extends Command
|
||||
}
|
||||
|
||||
// Fire recurring transaction cron job.
|
||||
try {
|
||||
$this->recurringCronJob($force, $date);
|
||||
} catch (FireflyException $e) {
|
||||
app('log')->error($e->getMessage());
|
||||
app('log')->error($e->getTraceAsString());
|
||||
$this->friendlyError($e->getMessage());
|
||||
if ($doAll || $this->option('create-recurring')) {
|
||||
try {
|
||||
$this->recurringCronJob($force, $date);
|
||||
} catch (FireflyException $e) {
|
||||
app('log')->error($e->getMessage());
|
||||
app('log')->error($e->getTraceAsString());
|
||||
$this->friendlyError($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// Fire auto-budget cron job:
|
||||
try {
|
||||
$this->autoBudgetCronJob($force, $date);
|
||||
} catch (FireflyException $e) {
|
||||
app('log')->error($e->getMessage());
|
||||
app('log')->error($e->getTraceAsString());
|
||||
$this->friendlyError($e->getMessage());
|
||||
if ($doAll || $this->option('create-auto-budgets')) {
|
||||
try {
|
||||
$this->autoBudgetCronJob($force, $date);
|
||||
} catch (FireflyException $e) {
|
||||
app('log')->error($e->getMessage());
|
||||
app('log')->error($e->getTraceAsString());
|
||||
$this->friendlyError($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// Fire bill warning cron job
|
||||
try {
|
||||
$this->billWarningCronJob($force, $date);
|
||||
} catch (FireflyException $e) {
|
||||
app('log')->error($e->getMessage());
|
||||
app('log')->error($e->getTraceAsString());
|
||||
$this->friendlyError($e->getMessage());
|
||||
if ($doAll || $this->option('send-bill-warnings')) {
|
||||
try {
|
||||
$this->billWarningCronJob($force, $date);
|
||||
} catch (FireflyException $e) {
|
||||
app('log')->error($e->getMessage());
|
||||
app('log')->error($e->getTraceAsString());
|
||||
$this->friendlyError($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
$this->friendlyInfo('More feedback on the cron jobs can be found in the log files.');
|
||||
|
@@ -33,6 +33,7 @@ use Illuminate\Console\Command;
|
||||
class CorrectAccountBalance extends Command
|
||||
{
|
||||
use ShowsFriendlyMessages;
|
||||
|
||||
public const string CONFIG_NAME = '610_correct_balances';
|
||||
protected $description = 'Recalculate all account balance amounts';
|
||||
protected $signature = 'firefly-iii:correct-account-balance {--F|force : Force the execution of this command.}';
|
||||
@@ -44,23 +45,30 @@ class CorrectAccountBalance extends Command
|
||||
|
||||
return 0;
|
||||
}
|
||||
$this->friendlyWarning('This command has been disabled.');
|
||||
$this->markAsExecuted();
|
||||
if (config('firefly.feature_flags.running_balance_column')) {
|
||||
$this->friendlyInfo('Will recalculate account balances. This may take a LONG time. Please be patient.');
|
||||
$this->markAsExecuted();
|
||||
$this->correctBalanceAmounts();
|
||||
$this->friendlyInfo('Done recalculating account balances.');
|
||||
|
||||
return 0;
|
||||
}
|
||||
$this->friendlyWarning('This command has been disabled.');
|
||||
|
||||
// $this->correctBalanceAmounts();
|
||||
return 0;
|
||||
}
|
||||
|
||||
private function correctBalanceAmounts(): void
|
||||
{
|
||||
AccountBalanceCalculator::recalculateAll();
|
||||
return;
|
||||
AccountBalanceCalculator::recalculateAll(true);
|
||||
}
|
||||
|
||||
private function isExecuted(): bool
|
||||
{
|
||||
$configVar = app('fireflyconfig')->get(self::CONFIG_NAME, false);
|
||||
|
||||
return (bool)$configVar?->data;
|
||||
return (bool) $configVar?->data;
|
||||
}
|
||||
|
||||
private function markAsExecuted(): void
|
||||
|
@@ -54,6 +54,7 @@ class MigrateRuleActions extends Command
|
||||
}
|
||||
$this->replaceEqualSign();
|
||||
$this->replaceObsoleteActions();
|
||||
$this->markAsExecuted();
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -179,4 +180,9 @@ class MigrateRuleActions extends Command
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function markAsExecuted(): void
|
||||
{
|
||||
app('fireflyconfig')->set(self::CONFIG_NAME, true);
|
||||
}
|
||||
}
|
||||
|
@@ -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',
|
||||
|
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
|
||||
|
@@ -225,7 +225,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();
|
||||
}
|
||||
|
@@ -200,7 +200,7 @@ trait MetaCollection
|
||||
|
||||
$this->joinMetaDataTables();
|
||||
$this->query->where('journal_meta.name', '=', 'internal_reference');
|
||||
$this->query->where('journal_meta.data', 'NOT LIKE', sprintf('%%%s%%', $internalReference));
|
||||
$this->query->whereNotLike('journal_meta.data', sprintf('%%%s%%', $internalReference));
|
||||
|
||||
return $this;
|
||||
}
|
||||
@@ -221,7 +221,7 @@ trait MetaCollection
|
||||
|
||||
$this->joinMetaDataTables();
|
||||
$this->query->where('journal_meta.name', '=', 'external_id');
|
||||
$this->query->where('journal_meta.data', 'LIKE', sprintf('%%%s%%', $externalId));
|
||||
$this->query->whereLike('journal_meta.data', sprintf('%%%s%%', $externalId));
|
||||
|
||||
return $this;
|
||||
}
|
||||
@@ -233,7 +233,7 @@ trait MetaCollection
|
||||
|
||||
$this->joinMetaDataTables();
|
||||
$this->query->where('journal_meta.name', '=', 'external_id');
|
||||
$this->query->where('journal_meta.data', 'NOT LIKE', sprintf('%%%s%%', $externalId));
|
||||
$this->query->whereNotLike('journal_meta.data', sprintf('%%%s%%', $externalId));
|
||||
|
||||
return $this;
|
||||
}
|
||||
@@ -245,7 +245,7 @@ trait MetaCollection
|
||||
|
||||
$this->joinMetaDataTables();
|
||||
$this->query->where('journal_meta.name', '=', 'external_id');
|
||||
$this->query->where('journal_meta.data', 'NOT LIKE', sprintf('%%%s"', $externalId));
|
||||
$this->query->whereNotLike('journal_meta.data', sprintf('%%%s"', $externalId));
|
||||
|
||||
return $this;
|
||||
}
|
||||
@@ -257,7 +257,7 @@ trait MetaCollection
|
||||
|
||||
$this->joinMetaDataTables();
|
||||
$this->query->where('journal_meta.name', '=', 'external_id');
|
||||
$this->query->where('journal_meta.data', 'LIKE', sprintf('"%s%%', $externalId));
|
||||
$this->query->whereLike('journal_meta.data', sprintf('"%s%%', $externalId));
|
||||
|
||||
return $this;
|
||||
}
|
||||
@@ -269,7 +269,7 @@ trait MetaCollection
|
||||
|
||||
$this->joinMetaDataTables();
|
||||
$this->query->where('journal_meta.name', '=', 'external_id');
|
||||
$this->query->where('journal_meta.data', 'LIKE', sprintf('%%%s"', $externalId));
|
||||
$this->query->whereLike('journal_meta.data', sprintf('%%%s"', $externalId));
|
||||
|
||||
return $this;
|
||||
}
|
||||
@@ -281,7 +281,7 @@ trait MetaCollection
|
||||
|
||||
$this->joinMetaDataTables();
|
||||
$this->query->where('journal_meta.name', '=', 'external_id');
|
||||
$this->query->where('journal_meta.data', 'LIKE', sprintf('"%s%%', $externalId));
|
||||
$this->query->whereLike('journal_meta.data', sprintf('"%s%%', $externalId));
|
||||
|
||||
return $this;
|
||||
}
|
||||
@@ -292,7 +292,7 @@ trait MetaCollection
|
||||
$url = (string)json_encode($url);
|
||||
$url = str_replace('\\', '\\\\', trim($url, '"'));
|
||||
$this->query->where('journal_meta.name', '=', 'external_url');
|
||||
$this->query->where('journal_meta.data', 'LIKE', sprintf('%%%s%%', $url));
|
||||
$this->query->whereLike('journal_meta.data', sprintf('%%%s%%', $url));
|
||||
|
||||
return $this;
|
||||
}
|
||||
@@ -303,7 +303,7 @@ trait MetaCollection
|
||||
$url = (string)json_encode($url);
|
||||
$url = str_replace('\\', '\\\\', trim($url, '"'));
|
||||
$this->query->where('journal_meta.name', '=', 'external_url');
|
||||
$this->query->where('journal_meta.data', 'NOT LIKE', sprintf('%%%s%%', $url));
|
||||
$this->query->whereNotLike('journal_meta.data', sprintf('%%%s%%', $url));
|
||||
|
||||
return $this;
|
||||
}
|
||||
@@ -314,7 +314,7 @@ trait MetaCollection
|
||||
$url = (string)json_encode($url);
|
||||
$url = str_replace('\\', '\\\\', ltrim($url, '"'));
|
||||
$this->query->where('journal_meta.name', '=', 'external_url');
|
||||
$this->query->where('journal_meta.data', 'NOT LIKE', sprintf('%%%s', $url));
|
||||
$this->query->whereNotLike('journal_meta.data', sprintf('%%%s', $url));
|
||||
|
||||
return $this;
|
||||
}
|
||||
@@ -327,7 +327,7 @@ trait MetaCollection
|
||||
// var_dump($url);
|
||||
|
||||
$this->query->where('journal_meta.name', '=', 'external_url');
|
||||
$this->query->where('journal_meta.data', 'NOT LIKE', sprintf('%s%%', $url));
|
||||
$this->query->whereNotLike('journal_meta.data', sprintf('%s%%', $url));
|
||||
|
||||
return $this;
|
||||
}
|
||||
@@ -338,7 +338,7 @@ trait MetaCollection
|
||||
$url = (string)json_encode($url);
|
||||
$url = str_replace('\\', '\\\\', ltrim($url, '"'));
|
||||
$this->query->where('journal_meta.name', '=', 'external_url');
|
||||
$this->query->where('journal_meta.data', 'LIKE', sprintf('%%%s', $url));
|
||||
$this->query->whereLike('journal_meta.data', sprintf('%%%s', $url));
|
||||
|
||||
return $this;
|
||||
}
|
||||
@@ -351,7 +351,7 @@ trait MetaCollection
|
||||
// var_dump($url);
|
||||
|
||||
$this->query->where('journal_meta.name', '=', 'external_url');
|
||||
$this->query->where('journal_meta.data', 'LIKE', sprintf('%s%%', $url));
|
||||
$this->query->whereLike('journal_meta.data', sprintf('%s%%', $url));
|
||||
|
||||
return $this;
|
||||
}
|
||||
@@ -404,7 +404,7 @@ trait MetaCollection
|
||||
|
||||
$this->joinMetaDataTables();
|
||||
$this->query->where('journal_meta.name', '=', 'internal_reference');
|
||||
$this->query->where('journal_meta.data', 'LIKE', sprintf('%%%s%%', $internalReference));
|
||||
$this->query->whereLike('journal_meta.data', sprintf('%%%s%%', $internalReference));
|
||||
|
||||
return $this;
|
||||
}
|
||||
@@ -416,7 +416,7 @@ trait MetaCollection
|
||||
|
||||
$this->joinMetaDataTables();
|
||||
$this->query->where('journal_meta.name', '=', 'internal_reference');
|
||||
$this->query->where('journal_meta.data', 'NOT LIKE', sprintf('%%%s%%', $internalReference));
|
||||
$this->query->whereNotLike('journal_meta.data', sprintf('%%%s%%', $internalReference));
|
||||
|
||||
return $this;
|
||||
}
|
||||
@@ -428,7 +428,7 @@ trait MetaCollection
|
||||
|
||||
$this->joinMetaDataTables();
|
||||
$this->query->where('journal_meta.name', '=', 'internal_reference');
|
||||
$this->query->where('journal_meta.data', 'NOT LIKE', sprintf('%%%s"', $internalReference));
|
||||
$this->query->whereNotLike('journal_meta.data', sprintf('%%%s"', $internalReference));
|
||||
|
||||
return $this;
|
||||
}
|
||||
@@ -440,7 +440,7 @@ trait MetaCollection
|
||||
|
||||
$this->joinMetaDataTables();
|
||||
$this->query->where('journal_meta.name', '=', 'internal_reference');
|
||||
$this->query->where('journal_meta.data', 'LIKE', sprintf('"%s%%', $internalReference));
|
||||
$this->query->whereLike('journal_meta.data', sprintf('"%s%%', $internalReference));
|
||||
|
||||
return $this;
|
||||
}
|
||||
@@ -452,7 +452,7 @@ trait MetaCollection
|
||||
|
||||
$this->joinMetaDataTables();
|
||||
$this->query->where('journal_meta.name', '=', 'internal_reference');
|
||||
$this->query->where('journal_meta.data', 'LIKE', sprintf('%%%s"', $internalReference));
|
||||
$this->query->whereLike('journal_meta.data', sprintf('%%%s"', $internalReference));
|
||||
|
||||
return $this;
|
||||
}
|
||||
@@ -464,7 +464,7 @@ trait MetaCollection
|
||||
|
||||
$this->joinMetaDataTables();
|
||||
$this->query->where('journal_meta.name', '=', 'internal_reference');
|
||||
$this->query->where('journal_meta.data', 'LIKE', sprintf('"%s%%', $internalReference));
|
||||
$this->query->whereLike('journal_meta.data', sprintf('"%s%%', $internalReference));
|
||||
|
||||
return $this;
|
||||
}
|
||||
@@ -472,7 +472,7 @@ trait MetaCollection
|
||||
public function notesContain(string $value): GroupCollectorInterface
|
||||
{
|
||||
$this->withNotes();
|
||||
$this->query->where('notes.text', 'LIKE', sprintf('%%%s%%', $value));
|
||||
$this->query->whereLike('notes.text', sprintf('%%%s%%', $value));
|
||||
|
||||
return $this;
|
||||
}
|
||||
@@ -502,7 +502,7 @@ trait MetaCollection
|
||||
$this->withNotes();
|
||||
$this->query->where(static function (Builder $q) use ($value): void { // @phpstan-ignore-line
|
||||
$q->whereNull('notes.text');
|
||||
$q->orWhere('notes.text', 'NOT LIKE', sprintf('%%%s%%', $value));
|
||||
$q->orWhereNotLike('notes.text', sprintf('%%%s%%', $value));
|
||||
});
|
||||
|
||||
return $this;
|
||||
@@ -513,7 +513,7 @@ trait MetaCollection
|
||||
$this->withNotes();
|
||||
$this->query->where(static function (Builder $q) use ($value): void { // @phpstan-ignore-line
|
||||
$q->whereNull('notes.text');
|
||||
$q->orWhere('notes.text', 'NOT LIKE', sprintf('%%%s', $value));
|
||||
$q->orWhereNotLike('notes.text', sprintf('%%%s', $value));
|
||||
});
|
||||
|
||||
return $this;
|
||||
@@ -524,7 +524,7 @@ trait MetaCollection
|
||||
$this->withNotes();
|
||||
$this->query->where(static function (Builder $q) use ($value): void { // @phpstan-ignore-line
|
||||
$q->whereNull('notes.text');
|
||||
$q->orWhere('notes.text', 'NOT LIKE', sprintf('%s%%', $value));
|
||||
$q->orWhereNotLike('notes.text', sprintf('%s%%', $value));
|
||||
});
|
||||
|
||||
return $this;
|
||||
@@ -533,7 +533,7 @@ trait MetaCollection
|
||||
public function notesEndWith(string $value): GroupCollectorInterface
|
||||
{
|
||||
$this->withNotes();
|
||||
$this->query->where('notes.text', 'LIKE', sprintf('%%%s', $value));
|
||||
$this->query->whereLike('notes.text', sprintf('%%%s', $value));
|
||||
|
||||
return $this;
|
||||
}
|
||||
@@ -560,7 +560,7 @@ trait MetaCollection
|
||||
public function notesStartWith(string $value): GroupCollectorInterface
|
||||
{
|
||||
$this->withNotes();
|
||||
$this->query->where('notes.text', 'LIKE', sprintf('%s%%', $value));
|
||||
$this->query->whereLike('notes.text', sprintf('%s%%', $value));
|
||||
|
||||
return $this;
|
||||
}
|
||||
@@ -717,7 +717,7 @@ trait MetaCollection
|
||||
|
||||
$this->joinMetaDataTables();
|
||||
$this->query->where('journal_meta.name', '=', 'internal_reference');
|
||||
$this->query->where('journal_meta.data', '=', $internalReference);
|
||||
$this->query->where('journal_meta.data', '=', sprintf('%s', json_encode($internalReference)));
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
@@ -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.');
|
||||
}
|
||||
}
|
||||
|
@@ -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);
|
||||
|
||||
|
@@ -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();
|
||||
|
||||
|
56
app/Http/Requests/ExistingTokenFormRequest.php
Normal file
56
app/Http/Requests/ExistingTokenFormRequest.php
Normal file
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* TokenFormRequest.php
|
||||
* Copyright (c) 2019 james@firefly-iii.org
|
||||
*
|
||||
* This file is part of Firefly III (https://github.com/firefly-iii).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace FireflyIII\Http\Requests;
|
||||
|
||||
use FireflyIII\Support\Request\ChecksLogin;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Validation\Validator;
|
||||
|
||||
/**
|
||||
* Class ExistingTokenFormRequest.
|
||||
*/
|
||||
class ExistingTokenFormRequest extends FormRequest
|
||||
{
|
||||
use ChecksLogin;
|
||||
|
||||
/**
|
||||
* Rules for this request.
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
// fixed
|
||||
return [
|
||||
'password' => 'required|currentPassword',
|
||||
'code' => 'required|existingMfaCode',
|
||||
];
|
||||
}
|
||||
|
||||
public function withValidator(Validator $validator): void
|
||||
{
|
||||
if ($validator->fails()) {
|
||||
Log::channel('audit')->error(sprintf('Validation errors in %s', __CLASS__), $validator->errors()->toArray());
|
||||
}
|
||||
}
|
||||
}
|
@@ -42,7 +42,8 @@ class TokenFormRequest extends FormRequest
|
||||
{
|
||||
// fixed
|
||||
return [
|
||||
'code' => 'required|2faCode',
|
||||
'password' => 'required|currentPassword',
|
||||
'code' => 'required|2faCode',
|
||||
];
|
||||
}
|
||||
|
||||
|
@@ -177,10 +177,11 @@ class CreateRecurringTransactions implements ShouldQueue
|
||||
// has repeated X times.
|
||||
$journalCount = $this->repository->getJournalCount($recurrence);
|
||||
if (0 !== $recurrence->repetitions && $journalCount >= $recurrence->repetitions && false === $this->force) {
|
||||
app('log')->info(sprintf('Recurrence #%d has run %d times, so will run no longer.', $recurrence->id, $recurrence->repetitions));
|
||||
app('log')->info(sprintf('Recurrence #%d has run %d times, so will run no longer.', $recurrence->id, $journalCount));
|
||||
|
||||
return false;
|
||||
}
|
||||
app('log')->debug(sprintf('Recurrence #%d has run %d times, max is %d times.', $recurrence->id, $journalCount, $recurrence->repetitions));
|
||||
|
||||
// is no longer running
|
||||
if ($this->repeatUntilHasPassed($recurrence)) {
|
||||
@@ -202,8 +203,8 @@ class CreateRecurringTransactions implements ShouldQueue
|
||||
sprintf(
|
||||
'Recurrence #%d is set to run on %s, and today\'s date is %s. Skipped.',
|
||||
$recurrence->id,
|
||||
$recurrence->first_date->format('Y-m-d'),
|
||||
$this->date->format('Y-m-d')
|
||||
$recurrence->first_date->format('Y-m-d H:i:s'),
|
||||
$this->date->format('Y-m-d H:i:s')
|
||||
)
|
||||
);
|
||||
|
||||
@@ -244,9 +245,10 @@ class CreateRecurringTransactions implements ShouldQueue
|
||||
private function hasNotStartedYet(Recurrence $recurrence): bool
|
||||
{
|
||||
$startDate = $this->getStartDate($recurrence);
|
||||
app('log')->debug(sprintf('Start date is %s', $startDate->format('Y-m-d')));
|
||||
app('log')->debug(sprintf('Start date is %s', $startDate->format('Y-m-d H:i:s')));
|
||||
app('log')->debug(sprintf('Ask date is %s', $this->date->format('Y-m-d H:i:s')));
|
||||
|
||||
return $startDate->gt($this->date);
|
||||
return $startDate->gte($this->date);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -341,7 +343,7 @@ class CreateRecurringTransactions implements ShouldQueue
|
||||
app('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')));
|
||||
|
||||
@@ -359,14 +361,12 @@ class CreateRecurringTransactions implements ShouldQueue
|
||||
}
|
||||
|
||||
// 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;
|
||||
@@ -378,14 +378,14 @@ class CreateRecurringTransactions implements ShouldQueue
|
||||
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));
|
||||
|
||||
@@ -394,7 +394,8 @@ class CreateRecurringTransactions implements ShouldQueue
|
||||
$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;
|
||||
|
@@ -28,8 +28,6 @@ class AccountSchema extends Schema
|
||||
*/
|
||||
public function fields(): array
|
||||
{
|
||||
Log::debug(__METHOD__);
|
||||
|
||||
return [
|
||||
ID::make(),
|
||||
Attribute::make('created_at'),
|
||||
|
@@ -14,7 +14,7 @@ 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'];
|
||||
|
||||
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;
|
||||
@@ -53,6 +54,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(
|
||||
|
@@ -51,7 +51,7 @@ class AvailableBudget extends Model
|
||||
'transaction_currency_id' => 'int',
|
||||
];
|
||||
|
||||
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).
|
||||
|
@@ -68,6 +68,7 @@ class Bill extends Model
|
||||
'user_group_id',
|
||||
'amount_max',
|
||||
'date',
|
||||
'date_tz',
|
||||
'repeat_freq',
|
||||
'skip',
|
||||
'automatch',
|
||||
@@ -75,6 +76,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'];
|
||||
|
@@ -54,7 +54,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).
|
||||
|
@@ -49,7 +49,7 @@ class CurrencyExchangeRate extends Model
|
||||
'to_currency_id' => 'int',
|
||||
'date' => 'datetime',
|
||||
];
|
||||
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
|
||||
{
|
||||
|
@@ -44,7 +44,7 @@ class InvitedUser extends Model
|
||||
'expires' => 'datetime',
|
||||
'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).
|
||||
|
@@ -53,7 +53,7 @@ class PiggyBank extends Model
|
||||
'encrypted' => 'boolean',
|
||||
];
|
||||
|
||||
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'];
|
||||
|
||||
|
@@ -42,7 +42,7 @@ class PiggyBankEvent extends Model
|
||||
'date' => 'date',
|
||||
];
|
||||
|
||||
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'];
|
||||
|
||||
|
@@ -45,7 +45,7 @@ class PiggyBankRepetition extends Model
|
||||
'targetdate' => 'date',
|
||||
];
|
||||
|
||||
protected $fillable = ['piggy_bank_id', 'startdate', 'targetdate', 'currentamount'];
|
||||
protected $fillable = ['piggy_bank_id', 'startdate', 'startdate_tz', 'targetdate', 'targetdate_tz', 'currentamount'];
|
||||
|
||||
public function piggyBank(): BelongsTo
|
||||
{
|
||||
|
@@ -60,7 +60,7 @@ class Recurrence extends Model
|
||||
];
|
||||
|
||||
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;
|
||||
@@ -58,6 +59,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);
|
||||
|
@@ -53,7 +53,7 @@ class Tag extends Model
|
||||
'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'];
|
||||
|
||||
|
@@ -37,6 +37,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphMany;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
/**
|
||||
@@ -76,6 +77,7 @@ class TransactionJournal extends Model
|
||||
'completed',
|
||||
'order',
|
||||
'date',
|
||||
'date_tz',
|
||||
];
|
||||
|
||||
protected $hidden = ['encrypted'];
|
||||
@@ -88,7 +90,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 +169,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 +244,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;
|
||||
@@ -53,6 +54,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,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
@@ -197,7 +197,7 @@ class JournalRepository implements JournalRepositoryInterface
|
||||
->orderBy('date', 'DESC')
|
||||
;
|
||||
if ('' !== $search) {
|
||||
$query->where('description', 'LIKE', sprintf('%%%s%%', $search));
|
||||
$query->whereLike('description', sprintf('%%%s%%', $search));
|
||||
}
|
||||
|
||||
return $query->take($limit)->get();
|
||||
|
@@ -120,7 +120,7 @@ class ObjectGroupRepository implements ObjectGroupRepositoryInterface
|
||||
$parts = explode(' ', $query);
|
||||
foreach ($parts as $part) {
|
||||
$search = sprintf('%%%s%%', $part);
|
||||
$dbQuery->where('title', 'LIKE', $search);
|
||||
$dbQuery->whereLike('title', $search);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -178,12 +178,12 @@ trait ModifiesPiggyBanks
|
||||
*/
|
||||
public function store(array $data): PiggyBank
|
||||
{
|
||||
$order = $this->getMaxOrder() + 1;
|
||||
$order = $this->getMaxOrder() + 1;
|
||||
if (array_key_exists('order', $data)) {
|
||||
$order = $data['order'];
|
||||
}
|
||||
$data['order'] = 31337; // very high when creating.
|
||||
$piggyData = $data;
|
||||
$data['order'] = 31337; // very high when creating.
|
||||
$piggyData = $data;
|
||||
// unset fields just in case.
|
||||
unset($piggyData['object_group_title'], $piggyData['object_group_id'], $piggyData['notes'], $piggyData['current_amount']);
|
||||
|
||||
@@ -192,6 +192,9 @@ trait ModifiesPiggyBanks
|
||||
$piggyData['targetamount'] = '0';
|
||||
}
|
||||
|
||||
$piggyData['startdate_tz'] = $piggyData['startdate']?->format('e');
|
||||
$piggyData['targetdate_tz'] = $piggyData['targetdate']?->format('e');
|
||||
|
||||
try {
|
||||
/** @var PiggyBank $piggyBank */
|
||||
$piggyBank = PiggyBank::create($piggyData);
|
||||
@@ -208,13 +211,13 @@ trait ModifiesPiggyBanks
|
||||
$this->updateNote($piggyBank, $data['notes']);
|
||||
|
||||
// repetition is auto created.
|
||||
$repetition = $this->getRepetition($piggyBank);
|
||||
$repetition = $this->getRepetition($piggyBank);
|
||||
if (null !== $repetition && array_key_exists('current_amount', $data) && '' !== $data['current_amount']) {
|
||||
$repetition->currentamount = $data['current_amount'];
|
||||
$repetition->save();
|
||||
}
|
||||
|
||||
$objectGroupTitle = $data['object_group_title'] ?? '';
|
||||
$objectGroupTitle = $data['object_group_title'] ?? '';
|
||||
if ('' !== $objectGroupTitle) {
|
||||
$objectGroup = $this->findOrCreateObjectGroup($objectGroupTitle);
|
||||
if (null !== $objectGroup) {
|
||||
@@ -223,7 +226,7 @@ trait ModifiesPiggyBanks
|
||||
}
|
||||
}
|
||||
// try also with ID
|
||||
$objectGroupId = (int)($data['object_group_id'] ?? 0);
|
||||
$objectGroupId = (int)($data['object_group_id'] ?? 0);
|
||||
if (0 !== $objectGroupId) {
|
||||
$objectGroup = $this->findObjectGroupById($objectGroupId);
|
||||
if (null !== $objectGroup) {
|
||||
@@ -373,10 +376,12 @@ trait ModifiesPiggyBanks
|
||||
$piggyBank->targetamount = '0';
|
||||
}
|
||||
if (array_key_exists('targetdate', $data) && '' !== $data['targetdate']) {
|
||||
$piggyBank->targetdate = $data['targetdate'];
|
||||
$piggyBank->targetdate = $data['targetdate'];
|
||||
$piggyBank->targetdate_tz = $data['targetdate']->format('e');
|
||||
}
|
||||
if (array_key_exists('startdate', $data)) {
|
||||
$piggyBank->startdate = $data['startdate'];
|
||||
$piggyBank->startdate = $data['startdate'];
|
||||
$piggyBank->startdate_tz = $data['targetdate']->format('e');
|
||||
}
|
||||
$piggyBank->save();
|
||||
|
||||
|
@@ -343,7 +343,7 @@ class PiggyBankRepository implements PiggyBankRepositoryInterface
|
||||
{
|
||||
$search = $this->user->piggyBanks();
|
||||
if ('' !== $query) {
|
||||
$search->where('piggy_banks.name', 'LIKE', sprintf('%%%s%%', $query));
|
||||
$search->whereLike('piggy_banks.name', sprintf('%%%s%%', $query));
|
||||
}
|
||||
$search->orderBy('piggy_banks.order', 'ASC')
|
||||
->orderBy('piggy_banks.name', 'ASC')
|
||||
|
@@ -67,13 +67,13 @@ class RecurringRepository implements RecurringRepositoryInterface
|
||||
$set
|
||||
= TransactionJournalMeta::where(static function (Builder $q1) use ($recurrence): void {
|
||||
$q1->where('name', 'recurrence_id');
|
||||
$q1->where('data', json_encode((string)$recurrence->id));
|
||||
$q1->where('data', json_encode((string) $recurrence->id));
|
||||
})->get(['journal_meta.transaction_journal_id']);
|
||||
|
||||
// there are X journals made for this recurrence. Any of them meant for today?
|
||||
foreach ($set as $journalMeta) {
|
||||
$count = TransactionJournalMeta::where(static function (Builder $q2) use ($date): void {
|
||||
$string = (string)$date;
|
||||
$string = (string) $date;
|
||||
app('log')->debug(sprintf('Search for date: %s', json_encode($string)));
|
||||
$q2->where('name', 'recurrence_date');
|
||||
$q2->where('data', json_encode($string));
|
||||
@@ -141,7 +141,7 @@ class RecurringRepository implements RecurringRepositoryInterface
|
||||
/** @var RecurrenceTransactionMeta $meta */
|
||||
foreach ($recTransaction->recurrenceTransactionMeta as $meta) {
|
||||
if ('bill_id' === $meta->name) {
|
||||
$return = (int)$meta->value;
|
||||
$return = (int) $meta->value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,7 +158,7 @@ class RecurringRepository implements RecurringRepositoryInterface
|
||||
/** @var RecurrenceTransactionMeta $meta */
|
||||
foreach ($recTransaction->recurrenceTransactionMeta as $meta) {
|
||||
if ('budget_id' === $meta->name) {
|
||||
$return = (int)$meta->value;
|
||||
$return = (int) $meta->value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,7 +175,7 @@ class RecurringRepository implements RecurringRepositoryInterface
|
||||
/** @var RecurrenceTransactionMeta $meta */
|
||||
foreach ($recTransaction->recurrenceTransactionMeta as $meta) {
|
||||
if ('category_id' === $meta->name) {
|
||||
$return = (int)$meta->value;
|
||||
$return = (int) $meta->value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,7 +192,7 @@ class RecurringRepository implements RecurringRepositoryInterface
|
||||
/** @var RecurrenceTransactionMeta $meta */
|
||||
foreach ($recTransaction->recurrenceTransactionMeta as $meta) {
|
||||
if ('category_name' === $meta->name) {
|
||||
$return = (string)$meta->value;
|
||||
$return = (string) $meta->value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,6 +204,7 @@ class RecurringRepository implements RecurringRepositoryInterface
|
||||
*/
|
||||
public function getJournalCount(Recurrence $recurrence, ?Carbon $start = null, ?Carbon $end = null): int
|
||||
{
|
||||
Log::debug(sprintf('Now in getJournalCount(#%d, "%s", "%s")', $recurrence->id, $start?->format('Y-m-d H:i:s'), $end?->format('Y-m-d H:i:s')));
|
||||
$query = TransactionJournal::leftJoin('journal_meta', 'journal_meta.transaction_journal_id', '=', 'transaction_journals.id')
|
||||
->where('transaction_journals.user_id', $recurrence->user_id)
|
||||
->whereNull('transaction_journals.deleted_at')
|
||||
@@ -216,8 +217,10 @@ class RecurringRepository implements RecurringRepositoryInterface
|
||||
if (null !== $end) {
|
||||
$query->where('transaction_journals.date', '<=', $end->format('Y-m-d 00:00:00'));
|
||||
}
|
||||
$count = $query->count('transaction_journals.id');
|
||||
Log::debug(sprintf('Count is %d', $count));
|
||||
|
||||
return $query->count('transaction_journals.id');
|
||||
return $count;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -228,7 +231,7 @@ class RecurringRepository implements RecurringRepositoryInterface
|
||||
return TransactionJournalMeta::leftJoin('transaction_journals', 'transaction_journals.id', '=', 'journal_meta.transaction_journal_id')
|
||||
->where('transaction_journals.user_id', $this->user->id)
|
||||
->where('journal_meta.name', '=', 'recurrence_id')
|
||||
->where('journal_meta.data', '=', json_encode((string)$recurrence->id))
|
||||
->where('journal_meta.data', '=', json_encode((string) $recurrence->id))
|
||||
->get(['journal_meta.transaction_journal_id'])->pluck('transaction_journal_id')->toArray()
|
||||
;
|
||||
}
|
||||
@@ -241,7 +244,7 @@ class RecurringRepository implements RecurringRepositoryInterface
|
||||
/** @var null|Note $note */
|
||||
$note = $recurrence->notes()->first();
|
||||
|
||||
return (string)$note?->text;
|
||||
return (string) $note?->text;
|
||||
}
|
||||
|
||||
public function getPiggyBank(RecurrenceTransaction $transaction): ?int
|
||||
@@ -251,7 +254,7 @@ class RecurringRepository implements RecurringRepositoryInterface
|
||||
/** @var RecurrenceTransactionMeta $metaEntry */
|
||||
foreach ($meta as $metaEntry) {
|
||||
if ('piggy_bank_id' === $metaEntry->name) {
|
||||
return (int)$metaEntry->value;
|
||||
return (int) $metaEntry->value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -281,12 +284,12 @@ class RecurringRepository implements RecurringRepositoryInterface
|
||||
->whereNull('transaction_journals.deleted_at')
|
||||
->where('transaction_journals.user_id', $this->user->id)
|
||||
->where('name', 'recurrence_id')
|
||||
->where('data', json_encode((string)$recurrence->id))
|
||||
->where('data', json_encode((string) $recurrence->id))
|
||||
->get()->pluck('transaction_journal_id')->toArray()
|
||||
;
|
||||
$search = [];
|
||||
foreach ($journalMeta as $journalId) {
|
||||
$search[] = (int)$journalId;
|
||||
$search[] = (int) $journalId;
|
||||
}
|
||||
|
||||
/** @var GroupCollectorInterface $collector */
|
||||
@@ -314,13 +317,13 @@ class RecurringRepository implements RecurringRepositoryInterface
|
||||
->whereNull('transaction_journals.deleted_at')
|
||||
->where('transaction_journals.user_id', $this->user->id)
|
||||
->where('name', 'recurrence_id')
|
||||
->where('data', json_encode((string)$recurrence->id))
|
||||
->where('data', json_encode((string) $recurrence->id))
|
||||
->get()->pluck('transaction_journal_id')->toArray()
|
||||
;
|
||||
$search = [];
|
||||
|
||||
foreach ($journalMeta as $journalId) {
|
||||
$search[] = (int)$journalId;
|
||||
$search[] = (int) $journalId;
|
||||
}
|
||||
if (0 === count($search)) {
|
||||
return new Collection();
|
||||
@@ -442,28 +445,28 @@ class RecurringRepository implements RecurringRepositoryInterface
|
||||
if (is_array($language)) {
|
||||
$language = 'en_US';
|
||||
}
|
||||
$language = (string)$language;
|
||||
$language = (string) $language;
|
||||
if ('daily' === $repetition->repetition_type) {
|
||||
return (string)trans('firefly.recurring_daily', [], $language);
|
||||
return (string) trans('firefly.recurring_daily', [], $language);
|
||||
}
|
||||
if ('weekly' === $repetition->repetition_type) {
|
||||
$dayOfWeek = trans(sprintf('config.dow_%s', $repetition->repetition_moment), [], $language);
|
||||
if ($repetition->repetition_skip > 0) {
|
||||
return (string)trans('firefly.recurring_weekly_skip', ['weekday' => $dayOfWeek, 'skip' => $repetition->repetition_skip + 1], $language);
|
||||
return (string) trans('firefly.recurring_weekly_skip', ['weekday' => $dayOfWeek, 'skip' => $repetition->repetition_skip + 1], $language);
|
||||
}
|
||||
|
||||
return (string)trans('firefly.recurring_weekly', ['weekday' => $dayOfWeek], $language);
|
||||
return (string) trans('firefly.recurring_weekly', ['weekday' => $dayOfWeek], $language);
|
||||
}
|
||||
if ('monthly' === $repetition->repetition_type) {
|
||||
if ($repetition->repetition_skip > 0) {
|
||||
return (string)trans(
|
||||
return (string) trans(
|
||||
'firefly.recurring_monthly_skip',
|
||||
['dayOfMonth' => $repetition->repetition_moment, 'skip' => $repetition->repetition_skip + 1],
|
||||
$language
|
||||
);
|
||||
}
|
||||
|
||||
return (string)trans(
|
||||
return (string) trans(
|
||||
'firefly.recurring_monthly',
|
||||
['dayOfMonth' => $repetition->repetition_moment, 'skip' => $repetition->repetition_skip - 1],
|
||||
$language
|
||||
@@ -474,7 +477,7 @@ class RecurringRepository implements RecurringRepositoryInterface
|
||||
// first part is number of week, second is weekday.
|
||||
$dayOfWeek = trans(sprintf('config.dow_%s', $parts[1]), [], $language);
|
||||
|
||||
return (string)trans('firefly.recurring_ndom', ['weekday' => $dayOfWeek, 'dayOfMonth' => $parts[0]], $language);
|
||||
return (string) trans('firefly.recurring_ndom', ['weekday' => $dayOfWeek, 'dayOfMonth' => $parts[0]], $language);
|
||||
}
|
||||
if ('yearly' === $repetition->repetition_type) {
|
||||
$today = today(config('app.timezone'))->endOfYear();
|
||||
@@ -482,11 +485,11 @@ class RecurringRepository implements RecurringRepositoryInterface
|
||||
if (null === $repDate) {
|
||||
$repDate = clone $today;
|
||||
}
|
||||
$diffInYears = (int)$today->diffInYears($repDate, true);
|
||||
$diffInYears = (int) $today->diffInYears($repDate, true);
|
||||
$repDate->addYears($diffInYears); // technically not necessary.
|
||||
$string = $repDate->isoFormat((string)trans('config.month_and_day_no_year_js'));
|
||||
$string = $repDate->isoFormat((string) trans('config.month_and_day_no_year_js'));
|
||||
|
||||
return (string)trans('firefly.recurring_yearly', ['date' => $string], $language);
|
||||
return (string) trans('firefly.recurring_yearly', ['date' => $string], $language);
|
||||
}
|
||||
|
||||
return '';
|
||||
@@ -496,7 +499,7 @@ class RecurringRepository implements RecurringRepositoryInterface
|
||||
{
|
||||
$search = $this->user->recurrences();
|
||||
if ('' !== $query) {
|
||||
$search->where('recurrences.title', 'LIKE', sprintf('%%%s%%', $query));
|
||||
$search->whereLike('recurrences.title', sprintf('%%%s%%', $query));
|
||||
}
|
||||
$search
|
||||
->orderBy('recurrences.title', 'ASC')
|
||||
@@ -520,16 +523,16 @@ class RecurringRepository implements RecurringRepositoryInterface
|
||||
public function totalTransactions(Recurrence $recurrence, RecurrenceRepetition $repetition): int
|
||||
{
|
||||
// if repeat = null just return 0.
|
||||
if (null === $recurrence->repeat_until && 0 === (int)$recurrence->repetitions) {
|
||||
if (null === $recurrence->repeat_until && 0 === (int) $recurrence->repetitions) {
|
||||
return 0;
|
||||
}
|
||||
// expect X transactions then stop. Return that number
|
||||
if (null === $recurrence->repeat_until && 0 !== (int)$recurrence->repetitions) {
|
||||
return (int)$recurrence->repetitions;
|
||||
if (null === $recurrence->repeat_until && 0 !== (int) $recurrence->repetitions) {
|
||||
return (int) $recurrence->repetitions;
|
||||
}
|
||||
|
||||
// need to calculate, this depends on the repetition:
|
||||
if (null !== $recurrence->repeat_until && 0 === (int)$recurrence->repetitions) {
|
||||
if (null !== $recurrence->repeat_until && 0 === (int) $recurrence->repetitions) {
|
||||
$occurrences = $this->getOccurrencesInRange($repetition, $recurrence->first_date ?? today(), $recurrence->repeat_until);
|
||||
|
||||
return count($occurrences);
|
||||
|
@@ -213,7 +213,7 @@ class RuleRepository implements RuleRepositoryInterface
|
||||
{
|
||||
$search = $this->user->rules();
|
||||
if ('' !== $query) {
|
||||
$search->where('rules.title', 'LIKE', sprintf('%%%s%%', $query));
|
||||
$search->whereLike('rules.title', sprintf('%%%s%%', $query));
|
||||
}
|
||||
$search->orderBy('rules.order', 'ASC')
|
||||
->orderBy('rules.title', 'ASC')
|
||||
|
@@ -371,7 +371,7 @@ class RuleGroupRepository implements RuleGroupRepositoryInterface
|
||||
{
|
||||
$search = $this->user->ruleGroups();
|
||||
if ('' !== $query) {
|
||||
$search->where('rule_groups.title', 'LIKE', sprintf('%%%s%%', $query));
|
||||
$search->whereLike('rule_groups.title', sprintf('%%%s%%', $query));
|
||||
}
|
||||
$search->orderBy('rule_groups.order', 'ASC')
|
||||
->orderBy('rule_groups.title', 'ASC')
|
||||
|
@@ -205,7 +205,7 @@ class TagRepository implements TagRepositoryInterface
|
||||
{
|
||||
$search = sprintf('%%%s%%', $query);
|
||||
|
||||
return $this->user->tags()->where('tag', 'LIKE', $search)->get(['tags.*']);
|
||||
return $this->user->tags()->whereLike('tag', $search)->get(['tags.*']);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -217,7 +217,7 @@ class TagRepository implements TagRepositoryInterface
|
||||
$tags = $this->user->tags()->orderBy('tag', 'ASC');
|
||||
if ('' !== $query) {
|
||||
$search = sprintf('%%%s%%', $query);
|
||||
$tags->where('tag', 'LIKE', $search);
|
||||
$tags->whereLike('tag', $search);
|
||||
}
|
||||
|
||||
return $tags->take($limit)->get('tags.*');
|
||||
@@ -309,14 +309,14 @@ class TagRepository implements TagRepositoryInterface
|
||||
{
|
||||
$search = sprintf('%%%s', $query);
|
||||
|
||||
return $this->user->tags()->where('tag', 'LIKE', $search)->get(['tags.*']);
|
||||
return $this->user->tags()->whereLike('tag', $search)->get(['tags.*']);
|
||||
}
|
||||
|
||||
public function tagStartsWith(string $query): Collection
|
||||
{
|
||||
$search = sprintf('%s%%', $query);
|
||||
|
||||
return $this->user->tags()->where('tag', 'LIKE', $search)->get(['tags.*']);
|
||||
return $this->user->tags()->whereLike('tag', $search)->get(['tags.*']);
|
||||
}
|
||||
|
||||
public function transferredInPeriod(Tag $tag, Carbon $start, Carbon $end): array
|
||||
|
@@ -62,6 +62,6 @@ class TransactionTypeRepository implements TransactionTypeRepositoryInterface
|
||||
return TransactionType::get();
|
||||
}
|
||||
|
||||
return TransactionType::where('type', 'LIKE', sprintf('%%%s%%', $query))->take($limit)->get();
|
||||
return TransactionType::whereLike('type', sprintf('%%%s%%', $query))->take($limit)->get();
|
||||
}
|
||||
}
|
||||
|
@@ -276,6 +276,7 @@ class UserRepository implements UserRepositoryInterface
|
||||
$invitee->email = $email;
|
||||
$invitee->redeemed = false;
|
||||
$invitee->expires = $now;
|
||||
$invitee->expires_tz = $now->format('e');
|
||||
$invitee->save();
|
||||
|
||||
return $invitee;
|
||||
|
@@ -270,7 +270,7 @@ class AccountRepository implements AccountRepositoryInterface
|
||||
$query->where('accounts.active', $value);
|
||||
}
|
||||
if ('name' === $column) {
|
||||
$query->where('accounts.name', 'LIKE', sprintf('%%%s%%', $value));
|
||||
$query->whereLike('accounts.name', sprintf('%%%s%%', $value));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -298,34 +298,38 @@ class AccountRepository implements AccountRepositoryInterface
|
||||
return $query->get(['accounts.*']);
|
||||
}
|
||||
|
||||
public function searchAccount(array $query, array $types, int $limit): Collection
|
||||
public function searchAccount(string $query, array $types, int $page, int $limit): Collection
|
||||
{
|
||||
// search by group, not by user
|
||||
$dbQuery = $this->userGroup->accounts()
|
||||
->where('active', true)
|
||||
->orderBy('accounts.updated_at', 'ASC')
|
||||
->orderBy('accounts.order', 'ASC')
|
||||
->orderBy('accounts.account_type_id', 'ASC')
|
||||
->orderBy('accounts.name', 'ASC')
|
||||
->with(['accountType'])
|
||||
;
|
||||
if (count($query) > 0) {
|
||||
// split query on spaces just in case:
|
||||
|
||||
// split query on spaces just in case:
|
||||
if ('' !== trim($query)) {
|
||||
$dbQuery->where(function (EloquentBuilder $q) use ($query): void {
|
||||
foreach ($query as $line) {
|
||||
$parts = explode(' ', $line);
|
||||
foreach ($parts as $part) {
|
||||
$search = sprintf('%%%s%%', $part);
|
||||
$q->orWhere('name', 'LIKE', $search);
|
||||
}
|
||||
$parts = explode(' ', $query);
|
||||
foreach ($parts as $part) {
|
||||
$search = sprintf('%%%s%%', $part);
|
||||
$q->orWhereLike('name', $search);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (0 !== count($types)) {
|
||||
$dbQuery->leftJoin('account_types', 'accounts.account_type_id', '=', 'account_types.id');
|
||||
$dbQuery->whereIn('account_types.type', $types);
|
||||
}
|
||||
|
||||
return $dbQuery->take($limit)->get(['accounts.*']);
|
||||
$dbQuery->skip(($page - 1) * $limit)->take($limit);
|
||||
|
||||
return $dbQuery->get(['accounts.*']);
|
||||
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
|
@@ -80,7 +80,7 @@ interface AccountRepositoryInterface
|
||||
*/
|
||||
public function resetAccountOrder(): void;
|
||||
|
||||
public function searchAccount(array $query, array $types, int $limit): Collection;
|
||||
public function searchAccount(string $query, array $types, int $page, int $limit): Collection;
|
||||
|
||||
public function setUser(User $user): void;
|
||||
|
||||
|
@@ -41,7 +41,7 @@ class CategoryRepository implements CategoryRepositoryInterface
|
||||
$parts = explode(' ', $line);
|
||||
foreach ($parts as $part) {
|
||||
$search = sprintf('%%%s%%', $part);
|
||||
$q->orWhere('name', 'LIKE', $search);
|
||||
$q->orWhereLike('name', $search);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@@ -320,7 +320,7 @@ class CurrencyRepository implements CurrencyRepositoryInterface
|
||||
{
|
||||
$query = TransactionCurrency::where('enabled', true);
|
||||
if ('' !== $search) {
|
||||
$query->where('name', 'LIKE', sprintf('%%%s%%', $search));
|
||||
$query->whereLike('name', sprintf('%%%s%%', $search));
|
||||
}
|
||||
|
||||
return $query->take($limit)->get();
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user